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 #
- tariff noise: keeping SPX book light until macro picture clears. health score will tell me when options edge is back.
- BTC trailing stop at $86,200 — if we break that, systematic exit, reassess. no opinions, just the rule.
- colo NIC data: need 10 more days before i’m confident enough to update the grafana alert thresholds.
- 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