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
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
"""Central market analyzer for Aether Quant V2: combines expert, regime,
|
|
2
|
+
topology, liquidity and risk-engine outputs into one explainable per-asset
|
|
3
|
+
action."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import math
|
|
8
|
+
from dataclasses import asdict, dataclass, field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
ACTIONS = ("observe", "simulate", "trade", "reduce_risk", "retrain_candidate")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class MarketAnalysisDecision:
|
|
16
|
+
action: str
|
|
17
|
+
signal: str
|
|
18
|
+
target_weight: float
|
|
19
|
+
confidence: float
|
|
20
|
+
probability_up: float
|
|
21
|
+
trading_eligible: bool
|
|
22
|
+
topology_considered: bool
|
|
23
|
+
liquidity_considered: bool
|
|
24
|
+
reasons: list[str]
|
|
25
|
+
signal_quality_score: float = 0.0
|
|
26
|
+
signal_quality_breakdown: dict = field(default_factory=dict)
|
|
27
|
+
predicted_return_magnitude: float | None = None
|
|
28
|
+
predicted_volatility: float | None = None
|
|
29
|
+
# V5.1 Phase 1 (development/Problems.md, item 3 - the actual root cause
|
|
30
|
+
# of the fee drag) - execution/cost_model.py::NetEdgeDecision.to_dict(),
|
|
31
|
+
# or None when the cost gate is disabled/uncalibrated/no rank
|
|
32
|
+
# prediction this bar. Defaulted so every existing positional
|
|
33
|
+
# construction and to_dict() consumer is unaffected.
|
|
34
|
+
net_edge: dict | None = None
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> dict:
|
|
37
|
+
return asdict(self)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _clamp01(value: float) -> float:
|
|
41
|
+
# V5.4.7 (development/Problems.md #121): min(1.0, nan) evaluates False
|
|
42
|
+
# and returns 1.0, so a NaN confidence component became a PERFECT
|
|
43
|
+
# quality score and sailed through the trade gate. Non-finite now
|
|
44
|
+
# degrades to 0.0 (no confidence), matching moe/gating.py's clamp.
|
|
45
|
+
if not math.isfinite(value):
|
|
46
|
+
return 0.0
|
|
47
|
+
return max(0.0, min(1.0, value))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def compute_signal_quality_score(
|
|
51
|
+
confidence: float,
|
|
52
|
+
regime_confidence: float,
|
|
53
|
+
topology: dict,
|
|
54
|
+
liquidity: dict,
|
|
55
|
+
) -> tuple[float, dict]:
|
|
56
|
+
"""Bounded [0,1] composite of raw model confidence, regime confidence,
|
|
57
|
+
topology peer-support and liquidity friction. Always computed and
|
|
58
|
+
exposed (signal_quality_score/signal_quality_breakdown) for dashboard
|
|
59
|
+
visibility; only influences routing in build_market_analysis_decision()
|
|
60
|
+
when use_composite_signal_score=True - see analyzer/README.md. Small
|
|
61
|
+
hand-tuned weights, mirroring moe/gating.py's
|
|
62
|
+
_quality_multiplier/_performance_score style - not a trained model."""
|
|
63
|
+
confidence_component = _clamp01(float(confidence))
|
|
64
|
+
regime_component = _clamp01(float(regime_confidence))
|
|
65
|
+
|
|
66
|
+
correlation_strength = _clamp01(float(topology.get("correlation_strength", 0.0) or 0.0))
|
|
67
|
+
topology_penalty = {"isolated": 0.3, "elevated": 0.6}.get(str(topology.get("topology_risk", "unknown")), 1.0)
|
|
68
|
+
topology_component = correlation_strength * topology_penalty if topology else regime_component
|
|
69
|
+
|
|
70
|
+
participation_rate = _clamp01(float(liquidity.get("participation_rate", 0.0) or 0.0))
|
|
71
|
+
liquidity_component = _clamp01(1.0 - participation_rate) if liquidity else 1.0
|
|
72
|
+
|
|
73
|
+
weights = {"confidence": 0.45, "regime": 0.20, "topology": 0.20, "liquidity": 0.15}
|
|
74
|
+
score = (
|
|
75
|
+
weights["confidence"] * confidence_component
|
|
76
|
+
+ weights["regime"] * regime_component
|
|
77
|
+
+ weights["topology"] * topology_component
|
|
78
|
+
+ weights["liquidity"] * liquidity_component
|
|
79
|
+
)
|
|
80
|
+
breakdown = {
|
|
81
|
+
"confidence_component": confidence_component,
|
|
82
|
+
"regime_component": regime_component,
|
|
83
|
+
"topology_component": topology_component,
|
|
84
|
+
"liquidity_component": liquidity_component,
|
|
85
|
+
"weights": dict(weights),
|
|
86
|
+
}
|
|
87
|
+
return _clamp01(score), breakdown
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def compute_trade_metric(
|
|
91
|
+
confidence: float,
|
|
92
|
+
regime_confidence: float,
|
|
93
|
+
topology: dict,
|
|
94
|
+
liquidity: dict,
|
|
95
|
+
use_composite_signal_score: bool,
|
|
96
|
+
) -> float:
|
|
97
|
+
"""V5.2.6 (development/Problems.md) - extracted verbatim from
|
|
98
|
+
build_market_analysis_decision()'s own inline trade_metric computation
|
|
99
|
+
so a calibration tool (evaluation/confidence_threshold_calibration.py)
|
|
100
|
+
can compute the IDENTICAL statistic against historical data without
|
|
101
|
+
re-deriving the formula and risking it silently drifting apart from
|
|
102
|
+
what the live gate actually checks. Pure extraction - zero behavior
|
|
103
|
+
change, build_market_analysis_decision() now calls this instead of
|
|
104
|
+
repeating the expression inline."""
|
|
105
|
+
signal_quality_score, _ = compute_signal_quality_score(confidence, regime_confidence, topology, liquidity)
|
|
106
|
+
return signal_quality_score if use_composite_signal_score else float(confidence)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def build_market_analysis_decision(
|
|
110
|
+
signal_name: str,
|
|
111
|
+
confidence: float,
|
|
112
|
+
probability_up: float,
|
|
113
|
+
target_weight: float,
|
|
114
|
+
regime: dict,
|
|
115
|
+
gating: dict,
|
|
116
|
+
trading_eligible: bool,
|
|
117
|
+
trade_lock_active: bool,
|
|
118
|
+
trade_lock_reason: str | None = None,
|
|
119
|
+
topology: dict | None = None,
|
|
120
|
+
liquidity: dict | None = None,
|
|
121
|
+
min_confidence_to_trade: float = 0.12,
|
|
122
|
+
retrain_min_regime_confidence: float = 0.20,
|
|
123
|
+
low_regime_confidence_threshold: float = 0.35,
|
|
124
|
+
use_composite_signal_score: bool = False,
|
|
125
|
+
predicted_return_magnitude: float | None = None,
|
|
126
|
+
predicted_volatility: float | None = None,
|
|
127
|
+
is_currently_invested: bool = False,
|
|
128
|
+
net_edge: dict | None = None,
|
|
129
|
+
# V5.2.6 (development/Problems.md) - is_book_selected/
|
|
130
|
+
# min_confidence_to_trade_book_selected let a book-selected symbol
|
|
131
|
+
# (already ranked into the top/bottom-N by a DIFFERENT metric,
|
|
132
|
+
# predicted_rank_20d, before this function ever runs) clear Priority
|
|
133
|
+
# 7 via its own, separately-calibrated confidence floor instead of
|
|
134
|
+
# being vetoed by a threshold measuring the same rank-derived
|
|
135
|
+
# confidence value against itself. min_confidence_to_trade_book_selected=None
|
|
136
|
+
# (the default) is a strict no-op - every symbol, book-selected or
|
|
137
|
+
# not, uses min_confidence_to_trade exactly as before this existed.
|
|
138
|
+
# risk_off_override_min_severity narrows Priority 2 the same way -
|
|
139
|
+
# None (the default) reproduces today's unconditional override
|
|
140
|
+
# exactly; see that priority's own comment for why a bare 0.0 default
|
|
141
|
+
# would NOT be a safe no-op.
|
|
142
|
+
is_book_selected: bool = False,
|
|
143
|
+
min_confidence_to_trade_book_selected: float | None = None,
|
|
144
|
+
risk_off_override_min_severity: float | None = None,
|
|
145
|
+
) -> MarketAnalysisDecision:
|
|
146
|
+
# signal_name == "short" (Phase 3 of the 5/10 -> 9/10 roadmap,
|
|
147
|
+
# portfolio/book_construction.py) is treated identically to "buy"/"sell"
|
|
148
|
+
# by every safety-tier check below (`in {"buy", "sell", "short"}`) - a
|
|
149
|
+
# book-selected short position passes through the exact same
|
|
150
|
+
# deterministic trade-lock/risk-off/topology/liquidity categorization
|
|
151
|
+
# as any other directional signal, never bypassing it. See
|
|
152
|
+
# analyzer/README.md for why this categorization stays deterministic.
|
|
153
|
+
reasons: list[str] = []
|
|
154
|
+
|
|
155
|
+
# Priority 0 (evaluated before even trade-lock): closing an EXISTING
|
|
156
|
+
# position is risk-REDUCING by construction, so none of the protective
|
|
157
|
+
# vetoes below (trade-lock, risk-off regime, elevated topology) may
|
|
158
|
+
# block it - only ever suppress a new/added exposure. Before this fix
|
|
159
|
+
# they applied identically to "sell", so a risk-off regime or an
|
|
160
|
+
# elevated-topology reading during exactly the periods you'd most want
|
|
161
|
+
# to cut a position instead trapped it open (development/Problems.md:
|
|
162
|
+
# confirmed to be why the COVID-2020 drawdown window closed zero
|
|
163
|
+
# positions in the first real backtest). "sell" is the one signal that
|
|
164
|
+
# universally means "close whatever is open" (main.py::_apply_signal()
|
|
165
|
+
# liquidates on it regardless of long/short), so this only ever reduces
|
|
166
|
+
# risk, never increases it - a "short"/"buy" signal still goes through
|
|
167
|
+
# every tier below unchanged, and a "sell" with nothing currently open
|
|
168
|
+
# falls through too (there's nothing to protect by fast-pathing it).
|
|
169
|
+
if signal_name == "sell" and is_currently_invested:
|
|
170
|
+
reasons.append("exit_signal_for_open_position_bypasses_risk_vetoes")
|
|
171
|
+
bypass_quality_score, bypass_quality_breakdown = compute_signal_quality_score(
|
|
172
|
+
confidence, float((regime or {}).get("confidence", 0.0) or 0.0), topology or {}, liquidity or {}
|
|
173
|
+
)
|
|
174
|
+
return MarketAnalysisDecision(
|
|
175
|
+
action="trade",
|
|
176
|
+
signal=signal_name,
|
|
177
|
+
target_weight=target_weight,
|
|
178
|
+
confidence=confidence,
|
|
179
|
+
probability_up=probability_up,
|
|
180
|
+
trading_eligible=trading_eligible,
|
|
181
|
+
topology_considered=bool(topology),
|
|
182
|
+
liquidity_considered=bool(liquidity),
|
|
183
|
+
reasons=reasons,
|
|
184
|
+
signal_quality_score=bypass_quality_score,
|
|
185
|
+
signal_quality_breakdown=bypass_quality_breakdown,
|
|
186
|
+
predicted_return_magnitude=predicted_return_magnitude,
|
|
187
|
+
predicted_volatility=predicted_volatility,
|
|
188
|
+
)
|
|
189
|
+
decision_source = str(gating.get("decision_source", "unknown"))
|
|
190
|
+
regime_confidence = float(regime.get("confidence", 0.0) or 0.0)
|
|
191
|
+
risk_regime = str(regime.get("risk_regime", "risk_neutral"))
|
|
192
|
+
# V5.2.6 (development/Problems.md) - the continuous severity score
|
|
193
|
+
# behind risk_regime's own categorical classification (regime/
|
|
194
|
+
# market_regime.py::classify_risk_regime()) - Priority 2 below uses
|
|
195
|
+
# this to narrow its override to genuinely severe risk_off readings
|
|
196
|
+
# only, when configured to do so.
|
|
197
|
+
risk_score = float(regime.get("risk_score", 0.0) or 0.0)
|
|
198
|
+
topology = topology or {}
|
|
199
|
+
topology_considered = bool(topology)
|
|
200
|
+
topology_risk = str(topology.get("topology_risk", "unknown")) if topology_considered else "unknown"
|
|
201
|
+
liquidity = liquidity or {}
|
|
202
|
+
liquidity_considered = bool(liquidity)
|
|
203
|
+
liquidity_action = str(liquidity.get("recommended_action", "allow")) if liquidity_considered else "allow"
|
|
204
|
+
if topology_considered:
|
|
205
|
+
reasons.append(f"topology_state={topology.get('state', 'unknown')}")
|
|
206
|
+
else:
|
|
207
|
+
reasons.append("topology_absent_v2_11_pending")
|
|
208
|
+
|
|
209
|
+
# Always computed and exposed for dashboard visibility, regardless of
|
|
210
|
+
# use_composite_signal_score - see compute_signal_quality_score()'s
|
|
211
|
+
# docstring and analyzer/README.md. trade_metric is what priorities 7/8
|
|
212
|
+
# actually gate on below: the composite score when the flag is on,
|
|
213
|
+
# otherwise raw confidence - byte-identical to pre-flag behavior when
|
|
214
|
+
# use_composite_signal_score=False (the default).
|
|
215
|
+
signal_quality_score, signal_quality_breakdown = compute_signal_quality_score(
|
|
216
|
+
confidence, regime_confidence, topology, liquidity
|
|
217
|
+
)
|
|
218
|
+
# Deliberately NOT calling compute_trade_metric() here even though it
|
|
219
|
+
# computes the identical value - that would recompute
|
|
220
|
+
# compute_signal_quality_score() a second time per symbol per bar in
|
|
221
|
+
# this hot loop. compute_trade_metric() exists for callers (the
|
|
222
|
+
# calibration tool) that don't already have signal_quality_score in
|
|
223
|
+
# hand; this call site does, so it reuses it directly. Both paths are
|
|
224
|
+
# provably the same formula - see compute_trade_metric()'s own
|
|
225
|
+
# docstring.
|
|
226
|
+
trade_metric = signal_quality_score if use_composite_signal_score else float(confidence)
|
|
227
|
+
|
|
228
|
+
# Priority 1: reduce_risk - portfolio-level risk lock always wins.
|
|
229
|
+
if trade_lock_active:
|
|
230
|
+
reasons.append(trade_lock_reason or "risk_lock_active")
|
|
231
|
+
return MarketAnalysisDecision(
|
|
232
|
+
action="reduce_risk",
|
|
233
|
+
signal="hold",
|
|
234
|
+
target_weight=0.0,
|
|
235
|
+
confidence=confidence,
|
|
236
|
+
probability_up=probability_up,
|
|
237
|
+
trading_eligible=trading_eligible,
|
|
238
|
+
topology_considered=topology_considered,
|
|
239
|
+
liquidity_considered=liquidity_considered,
|
|
240
|
+
reasons=reasons,
|
|
241
|
+
signal_quality_score=signal_quality_score,
|
|
242
|
+
signal_quality_breakdown=signal_quality_breakdown,
|
|
243
|
+
predicted_return_magnitude=predicted_return_magnitude,
|
|
244
|
+
predicted_volatility=predicted_volatility,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Priority 2: reduce_risk - asset-level risk regime override even
|
|
248
|
+
# without a portfolio-wide lock (e.g. risk_off + would-be trade).
|
|
249
|
+
#
|
|
250
|
+
# V5.2.6 (development/Problems.md) - narrowed to fire only when
|
|
251
|
+
# risk_score is at least risk_off_override_min_severity severe, when
|
|
252
|
+
# configured. Why a None-sentinel and not a plain 0.0 default: traced
|
|
253
|
+
# classify_risk_regime()'s three independent risk_off paths and found
|
|
254
|
+
# the drawdown-triggered path (normalized_drawdown >= threshold and
|
|
255
|
+
# volatility_regime != "low_volatility") can coincide with a
|
|
256
|
+
# risk_score as high as +0.10 (bullish trend + normal volatility +
|
|
257
|
+
# drawdown breach) - i.e. risk_off can be true even when the
|
|
258
|
+
# continuous severity score is mildly POSITIVE. A bare
|
|
259
|
+
# "risk_score <= 0.0" default would therefore NOT reproduce today's
|
|
260
|
+
# unconditional-override behavior for that case; only a None-sentinel
|
|
261
|
+
# (bypassing the severity check entirely when unconfigured) is a
|
|
262
|
+
# provably safe no-op. The model's own regime_risk_on/off/neutral and
|
|
263
|
+
# regime_signal_risk_score_scaled are active training features, so a
|
|
264
|
+
# milder risk_off reading falling through here lets the model's own
|
|
265
|
+
# already-learned regime sizing stand instead of being additionally,
|
|
266
|
+
# externally overridden by a rule the offline Sharpe number being
|
|
267
|
+
# chased was never subject to.
|
|
268
|
+
if (
|
|
269
|
+
risk_regime == "risk_off"
|
|
270
|
+
and (risk_off_override_min_severity is None or risk_score <= -risk_off_override_min_severity)
|
|
271
|
+
and signal_name in {"buy", "sell", "short"}
|
|
272
|
+
):
|
|
273
|
+
reasons.append("risk_off_regime_overrides_directional_signal")
|
|
274
|
+
return MarketAnalysisDecision(
|
|
275
|
+
action="reduce_risk",
|
|
276
|
+
signal="hold",
|
|
277
|
+
target_weight=0.0,
|
|
278
|
+
confidence=confidence,
|
|
279
|
+
probability_up=probability_up,
|
|
280
|
+
trading_eligible=trading_eligible,
|
|
281
|
+
topology_considered=topology_considered,
|
|
282
|
+
liquidity_considered=liquidity_considered,
|
|
283
|
+
reasons=reasons,
|
|
284
|
+
signal_quality_score=signal_quality_score,
|
|
285
|
+
signal_quality_breakdown=signal_quality_breakdown,
|
|
286
|
+
predicted_return_magnitude=predicted_return_magnitude,
|
|
287
|
+
predicted_volatility=predicted_volatility,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Priority 3: reduce_risk - elevated cross-sectional volatility pressure
|
|
291
|
+
# from the deterministic topology layer overrides a directional signal.
|
|
292
|
+
# V2-17.5 added a probabilistic topology overlay (topology.learned_topology)
|
|
293
|
+
# feeding the retrain-trigger/retraining pipeline, but deliberately left
|
|
294
|
+
# this rule reading only the deterministic topology_risk - see
|
|
295
|
+
# analyzer/README.md and development/architecture.md's V2-17.5 section.
|
|
296
|
+
if topology_risk == "elevated" and signal_name in {"buy", "sell", "short"}:
|
|
297
|
+
reasons.append("topology_elevated_volatility_pressure_overrides_directional_signal")
|
|
298
|
+
return MarketAnalysisDecision(
|
|
299
|
+
action="reduce_risk",
|
|
300
|
+
signal="hold",
|
|
301
|
+
target_weight=0.0,
|
|
302
|
+
confidence=confidence,
|
|
303
|
+
probability_up=probability_up,
|
|
304
|
+
trading_eligible=trading_eligible,
|
|
305
|
+
topology_considered=topology_considered,
|
|
306
|
+
liquidity_considered=liquidity_considered,
|
|
307
|
+
reasons=reasons,
|
|
308
|
+
signal_quality_score=signal_quality_score,
|
|
309
|
+
signal_quality_breakdown=signal_quality_breakdown,
|
|
310
|
+
predicted_return_magnitude=predicted_return_magnitude,
|
|
311
|
+
predicted_volatility=predicted_volatility,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Priority 4: retrain_candidate - zero experts contributing AND the
|
|
315
|
+
# regime read is itself low-confidence. Stateless per-bar heuristic,
|
|
316
|
+
# separate from the real trailing-window retrain-trigger system V2-16/17
|
|
317
|
+
# built in performance/triggers.py + retraining/ - this rule was never
|
|
318
|
+
# wired to it and still isn't; see analyzer/README.md.
|
|
319
|
+
if decision_source == "baseline_fallback" and regime_confidence < retrain_min_regime_confidence:
|
|
320
|
+
reasons.append("baseline_fallback_with_low_regime_confidence")
|
|
321
|
+
return MarketAnalysisDecision(
|
|
322
|
+
action="retrain_candidate",
|
|
323
|
+
signal="hold",
|
|
324
|
+
target_weight=0.0,
|
|
325
|
+
confidence=confidence,
|
|
326
|
+
probability_up=probability_up,
|
|
327
|
+
trading_eligible=trading_eligible,
|
|
328
|
+
topology_considered=topology_considered,
|
|
329
|
+
liquidity_considered=liquidity_considered,
|
|
330
|
+
reasons=reasons,
|
|
331
|
+
signal_quality_score=signal_quality_score,
|
|
332
|
+
signal_quality_breakdown=signal_quality_breakdown,
|
|
333
|
+
predicted_return_magnitude=predicted_return_magnitude,
|
|
334
|
+
predicted_volatility=predicted_volatility,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Priority 5: simulate - liquidity blocked (zero volume or DDV below
|
|
338
|
+
# the floor). An unexecutable order must not reach the order book.
|
|
339
|
+
# See V2-18 for future data-driven calibration of these thresholds.
|
|
340
|
+
if liquidity_action == "block" and signal_name in {"buy", "sell", "short"}:
|
|
341
|
+
reasons.append("liquidity_blocked_insufficient_volume_simulate_instead")
|
|
342
|
+
return MarketAnalysisDecision(
|
|
343
|
+
action="simulate",
|
|
344
|
+
signal=signal_name,
|
|
345
|
+
target_weight=0.0,
|
|
346
|
+
confidence=confidence,
|
|
347
|
+
probability_up=probability_up,
|
|
348
|
+
trading_eligible=trading_eligible,
|
|
349
|
+
topology_considered=topology_considered,
|
|
350
|
+
liquidity_considered=liquidity_considered,
|
|
351
|
+
reasons=reasons,
|
|
352
|
+
signal_quality_score=signal_quality_score,
|
|
353
|
+
signal_quality_breakdown=signal_quality_breakdown,
|
|
354
|
+
predicted_return_magnitude=predicted_return_magnitude,
|
|
355
|
+
predicted_volatility=predicted_volatility,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
# Priority 6: simulate - thin market; participation rate would be
|
|
359
|
+
# uncomfortably large relative to daily volume.
|
|
360
|
+
if liquidity_action == "simulate_instead" and signal_name in {"buy", "sell", "short"}:
|
|
361
|
+
reasons.append("liquidity_thin_market_simulate_instead")
|
|
362
|
+
return MarketAnalysisDecision(
|
|
363
|
+
action="simulate",
|
|
364
|
+
signal=signal_name,
|
|
365
|
+
target_weight=0.0,
|
|
366
|
+
confidence=confidence,
|
|
367
|
+
probability_up=probability_up,
|
|
368
|
+
trading_eligible=trading_eligible,
|
|
369
|
+
topology_considered=topology_considered,
|
|
370
|
+
liquidity_considered=liquidity_considered,
|
|
371
|
+
reasons=reasons,
|
|
372
|
+
signal_quality_score=signal_quality_score,
|
|
373
|
+
signal_quality_breakdown=signal_quality_breakdown,
|
|
374
|
+
predicted_return_magnitude=predicted_return_magnitude,
|
|
375
|
+
predicted_volatility=predicted_volatility,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Priority 6.5 (V5.1 Phase 1, development/Problems.md, item 3 - the
|
|
379
|
+
# actual root cause of the fee drag: $2,769 in fees on $3,159 gross
|
|
380
|
+
# profit in the last representative backtest): the trade is
|
|
381
|
+
# directionally right often enough to show a rank-IC, but not by
|
|
382
|
+
# enough to pay the spread + impact + commission. `not
|
|
383
|
+
# is_currently_invested` matters - an EXIT must never be blocked by a
|
|
384
|
+
# cost gate, the same reasoning Priority 0's is_currently_invested
|
|
385
|
+
# bypass above already uses (closing risk-reducing-by-construction
|
|
386
|
+
# positions is not something a cost estimate should be able to trap
|
|
387
|
+
# open).
|
|
388
|
+
#
|
|
389
|
+
# Gates on net_edge["passes"] - the SAME field
|
|
390
|
+
# execution/cost_model.py::build_net_edge_decision() already computed
|
|
391
|
+
# (passes=True, reason="net_edge_gate_disabled" whenever the gate is
|
|
392
|
+
# disabled/uncalibrated/no rank prediction this bar) - deliberately
|
|
393
|
+
# NOT re-deriving "net_edge_bps < some threshold" here. Problems.md
|
|
394
|
+
# #76: an earlier version of this tier re-compared net_edge_bps
|
|
395
|
+
# against min_net_edge_bps directly; a "disabled" NetEdgeDecision's
|
|
396
|
+
# net_edge_bps defaults to 0.0 (not None), so once a preset set a real
|
|
397
|
+
# positive min_net_edge_bps threshold, 0.0 < threshold was true on
|
|
398
|
+
# EVERY bar for EVERY symbol - a supposedly no-op cost gate blocked
|
|
399
|
+
# 100% of entries (Lean Backtest 1: 0 orders end to end). Trusting the
|
|
400
|
+
# single "passes" verdict computed once, in one place, is what
|
|
401
|
+
# actually satisfies the "disabled config is a strict no-op" contract.
|
|
402
|
+
if (
|
|
403
|
+
net_edge is not None
|
|
404
|
+
and signal_name in {"buy", "sell", "short"}
|
|
405
|
+
and not is_currently_invested
|
|
406
|
+
and not net_edge.get("passes", True)
|
|
407
|
+
):
|
|
408
|
+
reasons.append("expected_net_edge_below_cost_simulate_instead")
|
|
409
|
+
return MarketAnalysisDecision(
|
|
410
|
+
action="simulate",
|
|
411
|
+
signal=signal_name,
|
|
412
|
+
target_weight=0.0,
|
|
413
|
+
confidence=confidence,
|
|
414
|
+
probability_up=probability_up,
|
|
415
|
+
trading_eligible=trading_eligible,
|
|
416
|
+
topology_considered=topology_considered,
|
|
417
|
+
liquidity_considered=liquidity_considered,
|
|
418
|
+
reasons=reasons,
|
|
419
|
+
signal_quality_score=signal_quality_score,
|
|
420
|
+
signal_quality_breakdown=signal_quality_breakdown,
|
|
421
|
+
predicted_return_magnitude=predicted_return_magnitude,
|
|
422
|
+
predicted_volatility=predicted_volatility,
|
|
423
|
+
net_edge=net_edge,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Priority 7: trade - only for trading-eligible assets with an actionable
|
|
427
|
+
# directional signal, sufficient confidence, no topology isolation, and
|
|
428
|
+
# no liquidity block (redundant guard — tiers 5/6 already caught those).
|
|
429
|
+
# Gates on trade_metric, not raw confidence directly - see trade_metric's
|
|
430
|
+
# definition above (identical to confidence unless
|
|
431
|
+
# use_composite_signal_score=True).
|
|
432
|
+
#
|
|
433
|
+
# V5.2.6 (development/Problems.md) - a book-selected symbol's
|
|
434
|
+
# `confidence` (main.py) is DERIVED from predicted_rank_20d, the same
|
|
435
|
+
# score that already selected it into the book's top/bottom-N before
|
|
436
|
+
# this function ever runs - reusing min_confidence_to_trade verbatim
|
|
437
|
+
# compares that symbol against itself via two different names for the
|
|
438
|
+
# same information. min_confidence_to_trade_book_selected lets book
|
|
439
|
+
# members clear a separately-calibrated floor instead; None (the
|
|
440
|
+
# default) reproduces today's single-threshold behavior exactly for
|
|
441
|
+
# every symbol, book-selected or not.
|
|
442
|
+
effective_min_confidence_to_trade = (
|
|
443
|
+
min_confidence_to_trade_book_selected
|
|
444
|
+
if is_book_selected and min_confidence_to_trade_book_selected is not None
|
|
445
|
+
else min_confidence_to_trade
|
|
446
|
+
)
|
|
447
|
+
if (
|
|
448
|
+
trading_eligible
|
|
449
|
+
and signal_name in {"buy", "sell", "short"}
|
|
450
|
+
and trade_metric >= effective_min_confidence_to_trade
|
|
451
|
+
and topology_risk != "isolated"
|
|
452
|
+
and liquidity_action not in {"block", "simulate_instead"}
|
|
453
|
+
):
|
|
454
|
+
reasons.append(
|
|
455
|
+
"trading_eligible_book_selected_directional_signal_above_confidence_threshold"
|
|
456
|
+
if is_book_selected else
|
|
457
|
+
"trading_eligible_directional_signal_above_confidence_threshold"
|
|
458
|
+
)
|
|
459
|
+
return MarketAnalysisDecision(
|
|
460
|
+
action="trade",
|
|
461
|
+
signal=signal_name,
|
|
462
|
+
target_weight=target_weight,
|
|
463
|
+
confidence=confidence,
|
|
464
|
+
probability_up=probability_up,
|
|
465
|
+
trading_eligible=trading_eligible,
|
|
466
|
+
topology_considered=topology_considered,
|
|
467
|
+
liquidity_considered=liquidity_considered,
|
|
468
|
+
reasons=reasons,
|
|
469
|
+
signal_quality_score=signal_quality_score,
|
|
470
|
+
signal_quality_breakdown=signal_quality_breakdown,
|
|
471
|
+
predicted_return_magnitude=predicted_return_magnitude,
|
|
472
|
+
predicted_volatility=predicted_volatility,
|
|
473
|
+
# Visibility only - Priority 6.5 above already vetoed any trade
|
|
474
|
+
# whose net_edge failed the gate, so a trade reaching here
|
|
475
|
+
# either passed it or the gate was disabled/uncalibrated
|
|
476
|
+
# (net_edge=None) this bar.
|
|
477
|
+
net_edge=net_edge,
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
# Priority 8: simulate vs observe.
|
|
481
|
+
if signal_name in {"buy", "sell", "short"} and regime_confidence >= low_regime_confidence_threshold:
|
|
482
|
+
if topology_risk == "isolated":
|
|
483
|
+
reasons.append("topology_isolated_asset_lacks_peer_confirmation_simulate_instead")
|
|
484
|
+
elif trading_eligible:
|
|
485
|
+
reasons.append(
|
|
486
|
+
"book_selected_confidence_below_book_trade_threshold_simulate_instead"
|
|
487
|
+
if is_book_selected else
|
|
488
|
+
"confidence_below_trade_threshold_simulate_instead"
|
|
489
|
+
)
|
|
490
|
+
else:
|
|
491
|
+
reasons.append("observation_only_asset_directional_signal_simulate_instead")
|
|
492
|
+
return MarketAnalysisDecision(
|
|
493
|
+
action="simulate",
|
|
494
|
+
signal=signal_name,
|
|
495
|
+
target_weight=0.0,
|
|
496
|
+
confidence=confidence,
|
|
497
|
+
probability_up=probability_up,
|
|
498
|
+
trading_eligible=trading_eligible,
|
|
499
|
+
topology_considered=topology_considered,
|
|
500
|
+
liquidity_considered=liquidity_considered,
|
|
501
|
+
reasons=reasons,
|
|
502
|
+
signal_quality_score=signal_quality_score,
|
|
503
|
+
signal_quality_breakdown=signal_quality_breakdown,
|
|
504
|
+
predicted_return_magnitude=predicted_return_magnitude,
|
|
505
|
+
predicted_volatility=predicted_volatility,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
reasons.append("no_actionable_edge")
|
|
509
|
+
return MarketAnalysisDecision(
|
|
510
|
+
action="observe",
|
|
511
|
+
signal="hold",
|
|
512
|
+
target_weight=0.0,
|
|
513
|
+
confidence=confidence,
|
|
514
|
+
probability_up=probability_up,
|
|
515
|
+
trading_eligible=trading_eligible,
|
|
516
|
+
topology_considered=topology_considered,
|
|
517
|
+
liquidity_considered=liquidity_considered,
|
|
518
|
+
reasons=reasons,
|
|
519
|
+
signal_quality_score=signal_quality_score,
|
|
520
|
+
signal_quality_breakdown=signal_quality_breakdown,
|
|
521
|
+
)
|