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