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.

In [1]:
# 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 or take_profit_long_percentage to apply the stops to long positions, and also take_profit_short_atr or take_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 or stop_loss_long_percentage to apply the stops to long positions, and also stop_loss_short_atr or stop_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 or max_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.

In [2]:
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
)
Run last pass...
Load data...
fetched chunk 1/1 0s
Data loaded 0s
Run strategy...
State saved.
Load data for cleanup...
fetched chunk 1/1 0s
Data loaded 0s
Output cleaning...
fix uniq
ffill if the current price is None...
Check liquidity...
WARNING! Strategy trades non-liquid assets.
Fix liquidity...
Ok.
Check missed dates...
Ok.
Normalization...
Output cleaning is complete.
Write result...
Write output: /root/fractions.nc.gz
State saved.
---
Run first pass...
Load data...
fetched chunk 1/1 0s
Data loaded 0s
Run strategy...
State saved.
---
Load full data...
fetched chunk 1/6 0s
fetched chunk 2/6 0s
fetched chunk 3/6 0s
fetched chunk 4/6 0s
fetched chunk 5/6 0s
fetched chunk 6/6 0s
Data loaded 0s
---
Run iterations...

State saved.
100% (4641 of 4641) |####################| Elapsed Time: 0:05:43 Time:  0:05:43
Merge outputs...
Load data for cleanup and analysis...
fetched chunk 1/7 0s
fetched chunk 2/7 0s
fetched chunk 3/7 0s
fetched chunk 4/7 0s
fetched chunk 5/7 0s
fetched chunk 6/7 0s
fetched chunk 7/7 0s
Data loaded 0s
Output cleaning...
fix uniq
ffill if the current price is None...
Check liquidity...
WARNING! Strategy trades non-liquid assets.
Fix liquidity...
Ok.
Check missed dates...
Ok.
Normalization...
Output cleaning is complete.
Write result...
Write output: /root/fractions.nc.gz
State saved.
---
Analyze results...
Check...
Check liquidity...
Ok.
Check missed dates...
Ok.
Check the sharpe ratio...
Period: 2006-01-01 - 2024-06-11
Sharpe Ratio = 0.7522490163545448
ERROR! The Sharpe Ratio is too low. 0.7522490163545448 < 1
Improve the strategy and make sure that the in-sample Sharpe Ratio more than 1.
---
Align...
Calc global stats...
---
Calc stats per asset...
Build plots...
---
Select the asset (or leave blank to display the overall stats):
interactive(children=(Combobox(value='', description='asset', options=('', 'NAS:AAL', 'NAS:AAPL', 'NAS:ABNB', …

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.