My Trading Bot "Perma-Held" a Delisted Stock: The Silent Bug of Disappearing Historical Data A developer known as 'Ojii' discovered that his AI trading bot silently held a delisted stock for months because the code filtered out assets with insufficient historical data, treating them as nonexistent. The bug stemmed from an external API that stopped returning data for delisted stocks, causing the exit logic to skip them without errors. He fixed it by reversing the flow to check data freshness for each held position and flagging stale data as abnormal. 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. 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. 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: 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. 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.