问题描述
解决方案
这是一个基于 PTrade 框架的投资组合季度再平衡策略。
策略逻辑说明
- 目标设定:在
initialize中设定一个目标股票池及其对应的目标权重(例如:5只股票,每只20%)。 - 调仓频率:策略设定为每季度(1月、4月、7月、10月)的第一个交易日进行调仓。
- 资金分配:根据当前账户的总资产(
portfolio_value)乘以目标权重,计算每只股票的目标持仓市值。 - 交易执行:
- 使用
order_target_value函数,该函数会自动计算买卖数量,将持仓调整至目标金额。 - 优化逻辑:为了防止资金不足,策略会先卖出(持仓过重的股票),释放资金后再买入(持仓过轻的股票)。
- 使用
策略代码
def initialize(context):
"""
初始化函数,设置策略参数
"""
# 1. 设定目标投资组合:股票代码 -> 目标权重
# 注意:权重之和建议不超过 1.0 (100%),预留少量现金应对滑点和手续费
g.target_portfolio = {
'600519.SS': 0.20, # 贵州茅台
'000858.SZ': 0.20, # 五粮液
'600036.SS': 0.20, # 招商银行
'000001.SZ': 0.20, # 平安银行
'601318.SS': 0.19 # 中国平安 (预留1%现金)
}
# 2. 设置股票池
g.security_list = list(g.target_portfolio.keys())
set_universe(g.security_list)
# 3. 设定调仓月份 (1月, 4月, 7月, 10月)
g.rebalance_months = [1, 4, 7, 10]
# 4. 记录上一次调仓的月份,防止同月重复调仓
g.last_rebalance_month = 0
# 5. 设置手续费 (可选,模拟真实交易成本)
set_commission(commission_ratio=0.0003, min_commission=5.0, type='STOCK')
def handle_data(context, data):
"""
盘中运行函数,每日/每分钟调用
"""
# 获取当前日期
current_dt = context.blotter.current_dt
current_month = current_dt.month
# 判断是否满足调仓条件:
# 1. 当前月份在设定的调仓月份列表中
# 2. 当前月份尚未进行过调仓
if current_month in g.rebalance_months and current_month != g.last_rebalance_month:
log.info("达到季度调仓时间点: %s-%s,开始执行再平衡..." % (current_dt.year, current_month))
# 执行再平衡逻辑
rebalance_portfolio(context, data)
# 更新标记,本月不再调仓
g.last_rebalance_month = current_month
def rebalance_portfolio(context, data):
"""
执行投资组合再平衡的核心逻辑
"""
# 获取当前账户总资产 (现金 + 持仓市值)
total_value = context.portfolio.portfolio_value
# 获取当前所有持仓
positions = context.portfolio.positions
# 分类待买入和待卖出的股票,以便先卖后买
sell_list = []
buy_list = []
# 遍历目标组合中的每只股票
for stock, weight in g.target_portfolio.items():
# 计算该股票的目标持仓市值
target_value = total_value * weight
# 获取当前该股票的持仓市值
current_value = 0.0
if stock in positions:
current_value = positions[stock].price * positions[stock].amount
# 判断是买入还是卖出
if target_value < current_value:
# 如果目标市值 < 当前市值,需要卖出
sell_list.append((stock, target_value))
elif target_value > current_value:
# 如果目标市值 > 当前市值,需要买入
buy_list.append((stock, target_value))
# --- 第一步:执行卖出操作 ---
# 先卖出权重过高的股票,释放资金,避免买入时资金不足
for stock, target_val in sell_list:
# 检查股票是否停牌或数据缺失
if stock in data and not math.isnan(data[stock]['close']):
order_target_value(stock, target_val)
log.info("卖出调仓: %s, 目标市值: %.2f" % (stock, target_val))
# --- 第二步:执行买入操作 ---
for stock, target_val in buy_list:
# 检查股票是否停牌或数据缺失
if stock in data and not math.isnan(data[stock]['close']):
order_target_value(stock, target_val)
log.info("买入调仓: %s, 目标市值: %.2f" % (stock, target_val))
# --- 第三步:清理不在目标组合中的持仓 ---
# 如果账户里有不在 g.target_portfolio 里的股票,将其全部清仓
for stock in list(positions.keys()):
if stock not in g.target_portfolio and positions[stock].amount > 0:
if stock in data and not math.isnan(data[stock]['close']):
order_target(stock, 0)
log.info("清仓非目标股票: %s" % stock)
# 引入 math 库用于判断 NaN
import math
代码关键点解析
-
g.target_portfolio:- 这是策略的核心配置。你可以随意修改里面的股票代码和权重。
- 注意:权重的总和最好略小于 1.0(例如 0.99),因为交易需要手续费,且股价波动可能导致计算出的股数所需的资金微略超过可用现金。
-
g.last_rebalance_month:- 这是一个状态变量。因为
handle_data每天都会运行,我们需要确保在 1 月份只在第 1 个交易日调仓,而不是 1 月份的每一天都调仓。
- 这是一个状态变量。因为
-
先卖后买机制:
- 在
rebalance_portfolio函数中,我们将操作分为sell_list和buy_list。 - 先执行
order_target_value减少持仓(卖出),资金回到账户后,再执行增加持仓(买入)。这有效避免了“可用资金不足”的报错。
- 在
-
order_target_value(stock, value):- 这是 PTrade 非常方便的一个 API。你不需要计算要买多少股,只需要告诉它“我希望这只股票最终持有多少钱”,系统会自动计算差额进行买卖。
-
异常处理:
- 代码中加入了
if stock in data and not math.isnan(...)的判断。这是为了防止股票停牌或退市导致没有行情数据时,策略报错停止。
- 代码中加入了
如何使用
- 将代码复制到 PTrade 的策略编辑器中。
- 修改
initialize中的g.target_portfolio为你想要的股票和权重。 - 设置回测时间段(例如过去3年),资金(例如100万),频率选择“日线”。
- 运行回测即可看到每季度自动调仓的效果。
Q&A
Q: 如何将调仓频率改为每月一次?
A: 将 initialize 中的 g.rebalance_months 修改为 list(range(1, 13)),即 [1, 2, 3, ..., 12]。
Q: 为什么我的回测中有些股票没有成交?
A: 可能是因为该股票在调仓日停牌,或者处于涨跌停状态无法成交。代码中的 check_limit 或简单的 data 检查可以避免报错,但无法强制成交。
Q: order_target_value 会自动处理手数取整吗?
A: 是的,PTrade 的交易函数会自动向下取整到 100 股(1手)的整数倍。不足 100 股的部分资金会保留为现金。