Regime Change Detection: How the Econ Bot Knows When the Market Has Shifted
TL;DR / Key Takeaways
- The Econ Bot's regime change detector fires when the Cleveland Fed nowcast shifts by 0.15 probability or more between cycles, signaling the market has moved under your feet.
- When it fires, the bot halts new position entries and optionally closes existing positions that are now on the wrong side of the updated signal.
- The strike-consistency enforcer runs alongside it to prevent contradictory positions from surviving a regime flip.
- Predict & Profit's Econ Bot ships with this logic built in, including 28 unit tests for false-positive prevention.
Most trading bots have an entry system and a risk system. The entry system finds edges. The risk system sets position limits and stops you from going too big. That's the standard two-part architecture and it works fine when the underlying signal is stable.
The problem with inflation markets is that the signal is not always stable.
The Cleveland Fed updates its CPI nowcast on a rolling basis as new data comes in. Some days nothing changes. Other days a single component reading moves the estimate by 0.10 to 0.20 probability points. When that happens mid-cycle, positions the bot entered an hour ago can go from correct to stranded in the same session.
Without a regime change detector, the bot just holds. It entered because the edge was there. The edge is now gone. The bot doesn't know.
That's the problem regime_change.py solves.
What "Regime Change" Means Here
I'm not using "regime change" in the macroeconomic textbook sense. I'm using it to mean one specific thing: the Cleveland Fed nowcast has moved enough that the positions currently in the book no longer reflect the bot's current probability estimate.
The threshold is 0.15.
If the nowcast was 0.62 when the bot entered a position and it updates to 0.47, that's a 0.15 swing. The regime has changed. Whatever the bot thought the market mispricing was at 0.62, it's now doing that math on stale inputs.
The detector runs every cycle, right after the nowcast scraper pulls a fresh reading. It compares the current nowcast to the nowcast value at the time each open position was entered. That entry-time value is stored in the database when the trade is logged.
def check_regime_change(
current_nowcast: float,
position: dict,
threshold: float = 0.15
) -> bool:
"""
Returns True if the nowcast has shifted enough to flag a regime change
for this position.
position dict must include:
- entry_nowcast: float (nowcast at time of entry)
- side: str ('yes' or 'no')
- strike: float (CPI strike, e.g. 3.2)
"""
entry_nowcast = position.get("entry_nowcast")
if entry_nowcast is None:
return False # can't evaluate without baseline
delta = abs(current_nowcast - entry_nowcast)
return delta >= threshold
Simple. The complexity isn't in the detection. It's in what happens next.
What the Bot Does When It Fires
There are two responses, and they're configurable independently.
The first response is always active: halt new position entries for the affected event. If the regime has changed on the July CPI release, the bot will not open any new strikes on that event until the nowcast stabilizes or the cycle resets.
The second response is optional and off by default in the shipped config: close existing positions that are now directionally wrong.
"Directionally wrong" has a precise definition here. The bot entered a "Yes" on "CPI above 3.2%" because the nowcast was 0.62, meaning it estimated a 62% chance CPI comes in above 3.2%. If the nowcast drops to 0.44, the bot now thinks there's only a 44% chance. If the market is pricing that contract at 0.55, the position has flipped from edge-positive to edge-negative. The bot should be on the other side of that trade, or flat.
def evaluate_position_after_regime_change(
current_nowcast: float,
position: dict,
market_price: float
) -> str:
"""
Returns 'close', 'hold', or 'skip' after a regime change is detected.
'close' — position is now directionally wrong, recommend closing
'hold' — position still has edge despite regime shift
'skip' — position is within 1 hour of settlement, don't touch it
"""
time_to_settle = position.get("minutes_to_settlement", 9999)
if time_to_settle < 60:
return "skip"
side = position.get("side")
current_edge = compute_edge(current_nowcast, market_price, side)
if current_edge < 0:
return "close"
return "hold"
The settlement window check matters. If a CPI position has 45 minutes left until resolution, closing it is almost always wrong. The contract is about to settle at 0 or 1. You're paying spread to exit a position that's 45 minutes from being worth exactly what it's worth. The bot skips close recommendations for anything inside that window.
The Close Order Itself
When the detector recommends closing, it goes through the same order infrastructure the rest of the bot uses. Limit orders only. IOC (immediate-or-cancel) at a price that reflects the current bid-ask midpoint. No market orders.
If the position doesn't fill, it logs the attempt and moves on. It won't keep retrying in the same cycle and it won't send a market order to force the exit. A partial fill gets logged. The remaining open size stays tracked until the next cycle.
The bot logs every close attempt with reason regime_change so it's traceable in the database. That's how you distinguish "bot exited because regime changed" from "bot exited because early-exit profit threshold was hit." Both are valid exit reasons. They should be distinct in the log.
The Strike Consistency Enforcer
The regime detector has a companion module: the strike-consistency enforcer. They're separate because they handle different failure modes, but they run in sequence.
The enforcer's job is to prevent the bot from holding contradictory positions on the same underlying. The canonical example: the bot is long "CPI above 3.2%" and also long "CPI above 3.0%." Those two positions can coexist and be consistent. But if the bot is long "CPI above 3.2%" and simultaneously long "CPI below 3.0%," that's a logical contradiction. Both can't settle at Yes.
Before the enforcer existed, a regime change could create this situation. The bot entered position A at nowcast 0.65. Nowcast drops to 0.42. The bot's new probability estimate now favors the opposite side. If the cycle runs before the regime detector closes position A, the bot could enter position B on the wrong side and briefly hold both.
The enforcer catches this. It scans the open position set at the start of each cycle and flags any strike pair where the bot holds contradictory exposure on the same event. Flagged positions are blocked from receiving new additions and queued for review.
def find_contradictory_positions(open_positions: list[dict]) -> list[tuple]:
"""
Scans open positions for logical contradictions on the same event.
Returns a list of (position_a, position_b) tuples that are contradictory.
Contradiction definition:
Both 'yes' on above X and above Y where the positions together
cannot both settle yes simultaneously, or one 'yes above' and
one 'yes below' on a strike where only one can win.
"""
contradictions = []
by_event = defaultdict(list)
for pos in open_positions:
by_event[pos["event_ticker"]].append(pos)
for event, positions in by_event.items():
for i, a in enumerate(positions):
for b in positions[i + 1:]:
if is_contradiction(a, b):
contradictions.append((a, b))
return contradictions
The 34 unit tests for the enforcer cover edge cases like positions on different events sharing similar tickers, positions that expired but weren't cleaned from the local DB, and positions where the side metadata was missing entirely. That last case defaults to "block, don't trade" rather than "assume it's fine."
Why This Is the Most Important Risk Feature
Position sizing limits matter. Fee filters matter. The daily cap matters. But all of those are static constraints. They bound the bot's behavior from above.
The regime detector is dynamic. It responds to the signal changing, which is the actual thing that causes losses in this kind of system.
Here's the failure mode it's preventing: the bot enters six CPI positions on a Tuesday morning based on a nowcast of 0.68. At 2pm the Cleveland Fed updates. Nowcast is now 0.51. If the bot doesn't know this, it holds six positions that were built on a probability estimate that no longer exists. The market will figure out the nowcast moved before the positions settle. You're on the wrong side of that repricing.
I've seen this happen in backtesting. The positions don't blow up catastrophically. They just leak. Small edges from the morning become liabilities by afternoon. You don't lose 50%. You lose 8% on six trades that should have been flat, and you wonder why your good-signal days underperform.
The regime detector stops that leak at the source.
What It Doesn't Do
It doesn't predict regime changes in advance. It can't. The Cleveland Fed nowcast updates when new data comes in and that data arrives on its own schedule. The detector is reactive, not predictive.
It also doesn't close positions that are merely losing. A position can be underwater and still have positive current edge. Those stay open. The detector only fires on direction-flip scenarios where the current signal now disagrees with the entry signal beyond the threshold.
And it doesn't operate on the Weather Bot side of the system. The Weather Bot has its own equivalent logic via the agreement filter and early-exit mechanism, but the underlying signal there is ensemble convergence, not a nowcast scrape. Different architecture, different problem, separate solution.
Tuning the Threshold
The 0.15 default was chosen empirically after backtesting against historical Cleveland Fed nowcast history. Below 0.10 generates too many false positives: normal intraday noise triggers closes unnecessarily and you end up churning positions that would have been fine. Above 0.20 is too slow: the market has already repriced before the detector fires.
If you buy the bot and run it in dry-run mode for a few cycles, the logs will show you every REGIME_CHANGE_DETECTED event with the before and after nowcast values. Adjust the threshold config value from there. The shipped default is conservative. It errs toward holding rather than churning.
The 28 unit tests cover the boundary: 0.149 does not fire, 0.150 fires, 0.151 fires. The comparison is a simple >= and the tests make sure it stays that way through any future refactors.
The detector is one of those features that's invisible when it works. You only notice it when you turn it off and watch what happens to a session where the nowcast moved. Then it becomes obvious why it exists.
The Econ Bot ships with this built in as part of the $97 bundle at predictandprofit.gumroad.com/l/predict-and-profit. Regime change logic, strike-consistency enforcer, per-event position caps: all included, all tested, all running in the same codebase.