Skip to content
Level 5 · Real-Time AmiBroker UserProjectPart Capstone · page 7 of 960 min
60Minutes
22AFL functions
6Sources
StandardRequires
AFL functions taught here22

Component 6: The Trading Strategy

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 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
// 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. account
SetOption( "InitialEquity", StartingEquity );
SetOption( "MaxOpenPositions", PosQty );
SetOption( "AllowPositionShrinking", True );
SetOption( "MinPosValue", 1000 );
SetOption( "AccountMargin", 100 ); // 100 = fully funded, no margin
SetOption( "InterestRate", 0 );
SetOption( "CommissionMode", 1 ); // 1 = percent of trade value
SetOption( "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. universe
Turnover = Median( Close * Volume, LiquidityPeriod );
BaseDollars = StartingEquity / PosQty;
CapDollars = ( ParticipationPct / 100 ) * Turnover;
Tradeable = Turnover >= MinTurnover
AND Close >= MinPrice
AND Volume > 0
AND BaseDollars <= CapDollars;
// -------------------------------------------------------------- 5. regime
HaveBench = 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 rules
TrendMa = 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;

Download strategy.afl176 lines

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.

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.

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.

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.

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.

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.

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.

  1. 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.

  2. 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.

  3. Swap the sizing to ATR risk-based using the Part 34 lab, on identical signals.

  4. 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.

  5. 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.

The completed specification template, verbatim, plus:

  • The .afl file, 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

Question 1. Why must the stop distance be shifted by hand even though SetTradeDelays( 1, 1, 1, 1 ) is in force?
StopAtEntry = Nz( Ref( StopDistance, -1 ), 0 );
ApplyStop( stopTypeLoss, stopModePoint, StopAtEntry, 2 );
Show the answer and why

Answer: Because trade delays shift the Buy/Sell/Short/Cover arrays and nothing else — any other array the backtester consumes stays aligned with the undelayed signal

The documented mechanism shifts exactly four arrays. Without the manual shift, the stop distance read on the entry bar would be the ATR of the entry bar rather than of the signal bar that justified the trade.

Question 2. The specification and the formula disagree. Which do you change?
Show the answer and why

Answer: The specification first — it is the deliverable — and then the code to match it

A specification rewritten to describe whatever the code happens to do is documentation of an accident. The words state the intent; the code implements it; a disagreement means the intent was not achieved and must be restated deliberately before the code is touched.

Question 3. Why does the participation cap use StartingEquity rather than current equity?
BaseDollars = StartingEquity / PosQty;
CapDollars  = ( ParticipationPct / 100 ) * Turnover;
Show the answer and why

Answer: Because it does not compound, so the ceiling stays put as the account grows — under-stating capacity rather than over-stating it

A compounding ceiling would let the simulation take progressively larger positions in the same thin instruments — which is the exact error Part 30 documents. Choosing the conservative direction deliberately, and recording that you did, is the point.

Question 4. Which items belong in the "explicitly not modelled" list for this strategy? Select all that apply.
Show the answer and why

Answer: Borrow cost and availability, Queue position on any order, Taxes

Commission is charged on both legs by CommissionMode 1, so it is modelled. The other three are genuinely absent — and listing them converts them from silent errors into stated limitations, which is the only honest option available.

Question 5. A colleague's strategy uses a 17-bar lookback and a 6.5% threshold. What should you ask?
Show the answer and why

Answer: Where those numbers came from — oddly specific values usually indicate a grid-search winner reported as though it were the only specification tried

Nobody chooses 17 from theory. The question is not whether optimising is allowed — it is — but whether the reported number is the best of many tries, and whether that count is disclosed. That is what the research log exists to answer.

Sources for this lesson

6 verified · checked 2026-08-31

  1. 01AmiBroker User's Guide — Portfolio-level backtestingamibroker.com/guide/h_portfolio.html2026-08-31
  2. 02AFL Function Reference — ApplyStop§ ExitAtStopamibroker.com/guide/afl/applystop.html2026-08-31
  3. 03AFL Function Reference — SetOptionamibroker.com/guide/afl/setoption.html2026-08-31
  4. 04AFL Function Reference — SetPositionSizeamibroker.com/guide/afl/setpositionsize.html2026-08-31
  5. 05AFL Function Reference — SetTradeDelaysamibroker.com/guide/afl/settradedelays.html2026-08-31
  6. 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.