Trading Orders

Trading Orders

This chapter covers placing orders, managing positions, and tracking profit.

Placing entry orders

Buy

Buy() places a buy order to initiate or add to a long position:

rs.Buy()                                          // Buy 1 unit at market
rs.Buy(3)                                         // Buy 3 units at market
rs.Buy(1, engine.AtMarket)                        // Explicit market order
rs.Buy(1, engine.AtOrHigher, price)               // Buy at limit price or higher
rs.Buy(1, engine.AtOrLower, price)                // Buy at stop price or lower
rs.Buy(1, engine.AtMarket, callback, "OrderName") // With callback and name

SellShort

SellShort() places a sell order to initiate or add to a short position:

rs.SellShort()                                     // Short 1 unit at market
rs.SellShort(1, engine.AtOrLower, price)           // Short at limit
rs.SellShort(1, engine.AtOrHigher, price)          // Short at stop

Placing exit orders

ExitLong

ExitLong() reduces or fully exits a long position:

rs.ExitLong(engine.AtMarket)                      // Exit at market
rs.ExitLong(engine.AtOrLower, stopPrice)           // Exit at stop
rs.ExitLong(engine.AtOrHigher, limitPrice)         // Exit at limit
rs.ExitLong(engine.AtMarket, callback, "StopName") // With callback and name

ExitShort

ExitShort() reduces or fully exits a short position:

rs.ExitShort(engine.AtMarket)
rs.ExitShort(engine.AtOrHigher, stopPrice)
rs.ExitShort(engine.AtOrLower, limitPrice)

Execution methods

MethodDescription
AtMarketExecute at the next available price
AtOrHigherExecute at the specified price or higher (limit for shorts, stop for longs)
AtOrLowerExecute at the specified price or lower (limit for longs, stop for shorts)
AtCloseExecute at the close of the current bar

Order placement timing

When out-of-session execution is enabled (the default), Algolang loads raw 1-minute “shadow bars” covering all trading hours outside the defined session. Outstanding orders are evaluated against these shadow bars between sessions. The OrderPlacement parameter controls whether an entry order participates in this out-of-session evaluation:

PlacementDescription
EnterNowOrder is available for evaluation immediately, including on out-of-session shadow bars. This is the default if no placement is specified.
EnterNextSessionOrder is held until the start of the next trading session. It is not evaluated on shadow bars.

Pass the placement as an argument to any entry order:

// This entry can fill overnight on shadow bars (default behaviour)
rs.Buy(1, engine.AtOrHigher, breakoutPrice)

// This entry will only fill during the next trading session
rs.Buy(1, engine.AtOrHigher, breakoutPrice, engine.EnterNextSession)

When to use EnterNextSession: Use this when you want entries to occur only during regular trading hours – for example, if your strategy logic depends on session-hours liquidity or you want to avoid overnight fills at potentially illiquid prices.

When to use EnterNow (or omit the placement): Use this when you want entries to fill as soon as the price is hit, regardless of whether the market is in-session or not. This is the default and is appropriate for most strategies.

Note that exit orders (ExitLong, ExitShort) and protective stops (SetStopLoss, SetProfitTarget, etc.) are always evaluated on shadow bars regardless of placement – they do not support EnterNextSession. This ensures that risk management orders are always active.

Fills that occur on shadow bars are annotated with * in the trade list report.

Named orders and callbacks

Orders can be given names for identification and callbacks for notification when filled:

onFilled := func(rs *engine.RunState, order *engine.Order) {
    fmt.Printf("Filled: %s at %.2f\n", order.OrderName(), order.Price())
}

rs.Buy(1, engine.AtMarket, onFilled, "MyEntry")

Named orders are particularly useful when managing multiple entries (pyramiding) or when using the MarketPosition() function with entry name filters.

Linked orders

When a strategy places a conditional entry (such as a limit order), it normally cannot know on the same bar whether the entry will fill. Exit orders can only be placed after the strategy detects the filled position on the next bar, introducing a one-bar delay. This one-bar delay is a fundamental limitation of TradeStation and EasyLanguage, which provide no mechanism for placing orders in response to a fill event. Algolang’s linked orders eliminate this limitation entirely:

BarWhat happens (without linked orders)
NStrategy places Buy(AtOrLower, 100.0)
N+1Entry fills; strategy detects position, places exit
N+2Exit order evaluates for the first time

Linked orders eliminate this delay by placing exit orders inside the entry’s fill callback. When the entry fills, the callback fires immediately within the same bar’s tick-by-tick evaluation, and the exits become active right away:

BarWhat happens (with linked orders)
NStrategy places Buy(AtOrLower, 100.0, callback)
NEntry fills on tick T; callback places exit and stop
NStop evaluates on tick T (same tick); exit evaluates on tick T+1

How it works

The Buy() and SellShort() functions accept an optional callback of type func(*engine.RunState, *engine.Order). When the entry fills, the engine invokes the callback before continuing to the next order. Inside the callback, you have full access to RunState and the filled entry order, so you can:

  • Place exit orders (ExitLong, ExitShort)
  • Set protective stops (SetStopLoss, SetProfitTarget, SetBreakEven, SetDollarTrailing, SetPercentTrailing)
  • Use the fill’s price via order.Price() to compute dynamic exit levels

Orders placed inside the callback are appended to the engine’s order list and participate in the current bar’s remaining tick evaluation. Protective stops (which are evaluated after all orders on each tick) are active from the same tick as the fill. Exit orders are active from the next tick within the same bar.

Basic example: entry with stop loss and profit target

