Align order costs and rebalance priority with rqalpha
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user