"""
静的DD と トレーリングDD は何が違うのか。Moneta Instant Pro を題材にしたモンテカルロ。

同じ乱数列（＝まったく同じトレード列）を、ドローダウンのルールだけ変えて
2本同時に流す。これで「トレーリングであること自体が何を奪っているか」が分離できる。

--------------------------------------------------------------------------
比べる2つのルール（どちらも最大DDは8%）
--------------------------------------------------------------------------
  静的8%         失格ラインは初期残高 −8% に固定。口座がいくら伸びても動かない。
  トレーリング8%  失格ラインは「最高エクイティ −8%」。利益が乗るたびに上がる。
                 ただし初期残高に到達したらそこで永久ロックし、以後は動かない。
                 （＝ +8% まで伸ばせた時点で、静的8%と同じ性質の口座に変わる）

Moneta Instant Pro は後者。公式表記は「Max Loss (trailing) 8%」で、
トレーリングラインは初期残高を超えて上がらないと明記されている。

--------------------------------------------------------------------------
この検証に入れていないもの
--------------------------------------------------------------------------
  - 日次損失4%（トレーリングと静的の差を見る主題から外れるため）
  - 同一銘柄の含み損3%トリガー（トレード単位の離散モデルでは表現できない）
  1日の利益上限4%だけは、両方のルールに同じように効くので残してある。
  参加費は【5】でのみ扱う（損益分岐となる参加費を逆算するため）。

--------------------------------------------------------------------------
手法の表し方
--------------------------------------------------------------------------
  1トレード = 勝率 p で +R×r、負けで -r（r は1トレードのリスク、初期残高比）。
  期待値は (p*R - (1-p)) * r。期待値を固定して勝率とRRだけ振れば、
  差がどこから来ているのかを手法の"形"に帰着できる。

使い方: python scripts/sim-instant-pro-trailing.py
"""

import numpy as np

SEED = 20260912
N_PATHS = 40_000
N_DAYS = 60
TRADES_PER_DAY = 3

MAX_DD = 0.08
DAILY_PROFIT_CAP = 0.04

ACCOUNT = 100_000.0     # 【5】の金額換算用
SPLIT = 0.88            # Instant Pro の利益分配率


def run_pair(p, R, r, n_days=N_DAYS, trades_per_day=TRADES_PER_DAY,
             daily_profit_cap=DAILY_PROFIT_CAP, seed=SEED):
    """同じ乱数列で「トレーリング8%」と「静的8%」を並走させる。

    要点となる戻り値:
      only_trail_dead        静的なら生き残っていたのに、トレーリングだから死んだ割合
      lock_rate              トレーリング側が +8% に到達してラインが固定された割合
      dead_before_lock_share トレーリングの死のうち、ロック前に起きた割合
      payout_trail / payout_static
                             1口座あたりの期待受取額（失格は0、分配率を掛けた後）。
                             これがそのまま「損益分岐となる参加費」になる。
    """
    rng = np.random.default_rng(seed)

    eq_t = np.ones(N_PATHS)
    eq_s = np.ones(N_PATHS)
    hwm = np.ones(N_PATHS)
    alive_t = np.ones(N_PATHS, dtype=bool)
    alive_s = np.ones(N_PATHS, dtype=bool)
    locked = np.zeros(N_PATHS, dtype=bool)
    lock_day = np.full(N_PATHS, np.nan)
    dead_before_lock = np.zeros(N_PATHS, dtype=bool)
    static_floor = 1.0 - MAX_DD

    for d in range(n_days):
        day_t = np.zeros(N_PATHS)
        day_s = np.zeros(N_PATHS)
        for _ in range(trades_per_day):
            win = rng.random(N_PATHS) < p
            raw = np.where(win, R * r, -r)

            # 1日の利益上限は両方に同じように効かせる
            can_t = alive_t & (day_t < daily_profit_cap)
            can_s = alive_s & (day_s < daily_profit_cap)
            eq_t = eq_t + raw * can_t
            eq_s = eq_s + raw * can_s
            day_t = day_t + raw * can_t
            day_s = day_s + raw * can_s

            hwm = np.maximum(hwm, eq_t)
            floor_t = np.minimum(hwm - MAX_DD, 1.0)

            hit_t = alive_t & (eq_t <= floor_t)
            dead_before_lock |= hit_t & ~locked
            alive_t &= ~hit_t

            alive_s &= ~(alive_s & (eq_s <= static_floor))

            newly = alive_t & ~locked & (hwm >= 1.0 + MAX_DD)
            lock_day = np.where(newly, d + 1, lock_day)
            locked |= newly

    dead_t = ~alive_t
    dead_s = ~alive_s
    profit_t = np.where(alive_t, np.maximum(eq_t - 1.0, 0.0), 0.0)
    profit_s = np.where(alive_s, np.maximum(eq_s - 1.0, 0.0), 0.0)

    return dict(
        trail_dead=dead_t.mean(),
        static_dead=dead_s.mean(),
        only_trail_dead=(dead_t & alive_s).mean(),
        only_static_dead=(dead_s & alive_t).mean(),   # 理屈上ほぼ0（検算用）
        lock_rate=locked.mean(),
        mean_lock_day=np.nanmean(lock_day) if locked.any() else np.nan,
        dead_before_lock_share=(dead_before_lock.sum() / dead_t.sum()) if dead_t.any() else np.nan,
        mean_peak_of_trail_dead=np.nanmean(np.where(dead_t, hwm - 1.0, np.nan)) if dead_t.any() else np.nan,
        mean_trail=eq_t.mean(),
        mean_static=eq_s.mean(),
        payout_trail=profit_t.mean() * ACCOUNT * SPLIT,
        payout_static=profit_s.mean() * ACCOUNT * SPLIT,
    )


