Add JoinQuant simulated trading automation

This commit is contained in:
Yuxuan Yan
2026-07-04 18:12:01 +08:00
parent 39a93259e1
commit d05931f41a
5 changed files with 311 additions and 72 deletions
+172 -66
View File
@@ -1,4 +1,4 @@
"""Browser automation for JoinQuant cloud backtest runs.
"""Browser automation for JoinQuant cloud backtest and simulated-trading runs.
JoinQuant's public ``jqdatasdk`` is a data API; cloud strategy upload/run/export
is exposed through the web application. This module keeps that automation
@@ -29,15 +29,124 @@ def _require_playwright():
raise RuntimeError(
"Playwright is required for JoinQuant browser automation. Install it "
"in this uv environment, then install Chromium:\n"
" uv pip install playwright\n"
" uv sync --extra joinquant-browser\n"
" uv run playwright install chromium"
) from exc
return sync_playwright, PlaywrightTimeoutError
def default_browser_config(strategy_url: str = "") -> dict[str, Any]:
def _backtest_actions() -> list[dict[str, Any]]:
return [
{"type": "goto", "url": "{strategy_url}"},
{
"type": "paste_text_file",
"selector": "textarea, .ace_text-input, .cm-content, .CodeMirror textarea",
"path": "{wrapper_path}",
"description": "Paste generated wrapper strategy into the code editor.",
},
{
"type": "set_input_files",
"selector": "input[type=file]",
"paths": "{target_csvs}",
"description": "Upload all aligned daily target CSV files.",
"optional": True,
},
{
"type": "fill",
"selector": "input[name=start_date], input[placeholder*=开始]",
"text": "{backtest_start_date}",
"optional": True,
},
{
"type": "fill",
"selector": "input[name=end_date], input[placeholder*=结束]",
"text": "{backtest_end_date}",
"optional": True,
},
{
"type": "click",
"selector": "text=/运行回测|开始回测|回测|Run Backtest|Run/i",
"description": "Start the JoinQuant backtest.",
},
{
"type": "wait_for_selector",
"selector": "text=/回测完成|运行完成|Backtest complete|Finished/i",
"timeout_ms": 600_000,
"optional": True,
},
{
"type": "download",
"selector": "text=/导出成交|下载成交|fills|trades/i",
"save_as": "{expected_joinquant_csvs.fills}",
"optional": True,
},
{
"type": "download",
"selector": "text=/导出持仓|下载持仓|positions/i",
"save_as": "{expected_joinquant_csvs.positions}",
"optional": True,
},
{
"type": "download",
"selector": "text=/导出收益|下载收益|pnl|收益/i",
"save_as": "{expected_joinquant_csvs.pnl}",
"optional": True,
},
{"type": "screenshot", "path": "{run_artifact_dir}/final.png"},
]
def _sim_trade_actions() -> list[dict[str, Any]]:
return [
{"type": "goto", "url": "{strategy_url}"},
{
"type": "paste_text_file",
"selector": "textarea, .ace_text-input, .cm-content, .CodeMirror textarea",
"path": "{wrapper_path}",
"description": "Paste generated wrapper strategy into the simulated-trading strategy editor.",
},
{
"type": "set_input_files",
"selector": "input[type=file]",
"paths": "{target_csvs}",
"description": "Upload frozen target CSV files for 模拟盘.",
"optional": True,
},
{
"type": "click",
"selector": "text=/保存|Save/i",
"description": "Save the strategy code and uploaded files.",
"optional": True,
},
{
"type": "wait_for_selector",
"selector": "text=/保存成功|已保存|Saved/i",
"timeout_ms": 120_000,
"optional": True,
},
{
"type": "click",
"selector": "text=/模拟盘|模拟交易|启动模拟|运行模拟|启动|重启|Run Sim|Start|Restart/i",
"description": "Start or restart the JoinQuant simulated-trading job.",
},
{
"type": "wait_for_selector",
"selector": "text=/运行中|已启动|模拟交易运行|Started|Running/i",
"timeout_ms": 180_000,
"optional": True,
},
{"type": "screenshot", "path": "{run_artifact_dir}/sim_trade_final.png"},
]
def default_browser_config(strategy_url: str = "", *, flow: str = "backtest") -> dict[str, Any]:
"""Return a selector/action template for JoinQuant browser automation."""
if flow not in {"backtest", "sim-trade", "sim_trade"}:
raise ValueError("flow must be 'backtest' or 'sim-trade'")
normalized_flow = "sim-trade" if flow == "sim_trade" else flow
actions = _sim_trade_actions() if normalized_flow == "sim-trade" else _backtest_actions()
return {
"flow": normalized_flow,
"strategy_url": strategy_url,
"login_url": DEFAULT_LOGIN_URL,
"headless": False,
@@ -47,73 +156,25 @@ def default_browser_config(strategy_url: str = "") -> dict[str, Any]:
"Use `joinquant browser-snapshot` to capture HTML/screenshots for selector tuning.",
"The action list is declarative and runs in order.",
],
"actions": [
{"type": "goto", "url": "{strategy_url}"},
{
"type": "paste_text_file",
"selector": "textarea, .ace_text-input, .cm-content, .CodeMirror textarea",
"path": "{wrapper_path}",
"description": "Paste generated wrapper strategy into the code editor.",
},
{
"type": "set_input_files",
"selector": "input[type=file]",
"paths": "{target_csvs}",
"description": "Upload all aligned daily target CSV files.",
"optional": True,
},
{
"type": "fill",
"selector": "input[name=start_date], input[placeholder*=开始]",
"text": "{backtest_start_date}",
"optional": True,
},
{
"type": "fill",
"selector": "input[name=end_date], input[placeholder*=结束]",
"text": "{backtest_end_date}",
"optional": True,
},
{
"type": "click",
"selector": "text=/运行回测|开始回测|回测|Run Backtest|Run/i",
"description": "Start the JoinQuant backtest.",
},
{
"type": "wait_for_selector",
"selector": "text=/回测完成|运行完成|Backtest complete|Finished/i",
"timeout_ms": 600_000,
"optional": True,
},
{
"type": "download",
"selector": "text=/导出成交|下载成交|fills|trades/i",
"save_as": "{expected_joinquant_csvs.fills}",
"optional": True,
},
{
"type": "download",
"selector": "text=/导出持仓|下载持仓|positions/i",
"save_as": "{expected_joinquant_csvs.positions}",
"optional": True,
},
{
"type": "download",
"selector": "text=/导出收益|下载收益|pnl|收益/i",
"save_as": "{expected_joinquant_csvs.pnl}",
"optional": True,
},
{"type": "screenshot", "path": "{run_artifact_dir}/final.png"},
],
"actions": actions,
}
def write_browser_config_template(path: str | Path, *, strategy_url: str = "") -> Path:
def write_browser_config_template(
path: str | Path,
*,
strategy_url: str = "",
flow: str = "backtest",
) -> Path:
"""Write a JSON config template for browser automation."""
out_path = Path(path)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(
json.dumps(default_browser_config(strategy_url), indent=2, ensure_ascii=False) + "\n",
json.dumps(
default_browser_config(strategy_url, flow=flow),
indent=2,
ensure_ascii=False,
) + "\n",
encoding="utf-8",
)
return out_path
@@ -302,7 +363,7 @@ def _run_action(page: Any, action: dict[str, Any], context: dict[str, Any], time
return record
def run_browser_backtest(
def run_browser_flow(
*,
manifest_path: str | Path,
config_path: str | Path,
@@ -310,12 +371,14 @@ def run_browser_backtest(
out_dir: str | Path | None = None,
headless: bool | None = None,
auto_reconcile: bool = True,
flow_name: str | None = None,
) -> dict[str, Any]:
"""Run configured browser automation for a JoinQuant backtest."""
"""Run configured browser automation for a JoinQuant web workflow."""
sync_playwright, _ = _require_playwright()
manifest = load_json(manifest_path)
config = load_json(config_path)
artifact_dir = Path(out_dir or Path(str(manifest["joinquant_export_dir"])).parent / "browser_run")
flow = flow_name or str(config.get("flow") or "browser")
artifact_dir = Path(out_dir or Path(str(manifest["joinquant_export_dir"])).parent / f"browser_{flow}")
artifact_dir.mkdir(parents=True, exist_ok=True)
context_vars = _manifest_context(manifest, artifact_dir, config)
@@ -371,6 +434,7 @@ def run_browser_backtest(
"created_at": datetime.now(timezone.utc).isoformat(),
"manifest_path": str(manifest_path),
"config_path": str(config_path),
"flow": flow,
"storage_state": str(storage_state),
"artifact_dir": str(artifact_dir),
"actions": records,
@@ -384,3 +448,45 @@ def run_browser_backtest(
encoding="utf-8",
)
return report
def run_browser_backtest(
*,
manifest_path: str | Path,
config_path: str | Path,
storage_state: str | Path,
out_dir: str | Path | None = None,
headless: bool | None = None,
auto_reconcile: bool = True,
) -> dict[str, Any]:
"""Run configured browser automation for a JoinQuant backtest."""
return run_browser_flow(
manifest_path=manifest_path,
config_path=config_path,
storage_state=storage_state,
out_dir=out_dir,
headless=headless,
auto_reconcile=auto_reconcile,
flow_name="backtest",
)
def run_browser_sim_trade(
*,
manifest_path: str | Path,
config_path: str | Path,
storage_state: str | Path,
out_dir: str | Path | None = None,
headless: bool | None = None,
auto_reconcile: bool = True,
) -> dict[str, Any]:
"""Run configured browser automation for JoinQuant 模拟盘."""
return run_browser_flow(
manifest_path=manifest_path,
config_path=config_path,
storage_state=storage_state,
out_dir=out_dir,
headless=headless,
auto_reconcile=auto_reconcile,
flow_name="sim-trade",
)