QuantDinger — a self-hosted quant platform where you stand up the PostgreSQL and the Redis yourself

QuantDinger 5.4.1 built from source in 4 minutes 53 seconds with no Docker, then the dashboard, the Agent Gateway and MCP all put through their paces. Includes

Part 14. QuantDinger — standing an entire strategy runtime up on your own server

Done when: you have stood up the backend, PostgreSQL, two Redis instances and the dashboard with no Docker, pulled prices with no key, and run a backtest grid to pick one parameter set.

What it does AI market research, a Python indicator and strategy editor, server-side backtesting, paper and live execution, and five entrances — web, mobile, API, Agent Gateway and MCP — all in one repository
Who it fits Anyone who writes strategies in Python themselves and wants to fix them while watching a chart. Anyone who wants market data, strategy code and broker credentials under their own control
Who it doesn't fit Anyone who wants "no install, just go" (you stand up PostgreSQL and two Redis instances first) · anyone who only needs 24-hour residency with the strategy already decided (you carry the operational load without using most of what this has)
What you end up with The whole stack running locally with no Docker, prices pulled with 0 keys, four backtest grid results, and one real bug found in the indicator layer
What you need · cost Code free (Apache-2.0, no signup) · Model only if you use the AI features (OpenRouter, OpenAI or DeepSeek) · Market data free, 0 exchange API keys · Disk 1.5 GB
Time · difficulty 4 minutes 53 seconds from clone to healthy (the pure install is about 15 seconds). Difficulty high — the heaviest tool in this series
Relationship to weltiq They overlap a lot. So section 5 is not "how to slot it in" but which side belongs where. The combination actually run: QuantDinger decides, weltiq fills, and that record goes back to QuantDinger
Test environment · date macOS 26.6.2 · arm64 · Python 3.12 · PostgreSQL 16.13 · Redis 8.6.2 · Node 26.8.1 · source 5.4.1 (commit 96cec2dc) · 2026-09-22 01:45–02:40 KST
Disclosure weltiq pays me to write this series. Everything said about the tool comes from runs I did myself

The human sentences are the commands I actually typed, rewritten as conversation. The output is measured and the numbers are untouched. Every order went to weltiq paper (simulated); there was not one live-account connection. I put zero exchange API keys into QuantDinger — prices and backtests work without them. None of this is a trading opinion. The raw logs are in the verification record.


1. What this is

OpenByteInc/QuantDinger calls itself "an open-source AI trading OS." The name sounds inflated until you open it. What matters is that it is self-hosted — the README's first paragraph says it keeps market data, strategy code, broker credentials and deployment under the operator's control, and you genuinely do not create a cloud account. Nothing came off the internet for this article except source code and Python packages.

In terms of where it sits, it both decides and executes. The AI turns a trading idea into a Python strategy, and that strategy goes out as a real order through an exchange connector. The execution layer is isolated in app/services/live_trading/ and the README explicitly says "adapter extensions", so I read it as having a seat left open — whether that read was right gets checked against the code in section 5.

Item Value How it was checked
URL github.com/OpenByteInc/QuantDinger
License Apache-2.0 GitHub API
Stars ★11,912 · 2,450 forks (observed 2026-09-22) GitHub API
Last push 2026-09-21 08:01 UTC GitHub API
Created 2025-12-28 · 41 open issues · 78 watchers · not archived GitHub API
Source version VERSION file 5.4.1 (commit pulled: 96cec2dc) repository
Frontend separate repository OpenByteInc/QuantDinger-Vue README
MCP package PyPI quantdinger-mcp 0.6.2, 58 tools counted directly in section 3
Agent Gateway 51 routes docs/agent/agent-openapi.json
Backend size 444 Python files · 15 compose services find app -name "*.py" \| wc -l

One small confusion: the VERSION file says 5.4.1 while the banner the server prints on boot says QuantDinger Python API v2.2.2. They are two different numbering schemes — when filing an issue, it is safer to quote both.


2. Installing

Both official paths require Docker.

What Prerequisite
Option A curl -fsSL .../install.sh \| bash — pull the GHCR image and start it with compose Docker + Compose v2
Option B Check out the source, then docker compose up -d --build Docker + Compose v2

