481 lines
22 KiB
Python
481 lines
22 KiB
Python
"""Opt-in WorldQuant integration: at most six simulations, never official submission.
|
|
|
|
Run from backend with --execute and an explicitly authorized credentials file.
|
|
Uses an isolated SQLite database and a local deterministic model for orchestration;
|
|
only catalog/authentication/simulation/PnL requests reach the official platform.
|
|
"""
|
|
|
|
import argparse
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import httpx
|
|
from cryptography.fernet import Fernet
|
|
from sqlalchemy import select
|
|
|
|
from app.config import Settings
|
|
from app.main import create_app
|
|
from app.models import Base, SimulationAttempt
|
|
from app.research.workspace_contracts import TemplateSpec
|
|
from tests.test_ai import configure
|
|
|
|
SCOPE = {"instrument_type": "EQUITY", "region": "USA", "universe": "TOP3000", "delay": 1}
|
|
|
|
|
|
def spec(window):
|
|
return {
|
|
"name": f"真实联调反转 {window}",
|
|
"description": "接口验收,不代表投资结论",
|
|
"expression": f"-rank(ts_delta({{field}}, {window}))",
|
|
"variables": {"field": {"kind": "field", "field_type": "MATRIX", "values": ["close"]}},
|
|
}
|
|
|
|
|
|
async def main(args):
|
|
out = Path(args.output).resolve()
|
|
out.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
database = out / "research.sqlite"
|
|
if database.exists() and not args.resume:
|
|
raise RuntimeError("验收数据库已存在:保留原始运行,不重新发送;请选择新目录或只读核验")
|
|
secret_file = out / "encryption.key"
|
|
key = secret_file.read_text() if args.resume else Fernet.generate_key().decode()
|
|
if not args.resume:
|
|
secret_file.write_text(key)
|
|
secret_file.chmod(0o600)
|
|
settings = Settings(
|
|
_env_file=None,
|
|
database_url=f"sqlite+aiosqlite:///{database}",
|
|
admin_password="isolated-live-research-only",
|
|
encryption_key=key,
|
|
enable_runner=False,
|
|
public_origin="http://testserver",
|
|
retry_attempts=1,
|
|
)
|
|
app = create_app(settings)
|
|
async with app.state.engine.begin() as db:
|
|
await db.run_sync(Base.metadata.create_all)
|
|
database.chmod(0o600)
|
|
evidence = {
|
|
"platform": "https://api.worldquantbrain.com",
|
|
"model": "local deterministic fixture, no external model",
|
|
"simulation_cap": 6,
|
|
"simulations_sent": 0,
|
|
"stages": [],
|
|
}
|
|
|
|
if args.resume:
|
|
evidence = json.loads((out / "evidence.json").read_text())
|
|
stages = [
|
|
row
|
|
for row in evidence["stages"]
|
|
if row["stage"]
|
|
in (
|
|
"stage1_template",
|
|
"stage1_structure",
|
|
"stage1_settings",
|
|
"stage2_feature",
|
|
"stage3_pipeline",
|
|
"stage4_quantflow",
|
|
)
|
|
]
|
|
async with app.state.sessions() as db:
|
|
attempts = list(await db.scalars(select(SimulationAttempt)))
|
|
if any(a.state != "completed" for a in attempts) or len(stages) != len(attempts):
|
|
raise RuntimeError("存在未核实或未完整记录的模拟,请恢复原运行,不重新发送")
|
|
|
|
def record(stage, **data):
|
|
row = {"stage": stage, **data}
|
|
evidence["stages"].append(row)
|
|
(out / "evidence.json").write_text(json.dumps(evidence, ensure_ascii=False, indent=2))
|
|
print(json.dumps(row, ensure_ascii=False), flush=True)
|
|
|
|
remote = app.state.runner.client
|
|
|
|
async def guard_request(request):
|
|
if request.method in ("PATCH", "PUT", "DELETE"):
|
|
raise RuntimeError("验收禁止平台属性写入")
|
|
if request.method == "POST" and request.url.path != "/authentication":
|
|
if request.url.path != "/simulations":
|
|
raise RuntimeError("验收禁止其他平台写入")
|
|
payload = json.loads(request.content)
|
|
count = len(payload) if isinstance(payload, list) else 1
|
|
if evidence["simulations_sent"] + count > 6:
|
|
raise RuntimeError("真实模拟硬上限已达到")
|
|
evidence["simulations_sent"] += count
|
|
record("simulation_intent", count=count, total=evidence["simulations_sent"])
|
|
|
|
remote.client.event_hooks["request"].append(guard_request)
|
|
async with app.router.lifespan_context(app):
|
|
async with httpx.AsyncClient(
|
|
transport=httpx.ASGITransport(app), base_url="http://testserver", headers={"X-WQ-Request": "1"}
|
|
) as client:
|
|
|
|
async def api(method, path, body=None):
|
|
response = (
|
|
await client.request(method, "/api/v1" + path, json=body)
|
|
if body is not None
|
|
else await client.request(method, "/api/v1" + path)
|
|
)
|
|
if not response.is_success:
|
|
raise RuntimeError(
|
|
f"{method} {path}: HTTP {response.status_code}; {response.json().get('detail', '')}"
|
|
)
|
|
return response.json()
|
|
|
|
await api("POST", "/auth/login", {"username": "admin", "password": "isolated-live-research-only"})
|
|
credentials = json.loads(Path(args.credentials).read_text())
|
|
await api(
|
|
"PUT",
|
|
"/account/credentials",
|
|
{"email": credentials["account"], "password": credentials["password"]},
|
|
)
|
|
del credentials
|
|
job = await api("POST", "/account/connect")
|
|
await app.state.runner.execute(job["id"])
|
|
status = await api("GET", f"/sync-jobs/{job['id']}")
|
|
assert status["status"] == "completed", "账户连接未完成"
|
|
record("authentication", status="connected")
|
|
operators = await api("POST", "/catalog/operators/refresh")
|
|
options = await api("POST", "/catalog/setting-options/refresh")
|
|
record(
|
|
"metadata",
|
|
operators=len(operators["content"]["items"]),
|
|
setting_rows=len(options["content"]["items"]),
|
|
)
|
|
|
|
async def fixed(scope):
|
|
previous = next(
|
|
(
|
|
row
|
|
for row in evidence["stages"]
|
|
if row["stage"] == "fixed_input" and row["scope"] == scope
|
|
),
|
|
None,
|
|
)
|
|
if previous:
|
|
return previous["id"]
|
|
for dataset in (None, "pv1"):
|
|
job = await api("POST", "/catalog/sync-jobs", {"scope": scope, "dataset_id": dataset})
|
|
await app.state.runner.execute(job["id"])
|
|
result = await api("GET", f"/sync-jobs/{job['id']}")
|
|
if result["status"] != "completed":
|
|
record("catalog_error", status=result["status"], error=result.get("error"))
|
|
raise RuntimeError("真实目录同步失败")
|
|
params = str(httpx.QueryParams(scope))
|
|
fields = await api("GET", f"/catalog/datasets/pv1/fields?{params}&limit=100")
|
|
fixed_input = await api(
|
|
"POST",
|
|
"/data-preparations/from-dataset",
|
|
{
|
|
"scope": scope,
|
|
"dataset_id": "pv1",
|
|
"collection_version": fields["collection_version"],
|
|
},
|
|
)
|
|
fixed_input = (await api("POST", "/data-preparations/freeze", {
|
|
"items": [{"id": fixed_input["id"], "version": fixed_input["version"]}]}))["items"][0]
|
|
availability = await api(
|
|
"POST", "/catalog/field-availability/refresh", {"field_id": "close", "scope": scope}
|
|
)
|
|
assert availability["content"]["status"] == "available", availability["content"]
|
|
record(
|
|
"fixed_input",
|
|
id=fixed_input["id"],
|
|
scope=scope,
|
|
fields=len(fixed_input["field_ids"]),
|
|
availability_rows=len(availability["content"]["items"]),
|
|
)
|
|
return fixed_input["id"]
|
|
|
|
input_id = await fixed(SCOPE)
|
|
|
|
async def save(kind, content):
|
|
return await api("POST", "/research/assets", {"kind": kind, "content": content})
|
|
|
|
async def expand(asset, parents=None):
|
|
return await api(
|
|
"POST",
|
|
"/research/experiments",
|
|
{
|
|
"asset_id": asset["id"],
|
|
"version": asset["version"],
|
|
"input_ids": [input_id],
|
|
"settings": {k: SCOPE[k] for k in ("region", "universe", "delay")},
|
|
"hypothesis": "真实接口验收",
|
|
"limit": 1,
|
|
"parent_alpha_ids": parents or [],
|
|
},
|
|
)
|
|
|
|
async def collect(run_id):
|
|
deadline = time.monotonic() + 900
|
|
while time.monotonic() < deadline:
|
|
async with app.state.sessions() as db:
|
|
attempts = list(
|
|
await db.scalars(
|
|
select(SimulationAttempt).where(SimulationAttempt.run_id == run_id)
|
|
)
|
|
)
|
|
ids = [
|
|
a.id
|
|
for a in attempts
|
|
if a.state in ("queued", "submitting", "submitted", "collecting")
|
|
and (
|
|
a.next_poll_at is None
|
|
or a.next_poll_at.replace(tzinfo=timezone.utc) <= datetime.now(timezone.utc)
|
|
)
|
|
]
|
|
for aid in ids:
|
|
await app.state.runner.backtests.step(aid)
|
|
run = await api("GET", f"/backtests/runs/{run_id}")
|
|
if run["status"] in ("completed", "completed_with_errors", "needs_review", "stopped"):
|
|
results = await api("GET", f"/backtests/runs/{run_id}/results")
|
|
rows = [
|
|
{
|
|
"alpha_id": i.get("alpha_id"),
|
|
"status": i.get("platform_status"),
|
|
"error": i.get("error"),
|
|
"complete": (i.get("result") or {}).get("complete"),
|
|
}
|
|
for i in results["items"]
|
|
]
|
|
record("backtest", id=run_id, status=run["status"], results=rows)
|
|
if run["status"] != "completed":
|
|
raise RuntimeError("模拟未正常完成,保留原运行,不自动重提")
|
|
return results
|
|
await asyncio.sleep(5)
|
|
raise RuntimeError("回测等待超时;数据库保留 progress URL,不自动重发")
|
|
|
|
async def simulate(experiment, label):
|
|
previous = next((row for row in evidence["stages"] if row["stage"] == label), None)
|
|
if previous:
|
|
result = await api("GET", f"/backtests/runs/{previous['run_id']}/results")
|
|
return result["items"][0]["alpha_id"]
|
|
preview = await api("POST", f"/research/experiments/{experiment['id']}/preview", {})
|
|
assert preview["total"] == 1
|
|
run = await api(
|
|
"POST",
|
|
"/backtests/runs",
|
|
{
|
|
"preview_id": preview["preview_id"],
|
|
"version": preview["version"],
|
|
"idempotency_key": label,
|
|
},
|
|
)
|
|
result = await collect(run["backtest_run_id"])
|
|
record(
|
|
label,
|
|
experiment_id=experiment["id"],
|
|
preview_id=preview["preview_id"],
|
|
run_id=run["backtest_run_id"],
|
|
)
|
|
return result["items"][0]["alpha_id"]
|
|
|
|
base = await save("template", spec(5))
|
|
seed = await simulate(await expand(base), "stage1_template")
|
|
variant = await save("template", spec(10))
|
|
variant_id = await simulate(await expand(variant, [seed]), "stage1_structure")
|
|
target_id = await fixed({**SCOPE, "universe": "TOP1000"})
|
|
settings_variant = await api(
|
|
"POST",
|
|
"/research/variants/settings",
|
|
{"alpha_id": seed, "input_ids": [input_id, target_id], "hypothesis": "同表达式不同股票池"},
|
|
)
|
|
await simulate(settings_variant, "stage1_settings")
|
|
feature = await save(
|
|
"feature",
|
|
{
|
|
"name": "真实特征方案",
|
|
"hypothesis": "价格短期反转",
|
|
"input_ids": [input_id],
|
|
"steps": [
|
|
{
|
|
"name": "变化及排序",
|
|
"rationale": "比较截面价格变化",
|
|
"expression": "-rank(ts_delta(close, 20))",
|
|
}
|
|
],
|
|
"template": spec(20),
|
|
},
|
|
)
|
|
feature_template = await api(
|
|
"POST", f"/research/features/{feature['id']}/template", {"version": 1}
|
|
)
|
|
feature_alpha = await simulate(await expand(feature_template), "stage2_feature")
|
|
report = await api("POST", "/research/evaluations", {"alpha_id": feature_alpha})
|
|
lineage = await api("GET", f"/research/lineage?alpha_id={variant_id}")
|
|
record(
|
|
"stage2_evaluation",
|
|
alpha_id=feature_alpha,
|
|
evaluation_id=report["id"],
|
|
verdict=report["report"]["verdict"],
|
|
lineage_nodes=len(lineage.get("items", [])),
|
|
)
|
|
await configure(app, client)
|
|
|
|
async def local_model(ai, context, output_type, revision):
|
|
return TemplateSpec.model_validate(spec(8)), {
|
|
"model": "local deterministic acceptance fixture",
|
|
"revision": revision,
|
|
}
|
|
|
|
flow_body = {
|
|
"request_id": "live-pipeline",
|
|
"name": "真实平台固定流水线",
|
|
"input_ids": [input_id],
|
|
"hypothesis": "真实平台运行链验收",
|
|
"settings": {k: SCOPE[k] for k in ("region", "universe", "delay")},
|
|
"budget": {"max_rounds": 1, "max_simulations": 1, "max_model_calls": 1},
|
|
"batch_candidates": 1,
|
|
"template_id": base["id"],
|
|
"template_version": 1,
|
|
}
|
|
|
|
async def drive(body, label):
|
|
if any(row["stage"] == label for row in evidence["stages"]):
|
|
return
|
|
run = await api("POST", "/research/flows/runs", body)
|
|
for _ in range(35):
|
|
await app.state.research.advance(run["id"])
|
|
current = await api("GET", f"/research/flows/runs/{run['id']}")
|
|
for step in current["steps"]:
|
|
if step["status"] == "waiting":
|
|
await collect(step["backtest_run_id"])
|
|
if current["status"] not in ("queued", "running"):
|
|
record(
|
|
label,
|
|
run_id=run["id"],
|
|
status=current["status"],
|
|
simulations=current["simulations_used"],
|
|
model_fixture_calls=current["model_calls_used"],
|
|
steps=len(current["steps"]),
|
|
error=current["error"],
|
|
)
|
|
assert current["status"] == "completed"
|
|
return
|
|
raise RuntimeError("研究运行未结束")
|
|
|
|
with patch("app.research.runtime.request_model", local_model):
|
|
await drive(flow_body, "stage3_pipeline")
|
|
graph = {
|
|
"name": "真实平台原生画布",
|
|
"nodes": [
|
|
{"id": kind, "type": kind, "label": kind}
|
|
for kind in ("input", "expand", "backtest", "evaluate", "condition", "summarize")
|
|
],
|
|
"edges": [
|
|
{"source": a, "target": b}
|
|
for a, b in zip(
|
|
("input", "expand", "backtest", "evaluate", "condition"),
|
|
("expand", "backtest", "evaluate", "condition", "summarize"),
|
|
)
|
|
],
|
|
}
|
|
workflow = await save("workflow", graph)
|
|
flow_template = await save("template", spec(15))
|
|
await drive(
|
|
{
|
|
**flow_body,
|
|
"request_id": "live-quantflow",
|
|
"name": "真实平台 QuantFlow",
|
|
"template_id": flow_template["id"],
|
|
"workflow_id": workflow["id"],
|
|
"workflow_version": 1,
|
|
},
|
|
"stage4_quantflow",
|
|
)
|
|
record("completed", simulations_sent=evidence["simulations_sent"], official_submissions=0)
|
|
secret_file.chmod(0o600)
|
|
|
|
|
|
async def inspect_existing(args):
|
|
"""Read platform PnL and refresh snapshots without issuing any simulation."""
|
|
out = Path(args.output).resolve()
|
|
evidence = json.loads((out / "evidence.json").read_text())
|
|
settings = Settings(
|
|
_env_file=None,
|
|
database_url=f"sqlite+aiosqlite:///{out / 'research.sqlite'}",
|
|
admin_password="isolated-live-research-only",
|
|
encryption_key=(out / "encryption.key").read_text(),
|
|
enable_runner=False,
|
|
public_origin="http://testserver",
|
|
retry_attempts=4,
|
|
)
|
|
app = create_app(settings)
|
|
|
|
async def read_only(request):
|
|
if request.method not in ("GET", "OPTIONS") and not (
|
|
request.method == "POST" and request.url.path == "/authentication"
|
|
):
|
|
raise RuntimeError("后续核验禁止平台写入,包括模拟")
|
|
|
|
app.state.runner.client.client.event_hooks["request"].append(read_only)
|
|
async with app.router.lifespan_context(app):
|
|
await app.state.runner.ensure_connected()
|
|
async with httpx.AsyncClient(
|
|
transport=httpx.ASGITransport(app), base_url="http://testserver", headers={"X-WQ-Request": "1"}
|
|
) as client:
|
|
await client.post(
|
|
"/api/v1/auth/login", json={"username": "admin", "password": "isolated-live-research-only"}
|
|
)
|
|
ids = [row["results"][0]["alpha_id"] for row in evidence["stages"] if row["stage"] == "backtest"][
|
|
:2
|
|
]
|
|
job = (
|
|
await client.post("/api/v1/sync-jobs", json={"kind": "pnl_refresh", "alpha_ids": ids})
|
|
).json()
|
|
await app.state.runner.execute(job["id"])
|
|
current = (await client.get(f"/api/v1/sync-jobs/{job['id']}")).json()
|
|
assert current["status"] == "completed", current["status"]
|
|
comparison = (await client.post("/api/v1/research/compare", json={"alpha_ids": ids})).json()
|
|
assert comparison["common_dates"], "真实 PnL 没有共同日期窗口"
|
|
evaluation = next(row for row in evidence["stages"] if row["stage"] == "stage2_evaluation")
|
|
lineage = (await client.get(f"/api/v1/research/lineage?alpha_id={ids[1]}")).json()
|
|
assert lineage["items"] and lineage["edges"] and lineage["sources"], "真实来源链不完整"
|
|
evaluation["lineage_nodes"] = len(lineage["items"])
|
|
baseline = (await client.get(f"/api/v1/research/lineage?alpha_id={ids[0]}")).json()
|
|
kinds = {item["source"]["kind"] for item in baseline["sources"]["items"]}
|
|
assert baseline["sources"]["total"] == 2 and {"template", "pipeline"}.issubset(kinds)
|
|
before = (await client.get(f"/api/v1/research/evaluations/{evaluation['evaluation_id']}")).json()
|
|
job = (
|
|
await client.post(
|
|
"/api/v1/sync-jobs", json={"kind": "alpha_refresh", "alpha_ids": [evaluation["alpha_id"]]}
|
|
)
|
|
).json()
|
|
await app.state.runner.execute(job["id"])
|
|
after = (await client.get(f"/api/v1/research/evaluations/{evaluation['evaluation_id']}")).json()
|
|
assert before == after, "同步覆盖了历史评估"
|
|
record = {
|
|
"stage": "comparison_and_history",
|
|
"alpha_ids": ids,
|
|
"common_dates": len(comparison["common_dates"]),
|
|
"window": comparison["window"],
|
|
"different_settings": comparison["different_settings"],
|
|
"evaluation_unchanged": True,
|
|
"lineage_experiments": len(lineage["items"]),
|
|
"lineage_edges": len(lineage["edges"]),
|
|
"baseline_sources": baseline["sources"]["total"],
|
|
}
|
|
evidence["stages"].append(record)
|
|
(out / "evidence.json").write_text(json.dumps(evidence, ensure_ascii=False, indent=2))
|
|
print(json.dumps(record, ensure_ascii=False), flush=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--execute", action="store_true")
|
|
parser.add_argument("--inspect-existing", action="store_true")
|
|
parser.add_argument("--resume", action="store_true")
|
|
parser.add_argument("--credentials", required=True)
|
|
parser.add_argument("--output", required=True)
|
|
args = parser.parse_args()
|
|
os.umask(0o077)
|
|
if args.execute == args.inspect_existing:
|
|
parser.error("选择 --execute 或 --inspect-existing 之一")
|
|
asyncio.run(inspect_existing(args) if args.inspect_existing else main(args))
|