Skip to content
Level 4 · Trading System ResearcherLessonPart 31 · page 1 of 528 min
28Minutes
9AFL functions
9Sources
StandardRequires
AFL functions taught here9

What Optimize() Actually Does

Optimize() is one line of AFL with an unusual property: what it does depends entirely on which button you pressed. Press Backtest and it is an elaborate way of writing a constant. Press Optimize and the same line turns your formula into several hundred formulas. Most of the trouble people get into with optimization starts with not knowing which of those two things is happening.

By the end of this lesson you will be able to write the function correctly, say what it returns in every Analysis mode, work out how many backtests a set of ranges will produce before you start one, run an optimization on a complete portfolio system, and read what comes back without being misled by the way AmiBroker chooses to sort it.

The official reference gives one form, with five positional arguments and no optional ones:

Fragment — not a complete formula

FastPeriod = Optimize( "Fast MA", 20, 5, 60, 5 );

Read as Optimize( "description", default, min, max, step ):

Argument What it is
"description" A string identifying this variable. It becomes the column heading for the parameter in the optimization result list.
default The value the function returns in exploration, indicator, commentary, scan and normal backtest modes.
min The lowest value the optimizer will try.
max The highest value the optimizer will try.
step The increment used to walk from min to max.

The function returns a NUMBER — a scalar, not an array — which is why it can be used anywhere a plain number can, including as an argument to another function.

What it returns when you are not optimizing

Section titled “What it returns when you are not optimizing”

This is the fact that catches people, and it is stated plainly on the reference page: in normal backtesting, scanning, exploration and commentary modes, Optimize() returns the default value. Only in Optimization mode does it return successive values from min to max, inclusively, stepping by step.

What you ran What Optimize( "Fast MA", 20, 5, 60, 5 ) gives you
Indicator / chart pane 20
Commentary 20
Scan 20
Exploration 20
Backtest 20
Optimization 5, then 10, then 15 … up to 60

So the line above, on every run that is not an optimization, is exactly equivalent to FastPeriod = 20;. That design is deliberate and useful: one file charts, scans, backtests and optimizes without being edited. It also produces the single most common optimization mistake in AmiBroker.

That manual step reads like an omission and is closer to a favour. The moment a program starts silently rewriting your formula with the best of several hundred fitted values, you lose the record of what you chose and why. Typing it in is a small tax that keeps the decision visible, and the lab at the end of this part turns it into a documented act rather than an afterthought.

The two functions look almost identical and do opposite things.

  • Param( "name", defaultval, min, max, step, sincr ) puts a slider in the Parameters dialog. Drag it and the chart redraws immediately. It does not create an optimization sweep.
  • Optimize( "description", default, min, max, step ) creates an optimization sweep. It puts nothing in the Parameters dialog, so an indicator built on Optimize() looks frozen on a chart with no way to explore it interactively.

AmiBroker’s author published a helper in a comment on the official Optimize page that combines the two, so a single declaration both slides and sweeps:

Fragment — not a complete formula

function ParamOptimize( pname, defaultval, minv, maxv, step )
{
return Optimize( pname,
Param( pname, defaultval, minv, maxv, step ),
minv, maxv, step );
}

Ranges, steps and how many backtests you just asked for

Section titled “Ranges, steps and how many backtests you just asked for”

The values run from min to max inclusively. The number of values one Optimize() call produces is therefore:

Pseudocode — not valid AFL

number of values = (max - min) / step, plus one

The plus one matters and the User’s Guide is loose about it: the optimization chapter says each call generates (max - min)/step loops, while the same page says the values run from min to max inclusively. Those two statements disagree by one. The inclusive count is right, and AmiBroker’s own Knowledge Base confirms it — the article on the Exclude statement describes two parameters each running 1 to 100 in steps of 1 as “all 10000 backtest runs”, which is 100 × 100, and 100 is (100 − 1)/1 + 1.

Declaration Values tried Count
Optimize( "p", 20, 5, 60, 5 ) 5, 10, 15 … 60 12
Optimize( "p", 20, 10, 20, 1 ) 10, 11, 12 … 20 11
Optimize( "p", 2, 1, 3, 0.5 ) 1, 1.5, 2, 2.5, 3 5

