qis 3.2.7__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.7 → qis-3.2.8}/PKG-INFO +1 -1
  2. {qis-3.2.7 → qis-3.2.8}/pyproject.toml +1 -1
  3. {qis-3.2.7 → qis-3.2.8}/qis/examples/factsheets/strategy_benchmark.py +4 -2
  4. {qis-3.2.7 → qis-3.2.8}/qis/portfolio/__init__.py +2 -2
  5. {qis-3.2.7 → qis-3.2.8}/qis/portfolio/portfolio_data.py +39 -225
  6. {qis-3.2.7 → qis-3.2.8}/qis/portfolio/reports/strategy_benchmark_factsheet.py +15 -1
  7. {qis-3.2.7 → qis-3.2.8}/qis/portfolio/reports/strategy_signal_factsheet.py +2 -1
  8. qis-3.2.8/qis/portfolio/signal_data.py +238 -0
  9. {qis-3.2.7 → qis-3.2.8}/qis/settings.yaml +0 -1
  10. {qis-3.2.7 → qis-3.2.8}/LICENSE.txt +0 -0
  11. {qis-3.2.7 → qis-3.2.8}/README.md +0 -0
  12. {qis-3.2.7 → qis-3.2.8}/qis/__init__.py +0 -0
  13. {qis-3.2.7 → qis-3.2.8}/qis/examples/best_returns.py +0 -0
  14. {qis-3.2.7 → qis-3.2.8}/qis/examples/bootstrap_analysis.py +0 -0
  15. {qis-3.2.7 → qis-3.2.8}/qis/examples/boxplot_conditional_returns.py +0 -0
  16. {qis-3.2.7 → qis-3.2.8}/qis/examples/btc_asset_corr.py +0 -0
  17. {qis-3.2.7 → qis-3.2.8}/qis/examples/constant_notional.py +0 -0
  18. {qis-3.2.7 → qis-3.2.8}/qis/examples/constant_weight_portfolios.py +0 -0
  19. {qis-3.2.7 → qis-3.2.8}/qis/examples/core/perf_bbg_prices.py +0 -0
  20. {qis-3.2.7 → qis-3.2.8}/qis/examples/core/price_plots.py +0 -0
  21. {qis-3.2.7 → qis-3.2.8}/qis/examples/core/us_election.py +0 -0
  22. {qis-3.2.7 → qis-3.2.8}/qis/examples/credit_spreads.py +0 -0
  23. {qis-3.2.7 → qis-3.2.8}/qis/examples/europe_futures.py +0 -0
  24. {qis-3.2.7 → qis-3.2.8}/qis/examples/factsheets/multi_assets.py +0 -0
  25. {qis-3.2.7 → qis-3.2.8}/qis/examples/factsheets/multi_strategy.py +0 -0
  26. {qis-3.2.7 → qis-3.2.8}/qis/examples/factsheets/pyblogs_reports.py +0 -0
  27. {qis-3.2.7 → qis-3.2.8}/qis/examples/factsheets/strategy.py +0 -0
  28. {qis-3.2.7 → qis-3.2.8}/qis/examples/generate_option_rolls.py +0 -0
  29. {qis-3.2.7 → qis-3.2.8}/qis/examples/interpolation_infrequent_returns.py +0 -0
  30. {qis-3.2.7 → qis-3.2.8}/qis/examples/leveraged_strategies.py +0 -0
  31. {qis-3.2.7 → qis-3.2.8}/qis/examples/long_short.py +0 -0
  32. {qis-3.2.7 → qis-3.2.8}/qis/examples/momentum_indices.py +0 -0
  33. {qis-3.2.7 → qis-3.2.8}/qis/examples/ohlc_vol_analysis.py +0 -0
  34. {qis-3.2.7 → qis-3.2.8}/qis/examples/overnight_returns.py +0 -0
  35. {qis-3.2.7 → qis-3.2.8}/qis/examples/perf_external_assets.py +0 -0
  36. {qis-3.2.7 → qis-3.2.8}/qis/examples/readme_performances.py +0 -0
  37. {qis-3.2.7 → qis-3.2.8}/qis/examples/risk_return_frontier.py +0 -0
  38. {qis-3.2.7 → qis-3.2.8}/qis/examples/rolling_performance.py +0 -0
  39. {qis-3.2.7 → qis-3.2.8}/qis/examples/seasonality.py +0 -0
  40. {qis-3.2.7 → qis-3.2.8}/qis/examples/sharpe_vs_sortino.py +0 -0
  41. {qis-3.2.7 → qis-3.2.8}/qis/examples/simulate_quant_strats.py +0 -0
  42. {qis-3.2.7 → qis-3.2.8}/qis/examples/test_ewm.py +0 -0
  43. {qis-3.2.7 → qis-3.2.8}/qis/examples/test_scatter.py +0 -0
  44. {qis-3.2.7 → qis-3.2.8}/qis/examples/try_pybloqs.py +0 -0
  45. {qis-3.2.7 → qis-3.2.8}/qis/examples/universe_corrs.py +0 -0
  46. {qis-3.2.7 → qis-3.2.8}/qis/examples/vix_beta_to_equities_bonds.py +0 -0
  47. {qis-3.2.7 → qis-3.2.8}/qis/examples/vix_conditional_returns.py +0 -0
  48. {qis-3.2.7 → qis-3.2.8}/qis/examples/vix_spy_by_year.py +0 -0
  49. {qis-3.2.7 → qis-3.2.8}/qis/examples/vix_tenor_analysis.py +0 -0
  50. {qis-3.2.7 → qis-3.2.8}/qis/examples/vol_without_weekends.py +0 -0
  51. {qis-3.2.7 → qis-3.2.8}/qis/file_utils.py +0 -0
  52. {qis-3.2.7 → qis-3.2.8}/qis/local_path.py +0 -0
  53. {qis-3.2.7 → qis-3.2.8}/qis/models/README.md +0 -0
  54. {qis-3.2.7 → qis-3.2.8}/qis/models/__init__.py +0 -0
  55. {qis-3.2.7 → qis-3.2.8}/qis/models/linear/__init__.py +0 -0
  56. {qis-3.2.7 → qis-3.2.8}/qis/models/linear/auto_corr.py +0 -0
  57. {qis-3.2.7 → qis-3.2.8}/qis/models/linear/corr_cov_matrix.py +0 -0
  58. {qis-3.2.7 → qis-3.2.8}/qis/models/linear/ewm.py +0 -0
  59. {qis-3.2.7 → qis-3.2.8}/qis/models/linear/ewm_convolution.py +0 -0
  60. {qis-3.2.7 → qis-3.2.8}/qis/models/linear/ewm_factors.py +0 -0
  61. {qis-3.2.7 → qis-3.2.8}/qis/models/linear/ewm_winsor_outliers.py +0 -0
  62. {qis-3.2.7 → qis-3.2.8}/qis/models/linear/pca.py +0 -0
  63. {qis-3.2.7 → qis-3.2.8}/qis/models/linear/plot_correlations.py +0 -0
  64. {qis-3.2.7 → qis-3.2.8}/qis/models/linear/ra_returns.py +0 -0
  65. {qis-3.2.7 → qis-3.2.8}/qis/models/stats/__init__.py +0 -0
  66. {qis-3.2.7 → qis-3.2.8}/qis/models/stats/bootstrap.py +0 -0
  67. {qis-3.2.7 → qis-3.2.8}/qis/models/stats/ohlc_vol.py +0 -0
  68. {qis-3.2.7 → qis-3.2.8}/qis/models/stats/rolling_stats.py +0 -0
  69. {qis-3.2.7 → qis-3.2.8}/qis/models/stats/test_bootstrap.py +0 -0
  70. {qis-3.2.7 → qis-3.2.8}/qis/perfstats/README.md +0 -0
  71. {qis-3.2.7 → qis-3.2.8}/qis/perfstats/__init__.py +0 -0
  72. {qis-3.2.7 → qis-3.2.8}/qis/perfstats/cond_regression.py +0 -0
  73. {qis-3.2.7 → qis-3.2.8}/qis/perfstats/config.py +0 -0
  74. {qis-3.2.7 → qis-3.2.8}/qis/perfstats/desc_table.py +0 -0
  75. {qis-3.2.7 → qis-3.2.8}/qis/perfstats/fx_ops.py +0 -0
  76. {qis-3.2.7 → qis-3.2.8}/qis/perfstats/perf_stats.py +0 -0
  77. {qis-3.2.7 → qis-3.2.8}/qis/perfstats/regime_classifier.py +0 -0
  78. {qis-3.2.7 → qis-3.2.8}/qis/perfstats/returns.py +0 -0
  79. {qis-3.2.7 → qis-3.2.8}/qis/perfstats/timeseries_bfill.py +0 -0
  80. {qis-3.2.7 → qis-3.2.8}/qis/plots/README.md +0 -0
  81. {qis-3.2.7 → qis-3.2.8}/qis/plots/__init__.py +0 -0
  82. {qis-3.2.7 → qis-3.2.8}/qis/plots/bars.py +0 -0
  83. {qis-3.2.7 → qis-3.2.8}/qis/plots/boxplot.py +0 -0
  84. {qis-3.2.7 → qis-3.2.8}/qis/plots/contour.py +0 -0
  85. {qis-3.2.7 → qis-3.2.8}/qis/plots/derived/__init__.py +0 -0
  86. {qis-3.2.7 → qis-3.2.8}/qis/plots/derived/data_timeseries.py +0 -0
  87. {qis-3.2.7 → qis-3.2.8}/qis/plots/derived/desc_table.py +0 -0
  88. {qis-3.2.7 → qis-3.2.8}/qis/plots/derived/drawdowns.py +0 -0
  89. {qis-3.2.7 → qis-3.2.8}/qis/plots/derived/perf_table.py +0 -0
  90. {qis-3.2.7 → qis-3.2.8}/qis/plots/derived/prices.py +0 -0
  91. {qis-3.2.7 → qis-3.2.8}/qis/plots/derived/regime_class_table.py +0 -0
  92. {qis-3.2.7 → qis-3.2.8}/qis/plots/derived/regime_data.py +0 -0
  93. {qis-3.2.7 → qis-3.2.8}/qis/plots/derived/regime_pdf.py +0 -0
  94. {qis-3.2.7 → qis-3.2.8}/qis/plots/derived/regime_scatter.py +0 -0
  95. {qis-3.2.7 → qis-3.2.8}/qis/plots/derived/returns_heatmap.py +0 -0
  96. {qis-3.2.7 → qis-3.2.8}/qis/plots/derived/returns_scatter.py +0 -0
  97. {qis-3.2.7 → qis-3.2.8}/qis/plots/errorbar.py +0 -0
  98. {qis-3.2.7 → qis-3.2.8}/qis/plots/heatmap.py +0 -0
  99. {qis-3.2.7 → qis-3.2.8}/qis/plots/histogram.py +0 -0
  100. {qis-3.2.7 → qis-3.2.8}/qis/plots/histplot2d.py +0 -0
  101. {qis-3.2.7 → qis-3.2.8}/qis/plots/lineplot.py +0 -0
  102. {qis-3.2.7 → qis-3.2.8}/qis/plots/pie.py +0 -0
  103. {qis-3.2.7 → qis-3.2.8}/qis/plots/qqplot.py +0 -0
  104. {qis-3.2.7 → qis-3.2.8}/qis/plots/reports/__init__.py +0 -0
  105. {qis-3.2.7 → qis-3.2.8}/qis/plots/reports/econ_data_single.py +0 -0
  106. {qis-3.2.7 → qis-3.2.8}/qis/plots/reports/gantt_data_history.py +0 -0
  107. {qis-3.2.7 → qis-3.2.8}/qis/plots/reports/price_history.py +0 -0
  108. {qis-3.2.7 → qis-3.2.8}/qis/plots/reports/utils.py +0 -0
  109. {qis-3.2.7 → qis-3.2.8}/qis/plots/scatter.py +0 -0
  110. {qis-3.2.7 → qis-3.2.8}/qis/plots/stackplot.py +0 -0
  111. {qis-3.2.7 → qis-3.2.8}/qis/plots/table.py +0 -0
  112. {qis-3.2.7 → qis-3.2.8}/qis/plots/time_series.py +0 -0
  113. {qis-3.2.7 → qis-3.2.8}/qis/plots/utils.py +0 -0
  114. {qis-3.2.7 → qis-3.2.8}/qis/portfolio/README.md +0 -0
  115. {qis-3.2.7 → qis-3.2.8}/qis/portfolio/backtester.py +0 -0
  116. {qis-3.2.7 → qis-3.2.8}/qis/portfolio/ewm_portfolio_risk.py +0 -0
  117. {qis-3.2.7 → qis-3.2.8}/qis/portfolio/multi_portfolio_data.py +0 -0
  118. {qis-3.2.7 → qis-3.2.8}/qis/portfolio/reports/__init__.py +0 -0
  119. {qis-3.2.7 → qis-3.2.8}/qis/portfolio/reports/brinson_attribution.py +0 -0
  120. {qis-3.2.7 → qis-3.2.8}/qis/portfolio/reports/config.py +0 -0
  121. {qis-3.2.7 → qis-3.2.8}/qis/portfolio/reports/multi_assets_factsheet.py +0 -0
  122. {qis-3.2.7 → qis-3.2.8}/qis/portfolio/reports/multi_strategy_factseet_pybloqs.py +0 -0
  123. {qis-3.2.7 → qis-3.2.8}/qis/portfolio/reports/multi_strategy_factsheet.py +0 -0
  124. {qis-3.2.7 → qis-3.2.8}/qis/portfolio/reports/strategy_benchmark_factsheet_pybloqs.py +0 -0
  125. {qis-3.2.7 → qis-3.2.8}/qis/portfolio/reports/strategy_factsheet.py +0 -0
  126. {qis-3.2.7 → qis-3.2.8}/qis/portfolio/strats/__init__.py +0 -0
  127. {qis-3.2.7 → qis-3.2.8}/qis/portfolio/strats/quant_strats_delta1.py +0 -0
  128. {qis-3.2.7 → qis-3.2.8}/qis/portfolio/strats/seasonal_strats.py +0 -0
  129. {qis-3.2.7 → qis-3.2.8}/qis/sql_engine.py +0 -0
  130. {qis-3.2.7 → qis-3.2.8}/qis/test_data.py +0 -0
  131. {qis-3.2.7 → qis-3.2.8}/qis/utils/README.md +0 -0
  132. {qis-3.2.7 → qis-3.2.8}/qis/utils/__init__.py +0 -0
  133. {qis-3.2.7 → qis-3.2.8}/qis/utils/dates.py +0 -0
  134. {qis-3.2.7 → qis-3.2.8}/qis/utils/df_agg.py +0 -0
  135. {qis-3.2.7 → qis-3.2.8}/qis/utils/df_cut.py +0 -0
  136. {qis-3.2.7 → qis-3.2.8}/qis/utils/df_freq.py +0 -0
  137. {qis-3.2.7 → qis-3.2.8}/qis/utils/df_groups.py +0 -0
  138. {qis-3.2.7 → qis-3.2.8}/qis/utils/df_melt.py +0 -0
  139. {qis-3.2.7 → qis-3.2.8}/qis/utils/df_ops.py +0 -0
  140. {qis-3.2.7 → qis-3.2.8}/qis/utils/df_str.py +0 -0
  141. {qis-3.2.7 → qis-3.2.8}/qis/utils/df_to_scores.py +0 -0
  142. {qis-3.2.7 → qis-3.2.8}/qis/utils/df_to_weights.py +0 -0
  143. {qis-3.2.7 → qis-3.2.8}/qis/utils/generic.py +0 -0
  144. {qis-3.2.7 → qis-3.2.8}/qis/utils/np_ops.py +0 -0
  145. {qis-3.2.7 → qis-3.2.8}/qis/utils/ols.py +0 -0
  146. {qis-3.2.7 → qis-3.2.8}/qis/utils/sampling.py +0 -0
  147. {qis-3.2.7 → 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.7
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.7"
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:
@@ -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
 
@@ -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,
@@ -0,0 +1,238 @@
1
+ """
2
+ signal data outpout for portfolio data reporting
3
+ """
4
+ from __future__ import annotations
5
+ import numpy as np
6
+ import pandas as pd
7
+ from statsmodels.regression.linear_model import RegressionResults as RegModel
8
+ from dataclasses import dataclass, asdict
9
+ from typing import Dict, List, Optional, Tuple
10
+ from qis.utils.dates import TimePeriod
11
+ from qis.utils.df_str import date_to_str
12
+ from qis.utils.df_groups import get_group_dict
13
+ from qis.utils.df_melt import melt_df_by_columns
14
+ from qis.utils.ols import fit_ols
15
+ from qis.utils.df_freq import df_resample_at_int_index, df_resample_at_freq
16
+
17
+
18
+ @dataclass
19
+ class StrategySignalData:
20
+ """
21
+ data class instance applied for output of strategy backtest data
22
+ """
23
+ log_returns: pd.DataFrame = None
24
+ ra_carry: pd.DataFrame = None # risk-adjusted carry
25
+ momentum: pd.DataFrame = None
26
+ signal: pd.DataFrame = None # signal output
27
+ instrument_vols: pd.DataFrame = None # instrument vols
28
+ instrument_target_vols: pd.DataFrame = None # target vols for portfolio allocation
29
+ instrument_target_signal_vol_weights: pd.DataFrame = None # target vols * signal
30
+ instrument_portfolio_leverages: pd.DataFrame = None # = portfolio weight / instrument_target_signal_vol_weights
31
+ weights: pd.DataFrame = None # final weights
32
+ kwargs: Dict[str, pd.DataFrame] = None # any other outputs
33
+
34
+ def locate_period(self, time_period: TimePeriod) -> StrategySignalData:
35
+ # nb: does not work for returns
36
+ data_dict = asdict(self)
37
+ for key, df in data_dict.items():
38
+ if df is not None:
39
+ data_dict[key] = time_period.locate(df)
40
+ return StrategySignalData(**data_dict)
41
+
42
+ def rename_data(self, names_map: Dict[str, str]) -> StrategySignalData:
43
+ data_dict = asdict(self)
44
+ for key, df in data_dict.items():
45
+ if df is not None:
46
+ data_dict[key] = df.rename(names_map, axis=1)
47
+ return StrategySignalData(**data_dict)
48
+
49
+ def get_current_signal_by_groups(self, group_data: pd.Series,
50
+ group_order: List[str] = None
51
+ ) -> Dict[str, pd.DataFrame]:
52
+ group_dict = get_group_dict(group_data=group_data,
53
+ group_order=group_order,
54
+ total_column=None)
55
+ group_signals = {}
56
+ agg_by_group = {}
57
+ last_date = date_to_str(self.signal.index[-21])
58
+ current_date = date_to_str(self.signal.index[-1])
59
+ last_signals = self.signal.iloc[-21, :]
60
+ current_signals = self.signal.iloc[-1, :]
61
+ for group, tickers in group_dict.items():
62
+ last_signals_ = last_signals[tickers]
63
+ current_signals_ = current_signals[tickers]
64
+ group_signals[group] = pd.concat([last_signals_.rename(last_date),
65
+ current_signals_.rename(current_date)
66
+ ], axis=1)
67
+
68
+ agg_by_group[group] = pd.Series({last_date: np.nanmean(last_signals_),
69
+ current_date: np.nanmean(current_signals_)})
70
+ agg_by_group = {'Total by groups': pd.DataFrame.from_dict(agg_by_group, orient='index')}
71
+ agg_by_group.update(group_signals)
72
+ return agg_by_group
73
+
74
+ def asdiff(self, tickers: List[str] = None,
75
+ freq: str = None,
76
+ sample_size: Optional[int] = 21, # can use rolling instead for freq
77
+ time_period: TimePeriod = None
78
+ ) -> StrategySignalData:
79
+
80
+ if tickers is None:
81
+ tickers = self.log_returns.columns.to_list()
82
+ # nb: does not work for returns
83
+ if time_period is not None:
84
+ ssd = self.locate_period(time_period=time_period)
85
+ else:
86
+ ssd = self
87
+
88
+ data_dict = asdict(ssd)
89
+ if sample_size is not None:
90
+ for key, df in data_dict.items():
91
+ if df is not None:
92
+ data_dict[key] = df_resample_at_int_index(df=df[tickers], func=None, sample_size=sample_size).diff()
93
+ else:
94
+ for key, df in data_dict.items():
95
+ if df is not None:
96
+ data_dict[key] = df_resample_at_freq(df=df[tickers], freq=freq, include_end_date=True).diff()
97
+ return StrategySignalData(**data_dict)
98
+
99
+ def estimate_signal_changes_joint(self,
100
+ tickers: List[str] = None,
101
+ freq: Optional[str] = None,
102
+ sample_size: Optional[int] = 21, # can use rolling instead for freq
103
+ time_period: TimePeriod = None
104
+ ) -> Tuple[pd.DataFrame, RegModel, TimePeriod]:
105
+ if tickers is None:
106
+ tickers = self.log_returns.columns.to_list()
107
+ ssd = self.asdiff(tickers=tickers, sample_size=sample_size, freq=freq, time_period=time_period)
108
+ y_var_name = 'weight_change'
109
+ y = melt_df_by_columns(ssd.weights.iloc[:-1, :], y_var_name=y_var_name)[y_var_name]
110
+ x_var_name1 = 'momentum_change'
111
+ x1 = melt_df_by_columns(ssd.momentum.iloc[:-1, :], y_var_name=x_var_name1)[x_var_name1]
112
+ x_var_name2 = 'target_vol_change'
113
+ x2 = melt_df_by_columns(ssd.instrument_target_vols.iloc[:-1, :], y_var_name=x_var_name2)[x_var_name2]
114
+ x_var_name3 = 'port_leverage_change'
115
+ x3 = melt_df_by_columns(ssd.instrument_portfolio_leverages.iloc[:-1, :], y_var_name=x_var_name3)[x_var_name3]
116
+ if self.ra_carry is not None:
117
+ x_var_name0 = 'carry_change'
118
+ x0 = melt_df_by_columns(ssd.ra_carry.iloc[:-1, :], y_var_name=x_var_name0)[x_var_name0]
119
+ x = pd.concat([x0, x1, x2, x3], axis=1).dropna()
120
+ else:
121
+ x = pd.concat([x1, x2, x3], axis=1).dropna()
122
+ x_names = x.columns.to_list()
123
+ y = y.reindex(index=x.index)
124
+
125
+ # keep last obs for prediction
126
+ fitted_model = fit_ols(x=x.to_numpy(), y=y.to_numpy(), order=1, fit_intercept=False)
127
+ actual_change = ssd.weights.iloc[-1, :]
128
+ predictions = {}
129
+ for ticker in tickers:
130
+ if self.ra_carry is not None:
131
+ x_ts = np.array([ssd.ra_carry[ticker].iloc[-1],
132
+ ssd.momentum[ticker].iloc[-1],
133
+ ssd.instrument_target_vols[ticker].iloc[-1],
134
+ ssd.instrument_portfolio_leverages[ticker].iloc[-1]])
135
+ else:
136
+ x_ts = np.array([ssd.momentum[ticker].iloc[-1],
137
+ ssd.instrument_target_vols[ticker].iloc[-1],
138
+ ssd.instrument_portfolio_leverages[ticker].iloc[-1]])
139
+ pred_t = {}
140
+ total_pred = 0.0
141
+ for idx, x_t in enumerate(x_ts):
142
+ pred_x = fitted_model.params[idx] * x_t
143
+ total_pred += pred_x
144
+ pred_t[x_names[idx]] = pred_x
145
+ pred_t['predicted'] = total_pred
146
+ pred_t['actual'] = actual_change[ticker]
147
+ pred_t['residual'] = actual_change[ticker] - total_pred
148
+ pred_t['residual %'] = total_pred / actual_change[ticker]
149
+ pred_t['r2'] = fitted_model.rsquared
150
+ predictions[ticker] = pd.Series(pred_t)
151
+
152
+ predictions = pd.DataFrame.from_dict(predictions, orient='index')
153
+ prediction_period = TimePeriod(start=ssd.weights.index[-2], end=ssd.weights.index[-1])
154
+
155
+ return predictions, fitted_model, prediction_period
156
+
157
+ def estimate_signal_changes_by_groups(self,
158
+ group_data: pd.Series, group_order: List[str] = None,
159
+ freq: Optional[str] = None,
160
+ sample_size: Optional[int] = 21, # can use rolling instead for freq
161
+ time_period: TimePeriod = None
162
+ ) -> Tuple[Dict[str, pd.DataFrame], Dict[str, RegModel], TimePeriod]:
163
+ """
164
+ estimate weight change for groups
165
+ """
166
+ group_dict = get_group_dict(group_data=group_data,
167
+ group_order=group_order,
168
+ total_column=None)
169
+ predictions = {}
170
+ fitted_models = {}
171
+ prediction_period = None
172
+ for group, tickers in group_dict.items():
173
+ prediction, fitted_model, prediction_period = self.estimate_signal_changes_joint(
174
+ tickers=tickers, freq=freq,
175
+ sample_size=sample_size,
176
+ time_period=time_period)
177
+ predictions[group] = prediction
178
+ fitted_models[group] = fitted_model
179
+ return predictions, fitted_models, prediction_period
180
+
181
+ def estimate_signal_changes_individual(self,
182
+ tickers: List[str] = None,
183
+ freq: Optional[str] = None,
184
+ sample_size: Optional[int] = 21, # can use rolling instead for freq
185
+ time_period: TimePeriod = None
186
+ ) -> Tuple[pd.DataFrame, Dict[str, RegModel]]:
187
+ if tickers is None:
188
+ tickers = self.log_returns.columns.to_list()
189
+ ssd = self.asdiff(tickers=tickers, sample_size=sample_size, freq=freq, time_period=time_period)
190
+ y_var_name = 'weight_change'
191
+ y = ssd.weights
192
+ x_var_name1 = 'momentum_change'
193
+ x1 = ssd.momentum
194
+ x_var_name2 = 'target_vol_change'
195
+ x2 = ssd.instrument_target_vols
196
+ x_var_name3 = 'port_leverage_change'
197
+ x3 = ssd.instrument_portfolio_leverages
198
+ if self.ra_carry is not None:
199
+ x_var_name0 = 'carry_change'
200
+ x0 = ssd.ra_carry
201
+ x_names = [x_var_name0, x_var_name1, x_var_name2, x_var_name3]
202
+ else:
203
+ x_names = [x_var_name1, x_var_name2, x_var_name3]
204
+
205
+ predictions = {}
206
+ fitted_models = {}
207
+ for ticker in tickers:
208
+ if self.ra_carry is not None:
209
+ x = pd.concat([x0[ticker].rename(x_var_name0),
210
+ x1[ticker].rename(x_var_name1),
211
+ x2[ticker].rename(x_var_name2),
212
+ x3[ticker].rename(x_var_name3)], axis=1)
213
+ else:
214
+ x = pd.concat([x1[ticker].rename(x_var_name1),
215
+ x2[ticker].rename(x_var_name2),
216
+ x3[ticker].rename(x_var_name3)], axis=1)
217
+
218
+ # keep last obs for prediction
219
+ fitted_model = fit_ols(x=x.iloc[:-1, :].to_numpy(), y=y[ticker].iloc[:-1].to_numpy(), order=1, fit_intercept=False)
220
+ actual_change = y[ticker].iloc[-1]
221
+ x_ts = x.iloc[-1, :].to_numpy()
222
+ pred_t = {}
223
+ total_pred = 0.0
224
+ for idx, x_t in enumerate(x_ts):
225
+ pred_x = fitted_model.params[idx] * x_t
226
+ total_pred += pred_x
227
+ pred_t[x_names[idx]] = pred_x
228
+ pred_t['predicted'] = total_pred
229
+ pred_t['actual'] = actual_change
230
+ pred_t['residual'] = actual_change - total_pred
231
+ pred_t['residual %'] = total_pred / actual_change
232
+ pred_t['r2'] = fitted_model.rsquared
233
+ predictions[ticker] = pd.Series(pred_t)
234
+ fitted_models[ticker] = fitted_model
235
+
236
+ predictions = pd.DataFrame.from_dict(predictions, orient='index')
237
+
238
+ return predictions, fitted_models
@@ -18,4 +18,3 @@ OUTPUT_PATH:
18
18
 
19
19
  AWS_POSTGRES:
20
20
  ""
21
-
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes