#!/usr/bin/env python3
"""Per-pair notional cap and the wallet size it implies, from 5m quote volume.

Answers "how large can the account get before market impact stops being
negligible", which the OHLC backtest cannot model at all (it fills any size at
the requested price).

Sizing chain (RiskCap, see risk_cap_math.risk_capped_stake):

    margin  = wallet * risk_fraction / max(atr_loss_fraction, hard_stop_fraction)
    notional = margin * leverage

`atr_loss_fraction >= hard_stop_fraction` always holds after the RiskCap floor,
so the largest possible notional is at the floor:

    max_notional = wallet * risk_fraction / hard_stop_fraction * leverage

The `max_stake_wallet_fraction` cap is checked and reported when it would bind.

Participation anchor: bot1's measured entry fill takes ~6.6s median / 39.8s P90
of a 300s bar, i.e. ~13% of the bar at P90. Taking 1% of a whole bar's quote
volume is therefore ~10% participation inside the actual fill window, the usual
conservative ceiling. Default --participation 0.01.

Two windows, and they disagree by ~5x:

* unconditional (default) — the quiet-percentile bar of the whole period. This
  is the "could enter at any moment" bound, and it is too pessimistic here: the
  strategy only enters on a Donchian48 break with ATR expansion, never on a
  dead bar.
* ``--entries`` — the bars the bot actually entered on, read from the trades
  database. This is the decision-grade number. Prefer it, and fall back to the
  unconditional bound only where the entry sample is too thin to trust.

Caveat: this bounds the *entry* limit order only. The exchange-side stop-market
exit consumes book depth in one shot; depth is not in OHLCV and can only be
measured with real orders or order-book snapshots.

Run inside the freqtrade container (the host has no pandas):

    ssh ubuntu@43.164.75.52 'docker exec -i freqtrade python3 -' \
      < freqtrade/scripts/liquidity_capacity.py
"""
from __future__ import annotations

import argparse
import json
import sqlite3
from datetime import datetime, timedelta, timezone
from pathlib import Path

import pandas as pd

# Defaults mirror VolatilityBreakout (v5_d48.py) — override only to test sensitivity.
RISK_FRACTION = 0.0075
HARD_STOP_FRACTION = 0.05
WALLET_CAP_FRACTION = 0.18
LEVERAGE = 2.0
# Low-liquidity percentile: sizing must survive quiet hours, not the average bar.
QUIET_PERCENTILE = 0.10
MIN_BARS = 2_000


def max_notional_wallet_fraction(
    risk_fraction: float,
    hard_stop_fraction: float,
    leverage: float,
    wallet_cap_fraction: float,
) -> tuple[float, bool]:
    """Largest notional per trade as a fraction of wallet, and whether the wallet cap binds."""
    risk_margin = risk_fraction / hard_stop_fraction
    wallet_cap_binds = wallet_cap_fraction < risk_margin
    return min(risk_margin, wallet_cap_fraction) * leverage, wallet_cap_binds


def quote_volume(frame: pd.DataFrame) -> pd.Series:
    """Feather volume is base currency; impact scales with quote notional."""
    return frame["volume"] * frame["close"]


def pair_to_feather(data_dir: Path, pair: str) -> Path:
    base, quote = pair.split(":")[0].split("/")
    return data_dir / f"{base}_{quote}_{quote}-5m-futures.feather"


def pair_capacity(data_dir: Path, pair: str, days: int) -> dict | None:
    path = pair_to_feather(data_dir, pair)
    if not path.exists():
        return None
    frame = pd.read_feather(path)
    frame["date"] = pd.to_datetime(frame["date"], utc=True)
    cutoff = frame["date"].max() - timedelta(days=days)
    recent = frame[frame["date"] >= cutoff]
    if len(recent) < MIN_BARS:
        return None
    qv = quote_volume(recent)
    return {
        "pair": pair,
        "bars": len(recent),
        "until": recent["date"].max(),
        "qv_quiet": float(qv.quantile(QUIET_PERCENTILE)),
        "qv_median": float(qv.median()),
    }