Two parameters do not cost you the sum of their step counts. They cost the product, because every value of one is tried against every value of the other. The guide works the arithmetic through with parameters ranging 1 to 100 in steps of 1:

Parameters Combinations
1 100
2 10,000
3 1,000,000
4 100,000,000
5 10,000,000,000

Each of those combinations is a complete backtest over your whole universe and date range. If one backtest takes a second, the three-parameter case takes about eleven days of machine time. This is the honest reason to limit exhaustive optimization to a small number of parameters — the guide’s own advice is to keep it “to just a few” — and it is also, quietly, a research argument rather than a computing one. A system with five tunable parameters has five degrees of freedom with which to fit the past, and Part 30’s lesson on curve fitting explains why that is a liability rather than a feature.

The User’s Guide states two hard limits: up to 64 calls to Optimize(), and a maximum search space of 264 combinations.

Three requirements come from the documentation, and breaking any of them corrupts the run rather than producing an error message.

  1. Call each Optimize() exactly once, near the top of the formula. The guide states it directly: each call generates new optimization loops. Putting one inside an if branch, a loop, or a function that runs conditionally does not give you a conditional parameter; it gives you a search space that is not the one you think you declared.
  2. Give every call a unique description string. A comment published on the official page reports that copy-pasting an Optimize() line without changing the description produced all-zero results. Duplicate descriptions are a bug with no diagnostic.
  3. The description is the column heading. Name it so that you can still tell what it meant when you read the result list a month later. "Fast MA" and "Slow MA" beat "p1" and "p2" for exactly the reason MaPeriod beats p everywhere else in this course.

Half the grid of a two-average system is nonsense: every combination where the “fast” average is slower than the “slow” one. Those runs still consume time and still print rows.

The reserved variable Exclude is the documented tool. It is a variable, not a function — there is no Exclude() page in the function reference — and setting it true removes the current run from scan, exploration and backtest statistics, and from buy-and-hold calculations:

Fragment — not a complete formula

FastPeriod = Optimize( "Fast MA", 20, 5, 60, 5 );
SlowPeriod = Optimize( "Slow MA", 120, 60, 260, 10 );
Exclude = FastPeriod >= SlowPeriod;

We need a system with a parameter space worth looking at: two parameters, so the result is a surface rather than a line; a sensible portfolio setup, so the numbers are not fantasy; and rules simple enough that nothing hides behind them. A two-moving-average crossover is the obvious candidate — not because it is a good system, but because everybody already understands what its parameters mean, which lets the lesson be about the search rather than about the rules.

Complete runnable AFL

