#!/usr/bin/env python3
"""
Multi-Strategy Dip Buyer Engine
===============================
Engine ottimizzato con multiple strategie temporali.

Strategie testate su 2 anni di dati con fee reali:
- 24h_alpha: D8/TP15/SL12 → +126.77% (Alpha +37%)
- 48h_swing: D12/TP20/SL15 → +118.61% (Alpha +29%)
- 7d_macro: D15/TP15/SL10 → +116.14% (Alpha +26%)
- 24h_fast: D7/TP10/SL7 → +96.55% (Alpha +7%)
"""

import asyncio
import signal
from datetime import datetime
from typing import Dict, Any, Optional, List
import requests
import psycopg2
from psycopg2.extras import RealDictCursor
import numpy as np

# Configuration
DB_CONFIG = {
    'host': 'localhost',
    'port': 5432,
    'dbname': 'pippo',
    'user': 'pippo',
    'password': 'pippo_trading_2026'
}

BINANCE_API = 'https://api.binance.com/api/v3'
CHECK_INTERVAL = 60  # seconds

# Kraken fees
KRAKEN_MAKER_FEE = 0.0016  # 0.16%
SPREAD = 0.0003  # 0.03%

# ============================================
# CONFIGURAZIONI OTTIMIZZATE (backtest reali)
# ============================================
TRADER_CONFIGS = {
    # TOP 1: Migliore assoluto
    'Alpha_24h': {
        'dip_threshold': 0.08,   # 8% dip da 24h high
        'profit_target': 0.15,   # 15% take profit
        'stop_loss': 0.12,       # 12% stop loss
        'position_size': 1.00,
        'lookback_minutes': 1440,  # 24h
        'backtest_return': '+126.77%',
        'backtest_alpha': '+37.03%',
    },
    # TOP 2: Swing trader 48h
    'Swing_48h': {
        'dip_threshold': 0.12,   # 12% dip da 48h high
        'profit_target': 0.20,   # 20% take profit
        'stop_loss': 0.15,       # 15% stop loss
        'position_size': 1.00,
        'lookback_minutes': 2880,  # 48h
        'backtest_return': '+118.61%',
        'backtest_alpha': '+28.87%',
    },
    # TOP 3: Macro trader 7d
    'Macro_7d': {
        'dip_threshold': 0.15,   # 15% dip da 7d high
        'profit_target': 0.15,   # 15% take profit
        'stop_loss': 0.10,       # 10% stop loss
        'position_size': 1.00,
        'lookback_minutes': 10080,  # 7d
        'backtest_return': '+116.14%',
        'backtest_alpha': '+26.40%',
    },
    # TOP 4: Fast trader - più trade
    'Fast_24h': {
        'dip_threshold': 0.07,   # 7% dip
        'profit_target': 0.10,   # 10% take profit
        'stop_loss': 0.07,       # 7% stop loss
        'position_size': 1.00,
        'lookback_minutes': 1440,  # 24h
        'backtest_return': '+96.55%',
        'backtest_alpha': '+6.81%',
    },
    # Alternativa: Dip profondo con SL largo
    'Deep_48h': {
        'dip_threshold': 0.10,   # 10% dip
        'profit_target': 0.15,   # 15% TP
        'stop_loss': 0.12,       # 12% SL
        'position_size': 1.00,
        'lookback_minutes': 2880,  # 48h
        'backtest_return': '+98.16%',
        'backtest_alpha': '+8.42%',
    },
    # High win rate
    'Safe_48h': {
        'dip_threshold': 0.12,   # 12% dip
        'profit_target': 0.15,   # 15% TP
        'stop_loss': 0.10,       # 10% SL
        'position_size': 1.00,
        'lookback_minutes': 2880,
        'backtest_return': '+105.97%',
        'backtest_alpha': '+16.22%',
    },
}

# Global state
running = True
traders: Dict[int, Dict[str, Any]] = {}
price_cache: Dict[int, List[float]] = {}  # lookback -> prices


def log(msg: str, trader_id: Optional[int] = None):
    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    prefix = f"[Trader {trader_id}]" if trader_id else "[Engine]"
    print(f"[{timestamp}] {prefix} {msg}")


