"""
KALSHIBOT ITERATION ENGINE v6 — TIMESTAMP-ISOLATED LEARNING
============================================================
Major improvements over v5:

1. TIMESTAMP ISOLATION: Each experiment evaluated using ONLY its own trades.
   No more cross-contamination between experiments.

2. EV-BASED EVALUATION: Expected Value per trade is the true profitability
   metric. EV = WR * avg_win + (1-WR) * avg_loss. Goal: EV > 0.

3. INCONCLUSIVE HANDLING: < 5 trades = retry, not a wasted slot.

4. DEAD MARKET RECOVERY: 0 trades = blast all thresholds open next cycle.

5. PENNY BETS ENFORCED: All experiments use penny bets. Scale up after
   finding a profitable config.

6. BET SIZING REMOVED FROM QUEUE: Doesn't affect win/loss outcome.
   Those slots now test signal combos, phases, stop/target R:R.

7. EXPERIMENT STATE FILE: Writes experiment_state.json after each restart
   so smart_loop.py and the next iteration know the exact trade baseline.
"""
import json, os, re, subprocess, sys, time, datetime
from collections import defaultdict

# ── Paths ──────────────────────────────────────────────────────────────────────
TRACKER_DIR           = r"C:\kalshibot\Automated\tracker"
BOT_SCRIPT            = r"C:\kalshibot\Automated\paper_trade.py"
ANALYTICS_PATH        = r"C:\kalshibot\Automated\trade_analytics.json"
BOT_STATE_PATH        = r"C:\kalshibot\Automated\bot_state.json"
ITER_LOG_PATH         = r"C:\kalshibot\Automated\tracker\iteration_history.json"
LEARNING_PATH         = r"C:\kalshibot\Automated\tracker\learning.json"
EXP_LOG_PATH          = r"C:\kalshibot\Automated\tracker\EXPERIMENT_LOG.txt"
CHANGES_LOG           = r"C:\kalshibot\Automated\tracker\CHANGES_LOG.txt"
PID_FILE              = r"C:\kalshibot\Automated\bot.pid"
EXPERIMENT_STATE_PATH = r"C:\kalshibot\Automated\tracker\experiment_state.json"

# ── Config ─────────────────────────────────────────────────────────────────────
MIN_EXPERIMENT_TRADES = 5
EV_WIN_THRESHOLD      = 0.005
EV_LOSE_THRESHOLD     = -0.010

# ── Parameter ranges ───────────────────────────────────────────────────────────
PARAM_RANGES = {
    "SCORE_HIGH":          (8,    70),
    "SCORE_MEDIUM":        (4,    60),
    "BET_HIGH_MIN":        (0.01, 250.0),
    "BET_HIGH_MAX":        (0.01, 500.0),
    "BET_MED_MIN":         (0.01, 150.0),
    "BET_MED_MAX":         (0.01, 300.0),
    "MAX_POSITIONS":       (1,    100),
    "ENTRY_COOLDOWN":      (1,    1800),
    "MOMENTUM_SECONDS":    (15,   300),
    "MOMENTUM_THRESHOLD":  (0.00005, 0.020),
    "LAG_WINDOW_SECS":     (8,    120),
    "LAG_MIN_BTC_MOVE":    (0.00005, 0.020),
    "MIN_MISPRICING":      (0.002,   0.060),
    "MAX_ENTRY_PRICE":     (0.50,    0.99),
    "MIN_ENTRY_PRICE":     (0.01,    0.60),
    "PROFIT_TARGET":       (0.05,    0.90),
    "TRAILING_STOP_PCT":   (0.03,    0.60),
    "STOP_LOSS":           (0.10,    0.90),
}
SIMPLE_PARAMS = set(PARAM_RANGES.keys())

# ── Baseline config ────────────────────────────────────────────────────────────
BASELINE_CONFIG = {
    "SCORE_MEDIUM": 18, "SCORE_HIGH": 38,
    "BET_HIGH_MIN": 0.01, "BET_HIGH_MAX": 0.10,
    "BET_MED_MIN": 0.01, "BET_MED_MAX": 0.05,
    "MAX_POSITIONS": 5, "ENTRY_COOLDOWN": 10,
    "MOMENTUM_THRESHOLD": 0.0005, "MOMENTUM_SECONDS": 60,
    "LAG_WINDOW_SECS": 30, "LAG_MIN_BTC_MOVE": 0.0006,
    "STOP_LOSS": 0.40, "PROFIT_TARGET": 0.35,
    "MIN_MISPRICING": 0.010, "MAX_ENTRY_PRICE": 0.88,
    "MIN_ENTRY_PRICE": 0.35, "TRAILING_STOP_PCT": 0.20,
    "BLOCK_EARLY": False, "BLOCK_MID": False, "BLOCK_LATE": True,
}

