From 56276384e5a05758d7c72d7b347b33856e983917 Mon Sep 17 00:00:00 2001 From: shy3130 <415333856@qq.com> Date: Fri, 3 Jul 2026 09:51:30 +0800 Subject: [PATCH] =?UTF-8?q?fix(strategy):=20=E4=BF=AE=E5=A4=8D=20Docker=20?= =?UTF-8?q?=E9=95=9C=E5=83=8F=E4=B8=AD=20AI=20=E7=AD=96=E7=95=A5=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=AF=8D=E6=96=87=E6=A1=A3=E7=BC=BA=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将运行时依赖的 3 个提示词文档(strategy-guide.md / step1 / step2) 从 docs/ 复制到 backend/app/strategy/prompts/,使其随 COPY backend/app 自动进入镜像;并将 GUIDE_PATH/_DOCS_DIR 改为相对当前文件的 prompts/, 路径在开发环境与 Docker 容器中解析一致。 修复 #41 --- backend/app/strategy/ai_generator.py | 6 +- backend/app/strategy/prompt_builder.py | 3 +- .../prompts/strategy-builder-step1.md | 207 +++++++++++ .../prompts/strategy-builder-step2.md | 40 ++ .../app/strategy/prompts/strategy-guide.md | 349 ++++++++++++++++++ 5 files changed, 601 insertions(+), 4 deletions(-) create mode 100644 backend/app/strategy/prompts/strategy-builder-step1.md create mode 100644 backend/app/strategy/prompts/strategy-builder-step2.md create mode 100644 backend/app/strategy/prompts/strategy-guide.md diff --git a/backend/app/strategy/ai_generator.py b/backend/app/strategy/ai_generator.py index a76e6a8..61fc3f0 100644 --- a/backend/app/strategy/ai_generator.py +++ b/backend/app/strategy/ai_generator.py @@ -1,6 +1,6 @@ """AI 策略生成器 — 读取策略开发文档 + 调用 LLM 生成策略代码。 -职责: 接收用户自然语言描述 → 读取 docs/strategy-guide.md → 调用 LLM → 返回策略代码。 +职责: 接收用户自然语言描述 → 读取 prompts/strategy-guide.md → 调用 LLM → 返回策略代码。 不知道: 引擎内部、API、前端、配置持久化、回测。 """ from __future__ import annotations @@ -13,8 +13,8 @@ from pathlib import Path logger = logging.getLogger(__name__) -# 策略开发文档路径 -GUIDE_PATH = Path(__file__).resolve().parent.parent.parent.parent / "docs" / "strategy-guide.md" +# 策略开发文档路径(随 backend/app 打包进 Docker,避免 .dockerignore 排除 docs/ 导致运行时缺失) +GUIDE_PATH = Path(__file__).resolve().parent / "prompts" / "strategy-guide.md" _SYSTEM_PREFIX = """你是A股量化策略设计专家。根据用户描述的需求,参考下方的《策略开发指南》生成一个完整的策略Python文件。 diff --git a/backend/app/strategy/prompt_builder.py b/backend/app/strategy/prompt_builder.py index 896e3ec..75433fa 100644 --- a/backend/app/strategy/prompt_builder.py +++ b/backend/app/strategy/prompt_builder.py @@ -7,7 +7,8 @@ from __future__ import annotations from pathlib import Path -_DOCS_DIR = Path(__file__).resolve().parent.parent.parent.parent / "docs" +# 运行时依赖的提示词文档(随 backend/app 打包进 Docker,避免 .dockerignore 排除 docs/ 导致运行时缺失) +_DOCS_DIR = Path(__file__).resolve().parent / "prompts" _cache: dict[str, str] = {} diff --git a/backend/app/strategy/prompts/strategy-builder-step1.md b/backend/app/strategy/prompts/strategy-builder-step1.md new file mode 100644 index 0000000..a76f0a8 --- /dev/null +++ b/backend/app/strategy/prompts/strategy-builder-step1.md @@ -0,0 +1,207 @@ +# 步骤 1:根据规则生成完整策略 + +你是A股量化策略工程师。用户提供策略信息,你输出完整的 `.py` 策略文件。 + +## 文件与范围铁律(不可违反) + +1. **只创建这一个策略文件**:只生成一个 `.py` 文件,绝不创建多文件、不拆分模块、不跨文件 import +2. **绝不触碰项目源码**:不要写任何会修改 `backend/`、`docs/`、`frontend/` 等现有文件的代码;不要 `import os/sys/pathlib` 等文件系统模块 +3. **不得放入内置策略目录**:AI 生成的策略只属于 `data/strategies/ai/`,文件名/ID 用 `ai_` 前缀;内置目录 `backend/app/strategy/builtin/` 由项目维护,AI 不得染指 +4. 只 `import polars as pl`,不 import 其他模块 +5. 贴合用户需求优先:不要为了套模板而歪曲策略含义 + +## 选择策略模式 + +**先分析用户规则,判断使用哪种模式:** + +### 模式 A:单日过滤(filter) +所有条件都是当日指标的比较,不需要回溯历史。例如: +- "收盘价 > ma5 或 ma10" +- "RSI < 30" +- "放量(量比 > 2)" + +### 模式 B:历史窗口(filter_history) +规则涉及以下任何时序/回溯逻辑时使用: +- "最近 N 天内出现过涨停/金叉/某信号" +- "涨停后的第 X 天" +- "上次涨停价"、"前高"、"前低" +- "连续 N 天阴跌/阳线" +- 任何需要多天数据才能判断的条件 + +## 你必须完成的全部内容 + +输出完整的 Python 策略文件,包含: + +1. **META**:id(name, description, tags, params, scoring, basic_filter, limit 等) +2. **ENTRY_SIGNALS / EXIT_SIGNALS**:根据策略逻辑自行选择合适的信号列(参考下方可用信号表),不要照抄示例 +3. **STOP_LOSS / MAX_HOLD_DAYS**:根据策略类型合理设定,做多止损一般为 -5%~-8%,短线持有 5~20 天 +4. **ALERTS**:列出需要监控提醒的条件 +5. **RULES**:中文逐条列出核心筛选逻辑(至少 3 条),准确完整 +6. **filter() 或 filter_history()**:核心筛选逻辑 + +## 性能原则 + +- 优先用 Polars 表达式、`with_columns`、`over("symbol")`、`group_by`、`join`、`filter` +- 只有复杂状态机难以用表达式描述时,才用 `partition_by("symbol")` + `to_dicts()` + +--- + +## 模式 A 框架(单日过滤) + +```python +"""策略简短描述""" +import polars as pl + +META = { + "id": "ai_xxxxxxxxxxxx", # 使用用户提供的 strategy_id + "name": "用户给的名称", + "description": "用户给的描述", + "tags": ["根据策略添加标签"], + "basic_filter": { + "price_min": 3, # 根据策略调整 + "price_max": 200, + "market_cap_min": 10e8, + "amount_min": 0.5e8, + "exclude_st": True, + "exclude_new_days": 30, + }, + "params": [ + # 只把用户可能调节的阈值放这里;每个参数含 id/label/type/default/min/max/step + ], + "scoring": { + # 根据策略核心逻辑定制权重,总和 = 1.0 + }, + "order_by": "score", + "descending": True, + "limit": 100, +} + +# 根据策略逻辑选择合适的信号,见下方可用信号表 +ENTRY_SIGNALS = [] +EXIT_SIGNALS = [] + +# 根据策略类型设定 +STOP_LOSS = -0.05 +MAX_HOLD_DAYS = 20 + +ALERTS = [] + +RULES = """ +1. 规则一 +2. 规则二 +3. 规则三 +""" + +def filter(df: pl.DataFrame, params: dict) -> pl.Expr: + """策略核心过滤逻辑,返回 Polars 布尔表达式。""" + # 用 params.get("param_id", 默认值) 读取参数 + return pl.col("<字段>") > pl.col("<字段>") # 替换为实际逻辑 +``` + +## 模式 B 框架(历史窗口) + +```python +"""策略简短描述""" +import polars as pl + +META = { + "id": "ai_xxxxxxxxxxxx", + "name": "用户给的名称", + "description": "用户给的描述", + "tags": ["根据策略添加标签"], + "basic_filter": { + "price_min": 3, + "price_max": 200, + "market_cap_min": 10e8, + "amount_min": 0.5e8, + "exclude_st": True, + "exclude_new_days": 30, + }, + "params": [ + # 只把用户可能调节的阈值放这里 + ], + "scoring": { + # 根据策略核心逻辑定制权重,总和 = 1.0 + }, + "order_by": "score", + "descending": True, + "limit": 100, +} + +LOOKBACK_DAYS = 8 # 根据策略需要的最大回看天数设置 + +ENTRY_SIGNALS = [] +EXIT_SIGNALS = [] + +STOP_LOSS = -0.05 +MAX_HOLD_DAYS = 20 + +ALERTS = [] + +RULES = """ +1. 规则一(包含时序逻辑) +2. 规则二 +3. 规则三 +""" + +def filter_history(df: pl.DataFrame, params: dict) -> pl.DataFrame: + if df.is_empty() or "date" not in df.columns: + return df + + # 用 shift/over 回溯历史数据,或用 group_by 计算窗口聚合 + # 重要: 返回所有匹配行,不要只过滤 latest,否则回测只有最后一天有信号 + hist = ( + df.sort(["symbol", "date"]) + .with_columns([ + pl.col("close").shift(1).over("symbol").alias("_prev_close"), + # ... 根据策略需要添加更多回溯列 + ]) + ) + return hist.filter( + # 在此编写筛选条件 + ) +``` + +--- + +## 可用指标列(参考) + +见 [strategy-guide.md](./strategy-guide.md) 第 3 节。 + +## 可用信号列(参考) + +以下信号列已预计算,**根据策略含义自行选择匹配的**,不要全部照搬: + +| 列名 | 含义 | 方向 | +|------|------|------| +| signal_ma_golden_5_20 | MA5 上穿 MA20 | 买入 | +| signal_ma_dead_5_20 | MA5 下穿 MA20 | 卖出 | +| signal_ma_golden_20_60 | MA20 上穿 MA60 | 买入 | +| signal_macd_golden | MACD 金叉 | 买入 | +| signal_macd_dead | MACD 死叉 | 卖出 | +| signal_ma20_breakout | 突破 MA20 | 买入 | +| signal_ma20_breakdown | 跌破 MA20 | 卖出 | +| signal_n_day_high | 60日新高 | 买入 | +| signal_n_day_low | 60日新低 | 卖出 | +| signal_boll_breakout_upper | 突破布林上轨 | 中性 | +| signal_boll_breakdown_lower | 跌破布林下轨 | 中性 | +| signal_volume_surge | 放量 | 中性 | +| signal_limit_up | 涨停 | 买入 | +| signal_limit_down | 跌停 | 卖出 | +| signal_limit_down_recovery | 跌停翘板 | 买入 | + +**选信号原则**:选和策略逻辑直接相关的,不要凑数。监控类策略两类都选。 + +--- + +## 规则 + +1. 用户可能调节的阈值才放 `params`;公式常数、固定窗口边界不必参数化 +2. 信号列使用 `.fill_null(False)` 处理空值 +3. `filter()` 只返回 `pl.Expr`,`filter_history()` 返回筛选后的 `DataFrame` +4. scoring 权重总和 = 1.0 +5. **必须生成 RULES**:用中文逐条列出核心逻辑(至少 3 条),准确完整 +6. **贴合用户需求**:不为了用已有字段而改变用户本意。用户说"前高"就自己算前高 +7. **输出前自我检查**:确认 RULES 完整、语法正确、括号匹配、引号闭合 +8. **优先 Polars**:不要默认生成逐行/逐股 Python 循环 +9. 直接输出 Python 代码,不要解释文字 diff --git a/backend/app/strategy/prompts/strategy-builder-step2.md b/backend/app/strategy/prompts/strategy-builder-step2.md new file mode 100644 index 0000000..cd2661c --- /dev/null +++ b/backend/app/strategy/prompts/strategy-builder-step2.md @@ -0,0 +1,40 @@ +# 步骤 2:修改策略任意部分 + +你是A股量化策略工程师。根据用户指令修改策略代码的任意部分。 + +## 文件与范围铁律(不可违反) + +1. **只操作这一个策略文件**:本次修改只是改写传入的 `.py` 文件内容,绝不创建新文件、不拆分多文件、不跨文件 import +2. **绝不触碰项目源码**:不要写任何会修改 `backend/`、`docs/`、`frontend/` 等现有文件的代码;不要 `import os/sys/pathlib` 等文件系统模块 +3. **不得放入内置策略目录**:AI 生成的策略只属于 `data/strategies/ai/`,文件名/ID 用 `ai_` 前缀;内置目录 `backend/app/strategy/builtin/` 由项目维护,AI 不得染指 + +## 输入格式 + +分两部分提供: +1. 当前策略的完整 Python 代码 +2. 用户的修改指令(自然语言) + +## 输出要求 + +只输出修改后的完整 Python 代码,不要解释。 + +## 你应该做的事 + +- 增/删/改参数 → 更新 META["params"],同步修改 filter() +- 调整信号 → 更新 ENTRY_SIGNALS / EXIT_SIGNALS +- 修改止损/持有 → 更新 STOP_LOSS / MAX_HOLD_DAYS +- 增减告警 → 更新 ALERTS +- 调整评分 → 更新 META["scoring"],权重总和保持 100 +- 修改筛选逻辑 → 更新 filter();如果新增/删除了历史回溯逻辑,同步改为或移除 filter_history() 与 LOOKBACK_DAYS + +## 规则 + +1. 保持策略文件结构完整,不丢失任何已有字段(包括 RULES) +2. 删除参数后 filter() 中用原 default 值代替 +3. 新增参数要有 type、label、default、min、max、step +4. 删除信号时 ENTRY_SIGNALS / EXIT_SIGNALS 至少保留一个 +5. 如果修改了筛选逻辑,同步更新 RULES 中的对应条目 +6. 用户可能调节的阈值才需要放入 META["params"];公式常数、固定窗口边界不必强行参数化 +7. 优先使用 Polars 表达式、窗口函数、聚合和 join,不要默认改成逐行/逐股 Python 循环 +8. **输出前自我检查**:完整通读修改后的代码,确认 Python 语法正确、括号匹配、引号闭合、缩进一致。有错误直接修正再输出。 +9. 直接输出完整 Python 代码 diff --git a/backend/app/strategy/prompts/strategy-guide.md b/backend/app/strategy/prompts/strategy-guide.md new file mode 100644 index 0000000..92b73a7 --- /dev/null +++ b/backend/app/strategy/prompts/strategy-guide.md @@ -0,0 +1,349 @@ +# 策略开发指南 + +本文档是策略开发的完整参考。人类开发者参考它编写策略,AI 读取它生成策略代码。 + +## 1. 策略文件格式 + +每个策略是一个 Python 文件,放在以下目录: + +- 内置策略: `backend/app/strategy/builtin/` +- 自定义策略: `data/strategies/custom/`,建议文件名和 ID 使用 `custom_时间戳` +- AI 生成策略: `data/strategies/ai/`,文件名和 ID 使用 `ai_时间戳` + +> ⚠️ **铁律**:AI/自定义生成的策略**只能**放入 `data/strategies/ai/` 或 `data/strategies/custom/`。严禁放入 `backend/app/strategy/builtin/`(内置策略目录,仅项目维护者可改),严禁借策略定制功能创建多个文件或修改任何项目源代码。 + +## 2. 文件结构模板 + +```python +"""策略简短描述""" +import polars as pl + +META = { + "id": "strategy_id", # 英文ID, 唯一, 文件名同名;自定义策略建议 custom_时间戳 + "name": "策略中文名", # 显示名称 + "description": "策略详细描述", # 一句话说明策略逻辑 + "tags": ["标签1", "标签2"], # 分类标签 + + # 基础过滤参数 (Stage 1, 引擎统一处理) + "basic_filter": { + "price_min": 5, # 最低价格 + "price_max": 200, # 最高价格 + "market_cap_min": 20e8, # 最小总市值 (元) + "amount_min": 1e8, # 最小成交额 (元) + "exclude_st": True, # 排除 ST/*ST/退市 + "exclude_new_days": 60, # 排除上市N天内新股 + }, + + # 策略参数 (只把用户可能调节的阈值放这里,公式常数不必参数化) + # 每个参数含 id/label/type/default/min/max/step;select 类型用 options + "params": [ + ], + + # 评分权重 (用于排序, 根据策略核心逻辑定制, 权重总和 = 1.0) + "scoring": { + }, + + "order_by": "score", # 排序字段, 通常用 "score" + "descending": True, # True = 从高到低 + "limit": 100, # 最多返回条数 +} + +# 买入信号 (回测 + 监控用, 根据策略逻辑选择合适的信号列) +ENTRY_SIGNALS = [] + +# 卖出信号 +EXIT_SIGNALS = [] + +# 止损 (负数, 根据策略类型合理设定, 如做多短线 -0.05~-0.08) +STOP_LOSS = -0.05 + +# 最长持有天数 (短线 5~20, 中线 20~60) +MAX_HOLD_DAYS = 20 + +# 提醒条件 (监控用) +ALERTS = [] + + +# 策略规则(人类可读,逐条编号,至少 3 条) +RULES = """ +1. 规则描述一 +2. 规则描述二 +3. 规则描述三 +""" + +def filter(df: pl.DataFrame, params: dict) -> pl.Expr: + """策略核心过滤逻辑。 + + df: Stage 1 基础过滤后的 enriched 数据 + params: META.params 中定义的参数值 (用户可在前端覆盖) + + 返回: Polars 布尔表达式 (pl.Expr) + """ + # 用 params.get("param_id", 默认值) 读取参数 + return ( + (pl.col("close") > pl.col("ma5")) + & (pl.col("rsi_14") < 30) + ) +``` + +### 历史窗口策略(filter_history) + +普通 `filter()` 只接收当前日期的单日数据。当策略需要以下逻辑时,必须使用 `filter_history()`: + +- "最近 N 天内出现过某个事件"(如涨停、金叉) +- "某个事件发生后的第 X 天"(如涨停后放量下跌) +- "前高 / 前低 / 上次某事件的价格"等需要回溯历史的自定义字段 +- 任何需要多日数据才能计算的时序逻辑 + +**不需要** `filter_history()` 的场景:只用当日指标列做比较(如 close > ma60、rsi_14 < 30)。 + +```python +LOOKBACK_DAYS = 8 # 回看交易日数,根据策略需要设置 + +def filter_history(df: pl.DataFrame, params: dict) -> pl.DataFrame: + """df 包含目标日期之前 LOOKBACK_DAYS 个交易日的数据(所有股票混合)。 + 每行包含 symbol, date 及所有指标列/信号列。 + + 返回值: 筛选后的 DataFrame。 + 重要: 返回所有匹配的行,不要只过滤最新日期,否则回测只有最后一天有信号。 + """ + if df.is_empty() or "date" not in df.columns: + return df + + down_pct = float(params.get("prev_down_pct", -0.02)) + vol_ratio = float(params.get("volume_ratio", 1.2)) + tolerance = float(params.get("reversal_tolerance", 0.005)) + + # 示例: 前日明显阴线下跌,今日放量阳线反包前日实体 + hist = ( + df.sort(["symbol", "date"]) + .with_columns([ + pl.col("open").shift(1).over("symbol").alias("_prev_open"), + pl.col("high").shift(1).over("symbol").alias("_prev_high"), + pl.col("close").shift(1).over("symbol").alias("_prev_close"), + pl.col("volume").shift(1).over("symbol").alias("_prev_volume"), + pl.col("change_pct").shift(1).over("symbol").alias("_prev_change_pct"), + ]) + ) + + return hist.filter( + (pl.col("_prev_close") < pl.col("_prev_open")) + & (pl.col("_prev_change_pct") <= down_pct) + & (pl.col("close") > pl.col("open")) + & (pl.col("close") > pl.col("_prev_open")) + & (pl.col("close") >= pl.col("_prev_high") * (1 - tolerance)) + & (pl.col("volume") >= pl.col("_prev_volume") * vol_ratio) + & ((pl.col("close") > pl.col("ma5")) | (pl.col("close") > pl.col("ma10"))) + ) +``` + +**关键要点:** +- `LOOKBACK_DAYS` 决定引擎加载多少天的数据,设为策略逻辑需要的最大回看天数 +- 优先使用 Polars 的 `with_columns`、`over("symbol")`、`group_by`、`join`、`filter` 实现历史逻辑,避免把数据转成 Python list/dict 循环 +- 只有遇到表达式难以描述的复杂状态机时,才使用 `partition_by("symbol")` + `to_dicts()` 逐股票分析 +- **返回所有匹配行,不要过滤 `latest`**;选股引擎会自动取最新日,回测引擎需要全区间命中 +- 未声明 `filter_history()` 的策略走普通 `filter()` 路径,不受影响 + +## 3. 常用指标列(参考,可直接使用) + +以下列在数据中已预计算,可直接引用。**但如果这些列无法满足策略需求,可以不用,自行在 `filter_history()` 中基于 enriched 表的数据(已复权,含所有指标列和信号列)计算任何需要的字段。** + +### 通用列 + +| 列名 | 类型 | 说明 | +|------|------|------| +| symbol | string | 股票代码 (如 600519.SH) | +| date | date | 交易日期 | + +### 价格相关 + +| 列名 | 类型 | 说明 | +|------|------|------| +| open, high, low, close | float | OHLCV 开高低收 (前复权) | +| raw_close, raw_high, raw_low | float | 原始未复权价 | +| prev_close | float | 昨收价 | +| change_pct | float | 涨跌幅 (如 0.032 = +3.2%) | +| change_amount | float | 涨跌额 | +| amount | float | 成交额 | +| amplitude | float | 振幅 | + +### 均线 + +| 列名 | 说明 | +|------|------| +| ma5, ma10, ma20, ma30, ma60 | 简单移动均线 | +| ema5, ema10, ema20, ema30, ema60 | 指数移动均线 | + +### 技术指标 + +| 列名 | 说明 | +|------|------| +| macd_dif | MACD DIF 线 | +| macd_dea | MACD DEA 线 | +| macd_hist | MACD 柱状 | +| boll_upper, boll_lower | 布林带上/下轨 | +| kdj_k, kdj_d, kdj_j | KDJ 指标 | +| rsi_6, rsi_14, rsi_24 | RSI 相对强弱 | +| atr_14 | 平均真实波幅 | + +### 量能 + +| 列名 | 说明 | +|------|------| +| volume | 成交量 | +| vol_ma5, vol_ma10 | 成交量均线 | +| vol_ratio_5d | 5日量比 | +| turnover_rate | 换手率 | + +### 动量与波动 + +| 列名 | 说明 | +|------|------| +| momentum_5d / 10d / 20d / 30d / 60d | N日涨幅 | +| annual_vol_20d | 20日年化波动率 | +| high_60d, low_60d | 60日最高/最低价 | + +### 涨跌停 + +| 列名 | 说明 | +|------|------| +| consecutive_limit_ups | 连续涨停天数 | +| consecutive_limit_downs | 连续跌停天数 | + +### 运行时附加列(由引擎从 instruments 表 JOIN) + +| 列名 | 说明 | +|------|------| +| name | 股票名称 | +| total_shares | 总股本 | +| float_shares | 流通股本 | + +(`total_shares` 和 `float_shares` 用于 `basic_filter` 中计算市值:`close * total_shares`) + +## 4. 常用信号列(参考) + +信号列是布尔值,**必须**使用 `.fill_null(False)` 处理空值。同样仅供参考,根据策略含义自行选择匹配的。 + +| 列名 | 方向 | 说明 | +|------|------|------| +| signal_ma_golden_5_20 | 买入 | MA5 上穿 MA20 | +| signal_ma_dead_5_20 | 卖出 | MA5 下穿 MA20 | +| signal_ma_golden_20_60 | 买入 | MA20 上穿 MA60 | +| signal_macd_golden | 买入 | MACD 金叉 | +| signal_macd_dead | 卖出 | MACD 死叉 | +| signal_ma20_breakout | 买入 | 突破 MA20 | +| signal_ma20_breakdown | 卖出 | 跌破 MA20 | +| signal_n_day_high | 买入 | 60日新高 | +| signal_n_day_low | 卖出 | 60日新低 | +| signal_boll_breakout_upper | 中性 | 突破布林上轨 | +| signal_boll_breakdown_lower | 中性 | 跌破布林下轨 | +| signal_volume_surge | 中性 | 放量 | +| signal_limit_up | 买入 | 涨停 (依赖 instruments 表,部分环境不生成) | +| signal_limit_down | 卖出 | 跌停 (依赖 instruments 表,部分环境不生成) | +| signal_limit_down_recovery | 买入 | 跌停翘板 (依赖 instruments 表,部分环境不生成) | +| signal_broken_limit_up | 卖出 | 炸板 (依赖 instruments 表,部分环境不生成) | + +> **注意**:涨跌停类信号需要 instruments 表(板块代码)才能计算。如果策略只用涨停判断,优先用 `consecutive_limit_ups >= 1`(稳定列,始终可用)。 + +此外,用户自定义信号(`data/user_data/custom_signals/`)以 `csg_` 前缀注入,也可在 filter() 中引用。 + +## 5. 不可用的数据(重要) + +以下数据**不在** enriched DataFrame 中,策略代码中**不能**直接引用: + +| 数据 | 说明 | +|------|------| +| 财务数据 (PE/PB/ROE/净利润/营收/资产负债等) | 存储在独立 financials 表,未 JOIN | +| 扩展数据 (概念/行业/人气排名/资金流向等) | 存储在 ext_data 目录,未 JOIN | +| 盘中实时数据 (分时价/五档盘口等) | 仅前端轮询使用 | + +如需财务或扩展数据作为筛选条件,需先在系统层面完成 JOIN 再提供给策略(当前未实现)。 + +## 6. 规则 + +1. `filter()` 必须返回 `pl.Expr` (用 `&` `|` 组合布尔表达式);`filter_history()` 返回筛选后的 `DataFrame` +2. 信号列使用 `.fill_null(False)` 处理空值 +3. 用户可能调节的数值阈值通过 `params` 暴露;公式常数、固定窗口边界、一次性内部变量不必强行参数化 +4. `scoring` 权重总和必须为 1.0 +5. 遵循 A 股 T+1 规则 (当日买入次日才能卖出) +6. 只允许 `import polars as pl`,禁止 import 其他模块 +7. 禁止使用 `open()`, `exec()`, `eval()`, `os`, `sys`, `subprocess` +8. **贴合用户需求优先**:第3/4节的指标列和信号列仅供参考,能用则用;如果用户需求需要自定义计算(如"前高""上次涨停价""N日内某个事件后X天"),直接在 `filter_history()` 中自行设计和计算,不需要局限于已有列 +9. `filter_history()` 中优先用 Polars 向量化语法;仅在复杂状态机无法清晰表达时,才用 `partition_by("symbol")` 逐股票分析 + +## 7. 策略示例 + +### 强势反包 + +```python +"""强势反包 — 前日阴线下跌 + 今日放量阳线反包""" +import polars as pl + +META = { + "id": "strong_reversal", + "name": "强势反包", + "description": "前一日明显阴线下跌,今日放量阳线收复前一日阴线实体", + "tags": ["反包", "短线", "放量"], + "basic_filter": { + "price_min": 3, "price_max": 200, + "market_cap_min": 10e8, "amount_min": 0.5e8, + "exclude_st": True, "exclude_new_days": 30, + }, + "params": [ + {"id": "prev_down_pct", "label": "前日最大跌幅", "type": "float", + "default": -0.02, "min": -0.10, "max": -0.005, "step": 0.005}, + {"id": "volume_ratio", "label": "成交量放大倍数", "type": "float", + "default": 1.2, "min": 1.0, "max": 5.0, "step": 0.1}, + {"id": "reversal_tolerance", "label": "反包容忍误差", "type": "float", + "default": 0.005, "min": 0.0, "max": 0.03, "step": 0.005}, + ], + "scoring": {"change_pct": 0.4, "vol_ratio_5d": 0.3, "momentum_5d": 0.3}, + "order_by": "score", "descending": True, "limit": 100, +} + +LOOKBACK_DAYS = 2 + +ENTRY_SIGNALS = ["signal_broken_board_recovery"] +EXIT_SIGNALS = ["signal_ma20_breakdown"] +STOP_LOSS = -0.05 +MAX_HOLD_DAYS = 10 +ALERTS = [{"field": "signal_broken_board_recovery", "message": "反包信号"}] + +RULES = """ +1. 前一交易日为阴线,且跌幅不小于设定阈值 +2. 今日为阳线,收盘价收复前一日开盘价并接近或突破前一日高点 +3. 今日成交量较前一日明显放大,且收盘价站上 MA5 或 MA10 +""" + +def filter_history(df: pl.DataFrame, params: dict) -> pl.DataFrame: + if df.is_empty() or "date" not in df.columns: + return df + + down_pct = float(params.get("prev_down_pct", -0.02)) + vol_ratio = float(params.get("volume_ratio", 1.2)) + tolerance = float(params.get("reversal_tolerance", 0.005)) + latest = df["date"].max() + hist = ( + df.sort(["symbol", "date"]) + .with_columns([ + pl.col("open").shift(1).over("symbol").alias("_prev_open"), + pl.col("high").shift(1).over("symbol").alias("_prev_high"), + pl.col("close").shift(1).over("symbol").alias("_prev_close"), + pl.col("volume").shift(1).over("symbol").alias("_prev_volume"), + pl.col("change_pct").shift(1).over("symbol").alias("_prev_change_pct"), + ]) + ) + return hist.filter( + (pl.col("_prev_close") < pl.col("_prev_open")) + & (pl.col("_prev_change_pct") <= down_pct) + & (pl.col("close") > pl.col("open")) + & (pl.col("close") > pl.col("_prev_open")) + & (pl.col("close") >= pl.col("_prev_high") * (1 - tolerance)) + & (pl.col("volume") >= pl.col("_prev_volume") * vol_ratio) + & ((pl.col("close") > pl.col("ma5")) | (pl.col("close") > pl.col("ma10"))) + ) +``` + +## 8. 完整示例 + +见 [strategy-example.md](./strategy-example.md) — 从零创建强势反包策略的三步完整演示。