sf-quant 0.2.0__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. {sf_quant-0.2.0 → sf_quant-0.2.2}/.gitignore +2 -0
  2. {sf_quant-0.2.0 → sf_quant-0.2.2}/PKG-INFO +1 -1
  3. sf_quant-0.2.2/docs/reference/backtester/sf_quant.backtester.dynamic_backtest_parallel.rst +6 -0
  4. sf_quant-0.2.2/docs/reference/backtester/sf_quant.backtester.dynamic_parallel.rst +12 -0
  5. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/backtester.rst +2 -1
  6. sf_quant-0.2.2/docs/reference/optimizer/sf_quant.optimizer.dynamic_mve_optimizer.rst +6 -0
  7. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/optimizer.rst +1 -0
  8. {sf_quant-0.2.0 → sf_quant-0.2.2}/pyproject.toml +1 -1
  9. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/backtester/__init__.py +2 -1
  10. sf_quant-0.2.2/src/sf_quant/backtester/dynamic_parallel.py +244 -0
  11. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/backtester/parallel.py +46 -5
  12. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/backtester/sequential.py +17 -7
  13. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/data/__init__.py +13 -1
  14. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/data/_tables.py +5 -0
  15. sf_quant-0.2.2/src/sf_quant/data/alphas.py +251 -0
  16. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/data/covariance_matrix.py +29 -0
  17. sf_quant-0.2.2/src/sf_quant/data/optimal_weights.py +111 -0
  18. sf_quant-0.2.2/src/sf_quant/data/scores.py +143 -0
  19. sf_quant-0.2.2/src/sf_quant/data/signals.py +143 -0
  20. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/optimizer/__init__.py +2 -1
  21. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/optimizer/constraints.py +120 -19
  22. sf_quant-0.2.2/src/sf_quant/optimizer/optimizers.py +390 -0
  23. sf_quant-0.2.0/src/sf_quant/optimizer/optimizers.py +0 -105
  24. {sf_quant-0.2.0 → sf_quant-0.2.2}/.github/workflows/publish.yml +0 -0
  25. {sf_quant-0.2.0 → sf_quant-0.2.2}/.readthedocs.yaml +0 -0
  26. {sf_quant-0.2.0 → sf_quant-0.2.2}/README.md +0 -0
  27. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/Makefile +0 -0
  28. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/conf.py +0 -0
  29. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/index.rst +0 -0
  30. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/make.bat +0 -0
  31. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/backtester/sf_quant.backtester.backtest_parallel.rst +0 -0
  32. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/backtester/sf_quant.backtester.backtest_sequential.rst +0 -0
  33. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.construct_covariance_matrix.rst +0 -0
  34. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.get_assets_columns.rst +0 -0
  35. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.get_crsp_daily_columns.rst +0 -0
  36. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.get_crsp_monthly_columns.rst +0 -0
  37. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.get_crsp_v2_daily_columns.rst +0 -0
  38. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.get_crsp_v2_monthly_columns.rst +0 -0
  39. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.get_exposures_columns.rst +0 -0
  40. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.get_factor_names.rst +0 -0
  41. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.get_factors_columns.rst +0 -0
  42. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.get_fama_french_columns.rst +0 -0
  43. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.load_assets.rst +0 -0
  44. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.load_assets_by_date.rst +0 -0
  45. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.load_benchmark.rst +0 -0
  46. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.load_benchmark_returns.rst +0 -0
  47. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.load_crsp_daily.rst +0 -0
  48. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.load_crsp_monthly.rst +0 -0
  49. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.load_crsp_v2_daily.rst +0 -0
  50. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.load_crsp_v2_monthly.rst +0 -0
  51. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.load_exposures.rst +0 -0
  52. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.load_factors.rst +0 -0
  53. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data/sf_quant.data.load_fama_french.rst +0 -0
  54. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/data.rst +0 -0
  55. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/optimizer/sf_quant.optimizer.FullInvestment.rst +0 -0
  56. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/optimizer/sf_quant.optimizer.LongOnly.rst +0 -0
  57. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/optimizer/sf_quant.optimizer.NoBuyingOnMargin.rst +0 -0
  58. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/optimizer/sf_quant.optimizer.UnitBeta.rst +0 -0
  59. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/optimizer/sf_quant.optimizer.ZeroBeta.rst +0 -0
  60. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/optimizer/sf_quant.optimizer.ZeroInvestment.rst +0 -0
  61. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/optimizer/sf_quant.optimizer.mve_optimizer.rst +0 -0
  62. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/performance/sf_quant.performance.generate_alpha_ics.rst +0 -0
  63. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/performance/sf_quant.performance.generate_drawdown_chart.rst +0 -0
  64. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/performance/sf_quant.performance.generate_drawdown_from_returns.rst +0 -0
  65. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/performance/sf_quant.performance.generate_drawdown_summary_table.rst +0 -0
  66. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/performance/sf_quant.performance.generate_ic_chart.rst +0 -0
  67. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/performance/sf_quant.performance.generate_leverage_chart.rst +0 -0
  68. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/performance/sf_quant.performance.generate_leverage_from_weights.rst +0 -0
  69. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/performance/sf_quant.performance.generate_leverage_summary_table.rst +0 -0
  70. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/performance/sf_quant.performance.generate_multi_returns_chart.rst +0 -0
  71. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/performance/sf_quant.performance.generate_multi_returns_from_weights.rst +0 -0
  72. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/performance/sf_quant.performance.generate_multi_returns_summary_table.rst +0 -0
  73. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/performance/sf_quant.performance.generate_returns_chart.rst +0 -0
  74. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/performance/sf_quant.performance.generate_returns_from_weights.rst +0 -0
  75. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/performance/sf_quant.performance.generate_returns_summary_table.rst +0 -0
  76. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/performance/sf_quant.performance.get_turnover_stats.rst +0 -0
  77. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/performance/sf_quant.performance.plot_turnover.rst +0 -0
  78. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/performance.rst +0 -0
  79. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/research/sf_quant.research.beta_scale_ports.rst +0 -0
  80. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/research/sf_quant.research.generate_quantile_ports.rst +0 -0
  81. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/research/sf_quant.research.get_signal_distribution.rst +0 -0
  82. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/research/sf_quant.research.get_signal_stats.rst +0 -0
  83. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/research/sf_quant.research.run_ff_regression.rst +0 -0
  84. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/research/sf_quant.research.run_quantile_ff_regression.rst +0 -0
  85. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/research/sf_quant.research.vol_scale_ports.rst +0 -0
  86. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/reference/research.rst +0 -0
  87. {sf_quant-0.2.0 → sf_quant-0.2.2}/docs/requirements.txt +0 -0
  88. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/__init__.py +0 -0
  89. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/data/_config.py +0 -0
  90. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/data/_factors.py +0 -0
  91. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/data/_views.py +0 -0
  92. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/data/assets.py +0 -0
  93. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/data/benchmark.py +0 -0
  94. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/data/covariances.py +0 -0
  95. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/data/crsp_daily.py +0 -0
  96. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/data/crsp_monthly.py +0 -0
  97. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/data/crsp_v2_daily.py +0 -0
  98. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/data/crsp_v2_monthly.py +0 -0
  99. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/data/exposures.py +0 -0
  100. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/data/factors.py +0 -0
  101. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/data/fama_french.py +0 -0
  102. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/performance/__init__.py +0 -0
  103. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/performance/charts.py +0 -0
  104. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/performance/drawdown.py +0 -0
  105. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/performance/ics.py +0 -0
  106. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/performance/leverage.py +0 -0
  107. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/performance/returns.py +0 -0
  108. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/performance/tables.py +0 -0
  109. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/performance/turnover.py +0 -0
  110. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/research/__init__.py +0 -0
  111. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/research/ff_regression.py +0 -0
  112. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/research/quantile_portfolios.py +0 -0
  113. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/research/signal_analysis.py +0 -0
  114. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/schema/__init__.py +0 -0
  115. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/schema/alpha_schema.py +0 -0
  116. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/schema/drawdown_schema.py +0 -0
  117. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/schema/fama_french_schema.py +0 -0
  118. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/schema/ic_schema.py +0 -0
  119. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/schema/leverage_schema.py +0 -0
  120. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/schema/portfolio_schema.py +0 -0
  121. {sf_quant-0.2.0 → sf_quant-0.2.2}/src/sf_quant/schema/returns_schema.py +0 -0
  122. {sf_quant-0.2.0 → sf_quant-0.2.2}/uv.lock +0 -0