def get_db_connection():
    return psycopg2.connect(**DB_CONFIG, cursor_factory=RealDictCursor)


def fetch_prices(limit=10500):  # ~7 days + buffer
    """Fetch prices from Binance."""
    global price_cache
    try:
        resp = requests.get(f'{BINANCE_API}/klines', params={
            'symbol': 'BTCEUR',
            'interval': '1m',
            'limit': limit
        }, timeout=30)
        if resp.ok:
            data = resp.json()
            highs = [float(k[2]) for k in data]  # High prices
            closes = [float(k[4]) for k in data]  # Close prices

            # Pre-calculate highs for different lookbacks
            price_cache = {
                'closes': closes,
                'current_price': closes[-1] if closes else 0,
            }

            # Calculate rolling max for each lookback
            for lookback in [720, 1440, 2880, 10080]:
                if len(highs) >= lookback:
                    rolling_max = max(highs[-lookback:])
                else:
                    rolling_max = max(highs) if highs else 0
                price_cache[f'high_{lookback}'] = rolling_max

            return price_cache['current_price']
    except Exception as e:
        log(f"Error fetching prices: {e}")
    return 0


def get_dip_from_high(current_price: float, lookback: int) -> float:
    """Calculate dip from high for specific lookback."""
    high_key = f'high_{lookback}'
    if high_key not in price_cache:
        return 0
    ref_high = price_cache[high_key]
    if ref_high <= 0:
        return 0
    return (current_price - ref_high) / ref_high


def load_active_traders():
    """Load active traders from database."""
    global traders

    try:
        conn = get_db_connection()
        cur = conn.cursor()

        cur.execute("""
            SELECT t.*, ts.position, ts.position_amount, ts.entry_price, ts.entry_time
            FROM traders t
            LEFT JOIN trader_states ts ON t.id = ts.trader_id
            WHERE t.status IN ('paper', 'live')
        """)

        rows = cur.fetchall()
        cur.close()
        conn.close()

        for row in rows:
            trader_id = row['id']
            name = row['name']

            # Match config by name
            config_key = None
            for key in TRADER_CONFIGS.keys():
                if key in name:
                    config_key = key
                    break

            if config_key is None:
                # Fallback: use first matching pattern
                for key in TRADER_CONFIGS.keys():
                    parts = key.split('_')
                    if any(p.lower() in name.lower() for p in parts):
                        config_key = key
                        break

            if config_key is None:
                config_key = 'Alpha_24h'  # Default

            config = TRADER_CONFIGS[config_key]

            if trader_id not in traders:
                traders[trader_id] = {
                    'id': trader_id,
                    'name': name,
                    'status': row['status'],
                    'initial_capital': float(row['initial_capital']),
                    'current_capital': float(row['current_capital']),
                    'pair': row['pair'],
                    'position': row['position'] or 0,
                    'position_amount': float(row['position_amount'] or 0),
                    'entry_price': float(row['entry_price'] or 0),
                    'entry_time': row['entry_time'],
                    'config': config,
                    'config_name': config_key,
                }
                lb_h = config['lookback_minutes'] // 60
                log(f"Loaded: {name} | {config_key} | Dip {config['dip_threshold']*100:.0f}% / "
                    f"TP {config['profit_target']*100:.0f}% / SL {config['stop_loss']*100:.0f}% | "
                    f"Lookback {lb_h}h | Backtest: {config.get('backtest_return', 'N/A')}", trader_id)
            else:
                traders[trader_id]['current_capital'] = float(row['current_capital'])

        # Remove stopped
        active_ids = {row['id'] for row in rows}
        for tid in list(traders.keys()):
            if tid not in active_ids:
                del traders[tid]

    except Exception as e:
        log(f"Error loading traders: {e}")


