projects

Optimizing 15-Minute BTC Trading Signals for Kalshi

February 4, 2026
Updated Mar 21, 2026
tradingkalshicryptosignalsquantitativeresearch

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

  1. Executive Summary
  2. Current Signal Analysis
  3. Academic & Quantitative Research
  4. Cutoff-Relative Signal Design
  5. Historical Move Distribution
  6. Backtesting Framework
  7. Recommended Signal Redesign
  8. Implementation Plan

1. Executive Summary

The Core Problem

The current 9 generic directional signals (RSI, MACD, momentum, etc.) answer the wrong question:

Current Signals AskThe 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

  1. Distance to cutoff is the primary variable — All other signals are modifiers
  2. Time decay matters — 10 minutes left vs 3 minutes left changes everything
  3. BTC 15-minute moves are bounded — 99% of 15-minute moves are under 2%
  4. Momentum at short timeframes shows weak persistence — Academic research shows ~52-55% continuation
  5. 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:

#SignalTypeTypical Use
1RSI(14)Mean ReversionOverbought/oversold
2MACD HistogramMomentumTrend strength
3MACD Signal CrossMomentumDirection change
4Price vs EMA(20)TrendTrend alignment
5Price vs EMA(50)TrendLonger trend
6Bollinger Band PositionVolatilityOverextension
7Volume TrendConfirmationMove strength
8Recent Candle PatternPatternReversal/continuation
9ATR-based VolatilityRegimeVolatility state

2.2 Signal Relevance for 15-Minute Prediction

Quantitative assessment based on research:

Signal15-min Predictive PowerIssuesRecommendation
RSI(14)⭐⭐ (55-57%)14-period RSI on 1m data = 14 minutes lookback, okayMODIFY: Use RSI(5) for faster response
MACD⭐ (51-53%)Standard MACD (12,26,9) is too slow for 15-minDROP: Too lagging
EMA(20)⭐⭐ (53-55%)20-min EMA is relevant timeframeKEEP: As trend context
EMA(50)⭐ (50-52%)50-min EMA is too slowDROP: Not relevant
Bollinger Bands⭐⭐⭐ (58-62%)Excellent for mean reversion detectionKEEP: Core signal
Volume⭐⭐ (54-56%)Only useful if comparing to rolling averageMODIFY: Use relative volume
Candle Patterns⭐ (51-53%)Single candles on 1-min are noiseDROP: Replace with price action
ATRContext onlyNot directionalKEEP: 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:

  1. When new information arrives (first 1-3 minutes after price moves)
  2. During high volatility (market makers widen spreads, creating opportunities)
  3. At off-hours (fewer sophisticated traders)

Markets are more efficient:

  1. Near settlement (arbitrageurs converge prices)
  2. During stable periods (no new information)
  3. 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:

ScenarioDistanceTime LeftTAP
1% above cutoff+1.0%10 min91%
1% above cutoff+1.0%3 min97%
0.2% above cutoff+0.2%10 min62%
0.2% above cutoff+0.2%3 min68%
0.5% below cutoff-0.5%10 min24%
0.5% below cutoff-0.5%3 min11%

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):

PercentileMove SizeInterpretation
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
50th0.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 Cutoff15 min left10 min left5 min left2 min left
+2.0% above99.5%99.7%99.9%~100%
+1.0% above95%97%99%99.5%
+0.5% above82%86%92%97%
+0.2% above65%68%75%85%
AT cutoff50%50%50%50%
-0.2% below35%32%25%15%
-0.5% below18%14%8%3%
-1.0% below5%3%1%0.5%
-2.0% below0.5%0.3%0.1%~0%

5.3 Volatility Regime Adjustments

Multiply the distance by these factors:

Volatility RegimeAdjustment 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 SourcePurposeGranularity
Binance BTC/USDTSpot price history1-minute OHLCV
Kalshi APIHistorical marketsSettlement times, cutoffs
Kalshi Order BookHistorical pricingYES/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

