Skip to content

aether_quant.costs

Trading costs, grouped by the job (V6.6.5): composable cost models with the live path's fee presets, the square-root impact term, and the round-trip and net-edge checks the order path runs.

Trading costs, grouped by the job (V6.6.5): composable cost models with the live path's fee presets, the square-root impact term, and the round-trip and net-edge checks the order path runs.

CompositeCostModel dataclass

The sum of its parts.

models class-attribute instance-attribute

models: Sequence[CostModel] = field(default_factory=tuple)

cost_bps

cost_bps(trade_value: float, *, adv_usd: float | None = None, daily_vol: float | None = None) -> float

CostModel

Bases: Protocol

Anything with cost_bps(trade_value, *, adv_usd, daily_vol) -> float (never negative, never NaN).

cost_bps

cost_bps(trade_value: float, *, adv_usd: float | None = None, daily_vol: float | None = None) -> float

LinearCostModel dataclass

A commission and half the quoted spread, both per side in bps, plus an optional dollar minimum per trade.

commission_bps class-attribute instance-attribute

commission_bps: float = 0.0

half_spread_bps class-attribute instance-attribute

half_spread_bps: float = 0.0

min_fee_usd class-attribute instance-attribute

min_fee_usd: float = 0.0

cost_bps

cost_bps(trade_value: float, *, adv_usd: float | None = None, daily_vol: float | None = None) -> float

SquareRootImpactModel dataclass

Temporary impact eta * daily_vol * sqrt(trade / ADV), in bps. Zero when ADV or volatility is unknown, so an unknown is visible as a missing input rather than a guessed cost.

eta class-attribute instance-attribute

eta: float = 0.5

cost_bps

cost_bps(trade_value: float, *, adv_usd: float | None = None, daily_vol: float | None = None) -> float

build_net_edge_decision

build_net_edge_decision(predicted_rank: float | None, liquidity_payload: dict[str, Any], order_value: float, cost_config: dict[str, Any], *, trade_direction: int = 1, security_type: str | None = None) -> NetEdgeDecision

The one call site analyzer/market_analyzer.py's Priority 6.5 tier and risk/position_sizing.py::cost_sizing_multiplier() both consume.

trade_direction (+1 long, -1 short) must be the sign of the trade actually being evaluated - see expected_edge_bps()'s docstring for why this parameter exists at all. Defaults to 1 (long) only so callers that genuinely never trade short (or existing tests exercising the long side) do not need to pass it; every live call site must supply the real signed direction.

FAIL-OPEN, always: returns passes=True, reason="net_edge_gate_disabled" whenever cost_config["enabled"] is false, edge_bps_per_rank_unit is 0.0 (uncalibrated - see aq evaluate --calibrate-edge), or predicted_rank is None. An uncalibrated or disabled cost model must never block a trade - the exact same "missing/degraded signal never changes trading behavior" contract every other optional overlay in this codebase (topology_sizing_multiplier(), rank_sizing_multiplier()) already follows.

coinbase_model

coinbase_model(date: str = '2026-01-01', *, slippage_bps: float = 10.0) -> LinearCostModel

Coinbase market orders as the live path and its simulator charge them: the date-effective taker fee plus the crypto slippage the target-weights simulator assumes (portfolio.target_weights.DEFAULT_SLIPPAGE_BPS).

compute_sqrt_impact_bps

compute_sqrt_impact_bps(*, trade_size_fraction: float, daily_volatility: float, avg_daily_dollar_volume: float, portfolio_value: float, eta: float = 0.5) -> float

Computes temporary market impact in basis points.

Parameters:

Name Type Description Default
trade_size_fraction float

absolute weight change (e.g. 0.05 = 5% of book).

required
daily_volatility float

rolling daily return std (e.g. 0.02 = 2%).

required
avg_daily_dollar_volume float

asset's ADV in dollars (e.g. 50_000_000).

required
portfolio_value float

total book value in dollars.

required
eta float

impact coefficient — higher for less liquid assets. Calibrated defaults: equity ~0.5, crypto ~1.0, bond ETF ~0.3.

