Expose account runtime metrics

This commit is contained in:
boris
2026-04-23 20:03:49 -07:00
parent 9f10afddec
commit e0a5d0c945
7 changed files with 367 additions and 19 deletions

View File

@@ -307,6 +307,7 @@ impl Position {
#[derive(Debug, Clone)]
pub struct PortfolioState {
initial_cash: f64,
cash: f64,
positions: IndexMap<String, Position>,
cash_receivables: Vec<CashReceivable>,
@@ -326,12 +327,21 @@ pub(crate) struct SuccessorConversionOutcome {
impl PortfolioState {
pub fn new(initial_cash: f64) -> Self {
Self {
initial_cash,
cash: initial_cash,
positions: IndexMap::new(),
cash_receivables: Vec::new(),
}
}
pub fn starting_cash(&self) -> f64 {
self.initial_cash
}
pub fn units(&self) -> f64 {
self.initial_cash
}
pub fn cash(&self) -> f64 {
self.cash
}
@@ -423,10 +433,72 @@ impl PortfolioState {
self.positions.values().map(Position::market_value).sum()
}
pub fn transaction_cost(&self) -> f64 {
self.positions
.values()
.map(Position::transaction_cost)
.sum()
}
pub fn trading_pnl(&self) -> f64 {
self.positions
.values()
.map(|position| position.trading_pnl)
.sum()
}
pub fn position_pnl(&self) -> f64 {
self.positions
.values()
.map(|position| position.position_pnl)
.sum()
}
pub fn daily_pnl(&self) -> f64 {
self.trading_pnl() + self.position_pnl()
}
pub fn total_equity(&self) -> f64 {
self.cash + self.market_value()
}
pub fn total_value(&self) -> f64 {
self.total_equity()
}
pub fn portfolio_value(&self) -> f64 {
self.total_equity()
}
pub fn unit_net_value(&self) -> f64 {
if self.initial_cash.abs() < f64::EPSILON {
0.0
} else {
self.total_equity() / self.initial_cash
}
}
pub fn static_unit_net_value(&self) -> f64 {
if self.initial_cash.abs() < f64::EPSILON {
0.0
} else {
(self.total_equity() - self.daily_pnl()) / self.initial_cash
}
}
pub fn daily_returns(&self) -> f64 {
let previous_value = self.total_equity() - self.daily_pnl();
if previous_value.abs() < f64::EPSILON {
0.0
} else {
self.daily_pnl() / previous_value
}
}
pub fn total_returns(&self) -> f64 {
self.unit_net_value() - 1.0
}
pub fn holdings_summary(&self, date: NaiveDate) -> Vec<HoldingSummary> {
let total_equity = self.total_equity();
self.positions
@@ -819,6 +891,47 @@ mod tests {
assert!((summary[0].transaction_cost - 3.0).abs() < 1e-6);
assert!(summary[0].value_percent > 0.0);
}
#[test]
fn portfolio_exposes_rqalpha_style_account_metrics() {
let prev_date = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
let date = NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
let mut portfolio = PortfolioState::new(10_000.0);
portfolio
.position_mut("000001.SZ")
.buy(prev_date, 100, 10.0);
portfolio.begin_trading_day();
portfolio.position_mut("000001.SZ").buy(date, 50, 11.0);
portfolio
.position_mut("000001.SZ")
.sell(40, 12.0)
.expect("sell");
portfolio.position_mut("000001.SZ").record_trade_cost(3.0);
assert!((portfolio.starting_cash() - 10_000.0).abs() < 1e-6);
assert!((portfolio.units() - 10_000.0).abs() < 1e-6);
assert!((portfolio.transaction_cost() - 3.0).abs() < 1e-6);
assert!((portfolio.trading_pnl() - 47.0).abs() < 1e-6);
assert!((portfolio.position_pnl() - 200.0).abs() < 1e-6);
assert!((portfolio.daily_pnl() - 247.0).abs() < 1e-6);
assert!((portfolio.total_value() - portfolio.total_equity()).abs() < 1e-6);
assert!((portfolio.portfolio_value() - portfolio.total_equity()).abs() < 1e-6);
assert!((portfolio.unit_net_value() - portfolio.total_equity() / 10_000.0).abs() < 1e-6);
assert!(
(portfolio.static_unit_net_value()
- (portfolio.total_equity() - portfolio.daily_pnl()) / 10_000.0)
.abs()
< 1e-6
);
assert!(
(portfolio.daily_returns()
- portfolio.daily_pnl() / (portfolio.total_equity() - portfolio.daily_pnl()))
.abs()
< 1e-6
);
assert!((portfolio.total_returns() - (portfolio.unit_net_value() - 1.0)).abs() < 1e-6);
assert_eq!(portfolio.cash_receivables().len(), 0);
}
}
#[derive(Debug, Clone, Serialize)]