QuantDinger QuantDinger Docs
Home GitHub Launch App
QuantDinger v3.0.20

QuantDinger Documentation

Self-hosted AI trading platform for quant research, Python strategy development, backtesting, and live execution. One stack for AI analysis, charting, strategy code, backtests, quick trade, and live operations — on infrastructure you fully control.

GHCR prebuilt images

Pull quantdinger-backend and quantdinger-frontend from GHCR — no Node.js, no local frontend build.

Python-native

Write strategies in pandas dataframes or event-driven on_bar scripts.

AI built into the loop

OpenAI, Claude, Gemini, DeepSeek, Grok — analysis, generation, ensemble, calibration.

Mobile + Web + Native

Full mobile H5, Android Capacitor build, Vue desktop — all talk to the same API.

Introduction #

QuantDinger is a self-hosted quantitative trading and algorithmic trading platform for AI-assisted research, Python strategy development, backtesting, and live execution. Your credentials, strategy code, market workflows, and operational data stay under your control.

Capabilities include:

  • AI market analysis — structured, low-latency analysis with memory, ensemble voting, and confidence calibration across OpenAI, Claude, Gemini, DeepSeek, Grok and more.
  • Python strategy development — four-way IndicatorStrategy (open_long / close_long / open_short / close_short) with # @strategy risk defaults, or event-driven ScriptStrategy with explicit ctx.open_* / ctx.close_* intents. AI can draft a starting point; you stay in control of the code.
  • Deterministic backtesting — commission and slippage modeling, trade-by-trade analytics, equity curves. Optional strict_mode aligns fills with live execution semantics.
  • AI agents (MCP)quantdinger-mcp 0.2.0 on PyPI exposes 26 scoped tools (read, workspace, indicators, backtest, SSE jobs) against /api/agent/v1.
  • Autonomous trading bots — Grid, Martingale, Trend Following, and DCA. Execution-aware, restart-resilient, signal or live execution modes.
  • Operator-ready — multi-user PostgreSQL, role-based access, Google / GitHub OAuth, memberships, credits, USDT payments, Telegram / Email / SMS / Discord / Webhook alerts.

Architecture #

QuantDinger runs as a self-hosted application stack:

LayerTechnology
FrontendPrebuilt Vue SPA in ghcr.io/brokermr810/quantdinger-frontend (Nginx + API proxy via BACKEND_URL)
BackendFlask API in ghcr.io/brokermr810/quantdinger-backend (or built locally from backend_api_python/)
StoragePostgreSQL 16
Cache / worker supportRedis 7
Trading layerExchange adapters, IBKR, MT5
AI layerLLM provider integration, memory, calibration, optional workers
BillingMembership, credits, USDT TRC20 payment flow
DeploymentDocker Compose — docker-compose.ghcr.yml (zero-clone) or docker-compose.yml (dev / patches)

Execution model

  • Market data is pulled through a pluggable data layer.
  • Backtests run on the server-side strategy engine, including strategy snapshot handling.
  • Live strategies run through runtime services that generate order intent.
  • Pending orders are dispatched through exchange-specific execution adapters.
  • Crypto live execution is intentionally separated from market-data collection concerns.

Deployment paths #

QuantDinger v3.0.20 supports two first-class install paths. Pick based on whether you need the full Git repository on the server.

PathCompose fileBest for
A · GHCR pull (recommended) docker-compose.ghcr.yml Production VPS, 1Panel, Railway-style deploys — only two files + backend.env
B · Clone repo docker-compose.yml Developers patching backend Python, reading migrations, or using docker-compose.build.yml for local Vue builds

Image sources (both paths):

  • ghcr.io/brokermr810/quantdinger-frontend:<tag> — prebuilt UI (source lives in QuantDinger-Vue)
  • ghcr.io/brokermr810/quantdinger-backend:<tag> — API image (GHCR path) or local Docker build (clone path)
  • Pin releases with IMAGE_TAG=3.0.20 in project-root .env (semver tags on GHCR omit the leading v)

Full cloud deployment guide (Chinese): docs/CLOUD_DEPLOYMENT_CN.md · English: CLOUD_DEPLOYMENT_EN.md

Quick Start #

Fastest path: pull prebuilt GHCR images. No repository clone required.

bash
mkdir quantdinger && cd quantdinger

curl -fsSLO https://raw.githubusercontent.com/brokermr810/QuantDinger/main/docker-compose.ghcr.yml
curl -fsSLO https://raw.githubusercontent.com/brokermr810/QuantDinger/main/backend_api_python/env.example \
  -o backend.env

# optional: pin release
echo 'IMAGE_TAG=3.0.20' >> .env

docker compose -f docker-compose.ghcr.yml up -d
docker compose -f docker-compose.ghcr.yml ps

The backend entrypoint auto-generates SECRET_KEY on first boot and writes it back into backend.env. Edit admin password and API keys there, then restart backend.

bash
git clone https://github.com/brokermr810/QuantDinger.git
cd QuantDinger
cp backend_api_python/env.example backend_api_python/.env
./scripts/generate-secret-key.sh
docker compose up -d --build
powershell
git clone https://github.com/brokermr810/QuantDinger.git
cd QuantDinger
Copy-Item backend_api_python\env.example -Destination backend_api_python\.env
$key = py -c "import secrets; print(secrets.token_hex(32))"
(Get-Content backend_api_python\.env) -replace '^SECRET_KEY=.*$', "SECRET_KEY=$key" | Set-Content backend_api_python\.env -Encoding UTF8
docker compose up -d --build

