strategy

Trend-Following System with Custom Arguments

A trend-following strategy for futures which uses different parameters for different assets.

You can clone and edit this example there (tab Examples).


In [1]:
# Import libraries
import xarray as xr
import xarray.ufuncs as xruf
import numpy as np
import pandas as pd

import qnt.output as qnout
import qnt.ta as qnta
import qnt.data    as qndata
import qnt.stats   as qnstats
import qnt.graph   as qngraph
import datetime    as dt
import plotly.graph_objs as go
import xarray.ufuncs as xruf
import time
2022-06-20T00

Load futures data

Quantnet provides data for 75 global derivatives. The underlying assets are currencies, cross-rates, indices, bonds, energy and metals from the world's futures exchanges.

Suppose we want to download the data for the last 20 years. One can use the following function:

In [2]:
fut_data = qndata.futures.load_data(tail = 20*365)
# The complete list
fut_data.asset
100% (33631792 of 33631792) |############| Elapsed Time: 0:00:00 Time:  0:00:00
Out[2]:
<xarray.DataArray 'asset' (asset: 71)>
array(['F_AD', 'F_AE', 'F_AG', 'F_AH', 'F_AX', 'F_BC', 'F_BG', 'F_BO', 'F_BP',
       'F_C', 'F_CA', 'F_CC', 'F_CD', 'F_CF', 'F_CL', 'F_CT', 'F_DE', 'F_DM',
       'F_DT', 'F_DX', 'F_EB', 'F_EC', 'F_ED', 'F_ES', 'F_F', 'F_FB', 'F_FP',
       'F_FV', 'F_FY', 'F_GC', 'F_GS', 'F_GX', 'F_HG', 'F_HO', 'F_JY', 'F_KC',
       'F_LR', 'F_LX', 'F_MD', 'F_MP', 'F_ND', 'F_NG', 'F_NH', 'F_NQ', 'F_NY',
       'F_OJ', 'F_PA', 'F_PL', 'F_QT', 'F_RB', 'F_RF', 'F_RP', 'F_RR', 'F_RU',
       'F_RY', 'F_S', 'F_SB', 'F_SF', 'F_SI', 'F_SS', 'F_SX', 'F_TR', 'F_TU',
       'F_TY', 'F_UB', 'F_US', 'F_UZ', 'F_VX', 'F_W', 'F_XX', 'F_YM'],
      dtype=object)
Coordinates:
  * asset    (asset) object 'F_AD' 'F_AE' 'F_AG' 'F_AH' ... 'F_W' 'F_XX' 'F_YM'
In [3]:
# we can see historical data on a chart
trend_fig = [
    go.Scatter(
        x = fut_data.sel(asset = 'F_DX').sel(field = 'close').to_pandas().index,
        y = fut_data.sel(asset = 'F_DX').sel(field = 'close'),
        line = dict(width=1,color='black'))]

# draw chart
fig = go.Figure(data = trend_fig)
fig.update_yaxes(fixedrange=False) # unlock vertical scrolling
fig.show()

Weights allocation

This function calculates positions using wma and roc as trend indicators.

In [4]:
def calc_positions(futures, ma_periods, roc_periods, sideways_threshold):
    """ Calculates positions for given data(futures) and parameters """
    close = futures.sel(field='close')
    
    # calculate MA 
    ma = qnta.lwma(close, ma_periods)
    # calcuate ROC
    roc = qnta.roc(ma, roc_periods)

    # positive trend direction
    positive_trend = roc > sideways_threshold
    # negtive trend direction
    negative_trend = roc < -sideways_threshold 
    # sideways
    sideways_trend = abs(roc) <= sideways_threshold
    
    # We suppose that a sideways trend after a positive trend is also positive
    side_positive_trend = positive_trend.where(sideways_trend == False).ffill('time').fillna(False)
    # and a sideways trend after a negative trend is also negative
    side_negative_trend = negative_trend.where(sideways_trend == False).ffill('time').fillna(False)

    # define signals
    buy_signal = positive_trend
    buy_stop_signal = side_negative_trend

    sell_signal = negative_trend
    sell_stop_signal = side_positive_trend

    # calc positions 
    position = close.copy(True)
    position[:] = np.nan
    position = xr.where(buy_signal, 1, position)
    position = xr.where(sell_signal, -1, position)
    position = xr.where(xruf.logical_and(buy_stop_signal, position.ffill('time') > 0), 0, position)
    position = xr.where(xruf.logical_and(sell_stop_signal, position.ffill('time') < 0), 0, position)
    position = position.ffill('time').fillna(0)

    return position

Select asset and adjust parameters:

In [5]:
asset = 'F_DX' ###

sdat = fut_data.sel(asset=asset).dropna('time','any')
sout = calc_positions(sdat, 40, 6, 1)
sout = xr.concat([sout], pd.Index([asset], name='asset'))

ssta = qnstats.calc_stat(fut_data, sout)

display(ssta.to_pandas().tail())

