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# @strategyrisk defaults, or event-drivenScriptStrategywith explicitctx.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_modealigns fills with live execution semantics. - AI agents (MCP) —
quantdinger-mcp0.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:
| Layer | Technology |
|---|---|
| Frontend | Prebuilt Vue SPA in ghcr.io/brokermr810/quantdinger-frontend (Nginx + API proxy via BACKEND_URL) |
| Backend | Flask API in ghcr.io/brokermr810/quantdinger-backend (or built locally from backend_api_python/) |
| Storage | PostgreSQL 16 |
| Cache / worker support | Redis 7 |
| Trading layer | Exchange adapters, IBKR, MT5 |
| AI layer | LLM provider integration, memory, calibration, optional workers |
| Billing | Membership, credits, USDT TRC20 payment flow |
| Deployment | Docker 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.
| Path | Compose file | Best 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.20in project-root.env(semver tags on GHCR omit the leadingv)
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.
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.
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
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
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.ymlbackend.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
# 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.ymlif you clone QuantDinger-Vue locally - Config —
backend_api_python/.env(notbackend.env)
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:
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/443proxying to127.0.0.1:8888
Common commands #
docker compose ps
docker compose logs -f backend
docker compose restart backend
docker compose up -d --build
docker compose exec backend bash
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.
| File | Used by | Contains |
|---|---|---|
backend.env | GHCR path | SECRET_KEY, API keys, OAuth, billing, workers |
backend_api_python/.env | Clone path | Same runtime keys, bind-mounted into backend container |
.env (project root) | docker compose | IMAGE_TAG, FRONTEND_PORT, BACKEND_URL, IMAGE_PREFIX |
# 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.
| Area | Example keys |
|---|---|
| Authentication | SECRET_KEY, ADMIN_USER, ADMIN_PASSWORD |
| Database | DATABASE_URL |
| LLM / AI | LLM_PROVIDER, OPENAI_API_KEY, OPENROUTER_API_KEY |
| OAuth | GOOGLE_CLIENT_ID, GITHUB_CLIENT_ID |
| Security | TURNSTILE_SITE_KEY, ENABLE_REGISTRATION |
| Billing | BILLING_ENABLED, BILLING_COST_AI_ANALYSIS |
| Membership | MEMBERSHIP_MONTHLY_PRICE_USD, MEMBERSHIP_MONTHLY_CREDITS |
| USDT payments | USDT_PAY_ENABLED, USDT_TRC20_XPUB, TRONGRID_API_KEY |
| Proxy | PROXY_URL |
| Workers | ENABLE_PENDING_ORDER_WORKER, ENABLE_PORTFOLIO_MONITOR, ENABLE_REFLECTION_WORKER |
| AI tuning | ENABLE_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.
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
Database #
Point DATABASE_URL at PostgreSQL 16. The default value targets the in-stack postgres service from Docker Compose:
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.
| Provider | Key | Typical use |
|---|---|---|
| OpenRouter | OPENROUTER_API_KEY | Aggregator — best first choice |
| OpenAI | OPENAI_API_KEY | GPT-4o / GPT-5 family |
| Anthropic | CLAUDE_API_KEY | Claude 3.x / 4.x |
GEMINI_API_KEY | Gemini 1.5 / 2.0 | |
| DeepSeek | DEEPSEEK_API_KEY | Cost-effective reasoning |
| xAI | GROK_API_KEY | Grok family |
Ensemble and calibration
For teams that want more robust AI outputs, turn on ensemble voting and confidence calibration:
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:
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
/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:
TURNSTILE_ENABLED=true
TURNSTILE_SITE_KEY=0x4AAA...
TURNSTILE_SECRET_KEY=0x4AAA...
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:
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.
USDT_PAY_ENABLED=true
USDT_TRC20_XPUB=xpub6... # BIP-32 extended public key (TRC20 HD derivation)
TRONGRID_API_KEY=...
USDT_CONFIRMATIONS_REQUIRED=12
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:
# 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:
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
| Form | Execution columns | When 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:
# 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
| Column | Meaning |
|---|---|
open_long | Open or add long |
close_long | Close long (full or reduce per engine config) |
open_short | Open or add short |
close_short | Close 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# @paramlines.
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.
# 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
| Key | Type | Range | Meaning |
|---|---|---|---|
stopLossPct | float | 0–1 | Stop when price moves against the position by this fraction (0.001 = 0.1%, 0.03 = 3%). 0 disables. |
takeProfitPct | float | 0–5 | Take profit at this favorable price move. 0 disables. |
entryPct | float | 0.01–1 | Capital fraction per open (1 = 100%, 0.25 = 25%). Legacy percent values (>1) are auto-normalized. |
trailingEnabled | bool | true / false | Let the engine trail stops after activation threshold is reached. |
trailingStopPct | float | 0–1 | Trail distance as price retracement from peak (long) or trough (short). |
trailingActivationPct | float | 0–1 | Minimum favorable move before trailing arms. If omitted, engine may reuse takeProfitPct. |
tradeDirection | enum | long · short · both | Which legs the strategy may trade. Filters four-way / two-way signals — does not replace execution columns. |
Who owns exits?
| Layer | Mechanism | Typical use |
|---|---|---|
| Indicator signals | Four-way close_* (edge-triggered) | Trend flip, logic-driven exits, “close only, no flip” |
| Engine risk | # @strategy stop / take-profit / trailing | Fixed 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 (
leveragein@strategyis ignored). signal_mode/exit_signal_mode— saved strategy / trading config (recommended:confirmedso entries and exits read the last closed bar, aligned with backtest). See the execution standard.
*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
| Hook | Required | Notes |
|---|---|---|
on_bar(ctx, bar) | Yes | Compiler rejects scripts without it. Receives the latest closed bar (or bot pseudo-bar). |
on_init(ctx) | Recommended | Seed 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”):
| Method | Meaning |
|---|---|
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
| Member | Description |
|---|---|
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.equity | Quote balance and mark-to-market equity. |
ctx.position | Hedge-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
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:
# 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-close —
on_barruns after each confirmed candle (default backtest / live script path). - Bot mode — pseudo tick bars for grid/DCA; test separately from bar-close strategies.
- Sizing —
amounton ctx methods is an order intent; saved-strategy backtests still normalize exposure viaentryPctand trading config (see Risk controls). Validate with a saved-strategy backtest before going live. - Engine SL/TP — optional server-side brackets from strategy panel /
# @strategystill apply alongside script intents; avoid duplicating the same exit in both places.
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.
# 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
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 #
- Log in as admin → issue an Agent token with scopes (
Rread,Bbacktest, …). - Smoke-test:
GET /api/agent/v1/whoamiwithAuthorization: Bearer <token>. - Run a backtest job with optional
"strictMode": true; poll/api/agent/v1/jobs/<id>or subscribe to SSE. - Wire the same base URL into
quantdinger-mcpfor 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.
| Parameter | Meaning |
|---|---|
| grid_pct | Price step between grid levels, as a fraction of the anchor. |
| grid_levels | How many levels exist on each side. |
| order_pct | Order size per level as a fraction of balance. |
| max_position_value_pct | Cap on total capital deployed. |
| take_profit_pct | Close 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.
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 #
| Venue | Coverage |
|---|---|
| Binance | Spot, Futures, Margin |
| OKX | Spot, Perpetual, Options |
| Bitget | Spot, Futures, Copy Trading |
| Bybit | Spot, Linear Futures |
| Coinbase | Spot |
| Kraken | Spot, Futures |
| KuCoin | Spot, Futures |
| Gate.io | Spot, Futures |
| Deepcoin | Derivatives integration |
| HTX | Spot, 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.
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:
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
$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
npm run build
npx cap sync android
4. Point Gradle at the SDK
Create android/local.properties:
sdk.dir=C\:\\Users\\YourName\\AppData\\Local\\Android\\Sdk
5. Run the Gradle build
cd android
./gradlew assembleDebug # Linux/macOS
.\gradlew.bat assembleDebug # Windows
The APK lands at android/app/build/outputs/apk/debug/app-debug.apk.
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:
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_KEYwith 64 random hex bytes. - Change
ADMIN_USER/ADMIN_PASSWORDfrom the defaults. - Bind the backend to
127.0.0.1and terminate TLS at the host Nginx. - Enable Turnstile on public login/register/reset endpoints.
- Point
DATABASE_URLat 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.
| Layer | Bind address | Role |
|---|---|---|
| Host Nginx | 0.0.0.0:443 | TLS, client_max_body_size, rate limits |
frontend container | 127.0.0.1:8888 | Vue SPA + built-in /api/* reverse proxy |
backend container | 127.0.0.1:5000 | Flask API (not required on public internet) |
postgres | 127.0.0.1:5432 | Database (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.
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.
/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.
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
/loginis 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_PREFIXin project-root.envfor 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 pullbeforeup -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:
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:
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) ordocker 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.comunderscript-srcandframe-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:
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.2.1-all.zip
networkTimeout=60000
ERR_SSL_PROTOCOL_ERROR in the browser
- Restrict
ssl_protocolstoTLSv1.2 TLSv1.3only — dropTLSv1andTLSv1.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 #
- GitHub: brokermr810/QuantDinger
- Telegram: @quantdinger
- Discord: Join the server
- YouTube: @quantdinger
- Email: support@quantdinger.com
Found a bug or have a feature request? Open an issue on GitHub — we triage weekly.
QuantDinger