Is QuantDinger worth using? As something to learn from, yes. As a self-hosted platform for real money, not yet, in my reading. The order plumbing is careful (idempotent orders, leases, several layers of reconciliation) and the backtester goes out of its way to avoid look-ahead. But the account-level risk limits exist in the code without being called from live trading, there's no global kill switch, and user strategy code runs in the same process that holds the exchange API keys.
This is part 2 of the series. In part 1 I read 19 public Jev finance projects; QuantDinger was the biggest and the only full platform, so it gets three posts of its own, starting with the platform as a whole.
What QuantDinger is
A self-hosted, multi-user trading platform, open source (backend under Apache-2.0, about 12,000 GitHub stars, version 5.4.1) and also sold as a SaaS by its authors. The pitch: write a strategy in Python, backtest, paper trade, go live, monitor. Plus a lot of AI: strategy code generation, market analysis, an agent gateway over MCP, and the Jev entry filter I look at in part 4.
The backend is big: 787 Python files, 291 test files with roughly 2,200 test functions, 24 SQL migrations. The frontend lives in a separate repo under a source-available licence, and the brand needs a commercial licence.
How I reviewed it
I read the architecture docs and the backend code at commit 96cec2d (21 Sep 2026). I checked the heaviest claims below directly in the source. I didn't run it; that needs Docker, Postgres and Redis, and I wasn't going to connect exchange keys to it anyway. So this is a code review, not a live test, and a newer version may have fixed some of this.
What does it do well?
| Area | What I found |
|---|---|
| Process layout | API, trading worker, scheduler and Celery can run as separate roles; orders are claimed with FOR UPDATE ... SKIP LOCKED and workers hold a lease with a heartbeat |
| Order idempotency | a fixed client order id per pending order, saved before sending |
| Reconciliation | order status over REST, WebSocket for five exchanges, periodic position sync, recovery of fills that were never acknowledged |
| Manual positions | if you hold more than the strategies' allocation, the bot won't close your part |
| Look-ahead | higher-timeframe bars only become visible once closed; the unfinished live bar is dropped; fundamentals carry an available_at date; there are tests for this |
| CI | ruff, pytest on real Postgres, pip-audit, bandit, Gitleaks, CodeQL |
The look-ahead handling is more careful than in many retail backtesters. If you build your own multi-timeframe system, the "higher timeframe only after it closes" rule is the one to copy.
Is the backtest realistic?
Partly. The event order inside each bar is sensible: pending orders fill at the open, then SL/TP are checked against the bar's high and low, then your code runs and its orders fill at the next open. When SL and TP are both hit in one bar, the default assumes the stop hit first.
But the cost model is thin:
| Setting | Value |
|---|---|
| Fee | 0.05% fixed |
| Slippage | 0.05% fixed |
| Spread | none |
| Limit orders | filled as soon as high or low touches the price |
| Metrics | return, max drawdown, Sharpe, win rate, profit factor; no walk-forward, no in/out-of-sample split |
| Data | cached in Redis with a TTL, re-downloaded from free sources, so a rerun can give different numbers |
| Range limits | 1m bars up to 30 days; H1/D1 up to 3 years |
No spread is a big deal for forex and gold. And "touch equals fill" on limit orders flatters any strategy that trades at round levels.
Where are the risks with real money?
Ranked by how much they'd worry me:
1. Strategy code runs next to the exchange keys. Strategy scripts run through exec inside the same process, guarded by a home-made blocklist (regex, AST checks, trimmed builtins) rather than a container or RestrictedPython. A comment in the code records that the sandbox was bypassed once, in August 2026. There's a 10-second timeout on loading the module, but none on initialize or handle_data, and resource limits are off by default. That process also has the database connection and the key that decrypts users' exchange API keys. On a multi-user install, escaping the sandbox means reaching other people's keys. An infinite loop in handle_data is enough to hang the API. (Indicator scripts, by contrast, run in a properly isolated subprocess.)
2. Account-level risk limits aren't connected. account_risk.py has limits for total notional, margin, leverage and per-symbol exposure. I couldn't find it called anywhere except the tests. There's no daily loss limit or drawdown stop for user-written strategies. The "emergency stop" cancels orders placed by the AI agent; it doesn't stop strategies or close positions, and there's no stop-everything button.
3. An agent token with write scope can edit live strategies. Letting the AI agent trade live needs several gates: the right scope, an explicit risk acknowledgement, a feature flag, allowed markets, per-order and per-day notional caps. But a token with only write scope can create or edit a strategy with execution_mode=live, swap its exchange keys and change leverage, which skips all of those gates. It can't start a strategy, but it can change one that's already running. Agent tokens don't expire by default.
4. One key protects every exchange key. API keys are encrypted with Fernet using a single key from the environment, with no KDF, no KMS and no rotation. That environment file can be edited from the admin page. A few code paths fall back to user 1 (the admin) when a user id is missing.
5. Stop losses can quietly fail. On perpetual swaps, the SL/TP order is placed on the exchange after the entry fills; if placing it fails, that's logged and the position stays open. On spot, IBKR and Alpaca, strategy SL/TP is checked by polling.
Smaller items: after a restart, orders stuck for 90 seconds are resent with the same client id without first asking the exchange whether they exist. Manual installs default to admin password 123456 and database password quantdinger123 (the install script generates random ones). Alertmanager ships seven rules and no receiver, so alerts go nowhere, and there are no trading metrics (fills, reconciliation drift, P&L). Webhook URLs aren't checked against internal IPs.
Can I use it for forex or MT5?
No. Live trading covers Binance, OKX, Bitget, Bybit, Gate, HTX, Interactive Brokers and Alpaca. There's no forex broker and no MetaTrader connector. Forex exists only as data (Twelve Data, Tiingo, yfinance) for charts and backtests, and those backtests have no spread.
Who is it for?
- Developers building their own platform: read it. The process roles, idempotent order flow, reconciliation and look-ahead rules are worth studying.
- A single user on a private machine, crypto only: risks 1 and 3 shrink a lot if nobody else can reach it. Risks 2 and 5 don't.
- Anyone self-hosting for other people's money: I wouldn't, until the sandbox, the account-level limits and the key handling are fixed.
What I didn't verify
- I didn't run the platform or place an order with it.
- The SaaS may be hardened beyond the open-source defaults.
- The August 2026 sandbox bypass is from a code comment; I didn't reproduce it.
- Anything after commit
96cec2d.
Next in this series: the trade-management ideas in QuantDinger's bots that I'd actually copy into an EA, and the one I wouldn't touch.
Originally published at moonthetrain.com.
Top comments (1)
Checking the heavy claims against a pinned commit and saying plainly that you didn't run it makes this much easier to trust than most platform reviews.
One gap to add on the backtest side: nothing counts the runs. A platform that generates strategy code with AI, backtests it and lets you edit and rerun is a search engine for good-looking Sharpe ratios, and the report shows the Sharpe of the latest run with no record of how many variants came before it. Logging every backtest per strategy (parameters, date range, result) would let the report say "best of 40 runs", which is the number you need to judge whether a Sharpe of 1.5 means anything. The Redis cache you mention makes this worse: if a rerun can give different numbers, the same code tried twice is already two draws.
And on "touch equals fill": a cheap fix many backtesters use is to fill a limit order only when the bar trades through the price by at least one tick, or to fill a fraction of touches. It removes most of the round-number flattery without needing order book data.