Files
stock/local/docs/strategy-guide.md
T
2026-07-04 16:32:48 +08:00

348 lines
14 KiB
Markdown
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.
# 策略开发指南
本文档是策略开发的完整参考。人类开发者参考它编写策略,AI 读取它生成策略代码。
## 1. 策略文件格式
每个策略是一个 Python 文件,放在以下目录:
- 内置策略: `backend/app/strategy/builtin/`
- 自定义策略: `data/strategies/custom/`,建议文件名和 ID 使用 `custom_时间戳`
- AI 生成策略: `data/strategies/ai/`,文件名和 ID 使用 `ai_时间戳`
## 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/stepselect 类型用 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) — 从零创建强势反包策略的三步完整演示。