def sample_paths(p, R, r, n_paths=400, seed=SEED, n_days=N_DAYS,
                 trades_per_day=TRADES_PER_DAY):
    """グラフ用。同じトレード列に対する2つのDDラインと、それぞれの生死を返す。"""
    rng = np.random.default_rng(seed)
    steps = n_days * trades_per_day
    eq = np.ones((n_paths, steps + 1))
    fl = np.full((n_paths, steps + 1), 1.0 - MAX_DD)
    dead_t = np.full(n_paths, -1)
    dead_s = np.full(n_paths, -1)

    cur = np.ones(n_paths)
    hwm = np.ones(n_paths)
    at = np.ones(n_paths, dtype=bool)
    a_s = np.ones(n_paths, dtype=bool)
    k = 0
    for d in range(n_days):
        day = np.zeros(n_paths)
        for _ in range(trades_per_day):
            k += 1
            can = at & (day < DAILY_PROFIT_CAP)
            step = np.where(rng.random(n_paths) < p, R * r, -r)
            cur = cur + step * can
            day = day + step * can

            hwm = np.maximum(hwm, cur)
            floor = np.minimum(hwm - MAX_DD, 1.0)

            ht = at & (cur <= floor)
            dead_t = np.where(ht & (dead_t < 0), k, dead_t)
            at &= ~ht

            hs = a_s & (cur <= 1.0 - MAX_DD)
            dead_s = np.where(hs & (dead_s < 0), k, dead_s)
            a_s &= ~hs

            eq[:, k] = cur
            fl[:, k] = floor
    return eq, fl, dead_t, dead_s


SHAPES = [(0.30, 3.000), (0.40, 2.000), (0.50, 1.400),
          (0.60, 1.000), (0.70, 0.714), (0.80, 0.500), (0.90, 0.333)]

# 勝率50%の人のリスクリワード別（RR1.0 が期待値ゼロ）
RR50 = [1.00, 1.10, 1.20, 1.40, 1.60, 1.80, 2.00]


def header(title):
    print()
    print("=" * 112)
    print(title)
    print("=" * 112)


