qis 3.2.5__tar.gz → 3.2.8__tar.gz

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 (147) hide show
  1. {qis-3.2.5 → qis-3.2.8}/PKG-INFO +1 -1
  2. {qis-3.2.5 → qis-3.2.8}/pyproject.toml +1 -1
  3. {qis-3.2.5 → qis-3.2.8}/qis/examples/factsheets/strategy_benchmark.py +4 -2
  4. {qis-3.2.5 → qis-3.2.8}/qis/models/__init__.py +2 -1
  5. {qis-3.2.5 → qis-3.2.8}/qis/models/linear/ewm.py +96 -42
  6. {qis-3.2.5 → qis-3.2.8}/qis/plots/time_series.py +4 -2
  7. {qis-3.2.5 → qis-3.2.8}/qis/portfolio/__init__.py +2 -2
  8. {qis-3.2.5 → qis-3.2.8}/qis/portfolio/portfolio_data.py +39 -225
  9. {qis-3.2.5 → qis-3.2.8}/qis/portfolio/reports/strategy_benchmark_factsheet.py +15 -1
  10. {qis-3.2.5 → qis-3.2.8}/qis/portfolio/reports/strategy_factsheet.py +1 -1
  11. {qis-3.2.5 → qis-3.2.8}/qis/portfolio/reports/strategy_signal_factsheet.py +2 -1
  12. qis-3.2.8/qis/portfolio/signal_data.py +238 -0
  13. {qis-3.2.5 → qis-3.2.8}/LICENSE.txt +0 -0
  14. {qis-3.2.5 → qis-3.2.8}/README.md +0 -0
  15. {qis-3.2.5 → qis-3.2.8}/qis/__init__.py +0 -0
  16. {qis-3.2.5 → qis-3.2.8}/qis/examples/best_returns.py +0 -0
  17. {qis-3.2.5 → qis-3.2.8}/qis/examples/bootstrap_analysis.py +0 -0
  18. {qis-3.2.5 → qis-3.2.8}/qis/examples/boxplot_conditional_returns.py +0 -0
  19. {qis-3.2.5 → qis-3.2.8}/qis/examples/btc_asset_corr.py +0 -0
  20. {qis-3.2.5 → qis-3.2.8}/qis/examples/constant_notional.py +0 -0
  21. {qis-3.2.5 → qis-3.2.8}/qis/examples/constant_weight_portfolios.py +0 -0
  22. {qis-3.2.5 → qis-3.2.8}/qis/examples/core/perf_bbg_prices.py +0 -0
  23. {qis-3.2.5 → qis-3.2.8}/qis/examples/core/price_plots.py +0 -0
  24. {qis-3.2.5 → qis-3.2.8}/qis/examples/core/us_election.py +0 -0
  25. {qis-3.2.5 → qis-3.2.8}/qis/examples/credit_spreads.py +0 -0
  26. {qis-3.2.5 → qis-3.2.8}/qis/examples/europe_futures.py +0 -0
  27. {qis-3.2.5 → qis-3.2.8}/qis/examples/factsheets/multi_assets.py +0 -0
  28. {qis-3.2.5 → qis-3.2.8}/qis/examples/factsheets/multi_strategy.py +0 -0
  29. {qis-3.2.5 → qis-3.2.8}/qis/examples/factsheets/pyblogs_reports.py +0 -0
  30. {qis-3.2.5 → qis-3.2.8}/qis/examples/factsheets/strategy.py +0 -0
  31. {qis-3.2.5 → qis-3.2.8}/qis/examples/generate_option_rolls.py +0 -0
  32. {qis-3.2.5 → qis-3.2.8}/qis/examples/interpolation_infrequent_returns.py +0 -0
  33. {qis-3.2.5 → qis-3.2.8}/qis/examples/leveraged_strategies.py +0 -0
  34. {qis-3.2.5 → qis-3.2.8}/qis/examples/long_short.py +0 -0
  35. {qis-3.2.5 → qis-3.2.8}/qis/examples/momentum_indices.py +0 -0
  36. {qis-3.2.5 → qis-3.2.8}/qis/examples/ohlc_vol_analysis.py +0 -0
  37. {qis-3.2.5 → qis-3.2.8}/qis/examples/overnight_returns.py +0 -0
  38. {qis-3.2.5 → qis-3.2.8}/qis/examples/perf_external_assets.py +0 -0
  39. {qis-3.2.5 → qis-3.2.8}/qis/examples/readme_performances.py +0 -0
  40. {qis-3.2.5 → qis-3.2.8}/qis/examples/risk_return_frontier.py +0 -0
  41. {qis-3.2.5 → qis-3.2.8}/qis/examples/rolling_performance.py +0 -0
  42. {qis-3.2.5 → qis-3.2.8}/qis/examples/seasonality.py +0 -0
  43. {qis-3.2.5 → qis-3.2.8}/qis/examples/sharpe_vs_sortino.py +0 -0
  44. {qis-3.2.5 → qis-3.2.8}/qis/examples/simulate_quant_strats.py +0 -0
  45. {qis-3.2.5 → qis-3.2.8}/qis/examples/test_ewm.py +0 -0
  46. {qis-3.2.5 → qis-3.2.8}/qis/examples/test_scatter.py +0 -0
  47. {qis-3.2.5 → qis-3.2.8}/qis/examples/try_pybloqs.py +0 -0
  48. {qis-3.2.5 → qis-3.2.8}/qis/examples/universe_corrs.py +0 -0
  49. {qis-3.2.5 → qis-3.2.8}/qis/examples/vix_beta_to_equities_bonds.py +0 -0
  50. {qis-3.2.5 → qis-3.2.8}/qis/examples/vix_conditional_returns.py +0 -0
  51. {qis-3.2.5 → qis-3.2.8}/qis/examples/vix_spy_by_year.py +0 -0
  52. {qis-3.2.5 → qis-3.2.8}/qis/examples/vix_tenor_analysis.py +0 -0
  53. {qis-3.2.5 → qis-3.2.8}/qis/examples/vol_without_weekends.py +0 -0
  54. {qis-3.2.5 → qis-3.2.8}/qis/file_utils.py +0 -0
  55. {qis-3.2.5 → qis-3.2.8}/qis/local_path.py +0 -0
  56. {qis-3.2.5 → qis-3.2.8}/qis/models/README.md +0 -0
  57. {qis-3.2.5 → qis-3.2.8}/qis/models/linear/__init__.py +0 -0
  58. {qis-3.2.5 → qis-3.2.8}/qis/models/linear/auto_corr.py +0 -0
  59. {qis-3.2.5 → qis-3.2.8}/qis/models/linear/corr_cov_matrix.py +0 -0
  60. {qis-3.2.5 → qis-3.2.8}/qis/models/linear/ewm_convolution.py +0 -0
  61. {qis-3.2.5 → qis-3.2.8}/qis/models/linear/ewm_factors.py +0 -0
  62. {qis-3.2.5 → qis-3.2.8}/qis/models/linear/ewm_winsor_outliers.py +0 -0
  63. {qis-3.2.5 → qis-3.2.8}/qis/models/linear/pca.py +0 -0
  64. {qis-3.2.5 → qis-3.2.8}/qis/models/linear/plot_correlations.py +0 -0
  65. {qis-3.2.5 → qis-3.2.8}/qis/models/linear/ra_returns.py +0 -0
  66. {qis-3.2.5 → qis-3.2.8}/qis/models/stats/__init__.py +0 -0
  67. {qis-3.2.5 → qis-3.2.8}/qis/models/stats/bootstrap.py +0 -0
  68. {qis-3.2.5 → qis-3.2.8}/qis/models/stats/ohlc_vol.py +0 -0
  69. {qis-3.2.5 → qis-3.2.8}/qis/models/stats/rolling_stats.py +0 -0
  70. {qis-3.2.5 → qis-3.2.8}/qis/models/stats/test_bootstrap.py +0 -0
  71. {qis-3.2.5 → qis-3.2.8}/qis/perfstats/README.md +0 -0
  72. {qis-3.2.5 → qis-3.2.8}/qis/perfstats/__init__.py +0 -0
  73. {qis-3.2.5 → qis-3.2.8}/qis/perfstats/cond_regression.py +0 -0
  74. {qis-3.2.5 → qis-3.2.8}/qis/perfstats/config.py +0 -0
  75. {qis-3.2.5 → qis-3.2.8}/qis/perfstats/desc_table.py +0 -0
  76. {qis-3.2.5 → qis-3.2.8}/qis/perfstats/fx_ops.py +0 -0
  77. {qis-3.2.5 → qis-3.2.8}/qis/perfstats/perf_stats.py +0 -0
  78. {qis-3.2.5 → qis-3.2.8}/qis/perfstats/regime_classifier.py +0 -0
  79. {qis-3.2.5 → qis-3.2.8}/qis/perfstats/returns.py +0 -0
  80. {qis-3.2.5 → qis-3.2.8}/qis/perfstats/timeseries_bfill.py +0 -0
  81. {qis-3.2.5 → qis-3.2.8}/qis/plots/README.md +0 -0
  82. {qis-3.2.5 → qis-3.2.8}/qis/plots/__init__.py +0 -0
  83. {qis-3.2.5 → qis-3.2.8}/qis/plots/bars.py +0 -0
  84. {qis-3.2.5 → qis-3.2.8}/qis/plots/boxplot.py +0 -0
  85. {qis-3.2.5 → qis-3.2.8}/qis/plots/contour.py +0 -0
  86. {qis-3.2.5 → qis-3.2.8}/qis/plots/derived/__init__.py +0 -0
  87. {qis-3.2.5 → qis-3.2.8}/qis/plots/derived/data_timeseries.py +0 -0
  88. {qis-3.2.5 → qis-3.2.8}/qis/plots/derived/desc_table.py +0 -0
  89. {qis-3.2.5 → qis-3.2.8}/qis/plots/derived/drawdowns.py +0 -0
  90. {qis-3.2.5 → qis-3.2.8}/qis/plots/derived/perf_table.py +0 -0
  91. {qis-3.2.5 → qis-3.2.8}/qis/plots/derived/prices.py +0 -0
  92. {qis-3.2.5 → qis-3.2.8}/qis/plots/derived/regime_class_table.py +0 -0
  93. {qis-3.2.5 → qis-3.2.8}/qis/plots/derived/regime_data.py +0 -0
  94. {qis-3.2.5 → qis-3.2.8}/qis/plots/derived/regime_pdf.py +0 -0
  95. {qis-3.2.5 → qis-3.2.8}/qis/plots/derived/regime_scatter.py +0 -0
  96. {qis-3.2.5 → qis-3.2.8}/qis/plots/derived/returns_heatmap.py +0 -0
  97. {qis-3.2.5 → qis-3.2.8}/qis/plots/derived/returns_scatter.py +0 -0
  98. {qis-3.2.5 → qis-3.2.8}/qis/plots/errorbar.py +0 -0
  99. {qis-3.2.5 → qis-3.2.8}/qis/plots/heatmap.py +0 -0
  100. {qis-3.2.5 → qis-3.2.8}/qis/plots/histogram.py +0 -0
  101. {qis-3.2.5 → qis-3.2.8}/qis/plots/histplot2d.py +0 -0
  102. {qis-3.2.5 → qis-3.2.8}/qis/plots/lineplot.py +0 -0
  103. {qis-3.2.5 → qis-3.2.8}/qis/plots/pie.py +0 -0
  104. {qis-3.2.5 → qis-3.2.8}/qis/plots/qqplot.py +0 -0
  105. {qis-3.2.5 → qis-3.2.8}/qis/plots/reports/__init__.py +0 -0
  106. {qis-3.2.5 → qis-3.2.8}/qis/plots/reports/econ_data_single.py +0 -0
  107. {qis-3.2.5 → qis-3.2.8}/qis/plots/reports/gantt_data_history.py +0 -0
  108. {qis-3.2.5 → qis-3.2.8}/qis/plots/reports/price_history.py +0 -0
  109. {qis-3.2.5 → qis-3.2.8}/qis/plots/reports/utils.py +0 -0
  110. {qis-3.2.5 → qis-3.2.8}/qis/plots/scatter.py +0 -0
  111. {qis-3.2.5 → qis-3.2.8}/qis/plots/stackplot.py +0 -0
  112. {qis-3.2.5 → qis-3.2.8}/qis/plots/table.py +0 -0
  113. {qis-3.2.5 → qis-3.2.8}/qis/plots/utils.py +0 -0
  114. {qis-3.2.5 → qis-3.2.8}/qis/portfolio/README.md +0 -0
  115. {qis-3.2.5 → qis-3.2.8}/qis/portfolio/backtester.py +0 -0
  116. {qis-3.2.5 → qis-3.2.8}/qis/portfolio/ewm_portfolio_risk.py +0 -0
  117. {qis-3.2.5 → qis-3.2.8}/qis/portfolio/multi_portfolio_data.py +0 -0
  118. {qis-3.2.5 → qis-3.2.8}/qis/portfolio/reports/__init__.py +0 -0
  119. {qis-3.2.5 → qis-3.2.8}/qis/portfolio/reports/brinson_attribution.py +0 -0
  120. {qis-3.2.5 → qis-3.2.8}/qis/portfolio/reports/config.py +0 -0
  121. {qis-3.2.5 → qis-3.2.8}/qis/portfolio/reports/multi_assets_factsheet.py +0 -0
  122. {qis-3.2.5 → qis-3.2.8}/qis/portfolio/reports/multi_strategy_factseet_pybloqs.py +0 -0
  123. {qis-3.2.5 → qis-3.2.8}/qis/portfolio/reports/multi_strategy_factsheet.py +0 -0
  124. {qis-3.2.5 → qis-3.2.8}/qis/portfolio/reports/strategy_benchmark_factsheet_pybloqs.py +0 -0
  125. {qis-3.2.5 → qis-3.2.8}/qis/portfolio/strats/__init__.py +0 -0
  126. {qis-3.2.5 → qis-3.2.8}/qis/portfolio/strats/quant_strats_delta1.py +0 -0
  127. {qis-3.2.5 → qis-3.2.8}/qis/portfolio/strats/seasonal_strats.py +0 -0
  128. {qis-3.2.5 → qis-3.2.8}/qis/settings.yaml +0 -0
  129. {qis-3.2.5 → qis-3.2.8}/qis/sql_engine.py +0 -0
  130. {qis-3.2.5 → qis-3.2.8}/qis/test_data.py +0 -0
  131. {qis-3.2.5 → qis-3.2.8}/qis/utils/README.md +0 -0
  132. {qis-3.2.5 → qis-3.2.8}/qis/utils/__init__.py +0 -0
  133. {qis-3.2.5 → qis-3.2.8}/qis/utils/dates.py +0 -0
  134. {qis-3.2.5 → qis-3.2.8}/qis/utils/df_agg.py +0 -0
  135. {qis-3.2.5 → qis-3.2.8}/qis/utils/df_cut.py +0 -0
  136. {qis-3.2.5 → qis-3.2.8}/qis/utils/df_freq.py +0 -0
  137. {qis-3.2.5 → qis-3.2.8}/qis/utils/df_groups.py +0 -0
  138. {qis-3.2.5 → qis-3.2.8}/qis/utils/df_melt.py +0 -0
  139. {qis-3.2.5 → qis-3.2.8}/qis/utils/df_ops.py +0 -0
  140. {qis-3.2.5 → qis-3.2.8}/qis/utils/df_str.py +0 -0
  141. {qis-3.2.5 → qis-3.2.8}/qis/utils/df_to_scores.py +0 -0
  142. {qis-3.2.5 → qis-3.2.8}/qis/utils/df_to_weights.py +0 -0
  143. {qis-3.2.5 → qis-3.2.8}/qis/utils/generic.py +0 -0
  144. {qis-3.2.5 → qis-3.2.8}/qis/utils/np_ops.py +0 -0
  145. {qis-3.2.5 → qis-3.2.8}/qis/utils/ols.py +0 -0
  146. {qis-3.2.5 → qis-3.2.8}/qis/utils/sampling.py +0 -0
  147. {qis-3.2.5 → qis-3.2.8}/qis/utils/struct_ops.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: qis
