Components 7 and 8: Backtest and Robustness Evaluation
The order these must be done in
Section titled “The order these must be done in”Validation before results, every time
- 1. Run the portfolio backtestOver the stated universe and range, with the report's trade list turned on. Do not read the summary yet.
- 2. Hand-check three tradesA winner, a loser, and one that exited by stop. Reconstruct each bar by bar against the chart.
- 3. Run the execution auditWas every trade physically possible? Same-bar leak, participation, gaps through the stop.
- 4. NOW read the summaryExposure first, then Risk Adjusted Return, then drawdown, then the return figure.
- 5. Parameter sensitivityThe shape of the surface, not the best cell.
- 6. Out-of-sampleOn data you have genuinely not looked at. Once.
- 7. Walk-forwardTesting the process rather than a parameter set.
- 8. Assemble the evidenceEvery number attached to its assumptions.
Steps 2 and 3 come before step 4 for a reason that is easy to state and hard to obey: once you have seen a good number, you will not look for the reason it is wrong.
Component 7: the portfolio backtest
Section titled “Component 7: the portfolio backtest”Procedure
Section titled “Procedure”- Settings check. Periodicity daily. Initial equity matching the formula. Margin 100. Interest
rate 0. Max open positions matching
PosQty. Delays 1 on all four. “Include trade list in the report” on. - Save the configuration as an
.APX. The Analysis window’s Apply to and Range live in a dialog and appear nowhere in the formula. They are the two settings most likely to differ from what you believe, and an.APXis self-contained. - Run it. Once, at the specification’s parameters.
- Do not read the summary. Go to step 2 of the list above.
Hand-checking three trades
Section titled “Hand-checking three trades”Pick a winner, a loser, and one that exited by stop — different faults hide in each. For every one, verify in order:
| Check | What must be true |
|---|---|
| Signal bar | The entry condition was true on that bar, including the regime and liquidity gates |
| Entry bar | The next bar. If it is the same bar, your delays are not what you think |
| Entry price | That bar’s open × (1 + slippage), unless the open was the bar’s high — in which case exactly the high, because of price-bound clamping |
| Share count | Position value ÷ entry price, whole shares, value = one PosQty-th of that bar’s equity |
| Exit | Which fired — the rule or the stop? If the stop, a prior bar’s range went below the level and the exit is on the following bar |
| Commission | CommissionPct of each leg’s value |
If all six agree on all three trades, the machinery is doing what you believe. If any one disagrees, the same discrepancy is in every other trade too.
The execution audit
Section titled “The execution audit”Complete runnable AFL
// strategy-audit.afl// Capstone Component 7 - the evidence that the backtest was physically possible//// GOAL// A backtest report says what the engine decided. This exploration says// whether those decisions could have been executed. Run it over the same// universe and range as the capstone backtest, and attach the output to the// research report.//// It answers five questions, one column group each:// 1. Was the entry decided on a bar and filled on a LATER one?// 2. How much would a same-bar fill have handed the system for free?// 3. How large is the intended position against what actually traded?// 4. How often would the stop have been gapped through?// 5. What does the round trip cost, as a percentage?//// HOW TO RUN// Analysis -> Apply to: the capstone universe. Periodicity: Daily.// Range: the capstone range. Press EXPLORE.// Sort by "Participation % of bar volume" descending and read the top rows.//// ASSUMPTIONS// Account size stated, not read from a live equity curve. A compounding// backtest will want LARGER positions than these as equity// grows, so treat these figures as the optimistic case.// Ref( ..., 1 ) reads the bar after the signal. Legitimate in an AUDIT,// which looks backwards at what followed. The identical// expression inside a Buy rule is a bug.// Scope this checks execution realism. It says nothing about// whether the rules have an edge.
SetBarsRequired( sbrAll, sbrAll );
BenchSymbol = ParamStr( "Benchmark symbol", "" );AccountSize = Param( "Assumed account size", 100000, 10000, 10000000, 10000 );PosQty = Param( "Max open positions", 10, 1, 40, 1 );RegimePeriod = Param( "Benchmark regime average", 200, 20, 400, 10 );TrendPeriod = Param( "Symbol trend average", 200, 20, 400, 10 );BreakoutPeriod = Param( "Entry look-back (bars)", 50, 5, 250, 5 );StopAtrMult = Param( "Stop distance (x ATR)", 3, 0.5, 10, 0.5 );AtrPeriod = Param( "ATR period", 20, 2, 100, 1 );LiquidityPeriod = Param( "Liquidity look-back (bars)", 50, 5, 400, 5 );CommissionPct = Param( "Commission per trade (%)", 0.10, 0, 1.00, 0.01 );SlippagePct = Param( "Slippage per fill (%)", 0.15, 0, 2.00, 0.01 );
// ------------------------------------------------ the rule under audit// Identical to strategy.afl's entry. If you change the strategy, change this.Turnover = Median( Close * Volume, LiquidityPeriod );TrendMa = MA( Close, TrendPeriod );BreakoutLevel = Ref( HHV( High, BreakoutPeriod ), -1 );
HaveBench = StrLen( BenchSymbol ) > 0;
if( HaveBench ){ BenchClose = Foreign( BenchSymbol, "C" ); RegimeOpen = NOT IsNull( BenchClose ) AND BenchClose > 0 AND BenchClose > MA( BenchClose, RegimePeriod );}else{ RegimeOpen = True;}
EntrySignal = RegimeOpen AND Close > TrendMa AND Cross( Close, BreakoutLevel ) AND Volume > 0;
// ------------------------------------------ 1 and 2. the fill assumptionFillSameBarOpen = Open;FillSameBarClose = Close;FillNextBarOpen = Ref( Open, 1 );
// The gap between deciding on the close and being filled at that same bar's// open. Positive means the leak paid the system before the trade began.SameBarLeakPct = 100 * SafeDivide( Close - Open, Open, 0 );
// What the honest assumption costs relative to the leaky one, on this bar.OvernightGapPct = 100 * SafeDivide( FillNextBarOpen - Close, Close, Null );
// ------------------------------------------------ 3. size and absorptionIntendedValue = AccountSize / PosQty;FillPrice = FillNextBarOpen * ( 1 + SlippagePct / 100 );IntendedShares = SafeDivide( IntendedValue, FillPrice, Null );
EntryBarVolume = Ref( Volume, 1 );ParticipationBar = 100 * SafeDivide( IntendedShares, EntryBarVolume, Null );ParticipationMed = 100 * SafeDivide( IntendedValue, Turnover, Null );
// ---------------------------------------------------- 4. the stop realismStopDistance = StopAtrMult * ATR( AtrPeriod );StopLevel = FillPrice - StopDistance;
// Did the bar AFTER the fill open below where the stop would have sat? If so, a// backtest that exits "at the stop level" is claiming a price that was never// available on that bar.NextNextOpen = Ref( Open, 2 );GapThroughStop = NOT IsNull( NextNextOpen ) AND NextNextOpen < StopLevel;
// ------------------------------------------------------------- 5. costsRoundTripCostPct = 2 * CommissionPct + 2 * SlippagePct;
// A cost that is large relative to the stop distance means the system is paying// a meaningful fraction of its own risk budget just to open and close.StopPct = 100 * SafeDivide( StopDistance, FillPrice, Null );CostVsRiskPct = 100 * SafeDivide( RoundTripCostPct, StopPct, Null );
// -------------------------------------------------------------- outputFilter = EntrySignal AND Status( "barinrange" ) AND NOT IsNull( FillNextBarOpen );
AddColumn( DateTime(), "Signal bar", formatDateTimeISO );AddColumn( Close, "Decision close", 1.3 );
AddColumn( FillSameBarOpen, "Fill: same-bar open", 1.3 );AddColumn( FillSameBarClose, "Fill: same-bar close", 1.3 );AddColumn( FillNextBarOpen, "Fill: next-bar open", 1.3 );AddColumn( SameBarLeakPct, "Same-bar leak %", 1.2 );AddColumn( OvernightGapPct, "Overnight gap %", 1.2 );
AddColumn( IntendedValue, "Intended position", 1.0 );AddColumn( IntendedShares, "Intended shares", 1.0 );AddColumn( EntryBarVolume, "Entry bar volume", 1.0 );AddColumn( ParticipationBar, "Participation % of bar volume", 1.2 );AddColumn( ParticipationMed, "Participation % of median turnover", 1.2 );
AddColumn( StopLevel, "Stop level", 1.3 );AddColumn( StopPct, "Stop distance %", 1.2 );AddColumn( GapThroughStop, "Gapped through stop", 1.0 );
AddColumn( RoundTripCostPct, "Round-trip cost %", 1.2 );AddColumn( CostVsRiskPct, "Cost as % of risk budget", 1.1 );
// COUNT and AVERAGE. The average same-bar leak and the average participation// are the two numbers to quote in the report.AddSummaryRows( 2 | 8 | 16, 1.2 );Run as an Exploration over the same universe and range. It prints one row per entry signal with the numbers that decide whether the backtest was physically possible.
Component 8: robustness
Section titled “Component 8: robustness”Parameter sensitivity
Section titled “Parameter sensitivity”Complete runnable AFL
// strategy-sensitivity.afl// Capstone Component 8 - parameter sensitivity//// GOAL// The same strategy as strategy.afl, with the four parameters most likely to// matter exposed to the optimiser. Its purpose is NOT to find the best cell.// It is to see the SHAPE of the surface, which is the only thing an// optimisation can honestly tell you on its own.//// Broad plateau neighbouring parameter pairs behave alike. Encouraging: the// result looks like a property of the idea rather than of two// numbers.// Single spike one cell is good and its neighbours are not. That is what// curve fitting looks like from outside.// Flat and dull everything behaves alike and much like the benchmark. Also// a result, and a common one.//// WHAT YOU MUST NOT DO WITH THIS FILE// Report the best cell as your capstone result. You have just run hundreds of// tests against one dataset, and the best of hundreds looks good partly// because it is the best of hundreds. The number you report is the one from// the specification you wrote down in advance; this file tells you how much// confidence that number deserves.//// HOW TO RUN// 1. Analysis -> Optimize, over the SAME universe, range and costs as// strategy.afl. Record the whole result table, not the top row.// 2. Sort by CAR/MaxDD and look at where the good cells SIT relative to each// other, not at how good the best one is.// 3. Then run it once as an ordinary Backtest at the specification defaults,// which is your actual result.//// ASSUMPTIONS// Identical to strategy.afl in every respect except that four Param() calls// have become Optimize() calls. If you change anything else, the sensitivity// study is measuring the change instead of the parameters.
BenchSymbol = ParamStr( "Benchmark symbol", "" );PosQty = Param( "Max open positions", 10, 1, 40, 1 );StartingEquity = Param( "Starting equity", 100000, 10000, 10000000, 10000 );
// ------------------------------------------------- the four under test// Ranges are wide enough to show the surface and coarse enough to finish. A// finer grid does not make the study more informative; it makes it slower and// increases the number of specifications you have to admit to having tried.BreakoutPeriod = Optimize( "Entry look-back", 50, 20, 100, 10 );ExitPeriod = Optimize( "Exit look-back", 25, 10, 60, 5 );StopAtrMult = Optimize( "Stop (x ATR)", 3, 1.5, 6, 0.5 );TrendPeriod = Optimize( "Symbol trend average", 200, 100, 300, 50 );
RegimePeriod = Param( "Benchmark regime average", 200, 20, 400, 10 );AtrPeriod = Param( "ATR period", 20, 2, 100, 1 );LiquidityPeriod = Param( "Liquidity look-back (bars)", 50, 5, 400, 5 );MinTurnover = Param( "Minimum median turnover", 2000000, 0, 100000000, 250000 );MinPrice = Param( "Minimum close", 2, 0, 500, 0.5 );ParticipationPct = Param( "Max % of median turnover", 1, 0.05, 25, 0.05 );CommissionPct = Param( "Commission per trade (%)", 0.10, 0, 1.00, 0.01 );SlippagePct = Param( "Slippage per fill (%)", 0.15, 0, 2.00, 0.01 );
// ------------------------------------------------------------ accountSetOption( "InitialEquity", StartingEquity );SetOption( "MaxOpenPositions", PosQty );SetOption( "AllowPositionShrinking", True );SetOption( "MinPosValue", 1000 );SetOption( "AccountMargin", 100 );SetOption( "InterestRate", 0 );SetOption( "CommissionMode", 1 );SetOption( "CommissionAmount", CommissionPct );SetOption( "AllowSameBarExit", False );SetOption( "UsePrevBarEquityForPosSizing", True );
RoundLotSize = 1;SetBacktestMode( backtestRegular );SetPositionSize( 100 / PosQty, spsPercentOfEquity );SetTradeDelays( 1, 1, 1, 1 );
BuyPrice = Open * ( 1 + SlippagePct / 100 );SellPrice = Open * ( 1 - SlippagePct / 100 );ShortPrice = Open * ( 1 - SlippagePct / 100 );CoverPrice = Open * ( 1 + SlippagePct / 100 );
// ------------------------------------------------------------ universeTurnover = Median( Close * Volume, LiquidityPeriod );BaseDollars = StartingEquity / PosQty;CapDollars = ( ParticipationPct / 100 ) * Turnover;
Tradeable = Turnover >= MinTurnover AND Close >= MinPrice AND Volume > 0 AND BaseDollars <= CapDollars;
// -------------------------------------------------------------- regimeHaveBench = StrLen( BenchSymbol ) > 0;
if( HaveBench ){ BenchClose = Foreign( BenchSymbol, "C" ); RegimeOpen = NOT IsNull( BenchClose ) AND BenchClose > 0 AND BenchClose > MA( BenchClose, RegimePeriod );}else{ RegimeOpen = True;}
// --------------------------------------------------------------- rulesTrendMa = MA( Close, TrendPeriod );BreakoutLevel = Ref( HHV( High, BreakoutPeriod ), -1 );ExitLevel = Ref( LLV( Low, ExitPeriod ), -1 );
RequiredBars = Max( Max( TrendPeriod, RegimePeriod ), Max( BreakoutPeriod, LiquidityPeriod ) ) + 1;Ready = BarIndex() >= RequiredBars;
// A combination whose exit window is longer than its entry window is a// different kind of system, not a variant of this one. Silencing it keeps// meaningless rows out of the table rather than leaving you to spot them.if( ExitPeriod >= BreakoutPeriod ){ Buy = 0; Sell = 0;}else{ Buy = IIf( Ready, RegimeOpen AND Tradeable AND Close > TrendMa AND Cross( Close, BreakoutLevel ), False );
Sell = Cross( ExitLevel, Close );}
Short = False;Cover = False;
StopDistance = StopAtrMult * ATR( AtrPeriod );StopAtEntry = Nz( Ref( StopDistance, -1 ), 0 );ApplyStop( stopTypeLoss, stopModePoint, StopAtEntry, 2, False, 0, 0, -1 );
PositionScore = Turnover;Identical to the strategy in every respect except that four Param() calls have become Optimize()
calls. If you change anything else, the study measures the change instead of the parameters.
Run it as an Optimization over the same universe, range and costs. Then look at the shape:
| Shape | What it means |
|---|---|
| Broad plateau | Neighbouring parameter pairs behave alike. Encouraging — the result looks like a property of the idea rather than of two numbers. |
| Single spike | One cell good, its neighbours not. This is what curve fitting looks like from outside. |
| Flat and dull | Everything behaves alike, and much like the benchmark. Also a result, and a common one. |
Out-of-sample
Section titled “Out-of-sample”The rule is simple and the discipline is not: hold back a period you have genuinely not looked at, test on it once, and report what happened.
Practically:
- Reserve the most recent 20–30% of your range, or an early period if your data allows.
- Do all of Component 7 and the sensitivity study on the remaining part.
- Then run the specification’s parameters on the held-out part. Once.
Walk-forward
Section titled “Walk-forward”Out-of-sample tests a parameter set. Walk-forward tests the process: optimise on a window, apply the winner to the next window, roll forward, repeat, and evaluate only the concatenated out-of-sample segments.
The workflow, the settings and the interpretation are Part 32’s subject — see the AmiBroker walk-forward workflow and interpreting degradation. For the capstone you need three things from it:
- The out-of-sample equity curve, concatenated across steps.
- The degradation — how much worse out-of-sample is than in-sample, expressed as a ratio you can quote.
- Whether the selected parameters were stable across steps. Parameters that jump to a different corner of the grid at every step are telling you the optimiser is fitting noise, and that is often more informative than the performance figures.
Monte Carlo, where appropriate
Section titled “Monte Carlo, where appropriate”AmiBroker’s Monte Carlo facility resamples your trade sequence to produce a distribution of outcomes rather than one path. Part 33 covers it and its limits.
Assembling the evidence
Section titled “Assembling the evidence”Every number goes into the report with six clauses attached: what rules, over what universe, over what period, at what costs, with what fill assumption, from what data source. A figure without those six is not evidence.
| Evidence | From | What it establishes |
|---|---|---|
| Backtest summary | Component 7 | What the simulation produced |
| Trade list, three hand-checked | Component 7 | That the engine did what you believe |
| Execution audit table | Component 7 | That the trades were physically possible |
| Parameter surface | Component 8 | Whether the result is a property or a coincidence |
| Out-of-sample result | Component 8 | Whether it survives data you did not fit to |
| Walk-forward summary and degradation | Component 8 | Whether the process survives |
| Parameter stability across steps | Component 8 | Whether the optimiser is fitting noise |
| Specification count from the log | Research log | How many tries produced the reported number |
Validation checklist
Section titled “Validation checklist”Before you write Component 9, confirm every one of these.
- The
.APXreproduces the backtest exactly. - Three trades hand-checked, all six checks each.
- The execution audit was run over the same universe and range.
- Exposure was read before any return figure.
- The parameter sensitivity study changed only the four parameters.
- The out-of-sample data was looked at once, and the log records the date.
- The walk-forward result reports out-of-sample segments only, not the in-sample ones.
- Every quoted number has its six clauses.
- The research log has a count of specifications evaluated.
- You can state what result would have refuted the hypothesis, and whether it occurred.
Common errors
Section titled “Common errors”Reading the summary before validating. The most consequential error in the whole capstone, and the hardest to undo, because you cannot un-see a good number.
Comparing return figures across different exposures. Read Exposure % first; use Risk Adjusted Return % when exposures differ.
Reporting the optimisation’s best cell. See the warning above.
Changing the model after seeing the out-of-sample result. The hold-out is spent.
Walk-forward reported including in-sample segments. The whole point is the out-of-sample concatenation. Check what the report is summarising.
A Monte Carlo confidence interval quoted without its assumptions. Independence and regime coverage are both violated. State them or omit the figure.
The audit run over a different range than the backtest. Then it is auditing a different set of
signals. Use the same .APX-recorded range.
Sensitivity study with something else changed. A different universe, a different cost, a different slot count. The surface then shows the effect of that change, not of the parameters.
What to record for the report
Section titled “What to record for the report”Everything in the evidence table, plus:
- The date the out-of-sample data was first examined.
- The number of specifications evaluated, from the log.
- Your prediction of the parameter surface’s shape, dated before you ran it.
Check your understanding
Sources for this lesson
7 verified · checked 2026-09-01
- 01AmiBroker User's Guide — Portfolio-level backtestingamibroker.com/guide/h_portfolio.html2026-08-31
- 02AmiBroker User's Guide — Walk-forward testingamibroker.com/guide/h_walkforward.html2026-08-31
- 03AmiBroker User's Guide — Optimizationamibroker.com/guide/h_optimization.html2026-08-31
- 04AmiBroker User's Guide — Monte Carlo simulationamibroker.com/guide/h_montecarlo.html2026-08-31
- 05AmiBroker User's Guide — Backtest reportamibroker.com/guide/w_report.html2026-08-31
- 06AFL Function Reference — Optimizeamibroker.com/guide/afl/optimize.html2026-08-31
- 07AmiBroker User's Guide — Settings windowamibroker.com/guide/w_settings.html2026-09-01
Every technical claim on this page was checked against the official AmiBroker documentation on the date shown. Where the course disagrees with folklore, the source is how you can tell which one to trust.