The Mac this was written on had no Docker. So I took a third path — read what the compose file brings up and stand the same thing up as local processes. The short version: it works. And it was faster than Docker.

You need Python 3.12, PostgreSQL 16.13 (the badge says 18, but 16 ran it fine), two Redis 8.6.2 instances (cache and durable job queue — QuantDinger separates them because the eviction policies differ), and Node.js only if you want the web dashboard.

# (1) clone — 2.581 seconds, 37 MB
git clone --depth 1 https://github.com/OpenByteInc/QuantDinger.git

# (2) Python dependencies — 130 of them. 6.27s cold / 3.75s warm. venv is 475 MB
uv venv -p 3.12 venv
uv pip install --python venv/bin/python -r QuantDinger/backend_api_python/requirements.txt

# (3) DB and queue — do not use the default ports (5432/6379). A dedicated cluster in the 9080s
export LC_ALL=C LANG=C
initdb -D pgdata -U tut14 --auth=trust -E UTF8 --locale=C          # 0.37 seconds
pg_ctl -D pgdata -o "-p 9080 -c unix_socket_directories= -c listen_addresses=127.0.0.1" -l pg.log start
psql -h 127.0.0.1 -p 9080 -U tut14 -d postgres -c "CREATE DATABASE tut14_quantdinger;"
redis-server --port 9081 --dir redisdata --dbfilename cache.rdb --daemonize yes --save ""
redis-server --port 9082 --dir redisdata --dbfilename jobs.rdb --daemonize yes --appendonly no

The heavy ones are pandas 3.0.6, ccxt 4.5.82, TA-Lib 0.7.1, litellm, alpaca-py and ib_insync. TA-Lib is notorious for needing a C library installed separately, but the 0.7.x wheel contains it, so it just installed.

Configuration means copying env.example to .env. It is 724 lines, but only six fields actually need touching — SECRET_KEY and CREDENTIAL_ENCRYPTION_KEY (a secrets.token_hex(32) each), ADMIN_USER and ADMIN_PASSWORD (123456 is rejected even by the one-click installer), DATABASE_URL, and the hosts and ports for REDIS_*, CELERY_REDIS_* and PYTHON_API_*.

cd backend_api_python && LC_ALL=C python run.py

Get here and it is working. The schema comes up on its own.

DB_POOL_MAX=auto selected max=50
  (postgres max_connections=100, reserved=3, reserve_for_other_clients=20, gunicorn_workers=1)
[migration] schema-init: applying 136452 bytes
[migration] market-symbols-master: applying 1845804 bytes
[migration] strategy-v2-templates: applying 37490 bytes
[migration] all components complete
DB permission probe OK (10 critical tables readable)   Created user: tut14admin (id=1)
Agent Gateway v1 mounted at /api/agent/v1
QuantDinger Python API v2.2.2   Service starting at: http://127.0.0.1:9083

SKIP_AUTO_MIGRATE=false is the default, so it reconciles the schema on every boot (all CREATE TABLE IF NOT EXISTS, so it is harmless). When it finishes there are 90 tables and a 62 MB database, of which the 1.8 MB market-symbols-master plants a global symbol master of 14,171 rows. The automatic pool sizing impressed me — it actually reads max_connections, sets aside 20 for other clients, and takes 50 for itself.

git clone at 01:45:05 → /api/health reporting healthy at 01:49:58. 4 minutes 53 seconds in total. Disk is the Python venv 475 MB + frontend node_modules 433 MB + PG data 155 MB + repositories 64 MB = 1.5 GB. Memory is the backend Python at 464 MB, PostgreSQL 23 MB and the two Redis instances 8.4 MB combined, plus 578 MB if you run the vite dev server alongside. My "heavy tool" expectation was only half right — there are a lot of parts (5 processes, 5 ports), but four of those five minutes were me stuck, and the pure install is about 15 seconds.

The first screen after install — the QuantDinger web dashboard login

The first screen after bringing the dashboard up on 127.0.0.1:9084. The product blurb on the left ships in the bundle; the Welcome to QuantDinger on the right is where the ADMIN_USER and ADMIN_PASSWORD from .env go. There is no signup and no external account linking — get in here and you are already the administrator.

Where it gets stuck — seven places, and I am not hiding them

This is where this part earns its keep. Every place I actually got stuck.

