第 7 课,共 10 课

The Track Record Starts at One

Keisuke Kurosawa · 发布: 2026-09-16

A backtest that survives module 6's gate is a claim, not a result. Live is where the claim gets tested — and it is the only window you cannot re-run, cannot retune, and cannot quietly decide to exclude.

This module is the machinery that makes the comparison possible at all, the reasons live and backtest disagree even when nothing is broken, and an honest look at what our own record currently says.

The backtest and the live EA were different programs

Start with the deepest reason the two can disagree, because it is not slippage and it is not the broker. It is that you tested a program that does not exist.

Every EA in the suite counts consecutive losses, and module 5's loss-streak gate halts trading at a threshold. That counter was updated inside the trade-reporting hook — and the reporting hook returns early in the tester, because you do not want a backtest posting to a live API.

The consequence went unnoticed for months: GATE_BLOCK_LOSS never fired in any backtest. Every published profit factor described an EA that traded straight through losing streaks that the real one stops on. Not a small modelling difference — a different strategy.

The fix is a separation, and the comment now spells out what it is for:

   // Reporting (HTTP POST + CSV ledger) is suppressed in the tester/optimizer
   // and by the caller's toggle. The loss-streak bookkeeping further down is
   // NOT gated on it: consecLosses feeds the risk guard, so it has to behave
   // identically in a backtest and live. Gating it here is what made every
   // optimization run trade through losing streaks that live would have halted.

And at the point where it matters, the boundary is marked explicitly:

      // Risk bookkeeping — runs in the tester and with tracking off, because
      // the loss-streak gate depends on it. Everything below this point is
      // reporting only.
      consecLosses  = (profit < 0.0) ? (consecLosses + 1) : 0;
      lastCloseTime = TimeCurrent();

      if(!reportingOn)
         return;

The generalisable rule is worth more than the bug. In any code that behaves differently in a test harness, draw one explicit line between "things that change behaviour" and "things that only observe it", and gate only the second. Telemetry, logging and reporting can be switched off freely. State that a decision reads cannot, ever — the moment it is, your test is running a different program from your production.

Every result filed before that fix was marked retired with the reason stated, rather than deleted. They describe an EA that no longer exists, which is a fact about the record, not a reason to hide it.

A trade that is not reported did not happen

The track record is only evidence if it cannot quietly lose trades. Three properties do that work.

Retry, but only what is safe to retry:

   // Bounded retry. Records carry an eventId, so the server can dedupe a
   // resend -> safe to retry TRANSIENT failures (WebRequest -1 / HTTP 5xx,
   // which almost certainly never processed). A 4xx is a permanent client
   // rejection: do not retry it.
   const int maxAttempts = 3;   // 1 initial try + up to 2 retries

Idempotency first, retries second — that is the correct order and most people do it backwards. Because every record carries an event id the server can deduplicate on, a resend is harmless, which is what makes retrying safe at all. Without the id, a retry after a timeout risks double-counting a trade in your own performance figures.

The transient/permanent split matters as much. A 5xx or a dead connection almost certainly never processed, so try again. A 4xx means the server understood you and said no; retrying it twice more just delays the log line that tells you what is wrong.

One failure gets its own error message:

         // 401/403 means the key is wrong or revoked. That is a silent,
         // permanent outage of the whole track-record pipeline, so name it
         // loudly rather than letting it read as one more failed POST.
         if(status == 401 || status == 403)
            Print("TRACK AUTH FAILED (", status, ") - the ingest API rejected the API key. ",
                  "No trades are being recorded. Check KurosawaSecrets.mqh. tag=", debugTag);

This is earned experience. The tracking header once shipped with a placeholder key, so every post failed authentication and no trade was ever recorded — while the EA traded on perfectly happily. An auth failure is not one bad request; it is the entire pipeline being down for as long as the key is wrong. It deserves a message that says so in words, not a status code buried among others.

And there is a local copy. Every close is also appended to a CSV ledger in the terminal's own files folder. If the API is unreachable for a day, the trades still exist somewhere that is not a remote server you do not control.

Disagreements that are nobody's fault

Once the pipeline is honest, live will still not match the backtest. Some of that is real and expected:

