from __future__ import annotations

import math
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Dict, Tuple

import numpy as np
import pandas as pd

ROOT = Path('/mnt/data')
OUT = ROOT / 'kartsell_v12_results'
OUT.mkdir(exist_ok=True)

FILES = {
    'SPY': ROOT / 'SPY_Historical_Data.csv',
    'AAPL': ROOT / 'AAPL.csv',
    'AMZN': ROOT / 'AMZN.csv',
    'NVDA': ROOT / 'NVDA.csv',
    'TSLA': ROOT / 'TSLA.csv',
    'AIG': ROOT / 'AIG.csv',
    'AAMRQ': ROOT / 'AAMRQ.csv',
}
ASSET_CLASS = {
    'SPY': 'broad_index_etf',
    'AAPL': 'large_quality_stock',
    'AMZN': 'large_quality_stock',
    'NVDA': 'high_vol_stock',
    'TSLA': 'high_vol_stock',
    'AIG': 'cyclical_stock',
    'AAMRQ': 'distressed_stock',
}
START = pd.Timestamp('2007-01-01')
END = pd.Timestamp('2026-08-01')
ONE_WAY_COST = 0.0007  # 7 bps: 2 bps commission + 5 bps slippage; FX/tax excluded.


def load_symbol(symbol: str, path: Path) -> pd.DataFrame:
    df = pd.read_csv(path)
    # Drop unnamed index columns.
    df = df.loc[:, ~df.columns.astype(str).str.startswith('Unnamed')]
    if df.columns[0] == '':
        df = df.iloc[:, 1:]
    rename = {c: c.strip().lower().replace('datetime', 'date') for c in df.columns}
    df = df.rename(columns=rename)
    if 'date' not in df.columns and 'DateTime' in df.columns:
        df = df.rename(columns={'DateTime': 'date'})
    df['date'] = pd.to_datetime(df['date'], errors='coerce', format='mixed')
    for c in ['open', 'high', 'low', 'close', 'volume']:
        df[c] = pd.to_numeric(df[c], errors='coerce')
    df = df.dropna(subset=['date', 'open', 'high', 'low', 'close']).copy()
    df = df[(df['date'] >= START) & (df['date'] <= END)]
    df = df.sort_values('date').drop_duplicates('date', keep='last').reset_index(drop=True)
    df['symbol'] = symbol
    df['adj_close'] = df['close']
    return df[['date','symbol','open','high','low','close','adj_close','volume']]


def data_quality(symbol: str, df: pd.DataFrame) -> dict:
    raw = pd.read_csv(FILES[symbol])
    date_col = 'DateTime' if 'DateTime' in raw.columns else 'date'
    raw_dates = pd.to_datetime(raw[date_col], errors='coerce', format='mixed')
    sorted_raw = raw_dates.dropna().is_monotonic_increasing
    dup = raw_dates.duplicated().sum()
    invalid_ohlc = ((df['high'] < df[['open','close','low']].max(axis=1)) |
                    (df['low'] > df[['open','close','high']].min(axis=1))).sum()
    ret = df['close'].pct_change()
    jumps50 = (ret.abs() > 0.50).sum()
    gaps = df['date'].diff().dt.days
    max_gap = int(gaps.max()) if len(gaps.dropna()) else 0
    zero_vol = int((df['volume'].fillna(0) <= 0).sum())
    return {
        'symbol': symbol,
        'start': df['date'].min().date().isoformat() if len(df) else None,
        'end': df['date'].max().date().isoformat() if len(df) else None,
        'rows': len(df),
        'raw_sorted': bool(sorted_raw),
        'raw_duplicate_dates': int(dup),
        'invalid_ohlc_rows': int(invalid_ohlc),
        'abs_daily_return_gt_50pct': int(jumps50),
        'max_calendar_gap_days': max_gap,
        'zero_or_missing_volume_rows': zero_vol,
        'total_return_available': False,
        'pit_fundamentals_available': False,
        'corporate_action_table_available': False,
    }