performance = ssta.to_pandas()["equity"]
qngraph.make_plot_filled(performance.index, performance, name="PnL (Equity)", type="log")
WARNING: some dates are missed in the portfolio_history
field equity relative_return volatility underwater max_drawdown sharpe_ratio mean_return bias instruments avg_turnover avg_holding_time
time
2022-06-14 1.657369 3.582112e-03 0.075977 0.000000 -0.209105 0.324104 0.024625 1.0 1.0 0.026109 46.847619
2022-06-15 1.650970 -3.860518e-03 0.075975 -0.003861 -0.209105 0.321540 0.024429 1.0 1.0 0.026105 46.847619
2022-06-16 1.627156 -1.442417e-02 0.076035 -0.018229 -0.209105 0.311808 0.023708 1.0 1.0 0.026102 46.847619
2022-06-17 1.643992 1.034643e-02 0.076061 -0.008071 -0.209105 0.318309 0.024211 1.0 1.0 0.026100 46.847619
2022-06-20 1.643992 -3.352995e-10 0.076053 -0.008071 -0.209105 0.318278 0.024206 1.0 1.0 0.026097 46.820755

This function calculate positions for multiple instruments with different parameters.

In [6]:
def calc_output_all(data, params):
    positions = data.sel(field='close').copy(True)
    positions[:] = np.nan
    for futures_name in params.keys(): 
        p = params[futures_name]
        futures_data = data.sel(asset=futures_name).dropna('time','any')
        p = calc_positions(futures_data, p['ma_periods'], p['roc_periods'], p['sideways_threshold'])
        positions.loc[{'asset':futures_name, 'time':p.time}] = p
    
    return positions
In [7]:
# say we select futures and their parameters for technical algorithm
params = {
    'F_NY': {
        'ma_periods': 200, 
        'roc_periods': 5, 
        'sideways_threshold': 2,
    },
    'F_GX': {
        'ma_periods': 200, 
        'roc_periods': 20, 
        'sideways_threshold': 2
    },
    'F_DX': {
        'ma_periods': 40, 
        'roc_periods': 6, 
        'sideways_threshold': 1
    },
}

futures_list = list(params.keys())

# form the output
output = calc_output_all(fut_data.sel(asset = futures_list), params)

# check the output
qnout.check(output, fut_data)

# write the result
qnout.write(output)

# show statistics
stat = qnstats.calc_stat(fut_data, output.sel(time=slice('2006-01-01', None)))
display(stat.to_pandas().tail())

# show plot with profit and losses:
performance = stat.to_pandas()["equity"]
qngraph.make_plot_filled(performance.index, performance, name="PnL (Equity)", type="log")
Check missed dates...
Ok.
Check sharpe ratio.
ERROR! The sharpe ratio is too low. 0.4426258583615881 < 1
Check correlation.

Ok. This strategy does not correlate with other strategies.
Write output: /root/fractions.nc.gz
field equity relative_return volatility underwater max_drawdown sharpe_ratio mean_return bias instruments avg_turnover avg_holding_time
time
2022-06-14 1.923523 0.015101 0.073792 -0.025773 -0.130363 0.528217 0.038978 0.0 2.0 0.036076 38.168182
2022-06-15 1.914695 -0.004589 0.073793 -0.030244 -0.130363 0.524305 0.038690 0.0 2.0 0.036071 38.168182
2022-06-16 1.908642 -0.003162 0.073789 -0.033310 -0.130363 0.521606 0.038489 0.0 2.0 0.036065 38.168182
2022-06-17 1.923210 0.007633 0.073802 -0.025931 -0.130363 0.527639 0.038941 0.0 2.0 0.036061 38.168182
2022-06-20 1.937709 0.007539 0.073815 -0.018588 -0.130363 0.533596 0.039387 0.0 2.0 0.036054 38.216216

Multi-pass implementation

Now, let's use multi-pass approach to verify the strategy. It is much slower but it is the best way to properly test it and to avoid looking-forward.

In [8]:
%%javascript
IPython.OutputArea.prototype._should_scroll = function(lines) { return false; }
// disable widget scrolling
In [9]:
# In your final submission you can remove/deactivate all the other cells to reduce the checking time.
# The checking system will run this book multiple times for every trading day within the in-sample period.
# Every pass the available data will be isolated till the current day.
# qnt.backtester is optimized to work with the checking system.
# The checking system will override test_period=1 to make your strategy to produce weights for 1 day per pass.

import xarray as xr
import numpy as np

import qnt.ta as qnta
import qnt.backtester as qnbt
import qnt.data as qndata
import qnt.xr_talib as xrtl
import xarray.ufuncs as xruf
import qnt.ta as qnta


def load_data(period):
    return qndata.futures_load_data(tail=period)


