"""
E8 Pro は買っていいのか。「1日の利益上限2%」が何を奪っているかを数える。

E8 Pro は静的DD・一貫性なし・最低取引日数なし・出金毎日可という、
プロップの面倒な条項をほぼ全部外した構成になっている。
そのかわりに他社にほとんど無い制約が1つだけ入っている ——
**1日に増やせる残高は初期残高の2%まで。超えた分は翌日0〜1時に口座から削除される**
（公式ヘルプ Daily profit cap の明記。"Exceeds profits above 2% are being removed every day"）。

つまり利益は毎日切り落とされ、損失は切り落とされない。
この非対称が実力にどう効くのかを、同じ乱数列で並走させて測る。

比較対象は「同じ$366で買えるもの」に揃える（$100K・コード E8 で25%OFF・2026-09-13実測）:
    E8 Pro    8%静的DD  / 目標8%  / 日次2.5% / 利益上限2% → $488 → $366
    E8 One    6%動的DD  / 目標9%  / 日次4.0% / 上限なし   → $488 → $366

使い方: python scripts/sim-e8-pro.py
"""

from dataclasses import dataclass, field

import numpy as np

SEED = 20260913
N_PATHS = 40_000
TOTAL_DAYS = 120          # 約6か月
TRADES_PER_DAY = 3
ACCOUNT = 100_000.0


@dataclass
class Program:
    name: str
    fee_pct: float                   # 参加費は口座サイズ比（通貨・サイズに依存しないため）
    split: float
    max_dd: float                    # 初期残高比
    trailing: bool                   # True: 確定残高の最高値に追従し、初期残高でロック
    target: float                    # 評価フェーズの利益目標（1フェーズ）
    daily_loss: float | None = None
    daily_profit_cap: float | None = None   # 超過分は日をまたぐ時点で残高から削除される
    phases: list = field(default_factory=list)   # 2フェーズ以上のとき使う。空なら target を使う
    stop_at_cap: bool = True         # その日の残り枠を超える利確は狙わない（規律ある運用）


def run(prog, p, R, r, total_days=TOTAL_DAYS, trades_per_day=TRADES_PER_DAY, seed=SEED):
    rng = np.random.default_rng(seed)
    n = N_PATHS

    phases = prog.phases if prog.phases else ([prog.target] if prog.target else [])
    n_phases = len(phases)
    targets = np.array(phases + [np.inf]) if n_phases else np.array([np.inf])

    eq = np.ones(n)
    hwm = np.ones(n)
    alive = np.ones(n, dtype=bool)
    stage = np.zeros(n, dtype=int)
    funded_at = np.full(n, -1)
    cut_total = np.zeros(n)            # 利益上限で削除された累計
    cut_days = np.zeros(n)             # 削除が起きた日数

    for d in range(total_days):
        day_open = eq.copy()
        day = np.zeros(n)
        for _ in range(trades_per_day):
            can = alive.copy()
            # 規律ある運用: その日の残り枠より利確幅が大きいなら、勝っても削られるので建てない
            if prog.daily_profit_cap is not None and prog.stop_at_cap:
                can &= (day + R * r) <= prog.daily_profit_cap + 1e-12
            step = np.where(rng.random(n) < p, R * r, -r)
            eq = eq + step * can
            day = day + step * can

            if prog.daily_loss is not None:
                alive &= ~(can & (day <= -prog.daily_loss))

            if prog.trailing:
                hwm = np.maximum(hwm, eq)
            floor = np.minimum(hwm - prog.max_dd, 1.0) if prog.trailing else np.full(n, 1.0 - prog.max_dd)
            alive &= ~(alive & (eq <= floor))

            if n_phases:
                tgt = targets[np.minimum(stage, n_phases)]
                passed = alive & (stage < n_phases) & (eq >= 1.0 + tgt)
                if passed.any():
                    stage = np.where(passed, stage + 1, stage)
                    eq = np.where(passed, 1.0, eq)
                    hwm = np.where(passed, 1.0, hwm)
                    day = np.where(passed, 0.0, day)
                    day_open = np.where(passed, 1.0, day_open)
                    funded_at = np.where(passed & (stage == n_phases) & (funded_at < 0), d + 1, funded_at)

        # 日をまたぐ時点で、上限を超えた分は口座から削除される
        if prog.daily_profit_cap is not None:
            limit = day_open + prog.daily_profit_cap
            over = alive & (eq > limit)
            cut_total += np.where(over, eq - limit, 0.0)
            cut_days += over
            eq = np.where(over, limit, eq)

    funded = alive & (stage == n_phases)
    profit = np.where(funded, np.maximum(eq - 1.0, 0.0), 0.0)
    payout = profit * ACCOUNT * prog.split
    return dict(
        name=prog.name,
        fee_pct=prog.fee_pct,
        dead=(~alive).mean(),
        reach_funded=(stage == n_phases).mean(),
        payout_pct=payout.mean() / ACCOUNT,    # ＝損益分岐となる参加費（口座サイズ比）
        net_pct=payout.mean() / ACCOUNT - prog.fee_pct,
        mean_funded_day=np.nanmean(np.where(funded_at >= 0, funded_at, np.nan)) if (funded_at >= 0).any() else np.nan,
        cut_pct=cut_total.mean(),              # 削除された利益の平均（口座サイズ比）
        cut_days=cut_days.mean(),
        win_rate=(payout > prog.fee_pct * ACCOUNT).mean(),
    )


