"""FTMO・The5ers・Fintokei・Hantec の2ステップ系を、同じ前提で1年間まわして比べるモンテカルロ。

ルール（2026-09-27 に各社公式で確認）
- FTMO 2-Step: 10%→5%／日次5%（当日0時の残高−初期の5%）／最大10%静的（出金しても動かない）／最低4取引日／分配80%／
  参加費は初回報酬で100%返金。€540 → $619
- FTMO 1-Step: 10%／日次3%／最大10%は日末の最高残高に追従、報酬で新口座になると初期の90%に戻る／ベストデイ50%／分配90%／返金なし。€499 → $572
- The5ers High Stakes $100K Classic: 8%→5%／New: 10%→5%／日次4%・最大8%（初期残高基準、出金で余裕が減る）／各ステップ 0.5%以上の利益日×3／
  分配80%／出金は1回$4,000まで・最低$500／参加費はステップ1通過で10%・ステップ2通過で20%がハブクレジット（次の購入にだけ使える）、
  ファンド到達で70%が口座残高に加算。Classic $455 / New $405
- Fintokei チャレンジ（サファイヤ 2,000万円 ¥109,800＝口座の0.549%）: 8%→6%／日次5%／最大10%静的／最低3取引日／分配80%／
  スケーリング道場の白帯（取引20日＋報酬2回、2026-06-03開始）で購入時と同額の契約金＝2回目の報酬で参加費が戻る
- Hantec Enhanced: 10%→5%／日次5%／最大10%静的だが初回出金で初期残高に固定／各ステージと出金ごとに0.5%以上の利益日×3／分配80%。$599
- Hantec Endurance: 6%×3／日次4%／最大8%静的（出金しても動かない）／各ステージ3取引日／分配80%。$299

前提（固定）: 1日の損失は全プラン共通で初期残高の2.5%まで（超えそうならその日は張らない）・口座$100K相当・250営業日・1日3トレード・RR1:1・ロットは初期残高の r% 固定・ポジションは1つずつ
            （同時に持たないので含み損3%系のルールには当たらない）・コスト0.05R/トレード・失格したら同じプランを買い直す・
            スケーリングなし・年末に口座に残った利益は数えない・出金サイクル14日（10営業日）。
動かすもの: プラン、勝率、r、出金時に口座へ残す余裕 B（全額出金の決まりがある FTMO 1-Step は 0 固定）。
"""
import sys
import json
import numpy as np
from numba import njit, prange

DAYS, K, CYCLE = 250, 3, 10
DAILY_STOP = 0.025  # 全プラン共通: 1日の損失は初期残高の2.5%まで（会社の日次損失がもっと厳しければそちら）

PLANS = {
    #                     price  targets              ns  mdd   daily doi lock trail reset md mdn cons  cm full split refund cr1  cr2  fcash cap   pmin  pdays
    "FTMO 2-Step":        (619, (0.10, 0.05, 0.0),    2, 0.10, 0.05, 1, 0, 0, 0, 2, 4, 0.0, 0, 0, 0.8, 1, 0.0, 0.0, 0.0, 9.0, 0.0002, 0),
    "FTMO 1-Step":        (572, (0.10, 0.0, 0.0),     1, 0.10, 0.03, 1, 0, 2, 1, 0, 0, 0.5, 1, 1, 0.9, 0, 0.0, 0.0, 0.0, 9.0, 0.0002, 0),
    "The5ers Classic":    (455, (0.08, 0.05, 0.0),    2, 0.08, 0.04, 0, 0, 0, 0, 1, 3, 0.0, 0, 0, 0.8, 0, 0.1, 0.2, 0.7, 0.04, 0.005, 3),
    "The5ers New":        (405, (0.10, 0.05, 0.0),    2, 0.08, 0.04, 0, 0, 0, 0, 1, 3, 0.0, 0, 0, 0.8, 0, 0.1, 0.2, 0.7, 0.04, 0.005, 3),
    "Fintokei":           (549, (0.08, 0.06, 0.0),    2, 0.10, 0.05, 0, 0, 0, 0, 2, 3, 0.0, 0, 0, 0.8, 2, 0.0, 0.0, 0.0, 9.0, 0.0002, 0),
    "Hantec Enhanced":    (599, (0.10, 0.05, 0.0),    2, 0.10, 0.05, 0, 1, 0, 0, 1, 3, 0.0, 0, 0, 0.8, 0, 0.0, 0.0, 0.0, 9.0, 0.0002, 3),
    "Hantec Endurance":   (299, (0.06, 0.06, 0.06),   3, 0.08, 0.04, 0, 0, 0, 0, 2, 3, 0.0, 0, 0, 0.8, 0, 0.0, 0.0, 0.0, 9.0, 0.0002, 0),
}
KEYS = ["price", "tg", "ns", "mdd", "daily", "doi", "lock", "trail", "reset", "md", "mdn", "cons", "cm", "full", "split",
        "refund", "cr1", "cr2", "fcash", "cap", "pmin", "pdays"]
