Skip to content

aether_quant.portfolio

Book construction: rank-based long/short selection, neutrality, tranches, class caps and the sleeve loss guard (formally verified in formal/AetherFormal/{BookNeutrality,BookConstruction,SleeveGuard}.lean).

Book construction: rank-based long/short selection, neutrality, tranches, class caps and the sleeve loss guard (formally verified in formal/AetherFormal/{BookNeutrality,BookConstruction,SleeveGuard}.lean).

BookAllocation dataclass

One name's place in the rank book: long or short, its multiplier, its predicted rank and why it was chosen.

book_reason instance-attribute

book_reason: str

book_role_multiplier instance-attribute

book_role_multiplier: float

predicted_rank_20d instance-attribute

predicted_rank_20d: float

rank_head class-attribute instance-attribute

rank_head: str = 'blend'

role instance-attribute

role: str

target_weight class-attribute instance-attribute

target_weight: float | None = None

to_dict

to_dict() -> dict[str, Any]

ClassSleeveConfig dataclass

Settings of one asset-class sleeve: whether it runs, names per side and its weights.

asset_class instance-attribute

asset_class: str

enabled class-attribute instance-attribute

enabled: bool = False

gross_budget class-attribute instance-attribute

gross_budget: float = 0.0

hysteresis_rank_margin class-attribute instance-attribute

hysteresis_rank_margin: float = 0.0

max_weight_per_name class-attribute instance-attribute

max_weight_per_name: float | None = None

min_rank_confidence_spread class-attribute instance-attribute

min_rank_confidence_spread: float = 0.0

min_trade_usd class-attribute instance-attribute

min_trade_usd: float = 0.0

per_side class-attribute instance-attribute

per_side: int = 2

rebalance_every_bars class-attribute instance-attribute

rebalance_every_bars: int = 20

signal class-attribute instance-attribute

signal: str = 'model_rank'

MarginSizingDecision dataclass

Margin-tier sibling of OptionsMultiLegPositionDecision - same legs/expiries/contracts/strategy_name/sizing_reason shape (so main.py's record-building code can treat either decision type identically via duck typing), plus margin_required/margin_utilization in place of net_debit_or_credit/net_delta/net_vega (a margin-tier position's defining risk unit is margin consumed, not vega).

contracts instance-attribute

contracts: int

expiries instance-attribute

expiries: tuple[str, ...]

legs instance-attribute

legs: tuple[OptionsSpreadLeg, ...]

margin_required instance-attribute

margin_required: float

margin_utilization instance-attribute

margin_utilization: float

sizing_reason instance-attribute

sizing_reason: str

strategy_name instance-attribute

strategy_name: str

to_dict

to_dict() -> dict[str, Any]

MultiLegSpec dataclass

One leg of a multi-leg OptionStrategy factory result. side/ratio are transcribed directly from OptionStrategies.cs's actual Quantity field for that leg (e.g. a butterfly's body leg is ratio=2, side= "short" for a long butterfly) - never inferred from the factory's positional strike-name alone (a real bug this pass found and fixed, see the module-level note above). is_anchor marks a leg selected FIRST, by confidence-scaled target delta; every other leg in the same strategy is selected as a strike/expiry offset from an anchor leg. Iron condor/butterfly have TWO anchor legs (short_put, short_call) - one per composed vertical selection.

expiry_role class-attribute instance-attribute

expiry_role: str = 'single'

is_anchor class-attribute instance-attribute

is_anchor: bool = False

ratio instance-attribute

ratio: int

right instance-attribute

right: str

role instance-attribute

role: str

side instance-attribute

side: str

strike_role instance-attribute

strike_role: str

OptionsMultiLegPositionDecision dataclass

N-leg sibling of OptionsSpreadPositionDecision - generalized to any leg count/ratio and to 1 OR 2 distinct expiries (the calendar family), rather than OptionsSpreadPositionDecision's fixed 2-leg/1-expiry shape. A SEPARATE dataclass, not a generalization of it - every existing caller of OptionsSpreadPositionDecision (bull_call_spread/ bear_put_spread's own dedicated sizing path) is completely untouched.

contracts instance-attribute

contracts: int

expiries instance-attribute

expiries: tuple[str, ...]

legs instance-attribute

legs: tuple[OptionsSpreadLeg, ...]

net_debit_or_credit instance-attribute

net_debit_or_credit: float

net_delta instance-attribute

net_delta: float

net_vega instance-attribute

net_vega: float

sizing_reason instance-attribute

sizing_reason: str

strategy_name instance-attribute

strategy_name: str

to_dict

to_dict() -> dict[str, Any]

Same manual-field-access convention as every other decision dataclass in this module - never dataclasses.asdict().

OptionsPositionDecision dataclass

One option order: contracts, right, strike, expiry and the reasons for it.

actual_delta instance-attribute

actual_delta: float

contract_symbol class-attribute instance-attribute

contract_symbol: object = None

contracts instance-attribute

contracts: int

expiry instance-attribute

expiry: str

right instance-attribute

right: str

sizing_reason instance-attribute

sizing_reason: str

strike instance-attribute

strike: float

target_delta instance-attribute

target_delta: float

vega_budget_used instance-attribute

vega_budget_used: float

to_dict

to_dict() -> dict[str, Any]

Deliberately NOT dataclasses.asdict(self) - asdict() deep-copies every field, and a raw Lean Symbol (a C#/.NET interop object) isn't guaranteed deepcopy-safe. Manual field access avoids that risk entirely and stringifies contract_symbol so the result is always JSON-serializable - required for main.py's dashboard state writer (see main.py::on_data()'s "dynamic_sizing_for_state" sanitization, which calls this before signals[...] ever reaches json.dumps()).

OptionsSpreadLeg dataclass

One leg of a 2-leg vertical spread. side is "long"/"short" (never a signed quantity) - keeps this symmetric with how Lean's own OptionStrategies/Leg model a spread: a strike, a side, a contract.

contract_symbol class-attribute instance-attribute

contract_symbol: object = None

right instance-attribute

right: str

side instance-attribute

side: str

strike instance-attribute

strike: float

OptionsSpreadPositionDecision dataclass

2-leg vertical spread sibling of OptionsPositionDecision - a SEPARATE dataclass, not a generalization of it (OptionsPositionDecision stays single-leg-shaped, every existing caller untouched).

contracts instance-attribute

contracts: int

expiry instance-attribute

expiry: str

legs instance-attribute

legs: tuple[OptionsSpreadLeg, OptionsSpreadLeg]

net_debit_or_credit instance-attribute

net_debit_or_credit: float

net_delta instance-attribute

net_delta: float

net_vega instance-attribute

net_vega: float

sizing_reason instance-attribute

sizing_reason: str

strategy_name instance-attribute

strategy_name: str

to_dict

to_dict() -> dict[str, Any]

Same manual-field-access convention as OptionsPositionDecision.to_dict() above - never dataclasses.asdict(), the legs' contract_symbol fields are real Lean Symbol objects, not deepcopy-safe.

ResearchPanel dataclass

What sleeves may read: known closes by session, each ticker's class, and FRED series as {key: dated rows}.

asset_class instance-attribute

asset_class: Mapping[str, str]

extras class-attribute instance-attribute

extras: Mapping[str, DataFrame] = field(default_factory=dict)

fred class-attribute instance-attribute

fred: Mapping[str, Series] = field(default_factory=dict)

known instance-attribute

known: DataFrame

truncated

truncated(end: str) -> ResearchPanel

SleeveGuardConfig dataclass

Settings of a sleeve's loss guard: on or off, the loss limit as a share of NAV and the lookback.

cooldown_bars class-attribute instance-attribute

cooldown_bars: int = 60

enabled class-attribute instance-attribute

enabled: bool = False

lookback_bars class-attribute instance-attribute

lookback_bars: int = 60

max_loss_fraction_nav class-attribute instance-attribute

max_loss_fraction_nav: float = 0.01

SleeveSpec dataclass

A sleeve to research or trade: its id, track, kind (time series or cross-sectional), family, universe, rebalance cadence and parameters.

family instance-attribute

family: str

id instance-attribute

id: str

kind instance-attribute

kind: str

params class-attribute instance-attribute

params: Mapping[str, float] = field(default_factory=dict)

rebalance_every instance-attribute

rebalance_every: int

track instance-attribute

track: str

universe instance-attribute

universe: tuple[str, ...]

StrategySpec dataclass

One registry entry - everything main.py's generic order-placement layer needs to resolve and call the real OptionStrategies factory, plus everything the shape-family selectors below need to pick strikes/ expiries for it. factory_name is resolved via getattr(OptionStrategies, factory_name) at the main.py call site (never imported per-strategy here - this module must not import Lean's AlgorithmImports, matching every other pure function in this file).

arg_order instance-attribute

arg_order: tuple[str, ...]

covering_equity_side class-attribute instance-attribute

covering_equity_side: str | None = None

factory_name instance-attribute

factory_name: str

has_expiry_pair instance-attribute

has_expiry_pair: bool

legs instance-attribute

legs: tuple[MultiLegSpec, ...]

risk_tier instance-attribute

risk_tier: str

shape_family instance-attribute

shape_family: str

TargetWeightsConfig dataclass

Settings of the target-weights path: the weights file and its pin, deadbands, minimum trades, fees, slippage and financing.

borrow_bps class-attribute instance-attribute

borrow_bps: float = 25.0

enabled class-attribute instance-attribute

enabled: bool = False

equity_fee_bps class-attribute instance-attribute

equity_fee_bps: float = 1.5

equity_min_fee_usd class-attribute instance-attribute

equity_min_fee_usd: float = 1.0

financing_enabled class-attribute instance-attribute

financing_enabled: bool = True

forex_min_trade_usd class-attribute instance-attribute

forex_min_trade_usd: float = 5000.0

fx_spread_bps class-attribute instance-attribute

fx_spread_bps: float = 50.0

live_reload class-attribute instance-attribute

live_reload: bool = False

margin_spread_bps class-attribute instance-attribute

margin_spread_bps: float = 150.0

min_trade_usd class-attribute instance-attribute

min_trade_usd: float = 100.0

min_weight_change class-attribute instance-attribute

min_weight_change: float = 0.002

no_bar_exit class-attribute instance-attribute

no_bar_exit: bool = False

no_bar_exit_after class-attribute instance-attribute

no_bar_exit_after: int = 2

no_bar_haircut_by_class class-attribute instance-attribute

no_bar_haircut_by_class: Mapping[str, float] = field(default_factory=lambda: dict(DEFAULT_NO_BAR_HAIRCUT))

reserve_gross class-attribute instance-attribute

reserve_gross: float = 0.3

slippage_bps_by_class class-attribute instance-attribute

slippage_bps_by_class: Mapping[str, float] = field(default_factory=lambda: dict(DEFAULT_SLIPPAGE_BPS))

uses_model class-attribute instance-attribute

uses_model: bool = False

weights_path class-attribute instance-attribute

weights_path: str = ''

weights_sha256 class-attribute instance-attribute

weights_sha256: str = ''

TrendSleeveConfig dataclass

Settings of the trend sleeve: on or off, its symbols, the signal and the volatility target.

enabled class-attribute instance-attribute

enabled: bool = False

gross_cap class-attribute instance-attribute

gross_cap: float = 0.25

history_needed property

history_needed: int

Closes a symbol needs before it can carry a signal and a volatility.

max_weight_per_name class-attribute instance-attribute

max_weight_per_name: float = 0.08

min_weight_change class-attribute instance-attribute

min_weight_change: float = 0.005

rebalance_every_bars class-attribute instance-attribute

rebalance_every_bars: int = 20

signal class-attribute instance-attribute

signal: str = 'sign_252'

symbols class-attribute instance-attribute

symbols: tuple[str, ...] = ()

target_vol class-attribute instance-attribute

target_vol: float = 0.03

vol_window class-attribute instance-attribute

vol_window: int = 60

WeightSettings dataclass

How a sleeve's signal becomes weights: the volatility target, the gross cap and the per-name and per-class caps.

class_caps class-attribute instance-attribute

class_caps: Mapping[str, float] = field(default_factory=lambda: {'crypto': 0.15})

gross_cap class-attribute instance-attribute

gross_cap: float = 1.4

name_cap class-attribute instance-attribute

name_cap: float = 0.15

name_cap_by_class class-attribute instance-attribute

name_cap_by_class: Mapping[str, float] = field(default_factory=lambda: {'crypto': 0.05})

target_vol class-attribute instance-attribute

target_vol: float = 0.07

advance_strategy_value

advance_strategy_value(state: dict[str, Any] | None, total_value: float, sweep_profit: float) -> tuple[dict, float]

The value the risk controls read: an index that compounds the strategy's own P&L (the portfolio's change minus the sweep's) over the TOTAL value the strategy is sized on, starting at the first total value seen. Subtracting the cumulative carry from the total instead (V6.2.0's first BT1) divides the strategy's dollar P&L by a smaller base than it was sized on: ~8% inflated returns and drawdown speed by mid-2024, enough to trip the kill switch on a day V6.1 did not. The offline simulator's kill switch reads the same quantity (book + legacy return as a fraction of NAV). Returns (new state, value); a non-finite or non-positive input keeps the previous value.

align_score_table

align_score_table(table: DataFrame, asset_class: Mapping[str, str]) -> pd.DataFrame

A score table keyed by dataset date: a crypto or forex score dated D reads bar D, which closes after D's equity decision, so it may only be used a row later. test_no_lookahead can't see into a precomputed table.

amihud

amihud(known: DataFrame, quote_volume: DataFrame, window: int = 30) -> pd.DataFrame

Illiquidity: the trailing mean of |return| per unit of quote volume. A zero or missing volume leaves that day out; half a window is the minimum.

announcement_proximity

announcement_proximity(rows: DataFrame, index: Index, columns: Index, *, window_days: int = 14) -> pd.DataFrame

1.0 during the window_days calendar days before a name's expected announcement (about ten sessions), 0.0 otherwise, NaN for a name with no earlier announcement. The expected date is an announcement 52 weeks earlier (Frazzini and Lamont's announcement premium), so no filing at or after the session is read. An expected date beyond the last session flags only the sessions that really lie inside its window.

announcement_return

