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.
Files changed (33) hide show
  1. trendfollowing/__init__.py +37 -0
  2. trendfollowing/analysis/__init__.py +3 -0
  3. trendfollowing/analysis/autocorr_analysis.py +288 -0
  4. trendfollowing/analysis/check_vol_target.py +30 -0
  5. trendfollowing/analysis/signal_plots.py +200 -0
  6. trendfollowing/analysis/simple_backtest.py +26 -0
  7. trendfollowing/analysis/tf_signal_screener.py +196 -0
  8. trendfollowing/analysis/trade_ar_process.py +162 -0
  9. trendfollowing/analytics/__init__.py +28 -0
  10. trendfollowing/analytics/autocorrelation.py +122 -0
  11. trendfollowing/analytics/expected_return.py +169 -0
  12. trendfollowing/analytics/filters.py +43 -0
  13. trendfollowing/analytics/sharpe.py +268 -0
  14. trendfollowing/analytics/sharpe_test.py +95 -0
  15. trendfollowing/analytics/skewness.py +79 -0
  16. trendfollowing/backtests.py +477 -0
  17. trendfollowing/conventions.py +39 -0
  18. trendfollowing/local_path.py +76 -0
  19. trendfollowing/processes/__init__.py +3 -0
  20. trendfollowing/processes/arfima.py +154 -0
  21. trendfollowing/processes/path_engine.py +256 -0
  22. trendfollowing/settings.yaml +16 -0
  23. trendfollowing/systems/__init__.py +3 -0
  24. trendfollowing/systems/american.py +146 -0
  25. trendfollowing/systems/backtest_utils.py +169 -0
  26. trendfollowing/systems/european.py +153 -0
  27. trendfollowing/systems/tsmom.py +99 -0
  28. trendfollowing/universe.py +114 -0
  29. trendfollowing-1.0.4.dist-info/METADATA +417 -0
  30. trendfollowing-1.0.4.dist-info/RECORD +33 -0
  31. trendfollowing-1.0.4.dist-info/WHEEL +5 -0
  32. trendfollowing-1.0.4.dist-info/licenses/LICENSE +674 -0
  33. 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,3 @@
1
+ """
2
+ exploratory research scripts, not part of the paper exhibits
3
+ """
@@ -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)