qis 3.2.7__tar.gz → 3.2.9__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.7 → qis-3.2.9}/PKG-INFO +1 -1
  2. {qis-3.2.7 → qis-3.2.9}/pyproject.toml +1 -1
  3. {qis-3.2.7 → qis-3.2.9}/qis/examples/factsheets/strategy_benchmark.py +4 -2
  4. {qis-3.2.7 → qis-3.2.9}/qis/portfolio/__init__.py +2 -2
  5. {qis-3.2.7 → qis-3.2.9}/qis/portfolio/multi_portfolio_data.py +39 -8
  6. {qis-3.2.7 → qis-3.2.9}/qis/portfolio/portfolio_data.py +40 -225
  7. {qis-3.2.7 → qis-3.2.9}/qis/portfolio/reports/strategy_benchmark_factsheet.py +52 -20
  8. {qis-3.2.7 → qis-3.2.9}/qis/portfolio/reports/strategy_signal_factsheet.py +2 -1
  9. qis-3.2.9/qis/portfolio/signal_data.py +238 -0
  10. {qis-3.2.7 → qis-3.2.9}/LICENSE.txt +0 -0
  11. {qis-3.2.7 → qis-3.2.9}/README.md +0 -0
  12. {qis-3.2.7 → qis-3.2.9}/qis/__init__.py +0 -0
  13. {qis-3.2.7 → qis-3.2.9}/qis/examples/best_returns.py +0 -0
  14. {qis-3.2.7 → qis-3.2.9}/qis/examples/bootstrap_analysis.py +0 -0
  15. {qis-3.2.7 → qis-3.2.9}/qis/examples/boxplot_conditional_returns.py +0 -0
  16. {qis-3.2.7 → qis-3.2.9}/qis/examples/btc_asset_corr.py +0 -0
  17. {qis-3.2.7 → qis-3.2.9}/qis/examples/constant_notional.py +0 -0
  18. {qis-3.2.7 → qis-3.2.9}/qis/examples/constant_weight_portfolios.py +0 -0
  19. {qis-3.2.7 → qis-3.2.9}/qis/examples/core/perf_bbg_prices.py +0 -0
  20. {qis-3.2.7 → qis-3.2.9}/qis/examples/core/price_plots.py +0 -0
  21. {qis-3.2.7 → qis-3.2.9}/qis/examples/core/us_election.py +0 -0
  22. {qis-3.2.7 → qis-3.2.9}/qis/examples/credit_spreads.py +0 -0
  23. {qis-3.2.7 → qis-3.2.9}/qis/examples/europe_futures.py +0 -0
  24. {qis-3.2.7 → qis-3.2.9}/qis/examples/factsheets/multi_assets.py +0 -0
  25. {qis-3.2.7 → qis-3.2.9}/qis/examples/factsheets/multi_strategy.py +0 -0
  26. {qis-3.2.7 → qis-3.2.9}/qis/examples/factsheets/pyblogs_reports.py +0 -0
  27. {qis-3.2.7 → qis-3.2.9}/qis/examples/factsheets/strategy.py +0 -0
  28. {qis-3.2.7 → qis-3.2.9}/qis/examples/generate_option_rolls.py +0 -0
  29. {qis-3.2.7 → qis-3.2.9}/qis/examples/interpolation_infrequent_returns.py +0 -0
  30. {qis-3.2.7 → qis-3.2.9}/qis/examples/leveraged_strategies.py +0 -0
  31. {qis-3.2.7 → qis-3.2.9}/qis/examples/long_short.py +0 -0
  32. {qis-3.2.7 → qis-3.2.9}/qis/examples/momentum_indices.py +0 -0
  33. {qis-3.2.7 → qis-3.2.9}/qis/examples/ohlc_vol_analysis.py +0 -0
  34. {qis-3.2.7 → qis-3.2.9}/qis/examples/overnight_returns.py +0 -0
  35. {qis-3.2.7 → qis-3.2.9}/qis/examples/perf_external_assets.py +0 -0
  36. {qis-3.2.7 → qis-3.2.9}/qis/examples/readme_performances.py +0 -0
  37. {qis-3.2.7 → qis-3.2.9}/qis/examples/risk_return_frontier.py +0 -0
  38. {qis-3.2.7 → qis-3.2.9}/qis/examples/rolling_performance.py +0 -0
  39. {qis-3.2.7 → qis-3.2.9}/qis/examples/seasonality.py +0 -0
  40. {qis-3.2.7 → qis-3.2.9}/qis/examples/sharpe_vs_sortino.py +0 -0
  41. {qis-3.2.7 → qis-3.2.9}/qis/examples/simulate_quant_strats.py +0 -0
  42. {qis-3.2.7 → qis-3.2.9}/qis/examples/test_ewm.py +0 -0
  43. {qis-3.2.7 → qis-3.2.9}/qis/examples/test_scatter.py +0 -0
  44. {qis-3.2.7 → qis-3.2.9}/qis/examples/try_pybloqs.py +0 -0
  45. {qis-3.2.7 → qis-3.2.9}/qis/examples/universe_corrs.py +0 -0
  46. {qis-3.2.7 → qis-3.2.9}/qis/examples/vix_beta_to_equities_bonds.py +0 -0
  47. {qis-3.2.7 → qis-3.2.9}/qis/examples/vix_conditional_returns.py +0 -0
  48. {qis-3.2.7 → qis-3.2.9}/qis/examples/vix_spy_by_year.py +0 -0
  49. {qis-3.2.7 → qis-3.2.9}/qis/examples/vix_tenor_analysis.py +0 -0
  50. {qis-3.2.7 → qis-3.2.9}/qis/examples/vol_without_weekends.py +0 -0
  51. {qis-3.2.7 → qis-3.2.9}/qis/file_utils.py +0 -0
  52. {qis-3.2.7 → qis-3.2.9}/qis/local_path.py +0 -0
  53. {qis-3.2.7 → qis-3.2.9}/qis/models/README.md +0 -0
  54. {qis-3.2.7 → qis-3.2.9}/qis/models/__init__.py +0 -0
  55. {qis-3.2.7 → qis-3.2.9}/qis/models/linear/__init__.py +0 -0
  56. {qis-3.2.7 → qis-3.2.9}/qis/models/linear/auto_corr.py +0 -0
  57. {qis-3.2.7 → qis-3.2.9}/qis/models/linear/corr_cov_matrix.py +0 -0
  58. {qis-3.2.7 → qis-3.2.9}/qis/models/linear/ewm.py +0 -0
  59. {qis-3.2.7 → qis-3.2.9}/qis/models/linear/ewm_convolution.py +0 -0
  60. {qis-3.2.7 → qis-3.2.9}/qis/models/linear/ewm_factors.py +0 -0
  61. {qis-3.2.7 → qis-3.2.9}/qis/models/linear/ewm_winsor_outliers.py +0 -0
  62. {qis-3.2.7 → qis-3.2.9}/qis/models/linear/pca.py +0 -0
  63. {qis-3.2.7 → qis-3.2.9}/qis/models/linear/plot_correlations.py +0 -0
  64. {qis-3.2.7 → qis-3.2.9}/qis/models/linear/ra_returns.py +0 -0
  65. {qis-3.2.7 → qis-3.2.9}/qis/models/stats/__init__.py +0 -0
  66. {qis-3.2.7 → qis-3.2.9}/qis/models/stats/bootstrap.py +0 -0
  67. {qis-3.2.7 → qis-3.2.9}/qis/models/stats/ohlc_vol.py +0 -0
  68. {qis-3.2.7 → qis-3.2.9}/qis/models/stats/rolling_stats.py +0 -0
  69. {qis-3.2.7 → qis-3.2.9}/qis/models/stats/test_bootstrap.py +0 -0
  70. {qis-3.2.7 → qis-3.2.9}/qis/perfstats/README.md +0 -0
  71. {qis-3.2.7 → qis-3.2.9}/qis/perfstats/__init__.py +0 -0
  72. {qis-3.2.7 → qis-3.2.9}/qis/perfstats/cond_regression.py +0 -0
  73. {qis-3.2.7 → qis-3.2.9}/qis/perfstats/config.py +0 -0
  74. {qis-3.2.7 → qis-3.2.9}/qis/perfstats/desc_table.py +0 -0
  75. {qis-3.2.7 → qis-3.2.9}/qis/perfstats/fx_ops.py +0 -0
  76. {qis-3.2.7 → qis-3.2.9}/qis/perfstats/perf_stats.py +0 -0
  77. {qis-3.2.7 → qis-3.2.9}/qis/perfstats/regime_classifier.py +0 -0
  78. {qis-3.2.7 → qis-3.2.9}/qis/perfstats/returns.py +0 -0
  79. {qis-3.2.7 → qis-3.2.9}/qis/perfstats/timeseries_bfill.py +0 -0
  80. {qis-3.2.7 → qis-3.2.9}/qis/plots/README.md +0 -0
  81. {qis-3.2.7 → qis-3.2.9}/qis/plots/__init__.py +0 -0
  82. {qis-3.2.7 → qis-3.2.9}/qis/plots/bars.py +0 -0
  83. {qis-3.2.7 → qis-3.2.9}/qis/plots/boxplot.py +0 -0
  84. {qis-3.2.7 → qis-3.2.9}/qis/plots/contour.py +0 -0
  85. {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/__init__.py +0 -0
  86. {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/data_timeseries.py +0 -0
  87. {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/desc_table.py +0 -0
  88. {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/drawdowns.py +0 -0
  89. {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/perf_table.py +0 -0
  90. {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/prices.py +0 -0
  91. {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/regime_class_table.py +0 -0
  92. {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/regime_data.py +0 -0
  93. {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/regime_pdf.py +0 -0
  94. {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/regime_scatter.py +0 -0
  95. {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/returns_heatmap.py +0 -0
  96. {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/returns_scatter.py +0 -0
  97. {qis-3.2.7 → qis-3.2.9}/qis/plots/errorbar.py +0 -0
  98. {qis-3.2.7 → qis-3.2.9}/qis/plots/heatmap.py +0 -0
  99. {qis-3.2.7 → qis-3.2.9}/qis/plots/histogram.py +0 -0
  100. {qis-3.2.7 → qis-3.2.9}/qis/plots/histplot2d.py +0 -0
  101. {qis-3.2.7 → qis-3.2.9}/qis/plots/lineplot.py +0 -0
  102. {qis-3.2.7 → qis-3.2.9}/qis/plots/pie.py +0 -0
  103. {qis-3.2.7 → qis-3.2.9}/qis/plots/qqplot.py +0 -0
  104. {qis-3.2.7 → qis-3.2.9}/qis/plots/reports/__init__.py +0 -0
  105. {qis-3.2.7 → qis-3.2.9}/qis/plots/reports/econ_data_single.py +0 -0
  106. {qis-3.2.7 → qis-3.2.9}/qis/plots/reports/gantt_data_history.py +0 -0
  107. {qis-3.2.7 → qis-3.2.9}/qis/plots/reports/price_history.py +0 -0
  108. {qis-3.2.7 → qis-3.2.9}/qis/plots/reports/utils.py +0 -0
  109. {qis-3.2.7 → qis-3.2.9}/qis/plots/scatter.py +0 -0
  110. {qis-3.2.7 → qis-3.2.9}/qis/plots/stackplot.py +0 -0
  111. {qis-3.2.7 → qis-3.2.9}/qis/plots/table.py +0 -0
  112. {qis-3.2.7 → qis-3.2.9}/qis/plots/time_series.py +0 -0
  113. {qis-3.2.7 → qis-3.2.9}/qis/plots/utils.py +0 -0
  114. {qis-3.2.7 → qis-3.2.9}/qis/portfolio/README.md +0 -0
  115. {qis-3.2.7 → qis-3.2.9}/qis/portfolio/backtester.py +0 -0
  116. {qis-3.2.7 → qis-3.2.9}/qis/portfolio/ewm_portfolio_risk.py +0 -0
  117. {qis-3.2.7 → qis-3.2.9}/qis/portfolio/reports/__init__.py +0 -0
  118. {qis-3.2.7 → qis-3.2.9}/qis/portfolio/reports/brinson_attribution.py +0 -0
  119. {qis-3.2.7 → qis-3.2.9}/qis/portfolio/reports/config.py +0 -0
  120. {qis-3.2.7 → qis-3.2.9}/qis/portfolio/reports/multi_assets_factsheet.py +0 -0
  121. {qis-3.2.7 → qis-3.2.9}/qis/portfolio/reports/multi_strategy_factseet_pybloqs.py +0 -0
  122. {qis-3.2.7 → qis-3.2.9}/qis/portfolio/reports/multi_strategy_factsheet.py +0 -0
  123. {qis-3.2.7 → qis-3.2.9}/qis/portfolio/reports/strategy_benchmark_factsheet_pybloqs.py +0 -0
  124. {qis-3.2.7 → qis-3.2.9}/qis/portfolio/reports/strategy_factsheet.py +0 -0
  125. {qis-3.2.7 → qis-3.2.9}/qis/portfolio/strats/__init__.py +0 -0
  126. {qis-3.2.7 → qis-3.2.9}/qis/portfolio/strats/quant_strats_delta1.py +0 -0
  127. {qis-3.2.7 → qis-3.2.9}/qis/portfolio/strats/seasonal_strats.py +0 -0
  128. {qis-3.2.7 → qis-3.2.9}/qis/settings.yaml +0 -0
  129. {qis-3.2.7 → qis-3.2.9}/qis/sql_engine.py +0 -0
  130. {qis-3.2.7 → qis-3.2.9}/qis/test_data.py +0 -0
  131. {qis-3.2.7 → qis-3.2.9}/qis/utils/README.md +0 -0
  132. {qis-3.2.7 → qis-3.2.9}/qis/utils/__init__.py +0 -0
  133. {qis-3.2.7 → qis-3.2.9}/qis/utils/dates.py +0 -0
  134. {qis-3.2.7 → qis-3.2.9}/qis/utils/df_agg.py +0 -0
  135. {qis-3.2.7 → qis-3.2.9}/qis/utils/df_cut.py +0 -0
  136. {qis-3.2.7 → qis-3.2.9}/qis/utils/df_freq.py +0 -0
  137. {qis-3.2.7 → qis-3.2.9}/qis/utils/df_groups.py +0 -0
  138. {qis-3.2.7 → qis-3.2.9}/qis/utils/df_melt.py +0 -0
  139. {qis-3.2.7 → qis-3.2.9}/qis/utils/df_ops.py +0 -0
  140. {qis-3.2.7 → qis-3.2.9}/qis/utils/df_str.py +0 -0
  141. {qis-3.2.7 → qis-3.2.9}/qis/utils/df_to_scores.py +0 -0
  142. {qis-3.2.7 → qis-3.2.9}/qis/utils/df_to_weights.py +0 -0
  143. {qis-3.2.7 → qis-3.2.9}/qis/utils/generic.py +0 -0
  144. {qis-3.2.7 → qis-3.2.9}/qis/utils/np_ops.py +0 -0
  145. {qis-3.2.7 → qis-3.2.9}/qis/utils/ols.py +0 -0
  146. {qis-3.2.7 → qis-3.2.9}/qis/utils/sampling.py +0 -0
  147. {qis-3.2.7 → qis-3.2.9}/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.7
3
+ Version: 3.2.9
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.7"
3
+ version = "3.2.9"
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:
@@ -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
 
@@ -192,8 +192,11 @@ class MultiPortfolioData:
192
192
 
193
193
  def compute_tracking_error_implied_by_covar(self,
194
194
  strategy_idx: int = 0,
195
- benchmark_idx: int = 1
196
- ) -> pd.Series:
195
+ benchmark_idx: int = 1,
196
+ is_grouped: bool = False,
197
+ group_data: pd.Series = None,
198
+ group_order: List[str] = None
199
+ ) -> Union[pd.Series, pd.DataFrame]:
197
200
  """
198
201
  compute Ex ante tracking error =
199
202
  (strategy_weights - strategy_weights) @ covar @ (strategy_weights - strategy_weights).T
@@ -208,11 +211,33 @@ class MultiPortfolioData:
208
211
  benchmark_weights = benchmark_weights.reindex(index=covar_index, columns=investable_assets).ffill().fillna(0.0)
209
212
 
210
213
  weight_diffs = benchmark_weights - strategy_weights
211
- tracking_error = {}
212
- for date, pd_covar in self.covar_dict.items():
213
- w = weight_diffs.loc[date]
214
- tracking_error[date] = np.sqrt(w @ pd_covar @ w.T)
215
- tracking_error = pd.Series(tracking_error, name='Tracking error')
214
+ if not is_grouped:
215
+ tracking_error = {}
216
+ for date, pd_covar in self.covar_dict.items():
217
+ w = weight_diffs.loc[date]
218
+ tracking_error[date] = np.sqrt(w @ pd_covar @ w.T)
219
+ tracking_error = pd.Series(tracking_error, name='Tracking error')
220
+ else:
221
+ if group_data is None:
222
+ group_data = self.portfolio_datas[strategy_idx].group_data
223
+ if group_order is None:
224
+ group_order = self.portfolio_datas[strategy_idx].group_order
225
+ group_dict = dfg.get_group_dict(group_data=group_data,
226
+ group_order=group_order,
227
+ total_column='Total')
228
+ tracking_error = {key: {} for key in group_dict.keys()}
229
+ for date, pd_covar in self.covar_dict.items():
230
+ w = weight_diffs.loc[date]
231
+ for key, tickers in group_dict.items():
232
+ w_g = w.loc[tickers]
233
+ pd_covar_g = pd_covar.loc[tickers, tickers]
234
+ tracking_error[key][date] = np.sqrt(w_g @ pd_covar_g @ w_g.T)
235
+ # merge
236
+ tracking_error_pd = {}
237
+ for key in group_dict.keys():
238
+ tracking_error_pd[key] = pd.Series(tracking_error[key], name=key)
239
+ tracking_error = pd.DataFrame.from_dict(tracking_error_pd, orient='columns')
240
+
216
241
  return tracking_error
217
242
 
218
243
  def compute_tracking_error_table(self,
@@ -1007,6 +1032,9 @@ class MultiPortfolioData:
1007
1032
  def plot_tre_time_series(self,
1008
1033
  strategy_idx: int = 0,
1009
1034
  benchmark_idx: int = 1,
1035
+ is_grouped: bool = False,
1036
+ group_data: pd.Series = None,
1037
+ group_order: List[str] = None,
1010
1038
  regime_benchmark: str = None,
1011
1039
  regime_params: BenchmarkReturnsQuantileRegimeSpecs = REGIME_PARAMS,
1012
1040
  time_period: TimePeriod = None,
@@ -1015,13 +1043,16 @@ class MultiPortfolioData:
1015
1043
  ax: plt.Subplot = None,
1016
1044
  **kwargs
1017
1045
  ) -> None:
1018
- tre = self.compute_tracking_error_implied_by_covar(strategy_idx=strategy_idx, benchmark_idx=benchmark_idx)
1046
+ tre = self.compute_tracking_error_implied_by_covar(strategy_idx=strategy_idx, benchmark_idx=benchmark_idx,
1047
+ is_grouped=is_grouped, group_data=group_data,
1048
+ group_order=group_order)
1019
1049
  if time_period is not None:
1020
1050
  tre = time_period.locate(tre)
1021
1051
  pts.plot_time_series(df=tre,
1022
1052
  var_format=var_format,
1023
1053
  legend_stats=pts.LegendStats.AVG_NONNAN_LAST,
1024
1054
  title=title,
1055
+ y_limits=(0.0, None),
1025
1056
  ax=ax,
1026
1057
  **kwargs)
1027
1058
  if regime_benchmark is not None:
@@ -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,42 @@ 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
+ turnover_rolling_period: Optional[int] = 260,
1488
+ freq_turnover: Optional[str] = 'B',
1489
+ add_total: bool = True,
1490
+ title: 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=turnover_rolling_period,
1501
+ add_total=add_total,
1502
+ freq=freq_turnover,
1503
+ **kwargs)
1504
+ freq = pd.infer_freq(turnover.index)
1505
+ turnover_title = title or f"{turnover_rolling_period}-period rolling {freq}-freq Turnover"
1506
+ qis.plot_time_series(df=turnover,
1507
+ var_format='{:,.2%}',
1508
+ y_limits=(0.0, None),
1509
+ legend_stats=qis.LegendStats.AVG_NONNAN_LAST,
1510
+ title=turnover_title,
1511
+ ax=ax,
1512
+ **kwargs)
1513
+ if regime_benchmark is not None:
1514
+ self.add_regime_shadows(ax=ax, regime_benchmark=regime_benchmark, index=turnover.index,
1515
+ regime_params=regime_params)
1701
1516
 
1702
1517
 
1703
1518
  @njit
@@ -665,22 +665,6 @@ def weights_tracking_error_report_by_ac_subac(multi_portfolio_data: MultiPortfol
665
665
  allow_negative=True,
666
666
  **kwargs)
667
667
 
668
- # tracking error
669
- fig, ax = plt.subplots(1, 1, figsize=figsize, tight_layout=True)
670
- figs['tre_time_series'] = fig
671
- if add_titles:
672
- title = 'Tracking Error'
673
- else:
674
- title = None
675
- multi_portfolio_data.plot_tre_time_series(strategy_idx=strategy_idx,
676
- benchmark_idx=benchmark_idx,
677
- regime_benchmark=regime_benchmark,
678
- regime_params=regime_params,
679
- title=title,
680
- ax=ax,
681
- time_period=time_period,
682
- **kwargs)
683
-
684
668
  # brinson by asset class
685
669
  totals_table, active_total, grouped_allocation_return, grouped_selection_return, grouped_interaction_return = \
686
670
  multi_portfolio_data.compute_brinson_attribution(strategy_idx=strategy_idx,
@@ -744,17 +728,65 @@ def weights_tracking_error_report_by_ac_subac(multi_portfolio_data: MultiPortfol
744
728
  figs['brinson_table_subac'] = qis.plot_brinson_totals_table(totals_table=totals_table, **kwargs)
745
729
  dfs['brinson_table_subac'] = totals_table
746
730
 
731
+ # tracking error
732
+ fig, ax = plt.subplots(1, 1, figsize=figsize, tight_layout=True)
733
+ figs['tre_time_series'] = fig
734
+ if add_titles:
735
+ title = 'Tracking Error'
736
+ else:
737
+ title = None
738
+ multi_portfolio_data.plot_tre_time_series(strategy_idx=strategy_idx,
739
+ benchmark_idx=benchmark_idx,
740
+ regime_benchmark=regime_benchmark,
741
+ regime_params=regime_params,
742
+ title=title,
743
+ ax=ax,
744
+ time_period=time_period,
745
+ **kwargs)
746
+
747
+ # group tracking error
748
+ fig, ax = plt.subplots(1, 1, figsize=figsize, tight_layout=True)
749
+ figs['tre_group_time_series'] = fig
750
+ if add_titles:
751
+ title = 'Asset Class Tracking Error'
752
+ else:
753
+ title = None
754
+ multi_portfolio_data.plot_tre_time_series(strategy_idx=strategy_idx,
755
+ benchmark_idx=benchmark_idx,
756
+ is_grouped=True,
757
+ group_data=ac_group_data,
758
+ group_order=ac_group_order,
759
+ regime_benchmark=regime_benchmark,
760
+ regime_params=regime_params,
761
+ title=title,
762
+ ax=ax,
763
+ time_period=time_period,
764
+ **kwargs)
765
+
747
766
  # turnover
748
767
  fig, ax = plt.subplots(1, 1, figsize=figsize, tight_layout=True)
749
- figs['turnover'] = fig
768
+ figs['joint_turnover'] = fig
750
769
  multi_portfolio_data.plot_turnover(ax=ax,
751
770
  time_period=time_period,
752
771
  regime_benchmark=regime_benchmark,
753
772
  regime_params=regime_params,
754
- #turnover_rolling_period=260,
755
- #freq_turnover=None,
756
773
  **kwargs)
757
-
774
+ if not add_titles:
775
+ ax.title.set_visible(False)
776
+ # group turnover
777
+ fig, ax = plt.subplots(1, 1, figsize=figsize, tight_layout=True)
778
+ figs['group_turnover'] = fig
779
+ multi_portfolio_data.portfolio_datas[strategy_idx].plot_turnover(ax=ax,
780
+ time_period=time_period,
781
+ regime_benchmark=regime_benchmark,
782
+ regime_params=regime_params,
783
+ is_grouped=True,
784
+ group_data=turnover_groups,
785
+ group_order=turnover_order,
786
+ add_total=False,
787
+ **kwargs)
788
+ if not add_titles:
789
+ ax.title.set_visible(False)
758
790
  return figs, dfs
759
791
 
760
792
 
@@ -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,