Skip to content
Level 4 · Trading System ResearcherLessonPart 29 · page 6 of 628 min
28Minutes
11AFL functions
6Sources
StandardRequires
AFL functions taught here11

Equity Curve Analysis

The statistics page compresses several years of an account’s life into about forty numbers. The equity curve does not compress it, and that is its value: it shows the order in which things happened, which every summary statistic throws away.

Order matters because you experience it. A 22 per cent drawdown that arrives in the third month and a 22 per cent drawdown that arrives in the eighth year produce the same Max. system % drawdown and completely different decisions. By the end of this lesson you will be able to read the curve for what the table cannot show, compute the one thing the report never gives you, and hold a suspiciously good-looking curve up to the light.

Two places, for two different purposes.

The report’s charts page. Report charts are ordinary AFL formulas living in the Charts tree’s Report Charts folder, each rendered to a bitmap of fixed dimensions and embedded in the report HTML. They are what you look at while reading a report.

The ~~~EQUITY symbol. After a portfolio backtest completes, AmiBroker writes the portfolio equity values into a special symbol called ~~~EQUITY, which you can read from any formula with Foreign( "~~~EQUITY", "C" ). This is what the built-in portfolio equity chart does, and it is what lets you compute things the report does not report.

Walk-forward testing produces two further composite symbols, ~~~ISEQUITY and ~~~OSEQUITY, holding the concatenated in-sample and out-of-sample equity. Those belong to Part 32.

Four questions, in order.

Is the slope roughly constant, and if not, where did it change? A curve that is flat for years and then rises steeply has not demonstrated a persistent effect; it has demonstrated one favourable period. Note the dates of the change and ask what was happening in the market then. If the rise coincides with one sustained trend, you have learned that the rule participates in trends, not that it works.

Where did the money come from? Follow the curve’s biggest single steps back to the trade list. If four trades produced most of the height, the summary statistics are averages over a distribution dominated by four events.

How long are the flat and declining stretches? This is the next section.

Does the curve’s texture match the market’s? A rule trading volatile instruments with meaningful position sizes should produce a curve with visible texture. One that does not is telling you something, and the last section of this lesson is about what.

Flat periods, and the two things they can mean

Section titled “Flat periods, and the two things they can mean”

A flat stretch on an equity curve has two completely different causes, and the curve alone cannot distinguish them:

  • No trades. The rule found nothing to do. Capital was idle. This shows up in Exposure % and in the gap between entries in the trade list.
  • Trades that netted to nothing. The rule traded continuously and the wins and losses cancelled. Capital was at risk the entire time, and every round trip paid costs.

These are not remotely the same proposition. The first is a system waiting; the second is a system paying to stand still. The trade list tells you which one you are looking at, and it is worth checking every time, because the second case is a good candidate for the rule having stopped working in that period.

Time underwater is the thing you live through

Section titled “Time underwater is the thing you live through”

A drawdown ends on the chart when equity makes a new high. It ends for a person somewhere else entirely — when they stop taking the signals.

As the risk metrics lesson established, AmiBroker reports how deep the worst decline was and does not report how long any decline lasted. There is no row for it and no metric string for it. Ulcer Index responds to duration implicitly, by summing squared drawdowns over every bar, but it does not report a duration in units you can act on.

So compute it.

Draw portfolio equity, the running peak that equity has ever reached, and the distance below that peak on every bar — then report the deepest decline and, the number the report does not contain, the longest stretch the account spent below a previous peak.

This is the shape the formula draws. The top pane holds the two curves; the bottom pane holds the third, which is simply the vertical distance between them:

Equity, its running peak, and the distance between them

Underwater %-100
  • Portfolio equity
  • Running peak
The running peak is flat exactly when the account is underwater, and every flat stretch in it is a stretch the lower pane spends below zero. The deepest bar of the lower pane is the maximum drawdown the report quotes; the widest flat run of the upper line is the recovery time it does not. The data in this chart is invented for the illustration. It is not market data and nothing should be inferred from it.

Complete runnable AFL

