Files
Auto-Trader/agent/backtest/engines/china_a.py
T
Haozhe Wu 8a6c578a76 chore: cleanup pass — 3 latent bug fixes + 913 LOC net delete
Bug fixes
- CompositeEngine no longer misroutes bare Chinese-futures codes like
  RB2410 to GlobalFuturesEngine. _is_china_futures moved into a shared
  _market_hooks module with a case-normalized product table and a
  non-CN exchange guard. 9 new regression cases (RB2410 / rb2410 /
  RB2410.SHFE / CL.NYMEX / M2412.CBOT / CF2412.ICE / AU2412.COMEX /
  FG2412.EUREX / bare CN-collision still routes Chinese).
- Session FTS5 indexes now persist timestamps so cross-session search
  can sort by date. Same path also fixes a re-upsert that was
  wall-clocking every session's started_at on title-rename.
- Vite dev-mode proxy gained the missing /alpha entry; AlphaZoo page
  resolves on npm run dev.

Cleanup
- Deleted swarm/api_models.py + swarm/mailbox.py + SwarmDashboard.tsx
  (dead modules / orphaned UI after swarm-via-agent flow).
- Deleted SessionService.resume_attempt + get_attempts + get_attempt
  + SessionStore.list_attempts + Attempt.mark_waiting_user chain
  (half-baked human-in-the-loop from initial commit).
- Deleted ChatLLM.achat (async sibling never called) and
  _extract_balanced_json (only-tested helper).
- Deleted american_exercise_value in options_portfolio.py
  (driver inlines bs_price logic).
- Deleted _compute_ic_series_reference in factor_analysis_core.py
  (kept-as-reference, no callers).
- Collapsed MEMORY_TYPES duplication between cli.py and
  memory.persistent; restored set-equality invariant assert.
- Deleted runSwarm (~197 LOC), swarmDash state, swarmCancelRef
  write-only ref (orphaned by new agent-driven swarm flow).
- Dropped 76 unused 'vw = vwap(...)' boilerplate lines across
  gtja191 alphas + their now-unused vwap imports.
- Cleared 11 F401 unused-import sites in tools/, agent/, providers/.

Test infrastructure
- test_e2e_harness_v2.py (real-LLM e2e) gated behind
  VIBE_TRADING_RUN_LIVE_E2E so CI no longer flips shape based on
  env-key presence. CI workflow now has explicit --ignore.
- test_auto_compact_with_long_history: monkeypatch TOKEN_THRESHOLD so
  the test deterministically triggers compact instead of relying on
  unrelated LLM behaviour.
- test_compact_tool_explicit: assertion corrected — compact tool is
  special-cased in loop._process_tool_calls and does NOT emit
  tool_call events. Assert on the 'compact' event instead.
- TestSwarmE2E: per-test @pytest.mark.timeout overrides matching the
  internal 300s poll deadlines; cancel test now polls for terminal
  state instead of fixed sleep(3).