# ── Hypothesis queue ────────────────────────────────────────────────────────────
# Ordered by expected impact on EV. Bet sizing experiments removed —
# bet size does not affect win/loss outcome. Penny bets enforced on all.
HYPOTHESIS_QUEUE = [
    # ═══ STOP LOSS / PROFIT TARGET — directly drive EV ═══
    ("stop_tight",        {"STOP_LOSS": 0.25},
                          "Tight stop (25%) — cut losses fast, lower breakeven WR"),
    ("stop_loose",        {"STOP_LOSS": 0.60},
                          "Loose stop (60%) — more breathing room per trade"),
    ("ideal_rr",          {"STOP_LOSS": 0.30, "PROFIT_TARGET": 0.60},
                          "2:1 R/R — 30% stop + 60% target, breakeven WR ~33%"),
    ("stop_plus_target",  {"STOP_LOSS": 0.35, "PROFIT_TARGET": 0.50},
                          "1.43:1 R/R — 35% stop + 50% target, breakeven WR ~41%"),
    ("rr_tight_stop",     {"TRAILING_STOP_PCT": 0.08},
                          "Tight trailing stop (8%) — cut per-trade losses fast"),
    ("rr_high_profit",    {"PROFIT_TARGET": 0.75},
                          "High profit target (75%) — let winners run further"),
    ("rr_combined_rr",    {"TRAILING_STOP_PCT": 0.08, "PROFIT_TARGET": 0.75},
                          "Combined — tight trailing stop + high profit target"),
    ("rr_cheap_entry",    {"MIN_ENTRY_PRICE": 0.05, "MAX_ENTRY_PRICE": 0.55},
                          "Only cheap contracts (<=0.55) — more upside room"),
    ("tight_entry_window",{"MAX_ENTRY_PRICE": 0.60, "MIN_ENTRY_PRICE": 0.08, "MIN_MISPRICING": 0.015},
                          "Narrow entry band — avoid extreme R/R contracts"),

    # ═══ SIGNAL COMBINATIONS ═══
    ("exp_only",          {"MOMENTUM_THRESHOLD": 0.015, "LAG_MIN_BTC_MOVE": 0.015, "MIN_MISPRICING": 0.012},
                          "EXP-only mode — disable MOM and LAG signals"),
    ("mom_only",          {"MOMENTUM_THRESHOLD": 0.0002, "MOMENTUM_SECONDS": 35,
                           "LAG_MIN_BTC_MOVE": 0.015, "MIN_MISPRICING": 0.050},
                          "MOM-only mode — disable EXP and LAG signals"),
    ("lag_only",          {"LAG_WINDOW_SECS": 15, "LAG_MIN_BTC_MOVE": 0.0002,
                           "MOMENTUM_THRESHOLD": 0.015, "MIN_MISPRICING": 0.050},
                          "LAG-only mode — disable EXP and MOM signals"),
    ("exp_lag",           {"MOMENTUM_THRESHOLD": 0.015, "LAG_MIN_BTC_MOVE": 0.0004, "MIN_MISPRICING": 0.012},
                          "EXP+LAG mode — disable MOM only"),
    ("exp_mom",           {"LAG_MIN_BTC_MOVE": 0.015, "MOMENTUM_THRESHOLD": 0.0003, "MIN_MISPRICING": 0.010},
                          "EXP+MOM mode — disable LAG only"),
    ("exp_mom_focused",   {"LAG_MIN_BTC_MOVE": 0.015, "MOMENTUM_THRESHOLD": 0.0002, "SCORE_MEDIUM": 20},
                          "EXP+MOM focused — data shows 78% WR on this combo"),
    ("require_two_signals",{"SCORE_MEDIUM": 24, "SCORE_HIGH": 42},
                          "Force 2+ signals — EXP alone historically 14% WR"),

    # ═══ PHASE FILTERING ═══
    ("block_early",       {"BLOCK_EARLY": True, "BLOCK_MID": False},
                          "Block early phase (>8 min left)"),
    ("block_early_mid",   {"BLOCK_EARLY": True, "BLOCK_MID": True},
                          "Only near+late phase (<4 min left)"),
    ("mid_near_only",     {"BLOCK_EARLY": True, "BLOCK_MID": False, "BLOCK_LATE": True},
                          "Mid+near only (2-8 min) — skip noisy early AND late"),
    ("near_only",         {"BLOCK_EARLY": True, "BLOCK_MID": True, "BLOCK_LATE": False, "SCORE_MEDIUM": 15},
                          "Near phase only (2-4 min) — highest certainty window"),
    ("all_phases",        {"BLOCK_EARLY": False, "BLOCK_MID": False, "BLOCK_LATE": False},
                          "All phases open — test no blocking at all"),
    ("late_only",         {"BLOCK_EARLY": True, "BLOCK_MID": True, "BLOCK_LATE": False,
                           "SCORE_MEDIUM": 10, "ENTRY_COOLDOWN": 3},
                          "Late phase only — final minutes, highest time certainty"),

    # ═══ SCORE THRESHOLDS ═══
    ("score_med_6",       {"SCORE_MEDIUM": 6},    "Very low score bar — maximum trade volume"),
    ("score_med_8",       {"SCORE_MEDIUM": 8},    "Low score bar — high frequency"),
    ("score_med_15",      {"SCORE_MEDIUM": 15},   "Medium score bar"),
    ("score_med_20",      {"SCORE_MEDIUM": 20},   "Raised score bar — quality focus"),
    ("score_med_28",      {"SCORE_MEDIUM": 28},   "High score bar — very selective"),
    ("score_high_45",     {"SCORE_HIGH": 45, "SCORE_MEDIUM": 40},
                          "Need all 3 signals (score 40+ requires EXP+LAG+MOM)"),
    ("rr_selective",      {"SCORE_MEDIUM": 22, "MIN_MISPRICING": 0.020},
                          "Higher entry bar — only trade cleaner signals"),

    # ═══ MOMENTUM WINDOWS ═══
    ("mom_fast",          {"MOMENTUM_SECONDS": 25, "MOMENTUM_THRESHOLD": 0.0003},
                          "Fast momentum (25s window)"),
    ("mom_slow",          {"MOMENTUM_SECONDS": 120, "MOMENTUM_THRESHOLD": 0.0008},
                          "Slow momentum (120s window)"),

    # ═══ LAG WINDOWS ═══
    ("lag_tight",         {"LAG_WINDOW_SECS": 15, "LAG_MIN_BTC_MOVE": 0.0003},
                          "Tight LAG window (15s)"),
    ("lag_wide",          {"LAG_WINDOW_SECS": 70, "LAG_MIN_BTC_MOVE": 0.0008},
                          "Wide LAG window (70s)"),

    # ═══ MISPRICING ═══
    ("misprice_low",      {"MIN_MISPRICING": 0.005},
                          "Very low mispricing bar — catch small edges"),
    ("misprice_high",     {"MIN_MISPRICING": 0.025},
                          "High mispricing bar — only trade large edges"),

    # ═══ POSITION CONCENTRATION ═══
    ("positions_1",       {"MAX_POSITIONS": 1, "ENTRY_COOLDOWN": 60},
                          "Single position — total concentration per contract"),
    ("positions_10",      {"MAX_POSITIONS": 10, "ENTRY_COOLDOWN": 5},
                          "10 positions — moderate spread"),
    ("positions_50",      {"MAX_POSITIONS": 50, "ENTRY_COOLDOWN": 1},
                          "50 positions — maximum spread, data flood"),

    # ═══ DATA-DRIVEN COMBOS ═══
    ("exp_mom_big_bets",  {"LAG_MIN_BTC_MOVE": 0.015, "MOMENTUM_THRESHOLD": 0.0002,
                           "BET_HIGH_MAX": 8.0, "BET_MED_MAX": 4.0},
                          "EXP+MOM + bigger bets — high conviction on best signal"),
]

