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 var | Default | Purpose |
|---|---|---|
TRADING_GATEWAY_URL | http://localhost:8080 | Gateway 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.
An SDK's reference lives in that SDK's own repo, next to the code it documents — Python is trading-gateway-sdk-python. As more languages ship, each brings its own reference rather than a copy in these docs. What stays here is the part that doesn't change with the language: what a strategy does and the platform rules it runs under.
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
| Event | Handler convention | Fires when |
|---|---|---|
CandleEvent | on_candle | A bar closes on a subscribed timeframe. event.bar carries OHLCV + symbol. |
OrderEvent | on_order | An order changes state — submitted, filled, cancelled, rejected. |
ConnectionEvent | inline | The gateway's broker connection opens, closes, or errors. |
ErrorEvent | inline | The 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.
The instrument is not configured in your code — it arrives on the stream. Read event.bar.symbol off the first candle and trade that. One uploaded image then runs against every instrument and timeframe you point a scenario at, with no rebuild. Both reference templates work this way.
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
Configdataclass 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
OrderRejectedErrorhandling - Clean
SIGTERMshutdown
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.
Both templates are built into the platform. Create a runnable version directly from one — no Docker build required:
tektii version upload a1b2c3d4-e5f6-4a1b-9c2d-3e4f5a6b7c8d --template ma-crossover
Then configure and run a backtest against it as in the write workflow.
Template environment variables
The template reads all of its parameters from environment variables:
| Name | Default | Description |
|---|---|---|
ORDER_EQUITY_FRACTION | 0.10 | Position size as a fraction of account equity (0.10 = 10%), sized at the signal-bar price on each entry |
MA_SHORT | 10 | Short SMA period, in bars |
MA_LONG | 20 | Long SMA period, in bars |
TIMEFRAME | 1m | Bar 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_LEVEL | INFO | Python logging level |
TRADING_GATEWAY_URL | http://localhost:8080 | Gateway base URL (read by the SDK) |
TRADING_GATEWAY_API_KEY | (unset) | API key for remote gateways (read by the SDK) |
ENV sets parameter defaults — override them per runBake your strategy's default parameters into the image with ENV in your Dockerfile:
ENV MA_SHORT=15 ENV MA_LONG=50 ENV STOP_LOSS_PCT=0.02
These are the defaults — they're no longer the only way to set a parameter. Each scenario config and each run takes a per-run env map that layers on top of the baked ENV at launch, so you can sweep MA_SHORT / MA_LONG / STOP_LOSS_PCT across runs against one image, with no rebuild. See per-run env overrides in the write workflow. (The instrument is not a parameter — it comes from the run's subscriptions, see the agnostic-strategy note above.)
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
- Python SDK reference — the complete method surface, versioned with the package
- 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