def floor5(value: datetime) -> datetime:
    return value.replace(minute=(value.minute // 5) * 5, second=0, microsecond=0)


def entry_bar_capacity(data_dir: Path, db: Path) -> list[dict]:
    """Quote volume on the bars the bot actually entered on, grouped by pair."""
    con = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
    trades = con.execute("SELECT pair, open_date FROM trades ORDER BY open_date").fetchall()
    con.close()
    series: dict[str, pd.Series] = {}
    samples: dict[str, list[float]] = {}
    for pair, open_date in trades:
        if pair not in series:
            path = pair_to_feather(data_dir, pair)
            if not path.exists():
                series[pair] = pd.Series(dtype=float)
            else:
                frame = pd.read_feather(path)
                frame["date"] = pd.to_datetime(frame["date"], utc=True)
                series[pair] = pd.Series(quote_volume(frame).values, index=frame["date"])
        bar = floor5(datetime.fromisoformat(str(open_date)).replace(tzinfo=timezone.utc))
        if bar in series[pair].index:
            samples.setdefault(pair, []).append(float(series[pair].loc[bar]))
    return [
        {
            "pair": pair,
            "bars": len(values),
            "qv_quiet": min(values),
            "qv_median": sorted(values)[len(values) // 2],
        }
        for pair, values in samples.items()
    ]


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--root", default="/freqtrade/user_data")
    parser.add_argument("--days", type=int, default=90)
    parser.add_argument(
        "--entries",
        action="store_true",
        help="condition on the bars the bot actually entered on (reads --db); decision-grade",
    )
    parser.add_argument("--db", default=None, help="trades db for --entries (default tradesv3-v5.sqlite)")
    parser.add_argument(
        "--participation",
        type=float,
        action="append",
        default=None,
        help="notional as a fraction of one 5m bar's quote volume; repeatable (default 0.005/0.01/0.02)",
    )
    parser.add_argument("--risk-fraction", type=float, default=RISK_FRACTION)
    parser.add_argument("--hard-stop", type=float, default=HARD_STOP_FRACTION)
    parser.add_argument("--wallet-cap", type=float, default=WALLET_CAP_FRACTION)
    parser.add_argument("--leverage", type=float, default=LEVERAGE)
    args = parser.parse_args()

    rates = args.participation or [0.005, 0.01, 0.02]
    root = Path(args.root)
    data_dir = root / "data" / "binance" / "futures"
    pairs = json.loads((root / "config.json").read_text())["exchange"]["pair_whitelist"]

    fraction, cap_binds = max_notional_wallet_fraction(
        args.risk_fraction, args.hard_stop, args.leverage, args.wallet_cap
    )
    print(
        f"单笔最大名义 = {fraction * 100:.1f}% 钱包 "
        f"(risk {args.risk_fraction:.4%} / 硬止损 {args.hard_stop:.0%} × {args.leverage:g}x)"
    )
    print(
        f"max_stake_wallet_fraction={args.wallet_cap:.0%} "
        f"{'仍会绑定' if cap_binds else '恒不绑定（风险定仓先封顶）'}"
    )

    if args.entries:
        db = Path(args.db) if args.db else root / "tradesv3-v5.sqlite"
        rows = entry_bar_capacity(data_dir, db)
        low_label, mid_label = "入场bar最低", "入场bar中位"
        caption = f"\n实际入场 K 线成交额（USDT/根），n={sum(r['bars'] for r in rows)} 笔，来源 {db.name}"
    else:
        rows = []
        for pair in pairs:
            row = pair_capacity(data_dir, pair, args.days)
            if row is None:
                print(f"  !! {pair} feather 缺失或样本不足，跳过")
                continue
            rows.append(row)
        low_label, mid_label = f"P{QUIET_PERCENTILE:.0%} 成交额", "中位成交额"
        caption = (
            f"\n近 {args.days} 天 5m 成交额（USDT/根），{low_label} = 清淡时段基准；"
            f"数据止于 {max(r['until'] for r in rows):%Y-%m-%d}"
            if rows
            else ""
        )
    if not rows:
        print("no pairs with usable data")
        return

    print(caption)
    header = f"{'pair':<18}{'笔数':>6}{low_label:>14}{mid_label:>14}" + "".join(
        f"{'钱包上限@' + format(r, '.1%'):>18}" for r in rates
    )
    print(header)
    print("-" * len(header))
    for row in sorted(rows, key=lambda r: r["qv_quiet"]):
        line = (
            f"{row['pair']:<18}{row['bars']:>6}"
            f"{row['qv_quiet']:>14,.0f}{row['qv_median']:>14,.0f}"
        )
        for rate in rates:
            line += f"{row['qv_quiet'] * rate / fraction:>18,.0f}"
        print(line)

    binding = min(rows, key=lambda r: r["qv_quiet"])
    print(f"\n约束对: {binding['pair']}（{low_label} 最小，n={binding['bars']}）")
    for rate in rates:
        print(
            f"  参与率 {rate:.1%} 一根 5m -> 单笔名义上限 "
            f"{binding['qv_quiet'] * rate:,.0f} USDT -> 账户上限 "
            f"{binding['qv_quiet'] * rate / fraction:,.0f} USDT"
        )
    print(
        "\n仅约束入场限价单。交易所端 stop-market 一次性吃盘口深度，"
        "深度不在 OHLCV 里，只能靠实单或订单簿快照测。"
    )


if __name__ == "__main__":
    main()
