Position Sizing with SetPositionSize()
Here is a fact that stops more first portfolio backtests than any other, stated in the User’s
Guide in almost these words: unless less than 100% of your funds go into a single security,
only one position can ever be open. You can set MaxOpenPositions to fifty. Without a
position size that leaves room for a second trade, you will get one.
That is the entry point to this lesson. Sizing is not a refinement you add later. It is the thing that decides whether a portfolio backtest is a portfolio backtest at all.
The variable came first
Section titled “The variable came first”PositionSize is a reserved array variable, introduced in version 3.9, and its encoding is
worth knowing even though you will rarely assign it directly, because SetPositionSize()
writes into it and the numbers show up in diagnostics.
| Value | Interpretation |
|---|---|
| above 0 | A currency amount. PositionSize = 1000; invests 1000 per trade. |
| −1 to −100 | A percentage of current portfolio equity. −25 means 25%, −100 means all of it. |
| −1000 to 0 | Percent of portfolio equity (the range SetPositionSize() encodes into) |
| −2000 to −1000 | Percent of the currently open position, used by scaling |
| below −2000 | A share count |
The documentation is explicit that assigning these encoded values directly is possible but
that new code should use SetPositionSize() for clarity. It is also explicit that a later
plain PositionSize = ... assignment clobbers whatever SetPositionSize() set, because
they are the same underlying variable. If a formula does both, the last one wins and the
first one is invisible.
SetPositionSize() and its four methods
Section titled “SetPositionSize() and its four methods”The signature is short: SetPositionSize( size, method ). Both arguments are arrays, which
matters more than it looks.
| Constant | Value | What size means |
|---|---|---|
spsNoChange |
0 | Leave the size already set for this bar alone |
spsValue |
1 | A currency amount |
spsPercentOfEquity |
2 | A percentage of portfolio-level equity |
spsPercentOfPosition |
3 | A percentage of the currently open position — scaling in and out only |
spsShares |
4 | A number of shares or contracts, which must be greater than zero |
Because method is an array, it can vary bar by bar, which is what makes the documented
scaling idiom work:
Fragment — not a complete formula
// Default to 100 shares, but on a scale-out bar reduce the position by half.SetPositionSize( 100, spsShares );SetPositionSize( 50, IIf( Buy == sigScaleOut, spsPercentOfPosition, spsNoChange ) );Pyramiding and scaling are a portfolio-backtester-only feature and a subject of their own;
what matters here is that spsNoChange exists precisely so that a second call can apply to
some bars and leave the rest alone.
Equal weight: the standard pairing
Section titled “Equal weight: the standard pairing”The most common sizing rule in the whole of AmiBroker is three lines, and the User’s Guide prints it as the canonical construction:
Fragment — not a complete formula
PosQty = 5;SetOption( "MaxOpenPositions", PosQty );SetPositionSize( 100 / PosQty, spsPercentOfEquity );Five slots, each holding a fifth of current equity. As equity grows the positions grow with it; as it falls they shrink. Two properties follow that are worth saying out loud. It compounds, which is what makes a long test’s terminal figure so sensitive to the early trades. And it never leaves the account fully invested in practice, because commissions and whole-share rounding always leave a little cash behind.
If less than 100% of cash is invested, the remainder earns the annual interest rate from Settings. Set that rate to zero unless you specifically intend to model a cash yield, or a share of your reported return will be interest you never negotiated with anybody.
Fixed fractional and risk-based sizing
Section titled “Fixed fractional and risk-based sizing”Equal weight equalises capital. It does not equalise risk: a fifth of equity in a quiet utility and a fifth in a volatile miner are two very different bets. Risk-based sizing starts from the other end — decide what you are willing to lose, then work out the share count that loses exactly that if the stop is hit.
The User’s Guide gives the arithmetic directly, in the form of a worked example: with a stop two average true ranges away, on a stock at 50 with the stop at 45, a 1,000 risk divided by the 5 per share of stop distance gives 200 shares. Those 200 shares are 10,000 of exposure — the allocation is ten times the risk. That distinction is the entire point of the method, and it is the one people lose first.
Written as a percentage of equity rather than a fixed sum, which is what makes it compound with the account, the same idea becomes the construction the guide uses for volatility sizing:
Fragment — not a complete formula
// Risk RiskPercent of portfolio equity per trade, with the stop StopDistance away.// Position value / equity = RiskPercent * price / StopDistance.StopDistance = 3 * Ref( ATR( 20 ), -1 );RiskWeight = 1.0 * SafeDivide( Close, StopDistance, 0 );SetPositionSize( Min( RiskWeight, 25 ), spsPercentOfEquity );Three details in three lines. Ref( ..., -1 ) because the average true range of the bar the
order fills on had not finished forming when the decision was made. SafeDivide because an
instrument with a zero range would otherwise ask for an infinite position. And Min( ..., 25 )
because a very tight stop asks for a very large position, and an unbounded risk rule
concentrates the account into whichever symbol happens to be quietest that week.
When the size does not fit
Section titled “When the size does not fit”Several settings decide what happens when the computed size collides with reality. All of them can silently produce “my system took no trades”.
SetOption( "AllowPositionShrinking", ... ) decides the case where the requested size
exceeds available cash. On, the position is entered with the size shrunk to what the cash
allows. Off — and off is a real choice, not an oversight — the trade is not entered at
all.
SetOption( "MinShares", n ) and SetOption( "MinPosValue", n ) set floors. If the
funds will not buy that many shares, or that much value, the trade is not entered. These are
useful — a 40-unit position is not a trade anybody would place — and they are also a common
cause of a portfolio that mysteriously stops trading after a drawdown, because the slot value
fell below the floor.
RoundLotSize is the block size. Zero means “use the default from Settings”; if that is
also zero, fractional share counts are allowed, which is convenient and unrealistic for most
equity markets. Set it to 1 for whole shares.
SetOption( "UsePrevBarEquityForPosSizing", True ) switches percentage sizing from
current intraday equity to the previous bar’s closing equity. The default is False. Think
about which one your account could actually have achieved: sizing a Wednesday-open order
using Wednesday’s intraday equity assumes you knew Wednesday’s marks before Wednesday
happened.
Why sizing moves the answer
Section titled “Why sizing moves the answer”Take a single fixed sequence of trades and change nothing about them except how much money was committed to each. The table below does exactly that. The eight trade results are invented for this illustration — they are not from any market, any symbol or any backtest — and their only job is to be identical across the three columns.
One invented sequence of eight trades, three sizing rules
| Bar | Start | +12% | −8% | +25% | −15% | +6% | −20% | +35% | −10% |
|---|---|---|---|---|---|---|---|---|---|
Full equity each trade | 100,000 | 112,000 | 103,040 | 128,800 | 109,480 | 116,049 | 92,839 | 125,333 | 112,799 |
Quarter of equity | 100,000 | 103,000 | 100,940 | 107,249 | 103,227 | 104,775 | 99,537 | 108,246 | 105,540 |
Fixed 25,000 per trade | 100,000 | 103,000 | 101,000 | 107,250 | 103,500 | 105,000 | 100,000 | 108,750 | 106,250 |
Same trades, same order, same entries and exits. The profit differs by more than a factor of two and the worst drawdown by a factor of four. Nothing about the entry rule was involved in producing that spread.
The honest way to state the general point is this: sizing controls the scale of the distribution of outcomes, and the entry rule controls its shape. A backtest that changes both at once cannot tell you which one moved. That is why the sizing bench below fixes the signals and varies only the method, and it is why Part 34’s lab runs three sizing models over one signal set rather than three systems.
The formula
Section titled “The formula”Complete runnable AFL
// position-sizing-bench.afl// Part 28 - Position Sizing with SetPositionSize()//// One set of signals, four documented ways of turning each signal into a number// of shares. Run it as a portfolio BACKTEST ("Apply to" a watch list), then run// it again as an OPTIMIZATION over SizingMethod to get all four in one table.//// ASSUMPTIONS// Fill price Next bar's open, taken exactly. No slippage, spread or partial// fill. Costs are whatever Settings says - set them on purpose.// Delays One bar on every signal.// Sizing input Everything the sizing arithmetic reads is from the bar BEFORE// the entry bar, so the size could genuinely have been decided// the previous evening. See section 4.// Liquidity Not modelled. A size this formula computes may be larger than// the volume that traded. Part 30 deals with that properly.// Account Cash account, no margin, no interest assumptions beyond the// Settings default.//// The four methods produce different equity curves from IDENTICAL signals. That// difference is a property of the sizing rule, not evidence about the entries.
// ------------------------------------------------------- 1. account and modeSetOption( "InitialEquity", 100000 );SetOption( "AllowPositionShrinking", True );SetOption( "MinPosValue", 1000 ); // veto trades too small to be realRoundLotSize = 1; // whole shares only
PosQty = 10;SetOption( "MaxOpenPositions", PosQty );
SetTradeDelays( 1, 1, 1, 1 );
BuyPrice = Open;SellPrice = Open;ShortPrice = Open;CoverPrice = Open;
// ------------------------------------------------------------ 2. the signals// Kept plain on purpose. The subject of this formula is the sizing, so the// entries must not be interesting enough to argue about.FastMa = MA( Close, 20 );SlowMa = MA( Close, 100 );
Buy = Cross( FastMa, SlowMa );Sell = Cross( SlowMa, FastMa );
// ------------------------------------------------- 3. the sizing parametersDollarsPerTrade = 10000; // method 1PercentOfEquity = 100 / PosQty; // method 2 - equal weight across PosQty slotsFixedShares = 100; // method 3RiskPercent = 1.0; // method 4 - percent of equity risked per tradeAtrPeriod = 20;AtrMultiple = 3;MaxWeight = 25; // method 4 cap, percent of equity in one position
// --------------------------------------------- 4. inputs known before entry// The entry happens on the bar AFTER the signal bar. Anything this formula// reads on the signal bar is therefore known before the order is placed, which// is what makes the size defensible. Using the entry bar's own ATR or its own// Open would mean sizing with information that had not printed when the// decision was made.RefPrice = Close; // the close that produced the signalStopDistance = AtrMultiple * ATR( AtrPeriod );
// SafeDivide keeps a zero-range instrument from producing an infinite size.RiskWeight = RiskPercent * SafeDivide( RefPrice, StopDistance, 0 );RiskWeight = Min( RiskWeight, MaxWeight );
// -------------------------------------------------------- 5. the four methods// The documented method constants are spsValue (=1), spsPercentOfEquity (=2),// spsPercentOfPosition (=3, scaling only) and spsShares (=4). Note that they are// NOT numbered in the order the documentation lists them - spsShares is 4 and// spsPercentOfPosition is 3 - so never write the numbers from memory.SizingMethod = Optimize( "Sizing method", 2, 1, 4, 1 );
if( SizingMethod == 1 ){ // Fixed dollar value per trade, written with the reserved variable, which is // how the User's Guide's own examples express it: a positive number IS a // dollar amount. The newer spelling SetPositionSize( DollarsPerTrade, // spsValue ) encodes into exactly this variable and means the same thing. // Fixed dollars do not compound: a 40% drawdown does not shrink the next // bet, so the account can be ground down by a size it can no longer afford. PositionSize = DollarsPerTrade;}
if( SizingMethod == 2 ){ // Equal weight. The standard portfolio pairing with MaxOpenPositions. SetPositionSize( PercentOfEquity, spsPercentOfEquity );}
if( SizingMethod == 3 ){ // Fixed share count. Equalises nothing: 100 shares of a $5 stock and 100 // shares of a $500 stock are two completely different bets. SetPositionSize( FixedShares, spsShares );}
if( SizingMethod == 4 ){ // Risk-based sizing expressed as a percentage of portfolio equity, which // is the construction the User's Guide gives for volatility sizing. Risking // RiskPercent of equity over a stop StopDistance away means holding a // position worth RiskPercent * price / StopDistance percent of equity. SetPositionSize( RiskWeight, spsPercentOfEquity );
// The stop that the sizing assumes must actually exist, or the risk figure // is fiction. ExitAtStop = 2 checks the bar's High-Low range but exits on // the NEXT bar's trade price rather than pretending to fill at the level. ApplyStop( stopTypeLoss, stopModePoint, StopDistance, 2 );}
// ---------------------------------------------------------- 6. ranking ties// With more signals than slots, something has to choose. Ranking by liquidity// says "prefer the ones we could actually have traded", which is an execution// argument rather than a return-seeking one. It also systematically prefers the// largest names in the universe - a bias you are choosing, so state it.PositionScore = MA( Close * Volume, 50 );Four methods, one set of signals. Run it as a portfolio Backtest with the default method,
then as an Optimization over SizingMethod to get all four rows in one table. Because the
signals are identical in every row, every difference in that table is attributable to the
sizing rule and to nothing else.
Two things to look at when you have it. First, the trade counts. They will not be equal:
fixed-share sizing on a high-priced symbol can exhaust the cash that percentage sizing would
have spread across three positions, so some entries are refused. Second, Exposure %. A
sizing rule that leaves cash idle produces a much lower exposure, which inflates
Risk Adjusted Return % without anything good having happened.
Without a PositionSize that leaves room for a second trade, a portfolio backtest holds one
position regardless of MaxOpenPositions. SetPositionSize( size, method ) expresses that
size four documented ways, whose constants are not numbered in the order they are listed, so
write the names. Equal weight is the standard pairing with MaxOpenPositions and it
compounds. Risk-based sizing separates the amount risked from the amount allocated, and it
depends on a stop that gaps can defeat. AllowPositionShrinking, MinShares, MinPosValue
and RoundLotSize can all refuse a trade outright, and the Detailed log names which one did.
And a change of sizing rule can move a result further than a change of entry rule, which is
why the two must never be varied in the same experiment.
Check your understanding
Sources for this lesson
5 verified · checked 2026-08-31
- 01AFL Function Reference — SetPositionSizeamibroker.com/guide/afl/setpositionsize.html2026-08-31
- 02AmiBroker User's Guide — Back-testing your trading ideas§ Position sizingamibroker.com/guide/h_backtest.html2026-08-31
- 03AmiBroker User's Guide — Portfolio-level backtesting§ Setting up position sizeamibroker.com/guide/h_portfolio.html2026-08-31
- 04AFL Function Reference — SetOptionamibroker.com/guide/afl/setoption.html2026-08-31
- 05AmiBroker User's Guide — System test report window§ Known differences between old and new backtesteramibroker.com/guide/w_report.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.