"""プロップファームの期待値を、共通の前提で1年間まわすモンテカルロ（会社ごとの設定は JSON で渡す）。

前提（固定・全社共通。ftmo-the5ers-fintokei-hantec-expected-value-2026-09 と同じ）
- 口座$100K相当・250営業日・1日最大3トレード・利確と損切りは1:1
- 1日の損失は初期残高の2.5%まで（次に負けたら2.5%か会社の日次を超える日は、その日はそれ以上張らない）
- 1回のリスク・含み損・1日の損失は3%を超えない（1回は0.5%・1.0%・1.5%）
- コストは1トレードあたりリスクの5%（勝ち +0.95R、負け −1.05R）
- 失格したら同じプランを買い直す・スケーリングなし・出金は cycle 営業日ごと・年末に口座に残った利益は数えない
- 出金時に口座へ残す利益 B はプランごとに一番いい量を選ぶ（全額出金の決まりがあるプランは0）

動かすもの: 実力（勝率 50/52/55/58%）、1回のリスク r

使い方:
  python scripts/sim-prop-ev.py plans.json [回数=20000] [結果JSON]
plans.json は {"プラン名": {設定}, ...}。設定のキーは PLAN_KEYS を参照。
"""
import json
import sys

import numpy as np
from numba import njit, prange

DAYS, K = 250, 3
DAILY_STOP = 0.025
COST = 0.05

# 設定のキーと既定値（口座比。$100K なら 0.01 が $1,000）
PLAN_KEYS = {
    "price": None,          # 参加費（$100K 相当の USD。円建てやユーロ建ては口座比で換算して入れる）
    "targets": [],          # 各ステップの利益目標（例 [0.08, 0.05]）。空なら評価なし（インスタント）
    "max_loss": 0.10,       # 最大損失（初期残高比）
    "daily": 0.05,          # 日次損失（0なら無し）
    "daily_from_initial": 0,  # 1: 当日0時の残高 − 初期残高×daily（FTMO型） 0: 当日0時の残高×(1−daily)
    "trailing": 0,          # 0: 静的 1: 確定残高に追従し初期残高でロック 2: 日末の最高残高に追従（上限なし） 3: 日末の確定残高に追従し初期残高でロック（Tradeify FX 型）
    "lock_on_payout": 0,    # 1: 初回出金で最大損失のラインが初期残高に上がる
    "reset_on_payout": 0,   # 1: 報酬のたびに新口座（残高とラインが初期に戻る）
    "min_days_kind": 0,     # 0: なし 1: 利益日（0.5%以上） 2: 取引日
    "min_days": 0,
    "consistency": 0.0,     # 一番良い日 ÷ プラスの日の合計 の上限（0なら無し）
    "consistency_funded": 1,  # 1: 出金時にも一貫性を見る 0: 評価中だけ
    "consistency_net": 0,   # 1: 一貫性の分母を「プラスの日の合計」ではなく純利益にする（Tradeify FX 型）
    "full_payout": 0,       # 1: 利益を全額出す決まり
    "split": 0.8,           # 分配
    "refund_after": 0,      # n回目の報酬で参加費が戻る（0なら無し）
    "refund_frac": 1.0,
    "consistency_eval": 1,  # 0: 評価中は一貫性を見ない（ファンドだけ。The5ers サマープランの2-Step）     # そのとき戻る参加費の割合（The5ers サマープランは70%）
    "refund_min_days": 0,   # 返金にファンド口座での取引日数の条件があるとき
    "credit1": 0.0,         # ステップ1通過で参加費のこの割合が次の購入クレジットに
    "credit2": 0.0,
    "funded_cash": 0.0,     # ファンド到達で参加費のこの割合が口座残高に加算
    "payout_cap": 9.0,      # 1回の出金の上限（口座比）
    "payout_min": 0.0002,   # 1回の出金の下限（口座比。$20 = 0.0002）
    "payout_profit_days": 0,  # 出金に必要な利益日（0.5%以上）の数（サイクルごと）
    "cycle": 10,            # 出金サイクル（営業日。14暦日 ≒ 10）
    "first_cycle": 10,      # 初回出金までの営業日
    "keep_buffer": 0.0,     # 出金後も口座に残さないといけない利益（口座比。Instant Lite の3%など）
}


