"""
Walk-forward Kelly backtest with Monte Carlo.

For each test season, training uses all prior seasons to:
  1. Identify which outcome (H/D/A) has CLV edge >5% per league
  2. Only bet outcomes that were profitable in training (ROI > 0, min 20 bets)

Test seasons are then compounded sequentially with Kelly staking.
MC runs 10,000 sims on the OOS bet sequence.
"""
import pandas as pd
import numpy as np
import os
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter

np.random.seed(42)

N_SIMS      = 10_000
START       = 1_000.0
KELLY_MULTS = [0.10, 0.25, 0.50]
EDGE_THRESH         = 0.05   # >5% CLV required
MIN_BETS            = 20     # min training bets to trust a league/outcome
RECENCY_DECAY       = 0.75   # weight multiplier per season back (most recent = 1.0)
MIN_SEASON_BETS     = 5      # min bets in a season for it to count in consistency check
MIN_PROFITABLE_FRAC = 0.50   # majority of qualifying seasons must have positive ROI
PLOT_OUT    = r'E:\soccer\walkforward_kelly.png'

seasons = ['1920','2021','2122','2223','2324','2425','2526']
league_names = {
    'E0': 'EPL', 'E1': 'Championship', 'E2': 'League One', 'E3': 'League Two', 'EC': 'Conference',
    'SP1': 'La Liga', 'SP2': 'Segunda',
    'I1': 'Serie A', 'I2': 'Serie B',
    'D1': 'Bundesliga', 'D2': '2.Bundesliga',
    'F1': 'Ligue 1', 'F2': 'Ligue 2',
    'N1': 'Eredivisie',
    'B1': 'Belgian Pro League',
    'P1': 'Primeira Liga',
    'T1': 'Super Lig',
    'G1': 'Greek SL',
    'SC0': 'Scottish Prem', 'SC1': 'Scottish Champ', 'SC2': 'Scottish L1', 'SC3': 'Scottish L2',
}

def remove_margin(h, d, a):
    over = 1/h + 1/d + 1/a
    return 1/h/over, 1/d/over, 1/a/over

# ── Load all data ──────────────────────────────────────────────────────────────
print("Loading data...")
all_dfs = []
for code, name in league_names.items():
    for s in seasons:
        path = f'E:/soccer/league_data/{code}_{s}.csv'
        try:
            d = pd.read_csv(path, encoding='utf-8-sig')
            if len(d) < 10 or 'FTR' not in d.columns or 'B365A' not in d.columns:
                continue
            d = d.dropna(subset=['FTR','B365A','B365H','B365D','PSCH','PSCD','PSCA'])
            if len(d) < 10: continue
            d['Season'] = s
            d['LeagueCode'] = code
            d['LeagueName'] = name
            all_dfs.append(d)
        except:
            pass

df = pd.concat(all_dfs, ignore_index=True)
df['Date'] = pd.to_datetime(df['Date'], dayfirst=True, errors='coerce')
df = df.dropna(subset=['Date']).sort_values('Date').reset_index(drop=True)

tH, tD, tA = zip(*df.apply(lambda r: remove_margin(r['PSCH'],r['PSCD'],r['PSCA']), axis=1))
df = df.copy()
df['p_H'] = pd.Series(tH, index=df.index)
df['p_D'] = pd.Series(tD, index=df.index)
df['p_A'] = pd.Series(tA, index=df.index)
df['PSC_fair_H'] = 1 / df['p_H']
df['PSC_fair_D'] = 1 / df['p_D']
df['PSC_fair_A'] = 1 / df['p_A']
df['edge_A'] = (df['B365A'] - df['PSC_fair_A']) / df['PSC_fair_A']
df['edge_H'] = (df['B365H'] - df['PSC_fair_H']) / df['PSC_fair_H']
df['edge_D'] = (df['B365D'] - df['PSC_fair_D']) / df['PSC_fair_D']

print(f"  {len(df):,} matches across {df['LeagueName'].nunique()} leagues")

# ── Walk-forward: train on all seasons before test_season ─────────────────────
# Need at least 2 training seasons, so test starts from season index 2
test_seasons = seasons[2:]   # 2122 onwards

print(f"\nWalk-forward: train on all prior seasons, test on each season")
print(f"Test seasons: {test_seasons}\n")

all_oos_bets = []
training_log = []

outcomes_cfg = [
    ('Away', 'edge_A', 'B365A', 'p_A', 'A'),
    ('Home', 'edge_H', 'B365H', 'p_H', 'H'),
    ('Draw', 'edge_D', 'B365D', 'p_D', 'D'),
]

