问题描述
解决方案
这是一个非常实用的量化需求。在PTrade中实现一个完整的打新(IPO)及中签后自动卖出策略,主要包含两个核心模块:
- 自动申购模块:利用
ipo_stocks_order接口在交易时间自动一键申购。 - 自动卖出模块:监控持仓中的新股,一旦满足卖出条件(通常是“开板”,即当前价格低于涨停价)就自动清仓。
策略设计思路
- 申购时间:通常建议在上午10:00至10:30之间进行申购,避开早盘高峰。
- 新股识别:通过
get_stock_info获取上市日期,判断持仓是否为近期上市的新股(例如上市60天内)。 - 卖出条件(开板卖出):
- 获取股票的实时快照数据 (
get_snapshot)。 - 比较
last_px(最新价) 和up_px(涨停价)。 - 如果
最新价 < 涨停价,说明涨停板打开(开板),立即市价卖出。
- 获取股票的实时快照数据 (
策略代码实现
以下是一个完整的、可直接在PTrade中运行的策略代码。
import datetime
def initialize(context):
"""
策略初始化函数
"""
# 设定新股定义的阈值,上市超过多少天就不再视为新股(防止误卖长期持仓)
# 这里设定为60天,即只监控上市60天内的股票
g.new_stock_days = 60
# 设定每天自动申购的时间,这里设置为上午 10:15
run_daily(context, auto_ipo_subscription, time='10:15')
# 开启日志打印
log.info("打新及自动卖出策略已启动")
def handle_data(context, data):
"""
盘中运行函数,用于监控持仓并执行卖出逻辑
"""
# 仅在交易模式下运行卖出逻辑,回测模式下打新很难模拟
if not is_trade():
return
# 执行新股监控与卖出逻辑
check_and_sell_new_stocks(context)
def auto_ipo_subscription(context):
"""
自动申购新股函数
"""
# 仅在交易模式下有效
if not is_trade():
log.info("当前为回测模式,跳过打新申购")
return
log.info("开始执行新股一键申购...")
# ipo_stocks_order 接口用于一键申购当日全部新股
# market_type 不传参默认申购全部市场
res = ipo_stocks_order()
if res:
log.info("新股申购指令已发送,返回结果: %s" % str(res))
else:
log.info("今日无新股申购或申购指令发送失败")
def check_and_sell_new_stocks(context):
"""
检查持仓,如果是新股且开板(价格低于涨停价),则卖出
"""
# 获取当前所有持仓
positions = context.portfolio.positions
if not positions:
return
# 获取持仓股票列表
holding_list = list(positions.keys())
# 获取持仓股票的基础信息(主要为了获取上市日期)
# 注意:get_stock_info 返回的是嵌套字典
stock_infos = get_stock_info(holding_list, field=['listed_date', 'stock_name'])
# 获取当前日期
current_date = context.blotter.current_dt.date()
for stock in holding_list:
# 获取该股票的持仓对象
position = positions[stock]
# 如果可用仓位为0,跳过(可能是刚买入或已冻结)
if position.enable_amount == 0:
continue
# 获取股票信息
info = stock_infos.get(stock)
if not info:
continue
listed_date_str = info.get('listed_date')
stock_name = info.get('stock_name')
# 如果没有上市日期信息,跳过
if not listed_date_str or listed_date_str == '0':
continue
# 计算上市天数
try:
# 将字符串日期转换为date对象
listed_date = datetime.datetime.strptime(listed_date_str, "%Y-%m-%d").date()
days_since_listing = (current_date - listed_date).days
except Exception as e:
log.error("日期解析错误 %s: %s" % (stock, e))
continue
# 判断是否为“新股”(在设定的天数范围内)
if 0 <= days_since_listing <= g.new_stock_days:
# 获取实时快照数据
snapshot = get_snapshot(stock)
if not snapshot:
continue
stock_snap = snapshot.get(stock)
if not stock_snap:
continue
last_px = stock_snap.get('last_px', 0) # 最新价
up_px = stock_snap.get('up_px', 0) # 涨停价
# 核心卖出逻辑:
# 1. 必须有有效价格 (last_px > 0)
# 2. 最新价 < 涨停价 (说明板没封住,或者已经开板)
# 3. 排除停牌状态 (last_px > 0 通常隐含了非停牌,但需注意)
if last_px > 0 and last_px < up_px:
log.info("新股开板卖出触发: %s (%s), 上市天数: %d, 最新价: %.2f, 涨停价: %.2f" % (
stock, stock_name, days_since_listing, last_px, up_px
))
# 市价清仓卖出
# 注意:科创板/创业板建议使用 order_market,主板可以使用 order_target 配合限价或市价
# 这里为了通用性,使用 order_target 卖出到 0
order_target(stock, 0)
代码详解
-
initialize(context):- 设置了一个全局变量
g.new_stock_days = 60。这是为了防止策略误卖你账户里原本持有的老股票。只有上市时间在60天以内的股票才会被监控。 - 使用
run_daily设定每天 10:15 执行申购函数。
- 设置了一个全局变量
-
auto_ipo_subscription(context):- 调用 PTrade 专用的
ipo_stocks_order()函数。这个函数会自动查询当天所有可申购的新股并按顶格或最大可申购数量下单。 - 加入了
is_trade()判断,防止在回测中报错(虽然回测打新意义不大,但保持代码健壮性)。
- 调用 PTrade 专用的
-
handle_data(context, data):- 这是策略的心跳,交易时间内会高频运行(取决于你的运行频率设置,建议设为分钟级或Tick级)。
- 它调用
check_and_sell_new_stocks来实时监控价格。
-
check_and_sell_new_stocks(context)(核心逻辑):- 筛选持仓:遍历当前账户所有持仓。
- 判断新股:利用
get_stock_info获取股票的listed_date(上市日期),计算它上市了多少天。如果超过60天,则认为是老仓位,忽略之。 - 判断开板:
- 使用
get_snapshot(stock)获取高频快照数据。 - 对比
last_px(最新成交价)和up_px(当日涨停价)。 - 条件:只要
最新价 < 涨停价,就意味着涨停板被打开了(或者根本没封住)。
- 使用
- 执行卖出:调用
order_target(stock, 0)将该股票仓位调整为0,即全部卖出。
部署建议
- 运行频率:建议将策略的运行频率设置为 “分钟级别”。虽然Tick级别反应更快,但分钟级别对于打新卖出通常已经足够,且能减少系统资源消耗。
- 交易权限:请确保你的PTrade终端已登录交易账号,并且券商已开通了API交易权限。
- 手动干预:虽然策略是自动的,但建议在新股上市首日(尤其是无涨跌幅限制的创业板、科创板新股)关注一下运行情况。上述逻辑中
last_px < up_px在无涨跌幅限制的首日(up_px通常很高或为0)可能需要特殊处理,但在PTrade中,通常首日up_px会返回一个基于规则的上限,或者你可以简单地修改逻辑为“只要有持仓且是新股,上市首日即卖出”。
Q&A
Q: 这个策略会卖出我账户里原本持有的老股票吗?
A: 不会。代码中包含 if 0 <= days_since_listing <= g.new_stock_days: 的判断逻辑。只有上市日期距离今天在60天以内的股票才会被判定为新股并执行卖出逻辑。你可以根据需要修改 g.new_stock_days 的值。
Q: 创业板和科创板前5天没有涨跌停限制,这个策略有效吗?
A: 对于无涨跌幅限制的股票,up_px(涨停价)的数据可能具有误导性或特定含义。如果你希望在上市首日直接卖出(无论价格如何),可以将判断逻辑修改为:只要 days_since_listing == 0(上市首日),就直接卖出。目前的逻辑更适合主板连板新股的“开板即卖”策略。
Q: 为什么要在10:15申购?
A: 这是一个经验值。通常早盘9:30-10:00系统负载较高,10:15相对稳定,且肯定已经开盘,避免了非交易时间报单的废单风险。