# ── Helpers ────────────────────────────────────────────────────────────────────
def load(path, default):
    try:
        with open(path, encoding="utf-8") as f:
            return json.load(f)
    except:
        return default

def save(path, data):
    with open(path, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2, default=str)

def log(msg):
    ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    line = f"[{ts}] {msg}"
    print(line, flush=True)
    try:
        with open(os.path.join(TRACKER_DIR, "iterate_run.log"), "a", encoding="utf-8") as f:
            f.write(line + "\n")
    except:
        pass

def pct(n, d): return round(n / d * 100, 1) if d > 0 else 0.0
def clamp(val, lo, hi): return max(lo, min(hi, val))

def read_param(content, name, default):
    m = re.search(rf'^{re.escape(name)}\s*=\s*([0-9.]+)', content, re.MULTILINE)
    return float(m.group(1)) if m else default

def get_current_params():
    with open(BOT_SCRIPT, encoding="utf-8") as f:
        content = f.read()
    params = {p: read_param(content, p, 0) for p in SIMPLE_PARAMS}
    params["BLOCK_EARLY"] = "if   mins_left > 8.0: time_conf = 0.00" in content
    params["BLOCK_MID"]   = "elif mins_left > 4.0: time_conf = 0.00" in content
    params["BLOCK_LATE"]  = "else:                 time_conf = 0.00" in content
    return params

# ── Overall trade analysis ─────────────────────────────────────────────────────
def analyze():
    analytics = load(ANALYTICS_PATH, [])
    exits = [t for t in analytics if t.get("event") == "exit"]

    r = {"total_exits": len(exits), "total_wins": 0, "total_losses": 0,
         "win_rate": 0.0, "total_pnl": 0.0, "avg_pnl": 0.0,
         "avg_win_pnl": 0.0, "avg_loss_pnl": 0.0,
         "ev_per_trade": 0.0, "breakeven_wr": 50.0,
         "recent_wr": 0.0, "recent_n": 0,
         "combos": {}, "phases": {}, "scores": {}}

    if not exits:
        return r

    wins   = [t for t in exits if t.get("outcome") == "WIN"]
    losses = [t for t in exits if t.get("outcome") == "LOSS"]
    pnls   = [t.get("pnl", 0) for t in exits]

    r["total_wins"]   = len(wins)
    r["total_losses"] = len(losses)
    r["win_rate"]     = pct(len(wins), len(exits))
    r["total_pnl"]    = round(sum(pnls), 4)
    r["avg_pnl"]      = round(sum(pnls)/len(pnls), 4) if pnls else 0
    r["avg_win_pnl"]  = round(sum(t.get("pnl",0) for t in wins)/len(wins), 4) if wins else 0
    r["avg_loss_pnl"] = round(sum(t.get("pnl",0) for t in losses)/len(losses), 4) if losses else 0

    wr_frac = r["win_rate"] / 100.0
    avg_w   = r["avg_win_pnl"]
    avg_l   = r["avg_loss_pnl"]
    r["ev_per_trade"] = round(wr_frac * avg_w + (1.0 - wr_frac) * avg_l, 4)
    if avg_w > 0 and avg_l < 0:
        r["breakeven_wr"] = round(abs(avg_l) / (avg_w + abs(avg_l)) * 100, 1)
    else:
        r["breakeven_wr"] = 50.0

    recent = exits[-10:]
    r["recent_n"]  = len(recent)
    r["recent_wr"] = pct(sum(1 for t in recent if t.get("outcome") == "WIN"), len(recent))

    combo_map = defaultdict(lambda: {"wins": 0, "total": 0, "pnl": 0.0})
    for t in exits:
        parts = (["EXP"] if t.get("exp_score",0)>0 else []) + \
                (["LAG"] if t.get("lag_score",0)>0 else []) + \
                (["MOM"] if t.get("mom_score",0)>0 else [])
        combo = "+".join(parts) or "NONE"
        combo_map[combo]["total"] += 1
        combo_map[combo]["pnl"]   += t.get("pnl", 0)
        if t.get("outcome") == "WIN": combo_map[combo]["wins"] += 1
    r["combos"] = {k: dict(v) for k, v in combo_map.items()}

    phase_map = defaultdict(lambda: {"wins": 0, "total": 0, "pnl": 0.0})
    for t in exits:
        secs  = t.get("secs_left", 300)
        phase = "early" if secs>480 else "mid" if secs>240 else "near" if secs>90 else "late"
        phase_map[phase]["total"] += 1
        phase_map[phase]["pnl"]   += t.get("pnl", 0)
        if t.get("outcome") == "WIN": phase_map[phase]["wins"] += 1
    r["phases"] = {k: dict(v) for k, v in phase_map.items()}

    score_map = defaultdict(lambda: {"wins": 0, "total": 0, "pnl": 0.0})
    for t in exits:
        sc = t.get("confidence_score", 0)
        b  = "55+" if sc>=55 else "40-54" if sc>=40 else "25-39" if sc>=25 else "18-24" if sc>=18 else "under-18"
        score_map[b]["total"] += 1
        score_map[b]["pnl"]   += t.get("pnl", 0)
        if t.get("outcome") == "WIN": score_map[b]["wins"] += 1
    r["scores"] = {k: dict(v) for k, v in score_map.items()}

    return r