announcement_return(rows: DataFrame, known: DataFrame, *, days: int = 3, decay: int = 60, bench: str = 'SPY') -> pd.DataFrame

The abnormal return over days rows from the announcement (Brandt, Kishore, Santa-Clara and Venkatachalam), held for decay rows once it is complete. Row start holds the close before the announcement day, row start + days the close of its last reaction day. Blank until those days have passed, and for bench itself.

annualised_vol

annualised_vol(closes: Sequence[float], window: int, annualization: float = 252.0) -> float | None

Sample std of the last window daily log returns x sqrt(annualization); None if too short, flat or invalid.

apply_book_neutrality

apply_book_neutrality(raw_weights_by_symbol: dict[str, float], *, sector_by_symbol: dict[str, str], dollar_neutral: bool, sector_neutral: bool, gross_exposure_cap: float, max_weight_per_name: float, sector_max_net_weight: float, reapply_sector_cap_after_dollar_neutral: bool = False) -> tuple[dict[str, float], dict]

Deterministic 4-step pipeline over an already-selected book's raw per-symbol weights (main.py's today-formula: min(max_position_weight, 0.10 + 0.15*confidence) * book_role_multiplier, one entry per book-selected symbol only - not the whole universe).

Steps, always in this order: 1. Per-name cap: clip every weight to [-max_weight_per_name, +max_weight_per_name]. 2. Sector neutrality (if sector_neutral): within each sector bucket, if |bucket net| exceeds sector_max_net_weight, SHRINK the whole bucket (every member, proportionally, sign preserved, never amplified) so its net lands exactly at the cap. A bucket already within the cap is left untouched.

 Deliberately NOT "demean to exact zero" (the pre-V5.1.3 behavior -
 see Problems.md #81). Demeaning only preserves information when a
 bucket has genuine within-bucket weight dispersion to fall back
 on. Both of this module's callers (main.py's book formula,
 evaluation/rank_book_simulator.py) equal-weight every name within
 a role, so a bucket that is monolithic to one role - e.g. every
 Forex ticker maps to a single "Forex" bucket, and Forex can never
 appear on the opposite leg - has NO within-bucket dispersion:
 demeaning drove every member to exactly zero, silently erasing the
 entire position instead of bounding a genuine sector tilt.
 Shrink-toward-cap degrades gracefully in every case: an already-
 balanced bucket is untouched, a lopsided or monolithic bucket is
 shrunk (never erased) toward the cap, and a single-member bucket
 (net == its own weight) needs no special case - same code path.
  1. Dollar neutrality (if dollar_neutral): scale the LARGER leg down so sum(long) == sum(|short|). Scale-down only, never up - this can never increase gross exposure. A one-sided book (no shorts, or no longs) has nothing to balance against; that step is skipped for that bar rather than zeroing the book out, and the skip is recorded in diagnostics.
  2. Gross cap: scale everything by min(1, gross_exposure_cap / gross).

Step 3 shrinks one leg only, so a sector holding both a long and a short can end above its cap (proved in formal/AetherFormal/BookNeutrality.lean, sector_cap_counterexample). reapply_sector_cap_after_dollar_neutral (default False, the shipped behaviour) runs step 2 again after step 3: every sector then ends within its cap, and the book may be left slightly off dollar-neutral (recorded as "sector_cap_reapplied").

A non-positive max_weight_per_name skips step 1 like the other two caps (V6.4.3: a cap of 0 zeroed the book and a negative one turned every weight, shorts included, into a long of |cap|).