optimizable-trend-system.afl
// optimizable-trend-system.afl
// Part 31 - Optimization
//
// A deliberately plain two-parameter trend system, written so that the SAME
// file runs as a chart, a scan, a backtest and an optimization without being
// edited. Its job in this part is not to be a good system. Its job is to give
// us a parameter space whose shape we can look at.
//
// ASSUMPTIONS - every number this file produces inherits all of them:
// - Daily bars. Portfolio backtest: one account, cash only, no margin.
// - Signals are computed from the close of the signal bar. Every entry and
// exit is delayed one bar and filled at the NEXT bar's open, so nothing is
// ever filled at a price that was not yet known when the signal formed.
// - Costs: commission is charged as a percentage of trade value, per side,
// and is set from the formula so that it is recorded with the run. The
// figure below is a stand-in for YOUR broker's commission plus an
// allowance for the spread you would have crossed. Raise it, do not
// lower it.
// - Liquidity: a symbol may not be bought unless its average turnover over
// the past 50 bars clears the floor below. Without this filter the
// optimizer finds its best cells in symbols you could never have
// transacted in, and the surface you read is largely fiction.
// - No survivorship correction is applied. If the database contains only
// companies that still exist, every result is optimistic by an amount
// this file cannot measure.
//
// Both optimized variables are declared exactly ONCE, at the top, with unique
// description strings. The User's Guide requires both: each call generates its
// own optimization loops, and duplicated description strings produce garbage.
_SECTION_BEGIN( "Optimizable Trend System" );
// --- The parameter space -------------------------------------------------
// Optimize( "description", default, min, max, step ) hands back "default" in
// every mode except Optimization. The two numbers below are therefore also the
// parameters this file trades with when you press Backtest, Scan or Explore.
FastPeriod = Optimize( "Fast MA", 20, 5, 60, 5 );
SlowPeriod = Optimize( "Slow MA", 120, 60, 260, 10 );
// Combinations where the "fast" average is not actually faster are not a
// system. Exclude suppresses their statistics so they do not clutter the
// result list or waste report time. It does not reduce the number of
// combinations the optimizer enumerates.
Exclude = FastPeriod >= SlowPeriod;
// --- Fixed assumptions ---------------------------------------------------
StartingEquity = 100000;
MaxPositions = 10;
CommissionPct = 0.30; // percent of trade value, charged per side
MinTurnover = 2000000; // in the currency of the database
TurnoverPeriod = 50;
SetOption( "InitialEquity", StartingEquity );
SetOption( "MaxOpenPositions", MaxPositions );
SetOption( "CommissionMode", 1 ); // 1 = percent of trade value
SetOption( "CommissionAmount", CommissionPct );
SetOption( "AllowPositionShrinking", True );
// Ten slots, so each entry asks for a tenth of current portfolio equity.
SetPositionSize( 100 / MaxPositions, spsPercentOfEquity );
// Signals form on the close; trades happen on the following open.
SetTradeDelays( 1, 1, 1, 1 );
BuyPrice = Open;
SellPrice = Open;
// --- Liquidity filter ----------------------------------------------------
// MA() is Null until it has enough bars, and any comparison with Null is Null,
// so the filter is written to be explicitly false during warm-up rather than
// silently undefined.
Turnover = Close * Volume;
AvgTurnover = MA( Turnover, TurnoverPeriod );
LiquidEnough = NOT IsNull( AvgTurnover ) AND AvgTurnover >= MinTurnover;
// --- The rules -----------------------------------------------------------
FastAvg = MA( Close, FastPeriod );
SlowAvg = MA( Close, SlowPeriod );
Buy = Cross( FastAvg, SlowAvg ) AND LiquidEnough;
Sell = Cross( SlowAvg, FastAvg );
// When more symbols signal on one bar than there is capital for, the
// backtester needs a rule for choosing between them. Ranking by how far price
// sits above its slow average is one choice among many, and it is part of the
// system being optimized - not a neutral detail.
PositionScore = 100 * ( Close - SlowAvg ) / SlowAvg;
// --- Chart output, so the same file can also be read visually -------------
Plot( Close, "Close", colorDefault, styleCandle );
Plot( FastAvg, "Fast MA(" + FastPeriod + ")", colorBlue, styleLine );
Plot( SlowAvg, "Slow MA(" + SlowPeriod + ")", colorRed, styleLine );
_SECTION_END();

Download optimizable-trend-system.afl93 lines

The file falls into five sections. The parameter space comes first, in the two Optimize() calls and the Exclude line, exactly as the guide requires. The fixed assumptions follow: starting equity, ten position slots, commission as a percentage of trade value set through SetOption( "CommissionMode", 1 ) and SetOption( "CommissionAmount", … ), and one-bar trade delays with fills at the next open. Setting those from the formula rather than the Settings dialog means they travel with the file, so a run is reproducible from the file alone.

The liquidity filter computes average turnover — close times volume, averaged — and compares it to a floor. It is written as NOT IsNull( AvgTurnover ) AND AvgTurnover >= … rather than the shorter comparison alone, because MA() returns Null during warm-up and any comparison against Null is Null rather than false.

The rules are two crossovers. The ranking line sets PositionScore to the percentage distance of price above its slow average, which decides who gets a slot when more symbols signal on one bar than there is cash for. That line is part of the system, not plumbing: change it and the optimization surface changes.

  • Optimize( "description", default, min, max, step ) — declares one swept variable; returns default outside optimization.
  • Exclude — a reserved variable. True suppresses statistics for the current run.
  • SetOption( "CommissionMode", 1 ) — commission expressed as a percentage of trade value; mode 2 is a fixed amount per trade and mode 3 is per share or contract.
  • SetTradeDelays( buydelay, selldelay, shortdelay, coverdelay ) — shifts the four signal arrays inside the backtester. It shifts nothing else: price arrays, PositionSize and PositionScore are untouched.
  • SetPositionSize( size, method ) with spsPercentOfEquity — size as a percentage of portfolio-level equity.