MetricTargetDescription
Accuracy>52%% of correct predictions
Edge Accuracy>55%Accuracy when edge >10%
Realized Edge>8%Avg (our_prob - implied) for wins
Calibration<5% errorWhen we say 60%, it hits 57-63%
Profit Factor>1.3Gross wins / Gross losses
Sharpe Ratio>1.5Risk-adjusted returns
Win Rate by MinuteVariesBest entry minute
Max Drawdown<20%Worst peak-to-trough

6.4 Statistical Significance Requirements

Minimum sample sizes:

Confidence LevelRequired TradesAt 55% Win Rate
90%270Statistically significant
95%385Strong significance
99%660Very strong significance

Rule of thumb: Need 300+ trades per strategy variant to draw conclusions.

6.5 Avoiding Overfitting

  1. Out-of-sample testing: Train on months 1-4, test on months 5-6
  2. Walk-forward analysis: Rolling 3-month train, 1-month test
  3. Parameter robustness: Test ±20% on all thresholds
  4. Regime robustness: Test in high-vol and low-vol periods separately
  5. Cross-validation: 5-fold CV on historical windows

7. Recommended Signal Redesign

7.1 Signals to DROP

SignalReason
MACDToo lagging for 15-minute windows
EMA(50)50-minute trend irrelevant for 15-min outcome
Single candle patternsPure noise at 1-minute timeframe

7.2 Signals to MODIFY

OriginalModified VersionRationale
RSI(14)RSI(5)Faster response for short-term
Volume absoluteVolume relative (vs 20-period avg)Normalized for comparability
ATRATR percentile (vs 100-period)Volatility regime context

7.3 NEW Signals to Add

SignalWeightDescription
Normalized Distance (ND)Core(Price - Cutoff) / (ATR × √time)
Time-Adjusted Probability (TAP)CoreStatistical probability estimate
Momentum Gap Analysis (MGA)15%Can momentum close the gap?
Mean Reversion Pressure (MRP)15%Is the move overextended?
Volatility Regime (VRC)ModifierAdjusts 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

SignalTypeWeight/Role
Normalized DistanceInputCore input to TAP
Time RemainingInputCore input to TAP
Volatility (ATR)InputCore input to TAP
TAP (probability)OutputBase probability
MGA (momentum)Modifier±10% adjustment
MRP (reversion)Modifier±8% adjustment
VRC (vol regime)ModifierScales uncertainty
RSI(5)FilterExtreme filter only
Bollinger PositionFilterExtreme filter only

8. Implementation Plan

Phase 1: Data Pipeline (Week 1)

  1. Set up Binance data ingestion

    • 1-minute OHLCV for BTC/USDT
    • Real-time websocket feed
    • Historical data backfill (6+ months)
  2. Set up Kalshi data ingestion

    • 15-minute market tickers
    • Order book snapshots
    • Settlement outcomes

Phase 2: Signal Implementation (Week 2)

  1. Implement new signals

    • calculate_TAP() — Time-adjusted probability
    • calculate_MGA() — Momentum gap analysis
    • calculate_MRP() — Mean reversion pressure
    • calculate_VRC() — Volatility regime context
  2. Build scoring engine

    • Combine signals into final probability
    • Edge calculation
    • Entry decision logic

Phase 3: Backtesting (Week 3)

  1. Run historical backtests

    • 6 months of data
    • Walk-forward validation
    • Parameter sensitivity analysis
  2. Calibration tuning

    • Adjust signal weights based on results
    • Refine entry thresholds
    • Test across volatility regimes

Phase 4: Paper Trading (Week 4+)

  1. Forward testing

    • Run signals in real-time without executing
    • Compare predictions to outcomes
    • Track calibration and edge
  2. Iterate and refine

    • Identify failure modes
    • Add edge cases
    • Tune position sizing

Phase 5: Live Trading (After Validation)

  1. Start small

    • Minimum position sizes ($10-20 per trade)
    • Limit to 3-5 trades per day maximum
    • Track all metrics
  2. 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