#!/usr/bin/env python3
"""
Backtest SERIO trailing stop unificato 2° account.
Scarica candele 1m per ogni trade chiuso e simula il trailing step-by-step.
Regole:
  - Trigger: PnL% >= +1.5% → attiva trailing
  - Lock-in iniziale: breakeven (entry)
  - Step: 0.5% sopra il precedente (high water mark)
  - Esci quando PnL% scende sotto il trailing SL
"""
import sys, os, time
sys.path.insert(0, '/opt/charter-live/live_deploy_v2')
os.chdir('/opt/charter-live/live_deploy_v2')
from bybit_demo_client import BybitDemoClient
from datetime import datetime, timezone

c = BybitDemoClient()

# Step 1: prendi TUTTI i closed-pnl con start_ts
# Bybit closed-pnl ha createdTime/updatedTime in ms
all_trades = []
r = c._request('GET', '/v5/position/closed-pnl', {'category': 'linear', 'settleCoin': 'USDT', 'limit': '200'}, signed=True)
all_trades = r.get('result', {}).get('list', [])

print(f'TOTAL closed trades: {len(all_trades)}')

# Step 2: per ogni trade, dobbiamo trovare la data di APERTURA.
# closed-pnl.updatedTime è il momento di chiusura. Per l'apertura, dobbiamo
# guardare gli ordini chiusi (order history) e matchare per symbol+side+qty+entry
# Per semplicità, usiamo updatedTime - 1h come proxy di apertura (miglior stima)
# OPPURE scarichiamo gli order fills matchando per avgPrice

# Scarica anche gli order fills
all_orders = []
r2 = c._request('GET', '/v5/order/history', {'category': 'linear', 'limit': '200'}, signed=True)
all_orders = r2.get('result', {}).get('list', [])

# Match trade ↔ order fill per symbol+side+entry_price
def find_open_ts(tr, orders):
    """Trova timestamp apertura del trade matchando order fills."""
    sym = tr['symbol']
    side = tr['side']
    entry = float(tr['avgEntryPrice'])
    for o in orders:
        if o['symbol'] != sym:
            continue
        if o['side'] != side:
            continue
        try:
            op = float(o.get('avgPrice', 0))
        except Exception:
            continue
        if abs(op - entry) / max(entry, 1e-9) < 0.0005:  # 0.05% tolerance
            return int(o.get('createdTime', 0))
    return None

# Step 3: scarica candele 1m per ogni trade
def get_klines_1m(symbol, start_ms, end_ms):
    """Scarica candele 1m tra start_ms e end_ms (max 1000 per request)."""
    klines = []
    cursor = start_ms
    while cursor < end_ms:
        params = {
            'category': 'linear',
            'symbol': symbol,
            'interval': '1',
            'start': cursor,
            'end': min(end_ms, cursor + 1000 * 60 * 1000),  # 1000 candele = 1000 min
            'limit': '1000',
        }
        try:
            r = c._request('GET', '/v5/market/kline', params, signed=False)
            lst = r.get('result', {}).get('list', [])
            if not lst:
                break
            for k in lst:
                # k = [ts, open, high, low, close, volume, turnover]
                klines.append({
                    'ts': int(k[0]),
                    'o': float(k[1]),
                    'h': float(k[2]),
                    'l': float(k[3]),
                    'c': float(k[4]),
                })
            # ordina per ts crescente
            klines.sort(key=lambda x: x['ts'])
            if len(lst) < 1000:
                break
            cursor = klines[-1]['ts'] + 60 * 1000
            time.sleep(0.05)  # rate limit gentile
        except Exception as e:
            print(f"  ERR klines {symbol} @ {cursor}: {e}")
            break
    return klines

# Step 4: simulazione trailing per ogni trade
TRIGGER_PCT = 0.015
STEP_PCT = 0.005

def simulate(tr, klines, open_ts):
    """Simula trailing. Restituisce dict con risultati."""
    symbol = tr['symbol']
    side = tr['side']  # 'Buy' LONG, 'Sell' SHORT
    qty = float(tr['qty'])
    entry = float(tr['avgEntryPrice'])
    exit_price = float(tr['avgExitPrice'])
    pnl_real = float(tr['closedPnl'])
    close_ts = int(tr.get('updatedTime', 0))

    if entry <= 0 or not klines:
        return None

    trailing_active = False
    max_pnl_pct = 0.0      # high water mark
    sl_pnl_pct = None      # SL corrente come PnL%
    exit_idx = None
    exit_pnl_pct = None

    for i, k in enumerate(klines):
        # PnL% in base al current price
        if side == 'Buy':
            cur_pnl_pct = (k['c'] - entry) / entry
            cur_high_pct = (k['h'] - entry) / entry
            cur_low_pct = (k['l'] - entry) / entry
        else:  # Sell SHORT
            cur_pnl_pct = (entry - k['c']) / entry
            cur_high_pct = (entry - k['l']) / entry
            cur_low_pct = (entry - k['h']) / entry

        # update high water mark usando HIGH della candela
        if cur_high_pct > max_pnl_pct:
            max_pnl_pct = cur_high_pct

        # trigger trailing
        if not trailing_active and max_pnl_pct >= TRIGGER_PCT:
            trailing_active = True
            sl_pnl_pct = 0.0  # breakeven

        # se trailing attivo, muovi SL se nuovo max
        if trailing_active and max_pnl_pct - sl_pnl_pct >= STEP_PCT:
            sl_pnl_pct = max_pnl_pct - STEP_PCT

        # check se usciamo (close della candela sotto SL)
        if trailing_active and sl_pnl_pct is not None and cur_low_pct <= sl_pnl_pct:
            exit_idx = i
            exit_pnl_pct = sl_pnl_pct
            break

    # se trailing mai triggerato o mai uscito → esci al close_ts reale
    if exit_idx is None:
        if trailing_active:
            # trailing attivo ma non uscito entro la fine dei dati → esci al exit reale
            if side == 'Buy':
                exit_pnl_pct = (exit_price - entry) / entry
            else:
                exit_pnl_pct = (entry - exit_price) / entry
        else:
            # trailing mai attivato → PnL ideale = PnL reale
            exit_pnl_pct = None  # signal: trailing mai attivato

    if exit_pnl_pct is None:
        return {
            'symbol': symbol, 'pnl_real': pnl_real, 'pnl_ideale': pnl_real,
            'trailing_triggered': False, 'trailing_exited_early': False,
            'lock_in_pct': 0, 'final_pnl_pct': 0
        }

    pnl_ideale = exit_pnl_pct * qty * entry
    return {
        'symbol': symbol, 'pnl_real': pnl_real, 'pnl_ideale': pnl_ideale,
        'trailing_triggered': trailing_active,
        'trailing_exited_early': exit_idx is not None and exit_pnl_pct != ((exit_price - entry) / entry if side == 'Buy' else (entry - exit_price) / entry),
        'lock_in_pct': sl_pnl_pct or 0,
        'final_pnl_pct': exit_pnl_pct,
    }


