#!/usr/bin/env python3
"""
PRO TRADING ENGINE - 20 Bot Long/Short con Leverage
====================================================
Sistema professionale con:
- 20 bot diversificati
- Posizioni LONG e SHORT
- Leverage 1x, 2x, 3x
- Limit orders (maker 0.16%)
- Multi-timeframe: 12h, 24h, 48h, 7d

Backtest Results (2 anni, fee reali):
- TOP: LONG 48h 3x = +969.1%
- Buy & Hold: +89.15%
"""

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

# ============================================
# CONFIGURAZIONE DATABASE
# ============================================
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

# ============================================
# KRAKEN FEE STRUCTURE
# ============================================
KRAKEN_MAKER_FEE = 0.0016      # 0.16%
KRAKEN_TAKER_FEE = 0.0026      # 0.26%
MARGIN_OPEN_FEE = 0.0002       # 0.02%
MARGIN_ROLLOVER = 0.0002       # 0.02% / 4h
SPREAD = 0.0003

# ============================================
# 20 CONFIGURAZIONI OTTIMIZZATE
# ============================================
BOT_CONFIGS = {
    # === TOP LONG PERFORMERS (3x leverage) ===
    'L01_48h_3x_top': {
        'direction': 'long',
        'dip_threshold': 0.07,
        'profit_target': 0.10,
        'stop_loss': 0.07,
        'leverage': 3,
        'lookback_minutes': 2880,
        'backtest': '+969.1%',
    },
    'L02_7d_3x': {
        'direction': 'long',
        'dip_threshold': 0.15,
        'profit_target': 0.10,
        'stop_loss': 0.07,
        'leverage': 3,
        'lookback_minutes': 10080,
        'backtest': '+692.8%',
    },
    'L03_48h_3x_v2': {
        'direction': 'long',
        'dip_threshold': 0.12,
        'profit_target': 0.12,
        'stop_loss': 0.10,
        'leverage': 3,
        'lookback_minutes': 2880,
        'backtest': '+632.8%',
    },
    'L04_24h_3x': {
        'direction': 'long',
        'dip_threshold': 0.08,
        'profit_target': 0.15,
        'stop_loss': 0.10,
        'leverage': 3,
        'lookback_minutes': 1440,
        'backtest': '+584.2%',
    },
    'L05_7d_3x_v2': {
        'direction': 'long',
        'dip_threshold': 0.15,
        'profit_target': 0.10,
        'stop_loss': 0.05,
        'leverage': 3,
        'lookback_minutes': 10080,
        'backtest': '+551.9%',
    },

    # === LONG 2x LEVERAGE (più sicuro) ===
    'L06_48h_2x': {
        'direction': 'long',
        'dip_threshold': 0.07,
        'profit_target': 0.10,
        'stop_loss': 0.07,
        'leverage': 2,
        'lookback_minutes': 2880,
        'backtest': '+533.8%',
    },
    'L07_24h_2x': {
        'direction': 'long',
        'dip_threshold': 0.07,
        'profit_target': 0.10,
        'stop_loss': 0.07,
        'leverage': 2,
        'lookback_minutes': 1440,
        'backtest': '+350.5%',
    },
    'L08_7d_2x': {
        'direction': 'long',
        'dip_threshold': 0.12,
        'profit_target': 0.12,
        'stop_loss': 0.10,
        'leverage': 2,
        'lookback_minutes': 10080,
        'backtest': '+316.3%',
    },

    # === LONG 1x (no leverage, più safe) ===
    'L09_48h_1x': {
        'direction': 'long',
        'dip_threshold': 0.07,
        'profit_target': 0.10,
        'stop_loss': 0.07,
        'leverage': 1,
        'lookback_minutes': 2880,
        'backtest': '+182.3%',
    },
    'L10_24h_1x': {
        'direction': 'long',
        'dip_threshold': 0.08,
        'profit_target': 0.15,
        'stop_loss': 0.12,
        'leverage': 1,
        'lookback_minutes': 1440,
        'backtest': '+126.8%',
    },
    'L11_7d_1x': {
        'direction': 'long',
        'dip_threshold': 0.15,
        'profit_target': 0.15,
        'stop_loss': 0.10,
        'leverage': 1,
        'lookback_minutes': 10080,
        'backtest': '+116.1%',
    },
    'L12_12h_1x': {
        'direction': 'long',
        'dip_threshold': 0.06,
        'profit_target': 0.10,
        'stop_loss': 0.07,
        'leverage': 1,
        'lookback_minutes': 720,
        'backtest': '+79.6%',
    },

    # === LONG DIVERSIFICATI ===
    'L13_48h_deep': {
        'direction': 'long',
        'dip_threshold': 0.12,
        'profit_target': 0.20,
        'stop_loss': 0.15,
        'leverage': 2,
        'lookback_minutes': 2880,
        'backtest': '+355.8%',
    },
    'L14_24h_fast': {
        'direction': 'long',
        'dip_threshold': 0.05,
        'profit_target': 0.07,
        'stop_loss': 0.05,
        'leverage': 2,
        'lookback_minutes': 1440,
        'backtest': '+287.4%',
    },
    'L15_7d_swing': {
        'direction': 'long',
        'dip_threshold': 0.15,
        'profit_target': 0.20,
        'stop_loss': 0.12,
        'leverage': 2,
        'lookback_minutes': 10080,
        'backtest': '+410.6%',
    },

    # === SHORT POSITIONS ===
    'S16_12h_2x': {
        'direction': 'short',
        'dip_threshold': 0.07,  # pump threshold
        'profit_target': 0.07,
        'stop_loss': 0.05,
        'leverage': 2,
        'lookback_minutes': 720,
        'backtest': '+21.4%',
    },
    'S17_12h_1x': {
        'direction': 'short',
        'dip_threshold': 0.07,
        'profit_target': 0.07,
        'stop_loss': 0.05,
        'leverage': 1,
        'lookback_minutes': 720,
        'backtest': '+13.1%',
    },
    'S18_24h_1x': {
        'direction': 'short',
        'dip_threshold': 0.10,
        'profit_target': 0.10,
        'stop_loss': 0.07,
        'leverage': 1,
        'lookback_minutes': 1440,
        'backtest': '+6.7%',
    },
    'S19_7d_hedge': {
        'direction': 'short',
        'dip_threshold': 0.15,
        'profit_target': 0.15,
        'stop_loss': 0.10,
        'leverage': 1,
        'lookback_minutes': 10080,
        'backtest': '-0.1%',
    },

    # === CONSERVATIVE LONG ===
    'L20_48h_safe': {
        'direction': 'long',
        'dip_threshold': 0.12,
        'profit_target': 0.15,
        'stop_loss': 0.10,
        'leverage': 1,
        'lookback_minutes': 2880,
        'backtest': '+106.0%',
    },
}

