April 3rd was a big gap day. MSFT opened +4.6% over the prior close, AAPL +4.5%, V +4.8%. If you're running an overnight position in any of those, you need to know what your bot is going to do with that โ before it does it.
Here's how TradeSight's gap detection module works, what it logged that morning, and the reasoning behind the "consider taking partial profit" recommendation rather than an automatic exit.
Portfolio: $544.19 (+8.84% from $500 starting capital) ยท 4 open positions ยท VIX: 23.9 (normal regime)
Here's the actual output from the Apr 3 scan, lightly formatted:
The gap detector runs at the top of every trading scan. It compares the previous close to the current price for each held position:
def check_gaps(positions, threshold=0.03):
"""
Flag any held position that gapped significantly from prior close.
threshold: 0.03 = 3% gap triggers a warning
"""
alerts = []
for pos in positions:
symbol = pos['symbol']
prev_close = get_prev_close(symbol) # from daily bar data
current = float(pos['current_price'])
if prev_close is None:
continue
gap_pct = (current - prev_close) / prev_close
if abs(gap_pct) >= threshold:
direction = "UP" if gap_pct > 0 else "DOWN"
unrealized = float(pos['unrealized_pl'])
alerts.append({
'symbol': symbol,
'direction': direction,
'gap_pct': gap_pct,
'prev_close': prev_close,
'current': current,
'unrealized_pl': unrealized
})
logger.warning(
f"[Gap] {symbol} gapped {direction} {gap_pct:.1%}: "
f"prev_close=${prev_close:.2f} โ current=${current:.2f}"
)
if gap_pct > 0:
logger.info(
f"[Gap] {symbol} gap up โ unrealized=${unrealized:.2f}, "
f"consider taking partial profit"
)
else:
logger.warning(
f"[Gap] {symbol} gap down โ unrealized=${unrealized:.2f}, "
f"stop-loss active at {SL_PCT:.1%}"
)
return alerts
The threshold is set at 3% by default. A gap under that is noise. Above that, something real happened overnight that your intraday signal didn't see coming.
The log says "consider taking partial profit" โ not "sell now." That's intentional. Here's the reasoning:
The regime detector ran in parallel and returned normal (VIX=23.9). That matters because the same 4.6% gap reads differently in different regimes:
| VIX Range | Regime | Gap Response |
|---|---|---|
| <18 | low_vol | Gaps are rare โ treat as signal, tighten stop |
| 18โ28 | normal | Gaps happen โ alert only, let stops run |
| 28โ40 | high_vol | Gaps are frequent โ reduce position sizes, widen stops |
| >40 | extreme | New entries paused โ manage existing only |
At VIX=23.9, we're in normal territory. The system correctly classified it and didn't alter stop-loss parameters. Had VIX been above 28, the SL/TP config would have automatically widened to SL=7% / TP=15% to account for bigger intraday swings.
def get_regime(vix: float) -> str:
if vix < 18:
return "low_vol"
elif vix < 28:
return "normal"
elif vix < 40:
return "high_vol"
else:
return "extreme"
def get_sl_tp_config(regime: str) -> dict:
configs = {
"low_vol": {"sl": 0.04, "tp": 0.10, "trail": 0.02, "trail_activates": 0.015},
"normal": {"sl": 0.05, "tp": 0.12, "trail": 0.03, "trail_activates": 0.020},
"high_vol": {"sl": 0.07, "tp": 0.15, "trail": 0.04, "trail_activates": 0.030},
"extreme": {"sl": 0.08, "tp": 0.18, "trail": 0.05, "trail_activates": 0.040},
}
return configs[regime]
A few things Apr 3 exposed:
After ~5 weeks of paper trading, the portfolio is at +8.84% ($544.19 from $500). That's not spectacular but it's positive and the system is learning โ each tournament cycle, strategies compete on real performance metrics and the loser gets its parameters evolved. The MACD Crossover strategy has been the consistent winner; RSI mean reversion has been the consistent underperformer on this particular market regime.
Gap handling is one of the less glamorous parts of a trading bot but it's where you lose money fast if you get it wrong. A system that ignores overnight moves will hold positions into reversals. One that exits on every gap will constantly sell into continuation moves. The answer is almost always: alert, adjust stops, let the position ride unless the stop fires.
If you're building something similar, the TradeSight repo is open source. The gap detection code is in signals/signal_service.py.