# ── Isolated experiment analysis ───────────────────────────────────────────────
def analyze_isolated(start_time, start_count):
    """
    Analyze ONLY trades from the current experiment window.
    Uses timestamp as primary filter, count-based slice as fallback.
    Eliminates cross-contamination between experiments.
    """
    analytics = load(ANALYTICS_PATH, [])
    all_exits = [t for t in analytics if t.get("event") == "exit"]

    exp_exits = []
    if start_time > 0:
        exp_exits = [t for t in all_exits if t.get("entry_time", 0) >= start_time]

    if not exp_exits and start_count < len(all_exits):
        exp_exits = all_exits[start_count:]

    n = len(exp_exits)
    if n == 0:
        return {"n": 0, "wr": 0.0, "ev": 0.0, "avg_win": 0.0,
                "avg_loss": 0.0, "total_pnl": 0.0, "breakeven_wr": 50.0}

    wins   = [t for t in exp_exits if t.get("outcome") == "WIN"]
    losses = [t for t in exp_exits if t.get("outcome") == "LOSS"]
    pnls   = [t.get("pnl", 0) for t in exp_exits]

    wr       = round(len(wins) / n * 100, 1)
    avg_win  = round(sum(t.get("pnl",0) for t in wins)   / len(wins),   4) if wins   else 0.0
    avg_loss = round(sum(t.get("pnl",0) for t in losses) / len(losses), 4) if losses else 0.0
    wr_frac  = wr / 100
    ev       = round(wr_frac * avg_win + (1 - wr_frac) * avg_loss, 4)
    bkeven   = round(abs(avg_loss) / (avg_win + abs(avg_loss)) * 100, 1) \
               if avg_win > 0 and avg_loss < 0 else 50.0

    return {"n": n, "wr": wr, "ev": ev, "avg_win": avg_win,
            "avg_loss": avg_loss, "total_pnl": round(sum(pnls), 4), "breakeven_wr": bkeven}


# ── Hot streak trade signature analysis ───────────────────────────────────────
def analyze_hot_streak_trades(baseline_count, n=5):
    """
    Extract characteristics of the most recent N winning trades from an experiment.
    Returns a signature dict stored in learning.json as 'hot_streak_signature'.
    Identifies WHAT conditions produce winning streaks so future experiments can
    bias toward replicating them (hour, scores, price range, momentum, phase, etc.)
    """
    analytics = load(ANALYTICS_PATH, [])
    exits     = [t for t in analytics if t.get("event") == "exit"]
    exp_exits = exits[baseline_count:]

    # Most recent N wins from this experiment
    wins   = [t for t in exp_exits if t.get("outcome") == "WIN"]
    streak = wins[-n:] if len(wins) >= n else wins

    if not streak:
        return {}

    hours        = [t.get("hour",              -1) for t in streak]
    scores       = [t.get("confidence_score",   0) for t in streak]
    exp_scores   = [t.get("exp_score",          0) for t in streak]
    lag_scores   = [t.get("lag_score",          0) for t in streak]
    mom_scores   = [t.get("mom_score",          0) for t in streak]
    entry_prices = [t.get("entry_price",        0) for t in streak]
    secs_left    = [t.get("secs_left",          0) for t in streak]
    momentums    = [t.get("momentum_pct",       0) for t in streak]
    mom_strs     = [t.get("momentum_strength",  0) for t in streak]
    btc_vols     = [t.get("btc_volatility",     0) for t in streak]
    pnls         = [t.get("pnl",                0) for t in streak]

    direction_dist, phase_dist, tier_dist = {}, {}, {}
    for t in streak:
        d  = t.get("direction", "?")
        ph = t.get("phase",     "?")
        ti = t.get("tier",      "?")
        direction_dist[d]  = direction_dist.get(d,  0) + 1
        phase_dist[ph]     = phase_dist.get(ph,     0) + 1
        tier_dist[ti]      = tier_dist.get(ti,      0) + 1

    def avg(lst): return round(sum(lst) / len(lst), 4) if lst else 0
    def rng(lst): return [round(min(lst), 4), round(max(lst), 4)] if lst else [0, 0]

    return {
        "captured_at":        str(datetime.datetime.now()),
        "streak_size":        len(streak),
        "total_pnl":          round(sum(pnls), 4),
        # Timing
        "avg_hour":           avg(hours),
        "hour_range":         rng(hours),
        # Signal scores
        "avg_confidence":     avg(scores),
        "score_range":        rng(scores),
        "avg_exp_score":      avg(exp_scores),
        "avg_lag_score":      avg(lag_scores),
        "avg_mom_score":      avg(mom_scores),
        # Entry conditions
        "avg_entry_price":    avg(entry_prices),
        "entry_price_range":  rng(entry_prices),
        "avg_secs_left":      avg(secs_left),
        "secs_left_range":    rng(secs_left),
        # BTC conditions
        "avg_momentum_pct":   avg(momentums),
        "avg_mom_strength":   avg(mom_strs),
        "avg_btc_volatility": avg(btc_vols),
        # Distributions
        "direction_dist":     direction_dist,
        "phase_dist":         phase_dist,
        "tier_dist":          tier_dist,
        # PnL
        "avg_pnl":            avg(pnls),
        "min_pnl":            round(min(pnls), 4) if pnls else 0,
        "max_pnl":            round(max(pnls), 4) if pnls else 0,
    }
# ── Learning database ──────────────────────────────────────────────────────────
def init_learning():
    return {
        "champion_params":        dict(BASELINE_CONFIG),
        "champion_wr":            0.0,
        "champion_recent_wr":     0.0,
        "champion_ev":            -999.0,
        "proven_winners":         [],
        "proven_losers":          [],
        "tested_labels":          [],
        "inconclusive_labels":    [],
        "last_recent_wr":         0.0,
        "last_ev":                -999.0,
        "last_n_trades":          0,
        "iterations_no_improvement": 0,
        "dead_market_count":      0,
    }

