Component 6: The Trading Strategy
The specification template
Section titled “The specification template”Fill this in before you write any AFL and before you run any test. Every field is required, and “I’ll decide later” in any of them means the later decision will be made by whatever the results look like.
| Field | What goes in it |
|---|---|
| Hypothesis | One sentence, in plain language, stating what you believe and why it might be true. Written as a claim to be tested. |
| Universe | Which instruments, and how the list was built — including whether it is point-in-time. |
| Regime | The market-wide condition under which the strategy operates, read from a benchmark. |
| Entry | The condition, and the bar it is decided on. |
| Exit | The condition, and the bar it is decided on. |
| Stop | Where, in what units, and what fill assumption. |
| Sizing | The rule, and what it makes equal — money, risk, or share count. |
| Constraints | Slots, margin, participation cap, minimum position, shorting. |
| Costs | Commission, slippage, and where each one enters the simulation. |
| Explicitly not modelled | The list of things you know are absent. |
| What would refute it | The result that would make you abandon the idea. |
The last row is the one people leave out, and it is the one Component 9 asks about.
The worked example
Section titled “The worked example”The formula below implements a complete specification. Read the header block first, in full, before the code. If you are designing your own hypothesis — which is the point of the capstone — use this as the shape rather than the content.
Complete runnable AFL
// strategy.afl// Capstone Component 6 - The Trading Strategy//// ========================= THE SPECIFICATION ==========================// Everything below the line is the implementation. Everything above it is the// strategy, and it must be readable by somebody who does not read AFL. If the// two ever disagree, the specification is wrong, not the code - fix the words.//// HYPOTHESIS// Among liquid instruments in a market that is itself in an uptrend, an// instrument that has been trending up and then makes a new multi-week high// continues in that direction often enough, and far enough, to pay for the// trades that do not.//// This is a claim to be TESTED, not a belief. Components 7 and 8 test it, and// a negative result is a legitimate outcome of this capstone.//// UNIVERSE// The watch list the Analysis window points at, filtered on the decision bar// by median turnover over LiquidityPeriod bars and a minimum close.// KNOWN DEFECT: if the list is today's index membership, it excludes// everything that was delisted, and this test cannot repair that. Record it.//// REGIME GATE// Long entries are permitted only while the BENCHMARK is above its own// RegimePeriod-bar average. A market-wide gate, applied identically to every// candidate on the same date. It removes signals; it never creates them.//// ENTRY// Close crosses above the highest high of the previous BreakoutPeriod bars,// while the symbol's close is above its own TrendPeriod-bar average.// Decided on the bar's close. Filled at the NEXT bar's open.//// EXIT// Close crosses below the lowest low of the previous ExitPeriod bars, or the// stop below, whichever comes first.//// STOP// Maximum loss at StopAtrMult ATRs below the entry, sampled on the signal bar// and held for the trade. ExitAtStop = 2: the bar's High-Low range is checked// but the exit happens on the NEXT bar at the regular trade price, so a gap// through the level is paid at the gapped price.//// SIZING// Equal weight across PosQty slots, from portfolio equity, whole shares,// sized from the PREVIOUS bar's closing equity.//// CONSTRAINTS// No margin. No shorting. One position per symbol. A position must not exceed// ParticipationPct of the symbol's own median turnover.//// COSTS// CommissionPct of trade value on each leg, plus SlippagePct applied to every// fill price in the direction that hurts.//// EXPLICITLY NOT MODELLED// Borrow cost and availability. Dividends. Interest on idle cash (set to// zero). Taxes. Currency. Market impact beyond the participation cap. Queue// position. Corporate actions your data vendor did not adjust for.//// NOTHING THIS FORMULA PRODUCES IS A FORECAST. It is a simulation of a rule set// over one sample of the past under the assumptions listed above.// ======================================================================
// ---------------------------------------------------------- 1. parameters// Param(), not Optimize(). Choosing these honestly is Component 8's job, and// copying a grid-search winner into the defaults is the fault the Part 30// challenge exists to teach.BenchSymbol = ParamStr( "Benchmark symbol", "" );PosQty = Param( "Max open positions", 10, 1, 40, 1 );StartingEquity = Param( "Starting equity", 100000, 10000, 10000000, 10000 );
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 );ExitPeriod = Param( "Exit look-back (bars)", 25, 5, 150, 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 );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 );
// ------------------------------------------------------------ 2. accountSetOption( "InitialEquity", StartingEquity );SetOption( "MaxOpenPositions", PosQty );SetOption( "AllowPositionShrinking", True );SetOption( "MinPosValue", 1000 );SetOption( "AccountMargin", 100 ); // 100 = fully funded, no marginSetOption( "InterestRate", 0 );SetOption( "CommissionMode", 1 ); // 1 = percent of trade valueSetOption( "CommissionAmount", CommissionPct );SetOption( "AllowSameBarExit", False );SetOption( "UsePrevBarEquityForPosSizing", True );
RoundLotSize = 1;SetBacktestMode( backtestRegular );SetPositionSize( 100 / PosQty, spsPercentOfEquity );SetTradeDelays( 1, 1, 1, 1 );
// -------------------------------------------------------------- 3. fills// Slippage is a worse price, not a fee, so it belongs here rather than in the// commission. AmiBroker clamps any assigned price back inside the bar's// High-Low range, so on a bar that opened at its high the extra half-spread is// silently returned to us. Component 7 asks you to measure how often.BuyPrice = Open * ( 1 + SlippagePct / 100 );SellPrice = Open * ( 1 - SlippagePct / 100 );ShortPrice = Open * ( 1 - SlippagePct / 100 );CoverPrice = Open * ( 1 + SlippagePct / 100 );
// ------------------------------------------------------------ 4. universeTurnover = Median( Close * Volume, LiquidityPeriod );
BaseDollars = StartingEquity / PosQty;CapDollars = ( ParticipationPct / 100 ) * Turnover;
Tradeable = Turnover >= MinTurnover AND Close >= MinPrice AND Volume > 0 AND BaseDollars <= CapDollars;
// -------------------------------------------------------------- 5. regimeHaveBench = StrLen( BenchSymbol ) > 0;
if( HaveBench ){ BenchClose = Foreign( BenchSymbol, "C" ); RegimeOpen = NOT IsNull( BenchClose ) AND BenchClose > 0 AND BenchClose > MA( BenchClose, RegimePeriod );}else{ // The gate is open, and that is a DIFFERENT strategy from the specified // one. Say so in the report rather than quietly reporting it as this one. RegimeOpen = True;}
// --------------------------------------------------------- 6. the 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;
Buy = IIf( Ready, RegimeOpen AND Tradeable AND Close > TrendMa AND Cross( Close, BreakoutLevel ), False );
Sell = Cross( ExitLevel, Close );
Short = False;Cover = False;
// ---------------------------------------------------------------- 7. stop// SetTradeDelays shifts Buy/Sell/Short/Cover and nothing else, so an array// used by the backtester must be shifted by hand to make the value read on the// entry bar the one computed on the signal bar.StopDistance = StopAtrMult * ATR( AtrPeriod );StopAtEntry = Nz( Ref( StopDistance, -1 ), 0 );
ApplyStop( stopTypeLoss, stopModePoint, StopAtEntry, 2, False, 0, 0, -1 );
// ---------------------------------------------------------------- 8. rank// When candidates outnumber slots, prefer the more liquid. This is a CHOICE// with a known side effect - it systematically selects the largest names in// the universe - and it must be stated whenever a result is quoted.PositionScore = Turnover;How it works
Section titled “How it works”The hypothesis is a claim, not a belief
Section titled “The hypothesis is a claim, not a belief”Among liquid instruments in a market that is itself in an uptrend, an instrument that has been trending up and then makes a new multi-week high continues in that direction often enough, and far enough, to pay for the trades that do not.
Three things make that a usable hypothesis rather than a slogan.
It names the population (liquid instruments, in an uptrending market), the event (a new multi-week high after an uptrend) and the measurable consequence (enough to pay for the losers).
It is stated in a form that could be false. “Momentum works” cannot be false; this can.
And the header says so out loud: this is a claim to be TESTED, not a belief, and a negative result is a legitimate outcome of this capstone.
Every decision uses information that existed
Section titled “Every decision uses information that existed”Fragment — not a complete formula
BreakoutLevel = Ref( HHV( High, BreakoutPeriod ), -1 );ExitLevel = Ref( LLV( Low, ExitPeriod ), -1 );Turnover = Median( Close * Volume, LiquidityPeriod );The entry and exit levels are shifted so no bar is judged against a window containing itself. The liquidity filter is measured on the decision bar, not the fill bar. The regime is read from the benchmark’s close, which is known when the decision is made.
Then SetTradeDelays( 1, 1, 1, 1 ) moves every signal one bar forward, and the price arrays supply
the next bar’s open.
The stop’s fill assumption is stated and conservative
Section titled “The stop’s fill assumption is stated and conservative”Fragment — not a complete formula
StopDistance = StopAtrMult * ATR( AtrPeriod );StopAtEntry = Nz( Ref( StopDistance, -1 ), 0 );
ApplyStop( stopTypeLoss, stopModePoint, StopAtEntry, 2, False, 0, 0, -1 );Two things worth reading twice.
The manual shift. SetTradeDelays shifts Buy/Sell/Short/Cover and nothing else, so
an array the backtester consumes must be shifted by hand for the value read on the entry bar to be
the one computed on the signal bar.
ExitAtStop = 2. The bar’s High–Low range is checked, but the exit happens on the next bar at
the regular trade price. The backtest is therefore not awarded a fill at the level it nominated, and
a gap through the stop costs what a gap costs. Value 1 would make every result better and every
drawdown fictional.
All eight arguments are written out, including the four that would have defaulted, so nothing about the stop’s behaviour is implicit.
The participation cap
Section titled “The participation cap”Fragment — not a complete formula
BaseDollars = StartingEquity / PosQty;CapDollars = ( ParticipationPct / 100 ) * Turnover;
Tradeable = Turnover >= MinTurnover AND ... AND BaseDollars <= CapDollars;BaseDollars uses the starting equity, so the cap does not compound. As the account grows the
ceiling stays put, which under-states capacity rather than over-stating it — the conservative
direction, and a deliberate choice you should record.
Param(), not Optimize()
Section titled “Param(), not Optimize()”The parameters are Param() calls with defaults chosen from reasoning, not from a grid search. That
is not a stylistic preference: copying a grid-search winner into the defaults and then reporting the
backtest as a result is precisely the fault
Part 30’s impossible-backtest challenge
exists to teach.
Component 8 has a separate file with four parameters exposed to the optimiser, and its purpose is to show you the shape of the surface — not to choose the numbers you report.
The missing-benchmark case is a different strategy
Section titled “The missing-benchmark case is a different strategy”Fragment — not a complete formula
else{ // The gate is open, and that is a DIFFERENT strategy from the specified // one. Say so in the report rather than quietly reporting it as this one. RegimeOpen = True;}Running without a benchmark is fine. Running without one and reporting the result as though the regime gate had been active is not.
The assumptions block
Section titled “The assumptions block”Read the “EXPLICITLY NOT MODELLED” list in the formula header. Borrow cost and availability. Dividends. Interest on idle cash. Taxes. Currency. Market impact beyond the participation cap. Queue position. Corporate actions your vendor did not adjust for.
Expected result
Section titled “Expected result”Validation
Section titled “Validation”Diff the specification against the code, line by line. Every field in the template should be findable in the file. Anything in the file that is not in the specification is an undocumented decision.
Confirm the delay on a real trade. Trade list, pick a trade, find the signal bar on the chart, and confirm the entry is the following bar at that bar’s open plus slippage.
Confirm the stop shift. Pick a stopped-out trade and check that the stop distance corresponds to the ATR on the signal bar, not the entry bar. If they are noticeably different on that trade, the manual shift is doing its job.
Confirm the regime gate binds. Run once with the benchmark set, once with it blank. The difference is what the gate removed. If there is no difference over a range containing a decline, the gate is not connected.
Confirm the participation cap binds on something. Add a temporary exploration column for
BaseDollars <= CapDollars and count how many symbol-bars fail it. If none do, the cap is inert for
your universe at your account size — which is worth knowing and worth stating, because it means the
constraint is untested.
Common errors
Section titled “Common errors”The specification was written after the code. The symptom is a specification with no “what would refute it” row, and vague language in the universe row. There is no fix except to redo it.
Parameters copied from an optimisation. The tell is oddly specific numbers — a 17-bar lookback, a 6.5% threshold. If you cannot say why the number is what it is, it came from a search.
The stop uses ExitAtStop = 1. Every result improves and the drawdown figure becomes fiction. See
the stops lesson.
Slippage in the commission. Slippage is a worse price, not a fee. Putting it in
CommissionAmount gets the total roughly right and the per-trade arithmetic wrong — including where
a stop sits relative to the entry.
One position at a time. SetPositionSize at 100% of equity, or MaxOpenPositions at 1. Check
both; they have to agree.
Results change between runs with no edits. Something is being read from a dialog rather than the
file, or the date range is “n last quotations” and new data arrived. Save the configuration as an
.APX and re-run from that.
Extensions
Section titled “Extensions”Each of these is a genuine experiment, not a variation. Record all of them in the research log, because each one is a specification you evaluated.
-
Turn the regime gate off and re-run. How much of the result came from the gate rather than the entry? This is the cheapest and most informative single experiment available.
-
Replace the liquidity rank with two alternatives — lowest volatility, and closest to the trend average — and compare the trade lists. Same rules, three selection policies. If the results barely differ, your rank is not doing anything; if they differ a lot, your result depends on a choice you may not have thought hard about.
-
Swap the sizing to ATR risk-based using the Part 34 lab, on identical signals.
-
Add a short side and find out what breaks. Borrow, availability and cost are all unmodelled; the point of the exercise is to discover how much the result depends on assumptions you cannot support.
-
Write the specification for somebody else’s published strategy using this template. Most published strategies cannot be filled in completely, and finding out which rows are missing is a fast education in what to look for.
What to record for the report
Section titled “What to record for the report”The completed specification template, verbatim, plus:
- The
.aflfile, with its header intact. - The “explicitly not modelled” list.
- The refutation criterion, written before the first run and dated.
- Every variant you tried, from the research log, with a count.
Check your understanding
Sources for this lesson
6 verified · checked 2026-08-31
- 01AmiBroker User's Guide — Portfolio-level backtestingamibroker.com/guide/h_portfolio.html2026-08-31
- 02AFL Function Reference — ApplyStop§ ExitAtStopamibroker.com/guide/afl/applystop.html2026-08-31
- 03AFL Function Reference — SetOptionamibroker.com/guide/afl/setoption.html2026-08-31
- 04AFL Function Reference — SetPositionSizeamibroker.com/guide/afl/setpositionsize.html2026-08-31
- 05AFL Function Reference — SetTradeDelaysamibroker.com/guide/afl/settradedelays.html2026-08-31
- 06AFL Function Reference — Optimizeamibroker.com/guide/afl/optimize.html2026-08-31
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.