Strategies and the header
The strategy header in full. Size, fees, spread, slippage, swap, delay and margin, how orders fill, the account figures you can read, and the results.
A strategy is a script whose header is a strategy line and which places orders with the orders.* functions. The chart replays the orders on the bars it has and shows the trades. It never sends an order to a broker. Lesson 14: Your first strategy is the short introduction, Lesson 15: Testing in the Strategy Lab shows the longer tests of the Lab, and this page is the reference.
Words used on this page#
A lot is the standard trading size of an instrument. A tick is the smallest price step. The spread is the gap between the buying and the selling price, and slippage is the price moving between the order and its fill. Swap is the cost (or credit) of holding a position overnight. The profit factor is the money won divided by the money lost, and the drawdown is the fall of the account from its peak.
The header#
strategy "Crossover with costs"
edition = 1
display = price
setup:
capital = 10000
size = sizing.lots(0.1)
fees = fee.per_lot(3.5)
execution:
spread = 25 ticks
slippage = 2 ticks
swap_long = -3.2
swap_short = 1.1
latency = 300 ms
fills = worst
margin:
long = 5
short = 5
let fast = tech.ema(close, 10)
let slow = tech.ema(close, 30)
if tech.crosses_above(fast, slow)
orders.enter("Long", side.long)
if tech.crosses_below(fast, slow)
orders.close("Long")
plot(fast, name = "Fast")
plot(slow, name = "Slow")A strategy header has up to three blocks under the header lines: setup: (the money, the size and the fees), execution: (how orders fill) and margin: (how much of a position is held back). Each block starts at the left margin and its settings are indented under it. Every setting is live: it may read an input, and changing the input changes the next run. For example size = sizing.lots(lots) with input lots: float = 0.1 trades whatever the setting says.
setup:#
| Setting | Meaning |
|---|---|
capital | the money the account starts with on the chart |
size | how much one order trades: one of the four forms below. Without size, an order trades 1 unit |
fees | what a trade costs: one of the four forms below, charged at every fill (the entry and the exit) |
pyramiding | how many entries may be open in one direction at once (1 when you say nothing; with 3, three orders.enter calls in a row add up) |
| Size | Trades |
|---|---|
size = sizing.lots(0.1) | 0.1 lot of the instrument |
size = sizing.fixed(2) | 2 units of the instrument |
size = sizing.equity(10) | the number of units worth 10 percent of the account |
size = sizing.cash(5000) | the number of units worth 5,000 of account money |
| Fees | Charged at every fill |
|---|---|
fees = fee.per_lot(3.5) | 3.5 of account money for each lot |
fees = fee.per_unit(0.5) | 0.5 for each unit |
fees = fee.per_order(5) | 5 for each fill, whatever its size |
fees = fee.rate(0.05) | 0.05 percent of the value traded |
An order may also carry its own qty, in units: orders.enter("Long", side.long, qty = 3) trades 3 units whatever size says. A qty of na is an error (E-QTY-NA), never a silent default.
execution:#
| Setting | Meaning |
|---|---|
spread | in ticks of the symbol (25 ticks). Buys fill on the ask and sells on the bid, so a spread makes every trade start at a loss. auto asks for the instrument's live spread; a run on the chart reads 0 for it, because a snapshot of a live price cannot be repeated |
slippage | in ticks: market and stop orders fill this many ticks worse than their price |
slippage_random | true: each fill slips by a random part of slippage, not all of it |
swap | off: no overnight cost at all |
swap_long, swap_short | the overnight cost of a long or a short position for one lot and one night (a negative number is a cost, a positive one a credit), in the unit of swap_mode |
swap_mode | percent, money or points |
latency | the time between an order and its fill (300 ms, or a duration such as 1s). The price goes on moving meanwhile, so a long delay changes the fill price; a delay shorter than a candle often changes nothing on the chart |
fills | which of a stop and a target fills first when one candle reaches both: path (the default), worst, best, stop_first, target_first (see below) |
costs | none leaves out the instrument's commission and swap, and the swap you declare too; the fees of setup: are charged either way |
lots | round rounds every size to the instrument's lot step; an order that rounds to less than the smallest lot is refused (the run says how many were) |
fill_on_close | true fills an order at the close of the bar that placed it, instead of at the next bar's open |
The words ticks and ms after a number are accepted only on a setting of execution:. Anywhere else they are refused:
indicator "A unit outside the header"
edition = 1
plot(25 ticks)A setting a block does not have is an error, with the list of those it has:
strategy "A setting that does not exist"
edition = 1
display = price
execution:
speed = 25
if bar.index == 10
orders.enter("Long", side.long)margin:#
long = 5 and short = 5 are the percent of a position's value held back as margin. An order that needs more than the free margin is refused, and the run says how many orders were refused for that reason. 0 switches the margin check off. Without a margin: block a default margin still applies, so a position far larger than the account is refused too.
Orders#
| Call | Does |
|---|---|
orders.enter(key, side, ...) | opens or adds to a position; with limit or stop it waits for that price, and with expires = n it is cancelled after n bars |
orders.exit(key, entry, ...) | the exit of the trade opened by the entry called entry: a stop and a limit price, or loss_ticks and profit_ticks from the entry price; also a trailing stop (trail_ticks, trail_offset_ticks) |
orders.close(key) | closes the trades opened by that entry |
orders.close_all() | closes everything |
orders.cancel(key), orders.cancel_all() | take back orders that are still waiting |
An order that you place when a bar closes is live from the next price: a market order fills at the next bar's open (unless fill_on_close = true). orders.exit belongs to the trade it was placed for, and ends with it. It cannot capture the next trade by accident. The orders reference lists every parameter.
Keys are strict. If you close an entry with a key that differs from a used one only in case, spaces, _ or -, you almost certainly mistyped it, and the engine stops with a suggestion:
strategy "A mistyped id"
edition = 1
display = price
if bar.index == 10
orders.enter("Long", side.long)
if bar.index == 20
orders.close("long")strategy "A size that is na"
edition = 1
display = price
if close > open
orders.enter("Long", side.long, qty = na)Reading the account#
These are series like any other and can be used in conditions:
| Name | Is |
|---|---|
position.size | the open size, positive for a long and negative for a short |
position.avg_price | the average price of the open position |
stats.equity | the account value, with open profit |
stats.net_profit | the profit of the closed trades |
trades.closed_count | the number of closed trades |
stats.equity_low | the lowest equity inside the candle that has just closed |
stats.day_low_equity | the lowest equity inside the trading day so far |
stats.day_start_equity | the equity the trading day opened with |
A candle's close hides how far the account fell inside it. stats.equity_low and stats.day_low_equity show it, which is what a daily loss limit needs:
strategy "Daily loss guard"
edition = 1
display = price
setup:
capital = 10000
size = sizing.lots(0.1)
input max_loss_pct: float = 1.0, name = "Stop trading today after losing %", min = 0.1, step = 0.1
let fast = tech.ema(close, 10)
let slow = tech.ema(close, 30)
let day_loss = (stats.day_start_equity - stats.day_low_equity) / stats.day_start_equity * 100
let blocked = day_loss >= max_loss_pct
if tech.crosses_above(fast, slow) and not blocked
orders.enter("Long", side.long)
if tech.crosses_below(fast, slow)
orders.close("Long")
plot(day_loss, name = "Loss today, %")Pending orders work the same way as market orders; a stop entry waits for a price and gives up after expires bars:
strategy "Pending breakout entry"
edition = 1
display = price
let high20 = tech.highest(high, 20)
if bar.index % 50 == 0
orders.enter("Breakout", side.long, stop = high20, expires = 5)
plot(high20, name = "20-bar high")The risk.* functions stop new entries when a limit is reached, such as two entries a day or three losing days in a row; the risk reference lists them and the strategy samples use two of them.
When one candle reaches two prices#
The chart has only the open, high, low and close of a candle. When a candle reaches both your stop and your target, which came first? The fills setting decides:
path(the default) follows the candle's shape: a rising candle is taken to dip first, a falling one to rise first.worsttakes the order that leaves the account lower, andbestthe order that leaves it higher.stop_firstandtarget_firstalways take that order.
worst is the careful choice: it assumes the less favourable order, so a result built on it is not flattered by luck. The Strategy Lab can also replay the minutes inside each candle, which settles most of these cases instead of guessing.
The results#
On the chart a strategy shows a triangle at each entry (with the entry's key), a cross at each exit (green for a profit, red for a loss), a dotted line from entry to exit, and a table with the net profit and its percentage, the closed trades, the share of them that made money, the profit factor, the largest drawdown and the open trades. A Costs row appears when commission or swap were paid, and Guessed exits when the order of two events inside a candle was a guess that changed the account. The strategy's two inputs, Show the trades on the chart and Show the results table, switch them off. The marks and lines are kept for the newest 500 trades only, so on a long history the table may count more trades than the chart draws.
A refusal by the broker model (an order under the smallest lot, one beyond the free margin) is a warning of the run, said once with its count.
Not modelled, and not available yet#
- A header option that the replay does not model is refused with
E3130, which lists the settings it does take. fundingandhedgein the header, and thebracketorder statement, are not available yet;bracketis refused as belonging to a later edition (E9002).- A strategy cannot read the future (
future = trueisE3132), and it trades one symbol, the chart's. - Nothing here is a live order. The chart and the Strategy Lab simulate trades on past bars; MillionChart executes no real orders.
See also: Lesson 14: Your first strategy, Lesson 15: Testing in the Strategy Lab, Results that look too good, Trading day and sessions.