millionchart
فارسی

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#

Cuprel
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:#

SettingMeaning
capitalthe money the account starts with on the chart
sizehow much one order trades: one of the four forms below. Without size, an order trades 1 unit
feeswhat a trade costs: one of the four forms below, charged at every fill (the entry and the exit)
pyramidinghow 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)
SizeTrades
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
FeesCharged 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:#

SettingMeaning
spreadin 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
slippagein ticks: market and stop orders fill this many ticks worse than their price
slippage_randomtrue: each fill slips by a random part of slippage, not all of it
swapoff: no overnight cost at all
swap_long, swap_shortthe 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_modepercent, money or points
latencythe 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
fillswhich of a stop and a target fills first when one candle reaches both: path (the default), worst, best, stop_first, target_first (see below)
costsnone leaves out the instrument's commission and swap, and the swap you declare too; the fees of setup: are charged either way
lotsround 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_closetrue 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:

Cuprel · error E3270
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:

Cuprel · error E3130
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#

CallDoes
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:

Cuprel · error E-ORDER-ID
strategy "A mistyped id"
    edition = 1
    display = price

if bar.index == 10
    orders.enter("Long", side.long)
if bar.index == 20
    orders.close("long")
Cuprel · error E-QTY-NA
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:

NameIs
position.sizethe open size, positive for a long and negative for a short
position.avg_pricethe average price of the open position
stats.equitythe account value, with open profit
stats.net_profitthe profit of the closed trades
trades.closed_countthe number of closed trades
stats.equity_lowthe lowest equity inside the candle that has just closed
stats.day_low_equitythe lowest equity inside the trading day so far
stats.day_start_equitythe 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:

Cuprel
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:

Cuprel
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.
  • worst takes the order that leaves the account lower, and best the order that leaves it higher.
  • stop_first and target_first always 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.
  • funding and hedge in the header, and the bracket order statement, are not available yet; bracket is refused as belonging to a later edition (E9002).
  • A strategy cannot read the future (future = true is E3132), 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.