Skip to content
Single .md

Order types

The order_type kwarg of sdk.buy/sell/close controls how the order executes. Three values are accepted:

order_typeExecutesExtra kwargs
"market"Immediately, at the current price (default)-
"limit"At the specified price or betterprice (required)
"stop"When the price hits the trigger, becomes marketprice (trigger)

Any other value is not rejected and does not raise an error — on the strategy (frame) path the normalizer silently drops the unrecognized value and the order defaults to market, so a mistyped order_type still executes as a market order. The aliases stop_market and stop-market are not honored on this path either — they are silently downgraded to market (they survive only on the non-default PyO3 validate() path). A protective stop and target are not a separate order type — pass stop_loss / take_profit on a market, limit, or stop order (see Attached stop and target).

market

The default and most frequently used type. Executes immediately against the book.

python
sdk.buy(
    action="buy_to_open",
    qty=1,
    order_type="market",
)

Execution price:

  • Backtest: uses the current candle's close by default (or a more realistic model via execution_model=ohlc + slippage in the backtest params).
  • Chart trading: uses the current book price (ask for a buy, bid for a sell). Slippage is real.

For most scripts, order_type="market" is the appropriate choice.

limit

Only executes if the price reaches the limit or better. For a buy, "better" means lower; for a sell, higher.

python
close = sdk.candles[-1]["close"]
sdk.buy(
    action="buy_to_open",
    qty=1,
    order_type="limit",
    price=close * 0.99,  # try to buy 1% below the close
)

Behavior when it does not execute

The engine keeps monitoring the order until:

  • The price touches the limit -> executes.
  • The tif expires -> cancels.

In a backtest, if the price never touches, the order stays pending until the end of the period (and the backtest counts it as not executed - with no effect). In chart trading, it stays pending indefinitely with tif="gtc".

With probFillOnLimit (backtest)

In the backtest, the engine simulates realistic book behavior: sometimes the price touches the limit but does not execute because the order would be behind in the queue. The probFillOnLimit param (default 1.0 = always executes) controls that probability. Details in reading results.

stop

Inactive until the price crosses the trigger. When it crosses, becomes a market order.

python
sdk.buy(
    action="buy_to_open",
    qty=1,
    order_type="stop",
    price=close * 1.02,  # buy if it rises 2%
)

Classic case: entry on breakout. The buy should happen only when the price breaks a resistance level.

Attached stop and target

There is no bracket or stop_limit order type. To get an entry with a protective stop and a profit target, pass stop_loss and/or take_profit on the entry order (market, limit, or stop):

python
close = sdk.candles[-1]["close"]
sdk.buy(
    action="buy_to_open",
    qty=1,
    order_type="market",
    stop_loss=close * 0.98,
    take_profit=close * 1.05,
)

Result: market entry, stop 2% below, target 5% above. The engine keeps both alive while the position is open; whichever is touched first closes the position and the other is cancelled automatically (OCO-style), with no extra logic in the script. The same stop_loss / take_profit kwargs work on a limit or stop entry.

Time in Force (tif)

Regardless of order_type, tif controls how long the order stays alive while it does not execute:

tifMeaning
"day"Valid until the end of the day/session (default)
"gtc"Good-til-cancelled - lives until explicit cancellation
"this_bar"Valid only for the current bar; cancelled if it does not execute by the next candle

Any other value is not rejected and does not raise an error — on the strategy (frame) path the normalizer silently drops it and the order defaults to day. "ioc" and "fok" are not honored on this path; they are silently downgraded to day.

python
sdk.buy(
    action="buy_to_open",
    qty=1,
    order_type="limit",
    price=close * 0.99,
    tif="gtc",  # stays in the book until cancelled
)

For market, tif is irrelevant since the order executes immediately anyway.

Quantity with size_pct (alternative to qty)

Instead of an absolute quantity, you can pass the percentage of available cash:

python
sdk.buy(
    action="buy_to_open",
    size_pct=0.25,   # use 25% of the cash
    order_type="market",
)

The engine computes qty = (cash * size_pct) / price. Useful for strategies that scale with capital.

Warning: size_pct and qty are mutually exclusive. If you pass both, the engine prioritizes qty and ignores size_pct.

Combined example - breakout with attached stop and target

Breakout entry (stop order) with a fixed stop and target attached to it. The injected Indicator has no ATR method, so compute Wilder ATR over a bounded sdk.candles[-N:] window with a small helper — each frame stays O(1) in history length, and nothing is imported:

python
def _atr(candles, period=14):
    """Wilder ATR over a bounded window — O(period) per bar, no import."""
    rows = candles[-(period * 4):]                 # bounded window converges to the full-history ATR
    if len(rows) < period + 1:
        return None
    trs = []
    for prev, cur in zip(rows, rows[1:]):
        trs.append(max(cur["high"] - cur["low"],
                       abs(cur["high"] - prev["close"]),
                       abs(cur["low"] - prev["close"])))
    atr = sum(trs[:period]) / period               # seed
    for tr in trs[period:]:                        # Wilder smoothing over the window
        atr = (atr * (period - 1) + tr) / period
    return atr

def on_bar_strategy(sdk, params):
    if sdk.position != 0:
        return
    period = int((params or {}).get("atr_period", 14))
    if len(sdk.candles) < max(20, period + 1):
        return

    atr = _atr(sdk.candles, period)   # helper slices to a bounded window internally
    if atr is None:
        return

    # High of the last 20 candles
    recent_high = max(c["high"] for c in sdk.candles[-20:])
    close = sdk.candles[-1]["close"]

    # Only enter if the price is close to the top (breakout confirmation)
    if close < recent_high * 0.995:
        return

    sdk.buy(
        action="buy_to_open",
        qty=1,
        order_type="stop",             # only buy if it breaks...
        price=recent_high + atr * 0.1, # ...slightly above the top
        stop_loss=recent_high - atr * 2,
        take_profit=recent_high + atr * 4,
    )

The _atr helper slices to candles[-(period * 4):] internally, so each call is O(period), not O(n) — a bounded window converges to the full-history Wilder ATR. Do not hand an indicator the whole sdk.candles and let it scan every bar — e.g. Indicator.rsi([c["close"] for c in sdk.candles], 14) over all of sdk.candles is O(n) per bar and O(n²) over the backtest; a frame that overruns the per-bar budget can desync the protocol and abort the whole run with a fatal ProtocolError: Failed to parse persistent strategy output JSON: data did not match any variant of untagged enum StrategyOutput (which is not covered by the timeout tolerance).

Note: Indicator, Signal and ta/pandas_ta are pre-injected globals — import tesstrade_indicators is not available in the strategy editor (the validator allows only numpy, pandas, pandas_ta, talib, math, json, datetime). Indicator has no ATR method: use the bounded-window _atr helper above (or ta.atr), or keep the Wilder ATR state incrementally in sdk.state, updating it from only the newest bar each frame — never scan the whole sdk.candles.

Common mistakes

  • order_type="limit" without price: the engine rejects. Always pass price= on a limit.
  • Using a qty that is too small in crypto Spot: the book has a minimum size (for example, 0.00001 BTC). Below that, the matching engine rejects.
  • tif="day" in 24/7 chart trading: crypto has no formal "end of day"; the engine interprets day as 24h. Use gtc for a permanent order.
  • stop_loss above the entry price on a long: on a buy (long), the stop must be below the current price. Placing it above closes the position immediately.

Next steps