@@ -220,3 +220,5 @@ __marimo__/
220
220
 
221
221
  # macOS
222
222
  .DS_Store
223
+
224
+ test.ipynb
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sf-quant
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Python package for Silver Fund quant team research and trading tools.
5
5
  Author-email: Andrew Hall <andrewmartinhall2@gmail.com>
6
6
  Requires-Python: <3.14,>=3.11
@@ -0,0 +1,6 @@
1
+ sf\_quant.backtester.dynamic\_backtest\_parallel
2
+ ================================================
3
+
4
+ .. currentmodule:: sf_quant.backtester
5
+
6
+ .. autofunction:: dynamic_backtest_parallel
@@ -0,0 +1,12 @@
1
+ sf\_quant.backtester.dynamic\_parallel
2
+ ======================================
3
+
4
+ .. automodule:: sf_quant.backtester.dynamic_parallel
5
+
6
+
7
+ .. rubric:: Functions
8
+
9
+ .. autosummary::
10
+
11
+ dynamic_backtest_parallel
12
+
@@ -13,4 +13,5 @@ Backtester
13
13
  :nosignatures:
14
14
 
15
15
  backtest_sequential
16
- backtest_parallel
16
+ backtest_parallel
17
+ dynamic_backtest_parallel
@@ -0,0 +1,6 @@
1
+ sf\_quant.optimizer.dynamic\_mve\_optimizer
2
+ ===========================================
3
+
4
+ .. currentmodule:: sf_quant.optimizer
5
+
6
+ .. autofunction:: dynamic_mve_optimizer
@@ -13,6 +13,7 @@ Optimizer
13
13
  :nosignatures:
14
14
 
15
15
  mve_optimizer
16
+ dynamic_mve_optimizer
16
17
  FullInvestment
17
18
  ZeroInvestment
18
19
  LongOnly
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sf-quant"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "Python package for Silver Fund quant team research and trading tools."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11,<3.14"
@@ -7,5 +7,6 @@ internal execution details hidden.
7
7
 
8
8
  from .sequential import backtest_sequential
9
9
  from .parallel import backtest_parallel
10
+ from .dynamic_parallel import dynamic_backtest_parallel
10
11
 
11
- __all__ = ["backtest_sequential", "backtest_parallel"]
12
+ __all__ = ["backtest_sequential", "backtest_parallel", "dynamic_backtest_parallel"]
@@ -0,0 +1,244 @@
1
+ import polars as pl
2
+ import os
3
+ import ray
4
+ import datetime as dt
5
+ from ray.experimental import tqdm_ray
6
+ from sf_quant.data.benchmark import load_benchmark
7
+ from sf_quant.data.covariance_matrix import construct_factor_model_components
8
+ from sf_quant.optimizer.optimizers import dynamic_mve_optimizer
9
+ from sf_quant.optimizer.constraints import Constraint
10
+ from sf_quant.schema.portfolio_schema import PortfolioSchema
11
+
12
+
13
+ @ray.remote
14
+ def _construct_portfolio(
15
+ date_: dt.date,
16
+ data: pl.DataFrame,
17
+ constraints: list[Constraint],
18
+ gamma: float,
19
+ target_active_risk: float | None = None,
20
+ benchmark_df: pl.DataFrame | None = None,
21
+ progress_bar: tqdm_ray.tqdm | None = None,
22
+ ):
23
+ """
24
+ Construct a single optimized portfolio for a given date.
25
+
26
+ This is a Ray-remote helper function for parallel portfolio construction.
27
+ It filters data to a specific date, constructs the factor model components,
28
+ and solves a mean-variance optimization problem.
29
+
30
+ Parameters
31
+ ----------
32
+ date_ : dt.date
33
+ The date for which to construct the portfolio.
34
+ data : pl.DataFrame
35
+ Full dataset with columns 'date', 'barrid', 'alpha', optionally 'predicted_beta'.
36
+ constraints : list[Constraint]
37
+ Portfolio constraints to enforce.
38
+ gamma : float
39
+ Risk aversion parameter.
40
+ target_active_risk : float, optional
41
+ Target active risk for gamma calibration.
42
+ benchmark_df : pl.DataFrame, optional
43
+ Benchmark weights data with columns 'date', 'barrid', 'benchmark_weight'.
44
+ progress_bar : tqdm_ray.tqdm, optional
45
+ Ray-based progress bar for tracking completion.
46
+
47
+ Returns
48
+ -------
49
+ pl.DataFrame
50
+ Portfolio weights for the given date, with columns 'date', 'barrid', 'weight'.
51
+ """
52
+ subset = data.filter(pl.col("date").eq(date_)).sort("barrid")
53
+ barrids = subset["barrid"].to_list()
54
+ alphas = subset["alpha"].to_numpy()
55
+
56
+ betas = (
57
+ subset["predicted_beta"].to_numpy()
58
+ if "predicted_beta" in subset.columns
59
+ else None
60
+ )
61
+
62
+ benchmark_weights = None
63
+ if benchmark_df is not None:
64
+ # Get benchmark weights for portfolio barrids on this date
65
+ bmk_subset = benchmark_df.filter(pl.col("date").eq(date_))
66
+ # Join with portfolio to ensure alignment
67
+ aligned = (
68
+ pl.DataFrame({"barrid": barrids})
69
+ .join(bmk_subset[["barrid", "benchmark_weight"]], on="barrid", how="left")
70
+ .fill_null(0.0)
71
+ )
72
+ benchmark_weights = aligned["benchmark_weight"].to_numpy()
73
+
74
+ (
75
+ factor_exposures,
76
+ factor_covariance,
77
+ specific_risk,
78
+ ) = construct_factor_model_components(date_, barrids)
79
+
80
+ portfolio = dynamic_mve_optimizer(
81
+ ids=barrids,
82
+ alphas=alphas,
83
+ factor_exposures=factor_exposures,
84
+ factor_covariance=factor_covariance,
85
+ specific_risk=specific_risk,
86
+ constraints=constraints,
87
+ initial_gamma=gamma,
88
+ betas=betas,
89
+ target_active_risk=target_active_risk,
90
+ benchmark_weights=benchmark_weights,
91
+ )
92
+
93
+ portfolio = portfolio.with_columns(pl.lit(date_).alias("date")).select(
94
+ "date", "barrid", "weight", 'gamma', 'active_risk'
95
+ )
96
+
97
+ if progress_bar is not None:
98
+ progress_bar.update.remote(1)
99
+
100
+ return portfolio
101
+
102
+
103
+ def dynamic_backtest_parallel(
104
+ data: pl.DataFrame,
105
+ constraints: list[Constraint],
106
+ initial_gamma: float = 100,
107
+ target_active_risk: float = 0.05,
108
+ n_cpus: int | None = None,
109
+ ) -> pl.DataFrame:
110
+ """
111
+ Run a parallelized backtest of portfolio optimization using Ray.
112
+
113
+ This function distributes portfolio construction tasks across multiple CPUs
114
+ using Ray, solving mean–variance optimization problems for each date in parallel.
115
+ A Ray-based progress bar tracks computation progress.
116
+
117
+ Parameters
118
+ ----------
119
+ data : pl.DataFrame
120
+ Input dataset containing at least the following columns:
121
+
122
+ - ``date`` : datetime-like, the date of each observation.
123
+ - ``barrid`` : str, unique identifier for each asset.
124
+ - ``alpha`` : float, expected return (alpha) for each asset.
125
+ - ``predicted_beta`` : float, optional, factor exposures for constraints.
126
+
127
+ constraints : list[Constraint]
128
+ List of portfolio constraints to enforce during optimization.
129
+ initial_gamma : float, optional
130
+ Risk aversion parameter. Higher values penalize portfolio variance
131
+ more strongly. Used as the starting gamma for calibration if
132
+ ``target_active_risk`` is specified. Default is 100.
133
+ target_active_risk : float, optional
134
+ If specified, automatically calibrate gamma for each date to achieve this
135
+ target annualized active risk (e.g., 0.05 for 5%). Requires benchmark data
136
+ to be available via :func:`~sf_quant.data.benchmark.load_benchmark`.
137
+ n_cpus : int, optional
138
+ Number of CPUs to allocate to Ray. If ``None``, defaults to
139
+ ``os.cpu_count()`` but is capped at the number of unique dates.
140
+
141
+ Returns
142
+ -------
143
+ pl.DataFrame
144
+ A PortfolioSchema-validated Polars DataFrame containing optimized
145
+ portfolio weights across all backtest dates, with columns:
146
+
147
+ - ``date`` : datetime, portfolio date.
148
+ - ``barrid`` : str, asset identifier.
149
+ - ``weight`` : float, optimized portfolio weight.
150
+ - ``gamma`` : float, calibrated risk aversion parameter for each date.
151
+ - ``active_risk`` : float, achieved annualized active risk for each date.
152
+
153
+ Notes
154
+ -----
155
+ - Benchmark data is automatically loaded when ``target_active_risk`` is specified,
156
+ via :func:`~sf_quant.data.benchmark.load_benchmark`.
157
+ - Ray is initialized with ``ignore_reinit_error=True``, allowing safe
158
+ re-invocation within the same process.
159
+
160
+ See Also
161
+ --------
162
+ backtest_parallel : Sequential version without active risk calibration.
163
+ dynamic_mve_optimizer : Underlying optimizer with active risk calibration.
164
+
165
+ Examples
166
+ --------
167
+ >>> import sf_quant.data as sfd
168
+ >>> import sf_quant.backtester as sfb
169
+ >>> import sf_quant.optimizer as sfo
170
+ >>> import polars as pl
171
+ >>> import datetime as dt
172
+ >>> start = dt.date(2024, 1, 1)
173
+ >>> end = dt.date(2024, 1, 10)
174
+ >>> columns = ['date', 'barrid']
175
+ >>> data = (
176
+ ... sfd.load_assets(
177
+ ... start=start,
178
+ ... end=end,
179
+ ... in_universe=True,
180
+ ... columns=columns
181
+ ... )
182
+ ... .with_columns(
183
+ ... pl.lit(0).alias('alpha')
184
+ ... )
185
+ ... )
186
+ >>> constraints = [sfo.FullInvestment()]
187
+ >>> weights = sfb.dynamic_backtest_parallel(
188
+ ... data=data,
189
+ ... constraints=constraints,
190
+ ... initial_gamma=100,
191
+ ... )
192
+ shape: (5, 5)
193
+ ┌────────────┬─────────┬───────────┬───────┬──────────────┐
194
+ │ date ┆ barrid ┆ weight ┆ gamma ┆ active_risk │
195
+ │ --- ┆ --- ┆ --- ┆ --- ┆ --- │
196
+ │ date ┆ str ┆ f64 ┆ f64 ┆ f64 │
197
+ ╞════════════╪═════════╪═══════════╪═══════╪══════════════╡
198
+ │ 2024-01-02 ┆ USA06Z1 ┆ -0.000639 ┆ 100.0 ┆ 0.047 │
199
+ │ 2024-01-02 ┆ USA0771 ┆ -0.000083 ┆ 100.0 ┆ 0.047 │
200
+ │ 2024-01-02 ┆ USA0C11 ┆ -0.003044 ┆ 100.0 ┆ 0.047 │
201
+ │ 2024-01-02 ┆ USA0SY1 ┆ -0.002177 ┆ 100.0 ┆ 0.047 │
202
+ │ 2024-01-02 ┆ USA11I1 ┆ 0.001475 ┆ 100.0 ┆ 0.047 │
203
+ └────────────┴─────────┴───────────┴───────┴──────────────┘
204
+ """
205
+ # Get dates
206
+ dates = data["date"].unique().sort().to_list()
207
+
208
+ start_date = dates[0]
209
+ end_date = dates[-1]
210
+ benchmark_df = load_benchmark(start_date, end_date).rename({"weight": "benchmark_weight"})
211
+
212
+ # Set up ray
213
+ n_cpus = n_cpus or os.cpu_count()
214
+ n_cpus = min(len(dates), n_cpus)
215
+ # Empty runtime_env = no environment management. Workers use parent's Python directly.
216
+ # This prevents Ray from trying to serialize code or rebuild packages on air-gapped clusters.
217
+ ray.init(ignore_reinit_error=True, num_cpus=n_cpus, runtime_env={})
218
+
219
+ # Set up ray progress bar
220
+ remote_tqdm = ray.remote(tqdm_ray.tqdm)
221
+ progress_bar = remote_tqdm.remote(
222
+ total=len(dates), desc=f"Computing portfolios with {n_cpus} cpus"
223
+ )
224
+
225
+ # Dispatch parallel tasks
226
+ portfolio_futures = [
227
+ _construct_portfolio.remote(
228
+ date_=date_,
229
+ data=data,
230
+ constraints=constraints,
231
+ gamma=initial_gamma,
232
+ target_active_risk=target_active_risk,
233
+ benchmark_df=benchmark_df,
234
+ progress_bar=progress_bar,
235
+ )
236
+ for date_ in dates
237
+ ]
238
+
239
+ portfolio_list = ray.get(portfolio_futures)
240
+
241
+ progress_bar.close.remote()
242
+ ray.shutdown()
243
+
244
+ return pl.concat(portfolio_list)
@@ -3,7 +3,7 @@ import os
3
3
  import ray