After startup:

  • Frontend UI: http://localhost:8888
  • Backend health: http://localhost:5000/api/health (or via frontend proxy /api/health)
  • Default login: quantdinger / 123456 — change before exposing to the internet
Before production
Regenerate SECRET_KEY, change ADMIN_PASSWORD, bind services to 127.0.0.1, and terminate TLS at host Nginx (see Recommended topology).

GHCR install (recommended) #

Uses docker-compose.ghcr.yml. You only need:

  • docker-compose.ghcr.yml
  • backend.env — runtime secrets & feature flags (bind-mounted as /app/.env)
  • Optional project-root .env — ports, IMAGE_TAG, registry mirrors

Important: backend.env must be a regular file. If Docker creates an empty directory at that path, the backend will not read your config.

Production port binding

ini
# project-root .env (recommended on VPS)
FRONTEND_PORT=127.0.0.1:8888
BACKEND_PORT=127.0.0.1:5000
DB_PORT=127.0.0.1:5432
IMAGE_TAG=3.0.20
IMAGE_PREFIX=

Schema migrations: unlike the clone path, GHCR backend re-applies migrations/init.sql idempotently on every start — no host SQL mount required.

Clone & compose #

Uses docker-compose.yml from the repository:

  • Backend — built from backend_api_python/Dockerfile (local patches apply immediately with --build)
  • Frontend — pulled from GHCR by default; override with docker-compose.build.yml if you clone QuantDinger-Vue locally
  • Configbackend_api_python/.env (not backend.env)
bash
docker compose up -d --build          # default: GHCR frontend + local backend build
docker compose down                 # stop; add -v to wipe volumes
docker compose logs -f backend

Local frontend build (optional)

Clone QuantDinger-Vue into ./QuantDinger-Vue/, then:

bash
docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build

Prerequisites #

  • Docker 20+ and Docker Compose v2 (docker compose)
  • Host with at least 2 vCPU / 4 GB RAM / 20 GB disk for small teams
  • Node.js is not required for normal deployment — the UI ships as a GHCR image
  • (Production) Host Nginx, Caddy, or a panel (1Panel / OpenResty) on 80/443 proxying to 127.0.0.1:8888

Common commands #

bash
docker compose ps
docker compose logs -f backend
docker compose restart backend
docker compose up -d --build
docker compose exec backend bash
Faster rebuilds
If you only changed backend env (backend.env or backend_api_python/.env), run docker compose restart backend — no rebuild needed.

Compose .env knobs #

Project-root .env controls Docker Compose substitution — ports, image tags, registry mirrors. It is separate from backend runtime config.

FileUsed byContains
backend.envGHCR pathSECRET_KEY, API keys, OAuth, billing, workers
backend_api_python/.envClone pathSame runtime keys, bind-mounted into backend container
.env (project root)docker composeIMAGE_TAG, FRONTEND_PORT, BACKEND_URL, IMAGE_PREFIX
ini
# Production — bind to localhost; host Nginx terminates TLS
FRONTEND_PORT=127.0.0.1:8888
BACKEND_PORT=127.0.0.1:5000
DB_PORT=127.0.0.1:5432

# Pin GHCR release (optional; default pulls :latest)
IMAGE_TAG=3.0.20
# FRONTEND_TAG=3.0.20
# BACKEND_TAG=3.0.20

# Slow Docker Hub? pick a mirror prefix:
# IMAGE_PREFIX=docker.m.daocloud.io/library/

# Frontend container → backend API (default inside compose network)
BACKEND_URL=http://backend:5000
# Host-only backend (1Panel split proxy): BACKEND_URL=http://172.17.0.1:5000

Configuration overview #

Runtime configuration lives in backend.env (GHCR) or backend_api_python/.env (clone). Copy from backend_api_python/env.example in the repo.

AreaExample keys
AuthenticationSECRET_KEY, ADMIN_USER, ADMIN_PASSWORD
DatabaseDATABASE_URL
LLM / AILLM_PROVIDER, OPENAI_API_KEY, OPENROUTER_API_KEY
OAuthGOOGLE_CLIENT_ID, GITHUB_CLIENT_ID
SecurityTURNSTILE_SITE_KEY, ENABLE_REGISTRATION
BillingBILLING_ENABLED, BILLING_COST_AI_ANALYSIS
MembershipMEMBERSHIP_MONTHLY_PRICE_USD, MEMBERSHIP_MONTHLY_CREDITS
USDT paymentsUSDT_PAY_ENABLED, USDT_TRC20_XPUB, TRONGRID_API_KEY
ProxyPROXY_URL
WorkersENABLE_PENDING_ORDER_WORKER, ENABLE_PORTFOLIO_MONITOR, ENABLE_REFLECTION_WORKER
AI tuningENABLE_AI_ENSEMBLE, ENABLE_CONFIDENCE_CALIBRATION, AI_ENSEMBLE_MODELS

Authentication #

The SECRET_KEY signs all JWT tokens. It must be a cryptographically-random 64-char hex string — the backend refuses to start with the default placeholder.

env
SECRET_KEY=<run: python -c "import secrets; print(secrets.token_hex(32))">
ADMIN_USER=quantdinger
ADMIN_PASSWORD=change-me-before-going-live

# Token lifetime in seconds (default 7 days)
JWT_EXPIRATION=604800
Rotating SECRET_KEY
Rotating the key invalidates every user token immediately. All logged-in users will be forced to sign in again. Do this before launch, not during.

Database #

Point DATABASE_URL at PostgreSQL 16. The default value targets the in-stack postgres service from Docker Compose:

env
DATABASE_URL=postgresql://quantdinger:quantdinger@postgres:5432/quantdinger

Migrations run automatically on startup from backend_api_python/migrations/init.sql. For managed database providers (Supabase, Neon, AWS RDS…), replace the URL and make sure inbound connections are allowed from your host.

AI providers #

QuantDinger ships adapters for several LLM providers. Pick any subset that matches your budget and compliance posture.

ProviderKeyTypical use
OpenRouterOPENROUTER_API_KEYAggregator — best first choice
OpenAIOPENAI_API_KEYGPT-4o / GPT-5 family
AnthropicCLAUDE_API_KEYClaude 3.x / 4.x
GoogleGEMINI_API_KEYGemini 1.5 / 2.0
DeepSeekDEEPSEEK_API_KEYCost-effective reasoning
xAIGROK_API_KEYGrok family

Ensemble and calibration

For teams that want more robust AI outputs, turn on ensemble voting and confidence calibration:

env
ENABLE_AI_ENSEMBLE=true
AI_ENSEMBLE_MODELS=openai:gpt-4o,anthropic:claude-3-5-sonnet,google:gemini-2.0-flash
ENABLE_CONFIDENCE_CALIBRATION=true

Google / GitHub OAuth #

OAuth keeps the login experience modern and removes password friction. Register an application on each provider, then set:

env
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
GITHUB_CLIENT_ID=...
GITHUB_CLIENT_SECRET=...

# Whitelist of frontend origins the backend will redirect back to after OAuth.
# Include every domain you serve the app from (PC, mobile, staging).
OAUTH_ALLOWED_REDIRECTS=https://ai.quantdinger.com,https://m.quantdinger.com

# Primary frontend URL for redirect fallback
FRONTEND_URL=https://ai.quantdinger.com
Callback URL
The callback URL registered with Google / GitHub is always the backend's /api/auth/oauth/<provider>/callback, not the frontend URL. The backend then redirects back to the frontend origin listed in OAUTH_ALLOWED_REDIRECTS.

If the mobile app is served on a different origin, make sure that origin is added to OAUTH_ALLOWED_REDIRECTS before you test — otherwise the backend will redirect users to the fallback FRONTEND_URL.

Cloudflare Turnstile #

Turnstile gates the login, register, and password-reset flows to keep bots out. Enable it once you go public:

env
TURNSTILE_ENABLED=true
TURNSTILE_SITE_KEY=0x4AAA...
TURNSTILE_SECRET_KEY=0x4AAA...
CSP must allow challenges.cloudflare.com
If you added a Content-Security-Policy header (via Nginx or Cloudflare), the widget will fail to load. Whitelist https://challenges.cloudflare.com for both script-src and frame-src.

Billing & membership #

The backend ships with first-class memberships, credits, and per-action pricing. Start with a conservative default, then adjust as you see real usage:

env
BILLING_ENABLED=true
BILLING_COST_AI_ANALYSIS=1
BILLING_COST_STRATEGY_GEN=3
BILLING_COST_BACKTEST=2

MEMBERSHIP_MONTHLY_PRICE_USD=29
MEMBERSHIP_MONTHLY_CREDITS=1500
MEMBERSHIP_YEARLY_PRICE_USD=299
MEMBERSHIP_YEARLY_CREDITS=21000

USDT payments #

The USDT TRC20 flow generates a unique deposit address per user and polls TronGrid for confirmations. Fund credits are issued automatically when the network confirms.

env
USDT_PAY_ENABLED=true
USDT_TRC20_XPUB=xpub6...    # BIP-32 extended public key (TRC20 HD derivation)
TRONGRID_API_KEY=...
USDT_CONFIRMATIONS_REQUIRED=12
Hot-wallet hygiene
The xpub only produces receiving addresses — never the corresponding private keys. Keep the matching cold wallet on an offline machine and sweep balances out periodically.

Notifications #

Each user can wire their own Telegram / Email / SMS / Discord / Webhook from the in-app Profile → Notifications page. The system-level fallbacks (admin alerts, cron digests) are configured here:

env
# Email (SMTP)
SMTP_HOST=smtp.zoho.com
SMTP_PORT=465
SMTP_USER=alerts@yourdomain.com
SMTP_PASSWORD=...
SMTP_FROM=QuantDinger <alerts@yourdomain.com>

# Telegram (used for admin alerts; per-user bots are set in app)
TELEGRAM_BOT_TOKEN=...
TELEGRAM_ADMIN_CHAT_ID=...

# SMS (Twilio-compatible)
SMS_PROVIDER=twilio
TWILIO_ACCOUNT_SID=...
TWILIO_AUTH_TOKEN=...
TWILIO_FROM_NUMBER=+1555...

Worker toggles #

Background workers run inside the backend container. Disable any that you do not need — they save CPU and reduce startup noise:

env
ENABLE_PENDING_ORDER_WORKER=true    # watches pending limit orders across exchanges
ENABLE_PORTFOLIO_MONITOR=true       # P&L, risk, alert triggers
ENABLE_REFLECTION_WORKER=false      # long-horizon AI self-review
ENABLE_BACKTEST_QUEUE=true          # async backtest runner

Strategy Development #

QuantDinger supports two complementary strategy authoring models. Pick the one that matches the shape of your logic:

IndicatorStrategy

Dataframe-based Python. Compute indicators, then expose four-way execution columns (open_long / close_long / open_short / close_short) with edge triggering — the platform default for backtest/live alignment.

