功能更新
This commit is contained in:
@@ -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_j(KDJ 的 K/D/J 值)、rsi、macd_dif / macd_dea / macd_hist(MACD 的 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 Key(platform.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}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user