Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 875e31f71f | |||
| 61bd14d001 |
@@ -8324,7 +8324,7 @@ mod tests {
|
|||||||
|
|
||||||
fn limit_test_quote(last_price: f64, bid1: f64, ask1: f64) -> IntradayExecutionQuote {
|
fn limit_test_quote(last_price: f64, bid1: f64, ask1: f64) -> IntradayExecutionQuote {
|
||||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"),
|
||||||
@@ -11729,7 +11729,7 @@ mod tests {
|
|||||||
lower_limit: 5.27,
|
lower_limit: 5.27,
|
||||||
price_tick: 0.01,
|
price_tick: 0.01,
|
||||||
};
|
};
|
||||||
let quote = IntradayExecutionQuote {
|
let quote = IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 39, 59).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 39, 59).expect("valid timestamp"),
|
||||||
|
|||||||
@@ -47,8 +47,10 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
|||||||
data: &DataSet,
|
data: &DataSet,
|
||||||
symbols: &BTreeSet<String>,
|
symbols: &BTreeSet<String>,
|
||||||
execution_clock: Option<NaiveDateTime>,
|
execution_clock: Option<NaiveDateTime>,
|
||||||
) -> Result<Vec<pool::MarketSnapshot>, BacktestError> {
|
cumulative_conditions: bool,
|
||||||
symbols
|
) -> Result<(Vec<pool::MarketSnapshot>, Vec<String>), BacktestError> {
|
||||||
|
let mut unavailable = Vec::new();
|
||||||
|
let quotes = symbols
|
||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| {
|
.map(|symbol| {
|
||||||
let snapshot = data.market(date, symbol).ok_or_else(|| {
|
let snapshot = data.market(date, symbol).ok_or_else(|| {
|
||||||
@@ -134,11 +136,17 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
|||||||
None,
|
None,
|
||||||
calibration.as_ref(),
|
calibration.as_ref(),
|
||||||
)?;
|
)?;
|
||||||
|
let totals = if cumulative_conditions {
|
||||||
|
match data.execution_session_totals(symbol, clock) {
|
||||||
|
Ok(totals) => Some(totals),
|
||||||
|
Err(reason) => { unavailable.push(reason); None }
|
||||||
|
}
|
||||||
|
} else { None };
|
||||||
(
|
(
|
||||||
quote.last_price,
|
quote.last_price,
|
||||||
snapshot.prev_close,
|
snapshot.prev_close,
|
||||||
Some(quote.volume_delta as f64),
|
totals.map(|total| total.0),
|
||||||
Some(quote.amount_delta),
|
totals.map(|total| total.1),
|
||||||
Some(quote.bid1),
|
Some(quote.bid1),
|
||||||
Some(quote.ask1),
|
Some(quote.ask1),
|
||||||
buy,
|
buy,
|
||||||
@@ -153,13 +161,24 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
|||||||
}
|
}
|
||||||
// A daily open does not reveal the session's volume/turnover.
|
// A daily open does not reveal the session's volume/turnover.
|
||||||
let completed = self.effective_execution_price_field(date) == PriceField::Close;
|
let completed = self.effective_execution_price_field(date) == PriceField::Close;
|
||||||
|
let totals = if cumulative_conditions && !completed {
|
||||||
|
let at = execution_clock.unwrap_or_else(|| date.and_hms_opt(9,30,0).unwrap());
|
||||||
|
match data.execution_session_totals(symbol, at) {
|
||||||
|
Ok(totals) => Some(totals),
|
||||||
|
Err(reason) => { unavailable.push(reason); None }
|
||||||
|
}
|
||||||
|
} else { None };
|
||||||
|
let amount = if completed && cumulative_conditions {
|
||||||
|
data.factor(date, symbol).and_then(|row| row.extra_factors.get("amount")).copied()
|
||||||
|
.map(|value| decimal(value, "amount")).transpose()?
|
||||||
|
} else { totals.map(|total| total.1) };
|
||||||
(
|
(
|
||||||
price,
|
price,
|
||||||
snapshot.prev_close,
|
snapshot.prev_close,
|
||||||
completed.then_some(snapshot.volume as f64),
|
if completed { Some(Decimal::from(snapshot.volume)) } else { totals.map(|total| total.0) },
|
||||||
|
amount,
|
||||||
|
None,
|
||||||
None,
|
None,
|
||||||
Some(price),
|
|
||||||
Some(price),
|
|
||||||
self.snapshot_execution_price(data, snapshot, OrderSide::Buy, None)?,
|
self.snapshot_execution_price(data, snapshot, OrderSide::Buy, None)?,
|
||||||
self.snapshot_execution_price(data, snapshot, OrderSide::Sell, None)?,
|
self.snapshot_execution_price(data, snapshot, OrderSide::Sell, None)?,
|
||||||
)
|
)
|
||||||
@@ -168,8 +187,8 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
|||||||
symbol: symbol.clone(),
|
symbol: symbol.clone(),
|
||||||
last_price: decimal(price, "price")?,
|
last_price: decimal(price, "price")?,
|
||||||
prev_close: Some(decimal(prev, "prev_close")?),
|
prev_close: Some(decimal(prev, "prev_close")?),
|
||||||
volume: volume.map(|v| decimal(v, "volume")).transpose()?,
|
volume,
|
||||||
turnover: amount.map(|v| decimal(v, "amount")).transpose()?,
|
turnover: amount,
|
||||||
bid_price_1: bid.map(|v| decimal(v, "bid")).transpose()?,
|
bid_price_1: bid.map(|v| decimal(v, "bid")).transpose()?,
|
||||||
ask_price_1: ask.map(|v| decimal(v, "ask")).transpose()?,
|
ask_price_1: ask.map(|v| decimal(v, "ask")).transpose()?,
|
||||||
is_kcb: Some(instrument.board.eq_ignore_ascii_case("KSH")),
|
is_kcb: Some(instrument.board.eq_ignore_ascii_case("KSH")),
|
||||||
@@ -182,7 +201,8 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
|||||||
sell_sizing_price: Some(decimal(sell_price, "sell_price")?),
|
sell_sizing_price: Some(decimal(sell_price, "sell_price")?),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect()
|
.collect::<Result<Vec<_>, BacktestError>>()?;
|
||||||
|
Ok((quotes, unavailable))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pool_etf_fallback_reference(&self, date: NaiveDate, data: &DataSet, symbol: &str, clock: Option<NaiveDateTime>) -> Result<Option<crate::etf_execution::EtfFallbackReference>, BacktestError> {
|
fn pool_etf_fallback_reference(&self, date: NaiveDate, data: &DataSet, symbol: &str, clock: Option<NaiveDateTime>) -> Result<Option<crate::etf_execution::EtfFallbackReference>, BacktestError> {
|
||||||
@@ -328,8 +348,8 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
|||||||
fallback_references.insert(symbol.clone(), reference);
|
fallback_references.insert(symbol.clone(), reference);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let quotes =
|
let (quotes, unavailable) = self.pool_quote_inputs(date, data, "e_scope, *global_execution_cursor,
|
||||||
self.pool_quote_inputs(date, data, "e_scope, *global_execution_cursor)?;
|
crate::stock_pool_quote_facts::requires_session_totals(&contract.rule))?;
|
||||||
let positions = pool_positions(portfolio, date)?;
|
let positions = pool_positions(portfolio, date)?;
|
||||||
let execution_state = portfolio
|
let execution_state = portfolio
|
||||||
.stock_pool_execution_state(&contract.pool_id)
|
.stock_pool_execution_state(&contract.pool_id)
|
||||||
@@ -448,7 +468,11 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
|||||||
Decimal::ZERO,
|
Decimal::ZERO,
|
||||||
Some(&fee),
|
Some(&fee),
|
||||||
)
|
)
|
||||||
.map_err(BacktestError::Execution)?;
|
.map_err(|error| BacktestError::Execution(if !unavailable.is_empty()
|
||||||
|
&& (error.contains("requires volume") || error.contains("requires amount")) {
|
||||||
|
format!("{error}; {}", unavailable.join("; "))
|
||||||
|
} else { error }))?;
|
||||||
|
report.diagnostics.extend(unavailable.into_iter().map(|reason| format!("stock_pool_quote_fact_unavailable {reason}")));
|
||||||
let mut updated = execution_state
|
let mut updated = execution_state
|
||||||
.record_plan(contract.signal_date, &contract.generation, &plan)
|
.record_plan(contract.signal_date, &contract.generation, &plan)
|
||||||
.map_err(BacktestError::Execution)?;
|
.map_err(BacktestError::Execution)?;
|
||||||
|
|||||||
@@ -284,6 +284,8 @@ pub struct CorporateAction {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct IntradayExecutionQuote {
|
pub struct IntradayExecutionQuote {
|
||||||
|
#[serde(default)]
|
||||||
|
pub observation_kind: QuoteObservationKind,
|
||||||
#[serde(with = "date_format")]
|
#[serde(with = "date_format")]
|
||||||
pub date: NaiveDate,
|
pub date: NaiveDate,
|
||||||
pub symbol: String,
|
pub symbol: String,
|
||||||
@@ -301,6 +303,14 @@ pub struct IntradayExecutionQuote {
|
|||||||
pub trading_phase: Option<String>,
|
pub trading_phase: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum QuoteObservationKind {
|
||||||
|
#[default]
|
||||||
|
Unspecified,
|
||||||
|
MinuteBar,
|
||||||
|
}
|
||||||
|
|
||||||
/// Sparse same-day fields layered onto an already-built immutable daily panel.
|
/// Sparse same-day fields layered onto an already-built immutable daily panel.
|
||||||
///
|
///
|
||||||
/// These fields do not participate in daily price series, adjustment series,
|
/// These fields do not participate in daily price series, adjustment series,
|
||||||
@@ -1407,6 +1417,7 @@ pub struct DataSet {
|
|||||||
corporate_actions_by_date: Arc<BTreeMap<NaiveDate, Vec<CorporateAction>>>,
|
corporate_actions_by_date: Arc<BTreeMap<NaiveDate, Vec<CorporateAction>>>,
|
||||||
execution_quotes_by_date: Arc<HashMap<NaiveDate, HashMap<String, Vec<IntradayExecutionQuote>>>>,
|
execution_quotes_by_date: Arc<HashMap<NaiveDate, HashMap<String, Vec<IntradayExecutionQuote>>>>,
|
||||||
execution_quote_dates: Arc<Vec<NaiveDate>>,
|
execution_quote_dates: Arc<Vec<NaiveDate>>,
|
||||||
|
condition_totals: Arc<std::sync::Mutex<crate::stock_pool_quote_facts::SessionTotalsCache>>,
|
||||||
order_book_depth_index: Arc<HashMap<(NaiveDate, String), Vec<IntradayOrderBookDepthLevel>>>,
|
order_book_depth_index: Arc<HashMap<(NaiveDate, String), Vec<IntradayOrderBookDepthLevel>>>,
|
||||||
benchmark_by_date: Arc<BTreeMap<NaiveDate, BenchmarkSnapshot>>,
|
benchmark_by_date: Arc<BTreeMap<NaiveDate, BenchmarkSnapshot>>,
|
||||||
market_series_by_symbol: Arc<AHashMap<String, Arc<SymbolPriceSeries>>>,
|
market_series_by_symbol: Arc<AHashMap<String, Arc<SymbolPriceSeries>>>,
|
||||||
@@ -1941,6 +1952,7 @@ impl DataSet {
|
|||||||
candidate_row_positions_by_date: Arc::new(candidate_row_positions_by_date),
|
candidate_row_positions_by_date: Arc::new(candidate_row_positions_by_date),
|
||||||
corporate_actions_by_date: Arc::new(corporate_actions_by_date),
|
corporate_actions_by_date: Arc::new(corporate_actions_by_date),
|
||||||
execution_quotes_by_date: Arc::new(execution_quotes_by_date),
|
execution_quotes_by_date: Arc::new(execution_quotes_by_date),
|
||||||
|
condition_totals: Arc::new(std::sync::Mutex::new(Default::default())),
|
||||||
execution_quote_dates: Arc::new(execution_quote_dates),
|
execution_quote_dates: Arc::new(execution_quote_dates),
|
||||||
order_book_depth_index: Arc::new(order_book_depth_index),
|
order_book_depth_index: Arc::new(order_book_depth_index),
|
||||||
benchmark_by_date: Arc::new(benchmark_by_date),
|
benchmark_by_date: Arc::new(benchmark_by_date),
|
||||||
@@ -2279,6 +2291,15 @@ impl DataSet {
|
|||||||
.unwrap_or(&[])
|
.unwrap_or(&[])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn execution_session_totals(&self, symbol: &str, at: NaiveDateTime) -> Result<(rust_decimal::Decimal, rust_decimal::Decimal), String> {
|
||||||
|
let mut cache = self.condition_totals.lock().map_err(|_| "stock_pool_session_prefix_cache_poisoned")?;
|
||||||
|
if cache.date != Some(at.date()) {
|
||||||
|
cache.date = Some(at.date());
|
||||||
|
cache.symbols.clear();
|
||||||
|
}
|
||||||
|
cache.symbols.entry(symbol.into()).or_insert_with(|| crate::stock_pool_quote_facts::MinutePrefix::build(at.date(), symbol, self.execution_quotes_on(at.date(), symbol))).at(at)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn has_execution_quotes_on_date(&self, date: NaiveDate) -> bool {
|
pub fn has_execution_quotes_on_date(&self, date: NaiveDate) -> bool {
|
||||||
self.execution_quotes_by_date
|
self.execution_quotes_by_date
|
||||||
.get(&date)
|
.get(&date)
|
||||||
@@ -2451,6 +2472,7 @@ impl DataSet {
|
|||||||
/// Replaces the run-local execution quote layer without touching the
|
/// Replaces the run-local execution quote layer without touching the
|
||||||
/// immutable daily panel.
|
/// immutable daily panel.
|
||||||
pub fn replace_execution_quotes(&mut self, quotes: Vec<IntradayExecutionQuote>) -> usize {
|
pub fn replace_execution_quotes(&mut self, quotes: Vec<IntradayExecutionQuote>) -> usize {
|
||||||
|
self.condition_totals = Arc::new(std::sync::Mutex::new(Default::default()));
|
||||||
let execution_quotes_by_date = build_execution_quote_index(quotes);
|
let execution_quotes_by_date = build_execution_quote_index(quotes);
|
||||||
let quote_count = execution_quotes_by_date
|
let quote_count = execution_quotes_by_date
|
||||||
.values()
|
.values()
|
||||||
@@ -2466,6 +2488,7 @@ impl DataSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_execution_quotes(&mut self, quotes: Vec<IntradayExecutionQuote>) -> usize {
|
pub fn add_execution_quotes(&mut self, quotes: Vec<IntradayExecutionQuote>) -> usize {
|
||||||
|
self.condition_totals = Arc::new(std::sync::Mutex::new(Default::default()));
|
||||||
let mut grouped = HashMap::<NaiveDate, HashMap<String, Vec<IntradayExecutionQuote>>>::new();
|
let mut grouped = HashMap::<NaiveDate, HashMap<String, Vec<IntradayExecutionQuote>>>::new();
|
||||||
for quote in quotes {
|
for quote in quotes {
|
||||||
grouped
|
grouped
|
||||||
@@ -2566,6 +2589,7 @@ impl DataSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_execution_quotes_on_date(&mut self, date: NaiveDate) -> usize {
|
pub fn remove_execution_quotes_on_date(&mut self, date: NaiveDate) -> usize {
|
||||||
|
self.condition_totals = Arc::new(std::sync::Mutex::new(Default::default()));
|
||||||
let removed = Arc::make_mut(&mut self.execution_quotes_by_date).remove(&date);
|
let removed = Arc::make_mut(&mut self.execution_quotes_by_date).remove(&date);
|
||||||
let Some(rows_by_symbol) = removed else {
|
let Some(rows_by_symbol) = removed else {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -2578,6 +2602,7 @@ impl DataSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn release_execution_quotes_on_date(&mut self, date: NaiveDate) -> usize {
|
pub fn release_execution_quotes_on_date(&mut self, date: NaiveDate) -> usize {
|
||||||
|
self.condition_totals = Arc::new(std::sync::Mutex::new(Default::default()));
|
||||||
let row_count = self
|
let row_count = self
|
||||||
.execution_quotes_by_date
|
.execution_quotes_by_date
|
||||||
.get(&date)
|
.get(&date)
|
||||||
@@ -5158,7 +5183,7 @@ mod tests {
|
|||||||
&run_data.execution_quote_dates
|
&run_data.execution_quote_dates
|
||||||
));
|
));
|
||||||
|
|
||||||
run_data.add_execution_quotes(vec![IntradayExecutionQuote {
|
run_data.add_execution_quotes(vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
timestamp: NaiveDateTime::parse_from_str("2025-01-02 10:18:00", "%Y-%m-%d %H:%M:%S")
|
timestamp: NaiveDateTime::parse_from_str("2025-01-02 10:18:00", "%Y-%m-%d %H:%M:%S")
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
@@ -5301,7 +5326,7 @@ mod tests {
|
|||||||
vec![benchmark_row("2025-01-02", 12.0)],
|
vec![benchmark_row("2025-01-02", 12.0)],
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let quote = IntradayExecutionQuote {
|
let quote = IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -5403,7 +5428,7 @@ mod tests {
|
|||||||
successor_cash: None,
|
successor_cash: None,
|
||||||
};
|
};
|
||||||
corporate_actions.push(corporate_action.clone());
|
corporate_actions.push(corporate_action.clone());
|
||||||
execution_quotes.push(IntradayExecutionQuote {
|
execution_quotes.push(IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbols[0].to_string(),
|
symbol: symbols[0].to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
|
timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
|
||||||
@@ -6114,7 +6139,7 @@ mod tests {
|
|||||||
vec![benchmark_row("2025-01-02", 12.0)],
|
vec![benchmark_row("2025-01-02", 12.0)],
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let quote = |symbol: &str, time: &str| IntradayExecutionQuote {
|
let quote = |symbol: &str, time: &str| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
timestamp: NaiveDateTime::parse_from_str(
|
timestamp: NaiveDateTime::parse_from_str(
|
||||||
&format!("2025-01-02 {time}"),
|
&format!("2025-01-02 {time}"),
|
||||||
@@ -6193,7 +6218,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn shared_execution_quote_release_does_not_clone_the_base_map() {
|
fn shared_execution_quote_release_does_not_clone_the_base_map() {
|
||||||
let date = NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap();
|
let date = NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap();
|
||||||
let quote = IntradayExecutionQuote {
|
let quote = IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
timestamp: NaiveDateTime::parse_from_str("2025-01-02 10:18:00", "%Y-%m-%d %H:%M:%S")
|
timestamp: NaiveDateTime::parse_from_str("2025-01-02 10:18:00", "%Y-%m-%d %H:%M:%S")
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
|||||||
@@ -746,6 +746,15 @@ where
|
|||||||
if self.execution_quote_loader.is_none() {
|
if self.execution_quote_loader.is_none() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
let cumulative_conditions = decision.order_intents.iter().any(|intent| {
|
||||||
|
matches!(intent.unwrapped(), OrderIntent::StockPool { contract }
|
||||||
|
if crate::stock_pool_quote_facts::requires_session_totals(&contract.rule))
|
||||||
|
});
|
||||||
|
if cumulative_conditions && (self.broker.execution_price_field() != PriceField::Close
|
||||||
|
|| start_time.is_some() || self.broker.intraday_execution_start_time().is_some()) {
|
||||||
|
let mut symbols = execution_quote_symbols_for_decision(decision, portfolio, open_orders);
|
||||||
|
self.load_missing_execution_quotes(execution_date, None, None, &mut symbols)?;
|
||||||
|
}
|
||||||
let submission_time = start_time.or_else(|| self.broker.intraday_execution_start_time());
|
let submission_time = start_time.or_else(|| self.broker.intraday_execution_start_time());
|
||||||
let post_close_window = self.broker.post_close_execution_quote_window_for_order(
|
let post_close_window = self.broker.post_close_execution_quote_window_for_order(
|
||||||
execution_date,
|
execution_date,
|
||||||
@@ -5796,7 +5805,7 @@ mod tests {
|
|||||||
fn physical_on_day_rules_keep_each_actual_submission_time() {
|
fn physical_on_day_rules_keep_each_actual_submission_time() {
|
||||||
let date = d(2026, 7, 6);
|
let date = d(2026, 7, 6);
|
||||||
let quotes = vec![
|
let quotes = vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: SYMBOL.to_string(),
|
symbol: SYMBOL.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("morning timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("morning timestamp"),
|
||||||
@@ -5809,7 +5818,7 @@ mod tests {
|
|||||||
amount_delta: 110_000.0,
|
amount_delta: 110_000.0,
|
||||||
trading_phase: Some("continuous_auction".to_string()),
|
trading_phase: Some("continuous_auction".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: SYMBOL.to_string(),
|
symbol: SYMBOL.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 19, 0).expect("future timestamp"),
|
timestamp: date.and_hms_opt(10, 19, 0).expect("future timestamp"),
|
||||||
@@ -5822,7 +5831,7 @@ mod tests {
|
|||||||
amount_delta: 990_000.0,
|
amount_delta: 990_000.0,
|
||||||
trading_phase: Some("continuous_auction".to_string()),
|
trading_phase: Some("continuous_auction".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: SYMBOL.to_string(),
|
symbol: SYMBOL.to_string(),
|
||||||
timestamp: date.and_hms_opt(15, 10, 0).expect("post-close timestamp"),
|
timestamp: date.and_hms_opt(15, 10, 0).expect("post-close timestamp"),
|
||||||
@@ -5950,7 +5959,7 @@ mod tests {
|
|||||||
let closing_only = matches!(scenario,2|3);
|
let closing_only = matches!(scenario,2|3);
|
||||||
let delayed = scenario == 4;
|
let delayed = scenario == 4;
|
||||||
let date = if closing_only { d(2026, 7, 6) } else if delayed { d(2026, 6, 2) } else { d(2026, 6, 1) };
|
let date = if closing_only { d(2026, 7, 6) } else if delayed { d(2026, 6, 2) } else { d(2026, 6, 1) };
|
||||||
let quote = |hour, minute, price| IntradayExecutionQuote {
|
let quote = |hour, minute, price| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date, symbol: SYMBOL.into(), timestamp: date.and_hms_opt(hour, minute, 0).unwrap(),
|
date, symbol: SYMBOL.into(), timestamp: date.and_hms_opt(hour, minute, 0).unwrap(),
|
||||||
last_price: price, bid1: price, ask1: price, bid1_volume: 10_000, ask1_volume: 10_000,
|
last_price: price, bid1: price, ask1: price, bid1_volume: 10_000, ask1_volume: 10_000,
|
||||||
volume_delta: 10_000, amount_delta: price * 10_000.0, trading_phase: None,
|
volume_delta: 10_000, amount_delta: price * 10_000.0, trading_phase: None,
|
||||||
@@ -6073,7 +6082,7 @@ mod tests {
|
|||||||
Ok(request
|
Ok(request
|
||||||
.symbols
|
.symbols
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: request.date,
|
date: request.date,
|
||||||
symbol,
|
symbol,
|
||||||
timestamp: request.date.and_hms_opt(15, 5, 0).expect("valid timestamp"),
|
timestamp: request.date.and_hms_opt(15, 5, 0).expect("valid timestamp"),
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ pub mod stock_pool_execution;
|
|||||||
pub mod stock_pool_index_policy;
|
pub mod stock_pool_index_policy;
|
||||||
pub mod stock_pool_market_cap;
|
pub mod stock_pool_market_cap;
|
||||||
pub mod stock_pool_state;
|
pub mod stock_pool_state;
|
||||||
|
pub mod stock_pool_quote_facts;
|
||||||
pub mod signal_contract;
|
pub mod signal_contract;
|
||||||
pub mod strategy_ai;
|
pub mod strategy_ai;
|
||||||
pub mod universe;
|
pub mod universe;
|
||||||
|
|||||||
@@ -212,6 +212,11 @@ pub fn build_dataset_context(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn specs_in_value(value: &Value) -> Result<Vec<PatternSpec>, String> {
|
pub fn specs_in_value(value: &Value) -> Result<Vec<PatternSpec>, String> {
|
||||||
|
// A runner bundle also contains source/extract copies. Follow the same
|
||||||
|
// authoritative spec selection as the execution loader, not those copies.
|
||||||
|
if let Some(spec) = value.get("strategySpec").or_else(|| value.get("strategy_spec")) {
|
||||||
|
return specs_in_value(spec);
|
||||||
|
}
|
||||||
let mut specs = Vec::new();
|
let mut specs = Vec::new();
|
||||||
match value {
|
match value {
|
||||||
Value::String(text) => specs.extend(crate::daily_patterns::expression_specs(text)?),
|
Value::String(text) => specs.extend(crate::daily_patterns::expression_specs(text)?),
|
||||||
@@ -268,9 +273,15 @@ mod tests {
|
|||||||
for (pool_key, source_key) in [("stockPool", "sourceCode"), ("stock_pool", "source_code")] {
|
for (pool_key, source_key) in [("stockPool", "sourceCode"), ("stock_pool", "source_code")] {
|
||||||
let value = json!({pool_key:pool,source_key:source,"runtimeExpressions":{"trading":{"buyFilterExpr":expr}}});
|
let value = json!({pool_key:pool,source_key:source,"runtimeExpressions":{"trading":{"buyFilterExpr":expr}}});
|
||||||
assert_eq!(specs_in_value(&value).unwrap().len(), 2);
|
assert_eq!(specs_in_value(&value).unwrap().len(), 2);
|
||||||
|
for wrapper in ["strategySpec", "strategy_spec"] {
|
||||||
|
let bundle = json!({wrapper:value,"strategy_source":{"source_code":source},
|
||||||
|
"strategy_extract":{"parameters":{"source_code":source}}});
|
||||||
|
assert_eq!(specs_in_value(&bundle).unwrap().len(), 2);
|
||||||
|
}
|
||||||
let mut invalid = value.clone();
|
let mut invalid = value.clone();
|
||||||
invalid[pool_key]["exit_signals"][0]["when_expr"] = json!("pattern_signal(not-json)");
|
invalid[pool_key]["exit_signals"][0]["when_expr"] = json!("pattern_signal(not-json)");
|
||||||
assert!(specs_in_value(&invalid).is_err(), "invalid actual conditions must still fail");
|
assert!(specs_in_value(&invalid).is_err(), "invalid actual conditions must still fail");
|
||||||
|
assert!(specs_in_value(&json!({"strategySpec":invalid})).is_err());
|
||||||
}
|
}
|
||||||
assert_eq!(specs_in_value(&json!({"sourceCode":format!("risk.stop_loss({expr})")})).unwrap().len(),1);
|
assert_eq!(specs_in_value(&json!({"sourceCode":format!("risk.stop_loss({expr})")})).unwrap().len(),1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14718,7 +14718,7 @@ mod tests {
|
|||||||
let date = d(2025, 1, 2);
|
let date = d(2025, 1, 2);
|
||||||
let symbol = "000001.SZ";
|
let symbol = "000001.SZ";
|
||||||
let parts = single_symbol_platform_data(&[date], symbol).snapshot_components();
|
let parts = single_symbol_platform_data(&[date], symbol).snapshot_components();
|
||||||
let quotes = [(10, 18, 9.5), (14, 59, 10.5)].into_iter().map(|(hour, minute, price)| IntradayExecutionQuote {
|
let quotes = [(10, 18, 9.5), (14, 59, 10.5)].into_iter().map(|(hour, minute, price)| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date, symbol: symbol.to_string(), timestamp: date.and_hms_opt(hour, minute, 0).unwrap(),
|
date, symbol: symbol.to_string(), timestamp: date.and_hms_opt(hour, minute, 0).unwrap(),
|
||||||
last_price: price, bid1: price, ask1: price, bid1_volume: 1000, ask1_volume: 1000,
|
last_price: price, bid1: price, ask1: price, bid1_volume: 1000, ask1_volume: 1000,
|
||||||
volume_delta: 1000, amount_delta: price * 1000.0, trading_phase: Some("continuous".to_string()),
|
volume_delta: 1000, amount_delta: price * 1000.0, trading_phase: Some("continuous".to_string()),
|
||||||
@@ -17846,7 +17846,7 @@ mod tests {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
|
||||||
@@ -17989,7 +17989,7 @@ mod tests {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
|
||||||
@@ -18077,7 +18077,7 @@ mod tests {
|
|||||||
lower_limit: 4.50,
|
lower_limit: 4.50,
|
||||||
price_tick: 0.01,
|
price_tick: 0.01,
|
||||||
};
|
};
|
||||||
let quote = IntradayExecutionQuote {
|
let quote = IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
|
||||||
@@ -18209,7 +18209,7 @@ mod tests {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
|
||||||
@@ -18454,7 +18454,7 @@ mod tests {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 39, 59).unwrap(),
|
timestamp: date.and_hms_opt(10, 39, 59).unwrap(),
|
||||||
@@ -18871,7 +18871,7 @@ mod tests {
|
|||||||
],
|
],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: first_date,
|
date: first_date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: first_date.and_hms_opt(10, 40, 0).expect("valid timestamp"),
|
timestamp: first_date.and_hms_opt(10, 40, 0).expect("valid timestamp"),
|
||||||
@@ -18884,7 +18884,7 @@ mod tests {
|
|||||||
amount_delta: 23_990.0,
|
amount_delta: 23_990.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: second_date,
|
date: second_date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: second_date.and_hms_opt(10, 31, 0).expect("valid timestamp"),
|
timestamp: second_date.and_hms_opt(10, 31, 0).expect("valid timestamp"),
|
||||||
@@ -19149,7 +19149,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: delayed_symbol.to_string(),
|
symbol: delayed_symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
|
||||||
@@ -19162,7 +19162,7 @@ mod tests {
|
|||||||
amount_delta: 146_200.0,
|
amount_delta: 146_200.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: delayed_symbol.to_string(),
|
symbol: delayed_symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -19175,7 +19175,7 @@ mod tests {
|
|||||||
amount_delta: 145_000.0,
|
amount_delta: 145_000.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: other_symbol.to_string(),
|
symbol: other_symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -19450,7 +19450,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: delayed_symbol.to_string(),
|
symbol: delayed_symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
|
||||||
@@ -19463,7 +19463,7 @@ mod tests {
|
|||||||
amount_delta: 146_300.0,
|
amount_delta: 146_300.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: other_symbol.to_string(),
|
symbol: other_symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -19699,7 +19699,7 @@ mod tests {
|
|||||||
],
|
],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: first_date,
|
date: first_date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: first_date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
|
timestamp: first_date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
|
||||||
@@ -19712,7 +19712,7 @@ mod tests {
|
|||||||
amount_delta: 56_450.0,
|
amount_delta: 56_450.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: first_date,
|
date: first_date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: first_date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: first_date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -19725,7 +19725,7 @@ mod tests {
|
|||||||
amount_delta: 49_300.0,
|
amount_delta: 49_300.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: second_date,
|
date: second_date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: second_date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
|
timestamp: second_date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
|
||||||
@@ -20027,7 +20027,7 @@ mod tests {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
|
||||||
@@ -20485,7 +20485,7 @@ mod tests {
|
|||||||
vec![candidate],
|
vec![candidate],
|
||||||
vec![benchmark],
|
vec![benchmark],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(9, 30, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(9, 30, 0).expect("timestamp"),
|
||||||
@@ -20672,7 +20672,7 @@ mod tests {
|
|||||||
prev_close: 998.0,
|
prev_close: 998.0,
|
||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
};
|
};
|
||||||
let quote = |date: NaiveDate, last_price: f64, volume_delta: u64| IntradayExecutionQuote {
|
let quote = |date: NaiveDate, last_price: f64, volume_delta: u64| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 15, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(10, 15, 0).expect("timestamp"),
|
||||||
@@ -22285,7 +22285,7 @@ mod tests {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
|
||||||
@@ -22936,7 +22936,7 @@ mod tests {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
symbols
|
symbols
|
||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: (*symbol).to_string(),
|
symbol: (*symbol).to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
|
||||||
@@ -23090,7 +23090,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(9, 31, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(9, 31, 0).expect("timestamp"),
|
||||||
@@ -23103,7 +23103,7 @@ mod tests {
|
|||||||
amount_delta: 110_000.0,
|
amount_delta: 110_000.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
|
||||||
@@ -23247,7 +23247,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(9, 31, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(9, 31, 0).expect("timestamp"),
|
||||||
@@ -23260,7 +23260,7 @@ mod tests {
|
|||||||
amount_delta: 108_000.0,
|
amount_delta: 108_000.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
|
||||||
@@ -23404,7 +23404,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(9, 31, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(9, 31, 0).expect("timestamp"),
|
||||||
@@ -23417,7 +23417,7 @@ mod tests {
|
|||||||
amount_delta: 110_000.0,
|
amount_delta: 110_000.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
|
||||||
@@ -23574,7 +23574,7 @@ mod tests {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
symbols
|
symbols
|
||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: (*symbol).to_string(),
|
symbol: (*symbol).to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("timestamp"),
|
||||||
@@ -23889,7 +23889,7 @@ mod tests {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
|
||||||
@@ -24045,7 +24045,7 @@ mod tests {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
symbols
|
symbols
|
||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: (*symbol).to_string(),
|
symbol: (*symbol).to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 15, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(10, 15, 0).expect("timestamp"),
|
||||||
@@ -24214,7 +24214,7 @@ mod tests {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 15, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(10, 15, 0).expect("timestamp"),
|
||||||
@@ -24372,7 +24372,7 @@ mod tests {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"),
|
||||||
@@ -24494,7 +24494,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 40, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 40, 0).expect("valid timestamp"),
|
||||||
@@ -24507,7 +24507,7 @@ mod tests {
|
|||||||
amount_delta: 1_000.0,
|
amount_delta: 1_000.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(14, 58, 59).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(14, 58, 59).expect("valid timestamp"),
|
||||||
@@ -24520,7 +24520,7 @@ mod tests {
|
|||||||
amount_delta: 2_000.0,
|
amount_delta: 2_000.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(14, 59, 2).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(14, 59, 2).expect("valid timestamp"),
|
||||||
@@ -24743,7 +24743,7 @@ mod tests {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 39, 59).unwrap(),
|
timestamp: date.and_hms_opt(10, 39, 59).unwrap(),
|
||||||
@@ -24980,7 +24980,7 @@ mod tests {
|
|||||||
.collect(),
|
.collect(),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: execution_date,
|
date: execution_date,
|
||||||
symbol: limit_symbol.to_string(),
|
symbol: limit_symbol.to_string(),
|
||||||
timestamp: execution_date
|
timestamp: execution_date
|
||||||
@@ -24995,7 +24995,7 @@ mod tests {
|
|||||||
amount_delta: 233.0,
|
amount_delta: 233.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: execution_date,
|
date: execution_date,
|
||||||
symbol: fallback_symbol.to_string(),
|
symbol: fallback_symbol.to_string(),
|
||||||
timestamp: execution_date
|
timestamp: execution_date
|
||||||
@@ -25233,7 +25233,7 @@ mod tests {
|
|||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: execution_date,
|
date: execution_date,
|
||||||
symbol: candidate_symbol.to_string(),
|
symbol: candidate_symbol.to_string(),
|
||||||
timestamp: execution_date.and_hms_opt(9, 33, 0).unwrap(),
|
timestamp: execution_date.and_hms_opt(9, 33, 0).unwrap(),
|
||||||
@@ -27761,7 +27761,7 @@ mod tests {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -28084,7 +28084,7 @@ mod tests {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
symbols
|
symbols
|
||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: (*symbol).to_string(),
|
symbol: (*symbol).to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -28242,7 +28242,7 @@ mod tests {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
symbols
|
symbols
|
||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: (*symbol).to_string(),
|
symbol: (*symbol).to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
|
||||||
@@ -28572,7 +28572,7 @@ mod tests {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
symbols
|
symbols
|
||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: (*symbol).to_string(),
|
symbol: (*symbol).to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
|
timestamp: date.and_hms_opt(10, 40, 0).expect("timestamp"),
|
||||||
@@ -29342,7 +29342,7 @@ mod tests {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
symbols
|
symbols
|
||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: (*symbol).to_string(),
|
symbol: (*symbol).to_string(),
|
||||||
timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"),
|
||||||
@@ -29525,7 +29525,7 @@ mod tests {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
symbols
|
symbols
|
||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: (*symbol).to_string(),
|
symbol: (*symbol).to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -29736,7 +29736,7 @@ mod tests {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
symbols
|
symbols
|
||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: (*symbol).to_string(),
|
symbol: (*symbol).to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -29987,7 +29987,7 @@ mod tests {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
symbols
|
symbols
|
||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: (*symbol).to_string(),
|
symbol: (*symbol).to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -30203,7 +30203,7 @@ mod tests {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
symbols
|
symbols
|
||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: (*symbol).to_string(),
|
symbol: (*symbol).to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -30569,7 +30569,7 @@ mod tests {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -30750,7 +30750,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: delayed_symbol.to_string(),
|
symbol: delayed_symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
|
||||||
@@ -30763,7 +30763,7 @@ mod tests {
|
|||||||
amount_delta: 105_000.0,
|
amount_delta: 105_000.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: delayed_symbol.to_string(),
|
symbol: delayed_symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -30776,7 +30776,7 @@ mod tests {
|
|||||||
amount_delta: 90_000.0,
|
amount_delta: 90_000.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: held_symbol.to_string(),
|
symbol: held_symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -30789,7 +30789,7 @@ mod tests {
|
|||||||
amount_delta: 100_000.0,
|
amount_delta: 100_000.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: buy_symbol.to_string(),
|
symbol: buy_symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -30978,7 +30978,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: delayed_symbol.to_string(),
|
symbol: delayed_symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
|
||||||
@@ -30991,7 +30991,7 @@ mod tests {
|
|||||||
amount_delta: 4_200.0,
|
amount_delta: 4_200.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: held_symbol.to_string(),
|
symbol: held_symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -31004,7 +31004,7 @@ mod tests {
|
|||||||
amount_delta: 100_000.0,
|
amount_delta: 100_000.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: buy_symbol.to_string(),
|
symbol: buy_symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -31215,7 +31215,7 @@ mod tests {
|
|||||||
.flat_map(|symbol| {
|
.flat_map(|symbol| {
|
||||||
let mut quotes = Vec::new();
|
let mut quotes = Vec::new();
|
||||||
if *symbol == delayed_symbol {
|
if *symbol == delayed_symbol {
|
||||||
quotes.push(IntradayExecutionQuote {
|
quotes.push(IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: (*symbol).to_string(),
|
symbol: (*symbol).to_string(),
|
||||||
timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
|
||||||
@@ -31229,7 +31229,7 @@ mod tests {
|
|||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
quotes.push(IntradayExecutionQuote {
|
quotes.push(IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: (*symbol).to_string(),
|
symbol: (*symbol).to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -31455,7 +31455,7 @@ mod tests {
|
|||||||
.flat_map(|symbol| {
|
.flat_map(|symbol| {
|
||||||
let mut quotes = Vec::new();
|
let mut quotes = Vec::new();
|
||||||
if *symbol == delayed_symbol {
|
if *symbol == delayed_symbol {
|
||||||
quotes.push(IntradayExecutionQuote {
|
quotes.push(IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: (*symbol).to_string(),
|
symbol: (*symbol).to_string(),
|
||||||
timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(9, 31, 0).expect("valid timestamp"),
|
||||||
@@ -31469,7 +31469,7 @@ mod tests {
|
|||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
quotes.push(IntradayExecutionQuote {
|
quotes.push(IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: (*symbol).to_string(),
|
symbol: (*symbol).to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -32141,7 +32141,7 @@ mod tests {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
symbols
|
symbols
|
||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: (*symbol).to_string(),
|
symbol: (*symbol).to_string(),
|
||||||
timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"),
|
||||||
@@ -32430,7 +32430,7 @@ mod tests {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
symbols
|
symbols
|
||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: (*symbol).to_string(),
|
symbol: (*symbol).to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -32641,7 +32641,7 @@ mod tests {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
symbols
|
symbols
|
||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.clone(),
|
symbol: symbol.clone(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -32848,7 +32848,7 @@ mod tests {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
symbols
|
symbols
|
||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: (*symbol).to_string(),
|
symbol: (*symbol).to_string(),
|
||||||
timestamp: date.and_hms_opt(14, 59, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(14, 59, 0).expect("valid timestamp"),
|
||||||
@@ -32991,7 +32991,7 @@ mod tests {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("valid timestamp"),
|
||||||
@@ -33214,7 +33214,7 @@ mod tests {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"),
|
||||||
@@ -33351,7 +33351,7 @@ mod tests {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: other_symbol.to_string(),
|
symbol: other_symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
|
timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
|
||||||
@@ -33492,7 +33492,7 @@ mod tests {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: decision_date,
|
date: decision_date,
|
||||||
symbol: other_symbol.to_string(),
|
symbol: other_symbol.to_string(),
|
||||||
timestamp: decision_date.and_hms_opt(10, 18, 0).unwrap(),
|
timestamp: decision_date.and_hms_opt(10, 18, 0).unwrap(),
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
//! Condition facts are distinct from the quote's per-observation fill capacity.
|
||||||
|
//! Only a complete, declared raw-minute prefix can prove a session total.
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use chrono::{NaiveDate, NaiveDateTime, NaiveTime, Timelike};
|
||||||
|
use rust_decimal::Decimal;
|
||||||
|
use crate::data::IntradayExecutionQuote;
|
||||||
|
use crate::stock_pool_execution::{parse_stock_pool_condition, StockPoolExecutionRule};
|
||||||
|
|
||||||
|
pub fn requires_session_totals(rule: &StockPoolExecutionRule) -> bool {
|
||||||
|
[rule.buy_condition.as_str(), if rule.sell_trigger_mode == "condition" { rule.sell_condition.as_str() } else { "" }].into_iter().any(|condition| {
|
||||||
|
parse_stock_pool_condition(condition).is_some_and(|(_, field, _, _)| matches!(field.as_str(), "volume" | "amount"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The cash-equity minute feed includes the opening observation and a separate
|
||||||
|
/// post-close segment. Trading eligibility remains owned by the dated rules.
|
||||||
|
fn next_minute(time: NaiveTime) -> Option<NaiveTime> {
|
||||||
|
let minute = time.hour() * 60 + time.minute();
|
||||||
|
let next = match minute {
|
||||||
|
570..=689 | 781..=899 | 906..=929 => minute + 1,
|
||||||
|
690 => 781,
|
||||||
|
900 => 906,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
NaiveTime::from_hms_opt(next / 60, next % 60, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct SessionTotalsCache {
|
||||||
|
pub date: Option<NaiveDate>,
|
||||||
|
pub symbols: BTreeMap<String, MinutePrefix>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct MinutePrefix {
|
||||||
|
values: BTreeMap<NaiveTime, (Decimal, Decimal)>,
|
||||||
|
failure: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MinutePrefix {
|
||||||
|
pub fn build(date: NaiveDate, symbol: &str, quotes: &[IntradayExecutionQuote]) -> Self {
|
||||||
|
let mut values = BTreeMap::new();
|
||||||
|
let mut expected = NaiveTime::from_hms_opt(9, 30, 0).unwrap();
|
||||||
|
let mut volume = 0_u64;
|
||||||
|
let mut amount = Decimal::ZERO;
|
||||||
|
let mut failure = format!("stock_pool_session_prefix_missing:{symbol}:{date}:{expected}");
|
||||||
|
for quote in quotes {
|
||||||
|
let time = quote.timestamp.time();
|
||||||
|
if quote.date != date || quote.timestamp.date() != date || quote.symbol != symbol {
|
||||||
|
failure = format!("stock_pool_session_prefix_identity_invalid:{symbol}:{date}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if time != expected {
|
||||||
|
failure = format!("stock_pool_session_prefix_missing:{symbol}:{date}:expected={expected}:observed={time}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if quote.observation_kind != crate::data::QuoteObservationKind::MinuteBar {
|
||||||
|
failure = format!("stock_pool_session_prefix_basis_unverified:{symbol}:{date}:{time}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let Some(next_volume) = volume.checked_add(quote.volume_delta) else {
|
||||||
|
failure = format!("stock_pool_session_volume_overflow:{symbol}:{date}:{time}");
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let delta = if quote.amount_delta.is_finite() && quote.amount_delta >= 0.0 {
|
||||||
|
quote.amount_delta.to_string().parse::<Decimal>().ok()
|
||||||
|
} else { None };
|
||||||
|
let Some(next_amount) = delta.and_then(|delta| amount.checked_add(delta)) else {
|
||||||
|
failure = format!("stock_pool_session_amount_invalid:{symbol}:{date}:{time}");
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
volume = next_volume;
|
||||||
|
amount = next_amount;
|
||||||
|
values.insert(time, (Decimal::from(volume), amount));
|
||||||
|
let Some(next) = next_minute(time) else { break };
|
||||||
|
expected = next;
|
||||||
|
failure = format!("stock_pool_session_prefix_missing:{symbol}:{date}:{expected}");
|
||||||
|
}
|
||||||
|
Self { values, failure }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn at(&self, at: NaiveDateTime) -> Result<(Decimal, Decimal), String> {
|
||||||
|
let time = at.time().with_second(0).unwrap().with_nanosecond(0).unwrap();
|
||||||
|
self.values.get(&time).copied().ok_or_else(|| self.failure.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
fn quote(hour: u32, minute: u32, volume: u64, amount: f64) -> IntradayExecutionQuote {
|
||||||
|
let date = NaiveDate::from_ymd_opt(2026, 9, 11).unwrap();
|
||||||
|
IntradayExecutionQuote { observation_kind: crate::data::QuoteObservationKind::MinuteBar, date, symbol: "000001.SZ".into(), timestamp: date.and_hms_opt(hour, minute, 0).unwrap(),
|
||||||
|
last_price: 10., bid1: 0., ask1: 0., bid1_volume: 0, ask1_volume: 0,
|
||||||
|
volume_delta: volume, amount_delta: amount, trading_phase: Some("minute_execution_prices:raw-minute".into()) }
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn totals_use_only_the_complete_observed_prefix_and_keep_decimal_amounts() {
|
||||||
|
let mut rows = vec![quote(9,30,100,10.01), quote(9,31,0,0.), quote(9,32,200,20.02)];
|
||||||
|
let prefix = MinutePrefix::build(rows[0].date, "000001.SZ", &rows);
|
||||||
|
assert_eq!(prefix.at(rows[1].timestamp).unwrap(), (100.into(), Decimal::new(1001,2)));
|
||||||
|
assert_eq!(prefix.at(rows[2].timestamp).unwrap(), (300.into(), Decimal::new(3003,2)));
|
||||||
|
rows[2].volume_delta = 999999;
|
||||||
|
rows[2].amount_delta = f64::NAN;
|
||||||
|
let changed = MinutePrefix::build(rows[0].date, "000001.SZ", &rows);
|
||||||
|
assert_eq!(changed.at(rows[1].timestamp).unwrap(), prefix.at(rows[1].timestamp).unwrap());
|
||||||
|
assert!(changed.at(rows[2].timestamp).unwrap_err().contains("amount_invalid"));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn sparse_unverified_and_overflowing_quotes_cannot_be_called_session_totals() {
|
||||||
|
let first = quote(9,30,100,1000.);
|
||||||
|
for rows in [vec![quote(9,31,100,1000.)], vec![first.clone(), quote(9,32,100,1000.)]] {
|
||||||
|
let prefix = MinutePrefix::build(first.date, "000001.SZ", &rows);
|
||||||
|
assert!(prefix.at(rows.last().unwrap().timestamp).unwrap_err().contains("prefix_missing"));
|
||||||
|
}
|
||||||
|
let mut unknown = first.clone(); unknown.observation_kind = Default::default();
|
||||||
|
assert!(MinutePrefix::build(first.date, "000001.SZ", &[unknown]).at(first.timestamp).unwrap_err().contains("basis_unverified"));
|
||||||
|
let rows = [quote(9,30,u64::MAX,0.), quote(9,31,1,0.)];
|
||||||
|
assert!(MinutePrefix::build(first.date, "000001.SZ", &rows).at(rows[1].timestamp).unwrap_err().contains("volume_overflow"));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn lunch_and_post_close_gaps_follow_the_minute_feed_segments() {
|
||||||
|
let mut rows = Vec::new(); let mut time = NaiveTime::from_hms_opt(9,30,0).unwrap();
|
||||||
|
loop {
|
||||||
|
rows.push(quote(time.hour(), time.minute(), 1, 0.01));
|
||||||
|
let Some(next) = next_minute(time) else { break }; time=next;
|
||||||
|
}
|
||||||
|
let prefix=MinutePrefix::build(rows[0].date,"000001.SZ",&rows);
|
||||||
|
assert_eq!(prefix.at(rows.last().unwrap().timestamp).unwrap(), (Decimal::from(rows.len()), Decimal::new(rows.len() as i64,2)));
|
||||||
|
assert!(!rows.iter().any(|row| row.timestamp.time().hour()==12));
|
||||||
|
assert!(!rows.iter().any(|row| row.timestamp.time()==NaiveTime::from_hms_opt(13,0,0).unwrap()));
|
||||||
|
assert!(!rows.iter().any(|row| row.timestamp.time().hour()==15 && (1..6).contains(&row.timestamp.time().minute())));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires FIDC_SESSION_PREFIX_SOURCE_JSON from the frozen Source minute response"]
|
||||||
|
fn real_source_session_prefix_matches_observed_checkpoints() {
|
||||||
|
let path=std::env::var("FIDC_SESSION_PREFIX_SOURCE_JSON").expect("explicit Source evidence path");
|
||||||
|
let rows:Vec<IntradayExecutionQuote>=serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap();
|
||||||
|
let date=NaiveDate::from_ymd_opt(2026,9,8).unwrap();
|
||||||
|
assert_eq!(rows.len(),242);
|
||||||
|
let prefix=MinutePrefix::build(date,"000063.SZ",&rows);
|
||||||
|
for (hour,minute,volume,amount) in [(9,30,512700,17103672),(9,31,2296631,76576756),(9,32,2983531,99471024),(11,30,27868847,928167630),(13,1,28495518,948994890),(15,0,45625008,1518115100)] {
|
||||||
|
assert_eq!(prefix.at(date.and_hms_opt(hour,minute,0).unwrap()).unwrap(),(Decimal::from(volume),Decimal::from(amount)));
|
||||||
|
}
|
||||||
|
assert!(prefix.at(date.and_hms_opt(15,30,0).unwrap()).unwrap_err().contains("prefix_missing"),"one final aggregate is not a verified intraday prefix");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -196,7 +196,7 @@ fn engine_uses_preplanned_decision_symbols_without_recomputing_strategy_plan() {
|
|||||||
Ok(request
|
Ok(request
|
||||||
.symbols
|
.symbols
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: request.date,
|
date: request.date,
|
||||||
symbol,
|
symbol,
|
||||||
timestamp: request.date.and_time(t(10, 17, 59)),
|
timestamp: request.date.and_time(t(10, 17, 59)),
|
||||||
@@ -411,7 +411,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
|||||||
Ok(request
|
Ok(request
|
||||||
.symbols
|
.symbols
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: request.date,
|
date: request.date,
|
||||||
symbol,
|
symbol,
|
||||||
timestamp: request.date.and_time(t(10, 39, 59)),
|
timestamp: request.date.and_time(t(10, 39, 59)),
|
||||||
@@ -556,7 +556,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
|||||||
],
|
],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: first,
|
date: first,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp: first.and_time(t(10, 39, 59)),
|
timestamp: first.and_time(t(10, 39, 59)),
|
||||||
@@ -569,7 +569,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
|||||||
amount_delta: 100_000.0,
|
amount_delta: 100_000.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: second,
|
date: second,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp: second.and_time(t(10, 39, 59)),
|
timestamp: second.and_time(t(10, 39, 59)),
|
||||||
@@ -826,7 +826,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
|||||||
Ok(request
|
Ok(request
|
||||||
.symbols
|
.symbols
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: request.date,
|
date: request.date,
|
||||||
symbol,
|
symbol,
|
||||||
timestamp: request.date.and_time(start_time) - Duration::seconds(1),
|
timestamp: request.date.and_time(start_time) - Duration::seconds(1),
|
||||||
|
|||||||
@@ -2209,7 +2209,7 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
|
|||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp: dt(2025, 1, 2, 10, 18, 0),
|
timestamp: dt(2025, 1, 2, 10, 18, 0),
|
||||||
@@ -2222,7 +2222,7 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
|
|||||||
amount_delta: 10_200.0,
|
amount_delta: 10_200.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: dt(2025, 1, 2, 10, 18, 0),
|
timestamp: dt(2025, 1, 2, 10, 18, 0),
|
||||||
@@ -2235,7 +2235,7 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
|
|||||||
amount_delta: 20_400.0,
|
amount_delta: 20_400.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp: dt(2025, 1, 2, 10, 19, 0),
|
timestamp: dt(2025, 1, 2, 10, 19, 0),
|
||||||
@@ -2341,7 +2341,7 @@ fn engine_skips_empty_platform_style_minute_callbacks_between_schedule_times() {
|
|||||||
let date = d(2025, 1, 2);
|
let date = d(2025, 1, 2);
|
||||||
let mut data = single_day_anchor_data(date);
|
let mut data = single_day_anchor_data(date);
|
||||||
data.add_execution_quotes(vec![
|
data.add_execution_quotes(vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp: dt(2025, 1, 2, 10, 18, 0),
|
timestamp: dt(2025, 1, 2, 10, 18, 0),
|
||||||
@@ -2354,7 +2354,7 @@ fn engine_skips_empty_platform_style_minute_callbacks_between_schedule_times() {
|
|||||||
amount_delta: 10_200.0,
|
amount_delta: 10_200.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp: dt(2025, 1, 2, 10, 19, 0),
|
timestamp: dt(2025, 1, 2, 10, 19, 0),
|
||||||
@@ -2519,7 +2519,7 @@ fn strategy_context_exposes_engine_native_data_helpers() {
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let quotes = vec![
|
let quotes = vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp: dt(2025, 1, 3, 14, 30, 0),
|
timestamp: dt(2025, 1, 3, 14, 30, 0),
|
||||||
@@ -2532,7 +2532,7 @@ fn strategy_context_exposes_engine_native_data_helpers() {
|
|||||||
amount_delta: 10_150.0,
|
amount_delta: 10_150.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: date3,
|
date: date3,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp: dt(2025, 1, 6, 10, 18, 0),
|
timestamp: dt(2025, 1, 6, 10, 18, 0),
|
||||||
@@ -2545,7 +2545,7 @@ fn strategy_context_exposes_engine_native_data_helpers() {
|
|||||||
amount_delta: 10_250.0,
|
amount_delta: 10_250.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: date3,
|
date: date3,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp: dt(2025, 1, 6, 10, 19, 0),
|
timestamp: dt(2025, 1, 6, 10, 19, 0),
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ fn intraday_liquidity_data(date: NaiveDate, symbol: &str) -> DataSet {
|
|||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
|
timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
|
||||||
@@ -159,7 +159,7 @@ fn intraday_liquidity_data(date: NaiveDate, symbol: &str) -> DataSet {
|
|||||||
amount_delta: 10_000.0,
|
amount_delta: 10_000.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 19, 0).unwrap(),
|
timestamp: date.and_hms_opt(10, 19, 0).unwrap(),
|
||||||
@@ -172,7 +172,7 @@ fn intraday_liquidity_data(date: NaiveDate, symbol: &str) -> DataSet {
|
|||||||
amount_delta: 10_000.0,
|
amount_delta: 10_000.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 20, 0).unwrap(),
|
timestamp: date.and_hms_opt(10, 20, 0).unwrap(),
|
||||||
@@ -373,7 +373,7 @@ fn broker_executes_explicit_order_value_buy() {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
|
timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
|
||||||
@@ -527,7 +527,7 @@ fn broker_delayed_limit_open_sell_uses_minute_price() {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(9, 31, 0).unwrap(),
|
timestamp: date.and_hms_opt(9, 31, 0).unwrap(),
|
||||||
@@ -663,7 +663,7 @@ fn broker_executes_order_shares_and_order_lots() {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
|
timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
|
||||||
@@ -1104,7 +1104,7 @@ fn broker_executes_target_portfolio_smart_with_algo_order_style() {
|
|||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(9, 31, 0).unwrap(),
|
timestamp: date.and_hms_opt(9, 31, 0).unwrap(),
|
||||||
@@ -1117,7 +1117,7 @@ fn broker_executes_target_portfolio_smart_with_algo_order_style() {
|
|||||||
amount_delta: 0.0,
|
amount_delta: 0.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(9, 35, 0).unwrap(),
|
timestamp: date.and_hms_opt(9, 35, 0).unwrap(),
|
||||||
@@ -1920,7 +1920,7 @@ fn broker_applies_tick_size_slippage_on_intraday_last_fills() {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
|
timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
|
||||||
@@ -2153,7 +2153,7 @@ fn broker_executes_intraday_last_on_start_quote_with_trade_delta() {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(9, 33, 0).unwrap(),
|
timestamp: date.and_hms_opt(9, 33, 0).unwrap(),
|
||||||
@@ -2273,7 +2273,7 @@ fn broker_cancels_market_order_remainder_when_intraday_quote_liquidity_exhausted
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
|
timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
|
||||||
@@ -2509,7 +2509,7 @@ fn broker_splits_intraday_quote_fills_and_tracks_commission_by_order() {
|
|||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
|
timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
|
||||||
@@ -2522,7 +2522,7 @@ fn broker_splits_intraday_quote_fills_and_tracks_commission_by_order() {
|
|||||||
amount_delta: 0.0,
|
amount_delta: 0.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 6).unwrap(),
|
timestamp: date.and_hms_opt(10, 18, 6).unwrap(),
|
||||||
@@ -2682,7 +2682,7 @@ fn broker_aggregates_intraday_quote_fills_into_vwap_leg() {
|
|||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
|
timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
|
||||||
@@ -2695,7 +2695,7 @@ fn broker_aggregates_intraday_quote_fills_into_vwap_leg() {
|
|||||||
amount_delta: 0.0,
|
amount_delta: 0.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 6).unwrap(),
|
timestamp: date.and_hms_opt(10, 18, 6).unwrap(),
|
||||||
@@ -2839,7 +2839,7 @@ fn broker_executes_algo_vwap_value_with_time_window() {
|
|||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 17, 59).unwrap(),
|
timestamp: date.and_hms_opt(10, 17, 59).unwrap(),
|
||||||
@@ -2852,7 +2852,7 @@ fn broker_executes_algo_vwap_value_with_time_window() {
|
|||||||
amount_delta: 0.0,
|
amount_delta: 0.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
|
timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
|
||||||
@@ -2865,7 +2865,7 @@ fn broker_executes_algo_vwap_value_with_time_window() {
|
|||||||
amount_delta: 0.0,
|
amount_delta: 0.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 6).unwrap(),
|
timestamp: date.and_hms_opt(10, 18, 6).unwrap(),
|
||||||
@@ -2878,7 +2878,7 @@ fn broker_executes_algo_vwap_value_with_time_window() {
|
|||||||
amount_delta: 0.0,
|
amount_delta: 0.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 40).unwrap(),
|
timestamp: date.and_hms_opt(10, 18, 40).unwrap(),
|
||||||
@@ -3001,7 +3001,7 @@ fn broker_executes_algo_twap_percent_across_window_quotes() {
|
|||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 0, 0).unwrap(),
|
timestamp: date.and_hms_opt(10, 0, 0).unwrap(),
|
||||||
@@ -3014,7 +3014,7 @@ fn broker_executes_algo_twap_percent_across_window_quotes() {
|
|||||||
amount_delta: 0.0,
|
amount_delta: 0.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 15, 0).unwrap(),
|
timestamp: date.and_hms_opt(10, 15, 0).unwrap(),
|
||||||
@@ -3027,7 +3027,7 @@ fn broker_executes_algo_twap_percent_across_window_quotes() {
|
|||||||
amount_delta: 0.0,
|
amount_delta: 0.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 30, 0).unwrap(),
|
timestamp: date.and_hms_opt(10, 30, 0).unwrap(),
|
||||||
@@ -3165,7 +3165,7 @@ fn broker_uses_best_own_price_for_intraday_matching() {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
|
timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
|
||||||
@@ -3284,7 +3284,7 @@ fn broker_uses_best_counterparty_price_for_intraday_matching() {
|
|||||||
volume: 1_000_000,
|
volume: 1_000_000,
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![IntradayExecutionQuote {
|
vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
|
timestamp: date.and_hms_opt(10, 18, 3).unwrap(),
|
||||||
@@ -4915,7 +4915,7 @@ fn two_day_limit_order_data(day1_open: f64, day2_open: f64) -> DataSet {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
[(day1, day1_open), (day2, day2_open)].into_iter().map(|(date, price)| IntradayExecutionQuote {
|
[(day1, day1_open), (day2, day2_open)].into_iter().map(|(date, price)| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date, symbol: "000002.SZ".into(), timestamp: date.and_hms_opt(9, 30, 0).unwrap(),
|
date, symbol: "000002.SZ".into(), timestamp: date.and_hms_opt(9, 30, 0).unwrap(),
|
||||||
last_price: price, bid1: price, ask1: price, bid1_volume: 0, ask1_volume: 0,
|
last_price: price, bid1: price, ask1: price, bid1_volume: 0, ask1_volume: 0,
|
||||||
volume_delta: 100_000, amount_delta: 100_000.0 * price,
|
volume_delta: 100_000, amount_delta: 100_000.0 * price,
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ fn dataset(day_count: usize, bars_per_day: usize) -> (DataSet, Vec<NaiveDate>) {
|
|||||||
let session_start = date.and_hms_opt(9, 30, 0).expect("valid session start");
|
let session_start = date.and_hms_opt(9, 30, 0).expect("valid session start");
|
||||||
for offset in 0..bars_per_day {
|
for offset in 0..bars_per_day {
|
||||||
let timestamp = session_start + Duration::minutes(offset as i64);
|
let timestamp = session_start + Duration::minutes(offset as i64);
|
||||||
quotes.push(IntradayExecutionQuote {
|
quotes.push(IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: *date,
|
date: *date,
|
||||||
symbol: SYMBOL.to_string(),
|
symbol: SYMBOL.to_string(),
|
||||||
timestamp,
|
timestamp,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use fidc_core::{
|
|||||||
PortfolioState, PriceField, StrategyDecision, platform_expr_config_from_value,
|
PortfolioState, PriceField, StrategyDecision, platform_expr_config_from_value,
|
||||||
};
|
};
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
|
use fidc_core::IntradayExecutionQuote;
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
fn day(n: u32) -> NaiveDate {
|
fn day(n: u32) -> NaiveDate {
|
||||||
@@ -142,7 +143,7 @@ fn data_with_fund_rules(
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let quotes = market.iter().filter(|row| row.symbol != "000300.SH").map(|row| fidc_core::IntradayExecutionQuote {
|
let quotes = market.iter().filter(|row| row.symbol != "000300.SH").map(|row| fidc_core::IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: row.date, symbol: row.symbol.to_string(), timestamp: row.date.and_hms_opt(9, 30, 0).unwrap(),
|
date: row.date, symbol: row.symbol.to_string(), timestamp: row.date.and_hms_opt(9, 30, 0).unwrap(),
|
||||||
last_price: row.open, bid1: row.open, ask1: row.open, bid1_volume: 0, ask1_volume: 0,
|
last_price: row.open, bid1: row.open, ask1: row.open, bid1_volume: 0, ask1_volume: 0,
|
||||||
volume_delta: row.volume, amount_delta: row.open * row.volume as f64,
|
volume_delta: row.volume, amount_delta: row.open * row.volume as f64,
|
||||||
@@ -489,6 +490,72 @@ fn repeating_the_same_partial_exit_generation_does_not_reduce_again() {
|
|||||||
assert_eq!(new_signal.fill_events.iter().filter(|fill|fill.symbol==code(1)).map(|fill|fill.quantity).sum::<u32>(),300);
|
assert_eq!(new_signal.fill_events.iter().filter(|fill|fill.symbol==code(1)).map(|fill|fill.quantity).sum::<u32>(),300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn daily_execution_price_does_not_satisfy_an_unobserved_order_book_condition() {
|
||||||
|
let data = data(false);
|
||||||
|
for field in ["bid1", "ask1"] {
|
||||||
|
let broker = broker(false);
|
||||||
|
let mut account = PortfolioState::new(30000.);
|
||||||
|
let mut intent = contract(day(2), 1, false);
|
||||||
|
intent.rule.trigger_mode = "condition".into();
|
||||||
|
intent.rule.buy_condition = format!("{field}>0");
|
||||||
|
let result = broker.execute_with_event_dates(day(5), day(2), day(2), &mut account, &data, &decision(intent));
|
||||||
|
assert!(result.unwrap_err().to_string().contains(field));
|
||||||
|
assert!(account.positions().is_empty());
|
||||||
|
assert_eq!(account.cash(), 30000.);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cumulative_conditions_do_not_consume_future_bars_or_inflate_fill_capacity() {
|
||||||
|
let mut data = data(false);
|
||||||
|
let mut quotes = Vec::new();
|
||||||
|
for n in 1..=2 {
|
||||||
|
let price = if n == 1 {20.} else {10.};
|
||||||
|
for (minute, volume) in [(30,600), (31,0), (32,400)] {
|
||||||
|
quotes.push(IntradayExecutionQuote {
|
||||||
|
observation_kind: fidc_core::data::QuoteObservationKind::MinuteBar,
|
||||||
|
date: day(5), symbol: code(n), timestamp: day(5).and_hms_opt(9,minute,0).unwrap(),
|
||||||
|
last_price: price, bid1: 0., ask1: 0., bid1_volume: 0, ask1_volume: 0,
|
||||||
|
volume_delta: volume, amount_delta: volume as f64 * price, trading_phase: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.replace_execution_quotes(quotes.clone());
|
||||||
|
let at = chrono::NaiveTime::from_hms_opt(9,32,0).unwrap();
|
||||||
|
for condition in ["volume>=1000", "amount>=20000"] {
|
||||||
|
let broker=broker(true).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(at);
|
||||||
|
let mut account=PortfolioState::new(30000.);
|
||||||
|
let mut intent=contract(day(5),1,false);
|
||||||
|
intent.rule.buy_condition=condition.into();intent.rule.trigger_mode="condition".into();
|
||||||
|
let report=broker.execute_with_event_dates(day(5),day(5),day(5),&mut account,&data,&decision(intent)).unwrap();
|
||||||
|
assert_eq!(report.fill_events.iter().map(|fill|fill.quantity).sum::<u32>(),100,"{condition}: {report:?}");
|
||||||
|
assert_eq!(data.execution_quotes_on(day(5),&code(1))[2].volume_delta,400);
|
||||||
|
}
|
||||||
|
let mut future=quotes.last().unwrap().clone();future.symbol=code(1);future.timestamp=day(5).and_hms_opt(9,33,0).unwrap();future.volume_delta=9000;future.amount_delta=180000.;
|
||||||
|
data.add_execution_quotes(vec![future]);
|
||||||
|
let broker=broker(false).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(at);
|
||||||
|
let mut account=PortfolioState::new(30000.);
|
||||||
|
let mut intent=contract(day(5),1,false);intent.rule.buy_condition="volume>1000".into();intent.rule.trigger_mode="condition".into();
|
||||||
|
let report=broker.execute_with_event_dates(day(5),day(5),day(5),&mut account,&data,&decision(intent)).unwrap();
|
||||||
|
assert!(report.fill_events.is_empty(),"future volume must not satisfy this signal: {report:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn session_total_cache_is_invalidated_without_mutating_other_dataset_clones() {
|
||||||
|
let mut original=data(false);
|
||||||
|
let quote=IntradayExecutionQuote { observation_kind:fidc_core::data::QuoteObservationKind::MinuteBar,date:day(5),symbol:code(1),timestamp:day(5).and_hms_opt(9,30,0).unwrap(),last_price:20.,bid1:0.,ask1:0.,bid1_volume:0,ask1_volume:0,volume_delta:100,amount_delta:2000.,trading_phase:None };
|
||||||
|
original.replace_execution_quotes(vec![quote.clone()]);
|
||||||
|
assert_eq!(original.execution_session_totals(&code(1),quote.timestamp).unwrap().0,Decimal::from(100));
|
||||||
|
let mut changed=original.clone();let mut next=quote.clone();next.timestamp=day(5).and_hms_opt(9,31,0).unwrap();
|
||||||
|
changed.add_execution_quotes(vec![next.clone()]);
|
||||||
|
assert_eq!(changed.execution_session_totals(&code(1),next.timestamp).unwrap().0,Decimal::from(200));
|
||||||
|
assert!(original.execution_session_totals(&code(1),next.timestamp).is_err());
|
||||||
|
changed.remove_execution_quotes_on_date(day(5));
|
||||||
|
assert!(changed.execution_session_totals(&code(1),quote.timestamp).is_err());
|
||||||
|
assert_eq!(original.execution_session_totals(&code(1),quote.timestamp).unwrap().0,Decimal::from(100));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parsed_pool_program_executes_daily_membership_changes_without_legacy_translation() {
|
fn parsed_pool_program_executes_daily_membership_changes_without_legacy_translation() {
|
||||||
let intent = contract(day(2), 1, false);
|
let intent = contract(day(2), 1, false);
|
||||||
|
|||||||
Reference in New Issue
Block a user