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
| Method | Description |
|---|---|
AtMarket | Execute at the next available price |
AtOrHigher | Execute at the specified price or higher (limit for shorts, stop for longs) |
AtOrLower | Execute at the specified price or lower (limit for longs, stop for shorts) |
AtClose | Execute 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:
| Placement | Description |
|---|---|
EnterNow | Order is available for evaluation immediately, including on out-of-session shadow bars. This is the default if no placement is specified. |
EnterNextSession | Order 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:
| Bar | What happens (without linked orders) |
|---|---|
| N | Strategy places Buy(AtOrLower, 100.0) |
| N+1 | Entry fills; strategy detects position, places exit |
| N+2 | Exit 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:
| Bar | What happens (with linked orders) |
|---|---|
| N | Strategy places Buy(AtOrLower, 100.0, callback) |
| N | Entry fills on tick T; callback places exit and stop |
| N | Stop 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
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.Stop vs exit evaluation timing. Protective stops (
SetStopLoss,SetProfitTarget, etc.) are evaluated on the same tick as the fill becausetryStops()runs aftertryOrders()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.If the entry doesn’t fill, the callback never fires, and no exits are placed. There is nothing to clean up.
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 inStrategy()on subsequent bars.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.One callback per entry. Each
Buy()orSellShort()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:
| Function | Description |
|---|---|
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
| Function | Returns |
|---|---|
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
| Function | Returns |
|---|---|
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()
}