# Where The message How I got past it
1 Starting the install docker not found Both official paths are blocked. I read the compose file and stood it up from source
2 initdb invalid locale settings; check LANG and LC_* environment variables export LC_ALL=C LANG=C, then --locale=C
3 pg_ctl start Unix-domain socket path is too long (maximum 103 bytes) The data path is long. -c unix_socket_directories= turns the socket off and uses TCP only
4 pg_ctl start (again) postmaster became multithreaded during startup The LC_ALL=C from (2) has to be set in the shell that calls pg_ctl too
5 Issuing an Agent token HTTP 200, no error. And then GET /markets returns an empty array Passing a JSON array like {"markets":["Crypto"]} stores it as ["['Crypto']"], so no market is permitted at all. You have to pass a comma-separated string, "markets":"Crypto" (parse_csv_list(str) at app/utils/agent_auth.py:231). The Agent OpenAPI types this body as a bare object, so there is no way to know the shape
6 AI strategy generation {"error":"strategy.invalidAssetType"} The only accepted values for assetType are script and portfolio_strategy. It takes a strategy kind, not an asset class
7 AI strategy generation {"error":"strategyV2.aiBehaviorOpenLegMissing:long"} Three attempts across two models, rejected in the same place every time. Section 4 is entirely about this

Number 5 was the nastiest, because a failure returns 200. The token is issued and the issuing response even prints "markets": ["['Crypto']"] — miss that extra quote and every request after it quietly returns nothing.


3. The first result

Start by checking what works with no keys at all.

What you say

Log in to the QuantDinger I just brought up and get me the bitcoin price and the last five daily candles.

What comes back

POST /api/auth/login                                   HTTP 200
  role=admin  permissions=[dashboard, view, indicator, backtest,
                           strategy, portfolio, settings, user_manage, credentials]
GET /api/agent/v1/price?market=Crypto&symbol=BTC/USDT  HTTP 200
  price 85939.4   high 86351.4   low 80588.0   open 81288.6   +5.72%
  source "ticker"  quote_type "latest_available"
GET /api/agent/v1/klines?...&frequency=1d&limit=5      HTTP 200
  2026-09-19  O 80903.0  H 81953.1  L 80847.5  C 81265.8  V 4232.76
  2026-09-20  O 81265.8  H 81500.0  L 80133.4  C 81179.6  V 3666.85
  2026-09-21  O 81180.1  H 86351.4  L 80857.6  C 85939.4  V 10228.51

This is what it looks like when it works. Live prices arrive with no exchange key at all — prices come from ccxt's public endpoints. Keys are only needed for orders.

The dashboard — the frontend is a separate repository

This surprised me. The backend repository has no frontend source. The compose file pulls the ghcr.io/openbyteinc/quantdinger-frontend image, and with no Docker that path is closed. Line 673 of the README has the answer — the source is in a separate repository, OpenByteInc/QuantDinger-Vue.

git clone --depth 1 https://github.com/OpenByteInc/QuantDinger-Vue.git && cd QuantDinger-Vue
npx --yes pnpm@10.33.4 install --frozen-lockfile          # 6.47 seconds
VITE_DEV_PROXY_TARGET=http://127.0.0.1:9083 \
  npx --yes pnpm@10.33.4 exec vite --port 9084 --host 127.0.0.1   # up in 871 ms

6.47 seconds to install, 871 milliseconds to start — faster than pulling a Docker image. The frontend finds the backend through exactly one thing, VITE_DEV_PROXY_TARGET; leave it out and it goes to the default http://127.0.0.1:5000 and nothing appears. With this many parts, here is which port is what.

Port What Default Where to change it
9084 The web dashboard — this is where you go 8888 vite --port / FRONTEND_PORT
9083 Backend API (/api/...) 5000 PYTHON_API_PORT in .env
9080 PostgreSQL 5432 DATABASE_URL · pg_ctl -o "-p"
9081 · 9082 Redis (cache · job queue) 6379 REDIS_PORT · CELERY_REDIS_PORT

Login is the account in .env and nothing else — no signup, no email verification. Exchange keys are unnecessary for prices, charts, backtests and strategy editing, and needed only for orders. LLM keys are needed only for the AI features. Billing is BILLING_ENABLED=false by default, so self-hosting has no limits.