for test_s in test_seasons:
    train_seasons = seasons[:seasons.index(test_s)]
    train_df = df[df['Season'].isin(train_seasons)]
    test_df  = df[df['Season'] == test_s]

    # For each league, pick the best outcome from training.
    # Qualify: recency-weighted ROI > 0, min total bets, AND
    # majority of individual seasons (with enough bets) must be profitable.
    rules = {}
    n_train = len(train_seasons)
    # Season weights: most recent = 1.0, each season back multiplied by RECENCY_DECAY
    season_weights = {s: RECENCY_DECAY ** (n_train - 1 - i)
                      for i, s in enumerate(train_seasons)}

    for league in df['LeagueName'].unique():
        best_weighted_roi = -999
        best = None
        tr = train_df[train_df['LeagueName'] == league]
        for label, edge_col, odds_col, p_col, result in outcomes_cfg:
            sub = tr[tr[edge_col] > EDGE_THRESH]
            if len(sub) < MIN_BETS:
                continue

            # Recency-weighted ROI
            weighted_pl = 0.0
            weighted_n  = 0.0
            for _, r in sub.iterrows():
                w = season_weights.get(r['Season'], 1.0)
                pl = (r[odds_col] - 1) if r['FTR'] == result else -1
                weighted_pl += pl * w
                weighted_n  += w
            w_roi = (weighted_pl / weighted_n * 100) if weighted_n > 0 else -999

            # Season consistency: majority of seasons with >=MIN_SEASON_BETS must be +ROI
            season_results = []
            for s in train_seasons:
                s_sub = sub[sub['Season'] == s]
                if len(s_sub) < MIN_SEASON_BETS:
                    continue
                s_pl = sum((r[odds_col]-1) if r['FTR']==result else -1
                           for _, r in s_sub.iterrows())
                season_results.append(s_pl > 0)

            if len(season_results) == 0:
                continue
            profitable_frac = sum(season_results) / len(season_results)

            if w_roi > 0 and profitable_frac >= MIN_PROFITABLE_FRAC and w_roi > best_weighted_roi:
                best_weighted_roi = w_roi
                best = (label, odds_col, p_col, result, len(sub), w_roi,
                        profitable_frac, len(season_results))

        if best:
            label, odds_col, p_col, result, n, w_roi, pfrac, n_seasons = best
            rules[league] = (label, odds_col, p_col, result, n, w_roi)
            training_log.append({
                'TestSeason':      test_s,
                'TrainSeasons':    ','.join(train_seasons),
                'League':          league,
                'Outcome':         label,
                'TrainN':          n,
                'WeightedROI':     round(w_roi, 2),
                'ProfitableFrac':  round(pfrac, 2),
                'SeasonsChecked':  n_seasons,
            })

    # Apply rules to test season
    for league, (label, odds_col, p_col, result, _, _) in rules.items():
        te = test_df[(test_df['LeagueName'] == league) & (test_df[f'edge_{result}'] > EDGE_THRESH)]
        for _, r in te.iterrows():
            b = r[odds_col] - 1
            p = r[p_col]
            kelly_f = max((p * b - (1 - p)) / b, 0) if b > 0 else 0
            if kelly_f > 0:
                all_oos_bets.append({
                    'Date':      r['Date'],
                    'Season':    test_s,
                    'League':    league,
                    'Outcome':   label,
                    'Odds':      r[odds_col],
                    'Kelly':     kelly_f,
                    'p_win':     p,
                    'Won':       r['FTR'] == result,
                })

bets_df = pd.DataFrame(all_oos_bets).sort_values('Date').reset_index(drop=True)
bets_df['Week'] = bets_df['Date'].dt.to_period('W')

print(f"OOS bets: {len(bets_df):,}  ({bets_df['Date'].min().date()} to {bets_df['Date'].max().date()})")
print(f"Overall OOS win rate:   {bets_df['Won'].mean()*100:.1f}%")
print(f"Avg Pinnacle fair prob: {bets_df['p_win'].mean()*100:.1f}%")

# Save bets
bets_df.to_csv(r'E:\soccer\walkforward_bets.csv', index=False)

# Training rules summary
tlog = pd.DataFrame(training_log)
print(f"\nTraining rules selected (last test season = {test_seasons[-1]}):")
last = tlog[tlog['TestSeason'] == test_seasons[-1]].sort_values('WeightedROI', ascending=False)
print(f"  {'League':<22} {'Outcome':<8} {'N':>6}  {'WtdROI':>8}  {'Prof%':>7}  {'Seasons':>8}")
print("  " + "-"*65)
for _, row in last.iterrows():
    print(f"  {row['League']:<22} {row['Outcome']:<8} {int(row['TrainN']):>6}  "
          f"{row['WeightedROI']:>7.1f}%  {row['ProfitableFrac']*100:>6.0f}%  "
          f"{int(row['SeasonsChecked']):>8}")

