style: normalize fidc core formatting

This commit is contained in:
boris
2026-09-04 22:18:35 +08:00
parent b15b93eec1
commit 92322349d4
9 changed files with 231 additions and 219 deletions
+8 -13
View File
@@ -621,7 +621,7 @@ impl<C, R> BrokerSimulator<C, R> {
self.runtime_order_created_date.get(), self.runtime_order_created_date.get(),
self.submission_time(), self.submission_time(),
) )
.map(|(start, end)| (date.and_time(start), date.and_time(end))) .map(|(start, end)| (date.and_time(start), date.and_time(end)))
} }
fn post_close_execution_quote_window_for_submission( fn post_close_execution_quote_window_for_submission(
@@ -1208,8 +1208,7 @@ where
matching_type: MatchingType, matching_type: MatchingType,
) -> Option<f64> { ) -> Option<f64> {
if self.is_post_close_fixed_price(snapshot.date) { if self.is_post_close_fixed_price(snapshot.date) {
return (snapshot.close.is_finite() && snapshot.close > 0.0) return (snapshot.close.is_finite() && snapshot.close > 0.0).then_some(snapshot.close);
.then_some(snapshot.close);
} }
let raw_price = match matching_type { let raw_price = match matching_type {
MatchingType::MinuteBestOwn => match side { MatchingType::MinuteBestOwn => match side {
@@ -7684,8 +7683,7 @@ where
match matching_type { match matching_type {
MatchingType::OpenAuction MatchingType::OpenAuction
| MatchingType::CurrentBarClose | MatchingType::CurrentBarClose
| MatchingType::NextBarOpen | MatchingType::NextBarOpen => false,
=> false,
MatchingType::MinuteLast => self.liquidity_limit, MatchingType::MinuteLast => self.liquidity_limit,
MatchingType::MinuteBestOwn MatchingType::MinuteBestOwn
| MatchingType::MinuteBestCounterparty | MatchingType::MinuteBestCounterparty
@@ -8082,14 +8080,11 @@ mod tests {
EquityExecutionPhase::ContinuousAuction EquityExecutionPhase::ContinuousAuction
); );
let next_open = BrokerSimulator::new( let next_open = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
ChinaAShareCostModel::default(), .with_matching_type(MatchingType::NextBarOpen)
ChinaEquityRuleHooks, .with_intraday_execution_start_time(
) NaiveTime::from_hms_opt(15, 0, 0).expect("valid signal time"),
.with_matching_type(MatchingType::NextBarOpen) );
.with_intraday_execution_start_time(
NaiveTime::from_hms_opt(15, 0, 0).expect("valid signal time"),
);
next_open next_open
.runtime_order_created_date .runtime_order_created_date
.set(Some(date.pred_opt().expect("previous date"))); .set(Some(date.pred_opt().expect("previous date")));
+122 -101
View File
@@ -885,12 +885,7 @@ impl SymbolPriceSeries {
self.moving_average_at_end(end, lookback, field) self.moving_average_at_end(end, lookback, field)
} }
fn moving_average_at_end( fn moving_average_at_end(&self, end: usize, lookback: usize, field: PriceField) -> Option<f64> {
&self,
end: usize,
lookback: usize,
field: PriceField,
) -> Option<f64> {
if end < lookback { if end < lookback {
return None; return None;
} }
@@ -1813,7 +1808,8 @@ impl DataSet {
let market_series_end_positions_by_symbol_id = let market_series_end_positions_by_symbol_id =
build_symbol_series_end_positions(&market_series_by_symbol_id, &calendar); build_symbol_series_end_positions(&market_series_by_symbol_id, &calendar);
let execution_quotes_by_date = build_execution_quote_index(execution_quotes); let execution_quotes_by_date = build_execution_quote_index(execution_quotes);
let mut execution_quote_dates = execution_quotes_by_date.keys().copied().collect::<Vec<_>>(); let mut execution_quote_dates =
execution_quotes_by_date.keys().copied().collect::<Vec<_>>();
execution_quote_dates.sort_unstable(); execution_quote_dates.sort_unstable();
let order_book_depth_index = build_order_book_depth_index(order_book_depth); let order_book_depth_index = build_order_book_depth_index(order_book_depth);
@@ -1910,11 +1906,7 @@ impl DataSet {
{ {
return rows.get(index); return rows.get(index);
} }
find_by_symbol_id( find_by_symbol_id(rows, self.market_symbol_ids_by_date.get(&date)?, symbol_id)
rows,
self.market_symbol_ids_by_date.get(&date)?,
symbol_id,
)
} }
pub(crate) fn daily_snapshot_view(&self, date: NaiveDate) -> DailySnapshotView<'_> { pub(crate) fn daily_snapshot_view(&self, date: NaiveDate) -> DailySnapshotView<'_> {
@@ -2021,11 +2013,7 @@ impl DataSet {
{ {
return rows.get(index); return rows.get(index);
} }
find_by_symbol_id( find_by_symbol_id(rows, self.factor_symbol_ids_by_date.get(&date)?, symbol_id)
rows,
self.factor_symbol_ids_by_date.get(&date)?,
symbol_id,
)
} }
pub fn candidate(&self, date: NaiveDate, symbol: &str) -> Option<&CandidateEligibility> { pub fn candidate(&self, date: NaiveDate, symbol: &str) -> Option<&CandidateEligibility> {
@@ -2067,7 +2055,9 @@ impl DataSet {
.map(|series| { .map(|series| {
series_end series_end
.map(|end| series.moving_averages_at_end(end, close_lookbacks)) .map(|end| series.moving_averages_at_end(end, close_lookbacks))
.unwrap_or_else(|| series.moving_averages(date, close_lookbacks, include_now)) .unwrap_or_else(|| {
series.moving_averages(date, close_lookbacks, include_now)
})
}) })
.unwrap_or([None; 7]) .unwrap_or([None; 7])
} else { } else {
@@ -2243,10 +2233,7 @@ impl DataSet {
if let Ok(index) = dates.binary_search(&date) { if let Ok(index) = dates.binary_search(&date) {
dates.remove(index); dates.remove(index);
} }
rows_by_symbol rows_by_symbol.into_values().map(|rows| rows.len()).sum()
.into_values()
.map(|rows| rows.len())
.sum()
} }
pub fn snapshot_components(&self) -> DataSetSnapshotComponents { pub fn snapshot_components(&self) -> DataSetSnapshotComponents {
@@ -3249,31 +3236,39 @@ impl DataSet {
.map(|end| series.moving_average_at_end(end, lookback)) .map(|end| series.moving_average_at_end(end, lookback))
.unwrap_or_else(|| series.decision_moving_average(date, lookback)) .unwrap_or_else(|| series.decision_moving_average(date, lookback))
}), }),
"volume" | "stock_volume" => self "volume" | "stock_volume" => {
.market_series_by_symbol_id(symbol_id) self.market_series_by_symbol_id(symbol_id)
.and_then(|series| { .and_then(|series| {
self.market_series_end_index_by_symbol_id(date, symbol_id, false) self.market_series_end_index_by_symbol_id(date, symbol_id, false)
.map(|end| { .map(|end| {
series.valid_volume_window(end, lookback).map(|(start, end)| { series
normalize_rolling_factor( .valid_volume_window(end, lookback)
(series.valid_volume_sum_prefix[end] .map(|(start, end)| {
- series.valid_volume_sum_prefix[start]) normalize_rolling_factor(
/ lookback as f64, (series.valid_volume_sum_prefix[end]
12, - series.valid_volume_sum_prefix[start])
) / lookback as f64,
12,
)
})
}) })
}) .unwrap_or_else(|| {
.unwrap_or_else(|| series.decision_volume_moving_average(date, lookback)) series.decision_volume_moving_average(date, lookback)
}), })
"day_open" | "dayopen" => self })
.market_series_by_symbol_id(symbol_id) }
.and_then(|series| { "day_open" | "dayopen" => {
self.market_series_end_index_by_symbol_id(date, symbol_id, false) self.market_series_by_symbol_id(symbol_id)
.map(|end| series.moving_average_at_end(end, lookback, PriceField::DayOpen)) .and_then(|series| {
.unwrap_or_else(|| { self.market_series_end_index_by_symbol_id(date, symbol_id, false)
series.moving_average(date, lookback, PriceField::DayOpen) .map(|end| {
}) series.moving_average_at_end(end, lookback, PriceField::DayOpen)
}), })
.unwrap_or_else(|| {
series.moving_average(date, lookback, PriceField::DayOpen)
})
})
}
"open" => self "open" => self
.market_series_by_symbol_id(symbol_id) .market_series_by_symbol_id(symbol_id)
.and_then(|series| { .and_then(|series| {
@@ -3281,13 +3276,18 @@ impl DataSet {
.map(|end| series.moving_average_at_end(end, lookback, PriceField::Open)) .map(|end| series.moving_average_at_end(end, lookback, PriceField::Open))
.unwrap_or_else(|| series.moving_average(date, lookback, PriceField::Open)) .unwrap_or_else(|| series.moving_average(date, lookback, PriceField::Open))
}), }),
"last" | "last_price" => self "last" | "last_price" => {
.market_series_by_symbol_id(symbol_id) self.market_series_by_symbol_id(symbol_id)
.and_then(|series| { .and_then(|series| {
self.market_series_end_index_by_symbol_id(date, symbol_id, false) self.market_series_end_index_by_symbol_id(date, symbol_id, false)
.map(|end| series.moving_average_at_end(end, lookback, PriceField::Last)) .map(|end| {
.unwrap_or_else(|| series.moving_average(date, lookback, PriceField::Last)) series.moving_average_at_end(end, lookback, PriceField::Last)
}), })
.unwrap_or_else(|| {
series.moving_average(date, lookback, PriceField::Last)
})
})
}
other => self.factor_moving_average(date, symbol, other, lookback), other => self.factor_moving_average(date, symbol, other, lookback),
} }
} }
@@ -3335,31 +3335,37 @@ impl DataSet {
.map(|end| series.moving_average_at_end(end, lookback)) .map(|end| series.moving_average_at_end(end, lookback))
.unwrap_or_else(|| series.current_moving_average(date, lookback)) .unwrap_or_else(|| series.current_moving_average(date, lookback))
}), }),
"volume" | "stock_volume" => self "volume" | "stock_volume" => {
.market_series_by_symbol_id(symbol_id) self.market_series_by_symbol_id(symbol_id)
.and_then(|series| { .and_then(|series| {
self.market_series_end_index_by_symbol_id(date, symbol_id, true) self.market_series_end_index_by_symbol_id(date, symbol_id, true)
.map(|end| { .map(|end| {
series.valid_volume_window(end, lookback).map(|(start, end)| { series
normalize_rolling_factor( .valid_volume_window(end, lookback)
(series.valid_volume_sum_prefix[end] .map(|(start, end)| {
- series.valid_volume_sum_prefix[start]) normalize_rolling_factor(
/ lookback as f64, (series.valid_volume_sum_prefix[end]
12, - series.valid_volume_sum_prefix[start])
) / lookback as f64,
12,
)
})
}) })
}) .unwrap_or_else(|| series.current_volume_moving_average(date, lookback))
.unwrap_or_else(|| series.current_volume_moving_average(date, lookback)) })
}), }
"day_open" | "dayopen" => self "day_open" | "dayopen" => {
.market_series_by_symbol_id(symbol_id) self.market_series_by_symbol_id(symbol_id)
.and_then(|series| { .and_then(|series| {
self.market_series_end_index_by_symbol_id(date, symbol_id, true) self.market_series_end_index_by_symbol_id(date, symbol_id, true)
.map(|end| series.moving_average_at_end(end, lookback, PriceField::DayOpen)) .map(|end| {
.unwrap_or_else(|| { series.moving_average_at_end(end, lookback, PriceField::DayOpen)
series.moving_average(date, lookback, PriceField::DayOpen) })
}) .unwrap_or_else(|| {
}), series.moving_average(date, lookback, PriceField::DayOpen)
})
})
}
"open" => self "open" => self
.market_series_by_symbol_id(symbol_id) .market_series_by_symbol_id(symbol_id)
.and_then(|series| { .and_then(|series| {
@@ -3367,13 +3373,18 @@ impl DataSet {
.map(|end| series.moving_average_at_end(end, lookback, PriceField::Open)) .map(|end| series.moving_average_at_end(end, lookback, PriceField::Open))
.unwrap_or_else(|| series.moving_average(date, lookback, PriceField::Open)) .unwrap_or_else(|| series.moving_average(date, lookback, PriceField::Open))
}), }),
"last" | "last_price" => self "last" | "last_price" => {
.market_series_by_symbol_id(symbol_id) self.market_series_by_symbol_id(symbol_id)
.and_then(|series| { .and_then(|series| {
self.market_series_end_index_by_symbol_id(date, symbol_id, true) self.market_series_end_index_by_symbol_id(date, symbol_id, true)
.map(|end| series.moving_average_at_end(end, lookback, PriceField::Last)) .map(|end| {
.unwrap_or_else(|| series.moving_average(date, lookback, PriceField::Last)) series.moving_average_at_end(end, lookback, PriceField::Last)
}), })
.unwrap_or_else(|| {
series.moving_average(date, lookback, PriceField::Last)
})
})
}
other => self.factor_moving_average(date, symbol, other, lookback), other => self.factor_moving_average(date, symbol, other, lookback),
} }
} }
@@ -4732,8 +4743,12 @@ mod tests {
execution_quotes.clone(), execution_quotes.clone(),
) )
.expect("flat dataset"); .expect("flat dataset");
let grouped = DataSet::from_daily_bundles_with_execution_quotes(instruments, bundles, execution_quotes) let grouped = DataSet::from_daily_bundles_with_execution_quotes(
.expect("daily bundle dataset"); instruments,
bundles,
execution_quotes,
)
.expect("daily bundle dataset");
assert_eq!(flat.calendar().days(), grouped.calendar().days()); assert_eq!(flat.calendar().days(), grouped.calendar().days());
assert_eq!(flat.benchmark_code(), grouped.benchmark_code()); assert_eq!(flat.benchmark_code(), grouped.benchmark_code());
@@ -4901,7 +4916,8 @@ mod tests {
let signal_id = data.symbol_id("000300.SH").unwrap(); let signal_id = data.symbol_id("000300.SH").unwrap();
let day = data.daily_snapshot_view(date); let day = data.daily_snapshot_view(date);
assert_eq!( assert_eq!(
data.market_by_symbol_id(date, signal_id).map(|row| row.symbol.as_str()), data.market_by_symbol_id(date, signal_id)
.map(|row| row.symbol.as_str()),
Some("000300.SH") Some("000300.SH")
); );
assert!(data.factor_by_symbol_id(date, signal_id).is_none()); assert!(data.factor_by_symbol_id(date, signal_id).is_none());
@@ -4923,9 +4939,10 @@ mod tests {
data.get_factor("000300.SH", date, date, "market_cap") data.get_factor("000300.SH", date, date, "market_cap")
.is_empty() .is_empty()
); );
assert!(data assert!(
.get_factor("999999.SZ", date, date, "market_cap") data.get_factor("999999.SZ", date, date, "market_cap")
.is_empty()); .is_empty()
);
} }
#[test] #[test]
@@ -4955,11 +4972,7 @@ mod tests {
.iter() .iter()
.enumerate() .enumerate()
.map(|(index, symbol)| { .map(|(index, symbol)| {
let mut row = market_row( let mut row = market_row("2025-01-02", 10.0 + index as f64 / 1000.0, 1_000_000);
"2025-01-02",
10.0 + index as f64 / 1000.0,
1_000_000,
);
row.symbol = symbol.clone(); row.symbol = symbol.clone();
row row
}) })
@@ -5407,7 +5420,11 @@ mod tests {
.iter() .iter()
.enumerate() .enumerate()
.map(|(index, date)| { .map(|(index, date)| {
market_row(&date.format("%Y-%m-%d").to_string(), 10.0 + index as f64, 100 + index as u64) market_row(
&date.format("%Y-%m-%d").to_string(),
10.0 + index as f64,
100 + index as u64,
)
}) })
.collect(), .collect(),
dates dates
@@ -5420,7 +5437,10 @@ mod tests {
pe_ttm: 10.0, pe_ttm: 10.0,
turnover_ratio: None, turnover_ratio: None,
effective_turnover_ratio: None, effective_turnover_ratio: None,
extra_factors: BTreeMap::from([(Cow::Borrowed(BACKWARD_ADJUSTMENT_FACTOR_FIELD), 1.0)]), extra_factors: BTreeMap::from([(
Cow::Borrowed(BACKWARD_ADJUSTMENT_FACTOR_FIELD),
1.0,
)]),
}) })
.collect(), .collect(),
Vec::new(), Vec::new(),
@@ -5507,10 +5527,11 @@ mod tests {
let symbol_id = data.symbol_id("000001.SZ").expect("symbol id"); let symbol_id = data.symbol_id("000001.SZ").expect("symbol id");
let dates = data.calendar().days(); let dates = data.calendar().days();
assert!(data assert!(
.market_series_end_positions_by_symbol_id data.market_series_end_positions_by_symbol_id
.as_ref() .as_ref()
.is_some()); .is_some()
);
assert_eq!( assert_eq!(
data.market_series_end_index_by_symbol_id(dates[0], symbol_id, false), data.market_series_end_index_by_symbol_id(dates[0], symbol_id, false),
Some(0) Some(0)
+10 -12
View File
@@ -2226,8 +2226,7 @@ where
let coarse_schedule_rules = schedule_rules let coarse_schedule_rules = schedule_rules
.iter() .iter()
.filter(|rule| { .filter(|rule| {
keep_timed_rules_on_coarse_stage keep_timed_rules_on_coarse_stage || !is_on_day_or_bar_physical_time_rule(rule)
|| !is_on_day_or_bar_physical_time_rule(rule)
}) })
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -2774,9 +2773,7 @@ where
]); ]);
let minute_all_time_rules = intraday_schedule_rules let minute_all_time_rules = intraday_schedule_rules
.iter() .iter()
.filter(|rule| { .filter(|rule| rule.stage == ScheduleStage::Minute && rule.time_rule.is_none())
rule.stage == ScheduleStage::Minute && rule.time_rule.is_none()
})
.cloned() .cloned()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let minute_schedule_all_times = !minute_all_time_rules.is_empty(); let minute_schedule_all_times = !minute_all_time_rules.is_empty();
@@ -2798,10 +2795,9 @@ where
loop { loop {
let next_quote_timestamp = minute_quotes.peek().map(|quote| quote.timestamp); let next_quote_timestamp = minute_quotes.peek().map(|quote| quote.timestamp);
let next_schedule_timestamp = minute_schedule_timestamps.peek().copied(); let next_schedule_timestamp = minute_schedule_timestamps.peek().copied();
let Some(minute_timestamp) = next_minute_event_timestamp( let Some(minute_timestamp) =
next_quote_timestamp, next_minute_event_timestamp(next_quote_timestamp, next_schedule_timestamp)
next_schedule_timestamp, else {
) else {
break; break;
}; };
let minute_time = minute_timestamp.time(); let minute_time = minute_timestamp.time();
@@ -2816,8 +2812,7 @@ where
.expect("peeked minute quote must be available"), .expect("peeked minute quote must be available"),
); );
} }
let has_specific_schedule = let has_specific_schedule = next_schedule_timestamp == Some(minute_timestamp);
next_schedule_timestamp == Some(minute_timestamp);
if has_specific_schedule { if has_specific_schedule {
minute_schedule_timestamps.next(); minute_schedule_timestamps.next();
} }
@@ -5649,7 +5644,10 @@ mod tests {
result.fills[0].execution_start_timestamp, result.fills[0].execution_start_timestamp,
date.and_hms_opt(10, 18, 0) date.and_hms_opt(10, 18, 0)
); );
assert_eq!(result.fills[1].execution_timestamp, date.and_hms_opt(15, 10, 0)); assert_eq!(
result.fills[1].execution_timestamp,
date.and_hms_opt(15, 10, 0)
);
assert_eq!(result.fills[1].price, 10.0); assert_eq!(result.fills[1].price, 10.0);
assert_eq!(result.fills[0].reason, "morning"); assert_eq!(result.fills[0].reason, "morning");
assert_eq!(result.fills[1].reason, "post_close"); assert_eq!(result.fills[1].reason, "post_close");
+4 -5
View File
@@ -75,11 +75,10 @@ pub use platform_strategy_spec::{
StrategyExpressionActionConfig, StrategyExpressionAllocationConfig, StrategyExpressionActionConfig, StrategyExpressionAllocationConfig,
StrategyExpressionOrderingConfig, StrategyExpressionRiskConfig, StrategyExpressionOrderingConfig, StrategyExpressionRiskConfig,
StrategyExpressionScheduleConfig, StrategyExpressionSelectionConfig, StrategyExpressionScheduleConfig, StrategyExpressionSelectionConfig,
StrategyExpressionTradingConfig, StrategyPortfolioDrawdownControlConfig, StrategyExpressionTradingConfig, StrategyPortfolioDrawdownControlConfig, StrategyRebalanceSpec,
StrategyRebalanceSpec, StrategyRiskPolicySpec, StrategyRuntimeEnvironment, StrategyRiskPolicySpec, StrategyRuntimeEnvironment, StrategyRuntimeExpressions,
StrategyRuntimeExpressions, StrategyRuntimeSpec, StrategyUniverseSpec, StrategyRuntimeSpec, StrategyUniverseSpec, platform_expr_config_from_spec,
platform_expr_config_from_spec, platform_expr_config_from_value, platform_expr_config_from_value, validate_strategy_risk_policy_fields,
validate_strategy_risk_policy_fields,
}; };
pub use portfolio::{CashReceivable, HoldingSummary, PendingCashFlow, PortfolioState, Position}; pub use portfolio::{CashReceivable, HoldingSummary, PendingCashFlow, PortfolioState, Position};
pub use risk_control::{ pub use risk_control::{
+60 -69
View File
@@ -1325,7 +1325,8 @@ impl PlatformExprStrategy {
Self::stock_extra_factors_required_for_config(&config, &prelude_declared_identifiers); Self::stock_extra_factors_required_for_config(&config, &prelude_declared_identifiers);
let stock_extra_factor_identifiers = let stock_extra_factor_identifiers =
Self::stock_extra_factor_identifiers_for_config(&config, &prelude_declared_identifiers); Self::stock_extra_factor_identifiers_for_config(&config, &prelude_declared_identifiers);
let stock_extra_factor_map_required = Self::stock_extra_factor_map_required_for_config(&config); let stock_extra_factor_map_required =
Self::stock_extra_factor_map_required_for_config(&config);
let stock_text_factors_required = Self::stock_text_factors_required_for_config( let stock_text_factors_required = Self::stock_text_factors_required_for_config(
&config, &config,
&normalized_stock_filter_expr, &normalized_stock_filter_expr,
@@ -1503,8 +1504,7 @@ impl PlatformExprStrategy {
when_expr, when_expr,
.. ..
} => { } => {
let target_field = let target_field = format!("explicit_actions[{index}].target_weights_expr");
format!("explicit_actions[{index}].target_weights_expr");
if !Self::collect_preflight_float_map_values( if !Self::collect_preflight_float_map_values(
&target_field, &target_field,
target_weights_expr, target_weights_expr,
@@ -4019,20 +4019,26 @@ impl PlatformExprStrategy {
return Ok(Arc::clone(state)); return Ok(Arc::clone(state));
} }
let market = ctx.data.market_by_symbol_id(date, symbol_id).ok_or_else(|| { let market = ctx
BacktestError::Data(crate::data::DataSetError::MissingSnapshot { .data
kind: "market", .market_by_symbol_id(date, symbol_id)
date, .ok_or_else(|| {
symbol: symbol.to_string(), BacktestError::Data(crate::data::DataSetError::MissingSnapshot {
}) kind: "market",
})?; date,
let candidate = ctx.data.candidate_by_symbol_id(date, symbol_id).ok_or_else(|| { symbol: symbol.to_string(),
BacktestError::Data(crate::data::DataSetError::MissingSnapshot { })
kind: "candidate", })?;
date, let candidate = ctx
symbol: symbol.to_string(), .data
}) .candidate_by_symbol_id(date, symbol_id)
})?; .ok_or_else(|| {
BacktestError::Data(crate::data::DataSetError::MissingSnapshot {
kind: "candidate",
date,
symbol: symbol.to_string(),
})
})?;
let feature_market = if factor_date == date { let feature_market = if factor_date == date {
market market
} else { } else {
@@ -4040,13 +4046,16 @@ impl PlatformExprStrategy {
.market_by_symbol_id(factor_date, symbol_id) .market_by_symbol_id(factor_date, symbol_id)
.unwrap_or(market) .unwrap_or(market)
}; };
let factor = ctx.data.factor_by_symbol_id(factor_date, symbol_id).ok_or_else(|| { let factor = ctx
BacktestError::Data(crate::data::DataSetError::MissingSnapshot { .data
kind: "factor", .factor_by_symbol_id(factor_date, symbol_id)
date: factor_date, .ok_or_else(|| {
symbol: symbol.to_string(), BacktestError::Data(crate::data::DataSetError::MissingSnapshot {
}) kind: "factor",
})?; date: factor_date,
symbol: symbol.to_string(),
})
})?;
let intraday_same_day_factor = self.uses_intraday_execution_quotes() let intraday_same_day_factor = self.uses_intraday_execution_quotes()
&& factor_date == date && factor_date == date
&& !ctx.is_lagged_execution(); && !ctx.is_lagged_execution();
@@ -6353,13 +6362,7 @@ impl PlatformExprStrategy {
args: &[String], args: &[String],
) -> Result<RuntimeHelperResolution, BacktestError> { ) -> Result<RuntimeHelperResolution, BacktestError> {
if let Some(compiled_args) = Self::compile_runtime_helper_args(helper, args) { if let Some(compiled_args) = Self::compile_runtime_helper_args(helper, args) {
return self.resolve_compiled_runtime_helper( return self.resolve_compiled_runtime_helper(ctx, day, stock, helper, &compiled_args);
ctx,
day,
stock,
helper,
&compiled_args,
);
} }
match helper { match helper {
"factor" => { "factor" => {
@@ -7665,8 +7668,8 @@ impl PlatformExprStrategy {
&self.config.position_exposure_schedule, &self.config.position_exposure_schedule,
ctx.execution_date, ctx.execution_date,
) )
.unwrap_or(strategy_exposure) .unwrap_or(strategy_exposure)
.clamp(0.0, 1.0); .clamp(0.0, 1.0);
let Some(controller) = self.portfolio_drawdown_controller.as_mut() else { let Some(controller) = self.portfolio_drawdown_controller.as_mut() else {
return Ok(risk_on_exposure); return Ok(risk_on_exposure);
}; };
@@ -9231,14 +9234,13 @@ impl PlatformExprStrategy {
continue; continue;
} }
let synthetic_candidate; let synthetic_candidate;
let candidate = let candidate = if let Some(candidate) = execution_day.candidate(symbol_id) {
if let Some(candidate) = execution_day.candidate(symbol_id) { candidate
candidate } else {
} else { synthetic_candidate =
synthetic_candidate = crate::data::missing_candidate_risk_state(date, &factor.symbol);
crate::data::missing_candidate_risk_state(date, &factor.symbol); &synthetic_candidate
&synthetic_candidate };
};
let Some(market) = execution_day.market(symbol_id) else { let Some(market) = execution_day.market(symbol_id) else {
continue; continue;
}; };
@@ -9579,10 +9581,7 @@ impl PlatformExprStrategy {
fn rank_reuses_market_cap_order(&self) -> bool { fn rank_reuses_market_cap_order(&self) -> bool {
self.config.rank_expr.trim().is_empty() self.config.rank_expr.trim().is_empty()
&& !self.config.rank_desc && !self.config.rank_desc
&& matches!( && matches!(self.config.rank_by.trim(), "market_cap" | "market_cap_bn")
self.config.rank_by.trim(),
"market_cap" | "market_cap_bn"
)
} }
fn selection_candidate_passes_filters( fn selection_candidate_passes_filters(
@@ -12559,12 +12558,11 @@ mod tests {
use super::{ use super::{
CompiledRuntimeHelperArgs, PlatformAccountActionKind, PlatformExplicitActionStage, CompiledRuntimeHelperArgs, PlatformAccountActionKind, PlatformExplicitActionStage,
PlatformExplicitCancelKind, PlatformExplicitOrderKind, PlatformExprStrategy, PlatformExplicitCancelKind, PlatformExplicitOrderKind, PlatformExprStrategy,
PlatformExprStrategyConfig, PlatformExprStrategyConfig, PlatformPortfolioDrawdownControlConfig,
PlatformPortfolioDrawdownControlConfig, PlatformPortfolioDrawdownController, PlatformPortfolioDrawdownController, PlatformRebalanceSchedule, PlatformScheduleFrequency,
PlatformRebalanceSchedule, PlatformScheduleFrequency, PlatformStopTakeReferencePriceMode, PlatformStopTakeReferencePriceMode, PlatformTradeAction, PlatformUniverseActionKind,
PlatformTradeAction, PlatformUniverseActionKind, RuntimeHelperResolution, RuntimeHelperResolution, SelectionRiskDeferral, StockFilterQuoteUsage,
SelectionRiskDeferral, StockFilterQuoteUsage, framework_stock_rolling_factor_requirement, framework_stock_rolling_factor_requirement, scheduled_position_exposure,
scheduled_position_exposure,
}; };
use crate::{ use crate::{
AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction, AlgoOrderStyle, BenchmarkSnapshot, CandidateEligibility, CorporateAction,
@@ -12582,10 +12580,7 @@ mod tests {
#[test] #[test]
fn dated_position_exposure_uses_the_latest_effective_point() { fn dated_position_exposure_uses_the_latest_effective_point() {
let schedule = BTreeMap::from([ let schedule = BTreeMap::from([(d(2026, 8, 14), 0.6451), (d(2026, 8, 20), 0.3225)]);
(d(2026, 8, 14), 0.6451),
(d(2026, 8, 20), 0.3225),
]);
assert_eq!(scheduled_position_exposure(&schedule, d(2026, 8, 13)), None); assert_eq!(scheduled_position_exposure(&schedule, d(2026, 8, 13)), None);
assert_eq!( assert_eq!(
scheduled_position_exposure(&schedule, d(2026, 8, 14)), scheduled_position_exposure(&schedule, d(2026, 8, 14)),
@@ -14429,7 +14424,9 @@ mod tests {
.stock_state_with_factor_date(&ctx, date, date, present_symbol) .stock_state_with_factor_date(&ctx, date, date, present_symbol)
.expect("factor map stock state"); .expect("factor map stock state");
assert!(map_stock.extra_factors.contains_key("unused_factor")); assert!(map_stock.extra_factors.contains_key("unused_factor"));
let map_day = map_strategy.day_state(&ctx, date).expect("factor map day state"); let map_day = map_strategy
.day_state(&ctx, date)
.expect("factor map day state");
assert!( assert!(
map_strategy map_strategy
.stock_passes_expr(&ctx, &map_day, &map_stock) .stock_passes_expr(&ctx, &map_day, &map_stock)
@@ -33310,11 +33307,7 @@ let target_exposure = csi_ready ? dynamic_exposure : 0.0;
factor_value(\"quality_score\", 1)", factor_value(\"quality_score\", 1)",
); );
let vm = plan.numeric_vm.as_ref().expect("numeric VM plan"); let vm = plan.numeric_vm.as_ref().expect("numeric VM plan");
let bindings = vm let bindings = vm.helper_bindings.iter().flatten().collect::<Vec<_>>();
.helper_bindings
.iter()
.flatten()
.collect::<Vec<_>>();
assert_eq!(bindings.len(), 12); assert_eq!(bindings.len(), 12);
assert!( assert!(
@@ -33336,14 +33329,12 @@ let target_exposure = csi_ready ? dynamic_exposure : 0.0;
let generic_started = std::time::Instant::now(); let generic_started = std::time::Instant::now();
let mut generic_checksum = 0usize; let mut generic_checksum = 0usize;
for _ in 0..iterations { for _ in 0..iterations {
let field = PlatformExprStrategy::parse_string_or_identifier(std::hint::black_box( let field =
&args[0], PlatformExprStrategy::parse_string_or_identifier(std::hint::black_box(&args[0]))
)) .expect("field");
.expect("field"); let lookback =
let lookback = PlatformExprStrategy::parse_positive_usize(std::hint::black_box( PlatformExprStrategy::parse_positive_usize(std::hint::black_box(&args[1]))
&args[1], .expect("lookback");
))
.expect("lookback");
generic_checksum = generic_checksum.wrapping_add(field.len() + lookback); generic_checksum = generic_checksum.wrapping_add(field.len() + lookback);
} }
let generic_seconds = generic_started.elapsed().as_secs_f64(); let generic_seconds = generic_started.elapsed().as_secs_f64();
@@ -4057,8 +4057,7 @@ mod tests {
}); });
let cfg = platform_expr_config_from_value("", "", &spec).expect("config"); let cfg = platform_expr_config_from_value("", "", &spec).expect("config");
assert_eq!( assert_eq!(
cfg.position_exposure_schedule cfg.position_exposure_schedule[&NaiveDate::from_ymd_opt(2026, 8, 14).unwrap()],
[&NaiveDate::from_ymd_opt(2026, 8, 14).unwrap()],
0.6451, 0.6451,
); );
assert_eq!(cfg.position_exposure_schedule.len(), 2); assert_eq!(cfg.position_exposure_schedule.len(), 2);
+4 -2
View File
@@ -3,10 +3,10 @@ use std::collections::BTreeSet;
use chrono::NaiveDate; use chrono::NaiveDate;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::OrderSide;
use crate::data::{CandidateEligibility, DailyMarketSnapshot, PriceField}; use crate::data::{CandidateEligibility, DailyMarketSnapshot, PriceField};
use crate::instrument::Instrument; use crate::instrument::Instrument;
use crate::portfolio::Position; use crate::portfolio::Position;
use crate::OrderSide;
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
pub struct ChinaAShareRiskControl; pub struct ChinaAShareRiskControl;
@@ -560,7 +560,9 @@ impl ChinaAShareRiskControl {
// lifecycle fact must still protect the sell path. Otherwise a // lifecycle fact must still protect the sell path. Otherwise a
// `inactive_or_delisted` candidate could fall through to a synthetic // `inactive_or_delisted` candidate could fall through to a synthetic
// sell price and violate the unresolved-delisted holding contract. // sell price and violate the unresolved-delisted holding contract.
if let Some(reason) = candidate_active_status_rejection(candidate, config, RiskCheckScope::Sell) { if let Some(reason) =
candidate_active_status_rejection(candidate, config, RiskCheckScope::Sell)
{
return Some(reason); return Some(reason);
} }
if config.static_rules.reject_paused_sell && (market.paused || candidate.is_paused) { if config.static_rules.reject_paused_sell && (market.paused || candidate.is_paused) {
+21 -12
View File
@@ -284,18 +284,27 @@ mod tests {
assert!(scheduler.is_due_on(d(2025, 1, 30), &daily)); assert!(scheduler.is_due_on(d(2025, 1, 30), &daily));
assert!(scheduler.is_due_on(d(2025, 1, 31), &daily)); assert!(scheduler.is_due_on(d(2025, 1, 31), &daily));
assert!(scheduler.triggered_rules_at( assert!(
d(2025, 1, 30), scheduler
ScheduleStage::OnDay, .triggered_rules_at(
Some(NaiveTime::from_hms_opt(15, 0, 0).unwrap()), d(2025, 1, 30),
std::slice::from_ref(&daily), ScheduleStage::OnDay,
).len() == 1); Some(NaiveTime::from_hms_opt(15, 0, 0).unwrap()),
assert!(scheduler.triggered_rules_at( std::slice::from_ref(&daily),
d(2025, 1, 30), )
ScheduleStage::OnDay, .len()
Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()), == 1
std::slice::from_ref(&daily), );
).is_empty()); assert!(
scheduler
.triggered_rules_at(
d(2025, 1, 30),
ScheduleStage::OnDay,
Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()),
std::slice::from_ref(&daily),
)
.is_empty()
);
} }
#[test] #[test]
@@ -52,9 +52,7 @@ fn dataset(day_count: usize, bars_per_day: usize) -> (DataSet, Vec<NaiveDate>) {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut quotes = Vec::with_capacity(day_count * bars_per_day); let mut quotes = Vec::with_capacity(day_count * bars_per_day);
for date in &dates { for date in &dates {
let session_start = date let session_start = date.and_hms_opt(9, 30, 0).expect("valid session start");
.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 {