修复补减仓时对原持仓重复计入交易滑点
This commit is contained in:
@@ -1588,7 +1588,17 @@ pub fn build_stock_pool_target_plan_with_fee_model(
|
||||
if sizing_price <= Decimal::ZERO {
|
||||
return Err(format!("{symbol} execution sizing price is invalid"));
|
||||
}
|
||||
let raw_target = (target_value / sizing_price).floor();
|
||||
// Existing shares are marked at the observed market price. Only the
|
||||
// new buy leg pays its executable/slippage price; repricing the whole
|
||||
// position would charge fictitious slippage and miss a board lot.
|
||||
let current_value = current_quantity * quote.last_price;
|
||||
let raw_target = if target_value >= current_value {
|
||||
current_quantity + ((target_value - current_value) / sizing_price).floor()
|
||||
} else {
|
||||
// Sale slippage changes proceeds, not the marked shares we must
|
||||
// remove to reach a market-value target.
|
||||
(target_value / quote.last_price).floor()
|
||||
};
|
||||
let (step, minimum_buy) = order_quantity_rules(quote)?;
|
||||
let mut target_quantity = current_quantity;
|
||||
let mut delta = Decimal::ZERO;
|
||||
@@ -1911,7 +1921,7 @@ pub fn build_stock_pool_target_plan_with_fee_model(
|
||||
let cost = |quantity: Decimal| {
|
||||
Ok(quantity * price + fee_for(&row.symbol, OrderSide::Buy, quantity * price)?)
|
||||
};
|
||||
let own_budget = (row.target_value - row.current_quantity * price).max(Decimal::ZERO);
|
||||
let own_budget = (row.target_value - row.current_quantity * quote.last_price).max(Decimal::ZERO);
|
||||
let allocation_quantity = max_affordable_buy_quantity_with_cost(
|
||||
own_budget,
|
||||
row.delta_quantity,
|
||||
|
||||
@@ -2,12 +2,12 @@ use super::*;
|
||||
|
||||
#[test]
|
||||
fn equal_thirty_seats_use_full_precision_at_a_board_lot_boundary() {
|
||||
for (equity, price, held) in [("999377.147617", "3.070307", 2000), ("995624.8819", "6.420642", 900)] {
|
||||
for (equity, price, executable, held) in [("999377.147617", "3.07", "3.070307", 2000), ("995624.8819", "6.42", "6.420642", 900)] {
|
||||
let pool = members(30);
|
||||
let mut market = quotes(30);
|
||||
let last = market.last_mut().unwrap();
|
||||
last.last_price = price.parse().unwrap();
|
||||
last.buy_sizing_price = Some(last.last_price);
|
||||
last.buy_sizing_price = Some(executable.parse().unwrap());
|
||||
let positions = vec![Position { symbol: last.symbol.clone(), quantity: held.into(), closable_quantity: held.into(), average_cost: last.last_price }];
|
||||
let mut selection = selection(30, 30);
|
||||
let constraints = StockPoolDecisionConstraints { target_holding_count: Some(30), reserve_cash_slots: 1, ..Default::default() };
|
||||
@@ -32,6 +32,19 @@ fn equal_thirty_seats_use_full_precision_at_a_board_lot_boundary() {
|
||||
assert_eq!(state, restored);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sale_slippage_does_not_prevent_a_marked_value_board_lot_reduction() {
|
||||
let pool = members(1);
|
||||
let mut market = quotes(1);
|
||||
market[0].sell_sizing_price = Some(Decimal::new(99,1));
|
||||
let positions = vec![Position { symbol:symbol(1),quantity:200.into(),closable_quantity:200.into(),average_cost:10.into() }];
|
||||
let plan = build_stock_pool_target_plan_with_constraints(&selection(1,1),&pool,&StockPoolExecutionRule::default(),
|
||||
&AccountSnapshot {total_equity:2000.into(),cash:Decimal::ZERO,frozen_cash:Decimal::ZERO},&positions,&market,
|
||||
5000,Decimal::ZERO,"hold","full_rebalance",&StockPoolDecisionConstraints::default(),"reduce",Decimal::ZERO,Decimal::ZERO,Decimal::ZERO).unwrap();
|
||||
assert_eq!(plan.rows[0].target_value,Decimal::from(1000));
|
||||
assert_eq!(plan.rows[0].delta_quantity,Decimal::from(-100));
|
||||
}
|
||||
use serde_json::json;
|
||||
|
||||
fn symbol(index: usize) -> String {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
此前按10000整数基点分配等权,然后反算资金。30只股票的333/334基点并不等于1/30,在临界整手处会漏补仓。修复将 `target_weight_bps` 保留为展示/旧数据合同,新增独立 `target_weight_ratios` 计算预算。
|
||||
|
||||
临界样例同时复现了第二个错误:原持仓按含买入滑点的价格重新估值,将未发生交易的滑点也扣进可买预算。补仓现在用目标市值减去原持仓行情市值,再按新买入价格和费用计算;卖出滑点只改变回款,不改变待减少的行情市值股数。
|
||||
|
||||
停牌持仓优先保留已记录的高精度比例;旧状态只有整数基点时保留已证明的旧预算,不反猜精确1/N。退出、候补、保护席位、指数仓位、资金预留和显式部分权重保留原规则。实际下单数量仍经过资金/费用、整手、T+1及风控检查。
|
||||
|
||||
执行状态新增 `last_target_weight_ratios`,首次记录精确比例升级schema2。旧schema1可读但不得携带新比例字段;旧消费者应拒绝新状态,回滚不能删除或降精度重写状态。回测、Paper、Live及Strategy Runtime都必须共同消费该比例,ETF顺延目标也携带相同比例。
|
||||
|
||||
Reference in New Issue
Block a user