Auto-Close on Regime Change: The Feature I Was Afraid to Ship
TL;DR / Key Takeaways
- The Econ Bot can now automatically close existing positions when the Cleveland Fed nowcast shifts more than 0.15 probability points, not just halt new entries.
- This feature sat in a branch for weeks because auto-close is the kind of thing that can blow up your account if you ship it wrong.
- The testing path was: unit tests first, dry-run mode second, live on my personal Kalshi account third, then customer release.
- The Predict & Profit Econ Bot ships with regime-change auto-close included, defaulting to conservative thresholds that won't fire on noise.
I built the regime-change detector months before I let it touch a real order. The detection logic was easy. The auto-close part scared me.
Halting new trades when the data shifts is low-stakes. You miss an opportunity. Fine. But closing existing positions is different. That's the bot reaching into your account and selling something you own. If the logic is wrong, if the threshold is too sensitive, if there's a data hiccup from the Cleveland Fed scraper, you can close a winning position for no reason. Or close it at a bad price. Or, in some worst-case scenario I kept running through my head, close the wrong position entirely.
So I sat on the feature. Kept it in a branch. Kept telling myself I'd ship it when I was sure.
Here's what finally got me sure.
What Regime Change Actually Means
The Econ Bot's primary signal is the Cleveland Fed nowcast. It publishes a probability estimate for where CPI will land. The bot reads that number, compares it to the Kalshi market price, and looks for edge.
The nowcast updates regularly. Most of the time the update is small. A decimal point moves. Nothing to act on. But sometimes there's a real shift. A gas price spike hits the FRED feed. A hot PPI number drops. The nowcast moves 0.15 probability points or more. When that happens, a position that looked correct when you opened it can be on the wrong side of the market now.
Before regime-change auto-close, the bot would just stop placing new trades on the affected event. The old positions stayed open. You'd have to manually decide what to do with them.
That's not a trading system. That's half a trading system.
The regime_change.py Module
The detection logic lives in regime_change.py. It runs on every Econ Bot cycle, after the nowcast scrape and before the trade scan.
def detect_regime_change(
current_nowcast: float,
previous_nowcast: float,
threshold: float = 0.15
) -> bool:
"""
Returns True if the nowcast has shifted enough to consider
existing positions stale and potentially on the wrong side.
"""
delta = abs(current_nowcast - previous_nowcast)
return delta >= threshold
Simple. The threshold is configurable. The default is 0.15. That's not a number I pulled from thin air. I ran the Cleveland Fed historical nowcast data through a backtest to see how often the nowcast moves more than a given threshold between update cycles. At 0.10, it fires too often. At 0.20, you miss real moves. At 0.15, it catches genuine shifts without triggering on noise.
The previous nowcast is persisted to the database so it survives bot restarts. First run after a fresh start has no previous value, so the detector skips. No phantom triggers on initialization.
What Happens When It Fires
When detect_regime_change() returns True, the bot doesn't immediately close everything. It evaluates each open position against the new nowcast direction.
The key function is evaluate_position_for_close():
def evaluate_position_for_close(
position: dict,
current_nowcast: float,
direction: str # 'higher' or 'lower'
) -> bool:
"""
Returns True if this position is now on the wrong side
of the updated nowcast direction.
position: dict from Kalshi API with 'side', 'ticker', 'quantity'
current_nowcast: float probability from Cleveland Fed
direction: which way the nowcast moved
"""
side = position.get("side")
strike_above = is_above_strike(position.get("ticker", ""))
if direction == "higher" and side == "no" and strike_above:
return True
if direction == "lower" and side == "yes" and strike_above:
return True
return False
is_above_strike() parses the Kalshi ticker to determine if the contract is structured as "CPI above X%". That parsing is its own function because Kalshi ticker formats are not always consistent and I wanted the logic isolated and testable.
If evaluate_position_for_close() returns True, the bot calls close_position().
The close_position() Function
def close_position(
client: KalshiClient,
ticker: str,
quantity: int,
side: str,
dry_run: bool = True
) -> dict:
"""
Closes an open position by placing a market-side limit order
at the current best bid/ask. Uses IOC to avoid partial fill hangs.
Returns the order response or a dry-run stub.
"""
if dry_run:
logger.info(f"[DRY RUN] Would close {quantity}x {ticker} side={side}")
return {"status": "dry_run", "ticker": ticker, "quantity": quantity}
# Fetch current book to get best price
book = client.get_order_book(ticker)
close_side = "no" if side == "yes" else "yes"
best_price = get_best_price(book, close_side)
if best_price is None:
logger.warning(f"No liquidity to close {ticker}. Skipping.")
return {"status": "skipped", "reason": "no_liquidity"}
order = client.place_order(
ticker=ticker,
side=close_side,
count=quantity,
type="limit",
limit_price=best_price,
time_in_force="ioc"
)
logger.info(
f"REGIME_CLOSE ticker={ticker} side={close_side} "
f"qty={quantity} price={best_price} order_id={order.get('order_id')}"
)
return order
A few things worth noting here.
It's IOC. Immediate-or-cancel. If the order doesn't fill at that price, it cancels. The bot doesn't leave a resting limit order sitting in the book. That felt important for a safety feature. You want the close attempt to either work now or fail loudly, not silently queue.
If there's no liquidity on the close side, it logs and skips. It doesn't retry aggressively. In a thin Kalshi CPI market, forcing a close at a terrible price is worse than holding the position and letting it settle.
The dry_run flag is checked first, before any API call. That flag defaults to True in the function signature. I had to explicitly pass dry_run=False to make it go live. That was a deliberate design choice.
The Testing Path
I said I was afraid to ship it. Here's what actually got me to ship it.
Step one: unit tests. Regime change ships with 28 unit tests. They cover: threshold detection at boundary values (0.149 does not trigger, 0.150 does), direction inference when the nowcast moves up vs. down, position evaluation for every combination of side and strike type, the no-liquidity skip path, and the dry-run stub behavior. None of them touch a live API. They run against mocked clients and stubbed data.
Step two: dry-run on my own account. I ran the bot in dry-run mode for two full CPI cycles. The logs showed which positions would have been closed and at what prices. I compared those closes to what actually happened at settlement. In both cycles, the regime detector fired once each. In both cases, the positions it flagged would have been losers at settlement. The close would have been correct.
That was reassuring. But two data points isn't a sample size.
Step three: live on a small personal account. Before the customer release, I ran the auto-close live on my own Kalshi account with smaller position sizes. Two more cycles. One regime trigger, one correct close. No bad fills. No liquidity failures. No ghost orders.
Four data points is still not a sample size. But at some point you have to ship the thing.
Step four: customer release with conservative defaults. The threshold ships at 0.15. The bot logs REGIME_CHANGE_DETECTED and REGIME_CLOSE events clearly. Customers can see exactly what fired and why. If someone wants to raise the threshold to 0.20 or disable auto-close entirely, that's a config flag.
Why the Default Is Conservative
I could have set the threshold lower. At 0.10, the bot would close positions more aggressively. It would also fire on noise more often, particularly in the 24 hours before a CPI release when the Cleveland Fed nowcast can move in small increments as new data arrives.
The cost of a false positive here is real. You close a position, pay the spread, and watch it settle in your favor anyway. That's a loss you didn't have to take.
The cost of a false negative is smaller. The nowcast moves, you don't close, the position settles against you. That's a loss too, but it's a loss the original trade entry already priced in at some level.
False positives feel worse because the bot caused them. So I defaulted conservative and gave customers the knob to tune it.
What the Logs Look Like When It Fires
2026-04-12 08:23:41 INFO NOWCAST_UPDATE prev=0.412 current=0.563 delta=0.151
2026-04-12 08:23:41 INFO REGIME_CHANGE_DETECTED delta=0.151 threshold=0.150
2026-04-12 08:23:41 INFO Evaluating 3 open positions for regime close
2026-04-12 08:23:41 INFO POSITION_EVAL ticker=CPI-26APR-A32 side=yes wrong_side=True
2026-04-12 08:23:41 INFO POSITION_EVAL ticker=CPI-26APR-A34 side=yes wrong_side=True
2026-04-12 08:23:41 INFO POSITION_EVAL ticker=CPI-26APR-B30 side=no wrong_side=False
2026-04-12 08:23:42 INFO REGIME_CLOSE ticker=CPI-26APR-A32 side=no qty=5 price=0.41 order_id=ord_abc123
2026-04-12 08:23:42 INFO REGIME_CLOSE ticker=CPI-26APR-A34 side=no qty=3 price=0.39 order_id=ord_abc124
2026-04-12 08:23:42 INFO REGIME_CLOSE_COMPLETE closed=2 skipped=1
That's the full trace. You can see exactly what moved, by how much, which positions got flagged, which ones didn't, and what orders went out. The CPI-26APR-B30 side=no position survived because the nowcast moving higher actually makes a "below 3.0%" position more wrong, not the "below 3.0% no" position. The evaluation logic preserved it correctly.
The One Condition I Almost Got Wrong
Early versions of the evaluation logic had a subtle bug. When the nowcast moved higher, I was flagging all "yes" positions as wrong-side candidates. But that's not always correct.
A "yes" on "CPI above 2.8%" becomes more right when the nowcast moves higher, not less. The issue was that I was conflating "yes" side with "above-strike" structure, when the correct logic requires knowing both.
The is_above_strike() parser handles this. It checks the Kalshi ticker format to determine whether the contract pays out if CPI lands above the strike or below it. Combined with the side, that gives you the correct directionality.
The unit tests caught this. Specifically, the test case for a "yes" position on an above-strike contract when the nowcast moves higher. Without that test, the bug would have shipped and closed correct positions on a nowcast confirmation move. Which would have been very bad.
I sat on this feature longer than I should have. But I think the time I spent on it shows in the final code. The default is conservative. The logging is thorough. The dry-run path works before any real order goes out. And the strike-consistency enforcer runs alongside it, so you can't end up in a situation where the regime close creates a contradictory position set by accident.
The feature is in the current release. Both bots ship in the $97 bundle at predictandprofit.gumroad.com. If you're building your own version of this, the testing path matters more than the code. Ship the dry-run first. Watch it for two cycles minimum. Then decide.