完善策略调度执行价校验

This commit is contained in:
boris
2026-06-13 15:26:56 +08:00
parent 4cf90d83a3
commit 0dca8e0eff
5 changed files with 744 additions and 51 deletions
+229 -16
View File
@@ -445,6 +445,38 @@ pub struct EligibleUniverseSnapshot {
pub free_float_cap_bn: f64,
}
pub fn decision_adjusted_cap_bn(
factor_date: NaiveDate,
raw_cap_bn: f64,
market: &DailyMarketSnapshot,
) -> f64 {
if !raw_cap_bn.is_finite() || raw_cap_bn <= 0.0 {
return f64::NAN;
}
if factor_date != market.date {
return raw_cap_bn;
}
if !market.close.is_finite()
|| market.close <= 0.0
|| !market.prev_close.is_finite()
|| market.prev_close <= 0.0
{
return f64::NAN;
}
raw_cap_bn * market.prev_close / market.close
}
pub fn decision_market_cap_bn(factor: &DailyFactorSnapshot, market: &DailyMarketSnapshot) -> f64 {
decision_adjusted_cap_bn(factor.date, factor.market_cap_bn, market)
}
pub fn decision_free_float_cap_bn(
factor: &DailyFactorSnapshot,
market: &DailyMarketSnapshot,
) -> f64 {
decision_adjusted_cap_bn(factor.date, factor.free_float_cap_bn, market)
}
#[derive(Debug, Clone)]
struct SymbolPriceSeries {
snapshots: Vec<DailyMarketSnapshot>,
@@ -597,11 +629,16 @@ impl SymbolPriceSeries {
}
fn decision_volume_moving_average(&self, date: NaiveDate, lookback: usize) -> Option<f64> {
let values = self.decision_volume_values(date, lookback)?;
if values.len() < lookback {
if lookback == 0 {
return None;
}
let sum = values.iter().sum::<f64>();
let end = self.previous_completed_end_index(date)?;
let end_count = *self.unpaused_count_prefix.get(end)?;
if end_count < lookback {
return None;
}
let start_count = end_count - lookback;
let sum = self.unpaused_volume_prefix[end_count] - self.unpaused_volume_prefix[start_count];
Some(sum / lookback as f64)
}
@@ -691,6 +728,7 @@ struct BenchmarkPriceSeries {
dates: Vec<NaiveDate>,
opens: Vec<f64>,
closes: Vec<f64>,
prev_closes: Vec<f64>,
open_prefix: Vec<f64>,
close_prefix: Vec<f64>,
}
@@ -702,12 +740,14 @@ impl BenchmarkPriceSeries {
let dates = sorted.iter().map(|row| row.date).collect::<Vec<_>>();
let opens = sorted.iter().map(|row| row.open).collect::<Vec<_>>();
let closes = sorted.iter().map(|row| row.close).collect::<Vec<_>>();
let prev_closes = sorted.iter().map(|row| row.prev_close).collect::<Vec<_>>();
let open_prefix = prefix_sums(&opens);
let close_prefix = prefix_sums(&closes);
Self {
dates,
opens,
closes,
prev_closes,
open_prefix,
close_prefix,
}
@@ -717,6 +757,24 @@ impl BenchmarkPriceSeries {
self.moving_average_for(date, lookback, PriceField::Close)
}
fn decision_close(&self, date: NaiveDate) -> Option<f64> {
match self.dates.binary_search(&date) {
Ok(idx) => self
.prev_closes
.get(idx)
.copied()
.filter(|value| value.is_finite() && *value > 0.0)
.or_else(|| {
idx.checked_sub(1)
.and_then(|prev| self.closes.get(prev).copied())
}),
Err(0) => None,
Err(idx) => idx
.checked_sub(1)
.and_then(|prev| self.closes.get(prev).copied()),
}
}
fn decision_moving_average(&self, date: NaiveDate, lookback: usize) -> Option<f64> {
if lookback == 0 {
return None;
@@ -734,12 +792,7 @@ impl BenchmarkPriceSeries {
Some(sum / lookback as f64)
}
fn decision_values_for(
&self,
date: NaiveDate,
lookback: usize,
field: PriceField,
) -> Vec<f64> {
fn decision_values_for(&self, date: NaiveDate, lookback: usize, field: PriceField) -> Vec<f64> {
if lookback == 0 {
return Vec::new();
}
@@ -2278,6 +2331,10 @@ impl DataSet {
self.benchmark_series_cache.moving_average(date, lookback)
}
pub fn benchmark_decision_close(&self, date: NaiveDate) -> Option<f64> {
self.benchmark_series_cache.decision_close(date)
}
pub fn benchmark_decision_moving_average(
&self,
date: NaiveDate,
@@ -2318,11 +2375,9 @@ impl DataSet {
"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,
),
_ => self
.benchmark_series_cache
.decision_values_for(date, lookback, PriceField::Close),
}
}
@@ -3409,10 +3464,15 @@ fn build_eligible_universe(
{
continue;
}
let market_cap_bn = decision_market_cap_bn(factor, market);
if market_cap_bn <= 0.0 || !market_cap_bn.is_finite() {
continue;
}
let free_float_cap_bn = decision_free_float_cap_bn(factor, market);
rows.push(EligibleUniverseSnapshot {
symbol: factor.symbol.clone(),
market_cap_bn: factor.market_cap_bn,
free_float_cap_bn: factor.free_float_cap_bn,
market_cap_bn,
free_float_cap_bn,
});
}
rows.sort_by(|left, right| {
@@ -3471,6 +3531,17 @@ mod tests {
}
}
fn benchmark_row(date: &str, close: f64) -> BenchmarkSnapshot {
BenchmarkSnapshot {
date: NaiveDate::parse_from_str(date, "%Y-%m-%d").unwrap(),
benchmark: "000852.SH".to_string(),
open: close,
close,
prev_close: close - 1.0,
volume: 1_000_000,
}
}
#[test]
fn baseline_selection_uses_structured_instrument_dates_and_status_only() {
let date = NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap();
@@ -3504,6 +3575,14 @@ mod tests {
Some(&instrument("正常名称", "delisted", None)),
date
));
assert!(instrument_passes_baseline_selection(
Some(&instrument(
"正常名称",
"delisted",
Some(NaiveDate::parse_from_str("2025-04-30", "%Y-%m-%d").unwrap()),
)),
date
));
assert!(!instrument_passes_baseline_selection(
Some(&instrument(
"正常名称",
@@ -3545,6 +3624,28 @@ mod tests {
);
}
#[test]
fn decision_close_average_ignores_current_day_close() {
let mut current = market_row("2025-01-06", 12.0, 10_000);
current.close = 9_999.0;
current.last_price = 9_999.0;
let series = SymbolPriceSeries::new(&[
market_row("2025-01-02", 10.0, 100),
market_row("2025-01-03", 11.0, 200),
current,
]);
let decision_date = NaiveDate::parse_from_str("2025-01-06", "%Y-%m-%d").unwrap();
assert_eq!(
series.decision_close_moving_average(decision_date, 2),
Some(11.5)
);
assert_eq!(
series.moving_average(decision_date, 2, PriceField::Close),
Some((11.0 + 9_999.0) / 2.0)
);
}
#[test]
fn decision_volume_average_skips_paused_days_before_counting_window() {
let mut paused = market_row("2025-01-03", 11.0, 0);
@@ -3572,6 +3673,118 @@ mod tests {
);
}
#[test]
fn eligible_universe_uses_decision_market_cap_same_date() {
let date = NaiveDate::parse_from_str("2025-01-06", "%Y-%m-%d").unwrap();
let instrument = |symbol: &str| Instrument {
symbol: symbol.to_string(),
name: symbol.to_string(),
board: if symbol.ends_with(".SH") { "SH" } else { "SZ" }.to_string(),
round_lot: 100,
listed_at: Some(NaiveDate::parse_from_str("2020-01-01", "%Y-%m-%d").unwrap()),
delisted_at: None,
status: "active".to_string(),
};
let market = |symbol: &str, prev_close: f64, close: f64| DailyMarketSnapshot {
date,
symbol: symbol.to_string(),
timestamp: Some("2025-01-06 10:18:00".to_string()),
day_open: prev_close,
open: prev_close,
high: close.max(prev_close),
low: close.min(prev_close),
close,
last_price: prev_close,
bid1: prev_close,
ask1: prev_close,
prev_close,
volume: 100_000,
tick_volume: 1_000,
bid1_volume: 1_000,
ask1_volume: 1_000,
trading_phase: Some("continuous".to_string()),
paused: false,
upper_limit: prev_close * 1.1,
lower_limit: prev_close * 0.9,
price_tick: 0.01,
};
let factor =
|symbol: &str, market_cap_bn: f64, free_float_cap_bn: f64| DailyFactorSnapshot {
date,
symbol: symbol.to_string(),
market_cap_bn,
free_float_cap_bn,
pe_ttm: 10.0,
turnover_ratio: Some(1.0),
effective_turnover_ratio: Some(1.0),
extra_factors: BTreeMap::new(),
};
let candidate = |symbol: &str| CandidateEligibility {
date,
symbol: symbol.to_string(),
is_st: false,
is_new_listing: false,
is_paused: false,
allow_buy: true,
allow_sell: true,
is_kcb: false,
is_one_yuan: false,
};
let data = DataSet::from_components(
vec![instrument("000001.SZ"), instrument("000002.SZ")],
vec![
market("000001.SZ", 10.0, 20.0),
market("000002.SZ", 10.0, 10.0),
],
vec![
factor("000001.SZ", 12.0, 4.0),
factor("000002.SZ", 10.0, 5.0),
],
vec![candidate("000001.SZ"), candidate("000002.SZ")],
vec![BenchmarkSnapshot {
date,
benchmark: "000852.SH".to_string(),
open: 100.0,
close: 101.0,
prev_close: 99.0,
volume: 1_000_000,
}],
)
.expect("dataset");
let rows = data.eligible_universe_on(date);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].symbol, "000001.SZ");
assert!((rows[0].market_cap_bn - 6.0).abs() < 1e-9);
assert!((rows[0].free_float_cap_bn - 2.0).abs() < 1e-9);
assert_eq!(rows[1].symbol, "000002.SZ");
assert!((rows[1].market_cap_bn - 10.0).abs() < 1e-9);
}
#[test]
fn benchmark_decision_close_windows_exclude_current_close() {
let series = BenchmarkPriceSeries::new(&[
benchmark_row("2025-01-02", 100.0),
benchmark_row("2025-01-03", 200.0),
benchmark_row("2025-01-06", 9_999.0),
]);
let decision_date = NaiveDate::parse_from_str("2025-01-06", "%Y-%m-%d").unwrap();
assert_eq!(series.decision_close(decision_date), Some(9_998.0));
assert_eq!(
series.decision_moving_average(decision_date, 2),
Some(150.0)
);
assert_eq!(
series.decision_values_for(decision_date, 2, PriceField::Close),
vec![100.0, 200.0]
);
assert_eq!(
series.moving_average(decision_date, 2),
Some((200.0 + 9_999.0) / 2.0)
);
}
#[test]
fn reads_mixed_numeric_and_text_extra_factors_from_quoted_csv_json() {
let path = temp_csv_path("mixed_factor_maps");