Spread in the tester is a model; live it is a distribution with a fat tail at exactly the moments your strategy wants to trade. Module 4's broker-widened stops do not exist in a friendly test. And a partial fill — which CTrade reports as success — silently rescales a trade's contribution to every statistic you compute afterwards.

Then there is the one nobody warns you about, which we met on the first live fix day: margin. Three fix charts fire at 09:55 within the same second. The first one filled. The other two were refused by the broker for insufficient funds, because the account did not hold enough margin for three simultaneous positions.

Nothing was wrong with the EAs. Nothing was wrong with the strategy. The backtest had modelled each chart independently, as the tester does — one instance, one symbol, an account that always has room. Live, they compete for the same balance.

That is a divergence no amount of tick-level modelling would have caught, and it is the practical argument for module 5's account-level caps: the tester will never show you what your charts do to each other.

The metrics have to mean the same thing on both sides

A comparison between backtest and live is worthless if the two numbers are computed differently. Three decisions keep ours honest, and all three were corrections.

Profit factor is undefined, not large. An EA with no losing trades has no denominator. Ours reported 99 — which reads as a 99× profit factor, and three live EAs were publishing it off a single winning trade each. It is now null, the pages render a dash, and the tooltip says why. You can see it live right now: the one EA with a closed trade this month shows a win rate of 100% and no profit factor at all, which is the correct pair of statements about a sample of one.

The portfolio total is the portfolio's, not an average. Profit factor across the book is gross win over gross loss on all trades; max drawdown is peak-to-trough on one merged equity curve. The earlier version averaged per-EA ratios and took the worst single EA's drawdown — which answers "what does a typical EA look like", not "did this book make money". Correcting it moved the published drawdown from 2,220 to 9,334.

Drawdown is closed-trade basis, and the page says so. It does not count how far a position ran against us before it closed. Fixing that needs data the ingest side does not record, so the limitation is labelled rather than quietly ignored.

Note the direction of all three corrections. Every one made the published number worse. That is what to expect: figures assembled from convenient parts drift optimistic, because nobody goes chasing a number that already looks bad.

What the record actually says

Here is ours, and it is not flattering.

Across every EA that has ever reported, the track record holds 313 closed trades, a 43.5% win rate, a portfolio profit factor of 0.74, and a net result of −8,332 yen at minimum lot. Eighteen of the thirty-two EA ids lost money. And 196 of those 313 trades belong to ids that have no row in the preset catalogue at all — they predate it, and the gate in module 6 along with it. Most of them stopped trading in January and February 2026. The record is, to a large extent, a picture of what we were doing before any of this discipline existed.

And the fleet running today — the four fix EAs and the four range-revert instances from module 5 — has one closed trade between them, filed on 15 September 2026, for fourteen yen.

One. Not thirty, not a hundred. The strategies are freshly attached, several were switched to new preset versions days ago, and the fix EAs trade once a day each.

That number is the honest answer to "does this work live", and the honest answer today is we do not know yet. Publishing it at one trade rather than waiting until it looks like something is the whole discipline. A track record that only appears once it is flattering is marketing wearing a lab coat.

Thirty trades

The gate's last rule is what the live period is for:

Then `proven` → `live`: run at minimum lot until at least 30 live trades, and only size up
if `backtest_vs_live_pf_delta` stays within tolerance.

Thirty is not a magic number — module 6 was explicit that the standard error at n=30 is still wide. It is a floor, not a proof. What the first thirty trades are actually for is detecting the divergences in this module: whether fills happen where the tester said, whether spread at your entry time resembles what you modelled, whether the account can even carry the positions.

Those are questions with answers at n=30. "Is the edge real" is not, and sizing up on thirty live trades because the profit factor looked good would be exactly the mistake module 6 spent its length arguing against.

What you should have now

A hard line between code that observes and code that decides, so your test harness runs the same program you ship; a reporting path with idempotent records, bounded retries on transient failures only, a named alarm for a dead credential, and a local copy; metrics defined once and shared by the page and the API; and a live record published from its first trade, not its thirtieth.

And the expectation to carry into your own first live month: your backtest was the optimistic case. It was built on your data, your assumptions about cost, and an account that never ran out of margin. Live is the same strategy with none of those courtesies.

Next module: what to do when live and backtest disagree — separating a broken assumption from a dying edge, and why "retune it" is almost always the wrong first move.