ScriptStrategy

Event-driven on_init / on_bar. Use explicit four-way intents — ctx.open_long, ctx.close_long, ctx.open_short, ctx.close_short — with hedge-aware ctx.position. Same runtime for backtest and live.

IndicatorStrategy #

IndicatorStrategy runs on a pandas df: compute indicators, declare risk defaults with # @strategy, and output an output dict for chart overlays. Execution follows the Signal & Execution Standard v1.

Signal forms

FormExecution columnsWhen to use
B · Four-way (platform default) open_long, close_long, open_short, close_short New strategies, explicit long/short exits, flip/close semantics, backtest ↔ live queue alignment
A · Two-way (legacy) buy, sell Simple crossovers; meaning depends on tradeDirection (long / short / both). Do not mix A and B columns in one script.

MUST for all forms: df = df.copy() first; bool execution columns with fillna(False).astype(bool); edge trigger (see below); no shift(-1) or future data.

Four-way template (EMA crossover)

Same structure as the IDE default template — golden cross opens long / closes short, death cross the opposite, with engine-managed stop-loss / take-profit via # @strategy:

python
# QuantDinger default template — Form B (four-way) · contract v1
# signal_form: four_way    exit_owner: engine    flip_mode: R2

my_indicator_name = "EMA Crossover (Four-Way)"
my_indicator_description = "Dual EMA cross with explicit open/close legs and edge triggering."

# @strategy stopLossPct 0.03
# @strategy takeProfitPct 0.06
# @strategy entryPct 0.25
# @strategy trailingEnabled false
# @strategy tradeDirection both

# @param fast_period int 10 Fast EMA length
# @param slow_period int 30 Slow EMA length

def edge(s):
    """Fire only on false→true transitions."""
    s = s.fillna(False).astype(bool)
    return s & ~s.shift(1).fillna(False)

fast_period = int(params.get("fast_period", 10))
slow_period = int(params.get("slow_period", 30))

df = df.copy()

ema_fast = df["close"].ewm(span=fast_period, adjust=False).mean()
ema_slow = df["close"].ewm(span=slow_period, adjust=False).mean()

golden = (ema_fast > ema_slow) & (ema_fast.shift(1) <= ema_slow.shift(1))
death  = (ema_fast < ema_slow) & (ema_fast.shift(1) >= ema_slow.shift(1))

# Flip bar: close opposite leg, then open (R2)
raw_open_long   = golden
raw_open_short  = death
raw_close_long  = death
raw_close_short = golden

df["open_long"]   = edge(raw_open_long)
df["open_short"]  = edge(raw_open_short)
df["close_long"]  = edge(raw_close_long)
df["close_short"] = edge(raw_close_short)

n = len(df)
open_long_marks = [
    df["low"].iloc[i] * 0.995 if bool(df["open_long"].iloc[i]) else None for i in range(n)
]
open_short_marks = [
    df["high"].iloc[i] * 1.005 if bool(df["open_short"].iloc[i]) else None for i in range(n)
]

output = {
    "name": my_indicator_name,
    "plots": [
        {"name": f"EMA{fast_period}", "data": ema_fast.fillna(0).tolist(),
         "color": "#FF9800", "overlay": True},
        {"name": f"EMA{slow_period}", "data": ema_slow.fillna(0).tolist(),
         "color": "#3F51B5", "overlay": True},
    ],
    "signals": [
        {"type": "buy",  "text": "L", "data": open_long_marks,  "color": "#00E676"},
        {"type": "sell", "text": "S", "data": open_short_marks, "color": "#FF5252"},
    ],
}

Four-way column semantics

ColumnMeaning
open_longOpen or add long
close_longClose long (full or reduce per engine config)
open_shortOpen or add short
close_shortClose short

output["signals"] is chart decoration only — fills follow the four bool columns above. Prefer close_* before open_* on the same bar when flipping; avoid both open_long and open_short true on one bar. Default stop-loss, take-profit, sizing, and direction come from # @strategy risk controls.

Script inputs

  • df — OHLCV + ts (pandas DataFrame).
  • params — user values from # @param lines.
Need close-only without flip?
Use four-way close_* columns — not buy/sell under tradeDirection both. For partial exits, scale-in, or stateful position logic, migrate to ScriptStrategy with explicit ctx.open_* / ctx.close_*.