3
- Version: 3.2.5
3
+ Version: 3.2.8
4
4
  Summary: Implementation of visualisation and reporting analytics for Quantitative Investment Strategies
5
5
  License: LICENSE.txt
6
6
  Keywords: quantitative,investing,portfolio optimization,systematic strategies,volatility
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "qis"
3
- version = "3.2.5"
3
+ version = "3.2.8"
4
4
  description = "Implementation of visualisation and reporting analytics for Quantitative Investment Strategies"
5
5
  license = "LICENSE.txt"
6
6
  authors = ["Artur Sepp <artursepp@gmail.com>"]
@@ -147,10 +147,12 @@ def run_unit_test(unit_test: UnitTests):
147
147
  ac_group_data = multi_portfolio_data.portfolio_datas[0].group_data
148
148
  asset_tickers = multi_portfolio_data.portfolio_datas[0].weights.columns
149
149
  sub_ac_group_data = pd.Series(asset_tickers, index=asset_tickers)
150
-
150
+ turnover_groups = multi_portfolio_data.portfolio_datas[0].group_data
151
+ multi_portfolio_data.portfolio_datas[0].benchmark_prices = multi_portfolio_data.benchmark_prices
151
152
  weights_tracking_error_report_by_ac_subac(multi_portfolio_data=multi_portfolio_data,
152
153
  ac_group_data=ac_group_data,
153
154
  sub_ac_group_data=sub_ac_group_data,
155
+ turnover_groups=turnover_groups,
154
156
  time_period=time_period)
