#!/usr/bin/env python3
"""
AA Deal Desk | Sample trading bot skeleton (paper mode)

What this file shows
  1. A rules-based strategy written as plain functions (breakout with a trend filter).
  2. A paper broker that charges fees and slippage and sizes each trade by fixed risk.
  3. A backtest loop that uses the same strategy function a live bot would call.
  4. A Telegram alert helper and a Binance Testnet client stub (not run here).

What it does not do
  It does not trade real money. It runs on synthetic price data to demonstrate structure,
  testing and reporting. Results on synthetic data say nothing about future markets.

Run:  python3 bot_sample.py --backtest
"""
from __future__ import annotations
import argparse
import math
import random
from dataclasses import dataclass, field

# ----------------------------------------------------------------------------- config
CONFIG = {
    "symbol": "BTCUSDT",
    "timeframe_minutes": 60,
    "ema_trend": 200,
    "breakout_lookback": 120,
    "atr_period": 14,
    "stop_atr": 3.0,
    "trail_atr": 5.0,
    "risk_per_trade": 0.005,     # 0.5% of equity risked per trade
    "fee_per_side": 0.0004,      # 0.04% taker fee
    "slippage_per_side": 0.0002,  # 0.02% assumed slippage
    "start_equity": 10_000.0,
    "max_leverage": 3.0,
}


# ----------------------------------------------------------------------------- data
def synthetic_candles(n: int = 40_000, seed: int = 7, start_price: float = 30_000.0):
    """Generate OHLC candles with alternating trend and range regimes (synthetic, for demonstration only)."""
    rnd = random.Random(seed)
    price = start_price
    out = []
    regime_left = 0
    drift = 0.0
    vol = 0.0015
    for i in range(n):
        if regime_left <= 0:
            regime_left = rnd.randint(300, 1800)
            kind = rnd.random()
            if kind < 0.40:
                drift = rnd.uniform(0.00004, 0.00011)       # uptrend
            elif kind < 0.65:
                drift = -rnd.uniform(0.00004, 0.00010)      # downtrend
            else:
                drift = 0.0                                  # range
            vol = rnd.uniform(0.0009, 0.0022)
        regime_left -= 1
        o = price
        c = o * math.exp(drift + rnd.gauss(0, vol))
        hi = max(o, c) * (1 + abs(rnd.gauss(0, vol * 0.45)))
        lo = min(o, c) * (1 - abs(rnd.gauss(0, vol * 0.45)))
        out.append((o, hi, lo, c))
        price = c
    return out


# ----------------------------------------------------------------------------- indicators
def ema(values, period):
    k = 2 / (period + 1)
    out, e = [], values[0]
    for v in values:
        e = v * k + e * (1 - k)
        out.append(e)
    return out


def atr(candles, period):
    trs, out, prev_close = [], [], candles[0][3]
    a = None
    for o, h, l, c in candles:
        tr = max(h - l, abs(h - prev_close), abs(l - prev_close))
        a = tr if a is None else (a * (period - 1) + tr) / period
        out.append(a)
        prev_close = c
    return out


# ----------------------------------------------------------------------------- strategy
def signal(i, candles, closes, ema_t, highs, lows, cfg=CONFIG):
    """Return +1 (long), -1 (short) or 0 for the candle that just closed at index i."""
    n = cfg["breakout_lookback"]
    if i < max(cfg["ema_trend"], n) + 1:
        return 0
    prior_high = max(highs[i - n:i])
    prior_low = min(lows[i - n:i])
    if closes[i] > prior_high and closes[i] > ema_t[i]:
        return 1
    if closes[i] < prior_low and closes[i] < ema_t[i]:
        return -1
    return 0


# ----------------------------------------------------------------------------- broker and backtest
@dataclass
class Trade:
    side: int
    entry_i: int
    entry: float
    exit_i: int = 0
    exit: float = 0.0
    qty: float = 0.0
    pnl: float = 0.0
    reason: str = ""


@dataclass
class Result:
    equity: list = field(default_factory=list)
    trades: list = field(default_factory=list)