Just after login — the AI Research workspace

The first screen you land on. The values in the Watchlist on the right — MSFT 496.19, TSLA 373.38, SOL/USDT 117.74 — are the seven default watchlist entries filling in live, which confirms prices are attached on login alone. The What are you researching? in the middle needs an LLM key to answer.

Arriving for the first time, it is not obvious what to press. In my experience the best first click is Indicator in the top menu — it works with no key and shows you in one screen what this thing is.

The Indicator screen — Python editor on the left, chart and order panel on the right

This one image is QuantDinger's defining picture. On the left is the Python source of a bundled sample indicator (SuperTrend) — the comments even say "the final band is path dependent, so we use a bar loop instead of vectorizing" and "we compare only against the previous bar's final band, so there is no look-ahead." Top middle is that indicator on a BTC/USDT daily chart. Below is the Quick Trade order panel, and the AI Decision Filter toggle at the far right asks an LLM once before entering. Editor, chart and order panel in one screen says exactly what kind of tool this is.

For anyone driving by conversation, I checked MCP too. uv pip install "quantdinger-mcp==0.6.2" takes 1.39 seconds, and attaching over stdio and counting tools/list gives 58 — from reads like whoami and get_klines to submit_backtest, save_strategy_source, place_quick_order and emergency_stop_trading. Two design choices stand out: every write tool requires an idempotency_key (retrying does not send it twice), and orders, stops and alert sends additionally require confirm_*=true (a guard against an LLM calling them by accident).


4. The best way to use it — does one sentence become a Python strategy that runs

QuantDinger's opening line is "turn a trading idea into a Python strategy", so that is where to start measuring.

What you say

Build me a strategy on BTC/USDT hourly candles: go long when the fast EMA crosses above the slow EMA, close everything when it crosses below. Add an ATR-based stop, and make the parameters settable from outside.

What comes back

POST /api/strategies/generate          HTTP 400   (36.3 seconds)
{"msg":"strategyV2.generationInvalid",
 "data":{"error":"strategyV2.aiBehaviorOpenLegMissing:long"}}
server log:
  repairing invalid generated strategy attempt=1/2: strategyV2.aiBehaviorOpenLegMissing:long
  repairing invalid generated strategy attempt=2/2: strategyV2.aiBehaviorOpenLegMissing:long
  strategy generation failed: strategyV2.aiBehaviorOpenLegMissing:long

A simpler prompt (46.0 seconds) — the same place. Switching the model from openai/gpt-5.6-luna to anthropic/claude-sonnet-5 (177.4 seconds) — the same place again. Three failures, and on failure it returns not one line of code.

So what is that gate — this is the best part of this tool

I opened app/services/strategy_ai_behavior.py. QuantDinger does not stop at compiling the strategy the AI produced. It actually runs it over deterministically generated synthetic candles and checks whether it ever opened a position in the requested direction.

