Iterate rebalance safety like rqalpha
This commit is contained in:
@@ -33,7 +33,6 @@ struct TargetConstraint {
|
||||
min_target_qty: u32,
|
||||
max_target_qty: u32,
|
||||
provisional_target_qty: u32,
|
||||
target_weight: f64,
|
||||
price: f64,
|
||||
minimum_order_quantity: u32,
|
||||
order_step_size: u32,
|
||||
@@ -506,14 +505,12 @@ where
|
||||
min_target_qty,
|
||||
max_target_qty,
|
||||
provisional_target_qty,
|
||||
target_weight: *target_weights.get(&symbol).unwrap_or(&0.0),
|
||||
price,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
});
|
||||
}
|
||||
|
||||
let safety = if target_weight_sum > 0.95 { 1.2 } else { 1.0 };
|
||||
let mut targets = BTreeMap::new();
|
||||
for constraint in &constraints {
|
||||
if constraint.provisional_target_qty > constraint.current_qty {
|
||||
@@ -524,67 +521,81 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
let mut buy_constraints = constraints
|
||||
let buy_constraints = constraints
|
||||
.iter()
|
||||
.filter(|constraint| constraint.provisional_target_qty > constraint.current_qty)
|
||||
.collect::<Vec<_>>();
|
||||
buy_constraints.sort_by(|lhs, rhs| {
|
||||
rhs.target_weight
|
||||
.partial_cmp(&lhs.target_weight)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
.then_with(|| {
|
||||
let lhs_gap = (lhs.provisional_target_qty.saturating_sub(lhs.current_qty))
|
||||
as f64
|
||||
* lhs.price;
|
||||
let rhs_gap = (rhs.provisional_target_qty.saturating_sub(rhs.current_qty))
|
||||
as f64
|
||||
* rhs.price;
|
||||
rhs_gap
|
||||
.partial_cmp(&lhs_gap)
|
||||
.unwrap_or(std::cmp::Ordering::Equal)
|
||||
})
|
||||
.then_with(|| lhs.symbol.cmp(&rhs.symbol))
|
||||
});
|
||||
|
||||
for constraint in buy_constraints {
|
||||
let mut target_qty = if safety > 1.0 {
|
||||
let scaled_desired_qty = ((constraint.desired_qty as f64) * safety).floor() as u32;
|
||||
self.round_buy_quantity(
|
||||
scaled_desired_qty,
|
||||
constraint.minimum_order_quantity,
|
||||
constraint.order_step_size,
|
||||
)
|
||||
.clamp(constraint.current_qty, constraint.max_target_qty)
|
||||
} else {
|
||||
constraint.provisional_target_qty
|
||||
};
|
||||
target_qty = target_qty.max(constraint.current_qty);
|
||||
let desired_additional = target_qty.saturating_sub(constraint.current_qty);
|
||||
let affordable_additional = self.affordable_buy_quantity(
|
||||
date,
|
||||
projected_cash,
|
||||
None,
|
||||
constraint.price,
|
||||
desired_additional,
|
||||
constraint.minimum_order_quantity,
|
||||
constraint.order_step_size,
|
||||
);
|
||||
target_qty = (constraint.current_qty + affordable_additional)
|
||||
.clamp(constraint.min_target_qty, constraint.max_target_qty);
|
||||
if target_qty > constraint.current_qty {
|
||||
projected_cash -= self.estimated_buy_cash_out(
|
||||
date,
|
||||
constraint.price,
|
||||
target_qty - constraint.current_qty,
|
||||
);
|
||||
}
|
||||
|
||||
if target_qty > 0 {
|
||||
targets.insert(constraint.symbol.clone(), target_qty);
|
||||
}
|
||||
if buy_constraints.is_empty() {
|
||||
return Ok(targets);
|
||||
}
|
||||
|
||||
Ok(targets)
|
||||
let mut best_targets = targets.clone();
|
||||
let mut best_proportion_diff = f64::INFINITY;
|
||||
let mut safety = if target_weight_sum > 0.95 { 1.2 } else { 1.0 };
|
||||
loop {
|
||||
let mut candidate_targets = targets.clone();
|
||||
let mut buy_cash_out = 0.0;
|
||||
for constraint in &buy_constraints {
|
||||
let scaled_desired_qty = ((constraint.desired_qty as f64) * safety).floor() as u32;
|
||||
let mut target_qty = self
|
||||
.round_buy_quantity(
|
||||
scaled_desired_qty,
|
||||
constraint.minimum_order_quantity,
|
||||
constraint.order_step_size,
|
||||
)
|
||||
.clamp(constraint.min_target_qty, constraint.max_target_qty)
|
||||
.max(constraint.current_qty);
|
||||
if target_qty < constraint.current_qty {
|
||||
target_qty = constraint.current_qty;
|
||||
}
|
||||
if target_qty > constraint.current_qty {
|
||||
buy_cash_out += self.estimated_buy_cash_out(
|
||||
date,
|
||||
constraint.price,
|
||||
target_qty - constraint.current_qty,
|
||||
);
|
||||
}
|
||||
if target_qty > 0 {
|
||||
candidate_targets.insert(constraint.symbol.clone(), target_qty);
|
||||
}
|
||||
}
|
||||
|
||||
let total_target_value = constraints
|
||||
.iter()
|
||||
.map(|constraint| {
|
||||
candidate_targets
|
||||
.get(&constraint.symbol)
|
||||
.copied()
|
||||
.unwrap_or(0) as f64
|
||||
* constraint.price
|
||||
})
|
||||
.sum::<f64>();
|
||||
let proportion_diff = if equity > 0.0 {
|
||||
((total_target_value / equity) - target_weight_sum).abs()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
if buy_cash_out <= projected_cash + 1e-6 {
|
||||
if proportion_diff <= best_proportion_diff + 1e-12 {
|
||||
best_targets = candidate_targets;
|
||||
best_proportion_diff = proportion_diff;
|
||||
} else if best_proportion_diff.is_finite() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if safety <= 0.0 {
|
||||
break;
|
||||
}
|
||||
let step = (proportion_diff / 10.0).clamp(0.0001, 0.002);
|
||||
let next_safety = (safety - step).max(0.0);
|
||||
if (next_safety - safety).abs() < f64::EPSILON {
|
||||
break;
|
||||
}
|
||||
safety = next_safety;
|
||||
}
|
||||
|
||||
Ok(best_targets)
|
||||
}
|
||||
|
||||
fn minimum_target_quantity(
|
||||
|
||||
@@ -631,16 +631,19 @@ fn rebalance_optimizer_prioritizes_higher_target_weight_when_cash_is_tight() {
|
||||
)
|
||||
.expect("broker execution");
|
||||
|
||||
assert_eq!(
|
||||
portfolio
|
||||
.position("000002.SZ")
|
||||
.map(|position| position.quantity)
|
||||
.unwrap_or(0),
|
||||
900
|
||||
);
|
||||
let lower_weight_qty = portfolio
|
||||
.position("000001.SZ")
|
||||
.map(|position| position.quantity)
|
||||
.unwrap_or(0);
|
||||
let higher_weight_qty = portfolio
|
||||
.position("000002.SZ")
|
||||
.map(|position| position.quantity)
|
||||
.unwrap_or(0);
|
||||
assert!(
|
||||
portfolio.position("000001.SZ").is_none(),
|
||||
"higher target weight should consume the limited rebalance cash first"
|
||||
higher_weight_qty > lower_weight_qty,
|
||||
"cash-constrained rebalance should preserve more exposure for the higher target weight, got low={} high={}",
|
||||
lower_weight_qty,
|
||||
higher_weight_qty
|
||||
);
|
||||
assert!(
|
||||
report
|
||||
|
||||
Reference in New Issue
Block a user