说明:本报告只基于代码路径解读策略逻辑,不构成投资建议。标题、注释或原帖收益宣传不能直接视为策略有效性结论。
## 1. 策略一句话概括
这个策略本质上是“小市值股票池 + 竞价后低开买入 + 下午或盘中止盈卖出 + 涨停股延迟卖出”的 A 股短线策略。
它试图赚的是:小市值股票在短期趋势仍偏强时,次日明显低开后的反弹或修复收益。策略收益假设主要来自小市值因子、低开反转、短线高换手和涨停延迟卖出。
## 2. 执行时间线
### 初始化阶段
`initialize(context)` 中完成以下设置:
1. `g.choice = 500`:每天先取市值最小的 500 只股票作为候选。
2. `g.amount = 5`:最大持仓数量为 5 只。
3. `g.muster = []`:盘前候选股票池。
4. `set_benchmark('000001.XSHG')` 后又执行 `set_benchmark('000905.XSHG')`,最终基准应为中证 500,前一次设置被覆盖。
5. `set_option('use_real_price', True)`:使用真实价格口径。
6. `set_slippage(PriceRelatedSlippage(0.000))`:滑点为 0。
7. 股票交易成本为:买入佣金 0.011%,卖出佣金 0.011%,卖出印花税 0.1%,最低佣金 5 元。
### 每日调度顺序
1. `9:15` 执行 `prepare(context)`:构建小市值候选池并过滤不可买股票。
2. `9:26` 执行 `buy(context)`:根据低开和趋势条件买入,最多补到 5 只。
3. `13:06` 执行 `sell(context)`:对非涨停持仓尝试清仓。
4. `every_bar` 执行 `high_sell(context)`:9:35 之后,若持仓相对开盘涨幅超过 6.5% 且未涨停,尝试卖出。
代码没有定义 `before_trading_start`、`handle_data`、`after_trading_end`。`print_trade_info(context)` 虽然存在,但没有被调度执行。
## 3. 交易标的与股票池
交易标的是 A 股股票,代码中通过 `get_security_info(stock).type == 'stock'` 在盘中卖出逻辑里进一步确认持仓类型。
股票池构建在 `prepare(context)`:
1. 使用 `get_fundamentals(query(valuation.code, valuation.market_cap).order_by(valuation.market_cap.asc()).limit(g.choice))`。
2. 也就是按总市值从小到大排序,取前 500 只。
3. 然后过滤停牌、ST、名称含 `ST`、`*`、`退` 的股票。
4. 过滤科创板和北交所:代码排除 `4`、`8`、`68` 开头的股票。
5. 过滤当前涨停、跌停股票。
6. 过滤当前已经持有的股票。
注意:代码中定义了 `filter_new_stock(context, stock_list)`,但实际没有调用,所以并没有过滤次新股。注释说“30为创业板”,但 `filter_kcbj_stock` 没有排除 `30` 开头的创业板股票。
## 4. 数据来源与信号逻辑
### 数据来源
策略使用的数据包括:
- `get_fundamentals`:读取估值表中的股票代码和总市值。
- `get_current_data()`:读取停牌、ST、名称、开盘价、当前价、涨停价、跌停价。
- `history`:读取分钟收盘、日线停牌、日线高低价、日线收盘价。
- `get_price`:读取前一交易日收盘价和最低价。
- `talib.EMA`:用 60 日收盘序列计算 5 日 EMA。
### 候选过滤
`prepare(context)` 在 9:15 先取最小市值 500 只,再进行过滤。关键条件之一是:
```python
current_data[s].low_limit < current_data[s].day_open < current_data[s].high_limit
```
这个条件意图是排除开盘即涨停或跌停的股票。但调度时间是 9:15,当时 `day_open` 是否已经是可用且稳定的当日开盘价,需要在聚宽分钟回测环境中确认。
### 买入信号
`buy(context)` 中对 `g.muster` 逐只检查:
1. 最近 5 日没有停牌:`history(5, '1d', 'paused', s).max().values[0] == 0`。
2. 最近 4 日振幅不超过 20%:
```python
low = history(4, '1d', 'low', s).min().values[0]
high = history(4, '1d', 'high', s).max().values[0]
precent = (high - low) / low * 100
precent < = 20
```
3. 当日开盘价低于昨日最低价:
```python
day_open = data_today[s].day_open
day1_low = get_price(s, count=1, end_date=context.previous_date).iloc[-1]['low']
day_open < day1_low
```
4. 昨日收盘价高于 5 日 EMA:
```python
day1_close = get_price(s, count=1, end_date=context.previous_date).iloc[-1]['close']
his = history(60, '1d', 'close', s)
ema1 = talib.EMA(his.values.flatten(), timeperiod=5)[-1]
day1_close > ema1
```
综合看,买入条件是:小市值股票,近期没有剧烈波动,昨日仍在短均线上方,但今天竞价后低开到昨日最低价以下。
需要注意:`history(60, '1d', 'close', s)` 和 `history(4, '1d', ...)` 在 9:26 是否包含当日未完成日线,取决于聚宽 API 在分钟回测中的返回语义。代码没有显式把这些 `history` 调用限定到 `context.previous_date`。
## 5. 买卖和仓位管理
### 买入方式
最大持仓数为 5:
```python
available_slots = g.amount - len(context.portfolio.positions)
allocation = context.portfolio.cash / available_slots
```
策略用当前现金按剩余仓位等分,每只股票按 100 股整数买入:
```python
nums = math.floor(int(allocation / last_price))
int_nums = int(nums // 100) * 100
order(s, int_nums, LimitOrderStyle(last_price * 1.017))
```
买入限价是当前价上浮 1.7%。这提高了成交概率,但对于小市值、竞价后快速波动股票,回测成交价和真实成交价可能差异很大。
### 卖出方式
`sell(context)` 每天 13:06 对所有持仓检查:
```python
if current_data[s].last_price < current_data[s].high_limit:
order_target(s, 0, LimitOrderStyle(last_price * 0.983))
```
也就是只要当前没有涨停,就用当前价下浮 1.7% 的限价单清仓。若股票仍然涨停,则不卖。
`high_sell(context)` 在 9:35 之后每个 bar 检查持仓:
```python
zhangfu_stock = (nowprice - day_open) / day_open
if zhangfu_stock > 0.065 and nowprice < high_limit:
order_target(i, 0, LimitOrderStyle(nowprice * 0.983))
```
即从当日开盘价算起涨幅超过 6.5%,且未封涨停,就提前卖出。
### A 股 T+1 影响
这是该策略最关键的******约束之一。策略在 9:26 买入,13:06 又对所有持仓执行卖出逻辑。A 股股票通常 T+1,今天买入的股票当天不能卖出。如果回测撮合模型没有严格处理 T+1,收益会被严重高估。即使聚宽处理了 T+1,也需要在交易日志中确认当天买入部分是否被拒绝卖出。
## 6. 风控和约束
代码中真实触发交易的风控包括:
- 最大持仓 5 只。
- 停牌过滤。
- ST 和退市名称过滤。
- 科创板、北交所过滤。
- 涨跌停过滤。
- 近期 4 日振幅不超过 20%,用于过滤剧烈波动。
- 盘中涨幅超过 6.5% 且未涨停时卖出。
- 下午 13:06 对非涨停持仓清仓。
代码中存在但未实际生效的风控:
- `filter_new_stock` 未被调用,所以没有过滤次新股。
- `g.bucket`、`g.summit` 没有实际使用。
- `print_trade_info` 未被调度,所以成交和持仓日志不会自动输出。
## 7. 关键参数
- `g.choice = 500`:小市值候选池数量。越小,小市值暴露越极端,容量和流动性风险越高。
- `g.amount = 5`:最大持仓数。持仓越少,单票风险越集中。
- `precent < = 20`:最近 4 日高低点振幅阈值。阈值越低,越偏向低波动形态;阈值越高,可能纳入更多急跌急涨股票。
- `day_open < day1_low`:低开破昨日最低价,是主要反转信号。
- `day1_close > ema1`:趋势过滤条件。
- `0.065`:盘中止盈阈值,相对当日开盘涨幅超过 6.5% 卖出。
- `1.017` 和 `0.983`:买入上浮、卖出下浮的限价比例。
- `0` 滑点:对高换手短线小市值策略影响很大,容易高估收益。
## 8. 主要风险分级
### 高风险
1. 9:15 使用 `current_data[s].day_open` 过滤股票,可能存在当日开盘数据时点不可得或不稳定的问题。需要聚宽分钟回测日志确认。
2. 9:26 使用 `history(..., '1d', ...)` 计算最近高低价和 EMA,代码没有显式指定 `end_date=context.previous_date`。如果包含当日未完成日线,就有未来数据或当日数据泄露风险。
3. 9:26 买入后,13:06 卖出逻辑会覆盖所有持仓。A 股 T+1 下,当天买入股票不能当天卖出,必须确认聚宽是否拒绝这部分卖单。
4. 小市值、竞价后、短线高换手策略使用 0 滑点,回测结果可能明显高估。
### 中风险
1. `get_fundamentals` 未显式传入 `context.previous_date`,估值数据时点需要确认。
2. 未调用 `set_option('avoid_future_data', True)`,不能依赖平台自动防未来函数。
3. 未过滤次新股,尽管代码里写了 `filter_new_stock`。
4. 未排除创业板股票,尽管注释提到“30为创业板”。
5. 下单后没有检查订单状态、部分成交、未成交、撤单或资金不足。
6. 买入数量可能为 0,代码仍会调用 `order(s, int_nums, ...)`。
7. `math.floor` 使用了 `math`,但文件没有显式 `import math`。如果聚宽环境没有预置 `math`,买入触发时会报错。
### 低风险
1. 存在未使用变量:`g.bucket`、`g.summit`、`data_today` 等。
2. 存在未调度函数:`print_trade_info`。
3. 日志较少,无法解释每只股票为什么买入、为什么过滤、为什么未成交。
4. 函数和注释有不一致之处,例如“过滤次新股”定义了但未执行。
## 9. 回测可信度检查
如果要判断这个策略是否值得继续研究,建议至少做以下验证:
1. 查看分年度收益,不要只看总收益或标题宣传。
2. 查看最大回撤、回撤持续时间、夏普、卡玛、波动率。
3. 查看换手率、交易次数、平均持仓天数和手续费占收益比例。
4. 查看每天 9:26 买入与 13:06 卖出的订单日志,确认 T+1 限制是否正确生效。
5. 查看未成交订单,尤其是涨停买不进、跌停卖不出、停牌无法交易。
6. 做滑点敏感性测试:0、0.1%、0.3%、0.5% 滑点下收益是否仍然稳定。
7. 做容量测试:单票成交金额、成交量占比、买入限价成交概率。
8. 把所有日线 `history` 信号改为显式使用 `context.previous_date` 后复测,观察收益是否明显衰减。
9. 单独测试 `g.choice`、`g.amount`、`precent`、`0.065` 等参数的敏感性,避免结果来自参数反推。
10. 做样本外和滚动窗口测试,尤其覆盖 2021 至 2025 年不同市场环境。
## 10. 可改进方向
1. 在 `initialize` 中增加 `set_option('avoid_future_data', True)`。
2. 将 `get_fundamentals` 显式绑定到 `context.previous_date` 或可确认的信号日期。
3. 将日线 `history` 调用改为明确不包含当日未完成数据。
4. 明确区分昨日持仓和今日新买持仓,避免 T+1 逻辑混淆。
5. 调用或删除 `filter_new_stock`,并明确是否排除创业板。
6. 买入前检查 `int_nums > 0`。
7. 显式 `import math`,如果使用 `datetime.timedelta` 则显式 `import datetime`。
8. 增加订单状态和未成交日志。
9. 对小市值策略增加成交额和流动性过滤。
10. 用更保守的滑点和冲击成本重新回测。
## 11. 需要确认的问题
1. 聚宽分钟回测中,`9:15` 的 `current_data[s].day_open` 是否已经可用,是否等于最终开盘价。
2. `9:26` 调用 `history(..., '1d', ...)` 是否包含当日未完成日线。
3. 原回测是否严格执行 A 股 T+1。
4. 原回测资金规模是多少,是否考虑小市值容量。
5. 原收益是否集中在少数年份、少数股票或少数参数组合。
## 12. 最小结论
这个策略值得学习的地方是:它把小市值暴露、低开反转、涨停不卖和盘中止盈组合成了一个非常短线的交易流程。
但它最需要警惕的地方也很明确:信号时点、T+1、滑点、流动性和未成交处理。只要其中任一项在回测中处理得过于理想,标题中的高收益都可能被大幅高估。
15天前