Skip to content
Level 2 · AmiBroker AnalystLessonPart 12 · page 8 of 926 min
26Minutes
24AFL functions
10Sources
StandardRequires
AFL functions taught here24

Exporting Results and Building a Daily Workflow

A screen that lives inside AmiBroker’s result list is a screen you run when you happen to remember. A screen with a route out — into a file, a watch list, a journal — is the beginning of a routine.

This lesson is about that route, and about a discipline that matters more than any of the mechanics: keeping a record of what you actually ran, so that a candidate list you saved three months ago can still be explained.

The menu is not where people look for it.

To export data to a CSV file or an HTML file, use the File → Export HTML/CSV menu (from the main window). Note that these menu items appear only if you have the New Analysis window active.

That last sentence is the whole difficulty. The command lives on the main window’s File menu, and it only appears while the Analysis window is the focused window. Click into the Analysis window, then open File, and it is there.

File → Import HTML… reads a previously exported HTML file back in, which is occasionally useful for comparing today’s list against last week’s.

Designing an exploration for export rather than for reading

Section titled “Designing an exploration for export rather than for reading”

An exploration you read and an exploration you export want different things. On screen you want readability. In a file you want predictability — fixed column positions, unambiguous dates, and no formatting that a parser has to undo.

Three settings do most of the work.

SetOption( "NoDefaultColumns", True ) removes the automatic Ticker and Date/Time columns. That sounds like a loss and is a gain: the automatic columns’ presence depends on a setting somebody could change, so leaving them in means your column numbers are not yours to control. Turn them off, then add exactly the columns you want, in the order you want them.

formatDateTimeISO is the format constant to use for any exported date. NumToStr’s documentation introduces it in 6.20: it produces YYYY-MM-DD HH:MM:SS, an international format, rather than formatDateTime, which follows Windows regional settings. A file whose date column means something different on a colleague’s machine is a file you will eventually misread.

SetSortColumns( n ) takes one-based column numbers; a negative number sorts descending. For an export that will be appended to over time, sort ascending by date so the accumulated file stays chronological.

Complete runnable AFL

signal-export.afl
// signal-export.afl
// Part 12 - Exporting Results and Building a Daily Workflow
//
// An exploration whose output is not meant to be looked at. Every column is
// chosen so that the exported CSV can be read by another program without any
// cleaning step: no locale-dependent dates, no thousands separators in the
// wrong place, no automatic columns whose position depends on a setting.
//
// This is the shape of the official "signal file" recipe, extended with the
// two columns a downstream tool almost always needs: the symbol and a
// reference price.
//
// Assumptions:
// - Daily bars.
// - The consumer of this file treats it as a record of what the rules said
// on a given bar, not as an instruction. Nothing here models slippage,
// costs, position size or whether the price was reachable.
//
// TO EXPORT: run it with Explore, then use the MAIN window's
// File -> Export HTML/CSV menu. That menu entry only appears while the
// Analysis window is the active window, which is why people cannot find it.
FastPeriod = 20;
SlowPeriod = 50;
Fast = MA( Close, FastPeriod );
Slow = MA( Close, SlowPeriod );
Buy = Cross( Fast, Slow );
Sell = Cross( Slow, Fast );
// The two automatic columns are switched off so that column positions are
// fixed and predictable. Everything the file needs is then added explicitly,
// in the order the consumer expects. Remember that this renumbering also
// shifts every column number used by SetSortColumns and AddSummaryRows.
SetOption( "NoDefaultColumns", True );
Filter = Buy OR Sell;
AddTextColumn( Name(), "Symbol", 16 );
AddColumn( DateTime(), "Date", formatDateTimeISO );
// formatChar prints the single ASCII character whose code is in the array:
// 66 is "B", 83 is "S". One character is easier for a downstream parser than
// a word whose spelling might change.
AddColumn( IIf( Buy, 66, 83 ), "Side", formatChar );
AddColumn( Close, "RefPrice", 1.4 );
AddColumn( Volume, "Volume", 1.0 );
// Oldest first, so that appending a later export to the same file keeps the
// rows in chronological order. Column 2 is the date because the automatic
// columns are gone.
SetSortColumns( 2 );

Download signal-export.afl54 lines

Notice the Side column:

Fragment — not a complete formula

// 66 is "B", 83 is "S". formatChar prints the ASCII character
// whose code is in the array.
AddColumn( IIf( Buy, 66, 83 ), "Side", formatChar );

