"""开发自检脚本:跑一遍 /health 与 /backtest,打印结果。 用 FastAPI TestClient(无需起服务器,同进程验证全链路)。 用法: uv run --with httpx --directory backend python smoke_test.py """ import json from fastapi.testclient import TestClient from app.main import app # 必须用 with:lifespan(建表)只在进入上下文时执行 with TestClient(app) as c: r = c.get("/api/health") print("== /api/health ==", r.status_code, r.json()) r = c.post( "/api/backtest", json={ "symbol": "DEMO", "strategy": "macd_cross", "params": {"fast": 12, "slow": 26, "signal": 9}, "initial_cash": 100000.0, "fast_mode": False, }, ) print("== /api/backtest ==", r.status_code) if r.status_code != 200: print("ERROR:", r.text) raise SystemExit(1) d = r.json() print("candles :", len(d["candles"])) print("signals :", len(d["signals"]), "(买卖点)") print("equity pts :", len(d["equity"])) print("final_cash :", round(d["final_cash"], 2)) print("final_pos :", d["final_position"]) print("metrics :", json.dumps(d["metrics"], ensure_ascii=False, indent=2)) print("first signal :", d["signals"][0] if d["signals"] else None) assert len(d["candles"]) > 100 # 周期聚合:周线 K 线数应明显少于日线 rw = c.post( "/api/backtest", json={"symbol": "DEMO", "timeframe": "1w", "strategy": "macd_cross", "params": {"fast": 12, "slow": 26, "signal": 9}, "initial_cash": 100000.0}, ) wd = rw.json() print("== weekly ==", rw.status_code, "candles:", len(wd["candles"]), "vs daily", len(d["candles"])) assert rw.status_code == 200 assert len(wd["candles"]) < len(d["candles"]) print("\n✅ 后端全链路自检通过(含周期聚合)") # ---------- 智能选股 ---------- print("\n== 智能选股 ==") # 1) JSON 提取容错(不联网):代码围栏 / 多余文本 from app.screener.llm import _extract_json assert _extract_json('```json\n{"a": 1}\n```') == {"a": 1} assert _extract_json('好的,结果如下:{"indicator": [], "snapshot": []} 谢谢')["indicator"] == [] print("_extract_json 围栏/噪音容错 ✅") # 2) 未配置 LLM_API_KEY 时 /run 返回 503(确定性,不联网) from app.config import settings as _s r = c.post("/api/screener/run", json={"text": "这两天 KDJ 的 J 小于 10"}) if not _s.llm_api_key: assert r.status_code == 503, f"无 key 应 503,实际 {r.status_code}" print("无 LLM_API_KEY -> 503 ✅") else: print("已配置 LLM_API_KEY,跳过 503 用例") # 3) 直传条件选股(不依赖 LLM;依赖已同步的全市场数据) from app.models import MarketDaily # noqa: F401 from sqlalchemy import select, func from app.db import async_session import asyncio async def _has_data() -> bool: async with async_session() as session: return (await session.scalar(select(func.count()).select_from(MarketDaily))) or 0 > 0 if asyncio.run(_has_data()): r = c.post("/api/screener/run", json={ "text": "测试直传", "conditions": { "indicator": [{"indicator": "kdj_j", "params": {"n": 9, "m1": 3, "m2": 3}, "op": "lt", "value": 0, "lookback": 1, "match": "all"}], "snapshot": [], }, }) assert r.status_code == 200, f"直传选股失败 {r.status_code}: {r.text}" d = r.json() assert d["total"] >= 0 print(f"KDJ J<0 选股 ✅ 命中 {d['total']} 只,基准日 {(d['trade_date'] or '')[:10]}") else: print("(未同步全市场数据,跳过直传选股用例;运行 POST /api/screener/sync 后再试)") print("\n✅ 智能选股自检通过")