Build the cost model
Why this lesson
Section titled “Why this lesson”This is the first thing you’ll build that free content mostly doesn’t hand you: a real per-instrument cost model. Lesson 1.3 costed one trade by hand; Lesson 1.4 measured trades in R. Now you connect them, so that any trade log can be run through the stack automatically and produce net numbers. Once this exists, “is there an edge?” becomes a computation instead of a hope — and this same model plugs straight into your backtester in Level 2.
Two references for the cost taxonomy. Both are shot in Zorro/MT4, so treat them as concept sources, not code to copy — a pandas model needs the ideas translated, not the file formats reused.
Segment: ~17:40–20:55 — modeling variable slippagewatch full video
The explainer
Section titled “The explainer”A cost model is a function: trade in, cost out. Feed it a trade’s context — when it was entered, how long it was held, how it exited, how big it was — and it returns the total friction that trade paid, so you can subtract it from the gross result. The whole point is to compute one number you cannot get any other way: the gross-vs-net gap, the amount of your apparent edge that the cost stack quietly consumes. If gross expectancy is +0.30R and net expectancy is +0.02R, the gap is 0.28R and you have almost no edge left. If net goes negative, you have none.
The model has four components, one per cost from Lesson 1.3, and each has a normalization trap the Zorro videos above hammer on:
- Spread, keyed by session. Spread is not one number; it’s tight in the London/NY overlap, wider in Asian hours, and blows out at the 5pm ET rollover (session behavior, rollover spread). So the model looks up spread by the entry hour, and adds a penalty if the trade fired during the rollover spike. The discipline the video stresses: when in doubt, set spread pessimistically, because an optimistic snapshot flatters everything downstream.
- Commission, per round-turn lot, roughly $3–$7 on a raw account (broker models). The trap: a quote “per side” must be doubled for the round turn, and a quote in another currency or per-100k must be rebased to your units. Skipping this is how beginners default commission to zero and inflate a backtest.
- Swap, per night, with the triple-swap Wednesday rule (FP Markets guide). The model counts nights held and charges triple for any rollover that lands on a Wednesday. A swing system holding several nights can lose more to swap than to spread — this is the component intraday-only tutorials omit entirely.
- Slippage, as a haircut on stop exits, since stops fill worse than their trigger and lean negative — 57% negative in FXCM’s stats (FXCM). The honest version, as the video shows, is a distribution of slip, but a fixed pessimistic haircut is the right starting point.
Assemble those four and you have a per-instrument model. Run a trade log through it, recompute each trade’s result net of its own cost, convert to R, and average: that average is your net expectancy. Report it beside gross expectancy and you’ve quantified the gap that decides whether anything is real. The code below is deliberately small and readable — it is the honest core, and you’ll extend it (a slip distribution, per-instrument parameter tables, tick-level spread) as you grow. But even this version will kill more bad ideas than any indicator ever will.
Build the model and run a log through it. Save this as cost_model.py and run it — it’s self-contained.
import pandas as pd
# --- 1. Per-instrument cost parameters (EUR/USD, raw account) ---PIP_VALUE = 10.0 # USD per pip, per standard lotCOMMISSION = 6.0 # USD per round-turn standard lotSWAP_PER_NIGHT = -5.0 # USD per night, per standard lot (negative = you pay)SLIP_STOP = 2.0 # pips of negative slippage assumed on a stop exit
def session_spread(hour): """Spread in pips, by entry hour (UTC). Set pessimistically.""" if 7 <= hour < 16: # London / NY overlap — tightest return 0.8 if 0 <= hour < 7: # Asian session — wider return 1.6 return 1.2 # other hours
def rollover_penalty(hour): return 12.0 if hour == 21 else 0.0 # ~21:00 UTC = 5pm ET spread spike
def swap_units(entry_weekday, nights): """Count swap charges over the hold; a Wednesday rollover (weekday 2) triples.""" units = 0 for k in range(1, nights + 1): day = (entry_weekday + k) % 7 units += 3 if day == 2 else 1 return units
# --- 2. A trade log: gross results, straight off the price chart ---# hour = entry hour UTC, weekday = Mon..Sun as 0..6,# lots = position size, nights = nights held, stop = exited via a stop,# gross = gross result in pips, risk = initial stop distance in pips (1R)trades = pd.DataFrame([ dict(hour=9, weekday=0, lots=1.0, nights=0, stop=True, gross=15, risk=10), dict(hour=21, weekday=2, lots=1.0, nights=1, stop=True, gross=22, risk=10), dict(hour=3, weekday=1, lots=0.5, nights=4, stop=False, gross=40, risk=20), dict(hour=13, weekday=4, lots=2.0, nights=2, stop=True, gross=-10, risk=10),])
# --- 3. Apply the cost model, trade by trade ---def cost_usd(t): spread = (session_spread(t.hour) + rollover_penalty(t.hour)) * PIP_VALUE * t.lots comm = COMMISSION * t.lots swap = swap_units(t.weekday, t.nights) * (-SWAP_PER_NIGHT) * t.lots slip = (SLIP_STOP if t.stop else 0.0) * PIP_VALUE * t.lots return spread + comm + swap + slip
trades["risk_usd"] = trades.risk * PIP_VALUE * trades.lots # 1R, in moneytrades["gross_usd"] = trades.gross * PIP_VALUE * trades.lotstrades["cost_usd"] = trades.apply(cost_usd, axis=1)trades["net_usd"] = trades.gross_usd - trades.cost_usd
trades["gross_R"] = trades.gross_usd / trades.risk_usdtrades["net_R"] = trades.net_usd / trades.risk_usd
# --- 4. The numbers that matter ---gross_exp = trades.gross_R.mean()net_exp = trades.net_R.mean()print(trades[["gross","cost_usd","net_usd","gross_R","net_R"]].round(2))print(f"\ngross expectancy {gross_exp:.3f}R")print(f"net expectancy {net_exp:.3f}R")print(f"gross-vs-net gap {gross_exp - net_exp:.3f}R")Then:
- Run it. Read the per-trade table — notice the rollover trade (
hour=21) and the 4-night trade paying far more cost than the clean overlap trade. - Read the three summary lines. How much of the gross edge survived? That’s the whole game.
- Break it on purpose. Change the Asian-session trade’s entry to
hour=9and watch its cost fall — session timing is a real lever. Then setSLIP_STOP = 8(a news-heavy assumption) and watch net expectancy sink. - Make it yours. Replace
COMMISSION,SWAP_PER_NIGHT, and thesession_spreadnumbers with the real figures from yourbroker-spec.md. Feed in the 12-trade R-log you built in Lesson 1.4 (you’ll need to addhour,weekday,nights,stopcolumns) and compare net expectancy against the gross expectancy you computed there.
Keep cost_model.py. This is a direct deliverable — Level 2’s backtester calls this exact function so that every simulated trade pays realistic costs.
Terms introduced
Section titled “Terms introduced”Check yourself
The single most important output of a cost model is:
When modeling swap, the triple charge is applied on:
For a backtest, the safest way to set the spread assumption is:
A commission quoted "per side, per standard lot" must be, for a round-turn cost:
Net expectancy is:
You can move on when you can… build a per-instrument cost model in pandas (spread by session, commission, swap, slippage estimate) and run a trade log through it to get net numbers.
Go deeper
Section titled “Go deeper”- QuantStart and the Robot Wealth blog — the best free writing on why realistic costs decide whether a backtest means anything.
- The rest of the Darwinex Zorro cost series (swaps/margin/leverage, commissions FX vs non-FX) drills the unit-normalization traps if you want more worked examples — same platform caveat throughout.
- The book that owns this topic: Jansen’s Machine Learning for Algorithmic Trading ch. 18 is the systematic reference for transaction-cost modeling; Carver’s Systematic Trading frames costs as the first hurdle any rule must clear.