def add_features(df: pd.DataFrame) -> pd.DataFrame:
    x = df.copy()
    prev = x['close'].shift(1)
    tr = pd.concat([(x['high']-x['low']), (x['high']-prev).abs(), (x['low']-prev).abs()], axis=1).max(axis=1)
    x['atr20'] = tr.rolling(20, min_periods=20).mean()
    x['sma50'] = x['close'].rolling(50, min_periods=50).mean()
    x['sma200'] = x['close'].rolling(200, min_periods=200).mean()
    x['sma50_slope20'] = x['sma50'] / x['sma50'].shift(20) - 1
    x['ret1'] = x['close'].pct_change()
    x['vol63'] = x['ret1'].rolling(63, min_periods=30).std() * math.sqrt(252)
    x['vol252_med'] = x['vol63'].rolling(252, min_periods=63).median()
    x['high20_prev'] = x['close'].rolling(20, min_periods=20).max().shift(1)
    x['high63_prev'] = x['close'].rolling(63, min_periods=40).max().shift(1)
    x['low20_prev'] = x['close'].rolling(20, min_periods=20).min().shift(1)
    x['weekly_close'] = x.set_index('date')['close'].resample('W-FRI').last().reindex(x['date'], method='ffill').to_numpy()
    return x


def activation_gain(asset_class: str) -> float:
    return {
        'broad_index_etf': 0.12,
        'large_quality_stock': 0.18,
        'high_vol_stock': 0.25,
        'cyclical_stock': 0.22,
        'distressed_stock': 0.30,
    }[asset_class]


def core_floor(asset_class: str) -> float:
    return {
        'broad_index_etf': 0.55,
        'large_quality_stock': 0.40,
        'high_vol_stock': 0.25,
        'cyclical_stock': 0.30,
        'distressed_stock': 0.15,
    }[asset_class]


def giveback_cap_v11(asset_class: str, peak_gain: float) -> float:
    if asset_class == 'broad_index_etf':
        return 0.12 if peak_gain < .25 else 0.10 if peak_gain < .50 else 0.08 if peak_gain < 1 else 0.07
    if asset_class == 'large_quality_stock':
        return 0.15 if peak_gain < .25 else 0.14 if peak_gain < .50 else 0.12 if peak_gain < 1 else 0.10
    return 0.18 if peak_gain < .50 else 0.15 if peak_gain < 1 else 0.12


def giveback_cap_v12(asset_class: str, peak_gain: float) -> float:
    # Deliberately wider than v11 to preserve secular winners; the portfolio floor handles systemic risk.
    if asset_class == 'broad_index_etf':
        return 0.14 if peak_gain < .25 else 0.12 if peak_gain < .50 else 0.10 if peak_gain < 1 else 0.08
    if asset_class == 'large_quality_stock':
        return 0.19 if peak_gain < .25 else 0.17 if peak_gain < .50 else 0.14 if peak_gain < 1 else 0.11
    if asset_class == 'cyclical_stock':
        return 0.20 if peak_gain < .50 else 0.17 if peak_gain < 1 else 0.14
    if asset_class == 'distressed_stock':
        return 0.28 if peak_gain < .50 else 0.23 if peak_gain < 1 else 0.18
    return 0.25 if peak_gain < .50 else 0.20 if peak_gain < 1 else 0.16


def v11_positions(df: pd.DataFrame, asset_class: str) -> Tuple[pd.DataFrame, pd.DataFrame]:
    x = add_features(df)
    pos = 1.0
    entry = float(x.iloc[0]['close'])
    peak = entry
    prev_floor = -np.inf
    breach_count = 0
    last_sell_idx = -10000
    rows, events = [], []
    for i, row in x.iterrows():
        px = float(row['close'])
        peak = max(peak, px)
        peak_gain = peak / entry - 1
        atr = float(row['atr20']) if pd.notna(row['atr20']) else np.nan
        dd = px / peak - 1
        trend_bad = pd.notna(row['sma200']) and px < row['sma200']
        regime = min(1, max(0, (-dd - .08)/.22))
        flow = min(1, max(0, (float(row['vol63'])-.20)/.50)) if pd.notna(row['vol63']) else 0
        lock = np.clip(.30 + .25*regime + .15*flow, .30, .85)
        floor = np.nan
        active = peak_gain >= activation_gain(asset_class)
        if pd.notna(atr) and peak-entry >= 4*atr:
            active = True
        old = pos
        reason = None
        if active and pd.notna(atr):
            atr_mult = np.clip(3.5-1.7*regime-.8*flow,1.6,3.5)
            floor = max(entry + lock*(peak-entry), peak-atr_mult*atr, peak*(1-giveback_cap_v11(asset_class,peak_gain)), prev_floor)
            prev_floor = floor
            breach_count = breach_count+1 if px<floor else 0
            gap=(floor-px)/atr if atr>0 else 0
            if i-last_sell_idx>=5:
                if gap>=1.5:
                    pos=max(.35,pos-.40); last_sell_idx=i; breach_count=0; reason='gap_floor'
                elif breach_count>=2:
                    pos=max(.50,pos-.20); last_sell_idx=i; breach_count=0; reason='two_close_floor'
        if trend_bad and dd<=-.18 and i-last_sell_idx>=5:
            f=.35 if asset_class=='broad_index_etf' else .20
            pos=max(f,pos-.25); last_sell_idx=i; reason='trend_crisis'
        no_new_low=i>=5 and px>float(x.iloc[max(0,i-5):i+1]['close'].min())
        recovery=pd.notna(row['sma200']) and px>row['sma200']
        if pos<1 and no_new_low and recovery:
            pos=min(1,pos+.15)
            if pos>old: reason='daily_reentry'
        if pos != old:
            events.append({'date':row['date'],'old_position':old,'new_position':pos,'side':'BUY' if pos>old else 'SELL','reason':reason,'price':px,'peak':peak,'cycle_entry':entry,'floor':floor})
        rows.append({'date':row['date'],'position':pos,'floor':floor,'entry':entry,'peak':peak})
    return pd.DataFrame(rows), pd.DataFrame(events)


