aether_quant.risk¶
Risk controls: the kill switch, the bounded sizing multipliers and the exposure / drawdown gates (formally verified in formal/AetherFormal/{KillSwitch,PositionSizing,RiskControls}.lean).
Risk controls: the kill switch, the bounded sizing multipliers and the exposure / drawdown gates (formally verified in formal/AetherFormal/{KillSwitch,PositionSizing,RiskControls}.lean).
ForexSizingDecision
dataclass
¶
One forex order's size: lots, notional, margin and the caps that bound it.
FuturesSizingDecision
dataclass
¶
One futures order's size: contracts, notional, margin and the caps that bound it.
KillSwitchDecision
dataclass
¶
The kill switch's verdict on one bar: tripped or not, severity, triggers, the reason and what it observed against which thresholds.
rolling_sharpe_below_run
property
¶
rolling_sharpe_below_run: int
The next bar's runtime_metrics["rolling_sharpe_below_run_last_bar"] (trip_confirm_bars's counter): the
consecutive bars the rolling Sharpe has read below the floor, whether or not the trip is confirmed yet.
rolling_sharpe_tripped
property
¶
rolling_sharpe_tripped: bool
The next bar's runtime_metrics["rolling_sharpe_tripped_last_bar"] (the release_rolling_sharpe band).
PositionSizingDecision
dataclass
¶
One position's sizing: the base and final weight and every multiplier between them (volatility, topology, rank, RL, cost).
cost_sizing_reason
class-attribute
instance-attribute
¶
cost_sizing_reason: str = 'cost_sizing_disabled_or_absent'
rank_sizing_reason
class-attribute
instance-attribute
¶
rank_sizing_reason: str = 'rank_sizing_disabled_or_absent'
rl_sizing_reason
class-attribute
instance-attribute
¶
rl_sizing_reason: str = 'rl_sizing_disabled_or_absent'
active_position_limit_reached ¶
active_position_limit_reached(active_positions: int, max_active_positions: int, is_currently_invested: bool) -> bool
Whether a new position would exceed the active-position limit (a name already held never counts against it).
assess_drawdown_lock ¶
assess_drawdown_lock(daily_drawdown: float, total_drawdown: float, max_daily_drawdown_pct: float, max_total_drawdown_pct: float) -> tuple[bool, str | None]
Whether the daily or total drawdown has reached its limit, and which (total first): the sticky trade lock's test.
asset_listed_on_venue ¶
asset_listed_on_venue(ticker: str, on_date: date | datetime | str, listing_dates: dict[str, dict] | None) -> bool
False before a pair's venue listing date, from its delisting date on, or inside a suspension window (e.g. Coinbase suspended XRP from
2021-01-19 to 2023-07-13): a backtest must not trade a pair the exchange did not list. Tickers without an entry, and
an empty mapping, are always listed. on_date is a date/datetime or an ISO string.
book_candidate_trading_eligible ¶
book_candidate_trading_eligible(trading_eligible: bool, asset_class: str | None, futures_risk_enabled: bool, options_risk_enabled: bool, forex_risk_enabled: bool) -> bool
Book-selection eligibility: a symbol of a disabled asset class (sizing zeroed, positions liquidated) must not take a slot or neutrality weight in the long/short book it can never trade. One definition for main.py's live candidates and the offline simulator's candidate metadata.
build_dynamic_position_sizing ¶
build_dynamic_position_sizing(base_target_weight: float, confidence: float, rolling_volatility: float, max_position_weight: float, target_daily_volatility: float = 0.015, min_position_weight: float = 0.0, low_volatility_threshold: float = 0.01, high_volatility_threshold: float = 0.03, min_volatility_multiplier: float = 0.35, max_volatility_multiplier: float = 1.25, max_leverage: float = 1.0, topology_source: str | None = None, topology_confidence: float | None = None, topology_disagreement: float | None = None, min_topology_multiplier: float = 0.5, max_topology_multiplier: float = 1.0, predicted_volatility: float | None = None, use_predicted_volatility: bool = False, predicted_rank_20d: float | None = None, rank_sizing_enabled: bool = False, min_rank_multiplier: float = 0.75, max_rank_multiplier: float = 1.25, rl_model: dict[str, Any] | None = None, rl_state: dict[str, Any] | None = None, rl_sizing_enabled: bool = False, min_rl_multiplier: float = 0.6, max_rl_multiplier: float = 1.0) -> PositionSizingDecision
Size one position: the base weight scaled by confidence, volatility, topology, rank, RL and cost multipliers, never past the cap and never flipped.
build_forex_order_sizing_record ¶
build_forex_order_sizing_record(date_str: str, symbol: str, direction: str, target_weight: float, portfolio_value: float, close_price: float, order_units: int) -> dict[str, Any]
V5.2.7 (development/Problems.md) - a diagnostic snapshot of one forex order-sizing decision, for main.py's optional (config-gated, off by default, backtest-only) forex-order-sizing log. Exists because compute_forex_order_units()'s "Lean forex MarketOrder takes raw units" assumption cannot be verified from this Python codebase alone - notional_ratio is the direct, human-checkable confirmation a real backtest's log can provide instead: it should sit very close to 1.0 for correct sizing (only whole-unit rounding slop); a value wildly off (e.g. 100x or 0.01x) is the exact, unambiguous symptom of a wrong units assumption, and should be treated as disqualifying for that backtest's other results until investigated.
Pure: no I/O, no self, never raises on well-formed inputs.
intended_notional == 0.0 (target_weight == 0.0) returns
notional_ratio=None rather than dividing by zero - a symbol with no
real order to place has nothing to sanity-check.
build_forex_position_sizing ¶
build_forex_position_sizing(base_target_weight: float, confidence: float, price: float, pair_spec: dict[str, Any] | None, portfolio_value: float, target_leverage_utilization: float = 0.2, max_leverage_utilization: float = 0.4) -> ForexSizingDecision
Leverage-utilization-targeted sizing: computes the max lots affordable at max_leverage_utilization (the hard ceiling), then scales toward target_leverage_utilization by confidence using the same "0.5 + 0.5*confidence" shape as risk/futures_risk.py::build_futures_position_sizing()'s confidence_multiplier, and floors to an integer lot_count.
Per-lot margin is lot_size * price * margin_pct (equivalently
lot_size * price / leverage_max, since margin_pct and leverage_max
are reciprocals by construction in forex_pair_specs.json).
target_weight is a DERIVED reconciliation value for downstream weight-based consumers (exposure caps) - lot_count is the source of truth and is what main.py::_apply_signal() actually executes via MarketOrder(), never fed back into SetHoldings().
Returns an all-zero decision (lot_count=0) when pair_spec is missing/empty, confidence is 0, base_target_weight is 0, or portfolio_value/price/lot_size/margin_pct are non-positive - never raises, matching risk/futures_risk.py's "no active signal -> zero position" convention.
build_futures_position_sizing ¶
build_futures_position_sizing(base_target_weight: float, confidence: float, price: float, contract_spec: dict[str, Any] | None, portfolio_value: float, target_margin_utilization: float = 0.2, max_margin_utilization: float = 0.4) -> FuturesSizingDecision
Margin-utilization-targeted sizing: computes the max contracts affordable at max_margin_utilization (the hard ceiling), then scales toward target_margin_utilization by confidence using the same "0.5 + 0.5*confidence" shape as risk/position_sizing.py::build_dynamic_position_sizing()'s confidence_multiplier, and floors to an integer contract_count (Lean trades futures in whole contracts, never fractional weights).
target_weight is a DERIVED reconciliation value for downstream weight- based consumers (exposure caps, book construction) - contract_count is the source of truth and is what main.py::_apply_signal() actually executes via MarketOrder(), never fed back into SetHoldings().
Returns an all-zero decision (contract_count=0) when contract_spec is missing/empty, confidence is 0, base_target_weight is 0, or portfolio_value/price/multiplier/margin are non-positive - never raises, matching risk/position_sizing.py's "no active signal -> zero weight" convention.
build_live_contract_spec ¶
build_live_contract_spec(multiplier: float, initial_margin_usd: float, description: str = '', exchange: str = '') -> dict[str, Any]
Constructs a contract_spec dict in the exact shape build_futures_position_sizing() already consumes (same keys load_futures_contract_specs() produces from the static JSON file), from live values main.py resolves via Lean's own Security.SymbolProperties/BuyingPowerModel. Pure - no Lean coupling itself; only main.py's caller touches self.Securities. description/ exchange default to "" (informational only, never read by build_futures_position_sizing()'s own sizing math) since Lean's BuyingPowerModel query has no equivalent of either - callers typically pass the static spec's own values through for display continuity when a live spec replaces it.
build_rl_sizing_state ¶
build_rl_sizing_state(base_features: dict[str, Any], confidence: float, state_keys: list[str] | None = None, extra_state: dict[str, Any] | None = None) -> dict[str, Any] | None
Pulls RL_SIZING_STATE_KEYS out of the SAME base_features dict main.py's _build_model_input_impl() already assembled this bar (plus the model's own separately-computed confidence score, not itself a base_feature) - no new computation, no new data source. Returns None if any key is absent (e.g. Component D's alt-data features not yet in input_set/retrained), which rl_sizing_multiplier() treats identically to "no model" - a strict 1.0 no-op, never a crash or a partially-built state vector.
cap_forex_target_to_book_weight ¶
cap_forex_target_to_book_weight(target_weight: float, book_weight: float) -> float
V5.5.0 (Problems.md #128) - forex sizing above is margin-driven: a pair's target_weight is lots * lot_size * price / NAV, i.e. several TIMES NAV at 20% margin utilization and 1:50 leverage, and the book's own weight only chose the direction. Only the exposure caps limited it (shorts shared one 30% budget with equities), so the 2026-08-27 run held up to 60% of NAV short in forex while the book wanted ~8% a name.
Caps |target_weight| at |book_weight| (the weight the book assigned this pair), keeping the sign. Margin sizing stays as an upper bound, never a target. Non-finite input -> 0.0 (no trade).
cap_target_weight ¶
cap_target_weight(target_weight: float, current_exposure: float, exposure_cap: float) -> tuple[float, bool]
(weight, blocked): the target clipped to the exposure left under the cap, sign preserved. A non-finite target is
blocked (V6.4.3: nan >= 0.0 is False, so a NaN target came out as a full short of the remaining capacity).
classify_volatility_regime ¶
classify_volatility_regime(rolling_volatility: float, low_volatility_threshold: float, high_volatility_threshold: float) -> str
Low, normal or high volatility for the sizing multiplier.
compute_forex_order_units ¶
compute_forex_order_units(target_weight: float, close_price: float, portfolio_value: float) -> int
V5.2.7 (development/Problems.md) - converts a target portfolio weight into whole base-currency UNITS for a Forex MarketOrder - NOT a lot count. QuantConnect Lean's MarketOrder() for SecurityType.Forex takes raw base-currency units (e.g. MarketOrder("EURUSD", 10000) = 10,000 EUR), matching Portfolio[symbol].Quantity's own reporting convention - standard, documented Lean/IB behavior, but UNVERIFIED empirically in this repo/Lean image as of this fix. See build_forex_order_sizing_record()'s notional_ratio field for the direct, human-checkable confirmation this assumption needs from a real backtest.
Bug this closes: main.py::_forex_lot_count_for_weight() used to divide notional by (lot_size * close_price) - a full 100,000-unit standard lot - which on a $100k backtest account with a realistic book-member position weight (4-12%) produces a notional ($4,000- $12,000) far smaller than even one lot ($67,000-$130,000+), so the division always rounded to 0. Confirmed directly: forex allocations that cleared every other risk/liquidity/confidence gate and reached a real "trade" decision still silently produced zero Lean orders.
Deliberately does NOT round to a whole lot_size multiple either - that would silently reproduce the exact same always-zero-orders bug under a new name, since the same realistic position sizes are still far below one full lot. Rounds to the nearest whole UNIT instead (IB/Lean forex orders must be whole units, never fractional currency). Returns 0 (never raises) when close_price or portfolio_value is non-positive.
compute_held_weight ¶
compute_held_weight(holdings_value: float, total_portfolio_value: float) -> float
The current_weight formula _apply_signal()'s scaling branches already compute inline (twice, once per direction) - extracted so every caller reads one shared formula instead of maintaining duplicate copies that could silently drift if one is ever edited without the other. Floors total_portfolio_value at 1.0 (never divide by zero/negative).
compute_incremental_order_quantity ¶
compute_incremental_order_quantity(target_quantity: float, current_quantity: float) -> float
target_quantity is an ABSOLUTE, already-signed sizing target (futures contract count, or a matched option contract's/spread's target quantity), recomputed fresh every bar; current_quantity is whatever is actually held right now. Returns the signed delta an incremental order primitive (MarketOrder/Buy) must submit to converge toward target_quantity, instead of firing the absolute target every bar and silently overshooting it. Pure arithmetic, never raises - callers round/zero-check the result themselves.
compute_position_exit_tracking_update ¶
compute_position_exit_tracking_update(was_invested: bool, is_invested: bool, close_price: float, signal_name: str, peak_price_since_entry: float | None, direction: str | None, bar_index: int, has_entry_state: bool = True, held_quantity: float | None = None, average_entry_price: float | None = None, clear_stale_state: bool = False) -> dict[str, Any]
V4.10 - pure extraction of main.py::_update_position_exit_tracking().
V5.5.0 (Problems.md #134): the closing fill also lands on the NEXT bar, so
the invested -> flat transition is likewise never observed here and the
entry state of a closed position stayed behind. The symbol's next entry
was then ADOPTED never (state "exists"), inherited the old entry bar and
peak, and was force-sold for max_holding_age_exceeded / the trailing stop
the day after it filled: 154 of the 196 re-entries in the 2026-10-07
backtest lasted under 2 days (all 44 first entries were fine). With
clear_stale_state=True, a flat symbol that is still carrying entry state
returns {"action": "clear"}. Default False keeps the pre-fix behavior.
V5.5.0 (Problems.md #128): a daily market/limit order fills on the NEXT
bar, so the was-flat -> is-invested transition this function keys on is
never observed inside a single Pass 2 iteration - the position just
appears already-held one bar later. Without adoption, no entry state was
ever recorded: max_holding_bars never fired, shorts defaulted to a
"long" trailing stop (inverted), and peaks never reset between holdings.
With has_entry_state=False and an invested+was_invested symbol, the
position is ADOPTED as a fresh entry here, direction taken from the real
held_quantity sign and entry price from average_entry_price when given.
Defaults (has_entry_state=True) reproduce the pre-V5.5.0 behavior.
Observes the net invested/not-invested transition (was_invested vs.
is_invested) rather than hooking into any asset-class-specific
routing - robust to every routing path by construction, since it only
cares about the outcome.
Returns the NEW state as a dict rather than mutating dicts in place, so this stays pure/testable - the caller applies the result to its own tracking dicts: - {"action": "enter", "entry_bar_index":, "entry_price":, "peak_price_since_entry":, "direction":} on a fresh entry. - {"action": "hold", "peak_price_since_entry":} while an existing position is held (entry_bar_index/entry_price are untouched by design - not present in this dict). - {"action": "clear"} once a held position is closed. - {"action": "noop"} otherwise (was never invested and still isn't - matches the original method's implicit no-op branch).
cost_sizing_multiplier ¶
cost_sizing_multiplier(expected_edge_bps: float, expected_cost_bps: float, cost_sizing_enabled: bool, min_cost_multiplier: float = 0.25) -> tuple[float, str]
Bounded, continuous, shrink-only size adjustment (V5.1 Phase 1, development/Problems.md, item 3) - follows topology_sizing_multiplier()'s exact contract: never above 1.0 (never amplifies size beyond what the deterministic sizing already computed), and a disabled/absent input is always a strict no-op.
multiplier = clamp((edge - cost) / max(edge, eps), min_cost_multiplier, 1.0) - a trade whose edge comfortably clears its cost sizes at full conviction (multiplier 1.0); one whose edge is barely above cost (or below it - analyzer/market_analyzer.py's Priority 6.5 tier is the actual entry veto for that case, this is a size-scaling BACKSTOP for whatever reaches sizing regardless, e.g. an already-open position being resized) shrinks toward min_cost_multiplier rather than sizing at full weight on a marginal edge.
Config-gated, additive, default OFF (phase_v2.costs.cost_sizing_enabled) - same off-by-default precedent as rank_sizing_enabled/rl_sizing_enabled, since this depends on the same not-yet-calibrated edge_bps_per_rank_unit execution/cost_model.py::build_net_edge_decision() documents.
evaluate_kill_switch ¶
evaluate_kill_switch(runtime_metrics: dict[str, Any], config: dict[str, Any]) -> KillSwitchDecision
Evaluated once per bar by main.py, fed a runtime_metrics dict it
assembles from state it already tracks:
- "recent_bar_returns": list[float] - a rolling per-bar portfolio
return history (main.py's own bounded deque), used to compute a
rolling Sharpe over the trailing evaluation_bars window.
- "drawdown_velocity_pct_per_bar": float | None - this bar's total
drawdown minus the previous bar's (a one-bar rate of change, not
an average - a single sharp bar is exactly what this exists to
catch quickly).
- "live_rank_ic": float | None - the most recent rank-IC-decay
trigger's metric_value, if the LATEST entry in
visualization/grafana/retraining_status.json's last_trigger
happens to be a rank_ic_decay_trigger (main.py never opens its own
Postgres connection - this is the same "read a locally-cached file
a separate Postgres-connected worker refreshes" pattern
read_manual_trade_lock_override()/load_cached_fred_series() already
use). None whenever the latest trigger isn't rank-IC-shaped, not
just when the file is missing - a stale/wrong-shaped signal must
never be treated as a real reading.
- "consecutive_losses": int | None - main.py's own realized-trade
streak counter.
- "slippage_divergence_bps": float | None - realized-vs-expected
round-trip cost divergence from the most recent fill(s)
(execution/cost_model.py's expected estimate vs. the actual fill
price main.py's OnOrderEvent observes). None until at least one
fill has been observed this session.
- "model_age_days": float | None - age of the active model artifact
on disk.
- "reconciliation_breach": bool - True when execution/reconciliation.py::
reconcile_positions()'s max_abs_weight_drift exceeded
phase_v2.reconciliation.max_tolerated_drift this bar (Step 6.2)
and phase_v2.reconciliation.trips_kill_switch is true - reconciliation
failures trip the switch rather than being logged and ignored.
Any key's value being None simply skips that one condition (never raises, never counts as a trip) - a bar where a given signal isn't available yet degrades that ONE condition, not the whole switch.
evaluate_non_model_exit ¶
evaluate_non_model_exit(bar_index: int, exits_enabled: bool, exits_max_holding_bars: int, exits_trailing_stop_pct: float, entry_bar_index: int | None, peak_price_since_entry: float | None, direction: str, close_price: float) -> str | None
V4.10 - pure extraction of main.py::_check_non_model_exit(), asset-class-agnostic (same trailing-stop/max-age math for equity, crypto, futures, options, forex alike). Two checks, first match wins:
- Max holding age: fires once bar_index - entry_bar_index reaches exits_max_holding_bars, regardless of price.
- Trailing stop: fires once price has drawn down exits_trailing_stop_pct from its best price since entry (direction-aware: "best" is the highest close for a long, lowest for a short).
exits_enabled=False restores pre-fix behavior byte-for-byte - no non-model exit ever fires. Caller resolves the per-symbol dict lookups (entry_bar_index/peak_price_since_entry/direction) before calling - this function itself touches no dicts, no self.
is_backtest_safety_bypass_active ¶
is_backtest_safety_bypass_active(runtime_mode: str, bypass_flag: bool) -> bool
True only when a backtest is running with phase_v2.backtest.bypass_safety_gates explicitly set to true - any non-backtest runtime_mode always returns False regardless of the flag, and the flag defaults to False (gates active, honest/deployable- representative) so this is opt-in, not implicit. Deliberately independent of aq trade-lock's on/off/auto override, which keeps its existing, separately-documented meaning unchanged in every runtime mode.
V5.2.7 (development/Problems.md) - CORRECTED docstring: this used to claim "scoped narrowly to the sticky total-drawdown lock and the regime risk_off drawdown branch," but that was never accurate once V5.1 Phase 6 extended main.py's session-rollover sticky-clear branch to ALSO cover any kill_switch_* trade_lock_reason, not just total_drawdown_limit_breached - this flag's real effect was always broader than its own docstring said. This is now the LEGACY combined flag, kept only for backward compatibility (still fully functional, still bundles both behaviors exactly as before) - new callers should use is_sticky_trade_lock_bypass_active()/is_regime_drawdown_bypass_active() below instead, which split the same two behaviors into independently configurable flags while still OR-ing this legacy flag in so nobody's existing config silently stops working.
is_cooldown_exempt ¶
is_cooldown_exempt(signal_name: str, is_currently_invested: bool, is_forced_exit: bool = False) -> bool
V5.5.0 (Problems.md #128/#133) - a FORCED exit of an open position (the book's rotation exit, or the max-holding / trailing-stop backstops) is never blocked by trade_cooldown_bars: the cooldown exists to stop entry whipsaw, and applied to those it held rotated-out book names ~14 days.
A plain legacy "sell" SIGNAL is NOT exempt. The first version exempted every sell, and the legacy probability signal then flipped book and sleeve positions out within a day: 230 of 355 trades in the 2026-10-07 backtest closed within 2 days (mean hold 5.8 days vs 39 before), 6-7 bps of fees each way for nothing. The cooldown is what rate-limits that noisy signal, so it keeps doing so.
is_position_resize_permitted ¶
is_position_resize_permitted(position_scaling_enabled: bool, is_book_selected: bool, is_rebalance_bar: bool) -> bool
V5.2.1 (development/Problems.md) - a book-selected symbol's SIZE may only resize on a rebalance bar; every other symbol (an individual- signal trade, or every symbol when the book is disabled entirely) is completely unaffected by this gate.
Bug this closes: should_scale_position() was being re-evaluated every single bar for book members too, entirely independent of the book's own is_rebalance_bar cadence - a book member's base weight is frozen between rebalances, but the downstream sizing pipeline (confidence/ volatility/liquidity/cost multipliers) reinterprets that same frozen weight differently bar to bar from live noise, tripping rebalance_threshold_weight and firing real resize orders on non- rebalance bars. That noise-churn is what this gates out.
Deliberately does NOT touch full exits (signal_name == "sell") or the V4.3.0 futures/options incremental-quantity protection (compute_incremental_order_quantity()) - both are unconditional, separate code paths, never gated by this function.
is_regime_drawdown_bypass_active ¶
is_regime_drawdown_bypass_active(runtime_mode: str, regime_bypass_flag: bool, legacy_bypass_flag: bool) -> bool
V5.2.7 (development/Problems.md) - True when a backtest is running with EITHER phase_v2.backtest.bypass_regime_drawdown_gate OR the legacy phase_v2.backtest.bypass_safety_gates explicitly true. Controls ONLY main.py::_build_regime_payload()'s risk_off_drawdown_threshold override (set to infinity when active, disabling just the drawdown-driven branch of classify_risk_regime - the bearish-trend/high-vol and composite-risk-score branches stay active regardless) - never the sticky trade-lock clear (see is_sticky_trade_lock_bypass_active() for that). Any non-backtest runtime_mode always returns False regardless of either flag.
Deliberately a SEPARATE flag from the sticky-lock bypass above, even though both were previously bundled under the single bypass_safety_gates flag - a user who wants a stuck kill-switch lock to be able to clear should not be forced to also disable this unrelated regime-level protection as a side effect.
is_sticky_trade_lock_bypass_active ¶
is_sticky_trade_lock_bypass_active(runtime_mode: str, sticky_bypass_flag: bool, legacy_bypass_flag: bool) -> bool
V5.2.7 (development/Problems.md) - True when a backtest is running with EITHER phase_v2.backtest.bypass_sticky_trade_lock OR the legacy phase_v2.backtest.bypass_safety_gates explicitly true. Controls ONLY main.py's session-rollover total_drawdown_limit_breached/kill_switch_* sticky-lock clear - never the regime risk_off drawdown branch (see is_regime_drawdown_bypass_active() for that, now a genuinely independent flag instead of the two being bundled together under one name). Any non-backtest runtime_mode always returns False regardless of either flag.
Bug this closes: a real backtest showed a kill-switch trip on 2020-02-27 stay locked for the remaining 13 months of a ~2.2-year backtest (336/336 book-member decisions forced to reduce_risk in that window, vs. 16% before the trip) - kill_switch_* reasons are deliberately exempt from the normal daily auto-clear (a real, correct safety choice for live/paper trading, where a human should decide when to resume after a genuine circuit-breaker trip), but an unattended historical backtest has no human to ever clear it, so a single trip event silently darkens the rest of the evaluation window regardless of whether the underlying conditions later recover.
load_forex_pair_specs ¶
load_forex_pair_specs(path: Path = DEFAULT_PAIR_SPECS_PATH) -> dict[str, Any]
Defensive load - returns {} (not a raised exception) on a missing or unparseable specs file, same convention as risk/futures_risk.py::load_futures_contract_specs(). A missing spec for a given pair means build_forex_position_sizing() can't size it - callers must treat that as "no position", not a crash.
load_futures_contract_specs ¶
load_futures_contract_specs(path: Path = DEFAULT_CONTRACT_SPECS_PATH) -> dict[str, Any]
Defensive load - returns {} (not a raised exception) on a missing or unparseable specs file, same convention as train.py::load_sector_mapping(). A missing spec for a given ticker means build_futures_position_sizing() can't size it - callers must treat that as "no position", not a crash.
load_rl_sizing_model ¶
load_rl_sizing_model(path: Path) -> dict[str, Any] | None
Missing file -> None (fresh checkout / never trained - the documented default state). Malformed JSON -> None (never raises). Mirrors main.py's own "missing/malformed model -> None, never a hard failure" convention already established for every other optional learned artifact (topology, gating, experts).
load_venue_listing_dates ¶
load_venue_listing_dates(path: str | Path) -> dict[str, dict]
{ticker: {"listed": "YYYY-MM-DD", "suspended": [["start", "end"], ...]}} from a JSON file (data/reference/crypto_listing_dates.json, V5.8.0). Missing or unreadable file -> {} (no guard, the old behaviour).
projected_signed_quantity ¶
projected_signed_quantity(held_quantity: float, open_order_quantity: float) -> float
V5.5.0 (Problems.md #128) - the position size AFTER every in-flight order fills: held + open (signed, remaining) order quantity. Sizing off held quantity alone re-submitted the same target on every tick until the first order filled (forex is evaluated on both the equity-session and the forex-session tick), stacking up to 60% short. Non-finite inputs count as 0.0.
rank_sizing_multiplier ¶
rank_sizing_multiplier(rank_prediction: float | None, rank_sizing_enabled: bool, min_rank_multiplier: float = 0.75, max_rank_multiplier: float = 1.25) -> tuple[float, str]
Bounded, continuous, direction-preserving size adjustment driven by the sequence/multitask model's rank_20d head (predicted cross- sectional percentile rank of 20-day forward return, [0, 1] - see train.py::compute_rank_ic()/development/Changelog.md's "frontier-model edge investigation" entry: the one signal in the whole system with a statistically significant backtest rank-IC, 0.073 mean IC / t-stat 4.40 on the sequence model's full backtest series).
A predicted rank near 1.0 (top of the universe) scales size UP toward max_rank_multiplier; a rank near 0.0 (predicted bottom) scales it DOWN toward min_rank_multiplier; a rank of exactly 0.5 (predicted median) is a no-op (multiplier 1.0). Never flips direction/sign - this only ever scales the SAME direction the existing 1d-direction gating decision already picked, matching topology_sizing_multiplier()'s "probabilistic scoring may only ever produce a bounded, continuous adjustment, never a randomized or amplified decision" convention.
Config-gated, additive, default OFF (phase_v2.dynamic_risk.rank_sizing_enabled) - this signal's own non-overlapping-date backtest subsample was not independently significant (t-stat 1.20 on 28 independent 20-day windows, vs. 4.40 on the full autocorrelated series), so it ships available, tested, and wired end-to-end, but stays off: that evidence does not support turning it on.
read_kill_switch_manual_override ¶
read_kill_switch_manual_override(config_path: Path) -> bool | None
True = force-armed, False = force-disarmed, None = defer entirely to config.json's own phase_v2.risk.kill_switch.enabled. Never raises - same missing-file/missing-key degrade-to-None contract as read_manual_trade_lock_override().
read_manual_trade_lock_override ¶
read_manual_trade_lock_override(config_path: Path) -> bool | None
Returns True/False if a manual override is set, None if absent/unset or the config file doesn't exist yet - never raises. Mtime-gated cache (see execution/config_cache.py) - picks up an edit as soon as the file's mtime changes, not on a fixed schedule.
resolve_asset_class_enabled ¶
resolve_asset_class_enabled(asset_class: str | None, futures_risk_enabled: bool, options_risk_enabled: bool, forex_risk_enabled: bool = True) -> bool
True iff main.py may open/maintain a position in asset_class
right now (used by main.py's per-bar
_liquidate_positions_for_disabled_asset_classes() sweep).
Equity/crypto/bond (and any unrecognized asset_class) have NO enable/disable flag anywhere in this codebase - always True for those, by construction, matching route_position_sizing()'s own "fall back to equity behavior for anything unrecognized" default above. future -> futures_risk_enabled (phase_v2.futures_risk.enabled); option -> options_risk_enabled (phase_v2.options_risk.enabled); forex -> forex_risk_enabled (phase_v2.forex_risk.enabled, V4.6). Pure lookup, never raises. forex_risk_enabled defaults True only to keep this signature backward-compatible for any caller that hasn't been updated to pass it explicitly - main.py's own call site always passes it.
resolve_benchmark_ticker ¶
resolve_benchmark_ticker(runtime: dict[str, Any] | None, universe_tickers: Iterable[str]) -> str | None
Ticker Lean's benchmark should follow (runtime.benchmark_ticker), or None to keep Lean's default.
Lean's default benchmark is an internal HOUR SPY feed; the local hourly zip is Lean's sample file and ends 2021-03-31, so every later run charted a constant benchmark. Pointing the benchmark at the daily universe subscription fixes that without a second feed; a ticker outside the universe falls back to the default rather than adding a subscription.
resolve_futures_margin_source ¶
resolve_futures_margin_source(raw_value: str) -> str
Validates phase_v2.futures_risk.margin_source - "static" (default, the offline data/reference/futures_contract_specs.json file) or "live" (Lean's own IB-calibrated BuyingPowerModel, resolved once per security at add-time and queried per sizing call - see main.py:: _resolve_futures_contract_spec()). "live" here means Lean's own LOCAL margin-model calculation (InteractiveBrokersBrokerageModel's bundled margin rules), never a live network round-trip to IB's servers - that would be far too slow inside on_data() and is not what this option does. Any unrecognized value (a config typo) falls back to "static" rather than raising - never crashes the algorithm over a bad string.
resolve_lean_window ¶
resolve_lean_window(runtime: dict[str, Any] | None, windows: dict[str, Any]) -> tuple[tuple[int, int, int], tuple[int, int, int]]
((start y, m, d), (end y, m, d)) of the Lean run: runtime.lean_window_override {"start", "end"} when both are
ISO dates, else phase1.windows.backtest. The override exists so a Lean run can cover the holdout period (V5.8.0 BT2)
without editing phase1.windows, which also defines the dataset split the models were trained and judged on.
resolve_rl_shadow_guard ¶
resolve_rl_shadow_guard(raw: dict[str, Any] | None) -> dict[str, Any]
phase_v2.dynamic_risk.rl_shadow_guard -> {"enabled", "window_bars", "tolerance_nav_fraction"}; absent =
disabled (RL, when enabled, is never switched off by its own track record - the pre-V5.8.0 behaviour).
resolve_trading_days_per_year ¶
resolve_trading_days_per_year(runtime: dict[str, Any] | None) -> int | None
runtime.trading_days_per_year for Lean's statistics (V6.2.0), or None to keep Lean's default (252 under the
default brokerage model). The equity curve holds a point for every calendar day once crypto trades on weekends, so
365 annualises it the way Lean itself does for a crypto brokerage. Only Lean's reported statistics change.
rl_excess_return ¶
rl_excess_return(positions: dict[str, tuple[float, float]], returns: dict[str, float]) -> float
V5.8.0: one bar's return (NAV fraction) of the RL-sized book minus the same book at constant 1.0 sizing.
positions maps symbol -> (signed weight actually held, RL multiplier applied when it was sized); the constant book
would have held weight / multiplier, so the excess is weight * (1 - 1/multiplier) * return. Non-finite or
non-positive multipliers and missing returns contribute nothing.
rl_shadow_guard_allows ¶
rl_shadow_guard_allows(excess_history: list[float], guard: dict[str, Any]) -> bool
True while RL may size: always when the guard is off; otherwise the summed excess return over the trailing
window_bars must not be below -tolerance_nav_fraction (RL that has cost more than the tolerance against
constant sizing falls back to 1.0 until the loss leaves the window).
rl_sizing_multiplier ¶
rl_sizing_multiplier(model: dict[str, Any] | None, state: dict[str, Any] | None, rl_sizing_enabled: bool, min_rl_multiplier: float = DEFAULT_MIN_RL_MULTIPLIER, max_rl_multiplier: float = DEFAULT_MAX_RL_MULTIPLIER, expected_state_keys: list[str] | None = None) -> tuple[float, str]
expected_state_keys (V5.7.0, default RL_SIZING_STATE_KEYS) is the key list the CALLER's state was built with; a
model trained for another list (the forex policy) is refused, never silently misaligned.
Bounded, deterministic (argmax, never sampled), shrink-only size
adjustment - exact contract clone of
risk/position_sizing.py::rank_sizing_multiplier()/topology_sizing_multiplier():
returns (1.0, "
THREE safety properties, all enforced HERE rather than trusted from the artifact (an untrusted/stale/hand-edited model file must never be able to violate them): 1. Argmax, never sampled - see _softmax_argmax_index()'s docstring. 2. Clamped to [min_rl_multiplier, max_rl_multiplier] AFTER the argmax, so a corrupted/adversarial action table can never emit a multiplier outside the configured band, regardless of what the model file claims its own actions are. 3. max_rl_multiplier defaults to 1.0 - SHRINK-ONLY, matching topology_sizing_multiplier()'s "never amplifies size beyond what deterministic-only sizing already computed" rule. Amplification (max_rl_multiplier > 1.0) is technically possible via config but is NOT the shipped/recommended posture for v1 - see development/Problems.md #71's abandon criteria.
rollover_due ¶
rollover_due(current_date: date, contract_expiry_date: date | None, rollover_days_before_expiry: int = 5) -> bool
Pure date comparison - a diagnostic/logging signal only. Actual contract rollover is entirely delegated to Lean's native add_future() + continuous-contract SetFilter() mapping (main.py::_add_asset()); this function must never itself trigger a trade. Returns False when contract_expiry_date is unknown (e.g. Lean's continuous-contract subscription hasn't surfaced the mapped contract's expiry yet).
route_multi_leg_option_sizing ¶
route_multi_leg_option_sizing(enabled_strategy_names: list[Any], signal_name: str, confidence: float, available_chain: list[Any], portfolio_value: float, underlying_price: float, volatility_view: str, risk_tier_preference: str, margin_family_enabled: bool, *, target_delta_at_full_confidence: float, max_vega_budget_pct_of_equity: float, short_leg_delta_offset: float, contract_multiplier: float, target_margin_utilization: float, max_margin_utilization: float, pct_of_underlying_value: float, min_pct_of_underlying_value: float, current_date: date = None, risk_free_rate: float = 0.045, arbitrage_detector_enabled: bool = False, min_mispricing_bps: float = 15.0, arbitrage_max_contracts_per_signal: int = 1, strategy_selector_scores: dict[str, Any] | None = None) -> tuple[PositionSizingDecision, dict] | None
V4.5 - the full-coverage sibling of route_position_sizing()'s
option branch: tries enabled_strategy_names, reordered by
order_enabled_strategies() (§9.2's priority-list semantic), stopping
at the FIRST candidate that actually sizes. "single_leg" has no
MULTI_LEG_STRATEGY_REGISTRY entry (it's not an OptionStrategies
factory) and is special-cased to the existing
build_options_position_sizing() path; every other name is looked up
in the registry and dispatched by risk_tier:
- "vega_budget" -> build_multi_leg_position_sizing() (this includes
bull_call_spread/bear_put_spread too when reached via this NEW
router - the registry-driven selector is provably identical
geometry to the OLD dedicated build_vertical_spread_position_sizing()
for those 2, see options_strategy.py's select_vertical_legs()).
- margin tiers -> build_margin_position_sizing(), but ONLY when
margin_family_enabled (§5's hard gate - main.py additionally
requires runtime_mode == "backtest" before ever setting this True,
see main.py's own gate).
- "covered_protective" -> skipped here entirely; main.py's
corrected cross-asset design sizes these separately (needs the
held EQUITY quantity, not available_chain alone - see
build_covered_protective_position_sizing()).
- "unreachable_arbitrage" -> always skipped, by design (§8).
strategy_selector_scores (V4.7, development/Problems.md #29's own framing) - the learned strategy-selector model's live inference plug point: when truthy (a trained model is loaded AND phase_v2.strategy_selector.enabled), reranks enabled_strategy_names by score (portfolio/options_strategy.py::rerank_enabled_strategies_by_score()) INSTEAD OF order_enabled_strategies()'s static risk-tier-preference reordering. Defaults to None - the only value ever passed until both conditions hold, reproducing today's exact ordered_names output byte-identically. Either way, the actual per-strategy_name dispatch loop below (leg construction, sizing, risk math) is completely unchanged - only WHICH NAME IS TRIED FIRST can differ.
straddle/strangle/iron_condor/iron_butterfly shape families are additionally gated to strategies_for_volatility_view(volatility_view) - every other shape family (vertical, butterfly, calendar, backspread, ladder, naked) is NOT volatility-gated at all, firing whenever it's the enabled candidate and its own selector finds a usable chain (§4's "within-bucket selection follows directly from the allowlist").
Returns None when nothing in the (possibly empty) enabled list sizes - callers must treat that as "no option position this bar", same degrade-to-absent contract as every other branch in this module.
route_position_sizing ¶
route_position_sizing(asset_class: str, signal_name: str, confidence: float, base_target_weight: float, *, equity_crypto_kwargs: dict[str, Any] | None = None, price: float = 0.0, portfolio_value: float = 0.0, contract_spec: dict[str, Any] | None = None, futures_kwargs: dict[str, Any] | None = None, pair_spec: dict[str, Any] | None = None, forex_kwargs: dict[str, Any] | None = None, available_chain: list[dict] | None = None, options_kwargs: dict[str, Any] | None = None) -> tuple[PositionSizingDecision, dict]
Dispatches on asset_class (falls back to "equity" behavior for any unrecognized value - the existing, safest, most-tested path). Returns (PositionSizingDecision, extra) - extra is {} for equity/crypto/bond, {"contract_count": int} for future, {"lot_count": int} for forex (V4.6), {"options_decision": OptionsPositionDecision | OptionsSpreadPositionDecision} for option (only when a position was actually sized; {} when the asset-class-specific sizer returned no position, e.g. an empty options chain because IB is disabled). options_kwargs["spread_strategy"] ("single_leg" default, or "vertical") picks which sizer runs - default reproduces the exact single-leg behavior every existing caller already depends on, by construction.
should_liquidate_disabled_asset_class_position ¶
should_liquidate_disabled_asset_class_position(asset_class_enabled: bool, is_invested: bool) -> bool
Pure predicate: True iff a currently-open position must be liquidated purely because its asset class is disabled this bar - independent of whatever buy/sell/hold signal this bar's model output computed. Equity/crypto/bond positions can never satisfy this (resolve_asset_class_enabled() always returns True for them, so a caller never even reaches this predicate with asset_class_enabled=False for those classes) - this predicate doesn't special-case class names itself, it trusts asset_class_enabled as already resolved.
should_lock_in_duration_beta ¶
should_lock_in_duration_beta(window_length: int, treasury_window_length: int, target_window_length: int) -> bool
V5.2.5 (development/Problems.md #91-continuation) - whether enough history has accumulated to compute bond_empirical_duration_beta ONCE and cache it, rather than re-rolling a fresh (and therefore drifting) estimate every bar.
Bug this closes: main.py::_bond_empirical_duration_beta_for_symbol() used to recompute this OLS slope from a ROLLING window every single bar for the life of the backtest, while train.py::build_bond_features_by_date() (the offline path that produced the model's OWN training data) computes it ONCE per ticker - a single slope over that asset's entire available history - and broadcasts that unchanged value to every row. The model was trained on a stable, whole-history characteristic per bond asset; live was feeding it a noisy, continuously-drifting value instead - an out-of-training-distribution input for every bond-tagged symbol, every bar, confirmed directly correlated with the bond-ETF-heavy live-vs- offline selection mismatch pattern found in the V5.2.4 reconciliation.
True once BOTH the price window and the treasury-yield window have
reached target_window_length (main.py's own self.long_bar_history_size,
260 - the deque's own maxlen, the most history practically available to
a live algorithm that can never see future data the way an offline
batch process can) - mirrors train.py's own "compute once per ticker,
broadcast unchanged" semantic as closely as a real-time algorithm's
inherent lack of future history allows. Both windows must independently
clear the bar (AND, not OR) - a symbol with plenty of price history but
a still-filling treasury-yield history (or vice versa) isn't ready yet.
should_scale_position ¶
should_scale_position(current_weight: float, target_weight: float, rebalance_threshold_weight: float) -> bool
True iff an already-open, same-direction position's target has moved far enough (>= threshold) to warrant resubmitting an order, rather than treating trivial confidence wiggle as churn. Direction-agnostic (abs) - callers only invoke this once a same-direction hold is already established via the unchanged previous_signal gate.
topology_sizing_multiplier ¶
topology_sizing_multiplier(topology_source: str | None, topology_confidence: float | None, topology_disagreement: float | None, min_topology_multiplier: float = 0.5, max_topology_multiplier: float = 1.0) -> tuple[float, str]
Bounded, continuous, shrink-only adjustment - never above max_topology_multiplier (1.0 by default), i.e. never amplifies size beyond what the deterministic-only sizing already computed. Only engages when topology_source == "learned" - topology/learned_topology.py already gates that label on confidence clearing min_confidence_for_learned upstream, so "fallback"/missing/None here is always a strict no-op, matching the same "a missing/degraded learned model never changes trading behavior" convention already established for the topology overlay itself (see analyzer/README.md and topology/README.md's safety rule: probabilistic scoring may only ever produce a bounded, continuous adjustment, never a randomized or amplified decision).
write_kill_switch_manual_override ¶
write_kill_switch_manual_override(value: bool | None, config_path: Path) -> None
Read-modify-write config.json, setting only phase_v2.risk.kill_switch_manual_override - every other key (including phase_v2.risk.manual_trade_lock_override) is preserved untouched. value=None removes the override key entirely (returns to fully config-driven kill_switch.enabled behavior).
write_manual_trade_lock_override ¶
write_manual_trade_lock_override(value: bool | None, config_path: Path) -> None
Read-modify-write config.json, setting only phase_v2.risk.manual_trade_lock_override - every other key is preserved untouched. value=None removes the override key entirely (returns to fully automatic behavior).