Emit rebalance clipping diagnostics
This commit is contained in:
@@ -16,6 +16,7 @@ pub struct BrokerExecutionReport {
|
||||
pub fill_events: Vec<FillEvent>,
|
||||
pub position_events: Vec<PositionEvent>,
|
||||
pub account_events: Vec<AccountEvent>,
|
||||
pub diagnostics: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -294,11 +295,12 @@ where
|
||||
return Ok(report);
|
||||
}
|
||||
|
||||
let target_quantities = if decision.rebalance {
|
||||
let (target_quantities, rebalance_diagnostics) = if decision.rebalance {
|
||||
self.target_quantities(date, portfolio, data, &decision.target_weights)?
|
||||
} else {
|
||||
BTreeMap::new()
|
||||
(BTreeMap::new(), Vec::new())
|
||||
};
|
||||
report.diagnostics.extend(rebalance_diagnostics);
|
||||
|
||||
let mut sell_symbols = BTreeSet::new();
|
||||
sell_symbols.extend(portfolio.positions().keys().cloned());
|
||||
@@ -437,10 +439,11 @@ where
|
||||
portfolio: &PortfolioState,
|
||||
data: &DataSet,
|
||||
target_weights: &BTreeMap<String, f64>,
|
||||
) -> Result<BTreeMap<String, u32>, BacktestError> {
|
||||
) -> Result<(BTreeMap<String, u32>, Vec<String>), BacktestError> {
|
||||
let equity = self.total_equity_at(date, portfolio, data, self.execution_price_field)?;
|
||||
let target_weight_sum = target_weights.values().copied().sum::<f64>();
|
||||
let mut desired_targets = BTreeMap::new();
|
||||
let mut diagnostics = Vec::new();
|
||||
for (symbol, weight) in target_weights {
|
||||
let price = data
|
||||
.price(date, symbol, self.execution_price_field)
|
||||
@@ -500,6 +503,12 @@ where
|
||||
order_step_size,
|
||||
);
|
||||
let provisional_target_qty = desired_qty.clamp(min_target_qty, max_target_qty);
|
||||
if provisional_target_qty != desired_qty && diagnostics.len() < 16 {
|
||||
diagnostics.push(format!(
|
||||
"rebalance_target_clipped symbol={} desired={} min={} max={} provisional={}",
|
||||
symbol, desired_qty, min_target_qty, max_target_qty, provisional_target_qty
|
||||
));
|
||||
}
|
||||
if current_qty > provisional_target_qty {
|
||||
projected_cash += self.estimated_sell_net_cash(
|
||||
date,
|
||||
@@ -535,12 +544,13 @@ where
|
||||
.filter(|constraint| constraint.provisional_target_qty > constraint.current_qty)
|
||||
.collect::<Vec<_>>();
|
||||
if buy_constraints.is_empty() {
|
||||
return Ok(targets);
|
||||
return Ok((targets, diagnostics));
|
||||
}
|
||||
|
||||
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 };
|
||||
let initial_safety = if target_weight_sum > 0.95 { 1.2 } else { 1.0 };
|
||||
let mut safety = initial_safety;
|
||||
loop {
|
||||
let mut candidate_targets = targets.clone();
|
||||
let mut buy_cash_out = 0.0;
|
||||
@@ -604,7 +614,30 @@ where
|
||||
safety = next_safety;
|
||||
}
|
||||
|
||||
Ok(best_targets)
|
||||
if safety < initial_safety && diagnostics.len() < 16 {
|
||||
diagnostics.push(format!(
|
||||
"rebalance_safety_scaled final_safety={:.4} target_weight_sum={:.4} projected_cash={:.2}",
|
||||
safety, target_weight_sum, projected_cash
|
||||
));
|
||||
}
|
||||
|
||||
for constraint in &buy_constraints {
|
||||
let final_target_qty = best_targets
|
||||
.get(&constraint.symbol)
|
||||
.copied()
|
||||
.unwrap_or(constraint.current_qty);
|
||||
if final_target_qty < constraint.provisional_target_qty && diagnostics.len() < 16 {
|
||||
diagnostics.push(format!(
|
||||
"rebalance_buy_reduced symbol={} provisional={} final={} current={}",
|
||||
constraint.symbol,
|
||||
constraint.provisional_target_qty,
|
||||
final_target_qty,
|
||||
constraint.current_qty
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok((best_targets, diagnostics))
|
||||
}
|
||||
|
||||
fn minimum_target_quantity(
|
||||
|
||||
Reference in New Issue
Block a user