Returns (weights, diagnostics) where diagnostics is JSON-serializable (state.json's state["book_neutrality"]): {"pre_gross", "post_gross", "net_before", "net_after", "per_sector_net": {sector: net_weight, ...}, "steps_applied": [...]}

Never raises. An empty input returns ({}, diagnostics-with-zeros). Pure dict-in/dict-out - result does not depend on input dict iteration order (every step is a symmetric sum/mean over the group, not a positional operation).

apply_book_weight_floor

apply_book_weight_floor(target_weight: float, base_target_weight: float, floor_fraction: float, skip_floor: bool = False) -> float

V5.5.0 (Problems.md #128) - bound how far the multiplier chain can shrink a book member below the weight the book chose for it.

The offline simulation trades the book's own weight (~8% a name); live stacks volatility (0.35-1.25), confidence (0.5-1.0), topology (0.5-1.0), cost (>=0.25) and liquidity (0.35) multipliers on top, which left equity at ~3% of NAV. The floor is symmetric (|weight| only, sign kept) so it cannot tilt the long/short balance, and is a no-op at floor_fraction <= 0 (the pre-V5.5.0 behavior). skip_floor is for the liquidity "reduce_size" haircut, which is a safety cut that must never be undone. Never raises the weight above |base_target_weight|.

apply_class_weight_caps

apply_class_weight_caps(weights: dict[str, float], asset_class_by_symbol: dict[str, str | None], caps: dict[str, dict]) -> dict[str, float]

V5.8.0: bounds the book weights of a class — each name to per_name_cap, then the class to class_gross_cap (proportional scale-down) — leaving exempt names (the proven core, e.g. BTC/LTC) and every other class untouched. Used for the names that must trade without a proven edge (the 10 crypto pairs that became eligible with the extended window). Capping a leg of a dollar-neutral book can leave a small net exposure; the sizing chain and the kill switch run after this, exactly as live and as-live both do. Pure; input is not mutated.

apply_direction_overlay

apply_direction_overlay(ranks: dict[str, float], probabilities: dict[str, float | None], params: dict | None, group_by_symbol: dict[str, str] | None = None) -> dict[str, float]

The overlay on one bar's percentile ranks ({symbol: rank}); params from resolve_direction_overlay (None or an empty rank dict returns ranks untouched). Symbols without a probability keep their rank (z = 0 / no filter).

apply_hrp_weights

apply_hrp_weights(book_allocations: dict[str, BookAllocation], returns_by_symbol: dict[str, list[float]], gross_exposure: float = 1.0) -> dict[str, float]

V5.4.4 - HRP book SIZING over an already-selected book. Replaces the per-symbol confidence formula (min(max_position_weight, 0.10+0.15*confidence) * book_role_multiplier) with Hierarchical Risk Parity inverse-variance weights derived from the selected symbols' return correlation structure - SAME symbols, SAME roles, different sizing only. Selection (build_rank_based_book()) always runs first and is never affected; this function must be called with its output.

returns_by_symbol is {symbol_key: [close-to-close returns]} - main.py builds it from each selected symbol's own rolling window (self.symbol_windows, the 25-bar deque) via pct_returns_from_closes(); symbols missing from the dict (or with < 2 usable returns) are simply excluded from the covariance estimation by build_hrp_allocation()'s own filtering, never a KeyError.

Per-leg weight contract (mirrors portfolio/hrp_allocation.py:: build_hrp_allocation(), which this delegates to): with BOTH legs populated each leg sums to +/-gross_exposure/2 (dollar-neutral book); a single-sided book (long_flat strategy_mode, bottom_n=0) gives the present leg the FULL gross. Insufficient history (< 2 symbols with usable returns) or an HRP convergence failure degrades to equal weight within each leg - sizing always covers exactly the selected symbol set, {} only when book_allocations itself is empty.

Deliberately a lazy import of portfolio.hrp_allocation inside the function body: that module imports scipy at module scope, and this keeps book_construction.py (imported by main.py on EVERY run, HRP engaged or not) scipy-free on the default "rank" path - the same isolator-timeout/import-weight concern that keeps evaluation/ rank_ic_core.py torch-free (development/Problems.md #16/#102).

apply_legacy_sleeve_policy

apply_legacy_sleeve_policy(signal_name: str, target_weight: float, is_book_member: bool, is_currently_invested: bool, book_is_active: bool, sleeve_gross_used: float, sleeve_config: dict[str, Any], is_book_owned: bool = False) -> dict[str, Any]

Decide how much of a non-book symbol's legacy signal may trade.

Returns {"signal", "target_weight", "reason", "sleeve_gross_used"}. - A NEW entry ("buy"/"short" on a flat, non-book symbol) is admitted only inside the gross budget and the per-name cap. The returned sleeve_gross_used includes it, so the caller threads it through the rest of the bar's loop and the budget is atomic across symbols. - A HELD sleeve position (invested, not book-owned) that the legacy signal wants to size up is capped at the per-name weight - otherwise the 2% entry would be scaled to the signal's full ~8% on the next bar. - Book members, positions the BOOK opened (is_book_owned - during a veto they are non-members too, and must not be shrunk to a sleeve weight), exits ("sell") and "hold" pass through untouched.

apply_min_change

apply_min_change(previous: Mapping[str, float], target: Mapping[str, float], min_change: float) -> dict[str, float]

Keeps a held weight when the new target moves it by less than min_change (fewer small, fee-heavy trades). A symbol whose target is missing or zero always closes; a new symbol always opens.

assignment_risk_flag

assignment_risk_flag(score: float, threshold: float) -> bool

Pure threshold predicate - the sweep-trigger boundary.

assignment_risk_score

assignment_risk_score(moneyness: float | None, right: str, extrinsic_value: float | None, expected_dividend_amount: float | None, days_to_next_ex_div: int | None, window_days: int) -> float

0.0-1.0 composite early-call-assignment-risk score.

Call-only by construction: a put always scores 0.0 (an upcoming dividend never makes early exercise of a long put rational - dividends only ever pull CALL exercise forward). Also 0.0 whenever: the leg isn't in-the-money (moneyness <= 1.0), there's no known upcoming ex-dividend date within window_days (including a negative days_to_next_ex_div - already past due, stale schedule), window_days itself is non-positive, or extrinsic_value/expected_dividend_amount is unknown (None) or expected_dividend_amount is non-positive.

Within the risk window: scores at the time-proximity ceiling when early_exercise_optimal_for_dividend() is already True (extrinsic value below the expected dividend); otherwise decays smoothly via this module's own _normal_cdf() as the extrinsic-value cushion over the dividend widens (reusing this codebase's existing probabilistic vocabulary rather than inventing an ad hoc formula), further scaled by how close the ex-dividend date actually is within the window. Never raises.

atm_implied_volatility

atm_implied_volatility(available_chain: list[dict], expiry: str) -> float | None

Nearest-to-spot call+put average IV for one expiry - "nearest to spot" approximated here as the LOWEST |delta - 0.5| call combined with its same-strike put (a call delta near 0.50 is the standard ATM proxy when a real spot/strike distance isn't threaded through this function). Returns None when the expiry has no usable call (chain empty, IB disabled, or no "iv"/"implied_volatility" field present) - never a crash, matching this module's degrade-to-absent convention.

bond_credit_timing

bond_credit_timing(panel: ResearchPanel, spec: SleeveSpec) -> pd.DataFrame

Long credit against duration while the BAA spread is narrowing over the last quarter, and the reverse.

bond_slope_timing

bond_slope_timing(panel: ResearchPanel, spec: SleeveSpec) -> pd.DataFrame

Long duration against the short end while the 10y-3m slope is above its one-year mean, and the reverse.

book_history_mode_allowed

book_history_mode_allowed(runtime_mode: str, enabled: bool, paper_enabled: bool = False) -> tuple[bool, bool]

(write the book history this rebalance?, bounded record). Backtests log the full record; paper_enabled (V6.4.0, default off) also logs paper runs with the bounded record (no full universe, no feature snapshot: one small line per rebalance); live never logs. Disabled = nothing.

box_spread_fair_value

box_spread_fair_value(higher_strike: float, lower_strike: float, risk_free_rate: float, time_to_expiry_years: float) -> float

A box spread (long put + short call at the higher strike, short put + long call at the lower strike, same expiry) synthesizes a risk-free zero-coupon bond paying (higher_strike - lower_strike) at expiry - its fair value today is that payoff discounted at the risk-free rate. Standard textbook result.

breadth_timing

breadth_timing(known: DataFrame, liquid: DataFrame, window: int = 50, floor: float = 0.5, core: tuple[str, ...] = CORE) -> pd.DataFrame

1.0 on the core coins while at least floor of the liquid coins trade above their own window-session average, else 0.0. NaN elsewhere and while breadth can't be measured.

build_arbitrage_position_sizing

build_arbitrage_position_sizing(strategy_name: str, available_chain: list[dict], current_date: date, risk_free_rate: float, min_mispricing_bps: float, max_contracts_per_signal: int = 1, target_delta: float = 0.5) -> OptionsMultiLegPositionDecision | None

The ONLY sizing path for the 6 arbitrage strategies - deliberately NOT vega-budget or margin-tier sized (an arbitrage structure is delta/vega-neutral by construction, so a vega budget has nothing meaningful to scale against). Sized instead to a small, FIXED max_contracts_per_signal (default 1) whenever select_arbitrage_signal() confirms a real mispricing this bar - a deliberately conservative first-cut sizing model for a genuinely new capability, not a risk-budgeted one; a real margin-aware arbitrage sizer is a reasonable future follow-up once this is IB-verified. Returns None when no mispricing is detected, selection fails, or max_contracts_per_signal isn't positive - never raises.

build_book_history_record

build_book_history_record(date_str: str, book_allocations: dict[str, BookAllocation], raw_scores_by_symbol: dict[str, float], target_weights_by_symbol: dict[str, float], sector_by_symbol: dict[str, str], rank_signal_policy: dict[str, Any], full_universe_signals: dict[str, dict] | None = None, book_member_decisions: dict[str, dict] | None = None, gate_veto_reason: str | None = None, feature_snapshot: dict[str, dict] | None = None, run_id: str | None = None, rolling_ic_state: dict[str, Any] | None = None, member_probabilities: dict[str, dict] | None = None) -> dict[str, Any]

V5.2.2 (development/Problems.md) - a diagnostic snapshot of one rebalance bar's book, for main.py's optional (config-gated, off by default, backtest-only) book-history log. Exists to make a real, ground-truth reconciliation possible between what a live/backtest run actually selected and what evaluation/rank_book_simulator.py's offline simulator would select for the identical date - the tool this project needed after six other hypotheses for a large live-vs- offline Sharpe gap were tested and ruled out without finding the cause (see Problems.md #89-#90).

Pure: no I/O, no self, never raises on well-formed inputs - every per-symbol field degrades via .get(..., None) rather than KeyError, since a symbol missing from raw_scores_by_symbol or target_weights_by_symbol (e.g. target_weight is never populated when phase_v2.portfolio_book.neutrality.enabled is False - see BookAllocation.target_weight's own None-means-"not applicable" convention) is expected, not exceptional.

rank_signal_policy (main.py's own self._rank_signal_policy, resolved once in Initialize()) is embedded INLINE in every record, not referenced externally - deliberately, so the log stays self- contained and correctly interpretable even if config or training metrics change between when a run produced this line and when a later reconciliation reads it back.

full_universe_signals (V5.2.3, opt-in via phase_v2.diagnostics.book_history.include_full_universe): None (default) reproduces this function's exact V5.2.2 return shape - no "universe" key at all, so every existing caller/log consumer is unaffected. When given {symbol: {"raw_rank_score", "feature_ready", "reason", "trading_eligible", "security_type"}} (main.py's own Phase 1a signals payload, one entry per symbol with a bar this tick, selected or not), adds a "universe" key with the same shape - closes the exact gap that made the V5.2.2 tool unable to see WHY a symbol was never selected (e.g. crypto/FX consistently absent from every live book - Problems.md #91): the V5.2.2 log only ever recorded book_allocations, i.e. symbols that already made the cut, with no way to see what score/readiness a NON-selected symbol had that day. Each per-symbol sub-dict degrades the same .get(..., None) way as every other field here - a caller-supplied dict missing a key never raises.

book_member_decisions (V5.2.6, opt-in via phase_v2.diagnostics.book_history.include_decisions): None (default) reproduces this function's exact pre-V5.2.6 return shape - no "book_member_decisions" key at all, same convention as full_universe_signals. When given {symbol: MarketAnalysisDecision.to_dict()} (main.py's own Pass 2, collected per book member as each symbol's decision is computed), adds a "book_member_decisions" key recording each book member's final action/reasons - closes the gap where a symbol could be logged here as "selected" via book_allocations even though it was later diverted to simulate/reduce_risk/retrain_candidate by a risk/execution gate book_allocations alone can never reveal (see development/Problems.md - the crypto/FX zero-order-execution finding).

gate_veto_reason (V5.3.5, development/Problems.md #102): None (default) omits the key entirely, same convention as the two fields above. When main.py calls this on a genuine rebalance bar whose build_rank_based_book() call came back empty, it passes the veto_reason_out["reason"] string that call populated (see _select_book_group()'s own docstring) - without this, a vetoed rebalance bar left ZERO trace in this log (the write was gated on book_allocations being truthy), making it impossible to tell from the log alone whether a disengaged stretch came from min_rank_confidence_spread or the new rolling-IC gate, or anything else. main.py now writes a record on every genuine rebalance bar regardless of whether book_allocations came back empty.

feature_snapshot (V5.3.5.3 Workstream B, development/Problems.md

91/#100): None (default) omits the key entirely, same convention

as the three fields above. When given {symbol: {feature: value}} (main.py's own pass1_state[symbol]["feature_payload"]["base_features"] dict, captured ONLY for the symbols in phase_v2.diagnostics.book_history.feature_snapshot_symbols's allowlist and only when phase_v2.diagnostics.book_history.include_feature_snapshot is on), adds a "feature_snapshot" key - the raw per-feature values the live model actually saw for those symbols that bar. Exists so evaluation/feature_reconciliation.py::reconcile_feature_snapshot() can diff these against the same (ticker, date) row of ml/datasets/full_dataset.csv: XOM is the one tracked ticker whose live-vs-offline book-selection mismatch never resolved across

91/#97/#99/#100, and until this field existed there was nothing in

the log to diff feature-by-feature. Allowlist-bounded by design - without it every bar would serialize ~77 symbols x dozens of floats, ballooning this log for a diagnostic only ever needed for a handful of tickers.

run_id (V5.7.0): the backtest's start stamp; None omits the key. Lets the audit and reconcile tools select a run by id instead of guessing run boundaries from a date that goes backwards.

build_book_raw_weights

build_book_raw_weights(book_allocations: Mapping[str, BookAllocation], max_position_weight: float) -> dict[str, float]

V5.5.0 (Problems.md #128) - the live book's pre-neutrality weight per symbol: min(max_position_weight, 0.10 + 0.15 * confidence) * role multiplier, confidence = min(1, |rank - 0.5| * 2). Extracted from main.py's inline loop so the offline simulator's as-live mode feeds apply_book_neutrality() the IDENTICAL raw weights instead of equal weights (parity rule, AGENTS.md). Non-finite rank -> skipped.

build_class_sleeve_weights

build_class_sleeve_weights(allocations: Mapping[str, BookAllocation], gross_budget: float, max_weight_per_name: float | None = None) -> dict[str, float]

Signed target weight (fraction of NAV) per selected name: each leg gets gross_budget / 2, split evenly over its names, optionally capped per name. A one-sided book (no shorts, no longs) gets {} - a class sleeve is dollar-neutral by construction, never a directional bet on the class. Non-finite or non-positive budget -> {}.

build_covered_protective_position_sizing

build_covered_protective_position_sizing(strategy_name: str, confidence: float, available_chain: list[dict], equity_quantity: float, contract_multiplier: float = 100.0, target_delta_at_full_confidence: float = 0.3) -> OptionsMultiLegPositionDecision | None

Sizes covered_call/covered_put/protective_call/protective_put/ protective_collar's option leg(s) purely as floor(|equity_quantity| / contract_multiplier) - the number of contracts the CURRENTLY HELD equity position actually covers, never a vega/confidence-scaled target of its own (§9.3's floor-rounding: any fractional remainder is carried as plain uncovered equity, never partially hedged).

equity_quantity must carry the correct SIGN for this strategy's covering_equity_side (positive/long for covered_call, protective_put, protective_collar; negative/short for covered_put, protective_call) - a mismatched sign (e.g. a short equity holding passed for covered_call, which needs a LONG holding) returns None rather than sizing against the wrong side of the book. Returns None for any strategy_name whose risk_tier isn't "covered_protective", for confidence <= 0, or when floor rounding or leg selection yields nothing - never raises.

build_forex_sleeve_weights

build_forex_sleeve_weights(allocations: Mapping[str, BookAllocation], gross_budget: float, max_weight_per_pair: float | None = None) -> dict[str, float]

Signed target weight (fraction of NAV) per selected pair: each leg gets gross_budget / 2, split evenly over its names, optionally capped per pair. A one-sided book (no shorts, no longs) gets {} - the sleeve is dollar-neutral by construction, never a directional currency bet. Non-finite or non-positive budget -> {}.

build_margin_position_sizing

build_margin_position_sizing(strategy_name: str, signal_direction: str, confidence: float, available_chain: list[dict], underlying_price: float, portfolio_value: float, contract_multiplier: float = 100.0, target_delta_at_full_confidence: float = 0.6, target_margin_utilization: float = 0.2, max_margin_utilization: float = 0.4, pct_of_underlying_value: float = 0.2, min_pct_of_underlying_value: float = 0.1, **selector_tuning_kwargs: Any) -> MarginSizingDecision | None

The single public entry point for every margin-tier strategy (risk_tier in {"margin_naked", "margin_uncovered_leg", "margin_bounded_backspread"}) - dispatches leg selection via options_strategy.select_strategy_legs() (the SAME registry-driven selector every vega-budget strategy uses, so a ladder's or backspread's strikes are chosen identically regardless of which sizing model ultimately budgets it), then sizes via the sub-model its risk_tier maps to. Returns None for confidence/portfolio_value <= 0, an invalid signal_direction, a non-margin-tier strategy_name (sized by options_strategy.py instead), or when selection/margin-budget rounds to zero contracts - never raises.

build_margin_position_sizing_for_legs

build_margin_position_sizing_for_legs(strategy_name: str, held_rows_by_role: dict[str, dict], underlying_price: float, portfolio_value: float, contract_multiplier: float = 100.0, max_margin_utilization: float = 0.4, pct_of_underlying_value: float = 0.2, min_pct_of_underlying_value: float = 0.1) -> MarginSizingDecision | None

Resize-in-place sibling of build_margin_position_sizing() - sizes an ALREADY-HELD margin-tier position on its legs' own current chain rows, skipping selection entirely (held_rows_by_role must carry an entry for every role in MULTI_LEG_STRATEGY_REGISTRY[strategy_name].legs, same shape select_strategy_legs() returns). No confidence-scaled soft target here - the position already exists, so sizing is to the full max_margin_utilization ceiling directly, the same "size to budget on already-resolved greeks, no fresh confidence input" convention options_strategy.py's build_multi_leg_position_sizing_for_legs() and build_options_position_sizing_for_contract() already use.

build_multi_leg_position_sizing

build_multi_leg_position_sizing(strategy_name: str, signal_direction: str, confidence: float, available_chain: list[dict], portfolio_value: float, target_delta_at_full_confidence: float = 0.6, max_vega_budget_pct_of_equity: float = 0.02, grouped_chain_by_expiry: dict[str, list[dict]] | None = None, **selector_tuning_kwargs: Any) -> OptionsMultiLegPositionDecision | None

The single public entry point for every NEW (V4.5) vega-budget-tier strategy - dispatches leg selection via select_strategy_legs(), then sizes via _size_multi_leg(). Returns None for any margin-tier or unreachable-arbitrage strategy_name (those are sized by portfolio/options_margin_sizing.py, or never sized at all, respectively) - a caller passing the wrong tier's strategy_name here gets a clean None, not a wrong/unsafe budget calculation.

grouped_chain_by_expiry (V4.9 Priority 3) passes straight through to select_strategy_legs() - see its own docstring. Default None reproduces the exact original per-call regroup behavior.

build_multi_leg_position_sizing_for_legs

build_multi_leg_position_sizing_for_legs(strategy_name: str, held_rows_by_role: dict[str, dict], portfolio_value: float, max_vega_budget_pct_of_equity: float = 0.02) -> OptionsMultiLegPositionDecision | None

Resize-in-place sibling of build_multi_leg_position_sizing() - sizes an ALREADY-HELD position on its legs' own current greeks, skipping selection entirely. held_rows_by_role must carry an entry for every role in MULTI_LEG_STRATEGY_REGISTRY[strategy_name].legs (same shape select_strategy_legs() returns) - same degrade-to-None contract as build_multi_leg_position_sizing() for non-positive portfolio_value or non-positive net vega.

build_options_position_sizing

build_options_position_sizing(signal_direction: str, confidence: float, available_chain: list[dict], portfolio_value: float, target_delta_at_full_confidence: float = 0.6, max_vega_budget_pct_of_equity: float = 0.02) -> OptionsPositionDecision | None

Translates the existing direction+confidence prediction into ONE single-leg options position. signal_direction == "buy" selects calls; {"sell", "short"} select puts (matches portfolio/book_construction.py's own "buy"/"short" role vocabulary and main.py::_apply_signal()'s signal_name values).

target_delta scales linearly with confidence: target_delta_at_full_confidence * confidence. contract count is capped so total position vega stays under max_vega_budget_pct_of_equity * portfolio_value / contract_vega - vega (not notional) is the risk unit an options book should budget against, since a small-notional far-OTM option can carry disproportionate vega risk relative to its price.

Returns None when: confidence is 0, signal_direction doesn't map to a right, available_chain has no usable contract for that right (e.g. IB disabled -> empty chain, or select_single_leg_contract() found no match), the selected contract's vega is non-positive, or portfolio_value is non-positive - degrade-to-absent, never a crash, matching risk/futures_risk.py::build_futures_position_sizing()'s "no active signal -> None/zero" convention.

build_options_position_sizing_for_contract

build_options_position_sizing_for_contract(held_contract: dict[str, Any], portfolio_value: float, max_vega_budget_pct_of_equity: float = 0.02) -> OptionsPositionDecision | None

V4.4 - sizes the EXACT held contract on its own current greeks, skipping select_single_leg_contract() entirely (no re-selection).

Closes a real architectural gap: without this, a position whose ideal strike has drifted away from what main.py is currently holding could only ever be handled by RE-SELECTING a contract (which picks a DIFFERENT one, forcing a binary rotate-or-freeze choice) - this lets the held position keep being managed - scaled up or down - on its own delta/vega, on bars where main.py chooses not to rotate.

held_contract is a live chain row for the currently-held contract - same shape select_single_leg_contract() returns (must carry right/strike/expiry/delta/vega/symbol, e.g. found by matching the held contract_symbol against this bar's available_chain). There is no fresh confidence-scaled target here - the position already exists - so target_delta/actual_delta are both just the held contract's own current delta.

Same degrade-to-None contract as build_options_position_sizing(): non-positive portfolio_value, missing/non-positive vega, or a budget that rounds to zero contracts all return None, never raise.

build_rank_based_book

build_rank_based_book(book_candidates: dict[str, dict], top_n: int, bottom_n: int, min_rank_confidence_spread: float = 0.0, per_asset_class_slots: dict[str, tuple[int, int]] | None = None, previous_allocations: dict[str, BookAllocation] | None = None, hysteresis_rank_margin: float = 0.0, spread_check_ranks: dict[str, float] | None = None, rolling_ic_gate_result: dict[str, Any] | None = None, veto_reason_out: dict[str, Any] | None = None) -> dict[str, BookAllocation]

Discrete top-N-long / bottom-N-short book construction from each symbol's predicted_rank_20d for this bar. Continuous rank-weighted sizing (rather than a hard top/bottom-N cutoff) is a documented future extension, not built here - the discrete version is simpler to reason about and test first.

book_candidates is {symbol: {"predicted_rank_20d": float | None, "trading_eligible": bool, "asset_class": str | None, ...}} - one entry per symbol Pass 1 of main.py::on_data() collected this bar (extra keys like probability_up/confidence are ignored here, kept only so callers can pass the same dict they already built for other purposes). A symbol is eligible for book selection only if trading_eligible is true (this is exactly how phase9.asset_quality's observation-only assets get excluded - main.py already computes this per-symbol flag identically for every other decision) and predicted_rank_20d is not None (model unavailable/still warming up that bar).

bottom_n == 0 requests a deliberate long-only book (no shorts at all) - used by main.py to honor phase5.backtest.strategy_mode == "long_flat" while still getting rank-driven entry/rotation on the long side. Any bottom_n > 0 still requires at least one eligible symbol on EACH side to form a book at all - a one-sided book isn't attempted when a short side was actually requested but nothing was available for it; returns {} in that case, same as every other degenerate-input case below. top_n/bottom_n (or each asset class's own pair, see per_asset_class_slots below) exceeding the number of eligible symbols degrades gracefully to however many are actually available (never raises) - same "pad/truncate rather than error on a thin universe" convention as topology/market_topology.py's rank_correlated_peers().

min_rank_confidence_spread is a floor on (mean long-side rank - mean short-side rank) before a book (or, with per_asset_class_slots, each individual asset class's own book) engages at all - on a day where the ranked pool's predicted ranks are all clustered near 0.5 (no real dispersion), forcing a long/short split would be noise, not signal; the book disengages entirely (returns {} for that pool) rather than trading a meaningless split. Every symbol then falls through to whatever non-book decision main.py's Pass 2 would have made anyway - byte-identical to this module not existing at all, the same "missing/ degraded signal never changes trading behavior" contract this codebase's other optional overlays already guarantee.

per_asset_class_slots (default None - pooled ranking, this function's original and only behavior before this parameter existed) is an optional {asset_class: (top_n, bottom_n)} map. When provided, top_n/ bottom_n are ignored and every eligible symbol is ranked within its own asset_class group only instead of one combined-universe pool - so e.g. equities and crypto each get their own long/short slot budget instead of one side potentially being filled entirely by a single dominant asset class. A symbol whose asset_class isn't a key in per_asset_class_slots is excluded from book selection entirely (not silently folded into some catch-all group) - same explicit-opt-in convention risk/asset_class_router.py already uses for future/option. min_rank_confidence_spread is applied independently per asset class (each class's own long/short spread must individually clear the bar - pooling the check across classes with potentially very different rank distributions would be misleading). Results from every class are unioned into one returned dict, same shape as the pooled path.

Returns a dict covering ONLY the symbols the book actively wants long or short (not a "flat" entry for every candidate) - callers should treat "symbol absent from the returned dict" as "book has no view on this symbol," not "book says flat."

previous_allocations/hysteresis_rank_margin (V5.1 Phase 0/1, defaults None/0.0 - a strict no-op reproducing pre-hysteresis selection exactly): threaded straight through to _select_book_group() (pooled path) or applied identically within each asset class (per-asset-class path) - see _hysteresis_adjusted_selection()'s docstring for the actual sticky-selection behavior.

spread_check_ranks (V5.1 Phase 1, development/Problems.md #77): optional {symbol: raw_score} override for the min_rank_confidence_spread check only - see _select_book_group()'s docstring for why this must be the PRE-normalization score when the caller's book_candidates predicted_rank_20d values are already a cross-sectional percentile. Defaults to None (falls back to book_candidates' own predicted_rank_20d, byte-identical to before this parameter existed).

rolling_ic_gate_result/veto_reason_out (V5.3.5, development/ Problems.md #102): threaded straight through to _select_book_group() - see that function's own docstring. Both default to None, a strict no-op. With per_asset_class_slots, rolling_ic_gate_result is applied identically to every class's own selection (this signal is portfolio-wide, not asset-class-specific, unlike min_rank_confidence_spread which already gets a note above about being checked per class); if more than one class vetoes, veto_reason_out ends up holding whichever class's reason was computed last - an accepted, documented simplification, since the only thing that consumes this diagnostic (main.py's book-history logging) only reads it when the OVERALL allocations result comes back completely empty, at which point any one real reason is more useful than none.

build_vertical_spread_position_sizing

build_vertical_spread_position_sizing(signal_direction: str, confidence: float, available_chain: list[dict], portfolio_value: float, target_delta_at_full_confidence: float = 0.6, short_leg_delta_offset: float = 0.2, max_vega_budget_pct_of_equity: float = 0.02) -> OptionsSpreadPositionDecision | None

Vertical-spread sibling of build_options_position_sizing() above - same signal_direction -> right mapping ("buy" -> call vertical/ bull_call_spread, "sell"/"short" -> put vertical/bear_put_spread), same confidence-scaled target_delta for the long leg.

Sizes by NET vega (long leg vega minus short leg vega) rather than the long leg's vega alone - a vertical's defining risk reduction versus a single leg, and the correct risk unit to budget a spread position against. Returns None on the same degrade-to-absent conditions build_options_position_sizing() already documents, plus: no usable short leg found (select_vertical_spread_legs() returned None), or net_vega is non-positive (a spread with ~zero or negative net vega is unsizeable by this budget, not an error).

build_vertical_spread_position_sizing_for_legs

build_vertical_spread_position_sizing_for_legs(held_long: dict[str, Any], held_short: dict[str, Any], portfolio_value: float, max_vega_budget_pct_of_equity: float = 0.02) -> OptionsSpreadPositionDecision | None

V4.4 - net-vega sibling of build_options_position_sizing_for_contract() for a held 2-leg vertical spread: sizes the EXACT held long/short legs on their own current greeks, skipping select_vertical_spread_legs() entirely. held_long/held_short are live chain rows for the currently- held legs (same shape select_vertical_spread_legs() returns each of its pair). strategy_name/right are derived from held_long's own "right" field rather than passed in - there is no fresh signal here, the spread already exists. Same degrade-to-None contract as build_vertical_spread_position_sizing().

calibrate_probability

calibrate_probability(probability: float, calibration: dict | None) -> float

The fitted map applied to one probability; unknown or missing maps leave it unchanged.

cash_profitability

cash_profitability(fundamentals: DataFrame, index: Index, columns: Index, *, max_age: int = 400) -> pd.DataFrame

Trailing operating cash flow over assets (Ball et al.), each as last filed before the session.

cash_sweep_target_weight

cash_sweep_target_weight(*, long_weight: float, short_weight: float, margin_used: float, reserve: dict[str, Any], config: dict[str, Any]) -> float

Sweep weight (fraction of NAV) inside both limits and max_weight; 0.0 for any non-finite input.

classify_volatility_view

classify_volatility_view(predicted_volatility_annualized: float, atm_iv: float, margin: float = 0.05) -> str

Pure, unit-agnostic classifier: "long_vol" when the model's own annualized volatility forecast exceeds the chain's current ATM implied vol by more than margin, "short_vol" when it's below by more than margin, else "neutral". The annualization (predicted_volatility * ANNUALIZATION_FACTOR_DAILY_TO_ANNUAL) MUST happen at the call site, never inside this function - train.py's target_volatility_next_day is a raw daily high-low-range proxy, not annualized, and comparing it against chain IV (always annualized) unit-mismatched would produce an almost-always-false signal. Documented approximation regardless of units: a range-based estimator and a lognormal-return-implied vol aren't the same statistical object even after unit-matching.

combine_sleeves

combine_sleeves(sleeve_weights: Mapping[str, Mapping[str, Mapping[str, float]]], panel: ResearchPanel, *, method: str = 'inverse_vol', every: int = 21, settings: WeightSettings | None = None, start: str | None = None, end: str | None = None) -> dict[str, dict[str, float]]

A fixed-rule mix of sleeves, never fitted to their returns: each month a sleeve's budget comes from the trailing year of its own gross returns (inverse volatility, or hierarchical risk parity), and the sum is scaled to one volatility target.

combine_tranche_weights

combine_tranche_weights(tranche_weights: list[dict[str, float]]) -> dict[str, float]

The live book target: the sum of the tranches' signed weights per name (a name held by two tranches nets); names that net to zero are dropped.

compute_confidence_spread

compute_confidence_spread(long_symbols: list[str], short_symbols: list[str], spread_ranks: dict[str, float], eligible_ranks: dict[str, float]) -> float | None

mean(long-side score) - mean(short-side score), the exact quantity _select_book_group()'s min_rank_confidence_spread gate compares against a floor. Public (not module-private) so a calibration tool can compute the identical statistic against historical data without re-deriving the formula - the two can then never disagree on what "spread" means for a given day's selection.

spread_ranks.get(symbol, eligible_ranks[symbol]) - a symbol missing from spread_ranks (e.g. a caller-supplied dict that doesn't cover every eligible symbol) falls back to its own eligible_ranks value rather than a KeyError, same defensive convention as every other optional-override lookup in this module.

None if either list is empty - spread is undefined without both sides (mirrors the caller never reaching this check for a long-only book).

compute_hrp_weights

compute_hrp_weights(returns: 'pd.DataFrame') -> 'pd.Series'

Hierarchical risk parity weights (portfolio/hrp_allocation.py; needs scipy, imported on call).

compute_rolling_ic_state

compute_rolling_ic_state(event_buffer: list[dict], *, horizon_days: int, rolling_window_days: int, min_names_per_date: int = 2) -> dict[str, Any]

Resolves event_buffer (main.py's own flat, in-memory {"ticker", "created_at", "resolved_predicted_rank_20d", "close_price"} deque, or the offline replay/calibration tools' equivalent) via evaluation.rank_ic_core.resolve_realized_rank_ic_observations(), then restricts to the trailing rolling_window_days UNIQUE resolved origin dates (not calendar days, and not raw observation count - a date with more symbols resolved must not count for more "days" of trailing history than a thin one) before aggregating.

min_names_per_date (V5.3.5, development/Problems.md #102) - default 2 preserves this function's original behavior exactly (see evaluation.rank_ic_core.rank_ic_from_arrays()'s own docstring for why this exists: a 2-name origin date is mathematically forced to a +-1 IC reading regardless of real skill, and real historical data confirmed this dominates the low tail of a walk-forward calibration's distribution). Callers evaluating a live or replayed rolling-IC reading - where thin-universe noise masquerading as "the model is badly wrong" would cause a spurious veto - should pass a value well above 2 (evaluation/rolling_ic_gate_calibration.py and evaluation/ rolling_ic_gate_replay.py both default to 10, sourced from phase_v2.rolling_ic_gate.min_names_per_date).

Returns {"rolling_mean_ic": float | None, "num_resolved_dates": int, "num_resolved_observations": int}. rolling_mean_ic is None (never 0.0) when zero observations have resolved yet - "unknown" must never be silently treated as "IC is exactly zero" by evaluate_rolling_ic_gate() below, the same None-vs-neutral-default distinction every other pure signal function in this codebase already draws (e.g. risk/kill_switch.py::_rolling_sharpe()).

compute_sleeve_gross_used

compute_sleeve_gross_used(held_weights_by_symbol: dict[str, float], book_member_keys: set[str] | frozenset[str]) -> float

Gross |weight| currently held outside the book: every invested symbol that is neither in this bar's book nor in the last formed book. During a veto the book is empty, so remembered members still count as book (they are positions the book opened), not sleeve.

conversion_parity_value

conversion_parity_value(call_price: float, put_price: float, spot: float, strike: float, risk_free_rate: float, time_to_expiry_years: float, pv_dividends: float = 0.0) -> float

Put-call parity: call_price - put_price should equal spot - pv_dividends - strike * exp(-r*T). Returns the SIGNED discrepancy (actual minus theoretical). A conversion (long stock, long put, short call) locks in call - put - spot + PV(strike) + PV(dividends), which is this discrepancy, so it profits when it is positive (calls rich against puts); a reverse conversion profits when it is negative. Without pv_dividends, a dividend payer's calls read cheap: a reverse conversion is short the stock and owes each dividend, which is why it fired on AAPL at fair prices.

core_constant

core_constant(known: DataFrame, core: tuple[str, ...] = CORE) -> pd.DataFrame

1.0 on the core coins wherever they have a price: a volatility-managed hold, whose size the weight pipeline sets.

cross_asset_trend

cross_asset_trend(known: DataFrame, tickers: tuple[str, ...] | list[str], *, long: int = 252, skip: int = 21) -> pd.DataFrame

1.0 where the 12-1 month return (long rows back to skip rows back) is positive, else 0.0; NaN without that history. Only the named assets.

cross_sectional_rank

cross_sectional_rank(table: DataFrame) -> pd.DataFrame

Each row's percentile ranks across the columns.

cross_sectional_rank_scores

cross_sectional_rank_scores(scores_by_symbol: dict[str, float]) -> dict[str, float]

Per-bar percentile rank across whatever symbols have a raw score this bar, ties averaged (matching pandas' .rank(pct=True) semantics - the same transform train.py::_rank_ic_from_arrays() applies to predictions before computing rank-IC, so a raw score's absolute scale can never matter, only its ordering within this bar's cross-section). Pure Python - no pandas import on the runtime hot path.

<=1 eligible symbol returns {} (no meaningful cross-section to rank within, so the book has no view this bar) - NEVER a spurious 0.5 for a lone symbol, which would silently look like "predicted median" rather than "no ranking possible.".

cross_sectional_return

cross_sectional_return(panel: ResearchPanel, spec: SleeveSpec) -> pd.DataFrame

Rank on the trailing return over lookback, skipping the last skip sessions.

crypto_core_only_settings

crypto_core_only_settings(class_weight_caps_config: dict[str, Any] | None) -> tuple[bool, frozenset[str]]

(core_only, core names) from phase_v2.class_weight_caps.crypto: core_only (V6.4.0, default false, lead L9) keeps the crypto pairs that are not exempt (the proven core, BTC / LTC) out of the main book's candidates.

crypto_graduate_excluded

crypto_graduate_excluded(ticker: str, asset_class: str | None, core_only: bool, core_names: frozenset[str]) -> bool

True when core_only is on and ticker is a crypto pair outside the core: it takes no book slot or neutrality weight. One definition for main.py's live candidates and the offline simulator's candidate metadata.

crypto_tables

crypto_tables(known: DataFrame, funding: DataFrame | None, liquid: DataFrame, btc: str = 'BTCUSD') -> dict[str, pd.DataFrame]

c1-c4, session x coin, masked to the liquid set. known already holds each coin's bar from before the session, and funding is shifted the same way: a day's settlements aren't all known at that day's equity close.

crypto_trend

crypto_trend(panel: ResearchPanel, spec: SleeveSpec) -> pd.DataFrame

Long while the trailing return over the lookback is positive; flat otherwise (long-only: no reliable short venue).

daily_financing_usd

daily_financing_usd(*, long_usd: float, short_usd: float, nav: float, rf_annual_pct: float, fx_positions: Iterable[tuple[float, float]] = (), days: float = 1.0, margin_spread_bps: float = 150.0, borrow_bps: float = 25.0, fx_spread_bps: float = 50.0) -> float

Financing for days calendar days. long_usd and short_usd (positive) cover everything but forex, including the T-bill sweep. The debit is what the longs need beyond NAV and the short proceeds, charged at the risk-free rate plus the spread. Each forex position is (signed USD notional of the base currency, r_base - r_quote in percentage points): long the base earns the differential, either way less the spread. Positive cash earns nothing, as in Lean.

days_to_next_ex_dividend

days_to_next_ex_dividend(current_date: date | None, next_ex_dividend_date: date | None) -> int | None

Pure date arithmetic, mirrors options_strategy.option_auto_close_due()'s own calendar-day-diff pattern. None when either input is missing - never raises.

detect_executable_edge

detect_executable_edge(edge: float, min_mispricing_bps: float, notional: float) -> bool

True when entering the structure at executable prices beats its fair value by at least the threshold. One-sided: a structure priced dear is no arbitrage for the side that would pay up, and only its opposite may fire.

detect_mispricing

detect_mispricing(actual_net_cost: float, fair_value: float, min_mispricing_bps: float, notional: float) -> bool

Shared threshold predicate for all 3 families above: True when the discrepancy between actual_net_cost and fair_value exceeds min_mispricing_bps of notional - requiring a MINIMUM edge (not a bare non-zero difference) accounts for real bid/ask spread and transaction costs a genuine arbitrage must clear to be worth trading. Returns False (never trades, never raises) when notional is non-positive.

early_exercise_optimal_for_dividend

early_exercise_optimal_for_dividend(extrinsic_value: float | None, expected_dividend_amount: float | None) -> bool

Classic textbook rule: early call exercise is optimal only when the option's extrinsic value is LESS than the dividend the holder would capture by exercising before ex-div (they give up less time value than they gain in dividend). Returns False (never optimal) when either input is None - an unknown value never triggers a false-positive risk flag.

equity_fee_usd

equity_fee_usd(notional: float, bps: float, min_usd: float) -> float

Commission in basis points with a dollar minimum. Used instead of Lean's per-share IB model on this path: the data is split- and dividend-adjusted, so an old adjusted price of $0.49 would make a per-share fee 1% of the trade.

evaluate_rolling_ic_gate

evaluate_rolling_ic_gate(rolling_ic_state: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]

Pure decision function - the direct sibling of portfolio.book_construction.compute_confidence_spread()'s consumer at _select_book_group(). rolling_ic_state is compute_rolling_ic_state()'s own output; config is phase_v2.rolling_ic_gate's dict.

GUARDRAIL - fail-open at two levels, matching risk/kill_switch.py::evaluate_kill_switch()'s own documented contract: 1. config["enabled"] is False -> always engaged, full no-op, regardless of any other input. 2. num_resolved_dates < min_resolved_dates_required (default: config's own rolling_window_days, matching evaluate_kill_switch()'s min_bars_for_sharpe precedent - "the trailing window size" is the value's own documented intent) -> also engaged. A cold-start buffer (the first ~40-60 live bars of any run, before enough predictions have had time to resolve against a realized 20-day forward return) must never masquerade as "IC is bad" - it must masquerade as "gate not evaluated yet".

Returns {"engaged": bool, "reason": str, "observed_rolling_mean_ic": float | None, "min_rolling_mean_ic": float, "num_resolved_dates": int}.

evaluate_sleeve_guard

evaluate_sleeve_guard(pnl_increments: Sequence[float], nav: float, config: SleeveGuardConfig, *, current_bar: int, suspended_until_bar: int | None = None) -> dict[str, Any]

Decides whether the sleeve may trade on current_bar.

pnl_increments is the sleeve's per-bar P&L history (dollars, or NAV fractions with nav=1.0), oldest first. Returns {"active": bool, "reason": str, "suspended_until_bar": int | None, "drawdown": float, "limit": float}. A suspension started earlier holds until suspended_until_bar (exclusive); a fresh breach starts a new one. The history is NOT reset by the caller: after the cooldown the trailing window usually no longer contains the loss, and if it still does the sleeve stays off - a loss that has not healed is not a reason to resume.

events_to_sessions

events_to_sessions(events: DataFrame, index: Index, columns: Index, *, date: str, value: str, limit: int | None) -> pd.DataFrame

The newest value per name as of each session, from rows with a date and a ticker. Visible from the first session strictly after date; a later event at the same row wins; carried forward at most limit rows.

external_score

external_score(panel: ResearchPanel, spec: SleeveSpec) -> pd.DataFrame

A score table computed elsewhere (a walk-forward model, COT positioning, earnings surprise), already as of each session.

extrinsic_value

extrinsic_value(option_price: float | None, spot: float, strike: float, right: str) -> float | None

option_price minus intrinsic value. Returns None (not 0.0) when option_price is missing/non-positive - "unknown extrinsic value" must never be indistinguishable from "zero extrinsic value" (a real, meaningful early-exercise trigger condition in its own right), the same None-vs-neutral-default distinction features/bond_features.py's module docstring already establishes for this codebase.

forex_resize_below_minimum

forex_resize_below_minimum(delta_units: float, price: float, min_trade_usd: float) -> bool

True when a resize of delta_units base-currency units at price is worth less than min_trade_usd in notional: paying IB's dollar minimum to move such a small amount costs more bps than the move is worth. min_trade_usd <= 0 disables the check (False). Non-finite inputs -> True (do not trade on garbage).

forward_table

forward_table(spec: Mapping[str, Any], known: DataFrame) -> pd.DataFrame

A round-4C trial as the forward feed can compute it, from the marks alone (no volume, no funding): {"builder": name, "params": {...}, "gate": "regime"} for one signal, or {"rank_mean": [spec, ...], "minimum": n, "gate": "regime"} for a composite. The builder comes from a closed set. breadth_timing counts every coin in known as liquid, since the feed holds only the coins the definition lists.

fred_as_known

fred_as_known(series: dict[str, Any], index: Index, *, keys: tuple[str, ...] = ('treasury_10yr', 'treasury_3mo', 'credit_spread_baa10y'), lag_sessions: int = 1) -> pd.DataFrame

FRED series ({key: Series by date}) as the session knows them: carried across days with no mark, then read lag_sessions late, since FRED's daily marks post after the equity close.

fred_series_table

fred_series_table(fred_rows: Mapping[str, list[dict]], keys: tuple[str, ...]) -> dict[str, pd.Series]

{key: value by date string} from load_cached_fred_series() rows.

funding_filtered_trend

funding_filtered_trend(known: DataFrame, funding: DataFrame | None, window: int = 14, rank_window: int = 180, cut: float = 0.8) -> pd.DataFrame

The trend votes, set to 0 (flat) where a coin's mean funding sits above its own trailing cut quantile, since crowded longs paid to stay long. Missing funding leaves the vote alone.

fx_carry_signal

fx_carry_signal(panel: ResearchPanel, spec: SleeveSpec) -> pd.DataFrame

Rank on the 3-month rate differential; momentum_weight adds a rank of the trailing 63-session return.

group_chain_by_expiry

group_chain_by_expiry(available_chain: list[dict]) -> dict[str, list[dict]]

Single O(n) pass grouping chain rows by expiry - built ONCE per selector call and reused for every leg lookup within it, rather than re-scanning the full chain per leg (this is the "latency-efficient" requirement for a function that runs every bar, per candidate strategy, per symbol). Also the mechanism that makes expiry-anchoring possible: every leg after the anchor is selected ONLY from grouped[anchor_expiry], never re-scanning the other expiries.

Public (V4.9 Priority 3, renamed from the original _group_chain_by_expiry) since risk/asset_class_router.py::route_multi_leg_option_sizing() now calls this ONCE per routing call (not per selector call) when the calendar shape family is in play, avoiding a repeated regroup of the same chain across multiple candidate strategies tried in the same bar - see select_calendar_legs()'s/select_arbitrage_jelly_roll_legs()'s own grouped_chain_by_expiry parameter.

grouped_cross_sectional_rank_scores

grouped_cross_sectional_rank_scores(scores_by_symbol: dict[str, float], group_by_symbol: dict[str, str] | None, sector_by_symbol: dict[str, str] | None = None, min_sector_size: int = 3) -> dict[str, float]

cross_sectional_rank_scores() applied WITHIN each rank group (V5.8.0): a model trained on grouped rank targets (bonds ranked among bonds, forex among forex) emits scores that are only comparable inside their group, so the percentile is taken per group. With no mapping (or every symbol in "main") this equals cross_sectional_rank_scores().

sector_by_symbol (V6.4.0, rank_signal.normalization: "sector_relative", default None = off) de-means each group's scores by their sector's mean first (sector_relative_scores), so the percentile ranks a name against its sector peers.

high_proximity

high_proximity(known: DataFrame, window: int = 365, min_periods: int = 180) -> pd.DataFrame

Close over its trailing high (George and Hwang): 1.0 at the high.

idio_vol

idio_vol(known: DataFrame, btc: str = BTC, window: int = 60) -> pd.DataFrame

Minus the standard deviation of the return left after BTC.

is_rotation_exit_candidate

is_rotation_exit_candidate(is_currently_invested: bool, sleeve_enabled: bool, is_book_owned: bool) -> bool

V5.5.0 (Problems.md #128) - may the rotation exit force-sell this position when the book is active and did not select it?

should_exit_non_selected_book_symbol() was documented as applying to a symbol "previously book-selected" but applied to EVERY invested non-member. With the legacy sleeve on, that would liquidate a sleeve entry the very next bar (and, with exits no longer blocked by the trade cooldown, re-enter and re-exit it - pure churn), so the sleeve's positions must be left to the non-model exits (max holding age, trailing stop) and their own sell signal. With the sleeve OFF this is the pre-V5.5.0 rule unchanged: every invested non-member is a candidate.

jelly_roll_fair_value

jelly_roll_fair_value(strike: float, risk_free_rate: float, near_time_to_expiry_years: float, far_time_to_expiry_years: float) -> float

The interest-rate-implied fair cost of a jelly roll (rolling a synthetic position from the near expiry to the far one) - the discounted strike difference between the two expiries, the standard cost-of-carry result for extending a synthetic forward's expiry.

known_closes

known_closes(dates: list[str], tickers: list[str], closes: ndarray, asset_class: Mapping[str, str], sessions: set[str]) -> pd.DataFrame

Closes known at each session's equity close. A crypto or forex bar dated D closes at the midnight after that close, so on session D the latest usable bar of those classes is the one dated before D.

live_names_per_rebalance

live_names_per_rebalance(table: DataFrame, rebalance_every: int) -> dict[str, float]

How many names a signal can rank, on the days it decides: an empty book must not read as a sleeve with no skill.

load_live_weight_file

load_live_weight_file(path: Path | str) -> dict[str, dict[str, float]]

A growing weight file, checked against its sidecar. Raises as load_weight_file does; the caller keeps what it has.

load_rate_series

load_rate_series(path: Path | str) -> tuple[list[str], list[float]]

(ISO dates, rates in percent) of Lean's interest-rate.csv; empty when the file is missing or unreadable, so a run without it charges a 0% base rate plus the spread rather than failing.

load_weight_file

load_weight_file(path: Path | str, expected_sha256: str = '') -> dict[str, dict[str, float]]

date -> ticker -> weight. Raises ValueError on a hash mismatch, a malformed row or a non-finite weight: a run must not trade a file other than the one its config froze.

lottery_max

lottery_max(known: DataFrame, window: int = 30) -> pd.DataFrame

The coin with the smaller best day ranks higher: minus the trailing maximum daily return (Bali, Cakici, Whitelaw's MAX effect).

low_beta

low_beta(known: DataFrame, btc: str = BTC, window: int = 60) -> pd.DataFrame

Minus the trailing beta to BTC.

low_vol

low_vol(known: DataFrame, window: int = 60) -> pd.DataFrame

The calmer coin ranks higher: minus the trailing standard deviation of daily returns.

low_volatility

low_volatility(panel: ResearchPanel, spec: SleeveSpec) -> pd.DataFrame

Minus the trailing volatility of each name's returns: the low-volatility signal.

margin_fraction

margin_fraction(weights: list[tuple[str, float]]) -> float

Margin Lean's default model holds for these (security type, signed weight) positions, as a fraction of NAV.

names_to_exit_without_bars

names_to_exit_without_bars(missing: Mapping[str, int], config: TargetWeightsConfig) -> list[str]

Held names whose bars stopped, in a fixed order. This reacts only to bars already missing, so it reads no future: a delisted name can't be sold at any later price, and holding it at its last close forever would hide the loss.

net_debit_or_credit_for_legs

net_debit_or_credit_for_legs(spec: StrategySpec, legs_by_role: dict[str, dict]) -> float | None

Signed net premium to trade every leg in spec.legs at legs_by_role's current bid/ask - long legs value at ask, short legs at bid (the conservative/executable-price convention _size_multi_leg() below already uses, and options_arbitrage_detector.py's own _actual_net_cost() also uses independently). Positive = net DEBIT (paying more for long legs than received for short legs), negative = net CREDIT - same sign convention as OptionsMultiLegPositionDecision's own net_debit_or_credit field.

Returns None (not 0.0) when any leg's role is missing a bid/ask row - "cannot compute this bar" must not be indistinguishable from "computed a genuine 0.0 premium", the same None-vs-neutral-default distinction features/bond_features.py's module docstring and features/options_greeks.py::implied_volatility() already draw. _size_multi_leg() below deliberately keeps ITS OWN reset-to-0.0-on- missing-price behavior (a documented best-effort SIZING diagnostic, unchanged) by coercing this function's None to 0.0 at its own call site - this function is the stricter sibling, extracted for a caller (main.py's per-strategy realized-outcome computation, V4.7) where "unknown" must never silently look like "breakeven".

next_unapplied_row

next_unapplied_row(weights: Mapping[str, Mapping[str, float]], tick_date: str, applied_through: str | None) -> tuple[str, dict[str, float]] | None

Paper and live (live_reload): the latest row dated at or before this tick and after the last one applied. Row D is written after D's close, so the exact-date lookup a backtest uses would miss every row.

no_bar_exit_cost_usd

no_bar_exit_cost_usd(units: float, last_close_usd: float, asset_class: str, config: TargetWeightsConfig) -> float

What such an exit loses against the last close: Lean fills the order at that stale price, so this is charged on top.

normalize_per_asset_class_slots

normalize_per_asset_class_slots(raw: dict[str, object] | None) -> tuple[dict[str, tuple[int, int]], list[str]]

Validates and coerces a raw phase_v2.portfolio_book.per_asset_class_slots config dict into build_rank_based_book()'s expected shape. build_rank_based_book() unpacks each value unconditionally (for asset_class, (top_n, bottom_n) in per_asset_class_slots.items()), so a malformed entry (wrong length, wrong type) would otherwise hard- crash every bar instead of degrading gracefully like every other optional config-driven feature in this codebase - development/ Problems.md. Each value must be a 2-element [top_n, bottom_n] list/tuple; anything else is skipped, not fatal.

Returns (valid_slots, skipped_asset_classes) - pure and no-logging by design (so it stays independently testable); callers with access to a logger (main.py's self.Debug()) should report the skipped list themselves.

null_signal

null_signal(signal: DataFrame, mode: str, *, seed: int, asset_class: Mapping[str, str], block: int = 60, min_shift: int = 252) -> pd.DataFrame

A signal with the skill removed, for placebos.

asset_block: relabel assets within their class, held for block sessions; keeps turnover and persistence. time_shift: a circular shift of at least a year; keeps each asset's long-run tilt, so a survivor or class effect that an asset relabelling would credit as skill is matched here. sign_block: for time-series and timing sleeves, reverse the position per asset in block-session blocks (a long/flat signal in 0..1 swaps long and flat).

opportunistic_buying

opportunistic_buying(trades: DataFrame, index: Index, columns: Index, *, routine_years: int = 3, window: int = 63) -> pd.DataFrame

Open-market purchases (code P) by owners who weren't routine buyers (Cohen, Malloy, Pomorski): an owner who bought in the same calendar month in each of the previous routine_years years is routine and left out. The value over the last window rows, logged. Dated by filed; 0.0 means no such purchase.

option_auto_close_due

option_auto_close_due(current_date: date, expiry_date: date, auto_close_days_before_expiry: int = 2) -> bool

§9.5 - the narrow expiry-day safety net: True once expiry_date is within auto_close_days_before_expiry calendar days of current_date. Pure date comparison, mirroring risk/futures_risk.py::rollover_due()'s own pattern exactly (generalized from futures rollover to options expiry) - a diagnostic/trigger signal only, main.py's own force- liquidate call is what actually acts on it. Returns False when expiry_date is unparseable/unknown - never raises, matching every other pure predicate in this module's degrade-to-absent convention.

order_enabled_strategies

order_enabled_strategies(enabled_strategy_names: list[str], risk_tier_preference: str = 'defined_risk_first') -> list[str]

Pure reordering (§9.2): enabled_strategy_names is a PRIORITY list, not just a membership set - the first enabled name (in the returned order) that matches the current bucket and successfully sizes wins. "defined_risk_first" (the safe default) stable-sorts every defined-risk name (iron condor/butterfly variants) ahead of every unbounded-risk name (short straddle/strangle), preserving the caller's own relative order within each group; "premium_maximizing_first" inverts that. Names outside both known sets (every other strategy) keep their original relative position, interleaved after the reordered ones - this function only ever reorders, never drops or adds a name.

partial_adjust_weights

partial_adjust_weights(current: dict[str, float], target: dict[str, float], fraction: float, min_weight: float = 0.001) -> dict[str, float]

V6.4.0 (lead L6): move fraction of the way from the held weights to the new target on a rebalance, current + fraction * (target - current) per name; weights that fall under min_weight are dropped. A fraction of 1 returns the target unchanged (the shipped book); the blend of two valid books stays inside the same gross cap.

pct_returns_from_closes

pct_returns_from_closes(closes: list[Any]) -> list[float]

V5.4.4 - close-to-close simple returns from a symbol's rolling close window (main.py's self.symbol_windows[symbol] "close" values, oldest first). The per-symbol return history apply_hrp_weights() consumes for its covariance estimation.

Pairs containing a missing/non-positive/non-finite close are SKIPPED (not emitted as NaN/inf/-1.0) - a split-adjusted 0 or a None from a partially-populated window would otherwise poison the covariance matrix HRP estimates downstream (a 0 CURRENT close would emit a -100% return, a variance bomb, even though its previous close is technically usable). V5.4.5 (#117): NaN closes pass the old <= 0.0 check (nan <= 0 is False) and poisoned the covariance the same way. Fewer than 2 usable closes returns [] (no return can exist without at least one previous close to diff against).

pv_dividends_to_expiry

pv_dividends_to_expiry(payments: Iterable[tuple[date, float]] | None, current_date: date, expiry_date_str: str, risk_free_rate: float) -> float

Present value of the dividends going ex after current_date and on or before the expiry. payments holds (ex_date, cash_per_share) pairs. A row it can't read counts as no dividend, as the detector did before it knew of any.

rank_group_by_symbol

rank_group_by_symbol(asset_class_by_symbol: dict[str, str | None], separate_rank_groups: dict[str, Any] | None) -> dict[str, str]

{symbol: rank group} from phase1.target.ranking.separate_rank_groups ({group: [asset classes]}): a symbol whose class is listed under a group belongs to it, everything else to "main". The same mapping train.py uses to build the grouped rank targets (V5.7.0), so live ranks each group the way the model was trained.

rank_mean

rank_mean(tables: list[DataFrame], *, minimum: int) -> pd.DataFrame

The mean of each table's cross-sectional rank, where at least minimum of them have a value.

rate_asof

rate_asof(dates: list[str], rates: list[float], date: str) -> float

The last rate at or before date; 0.0 before the first observation.

rebalance_deltas

rebalance_deltas(held_units: Mapping[str, float], usd_per_unit: Mapping[str, float], nav: float, target: Mapping[str, float], config: TargetWeightsConfig, *, lot_size: Mapping[str, float] | None = None, class_by_ticker: Mapping[str, str] | None = None) -> list[tuple[str, float]]

(ticker, signed units to trade), reductions first: Lean checks buying power when an order is submitted and does not net sells that have not filled. A ticker with no usable price is skipped, never traded on a guess. A ticker the target omits is closed in full; otherwise a change below the deadband or the minimum trade size is left alone.

regime_gate

regime_gate(table: DataFrame, known: DataFrame, btc: str = BTC, window: int = 120) -> pd.DataFrame

The table on sessions when BTC trades above its window-session average; no signal at all (a flat book) otherwise, and while BTC's average can't be measured.

remember_formed_book

remember_formed_book(previous_formed: dict[str, Any] | None, new_allocations: dict[str, Any] | None) -> dict[str, Any]

V5.5.0 (Problems.md #128) - the hysteresis memory a veto must NOT wipe.

main.py stored every rebalance result into the one dict it also uses as "the book to trade", so a gate veto (empty allocations) erased the previous book. The next rebalance then selected with no hysteresis anchor, and has_previous_allocation read False so the book re-formed on every following bar until a gate opened. The positions the book had opened were still held the whole time. This keeps the last NON-EMPTY book as the anchor: a veto leaves it untouched, a real book replaces it.

rerank_enabled_strategies_by_score

rerank_enabled_strategies_by_score(enabled_strategy_names: list[str], strategy_selector_scores: dict[str, float]) -> list[str]

V4.7 (development/Problems.md #29's own framing) - the learned strategy-selector model's live inference plug point: a drop-in ALTERNATIVE to order_enabled_strategies()'s static reordering above, not a replacement for it. Stable-sorts enabled_strategy_names descending by strategy_selector_scores.get(name, float("-inf")) - names with no score (the model hasn't seen that strategy_name yet, or strategy_selector_scores is a stale/partial version) sink to the end in their ORIGINAL relative order, never dropped - same "only ever reorders, never drops or adds a name" contract order_enabled_strategies() itself keeps.

Callers must only invoke this when strategy_selector_scores is truthy (non-empty) - risk/asset_class_router.py::route_multi_leg_option_sizing() falls back to order_enabled_strategies() itself whenever no trained model's scores are available, so this function has no fallback path of its own; it is never the byte-identical default.

residual_momentum

residual_momentum(panel: ResearchPanel, spec: SleeveSpec) -> pd.DataFrame

Rank on the market-residual return from lookback to skip sessions ago.

residual_reversal

residual_reversal(panel: ResearchPanel, spec: SleeveSpec) -> pd.DataFrame

Rank against the market-residual return of the last window sessions.

resize_below_minimum

resize_below_minimum(delta_units: float, price: float, min_trade_usd: float) -> bool

True when a resize of delta_units at price is worth less than min_trade_usd in notional (a fixed per-order fee would cost more bps than the move is worth). min_trade_usd <= 0 disables the check (False). Non-finite inputs -> True (do not trade on garbage).

resolve_cash_sweep_config

resolve_cash_sweep_config(raw: dict[str, Any] | None) -> dict[str, Any]

phase_v2.cash_sweep with defaults; out-of-range numbers fall back to the default rather than raise.

resolve_class_sleeve_config

resolve_class_sleeve_config(asset_class: str, raw: dict | None) -> ClassSleeveConfig

Config block (phase_v2.<class>_sleeve) -> ClassSleeveConfig. Absent block = disabled.

resolve_class_weight_caps

resolve_class_weight_caps(raw: dict[str, Any] | None) -> dict[str, dict]

phase_v2.class_weight_caps -> {asset_class: {"per_name_cap", "class_gross_cap", "exempt"}} for the enabled classes. Absent/empty = no caps (the pre-V5.8.0 behaviour). A cap of 0 or less means "no cap on that dimension".

resolve_direction_overlay

resolve_direction_overlay(raw: dict | None) -> dict | None

phase_v2.rank_signal.direction_overlay -> params, or None when absent / disabled / malformed (the shipped book).

resolve_enabled_strategy_names

resolve_enabled_strategy_names(asset: dict[str, Any], global_default: list[str]) -> list[str]

V4.6 (development/Problems.md #59's own deferred item) - per-asset enabled_strategy_names override: an option asset's phase1.universe. assets entry may carry an optional "options_strategy_override": {"enabled_strategy_names": [...]} key to trade a DIFFERENT strategy set than the global default (e.g. one ticker trades iron condors, another trades covered calls). Returns the override when present and non-empty, else global_default unchanged - byte-identical to every asset with no override key at all, which is every asset today (V4.6 adds no default overrides). Never raises on a malformed override (falls back to global_default).

resolve_late_bar_event_count

resolve_late_bar_event_count(gate_config: dict[str, Any] | None) -> str

rolling_ic_gate.late_bar_event_count (V6.2.0): "per_tick" (default, the behaviour up to V6.1.0) records a crypto / forex symbol's event on its own midnight tick AND again on the next equity tick, where its last bar is re-scored, so it weighs twice per date in the gate's IC; "once" records it on its own tick only.

resolve_legacy_signal_params

resolve_legacy_signal_params(config: dict[str, Any], decision_threshold: float) -> dict[str, Any]

The probability signal's settings, read the way main.py reads them (V6.1.0: one definition for main.py and the offline legacy replay): buy / sell thresholds around the baseline model's decision_threshold (phase5.backtest.*_threshold_offset, clamped to [0.25, 0.75]), the adaptive sell band (phase_v2.exits.adaptive_band_*) and the position cap the signal sizes against (phase6.risk.max_position_weight).

resolve_legacy_sleeve_config

resolve_legacy_sleeve_config(raw: dict[str, Any] | None) -> dict[str, Any]

Normalize phase_v2.legacy_sleeve; any malformed value falls back to the default (sleeve disabled == the pre-V5.5.0 unbounded behavior).

resolve_rank_signal_policy

resolve_rank_signal_policy(training_metrics_by_model: dict[str, dict | None], config: dict[str, Any]) -> dict[str, Any]

Resolved ONCE in main.py::Initialize() (never per-bar - the promotion-gate verdicts this reads only change when a new model is installed). Reads phase_v2.rank_signal:

heads: {"rank_20d": 1.0, "rank_5d": 0.0} - the blend weights. model_priority: ["sequence", "multitask"] - which model's value for a given head wins when both are available. demote_not_promotable_heads: when true, a head whose backtest promotion-gate verdict (backtest.{head}_ranking_quality.quality_status, checked against the FIRST model in model_priority that has a verdict for it - the same precedence select_raw_rank_score() uses at runtime) is not in min_promotion_status has its weight zeroed and redistributed proportionally across the surviving heads. A head configured at weight 0.0 is still checked and can still become a survivor - "listed with zero weight" means "tracked, currently inactive," not "not configured" (development/Problems.md); it only actually receives weight if every positively-weighted head gets demoted, in which case survivors split the freed weight equally rather than dividing by the now-zero surviving total.

Returns {"heads": {...}, "model_priority": [...], "normalization": ..., "demoted": [...], "reason": "...", "demotion_fallback": bool, "failed_heads": [...]}.

FAIL-OPEN, always: never raises. Missing/unreadable metrics for a head leave that head's configured weight untouched (a promotion verdict that cannot be checked is not evidence of a problem). If demotion would zero out EVERY head's weight, the entire operation is skipped and the originally configured policy is returned unchanged, with reason recording why - demoting every head would silently stop the book from ever engaging, a far bigger behavior change than "prefer a different head" implies.

resolve_sleeve_guard_config

resolve_sleeve_guard_config(raw: dict[str, Any] | None) -> SleeveGuardConfig

Config block -> SleeveGuardConfig; absent or malformed keys fall back to the defaults (disabled).

resolve_target_weights_config

resolve_target_weights_config(raw: Mapping[str, Any] | None) -> TargetWeightsConfig

A TargetWeightsConfig from phase_v2.target_weights, every number finite and non-negative, defaults where missing.

resolve_trend_sleeve_config

resolve_trend_sleeve_config(raw: Mapping | None) -> TrendSleeveConfig

phase_v2.trend_sleeve -> TrendSleeveConfig. Absent block = disabled; a malformed number keeps its default.

rotation_cooldown_active

rotation_cooldown_active(current_bar_index: int, last_rotation_bar: int | None, cooldown_bars: int) -> bool

V4.6 (development/Problems.md #57/#58/#59) - anti-thrashing guard for options rotation: True while current_bar_index is within cooldown_bars of the last bar a rotation (liquidate + re-enter) actually executed for this symbol_key. Pure bar-index arithmetic, mirroring option_auto_close_due()'s own pure-predicate-only pattern - main.py's rotate_on_drift branch is what actually degrades to the "manage nearest position in place" fallback when this returns True, never a hard block. Returns False (cooldown never active) when last_rotation_bar is None - no rotation has ever fired for this symbol_key yet, never a crash on the first-ever rotation.

same_month_seasonality

same_month_seasonality(known: DataFrame, *, years: int = 7, min_years: int = 5) -> pd.DataFrame

The mean return of this calendar month over the previous years years (Heston and Sadka), needing min_years of them. Only completed months of earlier years are read.

scale_and_cap

scale_and_cap(raw: Series, covariance: DataFrame, asset_class: Mapping[str, str], settings: WeightSettings) -> dict[str, float]

One ex-ante volatility target, then the per-name, per-class and gross caps. Crypto is long-only.

sector_relative_scores

sector_relative_scores(scores_by_symbol: dict[str, float], sector_by_symbol: dict[str, str] | None, min_sector_size: int = 3) -> dict[str, float]

Each score minus the mean of its sector's scores in this cross-section (V6.4.0, lead L4): a rank within the sector's own peers rather than across sectors. A sector with fewer than min_sector_size scored names (or none mapped) is de-meaned by the whole cross-section's mean instead, so its names keep their relative order.

select_arbitrage_box_legs

select_arbitrage_box_legs(spec: StrategySpec, available_chain: list[dict], target_delta: float) -> dict[str, dict] | None

shape_family == "arbitrage_box" (box_spread/short_box_spread) - wired-but-unreachable (development/Problems.md): registry/selector exist and are unit-tested, but no signal source in this codebase ever calls this - a real box spread requires a mispricing detector (rich/ cheap vs. the risk-free-rate-implied fair value), a separate future project. Reuses select_vertical_legs()-style same-expiry anchoring: the "higher"-strike-role legs (long_put, short_call) anchor by delta, the "lower"-strike-role legs (short_put, long_call) share the SAME strike as one of the anchor legs by construction (a box spread is a single strike pair, not 4 independent strikes).

select_arbitrage_conversion_legs

select_arbitrage_conversion_legs(spec: StrategySpec, available_chain: list[dict], target_delta: float) -> dict[str, dict] | None

shape_family == "arbitrage_conversion" (conversion/reverse_conversion) - wired-but-unreachable, same caveat as select_arbitrage_box_legs() above. Single shared strike for both legs (same-expiry, same-strike call+put pair - identical selection shape to select_straddle_legs()).

select_arbitrage_jelly_roll_legs

select_arbitrage_jelly_roll_legs(spec: StrategySpec, available_chain: list[dict], target_delta: float, grouped_chain_by_expiry: dict[str, list[dict]] | None = None) -> dict[str, dict] | None

shape_family == "arbitrage_jelly_roll" (jelly_roll/short_jelly_roll) - wired-but-unreachable, same caveat as the other 2 arbitrage selectors above. Same expiry-grouping primitive as select_calendar_legs(), but requires all 4 legs (near call, far call, near put, far put) to share ONE strike across both expiries.

grouped_chain_by_expiry (V4.9 Priority 3) - same optional precomputed-grouping parameter as select_calendar_legs(), same default- None-reproduces-original-behavior contract. In practice never actually passed today (this shape family is permanently unreachable via route_multi_leg_option_sizing()'s own risk_tier routing - see its "unreachable_arbitrage" branch), kept for interface symmetry with select_calendar_legs() and in case a future caller reaches it directly.

select_arbitrage_signal

select_arbitrage_signal(strategy_name: str, available_chain: list[dict], current_date: date, risk_free_rate: float, min_mispricing_bps: float, target_delta: float = 0.5, dividend_payments: Iterable[tuple[date, float]] | None = None) -> bool

Composes the existing V4.5 arbitrage leg selectors (select_strategy_legs(), already registry-driven) with the fair-value/ threshold functions above: True iff THIS bar's chain shows an exploitable mispricing for strategy_name, using each leg's own bid/ask and days-to-expiry. Strike/expiry ROLES (which role is "higher"/"lower"/"near"/"far") are resolved from MULTI_LEG_STRATEGY_REGISTRY, never hardcoded per strategy_name - the short_* variants invert which role holds which strike/side while keeping the same role NAMES. Returns False (never a crash) on any missing selection/pricing/date data, or for any strategy_name outside the 6 arbitrage families - degrade-to-absent, matching every other selector's convention in this module's sibling file.

select_backspread_legs

select_backspread_legs(spec: StrategySpec, available_chain: list[dict], target_delta: float, backspread_delta_offset: float = 0.2) -> dict[str, dict] | None

shape_family == "backspread" - 1:2-ratio 2-leg selector shared by all 4 variants (call_backspread/short_call_backspread/put_backspread/ short_put_backspread); which strike_role ("lower"/"higher") is the anchor and which side each role maps to both come from the registry, so this one function is correct for every variant without branching on strategy_name.

select_butterfly_legs

select_butterfly_legs(spec: StrategySpec, available_chain: list[dict], target_delta: float, wing_width_delta_offset: float = 0.2, max_strike_asymmetry_tolerance: float = 0.15) -> dict[str, dict] | None

shape_family == "butterfly" - middle strike anchored by delta (the 2x-ratio body leg), then wings picked as the nearest available strikes ABOVE/BELOW it within the same expiry, degrading to None (never a forced/asymmetric fill) when the two wing strikes aren't within max_strike_asymmetry_tolerance of being equidistant from the middle - real discrete strike ladders widen away from spot, so "best-effort near-symmetric" is documented here as the honest contract, not exact equidistance.

select_calendar_legs

select_calendar_legs(spec: StrategySpec, available_chain: list[dict], target_delta: float, grouped_chain_by_expiry: dict[str, list[dict]] | None = None) -> dict[str, dict] | None

shape_family == "calendar" - a genuinely new primitive (nothing before this pass compares two DIFFERENT expiries): groups the chain by expiry first (group_chain_by_expiry(), O(n) once), requires at least 2 distinct expiries, picks the near leg's strike by delta within the nearest expiry, then requires the SAME strike be present in every LATER expiry group - the far leg uses the closest-dated expiry that has it. Returns None for a chain with fewer than 2 expiries (routine for a thinly configured universe, never a crash).

grouped_chain_by_expiry (V4.9 Priority 3), when given, is used INSTEAD OF recomputing group_chain_by_expiry(available_chain) here - lets a caller trying several calendar-family candidates against the SAME chain in one bar (risk/asset_class_router.py:: route_multi_leg_option_sizing()) group it once and reuse it. Default None reproduces the exact original per-call regroup behavior.

select_covered_protective_leg

select_covered_protective_leg(spec: StrategySpec, available_chain: list[dict], target_delta: float) -> dict[str, dict] | None

shape_family in {"covered_protective", "collar"} - the OPTION leg(s) only (never the equity side - see main.py's corrected cross-asset design, the equity leg is read from the currently-held quantity, not selected from a chain). protective_collar has 2 anchor legs (call and put), each selected independently by delta since a collar's call/put strikes are deliberately NOT required to be equidistant from spot (call_strike > put_strike is the only real constraint, enforced upstream by OptionStrategies.ProtectiveCollar itself).

select_iron_butterfly_legs

select_iron_butterfly_legs(spec: StrategySpec, available_chain: list[dict], target_delta: float, wing_delta_offset: float = 0.2, max_strike_asymmetry_tolerance: float = 0.15) -> dict[str, dict] | None

shape_family == "iron_butterfly" - the ATM strike is shared by BOTH middle legs (short_put and short_call both sit at the same strike, per OptionStrategies.cs's IronButterfly == IronCondor with its 2 middle strikes collapsed together), with the OTM wings picked equidistant (tolerance-based, same contract as select_butterfly_legs()) rather than independently re-selected by delta on each side.

select_iron_condor_legs

select_iron_condor_legs(spec: StrategySpec, available_chain: list[dict], target_delta: float, short_leg_delta_offset: float = 0.2, wing_delta_offset: float = 0.1) -> dict[str, dict] | None

shape_family == "iron_condor" - composed from 2 independent credit-vertical-style selections (put side, call side), both anchored to the SAME expiry (the put side's anchor expiry is picked first and passed down to the call side, closing the same expiry-drift risk select_vertical_legs() closes for a plain 2-leg vertical). The inner legs are the registry's anchors: the shorts of an iron condor, the longs of a short iron condor, whose short legs are the outer wings. Lean's factory rejects anything but ascending strikes.

select_ladder_legs

select_ladder_legs(spec: StrategySpec, available_chain: list[dict], target_delta: float, ladder_wing_delta_offset: float = 0.15) -> dict[str, dict] | None

shape_family == "ladder" - 3-strike, 1:1:1-ratio selector shared by all 4 variants (bear_call_ladder/bull_call_ladder/bear_put_ladder/ bull_put_ladder). Anchors on the spec's anchor leg by delta, then picks the other 2 legs as the nearest available strikes further from the anchor in the direction _outer_strike_predicate defines for this right - same expiry-anchoring guarantee as every other selector here.

select_naked_leg

select_naked_leg(spec: StrategySpec, available_chain: list[dict], target_delta: float) -> dict[str, dict] | None

shape_family == "naked" (naked_call/naked_put) - a single short leg, nearest-|delta| selection, no expiry-anchoring needed (only 1 leg).

select_raw_rank_score

select_raw_rank_score(sequence_prediction: dict[str, Any] | None, multitask_payload: dict[str, Any] | None, policy: dict[str, Any]) -> tuple[float | None, str]

Replaces main.py's previous hardcode (always prefer the sequence model's rank_20d, fall back to multitask's). Now a weighted blend across policy["heads"], each head's own value resolved via policy["model_priority"] (sequence before multitask, by default).

Returns (score, source) where source records exactly which heads/ models contributed, e.g. "sequence:0.6rank_5d+0.4rank_20d" or "sequence:1.0rank_20d,multitask:1.0rank_5d" when different heads come from different models. Returns (None, "no_rank_available") when no configured head has a value from any model this bar (e.g. every model still warming up).

select_single_leg_contract

select_single_leg_contract(available_chain: list[dict], target_delta: float, right: str) -> dict[str, Any] | None

Picks the chain entry of the requested right ("call"/"put") whose |delta| is closest to |target_delta| - nearest-strike-by-delta selection, no ML, no spread construction. Each entry in available_chain must already carry a computed "delta" key (see features/options_greeks.py::compute_greeks() - this function does no pricing itself, purely a selection over already-priced rows).

Returns None when available_chain is empty or has no entry of the requested right (e.g. IB disabled -> empty chain, or a chain with only calls when a put was requested) - degrade-to-absent, never a crash.

select_straddle_legs

select_straddle_legs(spec: StrategySpec, available_chain: list[dict], target_delta: float) -> dict[str, dict] | None

shape_family == "straddle" (straddle/short_straddle) - same-strike, same-expiry call+put pair. Anchors on the call leg by delta, then requires a put at the EXACT same strike/expiry (never a nearest-delta put pick - a straddle's defining property is one shared strike).

select_strangle_legs

select_strangle_legs(spec: StrategySpec, available_chain: list[dict], target_delta: float, strangle_delta_offset: float = 0.15) -> dict[str, dict] | None

shape_family == "strangle" (strangle/short_strangle) - offset-strike, same-expiry call+put pair (call strike above put strike, enforced by _outer_strike_predicate-style filtering anchored on the call leg's expiry, so both legs land on the same expiry by construction).

select_strategy_legs

select_strategy_legs(strategy_name: str, available_chain: list[dict], target_delta: float, grouped_chain_by_expiry: dict[str, list[dict]] | None = None, **tuning_kwargs: Any) -> dict[str, dict] | None

Single dispatch point every caller (main.py, tests) should use instead of calling a shape-family selector directly - looks up strategy_name's StrategySpec then its shape_family's selector, a plain dict lookup (O(1)) rather than an if/elif chain over 43 names, matching this module's "registry, not 41 near-duplicate functions" design goal. "vertical" strategies use select_vertical_legs() directly (not in the dispatch dict above, since it takes the full spec + is reused by 4 strategy_names identically). tuning_kwargs passes through to whichever selector is chosen (e.g. short_leg_delta_offset, wing_width_delta_offset) - unrecognized kwargs for a given selector are simply not in its signature and would raise, exactly like any other Python call; callers should only pass kwargs relevant to the strategy family they're invoking, or none at all to accept every selector's own defaults.

grouped_chain_by_expiry (V4.9 Priority 3) is a DEDICATED top-level parameter, not folded into tuning_kwargs - it's only meaningful for the 2 shape families in _GROUPED_CHAIN_SHAPE_FAMILIES (calendar, arbitrage_jelly_roll) and is deliberately swallowed (never forwarded) for every other strategy_name, since blindly passing it through tuning_kwargs would raise a TypeError for the 12 selectors that don't accept it. Default None reproduces the exact original per-call regroup behavior for every strategy_name, unchanged.

select_vertical_legs

select_vertical_legs(spec: StrategySpec, available_chain: list[dict], target_delta: float, short_leg_delta_offset: float = 0.2) -> dict[str, dict] | None

Shared 2-leg vertical selector for shape_family == "vertical" - covers debit (bull_call_spread/bear_put_spread) AND credit (bear_call_spread/bull_put_spread) verticals identically: the ANCHOR leg (spec's is_anchor=True role) is picked by nearest-|delta| exactly like the existing single-leg selector; the OUTER leg is the nearest-|delta - short_leg_delta_offset| match among the SAME expiry's rows on the further-OTM side (_outer_strike_predicate). Which role ends up "long" vs "short" is irrelevant to this SELECTION geometry - that's already fixed by the registry (spec.legs[*].side) - so the same function is correct for both debit and credit shapes, closing the debit/credit leg-role-inversion bug without needing 2 separate selection algorithms.

select_vertical_spread_legs

select_vertical_spread_legs(available_chain: list[dict], target_delta: float, right: str, short_leg_delta_offset: float) -> tuple[dict, dict] | None

Picks two same-expiry, same-right chain rows for a vertical spread: the long leg via the EXACT same nearest-|delta| selection select_single_leg_contract() already uses (no new selection logic duplicated for it), and the short leg as the nearest-|delta| match to (target_delta - short_leg_delta_offset) among rows of the SAME expiry as the long leg, filtered to the side that caps risk (strike > long strike for a call vertical, strike < long strike for a put vertical - enforced explicitly by filtering on strike, not inferred from delta ordering, since assuming monotonic real-market delta-vs-strike ordering shouldn't be load-bearing here).

Returns None when: no usable long leg (same degrade-to-absent condition as select_single_leg_contract()), or no candidate short leg exists at the long leg's expiry on the risk-capping side - a chain with only one usable strike degrades to "no spread", never a crash.

short_reversal

short_reversal(known: DataFrame, window: int = 7) -> pd.DataFrame

Minus the trailing return: last week's losers rank higher.

should_exit_non_selected_book_symbol

should_exit_non_selected_book_symbol(book_is_active: bool, portfolio_book_enabled: bool, is_currently_invested: bool) -> bool

V5.2.4 (development/Problems.md #91) - whether main.py's Pass 2 should force-close a currently-invested, book-managed position because the book engaged this bar and didn't select it (the "rotation exit").

Returns False whenever book_is_active is False (this bar's book_allocations came back empty) - matching build_rank_based_book()'s own documented contract just above: a disengaged book (min_rank_confidence_spread gate failure, or - pre-V5.2.4 - a thin/ off-session-tick artifact) is "byte-identical to this module not existing at all", never a liquidation trigger. Before this function existed, main.py's own Pass 2 violated that documented contract - force-selling EVERY currently-invested book symbol whenever book_allocations didn't contain it, without distinguishing "the whole book disengaged today" from "this specific symbol got rotated out of a REAL, active book" (the only case that should ever force an exit).

Callers pass book_is_active=bool(book_allocations) - symbol_key not in book_allocations (i.e. "this symbol specifically wasn't selected") is the caller's own responsibility to check separately, same as before; this function only answers "is a force-exit ever appropriate given the book's overall state this bar", not "for this particular symbol".

should_hold_owned_position_on_veto

should_hold_owned_position_on_veto(enabled: bool, is_book_member: bool, is_book_owned: bool, is_currently_invested: bool, book_is_active: bool) -> bool

V5.5.0 (Problems.md #133) - while the book is VETOED (empty allocations: spread / rolling-IC gate), a position the book opened must be HELD, not handed to the legacy probability signal. Live never liquidates on a veto (should_exit_non_selected_book_symbol needs an active book) and the offline as-live simulator holds through one (hold_positions_on_veto), but main.py let the legacy signal manage those positions: its next "sell" closed them, so book positions lasted ~6 days instead of the designed ~20+ (and shorts were re-sized to legacy weights). Only the max-holding / trailing-stop backstops and the next ACTIVE book's rotation exit may close an owned position. Off (False) reproduces pre-V5.5.0.

should_rebalance_this_bar

should_rebalance_this_bar(bar_index: int, rebalance_every_bars: int, has_previous_allocation: bool, is_trading_day_bar: bool = True, phase_offset: int = 0) -> bool

Stage 2 of the rank-pivot roadmap (development/Problems.md#43): the book previously re-ranked and re-formed on EVERY bar, churning positions far faster than rank_20d's actual ~20-trading-day horizon can support (653 orders/~2yr, no edge left after costs). This is a pure, main.py- independent extraction of main.py::on_data()'s rebalance-gating decision so it's unit-testable without a running Lean/QCAlgorithm instance (this module has no Lean import, main.py does).

bar_index is main.py's 1-indexed self.bar_index (incremented to 1 on the very first on_data() call, before any bar has been processed). rebalance_every_bars is phase_v2.portfolio_book.rebalance_every_bars (1 reproduces the previous every-bar-rebalance behavior exactly, since (n - 1) % 1 == 0 for every n). has_previous_allocation should be False only on a genuinely fresh run (no cached book yet) - it forces a rebalance regardless of the modulo so the book is never left empty.

is_trading_day_bar (V5.2.4, development/Problems.md #91) - default True, fully backward-compatible with every existing caller. False on a tick main.py has determined isn't the equity-session tick (e.g. an off-session crypto/forex-only Lean Slice, see main.py's own is_equity_session_bar) - never rebalances on such a tick, full stop, regardless of bar_index's modulo or has_previous_allocation. This exists because main.py's bar_index only advances on equity-session ticks post-V5.2.4 - without this explicit gate, several consecutive off-session ticks following a real rebalance-eligible bar would keep re-evaluating the SAME already-satisfied modulo condition and could re-trigger a rebalance that already happened.

Returns True on bar_index 1, 1+N, 1+2N, ... (i.e. 0, N, 2N, ... in 0-indexed bar terms) - False on every bar in between, where the caller should reuse its previously cached allocation and simply hold.

phase_offset (V6.2.0, staggered tranches): shifts the cadence by that many bars (0 = the cadence above).

should_record_ic_event

should_record_ic_event(security_type: str, fresh_bar: bool, mode: str) -> bool

Whether this tick's rank event for one symbol enters the rolling-IC buffer: always, except a late-bar symbol re-scored without a fresh bar under "once". Shared by main.py and the offline replica (evaluation.live_parity).

signal_lookbacks

signal_lookbacks(signal: str) -> tuple[int, ...]

The lookbacks a trend signal name stands for (sign_252 is 252; sign_blend the blend's).

signal_to_weights

signal_to_weights(signal: DataFrame, panel: ResearchPanel, *, kind: str, rebalance_every: int, phase_offset: int = 0, settings: WeightSettings | None = None, start: str | None = None, end: str | None = None, listed: Callable[[str, str], bool] | None = None, top_k: int | None = None) -> dict[str, dict[str, float]]

{date: {ticker: weight}} on every rebalance_every-th session from start, counted from phase_offset.

similarity_table

similarity_table(rows: DataFrame, index: Index, columns: Index, *, max_age: int = 130) -> pd.DataFrame

The newest 10-K or 10-Q's similarity to the same form a year earlier (Cohen, Malloy, Nguyen): high means the filing is unchanged. Blank after max_age sessions without a newer one.

sleeve_classes_out_of_main_pool

sleeve_classes_out_of_main_pool(configs: Mapping[str, ClassSleeveConfig], class_enabled: Mapping[str, bool]) -> frozenset[str]

Asset classes that leave the main book's candidate pool: those with an enabled sleeve whose class may trade.

sleeve_signal

sleeve_signal(spec: SleeveSpec, panel: ResearchPanel) -> pd.DataFrame

The sleeve's raw signal, masked where a name lacks enough history for its risk estimate.

slippage_bps_for

slippage_bps_for(asset_class: str, config: TargetWeightsConfig) -> float

The slippage the simulator charges one asset class, in bps.

strategies_for_volatility_view

strategies_for_volatility_view(volatility_view: str) -> frozenset[str]

Pure lookup: which registered strategy_names express volatility_view ("long_vol" -> long straddle/strangle; "short_vol" -> short straddle/ strangle + iron condor/butterfly, both defined- and unbounded-risk variants included so risk_tier_preference below can choose between them; "neutral" -> empty, no strategy actively expresses a neutral view here). Iron condor/butterfly are "short_vol" regardless of the long/short spelling of their own name (short_iron_condor is itself already the credit/short-vol side; there is no long_iron_condor concept in this shape family).

strategy_reserve

strategy_reserve(*, rebalance_next: bool, book_gross_cap: float, max_short_exposure: float, formed_long: float, formed_short: float, book_long: float, book_short: float, other_budget_remaining: float, neutral_budget: float = 0.0) -> dict[str, Any]

Gross the strategy may add by the next session, by side, and the margin it would hold. On a rebalance attempt a whole new book can be ordered (long up to half the cap, short up to half the cap and max_short_exposure) while the current one is still held, since sells free nothing until they fill; otherwise only the formed book's unfilled part. other_budget_remaining (the legacy signal) is counted as long at leverage 1, the worst case for both limits; neutral_budget (the class sleeves, dollar-neutral by construction: build_class_sleeve_weights) as half long and half short at equity leverage.

sweep_order_needed

sweep_order_needed(current_weight: float, target_weight: float, min_trade_weight: float) -> bool

Trade when the gap is at least min_trade_weight, or whenever the sweep is above its target (a cut for the limits is never deferred).

sweep_reserve

sweep_reserve(config: TargetWeightsConfig) -> dict[str, Any]

What the T-bill sweep leaves free for the next session's orders on this path: a fixed share of gross, since the next row of the file must not be read before its date.

term_credit_timing

term_credit_timing(fred_table: DataFrame, known: DataFrame, *, ticker: str = 'SPY', window: int = 252) -> pd.DataFrame

1.0 on ticker while the term spread (10 years less 3 months) is positive and the Baa credit spread is at or below its own trailing median, else 0.0. NaN when a rate is missing.

time_series_momentum

time_series_momentum(panel: ResearchPanel, spec: SleeveSpec) -> pd.DataFrame

Direction of the trailing return, averaged over the lookbacks.

trailing_drawdown

trailing_drawdown(pnl_increments: Sequence[float], lookback_bars: int) -> float

The current drawdown at the latest bar (a positive number, same unit as the increments): the cumulative P&L's running peak over the last lookback_bars increments minus where it ends, not the largest fall inside the window (formal/AetherFormal/SleeveGuard.lean). Non-finite increments count as 0. A non-positive lookback reads no window (V6.4.3: xs[-0:] is the whole history).

tranche_book_params

tranche_book_params(top_n: int, bottom_n: int, gross_exposure_cap: float, tranches: int, max_active_positions: int | None = None) -> dict[str, Any]

One tranche's share of the book: ceil(top_n / K) longs and ceil(bottom_n / K) shorts (a side with 0 names stays 0), gross cap / K and, when given, ceil(position cap / K) positions. K = 1 returns the book's own values.

tranche_offsets

tranche_offsets(rebalance_every_bars: int, tranches: int) -> list[int]

V6.2.0 staggered book: the phase offset of each of tranches sub-books, spread evenly over one cadence (20 bars, 2 tranches -> [0, 10]). One tranche -> [0], the single book.

trend_signal

trend_signal(closes: Sequence[float], signal: str) -> float | None

sign_N: the sign of the trailing N-session log return (-1, 0, +1); sign_blend: the mean of sign_63/126/252. None when the history is shorter than the longest lookback + 1 or holds a non-finite / non-positive close.

trend_sleeve_weights

trend_sleeve_weights(closes_by_symbol: Mapping[str, Sequence[float]], config: TrendSleeveConfig, *, annualization: float = 252.0) -> dict[str, float]

Signed target weight (fraction of NAV) per symbol of config.symbols with a non-zero signal and a volatility: w = s * (target_vol / sqrt(N_active)) / sigma, each |w| capped at max_weight_per_name, then all scaled down so that sum |w| <= gross_cap. {} when disabled or nothing qualifies.

trend_votes

trend_votes(known: DataFrame, lookbacks: tuple[int, ...] = (20, 60, 120)) -> pd.DataFrame

The installed rule's votes: the share of lookbacks over which the trailing return is positive. NaN until the longest has a price.

turn_of_month

turn_of_month(panel: ResearchPanel, spec: SleeveSpec) -> pd.DataFrame

Hold SPY over the month's last session and the next three. A decision is filled at the next open, so the holding days are decided one session earlier. Calendar arithmetic only (weekdays, no holiday table: never reads the future).

update_book_owned_symbols

update_book_owned_symbols(owned: set[str], symbol_key: str, is_book_member: bool, is_currently_invested: bool) -> bool

V5.5.0 (Problems.md #128) - which held positions the BOOK opened, as opposed to the legacy-signal sleeve. Mutates owned in place and returns whether symbol_key is book-owned after the update: invested AND a member this bar adds it; a flat symbol is dropped; an invested non-member keeps whatever status it already had (that is exactly the rotated-out case).

update_missing_bars

update_missing_bars(missing: Mapping[str, int], held_units: Mapping[str, float], has_bar: Mapping[str, bool]) -> dict[str, int]

Consecutive bars each held name has gone without one. A name not held, or with a bar, starts again from zero.

vix_term_structure

vix_term_structure(panel: ResearchPanel, spec: SleeveSpec) -> pd.DataFrame

Long SPY while VIX is below VIX3M. threshold gives an on/off rule; without it, exposure grows with the gap.

volume_trend

volume_trend(quote_volume: DataFrame, short: int = 7, long: int = 28) -> pd.DataFrame

Log of the short mean volume over the long mean: rising attention ranks higher.

weights_at_tick

weights_at_tick(weights: Mapping[str, Mapping[str, float]], tick_date: str) -> dict[str, float] | None

The decision taken at this date's equity close; None when the file has no row for it (hold what is held).

weights_sidecar

weights_sidecar(path: Path | str) -> Path

<weights>.sha256: what a live run checks each reload against. The file grows daily, so no config can pin its hash.

wide_tables

wide_tables(features: Mapping[str, DataFrame], member: DataFrame) -> dict[str, pd.DataFrame]

Every w-trial's table that its features allow. A feature all NaN (no data for this window) leaves its trial with an empty book, which the minimum-names check reports rather than reading as no skill.

write_weight_file

write_weight_file(weights_by_date: Mapping[str, Mapping[str, float]], path: Path | str) -> str

Writes the file (LF, rows sorted, so one set of weights has one hash) and returns its sha256. A date with no positions gets a __FLAT__ row with weight 0: without it, "go flat" would read as "no decision today".

write_weights_sidecar

write_weights_sidecar(path: Path | str) -> str

Records the file's current hash, atomically, so a reload never reads a half-written one.