Add smart target-portfolio order intent
This commit is contained in:
@@ -756,6 +756,25 @@ where
|
||||
commission_state,
|
||||
report,
|
||||
),
|
||||
OrderIntent::TargetPortfolioSmart {
|
||||
target_weights,
|
||||
order_prices,
|
||||
valuation_prices,
|
||||
reason,
|
||||
} => self.process_target_portfolio_smart(
|
||||
date,
|
||||
portfolio,
|
||||
data,
|
||||
target_weights,
|
||||
order_prices.as_ref(),
|
||||
valuation_prices.as_ref(),
|
||||
reason,
|
||||
intraday_turnover,
|
||||
execution_cursors,
|
||||
global_execution_cursor,
|
||||
commission_state,
|
||||
report,
|
||||
),
|
||||
OrderIntent::CancelOrder { order_id, reason } => {
|
||||
self.cancel_open_order(date, *order_id, reason, report);
|
||||
Ok(())
|
||||
@@ -929,6 +948,15 @@ where
|
||||
.retain(|existing| existing.order_id != order_id);
|
||||
}
|
||||
|
||||
fn extend_report(into: &mut BrokerExecutionReport, mut other: BrokerExecutionReport) {
|
||||
into.order_events.append(&mut other.order_events);
|
||||
into.fill_events.append(&mut other.fill_events);
|
||||
into.position_events.append(&mut other.position_events);
|
||||
into.account_events.append(&mut other.account_events);
|
||||
into.process_events.append(&mut other.process_events);
|
||||
into.diagnostics.append(&mut other.diagnostics);
|
||||
}
|
||||
|
||||
fn reserved_open_sell_quantity(&self, symbol: &str, exclude_order_id: Option<u64>) -> u32 {
|
||||
self.open_orders
|
||||
.borrow()
|
||||
@@ -1187,12 +1215,25 @@ where
|
||||
data: &DataSet,
|
||||
target_weights: &BTreeMap<String, f64>,
|
||||
) -> Result<(BTreeMap<String, u32>, Vec<String>), BacktestError> {
|
||||
let equity = self.rebalance_total_equity_at(date, portfolio, data)?;
|
||||
self.target_quantities_with_valuation_prices(date, portfolio, data, target_weights, None)
|
||||
}
|
||||
|
||||
fn target_quantities_with_valuation_prices(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
portfolio: &PortfolioState,
|
||||
data: &DataSet,
|
||||
target_weights: &BTreeMap<String, f64>,
|
||||
valuation_prices: Option<&BTreeMap<String, f64>>,
|
||||
) -> Result<(BTreeMap<String, u32>, Vec<String>), BacktestError> {
|
||||
let equity =
|
||||
self.rebalance_total_equity_at_with_overrides(date, portfolio, data, valuation_prices)?;
|
||||
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 = self.rebalance_valuation_price(date, symbol, data)?;
|
||||
let price =
|
||||
self.rebalance_valuation_price_with_overrides(date, symbol, data, valuation_prices)?;
|
||||
let raw_qty = ((equity * weight) / price).floor() as u32;
|
||||
desired_targets.insert(
|
||||
symbol.clone(),
|
||||
@@ -1216,7 +1257,12 @@ where
|
||||
.map(|pos| pos.quantity)
|
||||
.unwrap_or(0);
|
||||
let desired_qty = *desired_targets.get(&symbol).unwrap_or(&0);
|
||||
let price = self.rebalance_valuation_price(date, &symbol, data)?;
|
||||
let price = self.rebalance_valuation_price_with_overrides(
|
||||
date,
|
||||
&symbol,
|
||||
data,
|
||||
valuation_prices,
|
||||
)?;
|
||||
let minimum_order_quantity = self.minimum_order_quantity(data, &symbol);
|
||||
let order_step_size = self.order_step_size(data, &symbol);
|
||||
let min_target_qty = self.minimum_target_quantity(
|
||||
@@ -1411,6 +1457,123 @@ where
|
||||
Ok((best_targets, diagnostics))
|
||||
}
|
||||
|
||||
fn process_target_portfolio_smart(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
portfolio: &mut PortfolioState,
|
||||
data: &DataSet,
|
||||
target_weights: &BTreeMap<String, f64>,
|
||||
order_prices: Option<&BTreeMap<String, f64>>,
|
||||
valuation_prices: Option<&BTreeMap<String, f64>>,
|
||||
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 (target_quantities, diagnostics) = self.target_quantities_with_valuation_prices(
|
||||
date,
|
||||
portfolio,
|
||||
data,
|
||||
target_weights,
|
||||
valuation_prices,
|
||||
)?;
|
||||
report.diagnostics.extend(diagnostics);
|
||||
|
||||
let mut symbols = BTreeSet::new();
|
||||
symbols.extend(portfolio.positions().keys().cloned());
|
||||
symbols.extend(target_quantities.keys().cloned());
|
||||
|
||||
for symbol in &symbols {
|
||||
let current_qty = portfolio.position(symbol).map(|pos| pos.quantity).unwrap_or(0);
|
||||
let target_qty = target_quantities.get(symbol).copied().unwrap_or(0);
|
||||
if current_qty <= target_qty {
|
||||
continue;
|
||||
}
|
||||
let sell_qty = current_qty - target_qty;
|
||||
let mut local_report = BrokerExecutionReport::default();
|
||||
if let Some(limit_price) =
|
||||
self.required_custom_order_price(date, symbol, order_prices)?
|
||||
{
|
||||
self.process_limit_shares(
|
||||
date,
|
||||
portfolio,
|
||||
data,
|
||||
symbol,
|
||||
-(sell_qty as i32),
|
||||
limit_price,
|
||||
reason,
|
||||
intraday_turnover,
|
||||
execution_cursors,
|
||||
global_execution_cursor,
|
||||
commission_state,
|
||||
&mut local_report,
|
||||
)?;
|
||||
} else {
|
||||
self.process_shares(
|
||||
date,
|
||||
portfolio,
|
||||
data,
|
||||
symbol,
|
||||
-(sell_qty as i32),
|
||||
reason,
|
||||
intraday_turnover,
|
||||
execution_cursors,
|
||||
global_execution_cursor,
|
||||
commission_state,
|
||||
&mut local_report,
|
||||
)?;
|
||||
}
|
||||
Self::extend_report(report, local_report);
|
||||
}
|
||||
|
||||
for symbol in &symbols {
|
||||
let current_qty = portfolio.position(symbol).map(|pos| pos.quantity).unwrap_or(0);
|
||||
let target_qty = target_quantities.get(symbol).copied().unwrap_or(0);
|
||||
if target_qty <= current_qty {
|
||||
continue;
|
||||
}
|
||||
let buy_qty = target_qty - current_qty;
|
||||
let mut local_report = BrokerExecutionReport::default();
|
||||
if let Some(limit_price) =
|
||||
self.required_custom_order_price(date, symbol, order_prices)?
|
||||
{
|
||||
self.process_limit_shares(
|
||||
date,
|
||||
portfolio,
|
||||
data,
|
||||
symbol,
|
||||
buy_qty as i32,
|
||||
limit_price,
|
||||
reason,
|
||||
intraday_turnover,
|
||||
execution_cursors,
|
||||
global_execution_cursor,
|
||||
commission_state,
|
||||
&mut local_report,
|
||||
)?;
|
||||
} else {
|
||||
self.process_shares(
|
||||
date,
|
||||
portfolio,
|
||||
data,
|
||||
symbol,
|
||||
buy_qty as i32,
|
||||
reason,
|
||||
intraday_turnover,
|
||||
execution_cursors,
|
||||
global_execution_cursor,
|
||||
commission_state,
|
||||
&mut local_report,
|
||||
)?;
|
||||
}
|
||||
Self::extend_report(report, local_report);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn minimum_target_quantity(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
@@ -3380,12 +3543,23 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn rebalance_valuation_price(
|
||||
fn rebalance_valuation_price_with_overrides(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
symbol: &str,
|
||||
data: &DataSet,
|
||||
valuation_prices: Option<&BTreeMap<String, f64>>,
|
||||
) -> Result<f64, BacktestError> {
|
||||
if let Some(prices) = valuation_prices {
|
||||
if let Some(price) = prices.get(symbol).copied().filter(|price| *price > 0.0) {
|
||||
return Ok(price);
|
||||
}
|
||||
return Err(BacktestError::MissingPrice {
|
||||
date,
|
||||
symbol: symbol.to_string(),
|
||||
field: "custom valuation",
|
||||
});
|
||||
}
|
||||
let snapshot = data
|
||||
.market(date, symbol)
|
||||
.ok_or_else(|| BacktestError::MissingPrice {
|
||||
@@ -3406,28 +3580,50 @@ where
|
||||
date: NaiveDate,
|
||||
portfolio: &PortfolioState,
|
||||
data: &DataSet,
|
||||
) -> Result<f64, BacktestError> {
|
||||
self.rebalance_total_equity_at_with_overrides(date, portfolio, data, None)
|
||||
}
|
||||
|
||||
fn rebalance_total_equity_at_with_overrides(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
portfolio: &PortfolioState,
|
||||
data: &DataSet,
|
||||
valuation_prices: Option<&BTreeMap<String, f64>>,
|
||||
) -> Result<f64, BacktestError> {
|
||||
let mut market_value = 0.0;
|
||||
for position in portfolio.positions().values() {
|
||||
let snapshot =
|
||||
data.market(date, &position.symbol)
|
||||
.ok_or_else(|| BacktestError::MissingPrice {
|
||||
date,
|
||||
symbol: position.symbol.clone(),
|
||||
field: self.rebalance_valuation_price_field_name(),
|
||||
})?;
|
||||
let price = self
|
||||
.rebalance_valuation_price_for_snapshot(snapshot)
|
||||
.ok_or_else(|| BacktestError::MissingPrice {
|
||||
date,
|
||||
symbol: position.symbol.clone(),
|
||||
field: self.rebalance_valuation_price_field_name(),
|
||||
})?;
|
||||
let price = self.rebalance_valuation_price_with_overrides(
|
||||
date,
|
||||
&position.symbol,
|
||||
data,
|
||||
valuation_prices,
|
||||
)?;
|
||||
market_value += price * position.quantity as f64;
|
||||
}
|
||||
Ok(portfolio.cash() + market_value)
|
||||
}
|
||||
|
||||
fn required_custom_order_price(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
symbol: &str,
|
||||
order_prices: Option<&BTreeMap<String, f64>>,
|
||||
) -> Result<Option<f64>, BacktestError> {
|
||||
let Some(prices) = order_prices else {
|
||||
return Ok(None);
|
||||
};
|
||||
if let Some(price) = prices.get(symbol).copied().filter(|price| *price > 0.0) {
|
||||
Ok(Some(price))
|
||||
} else {
|
||||
Err(BacktestError::MissingPrice {
|
||||
date,
|
||||
symbol: symbol.to_string(),
|
||||
field: "custom order",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn round_buy_quantity(
|
||||
&self,
|
||||
quantity: u32,
|
||||
|
||||
@@ -364,6 +364,12 @@ pub enum OrderIntent {
|
||||
limit_price: f64,
|
||||
reason: String,
|
||||
},
|
||||
TargetPortfolioSmart {
|
||||
target_weights: BTreeMap<String, f64>,
|
||||
order_prices: Option<BTreeMap<String, f64>>,
|
||||
valuation_prices: Option<BTreeMap<String, f64>>,
|
||||
reason: String,
|
||||
},
|
||||
CancelOrder {
|
||||
order_id: u64,
|
||||
reason: String,
|
||||
|
||||
Reference in New Issue
Block a user