aether-quant 0.0.1__py3-none-any.whl

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 (99) hide show
  1. aether_quant-0.0.1.dist-info/METADATA +7 -0
  2. aether_quant-0.0.1.dist-info/RECORD +99 -0
  3. aether_quant-0.0.1.dist-info/WHEEL +5 -0
  4. aether_quant-0.0.1.dist-info/entry_points.txt +2 -0
  5. aether_quant-0.0.1.dist-info/licenses/LICENSE +133 -0
  6. aether_quant-0.0.1.dist-info/top_level.txt +13 -0
  7. analyzer/README.md +69 -0
  8. analyzer/__init__.py +17 -0
  9. analyzer/market_analyzer.py +521 -0
  10. aq_cli.py +3633 -0
  11. audit/README.md +47 -0
  12. audit/__init__.py +38 -0
  13. audit/hash_chain.py +52 -0
  14. audit/postgres_audit.py +61 -0
  15. audit/postgres_worker.py +402 -0
  16. audit/redis_queue.py +124 -0
  17. audit/status_export.py +62 -0
  18. data_pipeline/README.md +257 -0
  19. data_pipeline/__init__.py +6 -0
  20. data_pipeline/bar_synthesis.py +97 -0
  21. data_pipeline/dividend_backfill.py +243 -0
  22. data_pipeline/factor_file_backfill.py +276 -0
  23. data_pipeline/fetch.py +206 -0
  24. data_pipeline/fred_backfill.py +435 -0
  25. data_pipeline/ib_backfill.py +287 -0
  26. data_pipeline/v2_manifest.py +75 -0
  27. data_pipeline/yfinance_backfill.py +501 -0
  28. evaluation/README.md +152 -0
  29. evaluation/__init__.py +73 -0
  30. evaluation/ablation.py +310 -0
  31. evaluation/benchmark_comparison.py +195 -0
  32. evaluation/confidence_threshold_calibration.py +157 -0
  33. evaluation/factor_neutralization_check.py +94 -0
  34. evaluation/feature_parity.py +321 -0
  35. evaluation/feature_reconciliation.py +286 -0
  36. evaluation/impact_model.py +77 -0
  37. evaluation/kill_switch_replay.py +186 -0
  38. evaluation/limit_fill_simulator.py +152 -0
  39. evaluation/model_predictions.py +192 -0
  40. evaluation/monte_carlo.py +161 -0
  41. evaluation/public_benchmarks.py +117 -0
  42. evaluation/rank_book_simulator.py +494 -0
  43. evaluation/rank_ic_core.py +204 -0
  44. evaluation/rank_signal_calibration.py +690 -0
  45. evaluation/rolling_ic_gate_calibration.py +190 -0
  46. evaluation/rolling_ic_gate_replay.py +138 -0
  47. execution/README.md +281 -0
  48. execution/__init__.py +80 -0
  49. execution/config_cache.py +48 -0
  50. execution/cost_model.py +213 -0
  51. execution/lean_config_render.py +131 -0
  52. execution/live_credentials.py +38 -0
  53. execution/live_credentials_io.py +52 -0
  54. execution/order_gate.py +388 -0
  55. execution/paper_readiness.py +181 -0
  56. execution/paper_readiness_io.py +54 -0
  57. execution/paper_readiness_report.py +103 -0
  58. execution/paper_readiness_scheduler.py +137 -0
  59. execution/reconciliation.py +160 -0
  60. execution/runtime_config_io.py +38 -0
  61. execution/secret_scan.py +73 -0
  62. features/README.md +77 -0
  63. features/__init__.py +137 -0
  64. features/alt_data_features.py +154 -0
  65. features/bond_features.py +275 -0
  66. features/cross_asset_sensitivity.py +79 -0
  67. features/derivatives_macro_features.py +72 -0
  68. features/macro_features.py +79 -0
  69. features/options_greeks.py +329 -0
  70. features/sector_map.py +53 -0
  71. features/technical_indicators.py +185 -0
  72. generate_backtest_report.py +311 -0
  73. generate_evaluation_report.py +712 -0
  74. inference/README.md +105 -0
  75. inference/__init__.py +36 -0
  76. inference/exported_model.py +929 -0
  77. inference/parallel_inference.py +160 -0
  78. inference/strategy_selector_inference.py +75 -0
  79. json_safety.py +130 -0
  80. portfolio/README.md +447 -0
  81. portfolio/__init__.py +41 -0
  82. portfolio/book_construction.py +786 -0
  83. portfolio/book_neutrality.py +178 -0
  84. portfolio/hrp_allocation.py +159 -0
  85. portfolio/options_arbitrage_detector.py +232 -0
  86. portfolio/options_assignment_risk.py +119 -0
  87. portfolio/options_margin_sizing.py +333 -0
  88. portfolio/options_strategy.py +1999 -0
  89. portfolio/rank_signal.py +219 -0
  90. portfolio/rolling_ic_gate.py +183 -0
  91. risk/README.md +275 -0
  92. risk/__init__.py +38 -0
  93. risk/asset_class_router.py +584 -0
  94. risk/forex_risk.py +158 -0
  95. risk/futures_risk.py +202 -0
  96. risk/kill_switch.py +261 -0
  97. risk/manual_override.py +118 -0
  98. risk/position_sizing.py +303 -0
  99. risk/rl_sizing.py +189 -0