# Per-season OOS results
print(f"\nOOS results by season (flat stake):")
print(f"  {'Season':<8} {'Bets':>5}  {'P&L':>8}  {'ROI':>7}  {'WR':>6}")
print("  " + "-"*42)
for s in test_seasons:
    sub = bets_df[bets_df['Season'] == s]
    if sub.empty: continue
    profits = (sub['Odds']-1).where(sub['Won'], -1)
    roi = profits.mean() * 100
    print(f"  {s:<8} {len(sub):>5}  {profits.sum():>+8.2f}  {roi:>+6.2f}%  {sub['Won'].mean()*100:>5.1f}%")

total_profits = (bets_df['Odds']-1).where(bets_df['Won'], -1)
print(f"\n  {'TOTAL':<8} {len(bets_df):>5}  {total_profits.sum():>+8.2f}  {total_profits.mean()*100:>+6.2f}%  {bets_df['Won'].mean()*100:>5.1f}%")

# ── Deterministic Kelly compounding (OOS) ──────────────────────────────────────
print("\n" + "="*65)
print("DETERMINISTIC KELLY COMPOUNDING (OOS only)")
print("="*65)
print(f"  {'Kelly mult':<15} {'Final':>12}  {'x':>7}  {'MaxDD':>7}")
print("  " + "-"*50)
for km in KELLY_MULTS + [1.0]:
    bk = START; peak = START; max_dd = 0.0
    for _, bet in bets_df.iterrows():
        f = min(bet['Kelly'] * km, 0.25)
        stake = bk * f
        bk += stake * (bet['Odds']-1) if bet['Won'] else -stake
        if bk <= 0: bk = 0; break
        peak = max(peak, bk)
        max_dd = max(max_dd, (peak - bk) / peak)
    print(f"  {km:.2f}x Kelly    {bk:>12,.2f}  {bk/START:>7.1f}x  {max_dd*100:>6.1f}%")

# ── Monte Carlo ────────────────────────────────────────────────────────────────
print("\n" + "="*65)
print(f"MONTE CARLO ({N_SIMS:,} simulations, OOS bets only)")
print("="*65)

N      = len(bets_df)
odds   = bets_df['Odds'].values
p_win  = bets_df['p_win'].values
kelly  = bets_df['Kelly'].values

mc_results = {}
N_PLOT = 500

for km in KELLY_MULTS:
    stakes = np.minimum(kelly * km, 0.25)
    terminal = np.zeros(N_SIMS)
    max_dds  = np.zeros(N_SIMS)
    paths    = np.zeros((N_PLOT, N + 1))
    paths[:, 0] = START

    for sim in range(N_SIMS):
        outcomes = np.random.random(N) < p_win
        bk = START; peak = START; max_dd = 0.0
        for i in range(N):
            s = bk * stakes[i]
            bk += s * (odds[i]-1) if outcomes[i] else -s
            if bk <= 0: bk = 0; break
            peak = max(peak, bk)
            max_dd = max(max_dd, (peak - bk) / peak)
            if sim < N_PLOT:
                paths[sim, i+1] = bk
        terminal[sim] = bk
        max_dds[sim]  = max_dd
        if sim < N_PLOT and bk <= 0:
            paths[sim, i+1:] = 0

    mc_results[km] = {'terminal': terminal, 'max_dds': max_dds, 'paths': paths}
    print(f"\n  {km:.2f}x Kelly:")
    print(f"    Median:       {np.median(terminal):>12,.0f}  ({np.median(terminal)/START:.1f}x)")
    print(f"    P10:          {np.percentile(terminal,10):>12,.0f}")
    print(f"    P25:          {np.percentile(terminal,25):>12,.0f}")
    print(f"    P75:          {np.percentile(terminal,75):>12,.0f}")
    print(f"    P90:          {np.percentile(terminal,90):>12,.0f}")
    print(f"    P(profit):     {(terminal>START).mean()*100:.1f}%")
    print(f"    P(> 5x):       {(terminal>START*5).mean()*100:.1f}%")
    print(f"    P(> 10x):      {(terminal>START*10).mean()*100:.1f}%")
    print(f"    P(ruin <50%):  {(terminal<START*0.5).mean()*100:.1f}%")
    print(f"    Median MaxDD:  {np.median(max_dds)*100:.1f}%")
    print(f"    95th pct DD:   {np.percentile(max_dds,95)*100:.1f}%")

