问题描述
解决方案
这是一个非常经典的**定期等权重再平衡(Equal Weight Rebalancing)**策略。
策略逻辑分析
- 选股(Universe):在
initialize中设定你想要持有的股票列表。 - 择时(Timing):我们需要判断“每个月调整一次”的时间点。通常的做法是在每个月的第一个交易日进行调仓。
- 判断方法:获取当前日期和上一个交易日的日期,如果两者的月份不同,说明今天是一个新月份的开始。
- 交易(Trading):
- 计算账户总资产(现金 + 持仓市值)。
- 计算每只股票的目标持仓市值 = 总资产 / 股票数量。
- 使用
order_target_value函数,将每只股票的持仓调整到目标市值。
策略代码实现
以下是完整的 PTrade 策略代码。为了保证交易成功率,代码中增加了“先卖后买”的逻辑,防止因可用资金不足导致买入失败。
def initialize(context):
"""
初始化函数,设置股票池和定时任务
"""
# 1. 设置我们要持有的股票列表(示例为几只蓝筹股,可根据需求修改)
g.security = ['600519.SS', '000858.SZ', '601318.SS', '600036.SS']
# 2. 设置股票池
set_universe(g.security)
# 3. 设置手续费(可选,这里设为万分之三)
set_commission(commission_ratio=0.0003, min_commission=5.0, type="STOCK")
# 4. 每天上午 10:00 检查是否需要调仓
# 选择 10:00 是为了避开开盘剧烈波动,也可以设为 '14:50' 尾盘调仓
run_daily(context, monthly_rebalance_check, time='10:00')
def monthly_rebalance_check(context):
"""
每日检查函数:判断今天是否是本月第一个交易日
"""
# 获取当前回测/交易日期的 date 对象
current_date = context.blotter.current_dt.date()
# 获取上一个交易日的 date 对象
# get_trading_day(-1) 返回的是 datetime.date 对象
prev_trading_day = get_trading_day(-1)
# 如果当前日期的月份 与 上一交易日的月份 不同,说明跨月了,今天是本月第一天
if current_date.month != prev_trading_day.month:
log.info("检测到月份变化,开始执行月度等权重调仓...")
rebalance(context)
def rebalance(context):
"""
调仓核心逻辑:将所有股票调整为等市值
"""
# 获取股票数量
stock_count = len(g.security)
if stock_count == 0:
return
# 获取账户总资产 (现金 + 持仓市值)
total_value = context.portfolio.portfolio_value
# 计算每只股票的目标市值
target_value = total_value / stock_count
log.info("当前账户总资产: %.2f, 单只股票目标市值: %.2f" % (total_value, target_value))
# --- 交易执行部分 ---
# 为了防止资金不足,建议分两步走:先卖出超出比例的,再买入不足比例的
# 第一轮:卖出操作
# 遍历股票池,如果当前持仓市值 > 目标市值,则卖出差额
for stock in g.security:
position = get_position(stock)
# 计算当前该股持仓市值 (价格 * 数量)
current_stock_value = position.last_sale_price * position.amount
# 如果当前持仓过重,先卖出
if current_stock_value > target_value:
order_target_value(stock, target_value)
# 第二轮:买入操作
# 遍历股票池,如果当前持仓市值 < 目标市值,则买入
for stock in g.security:
position = get_position(stock)
current_stock_value = position.last_sale_price * position.amount
# 如果当前持仓不足(或没持仓),买入
if current_stock_value < target_value:
order_target_value(stock, target_value)
def handle_data(context, data):
"""
盘中运行函数,本策略主要逻辑在 run_daily 中,此处留空即可
"""
pass
代码关键点解析
-
run_daily(..., time='10:00'):- 我们使用定时任务来触发检查,而不是在
handle_data里每分钟都检查。这样效率更高,且逻辑更清晰。 - 时间设为
10:00是为了等待开盘行情稳定。如果你希望以收盘价为准,可以改为14:50。
- 我们使用定时任务来触发检查,而不是在
-
月份判断逻辑:
current_date.month != prev_trading_day.month:这是判断“月初”最简单有效的方法。只要今天和上一个交易日不在同一个月,今天必然是该月的第一天。
-
order_target_value(stock, target_value):- 这是 PTrade 非常强大的一个函数。你不需要计算要买卖多少股,只需要告诉它“我希望这只股票最终持有多少钱”,系统会自动计算差额并下单。
-
先卖后买:
- 在
rebalance函数中,我写了两个循环。第一个循环先处理需要减仓的股票,释放出现金;第二个循环处理需要加仓的股票。这样可以避免因为满仓状态下,先发出买单导致“资金不足”的废单。
- 在
常见问题 (Q&A)
Q1: 如果我想每季度调整一次怎么办?
A: 可以修改 monthly_rebalance_check 中的判断逻辑。除了判断月份不同外,还可以判断当前月份是否是季度的第一个月(1月, 4月, 7月, 10月)。
例如:
if current_date.month != prev_trading_day.month and current_date.month in [1, 4, 7, 10]:
rebalance(context)
Q2: 为什么不直接在 handle_data 里写逻辑?
A: handle_data 的运行频率取决于回测设置(分钟级或日级)。如果在分钟级回测中直接写日期判断,会导致该月第一天的每一分钟都触发调仓,造成重复交易和巨额手续费。使用 run_daily 可以确保每天只运行一次检查。
Q3: order_target_value 会自动处理整手(100股)吗?
A: 是的。PTrade 的交易函数会自动向下取整到 100 股(股票交易规则)。因此,实际持仓市值可能会略小于计算出的 target_value,剩下的零头资金会保留在现金账户中。
Q4: 如果某只股票停牌了怎么办?
A: 如果股票停牌,order_target_value 下单会失败(或者无法成交)。策略中的 try...except 机制(虽然示例代码为了简洁未加,但 PTrade 内部有容错)通常会跳过该股票。在下个月调仓时,如果复牌了,会再次尝试调整。