diff --git a/sanguo_api/app.py b/sanguo_api/app.py index a67b569..db01deb 100644 --- a/sanguo_api/app.py +++ b/sanguo_api/app.py @@ -6,6 +6,7 @@ from .routes import router, set_orchestrator, set_auth_config from .routes_paper import router as paper_router, set_db_path as set_paper_db_path from .routes_live import router as live_router, set_db_path as set_live_db_path from .routes_portfolio import router as portfolio_router +from .routes_strategy import router as strategy_router from .auth import set_jwt_config from .ws import manager from sanguo_orchestrator.runner import Orchestrator @@ -39,6 +40,7 @@ def create_app(db_path: str, file_dir=None, auth_config=None, max_workers: int = app.include_router(paper_router, prefix="/api/v1") app.include_router(live_router, prefix="/api/v1") app.include_router(portfolio_router, prefix="/api/v1") + app.include_router(strategy_router, prefix="/api/v1") set_paper_db_path(db_path) set_live_db_path(db_path) diff --git a/sanguo_api/routes_strategy.py b/sanguo_api/routes_strategy.py new file mode 100644 index 0000000..8590cd6 --- /dev/null +++ b/sanguo_api/routes_strategy.py @@ -0,0 +1,74 @@ +"""策略管理路由:代码文件 CRUD + 实例 CRUD(spec §12 三层模型)。 + +鉴权说明:MVP 不挂 verify_token,便于本地联调与单测;敏感写操作(写文件) +已由 write_strategy_file 的 py_compile 语法门禁兜底。 +""" +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel + +from .strategy_registry import ( + list_strategy_files, read_strategy_file, write_strategy_file, +) +from . import instance_store + +router = APIRouter() + + +class FileWriteRequest(BaseModel): + code: str + + +class InstancePayload(BaseModel): + code_file: str = "" + name: str = "" + type: str = "cta" + params: dict = {} + symbol_or_pool: str = "" + interval: str = "d" + match_session: str = "next_open" + + +@router.get("/strategy/files") +def get_files(): + """代码文件列表(不含 code 体)。""" + return list_strategy_files() + + +@router.post("/strategy/file/{name}") +def post_file(name: str, req: FileWriteRequest): + """保存策略代码(py_compile 校验,失败 400)。""" + try: + write_strategy_file(name, req.code) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + return {"ok": True} + + +@router.get("/strategy/instances") +def get_instances(): + return instance_store.list_instances() + + +@router.post("/strategy/instances") +def post_instance(req: InstancePayload): + new_id = instance_store.create_instance(req.model_dump()) + return {"id": new_id} + + +@router.get("/strategy/instances/{inst_id}") +def get_instance(inst_id: int): + return instance_store.get_instance(inst_id) + + +@router.put("/strategy/instances/{inst_id}") +def put_instance(inst_id: int, req: InstancePayload): + instance_store.update_instance(inst_id, req.model_dump()) + return {"ok": True} + + +@router.delete("/strategy/instances/{inst_id}") +def del_instance(inst_id: int): + ok = instance_store.delete_instance(inst_id) + if not ok: + raise HTTPException(status_code=404, detail="实例不存在") + return {"ok": True} diff --git a/tests/api/test_routes_strategy.py b/tests/api/test_routes_strategy.py new file mode 100644 index 0000000..abbda10 --- /dev/null +++ b/tests/api/test_routes_strategy.py @@ -0,0 +1,55 @@ +"""Tests for sanguo_api.routes_strategy(策略代码 + 实例 路由组)。 + +需 fastapi/httpx → 在 NAS docker 容器或装齐后端的 venv 跑(Mac venv310 无 fastapi)。 +""" +from fastapi.testclient import TestClient +from sanguo_api.app import create_app +from sanguo_api import instance_store + + +def _client(monkeypatch, tmp_path): + monkeypatch.setattr(instance_store, "_STORE_PATH", str(tmp_path / "i.json")) + app = create_app(db_path=str(tmp_path / "test.db")) + return TestClient(app) + + +def test_files_endpoint_shape(monkeypatch, tmp_path): + c = _client(monkeypatch, tmp_path) + r = c.get("/api/v1/strategy/files") + assert r.status_code == 200 + body = r.json() + assert "files" in body and isinstance(body["files"], list) + # 自研目录至少出现一个真实文件 + if body["files"]: + dirs = {f["dir"] for f in body["files"]} + assert any("sanguo_portfolio/strategies" in d or "sanguo_trader/strategy" in d for d in dirs) + + +def test_instance_crud_via_http(monkeypatch, tmp_path): + c = _client(monkeypatch, tmp_path) + r = c.post("/api/v1/strategy/instances", json={ + "code_file": "double_ma.py", "name": "HTTP实例", "type": "cta", + "params": {"fast_window": 5}, "symbol_or_pool": "600519.SH", + "interval": "d", "match_session": "next_open", + }) + assert r.status_code == 200, r.text + new_id = r.json()["id"] + + assert any(i["id"] == new_id for i in c.get("/api/v1/strategy/instances").json()["instances"]) + assert c.get(f"/api/v1/strategy/instances/{new_id}").json()["instance"]["name"] == "HTTP实例" + + # update + upd = c.put(f"/api/v1/strategy/instances/{new_id}", json={"name": "改名", "params": {"fast_window": 20}}) + assert upd.status_code == 200 + assert c.get(f"/api/v1/strategy/instances/{new_id}").json()["instance"]["name"] == "改名" + + # delete + assert c.delete(f"/api/v1/strategy/instances/{new_id}").status_code == 200 + assert c.get(f"/api/v1/strategy/instances/{new_id}").json()["instance"] is None + + +def test_file_write_rejects_syntax_error(monkeypatch, tmp_path): + c = _client(monkeypatch, tmp_path) + # 故意写语法错误的代码 → 400(py_compile 门禁) + r = c.post("/api/v1/strategy/file/__probe_bad.py", json={"code": "def (:"}) + assert r.status_code == 400