Tektii just went live. We're shipping fixes daily — spot a bug? Let us know

Writing a Strategy

A Tektii strategy is a plain Python program built on the Tektii Python SDK. It connects to a Trading Gateway, consumes a stream of market events, and submits orders in response. The same code runs unchanged in a backtest and against a live broker — the SDK handles the difference internally, so there is no mode flag in your strategy.

This page covers the strategy shape: the event loop, the callbacks, order submission, and position sizing. The complete working example is the ma_crossover template — the same image the platform provisions when you create a version with --template ma-crossover.

Install the SDK

pip install tektii

The SDK exposes two clients: AsyncTradingGateway (asyncio, recommended) and a synchronous TradingGateway. Both read the gateway address and API key from the environment, so the same binary points at a local gateway or the platform with no code change:

Env varDefaultPurpose
TRADING_GATEWAY_URLhttp://localhost:8080Gateway base URL
TRADING_GATEWAY_API_KEY(unset)API key for remote gateways

Both also have TEKTII_-prefixed variants (TEKTII_TRADING_GATEWAY_URL, TEKTII_TRADING_GATEWAY_API_KEY) that take precedence over the unprefixed names when both are set.

The strategy shape

The canonical shape is a stateful class plus an event loop. You open the client, enter the stream, and dispatch each event to a handler:

import asyncio
from tektii import (
    AsyncTradingGateway,
    CandleEvent,
    ConnectionEvent,
    ErrorEvent,
    OrderEvent,
)

class MyStrategy:
    def __init__(self, gw: AsyncTradingGateway) -> None:
        self._gw = gw

    async def on_candle(self, event: CandleEvent) -> None:
        bar = event.bar  # symbol, open, high, low, close, volume, timestamp
        ...              # indicator updates + entry/exit decisions go here

    def on_order(self, event: OrderEvent) -> None:
        ...              # fills, cancellations, rejects

    async def run(self) -> None:
        async with self._gw.stream() as events:
            async for event in events:
                match event:
                    case CandleEvent():
                        await self.on_candle(event)
                    case OrderEvent():
                        self.on_order(event)
                    case ConnectionEvent(event=ev, broker=broker, error=err):
                        print(f"connection {ev} broker={broker} error={err}")
                    case ErrorEvent(code=code, message=msg):
                        print(f"gateway error {code}: {msg}")

async def main() -> None:
    async with AsyncTradingGateway() as gw:
        await MyStrategy(gw).run()

if __name__ == "__main__":
    asyncio.run(main())

The events you receive are the ones the run subscribes to. In a backtest, the scenario configuration's subscriptions decide which instrument and candle timeframe stream into your strategy — see the scenario configuration reference.

Key callbacks

EventHandler conventionFires when
CandleEventon_candleA bar closes on a subscribed timeframe. event.bar carries OHLCV + symbol.
OrderEventon_orderAn order changes state — submitted, filled, cancelled, rejected.
ConnectionEventinlineThe gateway's broker connection opens, closes, or errors.
ErrorEventinlineThe gateway reports an error outside any one order.

Submitting orders

submit_order places an order and returns an OrderHandle (id + status). The minimal call is symbol, side, and quantity — a market order:

handle = await gw.submit_order(
    symbol=bar.symbol,
    side="buy",            # or "sell"
    quantity=qty,          # Decimal or string — a fixed instrument amount
)

side is case-insensitive — "buy" / "sell" and "BUY" / "SELL" are equivalent. If you prefer the typed enum, from tektii import Side and pass Side.BUY / Side.SELL; its members carry the upper-case values BUY and SELL.

The most common optional parameters are order_type ("market", "limit", "stop", "stop_limit", "trailing_stop"), limit_price, stop_price, time_in_force, and bracket legs; the SDK exposes more — trailing distances, OCO groups, reduce-only, leverage — for brokers that support them:

handle = await gw.submit_order(
    symbol=bar.symbol,
    side="buy",
    quantity=qty,
    stop_loss=sl_price,     # attach a stop-loss to the entry
    take_profit=tp_price,   # attach a take-profit to the entry
)

