说明:本报告只基于代码路径、聚宽 API 调用和 A 股交易规则进行解读,不构成投资建议。文件注释中的原帖标题、作者、回测区间和样例个股只作为来源信息,不作为收益真实性结论。
## 1. 策略一句话概括
这个策略本质上是创业板/科创板方向的“昨日涨停后次日集合竞价强势延续”短线策略:盘前找昨日收盘涨停股票,9:26 读取 9:24 到 9:25:10 的 tick 数据,筛出竞价金额较大、竞价量明显放大且高开超过 5% 的股票,9:30 用组合总资产的 50% 分散买入,14:55 对未封涨停的可卖持仓执行卖出。
主要收益假设来自:昨日涨停代表短线资金强势,次日集合竞价继续高开且放量,说明资金承接较强,后续有继续冲高或连板的概率。
## 2. 执行时间线
代码没有标准 `initialize(context)`,而是使用 `after_code_changed(context)` 作为初始化入口。是否在回测启动时稳定执行,需要在聚宽环境中用日志确认。
1. `after_code_changed(context)`
- 调用 `unschedule_all()` 清空已有调度。
- 调用 `set_params()` 设置策略参数。
- 调用 `set_variables()` 设置中间变量。
- 调用 `set_backtest()` 设置基准、真实价格、防未来函数和日志。
- 设置股票交易成本:买入佣金万三,卖出佣金万三加千一印花税,最低佣金 5 元。
- 注册每日任务:
- 7:00 执行 `before_market_open`
- 9:26 执行 `call_auction`
- 9:30 执行 `market_open`
- 14:55 执行 `market_run`
2. 7:00 `before_market_open(context)`
- 读取 `context.current_dt.date()` 作为当天日期。
- 读取 `context.previous_date` 作为昨日信号日期。
- 通过 `get_current_data()` 获取当前状态。
- 构建初始股票池:
- `g.index == 'all'` 时使用 `get_all_securities(['stock']).index`
- `g.index == 'zz'` 时拼接沪深 300、中证 500、中证 1000 成分股
- 其他情况使用 `get_index_stocks(g.index, date=None)`
- 过滤停牌、ST、名称含“退”、上市不足 30 天的股票。
- 只保留代码以 `3`、`68`、`69` 开头的股票,即主要指向创业板和科创板。
- 调用 `get_up_filter_jiang(context, stocklist, lastd_date, 1, 0, 1)` 筛选昨日涨停股票,结果存入 `g.poollist`。
3. 9:26 `call_auction(context)`
- 若 `g.poollist` 为空,则当天没有买入信号。
- 对昨日涨停池逐只读取:
- 当日 `09:24:00` 到 `09:25:10` 的 tick 数据:`get_ticks`
- 昨日收盘价和最高价:`get_price(..., end_date=context.previous_date, count=1)`
- 计算竞价指标:
- `p_ratio`:竞价价格相对 9:24 初始卖一价的变化
- `v_ratio`:竞价量相对 9:24 初始卖一量的变化
- `money`:竞价成交金额
- `open_close`:竞价价格相对昨日收盘价
- `open_high`:竞价价格相对昨日最高价,计算了但没有参与筛选
- 筛选满足条件的股票,存入 `g.auct_list`。
4. 9:30 `market_open(context)`
- 若 `g.auct_list` 为空,则当天不买。
- 计算每只股票买入目标金额:
- `buy_cash = 0.5 * context.portfolio.total_value / len(g.auct_list)`
- 对不在当前持仓中的信号股调用 `buy_stock(context, stockcode, buy_cash)`。
- 本质是信号股等金额买入,总买入目标约为组合总资产的 50%。
5. 14:55 `market_run(context)`
- 遍历当前持仓。
- 若股票停牌则跳过。
- 若 `closeable_amount == 0` 则跳过,意味着当天新买入股票因 T+1 通常不会被卖出。
- 若当前价不等于涨停价,则调用 `sell_stock(context, stockcode, 0)` 卖到目标市值 0。
- 逻辑可概括为:可卖持仓只要没有封涨停就卖出;封涨停则继续持有。
## 3. 股票池和信号来源
### 交易标的
策略交易 A 股股票,但经过代码过滤后主要限定为:
- 创业板:代码以 `3` 开头
- 科创板:代码以 `68` 或 `69` 开头
这意味着策略集中在 20cm 涨跌幅方向,波动和流动性风险高于宽基蓝筹策略。
### 初始股票池
默认 `g.index = 'all'`,所以初始股票池来自 `get_all_securities(['stock']).index`。如果改为指数代码,则来自指数成分;如果改为 `zz`,则来自沪深 300、中证 500、中证 1000 的拼接。
需要注意:`get_all_securities(['stock'])` 和 `get_index_stocks(..., date=None)` 是否严格按历史时点返回,需要在聚宽环境中确认。代码没有显式传入 `context.previous_date`。
### 昨日涨停过滤
`get_up_filter_jiang` 使用:
```python
df_up = df_price[(df_price.close == df_price.high_limit) & (df_price.paused == 0)]
```
由于调用参数为 `check_duration=1`、`up_num=0`、`direction=1`,实际含义是:过去 1 个交易日内收盘涨停次数大于 0,也就是昨日收盘涨停。
这里与注释“盘中过10%也算”不一致。实际代码只认收盘价等于涨停价,不认盘中触及涨停后回落。
## 4. 集合竞价筛选逻辑
9:26 的筛选条件是:
```python
df_abn = df_pool[
(df_pool.money > 1e7) &
(df_pool.v_ratio > 2.5) &
(df_pool.p_ratio > 0.94) &
(df_pool.open_close > 1.05)
]
```
逐项含义:
- `money > 1e7`:竞价成交金额超过 1000 万。
- `v_ratio > 2.5`:9:25 附近竞价量相对 9:24 初始卖一量或成交量口径放大超过 2.5 倍。
- `p_ratio > 0.94`:竞价价格相对 9:24 初始卖一价没有大幅下滑。
- `open_close > 1.05`:相对昨日收盘价高开超过 5%。
没有排序逻辑,所有满足条件的股票都进入买入列表。`g.stocknum` 被设置为 0,但后续没有使用,因此不能限制每日买入数量。
## 5. 买卖和仓位管理
### 买入
`market_open` 在 9:30 执行买入:
```python
buy_cash = 0.5 * total_value / len(g.auct_list)
```
如果当天有 N 只信号股,每只目标市值为组合总资产的 `50% / N`。已有持仓中的股票不重复买入。
普通股票使用:
```python
order_target_value(stockcode, cash)
```
科创板 688 开头股票使用带限价的 `MarketOrderStyle(1.1 * last_price)`。但 689、690 等以 `68`、`69` 进入股票池的代码,在买卖函数里只有 `stockcode[0:3] == '688'` 才特殊处理,其他科创板代码可能不会套用同样限价逻辑。
### 卖出
`market_run` 在 14:55 执行:
```python
if current_data[stockcode].last_price != current_data[stockcode].high_limit:
sell_stock(context, stockcode, 0)
```
这不是止损止盈逻辑,而是“非涨停即出”。如果持仓当日仍封涨停,则继续持有;如果没有封涨停且可卖,则清仓。
由于 A 股股票通常 T+1,当天 9:30 买入的股票在 14:55 `closeable_amount` 通常为 0,所以当天炸板并不会被卖出。这个细节会显著影响******风险。
## 6. 风控和交易约束
代码体现的约束:
- 过滤停牌股票。
- 过滤 ST 股票。
- 过滤名称含“退”的股票。
- 过滤上市不足 30 天的新股。
- 只交易创业板/科创板方向。
- 设置佣金、印花税和最低佣金。
- 14:55 对非涨停且可卖持仓清仓。
- 对 688 开头股票下单时使用带价格限制的 `MarketOrderStyle`。
代码未体现或不足:
- 没有设置滑点。
- 没有成交量容量约束。
- 没有处理涨停买不进、跌停卖不出、一字板排队失败的显式逻辑。
- 没有最大持仓数量限制。
- 没有单票上限之外的风险预算,单票目标权重由当天信号数决定。
- 没有指数择时、市场风险开关或极端行情空仓机制。
- 没有记录未成交订单和成交失败原因。
## 7. 关键参数
- `g.index = 'all'`:决定初始股票池。默认全市场,再过滤创业板/科创板。
- `g.begin_times = ' 09:24:00'`:竞价 tick 起始时间。
- `g.end_times = ' 09:25:10'`:竞价 tick 结束时间。
- `check_duration = 1`:只检查昨日是否涨停。
- `up_num = 0`、`direction = 1`:筛出昨日涨停次数大于 0 的股票。
- `money > 1e7`:竞价成交金额阈值。
- `v_ratio > 2.5`:竞价量放大阈值。
- `p_ratio > 0.94`:竞价价格保持阈值。
- `open_close > 1.05`:高开阈值。
- `0.5 * total_value`:当天总买入目标为组合资产 50%。
- 14:55:非涨停清仓检查时间。
这些参数会直接影响交易频率、信号数量、换手率、回撤、成交可实现性和过拟合风险。特别是 `money`、`v_ratio`、`open_close` 是典型短线竞价参数,需要做样本外和滚动验证。
## 8. 风险分级
### 高风险
1. 初始化入口风险
代码使用 `after_code_changed(context)` 注册调度,没有标准 `initialize(context)`。如果聚宽运行环境没有在策略启动时调用该函数,策略可能不会注册每日任务。需要用回测日志确认初始化确实发生。
2. T+1 与竞价追高风险
策略 9:30 买入高开超过 5% 的昨日涨停股,若当日冲高回落,当天通常无法卖出。14:55 的“非涨停即出”只对已有可卖持仓有效,对当天新仓不构成当日风控。
3. 成交假设可能高估收益
策略交易对象是涨停后次日高开异动票,容易出现快速封板、排队买不进、剧烈滑点和冲击成本。代码没有设置滑点,也没有显式处理未成交订单,回测可能高估可成交性。
4. 竞价 tick 口径依赖平台实现
`get_ticks(stockcode, end, begin, None, ...)` 的参数顺序和返回数据时点需要在聚宽环境中验证。若 tick 数据包含 9:25:10 后才稳定可见的信息,或回测中使用历史完整竞价数据生成 9:26 决策,******会有时点偏差。
### 中风险
1. 股票池日期未显式历史化
默认全市场使用 `get_all_securities(['stock'])`,指数池使用 `get_index_stocks(..., date=None)`。代码没有传入 `context.previous_date`。需要确认聚宽回测中这些 API 是否按回测日期返回,避免股票池幸存者偏差或成分股时点偏差。
2. tick 数据缺少边界处理
代码直接访问 `df_ticks['current'].values[-1]`、`df_ticks['a1_p'].values[0]`、`df_ticks['a1_v'].values[0]`。如果 tick 为空、字段缺失、初始卖一量为 0,可能报错或产生无意义指标。
3. 沪深/科创口径不一致
当 `current == 0` 时使用 `a1_p`、`a1_v` 计算;否则使用 `current`、`volume` 计算。不同市场或不同 tick 状态下指标口径不一致,可能导致筛选结果不稳定。
4. 买入数量不受控
`g.stocknum` 没有参与买入逻辑。极端行情中信号数过多会导致持仓分散、单票金额降低;信号数很少时单票仓位可能很高。
5. 科创板下单特殊处理不完整
股票池允许 `68`、`69` 开头,但买卖函数只对 `688` 开头使用特殊限价处理。需要确认 689、690 等代码在聚宽交易 API 中是否需要同样处理。
### 低风险
1. 未使用变量和依赖较多
`LinearRegression`、`get_factor_values`、`np`、`g.stocknum`、`g.sell_list`、`open_high`、`b1_v` 等没有实际参与策略主逻辑。
2. 注释与实际代码不一致
注释称“盘中过10%也算”,实际只筛选收盘涨停。
3. `df_pool.append` 可迁移性差
`DataFrame.append` 在新版本 pandas 中已废弃。聚宽旧环境可能可用,但迁移到新环境会出问题。
4. 日志不足
代码会打印候选池和买卖动作,但没有记录每只股票被过滤或入选的具体原因,也没有记录订单成交结果。
## 9. 未来函数和数据泄露检查
代码开启了:
```python
set_option("avoid_future_data", True)
```
但这不能替代人工时点检查。
相对安全的部分:
- 昨日涨停筛选使用 `context.previous_date` 作为 `get_price` 的 `end_date`,没有直接读取当日收盘数据。
- 9:26 使用 9:24 到 9:25:10 的 tick 数据,理论上是集合竞价结束后可见的信息;但需要验证聚宽回测和******环境的 tick 可见性。
需要核实的部分:
- `get_all_securities(['stock'])` 没有显式日期参数,历史回测中是否返回当时已上市股票,需要确认。
- `get_index_stocks(..., date=None)` 若启用指数池,是否使用当前成分而非历史成分,需要确认。
- `get_current_data()` 在 7:00 读取 `paused`、`is_st`、名称和涨跌停价等字段,具体是否为当时可得状态,需要结合聚宽 API 语义验证。
- 9:26 读取到 `09:25:10` 的 tick 后在 9:30 买入,逻辑上接近******可执行,但集合竞价结果、开盘价格和排队成交仍需要用逐笔日志核实。
没有看到机器学习训练、全样本优化或直接使用未来收益标签的代码路径。
## 10. 回测可信度和******可实现性
该策略属于高换手、短周期、竞价交易策略,回测可信度高度依赖成交模型。
重点问题:
- 9:30 买入昨日涨停且高开 5% 以上的股票,******可能滑点较大。
- 强势票可能开盘快速封板,回测成交但******排队买不进。
- 弱转强失败时,当日可能大幅回落,但 T+1 限制下不能当天止损。
- 14:55 卖出可能遇到跌停、流动性不足或大额冲击。
- 策略集中在创业板/科创板,单日波动和尾部风险较高。
- 若回测资金规模较大,需要检查单票成交额占比,竞价金额超过 1000 万不代表能承载大资金无冲击成交。
因此,不能只看年化收益、夏普或最大回撤。该策略必须结合订单日志、未成交记录、滑点敏感性和资金容量分析。
## 11. 可改进和可验证项
建议优先验证:
1. 初始化验证
在聚宽回测日志中确认 `after_code_changed` 是否在回测启动时执行。如果没有,应改为标准 `initialize(context)` 或在 `initialize` 中调用现有初始化逻辑。
2. 成交真实性验证
输出每笔订单的委托价、成交价、成交数量、未成交数量和成交时间,重点看 9:30 买入是否大量理想成交。
3. T+1 风险验证
统计信号股买入当日从开盘到收盘的回撤、次日开盘收益和次日可卖价格,单独评估“当天炸板但不能卖”的损失。
4. 参数敏感性测试
对 `money`、`v_ratio`、`p_ratio`、`open_close` 做网格或滚动窗口测试,观察收益是否集中在少数组合。
5. 分年度和分行情阶段
至少拆分牛市、熊市、震荡市、极端行情,检查策略收益是否只集中在短线情绪强的阶段。
6. 容量和冲击成本
统计每笔目标成交金额占开盘 1 分钟、5 分钟成交额的比例。对 50 万、100 万、500 万、1000 万等资金规模做容量模拟。
7. 股票池时点验证
将 `get_all_securities`、`get_index_stocks` 的日期显式绑定到 `context.previous_date` 或对应历史日期,并比较信号差异。
8. 边界处理验证
增加对空 tick、零卖一量、停牌、涨跌停买卖失败、NaN 的日志和跳过逻辑。
6天前