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
- A Tektii account — sign up at app.tektii.com
- The CLI installed — Install
- Your API key configured — Authenticate
- Docker installed and running
You can skip the packaging below entirely and create a runnable version from a platform template:
tektii version upload a1b2c3d4-e5f6-4a1b-9c2d-3e4f5a6b7c8d --template ma-crossover
Then jump straight to running the backtest.
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.
Native builds target arm64, which the platform can't run. Enable Rosetta in Docker Desktop or build with buildx — see Apple Silicon setup.
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
- Writing a Strategy — the SDK in full: callbacks, order types, position sizing, history warm-up
- Available Instruments — every symbol and date range you can point a scenario at
- Write Workflow — the upload loop in detail: versioning, parameters, saved configurations
- Read Workflow — download the equity curve and per-trade log