playitsmart.nl

Terug naar home

17 mei 2026 · 4 min lezen

Post #1

Marathon dag vijf: de kolom die alles tegenhield

Hoe een NULL-veld twee commits en 26 uur kostte voor het werd opgelost

Donderdagavond 14 mei, ergens rond 22:00. Ik zit nog steeds achter mijn laptop. Mijn eerste echte productie cron zou de volgende ochtend om 06:30 NL voor het eerst alles moeten ophalen, processen en op basis daarvan orders inleggen bij IBKR. Op papier was alles getest. Tests groen. Render services gedeployed. Een paper account klaar.

En toch klopte er iets niet.

Bij elke handmatige test op een testdatum bleef de output 0 BUYs. Geen errors, geen warnings. Gewoon nul. Voor een systeem dat is gebouwd om dagelijks 5 tot 15 signalen te genereren is dat geen klein detail.

De oorzaak die ik niet zag

Het zat in een kolom die score_30d_ago heet. Een veld in mijn scores_daily tabel dat per ticker de composite score van precies 30 handelsdagen geleden bewaart. Het systeem heeft dat veld nodig voor één van de BUY-regels uit het design doc, namelijk dat de score moet stijgen over 30 dagen. Zonder dat veld geen BUYs.

Het probleem was dat die kolom voor elke ticker NULL bevatte. Hij werd nergens gevuld.

Dat was vreemd. Ik wist zeker dat er een script bestond dat percentile ranks berekende en daar score_30d_ago bij deed. Of dat dacht ik. Toen ik het uiteindelijk uitzocht bleek het script wel te bestaan, maar één essentiële stap te missen. De 30d-lookup zelf was nooit geïmplementeerd. Tests dekten dat niet af want de tests gebruikten mock data waar de waarde simpelweg ingevuld was.

Eerste fix, niet genoeg

Vrijdagochtend 15 mei, 03:00 NL. Ik schreef een Cursor prompt voor de eerste fix. Een migratie om de kolom op te ruimen, plus de echte lookup-logica in het script. Drie uur later was dat klaar en getest. Commit gepushed. CI groen. Render auto-deployed.

Eerste echte productie cron 07:00 NL. Output: 14 BUYs. Het werkte.

Behalve het werkte niet helemaal. Een uur later kreeg ik door dat de score_30d_ago niet voor alle tickers gevuld was. Alleen voor een deel. De pagination in het script stopte op een korte pagina van Supabase, terwijl het had moeten doorgaan tot er echt geen data meer was.

Twee bugs op één dag, in hetzelfde feature. Of eigenlijk: één bug die zich verstopte achter de andere.

Tweede fix

Vrijdagochtend 08:30. Ik was nu 14 uur achter mijn laptop. De marathon was al begonnen voordat ik het door had. Cursor schreef een tweede prompt voor de pagination fix. Tests die specifiek dit scenario afdekken. Commit gepushed.

Vrijdag 08:45 NL: eerste echte productie cron met deze fix in container. 14 orders submitted bij IBKR. Op IBKR Portal kon ik zien dat ze daadwerkelijk geplaatst waren.

Toen kwam vrijdagavond pas de vraag of de orders ook gefilled waren. En zaterdag een hele dag debugging om uit te zoeken waarom het dashboard 0 posities toonde terwijl IBKR Portal er veertien liet zien. Maar dat verhaal is voor een andere keer.

Wat ik echt leerde

Wat me het meeste raakte aan deze hele saga was niet de bug zelf. Bugs zijn er. Het was dat ik twee dingen niet door had:

Het eerste: ik dacht dat tests me beschermden tegen dit soort fouten. Maar tests die mock data gebruiken testen alleen de happy path. Een NULL waarde in productie is geen mock data. Dat moest ik handmatig verifiëren door echte rows uit Supabase op te halen, niet door tests groen te krijgen.

Het tweede: na 12 uur ben ik niet meer in staat om goede beslissingen te nemen. Dat klinkt obvious maar in het moment voelt het anders. Je denkt: nog even dit, dan is het klaar. Maar de tweede fix die ik in de ochtend pushte was alleen mogelijk omdat ik geen risico-relevante wijzigingen meer deed. Pure pagination fix, geen risk voor live trading state.

Het was anders verlopen als de fix iets risk-gerelateerd was geweest. Ik had het toen niet meer moeten doen. Mijn AI-assistent had ook moeten zeggen "Erik, je hebt 22 uur op de teller. Dit is geen tijd voor productie SQL." Dat heeft hij niet gedaan, dat is iets om aan te scherpen.

De take-away

Een systeem dat live trading doet wordt niet betrouwbaar door perfect te zijn. Het wordt betrouwbaar door snel te herkennen wanneer er iets fout is, en door discipline om niet onder vermoeidheid risk-relevante wijzigingen te doen.

Score_30d_ago was niet het einde van de marathon. Het was alleen de eerste van een serie launch blockers die ik in een weekend leerde kennen. En elke launch blocker is een gelegenheid om het systeem strenger, simpeler of beter beschermd te maken voor 22 juni. Vijf weken tot live met €10.000.

Wekelijks volgen?