The Bug That Made My Bot Blind to Its Own Positions
TL;DR / Key Takeaways
- A one-line dict key error in
get_positions()caused the Econ Bot to return an empty position list on every call, making it functionally blind to its own open trades. - The downstream failures were subtle: the regime-change detector never fired, and the open-trade counter thought the book was always empty.
- Silent data access bugs are the hardest class of bug in automated trading because the bot keeps running, keeps logging, keeps looking normal while making decisions on phantom data.
- The Predict & Profit Econ Bot ships with this fix in place, plus a live Kalshi API position count replacing the stale DB count that compounded the problem.
This one took me longer to find than I care to admit.
The Econ Bot was running. No crashes. No exceptions in the logs. The regime-change detector was silent. Trades were going out. Everything looked fine from the outside if you did not look too carefully.
But something was off. The bot kept entering positions on the same CPI event across multiple scan cycles. Not wildly. Just enough that I noticed the sizing felt wrong. I had position limits in the config. They were not triggering. That was the tell.
What It Looked Like From the Outside
When a bot over-trades a single event, the first thing you check is your limit logic. I had an open trade limit. I had a per-event cap. Both were wired up and logging. Neither was blocking.
The open trade counter was reporting zero or near-zero open positions on almost every cycle. My Kalshi dashboard told a different story. I had four open positions. The bot thought it had none.
My first instinct was the database. I assumed the settlement status bug I had already fixed earlier was still causing closed trades to show as open in the local SQLite DB, which would inflate the count. But this was the opposite problem. The count was too low, not too high.
So I started tracing _check_open_trade_limit() backward.
Following the Data
The open trade limiter calls get_positions() in kalshi_client.py. That function hits the Kalshi REST API, gets a response, and returns a list of positions. I added a raw log line to dump the full API response before parsing.
The response came back clean. The API was returning data. Here is roughly what Kalshi sends back:
{
"event_positions": [...],
"market_positions": [
{
"market_ticker": "CPI-24NOV-T3.2",
"position": 5,
"position_fp": 500,
...
}
]
}
Now here is what my wrapper was doing with it:
# BEFORE (broken)
def get_positions(self) -> list:
response = self._get("/portfolio/positions")
return response.get("positions", [])
The key "positions" does not exist in the Kalshi response. It never did. The API returns "market_positions". So response.get("positions", []) was returning an empty list every single time. No exception. No warning. Just a quiet empty list.
The fix was one line:
# AFTER (fixed)
def get_positions(self) -> list:
response = self._get("/portfolio/positions")
return response.get("market_positions", [])
That is it. Wrong key, right default, silent failure. The most dangerous kind.
Why It Did Not Blow Up Loudly
This is the thing that makes bugs like this hard. The function signature was correct. The return type was correct. An empty list is a valid list. Every downstream consumer of get_positions() received a perfectly valid Python object and processed it without complaint.
The open trade counter got an empty list and counted zero positions. Correct behavior given its input. The regime-change detector got an empty list and found no positions to evaluate. Also correct behavior given its input. The event position cap got an empty list and found zero entries for the current event. Completely logical.
Every component was working exactly as designed. The bug was invisible to all of them because none of them could distinguish between "no positions exist" and "the API call returned garbage."
This is why I say silent data access bugs are the hardest class of bug in automated trading. A crash tells you something is wrong. A wrong dict key tells you nothing.
The Compounding Problem
Once I fixed the dict key, I found a second issue hiding behind it.
Even with the correct key, _check_open_trade_limit() was still reading from the local SQLite database to count open trades, not from the live Kalshi API. The database count could be stale. Positions that had settled, been closed, or partially filled were not always reflecting accurately in the local table, especially around edge cases I had not fully tested.
So I replaced the DB count entirely:
# AFTER (also fixed)
def _check_open_trade_limit(self) -> bool:
positions = self.kalshi_client.get_positions()
# Count positions with actual exposure plus resting limit orders
active_positions = [
p for p in positions
if p.get("position_fp", 0) != 0
]
resting_orders = self.kalshi_client.get_resting_orders()
total_active = len(active_positions) + len(resting_orders)
if total_active >= self.config["max_open_trades"]:
self.logger.info(
f"OPEN_TRADE_LIMIT total_active={total_active} "
f"limit={self.config['max_open_trades']}"
)
return False
return True
Now the limit check uses what Kalshi actually sees, not what the local DB thinks it sees. These should agree. When they do not, you want to trust the exchange.
The Downstream Failures Were Real
Once I fixed both issues, the regime-change detector started firing correctly. It had been getting an empty position list and silently deciding there was nothing to close. The event position cap started blocking entries it should have been blocking for weeks.
The bot had been running in a partially lobotomized state. It could place trades. It could log. It could pull signals. It just could not see what it had already done.
In a slower-moving market that might not have cost much. In a market that moves during a CPI release, entering the same side multiple times when you already have a full position is how you create real risk.
The Lesson I Keep Relearning
Log what the bot sees, not just what it does.
I had logging on trade execution. I had logging on signal generation. I did not have logging on the raw position fetch before parsing. That one missing log line is why this took as long as it did to find.
Now every call to get_positions() logs the raw count before it returns anything:
def get_positions(self) -> list:
response = self._get("/portfolio/positions")
positions = response.get("market_positions", [])
self.logger.debug(f"POSITIONS_FETCH raw_count={len(positions)}")
return positions
One line. Cheap. It gives you a baseline you can compare against your dashboard. If the bot logs POSITIONS_FETCH raw_count=0 and your Kalshi account shows four open trades, you know exactly where to look.
The bot running silently is not the same as the bot running correctly. I had to learn that the hard way with this one.
The Econ Bot ships with all of this fixed. The wrong key is corrected, the DB count is replaced with a live API count, and the raw position fetch is logged on every cycle. If you buy the source code, you are getting the patched version, not the version that was quietly trading blind.