The only tool is a rolling walk-forward, and what it validates is not a set of parameters.
You have read the same sentence in textbooks, and even in our own How to Read a Backtest: do not optimize on all of the data, hold out at least 20–30% for out-of-sample validation. Then you open the workspace, click through every tab, and cannot find that ratio setting anywhere.
Not finding it is normal — the product does not have that feature. Out-of-sample on Blave has exactly one implementation: the rolling walk-forward on the fifth tab, which takes days rather than a ratio and validates something rather different from a simple split. This article sets out where the difference lies and how to read every number on that tab.
How to Avoid Overfitting has a table grading three approaches: optimize on all data (useless), train on the first 70% and test on the last 30% (useful but limited), and rolling re-selection (the most realistic). That table is a judgement at the conceptual level; the product-level fact is that only the third one has a button.
| Approach | Conceptually | On Blave |
|---|---|---|
| Optimize on all history, report on all history | In-sample, necessarily optimistic | The Backtest tab scores on the whole history by design, which is exactly why a separate check is needed |
| One cut: train on the front, test on the back | Useful, but usable only once | This feature does not exist; there is no ratio setting to find |
| Rolling walk-forward: re-pick each run, test the next stretch | The closest thing to live trading | The workspace's fifth tab, Out-of-Sample |
The difference is more than the interface. A simple split validates whether this one parameter set does well on the later data; a rolling walk-forward validates whether the practice of picking parameters from history still stands up on this strategy at all — it repeats the act of "pick parameters from a stretch of history" n times over, scoring each one on the next stretch it has not seen.
In one sentence: re-pick parameters inside every training window, apply them to the test window that immediately follows, and stitch all the test windows into one line.
| Run | Training window | Test window |
|---|---|---|
| 1 | 2022-01-05 → 2024-12-30 | 2024-12-31 → 2025-01-29 |
| 2 | 2022-02-04 → 2025-01-29 | 2025-01-30 → 2025-02-28 |
| 3 | 2022-03-06 → 2025-02-28 | 2025-03-01 → 2025-03-30 |
| … | (the runs in between advance by one test window each time, 20 runs in total) | |
| 20 | 2023-07-29 → 2026-07-23 | 2026-07-24 → 2026-08-22 |
The test windows are joined end to end, to the day: run 1 stops at 2025-01-29, run 2 picks up from 2025-01-30, with no gap and no overlap in between — the 20 stretches joined together are that out-of-sample series.
The training windows, by contrast, overlap heavily: a three-year window advances only 30 days per run, so the training periods of any two runs are very nearly the same data. Things that overlap cannot be stitched into one continuous line — which is why the chart carries only the out-of-sample one.
In implementation it makes two passes: the first computes the signal of every candidate parameter set exactly once over the whole history and prices each run's training slice out of it; the second recomputes only each run's winning set, strings the test slices into one series and prices the whole stretch in a single pass. That makes it far faster than "running n full scans" — the 20-run, 4.7-year hourly example below finished in 3.5 seconds in the 2026-09-17 run (the interface's confirmation dialog states the general case: "Usually takes about two minutes; results appear in this tab.").
Rolling means the whole training window slides forward each run; anchored means the start is fixed and the training window grows longer and longer. The product offers rolling only; anchored is deliberately not offered. Ask the agent for anchored and it replies with one fixed line, then runs rolling anyway:
There is no point trying to work around it either: the lib that runs walk-forward is read-only and the resident runtime refuses the edit outright; and the result file has no field to mark a run as anchored, so even if you did change it, the display would still read as rolling.
The tab has just two input boxes — training window and test window, both in days — with a live readout beside them showing how many runs that cuts. Leave both blank and this default table applies:
| Data length (days) | Training window (days) | Test window (how often to re-pick, days) |
|---|---|---|
| ≥ 1185 | 1095(three years) | 30 |
| 455–1184 | 365(one year) | 30 |
| < 455 | 4× the test window | total days ÷ 12, minimum 30 |
The three-year tier exists for a reason: it is closer to what users actually do, which is to pick one fixed parameter set out of the whole backtest. If a medium-length history were run straight through a ratio formula, the training window would come out shorter than a year, so the 365 / 30 tier was added in the middle.
Two hard limits stop it outright rather than hand you a half-baked result: fewer than 3 runs will not run and writes no result file ("Only enough data for {n} runs — 3 is the minimum to read anything out of sample"); more than 1000 runs is blocked the same way ("widen the test window"). Leftover data at the tail that does not make a full run is never padded out; a note below the chart says "last {d} days are shorter than one run — excluded".
The verdict card looks at a single number:
WFE = out-of-sample Sharpe ÷ average in-sample Sharpe
The numerator is the Sharpe of the whole stitched-together out-of-sample series; the denominator is the average of each winning cell's own Sharpe inside each run's training window. The four labels follow these boundaries (all of them "less than", so 0.5 lands on "just above the bar" and 0.7 on "above the bar"):
| WFE | Verdict card shows |
|---|---|
| Cannot be computed | OOS efficiency: cannot be judged |
| < 0.2 | OOS efficiency: far below the bar |
| < 0.5 | OOS efficiency: below the bar |
| < 0.7 | OOS efficiency: just above the bar |
| ≥ 0.7 | OOS efficiency: above the bar |
And please read the explanation beside that number literally:
A very small or negative denominator turns the whole diagnosis upside down: a strategy that was never strong in-sample to begin with ends up with a very high "efficiency" precisely because it had nothing to give back. Hence two guards:
The tutorial example strategy btc_sma_cross (BTCUSDT 1h, from 2022-01-01, 1,720 days in all, SMA 45 / 100, not a single parameter touched). The data falls in the first tier, so the default windows 1095 / 30 apply, cutting 20 runs (600 days of test windows in total); the last 25 days do not make a full run and are excluded.
| Item | Out-of-sample (test windows stitched, 600 days) | In-sample (each of the 20 training windows computed, then averaged) |
|---|---|---|
| Sharpe | −0.45 | 1.3471 |
| Annualized return | −16.55% | +50.16% |
| Max drawdown | −46.57% | — |
| Trades | 183 | — |
Out-of-sample efficiency WFE = −0.45 ÷ 1.3471 = −0.334 → far below the bar
Three things worth pointing out. First, not a single run was excluded — the training-window Sharpe of all 20 runs was above 0, so the denominator is the full 20-run average, not what was left after culling.
Second, the denominator of 1.3471 is far above 0.25, so the second guard never fired and the division is valid: "WFE is negative" and "WFE cannot be judged" are two different things — the first is a computed result, the second is a refusal to answer.
Third, the two columns cover different periods by construction — in-sample is the average of 20 mutually overlapping training windows, out-of-sample is the 600 days that follow them, so this is not a clean like-for-like comparison. That gap is precisely what WFE sets out to measure.
Same day, same code: this strategy's MCPT p-value is 0.0160 (n = 2000), and stdout prints significant edge at 95% itself — by the listing gate's standard, that is a pass.
| Test | The question it asks | Result |
|---|---|---|
| MCPT | On this fixed position series, can this result be told apart from luck? | p = 0.0160 → Pass |
| Walk-forward | Does picking parameters from history still work on the next stretch? | WFE = −0.334 → far below the bar |
Neither is miscalculated, because they are not answering the same question. This is also why no p-value, however beautiful, can be taken as having passed out-of-sample (see How to Read MCPT p-values). To be clear about one more thing: this is not saying the strategy is broken — it is the tutorial example that ships with the workspace, and what it demonstrates here is the difference in meaning between the two tests.
Each run picks its parameters on a 3×3 neighbourhood average (the same rule as the parameter scan, which is the plateau-not-peak approach described in How to Avoid Overfitting), but the training Sharpe reported in the run table is the winning cell's own number. This is deliberate:
| What the denominator uses | Consequence |
|---|---|
| That cell's own Sharpe (current behaviour) | Denominator runs high, so WFE runs conservative |
| 3×3 neighbourhood average | A neighbourhood average is structurally bound to be lower than the cell it surrounds → the denominator is pushed down → WFE is inflated, and the whole tab reads over-optimistic |
A diagnosis with an inflated denominator is worse than no diagnosis at all.
At the join between two runs, the series carries the new run's own position — it is cut open, not zeroed out. Forcing a flat at every seam would invent n−1 exits that nobody ever traded.
The cost of changing parameters is not lost either: the weight changes at the seam, and fees are charged for it at pricing time. And because every run's numbers are cut out of the same stitched series rather than priced again, the run table sums exactly to the whole-stretch total.
Signals are computed over the whole history (rolling indicators are only accurate that way), but each training slice skips its own leading warm-up bars — not just the first run. Without that step, run 1 would train on bars where the indicator has not warmed up yet and pick noise, and the effective lengths of the runs would differ and not be comparable. The run table above is the proof: the strategy's START says 2022-01-01, but run 1's training window only begins at 2022-01-05 — those few days are exactly the WARMUP = 100 hourly bars (about 4.2 days) being deducted.
The test window is not trimmed, because it is already warm and because it has to stay continuous in order to be stitched. While we are here, an advanced reader's frequent question: no embargo period is needed here — the test window sits immediately after the training window that picked its parameters, and none of the data after it is used at all.
When it finishes there is no "apply the suggested parameters" button, and the agent is not allowed to edit your strategy code on the strength of this result either. The empty state says so up front:
Why can they not be copied? Look at the parameter drift of that run. Across 20 runs it picked only 4 distinct parameter sets:
(45,100) ×14 → (55,80) → (45,100) → (45,80) ×3 → (35,80)
The first 14 runs all stayed on the same cell. Even if "it picks 45 / 100 most of the time" looks like a conclusion, that −0.45 out-of-sample curve is not the score of 45 / 100 — it is one continuous stretch run as a relay by 4 parameter sets, belonging to none of them, so there is no single set that could be "adopted". When you ask for them to be applied, the agent replies with one fixed line:
The parameter drift grid plots which cell each run picked and how many runs picked it, a dashed box marks your current parameters, and the legend spells out that "the last run's pick is for reference only" — the last run of that execution picked 35 / 80, which only means "that final run's training window happened to land on this cell". Also, this tab shows no p-value and never runs MCPT; one walk-forward run counts as one iteration in the agent's working rules.
Are there enough runs? Fewer than 3 will not run at all. When the run count is low, WFE is just the luck of one or two test windows.
Were any runs excluded? The per-run detail notes which runs had a training-window Sharpe ≤ 0. In the run above, not one was excluded; the more runs get excluded, the more it means this strategy often fails to find anything even inside its own training windows.
Is WFE "cannot be judged" or a negative number? Not the same thing. "Cannot be judged" means the in-sample average is below 0.25 and the ratio is meaningless; a negative number means the division was done and the out-of-sample stretch lost money.
How badly do the parameters drift? Look at the "{n} runs picked {u} distinct pairs" line. The run above was 20 runs, 4 sets; if every run jumps to a completely different cell, each re-pick is chasing something different.
Are you looking for suggested parameters? There are none. To change parameters, use the robust point from the parameter scan, not a copy of the last run.
Once you have run it, if WFE falls apart badly, the problem is usually not in the out-of-sample step but in the parameter-picking step — and that is the territory of How to Avoid Overfitting. If you do not yet have a strategy worth validating, every official strategy in the Strategy Library ships with a real backtest and its quality-gate results, so you can read the numbers before deciding: Strategy Library.