def save_trader_state(trader_id: int, state: Dict[str, Any]):
    """Save trader state."""
    try:
        conn = get_db_connection()
        cur = conn.cursor()

        cur.execute("""
            INSERT INTO trader_states (trader_id, position, position_amount, entry_price, entry_time, updated_at)
            VALUES (%s, %s, %s, %s, %s, NOW())
            ON CONFLICT (trader_id) DO UPDATE SET
                position = EXCLUDED.position,
                position_amount = EXCLUDED.position_amount,
                entry_price = EXCLUDED.entry_price,
                entry_time = EXCLUDED.entry_time,
                updated_at = NOW()
        """, (
            trader_id,
            state['position'],
            state['position_amount'],
            state['entry_price'],
            state['entry_time']
        ))

        cur.execute("""
            UPDATE traders SET current_capital = %s, updated_at = NOW() WHERE id = %s
        """, (state['current_capital'], trader_id))

        conn.commit()
        cur.close()
        conn.close()
    except Exception as e:
        log(f"Error saving state: {e}", trader_id)


def save_trade(trader_id: int, trade: Dict[str, Any], mode: str):
    """Save trade to database."""
    try:
        conn = get_db_connection()
        cur = conn.cursor()

        cur.execute("""
            INSERT INTO trades (
                trader_id, mode, pair, direction, action,
                entry_price, exit_price, amount,
                fee_entry, fee_exit, gross_pnl, net_pnl, pnl_percent,
                exit_reason, entry_time, exit_time, duration_minutes,
                capital_before, capital_after
            ) VALUES (
                %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
            )
        """, (
            trader_id, mode,
            trade.get('pair', 'BTC/EUR'),
            trade.get('direction'),
            trade.get('action'),
            trade.get('entry_price'),
            trade.get('exit_price'),
            trade.get('amount'),
            trade.get('fee_entry', 0),
            trade.get('fee_exit', 0),
            trade.get('gross_pnl'),
            trade.get('net_pnl'),
            trade.get('pnl_percent'),
            trade.get('exit_reason'),
            trade.get('entry_time'),
            trade.get('exit_time'),
            trade.get('duration_minutes'),
            trade.get('capital_before'),
            trade.get('capital_after'),
        ))

        conn.commit()
        cur.close()
        conn.close()
    except Exception as e:
        log(f"Error saving trade: {e}", trader_id)


def execute_buy(trader: Dict[str, Any], price: float, dip_pct: float):
    """Execute buy order."""
    config = trader['config']
    trade_value = trader['current_capital'] * config['position_size']

    exec_price = price * (1 + SPREAD)
    fee = trade_value * KRAKEN_MAKER_FEE
    amount = (trade_value - fee) / exec_price

    capital_before = trader['current_capital']
    trader['current_capital'] -= trade_value
    trader['position'] = 1
    trader['position_amount'] = amount
    trader['entry_price'] = exec_price
    trader['entry_time'] = datetime.now()

    save_trade(trader['id'], {
        'pair': trader['pair'],
        'direction': 'LONG',
        'action': 'OPEN',
        'entry_price': exec_price,
        'amount': amount,
        'fee_entry': fee,
        'entry_time': trader['entry_time'],
        'capital_before': capital_before,
        'capital_after': trader['current_capital'],
    }, trader['status'])

    save_trader_state(trader['id'], trader)

    lookback_h = config['lookback_minutes'] // 60
    log(f"BUY {amount:.6f} BTC @ €{exec_price:,.2f} | Dip: {dip_pct*100:.1f}% ({lookback_h}h) | "
        f"Fee: €{fee:.2f} | TP: €{exec_price*(1+config['profit_target']):,.0f} | "
        f"SL: €{exec_price*(1-config['stop_loss']):,.0f}", trader['id'])


