Skip to main content

q2 week 1: health scoring live, colo nic split, first numbers

2:15am friday.

Q2 week 1 is done. walked in from the kitchen, A. fell asleep at her desk again — laptop open, ambient music still running. grabbed a blanket from the couch and put it over her. then came back and pulled up the weekly numbers.

let’s go through it.


week 1 numbers
#

modest. intentional. +$4,200 on the week. +0.35% on account.

starting position after Q1 close was $1.197M. came in at 60% of normal position size across everything except crypto (which runs full size — that strategy held all of Q1 so no reason to throttle it). the whole point of week 1 was: let the health scoring system run live, let the new colo setup breathe, don’t chase performance until both are confirmed working.

breakdown:

  • SPX premium selling: +$1,800 collected. 2 iron condors, tight size, pure theta harvest. closed both by thursday. no drama.
  • crypto momentum (BTC/ETH): +$3,100. BTC held trend all week. ETH followed. vol regime filter correctly sat out the tuesday consolidation — avoided chasing a move that reversed.
  • equity options / everything else: flat. rebuilding the options book around the new health framework, no rush.

the tariff headlines are real and the market is pricing event risk into april. the light sizing was the right call — i’ll be at full deployment when the health scoring confirms edge is present, not before.


health scoring: first live week
#

the system i built and wrote up monday is officially in production. this is the first week of real data.

here’s what the scores looked like across the week:

strategy monday open friday close direction
SPX premium selling 61/100 68/100 ↑ improving
equity momentum 84/100 82/100 stable
crypto BTC/ETH 79/100 83/100 ↑ improving
ES futures 58/100 61/100 ↑ slowly

read on the options book: opened Q2 in yellow zone (60-75). health score is below green threshold, which is why position sizing stayed at 60%. by friday it’s trending up — the SPX condors performing cleanly pushed the rolling IR score higher and the regime mismatch component dropped as vol normalized slightly. if this continues through next week, the sizing ramp starts.

the thing that confirmed the system is working: i almost talked myself into adding a third condor on wednesday when the setup looked good. checked the health score — still 63 at that point. held off. thursday saw a nasty 0.8% intraday spike that would’ve forced a defensive roll on that position. the health score said no before the market said no. that’s the whole point.

i was reading through a long-running algo journal thread on NexusFi a while back where someone made this exact observation: position sizing decisions shouldn’t rely on gut feel about “how the strategy feels” in a given week. you need a systematic signal. took me a while to actually build one, but here we are.

one refinement i added this week: score velocity tracking. not just current score but rate of change. a score of 68 that moved up from 55 in 5 days is fundamentally different from 68 that’s been drifting down from 75. added a 10-day velocity field to the monitoring dashboard.

from dataclasses import dataclass, field
from typing import Deque
from collections import deque
import numpy as np

@dataclass
class HealthScoreTracker:
    strategy_id: str
    history_window: int = 10
    _score_history: Deque[float] = field(default_factory=deque)

    def update(self, score: float) -> None:
        self._score_history.append(score)
        if len(self._score_history) > self.history_window:
            self._score_history.popleft()

    @property
    def current_score(self) -> float:
        return self._score_history[-1] if self._score_history else 0.0

    @property
    def velocity(self) -> float:
        """Points per day over the window. Positive = improving, negative = decaying."""
        if len(self._score_history) < 2:
            return 0.0
        scores = list(self._score_history)
        x = np.arange(len(scores))
        slope, _ = np.polyfit(x, scores, 1)
        return float(slope)

    @property
    def velocity_label(self) -> str:
        v = self.velocity
        if v > 2.0: return "improving fast"
        if v > 0.5: return "improving"
        if v > -0.5: return "stable"
        if v > -2.0: return "degrading"
        return "degrading fast"

    def sizing_multiplier(self) -> float:
        """Position sizing based on score AND velocity."""
        score = self.current_score
        vel = self.velocity

        if score >= 75:
            return 1.0                      # green zone: full size
        elif score >= 60:
            # yellow zone: size depends on direction
            if vel > 0.5:
                return 0.75                 # improving: increase toward full
            elif vel < -0.5:
                return 0.50                 # degrading: reduce further
            else:
                return 0.60                 # stable: hold at 60%
        elif score >= 45:
            return 0.35                     # orange zone: minimal
        else:
            return 0.0                      # red zone: off

    def summary(self) -> dict:
        return {
            "strategy": self.strategy_id,
            "score": self.current_score,
            "velocity": self.velocity,
            "velocity_label": self.velocity_label,
            "sizing_multiplier": self.sizing_multiplier(),
            "data_points": len(self._score_history)
        }