def load_plans(path):
    raw = json.load(open(path, encoding="utf-8"))
    plans = {}
    for name, cfg in raw.items():
        unknown = set(cfg) - set(PLAN_KEYS) - {"note", "source"}
        if unknown:
            raise ValueError(f"{name}: 不明なキー {unknown}")
        p = {k: cfg.get(k, v) for k, v in PLAN_KEYS.items()}
        if p["price"] is None:
            raise ValueError(f"{name}: price がない")
        plans[name] = p
    return plans


@njit(parallel=True, cache=True)
def _run(n, seed, p, r, price, targets, n_stages, mdd, daily, doi, trailing, lock, reset, mdk, mdn, cons, cons_f,
         full, split, refund_after, refund_min_days, cr1, cr2, fcash, cap, pmin, pdays_req, cycle, first_cycle,
         keep_buffer, buf, cons_net, refund_frac, cons_e):
    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
        stage = 0
        mode = 1 if n_stages == 0 else 0
        bal = 1.0; line = 1.0 - mdd; hwm = 1.0; eodhigh = 1.0; locked = 0
        pdays = 0; tdays = 0; best = 0.0; possum = 0.0; netsum = 0.0
        since = 0; npay = 0; fdays = 0; refunded = 0
        if mode == 1:
            funded_n[i] += 1.0
            bal += fcash * price
        for d in range(DAYS):
            start = bal
            dead = False
            reached = False
            if daily > 0:
                dl = start - daily if doi == 1 else start * (1.0 - daily)
            else:
                dl = -1e9
            stopline = start - DAILY_STOP
            if dl > stopline:
                stopline = dl
            for k in range(K):
                if reached:
                    break
                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 trailing == 1 and locked == 0:
                    if bal > hwm:
                        hwm = bal
                    nl = hwm - mdd
                    if nl >= 1.0:
                        nl = 1.0
                        locked = 1
                    if nl > line:
                        line = nl
                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
                stage = 0
                mode = 1 if n_stages == 0 else 0
                bal = 1.0; line = 1.0 - mdd; hwm = 1.0; eodhigh = 1.0; locked = 0
                pdays = 0; tdays = 0; best = 0.0; possum = 0.0; netsum = 0.0; since = 0; npay = 0; fdays = 0; refunded = 0
                if mode == 1:
                    funded_n[i] += 1.0
                    bal += fcash * price
                continue
            dp = bal - start
            tdays += 1
            if dp >= 0.005 - 1e-12:
                pdays += 1
            if dp > best:
                best = dp
            if dp > 0:
                possum += dp
            netsum += dp
            if trailing == 3 and locked == 0:
                if bal > hwm:
                    hwm = bal
                nl = hwm - mdd
                if nl >= 1.0:
                    nl = 1.0
                    locked = 1
                if nl > line:
                    line = nl
            if trailing == 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 mdk == 1 and pdays < mdn:
                        ok = False
                    if mdk == 2 and tdays < mdn:
                        ok = False
                    den = (bal - 1.0) if cons_net == 1 else possum
                    if cons > 0 and cons_e == 1 and ((cons_net == 1 and den <= 0) or best > cons * den + 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; hwm = 1.0; eodhigh = 1.0; locked = 0
                        pdays = 0; tdays = 0; best = 0.0; possum = 0.0; netsum = 0.0
                        if stage >= n_stages:
                            mode = 1; since = 0; npay = 0; fdays = 0; refunded = 0
                            bal += fcash * price
                            funded_n[i] += 1.0
            else:
                since += 1
                fdays += 1
                need = first_cycle if npay == 0 else cycle
                if since >= need:
                    profit = bal - 1.0
                    ok = True
                    if pdays_req > 0 and pdays < pdays_req:
                        ok = False
                    den = netsum if cons_net == 1 else possum
                    if cons > 0 and cons_f == 1 and profit > 0 and ((cons_net == 1 and den <= 0) or best > cons * den + 1e-12):
                        ok = False
                    if full == 1:
                        amt = profit - keep_buffer
                    else:
                        amt = bal - (1.0 + buf + keep_buffer)
                    if amt > cap:
                        amt = cap
                    if ok and amt >= pmin:
                        paid[i] += amt * split
                        npay += 1
                        if refunded == 0 and refund_after > 0 and npay >= refund_after and fdays >= refund_min_days:
                            paid[i] += price * refund_frac
                            refunded = 1
                        bal -= amt
                        since = 0; pdays = 0; best = 0.0; possum = 0.0; netsum = 0.0
                        if lock == 1:
                            line = 1.0
                            locked = 1
                        if reset == 1:
                            bal = 1.0; line = 1.0 - mdd; hwm = 1.0; eodhigh = 1.0; locked = 0
    return paid, fees, funded_n


def simulate(plan, p, r, n=20000, seed=12345, buf=0.0):
    tg = np.array(list(plan["targets"]) + [0.0] * (3 - len(plan["targets"])), dtype=np.float64)
    paid, fees, fn = _run(n, seed, p, r, plan["price"] / 100000, tg, len(plan["targets"]), plan["max_loss"],
                          plan["daily"], plan["daily_from_initial"], plan["trailing"], plan["lock_on_payout"],
                          plan["reset_on_payout"], plan["min_days_kind"], plan["min_days"], plan["consistency"],
                          plan["consistency_funded"], plan["full_payout"], plan["split"], plan["refund_after"],
                          plan["refund_min_days"], plan["credit1"], plan["credit2"], plan["funded_cash"],
                          plan["payout_cap"], plan["payout_min"], plan["payout_profit_days"], plan["cycle"],
                          plan["first_cycle"], plan["keep_buffer"], buf, plan["consistency_net"], plan["refund_frac"], plan["consistency_eval"])
    net = (paid - fees) * 100000
    return dict(net=float(net.mean()), pos=float((net > 0).mean()), paid=float(paid.mean() * 100000),
                fee=float(fees.mean() * 100000), funded=float((fn > 0).mean()))


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]


