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.
- {qis-3.2.7 → qis-3.2.9}/PKG-INFO +1 -1
- {qis-3.2.7 → qis-3.2.9}/pyproject.toml +1 -1
- {qis-3.2.7 → qis-3.2.9}/qis/examples/factsheets/strategy_benchmark.py +4 -2
- {qis-3.2.7 → qis-3.2.9}/qis/portfolio/__init__.py +2 -2
- {qis-3.2.7 → qis-3.2.9}/qis/portfolio/multi_portfolio_data.py +39 -8
- {qis-3.2.7 → qis-3.2.9}/qis/portfolio/portfolio_data.py +40 -225
- {qis-3.2.7 → qis-3.2.9}/qis/portfolio/reports/strategy_benchmark_factsheet.py +52 -20
- {qis-3.2.7 → qis-3.2.9}/qis/portfolio/reports/strategy_signal_factsheet.py +2 -1
- qis-3.2.9/qis/portfolio/signal_data.py +238 -0
- {qis-3.2.7 → qis-3.2.9}/LICENSE.txt +0 -0
- {qis-3.2.7 → qis-3.2.9}/README.md +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/__init__.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/best_returns.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/bootstrap_analysis.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/boxplot_conditional_returns.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/btc_asset_corr.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/constant_notional.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/constant_weight_portfolios.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/core/perf_bbg_prices.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/core/price_plots.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/core/us_election.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/credit_spreads.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/europe_futures.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/factsheets/multi_assets.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/factsheets/multi_strategy.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/factsheets/pyblogs_reports.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/factsheets/strategy.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/generate_option_rolls.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/interpolation_infrequent_returns.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/leveraged_strategies.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/long_short.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/momentum_indices.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/ohlc_vol_analysis.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/overnight_returns.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/perf_external_assets.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/readme_performances.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/risk_return_frontier.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/rolling_performance.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/seasonality.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/sharpe_vs_sortino.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/simulate_quant_strats.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/test_ewm.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/test_scatter.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/try_pybloqs.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/universe_corrs.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/vix_beta_to_equities_bonds.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/vix_conditional_returns.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/vix_spy_by_year.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/vix_tenor_analysis.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/examples/vol_without_weekends.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/file_utils.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/local_path.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/models/README.md +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/models/__init__.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/models/linear/__init__.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/models/linear/auto_corr.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/models/linear/corr_cov_matrix.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/models/linear/ewm.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/models/linear/ewm_convolution.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/models/linear/ewm_factors.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/models/linear/ewm_winsor_outliers.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/models/linear/pca.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/models/linear/plot_correlations.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/models/linear/ra_returns.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/models/stats/__init__.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/models/stats/bootstrap.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/models/stats/ohlc_vol.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/models/stats/rolling_stats.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/models/stats/test_bootstrap.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/perfstats/README.md +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/perfstats/__init__.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/perfstats/cond_regression.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/perfstats/config.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/perfstats/desc_table.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/perfstats/fx_ops.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/perfstats/perf_stats.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/perfstats/regime_classifier.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/perfstats/returns.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/perfstats/timeseries_bfill.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/README.md +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/__init__.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/bars.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/boxplot.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/contour.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/__init__.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/data_timeseries.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/desc_table.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/drawdowns.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/perf_table.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/prices.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/regime_class_table.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/regime_data.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/regime_pdf.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/regime_scatter.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/returns_heatmap.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/derived/returns_scatter.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/errorbar.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/heatmap.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/histogram.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/histplot2d.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/lineplot.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/pie.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/qqplot.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/reports/__init__.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/reports/econ_data_single.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/reports/gantt_data_history.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/reports/price_history.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/reports/utils.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/scatter.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/stackplot.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/table.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/time_series.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/plots/utils.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/portfolio/README.md +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/portfolio/backtester.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/portfolio/ewm_portfolio_risk.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/portfolio/reports/__init__.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/portfolio/reports/brinson_attribution.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/portfolio/reports/config.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/portfolio/reports/multi_assets_factsheet.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/portfolio/reports/multi_strategy_factseet_pybloqs.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/portfolio/reports/multi_strategy_factsheet.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/portfolio/reports/strategy_benchmark_factsheet_pybloqs.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/portfolio/reports/strategy_factsheet.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/portfolio/strats/__init__.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/portfolio/strats/quant_strats_delta1.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/portfolio/strats/seasonal_strats.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/settings.yaml +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/sql_engine.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/test_data.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/utils/README.md +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/utils/__init__.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/utils/dates.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/utils/df_agg.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/utils/df_cut.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/utils/df_freq.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/utils/df_groups.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/utils/df_melt.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/utils/df_ops.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/utils/df_str.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/utils/df_to_scores.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/utils/df_to_weights.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/utils/generic.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/utils/np_ops.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/utils/ols.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/utils/sampling.py +0 -0
- {qis-3.2.7 → qis-3.2.9}/qis/utils/struct_ops.py +0 -0
{qis-3.2.7 → qis-3.2.9}/PKG-INFO
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: qis
|
3
|
-
Version: 3.2.
|
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
|
@@ -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.
|
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
|
-
|
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
|
-
|
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
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
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
|
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
|
-
|
1481
|
-
|
1482
|
-
|
1483
|
-
|
1484
|
-
|
1485
|
-
|
1486
|
-
|
1487
|
-
|
1488
|
-
|
1489
|
-
|
1490
|
-
|
1491
|
-
|
1492
|
-
|
1493
|
-
|
1494
|
-
|
1495
|
-
|
1496
|
-
|
1497
|
-
|
1498
|
-
|
1499
|
-
|
1500
|
-
|
1501
|
-
|
1502
|
-
|
1503
|
-
|
1504
|
-
|
1505
|
-
|
1506
|
-
|
1507
|
-
|
1508
|
-
|
1509
|
-
|
1510
|
-
|
1511
|
-
|
1512
|
-
|
1513
|
-
|
1514
|
-
|
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['
|
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
|
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,
|