Risk controls (# @strategy) #

Put default risk and sizing at the top of an IndicatorStrategy script with # @strategy <key> <value>. The backtest engine and live executor read these (unless overridden in the Indicator IDE or saved strategy panel). They define engine-managed exits — separate from your four-way close_* signal columns.

python
# Unit: 0–1 = underlying price move (NOT margin PnL; do not divide by leverage)
# @strategy stopLossPct 0.03          # 3% adverse price → stop out
# @strategy takeProfitPct 0.06        # 6% favorable price → take profit
# @strategy entryPct 0.25             # use 25% of capital per entry (1 = 100%)
# @strategy trailingEnabled false
# @strategy trailingStopPct 0.015     # 1.5% retracement after activation
# @strategy trailingActivationPct 0.03  # start trailing after +3% move
# @strategy tradeDirection both       # long | short | both

Supported keys

KeyTypeRangeMeaning
stopLossPctfloat0–1Stop when price moves against the position by this fraction (0.001 = 0.1%, 0.03 = 3%). 0 disables.
takeProfitPctfloat0–5Take profit at this favorable price move. 0 disables.
entryPctfloat0.01–1Capital fraction per open (1 = 100%, 0.25 = 25%). Legacy percent values (>1) are auto-normalized.
trailingEnabledbooltrue / falseLet the engine trail stops after activation threshold is reached.
trailingStopPctfloat0–1Trail distance as price retracement from peak (long) or trough (short).
trailingActivationPctfloat0–1Minimum favorable move before trailing arms. If omitted, engine may reuse takeProfitPct.
tradeDirectionenumlong · short · bothWhich legs the strategy may trade. Filters four-way / two-way signals — does not replace execution columns.

Who owns exits?

LayerMechanismTypical use
Indicator signalsFour-way close_* (edge-triggered)Trend flip, logic-driven exits, “close only, no flip”
Engine risk# @strategy stop / take-profit / trailingFixed protective brackets on every position

Pick one primary exit owner for the same risk event. If your indicator already emits close_* on touch-style take-profit / stop logic, keep trailingEnabled false unless you explicitly want engine trailing on top — otherwise you can get double exits and live/backtest timing drift.

Not in @strategy

  • Leverage — set in the Indicator IDE backtest panel or live strategy settings (leverage in @strategy is ignored).
  • signal_mode / exit_signal_mode — saved strategy / trading config (recommended: confirmed so entries and exits read the last closed bar, aligned with backtest). See the execution standard.
Percent semantics
All *Pct fields in @strategy are underlying price ratios, not ROE on margin. A 3× leveraged position still uses stopLossPct 0.03 for a 3% price move — do not divide by leverage in the script.

ScriptStrategy #

ScriptStrategy is event-driven Python: the engine calls your hooks bar-by-bar (or tick-style in bot mode). Backtest and live share the same context implementation (strategy_script_runtime.py) so fills and position state stay aligned.

Use this when logic depends on runtime position state — partial exits, grid legs, flip/close-only semantics, cooldowns — not just historical boolean columns on a dataframe.

When to choose ScriptStrategy

  • Stop/take-profit rules that read the current entry price or leg size
  • Scale-in / scale-out, or long and short legs at the same time (hedge / neutral grid)
  • Bot-style execution (grid, DCA) driven by pseudo-tick bars

Lifecycle

HookRequiredNotes
on_bar(ctx, bar)YesCompiler rejects scripts without it. Receives the latest closed bar (or bot pseudo-bar).
on_init(ctx)RecommendedSeed ctx.param(...) defaults and log startup. Some UI validation paths expect both hooks.

bar exposes open, high, low, close, volume, timestamp.

Order intents (recommended: four-way)

Prefer explicit methods — they map 1:1 to execution signals and avoid ambiguity in hedge mode (e.g. whether a buy means “cover short” or “add long”):

MethodMeaning
ctx.open_long(amount=, price=, reason=)Open / add long leg
ctx.close_long(amount=, price=, reason=)Reduce / close long (amount in quote notional when set)
ctx.open_short(amount=, price=, reason=)Open / add short leg
ctx.close_short(amount=, price=, reason=)Reduce / close short
ctx.close_position()Flatten net exposure (legacy “exit all”)

Legacy: ctx.buy(...) / ctx.sell(...) accept optional intent= (open_long, close_short, …) and reason=. Prefer the four explicit methods for new code — built-in grid bots already do.

Context API

MemberDescription
ctx.param(name, default)Script-level tunables (grid bounds, MA lengths, …). Persisted in ctx._params across bars.
ctx.bars(n)Last n bars including current (oldest first).
ctx.balance / ctx.equityQuote balance and mark-to-market equity.
ctx.positionHedge-aware legs: long_size, short_size, long_entry, short_entry, plus has_long(), has_short(), is_flat().
int(ctx.position)Net view: >0 net long, <0 net short, 0 flat — legacy scripts still work.
ctx.log(msg)Append to strategy log stream.

Example — EMA crossover with explicit four-way intents

python
def on_init(ctx):
    ctx.param("fast_len", 10)
    ctx.param("slow_len", 30)
    ctx.param("order_usdt", 100.0)
    ctx.log("EMA script initialized")


def on_bar(ctx, bar):
    fast_len = int(ctx.param("fast_len", 10))
    slow_len = int(ctx.param("slow_len", 30))
    order_usdt = float(ctx.param("order_usdt", 100.0))
    price = float(bar.close or 0)
    if price <= 0:
        return

    bars = ctx.bars(slow_len + 2)
    if len(bars) < slow_len:
        return
    closes = [b.close for b in bars]
    fast_ma = sum(closes[-fast_len:]) / fast_len
    slow_ma = sum(closes[-slow_len:]) / slow_len

    if fast_ma > slow_ma:
        if ctx.position.has_short():
            ctx.close_short(amount=order_usdt, price=price, reason="flip_close_short")
        if not ctx.position.has_long():
            ctx.open_long(amount=order_usdt, price=price, reason="golden_cross")
    elif fast_ma < slow_ma:
        if ctx.position.has_long():
            ctx.close_long(amount=order_usdt, price=price, reason="flip_close_long")
        if not ctx.position.has_short():
            ctx.open_short(amount=order_usdt, price=price, reason="death_cross")

Grid bot pattern (hedge legs)

Canonical grid scripts read ctx.position.long_size / short_size independently and emit explicit intents — see bot_scripts/grid_template.py in the repo:

python
# On a down-cross: cover short first, then open long with leftover notional
if short_size > 0:
    ctx.close_short(amount=use_usdt, price=price, reason="grid_buy_cover")
    ctx.open_long(amount=leftover_usdt, price=price, reason="grid_buy_open")
else:
    ctx.open_long(amount=amt, price=price, reason="grid_buy_open")

Runtime modes & sizing

  • Standard bar-closeon_bar runs after each confirmed candle (default backtest / live script path).
  • Bot mode — pseudo tick bars for grid/DCA; test separately from bar-close strategies.
  • Sizingamount on ctx methods is an order intent; saved-strategy backtests still normalize exposure via entryPct and trading config (see Risk controls). Validate with a saved-strategy backtest before going live.
  • Engine SL/TP — optional server-side brackets from strategy panel / # @strategy still apply alongside script intents; avoid duplicating the same exit in both places.
Deep dive
Full ScriptStrategy guide: STRATEGY_DEV_GUIDE.md §6

Declaring parameters #

Both strategy types use the same comment syntax so the UI renders a typed form. Tunables use # @param; engine risk defaults are documented in Risk controls.

python
# Engine risk defaults (IndicatorStrategy) — see Risk controls
# @strategy stopLossPct 0.03
# @strategy takeProfitPct 0.06
# @strategy entryPct 0.25
# @strategy tradeDirection both

# @param sma_short  int    14     Short moving average
# @param sma_long   int    28     Long moving average
# @param use_filter bool   true   Require trend filter
# @param symbol     string BTCUSDT Instrument to trade

Supported types: int, float, bool, string. The last field is the human-readable label.

Backtesting #

Every backtest is pinned to:

  • the exact code hash of the script,
  • a frozen parameter snapshot,
  • the commission and slippage model you chose,
  • the market data range.

Outputs are persisted to PostgreSQL and include per-trade P&L, equity curve, max drawdown, Sharpe, win rate, and exposure time. Re-running with the same snapshot reproduces results deterministically.

Indicator backtests consume edge-triggered four-way columns (or legacy buy/sell) per the Signal & Execution Standard. Saved strategies should use signal_mode / exit_signal_mode of confirmed so entries and exits read the last closed bar — matching backtest timing.

Enable strictMode (UI toggle or Agent API field) when you want backtest fills aligned with live execution semantics — see Strategy Dev Guide.

MCP overview #

QuantDinger ships an Agent Gateway at /api/agent/v1 plus a published MCP server on PyPI:

  • Package: quantdinger-mcp · current release 0.2.0
  • 26 scoped tools — read, workspace, indicators, backtest (strictMode), SSE jobs, and more
  • Transports: stdio (Cursor / Claude Code), sse, streamable-http
bash
uvx quantdinger-mcp
# set QUANTDINGER_BASE_URL=https://ai.quantdinger.com  (hosted, paper-only)
# or http://localhost:8888  (self-host)

Deep dives: MCP_SETUP.md · AGENT_QUICKSTART.md · OpenAPI: agent-openapi.json

Agent quickstart #

  1. Log in as admin → issue an Agent token with scopes (R read, B backtest, …).
  2. Smoke-test: GET /api/agent/v1/whoami with Authorization: Bearer <token>.
  3. Run a backtest job with optional "strictMode": true; poll /api/agent/v1/jobs/<id> or subscribe to SSE.
  4. Wire the same base URL into quantdinger-mcp for Cursor / Claude Code / Codex.

Hosted SaaS (ai.quantdinger.com) pins paper-only execution; self-host controls live trading via AGENT_LIVE_TRADING_ENABLED.

Trading Bots #

Four built-in bot archetypes cover most mechanical-trading workflows. Each one is a first-class ScriptStrategy template with a guided UI.

Grid #

Classic range trader that lays a ladder of buy/sell orders around an anchor price. Best for sideways, mean-reverting instruments.

ParameterMeaning
grid_pctPrice step between grid levels, as a fraction of the anchor.
grid_levelsHow many levels exist on each side.
order_pctOrder size per level as a fraction of balance.
max_position_value_pctCap on total capital deployed.
take_profit_pctClose the whole stack when P&L crosses this threshold.

Martingale #

Averaging-down system that increases position size after adverse moves. Highest reward when markets mean-revert, highest risk when trends break out.

Use strict risk limits
Always configure max_daily_loss and max_position_value. Martingale can wipe out an account during a single prolonged trend.

Trend #

Break-out and moving-average crossover bot. Enters with the trend, rides momentum, and uses trailing stops to lock in gains. Best for directional markets.

DCA #

Dollar-cost averaging bot that buys a fixed notional on a schedule — daily, weekly, or on dips past a threshold. Ideal for long-horizon accumulation in spot markets.

Crypto exchanges #

VenueCoverage
BinanceSpot, Futures, Margin
OKXSpot, Perpetual, Options
BitgetSpot, Futures, Copy Trading
BybitSpot, Linear Futures
CoinbaseSpot
KrakenSpot, Futures
KuCoinSpot, Futures
Gate.ioSpot, Futures
DeepcoinDerivatives integration
HTXSpot, USDT-margined perpetuals

Add credentials from Profile → API keys. Keys are encrypted at rest and only decrypted in the execution path.

US stocks via IBKR #

Interactive Brokers is wired up through the standard TWS / IB Gateway bridge. Data is available through Yahoo Finance and Finnhub when a live IBKR subscription is not desired for research.

env
IBKR_HOST=127.0.0.1
IBKR_PORT=7497         # TWS paper: 7497, live: 7496. Gateway paper: 4002, live: 4001.
IBKR_CLIENT_ID=17
FINNHUB_API_KEY=...    # optional, richer fundamentals

Forex via MT5 #

MT5 integration runs through the Python MetaTrader 5 bridge. Both data and execution are supported. OANDA is supported as a read-only data source.

Mobile web (H5) #

The mobile client is a separate Vue 3 + Vant app that talks to the same backend API as the desktop one. To build the H5 bundle for deployment:

bash
cd quantdinger_mobile
npm install          # first time only
npm run build        # outputs to ./dist

Serve dist/ with any static web server. A production Nginx config is documented in Mobile Nginx config below.

Android APK build #

The mobile app is wrapped with Capacitor. To produce a debug APK:

1. Install Android Studio

Install the stable release of Android Studio. It bundles the JDK (jbr/) and the Android SDK you need.

2. Set environment variables

powershell
$env:JAVA_HOME   = "C:\Program Files\Android\Android Studio\jbr"
$env:ANDROID_HOME = "$env:LOCALAPPDATA\Android\Sdk"
$env:Path = "$env:JAVA_HOME\bin;$env:ANDROID_HOME\platform-tools;$env:Path"

3. Build the web bundle and sync

bash
npm run build
npx cap sync android

4. Point Gradle at the SDK

Create android/local.properties:

ini
sdk.dir=C\:\\Users\\YourName\\AppData\\Local\\Android\\Sdk

5. Run the Gradle build

bash
cd android
./gradlew assembleDebug     # Linux/macOS
.\gradlew.bat assembleDebug # Windows

The APK lands at android/app/build/outputs/apk/debug/app-debug.apk.

Gradle download times out?
Swap the Gradle mirror inside android/gradle/wrapper/gradle-wrapper.properties — for example https://mirrors.cloud.tencent.com/gradle/gradle-8.2.1-all.zip. This is the most common fix in regions where services.gradle.org is slow.

Release APK

Generate a keystore, add a signingConfigs block in android/app/build.gradle, then run ./gradlew assembleRelease. Output: android/app/build/outputs/apk/release/app-release.apk.

Mobile Nginx config #

Reference Nginx config for serving the H5 bundle on m.quantdinger.com, with SPA history-mode fallback and reverse-proxy to the backend:

nginx
server {
    listen 80;
    server_name m.quantdinger.com;
    location ^~ /.well-known/acme-challenge { allow all; root /usr/share/nginx/html; }
    location / { return 301 https://$host$request_uri; }
}

server {
    listen 443 ssl http2;
    server_name m.quantdinger.com;

    ssl_certificate     /www/sites/m.quantdinger.com/ssl/fullchain.pem;
    ssl_certificate_key /www/sites/m.quantdinger.com/ssl/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_session_cache  shared:SSL:10m;
    ssl_session_timeout 10m;

    add_header Strict-Transport-Security "max-age=31536000" always;

    root  /www/sites/m.quantdinger.com/index;
    index index.html;

    location /api/ {
        proxy_pass         http://127.0.0.1:5000/api/;
        proxy_http_version 1.1;
        proxy_set_header   Host              $host;
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_read_timeout 120s;
    }

    location ~* \.(?:js|css|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|ico|webp|map)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
        try_files $uri =404;
        access_log off;
    }

    location = /index.html {
        add_header Cache-Control "no-cache, no-store, must-revalidate";
        expires off;
    }

    # SPA history-mode fallback — required for routes like /login, /trading, etc.
    location / {
        try_files $uri $uri/ /index.html;
    }
}

Production checklist #

  • Regenerate SECRET_KEY with 64 random hex bytes.
  • Change ADMIN_USER / ADMIN_PASSWORD from the defaults.
  • Bind the backend to 127.0.0.1 and terminate TLS at the host Nginx.
  • Enable Turnstile on public login/register/reset endpoints.
  • Point DATABASE_URL at a managed PostgreSQL with automated backups — or snapshot the Docker volume daily.
  • Allowlist mobile + desktop origins in OAUTH_ALLOWED_REDIRECTS.
  • Turn off any worker you don't use (reflection, portfolio monitor).
  • Rotate exchange API keys quarterly; create read-only keys wherever possible.

Recommended topology #

Production deployments should expose only ports 80 and 443 on the host. Bind Docker services to localhost and let host Nginx (or 1Panel / OpenResty) terminate TLS.

LayerBind addressRole
Host Nginx0.0.0.0:443TLS, client_max_body_size, rate limits
frontend container127.0.0.1:8888Vue SPA + built-in /api/* reverse proxy
backend container127.0.0.1:5000Flask API (not required on public internet)
postgres127.0.0.1:5432Database (never expose publicly)

Same-domain (recommended): proxy everything/, /assets/*, and /api/* — to the frontend container. The frontend image already forwards /api/* to backend:5000 inside the Compose network.

text
Browser
  → https://app.example.com
  → Host Nginx :443
  → 127.0.0.1:8888  (frontend container)
       └─ /api/*  →  backend:5000  (inside Docker network)

Dual-domain (advanced): serve the UI at app.example.com → :8888 and the API at api.example.com → :5000. You must configure CORS and frontend API base URLs — only use this when you control a custom frontend build.

Full walkthrough (CN / EN): CLOUD_DEPLOYMENT_CN.md · CLOUD_DEPLOYMENT_EN.md

Host Nginx / 1Panel #

For the desktop app at e.g. ai.example.com, reverse-proxy the entire site to the frontend container. Do not serve static files from a host directory — the UI lives inside the GHCR image.

1Panel / OpenResty pitfall
If the homepage loads but /assets/*.js returns 404 externally, you likely pointed the panel at a local static root instead of proxying to 127.0.0.1:8888. Remove host-side static roots and proxy / and /assets/* to the frontend container.
nginx
server {
    listen 443 ssl http2;
    server_name ai.example.com;

    ssl_certificate     /etc/letsencrypt/live/ai.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ai.example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;

    client_max_body_size 20m;

    # Same-domain: one upstream — frontend handles /api internally
    location / {
        proxy_pass         http://127.0.0.1:8888;
        proxy_http_version 1.1;
        proxy_set_header   Host              $host;
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_read_timeout 120s;
    }
}

If you must split API to the backend port (dual-domain setups), add a separate server block for api.example.com → 127.0.0.1:5000 and configure the frontend build accordingly — not needed for the default GHCR image.

SSL / HTTPS #

Use Let's Encrypt with certbot or your hosting panel's built-in ACME. Keep only TLSv1.2 and TLSv1.3 — older versions (TLSv1, TLSv1.1) are rejected by modern browsers and will surface as ERR_SSL_PROTOCOL_ERROR.

OAuth allowed redirects #

After OAuth, the backend issues a short-lived oauth_token and redirects the user to the frontend origin. For that redirect to succeed:

  • The frontend origin must be listed in OAUTH_ALLOWED_REDIRECTS.
  • The frontend route must exist — for history-mode SPAs, ensure /login is reachable and not a 404 (see the SPA fallback rule).
  • If your desktop client uses hash-mode routing, the backend preserves the hash (/#/user/login) automatically.

Troubleshooting #

GHCR image pull fails or is very slow

  • Set IMAGE_PREFIX in project-root .env for postgres/redis mirrors, e.g. docker.m.daocloud.io/library/.
  • Pin a known-good tag: IMAGE_TAG=3.0.20.
  • On GHCR path use docker compose -f docker-compose.ghcr.yml pull before up -d.

backend.env exists but backend ignores config

Docker creates a directory at the mount path when the host file is missing. Remove the directory, recreate backend.env as a regular file from env.example, then restart:

bash
docker compose -f docker-compose.ghcr.yml down
rm -rf backend.env          # if it became a directory
curl -o backend.env https://raw.githubusercontent.com/brokermr810/QuantDinger/main/backend_api_python/env.example
docker compose -f docker-compose.ghcr.yml up -d

Homepage loads but /assets/*.js is 404 (1Panel / Nginx)

The panel is serving a host static root instead of proxying to 127.0.0.1:8888. Remove local static roots; proxy the entire site to the frontend container (see Host Nginx / 1Panel).

Backend refuses to start: "SECRET_KEY is unsafe"

Regenerate the key and restart:

bash
python -c "import secrets; print(secrets.token_hex(32))"
# GHCR path → backend.env   |   clone path → backend_api_python/.env
docker compose restart backend

Google / GitHub OAuth redirects to the wrong domain

  • Add the exact origin (including scheme and port if non-default) to OAUTH_ALLOWED_REDIRECTS.
  • Rebuild or restart backend after env changes: docker compose up -d --build backend (clone) or docker compose restart backend (GHCR).
  • Clear cookies for the OAuth provider and retry.

Mobile H5 shows 404 on /login after OAuth

The Nginx config is missing the SPA fallback. Add try_files $uri $uri/ /index.html; inside location / (mobile H5 is still served from host static files — unlike the GHCR desktop frontend).

Turnstile widget fails to load (code 600010)

  • Check the response headers for Content-Security-Policy.
  • Whitelist https://challenges.cloudflare.com under script-src and frame-src, or remove the CSP header entirely while you debug.
  • Confirm the Site Key's Hostname Management in the Cloudflare dashboard includes the domain you are serving from.

Gradle wrapper download times out

Edit android/gradle/wrapper/gradle-wrapper.properties and swap distributionUrl to a nearby mirror:

properties
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.2.1-all.zip
networkTimeout=60000

ERR_SSL_PROTOCOL_ERROR in the browser

  • Restrict ssl_protocols to TLSv1.2 TLSv1.3 only — drop TLSv1 and TLSv1.1.
  • Verify the certificate files exist at the paths configured in Nginx.
  • Reload Nginx (nginx -t && nginx -s reload).

FAQ #

Is QuantDinger really self-hosted?

Yes. The default deployment model is your own Docker Compose stack with your own database, Redis instance, credentials, and environment configuration.

Is it only for crypto trading?

No. Crypto is a major focus, but the platform also includes IBKR workflows for US stocks and MT5 workflows for forex.

Can I write strategies directly in Python?

Yes — both models are raw Python. IndicatorStrategy outputs edge-triggered four-way columns (platform default); ScriptStrategy uses on_bar(ctx, bar) with explicit ctx.open_* / ctx.close_* intents. AI can generate a starting point and you edit from there. See Strategy Development.

Is this a research tool or a live trading platform?

Both. QuantDinger is built to connect AI research, charting, strategy development, backtesting, quick trade flows, and live execution operations in one system.

Can I use QuantDinger commercially?

The backend is Apache 2.0. The frontend has a separate source-available license — commercial use is supported but review the license files in the repository first.

License #

  • Backend: Apache License 2.0 (see LICENSE).
  • Frontend UI is distributed here as prebuilt files. The Vue source lives at QuantDinger-Vue under the QuantDinger Frontend Source-Available License v1.0.
  • Non-commercial and eligible qualified non-profit use of the frontend is free of charge; commercial use requires a separate commercial license — apply on the product home.
  • Trademark, branding, attribution, and watermark usage are governed by TRADEMARKS.md.

Community & support #

Found a bug or have a feature request? Open an issue on GitHub — we triage weekly.