def v12_positions(df: pd.DataFrame, asset_class: str) -> Tuple[pd.DataFrame, pd.DataFrame]:
    """K-ArtSell 12.1 balanced candidate.

    Keeps v11's strong profit floor, but fixes the stale-cycle and daily-refill defects:
    - separated core floor by asset class;
    - re-entry only after 10 sessions, 50D trend recovery and 20D breakout;
    - no more than one 20%-point refill per 10 sessions;
    - every refill starts a new tactical protection cycle.
    """
    x = add_features(df)
    pos = 1.0
    entry = float(x.iloc[0]['close'])
    peak = entry
    prev_floor = -np.inf
    breach_count = 0
    last_sell = -10000
    last_buy = -10000
    below200_count = 0
    rows, events = [], []
    min_pos = {
        'broad_index_etf': .35,
        'large_quality_stock': .25,
        'high_vol_stock': .20,
        'cyclical_stock': .20,
        'distressed_stock': .10,
    }[asset_class]
    crisis_dd = {
        'broad_index_etf': -.18,
        'large_quality_stock': -.18,
        'high_vol_stock': -.22,
        'cyclical_stock': -.20,
        'distressed_stock': -.30,
    }[asset_class]

    for i, row in x.iterrows():
        px = float(row['close'])
        peak = max(peak, px)
        peak_gain = peak / entry - 1.0
        atr = float(row['atr20']) if pd.notna(row['atr20']) else np.nan
        dd = px / peak - 1.0
        below200_count = below200_count + 1 if pd.notna(row['sma200']) and px < row['sma200'] else 0
        regime = np.clip((-dd - .08) / .22, 0, 1)
        flow_proxy = np.clip((float(row['vol63']) - .20) / .50, 0, 1) if pd.notna(row['vol63']) else 0
        floor = np.nan
        old = pos
        reason = None

        active = peak_gain >= activation_gain(asset_class)
        if pd.notna(atr) and peak - entry >= 4.0 * atr:
            active = True

        if active and pd.notna(atr):
            lock = np.clip(.30 + .25 * regime + .15 * flow_proxy, .30, .85)
            atr_mult = np.clip(3.5 - 1.7 * regime - .8 * flow_proxy, 1.6, 3.5)
            floor = max(
                entry + lock * (peak - entry),
                peak - atr_mult * atr,
                peak * (1.0 - giveback_cap_v11(asset_class, peak_gain)),
                prev_floor,
            )
            prev_floor = floor
            breach_count = breach_count + 1 if px < floor else 0
            gap = (floor - px) / atr if atr > 0 else 0
            if i - last_sell >= 5 and pos > min_pos:
                if gap >= 1.5:
                    pos = max(min_pos, pos - .40)
                    last_sell = i
                    breach_count = 0
                    reason = 'gap_floor'
                elif breach_count >= 2:
                    pos = max(min_pos, pos - .20)
                    last_sell = i
                    breach_count = 0
                    reason = 'two_close_floor'

        if below200_count >= 1 and dd <= crisis_dd and i - last_sell >= 5 and pos > min_pos:
            pos = max(min_pos, pos - .25)
            last_sell = i
            reason = 'trend_crisis'

        # Staged, non-daily re-entry. Both trend recovery and a fresh breakout are required.
        if pos < 1.0 and i - last_sell >= 10 and i - last_buy >= 10:
            reclaim50 = (
                pd.notna(row['sma50']) and px > row['sma50'] and
                pd.notna(row['sma50_slope20']) and row['sma50_slope20'] > 0
            )
            breakout20 = pd.notna(row['high20_prev']) and px > row['high20_prev']
            if reclaim50 and breakout20:
                pos = min(1.0, pos + .20)
                last_buy = i
                reason = 'reentry_reclaim50_breakout20'
                # New tactical capital must not inherit an obsolete historical floor.
                entry = px
                peak = px
                prev_floor = -np.inf
                breach_count = 0

        if pos != old:
            events.append({
                'date': row['date'], 'old_position': old, 'new_position': pos,
                'side': 'BUY' if pos > old else 'SELL', 'reason': reason,
                'price': px, 'peak': peak, 'cycle_entry': entry, 'floor': floor,
            })
        rows.append({
            'date': row['date'], 'position': pos, 'floor': floor,
            'entry': entry, 'peak': peak, 'regime_proxy': regime,
            'volshock_proxy': flow_proxy,
        })
    return pd.DataFrame(rows), pd.DataFrame(events)