Applied to a chart, you see candles with two moving averages using the default periods — 20 and 120 — because that is what Optimize() returns outside an optimization. Sent to the Analysis window and backtested, you get one report for the 20/120 pair. Optimized, you get a result list with one row per surviving combination and two extra columns at the far right headed Fast MA and Slow MA.

Run a Backtest first and write down two numbers from the report: Annual Return % and the number of trades. Then edit the two defaults to some other pair inside the ranges, apply, and backtest again. Both numbers should change. If they do not, the formula is not reading your edits — check that you applied the formula rather than just saving it, and that you edited the second argument of Optimize() and not the third.

  • Editing min instead of default. The backtest keeps using the old default and you conclude the parameters do not matter.
  • Two Optimize() calls with the same description string. No error; garbage results.
  • Forgetting Exclude. The result list fills with combinations in which the fast average is slower than the slow one, and some of them will float to the top, because a system that trades almost never can post a striking-looking ratio.
  • Leaving costs at zero. The optimizer will then reliably prefer the shortest holding periods available in your grid, since their only disadvantage has been switched off.

Add a third Optimize() call for the turnover floor and watch the run time. Before you start it, work out the new combination count on paper. That arithmetic is the point of the exercise; running it is optional.

One optimization run, end to end

  1. Declare the spaceOne Optimize() call per parameter, at the top, unique descriptions
  2. Count it(max − min)/step + 1 per parameter, then multiply
  3. Set Apply to and RangeWhich symbols, which dates — recorded as part of the result
  4. Press OptimizeOne complete backtest per combination
  5. Read the whole listNot just the top row

The New Analysis window is where this happens. Apply to chooses All symbols, Current symbol or Filter; Range chooses All quotations, N recent bars, N recent days, or From-To dates. Both belong in your notes, because a result without a universe and a period attached is not a result.

The drop-down arrow on the Optimize button holds four commands: Optimize, Walk-Forward, 3D Optimization chart and Individual Optimize. Only the first is in scope here; the 3D chart appears in the next lesson and walk-forward is Part 32.

Two practical notes before you start a long run:

  • The warning threshold. Settings → Report tab has a checkbox, Warn before running time-consuming optimizations, which triggers above 300 steps. If you dismiss that dialog by reflex you have removed the one thing standing between you and an overnight run you did not intend.
  • Threads. AmiBroker’s threading model is one operation on one symbol equals one thread, so an optimization over many symbols parallelises well while a single-symbol optimization is single-threaded. Individual Optimize, added in 5.70, is the documented exception: it spreads a single-symbol optimization across cores, at the cost of not supporting the custom backtester and not supporting the smart search engines. The Standard edition allows 2 threads per Analysis window and the Professional edition up to 32, so the same run can differ by more than an order of magnitude in wall-clock time between editions. Neither edition changes the results.

One row per combination. The parameter values occupy the last columns by default — the guide says so explicitly — which on a wide result list means scrolling right past every metric to find out which run you are even looking at. SetOption( "ExtraColumnsLocation", 1 ) moves them to the front. The guide notes this changes the visual order only, not the export or copy-paste order, so what you copy out keeps its original arrangement.

Then the fact that changes how you should read the whole thing:

Sorting by any column is supported, and the guide names the obvious ones: lowest drawdown, lowest number of trades, largest profit factor, lowest market exposure, highest risk-adjusted annual return. Two habits are worth forming now. First, always sort by the number of trades once, just to see how thin the extremes are. Second, look at the rows adjacent in parameter space to whichever row you liked — which is the whole subject of the next two lessons.

One more default worth knowing: backtest reports are disabled during optimization. The SetOption reference states it directly under GenerateReport. You get the result list, not several hundred HTML reports, which is almost always what you want; forcing full reports with SetOption( "GenerateReport", 1 ) is possible and will cost you disk and time in proportion to the grid.