# 価格はすべて2026-09-13にチェックアウト／料金シミュレーターで実測、$100K・クーポン適用後。
#   E8 Pro  8%DD /分配80%   $488 → $366（コード E8 25%OFF）
#   E8 Pro  8%DD /分配100%  $586 → $440
#   E8 Pro 10%DD /分配80%   $539 → $405
#   E8 One  6%DD /分配80%   $488 → $366
#   E8 One  8%DD /分配80%   $586 → $440
#   Moneta 2-Step 5%/10%    $950 → $475（TOKONATSU50 50%OFF）
#   Moneta Instant Pro    $5,300 → $2,650（同）
E8_PRO_8   = Program("E8 Pro 8%DD",       0.00366, 0.80, 0.08, False, 0.08, daily_loss=0.025, daily_profit_cap=0.02)
E8_PRO_10  = Program("E8 Pro 10%DD",      0.00405, 0.80, 0.10, False, 0.10, daily_loss=0.025, daily_profit_cap=0.02)
E8_PRO_100 = Program("E8 Pro 分配100%",   0.00440, 1.00, 0.08, False, 0.08, daily_loss=0.025, daily_profit_cap=0.02)
E8_ONE_6   = Program("E8 One 6%DD",       0.00366, 0.80, 0.06, True,  0.09, daily_loss=0.040)
E8_ONE_8   = Program("E8 One 8%DD",       0.00440, 0.80, 0.08, True,  0.12, daily_loss=0.053)
MONETA_2S  = Program("Moneta 2-Step",     0.00475, 0.88, 0.10, False, 0.0,  daily_loss=0.050, phases=[0.05, 0.10])
MONETA_IP  = Program("Moneta Instant Pro", 0.02650, 0.88, 0.08, True,  0.0,  daily_loss=0.040, daily_profit_cap=0.04)

MAIN = [E8_PRO_8, E8_ONE_6, MONETA_2S, MONETA_IP]
SKILLS = [(0.50, 1.00, "【エッジゼロ】勝率50% RR1.0"),
          (0.50, 1.20, "【小さなエッジ】勝率50% RR1.2"),
          (0.55, 1.20, "【そこそこ】勝率55% RR1.2"),
          (0.60, 1.20, "【強い】勝率60% RR1.2")]
RISK = 0.008              # E8 Pro の日次2.5%に3トレードで収まる最大水準