AddColumn() takes a numeric array, so text cannot be put in it directly. formatChar prints the single character whose ASCII code the array holds, which gives a one-character side flag — easier for a downstream parser than a word, and impossible to get wrong through a spelling change.

Sometimes you do not want a file — you want the candidates to appear in AmiBroker’s own symbol tree so you can page through their charts.

CategoryAddSymbol( symbol, category, number ) adds a symbol to a category. For categoryWatchlist the number is the watch list number, and the documentation notes that watch lists — unlike markets, groups and sectors — allow a symbol to belong to more than one. An empty symbol string means the current symbol.

Fragment — not a complete formula

// Inside a Scan or Exploration, on the last bar of the range only.
CandidateToday = Status( "lastbarinrange" ) AND Liquid AND UpTrend AND Setup;
if( LastValue( CandidateToday ) )
CategoryAddSymbol( "", categoryWatchlist, 31 );

Two cautions, both practical rather than exotic.

Adding is not replacing. Run this daily and watch list 31 accumulates. Clear it first — right-click the watch list in the symbol tree — or accept that it is a running log rather than today’s list.

Pick an empty watch list number and write it down. Watch lists are stored as .TLS text files in the Watchlists folder of the database, with index.txt defining the order, so a mistake here is recoverable — but it is far easier not to overwrite something you care about.

Two mechanisms exist, and they are for different sizes of job.

#pragma sequence — quick chaining inside one Analysis window

Section titled “#pragma sequence — quick chaining inside one Analysis window”

AmiBroker 6.40 added a Run Sequence toolbar button in the Analysis window, driven by a preprocessor directive:

Fragment — not a complete formula

#pragma sequence( scan, explore )
if( Status( "action" ) == actionScan )
{
AddToComposite( /* ... */ );
_exit(); // nothing else to do in the scan pass
}
Filter = /* ... */; // the exploration pass, which can use
AddColumn( /* ... */ ); // whatever the scan pass just built

The documented use case is exactly this shape: a first pass that builds something — composites, static variables — and a second pass that consumes it, both from one button. Status( "action" ) is what lets one file behave differently in each pass, and _exit() ends the pass early once its work is done.

The official documentation is explicit that this is not a replacement for batches. It is for “quick hacking” — chaining two or three steps you are actively developing.

Introduced in version 6.20, the Batch window automates sequences that previously needed OLE programming. It works on .APX files — Analysis Project files — and that is the key idea: an .APX is self-contained and holds the formula, all the options and settings, and the apply-to and range selections.

Documented batch step types include:

  • load an .APX project
  • optionally set the current symbol
  • run a Scan, Exploration, Backtest, Optimization or Walk-forward
  • export results to CSV or HTML
  • notify with a sound file or spoken text
  • write text to a log file
  • load or save the database
  • execute an external program and wait for it
  • run a vendor-specific data-plugin command
  • import quotation data from ASCII files

The part that matters most: a record of what you ran

Section titled “The part that matters most: a record of what you ran”

Here is the situation this section exists to prevent. You find a folder of exported candidate lists from six months ago. You cannot reproduce them. Was the liquidity threshold a million or five? Which watch list? Which date range? Was that before or after you changed the trend filter?

The formula was in the file. The settings were not — they were in a dialog.

Complete runnable AFL

