==================== 103 markdown ## 25. The deployed decision: the method, the side and the purchase gate (2026-09-29) **The question.** The deployed plan risks 3% a trade in the assessment and about 1.5% once funded at reward-to-risk 1, with the target clipped and the stop at the floor, one \$3,000 account, and a random side each trade. This section measures what the random side's swap costs, finds the funded risk by line search within the plateau, and states the purchase gate in numbers. #### Concepts used in this section **Swap nights.** What it is: the rollovers at 21:00 UTC a trade is open through, Wednesday's counted three times for the weekend. How it is measured: plain brackets through the broker, coin side, ten seeds, at the deployed stop and window. How it affects the result: always short collects +\$1.76 a lot a night; a random side pays the mean of that and the long side's -\$6.24, so the swap turns from a credit into a cost. Expected against found: below. **The plateau.** What it is: the funded risks whose chance of any payout sits within one point of the best. How it is used: below the kink at reward-to-risk x risk = 0.35 x the minimum request the chance is a sawtooth, jumping wherever one fewer net win clears the request, at r = m1 / (j - n gamma), n the payout clock in trades; a line search in small steps sees every tooth, and the choice is made within the plateau, not at its argmax, because no measurement on this history orders cells a point apart. How it affects the result: the argmax wanders with the cost; the plateau does not. Expected against found: below. **The purchase gate.** What it is: buy only if the wallet's expected log growth is positive with 80% of payouts honoured, at the measured cost. How it is used: `wallet_mc` at the chosen cell, \$1,000 on the \$3,000 tier, three years, 10,000 paths. How it affects the result: it is the one rule that can say no to the play. Expected against found: below. #### Briefing, stated before scoring - **Assessment.** At reward-to-risk 1 the pass rate rises with the risk until the 35% single-day rule, read gross (commission is charged at the open and a trade lasts two days, so the closing day shows the whole win): 0.35 x 9% = 3.15%. The deployed risk is 3%, a margin of 0.15% under it. - **Funded.** A line search at reward-to-risk 1 from 0.6% to 2% in 0.025% steps, at each cost scenario. The pick: within the plateau, the fewest median trades to the first payout, ties to the larger gross withdrawn. - **Payout clock.** Fourteen calendar days, in trades at the deployed pace of section 24: 14 / 2.03, seven trades. The funded line search, the chains and the wallet all run on it. - **Cost scenarios, as shares of the risk at the 34.8-pip mean stop.** Commission only (the broker's); commission and a 0.5-pip spread; that plus the random side's swap; that plus the measured 1.33 step-asides a trade, each paying commission and spread again. - **Gate.** Declared above. It is evaluated at every scenario; the scenario that applies is the one the three pending measurements (aside rate on a narrower calendar, live spread, strict daily-loss breaches) will pin. - **What it cannot see.** Whether the firm pays; the spread at the aside instants; floating equity between closes. ==================== 104 code # the swap nights of a trade at the deployed stop, and the cost share of each side policy, about a minute NIGHTS25 = BM.swap_nights() SWAP_LONG_USD, SWAP_SHORT_USD = -6.24, 1.76 # per lot per night, the platform's View Details, 2026-09-16 STOP25 = STOP24 WT25 = max(PA.MIN_DAYS, int(round(PA.WAIT / pace[1.0]))) # 14 calendar days in trades at the deployed pace H25 = int(round(3 * 365 / pace[1.0])) # three years in trades side_share = {name: -(usd / 10.0) * NIGHTS25.mean() / STOP25 for name, usd in (("short", SWAP_SHORT_USD), ("coin", (SWAP_LONG_USD + SWAP_SHORT_USD) / 2), ("long", SWAP_LONG_USD))} print(f"closed trades {len(NIGHTS25)}: swap nights a trade, mean {NIGHTS25.mean():.3f}, none {np.mean(NIGHTS25 == 0):.1%}") for k, v in side_share.items(): print(f" {k:5}: swap adds {v:+.2%} of the risk to the cost share") G25 = {"commission only": PA.cost_share(STOP25, s=0.0), "commission and spread": PA.cost_share(STOP25), "random side, swap charged": PA.cost_share(STOP25) + side_share["coin"], "random side, with the asides": PA.cost_share(STOP25) * 2.33 + side_share["coin"]} for k, v in G25.items(): print(f"cost share, {k:30}: {v:.2%} of the risk") print(f"payout clock {WT25} trades at {pace[1.0]:.2f} days a trade; gate horizon {H25} trades, three years") ==================== 105 code # the assessment at the cap and at the deployed margin; the funded line search per scenario, a few minutes CAP25 = 0.35 * PA.TARGET rows = [] for name, g in G25.items(): cap = PA.eval_chain(CAP25, g * CAP25, rr=1.0, clip_up=True, clip_dn=True) dep = PA.eval_chain(0.03, g * 0.03, rr=1.0, clip_up=True, clip_dn=True) rows.append(dict(scenario=name, cost=g, pass_cap=cap["p_pass"], pass_3=dep["p_pass"], trades_3=dep["e_trades"])) A25 = pd.DataFrame(rows) print(f"assessment at reward-to-risk 1: the cap {CAP25:.2%} (gross) against the deployed 3%") print(A25.to_string(index=False, formatters={"cost": "{:.2%}".format, "pass_cap": "{:.2%}".format, "pass_3": "{:.2%}".format, "trades_3": "{:.1f}".format})) LS25, PICK25 = {}, {} for name, g in G25.items(): rows_ = PA.funded_line_search(g, lo=0.006, hi=0.020, step=0.00025, wait=WT25) LS25[name] = rows_ plat = PA.plateau(rows_) PICK25[name] = min(plat, key=lambda r: (r["med_first_pay"], -r["e_W"])) best = max(rows_, key=lambda r: r["p_any"]) teeth = [e for e in PA.tooth_edges(g, n=WT25) if 0.006 < e < 0.35 * 100 / 0.7 / 3000] print(f"{name:30} best {best['risk']:.3%} ({best['p_any']:.2%}); plateau {plat[0]['risk']:.3%} to {plat[-1]['risk']:.3%}; " f"pick {PICK25[name]['risk']:.3%} ({PICK25[name]['p_any']:.2%}, first payout {PICK25[name]['med_first_pay']} trades); " f"teeth at {', '.join(f'{e:.3%}' for e in teeth)}") ==================== 106 code fig, ax = plt.subplots(1, 1, figsize=(10, 4.2)) cols = [PALETTE["MUT"], PALETTE["BLUE"], PALETTE["GREEN"], PALETTE["RED"]] for (name, rows_), col in zip(LS25.items(), cols): r_ = np.array([x["risk"] for x in rows_]); a_ = np.array([x["p_any"] for x in rows_]) ax.plot(100 * r_, 100 * a_, color=col, lw=1.2, label=f"{name} ({100 * G25[name]:.2f}% of risk)") ax.scatter([100 * PICK25[name]["risk"]], [100 * PICK25[name]["p_any"]], color=col, s=30, zorder=5) for e in PA.tooth_edges(G25[name], n=WT25): if 0.6 < 100 * e < 100 * 0.35 * 100 / 0.7 / 3000: ax.axvline(100 * e, color=col, lw=0.6, ls=":") ax.axvline(100 * 0.35 * 100 / 0.7 / 3000, color=PALETTE["INK"], lw=1, ls="--", label="kink: 0.35 x the minimum request") ax.set_xlabel("funded risk per trade, % of the bankroll"); ax.set_ylabel("chance of any payout, %") ax.legend(frameon=False, fontsize=7.5, loc="lower center") style_axes(ax) fig.tight_layout() fig.savefig("src/notebooks/reports/topics/prime_academy/figures/funded_sawtooth.png", dpi=130) plt.show() ==================== 107 markdown **Figure.** The funded chance of any payout along the risk at reward-to-risk 1, one line per cost scenario, the tooth positions r = m1 / (j - n gamma) dotted, the kink dashed, each scenario's pick marked. Check that every jump sits on a dotted line: a jump off them would mean the chain and the formula disagree about the lattice. ==================== 108 code # the purchase gate: the wallet at the deployed cell, per scenario, about two minutes T3_25 = next(t for t in TIERS if t.bankroll == 3000) rows = [] for name, g in G25.items(): rf = PICK25[name]["risk"] e = PA.eval_chain(0.03, g * 0.03, rr=1.0, clip_up=True, clip_dn=True) f = PA.funded_chain(rf, g * rf, PA.payout_threshold(rf, 1.0, 3000, SPLIT), rr=1.0, clip_dn=True, wait=WT25) cash = SPLIT * 3000 * e["p_pass"] * f["e_W"] g_ = {} for honour in (1.0, 0.8): w = PA.wallet_mc(T3_25, 0.03, g * 0.03, SPLIT, honour, 1000, horizon_days=H25, n=10_000, seed=SEED, rr=1.0, wait=WT25, funded_risk=rf, funded_cost=g * rf, clip_up=True, clip_dn=True) g_[honour] = w["e_log_growth"] rows.append(dict(scenario=name, cost=g, funded_risk=rf, p_pass=e["p_pass"], p_any=f["p_any"], p_cash=e["p_pass"] * f["p_any"], cash_fee=cash / T3_25.fee, breakeven=T3_25.fee / cash, g_full=g_[1.0], g_80=g_[0.8], gate=g_[0.8] > 0)) GATE25 = pd.DataFrame(rows) print("the deployed cell per cost scenario: assessment 3%, funded at the scenario's pick, $1,000 on the $3,000 tier") print(GATE25.to_string(index=False, formatters={"cost": "{:.2%}".format, "funded_risk": "{:.3%}".format, "p_pass": "{:.1%}".format, "p_any": "{:.1%}".format, "p_cash": "{:.1%}".format, "cash_fee": "{:.2f}".format, "breakeven": "{:.0%}".format, "g_full": "{:+.3f}".format, "g_80": "{:+.3f}".format})) ==================== 109 code # the paper reads this file dec25 = {"commit": subprocess.run(["git", "rev-parse", "--short", "HEAD"], capture_output=True, text=True).stdout.strip(), "nights_mean": float(NIGHTS25.mean()), "nights_none": float(np.mean(NIGHTS25 == 0)), "n_trades": int(len(NIGHTS25)), "side_share": {k: float(v) for k, v in side_share.items()}, "stop": STOP25, "cap": CAP25, "wait": WT25, "horizon": H25, "scenarios": {name: {"cost": float(G25[name]), "assess": A25[A25.scenario == name].iloc[0][["pass_cap", "pass_3", "trades_3"]].astype(float).to_dict(), "pick": {k: float(v) for k, v in PICK25[name].items()}, "plateau": [float(PA.plateau(LS25[name])[0]["risk"]), float(PA.plateau(LS25[name])[-1]["risk"])], "teeth": [float(e) for e in PA.tooth_edges(G25[name], n=WT25) if 0.006 < e < 0.35 * 100 / 0.7 / 3000], "curve": [[float(x["risk"]), float(x["p_any"])] for x in LS25[name]], "gate": {k: (bool(v) if k == "gate" else float(v)) for k, v in GATE25[GATE25.scenario == name].iloc[0].drop("scenario").items()}} for name in G25}} json.dump(dec25, open("src/notebooks/reports/topics/prime_academy/paper/decision.json", "w"), indent=1) open_ = [n for n in G25 if dec25["scenarios"][n]["gate"]["gate"]] verdict25 = (f"GATE OPEN in {', '.join(open_)}; CLOSED in the others" if open_ else "GATE CLOSED in every scenario") print(f"25 verdict by the declared rule: {verdict25}. The scenario that applies is set by the pending measurements.") ==================== 110 markdown ## 26. The optimisation: three objectives and the front (2026-09-30) **The question.** The operator wants money soon, money in total, and a shallow wallet drawdown, without a weighted sum, since any weight between days and dollars would be arbitrary. This section states the criteria as three objectives and a set of limits, reduces the decision vector by calculus, computes every remaining cell on the chains and the wallet, and reports the Pareto front: the cells that no other cell beats on all three objectives at once. #### Concepts used in this section **Pareto front.** What it is: the feasible cells that no other feasible cell beats on every objective at once. How it is used: a cell is dropped when another is no worse on T, G and D and strictly better on one; what is left is the menu. How it affects the result: no weight is ever chosen; a drawdown cap in dollars, which the operator states, picks from the menu. Expected against found: below. **T, days to first cash.** What it is: the median calendar days from the first purchase until the wallet receives its first honoured payout, counting failed purchases and their replacements. How it is measured: the wallet simulation over three years, a path with no cash counted as never, so the median is over every path: $$T = \operatorname{median}\ \tau_{\text{cash}}$$ where τ_cash is the day of the first honoured payout into the wallet. How it affects the result: it is the "money soon" axis. Expected against found: below. **G, the cash rate.** What it is: the wallet's long-run net cash in dollars a year, at 80% of payouts honoured. How it is computed: a purchase costs the fee and returns honoured payouts over its life, and the next purchase follows the last one's death, so by the renewal-reward theorem the rate is the expected reward of one purchase over its expected length: $$G = N \cdot \frac{365\,(h \alpha B P\, E[W] - F)}{d\,(E[n_A] + P\, E[L_F])}$$ with N accounts held at once, h = 0.8 the honour rate, α = 0.7 the split, B = $3,000 the bankroll, P the pass rate, E[W] the gross withdrawn by a funded account as a share of B, F = $38 the fee, d the calendar days per trade, E[n_A] the expected trades to resolve the assessment and E[L_F] the expected funded life in trades. Every term but d comes exactly from the chains. How it affects the result: it is the "money in total" axis. Expected against found: below. **D, wallet drawdown.** What it is: the 90th percentile over wallet paths of the largest peak-to-trough fall in the first year, the peak floored at the opening $1,000. How it is measured: the same simulation. How it affects the result: it is the drawdown axis, and every fee is a dollar of it until cash arrives. Expected against found: below. **The pace along the stop.** What it is: calendar days per trade at a stop of ℓ pips. How it is measured: Wald's identity makes a bracket's expected length a function of the product of its legs alone, so the broker's measured pace at ratio RR and the deployed stop (section 24) is the pace of a ratio-1 bracket whose stop is $$\ell = \sqrt{RR}\ \ell_0$$ with ℓ₀ = 34.8 pips the deployed mean stop. Below the smallest ratio measured (0.5, a 24.6-pip stop) the line through the two lowest points is extended, floored at one entry a trading day, 7/5 calendar days. How it affects the result: it converts every duration and the 14-day payout clock into days. Expected against found: below. #### Briefing, stated before scoring - **Decision vector.** Stop ℓ, reward-to-risk, the two stage risks, trades a day k, tier, accounts held at once N, cost scenario. Calculus fixes all but ℓ and N: reward-to-risk 1, the assessment at 3% (the 35% cap read gross, 3.15%), the tier $3,000 (the wallet's ruin cap), the funded risk by the line search of section 25 within its plateau at each stop, and k = 1 (checked below). - **Cells.** Stops 15, 20, 25, 28, 30, 34.8, 40, 45 pips; N = 1, 2, 3, the accounts entered at hours whose outcomes correlate as section 15 measured; the four cost scenarios of section 25, each component scaled with the stop (commission and spread as 1/ℓ, swap and step-asides as ℓ); 96 cells, 10,000 wallet paths each, $1,000 wallet, 80% honoured, the payout clock 14 days in trades at each stop's pace. - **Limits.** Ruin within three years at most 5%; the purchase gate, expected log growth over three years above zero; the funded chance of any payout at least 50%; T finite. - **Dominance.** No worse on all three and better on one, where T and D must differ by more than two standard errors of the difference (bootstrap over paths) and G, exact given its inputs, by any amount. - **Pick.** For a drawdown cap D* of $300, $400, $500 and $600, the fastest and the richest front cell with D ≤ D*. The operator chooses D* and between the two. - **What would change the plan.** A front cell at N = 1 other than the deployed stop that beats it on T and G within the same drawdown; a cell at N = 2 on the front; or no feasible cell at all in a scenario, which is the gate closed at every stop. ==================== 111 code # the cost share and the calendar pace along the stop, seconds L26 = [15.0, 20.0, 25.0, 28.0, 30.0, STOP25, 40.0, 45.0] ASIDES26 = 1.33 # step-asides per trade at the deployed stop, section 18 FLOOR26 = 7 / 5 # one entry a trading day, in calendar days SCEN26 = ["commission only", "commission and spread", "random side, swap charged", "random side, with the asides"] X26 = np.array(BM.RR, float) # a b / stop^2 at the deployed stop is the ratio itself D26 = np.array([pace[r] for r in BM.RR]) # the broker's calendar days per trade, section 24 def pace26(l): """Calendar days per trade at a stop of l pips, ratio 1: the broker's pace at equal a b.""" x = (l / STOP24) ** 2 if x < X26[0]: slope = (D26[1] - D26[0]) / (X26[1] - X26[0]) return max(FLOOR26, float(D26[0] + slope * (x - X26[0]))) return float(np.interp(x, X26, D26)) def cost26(l, scen): """Cost share of the risk at a stop of l pips. Swap nights and step-asides scale with the holding time, l^2 by Wald, so their shares scale as l.""" g = PA.cost_share(l, s=0.0) if scen == "commission only" else PA.cost_share(l) if scen.startswith("random side"): g += side_share["coin"] * l / STOP25 if scen.endswith("asides"): g += ASIDES26 * PA.cost_share(l) * (l / STOP25) ** 2 return g print("stop days/trade clock(trades) " + " ".join(f"{s[:22]:>22}" for s in SCEN26)) for l in L26: d = pace26(l) print(f"{l:5.1f} {d:6.2f} {max(PA.MIN_DAYS, round(PA.WAIT / d)):3d} " + " ".join(f"{cost26(l, s):22.2%}" for s in SCEN26)) ==================== 112 code # the deployed rule's knobs for each mean stop, on the section 24 entries: the multiple of yesterday's sigma at the 30-pip floor, and the floor at 0.75 SIG26 = sig24[entry24] def mean_stop26(beta, floor): return float(np.mean(np.maximum(beta * SIG26, floor))) def knob26(l): """(beta at a 30-pip floor, floor at beta 0.75) whose mean stop is l pips; nan where the knob cannot reach l.""" beta = brentq(lambda b: mean_stop26(b, 30.0) - l, 1e-3, 5.0) if l > mean_stop26(1e-3, 30.0) else np.nan floor = brentq(lambda f: mean_stop26(0.75, f) - l, 0.0, 300.0) if l > mean_stop26(0.75, 0.0) else np.nan return beta, floor RULE26 = {l: knob26(l) for l in L26} print(f"entries {len(SIG26)}; yesterday's sigma median {np.median(SIG26):.1f} pips; mean of 0.75 sigma alone {mean_stop26(0.75, 0.0):.1f} pips") for l, (b, f) in RULE26.items(): print(f"mean stop {l:5.1f} pips: max({b:.3f} sigma, 30) or max(0.75 sigma, {f:.1f})" if np.isfinite(b) and np.isfinite(f) else f"mean stop {l:5.1f} pips: " + (f"max(0.75 sigma, {f:.1f})" if np.isfinite(f) else "below both knobs: a fixed stop")) ==================== 113 markdown **One trade a day against several.** With up to k fair brackets a day and the day ended at the first loss, the trades taken in a day have mean $$E[n_k] = \sum_{j=1}^{k} 2^{-(j-1)} = 2\,(1 - 2^{-k})$$ and the day's largest gain is k r, so the 35% cap gives r at most 3.15% / k. Ended at the first win instead, the day's largest loss is k r (1 + γ), and the daily rule at the floor gives r at most 4.55% / (1 + γ) / k. The account's pace in days is set by the variance a day, r² E[n_k], and its odds in the diffusion limit by the drift over the variance a trade, 2γ / r. Both move against k: the variance a day falls, and the smaller risk raises 2γ / r. Checked below on plain barriers at the deployed cost, 200,000 accounts each; the one-a-day row carries the lattice trap of an unclipped 3% step, which the clip removes (section 25: 45.4%). ==================== 114 code # one trade a day against several, the assessment on plain barriers, about a minute g26 = PA.cost_share(STOP25) CAP26 = {"loss": 0.35 * PA.TARGET, # k wins in a day: the 35% cap on the gain "win": PA.DAILY * (1 - PA.MAXDD) / (1 + g26)} # k losses in a day: the daily rule at the floor rows = [dict(k=1, rule="-", risk=0.03, **PA.day_split_mc(1, 0.03, g26 * 0.03))] for rule in ("loss", "win"): for k in (2, 3): r = CAP26[rule] / k rows.append(dict(k=k, rule=f"stop after a {rule}", risk=r, **PA.day_split_mc(k, r, g26 * r, rule))) K26 = pd.DataFrame(rows) K26["days_vs_1"] = K26.days_pass / K26.days_pass.iloc[0] K26["var_vs_1"] = [(r["risk"] ** 2 * 2 * (1 - 2.0 ** -r["k"])) / 0.03 ** 2 if r["k"] > 1 else 1.0 for _, r in K26.iterrows()] print(K26.to_string(index=False, formatters={"risk": "{:.3%}".format, "p_pass": "{:.1%}".format, "days_pass": "{:.1f}".format, "trades": "{:.1f}".format, "days_vs_1": "{:.2f}".format, "var_vs_1": "{:.2f}".format})) ==================== 115 markdown **The cells.** 32 chains, a line search of the funded risk in each, then the wallet at one, two and three accounts for each: about 25 minutes, progress printed per cell. ==================== 116 code # the front's cells: chains per stop and scenario, then the wallet at 1 to 3 accounts, ~10 minutes N26, YEARS26, HONOUR26 = 10_000, 3, 0.8 def chain26(l, scen): g, d = cost26(l, scen), pace26(l) wt = max(PA.MIN_DAYS, int(round(PA.WAIT / d))) e = PA.eval_chain(0.03, g * 0.03, rr=1.0, clip_up=True, clip_dn=True) rows_ = PA.funded_line_search(g, lo=0.006, hi=0.020, step=0.00025, wait=wt) pk = min(PA.plateau(rows_), key=lambda r: (r["med_first_pay"], -r["e_W"])) f = PA.funded_chain(pk["risk"], g * pk["risk"], PA.payout_threshold(pk["risk"], 1.0, 3000, SPLIT), rr=1.0, clip_dn=True, wait=wt) cash = SPLIT * 3000 * e["p_pass"] * f["e_W"] days = d * (e["e_trades"] + e["p_pass"] * f["e_life"]) return dict(stop=l, scenario=scen, cost=g, pace=d, wait=wt, rf=pk["risk"], p_pass=float(e["p_pass"]), p_any=float(f["p_any"]), cash_fee=cash / T3_25.fee, cycle_days=days, G1=(HONOUR26 * cash - T3_25.fee) * 365 / days) def boot_se(x, stat, reps=200, seed=0): rng = np.random.default_rng(seed) return float(np.std([stat(x[rng.integers(0, len(x), len(x))]) for _ in range(reps)], ddof=1)) def wallet26(c, rho): H, H1 = int(round(YEARS26 * 365 / c["pace"])), int(round(365 / c["pace"])) out = [] for k, F, G in SP.family(T3_25, rho, 0.03, c["cost"] * 0.03, SPLIT, 1.0, H, N26, SEED, k_max=3, p_paid=HONOUR26, funded_risk=c["rf"], funded_cost=c["cost"] * c["rf"], clip_up=True, clip_dn=True, wait=c["wait"]): w = HG.shared_wallet(F, 1000.0, G, paths=True) p = w.pop("paths") dd1 = HG.shared_wallet(F[:, :H1], 1000.0, G[:, :H1], paths=True)["paths"]["maxdd"] t = np.where(np.isnan(p["first_cash"]), np.inf, p["first_cash"] + 1) * c["pace"] q90 = lambda x: np.percentile(x, 90) out.append(dict(c, N=k, G=k * c["G1"], T=float(np.median(t)), T_se=boot_se(t, np.median), D=float(q90(dd1)), D_se=boot_se(dd1, q90), D_med=float(np.median(dd1)), ruin=w["p_ruin"], log_g=w["e_log_growth"], log_se=float(p["log_g"].std(ddof=1) / np.sqrt(N26)), mean_final=w["mean_final"])) return out t0 = time.time() CH26 = [chain26(l, s) for s in SCEN26 for l in L26] print(f"chains {time.time() - t0:.0f} s") rows = [] for c in CH26: rows += wallet26(c, SP_RHOS["staggered"]) print(f"{c['scenario'][:24]:24} {c['stop']:5.1f}: {time.time() - t0:.0f} s", flush=True) FR26 = pd.DataFrame(rows) gc.collect() ==================== 117 code # the declared rule: the feasible cells, then the front, per scenario PAYFLOOR26, RUIN26 = 0.50, 0.05 DSTAR26 = [300.0, 400.0, 500.0, 600.0] def dominated26(df): """A cell is dominated when another is no worse on T, G and D and better on one, where T and D differ only beyond two standard errors of their difference and G, exact given its inputs, by any amount.""" T, Ts, G, D, Ds = (df[k].to_numpy() for k in ("T", "T_se", "G", "D", "D_se")) out = np.zeros(len(df), bool) for i in range(len(df)): mT, mD = 2 * np.hypot(Ts[i], Ts), 2 * np.hypot(Ds[i], Ds) no_worse = (T <= T[i] + mT) & (G >= G[i]) & (D <= D[i] + mD) better = (T < T[i] - mT) | (G > G[i]) | (D < D[i] - mD) hit = no_worse & better hit[i] = False out[i] = hit.any() return out FR26["feasible"] = ((FR26.ruin <= RUIN26) & (FR26.log_g > 0) & (FR26.p_any >= PAYFLOOR26) & np.isfinite(FR26["T"])) FR26["front"] = False for s in SCEN26: m = (FR26.scenario == s) & FR26.feasible FR26.loc[m, "front"] = ~dominated26(FR26[m]) def picks26(s): """The fastest and the richest front cell under each drawdown cap.""" f = FR26[(FR26.scenario == s) & FR26.front] out = [] for cap in DSTAR26: ok = f[f.D <= cap] if len(ok): out.append(dict(scenario=s, cap=cap, fastest=ok.loc[ok["T"].idxmin()], richest=ok.loc[ok.G.idxmax()])) return out PICK26 = [p for s in SCEN26 for p in picks26(s)] for s in SCEN26: d = FR26[FR26.scenario == s] print(f"\n{s}: {int(d.feasible.sum())} of {len(d)} cells feasible, {int(d.front.sum())} on the front") for _, r in d.iterrows(): tag = "front" if r.front else ("dominated" if r.feasible else "infeasible") print(f" stop {r.stop:4.1f} N {r.N} T {r['T']:5.0f} d (se {r.T_se:3.0f}) G {r.G:+6.0f} $/yr D p90 ${r.D:4.0f} (se {r.D_se:3.0f}) " f"ruin {r.ruin:5.1%} log g {r.log_g:+.3f} any payout {r.p_any:5.1%} funded {r.rf:.3%} {tag}") print() for p in PICK26: fa, ri = p["fastest"], p["richest"] print(f"{p['scenario'][:26]:26} D* ${p['cap']:.0f}: fastest stop {fa.stop:.1f} N {fa.N} ({fa['T']:.0f} d, {fa.G:+.0f} $/yr, ${fa.D:.0f}); " f"richest stop {ri.stop:.1f} N {ri.N} ({ri['T']:.0f} d, {ri.G:+.0f} $/yr, ${ri.D:.0f})") ==================== 118 code open26 = [s for s in SCEN26 if FR26[FR26.scenario == s].feasible.any()] fig, ax = plt.subplots(2, max(1, len(open26)), figsize=(4.2 * max(1, len(open26)), 6.4), sharex="col", squeeze=False) cols26 = {1: PALETTE["BLUE"], 2: PALETTE["ORANGE"], 3: PALETTE["RED"]} for j, s in enumerate(open26): d = FR26[FR26.scenario == s] for n, g in d.groupby("N"): g = g.sort_values("stop") for i, key in enumerate(("G", "D")): ax[i, j].plot(g["T"], g[key], color=cols26[n], lw=0.8, label=f"{n} account{'s' if n > 1 else ''}") for _, r in g.iterrows(): face = cols26[n] if r.front else "white" mark = "o" if r.feasible else "x" ax[i, j].scatter([r["T"]], [r[key]], s=18, marker=mark, facecolor=face if mark == "o" else cols26[n], edgecolor=cols26[n], zorder=5) if n == 1 and i == 0: ax[i, j].annotate(f"{r.stop:.0f}", (r["T"], r[key]), fontsize=6, xytext=(3, 3), textcoords="offset points") ax[0, j].set_title(s, fontsize=9) ax[0, j].set_ylabel("G, $ a year at 80% honoured") ax[1, j].set_ylabel("D, p90 year-one drawdown, $") ax[1, j].set_xlabel("T, median days to first cash") ax[0, j].axhline(0, color=PALETTE["INK"], lw=0.6) for a_ in ax[:, j]: style_axes(a_) ax[0, 0].legend(frameon=False, fontsize=7) fig.tight_layout() fig.savefig("src/notebooks/reports/topics/prime_academy/figures/front.png", dpi=130) plt.show() ==================== 119 markdown **Figure.** One column per cost scenario with any feasible cell. Top: the cash rate G against the days to first cash T; bottom: the year-one drawdown D against T. One line per number of accounts through the stops, each one-account point labelled by its stop. Filled: on the front; hollow: feasible but dominated; cross: fails a limit. A front that ran up and to the left in the top panel while falling in the bottom one would mean speed, cash and drawdown do not conflict and one cell wins outright; they do conflict wherever the lines rise to the left in both panels. ==================== 120 code # the paper reads this file fr26 = {"commit": subprocess.run(["git", "rev-parse", "--short", "HEAD"], capture_output=True, text=True).stdout.strip(), "paths": N26, "years": YEARS26, "honour": HONOUR26, "payfloor": PAYFLOOR26, "ruin_cap": RUIN26, "stop0": STOP24, "floor_days": FLOOR26, "asides": ASIDES26, "swap_coin": side_share["coin"], "pace_rr": [[float(x), float(y)] for x, y in zip(X26, D26)], "ksplit": K26.to_dict(orient="records"), "rule": [[float(l), float(b), float(f)] for l, (b, f) in RULE26.items()], "cells": json.loads(FR26.to_json(orient="records")), "picks": [dict(scenario=p["scenario"], cap=p["cap"], fastest=dict(stop=float(p["fastest"].stop), N=int(p["fastest"].N)), richest=dict(stop=float(p["richest"].stop), N=int(p["richest"].N))) for p in PICK26]} json.dump(fr26, open("src/notebooks/reports/topics/prime_academy/paper/front.json", "w"), indent=1) n_front = {s: int(FR26[(FR26.scenario == s) & FR26.front].shape[0]) for s in SCEN26} print("26 verdict by the declared rule: front cells per scenario " + ", ".join(f"{s}: {n}" for s, n in n_front.items())) ==================== 121 markdown ## 12. What this doesn't show **It does not show that the firm pays.** Every number here is conditional on `P(paid)`, and nothing in a rulebook measures it. Governing law is Seychelles and unresolved disputes go to LCIA arbitration in London, one arbitrator, registration alone near GBP 1,950 and non-refundable. Any payout this play generates is smaller than the cost of contesting a refusal, so the discretion is final in practice. General Terms 23.1 also permits termination in the Company's sole discretion at any time, 24.1(b) keeps the fee, and clause 16 forfeits "any existing or future right or entitlement" to payouts. **No rule here is broken.** A fixed-size entry method with a real stop is not martingale sizing, is not the named arbitrage conduct, and is not "gambling-focused". Random entry directions were how `eval_convexity` proved zero edge in simulation; they are not required in deployment, because the pass rate depends only on the barrier ratio and not on how entries are chosen. News trading is banned within 10 minutes either side of high-impact releases, on opening and on closing. Section 18 prices the rule on the exit side, where the venue-held bracket breaks it, and adds the dark rule and the guard the strategy now carries. The venue side of that rule is measured (2026-09-21): deleting a protective leg by its id detaches it with the position left open, and a PATCH re-attaches both under new ids. What is not verified is that the firm's high-impact list is the all-currency one the calendar uses. Expert Advisors and HFT are banned, and this play needs neither. **Both barrier overshoots are assumptions, not measurements.** Every pass rate above absorbs on crossing, so a win lands past the target and a loss past the floor. Section 19 prices what happens if either is wrong. The target overshoot is a free choice and removing it gains several points. The floor overshoot is the firm's to impose, the rulebook's wording is ambiguous, and the reading moves the headline pass rate by about seven points. The lattice, the parametric simulation and the real-path control agree with each other because all three encode the same assumption, which is not evidence that it holds. **The path model is a proxy.** The parametric walk is the right object for a scale-invariant barrier problem, but the real-path control runs on MNQ futures sessions, not forex. It bounds the intrabar and autocorrelation penalty at about 3 points at 4% risk; it does not establish that forex paths carry the same penalty. **The cost stack is documented at the low end.** Commission is $5 per lot at opening on forex (Rules and Conditions V.2 p.3) and the account is RAW with spreads quoted from 0.0 pips (FAQ), so at the 60-pip intraday stop of section 3c the commission is 0.83% of risk and a conservative 0.5-pip spread another 0.83%, the derived 1.67%. The 5% cost column is a stress case. The pair's daily sigma, 60 pips, is an input and sets the stop cap and through it the cost. Evaluation accounts carry standard swaps, so an overnight hold pays a cost this model does not include. Still unverified: whether the 35% distribution cap applies during the assessment, any recurring charge once funded, where the 14-day payout clock starts, the live spread behind the login, and the 0.01 minimum lot the tier-free pass rate rests on. The complaint policy is read: email or live chat first, then LCIA. Whether the firm liquidates AT the drawdown limit or after it turns out **not** to matter. Withdrawals all happen before the account dies, so the trade that kills it cannot be withdrawn either way. Measured both ways: E[withdrawn] 9.63% with overshoot allowed against 9.69% with a hard floor, while the firm's booked loss moves from 10.27% to 9.00%. The rule moves the firm's loss and nothing of the trader's. **The wallet model of sections 6 to 14 holds one account at a time.** Section 15 lifts that. Both ignore the 30-day inactivity rule, which is slack at one trade per day, and assume the wallet is used for nothing else. **Maven (section 13).** Its funded floor trails the cycle's highest equity, and the simulation reads equity once a day, so an intraday peak above the day's close would set the floor higher than modelled; the number is optimistic by that gap. Four rules are unverified and each moves the result: whether the 3% minimum and the 10-day cadence apply to the BNPL funded stage, whether the trailing floor caps at breakeven, the live spread, and any evaluation time limit. The 2% account saver is modelled as a hard cap on the stop, not simulated; a stop filled 0.5% through it on a gap would halve the split. No Maven rule is broken by one bracket trade a day at RR 1.5: EAs are not used, the bracket is above the 1:1.25 "gamifying" line, and 4% of the account is not "a large percentage". **Hedging (section 14).** The mirror is exact in the model. One uniform decides both accounts, so on every day one wins and the other loses. On two live platforms the feeds differ by a fraction of a pip, so a touch at one level can close one account and leave the other open, and that trade is no longer offset. Nothing here models that, or detection, or a payout refused because of the pairing. The first removes part of the offset the gain comes from, and the other two lower P(paid). The Prime Academy leg trades RR 2/3 on a 73.5-pip stop, where the cost share is 1.36% of risk against the traded cell's 1.67%. Its pass rate of 49.12% against 48.67% comes from that wider stop, not from the lower ratio, and section 3b's ranking of ratios at matched cost is unchanged. Maven's side is the section 13 simulation, with the same four unverified rules. **Spreading (section 15).** The k accounts share one common factor with the pairwise correlation of two entry hours, 07:00 and 16:00 UTC on the 30-pip stop. A third and fourth account need more entry hours, and hours closer together correlate more (0.45 at 07:00 against 12:00), so the staggered family is optimistic for k above 2. The measurement is one pair of hours on one pair over 720 days, on hourly bars with the stop taken on a tie. Twelve hand-placed brackets a day is an assumed ceiling, not a rule; the rulebook's own ceiling is $300,000 across all active accounts. Clause 15(h) allows the accounts, and nothing here prices what a risk team makes of one person holding twelve of them at payout time. Every number is at P(paid) = 1. **Which instrument (section 16).** The spread is a top-of-book snapshot at three hours of one day, not a distribution, and the ranking moves with it. The daily bars are the venue's, unadjusted, 250 at most and about 90 for anything listed in May 2026. The universe is the 174 instruments the interface lists, read off the rendered page; the API returns 320 and says leverage 30 on all of them, where the interface enforces 2 on crypto and 5 on stocks. The swap is the interface's figure on the day it was read, and swap rates move. The pace that turns a swap per night into a cost per trade is EURUSD's 3.6 days applied to every instrument, since no other instrument's pace has been measured; the corner's one-day pace is shown as the other end. Nothing here re-runs the real-path control or the walk-forward on another instrument. The interface's markets list once showed NDX100 at 5.53 while its details dialog and the API read 29,092; quotes come from the API, and the list is used for the universe and the leverage only. **GARCH (section 17).** One fit, on 396 days, sits on a + b = 1, so what was tested is not a model with a long-run level but an integrated GARCH with a drift term. A longer sample might identify a level, and a different family with a level this sample does identify has not been tried. Normal errors were assumed in the fit; a heavier-tailed likelihood changes the parameters but not the boundary. The comparison is one split of one pair, the same as section 11. **Verification.** `eval_convexity` loads its path array from a /tmp scratchpad belonging to a session that ended 2026-08-07; the file survives only by luck and nothing in the repo rebuilds it. `core.session_paths()` here reconstructs the same 467 x 350 x 4 array from committed `historical_data/MNQ/ohlcv/60/` bars and caches it under `reports/_cache/prime_academy/`. Re-run clean with `jupyter nbconvert --execute --to notebook --inplace` from the repo root.