# ── Evaluate last experiment ───────────────────────────────────────────────────
def update_learning(learning, last_iter, isolated, overall_analysis):
    """
    Evaluate last experiment using only its timestamp-isolated trades.
    Returns (updated_learning, status): "OK", "INCONCLUSIVE", or "DEAD_MARKET"
    """
    last_label = last_iter.get("hypothesis_label", "")
    n          = isolated["n"]

    if n == 0:
        log(f"DEAD MARKET: Zero trades in '{last_label}' — will retry")
        learning["dead_market_count"] = learning.get("dead_market_count", 0) + 1
        if last_label in learning["tested_labels"]:
            learning["tested_labels"].remove(last_label)
        return learning, "DEAD_MARKET"

    learning["dead_market_count"] = 0

    if n < MIN_EXPERIMENT_TRADES:
        log(f"INCONCLUSIVE: Only {n} trades in '{last_label}' (need {MIN_EXPERIMENT_TRADES}) — retrying")
        if last_label not in learning.get("inconclusive_labels", []):
            learning.setdefault("inconclusive_labels", []).append(last_label)
        if last_label in learning["tested_labels"]:
            learning["tested_labels"].remove(last_label)
        return learning, "INCONCLUSIVE"

    if last_label in learning.get("inconclusive_labels", []):
        learning["inconclusive_labels"].remove(last_label)

    champ_ev = learning.get("champion_ev", -999.0)
    exp_ev   = isolated["ev"]
    exp_wr   = isolated["wr"]

    log(f"Experiment '{last_label}': {n} trades | WR={exp_wr}% | EV=${exp_ev:+.4f}")
    log(f"  Champion EV: ${champ_ev:+.4f} | Delta: ${exp_ev - champ_ev:+.4f}")

    # ── 100% WIN RATE: Separate elite category regardless of experiment length ────
    if exp_wr == 100.0 and n >= MIN_EXPERIMENT_TRADES:
        perfects = learning.setdefault("perfect_experiments", [])
        existing = next((p for p in perfects if p["label"] == last_label), None)
        if existing:
            existing["runs"]      = existing.get("runs", 1) + 1
            existing["best_ev"]   = max(existing.get("best_ev", exp_ev), exp_ev)
            existing["last_seen"] = str(datetime.datetime.now())
        else:
            perfects.append({
                "label":     last_label,
                "n":         n,
                "wr":        exp_wr,
                "ev":        exp_ev,
                "runs":      1,
                "best_ev":   exp_ev,
                "last_seen": str(datetime.datetime.now()),
            })
        log(f"  *** PERFECT EXPERIMENT: '{last_label}' 100% WR over {n} trades! EV=${exp_ev:+.4f} ***")

    if exp_ev > champ_ev:
        learning["champion_ev"]        = exp_ev
        learning["champion_wr"]        = exp_wr
        learning["champion_recent_wr"] = exp_wr
        learning["champion_params"]    = get_current_params()
        log(f"  NEW CHAMPION: EV=${exp_ev:+.4f} (was ${champ_ev:+.4f}) — params saved")

    ev_delta     = exp_ev - champ_ev
    winner_entry = next((w for w in learning["proven_winners"] if w["label"] == last_label), None)
    loser_entry  = next((l for l in learning["proven_losers"]  if l["label"] == last_label), None)

    if ev_delta > EV_WIN_THRESHOLD:
        if winner_entry:
            winner_entry["runs"]        += 1
            winner_entry["total_delta"]  = winner_entry.get("total_delta", 0) + ev_delta * 100
            winner_entry["avg_delta"]    = winner_entry["total_delta"] / winner_entry["runs"]
            winner_entry["last_ev_delta"] = ev_delta
        else:
            hyp = next((h for h in HYPOTHESIS_QUEUE if h[0] == last_label), None)
            if hyp:
                learning["proven_winners"].append({
                    "label": last_label, "params": hyp[1], "desc": hyp[2],
                    "runs": 1, "total_delta": ev_delta * 100, "avg_delta": ev_delta * 100,
                    "last_ev_delta": ev_delta, "exp_wr": exp_wr, "exp_ev": exp_ev,
                })
        log(f"  WINNER: '{last_label}' improved EV by +${ev_delta:.4f}/trade")
        learning["iterations_no_improvement"] = 0

    elif ev_delta < EV_LOSE_THRESHOLD:
        if loser_entry:
            loser_entry["runs"]        += 1
            loser_entry["total_delta"]  = loser_entry.get("total_delta", 0) + ev_delta * 100
            loser_entry["avg_delta"]    = loser_entry["total_delta"] / loser_entry["runs"]
        else:
            learning["proven_losers"].append({
                "label": last_label, "runs": 1,
                "total_delta": ev_delta * 100, "avg_delta": ev_delta * 100,
            })
        log(f"  LOSER: '{last_label}' hurt EV by ${ev_delta:.4f}/trade")
        learning["iterations_no_improvement"] = learning.get("iterations_no_improvement", 0) + 1
    else:
        log(f"  NEUTRAL: '{last_label}' EV delta ${ev_delta:+.4f} — not significant")
        learning["iterations_no_improvement"] = learning.get("iterations_no_improvement", 0) + 1

    learning["proven_winners"].sort(key=lambda w: -w.get("avg_delta", 0))
    learning["last_recent_wr"] = overall_analysis["recent_wr"]
    learning["last_ev"]        = overall_analysis["ev_per_trade"]
    learning["last_n_trades"]  = overall_analysis["total_exits"]

    return learning, "OK"

