friday night. Q1 officially in the books.
did the math earlier while A. was cooking. she noticed i went quiet and just left me to it. that’s one of the things i didn’t expect about being married — how well she reads when to give space. anyway.
three months of one of the more unpredictable Q1s i’ve run through. tariff headlines every 72 hours, two vol spikes above VIX 25, a FOMC where fed dots shifted mid-cycle. and yet the algos ended up flat. not what you’d call a crushing performance, but given what the market threw at systematic premium selling, flat is survivable.
q1 final numbers #
updated the factor attribution this morning after the 4pm close.
jan: +2.1%. feb: -1.3%. march final: -0.7% (recovered a bit from the -0.9% estimate i posted sunday — the last three sessions were cleaner than mid-march).
quarter: +0.09%. account moved from $1.196M to $1.197M. net: +$1,072 on the quarter. lol.
not ideal. not a disaster. the SPX premium selling book ate it from both directions — elevated vol hurt existing short positions, then when it calmed down the theta wasn’t enough to compensate for the delta drift drag i wrote about monday. crypto book was the actual bright spot: BTC/ETH momentum strategy returned +4.2% on its slice, which masked a lot of the options pain.
the two brutal march weeks (W2 and W3) line up exactly with the VIX spike i wrote about march 20. after the greeks aggregation system came online wednesday, march W4 and this final partial week were cleaner — tighter position sizing when aggregate gamma exceeded threshold, which meant less P&L volatility when headlines dropped.
execution timing analysis — what i learned from q1 data #
one thing that came out of the factor attribution deep dive: slippage distribution wasn’t uniform across the trading day. i knew this intellectually but didn’t have the data to quantify it.
ran an analysis on all 847 options fills from Q1. here’s the code:
import pandas as pd
import numpy as np
from dataclasses import dataclass
from typing import Optional
import pytz
EASTERN = pytz.timezone('US/Eastern')
@dataclass
class OptionsFill:
fill_id: str
symbol: str
expiry: str
strike: float
right: str # 'C' or 'P'
quantity: int
fill_price: float
mid_at_fill: float
exec_ts: pd.Timestamp
strategy: str
def load_q1_fills(db_conn) -> list[OptionsFill]:
"""Pull all Q1 2026 options fills from TimescaleDB."""
query = """
SELECT
fill_id,
symbol,
expiry,
strike,
right,
quantity,
fill_price,
mid_at_fill,
exec_ts,
strategy
FROM trade_fills
WHERE exec_ts >= '2026-01-01'
AND exec_ts < '2026-04-01'
AND asset_class = 'option'
ORDER BY exec_ts
"""
rows = db_conn.execute(query).fetchall()
return [OptionsFill(*r) for r in rows]
def compute_slippage_bps(fill: OptionsFill) -> float:
"""Slippage in basis points vs mid-market at execution time."""
if fill.mid_at_fill <= 0:
return 0.0
# positive = paid more than mid (buying), or received less (selling)
direction = 1 if fill.quantity > 0 else -1
slip_dollars = direction * (fill.fill_price - fill.mid_at_fill)
return (slip_dollars / fill.mid_at_fill) * 10_000
def analyze_by_time_bucket(fills: list[OptionsFill]) -> pd.DataFrame:
"""Group fills into 30-minute EST buckets and compute median slippage."""
records = []
for f in fills:
est_ts = f.exec_ts.tz_convert(EASTERN)
bucket_hour = est_ts.hour
bucket_half = est_ts.minute // 30
bucket_label = f"{bucket_hour:02d}:{bucket_half * 30:02d}"
records.append({
'bucket': bucket_label,
'slippage_bps': compute_slippage_bps(f),
'strategy': f.strategy,
'symbol': f.symbol
})
df = pd.DataFrame(records)
summary = (
df.groupby('bucket')['slippage_bps']
.agg(['median', 'mean', 'std', 'count'])
.reset_index()
.sort_values('bucket')
)
summary.columns = ['time_bucket_est', 'median_slip_bps', 'mean_slip_bps', 'std_bps', 'fill_count']
return summary
def find_optimal_windows(summary: pd.DataFrame, max_slip_bps: float = 3.0) -> list[str]:
"""Return time buckets where median slippage is below threshold."""
good_windows = summary[summary['median_slip_bps'] <= max_slip_bps]
return good_windows['time_bucket_est'].tolist()
def strategy_slippage_breakdown(fills: list[OptionsFill]) -> pd.DataFrame:
"""Slippage cost breakdown by strategy family."""
records = [
{
'strategy': f.strategy,
'slippage_bps': compute_slippage_bps(f),
'notional': abs(f.quantity) * f.fill_price * 100 # options multiplier
}
for f in fills
]
df = pd.DataFrame(records)
breakdown = (
df.groupby('strategy')
.agg(
median_slip_bps=('slippage_bps', 'median'),
total_slip_cost=('slippage_bps', lambda x: (x * df.loc[x.index, 'notional'] / 10_000).sum()),
fill_count=('slippage_bps', 'count')
)
.reset_index()
.sort_values('total_slip_cost', ascending=False)
)
return breakdown
the findings were not subtle. median slippage between 9:30-10:00 EST was 8.4 bps. between 10:30-11:30 it dropped to 2.1 bps. after 2:00pm it crept back up as liquidity thinned into close.
so the “open as fast as possible” instinct was costing me money. not a ton — roughly $1,800 in total Q1 slippage costs from sub-optimal timing — but fixable with a simple execution delay rule. routing orders placed in the first 20 minutes post-open to a 10-minute minimum wait timer.
implemented it tuesday. too early to have data but the anecdotal fills this week looked better.
colo benchmark update — chicago still earning its rent #
did a full remote benchmark session on the chicago box tuesday evening. here’s what the Q1 latency data looks like across 847 order submissions:
P50 at ~310 microseconds, P99 below 1.5ms. for options execution that’s fine — options fills aren’t latency-sensitive the way futures scalping is. you’re not competing on speed, you’re competing on routing and timing. the colo earns its $680/month because it sits on the CME network backbone, not because it’s fast enough to HFT.
what i did find: P99.9 spikes correlated almost perfectly with CME gateway maintenance windows. three specific timestamps in Q1 where latency jumped above 8ms — all within 30 minutes of scheduled gateway rollovers. added those to the execution scheduler as blackout windows.
the box itself is running clean. CPU at ~23% average during market hours (futures algo has a heavier footprint than options), disk I/O well below limits, network utilization under 2% of provisioned bandwidth. no hardware concerns heading into Q2.
brief personal note before i close this out #
A. made this big taco spread for “Q1 closing dinner.” she’s figured out that marking my trading milestones with food is a language i actually respond to. flat quarter, not the quarter i wanted — but she made it feel like it was worth noting anyway.
she had no idea i had the latency charts pulled up on the second monitor the whole time. multitasking. she’d laugh if she knew.
q2 setup #
a few things i’m changing based on Q1 learnings:
- execution timing rule — minimum 20-minute post-open delay on new positions, aggressive close before final 30 minutes
- greek aggregation thresholds — tighter on short gamma especially during elevated VIX regimes (the new system from wednesday handles this but i need to tune the thresholds)
- crypto allocation bump — crypto book outperformed options book in Q1. not ready to shift allocation dramatically but +5% to crypto (from 30% to 35%) seems warranted
- tariff sensitivity filters — added a headline risk parser that checks CME volatility surface structure before letting new positions open. if term structure is inverted above a threshold, no new opens until it normalizes
been lurking on NexusFi where the diversified option selling thread has been active all year — some good discussion in there about managing premium books through vol regimes like what we saw in march. worth reading if you’re running similar strategies.
april starts monday. algos are prepped. chicago box is clean. Q2 budget is whatever i can make work after a flat Q1.
something quiet i noticed tonight. did the math while cleaning up the charts: Q1 2026 ends 3 years and 87 days after new year’s eve 2022. sometimes the calendar does that thing where a number feels heavier than it should.
anyway.
-AK