Trend following is often the easiest in a matter of both getting started with algo-trading and being profitable. In the previous article, I wrote a trend following strategy and even boosted its performance by using a bigger timeframe for determining the trend. But it was a simple enter-at-once and exit-at-once strategy.
The point of this tutorial is to get you started with a few of Jesse's features that help you write most of the trend following strategies you'll find. Few of such features are:
First I create a new strategy with the
jesse make-strategy TrendFollowingStrategy
And I edit my
routes.py file to use this strategy, and I set the timeframe to
# trading routes routes = [ ('Bitfinex', 'BTC-USD', '4h', 'TrendFollowingStrategy'), ] # in case your strategy required extra candles, timeframes, ... extra_candles = 
Then I open the newly created strategy file and it looks like this:
from jesse.strategies import Strategy import jesse.indicators as ta from jesse import utils class TrendFollowingStrategy(Strategy): def should_long(self) -> bool: return False def should_short(self) -> bool: return False def should_cancel(self) -> bool: return False def go_long(self): pass def go_short(self): pass
I want to go long when:
def should_long(self) -> bool: return self.current_candle_touches_long_ema and self.trend == 1 def should_short(self) -> bool: return self.current_candle_touches_long_ema and self.trend == -1
They say an image is worth a 1000 words, so here's an image showing what I count as an uptrend in this case:
And here is what I mean by the current candle touching the 50 EMA (the orange line is the 50 EMA):
Now let's write the code for
@property def trend(self): short_ema = ta.ema(self.candles, 50) long_ema = ta.ema(self.candles, 100) longer_ema = ta.ema(self.candles, 200) if short_ema > long_ema > longer_ema: return 1 elif short_ema < long_ema < longer_ema: return -1 else: return 0 @property def current_candle_touches_long_ema(self): long_ema = ta.ema(self.candles, 50) return self.high >= long_ema >= self.low
I used three EMA lines to determine the trend direction. I return
1 for an uptrend, and
-1 for a downtrend.
current_candle_touches_long_ema is pretty simple, I just have to make sure the current candle's high price is bigger than the
long_ema line (which is an EMA with the period of 50) and that the current candle's low price is lower than the
The entry price is going to be the high of the current candle for long trades. The stop-loss price will be 3 times the current ATR away from my entry price.
In this strategy, I want to enter trades all at once, but exit at two points. I will exit first half of my position at the previous high of the trend. Here is what I mean by the high of the uptrend (the blue line is my target):
To code this, I first select the high prices of the last 20 candle bars. And then simply return the maximum of them.
For position sizing, I want to risk 5% of my total capital per each trade. To calculate the
qty I use the
And of course, the opposite goes for short trades. Here's the code:
def go_long(self): entry = self.high stop = entry - ta.atr(self.candles)*3 qty = utils.risk_to_qty(self.capital, 5, entry, stop, self.fee_rate) # highest price of the last 20 bars last_20_highs = self.candles[-20:, 3] previous_high = np.max(last_20_highs) self.buy = qty, entry self.stop_loss = qty, stop self.take_profit = qty/2, previous_high def go_short(self): entry = self.low stop = entry + ta.atr(self.candles) * 3 qty = utils.risk_to_qty(self.capital, 5, entry, stop, self.fee_rate) # lowest price of the last 20 bars last_20_lows = self.candles[-20:, 4] previous_low = np.min(last_20_lows) self.sell = qty, entry self.stop_loss = qty, stop self.take_profit = qty / 2, previous_low
As you can see, I'm only exiting half of my position size at the take-profit price. In other words, after my position is reduced, I want to move my stop price to break-even. To write the code for it in Jesse, I'll use the pre builtin on_reduced_position method.
self.stop_loss is all I needed to do to tell jesse update my stop-loss order. Jesse automatically picks up on it, cancels the previous stop order, and submits a new one. It couldn't get any easier!
def on_reduced_position(self): self.stop_loss = self.position.qty, self.position.entry_price
To learn more about events and see a list of all available event methods in Jesse, make sure to read its documentation.
For this strategy, I intend to exit the second half of my position in a dynamic situation. The idea behind this situation is to exit when the price is highly overbought and about to take a serious correction. To say it in quant's language, I want to exit when the RSI indicator reaches above 80.
First I'll use the builtin
update_position() method to write my logic in. This method gets executed after every new candle only if we have an open position. Hence, it is used to update a position. Which means we don't need to check if the position is open or not.
The next thing to consider here is that I want to exit only the second half of my position. In other words, I want to liquidate the open position if it has been reduced. The easiest way to check if my position has been reduced is by using the builtin reduced_count property and the liquidate() method.
def update_position(self): # the RSI logic is intended for the second half of the trade if self.reduced_count > 0: rsi = ta.rsi(self.candles) if self.is_long and rsi > 80: # self.liquidate() closes the position with a market order self.liquidate() elif self.is_short and rsi < 20: self.liquidate()
My strategy looks good so far, let's run a backtest and see how it goes:
jesse backtest 2019-01-01 2020-05-01
After around 4% percent of the backtest I get an error:
Uncaught Exception: InvalidStrategy: take-profit(3601.6) must be below entry-price(3601.6) in a short position
The error is trying to tell us at some point our strategy's take-profit and entry prices are equal ($3601.6) which isn't acceptable. This is a tricky issue to debug, but you'll get good at debugging with Jesse after writing your first few strategies.
To explain why this issue is happening, we need to take another look at the take-profit and entry:
def go_long(self): # entry: the high of the current candle entry = self.high stop = entry - ta.atr(self.candles)*3 qty = utils.risk_to_qty(self.capital, 5, entry, stop, self.fee_rate) last_20_highs = self.candles[-20:, 3] previous_high = np.max(last_20_highs) self.buy = qty, entry self.stop_loss = qty, stop # (first) take-profit: the high of the last 20 candles self.take_profit = qty/2, previous_high
The error told us that the entry and the take-profit are the same at some points. This means, at that point, the current candle's high is the highest of the last 20 bars. Which isn't the type of trade we had in mind for this strategy.
We could prevent this from happening with a few dirty if-else statements in our
should_long method, or by using a filter which is designed specificity for these kinds of cases.
A filter is just a function that returns a boolean value. A filter is passed by returning a
True value, and vice versa. I define a filter and name it
reward_to_risk_filter. The name could be anything, but it is a generally good practice to either start or end the name of a filter method with the word
filter. This filter's job is to make sure that the trade we're trying to enter is worth it.
def reward_to_risk_filter(self): profit = abs(self.average_entry_price - self.average_take_profit) loss = abs(self.average_entry_price - self.average_stop_loss) win_loss_ratio = profit / loss return win_loss_ratio > 1
At this point, Jesse still doesn't know that
reward_to_risk_filter() is a filter. To make it recognize my filter, I need to add it to the
filters() method which is a pre builtin method that returns a Python list:
def filters(self): return [ ]
Now I'm going to add
reward_to_risk_filter inside the returning list as a variable. What that means is that no parentheses must be present at the end of it:
def filters(self): return [ self.reward_to_risk_filter, ]
Now let's execute the backtest one more time:
jesse backtest 2019-01-01 2020-05-01
This time everything goes smoothly.
The simpler your can keep your strategies, the easier it will be to debug them, and even improve over the time. Writing strategies with Jesse is as simple as trading your strategies manually. So next time you find a strategy introduced in a trading book, or by a trading guru, just write the code for it, and backtest it.