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