The code snippet in this article will show you how to size a position so that if your stop loss is hit, the impact will result in a fixed % drawdown of your account cash. In other words, we are sizing a position based on a stop loss. For example, we size our position so that we will lose 20% of our available cash when the stop-loss is only 5% away from price.
It is worth noting that this technique mostly requires the use of leverage. This is because we often need to have such large size that it would cost more than the total cash available in our account. As such, this technique is more commonly used in leveraged markets such as Forex.
Please Note: This post does not take into account available margin and free margin requirements that are needed to open and maintain leveraged positions. For more information on margin in Forex markets see: Backtrader: Oanda Margin and Leverage
Before We Start
The code in this post will be executed on test data specifically created for verifying our code is correct. You can obtain a copy of the test data here: Stop Loss Position Sizing Test Data
The test data contains a short set of daily candles. It is all we need to run the tests. To start, the data will open and close at 100 USD. It will maintain these same prices for 10 days. The reason for this is that it will allow us to enter at exactly 100 USD (because we like easy mathematics!). It will then drop to 90 for another 10 days before dropping to 50 for the final 10 days. This should provide a nice range of prices for testing long stop losses.
The Magic Formula
The key formula we need to size the position is this:
SIZE = $ AMOUNT TO RISK / (ENTRY PRICE – TARGET EXIT PRICE)
So for example, you might want to risk 10% of $10000. In this case, the dollar amount of risk would be $1000. Now let’s say you want to buy an asset for $100 and have a stop loss at $95. Pushing the numbers through the formula would result in:
- SIZE = $1000 / (100 – 95)
- SIZE = $1000 / 5
- SIZE = 200
Let’s verify the formula is correct. Assuming we bought the asset for $100, the total cost/value of our investment is $100 x 200 which equals $20,000. Side Note: Now we can see why this technique requires a leveraged account. We only started with $10,000! If the price then drops to $95, the value of our investment is worth $95 * 200, which equals $19,000. Assuming we exit at this price it would result in a loss of $1000 which is 10% of our account.
Of course, this is an ideal example. We may not always get filled at our desired level due to gapping or slippage. Additionally, we will choose to round down when the numbers do not divide so nicely. Therefore, we should not expect a perfect drawdown on every trade but we should at least be very close!
Now, I hope you paid attention to the figures in that example because we are going to recreate exactly the same trade in the code!
Code: Sizing a Position Based on a Stop Loss
''' Author: www.backtest-rookies.com MIT License Copyright (c) 2018 backtest-rookies.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ''' import backtrader as bt from datetime import datetime import math class TestStrategy(bt.Strategy): params = (('risk', 0.1), #risk 10% ('stop_dist', 0.05)) #stoploss distance 5% def next(self): date = self.data.datetime.date() bar = len(self) cash = self.broker.get_cash() if bar == 7: stop_price = (self.data.close[0] * (1 - self.p.stop_dist)) qty = math.floor((cash * self.p.risk) / (self.data.close[0] - stop_price)) self.buy(size=qty) self.sell(exectype=bt.Order.Stop, size=qty, price=stop_price) def notify_trade(self, trade): date = self.data.datetime.datetime() if trade.isclosed: print('-'*32,' NOTIFY TRADE ','-'*32) print('{}, Avg Price: {}, Profit, Gross {}, Net {}'.format( date, trade.price, round(trade.pnl,2), round(trade.pnlcomm,2))) print('-'*80) startcash = 10000 #Create an instance of cerebro cerebro = bt.Cerebro() #Add our strategy cerebro.addstrategy(TestStrategy) #Set commission cerebro.broker.setcommission(leverage=20) # Create a Data Feed data = bt.feeds.GenericCSVData( timeframe=bt.TimeFrame.Days, compression=1, dataname='data/PositionSizingTestData.csv', nullvalue=0.0, dtformat=('%m/%d/%Y'), datetime=0, time=-1, high=2, low=3, open=1, close=4, volume=-1, openinterest=-1 #-1 means not used ) # Add the data cerebro.adddata(data) # Set our desired cash start cerebro.broker.setcash(startcash) # Run over everything cerebro.run() #Get final portfolio Value portvalue = cerebro.broker.getvalue() pnl = portvalue - startcash #Print out the final result print('Final Portfolio Value: ${}'.format(portvalue)) print('P/L: ${}'.format(pnl)) print('P/L: {}%'.format(((portvalue - startcash)/startcash)*100)) #Finally plot the end results cerebro.plot(style='candlestick')
Code Commentary
This post is not a beginners post. Therefore, the commentary will not cover every part in details. If you are new to Backtrader, you can check out the getting started series.
In all honesty, there is not much to write home about on this strategy. In fact, there are only 3 parts really worth mentioning!
- Don’t forget to change the data path! i.e
dataname='data/PositionSizingTestData.csv'
. You will likely have a different folder structure or saved it under a different name. - We set our leverage during the setup of cerebro. This will ensure we can open the position in the trade. If you want to tweak the amount of leverage used, look for the line
cerebro.broker.setcommission(leverage=50)
- Our formula for determining the
qty
looks slightly different to the example formula at the beginning of the post.qty = math.floor((cash * self.p.risk) / (self.data.close[0] - stop_price))
. However, it is the same. Here we are just calculating the dollar amount of risk by multiplying our cash by the percentage we want to risk. We place this in brackets()
and it is calculated first before dividing it. We use theclose
price as our assumed entry level. We know we will get filled at this level because we are using test data but in practice, the price may gap a little between theclose
and theopen
of the next bar when you get filled. Unfortunately, we don’t have a magic 8 ball to help us make the calculation perfect!
References:
- Backtrader: Commission Schemes Documentation (including setting leverage)
Results
Running the script you should see a chart that looks like this:
With the following output:
-------------------------------- NOTIFY TRADE -------------------------------- 2018-01-11 23:59:59.999989, Avg Price: 100.0, Profit, Gross -1000.0, Net -1000.0 -------------------------------------------------------------------------------- Final Portfolio Value: $9000.0 P/L: $-1000.0 P/L: -10.0%
You will notice that the amount lost is exactly 10% of the account. You can go ahead and experiment with different risk and stop loss levels. To do this just tweak the parameters at the start of the strategy accordingly.
params = (('risk', 0.1), #risk 10% ('stop_dist', 0.05)) #stoploss distance 5%
And that is pretty much all there is to it. If you find any issues or have suggestions, please feel free to voice them in the comments below!
Find This Post Useful?
If this post saved you time and effort, please consider support the site! There are many ways to support us and some won’t even cost you a penny.
Backtest Rookies is a registered with Brave publisher!
Brave users can drop us a tip.
Alternatively, support us by switching to Brave using this referral link and we will receive some BAT!
Referral Link
Enjoying the content and thinking of subscribing to Tradingview? Support this site by clicking the referral link before you sign up!
3HxNVyh5729ieTfPnTybrtK7J7QxJD9gjD
0x9a2f88198224d59e5749bacfc23d79507da3d431
M8iUBnb8KamHR18gpgi2sSG7jopddFJi8S
Hi,
I tried the implementation with this approach, but I think self.data.close[0] is not the order executed price. How to connect the stop loss to the order executed price?
Looking at the docs https://www.backtrader.com/blog/posts/2018-02-01-stop-trading/stop-trading/#automating-the-approach:
Should the buy order now be placed with transmit=False and the sell stop be placed with parent=thebuyorder so the sell stop awaits the buy order?
self.buy_order = self.buy(size=qty, transmit=False)
self.sell(exectype=bt.Order.Stop, size=qty, price=stop_price, parent, self.buy_order)
Also what about the cancel step?
if self.buy_order: # something was pending
self.cancel(self.buy_order)
Thanks!
Wouldn’t it be better if this can be coded into a sizer ?