# ── Experiment picker ──────────────────────────────────────────────────────────
def pick_experiment(learning, analysis, iteration_num, last_status):
    n           = analysis["total_exits"]
    winners     = learning["proven_winners"]
    losers      = [l["label"] for l in learning["proven_losers"]]
    tested      = set(learning["tested_labels"])
    stuck_iters = learning.get("iterations_no_improvement", 0)
    dead_count  = learning.get("dead_market_count", 0)

    # ── HOT STREAK: re-run champion params to confirm the edge ──
    if last_status == "HOT_STREAK":
        log("*** HOT STREAK CONFIRM: Re-running champion params to validate win streak ***")
        return ("CONFIRM_CHAMPION", "confirm_champion", {},
                "HOT STREAK CONFIRM — re-running champion params to prove the edge is real")

    if last_status == "DEAD_MARKET" or dead_count >= 2:
        log(f"DEAD MARKET RECOVERY: Opening all thresholds to force market activity")
        return ("MARKET_WAKE", "market_wake",
                {"SCORE_MEDIUM": 4, "MOMENTUM_THRESHOLD": 0.0001,
                 "LAG_MIN_BTC_MOVE": 0.0001, "MIN_MISPRICING": 0.002,
                 "MIN_ENTRY_PRICE": 0.05, "MAX_ENTRY_PRICE": 0.95,
                 "BLOCK_EARLY": False, "BLOCK_MID": False, "BLOCK_LATE": False,
                 "MAX_POSITIONS": 20, "ENTRY_COOLDOWN": 2},
                "Dead market recovery — opening all filters to force trades")

    if n < 8:
        return ("DATA_GATHER", "data_gather",
                {"SCORE_MEDIUM": 6, "MOMENTUM_THRESHOLD": 0.0002,
                 "MIN_MISPRICING": 0.005, "LAG_MIN_BTC_MOVE": 0.0002,
                 "BLOCK_EARLY": False, "BLOCK_MID": False,
                 "MAX_POSITIONS": 20, "ENTRY_COOLDOWN": 2},
                "Gathering baseline data — need minimum trades to start learning")

    if len(winners) >= 2 and iteration_num % 4 == 0:
        combined = {}
        labels   = []
        for w in winners:
            combined.update(w["params"])
            labels.append(w["label"])
        label = "combine_" + "+".join(labels[:3])
        desc  = f"COMBINE: Testing all {len(winners)} proven winners: {', '.join(labels[:4])}"
        log(f"COMBINE run: merging {len(winners)} proven winners")
        return ("COMBINE", label, combined, desc)

    for label, params, desc in HYPOTHESIS_QUEUE:
        if label not in tested and label not in losers:
            log(f"EXPLORE: Testing '{label}'")
            return ("SINGLE_TEST", label, params, f"EXPERIMENT: {desc}")

    if winners:
        top = winners[0]
        for w2 in winners[1:]:
            combo_label = f"combo_{top['label']}+{w2['label']}"
            if combo_label not in tested:
                combined = {}
                combined.update(top["params"])
                combined.update(w2["params"])
                return ("COMBINE2", combo_label, combined,
                        f"COMBINE2: {top['label']} (best) + {w2['label']}")

    if stuck_iters >= 3:
        wild_options = [
            ("wild_flood",    {"MAX_POSITIONS": 50, "ENTRY_COOLDOWN": 1, "SCORE_MEDIUM": 4},
                              "WILD: Flood mode — max positions, max data"),
            ("wild_allthree", {"SCORE_MEDIUM": 50, "MAX_POSITIONS": 3, "ENTRY_COOLDOWN": 10},
                              "WILD: Require all 3 signals (score 50+)"),
            ("wild_late",     {"BLOCK_EARLY": True, "BLOCK_MID": True, "SCORE_MEDIUM": 12,
                               "ENTRY_COOLDOWN": 3},
                              "WILD: Late-only — highest time certainty window"),
        ]
        untried = [(l,p,d) for l,p,d in wild_options if l not in tested]
        if untried:
            label, params, desc = untried[iteration_num % len(untried)]
            log(f"WILD: Stuck {stuck_iters} iters — trying {label}")
            return ("WILD", label, params, desc)

    return ("BASELINE", "baseline_check", {},
            "Champion config unchanged — confirming baseline stability")

# ── Build full param set ───────────────────────────────────────────────────────
def build_param_set(experiment_type, overrides, learning):
    base  = dict(learning["champion_params"])
    final = dict(base)

    if experiment_type in ("SINGLE_TEST", "COMBINE2", "WILD", "RR_FIX", "MARKET_WAKE", "CONFIRM_CHAMPION"):
        for w in learning["proven_winners"]:
            final.update(w["params"])
            log(f"  [proven] Applied winner: {w['label']}")

    final.update(overrides)

    # Enforce penny bets unless this experiment explicitly sets bet sizing
    BET_KEYS = {"BET_HIGH_MIN", "BET_HIGH_MAX", "BET_MED_MIN", "BET_MED_MAX"}
    if not any(k in overrides for k in BET_KEYS):
        final["BET_HIGH_MIN"] = 0.01
        final["BET_HIGH_MAX"] = 0.10
        final["BET_MED_MIN"]  = 0.01
        final["BET_MED_MAX"]  = 0.05
        log("  [safety] Penny bets enforced — minimize risk while learning")

    return final

# ── Apply params to paper_trade.py ────────────────────────────────────────────
def apply_params(final_params, iteration_num, experiment_label):
    with open(BOT_SCRIPT, encoding="utf-8") as f:
        content = f.read()

    applied = []

    for param, value in final_params.items():
        if param in SIMPLE_PARAMS:
            lo, hi = PARAM_RANGES[param]
            if isinstance(value, bool):
                value = float(value)
            is_int = param in ("MAX_POSITIONS","ENTRY_COOLDOWN","MOMENTUM_SECONDS",
                                "LAG_WINDOW_SECS","SCORE_HIGH","SCORE_MEDIUM")
            val_str = str(int(clamp(float(value), lo, hi))) if is_int \
                      else str(round(clamp(float(value), lo, hi), 6))

            pattern = rf'^({re.escape(param)}\s*=\s*)[0-9.]+(.*)$'
            new_content, n = re.subn(pattern, rf'\g<1>{val_str}\2', content, flags=re.MULTILINE)
            if n > 0:
                content = new_content
                applied.append((param, val_str))
                log(f"  SET {param} = {val_str}")
            else:
                log(f"  WARN: {param} not found in bot script")

        elif param == "BLOCK_EARLY":
            prefix  = "if   mins_left > 8.0: time_conf ="
            new_val = "0.00" if value else "0.55"
            pattern = rf'({re.escape(prefix)}\s*)([0-9.]+)'
            new_content, n = re.subn(pattern, rf'\g<1>{new_val}', content)
            if n > 0:
                content = new_content
                applied.append(("BLOCK_EARLY", "YES" if value else "NO"))
                log(f"  EARLY phase: {'BLOCKED' if value else 'OPEN'}")

        elif param == "BLOCK_MID":
            prefix  = "elif mins_left > 4.0: time_conf ="
            new_val = "0.00" if value else "0.80"
            pattern = rf'({re.escape(prefix)}\s*)([0-9.]+)'
            new_content, n = re.subn(pattern, rf'\g<1>{new_val}', content)
            if n > 0:
                content = new_content
                applied.append(("BLOCK_MID", "YES" if value else "NO"))
                log(f"  MID phase: {'BLOCKED' if value else 'OPEN'}")

        elif param == "BLOCK_LATE":
            prefix  = "else:                 time_conf ="
            new_val = "0.00" if value else "1.00"
            pattern = rf'({re.escape(prefix)}\s*)([0-9.]+)'
            new_content, n = re.subn(pattern, rf'\g<1>{new_val}', content)
            if n > 0:
                content = new_content
                applied.append(("BLOCK_LATE", "YES" if value else "NO"))
                log(f"  LATE phase: {'BLOCKED' if value else 'OPEN'}")

    with open(BOT_SCRIPT, "w", encoding="utf-8") as f:
        f.write(content)

    return applied