def table(progs, p, R, r, label):
    print()
    print("=" * 118)
    print(f"{label}   1トレード{r*100:.2f}% / 1日{TRADES_PER_DAY}回 / {TOTAL_DAYS}営業日")
    print("=" * 118)
    print(f"  {'プログラム':22} {'参加費':>8} {'ファンド到達':>12} {'失格率':>8} {'到達':>7} "
          f"{'削られた利益':>12} {'払っていい上限':>13} {'差引':>9} {'倍率':>9} {'判定':>9}")
    for prog in progs:
        x = run(prog, p, R, r)
        day = f"{x['mean_funded_day']:.0f}日" if not np.isnan(x["mean_funded_day"]) else "初日"
        cut = f"{x['cut_pct']*100:11.2f}%" if prog.daily_profit_cap else f"{'-':>12}"
        print(f"  {x['name']:22} {x['fee_pct']*100:7.3f}% {x['reach_funded']*100:11.1f}% "
              f"{x['dead']*100:7.1f}% {day:>7} {cut} {x['payout_pct']*100:12.3f}% "
              f"{x['net_pct']*100:+8.3f}% {x['payout_pct']/x['fee_pct']:8.2f}倍 "
              f"{'買ってよい' if x['net_pct'] > 0 else '割高':>9}")

def perf_stage(p, R, r, thresh, split=0.80, max_dd=0.08, daily=0.025, cap=0.02,
               total_days=TOTAL_DAYS, trades_per_day=TRADES_PER_DAY, seed=SEED):
    """ファンド段階だけを取り出す。出金すると静的DDが初期残高へ上がる影響を見る。

    thresh=None なら請求しない。数値なら「初期残高比でその％たまったら半分を請求」。
    公式ヘルプ: 請求額は(残高 - 初期残高)の50%、残り50%は口座に残る。
    そして初回請求の時点で静的DDのラインが初期残高へ恒久的に移動する。
    """
    rng = np.random.default_rng(seed)
    n = N_PATHS
    eq = np.ones(n)
    alive = np.ones(n, dtype=bool)
    paid = np.zeros(n)
    floor = np.full(n, 1.0 - max_dd)
    ever = np.zeros(n, dtype=bool)

    for d in range(total_days):
        op = eq.copy()
        day = np.zeros(n)
        for _ in range(trades_per_day):
            can = alive & ((day + R * r) <= cap + 1e-12)
            step = np.where(rng.random(n) < p, R * r, -r)
            eq = eq + step * can
            day = day + step * can
            alive &= ~(can & (day <= -daily))
            alive &= ~(alive & (eq <= floor))
        over = alive & (eq > op + cap)
        eq = np.where(over, op + cap, eq)
        if thresh is not None:
            req = alive & ((eq - 1.0) >= thresh)
            amt = np.where(req, 0.5 * (eq - 1.0), 0.0)
            paid += amt * split
            eq = eq - amt
            ever |= req
            floor = np.where(ever, 1.0, 1.0 - max_dd)

    left = np.where(alive, np.maximum(eq - 1.0, 0.0), 0.0) * split
    return dict(dead=(~alive).mean(), paid=paid.mean(), left=left.mean(),
                total=(paid + left).mean())


def section_a():
    for p, R, lab in SKILLS:
        table(MAIN, p, R, RISK, lab)


def section_b():
    print()
    print("=" * 100)
    print("B. 「1日の利益上限2%」をどう扱うかで結果がどれだけ変わるか（E8 Pro 8%DD・リスク0.8%）")
    print("=" * 100)
    print(f"  {'実力':14} {'(1)上限が無いと':>14} {'(2)枠を意識する':>15} {'(3)気にせず回す':>15} "
          f"{'(3)/(2)':>9} {'(3)で削除':>10} {'(2)失格':>8} {'(3)失格':>8}")
    import dataclasses
    for p, R, lab in SKILLS:
        lab2 = lab.split("】")[0].replace("【", "")
        a = run(dataclasses.replace(E8_PRO_8, daily_profit_cap=None), p, R, RISK)
        b = run(E8_PRO_8, p, R, RISK)
        c = run(dataclasses.replace(E8_PRO_8, stop_at_cap=False), p, R, RISK)
        print(f"  {lab2:14} {a['payout_pct']*100:13.3f}% {b['payout_pct']*100:14.3f}% {c['payout_pct']*100:14.3f}% "
              f"{(c['payout_pct']/b['payout_pct']-1)*100:+8.1f}% {c['cut_pct']*100:9.2f}% "
              f"{b['dead']*100:7.1f}% {c['dead']*100:7.1f}%")


