# Prime Academy zero-edge play: correctness and robustness audit (candidate 2)

Audited 2026-10-01 against: `trading-framework` research checkout at `a183ffa` (modules, paper, `decision.json` / `front.json` / `broker_map.json` at commit `a9c26ae`), `trading-framework-prime` at `c4b8e85` (strategy, runner, notebook sections 24-26, executed copy `scratchpad/s26/run2.ipynb`). Every number below was recomputed with the repository's own `modules/prime.py`, `spread.py` and `hedge.py`, or read from the files cited. Monte Carlo runs are 5,000 paths each, one process at a time. Scripts and their outputs are in this folder and are named at each finding.

Labels: **verified** (by execution or by reading the code and the file), **inferred** (arithmetic on verified numbers), **unverified** (depends on the firm or on data not held).

## Verdict in four lines

- The deployed purchase gate is not open. Its statistic is zero within Monte Carlo error, and every input threshold the paper lists sits exactly at the assumed value.
- The sign of the gate is set by a modelling choice the paper does not list as open: what a refused payout does to the account. The three readings give growth of +0.16, 0.00 and -0.18.
- The code that would trade, `run_live.py`, wires 4% risk, no target clip, no floor clip, no funded stage and no payout logic. The paper's plan table describes a configuration that no runnable entry point produces.
- The chains, the wallet, the rules encoding and the front's arithmetic are correct where I could check them; the fragility is in the inputs and in two inconsistent clocks, not in the kernels.

## Findings, by severity

### 1. The purchase gate reads zero within error; "open" is a one-seed result

- **Claim.** The published gate value, expected log growth +0.013 at 80% honoured (paper line 76, `tab_gate.tex`, `decision.json` `g_80` 0.0132), comes from one seed at 10,000 paths and is about 1.6 standard errors from zero. Verified by execution.
- **Evidence.** `c2_seeds.py` -> `c2_seeds.out`: 8 seeds x 5,000 paths at the deployed cell (3% then 1.575%, cost 3.35%, 7-trade clock, 541 trades) give per-seed growth +0.015, -0.012, -0.020, +0.011, +0.004, +0.009, +0.000, +0.009. Mean +0.002, seed-to-seed SD 0.012, within-run SE 0.008, implied SE at 10,000 paths 0.0085. Four of eight seeds are negative. `c2_gate.py` [2] on seeds 1-4: mean -0.001.
- **Why so noisy.** The log-growth mean is dominated by the ruin atom: 13 to 29 ruined paths per 5,000 (ruin 0.3% to 0.6%), each adding about -4.3 to the log, so one ruined path moves the mean by -0.0009 (`c2_seeds.out`). The notebook's dominance rule gives T and D a 2-SE margin but the gate none (cell 117: `FR26.log_g > 0`; cell 108 `gate=g_[0.8] > 0`).
- **Changes a decision.** Yes: the paper's "with the spread, the commission and the random side's swap the gate is open, by a small margin" (line 76) is not supported. The honest reading is undecided at the assumed inputs.
- **Fix.** Return the standard error from `wallet_mc` (std of the per-path log over sqrt(n)) and declare the gate as growth above two standard errors, or run the gate cell with common random numbers at 200,000 paths across seeds (memory permitting, in chunks). State the verdict as "undecided" until then.

### 2. Every flip threshold sits at the assumed input

- **Claim.** The cost share, the honour rate, the clock and the spread all cross the gate's zero within rounding of the values the paper assumes. Verified by execution (`c2_gate.py` [3]-[5], [9]).
- **Evidence.**
  - Cost share: growth at 80% honoured crosses zero at 3.34% of the risk against the assumed 3.35% (scan 2.49% to 4.50%, pick re-searched at each cost; slope about -0.10 per point of cost share).
  - Honour: crosses zero at 80.1% against the gate's own 80%.
  - Clock: 5 or 6 trades (pace at or above 2.33 days a trade) +0.006; 7 trades -0.001; 9 to 10 trades -0.01; 14 trades -0.032. All inside one standard error except the 12- and 14-trade rows.
  - Spread: the 3.34% root equals a round trip of 1.00 pip, so the gate needs a live spread under 0.50 pip at the measured swap and no step-asides. Each 0.1 pip moves growth by about -0.028.
  - Swap: the random side's swap share +0.48% of risk is 0.01 points above the flip; the credited short side (cost 2.50%) reads +0.084 and is clearly open ([8]).
