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>
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
"""HTTP 路由(OpenAPI 契约的载体)。
|
||||
|
||||
GET /api/health 健康检查
|
||||
GET /api/candles/{sym} 取 K 线(支持 1d/1w/1M/1y 周期,日线为基底聚合)
|
||||
POST /api/backtest 跑回测,返回 K线+指标+买卖点+净值+绩效
|
||||
GET /api/health 健康检查
|
||||
GET /api/candles/{sym} 取 K 线(支持 1d/1w/1M/1y 周期,日线为基底聚合)
|
||||
POST /api/backtest 跑回测,返回 K线+指标+买卖点+净值+绩效
|
||||
POST /api/screener/run 智能选股:自然语言 -> 条件 -> 全市场筛选
|
||||
POST /api/screener/sync 启动全市场数据同步(后台任务)
|
||||
GET /api/screener/sync/status 同步任务状态与数据实况
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -14,6 +17,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from .backtest.engine import BacktestConfig, run_backtest
|
||||
from .backtest.strategies import build_strategy
|
||||
from .config import settings
|
||||
from .data import fetcher, repository
|
||||
from .data.aggregation import bars_per_year, resample_bars
|
||||
from .data.synthetic import seed_if_empty
|
||||
@@ -27,10 +31,17 @@ from .schemas import (
|
||||
EquityPoint,
|
||||
IndicatorOut,
|
||||
MetricsOut,
|
||||
ScreenerRunRequest,
|
||||
ScreenerRunResponse,
|
||||
ScreenerSyncRequest,
|
||||
ScreenerSyncStatus,
|
||||
SignalOut,
|
||||
SyncRequest,
|
||||
SyncResponse,
|
||||
)
|
||||
from .screener import engine, market_sync
|
||||
from .screener.engine import DataNotReadyError
|
||||
from .screener.llm import ScreenerError, parse_conditions
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
@@ -165,3 +176,46 @@ async def backtest(
|
||||
final_position=result["final_position"],
|
||||
initial_cash=req.initial_cash,
|
||||
)
|
||||
|
||||
|
||||
# ---------- 智能选股 ----------
|
||||
@router.post("/screener/run", response_model=ScreenerRunResponse)
|
||||
async def screener_run(
|
||||
req: ScreenerRunRequest, session: AsyncSession = Depends(get_session)
|
||||
) -> ScreenerRunResponse:
|
||||
"""自然语言 -> LLM 解析条件 -> 全市场筛选。也可直传 conditions 跳过 LLM(微调再跑)。"""
|
||||
try:
|
||||
conds = req.conditions or await parse_conditions(req.text)
|
||||
if not conds.indicator and not conds.snapshot:
|
||||
raise HTTPException(status_code=400, detail="AI 未从描述中解析出任何筛选条件,请换种说法")
|
||||
result = await engine.run_screen(session, conds, settings.screener_default_limit)
|
||||
return ScreenerRunResponse(**result)
|
||||
except HTTPException:
|
||||
raise
|
||||
except DataNotReadyError as e:
|
||||
raise HTTPException(status_code=409, detail=str(e))
|
||||
except ValueError as e: # 未知指标/字段、条件为空
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except ScreenerError as e:
|
||||
code = 503 if "未配置 LLM_API_KEY" in str(e) else 502
|
||||
raise HTTPException(status_code=code, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/screener/sync", response_model=ScreenerSyncStatus)
|
||||
async def screener_sync_start(
|
||||
req: ScreenerSyncRequest, session: AsyncSession = Depends(get_session)
|
||||
) -> ScreenerSyncStatus:
|
||||
"""启动全市场数据同步(后台任务,立即返回状态)。"""
|
||||
try:
|
||||
await market_sync.start_sync(session, req.days, req.force)
|
||||
except ScreenerError as e:
|
||||
raise HTTPException(status_code=503, detail=str(e))
|
||||
status = await market_sync.get_sync_status(session)
|
||||
return ScreenerSyncStatus(**{k: status.get(k) for k in ScreenerSyncStatus.model_fields})
|
||||
|
||||
|
||||
@router.get("/screener/sync/status", response_model=ScreenerSyncStatus)
|
||||
async def screener_sync_status(session: AsyncSession = Depends(get_session)) -> ScreenerSyncStatus:
|
||||
"""同步任务状态 + 数据实况(最新交易日/行数/ready)。"""
|
||||
status = await market_sync.get_sync_status(session)
|
||||
return ScreenerSyncStatus(**{k: status.get(k) for k in ScreenerSyncStatus.model_fields})
|
||||
|
||||
Reference in New Issue
Block a user