tqx · diff

v2.3.4 to v2.4.0

1 added, 1 removed. Audit A to A.

---
name: tqx
- version: 2.3.4
+ version: 2.4.0
description: |
TQX (tqx.trade) HK/US stock quant: factor analysis, strategy backtests, and agent-driven trading (paper or live) via the TQX CLIs.
Use when the user wants to run factor IC/IR analysis, backtest a Python trading strategy on Hong Kong or US stocks, or set up agent-automated trading on a paper or live account (e.g. "backtest a moving-average strategy on AAPL", "analyze a momentum factor on HK stocks", "let the agent trade my TQX account", "join the TQX trading competition").
author: starchild
tags: [quant, backtest, factor-analysis, stocks, tqx, trading]
---
# TQX Quant — Factor Analysis & Strategy Backtest
TQX (https://www.tqx.trade) is a HK/US stock quant platform. This skill drives it through the official `tqx-cli` pip package.
## Official TQX skills (source of truth — read these first)
TQX publishes two official skills. Fetch the raw URLs directly (the repo UI is a JS SPA; only `/-/git/raw/` URLs return content):
**1. tqx-research** — factor analysis, strategy backtest, workflow management (Python `tqx-cli`, email/password login):
- Main: `https://cnb.cool/liangyunzhijing/clis/-/git/raw/main/skills/tqx-research/SKILL.md`
- References (read before generating code — do NOT guess APIs):
- commands: `.../skills/tqx-research/references/commands.md`
- strategy templates: `.../skills/tqx-research/references/strategy_templates.md`
- US API: `.../skills/tqx-research/references/stock_us_api.md`
- HK API: `.../skills/tqx-research/references/stock_hk_api.md`
- tqx_data: `.../skills/tqx-research/references/tqx_data_usage.md`
(replace `...` with `https://cnb.cool/liangyunzhijing/clis/-/git/raw/main`)
**2. tqx-trading** — account/positions/orders/trades queries + authorized order placement (TypeScript `@tqx-ai/cli@0.1.3`, API-key auth, pin the version):
- Main: `https://cnb.cool/liangyunzhijing/clis/-/git/raw/main/skills/tqx-trading/SKILL.md`
- OpenAPI: `https://www.tqx.trade/openapi/v1/openapi.json` (interactive: `/openapi/v1/scalar`)
**Routing rule:** research/backtest/factor work → tqx-research (`tqx-cli`, Python). Live account state & order execution → tqx-trading (`tqx`, TS CLI). They use DIFFERENT CLIs and DIFFERENT auth (email login vs `TQX_API_KEY`) — don't mix them. This skill below adds what the official docs don't cover: verified onboarding, failure-mode table, and agent-automation patterns.
## User onboarding (first-time setup, ~3 minutes)
1. **Register** at https://www.tqx.trade (email signup). A PAPER (simulation) account is provisioned automatically — all workflows below are safe to run on it. Paper and live are the SAME API: `tqx trading account` reports `"mode": "PAPER"` or `"mode": "LIVE"`, and every command (`positions`, `orders place|modify|cancel`, `trades`) is identical on both. Switching to real money is an account-level change on TQX, not a different code path — which is exactly why order-size caps and the human-approval rule below matter.
**Trading competition:** TQX runs an agent trading competition — entry and rules at https://luma.com/dawwrdxk?tk=adzfZA. Competition accounts are created from the same dashboard (https://www.tqx.trade/hk/competition-manage?catalog=2), which is also where the `TQX_API_KEY` for the trading CLI is issued.
2. **Collect credentials securely**: agents must use `request_env_input` for `TQX_EMAIL` and `TQX_PASSWORD` — never ask for credentials in chat.
3. **Install + login + verify**:
```bash
pip install tqx-cli
tqx-cli login --email "$TQX_EMAIL" --password "$TQX_PASSWORD"
tqx-cli --json balance # non-error response = onboarding complete
```
Token is cached in `~/.tqx/config.yaml`.
4. **First quick win** (recommended demo): run the "5d momentum" factor analysis from §1 below — completes in ~1–2 min and produces IC/IR/Sharpe numbers you can show immediately.
**⚠️ Token expiry gotcha:** both accessToken AND refresh_token can expire together. Do NOT only match one specific error string — re-login on ANY response containing `LOGIN_REQUIRED`, `均已失效`, or `Please log in to continue`. A strict matcher silently fails and every later call returns auth errors.
## CLI command map
```
factor_create / factor_run / factor_result / factor_list / factor_delete
strategy_create / strategy_run / strategy_result / strategy_list / strategy_delete
backtest_result # per-backtest detail: summary/account/position/profit/trade/log sections
workflow_list / workflow_stop / balance
```
Add `--json` for machine-readable output.
## 1. Factor analysis (cross-sectional IC/IR)
```bash
tqx-cli --json factor_create --market us --name "5d momentum" \
--formula "close/ref(close,5)-1" \
--start-date 20250101 --end-date 20250701 --group-number 2
tqx-cli --json factor_run <factor_id> # waits and returns results
```
**Result parsing gotcha:** the result JSON has TWO formats depending on backend version — legacy `nodes[].result_json` and current root-level `factor_analysis`. Handle both. Key metrics: IC mean, IR, t-stat, annualized group returns, Sharpe.
## 2. Strategy backtest (panda_backtest engine)
```bash
tqx-cli --json strategy_create --market us --name "AAPL SMA cross" \
--code "$(cat strategy.py)" \
--start-date 20250101 --end-date 20250701 \
--start-capital 1000000 --commission-rate 0.0003 --slippage 0.001 --frequency 1d
tqx-cli --json strategy_run <strategy_id>
```
### Strategy code contract
```python
from panda_backtest.api.api import * # common trading API — MANDATORY
from panda_backtest.api.stock_us_api import * # US market data API — MANDATORY
# HK market: from panda_backtest.api.stock_hk_api import *
import tqx_data
def initialize(context):
# Account ID is per-user — discover it once, do NOT hardcode.
# The '8888' from CN-market docs does NOT exist for HK/US backtests.
context.account = list(context.stock_account_dict.keys())[0]
context.symbol = 'AAPL.NB' # symbol format: US = TICKER.NB (NOT .US!), HK = 00700.HK
context.closes = []
def handle_data(context, data):
account = context.stock_account_dict.get(context.account)
if account is None:
return
bar = data.get(context.symbol)
# bar CAN be None — with a WRONG suffix it is None EVERY day (silent 0-trade run)
if bar is None or getattr(bar, 'close', None) is None or float(bar.close) <= 0:
return
price = float(bar.close)
context.closes.append(price)
if len(context.closes) < 20:
return
fast = sum(context.closes[-5:]) / 5
slow = sum(context.closes[-20:]) / 20
position = account.positions.get(context.symbol)
quantity = 0 if position is None else position.quantity
sellable = 0 if position is None else position.sellable
if fast > slow and quantity == 0:
buy_qty = int(account.cash * 0.9 // price)
if buy_qty > 0:
order_shares(context.account, context.symbol, buy_qty, style=MarketOrderStyle)
print(f"BUY {buy_qty} @ {price:.2f}") # print() = strategy log; SRLog is FORBIDDEN
elif fast < slow and quantity > 0 and sellable > 0:
order_shares(context.account, context.symbol, -sellable, style=MarketOrderStyle)
print(f"SELL {sellable} @ {price:.2f}")
```
Reference run: this exact code on AAPL.NB, 20250101–20251231, produces 18 real fills (check with `backtest_result <backtest_id> --section trade --all-pages`).
Hard rules learned from real failures:
| Symptom | Root cause | Fix |
|---|---|---|
| `禁止使用危险函数 dir()` | Security filter blocks introspection | Never use `dir()`/`eval()`/`exec`; to inspect context, `raise Exception(str(...))` and read the error_detail in run logs |
| `访问了不存在的键` on order | Wrong account ID (e.g. `'8888'`) | Use `list(context.stock_account_dict.keys())[0]` |
| `order_shares() missing 1 required positional argument` | Called with 2 args | Signature is `order_shares(account, symbol, quantity)` |
| `股票X不属于当前股票回测市场` | Wrong symbol suffix | US = `TICKER.NB` (NOT `.US`!), HK = `XXXXX.HK`; `.O`/`.N`/bare tickers are rejected |
| Backtest SUCCESS but 0 trades, 0 log lines, profit = 0.0 | Symbol suffix `.US` (or any wrong suffix) → every bar returns None → defensive guard skips all days silently | Use `TICKER.NB` for US stocks. ALWAYS verify via `backtest_result <id> --section trade` — SUCCESS ≠ trades executed |
| Run FAILED immediately (~0.3s, node failed) | `SRLog` is not a valid API in strategy code | Use plain `print()` for strategy logging (visible in `--section log`) |
| Run status FAILED but NO failed node (all nodes success/pending) | Transient TQX queue/scheduler error, not your code | Resubmit the same workflow once — typically succeeds in ~30s. Only debug strategy code if a node actually failed |
| Backtest SUCCESS but 0 trades, `标的不在当前回测数据集内` | Date range beyond ingested market data (recent months may not be loaded even though the benchmark series exists) | Shift the window earlier (e.g. use last year's range); verify trades>0 in the `trade` section before trusting metrics |
| `frequency` rejected | Only `1d` and `1M` are valid | — |
### Reading results
`strategy_run`/`strategy_result` returns run status + node outputs. For full detail, extract the backtest id from run logs (`BacktestNodeIdentifier:` line) or node output, then:
```bash
tqx-cli --json backtest_result <backtest_id> --section summary # profit, alpha, beta, sharpe, IR
tqx-cli --json backtest_result <backtest_id> --section trade # ⚠️ always check trades executed
tqx-cli --json backtest_result <backtest_id> --section log # per-order rejection reasons
```
**A run can report SUCCESS with zero trades** (orders silently rejected day by day). Always confirm the `trade` section is non-empty before reporting performance numbers.
#### Daily equity curve (`--section profit`)
The `profit` section is the day-by-day NAV series — one row per trading day with `gmt_create` (YYYYMMDD), `strategy_profit` (cumulative strategy return, decimal) and `csi_stock` (cumulative benchmark return, decimal).
```bash
# ⚠️ --all-pages does NOT work for the profit section: it silently returns only page 1 (100 rows).
tqx-cli --json backtest_result <backtest_id> --section profit --page-size 1000
# -> pagination {total: 252, page: 1, page_size: 1000, total_pages: 1} ✅ full year in one call
```
Two more behaviours to handle (reference: AAPL.NB, full-year 2025 = 252 rows):
- Rows come back **unsorted** — always sort by `gmt_create` ascending before plotting; do not assume the API order.
- Values are **decimals, not percent** (`-0.1174` = −11.74%) and are already cumulative, so plot them directly; don't compound them again.
- The last row must match the `summary` section's total return — use that as your correctness check.
Cache the series locally (one file per run) instead of re-fetching: the call costs ~2–4 s and the data is immutable once the run is done.
### Debugging failed runs
Error details are NOT in the top-level status — fetch run logs and read `error_detail`, which includes the exact strategy line number and exception message:
```python
from tqx_cli.config import load_config
from tqx_cli.auth import require_login
from tqx_cli.workflow import get_run_logs
cfg = load_config(); token, uid, _ = require_login(cfg, cfg.get("_config_path"))
for l in get_run_logs(cfg, token, uid, run_id).get("logs") or []:
if l.get("error_detail"): print(l["error_detail"])
```
## Cost & pacing
Backtests are billed in TQX compute credits (`tqx-cli balance`). A 6-month daily-frequency single-stock backtest takes ~2 minutes wall time. Poll `strategy_result` every 3s; don't fire concurrent runs of the same workflow.
## 3. Templates (copy-paste starting points)
### T1 — Momentum factor (cross-sectional, whole market)
```bash
tqx-cli --json factor_create --market us --name "5d momentum" \
--formula "close/ref(close,5)-1" \
--start-date 20250101 --end-date 20250701 --group-number 5
```
Other verified formulas: mean reversion `-(close/ref(close,5)-1)`, volume surge `volume/mean(volume,20)`.
Factor mode is whole-market cross-sectional only (`--market hk|us`) — it CANNOT target one stock; for single-stock questions use a strategy backtest (T2).
### T2 — Single-stock backtest (SMA cross on AAPL)
Use the strategy code contract in §2 verbatim — it IS the template. Change `context.symbol` and the signal logic only. Keep the None-bar guard and dynamic account discovery.
### T3 — Agent-driven automated trading loop
Pattern verified over a 10-round live run (~21 min, end-to-end):
```
loop every N minutes:
1. fetch live positions + account state
2. compute signal (factor value or strategy rule)
3. decide: buy / sell / hold ← agent reasoning step
4. place order (paper account, small fixed qty during development)
5. journal the decision: {ts, reasoning, tool_calls, params, result, position_delta}
```
Hard rules for automation:
- **PAPER account by default.** Live trading uses the identical command surface, so nothing in the code stops a live order — only your gating does. Require an explicit user approval before pointing the loop at a live account, keep a hard order-size cap, and always pass `--idempotencyKey` so a retry can never double-fill.
- **Journal every decision** (JSONL is enough) — users must be able to audit why each trade happened.
- **Re-login on ANY auth-ish error string** (see token gotcha above); a mid-loop token expiry must self-heal, not kill the loop.
- **Check compute balance before each backtest-class call** to avoid silent overdraft.
### T4 — Studio: ready-to-run companion UI (`templates/studio/`)
A complete, tested web workbench ships with this skill — do NOT build a dashboard from scratch. Copy `templates/studio/` into the user's workspace, start it, and adapt.
**Files:**
| File | Role |
|---|---|
| `server.py` | Stdlib HTTP backend (port 8090, no pip deps). Proxies `tqx-cli`, auto re-login on token expiry, serves all `/api/*` routes |
| `index.html` | Single-file frontend: factor analysis, backtest submit/history, positions, agent decision timeline |
| `agent.py` | Agent trading loop (LLM via `proxied_post` + tool calls), JSONL decision journal |
| `backtests.py` / `strategies.py` / `journal.py` | Disk persistence modules (see data spec below) |
**UI language:** the template ships English-only. Any Chinese you see at runtime comes from the TQX API itself (metric key names, error strings) — do not translate those, they are matched against.
**Run:** `python3 server.py` from the studio dir (background), then `preview(action="serve")` on it. Credentials come from `TQX_EMAIL` / `TQX_PASSWORD` env vars (collect via secure input — never hardcode).
**Design spec (keep when restyling):**
- Minimalist quant-tech aesthetic: dark-first, monospace numerals, dense tables, no decorative graphics. Avoid bright or cyberpunk themes.
- History rows must be loadable: clicking Load on any history record re-displays its full result in the top result area (factor runs → factor metrics/charts via `loadBt`; strategy runs → dedicated strategy-result card via `loadStgBt`: return vs benchmark, annualized, Sharpe, max drawdown, trade count, params + code link). A history table that only lists rows without load-back is incomplete.
- UI exposes only high-frequency params (formula, market, date range, groups, rebalance, direction) + account switcher + NAV curve + decision timeline. Everything complex (custom strategy code, stock pools, commission/slippage) stays in the conversation layer — the agent has the full CLI surface, the UI must not duplicate it.
- Amounts: format gold/cash values with a single dedicated formatter two competing formatters produce inconsistent output. Check dark-mode contrast on every card.
**Persistence & paths (why the two directories):**
`tqx-cli` writes its login config to `/root/.tqx/config.yaml`, which lives on the container's ephemeral layer and is wiped on every machine restart — the agent would silently lose its session. Only `/data/workspace` survives restarts, so the studio mirrors the config into `/data/workspace/.tqx/` and syncs both ways on startup (`sync_tqx_config()` in `server.py`, `_sync_tqx_config()` in `agent.py`).
These paths are intentionally fixed, not a portability oversight: every agent has the identical layout — `/data/workspace` = persistent volume, `/root` = ephemeral. Keep the rule when extending the studio: **anything that must survive a restart is written under the workspace, never under `/root` or `/tmp`.** In-studio state (`data/backtests`, `data/journal`, `data/strategies`) is already `__file__`-relative, so it inherits persistence as long as the studio itself is copied into the workspace. If you ever run this outside a Starchild machine, override the persistent root — nothing else in the code assumes an absolute path.
**Data storage spec:**
- `data/backtests/index.json` + one JSON per run — MUST persist the full strategy code string (not a truncated preview) so any run can be reloaded and re-edited.
- `data/journal/*.jsonl` — one line per agent decision, tagged `source: manual | agent`, append-only, never rewritten.
- `data/strategies/` — named saved strategies. All state is plain JSON on disk; no DB.
**Testing spec (before declaring the studio 'working'):**
1. `curl localhost:8090/api/health` (or any GET route) returns JSON — backend alive.
2. Submit one REAL backtest through the UI and confirm it appears in the history panel with full code + metrics. A rendered page alone is NOT verification.
3. Verify fills exist via `backtest_result <id> --section trade` — SUCCESS status with 0 trades means a symbol-suffix bug (see failure table).
4. Kill/restart `server.py` and confirm history persists (disk, not memory).