18 mei 2026 · 4 min lezen
Post #1Wanneer mijn eigen script tegen me loog
Waarom mijn dashboard nul posities toonde terwijl IBKR er veertien had
Zaterdagochtend 16 mei, 09:00. Ik open playitsmart.nl/live om te checken hoe de paper portfolio er bij staat na vrijdag's eerste 14 orders. De pagina laadt. Onderaan staat het cijfer dat me even doet stoppen.
Posities: 0 van 8.
Maar ik weet zeker dat er fills zijn geweest. IBKR Portal liet ze zien op vrijdagmiddag. Veertien stuks. Zeven Nederlandse aandelen, zeven Amerikaanse. Ik klik over naar mijn IBKR Portal in een ander tabblad. Daar staan ze nog. Veertien posities, allemaal OPEN, allemaal met een entry price die overeenkomt met de orders die we vrijdag hebben ingediend.
Mijn eigen systeem zegt nul. IBKR Portal zegt veertien. Eén van de twee liegt. En ik weet niet welke.
Drie hypotheses
Voordat ik aan een fix begin, schrijf ik drie mogelijke oorzaken op. Dat is een gewoonte die ik mezelf heb opgelegd voor live trading werk. Eerst denken, dan handelen.
Hypothese A: de fill_sync werkt niet en haalt geen executions op uit IBKR.
Hypothese B: de positions_sync werkt niet en synchroniseert geen open posities terug naar Supabase.
Hypothese C: er zijn helemaal geen positions_sync. Het systeem leest posities af van de fills, en als fill_sync stuk is, is het hele beeld stuk.
Diagnose script schrijven, niet meteen fixen. Ik open Render Shell op de playitsmart-trader-daily service en run handmatig een script dat IBKR's get_positions() call doet via de bestaande IBKRClient class. Het zou meteen 14 posities moeten retourneren.
Het retourneert nul.
Het detail dat ik niet had gezien
Even pauze nemen, koffie zetten. De IBKRClient doet wel een verbinding met de Gateway. Logs tonen connected client_id=11. Dus de IBKR connectie zelf werkt. Maar de positions request geeft een lege lijst terug.
Ik open de IBKR API documentatie. Voor reqPositions heb je een account specifier nodig. Mijn account ID bij IBKR is DUP*****. Dat is wat IBKR weet en herkent.
Dan kijk ik naar de constructor van de class in reconcile.py. De regel die mij later veel zou leren over de prijs van shortcuts:
ibkr = IBKRClient(gateway, args.account_id)
args.account_id was niet DUP*****. Het was nl-individual-***. Dat is mijn interne app account identifier. Een veld dat ik gebruik om verschillende portefeuilles te onderscheiden in mijn Supabase tabellen.
De IBKRClient constructor accepteerde elke string als account ID. Geen validatie. Geen error. Een interne app identifier werd naar IBKR's API gestuurd alsof het een geldige IBKR account was. IBKR zei netjes "I don't recognize that account, here are zero positions" en mijn systeem accepteerde dat.
De fix die langer duurde dan ik dacht
Twee opties om dit op te lossen.
Optie 1: in reconcile.py direct het juiste IBKR account ID hardcoden of uit env halen.
Optie 2: een factory pattern bouwen waar de juiste account ID centraal wordt geïnjecteerd, zodat dezelfde fout niet ergens anders weer opduikt.
Ik kies voor optie 2. Niet omdat het de snelste fix is, maar omdat ik bij grep door de codebase ontdek dat de fout ook in route_orders.py en orchestrator.py zit. Drie scripts met dezelfde error. Een factory voorkomt dat een vierde script in de toekomst hetzelfde herhaalt.
scripts/ibkr_client_factory.py als nieuwe file. Centraal punt waar IBKR_ACCOUNT_ID uit env wordt gelezen en gevalideerd. Drie scripts updated. Eén nieuwe test om te checken dat de factory de juiste account doorgeeft. Commit gepushed.
sync_fills.py was overigens al correct. Die las het account ID direct uit de juiste env var. Een eerdere versie van mezelf had daar wel goed nagedacht, terwijl ik bij reconcile.py blijkbaar slordig was geweest.
Wat ik ervan opdiep
De bug zelf was klein. Een parameter. Eén regel. De fix was tien minuten code en twintig minuten test. Maar de gevolgen waren groot. Veertien posities die niet zichtbaar waren. Een dashboard dat niet klopte. Verwarring tijdens een launch-prep weekend.
De les: constructors die elke input accepteren zonder validatie zijn een aanval-oppervlak voor toekomstige fouten. Cursor stelde de validatie niet voor want het was niet expliciet gevraagd. Het bouwde wat ik vroeg, niets meer. Dat is wat goede AI doet. Maar ik moet als reviewer specifieker zijn over wat ik wil zien beschermd.
De andere les: drie hypotheses opschrijven voordat je gaat fixen redt je van vroege conclusies. Ik had makkelijk meteen de fill_sync kunnen gaan debuggen, terwijl die niet eens het probleem was. De gewoonte om eerst te schrijven wat ik vermoed, te ranken op waarschijnlijkheid, en dan pas te valideren, kost vijf minuten en bespaart soms uren.
De take-away
Mijn eigen script loog niet met opzet. Het deed precies wat ik had gevraagd. Maar wat ik had gevraagd was niet wat ik bedoelde. Tussen "construct an IBKR client with this string as account" en "construct an IBKR client with the right IBKR account" zit een wereld van verschil. Een wereld die Cursor niet voor mij invult, omdat dat mijn job is.
Veertien posities zaten in IBKR, gewoon waar ze hoorden te zijn. Niet de wereld die kapot was. Alleen mijn beeld ervan.