Betalens新手系列 · 15 调仓审计与现金管理:读懂 rebalance_log 与 actual_weight
阅读提示
这篇专门讲解 BacktestBase 的调仓审计日志(rebalance_log)和现金管理机制。如果你想理解每次调仓具体发生了什么——成交了多少股、花了多少钱、手续费多少——这篇会给你完整的答案。
导言 每次调仓(Rebalance),BacktestBase 内部会执行一次完整的撮合过程:
1 目标权重 → 计算目标股数 → 取整到整数手 → 计算实际成交金额 → 更新持仓 → 计算净值
rebalance_log 记录了这个过程里每一笔交易的明细 ——日期、股票代码、买入/卖出方向、成交价格、股数、手数、金额、手续费。如果回测出了问题,rebalance_log 是最重要的调试数据。
rebalance_log 字段说明 1 2 log = engine.rebalance_log print (log.columns.tolist())
常见分析场景 场景 1:看总交易成本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 log = engine.rebalance_log buys = log[log["side" ] == "buy" ] sells = log[log["side" ] == "sell" ] print (f"买入总额:{buys['value' ].sum ():,.2 f} " )print (f"卖出总额:{sells['value' ].sum ():,.2 f} " )print (f"总手续费:{log['commission' ].sum ():,.2 f} " )print (f"总滑点损失:{log['slippage' ].sum ():,.2 f} " )print (f"总交易成本:{(log['commission' ].sum () + log['slippage' ].sum ()):,.2 f} " )total_cost = log['commission' ].sum () + log['slippage' ].sum () cost_ratio = total_cost / engine.amount print (f"交易成本率:{cost_ratio:.2 %} " )
场景 2:看哪只股票换手最频繁 1 2 3 4 5 6 7 8 9 turnover_count = log.groupby("code" )["side" ].count().sort_values(ascending=False ) print ("换手最频繁的10只股票:" )print (turnover_count.head(10 ))code = "000001.SZ" stock_log = log[log["code" ] == code] print (stock_log[["rebalance_date" , "side" , "shares" , "price" , "value" ]])
场景 3:看整数手的碎片化损耗 整数手约束会导致”凑不够一手”的碎片化损耗:
1 2 3 4 5 6 log["target_shares" ] = log["value" ] / log["price" ] log["round_lot_loss" ] = log["target_shares" ] - log["shares" ] print (f"总碎片损耗股数:{log['round_lot_loss' ].sum ():,.0 f} " )print (f"碎片损耗占总目标股数:{log['round_lot_loss' ].sum () / log['target_shares' ].sum ():.2 %} " )
碎片化损耗在资金小、持仓多时尤其严重。如果总损耗超过 1%,说明初始资金可能太小,或者每组股票数量过多。
场景 4:审计停牌导致的”被迫卖出” 1 2 3 4 5 6 forced_sells = log[log["slippage" ] > 0 ] if len (forced_sells) > 0 : print ("被迫卖出记录:" ) print (forced_sells[["rebalance_date" , "code" , "side" , "value" , "slippage" ]])
场景 5:重建每日持仓明细 1 2 3 4 5 6 7 8 9 trades = log[["rebalance_date" , "code" , "side" , "shares" , "price" ]].copy() trades["signed_shares" ] = trades.apply( lambda r: r["shares" ] if r["side" ] == "buy" else -r["shares" ], axis=1 ) position_from_log = trades.groupby(["code" , "rebalance_date" ])["signed_shares" ].sum () print (position_from_log.head(20 ))
actual_weight 与 position 的关系 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 print (engine.actual_weight.head())print (engine.position.head())total_value = engine.daily_position_value for date in engine.actual_weight.index[:5 ]: for code in engine.actual_weight.columns[:-1 ]: weight = engine.actual_weight.loc[date, code] pos = engine.position.loc[date, code] price = engine.daily_pnl.loc[date, code] if pd.notna(weight) and pd.notna(pos): assert abs (weight * total_value.loc[date] - pos * price) < 1 , "数据不一致!"
开发者侧:rebalance_log 的生成时机 rebalance_log 不是”每天都生成一行”,而是每次调仓时生成 。如果某只股票从调仓日1到调仓日2之间没有发生调仓,对应的 rebalance_log 里就不会有记录——这是合理的,因为没有交易就没有审计日志。
1 2 3 4 5 6 print (f"调仓次数:{engine.rebalance_log['rebalance_date' ].nunique()} " )rebal_dates = engine.rebalance_log['rebalance_date' ].unique() print (f"平均每次调仓股票数:{len (engine.rebalance_log) / len (rebal_dates):.1 f} " )
现金管理的细节 现金流入
卖出证券 :卖出金额扣除手续费后进入现金。
停牌转现金 :trade_status_mode="to_cash" 时,停牌股票在下一个交易日开盘被卖出(或等比例转现金)。
现金流出
买入证券 :买入金额从现金扣除,加上手续费。
整数手碎片 :每次买股的碎片化”凑不够一手”部分资金留在现金。
现金的跟踪 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 cash_series = [] for date in engine.nav.index: position_value = 0 for code in engine.actual_weight.columns[:-1 ]: weight = engine.actual_weight.loc[date, code] if pd.notna(weight): position_value += weight * engine.nav.loc[date] * engine.amount nav = engine.nav.loc[date] total_assets = nav * engine.amount cash = total_assets - position_value cash_series.append(cash) import pandas as pdcash_df = pd.Series(cash_series, index=engine.nav.index, name="cash" ) print (cash_df.describe())
常见问题 1. rebalance_log 是空的
1 2 print (engine.rebalance_log)
原因:actual_weight 全为 0(资金太小买不了任何股票)。见 04 篇的诊断流程。
2. side 字段全是 buy
如果策略是纯多头,每次调仓都会先卖出旧持仓(sell),再买入新持仓(buy)。全是 buy 说明要么是初始建仓,要么是 position 保留了旧股票但 rebalance_log 没有记录(可能是 trade_status_mode="hold")。
3. 手续费比预期高
A 股印花税是卖出时收取 的(0.1%),所以 commission 里卖出交易的手续费应该明显高于买入。如果买入和卖出手续费差不多,可能是 ptc 设成了双向(应该只设卖出方向的)。
延伸阅读