问题描述
解决方案
这是一个非常经典的事件驱动策略。在聚宽(JoinQuant)平台上,我们可以利用 finance.run_query 接口查询 高管增持(STK_MANAGEMENT_HOLD_CHANGE) 数据来实现。
策略设计思路
- 数据来源:使用
finance.STK_MANAGEMENT_HOLD_CHANGE表。 - 选股逻辑:
- 在每天开盘前,查询 前一个交易日 公告的增持信息。
- 筛选条件:
change_number > 0(变动股数大于0,即增持)。
- 交易逻辑:
- 买入:如果有股票出现高管增持信号,且当前没有持仓或持仓时间已到,则买入。
- 卖出:由于高管增持通常被视为中长期利好,我们设定一个 持仓周期(例如 20 个交易日)。买入后持有满 20 天,如果期间没有新的增持信号,则卖出。
- 资金管理:将资金等分,或者根据信号数量动态分配。
策略代码实现
以下是完整的策略代码,您可以直接复制到聚宽的回测环境中运行。
# -*- coding: utf-8 -*-
from jqdata import *
import pandas as pd
def initialize(context):
"""
初始化函数,设定基准、手续费、全局变量等
"""
# 设定沪深300作为基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
# 股票类每笔交易时的手续费是:买入时佣金万分之三,卖出时佣金万分之三加千分之一印花税, 每笔交易佣金最低扣5块钱
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
# --- 策略参数设置 ---
g.hold_period = 20 # 设定持仓天数,高管增持通常为中长线逻辑
g.max_stock_count = 10 # 最大持仓股票数量,防止资金过于分散
# 用于记录每只股票的持仓天数:{stock_code: days_held}
g.hold_days_counter = {}
def before_market_open(context):
"""
开盘前运行:查询高管增持数据
"""
# 获取前一个交易日
prev_date = context.previous_date
# 查询 finance.STK_MANAGEMENT_HOLD_CHANGE 表
# 筛选条件:
# 1. pub_date (公告日期) 为前一交易日
# 2. change_number > 0 (增持)
q = query(
finance.STK_MANAGEMENT_HOLD_CHANGE.code,
finance.STK_MANAGEMENT_HOLD_CHANGE.change_number,
finance.STK_MANAGEMENT_HOLD_CHANGE.pub_date
).filter(
finance.STK_MANAGEMENT_HOLD_CHANGE.pub_date == prev_date,
finance.STK_MANAGEMENT_HOLD_CHANGE.change_number > 0
)
df = finance.run_query(q)
# 今日计划买入的股票列表
g.buy_list = []
if not df.empty:
# 去重,因为同一天可能有多个高管增持同一只股票
g.buy_list = list(set(df['code'].tolist()))
log.info(f"检测到高管增持信号的股票: {g.buy_list}")
else:
log.info("昨日无高管增持公告")
def market_open(context):
"""
盘中运行:执行买卖操作
"""
# 1. 卖出逻辑:检查持仓天数
# 获取当前所有持仓
current_positions = list(context.portfolio.positions.keys())
for stock in current_positions:
# 如果股票在今天的买入名单里(说明又有高管增持了),重置持仓天数,继续持有
if stock in g.buy_list:
g.hold_days_counter[stock] = 0
log.info(f"{stock} 再次出现增持信号,重置持仓时间")
continue
# 更新持仓天数
if stock in g.hold_days_counter:
g.hold_days_counter[stock] += 1
else:
g.hold_days_counter[stock] = 1
# 如果持仓超过设定周期,卖出
if g.hold_days_counter[stock] >= g.hold_period:
order_target_value(stock, 0)
log.info(f"{stock} 持仓满 {g.hold_period} 天,执行止盈/止损卖出")
del g.hold_days_counter[stock]
# 2. 买入逻辑
# 过滤掉停牌、退市或ST的股票(这里简单过滤停牌)
current_data = get_current_data()
# 计算当前可用资金分配
# 假设我们要维持 g.max_stock_count 只股票,计算每只股票应该分配的资金
# 注意:这里采用简单的资金管理,有空位才买
current_hold_count = len(context.portfolio.positions)
if current_hold_count < g.max_stock_count:
# 剩余可买的股票数量
slots_available = g.max_stock_count - current_hold_count
# 待买入列表(剔除已经在持仓中的)
targets = [s for s in g.buy_list if s not in context.portfolio.positions]
# 如果待买入的比空位多,截取前面几个
targets = targets[:slots_available]
if len(targets) > 0:
# 将剩余可用资金平均分配给要买的股票
cash_per_stock = context.portfolio.available_cash / len(targets)
for stock in targets:
if current_data[stock].paused:
log.info(f"{stock} 停牌,跳过")
continue
# 买入
order_value(stock, cash_per_stock)
# 初始化持仓计数器
g.hold_days_counter[stock] = 0
log.info(f"买入高管增持股票: {stock}")
def after_market_close(context):
pass
代码关键点解析
-
数据查询 (
finance.run_query):- 我们查询的是
finance.STK_MANAGEMENT_HOLD_CHANGE表。 pub_date == context.previous_date: 这一点非常重要。为了避免“未来函数”,我们只能在今天开盘前,获取昨天晚上或者昨天白天公告的数据。change_number > 0: 确保是增持(减持为负数)。
- 我们查询的是
-
持仓控制 (
g.hold_days_counter):- 高管增持不是短线打板策略,通常需要一定的时间发酵。因此策略设定了
g.hold_period = 20(20个交易日,约一个月)。 - 使用一个字典来记录每只股票持有了多少天。
- 逻辑增强:如果一只股票在持有期间,再次发布了增持公告,我们将它的持仓计数器重置为0,重新开始计算持有期(相当于利好加强,继续持有)。
- 高管增持不是短线打板策略,通常需要一定的时间发酵。因此策略设定了
-
资金管理:
- 设置了
g.max_stock_count来限制最大持仓只数,避免资金过于分散导致收益微薄,或者资金不足导致无法买入。
- 设置了
常见问题 Q&A
Q: 为什么回测时有时候买入的股票很少?
A: 高管增持并不是每天都会发生的事件。在市场低迷期,增持公告会变多;在市场高位,增持相对较少。这是正常的事件驱动特征。
Q: 如何优化这个策略?
A:
- 增加过滤条件:目前只要增持就买。可以优化为:增持金额超过 100 万,或者增持比例超过总股本的 0.1%,以过滤掉象征性的增持。
- 结合估值:只买入增持且 PE/PB 处于历史低位的股票。
- 结合技术面:增持且股价站上 20 日均线时再买入。
Q: 为什么使用 pub_date 而不是 change_date?
A: change_date 是高管实际买入股票的日期,但普通投资者只有在上市公司发布公告(pub_date)后才知道。如果在回测中使用 change_date,就属于使用了“未来数据”,实盘中是无法实现的。必须使用 pub_date。