features/__init__.py ADDED
@@ -0,0 +1,137 @@
1
+ from .alt_data_features import (
2
+ ALT_DATA_FEATURE_NAMES,
3
+ FINANCIAL_CONDITIONS_CHANGE_NEUTRAL,
4
+ IMPLIED_VOL_TERM_STRUCTURE_NEUTRAL,
5
+ IMPLIED_VOLATILITY_LEVEL_NEUTRAL,
6
+ financial_conditions_change,
7
+ implied_vol_term_structure,
8
+ implied_volatility_level,
9
+ )
10
+ from .bond_features import (
11
+ BOND_FEATURE_NAMES,
12
+ CREDIT_SPREAD_LEVEL_NEUTRAL,
13
+ YIELD_CURVE_CURVATURE_NEUTRAL,
14
+ YIELD_CURVE_LEVEL_NEUTRAL,
15
+ analytic_convexity,
16
+ analytic_modified_duration,
17
+ bond_dv01,
18
+ credit_spread_level,
19
+ empirical_duration_beta,
20
+ nearest_yield_curve_point,
21
+ yield_curve_curvature,
22
+ yield_curve_level,
23
+ yield_curve_slope as bond_yield_curve_slope,
24
+ )
25
+ from .cross_asset_sensitivity import (
26
+ CROSS_ASSET_SENSITIVITY_FEATURE_NAMES,
27
+ rolling_sensitivity,
28
+ sensitivity_interaction,
29
+ )
30
+ from .derivatives_macro_features import (
31
+ DERIVATIVES_MACRO_FEATURE_NAMES,
32
+ FUTURES_MACRO_FEATURE_NAMES,
33
+ FUTURES_TERM_STRUCTURE_SLOPE_NEUTRAL,
34
+ OPTIONS_IMPLIED_VOL_SKEW_NEUTRAL,
35
+ OPTIONS_MACRO_FEATURE_NAMES,
36
+ OPTIONS_PUT_CALL_RATIO_NEUTRAL,
37
+ futures_term_structure_slope,
38
+ options_implied_vol_skew,
39
+ options_put_call_ratio,
40
+ )
41
+ from .options_greeks import (
42
+ baw_american_price,
43
+ bs_price,
44
+ compute_greeks,
45
+ delta as option_delta,
46
+ gamma as option_gamma,
47
+ implied_volatility,
48
+ rho as option_rho,
49
+ theta as option_theta,
50
+ vega as option_vega,
51
+ )
52
+ from .macro_features import (
53
+ CREDIT_SPREAD_NEUTRAL,
54
+ CRYPTO_RISK_APPETITE_NEUTRAL,
55
+ YIELD_CURVE_SLOPE_NEUTRAL,
56
+ credit_spread_proxy,
57
+ crypto_risk_appetite_proxy,
58
+ yield_curve_slope_proxy,
59
+ )
60
+ from .sector_map import (
61
+ DEFAULT_SECTOR_MAPPING_PATH,
62
+ UNKNOWN_SECTOR_LABEL,
63
+ load_sector_mapping,
64
+ resolve_sector,
65
+ )
66
+ from .technical_indicators import (
67
+ CROSS_SECTIONAL_MOMENTUM_RANK_NEUTRAL,
68
+ average_true_range_pct,
69
+ bollinger_pctb,
70
+ cross_sectional_momentum_rank,
71
+ distance_from_52w_high,
72
+ macd_histogram_normalized,
73
+ relative_strength_index,
74
+ volume_zscore,
75
+ )
76
+
77
+ __all__ = [
78
+ "ALT_DATA_FEATURE_NAMES",
79
+ "BOND_FEATURE_NAMES",
80
+ "CREDIT_SPREAD_LEVEL_NEUTRAL",
81
+ "CREDIT_SPREAD_NEUTRAL",
82
+ "CROSS_ASSET_SENSITIVITY_FEATURE_NAMES",
83
+ "CROSS_SECTIONAL_MOMENTUM_RANK_NEUTRAL",
84
+ "CRYPTO_RISK_APPETITE_NEUTRAL",
85
+ "DEFAULT_SECTOR_MAPPING_PATH",
86
+ "DERIVATIVES_MACRO_FEATURE_NAMES",
87
+ "FINANCIAL_CONDITIONS_CHANGE_NEUTRAL",
88
+ "FUTURES_MACRO_FEATURE_NAMES",
89
+ "FUTURES_TERM_STRUCTURE_SLOPE_NEUTRAL",
90
+ "IMPLIED_VOL_TERM_STRUCTURE_NEUTRAL",
91
+ "IMPLIED_VOLATILITY_LEVEL_NEUTRAL",
92
+ "OPTIONS_IMPLIED_VOL_SKEW_NEUTRAL",
93
+ "OPTIONS_MACRO_FEATURE_NAMES",
94
+ "OPTIONS_PUT_CALL_RATIO_NEUTRAL",
95
+ "UNKNOWN_SECTOR_LABEL",
96
+ "YIELD_CURVE_CURVATURE_NEUTRAL",
97
+ "YIELD_CURVE_LEVEL_NEUTRAL",
98
+ "YIELD_CURVE_SLOPE_NEUTRAL",
99
+ "analytic_convexity",
100
+ "analytic_modified_duration",
101
+ "average_true_range_pct",
102
+ "baw_american_price",
103
+ "bollinger_pctb",
104
+ "bond_dv01",
105
+ "bond_yield_curve_slope",
106
+ "bs_price",
107
+ "compute_greeks",
108
+ "credit_spread_level",
109
+ "credit_spread_proxy",
110
+ "cross_sectional_momentum_rank",
111
+ "crypto_risk_appetite_proxy",
112
+ "distance_from_52w_high",
113
+ "empirical_duration_beta",
114
+ "financial_conditions_change",
115
+ "futures_term_structure_slope",
116
+ "implied_vol_term_structure",
117
+ "implied_volatility",
118
+ "implied_volatility_level",
119
+ "load_sector_mapping",
120
+ "macd_histogram_normalized",
121
+ "nearest_yield_curve_point",
122
+ "option_delta",
123
+ "option_gamma",
124
+ "option_rho",
125
+ "option_theta",
126
+ "option_vega",
127
+ "options_implied_vol_skew",
128
+ "options_put_call_ratio",
129
+ "relative_strength_index",
130
+ "resolve_sector",
131
+ "rolling_sensitivity",
132
+ "sensitivity_interaction",
133
+ "volume_zscore",
134
+ "yield_curve_curvature",
135
+ "yield_curve_level",
136
+ "yield_curve_slope_proxy",
137
+ ]
@@ -0,0 +1,154 @@
1
+ """Shared, pure alternative-data feature implementations (development/
2
+ Problems.md #71, Phase 4.12) - additive sibling to features/bond_features.py
3
+ and features/macro_features.py, backed by data_pipeline/fred_backfill.py's
4
+ implied-volatility and financial-conditions series rather than price data.
5
+
6
+ Broadcast identically to EVERY asset's model input (date-only, not
7
+ asset-specific) - same "compute once per date/bar, every asset sees it"
8
+ shape as bond_features.py/macro_features.py.
9
+
10
+ Imported by BOTH train.py (offline, per-date FRED-series lookup via
11
+ data_pipeline.fred_backfill.series_value_asof()/series_change_asof()) and
12
+ main.py (runtime, once per bar) for parity by construction - same
13
+ convention as features/bond_features.py.
14
+
15
+ Every function neutral-defaults to 0.0 on missing input - never raises.
16
+ The 0.0 default is chosen so it means something PLAUSIBLE, not merely
17
+ "absent": implied_volatility_level() is a log-ratio to a reference VIX
18
+ level (see VIX_REFERENCE_LEVEL below), so its neutral default corresponds
19
+ to "VIX = 20," an unremarkable reading, not "VIX = 0," which cannot
20
+ happen. This mirrors every other neutral-default in this codebase
21
+ (bond_features.py, macro_features.py) using 0.0 to mean "no signal."
22
+
23
+ These three features were chosen from a wider candidate list (VIX, VXV,
24
+ NFCI, STLFSI4, UMCSENT, TEDRATE, SOFR, BAMLH0A0HYM2, DTWEXBGS) after
25
+ live-verifying each one's real 2014-2021 coverage and checking pairwise
26
+ collinearity - see data_pipeline/fred_backfill.py's
27
+ DEFAULT_ALT_DATA_REFERENCE_SERIES docstring for the rejected candidates
28
+ and why. The surviving three are deliberately NOT redundant with each
29
+ other or with the existing 8 bond_* features: implied_volatility_level
30
+ alone cannot distinguish the 2019-12-27..2020-03-25 COVID-crash era (mean
31
+ VIX 28.54) from the immediately-following recovery era (mean VIX 29.24) -
32
+ term structure and financial-conditions CHANGE can (measured: term
33
+ structure mean +0.013 during the crash vs +0.105 after; NFCI 4-week
34
+ change mean +0.154/max +0.608 during the crash vs a total 2014-2019 range
35
+ of [-0.091, +0.091]).
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import math
41
+
42
+ IMPLIED_VOLATILITY_LEVEL_NEUTRAL = 0.0
43
+ IMPLIED_VOL_TERM_STRUCTURE_NEUTRAL = 0.0
44
+ FINANCIAL_CONDITIONS_CHANGE_NEUTRAL = 0.0
45
+ REAL_RATE_CHANGE_NEUTRAL = 0.0
46
+ BREAKEVEN_INFLATION_CHANGE_NEUTRAL = 0.0
47
+ DOLLAR_INDEX_CHANGE_NEUTRAL = 0.0
48
+
49
+ # Divisor for implied_volatility_level()'s log-ratio - an unremarkable,
50
+ # roughly-median VIX reading (measured p50 15.1 over the 2014-12..2021-03
51
+ # common_window), chosen so the neutral default (log(1.0) = 0.0) maps back
52
+ # to a plausible market state rather than an impossible one (a raw VIX
53
+ # feature's 0.0 default would mean "VIX = 0", which never happens).
54
+ VIX_REFERENCE_LEVEL = 20.0
55
+
56
+ ALT_DATA_FEATURE_NAMES = [
57
+ "alt_implied_volatility_level",
58
+ "alt_implied_vol_term_structure",
59
+ "alt_financial_conditions_change",
60
+ ]
61
+
62
+
63
+ def implied_volatility_level(vix_close: float | None) -> float:
64
+ """log(VIX / VIX_REFERENCE_LEVEL). VIX is right-skewed (measured p50
65
+ 15.1 / p90 26.7 / p99 53.9 over the common_window) - a raw level would
66
+ let extreme prints (82.69 in March 2020) dominate the feature scaler's
67
+ clip/winsorize range; the log compresses that skew the same way
68
+ close_to_close_return features already use log-scale-friendly
69
+ transforms elsewhere in this codebase. Returns
70
+ IMPLIED_VOLATILITY_LEVEL_NEUTRAL (0.0) when vix_close is missing or
71
+ non-positive (a non-positive VIX reading is not physically meaningful
72
+ - never log() a non-positive number)."""
73
+ if vix_close is None or vix_close <= 0.0:
74
+ return IMPLIED_VOLATILITY_LEVEL_NEUTRAL
75
+ return math.log(float(vix_close) / VIX_REFERENCE_LEVEL)
76
+
77
+
78
+ def implied_vol_term_structure(vix_close: float | None, vix_3m_close: float | None) -> float:
79
+ """(VIX 3-month - VIX) / VIX - positive means contango (the market is
80
+ calm: longer-dated implied vol priced higher than spot), negative
81
+ means backwardation (stress RIGHT NOW: spot implied vol priced above
82
+ the 3-month tenor, since a acute near-term shock is expected to fade).
83
+ Normalized by the spot VIX level so the ratio is comparable across
84
+ vol regimes - the whole reason this feature exists alongside
85
+ implied_volatility_level() rather than instead of it: VIX level alone
86
+ cannot separate the COVID-crash era from the immediately-following
87
+ recovery era (both ~28-29 mean VIX), while term structure can (+0.013
88
+ during the crash vs +0.105 after, with a -0.256 backwardation minimum
89
+ during the crash). Returns IMPLIED_VOL_TERM_STRUCTURE_NEUTRAL (0.0)
90
+ when either leg is missing or vix_close is non-positive."""
91
+ if vix_close is None or vix_3m_close is None or vix_close <= 0.0:
92
+ return IMPLIED_VOL_TERM_STRUCTURE_NEUTRAL
93
+ return (float(vix_3m_close) - float(vix_close)) / float(vix_close)
94
+
95
+
96
+ def financial_conditions_change(nfci_now: float | None, nfci_prior: float | None) -> float:
97
+ """NFCI(t) - NFCI(t - 4 weeks) - the Chicago Fed National Financial
98
+ Conditions Index's own 4-week CHANGE, not its level. The level is
99
+ deliberately not a feature here: measured correlation of rho=0.73
100
+ against the existing bond_credit_spread_level feature (BAA10Y), i.e.
101
+ the model already has substantially the same information via that
102
+ channel. The 4-week CHANGE is a genuinely different signal (rho=0.23
103
+ against bond_credit_spread_level) - a near-binary "conditions are
104
+ tightening violently right now" indicator: measured mean +0.154 (max
105
+ +0.608) during the 2019-12-27..2020-03-25 COVID-crash era, versus a
106
+ total 2014-12..2019-12 pre-COVID range of only [-0.091, +0.091] - the
107
+ crash reading sits roughly 6.7x outside five years of prior variation.
108
+ Returns FINANCIAL_CONDITIONS_CHANGE_NEUTRAL (0.0) when either endpoint
109
+ is missing (e.g. fewer than 4 weeks of history available yet, or the
110
+ series cache is absent)."""
111
+ if nfci_now is None or nfci_prior is None:
112
+ return FINANCIAL_CONDITIONS_CHANGE_NEUTRAL
113
+ return float(nfci_now) - float(nfci_prior)
114
+
115
+
116
+ def real_rate_change(real_rate_now: float | None, real_rate_prior: float | None) -> float:
117
+ """DFII10(t) - DFII10(t - N) - the 10-year TIPS (real) yield's own
118
+ CHANGE, not its level. V5.1 Phase 2 (item 8 / F2, development/
119
+ Problems.md): a stationary driver for
120
+ features/cross_asset_sensitivity.py's per-asset real-rate sensitivity
121
+ beta - a level would be non-stationary and comparing betas fit against
122
+ a trending regressor across different market eras is not meaningful,
123
+ the same reasoning financial_conditions_change() above already
124
+ documents for NFCI. Returns REAL_RATE_CHANGE_NEUTRAL (0.0) when either
125
+ endpoint is missing."""
126
+ if real_rate_now is None or real_rate_prior is None:
127
+ return REAL_RATE_CHANGE_NEUTRAL
128
+ return float(real_rate_now) - float(real_rate_prior)
129
+
130
+
131
+ def breakeven_inflation_change(breakeven_now: float | None, breakeven_prior: float | None) -> float:
132
+ """T10YIE(t) - T10YIE(t - N) - the 10-year breakeven inflation rate's
133
+ own CHANGE. Same stationarity reasoning as real_rate_change() above -
134
+ a driver for features/cross_asset_sensitivity.py, not a broadcast
135
+ level feature. Returns BREAKEVEN_INFLATION_CHANGE_NEUTRAL (0.0) when
136
+ either endpoint is missing."""
137
+ if breakeven_now is None or breakeven_prior is None:
138
+ return BREAKEVEN_INFLATION_CHANGE_NEUTRAL
139
+ return float(breakeven_now) - float(breakeven_prior)
140
+
141
+
142
+ def dollar_index_change(dollar_index_now: float | None, dollar_index_prior: float | None) -> float:
143
+ """DTWEXBGS(t) - DTWEXBGS(t - N) - the trade-weighted dollar index's
144
+ own CHANGE. Same stationarity reasoning as real_rate_change() above -
145
+ a driver for features/cross_asset_sensitivity.py, not a broadcast
146
+ level feature (see data_pipeline/fred_backfill.py's
147
+ DEFAULT_ALT_DATA_REFERENCE_SERIES docstring for why DTWEXBGS is being
148
+ re-added as a sensitivity driver rather than a level, having
149
+ previously been evaluated and left out of the original alt-data
150
+ trio). Returns DOLLAR_INDEX_CHANGE_NEUTRAL (0.0) when either endpoint
151
+ is missing."""
152
+ if dollar_index_now is None or dollar_index_prior is None:
153
+ return DOLLAR_INDEX_CHANGE_NEUTRAL
154
+ return float(dollar_index_now) - float(dollar_index_prior)
@@ -0,0 +1,275 @@
1
+ """Shared, pure real-yield-curve/credit-spread/duration feature
2
+ implementations - additive sibling to features/macro_features.py, using
3
+ data_pipeline/fred_backfill.py's actual FRED Treasury-yield/credit-spread
4
+ series rather than macro_features.py's bond-ETF-price-momentum proxies.
5
+
6
+ Broadcast identically to EVERY asset's model input (not just bond-tagged
7
+ assets) - same "compute once per date/bar, every asset sees it" shape as
8
+ macro_features.py, and the concrete mechanism by which bond/yield-curve
9
+ signal becomes usable for equity/crypto/future/option predictions too, not
10
+ just bonds themselves.
11
+
12
+ Imported by BOTH train.py (offline, per-date FRED-series lookup) and
13
+ main.py (runtime, once per bar) for parity by construction - same
14
+ convention as features/technical_indicators.py and features/macro_features.py.
15
+
16
+ Every function neutral-defaults to 0.0 on missing input - never raises -
17
+ except empirical_duration_beta(), which returns None (not 0.0) below its
18
+ minimum-observation floor: an unknown duration sensitivity must never be
19
+ indistinguishable from a genuinely-zero one, the same "None = absent, 0.0 =
20
+ neutral" distinction risk/position_sizing.py::rank_sizing_multiplier()
21
+ already draws for missing rank predictions.
22
+
23
+ V4.6 (development/Problems.md, Roadmap "Assets" - "single-bond trading")
24
+ added analytic_modified_duration()/analytic_convexity()/bond_dv01(), a
25
+ real closed-form bond-math sibling to empirical_duration_beta() above -
26
+ NOT a replacement for it. Individual-bond trading was confirmed
27
+ INFEASIBLE under this Lean version's security model (no SecurityType.Bond,
28
+ no AddBond, no Common/Securities/Bond/ implementation anywhere in the real
29
+ Lean source tree - every "bond" Lean supports is an equity-typed ETF
30
+ wrapper, exactly what this codebase already does) - these 3 functions
31
+ instead deepen the EXISTING bond-ETF sleeve's analytics, using
32
+ bond_metadata.duration_proxy_years (already configured per-ETF in
33
+ config.json) as an assumed maturity and bond_metadata.assumed_coupon_rate
34
+ (or, when absent, the treasury yield-curve point nearest that maturity, as
35
+ an at-par-pricing proxy coupon) as an assumed coupon - explicitly an
36
+ ETF-level APPROXIMATION, not real per-bond cash-flow data.
37
+
38
+ V4.7 merged these 3 functions' output into base_features/BOND_FEATURE_NAMES
39
+ as real model inputs (main.py::_build_model_input_impl(),
40
+ train.py::build_bond_features_by_date()) - code-complete, but deploying it
41
+ live requires running a retrain first: config.json's
42
+ phase1.features.input_set now lists the 3 new names, and main.py's
43
+ scaling loop indexes base_features[name] with no default, so an
44
+ un-retrained ml/feature_schema.json (still listing only the original 34
45
+ names) would KeyError on the very next bar. Until that retrain runs,
46
+ main.py's own latest_bond_analytics_by_symbol side-table remains the only
47
+ place these values are actually visible (dashboard/state), same as before.
48
+ """
49
+
50
+ from __future__ import annotations
51
+
52
+ YIELD_CURVE_LEVEL_NEUTRAL = 0.0
53
+ YIELD_CURVE_SLOPE_NEUTRAL = 0.0
54
+ YIELD_CURVE_CURVATURE_NEUTRAL = 0.0
55
+ CREDIT_SPREAD_LEVEL_NEUTRAL = 0.0
56
+
57
+ BOND_FEATURE_NAMES = [
58
+ "bond_yield_curve_level",
59
+ "bond_yield_curve_slope",
60
+ "bond_yield_curve_curvature",
61
+ "bond_credit_spread_level",
62
+ "bond_empirical_duration_beta",
63
+ "bond_analytic_modified_duration",
64
+ "bond_analytic_convexity",
65
+ "bond_dv01",
66
+ ]
67
+
68
+
69
+ def yield_curve_level(treasury_10yr_yield: float | None) -> float:
70
+ """Raw level of the 10yr Treasury yield (e.g. 0.045 for 4.5%). Returns
71
+ YIELD_CURVE_LEVEL_NEUTRAL (0.0) when unavailable."""
72
+ if treasury_10yr_yield is None:
73
+ return YIELD_CURVE_LEVEL_NEUTRAL
74
+ return float(treasury_10yr_yield)
75
+
76
+
77
+ def yield_curve_slope(treasury_10yr_yield: float | None, treasury_3mo_yield: float | None) -> float:
78
+ """10yr - 3mo - the standard recession-signal curve slope (negative =
79
+ inverted curve). Real-data replacement for
80
+ macro_features.yield_curve_slope_proxy()'s price-momentum proxy.
81
+ Returns YIELD_CURVE_SLOPE_NEUTRAL (0.0) when either input is missing."""
82
+ if treasury_10yr_yield is None or treasury_3mo_yield is None:
83
+ return YIELD_CURVE_SLOPE_NEUTRAL
84
+ return float(treasury_10yr_yield) - float(treasury_3mo_yield)
85
+
86
+
87
+ def yield_curve_curvature(
88
+ treasury_2yr_yield: float | None,
89
+ treasury_5yr_yield: float | None,
90
+ treasury_10yr_yield: float | None,
91
+ ) -> float:
92
+ """2*5yr - 2yr - 10yr - the standard butterfly/curvature measure of
93
+ curve shape independent of its overall level or slope. Returns
94
+ YIELD_CURVE_CURVATURE_NEUTRAL (0.0) when any input is missing."""
95
+ if treasury_2yr_yield is None or treasury_5yr_yield is None or treasury_10yr_yield is None:
96
+ return YIELD_CURVE_CURVATURE_NEUTRAL
97
+ return 2.0 * float(treasury_5yr_yield) - float(treasury_2yr_yield) - float(treasury_10yr_yield)
98
+
99
+
100
+ def credit_spread_level(baa10y_spread: float | None) -> float:
101
+ """Raw BAA10Y (Moody's Baa corporate bond yield relative to the 10yr
102
+ Treasury) level - widens when credit risk appetite deteriorates.
103
+ Real-data replacement for macro_features.credit_spread_proxy()'s
104
+ momentum-based proxy. Returns CREDIT_SPREAD_LEVEL_NEUTRAL (0.0) when
105
+ unavailable."""
106
+ if baa10y_spread is None:
107
+ return CREDIT_SPREAD_LEVEL_NEUTRAL
108
+ return float(baa10y_spread)
109
+
110
+
111
+ def empirical_duration_beta(
112
+ etf_daily_returns: list[float | None],
113
+ delta_10yr_yield_daily: list[float | None],
114
+ min_observations: int = 60,
115
+ ) -> float | None:
116
+ """Rolling OLS slope of ETF daily returns regressed on same-day
117
+ Delta-10yr-yield: an empirical "how much does this ETF move per unit of
118
+ yield change" duration proxy, since these are ETFs with no single
119
+ stated maturity, not individual bonds with a textbook duration figure.
120
+
121
+ Pairs the two series positionally (index i of each list must already
122
+ correspond to the same trading date - callers are responsible for
123
+ alignment), drops any pair where either side is None, and requires at
124
+ least min_observations valid pairs. Returns None (not 0.0) below that
125
+ floor - a missing/insufficient-history duration estimate must be
126
+ distinguishable from "genuinely zero duration sensitivity"."""
127
+ paired = [
128
+ (x, y)
129
+ for x, y in zip(etf_daily_returns, delta_10yr_yield_daily)
130
+ if x is not None and y is not None
131
+ ]
132
+ if len(paired) < min_observations:
133
+ return None
134
+
135
+ xs = [float(pair[1]) for pair in paired]
136
+ ys = [float(pair[0]) for pair in paired]
137
+ mean_x = sum(xs) / len(xs)
138
+ mean_y = sum(ys) / len(ys)
139
+ covariance = sum((x - mean_x) * (y - mean_y) for x, y in zip(xs, ys))
140
+ variance_x = sum((x - mean_x) ** 2 for x in xs)
141
+ # An exact `== 0.0` check is not robust here: for a mathematically
142
+ # constant xs, whether mean_x/variance_x round to exactly 0.0 depends on
143
+ # sum()'s internal summation algorithm, which CPython changed (naive
144
+ # left-to-right -> compensated) between 3.11 and 3.12 - the same input
145
+ # can produce a true 0.0 on one Python version and a tiny rounding-noise
146
+ # variance (~1e-20 scale, versus real delta-yield variance on the order
147
+ # of 1e-4 to 1e-8) on another, silently producing a wild
148
+ # covariance/near-zero-noise ratio instead of the intended None. See
149
+ # development/Problems.md#10.
150
+ if variance_x < 1e-12:
151
+ return None
152
+ return covariance / variance_x
153
+
154
+
155
+ def _bond_cash_flows(coupon_rate: float, years_to_maturity: float, payments_per_year: int, face_value: float = 100.0) -> list[float]:
156
+ """Coupon + principal cash-flow schedule for a plain-vanilla,
157
+ fixed-coupon bond priced at face_value=100 (a standard convention -
158
+ duration/convexity are scale-invariant per-unit-price ratios, so the
159
+ actual face value used doesn't affect the resulting figures). One
160
+ entry per period; the LAST entry additionally includes the face_value
161
+ repayment. Returns [] for a non-positive period count."""
162
+ num_periods = max(int(round(years_to_maturity * payments_per_year)), 0)
163
+ if num_periods == 0:
164
+ return []
165
+ coupon_per_period = coupon_rate / payments_per_year * face_value
166
+ cash_flows = [coupon_per_period] * num_periods
167
+ cash_flows[-1] += face_value
168
+ return cash_flows
169
+
170
+
171
+ def analytic_modified_duration(
172
+ yield_to_maturity: float | None,
173
+ coupon_rate: float | None,
174
+ years_to_maturity: float | None,
175
+ payments_per_year: int = 2,
176
+ ) -> float | None:
177
+ """Standard closed-form modified duration from a plain-vanilla,
178
+ fixed-coupon cash-flow schedule: Macaulay duration (the price-weighted
179
+ average time to each cash flow, in years) divided by
180
+ (1 + yield_to_maturity/payments_per_year).
181
+
182
+ A genuinely DIFFERENT estimate from empirical_duration_beta() above,
183
+ not a replacement for it: empirical_duration_beta() measures how much
184
+ an ETF's OWN observed price actually moved per unit of yield change (a
185
+ market-realized sensitivity); this function computes what a
186
+ HYPOTHETICAL plain-vanilla bond with the given yield/coupon/maturity
187
+ would theoretically be sensitive to, from cash-flow math alone - see
188
+ this module's own docstring for the ETF-level-approximation caveat on
189
+ what yield_to_maturity/coupon_rate/years_to_maturity should be sourced
190
+ from in practice.
191
+
192
+ Returns None (never raises) when any input is missing, years_to_maturity
193
+ is non-positive, or the computed price is non-positive (a degenerate/
194
+ extreme-negative-yield edge case) - an unknown/undefined duration must
195
+ never be indistinguishable from a genuinely-zero one, the same
196
+ convention empirical_duration_beta() already established."""
197
+ if yield_to_maturity is None or coupon_rate is None or years_to_maturity is None or years_to_maturity <= 0:
198
+ return None
199
+ period_yield = float(yield_to_maturity) / payments_per_year
200
+ if period_yield <= -1.0:
201
+ return None
202
+ cash_flows = _bond_cash_flows(float(coupon_rate), float(years_to_maturity), payments_per_year)
203
+ if not cash_flows:
204
+ return None
205
+
206
+ discounted = [cf / (1.0 + period_yield) ** (t + 1) for t, cf in enumerate(cash_flows)]
207
+ price = sum(discounted)
208
+ if price <= 0.0:
209
+ return None
210
+ weighted_time = sum(((t + 1) / payments_per_year) * pv for t, pv in enumerate(discounted))
211
+ macaulay_duration = weighted_time / price
212
+ return macaulay_duration / (1.0 + period_yield)
213
+
214
+
215
+ def analytic_convexity(
216
+ yield_to_maturity: float | None,
217
+ coupon_rate: float | None,
218
+ years_to_maturity: float | None,
219
+ payments_per_year: int = 2,
220
+ ) -> float | None:
221
+ """Standard closed-form convexity (the second derivative of bond price
222
+ with respect to yield, annualized) from the same plain-vanilla
223
+ cash-flow schedule analytic_modified_duration() uses - same
224
+ ETF-level-approximation caveat and degrade-to-None convention."""
225
+ if yield_to_maturity is None or coupon_rate is None or years_to_maturity is None or years_to_maturity <= 0:
226
+ return None
227
+ period_yield = float(yield_to_maturity) / payments_per_year
228
+ if period_yield <= -1.0:
229
+ return None
230
+ cash_flows = _bond_cash_flows(float(coupon_rate), float(years_to_maturity), payments_per_year)
231
+ if not cash_flows:
232
+ return None
233
+
234
+ discounted = [cf / (1.0 + period_yield) ** (t + 1) for t, cf in enumerate(cash_flows)]
235
+ price = sum(discounted)
236
+ if price <= 0.0:
237
+ return None
238
+ convexity_sum = sum(
239
+ (t + 1) * (t + 2) * pv / (1.0 + period_yield) ** 2 for t, pv in enumerate(discounted)
240
+ )
241
+ return convexity_sum / (price * payments_per_year ** 2)
242
+
243
+
244
+ def bond_dv01(price: float | None, modified_duration: float | None, notional: float = 1.0) -> float | None:
245
+ """Dollar duration per 1bp yield move (DV01):
246
+ price * modified_duration * 0.0001 * notional - a directly useful
247
+ risk-sizing number (expected dollar P&L for a 1 basis point yield
248
+ change). Returns None (never raises) when price/modified_duration is
249
+ missing, or price/notional is non-positive."""
250
+ if price is None or modified_duration is None or price <= 0.0 or notional <= 0.0:
251
+ return None
252
+ return float(price) * float(modified_duration) * 0.0001 * float(notional)
253
+
254
+
255
+ def nearest_yield_curve_point(years_to_maturity: float | None, curve_points: dict) -> float | None:
256
+ """Picks the yield-curve observation whose maturity (in years, the
257
+ dict's keys) is CLOSEST to years_to_maturity - the at-par-pricing
258
+ proxy coupon/yield fallback analytic_modified_duration()/
259
+ analytic_convexity() need when bond_metadata.assumed_coupon_rate isn't
260
+ explicitly configured for an ETF. A coarse approximation by
261
+ construction (this codebase's own Treasury curve only has 4 points -
262
+ 3mo/2yr/5yr/10yr - so a long-duration ETF's true maturity is
263
+ necessarily approximated by the longest available point); callers
264
+ already document that limitation, this function just picks the
265
+ nearest one rather than guessing further. Skips any None-valued point
266
+ (a FRED series with no observation as-of the current date) - returns
267
+ None (never raises) when no curve point has a value at all, or
268
+ years_to_maturity is None."""
269
+ if years_to_maturity is None:
270
+ return None
271
+ usable_points = {maturity: yield_value for maturity, yield_value in curve_points.items() if yield_value is not None}
272
+ if not usable_points:
273
+ return None
274
+ nearest_maturity = min(usable_points, key=lambda maturity: abs(maturity - years_to_maturity))
275
+ return usable_points[nearest_maturity]
@@ -0,0 +1,79 @@
1
+ """Per-asset macro sensitivities (V5.1 Phase 2, item 8 / F2 of the plan) -
2
+ additive sibling to features/bond_features.py, features/macro_features.py,
3
+ features/alt_data_features.py.
4
+
5
+ F2's finding: every existing macro_*/bond_*/alt_* feature (except
6
+ bond_empirical_duration_beta) is written IDENTICALLY to every asset's row
7
+ on a date - a per-date constant that shifts every asset's score equally,
8
+ which a cross-sectional ranker is by construction invariant to (Spearman/
9
+ ListNet ignore any transform that preserves relative order within a date).
10
+ "Add more macro features," taken literally, is near-worthless for a rank
11
+ model. The version that works is a per-asset SENSITIVITY: how much does
12
+ THIS asset's return move per unit of macro change - a quantity that varies
13
+ across the cross-section, plus the interaction term (sensitivity x today's
14
+ macro move), which is the closest thing to a forward-looking macro-driven
15
+ return prediction that stays scale-consistent asset to asset.
16
+
17
+ Imported by BOTH train.py (offline, per-date/per-asset rolling regression
18
+ over the full history) and main.py (runtime, once per symbol per bar using
19
+ the same rolling-deque pattern main.py's
20
+ _bond_empirical_duration_beta_for_symbol() already established) for
21
+ train/inference parity by construction - same convention as every other
22
+ features/ module. main.py cannot import train.py (torch), which is the
23
+ whole reason this module lives here rather than in train.py alongside
24
+ build_cross_asset_sensitivity_features().
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ from features.bond_features import empirical_duration_beta
30
+
31
+ CROSS_ASSET_SENSITIVITY_FEATURE_NAMES = [
32
+ "sens_vix_beta",
33
+ "sens_vix_interaction",
34
+ "sens_real_rate_beta",
35
+ "sens_real_rate_interaction",
36
+ "sens_credit_beta",
37
+ "sens_credit_interaction",
38
+ "sens_dollar_beta",
39
+ "sens_dollar_interaction",
40
+ ]
41
+
42
+
43
+ def rolling_sensitivity(
44
+ asset_returns: list[float | None],
45
+ macro_changes: list[float | None],
46
+ *,
47
+ lookback: int,
48
+ min_observations: int,
49
+ ) -> float | None:
50
+ """Trailing-window OLS slope of asset_returns on macro_changes,
51
+ restricted to the last `lookback` positions of each series (positionally
52
+ paired - callers are responsible for date alignment, the same contract
53
+ empirical_duration_beta() itself already documents). A thin wrapper -
54
+ REUSES empirical_duration_beta() rather than reimplementing OLS, exactly
55
+ as bond_empirical_duration_beta already does for a whole-history
56
+ (non-rolling) beta.
57
+
58
+ lookback <= 0 disables windowing (uses the full series - a whole-history
59
+ beta, matching bond_empirical_duration_beta's convention). Returns None
60
+ (not 0.0) when fewer than min_observations valid pairs exist in the
61
+ window - a missing/insufficient-history sensitivity must stay
62
+ distinguishable from a genuinely-zero one, same as
63
+ empirical_duration_beta()'s own return contract."""
64
+ if lookback > 0:
65
+ asset_returns = asset_returns[-lookback:]
66
+ macro_changes = macro_changes[-lookback:]
67
+ return empirical_duration_beta(asset_returns, macro_changes, min_observations=min_observations)
68
+
69
+
70
+ def sensitivity_interaction(sensitivity: float | None, macro_change: float | None) -> float:
71
+ """beta_i * delta_macro_t - the interaction term that turns a per-date
72
+ constant macro move into a per-asset-varying signal a cross-sectional
73
+ ranker can actually use. Neutral 0.0 on any missing input, the same
74
+ "None input -> 0.0 neutral output" convention every features/*.py
75
+ interaction/composite function already follows (e.g.
76
+ features/alt_data_features.py's own neutral defaults)."""
77
+ if sensitivity is None or macro_change is None:
78
+ return 0.0
79
+ return float(sensitivity) * float(macro_change)