"""
インスタント（評価なし）口座の比較モンテカルロ。Moneta Funded と Blueberry Funded。

記事 content/articles/moneta-instant-pro-half-price-worth-it-2026-09.mdx の数値の裏付け。
価格・ルールはすべて 2026-09-10 に各社の公式チェックアウト／ヘルプセンターで実測した。

--------------------------------------------------------------------------
比較対象
--------------------------------------------------------------------------
  A. Moneta Instant Pro       分配88% / 最大DD8%トレーリング
                              日次損失4% / 1日の利益上限4%（到達で当日終了）
                              $10K $250（定価$500）/ $100K $2,650（定価$5,300）
                              いずれも TOKONATSU50（50%OFF）適用後
  B. Blueberry Instant Elite  分配80% / 最大DD10%トレーリングロック
                              日次損失なし / 利益上限なし / 最低5取引日
                              $10K $340（定価$400）/ $100K $2,380（定価$2,800）
                              いずれも BERRY15（15%OFF）適用後
  C. Blueberry Instant Lite   分配80% / 最大DD4%トレーリングロック / 日次2% / 一貫性15%
                              $10K $118.15（定価$139）/ $100K $667.25（定価$785）
                              いずれも BERRY15（15%OFF）適用後

--------------------------------------------------------------------------
前提
--------------------------------------------------------------------------
  - トレーダーの実力は「初期残高に対する日次損益率」の正規分布で表す。
    同じ人が同じ手法を回す想定なので、全プランに同じ乱数列を当てる（対応のある比較）。
  - トレーリングDDは「最高エクイティに追従し、初期残高に達したらそこでロック」。
    Moneta / Blueberry とも公式の説明はこの形。
  - 出金は期末に1回だけ行う簡略化。失格すると受取0、参加費は戻らない。
  - 一貫性ルール（Instant Lite の15%）は出金の「保留」であって没収ではないので、
    手取りからは引いていない。60日時点で保留に当たる割合を「出金保留率」として別に出す。
  - 最大DD・日次DDは公式にはエクイティ（含み損益込み）で判定されるが、
    ここは日次の終値ベースなので、日中の一時的な逆行で飛ぶケースを拾えていない。
    したがって失格率はどのプランも実態より低めに出る。
  - 1トレードアイデアのリスク上限（Moneta 2%→1.5% / Blueberry 1.5%）は
    日次粒度では表現できないため未実装。実際はこれが上振れの裾を削るので、
    ここで出る成績は実態より「甘め」に出ていることに注意。
  - 最低取引日数（Blueberry Elite の5日など）も未実装。

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

import numpy as np

SEED = 20260910
N_PATHS = 40_000
N_DAYS = 60          # 約3か月の営業日
SIGMA = 0.01         # 日次損益の標準偏差（初期残高比 1%）


class Plan:
    def __init__(self, name, fee, split, max_dd, trailing,
                 daily_loss=None, daily_profit_cap=None, consistency=None):
        self.name = name
        self.fee = fee
        self.split = split
        self.max_dd = max_dd                      # 初期残高比
        self.trailing = trailing                  # True: 最高値追従（初期残高でロック）
        self.daily_loss = daily_loss              # 日次損失上限（初期残高比）
        self.daily_profit_cap = daily_profit_cap  # 1日の利益上限（初期残高比）
        self.consistency = consistency            # 出金時チェック：1日の利益が総利益に占める上限


BOOKS = {
    10_000: [
        Plan("Moneta Instant Pro",      250.00, 0.88, 0.08, True, daily_loss=0.04, daily_profit_cap=0.04),
        Plan("BB Instant Elite",        340.00, 0.80, 0.10, True),
        Plan("BB Instant Lite",         118.15, 0.80, 0.04, True, daily_loss=0.02, consistency=0.15),
    ],
    100_000: [
        Plan("Moneta Instant Pro",     2650.00, 0.88, 0.08, True, daily_loss=0.04, daily_profit_cap=0.04),
        Plan("BB Instant Elite",       2380.00, 0.80, 0.10, True),
        Plan("BB Instant Lite",         667.25, 0.80, 0.04, True, daily_loss=0.02, consistency=0.15),
    ],
}


def run_plan(plan, daily, account):
    """daily: (paths, days) の日次損益率。戻り値: 受取純額（参加費控除後）と失格フラグ。"""
    paths, days = daily.shape
    bal = np.ones(paths)                 # 初期残高=1.0（初期残高比）
    hwm = np.ones(paths)
    alive = np.ones(paths, dtype=bool)
    max_day_profit = np.zeros(paths)     # 一貫性判定用

    for d in range(days):
        r = daily[:, d].copy()

        # 1日の利益上限：超えた分は取れない（到達時点で当日終了）
        if plan.daily_profit_cap is not None:
            r = np.minimum(r, plan.daily_profit_cap)

        # 日次損失上限：下回ったらその日で失格
        if plan.daily_loss is not None:
            hit = alive & (r <= -plan.daily_loss)
            alive &= ~hit

        bal = np.where(alive, bal + r, bal)
        max_day_profit = np.where(alive & (r > max_day_profit), r, max_day_profit)

        hwm = np.maximum(hwm, bal)
        if plan.trailing:
            # 最高値追従。ラインが初期残高に達したらそこでロックされ、以後は上がらない
            floor = np.minimum(hwm - plan.max_dd, 1.0)
        else:
            floor = np.full(paths, 1.0 - plan.max_dd)

        alive &= bal > floor

    profit = np.where(alive, np.maximum(bal - 1.0, 0.0), 0.0) * account
    net = profit * plan.split - plan.fee

    # 一貫性ルールは「出金リクエストの保留」であって没収ではない（公式明記：
    # "the request is paused rather than declined ... Nothing is deducted"）。
    # したがって手取りからは引かず、60日時点で保留になる割合を別途返す。
    paused = np.zeros(paths, dtype=bool)
    if plan.consistency is not None:
        total = np.maximum(bal - 1.0, 1e-12)
        paused = alive & (profit > 0) & ((max_day_profit / total) > plan.consistency)

    return net, ~alive, paused


def cost_table(account):
    """参加費が買っている実効DD枠と、参加費を回収する地点（解析解）。

    実効DD枠 = 口座サイズ × 最大DD% × 分配率
    （損は口座の総額で判定されるのに、益は分配率を掛けた分しか受け取れないため）
    """
    out = []
    for plan in BOOKS[account]:
        effective_dd = account * plan.max_dd * plan.split
        breakeven = plan.fee / (plan.split * account)
        out.append((plan.name, plan.fee, effective_dd, effective_dd / plan.fee, breakeven * 100))
    return out


def summarize(account, mu):
    # 呼ぶたびに同じ種から引き直す。こうしないと呼び出し順で数字が変わり、
    # 記事の表とグラフが食い違う。実力水準どうしも同じ乱数列で比べられる。
    daily = np.random.default_rng(SEED).normal(mu, SIGMA, size=(N_PATHS, N_DAYS))
    rows = []
    for plan in BOOKS[account]:
        net, dead, paused = run_plan(plan, daily, account)
        rows.append((plan.name, net.mean(), (net > 0).mean(), dead.mean(), paused.mean()))
    return rows


if __name__ == "__main__":
    for account in (10_000, 100_000):
        print("=" * 92)
        print(f"口座サイズ ${account:,} / モンテカルロ {N_PATHS:,}本 × {N_DAYS}営業日 / 日次ボラ {SIGMA*100:.1f}%")
        print("=" * 92)

        print("\n[解析解] 参加費で買える実効DD枠（口座 × 最大DD% × 分配率）と、参加費を回収する地点")
        print(f"  {'プラン':26} {'参加費':>10} {'実効DD枠':>12} {'枠/参加費':>10} {'±0になる利益':>14}")
        for name, fee, eff, ratio, be in cost_table(account):
            print(f"  {name:26} {fee:9,.0f}$ {eff:11,.0f}$ {ratio:9.2f}倍 {be:13.2f}%")

        for mu in (0.0000, 0.0005, 0.0010, 0.0020):
            exp = mu * N_DAYS * 100
            print(f"\n[日次期待値 {mu*100:+.2f}% / {N_DAYS}日で期待 {exp:+.1f}%]")
            print(f"  {'プラン':26} {'平均手取り':>12} {'プラス確率':>10} {'失格率':>8} {'出金保留率':>10}")
            for name, mean, pwin, dead, paused in summarize(account, mu):
                print(f"  {name:26} {mean:11.0f}$ {pwin*100:9.1f}% {dead*100:7.1f}% {paused*100:9.1f}%")
        print()
