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.
- aether_quant-0.0.1.dist-info/METADATA +7 -0
- aether_quant-0.0.1.dist-info/RECORD +99 -0
- aether_quant-0.0.1.dist-info/WHEEL +5 -0
- aether_quant-0.0.1.dist-info/entry_points.txt +2 -0
- aether_quant-0.0.1.dist-info/licenses/LICENSE +133 -0
- aether_quant-0.0.1.dist-info/top_level.txt +13 -0
- analyzer/README.md +69 -0
- analyzer/__init__.py +17 -0
- analyzer/market_analyzer.py +521 -0
- aq_cli.py +3633 -0
- audit/README.md +47 -0
- audit/__init__.py +38 -0
- audit/hash_chain.py +52 -0
- audit/postgres_audit.py +61 -0
- audit/postgres_worker.py +402 -0
- audit/redis_queue.py +124 -0
- audit/status_export.py +62 -0
- data_pipeline/README.md +257 -0
- data_pipeline/__init__.py +6 -0
- data_pipeline/bar_synthesis.py +97 -0
- data_pipeline/dividend_backfill.py +243 -0
- data_pipeline/factor_file_backfill.py +276 -0
- data_pipeline/fetch.py +206 -0
- data_pipeline/fred_backfill.py +435 -0
- data_pipeline/ib_backfill.py +287 -0
- data_pipeline/v2_manifest.py +75 -0
- data_pipeline/yfinance_backfill.py +501 -0
- evaluation/README.md +152 -0
- evaluation/__init__.py +73 -0
- evaluation/ablation.py +310 -0
- evaluation/benchmark_comparison.py +195 -0
- evaluation/confidence_threshold_calibration.py +157 -0
- evaluation/factor_neutralization_check.py +94 -0
- evaluation/feature_parity.py +321 -0
- evaluation/feature_reconciliation.py +286 -0
- evaluation/impact_model.py +77 -0
- evaluation/kill_switch_replay.py +186 -0
- evaluation/limit_fill_simulator.py +152 -0
- evaluation/model_predictions.py +192 -0
- evaluation/monte_carlo.py +161 -0
- evaluation/public_benchmarks.py +117 -0
- evaluation/rank_book_simulator.py +494 -0
- evaluation/rank_ic_core.py +204 -0
- evaluation/rank_signal_calibration.py +690 -0
- evaluation/rolling_ic_gate_calibration.py +190 -0
- evaluation/rolling_ic_gate_replay.py +138 -0
- execution/README.md +281 -0
- execution/__init__.py +80 -0
- execution/config_cache.py +48 -0
- execution/cost_model.py +213 -0
- execution/lean_config_render.py +131 -0
- execution/live_credentials.py +38 -0
- execution/live_credentials_io.py +52 -0
- execution/order_gate.py +388 -0
- execution/paper_readiness.py +181 -0
- execution/paper_readiness_io.py +54 -0
- execution/paper_readiness_report.py +103 -0
- execution/paper_readiness_scheduler.py +137 -0
- execution/reconciliation.py +160 -0
- execution/runtime_config_io.py +38 -0
- execution/secret_scan.py +73 -0
- features/README.md +77 -0
- features/__init__.py +137 -0
- features/alt_data_features.py +154 -0
- features/bond_features.py +275 -0
- features/cross_asset_sensitivity.py +79 -0
- features/derivatives_macro_features.py +72 -0
- features/macro_features.py +79 -0
- features/options_greeks.py +329 -0
- features/sector_map.py +53 -0
- features/technical_indicators.py +185 -0
- generate_backtest_report.py +311 -0
- generate_evaluation_report.py +712 -0
- inference/README.md +105 -0
- inference/__init__.py +36 -0
- inference/exported_model.py +929 -0
- inference/parallel_inference.py +160 -0
- inference/strategy_selector_inference.py +75 -0
- json_safety.py +130 -0
- portfolio/README.md +447 -0
- portfolio/__init__.py +41 -0
- portfolio/book_construction.py +786 -0
- portfolio/book_neutrality.py +178 -0
- portfolio/hrp_allocation.py +159 -0
- portfolio/options_arbitrage_detector.py +232 -0
- portfolio/options_assignment_risk.py +119 -0
- portfolio/options_margin_sizing.py +333 -0
- portfolio/options_strategy.py +1999 -0
- portfolio/rank_signal.py +219 -0
- portfolio/rolling_ic_gate.py +183 -0
- risk/README.md +275 -0
- risk/__init__.py +38 -0
- risk/asset_class_router.py +584 -0
- risk/forex_risk.py +158 -0
- risk/futures_risk.py +202 -0
- risk/kill_switch.py +261 -0
- risk/manual_override.py +118 -0
- risk/position_sizing.py +303 -0
- 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)
|