trendfollowing 1.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trendfollowing/__init__.py +37 -0
- trendfollowing/analysis/__init__.py +3 -0
- trendfollowing/analysis/autocorr_analysis.py +288 -0
- trendfollowing/analysis/check_vol_target.py +30 -0
- trendfollowing/analysis/signal_plots.py +200 -0
- trendfollowing/analysis/simple_backtest.py +26 -0
- trendfollowing/analysis/tf_signal_screener.py +196 -0
- trendfollowing/analysis/trade_ar_process.py +162 -0
- trendfollowing/analytics/__init__.py +28 -0
- trendfollowing/analytics/autocorrelation.py +122 -0
- trendfollowing/analytics/expected_return.py +169 -0
- trendfollowing/analytics/filters.py +43 -0
- trendfollowing/analytics/sharpe.py +268 -0
- trendfollowing/analytics/sharpe_test.py +95 -0
- trendfollowing/analytics/skewness.py +79 -0
- trendfollowing/backtests.py +477 -0
- trendfollowing/conventions.py +39 -0
- trendfollowing/local_path.py +76 -0
- trendfollowing/processes/__init__.py +3 -0
- trendfollowing/processes/arfima.py +154 -0
- trendfollowing/processes/path_engine.py +256 -0
- trendfollowing/settings.yaml +16 -0
- trendfollowing/systems/__init__.py +3 -0
- trendfollowing/systems/american.py +146 -0
- trendfollowing/systems/backtest_utils.py +169 -0
- trendfollowing/systems/european.py +153 -0
- trendfollowing/systems/tsmom.py +99 -0
- trendfollowing/universe.py +114 -0
- trendfollowing-1.0.4.dist-info/METADATA +417 -0
- trendfollowing-1.0.4.dist-info/RECORD +33 -0
- trendfollowing-1.0.4.dist-info/WHEEL +5 -0
- trendfollowing-1.0.4.dist-info/licenses/LICENSE +674 -0
- trendfollowing-1.0.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
trendfollowing: replication package for The Science and Practice of Trend-Following Systems
|
|
3
|
+
reference: Sepp, A. and Lucic, V., The Science and Practice of Trend-Following Systems,
|
|
4
|
+
https://ssrn.com/abstract=3167787
|
|
5
|
+
"""
|
|
6
|
+
__version__ = "1.0.0"
|
|
7
|
+
|
|
8
|
+
from trendfollowing.analytics.filters import (span_to_nu,
|
|
9
|
+
compute_ewm_long_short_weights)
|
|
10
|
+
from trendfollowing.analytics.autocorrelation import (population_acf,
|
|
11
|
+
compute_psi_nu,
|
|
12
|
+
power_autocorr)
|
|
13
|
+
from trendfollowing.analytics.expected_return import (expected_pnl_white_noise,
|
|
14
|
+
expected_pnl_ar1,
|
|
15
|
+
expected_pnl_ma1,
|
|
16
|
+
expected_pnl_arfima,
|
|
17
|
+
expected_turnover)
|
|
18
|
+
from trendfollowing.analytics.sharpe import (SignalMoments,
|
|
19
|
+
compute_signal_moments,
|
|
20
|
+
compute_daily_moments,
|
|
21
|
+
compute_annualised_sharpe,
|
|
22
|
+
compute_realized_sharpe,
|
|
23
|
+
sharpe_white_noise,
|
|
24
|
+
sharpe_white_noise_approx,
|
|
25
|
+
sharpe_ar1,
|
|
26
|
+
sharpe_ar1_approx,
|
|
27
|
+
sharpe_arfima,
|
|
28
|
+
expected_annual_return)
|
|
29
|
+
from trendfollowing.analytics.skewness import (aggregated_third_moment_white_noise,
|
|
30
|
+
skewness_white_noise,
|
|
31
|
+
skewness_master_curve,
|
|
32
|
+
skewness_peak_horizon)
|
|
33
|
+
|
|
34
|
+
from trendfollowing.conventions import (AF_DAILY,
|
|
35
|
+
PPY_QUARTERLY,
|
|
36
|
+
PPY_MONTHLY,
|
|
37
|
+
compute_daily_annualised_vol)
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""
|
|
2
|
+
empirical autocorrelation analysis of futures returns: per-instrument and
|
|
3
|
+
per-group acf estimates and plots behind the autocorrelation exhibits of the paper
|
|
4
|
+
reference: Sepp, A. and Lucic, V., The Science and Practice of Trend-Following Systems,
|
|
5
|
+
https://ssrn.com/abstract=3167787
|
|
6
|
+
"""
|
|
7
|
+
# packages
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
import matplotlib.pyplot as plt
|
|
11
|
+
import seaborn as sns
|
|
12
|
+
import qis as qis
|
|
13
|
+
from typing import Tuple, Dict, List, Union
|
|
14
|
+
from enum import Enum
|
|
15
|
+
|
|
16
|
+
import qis.utils.df_groups as dfg
|
|
17
|
+
import qis.utils.dates as da
|
|
18
|
+
import qis.models.linear.ra_returns as tra
|
|
19
|
+
import qis.plots.boxplot as box
|
|
20
|
+
import qis.perfstats.returns as ret
|
|
21
|
+
from qis.models.linear.auto_corr import estimate_acf_from_paths
|
|
22
|
+
|
|
23
|
+
from futures_strats.data.universes.futures.bbg_futures import Universes
|
|
24
|
+
from futures_strats.local_path import LOCAL_PATH
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def compute_returns_autocorrelation(prices: Union[pd.DataFrame, pd.Series],
|
|
28
|
+
freq: str = 'B',
|
|
29
|
+
span: int = 31,
|
|
30
|
+
autocorr_span: int = 31,
|
|
31
|
+
is_ra_returns: bool = False
|
|
32
|
+
) -> Union[pd.DataFrame, pd.Series]:
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
sample acf of log returns per instrument at the given lags
|
|
36
|
+
"""
|
|
37
|
+
if isinstance(prices, pd.Series):
|
|
38
|
+
prices = prices.to_frame()
|
|
39
|
+
returns_1 = qis.to_returns(prices=prices, is_log_returns=True, freq=None, drop_first=True)
|
|
40
|
+
autocorr_df = qis.ewm_xy_convolution(returns=returns_1,
|
|
41
|
+
freq=freq,
|
|
42
|
+
convolution_type=qis.ConvolutionType.AUTO_CORR,
|
|
43
|
+
is_ra_returns=is_ra_returns)
|
|
44
|
+
return autocorr_df
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def compute_returns_autocorrelation0(prices: Union[pd.DataFrame, pd.Series],
|
|
48
|
+
freq: str = 'B',
|
|
49
|
+
span: int = 31,
|
|
50
|
+
autocorr_span: int = 31,
|
|
51
|
+
is_ra_returns: bool = False
|
|
52
|
+
) -> Union[pd.DataFrame, pd.Series]:
|
|
53
|
+
"""
|
|
54
|
+
sample acf of log returns per instrument, legacy variant kept for comparison
|
|
55
|
+
"""
|
|
56
|
+
if is_ra_returns:
|
|
57
|
+
returns_1 = qis.to_returns(prices=prices, is_log_returns=True, freq=None, drop_first=True)
|
|
58
|
+
returns = qis.compute_sum_freq_ra_returns(returns=returns_1, freq=freq, span=span,
|
|
59
|
+
is_log_returns_to_arithmetic=False,
|
|
60
|
+
is_norm=False,
|
|
61
|
+
warmup_period=100)
|
|
62
|
+
else:
|
|
63
|
+
returns = qis.to_returns(prices=prices, is_log_returns=True, freq=freq, drop_first=True)
|
|
64
|
+
|
|
65
|
+
autocorr_df = qis.compute_ewm_vector_autocorr_df(data=returns,
|
|
66
|
+
span=autocorr_span,
|
|
67
|
+
lag=1,
|
|
68
|
+
is_normalize=True)
|
|
69
|
+
return autocorr_df
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_prices(time_period: da.TimePeriod = None) -> Tuple[Dict, pd.Series]:
|
|
73
|
+
"""
|
|
74
|
+
load the asset-class price dictionary and group metadata for the acf exhibits
|
|
75
|
+
"""
|
|
76
|
+
universe_data = Universes.BBG_FUTURES.load_universe_data(local_path=LOCAL_PATH)
|
|
77
|
+
prices = universe_data.get_prices(time_period=time_period)
|
|
78
|
+
prices = prices.dropna(axis=1, how='all')
|
|
79
|
+
ac_data = universe_data.get_ac_data()[prices.columns]
|
|
80
|
+
ac_data = ac_data.apply(lambda x: x.value)
|
|
81
|
+
ac_prices = dfg.split_df_by_groups(df=prices, group_data=ac_data, total_column='Universe')
|
|
82
|
+
|
|
83
|
+
return ac_prices, ac_data
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def plot_instrument_acf(price: pd.Series,
|
|
87
|
+
freqs: List[str] = ('B', 'W-MON', 'ME', 'QE'),
|
|
88
|
+
ra_spans: List[int] = (2.5, 5, 21, 63),
|
|
89
|
+
nlags: int = 20,
|
|
90
|
+
is_ra_returns: bool = False
|
|
91
|
+
):
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
plot the sample acf and pacf bars of one instrument's returns
|
|
95
|
+
"""
|
|
96
|
+
with sns.axes_style("darkgrid"):
|
|
97
|
+
fig, axs = plt.subplots(2, 2, figsize=(12, 8), tight_layout=True)
|
|
98
|
+
axs = qis.to_flat_list(axs)
|
|
99
|
+
|
|
100
|
+
for idx, freq in enumerate(freqs):
|
|
101
|
+
returns_1 = qis.to_returns(prices=price, is_log_returns=True, freq=None, drop_first=True)
|
|
102
|
+
if is_ra_returns:
|
|
103
|
+
returns = qis.compute_sum_freq_ra_returns(returns=returns_1, freq=freq, span=ra_spans[idx],
|
|
104
|
+
is_log_returns_to_arithmetic=False,
|
|
105
|
+
is_norm=False)
|
|
106
|
+
else:
|
|
107
|
+
returns = qis.to_returns(prices=price, is_log_returns=True, freq=freq, drop_first=True)
|
|
108
|
+
|
|
109
|
+
acfs, pacfs = qis.estimate_acf_from_path(path=returns, nlags=nlags)
|
|
110
|
+
qis.plot_bars(df=acfs,
|
|
111
|
+
title=f"Returns frequency={freq}",
|
|
112
|
+
legend_loc=None,
|
|
113
|
+
x_rotation=0,
|
|
114
|
+
xlabel='lag',
|
|
115
|
+
ax=axs[idx])
|
|
116
|
+
ax = axs[idx]
|
|
117
|
+
n_error = 1.0 / np.sqrt(len(returns.index))
|
|
118
|
+
ax.axhline(n_error, color='red', linestyle='dashed', linewidth=1)
|
|
119
|
+
ax.axhline(-n_error, color='red', linestyle='dashed', linewidth=1)
|
|
120
|
+
qis.set_suptitle(fig, title=f"Autocorrelation of {price.name}: {qis.get_time_period(df=price).to_str()}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def plot_instrument_acf_ewm(prices: Union[pd.Series, pd.DataFrame],
|
|
124
|
+
freqs: List[str] = ('B', 'W-MON', 'ME', 'QE'),
|
|
125
|
+
ra_span: int = 31,
|
|
126
|
+
autocorr_spans: List[int] = (10*260, 10*52, 10*12, 10*4),
|
|
127
|
+
is_ra_returns: bool = False
|
|
128
|
+
):
|
|
129
|
+
|
|
130
|
+
"""
|
|
131
|
+
plot the ewm-smoothed acf of instrument returns across lags
|
|
132
|
+
"""
|
|
133
|
+
with sns.axes_style("darkgrid"):
|
|
134
|
+
fig, axs = plt.subplots(2, 2, figsize=(12, 8), tight_layout=True)
|
|
135
|
+
axs = qis.to_flat_list(axs)
|
|
136
|
+
|
|
137
|
+
for idx, freq in enumerate(freqs):
|
|
138
|
+
autocorr_df = compute_returns_autocorrelation(prices=prices,
|
|
139
|
+
freq=freq,
|
|
140
|
+
span=ra_span,
|
|
141
|
+
autocorr_span=autocorr_spans[idx],
|
|
142
|
+
is_ra_returns=is_ra_returns)
|
|
143
|
+
if isinstance(prices, pd.DataFrame) and len(prices.columns) > 10:
|
|
144
|
+
legend_loc = None
|
|
145
|
+
else:
|
|
146
|
+
legend_loc = 'upper left'
|
|
147
|
+
qis.plot_time_series(df=autocorr_df,
|
|
148
|
+
title=f"Returns frequency={freq}",
|
|
149
|
+
x_date_freq='YE',
|
|
150
|
+
date_format='%d-%b-%y',
|
|
151
|
+
legend_loc=legend_loc,
|
|
152
|
+
ax=axs[idx])
|
|
153
|
+
ax = axs[idx]
|
|
154
|
+
n_error = 1.0 / np.sqrt(autocorr_spans[idx])
|
|
155
|
+
ax.axhline(n_error, color='red', linestyle='dashed', linewidth=1)
|
|
156
|
+
ax.axhline(-n_error, color='red', linestyle='dashed', linewidth=1)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def plot_group_acf_ewm(prices: pd.DataFrame,
|
|
161
|
+
freqs: List[str] = ('B', 'W-MON', 'ME', 'QE'),
|
|
162
|
+
ra_span: int = 31,
|
|
163
|
+
autocorr_spans: List[int] = (10*260, 10*52, 10*12, 10*4),
|
|
164
|
+
is_ra_returns: bool = False
|
|
165
|
+
) -> plt.Figure:
|
|
166
|
+
|
|
167
|
+
"""
|
|
168
|
+
plot the ewm-smoothed acf aggregated by asset group
|
|
169
|
+
"""
|
|
170
|
+
with sns.axes_style("darkgrid"):
|
|
171
|
+
fig, axs = plt.subplots(2, 2, figsize=(12, 8), tight_layout=True)
|
|
172
|
+
axs = qis.to_flat_list(axs)
|
|
173
|
+
|
|
174
|
+
for idx, freq in enumerate(freqs):
|
|
175
|
+
autocorr_df = compute_returns_autocorrelation(prices=prices,
|
|
176
|
+
freq=freq,
|
|
177
|
+
span=ra_span,
|
|
178
|
+
autocorr_span=autocorr_spans[idx],
|
|
179
|
+
is_ra_returns=is_ra_returns)
|
|
180
|
+
autocorr_df = autocorr_df.replace({0.0: np.nan})
|
|
181
|
+
df = pd.concat([autocorr_df.quantile(q=0.25, axis=1).rename('25% Quantile'),
|
|
182
|
+
autocorr_df.median(axis=1).rename('Median'),
|
|
183
|
+
autocorr_df.quantile(q=0.75, axis=1).rename('75% Quantile')], axis=1)
|
|
184
|
+
|
|
185
|
+
qis.plot_time_series(df=df,
|
|
186
|
+
title=f"Returns frequency={freq}",
|
|
187
|
+
x_date_freq='YE',
|
|
188
|
+
date_format='%d-%b-%y',
|
|
189
|
+
framealpha=0.9,
|
|
190
|
+
trend_line=qis.TrendLine.ZERO_SHADOWS,
|
|
191
|
+
ax=axs[idx])
|
|
192
|
+
ax = axs[idx]
|
|
193
|
+
n_error = 1.0 / np.sqrt(autocorr_spans[idx])
|
|
194
|
+
ax.axhline(n_error, color='red', linestyle='dashed', linewidth=1)
|
|
195
|
+
ax.axhline(-n_error, color='red', linestyle='dashed', linewidth=1)
|
|
196
|
+
|
|
197
|
+
return fig
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def plot_acf(time_period: da.TimePeriod = None):
|
|
201
|
+
ac_prices, ac_data = get_prices(time_period=time_period)
|
|
202
|
+
|
|
203
|
+
freq = 'QE'
|
|
204
|
+
ac_acfs = []
|
|
205
|
+
for ac, prices in ac_prices.items():
|
|
206
|
+
returns = ret.to_returns(prices=prices, freq=freq)
|
|
207
|
+
acfs, m_acf, std_acf = estimate_acf_from_paths(paths=returns, is_pacf=True)
|
|
208
|
+
ac_acfs.append(acfs)
|
|
209
|
+
ac_acfs = pd.concat(ac_acfs, axis=1)
|
|
210
|
+
|
|
211
|
+
with sns.axes_style("darkgrid"):
|
|
212
|
+
fig, axs = plt.subplots(1, 1, figsize=(18, 10), tight_layout=True)
|
|
213
|
+
box.df_boxplot_by_index(df=ac_acfs, ax=axs)
|
|
214
|
+
|
|
215
|
+
returns = ret.to_returns(prices=ac_prices['Universe'], freq=freq)
|
|
216
|
+
returns, weights, _ = tra.compute_ra_returns(returns=ret.to_returns(prices=ac_prices['Universe'],is_first_zero=True), ewm_lambda=0.94)
|
|
217
|
+
# returns = returns.resample('QE').sum().iloc[1:, :]
|
|
218
|
+
print(returns)
|
|
219
|
+
|
|
220
|
+
acfs, m_acf, std_acf = estimate_acf_from_paths(paths=returns, is_pacf=True, nlags=100)
|
|
221
|
+
acfs = acfs.drop(0, axis=0)
|
|
222
|
+
ac_acfs = acfs.T.dropna(axis=0, how='all')
|
|
223
|
+
ac_acfs.columns = [f"lag-{n}" for n in range(len(ac_acfs.columns))]
|
|
224
|
+
ac_acfs['ac'] = ac_data
|
|
225
|
+
ac_acfs = ac_acfs.set_index('ac')
|
|
226
|
+
print(ac_acfs)
|
|
227
|
+
with sns.axes_style("darkgrid"):
|
|
228
|
+
fig, axs = plt.subplots(1, 1, figsize=(18, 10), tight_layout=True)
|
|
229
|
+
box.df_boxplot_by_hue_var(df=ac_acfs, x_index_var_name='ac', hue_var_name='lags', ax=axs)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class LocalTests(Enum):
|
|
233
|
+
DATA = 1
|
|
234
|
+
ACF = 2
|
|
235
|
+
INSTRUMENT_ACF = 3
|
|
236
|
+
TIME_SERIES_AUTOCORR = 4
|
|
237
|
+
GROUP_TIME_SERIES_AUTOCORR = 5
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def run_local_test(local_test: LocalTests):
|
|
241
|
+
"""Run local tests for development and debugging purposes.
|
|
242
|
+
|
|
243
|
+
These are integration tests that download real data and generate reports.
|
|
244
|
+
Use for quick verification during development.
|
|
245
|
+
"""
|
|
246
|
+
|
|
247
|
+
from futures_strats.local_path import LOCAL_PATH
|
|
248
|
+
universe_data = Universes.BBG_FUTURES.load_universe_data(local_path=LOCAL_PATH)
|
|
249
|
+
prices = universe_data.get_prices(freq='B').ffill()
|
|
250
|
+
time_period = da.TimePeriod('31Dec1990', None)
|
|
251
|
+
prices = time_period.locate(prices)
|
|
252
|
+
|
|
253
|
+
if local_test == LocalTests.DATA:
|
|
254
|
+
get_prices()
|
|
255
|
+
|
|
256
|
+
elif local_test == LocalTests.ACF:
|
|
257
|
+
plot_acf(time_period=time_period)
|
|
258
|
+
|
|
259
|
+
elif local_test == LocalTests.INSTRUMENT_ACF:
|
|
260
|
+
# price = prices['NI1 Index'].dropna()
|
|
261
|
+
price = prices['ES1 Index'].dropna()
|
|
262
|
+
plot_instrument_acf(price=price, is_ra_returns=False)
|
|
263
|
+
|
|
264
|
+
elif local_test == LocalTests.TIME_SERIES_AUTOCORR:
|
|
265
|
+
# price = prices['NI1 Index'].dropna()
|
|
266
|
+
# price = prices['ES1 Index'].dropna()
|
|
267
|
+
price = prices['GC1 Comdty'].dropna()
|
|
268
|
+
plot_instrument_acf_ewm(prices=price, is_ra_returns=True)
|
|
269
|
+
|
|
270
|
+
elif local_test == LocalTests.GROUP_TIME_SERIES_AUTOCORR:
|
|
271
|
+
group_data = universe_data.get_ac_data()
|
|
272
|
+
dfs = qis.split_df_by_groups(df=prices, group_data=group_data)
|
|
273
|
+
# plot_instrument_acf_ewm(prices=dfs['AcCom.EQ'], is_ra_returns=False)
|
|
274
|
+
figs = []
|
|
275
|
+
for key, df in dfs.items():
|
|
276
|
+
fig = plot_group_acf_ewm(prices=df, is_ra_returns=True)
|
|
277
|
+
qis.set_suptitle(fig, title=f"{key}")
|
|
278
|
+
figs.append(fig)
|
|
279
|
+
qis.save_figs_to_pdf(figs, file_name='group_acf', local_path=qis.get_output_path())
|
|
280
|
+
|
|
281
|
+
plt.show()
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
if __name__ == '__main__':
|
|
285
|
+
|
|
286
|
+
local_test = LocalTests.TIME_SERIES_AUTOCORR
|
|
287
|
+
|
|
288
|
+
run_local_test(local_test=local_test)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
numerical check of the inverse-volatility moments under gaussian returns: compares
|
|
3
|
+
the monte carlo mean and variance of the reciprocal realized volatility with the
|
|
4
|
+
closed-form inverse-chi moments, which quantifies the attenuation from volatility
|
|
5
|
+
normalization
|
|
6
|
+
reference: Sepp, A. and Lucic, V., The Science and Practice of Trend-Following Systems,
|
|
7
|
+
https://ssrn.com/abstract=3167787
|
|
8
|
+
"""
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
import matplotlib.pyplot as plt
|
|
12
|
+
import qis as qis
|
|
13
|
+
from scipy.special import gamma
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if __name__ == '__main__':
|
|
17
|
+
n = 240
|
|
18
|
+
n_path = 100000
|
|
19
|
+
x = np.random.normal(0.0, 1.0, size=(n, n_path))
|
|
20
|
+
x2 = np.square(x)
|
|
21
|
+
vol_bar = 0.15
|
|
22
|
+
scale = vol_bar / np.sqrt(n)
|
|
23
|
+
vol = scale*np.sqrt(np.sum(x2, axis=0))
|
|
24
|
+
inv_vol = 1.0 / vol
|
|
25
|
+
qis.plot_histogram(df=pd.Series(inv_vol))
|
|
26
|
+
print(f"e_mc={np.mean(inv_vol)}, var_mc={np.var(inv_vol)}")
|
|
27
|
+
e_an = (1.0/scale)*np.sqrt(0.5)*gamma(n/2.0-0.5)/gamma(n/2)
|
|
28
|
+
var_an = (1.0/scale)**2/(n-2.0) - e_an**2
|
|
29
|
+
print(f"e_an={e_an}, var_an={var_an}, e_an_limit={1.0/vol_bar}")
|
|
30
|
+
plt.show()
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""
|
|
2
|
+
illustration of signal plots
|
|
3
|
+
"""
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import os
|
|
6
|
+
import numpy as np
|
|
7
|
+
import qis as qis
|
|
8
|
+
import seaborn as sns
|
|
9
|
+
import matplotlib.pyplot as plt
|
|
10
|
+
from typing import Optional, Dict
|
|
11
|
+
from enum import Enum
|
|
12
|
+
|
|
13
|
+
# my projects
|
|
14
|
+
from trendfollowing.systems.backtest_utils import compute_vol_norm_returns
|
|
15
|
+
from trendfollowing.systems.european import compute_tf_signal_weight, compute_tf_signal, compute_tf_strat_pnl
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def plot_vol_norm_returns(returns_dict: Dict[str, pd.Series], vol_span: int = 31) -> plt.Figure:
|
|
19
|
+
"""
|
|
20
|
+
plot the volatility-normalized returns of the given instruments
|
|
21
|
+
"""
|
|
22
|
+
with sns.axes_style("darkgrid"):
|
|
23
|
+
fig, axs = plt.subplots(len(returns_dict.keys()), 2, figsize=(15, 12), tight_layout=True)
|
|
24
|
+
axs = qis.to_flat_list(axs)
|
|
25
|
+
for idx, (key, returns) in enumerate(returns_dict.items()):
|
|
26
|
+
vol_norm_returns = compute_vol_norm_returns(returns=returns.to_numpy(), vol_span=vol_span)
|
|
27
|
+
vol_norm_returns = pd.Series(vol_norm_returns, index=returns.index, name=key)
|
|
28
|
+
qis.plot_time_series(df=vol_norm_returns, title=f"{key} vol normalised returns",
|
|
29
|
+
ax=axs[2*idx])
|
|
30
|
+
qis.plot_histogram(df=vol_norm_returns, title=f"{key} vol normalised returns",
|
|
31
|
+
ax=axs[2*idx+1])
|
|
32
|
+
return fig
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def plot_signal(returns_dict: Dict[str, pd.Series],
|
|
36
|
+
long_span: int = 31,
|
|
37
|
+
short_span: Optional[int] = None,
|
|
38
|
+
vol_span: int = 31
|
|
39
|
+
) -> plt.Figure:
|
|
40
|
+
"""
|
|
41
|
+
plot the ewma tf signal of the given instruments
|
|
42
|
+
"""
|
|
43
|
+
with sns.axes_style("darkgrid"):
|
|
44
|
+
fig, axs = plt.subplots(len(returns_dict.keys()), 2, figsize=(15, 12), tight_layout=True)
|
|
45
|
+
axs = qis.to_flat_list(axs)
|
|
46
|
+
for idx, (key, returns) in enumerate(returns_dict.items()):
|
|
47
|
+
tf_signal = compute_tf_signal(returns=returns.to_numpy(), long_span=long_span, short_span=short_span, vol_span=vol_span)
|
|
48
|
+
tf_signal = pd.Series(tf_signal, index=returns.index, name=key)
|
|
49
|
+
qis.plot_time_series(df=tf_signal, title=f"{key} signal",
|
|
50
|
+
ax=axs[2*idx])
|
|
51
|
+
qis.plot_histogram(df=tf_signal, title=f"{key} signal",
|
|
52
|
+
ax=axs[2*idx+1])
|
|
53
|
+
return fig
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def plot_signal_weight(returns_dict: Dict[str, pd.Series],
|
|
57
|
+
long_span: int = 31,
|
|
58
|
+
short_span: Optional[int] = None,
|
|
59
|
+
vol_span: int = 33,
|
|
60
|
+
vol_target: float = 0.3
|
|
61
|
+
) -> plt.Figure:
|
|
62
|
+
"""
|
|
63
|
+
plot the signal and the volatility-target weight of the given instruments
|
|
64
|
+
"""
|
|
65
|
+
with sns.axes_style("darkgrid"):
|
|
66
|
+
fig, axs = plt.subplots(len(returns_dict.keys()), 2, figsize=(15, 12), tight_layout=True)
|
|
67
|
+
axs = qis.to_flat_list(axs)
|
|
68
|
+
for idx, (key, returns) in enumerate(returns_dict.items()):
|
|
69
|
+
tf_signal_weight, signals, vols = compute_tf_signal_weight(returns=returns.to_numpy(), long_span=long_span,
|
|
70
|
+
short_span=short_span, vol_span=vol_span,
|
|
71
|
+
vol_target=vol_target)
|
|
72
|
+
tf_signal_weight = pd.Series(tf_signal_weight, index=returns.index, name=key)
|
|
73
|
+
qis.plot_time_series(df=tf_signal_weight, title=f"{key} signal",
|
|
74
|
+
ax=axs[2*idx])
|
|
75
|
+
qis.plot_histogram(df=tf_signal_weight, title=f"{key} signal",
|
|
76
|
+
ax=axs[2*idx+1])
|
|
77
|
+
return fig
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def plot_strat_pnl(returns_dict: Dict[str, pd.Series],
|
|
81
|
+
long_span: int = 31,
|
|
82
|
+
short_span: Optional[int] = None,
|
|
83
|
+
vol_span: int = 33,
|
|
84
|
+
vol_target: float = 0.3
|
|
85
|
+
) -> plt.Figure:
|
|
86
|
+
"""
|
|
87
|
+
plot the strategy pnl of the given instruments under the ewma signal
|
|
88
|
+
"""
|
|
89
|
+
with sns.axes_style("darkgrid"):
|
|
90
|
+
fig, axs = plt.subplots(len(returns_dict.keys()), 2, figsize=(15, 12), tight_layout=True)
|
|
91
|
+
axs = qis.to_flat_list(axs)
|
|
92
|
+
for idx, (key, returns) in enumerate(returns_dict.items()):
|
|
93
|
+
pnl, weights, vols = compute_tf_strat_pnl(returns=returns.to_numpy(), long_span=long_span, short_span=short_span,
|
|
94
|
+
vol_span=vol_span,
|
|
95
|
+
vol_target=vol_target)
|
|
96
|
+
pnl = pd.Series(pnl, index=returns.index, name=key)
|
|
97
|
+
qis.plot_time_series(df=pnl, title=f"{key} cum p&l",
|
|
98
|
+
ax=axs[2*idx])
|
|
99
|
+
qis.plot_histogram(df=pnl.diff(1), title=f"{key} daily p&l",
|
|
100
|
+
ax=axs[2*idx+1])
|
|
101
|
+
return fig
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def check_filter_span(returns: pd.Series, long_span: int = 100, short_span: int = 10):
|
|
105
|
+
"""
|
|
106
|
+
visual check of the single and long-short filters at the given spans on one instrument
|
|
107
|
+
"""
|
|
108
|
+
long_spans = np.linspace(100, 1000, 10)
|
|
109
|
+
signals = {}
|
|
110
|
+
for long_span in long_spans:
|
|
111
|
+
signals[f"span={long_span}"] = qis.compute_ewm_long_short_filter(data=returns, long_span=long_span, short_span=None)
|
|
112
|
+
signals = pd.DataFrame.from_dict(signals, orient='columns')
|
|
113
|
+
with sns.axes_style("darkgrid"):
|
|
114
|
+
fig, ax = plt.subplots(1, 1, figsize=(15, 12), tight_layout=True)
|
|
115
|
+
qis.plot_histogram(df=signals, title='long', ax=ax)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def check_long_short_filters(returns: pd.DataFrame, long_span: int = 100, short_span: int = 10):
|
|
119
|
+
"""
|
|
120
|
+
visual check of the long-short filter combinations on a return panel
|
|
121
|
+
"""
|
|
122
|
+
signal_unit = qis.compute_ewm_long_short_filter(data=returns, long_span=long_span, short_span=None)
|
|
123
|
+
signal_2 = qis.compute_ewm_long_short_filter(data=returns, long_span=long_span, short_span=short_span)
|
|
124
|
+
|
|
125
|
+
with sns.axes_style("darkgrid"):
|
|
126
|
+
fig, axs = plt.subplots(1, 2, figsize=(15, 12), tight_layout=True)
|
|
127
|
+
qis.plot_histogram(df=signal_unit, title='long', ax=axs[0])
|
|
128
|
+
qis.plot_histogram(df=signal_2, title='ls', ax=axs[1])
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class LocalTests(Enum):
|
|
132
|
+
PLOT_VOL_NORM_RETURNS = 1
|
|
133
|
+
PLOT_SIGNAL = 2
|
|
134
|
+
PLOT_SIGNAL_WEIGHT = 3
|
|
135
|
+
PLOT_PNL = 4
|
|
136
|
+
CHECK_SIGNAL = 4
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def run_local_test(local_test: LocalTests):
|
|
140
|
+
"""Run local tests for development and debugging purposes.
|
|
141
|
+
|
|
142
|
+
These are integration tests that download real data and generate reports.
|
|
143
|
+
Use for quick verification during development.
|
|
144
|
+
"""
|
|
145
|
+
from trendfollowing.local_path import get_universe_data_path
|
|
146
|
+
local_path = get_universe_data_path()
|
|
147
|
+
|
|
148
|
+
prices = qis.load_df_from_csv(file_name='bbg_futures_close', local_path=local_path)
|
|
149
|
+
time_period = qis.TimePeriod(start='31Dec1999', end=None)
|
|
150
|
+
prices = time_period.locate(prices)
|
|
151
|
+
returns = qis.to_returns(prices=prices)
|
|
152
|
+
|
|
153
|
+
if local_test == LocalTests.PLOT_VOL_NORM_RETURNS:
|
|
154
|
+
returns_dict = {'S&P 500': returns['ES1 Index'].dropna(), 'UST10Y': returns['TY1 Comdty'].dropna()}
|
|
155
|
+
print(returns_dict)
|
|
156
|
+
plot_vol_norm_returns(returns_dict=returns_dict)
|
|
157
|
+
|
|
158
|
+
elif local_test == LocalTests.PLOT_SIGNAL:
|
|
159
|
+
returns_dict = {'S&P 500': returns['ES1 Index'].dropna(), 'UST10Y': returns['TY1 Comdty'].dropna()}
|
|
160
|
+
print(returns_dict)
|
|
161
|
+
plot_signal(returns_dict=returns_dict,
|
|
162
|
+
long_span=100,
|
|
163
|
+
short_span=None,
|
|
164
|
+
vol_span=31)
|
|
165
|
+
|
|
166
|
+
elif local_test == LocalTests.PLOT_SIGNAL_WEIGHT:
|
|
167
|
+
returns_dict = {'S&P 500': returns['ES1 Index'].dropna(), 'UST10Y': returns['TY1 Comdty'].dropna()}
|
|
168
|
+
print(returns_dict)
|
|
169
|
+
plot_signal_weight(returns_dict=returns_dict,
|
|
170
|
+
long_span=100,
|
|
171
|
+
short_span=None,
|
|
172
|
+
vol_span=31)
|
|
173
|
+
|
|
174
|
+
elif local_test == LocalTests.PLOT_PNL:
|
|
175
|
+
returns_dict = {'S&P 500': returns['ES1 Index'].dropna(), 'UST10Y': returns['TY1 Comdty'].dropna()}
|
|
176
|
+
print(returns_dict)
|
|
177
|
+
plot_strat_pnl(returns_dict=returns_dict,
|
|
178
|
+
long_span=250,
|
|
179
|
+
short_span=20,
|
|
180
|
+
vol_span=31)
|
|
181
|
+
|
|
182
|
+
elif local_test == LocalTests.CHECK_SIGNAL:
|
|
183
|
+
|
|
184
|
+
# signals = compute_tf_signal(returns=returns.to_numpy(), tf_span=120)
|
|
185
|
+
#signals = pd.DataFrame(signals, index=returns.index, columns=returns.columns)
|
|
186
|
+
#qis.plot_histogram(df=signals)
|
|
187
|
+
|
|
188
|
+
returns = pd.DataFrame(np.random.normal(loc=0.0, scale=0.2, size=(100000, 30)))
|
|
189
|
+
ra_returns, _, _ = qis.compute_ra_returns(returns=returns)
|
|
190
|
+
#check_long_short_filters(returns=ra_returns)
|
|
191
|
+
|
|
192
|
+
check_filter_span(returns=ra_returns.iloc[:, 0])
|
|
193
|
+
plt.show()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if __name__ == '__main__':
|
|
197
|
+
|
|
198
|
+
local_test = LocalTests.PLOT_PNL
|
|
199
|
+
|
|
200
|
+
run_local_test(local_test=local_test)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
illustration of backtest using QuantInvestStrats (qis) reporting analytics
|
|
3
|
+
"""
|
|
4
|
+
import qis as qis
|
|
5
|
+
from trendfollowing.systems.european import run_european_tf_system
|
|
6
|
+
from trendfollowing.universe import load_data
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
if __name__ == '__main__':
|
|
10
|
+
# load universe prices and volume costs
|
|
11
|
+
prices, volume_costs, benchmark_prices, descriptive_df, group_order = load_data()
|
|
12
|
+
# compute weights of the trend-following strategy
|
|
13
|
+
backtest_outputs = run_european_tf_system(prices=prices,
|
|
14
|
+
long_span=250,
|
|
15
|
+
short_span=20,
|
|
16
|
+
vol_span=33,
|
|
17
|
+
vol_target=0.0035,
|
|
18
|
+
portfolio_covar_span=250,
|
|
19
|
+
portfolio_target_vol=0.15,
|
|
20
|
+
volume_costs=volume_costs)
|
|
21
|
+
# compute executions of system portfolio
|
|
22
|
+
system_portfolio = qis.backtest_model_portfolio(prices=prices,
|
|
23
|
+
weights=backtest_outputs.weights)
|
|
24
|
+
# generate report
|
|
25
|
+
figs = qis.generate_strategy_factsheet(portfolio_data=system_portfolio,
|
|
26
|
+
benchmark_prices=benchmark_prices)
|