0.5

Returns:

Type Description
float

Impact cost in basis points (always >= 0). Zero when any input is

float

degenerate (zero size, zero ADV, zero vol).

cost_usd

cost_usd(model: CostModel, trade_value: float, *, adv_usd: float | None = None, daily_vol: float | None = None) -> float

The model's cost of one trade in dollars.

estimate_round_trip_cost_bps

estimate_round_trip_cost_bps(liquidity_payload: dict[str, Any], *, commission_bps_per_side: float, min_commission_usd: float, order_value: float, extra_slippage_bps: float) -> float

Reads liquidity_payload["estimated_round_trip_cost"] - already computed by liquidity/market_liquidity.py::build_liquidity_decision() (a fraction, e.g. 0.001 = 10bps) - and NEVER recomputes slippage; one estimate per bar, consistently used everywhere (the same rule main.py:1768-1771's own comment states). Adds the commission leg, expressed as bps of order_value with a min_commission_usd dollar floor (a small order can be commission-dominated even at a low bps rate), plus any extra_slippage_bps buffer (a deliberately simple manual safety margin, config-gated, defaults to 0.0).

order_value <= 0 (no order, or a zero-target-weight decision) returns just the liquidity round-trip cost plus extra_slippage_bps - the commission-floor conversion to bps is undefined at zero order value, so it is skipped rather than divided-by-zero.

V5.4.5 (development/Problems.md #116): the commission leg now covers BOTH sides of the round trip. The original math charged exactly one side's commission (rate leg and min_commission_usd floor alike) against a function whose contract - and this parameter's own name - is per-SIDE cost for a ROUND-TRIP estimate, understating expected cost by up to 2x and letting trades through whose true net edge was below the gate.

expected_edge_bps

expected_edge_bps(predicted_rank: float | None, *, edge_bps_per_rank_unit: float, holding_bars: int, horizon_days: int, trade_direction: int = 1) -> float

Linear-in-rank-deviation expected edge estimate, measured IN THE DIRECTION OF THE TRADE. rank_deviation = (rank - 0.5) * 2 is positive for a top-ranked asset (expected to rise) and negative for a bottom-ranked one (expected to fall) - trade_direction (+1 long, -1 short) says which side the caller actually intends to trade, so the result is the edge realized if that trade is entered, not merely the raw long-side edge.

A rank of 0.5 (median - no view) always has zero expected edge. A rank of 1.0/0.0 has the full edge_bps_per_rank_unit magnitude, on whichever side the rank itself favors (long the top, short the bottom) - the book case, where trade_direction is derived from the same rank that picked the side, so the product is always the FULL magnitude, never negative. A mismatched trade (e.g. going long a bottom-ranked asset) correctly comes out negative and gets vetoed downstream.

Scaled down by min(1, holding_bars/horizon_days) - the rank head's own forward horizon (horizon_days, e.g. 20 for rank_20d) is how long the FULL predicted move is expected to take; holding for fewer bars than that only captures a fraction of it.

predicted_rank=None (model unavailable this bar) -> 0.0, the safe no-edge default, matching every other "missing prediction never changes trading behavior" contract in this codebase.

GUARDRAIL (Problems.md - net-edge gate blocked 100% of shorts): before this parameter existed, the gate measured only the raw long-side edge and applied it unconditionally to buy/sell/short decisions alike, so every short entry (negative rank_deviation) was vetoed on every bar. trade_direction must always be threaded through from the caller's own signed intent (e.g. sign of the pre-cost target_weight) - never left at the default 1 for a short trade.

ib_equity_model

ib_equity_model(*, commission_bps: float = 1.5, min_fee_usd: float = 1.0, slippage_bps: float = 3.0, eta: float | None = 0.5) -> CostModel

US equities through IB as the target-weights path charges them (bps commission with a $1 floor, 3 bps slippage), with square-root impact on top unless eta is None.

route_aware_fee_bps

route_aware_fee_bps(override: dict[str, Any]) -> float | None

The route-aware per-side fee of a fee_by_type entry, or None when it carries no enabled route_aware block.