edgekit.costs
Transaction-cost models and the cost-stress harness. The repo used two conventions — crypto expresses cost as a fraction of price, FX as pips — and both live here. The decisive test is always the same: re-run at 1x/2x/3x cost. A real edge degrades gracefully; a fake one collapses.
What’s inside. Two frozen dataclasses — CostModel (fraction-of-price, the crypto convention) and PipCostModel(pips, the FX/index convention used by the engine’s EngineConfig) — plus one function, cost_stress, which is validation-gauntlet step 6. Both models carry a scaled(mult) method: that multiplier is the knob the stress harness turns.
CostModel.r_cost) or on the pip P&L (PipCostModel). Sizing to dollars happens later, from a single scalar. Keep it that way — a cost model that looks cheap on gross R can still kill an edge net of the spread you actually pay.CostModel#
CostModel#
The fraction-of-price cost model — the crypto convention, and the default cost used by every BaseStrategy.backtest. Cost is expressed as a fraction of the traded price: a round-trip spread plus a per-day financing/swap charge. Frozen (immutable) so a model can be shared safely and cheaply copied via scaled.
CostModel(spread_rt: float = 0.0012, swap_day: float = 0.0002)| Param | Type | Default | Meaning |
|---|---|---|---|
spread_rt | float | 0.0012 | Round-trip spread as a fraction of price (0.0012 = 12 bps). |
swap_day | float | 0.0002 | Financing/swap charged per day held, as a fraction of price (2 bps/day). |
Methods.
r_cost(entry_price, risk_per_unit, days) -> float— cost of one round trip expressed in R. Computes(spread_rt*price + swap_day*price*days) / risk_per_unit.risk_per_unitis the stop distance in price units (e.g.2 * ATR) — the same denominator that turns P&L into R. Returns0.0ifrisk_per_unit <= 0.scaled(mult) -> CostModel— a copy with both components multiplied bymult. This is the cost-stress knob.
from edgekit.costs import CostModel
cost = CostModel() # 12 bps round-trip + 2 bps/day
r = cost.r_cost(entry_price=30_000.0, risk_per_unit=1_200.0, days=8)
# -> ((0.0012*30000) + (0.0002*30000*8)) / 1200 == 0.07 R haircut
double = cost.scaled(2.0) # CostModel(spread_rt=0.0024, swap_day=0.0004)PipCostModel#
The pips-based cost model — the FX/index convention. This is the cost carried by the fixed-RR prop-firm engine (EngineConfig.cost). It rolls spread, per-side slippage and commission into a single round-trip cost_pips. Also frozen.
PipCostModel(spread_pips: float = 0.5, slippage_pips_per_side: float = 0.2,
commission_per_lot_rt: float = 3.0, pip_value_per_lot: float = 10.0)| Param | Type | Default | Meaning |
|---|---|---|---|
spread_pips | float | 0.5 | Round-trip spread in pips. |
slippage_pips_per_side | float | 0.2 | Slippage per side, in pips (counted on both entry and exit). |
commission_per_lot_rt | float | 3.0 | Round-trip commission per lot, in account currency. |
pip_value_per_lot | float | 10.0 | Value of one pip per lot — converts the commission into pips. |
Property & method.
cost_pips(property) — total round-trip cost in pips:spread_pips + 2*slippage_pips_per_side + commission_per_lot_rt/pip_value_per_lot.scaled(mult) -> PipCostModel— copy with spread, slippage and commission scaled bymult;pip_value_per_lotis a market constant and is not scaled.
from edgekit.costs import PipCostModel
pc = PipCostModel() # 0.5 spread + 2*0.2 slip + 3.0/10.0 commission
pc.cost_pips # -> 0.5 + 0.4 + 0.3 == 1.2 pips round-trip
pc.scaled(3.0).cost_pips # spread/slip/commission x3; pip value unchangedcost_stress#
cost_stress#
Gauntlet step 6. Re-run a strategy at escalating cost and return a {mult: metrics} map — the single most honest robustness check in the library. You supply run_fn, which takes a CostModel and returns a metrics dict (typically from trade_stats); cost_stress calls it once per multiplier with base.scaled(mult).
cost_stress(run_fn: Callable[[CostModel], dict], base: CostModel | None = None,
mults=(1.0, 2.0, 3.0)) -> dict| Param | Type | Default | Meaning |
|---|---|---|---|
run_fn | Callable[[CostModel], dict] | — | Runs the strategy at a given cost, returns a metrics dict. |
base | CostModel | None | None | Base cost to scale (defaults to CostModel() when None). |
mults | tuple | (1.0, 2.0, 3.0) | Multipliers applied to base; one metrics dict per multiplier. |
Returns: a dict keyed by each multiplier, whose values are whatever run_fn returns — e.g. {1.0: {...}, 2.0: {...}, 3.0: {...}}.
cost_stress is re-exported as edgekit.validation.cost_stress.from edgekit.costs import cost_stress
from edgekit import trade_stats
from edgekit.strategy import ORB
def run(cost):
return trade_stats(ORB(or_bars=30, target_r=2.0)
.backtest(rth, cost=cost, warmup=5, bars_per_day=390).r)
grid = cost_stress(run) # {1.0: {...}, 2.0: {...}, 3.0: {...}}
print(grid[1.0]["pf"], grid[2.0]["pf"], grid[3.0]["pf"]) # 0.71 / 0.45 / 0.29 — below 1, rejectedSee also#
- edgekit.metrics — the metrics dicts
run_fnreturns. - edgekit.sizing — turn net-of-cost R into dollar-risked equity.
- The validation gauntlet — where cost-stress is step 6.