Add JoinQuant simulated trading automation
This commit is contained in:
+172
-66
@@ -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",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user