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.

Where the API reference lives

This page teaches the shape of a strategy — the event loop, submitting an order, sizing it, warming up from history. It is deliberately not a method list. The complete surface — every order parameter, plus positions, market data, account and system calls, streaming, and the error types — is the API reference in the SDK's README. It ships in the same repo as the code and is versioned with it, so it describes the package you actually installed.

That reference is the source of truth for what the SDK can do. Check it first whenever something here looks incomplete.

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.

The events this example handles

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.

The stream carries more event types than the four dispatched above; the SDK reference lists the current set. Match the ones your strategy acts on and let the rest fall through.

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.

submit_order takes considerably more than those three arguments — the order type and its prices, time-in-force, bracket legs, and extras for brokers that support them. submit_order in the SDK reference carries the current signature. Bracket legs are worth showing here because they decide how a backtest exits a position without you writing exit logic:

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