# Summary table
print("\n" + "="*70)
print(f"  {'Metric':<22}", end="")
for km in KELLY_MULTS:
    print(f"  {km:.2f}x Kelly", end="")
print()
print("  " + "-"*68)
for label, fn in [
    ("Median",        lambda t: f"{np.median(t):>11,.0f}"),
    ("P10",           lambda t: f"{np.percentile(t,10):>11,.0f}"),
    ("P25",           lambda t: f"{np.percentile(t,25):>11,.0f}"),
    ("P75",           lambda t: f"{np.percentile(t,75):>11,.0f}"),
    ("P90",           lambda t: f"{np.percentile(t,90):>11,.0f}"),
    ("P(profit)",     lambda t: f"{(t>START).mean()*100:>10.1f}%"),
    ("P(> 5x)",       lambda t: f"{(t>START*5).mean()*100:>10.1f}%"),
    ("P(> 10x)",      lambda t: f"{(t>START*10).mean()*100:>10.1f}%"),
    ("P(ruin)",       lambda t: f"{(t<START*0.5).mean()*100:>10.1f}%"),
]:
    print(f"  {label:<22}", end="")
    for km in KELLY_MULTS:
        print(f"  {fn(mc_results[km]['terminal'])}", end="")
    print()
print(f"  {'Med MaxDD':<22}", end="")
for km in KELLY_MULTS:
    print(f"  {np.median(mc_results[km]['max_dds'])*100:>10.1f}%", end="")
print()

# ── Chart ─────────────────────────────────────────────────────────────────────
colors = {0.10: 'steelblue', 0.25: 'seagreen', 0.50: 'darkorange'}
n_test_yrs = len(test_seasons)

fig, axes = plt.subplots(1, 3, figsize=(18, 6))
fig.suptitle(
    f'Walk-Forward Kelly MC — CLV Best Outcome Per League (OOS only)\n'
    f'Train: prior seasons  |  Test: {test_seasons[0]} to {test_seasons[-1]}  '
    f'|  {len(bets_df):,} OOS bets  |  {N_SIMS:,} simulations',
    fontsize=11, fontweight='bold'
)

ax = axes[0]
for km in KELLY_MULTS:
    t = mc_results[km]['terminal']
    cap = np.percentile(t, 95)
    ax.hist(np.minimum(t, cap), bins=100, alpha=0.45, color=colors[km],
            label=f'{km:.2f}x Kelly  med={np.median(t):,.0f}', edgecolor='none')
    ax.axvline(np.median(t), color=colors[km], linewidth=2, linestyle='--')
ax.axvline(START, color='gray', linewidth=1, linestyle=':', label='Start 1,000')
ax.set_xlabel('Terminal bankroll')
ax.set_title('Terminal bankroll distribution\n(capped at 95th pct)')
ax.legend(fontsize=8)
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: f'{x:,.0f}'))

ax = axes[1]
x = np.arange(N + 1)
for km in KELLY_MULTS:
    pths = mc_results[km]['paths']
    p5  = np.percentile(pths, 5,  axis=0)
    p50 = np.percentile(pths, 50, axis=0)
    p95 = np.percentile(pths, 95, axis=0)
    ax.fill_between(x, p5, p95, alpha=0.12, color=colors[km])
    ax.plot(x, p50, color=colors[km], linewidth=2, label=f'{km:.2f}x Kelly')
ax.axhline(START, color='gray', linewidth=0.8, linestyle=':')
ax.set_xlabel('Bet number (OOS)')
ax.set_ylabel('Bankroll')
ax.set_title('Median path (shaded = 5th-95th pct)')
ax.legend(fontsize=8)
ax.set_yscale('log')
ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: f'{x:,.0f}'))

ax = axes[2]
for km in KELLY_MULTS:
    dd = mc_results[km]['max_dds'] * 100
    ax.hist(dd, bins=80, alpha=0.45, color=colors[km],
            label=f'{km:.2f}x Kelly  med={np.median(dd):.0f}%', edgecolor='none')
    ax.axvline(np.median(dd), color=colors[km], linewidth=2, linestyle='--')
ax.set_xlabel('Max drawdown (%)')
ax.set_title('Max drawdown distribution')
ax.legend(fontsize=8)
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: f'{x:.0f}%'))

plt.tight_layout()
plt.savefig(PLOT_OUT, dpi=150, bbox_inches='tight')
print(f"\nChart saved to {PLOT_OUT}")
print("Done.")
