Stateful Long-Short with Exits¶
This template shows you how to use the quantiacs exits library to implement conditional exits in your strategy and evaluate weights on a day-by-day basis using the multipass backtester.
First, we can start by importing all the needed libraries.
# Import basic libraries.
import xarray as xr
import pandas as pd
import numpy as np
import datetime
import os
import pickle
# Import Quantiacs libraries.
import qnt.data as qndata # load and manipulate data
import qnt.output as qnout # manage output
import qnt.backtester as qnbt # backtester
import qnt.stats as qnstats # statistical functions for analysis
import qnt.graph as qngraph # graphical tools
import qnt.ta as qnta # indicators library
import qnt.xr_talib as xr_talib # indicators library
import qnt.state as qnstate #state functions
import qnt.exits as qnte # exit functions
Using position exits in the trading strategy¶
The trading strategy below explains how to incorporate exit algorithms. You will find an overview of the strategy and step-by-step instructions to integrate take profit, stop loss and day counter effectively.
The strategy function uses several technical indicators to generate long and short signals. Here's a brief overview of the key components:
Strategy Logic¶
long_signal: Generated when the 40-day SMA crosses above the 200-day SMA.
long_signal_2: Generated when a green candlestick is 4.5 times bigger than the average candlestick in the last 100 days.
short_signal: Generated when 120-day RSI is above 65 (indicating a long time of overperformance and a likely retracement).
exit1: Closes the position when the last close is under the 70-day SMA
exit2: Closes the position after the price drops over 5% in 1 day
In this approach we will add all the signals up. This way, multiple long or short signals appearing on the same day can amplify or cancel each other out. In this case the exits are made to exit long positions, so we will multiply them with long signals. These exits are a part of our signal, and we will further filter it with exits that depend on our position.
Position Sizing¶
Positions are sized based on the ATR percentage weights = (entry/atr_perc)
in the code. This is a risk management method that invests more in stable periods of low volatility and less in high volatility periods instead of dividing the weights evenly across assets.
Adding Exit Signals:¶
take_profit functions determine whether positions should be exited based on the take profit criteria. Use
take_profit_long_atr
ortake_profit_long_percentage
to apply the stops to long positions, and alsotake_profit_short_atr
ortake_profit_short_percentage
to apply the stops to short positions. The atr_amount parameter in ATR functions determines how many ATRs the price needs to move upwards (long) or downwards (short) from the opening price in order for the position to be closed. ATR functions also take last day's ATR value as a parameter (last_atr). In percentage functions the percent parameter determins how many percentage points the price has to move before the stop is triggered (i.e. if percent = 5 then the trade will be closed after a 5% change)stop_loss functions determine whether positions should be exited based on the stop loss criteria. Use
stop_loss_long_atr
orstop_loss_long_percentage
to apply the stops to long positions, and alsostop_loss_short_atr
orstop_loss_short_percentage
to apply the stops to short positions. The atr_amount parameter in ATR functions determines how many ATRs the price needs to move downwards (long) or upwards (short) from the opening price in order for the position to be closed. ATR functions also take last day's ATR value as a parameter (last_atr). In percentage functions the percent parameter determins how many percentage points the price has to move before the stop is triggered (i.e. if percent = 5 then the trade will be closed after a 5% change)max_hold functions determine whether positions should be exited based on period parameter. The threshold parameter determines how many bars since entry need to pass to exit the position. Use
max_hold_long
ormax_hold_short
to apply it to long or short positions.
The weights are updated by multiplying them with all the exit signals (tp, sl, dc). This effectively exits positions if any of the exit conditions are met.
State Management¶
The state is updated with the new weights and written back to ensure persistence across function calls. In this implementation, positions are forwarded every day until an exit is hit. For example - if you want to enter a position after a big green bar happens, it doesn't need to happen again the next day. The system will stay in position until an exit happens, after which it looks for an entry signal again.
Note: Exit functions only work properly with the multi-pass backtester due to requiring previous state information. Make sure to apply at least one exit to your long and short signals, to avoid them being held indefinitely.
def strategy(data, state):
#Technical indicators
close = data.sel(field='close')
open_ = data.sel(field='open')
atr14 = qnta.atr(data.sel(field='high'), data.sel(field='low'), data.sel(field='close'), 14)
last_atr = atr14.isel(time=-1)
atr_perc = xr.where(atr14/close > 0.01, atr14/close, 0.01)
sma40 = qnta.sma(close, 40)
sma70 = qnta.sma(close, 70)
sma200 = qnta.sma(close, 200)
rsi120 = qnta.rsi(close, 120)
candle = close - open_
candlesma100 = qnta.sma(abs(candle), 100)
roc1day = qnta.roc(close, 1)
if state is None:
state = {
"weights": xr.zeros_like(close),
"open_price": xr.full_like(data.isel(time=-1).asset, np.nan, dtype=int),
"holding_time": xr.zeros_like(data.isel(time=-1).asset, dtype=int),
}
qnstate.write(state)
weights_prev = state['weights']
#To reuse the template, define your trading signals here ---------------------
long_signal = xr.where(sma40 > sma200, xr.where(sma40.shift(time=1) < sma200.shift(time=1), 4.5, 0), 0)
long_signal_2 = xr.where(candle > candlesma100.shift(time=1) * 4.5, 1, 0)
short_signal = xr.where(rsi120 > 65 , -15, 0)
exit1 = xr.where(close < sma70, 0, 1)
exit2 = xr.where(roc1day < -5, 0, 1)
entry_signal = short_signal + (long_signal + long_signal_2) * exit1 * exit2
entry_signal = entry_signal/atr_perc
# ----------------------------------------------------------------------------
#Keeping track of the previous position
weights_prev, entry_signal = xr.align(weights_prev, entry_signal, join='right')
weights = xr.where(entry_signal == 0, weights_prev.shift(time=1), entry_signal)
weights = weights.fillna(0)
#Define additional exit parameters here----------------------------------
open_price = qnte.update_open_price(data, weights, state) #Update open prices on position change to use with exits
signal_tp = qnte.take_profit_long_atr(data, weights, open_price, last_atr, atr_amount = 7) #Exit long positions if current close is bigger than entry price + 7*ATR
signal_sl = qnte.stop_loss_long_atr(data, weights, open_price, last_atr, atr_amount = 3) #Exit long positions if current close is lower than entry price - 3*ATR
signal_dc = qnte.max_hold_short(weights, state, max_period = 10) #Exit short positions after 10 periods (depending on the data - days, hours etc)
weights = weights * signal_tp * signal_sl * signal_dc
#------------------------------------------------------------------------
state['weights'] = weights
return weights, state
weights, state = qnbt.backtest(
competition_type="stocks_nasdaq100",
lookback_period=365, # lookback in calendar days
start_date="2006-01-01",
strategy=strategy,
analyze=True,
build_plots=True,
collect_all_states=False # if it is False, then the function returns the last state, otherwise - all states
)
Strategy Guidelines¶
Your trading algorithm can open both short and long positions.
This approach will work for any type of competition and data.
The Sharpe ratio of your system since January 1, 2006, must be greater than 0.7.
Your system must not replicate the current examples. We use a correlation filter to identify and remove duplicates in the submissions.
For more detailed rules, please visit our competition rules page.