if __name__ == "__main__":
    print(f"モンテカルロ {N_PATHS:,}本 × {N_DAYS}営業日 × 1日{TRADES_PER_DAY}トレード")
    print("同じ乱数列に「トレーリング8%」と「静的8%」を並走させて比較")
    print("※ 日次損失4%・同一銘柄3%トリガーは入れていない。1日の利益上限4%は両方に同じく適用。")

    header("【1】同じ期待値（+0.20r）で、勝率とRRだけを変えた場合  ※リスク r=1.0%")
    print(f"  {'勝率':>6} {'RR':>7} {'失格(トレ)':>12} {'失格(静的)':>12} {'差':>9} "
          f"{'静的なら生きていた':>18} {'平均残高(トレ)':>15} {'平均残高(静的)':>15}")
    for p, R in SHAPES:
        x = run_pair(p, R, 0.010)
        print(f"  {p*100:5.0f}% {R:7.2f} {x['trail_dead']*100:11.1f}% {x['static_dead']*100:11.1f}% "
              f"{(x['trail_dead']-x['static_dead'])*100:+8.1f}pt {x['only_trail_dead']*100:17.1f}% "
              f"{(x['mean_trail']-1)*100:14.2f}% {(x['mean_static']-1)*100:14.2f}%")

    header("【2】1トレードのリスクを変えた場合  ※勝率60% / RR1.0")
    print(f"  {'リスク/回':>10} {'失格(トレ)':>12} {'失格(静的)':>12} {'差':>9} "
          f"{'静的なら生きていた':>18} {'ロック到達':>11} {'平均残高(トレ)':>15}")
    for r in (0.0025, 0.0050, 0.0075, 0.0100, 0.0150, 0.0200, 0.0300):
        x = run_pair(0.60, 1.00, r)
        print(f"  {r*100:9.2f}% {x['trail_dead']*100:11.1f}% {x['static_dead']*100:11.1f}% "
              f"{(x['trail_dead']-x['static_dead'])*100:+8.1f}pt {x['only_trail_dead']*100:17.1f}% "
              f"{x['lock_rate']*100:10.1f}% {(x['mean_trail']-1)*100:14.2f}%")

    header("【3】エッジの大きさを変えた場合  ※勝率60% / リスク r=1.0%、RRで期待値を調整")
    print(f"  {'期待値/回':>10} {'RR':>7} {'失格(トレ)':>12} {'失格(静的)':>12} {'差':>9} "
          f"{'静的なら生きていた':>18} {'ロック到達':>11}")
    for ev in (0.00, 0.10, 0.20, 0.30, 0.40):
        R = (ev + 0.4) / 0.6          # p=0.6 で期待値が ev*r になるRR
        x = run_pair(0.60, R, 0.010)
        print(f"  {ev:9.2f}r {R:7.2f} {x['trail_dead']*100:11.1f}% {x['static_dead']*100:11.1f}% "
              f"{(x['trail_dead']-x['static_dead'])*100:+8.1f}pt {x['only_trail_dead']*100:17.1f}% "
              f"{x['lock_rate']*100:10.1f}%")

    header("【4】トレーリングの死は、どこで起きているか  ※リスク r=1.0%")
    print(f"  {'勝率':>6} {'RR':>7} {'ロック到達':>11} {'ロック日数':>11} "
          f"{'死のうちロック前':>17} {'死ぬまでの最高到達益':>21}")
    for p, R in SHAPES:
        x = run_pair(p, R, 0.010)
        print(f"  {p*100:5.0f}% {R:7.2f} {x['lock_rate']*100:10.1f}% {x['mean_lock_day']:10.1f} "
              f"{x['dead_before_lock_share']*100:16.1f}% {x['mean_peak_of_trail_dead']*100:20.2f}%")
    print()
    print("  検算: 静的で死んでトレーリングで生きているケース =",
          f"{run_pair(0.60, 1.00, 0.010)['only_static_dead']*100:.3f}%")

    header(f"【5】勝率50%の人にとって、期待値がゼロになる参加費は？  "
           f"※${ACCOUNT:,.0f}口座 / 分配{SPLIT*100:.0f}% / リスク r=1.0%")
    print("  期待受取額（失格は0、分配率を掛けた後）＝ そのまま損益分岐の参加費になる")
    print(f"  {'RR':>6} {'1回の期待値':>12} {'失格(トレ)':>12} {'失格(静的)':>12} "
          f"{'分岐参加費(トレ)':>18} {'分岐参加費(静的)':>18} {'トレーリングの代償':>20}")
    for R in RR50:
        x = run_pair(0.50, R, 0.010)
        ev = (0.5 * R - 0.5) * 0.010
        print(f"  {R:6.2f} {ev*100:11.3f}% {x['trail_dead']*100:11.1f}% {x['static_dead']*100:11.1f}% "
              f"{x['payout_trail']:17,.0f}$ {x['payout_static']:17,.0f}$ "
              f"{x['payout_static']-x['payout_trail']:19,.0f}$")
    print()
