Skip to content
Level 4 · Trading System ResearcherLessonPart 27 · page 4 of 528 min
28Minutes
6AFL functions
5Sources
StandardRequires
AFL functions taught here6

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.

Between the signal and the money

  1. SignalA 1 in the Buy array on some bar
  2. TimingWhich bar the order reaches the marketSetTradeDelays
  3. Fill priceThe price the simulation assignsBuyPrice / SellPrice
  4. SlippageThe difference between that price and reality
  5. CommissionWhat the broker chargesSetOption
  6. Size limitsHow much the market could absorb
  7. Realised tradeWhat actually reaches the equity curve
A default backtest models the first three layers and silently sets the rest to zero.

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.

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 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, percent
BuyPrice = 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.

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 value
SetOption( "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.

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.

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.

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
// 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.
// ---------------------------------------------------------------- costs
SlippagePct = 0.10; // one side, percent of the fill price
CommissionPct = 0.10; // one side, percent of trade value
// ------------------------------------------------------------ portfolio
StartEquity = 100000;
PositionSlots = 10;
PositionPct = 100 / PositionSlots;
// ------------------------------------------------------------ liquidity
TurnoverWindow = 50;
MinTurnover = 5000000; // quote currency, 50-day average
MaxParticipation = 0.01; // position value as a fraction of that average
SetOption( "InitialEquity", StartEquity );
SetOption( "MaxOpenPositions", PositionSlots );
SetOption( "AccountMargin", 100 ); // 100 = no margin
SetOption( "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 );

Download assumptions-block.afl126 lines

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.

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.

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

Question 1. Your system is assigned a fill of Open * 1.001 to model slippage, on a bar that opened at its own high. What price does the backtester actually use?
Show the answer and why

Answer: The bar’s high, because price-bound checking clamps values above the high

AmiBroker checks whether the assigned price fits the bar’s high-low range and adjusts it to the high if it is above. On a bar that opens at its high, the buy slippage is therefore discarded, making that trade slightly cheaper than you intended. The effect is small but it is in the optimistic direction, which is worth knowing.

Question 2. Which of these belong in a formula rather than in the Settings dialog? Select all that apply.
Show the answer and why

Answer: Commission mode and amount, Trade delays, Initial equity and maximum open positions

Anything that changes the simulated result should travel with the file, because a setting left in a dialog is invisible to a reader, does not survive being sent to someone else, and can be changed by whoever used the installation last. Cosmetic chart choices do not affect the result and can live wherever you like.

Question 3. A candidate passes your entry rule but one position slot would be 25% of its average daily turnover. What is the honest response?
Show the answer and why

Answer: Drop the signal, because a trade you could not have placed is not a trade you get to count

Shrinking converts an untradeable idea into a tradeable one and lets it contribute to your statistics anyway. The backtester models no depth at all: it will fill any size at the price you supplied. Dropping the signal is the only response that keeps the trade sample to trades you could have made.

Question 4. ApplyStop() is called with ExitAtStop set to 1, and the instrument gaps down through the stop level overnight. What does the simulation assume?
Show the answer and why

Answer: That you were filled at the exact stop level, which the market never traded at on that bar

Value 1 checks the high-low range and exits at the exact stop level on the triggering bar. When the bar gapped past the level, no trade occurred there, so the simulated exit price never existed. Value 2 checks the range but exits at the next bar’s regular trade price, which is the conservative model for gap risk.

Sources for this lesson

5 verified · checked 2026-08-31

  1. 01AFL Function Reference — SetOptionamibroker.com/guide/afl/setoption.html2026-08-31
  2. 02AFL Function Reference — SetPositionSizeamibroker.com/guide/afl/setpositionsize.html2026-08-31
  3. 03AFL Function Reference — ApplyStopamibroker.com/guide/afl/applystop.html2026-08-31
  4. 04AmiBroker User's Guide — Back-testing your trading ideas§ Controlling trade priceamibroker.com/guide/h_backtest.html2026-08-31
  5. 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.