# Main loop
results = []
not_found = 0
for i, tr in enumerate(all_trades):
    sym = tr['symbol']
    side = tr['side']
    qty = float(tr['qty'])
    entry = float(tr['avgEntryPrice'])
    exit_price = float(tr['avgExitPrice'])
    pnl_real = float(tr['closedPnl'])
    close_ts = int(tr.get('updatedTime', 0))

    # trova open_ts
    open_ts = find_open_ts(tr, all_orders)
    if open_ts is None:
        # stima: 1h prima del close
        open_ts = close_ts - 3600 * 1000
        not_found += 1

    # durata
    duration_min = max(1, (close_ts - open_ts) / 60000)

    # scarica candele
    klines = get_klines_1m(sym, open_ts, close_ts)

    # simula
    res = simulate(tr, klines, open_ts)
    if res:
        res['duration_min'] = duration_min
        res['open_ts'] = open_ts
        res['close_ts'] = close_ts
        results.append(res)

    if (i + 1) % 10 == 0:
        print(f"  [{i+1}/{len(all_trades)}] processed, klines OK so far")

print(f'\nTotale trade: {len(results)} (open_ts non trovati: {not_found})')

# Aggrega
total_real = sum(r['pnl_real'] for r in results)
total_ideale = sum(r['pnl_ideale'] for r in results)
diff = total_ideale - total_real
n_triggered = sum(1 for r in results if r['trailing_triggered'])
n_early_exit = sum(1 for r in results if r['trailing_exited_early'])

print(f'\n=== RISULTATI BACKTEST SERIO ===')
print(f'Trade analizzati:           {len(results)}')
print(f'Trade con trailing trigger:  {n_triggered}')
print(f'Trade con uscita anticipata: {n_early_exit}')
print()
print(f'PnL reale totale:       {total_real:+.2f} USDT')
print(f'PnL ideale (con trail): {total_ideale:+.2f} USDT')
print(f'Differenza:             {diff:+.2f} USDT')
print(f'Verdetto: {"PEGGO" if diff < 0 else "MEGLIO"} con il nuovo trailing')

# Per simbolo
by_sym = {}
for r in results:
    s = r['symbol']
    if s not in by_sym:
        by_sym[s] = {'n': 0, 'real': 0, 'ideal': 0, 'triggered': 0}
    by_sym[s]['n'] += 1
    by_sym[s]['real'] += r['pnl_real']
    by_sym[s]['ideal'] += r['pnl_ideale']
    if r['trailing_triggered']:
        by_sym[s]['triggered'] += 1

print(f'\n=== Per simbolo ===')
for s in sorted(by_sym.keys(), key=lambda k: -by_sym[k]['real']):
    info = by_sym[s]
    d = info['ideal'] - info['real']
    print(f'  {s:14} n={info["n"]:>3} trig={info["triggered"]:>3} real={info["real"]:+8.2f} ideal={info["ideal"]:+8.2f} diff={d:+7.2f}')

# Trade con uscita anticipata
print(f'\n=== Trade con EXIT ANTICIPATO da trailing (ideal > real) ===')
early = sorted([r for r in results if r['pnl_ideale'] > r['pnl_real']], key=lambda r: r['pnl_ideale'] - r['pnl_real'], reverse=True)
for r in early[:20]:
    d = r['pnl_ideale'] - r['pnl_real']
    print(f"  {r['symbol']:14} dur={r['duration_min']:>5.0f}min real={r['pnl_real']:+7.2f} ideal={r['pnl_ideale']:+7.2f} lock={r['lock_in_pct']*100:+.2f}% diff={d:+7.2f}")

# Trade dove trailing NON triggerato ma ideale = reale
print(f'\n=== Trade senza trailing (real = ideal) ===')
not_trig = [r for r in results if not r['trailing_triggered']]
print(f'  {len(not_trig)} trade senza trailing')
