Skip to main content

q1 close: final numbers, colo benchmarks, and q2 setup

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:

  1. execution timing rule — minimum 20-minute post-open delay on new positions, aggressive close before final 30 minutes
  2. 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)
  3. 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
  4. 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

Related

q1 factor attribution: theta is the edge, delta drift is the problem
q1 is in the books. three months, roughly flat performance, and a clear pattern in the trade data that tells me exactly what needs to change for q2. jan: +2.1%. feb: -1.3%. march: -0.9% (locked at friday close). quarter: -0.13% net. account moved from $1.196M to about $1.194M. call it flat with a slight downside tilt.
real-time greeks aggregation: knowing your portfolio delta/gamma at sub-second speed
2:15am wednesday. still processing this week. the q1 factor attribution post from sunday was cathartic but it also made me confront something i’d been papering over: i was flying blind on real-time greeks for most of march. not completely blind — i had position-level greeks from IB’s TWS feed. but aggregating them into a coherent portfolio view? that was a manual spreadsheet thing i’d run every few hours.
march vol spike: when the risk engine earns its keep
2:30am friday. rough week in the books. march has been a whole thing. tariff headlines dropping every 48 hours, VIX spiking then partially recovering, nobody knows what SPX does next. january was decent (+2.1%), february went against me (-1.3%). march hasn’t been great either. week ending today, i’m down about $2.3k for the five sessions. month’s probably closing around -1%.
chicago colocation 18 months - ROI analysis and lessons
18 months with chicago colo. time for ROI analysis. the setup recap # december 2023: deployed chicago colo
chicago colocation - 67ms to 12ms latency improvement, worth the cost
moved execution server to chicago colo march 2024. 3 months data in. latency dropped 67ms → 12ms average. why chicago # CME exchange location: chicago
implied vol surface in python: stop guessing what the market thinks
4:30 AM. been staring at vol surfaces for three weeks straight. finally got the pipeline clean enough to write about it. if you’re trading options without a vol surface you’re flying blind. period. everyone talks about delta and theta but the actual edge is in understanding where implied vol is mispriced relative to what it should be. that’s the surface. that’s where the money is.