A rejected order raises OrderRejectedError — catch it and decide whether to retry, resize, or stay flat:

from tektii import OrderRejectedError

try:
    handle = await gw.submit_order(symbol=bar.symbol, side="buy", quantity=qty)
except OrderRejectedError as err:
    log.warning("order rejected code=%s message=%s", err.code, err.message)
    return  # stay flat

Position sizing — quantity_for_notional

An order quantity is a fixed instrument amount, not a share of capital — 0.01 means 0.01 BTC regardless of your account size. On the default ~100k starting capital that is a near-zero position and produces a flat, meaningless backtest. Size your orders with the SDK helper instead:

# Target a share of account equity (10% here):
qty = await gw.quantity_for_notional(bar.symbol, equity_fraction="0.10")

# Or a fixed cash amount in the account currency:
qty = await gw.quantity_for_notional(bar.symbol, notional="5000")

# Pass the bar close as the reference price to skip a separate quote request:
qty = await gw.quantity_for_notional(
    bar.symbol, equity_fraction="0.10", price=bar.close
)

Provide exactly one of notional or equity_fraction. The helper reads account equity (for equity_fraction) and divides by the reference price — the quote midpoint by default, or the price you pass. It returns a full-precision Decimal ready for submit_order; round it to the venue's lot size when trading a real broker.

Warming up from history

Indicators need a window of bars before they produce a signal. Rather than discarding the first N live bars, back-fill from history with get_bars:

bars = await gw.get_bars(symbol, "1m", limit=25)
for bar in bars:
    update_indicators(bar.close)

The ma_crossover template runs this lazily on the first candle — the strategy is symbol-agnostic, so it only learns which instrument to back-fill once the stream delivers the first bar.

The complete example: ma_crossover

The ma_crossover template is the canonical reference strategy: a trend-follower that buys when a short SMA crosses above a long SMA (golden cross) and exits on the reverse (death cross). One position at a time; market-order entries with optional bracket stop-loss / take-profit.

It demonstrates every pattern on this page in ~450 lines:

  • A frozen Config dataclass parsed from environment variables, validated at startup
  • A small state machine (FLAT / LONG) that survives partial fills and broker rejects
  • Pure, unit-testable indicator helpers
  • Lazy history warm-up via get_bars
  • Equity-fraction sizing via quantity_for_notional
  • Bracket orders and OrderRejectedError handling
  • Clean SIGTERM shutdown

It also ships a full unit-test suite (test_strategy.py) — the reference for testing the pure indicator and state-machine logic without a gateway.

Its sibling, rsi_momentum, applies the same shape to a mean-reversion RSI strategy.

Template environment variables

The template reads all of its parameters from environment variables:

NameDefaultDescription
ORDER_EQUITY_FRACTION0.10Position size as a fraction of account equity (0.10 = 10%), sized at the signal-bar price on each entry
MA_SHORT10Short SMA period, in bars
MA_LONG20Long SMA period, in bars
TIMEFRAME1mBar resolution for the warm-up backfill; match it to the run's subscription timeframe
STOP_LOSS_PCT(unset)e.g. 0.02 attaches a 2% stop-loss to entries
TAKE_PROFIT_PCT(unset)e.g. 0.04 attaches a 4% take-profit to entries
LOG_LEVELINFOPython logging level
TRADING_GATEWAY_URLhttp://localhost:8080Gateway base URL (read by the SDK)
TRADING_GATEWAY_API_KEY(unset)API key for remote gateways (read by the SDK)

Run it locally

You can develop against a local gateway with the mock provider — no broker credentials and no platform account needed:

docker run -e GATEWAY_PROVIDER=mock -p 8080:8080 ghcr.io/tektii/gateway:latest

Then run your strategy directly:

python strategy.py

The SDK's defaults point at http://localhost:8080, so no configuration is required for the local loop.

Next steps

  • Write Workflow — package your strategy in Docker, upload it as a version, and run a backtest
  • Available Instruments — the symbols and date ranges you can subscribe to
  • Read Workflow — pull the equity curve, trades, and metrics after a run