this is running in the monitoring stack now. the grafana dashboard has a panel per strategy showing score + velocity trend line. the ES futures book is in the 58-61 range and degrading slowly, so it’s effectively off until that turns around.


colo upgrade: dual NIC split results
#

spent wednesday afternoon SSH’d into the chicago box doing the NIC split i’ve been putting off since february.

before: single 10Gbe NIC handling everything — polygon data ingest, thetadata options flow, IB order routing, tastyworks routing, all competing for the same logical pipe. fine during normal conditions. in high-vol windows, contention caused occasional 0.3-0.5ms latency spikes on order submission that i wasn’t happy about.

after: dual 10Gbe NICs, hard-separated by function.

  • eth0 (NIC-0): data feed ingest only — Polygon tick data, ThetaData options flow, market data aggregation
  • eth1 (NIC-1): order routing only — IB API, Tastyworks API, execution confirmations

the python change to support this is minimal. just binding sockets to their respective interfaces:

import socket
import os
from dataclasses import dataclass
from typing import Optional, Literal

@dataclass
class NetworkConfig:
    data_interface: str = "eth0"
    order_interface: str = "eth1"

def create_interface_socket(
    interface: str,
    sock_type: int = socket.SOCK_STREAM,
    nonblocking: bool = False
) -> socket.socket:
    """Create a socket bound to a specific network interface."""
    s = socket.socket(socket.AF_INET, sock_type)
    # SO_BINDTODEVICE requires root or CAP_NET_RAW
    s.setsockopt(
        socket.SOL_SOCKET,
        socket.SO_BINDTODEVICE,
        (interface + '\0').encode()
    )
    s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
    if nonblocking:
        s.setblocking(False)
    return s

class DataFeedSocket:
    def __init__(self, config: NetworkConfig, host: str, port: int):
        self.host = host
        self.port = port
        self._config = config
        self._sock: Optional[socket.socket] = None

    def connect(self) -> None:
        self._sock = create_interface_socket(self._config.data_interface)
        self._sock.settimeout(5.0)
        self._sock.connect((self.host, self.port))

class OrderSocket:
    def __init__(self, config: NetworkConfig):
        self._config = config

    def create_ib_socket(self) -> socket.socket:
        """IB TWS connection dedicated to NIC-1."""
        return create_interface_socket(self._config.order_interface, nonblocking=False)

    def create_tastyworks_socket(self) -> socket.socket:
        """Tastyworks API connection dedicated to NIC-1."""
        return create_interface_socket(self._config.order_interface)

prometheus scrapes both interfaces separately now so i can see feed lag vs order lag as distinct metrics in grafana. way cleaner signal isolation.

measured results after 4 days of production traffic:

p95 dropping from 2.1ms to 1.1ms is the win that matters. normal conditions, nobody cares about p95. vol spike conditions — exactly when fill quality matters most — that’s where the spike suppression pays off. the march chaos was my lab test case. this is the fix.

want to see 10 more days of data before calling it definitively. but week 1 looks clean.


event risk filter: first iteration
#

added a macro event filter to the risk engine last week. concept: when high-impact scheduled events are active (tariff decisions, major fed comms, CPI), cut new position sizes by 40% in the 2-hour windows around them.

