Explain rebalance denial reasons
This commit is contained in:
@@ -503,6 +503,42 @@ where
|
||||
order_step_size,
|
||||
);
|
||||
let provisional_target_qty = desired_qty.clamp(min_target_qty, max_target_qty);
|
||||
if desired_qty < current_qty
|
||||
&& min_target_qty >= current_qty
|
||||
&& diagnostics.len() < 16
|
||||
&& let Some(reason) = self.sell_target_denial_reason(
|
||||
date,
|
||||
portfolio,
|
||||
data,
|
||||
&symbol,
|
||||
current_qty,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
)
|
||||
{
|
||||
diagnostics.push(format!(
|
||||
"rebalance_target_denied symbol={} side=sell reason={}",
|
||||
symbol, reason
|
||||
));
|
||||
}
|
||||
if desired_qty > current_qty
|
||||
&& max_target_qty <= current_qty
|
||||
&& diagnostics.len() < 16
|
||||
&& let Some(reason) = self.buy_target_denial_reason(
|
||||
date,
|
||||
portfolio,
|
||||
data,
|
||||
&symbol,
|
||||
current_qty,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
)
|
||||
{
|
||||
diagnostics.push(format!(
|
||||
"rebalance_target_denied symbol={} side=buy reason={}",
|
||||
symbol, reason
|
||||
));
|
||||
}
|
||||
if provisional_target_qty != desired_qty && diagnostics.len() < 16 {
|
||||
diagnostics.push(format!(
|
||||
"rebalance_target_clipped symbol={} desired={} min={} max={} provisional={}",
|
||||
@@ -734,6 +770,92 @@ where
|
||||
gross - cost.total()
|
||||
}
|
||||
|
||||
fn sell_target_denial_reason(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
portfolio: &PortfolioState,
|
||||
data: &DataSet,
|
||||
symbol: &str,
|
||||
current_qty: u32,
|
||||
minimum_order_quantity: u32,
|
||||
order_step_size: u32,
|
||||
) -> Option<String> {
|
||||
if current_qty == 0 {
|
||||
return None;
|
||||
}
|
||||
let position = portfolio.position(symbol)?;
|
||||
let snapshot = data.require_market(date, symbol).ok()?;
|
||||
let candidate = data.require_candidate(date, symbol).ok()?;
|
||||
let rule = self.rules.can_sell(
|
||||
date,
|
||||
snapshot,
|
||||
candidate,
|
||||
position,
|
||||
self.execution_price_field,
|
||||
);
|
||||
if !rule.allowed {
|
||||
return rule.reason;
|
||||
}
|
||||
let sellable = position.sellable_qty(date);
|
||||
match self.market_fillable_quantity(
|
||||
snapshot,
|
||||
OrderSide::Sell,
|
||||
sellable.min(current_qty),
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
0,
|
||||
sellable >= current_qty,
|
||||
) {
|
||||
Ok(quantity) => {
|
||||
let quantity = quantity.min(sellable).min(current_qty);
|
||||
if quantity == 0 {
|
||||
Some("no sellable quantity".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(reason) => Some(reason),
|
||||
}
|
||||
}
|
||||
|
||||
fn buy_target_denial_reason(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
_portfolio: &PortfolioState,
|
||||
data: &DataSet,
|
||||
symbol: &str,
|
||||
current_qty: u32,
|
||||
minimum_order_quantity: u32,
|
||||
order_step_size: u32,
|
||||
) -> Option<String> {
|
||||
let snapshot = data.require_market(date, symbol).ok()?;
|
||||
let candidate = data.require_candidate(date, symbol).ok()?;
|
||||
let rule = self
|
||||
.rules
|
||||
.can_buy(date, snapshot, candidate, self.execution_price_field);
|
||||
if !rule.allowed {
|
||||
return rule.reason;
|
||||
}
|
||||
match self.market_fillable_quantity(
|
||||
snapshot,
|
||||
OrderSide::Buy,
|
||||
u32::MAX,
|
||||
minimum_order_quantity,
|
||||
order_step_size,
|
||||
0,
|
||||
false,
|
||||
) {
|
||||
Ok(quantity) => {
|
||||
if current_qty.saturating_add(quantity) <= current_qty {
|
||||
Some("no fillable buy quantity".to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(reason) => Some(reason),
|
||||
}
|
||||
}
|
||||
|
||||
fn estimated_buy_cash_out(&self, date: NaiveDate, price: f64, quantity: u32) -> f64 {
|
||||
if quantity == 0 {
|
||||
return 0.0;
|
||||
|
||||
Reference in New Issue
Block a user