def buyhold_positions(df):
    return pd.DataFrame({'date':df['date'],'position':1.0})


def trend10m_positions(df):
    x=df[['date','close']].copy()
    m=x.set_index('date')['close'].resample('ME').last().dropna().to_frame('close')
    m['sma10']=m['close'].rolling(10,min_periods=10).mean()
    # Execute only after the month-end observation becomes known.
    m['position']=(m['close']>=m['sma10']).astype(float).shift(1)
    daily=m['position'].reindex(x['date'],method='ffill').fillna(1.0).to_numpy()
    return pd.DataFrame({'date':x['date'],'position':daily})


def trailing_positions(df, trail=.12):
    pos=1.0; peak=float(df.iloc[0]['close']); cooldown=0; out=[]
    for _,r in df.iterrows():
        px=float(r['close'])
        if pos>0:
            peak=max(peak,px)
            if px<=peak*(1-trail): pos=0; cooldown=20
        else:
            cooldown-=1
            if cooldown<=0 and px>=df.loc[:r.name,'close'].rolling(50,min_periods=20).mean().iloc[-1]:
                pos=1; peak=px
        out.append({'date':r['date'],'position':pos})
    return pd.DataFrame(out)


def backtest(df: pd.DataFrame, posdf: pd.DataFrame, cost=ONE_WAY_COST) -> pd.DataFrame:
    z=df[['date','close']].merge(posdf[['date','position']],on='date',how='inner').copy()
    z['asset_return']=z['close'].pct_change().fillna(0)
    z['position_lag']=z['position'].shift(1).fillna(z['position'].iloc[0])
    delta=z['position'].diff().fillna(0)
    z['turnover']=delta.abs()
    z['cost']=z['turnover']*cost
    z['strategy_return']=z['position_lag']*z['asset_return']-z['cost']
    z['equity']=(1+z['strategy_return']).cumprod()
    return z


def max_recovery_days(equity: pd.Series) -> int:
    dd=equity/equity.cummax()-1
    cur=mx=0
    for v in dd<0:
        cur=cur+1 if v else 0; mx=max(mx,cur)
    return int(mx)


def metrics(bt: pd.DataFrame) -> dict:
    if len(bt)<2: return {}
    years=max((bt['date'].iloc[-1]-bt['date'].iloc[0]).days/365.25,1/365.25)
    eq=bt['equity']; ret=bt['strategy_return']
    final=float(eq.iloc[-1]); cagr=final**(1/years)-1
    vol=float(ret.std(ddof=1)*math.sqrt(252)) if ret.std(ddof=1)>0 else 0
    sh=float(ret.mean()/ret.std(ddof=1)*math.sqrt(252)) if ret.std(ddof=1)>0 else 0
    dd=eq/eq.cummax()-1; mdd=float(dd.min())
    downside=ret[ret<0]
    sortino=float(ret.mean()/downside.std(ddof=1)*math.sqrt(252)) if len(downside)>2 and downside.std(ddof=1)>0 else 0
    cvar=float(ret[ret<=ret.quantile(.05)].mean()) if len(ret)>20 else np.nan
    return {
        'start':bt['date'].iloc[0].date().isoformat(),'end':bt['date'].iloc[-1].date().isoformat(),'observations':len(bt),
        'cagr':cagr,'mdd':mdd,'calmar':cagr/abs(mdd) if mdd<0 else np.nan,'annual_vol':vol,'sharpe_zero_rf':sh,'sortino_zero_rf':sortino,
        'ulcer_index':float(np.sqrt(np.mean((dd*100)**2))),'recovery_days_max':max_recovery_days(eq),
        'turnover_annual':float(bt['turnover'].sum()/years),'avg_exposure':float(bt['position_lag'].mean()),
        'cvar_5_daily':cvar,'final_equity':final,
    }