155
157
 
156
158
  plt.show()
@@ -158,7 +160,7 @@ def run_unit_test(unit_test: UnitTests):
158
160
 
159
161
  if __name__ == '__main__':
160
162
 
161
- unit_test = UnitTests.STRATEGY_BENCHMARK_PLT
163
+ unit_test = UnitTests.TRACKING_ERROR
162
164
 
163
165
  is_run_all_tests = False
164
166
  if is_run_all_tests:
@@ -47,7 +47,8 @@ from qis.models.linear.ewm import (
47
47
  compute_roll_mean,
48
48
  compute_rolling_mean_adj,
49
49
  set_init_dim1,
50
- set_init_dim2
50
+ set_init_dim2,
51
+ compute_ewm_covar_newey_west
51
52
  )
52
53
 
53
54
  from qis.models.linear.ewm_convolution import ConvolutionType, SignalAggType, ewm_xy_convolution
@@ -237,6 +237,7 @@ def compute_ewm_long_short_filter(data: Union[pd.DataFrame, pd.Series, np.ndarra
237
237
 
238
238
  @njit
239
239
  def compute_ewm_covar(a: np.ndarray,
240
+ b: np.ndarray = None,
240
241
  span: Union[int, np.ndarray] = None,
241
242
  ewm_lambda: float = 0.94,
242
243
  covar0: np.ndarray = None,
@@ -245,7 +246,13 @@ def compute_ewm_covar(a: np.ndarray,
245
246
  ) -> np.ndarray:
246
247
  """
247
248
  compute ewm covariance matrix
249
+ b is optional, when given the covar is cross product a and b
248
250
  """
251
+ if b is None:
252
+ b = a
253
+ else:
254
+ assert a.shape[0] == b.shape[0]
255
+ assert a.shape[1] == b.shape[1]
249
256
 
250
257
  if span is not None:
251
258
  ewm_lambda = 1.0 - 2.0 / (span + 1.0)
@@ -263,7 +270,7 @@ def compute_ewm_covar(a: np.ndarray,
263
270
 
264
271
  last_covar = covar
265
272
  if a.ndim == 1: # ndarry array
266
- r_ij = np.outer(a, a)
273
+ r_ij = np.outer(a, b)
267
274
  covar = ewm_lambda_1 * r_ij + ewm_lambda * last_covar
268
275
  if nan_backfill == NanBackfill.FFILL:
269
276
  fill_value = last_covar
@@ -277,8 +284,7 @@ def compute_ewm_covar(a: np.ndarray,
277
284
  else: # loop over rows
278
285
  t = a.shape[0]
279
286
  for idx in range(0, t): # row in x:
280
- row = a[idx]
281
- r_ij = np.outer(row, row)
287
+ r_ij = np.outer(a[idx], b[idx])
282
288
  covar = ewm_lambda_1 * r_ij + ewm_lambda * last_covar
283
289
 
284
290
  if nan_backfill == NanBackfill.FFILL:
@@ -290,21 +296,59 @@ def compute_ewm_covar(a: np.ndarray,
290
296
 
291
297
  last_covar = np.where(np.isfinite(covar), covar, fill_value)
292
298
 
293
- if is_corr:
294
- if np.nansum(np.diag(last_covar)) > 1e-10:
295
- inv_vol = np.reciprocal(np.sqrt(np.diag(last_covar)))
296
- norm = np.outer(inv_vol, inv_vol)
297
- else:
298
- norm = np.identity(n)
299
- last_covar_ = norm * last_covar
299
+ # for covar normalise
300
+ if is_corr:
301
+ if np.nansum(np.diag(last_covar)) > 1e-10:
302
+ inv_vol = np.reciprocal(np.sqrt(np.diag(last_covar)))
303
+ norm = np.outer(inv_vol, inv_vol)
300
304
  else:
301
- last_covar_ = last_covar
302
-
303
- covar = last_covar_
305
+ norm = np.identity(n)
306
+ covar = norm * last_covar
307
+ else:
308
+ covar = last_covar
304
309
 
305
310
  return covar
306
311
 
307
312
 
313
+ @njit
314
+ def compute_ewm_covar_newey_west(a: np.ndarray,
315
+ num_lags: int = 2,
316
+ span: Union[int, np.ndarray] = None,
317
+ ewm_lambda: float = 0.94,
318
+ covar0: np.ndarray = None,
319
+ is_corr: bool = False,
320
+ nan_backfill: NanBackfill = NanBackfill.FFILL
321
+ ) -> np.ndarray:
322
+ """
323
+ implementation of newey west covar estimator
324
+ """
325
+ ewm0 = compute_ewm_covar(a=a, span=span, ewm_lambda=ewm_lambda, covar0=covar0, is_corr=False, nan_backfill=nan_backfill)
326
+ # compute m recursions
327
+ if num_lags > 0:
328
+ nw_adjustment = np.zeros_like(ewm0)
329
+ for m in np.arange(1, num_lags+1):
330
+ # lagged value
331
+ a_m = np.empty_like(a)
332
+ a_m[m:] = a[:-m]
333
+ a_m[:m] = np.nan
334
+ ewm_m1 = compute_ewm_covar(a=a, b=a_m, span=span)
335
+ # ewm_m2 = compute_ewm_covar(a=a_m, b=a, span=span)
336
+ nw_adjustment += (1.0-m/(num_lags+1))*(ewm_m1 + np.transpose(ewm_m1))
337
+ ewm_nw = ewm0 + nw_adjustment
338
+ else:
339
+ ewm_nw = ewm0
340
+
341
+ if is_corr:
342
+ if np.nansum(np.diag(ewm_nw)) > 1e-10:
343
+ inv_vol = np.reciprocal(np.sqrt(np.diag(ewm_nw)))
344
+ norm = np.outer(inv_vol, inv_vol)
345
+ else:
346
+ norm = np.identity(a.shape[1])
347
+ ewm_nw = norm * ewm_nw
348
+
349
+ return ewm_nw
350
+
351
+
308
352
  @njit
309
353
  def compute_ewm_covar_tensor(a: np.ndarray,
310
354
  span: Union[int, np.ndarray] = None,
@@ -663,6 +707,27 @@ def compute_ewm_vol(data: Union[pd.DataFrame, pd.Series, np.ndarray],
663
707
  return ewm
664
708
 
665
709
 
710
+ @njit
711
+ def matrix_recursion(a: np.ndarray,
712
+ a_m: np.ndarray,
713
+ span: Optional[Union[float, np.ndarray]] = None,
714
+ ewm_lambda: Union[float, np.ndarray] = 0.94
715
+ ) -> np.ndarray:
716
+ if span is not None:
717
+ ewm_lambda = 1.0 - 2.0 / (span + 1.0)
718
+ ewm_lambda_1 = 1.0 - ewm_lambda
719
+ t = a.shape[0]
720
+ last_covar = np.zeros((a.shape[1], a.shape[1]))
721
+ ewm_m = np.zeros_like(a_m)
722
+ for idx in range(0, t):
723
+ r_ij = np.outer(a[idx], a_m[idx])
724
+ covar = ewm_lambda_1 * r_ij + ewm_lambda * last_covar
725
+ fill_value = last_covar
726
+ last_covar = np.where(np.isfinite(covar), covar, fill_value)
727
+ ewm_m[idx, :] = np.diag(last_covar) + np.diag(np.transpose(last_covar))
728
+ return ewm_m
729
+
730
+
666
731
  def compute_ewm_newey_west_vol(data: Union[pd.DataFrame, pd.Series, np.ndarray],
667
732
  num_lags: int = 2,
668
733
  span: Optional[Union[float, np.ndarray]] = None,
@@ -704,36 +769,25 @@ def compute_ewm_newey_west_vol(data: Union[pd.DataFrame, pd.Series, np.ndarray],
704
769
  if isinstance(init_value, np.ndarray):
705
770
  init_value = float(init_value)
706
771
 
707
- if span is not None:
708
- ewm_lambda = 1.0 - 2.0 / (span + 1.0)
709
- ewm_lambda_1 = 1.0 - ewm_lambda
710
-
711
- def matrix_recursion(a_m: np.ndarray) -> np.ndarray:
712
- t = a.shape[0]
713
- last_covar = np.zeros((a.shape[1], a.shape[1]))
714
- ewm_m = np.zeros_like(a_m)
715
- for idx in range(0, t):
716
- r_ij = np.outer(a[idx], a_m[idx])
717
- covar = ewm_lambda_1 * r_ij + ewm_lambda * last_covar
718
- fill_value = last_covar
719
- last_covar = np.where(np.isfinite(covar), covar, fill_value)
720
- ewm_m[idx, :] = np.diag(last_covar) + np.diag(np.transpose(last_covar))
721
- return ewm_m
722
-
723
772
  ewm0 = ewm_recursion(a=np.square(a), ewm_lambda=ewm_lambda, init_value=init_value, nan_backfill=nan_backfill)
724
- nw_adjustment = np.zeros_like(ewm0)
725
- # compute m recursions
726
- for m in np.arange(1, num_lags+1):
727
- # lagged value
728
- a_m = np.empty_like(a)
729
- a_m[m:] = a[:-m]
730
- a_m[:m] = np.nan
731
- # qqq
732
- ewm_m = matrix_recursion(a_m=a_m)
733
- nw_adjustment += (1.0-m/(num_lags+1))*ewm_m
734
-
735
- ewm_nw = ewm0 + nw_adjustment
736
- nw_ratio = np.divide(ewm_nw, ewm0, where=ewm0 > 0.0)
773
+
774
+ if num_lags == 0:
775
+ ewm_nw = ewm0
776
+ nw_ratio = np.ones_like(ewm0)
777
+ else:
778
+ nw_adjustment = np.zeros_like(ewm0)
779
+ # compute m recursions
780
+ for m in np.arange(1, num_lags+1):
781
+ # lagged value
782
+ a_m = np.empty_like(a)
783
+ a_m[m:] = a[:-m]
784
+ a_m[:m] = np.nan
785
+ ewm_m = matrix_recursion(a=a, a_m=a_m, span=span)
786
+ nw_adjustment += (1.0-m/(num_lags+1))*ewm_m
787
+
788
+ ewm_nw = ewm0 + nw_adjustment
789
+ nw_ratio = np.divide(ewm_nw, ewm0, where=ewm0 > 0.0)
790
+ nw_ratio = np.where(nw_ratio > 0.0, nw_ratio, 1.0)
737
791
 
738
792
  if warmup_period is not None: # set to nan first nonnan in warmup_period
739
793
  ewm_nw = npo.set_nans_for_warmup_period(a=ewm_nw, warmup_period=warmup_period)
@@ -345,6 +345,7 @@ def plot_time_series_2ax(df1: Union[pd.Series, pd.DataFrame],
345
345
  is_log=is_logs[0],
346
346
  y_limits=y_limits,
347
347
  ylabel=ylabel1,
348
+ fontsize=fontsize,
348
349
  ax=ax,
349
350
  **kwargs)
350
351
 
@@ -358,16 +359,17 @@ def plot_time_series_2ax(df1: Union[pd.Series, pd.DataFrame],
358
359
  is_log=is_logs[1],
359
360
  y_limits=y_limits_ax2,
360
361
  ylabel=ylabel2,
362
+ fontsize=fontsize,
361
363
  ax=ax_twin,
362
364
  **kwargs)
363
365
 
366
+ ax.tick_params(axis='x', which='both', bottom=False)
367
+
364
368
  put.set_ax_ticks_format(ax=ax, fontsize=fontsize, xvar_format=None, yvar_format=var_format, set_ticks=False,
365
369
  yvar_major_ticks=yvar_major_ticks1, x_rotation=x_rotation, **kwargs)
366
370
  put.set_ax_ticks_format(ax=ax_twin, fontsize=fontsize, xvar_format=None, yvar_format=var_format_yax2, set_ticks=False,
367
371
  yvar_major_ticks=yvar_major_ticks2, x_rotation=x_rotation, **kwargs)
368
372
 
369
- ax.tick_params(axis='x', which='both', bottom=False)
370
-
371
373
  if legend_loc is not None:
372
374
  if legend_labels is None:
373
375
  df1.columns = [f"{x} (left)" for x in df1.columns]
@@ -2,8 +2,8 @@
2
2
  from qis.portfolio.portfolio_data import (PortfolioData,
3
3
  PortfolioInput,
4
4
  AttributionMetric,
5
- SnapshotPeriod,
6
- StrategySignalData)
5
+ SnapshotPeriod)
6
+ from qis.portfolio.signal_data import StrategySignalData
7
7
 
8
8
  from qis.portfolio.multi_portfolio_data import MultiPortfolioData
9
9
 
@@ -6,8 +6,7 @@ import pandas as pd
6
6
  import seaborn as sns
7
7
  import matplotlib.pyplot as plt
8
8
  from numba import njit
9
- from dataclasses import dataclass, asdict
10
- from statsmodels.regression.linear_model import RegressionResults as RegModel
9
+ from dataclasses import dataclass
11
10
  from typing import Union, Dict, Any, Optional, Tuple, List
12
11
  from enum import Enum
13
12
 
@@ -28,6 +27,7 @@ import qis.plots.derived.returns_scatter as prs
28
27
  import qis.plots.derived.returns_heatmap as rhe
29
28
  import qis.models.linear.ewm_factors as ef
30
29
  from qis.models.linear.ewm import compute_ewm_vol
30
+ from qis.portfolio.signal_data import StrategySignalData
31
31
  from qis.portfolio.ewm_portfolio_risk import compute_portfolio_vol, compute_portfolio_risk_contributions
32
32
 
33
33
 
@@ -347,7 +347,8 @@ class PortfolioData:
347
347
  add_total: bool = True,
348
348
  vol_span: int = 33,
349
349
  freq: Optional[str] = None,
350
- is_unit_based_traded_volume: bool = True
350
+ is_unit_based_traded_volume: bool = True,
351
+ **kwargs
351
352
  ) -> Union[pd.DataFrame, pd.Series]:
352
353
 
353
354
  if is_unit_based_traded_volume: # for unit generated backtest
@@ -1476,228 +1477,41 @@ class PortfolioData:
1476
1477
  ax=axs[1],
1477
1478
  **kwargs)
1478
1479
 
1479
-
1480
- @dataclass
1481
- class StrategySignalData:
1482
- """
1483
- data class instance applied for output of strategy backtest data
1484
- """
1485
- log_returns: pd.DataFrame = None
1486
- ra_carry: pd.DataFrame = None # risk-adjusted carry
1487
- momentum: pd.DataFrame = None
1488
- signal: pd.DataFrame = None # signal output
1489
- instrument_vols: pd.DataFrame = None # instrument vols
1490
- instrument_target_vols: pd.DataFrame = None # target vols for portfolio allocation
1491
- instrument_target_signal_vol_weights: pd.DataFrame = None # target vols * signal
1492
- instrument_portfolio_leverages: pd.DataFrame = None # = portfolio weight / instrument_target_signal_vol_weights
1493
- weights: pd.DataFrame = None # final weights
1494
- kwargs: Dict[str, pd.DataFrame] = None # any other outputs
1495
-
1496
- def locate_period(self, time_period: TimePeriod) -> StrategySignalData:
1497
- # nb: does not work for returns
1498
- data_dict = asdict(self)
1499
- for key, df in data_dict.items():
1500
- if df is not None:
1501
- data_dict[key] = time_period.locate(df)
1502
- return StrategySignalData(**data_dict)
1503
-
1504
- def rename_data(self, names_map: Dict[str, str]) -> StrategySignalData:
1505
- data_dict = asdict(self)
1506
- for key, df in data_dict.items():
1507
- if df is not None:
1508
- data_dict[key] = df.rename(names_map, axis=1)
1509
- return StrategySignalData(**data_dict)
1510
-
1511
- def get_current_signal_by_groups(self, group_data: pd.Series,
1512
- group_order: List[str] = None
1513
- ) -> Dict[str, pd.DataFrame]:
1514
- group_dict = dfg.get_group_dict(group_data=group_data,
1515
- group_order=group_order,
1516
- total_column=None)
1517
- group_signals = {}
1518
- agg_by_group = {}
1519
- last_date = qis.date_to_str(self.signal.index[-21])
1520
- current_date = qis.date_to_str(self.signal.index[-1])
1521
- last_signals = self.signal.iloc[-21, :]
1522
- current_signals = self.signal.iloc[-1, :]
1523
- for group, tickers in group_dict.items():
1524
- last_signals_ = last_signals[tickers]
1525
- current_signals_ = current_signals[tickers]
1526
- group_signals[group] = pd.concat([last_signals_.rename(last_date),
1527
- current_signals_.rename(current_date)
1528
- ], axis=1)
1529
-
1530
- agg_by_group[group] = pd.Series({last_date: np.nanmean(last_signals_),
1531
- current_date: np.nanmean(current_signals_)})
1532
- agg_by_group = {'Total by groups': pd.DataFrame.from_dict(agg_by_group, orient='index')}
1533
- agg_by_group.update(group_signals)
1534
- return agg_by_group
1535
-
1536
- def asdiff(self, tickers: List[str] = None,
1537
- freq: str = None,
1538
- sample_size: Optional[int] = 21, # can use rolling instead for freq
1539
- time_period: TimePeriod = None
1540
- ) -> StrategySignalData:
1541
-
1542
- if tickers is None:
1543
- tickers = self.log_returns.columns.to_list()
1544
- # nb: does not work for returns
1545
- if time_period is not None:
1546
- ssd = self.locate_period(time_period=time_period)
1547
- else:
1548
- ssd = self
1549
-
1550
- data_dict = asdict(ssd)
1551
- if sample_size is not None:
1552
- for key, df in data_dict.items():
1553
- if df is not None:
1554
- data_dict[key] = qis.df_resample_at_int_index(df=df[tickers], func=None, sample_size=sample_size).diff()
1555
- else:
1556
- for key, df in data_dict.items():
1557
- if df is not None:
1558
- data_dict[key] = qis.df_resample_at_freq(df=df[tickers], freq=freq, include_end_date=True).diff()
1559
- return StrategySignalData(**data_dict)
1560
-
1561
- def estimate_signal_changes_joint(self,
1562
- tickers: List[str] = None,
1563
- freq: Optional[str] = None,
1564
- sample_size: Optional[int] = 21, # can use rolling instead for freq
1565
- time_period: TimePeriod = None
1566
- ) -> Tuple[pd.DataFrame, RegModel, TimePeriod]:
1567
- if tickers is None:
1568
- tickers = self.log_returns.columns.to_list()
1569
- ssd = self.asdiff(tickers=tickers, sample_size=sample_size, freq=freq, time_period=time_period)
1570
- y_var_name = 'weight_change'
1571
- y = qis.melt_df_by_columns(ssd.weights.iloc[:-1, :], y_var_name=y_var_name)[y_var_name]
1572
- x_var_name1 = 'momentum_change'
1573
- x1 = qis.melt_df_by_columns(ssd.momentum.iloc[:-1, :], y_var_name=x_var_name1)[x_var_name1]
1574
- x_var_name2 = 'target_vol_change'
1575
- x2 = qis.melt_df_by_columns(ssd.instrument_target_vols.iloc[:-1, :], y_var_name=x_var_name2)[x_var_name2]
1576
- x_var_name3 = 'port_leverage_change'
1577
- x3 = qis.melt_df_by_columns(ssd.instrument_portfolio_leverages.iloc[:-1, :], y_var_name=x_var_name3)[x_var_name3]
1578
- if self.ra_carry is not None:
1579
- x_var_name0 = 'carry_change'
1580
- x0 = qis.melt_df_by_columns(ssd.ra_carry.iloc[:-1, :], y_var_name=x_var_name0)[x_var_name0]
1581
- x = pd.concat([x0, x1, x2, x3], axis=1).dropna()
1582
- else:
1583
- x = pd.concat([x1, x2, x3], axis=1).dropna()
1584
- x_names = x.columns.to_list()
1585
- y = y.reindex(index=x.index)
1586
-
1587
- # keep last obs for prediction
1588
- fitted_model = qis.fit_ols(x=x.to_numpy(), y=y.to_numpy(), order=1, fit_intercept=False)
1589
- actual_change = ssd.weights.iloc[-1, :]
1590
- predictions = {}
1591
- for ticker in tickers:
1592
- if self.ra_carry is not None:
1593
- x_ts = np.array([ssd.ra_carry[ticker].iloc[-1],
1594
- ssd.momentum[ticker].iloc[-1],
1595
- ssd.instrument_target_vols[ticker].iloc[-1],
1596
- ssd.instrument_portfolio_leverages[ticker].iloc[-1]])
1597
- else:
1598
- x_ts = np.array([ssd.momentum[ticker].iloc[-1],
1599
- ssd.instrument_target_vols[ticker].iloc[-1],
1600
- ssd.instrument_portfolio_leverages[ticker].iloc[-1]])
1601
- pred_t = {}
1602
- total_pred = 0.0
1603
- for idx, x_t in enumerate(x_ts):
1604
- pred_x = fitted_model.params[idx] * x_t
1605
- total_pred += pred_x
1606
- pred_t[x_names[idx]] = pred_x
1607
- pred_t['predicted'] = total_pred
1608
- pred_t['actual'] = actual_change[ticker]
1609
- pred_t['residual'] = actual_change[ticker] - total_pred
1610
- pred_t['residual %'] = total_pred / actual_change[ticker]
1611
- pred_t['r2'] = fitted_model.rsquared
1612
- predictions[ticker] = pd.Series(pred_t)
1613
-
1614
- predictions = pd.DataFrame.from_dict(predictions, orient='index')
1615
- prediction_period = TimePeriod(start=ssd.weights.index[-2], end=ssd.weights.index[-1])
1616
-
1617
- return predictions, fitted_model, prediction_period
1618
-
1619
- def estimate_signal_changes_by_groups(self,
1620
- group_data: pd.Series, group_order: List[str] = None,
1621
- freq: Optional[str] = None,
1622
- sample_size: Optional[int] = 21, # can use rolling instead for freq
1623
- time_period: TimePeriod = None
1624
- ) -> Tuple[Dict[str, pd.DataFrame], Dict[str, RegModel], TimePeriod]:
1625
- """
1626
- estimate weight change for groups
1627
- """
1628
- group_dict = dfg.get_group_dict(group_data=group_data,
1629
- group_order=group_order,
1630
- total_column=None)
1631
- predictions = {}
1632
- fitted_models = {}
1633
- prediction_period = None
1634
- for group, tickers in group_dict.items():
1635
- prediction, fitted_model, prediction_period = self.estimate_signal_changes_joint(
1636
- tickers=tickers, freq=freq,
1637
- sample_size=sample_size,
1638
- time_period=time_period)
1639
- predictions[group] = prediction
1640
- fitted_models[group] = fitted_model
1641
- return predictions, fitted_models, prediction_period
1642
-
1643
- def estimate_signal_changes_individual(self,
1644
- tickers: List[str] = None,
1645
- freq: Optional[str] = None,
1646
- sample_size: Optional[int] = 21, # can use rolling instead for freq
1647
- time_period: TimePeriod = None
1648
- ) -> Tuple[pd.DataFrame, Dict[str, RegModel]]:
1649
- if tickers is None:
1650
- tickers = self.log_returns.columns.to_list()
1651
- ssd = self.asdiff(tickers=tickers, sample_size=sample_size, freq=freq, time_period=time_period)
1652
- y_var_name = 'weight_change'
1653
- y = ssd.weights
1654
- x_var_name1 = 'momentum_change'
1655
- x1 = ssd.momentum
1656
- x_var_name2 = 'target_vol_change'
1657
- x2 = ssd.instrument_target_vols
1658
- x_var_name3 = 'port_leverage_change'
1659
- x3 = ssd.instrument_portfolio_leverages
1660
- if self.ra_carry is not None:
1661
- x_var_name0 = 'carry_change'
1662
- x0 = ssd.ra_carry
1663
- x_names = [x_var_name0, x_var_name1, x_var_name2, x_var_name3]
1664
- else:
1665
- x_names = [x_var_name1, x_var_name2, x_var_name3]
1666
-
1667
- predictions = {}
1668
- fitted_models = {}
1669
- for ticker in tickers:
1670
- if self.ra_carry is not None:
1671
- x = pd.concat([x0[ticker].rename(x_var_name0),
1672
- x1[ticker].rename(x_var_name1),
1673
- x2[ticker].rename(x_var_name2),
1674
- x3[ticker].rename(x_var_name3)], axis=1)
1675
- else:
1676
- x = pd.concat([x1[ticker].rename(x_var_name1),
1677
- x2[ticker].rename(x_var_name2),
1678
- x3[ticker].rename(x_var_name3)], axis=1)
1679
-
1680
- # keep last obs for prediction
1681
- fitted_model = qis.fit_ols(x=x.iloc[:-1, :].to_numpy(), y=y[ticker].iloc[:-1].to_numpy(), order=1, fit_intercept=False)
1682
- actual_change = y[ticker].iloc[-1]
1683
- x_ts = x.iloc[-1, :].to_numpy()
1684
- pred_t = {}
1685
- total_pred = 0.0
1686
- for idx, x_t in enumerate(x_ts):
1687
- pred_x = fitted_model.params[idx] * x_t
1688
- total_pred += pred_x
1689
- pred_t[x_names[idx]] = pred_x
1690
- pred_t['predicted'] = total_pred
1691
- pred_t['actual'] = actual_change
1692
- pred_t['residual'] = actual_change - total_pred
1693
- pred_t['residual %'] = total_pred / actual_change
1694
- pred_t['r2'] = fitted_model.rsquared
1695
- predictions[ticker] = pd.Series(pred_t)
1696
- fitted_models[ticker] = fitted_model
1697
-
1698
- predictions = pd.DataFrame.from_dict(predictions, orient='index')
1699
-
1700
- return predictions, fitted_models
1480
+ def plot_turnover(self,
1481
+ regime_benchmark: str = None,
1482
+ is_agg: bool = False,
1483
+ is_grouped: bool = False,
1484
+ group_data: pd.Series = None,
1485
+ group_order: List[str] = None,
1486
+ time_period: TimePeriod = None,
1487
+ roll_period: Optional[int] = 260,
1488
+ add_total: bool = True,
1489
+ title: str = None,
1490
+ freq: Optional[str] = None,
1491
+ regime_params: BenchmarkReturnsQuantileRegimeSpecs = None,
1492
+ ax: plt.Subplot = None,
1493
+ **kwargs
1494
+ ) -> None:
1495
+ turnover = self.get_turnover(is_agg=is_agg,
1496
+ is_grouped=is_grouped,
1497
+ group_data=group_data,
1498
+ group_order=group_order,
1499
+ time_period=time_period,
1500
+ roll_period=roll_period,
1501
+ add_total=add_total,
1502
+ freq=freq,
1503
+ **kwargs)
1504
+ turnover_title = title or f"{roll_period}-period rolling {freq}-freq Turnover"
1505
+ qis.plot_time_series(df=turnover,
1506
+ var_format='{:,.2%}',
1507
+ # y_limits=(0.0, None),
1508
+ legend_stats=qis.LegendStats.AVG_NONNAN_LAST,
1509
+ title=turnover_title,
1510
+ ax=ax,
1511
+ **kwargs)
1512
+ if regime_benchmark is not None:
1513
+ self.add_regime_shadows(ax=ax, regime_benchmark=regime_benchmark, index=turnover.index,
1514
+ regime_params=regime_params)
1701
1515
 
1702
1516
 
1703
1517
  @njit
@@ -746,7 +746,7 @@ def weights_tracking_error_report_by_ac_subac(multi_portfolio_data: MultiPortfol
746
746
 
747
747
  # turnover
748
748
  fig, ax = plt.subplots(1, 1, figsize=figsize, tight_layout=True)
749
- figs['turnover'] = fig
749
+ figs['joint_turnover'] = fig
750
750
  multi_portfolio_data.plot_turnover(ax=ax,
751
751
  time_period=time_period,
752
752
  regime_benchmark=regime_benchmark,
@@ -754,6 +754,20 @@ def weights_tracking_error_report_by_ac_subac(multi_portfolio_data: MultiPortfol
754
754
  #turnover_rolling_period=260,
755
755
  #freq_turnover=None,
756
756
  **kwargs)
757
+ # group turnover
758
+ fig, ax = plt.subplots(1, 1, figsize=figsize, tight_layout=True)
759
+ figs['group_turnover'] = fig
760
+ multi_portfolio_data.portfolio_datas[strategy_idx].plot_turnover(ax=ax,
761
+ time_period=time_period,
762
+ regime_benchmark=regime_benchmark,
763
+ regime_params=regime_params,
764
+ is_grouped=True,
765
+ group_data=turnover_groups,
766
+ group_order=turnover_order,
767
+ add_total=False,
768
+ #turnover_rolling_period=260,
769
+ #freq_turnover=None,
770
+ **kwargs)
757
771
 
758
772
  return figs, dfs
759
773
 
@@ -20,6 +20,7 @@ def generate_strategy_factsheet(portfolio_data: PortfolioData,
20
20
  benchmark_prices: Union[pd.DataFrame, pd.Series],
21
21
  time_period: TimePeriod = None,
22
22
  ytd_attribution_time_period: TimePeriod = qis.get_ytd_time_period(),
23
+ weight_report_time_period: TimePeriod = None,
23
24
  perf_params: PerfParams = PERF_PARAMS,
24
25
  regime_params: BenchmarkReturnsQuantileRegimeSpecs = REGIME_PARAMS,
25
26
  regime_benchmark: str = None, # default is set to benchmark_prices.columns[0]
@@ -39,7 +40,6 @@ def generate_strategy_factsheet(portfolio_data: PortfolioData,
39
40
  figsize: Tuple[float, float] = (8.5, 11.7), # A4 for portrait
40
41
  fontsize: int = 4,
41
42
  weight_change_sample_size: int = 20,
42
- weight_report_time_period: TimePeriod = None,
43
43
  add_current_position_var_risk_sheet: bool = False,
44
44
  add_weights_turnover_sheet: bool = False,
45
45
  add_grouped_exposures: bool = False,
@@ -9,7 +9,8 @@ import seaborn as sns
9
9
  from typing import Tuple, List, Dict, Optional
10
10
  import qis as qis
11
11
  from qis import TimePeriod, BenchmarkReturnsQuantileRegimeSpecs
12
- from qis.portfolio.portfolio_data import PortfolioData, StrategySignalData
12
+ from qis.portfolio.portfolio_data import PortfolioData
13
+ from qis.portfolio.signal_data import StrategySignalData
13
14
 
14
15
 
15
16
  def generate_weight_change_report(portfolio_data: PortfolioData,