Add account cash flow intents

This commit is contained in:
boris
2026-04-23 20:14:05 -07:00
parent e0a5d0c945
commit 85feee6dac
10 changed files with 608 additions and 27 deletions

View File

@@ -157,10 +157,11 @@ where
execution_date: NaiveDate,
decision_date: NaiveDate,
decision_index: usize,
portfolio: &PortfolioState,
portfolio: &mut PortfolioState,
open_orders: &[crate::strategy::OpenOrderView],
process_events: &mut Vec<ProcessEvent>,
decision: &mut crate::strategy::StrategyDecision,
directive_report: &mut BrokerExecutionReport,
) -> Result<(), BacktestError> {
if decision.order_intents.is_empty() {
return Ok(());
@@ -282,6 +283,119 @@ where
)?;
}
}
crate::strategy::OrderIntent::DepositWithdraw {
amount,
receiving_days,
reason,
} => {
let cash_before = portfolio.cash();
if receiving_days == 0 {
portfolio
.deposit_withdraw(amount)
.map_err(BacktestError::Execution)?;
directive_report.account_events.push(AccountEvent {
date: execution_date,
cash_before,
cash_after: portfolio.cash(),
total_equity: portfolio.total_equity(),
note: format!("deposit_withdraw amount={amount:.2} reason={reason}"),
});
} else {
let payable_date = self
.data
.next_trading_date(execution_date, receiving_days)
.ok_or_else(|| {
BacktestError::Execution(format!(
"no trading date for deposit_withdraw receiving_days={receiving_days} from {execution_date}"
))
})?;
portfolio
.schedule_deposit_withdraw(payable_date, amount, reason.clone())
.map_err(BacktestError::Execution)?;
directive_report.account_events.push(AccountEvent {
date: execution_date,
cash_before,
cash_after: portfolio.cash(),
total_equity: portfolio.total_equity(),
note: format!(
"deposit_withdraw_scheduled amount={amount:.2} payable_date={payable_date} reason={reason}"
),
});
}
decision.diagnostics.push(format!(
"account_deposit_withdraw amount={amount:.2} receiving_days={receiving_days}"
));
publish_custom_process_event(
&mut self.strategy,
&mut self.process_event_bus,
execution_date,
decision_date,
decision_index,
&self.data,
&*portfolio,
open_orders,
self.dynamic_universe.as_ref(),
&self.subscriptions,
process_events,
ProcessEvent {
date: execution_date,
kind: ProcessEventKind::AccountDepositWithdraw,
order_id: None,
symbol: None,
side: None,
detail: format!(
"reason={reason} amount={amount:.2} receiving_days={receiving_days} cash_before={cash_before:.2} cash_after={:.2}",
portfolio.cash()
),
},
)?;
}
crate::strategy::OrderIntent::FinanceRepay { amount, reason } => {
let cash_before = portfolio.cash();
let liabilities_before = portfolio.cash_liabilities();
portfolio
.finance_repay(amount)
.map_err(BacktestError::Execution)?;
directive_report.account_events.push(AccountEvent {
date: execution_date,
cash_before,
cash_after: portfolio.cash(),
total_equity: portfolio.total_equity(),
note: format!(
"finance_repay amount={amount:.2} liabilities_before={liabilities_before:.2} liabilities_after={:.2} reason={reason}",
portfolio.cash_liabilities()
),
});
decision.diagnostics.push(format!(
"account_finance_repay amount={amount:.2} liabilities={:.2}",
portfolio.cash_liabilities()
));
publish_custom_process_event(
&mut self.strategy,
&mut self.process_event_bus,
execution_date,
decision_date,
decision_index,
&self.data,
&*portfolio,
open_orders,
self.dynamic_universe.as_ref(),
&self.subscriptions,
process_events,
ProcessEvent {
date: execution_date,
kind: ProcessEventKind::AccountFinanceRepay,
order_id: None,
symbol: None,
side: None,
detail: format!(
"reason={reason} amount={amount:.2} cash_before={cash_before:.2} cash_after={:.2} liabilities_before={liabilities_before:.2} liabilities_after={:.2}",
portfolio.cash(),
portfolio.cash_liabilities()
),
},
)?;
}
other => retained.push(other),
}
}
@@ -352,6 +466,12 @@ where
for (execution_idx, execution_date) in execution_dates.iter().copied().enumerate() {
let mut corporate_action_notes = Vec::new();
portfolio.begin_trading_day();
let pending_cash_flow_report = self.settle_pending_cash_flows(
execution_date,
&mut portfolio,
&mut corporate_action_notes,
);
self.extend_result(&mut result, pending_cash_flow_report);
let receivable_report = self.settle_cash_receivables(
execution_date,
&mut portfolio,
@@ -377,6 +497,7 @@ where
let (decision_index, decision_date) =
decision_slot.unwrap_or((execution_idx, execution_date));
let mut process_events = Vec::new();
let mut directive_report = BrokerExecutionReport::default();
let pre_open_orders = self.broker.open_order_views();
let schedule_rules = self.strategy.schedule_rules();
publish_phase_event(
@@ -452,10 +573,11 @@ where
execution_date,
decision_date,
decision_index,
&portfolio,
&mut portfolio,
&pre_open_orders,
&mut process_events,
&mut before_trading_decision,
&mut directive_report,
)?;
publish_phase_event(
&mut self.strategy,
@@ -546,10 +668,11 @@ where
execution_date,
decision_date,
decision_index,
&portfolio,
&mut portfolio,
&pre_open_orders,
&mut process_events,
&mut auction_decision,
&mut directive_report,
)?;
let mut report = self.broker.execute(
execution_date,
@@ -738,10 +861,11 @@ where
execution_date,
decision_date,
decision_index,
&portfolio,
&mut portfolio,
&on_day_open_orders,
&mut process_events,
&mut decision,
&mut directive_report,
)?;
let mut intraday_report =
@@ -888,10 +1012,11 @@ where
execution_date,
decision_date,
decision_index,
&portfolio,
&mut portfolio,
&tick_open_orders,
&mut process_events,
&mut tick_decision,
&mut directive_report,
)?;
let mut tick_report = self.broker.execute_between(
execution_date,
@@ -1024,10 +1149,11 @@ where
execution_date,
decision_date,
decision_index,
&portfolio,
&mut portfolio,
&post_trade_open_orders,
&mut process_events,
&mut after_trading_decision,
&mut directive_report,
)?;
let mut close_report = self.broker.after_trading(execution_date);
publish_process_events(
@@ -1151,10 +1277,11 @@ where
execution_date,
decision_date,
decision_index,
&portfolio,
&mut portfolio,
&post_close_open_orders,
&mut process_events,
&mut settlement_decision,
&mut directive_report,
)?;
publish_phase_event(
&mut self.strategy,
@@ -1172,6 +1299,7 @@ where
ProcessEventKind::PostSettlement,
"settlement:post",
)?;
merge_broker_report(&mut report, directive_report);
let daily_fill_count = report.fill_events.len();
let day_orders = report.order_events.clone();
let day_fills = report.fill_events.clone();
@@ -1542,6 +1670,31 @@ where
Ok(report)
}
fn settle_pending_cash_flows(
&self,
date: NaiveDate,
portfolio: &mut PortfolioState,
notes: &mut Vec<String>,
) -> BrokerExecutionReport {
let mut report = BrokerExecutionReport::default();
for flow in portfolio.settle_pending_cash_flows(date) {
let cash_before = portfolio.cash() - flow.amount;
let note = format!(
"deposit_withdraw_settled amount={:.2} payable_date={} reason={}",
flow.amount, flow.payable_date, flow.reason
);
notes.push(note.clone());
report.account_events.push(AccountEvent {
date,
cash_before,
cash_after: portfolio.cash(),
total_equity: portfolio.total_equity(),
note,
});
}
report
}
fn settle_delisted_positions(
&self,
date: NaiveDate,