AVGO showed -$2,000 fifteen seconds in. Then GOOG wouldn’t fill. By the close, an AI was trading my money in public.
The strategy was the easy part. By the close I had learned more about what was wrong with my own platform than I did in the four weeks I spent preparing for this morning.
Austin Starks13 min read·Just now--
The strategy was the easy part. By the close I had learned more about what was wrong with my own platform than I did in the four weeks I spent preparing for this morning.
8:29 AM ET
Approve All. Rejected.
My alarm goes off at 8:29. I open one eye, then the other. The first thing I see is the dashboard on my monitor, where a teal robot icon in the top right corner is blinking. The entry filter decided which spreads to fire last night while I was asleep. They are sitting there now, polite, waiting on a click.
The clock ticks forward to 9:30. The market opens. I click Approve All.
The first order comes back rejected from Public.
The error reads: “Naked strategies involve unlimited risk and require a Level 4 options trading designation.” Public’s risk system was looking at the short call leg in isolation. The submission was supposed to be atomic. Both legs were supposed to land at the broker as one linked order so the short call would never get classified as standalone. Evidently the weekend rebuild was not as atomic as I had told myself it was.
From there, I knew it would be a long early morning.
The AVGO 430/490 bull call spread eventually clears. One contract, $1,975 net debit, both legs going in as a single atomic multi-leg order, the way I rebuilt the submission path over the weekend. Public takes it on the second attempt. Status: filled. Confirmation lands in my chat.
I exhale. That part finally worked.
Then I look at the portfolio.
9:31 AM ET
The portfolio dashboard says I just lost two thousand dollars.
The number on the screen is $23,043.06. The chart line cliffs straight down from $25,000. The percentage tag underneath it reads -$1,956.94, -7.03%.
My heart stops. I do not feel anything. I stare at the screen, emotionless, for a long second.
Then my second brain wave kicks in.
This is impossible.
The spread was a $1,975 net debit, but a debit is what I paid for the position, not what it is worth. The position itself is worth roughly what I paid for it, which means the dashboard should still read $25,000 minus a few dollars of slippage. I check the live brokerage. Public’s account value is right around $24,990. Ten dollars of friction on a freshly opened spread. Fine. Normal. Expected.
So the brokerage knows the truth. My own dashboard is lying to me by nearly two thousand dollars.
The bug
Options contracts represent 100 shares. Every time my code marked an option position to market, it read the per-share last price and used it as the position value without multiplying by 100. So a $20 long call quote that should have valued the position at $2,000 was being stored as $20. Multiply that by every leg of every spread, and a freshly-opened $1,975 spread looked like a $20 position with a roughly $1,950 hole next to it.
I have shipped this platform for five years and never once seen this. It only fires on options. Paper-trading options does not surface it because paper P&L is consistent within itself: if every option is wrong by the same factor, the curve still looks plausible. Live trading surfaces it the instant a real fill confirmation lands and the dashboard disagrees with the broker.
The fix took two commits, in two languages. One on the TypeScript getPositionMarketValue path that powers the dashboard and the server-side portfolio aggregator. One on the Rust position_from_public adapter inside the live trading engine, which had the exact same bug on a different code path. Same conceptual mistake, two languages, two layers, both wrong since the day options shipped. The dashboard refreshes. The hole disappears. The number is $24,990, off by ten dollars from where it started, which is exactly where Public has it.
I sit for a minute. The position was always fine. The number on the screen had me convinced, for several minutes, that I had vaporized two thousand dollars in fifteen seconds. The system was wrong, and the system was the only ground truth I trusted in the moment.
10:14 AM ET
GOOG won’t fill. Three submissions. Three “cancelled” with no reason.
The GOOG 385/415 bull call spread, two contracts, goes in. Public accepts. Public cancels. No reason. errorMessage: null. The spread did not fill, the order is closed, and there is no audit trail explaining why.
I resubmit. Public accepts. Public cancels. errorMessage: null.
I resubmit a third time, manually this time, just to be sure the agent isn’t doing something weird. Same result.
I am now staring at three rejected GOOG orders with no explanation, while a strategy I have spent four weeks defending in public is supposed to be opening positions. I check Public’s API documentation. I check Public’s status page. Both say nothing is wrong.
So I read the response payload by hand. And I find that Public’s REJECTED status comes back as HTTP 200 with the reason in the body. My own brokerage adapter was collapsing every terminal status (rejected, expired, cancelled) into CANCELED and dropping the reason on the floor. Every silent cancel I had been seeing all morning had a real reason in the response that the code was throwing away.
First fix: see the truth
I extended the Public adapter to read every reasonable field name on the response payload, propagate the broker’s reason onto the order’s errorMessage, and log the full payload on terminal status so I can grep my own production logs when this happens again. Twenty minutes from "what is happening" to seeing the actual cancel reason from Public.
I resubmit GOOG. Public cancels. This time the reason comes back inline.
The reason makes no sense. It says the order is for four contracts per leg. I submitted two.
12:36 PM ET
The bug that almost cost real money.
Public’s multi-leg API has a quirk. The total contracts per leg is computed as quantity × ratioQuantity. So a symmetric two-contract bull call spread should be submitted as quantity = 2 and ratioQuantity = [1, 1]. My code was sending quantity = 2 and ratioQuantity = [2, 2]. Public was reading 2 × 2 = four contracts on each leg. Four contracts is double what I intended, and double the buying power my account had set aside.
That is why Public was silently cancelling. Every GOOG submission was technically a buying-power violation, but Public was not telling me which kind of violation, and my code was hiding the message.
The earlier AVGO fill was not random luck. AVGO was a one-contract order. quantity = 1 and ratioQuantity = [1, 1] collapses to 1 × 1 = 1 contract per leg, which is what I intended. The bug was invisible at one contract. It only fires the moment you ask for two or more.
I patch it. Compute the per-leg greatest common divisor as the spread units, then set the top-level quantity = units and per-leg ratioQuantity = legQty / units. For a symmetric two-contract vertical: quantity = 2, ratio = [1, 1]. Resubmit GOOG. Filled.
My code was about to ask Public for twice the position I had funded. If Public’s risk system had been a little more lenient, the order would have filled. I would have paid roughly four thousand dollars for a position I sized at two. The bug had been latent in the multi-leg path since the day it shipped.
12:00 PM · in parallel
The engine kept trying to open the same spread.
While I was fighting Public over GOOG, my own live engine was fighting me. Every entry strategy gates on a condition like Open option spread count (AVGO) < 1 - a sentinel that is supposed to read the spread index and stop firing once a spread is already on the books. That gate was reading zero. The trader was looking at AVGO, seeing no open spreads, firing the entry signal, queuing a new order. A few seconds later, looking at AVGO again, seeing no open spreads, firing again. The same order, over and over, because the engine had no memory of the position it had just opened.
Here is why. The way the trader tracks spreads in theory: when two legs land that are linked by a portfolio-level “this is one spread” intent, the engine writes an OpenOptionSpread event and the spread index gets a new entry. The spread-count condition reads off that index. The theory only works if every OpenOptionSpread event has the full set of fields the consumer expects. Mine did not.
The way I was writing the spread metadata back to Mongo, only the keys I happened to set were getting written. The schema-level defaults Mongoose normally applies were getting bypassed because I was mutating a sub-object instead of replacing it. The Rust trader on the other side was deserializing the resulting document and rejecting it with the same generic message I had seen on Saturday: "missing field type". The index never got the entry. The spread-count gate never saw it. The strategy kept trying.
That is also why a hot-restart would have made it worse. Every time my live trader bounced to pick up one of the morning’s fixes, it forgot which spreads were already open and the next entry signal would have happily opened a fifth spread on top of the four I already had. The defined-risk story I have been telling for two months was held together by the fact that nothing had restarted yet. One bounce and it would have collapsed into double exposure.
Second fix: durable spread state
I rebuilt the spread-index path so it rehydrates from durable order history instead of an in-memory cache, and tightened the consumer side to be tolerant of missing optional fields while strict on the required discriminator. A 705-line diff across nineteen files. The unit test came in twenty minutes after the fix, with three spreads in the index and a forced restart in the middle, just to prove the index could survive what the day was going to put it through.
12:50 PM ET
The dashboard wrote those wrong numbers to durable history.
While the 100× bug was live, the live trader was happily writing portfolio snapshots to PortfolioHistory every minute. Each one was wrong. The public dashboard was showing a phantom dip approaching fifteen hundred dollars mid-morning that never actually happened. The brokerage account had not moved. The cached history had.
I cannot ship a “follow the trade in real time” challenge with a dashboard that lies. So I wrote a one-off migration. It pulls every filled order in the affected window, reconstructs the cash deltas, prices the held positions against Polygon’s minute data, and recomputes PortfolioHistory from a known-good starting cash state. Four hundred and eighty lines. I dry-ran it against a snapshot copy. It produced a curve that matched the brokerage. I ran it against production. The phantom dip disappeared.
The dashboard is right now. But the migration is a one-shot, and there is no live integrity check that would have caught the corruption while it was happening. That is on the followups list. The next bug that corrupts inputs will silently corrupt durable history again.
Throughout
The smaller fights.
A handful of less dramatic bugs got their own commits without their own panic. Net debit and net credit had three different sign conventions across the frontend modal, the order controller, and four brokerage adapters; the fix touched twenty-one files. The supervised options-chain refresh loop could leave a cache empty for minutes at the open, so I added an inline brokerage fallback for missing underlyings that reuses the existing rate-limit cooldown. And SendGrid threw a transient error on a chunk of the announcement-email recipients in the late afternoon, which I patched with retry-with-backoff and re-sent. If you got two copies of the email, that was me.
5:08 PM ET
End of Day 1.
By mid-afternoon AVGO is filled, GOOG is filled, and the dashboard is finally telling the truth. NVDA’s RSI never recovered. META’s RSI was at 18 on Monday evening and I did not expect it to fire today. But somewhere in the afternoon it started running, and by the close the 14-day RSI had crossed back above 50. The mechanical entry filter triggered.
I approved a META 610/655 bull call spread right around the close. The order routed to Public, one contract for a $1,470 net debit. Options markets thin out hard in the last few minutes and the spread did not fill at the limit. It is sitting as a working order, queued for execution at tomorrow’s open. If META gives back any of its afternoon move overnight I get a cleaner entry. If it gaps up the order may not fill at all.
That leaves two filled positions and one queued going into Wednesday. Account value at the close: $24,989.12. Day 1 P&L: -$10.88, basically flat. Roughly fifteen percent of the account is committed to defined-risk option exposure today, with another six percent waiting on the META fill at tomorrow’s open. Every position is defined-risk by structure: the most I can lose on any single spread is the debit I paid for it.
Underlying levels at the close: AVGO around $415, GOOG around $381, META up to roughly $625 from $610 the night before. NVDA stayed below its 14-day RSI 50 entry filter and was correctly skipped. The macro filter (SPY above its 200-day SMA) stayed green all day, so the regime side of the entry rule did not gate anything new.
How the basket exits
Every position has the same exit ladder, written into the strategy itself:
- +50% target. Close the position when its market value is up 50% from the debit paid. On AVGO that means a mark of $2,963 or higher. On GOOG (the 2-contract leg) it means $2,796 or higher. On META it means $2,205 or higher once filled.
- -30% stop. Close the position when it is down 30% from the debit. AVGO at $1,383, GOOG at $1,305, META at $1,029 if filled.
- DTE 7 time exit. Close any remaining position seven days before expiration regardless of P&L. With June 5 expiry, that fires around May 29.
- Per-name 25% drawdown stop. Close any name where the position drawdown exceeds 25%, independent of the basket-wide ladder.
What I’m watching tomorrow
- META at the open. The order is queued at Public for $1,470. If META gives back any of its afternoon move overnight I get a cleaner entry. If it gaps up the order may not fill at all.
- NVDA’s RSI. Sat at 35.6 Monday evening. Needs to climb back above 50 before the entry filter triggers. If NVDA rallies tomorrow it joins the basket; if not it stays benched.
- AVGO’s drawdown. -8.4% on the day-1 mark, well inside the per-name 25% drawdown stop. Comfortable margin.
- The watchdog. Runs every Monday morning by default. The next mandatory fire is May 11. Earlier triggers fire automatically if SPY breaks below its 200-day SMA.
Credibility note · the public chart was rewritten
The portfolio is shared at a fixed URL and anyone can open it. The chart you will see there does not show the morning’s phantom dip from the 100× bug. That is on purpose: while the bug was live, the live trader was persisting wrong values to PortfolioHistory every minute. I wrote a one-off migration that recomputed those snapshots from filled orders and minute-resolution price data, and rewrote the durable history with the corrected curve. The phantom dip in the screenshot above is real and it happened to me at 10:57 AM. The curve on the public chart no longer shows it because the curve was wrong while the bug was live. The migration is honest, but it is a one-way edit, and I want it on the record before anyone reads the chart and concludes the day was smoother than it was.
Reflection
An AI is now trading my money. In public.
As of the close, an AI-designed strategy is live on $25,000 of my own money in a real brokerage account. Two spreads filled today. One more is queued at the broker for tomorrow’s open. Every position came from a model bake-off I ran on Saturday: the structure was authored by Kimi K2.6, the mechanical entry rule decided which tickers fired this morning, and a separate watchdog agent reviewed the basket Sunday night and Monday evening before any of it went live.
The only thing my opposable thumbs did at the open was click Approve All.
That is the experiment I have been pointing at for two months. It is now running.
And I am running it in public on purpose. The portfolio is shared at a fixed URL. Anyone reading this can open it and see the same fills, the same drawdowns, the same marks I see, in the same minute I see them. There is no version of this story that gets retroactively edited. If the strategy works, you’ll see it work. If it doesn’t, you’ll see that too. The trade is on the tape.
The strategy was the easy part. The infrastructure was the hard part.
Three bugs surfaced today that had been latent in my codebase since options shipped. Paper trading did not surface any of them. They all surfaced inside the first three hours of live fills, against a brokerage that knows the truth. The 100× multiplier was wrong for years. The sign-convention disagreement was wrong across three layers since the multi-leg modal got built. The Public quantity bug only fires once you submit two contracts, so the entire single-contract paper history sailed past it. Paper does not call your code a liar. The live tape does.
That is the argument for trading in public. The only stress test that catches bugs in your own infrastructure is real money, real fills, on a real brokerage, in front of an audience that can verify what you claim happened.
Tomorrow I find out if META fills at the open and whether the two existing spreads move in the right direction. Then I find out if any of this actually makes money. The trade is live.
Originally published at https://nexustrade.io.