screen-log.afl
// screen-log.afl
// Part 12 - Exporting Results and Building a Daily Workflow
//
// A screen that keeps a journal of itself. Every run appends one line to a CSV
// file recording what was actually run: when, which formula, which universe
// setting, which watch list, which range, and the thresholds in force. Six
// months later that journal is the only thing that can tell you whether a
// candidate list you saved came from the rules you think it did.
//
// The journal records the INPUTS of the run. It cannot record how many rows
// came back, because it is written before the run finishes; the exported
// result file is the record of the output. Keeping the two side by side, named
// with the same date, is the whole discipline.
//
// Assumptions:
// - Daily bars.
// - The folder in LogFolder already exists. AmiBroker will not create it,
// and fopen() simply returns 0 if it cannot open the file.
// - Backslashes in AFL string literals must be doubled.
LogFolder = "C:\\AmiBrokerCourse\\";
LogPath = LogFolder + "screen-journal.csv";
MinTurnover = 1000000;
LiquidityPeriod = 50;
TrendPeriod = 200;
SetupPeriod = 10;
// Status("stocknum") == 0 is a recognised special form: AmiBroker detects this
// exact statement, runs the first symbol on a single thread and waits for it
// before starting the others. That makes it the sanctioned place for
// once-per-run work such as this. It must not be moved into an #include file,
// or the detection - and the single-threaded first pass - does not happen.
if( Status( "stocknum" ) == 0 )
{
// shared = True asks fopen to open the file in share-aware mode and to
// retry on a sharing violation. In a multi-threaded Analysis run that is
// not optional politeness; without it two threads can corrupt the file.
LogHandle = fopen( LogPath, "a", True );
if( LogHandle )
{
JournalLine =
Now( 0 ) + "," +
GetFormulaPath() + "," +
"applyto=" + NumToStr( GetOption( "ApplyTo" ), 1.0 ) + "," +
"watchlist=" + NumToStr( GetOption( "FilterIncludeWatchlist" ), 1.0 ) + "," +
"rangefrom=" + NumToStr( Status( "rangefromdate" ), 1.0 ) + "," +
"rangeto=" + NumToStr( Status( "rangetodate" ), 1.0 ) + "," +
"minturnover=" + NumToStr( MinTurnover, 1.0 ) + "," +
"trendperiod=" + NumToStr( TrendPeriod, 1.0 ) + "\n";
fputs( JournalLine, LogHandle );
fclose( LogHandle );
}
}
AvgTurnover = MA( Close * Volume, LiquidityPeriod );
Trend = MA( Close, TrendPeriod );
Liquid = AvgTurnover > MinTurnover;
UpTrend = Close > Trend;
Setup = Close >= HHV( Close, SetupPeriod );
Filter = Status( "lastbarinrange" ) AND IsTrue( Liquid AND UpTrend AND Setup );
AddTextColumn( FullName(), "Name", 34 );
AddColumn( Close, "Close", 1.2 );
AddColumn( AvgTurnover, "Turnover 50d", 1.0 );
AddColumn( 100 * ( Close - Trend ) / Trend, "% from MA200", 1.1 );
AddColumn( DateTime(), "As of", formatDateTimeISO );
AddSummaryRows( 16, 1.0, 4 );

Download screen-log.afl73 lines

Every run appends one line to a CSV recording when it ran, which formula file, the Apply To setting, the watch list filter, the date range and the thresholds in force.

The retrieval functions are all documented:

Call What it records
Now( 0 ) when the run happened
GetFormulaPath() the full path of the formula being executed
GetOption( "ApplyTo" ) 0 = all symbols, 1 = current symbol, 2 = filter
GetOption( "FilterIncludeWatchlist" ) −1 if no watch list filter, otherwise its index
Status( "rangefromdate" ) / Status( "rangetodate" ) the range actually used

The thresholds are written from the formula’s own constants, so the journal cannot disagree with the run.

Why it is wrapped in Status( "stocknum" ) == 0

Section titled “Why it is wrapped in Status( "stocknum" ) == 0”

An Analysis run executes the formula once per symbol, in parallel across threads. Without a guard, a journal line would be written once per symbol.

if( Status( "stocknum" ) == 0 ) is a recognised special form. AmiBroker detects this exact statement, runs the first symbol on a single thread and waits for it to finish before starting the others — which makes it the sanctioned place for once-per-run setup work.

Note what the journal deliberately does not record: how many rows came back. It is written before the run finishes. The exported result file is the record of the output; the journal is the record of the inputs. Naming both with the same date is the whole discipline.

A repeatable end-of-day routine

  1. Update dataAmiQuote or your feed. Two minutes, and it is the step whose failure is least visible — check the last bar date on a symbol you know.
  2. Run the screenOne Analysis window, one .APX, Run. The journal line is written automatically.
  3. Export the result listFile -> Export HTML/CSV from the MAIN window, with the Analysis window active. Name it with today's date.
  4. Look at the chartsPage through the candidates. This is the only step that cannot be automated, and it is the one that catches data errors.
  5. Write down what you decidedIncluding "nothing". A routine that only records action days cannot tell you how selective you actually were.

Two observations about this routine that are worth more than the routine itself.

The chart review is not optional and not automatable. A screen cannot tell you that a symbol gapped 40% on a takeover, that the “breakout” is a bad print, or that the instrument is a delisted shell. Ten seconds a chart on twenty candidates is three minutes, and it is where data problems get caught.

Recording the no-action days is what makes the record honest. A file that contains only the days you did something cannot tell you how often the screen fired, and a screen whose base rate you do not know is a screen you cannot evaluate. This is the same argument the reality-check lessons make about base rates, applied to your own behaviour.

