修复执行价索引和平台表达式回退

This commit is contained in:
boris
2026-06-12 23:46:44 +08:00
parent 9b4462f880
commit 4cf90d83a3
5 changed files with 362 additions and 124 deletions
+5 -1
View File
@@ -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");
+87 -26
View File
@@ -734,6 +734,27 @@ impl BenchmarkPriceSeries {
Some(sum / lookback as f64)
}
fn decision_values_for(
&self,
date: NaiveDate,
lookback: usize,
field: PriceField,
) -> Vec<f64> {
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<NaiveDate, Vec<CandidateEligibility>>,
candidate_index: HashMap<(NaiveDate, String), CandidateEligibility>,
corporate_actions_by_date: BTreeMap<NaiveDate, Vec<CorporateAction>>,
execution_quotes_index: HashMap<(NaiveDate, String), Vec<IntradayExecutionQuote>>,
execution_quotes_by_date: HashMap<NaiveDate, HashMap<String, Vec<IntradayExecutionQuote>>>,
order_book_depth_index: HashMap<(NaiveDate, String), Vec<IntradayOrderBookDepthLevel>>,
benchmark_by_date: BTreeMap<NaiveDate, BenchmarkSnapshot>,
market_series_by_symbol: HashMap<String, SymbolPriceSeries>,
@@ -1075,7 +1096,7 @@ impl DataSet {
.map(|item| ((item.date, item.symbol.clone()), item))
.collect::<HashMap<_, _>>();
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<IntradayExecutionQuote>) -> 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<IntradayExecutionQuote> {
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::<Vec<_>>();
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::<Vec<_>>();
@@ -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::<Vec<_>>();
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<f64> {
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,18 +3336,22 @@ fn build_futures_params_index(
fn build_execution_quote_index(
execution_quotes: Vec<IntradayExecutionQuote>,
) -> HashMap<(NaiveDate, String), Vec<IntradayExecutionQuote>> {
let mut grouped = HashMap::<(NaiveDate, String), Vec<IntradayExecutionQuote>>::new();
) -> HashMap<NaiveDate, HashMap<String, Vec<IntradayExecutionQuote>>> {
let mut grouped = HashMap::<NaiveDate, HashMap<String, Vec<IntradayExecutionQuote>>>::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() {
for rows_by_symbol in grouped.values_mut() {
for quotes in rows_by_symbol.values_mut() {
quotes.sort_by_key(|quote| quote.timestamp);
}
}
grouped
}
+235 -63
View File
@@ -439,19 +439,27 @@ fn precomputed_stock_rolling_mean(
field: &str,
lookback: usize,
) -> Option<f64> {
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)
.or_else(|| {
precomputed_stock_rolling_mean(
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 = ctx
.data
.market_decision_close_moving_average(date, symbol, self.config.stock_mid_ma_days)
.or_else(|| {
precomputed_stock_rolling_mean(
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 = ctx
.data
.market_decision_close_moving_average(date, symbol, self.config.stock_long_ma_days)
.or_else(|| {
precomputed_stock_rolling_mean(
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 = ctx
.data
let stock_ma5 = precomputed_stock_rolling_mean(&factor.extra_factors, "close", 5)
.or_else(|| {
ctx.data
.market_decision_close_moving_average(date, symbol, 5)
.or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "close", 5))
})
.unwrap_or(f64::NAN);
let stock_ma10 = ctx
.data
let stock_ma10 = precomputed_stock_rolling_mean(&factor.extra_factors, "close", 10)
.or_else(|| {
ctx.data
.market_decision_close_moving_average(date, symbol, 10)
.or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "close", 10))
})
.unwrap_or(f64::NAN);
let stock_ma20 = ctx
.data
let stock_ma20 = precomputed_stock_rolling_mean(&factor.extra_factors, "close", 20)
.or_else(|| {
ctx.data
.market_decision_close_moving_average(date, symbol, 20)
.or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "close", 20))
})
.unwrap_or(f64::NAN);
let stock_ma30 = ctx
.data
let stock_ma30 = precomputed_stock_rolling_mean(&factor.extra_factors, "close", 30)
.or_else(|| {
ctx.data
.market_decision_close_moving_average(date, symbol, 30)
.or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "close", 30))
})
.unwrap_or(f64::NAN);
let stock_volume_ma5 = ctx
.data
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)
.or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 5))
})
.unwrap_or(f64::NAN);
let stock_volume_ma10 = ctx
.data
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)
.or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 10))
})
.unwrap_or(f64::NAN);
let stock_volume_ma20 = ctx
.data
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)
.or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 20))
})
.unwrap_or(f64::NAN);
let stock_volume_ma60 = ctx
.data
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)
.or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 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<f64> {
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::<f64>() {
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(
precomputed_stock_rolling_mean(&stock.extra_factors, other, lookback).or_else(
|| {
ctx.data.market_decision_numeric_moving_average(
day.date,
&stock.symbol,
other,
lookback,
)
.or_else(|| {
precomputed_stock_rolling_mean(&stock.extra_factors, 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
let factor_date = if self.config.aiquant_transaction_cost {
decision_date
} else {
ctx.data
.previous_trading_date(decision_date, 1)
.unwrap_or(decision_date);
.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<bool> {
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)<rolling_mean(\"volume\",100)*max_volume_ratio"
{
return None;
}
let min_listed_days = self
.prelude_numeric_constant("min_listed_days")
.unwrap_or(0.0);
let max_volume_ratio = self
.prelude_numeric_constant("max_volume_ratio")
.unwrap_or(1.0);
let volume_ma100 = precomputed_stock_rolling_mean(&stock.extra_factors, "volume", 100)
.or_else(|| {
ctx.data
.market_decision_volume_moving_average(day.date, &stock.symbol, 100)
})
.unwrap_or(f64::NAN);
Some(
(stock.listed_days as f64) >= 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(),
+6 -5
View File
@@ -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(),
+2 -2
View File
@@ -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(),