underwater-curve.afl
// underwater-curve.afl
// Part 29 - Understanding Backtest Results
//
// Draws the portfolio equity of the LAST completed portfolio backtest, the
// running peak of that equity, and the underwater curve - how far below the
// previous peak the account stood on every single bar - and reports the
// deepest decline and the longest stretch spent below a previous peak.
//
// The backtest report tells you how deep the worst drawdown was. It does not
// report how long any drawdown lasted. This formula supplies the number the
// report leaves out, because duration is the part a person actually lives
// through.
//
// ------------------------------------------------------------------------
// ASSUMPTIONS
// ------------------------------------------------------------------------
// - Run a PORTFOLIO backtest first. AmiBroker writes portfolio equity into
// the special ~~~EQUITY symbol only after a backtest completes, so this
// chart shows the last run, not the current one.
// - Apply it as a chart pane. Do NOT paste it inside a backtest formula:
// during a run, Foreign( "~~~EQUITY", "C" ) returns the PREVIOUS
// backtest's equity, because the Analysis window keeps a private copy
// until the run has finished.
// - Depth is measured bar by bar on the equity CLOSE, which is the basis the
// portfolio backtester itself uses. Intra-bar excursions are not in it.
// - Duration is counted in BARS. On daily data one bar is one trading day,
// so roughly 21 bars is a month and roughly 252 bars is a year. Those are
// conversions you are doing, not figures AmiBroker reports.
// - The chart covers whatever range the chart is showing. To see the whole
// test, zoom out to all bars.
// ------------------------------------------------------------------------
_SECTION_BEGIN( "Underwater curve" );
// Highest() is a running maximum measured from the FIRST bar in the array, so
// this formula must see every bar. QuickAFL would otherwise hand the chart a
// truncated array and the "previous peak" would start part-way through the
// test. sbrAll (-2) turns QuickAFL off for this pane.
SetBarsRequired( sbrAll, sbrAll );
SetChartOptions( 0, chartShowDates | chartWrapTitle );
EquitySymbol = "~~~EQUITY";
// Nz() turns the Nulls outside the tested range into zeros so that a missing
// backtest fails visibly rather than propagating Null through everything.
PortEquity = Nz( Foreign( EquitySymbol, "C" ) );
// Highest() is a running maximum from the first bar in the database, which is
// exactly the "previous peak" an underwater plot needs.
PeakEquity = Highest( PortEquity );
// Zero at every new peak, negative everywhere else.
Underwater = IIf( PeakEquity > 0, 100 * ( PortEquity - PeakEquity ) / PeakEquity, 0 );
// A bar is at a peak when equity equals the running maximum, so the comparison
// has to be >=, not >.
AtPeak = PortEquity >= PeakEquity;
BarsBelowPeak = BarsSince( AtPeak );
DeepestPercent = LastValue( Lowest( Underwater ) );
LongestBars = LastValue( Highest( BarsBelowPeak ) );
CurrentPercent = LastValue( Underwater );
CurrentBars = LastValue( BarsBelowPeak );
Plot( PortEquity, "Portfolio equity", colorBlue, styleLine | styleThick );
Plot( PeakEquity, "Previous peak", colorLightGrey, styleLine | styleDashed );
Plot( Underwater, "Underwater %", colorRed, styleArea | styleOwnScale | styleNoLabel );
if ( LastValue( PeakEquity ) <= 0 )
{
Title = "No portfolio equity found in " + EquitySymbol +
". Run a portfolio backtest first, then refresh this chart.";
}
else
{
Title = StrFormat(
"Portfolio equity %g\n" +
"Deepest decline from a previous peak %.2f%%\n" +
"Longest stretch below a previous peak %.0f bars\n" +
"Right now %.2f%% below the peak, %.0f bars below it",
LastValue( PortEquity ), DeepestPercent, LongestBars,
CurrentPercent, CurrentBars );
}
_SECTION_END();

Download underwater-curve.afl86 lines

The formula has four logical sections.

It reads the equity of the last completed backtest from ~~~EQUITY through Foreign, wrapping it in Nz so that bars outside the tested range become zero rather than Null. A missing backtest then fails visibly instead of propagating Null through every later calculation.

