Execution Assumptions You Must State
A backtest is a set of claims about trades that never happened. Every one of those trades was assigned a price, a size and a moment, by rules you either chose or inherited. Someone reading your result cannot evaluate it without knowing what those rules were — and neither, six months later, can you.
This lesson produces one artefact: an assumptions block that goes at the top of every system formula you write from here on. Filling it in takes ten minutes the first time and about two minutes thereafter, and it converts a number that means nothing into a number that means something specific and arguable.
The stack between a signal and a trade
Section titled “The stack between a signal and a trade”Between the signal and the money
- SignalA 1 in the Buy array on some bar
- TimingWhich bar the order reaches the marketSetTradeDelays
- Fill priceThe price the simulation assignsBuyPrice / SellPrice
- SlippageThe difference between that price and reality
- CommissionWhat the broker chargesSetOption
- Size limitsHow much the market could absorb
- Realised tradeWhat actually reaches the equity curve
The three shaded layers are the ones AmiBroker will not fill in for you. They are also the ones that decide whether a short-horizon result survives contact with a broker.
Fill price assumptions
Section titled “Fill price assumptions”There are four fill models worth knowing, and each is a different claim about what you could have done.
Next bar’s open. The default of this course. SetTradeDelays( 1, 1, 1, 1 ) with
BuyPrice = Open. Claims: you processed the data overnight, your order reached the opening
auction, you were filled in full at the printed opening price. Honest about overnight gap
risk, because the gap is in the fill.
Next bar’s close. SetTradeDelays( 1, 1, 1, 1 ) with BuyPrice = Close. Claims a
market-on-close order placed during the following session. Reasonable, and it removes the gap
from the entry — but note that it also hands the rule a whole extra session of information
that it does not use, and it lengthens the delay between decision and action to about a day
and a half.
A resting level. A stop or limit order, expressed as a price array rather than a price field. The guide’s own idiom builds the entry from a crossing of the level and clamps the fill into the bar:
Fragment — not a complete formula
Buy = Cross( High, BuyStop );BuyPrice = Max( BuyStop, Low );Claims that your order was resting in the book and that it was filled when price reached your level. That is a strong claim for a limit order — reaching a price is not the same as being filled at it, because the queue in front of you may absorb all the volume. For a stop order it is weaker but still optimistic, because a stop becomes a market order and market orders in a fast move do not fill at the trigger price.
Some average of the bar. Avg, or a typical price. Claims that you traded gradually
through the session and achieved something near the average print. Defensible for a large,
patiently worked order; indefensible for a system that says “buy on the signal”.
Slippage
Section titled “Slippage”Slippage is the difference between the price your model assigned and the price you would have achieved. It has four sources, and only the first one is small:
- Crossing the spread. You buy at the ask and sell at the bid. Unavoidable for anything that demands immediacy.
- Market impact. Your own order consumes the near levels of the book and moves the price against you, in proportion to your size relative to available depth.
- Latency and queue position. Between deciding and arriving, the book changed.
- Adverse selection. The times your order fills easily are disproportionately the times someone better informed was happy to take the other side.
AmiBroker has no SetSlippage() function — do not go looking for one. The transparent way to
model it at this stage is to move the fill price against yourself in the formula:
Fragment — not a complete formula
SlippagePct = 0.10; // one side, percentBuyPrice = Open * ( 1 + SlippagePct / 100 );SellPrice = Open * ( 1 - SlippagePct / 100 );This is crude. Real slippage is not a constant percentage, it is worse in small instruments, worse in fast markets and worse on the days your signal is most likely to fire. Its virtue is that it is visible, sits in the formula next to the assumption that documents it, and can be doubled in one edit. Part 28 covers richer approaches.
Commission
Section titled “Commission”AmiBroker models commission through two documented options:
| Option | Documented meaning |
|---|---|
SetOption( "CommissionMode", 0 ) |
Use the portfolio manager commission table |
SetOption( "CommissionMode", 1 ) |
Percent of trade value |
SetOption( "CommissionMode", 2 ) |
Fixed amount per trade |
SetOption( "CommissionMode", 3 ) |
Amount per share or contract |
SetOption( "CommissionAmount", n ) |
The amount, interpreted according to modes 1 to 3 |
Fragment — not a complete formula
SetOption( "CommissionMode", 1 ); // percent of trade valueSetOption( "CommissionAmount", 0.10 );Two practical points. First, option names are strings, and a typo is silently ignored — there is no compile-time check, so a misspelled option simply does nothing and your test runs without the cost you thought you had applied. Verify by comparing one trade’s profit against the arithmetic by hand. Second, the mode you choose changes which systems look good: a fixed amount per trade penalises small positions heavily and is the realistic model for many retail brokers, while a percentage is closer to the truth in markets with ad-valorem charges and taxes. Choose the one that matches the account you would actually trade, and say which.
Liquidity limits
Section titled “Liquidity limits”The backtester does not know how much of an instrument traded. It will happily simulate a 100,000-unit position in a symbol whose entire daily turnover was 40,000. Nothing warns you.
Two separate constraints are needed, and they do different jobs.
A universe floor removes symbols that are too thin to study at all:
Fragment — not a complete formula
AverageTurnover = MA( Close * Volume, 50 );LiquidEnough = AverageTurnover >= 5000000;Turnover rather than share volume, because volume is not comparable across price levels. The window and the threshold are both choices; record them.
A participation cap limits how much of the available trading one position may represent:
Fragment — not a complete formula
SlotValue = StartEquity * PositionPct / 100;SmallEnough = SlotValue <= 0.01 * AverageTurnover;The two are not interchangeable. A symbol can clear a 5,000,000 floor comfortably and still be a symbol in which your intended position is 30% of a day’s trading, if your account is large. The floor is about the instrument; the cap is about you.
When a candidate fails the cap, drop the signal rather than shrinking the position. Shrinking looks kinder and is not: it quietly converts an untradeable idea into a tradeable one and lets it contribute to your statistics. A trade you could not have placed is not a trade you get to count.
Gaps through levels
Section titled “Gaps through levels”A stop level is not a fill price. If you place a stop at 48.00 and the instrument closes at 48.60 and opens the next morning at 44.10, you were not filled at 48.00. You were filled somewhere near 44.10, and the loss is roughly four times the one your risk calculation allowed for.
AmiBroker’s ApplyStop() controls this through its fourth argument, ExitAtStop, which is
three-valued rather than Boolean:
| Value | Documented behaviour |
|---|---|
| 0 | Check stops using only the trade price, and exit at the regular trade price |
| 1 | Check high-low prices and exit intraday at the exact stop level, on the bar the stop triggered |
| 2 | Check high-low prices but exit next bar, at the regular trade price |
Value 1 is the one everybody uses and it is the optimistic one: it assumes the market traded continuously through your level and that you were filled there. On a gap, it did not and you were not. Value 2 is the conservative model — the stop is recognised from the bar’s range, but the exit happens at the next bar’s trade price, which on a gap is the bad price you would really have received.
Passing True for this argument gets you mode 1, because True is 1. That is worth knowing,
because a great many published formulas pass True without their authors realising there was
a third option.
Part 28 covers ApplyStop() in full, including its documented stop types, its
sampled-at-entry third argument and the fixed precedence when two stops fire on the same bar.
For now the assumptions block needs one line: which model you used, and therefore what you are
claiming about gaps.
The assumptions block
Section titled “The assumptions block”Here is the artefact. It is a working formula with a harness above and below two rule lines, so the assumptions are not a comment that drifts away from the code — every stated number appears again as a named constant that the simulation actually uses.
Complete runnable AFL
// assumptions-block.afl// Part 27 - Execution Assumptions You Must State//// A harness, not a strategy. Everything above and below the two rule lines is// the part you reuse; the two rule lines are the part you replace. The point of// the file is that after you have filled in the header, a reader can tell what// market the simulated trades happened in without running anything.//// Every value in the ASSUMPTIONS header appears again as a named constant in// the code, so the prose and the simulation cannot drift apart. If you change// one, change the other in the same edit.//// ============================ ASSUMPTIONS =============================// Data source Your end-of-day database. Record the vendor and the date// the history was last refreshed in your research log.// Adjustment Split- and dividend-adjusted daily bars.// Interval Daily.// Universe Whatever watch list this is applied to, further restricted// by the liquidity floor below. The universe is part of the// result: a conclusion drawn here applies to nothing else.// Survivorship NOT controlled. A watch list of today's members answers a// question about survivors. See Part 30.// Signal timing Computed on the close of bar t from data up to bar t.// Order placed After the close of bar t.// Fill The opening print of bar t+1.// Trade delays 1 bar on buy, sell, short and cover.// Slippage 0.10% of the fill price, charged against you on entry and// again on exit, applied by moving BuyPrice/SellPrice.// Commission 0.10% of trade value per side, via CommissionMode 1.// Liquidity floor 50-day average turnover of at least 5,000,000 in the// quote currency on the signal bar.// Participation Position value capped at 1% of that average turnover, so// the simulated order is a small fraction of a normal day.// Position sizing 10 slots, 10% of portfolio equity each.// Initial equity 100,000 in the quote currency.// Interest 0% on idle cash, so the result is not flattered by a rate// that has nothing to do with the trading rule.// Short selling Not simulated. Long only.// Borrowing None. AccountMargin 100 means no margin.// Gaps An overnight gap through any level is taken as it comes,// because the fill is an actual printed open. There are no// intrabar stops in this harness, so nothing here assumes a// price that was never traded.// NOT MODELLED Bid-ask spread beyond the slippage figure; market impact// beyond the participation cap; taxes; borrow fees; partial// fills; exchange holidays that differ from your data.// ======================================================================//// How to run it:// Formula Editor -> paste -> Tools -> Send to Analysis// Apply to: Filter, and choose your watch list// Range: All quotations, or an explicit From-To range you record// Then press Backtest.
// ---------------------------------------------------------------- costsSlippagePct = 0.10; // one side, percent of the fill priceCommissionPct = 0.10; // one side, percent of trade value
// ------------------------------------------------------------ portfolioStartEquity = 100000;PositionSlots = 10;PositionPct = 100 / PositionSlots;
// ------------------------------------------------------------ liquidityTurnoverWindow = 50;MinTurnover = 5000000; // quote currency, 50-day averageMaxParticipation = 0.01; // position value as a fraction of that average
SetOption( "InitialEquity", StartEquity );SetOption( "MaxOpenPositions", PositionSlots );SetOption( "AccountMargin", 100 ); // 100 = no marginSetOption( "InterestRate", 0 );
// CommissionMode 1 means "percent of trade value"; CommissionAmount is then// read as that percentage. Modes 2 and 3 are per-trade and per-share instead.SetOption( "CommissionMode", 1 );SetOption( "CommissionAmount", CommissionPct );
SetPositionSize( PositionPct, spsPercentOfEquity );
// One bar of delay on every signal: the rule is computed on a close, the order// cannot exist until after that close, so the first tradeable price is the// next opening print.SetTradeDelays( 1, 1, 1, 1 );
// Slippage is applied by moving the fill against us on both sides. This is// crude - real slippage is neither constant nor symmetric - but it is visible,// it is in the formula rather than in a dialog nobody will read, and it can be// doubled in one edit to see whether the conclusion survives.BuyPrice = Open * ( 1 + SlippagePct / 100 );SellPrice = Open * ( 1 - SlippagePct / 100 );
// AmiBroker clamps trade prices into the bar's High-Low range, so on a bar// that opens at its own high the buy slippage above is silently truncated.// That makes the harness slightly optimistic, not pessimistic - worth knowing.
AverageTurnover = MA( Close * Volume, TurnoverWindow );LiquidEnough = AverageTurnover >= MinTurnover;
// ------------------------------------------------------- THE RULES ONLY// Replace these two lines with your own entry and exit. Everything else in// this file stays as it is.EntryRule = Cross( Close, MA( Close, 200 ) );ExitRule = Cross( MA( Close, 200 ), Close );// ----------------------------------------------------------------------
// The participation cap, expressed as a check rather than as a silent// adjustment: if one slot would be larger than MaxParticipation of the symbol's// average turnover, the entry is dropped instead of being quietly shrunk.// Dropping is the honest choice - a trade you could not have placed is not a// trade you get to count. SlotValue is measured against the STARTING equity, so// it is an approximation that gets less conservative as the account grows;// Part 30 returns to what compounding into thin symbols does to a result.SlotValue = StartEquity * PositionPct / 100;SmallEnough = SlotValue <= MaxParticipation * AverageTurnover;
// Every filter is applied before the excess signals are removed. Filtering// after ExRem would leave exits with no matching entry.Buy = EntryRule AND LiquidEnough AND SmallEnough;Sell = ExitRule;
// Redundant signals are removed here as well as inside the backtester, so that// a chart of this formula shows the same alternating pattern the report is// built from rather than a cloud of repeats.Buy = ExRem( Buy, Sell );Sell = ExRem( Sell, Buy );What each section is for
Section titled “What each section is for”The header is the part a reader without AmiBroker can evaluate. It names the data source, the adjustment, the universe, the timing, the costs, the sizing, the caps, and — the section people leave out — what is explicitly not modelled. That last list is what stops a reader assuming you handled something you did not.
The costs and portfolio constants convert the header into numbers in one place. When you run the doubling test, you change one constant rather than hunting through the formula.
The settings calls put every backtester option in the formula rather than in a dialog.
SetOption( "AccountMargin", 100 ) means no margin; SetOption( "InterestRate", 0 ) stops
idle cash earning a return that has nothing to do with your trading rule and can quietly
supply a meaningful share of the profit in a system with low exposure.
The execution section applies the delay and the slippage. The filters apply the
liquidity floor and the participation cap before ExRem(), because filtering after removing
redundant signals can leave an exit with no matching entry.
The two rule lines are the only part you replace.
The prose version
Section titled “The prose version”You will also want the block in a form you can paste into a research note, because the AFL version is only readable by people who read AFL:
| Assumption | Value | Why this one |
|---|---|---|
| Data source and refresh date | ||
| Adjustment | ||
| Interval and periodicity | ||
| Universe and membership date | ||
| Liquidity floor | ||
| Participation cap | ||
| Signal timing | ||
| Fill price and delay | ||
| Slippage per side | ||
| Commission model and amount | ||
| Initial equity and sizing | ||
| Interest on idle cash | ||
| Stop model and gap treatment | ||
| Explicitly not modelled |
An empty cell in that table is a decision that has been made by something other than you.
What none of this fixes
Section titled “What none of this fixes”Stating your assumptions makes a result interpretable. It does not make it correct. A test with an immaculate assumptions block can still be ruined by survivorship in the database, by a look-ahead in a multi-timeframe expansion, or by having been selected from among forty variations. Parts 30 and 32 deal with those, and they are larger problems than costs.
The assumptions block does one thing well: it makes the size of your remaining uncertainty visible, to a reader and to you. That is the precondition for everything else.
Four fill models, each a different claim: next open, next close, a resting level, an average of the bar. AmiBroker clamps whatever you choose into the bar’s high-low range, which protects you from impossible prices and can also quietly truncate a slippage adjustment.
Slippage has four sources and no dedicated AFL function; model it by moving the fill price
against yourself, then run the test again at twice the figure. Commission goes in the formula
through SetOption( "CommissionMode", ... ) and SetOption( "CommissionAmount", ... ), never
in a dialog, and a misspelled option name fails silently. Liquidity needs two separate
constraints — a floor on the instrument and a cap on your participation — and a candidate that
fails the cap should be dropped rather than shrunk.
A stop level is not a fill price. ApplyStop()’s ExitAtStop argument decides whether your
simulation assumes a fill at the level (1), at the regular trade price on the same bar (0), or
at the next bar’s trade price (2), and only the last of those is honest about gaps.
The assumptions block collects all of it in one place, in a form both a reader and the simulation use. Next: the project, where all of this gets attached to a real, deliberately plain system, and where we run the backtest and then refuse to improve it.
Check your understanding
Sources for this lesson
5 verified · checked 2026-08-31
- 01AFL Function Reference — SetOptionamibroker.com/guide/afl/setoption.html2026-08-31
- 02AFL Function Reference — SetPositionSizeamibroker.com/guide/afl/setpositionsize.html2026-08-31
- 03AFL Function Reference — ApplyStopamibroker.com/guide/afl/applystop.html2026-08-31
- 04AmiBroker User's Guide — Back-testing your trading ideas§ Controlling trade priceamibroker.com/guide/h_backtest.html2026-08-31
- 05AmiBroker User's Guide — Portfolio-level backtestingamibroker.com/guide/h_portfolio.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.