- **Changes a decision.** Yes: the gate is the boundary, not a verdict on it. The three "pending measurements" cannot resolve a statistic that is zero at the centre of its own input range.
- **Fix.** Report the gate as a boundary in input space (the table in the robustness section) and say which side the live spread and swap put it on once they are read off the platform.

### 3. The gate's sign is set by how a refused payout is modelled

- **Claim.** `wallet_mc` treats a refused request as "the equity leaves the account and the wallet gets nothing" (`modules/prime.py` lines 679-683: `honoured` drawn per request, then `eq = np.where(can, 0.0, eq)` for every request, honoured or not). Two readings at least as consistent with the documents give opposite gate verdicts. Verified by execution; which reading is true is unverified.
- **Evidence.** `c2_refusal.py` (a copy of `wallet_mc` with a `refusal` argument, reproducing the module to the digit at the same seed) -> `c2_refusal.out`, 4 seeds x 5,000 paths at the deployed cell: refusal = equity vanishes (the module) -0.001; refusal = equity stays and the clock restarts +0.162; refusal = account terminated (General Terms 23.1 and clause 16 in `rules.json` `termination`) -0.183. Breakeven honour rates 80.2%, 60.2% and 89.2%.
- **Where the paper states it.** Only inside the proof of the cash-rate proposition (line 1359, "A refused request withdraws the equity and pays nothing"); it is absent from the open-questions table (lines 1500-1518) and from the gate paragraph.
- **Changes a decision.** Yes: a 0.34 swing in the gate statistic from one unstated reading, larger than the whole cost-share range the paper scans.
- **Fix.** Add `refusal` to `wallet_mc`, print the gate under all three readings in notebook section 25, and add the row to the open-questions table with "the firm" as who settles it.

### 4. The live runner wires none of the deployed decision

- **Claim.** `run_live.py` trades 4% risk on every trade, never calls `set_account`, and so has no target clip, no floor clip, no funded stage at 1.575% and no payout logic. Verified by reading.
- **Evidence.**
  - `src/strategies/prime_academy_strategy/run_live.py` line 74: `FROZEN = dict(risk=0.04, ...)`; `build()` lines 273-277 constructs the strategy and the broker and nothing else; `grep -rn set_account src/` finds the definition (strategy line 261) and one caller, `modules/broker_map.py` line 63, a replay.
  - The strategy's account rules live only behind `acct_on` (`set_account`), so with the runner the legs are unclipped and the equity is never scored (strategy lines 713-739, 812-813).
  - `acceptance.py` line 53 gates the bare bracket at a $50,000 bankroll and 4% risk; `test_prime_academy_strategy.py` has no reference to `set_account`. The account-rules path has no parity gate and no unit test; its only check is section 24's broker map, whose declared test fails (`broker_map.json` `declared: "DISAGREES"`, 39% of cells beyond 2 SE).
  - The live unit runs this file: `systemctl show prime-academy` ExecStart `run_live.py`, active since 2026-09-29 23:43; `/etc/prime-academy.env` reads `PRIME_SIDE=coin`, `PRIME_WINDOW=0-0`, `PRIME_BANKROLL=3000` (keys read, not the password). The window is shut, so nothing has traded, and nothing was touched.
  - The paper contradicts itself about this. The plan table (lines 44-46) lists 3% then 1.575% and "the target shortened to land on +9%, the stop placed on the floor" as the deployed configuration. Section 17 (line 1548) says "The target is not clipped. ... It is not implemented. The strategy is deliberately blind to the firm's account rules". Section 14.11 says the strategy carries the rules in replays through `set_account`. All three are true of different code paths. `README.md` lines 5-6 still say 4% and a fixed side.
  - At 4% the runner's own winning day is 44% of the 9% target, which breaches the 35% cap the paper adopts as the binding rule in the assessment (3.15%, `decision.json` `cap`).
