Skip to content

Full-stack starter

A complete, runnable bundle: every file you need to deploy two strategies live on a real broker, through Docker, with risk discipline. Clone the structure, edit credentials, run.

This is what a production-shape qkt deployment looks like. The recipe pages cover individual tasks; this page is the whole picture.

What's in the bundle

my-trading-stack/
├── .env                          # broker credentials (gitignored)
├── .env.example                  # template for new contributors
├── docker-compose.yml            # qkt + mt5-gateway + Bybit venue gateway services
├── qkt.config.yaml               # broker profiles + risk rules
├── strategies/
│   ├── btc-trend.qkt             # Bybit-side strategy
│   └── eur-meanrev.qkt           # MT5-side strategy
└── reports/                      # backtest outputs (gitignored)

Every file is real. Copy-paste into a fresh directory and follow the walkthrough.

.env.example

.env.example
# Copy this to `.env` and fill in real values.
# .env is gitignored — never commit credentials.

# ---- Bybit (through the qkt-venue-gateway's Bybit adapter) ----
# Generate an API key at https://www.bybit.com/app/user/api-management
# Permissions: Read + Trade. NO Withdrawal. Set spending limits.
BYBIT_API_KEY=
BYBIT_API_SECRET=
BYBIT_ENVIRONMENT=testnet          # testnet first; mainnet is real money
BYBIT_TRADE_MODE=demo              # what qkt expects the gateway to report: demo, or real on mainnet
BYBIT_ACCOUNT_LOGIN=               # the account_login the gateway's /v1/health reports
BYBIT_TRADER_TOKEN=                # any long random strings (openssl rand -hex 32)
BYBIT_GUARDIAN_TOKEN=

# ---- Exness MT5 (demo) ----
# Get demo credentials from https://my.exness.com/
MT5_LOGIN=
MT5_PASSWORD=
MT5_SERVER=Exness-MT5Trial
MT5_ENABLE_ALGO_TRADING=1
MT5_API_KEY=replace-with-a-long-random-value

# ---- VNC for one-time MT5 login ----
# Pick anything; you'll use this when you connect to the gateway via VNC.
VNC_PASSWORD=

# ---- Internal (don't change unless you know why) ----
QKT_BROKER_EXNESS_GATEWAY_URL=http://mt5-gateway:5001

docker-compose.yml

docker-compose.yml
version: '3.9'

services:
  gateway-bybit-spot:
    # The qkt-venue-gateway, built from github.com/elitekaycy/qkt-venue-gateway.
    image: qkt-venue-gateway:local
    build: https://github.com/elitekaycy/qkt-venue-gateway.git#main
    environment:
      GATEWAY_ADAPTER: bybit
      GATEWAY_SETTING_CATEGORY: spot
      GATEWAY_SETTING_ENVIRONMENT: ${BYBIT_ENVIRONMENT}
      GATEWAY_LOGIN: ${BYBIT_API_KEY}
      GATEWAY_SECRET: ${BYBIT_API_SECRET}
      GATEWAY_TRADER_TOKEN: ${BYBIT_TRADER_TOKEN}
      GATEWAY_GUARDIAN_TOKEN: ${BYBIT_GUARDIAN_TOKEN}
    volumes:
      - bybit-spot-state:/data
    restart: unless-stopped
    networks: [qkt-net]

  mt5-gateway:
    image: elitekaycy/mt5-gateway-api:0.3.2
    container_name: mt5-gateway
    environment:
      MT5_LOGIN: ${MT5_LOGIN}
      MT5_PASSWORD: ${MT5_PASSWORD}
      MT5_SERVER: ${MT5_SERVER}
      MT5_ENABLE_ALGO_TRADING: ${MT5_ENABLE_ALGO_TRADING:-1}
      API_KEY: ${MT5_API_KEY}
      PASSWORD: ${VNC_PASSWORD}
    ports:
      - "3000:3000"        # VNC diagnostic fallback
      - "5001:5001"        # HTTP API
    healthcheck:
      test: ["CMD-SHELL", "curl -fsS -H \"Authorization: Bearer $$API_KEY\" http://localhost:5001/health/ready"]
      interval: 30s
      timeout: 5s
      retries: 6
      start_period: 60s
    restart: unless-stopped
    networks: [qkt-net]

  qkt:
    image: ghcr.io/elitekaycy/qkt:latest
    container_name: qkt
    depends_on:
      mt5-gateway:
        condition: service_healthy
      gateway-bybit-spot:
        condition: service_healthy
    environment:
      BYBIT_TRADER_TOKEN: ${BYBIT_TRADER_TOKEN}
      BYBIT_ACCOUNT_LOGIN: ${BYBIT_ACCOUNT_LOGIN}
      BYBIT_TRADE_MODE: ${BYBIT_TRADE_MODE}
      QKT_BROKER_EXNESS_GATEWAY_URL: ${QKT_BROKER_EXNESS_GATEWAY_URL}
      QKT_BROKER_EXNESS_API_KEY: ${MT5_API_KEY}
      QKT_STATE_DIR: /var/lib/qkt
    volumes:
      - ./strategies:/strategies:ro
      - ./qkt.config.yaml:/etc/qkt/qkt.config.yaml:ro
      - ./reports:/var/lib/qkt/reports
      - qkt-state:/var/lib/qkt
    ports:
      - "47999:47999"                  # daemon control plane (internal)
    command:
      - daemon
      - --config
      - /etc/qkt/qkt.config.yaml
      - --load-dir
      - /strategies
    restart: unless-stopped
    networks: [qkt-net]