4
4
  import datetime as dt
5
5
  from ray.experimental import tqdm_ray
6
- from sf_quant.data.covariance_matrix import construct_covariance_matrix
6
+ from sf_quant.data.covariance_matrix import construct_factor_model_components
7
7
  from sf_quant.optimizer.optimizers import mve_optimizer
8
8
  from sf_quant.optimizer.constraints import Constraint
9
9
  from sf_quant.schema.portfolio_schema import PortfolioSchema
@@ -17,6 +17,31 @@ def _construct_portfolio(
17
17
  gamma: float,
18
18
  progress_bar: tqdm_ray.tqdm | None = None,
19
19
  ):
20
+ """
21
+ Construct a single optimized portfolio for a given date.
22
+
23
+ This is a Ray-remote helper function for parallel portfolio construction.
24
+ It filters data to a specific date, constructs the factor model components,
25
+ and solves a mean-variance optimization problem.
26
+
27
+ Parameters
28
+ ----------
29
+ date_ : dt.date
30
+ The date for which to construct the portfolio.
31
+ data : pl.DataFrame
32
+ Full dataset with columns 'date', 'barrid', 'alpha', optionally 'predicted_beta'.
33
+ constraints : list[Constraint]
34
+ Portfolio constraints to enforce.
35
+ gamma : float
36
+ Risk aversion parameter.
37
+ progress_bar : tqdm_ray.tqdm, optional
38
+ Ray-based progress bar for tracking completion.
39
+
40
+ Returns
41
+ -------
42
+ pl.DataFrame
43
+ Portfolio weights for the given date, with columns 'date', 'barrid', 'weight'.
44
+ """
20
45
  subset = data.filter(pl.col("date").eq(date_)).sort("barrid")
