问题描述
解决方案
构建一个自定义权重的指数组合(或投资组合)在 PTrade 中主要涉及以下几个核心步骤:
- 定义股票池与权重:确定你要持有的股票列表以及每只股票的目标权重(例如:固定权重、市值加权或等权重)。
- 计算目标持仓市值:根据当前账户的总资产和目标权重,计算每只股票应该持有的金额。
- 定期调仓(Rebalance):设置定时任务(如每月或每周),将实际持仓调整至目标权重。
- 交易执行:使用
order_target_value函数来买入或卖出,使持仓达到预期金额。
下面是一个完整的 PTrade 策略示例。该策略演示了如何构建一个包含 3 只股票的自定义组合,并设定了不同的权重(例如:股票A 50%,股票B 30%,股票C 20%),且每月第一个交易日进行调仓。
策略代码实现
def initialize(context):
"""
初始化函数,设置策略参数
"""
# 1. 定义自定义权重的组合
# 格式:{'股票代码': 目标权重}
# 注意:权重之和最好为 1.0 (100%),如果不足 1.0 则会有现金剩余
g.target_portfolio = {
'600519.SS': 0.5, # 贵州茅台 50%
'000858.SZ': 0.3, # 五粮液 30%
'600036.SS': 0.2 # 招商银行 20%
}
# 设置股票池(用于回测数据预加载)
set_universe(list(g.target_portfolio.keys()))
# 2. 设置调仓周期
# 这里设置为每天检查,但在函数内部判断是否为月初
# time='10:00' 表示在交易日 10:00 执行调仓逻辑,避开开盘剧烈波动
run_daily(context, rebalance_portfolio, time='10:00')
def before_trading_start(context, data):
"""
盘前处理
"""
# 可以在这里根据动态逻辑更新 g.target_portfolio
# 如果是固定权重,则不需要操作
pass
def rebalance_portfolio(context):
"""
调仓核心逻辑
"""
# --- 1. 判断调仓时机 ---
# 获取当前日期
current_date = context.blotter.current_dt.date()
# 获取当月第一个交易日
# get_trade_days 返回的是字符串列表或 numpy array,需要处理
trade_days = get_trade_days(start_date=current_date.strftime("%Y%m01"), end_date=current_date.strftime("%Y%m%d"))
# 如果当前日期不是当月第一个交易日,则跳过(实现月度调仓)
# 注意:trade_days[0] 是当月第一天,如果当前日期等于它,说明是月初
# 为了演示简单,这里假设只要运行了就是调仓日。
# 实际生产中,建议使用 context.blotter.current_dt.month != context.previous_date.month 来判断换月
is_month_start = False
if context.previous_date.month != context.blotter.current_dt.month:
is_month_start = True
# 如果不是换月的第一天,且不是回测第一天,则不调仓
if not is_month_start and context.blotter.current_dt != get_trade_days(count=1)[0]:
return
log.info("开始执行月度调仓...")
# --- 2. 获取账户总资产 ---
# portfolio_value 包含 现金 + 持仓市值
total_value = context.portfolio.portfolio_value
# --- 3. 卖出不在目标组合中的股票 ---
current_positions = list(context.portfolio.positions.keys())
for stock in current_positions:
if stock not in g.target_portfolio:
# 如果当前持仓的股票不在目标清单里,清仓
order_target_value(stock, 0)
log.info("卖出非目标股票: %s" % stock)
# --- 4. 买入/调整目标股票 ---
for stock, weight in g.target_portfolio.items():
# 检查股票是否停牌或退市,避免下单失败
if not is_stock_tradable(stock):
log.warning("股票 %s 当前不可交易,跳过" % stock)
continue
# 计算该股票的目标持仓市值
target_value = total_value * weight
# 使用 order_target_value 直接调整持仓到目标金额
# 该函数会自动计算需要买入或卖出的数量
order_target_value(stock, target_value)
log.info("调整股票 %s 持仓至目标市值: %.2f (权重: %.2f)" % (stock, target_value, weight))
def is_stock_tradable(stock):
"""
辅助函数:判断股票是否可交易(非停牌、非退市)
"""
# 获取股票状态
# get_stock_status 返回字典 {code: bool}
# ST: 是否ST, HALT: 是否停牌, DELISTING: 是否退市
statuses = get_stock_status([stock], filter_type=["HALT", "DELISTING"])
# 如果返回 True,说明处于停牌或退市状态,不可交易
if statuses[stock]:
return False
return True
def handle_data(context, data):
"""
盘中运行函数(本策略主要逻辑在 run_daily 中,此处留空)
"""
pass
代码关键点解析
-
g.target_portfolio(全局变量):- 我们使用字典来存储股票代码和对应的权重。这种结构非常灵活,你可以很容易地将其修改为动态生成的字典(例如在
before_trading_start中根据市值或因子计算权重)。
- 我们使用字典来存储股票代码和对应的权重。这种结构非常灵活,你可以很容易地将其修改为动态生成的字典(例如在
-
order_target_value(security, value):- 这是构建指数组合最关键的 API。
- 它的作用是:无论当前持有多少股,系统会自动计算差额,买入或卖出,使得该股票的最终持仓市值等于
value。 - 例如:总资产 100万,茅台目标权重 50%。调用
order_target_value('600519.SS', 500000)。如果当前持有 40万,系统会自动买入 10万;如果当前持有 60万,系统会自动卖出 10万。
-
调仓频率控制:
- 示例代码中使用了
if context.previous_date.month != context.blotter.current_dt.month:这一逻辑来判断是否刚刚跨月。这是实现月度调仓的常用技巧。 - 如果不加这个判断,策略会每天都进行调仓,这会导致极高的手续费和滑点损耗。
- 示例代码中使用了
-
异常处理 (
is_stock_tradable):- 在实际交易或回测中,股票可能会停牌。如果对停牌的股票下单,订单会失败或一直挂单。因此,使用
get_stock_status检查股票状态是一个良好的编程习惯。
- 在实际交易或回测中,股票可能会停牌。如果对停牌的股票下单,订单会失败或一直挂单。因此,使用
如何扩展为动态权重?
如果你不想使用固定的权重,而是想根据流通市值来加权(类似沪深300指数的编制方法),你可以修改 rebalance_portfolio 中的逻辑:
def get_market_cap_weights(stock_list):
# 获取基本面数据:流通市值 (float_value)
# 注意:get_fundamentals 返回的是 DataFrame
q = get_fundamentals(stock_list, 'valuation', fields=['float_value'])
if q is None or len(q) == 0:
return {}
# 计算总流通市值
total_cap = q['float_value'].sum()
weights = {}
# 遍历 DataFrame 计算权重
# 注意:PTrade 返回的 DataFrame 索引通常是股票代码
for stock in stock_list:
if stock in q.index:
# 单只股票市值 / 总市值
w = q.loc[stock, 'float_value'] / total_cap
weights[stock] = w
else:
weights[stock] = 0.0
return weights
你可以在调仓前调用这个函数来动态更新 g.target_portfolio。
Q&A
Q: 为什么使用 order_target_value 而不是 order_target (按数量)?
A: 构建指数组合时,我们通常关注的是资产配置比例(即金额占比)。股票价格各不相同,如果按数量分配(例如每只买100股),高价股的权重会远超低价股,导致组合偏离预期。使用 order_target_value 可以直接根据资金量进行配置,更加准确。
Q: 如果权重加起来超过 1.0 (100%) 会怎样?
A: 如果 total_value * weight 的总和超过了账户里的 portfolio_value(包含现金),在执行最后的买入操作时,可能会因为可用资金不足而导致下单失败或部分成交。建议在计算权重时进行归一化处理(即所有权重除以权重之和),确保总和不超过 1.0。
Q: 如何处理停牌的股票?
A: 在示例代码中,我们使用了 is_stock_tradable 函数跳过了停牌股票。对于指数组合来说,如果某只成分股停牌,通常的做法是保持该股票持仓不变,只调整其他可交易股票的仓位,或者将该股票的目标资金暂时保留为现金。