功能更新

This commit is contained in:
2026-08-15 08:57:15 +08:00
parent 50fd032b45
commit c1c43d2ff7
30 changed files with 1908 additions and 888 deletions

View File

@@ -13,7 +13,7 @@ import re
import httpx
from ..config import settings
from ..schemas import ScreenConditions
from ..schemas import EventBacktestSpec, ScreenConditions
SYSTEM_PROMPT = """你是 A 股选股条件解析器。把用户的自然语言解析成一个 JSON 对象,只输出 JSON不要任何解释、注释或代码块围栏。完全无法理解时输出 {"error": "原因"}。
@@ -149,3 +149,69 @@ async def parse_conditions(text: str) -> ScreenConditions:
except Exception as e: # noqa: BLE001 —— JSON/校验失败,带错误重试
retry_error = str(e)[:300]
raise ScreenerError(f"AI 解析结果两次未通过校验,最后错误:{retry_error}")
# ---------- 事件回测解析(自然语言 -> EventBacktestSpec ----------
EVENT_SYSTEM_PROMPT = """你是 A 股事件回测参数解析器。用户描述一个「入场信号 + 买卖时机 + 持有期」的事件回测需求,把它解析成 JSON只输出 JSON不要任何解释或代码块围栏。完全无法理解时输出 {"error": "原因"}。
输出结构:
{"entry": {"indicator": [...], "snapshot": [], "exclude_st": true, "exclude_delisted": true, "exclude_bj": true}, "entry_timing": "next_open", "holding_days": 3, "exit_timing": "close"}
【entry.indicator 数组】入场信号条件(必填,至少 1 条),元素字段与白名单:
- "indicator": kdj_k / kdj_d / kdj_jKDJ 的 K/D/J 值、rsi、macd_dif / macd_dea / macd_histMACD 的 DIF/DEA/柱、ma收盘价均线、boll_upper / boll_mid / boll_lower布林轨道、close收盘价、pct_chg日涨跌幅%
- "params": 指标参数可选默认KDJ {"n":9,"m1":3,"m2":3}RSI {"period":14}MACD {"fast":12,"slow":26,"signal":9}MA {"period":20}BOLL {"period":20,"std":2}
- "op": "gt" | "ge" | "lt" | "le" | "between""value"between 时为下界)、"value2"(上界)
- "value_indicator": 指标与指标比较时填另一指标名(同白名单),如 "DIF 大于 DEA" -> indicator=macd_dif, op=gt, value_indicator=macd_dea, value=0
- "value_params": 比较对象指标参数不同时指定,如 "MA5 上穿 MA20" -> indicator=ma, params={"period":5}, op=gt, value_indicator=ma, value_params={"period":20}, value=0
- "lookback": 信号需连续/曾经满足的交易日窗口(默认 1
- "match": "all"(窗口内每天满足,默认)或 "any"(窗口内任一天满足)
【时间语义】"连续三天 J 小于 10" -> lookback=3, match="all""近 5 天曾经金叉" -> lookback=5, match="any"
【entry_timing】买入时机"第二天开盘购买/次日开盘买入" -> "next_open"(默认);"第二天收盘买入" -> "next_close"
【holding_days】买入后持有 N 个交易日int默认 3"未来三天的涨幅" -> holding_days=3"持有 10 天" -> 10"持有一个月" -> 20。
【exit_timing】到期卖出价"close"(收盘卖,默认)或 "open"(开盘卖)。
【entry.snapshot】截面过滤条件一般不适用于历史回测除非用户明确说"只回测市值大于 X 亿的股票"才填,其余情况留空数组。
示例:
输入:在连续三天 J 小于 10 的时候第二天开盘购买,之后未来三天的涨幅有多少
输出:{"entry":{"indicator":[{"indicator":"kdj_j","params":{"n":9,"m1":3,"m2":3},"op":"lt","value":10,"lookback":3,"match":"all"}],"snapshot":[],"exclude_st":true,"exclude_delisted":true,"exclude_bj":true},"entry_timing":"next_open","holding_days":3,"exit_timing":"close"}
示例:
输入RSI 低于 30 的第二天开盘买入持有 5 天收盘卖出
输出:{"entry":{"indicator":[{"indicator":"rsi","params":{"period":14},"op":"lt","value":30,"lookback":1,"match":"all"}],"snapshot":[],"exclude_st":true,"exclude_delisted":true,"exclude_bj":true},"entry_timing":"next_open","holding_days":5,"exit_timing":"close"}"""
def _build_event_messages(text: str, retry_error: str | None = None) -> list[dict]:
user = f"解析以下事件回测需求:{text}"
if retry_error:
user += f"\n\n上一次输出无法通过校验,错误:{retry_error}。请修正后重新只输出 JSON。"
return [{"role": "system", "content": EVENT_SYSTEM_PROMPT}, {"role": "user", "content": user}]
async def parse_event_spec(text: str) -> EventBacktestSpec:
"""自然语言 -> EventBacktestSpec。复用 _chat/_extract_json失败带错误重试 1 次。"""
if not settings.llm_api_key:
raise ScreenerError(
"未配置 LLM_API_KEY请在 backend/.env 填入 DeepSeek API Keyplatform.deepseek.com 获取)后重启后端"
)
retry_error: str | None = None
for _ in range(2):
content = await _chat(_build_event_messages(text, retry_error))
try:
obj = _extract_json(content)
if "error" in obj and not obj.get("entry"):
raise ScreenerError(f"AI 无法理解该回测需求:{obj['error']}")
spec = EventBacktestSpec.model_validate(obj)
if not spec.entry.indicator:
raise ValueError("entry.indicator 不能为空")
return spec
except ScreenerError:
raise
except Exception as e: # noqa: BLE001
retry_error = str(e)[:300]
raise ScreenerError(f"AI 解析回测参数两次未通过校验,最后错误:{retry_error}")

View File

@@ -16,7 +16,7 @@ from sqlalchemy import delete, func, insert, select
from sqlalchemy.ext.asyncio import AsyncSession
from ..config import settings
from ..models import DailySnapshot, MarketDaily, StockBasic, TradeCalendar
from ..models import AdjFactor, DailySnapshot, MarketDaily, StockBasic, TradeCalendar
from .llm import ScreenerError
# 进程内单例任务状态uvicorn --reload 单进程场景够用)
@@ -160,6 +160,18 @@ def _fetch_basic(pro, d: str) -> list[dict]:
return rows
def _fetch_adj_factor(pro, d: str) -> list[dict]:
"""拉取某交易日全市场复权因子K线 bfq->qfq/hfq 本地换算的底座)。"""
time.sleep(settings.screener_sync_interval)
df = _call_retry(pro.adj_factor, trade_date=d)
if df is None or df.empty:
return []
return [
{"trade_date": _parse_d(d), "ts_code": r["ts_code"], "adj_factor": float(r["adj_factor"])}
for _, r in df.iterrows()
]
def _sync_stock_list_sync(pro) -> list[dict]:
"""拉取在市股票列表。"""
time.sleep(settings.screener_sync_interval)
@@ -243,6 +255,16 @@ async def _run_sync(days: int, force: bool) -> None:
await _replace_day(session, MarketDaily, daily_rows, d)
_sync_state["done_days"] += 1
# 2.5) 复权因子(与日线同窗口增量;历史全量由 scripts/backfill_adj_factor.py 回补)
async with async_session() as session:
have_adj = set() if force else await _existing_dates(session, AdjFactor)
for d in [d for d in dates if d not in have_adj]:
_sync_state["step"] = f"正在同步 {d} 复权因子"
adj_rows = await asyncio.to_thread(_fetch_adj_factor, pro, d)
if adj_rows:
async with async_session() as session:
await _replace_day(session, AdjFactor, adj_rows, d)
# 3) 最新「有数据」交易日的快照daily_basic仅 1 次调用)
# 用 market_daily 实际最大交易日(今天的数据收盘后才生成,日历最新日会拉到空)
async with async_session() as session: