Files
stock/backend/smoke_test.py
cirry 528357c3f5 feat: AI 自然语言选股(GLM)+ 全市场数据管道 + 远程 PostgreSQL
- 首页双入口(智能选股/策略回测):引入 vue-router,顶部导航
- 智能选股:自然语言 -> LLM 解析结构化条件(智谱 GLM,OpenAI 兼容,/v4 兼容)-> SQL 快照预筛 + pandas 指标过滤(复用 indicators 单一事实源)
- 条件模型:指标 vs 常数/指标(value_indicator,如 DIF>DEA、close<布林下轨)、lookback+match 表达连续N天/近N天任一天、市值/PE/PB/换手率快照条件、默认排除 ST/退市/北交所
- 全市场数据同步:按 trade_date 批量拉取未复权日线(与回测 candles qfq 隔离),交易日历/股票列表本地缓存,daily_basic 仅最新截面,Tushare 限频兜底(分钟级重试/小时级降级)
- 存储:DATABASE_URL 切远程 PostgreSQL(cirry.cn/stock),本地 SQLite 已移除
- .env 入库(私有仓库);smoke_test 扩展选股链路

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 14:49:53 +08:00

101 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""开发自检脚本:跑一遍 /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
# 必须用 withlifespan建表只在进入上下文时执行
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✅ 智能选股自检通过")