问题描述
解决方案
这是一个非常经典的量化交易场景。在PTrade中实现多资产定期调仓策略(Rebalancing Strategy),核心逻辑分为三步:
- 定义目标:在
initialize中设置资产列表和目标权重。 - 判断时间:在
handle_data或before_trading_start中判断是否到达调仓日(例如每月第一个交易日)。 - 执行调仓:计算当前总资产,根据目标权重计算每只股票的目标市值,先卖后买以确保资金充足。
以下是完整的策略代码及详细说明。
策略逻辑说明
- 资产配置:我们选取了三只具有代表性的股票(示例:贵州茅台、招商银行、中国平安)作为投资组合。
- 调仓频率:策略设定为按月调仓。逻辑是通过比较“当前交易日”和“上一交易日”的月份是否发生变化来判断是否为月初。
- 资金分配:使用
context.portfolio.portfolio_value获取当前账户总资产(现金+持仓市值),乘以权重得到每只股票的目标持仓市值。 - 交易执行:使用
order_target_value函数。为了防止资金不足,代码中包含了一个排序逻辑,优先执行卖出操作(降低仓位),释放资金后再执行买入操作。
PTrade 策略代码
def initialize(context):
"""
初始化函数,设置股票池、权重和运行参数
"""
# 设定要操作的股票及其目标权重 (权重之和最好为1.0)
# 示例:茅台40%,招行30%,平安30%
g.target_portfolio = {
'600519.SS': 0.4,
'600036.SS': 0.3,
'601318.SS': 0.3
}
# 提取股票列表用于设置股票池
g.security_list = list(g.target_portfolio.keys())
# 设置股票池
set_universe(g.security_list)
# 设置基准(可选,这里设为沪深300)
set_benchmark('000300.SS')
# 设定手续费(可选,这里设为万分之三)
set_commission(commission_ratio=0.0003, min_commission=5.0, type="STOCK")
log.info("策略初始化完成,投资组合目标: %s" % g.target_portfolio)
def handle_data(context, data):
"""
盘中运行函数,每日/每分钟调用
"""
# 获取当前日期和上一交易日日期
current_date = context.blotter.current_dt.date()
previous_date = context.previous_date
# 判断是否为月初(调仓日)
# 逻辑:如果当前月份与上一交易日月份不同,说明今天是本月第一个交易日
if current_date.month != previous_date.month:
log.info("检测到月份变化,开始执行定期调仓,日期: %s" % current_date)
rebalance_portfolio(context, data)
def rebalance_portfolio(context, data):
"""
执行调仓逻辑的核心函数
"""
# 获取当前账户的总资产价值(包含现金和持仓市值)
total_value = context.portfolio.portfolio_value
# 临时列表,用于区分买入和卖出指令
sell_orders = []
buy_orders = []
# 遍历目标组合中的每一只股票
for stock, weight in g.target_portfolio.items():
# 获取该股票的当前价格
# 注意:如果股票停牌或无数据,data[stock]可能无法获取价格,需做保护
if stock not in data or math.isnan(data[stock].price):
log.warning("股票 %s 无行情数据或停牌,跳过本次调仓" % stock)
continue
current_price = data[stock].price
# 计算该股票的目标持仓市值
target_value = total_value * weight
# 获取当前该股票的持仓市值
current_position = get_position(stock)
current_holding_value = current_position.amount * current_price
# 判断是买入还是卖出
# 如果目标市值 < 当前市值,需要卖出
if target_value < current_holding_value:
sell_orders.append((stock, target_value))
# 如果目标市值 > 当前市值,需要买入
else:
buy_orders.append((stock, target_value))
# ---------------------------------------------------
# 关键步骤:先执行卖出,释放资金,再执行买入
# ---------------------------------------------------
# 1. 执行卖出
for stock, target_val in sell_orders:
order_target_value(stock, target_val)
log.info("卖出调整: %s, 目标市值: %.2f" % (stock, target_val))
# 2. 执行买入
for stock, target_val in buy_orders:
order_target_value(stock, target_val)
log.info("买入调整: %s, 目标市值: %.2f" % (stock, target_val))
# 引入 math 库用于判断 NaN
import math
代码关键点解析
-
order_target_value(security, value):- 这是PTrade中非常强大的接口。你不需要计算要买卖多少股,只需要告诉它“我希望这只股票最终持有多少钱的市值”。
- 如果当前持有0元,目标是10000元,它会自动买入10000元。
- 如果当前持有20000元,目标是10000元,它会自动卖出10000元。
-
先卖后买逻辑:
- 在
rebalance_portfolio函数中,我们将订单分为了sell_orders和buy_orders。 - 原因:如果账户接近满仓,直接发出买入指令可能会因为“可用资金不足”而废单。先执行卖出操作可以将仓位过重的资产变现,增加
context.portfolio.cash,从而保证后续的买入操作顺利成交。
- 在
-
调仓时间判断:
current_date.month != previous_date.month是判断月初最简单有效的方法。这意味着昨天还在上个月,今天已经进入新月份了,因此今天是本月第一个交易日。
-
Python 版本兼容性:
- 代码使用了
%s和%进行字符串格式化,完全兼容 Python 3.5 及以上版本(PTrade环境通常支持)。
- 代码使用了
常见问题 (Q&A)
Q: 如果某只股票停牌了,策略会怎么处理?
A: 代码中增加了 if stock not in data or math.isnan(data[stock].price): 的判断。如果股票停牌,通常无法获取最新价格,策略会跳过对该股票的操作,并在日志中打印警告,防止程序报错中断。
Q: 我可以将调仓频率改为每周吗?
A: 可以。在 handle_data 中,你可以使用 context.blotter.current_dt.isoweekday() 来判断。例如,判断 current_date.isoweekday() == 1 (周一) 且与上一次调仓日期不同,即可实现周度调仓。
Q: 为什么不直接在 initialize 里用 run_daily 定时?
A: 使用 run_daily 也是可以的,例如 run_daily(context, rebalance_portfolio, time='09:35')。但在函数内部依然需要判断日期是否符合“月初”的条件,因为 run_daily 是每天都会触发的。上面的写法直接在 handle_data 中判断日期变化,逻辑上更为直观。
Q: 权重之和必须严格等于1吗?
A: 建议等于1(即100%仓位)。如果小于1(例如0.8),则策略会保留20%的现金不进行投资。如果大于1,会导致资金不足无法完成所有买入操作。