# ── Kill and restart bot ───────────────────────────────────────────────────────
def restart_bot():
    log("Killing existing bot...")

    if os.path.exists(PID_FILE):
        try:
            with open(PID_FILE) as pf:
                old_pid = int(pf.read().strip())
            result = subprocess.run(["taskkill", "/F", "/PID", str(old_pid)],
                                     capture_output=True, timeout=10)
            log(f"Killed python PID {old_pid}: exit={result.returncode}")
        except Exception as e:
            log(f"PID kill failed: {e}")
        finally:
            try: os.remove(PID_FILE)
            except: pass

    try:
        subprocess.run(
            ["powershell", "-NonInteractive", "-Command",
             "Get-WmiObject Win32_Process | Where-Object { $_.CommandLine -like '*Automated*paper_trade*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }"],
            capture_output=True, timeout=12)
    except Exception as e:
        log(f"WMI cleanup note: {e}")

    time.sleep(2)
    log("Starting new bot...")

    bot_cmd = (
        f'title Kalshibot-Automated && '
        f'cd /d C:\\kalshibot\\Automated && '
        f'python paper_trade.py'
    )
    try:
        subprocess.Popen(['cmd', '/k', bot_cmd],
                         creationflags=subprocess.CREATE_NEW_CONSOLE, close_fds=True)
        log("Bot window launched via Popen")
    except Exception as e:
        log(f"Popen failed ({e}) — falling back to Start-Process")
        ps_cmd = (f'Start-Process cmd -ArgumentList \'/k title Kalshibot-Automated && '
                  f'cd /d C:\\kalshibot\\Automated && python paper_trade.py\' -WindowStyle Normal')
        subprocess.run(["powershell", "-NonInteractive", "-Command", ps_cmd],
                       capture_output=True, timeout=15)
        log("Bot window launched via PowerShell fallback")

    time.sleep(10)
    state = load(BOT_STATE_PATH, {})
    if state.get("btc_price", 0) > 0:
        log(f"Bot OK — BTC=${state['btc_price']}  Balance=${state.get('balance',0)}")
    else:
        log("Bot starting up (state not flushed yet — OK)")
    return True

# ── Save experiment state ──────────────────────────────────────────────────────
def save_experiment_state(label, n_trades_at_start, exp_type):
    """
    Written after each bot restart. Read by smart_loop.py (for baseline trade count)
    and by the next iterate.py run (for isolated evaluation).
    """
    state = {
        "label":             label,
        "experiment_type":   exp_type,
        "start_time":        time.time(),
        "start_trade_count": n_trades_at_start,
        "start_ts":          str(datetime.datetime.now()),
    }
    save(EXPERIMENT_STATE_PATH, state)
    log(f"Experiment state saved: '{label}' | baseline={n_trades_at_start} trades")

# ── Logging ───────────────────────────────────────────────────────────────────
def write_logs(iteration_num, experiment_type, label, desc, analysis, isolated, applied, learning):
    ts      = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    n       = analysis["total_exits"]
    wr      = analysis["win_rate"]
    rec     = analysis["recent_wr"]
    pnl     = analysis["total_pnl"]
    ev      = analysis["ev_per_trade"]
    bk_wr   = analysis["breakeven_wr"]
    winners = learning["proven_winners"]
    iso_n   = isolated.get("n", 0)
    iso_ev  = isolated.get("ev", 0.0)
    iso_wr  = isolated.get("wr", 0.0)

    lines = [
        f"\n{'='*72}",
        f"ITERATION #{iteration_num}  |  {ts}",
        f"  Type: {experiment_type}  |  Label: {label}",
        f"  {desc}",
        f"{'='*72}",
        f"  EXPERIMENT (isolated): {iso_n} trades | WR: {iso_wr}% | EV: ${iso_ev:+.4f}",
        f"  OVERALL: {n} trades | WR: {wr}% | Recent(10): {rec}% | P&L: ${pnl:+.4f}",
        f"  EV/trade: ${ev:+.4f}  |  Breakeven WR needed: {bk_wr}%",
        f"  Proven winners: {len(winners)} — {', '.join(w['label'] for w in winners[:5])}",
        "", "  PARAMS SET THIS RUN:",
    ]
    for p, v in applied:
        lines.append(f"    {p} = {v}")
    lines.append("")
    with open(CHANGES_LOG, "a", encoding="utf-8") as f:
        f.write("\n".join(lines) + "\n")

    ev_status = "PROFITABLE" if ev > 0 else f"NEEDS {bk_wr}% WR TO BREAK EVEN"
    block = f"""
================================================================================
ITERATION #{iteration_num} — {ts}
TYPE: {experiment_type}  |  LABEL: {label}
DESC: {desc}
================================================================================

EXPERIMENT RESULT (isolated {iso_n} trades):
  Win rate      : {iso_wr}%
  EV / trade    : ${iso_ev:+.4f}
  Total P&L     : ${isolated.get('total_pnl', 0):+.4f}
  Avg win       : ${isolated.get('avg_win', 0):+.4f}
  Avg loss      : ${isolated.get('avg_loss', 0):+.4f}

OVERALL DATA:
  Total trades  : {n}
  Win rate      : {wr}%  (breakeven needs {bk_wr}%)
  Recent(10) WR : {rec}%
  Total P&L     : ${pnl:+.4f}
  EV / trade    : ${ev:+.4f}  [{ev_status}]

SIGNAL COMBOS:
"""
    for combo, d in sorted(analysis["combos"].items(), key=lambda x: -x[1]["total"]):
        block += f"  {combo:<20}  {d['total']:>3} trades  WR:{pct(d['wins'],d['total']):>5}%  P&L:${d['pnl']:+.2f}\n"
    block += "\nPHASES:\n"
    for ph in ["early","mid","near","late"]:
        d = analysis["phases"].get(ph, {"wins":0,"total":0,"pnl":0.0})
        if d["total"] > 0:
            block += f"  {ph:<10} {d['total']:>3} trades  WR:{pct(d['wins'],d['total']):>5}%  P&L:${d['pnl']:+.2f}\n"
    block += "\nCURRENT PROVEN WINNERS:\n"
    for w in winners:
        block += f"  {w['label']:<25}  avg_delta={w.get('avg_delta',0):+.2f}  runs={w['runs']}\n"
    block += "\nPARAMS SET:\n"
    for p, v in applied:
        block += f"  {p} = {v}\n"
    block += "\n"

    with open(EXP_LOG_PATH, "a", encoding="utf-8") as f:
        f.write(block)

