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
@@ -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
+ )