def section_c():
    print()
    print("=" * 100)
    print("C. 1トレードのリスクをどこまで上げられるか（勝率50%・RR1.2・1日3トレード固定）")
    print("   日次2.5%は初期残高基準。3連敗で2.5%を超えるサイズは机上で即アウトになる")
    print("=" * 100)
    print(f"  {'リスク/回':>9} {'3連敗':>8} | {'E8Pro失格':>10} {'上限額':>9} {'倍率':>8} "
          f"| {'E8One失格':>10} {'上限額':>9} {'倍率':>8}")
    for r in (0.0025, 0.0040, 0.0050, 0.0065, 0.0080, 0.0083, 0.0090):
        a = run(E8_PRO_8, 0.50, 1.20, r)
        b = run(E8_ONE_6, 0.50, 1.20, r)
        print(f"  {r*100:8.2f}% {r*3*100:7.2f}% | {a['dead']*100:9.1f}% {a['payout_pct']*100:8.3f}% "
              f"{a['payout_pct']/a['fee_pct']:7.2f}倍 | {b['dead']*100:9.1f}% {b['payout_pct']*100:8.3f}% "
              f"{b['payout_pct']/b['fee_pct']:7.2f}倍")


def section_d():
    print()
    print("=" * 100)
    print("D. 構成の選び方（リスク0.8%・数字は倍率＝上限額÷参加費）")
    print("=" * 100)
    print(f"  {'構成':18} {'参加費':>8} " + " ".join(f"{l.split('】')[0].replace('【',''):>12}" for _, _, l in SKILLS))
    for prog in (E8_PRO_8, E8_PRO_10, E8_PRO_100, E8_ONE_6, E8_ONE_8):
        row = []
        for p, R, _ in SKILLS:
            x = run(prog, p, R, RISK)
            row.append(f"{x['payout_pct']/x['fee_pct']:11.1f}倍")
        print(f"  {prog.name:18} {prog.fee_pct*100:7.3f}% " + " ".join(row))


def section_e():
    print()
    print("=" * 100)
    print("E. 参加費の元が取れる勝率はどこか（RR1.0・リスク0.8%）")
    print("=" * 100)
    print(f"  {'勝率':>6} {'E8 Pro 上限額':>14} {'倍率':>9} | {'E8 One 上限額':>14} {'倍率':>9}")
    for p in (0.46, 0.47, 0.48, 0.49, 0.50, 0.52, 0.55):
        a = run(E8_PRO_8, p, 1.00, RISK)
        b = run(E8_ONE_6, p, 1.00, RISK)
        print(f"  {p*100:5.0f}% {a['payout_pct']*100:13.3f}% {a['payout_pct']/a['fee_pct']:8.2f}倍 "
              f"| {b['payout_pct']*100:13.3f}% {b['payout_pct']/b['fee_pct']:8.2f}倍")


def section_f():
    print()
    print("=" * 100)
    print("F. 出金の方針で何が変わるか（ファンド段階のみ120営業日・$100K・分配80%・リスク0.8%）")
    print("   初回請求の時点で静的DDのラインが初期残高へ動く。残る50%だけが命綱になる")
    print("=" * 100)
    print(f"  {'実力':14} {'出金の方針':18} {'失格率':>8} {'受取済み':>11} {'口座に残る':>11} {'合計':>11}")
    for p, R, lab in SKILLS:
        lab2 = lab.split("】")[0].replace("【", "")
        for th, tl in ((0.01, "1%で請求(最短)"), (0.02, "2%たまったら"), (0.04, "4%たまったら"),
                       (0.08, "8%たまったら"), (None, "請求しない")):
            x = perf_stage(p, R, RISK, th)
            print(f"  {lab2:14} {tl:18} {x['dead']*100:7.1f}% ${x['paid']*ACCOUNT:10,.0f} "
                  f"${x['left']*ACCOUNT:10,.0f} ${x['total']*ACCOUNT:10,.0f}")
        print()


if __name__ == "__main__":
    print(f"モンテカルロ {N_PATHS:,}本。参加費・上限額はすべて口座サイズ比（$100Kなら1%＝$1,000）")
    print("※ スプレッド・スワップ・スリッページは未実装")
    section_a()
    section_b()
    section_c()
    section_d()
    section_e()
    section_f()
