diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index 6d269f9..2e5bf2c 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -4694,7 +4694,11 @@ where ); continue; } - if candidate_gross <= cash + 1e-6 { + let candidate_cost = self + .cost_model + .calculate(snapshot.date, OrderSide::Buy, candidate_gross) + .total(); + if candidate_gross + candidate_cost <= cash + 1e-6 { break; } budget_block_reason = Some("insufficient cash after fees"); diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index 952f265..af85e45 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -734,6 +734,27 @@ impl BenchmarkPriceSeries { Some(sum / lookback as f64) } + fn decision_values_for( + &self, + date: NaiveDate, + lookback: usize, + field: PriceField, + ) -> Vec { + if lookback == 0 { + return Vec::new(); + } + let end = match self.dates.binary_search(&date) { + Ok(idx) => idx, + Err(0) => return Vec::new(), + Err(idx) => idx, + }; + let start = end.saturating_sub(lookback); + match field { + PriceField::DayOpen | PriceField::Open => self.opens[start..end].to_vec(), + PriceField::Close | PriceField::Last => self.closes[start..end].to_vec(), + } + } + fn moving_average_for( &self, date: NaiveDate, @@ -791,7 +812,7 @@ pub struct DataSet { candidate_by_date: BTreeMap>, candidate_index: HashMap<(NaiveDate, String), CandidateEligibility>, corporate_actions_by_date: BTreeMap>, - execution_quotes_index: HashMap<(NaiveDate, String), Vec>, + execution_quotes_by_date: HashMap>>, order_book_depth_index: HashMap<(NaiveDate, String), Vec>, benchmark_by_date: BTreeMap, market_series_by_symbol: HashMap, @@ -1075,7 +1096,7 @@ impl DataSet { .map(|item| ((item.date, item.symbol.clone()), item)) .collect::>(); let corporate_actions_by_date = group_by_date(corporate_actions, |item| item.date); - let execution_quotes_index = build_execution_quote_index(execution_quotes); + let execution_quotes_by_date = build_execution_quote_index(execution_quotes); let order_book_depth_index = build_order_book_depth_index(order_book_depth); let benchmark_by_date = benchmarks @@ -1105,7 +1126,7 @@ impl DataSet { candidate_by_date, candidate_index, corporate_actions_by_date, - execution_quotes_index, + execution_quotes_by_date, order_book_depth_index, benchmark_by_date, market_series_by_symbol, @@ -1177,14 +1198,22 @@ impl DataSet { } pub fn execution_quotes_on(&self, date: NaiveDate, symbol: &str) -> &[IntradayExecutionQuote] { - self.execution_quotes_index - .get(&(date, symbol.to_string())) + self.execution_quotes_by_date + .get(&date) + .and_then(|rows_by_symbol| rows_by_symbol.get(symbol)) .map(Vec::as_slice) .unwrap_or(&[]) } pub fn execution_quote_key_set(&self) -> HashSet<(NaiveDate, String)> { - self.execution_quotes_index.keys().cloned().collect() + self.execution_quotes_by_date + .iter() + .flat_map(|(date, rows_by_symbol)| { + rows_by_symbol + .keys() + .map(move |symbol| (*date, symbol.clone())) + }) + .collect() } pub fn add_execution_quotes(&mut self, quotes: Vec) -> usize { @@ -1192,7 +1221,12 @@ impl DataSet { let mut touched = HashSet::<(NaiveDate, String)>::new(); for quote in quotes { let key = (quote.date, quote.symbol.clone()); - let rows = self.execution_quotes_index.entry(key.clone()).or_default(); + let rows = self + .execution_quotes_by_date + .entry(quote.date) + .or_default() + .entry(quote.symbol.clone()) + .or_default(); if rows.iter().any(|existing| { existing.timestamp == quote.timestamp && existing.symbol == quote.symbol }) { @@ -1202,8 +1236,12 @@ impl DataSet { touched.insert(key); added += 1; } - for key in touched { - if let Some(rows) = self.execution_quotes_index.get_mut(&key) { + for (date, symbol) in touched { + if let Some(rows) = self + .execution_quotes_by_date + .get_mut(&date) + .and_then(|rows_by_symbol| rows_by_symbol.get_mut(&symbol)) + { rows.sort_by_key(|quote| quote.timestamp); } } @@ -1223,10 +1261,11 @@ impl DataSet { pub fn execution_quotes_on_date(&self, date: NaiveDate) -> Vec { let mut quotes = self - .execution_quotes_index - .iter() - .filter(|((quote_date, _), _)| *quote_date == date) - .flat_map(|(_, rows)| rows.iter().cloned()) + .execution_quotes_by_date + .get(&date) + .into_iter() + .flat_map(|rows_by_symbol| rows_by_symbol.values()) + .flat_map(|rows| rows.iter().cloned()) .collect::>(); quotes.sort_by(|left, right| { left.timestamp @@ -1346,10 +1385,10 @@ impl DataSet { return Vec::new(); } let mut quotes = self - .execution_quotes_index - .iter() - .filter(|((_, quote_symbol), _)| quote_symbol == symbol) - .flat_map(|(_, rows)| rows.iter()) + .execution_quotes_by_date + .values() + .filter_map(|rows_by_symbol| rows_by_symbol.get(symbol)) + .flat_map(|rows| rows.iter()) .filter(|quote| intraday_quote_visible(quote, date, active_datetime, include_now)) .cloned() .collect::>(); @@ -1846,12 +1885,11 @@ impl DataSet { .collect(), Some("1m") | Some("tick") => { let mut bars = self - .execution_quotes_index + .execution_quotes_by_date .iter() - .filter(|((date, quote_symbol), _)| { - quote_symbol == symbol && *date >= start && *date <= end - }) - .flat_map(|(_, rows)| rows.iter()) + .filter(|(date, _)| **date >= start && **date <= end) + .filter_map(|(_, rows_by_symbol)| rows_by_symbol.get(symbol)) + .flat_map(|rows| rows.iter()) .map(intraday_quote_price_bar) .collect::>(); bars.sort_by(|left, right| { @@ -2269,6 +2307,25 @@ impl DataSet { } } + pub fn benchmark_decision_numeric_values( + &self, + date: NaiveDate, + field: &str, + lookback: usize, + ) -> Vec { + let field = normalize_field(field); + match field.as_str() { + "open" | "day_open" | "dayopen" | "benchmark_open" => self + .benchmark_series_cache + .trailing_values_for(date, lookback, PriceField::Open), + _ => self.benchmark_series_cache.decision_values_for( + date, + lookback, + PriceField::Close, + ), + } + } + pub fn market_open_moving_average( &self, date: NaiveDate, @@ -3279,17 +3336,21 @@ fn build_futures_params_index( fn build_execution_quote_index( execution_quotes: Vec, -) -> HashMap<(NaiveDate, String), Vec> { - let mut grouped = HashMap::<(NaiveDate, String), Vec>::new(); +) -> HashMap>> { + let mut grouped = HashMap::>>::new(); for quote in execution_quotes { grouped - .entry((quote.date, quote.symbol.clone())) + .entry(quote.date) + .or_default() + .entry(quote.symbol.clone()) .or_default() .push(quote); } - for quotes in grouped.values_mut() { - quotes.sort_by_key(|quote| quote.timestamp); + for rows_by_symbol in grouped.values_mut() { + for quotes in rows_by_symbol.values_mut() { + quotes.sort_by_key(|quote| quote.timestamp); + } } grouped diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 3cbc4d6..864846e 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -439,19 +439,27 @@ fn precomputed_stock_rolling_mean( field: &str, lookback: usize, ) -> Option { - let key = match (field.trim().to_ascii_lowercase().as_str(), lookback) { - ("close" | "prev_close" | "stock_close" | "price", 5) => "ma5_prev_close", - ("close" | "prev_close" | "stock_close" | "price", 10) => "ma10_prev_close", - ("close" | "prev_close" | "stock_close" | "price", 20) => "ma20_prev_close", - ("close" | "prev_close" | "stock_close" | "price", 30) => "ma30_prev_close", - ("volume" | "stock_volume", 5) => "avg_volume5", - ("volume" | "stock_volume", 20) => "avg_volume20", - ("volume" | "stock_volume", 60) => "avg_volume60", + let keys: &[&str] = match (field.trim().to_ascii_lowercase().as_str(), lookback) { + ("close" | "prev_close" | "stock_close" | "price", 5) => { + &["ma5_prev_close", "sf_jq_v104_ma5", "ma5"] + } + ("close" | "prev_close" | "stock_close" | "price", 10) => { + &["ma10_prev_close", "sf_jq_v104_ma10", "ma10"] + } + ("close" | "prev_close" | "stock_close" | "price", 20) => { + &["ma20_prev_close", "sf_jq_v104_ma20", "ma20"] + } + ("close" | "prev_close" | "stock_close" | "price", 30) => { + &["ma30_prev_close", "sf_jq_v104_ma30", "ma30"] + } + ("volume" | "stock_volume", 5) => &["avg_volume5", "sf_jq_v104_v5", "vma5"], + ("volume" | "stock_volume", 20) => &["avg_volume20", "sf_jq_v104_v20", "vma20"], + ("volume" | "stock_volume", 60) => &["avg_volume60", "sf_jq_v104_v60", "vma60"], + ("volume" | "stock_volume", 100) => &["avg_volume100", "sf_jq_v104_v100", "vma100"], _ => return None, }; - extra_factors - .get(key) - .copied() + keys.iter() + .find_map(|key| extra_factors.get(*key).copied()) .filter(|value| value.is_finite()) } @@ -1155,6 +1163,21 @@ impl PlatformExprStrategy { }) } + fn has_execution_quote_at_or_after( + &self, + ctx: &StrategyContext<'_>, + date: NaiveDate, + symbol: &str, + execution_state: &ProjectedExecutionState, + ) -> bool { + let start_cursor = + self.projected_execution_start_cursor(ctx, date, symbol, execution_state); + ctx.data + .execution_quotes_on(date, symbol) + .iter() + .any(|quote| quote.timestamp >= start_cursor) + } + fn project_target_zero( &self, ctx: &StrategyContext<'_>, @@ -1190,7 +1213,7 @@ impl PlatformExprStrategy { execution_state, ) .or_else(|| { - if ctx.data.execution_quotes_on(date, symbol).is_empty() { + if !self.has_execution_quote_at_or_after(ctx, date, symbol, execution_state) { Some(ProjectedExecutionFill { price: self.projected_execution_price(market, OrderSide::Sell), quantity, @@ -1391,7 +1414,7 @@ impl PlatformExprStrategy { execution_state, ) .or_else(|| { - if ctx.data.execution_quotes_on(date, symbol).is_empty() { + if !self.has_execution_quote_at_or_after(ctx, date, symbol, execution_state) { Some(ProjectedExecutionFill { price: execution_price, quantity, @@ -1601,78 +1624,92 @@ impl PlatformExprStrategy { let factor = ctx.data.require_factor(factor_date, symbol)?; let candidate = ctx.data.require_candidate(date, symbol)?; let instrument = ctx.data.instrument(symbol); - let stock_ma_short = ctx - .data - .market_decision_close_moving_average(date, symbol, self.config.stock_short_ma_days) + let stock_ma_short = precomputed_stock_rolling_mean( + &factor.extra_factors, + "close", + self.config.stock_short_ma_days, + ) + .or_else(|| { + ctx.data.market_decision_close_moving_average( + date, + symbol, + self.config.stock_short_ma_days, + ) + }) + .unwrap_or(f64::NAN); + let stock_ma_mid = precomputed_stock_rolling_mean( + &factor.extra_factors, + "close", + self.config.stock_mid_ma_days, + ) + .or_else(|| { + ctx.data.market_decision_close_moving_average( + date, + symbol, + self.config.stock_mid_ma_days, + ) + }) + .unwrap_or(f64::NAN); + let stock_ma_long = precomputed_stock_rolling_mean( + &factor.extra_factors, + "close", + self.config.stock_long_ma_days, + ) + .or_else(|| { + ctx.data.market_decision_close_moving_average( + date, + symbol, + self.config.stock_long_ma_days, + ) + }) + .unwrap_or(f64::NAN); + let stock_ma5 = precomputed_stock_rolling_mean(&factor.extra_factors, "close", 5) .or_else(|| { - precomputed_stock_rolling_mean( - &factor.extra_factors, - "close", - self.config.stock_short_ma_days, - ) + ctx.data + .market_decision_close_moving_average(date, symbol, 5) }) .unwrap_or(f64::NAN); - let stock_ma_mid = ctx - .data - .market_decision_close_moving_average(date, symbol, self.config.stock_mid_ma_days) + let stock_ma10 = precomputed_stock_rolling_mean(&factor.extra_factors, "close", 10) .or_else(|| { - precomputed_stock_rolling_mean( - &factor.extra_factors, - "close", - self.config.stock_mid_ma_days, - ) + ctx.data + .market_decision_close_moving_average(date, symbol, 10) }) .unwrap_or(f64::NAN); - let stock_ma_long = ctx - .data - .market_decision_close_moving_average(date, symbol, self.config.stock_long_ma_days) + let stock_ma20 = precomputed_stock_rolling_mean(&factor.extra_factors, "close", 20) .or_else(|| { - precomputed_stock_rolling_mean( - &factor.extra_factors, - "close", - self.config.stock_long_ma_days, - ) + ctx.data + .market_decision_close_moving_average(date, symbol, 20) }) .unwrap_or(f64::NAN); - let stock_ma5 = ctx - .data - .market_decision_close_moving_average(date, symbol, 5) - .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "close", 5)) + let stock_ma30 = precomputed_stock_rolling_mean(&factor.extra_factors, "close", 30) + .or_else(|| { + ctx.data + .market_decision_close_moving_average(date, symbol, 30) + }) .unwrap_or(f64::NAN); - let stock_ma10 = ctx - .data - .market_decision_close_moving_average(date, symbol, 10) - .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "close", 10)) + let stock_volume_ma5 = precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 5) + .or_else(|| { + ctx.data + .market_decision_volume_moving_average(date, symbol, 5) + }) .unwrap_or(f64::NAN); - let stock_ma20 = ctx - .data - .market_decision_close_moving_average(date, symbol, 20) - .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "close", 20)) + let stock_volume_ma10 = precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 10) + .or_else(|| { + ctx.data + .market_decision_volume_moving_average(date, symbol, 10) + }) .unwrap_or(f64::NAN); - let stock_ma30 = ctx - .data - .market_decision_close_moving_average(date, symbol, 30) - .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "close", 30)) + let stock_volume_ma20 = precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 20) + .or_else(|| { + ctx.data + .market_decision_volume_moving_average(date, symbol, 20) + }) .unwrap_or(f64::NAN); - let stock_volume_ma5 = ctx - .data - .market_decision_volume_moving_average(date, symbol, 5) - .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 5)) - .unwrap_or(f64::NAN); - let stock_volume_ma10 = ctx - .data - .market_decision_volume_moving_average(date, symbol, 10) - .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 10)) - .unwrap_or(f64::NAN); - let stock_volume_ma20 = ctx - .data - .market_decision_volume_moving_average(date, symbol, 20) - .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 20)) - .unwrap_or(f64::NAN); - let stock_volume_ma60 = ctx - .data - .market_decision_volume_moving_average(date, symbol, 60) - .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 60)) + let stock_volume_ma60 = precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 60) + .or_else(|| { + ctx.data + .market_decision_volume_moving_average(date, symbol, 60) + }) .unwrap_or(f64::NAN); let touched_upper_limit = !market.paused && (market.is_at_upper_limit_price(market.close) @@ -1706,7 +1743,7 @@ impl PlatformExprStrategy { }) .or_else(|| decision_quote.and_then(|quote| quote.buy_price())) .unwrap_or(market.last_price), - prev_close: market.prev_close, + prev_close: feature_market.prev_close, amount, upper_limit: market.upper_limit, lower_limit: market.lower_limit, @@ -2416,6 +2453,60 @@ impl PlatformExprStrategy { Self::rewrite_ternary(&Self::normalize_runtime_field_aliases(expr.trim())) } + fn compact_expr(expr: &str) -> String { + let mut output = String::with_capacity(expr.len()); + let mut in_single_quote = false; + let mut in_double_quote = false; + let mut escaped = false; + for ch in expr.chars() { + if escaped { + output.push(ch); + escaped = false; + continue; + } + if ch == '\\' && (in_single_quote || in_double_quote) { + output.push(ch); + escaped = true; + continue; + } + if ch == '\'' && !in_double_quote { + in_single_quote = !in_single_quote; + output.push(ch); + continue; + } + if ch == '"' && !in_single_quote { + in_double_quote = !in_double_quote; + output.push(ch); + continue; + } + if !in_single_quote && !in_double_quote && ch.is_whitespace() { + continue; + } + output.push(ch); + } + output + } + + fn prelude_numeric_constant(&self, name: &str) -> Option { + for line in Self::normalize_prelude_for_eval(&self.config.prelude).lines() { + let trimmed = line.trim(); + let Some(body) = trimmed.strip_prefix("let ") else { + continue; + }; + let Some((lhs, rhs)) = body.split_once('=') else { + continue; + }; + if lhs.trim() != name { + continue; + } + let rhs = rhs.trim().trim_end_matches(';').trim(); + if let Ok(value) = rhs.parse::() { + return Some(value); + } + } + None + } + fn normalize_prelude_for_eval(prelude: &str) -> String { prelude .lines() @@ -3269,7 +3360,9 @@ impl PlatformExprStrategy { } let value = match field { "benchmark_open" => ctx.data.benchmark_open_moving_average(day.date, lookback), - "benchmark_close" => ctx.data.benchmark_moving_average(day.date, lookback), + "benchmark_close" => ctx + .data + .benchmark_decision_moving_average(day.date, lookback), "signal_open" => { ctx.data .market_open_moving_average(day.date, &self.config.signal_symbol, lookback) @@ -3290,16 +3383,16 @@ impl PlatformExprStrategy { "rolling_mean(\"{other}\", {lookback}) requires stock context" )) })?; - ctx.data - .market_decision_numeric_moving_average( - day.date, - &stock.symbol, - other, - lookback, - ) - .or_else(|| { - precomputed_stock_rolling_mean(&stock.extra_factors, other, lookback) - }) + precomputed_stock_rolling_mean(&stock.extra_factors, other, lookback).or_else( + || { + ctx.data.market_decision_numeric_moving_average( + day.date, + &stock.symbol, + other, + lookback, + ) + }, + ) } }; value.ok_or_else(|| { @@ -3375,7 +3468,7 @@ impl PlatformExprStrategy { .benchmark_numeric_values(day.date, "open", lookback), "benchmark_close" => ctx .data - .benchmark_numeric_values(day.date, "close", lookback), + .benchmark_decision_numeric_values(day.date, "close", lookback), "signal_open" => ctx.data.market_decision_numeric_values( day.date, &self.config.signal_symbol, @@ -3867,10 +3960,13 @@ impl PlatformExprStrategy { fn selection_dates(&self, ctx: &StrategyContext<'_>) -> (NaiveDate, NaiveDate) { let decision_date = ctx.decision_date; - let factor_date = ctx - .data - .previous_trading_date(decision_date, 1) - .unwrap_or(decision_date); + let factor_date = if self.config.aiquant_transaction_cost { + decision_date + } else { + ctx.data + .previous_trading_date(decision_date, 1) + .unwrap_or(decision_date) + }; let selection_date = if self.config.aiquant_transaction_cost && self.config.intraday_execution_time.is_some() { @@ -4764,6 +4860,9 @@ impl PlatformExprStrategy { if self.config.stock_filter_expr.trim().is_empty() { return Ok(true); } + if let Some(value) = self.fast_stock_passes_expr(ctx, day, stock) { + return Ok(value); + } match self.eval_bool(ctx, &self.config.stock_filter_expr, day, Some(stock), None) { Ok(value) => Ok(value), Err(error) if Self::is_missing_rolling_mean_error(&error) => Ok(false), @@ -4771,6 +4870,56 @@ impl PlatformExprStrategy { } } + fn fast_stock_passes_expr( + &self, + ctx: &StrategyContext<'_>, + day: &DayExpressionState, + stock: &StockExpressionState, + ) -> Option { + let compact = Self::compact_expr(&Self::normalize_expr(&self.config.stock_filter_expr)); + let ma_ratio = self.prelude_numeric_constant("ma_ratio").unwrap_or(1.0); + if compact == "stock_ma_short>stock_ma_mid*ma_ratio&&stock_ma_mid>stock_ma_long" { + return Some( + stock.stock_ma_short.is_finite() + && stock.stock_ma_mid.is_finite() + && stock.stock_ma_long.is_finite() + && stock.stock_ma_short > stock.stock_ma_mid * ma_ratio + && stock.stock_ma_mid > stock.stock_ma_long, + ); + } + + if compact + != "listed_days>=min_listed_days&&rolling_mean(\"close\",5)>rolling_mean(\"close\",10)*ma_ratio&&rolling_mean(\"close\",10)>rolling_mean(\"close\",30)*ma_ratio&&rolling_mean(\"volume\",5)= min_listed_days + && stock.stock_ma5.is_finite() + && stock.stock_ma10.is_finite() + && stock.stock_ma30.is_finite() + && stock.stock_volume_ma5.is_finite() + && volume_ma100.is_finite() + && stock.stock_ma5 > stock.stock_ma10 * ma_ratio + && stock.stock_ma10 > stock.stock_ma30 * ma_ratio + && stock.stock_volume_ma5 < volume_ma100 * max_volume_ratio, + ) + } + fn field_value(&self, row: &EligibleUniverseSnapshot) -> f64 { match self.config.market_cap_field.as_str() { "free_float_cap" | "free_float_market_cap" => row.free_float_cap_bn, @@ -5130,6 +5279,8 @@ impl PlatformExprStrategy { | "touched_lower_limit" | "hit_upper_limit" | "hit_lower_limit" + | "at_upper_limit" + | "at_lower_limit" ) }) } @@ -6964,6 +7115,7 @@ mod tests { .expect("stock state"); assert_eq!(stock.close, 9.9); + assert_eq!(stock.prev_close, 9.7); assert_eq!(stock.volume, 12_300); assert_eq!(stock.last, 19.06); assert_eq!(stock.market_cap, 8.0); @@ -7183,6 +7335,26 @@ mod tests { effective_turnover_ratio: Some(1.0), extra_factors: BTreeMap::new(), }, + DailyFactorSnapshot { + date: decision_date, + symbol: limit_symbol.to_string(), + market_cap_bn: 1.0, + free_float_cap_bn: 1.0, + pe_ttm: 8.0, + turnover_ratio: Some(1.0), + effective_turnover_ratio: Some(1.0), + extra_factors: BTreeMap::new(), + }, + DailyFactorSnapshot { + date: decision_date, + symbol: fallback_symbol.to_string(), + market_cap_bn: 2.0, + free_float_cap_bn: 2.0, + pe_ttm: 8.0, + turnover_ratio: Some(1.0), + effective_turnover_ratio: Some(1.0), + extra_factors: BTreeMap::new(), + }, DailyFactorSnapshot { date: execution_date, symbol: limit_symbol.to_string(), diff --git a/crates/fidc-core/tests/delisting.rs b/crates/fidc-core/tests/delisting.rs index 4c79830..71cb71b 100644 --- a/crates/fidc-core/tests/delisting.rs +++ b/crates/fidc-core/tests/delisting.rs @@ -43,7 +43,8 @@ impl Strategy for BuyThenHoldStrategy { #[test] fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run() { let date1 = d(2025, 1, 2); - let date2 = d(2025, 1, 3); + let delist_date = d(2025, 1, 3); + let date2 = d(2025, 1, 6); let data = DataSet::from_components( vec![ Instrument { @@ -52,8 +53,8 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run() board: "SZ".to_string(), round_lot: 100, listed_at: Some(d(2020, 1, 1)), - delisted_at: Some(date1), - status: "delisted".to_string(), + delisted_at: Some(delist_date), + status: "active".to_string(), }, Instrument { symbol: "000002.SZ".to_string(), @@ -115,7 +116,7 @@ fn engine_settles_delisted_position_before_missing_market_snapshot_breaks_run() DailyMarketSnapshot { date: date2, symbol: "000002.SZ".to_string(), - timestamp: Some("2025-01-03 10:18:00".to_string()), + timestamp: Some("2025-01-06 10:18:00".to_string()), day_open: 5.1, open: 5.1, high: 5.2, @@ -273,7 +274,7 @@ fn engine_applies_successor_conversion_before_delisted_cash_settlement() { round_lot: 100, listed_at: Some(d(2020, 1, 1)), delisted_at: Some(date2), - status: "delisted".to_string(), + status: "active".to_string(), }, Instrument { symbol: "000002.SZ".to_string(), diff --git a/crates/fidc-core/tests/engine_hooks.rs b/crates/fidc-core/tests/engine_hooks.rs index 32ea631..177eb62 100644 --- a/crates/fidc-core/tests/engine_hooks.rs +++ b/crates/fidc-core/tests/engine_hooks.rs @@ -329,7 +329,7 @@ impl Strategy for AuctionOrderStrategy { exit_symbols: BTreeSet::new(), order_intents: vec![fidc_core::OrderIntent::Value { symbol: "000001.SZ".to_string(), - value: 1_000.0, + value: 1_010.0, reason: "auction_buy".to_string(), }], notes: Vec::new(), @@ -3734,7 +3734,7 @@ impl Strategy for BuyMissingRowThenHoldStrategy { exit_symbols: BTreeSet::new(), order_intents: vec![OrderIntent::Value { symbol: "601028.SH".to_string(), - value: 1_000.0, + value: 1_010.0, reason: "seed_position".to_string(), }], notes: Vec::new(),