func (s *Strategy) Strategy(rs *engine.RunState) error {
    if rs.MarketPosition() == 0 && buyCondition {
        rs.Buy(1, engine.AtOrLower, entryPrice, func(rs *engine.RunState, fill *engine.Order) {
            rs.SetStopLoss(500.00)
            rs.SetProfitTarget(1000.00)
        })
    }
    return nil
}

When the buy fills, the stop loss and profit target are placed immediately. Both protective stops evaluate on the same tick as the fill, so they are active from the moment the position is opened.

Using the fill price for dynamic exits

The callback receives the filled order, giving access to the actual fill price:

rs.Buy(1, engine.AtOrLower, entryPrice, func(rs *engine.RunState, fill *engine.Order) {
    rs.ExitLong("TakeProfit", engine.AtOrHigher, fill.Price()+5.0)
    rs.SetStopLoss(200.00)
})

This places a limit exit 5 points above the actual fill price, rather than above the requested entry price. If the entry experiences slippage, the exit level adjusts accordingly.

Short entry with linked exits

The same pattern works for short entries:

rs.SellShort(1, engine.AtOrHigher, resistanceLevel, func(rs *engine.RunState, fill *engine.Order) {
    rs.ExitShort("Cover", engine.AtOrLower, fill.Price()-3.0)
    rs.SetStopLoss(400.00)
    rs.SetDollarTrailing(250.00)
})

Pyramiding with linked exits

When pyramiding is enabled, each entry can have its own linked exits. Use named orders and the linked entry name parameter on exits to associate each exit with its specific entry:

func (s *Strategy) Strategy(rs *engine.RunState) error {
    if rs.MarketPosition() == 0 && entrySignal {
        rs.Buy(1, engine.AtOrLower, price1, func(rs *engine.RunState, fill *engine.Order) {
            rs.SetStopLoss(500.00, "Stop1", "Entry1")
        }, "Entry1")
    }
    if rs.MarketPosition() > 0 && pyramidSignal {
        rs.Buy(1, engine.AtOrLower, price2, func(rs *engine.RunState, fill *engine.Order) {
            rs.SetStopLoss(500.00, "Stop2", "Entry2")
        }, "Entry2")
    }
    return nil
}

Market entry with linked exit

Linked orders are not limited to conditional entries. They work with market orders too, though the primary benefit is for conditional entries where the fill timing is uncertain:

rs.Buy(1, engine.AtMarket, func(rs *engine.RunState, fill *engine.Order) {
    rs.SetStopLoss(500.00)
    rs.SetProfitTarget(1000.00)
})

Important notes

  1. Callback timing. The callback fires during the engine’s tick-by-tick evaluation of the bar on which the entry fills. It does not fire during Strategy() execution.

  2. Stop vs exit evaluation timing. Protective stops (SetStopLoss, SetProfitTarget, etc.) are evaluated on the same tick as the fill because tryStops() runs after tryOrders() on each tick with a fresh view of the order list. Exit orders (ExitLong, ExitShort) are evaluated starting from the next tick within the same bar.

  3. If the entry doesn’t fill, the callback never fires, and no exits are placed. There is nothing to clean up.

  4. Callbacks and the strategy function. The callback does not replace logic in Strategy(). You can combine both approaches – use linked orders for the initial exit setup and adjust exits in Strategy() on subsequent bars.

  5. Fill price accuracy. The fill.Price() value in the callback reflects the actual execution price including any slippage, so exits computed from it account for real entry costs.

  6. One callback per entry. Each Buy() or SellShort() call accepts at most one callback. Place all linked exits and stops inside that single callback.

Protective stops and targets

RunState provides convenience functions for common exit strategies:

FunctionDescription
SetStopLoss(dollars)Exit if loss exceeds the specified dollar amount
SetProfitTarget(dollars)Exit when profit reaches the specified dollar amount
SetBreakEven(floor)Move stop to break-even after profit reaches the floor amount
SetDollarTrailing(dollars)Trailing stop that exits if more than the specified amount of achieved profit is given back
SetPercentTrailing(floor, pct)Trailing stop that exits if more than the specified percentage of achieved profit is given back

Example:

func (s *Strategy) Strategy(rs *engine.RunState) error {
    if rs.MarketPosition() == 0 {
        if buySignal {
            rs.Buy()
            rs.SetStopLoss(500.00)
            rs.SetProfitTarget(1000.00)
        }
    }
    return nil
}

Position tracking

FunctionReturns
MarketPosition()Current position: positive = long, negative = short, 0 = flat
PositionSize()Default position size (contracts/shares)
AverageEntryPrice()Average entry price of the current position
BarsSinceEntry()Bars held since last entry (-1 if no entries)
Fills()List of open fills
LastFillPrice()Price of the most recent fill

Profit tracking

FunctionReturns
NetProfit()Realised closed-trade profit in base currency
NetProfitPeak()Historical peak of closed net profit
OpenPositionProfit()Unrealised P&L on open positions (base currency)
OpenPositionProfitPoints()Unrealised P&L in points
EquityPeak()Peak equity seen during the backtest
MaxDrawdownValue()Maximum drawdown in currency
MaxDrawdownPercent()Maximum drawdown as a percentage

Pyramiding

By default, pyramiding (adding to an existing position) is disabled. When disabled, a Buy() call while already long is ignored.

Enable pyramiding via the --no-pyramiding false CLI flag, or in the strategy:

func (s *Strategy) Begin(rs *engine.RunState) error {
    rs.SetPyramiding(true)
    return nil
}

When pyramiding is enabled, multiple entries are tracked individually. MarketPosition() returns the total position size, and AverageEntryPrice() returns the weighted average across all entries.

SetExitOnClose

SetExitOnClose() places an order to exit the position at the close of the current bar. This is useful for strategies that must be flat by the end of each session:

if rs.IsLastBarOfSession(0) {
    rs.SetExitOnClose()
}