def backtest(candles, cfg=CONFIG):
    closes = [c[3] for c in candles]
    highs = [c[1] for c in candles]
    lows = [c[2] for c in candles]
    ema_t = ema(closes, cfg["ema_trend"])
    atr_v = atr(candles, cfg["atr_period"])
    eq = cfg["start_equity"]
    res = Result()
    pos = None
    stop = trail_ref = 0.0
    cost = cfg["fee_per_side"] + cfg["slippage_per_side"]
    for i in range(len(candles)):
        o, h, l, c = candles[i]
        if pos is not None:
            # stop check first (conservative: stops fill before any favorable move in the same candle)
            hit = (pos.side == 1 and l <= stop) or (pos.side == -1 and h >= stop)
            if hit:
                fill = stop * (1 - cost * pos.side)
                pos.exit, pos.exit_i, pos.reason = fill, i, "stop"
            else:
                trail_ref = max(trail_ref, c) if pos.side == 1 else min(trail_ref, c)
                new_stop = trail_ref - pos.side * cfg["trail_atr"] * atr_v[i]
                stop = max(stop, new_stop) if pos.side == 1 else min(stop, new_stop)
            if pos.exit_i:
                pos.pnl = pos.side * (pos.exit - pos.entry) * pos.qty
                eq += pos.pnl
                res.trades.append(pos)
                pos = None
        if pos is None:
            s = signal(i, candles, closes, ema_t, highs, lows, cfg)
            if s != 0 and i + 1 < len(candles):
                entry = candles[i + 1][0] * (1 + cost * s)     # enter at next open
                dist = cfg["stop_atr"] * atr_v[i]
                qty = (eq * cfg["risk_per_trade"]) / dist
                qty = min(qty, eq * cfg["max_leverage"] / entry)
                pos = Trade(side=s, entry_i=i + 1, entry=entry, qty=qty)
                stop = entry - s * dist
                trail_ref = entry
        mtm = eq + (pos.side * (c - pos.entry) * pos.qty if pos else 0.0)
        res.equity.append(mtm)
    return res


def metrics(res, cfg=CONFIG, bars_per_year=None):
    bars_per_year = bars_per_year or (365 * 24 * 60 / cfg["timeframe_minutes"])
    eq = res.equity
    start, end = eq[0], eq[-1]
    peak, mdd = eq[0], 0.0
    for v in eq:
        peak = max(peak, v)
        mdd = max(mdd, (peak - v) / peak)
    rets = [eq[i] / eq[i - 1] - 1 for i in range(1, len(eq))]
    mean = sum(rets) / len(rets)
    sd = math.sqrt(sum((r - mean) ** 2 for r in rets) / len(rets)) or 1e-12
    wins = [t.pnl for t in res.trades if t.pnl > 0]
    losses = [t.pnl for t in res.trades if t.pnl <= 0]
    years = len(eq) / bars_per_year
    return {
        "trades": len(res.trades),
        "net_return": end / cfg["start_equity"] - 1,
        "cagr": (end / cfg["start_equity"]) ** (1 / years) - 1 if years > 0 and end > 0 else -1,
        "max_drawdown": mdd,
        "win_rate": len(wins) / max(1, len(res.trades)),
        "profit_factor": (sum(wins) / abs(sum(losses))) if losses and sum(losses) != 0 else float("inf"),
        "avg_win": sum(wins) / max(1, len(wins)),
        "avg_loss": sum(losses) / max(1, len(losses)),
        "sharpe": mean / sd * math.sqrt(bars_per_year),
    }


# ----------------------------------------------------------------------------- live helpers (not run in this sample)
def send_telegram(token: str, chat_id: str, text: str):
    """Send an alert. Needs the requests package and a bot token. Not called in paper mode."""
    import requests
    return requests.post(f"https://api.telegram.org/bot{token}/sendMessage", json={"chat_id": chat_id, "text": text}, timeout=10)


class BinanceTestnetClient:
    """Stub showing where exchange calls would go. Use trade-only API keys with withdrawals disabled."""
    def __init__(self, api_key: str, api_secret: str):
        self.api_key, self.api_secret = api_key, api_secret

    def place_order(self, symbol: str, side: str, qty: float, stop: float):
        raise NotImplementedError("Wire this to the exchange SDK after paper trading is stable.")


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--backtest", action="store_true")
    ap.add_argument("--bars", type=int, default=40_000)
    args = ap.parse_args()
    if args.backtest:
        candles = synthetic_candles(args.bars)
        res = backtest(candles)
        m = metrics(res)
        for k, v in m.items():
            print(f"{k:>14}: {v:.4f}" if isinstance(v, float) else f"{k:>14}: {v}")


if __name__ == "__main__":
    main()