21
46
  barrids = subset["barrid"].to_list()
22
47
  alphas = subset["alpha"].to_numpy()
@@ -27,14 +52,18 @@ def _construct_portfolio(
27
52
  else None
28
53
  )
29
54
 
30
- covariance_matrix = (
31
- construct_covariance_matrix(date_, barrids).drop("barrid").to_numpy()
32
- )
55
+ (
56
+ factor_exposures,
57
+ factor_covariance,
58
+ specific_risk,
59
+ ) = construct_factor_model_components(date_, barrids)
33
60
 
34
61
  portfolio = mve_optimizer(
35
62
  ids=barrids,
36
63
  alphas=alphas,
37
- covariance_matrix=covariance_matrix,
64
+ factor_exposures=factor_exposures,
65
+ factor_covariance=factor_covariance,
66
+ specific_risk=specific_risk,
38
67
  gamma=gamma,
39
68
  constraints=constraints,
40
69
  betas=betas,
@@ -93,6 +122,18 @@ def backtest_parallel(
93
122
  - ``barrid`` : str, asset identifier.
94
123
  - ``weight`` : float, optimized portfolio weight.
95
124
 
125
+ Notes
126
+ -----
127
+ - Ray is initialized with ``ignore_reinit_error=True``, allowing safe
128
+ re-invocation within the same process.
129
+ - The number of CPUs is automatically limited to the number of unique dates
130
+ to avoid unnecessary idle workers.
131
+
132
+ See Also
133
+ --------
134
+ backtest_sequential : Sequential version for smaller datasets or debugging.
135
+ dynamic_backtest_parallel : Parallel version with active risk calibration.
136
+
96
137
  Examples
97
138
  --------
98
139
  >>> import sf_quant.data as sfd
@@ -1,6 +1,6 @@
1
1
  import polars as pl
2
2
  import tqdm
3
- from sf_quant.data.covariance_matrix import construct_covariance_matrix
3
+ from sf_quant.data.covariance_matrix import construct_factor_model_components
4
4
  from sf_quant.optimizer.optimizers import mve_optimizer
5
5
  from sf_quant.optimizer.constraints import Constraint
6
6
  from sf_quant.schema.portfolio_schema import PortfolioSchema
@@ -49,8 +49,14 @@ def backtest_sequential(
49
49
  Notes
50
50
  -----
51
51
  - The optimization is solved using :func:`~sf_quant.optimizer.optimizers.mve_optimizer`.
52
- - The covariance matrix is constructed using
53
- :func:`~sf_quant.data.covariance_matrix.construct_covariance_matrix`.
52
+ - The factor model components are constructed using
53
+ :func:`~sf_quant.data.covariance_matrix.construct_factor_model_components`.
54
+ - Results are validated against :class:`~sf_quant.schema.portfolio_schema.PortfolioSchema`.
55
+
56
+ See Also
57
+ --------
58
+ backtest_parallel : Parallelized version using Ray for faster execution.
59
+ dynamic_backtest_parallel : Parallel version with active risk calibration.
54
60
 
55
61
  Examples
56
62
  --------
@@ -112,14 +118,18 @@ def backtest_sequential(
112
118
  else None
113
119
  )
114
120
 
115
- covariance_matrix = (
116
- construct_covariance_matrix(date_, barrids).drop("barrid").to_numpy()
117
- )
121
+ (
122
+ factor_exposures,
123
+ factor_covariance,
124
+ specific_risk,
125
+ ) = construct_factor_model_components(date_, barrids)
118
126
 
119
127
  portfolio = mve_optimizer(
120
128
  ids=barrids,
121
129
  alphas=alphas,
122
- covariance_matrix=covariance_matrix,
130
+ factor_exposures=factor_exposures,
131
+ factor_covariance=factor_covariance,
132
+ specific_risk=specific_risk,
123
133
  gamma=gamma,
124
134
  constraints=constraints,
125
135
  betas=betas,
@@ -13,10 +13,13 @@ from .crsp_monthly import load_crsp_monthly, get_crsp_monthly_columns
13
13
  from .crsp_v2_monthly import load_crsp_v2_monthly, get_crsp_v2_monthly_columns
14
14
  from .exposures import load_exposures, load_exposures_by_date, get_exposures_columns
15
15
  from .covariances import load_covariances_by_date, get_covariances_columns
16
- from .covariance_matrix import construct_covariance_matrix
16
+ from .covariance_matrix import construct_covariance_matrix, construct_factor_model_components
17
17
  from .benchmark import load_benchmark, load_benchmark_returns
18
18
  from .factors import load_factors, get_factors_columns, get_factor_names
19
19
  from .fama_french import load_fama_french, get_fama_french_columns
20
+ from .signals import load_signals, load_signals_by_date
21
+ from .scores import load_scores, load_scores_by_date
22
+ from .alphas import load_alphas, load_alphas_by_date, load_combined_alphas, load_combined_alphas_by_date
20
23
 
21
24
  __all__ = [
22
25
  "env",
@@ -38,6 +41,7 @@ __all__ = [
38
41
  "load_covariances_by_date",
39
42
  "get_covariances_columns",
40
43
  "construct_covariance_matrix",
44
+ "construct_factor_model_components",
41
45
  "load_benchmark",
42
46
  "load_benchmark_returns",
43
47
  "load_factors",
@@ -45,4 +49,12 @@ __all__ = [
45
49
  "get_factor_names",
46
50
  "load_fama_french",
47
51
  "get_fama_french_columns",
52
+ "load_signals",
53
+ "load_signals_by_date",
54
+ "load_scores",
55
+ "load_scores_by_date",
56
+ "load_alphas",
57
+ "load_alphas_by_date",
58
+ "load_combined_alphas",
59
+ "load_combined_alphas_by_date",
48
60
  ]
@@ -56,3 +56,8 @@ exposures_table = Table("exposures")
56
56
  covariances_table = Table("covariances")
57
57
  factors_table = Table("factors")
58
58
  ff_table = FamaFrenchTable("ff5_factors")
59
+ signals_table = Table("signals")
60
+ scores_table = Table("scores")
61
+ alphas_table = Table("alphas")
62
+ combined_alphas_table = Table("combined_alphas")
63
+ optimal_weights_table = Table("optimal_weights")