def calc_positions(futures, ma_periods, roc_periods, sideways_threshold):
    """ Calculates positions for given data(futures) and parameters """
    close = futures.sel(field='close')
    
    # calculate MA 
    ma = qnta.lwma(close, ma_periods)
    # calcuate ROC
    roc = qnta.roc(ma, roc_periods)

    # positive trend direction
    positive_trend = roc > sideways_threshold
    # negtive trend direction
    negative_trend = roc < -sideways_threshold 
    # sideways
    sideways_trend = abs(roc) <= sideways_threshold
    
    # We suppose that a sideways trend after a positive trend is also positive
    side_positive_trend = positive_trend.where(sideways_trend == False).ffill('time').fillna(False)
    # and a sideways trend after a negative trend is also negative
    side_negative_trend = negative_trend.where(sideways_trend == False).ffill('time').fillna(False)

    # define signals
    buy_signal = positive_trend
    buy_stop_signal = side_negative_trend

    sell_signal = negative_trend
    sell_stop_signal = side_positive_trend

    # calc positions 
    position = close.copy(True)
    position[:] = np.nan
    position = xr.where(buy_signal, 1, position)
    position = xr.where(sell_signal, -1, position)
    position = xr.where(xruf.logical_and(buy_stop_signal, position.ffill('time') > 0), 0, position)
    position = xr.where(xruf.logical_and(sell_stop_signal, position.ffill('time') < 0), 0, position)
    position = position.ffill('time').fillna(0)

    return position


def calc_output_all(data, params):
    positions = data.sel(field='close').copy(True)
    positions[:] = np.nan
    for futures_name in params.keys(): 
        p = params[futures_name]
        futures_data = data.sel(asset=futures_name).dropna('time','any')
        p = calc_positions(futures_data, p['ma_periods'], p['roc_periods'], p['sideways_threshold'])
        positions.loc[{'asset':futures_name, 'time':p.time}] = p
    
    return positions

# say we select futures and their parameters for technical algorithm
params = {
    'F_NY': {
        'ma_periods': 200, 
        'roc_periods': 5, 
        'sideways_threshold': 2,
    },
    'F_GX': {
        'ma_periods': 200, 
        'roc_periods': 20, 
        'sideways_threshold': 2
    },
    'F_DX': {
        'ma_periods': 40, 
        'roc_periods': 6, 
        'sideways_threshold': 1
    },
}
futures_list = list(params.keys())


def strategy(data):
    output = calc_output_all(data.sel(asset = futures_list), params)
    return output.isel(time=-1)


weights = qnbt.backtest(
    competition_type="futures",
    load_data=load_data,
    lookback_period=5*365,
    start_date='2006-01-01',
    strategy=strategy
)
Run last pass...
Load data...
Run pass...
Ok.
---
Run first pass...
Load data...
Run pass...
Ok.
---
Load full data...
---
Run iterations...

100% (4299 of 4299) |####################| Elapsed Time: 0:04:36 Time:  0:04:36
Merge outputs...
Load data for cleanup and analysis...
Check missed dates...
Ok.
Normalization...
Done.
Write output: /root/fractions.nc.gz
---
Analyze results...
Check...
Check missed dates...
Ok.
Check sharpe ratio.
ERROR! The sharpe ratio is too low. 0.5458722911362777 < 1
Check correlation.

Ok. This strategy does not correlate with other strategies.
---
Calc global stats...
---
Calc stats per asset...
Build plots...
---
Output:
asset F_DX F_GX F_NY
time
2022-06-07 0.5 -0.5 0.0
2022-06-08 0.5 -0.5 0.0
2022-06-09 0.5 -0.5 0.0
2022-06-10 0.5 -0.5 0.0
2022-06-13 0.5 -0.5 0.0
2022-06-14 0.5 -0.5 0.0
2022-06-15 0.5 -0.5 0.0
2022-06-16 0.5 -0.5 0.0
2022-06-17 0.5 -0.5 0.0
2022-06-20 0.5 -0.5 0.0
Stats:
field equity relative_return volatility underwater max_drawdown sharpe_ratio mean_return bias instruments avg_turnover avg_holding_time
time
2022-06-07 1.836485 -0.006274 0.074005 -0.070733 -0.130288 0.494375 0.036586 0.0 2.0 0.032754 52.658684
2022-06-08 1.849921 0.007316 0.074017 -0.063934 -0.130288 0.500212 0.037024 0.0 2.0 0.032748 52.658684
2022-06-09 1.864407 0.007831 0.074032 -0.056604 -0.130288 0.506450 0.037493 0.0 2.0 0.032744 52.658684
2022-06-10 1.876890 0.006695 0.074040 -0.050288 -0.130288 0.511797 0.037893 0.0 2.0 0.032738 52.658684
2022-06-13 1.897842 0.011163 0.074080 -0.039686 -0.130288 0.520589 0.038565 0.0 2.0 0.032732 52.658684
2022-06-14 1.926500 0.015101 0.074160 -0.025185 -0.130288 0.532300 0.039475 0.0 2.0 0.032729 52.658684
2022-06-15 1.917660 -0.004589 0.074160 -0.029658 -0.130288 0.528366 0.039184 0.0 2.0 0.032725 52.658684
2022-06-16 1.911597 -0.003162 0.074156 -0.032726 -0.130288 0.525651 0.038980 0.0 2.0 0.032719 52.658684
2022-06-17 1.926188 0.007633 0.074169 -0.025343 -0.130288 0.531716 0.039437 0.0 2.0 0.032716 52.658684
2022-06-20 1.940709 0.007539 0.074182 -0.017995 -0.130288 0.537705 0.039888 0.0 2.0 0.032710 53.025000