Skip to content

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.

base_target_weight instance-attribute

base_target_weight: float

confidence_multiplier instance-attribute

confidence_multiplier: float

lot_count instance-attribute

lot_count: int

margin_required instance-attribute

margin_required: float

margin_utilization instance-attribute

margin_utilization: float

max_leverage_utilization instance-attribute

max_leverage_utilization: float

notional_value instance-attribute

notional_value: float

sizing_reason instance-attribute

sizing_reason: str

target_weight instance-attribute

target_weight: float

to_dict

to_dict() -> dict[str, Any]

FuturesSizingDecision dataclass

One futures order's size: contracts, notional, margin and the caps that bound it.

base_target_weight instance-attribute

base_target_weight: float

confidence_multiplier instance-attribute

confidence_multiplier: float

contract_count instance-attribute

contract_count: int

margin_required instance-attribute

margin_required: float

margin_utilization instance-attribute

margin_utilization: float

max_margin_utilization instance-attribute

max_margin_utilization: float

notional_value instance-attribute

notional_value: float

sizing_reason instance-attribute

sizing_reason: str

target_weight instance-attribute

target_weight: float

to_dict

to_dict() -> dict[str, Any]

KillSwitchDecision dataclass

The kill switch's verdict on one bar: tripped or not, severity, triggers, the reason and what it observed against which thresholds.

observed class-attribute instance-attribute

observed: dict = field(default_factory=dict)

reason class-attribute instance-attribute

reason: str = ''

recommended_action class-attribute instance-attribute

recommended_action: str = 'none'

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).

severity instance-attribute

severity: str

thresholds class-attribute instance-attribute

thresholds: dict = field(default_factory=dict)

triggers class-attribute instance-attribute

triggers: list[str] = field(default_factory=list)

tripped instance-attribute

tripped: bool

to_dict

to_dict() -> dict[str, Any]

PositionSizingDecision dataclass

One position's sizing: the base and final weight and every multiplier between them (volatility, topology, rank, RL, cost).

annualized_volatility instance-attribute

annualized_volatility: float

base_target_weight instance-attribute

base_target_weight: float

confidence_multiplier instance-attribute

confidence_multiplier: float

cost_multiplier class-attribute instance-attribute

cost_multiplier: float = 1.0

cost_sizing_reason class-attribute instance-attribute

cost_sizing_reason: str = 'cost_sizing_disabled_or_absent'

leverage_factor instance-attribute

leverage_factor: float

max_leverage instance-attribute

max_leverage: float

rank_multiplier class-attribute instance-attribute

rank_multiplier: float = 1.0

rank_sizing_reason class-attribute instance-attribute

rank_sizing_reason: str = 'rank_sizing_disabled_or_absent'

rl_multiplier class-attribute instance-attribute

rl_multiplier: float = 1.0

rl_sizing_reason class-attribute instance-attribute

rl_sizing_reason: str = 'rl_sizing_disabled_or_absent'

rolling_volatility instance-attribute

rolling_volatility: float

sizing_reason instance-attribute

sizing_reason: str

target_weight instance-attribute

target_weight: float

topology_multiplier instance-attribute

topology_multiplier: float

topology_sizing_reason instance-attribute

topology_sizing_reason: str

volatility_multiplier instance-attribute

volatility_multiplier: float

volatility_regime instance-attribute

volatility_regime: str

volatility_source class-attribute instance-attribute

volatility_source: str = 'rolling'

to_dict

to_dict() -> dict[str, Any]

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:

  1. Max holding age: fires once bar_index - entry_bar_index reaches exits_max_holding_bars, regardless of price.
  2. 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, "") whenever the flag is off, the model is missing/malformed, the state is incomplete, or the model's own state_keys disagree with RL_SIZING_STATE_KEYS - every failure mode is a strict no-op, never a crash and never an amplified size.

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).