From Candidate to Decision
An exploration finishes and eleven rows appear. Everything up to this point was arithmetic applied uniformly; everything after it involves a person who is tired, who has an opinion about one of the tickers, and who would quite like something to happen tonight. This is where a research process either holds or quietly stops being one.
By the end of this lesson you should be able to treat a candidate list as a question rather than an instruction, run a chart review that takes a fixed time and produces a written outcome, assess risk before deciding size rather than after, state exactly where the automated part of your process ends, and explain — with reasons rather than assertion — why this course stops at the human decision and does not teach automated live order execution.
The candidate list is a question
Section titled “The candidate list is a question”A candidate list says: these instruments met the conditions you defined, ranked in the order you specified, with these numbers attached. It does not say that any of them should be traded. The distinction sounds pedantic until you notice how the list behaves psychologically. A list of eleven rows, produced by a formula, sorted, formatted and dated, carries an authority that its contents have not earned. Every row is the same statement — “conditions met” — and the ranking is an ordering of attention, not a measure of merit.
Three properties make a candidate list usable:
It is short. Short enough that the review budget from Lesson 1 covers all of it. If the list routinely exceeds the budget, the fix is upstream in the screen or the rank, not in reviewing faster.
It carries the numbers, not just the tickers. This is why the screen is an Exploration and not a Scan. The stop level, the stop distance as a percentage of price, the planned size, the participation and the binding constraint all need to be visible before the chart is opened, or the review will silently become a search for reasons.
It is exported before it is reviewed. The list as generated, not the list as edited. This single habit is what makes Lesson 4’s measurement possible: without the original list you cannot later tell the difference between a signal you rejected and a signal that never fired.
A chart review protocol
Section titled “A chart review protocol”The purpose of chart review is narrow: to catch things the formula cannot see. It is not a second opinion on the strategy, and it is not an opportunity to find a better trade.
Give it a fixed order, a fixed set of questions and a hard time cap — two to three minutes per candidate — and require a written outcome for every row. Five checks are enough:
- Is the recent price history intelligible? Not “does it look good”. Does it look like trading? A chart whose last three months consist of a gap, a flat line and another gap has passed every numeric filter and should not be traded.
- Where is the signal relative to structure? The trigger fired at a level. Is that level in open space, or immediately beneath an obvious multi-year high, or in the middle of a range it has crossed eleven times? Part 5’s material is what you are applying here.
- Is the stop level survivable? Look at where the stop sits on the chart, not at the number. A stop inside the last three bars’ noise is a stop that will be hit by noise.
- What am I already carrying? Is this the fourth position in the same sector, or a symbol whose price has moved with two of your open positions for the last year? Concentration is a portfolio property and no per-symbol formula can see it.
- Is there something on the calendar? End-of-day OHLCV carries no corporate actions, no earnings dates, no index rebalances. If your process does not include a calendar check, it has a blind spot you should at least name.
The outcome of the review is one of three words — take, skip, defer — plus a short reason. Not a paragraph. The reason exists to be counted later, so it wants to be a category: structure, stop too wide, concentration, event, data, size not viable.
Risk assessment before entry
Section titled “Risk assessment before entry”Risk assessment comes before sizing, and the order is not negotiable, because the size is derived from the risk and not the other way round.
Stop level first. A volatility-based stop — some multiple of ATR() below the signal close —
has the property that it adapts to the instrument rather than to your preferences. Whatever
method you use, the stop is a price, decided before entry, written into the journal. Part 34
covers the alternatives properly.
Then the risk budget. A fixed fraction of account equity per trade. This is where the arithmetic lives.
Then the caps. The size implied by the risk budget is a maximum, not a target, and at least two other constraints can bind before it: a cap on position value as a fraction of equity, and a cap on participation as a fraction of the instrument’s average daily turnover.
Here is the arithmetic with clearly labelled illustrative figures. These are not results, and they are not from any market — they are chosen to make the caps bind so you can see them work.
| Quantity | Value | Where it came from |
|---|---|---|
| Account equity | 100,000 | Policy |
| Risk budget per trade | 0.5% = 500 | Policy |
| Signal close | 42.00 | The signal bar |
| ATR(20) | 1.05 | Computed |
| Stop distance, 2.5 × ATR | 2.63 | Policy × computed |
| Stop level | 39.37 | Close − stop distance |
| Shares implied by the risk budget | 500 / 2.63 = 190 | Risk arithmetic |
| Position value at 190 shares | 7,980 | Shares × close |
| Position value cap, 10% of equity | 10,000 → 238 shares | Policy |
| Average daily turnover | 600,000 | Computed |
| Participation cap, 1% of turnover | 6,000 → 142 shares | Policy |
| Planned size | 142 shares | The smallest of the three |
| Risk actually taken | 142 × 2.63 = 373, or 0.37% of equity | Consequence |
The last two rows are the point of the whole table. The participation cap binds, the position is smaller than the risk budget would allow, and the trade therefore risks less than the policy maximum. That is the correct outcome. The instinct to “use up” the risk budget by widening the stop or ignoring the cap is the instinct that turns a sizing rule into a decoration.
There is one more assessment that no formula performs: whether you can actually get out. The participation cap is computed from a normal day’s turnover. The day you want to exit may not be a normal day, and the instruments whose turnover collapses under stress are disproportionately the smaller ones your screen finds attractive. This is not a number in the sheet; it is a reason to keep the cap tighter than the arithmetic strictly requires.
The boundary: alert, human review, decision
Section titled “The boundary: alert, human review, decision”Where the machine stops
- ExplorationRules applied to today’s data
- AlertAlertIf, e-mail, sound - notification only
- Human reviewProtocol, time-capped, written outcome
- DecisionTake, skip or defer, with a reason
- Order entryBy hand, at your broker. Not taught here.
On the left of the boundary, AmiBroker does a great deal. AlertIf() fires an action when a
condition is true on one of the most recent bars: it writes to the Alert Output window, beeps,
plays a WAV file, sends an e-mail, or launches an external program or URL. Its flags argument
defaults to 1+2+4+8, where 4 and 8 suppress repeated alerts of the same type and the same
date-time respectively, and its type argument is the de-duplication key rather than a label —
giving Buy and Sell the same type makes the state machine suppress legitimate alternating
signals. The state, the type of the last alert, is kept per symbol. The Analysis window’s
Settings drop-down carries Auto repeat Scan/Explore with an interval, which is the sanctioned
way to run a screener repeatedly rather than building one by hand.
Alerts only reach you if the relevant subsystem is enabled under Tools → Preferences → Alerts,
which has separate checkboxes for Automatic Analysis, Commentary and custom indicators. If you
have ever wondered why an alert in a chart formula produced nothing, that is usually why. Where
you do not want the de-duplication state machine at all, SendEmail(), PlaySound() and
ShellExecute() do the same jobs unconditionally and leave you to manage your own state.
To the right of the boundary, this course stops. Not because the topic is uninteresting, and not because AmiBroker cannot do it.
Why the course stops short of automated execution
Section titled “Why the course stops short of automated execution”Five reasons, in descending order of how much they should worry you.
Every defect changes category. A wrong array reference in an exploration produces a wrong row
and you notice it, or you do not and nothing happens. The same defect behind an order interface
produces a position. All the things this course has taught you to catch — look-ahead, a Null
that propagated, a regime gate that failed open when the benchmark went missing, an off-by-one in
a Ref() — become financial events instead of research errors. The skill that catches them is
the same; the cost of missing one is not.
Repeated execution places repeated orders. AmiBroker’s own automatic-trading documentation is explicit about this: run order-placing code from an indicator and every chart refresh that satisfies the condition fires another order. The documented remedies are one-shot triggers, static variables holding order identifiers, or checking the pending list before acting. That is an entire discipline — idempotency, state reconciliation, error handling — and it is not the discipline this course teaches.
The operational surface is large and unglamorous. A live order path has to cope with a disconnected broker session, a partial fill, a rejected order, a stale position list, a symbol that maps differently at the broker than in your database, and a machine that goes to sleep. None of that is technical analysis and all of it decides whether the system works.
Your obligations are specific to you. What you are permitted to automate, what records you must keep and what your broker’s agreement says about unattended order entry vary by country, by account type and by broker. A course cannot answer that for you, and a course that pretends the question does not exist is doing something worse than omitting it.
The last thing you should automate is the judgement you have not yet measured. Lesson 4 shows how to measure the gap between what your rules said and what you did. Until you have that measurement, you do not know whether removing the human improves anything. Automating first and measuring afterwards gets the order exactly backwards.
What exists, so you can go and study it properly
Section titled “What exists, so you can go and study it properly”AmiBroker has an automatic trading interface for Interactive Brokers, called IBController. It is
a separate application, BrokerIB.EXE, which sits between AmiBroker and Trader Workstation and
is launched automatically by the first GetTradingInterface("IB") call. It exposes order
placement, modification and cancellation plus account and position queries; it provides no price
data. Its documented version is 1.3.8 and it is permanently labelled beta, explicitly because
the broker’s API changes without notice and fixes can only follow. By default it does not
transmit orders at all — transmission requires entering an unlock code after accepting an
agreement. Its own readme is intended for the Analysis window rather than for indicators, and
advises testing first on a demo connection, then on a paper-trading account, and not leaving
automated trading unattended.
Those are the facts, and they are the right facts to have before you decide to go further. If you do, treat it as a new subject with its own literature and its own testing regime, not as the last line of an AFL formula.
The candidate review sheet
Section titled “The candidate review sheet”One exploration that ends the mechanical half of the evening: apply the regime gate, the universe, the setup and the trigger, then print every number the reviewer needs — stop level, stop distance, planned size, which cap is binding, participation — so that the review can be a review rather than a research session.
Complete formula
Section titled “Complete formula”Complete runnable AFL
// candidate-review-sheet.afl// Part 35 - From Candidate to Decision//// The daily hand-off. This Exploration applies the regime gate, the universe// definition, the setup and the trigger, and then prints every number a human// needs in order to review a candidate and decide. It stops there. It places// no orders, and it deliberately produces no column that says "buy".//// The sizing arithmetic below is the arithmetic of the plan, not a// recommendation. It answers "if I were to take this trade at the risk budget// I have already committed to, how many shares would that be, and would that// position be too large for this instrument's turnover?"//// Run settings this formula assumes:// Apply to: the watch list that defines your universe// Range: All quotations, with Filter restricting output to the last bar//// Execution assumptions this sheet is built on. Change them here, not in your// head, and record the change in the journal:// - Signals are evaluated on the closed daily bar. Nothing is acted on// during the session that produced the bar.// - The planned entry is the NEXT bar's open. The sheet therefore never// shows a fill price, because on the evening you read it that price does// not exist yet.// - The stop is a price level, not an order type. Whether it rests at the// broker or is checked manually changes the fill you get and is a// decision recorded in the journal, not something this formula knows.// - Commission and slippage are not modelled here. They belong in the// backtest, where they change the answer; here they would only decorate// a number the human is about to read.
_SECTION_BEGIN("Candidate review sheet");
// ---- Account and risk policy --------------------------------------------
AccountEquity = Param( "Account equity", 100000, 1000, 100000000, 1000 );RiskPercent = Param( "Risk budget per trade (% of equity)", 0.5, 0.05, 5, 0.05 );MaxPositionPct = Param( "Position value cap (% of equity)", 10, 1, 100, 1 );MaxParticipation = Param( "Participation cap (% of daily turnover)", 1, 0.05, 25, 0.05 );
// ---- Universe --------------------------------------------------------------
MinTurnover = Param( "Liquidity floor: average daily turnover", 2000000, 0, 100000000, 100000 );TurnoverWindow = Param( "Turnover averaging window (bars)", 50, 5, 250, 5 );MinPrice = Param( "Price floor (per share)", 5, 0, 500, 0.5 );
// ---- Regime gate ----------------------------------------------------------
BenchmarkTicker = ParamStr( "Benchmark symbol", "^GSPC" );RegimePeriod = Param( "Benchmark trend period (bars)", 200, 20, 500, 10 );
// ---- Setup and trigger -----------------------------------------------------
TrendPeriod = Param( "Instrument trend period (bars)", 200, 20, 500, 10 );BreakoutPeriod = Param( "Breakout lookback (bars)", 50, 5, 250, 5 );AtrPeriod = Param( "ATR period", 20, 2, 200, 1 );StopAtrMult = Param( "Stop distance (ATR multiples)", 2.5, 0.5, 10, 0.25 );
RaiseAlerts = ParamToggle( "Raise an alert for each candidate", "No|Yes", 0 );
// ---- Universe membership --------------------------------------------------
Turnover = Close * Volume;AvgTurnover = MA( Turnover, TurnoverWindow );SafeTurnover = Max( Nz( AvgTurnover ), 1 );
InUniverse = Nz( AvgTurnover ) >= MinTurnover AND Close >= MinPrice;
// ---- Regime gate ----------------------------------------------------------// The gate does not claim to know what the market will do next. It says that// while the benchmark is below its own long average, this particular set of// rules is switched off, because that is the condition under which it was// tested and the condition under which its results were measured.
BenchClose = Foreign( BenchmarkTicker, "C" );BenchTrend = MA( BenchClose, RegimePeriod );RegimeOn = NOT IsNull( BenchClose ) AND BenchClose > BenchTrend;
// ---- Setup (a state) and trigger (an event) --------------------------------
InstrumentTrend = MA( Close, TrendPeriod );Setup = Close > InstrumentTrend;
// Ref( High, -1 ) excludes the current bar from its own breakout level, so the// level being crossed was established before today rather than by today.PriorHigh = HHV( Ref( High, -1 ), BreakoutPeriod );Trigger = Cross( Close, PriorHigh );
IsCandidate = InUniverse AND RegimeOn AND Setup AND Trigger;
// ---- Risk arithmetic -------------------------------------------------------// Stop first, then size. Sizing before the stop is known is how a position// ends up risking whatever happens to be left over.
Volatility = ATR( AtrPeriod );StopDistance = StopAtrMult * Volatility;StopLevel = Close - StopDistance;
// A floor on the denominator: an instrument whose ATR is reported as zero,// which happens on a symbol with a long run of identical closes, would// otherwise produce an infinite share count.SafeDistance = Max( Nz( StopDistance ), 0.000001 );
RiskBudget = AccountEquity * RiskPercent / 100;RiskShares = floor( RiskBudget / SafeDistance );
// Three caps, applied in the order that makes the binding one visible. The// smallest of them is what you would actually be able to trade.ValueCapShares = floor( ( AccountEquity * MaxPositionPct / 100 ) / Max( Close, 0.000001 ) );LiquidityCapShares = floor( ( SafeTurnover * MaxParticipation / 100 ) / Max( Close, 0.000001 ) );
PlannedShares = Min( RiskShares, Min( ValueCapShares, LiquidityCapShares ) );PlannedValue = PlannedShares * Close;PlannedRisk = PlannedShares * StopDistance;
// 0 risk budget, 1 position value cap, 2 turnover participation cap.BindingCap = IIf( PlannedShares == RiskShares, 0, IIf( PlannedShares == ValueCapShares, 1, 2 ) );BindingList = "risk budget\nposition value cap\nparticipation cap";
// What the position actually risks as a percentage of equity once the caps// have cut it down. When a cap binds, this is smaller than the risk budget -// which is the correct outcome, not a bug to be tuned away.RiskOfEquity = 100 * PlannedRisk / Max( AccountEquity, 1 );Participation = 100 * PlannedValue / SafeTurnover;
// ---- Output ----------------------------------------------------------------
Filter = IsCandidate AND Status( "lastbarinrange" );
AddTextColumn( FullName(), "Name", 40 );AddColumn( Close, "Signal close", 1.2 );AddColumn( StopLevel, "Stop level", 1.2 );AddColumn( StopDistance, "Stop distance", 1.2 );AddColumn( 100 * StopDistance / Max( Close, 0.000001 ), "Stop as % of price", 1.2 );AddColumn( PlannedShares, "Planned shares", 1.0 );AddColumn( PlannedValue, "Planned value", 1.0 );AddColumn( RiskOfEquity, "Risk % of equity", 1.2 );AddMultiTextColumn( BindingCap, BindingList, "Binding constraint", 22 );AddColumn( Participation, "Participation %", 1.2 );AddColumn( AvgTurnover, "Avg turnover", 1.0 );AddColumn( 100 * Volatility / Max( Close, 0.000001 ), "ATR as % of price", 1.2 );
// Counting the two default columns, "Participation %" is column 12. Reviewing// the least liquid candidates first is deliberate: they are the ones where the// plan is most likely to be undeliverable, and finding that out early saves// the review time for candidates you could actually trade.SetSortColumns( -12 );AddRankColumn();
if ( RaiseAlerts ){ // On end-of-day data run after the close, the last bar is complete and this // is the alert you want. On intraday data the forming bar can turn a // condition on and off within the bar; Part 25 covers the completed-bar // guard and the de-duplication state machine behind AlertIf. AlertIf( IsCandidate, "", "Candidate for review: " + Name(), 1 );}
_SECTION_END();How it works
Section titled “How it works”The header comment is the most important part of the file, and it is deliberately long. It states the execution assumptions — signals evaluated on the closed daily bar, planned entry at the next bar’s open, the stop as a price level rather than an order type, costs modelled in the backtest rather than here — because those assumptions are what make the sheet’s numbers mean anything.
The rules are assembled in the pipeline’s order: universe, then regime gate, then setup, then
trigger. The setup is a state (Close > MA( Close, TrendPeriod )) and the trigger is an event
(Cross() above a breakout level computed from Ref( High, -1 ), so the level being crossed was
established before today rather than by today). Part 8’s state-versus-event distinction is doing
real work here.
The risk section derives the stop first and the size second, then applies three caps and reports
which one bound. The Risk % of equity column shows what the position actually risks after the
caps have cut it, which is often less than the policy maximum — and seeing that in a column is
what stops people from quietly restoring the difference.
The output is sorted by participation descending, so the candidates whose plans are least deliverable are reviewed first. That ordering is a small thing that changes the evening: finding out early that three of eleven candidates cannot be traded at a sensible size leaves time to review the rest properly.
Key functions
Section titled “Key functions”ATR( period )— average true range, used as the volatility unit that the stop distance is expressed in. It takes only a period; the price arrays are implicit.Cross( array1, array2 )— true on the bar where the first crosses above the second. An event, not a state.HHV( Ref( High, -1 ), n )— the highest high of the previous n bars. Dropping theRef()is the classic way to build a breakout level that includes the bar breaking it.floor()andMin()— share counts are integers, and the binding cap is the smallest of several candidate sizes.Min()takes two arguments, so three caps need it twice.AddRankColumn()— adds a rank column based on the sort established by the precedingSetSortColumns(). Call order matters: with no prior sort it degenerates into a line-number column.AlertIf( condition, command, text, type, flags, lookback )— notification only. Note that the defaultlookbackis 1, so only the most recent bar is examined.
Expected result
Section titled “Expected result”Apply to your universe watch list, Range All quotations, press Explore. On a typical evening in a permitted regime you should get a handful of rows. On a great many evenings you should get none, and that is the formula working: a gated breakout system on a few hundred symbols is inactive most of the time.
Every row should have a stop level below the signal close, a planned share count greater than zero, and a binding-constraint column that reads sensibly against the numbers beside it.
Test it
Section titled “Test it”- Set the risk budget very high — 5 per cent — and confirm the binding constraint switches to
position value caporparticipation capfor most rows. The caps are meant to bind under pressure, and a cap that never binds has not been tested. - Set the participation cap to 25 per cent and watch planned sizes jump. Then ask yourself whether you would actually place those orders, which is a more useful exercise than reading about market impact.
- Pick one row and reproduce its arithmetic by hand from the columns shown. If your hand calculation and the sheet disagree, one of them is wrong, and finding out which is exactly the kind of check that catches a misplaced parenthesis.
- Set the ATR period to 2 and confirm stop distances collapse and share counts explode. This is not a bug; it is a demonstration that the sizing rule is only as sensible as the volatility estimate under it.
Common errors
Section titled “Common errors”- Every candidate has a huge share count. The stop distance is near zero, usually because
ATR()is being computed on a symbol with a long run of identical closes. The floored denominator in the formula prevents an infinity but cannot make the number meaningful. - The breakout fires on almost every bar.
Ref( High, -1 )was dropped from theHHV(), so the level includes the bar that is breaking it. - No rows, ever, on any date. Check the regime state first — it is the gate most likely to be closed for the whole range — then the universe floors, then the trigger.
- Alerts appear once and then never again. That is the
AlertIf()de-duplication state machine working as documented, keyed on alert type and stored per symbol. Drop flags 4 and 8 while experimenting. - Alerts never appear at all. The Automatic Analysis checkbox under Tools → Preferences → Alerts is off.
Extension
Section titled “Extension”Add a column that reports how many of tonight’s candidates share a sector, using SectorID(1).
It will not solve the concentration problem — that is a portfolio property and needs the
positions you already hold — but it turns the fourth review question from a memory exercise into
a number, and it makes it obvious on the evenings when a screen has found the same trade eight
times.
The candidate list is a question, and the ranking on it orders your attention rather than measures merit. Chart review is a time-capped protocol with five fixed questions, a three-word outcome and a categorised reason, and its authority is limited to rejection: an idea that arrives during review goes in the backlog, not into tonight’s orders. Risk assessment precedes sizing, and the planned size is the smallest of the risk budget, the position value cap and the participation cap — frequently less than the policy maximum, which is the correct outcome and not a shortfall to be recovered.
The boundary sits between the alert and the order. Everything to its left is taught here in full; everything to its right is described accurately and left as further study, because the failure modes change category at that line and the obligations on the other side of it are yours specifically.
The final lesson closes the loop: what to write down at the moment of decision, and how to use it later to tell whether the problem was the strategy or you.
Check your understanding
Sources for this lesson
8 verified · checked 2026-08-31
- 01AFL Function Reference — AlertIfamibroker.com/guide/afl/alertif.html2026-08-31
- 02AmiBroker User's Guide — Using formula-based alertsamibroker.com/guide/h_alerts.html2026-08-31
- 03AFL Function Reference — ATRamibroker.com/guide/afl/atr.html2026-08-31
- 04AFL Function Reference — AddRankColumnamibroker.com/guide/afl/addrankcolumn.html2026-08-31
- 05AmiBroker User's Guide — Rankingamibroker.com/guide/h_ranking.html2026-08-31
- 06AmiBroker Automatic Trading Interface (IBController) readmeamibroker.com/at2026-08-31
- 07AFL Function Reference — GetTradingInterfaceamibroker.com/guide/afl/gettradinginterface.html2026-08-31
- 08AmiBroker User's Guide — Using New Analysis window§ Auto repeat Scan/Exploreamibroker.com/guide/h_newanalysis.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.