# Global state
running = True
bots: Dict[int, Dict[str, Any]] = {}
price_cache: Dict[str, Any] = {}


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


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


def fetch_prices(limit=10500):
    """Fetch 7+ days of 1-min data."""
    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]
            lows = [float(k[3]) for k in data]
            closes = [float(k[4]) for k in data]

            price_cache = {
                'current_price': closes[-1],
                'closes': closes,
                'highs': highs,
                'lows': lows,
            }

            # Pre-calculate rolling max/min for each lookback
            for lb in [720, 1440, 2880, 10080]:
                if len(highs) >= lb:
                    price_cache[f'high_{lb}'] = max(highs[-lb:])
                    price_cache[f'low_{lb}'] = min(lows[-lb:])

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


def get_signal_data(lookback: int):
    """Get dip from high and pump from low for lookback period."""
    current = price_cache.get('current_price', 0)
    ref_high = price_cache.get(f'high_{lookback}', current)
    ref_low = price_cache.get(f'low_{lookback}', current)

    dip_from_high = (current - ref_high) / ref_high if ref_high > 0 else 0
    pump_from_low = (current - ref_low) / ref_low if ref_low > 0 else 0

    return dip_from_high, pump_from_low, ref_high, ref_low


def load_active_bots():
    """Load active bots from database."""
    global bots

    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:
            bot_id = row['id']
            name = row['name']

            # Match config
            config = None
            for cfg_name, cfg in BOT_CONFIGS.items():
                if cfg_name in name or name in cfg_name:
                    config = cfg
                    break

            # Fallback: match by pattern
            if config is None:
                for cfg_name, cfg in BOT_CONFIGS.items():
                    # Check if name contains key parts
                    parts = name.replace('_', ' ').split()
                    if any(p.lower() in cfg_name.lower() for p in parts):
                        config = cfg
                        break

            if config is None:
                config = list(BOT_CONFIGS.values())[0]

            if bot_id not in bots:
                bots[bot_id] = {
                    'id': bot_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,
                }

                dir_str = config['direction'].upper()
                lev = config['leverage']
                lb_h = config['lookback_minutes'] // 60
                log(f"Loaded: {name} | {dir_str} {lev}x | "
                    f"D{config['dip_threshold']*100:.0f}/TP{config['profit_target']*100:.0f}/SL{config['stop_loss']*100:.0f} | "
                    f"{lb_h}h | {config.get('backtest', 'N/A')}", bot_id)
            else:
                bots[bot_id]['current_capital'] = float(row['current_capital'])
                bots[bot_id]['position'] = row['position'] or 0

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

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


def save_bot_state(bot_id: int, state: Dict[str, Any]):
    """Save bot state to database."""
    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()
        """, (bot_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'], bot_id))

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


def save_trade(bot_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)
        """, (
            bot_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}", bot_id)