Tooling
- ruff per-file-ignores for factors/zoo/** (3783 → 0 F401 noise);
  F841 stays active in zoo to catch real formula bugs.
- frontend tsconfig: noUnusedLocals + noUnusedParameters as
  regression guards.
- highlight.js hoisted to direct dependency (was transitive via
  rehype-highlight).
- Vite proxy: dropped unused /run, /skills, /health, /api, /system;
  added /alpha.

Annotations
- direction parameter renamed to _direction in 6 calc_commission
  signatures (reserved for future borrow-fee asymmetry).
- _MARGIN_PER_CONTRACT in global_futures.py annotated as reference
  data.
- Backtest engines' _calc_pct_change variants cross-reference each
  other in docstrings.

Docs
- 2026-05-18 news entry added to README.md / README_zh.md /
  README_ja.md / README_ko.md / README_ar.md; 2026-05-15 entry
  rolled into the Earlier news <details>.
- Contributors heading: v0.1.7 cycle → v0.1.8 cycle in all 5
  READMEs.

126 files changed, +363 / -1276.
2026-05-18 15:16:24 +08:00

156 lines
5.4 KiB
Python

"""A-share (China mainland) backtest engine.
Market rules:
- T+1: cannot sell shares bought today
- No short selling for retail investors
- Price limits: ±10% main board, ±20% ChiNext/STAR, ±5% ST
- Minimum lot: 100 shares (odd lots can only be sold, not bought)
- Commission: ¥5 minimum, 0.025% bilateral
- Stamp tax: 0.05% sell-side only
- Transfer fee: 0.001% bilateral
"""
from __future__ import annotations
import pandas as pd
from backtest.engines.base import BaseEngine
class ChinaAEngine(BaseEngine):
"""A-share market engine.
Config keys:
- commission_rate: default 0.00025 (万2.5)
- commission_min: default 5.0 (RMB)
- stamp_tax: default 0.0005 (万5, sell-only)
- transfer_fee: default 0.00001 (万0.1)
- slippage: default 0.001
"""
def __init__(self, config: dict):
config = {**config, "leverage": 1.0} # A-shares: no leverage
super().__init__(config)
self.commission_rate: float = config.get("commission_rate", 0.00025)
self.commission_min: float = config.get("commission_min", 5.0)
self.stamp_tax: float = config.get("stamp_tax", 0.0005)
self.transfer_fee: float = config.get("transfer_fee", 0.00001)
self.slippage_rate: float = config.get("slippage", 0.001)
def can_execute(self, symbol: str, direction: int, bar: pd.Series) -> bool:
"""A-share execution rules.
Args:
symbol: Stock code (e.g. 000001.SZ).
direction: 1 (buy), -1 (short — always blocked), 0 (sell/close).
bar: Current bar (needs 'close', 'pre_close' or 'pct_chg').
Returns:
True if the trade is allowed.
"""
# 1. No short selling
if direction == -1:
return False
# 2. T+1: can't sell shares bought today
if direction == 0:
pos = self.positions.get(symbol)
if pos is not None:
bar_date = _bar_date(bar)
entry_date = pos.entry_time.date() if hasattr(pos.entry_time, "date") else None
if bar_date is not None and entry_date is not None and bar_date == entry_date:
return False
# 3. Price limits
pct_chg = _calc_pct_change(bar)
if pct_chg is not None:
limit = _price_limit(symbol)
if direction == 1 and pct_chg >= limit - 0.001:
return False # limit-up: can't buy
if direction == 0 and pct_chg <= -limit + 0.001:
return False # limit-down: can't sell
return True
def round_size(self, raw_size: float, price: float) -> float:
"""Round down to 100-share lots."""
return max(int(raw_size / 100) * 100, 0)
def calc_commission(self, size: float, price: float, _direction: int, is_open: bool) -> float:
"""A-share fee structure: commission + stamp tax (sell) + transfer fee.
``_direction`` is unused today — reserved for future asymmetric
long/short fee schedules (margin trading, securities lending).
"""
notional = size * price
# Commission: 万2.5, min ¥5
comm = max(notional * self.commission_rate, self.commission_min)
# Transfer fee: 万0.1 bilateral
comm += notional * self.transfer_fee
# Stamp tax: 万5 sell-only
if not is_open:
comm += notional * self.stamp_tax
return comm
def apply_slippage(self, price: float, direction: int) -> float:
"""A-share slippage (relatively small due to tick size)."""
return price * (1 + direction * self.slippage_rate)
# ── Helpers ──
def _bar_date(bar: pd.Series):
"""Extract date from bar, handling various column names."""
for col in ("trade_date", "date"):
if col in bar.index:
val = bar[col]
if hasattr(val, "date"):
return val.date()
try:
return pd.Timestamp(val).date()
except Exception:
pass
# Fall back to bar name (index timestamp)
if hasattr(bar, "name") and hasattr(bar.name, "date"):
return bar.name.date()
return None
# Note: china_futures and global_futures have variants that prioritise
# settle/pre_settle (futures-native); see those modules for the
# futures-specific logic.
def _calc_pct_change(bar: pd.Series):
"""Calculate price change percentage from bar data."""
if "pct_chg" in bar.index:
val = bar["pct_chg"]
if pd.notna(val):
return float(val) / 100.0 # tushare pct_chg is in percentage points
close = bar.get("close")
pre_close = bar.get("pre_close")
if close is not None and pre_close is not None and pre_close > 0:
return (float(close) - float(pre_close)) / float(pre_close)
return None
def _price_limit(symbol: str) -> float:
"""Determine price limit based on board.
Args:
symbol: Stock code (e.g. 300001.SZ, 688001.SH, 000001.SZ).
Returns:
Limit as fraction (0.10, 0.20, or 0.05).
"""
code = symbol.split(".")[0] if "." in symbol else symbol
# ChiNext (300xxx) / STAR (688xxx): ±20%
if code.startswith("300") or code.startswith("688"):
return 0.20
# ST stocks: ±5% (heuristic: can't fully detect from code alone)
# Beijing exchange (8xxxxx): ±30% — simplified to 0.30
if code.startswith("8") and len(code) == 6:
return 0.30
# Main board: ±10%
return 0.10