# _scenario_frame() — no randomness. A deterministic frame: two sine waves plus a regime flip
primary   = 320.0 * math.sin(offset / 18.0)
secondary =  95.0 * math.sin(offset / 5.5)
regime    = 90.0 if (offset // 72) % 2 == 0 else -90.0
closes.append(max(100.0, 2500.0 + primary + secondary + regime))

# declare long-only and you have to actually open a long to pass
if _required_open_sides(manifest) - opened_sides:
    raise StrategyV2ContractError(f"strategyV2.aiBehaviorOpenLegMissing:{detail}")

It is a gate that separates "code that compiles" from "code that trades." It is the strictest AI strategy generation in this series — far more tools hand back a strategy that parses and does nothing. The gate itself is sound: feeding the bundled example through the same gate passes.

bundled example strategy_v2_dual_ema_long.py → PASS
  {'executed': True, 'sample_count': 500, 'total_executions': 182, 'opened_sides': ['long']}

So why does the AI code fail every time — there is a bug in the indicator layer

Since the route does not return the generated code, I reproduced the pipeline with the same prompt and model and pulled the code out. The 99 lines that came back were correct on reading — enter on a golden cross, exit on a death cross, ATR stop. And it never enters. Instrumenting it found the cause.

bar=50   fp=2856.67  fc=2850.89  sp=2856.67  sc=2850.89  pos=0.000000
bar=300  fp=2471.50  fc=2457.93  sp=2471.50  sc=2457.93  pos=0.000000

ema(5) last=2457.9258 · ema(200) last=2457.9258 · identical=True     ← period is ignored
timeperiod= : ema(5)=2312.3110  ema(200)=2528.1882  identical=False  ← this name works
sma period= : sma(5)=2311.5846  sma(200)=2526.9734  identical=False  ← only SMA works
pandas ewm(5)=2312.3110                                              ← exactly matches timeperiod=5

The fast EMA and the slow EMA are identical to the decimal. A cross can never happen, so it never enters. The cause is three lines at app/services/factors/talib_adapter.py:92.

if library_id == "SMA" and "period" in normalized:
    period = normalized.pop("period")
    normalized.setdefault("timeperiod", period)

The period → timeperiod rename is wired only for SMA. EMA, RSI, ATR and the rest pass period=5 straight to TA-Lib, and TA-Lib's abstract interface silently discards arguments it does not know and uses the default timeperiod=30. So ema(period=5) and ema(period=200) are both EMA(30).

And it is natural for the AI to write period= — QuantDinger's own authoring contract (/strategy-sources/authoring-contract) uses indicator("supertrend", symbol, period=period, ...) as its example. The supertrend in that example is a built-in rather than TA-Lib, so it works; the moment you cross into TA-Lib it is ignored. This is not an AI-generation-only problem — the same indicator() is used in every backtest. Write indicator("rsi", sym, period=14) by hand and you get RSI(30). Silently, with no warning.

Workaround: for TA-Lib indicators use TA-Lib's own argument names rather than period= — timeperiod=, fastperiod=, slowperiod=. The MACD example on line 485 of docs/trading/STRATEGY_DEV_GUIDE.md correctly uses fastperiod=12, slowperiod=26. The authoring contract and the developer guide use different names.

It is very good at this instead — fixing things in conversation

Same AI, and conversation mode is a completely different thing. Putting the same request to the strategy IDE as a conversation produced this.

The strategy IDE — AI Collaborate comparing the request against the current source in a table

Look at the AI Collaborate panel on the right. It compares the request (hourly, EMA, ATR stop) against the current source (one-minute, SMA, fixed 5% stop) line by line in a table, and under "key limitations" it points out on its own that "the fast and slow periods are named like EMA parameters but are actually SMA periods." The editor on the left is still my source — "the editor is not changed until you press Apply" is written into the panel's own notice.

It cannot produce the code in one shot, but it is good at sitting beside you and reading it with you. That is where section 6's "who it fits" comes from.


5. Moving it to weltiq

Let me be honest first: this tool overlaps weltiq a lot. Most tools in earlier parts were "it decides but the order seat is empty." QuantDinger is not — it has a strategy editor, a backtest engine that states its fill assumptions, ledger reconciliation, connectors for eight exchanges, and live orders. So this section is not "how to slot in" but which side belongs where.

The order a person carries it in

Pick the parameters on QuantDinger (POST /api/agent/v1/backtest/run) → rewrite them as a weltiq rule document (POST /v1/strategy-rules) → write the thesis in your own words (POST /v1/ai/thesis; skip it and you get 422 thesis_required with three questions) → bring up a container with the limits (POST /v1/containers; no stop condition means 422 Field required). I got stuck once carrying it over — writing the exit as kind: "exit" produced invalid_order_rule. In the weltiq grammar exit is a field that wants a price level (stop/limit); closing everything on a condition is close_all.

The best combination — cost feedback

QuantDinger's backtest takes cost as an argument. POST /backtest/run accepts commission and slippage, and the result returns every value it used back to you as executionAssumptions. That creates a seat for measured values in those arguments.

(1) QuantDinger decides. I ported the bundled example (a strategy that sets target weight from the ordering of two moving averages) to BTC/USDT one-minute candles and ran four period combinations over the same window (2026-09-14 to 09-20, 10,080 candles). 4.5 seconds per run.

fast / slow Return Trades Fees PF Max DD
10 / 30 −32.82% 332 1,697.18 0.171 −32.90%
20 / 50 −19.00% 187 980.75 0.265 −19.11%
30 / 80 −16.00% 149 725.35 0.269 −16.22%
60 / 180 −5.34% 68 315.90 0.512 −8.62%

On capital of 10,000, the fees are 1,697. All four moving-average crossovers on bitcoin one-minute candles lost money that week, and the slower ones lost less. Because of fees.

The backtest center — six runs submitted by API, sitting in the UI

Look at Recent backtest runs on the right. The six backtests I submitted through the Agent Gateway are stacked in the web UI exactly as they were — −5.34%, −16.00%, −19.00% and −32.82% match the table above. What was done by API and what was done on screen land in the same record. On the left, 1m backtests can cover up to 29 days per run is the ceiling notice for minute-bar backtests.

The backtest result — fills marked on the chart, and the fill assumptions

Look at the green banner at the top. "Fills, fees, positions, cash, and final equity passed independent ledger reconciliation" — it states that fills, fees, positions, cash and final equity passed an independent ledger reconciliation and attaches a Ledger reconciled badge. Below it, Fill rule: Close signal → next bar open; scheduled order → current bar open, Commission 0.05%, Slippage 0.05% and Funding: Not modeled state every fill assumption. The green L and red Exit on the chart are the 68 round trips.*

(2) weltiq fills. I carried the chosen fast=60, slow=180 into a rule document (a 1:1 conversion into the ordering of sma(close, 60) and sma(close, 180), with the example's own 5% stop), replayed the same window and brought up a container.

POST /v1/backtest  → n_bars 10068 · n_fills 68 · n_trades 34
                     pnl 37.85877647000 · fees 53.41523101 · intent_outcomes {accepted: 68}  (0.6 s)
POST /v1/containers → status=running · stop {max_loss 150, until …T19:13:31Z}
first fill, 20s later  buy  0.01 @ 85979.70711050  fee 0.85979707  taker  OPEN
closed on stop         sell 0.01 @ 85861.83295750  fee 0.85861833         CLOSE

Before putting the numbers side by side, here is what differs. These two must not be compared directly.

QuantDinger weltiq
Window 2026-09-15 ~ 09-21 (9,673 candles) last 168 hours (10,068 candles)
Sizing basis 95% of equity (≈0.11 BTC) fixed 0.01 BTC
Protective stop 5% (inside the assumptions) 5% (a separate stop order)
Fee assumption 0.05% measured rate
Fills 63 round trips / 116 fills 34 round trips / 68 fills
Total fees 303.20 53.42

The sizing basis differs by 11x, so the return and P&L numbers are not comparable. What is comparable is the order of magnitude of the fill count and the share of fees in the P&L, and both say the same thing — this rule went in and out far too often that week.

The weltiq strategy container — the rule QuantDinger chose, running resident

The evidence that the loop connected. The single rule line under the title is QuantDinger's chosen 60/180 carried into the weltiq grammar, and buy 0.01 @ 85979.70711050 in the SYMBOLS row, the two ORDERS lines (the filled market buy plus the protective stop in acked state) and the 0.85979707 USDT fee in FILLS are the same records as the output above. Auto-stop −150 · 9/22, 04:13 AM at the far right is the boundary drawn in step 1.

(3) That record goes back to QuantDinger. Pulling the real cost out of one fill gave a fee rate twice the assumption (0.0010 vs 0.0005) and slippage one fifth of it (0.0001 vs 0.0005). I put those two values in and re-ran the same grid. The human only carries them across — no interpreting, no hints.

fast / slow 1st (assumed cost) 2nd (measured cost) Fee change
10 / 30 −32.82% −35.55% 1,697 → 3,331
20 / 50 −19.00% −20.76% 981 → 1,941
30 / 80 −16.00% −17.33% 725 → 1,440
60 / 180 −5.34% −5.96% 316 → 630

The ranking did not change. 60/180 is still first, and the feedback failed to overturn the conclusion — that is a result too, and it goes in as it is. But the slope of "slower loses less" got steeper. Higher costs push the optimum slower. So I widened the grid by two, and 90/270 came out at −3.30% (54 trades, 446 fees, PF 0.586) and 120/360 at −1.99% (33 trades, 308 fees, PF 0.679) — the answer changed. One weltiq fill record changed QuantDinger's answer.

(4) And weltiq could not take that answer. Carrying it over as-is was rejected with 422 {"code":"invalid_rules","detail":"period_limit"} — the weltiq rule grammar caps the period at 200, so it cannot take 360. Rather than approximating, I leave that fact here. A wall-clock equivalent does exist, though, so I moved the one-minute 120/360 to five-minute 24/72 and resubmitted, and the new container filled 2 minutes 40 seconds later and completed a round trip (buy 0.01 @ 85795.14865650 / sell 0.01 @ 85788.41030050, realized −0.0673835600, fees 1.71583559). That was my translation choice, not QuantDinger's answer.

The loop went round once. Ten QuantDinger calls plus fourteen weltiq calls, and the only two things the human did were carrying values across and changing the timeframe because of the period cap.

I killed the deciding side

My favourite part of this one.

02:13:41  QuantDinger process terminated   → /api/health HTTP 000 (dead)
02:14:00  buy 0.01 BTCUSDT @ 85979.70711050  fee 0.85979707  OPEN   ← 19s later, order went out with it off
02:15:12  QuantDinger restarted            → {"status":"healthy","role":"legacy"}

For the 91 seconds the deciding side was off, a position opened and was held. And QuantDinger is fine the moment it comes back — all the state is in PostgreSQL and the process holds none of it. (Recovery of QuantDinger's own resident strategies could not be checked because I did not start any — unverified.)

I could not make the stop condition fire — unverified

I tried twice to trip a tight limit and failed both times. The first (one-minute 60/180, limit 1 USDT) produced no fills at all in four minutes (no_signal · reason: pine_hold), so the limit never got a chance. The second (five-minute 24/72, limit 0.5 USDT) should arithmetically breach it — the first fill's fee alone is 0.86 USDT — and yet 18 minutes later it had not stopped.

The record says why: performance.basis is avg_cost_realized_pnl_minus_fees_sampled_at_fills. weltiq's stop condition is based on realized P&L, and realized P&L is 0 while a position is open, so no amount of unrealized swing reaches the limit. After 18 minutes I issued a stop myself, and it closed on the next five-minute candle, and only then did realized_pnl −0.5711299200 appear. On the number alone that breaches the −0.5 limit, but the value came into existence after the stop (fired_on_its_own: false, waited_seconds: 1080).

In this run the stop condition was not confirmed "all the way to firing." What was confirmed is that the limit attaches and stays in the record, and that its basis is realized P&L; actual firing is unverified. Next time I will use a rule that produces a realized loss immediately.

What this combination does not give you

Putting the cost in accurately does not guarantee the ranking changes. The first four grid points kept their order, and the answer only moved once the grid widened. Cost feedback raises precision; it does not manufacture a different conclusion.

And the API for re-measuring with a different rate did not work this time — GET /v1/fees/compare refused on mixed currencies (mixed_currency_not_supported, ["USD","USDT"]) and there is no argument to isolate the crypto fills. Noted as weltiq's to fix. Multiplying by hand, this part's four fills (3,434.25 USDT notional) give actual (taker 0.10%) 3.43425099 · all maker (0.02%) 0.68685020 · at the assumed rate (0.05%) 1.71712550. Filling everything as maker would have cost a fifth as much.

And where QuantDinger is better

Stage QuantDinger weltiq
(1) Design (building the strategy) Strong — Python editor, AI conversation, indicator IDE, verification on the chart rule grammar, templates, Pine import
(2) Validation (backtesting) Strong — stated fill assumptions, ledger reconciliation, data snapshot hash replay of the same document
(3) Residency (running 24 hours) You have to keep your own server on This is weltiq's seat
(4) Promotion (to live) It goes all the way to live live_orders: false — not yet

(1) and (2) overlap, and honestly QuantDinger's side is thicker. Seeing the decision points on a chart, editing an indicator and redrawing it on the spot, talking to an AI with the source in front of you — weltiq has none of that. (3) is where they split. Running QuantDinger 24 hours means keeping that stack on, and what I measured alone is 5 processes and 1.05 GB. (4) splits the opposite way — QuantDinger places live orders and weltiq does not. But that also means a live-account key goes inside that stack, and from then on the operational load of (3) stops being an annoyance and becomes a risk.

The adapter seat exists; the registration desk does not. app/services/live_trading/contracts.py defines an ExchangeOrderAdapter protocol with types, and FillSnapshot even carries a per-currency fee breakdown (fees_by_ccy) and whether the fee is final (fee_status: "actual" | "pending") — having that field at all tells you the designer knows what they are doing. And yet the thing that actually picks a client, create_client() at factory.py:218, is a hardcoded chain of if exchange_id == "binance" / "okx" / … / "alpaca", with no plugin registration and no entry points. Line 605 of the README is more honest about it — it means fork it or send a PR. (import ccxt appears only for market data, never on the order path.) So the route actually used here was to change not one line of the repository: QuantDinger decides, and the order goes to the weltiq API.

In fairness, both sides have a thread that stitches the stages together — QuantDinger has a codeHash on the compile manifest and a content hash of the candle data used in the backtest's dataProvenance. The difference is how far that thread reaches. QuantDinger's goes all the way to its own execution; weltiq's reaches backtest and paper, and the live stretch is still not open (live_orders: false). It is a gate rather than a capability, and I write it down precisely.


6. After using it

What was good

  1. It states every fill assumption. The backtest result carries nearly twenty fields including fillRule, barClosePolicy: closed_bars_only, intrabarMode: conservative, fundingMode: not_modeled and protectionRule: gap_open_then_intrabar_trigger. A backtest engine that writes down "funding is not modeled" about itself is not common, and it attaches a Ledger reconciled badge on top
  2. It actually runs the AI's strategy and blocks it. The gate that stopped me three times in section 4. No randomness in the synthetic candles, and a check that a position was really opened in the requested direction. It blocked my work, and this is praise
  3. It respects other people's resources. Taking the DB pool, it reads max_connections and sets aside 20 for other clients. The sandbox blocks introspection functions like type() and getattr(), every write API requires an idempotency key, and orders and stops require a separate confirmation flag
  4. It can stand up without Docker. The official docs only describe Docker, but the compose file is readable enough to port straight to local processes — 4 minutes 53 seconds

What fell short

  1. indicator(period=) is silently ignored for TA-Lib indicators. The biggest problem found here. ema(period=5) and ema(period=200) return the same value, with no warning. It was the actual cause of the AI generation failures, and a human writing it by hand gets caught the same way. The workaround is timeperiod=, and yet the tool's own authoring contract uses period=
  2. It returns nothing on failure. Across three failed AI generations all I got was one error string. Not the generated code, not which condition failed to open. The better the gate, the more it needs to return why it caught you
  3. There is a silent success. Pass a JSON array as an Agent token's markets and you get HTTP 200 and a token — and that token can use no market at all. That input should have been rejected in validation
  4. Residency is an operations job. 5 processes, 1.05 GB. Perfectly fine if you backtest and switch it off, but if you plan to run it 24 hours, from that point you are operating a server

Cost

Item Value
Code · market data Free (Apache-2.0 · 0 exchange API keys · no signup)
Model Only if you use the AI features. Zero if you don't
Time · resources Install 4 min 53 s · disk 1.5 GB · memory 1.05 GB (with the dashboard)

Who it fits — anyone who writes strategies in Python themselves and wants to fix them while watching a chart. Editor, chart, backtest and order panel in one screen is the most complete arrangement in this series. Who it does not fit — anyone who wants "no install, just go", and anyone who only needs 24-hour residency with the strategy already decided.


7. Coming next

Part 15 is moss-site/moss-trade-bot-skills (★385).

Where QuantDinger is a platform that has you stand up PostgreSQL and Redis, Moss is a skill bundle whose body is one SKILL.md. The weight is inverted. And what sits in the deciding seat is not the repository's code but your own LLM, having read those instructions, so "one-sentence mission → 36 parameter fields" finishes in a single exchange. We will see what becomes of the AI strategy generation that failed three times here.

Getting ready for weltiq — 3 lines 1. Sign up: {{CTA_URL}} 2. My page → Connections, issue an API key 3. claude mcp add weltiq --header "Authorization: Bearer <key>" https://weltiq.ai/mcp

That is where to look when you would rather hand off the residency seat than keep the QuantDinger stack on 24 hours a day.


Every command, output and number in this article came from actual runs on 2026-09-22. Zero exchange keys, no live-account connection, every order simulated. None of it is a trading opinion. All four weltiq containers created here were stopped and deleted. The trial and error, raw logs and actual LLM spend are in the verification record.