"""
「評価2フェーズ（静的DD）」と「インスタント（トレーリングDD）」はどちらが買いか。

同じ実力・同じロット・同じ期間で、プログラムのルールだけを変えて回す。
出力の主役は「損益分岐となる参加費」＝この口座に払っていい上限額。
実売価格がこの金額より安ければ買い、高ければ割高、という読み方をする。

--------------------------------------------------------------------------
モデル
--------------------------------------------------------------------------
  1トレード = 勝率 p で +R×r、負けで -r（r は1トレードのリスク、初期残高比）。
  評価フェーズは目標に到達したら次へ進み、残高は初期残高にリセット。
  ファンド段階に到達したらそこから期末までの利益に分配率を掛けたものが受取額。
  途中で失格したらその時点で終了（再挑戦しない）。

  期間は全プログラム共通の TOTAL_DAYS 営業日。
  インスタントは初日からファンド、評価型は通過に使った日数のぶん出遅れる。
  これを揃えないと「評価を通す時間」のコストが消えてしまう。

使い方: python scripts/sim-phases-vs-instant.py
"""

from dataclasses import dataclass, field

import numpy as np

SEED = 20260912
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: 最高エクイティ追従（初期残高でロック）
    daily_loss: float | None = None
    daily_profit_cap: float | None = None
    phases: list = field(default_factory=list)   # 各評価フェーズの利益目標。空ならインスタント


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

    eq = np.ones(n)
    hwm = np.ones(n)
    alive = np.ones(n, dtype=bool)
    stage = np.zeros(n, dtype=int)          # 0..len(phases)-1 が評価、len(phases) がファンド
    funded_at = np.full(n, -1)
    n_phases = len(prog.phases)
    targets = np.array(prog.phases + [np.inf]) if n_phases else np.array([np.inf])

    for d in range(total_days):
        day = np.zeros(n)
        for _ in range(trades_per_day):
            can = alive.copy()
            if prog.daily_profit_cap is not None:
                can &= day < prog.daily_profit_cap
            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:
                hit = can & (day <= -prog.daily_loss)
                alive &= ~hit

            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)
                    funded_at = np.where(passed & (stage == n_phases) & (funded_at < 0), d + 1, funded_at)

    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,
        win_rate=(payout > prog.fee_pct * ACCOUNT).mean(),
    )


# 価格はすべてクーポン適用後で揃える（2026-09-12に各社の公式画面で実測）。参加費は口座サイズ比。
#   Moneta Instant Pro    $100K $5,300 → $2,650  TOKONATSU50 50%OFF      → 2.650%
#   Moneta 2-Step 5%/10%  $100K $950   → $475    TOKONATSU50 50%OFF      → 0.475%
#   Fintokei ProTrader    サファイヤ 2,000万円 ¥109,800 → ¥93,330  15%OFF → 0.467%
PROGRAMS = [
    Program("Moneta Instant Pro", 0.02650, 0.88, 0.08, True,
            daily_loss=0.04, daily_profit_cap=0.04, phases=[]),
    Program("Moneta 2-Step", 0.00475, 0.88, 0.10, False,
            daily_loss=0.05, phases=[0.05, 0.10]),
    Program("Fintokei ProTrader", 0.00467, 0.80, 0.10, False,
            daily_loss=0.05, phases=[0.08, 0.06]),
]


def table(progs, p, R, r, label):
    print()
    print("=" * 112)
    print(f"{label}   勝率{p*100:.0f}% / RR{R:.2f} / 1トレード{r*100:.2f}% / {TOTAL_DAYS}営業日")
    print("=" * 112)
    print(f"  {'プログラム':28} {'参加費':>9} {'ファンド到達':>13} {'失格率':>9} "
          f"{'到達日数':>10} {'分岐参加費':>12} {'差引':>10} {'倍率':>9} {'判定':>10}")
    for prog in progs:
        x = run(prog, p, R, r)
        verdict = "買ってよい" if x["net_pct"] > 0 else "割高"
        day = f"{x['mean_funded_day']:.0f}日" if not np.isnan(x["mean_funded_day"]) else "初日"
        print(f"  {x['name']:28} {x['fee_pct']*100:8.3f}% {x['reach_funded']*100:12.1f}% "
              f"{x['dead']*100:8.1f}% {day:>10} {x['payout_pct']*100:11.3f}% {x['net_pct']*100:+9.3f}% "
              f"{x['payout_pct']/x['fee_pct']:8.2f}倍 {verdict:>10}")


if __name__ == "__main__":
    print(f"モンテカルロ {N_PATHS:,}本 / 1日{TRADES_PER_DAY}トレード / 参加費・分岐はすべて口座サイズ比")
    print("※ 同一銘柄の含み損トリガー・最低利益日数・手数料（スプレッド等）は未実装")

    table(PROGRAMS, 0.50, 1.00, 0.010, "【エッジゼロ】")
    table(PROGRAMS, 0.50, 1.20, 0.010, "【小さなエッジ】")
    table(PROGRAMS, 0.55, 1.20, 0.010, "【そこそこのエッジ】")
    table(PROGRAMS, 0.60, 1.20, 0.010, "【強いエッジ】")
    print()