It builds the running peak with Highest, which returns the highest value seen from the first bar in the array up to and including each bar. That is exactly the definition of “the previous peak” that an underwater plot needs.

It computes depth and duration. Depth is the percentage distance from the peak, guarded so a zero peak cannot cause a division by zero. Duration comes from BarsSince( AtPeak ), where a bar counts as being at a peak when equity equals the running maximum — which is why the comparison is >= and not >. The running maximum of that bar count is the longest stretch spent below a previous peak.

It reports, plotting equity and the peak on the main scale with the underwater series as a filled area on its own scale, and putting the four summary figures in the title.

The SetBarsRequired( sbrAll, sbrAll ) call at the top matters more than it looks. Under QuickAFL, AmiBroker may hand a chart formula a truncated array to save time. Highest would then compute a running maximum starting part-way through the test, and every underwater figure would be wrong — quietly, and in the flattering direction.

Function What it does here
Foreign( ticker, "C" ) Reads the close series of another symbol — here, portfolio equity
Nz( array ) Replaces Null with zero, so a missing backtest fails loudly
Highest( array ) Running maximum from the first bar of the array: the previous peak
Lowest( array ) Running minimum: used to find the deepest point of the underwater series
BarsSince( condition ) Bars elapsed since the condition was last true: the time underwater
LastValue( array ) The value at the final bar, used to pull the summary numbers out
SetBarsRequired( sbrAll, sbrAll ) Turns QuickAFL off so the running maximum sees every bar

Applied as a chart pane after a portfolio backtest, you should see the equity line, a dashed step-shaped line above it that only ever rises, and a red area hanging below zero on its own scale. The title should read something like:

Portfolio equity 127520
Deepest decline from a previous peak -22.40%
Longest stretch below a previous peak 318 bars
Right now -1.85% below the peak, 12 bars below it

The dashed peak line should be flat wherever the red area is non-zero, and should step up only at the moments the red area touches zero. If it does anything else, something is wrong.

Three checks that would catch this being wrong:

  1. The deepest decline should match the report. Compare the title’s figure with Max. system % drawdown on the statistics page. They are computed on the same basis — close prices, portfolio equity — so they should agree closely. A large discrepancy usually means QuickAFL truncated the array or the chart is not showing all bars.
  2. The red area should be exactly zero at every new high, and nowhere else. Zoom in on a peak and confirm.
  3. Run it on a symbol other than ~~~EQUITY. The output should be identical, because Foreign reads the equity symbol regardless of which symbol the chart is on. If it changes, you have edited the wrong line.
Symptom Cause
Title says no portfolio equity was found No portfolio backtest has been run in this database, or it was an Individual rather than a Portfolio backtest
Numbers do not match the report The chart is not showing all bars, or SetBarsRequired was removed and QuickAFL truncated the array
Equity looks like the previous run The formula was pasted into a backtest formula rather than used as a chart; ~~~EQUITY is only updated after a run completes
The underwater area is invisible It is plotted on its own scale; if the drawdowns are tiny the area is a thin line at the bottom of the pane, which is itself informative
Everything is zero Nz converted a Null equity series to zeros, which is the intended visible failure

Convert the bar counts into calendar time and print both. On daily data, BarsSince counts trading days, so roughly 21 bars is a month; a more honest version reads the actual dates with DateTime() and reports elapsed calendar days, which is what a person experiences. A second extension is to report the number of separate stretches spent more than, say, 10 per cent below a peak — because three separate 15 per cent drawdowns and one 15 per cent drawdown are different experiences that share a Max. system % drawdown.

Take the illustrative report from the trade statistics lesson: 214 trades, Net Profit 27,520, Avg. Profit/Loss 128.60, Payoff Ratio 1.903, Profit Factor 1.355. Suppose the largest single win was 9,400.

Largest win as a share of Net Profit = 9,400 / 27,520 = 34.2%

Now recompute the report without that one trade:

Figure With the trade Without it
Trades 214 213
Net Profit 27,520 18,120
Avg. Profit/Loss 128.60 85.07
Payoff Ratio 1.903 1.753
Profit Factor 1.355 1.234