def execute_sell(trader: Dict[str, Any], price: float, reason: str, pnl_pct: float):
    """Execute sell order."""
    amount = trader['position_amount']
    entry_price = trader['entry_price']

    exec_price = price * (1 - SPREAD)
    gross_value = amount * exec_price
    fee = gross_value * KRAKEN_MAKER_FEE

    gross_pnl = amount * (exec_price - entry_price)
    net_pnl = gross_pnl - fee

    capital_before = trader['current_capital']
    trader['current_capital'] += amount * entry_price + net_pnl

    duration = None
    if trader['entry_time']:
        duration = int((datetime.now() - trader['entry_time']).total_seconds() / 60)

    save_trade(trader['id'], {
        'pair': trader['pair'],
        'direction': 'LONG',
        'action': 'CLOSE',
        'entry_price': entry_price,
        'exit_price': exec_price,
        'amount': amount,
        'fee_exit': fee,
        'gross_pnl': gross_pnl,
        'net_pnl': net_pnl,
        'pnl_percent': pnl_pct * 100,
        'exit_reason': reason,
        'entry_time': trader['entry_time'],
        'exit_time': datetime.now(),
        'duration_minutes': duration,
        'capital_before': capital_before,
        'capital_after': trader['current_capital'],
    }, trader['status'])

    trader['position'] = 0
    trader['position_amount'] = 0
    trader['entry_price'] = 0
    trader['entry_time'] = None

    save_trader_state(trader['id'], trader)

    emoji = '+' if net_pnl > 0 else ''
    hours = duration // 60 if duration else 0
    log(f"SELL @ €{exec_price:,.2f} | {reason} | P&L: {emoji}€{net_pnl:.2f} ({pnl_pct*100:+.2f}%) | "
        f"Duration: {hours}h | Fee: €{fee:.2f}", trader['id'])


async def process_trader(trader: Dict[str, Any], price: float):
    """Process single trader."""
    config = trader['config']
    lookback = config['lookback_minutes']

    if trader['position'] == 1:
        pnl_pct = (price - trader['entry_price']) / trader['entry_price']

        if pnl_pct >= config['profit_target']:
            execute_sell(trader, price, 'take_profit', pnl_pct)
        elif pnl_pct <= -config['stop_loss']:
            execute_sell(trader, price, 'stop_loss', pnl_pct)

    elif trader['position'] == 0:
        dip_pct = get_dip_from_high(price, lookback)

        if dip_pct <= -config['dip_threshold']:
            execute_buy(trader, price, dip_pct)


async def main_loop():
    """Main trading loop."""
    global running

    log("Starting Multi-Strategy Dip Buyer Engine")
    log("=" * 60)
    log("Strategies optimized on 2-year backtest with real Kraken fees")
    log(f"Fee Maker: {KRAKEN_MAKER_FEE*100:.2f}% | Spread: {SPREAD*100:.2f}%")

    log("\nAvailable strategies:")
    for name, cfg in TRADER_CONFIGS.items():
        lb_h = cfg['lookback_minutes'] // 60
        log(f"  {name}: Dip {cfg['dip_threshold']*100:.0f}% / TP {cfg['profit_target']*100:.0f}% / "
            f"SL {cfg['stop_loss']*100:.0f}% ({lb_h}h) → {cfg.get('backtest_return', 'N/A')}")

    while running:
        try:
            load_active_traders()

            if not traders:
                await asyncio.sleep(10)
                continue

            price = fetch_prices(10500)
            if price <= 0:
                log("Could not get price, retrying...")
                await asyncio.sleep(30)
                continue

            # Process all traders
            for trader in traders.values():
                await process_trader(trader, price)

            # Log status periodically
            now = datetime.now()
            if now.minute % 5 == 0 and now.second < CHECK_INTERVAL:
                in_pos = sum(1 for t in traders.values() if t['position'] != 0)
                total_cap = sum(t['current_capital'] for t in traders.values())

                # Show dips for each lookback
                dips = []
                for lb in [1440, 2880, 10080]:
                    d = get_dip_from_high(price, lb)
                    dips.append(f"{lb//60}h:{d*100:+.1f}%")

                log(f"Price: €{price:,.2f} | Dips: {' | '.join(dips)} | "
                    f"{len(traders)} traders ({in_pos} in pos) | Cap: €{total_cap:,.2f}")

            await asyncio.sleep(CHECK_INTERVAL)

        except asyncio.CancelledError:
            break
        except Exception as e:
            log(f"Error in main loop: {e}")
            import traceback
            traceback.print_exc()
            await asyncio.sleep(30)

    log("Engine stopped")


def signal_handler(sig, frame):
    global running
    log("Shutting down...")
    running = False


if __name__ == '__main__':
    signal.signal(signal.SIGINT, signal_handler)
    signal.signal(signal.SIGTERM, signal_handler)

    asyncio.run(main_loop())
