woke up at 2am couldn’t sleep.
decided to run a full slippage analysis on last quarter’s trades.
what i found is annoying but fixable.
the invisible tax #
every algo trader knows slippage exists.
few actually measure it properly.
slippage is the difference between your backtest price and your actual fill.
it’s where your edge goes to die.
my Q4 2025 numbers:
- total trades: 847
- theoretical P&L: +$42,300
- actual P&L: +$31,850
- slippage cost: $10,450 (24.7% of gross)
that’s insane. almost a quarter of my edge getting eaten by execution.
order size matters more than you think #
pulled every ES fill from october through december.
slippage by contract size. anything over 25 contracts and you’re getting murdered. error bars show standard deviation.
the breakpoints:
- 1-10 contracts: 0.02-0.12% slippage (acceptable)
- 25-50 contracts: 0.28-0.52% slippage (getting painful)
- 100+ contracts: 0.89%+ slippage (edge destroyer)
my bigger trades (50+ lots) were killing me.
the algo was right on direction. execution was shit.
timing is everything #
spent 3 hours analyzing fill quality by time of day.
blue line is fill quality (% filled at mid or better). red bars show average slippage in basis points. opening and closing hours suck.
when to trade:
- 9:30-10:30 EST: best fills, lowest slippage (makes sense - most liquidity)
- 12:00-14:00 EST: lunch doldrums - still decent
- 6:00-9:00 EST: pre-market garbage - avoid if possible
- 15:30-16:00 EST: closing auction chaos - slippage spikes
been trading too many overnight positions that fill during pre-market.
bad habit. fixing it.
the fix #
wrote a simple execution optimizer.
from dataclasses import dataclass
from typing import List, Tuple
from enum import Enum
import numpy as np
class OrderUrgency(Enum):
LOW = "low" # can wait for good fill
MEDIUM = "medium" # prefer good fill, but time sensitive
HIGH = "high" # need to fill now, accept slippage
@dataclass
class ExecutionConfig:
max_single_order: int = 25 # contracts
slippage_threshold: float = 0.25 # max acceptable %
optimal_hours: Tuple[int, int] = (9, 14) # EST
twap_duration_minutes: int = 15
class ExecutionOptimizer:
def __init__(self, config: ExecutionConfig):
self.config = config
def split_order(self, total_contracts: int) -> List[int]:
"""Break large order into digestible chunks"""
if total_contracts <= self.config.max_single_order:
return [total_contracts]
chunks = []
remaining = total_contracts
while remaining > 0:
# vary chunk sizes to avoid detection
chunk = min(
remaining,
np.random.randint(
int(self.config.max_single_order * 0.6),
self.config.max_single_order + 1
)
)
chunks.append(chunk)
remaining -= chunk
return chunks
def should_delay(self, current_hour: int, urgency: OrderUrgency) -> bool:
"""Check if we should wait for better execution window"""
if urgency == OrderUrgency.HIGH:
return False
optimal_start, optimal_end = self.config.optimal_hours
in_optimal = optimal_start <= current_hour <= optimal_end
if urgency == OrderUrgency.LOW:
return not in_optimal
# MEDIUM: only delay if we're in really bad hours
bad_hours = [6, 7, 8, 15, 16]
return current_hour in bad_hours
def calculate_twap_schedule(
self,
total_contracts: int,
duration_minutes: int = None
) -> List[Tuple[int, int]]:
"""Generate TWAP schedule (minute_offset, contracts)"""
duration = duration_minutes or self.config.twap_duration_minutes
chunks = self.split_order(total_contracts)
# spread chunks evenly with some randomness
base_interval = duration / len(chunks)
schedule = []
current_time = 0
for chunk in chunks:
# add jitter to avoid predictable patterns
jitter = np.random.uniform(-0.2, 0.2) * base_interval
execute_at = max(0, current_time + jitter)
schedule.append((int(execute_at), chunk))
current_time += base_interval
return schedule
def estimate_slippage(self, contracts: int, hour: int) -> float:
"""Estimate slippage based on historical data"""
# size factor
if contracts <= 10:
size_slip = 0.05
elif contracts <= 25:
size_slip = 0.15
elif contracts <= 50:
size_slip = 0.35
else:
size_slip = 0.60
# time factor
time_multipliers = {
6: 1.8, 7: 1.5, 8: 1.3,
9: 0.8, 10: 0.7, 11: 0.9,
12: 1.0, 13: 0.95, 14: 0.85,
15: 1.4, 16: 1.6
}
time_mult = time_multipliers.get(hour, 1.0)
return size_slip * time_mult
nothing fancy.
split big orders into smaller chunks.
wait for good execution windows when possible.
add some randomness so the algos don’t front-run me.
results so far #
been running the optimizer for 2 weeks.
before optimization (Q4):
- avg slippage: 0.31%
- worst day: 0.89%
- slippage as % of gross P&L: 24.7%
after optimization (jan 1-17):
- avg slippage: 0.14%
- worst day: 0.38%
- slippage as % of gross P&L: 11.2%
cut slippage in half.
that’s real money - probably $4-5k/month back in my pocket.
what i learned #
1. measure everything
if you’re not tracking slippage per trade, you’re flying blind.
2. size kills
anything over 25 contracts on ES needs to be split. period.
3. timing matters
pre-market and close are slippage traps. avoid unless you have to.
4. don’t be predictable
add randomness to order sizes and timing. the HFT algos are watching.
been sharing some of this analysis with other algo traders on NexusFi - execution optimization is one of those topics that separates hobbyists from profitable traders.
next steps #
building a real-time slippage tracker that alerts me when fills are worse than expected.
if slippage spikes mid-day, something’s wrong with market microstructure.
want to know immediately so i can pause the algo.
2:45am saturday. couldn’t sleep so ran a slippage analysis instead. found i was losing 25% of my edge to execution. fixed it by splitting orders and timing execution better. cut slippage in half in 2 weeks. measure everything.
-AK