edgekit

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.

!Cost is charged in R, once
edgekit never bakes dollars into a strategy. A strategy is priced in R-multiples; cost is a haircut on that R (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)
ParamTypeDefaultMeaning
spread_rtfloat0.0012Round-trip spread as a fraction of price (0.0012 = 12 bps).
swap_dayfloat0.0002Financing/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_unit is the stop distance in price units (e.g. 2 * ATR) — the same denominator that turns P&L into R. Returns 0.0 if risk_per_unit <= 0.
  • scaled(mult) -> CostModel — a copy with both components multiplied by mult. This is the cost-stress knob.
costmodel.py
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)
ParamTypeDefaultMeaning
spread_pipsfloat0.5Round-trip spread in pips.
slippage_pips_per_sidefloat0.2Slippage per side, in pips (counted on both entry and exit).
commission_per_lot_rtfloat3.0Round-trip commission per lot, in account currency.
pip_value_per_lotfloat10.0Value 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 by mult; pip_value_per_lot is a market constant and is not scaled.
pipcostmodel.py
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 unchanged

cost_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
ParamTypeDefaultMeaning
run_fnCallable[[CostModel], dict]Runs the strategy at a given cost, returns a metrics dict.
baseCostModel | NoneNoneBase cost to scale (defaults to CostModel() when None).
multstuple(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: {...}}.

!Survivor rule of thumb
PF must stay > 1 at 2x and 3xcost. If profit factor drops below 1 the moment you double the spread, the “edge” was living inside the cost assumption, not the market. cost_stress is re-exported as edgekit.validation.cost_stress.
cost_stress.py
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, rejected

See also#