import pandas as pd
import numpy as np
import json


def atr(df, window=20):
    high, low, close = df['high'], df['low'], df['close']
    tr = pd.concat([
        high - low,
        (high - close.shift(1)).abs(),
        (low - close.shift(1)).abs()
    ], axis=1).max(axis=1)
    return tr.rolling(window).mean()


def adx(df, window=14):
    plus_dm = df['high'].diff()
    minus_dm = -df['low'].diff()
    plus_dm[plus_dm < 0] = 0
    minus_dm[minus_dm < 0] = 0
    tr = pd.concat([
        df['high'] - df['low'],
        (df['high'] - df['close'].shift(1)).abs(),
        (df['low'] - df['close'].shift(1)).abs()
    ], axis=1).max(axis=1)
    atr_val = tr.rolling(window).mean()
    plus_di = 100 * plus_dm.rolling(window).mean() / atr_val
    minus_di = 100 * minus_dm.rolling(window).mean() / atr_val
    dx = 100 * (plus_di - minus_di).abs() / (plus_di + minus_di)
    return dx.rolling(window).mean()


def hurst_rs(series, max_lag=100):
    """Estimate Hurst exponent via rescaled range (R/S)."""
    n = len(series)
    lags = list(range(2, min(max_lag, n // 4)))
    if len(lags) < 2:
        return np.nan
    rs_vals = []
    for lag in lags:
        chunks = n // lag
        chunk_rs = []
        for i in range(chunks):
            chunk = series.iloc[i*lag:(i+1)*lag].values
            if len(chunk) < 2:
                continue
            mean = chunk.mean()
            dev = chunk - mean
            cumulative = np.cumsum(dev)
            r = cumulative.max() - cumulative.min()
            s = chunk.std(ddof=1)
            if s == 0 or np.isnan(s):
                s = 1e-9
            chunk_rs.append(r / s)
        if chunk_rs:
            rs_vals.append(np.mean(chunk_rs))
    if len(rs_vals) < 2:
        return np.nan
    log_lags = np.log(lags[:len(rs_vals)])
    log_rs = np.log(rs_vals)
    poly = np.polyfit(log_lags, log_rs, 1)
    return poly[0]


class AdaptiveMomentumEngine:
    def __init__(self, capital=1_000_000, risk_per_trade=0.01,
                 target_vol=0.10, max_gross=1.5, max_single=0.15):
        self.capital = capital
        self.risk_per_trade = risk_per_trade
        self.target_vol = target_vol
        self.max_gross = max_gross
        self.max_single = max_single
        self.equity_curve = []
        self.trades = []
        self.current_positions = {}  # symbol -> {entry, shares, stop}

    def classify_regime(self, row):
        if row['adx'] > 25 and row.get('hurst', 0.5) > 0.55:
            return 'trend'
        if row['adx'] < 20 and row.get('hurst', 0.5) < 0.45:
            return 'mr'
        return 'neutral'

    def momentum_score(self, row):
        vol_penalty = max(0, row['vol20'] - self.target_vol)
        vwm = row['roc20'] * row['volume_ratio']
        return 0.40 * row['roc20'] + 0.30 * row['roc60'] + 0.20 * vwm - 0.10 * vol_penalty

    def aggregate_score(self, momentum, confirmation, regime):
        if regime == 'trend':
            return 0.6 * momentum + 0.4 * confirmation
        if regime == 'mr':
            return 0.4 * momentum + 0.6 * confirmation
        return 0.0

    def run(self, prices):
        prices = prices.copy().sort_index()
        prices['atr20'] = atr(prices)
        prices['roc20'] = prices['close'].pct_change(20)
        prices['roc60'] = prices['close'].pct_change(60)
        prices['vol20'] = prices['close'].pct_change().rolling(20).std() * np.sqrt(252)
        prices['volume_ratio'] = prices['volume'] / prices['volume'].rolling(60).mean()
        prices['adx'] = adx(prices)
        prices['hurst'] = prices['close'].rolling(120).apply(lambda s: hurst_rs(s, 50), raw=False)

        for date, row in prices.iterrows():
            if pd.isna(row['atr20']):
                self.equity_curve.append(self.capital)
                continue

            # Update stops and exits
            self.update_positions(row, date)

            regime = self.classify_regime(row)
            mom = self.momentum_score(row)
            conf = row.get('confirmation', 0.75)
            score = self.aggregate_score(mom, conf, regime)

            direction = 1
            if regime == 'mr' and mom < 0:
                direction = -1

            if abs(score) > 0.55 and not self.current_positions:
                target_size = self.size_position(row, score, regime)
                self.enter('SAMPLE', row, target_size, date, regime, direction)

            self.equity_curve.append(self.current_equity(row))

        return self.report()

    def size_position(self, row, score, regime):
        risk_amount = self.current_equity(row) * self.risk_per_trade
        shares = risk_amount / row['atr20']
        notional = shares * row['close']
        max_notional = self.current_equity(row) * self.max_single
        if score > 0.80 and regime == 'trend':
            notional *= 1.5
        if regime == 'neutral':
            notional *= 0.5
        if row.get('vol_percentile', 0) > 0.80:
            notional *= 0.5
        return min(notional, max_notional)

    def enter(self, symbol, row, notional, date, regime, direction=1):
        shares = direction * (notional / row['close'])
        stop_distance = 2 * row['atr20'] if regime == 'trend' else 1.5 * row['atr20']
        stop = row['close'] - direction * stop_distance
        self.current_positions[symbol] = {
            'entry': row['close'],
            'shares': shares,
            'stop': stop,
            'date': date,
            'regime': regime,
            'trailing': None,
            'direction': direction
        }
        self.trades.append({'type': 'entry', 'date': date, 'price': row['close'], 'shares': shares, 'direction': direction})

    def update_positions(self, row, date):
        for sym, pos in list(self.current_positions.items()):
            # hard stop
            if row['low'] <= pos['stop']:
                self.exit(sym, row, date, pos['stop'], 'stop')
                continue
            # trailing stop update
            if pos['regime'] == 'trend':
                new_trail = row['close'] - 1.5 * row['atr20']
                if pos['trailing'] is None and row['close'] > pos['entry'] + row['atr20']:
                    pos['trailing'] = new_trail
                elif pos['trailing'] is not None:
                    pos['trailing'] = max(pos['trailing'], new_trail)
                    if row['low'] <= pos['trailing']:
                        self.exit(sym, row, date, pos['trailing'], 'trailing')
                        continue
            # time stop
            if (date - pos['date']).days >= 10:
                self.exit(sym, row, date, row['close'], 'time')

    def exit(self, sym, row, date, price, reason):
        pos = self.current_positions.pop(sym)
        self.trades.append({'type': 'exit', 'date': date, 'price': price,
                            'shares': pos['shares'], 'reason': reason})

    def current_equity(self, row):
        cash = self.capital
        for sym, pos in self.current_positions.items():
            cash += pos['shares'] * (row['close'] - pos['entry'])
        return cash

    def report(self):
        eq = pd.Series(self.equity_curve)
        rets = eq.pct_change().dropna()
        return {
            'total_return': eq.iloc[-1] / eq.iloc[0] - 1,
            'annual_return': rets.mean() * 252,
            'annual_volatility': rets.std() * np.sqrt(252),
            'sharpe': rets.mean() / rets.std() * np.sqrt(252) if rets.std() else 0,
            'max_drawdown': (eq / eq.cummax() - 1).min(),
            'num_trades': len([t for t in self.trades if t['type'] == 'exit'])
        }


def generate_synthetic_data(n=1260, seed=42):
    """Generate synthetic OHLCV data for backtest demo."""
    np.random.seed(seed)
    rets = np.random.normal(0.0004, 0.012, n)
    close = 100 * np.exp(np.cumsum(rets))
    noise = 0.005
    high = close * (1 + np.abs(np.random.normal(0, noise, n)))
    low = close * (1 - np.abs(np.random.normal(0, noise, n)))
    volume = np.random.lognormal(16, 0.4, n)
    confirmation = np.clip(np.random.beta(7, 3, n), 0, 1)
    dates = pd.date_range('2020-01-01', periods=n, freq='B')
    return pd.DataFrame({
        'open': close * (1 + np.random.normal(0, 0.001, n)),
        'high': high,
        'low': low,
        'close': close,
        'volume': volume,
        'confirmation': confirmation
    }, index=dates)


if __name__ == '__main__':
    data = generate_synthetic_data()
    engine = AdaptiveMomentumEngine()
    report = engine.run(data)
    print(json.dumps(report, indent=2))
    # write equity curve for plotting
    pd.DataFrame({'equity': engine.equity_curve}, index=data.index).to_csv('equity_curve.csv')
