Expose account runtime metrics
This commit is contained in:
@@ -46,8 +46,8 @@ pub use scheduler::{
|
||||
};
|
||||
pub use strategy::{
|
||||
AlgoOrderStyle, CnSmallCapRotationConfig, CnSmallCapRotationStrategy, JqMicroCapConfig,
|
||||
JqMicroCapStrategy, OpenOrderView, OrderIntent, OrderRuntimeView, Strategy, StrategyContext,
|
||||
StrategyDecision, TargetPortfolioOrderPricing,
|
||||
JqMicroCapStrategy, OpenOrderView, OrderIntent, OrderRuntimeView, PortfolioRuntimeView,
|
||||
Strategy, StrategyContext, StrategyDecision, TargetPortfolioOrderPricing,
|
||||
};
|
||||
pub use strategy_ai::{
|
||||
ManualExample, ManualFactorSource, ManualField, ManualFieldGroup, ManualFunction,
|
||||
|
||||
@@ -281,8 +281,22 @@ struct DayExpressionState {
|
||||
signal_ma20: f64,
|
||||
signal_ma30: f64,
|
||||
cash: f64,
|
||||
available_cash: f64,
|
||||
frozen_cash: f64,
|
||||
market_value: f64,
|
||||
total_equity: f64,
|
||||
total_value: f64,
|
||||
portfolio_value: f64,
|
||||
starting_cash: f64,
|
||||
unit_net_value: f64,
|
||||
static_unit_net_value: f64,
|
||||
daily_pnl: f64,
|
||||
daily_returns: f64,
|
||||
total_returns: f64,
|
||||
transaction_cost: f64,
|
||||
trading_pnl: f64,
|
||||
position_pnl: f64,
|
||||
cash_liabilities: f64,
|
||||
current_exposure: f64,
|
||||
position_count: i64,
|
||||
max_positions: i64,
|
||||
@@ -455,8 +469,18 @@ impl PlatformExprStrategy {
|
||||
"benchmark_ma_long",
|
||||
"cash",
|
||||
"available_cash",
|
||||
"frozen_cash",
|
||||
"market_value",
|
||||
"total_equity",
|
||||
"total_value",
|
||||
"portfolio_value",
|
||||
"starting_cash",
|
||||
"unit_net_value",
|
||||
"static_unit_net_value",
|
||||
"daily_pnl",
|
||||
"daily_returns",
|
||||
"total_returns",
|
||||
"cash_liabilities",
|
||||
"current_exposure",
|
||||
"position_count",
|
||||
"max_positions",
|
||||
@@ -1033,14 +1057,10 @@ impl PlatformExprStrategy {
|
||||
.data
|
||||
.market_decision_close_moving_average(date, &self.config.signal_symbol, 30)
|
||||
.unwrap_or(benchmark_ma20);
|
||||
let cash = ctx.portfolio.cash();
|
||||
let market_value = ctx
|
||||
.portfolio
|
||||
.positions()
|
||||
.values()
|
||||
.map(|position| position.market_value())
|
||||
.sum::<f64>();
|
||||
let total_equity = cash + market_value;
|
||||
let account = ctx.account();
|
||||
let cash = account.cash;
|
||||
let market_value = account.market_value;
|
||||
let total_equity = account.total_equity;
|
||||
let current_exposure = if total_equity > 0.0 {
|
||||
market_value / total_equity
|
||||
} else {
|
||||
@@ -1066,8 +1086,22 @@ impl PlatformExprStrategy {
|
||||
signal_ma20: benchmark_ma20,
|
||||
signal_ma30: benchmark_ma30,
|
||||
cash,
|
||||
available_cash: account.available_cash,
|
||||
frozen_cash: account.frozen_cash,
|
||||
market_value,
|
||||
total_equity,
|
||||
total_value: account.total_value,
|
||||
portfolio_value: account.portfolio_value,
|
||||
starting_cash: account.starting_cash,
|
||||
unit_net_value: account.unit_net_value,
|
||||
static_unit_net_value: account.static_unit_net_value,
|
||||
daily_pnl: account.daily_pnl,
|
||||
daily_returns: account.daily_returns,
|
||||
total_returns: account.total_returns,
|
||||
transaction_cost: account.transaction_cost,
|
||||
trading_pnl: account.trading_pnl,
|
||||
position_pnl: account.position_pnl,
|
||||
cash_liabilities: account.cash_liabilities,
|
||||
current_exposure,
|
||||
position_count: ctx.portfolio.positions().len() as i64,
|
||||
max_positions: self.config.max_positions as i64,
|
||||
@@ -1231,9 +1265,22 @@ impl PlatformExprStrategy {
|
||||
scope.push("benchmark_ma_short", day.benchmark_ma_short);
|
||||
scope.push("benchmark_ma_long", day.benchmark_ma_long);
|
||||
scope.push("cash", day.cash);
|
||||
scope.push("available_cash", day.cash);
|
||||
scope.push("available_cash", day.available_cash);
|
||||
scope.push("frozen_cash", day.frozen_cash);
|
||||
scope.push("market_value", day.market_value);
|
||||
scope.push("total_equity", day.total_equity);
|
||||
scope.push("total_value", day.total_value);
|
||||
scope.push("portfolio_value", day.portfolio_value);
|
||||
scope.push("starting_cash", day.starting_cash);
|
||||
scope.push("unit_net_value", day.unit_net_value);
|
||||
scope.push("static_unit_net_value", day.static_unit_net_value);
|
||||
scope.push("daily_pnl", day.daily_pnl);
|
||||
scope.push("daily_returns", day.daily_returns);
|
||||
scope.push("total_returns", day.total_returns);
|
||||
scope.push("transaction_cost", day.transaction_cost);
|
||||
scope.push("trading_pnl", day.trading_pnl);
|
||||
scope.push("position_pnl", day.position_pnl);
|
||||
scope.push("cash_liabilities", day.cash_liabilities);
|
||||
scope.push("current_exposure", day.current_exposure);
|
||||
scope.push("position_count", day.position_count);
|
||||
scope.push("max_positions", day.max_positions);
|
||||
@@ -1343,8 +1390,31 @@ impl PlatformExprStrategy {
|
||||
Dynamic::from(day.benchmark_ma_long),
|
||||
);
|
||||
day_factors.insert("cash".into(), Dynamic::from(day.cash));
|
||||
day_factors.insert("available_cash".into(), Dynamic::from(day.available_cash));
|
||||
day_factors.insert("frozen_cash".into(), Dynamic::from(day.frozen_cash));
|
||||
day_factors.insert("market_value".into(), Dynamic::from(day.market_value));
|
||||
day_factors.insert("total_equity".into(), Dynamic::from(day.total_equity));
|
||||
day_factors.insert("total_value".into(), Dynamic::from(day.total_value));
|
||||
day_factors.insert("portfolio_value".into(), Dynamic::from(day.portfolio_value));
|
||||
day_factors.insert("starting_cash".into(), Dynamic::from(day.starting_cash));
|
||||
day_factors.insert("unit_net_value".into(), Dynamic::from(day.unit_net_value));
|
||||
day_factors.insert(
|
||||
"static_unit_net_value".into(),
|
||||
Dynamic::from(day.static_unit_net_value),
|
||||
);
|
||||
day_factors.insert("daily_pnl".into(), Dynamic::from(day.daily_pnl));
|
||||
day_factors.insert("daily_returns".into(), Dynamic::from(day.daily_returns));
|
||||
day_factors.insert("total_returns".into(), Dynamic::from(day.total_returns));
|
||||
day_factors.insert(
|
||||
"transaction_cost".into(),
|
||||
Dynamic::from(day.transaction_cost),
|
||||
);
|
||||
day_factors.insert("trading_pnl".into(), Dynamic::from(day.trading_pnl));
|
||||
day_factors.insert("position_pnl".into(), Dynamic::from(day.position_pnl));
|
||||
day_factors.insert(
|
||||
"cash_liabilities".into(),
|
||||
Dynamic::from(day.cash_liabilities),
|
||||
);
|
||||
day_factors.insert(
|
||||
"current_exposure".into(),
|
||||
Dynamic::from(day.current_exposure),
|
||||
@@ -5267,7 +5337,13 @@ mod tests {
|
||||
" && transaction_cost == 2.0 && position_market_value > 0.0",
|
||||
" && value_percent > 0.0 && unrealized_pnl > 0.0",
|
||||
" && realized_pnl > 0.0 && pnl > 0.0",
|
||||
" && day_trade_quantity_delta == 50 && trading_pnl > 90.0"
|
||||
" && day_trade_quantity_delta == 50 && trading_pnl > 90.0",
|
||||
" && available_cash == cash && frozen_cash == 0.0",
|
||||
" && total_value == total_equity && portfolio_value == total_equity",
|
||||
" && starting_cash == 1000000.0 && unit_net_value > 1.0",
|
||||
" && static_unit_net_value > 1.0 && daily_pnl > 290.0",
|
||||
" && daily_returns > 0.0 && total_returns > 0.0",
|
||||
" && cash_liabilities == 0.0"
|
||||
)
|
||||
.to_string();
|
||||
let mut strategy = PlatformExprStrategy::new(cfg);
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -95,6 +95,28 @@ pub struct OrderRuntimeView {
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PortfolioRuntimeView {
|
||||
pub starting_cash: f64,
|
||||
pub units: f64,
|
||||
pub cash: f64,
|
||||
pub available_cash: f64,
|
||||
pub frozen_cash: f64,
|
||||
pub market_value: f64,
|
||||
pub total_value: f64,
|
||||
pub portfolio_value: f64,
|
||||
pub total_equity: f64,
|
||||
pub unit_net_value: f64,
|
||||
pub static_unit_net_value: f64,
|
||||
pub daily_pnl: f64,
|
||||
pub daily_returns: f64,
|
||||
pub total_returns: f64,
|
||||
pub transaction_cost: f64,
|
||||
pub trading_pnl: f64,
|
||||
pub position_pnl: f64,
|
||||
pub cash_liabilities: f64,
|
||||
}
|
||||
|
||||
pub struct StrategyContext<'a> {
|
||||
pub execution_date: NaiveDate,
|
||||
pub decision_date: NaiveDate,
|
||||
@@ -323,6 +345,55 @@ impl StrategyContext<'_> {
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
|
||||
pub fn portfolio_view(&self) -> PortfolioRuntimeView {
|
||||
let frozen_cash = self.frozen_cash();
|
||||
let cash = self.portfolio.cash();
|
||||
let total_equity = self.portfolio.total_equity();
|
||||
PortfolioRuntimeView {
|
||||
starting_cash: self.portfolio.starting_cash(),
|
||||
units: self.portfolio.units(),
|
||||
cash,
|
||||
available_cash: (cash - frozen_cash).max(0.0),
|
||||
frozen_cash,
|
||||
market_value: self.portfolio.market_value(),
|
||||
total_value: self.portfolio.total_value(),
|
||||
portfolio_value: self.portfolio.portfolio_value(),
|
||||
total_equity,
|
||||
unit_net_value: self.portfolio.unit_net_value(),
|
||||
static_unit_net_value: self.portfolio.static_unit_net_value(),
|
||||
daily_pnl: self.portfolio.daily_pnl(),
|
||||
daily_returns: self.portfolio.daily_returns(),
|
||||
total_returns: self.portfolio.total_returns(),
|
||||
transaction_cost: self.portfolio.transaction_cost(),
|
||||
trading_pnl: self.portfolio.trading_pnl(),
|
||||
position_pnl: self.portfolio.position_pnl(),
|
||||
cash_liabilities: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn account(&self) -> PortfolioRuntimeView {
|
||||
self.portfolio_view()
|
||||
}
|
||||
|
||||
pub fn frozen_cash(&self) -> f64 {
|
||||
self.open_orders
|
||||
.iter()
|
||||
.filter(|order| order.side == OrderSide::Buy)
|
||||
.map(|order| {
|
||||
let price = if order.limit_price.is_finite() {
|
||||
order.limit_price.max(0.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
order.remaining_quantity as f64 * price
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
pub fn available_cash(&self) -> f64 {
|
||||
(self.portfolio.cash() - self.frozen_cash()).max(0.0)
|
||||
}
|
||||
|
||||
pub fn available_sellable_qty(&self, symbol: &str, raw_sellable_qty: u32) -> u32 {
|
||||
raw_sellable_qty.saturating_sub(self.symbol_open_sell_quantity(symbol))
|
||||
}
|
||||
|
||||
@@ -140,7 +140,9 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
ManualField { name: "benchmark_open/benchmark_close".to_string(), field_type: "float".to_string(), detail: "基准当日开盘价与前一日收盘价。".to_string() },
|
||||
ManualField { name: "signal_ma5/signal_ma10/signal_ma20/signal_ma30".to_string(), field_type: "float".to_string(), detail: "信号指数滚动均线。".to_string() },
|
||||
ManualField { name: "benchmark_ma5/benchmark_ma10/benchmark_ma20/benchmark_ma30".to_string(), field_type: "float".to_string(), detail: "基准指数滚动均线。".to_string() },
|
||||
ManualField { name: "cash/available_cash/market_value/total_equity".to_string(), field_type: "float".to_string(), detail: "账户资金与总资产。".to_string() },
|
||||
ManualField { name: "cash/available_cash/frozen_cash/market_value/total_equity".to_string(), field_type: "float".to_string(), detail: "账户可用资金、挂单冻结资金、市值与总权益;available_cash 会扣减当前买入挂单冻结估算。".to_string() },
|
||||
ManualField { name: "total_value/portfolio_value/starting_cash/unit_net_value/static_unit_net_value".to_string(), field_type: "float".to_string(), detail: "组合总权益别名、初始资金、实时净值和昨日静态净值,对齐 RQAlpha Portfolio 常用字段。".to_string() },
|
||||
ManualField { name: "daily_pnl/daily_returns/total_returns/transaction_cost/trading_pnl/position_pnl/cash_liabilities".to_string(), field_type: "float".to_string(), detail: "账户当日盈亏、日收益率、累计收益率、当日交易成本、交易盈亏、持仓盈亏和现金负债;股票账户现金负债默认为 0。".to_string() },
|
||||
ManualField { name: "position_count/max_positions/refresh_rate".to_string(), field_type: "int".to_string(), detail: "仓位计数与调仓周期。".to_string() },
|
||||
ManualField { name: "has_open_orders/open_order_count/open_buy_order_count/open_sell_order_count".to_string(), field_type: "bool/int".to_string(), detail: "当前阶段挂单簿摘要。".to_string() },
|
||||
ManualField { name: "open_buy_qty/open_sell_qty/latest_open_order_id".to_string(), field_type: "int".to_string(), detail: "当前阶段未成交买卖挂单的剩余数量汇总,以及最近一笔挂单 id。".to_string() },
|
||||
@@ -205,6 +207,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
ManualFunction { name: "is_suspended/is_st_stock".to_string(), signature: "ctx.is_suspended(symbol, count)".to_string(), detail: "读取指定证券截至当前交易日最近 count 个交易日的停牌或 ST 标记,返回 bool 序列,顺序从旧到新;对应 RQAlpha 的 is_suspended/is_st_stock 数据源能力。".to_string() },
|
||||
ManualFunction { name: "get_price".to_string(), signature: "ctx.get_price(symbol, start_date, end_date, \"1d\" | \"1m\" | \"tick\")".to_string(), detail: "按日期区间读取统一 PriceBar 序列。日线返回 open/high/low/close/last/volume/盘口字段;分钟或 tick 返回按 timestamp 排序的 last/bid1/ask1/volume_delta/amount_delta 映射,便于服务层转成表格或前端明细。".to_string() },
|
||||
ManualFunction { name: "order/order_status/order_avg_price/order_transaction_cost".to_string(), signature: "ctx.order(order_id)".to_string(), detail: "按订单 id 查询运行时订单对象,支持已结束订单和当前挂单。返回字段包括 status、filled_quantity、unfilled_quantity、avg_price、transaction_cost、symbol、side、reason;可用便捷函数读取状态、成交均价和费用,对齐 RQAlpha Order 的核心属性。".to_string() },
|
||||
ManualFunction { name: "account/portfolio_view".to_string(), signature: "ctx.account()".to_string(), detail: "返回当前股票账户/组合运行时视图,字段包括 cash、available_cash、frozen_cash、market_value、total_value、unit_net_value、daily_pnl、daily_returns、total_returns、transaction_cost、trading_pnl、position_pnl 等;DSL 中同名字段可直接使用。".to_string() },
|
||||
ManualFunction { name: "rolling_mean".to_string(), signature: "rolling_mean(\"field\", lookback)".to_string(), detail: "任意字段滚动均值,支持 volume/amount/turnover_ratio、signal_open/signal_close、benchmark_open/benchmark_close 等。任意成交量窗口推荐用它,比如 rolling_mean(\"volume\", 15)。".to_string() },
|
||||
ManualFunction { name: "sma".to_string(), signature: "sma(\"field\", lookback)".to_string(), detail: "rolling_mean 的别名。任意价格均线窗口推荐用它,比如 sma(\"close\", 15)。".to_string() },
|
||||
ManualFunction { name: "round/floor/ceil/abs/min/max/clamp".to_string(), signature: "round(x)".to_string(), detail: "常用数值函数。".to_string() },
|
||||
|
||||
@@ -6,8 +6,9 @@ use chrono::{NaiveDate, NaiveDateTime};
|
||||
use fidc_core::{
|
||||
BacktestConfig, BacktestEngine, BenchmarkSnapshot, BrokerSimulator, CandidateEligibility,
|
||||
ChinaAShareCostModel, ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet,
|
||||
Instrument, IntradayExecutionQuote, OrderIntent, PriceField, ProcessEventKind, ScheduleRule,
|
||||
ScheduleStage, ScheduleTimeRule, Strategy, StrategyContext, StrategyDecision,
|
||||
Instrument, IntradayExecutionQuote, OpenOrderView, OrderIntent, OrderSide, OrderStatus,
|
||||
PortfolioState, PriceField, ProcessEventKind, ScheduleRule, ScheduleStage, ScheduleTimeRule,
|
||||
Strategy, StrategyContext, StrategyDecision,
|
||||
};
|
||||
|
||||
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
|
||||
@@ -1226,6 +1227,79 @@ fn strategy_context_exposes_final_order_runtime_view() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strategy_context_exposes_rqalpha_style_account_runtime_view() {
|
||||
let prev_date = d(2025, 1, 2);
|
||||
let date = d(2025, 1, 3);
|
||||
let data = DataSet::from_components(
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
Vec::new(),
|
||||
vec![BenchmarkSnapshot {
|
||||
date,
|
||||
benchmark: "000300.SH".to_string(),
|
||||
open: 100.0,
|
||||
close: 100.0,
|
||||
prev_close: 99.0,
|
||||
volume: 1_000_000,
|
||||
}],
|
||||
)
|
||||
.expect("dataset");
|
||||
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);
|
||||
let open_orders = vec![OpenOrderView {
|
||||
order_id: 7,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
side: OrderSide::Buy,
|
||||
requested_quantity: 100,
|
||||
filled_quantity: 0,
|
||||
remaining_quantity: 50,
|
||||
unfilled_quantity: 50,
|
||||
status: OrderStatus::Pending,
|
||||
avg_price: 0.0,
|
||||
transaction_cost: 0.0,
|
||||
limit_price: 12.0,
|
||||
reason: "pending_buy".to_string(),
|
||||
}];
|
||||
let subscriptions = BTreeSet::new();
|
||||
let ctx = StrategyContext {
|
||||
execution_date: date,
|
||||
decision_date: date,
|
||||
decision_index: 0,
|
||||
data: &data,
|
||||
portfolio: &portfolio,
|
||||
open_orders: &open_orders,
|
||||
dynamic_universe: None,
|
||||
subscriptions: &subscriptions,
|
||||
process_events: &[],
|
||||
active_process_event: None,
|
||||
active_datetime: None,
|
||||
order_events: &[],
|
||||
fills: &[],
|
||||
};
|
||||
|
||||
let account = ctx.account();
|
||||
|
||||
assert!((account.starting_cash - 10_000.0).abs() < 1e-6);
|
||||
assert!((account.frozen_cash - 600.0).abs() < 1e-6);
|
||||
assert!((account.available_cash - 9_400.0).abs() < 1e-6);
|
||||
assert!((account.transaction_cost - 3.0).abs() < 1e-6);
|
||||
assert!((account.daily_pnl - 247.0).abs() < 1e-6);
|
||||
assert!((account.daily_returns - portfolio.daily_returns()).abs() < 1e-6);
|
||||
assert!((account.total_returns - portfolio.total_returns()).abs() < 1e-6);
|
||||
assert!((ctx.available_cash() - account.available_cash).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_rejects_pending_limit_orders_at_market_close() {
|
||||
let date1 = d(2025, 1, 2);
|
||||
|
||||
@@ -70,6 +70,17 @@ current alignment pass.
|
||||
- [x] final order object lookup by order id
|
||||
- [x] order average fill price and transaction cost aggregation
|
||||
|
||||
### Phase 9: Account / portfolio API parity
|
||||
|
||||
- [x] stock-account runtime view (`ctx.account()` / `ctx.portfolio_view()`)
|
||||
- [x] `cash`, `available_cash`, `frozen_cash`, `market_value`, `total_value`
|
||||
exposed to strategy runtime and DSL
|
||||
- [x] `unit_net_value`, `static_unit_net_value`, `daily_pnl`,
|
||||
`daily_returns`, `total_returns`, `transaction_cost`, `trading_pnl`,
|
||||
and `position_pnl` exposed to strategy runtime and DSL
|
||||
- [ ] explicit deposit / withdraw API
|
||||
- [ ] financing liability / repay API
|
||||
|
||||
## Execution Order
|
||||
|
||||
1. Close the explicit order API gap with target-shares / `order_to` parity.
|
||||
@@ -80,10 +91,10 @@ current alignment pass.
|
||||
6. Finish position accounting parity.
|
||||
7. Continue stock data-source API parity.
|
||||
8. Continue order object API parity.
|
||||
9. Continue parity audit for remaining account APIs.
|
||||
9. Continue account / portfolio API parity.
|
||||
|
||||
## Current Step
|
||||
|
||||
Active implementation target: continue parity audit for remaining account APIs
|
||||
after order object lookup, status, unfilled quantity, average fill price, and
|
||||
transaction cost aggregation are covered.
|
||||
Active implementation target: continue account parity after exposing the stock
|
||||
account runtime view and core Portfolio fields; next gaps are explicit
|
||||
deposit/withdraw and financing liability APIs.
|
||||
|
||||
Reference in New Issue
Block a user