Hey there, it's your friendly neighborhood 'Ojii' (old man). I'm 38, a corporate drone during the week, and spend my weekends tinkering with AI trading bots.
While doing routine maintenance on my weekend bots, I noticed an unfamiliar stock lingering in my portfolio. "Huh, when did I even enter this position?" I wondered.
Turns out, it was a stock that had been delisted several months ago. My bot wasn't selling it; it was just quietly "perma-holding" it. The P&L was negligible, but having a system maintain unintended positions is a serious issue. I immediately started investigating.
First, I dove into the logs. I found that at a certain point, the stock had completely disappeared from the list of assets considered for exit decisions. No errors. It was simply treated as if it no longer existed.
The root cause lay with the external API my bot used to fetch stock price data. When a stock is delisted, that API stops returning its historical data. That's a pretty standard API behavior.
The problem was in my code. A fundamental condition for triggering the exit logic was "having at least 260 days of candlestick data." This was necessary for calculating various technical indicators.
What happened to stocks that didn't meet this condition? They weren't throwing exceptions; they were simply skipped.
frames = {}
for ticker in ALL_TICKERS:
df = yf.download(ticker, period='2y')
if len(df.dropna()) >= 260:
frames[ticker] = df
This if
statement was the culprit. Stocks delisted and thus no longer providing data were silently excluded from processing here. No error logs, just a single warning log line. How could I have noticed? A classic silent bug pattern.
What made things even more complicated was the way I was using two different APIs:
The inconsistency between these two data sources critically delayed bug detection. In one world, it was a "non-existent stock," but in the other, it was an "owned asset." There was no mechanism to detect this discrepancy. That's brutal.
The fundamental problem was the flow: "first, gather data for all stocks, then select those for processing." This approach meant that the moment a stock disappeared from the data source, its existence became untraceable.
So, I reversed the order of operations. The new flow is: "first, list all currently held positions. Then, for each of those stocks, check if data can be properly retrieved."
If data cannot be retrieved or is too old, it's treated as an "abnormal situation." It's either forcibly marked for exit, or at the very least, a critical error notification is triggered.
Here's the conceptual code for the fix:
live_positions = get_current_positions() # Fetch current holdings from brokerage API
plan = {"sell": [], "stale": []}
for symbol, position in live_positions.items():
df = yf.download(symbol, period='2y')
if df is None or len(df.dropna()) < 260:
plan.stale.append(symbol)
log.error(f"Data for position {symbol} is stale or missing. Marked for investigation.")
continue
This ensures that stocks that disappear from the data source are no longer left unattended. Stocks marked as stale
can then be manually checked, liquidated, or routed to a separate emergency handling flow.
Three key lessons I took away from this incident:
try-except-pass
or continuing processing with just a warning log when conditions aren't met are convenient during development but become time bombs in production. For critical processes, explicitly fail or send alerts (e.g., to Slack).When you're coding side projects late at night or on weekends, it's easy to get lazy with error handling. But skimping on it here can lead to much larger time losses later. Good learning experience.
Hope this helps other independent bot developers out there.