PLANS = {k: dict(zip(KEYS, v)) for k, v in PLANS.items()}
DAILY_FULL = {"FTMO 2-Step": 0.015, "FTMO 1-Step": 0.009, "The5ers Classic": 0.0125, "The5ers New": 0.0125,
              "Fintokei": 0.015, "Hantec Enhanced": 0.015, "Hantec Endurance": 0.0125}


@njit(parallel=True, cache=True)
def run(n, seed, p, r, cost, price, targets, n_stages, mdd, daily, doi, lock, trail, reset, mdkind, mdn, cons, cm, full,
        split, refund, cr1, cr2, fcash, cap, pmin, pdays_req, buf):
    """price は口座比（$100K なら 0.00619 など）。戻り値は口座比。"""
    paid = np.zeros(n)
    fees = np.zeros(n)
    funded_n = np.zeros(n)
    for i in prange(n):
        np.random.seed(seed + i)
        credit = 0.0
        fees[i] = price
        mode = 0; stage = 0
        bal = 1.0; line = 1.0 - mdd; eodhigh = 1.0
        pdays = 0; tdays = 0; best = 0.0; possum = 0.0
        since = 0; paidthis = 0; npay = 0; fdays = 0
        for d in range(DAYS):
            start = bal
            dead = False
            reached = False
            dl = start - daily if doi == 1 else start * (1.0 - daily)
            for k in range(K):
                if reached:
                    break
                # 自分で決めた1日の損失上限（全プラン共通で初期残高の2.5%）と会社の日次損失の、厳しいほうを超えそうならその日は張らない
                stopline = start - DAILY_STOP
                if dl > stopline:
                    stopline = dl
                if bal - r * (1.0 + cost) < stopline - 1e-12:
                    break
                if np.random.random() < p:
                    bal += r * (1.0 - cost)
                else:
                    bal -= r * (1.0 + cost)
                if bal < line - 1e-12 or bal < dl - 1e-12:
                    dead = True
                    break
                if mode == 0 and bal >= 1.0 + targets[stage] - 1e-12:
                    reached = True
            if dead:
                use = credit if credit < price else price
                fees[i] += price - use
                credit -= use
                mode = 0; stage = 0; bal = 1.0; line = 1.0 - mdd; eodhigh = 1.0
                pdays = 0; tdays = 0; best = 0.0; possum = 0.0; since = 0; paidthis = 0; npay = 0; fdays = 0
                continue
            dp = bal - start
            tdays += 1
            if dp >= 0.005 - 1e-12:
                pdays += 1
            if dp > best:
                best = dp
            if dp > 0:
                possum += dp
            if trail == 2:
                if bal > eodhigh:
                    eodhigh = bal
                nl = eodhigh - mdd
                if nl > line:
                    line = nl
            if mode == 0:
                if bal >= 1.0 + targets[stage] - 1e-12:
                    ok = True
                    if mdkind == 1 and pdays < mdn:
                        ok = False
                    if mdkind == 2 and tdays < mdn:
                        ok = False
                    if cons > 0 and best > cons * possum + 1e-12:
                        ok = False
                    if ok:
                        if stage == 0:
                            credit += cr1 * price
                        elif stage == 1:
                            credit += cr2 * price
                        stage += 1
                        bal = 1.0; line = 1.0 - mdd; eodhigh = 1.0
                        pdays = 0; tdays = 0; best = 0.0; possum = 0.0
                        if stage >= n_stages:
                            mode = 1; since = 0; paidthis = 0; npay = 0; fdays = 0
                            bal += fcash * price  # The5ers: ファンド到達で参加費の70%が口座残高に加算
                            funded_n[i] += 1.0
            else:
                since += 1
                fdays += 1
                if since >= CYCLE:
                    profit = bal - 1.0
                    ok = True
                    if pdays_req > 0 and pdays < pdays_req:
                        ok = False
                    if cons > 0 and profit > 0 and best > cons * possum + 1e-12:
                        ok = False
                    if full == 1:
                        amt = profit
                    else:
                        amt = bal - (1.0 + buf)
                    if amt > cap:
                        amt = cap
                    if ok and amt >= pmin:
                        paid[i] += amt * split
                        npay += 1
                        # refund = 参加費が戻る報酬の回数（FTMO 2-Step は1回目、Fintokei は道場の白帯＝2回目かつ取引20日）
                        if paidthis == 0 and refund > 0 and npay >= refund and (refund < 2 or fdays >= 20):
                            paid[i] += price
                            paidthis = 1
                        bal -= amt
                        since = 0; pdays = 0; best = 0.0; possum = 0.0
                        if lock == 1:
                            line = 1.0
                        if reset == 1:
                            bal = 1.0; line = 1.0 - mdd; eodhigh = 1.0
    return paid, fees, funded_n


