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

@@ -917,6 +917,22 @@ where
));
Ok(())
}
OrderIntent::DepositWithdraw {
amount,
receiving_days,
reason,
} => {
report.diagnostics.push(format!(
"engine_account_intent_skipped kind=deposit_withdraw amount={amount:.2} receiving_days={receiving_days} reason={reason}"
));
Ok(())
}
OrderIntent::FinanceRepay { amount, reason } => {
report.diagnostics.push(format!(
"engine_account_intent_skipped kind=finance_repay amount={amount:.2} reason={reason}"
));
Ok(())
}
}
}

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,

View File

@@ -148,6 +148,8 @@ pub enum ProcessEventKind {
UniverseUpdated,
UniverseSubscribed,
UniverseUnsubscribed,
AccountDepositWithdraw,
AccountFinanceRepay,
}
impl ProcessEventKind {
@@ -187,6 +189,8 @@ impl ProcessEventKind {
Self::UniverseUpdated => "universe_updated",
Self::UniverseSubscribed => "universe_subscribed",
Self::UniverseUnsubscribed => "universe_unsubscribed",
Self::AccountDepositWithdraw => "account_deposit_withdraw",
Self::AccountFinanceRepay => "account_finance_repay",
}
}
}

View File

@@ -35,11 +35,12 @@ pub use events::{
pub use instrument::Instrument;
pub use metrics::{BacktestMetrics, compute_backtest_metrics};
pub use platform_expr_strategy::{
PlatformExplicitActionStage, PlatformExplicitCancelKind, PlatformExplicitOrderKind,
PlatformExprStrategy, PlatformExprStrategyConfig, PlatformRebalanceSchedule,
PlatformScheduleFrequency, PlatformTradeAction, PlatformUniverseActionKind,
PlatformAccountActionKind, PlatformExplicitActionStage, PlatformExplicitCancelKind,
PlatformExplicitOrderKind, PlatformExprStrategy, PlatformExprStrategyConfig,
PlatformRebalanceSchedule, PlatformScheduleFrequency, PlatformTradeAction,
PlatformUniverseActionKind,
};
pub use portfolio::{CashReceivable, HoldingSummary, PortfolioState, Position};
pub use portfolio::{CashReceivable, HoldingSummary, PendingCashFlow, PortfolioState, Position};
pub use rules::{ChinaEquityRuleHooks, EquityRuleHooks, RuleCheck};
pub use scheduler::{
ScheduleFrequency, ScheduleRule, ScheduleStage, ScheduleTimeRule, Scheduler, default_stage_time,

View File

@@ -114,6 +114,12 @@ pub enum PlatformUniverseActionKind {
Unsubscribe,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlatformAccountActionKind {
DepositWithdraw,
FinanceRepay,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlatformTradeAction {
Order {
@@ -139,6 +145,13 @@ pub enum PlatformTradeAction {
when_expr: Option<String>,
reason: String,
},
Account {
kind: PlatformAccountActionKind,
amount_expr: String,
receiving_days_expr: Option<String>,
when_expr: Option<String>,
reason: String,
},
Cancel {
kind: PlatformExplicitCancelKind,
symbol: Option<String>,
@@ -3078,6 +3091,42 @@ impl PlatformExprStrategy {
},
});
}
PlatformTradeAction::Account {
kind,
amount_expr,
receiving_days_expr,
when_expr,
reason,
} => {
if !self.action_when_matches(ctx, day, None, when_expr.as_deref())? {
continue;
}
let amount = self.eval_float(ctx, amount_expr, day, None, None)?;
if amount.abs() <= f64::EPSILON {
continue;
}
match kind {
PlatformAccountActionKind::DepositWithdraw => {
let receiving_days = receiving_days_expr
.as_deref()
.map(|expr| self.eval_i32(ctx, expr, day, None, None))
.transpose()?
.unwrap_or(0)
.max(0) as usize;
intents.push(OrderIntent::DepositWithdraw {
amount,
receiving_days,
reason: reason.clone(),
});
}
PlatformAccountActionKind::FinanceRepay => {
intents.push(OrderIntent::FinanceRepay {
amount,
reason: reason.clone(),
});
}
}
}
PlatformTradeAction::TargetPortfolioSmart {
target_weights_expr,
order_prices_expr,
@@ -3807,9 +3856,10 @@ mod tests {
use chrono::{NaiveDate, NaiveTime};
use super::{
PlatformExplicitActionStage, PlatformExplicitCancelKind, PlatformExplicitOrderKind,
PlatformExprStrategy, PlatformExprStrategyConfig, PlatformRebalanceSchedule,
PlatformScheduleFrequency, PlatformTradeAction, PlatformUniverseActionKind,
PlatformAccountActionKind, PlatformExplicitActionStage, PlatformExplicitCancelKind,
PlatformExplicitOrderKind, PlatformExprStrategy, PlatformExprStrategyConfig,
PlatformRebalanceSchedule, PlatformScheduleFrequency, PlatformTradeAction,
PlatformUniverseActionKind,
};
use crate::{
AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, DailyFactorSnapshot,
@@ -4008,12 +4058,19 @@ mod tests {
when_expr: Some("allow_buy".to_string()),
reason: "platform_cancel_symbol".to_string(),
},
PlatformTradeAction::Account {
kind: PlatformAccountActionKind::DepositWithdraw,
amount_expr: "cash * 0.01".to_string(),
receiving_days_expr: Some("1".to_string()),
when_expr: Some("daily_returns == 0.0".to_string()),
reason: "platform_deposit".to_string(),
},
];
let mut strategy = PlatformExprStrategy::new(cfg);
let decision = strategy.on_day(&ctx).expect("platform decision");
assert_eq!(decision.order_intents.len(), 2);
assert_eq!(decision.order_intents.len(), 3);
match &decision.order_intents[0] {
crate::strategy::OrderIntent::Value {
symbol,
@@ -4033,6 +4090,18 @@ mod tests {
}
other => panic!("unexpected explicit cancel intent: {other:?}"),
}
match &decision.order_intents[2] {
crate::strategy::OrderIntent::DepositWithdraw {
amount,
receiving_days,
reason,
} => {
assert!((*amount - 10_000.0).abs() < 1e-6);
assert_eq!(*receiving_days, 1);
assert_eq!(reason, "platform_deposit");
}
other => panic!("unexpected explicit account intent: {other:?}"),
}
assert!(
decision
.diagnostics

View File

@@ -308,9 +308,19 @@ impl Position {
#[derive(Debug, Clone)]
pub struct PortfolioState {
initial_cash: f64,
units: f64,
cash: f64,
cash_liabilities: f64,
positions: IndexMap<String, Position>,
cash_receivables: Vec<CashReceivable>,
pending_cash_flows: Vec<PendingCashFlow>,
}
#[derive(Debug, Clone)]
pub struct PendingCashFlow {
pub payable_date: NaiveDate,
pub amount: f64,
pub reason: String,
}
#[derive(Debug, Clone)]
@@ -328,24 +338,35 @@ impl PortfolioState {
pub fn new(initial_cash: f64) -> Self {
Self {
initial_cash,
units: initial_cash,
cash: initial_cash,
cash_liabilities: 0.0,
positions: IndexMap::new(),
cash_receivables: Vec::new(),
pending_cash_flows: Vec::new(),
}
}
pub fn starting_cash(&self) -> f64 {
self.units
}
pub fn initial_cash(&self) -> f64 {
self.initial_cash
}
pub fn units(&self) -> f64 {
self.initial_cash
self.units
}
pub fn cash(&self) -> f64 {
self.cash
}
pub fn cash_liabilities(&self) -> f64 {
self.cash_liabilities
}
pub fn positions(&self) -> &IndexMap<String, Position> {
&self.positions
}
@@ -377,6 +398,92 @@ impl PortfolioState {
self.refresh_dividend_receivables();
}
pub fn deposit_withdraw(&mut self, amount: f64) -> Result<(), String> {
if !amount.is_finite() {
return Err("deposit_withdraw amount must be finite".to_string());
}
if amount < 0.0 && self.cash + amount < -1e-6 {
return Err(format!(
"insufficient cash for withdrawal amount={:.2} cash={:.2}",
amount, self.cash
));
}
let unit_net_value = self.unit_net_value();
self.cash += amount;
self.rebase_units_after_external_cash_flow(unit_net_value);
Ok(())
}
pub fn schedule_deposit_withdraw(
&mut self,
payable_date: NaiveDate,
amount: f64,
reason: impl Into<String>,
) -> Result<(), String> {
if !amount.is_finite() {
return Err("deposit_withdraw amount must be finite".to_string());
}
if amount < 0.0 && self.cash + amount < -1e-6 {
return Err(format!(
"insufficient cash for scheduled withdrawal amount={:.2} cash={:.2}",
amount, self.cash
));
}
self.pending_cash_flows.push(PendingCashFlow {
payable_date,
amount,
reason: reason.into(),
});
self.pending_cash_flows
.sort_by_key(|flow| flow.payable_date);
Ok(())
}
pub fn settle_pending_cash_flows(&mut self, date: NaiveDate) -> Vec<PendingCashFlow> {
let mut settled = Vec::new();
let mut pending = Vec::new();
for flow in std::mem::take(&mut self.pending_cash_flows) {
if flow.payable_date <= date {
let unit_net_value = self.unit_net_value();
self.cash += flow.amount;
self.rebase_units_after_external_cash_flow(unit_net_value);
settled.push(flow);
} else {
pending.push(flow);
}
}
self.pending_cash_flows = pending;
settled
}
pub fn pending_cash_flows(&self) -> &[PendingCashFlow] {
&self.pending_cash_flows
}
pub fn finance_repay(&mut self, amount: f64) -> Result<(), String> {
if !amount.is_finite() {
return Err("finance_repay amount must be finite".to_string());
}
if amount > 0.0 {
self.cash_liabilities += amount;
self.cash += amount;
return Ok(());
}
if amount < 0.0 {
let repay_amount = (-amount).min(self.cash_liabilities);
if repay_amount > self.cash + 1e-6 {
return Err(format!(
"insufficient cash for finance repay amount={:.2} cash={:.2}",
repay_amount, self.cash
));
}
self.cash_liabilities -= repay_amount;
self.cash -= repay_amount;
}
Ok(())
}
pub fn settle_cash_receivables(&mut self, date: NaiveDate) -> Vec<CashReceivable> {
let mut settled = Vec::new();
let mut pending = Vec::new();
@@ -459,7 +566,7 @@ impl PortfolioState {
}
pub fn total_equity(&self) -> f64 {
self.cash + self.market_value()
self.cash + self.market_value() - self.cash_liabilities
}
pub fn total_value(&self) -> f64 {
@@ -471,18 +578,18 @@ impl PortfolioState {
}
pub fn unit_net_value(&self) -> f64 {
if self.initial_cash.abs() < f64::EPSILON {
if self.units.abs() < f64::EPSILON {
0.0
} else {
self.total_equity() / self.initial_cash
self.total_equity() / self.units
}
}
pub fn static_unit_net_value(&self) -> f64 {
if self.initial_cash.abs() < f64::EPSILON {
if self.units.abs() < f64::EPSILON {
0.0
} else {
(self.total_equity() - self.daily_pnl()) / self.initial_cash
(self.total_equity() - self.daily_pnl()) / self.units
}
}
@@ -619,6 +726,12 @@ impl PortfolioState {
position.set_dividend_receivable(per_symbol.get(symbol).copied().unwrap_or(0.0));
}
}
fn rebase_units_after_external_cash_flow(&mut self, unit_net_value_before: f64) {
if unit_net_value_before > 0.0 && unit_net_value_before.is_finite() {
self.units = self.total_equity() / unit_net_value_before;
}
}
}
#[cfg(test)]

View File

@@ -367,7 +367,7 @@ impl StrategyContext<'_> {
transaction_cost: self.portfolio.transaction_cost(),
trading_pnl: self.portfolio.trading_pnl(),
position_pnl: self.portfolio.position_pnl(),
cash_liabilities: 0.0,
cash_liabilities: self.portfolio.cash_liabilities(),
}
}
@@ -810,6 +810,15 @@ pub enum OrderIntent {
symbols: BTreeSet<String>,
reason: String,
},
DepositWithdraw {
amount: f64,
receiving_days: usize,
reason: String,
},
FinanceRepay {
amount: f64,
reason: String,
},
}
#[derive(Debug, Clone)]

View File

@@ -124,7 +124,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
},
ManualSection {
title: "trading.rotation / order.* / cancel.* / update_universe / subscribe".to_string(),
detail: "支持显式下单、撤单、AlgoOrder动态 universe 管理。可以用 trading.rotation(false) 关闭默认轮动链路,再用 trading.stage(\"open_auction\" | \"on_day\") 指定执行阶段;需要模拟 rqalpha 的 tick 订阅保护时,可写 trading.subscription_guard(true),未订阅 symbol 的显式订单会被拦截TargetPortfolioSmart + AlgoOrder 会过滤未订阅标的。用 trading.schedule.daily().at([\"10:18\"]) / trading.schedule.weekly(weekday=5).at([\"10:18\"]) / trading.schedule.weekly(tradingday=-1).at([\"10:18\"]) / trading.schedule.monthly(tradingday=1).at([\"10:18\"]) 指定触发频率和分钟级 time_rule然后写 order.shares(\"600000.SH\", 1000)、order.target_shares(\"600000.SH\", 2000)、order.value(\"600000.SH\", cash * 0.25)、order.target_percent(\"600000.SH\", 0.05)、order.limit_value(\"600000.SH\", cash * 0.25, open * 0.99)、order.vwap_value(\"600000.SH\", cash * 0.25, \"09:31\", \"09:40\")、order.twap_percent(\"600000.SH\", 0.05, \"10:00\", \"10:30\")、order.target_portfolio_smart(weights={\"600000.SH\": 0.3, \"000001.SZ\": 0.2}, order_prices=VWAPOrder(930, 940), valuation_prices={\"600000.SH\": prev_close})、order.target_portfolio_smart(weights={\"600000.SH\": 0.3, \"000001.SZ\": 0.2}, order_prices={\"600000.SH\": open * 0.99}, valuation_prices={\"600000.SH\": prev_close})、cancel.order(12345)、cancel.symbol(\"600000.SH\")、cancel.all()、update_universe([\"600000.SH\", \"000001.SZ\"])、subscribe([\"000001.SZ\"])、unsubscribe([\"000001.SZ\"])。其中 order.target_shares(...) 对应 rqalpha 的 order_toorder.target_portfolio_smart(...) 对应 rqalpha 的 order_target_portfolio_smart 批量目标权重语义order_prices 既可以是逐标的限价映射,也可以是 VWAPOrder/TWAPOrder 这类全局 AlgoOrderorder.vwap_* / order.twap_* 对应 rqalpha 的 AlgoOrder 时间窗订单风格,而 update_universe/subscribe/unsubscribe 对应 rqalpha 的动态 universe 与订阅接口。symbol 使用标准证券代码数量、金额、仓位、时间窗、限价、order_id 和 symbol 列表都支持表达式;这些语句也支持放进 when/unless 条件块。".to_string(),
detail: "支持显式下单、撤单、AlgoOrder动态 universe 和账户资金动作。可以用 trading.rotation(false) 关闭默认轮动链路,再用 trading.stage(\"open_auction\" | \"on_day\") 指定执行阶段;需要模拟 rqalpha 的 tick 订阅保护时,可写 trading.subscription_guard(true),未订阅 symbol 的显式订单会被拦截TargetPortfolioSmart + AlgoOrder 会过滤未订阅标的。用 trading.schedule.daily().at([\"10:18\"]) / trading.schedule.weekly(weekday=5).at([\"10:18\"]) / trading.schedule.weekly(tradingday=-1).at([\"10:18\"]) / trading.schedule.monthly(tradingday=1).at([\"10:18\"]) 指定触发频率和分钟级 time_rule然后写 order.shares(\"600000.SH\", 1000)、order.target_shares(\"600000.SH\", 2000)、order.value(\"600000.SH\", cash * 0.25)、order.target_percent(\"600000.SH\", 0.05)、order.limit_value(\"600000.SH\", cash * 0.25, open * 0.99)、order.vwap_value(\"600000.SH\", cash * 0.25, \"09:31\", \"09:40\")、order.twap_percent(\"600000.SH\", 0.05, \"10:00\", \"10:30\")、order.target_portfolio_smart(weights={\"600000.SH\": 0.3, \"000001.SZ\": 0.2}, order_prices=VWAPOrder(930, 940), valuation_prices={\"600000.SH\": prev_close})、order.target_portfolio_smart(weights={\"600000.SH\": 0.3, \"000001.SZ\": 0.2}, order_prices={\"600000.SH\": open * 0.99}, valuation_prices={\"600000.SH\": prev_close})、cancel.order(12345)、cancel.symbol(\"600000.SH\")、cancel.all()、update_universe([\"600000.SH\", \"000001.SZ\"])、subscribe([\"000001.SZ\"])、unsubscribe([\"000001.SZ\"])、account.deposit_withdraw(100000, receiving_days=0)、account.finance_repay(50000)。其中 order.target_shares(...) 对应 rqalpha 的 order_toorder.target_portfolio_smart(...) 对应 rqalpha 的 order_target_portfolio_smart 批量目标权重语义;account.deposit_withdraw(...) 和 account.finance_repay(...) 对应 RQAlpha 账户出入金与融资/还款语义;order_prices 既可以是逐标的限价映射,也可以是 VWAPOrder/TWAPOrder 这类全局 AlgoOrderorder.vwap_* / order.twap_* 对应 rqalpha 的 AlgoOrder 时间窗订单风格,而 update_universe/subscribe/unsubscribe 对应 rqalpha 的动态 universe 与订阅接口。symbol 使用标准证券代码数量、金额、仓位、时间窗、限价、order_id 和 symbol 列表都支持表达式;这些语句也支持放进 when/unless 条件块。".to_string(),
},
ManualSection {
title: "when / unless / else".to_string(),
@@ -208,6 +208,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
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: "deposit_withdraw/finance_repay".to_string(), signature: "account.deposit_withdraw(amount, receiving_days=0)".to_string(), detail: "策略账户资金动作。deposit_withdraw 正数入金、负数出金receiving_days 大于 0 时按交易日延迟到账并保持净值口径不把外部资金流当成收益。finance_repay 正数融资、负数还款,会同步维护 cash_liabilities。".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() },