- **Changes a decision.** Yes, for deployment: opening the window today would run a different play from the one priced.
- **Fix.** Give `run_live.py` `--risk-eval`, `--risk-funded`, `--stage` and call `set_account(stage, 0.03, 0.01575, 0.09, 0.09, 2 x commission_per_side, bankroll, 7, 100/0.7/3000, 0.35, 3, CYCLE_NONE)` in `build()`; add a `set_account` replay to `acceptance.py` pinned against `eval_chain`; rewrite the plan table to name the runner's actual state until then.

### 5. The paper's headline table mixes a 14-trade clock with a 7-trade clock

- **Claim.** In Table 2 of Section 1, "funded: days to the first payout 46, median" and "account life 123 days" come from the broker map run with `PAY_WAIT = 14` trades (about 28 calendar days), while "chance of any payout 52.9%" in the same table is at 7 trades. Verified by reading and execution.
- **Evidence.** `modules/broker_map.py` line 34 `PAY_WAIT = 14` and line 63 passes it to `set_account`, where the strategy counts it in trades (`since_pay += 1` per completed trade, line 316; `since_pay >= pay_wait`, line 338). `make_numbers.py` line 982 reads `\decdaysfirstpay` and `\decdayslife` from that grid (`numbers.tex`: 46 and 123). The whole of Section 14's funded map is on the same clock: `make_numbers.py` line 786 calls `funded_chain` without `wait`, so the module default 14 applies, and the text at line 1146 says "with the fourteen-trade clock". On the 7-trade clock the exact chain's median first payout is 14 trades, about 29 calendar days at the broker's 2.05 days a trade (`c2_floats.out` [c]); at RR 1 and 1.5%, cost 2.87%, the chance of any payout is 54.5% against the map's 52.4% (`\mapfundtoday`, `\mapanyonefive`), median first payout 14 against 18 trades, and the 4% comparison cell 32.3% against 29.4% (`c2_rho_clock.out` [b]).
- **Changes a decision.** Not the pick: the ridge and its ordering stand. It changes two quoted numbers in the headline table by about 17 days and the funded map's levels by about 2 points.
- **Fix.** Rerun `broker_map` with `PAY_WAIT` set from the pace (7) or label every broker duration with its clock; pass `wait=7` in `make_numbers.py` line 786.

### 6. The published pick rule does not select 1.575%; an unstated tie-break does

- **Claim.** The paper says the funded risk is "the risk with the fewest trades to the first payout" inside the plateau (Section 14.10). On the exact chain the median first payout is 14 trades for every plateau risk from 1.35% to 1.575%, so the rule does not choose; the notebook's tie-break "ties to the larger gross withdrawn" (cell 105) picks the plateau's upper edge. Verified by execution.
- **Evidence.** `c2_gate.out` [1], plateau rows: risk 1.35% to 1.575% all read median 14 trades, `e_W` rising from 0.0629 to 0.0654. The pick 1.575% has chance of any payout 52.88% against the plateau best 53.86% at 1.375%, that is 0.98 points under the best and 0.02 points inside the one-point plateau. The pick moves with cost only through the edge: 1.525% at 2.49%, 1.55% at 2.87%, 1.575% from 3.00% to 3.45%, 1.60% at 3.55% to 3.65%, 1.625% at 3.80% to 4.00%, 1.65% at 4.50% ([3]); stable at 1.575% for clocks of 5 to 12 trades, 1.60% at 14 ([5]).
- **Changes a decision.** Not in substance: by the paper's own argument any plateau member is equivalent. It changes what the paper should say, and it feeds the front: the sawtooth makes G jump between neighbouring stops. Example (`c2_front.out` [2]): scaling the swap by the measured pace lowers the 40-pip cost from 3.05% to 2.97%, the pick moves to 1.525%, and G falls from $68.0 to $66.6 a year, a lower cost giving less cash.
- **Fix.** State the tie-break in the paper, or pick the plateau's argmax, and print the pick's chance of any payout beside the best.

### 7. "40 pips is the richest" rests on $3 a year and a 4.4% pace error