not novel — every serious systematic shop has something like this. the difference is implementation. mine is running off a combination of my Bloomberg BRIEF API scrape + manual calendar entries. still refining the false-positive rate (news that mentions tariffs in historical context triggers it when it shouldn’t), but the core logic is solid:

from datetime import datetime, timedelta
from typing import List, Optional
from dataclasses import dataclass, field
import re

@dataclass
class CalendarEvent:
    name: str
    scheduled_time: datetime
    impact_level: str          # 'high', 'medium', 'low'
    pre_window_hours: float = 2.0
    post_window_hours: float = 2.0

    def is_active(self, now: Optional[datetime] = None) -> bool:
        now = now or datetime.now()
        window_start = self.scheduled_time - timedelta(hours=self.pre_window_hours)
        window_end = self.scheduled_time + timedelta(hours=self.post_window_hours)
        return window_start <= now <= window_end

@dataclass
class EventRiskFilter:
    base_multiplier: float = 1.0
    high_impact_multiplier: float = 0.60   # 40% reduction
    medium_impact_multiplier: float = 0.80  # 20% reduction
    _events: List[CalendarEvent] = field(default_factory=list)

    def add_event(self, event: CalendarEvent) -> None:
        self._events.append(event)

    def get_active_events(self, now: Optional[datetime] = None) -> List[CalendarEvent]:
        return [e for e in self._events if e.is_active(now)]

    def position_size_multiplier(self, now: Optional[datetime] = None) -> float:
        active = self.get_active_events(now)
        if not active:
            return self.base_multiplier
        # Use most restrictive active event
        has_high = any(e.impact_level == 'high' for e in active)
        has_medium = any(e.impact_level == 'medium' for e in active)
        if has_high:
            return self.high_impact_multiplier
        if has_medium:
            return self.medium_impact_multiplier
        return self.base_multiplier

    def status(self) -> dict:
        active = self.get_active_events()
        return {
            "active_events": [e.name for e in active],
            "current_multiplier": self.position_size_multiplier(),
            "is_restricted": self.position_size_multiplier() < 1.0
        }

for next week specifically: going into what looks like a heavy macro week (tariff announcements have been rolling through, earnings season starting to pick up), the filter is going to be active more than usual. that’s fine. the system knows to dial back. better to miss some edge than to get caught at full size when the feed goes nuts.


brief grief sidebar
#

april now. first full quarter as a married person in the books.

had this weird thought today while watching the tariff headlines scroll. dad used to talk about trade flows at the dinner table when i was a kid — it was his thing, he had opinions about everything macro. i ignored it then. would pay actual money to argue with him about it now.

not sad exactly. just present sometimes. he would’ve had a take.


q2 week 2: what i’m watching
#

  1. tariff noise: keeping SPX book light until macro picture clears. health score will tell me when options edge is back.
  2. BTC trailing stop at $86,200 — if we break that, systematic exit, reassess. no opinions, just the rule.
  3. colo NIC data: need 10 more days before i’m confident enough to update the grafana alert thresholds.
  4. ES futures health score: sitting at 61/100 and moving slowly. not touching that book until it hits 70+.

flat Q1 is behind us. infrastructure is cleaner than it’s been. health scoring is running live. Q2 starts quiet and that’s fine.

-AK

Related

strategy health scoring: detecting algo decay before it wrecks your Q2
2:45am monday. first trading day of Q2. Q1 is officially in the rearview — closed at basically flat, full numbers are in friday’s post. the weekend was heavy. not going into it right now. but Q2 starts regardless, and the algos don’t wait for you to process.
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.
execution quality audit: q1 slippage cost me more than i thought
2:30am wednesday. april 1st. no this is not a joke post. been staring at execution data for the last three hours and i have a headache. Q1 closed basically flat — detailed numbers in the march wrap. but flat is flat, and when i dug into why flat, the answer wasn’t strategy failure. it was execution bleed.
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%.
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.