networks:
  qkt-net:
    driver: bridge

volumes:
  qkt-state:
  bybit-spot-state:

qkt.config.yaml

qkt.config.yaml
# qkt configuration — broker profiles + risk rules.
# Edit the values, restart the daemon to pick up changes.

source: live                          # 'live' or 'backtest'
starting_balance: 10000               # USD (paper accounting baseline)
log_level: info                       # debug | info | warn | error

# ---- Broker profiles ----------------------------------------------------
# Each key here becomes a prefix usable in strategy SYMBOLS blocks:
#   SYMBOLS
#       btc = BYBIT_SPOT:BTCUSDT EVERY 1h
brokers:

  # Bybit Spot — through the venue gateway running its Bybit adapter (gateway-bybit-spot in compose).
  # The entry name is the prefix: bybit_spot serves BYBIT_SPOT:... See https://github.com/elitekaycy/qkt-venue-gateway/blob/main/adapter-bybit/README.md
  bybit_spot:
    type: gateway
    gateway_url: http://gateway-bybit-spot:8443
    api_key: env:BYBIT_TRADER_TOKEN
    expected_adapter: bybit
    expected_account_login: "${BYBIT_ACCOUNT_LOGIN}"
    expected_trade_mode: ${BYBIT_TRADE_MODE}

  # Exness MT5 — via mt5-gateway service in compose
  exness:
    type: mt5
    extends: exness                   # inherits built-in suffix + tz settings
    gateway_url: ${QKT_BROKER_EXNESS_GATEWAY_URL}
    api_key: ${QKT_BROKER_EXNESS_API_KEY}
    magic: 4242                       # unique per qkt instance
    deviation_points: 30              # max slippage on market orders

# ---- Account-level risk rules ------------------------------------------
# These apply across every strategy hosted in the daemon.
risk:
  rules:
    # Stop trading on a bad day (auto-resets at UTC midnight)
    - type: max-daily-loss
      pct: 3.0
      reset: daily

    # Permanent halt on deep drawdown (manual operator resume)
    - type: max-drawdown
      pct: 10.0
      reset: manual

    # Reject single trades that would exceed 5% of equity
    - type: max-position-pct
      pct: 5.0

    # Cap simultaneous open positions across all strategies
    - type: max-open-positions
      count: 5

strategies/btc-trend.qkt

strategies/btc-trend.qkt
-- BTC trend-follow on Bybit Spot, 1h
-- Risk: 1% per trade, ATR-sized stop

STRATEGY btc_trend VERSION 1

LET fast = 12
LET slow = 48

# Manual risk-sizing — Phase 24 will add `SIZING 1.0 PCT RISK` as a shortcut
LET btcStopDist = atr(btc, 14) * 2
LET btcRiskQty  = ACCOUNT.equity * 0.01 / btcStopDist     # 1% risk per trade

SYMBOLS
    btc = BYBIT_SPOT:BTCUSDT EVERY 1h

RULES
    WHEN ema(btc.close, fast) CROSSES ABOVE ema(btc.close, slow)
     AND POSITION.btc = 0
    THEN BUY btc SIZING btcRiskQty
         STOP_LOSS AT btc.close - btcStopDist
         LOG "long entry"
             fast=ema(btc.close, fast)
             slow=ema(btc.close, slow)
             atr=atr(btc, 14)

    WHEN ema(btc.close, fast) CROSSES BELOW ema(btc.close, slow)
     AND POSITION.btc > 0
    THEN CLOSE btc
         LOG "exit on cross-below"

strategies/eur-meanrev.qkt

strategies/eur-meanrev.qkt
-- EUR/USD mean-reversion on Exness MT5, 15m
-- Risk: 1% per trade, scale-out at 3 targets

STRATEGY eur_meanrev VERSION 1

SYMBOLS
    eur = EXNESS:EURUSD EVERY 15m

LET eurStopDist = atr(eur, 14) * 2
LET eurRiskQty  = ACCOUNT.equity * 0.01 / eurStopDist     # 1% risk per trade

RULES
    WHEN rsi(eur.close, 2) < 10
     AND eur.close > sma(eur.close, 200)        -- trend filter
     AND POSITION.eur = 0
    THEN BUY eur SIZING eurRiskQty
         BRACKET {
           STOP_LOSS AT eur.close - eurStopDist,
           TAKE_PROFIT {
             0.33 AT eur.close * 1.001,
             0.33 AT eur.close * 1.002,
             0.34 AT eur.close * 1.004
           }
         }
         LOG "oversold entry" rsi=rsi(eur.close, 2)

See also