def execute_open_position(bot: Dict[str, Any], price: float, signal_value: float):
    """Open LONG or SHORT position."""
    config = bot['config']
    direction = config['direction']
    leverage = config['leverage']

    position_value = bot['current_capital'] * 0.95
    margin_fee = position_value * leverage * MARGIN_OPEN_FEE if leverage > 1 else 0
    trading_fee = position_value * KRAKEN_MAKER_FEE

    if direction == 'long':
        exec_price = price * (1 + SPREAD)
        position_dir = 1
        dir_str = 'LONG'
    else:
        exec_price = price * (1 - SPREAD)
        position_dir = -1
        dir_str = 'SHORT'

    amount = position_value / exec_price
    capital_before = bot['current_capital']

    bot['current_capital'] -= trading_fee + margin_fee
    bot['position'] = position_dir
    bot['position_amount'] = amount
    bot['entry_price'] = exec_price
    bot['entry_time'] = datetime.now()

    save_trade(bot['id'], {
        'pair': bot['pair'],
        'direction': dir_str,
        'action': 'OPEN',
        'entry_price': exec_price,
        'amount': amount,
        'fee_entry': trading_fee + margin_fee,
        'entry_time': bot['entry_time'],
        'capital_before': capital_before,
        'capital_after': bot['current_capital'],
    }, bot['status'])

    save_bot_state(bot['id'], bot)

    tp_price = exec_price * (1 + config['profit_target']) if direction == 'long' else exec_price * (1 - config['profit_target'])
    sl_price = exec_price * (1 - config['stop_loss']) if direction == 'long' else exec_price * (1 + config['stop_loss'])

    log(f"OPEN {dir_str} {leverage}x | {amount:.6f} BTC @ €{exec_price:,.2f} | "
        f"Signal: {signal_value*100:+.1f}% | TP: €{tp_price:,.0f} | SL: €{sl_price:,.0f}", bot['id'])


def execute_close_position(bot: Dict[str, Any], price: float, reason: str):
    """Close position (LONG or SHORT)."""
    config = bot['config']
    direction = config['direction']
    leverage = config['leverage']

    amount = bot['position_amount']
    entry_price = bot['entry_price']

    if bot['position'] == 1:  # LONG
        exec_price = price * (1 - SPREAD)
        gross_pnl = amount * (exec_price - entry_price) * leverage
        dir_str = 'LONG'
    else:  # SHORT
        exec_price = price * (1 + SPREAD)
        gross_pnl = amount * (entry_price - exec_price) * leverage
        dir_str = 'SHORT'

    trading_fee = abs(gross_pnl) * KRAKEN_MAKER_FEE

    # Rollover fee
    rollover_cost = 0
    if bot['entry_time'] and leverage > 1:
        hours_in_pos = (datetime.now() - bot['entry_time']).total_seconds() / 3600
        rollover_periods = int(hours_in_pos / 4)
        rollover_cost = amount * entry_price * leverage * MARGIN_ROLLOVER * rollover_periods

    net_pnl = gross_pnl - trading_fee - rollover_cost
    pnl_pct = net_pnl / (amount * entry_price)

    capital_before = bot['current_capital']
    bot['current_capital'] += net_pnl + (amount * entry_price)  # Return principal + P&L

    duration = int((datetime.now() - bot['entry_time']).total_seconds() / 60) if bot['entry_time'] else 0

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

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

    save_bot_state(bot['id'], bot)

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


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

    dip_from_high, pump_from_low, ref_high, ref_low = get_signal_data(lookback)

    # Position management
    if bot['position'] != 0:
        entry_price = bot['entry_price']

        if bot['position'] == 1:  # LONG
            pnl_pct = (price - entry_price) / entry_price

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

        elif bot['position'] == -1:  # SHORT
            pnl_pct = (entry_price - price) / entry_price

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

    # Entry conditions
    elif bot['position'] == 0:
        if direction == 'long':
            if dip_from_high <= -config['dip_threshold']:
                execute_open_position(bot, price, dip_from_high)
        else:  # short
            if pump_from_low >= config['dip_threshold']:
                execute_open_position(bot, price, pump_from_low)


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

    log("="*70)
    log("PRO TRADING ENGINE - 20 Bot Long/Short con Leverage")
    log("="*70)
    log(f"Kraken Fees: Maker {KRAKEN_MAKER_FEE*100:.2f}% | Margin {MARGIN_OPEN_FEE*100:.3f}%")
    log(f"Leverage: 1x, 2x, 3x")

    log("\nConfigurations:")
    for name, cfg in list(BOT_CONFIGS.items())[:5]:
        log(f"  {name}: {cfg['direction'].upper()} {cfg['leverage']}x {cfg.get('backtest', '')}")
    log(f"  ... and {len(BOT_CONFIGS)-5} more")

    while running:
        try:
            load_active_bots()

            if not bots:
                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 bots
            for bot in bots.values():
                await process_bot(bot, price)

            # Log status
            now = datetime.now()
            if now.minute % 5 == 0 and now.second < CHECK_INTERVAL:
                long_pos = sum(1 for b in bots.values() if b['position'] == 1)
                short_pos = sum(1 for b in bots.values() if b['position'] == -1)
                total_cap = sum(b['current_capital'] for b in bots.values())

                log(f"Price: €{price:,.2f} | {len(bots)} bots (L:{long_pos} S:{short_pos}) | Cap: €{total_cap:,.2f}")

            await asyncio.sleep(CHECK_INTERVAL)

        except asyncio.CancelledError:
            break
        except Exception as e:
            log(f"Error: {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())