def main():
    plans = load_plans(sys.argv[1])
    n = int(sys.argv[2]) if len(sys.argv) > 2 else 20000
    out = sys.argv[3] if len(sys.argv) > 3 else None
    rows = []
    for name, plan in plans.items():
        for ename, p in EDGES:
            for r in RISKS:
                cands = []
                for b in ([0.0] if plan["full_payout"] else BUFS):
                    res = simulate(plan, p, r, n=n, buf=b)
                    res.update(plan=name, edge=ename, p=p, r=r, buf=b)
                    cands.append(res)
                rows.append(max(cands, key=lambda x: x["net"]))
    if out:
        json.dump(rows, open(out, "w", encoding="utf-8"), ensure_ascii=False, indent=1)
    for r in RISKS:
        print(f"\n## 1回{r*100:.1f}%（1年間の手取り−参加費の平均・$100K相当）")
        print("| 実力 | " + " | ".join(plans) + " |")
        print("|---|" + "---|" * len(plans))
        for ename, _ in EDGES:
            cells = []
            for name in plans:
                x = next(x for x in rows if x["plan"] == name and x["edge"] == ename and x["r"] == r)
                s = f"{'+' if x['net'] >= 0 else '−'}${abs(x['net']):,.0f}"
                cells.append(s)
            print(f"| {ename} | " + " | ".join(cells) + " |")
        print("\n（黒字になる確率）")
        for ename, _ in EDGES:
            print(f"  {ename}: " + " / ".join(
                f"{name} {next(x for x in rows if x['plan'] == name and x['edge'] == ename and x['r'] == r)['pos']*100:.0f}%"
                for name in plans))


if __name__ == "__main__":
    main()