- **Claim.** Under the swap scenario the front orders 40 pips above 34.8 pips on G by $68.0 against $65.2 a year, and the ordering flips when the mapped 40-pip pace is 2.38 instead of 2.28 calendar days a trade. Verified by execution (`c2_front.py` [1]-[2]).
- **Evidence.** The script rebuilds the notebook's `cost26`, `pace26` and `chain26` and reproduces front.json's G to 0.1 $/yr at 30, 40 and 45 pips. The pace at 40 pips is not measured there: it is read off the RR 1.25 and 1.5 cells through the Wald mapping (a stop of sqrt(RR) x 34.8 pips). Holding the funded pick fixed at 1.575% or 1.5% at both stops keeps 40 ahead; scaling the swap by pace keeps 40 ahead. The dominance rule treats G as exact "by any amount" (notebook cell 117) although the paper's own Section 15 names the pace as the one estimated input.
- **What does hold.** The wallet at one account, 3 seeds x 5,000 paths (`c2_front.out` [3]): 40 pips reads T 155-162 days, D $342, growth +0.036 +/- 0.011; 34.8 pips reads T 144-148, D $342-377, growth +0.005 +/- 0.006. The growth difference is about 2.5 standard errors and the T difference about 10 days, both robust. The 40-pip cell is also the one cell of the swap scenario whose gate is clearly open on its own (about 3 standard errors), where the deployed 34.8-pip cell is not (finding 1).
- **Corroboration of the mapping (pass).** The walk-forward table `tab_wf.tex` measured trading days a trade on the real pair with random-hour entries: 1.90 at a 34-pip mean, 2.43 at 45, 3.47 at 60 (second half). The ratios 1.28 and 1.83 match the Wald-mapped ratios 1.25 and about 1.72. Inferred from the paper's own table.
- **Changes a decision.** The operator's 40-pip, one-account choice stands on drawdown and on growth; the "richest" label is inside the pace's error and should not be the reason.
- **Fix.** Give G an error bar from the pace's block-clustered SE and the mapping's spread (the 45-pip residual of 2.5%), and apply the same 2-SE margin to G as to T and D.

### 8. The second account at 40 pips depends on an account correlation measured once

- **Claim.** The one front cell with two accounts under commission and spread (40 pips: T 98 days, G $161, D $532, ruin 3.0%, growth +0.060) stays feasible up to a normal correlation of about 0.69 between the two accounts' daily coins (outcome correlation about 0.48) and fails the gate above it. The paper uses 0.26 outcome correlation (normal 0.40) measured at the hours 07:00 and 16:00 UTC over 718 days. Verified by execution (`c2_front.py` [4], `c2_rho_clock.py` [a]).
- **Evidence.** 2 seeds x 5,000 paths, two streams through `spread.family` and `hedge.shared_wallet`: independent, ruin 1.7-2.1%, growth +0.10/+0.13; normal 0.40, ruin 3.0-3.5%, growth +0.055/+0.080 (published 3.0%, +0.060); 0.50, +0.039; 0.60, +0.023; 0.65, +0.012; 0.70, -0.005/-0.001 with ruin 4.4-4.7%; copies, ruin 8.7-9.2%, growth -0.17. The 34.8-pip two-account cell reproduces the published failure (ruin 5.0-5.2%, growth -0.02/-0.01).
- **Changes a decision.** Yes, conditionally: the second account is admissible only if the second entry hour's outcome correlation with the first is under about 0.45. A second hour inside the 07:00-10:00 window is closer to the first than 16:00 is, and the paper's own Section 17 says closer hours correlate more.
- **Fix.** Run `fxcontrol.pair_corr` at (07,08), (07,09), (07,10) and (07,16) on the cached bars before any second purchase; it needs no new data.

### 9. No step-aside rate opens the gate at a 0.5-pip spread

- **Claim.** With the gate's zero at a 3.34% cost share, the largest step-aside rate per trade that keeps it open is 0.00 at 0.5 pip of spread, 0.11 at 0.4 pip, 0.25 at 0.3, 0.42 at 0.2, 0.66 at 0.1 and 0.99 at zero spread. Inferred from the verified root and the paper's cost decomposition (`c2_gate.out` [9]).
- **Arithmetic.** With spread s in pips, aside rate A per trade and the measured swap share, the cost share is

$$
\gamma = \frac{0.5 + s}{34.8}(1 + A) + 0.0048
$$

and the gate needs it under 0.0334.