Optimize() takes five required arguments and returns a number. Outside Optimization mode it returns its default, so the same file charts, scans, backtests and optimizes unchanged — and so a backtest run straight after an optimization silently uses the default rather than the winner. Nothing is written back into your formula; you type the values in.

Values run inclusively from min to max, so one call produces (max − min)/step + 1 tests, and multiple calls multiply. The documented ceilings are 64 variables and a 264 search space, and the practical ceiling is far lower. Exclude prunes meaningless combinations from the statistics without shrinking the declared space.

And the result list arrives sorted by Net % profit, which is a presentation choice rather than a judgement about your system. What to do with the rest of that list is the rest of this part.

Check your understanding

Question 1. A formula contains `Period = Optimize( "Period", 20, 10, 50, 5 );`. You press Backtest, not Optimize. What value does `Period` hold?
Period = Optimize( "Period", 20, 10, 50, 5 );
Show the answer and why

Answer: 20, the default

Outside Optimization mode the function returns its second argument. That is what lets one file serve as chart, scan, backtest and optimization — and it is why the winning value from an optimization has no effect until you type it into that second argument yourself.

Question 2. How many backtests does this pair of declarations produce in an exhaustive optimization?
a = Optimize( "a", 10, 10, 30, 5 );
b = Optimize( "b", 100, 100, 160, 20 );
Show the answer and why

Answer: 25

`a` takes 10, 15, 20, 25, 30 — that is (30−10)/5 + 1 = 5 values. `b` takes 100, 120, 140, 160 — (160−100)/20 + 1 = 4 values. The calls multiply, so 5 × 4 = 20. The inclusive "plus one" is the part people drop, and the multiplication is the part that makes three-parameter grids unrunnable.

Question 3. Which of these will corrupt an optimization without producing any error message? Select all that apply.
Show the answer and why

Answer: Two Optimize() calls that share the same description string, Calling Optimize() inside an if branch

Duplicate description strings are reported to produce all-zero results, and the guide requires each Optimize() call to appear once at the beginning of the formula because each call generates its own loops. Exclude is the documented way to prune invalid combinations. Leaving the default sort is not corruption — but it is a trap, because that sort is by Net % profit rather than by anything you chose.

Question 4. You optimize a single symbol on the Standard edition and it feels slow. What does the documentation say about why?
Show the answer and why

Answer: One operation on one symbol is one thread, so a single-symbol optimization is single-threaded unless you use Individual Optimize

The threading model is one operation × one symbol = one thread. Individual Optimize, added in 5.70, is the documented exception that spreads a single-symbol optimization across cores — though it supports neither the custom backtester nor the smart search engines. Edition affects thread ceilings (2 versus 32 per Analysis window), not availability, and it never changes the numbers.

Sources for this lesson

9 verified · checked 2026-08-31

  1. 01AmiBroker AFL Function Reference — Optimizeamibroker.com/guide/afl/optimize.html2026-08-31
  2. 02AmiBroker User's Guide — How to optimize a trading systemamibroker.com/guide/h_optimization.html2026-08-31
  3. 03AmiBroker User's Guide — Using New Analysis windowamibroker.com/guide/h_newanalysis.html2026-08-31
  4. 04AmiBroker AFL Function Reference — SetOptionamibroker.com/guide/afl/setoption.html2026-08-31
  5. 05AmiBroker AFL Function Reference — Paramamibroker.com/guide/afl/param.html2026-08-31
  6. 06AmiBroker Knowledge Base — Using optimum parameter values in backtestingamibroker.com/kb/2015/01/02/using-optimum-parameter-values-in-backtesting2026-08-31
  7. 07AmiBroker Knowledge Base — Using Exclude statement to skip unwanted optimization stepsamibroker.com/kb/2015/02/05/using-exclude-statement-to-skip-unwanted-optimization-steps2026-08-31
  8. 08AmiBroker User's Guide — Efficient use of multi-threadingamibroker.com/guide/h_multithreading.html2026-08-31
  9. 09AmiBroker User's Guide — System test settings window§ Report tabamibroker.com/guide/w_settings.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.