Expose account runtime metrics
This commit is contained in:
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user