- **Evidence on the calendar.** The news battery cache (`_cache/prime_academy/news_battery.json`) counts 2,393 high-impact events on the cohort, of which USD 974 and EUR 206, so a EUR+USD calendar holds 49% of them; if asides scale with events the rate falls from 1.33 to about 0.66 a trade, which closes the gate at any spread above about 0.1 pip.
- **Changes a decision.** Yes: the paper (line 76) presents the step-aside rate on the pair's calendar as one of three measurements that decide the gate. It can open the gate only jointly with a spread at or under about 0.1 pip.
- **Fix.** Print the boundary table in section 25 and say that the spread, not the calendar, is the variable.

### 10. What checked out

All verified by execution unless marked.

- `decision.json` reproduces to four decimals from the modules: pick 1.575%, chance of any payout 0.5288, plateau 1.15% to 1.575%, pass 0.4461, cash over fee 1.612, breakeven honour 0.620 (`c2_gate.out` [1]). `front.json` reproduces G to 0.1 $/yr at 30, 40 and 45 pips and T, D and growth within seed error (`c2_front.out` [1], [3]).
- `eval_chain` against the rulebook Monte Carlo `assess` at the deployed cell, both clips: pass 0.4461 against 0.4530 at 5,000 accounts (+0.99 SE), mean trades 9.65 against 9.75 (`c2_floats.out` [b]). `funded_chain` against `funded` at the pick: 0.5067 against 0.5052 (-0.21 SE), gross 0.0640 against 0.0624 ([c]).
- Clipped legs land exactly: over 200,000 random equity states no clipped win misses the target and no clipped loss misses the floor in floating point ([a]), so `eq >= TARGET` and `eq <= -MAXDD` fire as the lattice does.
- Rules encoding at the deployed cell ([d]): target and floor 9% clipped net of the round trip (`_legs`, lines 205-216); 35% cap read gross gives 3.15% and the 3% win is 33% of the target; payout threshold max(4.76%, 1.575/0.35 = 4.50%) = 4.76%, so the $100 minimum binds and the cap does not; daily rule inert at one trade (loss 3.10% against 4.55%); 3-day minimum inert because `wait = max(MIN_DAYS, round(14/d))` keeps the clock at or above 3 (the chain has no `MIN_DAYS` term and would be wrong only below that); allocation cap `k_cap` 12 against N at most 3. The lattice rounds the funded risk to 1.5749% ([e]).
- The 7-trade clock is applied the same way in `funded_chain` (pay from trade `wait`), `wallet_mc` (`since >= wait` after the increment) and the strategy (`since_pay >= pay_wait` after `since_pay += 1`); the clock restarts at every payout in all three. Off-by-one checks on ruin (`ruined |= idle & ~buy`, line 647, the day after death), first-cash day (`first_cash + 1` times the pace in cell 116) and the shared-wallet purchase gate (wallet at the previous close against the day's need) found nothing.
- The Wald mapping of pace to stop is corroborated by the walk-forward's measured paces (finding 7).
- The swap and aside shares scale with the stop as Wald's holding time; replacing that with the measured pace changes the 40-pip cost by 0.08 points and flips nothing (inferred, `c2_front.out` [2]). At 15 to 20 pips the Wald scaling understates both by about three times, but those cells fail the limits for other reasons under spread.

### 11. Cosmetic

- Plan table line 50, "a second only after a payout is honoured": no model buys the second account after a payout; the front buys N on day 0 (`spread.family`). Verified by reading.
- `README.md` lines 5-6 describe 4% and a fixed side; "Not in this branch: the firm's account rules" is true of the runner and false of `set_account` in the same file. Verified by reading.
- The strategy's daily rule (`_score_account` line 319) compares the trade's loss to 5% of the equity at the trade's open, not the previous day's equity; equivalent at one trade a day, and the strict reading is already an open item. Verified by reading.
- The pace extrapolation below 24.6 pips sets the commission-only front's 15- and 20-pip cells (`front.json`: T 104 and 110 days, G $91 and $118). G scales as 1/d, so a pace of 2.0 instead of 1.4 days would read $64 and T about 149 days at 15 pips, dropping it off that front. Inferred; not the deployed decision.
- The funded payout-rate limit (at least 50%) holds down to a 4.5% cost share (50.2%, `c2_gate.out` [3]).

## Robustness table

Each cell gives the value of the input at which the decision flips, or the move it causes. "Gate" is expected log growth over three years at 80% honoured, open when positive; its Monte Carlo SE is about 0.0085 at 10,000 paths. Sources: `c2_gate.out`, `c2_seeds.out`, `c2_refusal.out`, `c2_front.out`, `c2_rho_clock.out`.

| input (assumed) | purchase gate (open at +0.013) | funded pick 1.575% | assessment 3% | 40 pips, one account | second account at 40 pips (spread scenario) |
|---|---|---|---|---|---|
| Monte Carlo seed | flips: 4 of 8 seeds negative; mean +0.002, SE 0.0085 at 10k paths | none (exact) | none | 40 beats 34.8 on growth by 2.5 SE and on T by 10 days; "richest" by $3/yr is inside error | growth +0.055 to +0.080 over 2 seeds |
| live spread (0.5 pip) | flips at 0.50 pip; -0.028 per 0.1 pip | 1.55% at 2.87% cost, 1.60% at 3.55% | none | no flip | infeasible with the swap at any spread; feasible without swap |
| random-side swap (+0.48% of risk) | flips at +0.47%; always short (+0.084) is open | none in 3.00-3.45% | none | no flip (cost 3.05% to 2.97% under pace scaling) | the swap scenario closes N=2 (published -0.049) |
| step-aside rate (1.33 a trade, closed) | opens only below 0.00 at 0.5 pip, 0.25 at 0.3 pip, 0.66 at 0.1 pip, 0.99 at zero spread; EUR+USD calendar is about 0.66 | 1.7% at 7.18% | none | no cell feasible with asides | no cell feasible with asides |
| honour rate (80%) | flips at 80.1% (equity vanishes), 60.2% (equity stays), 89.2% (terminated) | none | none | 40-pip cell's own gate about +0.036, flips near 76% (inferred from slope) | flips near 76% (inferred) |
| refusal reading (equity vanishes) | +0.162 stays / -0.001 vanishes / -0.183 terminated | none | none | same ordering, same direction | same |
| clock in trades (7, from 2.03 d a trade) | 5-6 trades +0.006; 8 -0.002; 10 -0.011; 14 -0.032 | 1.575% for 5-12 trades; 1.60% at 14 | none | the 40-pip clock is 6 trades (pace 2.28); 5 at 2.80 | same |
| 35% cap in the assessment (applies) | not applying: 4% reads +0.025 (1.5 SE), not a flip | none | flips to 4%: pass 44.6 to 45.6%, trades 9.65 to 5.85 | none | none |
| floor on the tick (yes) | on review: +0.091, clearly open | none (not re-picked) | pass 44.6 to 46.8% | none examined | none examined |
| pace at 40 pips (2.28 d, Wald-mapped) | not relevant | none | none | G ordering flips at 2.38 d (+4.4%) | T and D move with it, ruin does not |
| pace below 24.6 pips (extrapolated) | not relevant | none | none | none | none; it sets only the commission-only front's 15-20 pip cells |
| correlation of accounts (outcome 0.26, normal 0.40) | not relevant | none | none | none | feasible to normal 0.69 (outcome about 0.48); ruin under 5% to 0.70; copies ruin 9% |
| Wald scaling of swap and asides | no flip | moves the 40-pip pick 1.575% to 1.525% | none | no flip | no flip |

## What each decision should now read

- Purchase: undecided at the assumed inputs. Open only if the live spread reads under 0.5 pip with the random side's swap, or under about 0.1 pip with the pair's own news calendar, and only under the "equity vanishes" or "equity stays" reading of a refusal. The 40-pip cell is open with a margin the deployed 34.8-pip cell does not have.
- Funded risk: any plateau member; 1.575% is the edge the tie-break selects. Say so.
- Assessment risk: 3% stands under the cap reading; 4% if the firm says the cap does not apply.
- Stop and accounts: 40 pips, one account, on drawdown and growth; the second account only after `pair_corr` at the second hour reads under about 0.45.
- The runner: not the priced play until `set_account` is wired with the two risks and the 7-trade clock.