One trade out of 214 supplied a third of the profit, and removing it cuts the expectancy by a third. That is not a reason to remove it — deleting your best result is not a method, and outliers are a genuine feature of trend-following returns rather than a contaminant. It is a reason to ask a specific question: is this outlier a repeatable property of the rule, or an accident of one instrument on one date?

The trade list answers the first half. Look the trade up. Was it a merger announcement, a one-off gap, a symbol that no longer exists? If the rule’s logic gives no reason to expect such trades to recur, the summary statistics are describing a sample the future is unlikely to resemble. Part 33 turns this into a systematic procedure by resampling the trade sequence rather than deleting single trades by hand.

Version 6.00 added detailed buy-and-hold benchmark statistics to the backtest report automatically. The exact row labels of that block are not spelled out in the User’s Guide, so read them off your own report rather than trusting a list from anywhere — including this page. The old backtester’s labels, which you will meet in older material, were Buy-and-hold profit, Buy-and-hold % return, Annual B&H % return, Max. B&H drawdown, Max. B&H % drawdown and System to buy-and-hold index.

A fair comparison requires five things, and most published comparisons are missing at least two:

  1. The same period. Including start and end dates, because both figures are annualised over calendar days.
  2. Costs on both sides. Buy and hold pays a spread and a commission twice, not never. The difference is small over twenty years and it should still be there.
  3. A stated benchmark. “Buy and hold” of what? The same universe, equally weighted and rebalanced how often? An index? The index including dividends, or excluding them? These produce materially different numbers and the choice must be recorded.
  4. The same survivorship treatment. If your universe excludes delisted companies, so does your benchmark, and both are flattered by the same amount.
  5. An answer to the exposure question. A system at 30 per cent exposure that matches a fully-invested benchmark is doing something different, not obviously something better, and the other 70 per cent of the capital has to be doing something in real life.

Why a smooth curve is a warning as often as a comfort

Section titled “Why a smooth curve is a warning as often as a comfort”

AmiBroker reports three metrics that reward a smooth equity line:

Metric Definition Direction
Standard Error Choppiness of the equity line, measured about its linear regression Lower is better
Risk-Reward Ratio Slope of the equity line, meaning expected annual return, divided by its standard error Higher is better
K-Ratio Detects inconsistency in returns; the regression slope scaled by the standard error and the bar count; should be 1.0 or more Higher is better

Now the uncomfortable part. Markets are not smooth. A rule trading volatile instruments with meaningful position sizes produces a curve with visible texture, because the underlying returns have visible texture. When a backtest equity curve is much smoother than the thing it is trading, something absorbed the volatility, and the candidates are mostly defects:

  • Look-ahead. A rule that can see the future does not experience the future’s uncertainty. Part 30’s first lesson is entirely about this.
  • Survivorship. A universe containing only companies that survived has had its worst outcomes deleted.
  • A fill that was not available. Exiting at the exact stop price through a gap, or buying at a close that was only known after the close, removes precisely the events that create roughness.
  • Costs set to zero or near it. Costs are the one component that is certain, and omitting them smooths every curve.
  • Position sizes so small the curve is mostly cash. Check Exposure %. A curve that is 90 per cent deposit account is smooth for reasons that have nothing to do with the rule.
  • An in-sample fit. A parameter set chosen because it produced a smooth curve on this data will produce a smooth curve on this data. Parts 31 and 32 exist for this.
  • Repeated closes in the data. A symbol whose data source fills non-trading days by repeating the previous close contributes stretches of exactly zero change. That is a data defect being read as stability.

The hazard compounds when smoothness becomes an optimization target. Optimise on K-Ratio, Risk-Reward Ratio or Standard Error and the search will find the parameter set whose in-sample curve is straightest — which, among a few thousand candidates, is very often the most over-fitted one rather than the most robust one.

The checks a suspiciously smooth curve deserves

