fix: separate historical session capacity audits from execution sizing
This commit is contained in:
@@ -7,7 +7,7 @@ use chrono::{Duration, NaiveDate, NaiveDateTime, NaiveTime};
|
|||||||
use crate::cost::CostModel;
|
use crate::cost::CostModel;
|
||||||
use crate::data::{DataSet, IntradayExecutionQuote, PriceField};
|
use crate::data::{DataSet, IntradayExecutionQuote, PriceField};
|
||||||
use crate::engine::BacktestError;
|
use crate::engine::BacktestError;
|
||||||
use crate::execution_capacity::{CapacityError, ParticipationRate, VolumeObservation, VolumeObservationKind};
|
use crate::execution_capacity::{CapacityAuditSummary, CapacityError, ParticipationRate, SessionCapacityAudit, VolumeCapacityMode, VolumeObservation, VolumeObservationKind};
|
||||||
use crate::execution_schedule::TwapSchedule;
|
use crate::execution_schedule::TwapSchedule;
|
||||||
use crate::events::{
|
use crate::events::{
|
||||||
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
|
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
|
||||||
@@ -423,6 +423,7 @@ pub struct BrokerSimulator<C, R> {
|
|||||||
volume_percent: f64,
|
volume_percent: f64,
|
||||||
volume_rate: Result<ParticipationRate, CapacityError>,
|
volume_rate: Result<ParticipationRate, CapacityError>,
|
||||||
volume_limit: bool,
|
volume_limit: bool,
|
||||||
|
volume_capacity_mode: VolumeCapacityMode,
|
||||||
inactive_limit: bool,
|
inactive_limit: bool,
|
||||||
liquidity_limit: bool,
|
liquidity_limit: bool,
|
||||||
strict_value_budget: bool,
|
strict_value_budget: bool,
|
||||||
@@ -459,6 +460,7 @@ impl<C, R> BrokerSimulator<C, R> {
|
|||||||
volume_percent: 0.25,
|
volume_percent: 0.25,
|
||||||
volume_rate: ParticipationRate::new(0.25),
|
volume_rate: ParticipationRate::new(0.25),
|
||||||
volume_limit: true,
|
volume_limit: true,
|
||||||
|
volume_capacity_mode: VolumeCapacityMode::ExecutionObservation,
|
||||||
inactive_limit: true,
|
inactive_limit: true,
|
||||||
liquidity_limit: true,
|
liquidity_limit: true,
|
||||||
strict_value_budget: true,
|
strict_value_budget: true,
|
||||||
@@ -499,6 +501,7 @@ impl<C, R> BrokerSimulator<C, R> {
|
|||||||
volume_percent: 0.25,
|
volume_percent: 0.25,
|
||||||
volume_rate: ParticipationRate::new(0.25),
|
volume_rate: ParticipationRate::new(0.25),
|
||||||
volume_limit: true,
|
volume_limit: true,
|
||||||
|
volume_capacity_mode: VolumeCapacityMode::ExecutionObservation,
|
||||||
inactive_limit: true,
|
inactive_limit: true,
|
||||||
liquidity_limit: true,
|
liquidity_limit: true,
|
||||||
strict_value_budget: true,
|
strict_value_budget: true,
|
||||||
@@ -529,6 +532,29 @@ impl<C, R> BrokerSimulator<C, R> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_volume_capacity_mode(mut self, mode: VolumeCapacityMode) -> Self {
|
||||||
|
self.volume_capacity_mode = mode;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn capacity_audit_summary(&self) -> CapacityAuditSummary {
|
||||||
|
CapacityAuditSummary { mode: self.volume_capacity_mode, enabled: self.volume_limit,
|
||||||
|
participation_rate: self.volume_percent, ..Default::default() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn audit_completed_session_capacity(&self, date: NaiveDate, data: &DataSet) -> Result<Vec<SessionCapacityAudit>, BacktestError> {
|
||||||
|
if !self.volume_limit || self.volume_capacity_mode != VolumeCapacityMode::SessionCapacityAudit {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
let session = self.execution_session.borrow();
|
||||||
|
if session.date != Some(date) { return Ok(Vec::new()); }
|
||||||
|
let rate = self.volume_rate.map_err(|error| BacktestError::Execution(error.to_string()))?;
|
||||||
|
session.intraday_turnover.iter().filter(|(_, quantity)| **quantity > 0).map(|(symbol, quantity)| {
|
||||||
|
let market = data.market(date, symbol).ok_or_else(|| BacktestError::MissingPrice { date, symbol: symbol.clone(), field: "session capacity audit" })?;
|
||||||
|
Ok(SessionCapacityAudit::new(date, symbol.clone(), u64::from(*quantity), market.volume, rate))
|
||||||
|
}).collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn with_inactive_limit(mut self, enabled: bool) -> Self {
|
pub fn with_inactive_limit(mut self, enabled: bool) -> Self {
|
||||||
self.inactive_limit = enabled;
|
self.inactive_limit = enabled;
|
||||||
self
|
self
|
||||||
@@ -1458,6 +1484,8 @@ where
|
|||||||
) -> Result<BrokerExecutionReport, BacktestError> {
|
) -> Result<BrokerExecutionReport, BacktestError> {
|
||||||
if self.volume_limit {
|
if self.volume_limit {
|
||||||
self.volume_rate.map_err(|error| BacktestError::Execution(error.to_string()))?;
|
self.volume_rate.map_err(|error| BacktestError::Execution(error.to_string()))?;
|
||||||
|
self.volume_capacity_mode.validate(true, self.matching_type_uses_intraday_quotes())
|
||||||
|
.map_err(|error| BacktestError::Execution(error.to_string()))?;
|
||||||
}
|
}
|
||||||
let mut session = std::mem::take(&mut *self.execution_session.borrow_mut());
|
let mut session = std::mem::take(&mut *self.execution_session.borrow_mut());
|
||||||
session.activate(date);
|
session.activate(date);
|
||||||
@@ -7269,57 +7297,13 @@ where
|
|||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let uses_intraday_quantity = self.matching_type_uses_intraday_quotes();
|
let _ = (side, minimum_order_quantity, order_step_size, consumed_turnover, allow_odd_lot_sell);
|
||||||
let available_market_volume = if uses_intraday_quantity {
|
if self.inactive_limit && snapshot.paused { return Err("paused".into()); }
|
||||||
snapshot.minute_volume
|
self.volume_capacity_mode.validate(self.volume_limit, self.matching_type_uses_intraday_quotes())
|
||||||
} else {
|
.map_err(|error| error.to_string())?;
|
||||||
snapshot.volume
|
// Per-observation limits are applied to each actual quote below. The
|
||||||
};
|
// session-audit model must never size this order from the day's total.
|
||||||
let no_volume_reason = if uses_intraday_quantity {
|
Ok(requested_qty)
|
||||||
"minute no volume"
|
|
||||||
} else {
|
|
||||||
"daily no volume"
|
|
||||||
};
|
|
||||||
let volume_limit_reason = if uses_intraday_quantity {
|
|
||||||
"minute volume limit"
|
|
||||||
} else {
|
|
||||||
"daily volume limit"
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut max_fill = requested_qty;
|
|
||||||
|
|
||||||
if self.inactive_limit
|
|
||||||
&& (snapshot.paused || (!uses_intraday_quantity && available_market_volume == 0))
|
|
||||||
{
|
|
||||||
return Err(if snapshot.paused {
|
|
||||||
"paused".to_string()
|
|
||||||
} else {
|
|
||||||
no_volume_reason.to_string()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if uses_intraday_quantity {
|
|
||||||
return Ok(max_fill);
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.volume_limit {
|
|
||||||
let raw_limit = self.volume_rate.map_err(|error| error.to_string())?
|
|
||||||
.remaining(available_market_volume, u64::from(consumed_turnover), requested_qty);
|
|
||||||
if raw_limit == 0 {
|
|
||||||
return Err(volume_limit_reason.to_string());
|
|
||||||
}
|
|
||||||
let volume_limited = if side == OrderSide::Sell && allow_odd_lot_sell {
|
|
||||||
raw_limit
|
|
||||||
} else {
|
|
||||||
self.round_buy_quantity(raw_limit, minimum_order_quantity, order_step_size)
|
|
||||||
};
|
|
||||||
if volume_limited == 0 {
|
|
||||||
return Err(volume_limit_reason.to_string());
|
|
||||||
}
|
|
||||||
max_fill = max_fill.min(volume_limited);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(max_fill)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn price_satisfies_limit(
|
fn price_satisfies_limit(
|
||||||
@@ -7723,7 +7707,7 @@ where
|
|||||||
} else {
|
} else {
|
||||||
remaining_qty
|
remaining_qty
|
||||||
};
|
};
|
||||||
if self.volume_limit {
|
if self.volume_limit && self.volume_capacity_mode.limits_execution_quantity() {
|
||||||
let consumed = execution_ledger
|
let consumed = execution_ledger
|
||||||
.volume_consumed(symbol, quote.timestamp)
|
.volume_consumed(symbol, quote.timestamp)
|
||||||
.saturating_add(
|
.saturating_add(
|
||||||
@@ -7871,7 +7855,7 @@ where
|
|||||||
.saturating_add(take_qty)
|
.saturating_add(take_qty)
|
||||||
.min(state.displayed_quantity);
|
.min(state.displayed_quantity);
|
||||||
}
|
}
|
||||||
if self.volume_limit {
|
if self.volume_limit && self.volume_capacity_mode.limits_execution_quantity() {
|
||||||
let consumed = pending_volume_consumption
|
let consumed = pending_volume_consumption
|
||||||
.entry(quote.timestamp)
|
.entry(quote.timestamp)
|
||||||
.or_default();
|
.or_default();
|
||||||
@@ -7885,7 +7869,7 @@ where
|
|||||||
depth_price_bits,
|
depth_price_bits,
|
||||||
displayed_quantity,
|
displayed_quantity,
|
||||||
consume_depth,
|
consume_depth,
|
||||||
consume_volume: self.volume_limit,
|
consume_volume: self.volume_limit && self.volume_capacity_mode.limits_execution_quantity(),
|
||||||
quantity: take_qty,
|
quantity: take_qty,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ impl DailyEquityPoint {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct BacktestResult {
|
pub struct BacktestResult {
|
||||||
|
pub capacity_audit: crate::execution_capacity::CapacityAuditSummary,
|
||||||
pub strategy_name: String,
|
pub strategy_name: String,
|
||||||
pub equity_curve: Vec<DailyEquityPoint>,
|
pub equity_curve: Vec<DailyEquityPoint>,
|
||||||
pub benchmark_series: Vec<BenchmarkSnapshot>,
|
pub benchmark_series: Vec<BenchmarkSnapshot>,
|
||||||
@@ -280,6 +281,7 @@ pub struct AnalyzerRiskSummary {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct AnalyzerReport {
|
pub struct AnalyzerReport {
|
||||||
|
pub capacity_audit: crate::execution_capacity::CapacityAuditSummary,
|
||||||
pub strategy_name: String,
|
pub strategy_name: String,
|
||||||
pub trades: Vec<AnalyzerTradeRow>,
|
pub trades: Vec<AnalyzerTradeRow>,
|
||||||
pub positions: Vec<AnalyzerPositionRow>,
|
pub positions: Vec<AnalyzerPositionRow>,
|
||||||
@@ -294,6 +296,7 @@ pub struct AnalyzerReport {
|
|||||||
impl BacktestResult {
|
impl BacktestResult {
|
||||||
pub fn analyzer_report(&self) -> AnalyzerReport {
|
pub fn analyzer_report(&self) -> AnalyzerReport {
|
||||||
AnalyzerReport {
|
AnalyzerReport {
|
||||||
|
capacity_audit: self.capacity_audit.clone(),
|
||||||
strategy_name: self.strategy_name.clone(),
|
strategy_name: self.strategy_name.clone(),
|
||||||
trades: self
|
trades: self
|
||||||
.fills
|
.fills
|
||||||
@@ -2102,6 +2105,7 @@ where
|
|||||||
.map(|(execution_date, _)| *execution_date)
|
.map(|(execution_date, _)| *execution_date)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let mut result = BacktestResult {
|
let mut result = BacktestResult {
|
||||||
|
capacity_audit: self.broker.capacity_audit_summary(),
|
||||||
strategy_name: self.strategy.name().to_string(),
|
strategy_name: self.strategy.name().to_string(),
|
||||||
benchmark_series: self
|
benchmark_series: self
|
||||||
.data
|
.data
|
||||||
@@ -3415,6 +3419,16 @@ where
|
|||||||
execution_date,
|
execution_date,
|
||||||
);
|
);
|
||||||
let daily_fill_count = result.fills.len() - day_fill_start;
|
let daily_fill_count = result.fills.len() - day_fill_start;
|
||||||
|
for audit in self.broker.audit_completed_session_capacity(execution_date, &self.data)? {
|
||||||
|
result.capacity_audit.observe(&audit);
|
||||||
|
// Keep every audit in the durable event store, independent of
|
||||||
|
// debug phase retention. It never changes earlier executions.
|
||||||
|
result.process_events.push(ProcessEvent {
|
||||||
|
date: execution_date, kind: ProcessEventKind::SessionCapacityAudit,
|
||||||
|
order_id: None, symbol: Some(audit.symbol.clone()), side: None,
|
||||||
|
detail: serde_json::to_string(&audit).map_err(|error| BacktestError::Execution(error.to_string()))?,
|
||||||
|
});
|
||||||
|
}
|
||||||
let daily_order_count = result.order_events.len() - day_order_start;
|
let daily_order_count = result.order_events.len() - day_order_start;
|
||||||
let execution_risk_decisions =
|
let execution_risk_decisions =
|
||||||
risk_decisions_from_order_events(&result.order_events[day_order_start..]);
|
risk_decisions_from_order_events(&result.order_events[day_order_start..]);
|
||||||
|
|||||||
@@ -317,6 +317,7 @@ pub enum ProcessEventKind {
|
|||||||
AccountDepositWithdraw,
|
AccountDepositWithdraw,
|
||||||
AccountFinanceRepay,
|
AccountFinanceRepay,
|
||||||
AccountManagementFee,
|
AccountManagementFee,
|
||||||
|
SessionCapacityAudit,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProcessEventKind {
|
impl ProcessEventKind {
|
||||||
@@ -362,6 +363,7 @@ impl ProcessEventKind {
|
|||||||
Self::AccountDepositWithdraw => "account_deposit_withdraw",
|
Self::AccountDepositWithdraw => "account_deposit_withdraw",
|
||||||
Self::AccountFinanceRepay => "account_finance_repay",
|
Self::AccountFinanceRepay => "account_finance_repay",
|
||||||
Self::AccountManagementFee => "account_management_fee",
|
Self::AccountManagementFee => "account_management_fee",
|
||||||
|
Self::SessionCapacityAudit => "session_capacity_audit",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,6 +395,7 @@ impl ProcessEventKind {
|
|||||||
| Self::AccountDepositWithdraw
|
| Self::AccountDepositWithdraw
|
||||||
| Self::AccountFinanceRepay
|
| Self::AccountFinanceRepay
|
||||||
| Self::AccountManagementFee
|
| Self::AccountManagementFee
|
||||||
|
| Self::SessionCapacityAudit
|
||||||
| Self::Settlement
|
| Self::Settlement
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,19 @@ pub enum VolumeCapacityMode {
|
|||||||
SessionCapacityAudit,
|
SessionCapacityAudit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl VolumeCapacityMode {
|
||||||
|
pub fn validate(self, enabled: bool, has_execution_observations: bool) -> Result<(), CapacityError> {
|
||||||
|
if !enabled { return Ok(()); }
|
||||||
|
match self {
|
||||||
|
Self::ExecutionObservation if !has_execution_observations => Err(CapacityError::MissingObservation),
|
||||||
|
Self::CompletedBar => Err(CapacityError::MissingCompletedBar),
|
||||||
|
_ => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn limits_execution_quantity(self) -> bool { self != Self::SessionCapacityAudit }
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||||
pub enum CapacityError {
|
pub enum CapacityError {
|
||||||
#[error("execution capacity ratio must be finite and in (0, 1]")]
|
#[error("execution capacity ratio must be finite and in (0, 1]")]
|
||||||
@@ -26,6 +39,28 @@ pub enum CapacityError {
|
|||||||
WrongSession,
|
WrongSession,
|
||||||
#[error("execution-time capacity is missing; daily session volume cannot size an earlier fill")]
|
#[error("execution-time capacity is missing; daily session volume cannot size an earlier fill")]
|
||||||
MissingObservation,
|
MissingObservation,
|
||||||
|
#[error("completed_bar capacity requires declared bar end and availability; an undated daily total is not a completed observation")]
|
||||||
|
MissingCompletedBar,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CapacityAuditSummary {
|
||||||
|
pub mode: VolumeCapacityMode,
|
||||||
|
pub enabled: bool,
|
||||||
|
pub participation_rate: f64,
|
||||||
|
pub audited_symbol_sessions: usize,
|
||||||
|
pub failed_symbol_sessions: usize,
|
||||||
|
pub audit_passed: Option<bool>,
|
||||||
|
pub execution_time_capacity_proven: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CapacityAuditSummary {
|
||||||
|
pub fn observe(&mut self, audit: &SessionCapacityAudit) {
|
||||||
|
self.audited_symbol_sessions += 1;
|
||||||
|
self.failed_symbol_sessions += usize::from(!audit.passed);
|
||||||
|
self.audit_passed = Some(self.failed_symbol_sessions == 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decimal semantics of the frozen JSON rate, evaluated without a float product.
|
/// Decimal semantics of the frozen JSON rate, evaluated without a float product.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use crate::data::{
|
|||||||
decision_market_cap_bn,
|
decision_market_cap_bn,
|
||||||
};
|
};
|
||||||
use crate::engine::BacktestError;
|
use crate::engine::BacktestError;
|
||||||
use crate::execution_capacity::{CapacityError, ParticipationRate};
|
use crate::execution_capacity::{CapacityError, ParticipationRate, VolumeCapacityMode};
|
||||||
use crate::events::{OrderSide, ProcessEvent, ProcessEventKind};
|
use crate::events::{OrderSide, ProcessEvent, ProcessEventKind};
|
||||||
use crate::fixed_point::FixedMoney;
|
use crate::fixed_point::FixedMoney;
|
||||||
use crate::futures::{
|
use crate::futures::{
|
||||||
@@ -689,6 +689,7 @@ pub struct PlatformExprStrategyConfig {
|
|||||||
pub rebalance_cash_mode: RebalanceCashMode,
|
pub rebalance_cash_mode: RebalanceCashMode,
|
||||||
pub sell_then_buy_delay_slippage_rate: f64,
|
pub sell_then_buy_delay_slippage_rate: f64,
|
||||||
pub risk_config: FidcRiskControlConfig,
|
pub risk_config: FidcRiskControlConfig,
|
||||||
|
pub volume_capacity_mode: VolumeCapacityMode,
|
||||||
pub slippage_model: SlippageModel,
|
pub slippage_model: SlippageModel,
|
||||||
pub matching_type: MatchingType,
|
pub matching_type: MatchingType,
|
||||||
pub quote_quantity_limit: bool,
|
pub quote_quantity_limit: bool,
|
||||||
@@ -777,6 +778,7 @@ impl PlatformExprStrategyConfig {
|
|||||||
rebalance_cash_mode: RebalanceCashMode::default(),
|
rebalance_cash_mode: RebalanceCashMode::default(),
|
||||||
sell_then_buy_delay_slippage_rate: 0.0,
|
sell_then_buy_delay_slippage_rate: 0.0,
|
||||||
risk_config: FidcRiskControlConfig::default(),
|
risk_config: FidcRiskControlConfig::default(),
|
||||||
|
volume_capacity_mode: VolumeCapacityMode::ExecutionObservation,
|
||||||
slippage_model: SlippageModel::None,
|
slippage_model: SlippageModel::None,
|
||||||
matching_type: MatchingType::CurrentBarClose,
|
matching_type: MatchingType::CurrentBarClose,
|
||||||
quote_quantity_limit: true,
|
quote_quantity_limit: true,
|
||||||
@@ -3201,11 +3203,10 @@ impl PlatformExprStrategy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if constraints.volume_limit_enabled {
|
if constraints.volume_limit_enabled && self.config.volume_capacity_mode.limits_execution_quantity() {
|
||||||
let volume_basis = match quote {
|
let volume_basis = match quote {
|
||||||
Some(quote) => quote.volume_delta,
|
Some(quote) => quote.volume_delta,
|
||||||
None if market.minute_volume > 0 => market.minute_volume,
|
None => return Err(BacktestError::Execution(CapacityError::MissingObservation.to_string())),
|
||||||
None => market.volume,
|
|
||||||
};
|
};
|
||||||
if volume_basis == 0 {
|
if volume_basis == 0 {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|||||||
@@ -91,6 +91,8 @@ pub struct StrategyRebalanceSpec {
|
|||||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct StrategyExecutionSpec {
|
pub struct StrategyExecutionSpec {
|
||||||
|
#[serde(default, alias = "volume_capacity_mode")]
|
||||||
|
pub volume_capacity_mode: Option<crate::execution_capacity::VolumeCapacityMode>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub frequency: Option<String>,
|
pub frequency: Option<String>,
|
||||||
#[serde(default, alias = "matching_type")]
|
#[serde(default, alias = "matching_type")]
|
||||||
@@ -164,9 +166,22 @@ pub struct StrategyExecutionSpec {
|
|||||||
pub sell_then_buy_delay_slippage_rate: Option<f64>,
|
pub sell_then_buy_delay_slippage_rate: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl StrategyRuntimeSpec {
|
||||||
|
pub fn volume_capacity_mode(&self) -> Result<crate::execution_capacity::VolumeCapacityMode, String> {
|
||||||
|
let engine = self.engine_config.as_ref().and_then(|config| config.volume_capacity_mode);
|
||||||
|
let execution = self.execution.as_ref().and_then(|config| config.volume_capacity_mode);
|
||||||
|
if engine.zip(execution).is_some_and(|(a, b)| a != b) {
|
||||||
|
return Err("conflicting engine/execution volumeCapacityMode".into());
|
||||||
|
}
|
||||||
|
Ok(execution.or(engine).unwrap_or_default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct StrategyEngineConfig {
|
pub struct StrategyEngineConfig {
|
||||||
|
#[serde(default, alias = "volume_capacity_mode")]
|
||||||
|
pub volume_capacity_mode: Option<crate::execution_capacity::VolumeCapacityMode>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub frequency: Option<String>,
|
pub frequency: Option<String>,
|
||||||
#[serde(default, alias = "template_id")]
|
#[serde(default, alias = "template_id")]
|
||||||
@@ -1822,6 +1837,7 @@ pub fn platform_expr_config_from_spec(
|
|||||||
strategy_spec: Option<&StrategyRuntimeSpec>,
|
strategy_spec: Option<&StrategyRuntimeSpec>,
|
||||||
) -> Result<PlatformExprStrategyConfig, String> {
|
) -> Result<PlatformExprStrategyConfig, String> {
|
||||||
let mut cfg = PlatformExprStrategyConfig::generic();
|
let mut cfg = PlatformExprStrategyConfig::generic();
|
||||||
|
cfg.volume_capacity_mode = strategy_spec.map(StrategyRuntimeSpec::volume_capacity_mode).transpose()?.unwrap_or_default();
|
||||||
cfg.strategy_name = strategy_id.to_string();
|
cfg.strategy_name = strategy_id.to_string();
|
||||||
if !signal_symbol.trim().is_empty() {
|
if !signal_symbol.trim().is_empty() {
|
||||||
cfg.signal_symbol = signal_symbol.trim().to_string();
|
cfg.signal_symbol = signal_symbol.trim().to_string();
|
||||||
|
|||||||
Reference in New Issue
Block a user