Files
chinese-equity-quant/docs/joinquant_comparison_plugin.md
T
2026-07-04 17:43:09 +08:00

7.7 KiB

JoinQuant Comparison Plugin

Why a Plugin

JoinQuant is an external execution and simulation reference. Keeping this code under plugins/joinquant/ prevents vendor-specific assumptions from entering pipeline/portfolio/, where the internal reference simulator remains the canonical implementation.

What It Validates

The comparison is for system correctness:

  • date alignment
  • internal to JoinQuant symbol mapping
  • target position generation
  • once-per-day open execution timing
  • lot rounding and filled shares
  • position carry
  • trading cost
  • PnL accounting
  • blocked trades from suspension, limit-up, and limit-down conditions

What It Does Not Validate

It does not validate alpha quality, IC, IR, forecast skill, or whether the strategy is economically useful. Differences can be expected when JoinQuant uses different fee, slippage, cash, corporate-action, or internal rounding rules.

Historical Backtest Workflow

# 1. Build internal portfolio targets.
uv run python cli.py portfolio build ...

# 2. Export JoinQuant-compatible frozen targets.
uv run python cli.py joinquant export-targets \
  --positions-path portfolio/run1.pq \
  --portfolio-name run1 \
  --mode target_shares \
  --out-dir plugins_output/joinquant/targets

# 3. Generate and copy the wrapper strategy and target files into JoinQuant.
uv run python cli.py joinquant write-wrapper \
  --portfolio-name run1 \
  --mode target_shares \
  --out-path plugins_output/joinquant/wrapper_strategy_run1.py

# 4. Run the JoinQuant backtest or simulated trading job.
# 5. Export JoinQuant fills, positions, and daily PnL to CSV.

# 6. Ingest JoinQuant output.
uv run python cli.py joinquant ingest \
  --portfolio-name run1 \
  --fills-csv path/to/jq_fills.csv \
  --positions-csv path/to/jq_positions.csv \
  --pnl-csv path/to/jq_pnl.csv

# 7. Reconcile.
uv run python cli.py joinquant reconcile \
  --portfolio-name run1 \
  --targets-dir plugins_output/joinquant/targets/run1 \
  --our-fills-path fills/run1.pq \
  --our-positions-path portfolio/run1.pq \
  --our-pnl-path pnl/run1.pq \
  --jq-fills-path plugins_output/joinquant/ingested/run1/fills.pq \
  --jq-positions-path plugins_output/joinquant/ingested/run1/positions.pq \
  --jq-pnl-path plugins_output/joinquant/ingested/run1/pnl.pq

Forward-Testing Workflow

After the T-1 close and after the data update:

uv run python cli.py portfolio build ...
uv run python cli.py joinquant export-targets \
  --positions-path portfolio/run1.pq \
  --portfolio-name run1 \
  --mode target_shares \
  --start-date T \
  --end-date T

Before the T open, upload or expose the frozen target file to JoinQuant. During the T open, the JoinQuant wrapper reads that file and submits orders, while the internal simulator should run against the same frozen target. After T close or after JoinQuant results are available, ingest the JoinQuant CSV files and run joinquant reconcile.

Forward target files must be frozen before execution. Do not regenerate a target file after observing open or close data for the same trading date. The exporter writes a snapshot JSON with a SHA-256 hash for this reason and refuses to overwrite existing target/snapshot files unless --force is passed.

Target-Shares Mode

target_shares is the default and preferred correctness mode. The exported target_shares field comes from the internal position_shares column produced by portfolio build, because the internal simulator executes that discretized integer book. The generated wrapper calls:

order_target(jq_symbol, target_shares)

This mode makes filled shares, position carry, and blocked trades easiest to compare.

Target-Value Mode

target_value mode exports target_value and target_weight from the portfolio file. The generated wrapper calls:

order_target_value(jq_symbol, target_value)

This can be useful for portfolio-level comparisons, but JoinQuant may apply its own rounding, cash, and lot rules. Differences are often classified as JOINQUANT_INTERNAL_ROUNDING, LOT_ROUNDING, or CASH_CONSTRAINT depending on the observed output.

