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

Your First Backtest

Paste a working strategy, upload it, run it against three months of market data, and read the results. Every step is a command you can copy.

Before you start

1. Paste the strategy

A complete moving-average crossover — it streams candles, sizes each entry as a share of account equity, and submits market orders. Save it as strategy.py:

import asyncio
from collections import deque

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

SHORT_WINDOW = 10
LONG_WINDOW = 20
EQUITY_FRACTION = "0.10"


class MaCrossover:
    def __init__(self, gw: AsyncTradingGateway) -> None:
        self._gw = gw
        self._closes: deque[float] = deque(maxlen=LONG_WINDOW)
        self._long = False

    async def on_candle(self, event: CandleEvent) -> None:
        bar = event.bar  # symbol, open, high, low, close, volume, timestamp
        self._closes.append(float(bar.close))
        if len(self._closes) < LONG_WINDOW:
            return  # not enough history to compute the long SMA yet

        closes = list(self._closes)
        short_ma = sum(closes[-SHORT_WINDOW:]) / SHORT_WINDOW
        long_ma = sum(closes) / LONG_WINDOW

        if short_ma > long_ma and not self._long:
            await self._trade(bar, "buy")   # golden cross — enter long
            self._long = True
        elif short_ma < long_ma and self._long:
            await self._trade(bar, "sell")  # death cross — exit
            self._long = False

    async def _trade(self, bar, side: str) -> None:
        # quantity is a fixed instrument amount, so size by equity, not raw units
        qty = await self._gw.quantity_for_notional(
            bar.symbol, equity_fraction=EQUITY_FRACTION, price=bar.close
        )
        try:
            await self._gw.submit_order(symbol=bar.symbol, side=side, quantity=qty)
        except OrderRejectedError as err:
            print(f"order rejected code={err.code} message={err.message}")

    def on_order(self, event: OrderEvent) -> None:
        print(f"order update: {event}")  # 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 MaCrossover(gw).run()


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

Next to it, save a requirements.txt:

tektii

and a Dockerfile:

FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt

COPY strategy.py .
CMD ["python", "strategy.py"]

To try the strategy locally first, pip install tektii and run it against a local gateway — but you don't need to for this walkthrough.

2. Upload it

Create a strategy — the named container your uploads live under — and note the strategy ID it prints:

tektii strategy create --name "my-first-strategy"

Then, from the directory with your three files, upload. The CLI builds the Docker image, verifies it targets linux/amd64, and pushes it:

tektii version upload a1b2c3d4-e5f6-4a1b-9c2d-3e4f5a6b7c8d

Note the version ID from the output.

3. Run the backtest

Save this as backtest-config.json, with your version ID — it feeds the strategy three months of 1-minute Bitcoin candles:

{
  "strategyVersionId": "b2c3d4e5-f6a7-4b2c-8d3e-4f5a6b7c8d9e",
  "subscriptions": [
    { "instrument": "C:BTCUSD", "events": ["candle_1m"] }
  ],
  "startTime": "2024-01-01T00:00:00Z",
  "endTime": "2024-04-01T00:00:00Z"
}

Launch it:

tektii scenario create a1b2c3d4-e5f6-4a1b-9c2d-3e4f5a6b7c8d --config ./backtest-config.json

This returns a scenario ID immediately with state QUEUED — the run is enqueued, not finished.

4. Read the results

Check on the run until its state reaches COMPLETE (not COMPLETED):

tektii scenario get a1b2c3d4-e5f6-4a1b-9c2d-3e4f5a6b7c8d c3d4e5f6-a7b8-4c3d-9e4f-5a6b7c8d9e0f

A completed run prints the headline metrics — total return, Sharpe ratio, max drawdown, trade count, and win rate. That's your first backtest.

Make it yours