As most of you know I have a regular(ish) gig talking on the Top Traders Unplugged systematic investor podcast, every month or so with Niels Kaastrup-Larsen and Moritz Seibert.
Anyway on the most recent episode we got chatting about whether open or closed equity should matter when trading a position. More broadly, should your history of trading a position affect how you trade it now, or is it only what's happened in the market that matters?
Moritz and I had a bit of a debate about this; I'm a big fan of running my system on a 'stateless' basis where the only thing that matters is the market price. My logic is that the market does not know what my position is or has been, or how much profit I've made. That means if I'm using a stop loss, the size of the stop loss will remain the same regardless of what's happened to my p&l since I opened the trade.
Moritz on the other hand, seemed to imply that you should change your trading tactics depending on how the position has played out. The basic idea is that initially you should have pretty tight stops, and once you've made a decent profit you should increase the stop so that the position can 'breath'. Then you have a better chance of hitting a home run if the trade lasts a long time, without being stopped out early when you've just made a profit. These are dynamic stop losses, that adjust throughout the life of a trade.
I followed this up with a twitter thread where I clarified my thinking and got some interesting feedback. I also promised to do some more research. This blogpost is that research. But it's not just about that.
That's because this idea is closely related to another perpetual bone of contention polite discussion between myself and Moritz, which is whether positions and stop losses should be adjusted as volatility changes. I like dynamic vol control: adjusting position sizes as vol changes. His preference is for no adjustment, for the same reason that if the market is getting riskier you've got a better chance of having a big up trade.
What these two things have in common is that, intuitively at least, the 'purer' trend following tactic (no dynamic vol, but dynamic stop losses) should lead to more positive skew.
I would like to check that intution, and also see if this is an example of 'the no free lunch effect' (whereby you can only get better skew by giving up Sharpe Ratio). In plain english, what effect do these two changes have on Sharpe Ratio and skew? Then at least we can make an informed decision based on our preferences.
Discrete and continuous trading systems
- Something happens ('entry rule')
- We open a trade
- (Optionally) we make adjustments to the trade
- Something else happens ('exit rule'). A common exit rule is a stop loss.
- We close the trade
- We calculate an optimal position that we want to take
- We compare it to the position we currently have
- We adjust to get to our optimal position by trading
Code for the starter system
- use a single MAV rule with a binary forecast
- replace the positionSize stage with something that:
- calculates a 'preliminary position' which is just the binary position scaled for vol
- adjusts this preliminary position using the function stoploss to create discrete trades
def stoploss(price, vol, raw_position, dynamic_vol=False, dynamic_SL = False):
"""
assert all(vol.index == price.index)
assert all(price.index == raw_position.index)
# assume all lined up
simple_system_position = simpleSysystemPosition(
dynamic_vol=dynamic_vol,
dynamic_SL=dynamic_SL)
new_position_list = []
for iday in range(len(price)):
current_price = price[iday]
current_vol = vol[iday]
if simple_system_position.no_current_position:
# no position, check for signal
original_position_now = raw_position[iday]
new_position = simple_system_position.no_position_check_for_trade(original_position_now,
current_price, current_vol)
else:
new_position = simple_system_position.position_on_check_for_close(
current_price, current_vol)
new_position_list.append(new_position)
new_position_df = pd.DataFrame(new_position_list, raw_position.index)
return new_position_df
def no_position_check_for_trade(self, original_position_now, current_price, current_vol):
assert self.no_current_position
if np.isnan(original_position_now):
# no signal
return 0.0
if original_position_now ==0.0:
return 0.0
# potentially going long / short
# check last position to avoid whipsaw
if self.previous_position != 0.0:
# same way round avoid whipsaw
if sign(
original_position_now) == sign(self.previous_position):
return 0.0
self.initialise_trade(original_position_now, current_price, current_vol)
return original_position_now
def position_on_check_for_close(self, current_price, current_vol):
assert not self.no_current_position
self.update_price_series(current_price)
new_position = self.vol_adjusted_position(current_vol)
time_to_close_trade =self.check_if_hit_stoploss(current_vol)
if time_to_close_trade:
self.close_trade()
return new_position
def check_if_hit_stoploss(self, current_vol):
stoploss_gap = self.stoploss_gap(current_vol)
sign_position = sign(self.current_position)
if sign_position == 1:
# long
time_to_close_trade = self.check_if_long_stop_hit(stoploss_gap)
else:
# short
time_to_close_trade = self.check_if_short_stop_hit(stoploss_gap)
return time_to_close_trade
def check_if_long_stop_hit(self, stoploss_gap):
threshold = self.hwm - stoploss_gap
time_to_close_trade = self.current_price < threshold
return time_to_close_trade
def check_if_short_stop_hit(self, stoploss_gap):
threshold = self.hwm + stoploss_gap
time_to_close_trade = self.current_price > threshold
return time_to_close_trade
Dynamic vol control
So what is 'dynamic vol control'? Basically it's adjusting open positions as vol changes.
(I'm assuming that we always set our initial position according to the vol when the trade is opened. I explore the consequences of not doing that here.)
World has got riskier? Then your position should be smaller. Things chilled out? Bigger position is called for.
Note, and this is really important, if you're going to adjust your vol you must also adjust your stop loss. Since I set stop loss initially at 0.5xannual standard deviation, if vol doubles then the stop loss gap will double (get wider), if it halves then the gap will also half (get tighter).
Why is this so important? Well, suppose you halve your position, but don't widen your stop loss gap. Your risk on the trade is going to be too large; and you'll end up getting stopped out prematurely. Basically the stop loss and the postion size need to stay in synch, as I discussed in the series of posts that begins here.
Here's the relevant code from our uber-class, simpleSystemPosition:
def vol_adjusted_position(self, current_vol):
initial_position = self.initial_position
if self.dynamic_vol:
vol_adjusted_position = (self.initial_vol / current_vol) * initial_position
return vol_adjusted_position
else:
return initial_position
def stoploss_gap(self, current_vol):
xfactor = self.Xfactor
if self.dynamic_vol:
vol = current_vol
else:
vol = self.initial_vol
stoploss_gap = vol * xfactor
return stoploss_gap
Xfactor will be 8 here, because we use a 0.5x annual standard deviation stop loss to close positions and vol here is daily, so the multiple becomes 16 (square root of 256~approx # of trading days per year) multiplied by 0.5 = 8
(Of course we'll allow Xfactor to vary when we use dynamic stop losses)
Dynamically adjusting stop loss for p&l
@property
def Xfactor(self):
if self.dynamic_SL:
return self.dynamic_xfactor()
else:
return fixed_xfactor()
def dynamic_xfactor(self):
pandl_vol_units = self.vol_adjusted_profit_since_trade_points()
return dynamic_xfactor(pandl_vol_units)
# outside the class
def fixed_xfactor():
return 8.0
def dynamic_xfactor(pandl_vol_units):
MINIMUM_XFACTOR = 2.0
MAXIMUM_XFACTOR = 8.0
PANDL_UPPER_CUTOFF = 8.0
PANDL_LOWER_CUTOFF = 0.0
if pandl_vol_units<=PANDL_LOWER_CUTOFF:
return MINIMUM_XFACTOR
elif pandl_vol_units>PANDL_UPPER_CUTOFF:
return MAXIMUM_XFACTOR
else:
return MINIMUM_XFACTOR + (pandl_vol_units)*(MAXIMUM_XFACTOR - MINIMUM_XFACTOR)/(PANDL_UPPER_CUTOFF - PANDL_LOWER_CUTOFF)
PS: A stateless way of letting positions breath
Something that occured to me, but I didn't test, is that you can implement a dynamic stop without having to calculate previous p&l. For example you could measure the length or strength of a trend, thus creating something that was consistent with my conviction that 'the market doesn't know what my profit is, or when I put a trade on'.
If you put a gun to my head and said I had to do a dynamic stop loss, then this is how I'd do it.
Evaluating the results
instrument_code = "EDOLLAR"
pandl_returns_capital = pandl_capital(instrument_code, system, method_used="returns")
pandl_trades_capital = pandl_capital(instrument_code, system, method_used="trades")
pandl_returns_capital.hist(bins=100)
(The plot has some extremes removed for both tails).As I discussed at some length in this post from last year, the skew of daily returns p&l will often be slightly negative except perhaps for very fast systems. Here the skew is -0.24. Now what about trades?
pandl_trades_capital.hist(bins=30)
Wow! That is some seriously positive skew: 2.84 (I've cutoff the plot at the right tail). That is what we'd expect, because we're trend followers. - Sharpe Ratio (based on daily returns and annualised)
- Skew (based on daily returns)
- Skew (weekly returns)
- Skew (monthly returns)
- Skew (using trades)
Results
Summary
This post has confirmed my intuition:
- Adding dynamic vol control reduces positive skew, but adds Sharpe. The effect is starkest for trade by trade p&l, and for monthly returns.
- Adding dynamic stop losses increases positive skew, but drastically reduces SR. The skew bonus is very high for trade p&l, but relatively modest for weekly and monthly returns.
Now to be fair, I don't know exactly what other trend followers do in their trading system, since unlike me many of them don't have the freedom to open source it*. So I don't know if these tests accurately reflect what 'purer' trend followers are doing, especially when it comes to dynamic stop loss (dynamic vol control is less contentious since there is only really one way of doing it: you could change the measurement of vol, frequency of changes and the use of buffering; none of which will affect the results very much).
* 'Yes I know I'm charging you 2 and 20 for my black box, and I know I put the entire thing on github, but information want's to be free dammn it!'
It's most likely that they are running a milder version of what I've used, something that doesn't affect the SR anywhere near as much as the dynamic stop loss I outlined here. However, this will also lead to a smaller skew bonus: the no free lunch hypothesis is confirmed again, you can't buy more skew without selling SR. You can probably test this by changing the parameters of the dynamic skew function.
But the point of this post isn't to say 'this is the perfect way'. It's to show you the possible trade offs. What you will do will depend on your preferences for SR and skew. You could whip out Occams' razor and say you should run the simplest system, which has very good skew properties at weekly and monthly returns. Or you could take the view (like moi) that the extra SR of dynamic vol control is worth the complexity and reduction in skew. Or you could go hell bent for skew, and do dynamic stop losses (but not dynamic vol).
The only thing that doesn't make sense is doing both! That's ideologically inconsistent, over complicated, and also pretty crap.
Frankly, it's up to you. To me the most interesting thing for me about this exercise has been the contrast between the skew of trade p&l, and return p&l. You can be doing something that massively bumps up your positive skew on trade p&l (like very aggressive dynamic stop loss adjustment), and think you are being a 'purer' trend follower, and then when you look at your monthly return you see there is no meaningful skew effect :-(