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.
Symptom: Silently Vanishing from the Exit Logic
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.
# Problematic code: If data is less than 260 days, it's not even considered for exit
frames = {}
for ticker in ALL_TICKERS:
# Attempts to fetch 2 years of data from an external API
df = yf.download(ticker, period='2y')
# If data cannot be fetched or is less than 260 days, it's filtered out here
if len(df.dropna()) >= 260:
frames[ticker] = df
# ... Subsequent logic completely ignores tickers not in `frames`
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.
Cause: Inconsistency Between Two APIs
What made things even more complicated was the way I was using two different APIs:
- Decision-making API: Used for technical analysis and exit decisions (e.g., historical price data). -> Data vanished from here
- Valuation API: Used for portfolio valuation and actual position management (e.g., current holdings). -> The position continued to exist here
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.
Fix: Position-Driven Data Freshness Check
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:
# Revised conceptual code: Check data freshness individually for held positions
live_positions = get_current_positions() # Fetch current holdings from brokerage API
plan = {"sell": [], "stale": []}
for symbol, position in live_positions.items():
# Attempt to fetch data individually
df = yf.download(symbol, period='2y')
# If data is missing or insufficient, mark as "stale"
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
# ... If data is normal, proceed with regular exit logic
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.
Lessons Learned: No Data is an Error
Three key lessons I took away from this incident:
- Design for Data Source Inconsistencies: In systems using multiple APIs, data might vanish from one but remain in another. Especially when dealing with assets that have a lifecycle (like listing/delisting), a mechanism to detect this inconsistency is crucial.
- Don't Tolerate Silent Errors: Designs using
try-except-passor 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). - Seriously Handle "Non-Existence": If your logic assumes data "exists," it will break when it "disappears." The state of "data cannot be retrieved" isn't just a skip condition; it's a critical signal that the system is in an unexpected situation.
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.
Top comments (0)