Symbol Mapping

Internal symbols are converted as follows:

sh600000 -> 600000.XSHG
sh688001 -> 688001.XSHG
sz000001 -> 000001.XSHE
sz300001 -> 300001.XSHE

Reverse mapping is also supported. Invalid exchanges or unsupported A-share prefixes raise ValueError instead of silently guessing.

Wrapper Strategy Usage

Generate a configured wrapper:

uv run python cli.py joinquant write-wrapper \
  --portfolio-name run1 \
  --mode target_shares \
  --out-path plugins_output/joinquant/wrapper_strategy_run1.py

Copy the generated file and daily CSV target files into JoinQuant. The default loader uses JoinQuant read_file, which works for uploaded files. If your JoinQuant runtime allows HTTP or another storage backend, replace only _read_target_file() in the generated strategy.

The wrapper is long-only by default:

ALLOW_SHORT = False

Negative targets are clipped to zero and logged. Use --allow-short only if the target JoinQuant account supports the required shorting mechanics.

Ingesting JoinQuant Outputs

The ingest command accepts permissive CSV column names and writes strict plugin schemas:

plugins_output/joinquant/ingested/{portfolio_name}/fills.pq
plugins_output/joinquant/ingested/{portfolio_name}/positions.pq
plugins_output/joinquant/ingested/{portfolio_name}/pnl.pq

Missing cost fields default to zero. Missing blocked status defaults to zero. Symbols and dates are normalized.

Reading Reconciliation Reports

The reconcile command writes:

plugins_output/joinquant/reconcile/{portfolio_name}/daily_reconcile.pq
plugins_output/joinquant/reconcile/{portfolio_name}/summary.csv
plugins_output/joinquant/reconcile/{portfolio_name}/summary.md

daily_reconcile.pq is per-symbol and includes target shares, internal filled shares, JoinQuant filled shares, realized positions, trade prices, costs, PnL, and a diff_reason. summary.csv is the daily portfolio-level view for gross exposure, net exposure, cash, total value, PnL, cumulative PnL, turnover, and cost.

Difference reasons include:

MATCH SYMBOL_MAPPING PRICE_MISMATCH LOT_ROUNDING SUSPENSION LIMIT_UP_BLOCK
LIMIT_DOWN_BLOCK VOLUME_OR_LIQUIDITY COST_MODEL CASH_CONSTRAINT
SHORT_NOT_SUPPORTED CORPORATE_ACTION JOINQUANT_INTERNAL_ROUNDING
MISSING_IN_OUR_SYSTEM MISSING_IN_JOINQUANT UNKNOWN

Default tolerances are exact share matching, 1e-4 relative trade-price tolerance, and value tolerance max(1 yuan, 1e-6 * booksize). PnL tolerance is configurable with --pnl-tolerance.

Minimal Example

Create a 5-stock equal-weight or fixed-share test portfolio:

sh600000, sz000001, sh600519, sz002594, sz300750

Build positions for a small date range, export target_shares, upload the CSV files and wrapper to JoinQuant, run the JoinQuant backtest, export fills, positions, and PnL, then run ingest and reconcile. Start with one or two days before expanding the sample.

  1. One liquid stock with a fixed target share count.
  2. A 10-stock equal-weight long-only portfolio.
  3. A forced suspension, limit-up, and limit-down sample.
  4. A short target in long-only mode to confirm SHORT_NOT_SUPPORTED.
  5. A 5-day reversal portfolio after the mechanical checks pass.

Known Limitations

  • JoinQuant internal execution details may differ from the reference simulator.
  • External file loading depends on the JoinQuant environment.
  • Short selling may not be supported.
  • Fee, tax, slippage, and minimum-fee models may differ.
  • Corporate actions may need special handling and should not be hidden.
  • The internal simulator does not currently emit execution price in FILL_COLUMNS; price reconciliation uses explicit price columns if supplied.