def main():
    n = int(sys.argv[1]) if len(sys.argv) > 1 else 20000
    out = sys.argv[2] if len(sys.argv) > 2 else "sim_4firms.json"
    edges = [("ゼロ", 0.50), ("小", 0.52), ("中", 0.55), ("強", 0.58)]
    risks = [0.005, 0.01, 0.015]
    bufs = [0.0, 0.02, 0.04, 0.06, 0.08, 0.10, 0.12, 0.16]
    rows = []
    for name, P in PLANS.items():
        for ename, p in edges:
            for r in risks:
                for b in ([0.0] if P["full"] else bufs):
                    paid, fees, fn = run(n, 12345, p, r, 0.05, P["price"] / 100000, np.array(P["tg"]), P["ns"], P["mdd"],
                                         P["daily"], P["doi"], P["lock"], P["trail"], P["reset"], P["md"], P["mdn"], P["cons"],
                                         P["cm"], P["full"], P["split"], P["refund"], P["cr1"], P["cr2"], P["fcash"], P["cap"],
                                         P["pmin"], P["pdays"], b)
                    net = (paid - fees) * 100000
                    rows.append(dict(plan=name, edge=ename, r=r, buf=b, net=float(net.mean()), pos=float((net > 0).mean()),
                                     paid=float(paid.mean() * 100000), fee=float(fees.mean() * 100000), funded=float((fn > 0).mean())))
    json.dump(rows, open(out, "w", encoding="utf-8"), ensure_ascii=False)
    for title, rf in (("1回0.5%", lambda p: 0.005), ("1回1.0%", lambda p: 0.01), ("1回1.5%", lambda p: 0.015)):
        print(f"\n## {title}")
        for ename, _ in edges:
            cells = []
            for pl in PLANS:
                c = [x for x in rows if x["plan"] == pl and x["edge"] == ename and abs(x["r"] - rf(pl)) < 1e-9]
                b = max(c, key=lambda x: x["net"])
                cells.append(f"{pl[:12]:12s}{b['net']:+8.0f}({b['pos']*100:3.0f}% B{b['buf']*100:.0f})")
            print(f"{ename:3s} " + " ".join(cells))


if __name__ == "__main__":
    main()
