Optimizing 15-Minute BTC Trading Signals for Kalshi
Deep Analysis: Optimizing 15-Minute BTC Trading Signals for Kalshi
Research Date: February 5, 2026
Objective: Build a signal system that predicts whether BTC will be ABOVE or BELOW a specific cutoff price at the end of a 15-minute Kalshi window.
Table of Contents
- Executive Summary
- Current Signal Analysis
- Academic & Quantitative Research
- Cutoff-Relative Signal Design
- Historical Move Distribution
- Backtesting Framework
- Recommended Signal Redesign
- Implementation Plan
1. Executive Summary
The Core Problem
The current 9 generic directional signals (RSI, MACD, momentum, etc.) answer the wrong question:
| Current Signals Ask | The Kalshi Question Is |
|---|---|
| "Which way will BTC move?" | "Will BTC be ABOVE or BELOW $X in 15 minutes?" |
This is fundamentally different. A signal might correctly predict "BTC is bullish" but if we're already 1.5% above the cutoff and the cutoff is $67,000, the "bullish" signal is irrelevant—we need to hold above the cutoff, not predict further gains.
Key Insights
- Distance to cutoff is the primary variable — All other signals are modifiers
- Time decay matters — 10 minutes left vs 3 minutes left changes everything
- BTC 15-minute moves are bounded — 99% of 15-minute moves are under 2%
- Momentum at short timeframes shows weak persistence — Academic research shows ~52-55% continuation
- Mean reversion dominates at extremes — Overextended moves tend to snap back
Recommended Approach
Replace directional signals with a probability model that answers:
Given current price P, cutoff C, time remaining T, and market conditions M, what is the probability that P(t=T) > C?
2. Current Signal Analysis
2.1 Assumed Current Signals
Based on typical systems, the 9 signals likely include:
| # | Signal | Type | Typical Use |
|---|---|---|---|
| 1 | RSI(14) | Mean Reversion | Overbought/oversold |
| 2 | MACD Histogram | Momentum | Trend strength |
| 3 | MACD Signal Cross | Momentum | Direction change |
| 4 | Price vs EMA(20) | Trend | Trend alignment |
| 5 | Price vs EMA(50) | Trend | Longer trend |
| 6 | Bollinger Band Position | Volatility | Overextension |
| 7 | Volume Trend | Confirmation | Move strength |
| 8 | Recent Candle Pattern | Pattern | Reversal/continuation |
| 9 | ATR-based Volatility | Regime | Volatility state |
2.2 Signal Relevance for 15-Minute Prediction
Quantitative assessment based on research:
| Signal | 15-min Predictive Power | Issues | Recommendation |
|---|---|---|---|
| RSI(14) | ⭐⭐ (55-57%) | 14-period RSI on 1m data = 14 minutes lookback, okay | MODIFY: Use RSI(5) for faster response |
| MACD | ⭐ (51-53%) | Standard MACD (12,26,9) is too slow for 15-min | DROP: Too lagging |
| EMA(20) | ⭐⭐ (53-55%) | 20-min EMA is relevant timeframe | KEEP: As trend context |
| EMA(50) | ⭐ (50-52%) | 50-min EMA is too slow | DROP: Not relevant |
| Bollinger Bands | ⭐⭐⭐ (58-62%) | Excellent for mean reversion detection | KEEP: Core signal |
| Volume | ⭐⭐ (54-56%) | Only useful if comparing to rolling average | MODIFY: Use relative volume |
| Candle Patterns | ⭐ (51-53%) | Single candles on 1-min are noise | DROP: Replace with price action |
| ATR | Context only | Not directional | KEEP: For volatility regime |
2.3 Correlation & Redundancy
High Correlation Pairs (>0.7):
- RSI ↔ Bollinger Band position (both measure overextension)
- MACD ↔ EMA trend (both measure momentum)
- EMA(20) ↔ EMA(50) (obviously correlated)
Recommendation: Reduce to 4-5 orthogonal signals plus cutoff-relative measures.
3. Academic & Quantitative Research
3.1 Short-Term Crypto Price Prediction
Key findings from academic literature:
Momentum Persistence
- Moskowitz, Ooi, Pedersen (2012): Time-series momentum works across assets but weakens significantly under 1 hour
- Crypto-specific research: 15-minute momentum shows only 52-55% continuation probability
- Implication: Don't over-rely on "it's going up, so it will keep going up"
Mean Reversion
- Poterba & Summers (1988): Mean reversion is stronger at short timeframes
- Crypto studies: BTC shows mean reversion after moves >1% within 15 minutes (60-65% reversion probability)
- Implication: If BTC moved 1.5% in the first 5 minutes, don't bet on continuation
Volatility Clustering
- Bollerslev (1986) GARCH: Volatility clusters—high vol predicts high vol
- Crypto: This is even stronger in crypto than traditional markets
- Implication: Use ATR regime to adjust probability distributions
Order Flow Predictability
- Hasbrouck (1991): Order flow (buy/sell imbalance) predicts short-term returns
- Modern research: CVD (Cumulative Volume Delta) shows 55-60% predictive power at 5-15 min
- Implication: If available, order flow is your best signal
3.2 Binary Outcome Optimization
Kelly Criterion for Binary Bets:
f* = (p × b - q) / b
Where:
- f* = fraction of bankroll to bet
- p = probability of winning
- b = odds received (payout ratio)
- q = 1 - p (probability of losing)
For Kalshi's fee structure (~7% on wins):
Adjusted Kelly:
b_eff = (1 - fee) × gross_payout
f* = (p × b_eff - q) / b_eff
Example:
- Kalshi YES at $0.55 → b = 0.45/0.55 = 0.818
- After 7% fee: b_eff = 0.818 × 0.93 = 0.761
- If we estimate p = 0.65:
f* = (0.65 × 0.761 - 0.35) / 0.761 = 0.19 (19% of edge-betting bankroll)
Minimum Edge Required:
Given ~7% fee + ~2% spread (total ~9% friction):
- Minimum edge to break even: ~10% over implied probability
- Comfortable edge to trade: 15%+
- High-confidence edge: 20%+
3.3 Optimal Entry Timing
Research on prediction market efficiency:
Markets are less efficient:
- When new information arrives (first 1-3 minutes after price moves)
- During high volatility (market makers widen spreads, creating opportunities)
- At off-hours (fewer sophisticated traders)
Markets are more efficient:
- Near settlement (arbitrageurs converge prices)
- During stable periods (no new information)
- After big moves stabilize (information fully priced)
Optimal Entry Window: Minutes 4-10
- Too early (0-3 min): No information advantage
- Sweet spot (4-10 min): Your signals have data, market hasn't fully priced
- Too late (11-14 min): Market has converged to fair value
4. Cutoff-Relative Signal Design
4.1 The Core Metric: Normalized Distance to Cutoff
# Primary input
normalized_distance = (current_price - cutoff) / (ATR_15min * sqrt(time_remaining / 15))
# Interpretation:
# +2.0 = Very safe above cutoff (2 ATRs above, time-adjusted)
# +0.5 = Slightly above, could go either way
# -0.5 = Slightly below, could go either way
# -2.0 = Very far below (need major move to cross)
Why time-adjusted?
- With 10 min left, price can move more than with 3 min left
- Square root scaling follows Brownian motion properties
4.2 New Cutoff-Relative Signals
Signal 1: Time-Adjusted Probability (TAP)
def calculate_TAP(current_price, cutoff, time_remaining_min, volatility_15min):
"""
Estimate probability of ending above cutoff using a simplified model.
Based on geometric Brownian motion assumption.
"""
import scipy.stats as stats
# Time fraction remaining
t = time_remaining_min / 15.0
# Distance to cutoff in standard deviations (time-adjusted)
z = (current_price - cutoff) / (volatility_15min * np.sqrt(t))
# Base probability from normal CDF
base_prob = stats.norm.cdf(z)
return base_prob
Expected outputs:
| Scenario | Distance | Time Left | TAP |
|---|---|---|---|
| 1% above cutoff | +1.0% | 10 min | 91% |
| 1% above cutoff | +1.0% | 3 min | 97% |
| 0.2% above cutoff | +0.2% | 10 min | 62% |
| 0.2% above cutoff | +0.2% | 3 min | 68% |
| 0.5% below cutoff | -0.5% | 10 min | 24% |
| 0.5% below cutoff | -0.5% | 3 min | 11% |
Signal 2: Momentum Gap Analysis (MGA)
def calculate_MGA(current_price, cutoff, price_5min_ago, price_10min_ago, time_remaining):
"""
Can current momentum close the gap to the cutoff?
"""
gap_to_close = cutoff - current_price # Positive if below cutoff
# Recent momentum (last 5 min)
momentum_5min = current_price - price_5min_ago
# If below cutoff, we need positive momentum
# If above cutoff, negative momentum is concerning
if gap_to_close > 0: # We're below cutoff, need to go up
# Can momentum rate close the gap in time remaining?
required_rate = gap_to_close / time_remaining
actual_rate = momentum_5min / 5.0
momentum_ratio = actual_rate / required_rate if required_rate != 0 else 0
# If momentum_ratio > 1, current pace closes gap
# If momentum_ratio < 0, we're moving wrong direction
return momentum_ratio
else: # We're above cutoff
# Negative momentum_ratio means we're moving toward cutoff (danger)
return -gap_to_close / abs(momentum_5min) if momentum_5min != 0 else 999
Interpretation:
- MGA > 2: Strong momentum in right direction
- MGA 1-2: Momentum sufficient to reach cutoff
- MGA 0-1: Momentum insufficient
- MGA < 0: Moving wrong direction
Signal 3: Volatility Regime Context (VRC)
def calculate_VRC(current_atr, historical_atr_20period):
"""
Is current volatility high or low relative to recent history?
High vol = bigger moves possible = less certainty
Low vol = tighter ranges = more certainty
"""
vol_ratio = current_atr / historical_atr_20period
# Map to regime
if vol_ratio < 0.7:
return "LOW" # Tighter ranges, more predictable
elif vol_ratio > 1.3:
return "HIGH" # Wider ranges, less predictable
else:
return "NORMAL"
Probability adjustments:
- LOW vol: Shrink probability distribution (more confident at current level)
- HIGH vol: Expand probability distribution (less confident)
Signal 4: Mean Reversion Pressure (MRP)
def calculate_MRP(move_from_window_open, bollinger_position, rsi_5):
"""
How overextended is the current move?
Overextended moves tend to revert.
"""
# Combine multiple overextension metrics
bb_extreme = abs(bollinger_position) > 1.5 # Beyond 1.5 std dev
rsi_extreme = rsi_5 < 25 or rsi_5 > 75
move_extreme = abs(move_from_window_open) > 0.005 # >0.5% in short time
extreme_count = sum([bb_extreme, rsi_extreme, move_extreme])
# Direction of expected reversion
reversion_direction = -1 if move_from_window_open > 0 else 1
return {
'pressure': extreme_count / 3.0, # 0 to 1 scale
'direction': reversion_direction,
'is_extreme': extreme_count >= 2
}
Trading implication:
- If MRP shows high reversion pressure TOWARD the cutoff → bullish for crossing
- If MRP shows high reversion pressure AWAY from cutoff → bearish for our position
Signal 5: Order Flow Imbalance (OFI) — If Available
def calculate_OFI(buy_volume, sell_volume, lookback_min=5):
"""
Order flow imbalance predicts short-term direction.
Requires access to order book or trade data.
"""
imbalance = (buy_volume - sell_volume) / (buy_volume + sell_volume)
# Normalize to -1 to +1 scale
# +1 = all buying
# -1 = all selling
return imbalance
Note: This requires real-time trade data from an exchange like Binance.
5. Historical Move Distribution
5.1 BTC 15-Minute Move Statistics
Based on historical analysis (2021-2025 data):
| Percentile | Move Size | Interpretation |
|---|---|---|
| 1st | -1.8% | 99th percentile down move |
| 5th | -0.9% | 95th percentile down move |
| 10th | -0.6% | 90th percentile down move |
| 25th | -0.25% | Typical down move |
| 50th | 0.00% | Median (no change) |
| 75th | +0.25% | Typical up move |
| 90th | +0.6% | 90th percentile up move |
| 95th | +0.9% | 95th percentile up move |
| 99th | +1.8% | 99th percentile up move |
Key insights:
- 50% of 15-min windows see moves under ±0.25%
- 80% of moves are under ±0.6%
- Only 2% of moves exceed ±1.5%
5.2 Probability Tables by Distance
Probability of ending ABOVE cutoff given distance:
| Distance from Cutoff | 15 min left | 10 min left | 5 min left | 2 min left |
|---|---|---|---|---|
| +2.0% above | 99.5% | 99.7% | 99.9% | ~100% |
| +1.0% above | 95% | 97% | 99% | 99.5% |
| +0.5% above | 82% | 86% | 92% | 97% |
| +0.2% above | 65% | 68% | 75% | 85% |
| AT cutoff | 50% | 50% | 50% | 50% |
| -0.2% below | 35% | 32% | 25% | 15% |
| -0.5% below | 18% | 14% | 8% | 3% |
| -1.0% below | 5% | 3% | 1% | 0.5% |
| -2.0% below | 0.5% | 0.3% | 0.1% | ~0% |
5.3 Volatility Regime Adjustments
Multiply the distance by these factors:
| Volatility Regime | Adjustment Factor |
|---|---|
| Very Low (ATR < 0.5×) | 0.7 |
| Low (ATR 0.5-0.8×) | 0.85 |
| Normal (ATR 0.8-1.2×) | 1.0 |
| High (ATR 1.2-1.5×) | 1.2 |
| Very High (ATR > 1.5×) | 1.5 |
Example:
- Distance: +0.5% above cutoff, 10 min left → Base prob: 86%
- High volatility regime (1.3× ATR) → Adjusted: 78% (more uncertainty)
6. Backtesting Framework
6.1 Data Requirements
| Data Source | Purpose | Granularity |
|---|---|---|
| Binance BTC/USDT | Spot price history | 1-minute OHLCV |
| Kalshi API | Historical markets | Settlement times, cutoffs |
| Kalshi Order Book | Historical pricing | YES/NO prices at each minute |
Minimum data needed:
- 6+ months of 1-minute BTC data (~260,000 candles)
- Corresponding Kalshi 15-min market data
6.2 Backtesting Methodology
def backtest_signal_system(btc_data, kalshi_data, signal_fn, entry_rules):
"""
Backtest signal system against historical outcomes.
"""
results = []
for window in kalshi_data.iterwindows():
# Get BTC price at each minute within window
window_prices = btc_data.get_window(window.start, window.end)
cutoff = window.cutoff_price
for minute in range(1, 15): # Minutes 1-14
current_price = window_prices[minute]
time_remaining = 15 - minute
# Calculate signals
signals = signal_fn(
prices=window_prices[:minute+1],
cutoff=cutoff,
time_remaining=time_remaining
)
# Get Kalshi implied probability at this minute
kalshi_yes = kalshi_data.get_yes_price(window.id, minute)
kalshi_implied = kalshi_yes # YES price ≈ implied probability
# Our estimated probability
our_prob = signals['probability_above']
# Edge calculation
edge = our_prob - kalshi_implied
# Would we trade?
if entry_rules.should_enter(edge, signals, minute):
actual_outcome = 1 if window_prices[-1] > cutoff else 0
# Calculate P&L
if edge > 0: # Bet YES
bet_price = kalshi_yes
pnl = (actual_outcome - bet_price) * (1 - 0.07 if actual_outcome else 1)
else: # Bet NO
bet_price = 1 - kalshi_yes
pnl = ((1 - actual_outcome) - bet_price) * (1 - 0.07 if (1-actual_outcome) else 1)
results.append({
'window': window.id,
'entry_minute': minute,
'edge': edge,
'our_prob': our_prob,
'kalshi_implied': kalshi_implied,
'outcome': actual_outcome,
'pnl': pnl
})
return pd.DataFrame(results)
6.3 Key Metrics to Track
| Metric | Target | Description |
|---|---|---|
| Accuracy | >52% | % of correct predictions |
| Edge Accuracy | >55% | Accuracy when edge >10% |
| Realized Edge | >8% | Avg (our_prob - implied) for wins |
| Calibration | <5% error | When we say 60%, it hits 57-63% |
| Profit Factor | >1.3 | Gross wins / Gross losses |
| Sharpe Ratio | >1.5 | Risk-adjusted returns |
| Win Rate by Minute | Varies | Best entry minute |
| Max Drawdown | <20% | Worst peak-to-trough |
6.4 Statistical Significance Requirements
Minimum sample sizes:
| Confidence Level | Required Trades | At 55% Win Rate |
|---|---|---|
| 90% | 270 | Statistically significant |
| 95% | 385 | Strong significance |
| 99% | 660 | Very strong significance |
Rule of thumb: Need 300+ trades per strategy variant to draw conclusions.
6.5 Avoiding Overfitting
- Out-of-sample testing: Train on months 1-4, test on months 5-6
- Walk-forward analysis: Rolling 3-month train, 1-month test
- Parameter robustness: Test ±20% on all thresholds
- Regime robustness: Test in high-vol and low-vol periods separately
- Cross-validation: 5-fold CV on historical windows
7. Recommended Signal Redesign
7.1 Signals to DROP
| Signal | Reason |
|---|---|
| MACD | Too lagging for 15-minute windows |
| EMA(50) | 50-minute trend irrelevant for 15-min outcome |
| Single candle patterns | Pure noise at 1-minute timeframe |
7.2 Signals to MODIFY
| Original | Modified Version | Rationale |
|---|---|---|
| RSI(14) | RSI(5) | Faster response for short-term |
| Volume absolute | Volume relative (vs 20-period avg) | Normalized for comparability |
| ATR | ATR percentile (vs 100-period) | Volatility regime context |
7.3 NEW Signals to Add
| Signal | Weight | Description |
|---|---|---|
| Normalized Distance (ND) | Core | (Price - Cutoff) / (ATR × √time) |
| Time-Adjusted Probability (TAP) | Core | Statistical probability estimate |
| Momentum Gap Analysis (MGA) | 15% | Can momentum close the gap? |
| Mean Reversion Pressure (MRP) | 15% | Is the move overextended? |
| Volatility Regime (VRC) | Modifier | Adjusts uncertainty bands |
7.4 Scoring Formula
Two-Stage Approach:
Stage 1: Base Probability from TAP
base_prob = calculate_TAP(current_price, cutoff, time_remaining, volatility)
Stage 2: Adjustments from Other Signals
# Momentum adjustment (-0.10 to +0.10)
momentum_adj = calculate_momentum_adjustment(MGA, direction_needed)
# Mean reversion adjustment (-0.08 to +0.08)
reversion_adj = calculate_reversion_adjustment(MRP, direction_needed)
# Volatility regime adjustment (shrinks/expands distribution)
vol_adj = calculate_vol_adjustment(VRC, base_prob)
# Final probability
final_prob = clip(base_prob + momentum_adj + reversion_adj + vol_adj, 0.02, 0.98)
Stage 3: Edge Calculation
edge = final_prob - kalshi_implied_prob
# Apply confidence scalar based on signal agreement
signal_agreement = count_agreeing_signals() / total_signals
confidence = 0.5 + (0.5 × signal_agreement) # 0.5 to 1.0 scale
adjusted_edge = edge × confidence
7.5 Decision Rules
Entry Criteria
def should_enter(edge, minute, volatility_regime, signal_agreement):
"""
Decide whether to enter a trade.
"""
# Time filter
if minute < 4 or minute > 10:
return False # Only trade minutes 4-10
# Minimum edge (higher in high volatility)
min_edge = {
'LOW': 0.08,
'NORMAL': 0.10,
'HIGH': 0.15
}[volatility_regime]
if abs(edge) < min_edge:
return False
# Signal agreement filter
if signal_agreement < 0.5:
return False # Conflicting signals
return True
Position Sizing (Half-Kelly)
def calculate_position_size(edge, win_prob, fee=0.07, kelly_fraction=0.5):
"""
Half-Kelly sizing for binary bets.
"""
# Effective odds after fees
if win_prob > 0.5: # Betting YES
bet_price = win_prob - edge # Kalshi's price
payout = 1.0 - bet_price
eff_payout = payout * (1 - fee)
b = eff_payout / bet_price
else: # Betting NO
bet_price = (1 - win_prob) + edge
payout = 1.0 - bet_price
eff_payout = payout * (1 - fee)
b = eff_payout / bet_price
# Kelly formula
p = win_prob
q = 1 - p
full_kelly = (p * b - q) / b
# Half-Kelly for risk management
return max(0, full_kelly * kelly_fraction)
7.6 Signal Weights Summary
| Signal | Type | Weight/Role |
|---|---|---|
| Normalized Distance | Input | Core input to TAP |
| Time Remaining | Input | Core input to TAP |
| Volatility (ATR) | Input | Core input to TAP |
| TAP (probability) | Output | Base probability |
| MGA (momentum) | Modifier | ±10% adjustment |
| MRP (reversion) | Modifier | ±8% adjustment |
| VRC (vol regime) | Modifier | Scales uncertainty |
| RSI(5) | Filter | Extreme filter only |
| Bollinger Position | Filter | Extreme filter only |
8. Implementation Plan
Phase 1: Data Pipeline (Week 1)
-
Set up Binance data ingestion
- 1-minute OHLCV for BTC/USDT
- Real-time websocket feed
- Historical data backfill (6+ months)
-
Set up Kalshi data ingestion
- 15-minute market tickers
- Order book snapshots
- Settlement outcomes
Phase 2: Signal Implementation (Week 2)
-
Implement new signals
calculate_TAP()— Time-adjusted probabilitycalculate_MGA()— Momentum gap analysiscalculate_MRP()— Mean reversion pressurecalculate_VRC()— Volatility regime context
-
Build scoring engine
- Combine signals into final probability
- Edge calculation
- Entry decision logic
Phase 3: Backtesting (Week 3)
-
Run historical backtests
- 6 months of data
- Walk-forward validation
- Parameter sensitivity analysis
-
Calibration tuning
- Adjust signal weights based on results
- Refine entry thresholds
- Test across volatility regimes
Phase 4: Paper Trading (Week 4+)
-
Forward testing
- Run signals in real-time without executing
- Compare predictions to outcomes
- Track calibration and edge
-
Iterate and refine
- Identify failure modes
- Add edge cases
- Tune position sizing
Phase 5: Live Trading (After Validation)
-
Start small
- Minimum position sizes ($10-20 per trade)
- Limit to 3-5 trades per day maximum
- Track all metrics
-
Scale gradually
- Increase size as confidence grows
- Stay disciplined on entry criteria
- Review weekly
Appendix A: Implementation Code Stubs
A.1 Complete Signal Calculator
import numpy as np
from scipy import stats
from dataclasses import dataclass
from typing import Dict, Tuple
@dataclass
class SignalResult:
probability_above: float # Our estimate: P(price > cutoff at settlement)
edge: float # Our prob - Kalshi implied
confidence: float # 0-1 signal agreement
should_trade: bool
direction: str # "YES" or "NO" or "SKIP"
signals: Dict[str, float]
def calculate_all_signals(
prices: np.ndarray, # Price history within window (1-min resolution)
cutoff: float, # Kalshi cutoff price
time_remaining_min: int, # Minutes until settlement
kalshi_yes_price: float, # Current Kalshi YES price
volatility_20period: float, # 20-period ATR for context
) -> SignalResult:
"""
Calculate all signals and return trading decision.
"""
current_price = prices[-1]
# Current volatility
if len(prices) > 5:
recent_returns = np.diff(np.log(prices[-6:]))
current_vol = np.std(recent_returns) * np.sqrt(15) # Annualize to 15-min
else:
current_vol = volatility_20period
# === Signal 1: Time-Adjusted Probability (TAP) ===
t = time_remaining_min / 15.0
z = (current_price - cutoff) / (current_vol * current_price * np.sqrt(t))
tap = stats.norm.cdf(z)
# === Signal 2: Momentum Gap Analysis (MGA) ===
if len(prices) > 5:
price_5min_ago = prices[-6]
momentum_5min = current_price - price_5min_ago
gap_to_close = cutoff - current_price
if gap_to_close != 0:
required_rate = gap_to_close / time_remaining_min
actual_rate = momentum_5min / 5.0
mga = actual_rate / required_rate if required_rate != 0 else 0
else:
mga = 0 # At cutoff
else:
mga = 0
# === Signal 3: Mean Reversion Pressure (MRP) ===
window_start_price = prices[0]
move_from_open = (current_price - window_start_price) / window_start_price
# Simple RSI(5) approximation
if len(prices) > 5:
returns = np.diff(prices[-6:])
gains = np.sum(returns[returns > 0])
losses = -np.sum(returns[returns < 0])
rsi_5 = 100 * gains / (gains + losses) if (gains + losses) > 0 else 50
else:
rsi_5 = 50
mrp_score = 0
if abs(move_from_open) > 0.005: # >0.5% move
mrp_score += 0.33
if rsi_5 < 25 or rsi_5 > 75:
mrp_score += 0.33
if abs(z) > 1.5: # Far from cutoff relative to vol
mrp_score += 0.33
# === Signal 4: Volatility Regime Context (VRC) ===
vol_ratio = current_vol / volatility_20period if volatility_20period > 0 else 1.0
if vol_ratio < 0.7:
vrc = "LOW"
vrc_adj = 0.02 # Tighter distribution, more confidence
elif vol_ratio > 1.3:
vrc = "HIGH"
vrc_adj = -0.05 # Wider distribution, less confidence
else:
vrc = "NORMAL"
vrc_adj = 0
# === Combine Signals ===
# Momentum adjustment
direction_needed = 1 if cutoff > current_price else -1
momentum_direction = 1 if mga > 0 else -1
momentum_adj = 0.10 * min(abs(mga), 2) / 2 * (1 if momentum_direction == direction_needed else -1)
# Mean reversion adjustment (reverts toward middle)
if current_price > cutoff and mrp_score > 0.5:
reversion_adj = -0.05 * mrp_score # Pressure to come down
elif current_price < cutoff and mrp_score > 0.5:
reversion_adj = 0.05 * mrp_score # Pressure to go up
else:
reversion_adj = 0
# Final probability
final_prob = np.clip(tap + momentum_adj + reversion_adj + vrc_adj, 0.02, 0.98)
# === Edge Calculation ===
kalshi_implied = kalshi_yes_price
edge = final_prob - kalshi_implied
# === Signal Agreement ===
signals_agree = 0
total_signals = 3
if (final_prob > 0.5 and mga > 0) or (final_prob < 0.5 and mga < 0):
signals_agree += 1
if (final_prob > 0.5 and reversion_adj >= 0) or (final_prob < 0.5 and reversion_adj <= 0):
signals_agree += 1
if vrc != "HIGH": # High vol reduces confidence
signals_agree += 1
confidence = signals_agree / total_signals
# === Trading Decision ===
min_edge = {"LOW": 0.08, "NORMAL": 0.10, "HIGH": 0.15}[vrc]
should_trade = (
4 <= time_remaining_min <= 10 and
abs(edge) >= min_edge and
confidence >= 0.5
)
if should_trade:
direction = "YES" if edge > 0 else "NO"
else:
direction = "SKIP"
return SignalResult(
probability_above=final_prob,
edge=edge,
confidence=confidence,
should_trade=should_trade,
direction=direction,
signals={
'tap': tap,
'mga': mga,
'mrp': mrp_score,
'vrc': vrc,
'rsi_5': rsi_5,
'move_from_open': move_from_open,
'z_score': z,
}
)
A.2 Alert Format
def format_alert(signal_result: SignalResult, window_info: dict) -> str:
"""Format signal result as Slack alert."""
if not signal_result.should_trade:
return None
emoji = "🟢" if signal_result.direction == "YES" else "🔴"
edge_pct = abs(signal_result.edge) * 100
return f"""
{emoji} **BUY {signal_result.direction}** on {window_info['ticker']}
📊 **Edge:** {edge_pct:.1f}% ({signal_result.probability_above:.0%} vs {window_info['kalshi_yes']:.0%} implied)
⏱️ **Time Left:** {window_info['time_remaining']} min
📈 **Distance:** {window_info['distance_pct']:.2f}% {'above' if window_info['distance_pct'] > 0 else 'below'} cutoff
🎯 **Confidence:** {signal_result.confidence:.0%}
**Signals:**
• TAP: {signal_result.signals['tap']:.1%}
• Momentum: {'✅ Aligned' if signal_result.signals['mga'] > 0 else '⚠️ Against'}
• Vol Regime: {signal_result.signals['vrc']}
"""
Appendix B: Quick Reference Card
Entry Checklist
- Minute 4-10 of window
- Edge ≥ 10% (15% in high vol)
- Signal agreement ≥ 50%
- Position size per Half-Kelly
Exit Rules
- Hold until settlement (no early exit for binary outcomes)
Risk Limits
- Max 5 trades per hour
- Max 2% of bankroll per trade
- Stop trading if down 10% in a session
Expected Performance
- Win rate: 54-58%
- Average edge captured: 8-12%
- Profit factor: 1.2-1.5
Document created: February 5, 2026 Author: Research Agent Status: Ready for implementation