# ── Main ───────────────────────────────────────────────────────────────────────
def main():
    log("=" * 60)
    log("KALSHIBOT ITERATION ENGINE v6 — TIMESTAMP-ISOLATED LEARNING")
    log("=" * 60)

    history  = load(ITER_LOG_PATH, [])
    learning = load(LEARNING_PATH, None) or init_learning()

    for field, default in [("champion_ev", -999.0), ("last_ev", -999.0),
                            ("dead_market_count", 0), ("inconclusive_labels", []),
                            ("iterations_no_improvement", 0)]:
        if field not in learning:
            learning[field] = default

    iteration_num = len(history) + 1
    log(f"Iteration #{iteration_num} | Champion EV=${learning['champion_ev']:+.4f} | Winners={len(learning['proven_winners'])}")

    exp_state       = load(EXPERIMENT_STATE_PATH, {})
    exp_start_time  = exp_state.get("start_time", 0)
    exp_start_count = exp_state.get("start_trade_count", 0)


    # Check for hot streak flag written by smart_loop
    hot_streak = exp_state.get("hot_streak", False)
    if hot_streak:
        log("*** HOT STREAK FLAG detected — next experiment will CONFIRM champion params ***")
        last_status = "HOT_STREAK"

        # Analyse winning trades and store their signature for future experiments
        streak_n = exp_state.get("hot_streak_new_trades", 5)
        sig = analyze_hot_streak_trades(exp_start_count, n=streak_n)
        if sig:
            learning["hot_streak_signature"] = sig
            log(f"  Streak signature: {streak_n} wins | "
                f"avg score={sig.get('avg_confidence',0):.0f} | "
                f"avg entry={sig.get('avg_entry_price',0):.2f} | "
                f"avg secs_left={sig.get('avg_secs_left',0):.0f} | "
                f"directions={sig.get('direction_dist',{})} | "
                f"phases={sig.get('phase_dist',{})} | "
                f"total_pnl=${sig.get('total_pnl',0):+.2f}")
            save(LEARNING_PATH, learning)

        exp_state.pop("hot_streak", None)
        exp_state.pop("hot_streak_new_trades", None)
        save(EXPERIMENT_STATE_PATH, exp_state)
    else:
        last_status = "OK"

    analysis = analyze()
    n   = analysis["total_exits"]
    wr  = analysis["win_rate"]
    rec = analysis["recent_wr"]
    ev  = analysis["ev_per_trade"]
    bk  = analysis["breakeven_wr"]
    log(f"Overall: {n} trades | WR={wr}% | Recent={rec}% | EV=${ev:+.4f} | P&L=${analysis['total_pnl']:+.4f}")

    isolated = analyze_isolated(exp_start_time, exp_start_count)
    log(f"Isolated (last experiment): {isolated['n']} trades | WR={isolated['wr']}% | EV=${isolated['ev']:+.4f}")

    if history:
        if last_status != "HOT_STREAK":  # Don't overwrite hot streak flag
            learning, last_status = update_learning(learning, history[-1], isolated, analysis)
        log(f"Learning status: {last_status}")

    exp_type, label, overrides, desc = pick_experiment(learning, analysis, iteration_num, last_status)
    log(f"Experiment: [{exp_type}] {label}")
    log(f"  {desc}")

    if label not in learning["tested_labels"] and exp_type not in ("MARKET_WAKE", "DATA_GATHER", "CONFIRM_CHAMPION"):
        learning["tested_labels"].append(label)

    final_params = build_param_set(exp_type, overrides, learning)
    applied      = apply_params(final_params, iteration_num, label)
    log(f"Applied {len(applied)} parameter changes")

    write_logs(iteration_num, exp_type, label, desc, analysis, isolated, applied, learning)

    history.append({
        "iteration":        iteration_num,
        "timestamp":        str(datetime.datetime.now()),
        "experiment_type":  exp_type,
        "hypothesis_label": label,
        "description":      desc,
        "trades_seen":      n,
        "win_rate":         wr,
        "recent_wr":        rec,
        "ev_per_trade":     ev,
        "breakeven_wr":     bk,
        "total_pnl":        analysis["total_pnl"],
        "applied_params":   applied,
        "n_winners_known":  len(learning["proven_winners"]),
        "isolated_trades":  isolated["n"],
        "isolated_ev":      isolated["ev"],
        "isolated_wr":      isolated["wr"],
        "learning_status":  last_status,
    })
    save(ITER_LOG_PATH, history)
    save(LEARNING_PATH, learning)

    log("Restarting bot with new params...")
    restart_bot()

    current_n = analyze()["total_exits"]
    save_experiment_state(label, current_n, exp_type)

    log(f"Iteration #{iteration_num} complete.")
    log("=" * 60)

    winners  = learning["proven_winners"]
    champ_ev = learning.get("champion_ev", -999)
    ev_line  = f"EV=${ev:+.4f} ({'PROFITABLE' if ev>0 else f'needs {bk}% WR'})"
    print(f"\n{'='*60}", flush=True)
    print(f"  ITERATION #{iteration_num} | [{exp_type}] {label}", flush=True)
    print(f"  {desc[:65]}", flush=True)
    print(f"  Experiment: {isolated['n']} trades | WR: {isolated['wr']}% | EV: ${isolated['ev']:+.4f}", flush=True)
    print(f"  Overall: {n} trades | {ev_line}", flush=True)
    print(f"  Champion EV: ${champ_ev:+.4f} | Status: {last_status}", flush=True)
    print(f"  Proven winners ({len(winners)}): {', '.join(w['label'] for w in winners[:5])}", flush=True)
    print(f"{'='*60}\n", flush=True)

if __name__ == "__main__":
    main()
