Align order costs and rebalance priority with rqalpha

This commit is contained in:
boris
2026-04-22 23:36:20 -07:00
parent c85116c59d
commit ea2871a0f2
6 changed files with 436 additions and 36 deletions

View File

@@ -29,9 +29,11 @@ struct ExecutionFill {
struct TargetConstraint {
symbol: String,
current_qty: u32,
desired_qty: u32,
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,
@@ -263,6 +265,8 @@ where
let mut intraday_turnover = BTreeMap::<String, u32>::new();
let mut execution_cursors = BTreeMap::<String, NaiveDateTime>::new();
let mut global_execution_cursor = None::<NaiveDateTime>;
let mut commission_state = BTreeMap::<u64, f64>::new();
let mut next_order_id = 1_u64;
if !decision.order_intents.is_empty() {
for intent in &decision.order_intents {
self.process_order_intent(
@@ -273,6 +277,8 @@ where
&mut intraday_turnover,
&mut execution_cursors,
&mut global_execution_cursor,
&mut commission_state,
&mut next_order_id,
&mut report,
)?;
}
@@ -316,10 +322,12 @@ where
data,
&symbol,
requested_qty,
Self::reserve_order_id(&mut next_order_id),
sell_reason(decision, &symbol),
&mut intraday_turnover,
&mut execution_cursors,
&mut global_execution_cursor,
&mut commission_state,
&mut report,
)?;
}
@@ -339,10 +347,12 @@ where
data,
&symbol,
requested_qty,
Self::reserve_order_id(&mut next_order_id),
"rebalance_buy",
&mut intraday_turnover,
&mut execution_cursors,
&mut global_execution_cursor,
&mut commission_state,
None,
&mut report,
)?;
@@ -363,6 +373,8 @@ where
intraday_turnover: &mut BTreeMap<String, u32>,
execution_cursors: &mut BTreeMap<String, NaiveDateTime>,
global_execution_cursor: &mut Option<NaiveDateTime>,
commission_state: &mut BTreeMap<u64, f64>,
next_order_id: &mut u64,
report: &mut BrokerExecutionReport,
) -> Result<(), BacktestError> {
match intent {
@@ -376,10 +388,12 @@ where
data,
symbol,
*target_value,
next_order_id,
reason,
intraday_turnover,
execution_cursors,
global_execution_cursor,
commission_state,
report,
),
OrderIntent::Value {
@@ -392,15 +406,23 @@ where
data,
symbol,
*value,
next_order_id,
reason,
intraday_turnover,
execution_cursors,
global_execution_cursor,
commission_state,
report,
),
}
}
fn reserve_order_id(next_order_id: &mut u64) -> u64 {
let order_id = *next_order_id;
*next_order_id = next_order_id.saturating_add(1);
order_id
}
fn target_quantities(
&self,
date: NaiveDate,
@@ -409,6 +431,7 @@ where
target_weights: &BTreeMap<String, f64>,
) -> Result<BTreeMap<String, u32>, 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();
for (symbol, weight) in target_weights {
let price = data
@@ -477,40 +500,83 @@ where
);
}
constraints.push(TargetConstraint {
symbol,
symbol: symbol.clone(),
current_qty,
desired_qty,
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 {
let mut target_qty = constraint.provisional_target_qty;
if target_qty > constraint.current_qty {
let desired_additional = target_qty - constraint.current_qty;
let affordable_additional = self.affordable_buy_quantity(
date,
projected_cash,
None,
constraint.price,
desired_additional,
if constraint.provisional_target_qty > constraint.current_qty {
continue;
}
if constraint.provisional_target_qty > 0 {
targets.insert(constraint.symbol.clone(), constraint.provisional_target_qty);
}
}
let mut 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,
);
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 {
@@ -631,10 +697,12 @@ where
data: &DataSet,
symbol: &str,
requested_qty: u32,
order_id: u64,
reason: &str,
intraday_turnover: &mut BTreeMap<String, u32>,
execution_cursors: &mut BTreeMap<String, NaiveDateTime>,
global_execution_cursor: &mut Option<NaiveDateTime>,
commission_state: &mut BTreeMap<u64, f64>,
report: &mut BrokerExecutionReport,
) -> Result<(), BacktestError> {
let snapshot = data.require_market(date, symbol)?;
@@ -659,6 +727,7 @@ where
};
report.order_events.push(OrderEvent {
date,
order_id: Some(order_id),
symbol: symbol.to_string(),
side: OrderSide::Sell,
requested_quantity: requested_qty,
@@ -684,6 +753,7 @@ where
Err(limit_reason) => {
report.order_events.push(OrderEvent {
date,
order_id: Some(order_id),
symbol: symbol.to_string(),
side: OrderSide::Sell,
requested_quantity: requested_qty,
@@ -697,6 +767,7 @@ where
if filled_qty == 0 {
report.order_events.push(OrderEvent {
date,
order_id: Some(order_id),
symbol: symbol.to_string(),
side: OrderSide::Sell,
requested_quantity: requested_qty,
@@ -734,9 +805,13 @@ where
(filled_qty, self.sell_price(snapshot))
};
let gross_amount = execution_price * filled_qty as f64;
let cost = self
.cost_model
.calculate(date, OrderSide::Sell, gross_amount);
let cost = self.cost_model.calculate_with_order_state(
date,
OrderSide::Sell,
gross_amount,
Some(order_id),
commission_state,
);
let net_cash = gross_amount - cost.total();
let realized_pnl = portfolio
@@ -755,6 +830,7 @@ where
report.order_events.push(OrderEvent {
date,
order_id: Some(order_id),
symbol: symbol.to_string(),
side: OrderSide::Sell,
requested_quantity: requested_qty,
@@ -764,6 +840,7 @@ where
});
report.fill_events.push(FillEvent {
date,
order_id: Some(order_id),
symbol: symbol.to_string(),
side: OrderSide::Sell,
quantity: filled_qty,
@@ -806,10 +883,12 @@ where
data: &DataSet,
symbol: &str,
target_value: f64,
next_order_id: &mut u64,
reason: &str,
intraday_turnover: &mut BTreeMap<String, u32>,
execution_cursors: &mut BTreeMap<String, NaiveDateTime>,
global_execution_cursor: &mut Option<NaiveDateTime>,
commission_state: &mut BTreeMap<u64, f64>,
report: &mut BrokerExecutionReport,
) -> Result<(), BacktestError> {
let price = data
@@ -838,10 +917,12 @@ where
data,
symbol,
current_qty - target_qty,
Self::reserve_order_id(next_order_id),
reason,
intraday_turnover,
execution_cursors,
global_execution_cursor,
commission_state,
report,
)?;
} else if target_qty > current_qty {
@@ -851,16 +932,19 @@ where
data,
symbol,
target_qty - current_qty,
Self::reserve_order_id(next_order_id),
reason,
intraday_turnover,
execution_cursors,
global_execution_cursor,
commission_state,
None,
report,
)?;
} else if (current_value - target_value).abs() <= f64::EPSILON {
report.order_events.push(OrderEvent {
date,
order_id: None,
symbol: symbol.to_string(),
side: if current_qty > 0 {
OrderSide::Sell
@@ -884,10 +968,12 @@ where
data: &DataSet,
symbol: &str,
value: f64,
next_order_id: &mut u64,
reason: &str,
intraday_turnover: &mut BTreeMap<String, u32>,
execution_cursors: &mut BTreeMap<String, NaiveDateTime>,
global_execution_cursor: &mut Option<NaiveDateTime>,
commission_state: &mut BTreeMap<u64, f64>,
report: &mut BrokerExecutionReport,
) -> Result<(), BacktestError> {
if value.abs() <= f64::EPSILON {
@@ -928,10 +1014,12 @@ where
data,
symbol,
requested_qty,
Self::reserve_order_id(next_order_id),
reason,
intraday_turnover,
execution_cursors,
global_execution_cursor,
commission_state,
Some(value.abs()),
report,
)
@@ -948,10 +1036,12 @@ where
data,
symbol,
requested_qty,
Self::reserve_order_id(next_order_id),
reason,
intraday_turnover,
execution_cursors,
global_execution_cursor,
commission_state,
report,
)
}
@@ -980,10 +1070,12 @@ where
data: &DataSet,
symbol: &str,
requested_qty: u32,
order_id: u64,
reason: &str,
intraday_turnover: &mut BTreeMap<String, u32>,
execution_cursors: &mut BTreeMap<String, NaiveDateTime>,
global_execution_cursor: &mut Option<NaiveDateTime>,
commission_state: &mut BTreeMap<u64, f64>,
value_budget: Option<f64>,
report: &mut BrokerExecutionReport,
) -> Result<(), BacktestError> {
@@ -996,6 +1088,7 @@ where
if !rule.allowed {
report.order_events.push(OrderEvent {
date,
order_id: Some(order_id),
symbol: symbol.to_string(),
side: OrderSide::Buy,
requested_quantity: requested_qty,
@@ -1020,6 +1113,7 @@ where
Err(limit_reason) => {
report.order_events.push(OrderEvent {
date,
order_id: Some(order_id),
symbol: symbol.to_string(),
side: OrderSide::Buy,
requested_quantity: requested_qty,
@@ -1069,6 +1163,7 @@ where
if filled_qty == 0 {
report.order_events.push(OrderEvent {
date,
order_id: Some(order_id),
symbol: symbol.to_string(),
side: OrderSide::Buy,
requested_quantity: requested_qty,
@@ -1081,9 +1176,13 @@ where
let cash_before = portfolio.cash();
let gross_amount = execution_price * filled_qty as f64;
let cost = self
.cost_model
.calculate(date, OrderSide::Buy, gross_amount);
let cost = self.cost_model.calculate_with_order_state(
date,
OrderSide::Buy,
gross_amount,
Some(order_id),
commission_state,
);
let cash_out = gross_amount + cost.total();
portfolio.apply_cash_delta(-cash_out);
@@ -1100,6 +1199,7 @@ where
report.order_events.push(OrderEvent {
date,
order_id: Some(order_id),
symbol: symbol.to_string(),
side: OrderSide::Buy,
requested_quantity: requested_qty,
@@ -1109,6 +1209,7 @@ where
});
report.fill_events.push(FillEvent {
date,
order_id: Some(order_id),
symbol: symbol.to_string(),
side: OrderSide::Buy,
quantity: filled_qty,