Exporting is a main-window File menu that only appears while the Analysis window is active. An exploration built for export wants NoDefaultColumns on, ISO dates, fixed column positions, and column numbers that you own — remembering that turning the default columns off renumbers everything SetSortColumns() and AddSummaryRows() refer to. CategoryAddSymbol() puts candidates in a watch list; it adds rather than replaces. #pragma sequence chains a couple of steps behind one button; the Batch window automates real sequences via .APX project files. And the piece nobody builds until they have been burned: a journal line per run, written once under Status( "stocknum" ) == 0 with fopen( …, "a", True ), recording the settings that live in dialogs rather than in the formula.

Check your understanding

Question 1. You cannot find File -> Export HTML/CSV. What is the documented reason?
Show the answer and why

Answer: It is on the main window's File menu and only appears while the New Analysis window is the active window

The menu lives on the main window but is conditional on the Analysis window having focus. Click into the Analysis window first, then open the File menu.

Question 2. A formula uses SetSortColumns( 2 ) and sorts on the wrong column. Which explanations fit? Select all that apply.
Show the answer and why

Answer: SetOption( "NoDefaultColumns", True ) was added or removed, shifting every column position by two, A column was inserted earlier in the formula than the one being sorted on, SetSortColumns was called twice and only the last call takes effect

Column numbers are documented as ONE-based. The default Ticker and Date/Time columns occupy two positions when present, so toggling NoDefaultColumns shifts everything; inserting a column does the same; and each SetSortColumns call overwrites the previous one.

Question 3. Why is the journal block wrapped in if( Status( "stocknum" ) == 0 ) and why must that statement stay in the main formula file?
if( Status( "stocknum" ) == 0 )
{
    LogHandle = fopen( LogPath, "a", True );
    ...
}
Show the answer and why

Answer: AmiBroker detects this exact statement, runs the first symbol single-threaded and waits for it — so it is the sanctioned place for once-per-run work. Hiding it in an #include defeats the detection

The detection is of the statement itself, which is why moving it into an include file breaks the single-threaded first pass. Without the guard the journal line would be written once per symbol; without the single-threaded pass, concurrently.

Question 4. What does the shared argument of fopen( path, "a", True ) do?
Show the answer and why

Answer: It opens the file in share-aware mode and, on a sharing violation, waits 250 ms and retries up to four times before failing

The documented behaviour, added in 5.80. With sharing False — the default — no checking occurs, which the documentation says may cause data corruption when several threads or processes write at once. AmiBroker never creates the folder.

Question 5. Which statements about #pragma sequence and the Batch window are accurate? Select all that apply.
Show the answer and why

Answer: #pragma sequence adds a "Run Sequence" button to the Analysis window and runs the listed actions in order, The Batch window operates on .APX Analysis Project files, which hold the formula together with all settings and the apply-to and range selections, A batch step can export results, run an external program and wait, or import ASCII quotation data

The documentation presents sequences as augmenting batches for "quick hacking" within one Analysis window, explicitly not as a replacement. The Batch window is the full automation facility and works from self-contained .APX projects.

Sources for this lesson

10 verified · checked 2026-08-31

  1. 01AmiBroker User's Guide — New Analysis window§ Exporting and Importing the Result List; Running a sequence of actionsamibroker.com/guide/h_newanalysis.html2026-08-31
  2. 02AmiBroker User's Guide — Using Batch windowamibroker.com/guide/h_batch.html2026-08-31
  3. 03AmiBroker User's Guide — Working with watch listsamibroker.com/guide/h_watchlist.html2026-08-31
  4. 04AFL Function Reference — "#pragma"amibroker.com/guide/afl/_pragma.html2026-08-31
  5. 05AFL Function Reference — fopen§ shared parameter (new in 5.80)amibroker.com/guide/afl/fopen.html2026-08-31
  6. 06AFL Function Reference — GetOptionamibroker.com/guide/afl/getoption.html2026-08-31
  7. 07AFL Function Reference — SetSortColumnsamibroker.com/guide/afl/setsortcolumns.html2026-08-31
  8. 08AFL Function Reference — AddSummaryRowsamibroker.com/guide/afl/addsummaryrows.html2026-08-31
  9. 09AFL Function Reference — CategoryAddSymbolamibroker.com/guide/afl/categoryaddsymbol.html2026-08-31
  10. 10AmiBroker User's Guide — Multithreading§ Status("stocknum") == 0amibroker.com/guide/h_multithreading.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.