Introduction to the Custom Backtester Interface
Ordinary AFL talks to the backtester through arrays: you assign Buy, PositionSize,
PositionScore, and the engine does the rest. Those arrays are computed before the backtest
runs, which means they cannot depend on anything the backtest discovers while running.
The Custom Backtester Interface removes that limitation. It gives you an object-oriented view of the backtest’s second phase, where the engine walks bar by bar deciding which trades to take — and lets your code participate in that walk.
This lesson tells you what it is for, shows the smallest useful example, and is honest about where this course stops.
What the CBI can do that AFL alone cannot
Section titled “What the CBI can do that AFL alone cannot”The Portfolio Backtester Interface Reference lists the applications directly:
- Position sizing based on portfolio-level equity. Ordinary
PositionSizeis an array computed before the run; it cannot know what the equity curve is doing. - Advanced rotational systems, with access to ranking arrays and the ability to decide what to take after knowing which symbols score best on a bar-by-bar basis.
- Custom metrics added to the backtest report and to optimisation result lists.
- Custom slippage formulas.
- Advanced scaling in and out based on portfolio equity and run-time statistics.
- Systems that use portfolio-level statistics evaluated bar by bar to decide which trades to take.
The thread running through all of them: decisions that depend on state the backtest generates as it runs. If your rule can be expressed as an array computed from price and volume, ordinary AFL is simpler, faster and easier to check. If it genuinely needs to know the current drawdown, or how many positions are open, or what the equity was three bars ago, the CBI is the only route.
Three levels of access
Section titled “Three levels of access”The reference describes three approaches, and choosing the wrong one is the most common mistake.
The three documented approaches
- High-level — Backtest()One call runs the default backtest procedure unchanged. You can add custom metrics before and after. You cannot change how any individual trade is taken. Described as "the easiest".
- Mid-level — PreProcess() / ProcessTradeSignal() / PostProcess()Signals can be modified and open positions queried. Documented as "good for advanced position sizing".
- Low-level — PreProcess() / EnterTrade() / ExitTrade() / ScaleTrade() / UpdateStats() / HandleStops() / PostProcess()Full control over the entire process. The reference says: "for hard-coded programmers only".
The important consequence for anyone starting: at the high level you cannot get the trades wrong,
because you are not the one taking them. bo.Backtest() runs AmiBroker’s own procedure exactly as
it would have run anyway. Everything you add is descriptive.
At the low level, every piece of the engine’s correctness becomes your responsibility — including
HandleStops(), which the reference explicitly says must not be called in the high-level and
mid-level approaches, and must be called once per bar inside the trading loop in low-level mode.
Getting access to the interface
Section titled “Getting access to the interface”Two documented steps.
1. Enable the custom procedure. Three ways:
Fragment — not a complete formula
// The procedure is in THIS file.SetOption( "UseCustomBacktestProc", True );
// Or: the procedure is in a separate file.SetCustomBacktestProc( "C:\\MyPath\\MyCustomBacktest.afl" );Or tick it in Analysis → Settings → the Portfolio tab, specifying an external procedure file.
2. Get the object, in the right context:
Fragment — not a complete formula
if( Status( "action" ) == actionPortfolio ){ bo = GetBacktesterObject(); // ... your custom backtest code}The reference is explicit that GetBacktesterObject() should only be called when Status( "action" )
returns actionPortfolio. There is one documented exception: when using an external procedure
file you do not need the check, because external backtest procedures are called exclusively in
actionPortfolio mode.
The minimal working example
Section titled “The minimal working example”Complete runnable AFL
// custom-backtest-minimal.afl// Part 36 - Introduction to the Custom Backtester Interface//// The smallest custom backtest procedure that does something real. It runs// AmiBroker's own portfolio backtest completely unchanged, then adds two// descriptive metrics that the built-in report does not contain.//// WHICH LEVEL THIS IS// This is the HIGH-LEVEL approach described in the Portfolio Backtester// Interface Reference: one call to bo.Backtest() runs the default procedure.// It cannot change how any individual trade is taken. The mid-level and// low-level approaches can, and they are a much larger subject that this// course points at rather than teaches.//// ASSUMPTIONS - state these on every strategy formula you write// Universe: whatever watch list the Analysis window is pointed at.// Period: whatever range is selected. Nothing here fixes a date range.// Interval: daily end-of-day bars.// Fill price: next bar's open, set by SetTradeDelays plus BuyPrice/SellPrice.// Delay: one bar between signal and fill, on entry and on exit.// Sizing: 10% of portfolio equity per position, set below.// Commission: set in Analysis -> Settings. This formula does not set it,// so a run with commission left at zero is not a real result.// Slippage: not modelled. Assume real fills are worse than these.// Liquidity: not modelled. Position size is never capped against volume.//// The rules below are a plain trend filter, present only to give the// backtester something to process. They are not a recommendation, and// nothing this produces is an expectation about future returns.
SetCustomBacktestProc( "" ); // "" means: the custom procedure is in THIS file
// The threshold used by the second custom metric, in bars.MinBarsHeld = 5;
if( Status( "action" ) == actionPortfolio ){ // Only valid in the second phase of a portfolio backtest. Anywhere else // GetBacktesterObject() hands back an empty object. bo = GetBacktesterObject();
// Run the default portfolio backtest, unchanged, in one call. bo.Backtest();
// Metric 1 - derived from AmiBroker's own statistics. st = bo.GetPerformanceStats( 0 ); // 0 = all trades
NetProfitAll = st.GetValue( "NetProfit" ); TradeCount = st.GetValue( "AllQty" ); AvgBarsHeld = st.GetValue( "AllAvgBarsHeld" ); BarsExposed = TradeCount * AvgBarsHeld;
if( BarsExposed > 0 ) ProfitPerBar = NetProfitAll / BarsExposed; else ProfitPerBar = 0;
bo.AddCustomMetric( "Profit per bar held ($)", ProfitPerBar );
// Metric 2 - derived by walking the closed trade list. ShortHolds = 0;
for( trade = bo.GetFirstTrade(); trade; trade = bo.GetNextTrade() ) { if( trade.BarsInTrade < MinBarsHeld ) ShortHolds = ShortHolds + 1; }
bo.AddCustomMetric( "Trades held under " + NumToStr( MinBarsHeld, 1.0 ) + " bars", ShortHolds );}
// --- The system under test --------------------------------------------------SetTradeDelays( 1, 1, 1, 1 );SetPositionSize( 10, spsPercentOfEquity );
BuyPrice = Open;SellPrice = Open;
Trend = Close > MA( Close, 200 );
Buy = Cross( Close, MA( Close, 50 ) ) AND Trend;Sell = Cross( MA( Close, 50 ), Close );
Short = False;Cover = False;This is the high-level approach: bo.Backtest() runs the default procedure unchanged, and two
descriptive metrics are added around it.
Metric 1 — derived from the built-in statistics
Section titled “Metric 1 — derived from the built-in statistics”Fragment — not a complete formula
st = bo.GetPerformanceStats( 0 ); // 0 = all trades
NetProfitAll = st.GetValue( "NetProfit" );TradeCount = st.GetValue( "AllQty" );AvgBarsHeld = st.GetValue( "AllAvgBarsHeld" );GetPerformanceStats( Type ) returns a Stats object, with Type documented as 0 for all trades, 1
for long-only and 2 for short-only. GetValue( "MetricName" ) then reads any built-in metric by
name.
Multiplying the trade count by the average bars held gives total bars of exposure, and dividing net profit by that gives profit per bar held — a figure the standard report does not contain, and one that makes two systems with very different holding periods directly comparable.
Metric 2 — derived by walking the trade list
Section titled “Metric 2 — derived by walking the trade list”Fragment — not a complete formula
for( trade = bo.GetFirstTrade(); trade; trade = bo.GetNextTrade() ){ if( trade.BarsInTrade < MinBarsHeld ) ShortHolds = ShortHolds + 1;}GetFirstTrade() and GetNextTrade() iterate the closed trade list. The documentation notes
that GetFirstTrade() must be called before GetNextTrade(), and the loop condition works because
the object is falsy when the list is exhausted.
trade.BarsInTrade is a documented property of the Trade object.
Counting very short holds is a diagnostic rather than a performance figure: a trend system that produces a large number of two-bar trades is usually being chopped up by a stop that is too tight or an exit that fires on noise, and no summary statistic will tell you that.
Adding the metric
Section titled “Adding the metric”Fragment — not a complete formula
bo.AddCustomMetric( "Profit per bar held ($)", ProfitPerBar );The full documented signature is:
Pseudocode — not valid AFL
AddCustomMetric( Title, Value, [LongOnlyValue], [ShortOnlyValue], [DecPlaces = 2], [CombineMethod = 2] )Custom metrics appear in the backtest report, the backtest summary and the optimisation result list — which is the reason they matter more than they first appear. A custom metric can be an optimisation target, so the CBI is how you optimise for something AmiBroker does not natively measure.
When you actually need this
Section titled “When you actually need this”Be honest about the answer, because the CBI costs you debuggability and simplicity.
You need it when:
- The decision depends on portfolio state that does not exist until the backtest runs — current equity, current drawdown, the number of open positions, realised statistics so far.
- You want to optimise for a measure AmiBroker does not compute.
- You are implementing a rotational or scaling design that needs to see the ranking on each bar before deciding.
You do not need it when:
- The rule is a function of price, volume and time. That is an array, and arrays are what ordinary AFL is for.
- You want equal-weight sizing, percent-of-equity sizing or volatility-based sizing.
SetPositionSize()does all three. - You want to rank candidates.
PositionScoredoes that. - You want a stop.
ApplyStop()does that, and doing it yourself in low-level mode means reimplementingHandleStops()correctly.
Where to go to learn it properly
Section titled “Where to go to learn it properly”This course stops here, deliberately, and it is worth saying why: the mid-level and low-level approaches are a genuinely large subject with their own object model, their own failure modes and their own debugging techniques. Covering them badly would be worse than pointing at them clearly.
The material you need is official and complete:
- Portfolio Backtester Interface Reference — all objects, methods and properties, with the three approaches described in full. This is the primary source and there is no substitute for it.
- Custom metrics — the dedicated page on adding metrics.
Two practical suggestions for working through them.
Read the object model before writing anything. Four objects — Backtester, Signal, Trade, Stats — and only the Backtester object is directly accessible from AFL. Everything else is reached through its methods. Getting that picture straight first saves a great deal of confusion.
Debug with _TRACE, not the debugger. Since GetBacktesterObject() returns an empty object
outside the second phase, and the debugger runs in actionBacktest, tracing to the Log window is
the practical technique. Part 36’s
debugging lesson has the mechanics.
The Custom Backtester Interface gives your code access to the second phase of a portfolio backtest,
where decisions can depend on state the backtest itself generates. There are three documented levels
— high-level Backtest() plus metrics, mid-level signal processing, and low-level full control
described as being for hard-coded programmers only. Enable it with SetOption( "UseCustomBacktestProc", True ) or SetCustomBacktestProc(), and get the object only when Status( "action" ) is
actionPortfolio, or unconditionally in an external procedure file. Custom metrics reach the report,
the summary and the optimisation list, which makes them optimisable — with the caveat that you, not
AmiBroker, are responsible for choosing a defensible CombineMethod for walk-forward. And the right
default is the highest level that does what you need.
Check your understanding
Sources for this lesson
6 verified · checked 2026-08-31
- 01AmiBroker User's Guide — Portfolio Backtester Interface Referenceamibroker.com/guide/a_custombacktest.html2026-08-31
- 02AFL Function Reference — SetCustomBacktestProcamibroker.com/guide/afl/setcustombacktestproc.html2026-08-31
- 03AFL Function Reference — GetBacktesterObjectamibroker.com/guide/afl/getbacktesterobject.html2026-08-31
- 04AFL Function Reference — Status§ actionamibroker.com/guide/afl/status.html2026-08-31
- 05AmiBroker User's Guide — How to use AFL debugger§ GetBacktesterObject outside the second phaseamibroker.com/guide/h_debugger.html2026-08-31
- 06AmiBroker User's Guide — Custom metricsamibroker.com/guide/a_custommetrics.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.