Should You Cut Your Losses?

Every successful trader is disciplined,
this is a quote that trader/investor had heard for too many times.

Every successful trade should have a plan to enter, plan to exit and cut loss, cut loss is a risk management technique,
it can theoretically prevent your losses from getting worse.

Today we are going to see if cut loss is a good strategy to protect your trade.
Common cut loss values are 5%, 10%,
but it is highly dependent on you risk preference,
there is no magic number for cut loss value, for this study we are using 5% cut loss value.

Reading Data

I downloaded historical data of KLSE from Yahoo Finance in csv form.

We need to read it into memory to perform backtesting.

import pandas as pd

KLSE = pd.read_csv('KLSE.csv')
KLSE = KLSE.replace(',','', regex=True)
KLSE['Open'] = KLSE['Open'].astype(float)
KLSE['Close'] = KLSE['Close'].astype(float)
KLSE['Change'] = (KLSE['Close'] - KLSE['Close'].shift(1))/KLSE['Open'] + 1

KLSE.head()
DateOpenHighLowCloseAdj CloseVolumeChange
04-Jan-101272.311275.751272.251275.751275.7556508200NaN
15-Jan-101278.261290.551278.261288.241288.241366466001.009771
26-Jan-101288.861296.441288.021293.171293.171177403001.003825
37-Jan-101293.691299.701290.361291.421291.421150244000.998647
48-Jan-101294.931295.511290.861292.981292.98745872001.001205

Rules

We are using very simple moving average + 5% stop loss as our strategy,
the rules are:

  1. when the “fast” moving average goes above the “slow” moving average, we buy in.
  2. when the “fast” moving average goes below the “fast” moving average, we sell.
  3. Cut loss when paper loss > 5%.
  4. No trading cost.
def simulate(df,fast,slow,cutloss = False):
    
    import talib

    df['fast'] = talib.SMA(df['Close'],fast)
    df['slow'] = talib.SMA(df['Close'],slow)
    
    df.dropna(inplace = True)

    gold_cross = df[df['fast'] > df['slow']].index
    df.loc[gold_cross,'Cross'] = 1

    gold_cross = df[df['fast'] < df['slow']].index
    df.loc[gold_cross,'Cross'] = 0

    df['Buy'] = df['Cross'].diff()

    df['Return'] = df['Cross']*df['Change']

    def norm(x):
        if x == 0:
            return 1
        else:
            return x

    df['Return'] = df['Return'].apply(lambda x: norm(x))
    df['Nav'] = (df['Return']).cumprod()

    DD = 1 - df['Nav']/df['Nav'].cummax()
    
    #===================================
    if cutloss:
        # set cutloss to 5.5% for extra buffer
        c = DD[DD > 0.055].index
        df.loc[c,'Cross'] = 0

        df['Buy'] = df['Cross'].diff()

        df['Return'] = df['Cross']*df['Change']

        df['Return'] = df['Return'].apply(lambda x: norm(x))
        df['Nav'] = (df['Return']).cumprod()

    #====================================
    
    # divide by 252 because generally a year has 252 trading days
    num_periods = df.shape[0]/252
    rety = ((df['Nav'].iloc[-1] / df['Nav'].iloc[0]) ** (1 / (num_periods - 1)) - 1)*100.0
    
    price_in = df.loc[df['Buy'] == 1,'Close'].values
    price_out = df.loc[df['Buy'] == -1,'Close'].values
    
    if len(price_out) > len(price_in):
        price_out = price_out[:len(price_in)]

    if len(price_in) > len(price_out):
        price_in = price_in[:len(price_out)]

    VictoryRatio = ((price_out - price_in)>0).mean()*100.0
    DD = 1 - df['Nav']/df['Nav'].cummax()
    MDD = max(DD)*100.0
    
    return df, round(rety, 2), round(VictoryRatio, 2), round(MDD,2)

Strategy Performance

We are backtesting 4 combinations

  1. 10 and 20 (Short term trade), No cut loss
  2. 20 and 50 (Medium term trade), No cut loss
  3. 10 and 20 (Short term trade), 5% cut loss
  4. 20 and 50 (Medium term trade), 5% cut loss
MA1020,cagr1020,vr1020,mdd1020 = simulate(KLSE.copy(),10,20,False)
MA2050,cagr2050,vr2050,mdd2050 = simulate(KLSE.copy(),20,50,False)
MA1020_cutloss,cagr1020_cutloss,vr1020_cutloss,mdd1020_cutloss = simulate(KLSE.copy(),10,20,True)
MA2050_cutloss,cagr2050_cutloss,vr2050_cutloss,mdd2050_cutloss = simulate(KLSE.copy(),20,50,True)

KLSE['KLSE'] = (KLSE['Change']).cumprod()
import matplotlib.pyplot as plt
plt.style.use('seaborn')

ax = MA1020['Nav'].plot(figsize=(10, 6))
MA2050['Nav'].plot(ax=ax)
MA1020_cutloss['Nav'].plot(ax=ax)
MA2050_cutloss['Nav'].plot(ax=ax)

KLSE['KLSE'].plot(ax=ax)
# plt.plot( 'Date','Nav', data = MA2050, marker='', color='olive', linewidth=2)
ax.legend(['MA1020','MA2050','MA1020 5% Cutloss','MA2050 5% Cutloss','KLSE']);
plt.show()


from prettytable import PrettyTable

t = PrettyTable(['Strategy', 'CAGR', 'Win Rate', 'Max Drawdown'])
t.add_row(['MA1020', cagr1020,vr1020,mdd1020])
t.add_row(['MA2050', cagr2050,vr2050,mdd2050])
t.add_row(['MA1020 5% Cutloss', cagr1020_cutloss,vr1020_cutloss,mdd1020_cutloss])
t.add_row(['MA2050 5% Cutloss', cagr2050_cutloss,vr2050_cutloss,mdd2050_cutloss])

print(t)
+-------------------+------+----------+--------------+
|      Strategy     | CAGR | Win Rate | Max Drawdown |
+-------------------+------+----------+--------------+
|       MA1020      | 6.44 |  50.77   |     8.52     |
|       MA2050      | 3.14 |  30.77   |    12.15     |
| MA1020 5% Cutloss | 7.19 |  45.21   |     5.44     |
| MA2050 5% Cutloss | 4.54 |  20.59   |     5.46     |
+-------------------+------+----------+--------------+

Thoughts

From the backtesting results,
cut loss seems to be a very effective risk management technique.

With 5% cut loss, it performed better than the same strategy without cut loss.

Although the win rate is lower,
this could mean that having cut loss,
you will realize your lossing trade more frequently
which is good as it prevents the trade from going south further.

If you know coding,
you can try this out on other stocks that you like by getting csv data from Yahoo Finance.

This isn’t any trading advice and the studies and analysis done were for educational and sharing purposes only

Leave a Reply

Your email address will not be published.