def event_stats(symbol: str, df: pd.DataFrame, events: pd.DataFrame, strategy: str) -> dict:
    if events.empty:
        return {'symbol':symbol,'strategy':strategy,'sell_events':0,'buy_events':0,'median_gain_capture':np.nan,'median_63d_post_sell_return':np.nan,'false_exit_rate_63d':np.nan}
    close=df.set_index('date')['close']
    sells=events[events['side']=='SELL'].copy(); buys=events[events['side']=='BUY']
    captures=[]; fwd=[]; false=[]
    for _,e in sells.iterrows():
        d=e['date']; px=e['price']; entry=e['cycle_entry']; peak=e['peak']
        denom=peak-entry
        captures.append((px-entry)/denom if denom>0 else np.nan)
        idx=close.index.get_indexer([d])[0]
        if idx>=0 and idx+63<len(close):
            r=close.iloc[idx+63]/px-1; fwd.append(r); false.append(r>0.10)
    return {'symbol':symbol,'strategy':strategy,'sell_events':len(sells),'buy_events':len(buys),
            'median_gain_capture':float(np.nanmedian(captures)) if captures else np.nan,
            'median_63d_post_sell_return':float(np.nanmedian(fwd)) if fwd else np.nan,
            'false_exit_rate_63d':float(np.mean(false)) if false else np.nan}


def period_slice(bt, start, end):
    q=bt[(bt['date']>=pd.Timestamp(start)) & (bt['date']<=pd.Timestamp(end))].copy()
    if len(q):
        # Rebase within period.
        q['equity']=(1+q['strategy_return']).cumprod()
    return q


def portfolio_backtest(asset_bts: Dict[str,pd.DataFrame], use_capital_floor: bool) -> pd.DataFrame:
    rets=[]; exps=[]; turns=[]
    for s,bt in asset_bts.items():
        q=bt.set_index('date')
        rets.append(q['strategy_return'].rename(s)); exps.append(q['position_lag'].rename(s)); turns.append(q['turnover'].rename(s))
    R=pd.concat(rets,axis=1).sort_index()
    E=pd.concat(exps,axis=1).reindex(R.index)
    T=pd.concat(turns,axis=1).reindex(R.index)
    base=R.mean(axis=1,skipna=True).fillna(0)
    avgexp=E.mean(axis=1,skipna=True).fillna(0)
    avgturn=T.mean(axis=1,skipna=True).fillna(0)
    scale=1.0; equity=1.0; peak=1.0; prev_scale=1.0; rows=[]
    for d,r in base.items():
        dd=equity/peak-1
        target=1.0
        if use_capital_floor:
            if dd<=-.25: target=.35
            elif dd<=-.18: target=.55
            elif dd<=-.12: target=.75
            elif dd<=-.08: target=.90
            # Hysteresis: scale up only in 10%-points per day after recovery.
            if target>scale: scale=min(target,scale+.10)
            else: scale=target
        else: scale=1.0
        overlay_turn=abs(scale-prev_scale)
        strat=scale*r-overlay_turn*ONE_WAY_COST
        equity*=1+strat; peak=max(peak,equity)
        rows.append({'date':d,'asset_return':r,'position':scale*avgexp.loc[d],'position_lag':scale*avgexp.loc[d],
                     'turnover':scale*avgturn.loc[d]+overlay_turn,'cost':overlay_turn*ONE_WAY_COST,'strategy_return':strat,'equity':equity,'risk_scale':scale})
        prev_scale=scale
    return pd.DataFrame(rows)


