Forward looking¶
При разработке своей стратегии вы, скорее всего, столкнетесь с проблемой forward looking.
Суть ее заключается в том, что при распределении весов ваша стратегия использует данные из будущего. В результате, вам будет казаться, что вы разработали хорошую стратегию с высоким Sharpe Ratio, но при подневном расчете ваша стратегия будет быстро деградировать и Sharpe Ratio станет очень низким.
Есть несколько вспомогательных инструментов и подходов для решения этой проблемы.
Stepper¶
Первый подход - это вести пошаговый расчет с отсечением хвоста данных на каждом шагу так, чтобы при расчете весов текущего дня данные из будущего были бы недоступны.
Для этого есть qnt.stepper. Вот пример для buy and hold:
import qnt.data as qndata
import qnt.stats as qnstats
import qnt.xr_talib as qnxrtalib
import xarray as xr
import pandas as pd
from qnt.stepper import test_strategy
import datetime as dt
import qnt.exposure as qne
import xarray.ufuncs as xrf
# loads data
data = qndata.load_data(tail=dt.timedelta(days=4*365), dims=("time", "field", "asset"), forward_order=True)
# calculates TA indicators, they must not contain "forward looking"
wma = qnxrtalib.WMA(data.sel(field='close'), 120)
sroc = qnxrtalib.ROCP(wma, 60)
stoch = qnxrtalib.STOCH(data, 8, 3, 3)
k = stoch.sel(field='slowk')
d = stoch.sel(field='slowd')
# attaches TA indicators to the src data (you can add other data features)
data_ext = xr.concat([wma, sroc, k, d], pd.Index(['wma', 'sroc', 'k', 'd'], name='field'))
data_ext = xr.concat([data, data_ext], 'field')
# this is a global variable for the last day weights
weights = data.isel(time=0, field=0)
weights[:] = 0
# this function will be called step-by-step:
# first step: the data array contains a minimal number of days
# next step: the data array contains a one day more
# the process continues until data contains all days
def step(data):
# extracts the last day
latest = data.isel(time=-1)
is_liquid = latest.sel(field="is_liquid")
sroc = latest.sel(field='sroc')
k = latest.sel(field='k')
d = latest.sel(field='d')
# calculate signals
need_open = xrf.logical_and(sroc > 0.05, xrf.logical_and(k < 31, d < 31))
need_close = xrf.logical_or(xrf.logical_or(sroc < -0.05, is_liquid == 0), xrf.logical_and(k > 92, d > 92))
# modify weights accourding signals
global weights
weights.loc[need_open] = 1
weights.loc[need_close] = 0
# return normalized days for one day only
return (weights / weights.sum('asset')).fillna(0)
# this line runs step-by-step calculation
output = test_strategy(
data_ext, # data array for slicing
step=step, # step function
init_data_length=200 # data offset for the first step
)
# calc stats
stat = qnstats.calc_stat(data, output, max_periods=252 * 3)
print(stat.to_pandas())
qndata.write_output(output)
Однако, на практике этот подход может быть довольно медленным. К тому же, можно допустить ошибку и добавить фичу с «forward looking» в массив данных. Потому, стоит ознакомиться с другими подходами.
Forward looking test¶
Этот подход базируется на допущении, что, если стратегия не содержит «forward looking», то при двух прогонах на полном наборе данных и наборе данных с отсечением последнего года веса за пересекающийся промежуток времени будут одинаковы.
Вспомогательные функции реализованы в qnt.forward_looking и вот пример использования:
import qnt.data as qndata
import qnt.stats as qnstats
import qnt.xr_talib as qnxrtalib
import qnt.forward_looking as qnfl
data = qndata.load_data(min_date="2010-01-01", max_date=None, forward_order=True, dims=("time", "field", "asset"))
# this function will be called twice
# - with the entire data
# - with the data excluding last year
def strategy(data):
wma = qnxrtalib.WMA(data.sel(field='close'), 290)
sroc = qnxrtalib.ROCP(wma, 35)
is_liquid = data.sel(field="is_liquid")
weights = is_liquid.where(sroc > 0.0125)
weights = weights / weights.sum("asset", skipna=True)
return weights.fillna(0.0)
# this function calculte 2 passes and compare overlapping outputs
output = qnfl.calc_output_and_check_forward_looking(data, strategy)
stat = qnstats.calc_stat(data, output, max_periods=252 * 3)
print(stat.to_pandas())
qndata.write_output(output)
Этот подход работает хорошо с техническим анализом и с предобученными нейронными сетями. Но он работает плохо, если вы используете переобучение(или дообучение) нейронных сетей или если вы динамически решаете задачу оптимизации используя последние свежие данные.
В этом случае веса будут разниться и без реального пошагового прогона узнать значение In-Sample Sharpe ratio не получится.
Отсечение Out Sample¶
Этот подход используется в machine learning.
Еще один подход, это выделить период, на котором вы будете проверять вашу модель (к примеру, последний год), и не использовать его для обучения вашей модели.
Когда вы обучите вашу модель на старых данных, вы можете проверить как ваша модель работает на out sample. Так вы сможете примерно оценить качество вашей модели.
Хороший пример использования данного подхода: стратегия Trend Following with Adjasting. Эта стратегия использует оптимизацию параметров, это работает очень похоже на то, как работают нейронные сети.
precheck.ipynb¶
Последний подход - использовать precheck.ipynb. Эта вспомогательная книга позволяет позволяет проводить процесс пошаговой проверки стратегии также, как это происходит после отправки вашей стратегии в соревнование. Если вы поставите количество проходов 1000 или более, in-sample sharpe ratio будет такой же.
Этот инструмент похож на stepper, но обеспечивает наилучшую изоляцию данных при прогонах (лишние данные просто не будут загружены). Однако, такой процесс довольно затратный по времени.
Но даже 100 прогонов могут дать вам полезную информацию о реальной производительности вашей стратегии.
Также, можно в личном кабинете зарезервировать jupyter инстанс на сутки для проведения подобной проверки.