├── .gitignore ├── LICENSE.txt ├── README.md ├── notebooks ├── multi_asset_factsheet.ipynb ├── multi_strategy.ipynb ├── performance_plots.ipynb ├── strategy_benchmark_factsheet.ipynb ├── strategy_factsheet.ipynb └── us_presidential_election.ipynb ├── pyproject.toml ├── pyvenv.cfg ├── qis ├── __init__.py ├── examples │ ├── best_returns.py │ ├── bootstrap_analysis.py │ ├── boxplot_conditional_returns.py │ ├── btc_asset_corr.py │ ├── constant_notional.py │ ├── constant_weight_portfolios.py │ ├── core │ │ ├── perf_bbg_prices.py │ │ ├── price_plots.py │ │ └── us_election.py │ ├── credit_spreads.py │ ├── europe_futures.py │ ├── factsheets │ │ ├── multi_assets.py │ │ ├── multi_strategy.py │ │ ├── pyblogs_reports.py │ │ ├── strategy.py │ │ └── strategy_benchmark.py │ ├── figures │ │ ├── brinson_attribution.PNG │ │ ├── multi_strategy.PNG │ │ ├── multiassets.PNG │ │ ├── perf1.PNG │ │ ├── perf2.PNG │ │ ├── perf3.PNG │ │ ├── perf4.PNG │ │ ├── pnl_attribution.PNG │ │ ├── strategy1.PNG │ │ ├── strategy2.PNG │ │ ├── strategy3.PNG │ │ └── strategy_benchmark.PNG │ ├── generate_option_rolls.py │ ├── interpolation_infrequent_returns.py │ ├── leveraged_strategies.py │ ├── long_short.py │ ├── marginal_asset_contribution.py │ ├── momentum_indices.py │ ├── ohlc_vol_analysis.py │ ├── overnight_returns.py │ ├── perf_external_assets.py │ ├── readme_performances.py │ ├── risk_return_frontier.py │ ├── rolling_performance.py │ ├── seasonality.py │ ├── sharpe_vs_sortino.py │ ├── simulate_quant_strats.py │ ├── test_ewm.py │ ├── test_scatter.py │ ├── try_pybloqs.py │ ├── universe_corrs.py │ ├── vix_beta_to_equities_bonds.py │ ├── vix_conditional_returns.py │ ├── vix_spy_by_year.py │ ├── vix_tenor_analysis.py │ └── vol_without_weekends.py ├── file_utils.py ├── local_path.py ├── models │ ├── README.md │ ├── __init__.py │ ├── linear │ │ ├── __init__.py │ │ ├── auto_corr.py │ │ ├── corr_cov_matrix.py │ │ ├── ewm.py │ │ ├── ewm_convolution.py │ │ ├── ewm_factors.py │ │ ├── ewm_winsor_outliers.py │ │ ├── pca.py │ │ ├── plot_correlations.py │ │ └── ra_returns.py │ └── stats │ │ ├── __init__.py │ │ ├── bootstrap.py │ │ ├── ohlc_vol.py │ │ ├── rolling_stats.py │ │ └── test_bootstrap.py ├── perfstats │ ├── README.md │ ├── __init__.py │ ├── cond_regression.py │ ├── config.py │ ├── desc_table.py │ ├── fx_ops.py │ ├── perf_stats.py │ ├── regime_classifier.py │ ├── returns.py │ └── timeseries_bfill.py ├── plots │ ├── README.md │ ├── __init__.py │ ├── bars.py │ ├── boxplot.py │ ├── contour.py │ ├── derived │ │ ├── __init__.py │ │ ├── data_timeseries.py │ │ ├── desc_table.py │ │ ├── drawdowns.py │ │ ├── perf_table.py │ │ ├── prices.py │ │ ├── regime_class_table.py │ │ ├── regime_data.py │ │ ├── regime_pdf.py │ │ ├── regime_scatter.py │ │ ├── returns_heatmap.py │ │ └── returns_scatter.py │ ├── errorbar.py │ ├── heatmap.py │ ├── histogram.py │ ├── histplot2d.py │ ├── lineplot.py │ ├── pie.py │ ├── qqplot.py │ ├── reports │ │ ├── __init__.py │ │ ├── econ_data_single.py │ │ ├── gantt_data_history.py │ │ ├── price_history.py │ │ └── utils.py │ ├── scatter.py │ ├── stackplot.py │ ├── table.py │ ├── time_series.py │ └── utils.py ├── portfolio │ ├── README.md │ ├── __init__.py │ ├── backtester.py │ ├── ewm_portfolio_risk.py │ ├── multi_portfolio_data.py │ ├── portfolio_data.py │ ├── reports │ │ ├── __init__.py │ │ ├── brinson_attribution.py │ │ ├── config.py │ │ ├── multi_assets_factsheet.py │ │ ├── multi_strategy_factseet_pybloqs.py │ │ ├── multi_strategy_factsheet.py │ │ ├── overlays_smart_diversification.py │ │ ├── strategy_benchmark_factsheet.py │ │ ├── strategy_benchmark_factsheet_pybloqs.py │ │ ├── strategy_factsheet.py │ │ └── strategy_signal_factsheet.py │ ├── signal_data.py │ └── strats │ │ ├── __init__.py │ │ ├── quant_strats_delta1.py │ │ └── seasonal_strats.py ├── settings.yaml ├── sql_engine.py ├── test_data.py └── utils │ ├── README.md │ ├── __init__.py │ ├── dates.py │ ├── df_agg.py │ ├── df_cut.py │ ├── df_freq.py │ ├── df_groups.py │ ├── df_melt.py │ ├── df_ops.py │ ├── df_str.py │ ├── df_to_scores.py │ ├── df_to_weights.py │ ├── generic.py │ ├── np_ops.py │ ├── ols.py │ ├── sampling.py │ └── struct_ops.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # created by virtualenv automatically 2 | Lib/ 3 | Scripts/ 4 | 5 | # YAML 6 | *.yaml 7 | !/qis/settings.yaml 8 | 9 | # Byte-compiled / optimized / DLL files 10 | docs/figures/ 11 | 12 | __pycache__/ 13 | *.py[cod] 14 | *$py.class 15 | 16 | # C extensions 17 | *.so 18 | 19 | # Distribution / packaging 20 | .Python 21 | .idea/ 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | pip-wheel-metadata/ 35 | share/python-wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | .*xml 41 | .*iml 42 | 43 | # PyInstaller 44 | # Usually these files are written by a python script from a template 45 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 46 | *.manifest 47 | *.spec 48 | 49 | # Installer logs 50 | pip-log.txt 51 | pip-delete-this-directory.txt 52 | 53 | # Unit test / coverage reports 54 | htmlcov/ 55 | .tox/ 56 | .nox/ 57 | .coverage 58 | .coverage.* 59 | .cache 60 | nosetests.xml 61 | coverage.xml 62 | *.cover 63 | *.py,cover 64 | .hypothesis/ 65 | .pytest_cache/ 66 | 67 | # Translations 68 | *.mo 69 | *.pot 70 | 71 | # Django stuff: 72 | *.log 73 | local_settings.py 74 | db.sqlite3 75 | db.sqlite3-journal 76 | 77 | # Flask stuff: 78 | instance/ 79 | .webassets-cache 80 | 81 | # Scrapy stuff: 82 | .scrapy 83 | 84 | # Sphinx documentation 85 | docs/_build/ 86 | 87 | # PyBuilder 88 | target/ 89 | 90 | # Jupyter Notebook 91 | .ipynb_checkpoints 92 | 93 | # IPython 94 | profile_default/ 95 | ipython_config.py 96 | 97 | # pyenv 98 | .python-version 99 | 100 | # pipenv 101 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 102 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 103 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 104 | # install all needed dependencies. 105 | #Pipfile.lock 106 | 107 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 108 | __pypackages__/ 109 | 110 | # Celery stuff 111 | celerybeat-schedule 112 | celerybeat.pid 113 | 114 | # SageMath parsed files 115 | *.sage.py 116 | 117 | # Environments 118 | .env 119 | .venv 120 | env/ 121 | venv/ 122 | ENV/ 123 | env.bak/ 124 | venv.bak/ 125 | 126 | # Spyder project settings 127 | .spyderproject 128 | .spyproject 129 | 130 | # Rope project settings 131 | .ropeproject 132 | 133 | # mkdocs documentation 134 | /site 135 | 136 | # mypy 137 | .mypy_cache/ 138 | .dmypy.json 139 | dmypy.json 140 | 141 | # Pyre type checker 142 | .pyre/ 143 | 144 | legacy/ 145 | futures_strats/ 146 | my_projects/ 147 | testing/ 148 | get_local_keys.py 149 | read_qis_modules.py 150 | TODOs.md 151 | qis/examples/figures/perf1.PNG 152 | qis/examples/figures/perf2.PNG 153 | qis/examples/figures/perf3.PNG 154 | 155 | /quant_strats/ 156 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "qis" 3 | version = "3.2.20" 4 | description = "Implementation of visualisation and reporting analytics for Quantitative Investment Strategies" 5 | license = "LICENSE.txt" 6 | authors = ["Artur Sepp "] 7 | maintainers = ["Artur Sepp "] 8 | readme = "README.md" 9 | repository = "https://github.com/ArturSepp/QuantInvestStrats" 10 | documentation = "https://github.com/ArturSepp/QuantInvestStrats" 11 | keywords= ["quantitative", "investing", "portfolio optimization", "systematic strategies", "volatility"] 12 | classifiers=[ 13 | "Development Status :: 4 - Beta", 14 | "Environment :: Console", 15 | "Intended Audience :: Financial and Insurance Industry", 16 | "Intended Audience :: Science/Research", 17 | "License :: OSI Approved :: MIT License", 18 | "Natural Language :: English", 19 | "Operating System :: OS Independent", 20 | "Programming Language :: Python :: 3.10", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.8", 23 | "Programming Language :: Python :: 3 :: Only", 24 | "Topic :: Office/Business :: Financial :: Investment", 25 | ] 26 | packages = [ {include = "qis"}] 27 | exclude = ["qis/examples/figures/", 28 | "qis/examples/notebooks/"] 29 | 30 | [tool.poetry.urls] 31 | "Issues" = "https://github.com/ArturSepp/QuantInvestStrats/issues" 32 | "Personal website" = "https://artursepp.com" 33 | 34 | [tool.poetry.dependencies] 35 | python = ">=3.8" 36 | numba = ">=0.56.4" 37 | numpy = ">=1.22.4" 38 | scipy = ">=1.10" 39 | statsmodels = ">=0.13.5" 40 | pandas = ">=2.2.0" 41 | matplotlib = ">=3.2.2" 42 | seaborn = ">=0.12.2" 43 | openpyxl = ">=3.0.10" 44 | tabulate = ">=0.9.0" 45 | PyYAML = ">=6.0" 46 | easydev = ">=0.12.0" 47 | psycopg2 = ">=2.9.5" 48 | SQLAlchemy = ">=1.4.46" 49 | pyarrow = ">=10.0.1" 50 | fsspec = ">=2022.11.0" 51 | yfinance = ">=0.1.38" 52 | 53 | [build-system] 54 | requires = ["poetry-core>=1.0.0", "hatchling==1.27.0", "hatch-vcs"] 55 | #build-backend = "hatchling.build" 56 | build-backend = "poetry.core.masonry.api" -------------------------------------------------------------------------------- /pyvenv.cfg: -------------------------------------------------------------------------------- 1 | home = C:\Users\artur\AppData\Local\Programs\Python\Python311 2 | implementation = CPython 3 | version_info = 3.10.9.final.0 4 | virtualenv = 20.13.0 5 | include-system-site-packages = false 6 | base-prefix = C:\Users\artur\AppData\Local\Programs\Python\Python310 7 | base-exec-prefix = C:\Users\artur\AppData\Local\Programs\Python\Python310 8 | base-executable = C:\Users\artur\AppData\Local\Programs\Python\Python310\python.exe 9 | -------------------------------------------------------------------------------- /qis/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | import qis.local_path 3 | 4 | from qis.file_utils import ( 5 | FileTypes, 6 | append_df_to_csv, 7 | append_df_to_feather, 8 | save_figs_to_pdf, 9 | get_all_folder_files, 10 | get_local_file_path, 11 | get_output_path, 12 | get_paths, 13 | get_pdf_path, 14 | get_resource_path, 15 | join_file_name_parts, 16 | load_df_dict_from_csv, 17 | load_df_dict_from_excel, 18 | load_df_dict_from_feather, 19 | load_df_dict_from_parquet, 20 | load_df_dict_from_sql, 21 | load_df_from_csv, 22 | load_df_from_excel, 23 | load_df_from_feather, 24 | load_df_from_parquet, 25 | save_df_dict_to_csv, 26 | save_df_dict_to_excel, 27 | save_df_dict_to_feather, 28 | save_df_dict_to_parquet, 29 | save_df_dict_to_sql, 30 | save_df_to_csv, 31 | save_df_to_excel, 32 | save_df_to_feather, 33 | save_df_to_parquet, 34 | save_fig, 35 | save_figs, 36 | timer 37 | ) 38 | 39 | from qis.utils.__init__ import * 40 | 41 | from qis.perfstats.__init__ import * 42 | 43 | from qis.plots.__init__ import * 44 | 45 | from qis.models.__init__ import * 46 | 47 | from qis.portfolio.__init__ import * 48 | 49 | 50 | -------------------------------------------------------------------------------- /qis/examples/best_returns.py: -------------------------------------------------------------------------------- 1 | 2 | import pandas as pd 3 | import seaborn as sns 4 | import matplotlib.pyplot as plt 5 | from typing import List 6 | from enum import Enum 7 | import yfinance as yf 8 | 9 | 10 | def add_performance_metric(data: pd.DataFrame) -> List[str]: 11 | num_years = (data.index[-1] - data.index[0]).days / 365 12 | ratio = data.iloc[-1] / data.iloc[0] 13 | compounded_return_pa = ratio ** (1.0 / num_years) - 1 14 | print(compounded_return_pa) 15 | titles = [f"{key}: {v:0.0%} p.a." for key, v in compounded_return_pa.to_dict().items()] 16 | return titles 17 | 18 | 19 | class WoType(Enum): 20 | BEST = 1 21 | WORST = 2 22 | BOTH = 3 23 | 24 | 25 | def perf_wo_best_worst(prices: pd.Series, 26 | freq: str = 'ME', 27 | cut_off: int = 1, 28 | wo_type: WoType = WoType.BEST, 29 | ax: plt.Subplot = None 30 | ) -> pd.DataFrame: 31 | 32 | returns = prices.pct_change() 33 | returns_a = returns.groupby(pd.Grouper(freq=freq)) 34 | 35 | wo_best_returns = [] 36 | wo_worst_returns = [] 37 | for key, data in returns_a: 38 | data = data.sort_values() # sort by max returns 39 | wo_best = data.copy() 40 | wo_best.iloc[-cut_off:] = 0.0 41 | wo_best_returns.append(wo_best.sort_index()) # sort back 42 | wo_worst = data.copy() 43 | wo_worst.iloc[:cut_off] = 0.0 44 | #wo_worst.iloc[-cut_off:] = 0.0 45 | wo_worst_returns.append(wo_worst.sort_index()) # sort back 46 | 47 | wo_best_returns = pd.concat(wo_best_returns, axis=0) 48 | wo_worst_returns = pd.concat(wo_worst_returns, axis=0) 49 | 50 | wo_best_perf = wo_best_returns.add(1.0).cumprod(axis=0).multiply(prices.iloc[0]) 51 | wo_worst_perf = wo_worst_returns.add(1.0).cumprod(axis=0).multiply(prices.iloc[0]) 52 | 53 | if wo_type == WoType.BEST: 54 | joint_data = pd.concat([prices, wo_best_perf.rename('W/O Best')], axis=1) 55 | 56 | elif wo_type == WoType.WORST: 57 | joint_data = pd.concat([prices, wo_best_perf.rename('W/O Best'), wo_worst_perf.rename('W/O Worst')], axis=1) 58 | 59 | else: 60 | joint_data = pd.concat([prices, wo_worst_perf.rename('W/O Best and Worst') ], axis=1) 61 | 62 | # ppd.plot_prices(prices=joint_data) 63 | joint_data.columns = add_performance_metric(joint_data) 64 | sns.lineplot(data=joint_data, ax=ax) 65 | 66 | return joint_data 67 | 68 | 69 | class UnitTests(Enum): 70 | PERF1 = 1 71 | 72 | 73 | def run_unit_test(unit_test: UnitTests): 74 | 75 | ticker = 'SPY' 76 | prices = yf.download(ticker, start=None, end=None)['Close'].rename(ticker) 77 | 78 | freq = 'ME' 79 | wo_type = WoType.BEST 80 | if unit_test == UnitTests.PERF1: 81 | fig, ax = plt.subplots(1, 1, figsize=(10, 10), tight_layout=True) 82 | perf_wo_best_worst(prices=prices, freq=freq, wo_type=WoType.WORST, ax=ax) 83 | 84 | plt.show() 85 | 86 | 87 | if __name__ == '__main__': 88 | 89 | unit_test = UnitTests.PERF1 90 | 91 | is_run_all_tests = False 92 | if is_run_all_tests: 93 | for unit_test in UnitTests: 94 | run_unit_test(unit_test=unit_test) 95 | else: 96 | run_unit_test(unit_test=unit_test) 97 | -------------------------------------------------------------------------------- /qis/examples/boxplot_conditional_returns.py: -------------------------------------------------------------------------------- 1 | # packages 2 | import pandas as pd 3 | import matplotlib.pyplot as plt 4 | from enum import Enum 5 | import yfinance as yf 6 | import qis 7 | 8 | 9 | class UnitTests(Enum): 10 | RETURNS_BOXPLOT = 1 11 | 12 | 13 | def run_unit_test(unit_test: UnitTests): 14 | 15 | if unit_test == UnitTests.RETURNS_BOXPLOT: 16 | asset = 'SPY' 17 | regime_benchmark = '^VIX' 18 | prices = yf.download([asset, regime_benchmark], start=None, end=None)['Close'].dropna() 19 | prices = prices.asfreq('W-FRI', method='ffill') 20 | data = pd.concat([prices[asset].pct_change(), # use returns over (t_0, t_1] 21 | prices[regime_benchmark].shift(1) # use level at t_0 22 | ], axis=1).dropna() 23 | 24 | qis.df_boxplot_by_classification_var(df=data, 25 | x=regime_benchmark, 26 | y=asset, 27 | num_buckets=4, 28 | x_hue_name='VIX bucket', 29 | title=f"{asset} returns conditional on {regime_benchmark}", 30 | xvar_format='{:.2f}', 31 | yvar_format='{:.2%}', 32 | add_xy_mean_labels=True) 33 | 34 | plt.show() 35 | 36 | 37 | if __name__ == '__main__': 38 | 39 | unit_test = UnitTests.RETURNS_BOXPLOT 40 | 41 | is_run_all_tests = False 42 | if is_run_all_tests: 43 | for unit_test in UnitTests: 44 | run_unit_test(unit_test=unit_test) 45 | else: 46 | run_unit_test(unit_test=unit_test) 47 | 48 | -------------------------------------------------------------------------------- /qis/examples/btc_asset_corr.py: -------------------------------------------------------------------------------- 1 | """ 2 | compute rolling correlations between crypto and asset 3 | """ 4 | import pandas as pd 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | import seaborn as sns 8 | import qis as qis 9 | import yfinance as yf 10 | 11 | # define asset and cryptocurrency 12 | ASSET = 'QQQ' 13 | CRYPTO = 'BTC-USD' 14 | 15 | tickers = [ASSET, CRYPTO] 16 | # fetch yahoo data 17 | prices = yf.download(tickers, start=None, end=None)['Close'][tickers] 18 | # resample to business days 19 | prices = prices.asfreq('B', method='ffill').dropna() 20 | # % returns 21 | returns = prices.pct_change() 22 | # returns = np.log(prices).diff() 23 | 24 | # compute rolling correlations 25 | corr_3m = returns[CRYPTO].rolling(63).corr(returns[ASSET]).rename('3m') 26 | corr_1y = returns[CRYPTO].rolling(252).corr(returns[ASSET]).rename('1y') 27 | corr_2y = returns[CRYPTO].rolling(2*252).corr(returns[ASSET]).rename('2y') 28 | corrs = pd.concat([corr_3m, corr_1y, corr_2y], axis=1).dropna() 29 | 30 | # select period 31 | time_period = qis.TimePeriod('01Jan2016', None) 32 | corrs = time_period.locate(corrs) 33 | # qis.save_df_to_excel(data=corrs, file_name='btc_spy_corr') 34 | 35 | with sns.axes_style("darkgrid"): 36 | fig, ax = plt.subplots(1, 1, figsize=(15, 8), tight_layout=True) 37 | 38 | qis.plot_time_series(df=corrs, 39 | trend_line=qis.TrendLine.ZERO_SHADOWS, 40 | legend_stats=qis.LegendStats.AVG_LAST_SCORE, 41 | var_format='{:.0%}', 42 | fontsize=14, 43 | title=f"Rolling correlation of daily returns between {CRYPTO} and {ASSET} as function of rolling window", 44 | ax=ax) 45 | # qis.save_fig(fig=fig, file_name='btc_all_corr') 46 | 47 | plt.show() 48 | -------------------------------------------------------------------------------- /qis/examples/constant_notional.py: -------------------------------------------------------------------------------- 1 | """ 2 | run simulation of short exposure strategies 3 | follow the discussion in 4 | https://twitter.com/ArturSepp/status/1614551490093359104 5 | to run the code, install qis package (Quantitative Investment Strategies): 6 | pip install qis 7 | """ 8 | 9 | # imports 10 | import numpy as np 11 | import pandas as pd 12 | import seaborn as sns 13 | import matplotlib.pyplot as plt 14 | import yfinance as yf 15 | import qis 16 | 17 | # load dprice data for given ticker 18 | ticker = 'SPY' 19 | price = yf.download(ticker, start=None, end=None)['Close'].rename(ticker) 20 | price = price.loc['2016':] 21 | price_np = price.to_numpy() 22 | 23 | # specify position 24 | long_or_short = -1.0 25 | constant_notional = 1.0 26 | 27 | # start backtest 28 | # this will track constant notional strategy 29 | constant_notional_units, constant_notional_cum_nav = np.zeros_like(price_np), np.zeros_like(price_np) 30 | constant_notional_units[0], constant_notional_cum_nav[0] = long_or_short * constant_notional / price_np[0], constant_notional 31 | 32 | # this will track constant nav exposure strategy which is equivalent to long or short leverage ETF 33 | short_etf_units, short_etf_cum_nav = np.zeros_like(price_np), np.zeros_like(price_np) 34 | short_etf_units[0], short_etf_cum_nav[0] = long_or_short * constant_notional / price_np[0], constant_notional 35 | 36 | for idx, (price0, price1) in enumerate(zip(price_np[:-1], price_np[1:])): 37 | # time_t = idx+1 38 | constant_notional_cum_nav[idx+1] = constant_notional_cum_nav[idx] + constant_notional_units[idx] * (price1-price0) 39 | # exposure in unit is constant for the next step and computed using eod price 40 | constant_notional_units[idx+1] = long_or_short*constant_notional / price1 41 | 42 | short_etf_cum_nav[idx+1] = short_etf_cum_nav[idx] + short_etf_units[idx] * (price1-price0) 43 | # etf is rebalanced proportionally to the current eod nav 44 | short_etf_units[idx+1] = long_or_short*short_etf_cum_nav[idx+1]/price1 45 | 46 | # store 47 | constant_notional_cum_nav = pd.Series(constant_notional_cum_nav, index=price.index, name='constant_notional_cum_nav') 48 | short_etf_cum_nav = pd.Series(short_etf_cum_nav, index=price.index, name='short_etf_cum_nav') 49 | 50 | # add simple byu and hold starts 51 | buy_and_hold_units = np.ones_like(price_np) * constant_notional / price_np[0] 52 | buy_and_hold_cum_nav = pd.Series(constant_notional + buy_and_hold_units*(price_np-price_np[0]), index=price.index, name='buy_and_hold_cum_nav') 53 | 54 | sell_and_hold_units = - np.ones_like(price_np) * constant_notional / price_np[0] 55 | sell_and_hold_cum_nav = pd.Series(constant_notional + sell_and_hold_units*(price_np-price_np[0]), index=price.index, name='sell_and_hold_cum_nav') 56 | 57 | # prices 58 | prices = pd.concat([buy_and_hold_cum_nav, sell_and_hold_cum_nav, constant_notional_cum_nav, short_etf_cum_nav], axis=1) 59 | 60 | # portfolio units 61 | buy_and_hold_units = pd.Series(buy_and_hold_units, index=price.index, name='buy_and_hold_units') 62 | sell_and_hold_units = pd.Series(sell_and_hold_units, index=price.index, name='sell_and_hold_units') 63 | constant_notional_units = pd.Series(constant_notional_units, index=price.index, name='constant_notional_units') 64 | short_etf_units = pd.Series(short_etf_units, index=price.index, name='short_etf_units') 65 | portfolio_units = pd.concat([buy_and_hold_units, sell_and_hold_units, constant_notional_units, short_etf_units], axis=1) 66 | 67 | 68 | with sns.axes_style("darkgrid"): 69 | fig, axs = plt.subplots(2, 1, figsize=(9, 7), tight_layout=True) 70 | 71 | # plot performance 72 | qis.plot_prices_with_dd(prices=prices, 73 | perf_stats_labels=qis.PerfStatsLabels.TOTAL.value, 74 | title=f"Realized performance of strategies with short exposure to {ticker}", 75 | axs=axs) 76 | 77 | # plot units 78 | fig, ax = plt.subplots(1, 1, figsize=(9, 7), tight_layout=True) 79 | qis.plot_time_series(df=portfolio_units, 80 | title='Portfolio Units', 81 | ax=ax) 82 | 83 | fig, ax = plt.subplots(1, 1, figsize=(9, 7), tight_layout=True) 84 | qis.plot_returns_scatter(prices=prices, 85 | benchmark=prices.columns[0], 86 | freq='W-MON', 87 | return_type=qis.ReturnTypes.DIFFERENCE, # cannot use log and relative returns 88 | title='Scatterplot of weekly P&L relative to buy-and-hold', 89 | ylabel='Daily P&L of short strategies', 90 | xlabel='Daily P&L of buy-and-hold', 91 | var_format='{:,.0%}', 92 | ax=ax) 93 | 94 | plt.show() 95 | -------------------------------------------------------------------------------- /qis/examples/constant_weight_portfolios.py: -------------------------------------------------------------------------------- 1 | """ 2 | example of creating 60/40 equity bon portfolio 3 | """ 4 | import pandas as pd 5 | import matplotlib.pyplot as plt 6 | import seaborn as sns 7 | import qis as qis 8 | import yfinance as yf 9 | 10 | tickers_weights = dict(SPY=0.6, IEF=0.4) 11 | tickers = list(tickers_weights.keys()) 12 | prices = yf.download(tickers, start=None, end=None)['Close'][tickers] 13 | prices = prices.asfreq('B', method='ffill').dropna() 14 | 15 | balanced_60_40a = qis.backtest_model_portfolio(prices=prices, weights=tickers_weights, rebalancing_freq='QE', 16 | ticker='Zero Cost').get_portfolio_nav() 17 | balanced_60_40b = qis.backtest_model_portfolio(prices=prices, weights=tickers_weights, rebalancing_freq='QE', 18 | ticker='2% Cost', 19 | management_fee=0.02).get_portfolio_nav() 20 | navs = pd.concat([balanced_60_40a, balanced_60_40b], axis=1) 21 | 22 | with sns.axes_style("darkgrid"): 23 | fig, axs = plt.subplots(2, 1, figsize=(15, 12), tight_layout=True) 24 | qis.plot_prices_with_dd(prices=navs, axs=axs) 25 | 26 | plt.show() 27 | -------------------------------------------------------------------------------- /qis/examples/core/perf_bbg_prices.py: -------------------------------------------------------------------------------- 1 | # packages 2 | import matplotlib.pyplot as plt 3 | import qis as qis 4 | from enum import Enum 5 | from bbg_fetch import fetch_field_timeseries_per_tickers 6 | 7 | 8 | def run_report(): 9 | # tickers = {'TY1 Comdty': '10y', 'UXY1 Comdty': '10y Ultra'} 10 | tickers = { 11 | 'SPTR Index': 'SPTR Index', 12 | 'CIEQVEHG Index': 'Citi SPX 0D Vol Carry', 13 | 'CIEQVRUG Index': 'Citi SX5E 1W Vol Carry', 14 | 'CICXCOSE Index': 'Citi Brent Vol Carry', 15 | 'GSISXC07 Index': 'GS Multi Asset Carry', 16 | 'GSISXC11 Index': 'GS Macro Carry', 17 | 'XUBSPGRA Index': 'UBS Gold Strangles', 18 | 'XUBSU1D1 Index': 'UBS Short Vol Daily', 19 | 'BCKTARU2 Index': 'BNP Call on Short-vol Carry', 20 | 'BNPXAUUS Index': 'BNP Intraday SPX Vol Carry', 21 | 'BNPXAUTS Index': 'BNP Intraday NDX Vol Carry', 22 | 'BNPXOV3U Index': 'BNP 3M Long DHhedged Puts' 23 | } 24 | 25 | benchmark = 'HYG US Equity' 26 | tickers = { 27 | benchmark: benchmark, 28 | 'NMVVR1EL Index': 'IRVING1 EUR', 29 | 'NMVVR1UL Index': 'IRVING1 USD', 30 | 'NMVVR1L Index': 'IRVING1', 31 | 'BNPXLVRE Index': 'BNP Long Rates Vol EUR', 32 | 'BNPXLVRU Index': 'BNP Long Rates Vol USD', 33 | 'BXIIULSV Index': 'Barclays Long Rates Vol', 34 | 'BXIIUGNT Index': 'Barclays Gamma Neutral Vol', 35 | 'BXIIUENT Index': 'Barclays Triangle Vol' 36 | } 37 | 38 | benchmark = 'SPTR Index' 39 | tickers = { 40 | benchmark: benchmark, 41 | 'BNPIV1EE Index': 'BNP Europe 1Y Volatility', 42 | 'BNPIV1UE Index': 'BNP US 1Y Volatility', 43 | 'BNPXVO3A Index': 'BNP VOLA 3 Index', 44 | 'AIJPVT1U Index': 'JPM Volatility Trend Following', 45 | 'JPOSLVUS Index': 'JPM US Long Variance', 46 | 'JPOSPRU2 Index': 'JPM US Put Ratio', 47 | 'JPOSTUDN Index': 'JPM US Equity Tail Hedge', 48 | 'JPRC85BE Index': 'JPM Dynamic 85% Rolling Collar EU', 49 | 'JPRC85BU Index': 'JPM Dynamic 85% Rolling Collar US', 50 | 'JPUSVXCR Index': 'JPM US Volatility Call Ratio' 51 | } 52 | 53 | benchmark = 'XNDX Index' 54 | tickers = { 55 | benchmark: benchmark, 56 | 'BNPXTHUE Index': 'Thalia', 57 | 'BNPXTHUN Index': 'Thalia Neutral', 58 | 'BNPXTDUE Index': 'Thalia Dynamic', 59 | 'BNPXTDUN Index': 'Thalia Neutral Dynamic', 60 | 'BNPXLVRU Index': 'BNP Long Rates Vol USD' 61 | } 62 | 63 | prices = fetch_field_timeseries_per_tickers(tickers=tickers, freq='B', field='PX_LAST').ffill() 64 | print(prices) 65 | # qis.save_df_to_csv(df=prices, file_name='qis_vol_indices', local_path=qis.get_output_path()) 66 | 67 | time_period = qis.TimePeriod('31Dec2021', '17Apr2025') 68 | # kwargs = qis.fetch_default_report_kwargs(time_period=time_period, add_rates_data=False) 69 | # kwargs = qis.fetch_factsheet_config_kwargs(factsheet_config=qis.FACTSHEET_CONFIG_DAILY_DATA_SHORT_PERIOD, add_rates_data=False) 70 | kwargs = qis.fetch_factsheet_config_kwargs(factsheet_config=qis.FACTSHEET_CONFIG_DAILY_DATA_LONG_PERIOD, add_rates_data=False) 71 | 72 | fig = qis.generate_multi_asset_factsheet(prices=prices, 73 | benchmark=benchmark, 74 | time_period=time_period, 75 | **kwargs) 76 | qis.save_figs_to_pdf(figs=[fig], 77 | file_name=f"bbg_multiasset_report", orientation='landscape', 78 | # local_path='C://Users//uarts//outputs//', 79 | local_path=qis.get_output_path() 80 | ) 81 | 82 | 83 | def run_price(): 84 | tickers = {'CL1 Comdty': 'WTI'} 85 | prices = fetch_field_timeseries_per_tickers(tickers=tickers, freq='B', field='PX_LAST').ffill() 86 | print(prices) 87 | 88 | time_period = qis.TimePeriod('31Dec1989', '08Nov2024') 89 | prices = time_period.locate(prices) 90 | 91 | qis.plot_prices_with_dd(prices, 92 | start_to_one=False) 93 | 94 | 95 | class UnitTests(Enum): 96 | REPORT = 1 97 | PRICE = 2 98 | 99 | 100 | def run_unit_test(unit_test: UnitTests): 101 | 102 | if unit_test == UnitTests.REPORT: 103 | run_report() 104 | 105 | elif unit_test == UnitTests.PRICE: 106 | run_price() 107 | 108 | plt.show() 109 | 110 | 111 | if __name__ == '__main__': 112 | 113 | unit_test = UnitTests.REPORT 114 | 115 | is_run_all_tests = False 116 | if is_run_all_tests: 117 | for unit_test in UnitTests: 118 | run_unit_test(unit_test=unit_test) 119 | else: 120 | run_unit_test(unit_test=unit_test) 121 | 122 | 123 | plt.show() 124 | -------------------------------------------------------------------------------- /qis/examples/credit_spreads.py: -------------------------------------------------------------------------------- 1 | 2 | import pandas as pd 3 | import numpy as np 4 | import seaborn as sns 5 | import matplotlib.pyplot as plt 6 | import qis as qis 7 | from qis import PerfStat 8 | from bbg_fetch import fetch_field_timeseries_per_tickers 9 | 10 | assets = {'BASPCAAA Index': 'AAA10y', 11 | 'BICLA10Y Index': 'A10y', 12 | 'BICLB10Y Index': 'BAA10y', 13 | 'CSI BARC Index': 'HY 10Y'} 14 | 15 | spreads = fetch_field_timeseries_per_tickers(tickers=list(assets.keys()), field='PX_LAST', CshAdjNormal=True) 16 | spreads = spreads.rename(assets, axis=1) 17 | spreads['HY 10Y'] = 100.0*spreads['HY 10Y'] 18 | # to % 19 | spreads = spreads / 10000.0 20 | 21 | predictors = {'SPXT Index': 'SPX', 'gt10 Govt': 'UST10y'} 22 | benchmarks = fetch_field_timeseries_per_tickers(tickers=list(predictors.keys()), field='PX_LAST', CshAdjNormal=True) 23 | benchmarks = benchmarks.rename(predictors, axis=1) 24 | print(benchmarks) 25 | qis.plot_time_series(spreads, 26 | var_format='{:,.2%}', x_date_freq='YE') 27 | 28 | freq = 'ME' 29 | 30 | df1 = pd.concat([benchmarks['SPX'].asfreq(freq, method='ffill').pct_change(), 31 | spreads.asfreq(freq, method='ffill').diff()], axis=1).dropna() 32 | 33 | qis.plot_scatter(df=df1, x='SPX') 34 | 35 | df2 = pd.concat([benchmarks['UST10y'].asfreq(freq, method='ffill').diff()/100.0, 36 | spreads.asfreq(freq, method='ffill').diff()], axis=1).dropna() 37 | 38 | qis.plot_scatter(df=df2, x='UST10y') 39 | 40 | plt.show() 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /qis/examples/europe_futures.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import matplotlib.pyplot as plt 3 | import seaborn as sns 4 | import qis as qis 5 | from bbg_fetch import fetch_field_timeseries_per_tickers 6 | 7 | # tickers = {'CF1 Index': 'CAC', 'GX1 Index': 'DAX', 'IB1 Index': 'IBEX', 'ST1 Index': 'MIB', 'EO1 Index': 'AEX'} 8 | tickers = {'CF1 Index': 'CAC', 'GX1 Index': 'DAX', 'IB1 Index': 'IBEX', 'ST1 Index': 'MIB'} 9 | start_date = pd.Timestamp('1Jan1990') 10 | 11 | futures_prices = fetch_field_timeseries_per_tickers(tickers=tickers, freq='B', field='PX_LAST', 12 | start_date=start_date).ffill() 13 | # use 3m rolling volumes 14 | volumes = fetch_field_timeseries_per_tickers(tickers=tickers, freq='B', field='VOLUME', 15 | start_date=start_date).ffill().rolling(63).mean() 16 | price_volumes = volumes.multiply(futures_prices).dropna(how='all') 17 | 18 | # create volume-weighted portfolios 19 | futures_weights = qis.df_to_weight_allocation_sum1(df=price_volumes.asfreq('QE').ffill()) 20 | 21 | # plot futures price data 22 | kwargs = dict(x_date_freq='YE') 23 | with sns.axes_style("darkgrid"): 24 | fig, axs = plt.subplots(2, 2, figsize=(10, 10), tight_layout=True) 25 | qis.plot_prices_with_dd(prices=futures_prices, axs=axs[:, 0], **kwargs) 26 | qis.plot_time_series(df=volumes, title='Contact volumes', ax=axs[0, 1], **kwargs) 27 | qis.plot_time_series(df=price_volumes, title='Value volumes', ax=axs[1, 1], **kwargs) 28 | 29 | 30 | # add long cash SPTR 31 | sptr_prices = fetch_field_timeseries_per_tickers(tickers={'SPTR Index': 'SPTR'}, freq='B', field='PX_LAST', 32 | start_date=start_date).ffill() 33 | long_sptr_short_futures_weights = pd.concat([pd.Series(1.0, index=futures_weights.index, name='SPTR'), 34 | -1.0*futures_weights], axis=1) 35 | print(long_sptr_short_futures_weights) 36 | 37 | prices = pd.concat([sptr_prices, futures_prices], axis=1) 38 | 39 | # backtest total return portfolio 40 | time_period = qis.TimePeriod('31Dec1990', '27Sep2024') 41 | portfolio_data = qis.backtest_model_portfolio(prices=time_period.locate(prices), 42 | weights=time_period.locate(long_sptr_short_futures_weights), 43 | rebalancing_costs=0.0005, 44 | weight_implementation_lag=1, 45 | ticker='Long SPTR / Short EU futures') 46 | 47 | # create factcheet 48 | figs = qis.generate_strategy_factsheet(portfolio_data=portfolio_data, 49 | benchmark_prices=sptr_prices.iloc[:, 0].rename('SPTR long').to_frame(), 50 | add_current_position_var_risk_sheet=True, 51 | time_period=time_period, 52 | **qis.fetch_default_report_kwargs(time_period=time_period)) 53 | qis.save_figs_to_pdf(figs=figs, 54 | file_name=f"long_sptr_short_eu_strategy_factsheet", 55 | local_path=qis.local_path.get_output_path()) 56 | 57 | plt.show() 58 | -------------------------------------------------------------------------------- /qis/examples/factsheets/multi_assets.py: -------------------------------------------------------------------------------- 1 | """ 2 | performance report for a universe of several assets 3 | with comparison to 1-2 benchmarks 4 | output is one-page figure with key numbers 5 | """ 6 | # packages 7 | import matplotlib.pyplot as plt 8 | import yfinance as yf 9 | import qis as qis 10 | from enum import Enum 11 | from qis.portfolio.reports.multi_assets_factsheet import generate_multi_asset_factsheet 12 | from qis.portfolio.reports.config import fetch_default_report_kwargs 13 | 14 | 15 | class UnitTests(Enum): 16 | CORE_ETFS = 1 17 | BTC_SQQQ = 2 18 | HEDGED_ETFS = 3 19 | BBG = 4 20 | RATES_FUTURES = 5 21 | HYG_ETFS = 6 22 | 23 | 24 | def run_unit_test(unit_test: UnitTests): 25 | 26 | end_date = '21Apr2025' # performance repoting 27 | 28 | prices = None # if Noe, use yahoo finance data 29 | 30 | if unit_test == UnitTests.CORE_ETFS: 31 | benchmark = 'SPY' 32 | tickers = [benchmark, 'QQQ', 'EEM', 'TLT', 'IEF', 'LQD', 'HYG', 'SHY', 'GLD'] 33 | time_period = qis.TimePeriod('31Dec2007', end_date) # time period for reporting 34 | 35 | elif unit_test == UnitTests.BTC_SQQQ: 36 | benchmark = 'QQQ' 37 | tickers = [benchmark, 'BTC-USD', 'TQQQ', 'SQQQ'] 38 | time_period = qis.TimePeriod('31Dec2019', end_date) 39 | 40 | elif unit_test == UnitTests.HEDGED_ETFS: 41 | benchmark = 'SPY' 42 | tickers = [benchmark, 'SHY', 'LQDH', 'HYGH', 'FLOT'] 43 | time_period = qis.TimePeriod('27May2014', end_date) 44 | 45 | elif unit_test == UnitTests.BBG: 46 | benchmark = 'SPTR' 47 | tickers = {'SPTR Index': benchmark, 48 | 'SGEPMLU Index': 'SG AI long/short', 'SGEPMLUL Index': 'SG AI long', 'SGEPMLUS Index': 'SG AI short', 49 | 'SGIXAI Index': 'SG Alpha', 50 | 'AIPEX Index': 'HSBC Aipex', 51 | 'MQQFUSAI Index': 'MerQube AI', 52 | 'CSRPAIS Index': 'CS RavenPack', 53 | 'CITIMSTR Index': 'Citi Grand'} 54 | time_period = qis.TimePeriod('31Dec2019', end_date) 55 | from bbg_fetch import fetch_field_timeseries_per_tickers 56 | prices = fetch_field_timeseries_per_tickers(tickers=list(tickers.keys()), field='PX_LAST', CshAdjNormal=True).dropna() 57 | prices = prices.rename(tickers, axis=1) 58 | 59 | elif unit_test == UnitTests.RATES_FUTURES: 60 | benchmark = '2y UST' 61 | tickers = {'TU1 Comdty': benchmark, 62 | 'ED5 Comdty': 'USD IR', 63 | 'L 5 Comdty': 'GBP IR', 64 | 'ER4 Comdty': 'EUR IR', 65 | 'IR4 Comdty': 'AUD IR', 66 | 'COR4 Comdty': 'CAD IR'} 67 | time_period = qis.TimePeriod(start='02Apr1986', end=end_date) 68 | from bbg_fetch import fetch_field_timeseries_per_tickers 69 | prices = fetch_field_timeseries_per_tickers(tickers=list(tickers.keys()), field='PX_LAST', CshAdjNormal=True).dropna() 70 | prices = prices.rename(tickers, axis=1) 71 | 72 | elif unit_test == UnitTests.HYG_ETFS: 73 | benchmark = 'IBOXHY' 74 | tickers = {'IBOXHY Index': benchmark, 75 | 'HYDB US Equity': 'HYDB', 76 | 'HYG US Equity': 'HYG' 77 | } 78 | from bbg_fetch import fetch_field_timeseries_per_tickers 79 | prices = fetch_field_timeseries_per_tickers(tickers=tickers, field='PX_LAST', CshAdjNormal=True, freq='B').dropna() 80 | time_period = qis.get_time_period(prices) 81 | 82 | else: 83 | raise NotImplementedError 84 | 85 | if prices is None: 86 | prices = yf.download(tickers=tickers, start=None, end=None, ignore_tz=True)['Close'][tickers] 87 | 88 | prices = prices.asfreq('B', method='ffill') # make B frequency 89 | prices = prices.dropna() 90 | print(prices) 91 | 92 | kwargs = fetch_default_report_kwargs(time_period=time_period) 93 | fig = generate_multi_asset_factsheet(prices=prices, 94 | benchmark=benchmark, 95 | time_period=time_period, 96 | **kwargs) 97 | qis.save_figs_to_pdf(figs=[fig], 98 | file_name=f"multiasset_report", orientation='landscape', 99 | local_path=qis.local_path.get_output_path()) 100 | qis.save_fig(fig=fig, file_name=f"multiassets", local_path=qis.local_path.get_output_path()) 101 | 102 | plt.show() 103 | 104 | 105 | if __name__ == '__main__': 106 | 107 | unit_test = UnitTests.CORE_ETFS 108 | 109 | is_run_all_tests = False 110 | if is_run_all_tests: 111 | for unit_test in UnitTests: 112 | run_unit_test(unit_test=unit_test) 113 | else: 114 | run_unit_test(unit_test=unit_test) 115 | -------------------------------------------------------------------------------- /qis/examples/factsheets/multi_strategy.py: -------------------------------------------------------------------------------- 1 | """ 2 | multi strategy backtest is generated using same strategy with a set of different model parameters 3 | """ 4 | # packages 5 | import pandas as pd 6 | import matplotlib.pyplot as plt 7 | from typing import Tuple, List 8 | from enum import Enum 9 | import yfinance as yf 10 | import qis 11 | from qis import TimePeriod, MultiPortfolioData, generate_multi_portfolio_factsheet, fetch_default_report_kwargs 12 | 13 | 14 | def fetch_universe_data() -> Tuple[pd.DataFrame, pd.DataFrame, pd.Series]: 15 | """ 16 | define custom universe with asset class grouping 17 | """ 18 | universe_data = dict(SPY='Equities', 19 | QQQ='Equities', 20 | EEM='Equities', 21 | TLT='Bonds', 22 | IEF='Bonds', 23 | LQD='Credit', 24 | HYG='HighYield', 25 | GLD='Gold') 26 | tickers = list(universe_data.keys()) 27 | group_data = pd.Series(universe_data) # for portfolio reporting 28 | prices = yf.download(tickers=tickers, start=None, end=None, ignore_tz=True)['Close'][tickers] 29 | prices = prices.asfreq('B', method='ffill') 30 | benchmark_prices = prices[['SPY', 'TLT']] 31 | return prices, benchmark_prices, group_data 32 | 33 | 34 | def generate_volparity_multi_strategy(prices: pd.DataFrame, 35 | benchmark_prices: pd.DataFrame, 36 | group_data: pd.Series, 37 | time_period: TimePeriod, 38 | spans: List[int] = (5, 10, 20, 40, 60, 120), 39 | vol_target: float = 0.15, 40 | rebalancing_costs: float = 0.0010 41 | ) -> MultiPortfolioData: 42 | """ 43 | generate volparity sensitivity to span 44 | """ 45 | returns = qis.to_returns(prices=prices, is_log_returns=True) 46 | 47 | portfolio_datas = [] 48 | for span in spans: 49 | ra_returns, weights, ewm_vol = qis.compute_ra_returns(returns=returns, span=span, vol_target=vol_target) 50 | weights = weights.divide(weights.sum(1), axis=0) 51 | portfolio_data = qis.backtest_model_portfolio(prices=prices, 52 | weights=time_period.locate(weights), 53 | rebalancing_costs=rebalancing_costs, 54 | ticker=f"VP span={span}") 55 | portfolio_data.set_group_data(group_data=group_data, group_order=list(group_data.unique())) 56 | portfolio_datas.append(portfolio_data) 57 | 58 | multi_portfolio_data = MultiPortfolioData(portfolio_datas, benchmark_prices=benchmark_prices) 59 | return multi_portfolio_data 60 | 61 | 62 | class UnitTests(Enum): 63 | VOLPARITY_SPAN = 1 64 | 65 | 66 | def run_unit_test(unit_test: UnitTests): 67 | 68 | if unit_test == UnitTests.VOLPARITY_SPAN: 69 | # time period for portfolio reporting 70 | time_period = qis.TimePeriod('31Dec2005', '21Apr2025') 71 | 72 | prices, benchmark_prices, group_data = fetch_universe_data() 73 | multi_portfolio_data = generate_volparity_multi_strategy(prices=prices, 74 | benchmark_prices=benchmark_prices, 75 | group_data=group_data, 76 | time_period=time_period, 77 | vol_target=0.15, 78 | rebalancing_costs=0.0010 # per traded volume 79 | ) 80 | 81 | figs = generate_multi_portfolio_factsheet(multi_portfolio_data=multi_portfolio_data, 82 | time_period=time_period, 83 | add_group_exposures_and_pnl=True, 84 | **fetch_default_report_kwargs(time_period=time_period)) 85 | 86 | qis.save_fig(fig=figs[0], file_name=f"multi_strategy", local_path=qis.local_path.get_output_path()) 87 | 88 | qis.save_figs_to_pdf(figs=figs, 89 | file_name=f"volparity_span_factsheet_long", 90 | orientation='landscape', 91 | local_path=qis.local_path.get_output_path()) 92 | 93 | time_period_short = TimePeriod('31Dec2019', time_period.end) 94 | figs = generate_multi_portfolio_factsheet(multi_portfolio_data=multi_portfolio_data, 95 | time_period=time_period_short, 96 | add_group_exposures_and_pnl=True, 97 | **fetch_default_report_kwargs(time_period=time_period_short)) 98 | qis.save_figs_to_pdf(figs=figs, 99 | file_name=f"volparity_span_factsheet_short", 100 | orientation='landscape', 101 | local_path=qis.local_path.get_output_path()) 102 | # plt.show() 103 | 104 | 105 | if __name__ == '__main__': 106 | 107 | unit_test = UnitTests.VOLPARITY_SPAN 108 | 109 | is_run_all_tests = False 110 | if is_run_all_tests: 111 | for unit_test in UnitTests: 112 | run_unit_test(unit_test=unit_test) 113 | else: 114 | run_unit_test(unit_test=unit_test) 115 | -------------------------------------------------------------------------------- /qis/examples/factsheets/pyblogs_reports.py: -------------------------------------------------------------------------------- 1 | 2 | import pandas as pd 3 | from typing import Tuple, List 4 | from enum import Enum 5 | import yfinance as yf 6 | import qis 7 | from qis import TimePeriod, MultiPortfolioData 8 | from qis.portfolio.reports.config import fetch_default_report_kwargs 9 | 10 | # multiportfolio 11 | from qis.portfolio.reports.multi_strategy_factseet_pybloqs import generate_multi_portfolio_factsheet_with_pyblogs 12 | from qis.portfolio.reports.strategy_benchmark_factsheet_pybloqs import generate_strategy_benchmark_factsheet_with_pyblogs 13 | 14 | 15 | def fetch_riskparity_universe_data() -> Tuple[pd.DataFrame, pd.DataFrame, pd.Series]: 16 | """ 17 | define custom universe with asset class grouping 18 | """ 19 | universe_data = dict(SPY='Equities', 20 | QQQ='Equities', 21 | EEM='Equities', 22 | TLT='Bonds', 23 | IEF='Bonds', 24 | LQD='Credit', 25 | HYG='HighYield', 26 | GLD='Gold') 27 | tickers = list(universe_data.keys()) 28 | group_data = pd.Series(universe_data) # for portfolio reporting 29 | prices = yf.download(tickers=tickers, start=None, end=None, ignore_tz=True)['Close'][tickers] 30 | prices = prices.asfreq('B', method='ffill').loc['2003': ] 31 | benchmark_prices = prices[['SPY', 'TLT']] 32 | return prices, benchmark_prices, group_data 33 | 34 | 35 | def generate_volparity_multi_strategy(prices: pd.DataFrame, 36 | benchmark_prices: pd.DataFrame, 37 | group_data: pd.Series, 38 | time_period: TimePeriod, 39 | spans: List[int] = (5, 10, 20, 40, 60, 120), 40 | vol_target: float = 0.15, 41 | rebalancing_costs: float = 0.0010 42 | ) -> MultiPortfolioData: 43 | """ 44 | generate volparity sensitivity to span 45 | """ 46 | returns = qis.to_returns(prices=prices, is_log_returns=True) 47 | 48 | portfolio_datas = [] 49 | for span in spans: 50 | ra_returns, weights, ewm_vol = qis.compute_ra_returns(returns=returns, span=span, vol_target=vol_target) 51 | weights = weights.divide(weights.sum(1), axis=0) 52 | portfolio_data = qis.backtest_model_portfolio(prices=prices, 53 | weights=time_period.locate(weights), 54 | rebalancing_costs=rebalancing_costs, 55 | ticker=f"VP span={span}") 56 | portfolio_data.set_group_data(group_data=group_data, group_order=list(group_data.unique())) 57 | portfolio_datas.append(portfolio_data) 58 | 59 | multi_portfolio_data = MultiPortfolioData(portfolio_datas, benchmark_prices=benchmark_prices) 60 | return multi_portfolio_data 61 | 62 | 63 | class UnitTests(Enum): 64 | MULTI_PORTFOLIO = 1 65 | STRATEGY_BENCHMARK = 2 66 | 67 | 68 | def run_unit_test(unit_test: UnitTests): 69 | 70 | time_period = qis.TimePeriod('31Dec2005', '21Apr2025') # time period for portfolio reporting 71 | 72 | prices, benchmark_prices, group_data = fetch_riskparity_universe_data() 73 | multi_portfolio_data = generate_volparity_multi_strategy(prices=prices, 74 | benchmark_prices=benchmark_prices, 75 | group_data=group_data, 76 | time_period=time_period, 77 | vol_target=0.15, 78 | rebalancing_costs=0.0010 # per traded volume 79 | ) 80 | 81 | if unit_test == UnitTests.MULTI_PORTFOLIO: 82 | 83 | report = generate_multi_portfolio_factsheet_with_pyblogs(multi_portfolio_data=multi_portfolio_data, 84 | time_period=time_period, 85 | **fetch_default_report_kwargs(time_period=time_period)) 86 | 87 | filename = f"{qis.local_path.get_output_path()}_volparity_span_report_{pd.Timestamp.now().strftime('%Y%m%d_%H%M')}.pdf" 88 | report.save(filename) 89 | print(f"saved allocation report to {filename}") 90 | 91 | elif unit_test == UnitTests.STRATEGY_BENCHMARK: 92 | 93 | report = generate_strategy_benchmark_factsheet_with_pyblogs(multi_portfolio_data=multi_portfolio_data, 94 | strategy_idx=-1, 95 | benchmark_idx=0, 96 | time_period=time_period, 97 | **fetch_default_report_kwargs(time_period=time_period)) 98 | 99 | filename = f"{qis.local_path.get_output_path()}_volparity_pybloq_report_{pd.Timestamp.now().strftime('%Y%m%d_%H%M')}.pdf" 100 | report.save(filename) 101 | print(f"saved allocation report to {filename}") 102 | 103 | 104 | if __name__ == '__main__': 105 | 106 | unit_test = UnitTests.STRATEGY_BENCHMARK 107 | 108 | is_run_all_tests = False 109 | if is_run_all_tests: 110 | for unit_test in UnitTests: 111 | run_unit_test(unit_test=unit_test) 112 | else: 113 | run_unit_test(unit_test=unit_test) 114 | -------------------------------------------------------------------------------- /qis/examples/figures/brinson_attribution.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/QuantInvestStrats/03a55416465f7bdebeae31c3fd95e6abffde3e5a/qis/examples/figures/brinson_attribution.PNG -------------------------------------------------------------------------------- /qis/examples/figures/multi_strategy.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/QuantInvestStrats/03a55416465f7bdebeae31c3fd95e6abffde3e5a/qis/examples/figures/multi_strategy.PNG -------------------------------------------------------------------------------- /qis/examples/figures/multiassets.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/QuantInvestStrats/03a55416465f7bdebeae31c3fd95e6abffde3e5a/qis/examples/figures/multiassets.PNG -------------------------------------------------------------------------------- /qis/examples/figures/perf1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/QuantInvestStrats/03a55416465f7bdebeae31c3fd95e6abffde3e5a/qis/examples/figures/perf1.PNG -------------------------------------------------------------------------------- /qis/examples/figures/perf2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/QuantInvestStrats/03a55416465f7bdebeae31c3fd95e6abffde3e5a/qis/examples/figures/perf2.PNG -------------------------------------------------------------------------------- /qis/examples/figures/perf3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/QuantInvestStrats/03a55416465f7bdebeae31c3fd95e6abffde3e5a/qis/examples/figures/perf3.PNG -------------------------------------------------------------------------------- /qis/examples/figures/perf4.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/QuantInvestStrats/03a55416465f7bdebeae31c3fd95e6abffde3e5a/qis/examples/figures/perf4.PNG -------------------------------------------------------------------------------- /qis/examples/figures/pnl_attribution.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/QuantInvestStrats/03a55416465f7bdebeae31c3fd95e6abffde3e5a/qis/examples/figures/pnl_attribution.PNG -------------------------------------------------------------------------------- /qis/examples/figures/strategy1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/QuantInvestStrats/03a55416465f7bdebeae31c3fd95e6abffde3e5a/qis/examples/figures/strategy1.PNG -------------------------------------------------------------------------------- /qis/examples/figures/strategy2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/QuantInvestStrats/03a55416465f7bdebeae31c3fd95e6abffde3e5a/qis/examples/figures/strategy2.PNG -------------------------------------------------------------------------------- /qis/examples/figures/strategy3.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/QuantInvestStrats/03a55416465f7bdebeae31c3fd95e6abffde3e5a/qis/examples/figures/strategy3.PNG -------------------------------------------------------------------------------- /qis/examples/figures/strategy_benchmark.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/QuantInvestStrats/03a55416465f7bdebeae31c3fd95e6abffde3e5a/qis/examples/figures/strategy_benchmark.PNG -------------------------------------------------------------------------------- /qis/examples/generate_option_rolls.py: -------------------------------------------------------------------------------- 1 | """ 2 | the systematic rolls for option or futures at a given timestamp include: 3 | selection of the roll expiry given by parameter roll_freq (roll_freq='W-FRI', 'M-FRI') 4 | roll time set using roll_hour 5 | when the roll references second to first available roll expiry set by min_days_to_next_roll 6 | example on how to generate generate roll schedule: for each timestamp find the corresponding maturity of the next roll 7 | returned pd.DateTimeIndex is the current observation time and value is the corresponding option expiry of the current roll. 8 | """ 9 | import pandas as pd 10 | pd.set_option('display.max_rows', 500) 11 | import qis 12 | from qis import TimePeriod 13 | 14 | # set time period to fill with the roll dates 15 | time_period = TimePeriod('01Jun2023', '08Sep2023', tz='UTC') 16 | 17 | # weekly on Friday 18 | weekly_rolls = qis.generate_fixed_maturity_rolls(time_period=time_period, 19 | freq='D', # frequency of the returned datetime index 20 | roll_freq='W-FRI', # roll expiries frequency 21 | roll_hour=8, # hour of the roll expiry 22 | min_days_to_next_roll=6) # days before maturity of the first contract before we switch to next contract 23 | print(f"weekly_rolls:\n{weekly_rolls}") 24 | 25 | # last friday of a month 26 | monthly_rolls = qis.generate_fixed_maturity_rolls(time_period=time_period, 27 | freq='D', roll_freq='M-FRI', 28 | roll_hour=8, min_days_to_next_roll=28) # 4 weeks before 29 | print(f"monthly rolls:\n{monthly_rolls}") 30 | 31 | # third friday of a month 32 | friday3_monthly_rolls = qis.generate_fixed_maturity_rolls(time_period=time_period, 33 | freq='D', roll_freq='WOM-3FRI', 34 | roll_hour=8, min_days_to_next_roll=28) # 4 weeks before expiry 35 | print(f"monthly 3rd Friday rolls:\n{friday3_monthly_rolls}") 36 | 37 | # third wednesday of a month 38 | wednesday3_monthly_rolls = qis.generate_fixed_maturity_rolls(time_period=time_period, 39 | freq='D', roll_freq='WOM-3WED', 40 | roll_hour=8, min_days_to_next_roll=1) # 4 weeks before expiry 41 | print(f"monthly 3rd Wednesday rolls:\n{wednesday3_monthly_rolls}") 42 | 43 | 44 | # last Friday of quarter 45 | quarterly_rolls = qis.generate_fixed_maturity_rolls(time_period=time_period, 46 | freq='D', roll_freq='Q-FRI', 47 | roll_hour=8, min_days_to_next_roll=56) # 8 weeks before expiry 48 | print(f"quarterly rolls:\n{quarterly_rolls}") 49 | 50 | # third Friday of quarter 51 | quarterly_rolls = qis.generate_fixed_maturity_rolls(time_period=time_period, 52 | freq='D', roll_freq='Q-3FRI', 53 | roll_hour=8, min_days_to_next_roll=56) # 8 weeks before expiry 54 | print(f"quarterly 3rd Friday rolls:\n{quarterly_rolls}") 55 | -------------------------------------------------------------------------------- /qis/examples/interpolation_infrequent_returns.py: -------------------------------------------------------------------------------- 1 | """ 2 | illustrate interpolation of infrequent returns 3 | """ 4 | # packages 5 | import pandas as pd 6 | import numpy as np 7 | import matplotlib.pyplot as plt 8 | from enum import Enum 9 | import qis as qis 10 | 11 | 12 | class UnitTests(Enum): 13 | YF = 1 14 | BBG = 2 15 | 16 | 17 | def run_unit_test(unit_test: UnitTests): 18 | 19 | pd.set_option('display.max_rows', 500) 20 | pd.set_option('display.max_columns', 500) 21 | pd.set_option('display.width', 1000) 22 | 23 | if unit_test == UnitTests.YF: 24 | import yfinance as yf 25 | pivot = 'SPY' 26 | asset = 'QQQ' 27 | tickers = [pivot, asset] 28 | prices = yf.download(tickers=tickers, start=None, end=None)['Close'] 29 | 30 | elif unit_test == UnitTests.BBG: 31 | from bbg_fetch import fetch_field_timeseries_per_tickers 32 | pivot = 'SPTR Index' 33 | asset = 'XNDX Index' 34 | tickers = [pivot, asset] 35 | prices = fetch_field_timeseries_per_tickers(tickers=tickers, freq='B', field='PX_LAST').ffill() 36 | 37 | else: 38 | raise NotImplementedError 39 | 40 | is_log_returns = True 41 | infrequent_returns = qis.to_returns(prices[asset], is_log_returns=is_log_returns, freq='QE') 42 | pivot_returns = qis.to_returns(prices[pivot], is_log_returns=is_log_returns, freq='ME') 43 | i_backfill = qis.interpolate_infrequent_returns(infrequent_returns=infrequent_returns.dropna(), pivot_returns=pivot_returns, 44 | is_to_log_returns=False) 45 | 46 | known_returns = qis.to_returns(prices[asset], is_log_returns=is_log_returns, freq='ME') 47 | returns = pd.concat([pivot_returns, known_returns.rename(f"{asset} actual"), i_backfill.rename(f"{asset} interpolated")], axis=1).dropna() 48 | if is_log_returns: 49 | returns = np.expm1(returns) 50 | print(f"means={np.nanmean(returns, axis=0)}, stdevs={np.nanstd(returns, axis=0)}") 51 | 52 | navs = qis.returns_to_nav(returns) 53 | qis.plot_time_series(navs) 54 | returns = pd.concat([returns.rolling(3).sum(), infrequent_returns.rename('Q')], axis=1) 55 | print(returns) 56 | 57 | fig = plt.figure(figsize=(12, 10), constrained_layout=True) 58 | gs = fig.add_gridspec(nrows=3, ncols=2, wspace=0.0, hspace=0.0) 59 | qis.plot_ra_perf_table(prices=navs, perf_params=qis.PerfParams(freq='ME'), title='Monthly Sampling', ax=fig.add_subplot(gs[0, :])) 60 | qis.plot_ra_perf_table(prices=navs, perf_params=qis.PerfParams(freq='QE'), title='Quarterly Sampling', ax=fig.add_subplot(gs[1, :])) 61 | 62 | qis.plot_returns_corr_table(prices=navs, freq='ME', title='Monthly Sampling', ax=fig.add_subplot(gs[2, 0])) 63 | qis.plot_returns_corr_table(prices=navs, freq='QE', title='Quarterly Sampling', ax=fig.add_subplot(gs[2, 1])) 64 | 65 | plt.show() 66 | 67 | 68 | if __name__ == '__main__': 69 | 70 | unit_test = UnitTests.YF 71 | 72 | is_run_all_tests = False 73 | if is_run_all_tests: 74 | for unit_test in UnitTests: 75 | run_unit_test(unit_test=unit_test) 76 | else: 77 | run_unit_test(unit_test=unit_test) 78 | -------------------------------------------------------------------------------- /qis/examples/leveraged_strategies.py: -------------------------------------------------------------------------------- 1 | """ 2 | backtest example of leveraged strategies 3 | """ 4 | 5 | # packages 6 | import pandas as pd 7 | import matplotlib.pyplot as plt 8 | import yfinance as yf 9 | import qis as qis 10 | 11 | # select tickers 12 | benchmark = 'SPY' 13 | tickers = [benchmark, 'SSO', 'IEF'] 14 | 15 | # fetch prices 16 | prices = yf.download(tickers=tickers, start=None, end=None, ignore_tz=True)['Close'][tickers] 17 | prices = prices.asfreq('B', method='ffill').dropna() # make B frequency 18 | 19 | rebalancing_freq = 'B' # each business day 20 | rebalancing_costs = 0.0010 # 10bp for rebalancing 21 | 22 | # 50/50 SSO/IEF 23 | unleveraged_portfolio = qis.backtest_model_portfolio(prices=prices[['SSO', 'IEF']], 24 | weights={'SSO': 0.5, 'IEF': 0.5}, 25 | rebalancing_freq=rebalancing_freq, 26 | rebalancing_costs=rebalancing_costs, 27 | ticker='50/50 SSO/IEF').get_portfolio_nav() 28 | 29 | # leveraged is funded at 100bp + 3m UST 30 | funding_rate = 0.01 + yf.download('^IRX', start=None, end=None)['Close'].dropna() / 100.0 31 | leveraged_portfolio = qis.backtest_model_portfolio(prices=prices[['SPY', 'IEF']], 32 | weights={'SPY': 1.0, 'IEF': 0.5}, 33 | rebalancing_freq=rebalancing_freq, 34 | rebalancing_costs=rebalancing_costs, 35 | funding_rate=funding_rate, 36 | ticker='100/50 SPY/IEF').get_portfolio_nav() 37 | 38 | 39 | prices = pd.concat([prices, unleveraged_portfolio, leveraged_portfolio], axis=1) 40 | 41 | # generate report since SSO launch 42 | time_period = qis.TimePeriod('21Jun2006', '01Sep2023') 43 | fig = qis.generate_multi_asset_factsheet(prices=prices, 44 | benchmark=benchmark, 45 | time_period=time_period, 46 | **qis.fetch_default_report_kwargs(time_period=time_period)) 47 | 48 | qis.save_fig(fig=fig, file_name=f"leveraged_fund_analysis", local_path=qis.local_path.get_output_path()) 49 | 50 | qis.save_figs_to_pdf(figs=[fig], 51 | file_name=f"leveraged_fund_analysis", orientation='landscape', 52 | local_path=qis.local_path.get_output_path()) 53 | 54 | plt.show() 55 | -------------------------------------------------------------------------------- /qis/examples/long_short.py: -------------------------------------------------------------------------------- 1 | # packages 2 | import pandas as pd 3 | import matplotlib.pyplot as plt 4 | import seaborn as sns 5 | from enum import Enum 6 | import qis as qis 7 | from qis import PerfParams, TimePeriod 8 | import yfinance as yf 9 | 10 | 11 | class UnitTests(Enum): 12 | LONG_IEF_SHORT_LQD = 1 13 | 14 | 15 | def run_unit_test(unit_test: UnitTests): 16 | 17 | perf_params = PerfParams(freq_drawdown='B', freq='B') 18 | kwargs = dict(fontsize=12, digits_to_show=1, sharpe_digits=2, 19 | alpha_format='{0:+0.0%}', 20 | beta_format='{:0.1f}', 21 | perf_stats_labels=qis.PerfStatsLabels.TOTAL_DETAILED.value, 22 | framealpha=0.9) 23 | 24 | time_period = TimePeriod('31Dec2021', '19Sep2022') 25 | 26 | if unit_test == UnitTests.LONG_IEF_SHORT_LQD: 27 | prices = yf.download(tickers=['IEF', 'LQD', 'LQDH', 'IGIB'], start=None, end=None)['Close'] 28 | prices = time_period.locate(prices) 29 | rets = qis.to_returns(prices=prices, is_first_zero=True) 30 | navs1 = qis.returns_to_nav(returns=4.0*(rets.iloc[:, 0] - rets.iloc[:, 1]).rename('4x(Long 10y ETF / Short IG ETF)')) 31 | navs2 = qis.returns_to_nav(returns=-4.0*rets.iloc[:, 2].rename('4x(Short LQDH)')) 32 | navs3 = qis.returns_to_nav(returns=4.0*(0.5*rets.iloc[:, 0] - rets.iloc[:, 3]).rename('4x(Long 10y ETF / Short IGIB)')) 33 | spy = time_period.locate(yf.download(tickers=['SPY'])['Close']) 34 | prices2 = pd.concat([navs1, navs2, navs3, spy], axis=1) 35 | 36 | with sns.axes_style('darkgrid'): 37 | fig1, axs = plt.subplots(2, 1, figsize=(15, 5), constrained_layout=True) 38 | qis.plot_prices_with_dd(prices=prices, perf_params=perf_params, axs=axs) 39 | 40 | fig2, ax = plt.subplots(1, 1, figsize=(12, 8), constrained_layout=True) 41 | qis.plot_prices(prices=prices2, perf_params=perf_params, 42 | title='2022 YTD total performance of 4x(Long 10y ETF / Short IG ETF) vs SPY ETF', 43 | ax=ax, **kwargs) 44 | 45 | plt.show() 46 | 47 | 48 | if __name__ == '__main__': 49 | 50 | unit_test = UnitTests.LONG_IEF_SHORT_LQD 51 | 52 | is_run_all_tests = False 53 | if is_run_all_tests: 54 | for unit_test in UnitTests: 55 | run_unit_test(unit_test=unit_test) 56 | else: 57 | run_unit_test(unit_test=unit_test) 58 | -------------------------------------------------------------------------------- /qis/examples/marginal_asset_contribution.py: -------------------------------------------------------------------------------- 1 | """ 2 | example of creating 60/40 equity with and without BTC 3 | """ 4 | import pandas as pd 5 | import matplotlib.pyplot as plt 6 | import seaborn as sns 7 | import qis as qis 8 | import yfinance as yf 9 | 10 | btc_weight = 0.02 11 | tickers_weights_wo = {'SPY': 0.6, 'IEF': 0.4, 'BTC-USD': 0.0} 12 | tickers_weights_with = {'SPY': 0.6-0.5*btc_weight, 'IEF': 0.4-0.5*btc_weight, 'BTC-USD': btc_weight} 13 | 14 | 15 | tickers = list(tickers_weights_wo.keys()) 16 | prices = yf.download(tickers, start=None, end=None)['Close'][tickers].asfreq('B', method='ffill').dropna() 17 | prices = prices.loc['31Dec2018':, :] 18 | 19 | balanced_60_40_wo = qis.backtest_model_portfolio(prices=prices, weights=tickers_weights_wo, rebalancing_freq='QE', 20 | ticker='100% Balanced').get_portfolio_nav() 21 | balanced_60_40b = qis.backtest_model_portfolio(prices=prices, weights=tickers_weights_with, rebalancing_freq='QE', 22 | ticker=f"{1.0-btc_weight:0.0%} Balanced / {btc_weight:0.0%} BTC").get_portfolio_nav() 23 | navs = pd.concat([balanced_60_40_wo, balanced_60_40b], axis=1) 24 | 25 | perf_params = qis.PerfParams(freq='ME') 26 | with sns.axes_style("darkgrid"): 27 | fig = plt.figure(figsize=(15, 12), constrained_layout=True) 28 | gs = fig.add_gridspec(nrows=7, ncols=1, wspace=0.0, hspace=0.0) 29 | axs = [fig.add_subplot(gs[1:4, 0]), fig.add_subplot(gs[4:, 0])] 30 | qis.plot_prices_with_dd(prices=navs, perf_params=perf_params, axs=axs) 31 | qis.plot_ra_perf_table_benchmark(prices=navs, 32 | benchmark=balanced_60_40_wo.name, 33 | perf_params=perf_params, 34 | title='Risk-adjusted Performance Table', 35 | digits_to_show=1, 36 | ax=fig.add_subplot(gs[0, 0])) 37 | 38 | plt.show() 39 | -------------------------------------------------------------------------------- /qis/examples/momentum_indices.py: -------------------------------------------------------------------------------- 1 | """ 2 | run multi asset report for momentum indices: run with open Bloomberg terminal 3 | """ 4 | import qis as qis 5 | from bbg_fetch import fetch_field_timeseries_per_tickers 6 | 7 | # define momentum indices for Bloomberg fectch 8 | momentum_indices = {'SPTR Index': 'S&P 500', 9 | 'MTUM US Equity': 'MTUM Mom Beta', 10 | 'SP500MUP Index': 'S&P500 Mom Beta', 11 | 'DJTMNMOT Index': 'DJ Market-Neutral Mom', 12 | 'PMOMENUS Index': 'BBG Market-Neutral Mom'} 13 | prices = fetch_field_timeseries_per_tickers(tickers=list(momentum_indices.keys())).rename(momentum_indices, axis=1).dropna() 14 | 15 | # time period for performance measurement 16 | time_period = qis.TimePeriod('31Dec2019', '18Jul2024') 17 | fig = qis.generate_multi_asset_factsheet(prices=prices, 18 | benchmark='S&P 500', 19 | time_period=time_period, 20 | **qis.fetch_default_report_kwargs(time_period=time_period)) 21 | # save report to pdf 22 | qis.save_figs_to_pdf(figs=[fig], file_name=f"momentum_indices_report", orientation='landscape') 23 | -------------------------------------------------------------------------------- /qis/examples/ohlc_vol_analysis.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import seaborn as sns 4 | import matplotlib.pyplot as plt 5 | from enum import Enum 6 | from typing import Literal, List 7 | import yfinance as yf 8 | import qis 9 | from qis import OhlcEstimatorType 10 | 11 | AF_MULTIPLIERS = {'1d': 1, '1h': 24, '30m': 2*24, '15m': 4*24, '5m': 12*24, '1m': 60*24} 12 | 13 | 14 | def fetch_hf_ohlc(ticker: str = 'SPY', 15 | interval: Literal['1d', '1h', '30m', '15m', '5m', '1m'] = '30m' 16 | ) -> pd.DataFrame: 17 | """ 18 | fetch hf data using yf 19 | for m and h frequencies we shift the data forward because yf 20 | reports timestamps of bars at the start of the period: we shift it to the end of the period 21 | """ 22 | asset = yf.Ticker(ticker) 23 | if interval == '1d': # close to close 24 | # ohlc_data = asset.history(period="730d", interval='1d') 25 | ohlc_data = yf.download(tickers=ticker, start=None, end=None, ignore_tz=True) 26 | ohlc_data.index = ohlc_data.index.tz_localize('UTC') 27 | elif interval == '1h': 28 | ohlc_data = asset.history(period="730d", interval="1h") 29 | ohlc_data.index = [t + pd.Timedelta(minutes=60) for t in ohlc_data.index] 30 | elif interval == '30m': 31 | ohlc_data = asset.history(period="60d", interval="30m") 32 | ohlc_data.index = [t + pd.Timedelta(minutes=30) for t in ohlc_data.index] 33 | elif interval == '15m': 34 | ohlc_data = asset.history(period="60d", interval="15m") 35 | ohlc_data.index = [t + pd.Timedelta(minutes=15) for t in ohlc_data.index] 36 | elif interval == '5m': 37 | ohlc_data = asset.history(period="60d", interval="5m") 38 | ohlc_data.index = [t + pd.Timedelta(minutes=5) for t in ohlc_data.index] 39 | elif interval == '1m': 40 | ohlc_data = asset.history(period="7d", interval="1m") 41 | ohlc_data.index = [t + pd.Timedelta(minutes=1) for t in ohlc_data.index] 42 | else: 43 | raise NotImplementedError(f"interval={interval}") 44 | ohlc_data = ohlc_data.rename({'Open': 'open', 'High': 'high', 'Low': 'low', 'Close': 'close'}, axis=1) 45 | ohlc_data.index = ohlc_data.index.tz_convert('UTC') 46 | ohlc_data.index.name = 'timestamp' 47 | return ohlc_data 48 | 49 | 50 | def estimate_hf_vol(ticker: str = 'SPY', 51 | agg_freq: str = 'B', 52 | annualization_factor: float = 260, 53 | freqs: List[str] = ['1d', '1h', '30m', '15m', '5m'], 54 | ohlc_estimator_type: OhlcEstimatorType = OhlcEstimatorType.PARKINSON 55 | ) -> pd.DataFrame: 56 | # need to scale up the vol 57 | vols = {} 58 | for freq in freqs: 59 | ohlc_data = fetch_hf_ohlc(ticker=ticker, interval=freq) 60 | vols[freq] = qis.estimate_hf_ohlc_vol(ohlc_data=ohlc_data, 61 | ohlc_estimator_type=ohlc_estimator_type, 62 | agg_freq=agg_freq, 63 | annualization_factor=annualization_factor*AF_MULTIPLIERS[freq]) 64 | vols = pd.DataFrame.from_dict(vols, orient='columns').dropna() 65 | return vols 66 | 67 | 68 | def plot_hf_vols(ticker: str = 'SPY', 69 | agg_freq: str = 'B', 70 | annualization_factor: float = 260, 71 | freqs: List[str] = ['1d', '1h', '30m', '15m', '5m'], 72 | ohlc_estimator_type: OhlcEstimatorType = OhlcEstimatorType.PARKINSON 73 | ): 74 | vols = estimate_hf_vol(ticker=ticker, 75 | agg_freq=agg_freq, 76 | annualization_factor=annualization_factor, 77 | freqs=freqs, 78 | ohlc_estimator_type=ohlc_estimator_type) 79 | 80 | with sns.axes_style("darkgrid"): 81 | fig, ax = plt.subplots(1, 1, figsize=(10, 7)) 82 | qis.plot_time_series(df=vols, 83 | x_date_freq='W-MON', 84 | var_format='{:,.2%}', 85 | ax=ax) 86 | 87 | 88 | class UnitTests(Enum): 89 | HF_PRICES = 1 90 | HF_VOL = 2 91 | PLOT_HF_VOL = 3 92 | 93 | 94 | def run_unit_test(unit_test: UnitTests): 95 | 96 | if unit_test == UnitTests.HF_PRICES: 97 | intervals = ['1h', '30m', '15m', '1m'] 98 | # 'BTC-USD' 99 | df = fetch_hf_ohlc(ticker='ETH-USD', interval='5m') 100 | print(df) 101 | 102 | elif unit_test == UnitTests.HF_VOL: 103 | # use small number of num_samples for illustration 104 | df = estimate_hf_vol(ticker='SPY', agg_freq='B', annualization_factor=260) 105 | print(df) 106 | df.plot() 107 | 108 | elif unit_test == UnitTests.PLOT_HF_VOL: 109 | # plot_hf_vols(ticker='SPY', agg_freq='B', annualization_factor=260) 110 | plot_hf_vols(ticker='ETH-USD', agg_freq='D', annualization_factor=365, 111 | ohlc_estimator_type=OhlcEstimatorType.CLOSE_TO_CLOSE) 112 | 113 | plt.show() 114 | 115 | 116 | if __name__ == '__main__': 117 | 118 | unit_test = UnitTests.HF_PRICES 119 | 120 | is_run_all_tests = False 121 | if is_run_all_tests: 122 | for unit_test in UnitTests: 123 | run_unit_test(unit_test=unit_test) 124 | else: 125 | run_unit_test(unit_test=unit_test) 126 | -------------------------------------------------------------------------------- /qis/examples/overnight_returns.py: -------------------------------------------------------------------------------- 1 | """ 2 | compute split overnight and intraday returns 3 | """ 4 | import pandas as pd 5 | import seaborn as sns 6 | import matplotlib.pyplot as plt 7 | from typing import Tuple 8 | import yfinance as yf 9 | import qis 10 | 11 | 12 | def compute_returns(ticker: str = 'SPY', time_period: qis.TimePeriod = None) -> Tuple[pd.Series, pd.Series, pd.Series]: 13 | ohlc_data = yf.download(tickers=ticker, start=None, end=None, ignore_tz=True) 14 | if time_period is not None: 15 | ohlc_data = time_period.locate(ohlc_data) 16 | # need to adjust open price for dividends 17 | adjustment_ratio = ohlc_data['Close'] / ohlc_data['Close'] 18 | adjusted_open = adjustment_ratio.multiply(ohlc_data['Open']) 19 | adjusted_close = ohlc_data['Close'] 20 | overnight_return = adjusted_open.divide(adjusted_close.shift(1)) - 1.0 21 | intraday_return = adjusted_close.divide(adjusted_open) - 1.0 22 | close_to_close_return = adjusted_close.divide(adjusted_close.shift(1)) - 1.0 23 | return overnight_return.rename('Overnight'), intraday_return.rename('Intraday'), close_to_close_return.rename('Close-to-Close') 24 | 25 | 26 | def plot_split_returns(ticker: str = 'SPY', 27 | time_period: qis.TimePeriod = None, 28 | is_check_total: bool = False, 29 | ax: plt.Subplot = None 30 | ) -> None: 31 | overnight_return, intraday_return, close_to_close_return = compute_returns(ticker=ticker, time_period=time_period) 32 | 33 | if is_check_total: 34 | cum_performance = pd.concat([close_to_close_return.rename('Close-to-Close'), 35 | overnight_return.add(intraday_return).rename('Overnight+Intraday') 36 | ], axis=1).cumsum(0) 37 | else: 38 | cum_performance = pd.concat([overnight_return, intraday_return], axis=1).cumsum(0) 39 | 40 | if ax is None: 41 | fig, ax = plt.subplots(1, 1, figsize=(15, 8), tight_layout=True) 42 | qis.plot_time_series(df=cum_performance, 43 | var_format='{:,.0%}', 44 | title=f"{ticker} Performance", 45 | x_date_freq='YE', 46 | date_format='%Y', 47 | framealpha=0.9, 48 | legend_stats=qis.LegendStats.LAST, 49 | ax=ax) 50 | 51 | 52 | # tickers = ['SPY', 'QQQ', 'GLD', 'TLT', 'HYG', 'MSFT'] 53 | tickers = ['AAPL', 'MSFT', 'NVDA', 'AMZN', 'META', 'GOOG'] 54 | time_period = qis.TimePeriod('31Dec2004', None) 55 | 56 | with sns.axes_style("darkgrid"): 57 | fig, axs = plt.subplots(2, len(tickers)//2, figsize=(15, 8), tight_layout=True) 58 | 59 | for ticker, ax in zip(tickers, qis.to_flat_list(axs)): 60 | plot_split_returns(ticker=ticker, time_period=time_period, ax=ax) 61 | 62 | plt.show() 63 | -------------------------------------------------------------------------------- /qis/examples/perf_external_assets.py: -------------------------------------------------------------------------------- 1 | """ 2 | analyse performance of CBOE vol strats 3 | use downloaded data 4 | """ 5 | 6 | # imports 7 | import matplotlib.pyplot as plt 8 | import pandas as pd 9 | import seaborn as sns 10 | import yfinance as yf 11 | import qis 12 | 13 | 14 | # download from https://cdn.cboe.com/api/global/us_indices/daily_prices/SVRPO_History.csv 15 | svrpo = qis.load_df_from_csv(file_name='SVRPO_History', local_path=qis.get_resource_path()) 16 | # spy etf as benchmark 17 | benchmark = 'SPY' 18 | spy = yf.download([benchmark], start=None, end=None)['Close'].rename(benchmark) 19 | # merge 20 | prices = pd.concat([spy, svrpo], axis=1).dropna() 21 | # take last 3 years 22 | prices = prices.loc['2020':, :] 23 | 24 | # set parameters for computing performance stats including returns vols and regressions 25 | ust_3m_rate = yf.download('^IRX', start=None, end=None)['Close'].dropna() / 100.0 26 | perf_params = qis.PerfParams(freq='ME', freq_reg='W-WED', alpha_an_factor=52.0, rates_data=ust_3m_rate) 27 | 28 | # price perf 29 | with sns.axes_style("darkgrid"): 30 | fig1, axs = plt.subplots(2, 1, figsize=(10, 7)) 31 | qis.plot_prices_with_dd(prices=prices, 32 | regime_benchmark=benchmark, 33 | x_date_freq='QE', 34 | framealpha=0.9, 35 | perf_params=perf_params, 36 | axs=axs) 37 | fig2, ax = plt.subplots(1, 1, figsize=(10, 7)) 38 | qis.plot_returns_scatter(prices=prices, 39 | benchmark=benchmark, 40 | ylabel=svrpo.columns[0], 41 | title='Regression of weekly returns', 42 | freq='W-WED', 43 | ax=ax) 44 | 45 | 46 | plt.show() -------------------------------------------------------------------------------- /qis/examples/readme_performances.py: -------------------------------------------------------------------------------- 1 | # imports 2 | import matplotlib.pyplot as plt 3 | import seaborn as sns 4 | import yfinance as yf 5 | import qis as qis 6 | 7 | 8 | # define tickers and fetch price data 9 | tickers = ['SPY', 'QQQ', 'EEM', 'TLT', 'IEF', 'SHY', 'LQD', 'HYG', 'GLD'] 10 | prices = yf.download(tickers, start=None, end=None)['Close'][tickers].dropna() 11 | 12 | # minimum usage 13 | with sns.axes_style("darkgrid"): 14 | fig, ax = plt.subplots(1, 1, figsize=(10, 7)) 15 | qis.plot_prices(prices=prices, x_date_freq='YE', ax=ax) 16 | 17 | # skip 18 | qis.save_fig(fig, file_name='perf1', local_path="figures/") 19 | 20 | # with drawdowns using sns styles 21 | with sns.axes_style("darkgrid"): 22 | fig, axs = plt.subplots(2, 1, figsize=(10, 7)) 23 | qis.plot_prices_with_dd(prices=prices, x_date_freq='YE', axs=axs) 24 | 25 | # skip 26 | qis.save_fig(fig, file_name='perf2', local_path="figures/") 27 | 28 | # risk-adjusted performance table with specified data entries 29 | # add rates for excess Sharpe 30 | from qis import PerfStat 31 | ust_3m_rate = yf.download('^IRX', start=None, end=None)['Close'].dropna() / 100.0 32 | 33 | # set parameters for computing performance stats including returns vols and regressions 34 | perf_params = qis.PerfParams(freq='ME', freq_reg='QE', alpha_an_factor=4.0, rates_data=ust_3m_rate) 35 | # perf_columns is list to display different perfomance metrics from enumeration PerfStat 36 | fig = qis.plot_ra_perf_table(prices=prices, 37 | perf_columns=[PerfStat.TOTAL_RETURN, PerfStat.PA_RETURN, PerfStat.PA_EXCESS_RETURN, 38 | PerfStat.VOL, PerfStat.SHARPE_RF0, 39 | PerfStat.SHARPE_EXCESS, PerfStat.SORTINO_RATIO, PerfStat.CALMAR_RATIO, 40 | PerfStat.MAX_DD, PerfStat.MAX_DD_VOL, 41 | PerfStat.SKEWNESS, PerfStat.KURTOSIS], 42 | title=f"Risk-adjusted performance: {qis.get_time_period_label(prices, date_separator='-')}", 43 | perf_params=perf_params) 44 | 45 | # skip 46 | qis.save_fig(fig, file_name='perf3', local_path="figures/") 47 | 48 | # add benchmark regression using excess returns for linear beta 49 | # regression frequency is specified using perf_params.freq_reg 50 | # regression alpha is multiplied using perf_params.alpha_an_factor 51 | fig, _ = qis.plot_ra_perf_table_benchmark(prices=prices, 52 | benchmark='SPY', 53 | perf_columns=[PerfStat.TOTAL_RETURN, PerfStat.PA_RETURN, PerfStat.PA_EXCESS_RETURN, 54 | PerfStat.VOL, PerfStat.SHARPE_RF0, 55 | PerfStat.SHARPE_EXCESS, PerfStat.SORTINO_RATIO, PerfStat.CALMAR_RATIO, 56 | PerfStat.MAX_DD, PerfStat.MAX_DD_VOL, 57 | PerfStat.SKEWNESS, PerfStat.KURTOSIS, 58 | PerfStat.ALPHA_AN, PerfStat.BETA, PerfStat.R2], 59 | title=f"Risk-adjusted performance: {qis.get_time_period_label(prices, date_separator='-')} benchmarked with SPY", 60 | perf_params=perf_params) 61 | # skip 62 | qis.save_fig(fig, file_name='perf4', local_path="figures/") 63 | 64 | plt.show() 65 | -------------------------------------------------------------------------------- /qis/examples/risk_return_frontier.py: -------------------------------------------------------------------------------- 1 | """ 2 | run bond etf risk return scatter 3 | """ 4 | import matplotlib.pyplot as plt 5 | import seaborn as sns 6 | import yfinance as yf 7 | import qis as qis 8 | from qis import PerfStat 9 | 10 | # define bond etfs 11 | bond_etfs = {'SHV': '3m UST', 12 | 'SHY': '1-3y UST', 13 | 'IEI': '3-7y UST', 14 | 'IEF': '7-10y UST', 15 | 'TLT': '20y+ UST', 16 | 'TIP': 'TIPS', 17 | 'MUB': 'Munis', 18 | 'MBB': 'MBS', 19 | 'LQD': 'IG', 20 | 'HYG': 'HY', 21 | 'EMB': 'EM' 22 | } 23 | # fetch prices and rename to dict values 24 | tickers = list(bond_etfs.keys()) 25 | prices = yf.download(tickers, start=None, end=None)['Close'][tickers].rename(bond_etfs, axis=1) 26 | 27 | # set parameters for computing performance stats including returns vols and regressions 28 | ust_3m_rate = yf.download('^IRX', start=None, end=None)['Close'].dropna() / 100.0 29 | perf_params = qis.PerfParams(freq='W-WED', rates_data=ust_3m_rate) 30 | 31 | # time period for performance measurement 32 | time_period = qis.TimePeriod('31Dec2007', '04Sep2024') 33 | 34 | # plot scatter plot of x vs y performance variables 35 | xy1 = [PerfStat.VOL, PerfStat.PA_RETURN] 36 | xy2 = [PerfStat.VOL, PerfStat.SHARPE_EXCESS] 37 | xys = [xy1, xy2] 38 | # xys = [xy2] 39 | 40 | with sns.axes_style('darkgrid'): 41 | fig, axs = plt.subplots(1, len(xys), figsize=(12, 8), tight_layout=True) 42 | qis.set_suptitle(fig, f"P.a. returns vs Vol and Excess Sharpe ratio vs Vol for Fixed Income ETFs") 43 | axs = qis.to_flat_list(axs) 44 | for idx, xy in enumerate(xys): 45 | perf_table = qis.get_ra_perf_columns(prices=prices, 46 | perf_params=perf_params, 47 | perf_columns=xy, 48 | is_to_str=False) 49 | qis.plot_scatter(df=perf_table, 50 | x=xy[0].to_str(), y=xy[1].to_str(), 51 | title=f"{xy[0].to_str()} vs {xy[1].to_str()}", 52 | annotation_labels=perf_table.index.to_list(), 53 | xvar_format=xy[0].to_format(), yvar_format=xy[1].to_format(), 54 | full_sample_color='blue', 55 | full_sample_order=2, 56 | x_limits=(0.0, None), 57 | ax=axs[idx]) 58 | 59 | plt.show() -------------------------------------------------------------------------------- /qis/examples/rolling_performance.py: -------------------------------------------------------------------------------- 1 | # packages 2 | import matplotlib.pyplot as plt 3 | import pandas as pd 4 | import numpy as np 5 | import seaborn as sns 6 | import qis as qis 7 | from bbg_fetch import fetch_field_timeseries_per_tickers 8 | 9 | prices = fetch_field_timeseries_per_tickers(tickers={'SPTR Index': 'SPTR'}, freq='B', field='PX_LAST').ffill() 10 | 11 | yields = fetch_field_timeseries_per_tickers(tickers={'USGGBE10 Index': '10Y BE'}, freq='B', field='PX_LAST').ffill() / 100.0 12 | 13 | roll_periodss = {'5y': 5*252, '10y': 10*252} 14 | roll_periodss = {'10y': 10*252} 15 | perfs = {} 16 | for key, roll_periods in roll_periodss.items(): 17 | perf, _ = qis.compute_rolling_perf_stat(prices=prices, 18 | rolling_perf_stat=qis.RollingPerfStat.PA_RETURNS, 19 | roll_freq='B', 20 | roll_periods=roll_periods) 21 | perfs[key] = perf.iloc[:, 0] 22 | perfs = pd.DataFrame.from_dict(perfs, orient='columns').dropna(axis=0, how='all') 23 | 24 | with sns.axes_style("darkgrid"): 25 | fig, ax = plt.subplots(1, 1, figsize=(8, 6), tight_layout=True) 26 | yvar_major_ticks1 = np.linspace(-0.05, 0.3, 8) 27 | yvar_major_ticks2 = np.linspace(0.0, 0.03, 6) 28 | qis.plot_time_series_2ax(df1=perfs, 29 | df2=yields, 30 | legend_stats=qis.LegendStats.AVG_LAST, 31 | legend_stats2=qis.LegendStats.AVG_LAST, 32 | title='Rolling 10y p.a. returns of SPTR Index vs US 10Y BE', 33 | var_format='{:.1%}', 34 | var_format_yax2='{:.1%}', 35 | yvar_major_ticks1=yvar_major_ticks1, 36 | yvar_major_ticks2=yvar_major_ticks2, 37 | trend_line1=qis.TrendLine.ZERO_SHADOWS, 38 | framealpha=0.9, 39 | ax=ax) 40 | 41 | plt.show() -------------------------------------------------------------------------------- /qis/examples/seasonality.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import matplotlib.pyplot as plt 3 | import yfinance as yf 4 | import qis 5 | 6 | 7 | # select tickers 8 | tickers = ['SPY', 'TLT', 'GLD'] 9 | 10 | # fetch prices 11 | prices = yf.download(tickers=tickers, start=None, end=None, ignore_tz=True)['Close'][tickers] 12 | prices = prices.asfreq('B', method='ffill').dropna() # make B frequency 13 | returns = qis.to_returns(prices, freq='ME', drop_first=True) 14 | returns['month'] = returns.index.month 15 | df = returns.set_index('month', drop=True) 16 | qis.df_boxplot_by_hue_var(df=df, hue_var_name='asset', x_index_var_name='month', 17 | add_hue_to_legend_title=True, 18 | labels=tickers, 19 | add_zero_line=True) 20 | seasonal_returns = returns.groupby('month').agg(['mean', 'std', 'min', 'max']) 21 | print(seasonal_returns) 22 | 23 | plt.show() 24 | 25 | -------------------------------------------------------------------------------- /qis/examples/sharpe_vs_sortino.py: -------------------------------------------------------------------------------- 1 | # imports 2 | import matplotlib.pyplot as plt 3 | import seaborn as sns 4 | import yfinance as yf 5 | import qis as qis 6 | from qis import PerfStat 7 | 8 | # define tickers and fetch price data 9 | tickers = ['SPY', 'QQQ', 'EEM', 'TLT', 'IEF', 'SHY', 'LQD', 'HYG', 'GLD'] 10 | prices = yf.download(tickers, start=None, end=None)['Close'][tickers].dropna() 11 | 12 | # define frequencies for vol computations 13 | freqs = ['B', 'W-WED', 'ME', 'QE'] 14 | 15 | # plot scatter plot of PerfStat.SHARPE_RF0 vs PerfStat.SORTINO_RATIO 16 | with sns.axes_style('darkgrid'): 17 | fig, axs = plt.subplots(len(freqs)//2, len(freqs)//2, figsize=(10, 5), tight_layout=True) 18 | qis.set_suptitle(fig, f"Sortino ratio vs Sharpe ratio with rf=0 as function of returns frequency") 19 | axs = qis.to_flat_list(axs) 20 | for idx, freq in enumerate(freqs): 21 | perf_table = qis.get_ra_perf_columns(prices=prices, 22 | perf_params=qis.PerfParams(freq=freq), 23 | perf_columns=[PerfStat.SHARPE_RF0, PerfStat.SORTINO_RATIO], 24 | is_to_str=False) 25 | qis.plot_scatter(df=perf_table, 26 | x=PerfStat.SHARPE_RF0.to_str(), 27 | y=PerfStat.SORTINO_RATIO.to_str(), 28 | title=f"returns freq={freq}", 29 | annotation_labels=perf_table.index.to_list(), 30 | xvar_format='{:.2f}', yvar_format='{:.2f}', 31 | full_sample_color='blue', 32 | full_sample_order=1, 33 | fit_intercept=False, 34 | ax=axs[idx]) 35 | 36 | plt.show() 37 | -------------------------------------------------------------------------------- /qis/examples/test_scatter.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import matplotlib.pyplot as plt 4 | import seaborn as sns 5 | 6 | import qis.plots as qp 7 | 8 | np.random.seed(2) 9 | 10 | 11 | def get_random_data(is_random_beta: bool = True, 12 | n: int = 10000 13 | ) -> pd.DataFrame: 14 | 15 | x = np.random.normal(0.0, 1.0, n) 16 | eps = np.random.normal(0.0, 1.0, n) 17 | if is_random_beta: 18 | beta = np.random.normal(1.0, 1.0, n)*np.abs(x) 19 | else: 20 | beta = np.ones(n) 21 | y = beta*x + eps 22 | df = pd.concat([pd.Series(x, name='x'), pd.Series(y, name='y')], axis=1) 23 | df = df.sort_values(by='x', axis=0) 24 | return df 25 | 26 | 27 | df = get_random_data(n=100000) 28 | 29 | kwargs = dict(xvar_format='{:.1f}', yvar_format='{:.1f}', order=1) 30 | 31 | with sns.axes_style('darkgrid'): 32 | fig, axs = plt.subplots(1, 2, figsize=(10, 5), tight_layout=True) 33 | qp.plot_scatter(df=df, 34 | full_sample_order=1, 35 | fit_intercept=False, 36 | title='Linear regression', 37 | ax=axs[0], 38 | **kwargs) 39 | qp.plot_classification_scatter(df=df, 40 | full_sample_order=None, 41 | fit_intercept=False, 42 | title='Localized sextile regression', 43 | ax=axs[1], 44 | **kwargs) 45 | qp.align_y_limits_ax12(ax1=axs[0], ax2=axs[1]) 46 | qp.set_suptitle(fig, 'Estimation of noisy regression: y=beta*abs(x)*x + noise, beta=Normal(1, 1), x=Normal(0, 1), noise=Normal(0, 1)') 47 | 48 | 49 | plt.show() -------------------------------------------------------------------------------- /qis/examples/universe_corrs.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | from enum import Enum 3 | import yfinance as yf 4 | import qis as qis 5 | 6 | 7 | class UnitTests(Enum): 8 | CORR1 = 1 9 | 10 | 11 | def run_unit_test(unit_test: UnitTests): 12 | 13 | tickers = ['SPY', 'QQQ', 'TLT', 'GLD'] 14 | prices = yf.download(tickers, start=None, end=None)['Close'][tickers].dropna() 15 | 16 | if unit_test == UnitTests.CORR1: 17 | fig, ax = plt.subplots(1, 1, figsize=(7, 7), tight_layout=True) 18 | qis.plot_returns_ewm_corr_table(prices=prices, 19 | ewm_lambda=0.97, 20 | ax=ax) 21 | plt.show() 22 | 23 | 24 | if __name__ == '__main__': 25 | 26 | unit_test = UnitTests.CORR1 27 | 28 | is_run_all_tests = False 29 | if is_run_all_tests: 30 | for unit_test in UnitTests: 31 | run_unit_test(unit_test=unit_test) 32 | else: 33 | run_unit_test(unit_test=unit_test) 34 | -------------------------------------------------------------------------------- /qis/examples/vix_beta_to_equities_bonds.py: -------------------------------------------------------------------------------- 1 | """ 2 | analyse rolling betas of vix to SPY and TLT ETFs 3 | """ 4 | import pandas as pd 5 | import seaborn as sns 6 | import matplotlib.pyplot as plt 7 | import yfinance as yf 8 | import qis 9 | from qis import PortfolioData 10 | 11 | # load VIX ETH 12 | vix = yf.download(tickers=['VXX'], start=None, end=None, ignore_tz=True)['Close'].asfreq('B', method='ffill').rename('Long VIX ETF') 13 | 14 | # load becnhmarks benchmarks 15 | benchmark_prices = yf.download(tickers=['SPY', 'TLT'], start=None, end=None, ignore_tz=True)['Close'].asfreq('B', method='ffill') 16 | 17 | # create long-only portfolio using vix nav 18 | vix_portfolio = PortfolioData(nav=vix) 19 | 20 | # set timeperiod for analysis 21 | time_period = qis.TimePeriod('31Dec2021', None) 22 | perf_params = qis.PerfParams(freq='W-WED', freq_reg='W-WED', alpha_an_factor=52.0, 23 | rates_data=yf.download('^IRX', start=None, end=None)['Close'].dropna() / 100.0) 24 | regime_params = qis.BenchmarkReturnsQuantileRegimeSpecs(freq='ME') 25 | 26 | prices = pd.concat([vix, benchmark_prices], axis=1).sort_index().dropna() 27 | prices = time_period.locate(prices) 28 | 29 | with sns.axes_style("darkgrid"): 30 | fig, axs = plt.subplots(3, 1, figsize=(10, 10)) 31 | kwargs = dict(framealpha=0.9, fontsize=12, legend_loc='lower left') 32 | 33 | # plot performances 34 | qis.plot_prices(prices=prices, 35 | perf_params=perf_params, 36 | title='Log Performance', 37 | perf_stats_labels=qis.PerfStatsLabels.DETAILED_WITH_DD.value, 38 | is_log=True, 39 | ax=axs[0], 40 | **kwargs) 41 | qis.add_bnb_regime_shadows(ax=axs[0], pivot_prices=prices['SPY'], regime_params=regime_params) 42 | 43 | # plot vix betas to benchmarks 44 | span = 63 # use 3m for half-live 45 | vix_benchmark_betas = vix_portfolio.compute_portfolio_benchmark_betas(benchmark_prices=benchmark_prices, 46 | factor_beta_span=span) 47 | qis.plot_time_series(df=vix_benchmark_betas, 48 | var_format='{:,.2f}', 49 | legend_stats=qis.LegendStats.AVG_LAST, 50 | title='VIX ETF benchmark multi-variate betas', 51 | ax=axs[1], 52 | **kwargs) 53 | qis.add_bnb_regime_shadows(ax=axs[1], pivot_prices=prices['SPY'], regime_params=regime_params) 54 | 55 | # plot performance attribution to betas 56 | factor_attribution = vix_portfolio.compute_portfolio_benchmark_attribution(benchmark_prices=benchmark_prices, 57 | factor_beta_span=span, 58 | time_period=time_period) 59 | qis.plot_time_series(df=factor_attribution.cumsum(axis=0), 60 | var_format='{:,.0%}', 61 | legend_stats=qis.LegendStats.LAST, 62 | title='VIX ETF return attribution to benchmark betas', 63 | ax=axs[2], 64 | **kwargs) 65 | pivot_prices = benchmark_prices['SPY'].reindex(index=factor_attribution.index, method='ffill') 66 | qis.add_bnb_regime_shadows(ax=axs[2], pivot_prices=prices['SPY'], regime_params=regime_params) 67 | 68 | qis.align_x_limits_axs(axs=axs, is_invisible_xs=True) 69 | 70 | plt.show() 71 | -------------------------------------------------------------------------------- /qis/examples/vix_conditional_returns.py: -------------------------------------------------------------------------------- 1 | """ 2 | compute and display conditial returns on short front month VIX future strategy SPVXSPI conditioned on 3 | different predictors: VIX and vols 4 | """ 5 | 6 | import pandas as pd 7 | import numpy as np 8 | import seaborn as sns 9 | import matplotlib.pyplot as plt 10 | import qis as qis 11 | from qis import PerfStat 12 | from bbg_fetch import fetch_field_timeseries_per_tickers 13 | 14 | # define names for convenience 15 | benchmark_name = 'SPX' 16 | strategy_name = 'ShortVix' 17 | predictor_name = 'VIX' 18 | benchmark_vol_name = f"{benchmark_name} real vol" 19 | vix_vol_spread_name = f"Spread = {predictor_name} - real vol" 20 | strategy_vol_name = f"{strategy_name} real vol" 21 | 22 | # get index from bloomberg (run with open bloomberg terminal) 23 | assets = {'SPXT Index': benchmark_name, 'SPVXSPI Index': strategy_name, 'VIX Index': predictor_name} 24 | prices = fetch_field_timeseries_per_tickers(tickers=list(assets.keys()), field='PX_LAST', CshAdjNormal=True).dropna() 25 | prices = prices.rename(assets, axis=1) 26 | 27 | # define explanatory vars on monthly frequency, vol is multiplied by 100.0 28 | monthly_return = prices[strategy_name].asfreq('ME', method='ffill').pct_change() 29 | predictor_1 = prices[predictor_name].asfreq('ME', method='ffill').shift(1) 30 | monthly_benchmark_vol_1 = 100.0*np.sqrt(252)*prices[benchmark_name].pct_change().rolling(21).std().asfreq('ME', method='ffill').shift(1).rename(benchmark_vol_name) 31 | vix_vol_spread = predictor_1.subtract(monthly_benchmark_vol_1).rename(vix_vol_spread_name) 32 | strategy_vol = 100.0*np.sqrt(252)*prices[strategy_name].pct_change().rolling(21).std().asfreq('ME', method='ffill').shift(1).rename(strategy_vol_name) 33 | monthly_df = pd.concat([monthly_return, predictor_1, monthly_benchmark_vol_1, vix_vol_spread, strategy_vol], axis=1).dropna() 34 | 35 | with sns.axes_style("darkgrid"): 36 | fig1, axs = plt.subplots(2, 3, figsize=(18, 9)) 37 | qis.plot_prices_with_dd(prices=prices[[benchmark_name, strategy_name]], 38 | regime_benchmark=benchmark_name, 39 | perf_stats_labels=[PerfStat.PA_RETURN, PerfStat.VOL, PerfStat.SHARPE_RF0, PerfStat.MAX_DD], 40 | title=f"Performances of ShortVix (SPVXSPI Index) and SPX (SPXT Index)", 41 | x_date_freq='YE', 42 | framealpha=0.9, fontsize=8, 43 | axs=axs[:, 0]) 44 | 45 | kwargs = dict(y=strategy_name, 46 | num_buckets=6, ylabel=f"{strategy_name} monthly return", 47 | yvar_format='{:.0%}', xvar_format='{:.0f}', showfliers=True, fontsize=10) 48 | qis.df_boxplot_by_classification_var(df=monthly_df, 49 | x=predictor_name, 50 | title=f"Monthly returns conditional on {predictor_name} at month start", 51 | x_hue_name=f"{predictor_name} month start", 52 | ax=axs[0, 1], **kwargs) 53 | 54 | qis.df_boxplot_by_classification_var(df=monthly_df, 55 | x=benchmark_vol_name, 56 | title=f"Monthly returns conditional on {benchmark_vol_name} at month start", 57 | x_hue_name=f"{benchmark_vol_name} month start", 58 | ax=axs[1, 1], **kwargs) 59 | 60 | qis.df_boxplot_by_classification_var(df=monthly_df, 61 | x=vix_vol_spread_name, 62 | title=f"Monthly returns conditional on {vix_vol_spread_name} at month start", 63 | x_hue_name=f"{vix_vol_spread_name} month start", 64 | ax=axs[0, 2], **kwargs) 65 | 66 | qis.df_boxplot_by_classification_var(df=monthly_df, 67 | x=strategy_vol_name, 68 | title=f"Monthly returns conditional on {strategy_vol_name} at month start", 69 | x_hue_name=f"{strategy_vol_name} month start", 70 | ax=axs[1, 2], **kwargs) 71 | plt.show() 72 | -------------------------------------------------------------------------------- /qis/examples/vix_spy_by_year.py: -------------------------------------------------------------------------------- 1 | """ 2 | plot changes in VIX or ATM volatility predicted by the underlying asset 3 | data is split by years 4 | """ 5 | # packages 6 | import pandas as pd 7 | import numpy as np 8 | import matplotlib.pyplot as plt 9 | import seaborn as sns 10 | from enum import Enum 11 | import yfinance as yf 12 | import qis as qis 13 | 14 | 15 | def plot_vol_vs_underlying(spot: pd.Series, vol: pd.Series, time_period: qis.TimePeriod = None) -> plt.Figure: 16 | """ 17 | plot scatter plot of changes in the volatility predicted by returns in the underlying 18 | """ 19 | df = pd.concat([spot.pct_change(), 0.01 * vol.diff(1)], axis=1).dropna() 20 | 21 | if time_period is not None: 22 | df = time_period.locate(df) 23 | 24 | # insert year classifier 25 | hue = 'year' 26 | df[hue] = [x.year for x in df.index] 27 | vol1 = vol.reindex(index=df.index, method='ffill') 28 | df2 = 0.01 * vol1.rename(f"{vol1.name} avg by year").to_frame() 29 | df2[hue] = [x.year for x in df2.index] 30 | df2_avg_by_year = df2.groupby(hue).mean() 31 | 32 | df2_avg_by_year = pd.concat([pd.Series(np.nanmean(0.01 * vol1), index=['Full sample']), 33 | df2_avg_by_year.iloc[:, 0]], axis=0) 34 | 35 | kwargs = dict(fontsize=12, digits_to_show=1, sharpe_digits=2, 36 | alpha_format='{0:+0.0%}', 37 | beta_format='{:+0.1f}', 38 | perf_stats_labels=qis.PerfStatsLabels.TOTAL_DETAILED.value, 39 | framealpha=0.75, 40 | is_fixed_n_colors=False) 41 | 42 | with sns.axes_style('darkgrid'): 43 | fig = plt.figure(figsize=(18, 8), constrained_layout=True) 44 | gs = fig.add_gridspec(nrows=1, ncols=4, wspace=0.0, hspace=0.0) 45 | qis.plot_scatter(df=df, 46 | x=str(spot.name), 47 | y=str(vol.name), 48 | xlabel=f'{spot.name} daily return', 49 | ylabel=f'{vol.name} daily change', 50 | title=f'Daily change in the {vol.name} predicted by daily return of {spot.name} index split by years', 51 | xvar_format='{:.0%}', 52 | yvar_format='{:.0%}', 53 | hue=hue, 54 | order=2, 55 | fit_intercept=False, 56 | add_hue_model_label=True, 57 | ci=95, 58 | ax=fig.add_subplot(gs[0, :3]), 59 | **kwargs) 60 | 61 | qis.plot_bars(df2_avg_by_year, 62 | yvar_format='{:.0f}', 63 | xvar_format='{:,.0%}', 64 | title=f'Average {vol.name} by year', 65 | xlabel=f'Average {vol.name}', 66 | legend_loc=None, 67 | x_rotation=0, 68 | is_horizontal=True, 69 | ax=fig.add_subplot(gs[0, 3]), 70 | **kwargs) 71 | return fig 72 | 73 | 74 | class UnitTests(Enum): 75 | VIX_SPY = 1 76 | USDJPY = 2 77 | 78 | 79 | def run_unit_test(unit_test: UnitTests): 80 | 81 | time_period = qis.TimePeriod('01Jan1996', None) 82 | 83 | if unit_test == UnitTests.VIX_SPY: 84 | prices = yf.download(['SPY', '^VIX'], start=None, end=None)['Close'] 85 | fig = plot_vol_vs_underlying(spot=prices['SPY'].rename('S&P500'), 86 | vol=prices['^VIX'].rename('VIX'), 87 | time_period=time_period) 88 | qis.save_fig(fig, file_name='spx_vix') 89 | 90 | elif unit_test == UnitTests.USDJPY: 91 | # need to use bloomberg data 92 | from bbg_fetch import fetch_fields_timeseries_per_ticker 93 | spot = fetch_fields_timeseries_per_ticker(ticker='USDJPY Curncy', fields=['PX_LAST']).iloc[:, 0].rename('USDJPY') 94 | vol = fetch_fields_timeseries_per_ticker(ticker='USDJPYV1M BGN Curncy', fields=['PX_LAST']).iloc[:, 0].rename('USDJPY 1M ATM') 95 | fig = plot_vol_vs_underlying(spot=spot, vol=vol, time_period=time_period) 96 | qis.save_fig(fig, file_name='usdjpy') 97 | plt.show() 98 | 99 | 100 | if __name__ == '__main__': 101 | 102 | unit_test = UnitTests.VIX_SPY 103 | 104 | is_run_all_tests = False 105 | if is_run_all_tests: 106 | for unit_test in UnitTests: 107 | run_unit_test(unit_test=unit_test) 108 | else: 109 | run_unit_test(unit_test=unit_test) 110 | -------------------------------------------------------------------------------- /qis/examples/vix_tenor_analysis.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import seaborn as sns 3 | import matplotlib.pyplot as plt 4 | import qis as qis 5 | from bbg_fetch import fetch_field_timeseries_per_tickers 6 | 7 | vix_tickers = ['VIX1D Index', 'VIX9D Index', 'VIX Index', 'VIX3M Index', 'VIX6M Index', 'VIX1Y Index'] 8 | 9 | vols = 0.01*fetch_field_timeseries_per_tickers(tickers=vix_tickers).dropna() 10 | benchmark = 'SPX Index' 11 | snp = fetch_field_timeseries_per_tickers(tickers=[benchmark], field='PX_LAST', CshAdjNormal=True).dropna() 12 | snp = snp.reindex(index=vols.index, method='ffill') 13 | df = pd.concat([snp.pct_change(), vols.diff(1)], axis=1).dropna() 14 | 15 | with sns.axes_style("darkgrid"): 16 | # qis.plot_time_series(df=vols, ax=axs[0]) 17 | # qis.plot_prices(prices=snp, ax=axs[0]) 18 | kwargs = dict(fontsize=12, framealpha=0.75) 19 | 20 | corr = qis.compute_masked_covar_corr(data=df, is_covar=False) 21 | fig1, ax = plt.subplots(1, 1, figsize=(4, 4)) 22 | qis.plot_heatmap(df=corr, 23 | var_format='{:.0%}', 24 | cmap='PiYG', 25 | title=f"Correlation between daily changes for period {qis.get_time_period_label(df, date_separator='-')}", 26 | fontsize=12, 27 | ax=ax) 28 | """ 29 | fig1, axs = plt.subplots(2, 1, figsize=(18, 9)) 30 | qis.plot_scatter(df=df, 31 | x=benchmark, 32 | xlabel=f'{benchmark} daily return', 33 | ylabel=f'Vix daily change', 34 | title=f'Daily change in VIX indices predicted by daily return of benchmark', 35 | xvar_format='{:.0%}', 36 | yvar_format='{:.0%}', 37 | order=2, 38 | fit_intercept=True, 39 | add_hue_model_label=True, 40 | ci=95, 41 | ax=axs[0], 42 | **kwargs) 43 | """ 44 | fig2, axs = plt.subplots(2, 3, figsize=(18, 9)) 45 | axs = qis.to_flat_list(axs) 46 | for idx, vol in enumerate(vols.columns): 47 | qis.plot_classification_scatter(df=df[[benchmark, vol]], 48 | full_sample_order=1, 49 | num_buckets=2, 50 | xvar_format='{:.0%}', 51 | yvar_format='{:.0%}', 52 | fit_intercept=True, 53 | title=f"{vol}", 54 | ax=axs[idx], 55 | **kwargs) 56 | 57 | plt.show() 58 | -------------------------------------------------------------------------------- /qis/examples/vol_without_weekends.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | import seaborn as sns 4 | import matplotlib.pyplot as plt 5 | from enum import Enum 6 | from typing import Union 7 | import yfinance as yf 8 | import qis 9 | 10 | 11 | def fetch_hourly_data(ticker: str = 'BTC-USD') -> pd.DataFrame: 12 | """ 13 | yf reports timestamps of bars at the start of the period: we shift it to the end of the period 14 | """ 15 | asset = yf.Ticker(ticker) 16 | ohlc_data = asset.history(period="1y", interval="1h") 17 | ohlc_data.index = [t + pd.Timedelta(minutes=60) for t in ohlc_data.index] 18 | ohlc_data = ohlc_data.rename({'Open': 'open', 'High': 'high', 'Low': 'low', 'Close': 'close'}, axis=1) 19 | ohlc_data.index = pd.to_datetime(ohlc_data.index).tz_convert('UTC') 20 | ohlc_data.index.name = 'timestamp' 21 | return ohlc_data 22 | 23 | 24 | def compute_vols(prices: pd.Series, 25 | span: int = 5*24 26 | ) -> pd.DataFrame: 27 | """ 28 | compute rolling ewma with and without weekends 29 | need to adjust annualisation factor 30 | """ 31 | returns = np.log(prices).diff(1) 32 | init_value = np.nanvar(returns, axis=0) # set initial value to average variance 33 | vol = qis.compute_ewm_vol(data=returns, span=span, annualization_factor=365*24, init_value=init_value) 34 | 35 | returns1 = returns.where(returns.index.dayofweek < 5, other=np.nan) 36 | vol1 = qis.compute_ewm_vol(data=returns1, span=span, annualization_factor=260*24, init_value=init_value) 37 | vols = pd.concat([vol.rename('including weekends'), 38 | vol1.rename('excluding weekends')], axis=1) 39 | return vols 40 | 41 | 42 | class UnitTests(Enum): 43 | VOLS = 1 44 | 45 | 46 | def run_unit_test(unit_test: UnitTests): 47 | 48 | if unit_test == UnitTests.VOLS: 49 | prices = fetch_hourly_data(ticker='BTC-USD')['close'] 50 | vol = compute_vols(prices=prices) 51 | with sns.axes_style("darkgrid"): 52 | fig, ax = plt.subplots(1, 1, figsize=(10, 7)) 53 | qis.plot_time_series(df=vol, var_format='{:,.2%}', ax=ax) 54 | 55 | plt.show() 56 | 57 | 58 | if __name__ == '__main__': 59 | 60 | unit_test = UnitTests.VOLS 61 | 62 | is_run_all_tests = False 63 | if is_run_all_tests: 64 | for unit_test in UnitTests: 65 | run_unit_test(unit_test=unit_test) 66 | else: 67 | run_unit_test(unit_test=unit_test) 68 | -------------------------------------------------------------------------------- /qis/local_path.py: -------------------------------------------------------------------------------- 1 | """ 2 | local_path uses setting.yaml to return absolute path for file/folder paths 3 | example usage: 4 | import local_path as lp 5 | lp.get_resource_path() 6 | 7 | for linux use 8 | import os 9 | import sys 10 | sys.path.append(os.getcwd()) 11 | """ 12 | 13 | import yaml 14 | from pathlib import Path 15 | from typing import Dict 16 | 17 | 18 | def get_paths() -> Dict[str, str]: 19 | """ 20 | read path specs in settings.yaml 21 | """ 22 | full_file_path = Path(__file__).parent.joinpath('settings.yaml') 23 | with open(full_file_path) as settings: 24 | settings_data = yaml.load(settings, Loader=yaml.Loader) 25 | return settings_data 26 | 27 | 28 | def get_resource_path() -> str: 29 | """ 30 | read path specs in settings.yaml 31 | """ 32 | full_file_path = Path(__file__).parent.joinpath('settings.yaml') 33 | with open(full_file_path) as settings: 34 | settings_data = yaml.load(settings, Loader=yaml.Loader) 35 | return settings_data['RESOURCE_PATH'] 36 | 37 | 38 | def get_output_path() -> str: 39 | """ 40 | read path specs in settings.yaml 41 | """ 42 | full_file_path = Path(__file__).parent.joinpath('settings.yaml') 43 | with open(full_file_path) as settings: 44 | settings_data = yaml.load(settings, Loader=yaml.Loader) 45 | return settings_data['OUTPUT_PATH'] 46 | -------------------------------------------------------------------------------- /qis/models/README.md: -------------------------------------------------------------------------------- 1 | TO DO 2 | -------------------------------------------------------------------------------- /qis/models/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from qis.models.linear.auto_corr import ( 3 | compute_path_lagged_corr, 4 | compute_path_lagged_corr_given_lags, 5 | compute_path_autocorr, 6 | compute_path_autocorr_given_lags, 7 | compute_autocorr_df, 8 | compute_ewm_matrix_autocorr, 9 | compute_ewm_matrix_autocorr_df, 10 | estimate_acf_from_path, 11 | estimate_acf_from_paths, 12 | compute_ewm_vector_autocorr, 13 | compute_ewm_vector_autocorr_df, 14 | compute_autocorrelation_at_int_periods 15 | ) 16 | 17 | from qis.models.linear.corr_cov_matrix import ( 18 | CorrMatrixOutput, 19 | estimate_rolling_ewma_covar, 20 | compute_ewm_corr_df, 21 | compute_ewm_corr_single, 22 | compute_masked_covar_corr, 23 | corr_to_pivot_row, 24 | matrix_regularization, 25 | compute_path_corr 26 | ) 27 | 28 | from qis.models.linear.ewm import ( 29 | InitType, 30 | MeanAdjType, 31 | CrossXyType, 32 | NanBackfill, 33 | ewm_recursion, 34 | compute_ewm, 35 | compute_ewm_long_short, 36 | compute_ewm_long_short_filter, 37 | compute_ewm_alpha_r2_given_prediction, 38 | compute_ewm_beta_alpha_forecast, 39 | compute_ewm_cross_xy, 40 | compute_ewm_covar, 41 | compute_ewm_covar_tensor, 42 | compute_ewm_covar_tensor_vol_norm_returns, 43 | compute_ewm_sharpe, 44 | compute_ewm_sharpe_from_prices, 45 | compute_ewm_std1_norm, 46 | compute_ewm_vol, 47 | compute_ewm_newey_west_vol, 48 | compute_ewm_xy_beta_tensor, 49 | compute_one_factor_ewm_betas, 50 | compute_roll_mean, 51 | compute_rolling_mean_adj, 52 | set_init_dim1, 53 | set_init_dim2, 54 | compute_ewm_covar_newey_west 55 | ) 56 | 57 | from qis.models.linear.ewm_convolution import ConvolutionType, SignalAggType, ewm_xy_convolution 58 | 59 | from qis.models.linear.ewm_factors import (LinearModel, 60 | EwmLinearModel, 61 | compute_portfolio_benchmark_betas, 62 | compute_portfolio_benchmark_beta_alpha_attribution, 63 | compute_benchmarks_beta_attribution, 64 | estimate_linear_model) 65 | 66 | from qis.models.linear.pca import( 67 | compute_eigen_portfolio_weights, 68 | apply_pca, 69 | compute_data_pca_r2, 70 | compute_pca_r2 71 | ) 72 | 73 | from qis.models.linear.plot_correlations import( 74 | plot_returns_corr_matrix_time_series, 75 | plot_returns_corr_table, 76 | plot_returns_ewm_corr_table 77 | ) 78 | 79 | from qis.models.linear.ra_returns import( 80 | ReturnsTransform, 81 | compute_ewm_ra_returns_momentum, 82 | compute_ra_returns, 83 | compute_ewm_long_short_filtered_ra_returns, 84 | map_signal_to_weight, 85 | SignalMapType, 86 | compute_returns_transform, 87 | compute_rolling_ra_returns, 88 | compute_sum_freq_ra_returns, 89 | compute_sum_rolling_ra_returns, 90 | get_paired_rareturns_signals 91 | ) 92 | 93 | from qis.models.stats.bootstrap import ( 94 | BootsrapOutput, 95 | BootsrapType, 96 | bootstrap_ar_process, 97 | bootstrap_data, 98 | bootstrap_price_data, 99 | bootstrap_price_fundamental_data, 100 | compute_ar_residuals, 101 | generate_bootstrapped_indices 102 | ) 103 | 104 | from qis.models.stats.ohlc_vol import ( 105 | OhlcEstimatorType, 106 | estimate_hf_ohlc_vol, 107 | estimate_ohlc_var 108 | ) 109 | 110 | from qis.models.linear.ewm_winsor_outliers import ( 111 | ReplacementType, 112 | OutlierPolicy, 113 | OutlierPolicyTypes, 114 | filter_outliers, 115 | ewm_insample_winsorising, 116 | compute_ewm_score 117 | ) 118 | 119 | from qis.models.stats.rolling_stats import (RollingPerfStat, 120 | compute_rolling_perf_stat) -------------------------------------------------------------------------------- /qis/models/linear/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/QuantInvestStrats/03a55416465f7bdebeae31c3fd95e6abffde3e5a/qis/models/linear/__init__.py -------------------------------------------------------------------------------- /qis/models/linear/ewm_convolution.py: -------------------------------------------------------------------------------- 1 | """ 2 | ewm convolution 3 | """ 4 | # packages 5 | import numpy as np 6 | import pandas as pd 7 | from enum import Enum 8 | 9 | # qis 10 | import qis.utils.dates as da 11 | import qis.models.linear.ewm as ewm 12 | 13 | 14 | class ConvolutionType(Enum): 15 | AUTO_CORR = 1 16 | SIGNAL_CORR = 2 17 | SIGNAL_BETA = 3 18 | 19 | 20 | class SignalAggType(Enum): 21 | LAST_VALUE = 1 22 | MEAN = 2 23 | 24 | 25 | def ewm_xy_convolution(returns: pd.DataFrame, 26 | freq: str, 27 | signals: pd.DataFrame = None, 28 | convolution_type: ConvolutionType = ConvolutionType.AUTO_CORR, 29 | signal_agg_type: SignalAggType = SignalAggType.LAST_VALUE, 30 | is_ra_returns: bool = False, 31 | estimates_smoothing_lambda: float = None, 32 | mean_adj_type: ewm.MeanAdjType = ewm.MeanAdjType.NONE 33 | ) -> pd.DataFrame: 34 | """ 35 | ewm convolution, typical case: 36 | y is return, x is signal 37 | span defines the las the 38 | assumed frequency is daily 39 | """ 40 | 41 | signal_span, _ = da.get_period_days(freq=freq, is_calendar=True) 42 | 43 | if not np.isclose(signal_span, 1): 44 | ewm_lambda = 1.0 - 2.0 / (signal_span + 1.0) 45 | else: # take 1.5 for frequency of business day 46 | ewm_lambda = 0.5 / 2.5 47 | 48 | if is_ra_returns: 49 | ewm_vol = ewm.compute_ewm_vol(data=returns, 50 | ewm_lambda=0.94, 51 | annualize=False) 52 | returns = np.divide(returns, ewm_vol.shift(1), where=np.isclose(ewm_vol, 0.0)==False) 53 | 54 | # rolling returns by the span 55 | if not np.isclose(signal_span, 1): 56 | # norm_factor = np.sqrt(signal_span) 57 | rolling_returns = returns.rolling(signal_span).sum() 58 | else: 59 | rolling_returns = returns 60 | 61 | if signals is not None: 62 | if signal_agg_type == SignalAggType.LAST_VALUE: 63 | agg_signal = signals 64 | elif signal_agg_type == SignalAggType.MEAN: 65 | agg_signal = signals.rolling(signal_span).mean() 66 | else: 67 | raise TypeError(f"unknown {signal_agg_type}") 68 | 69 | agg_signal = agg_signal.reindex(index=rolling_returns.index, method='ffill') 70 | agg_signal = agg_signal.shift(signal_span) # shift backrard by the span 71 | else: 72 | agg_signal = None 73 | 74 | if convolution_type == ConvolutionType.AUTO_CORR: 75 | x_data = rolling_returns.shift(signal_span) # shift backward by the span 76 | y_data = rolling_returns 77 | cross_xy_type = ewm.CrossXyType.CORR 78 | 79 | elif convolution_type == ConvolutionType.SIGNAL_CORR: 80 | x_data = agg_signal 81 | y_data = rolling_returns 82 | cross_xy_type = ewm.CrossXyType.CORR 83 | 84 | elif convolution_type == ConvolutionType.SIGNAL_BETA: 85 | x_data = agg_signal 86 | y_data = rolling_returns 87 | cross_xy_type = ewm.CrossXyType.BETA 88 | 89 | else: 90 | raise ValueError(f"{convolution_type} is not implemented") 91 | 92 | # compute ewm cross 93 | corr = ewm.compute_ewm_cross_xy(x_data=x_data, 94 | y_data=y_data, 95 | ewm_lambda=ewm_lambda, 96 | cross_xy_type=cross_xy_type, 97 | mean_adj_type=mean_adj_type) 98 | 99 | if estimates_smoothing_lambda is not None: 100 | corr = ewm.compute_ewm(data=corr, ewm_lambda=estimates_smoothing_lambda) 101 | 102 | return corr 103 | -------------------------------------------------------------------------------- /qis/models/linear/pca.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from enum import Enum 4 | import qis.utils.dates as da 5 | import qis.perfstats.returns as ret 6 | import qis.models.linear.ewm as ewm 7 | 8 | 9 | def compute_eigen_portfolio_weights(covar: np.ndarray) -> np.ndarray: 10 | """ 11 | return weights for pca portolios with unit variance 12 | covar = eigen_vectors @ np.diag(eigen_values) @ eigen_vectors.T 13 | rows are principal portolio weights ranked 14 | """ 15 | vols = np.sqrt(np.diag(covar)) 16 | inv_vol = np.reciprocal(vols) 17 | norm = np.outer(inv_vol, inv_vol) 18 | corr = norm * covar 19 | eigen_values, eigen_vectors = apply_pca(cmatrix=corr, is_max_sign_positive=True) 20 | # eigen_values, eigen_vectors = np.linalg.eigh(corr) 21 | scale = np.outer(vols, np.sqrt(eigen_values).T) 22 | weights = np.reciprocal(scale) * eigen_vectors 23 | return weights.T 24 | 25 | 26 | def apply_pca(cmatrix: np.ndarray, 27 | is_max_sign_positive: bool = True, 28 | eigen_signs: np.ndarray = None 29 | ) -> (np.ndarray, np.ndarray): 30 | 31 | # from sample covar_model 32 | eig_vals, eig_vecs = np.linalg.eigh(cmatrix) 33 | 34 | # Make a list of (eigenvalue, eigenvector) tuples for sorting 35 | eig_pairs = [(eig_vals[i], eig_vecs[:, i]) for i in range(len(eig_vals))] 36 | 37 | # reverse (eigenvalue, eigenvector) tuples from high to low 38 | eig_pairs.reverse() 39 | 40 | # get back to ndarrays 41 | eigen_values = np.array([eig_pair[0] for eig_pair in eig_pairs]).T 42 | eigen_vectors = np.array([eig_pair[1] for eig_pair in eig_pairs]).T 43 | 44 | if is_max_sign_positive and eigen_signs is None: 45 | 46 | signed_eigen_vectors = eigen_vectors 47 | for idx, eigen_vector in enumerate(eigen_vectors.T): 48 | arg_max = np.argmax(np.abs(eigen_vector)) 49 | if eigen_vector[arg_max] < 0.0: 50 | eigen_vector = - eigen_vector 51 | signed_eigen_vectors[:, idx] = eigen_vector 52 | eigen_vectors = signed_eigen_vectors 53 | 54 | elif eigen_signs is not None: 55 | 56 | # eigen_vectors = eigen_signs * eigen_vectors 57 | signed_eigen_vectors = eigen_vectors.copy() 58 | for idx, eigen_vector in enumerate(eigen_vectors): 59 | if np.sign(eigen_vector[0]) != eigen_signs[idx]: 60 | signed_eigen_vectors[idx] = - eigen_vector 61 | eigen_vectors = signed_eigen_vectors 62 | 63 | return eigen_values, eigen_vectors 64 | 65 | 66 | def compute_pca_r2(cmatrix: np.ndarray, is_cumulative: bool = False) -> (np.ndarray, np.ndarray): 67 | eigen_values, _ = apply_pca(cmatrix=cmatrix) 68 | if is_cumulative: 69 | out = np.cumsum(eigen_values) / np.sum(eigen_values) 70 | else: 71 | out = eigen_values / np.sum(eigen_values) 72 | return out 73 | 74 | 75 | def compute_data_pca_r2(data: pd.DataFrame, 76 | freq: str = 'ME', 77 | time_period: da.TimePeriod = None, 78 | ewm_lambda: float = 0.94, 79 | is_corr: bool = True 80 | ) -> pd.DataFrame: 81 | 82 | corr_tensor_txy = ewm.compute_ewm_covar_tensor(a=data.to_numpy(), 83 | ewm_lambda=ewm_lambda, 84 | is_corr=is_corr) 85 | 86 | if time_period is None: 87 | time_period = da.get_time_period(df=data) 88 | sample_dates = time_period.to_pd_datetime_index(freq=freq) 89 | original_idx = pd.Series(range(len(data.index)), index=data.index) 90 | resampled_index = original_idx.reindex(index=sample_dates, method='ffill') 91 | 92 | pca_r2s = {} 93 | for date, date_idx in zip(resampled_index.index, resampled_index.to_numpy()): 94 | pca_r2s[date] = compute_pca_r2(cmatrix=corr_tensor_txy[date_idx]) 95 | 96 | pca_r2s = pd.DataFrame.from_dict(pca_r2s, 97 | orient='index', 98 | columns=[f"PC{n+1}" for n in range(len(data.columns))]) 99 | return pca_r2s 100 | 101 | 102 | class UnitTests(Enum): 103 | PCA_R2 = 1 104 | 105 | 106 | def run_unit_test(unit_test: UnitTests): 107 | 108 | from qis.test_data import load_etf_data 109 | prices = load_etf_data().dropna() 110 | print(prices) 111 | returns = ret.to_returns(prices=prices) 112 | 113 | if unit_test == UnitTests.PCA_R2: 114 | pca_r2 = compute_data_pca_r2(data=returns, 115 | freq='YE', 116 | ewm_lambda=0.97) 117 | print(pca_r2) 118 | 119 | 120 | if __name__ == '__main__': 121 | 122 | unit_test = UnitTests.PCA_R2 123 | 124 | is_run_all_tests = False 125 | if is_run_all_tests: 126 | for unit_test in UnitTests: 127 | run_unit_test(unit_test=unit_test) 128 | else: 129 | run_unit_test(unit_test=unit_test) 130 | -------------------------------------------------------------------------------- /qis/models/stats/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/QuantInvestStrats/03a55416465f7bdebeae31c3fd95e6abffde3e5a/qis/models/stats/__init__.py -------------------------------------------------------------------------------- /qis/models/stats/ohlc_vol.py: -------------------------------------------------------------------------------- 1 | 2 | # packages 3 | import numpy as np 4 | import pandas as pd 5 | from enum import Enum 6 | from typing import Optional 7 | import qis 8 | 9 | 10 | class OhlcEstimatorType(Enum): 11 | PARKINSON = 'Parkinson' 12 | GARMAN_KLASS = 'Garman-Klass' 13 | ROGERS_SATCHELL = 'Rogers-Satchell' 14 | CLOSE_TO_CLOSE = 'Close-to-Close' 15 | 16 | 17 | def estimate_ohlc_var(ohlc_data: pd.DataFrame, # must contain ohlc columnes 18 | ohlc_estimator_type: OhlcEstimatorType = OhlcEstimatorType.PARKINSON, 19 | min_size: int = 2 20 | ) -> pd.Series: 21 | 22 | if ohlc_data.empty or len(ohlc_data.index) < min_size: 23 | return np.nan 24 | 25 | log_ohlc = np.log(ohlc_data[['open', 'high', 'low', 'close']].to_numpy()) 26 | open, high, low, close = log_ohlc[:, 0], log_ohlc[:, 1], log_ohlc[:, 2], log_ohlc[:, 3] 27 | 28 | hc = high - close 29 | ho = high - open 30 | lc = low - close 31 | lo = low - open 32 | hl = high - low 33 | co = close - open 34 | 35 | if ohlc_estimator_type == OhlcEstimatorType.CLOSE_TO_CLOSE: 36 | sample_var = np.square(close[1:]-close[:-1]) 37 | sample_var = np.concatenate((np.array([np.nan]), sample_var), axis=0) 38 | 39 | elif ohlc_estimator_type == OhlcEstimatorType.PARKINSON: 40 | multiplier = 1.0 / (4.0 * np.log(2.0)) 41 | sample_var = multiplier * np.square(hl) 42 | 43 | elif ohlc_estimator_type == OhlcEstimatorType.GARMAN_KLASS: 44 | multiplier = 2.0 * np.log(2.0) - 1.0 45 | sample_var = 0.5 * np.square(hl) - multiplier * np.square(co) 46 | 47 | elif ohlc_estimator_type == OhlcEstimatorType.ROGERS_SATCHELL: 48 | sample_var = hc*ho + lc*lo 49 | 50 | else: 51 | raise TypeError(f"unknown ohlc_estimator_type={ohlc_estimator_type}") 52 | 53 | sample_var = pd.Series(sample_var, index=ohlc_data.index) 54 | return sample_var 55 | 56 | 57 | def estimate_hf_ohlc_vol(ohlc_data: pd.DataFrame, 58 | ohlc_estimator_type: OhlcEstimatorType = OhlcEstimatorType.PARKINSON, 59 | annualization_factor: float = None, # annualisation factor highly recomended 60 | is_exclude_weekends: bool = False, # for crypto 61 | agg_freq: Optional[str] = 'B' 62 | ) -> pd.Series: 63 | """ 64 | 65 | group hf data into daily or higher frequency bins 66 | for each sample compute vol at data freq and annualize at an 67 | """ 68 | sample_var = estimate_ohlc_var(ohlc_data=ohlc_data, ohlc_estimator_type=ohlc_estimator_type) 69 | if agg_freq is not None: 70 | sample_var = sample_var.resample(agg_freq).mean() 71 | 72 | if annualization_factor is None: 73 | annualization_factor = qis.infer_an_from_data(data=sample_var) 74 | 75 | vols = np.sqrt(annualization_factor*sample_var) 76 | if is_exclude_weekends: 77 | vols = vols[vols.index.dayofweek < 5] 78 | return vols 79 | -------------------------------------------------------------------------------- /qis/perfstats/README.md: -------------------------------------------------------------------------------- 1 | TO DO 2 | -------------------------------------------------------------------------------- /qis/perfstats/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from qis.perfstats.config import ( 4 | FULL_TABLE_COLUMNS, 5 | PerfParams, 6 | PerfStat, 7 | RA_TABLE_COLUMNS, 8 | RA_TABLE_COMPACT_COLUMNS, 9 | RegimeData, 10 | RegimeType, 11 | ReturnTypes, 12 | SD_PERF_COLUMNS, 13 | TRE_TABLE_COLUMNS 14 | ) 15 | 16 | from qis.perfstats.cond_regression import estimate_cond_regression, get_regime_regression_params 17 | 18 | from qis.perfstats.desc_table import DescTableType, compute_desc_table 19 | 20 | from qis.perfstats.perf_stats import ( 21 | STANDARD_TABLE_COLUMNS, 22 | LN_TABLE_COLUMNS, 23 | LN_BENCHMARK_TABLE_COLUMNS, 24 | LN_BENCHMARK_TABLE_COLUMNS_SHORT, 25 | EXTENDED_TABLE_COLUMNS, 26 | COMPACT_TABLE_COLUMNS, 27 | BENCHMARK_TABLE_COLUMNS, 28 | BENCHMARK_TABLE_COLUMNS2, 29 | compute_avg_max_dd, 30 | compute_desc_freq_table, 31 | compute_rolling_drawdowns, 32 | compute_rolling_drawdown_time_under_water, 33 | compute_info_ratio_table, 34 | compute_max_current_drawdown, 35 | compute_performance_table, 36 | compute_ra_perf_table, 37 | compute_ra_perf_table_with_benchmark, 38 | compute_risk_table, 39 | compute_te_ir_errors, 40 | compute_drawdowns_stats_table 41 | ) 42 | 43 | from qis.perfstats.regime_classifier import ( 44 | BenchmarkReturnsQuantileRegimeSpecs, 45 | BenchmarkReturnsQuantilesRegime, 46 | BenchmarkVolsQuantilesRegime, 47 | RegimeClassifier, 48 | VolQuantileRegimeSpecs, 49 | compute_bnb_regimes_pa_perf_table, 50 | compute_mean_freq_regimes, 51 | compute_regime_avg, 52 | compute_regimes_pa_perf_table_from_sampled_returns 53 | ) 54 | 55 | from qis.perfstats.returns import ( 56 | adjust_navs_to_portfolio_pa, 57 | compute_excess_returns, 58 | compute_grouped_nav, 59 | compute_net_return, 60 | compute_num_years, 61 | compute_pa_excess_returns, 62 | compute_pa_return, 63 | compute_returns_dict, 64 | compute_sampled_vols, 65 | compute_total_return, 66 | estimate_vol, 67 | get_excess_returns_nav, 68 | get_net_navs, 69 | log_returns_to_nav, 70 | portfolio_navs_to_additive, 71 | portfolio_returns_to_nav, 72 | prices_at_freq, 73 | returns_to_nav, 74 | to_portfolio_returns, 75 | long_short_to_relative_nav, 76 | to_returns, 77 | prices_to_scaled_nav, 78 | to_total_returns, 79 | to_zero_first_nonnan_returns, 80 | df_price_ffill_between_nans 81 | ) 82 | 83 | from qis.perfstats.timeseries_bfill import ( 84 | interpolate_infrequent_returns, 85 | append_time_series, 86 | bfill_timeseries, 87 | df_fill_first_nan_by_cross_median, 88 | df_price_fill_first_nan_by_cross_median, 89 | replace_nan_by_median, 90 | df_ffill_negatives 91 | ) 92 | 93 | from qis.perfstats.fx_ops import (get_aligned_fx_spots, 94 | compute_futures_fx_adjusted_returns) 95 | -------------------------------------------------------------------------------- /qis/perfstats/fx_ops.py: -------------------------------------------------------------------------------- 1 | """ 2 | analytics for fx data 3 | """ 4 | import pandas as pd 5 | import numpy as np 6 | from typing import Union, Dict 7 | 8 | 9 | def get_aligned_fx_spots(prices: pd.DataFrame, 10 | asset_ccy_map: Union[pd.Series, Dict], 11 | fx_prices: pd.DataFrame, 12 | quote_currency: str = 'USD' 13 | ) -> pd.DataFrame: 14 | """ 15 | get fx currency for price_data columns = instrument ticker 16 | universe_local_ccy is map {instrument ticker: fx_rate_ccy} 17 | fx_prices is fx prices columns = fx_rate_ccy 18 | """ 19 | # first backfill and the bbfill so prices will have corresponding fx spots data 20 | fx_prices = fx_prices.reindex(index=prices.index, method='ffill').ffill().bfill() 21 | fx_prices[quote_currency] = 1.0 22 | 23 | fx_spots = {} 24 | for asset, ccy in asset_ccy_map.items(): 25 | fx_spots[asset] = fx_prices[ccy] 26 | fx_spots = pd.DataFrame.from_dict(fx_spots, orient='columns') 27 | fx_spots = fx_spots.where(pd.isna(prices) == False) 28 | return fx_spots 29 | 30 | 31 | def compute_futures_fx_adjusted_returns(prices: pd.DataFrame, 32 | fx_spots: pd.DataFrame, 33 | periods: int = 1, 34 | is_log_returns: bool = False 35 | ) -> pd.DataFrame: 36 | """ 37 | futures returns adjusted for fx rate change 38 | fx_return affects only the future return 39 | """ 40 | price_return = prices / prices.shift(periods=periods) - 1.0 41 | fx_return = fx_spots / fx_spots.shift(periods=periods) - 1.0 42 | returns = np.log(1.0 + price_return + price_return * fx_return) 43 | if not is_log_returns: 44 | returns = np.expm1(returns) 45 | return returns 46 | 47 | 48 | def compute_cash_fx_adjusted_returns(prices: pd.DataFrame, 49 | fx_spots: pd.DataFrame, 50 | periods: int = 1, 51 | is_log_returns: bool = False 52 | ) -> pd.DataFrame: 53 | """ 54 | cash returns adjusted for fx rate change 55 | fx_return affects notional + return 56 | """ 57 | price_return = prices / prices.shift(periods=periods) - 1.0 58 | fx_return = fx_spots / fx_spots.shift(periods=periods) - 1.0 59 | returns = np.log(1.0 + fx_return + price_return + price_return * fx_return) 60 | if not is_log_returns: 61 | returns = np.expm1(returns) 62 | return returns 63 | -------------------------------------------------------------------------------- /qis/plots/README.md: -------------------------------------------------------------------------------- 1 | TO DO 2 | -------------------------------------------------------------------------------- /qis/plots/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from qis.plots.utils import ( 3 | TrendLine, 4 | LastLabel, 5 | LegendStats, 6 | add_scatter_points, 7 | align_x_limits_ax12, 8 | align_x_limits_axs, 9 | align_xy_limits, 10 | align_y_limits_ax12, 11 | align_y_limits_axs, 12 | autolabel, 13 | compute_heatmap_colors, 14 | create_dummy_line, 15 | get_cmap_colors, 16 | get_data_group_colors, 17 | get_legend_lines, 18 | get_n_cmap_colors, 19 | get_n_colors, 20 | get_n_fixed_colors, 21 | get_n_hatch, 22 | get_n_markers, 23 | get_n_mlt_colors, 24 | get_n_sns_colors, 25 | map_dates_index_to_str, 26 | rand_cmap, 27 | remove_spines, 28 | set_ax_tick_labels, 29 | set_ax_tick_params, 30 | set_ax_ticks_format, 31 | set_ax_xy_labels, 32 | set_date_on_axis, 33 | set_legend, 34 | set_legend_colors, 35 | set_legend_with_stats_table, 36 | set_linestyles, 37 | set_spines, 38 | set_suptitle, 39 | set_title, 40 | set_x_limits, 41 | set_y_limits, 42 | subplot_border, 43 | validate_returns_plot, 44 | calc_table_height, 45 | calc_table_width, 46 | calc_df_table_size, 47 | get_df_table_size, 48 | reset_xticks, 49 | set_labels_frequency, 50 | scale_ax_bar_width 51 | ) 52 | 53 | from qis.plots.bars import plot_bars, plot_vbars 54 | 55 | from qis.plots.boxplot import ( 56 | plot_box, 57 | df_boxplot_by_classification_var, 58 | df_boxplot_by_hue_var, 59 | df_boxplot_by_index, 60 | df_boxplot_by_columns, 61 | df_dict_boxplot_by_columns, 62 | df_dict_boxplot_by_classification_var 63 | ) 64 | 65 | from qis.plots.contour import plot_contour 66 | 67 | from qis.plots.errorbar import plot_errorbar 68 | 69 | from qis.plots.heatmap import plot_heatmap 70 | 71 | from qis.plots.histogram import plot_histogram, PdfType 72 | 73 | from qis.plots.histplot2d import plot_histplot2d 74 | 75 | from qis.plots.lineplot import plot_line, plot_lines_list 76 | 77 | from qis.plots.pie import plot_pie 78 | 79 | from qis.plots.qqplot import plot_qq, plot_xy_qq 80 | 81 | from qis.plots.scatter import (plot_scatter, 82 | plot_classification_scatter, 83 | plot_multivariate_scatter_with_prediction) 84 | 85 | from qis.plots.stackplot import plot_stack 86 | 87 | from qis.plots.table import ( 88 | plot_df_table, 89 | plot_df_table_with_ci, 90 | set_align_for_column, 91 | set_cells_facecolor, 92 | set_column_edge_color, 93 | set_data_colors, 94 | set_diag_cells_facecolor, 95 | set_row_edge_color 96 | ) 97 | 98 | from qis.plots.time_series import ( 99 | plot_time_series, 100 | plot_time_series_2ax 101 | ) 102 | 103 | 104 | from qis.plots.derived.prices import ( 105 | add_bnb_regime_shadows, 106 | get_performance_labels_for_stats, 107 | get_performance_labels_for_stats, 108 | PerfStatsLabels, 109 | plot_prices, 110 | plot_prices_2ax, 111 | plot_prices_with_dd, 112 | plot_prices_with_fundamentals, 113 | plot_rolling_perf_stat 114 | ) 115 | 116 | from qis.plots.derived.data_timeseries import plot_data_timeseries 117 | 118 | 119 | from qis.plots.derived.perf_table import ( 120 | plot_desc_freq_table, 121 | plot_ra_perf_annual_matrix, 122 | plot_ra_perf_bars, 123 | plot_ra_perf_by_dates, 124 | plot_ra_perf_scatter, 125 | get_ra_perf_columns, 126 | plot_ra_perf_table, 127 | plot_ra_perf_table_benchmark, 128 | get_ra_perf_benchmark_columns, 129 | plot_top_bottom_performers, 130 | plot_best_worst_returns 131 | ) 132 | 133 | from qis.plots.derived.regime_class_table import get_quantile_class_table, plot_quantile_class_table 134 | 135 | from qis.plots.derived.regime_pdf import plot_regime_pdf 136 | 137 | from qis.plots.derived.regime_scatter import plot_scatter_regression 138 | 139 | from qis.plots.derived.returns_heatmap import ( 140 | compute_periodic_returns_by_row_table, 141 | compute_periodic_returns_table, 142 | compute_periodic_returns, 143 | plot_periodic_returns_table, 144 | plot_returns_heatmap, 145 | plot_returns_table, 146 | plot_sorted_periodic_returns 147 | ) 148 | 149 | from qis.plots.derived.returns_scatter import plot_returns_scatter 150 | 151 | 152 | from qis.plots.derived.drawdowns import ( 153 | DdLegendType, 154 | plot_rolling_drawdowns, 155 | plot_rolling_time_under_water, 156 | plot_top_drawdowns_paths 157 | ) 158 | 159 | from qis.plots.derived.regime_data import ( 160 | plot_regime_data, 161 | plot_regime_boxplot, 162 | add_bnb_regime_shadows 163 | ) 164 | 165 | from qis.plots.derived.desc_table import plot_desc_table 166 | 167 | from qis.plots.reports.utils import ReportType 168 | 169 | from qis.plots.reports.econ_data_single import econ_data_report 170 | 171 | from qis.plots.reports.price_history import (plot_price_history, 172 | generate_price_history_report) 173 | -------------------------------------------------------------------------------- /qis/plots/contour.py: -------------------------------------------------------------------------------- 1 | """ 2 | 2-d countrur plot 3 | """ 4 | 5 | # packages 6 | import numpy as np 7 | import matplotlib.pyplot as plt 8 | from matplotlib.ticker import FuncFormatter 9 | from typing import List, Tuple, Optional 10 | from enum import Enum 11 | 12 | # qis 13 | import qis.plots.utils as put 14 | 15 | 16 | def plot_contour(x: np.ndarray, 17 | y: np.ndarray, 18 | z: np.ndarray, 19 | xvar_format: str = '{:.0%}', 20 | yvar_format: str = '{:.0%}', 21 | zvar_format: str = '{:.1f}', 22 | fontsize: int = 10, 23 | num_ranges: int = 7, 24 | cmap: str = 'RdYlGn', 25 | xlabel: str = 'x', 26 | ylabel: str = 'y', 27 | title: str = None, 28 | fig: plt.Figure = None, 29 | **kwargs 30 | ) -> Optional[plt.Figure]: 31 | 32 | if fig is None: 33 | fig, ax = plt.subplots() 34 | else: 35 | ax = fig.axes[0] 36 | 37 | X, Y = np.meshgrid(x, y) 38 | Z = z.T # need to transpose 39 | 40 | cbar = fig.axes[0].contourf(X, Y, Z, num_ranges, cmap=cmap) 41 | 42 | fmt = lambda x, pos: zvar_format.format(x) 43 | fig.colorbar(cbar, format=FuncFormatter(fmt)) 44 | # cbar.ax.tick_params(labelsize=fontsize) 45 | 46 | put.set_ax_ticks_format(ax=ax, fontsize=fontsize, xvar_format=xvar_format, yvar_format=yvar_format) 47 | put.set_ax_xy_labels(ax=ax, xlabel=xlabel, ylabel=ylabel, fontsize=fontsize, **kwargs) 48 | 49 | if title is not None: 50 | ax.set_title(label=title, fontsize=fontsize) 51 | 52 | return fig 53 | 54 | 55 | def contour_multi(x: np.ndarray, 56 | y: np.ndarray, 57 | zs: List[np.ndarray], 58 | xvar_format: str = '{:.0%}', 59 | yvar_format: str = '{:.0%}', 60 | zvar_format: str = '{:.1f}', 61 | fontsize: int = 10, 62 | num_ranges: int = 7, 63 | cmap: str = 'RdYlGn', 64 | xlabel: str = 'x', 65 | ylabel: str = 'y', 66 | titles: List[str] = None, 67 | figsize: Tuple[float, float] = (11, 6), 68 | **kwargs 69 | ) -> plt.Figure: 70 | 71 | fig, axs = plt.subplots(figsize=figsize, nrows=1, ncols=len(zs), sharex=True, sharey=True) 72 | 73 | X, Y = np.meshgrid(x, y) 74 | for idx, ax in enumerate(axs.flat): 75 | Z = zs[idx].T 76 | cbar = fig.axes[idx].contourf(X, Y, Z, num_ranges, cmap=cmap) 77 | fmt = lambda x, pos: zvar_format.format(x) 78 | fig.colorbar(cbar, ax=ax, format=FuncFormatter(fmt)) 79 | put.set_ax_ticks_format(ax=ax, fontsize=fontsize, xvar_format=xvar_format, yvar_format=yvar_format) 80 | 81 | ylabel = ylabel if idx == 0 else None 82 | put.set_ax_xy_labels(ax=ax, xlabel=xlabel, ylabel=ylabel, fontsize=fontsize, **kwargs) 83 | 84 | if titles[idx] is not None: 85 | ax.set_title(label=titles[idx], fontsize=fontsize) 86 | 87 | return fig 88 | 89 | 90 | class UnitTests(Enum): 91 | SHARPE_VOL = 1 92 | 93 | 94 | def run_unit_test(unit_test: UnitTests): 95 | 96 | if unit_test == UnitTests.SHARPE_VOL: 97 | 98 | global_kwargs = {'fontsize': 12} 99 | 100 | n = 41 101 | vol_ps = np.linspace(0.04, 0.20, n) 102 | vol_xys = np.linspace(0.00, 0.20, n) 103 | sharpes = np.zeros((n, n)) 104 | for n1, vol_p in enumerate(vol_ps): 105 | for n2, vol_xy in enumerate(vol_xys): 106 | sharpes[n1, n2] = (2.0*vol_xy*vol_xy-0.25*vol_p*vol_p)/vol_p 107 | 108 | fig, ax = plt.subplots(1, 1, figsize=(10, 10), tight_layout=True) 109 | plot_contour(x=vol_ps, 110 | y=vol_xys, 111 | z=sharpes, 112 | fig=fig, 113 | **global_kwargs) 114 | 115 | plt.show() 116 | 117 | 118 | if __name__ == '__main__': 119 | 120 | unit_test = UnitTests.SHARPE_VOL 121 | 122 | is_run_all_tests = False 123 | if is_run_all_tests: 124 | for unit_test in UnitTests: 125 | run_unit_test(unit_test=unit_test) 126 | else: 127 | run_unit_test(unit_test=unit_test) 128 | -------------------------------------------------------------------------------- /qis/plots/derived/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/QuantInvestStrats/03a55416465f7bdebeae31c3fd95e6abffde3e5a/qis/plots/derived/__init__.py -------------------------------------------------------------------------------- /qis/plots/derived/data_timeseries.py: -------------------------------------------------------------------------------- 1 | """ 2 | useful as interface when data can be either price or level time series 3 | """ 4 | import pandas as pd 5 | from typing import Optional, Union 6 | import matplotlib.pyplot as plt 7 | 8 | # qis 9 | import qis.utils.dates as da 10 | from qis.perfstats.config import PerfParams 11 | import qis.plots.time_series as pts 12 | import qis.plots.derived.prices as ppd 13 | 14 | 15 | PERF_PARAMS = PerfParams(freq_reg='B', freq_vol='B', freq_drawdown='B') 16 | 17 | 18 | def plot_data_timeseries(data: Union[pd.DataFrame, pd.Series], 19 | is_price_data: bool = False, 20 | legend_stats: pts.LegendStats = pts.LegendStats.FIRST_AVG_LAST, 21 | var_format: Optional[str] = None, 22 | time_period: da.TimePeriod = None, 23 | title: str = '', 24 | title_add_date: bool = True, 25 | perf_params: PerfParams = PERF_PARAMS, 26 | start_to_one: bool = False, 27 | ax: plt.Subplot = None, 28 | **kwargs 29 | ) -> None: 30 | """ 31 | define plot time series 32 | """ 33 | if time_period is not None: 34 | data = time_period.locate(data) 35 | 36 | if title_add_date: 37 | title = f"{title} {da.get_time_period(df=data.dropna()).to_str()}" 38 | else: 39 | title = title 40 | 41 | if var_format is None: 42 | if is_price_data: 43 | var_format = '{:,.2f}' 44 | else: 45 | var_format = '{:.2%}' 46 | 47 | if is_price_data: 48 | ppd.plot_prices(prices=data, 49 | perf_params=perf_params, 50 | title=title, 51 | start_to_one=start_to_one, 52 | is_log=False, 53 | var_format=var_format, 54 | perf_stats_labels=ppd.PerfStatsLabels.DETAILED_WITH_DDVOL.value, 55 | ax=ax, 56 | **kwargs) 57 | else: 58 | pts.plot_time_series(df=data, 59 | title=title, 60 | legend_stats=legend_stats, 61 | var_format=var_format, 62 | ax=ax, 63 | **kwargs) 64 | -------------------------------------------------------------------------------- /qis/plots/derived/desc_table.py: -------------------------------------------------------------------------------- 1 | """ 2 | plot descriptive table 3 | """ 4 | # packages 5 | import pandas as pd 6 | import matplotlib.pyplot as plt 7 | from typing import Union, Optional 8 | from enum import Enum 9 | 10 | # qis 11 | from qis.plots.table import plot_df_table 12 | from qis.perfstats.desc_table import compute_desc_table, DescTableType 13 | 14 | 15 | def plot_desc_table(df: Union[pd.DataFrame, pd.Series], 16 | desc_table_type: DescTableType = DescTableType.SHORT, 17 | var_format: str = '{:.2f}', 18 | annualize_vol: bool = False, 19 | is_add_tstat: bool = False, 20 | norm_variable_display_type: str = '{:.1f}', # for t-stsat 21 | ax: plt.Subplot = None, 22 | **kwargs 23 | ) -> Optional[plt.Figure]: 24 | """ 25 | data corresponds to matrix of returns with index = time and columns = tickers 26 | data can contain nan in columns 27 | output is index = tickers, columns = descriptive data 28 | data converted to str 29 | """ 30 | table_data = compute_desc_table(df=df, 31 | desc_table_type=desc_table_type, 32 | var_format=var_format, 33 | annualize_vol=annualize_vol, 34 | is_add_tstat=is_add_tstat, 35 | norm_variable_display_type=norm_variable_display_type, 36 | **kwargs) 37 | 38 | fig = plot_df_table(df=table_data, 39 | ax=ax, 40 | **kwargs) 41 | return fig 42 | 43 | 44 | class UnitTests(Enum): 45 | TABLE = 1 46 | 47 | 48 | def run_unit_test(unit_test: UnitTests): 49 | 50 | from qis.test_data import load_etf_data 51 | returns = load_etf_data().dropna().asfreq('QE').pct_change() 52 | 53 | if unit_test == UnitTests.TABLE: 54 | plot_desc_table(df=returns, 55 | desc_table_type=DescTableType.EXTENSIVE, 56 | var_format='{:.2f}', 57 | annualize_vol=True, 58 | is_add_tstat=False) 59 | 60 | plt.show() 61 | 62 | 63 | if __name__ == '__main__': 64 | 65 | unit_test = UnitTests.TABLE 66 | 67 | is_run_all_tests = False 68 | if is_run_all_tests: 69 | for unit_test in UnitTests: 70 | run_unit_test(unit_test=unit_test) 71 | else: 72 | run_unit_test(unit_test=unit_test) 73 | -------------------------------------------------------------------------------- /qis/plots/derived/regime_class_table.py: -------------------------------------------------------------------------------- 1 | """ 2 | plot tables for regime classification 3 | """ 4 | # packages 5 | import numpy as np 6 | import pandas as pd 7 | import matplotlib.pyplot as plt 8 | from typing import Optional 9 | from enum import Enum 10 | 11 | # qis 12 | import qis.utils.df_cut as dfc 13 | import qis.utils.df_str as dfs 14 | import qis.plots.table as ptb 15 | 16 | 17 | def get_quantile_class_table(data: pd.DataFrame, 18 | x_column: str, 19 | y_column: Optional[str] = None, 20 | num_buckets: int = 4, 21 | y_data_lag: Optional[int] = None, 22 | hue_name: str = 'hue', 23 | xvar_format: str = '{:.2f}' 24 | ) -> pd.DataFrame: 25 | scatter_data, _ = dfc.add_quantile_classification(df=data, x_column=x_column, hue_name=hue_name, 26 | num_buckets=num_buckets, 27 | bins=None, 28 | xvar_format=xvar_format) 29 | if y_column is None: 30 | y_column = x_column 31 | 32 | if y_data_lag is not None: 33 | scatter_data[y_column] = scatter_data[y_column].shift(y_data_lag) 34 | # scatter_data = scatter_data.dropna() 35 | 36 | stats = scatter_data[[y_column, hue_name]].groupby(hue_name, sort=False).agg(['count', 'mean', 'std']) 37 | stats.columns = stats.columns.get_level_values(1) 38 | stats.insert(loc=0, column='freq', value=stats['count'].to_numpy() / np.sum(stats['count'].to_numpy())) 39 | return stats 40 | 41 | 42 | def plot_quantile_class_table(data: pd.DataFrame, 43 | x_column: str, 44 | y_column: Optional[str] = None, 45 | num_buckets: int = 4, 46 | hue_name: str = 'hue', 47 | var_format: str = '{:.2%}', 48 | xvar_format: str = '{:.2f}', 49 | y_data_lag: Optional[int] = None, 50 | ax: plt.Subplot = None, 51 | **kwargs 52 | ) -> None: 53 | stats = get_quantile_class_table(data=data, x_column=x_column, y_column=y_column, 54 | num_buckets=num_buckets, 55 | y_data_lag=y_data_lag, 56 | hue_name=hue_name, 57 | xvar_format=xvar_format) 58 | 59 | # stats is freq, count, mean and std 60 | stats = dfs.df_to_str(stats, var_formats=['{:.0%}', '{:.0f}', var_format, var_format]) 61 | 62 | ptb.plot_df_table(df=stats, 63 | index_column_name=hue_name, 64 | ax=ax, 65 | **kwargs) 66 | 67 | 68 | class UnitTests(Enum): 69 | QUANTILE_CLASS_TABLE = 1 70 | 71 | 72 | def run_unit_test(unit_test: UnitTests): 73 | 74 | from qis.test_data import load_etf_data 75 | prices = load_etf_data().dropna() 76 | returns = prices.asfreq('QE', method='ffill').pct_change().dropna() 77 | 78 | if unit_test == UnitTests.QUANTILE_CLASS_TABLE: 79 | plot_quantile_class_table(data=returns, x_column='SPY', num_buckets=4, hue_name='quantile regime') 80 | 81 | plt.show() 82 | 83 | 84 | if __name__ == '__main__': 85 | 86 | unit_test = UnitTests.QUANTILE_CLASS_TABLE 87 | 88 | is_run_all_tests = False 89 | if is_run_all_tests: 90 | for unit_test in UnitTests: 91 | run_unit_test(unit_test=unit_test) 92 | else: 93 | run_unit_test(unit_test=unit_test) -------------------------------------------------------------------------------- /qis/plots/derived/regime_pdf.py: -------------------------------------------------------------------------------- 1 | """ 2 | plot returns heatmap table by monthly and annual 3 | """ 4 | # packages 5 | import pandas as pd 6 | import matplotlib.pyplot as plt 7 | import seaborn as sns 8 | from enum import Enum 9 | from matplotlib.ticker import FuncFormatter 10 | 11 | # qis 12 | import qis.plots.utils as put 13 | from qis.perfstats.regime_classifier import BenchmarkReturnsQuantileRegimeSpecs, BenchmarkReturnsQuantilesRegime 14 | 15 | 16 | def plot_regime_pdf(prices: pd.DataFrame, 17 | benchmark: str, 18 | regime_params: BenchmarkReturnsQuantileRegimeSpecs = BenchmarkReturnsQuantileRegimeSpecs(), 19 | ax: plt.Subplot = None, 20 | var_format: str = '{:.0%}', 21 | is_histogram: bool = False, 22 | is_multiple_stack: bool = False, 23 | title: str = None, 24 | fontsize: int = 10, 25 | bins: int = 30, 26 | legend_loc: str = None, 27 | **kwargs 28 | ) -> plt.Figure: 29 | 30 | if ax is None: 31 | fig, ax = plt.subplots() 32 | else: 33 | fig = None 34 | 35 | regime_classifier = BenchmarkReturnsQuantilesRegime(regime_params=regime_params) 36 | sampled_returns_with_regime_id = regime_classifier.compute_sampled_returns_with_regime_id(prices=prices, 37 | benchmark=benchmark, 38 | **regime_params._asdict()) 39 | 40 | if is_histogram: 41 | sns.histplot(data=sampled_returns_with_regime_id, 42 | x=benchmark, 43 | hue=regime_classifier.REGIME_COLUMN, 44 | hue_order=regime_classifier.get_regime_ids_colors().keys(), 45 | multiple='stack' if is_multiple_stack else 'layer', 46 | bins=bins, 47 | palette=regime_classifier.get_regime_ids_colors().values(), 48 | ax=ax) 49 | 50 | 51 | else: 52 | sns.kdeplot(data=sampled_returns_with_regime_id, 53 | x=benchmark, 54 | hue=regime_classifier.REGIME_COLUMN, 55 | hue_order=regime_classifier.get_regime_ids_colors().keys(), 56 | palette=regime_classifier.get_regime_ids_colors().values(), 57 | ax=ax) 58 | 59 | ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: var_format.format(x))) 60 | 61 | put.set_legend(ax=ax, legend_loc=legend_loc, fontsize=fontsize, **kwargs) 62 | 63 | ax.get_yaxis().set_visible(False) 64 | put.set_spines(ax=ax, **kwargs) 65 | if title is not None: 66 | ax.set_title(label=title, **kwargs) 67 | 68 | return fig 69 | 70 | 71 | class UnitTests(Enum): 72 | REGIME_PDF = 1 73 | 74 | 75 | def run_unit_test(unit_test: UnitTests): 76 | 77 | from qis.test_data import load_etf_data 78 | prices = load_etf_data()[['SPY', 'TLT']].dropna() 79 | 80 | if unit_test == UnitTests.REGIME_PDF: 81 | with sns.axes_style("darkgrid"): 82 | fig, axs = plt.subplots(1, 2, figsize=(15, 8), tight_layout=True) 83 | plot_regime_pdf(prices=prices, benchmark='SPY', is_histogram=False, ax=axs[0]) 84 | plot_regime_pdf(prices=prices, benchmark='SPY', is_histogram=True, ax=axs[1]) 85 | 86 | plt.show() 87 | 88 | 89 | if __name__ == '__main__': 90 | 91 | unit_test = UnitTests.REGIME_PDF 92 | 93 | is_run_all_tests = False 94 | if is_run_all_tests: 95 | for unit_test in UnitTests: 96 | run_unit_test(unit_test=unit_test) 97 | else: 98 | run_unit_test(unit_test=unit_test) 99 | -------------------------------------------------------------------------------- /qis/plots/derived/returns_scatter.py: -------------------------------------------------------------------------------- 1 | # packages 2 | import numpy as np 3 | import pandas as pd 4 | import matplotlib.pyplot as plt 5 | from typing import Optional, Union 6 | from enum import Enum 7 | 8 | # qis 9 | from qis.utils.df_melt import melt_scatter_data_with_xvar 10 | import qis.perfstats.returns as ret 11 | from qis.plots.scatter import plot_scatter 12 | from qis.perfstats.config import ReturnTypes 13 | 14 | 15 | def plot_returns_scatter(prices: pd.DataFrame, 16 | benchmark: str = None, 17 | benchmark_prices: Union[pd.Series, pd.DataFrame] = None, 18 | freq: Optional[str] = 'QE', 19 | order: int = 2, 20 | ci: Optional[int] = 95, 21 | add_45line: bool = False, 22 | is_vol_norm: bool = False, 23 | y_column: str = 'Strategy returns', 24 | xlabel: str = None, 25 | ylabel: str = 'returns', 26 | var_format: str = '{:.1%}', 27 | title: Union[str, None] = None, 28 | add_hue_model_label: bool = True, 29 | hue_name: str = 'hue', 30 | return_type: ReturnTypes = ReturnTypes.RELATIVE, 31 | ax: plt.Subplot = None, 32 | **kwargs 33 | ) -> plt.Figure: 34 | 35 | if benchmark_prices is None: 36 | price_data_full = prices 37 | else: 38 | if isinstance(benchmark_prices, pd.Series): # use benchmark set by series 39 | price_data_full = pd.concat([benchmark_prices, prices], axis=1) 40 | benchmark = benchmark_prices.name 41 | benchmark_prices = None 42 | else: # for df price data must be sries 43 | if not isinstance(prices, pd.Series): 44 | raise ValueError(f"must be series\n{prices}") 45 | price_data_full = pd.concat([prices, benchmark_prices], axis=1) 46 | 47 | returns = ret.to_returns(prices=price_data_full, 48 | include_start_date=True, 49 | include_end_date=True, 50 | return_type=return_type, 51 | freq=freq) 52 | if is_vol_norm: 53 | returns = returns.divide(np.nanstd(returns, axis=0), axis=1) 54 | 55 | if benchmark_prices is None: 56 | scatter_data = melt_scatter_data_with_xvar(df=returns, 57 | xvar_str=benchmark, 58 | y_column=y_column, 59 | hue_name=hue_name) 60 | else: 61 | scatter_data = melt_scatter_data_with_xvar(df=returns, 62 | xvar_str=str(prices.name), 63 | y_column=y_column, 64 | hue_name=hue_name) 65 | benchmark = y_column 66 | y_column = str(prices.name) 67 | 68 | fig = plot_scatter(df=scatter_data, 69 | x=benchmark, 70 | y=y_column, 71 | xlabel=xlabel or benchmark, 72 | ylabel=ylabel, 73 | hue=hue_name, 74 | xvar_format=var_format, 75 | yvar_format=var_format, 76 | add_universe_model_label=False, 77 | add_universe_model_prediction=False, 78 | add_universe_model_ci=False, 79 | add_hue_model_label=add_hue_model_label, 80 | add_45line=add_45line, 81 | title=title, 82 | order=order, 83 | ci=ci, 84 | ax=ax, 85 | **kwargs) 86 | return fig 87 | 88 | 89 | class UnitTests(Enum): 90 | RETURNS = 1 91 | RETURNS2 = 2 92 | 93 | 94 | def run_unit_test(unit_test: UnitTests): 95 | 96 | from qis.test_data import load_etf_data 97 | prices = load_etf_data().dropna() 98 | 99 | if unit_test == UnitTests.RETURNS: 100 | fig, ax = plt.subplots(1, 1, figsize=(8, 6)) 101 | global_kwargs = dict(fontsize=8, linewidth=0.5, weight='normal', markersize=1) 102 | plot_returns_scatter(prices=prices, 103 | benchmark='SPY', 104 | var_format='{:.2%}', 105 | ax=ax, 106 | **global_kwargs) 107 | 108 | elif unit_test == UnitTests.RETURNS2: 109 | 110 | fig, ax = plt.subplots(1, 1, figsize=(8, 6)) 111 | global_kwargs = dict(fontsize=8, linewidth=0.5, weight='normal', markersize=1) 112 | 113 | plot_returns_scatter(prices=prices[['SPY', 'TLT']], 114 | benchmark='TLT', 115 | y_column='benchmarks', 116 | ylabel='SPY', 117 | var_format='{:.2%}', 118 | ax=ax, 119 | **global_kwargs) 120 | 121 | plt.show() 122 | 123 | 124 | if __name__ == '__main__': 125 | 126 | unit_test = UnitTests.RETURNS 127 | 128 | is_run_all_tests = False 129 | if is_run_all_tests: 130 | for unit_test in UnitTests: 131 | run_unit_test(unit_test=unit_test) 132 | else: 133 | run_unit_test(unit_test=unit_test) 134 | -------------------------------------------------------------------------------- /qis/plots/errorbar.py: -------------------------------------------------------------------------------- 1 | """ 2 | errorbar plot 3 | """ 4 | 5 | # packages 6 | import numpy as np 7 | import pandas as pd 8 | import seaborn as sns 9 | import matplotlib.pyplot as plt 10 | from typing import List, Union, Tuple, Optional 11 | from enum import Enum 12 | # qis 13 | import qis.plots.utils as put 14 | 15 | 16 | def plot_errorbar(df: Union[pd.Series, pd.DataFrame], 17 | y_std_errors: Union[float, pd.Series, pd.DataFrame] = 0.5, 18 | exact: Union[pd.Series, pd.DataFrame] = None, # can add exact solution 19 | legend_title: str = None, 20 | legend_loc: Optional[Union[str, bool]] = 'upper left', 21 | xlabel: str = None, 22 | ylabel: str = None, 23 | var_format: Optional[str] = '{:.0f}', 24 | title: Union[str, bool] = None, 25 | fontsize: int = 10, 26 | capsize: int = 10, 27 | colors: List[str] = None, 28 | exact_colors: Union[str, List[str]] = 'green', 29 | marker: Optional[str] = 'o', 30 | exact_marker: str = "v", 31 | y_limits: Tuple[Optional[float], Optional[float]] = None, 32 | add_zero_line: bool = False, 33 | ax: plt.Subplot = None, 34 | **kwargs 35 | ) -> Optional[plt.Figure]: 36 | 37 | if isinstance(df, pd.DataFrame): 38 | pass 39 | elif isinstance(df, pd.Series): 40 | df = df.to_frame() 41 | else: 42 | raise TypeError(f"unsupported data type {type(df)}") 43 | 44 | columns = df.columns 45 | 46 | if ax is None: 47 | fig, ax = plt.subplots() 48 | else: 49 | fig = None 50 | 51 | if colors is None: 52 | colors = put.get_n_colors(n=len(columns), **kwargs) 53 | 54 | for idx, column in enumerate(columns): 55 | if isinstance(y_std_errors, pd.DataFrame): 56 | yerr = y_std_errors[column].to_numpy() # columnwese 57 | elif isinstance(y_std_errors, pd.Series): 58 | yerr = y_std_errors.to_numpy() 59 | else: 60 | yerr = y_std_errors 61 | 62 | ax.errorbar(x=df.index, y=df[column].to_numpy(), yerr=yerr, color=colors[idx], fmt=marker, capsize=capsize) 63 | 64 | if exact is not None: # add exact as scatter points 65 | 66 | if isinstance(exact, pd.Series): 67 | exact = exact.to_frame() 68 | 69 | labels = columns.to_list() 70 | markers = [marker] * len(columns) 71 | if isinstance(exact_colors, str): 72 | exact_colors = [exact_colors] * len(columns) 73 | for idx1, column in enumerate(exact.columns): 74 | for idx, index in enumerate(df.index): 75 | put.add_scatter_points(ax=ax, 76 | label_x_y=[(index, exact.loc[index, column])], 77 | color=exact_colors[idx1], 78 | marker=exact_marker, **kwargs) 79 | labels = labels + [column] 80 | colors = colors + [exact_colors[idx1]] 81 | markers = markers + [exact_marker] 82 | else: 83 | labels = columns 84 | markers = [marker]*len(columns) 85 | 86 | if title is not None: 87 | put.set_title(ax=ax, title=title, fontsize=fontsize) 88 | 89 | if legend_loc is not None: 90 | put.set_legend(ax=ax, 91 | markers=markers, 92 | labels=labels, 93 | colors=colors, 94 | legend_loc=legend_loc, 95 | legend_title=legend_title, 96 | handlelength=0, 97 | fontsize=fontsize, 98 | **kwargs) 99 | 100 | else: 101 | ax.legend().set_visible(False) 102 | 103 | if y_limits is not None: 104 | put.set_y_limits(ax=ax, y_limits=y_limits) 105 | 106 | if var_format is not None: 107 | put.set_ax_ticks_format(ax=ax, fontsize=fontsize, xvar_format=None, yvar_format=var_format, **kwargs) 108 | else: 109 | put.set_ax_ticks_format(ax=ax, fontsize=fontsize, **kwargs) 110 | put.set_ax_xy_labels(ax=ax, xlabel=xlabel, ylabel=ylabel, fontsize=fontsize, **kwargs) 111 | put.set_spines(ax=ax, **kwargs) 112 | 113 | if add_zero_line: 114 | ax.axhline(0, color='black', lw=1) 115 | 116 | return fig 117 | 118 | 119 | class UnitTests(Enum): 120 | ERROR_BAR = 1 121 | 122 | 123 | def run_unit_test(unit_test: UnitTests): 124 | 125 | n = 10 126 | x = np.linspace(0, 10, n) 127 | dy = 0.8 128 | y1 = pd.Series(np.sin(x) + dy * np.random.randn(n), index=x, name='y1') 129 | y2 = pd.Series(np.cos(x) + dy * np.random.randn(n), index=x, name='y2') 130 | data = pd.concat([y1, y2], axis=1) 131 | 132 | if unit_test == UnitTests.ERROR_BAR: 133 | 134 | global_kwargs = {'fontsize': 8, 135 | 'linewidth': 0.5, 136 | 'weight': 'normal', 137 | 'markersize': 1} 138 | 139 | with sns.axes_style('darkgrid'): 140 | fig, ax = plt.subplots(1, 1, figsize=(8, 6)) 141 | plot_errorbar(df=data, 142 | ax=ax, 143 | **global_kwargs) 144 | 145 | plt.show() 146 | 147 | 148 | if __name__ == '__main__': 149 | 150 | unit_test = UnitTests.ERROR_BAR 151 | 152 | is_run_all_tests = False 153 | if is_run_all_tests: 154 | for unit_test in UnitTests: 155 | run_unit_test(unit_test=unit_test) 156 | else: 157 | run_unit_test(unit_test=unit_test) 158 | -------------------------------------------------------------------------------- /qis/plots/heatmap.py: -------------------------------------------------------------------------------- 1 | """ 2 | heatmap plots 3 | """ 4 | 5 | # packages 6 | import pandas as pd 7 | import seaborn as sns 8 | import matplotlib.pyplot as plt 9 | from typing import List, Optional 10 | from enum import Enum 11 | 12 | # qis 13 | import qis.plots.utils as put 14 | 15 | 16 | def plot_heatmap(df: pd.DataFrame, 17 | transpose: bool = False, 18 | inverse: bool = False, 19 | date_format: Optional[str] = '%Y', 20 | cmap: str = 'RdYlGn', 21 | var_format: Optional[str] = '{:.1%}', 22 | alpha: float = 1.0, 23 | fontsize: int = 10, 24 | title: Optional[str] = None, 25 | top_x_label: bool = True, 26 | square: bool = False, 27 | vline_columns: List[int] = None, 28 | hline_rows: List[int] = None, 29 | vmin: float = None, 30 | vmax: float = None, 31 | labelpad: int = 50, 32 | ylabel: str = '', 33 | ax: plt.Subplot = None, 34 | **kwargs 35 | ) -> Optional[plt.Figure]: 36 | 37 | if ax is None: 38 | fig, ax = plt.subplots() 39 | else: # add table to existing axis 40 | fig = None 41 | 42 | df = df.copy() 43 | 44 | if date_format is not None: # index may include 'Total' 45 | df.index = [date.strftime(date_format) if isinstance(date, pd.Timestamp) else date for date in df.index] 46 | 47 | if transpose: 48 | df = df.T 49 | inverse = False 50 | 51 | if inverse: 52 | df = df.reindex(index=df.index[::-1]) 53 | 54 | if var_format is not None: 55 | var_format = var_format.replace('{:', '').replace('}', '') # no {} 56 | 57 | sns.heatmap(data=df, 58 | center=0, 59 | annot=True, 60 | fmt=var_format, 61 | cmap=cmap, 62 | alpha=alpha, 63 | cbar_kws={'size': fontsize}, 64 | cbar=False, 65 | annot_kws={'size': fontsize}, 66 | xticklabels=True, # important for full display of labels 67 | yticklabels=True, # important for full display of labels 68 | square=square, 69 | vmin=vmin, 70 | vmax=vmax, 71 | ax=ax) # ,"ha": 'right' #cbar_kws={'format': '%0.2f%%'} 72 | 73 | if top_x_label: 74 | ax.xaxis.tick_top() 75 | 76 | if not transpose: 77 | pass 78 | # bottom, top = ax.get_ylim() 79 | # ax.set_ylim(bottom + 0.5, top - 0.5) 80 | else: 81 | ax.xaxis.labelpad = labelpad 82 | 83 | put.set_ax_tick_params(ax=ax, fontsize=fontsize, labelbottom=not top_x_label, labeltop=top_x_label, **kwargs) 84 | put.set_ax_tick_labels(ax=ax, fontsize=fontsize, **kwargs) 85 | 86 | if vline_columns is not None: 87 | for vline_column in vline_columns: 88 | ax.vlines([vline_column], *ax.get_ylim(), lw=1) 89 | 90 | if hline_rows is not None: 91 | for hline_row in hline_rows: 92 | ax.hlines([hline_row], *ax.get_xlim(), lw=1) 93 | 94 | if title is not None: 95 | put.set_title(ax=ax, title=title, fontsize=fontsize, **kwargs) 96 | 97 | ax.set_ylabel(ylabel) 98 | ax.set_xlabel('') 99 | 100 | return fig 101 | 102 | 103 | class UnitTests(Enum): 104 | HEATMAP = 1 105 | 106 | 107 | def run_unit_test(unit_test: UnitTests): 108 | 109 | from qis.test_data import load_etf_data 110 | prices = load_etf_data().dropna() 111 | 112 | if unit_test == UnitTests.HEATMAP: 113 | corrs = prices.pct_change().corr() 114 | plot_heatmap(corrs, inverse=False, x_rotation=90) 115 | 116 | plt.show() 117 | 118 | 119 | if __name__ == '__main__': 120 | 121 | unit_test = UnitTests.HEATMAP 122 | 123 | is_run_all_tests = False 124 | if is_run_all_tests: 125 | for unit_test in UnitTests: 126 | run_unit_test(unit_test=unit_test) 127 | else: 128 | run_unit_test(unit_test=unit_test) 129 | -------------------------------------------------------------------------------- /qis/plots/histplot2d.py: -------------------------------------------------------------------------------- 1 | """ 2 | plot histogram 2d 3 | """ 4 | # packages 5 | import numpy as np 6 | import pandas as pd 7 | import seaborn as sns 8 | import matplotlib.pyplot as plt 9 | from scipy import stats 10 | from typing import Optional 11 | from enum import Enum 12 | 13 | # qis 14 | import qis.plots.utils as put 15 | 16 | 17 | def plot_histplot2d(df: pd.DataFrame, 18 | title: str = None, 19 | a_min: float = None, 20 | a_max: float = None, 21 | xvar_format: str = '{:.1f}', 22 | yvar_format: str = '{:.1f}', 23 | add_corr_legend: bool = True, 24 | legend_loc: Optional[str] = 'upper left', 25 | color: str = 'navy', 26 | fontsize: int = 10, 27 | ax: plt.Subplot = None, 28 | **kwargs 29 | ) -> plt.Figure: 30 | 31 | if len(df.columns) != 2: 32 | raise ValueError(f"should be 2 columns") 33 | 34 | if ax is None: 35 | fig, ax = plt.subplots(1, 1, figsize=(8, 6)) 36 | else: 37 | fig = None 38 | 39 | if a_min is not None or a_max is not None: 40 | df = np.clip(df, a_min=a_min, a_max=a_max) 41 | 42 | sns.histplot(data=df, 43 | x=df.columns[0], 44 | y=df.columns[1], 45 | bins=100, 46 | cbar=False, 47 | stat='probability', 48 | cbar_kws=dict(shrink=.75), 49 | ax=ax) 50 | 51 | put.set_ax_ticks_format(ax=ax, xvar_format=xvar_format, yvar_format=yvar_format, **kwargs) 52 | 53 | if add_corr_legend: 54 | rho, pval = stats.spearmanr(df.to_numpy(), nan_policy='omit', axis=0) # column is variable 55 | label = f"Rank corr={rho:0.2f}, p-val={pval:0.2f}" 56 | lines = [(label, {'color': color})] 57 | 58 | put.set_legend(ax=ax, 59 | legend_loc=legend_loc, 60 | fontsize=fontsize, 61 | lines=lines, 62 | **kwargs) 63 | 64 | put.align_xy_limits(ax=ax) 65 | 66 | if title is not None: 67 | ax.set_title(title, fontsize=fontsize, **kwargs) 68 | 69 | return fig 70 | 71 | 72 | class UnitTests(Enum): 73 | TEST = 1 74 | 75 | 76 | def run_unit_test(unit_test: UnitTests): 77 | 78 | if unit_test == UnitTests.TEST: 79 | np.random.seed(1) 80 | n_instruments = 1000 81 | exposures_nm = np.random.normal(0.0, 1.0, size=(n_instruments, 2)) 82 | data = pd.DataFrame(data=exposures_nm, columns=[f"id{n+1}" for n in range(2)]) 83 | 84 | fig, ax = plt.subplots(1, 1, figsize=(3.9, 3.4), tight_layout=True) 85 | global_kwargs = dict(fontsize=6, linewidth=0.5, weight='normal', first_color_fixed=True) 86 | plot_histplot2d(df=data, ax=ax, **global_kwargs) 87 | 88 | plt.show() 89 | 90 | 91 | if __name__ == '__main__': 92 | 93 | unit_test = UnitTests.TEST 94 | 95 | is_run_all_tests = False 96 | if is_run_all_tests: 97 | for unit_test in UnitTests: 98 | run_unit_test(unit_test=unit_test) 99 | else: 100 | run_unit_test(unit_test=unit_test) 101 | -------------------------------------------------------------------------------- /qis/plots/pie.py: -------------------------------------------------------------------------------- 1 | """ 2 | pieplot 3 | """ 4 | # packages 5 | import pandas as pd 6 | import seaborn as sns 7 | import matplotlib.pyplot as plt 8 | from typing import Optional, List 9 | from enum import Enum 10 | 11 | # qis 12 | import qis.plots.utils as put 13 | 14 | 15 | def plot_pie(df: [pd.Series, pd.DataFrame], 16 | y_column: str = None, 17 | ylabel: str = '', 18 | title: str = None, 19 | colors: List[str] = None, 20 | legend_loc: Optional[str] = None, 21 | autopct: Optional[str] = '%.0f%%', 22 | ax: plt.Subplot = None, 23 | **kwargs 24 | ) -> Optional[plt.Figure]: 25 | 26 | if ax is None: 27 | fig, ax = plt.subplots() 28 | else: 29 | fig = None 30 | 31 | if y_column is None and isinstance(df, pd.DataFrame): 32 | y_column = df.columns[0] 33 | if colors is None: 34 | colors = put.get_cmap_colors(n=len(df.index), **kwargs) 35 | 36 | df.plot.pie(y=y_column, autopct=autopct, colors=colors, ax=ax) 37 | 38 | if legend_loc is None: 39 | ax.legend().set_visible(False) 40 | 41 | if title is not None: 42 | put.set_title(ax=ax, title=title, **kwargs) 43 | 44 | ax.set_ylabel(ylabel) 45 | 46 | return fig 47 | 48 | 49 | class UnitTests(Enum): 50 | PORTFOLIO = 1 51 | 52 | 53 | def run_unit_test(unit_test: UnitTests): 54 | 55 | if unit_test == UnitTests.PORTFOLIO: 56 | 57 | df = pd.DataFrame({'Conservative': [0.5, 0.25, 0.25], 58 | 'Balanced': [0.30, 0.30, 0.40], 59 | 'Growth': [0.10, 0.40, 0.50]}, 60 | index=['Stables', 'Market-neutral', 'Crypto-Beta']) 61 | print(df) 62 | kwargs = dict(fontsize=8, linewidth=0.5, weight='normal', markersize=1) 63 | 64 | with sns.axes_style("darkgrid"): 65 | fig, ax = plt.subplots(1, 1, figsize=(8, 6), tight_layout=True) 66 | plot_pie(df=df, 67 | ax=ax, 68 | **kwargs) 69 | 70 | plt.show() 71 | 72 | 73 | if __name__ == '__main__': 74 | 75 | unit_test = UnitTests.PORTFOLIO 76 | 77 | is_run_all_tests = False 78 | if is_run_all_tests: 79 | for unit_test in UnitTests: 80 | run_unit_test(unit_test=unit_test) 81 | else: 82 | run_unit_test(unit_test=unit_test) 83 | -------------------------------------------------------------------------------- /qis/plots/qqplot.py: -------------------------------------------------------------------------------- 1 | """ 2 | quantile-quantile plot 3 | """ 4 | # packages 5 | import numpy as np 6 | import pandas as pd 7 | import matplotlib.pyplot as plt 8 | from scipy import stats as stats 9 | from statsmodels import api as sm 10 | from typing import List, Union, Tuple, Optional 11 | from enum import Enum 12 | 13 | # qis 14 | import qis.perfstats.returns as ret 15 | import qis.plots.utils as put 16 | import qis.perfstats.desc_table as dsc 17 | 18 | 19 | def plot_qq(df: Union[pd.DataFrame, pd.Series], 20 | colors: List[str] = None, 21 | markers: List[str] = None, 22 | legend_loc: str = 'upper left', 23 | var_format: str = '{:.2f}', 24 | is_drop_na: bool = True, 25 | fontsize: int = 10, 26 | markersize: int = 2, 27 | title: str = None, 28 | xlabel: str = 'Theoretical quantiles', 29 | ylabel: str = 'Empirical quantiles', 30 | desc_table_type: dsc.DescTableType = dsc.DescTableType.SHORT, 31 | legend_stats: put.LegendStats = put.LegendStats.NONE, 32 | x_limits: Tuple[Optional[float], Optional[float]] = None, 33 | y_limits: Tuple[Optional[float], Optional[float]] = None, 34 | ax: plt.Subplot = None, 35 | **kwargs 36 | ) -> plt.Figure: 37 | 38 | if ax is None: 39 | fig, ax = plt.subplots() 40 | else: 41 | fig = None 42 | 43 | if isinstance(df, pd.Series): 44 | df = df.to_frame() 45 | line = 'q' 46 | else: 47 | line= None 48 | 49 | if colors is None: 50 | colors = put.get_n_colors(n=len(df.columns), **kwargs) 51 | 52 | if markers is None: 53 | markers = len(df.columns) * ['o'] 54 | 55 | for idx, column in enumerate(df.columns): 56 | data0 = df[column] 57 | if is_drop_na: 58 | data0 = data0.dropna() 59 | sm.qqplot(data0, stats.norm, fit=True, line=line, ax=ax, fmt=colors[idx], 60 | markerfacecolor=colors[idx], markeredgecolor=colors[idx], marker=markers[idx], 61 | markersize=markersize) 62 | if line is None: 63 | sm.qqline(ax, line='45', fmt='-', color='red') 64 | 65 | if desc_table_type != dsc.DescTableType.NONE: 66 | stats_table = dsc.compute_desc_table(df=df, 67 | desc_table_type=desc_table_type, 68 | var_format=var_format) 69 | put.set_legend_with_stats_table(stats_table=stats_table, 70 | ax=ax, 71 | colors=colors, 72 | legend_loc=legend_loc, 73 | fontsize=fontsize, 74 | **kwargs) 75 | else: 76 | legend_labels = put.get_legend_lines(data=df, 77 | legend_stats=legend_stats, 78 | var_format=var_format) 79 | put.set_legend(ax=ax, 80 | labels=legend_labels, 81 | colors=colors, 82 | legend_loc=legend_loc, 83 | fontsize=fontsize, 84 | **kwargs) 85 | 86 | put.set_ax_xy_labels(ax=ax, xlabel=xlabel, ylabel=ylabel, fontsize=fontsize, **kwargs) 87 | put.set_ax_ticks_format(ax=ax, xvar_format=var_format, yvar_format=var_format, fontsize=fontsize, **kwargs) 88 | put.set_spines(ax=ax, **kwargs) 89 | 90 | if y_limits is not None: 91 | put.set_y_limits(ax=ax, y_limits=y_limits) 92 | if x_limits is not None: 93 | put.set_x_limits(ax=ax, x_limits=x_limits) 94 | 95 | if title is not None: 96 | put.set_title(ax=ax, title=title, fontsize=fontsize) 97 | 98 | return fig 99 | 100 | 101 | def plot_xy_qq(x: pd.Series, 102 | y: pd.Series, 103 | colors: List[str] = None, 104 | markers: List[str] = None, 105 | labels: List[str] = None, 106 | legend_loc: str = 'upper left', 107 | is_drop_na: bool = True, 108 | ax: plt.Subplot = None, 109 | **kwargs 110 | ) -> plt.Figure: 111 | 112 | if ax is None: 113 | fig, ax = plt.subplots() 114 | else: 115 | fig = None 116 | 117 | if colors is None: 118 | colors = put.get_n_colors(n=1) 119 | 120 | if is_drop_na: 121 | x = x.dropna() 122 | y = y.dropna() 123 | x = x.to_numpy() 124 | y = y.to_numpy() 125 | 126 | qs = np.linspace(0, 1, min(len(x), len(y))) 127 | 128 | x_qs = np.quantile(x, qs) 129 | y_qs = np.quantile(y, qs) 130 | ax.scatter(x_qs, y_qs, c=colors[0]) 131 | 132 | sm.qqline(ax, line='45', fmt='k--') 133 | 134 | put.set_legend(ax=ax, 135 | labels=labels, 136 | colors=colors, 137 | legend_loc=legend_loc, 138 | **kwargs) 139 | 140 | put.set_ax_xy_labels(ax=ax, **kwargs) 141 | put.set_ax_ticks_format(ax=ax, **kwargs) 142 | 143 | return fig 144 | 145 | 146 | class UnitTests(Enum): 147 | RETURNS = 1 148 | XY_PLOT = 2 149 | 150 | 151 | def run_unit_test(unit_test: UnitTests): 152 | 153 | from qis.test_data import load_etf_data 154 | prices = load_etf_data().dropna() 155 | 156 | df = ret.to_returns(prices=prices, drop_first=True) 157 | 158 | if unit_test == UnitTests.RETURNS: 159 | fig, ax = plt.subplots(1, 1, figsize=(8, 6)) 160 | global_kwargs = dict(fontsize=8, linewidth=0.5, weight='normal', markersize=1) 161 | 162 | plot_qq(df=df, 163 | desc_table_type=dsc.DescTableType.SKEW_KURTOSIS, 164 | ax=ax, 165 | **global_kwargs) 166 | 167 | elif unit_test == UnitTests.XY_PLOT: 168 | fig, ax = plt.subplots(1, 1, figsize=(8, 6)) 169 | global_kwargs = dict(fontsize=8, linewidth=0.5, weight='normal', markersize=1) 170 | plot_xy_qq(x=df.iloc[:, 1], 171 | y=df.iloc[:, 0], 172 | ax=ax, 173 | **global_kwargs) 174 | 175 | plt.show() 176 | 177 | 178 | if __name__ == '__main__': 179 | 180 | unit_test = UnitTests.RETURNS 181 | 182 | is_run_all_tests = False 183 | if is_run_all_tests: 184 | for unit_test in UnitTests: 185 | run_unit_test(unit_test=unit_test) 186 | else: 187 | run_unit_test(unit_test=unit_test) -------------------------------------------------------------------------------- /qis/plots/reports/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/QuantInvestStrats/03a55416465f7bdebeae31c3fd95e6abffde3e5a/qis/plots/reports/__init__.py -------------------------------------------------------------------------------- /qis/plots/reports/price_history.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import pandas as pd 3 | import qis as qis 4 | import qis.plots.utils as put 5 | from typing import Optional, Tuple 6 | 7 | 8 | def generate_price_history_report(prices: pd.DataFrame, 9 | figsize: Tuple[float, float] = (8.3, 11.7), 10 | **kwargs 11 | ) -> plt.Figure: 12 | fig, axs = plt.subplots(1, 2, figsize=figsize, tight_layout=True) 13 | qis.plot_ra_perf_table(prices=prices, 14 | title='Risk-Adjusted Performance', 15 | ax=axs[0], 16 | **kwargs) 17 | plot_price_history(prices=prices, 18 | title='Price History', 19 | ax=axs[1], 20 | **kwargs) 21 | return fig 22 | 23 | 24 | def plot_price_history(prices: pd.DataFrame, 25 | title: str = None, 26 | #date_format: str = '%d-%b-%y', 27 | ax: plt.Subplot = None, 28 | **kwargs 29 | ) -> Optional[plt.Figure]: 30 | 31 | start_dates = {} 32 | end_dates = {} 33 | durations = {} 34 | for asset in prices.columns: 35 | price = prices[asset].dropna() 36 | if not price.empty: 37 | start_dates[asset] = price.index[0] 38 | end_dates[asset] = price.index[-1] 39 | durations[asset] = qis.get_time_to_maturity(maturity_time=price.index[-1], 40 | value_time=price.index[0]) 41 | 42 | start_dates = pd.Series(start_dates).rename('Start') 43 | end_dates = pd.Series(end_dates).rename('End') 44 | durations = pd.Series(durations).rename('Period') 45 | df = pd.concat([start_dates, end_dates, durations], axis=1) 46 | df = df.iloc[::-1] # reverse index 47 | if ax is None: 48 | width, height, _, _ = put.calc_df_table_size(df=df, min_rows=len(df.index), min_cols=len(df.index)//2) 49 | fig, ax = plt.subplots(1, 1, figsize=(width, height), constrained_layout=True) 50 | else: 51 | fig = None 52 | 53 | ax.hlines(df.index, xmin=df['Start'], xmax=df['End']) 54 | 55 | # put.set_ax_tick_params(ax=ax) 56 | put.set_ax_ticks_format(ax=ax, **qis.update_kwargs(kwargs, dict())) 57 | 58 | if title is not None: 59 | put.set_title(ax=ax, title=title, **kwargs) 60 | ax.margins(x=0.015, y=0.015) 61 | return fig 62 | -------------------------------------------------------------------------------- /qis/plots/reports/utils.py: -------------------------------------------------------------------------------- 1 | # packages 2 | import pandas as pd 3 | import matplotlib.pyplot as plt 4 | from typing import Union, Dict, Tuple, Optional 5 | from enum import Enum 6 | import qis.utils.df_str as dfs 7 | import qis.plots.utils as put 8 | from qis.plots.table import plot_df_table 9 | from qis.perfstats.config import PerfParams 10 | 11 | DATE_FORMAT = '%d%b%Y' # 31Jan2020 - common across all reporting 12 | WEEK_DAYS_PER_YEAR = 260 # calendar days excluding weekends in a year 13 | 14 | PERF_PARAMS = PerfParams(freq_reg='B', freq_vol='B', freq_drawdown='B') 15 | 16 | 17 | class ReportType(Enum): 18 | SingleTimeSeries = 1 # ax = 1 19 | SingleTimeSeriesWithPDF = 2 # ax = 2 20 | WithInSampleTimeSeries = 3 # ax = 2 21 | WithInSampleTimeSeriesPDF = 4 # ax = 3 22 | 23 | 24 | def set_x_date_freq(data: Union[pd.Series, pd.DataFrame], 25 | kwargs: Dict 26 | ) -> Dict: 27 | if len(data.index) / WEEK_DAYS_PER_YEAR > 30: # increase freq 28 | local_kwargs = kwargs.copy() 29 | local_kwargs.update({'x_date_freq': '2YE'}) 30 | elif len(data.index) / WEEK_DAYS_PER_YEAR < 1.0: # increase freq 31 | local_kwargs = kwargs.copy() 32 | local_kwargs.update({'x_date_freq': 'ME'}) 33 | else: 34 | local_kwargs = kwargs 35 | return local_kwargs 36 | 37 | 38 | def get_summary_table_fig(data_start_dates: Union[pd.Series, pd.DataFrame], 39 | descriptive_data: Union[pd.Series, pd.DataFrame] = None, 40 | figsize: Tuple[float, Optional[float]] = (3.7, None), 41 | first_column_name: str = 'Instrument', 42 | **kwargs 43 | ) -> plt.Subplot: 44 | 45 | if isinstance(data_start_dates, pd.Series): 46 | summary_data = dfs.series_to_str(ds=data_start_dates, var_format=DATE_FORMAT).to_frame() 47 | elif isinstance(data_start_dates, pd.DataFrame): 48 | summary_data = dfs.df_to_str(df=data_start_dates, var_format=DATE_FORMAT) 49 | else: 50 | raise TypeError(f"unsupported type {type(data_start_dates)}") 51 | 52 | if descriptive_data is not None: 53 | summary_data = pd.concat([descriptive_data, summary_data], axis=1) 54 | index_column_name = first_column_name 55 | # summary_data = summary_data.reset_index() 56 | # summary_data.index = np.arange(1, len(summary_data) + 1) 57 | else: 58 | index_column_name = first_column_name 59 | 60 | height = put.calc_table_height(num_rows=len(summary_data.index)) 61 | fig, ax = plt.subplots(1, 1, figsize=(figsize[0], height)) 62 | plot_df_table(df=summary_data, 63 | index_column_name=index_column_name, 64 | ax=ax, 65 | **kwargs) 66 | return fig 67 | -------------------------------------------------------------------------------- /qis/portfolio/README.md: -------------------------------------------------------------------------------- 1 | TO DO 2 | -------------------------------------------------------------------------------- /qis/portfolio/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from qis.portfolio.portfolio_data import (PortfolioData, 3 | PortfolioInput, 4 | AttributionMetric, 5 | SnapshotPeriod) 6 | from qis.portfolio.signal_data import StrategySignalData 7 | 8 | from qis.portfolio.multi_portfolio_data import MultiPortfolioData 9 | 10 | from qis.portfolio.ewm_portfolio_risk import (limit_weights_to_max_var_limit, 11 | compute_portfolio_var_np, 12 | compute_portfolio_vol, 13 | compute_portfolio_correlated_var_by_groups, 14 | compute_portfolio_independent_var_by_ac, 15 | compute_portfolio_risk_contributions) 16 | 17 | from qis.portfolio.backtester import (backtest_model_portfolio, backtest_rebalanced_portfolio) 18 | 19 | from qis.portfolio.reports.config import (FactsheetConfig, 20 | FACTSHEET_CONFIG_DAILY_DATA_LONG_PERIOD, 21 | FACTSHEET_CONFIG_DAILY_DATA_SHORT_PERIOD, 22 | FACTSHEET_CONFIG_MONTHLY_DATA_LONG_PERIOD, 23 | FACTSHEET_CONFIG_MONTHLY_DATA_SHORT_PERIOD, 24 | fetch_factsheet_config_kwargs, 25 | fetch_default_perf_params, 26 | fetch_default_report_kwargs, 27 | ReportingFrequency) 28 | 29 | from qis.portfolio.reports.brinson_attribution import (compute_brinson_attribution_table, 30 | plot_brinson_totals_table, 31 | plot_brinson_attribution_table) 32 | 33 | from qis.portfolio.reports.multi_assets_factsheet import (MultiAssetsReport, generate_multi_asset_factsheet) 34 | 35 | from qis.portfolio.reports.strategy_factsheet import generate_strategy_factsheet 36 | 37 | from qis.portfolio.reports.strategy_benchmark_factsheet import (generate_strategy_benchmark_factsheet_plt, 38 | generate_strategy_benchmark_active_perf_plt, 39 | plot_exposures_strategy_vs_benchmark_stack, 40 | weights_tracking_error_report_by_ac_subac) 41 | 42 | from qis.portfolio.reports.multi_strategy_factsheet import generate_multi_portfolio_factsheet 43 | 44 | from qis.portfolio.reports.strategy_signal_factsheet import (generate_weight_change_report, 45 | generate_current_signal_report, 46 | generate_strategy_signal_factsheet_by_instrument) 47 | 48 | from qis.portfolio.reports.overlays_smart_diversification import SmartDiversificationReport 49 | 50 | # disable requirements for pyblogs 51 | # from qis.portfolio.reports.multi_strategy_factseet_pybloqs import generate_multi_portfolio_factsheet_with_pyblogs 52 | -------------------------------------------------------------------------------- /qis/portfolio/reports/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/QuantInvestStrats/03a55416465f7bdebeae31c3fd95e6abffde3e5a/qis/portfolio/reports/__init__.py -------------------------------------------------------------------------------- /qis/portfolio/strats/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/QuantInvestStrats/03a55416465f7bdebeae31c3fd95e6abffde3e5a/qis/portfolio/strats/__init__.py -------------------------------------------------------------------------------- /qis/portfolio/strats/quant_strats_delta1.py: -------------------------------------------------------------------------------- 1 | 2 | # packages 3 | import numpy as np 4 | import pandas as pd 5 | from typing import Tuple, Union, List 6 | 7 | # qis 8 | import qis.utils as qu 9 | import qis.perfstats.returns as ret 10 | import qis.models.linear.ewm as ewm 11 | 12 | 13 | def simulate_vol_target_strats(prices: Union[pd.DataFrame, pd.Series], 14 | vol_span: int = 21, 15 | vol_target: float = 0.15, 16 | constant_trade_level: bool = False, 17 | vol_af: float = 260 18 | ) -> Tuple[pd.DataFrame, pd.DataFrame]: 19 | """ 20 | simulate weights and returns on vol target 21 | """ 22 | log_returns = ret.to_returns(prices=prices, is_log_returns=True) 23 | returns = ret.to_returns(prices=prices, is_log_returns=False) 24 | ewm_vol = ewm.compute_ewm_vol(data=log_returns, 25 | span=vol_span, 26 | mean_adj_type=ewm.MeanAdjType.NONE, 27 | annualization_factor=vol_af) 28 | # vol target weights 29 | weights_100 = qu.to_finite_reciprocal(data=ewm_vol, fill_value=0.0, is_gt_zero=True) 30 | nav_weights = weights_100.multiply(vol_target) 31 | vt_returns = returns.multiply(nav_weights.shift(1)) 32 | vt_navs = ret.returns_to_nav(returns=vt_returns, constant_trade_level=constant_trade_level) 33 | return nav_weights, vt_navs 34 | 35 | 36 | def simulate_vol_target_strats_range(prices: Union[pd.DataFrame, pd.Series], 37 | vol_spans: List[int] = (21, 31), 38 | vol_target: float = 0.15, 39 | constant_trade_level: bool = False, 40 | vol_af: float = 260, 41 | add_asset: bool = True 42 | ) -> Tuple[pd.DataFrame, pd.DataFrame]: 43 | vt_nav_weights, vt_navs = [], [] 44 | for vol_span in vol_spans: 45 | vt_nav_weights_, vt_navs_ = simulate_vol_target_strats(prices=prices, vol_span=vol_span, vol_target=vol_target, 46 | constant_trade_level=constant_trade_level, vol_af=vol_af) 47 | if isinstance(prices, pd.Series): 48 | name = f"{prices.name} vol_span={vol_span}" 49 | vt_nav_weights_.name, vt_navs_.name = name, name 50 | else: 51 | names = [f"{x} vol_span={vol_span}" for x in prices.columns] 52 | vt_nav_weights_.columns, vt_navs_.columns = names, names 53 | vt_nav_weights.append(vt_nav_weights_) 54 | vt_navs.append(vt_navs_) 55 | vt_nav_weights, vt_navs = pd.concat(vt_nav_weights, axis=1), pd.concat(vt_navs, axis=1) 56 | if add_asset: 57 | vt_navs = pd.concat([prices, vt_navs], axis=1) 58 | return vt_nav_weights, vt_navs 59 | 60 | 61 | def simulate_trend_strats(prices: Union[pd.DataFrame, pd.Series], 62 | vol_span: int = 33, 63 | tf_span: int = 63, 64 | vol_target: float = 0.15, 65 | constant_trade_level: bool = False, 66 | vol_af: float = 260 67 | ) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: 68 | """ 69 | simulate weights and returns on tf strats 70 | """ 71 | log_returns = ret.to_returns(prices=prices, is_log_returns=True) 72 | returns = ret.to_returns(prices=prices) 73 | 74 | ewm_vol = ewm.compute_ewm_vol(data=log_returns, span=vol_span, mean_adj_type=ewm.MeanAdjType.NONE, 75 | annualize=False) 76 | 77 | # vol target weights 78 | weights_100 = qu.to_finite_reciprocal(data=ewm_vol, fill_value=0.0, is_gt_zero=True) 79 | vt_return_100 = returns.multiply(weights_100.shift(1)) 80 | # signal is unit var 81 | signals = ewm.compute_ewm(data=vt_return_100, span=tf_span, is_unit_vol_scaling=True) 82 | # normalized to target vol 83 | weights = signals.multiply(weights_100).multiply(vol_target/np.sqrt(vol_af)) 84 | vt_returns = returns.multiply(weights.shift(1)) 85 | vt_navs = ret.returns_to_nav(returns=vt_returns, constant_trade_level=constant_trade_level) 86 | return weights, vt_navs, signals 87 | 88 | 89 | def simulate_trend_strats_range(prices: Union[pd.DataFrame, pd.Series], 90 | vol_span: int = 33, 91 | tf_spans: List[int] = (21, 63), 92 | vol_target: float = 0.15, 93 | constant_trade_level: bool = False, 94 | vol_af: float = 260, 95 | add_asset: bool = True 96 | ) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: 97 | tf_nav_weights, tf_navs, signals = [], [], [] 98 | for tf_span in tf_spans: 99 | tf_nav_weights_, tf_navs_, signals_ = simulate_trend_strats(prices=prices, tf_span=tf_span, vol_span=vol_span, 100 | vol_target=vol_target, vol_af=vol_af, 101 | constant_trade_level=constant_trade_level) 102 | if isinstance(prices, pd.Series): 103 | name = f"{prices.name} tf_span={tf_span}" 104 | tf_nav_weights_.name, tf_navs_.name, signals_.name = name, name, name 105 | else: 106 | names = [f"{x} tf_span={tf_span}" for x in prices.columns] 107 | tf_nav_weights_.columns, tf_navs_.columns, signals_.columns = names, names, names 108 | tf_nav_weights.append(tf_nav_weights_) 109 | tf_navs.append(tf_navs_) 110 | signals.append(signals_) 111 | tf_nav_weights, tf_navs, signals = pd.concat(tf_nav_weights, axis=1), pd.concat(tf_navs, axis=1), pd.concat(signals, axis=1) 112 | if add_asset: 113 | tf_navs = pd.concat([prices, tf_navs], axis=1) 114 | return tf_nav_weights, tf_navs, signals 115 | -------------------------------------------------------------------------------- /qis/portfolio/strats/seasonal_strats.py: -------------------------------------------------------------------------------- 1 | """ 2 | compute seasonal strategy using monthly returns 3 | """ 4 | 5 | import numpy as np 6 | import pandas as pd 7 | import qis as qis 8 | 9 | 10 | def q60(x): 11 | return x.quantile(0.6) 12 | 13 | 14 | def q40(x): 15 | return x.quantile(0.4) 16 | 17 | 18 | def compute_seasonal_signal(prices: pd.DataFrame) -> pd.DataFrame: 19 | """ 20 | compute seasonal signal 21 | """ 22 | prices = prices.asfreq('B', method='ffill') # make B frequency 23 | returns = qis.to_returns(prices, freq='ME', drop_first=True) 24 | returns['month'] = returns.index.month 25 | seasonal_returns = returns.groupby('month').agg(['mean', q40, q60]) 26 | signals = {} 27 | for asset in prices.columns: 28 | df = seasonal_returns[asset] 29 | signal = np.where(df['q40'] > 0.0, +1.0, np.where(df['q60'] < 0.0, -1.0, 0.0)) 30 | signals[asset] = pd.Series(signal, index=df.index).fillna(0.0) 31 | signals = pd.DataFrame.from_dict(signals, orient='columns') 32 | return signals 33 | 34 | 35 | def compute_rolling_seasonal_signals(prices: pd.DataFrame, 36 | num_sample_years: int = 25 37 | ) -> pd.DataFrame: 38 | rebalancing_years = list(np.arange(2000, 2025)) 39 | monthly_returns = qis.to_returns(prices, freq='ME', drop_first=True, include_end_date=True) 40 | signals = [] 41 | for year in rebalancing_years: 42 | # print(year) 43 | estimation_sample = prices.loc[f"{year-num_sample_years}":f"{year-1}", :] 44 | if year == 2024: 45 | print('here') 46 | # print(estimation_sample) 47 | investment_sample = monthly_returns.iloc[monthly_returns.index.year==year, :] 48 | investment_sample_index = investment_sample.index 49 | investment_sample_index = investment_sample_index.shift(-1, freq="ME") # sift signal to end of previous month 50 | signal = compute_seasonal_signal(prices=estimation_sample) 51 | signal = signal.iloc[:len(investment_sample_index), :] # drop last few mon 52 | # set signal of the investment sample with weight at the end of prior months 53 | signal = signal.set_index(investment_sample_index) 54 | # now move to start of the month 55 | signals.append(signal) 56 | signals = pd.concat(signals, axis=0) 57 | return signals 58 | -------------------------------------------------------------------------------- /qis/settings.yaml: -------------------------------------------------------------------------------- 1 | # set pc/developer local paths 2 | # add to gitignore afterwards 3 | 4 | RESOURCE_PATH: 5 | "C:\\Users\\...\\" 6 | 7 | LOCAL_RESOURCE_PATH: 8 | "C:\\Users\\...\\" 9 | 10 | UNIVERSE_PATH: 11 | "C:\\Users\\...\\" 12 | 13 | OUTPUT_PATH: 14 | "C:\\Users\\...\\" 15 | 16 | AWS_POSTGRES: 17 | "postgresql://user:password@database:port" 18 | 19 | -------------------------------------------------------------------------------- /qis/sql_engine.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from sqlalchemy import create_engine 3 | from sqlalchemy.engine.base import Engine 4 | from pathlib import Path 5 | 6 | 7 | def get_engine(path: str = 'AWS_POSTGRES') -> Engine: 8 | full_file_path = Path(__file__).parent.joinpath('settings.yaml') 9 | with open(full_file_path) as settings: 10 | settings_data = yaml.load(settings, Loader=yaml.Loader) 11 | path = settings_data[path] 12 | engine = create_engine(path) 13 | return engine 14 | -------------------------------------------------------------------------------- /qis/test_data.py: -------------------------------------------------------------------------------- 1 | """ 2 | generate and load test data using yf library 3 | """ 4 | 5 | import pandas as pd 6 | import yfinance as yf 7 | from enum import Enum 8 | 9 | import qis.file_utils as fu 10 | import qis.local_path as local_path 11 | 12 | 13 | RESOURCE_PATH = local_path.get_paths()['RESOURCE_PATH'] 14 | 15 | 16 | def load_etf_data() -> pd.DataFrame: 17 | prices = fu.load_df_from_csv(file_name='etf_prices', local_path=RESOURCE_PATH) 18 | return prices 19 | 20 | 21 | class UnitTests(Enum): 22 | ETF_PRICES = 1 23 | TEST_LOADING = 2 24 | 25 | 26 | def run_unit_test(unit_test: UnitTests): 27 | 28 | if unit_test == UnitTests.ETF_PRICES: 29 | prices = yf.download(tickers=['SPY', 'QQQ', 'EEM', 'TLT', 'IEF', 'LQD', 'HYG', 'SHY', 'GLD'], 30 | start=None, end=None, 31 | ignore_tz=True)['Close'] 32 | print(prices) 33 | fu.save_df_to_csv(df=prices, file_name='etf_prices', local_path=RESOURCE_PATH) 34 | 35 | elif unit_test == UnitTests.TEST_LOADING: 36 | prices = load_etf_data() 37 | print(prices) 38 | 39 | 40 | if __name__ == '__main__': 41 | 42 | unit_test = UnitTests.ETF_PRICES 43 | 44 | is_run_all_tests = False 45 | if is_run_all_tests: 46 | for unit_test in UnitTests: 47 | run_unit_test(unit_test=unit_test) 48 | else: 49 | run_unit_test(unit_test=unit_test) 50 | -------------------------------------------------------------------------------- /qis/utils/README.md: -------------------------------------------------------------------------------- 1 | TO DO -------------------------------------------------------------------------------- /qis/utils/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from qis.utils.dates import ( 3 | TimePeriod, 4 | generate_dates_schedule, 5 | generate_rebalancing_indicators, 6 | set_rebalancing_timeindex_on_given_timeindex, 7 | generate_sample_dates, 8 | get_month_days, 9 | get_period_days, 10 | get_sample_dates_idx, 11 | get_time_period, 12 | get_time_period_label, 13 | get_time_period_shifted_by_years, 14 | get_current_time_with_tz, 15 | get_weekday, 16 | get_year_quarter, 17 | get_ytd_time_period, 18 | get_time_to_maturity, 19 | infer_an_from_data, 20 | is_leap_year, 21 | months_between, 22 | separate_number_from_string, 23 | shift_date_by_day, 24 | shift_dates_by_n_years, 25 | shift_dates_by_year, 26 | shift_time_period_by_days, 27 | split_df_by_freq, 28 | generate_fixed_maturity_rolls, 29 | min_timestamp, 30 | truncate_prior_to_start, 31 | find_upto_date_from_datetime_index, 32 | create_rebalancing_indicators_from_freqs 33 | ) 34 | 35 | 36 | from qis.utils.df_agg import ( 37 | abssum, 38 | abssum_negative, 39 | abssum_positive, 40 | agg_data_by_axis, 41 | agg_dfs, 42 | agg_median_mad, 43 | get_signed_np_data, 44 | nanmean, 45 | nanmean_clip, 46 | nanmean_positive, 47 | nanmedian, 48 | nansum, 49 | nansum_clip, 50 | nansum_negative, 51 | nansum_positive, 52 | sum_weighted, 53 | last_row, 54 | compute_df_desc_data 55 | ) 56 | 57 | from qis.utils.df_cut import ( 58 | add_classification, 59 | add_hue_fixed_years, 60 | add_hue_years, 61 | add_quantile_classification, 62 | sort_index_by_hue, 63 | x_bins_cut 64 | ) 65 | 66 | from qis.utils.df_freq import ( 67 | agg_remained_data_on_right, 68 | df_asfreq, 69 | df_index_to_str, 70 | df_resample_at_freq, 71 | df_resample_at_int_index, 72 | df_resample_at_other_index 73 | ) 74 | 75 | from qis.utils.df_groups import ( 76 | agg_df_by_group_with_avg, 77 | agg_df_by_groups, 78 | agg_df_by_groups_ax1, 79 | fill_df_with_group_avg, 80 | get_group_dict, 81 | sort_df_by_index_group, 82 | split_df_by_groups, 83 | set_group_loadings, 84 | convert_df_column_to_df_by_groups 85 | ) 86 | 87 | from qis.utils.df_melt import ( 88 | melt_df_by_columns, 89 | melt_paired_df, 90 | melt_scatter_data_with_xdata, 91 | melt_scatter_data_with_xvar, 92 | melt_signed_paired_df 93 | ) 94 | 95 | from qis.utils.df_ops import ( 96 | align_df1_to_df2, 97 | align_dfs_dict_with_df, 98 | compute_last_score, 99 | df12_merge_with_tz, 100 | df_indicator_like, 101 | df_joint_indicator, 102 | df_ones_like, 103 | df_time_dict_to_pd, 104 | df_zero_like, 105 | dfs_indicators, 106 | dfs_to_upper_lower_diag, 107 | drop_first_nan_data, 108 | factor_dict_to_asset_dict, 109 | get_first_before_nonnan_index, 110 | get_first_last_nonnan_index, 111 | get_first_nonnan_values, 112 | get_last_nonnan_values, 113 | get_last_nonnan, 114 | merge_dfs_on_column, 115 | compute_nans_zeros_ratio_after_first_non_nan, 116 | reindex_upto_last_nonnan, 117 | multiply_df_by_dt, 118 | norm_df_by_ax_mean, 119 | np_txy_tensor_to_pd_dict 120 | ) 121 | 122 | from qis.utils.df_str import ( 123 | date_to_str, 124 | df_all_to_str, 125 | df_index_to_str, 126 | df_to_numeric, 127 | df_to_str, 128 | df_with_ci_to_str, 129 | float_to_str, 130 | get_fmt_str, 131 | join_str_series, 132 | series_to_date_str, 133 | series_to_numeric, 134 | series_to_str, 135 | series_values_to_str, 136 | str_to_float, 137 | timeseries_df_to_str, 138 | idx_to_alphabet 139 | ) 140 | 141 | from qis.utils.df_to_weights import ( 142 | compute_long_short_ind, 143 | compute_long_short_ind_by_row, 144 | df_nans_to_one_zero, 145 | df_to_equal_weight_allocation, 146 | df_to_top_bottom_n_indicators, 147 | df_to_weight_allocation_sum1, 148 | df_to_long_only_allocation_sum1, 149 | fill_long_short_signal, 150 | compute_long_only_portfolio_weights, 151 | mult_df_columns_with_vector, 152 | mult_df_columns_with_vector_group 153 | ) 154 | from qis.utils.df_to_scores import ( 155 | df_to_max_score, 156 | df_to_cross_sectional_score, 157 | compute_aggregate_scores, 158 | select_top_integrated_scores 159 | ) 160 | 161 | from qis.utils.generic import ( 162 | ValueType, 163 | ColVar, 164 | ColumnData, 165 | column_datas_to_df, 166 | EnumMap, 167 | DotDict 168 | ) 169 | 170 | 171 | from qis.utils.np_ops import ( 172 | compute_expanding_power, 173 | compute_histogram_data, 174 | compute_paired_signs, 175 | covar_to_corr, 176 | find_nearest, 177 | to_nearest_values, 178 | np_array_to_df_columns, 179 | np_array_to_df_index, 180 | np_array_to_matrix, 181 | np_array_to_n_column_array, 182 | np_array_to_t_rows_array, 183 | np_nanmean, 184 | np_nansum, 185 | np_cumsum, 186 | np_nanstd, 187 | np_nanvar, 188 | np_get_sorted_idx, 189 | np_matrix_add_array, 190 | np_nonan_weighted_avg, 191 | np_shift, 192 | running_mean, 193 | to_finite_np, 194 | to_finite_ratio, 195 | to_finite_reciprocal, 196 | repeat_by_columns, 197 | repeat_by_rows, 198 | set_nans_for_warmup_period, 199 | select_non_nan_x_y 200 | ) 201 | 202 | from qis.utils.ols import ( 203 | estimate_alpha_beta_paired_dfs, 204 | estimate_ols_alpha_beta, 205 | fit_multivariate_ols, 206 | fit_ols, 207 | get_ols_x, 208 | reg_model_params_to_str 209 | ) 210 | 211 | from qis.utils.sampling import ( 212 | TrainLivePeriod, 213 | TrainLiveSamples, 214 | get_data_samples_df, 215 | split_to_samples, 216 | split_to_train_live_samples 217 | ) 218 | 219 | from qis.utils.struct_ops import ( 220 | assert_list_subset, 221 | assert_list_unique, 222 | flatten, 223 | flatten_dict_tuples, 224 | list_diff, 225 | list_intersection, 226 | list_to_unique_and_dub, 227 | merge_lists_unique, 228 | move_item_to_first, 229 | separate_number_from_string, 230 | split_dict, 231 | to_flat_list, 232 | update_kwargs 233 | ) 234 | 235 | from qis.file_utils import timer 236 | 237 | 238 | -------------------------------------------------------------------------------- /qis/utils/df_to_scores.py: -------------------------------------------------------------------------------- 1 | """ 2 | various df functions to create scores of df data 3 | """ 4 | import numpy as np 5 | import pandas as pd 6 | from typing import Union, Optional, List 7 | 8 | 9 | def df_to_cross_sectional_score(df: Union[pd.Series, pd.DataFrame], 10 | lower_clip: Optional[float] = -5.0, 11 | upper_clip: Optional[float] = 5.0, 12 | is_sorted: bool = False 13 | ) -> Union[pd.Series, pd.DataFrame]: 14 | """ 15 | compute cross sectional score 16 | """ 17 | if lower_clip is not None or upper_clip is not None: 18 | df = df.clip(lower=lower_clip, upper=upper_clip) 19 | 20 | if isinstance(df, pd.Series): 21 | score = (df - np.nanmean(df)) / np.nanstd(df) 22 | if is_sorted: 23 | score = score.sort_values(ascending=False) 24 | else: 25 | score = (df - np.nanmean(df, axis=1, keepdims=True)) / np.nanstd(df, axis=1, keepdims=True) 26 | return score 27 | 28 | 29 | def df_to_max_score(df: Union[pd.Series, pd.DataFrame]) -> Union[pd.Series, pd.DataFrame]: 30 | """ 31 | normalized rows by cross-sectional max: max element = 1.0 32 | """ 33 | if isinstance(df, pd.Series): 34 | score = df.divide(np.nanmax(df, axis=0)) 35 | else: 36 | score = df.divide(np.nanmax(df, axis=1, keepdims=True)) 37 | return score 38 | 39 | 40 | def compute_aggregate_scores(scores: List[pd.Series], 41 | lower_clip: Optional[float] = -5.0, 42 | upper_clip: Optional[float] = 5.0, 43 | normalize_to_unit_std: bool = True, 44 | penalise_nan_values: bool = True 45 | ) -> pd.Series: 46 | """ 47 | aggregate list scores 48 | nan values are penalised 49 | normalize_to_unit_std: avg of scores is unit std 50 | """ 51 | joint = pd.concat(scores, axis=1).clip(lower=lower_clip, upper=upper_clip) 52 | if penalise_nan_values: # scores with nan are penalised 53 | n = len(scores) 54 | if normalize_to_unit_std: 55 | norm = 1.0 / np.sqrt(n) 56 | else: 57 | norm = 1 / n 58 | joint_avg = norm * np.nansum(joint, axis=1) 59 | else: 60 | joint_avg = np.nanmean(joint, axis=1) 61 | joint_score = pd.Series(joint_avg, index=joint.index).sort_values(ascending=False) 62 | return joint_score 63 | 64 | 65 | def select_top_integrated_scores(scores: pd.DataFrame, top_quantile: float = 0.75) -> pd.DataFrame: 66 | """ 67 | 68 | """ 69 | score_quantiles = np.nanquantile(scores, q=top_quantile, axis=0) 70 | if len(scores.columns) == 1: 71 | joint = np.greater(scores.iloc[:, 0], score_quantiles[0]) 72 | else: 73 | top1 = np.greater(scores.iloc[:, 0], score_quantiles[0]) 74 | top2 = np.greater(scores.iloc[:, 1], score_quantiles[1]) 75 | joint = np.logical_and(top1, top2) 76 | 77 | if len(scores.columns) > 2: 78 | for idx in np.arange(2, len(scores.columns)): 79 | top_idx = np.greater(scores.iloc[:, idx], score_quantiles[idx]) 80 | joint = np.logical_and(joint, top_idx) 81 | 82 | scores = scores.loc[joint, :] 83 | return scores 84 | -------------------------------------------------------------------------------- /qis/utils/sampling.py: -------------------------------------------------------------------------------- 1 | """ 2 | utiliti 3 | """ 4 | # packages 5 | import pandas as pd 6 | import numpy as np 7 | import matplotlib.pyplot as plt 8 | from enum import Enum 9 | from dataclasses import dataclass 10 | from typing import NamedTuple, Dict, Union 11 | # qis 12 | from qis.utils.dates import TimePeriod 13 | 14 | 15 | class TrainLivePeriod(NamedTuple): 16 | train: TimePeriod 17 | live: TimePeriod 18 | 19 | 20 | @dataclass 21 | class TrainLiveSamples: 22 | """ 23 | dictionary when the traning start with TrainLivePeriod 24 | """ 25 | train_live_dates: Dict[TimePeriod, TrainLivePeriod] = None 26 | 27 | def __post_init__(self): 28 | self.train_live_dates = {} 29 | 30 | def add(self, date: TimePeriod, train_live_period: TrainLivePeriod): 31 | self.train_live_dates[date] = train_live_period 32 | 33 | def print(self): 34 | for key, samples in self.train_live_dates.items(): 35 | print(f"{key}: train={samples.train.to_str()}, live={samples.live.to_str()}") 36 | 37 | 38 | def split_to_train_live_samples(ts_index: Union[pd.DatetimeIndex, pd.Index], 39 | model_update_freq: 'str' = 'ME', 40 | roll_period: int = 12 41 | ) -> TrainLiveSamples: 42 | """ 43 | ts index is split into overlapping periods at freq = model_update_freq and period lenth of roll_period 44 | """ 45 | update_dates = pd.date_range(start=ts_index[0], 46 | end=ts_index[-1], 47 | freq=model_update_freq, 48 | inclusive='right') 49 | 50 | train_live_samples = TrainLiveSamples() 51 | for idx, date in enumerate(update_dates): 52 | # x_data is shifted backward 53 | # last point in y_data is applied for forecast 54 | # data used for estimation is [last_update, current update date] 55 | model_update_date = update_dates[idx - 1] 56 | 57 | if idx > roll_period + 1: 58 | date_t_0 = update_dates[idx-roll_period-1] 59 | train = TimePeriod(start=date_t_0, end=model_update_date).shift_end_date_by_days(backward=True) 60 | live = TimePeriod(start=model_update_date, end=date).shift_start_date_by_days(backward=False) 61 | 62 | train_live_samples.add(model_update_date, TrainLivePeriod(train, live)) 63 | 64 | return train_live_samples 65 | 66 | 67 | def split_to_samples(data: Union[pd.DataFrame, pd.Series], 68 | sample_freq: 'str' = 'YE', 69 | start_to_one: bool = False 70 | ) -> Dict[pd.Timestamp, pd.DataFrame]: 71 | data1 = data.resample(sample_freq).last() 72 | ts_index = data1.index 73 | update_dates = pd.date_range(start=ts_index[0], 74 | end=ts_index[-1], 75 | freq=sample_freq) 76 | data_samples = {} 77 | for idx, date in enumerate(update_dates): 78 | if idx > 1 and date < ts_index[-1]: 79 | period_data = data.loc[update_dates[idx-1]: date] 80 | if start_to_one: 81 | period_data = period_data.divide(period_data.iloc[0]) 82 | data_samples[date] = period_data 83 | 84 | return data_samples 85 | 86 | 87 | def get_data_samples_df(data: Union[pd.DataFrame, pd.Series], 88 | sample_freq: 'str' = 'YE', 89 | start_to_one: bool = False 90 | ) -> pd.DataFrame: 91 | data_samples = {} 92 | data_samples_dict = split_to_samples(data, sample_freq=sample_freq, start_to_one=start_to_one) 93 | for key, kdata in data_samples_dict.items(): 94 | data_samples[key] = kdata.reset_index(drop=True) 95 | data_samples_df = pd.DataFrame.from_dict(data_samples) 96 | data_samples_df = data_samples_df.ffill() 97 | return data_samples_df 98 | 99 | 100 | class UnitTests(Enum): 101 | SAMPLE_DATES = 1 102 | SPLIT_TO_SAMPLES = 2 103 | 104 | 105 | def run_unit_test(unit_test: UnitTests): 106 | 107 | if unit_test == UnitTests.SAMPLE_DATES: 108 | time_period = TimePeriod(start='31Dec2018', end='31Dec2020') 109 | 110 | ts_index = time_period.to_pd_datetime_index(freq='ME') 111 | train_live_samples = split_to_train_live_samples(ts_index=ts_index, model_update_freq='ME', roll_period=12) 112 | train_live_samples.print() 113 | 114 | elif unit_test == UnitTests.SPLIT_TO_SAMPLES: 115 | time_period = TimePeriod(start='31Dec2010', end='31Dec2020') 116 | 117 | ts_index = time_period.to_pd_datetime_index(freq='B') 118 | data = pd.DataFrame(data=np.random.normal(0, 1.0, (len(ts_index), 1)), index=ts_index, columns=['id1']) 119 | 120 | data_samples = split_to_samples(data=data, sample_freq='YE') 121 | print(data_samples) 122 | 123 | plt.show() 124 | 125 | 126 | if __name__ == '__main__': 127 | 128 | unit_test = UnitTests.SPLIT_TO_SAMPLES 129 | 130 | is_run_all_tests = False 131 | if is_run_all_tests: 132 | for unit_test in UnitTests: 133 | run_unit_test(unit_test=unit_test) 134 | else: 135 | run_unit_test(unit_test=unit_test) 136 | -------------------------------------------------------------------------------- /qis/utils/struct_ops.py: -------------------------------------------------------------------------------- 1 | """ 2 | common python structures operations 3 | """ 4 | import itertools 5 | import pandas as pd 6 | from collections.abc import Iterable 7 | from enum import Enum 8 | from typing import Dict, Tuple, List, Any, NamedTuple, Union, Optional 9 | 10 | 11 | def list_intersection(list_check: Union[List[Any], pd.Index], 12 | list_sample: Union[List[Any], pd.Index] 13 | ) -> List[Any]: 14 | list_out = list(filter(lambda x: x in list_check, list_sample)) 15 | if list_out is None: 16 | list_out = [] 17 | return list_out 18 | 19 | 20 | def list_diff(list_check: List[Any], 21 | list_sample: List[Any] 22 | ) -> List[Any]: 23 | list_out = list(filter(lambda x: x not in list_check, list_sample)) 24 | if list_out is None: 25 | list_out = [] 26 | return list_out 27 | 28 | 29 | def merge_lists_unique(list1: List[Any], 30 | list2: List[Any] 31 | ) -> List[Any]: 32 | list_out = list_intersection(list_check=list2, list_sample=list1) 33 | list_diffs1 = list_diff(list_check=list_out, list_sample=list1) 34 | list_diffs2 = list_diff(list_check=list_out, list_sample=list2) 35 | if len(list_diffs1) > 0: 36 | list_out.extend(list_diffs1) 37 | if len(list_diffs2) > 0: 38 | list_out.extend(list_diffs2) 39 | return list_out 40 | 41 | 42 | def assert_list_subset(large_list: List[Any], # larger list 43 | list_sample: List[Any], # smaller list, 44 | is_stop: bool = True, 45 | message: str = '' 46 | ) -> bool: 47 | outlyers = list_diff(list_check=large_list, list_sample=list_sample) 48 | output = True 49 | if len(outlyers) > 0: 50 | if is_stop: 51 | raise ValueError(f"{message}\nnot found {outlyers}") 52 | else: 53 | print(f"in assert_list_subset:\n{message}\n{outlyers}") 54 | output = False 55 | return output 56 | 57 | 58 | def list_to_unique_and_dub(lsdata: List) -> Tuple[List, List]: 59 | """ 60 | find unique and dublicates in list 61 | """ 62 | unique = set() 63 | dublicated = [] # can count occurencies if needed 64 | for x in lsdata: 65 | if x not in unique: 66 | unique.add(x) 67 | else: 68 | dublicated.append(x) 69 | unique = list(unique) 70 | return unique, dublicated 71 | 72 | 73 | def assert_list_unique(lsdata: List[str]) -> None: 74 | unique, duplicated = list_to_unique_and_dub(lsdata=lsdata) 75 | if len(duplicated) > 0: 76 | raise ValueError(f"list has duplicated elements = {duplicated}") 77 | 78 | 79 | def move_item_to_first(lsdata: List[Any], item: Any) -> List[Any]: 80 | out_list = lsdata.copy() 81 | out_list.remove(item) 82 | out_list = [item]+out_list 83 | return out_list 84 | 85 | 86 | def flatten_dict_tuples(dict_tuples: Dict[str, Union[Any, NamedTuple]]) -> Dict[str, Any]: 87 | data = {} 88 | for k, v in dict_tuples.items(): 89 | if isinstance(v, tuple): 90 | data.update(v._asdict()) # this will create a dict from named tuple 91 | else: 92 | data[k] = v 93 | return data 94 | 95 | 96 | def split_dict(d: Dict) -> Tuple[Dict, Dict]: 97 | """ 98 | split dictionary into 2 parts 99 | """ 100 | n = len(d) // 2 101 | i = iter(d.items()) 102 | 103 | d1 = dict(itertools.islice(i, n)) # grab first n items 104 | d2 = dict(i) # grab the rest 105 | 106 | return d1, d2 107 | 108 | 109 | def flatten(items: Iterable) -> Any: 110 | """ 111 | flatten list/items from any nested iterable 112 | """ 113 | for x in items: 114 | if isinstance(x, Iterable) and not isinstance(x, (str, bytes)): 115 | for sub_x in flatten(x): 116 | yield sub_x 117 | else: 118 | yield x 119 | 120 | 121 | def to_flat_list(items: Iterable) -> List[Any]: 122 | if isinstance(items, Iterable): 123 | flat_list = [item for item in flatten(items)] 124 | else: 125 | flat_list = [items] 126 | return flat_list 127 | 128 | 129 | def update_kwargs(kwargs: Dict[Any, Any], 130 | new_kwargs: Optional[Dict[Any, Any]] 131 | ) -> Dict[Any, Any]: 132 | """ 133 | update kwargs with optional kwargs dicts 134 | """ 135 | local_kwargs = kwargs.copy() 136 | if new_kwargs is not None and not len(new_kwargs) == 0: 137 | local_kwargs.update(new_kwargs) 138 | return local_kwargs 139 | 140 | 141 | def separate_number_from_string(string: str) -> List[str]: 142 | """ 143 | given 'A3' get 3 144 | """ 145 | previous_character = string[0] 146 | groups = [] 147 | newword = string[0] 148 | for x, i in enumerate(string[1:]): 149 | if i.isalpha() and previous_character.isalpha(): 150 | newword += i 151 | elif i.isnumeric() and previous_character.isnumeric(): 152 | newword += i 153 | else: 154 | groups.append(newword) 155 | newword = i 156 | previous_character = i 157 | if x == len(string) - 2: 158 | groups.append(newword) 159 | newword = '' 160 | 161 | return groups 162 | 163 | 164 | class UnitTests(Enum): 165 | FLATTEN = 1 166 | LIST = 2 167 | MERGE = 3 168 | STRINGS = 4 169 | 170 | 171 | def run_unit_test(unit_test: UnitTests): 172 | 173 | if unit_test == UnitTests.FLATTEN: 174 | items = [[1, 2], [[3]], 4] 175 | flat_items = flatten(items) 176 | [print(item) for item in flat_items] 177 | print(to_flat_list(items)) 178 | 179 | elif unit_test == UnitTests.LIST: 180 | rows_edge_lines = list(itertools.accumulate(10 * [5])) 181 | print(rows_edge_lines) 182 | 183 | elif unit_test == UnitTests.MERGE: 184 | list2 = ['EQ', 'HUI'] 185 | list1 = ['EQ', 'BD', 'STIR', 'FX', 'Energies', 'Metals', 'Ags'] 186 | groups = merge_lists_unique(list1=list1, list2=list2) 187 | print('groups1') 188 | print(groups) 189 | groups = merge_lists_unique(list1=list2, list2=list1) 190 | print('groups2') 191 | print(groups) 192 | 193 | elif unit_test == UnitTests.STRINGS: 194 | string = '123me45you0000me7+33.3' 195 | this = separate_number_from_string(string) 196 | print(this) 197 | 198 | 199 | if __name__ == '__main__': 200 | 201 | unit_test = UnitTests.STRINGS 202 | 203 | is_run_all_tests = False 204 | if is_run_all_tests: 205 | for unit_test in UnitTests: 206 | run_unit_test(unit_test=unit_test) 207 | else: 208 | run_unit_test(unit_test=unit_test) 209 | 210 | 211 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArturSepp/QuantInvestStrats/03a55416465f7bdebeae31c3fd95e6abffde3e5a/requirements.txt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | def read_requirements(file): 5 | with open(file) as f: 6 | return f.read().splitlines() 7 | 8 | 9 | def read_file(file): 10 | with open(file) as f: 11 | return f.read() 12 | 13 | 14 | long_description = read_file("README.md") 15 | requirements = read_requirements("requirements.txt") 16 | 17 | setup( 18 | name='qis', 19 | version='3.2.20', 20 | author='Artur Sepp', 21 | author_email='artursepp@gmail.com', 22 | url='https://github.com/ArturSepp/QuantInvestStrats', 23 | description='Implementation of statistical analytics, visualisation and reporting for Quantitative Investment Strategies', 24 | long_description_content_type="text/x-rst", # If this causes a warning, upgrade your setuptools package 25 | long_description=long_description, 26 | license="MIT license", 27 | packages=find_packages(exclude=["qis/examples/figures/", "qis/examples/notebooks/"]), # Don't include test directory in binary distribution 28 | install_requires=requirements, 29 | classifiers=[ 30 | "Programming Language :: Python :: 3", 31 | "License :: OSI Approved :: MIT License", 32 | "Operating System :: OS Independent", 33 | ] 34 | ) --------------------------------------------------------------------------------