Section titled “The checks a suspiciously smooth curve deserves”
  1. Count the trades. Smooth curves on few trades are almost always an artefact.
  2. Check Exposure %. If it is small, most of the smoothness is cash.
  3. Compare Max. trade % drawdown with Max. system % drawdown. If individual trades went badly wrong and the system did not, find out what absorbed it, and confirm that the thing that absorbed it was diversification rather than an assumption.
  4. Sample ten trades from the list. Is every fill price inside its bar’s range? Is every exit at a price that was knowable at the time?
  5. Re-run on a different date range and a different universe. A curve that is smooth on both is more interesting than one that is smooth on one.
  6. Set the costs to a pessimistic figure and run it again. If the smoothness survives doubling the cost assumption, it is more likely to be real.

The equity curve carries the ordering information that every summary statistic discards. Flat stretches have two causes that only the trade list can separate, and the length of the declining stretches is the number the report omits entirely — which is why this lesson built a formula that computes it from the ~~~EQUITY symbol. A single outlier trade can supply a third of the profit, and finding out whether it is a property of the rule or an accident of one date is a question the trade list answers. Buy-and-hold comparisons need the same period, the same costs, a stated benchmark, the same survivorship treatment and an answer to the exposure question. And a curve much smoother than the market it trades is a prompt to look for the assumption that made it so.

That closes Part 29. You can now read the report in AmiBroker’s own vocabulary, know which numbers are ratios of what, and know which popular statistics are not in it at all. Part 30 takes the next step and catalogues the ways a backtest can be wrong long before its report is printed.

Check your understanding

Question 1. You add Foreign("~~~EQUITY", "C") to your backtest formula so you can size positions from portfolio equity. What actually happens?
Show the answer and why

Answer: It returns the previous backtest’s equity, because ~~~EQUITY is only updated after a run completes

The Analysis window keeps a private copy of portfolio equity during a run and copies it to ~~~EQUITY only when the run finishes. A formula reading it mid-backtest therefore reads the previous run’s numbers, which is worse than failing because it looks like it worked. Reading current equity during a run requires the custom backtester interface.

Question 2. An equity curve is flat for eighteen months. Which report or list actually distinguishes the two possible causes?
Show the answer and why

Answer: The trade list, which shows whether trades were taken during that period at all

A flat stretch can mean the rule found nothing to do, or that it traded continuously and the wins and losses cancelled while paying costs throughout. The curve looks the same either way. Only the trade list shows which, and the second case is far more concerning because capital was at risk and costs were being paid for no result.

Question 3. Which of these could produce a backtest equity curve that is much smoother than the instruments it trades? Select all that apply.
Show the answer and why

Answer: Exiting at the exact stop price on every stop, including through gaps, A universe built from current index membership, A very low Exposure %, so most of the account is cash

Unavailable fills remove the worst individual outcomes, survivorship removes the companies that failed, and a mostly-cash account is smooth for reasons unrelated to the rule. A large number of trades over a long period is the opposite: it makes a smooth curve more credible, not less, because there was more opportunity for roughness to appear.

Question 4. Why does the underwater formula call SetBarsRequired( sbrAll, sbrAll )?
Show the answer and why

Answer: Because Highest() is a running maximum from the first bar of the array, and QuickAFL may hand the formula a truncated array

QuickAFL can pass a chart formula only the bars it thinks are needed. Highest() would then start its running maximum part-way through the test, so the "previous peak" would be wrong and every underwater figure would be understated. sbrAll turns QuickAFL off for the pane. It does not change what is displayed on screen, only what the formula is given to compute with.

Sources for this lesson

6 verified · checked 2026-08-31

  1. 01AmiBroker User's Guide — Equity function and portfolio equity§ Portfolio Equity special symbolamibroker.com/guide/a_equity.html2026-08-31
  2. 02AmiBroker User's Guide — System test report window§ New backtester reportamibroker.com/guide/w_report.html2026-08-31
  3. 03AmiBroker User's Guide — System test settings window§ Report tabamibroker.com/guide/w_settings.html2026-08-31
  4. 04AmiBroker User's Guide — Multithreading and performance§ Accessing the ~~~Equity symbolamibroker.com/guide/h_multithreading.html2026-08-31
  5. 05AmiBroker User's Guide — What's new§ Highlights of version 6.00amibroker.com/guide/whatsnew.html2026-08-31
  6. 06AFL Function Reference — SetBarsRequiredamibroker.com/guide/afl/setbarsrequired.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.