def main():
    dfs={s:load_symbol(s,p) for s,p in FILES.items()}
    pd.DataFrame([data_quality(s,df) for s,df in dfs.items()]).to_csv(OUT/'data_quality.csv',index=False)
    asset_rows=[]; period_rows=[]; event_rows=[]
    bts_by_strategy={k:{} for k in ['buy_hold','trend_10m_shifted','trailing_12','kartsell_v11_original','kartsell_v12_candidate']}
    periods={
        'research_2007_2013':('2007-01-01','2013-12-31'),
        'validation_2014_2017':('2014-01-01','2017-12-31'),
        'rolling_oos_2018_2023':('2018-01-01','2023-12-31'),
        'frozen_oos_2024_latest':('2024-01-01','2026-08-01'),
        'gfc_2007_2009':('2007-01-01','2009-12-31'),
        'covid_2020':('2020-01-01','2020-12-31'),
        'inflation_bear_2022':('2022-01-01','2022-12-31'),
    }
    for s,df in dfs.items():
        v11,e11=v11_positions(df,ASSET_CLASS[s]); v12,e12=v12_positions(df,ASSET_CLASS[s])
        posmap={'buy_hold':buyhold_positions(df),'trend_10m_shifted':trend10m_positions(df),'trailing_12':trailing_positions(df),
                'kartsell_v11_original':v11,'kartsell_v12_candidate':v12}
        for name,pos in posmap.items():
            bt=backtest(df,pos); bts_by_strategy[name][s]=bt
            asset_rows.append({'symbol':s,'asset_class':ASSET_CLASS[s],'strategy':name,**metrics(bt)})
            for pn,(ps,pe) in periods.items():
                q=period_slice(bt,ps,pe)
                if len(q)>=60:
                    period_rows.append({'symbol':s,'strategy':name,'period':pn,**metrics(q)})
        e11.to_csv(OUT/f'{s}_v11_events.csv',index=False); e12.to_csv(OUT/f'{s}_v12_events.csv',index=False)
        event_rows += [event_stats(s,df,e11,'kartsell_v11_original'),event_stats(s,df,e12,'kartsell_v12_candidate')]
    pd.DataFrame(asset_rows).to_csv(OUT/'asset_metrics.csv',index=False)
    pd.DataFrame(period_rows).to_csv(OUT/'period_metrics.csv',index=False)
    pd.DataFrame(event_rows).to_csv(OUT/'sell_event_stats.csv',index=False)

    port_rows=[]; port_bts={}
    portfolio_sets = {
        'core_six_dynamic_equal_weight': ['SPY','AAPL','AMZN','NVDA','TSLA','AIG'],
        'all_seven_including_delisted_stress': list(FILES.keys()),
    }
    for portfolio_name, symbols in portfolio_sets.items():
        for name,abd in bts_by_strategy.items():
            selected={k:v for k,v in abd.items() if k in symbols}
            p=portfolio_backtest(selected,use_capital_floor=False)
            port_bts[f'{portfolio_name}_{name}']=p
            port_rows.append({'portfolio':portfolio_name,'strategy':name,**metrics(p)})
        selected={k:v for k,v in bts_by_strategy['kartsell_v12_candidate'].items() if k in symbols}
        pcap=portfolio_backtest(selected,use_capital_floor=True)
        port_bts[f'{portfolio_name}_kartsell_v12_plus_capital_floor']=pcap
        port_rows.append({'portfolio':portfolio_name,'strategy':'kartsell_v12_plus_capital_floor',**metrics(pcap)})
    pd.DataFrame(port_rows).to_csv(OUT/'portfolio_metrics.csv',index=False)
    for name,p in port_bts.items(): p.to_csv(OUT/f'portfolio_{name}.csv',index=False)

    # Compact summary comparison.
    am=pd.DataFrame(asset_rows)
    wide=am.pivot(index='symbol',columns='strategy',values=['cagr','mdd','calmar','turnover_annual','avg_exposure'])
    wide.to_csv(OUT/'asset_comparison_wide.csv')
    print(pd.DataFrame(port_rows)[['strategy','cagr','mdd','calmar','turnover_annual','avg_exposure','final_equity']].to_string(index=False))
    print('\nAsset v11 vs v12:')
    print(am[am.strategy.isin(['kartsell_v11_original','kartsell_v12_candidate'])][['symbol','strategy','cagr','mdd','calmar','turnover_annual','avg_exposure']].to_string(index=False))

if __name__=='__main__':
    main()
