Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3c70ea566 | |||
| 07b7b181b6 | |||
| fe7243bbc3 | |||
| 875e31f71f | |||
| 61bd14d001 | |||
| 531df39911 | |||
| e912e2a6f5 | |||
| b1f2fcb85c | |||
| 828690a51e |
Generated
+30
@@ -145,6 +145,15 @@ version = "1.12.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "castaway"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.59"
|
version = "1.2.59"
|
||||||
@@ -192,6 +201,20 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "compact_str"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "79fcda08c33bb58b97008b2cdada6622500e949e060f5913361763121abd2416"
|
||||||
|
dependencies = [
|
||||||
|
"castaway",
|
||||||
|
"cfg-if",
|
||||||
|
"itoa",
|
||||||
|
"serde",
|
||||||
|
"static_assertions",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-random"
|
name = "const-random"
|
||||||
version = "0.1.18"
|
version = "0.1.18"
|
||||||
@@ -316,6 +339,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"compact_str",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"rayon",
|
"rayon",
|
||||||
"rhai",
|
"rhai",
|
||||||
@@ -2044,3 +2068,9 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
"syn 3.0.5",
|
"syn 3.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ license.workspace = true
|
|||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
compact_str = { version = "=0.10.0", features = ["serde"] }
|
||||||
rust_decimal.workspace = true
|
rust_decimal.workspace = true
|
||||||
ahash.workspace = true
|
ahash.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
|
|||||||
@@ -8299,7 +8299,7 @@ mod tests {
|
|||||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-02 09:33:00".to_string()),
|
timestamp: Some("2025-01-02 09:33:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -8324,7 +8324,7 @@ mod tests {
|
|||||||
|
|
||||||
fn limit_test_quote(last_price: f64, bid1: f64, ask1: f64) -> IntradayExecutionQuote {
|
fn limit_test_quote(last_price: f64, bid1: f64, ask1: f64) -> IntradayExecutionQuote {
|
||||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(9, 33, 0).expect("valid timestamp"),
|
||||||
@@ -8425,7 +8425,7 @@ mod tests {
|
|||||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -8505,7 +8505,7 @@ mod tests {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| {
|
.map(|symbol| {
|
||||||
let mut snapshot = limit_test_snapshot();
|
let mut snapshot = limit_test_snapshot();
|
||||||
snapshot.symbol = (*symbol).to_string();
|
snapshot.symbol = (*symbol).into();
|
||||||
if block_exit && *symbol == "000001.SZ" {
|
if block_exit && *symbol == "000001.SZ" {
|
||||||
snapshot.day_open = snapshot.lower_limit;
|
snapshot.day_open = snapshot.lower_limit;
|
||||||
snapshot.open = snapshot.lower_limit;
|
snapshot.open = snapshot.lower_limit;
|
||||||
@@ -8520,7 +8520,7 @@ mod tests {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| {
|
.map(|symbol| {
|
||||||
let mut candidate = limit_test_candidate(true, true);
|
let mut candidate = limit_test_candidate(true, true);
|
||||||
candidate.symbol = (*symbol).to_string();
|
candidate.symbol = (*symbol).into();
|
||||||
candidate
|
candidate
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -10479,7 +10479,7 @@ mod tests {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| {
|
.map(|symbol| {
|
||||||
let mut snapshot = limit_test_snapshot();
|
let mut snapshot = limit_test_snapshot();
|
||||||
snapshot.symbol = (*symbol).to_string();
|
snapshot.symbol = (*symbol).into();
|
||||||
if *symbol == "000001.SZ" {
|
if *symbol == "000001.SZ" {
|
||||||
snapshot.day_open = 9.0;
|
snapshot.day_open = 9.0;
|
||||||
snapshot.open = 9.0;
|
snapshot.open = 9.0;
|
||||||
@@ -10497,7 +10497,7 @@ mod tests {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| {
|
.map(|symbol| {
|
||||||
let mut candidate = limit_test_candidate(true, true);
|
let mut candidate = limit_test_candidate(true, true);
|
||||||
candidate.symbol = (*symbol).to_string();
|
candidate.symbol = (*symbol).into();
|
||||||
candidate
|
candidate
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -10631,7 +10631,7 @@ mod tests {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| {
|
.map(|symbol| {
|
||||||
let mut snapshot = limit_test_snapshot();
|
let mut snapshot = limit_test_snapshot();
|
||||||
snapshot.symbol = (*symbol).to_string();
|
snapshot.symbol = (*symbol).into();
|
||||||
snapshot
|
snapshot
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -10639,7 +10639,7 @@ mod tests {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| {
|
.map(|symbol| {
|
||||||
let mut candidate = limit_test_candidate(true, true);
|
let mut candidate = limit_test_candidate(true, true);
|
||||||
candidate.symbol = (*symbol).to_string();
|
candidate.symbol = (*symbol).into();
|
||||||
candidate
|
candidate
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -10696,7 +10696,7 @@ mod tests {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| {
|
.map(|symbol| {
|
||||||
let mut snapshot = limit_test_snapshot();
|
let mut snapshot = limit_test_snapshot();
|
||||||
snapshot.symbol = (*symbol).to_string();
|
snapshot.symbol = (*symbol).into();
|
||||||
snapshot
|
snapshot
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -10704,7 +10704,7 @@ mod tests {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| {
|
.map(|symbol| {
|
||||||
let mut candidate = limit_test_candidate(true, true);
|
let mut candidate = limit_test_candidate(true, true);
|
||||||
candidate.symbol = (*symbol).to_string();
|
candidate.symbol = (*symbol).into();
|
||||||
candidate
|
candidate
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -10834,7 +10834,7 @@ mod tests {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| {
|
.map(|symbol| {
|
||||||
let mut snapshot = limit_test_snapshot();
|
let mut snapshot = limit_test_snapshot();
|
||||||
snapshot.symbol = (*symbol).to_string();
|
snapshot.symbol = (*symbol).into();
|
||||||
snapshot
|
snapshot
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -10842,7 +10842,7 @@ mod tests {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| {
|
.map(|symbol| {
|
||||||
let mut candidate = limit_test_candidate(true, true);
|
let mut candidate = limit_test_candidate(true, true);
|
||||||
candidate.symbol = (*symbol).to_string();
|
candidate.symbol = (*symbol).into();
|
||||||
candidate
|
candidate
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -11623,7 +11623,7 @@ mod tests {
|
|||||||
.with_volume_limit(false)
|
.with_volume_limit(false)
|
||||||
.with_liquidity_limit(false);
|
.with_liquidity_limit(false);
|
||||||
let mut snapshot = limit_test_snapshot();
|
let mut snapshot = limit_test_snapshot();
|
||||||
snapshot.symbol = symbol.to_string();
|
snapshot.symbol = symbol.into();
|
||||||
snapshot.last_price = 4.21;
|
snapshot.last_price = 4.21;
|
||||||
snapshot.close = 4.21;
|
snapshot.close = 4.21;
|
||||||
snapshot.bid1 = 4.20;
|
snapshot.bid1 = 4.20;
|
||||||
@@ -11708,7 +11708,7 @@ mod tests {
|
|||||||
.with_inactive_limit(false);
|
.with_inactive_limit(false);
|
||||||
let snapshot = DailyMarketSnapshot {
|
let snapshot = DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
timestamp: Some("2023-05-08 15:00:00".to_string()),
|
timestamp: Some("2023-05-08 15:00:00".to_string()),
|
||||||
day_open: 5.86,
|
day_open: 5.86,
|
||||||
open: 5.86,
|
open: 5.86,
|
||||||
@@ -11729,7 +11729,7 @@ mod tests {
|
|||||||
lower_limit: 5.27,
|
lower_limit: 5.27,
|
||||||
price_tick: 0.01,
|
price_tick: 0.01,
|
||||||
};
|
};
|
||||||
let quote = IntradayExecutionQuote {
|
let quote = IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 39, 59).expect("valid timestamp"),
|
timestamp: date.and_hms_opt(10, 39, 59).expect("valid timestamp"),
|
||||||
@@ -11756,7 +11756,7 @@ mod tests {
|
|||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![CandidateEligibility {
|
vec![CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
|
|||||||
@@ -47,8 +47,10 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
|||||||
data: &DataSet,
|
data: &DataSet,
|
||||||
symbols: &BTreeSet<String>,
|
symbols: &BTreeSet<String>,
|
||||||
execution_clock: Option<NaiveDateTime>,
|
execution_clock: Option<NaiveDateTime>,
|
||||||
) -> Result<Vec<pool::MarketSnapshot>, BacktestError> {
|
cumulative_conditions: bool,
|
||||||
symbols
|
) -> Result<(Vec<pool::MarketSnapshot>, Vec<String>), BacktestError> {
|
||||||
|
let mut unavailable = Vec::new();
|
||||||
|
let quotes = symbols
|
||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| {
|
.map(|symbol| {
|
||||||
let snapshot = data.market(date, symbol).ok_or_else(|| {
|
let snapshot = data.market(date, symbol).ok_or_else(|| {
|
||||||
@@ -134,11 +136,17 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
|||||||
None,
|
None,
|
||||||
calibration.as_ref(),
|
calibration.as_ref(),
|
||||||
)?;
|
)?;
|
||||||
|
let totals = if cumulative_conditions {
|
||||||
|
match data.execution_session_totals(symbol, clock) {
|
||||||
|
Ok(totals) => Some(totals),
|
||||||
|
Err(reason) => { unavailable.push(reason); None }
|
||||||
|
}
|
||||||
|
} else { None };
|
||||||
(
|
(
|
||||||
quote.last_price,
|
quote.last_price,
|
||||||
snapshot.prev_close,
|
snapshot.prev_close,
|
||||||
Some(quote.volume_delta as f64),
|
totals.map(|total| total.0),
|
||||||
Some(quote.amount_delta),
|
totals.map(|total| total.1),
|
||||||
Some(quote.bid1),
|
Some(quote.bid1),
|
||||||
Some(quote.ask1),
|
Some(quote.ask1),
|
||||||
buy,
|
buy,
|
||||||
@@ -153,13 +161,24 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
|||||||
}
|
}
|
||||||
// A daily open does not reveal the session's volume/turnover.
|
// A daily open does not reveal the session's volume/turnover.
|
||||||
let completed = self.effective_execution_price_field(date) == PriceField::Close;
|
let completed = self.effective_execution_price_field(date) == PriceField::Close;
|
||||||
|
let totals = if cumulative_conditions && !completed {
|
||||||
|
let at = execution_clock.unwrap_or_else(|| date.and_hms_opt(9,30,0).unwrap());
|
||||||
|
match data.execution_session_totals(symbol, at) {
|
||||||
|
Ok(totals) => Some(totals),
|
||||||
|
Err(reason) => { unavailable.push(reason); None }
|
||||||
|
}
|
||||||
|
} else { None };
|
||||||
|
let amount = if completed && cumulative_conditions {
|
||||||
|
data.factor(date, symbol).and_then(|row| row.extra_factors.get("amount")).copied()
|
||||||
|
.map(|value| decimal(value, "amount")).transpose()?
|
||||||
|
} else { totals.map(|total| total.1) };
|
||||||
(
|
(
|
||||||
price,
|
price,
|
||||||
snapshot.prev_close,
|
snapshot.prev_close,
|
||||||
completed.then_some(snapshot.volume as f64),
|
if completed { Some(Decimal::from(snapshot.volume)) } else { totals.map(|total| total.0) },
|
||||||
|
amount,
|
||||||
|
None,
|
||||||
None,
|
None,
|
||||||
Some(price),
|
|
||||||
Some(price),
|
|
||||||
self.snapshot_execution_price(data, snapshot, OrderSide::Buy, None)?,
|
self.snapshot_execution_price(data, snapshot, OrderSide::Buy, None)?,
|
||||||
self.snapshot_execution_price(data, snapshot, OrderSide::Sell, None)?,
|
self.snapshot_execution_price(data, snapshot, OrderSide::Sell, None)?,
|
||||||
)
|
)
|
||||||
@@ -168,8 +187,8 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
|||||||
symbol: symbol.clone(),
|
symbol: symbol.clone(),
|
||||||
last_price: decimal(price, "price")?,
|
last_price: decimal(price, "price")?,
|
||||||
prev_close: Some(decimal(prev, "prev_close")?),
|
prev_close: Some(decimal(prev, "prev_close")?),
|
||||||
volume: volume.map(|v| decimal(v, "volume")).transpose()?,
|
volume,
|
||||||
turnover: amount.map(|v| decimal(v, "amount")).transpose()?,
|
turnover: amount,
|
||||||
bid_price_1: bid.map(|v| decimal(v, "bid")).transpose()?,
|
bid_price_1: bid.map(|v| decimal(v, "bid")).transpose()?,
|
||||||
ask_price_1: ask.map(|v| decimal(v, "ask")).transpose()?,
|
ask_price_1: ask.map(|v| decimal(v, "ask")).transpose()?,
|
||||||
is_kcb: Some(instrument.board.eq_ignore_ascii_case("KSH")),
|
is_kcb: Some(instrument.board.eq_ignore_ascii_case("KSH")),
|
||||||
@@ -182,7 +201,8 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
|||||||
sell_sizing_price: Some(decimal(sell_price, "sell_price")?),
|
sell_sizing_price: Some(decimal(sell_price, "sell_price")?),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect()
|
.collect::<Result<Vec<_>, BacktestError>>()?;
|
||||||
|
Ok((quotes, unavailable))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pool_etf_fallback_reference(&self, date: NaiveDate, data: &DataSet, symbol: &str, clock: Option<NaiveDateTime>) -> Result<Option<crate::etf_execution::EtfFallbackReference>, BacktestError> {
|
fn pool_etf_fallback_reference(&self, date: NaiveDate, data: &DataSet, symbol: &str, clock: Option<NaiveDateTime>) -> Result<Option<crate::etf_execution::EtfFallbackReference>, BacktestError> {
|
||||||
@@ -328,8 +348,8 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
|||||||
fallback_references.insert(symbol.clone(), reference);
|
fallback_references.insert(symbol.clone(), reference);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let quotes =
|
let (quotes, unavailable) = self.pool_quote_inputs(date, data, "e_scope, *global_execution_cursor,
|
||||||
self.pool_quote_inputs(date, data, "e_scope, *global_execution_cursor)?;
|
crate::stock_pool_quote_facts::requires_session_totals(&contract.rule))?;
|
||||||
let positions = pool_positions(portfolio, date)?;
|
let positions = pool_positions(portfolio, date)?;
|
||||||
let execution_state = portfolio
|
let execution_state = portfolio
|
||||||
.stock_pool_execution_state(&contract.pool_id)
|
.stock_pool_execution_state(&contract.pool_id)
|
||||||
@@ -448,7 +468,11 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
|||||||
Decimal::ZERO,
|
Decimal::ZERO,
|
||||||
Some(&fee),
|
Some(&fee),
|
||||||
)
|
)
|
||||||
.map_err(BacktestError::Execution)?;
|
.map_err(|error| BacktestError::Execution(if !unavailable.is_empty()
|
||||||
|
&& (error.contains("requires volume") || error.contains("requires amount")) {
|
||||||
|
format!("{error}; {}", unavailable.join("; "))
|
||||||
|
} else { error }))?;
|
||||||
|
report.diagnostics.extend(unavailable.into_iter().map(|reason| format!("stock_pool_quote_fact_unavailable {reason}")));
|
||||||
let mut updated = execution_state
|
let mut updated = execution_state
|
||||||
.record_plan(contract.signal_date, &contract.generation, &plan)
|
.record_plan(contract.signal_date, &contract.generation, &plan)
|
||||||
.map_err(BacktestError::Execution)?;
|
.map_err(BacktestError::Execution)?;
|
||||||
|
|||||||
+167
-109
@@ -5,6 +5,7 @@ use std::sync::{Arc, OnceLock};
|
|||||||
|
|
||||||
use ahash::AHashMap;
|
use ahash::AHashMap;
|
||||||
use chrono::{NaiveDate, NaiveDateTime};
|
use chrono::{NaiveDate, NaiveDateTime};
|
||||||
|
use compact_str::CompactString;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@@ -122,7 +123,7 @@ pub enum PriceField {
|
|||||||
pub struct DailyMarketSnapshot {
|
pub struct DailyMarketSnapshot {
|
||||||
#[serde(with = "date_format")]
|
#[serde(with = "date_format")]
|
||||||
pub date: NaiveDate,
|
pub date: NaiveDate,
|
||||||
pub symbol: String,
|
pub symbol: CompactString,
|
||||||
pub timestamp: Option<String>,
|
pub timestamp: Option<String>,
|
||||||
pub day_open: f64,
|
pub day_open: f64,
|
||||||
pub open: f64,
|
pub open: f64,
|
||||||
@@ -199,13 +200,13 @@ impl DailyMarketSnapshot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type NumericFactorMap = BTreeMap<Cow<'static, str>, f64>;
|
pub use crate::numeric_factors::NumericFactorMap;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct DailyFactorSnapshot {
|
pub struct DailyFactorSnapshot {
|
||||||
#[serde(with = "date_format")]
|
#[serde(with = "date_format")]
|
||||||
pub date: NaiveDate,
|
pub date: NaiveDate,
|
||||||
pub symbol: String,
|
pub symbol: CompactString,
|
||||||
pub market_cap_bn: f64,
|
pub market_cap_bn: f64,
|
||||||
pub free_float_cap_bn: f64,
|
pub free_float_cap_bn: f64,
|
||||||
pub pe_ttm: f64,
|
pub pe_ttm: f64,
|
||||||
@@ -232,7 +233,7 @@ pub struct BenchmarkSnapshot {
|
|||||||
pub struct CandidateEligibility {
|
pub struct CandidateEligibility {
|
||||||
#[serde(with = "date_format")]
|
#[serde(with = "date_format")]
|
||||||
pub date: NaiveDate,
|
pub date: NaiveDate,
|
||||||
pub symbol: String,
|
pub symbol: CompactString,
|
||||||
pub is_st: bool,
|
pub is_st: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub is_star_st: bool,
|
pub is_star_st: bool,
|
||||||
@@ -283,6 +284,8 @@ pub struct CorporateAction {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct IntradayExecutionQuote {
|
pub struct IntradayExecutionQuote {
|
||||||
|
#[serde(default)]
|
||||||
|
pub observation_kind: QuoteObservationKind,
|
||||||
#[serde(with = "date_format")]
|
#[serde(with = "date_format")]
|
||||||
pub date: NaiveDate,
|
pub date: NaiveDate,
|
||||||
pub symbol: String,
|
pub symbol: String,
|
||||||
@@ -300,6 +303,14 @@ pub struct IntradayExecutionQuote {
|
|||||||
pub trading_phase: Option<String>,
|
pub trading_phase: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum QuoteObservationKind {
|
||||||
|
#[default]
|
||||||
|
Unspecified,
|
||||||
|
MinuteBar,
|
||||||
|
}
|
||||||
|
|
||||||
/// Sparse same-day fields layered onto an already-built immutable daily panel.
|
/// Sparse same-day fields layered onto an already-built immutable daily panel.
|
||||||
///
|
///
|
||||||
/// These fields do not participate in daily price series, adjustment series,
|
/// These fields do not participate in daily price series, adjustment series,
|
||||||
@@ -1204,7 +1215,7 @@ impl SymbolPriceSeries {
|
|||||||
fn snapshot_at(&self, index: usize) -> DailyMarketSnapshot {
|
fn snapshot_at(&self, index: usize) -> DailyMarketSnapshot {
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: self.dates[index],
|
date: self.dates[index],
|
||||||
symbol: self.symbol.clone(),
|
symbol: self.symbol.as_str().into(),
|
||||||
timestamp: self.timestamps[index].clone(),
|
timestamp: self.timestamps[index].clone(),
|
||||||
day_open: self.day_opens[index],
|
day_open: self.day_opens[index],
|
||||||
open: self.opens[index],
|
open: self.opens[index],
|
||||||
@@ -1406,6 +1417,7 @@ pub struct DataSet {
|
|||||||
corporate_actions_by_date: Arc<BTreeMap<NaiveDate, Vec<CorporateAction>>>,
|
corporate_actions_by_date: Arc<BTreeMap<NaiveDate, Vec<CorporateAction>>>,
|
||||||
execution_quotes_by_date: Arc<HashMap<NaiveDate, HashMap<String, Vec<IntradayExecutionQuote>>>>,
|
execution_quotes_by_date: Arc<HashMap<NaiveDate, HashMap<String, Vec<IntradayExecutionQuote>>>>,
|
||||||
execution_quote_dates: Arc<Vec<NaiveDate>>,
|
execution_quote_dates: Arc<Vec<NaiveDate>>,
|
||||||
|
condition_totals: Arc<std::sync::Mutex<crate::stock_pool_quote_facts::SessionTotalsCache>>,
|
||||||
order_book_depth_index: Arc<HashMap<(NaiveDate, String), Vec<IntradayOrderBookDepthLevel>>>,
|
order_book_depth_index: Arc<HashMap<(NaiveDate, String), Vec<IntradayOrderBookDepthLevel>>>,
|
||||||
benchmark_by_date: Arc<BTreeMap<NaiveDate, BenchmarkSnapshot>>,
|
benchmark_by_date: Arc<BTreeMap<NaiveDate, BenchmarkSnapshot>>,
|
||||||
market_series_by_symbol: Arc<AHashMap<String, Arc<SymbolPriceSeries>>>,
|
market_series_by_symbol: Arc<AHashMap<String, Arc<SymbolPriceSeries>>>,
|
||||||
@@ -1574,48 +1586,15 @@ impl DataSet {
|
|||||||
benchmark_by_date: BTreeMap::new(),
|
benchmark_by_date: BTreeMap::new(),
|
||||||
corporate_actions_by_date: BTreeMap::new(),
|
corporate_actions_by_date: BTreeMap::new(),
|
||||||
};
|
};
|
||||||
for mut bundle in bundles {
|
// Indexed collection retains chronological error precedence while each
|
||||||
|
// worker validates and normalizes only its owned day buffers.
|
||||||
|
let prepared = bundles
|
||||||
|
.into_par_iter()
|
||||||
|
.map(normalize_daily_snapshot_bundle)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for bundle in prepared {
|
||||||
|
let bundle = bundle?;
|
||||||
let date = bundle.date;
|
let date = bundle.date;
|
||||||
if bundle.benchmark.date != date {
|
|
||||||
return Err(DataSetError::InvalidDailyBundleComponentDate {
|
|
||||||
kind: "benchmark",
|
|
||||||
bundle_date: date,
|
|
||||||
row_date: bundle.benchmark.date,
|
|
||||||
symbol: bundle.benchmark.benchmark.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
validate_daily_bundle_component_dates(
|
|
||||||
&bundle.market,
|
|
||||||
date,
|
|
||||||
"market",
|
|
||||||
|row| row.date,
|
|
||||||
|row| row.symbol.as_str(),
|
|
||||||
)?;
|
|
||||||
validate_daily_bundle_component_dates(
|
|
||||||
&bundle.factors,
|
|
||||||
date,
|
|
||||||
"factor",
|
|
||||||
|row| row.date,
|
|
||||||
|row| row.symbol.as_str(),
|
|
||||||
)?;
|
|
||||||
validate_daily_bundle_component_dates(
|
|
||||||
&bundle.candidates,
|
|
||||||
date,
|
|
||||||
"candidate",
|
|
||||||
|row| row.date,
|
|
||||||
|row| row.symbol.as_str(),
|
|
||||||
)?;
|
|
||||||
validate_daily_bundle_component_dates(
|
|
||||||
&bundle.corporate_actions,
|
|
||||||
date,
|
|
||||||
"corporate_action",
|
|
||||||
|row| row.date,
|
|
||||||
|row| row.symbol.as_str(),
|
|
||||||
)?;
|
|
||||||
sort_rows_by_symbol_if_needed(&mut bundle.market, |row| row.symbol.as_str());
|
|
||||||
bundle.factors = normalize_factor_snapshots(bundle.factors)?;
|
|
||||||
sort_rows_by_symbol_if_needed(&mut bundle.factors, |row| row.symbol.as_str());
|
|
||||||
sort_rows_by_symbol_if_needed(&mut bundle.candidates, |row| row.symbol.as_str());
|
|
||||||
if !bundle.market.is_empty() {
|
if !bundle.market.is_empty() {
|
||||||
grouped.market_by_date.insert(date, bundle.market);
|
grouped.market_by_date.insert(date, bundle.market);
|
||||||
}
|
}
|
||||||
@@ -1940,6 +1919,7 @@ impl DataSet {
|
|||||||
candidate_row_positions_by_date: Arc::new(candidate_row_positions_by_date),
|
candidate_row_positions_by_date: Arc::new(candidate_row_positions_by_date),
|
||||||
corporate_actions_by_date: Arc::new(corporate_actions_by_date),
|
corporate_actions_by_date: Arc::new(corporate_actions_by_date),
|
||||||
execution_quotes_by_date: Arc::new(execution_quotes_by_date),
|
execution_quotes_by_date: Arc::new(execution_quotes_by_date),
|
||||||
|
condition_totals: Arc::new(std::sync::Mutex::new(Default::default())),
|
||||||
execution_quote_dates: Arc::new(execution_quote_dates),
|
execution_quote_dates: Arc::new(execution_quote_dates),
|
||||||
order_book_depth_index: Arc::new(order_book_depth_index),
|
order_book_depth_index: Arc::new(order_book_depth_index),
|
||||||
benchmark_by_date: Arc::new(benchmark_by_date),
|
benchmark_by_date: Arc::new(benchmark_by_date),
|
||||||
@@ -2278,6 +2258,15 @@ impl DataSet {
|
|||||||
.unwrap_or(&[])
|
.unwrap_or(&[])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn execution_session_totals(&self, symbol: &str, at: NaiveDateTime) -> Result<(rust_decimal::Decimal, rust_decimal::Decimal), String> {
|
||||||
|
let mut cache = self.condition_totals.lock().map_err(|_| "stock_pool_session_prefix_cache_poisoned")?;
|
||||||
|
if cache.date != Some(at.date()) {
|
||||||
|
cache.date = Some(at.date());
|
||||||
|
cache.symbols.clear();
|
||||||
|
}
|
||||||
|
cache.symbols.entry(symbol.into()).or_insert_with(|| crate::stock_pool_quote_facts::MinutePrefix::build(at.date(), symbol, self.execution_quotes_on(at.date(), symbol))).at(at)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn has_execution_quotes_on_date(&self, date: NaiveDate) -> bool {
|
pub fn has_execution_quotes_on_date(&self, date: NaiveDate) -> bool {
|
||||||
self.execution_quotes_by_date
|
self.execution_quotes_by_date
|
||||||
.get(&date)
|
.get(&date)
|
||||||
@@ -2450,6 +2439,7 @@ impl DataSet {
|
|||||||
/// Replaces the run-local execution quote layer without touching the
|
/// Replaces the run-local execution quote layer without touching the
|
||||||
/// immutable daily panel.
|
/// immutable daily panel.
|
||||||
pub fn replace_execution_quotes(&mut self, quotes: Vec<IntradayExecutionQuote>) -> usize {
|
pub fn replace_execution_quotes(&mut self, quotes: Vec<IntradayExecutionQuote>) -> usize {
|
||||||
|
self.condition_totals = Arc::new(std::sync::Mutex::new(Default::default()));
|
||||||
let execution_quotes_by_date = build_execution_quote_index(quotes);
|
let execution_quotes_by_date = build_execution_quote_index(quotes);
|
||||||
let quote_count = execution_quotes_by_date
|
let quote_count = execution_quotes_by_date
|
||||||
.values()
|
.values()
|
||||||
@@ -2465,6 +2455,7 @@ impl DataSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_execution_quotes(&mut self, quotes: Vec<IntradayExecutionQuote>) -> usize {
|
pub fn add_execution_quotes(&mut self, quotes: Vec<IntradayExecutionQuote>) -> usize {
|
||||||
|
self.condition_totals = Arc::new(std::sync::Mutex::new(Default::default()));
|
||||||
let mut grouped = HashMap::<NaiveDate, HashMap<String, Vec<IntradayExecutionQuote>>>::new();
|
let mut grouped = HashMap::<NaiveDate, HashMap<String, Vec<IntradayExecutionQuote>>>::new();
|
||||||
for quote in quotes {
|
for quote in quotes {
|
||||||
grouped
|
grouped
|
||||||
@@ -2565,6 +2556,7 @@ impl DataSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_execution_quotes_on_date(&mut self, date: NaiveDate) -> usize {
|
pub fn remove_execution_quotes_on_date(&mut self, date: NaiveDate) -> usize {
|
||||||
|
self.condition_totals = Arc::new(std::sync::Mutex::new(Default::default()));
|
||||||
let removed = Arc::make_mut(&mut self.execution_quotes_by_date).remove(&date);
|
let removed = Arc::make_mut(&mut self.execution_quotes_by_date).remove(&date);
|
||||||
let Some(rows_by_symbol) = removed else {
|
let Some(rows_by_symbol) = removed else {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -2577,6 +2569,7 @@ impl DataSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn release_execution_quotes_on_date(&mut self, date: NaiveDate) -> usize {
|
pub fn release_execution_quotes_on_date(&mut self, date: NaiveDate) -> usize {
|
||||||
|
self.condition_totals = Arc::new(std::sync::Mutex::new(Default::default()));
|
||||||
let row_count = self
|
let row_count = self
|
||||||
.execution_quotes_by_date
|
.execution_quotes_by_date
|
||||||
.get(&date)
|
.get(&date)
|
||||||
@@ -2865,7 +2858,7 @@ impl DataSet {
|
|||||||
.filter_map(|snapshot| {
|
.filter_map(|snapshot| {
|
||||||
factor_numeric_value(snapshot, &field).map(|value| FactorValue {
|
factor_numeric_value(snapshot, &field).map(|value| FactorValue {
|
||||||
date: snapshot.date,
|
date: snapshot.date,
|
||||||
symbol: snapshot.symbol.clone(),
|
symbol: snapshot.symbol.to_string(),
|
||||||
field: field.clone(),
|
field: field.clone(),
|
||||||
value,
|
value,
|
||||||
})
|
})
|
||||||
@@ -2959,7 +2952,7 @@ impl DataSet {
|
|||||||
.or_else(|| row.extra_factors.get("margin_all"))
|
.or_else(|| row.extra_factors.get("margin_all"))
|
||||||
.is_some_and(|value| *value > 0.0)
|
.is_some_and(|value| *value > 0.0)
|
||||||
})
|
})
|
||||||
.map(|row| row.symbol.clone())
|
.map(|row| row.symbol.to_string())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
@@ -3051,7 +3044,7 @@ impl DataSet {
|
|||||||
if snapshot.prev_close.is_finite() && snapshot.prev_close > 0.0 {
|
if snapshot.prev_close.is_finite() && snapshot.prev_close > 0.0 {
|
||||||
Some(FactorValue {
|
Some(FactorValue {
|
||||||
date: snapshot.date,
|
date: snapshot.date,
|
||||||
symbol: snapshot.symbol.clone(),
|
symbol: snapshot.symbol.to_string(),
|
||||||
field: "price_change_rate".to_string(),
|
field: "price_change_rate".to_string(),
|
||||||
value: snapshot.close / snapshot.prev_close - 1.0,
|
value: snapshot.close / snapshot.prev_close - 1.0,
|
||||||
})
|
})
|
||||||
@@ -3173,7 +3166,7 @@ impl DataSet {
|
|||||||
if let Some(value) = factor_numeric_value(snapshot, field) {
|
if let Some(value) = factor_numeric_value(snapshot, field) {
|
||||||
return Some(FactorValue {
|
return Some(FactorValue {
|
||||||
date: *factor_date,
|
date: *factor_date,
|
||||||
symbol: snapshot.symbol.clone(),
|
symbol: snapshot.symbol.to_string(),
|
||||||
field: field.clone(),
|
field: field.clone(),
|
||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
@@ -3524,7 +3517,7 @@ impl DataSet {
|
|||||||
if let Some(value) = factor_numeric_value(snapshot, field) {
|
if let Some(value) = factor_numeric_value(snapshot, field) {
|
||||||
rows.push(FactorValue {
|
rows.push(FactorValue {
|
||||||
date: snapshot.date,
|
date: snapshot.date,
|
||||||
symbol: snapshot.symbol.clone(),
|
symbol: snapshot.symbol.to_string(),
|
||||||
field: output_field.clone(),
|
field: output_field.clone(),
|
||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
@@ -4322,7 +4315,7 @@ fn daily_market_price_bar(snapshot: &DailyMarketSnapshot) -> PriceBar {
|
|||||||
PriceBar {
|
PriceBar {
|
||||||
date: snapshot.date,
|
date: snapshot.date,
|
||||||
timestamp: snapshot.timestamp.clone(),
|
timestamp: snapshot.timestamp.clone(),
|
||||||
symbol: snapshot.symbol.clone(),
|
symbol: snapshot.symbol.to_string(),
|
||||||
frequency: "1d".to_string(),
|
frequency: "1d".to_string(),
|
||||||
open: snapshot.open,
|
open: snapshot.open,
|
||||||
high: snapshot.high,
|
high: snapshot.high,
|
||||||
@@ -4383,7 +4376,7 @@ fn normalize_factor_snapshots(
|
|||||||
{
|
{
|
||||||
return Err(DataSetError::ReservedTypedFactorInExtraMap {
|
return Err(DataSetError::ReservedTypedFactorInExtraMap {
|
||||||
date: snapshot.date,
|
date: snapshot.date,
|
||||||
symbol: snapshot.symbol,
|
symbol: snapshot.symbol.to_string(),
|
||||||
field: BACKWARD_ADJUSTMENT_FACTOR_FIELD,
|
field: BACKWARD_ADJUSTMENT_FACTOR_FIELD,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -4392,7 +4385,7 @@ fn normalize_factor_snapshots(
|
|||||||
{
|
{
|
||||||
return Err(DataSetError::InvalidBackwardAdjustmentFactor {
|
return Err(DataSetError::InvalidBackwardAdjustmentFactor {
|
||||||
date: snapshot.date,
|
date: snapshot.date,
|
||||||
symbol: snapshot.symbol,
|
symbol: snapshot.symbol.to_string(),
|
||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -4428,7 +4421,7 @@ fn normalize_factor_snapshots(
|
|||||||
{
|
{
|
||||||
return Err(DataSetError::ReservedTypedFactorInExtraMap {
|
return Err(DataSetError::ReservedTypedFactorInExtraMap {
|
||||||
date: snapshot.date,
|
date: snapshot.date,
|
||||||
symbol: snapshot.symbol,
|
symbol: snapshot.symbol.to_string(),
|
||||||
field: BACKWARD_ADJUSTMENT_FACTOR_FIELD,
|
field: BACKWARD_ADJUSTMENT_FACTOR_FIELD,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -4446,6 +4439,38 @@ fn normalize_history_frequency(frequency: &str) -> Option<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize_daily_snapshot_bundle(
|
||||||
|
mut bundle: DailySnapshotBundle,
|
||||||
|
) -> Result<DailySnapshotBundle, DataSetError> {
|
||||||
|
let date = bundle.date;
|
||||||
|
if bundle.benchmark.date != date {
|
||||||
|
return Err(DataSetError::InvalidDailyBundleComponentDate {
|
||||||
|
kind: "benchmark",
|
||||||
|
bundle_date: date,
|
||||||
|
row_date: bundle.benchmark.date,
|
||||||
|
symbol: bundle.benchmark.benchmark.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
validate_daily_bundle_component_dates(
|
||||||
|
&bundle.market, date, "market", |row| row.date, |row| row.symbol.as_str(),
|
||||||
|
)?;
|
||||||
|
validate_daily_bundle_component_dates(
|
||||||
|
&bundle.factors, date, "factor", |row| row.date, |row| row.symbol.as_str(),
|
||||||
|
)?;
|
||||||
|
validate_daily_bundle_component_dates(
|
||||||
|
&bundle.candidates, date, "candidate", |row| row.date, |row| row.symbol.as_str(),
|
||||||
|
)?;
|
||||||
|
validate_daily_bundle_component_dates(
|
||||||
|
&bundle.corporate_actions, date, "corporate_action", |row| row.date,
|
||||||
|
|row| row.symbol.as_str(),
|
||||||
|
)?;
|
||||||
|
sort_rows_by_symbol_if_needed(&mut bundle.market, |row| row.symbol.as_str());
|
||||||
|
bundle.factors = normalize_factor_snapshots(bundle.factors)?;
|
||||||
|
sort_rows_by_symbol_if_needed(&mut bundle.factors, |row| row.symbol.as_str());
|
||||||
|
sort_rows_by_symbol_if_needed(&mut bundle.candidates, |row| row.symbol.as_str());
|
||||||
|
Ok(bundle)
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_daily_bundle_component_dates<T, D, S>(
|
fn validate_daily_bundle_component_dates<T, D, S>(
|
||||||
rows: &[T],
|
rows: &[T],
|
||||||
bundle_date: NaiveDate,
|
bundle_date: NaiveDate,
|
||||||
@@ -4510,21 +4535,21 @@ fn build_symbol_id_index(
|
|||||||
for rows in market_by_date.values() {
|
for rows in market_by_date.values() {
|
||||||
for row in rows {
|
for row in rows {
|
||||||
if !symbols.contains(row.symbol.as_str()) {
|
if !symbols.contains(row.symbol.as_str()) {
|
||||||
symbols.insert(row.symbol.clone());
|
symbols.insert(row.symbol.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for rows in factor_by_date.values() {
|
for rows in factor_by_date.values() {
|
||||||
for row in rows {
|
for row in rows {
|
||||||
if !symbols.contains(row.symbol.as_str()) {
|
if !symbols.contains(row.symbol.as_str()) {
|
||||||
symbols.insert(row.symbol.clone());
|
symbols.insert(row.symbol.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for rows in candidate_by_date.values() {
|
for rows in candidate_by_date.values() {
|
||||||
for row in rows {
|
for row in rows {
|
||||||
if !symbols.contains(row.symbol.as_str()) {
|
if !symbols.contains(row.symbol.as_str()) {
|
||||||
symbols.insert(row.symbol.clone());
|
symbols.insert(row.symbol.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4942,7 +4967,7 @@ fn build_fundamental_universe_for_date(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
rows.push(EligibleUniverseSnapshot {
|
rows.push(EligibleUniverseSnapshot {
|
||||||
symbol: factor.symbol.clone(),
|
symbol: factor.symbol.to_string(),
|
||||||
market_cap_bn,
|
market_cap_bn,
|
||||||
free_float_cap_bn: decision_free_float_cap_bn(factor),
|
free_float_cap_bn: decision_free_float_cap_bn(factor),
|
||||||
});
|
});
|
||||||
@@ -5012,7 +5037,7 @@ fn build_eligible_universe_for_date_from_factors(
|
|||||||
date,
|
date,
|
||||||
candidate,
|
candidate,
|
||||||
market,
|
market,
|
||||||
instruments.get(&factor.symbol),
|
instruments.get(factor.symbol.as_str()),
|
||||||
risk_config,
|
risk_config,
|
||||||
)
|
)
|
||||||
.is_some()
|
.is_some()
|
||||||
@@ -5025,7 +5050,7 @@ fn build_eligible_universe_for_date_from_factors(
|
|||||||
}
|
}
|
||||||
let free_float_cap_bn = decision_free_float_cap_bn(factor);
|
let free_float_cap_bn = decision_free_float_cap_bn(factor);
|
||||||
rows.push(EligibleUniverseSnapshot {
|
rows.push(EligibleUniverseSnapshot {
|
||||||
symbol: factor.symbol.clone(),
|
symbol: factor.symbol.to_string(),
|
||||||
market_cap_bn,
|
market_cap_bn,
|
||||||
free_float_cap_bn,
|
free_float_cap_bn,
|
||||||
});
|
});
|
||||||
@@ -5042,7 +5067,7 @@ fn build_eligible_universe_for_date_from_factors(
|
|||||||
pub(crate) fn missing_candidate_risk_state(date: NaiveDate, symbol: &str) -> CandidateEligibility {
|
pub(crate) fn missing_candidate_risk_state(date: NaiveDate, symbol: &str) -> CandidateEligibility {
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -5070,7 +5095,7 @@ mod tests {
|
|||||||
fn market_row(date: &str, prev_close: f64, volume: u64) -> DailyMarketSnapshot {
|
fn market_row(date: &str, prev_close: f64, volume: u64) -> DailyMarketSnapshot {
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: NaiveDate::parse_from_str(date, "%Y-%m-%d").unwrap(),
|
date: NaiveDate::parse_from_str(date, "%Y-%m-%d").unwrap(),
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: None,
|
timestamp: None,
|
||||||
day_open: prev_close,
|
day_open: prev_close,
|
||||||
open: prev_close,
|
open: prev_close,
|
||||||
@@ -5157,7 +5182,7 @@ mod tests {
|
|||||||
&run_data.execution_quote_dates
|
&run_data.execution_quote_dates
|
||||||
));
|
));
|
||||||
|
|
||||||
run_data.add_execution_quotes(vec![IntradayExecutionQuote {
|
run_data.add_execution_quotes(vec![IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
timestamp: NaiveDateTime::parse_from_str("2025-01-02 10:18:00", "%Y-%m-%d %H:%M:%S")
|
timestamp: NaiveDateTime::parse_from_str("2025-01-02 10:18:00", "%Y-%m-%d %H:%M:%S")
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
@@ -5300,7 +5325,7 @@ mod tests {
|
|||||||
vec![benchmark_row("2025-01-02", 12.0)],
|
vec![benchmark_row("2025-01-02", 12.0)],
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let quote = IntradayExecutionQuote {
|
let quote = IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -5352,10 +5377,10 @@ mod tests {
|
|||||||
for (symbol_index, symbol) in symbols.into_iter().enumerate().rev() {
|
for (symbol_index, symbol) in symbols.into_iter().enumerate().rev() {
|
||||||
let close = 10.0 + date_index as f64 + symbol_index as f64;
|
let close = 10.0 + date_index as f64 + symbol_index as f64;
|
||||||
let mut market_row = market_row(&date_text, close, 1_000_000);
|
let mut market_row = market_row(&date_text, close, 1_000_000);
|
||||||
market_row.symbol = symbol.to_string();
|
market_row.symbol = symbol.into();
|
||||||
let factor_row = DailyFactorSnapshot {
|
let factor_row = DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
market_cap_bn: 100.0 + close,
|
market_cap_bn: 100.0 + close,
|
||||||
free_float_cap_bn: 80.0 + close,
|
free_float_cap_bn: 80.0 + close,
|
||||||
pe_ttm: 0.0,
|
pe_ttm: 0.0,
|
||||||
@@ -5366,7 +5391,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let candidate_row = CandidateEligibility {
|
let candidate_row = CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -5402,7 +5427,7 @@ mod tests {
|
|||||||
successor_cash: None,
|
successor_cash: None,
|
||||||
};
|
};
|
||||||
corporate_actions.push(corporate_action.clone());
|
corporate_actions.push(corporate_action.clone());
|
||||||
execution_quotes.push(IntradayExecutionQuote {
|
execution_quotes.push(IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: symbols[0].to_string(),
|
symbol: symbols[0].to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
|
timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
|
||||||
@@ -5516,6 +5541,39 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parallel_daily_bundle_validation_keeps_earliest_error_and_component_order() {
|
||||||
|
let bundles = || (2..30).rev().map(|day| {
|
||||||
|
let date = NaiveDate::from_ymd_opt(2025, 1, day).unwrap();
|
||||||
|
let mut benchmark = benchmark_row("2025-01-01", 20.0);
|
||||||
|
benchmark.date = date;
|
||||||
|
DailySnapshotBundle {
|
||||||
|
date, benchmark,
|
||||||
|
market: vec![market_row("2025-01-01", 10.0, 100)],
|
||||||
|
factors: Vec::new(), candidates: Vec::new(), corporate_actions: Vec::new(),
|
||||||
|
}
|
||||||
|
}).collect::<Vec<_>>();
|
||||||
|
for threads in [1, 2, 8] {
|
||||||
|
let pool = rayon::ThreadPoolBuilder::new().num_threads(threads).build().unwrap();
|
||||||
|
for _ in 0..4 {
|
||||||
|
let result = pool.install(|| DataSet::from_daily_bundles_with_execution_quotes(
|
||||||
|
Vec::new(), bundles(), Vec::new(),
|
||||||
|
));
|
||||||
|
assert!(matches!(result, Err(DataSetError::InvalidDailyBundleComponentDate {
|
||||||
|
kind: "market", bundle_date, ..
|
||||||
|
}) if bundle_date == NaiveDate::from_ymd_opt(2025, 1, 2).unwrap()));
|
||||||
|
}
|
||||||
|
let mut values = bundles();
|
||||||
|
values.last_mut().unwrap().benchmark.date = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap();
|
||||||
|
let result = pool.install(|| DataSet::from_daily_bundles_with_execution_quotes(
|
||||||
|
Vec::new(), values, Vec::new(),
|
||||||
|
));
|
||||||
|
assert!(matches!(result, Err(DataSetError::InvalidDailyBundleComponentDate {
|
||||||
|
kind: "benchmark", bundle_date, ..
|
||||||
|
}) if bundle_date == NaiveDate::from_ymd_opt(2025, 1, 2).unwrap()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn direct_symbol_id_snapshot_lookups_preserve_alignment_for_sparse_rows() {
|
fn direct_symbol_id_snapshot_lookups_preserve_alignment_for_sparse_rows() {
|
||||||
let date = NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap();
|
let date = NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap();
|
||||||
@@ -5534,12 +5592,12 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let market = |symbol: &str, close: f64| {
|
let market = |symbol: &str, close: f64| {
|
||||||
let mut row = market_row("2025-01-02", close, 1_000_000);
|
let mut row = market_row("2025-01-02", close, 1_000_000);
|
||||||
row.symbol = symbol.to_string();
|
row.symbol = symbol.into();
|
||||||
row
|
row
|
||||||
};
|
};
|
||||||
let factor = |symbol: &str, market_cap_bn: f64| DailyFactorSnapshot {
|
let factor = |symbol: &str, market_cap_bn: f64| DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
market_cap_bn,
|
market_cap_bn,
|
||||||
free_float_cap_bn: market_cap_bn,
|
free_float_cap_bn: market_cap_bn,
|
||||||
pe_ttm: 0.0,
|
pe_ttm: 0.0,
|
||||||
@@ -5550,7 +5608,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let candidate = |symbol: &str| CandidateEligibility {
|
let candidate = |symbol: &str| CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -5773,7 +5831,7 @@ mod tests {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|date| DailyFactorSnapshot {
|
.map(|date| DailyFactorSnapshot {
|
||||||
date: *date,
|
date: *date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 10.0,
|
market_cap_bn: 10.0,
|
||||||
free_float_cap_bn: 8.0,
|
free_float_cap_bn: 8.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
@@ -5977,7 +6035,7 @@ mod tests {
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, symbol)| {
|
.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.symbol = symbol.clone().into();
|
||||||
row
|
row
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -5986,7 +6044,7 @@ mod tests {
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, symbol)| DailyFactorSnapshot {
|
.map(|(index, symbol)| DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.clone(),
|
symbol: symbol.clone().into(),
|
||||||
market_cap_bn: 10.0 + index as f64 / 1000.0,
|
market_cap_bn: 10.0 + index as f64 / 1000.0,
|
||||||
free_float_cap_bn: 8.0,
|
free_float_cap_bn: 8.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
@@ -6000,7 +6058,7 @@ mod tests {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|symbol| CandidateEligibility {
|
.map(|symbol| CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.clone(),
|
symbol: symbol.clone().into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -6113,7 +6171,7 @@ mod tests {
|
|||||||
vec![benchmark_row("2025-01-02", 12.0)],
|
vec![benchmark_row("2025-01-02", 12.0)],
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let quote = |symbol: &str, time: &str| IntradayExecutionQuote {
|
let quote = |symbol: &str, time: &str| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
timestamp: NaiveDateTime::parse_from_str(
|
timestamp: NaiveDateTime::parse_from_str(
|
||||||
&format!("2025-01-02 {time}"),
|
&format!("2025-01-02 {time}"),
|
||||||
@@ -6192,7 +6250,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn shared_execution_quote_release_does_not_clone_the_base_map() {
|
fn shared_execution_quote_release_does_not_clone_the_base_map() {
|
||||||
let date = NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap();
|
let date = NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap();
|
||||||
let quote = IntradayExecutionQuote {
|
let quote = IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
timestamp: NaiveDateTime::parse_from_str("2025-01-02 10:18:00", "%Y-%m-%d %H:%M:%S")
|
timestamp: NaiveDateTime::parse_from_str("2025-01-02 10:18:00", "%Y-%m-%d %H:%M:%S")
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
@@ -6289,14 +6347,14 @@ mod tests {
|
|||||||
fn factor_numeric_value_normalizes_fields_without_changing_aliases() {
|
fn factor_numeric_value_normalizes_fields_without_changing_aliases() {
|
||||||
let snapshot = DailyFactorSnapshot {
|
let snapshot = DailyFactorSnapshot {
|
||||||
date: NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap(),
|
date: NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap(),
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 12.5,
|
market_cap_bn: 12.5,
|
||||||
free_float_cap_bn: 8.0,
|
free_float_cap_bn: 8.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: None,
|
turnover_ratio: None,
|
||||||
effective_turnover_ratio: None,
|
effective_turnover_ratio: None,
|
||||||
adjustment_factor_backward1: Some(1.25),
|
adjustment_factor_backward1: Some(1.25),
|
||||||
extra_factors: BTreeMap::from([("custom_factor".into(), 3.5)]),
|
extra_factors: From::from([("custom_factor".into(), 3.5)]),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(factor_numeric_value(&snapshot, " MARKET_CAP "), Some(12.5));
|
assert_eq!(factor_numeric_value(&snapshot, " MARKET_CAP "), Some(12.5));
|
||||||
@@ -6312,14 +6370,14 @@ mod tests {
|
|||||||
let date = NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap();
|
let date = NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap();
|
||||||
let clean = normalize_factor_snapshots(vec![DailyFactorSnapshot {
|
let clean = normalize_factor_snapshots(vec![DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 1.0,
|
market_cap_bn: 1.0,
|
||||||
free_float_cap_bn: 1.0,
|
free_float_cap_bn: 1.0,
|
||||||
pe_ttm: 1.0,
|
pe_ttm: 1.0,
|
||||||
turnover_ratio: None,
|
turnover_ratio: None,
|
||||||
effective_turnover_ratio: None,
|
effective_turnover_ratio: None,
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::from([(Cow::Borrowed("amount"), 10.0)]),
|
extra_factors: From::from([(Cow::Borrowed("amount"), 10.0)]),
|
||||||
}])
|
}])
|
||||||
.expect("normalize clean factor snapshot");
|
.expect("normalize clean factor snapshot");
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
@@ -6329,14 +6387,14 @@ mod tests {
|
|||||||
|
|
||||||
let dirty = normalize_factor_snapshots(vec![DailyFactorSnapshot {
|
let dirty = normalize_factor_snapshots(vec![DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 1.0,
|
market_cap_bn: 1.0,
|
||||||
free_float_cap_bn: 1.0,
|
free_float_cap_bn: 1.0,
|
||||||
pe_ttm: 1.0,
|
pe_ttm: 1.0,
|
||||||
turnover_ratio: None,
|
turnover_ratio: None,
|
||||||
effective_turnover_ratio: None,
|
effective_turnover_ratio: None,
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::from([
|
extra_factors: From::from([
|
||||||
(Cow::Owned(" CUSTOM_FACTOR ".to_string()), 2.0),
|
(Cow::Owned(" CUSTOM_FACTOR ".to_string()), 2.0),
|
||||||
(Cow::Borrowed("bad_nan"), f64::NAN),
|
(Cow::Borrowed("bad_nan"), f64::NAN),
|
||||||
]),
|
]),
|
||||||
@@ -6351,7 +6409,7 @@ mod tests {
|
|||||||
let date = NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap();
|
let date = NaiveDate::parse_from_str("2025-01-02", "%Y-%m-%d").unwrap();
|
||||||
let snapshot = |adjustment_factor_backward1, extra_factors| DailyFactorSnapshot {
|
let snapshot = |adjustment_factor_backward1, extra_factors| DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 1.0,
|
market_cap_bn: 1.0,
|
||||||
free_float_cap_bn: 1.0,
|
free_float_cap_bn: 1.0,
|
||||||
pe_ttm: 1.0,
|
pe_ttm: 1.0,
|
||||||
@@ -6364,12 +6422,12 @@ mod tests {
|
|||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
normalize_factor_snapshots(vec![snapshot(
|
normalize_factor_snapshots(vec![snapshot(
|
||||||
Some(1.0),
|
Some(1.0),
|
||||||
BTreeMap::from([(Cow::Borrowed(BACKWARD_ADJUSTMENT_FACTOR_FIELD), 1.0)]),
|
BTreeMap::from([(Cow::Borrowed(BACKWARD_ADJUSTMENT_FACTOR_FIELD), 1.0)]).into(),
|
||||||
)]),
|
)]),
|
||||||
Err(DataSetError::ReservedTypedFactorInExtraMap { .. })
|
Err(DataSetError::ReservedTypedFactorInExtraMap { .. })
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
normalize_factor_snapshots(vec![snapshot(Some(0.0), BTreeMap::new())]),
|
normalize_factor_snapshots(vec![snapshot(Some(0.0), BTreeMap::new().into())]),
|
||||||
Err(DataSetError::InvalidBackwardAdjustmentFactor { .. })
|
Err(DataSetError::InvalidBackwardAdjustmentFactor { .. })
|
||||||
));
|
));
|
||||||
for field in [
|
for field in [
|
||||||
@@ -6382,7 +6440,7 @@ mod tests {
|
|||||||
matches!(
|
matches!(
|
||||||
normalize_factor_snapshots(vec![snapshot(
|
normalize_factor_snapshots(vec![snapshot(
|
||||||
typed_value,
|
typed_value,
|
||||||
BTreeMap::from([(Cow::Borrowed(field), 2.0)]),
|
BTreeMap::from([(Cow::Borrowed(field), 2.0)]).into(),
|
||||||
)]),
|
)]),
|
||||||
Err(DataSetError::ReservedTypedFactorInExtraMap { .. })
|
Err(DataSetError::ReservedTypedFactorInExtraMap { .. })
|
||||||
),
|
),
|
||||||
@@ -6478,14 +6536,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: *date,
|
date: *date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 10.0,
|
market_cap_bn: 10.0,
|
||||||
free_float_cap_bn: 8.0,
|
free_float_cap_bn: 8.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: None,
|
turnover_ratio: None,
|
||||||
effective_turnover_ratio: None,
|
effective_turnover_ratio: None,
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors,
|
extra_factors: extra_factors.into(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
@@ -6537,14 +6595,14 @@ mod tests {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|date| DailyFactorSnapshot {
|
.map(|date| DailyFactorSnapshot {
|
||||||
date: *date,
|
date: *date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 10.0,
|
market_cap_bn: 10.0,
|
||||||
free_float_cap_bn: 8.0,
|
free_float_cap_bn: 8.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: None,
|
turnover_ratio: None,
|
||||||
effective_turnover_ratio: None,
|
effective_turnover_ratio: None,
|
||||||
adjustment_factor_backward1: Some(1.0),
|
adjustment_factor_backward1: Some(1.0),
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
@@ -6831,14 +6889,14 @@ mod tests {
|
|||||||
.zip(factors)
|
.zip(factors)
|
||||||
.map(|(date, factor)| DailyFactorSnapshot {
|
.map(|(date, factor)| DailyFactorSnapshot {
|
||||||
date: *date,
|
date: *date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 10.0,
|
market_cap_bn: 10.0,
|
||||||
free_float_cap_bn: 8.0,
|
free_float_cap_bn: 8.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: None,
|
turnover_ratio: None,
|
||||||
effective_turnover_ratio: None,
|
effective_turnover_ratio: None,
|
||||||
adjustment_factor_backward1: Some(factor),
|
adjustment_factor_backward1: Some(factor),
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
@@ -6929,14 +6987,14 @@ mod tests {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|date| DailyFactorSnapshot {
|
.map(|date| DailyFactorSnapshot {
|
||||||
date: *date,
|
date: *date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 10.0,
|
market_cap_bn: 10.0,
|
||||||
free_float_cap_bn: 8.0,
|
free_float_cap_bn: 8.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: None,
|
turnover_ratio: None,
|
||||||
effective_turnover_ratio: None,
|
effective_turnover_ratio: None,
|
||||||
adjustment_factor_backward1: (*date != dates[3]).then_some(1.0),
|
adjustment_factor_backward1: (*date != dates[3]).then_some(1.0),
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
@@ -7008,7 +7066,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let market = |symbol: &str, prev_close: f64, close: f64| DailyMarketSnapshot {
|
let market = |symbol: &str, prev_close: f64, close: f64| DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
timestamp: Some("2025-01-06 10:18:00".to_string()),
|
timestamp: Some("2025-01-06 10:18:00".to_string()),
|
||||||
day_open: prev_close,
|
day_open: prev_close,
|
||||||
open: prev_close,
|
open: prev_close,
|
||||||
@@ -7032,18 +7090,18 @@ mod tests {
|
|||||||
let factor =
|
let factor =
|
||||||
|symbol: &str, market_cap_bn: f64, free_float_cap_bn: f64| DailyFactorSnapshot {
|
|symbol: &str, market_cap_bn: f64, free_float_cap_bn: f64| DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
market_cap_bn,
|
market_cap_bn,
|
||||||
free_float_cap_bn,
|
free_float_cap_bn,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
};
|
};
|
||||||
let candidate = |symbol: &str| CandidateEligibility {
|
let candidate = |symbol: &str| CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -7108,7 +7166,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
vec![DailyMarketSnapshot {
|
vec![DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
timestamp: Some("2025-01-06 10:18:00".to_string()),
|
timestamp: Some("2025-01-06 10:18:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -7131,14 +7189,14 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
vec![DailyFactorSnapshot {
|
vec![DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
market_cap_bn: 10.0,
|
market_cap_bn: 10.0,
|
||||||
free_float_cap_bn: 9.0,
|
free_float_cap_bn: 9.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![BenchmarkSnapshot {
|
vec![BenchmarkSnapshot {
|
||||||
@@ -7193,7 +7251,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
vec![DailyMarketSnapshot {
|
vec![DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
timestamp: Some("2025-01-06 10:18:00".to_string()),
|
timestamp: Some("2025-01-06 10:18:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -7216,18 +7274,18 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
vec![DailyFactorSnapshot {
|
vec![DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
market_cap_bn: 10.0,
|
market_cap_bn: 10.0,
|
||||||
free_float_cap_bn: 9.0,
|
free_float_cap_bn: 9.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
}],
|
}],
|
||||||
vec![CandidateEligibility {
|
vec![CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -7268,14 +7326,14 @@ mod tests {
|
|||||||
let date = NaiveDate::parse_from_str("2025-01-06", "%Y-%m-%d").unwrap();
|
let date = NaiveDate::parse_from_str("2025-01-06", "%Y-%m-%d").unwrap();
|
||||||
let factor = DailyFactorSnapshot {
|
let factor = DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 12.0,
|
market_cap_bn: 12.0,
|
||||||
free_float_cap_bn: 4.0,
|
free_float_cap_bn: 4.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!((decision_market_cap_bn(&factor) - 12.0).abs() < 1e-9);
|
assert!((decision_market_cap_bn(&factor) - 12.0).abs() < 1e-9);
|
||||||
|
|||||||
@@ -746,6 +746,15 @@ where
|
|||||||
if self.execution_quote_loader.is_none() {
|
if self.execution_quote_loader.is_none() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
let cumulative_conditions = decision.order_intents.iter().any(|intent| {
|
||||||
|
matches!(intent.unwrapped(), OrderIntent::StockPool { contract }
|
||||||
|
if crate::stock_pool_quote_facts::requires_session_totals(&contract.rule))
|
||||||
|
});
|
||||||
|
if cumulative_conditions && (self.broker.execution_price_field() != PriceField::Close
|
||||||
|
|| start_time.is_some() || self.broker.intraday_execution_start_time().is_some()) {
|
||||||
|
let mut symbols = execution_quote_symbols_for_decision(decision, portfolio, open_orders);
|
||||||
|
self.load_missing_execution_quotes(execution_date, None, None, &mut symbols)?;
|
||||||
|
}
|
||||||
let submission_time = start_time.or_else(|| self.broker.intraday_execution_start_time());
|
let submission_time = start_time.or_else(|| self.broker.intraday_execution_start_time());
|
||||||
let post_close_window = self.broker.post_close_execution_quote_window_for_order(
|
let post_close_window = self.broker.post_close_execution_quote_window_for_order(
|
||||||
execution_date,
|
execution_date,
|
||||||
@@ -5425,7 +5434,7 @@ mod tests {
|
|||||||
fn market(date: NaiveDate, open: f64, close: f64) -> DailyMarketSnapshot {
|
fn market(date: NaiveDate, open: f64, close: f64) -> DailyMarketSnapshot {
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: SYMBOL.to_string(),
|
symbol: SYMBOL.into(),
|
||||||
timestamp: Some(format!("{date} 15:00:00")),
|
timestamp: Some(format!("{date} 15:00:00")),
|
||||||
day_open: open,
|
day_open: open,
|
||||||
open,
|
open,
|
||||||
@@ -5479,21 +5488,21 @@ mod tests {
|
|||||||
fn factor(date: NaiveDate) -> DailyFactorSnapshot {
|
fn factor(date: NaiveDate) -> DailyFactorSnapshot {
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: SYMBOL.to_string(),
|
symbol: SYMBOL.into(),
|
||||||
market_cap_bn: 10.0,
|
market_cap_bn: 10.0,
|
||||||
free_float_cap_bn: 8.0,
|
free_float_cap_bn: 8.0,
|
||||||
pe_ttm: 12.0,
|
pe_ttm: 12.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn candidate(date: NaiveDate) -> CandidateEligibility {
|
fn candidate(date: NaiveDate) -> CandidateEligibility {
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: SYMBOL.to_string(),
|
symbol: SYMBOL.into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -5796,7 +5805,7 @@ mod tests {
|
|||||||
fn physical_on_day_rules_keep_each_actual_submission_time() {
|
fn physical_on_day_rules_keep_each_actual_submission_time() {
|
||||||
let date = d(2026, 7, 6);
|
let date = d(2026, 7, 6);
|
||||||
let quotes = vec![
|
let quotes = vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: SYMBOL.to_string(),
|
symbol: SYMBOL.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 18, 0).expect("morning timestamp"),
|
timestamp: date.and_hms_opt(10, 18, 0).expect("morning timestamp"),
|
||||||
@@ -5809,7 +5818,7 @@ mod tests {
|
|||||||
amount_delta: 110_000.0,
|
amount_delta: 110_000.0,
|
||||||
trading_phase: Some("continuous_auction".to_string()),
|
trading_phase: Some("continuous_auction".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: SYMBOL.to_string(),
|
symbol: SYMBOL.to_string(),
|
||||||
timestamp: date.and_hms_opt(10, 19, 0).expect("future timestamp"),
|
timestamp: date.and_hms_opt(10, 19, 0).expect("future timestamp"),
|
||||||
@@ -5822,7 +5831,7 @@ mod tests {
|
|||||||
amount_delta: 990_000.0,
|
amount_delta: 990_000.0,
|
||||||
trading_phase: Some("continuous_auction".to_string()),
|
trading_phase: Some("continuous_auction".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: SYMBOL.to_string(),
|
symbol: SYMBOL.to_string(),
|
||||||
timestamp: date.and_hms_opt(15, 10, 0).expect("post-close timestamp"),
|
timestamp: date.and_hms_opt(15, 10, 0).expect("post-close timestamp"),
|
||||||
@@ -5950,7 +5959,7 @@ mod tests {
|
|||||||
let closing_only = matches!(scenario,2|3);
|
let closing_only = matches!(scenario,2|3);
|
||||||
let delayed = scenario == 4;
|
let delayed = scenario == 4;
|
||||||
let date = if closing_only { d(2026, 7, 6) } else if delayed { d(2026, 6, 2) } else { d(2026, 6, 1) };
|
let date = if closing_only { d(2026, 7, 6) } else if delayed { d(2026, 6, 2) } else { d(2026, 6, 1) };
|
||||||
let quote = |hour, minute, price| IntradayExecutionQuote {
|
let quote = |hour, minute, price| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date, symbol: SYMBOL.into(), timestamp: date.and_hms_opt(hour, minute, 0).unwrap(),
|
date, symbol: SYMBOL.into(), timestamp: date.and_hms_opt(hour, minute, 0).unwrap(),
|
||||||
last_price: price, bid1: price, ask1: price, bid1_volume: 10_000, ask1_volume: 10_000,
|
last_price: price, bid1: price, ask1: price, bid1_volume: 10_000, ask1_volume: 10_000,
|
||||||
volume_delta: 10_000, amount_delta: price * 10_000.0, trading_phase: None,
|
volume_delta: 10_000, amount_delta: price * 10_000.0, trading_phase: None,
|
||||||
@@ -6073,7 +6082,7 @@ mod tests {
|
|||||||
Ok(request
|
Ok(request
|
||||||
.symbols
|
.symbols
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: request.date,
|
date: request.date,
|
||||||
symbol,
|
symbol,
|
||||||
timestamp: request.date.and_hms_opt(15, 5, 0).expect("valid timestamp"),
|
timestamp: request.date.and_hms_opt(15, 5, 0).expect("valid timestamp"),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ pub mod broker;
|
|||||||
pub mod calendar;
|
pub mod calendar;
|
||||||
pub mod cost;
|
pub mod cost;
|
||||||
pub mod data;
|
pub mod data;
|
||||||
|
mod numeric_factors;
|
||||||
pub mod daily_patterns;
|
pub mod daily_patterns;
|
||||||
pub mod pattern_context;
|
pub mod pattern_context;
|
||||||
pub mod session_events;
|
pub mod session_events;
|
||||||
@@ -36,6 +37,7 @@ pub mod stock_pool_execution;
|
|||||||
pub mod stock_pool_index_policy;
|
pub mod stock_pool_index_policy;
|
||||||
pub mod stock_pool_market_cap;
|
pub mod stock_pool_market_cap;
|
||||||
pub mod stock_pool_state;
|
pub mod stock_pool_state;
|
||||||
|
pub mod stock_pool_quote_facts;
|
||||||
pub mod signal_contract;
|
pub mod signal_contract;
|
||||||
pub mod strategy_ai;
|
pub mod strategy_ai;
|
||||||
pub mod universe;
|
pub mod universe;
|
||||||
|
|||||||
@@ -0,0 +1,339 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::fmt;
|
||||||
|
use std::ops::Index;
|
||||||
|
|
||||||
|
use serde::de::{MapAccess, Visitor};
|
||||||
|
use serde::ser::SerializeMap;
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
|
/// Sorted numeric fields stored contiguously, without a tree node per snapshot.
|
||||||
|
#[derive(Clone, Default, PartialEq)]
|
||||||
|
pub struct NumericFactorMap {
|
||||||
|
entries: Vec<(Cow<'static, str>, f64)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NumericFactorMap {
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
entries: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.entries.len()
|
||||||
|
}
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.entries.is_empty()
|
||||||
|
}
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.entries.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, key: &str) -> Option<&f64> {
|
||||||
|
self.entries
|
||||||
|
.binary_search_by(|(name, _)| name.as_ref().cmp(key))
|
||||||
|
.ok()
|
||||||
|
.map(|index| &self.entries[index].1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mut(&mut self, key: &str) -> Option<&mut f64> {
|
||||||
|
self.entries
|
||||||
|
.binary_search_by(|(name, _)| name.as_ref().cmp(key))
|
||||||
|
.ok()
|
||||||
|
.map(|index| &mut self.entries[index].1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains_key(&self, key: &str) -> bool {
|
||||||
|
self.get(key).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insert(&mut self, key: Cow<'static, str>, value: f64) -> Option<f64> {
|
||||||
|
if self
|
||||||
|
.entries
|
||||||
|
.last()
|
||||||
|
.is_none_or(|(last, _)| last.as_ref() < key.as_ref())
|
||||||
|
{
|
||||||
|
self.entries.push((key, value));
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match self
|
||||||
|
.entries
|
||||||
|
.binary_search_by(|(name, _)| name.as_ref().cmp(key.as_ref()))
|
||||||
|
{
|
||||||
|
Ok(index) => Some(std::mem::replace(&mut self.entries[index].1, value)),
|
||||||
|
Err(index) => {
|
||||||
|
self.entries.insert(index, (key, value));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, key: &str) -> Option<f64> {
|
||||||
|
self.entries
|
||||||
|
.binary_search_by(|(name, _)| name.as_ref().cmp(key))
|
||||||
|
.ok()
|
||||||
|
.map(|index| self.entries.remove(index).1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn retain(&mut self, mut keep: impl FnMut(&Cow<'static, str>, &mut f64) -> bool) {
|
||||||
|
self.entries.retain_mut(|(key, value)| keep(key, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> Iter<'_> {
|
||||||
|
Iter(self.entries.iter())
|
||||||
|
}
|
||||||
|
pub fn keys(&self) -> impl DoubleEndedIterator<Item = &Cow<'static, str>> + ExactSizeIterator {
|
||||||
|
self.entries.iter().map(|(key, _)| key)
|
||||||
|
}
|
||||||
|
pub fn values(&self) -> impl DoubleEndedIterator<Item = &f64> + ExactSizeIterator {
|
||||||
|
self.entries.iter().map(|(_, value)| value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for NumericFactorMap {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_map().entries(self).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<&str> for NumericFactorMap {
|
||||||
|
type Output = f64;
|
||||||
|
fn index(&self, key: &str) -> &Self::Output {
|
||||||
|
self.get(key).expect("numeric factor key not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Iter<'a>(std::slice::Iter<'a, (Cow<'static, str>, f64)>);
|
||||||
|
impl<'a> Iterator for Iter<'a> {
|
||||||
|
type Item = (&'a Cow<'static, str>, &'a f64);
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
self.0.next().map(|(k, v)| (k, v))
|
||||||
|
}
|
||||||
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||||
|
self.0.size_hint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl DoubleEndedIterator for Iter<'_> {
|
||||||
|
fn next_back(&mut self) -> Option<Self::Item> {
|
||||||
|
self.0.next_back().map(|(k, v)| (k, v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ExactSizeIterator for Iter<'_> {}
|
||||||
|
impl<'a> IntoIterator for &'a NumericFactorMap {
|
||||||
|
type Item = (&'a Cow<'static, str>, &'a f64);
|
||||||
|
type IntoIter = Iter<'a>;
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl IntoIterator for NumericFactorMap {
|
||||||
|
type Item = (Cow<'static, str>, f64);
|
||||||
|
type IntoIter = std::vec::IntoIter<Self::Item>;
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.entries.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromIterator<(Cow<'static, str>, f64)> for NumericFactorMap {
|
||||||
|
fn from_iter<T: IntoIterator<Item = (Cow<'static, str>, f64)>>(iter: T) -> Self {
|
||||||
|
let mut entries: Vec<_> = iter.into_iter().collect();
|
||||||
|
// Stable sorting preserves last-value-wins for repeated input keys.
|
||||||
|
if !entries.windows(2).all(|pair| pair[0].0 <= pair[1].0) {
|
||||||
|
entries.sort_by(|left, right| left.0.cmp(&right.0));
|
||||||
|
}
|
||||||
|
entries.dedup_by(|later, earlier| {
|
||||||
|
if later.0 == earlier.0 {
|
||||||
|
earlier.1 = later.1;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Self { entries }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Extend<(Cow<'static, str>, f64)> for NumericFactorMap {
|
||||||
|
fn extend<T: IntoIterator<Item = (Cow<'static, str>, f64)>>(&mut self, iter: T) {
|
||||||
|
let mut incoming: Self = iter.into_iter().collect();
|
||||||
|
if incoming.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.is_empty() {
|
||||||
|
*self = incoming;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.entries.last().unwrap().0 < incoming.entries[0].0 {
|
||||||
|
self.entries.append(&mut incoming.entries);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Merge sorted sets in linear time; wide factor batches must not shift
|
||||||
|
// the existing vector once per field. Existing keys keep their identity.
|
||||||
|
let mut merged = Vec::with_capacity(self.len() + incoming.len());
|
||||||
|
let mut old = std::mem::take(&mut self.entries).into_iter().peekable();
|
||||||
|
let mut new = incoming.entries.into_iter().peekable();
|
||||||
|
while let (Some(left), Some(right)) = (old.peek(), new.peek()) {
|
||||||
|
match left.0.cmp(&right.0) {
|
||||||
|
std::cmp::Ordering::Less => merged.push(old.next().unwrap()),
|
||||||
|
std::cmp::Ordering::Greater => merged.push(new.next().unwrap()),
|
||||||
|
std::cmp::Ordering::Equal => {
|
||||||
|
let (key, _) = old.next().unwrap();
|
||||||
|
merged.push((key, new.next().unwrap().1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
merged.extend(old);
|
||||||
|
merged.extend(new);
|
||||||
|
self.entries = merged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<const N: usize> From<[(Cow<'static, str>, f64); N]> for NumericFactorMap {
|
||||||
|
fn from(entries: [(Cow<'static, str>, f64); N]) -> Self {
|
||||||
|
entries.into_iter().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<BTreeMap<Cow<'static, str>, f64>> for NumericFactorMap {
|
||||||
|
fn from(entries: BTreeMap<Cow<'static, str>, f64>) -> Self {
|
||||||
|
Self {
|
||||||
|
entries: entries.into_iter().collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for NumericFactorMap {
|
||||||
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
let mut map = serializer.serialize_map(Some(self.len()))?;
|
||||||
|
for (key, value) in self {
|
||||||
|
map.serialize_entry(key, value)?;
|
||||||
|
}
|
||||||
|
map.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'de> Deserialize<'de> for NumericFactorMap {
|
||||||
|
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||||
|
struct Fields;
|
||||||
|
impl<'de> Visitor<'de> for Fields {
|
||||||
|
type Value = NumericFactorMap;
|
||||||
|
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str("a numeric factor map")
|
||||||
|
}
|
||||||
|
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
while let Some((key, value)) = map.next_entry::<String, f64>()? {
|
||||||
|
entries.push((Cow::Owned(key), value));
|
||||||
|
}
|
||||||
|
Ok(entries.into_iter().collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deserializer.deserialize_map(Fields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn updates_order_removal_and_values_match_tree_map() {
|
||||||
|
let mut flat = NumericFactorMap::new();
|
||||||
|
let mut tree = BTreeMap::new();
|
||||||
|
let mut seed = 71_u64;
|
||||||
|
for index in 0..10000 {
|
||||||
|
seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||||
|
let key: Cow<'static, str> = Cow::Owned(format!("factor_{:04}", (seed >> 32) % 1000));
|
||||||
|
let value = (index as f64 - 5000.0) / 7.0;
|
||||||
|
if index % 11 == 0 {
|
||||||
|
assert_eq!(flat.remove(key.as_ref()), tree.remove(key.as_ref()));
|
||||||
|
} else {
|
||||||
|
assert_eq!(flat.insert(key.clone(), value), tree.insert(key, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
flat.iter().collect::<Vec<_>>(),
|
||||||
|
tree.iter().collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
flat.retain(|_, value| *value > 100.0);
|
||||||
|
tree.retain(|_, value| *value > 100.0);
|
||||||
|
assert_eq!(
|
||||||
|
flat.iter().collect::<Vec<_>>(),
|
||||||
|
tree.iter().collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
std::mem::size_of::<NumericFactorMap>(),
|
||||||
|
std::mem::size_of_val(&tree)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bulk_load_is_sorted_and_keeps_last_value_for_each_field() {
|
||||||
|
let input = vec![
|
||||||
|
(Cow::Borrowed("z"), 2.0),
|
||||||
|
(Cow::Borrowed("a"), 1.0),
|
||||||
|
(Cow::Borrowed("z"), 3.0),
|
||||||
|
(Cow::Borrowed("z"), 4.0),
|
||||||
|
];
|
||||||
|
let flat: NumericFactorMap = input.clone().into_iter().collect();
|
||||||
|
let tree: BTreeMap<_, _> = input.into_iter().collect();
|
||||||
|
assert_eq!(
|
||||||
|
flat.iter().collect::<Vec<_>>(),
|
||||||
|
tree.iter().collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
assert_eq!(flat["z"], 4.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn serialization_keeps_the_map_contract_and_precise_numbers() {
|
||||||
|
let input = [
|
||||||
|
(Cow::Borrowed("zero"), -0.0),
|
||||||
|
(Cow::Borrowed("tiny"), 1.0000000000000002),
|
||||||
|
(Cow::Borrowed("large"), 9.123456789123456e20),
|
||||||
|
];
|
||||||
|
let flat = NumericFactorMap::from(input.clone());
|
||||||
|
let tree = BTreeMap::from(input);
|
||||||
|
let json = serde_json::to_string(&flat).unwrap();
|
||||||
|
assert_eq!(json, serde_json::to_string(&tree).unwrap());
|
||||||
|
let decoded: NumericFactorMap = serde_json::from_str(&json).unwrap();
|
||||||
|
for (key, value) in &flat {
|
||||||
|
assert_eq!(value.to_bits(), decoded[key.as_ref()].to_bits());
|
||||||
|
}
|
||||||
|
let duplicate: NumericFactorMap = serde_json::from_str(r#"{"x":1,"x":2}"#).unwrap();
|
||||||
|
assert_eq!(duplicate["x"], 2.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clone_does_not_share_mutable_values() {
|
||||||
|
let original = NumericFactorMap::from([(Cow::Borrowed("signal"), 1.0)]);
|
||||||
|
let mut copy = original.clone();
|
||||||
|
*copy.get_mut("signal").unwrap() = 0.0;
|
||||||
|
copy.insert(Cow::Borrowed("other"), 2.0);
|
||||||
|
assert_eq!(original["signal"], 1.0);
|
||||||
|
assert!(!original.contains_key("other"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wide_batch_merge_matches_tree_and_preserves_old_key_ownership() {
|
||||||
|
let entries = (0..4096)
|
||||||
|
.map(|index| (Cow::Owned(format!("f_{index:05}")), index as f64))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut flat: NumericFactorMap = entries.clone().into_iter().collect();
|
||||||
|
let mut tree = BTreeMap::from_iter(entries);
|
||||||
|
flat.insert(Cow::Borrowed("shared"), -0.0);
|
||||||
|
tree.insert(Cow::Borrowed("shared"), -0.0);
|
||||||
|
let incoming = (2048..8192)
|
||||||
|
.rev()
|
||||||
|
.map(|index| (Cow::Owned(format!("f_{index:05}")), -(index as f64)))
|
||||||
|
.chain([(Cow::Owned("shared".to_owned()), 1.0)])
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
flat.extend(incoming.clone());
|
||||||
|
tree.extend(incoming);
|
||||||
|
assert_eq!(
|
||||||
|
flat.iter().collect::<Vec<_>>(),
|
||||||
|
tree.iter().collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
assert!(matches!(flat.keys().last(), Some(Cow::Borrowed("shared"))));
|
||||||
|
flat.extend([(Cow::Borrowed("zz"), f64::NAN)]);
|
||||||
|
assert!(flat["zz"].is_nan());
|
||||||
|
flat.extend(std::iter::empty());
|
||||||
|
assert_eq!(flat.len(), tree.len() + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -212,6 +212,11 @@ pub fn build_dataset_context(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn specs_in_value(value: &Value) -> Result<Vec<PatternSpec>, String> {
|
pub fn specs_in_value(value: &Value) -> Result<Vec<PatternSpec>, String> {
|
||||||
|
// A runner bundle also contains source/extract copies. Follow the same
|
||||||
|
// authoritative spec selection as the execution loader, not those copies.
|
||||||
|
if let Some(spec) = value.get("strategySpec").or_else(|| value.get("strategy_spec")) {
|
||||||
|
return specs_in_value(spec);
|
||||||
|
}
|
||||||
let mut specs = Vec::new();
|
let mut specs = Vec::new();
|
||||||
match value {
|
match value {
|
||||||
Value::String(text) => specs.extend(crate::daily_patterns::expression_specs(text)?),
|
Value::String(text) => specs.extend(crate::daily_patterns::expression_specs(text)?),
|
||||||
@@ -221,7 +226,15 @@ pub fn specs_in_value(value: &Value) -> Result<Vec<PatternSpec>, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Value::Object(items) => {
|
Value::Object(items) => {
|
||||||
for v in items.values() {
|
let typed_pool = items.get("stockPool").or_else(|| items.get("stock_pool"))
|
||||||
|
.is_some_and(Value::is_object);
|
||||||
|
for (key, v) in items {
|
||||||
|
// The executable pool already supplies decoded expressions.
|
||||||
|
// Its display/source serialization escapes those expressions
|
||||||
|
// one more time and is not another executable program.
|
||||||
|
if typed_pool && matches!(key.as_str(), "sourceCode" | "source_code") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
specs.extend(specs_in_value(v)?);
|
specs.extend(specs_in_value(v)?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,6 +261,31 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::{BenchmarkSnapshot, DailyFactorSnapshot, DailyMarketSnapshot, Instrument};
|
use crate::{BenchmarkSnapshot, DailyFactorSnapshot, DailyMarketSnapshot, Instrument};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
#[test]
|
||||||
|
fn structured_pool_conditions_are_not_rescanned_inside_serialized_source_code() {
|
||||||
|
let pattern = json!({"template":"expression","parameters":{"history_window":20},
|
||||||
|
"expression":{"kind":"operator","name":"GT","args":[{"kind":"field","name":"amount"},{"kind":"number","value":0}]}});
|
||||||
|
let expr = format!("pattern_signal({})", serde_json::to_string(&pattern.to_string()).unwrap());
|
||||||
|
let pool = json!({"schema_version":1,"pool_id":"fixture","version_id":"v1","members":[],
|
||||||
|
"allocation_policy":{},"timing_policy":{},"stop_take_policy":{},"out_of_pool_policy":"hold",
|
||||||
|
"exit_signals":[{"role":"risk_exit","when_expr":expr,"remaining_position_bps":5000,"reason":"fixture"}]});
|
||||||
|
let source = format!("stock_pool.config({pool})");
|
||||||
|
for (pool_key, source_key) in [("stockPool", "sourceCode"), ("stock_pool", "source_code")] {
|
||||||
|
let value = json!({pool_key:pool,source_key:source,"runtimeExpressions":{"trading":{"buyFilterExpr":expr}}});
|
||||||
|
assert_eq!(specs_in_value(&value).unwrap().len(), 2);
|
||||||
|
for wrapper in ["strategySpec", "strategy_spec"] {
|
||||||
|
let bundle = json!({wrapper:value,"strategy_source":{"source_code":source},
|
||||||
|
"strategy_extract":{"parameters":{"source_code":source}}});
|
||||||
|
assert_eq!(specs_in_value(&bundle).unwrap().len(), 2);
|
||||||
|
}
|
||||||
|
let mut invalid = value.clone();
|
||||||
|
invalid[pool_key]["exit_signals"][0]["when_expr"] = json!("pattern_signal(not-json)");
|
||||||
|
assert!(specs_in_value(&invalid).is_err(), "invalid actual conditions must still fail");
|
||||||
|
assert!(specs_in_value(&json!({"strategySpec":invalid})).is_err());
|
||||||
|
}
|
||||||
|
assert_eq!(specs_in_value(&json!({"sourceCode":format!("risk.stop_loss({expr})")})).unwrap().len(),1);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn normalized_rule_does_not_turn_an_omitted_window_into_explicit_null() {
|
fn normalized_rule_does_not_turn_an_omitted_window_into_explicit_null() {
|
||||||
let expression:Expr=serde_json::from_value(json!({"kind":"operator","name":"GT","args":[{"kind":"field","name":"close"},{"kind":"number","value":1}]})).unwrap();
|
let expression:Expr=serde_json::from_value(json!({"kind":"operator","name":"GT","args":[{"kind":"field","name":"close"},{"kind":"number","value":1}]})).unwrap();
|
||||||
@@ -298,7 +336,7 @@ mod tests {
|
|||||||
][n][i];
|
][n][i];
|
||||||
market.push(DailyMarketSnapshot {
|
market.push(DailyMarketSnapshot {
|
||||||
date: *d,
|
date: *d,
|
||||||
symbol: s.to_string(),
|
symbol: (*s).into(),
|
||||||
timestamp: None,
|
timestamp: None,
|
||||||
day_open: c,
|
day_open: c,
|
||||||
open: c,
|
open: c,
|
||||||
@@ -321,7 +359,7 @@ mod tests {
|
|||||||
});
|
});
|
||||||
factors.push(DailyFactorSnapshot {
|
factors.push(DailyFactorSnapshot {
|
||||||
date: *d,
|
date: *d,
|
||||||
symbol: s.to_string(),
|
symbol: (*s).into(),
|
||||||
market_cap_bn: 1.,
|
market_cap_bn: 1.,
|
||||||
free_float_cap_bn: 1.,
|
free_float_cap_bn: 1.,
|
||||||
pe_ttm: 10.,
|
pe_ttm: 10.,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1438,8 +1438,6 @@ mod tests {
|
|||||||
BenchmarkSnapshot, CandidateEligibility, DailyFactorSnapshot, DailyMarketSnapshot, DataSet,
|
BenchmarkSnapshot, CandidateEligibility, DailyFactorSnapshot, DailyMarketSnapshot, DataSet,
|
||||||
PriceField,
|
PriceField,
|
||||||
};
|
};
|
||||||
use std::collections::BTreeMap;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn cash_ledger_accumulates_micro_yuan_exactly() {
|
fn cash_ledger_accumulates_micro_yuan_exactly() {
|
||||||
let mut portfolio = PortfolioState::new(1_000_000.0);
|
let mut portfolio = PortfolioState::new(1_000_000.0);
|
||||||
@@ -1641,7 +1639,7 @@ mod tests {
|
|||||||
vec![
|
vec![
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: prev_date,
|
date: prev_date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: None,
|
timestamp: None,
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -1664,7 +1662,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: None,
|
timestamp: None,
|
||||||
day_open: 10.5,
|
day_open: 10.5,
|
||||||
open: 10.5,
|
open: 10.5,
|
||||||
@@ -1688,18 +1686,18 @@ mod tests {
|
|||||||
],
|
],
|
||||||
vec![DailyFactorSnapshot {
|
vec![DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 50.0,
|
market_cap_bn: 50.0,
|
||||||
free_float_cap_bn: 45.0,
|
free_float_cap_bn: 45.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
}],
|
}],
|
||||||
vec![CandidateEligibility {
|
vec![CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -1754,7 +1752,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
vec![DailyMarketSnapshot {
|
vec![DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: None,
|
timestamp: None,
|
||||||
day_open: 10.5,
|
day_open: 10.5,
|
||||||
open: 10.5,
|
open: 10.5,
|
||||||
@@ -1777,18 +1775,18 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
vec![DailyFactorSnapshot {
|
vec![DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 50.0,
|
market_cap_bn: 50.0,
|
||||||
free_float_cap_bn: 45.0,
|
free_float_cap_bn: 45.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
}],
|
}],
|
||||||
vec![CandidateEligibility {
|
vec![CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -1840,7 +1838,7 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
vec![DailyMarketSnapshot {
|
vec![DailyMarketSnapshot {
|
||||||
date: prev_date,
|
date: prev_date,
|
||||||
symbol: "601028.SH".to_string(),
|
symbol: "601028.SH".into(),
|
||||||
timestamp: None,
|
timestamp: None,
|
||||||
day_open: 10.2,
|
day_open: 10.2,
|
||||||
open: 10.2,
|
open: 10.2,
|
||||||
@@ -1923,7 +1921,7 @@ mod tests {
|
|||||||
vec![
|
vec![
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: buy_date,
|
date: buy_date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
timestamp: None,
|
timestamp: None,
|
||||||
day_open: 2.99,
|
day_open: 2.99,
|
||||||
open: 2.99,
|
open: 2.99,
|
||||||
@@ -1946,7 +1944,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: next_date,
|
date: next_date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
timestamp: None,
|
timestamp: None,
|
||||||
day_open: 3.03,
|
day_open: 3.03,
|
||||||
open: 3.03,
|
open: 3.03,
|
||||||
|
|||||||
@@ -869,7 +869,7 @@ mod tests {
|
|||||||
fn candidate(date: NaiveDate) -> CandidateEligibility {
|
fn candidate(date: NaiveDate) -> CandidateEligibility {
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: "002633.SZ".to_string(),
|
symbol: "002633.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -885,7 +885,7 @@ mod tests {
|
|||||||
fn market(date: NaiveDate, last_price: f64, lower_limit: f64) -> DailyMarketSnapshot {
|
fn market(date: NaiveDate, last_price: f64, lower_limit: f64) -> DailyMarketSnapshot {
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "002633.SZ".to_string(),
|
symbol: "002633.SZ".into(),
|
||||||
timestamp: Some(format!("{date} 10:18:00")),
|
timestamp: Some(format!("{date} 10:18:00")),
|
||||||
day_open: last_price,
|
day_open: last_price,
|
||||||
open: last_price,
|
open: last_price,
|
||||||
@@ -944,7 +944,7 @@ mod tests {
|
|||||||
let mut candidate=candidate(day);
|
let mut candidate=candidate(day);
|
||||||
let mut snapshot=market(day,1.2,0.5);
|
let mut snapshot=market(day,1.2,0.5);
|
||||||
snapshot.lower_limit=0.01;snapshot.upper_limit=10.;
|
snapshot.lower_limit=0.01;snapshot.upper_limit=10.;
|
||||||
let instrument=Instrument{symbol:candidate.symbol.clone(),name:"fixture fund".into(),board:"ETF".into(),round_lot:100,listed_at:Some(d(2024,1,2)),delisted_at:None,status:"active".into()};
|
let instrument=Instrument{symbol:candidate.symbol.to_string(),name:"fixture fund".into(),board:"ETF".into(),round_lot:100,listed_at:Some(d(2024,1,2)),delisted_at:None,status:"active".into()};
|
||||||
let config=FidcRiskControlConfig::default();
|
let config=FidcRiskControlConfig::default();
|
||||||
assert_eq!(ChinaAShareRiskControl::buy_rejection_reason_with_config(day,&candidate,&snapshot,Some(&instrument),0.9,&config),None);
|
assert_eq!(ChinaAShareRiskControl::buy_rejection_reason_with_config(day,&candidate,&snapshot,Some(&instrument),0.9,&config),None);
|
||||||
assert_eq!(ChinaAShareRiskControl::buy_rejection_reason_with_config(day,&candidate,&snapshot,Some(&instrument),0.,&config),Some("invalid execution price"));
|
assert_eq!(ChinaAShareRiskControl::buy_rejection_reason_with_config(day,&candidate,&snapshot,Some(&instrument),0.,&config),Some("invalid execution price"));
|
||||||
@@ -1047,7 +1047,7 @@ mod tests {
|
|||||||
config
|
config
|
||||||
.static_rules
|
.static_rules
|
||||||
.blacklisted_symbols
|
.blacklisted_symbols
|
||||||
.insert(candidate.symbol.clone());
|
.insert(candidate.symbol.to_string());
|
||||||
|
|
||||||
let selection_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config(
|
let selection_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config(
|
||||||
date, &candidate, &market, None, &config,
|
date, &candidate, &market, None, &config,
|
||||||
@@ -1092,7 +1092,7 @@ mod tests {
|
|||||||
let mut candidate = candidate(date);
|
let mut candidate = candidate(date);
|
||||||
let config = FidcRiskControlConfig::default();
|
let config = FidcRiskControlConfig::default();
|
||||||
for symbol in ["688001.SH", "689001.SH", "000001.SZ"] {
|
for symbol in ["688001.SH", "689001.SH", "000001.SZ"] {
|
||||||
candidate.symbol = symbol.to_string();
|
candidate.symbol = symbol.into();
|
||||||
for is_kcb in [false, true] {
|
for is_kcb in [false, true] {
|
||||||
candidate.is_kcb = is_kcb;
|
candidate.is_kcb = is_kcb;
|
||||||
let reason = ChinaAShareRiskControl::buy_rejection_reason_with_config(
|
let reason = ChinaAShareRiskControl::buy_rejection_reason_with_config(
|
||||||
@@ -1200,7 +1200,7 @@ mod tests {
|
|||||||
fn configurable_bjse_filter_can_be_disabled() {
|
fn configurable_bjse_filter_can_be_disabled() {
|
||||||
let date = d(2025, 1, 2);
|
let date = d(2025, 1, 2);
|
||||||
let mut candidate = candidate(date);
|
let mut candidate = candidate(date);
|
||||||
candidate.symbol = "430047.BJ".to_string();
|
candidate.symbol = "430047.BJ".into();
|
||||||
candidate.allow_sell = true;
|
candidate.allow_sell = true;
|
||||||
let market = market(date, 6.27, 5.63);
|
let market = market(date, 6.27, 5.63);
|
||||||
let default_selection =
|
let default_selection =
|
||||||
@@ -1232,7 +1232,7 @@ mod tests {
|
|||||||
fn concrete_kcb_reason_wins_over_generic_missing_risk_state() {
|
fn concrete_kcb_reason_wins_over_generic_missing_risk_state() {
|
||||||
let date = d(2025, 1, 2);
|
let date = d(2025, 1, 2);
|
||||||
let mut candidate = candidate(date);
|
let mut candidate = candidate(date);
|
||||||
candidate.symbol = "688506.SH".to_string();
|
candidate.symbol = "688506.SH".into();
|
||||||
candidate.is_kcb = true;
|
candidate.is_kcb = true;
|
||||||
candidate.risk_level_code = Some("missing_risk_state".to_string());
|
candidate.risk_level_code = Some("missing_risk_state".to_string());
|
||||||
let market = market(date, 6.27, 5.63);
|
let market = market(date, 6.27, 5.63);
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
//! Condition facts are distinct from the quote's per-observation fill capacity.
|
||||||
|
//! Only a complete, declared raw-minute prefix can prove a session total.
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use chrono::{NaiveDate, NaiveDateTime, NaiveTime, Timelike};
|
||||||
|
use rust_decimal::Decimal;
|
||||||
|
use crate::data::IntradayExecutionQuote;
|
||||||
|
use crate::stock_pool_execution::{parse_stock_pool_condition, StockPoolExecutionRule};
|
||||||
|
|
||||||
|
pub fn requires_session_totals(rule: &StockPoolExecutionRule) -> bool {
|
||||||
|
[rule.buy_condition.as_str(), if rule.sell_trigger_mode == "condition" { rule.sell_condition.as_str() } else { "" }].into_iter().any(|condition| {
|
||||||
|
parse_stock_pool_condition(condition).is_some_and(|(_, field, _, _)| matches!(field.as_str(), "volume" | "amount"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The cash-equity minute feed includes the opening observation and a separate
|
||||||
|
/// post-close segment. Trading eligibility remains owned by the dated rules.
|
||||||
|
fn next_minute(time: NaiveTime) -> Option<NaiveTime> {
|
||||||
|
let minute = time.hour() * 60 + time.minute();
|
||||||
|
let next = match minute {
|
||||||
|
570..=689 | 781..=899 | 906..=929 => minute + 1,
|
||||||
|
690 => 781,
|
||||||
|
900 => 906,
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
NaiveTime::from_hms_opt(next / 60, next % 60, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct SessionTotalsCache {
|
||||||
|
pub date: Option<NaiveDate>,
|
||||||
|
pub symbols: BTreeMap<String, MinutePrefix>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct MinutePrefix {
|
||||||
|
values: BTreeMap<NaiveTime, (Decimal, Decimal)>,
|
||||||
|
failure: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MinutePrefix {
|
||||||
|
pub fn build(date: NaiveDate, symbol: &str, quotes: &[IntradayExecutionQuote]) -> Self {
|
||||||
|
let mut values = BTreeMap::new();
|
||||||
|
let mut expected = NaiveTime::from_hms_opt(9, 30, 0).unwrap();
|
||||||
|
let mut volume = 0_u64;
|
||||||
|
let mut amount = Decimal::ZERO;
|
||||||
|
let mut failure = format!("stock_pool_session_prefix_missing:{symbol}:{date}:{expected}");
|
||||||
|
for quote in quotes {
|
||||||
|
let time = quote.timestamp.time();
|
||||||
|
if quote.date != date || quote.timestamp.date() != date || quote.symbol != symbol {
|
||||||
|
failure = format!("stock_pool_session_prefix_identity_invalid:{symbol}:{date}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if time != expected {
|
||||||
|
failure = format!("stock_pool_session_prefix_missing:{symbol}:{date}:expected={expected}:observed={time}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if quote.observation_kind != crate::data::QuoteObservationKind::MinuteBar {
|
||||||
|
failure = format!("stock_pool_session_prefix_basis_unverified:{symbol}:{date}:{time}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let Some(next_volume) = volume.checked_add(quote.volume_delta) else {
|
||||||
|
failure = format!("stock_pool_session_volume_overflow:{symbol}:{date}:{time}");
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let delta = if quote.amount_delta.is_finite() && quote.amount_delta >= 0.0 {
|
||||||
|
quote.amount_delta.to_string().parse::<Decimal>().ok()
|
||||||
|
} else { None };
|
||||||
|
let Some(next_amount) = delta.and_then(|delta| amount.checked_add(delta)) else {
|
||||||
|
failure = format!("stock_pool_session_amount_invalid:{symbol}:{date}:{time}");
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
volume = next_volume;
|
||||||
|
amount = next_amount;
|
||||||
|
values.insert(time, (Decimal::from(volume), amount));
|
||||||
|
let Some(next) = next_minute(time) else { break };
|
||||||
|
expected = next;
|
||||||
|
failure = format!("stock_pool_session_prefix_missing:{symbol}:{date}:{expected}");
|
||||||
|
}
|
||||||
|
Self { values, failure }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn at(&self, at: NaiveDateTime) -> Result<(Decimal, Decimal), String> {
|
||||||
|
let time = at.time().with_second(0).unwrap().with_nanosecond(0).unwrap();
|
||||||
|
self.values.get(&time).copied().ok_or_else(|| self.failure.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
fn quote(hour: u32, minute: u32, volume: u64, amount: f64) -> IntradayExecutionQuote {
|
||||||
|
let date = NaiveDate::from_ymd_opt(2026, 9, 11).unwrap();
|
||||||
|
IntradayExecutionQuote { observation_kind: crate::data::QuoteObservationKind::MinuteBar, date, symbol: "000001.SZ".into(), timestamp: date.and_hms_opt(hour, minute, 0).unwrap(),
|
||||||
|
last_price: 10., bid1: 0., ask1: 0., bid1_volume: 0, ask1_volume: 0,
|
||||||
|
volume_delta: volume, amount_delta: amount, trading_phase: Some("minute_execution_prices:raw-minute".into()) }
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn totals_use_only_the_complete_observed_prefix_and_keep_decimal_amounts() {
|
||||||
|
let mut rows = vec![quote(9,30,100,10.01), quote(9,31,0,0.), quote(9,32,200,20.02)];
|
||||||
|
let prefix = MinutePrefix::build(rows[0].date, "000001.SZ", &rows);
|
||||||
|
assert_eq!(prefix.at(rows[1].timestamp).unwrap(), (100.into(), Decimal::new(1001,2)));
|
||||||
|
assert_eq!(prefix.at(rows[2].timestamp).unwrap(), (300.into(), Decimal::new(3003,2)));
|
||||||
|
rows[2].volume_delta = 999999;
|
||||||
|
rows[2].amount_delta = f64::NAN;
|
||||||
|
let changed = MinutePrefix::build(rows[0].date, "000001.SZ", &rows);
|
||||||
|
assert_eq!(changed.at(rows[1].timestamp).unwrap(), prefix.at(rows[1].timestamp).unwrap());
|
||||||
|
assert!(changed.at(rows[2].timestamp).unwrap_err().contains("amount_invalid"));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn sparse_unverified_and_overflowing_quotes_cannot_be_called_session_totals() {
|
||||||
|
let first = quote(9,30,100,1000.);
|
||||||
|
for rows in [vec![quote(9,31,100,1000.)], vec![first.clone(), quote(9,32,100,1000.)]] {
|
||||||
|
let prefix = MinutePrefix::build(first.date, "000001.SZ", &rows);
|
||||||
|
assert!(prefix.at(rows.last().unwrap().timestamp).unwrap_err().contains("prefix_missing"));
|
||||||
|
}
|
||||||
|
let mut unknown = first.clone(); unknown.observation_kind = Default::default();
|
||||||
|
assert!(MinutePrefix::build(first.date, "000001.SZ", &[unknown]).at(first.timestamp).unwrap_err().contains("basis_unverified"));
|
||||||
|
let rows = [quote(9,30,u64::MAX,0.), quote(9,31,1,0.)];
|
||||||
|
assert!(MinutePrefix::build(first.date, "000001.SZ", &rows).at(rows[1].timestamp).unwrap_err().contains("volume_overflow"));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn lunch_and_post_close_gaps_follow_the_minute_feed_segments() {
|
||||||
|
let mut rows = Vec::new(); let mut time = NaiveTime::from_hms_opt(9,30,0).unwrap();
|
||||||
|
loop {
|
||||||
|
rows.push(quote(time.hour(), time.minute(), 1, 0.01));
|
||||||
|
let Some(next) = next_minute(time) else { break }; time=next;
|
||||||
|
}
|
||||||
|
let prefix=MinutePrefix::build(rows[0].date,"000001.SZ",&rows);
|
||||||
|
assert_eq!(prefix.at(rows.last().unwrap().timestamp).unwrap(), (Decimal::from(rows.len()), Decimal::new(rows.len() as i64,2)));
|
||||||
|
assert!(!rows.iter().any(|row| row.timestamp.time().hour()==12));
|
||||||
|
assert!(!rows.iter().any(|row| row.timestamp.time()==NaiveTime::from_hms_opt(13,0,0).unwrap()));
|
||||||
|
assert!(!rows.iter().any(|row| row.timestamp.time().hour()==15 && (1..6).contains(&row.timestamp.time().minute())));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires FIDC_SESSION_PREFIX_SOURCE_JSON from the frozen Source minute response"]
|
||||||
|
fn real_source_session_prefix_matches_observed_checkpoints() {
|
||||||
|
let path=std::env::var("FIDC_SESSION_PREFIX_SOURCE_JSON").expect("explicit Source evidence path");
|
||||||
|
let rows:Vec<IntradayExecutionQuote>=serde_json::from_slice(&std::fs::read(path).unwrap()).unwrap();
|
||||||
|
let date=NaiveDate::from_ymd_opt(2026,9,8).unwrap();
|
||||||
|
assert_eq!(rows.len(),242);
|
||||||
|
let prefix=MinutePrefix::build(date,"000063.SZ",&rows);
|
||||||
|
for (hour,minute,volume,amount) in [(9,30,512700,17103672),(9,31,2296631,76576756),(9,32,2983531,99471024),(11,30,27868847,928167630),(13,1,28495518,948994890),(15,0,45625008,1518115100)] {
|
||||||
|
assert_eq!(prefix.at(date.and_hms_opt(hour,minute,0).unwrap()).unwrap(),(Decimal::from(volume),Decimal::from(amount)));
|
||||||
|
}
|
||||||
|
assert!(prefix.at(date.and_hms_opt(15,30,0).unwrap()).unwrap_err().contains("prefix_missing"),"one final aggregate is not a verified intraday prefix");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3184,7 +3184,7 @@ mod tests {
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, date)| DailyMarketSnapshot {
|
.map(|(index, date)| DailyMarketSnapshot {
|
||||||
date: *date,
|
date: *date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
timestamp: Some(format!("{date} 10:18:00")),
|
timestamp: Some(format!("{date} 10:18:00")),
|
||||||
day_open: 10.0 + index as f64,
|
day_open: 10.0 + index as f64,
|
||||||
open: 10.0 + index as f64,
|
open: 10.0 + index as f64,
|
||||||
@@ -3210,21 +3210,21 @@ mod tests {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|date| DailyFactorSnapshot {
|
.map(|date| DailyFactorSnapshot {
|
||||||
date: *date,
|
date: *date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
market_cap_bn: 10.0,
|
market_cap_bn: 10.0,
|
||||||
free_float_cap_bn: 9.0,
|
free_float_cap_bn: 9.0,
|
||||||
pe_ttm: 12.0,
|
pe_ttm: 12.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let candidate_rows = dates
|
let candidate_rows = dates
|
||||||
.iter()
|
.iter()
|
||||||
.map(|date| CandidateEligibility {
|
.map(|date| CandidateEligibility {
|
||||||
date: *date,
|
date: *date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ impl SelectionContext<'_> {
|
|||||||
for factor in self.data.factor_snapshot_rows_on(self.decision_date) {
|
for factor in self.data.factor_snapshot_rows_on(self.decision_date) {
|
||||||
if self
|
if self
|
||||||
.dynamic_universe
|
.dynamic_universe
|
||||||
.is_some_and(|symbols| !symbols.is_empty() && !symbols.contains(&factor.symbol))
|
.is_some_and(|symbols| !symbols.is_empty() && !symbols.contains(factor.symbol.as_str()))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -307,7 +307,7 @@ mod tests {
|
|||||||
fn market(symbol: &str, price: f64) -> DailyMarketSnapshot {
|
fn market(symbol: &str, price: f64) -> DailyMarketSnapshot {
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: d(),
|
date: d(),
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
timestamp: Some("2025-01-02 10:00:00".to_string()),
|
timestamp: Some("2025-01-02 10:00:00".to_string()),
|
||||||
day_open: price,
|
day_open: price,
|
||||||
open: price,
|
open: price,
|
||||||
@@ -333,7 +333,7 @@ mod tests {
|
|||||||
fn factor(symbol: &str, market_cap_bn: f64) -> DailyFactorSnapshot {
|
fn factor(symbol: &str, market_cap_bn: f64) -> DailyFactorSnapshot {
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: d(),
|
date: d(),
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
market_cap_bn,
|
market_cap_bn,
|
||||||
free_float_cap_bn: market_cap_bn,
|
free_float_cap_bn: market_cap_bn,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
@@ -347,7 +347,7 @@ mod tests {
|
|||||||
fn candidate(symbol: &str, is_st: bool, is_kcb: bool) -> CandidateEligibility {
|
fn candidate(symbol: &str, is_st: bool, is_kcb: bool) -> CandidateEligibility {
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: d(),
|
date: d(),
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
is_st,
|
is_st,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ fn d(year: i32, month: u32, day: u32) -> NaiveDate {
|
|||||||
fn candidate() -> CandidateEligibility {
|
fn candidate() -> CandidateEligibility {
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: d(2024, 1, 3),
|
date: d(2024, 1, 3),
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -31,7 +31,7 @@ fn candidate() -> CandidateEligibility {
|
|||||||
fn snapshot(open: f64, upper_limit: f64, lower_limit: f64) -> DailyMarketSnapshot {
|
fn snapshot(open: f64, upper_limit: f64, lower_limit: f64) -> DailyMarketSnapshot {
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: d(2024, 1, 3),
|
date: d(2024, 1, 3),
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2024-01-03 10:18:00".to_string()),
|
timestamp: Some("2024-01-03 10:18:00".to_string()),
|
||||||
day_open: open,
|
day_open: open,
|
||||||
open,
|
open,
|
||||||
@@ -256,7 +256,7 @@ fn china_rule_hooks_allow_sell_when_last_price_is_above_lower_limit() {
|
|||||||
|
|
||||||
let snapshot = DailyMarketSnapshot {
|
let snapshot = DailyMarketSnapshot {
|
||||||
date: d(2024, 4, 7),
|
date: d(2024, 4, 7),
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2024-04-07 10:18:00".to_string()),
|
timestamp: Some("2024-04-07 10:18:00".to_string()),
|
||||||
day_open: 2.53,
|
day_open: 2.53,
|
||||||
open: 2.53,
|
open: 2.53,
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ impl Strategy for BuyAndHoldStrategy {
|
|||||||
fn stock_market_snapshot(date: NaiveDate) -> DailyMarketSnapshot {
|
fn stock_market_snapshot(date: NaiveDate) -> DailyMarketSnapshot {
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some(format!("{date} 10:18:00")),
|
timestamp: Some(format!("{date} 10:18:00")),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -139,21 +139,21 @@ fn stock_market_snapshot(date: NaiveDate) -> DailyMarketSnapshot {
|
|||||||
fn stock_factor_snapshot(date: NaiveDate) -> DailyFactorSnapshot {
|
fn stock_factor_snapshot(date: NaiveDate) -> DailyFactorSnapshot {
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 20.0,
|
market_cap_bn: 20.0,
|
||||||
free_float_cap_bn: 18.0,
|
free_float_cap_bn: 18.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stock_candidate(date: NaiveDate) -> CandidateEligibility {
|
fn stock_candidate(date: NaiveDate) -> CandidateEligibility {
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -195,7 +195,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
|||||||
vec![
|
vec![
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: buy_date,
|
date: buy_date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-01 10:18:00".to_string()),
|
timestamp: Some("2025-01-01 10:18:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -218,7 +218,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
|||||||
},
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: ex_date,
|
date: ex_date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -241,7 +241,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
|||||||
},
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: payable_date,
|
date: payable_date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-03 10:18:00".to_string()),
|
timestamp: Some("2025-01-03 10:18:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -266,42 +266,42 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
|||||||
vec![
|
vec![
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: buy_date,
|
date: buy_date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 20.0,
|
market_cap_bn: 20.0,
|
||||||
free_float_cap_bn: 18.0,
|
free_float_cap_bn: 18.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: ex_date,
|
date: ex_date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 20.0,
|
market_cap_bn: 20.0,
|
||||||
free_float_cap_bn: 18.0,
|
free_float_cap_bn: 18.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: payable_date,
|
date: payable_date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 20.0,
|
market_cap_bn: 20.0,
|
||||||
free_float_cap_bn: 18.0,
|
free_float_cap_bn: 18.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
vec![
|
vec![
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: buy_date,
|
date: buy_date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -314,7 +314,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
|||||||
},
|
},
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: ex_date,
|
date: ex_date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -327,7 +327,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
|||||||
},
|
},
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: payable_date,
|
date: payable_date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ fn single_day_quote_plan_data(date: NaiveDate) -> DataSet {
|
|||||||
fixture_instruments(),
|
fixture_instruments(),
|
||||||
vec![DailyMarketSnapshot {
|
vec![DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some(format!("{date} 15:00:00")),
|
timestamp: Some(format!("{date} 15:00:00")),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -128,7 +128,7 @@ fn single_day_quote_plan_data(date: NaiveDate) -> DataSet {
|
|||||||
}],
|
}],
|
||||||
vec![DailyFactorSnapshot {
|
vec![DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 10.0,
|
market_cap_bn: 10.0,
|
||||||
free_float_cap_bn: 10.0,
|
free_float_cap_bn: 10.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
@@ -139,7 +139,7 @@ fn single_day_quote_plan_data(date: NaiveDate) -> DataSet {
|
|||||||
}],
|
}],
|
||||||
vec![CandidateEligibility {
|
vec![CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -196,7 +196,7 @@ fn engine_uses_preplanned_decision_symbols_without_recomputing_strategy_plan() {
|
|||||||
Ok(request
|
Ok(request
|
||||||
.symbols
|
.symbols
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: request.date,
|
date: request.date,
|
||||||
symbol,
|
symbol,
|
||||||
timestamp: request.date.and_time(t(10, 17, 59)),
|
timestamp: request.date.and_time(t(10, 17, 59)),
|
||||||
@@ -269,7 +269,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
|||||||
vec![
|
vec![
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: first,
|
date: first,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2026-01-05 15:00:00".to_string()),
|
timestamp: Some("2026-01-05 15:00:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -292,7 +292,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
|||||||
},
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: second,
|
date: second,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2026-01-06 15:00:00".to_string()),
|
timestamp: Some("2026-01-06 15:00:00".to_string()),
|
||||||
day_open: 10.5,
|
day_open: 10.5,
|
||||||
open: 10.5,
|
open: 10.5,
|
||||||
@@ -317,7 +317,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
|||||||
vec![
|
vec![
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: first,
|
date: first,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 10.0,
|
market_cap_bn: 10.0,
|
||||||
free_float_cap_bn: 10.0,
|
free_float_cap_bn: 10.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
@@ -328,7 +328,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
|||||||
},
|
},
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: second,
|
date: second,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 10.0,
|
market_cap_bn: 10.0,
|
||||||
free_float_cap_bn: 10.0,
|
free_float_cap_bn: 10.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
@@ -341,7 +341,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
|||||||
vec![
|
vec![
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: first,
|
date: first,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -354,7 +354,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
|||||||
},
|
},
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: second,
|
date: second,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -411,7 +411,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
|||||||
Ok(request
|
Ok(request
|
||||||
.symbols
|
.symbols
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: request.date,
|
date: request.date,
|
||||||
symbol,
|
symbol,
|
||||||
timestamp: request.date.and_time(t(10, 39, 59)),
|
timestamp: request.date.and_time(t(10, 39, 59)),
|
||||||
@@ -439,7 +439,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
|||||||
vec![
|
vec![
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: first,
|
date: first,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2026-01-05 15:00:00".to_string()),
|
timestamp: Some("2026-01-05 15:00:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -462,7 +462,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
|||||||
},
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: second,
|
date: second,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2026-01-06 15:00:00".to_string()),
|
timestamp: Some("2026-01-06 15:00:00".to_string()),
|
||||||
day_open: 10.5,
|
day_open: 10.5,
|
||||||
open: 10.5,
|
open: 10.5,
|
||||||
@@ -487,7 +487,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
|||||||
vec![
|
vec![
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: first,
|
date: first,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 10.0,
|
market_cap_bn: 10.0,
|
||||||
free_float_cap_bn: 10.0,
|
free_float_cap_bn: 10.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
@@ -498,7 +498,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
|||||||
},
|
},
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: second,
|
date: second,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 10.0,
|
market_cap_bn: 10.0,
|
||||||
free_float_cap_bn: 10.0,
|
free_float_cap_bn: 10.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
@@ -511,7 +511,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
|||||||
vec![
|
vec![
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: first,
|
date: first,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -524,7 +524,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
|||||||
},
|
},
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: second,
|
date: second,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -556,7 +556,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
|||||||
],
|
],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: first,
|
date: first,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp: first.and_time(t(10, 39, 59)),
|
timestamp: first.and_time(t(10, 39, 59)),
|
||||||
@@ -569,7 +569,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
|||||||
amount_delta: 100_000.0,
|
amount_delta: 100_000.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: second,
|
date: second,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp: second.and_time(t(10, 39, 59)),
|
timestamp: second.and_time(t(10, 39, 59)),
|
||||||
@@ -674,7 +674,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
|||||||
vec![
|
vec![
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: first,
|
date: first,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2026-01-05 15:00:00".to_string()),
|
timestamp: Some("2026-01-05 15:00:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -697,7 +697,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
|||||||
},
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: second,
|
date: second,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2026-01-06 15:00:00".to_string()),
|
timestamp: Some("2026-01-06 15:00:00".to_string()),
|
||||||
day_open: 10.5,
|
day_open: 10.5,
|
||||||
open: 10.5,
|
open: 10.5,
|
||||||
@@ -722,7 +722,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
|||||||
vec![
|
vec![
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: first,
|
date: first,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 10.0,
|
market_cap_bn: 10.0,
|
||||||
free_float_cap_bn: 10.0,
|
free_float_cap_bn: 10.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
@@ -733,7 +733,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
|||||||
},
|
},
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: second,
|
date: second,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 10.0,
|
market_cap_bn: 10.0,
|
||||||
free_float_cap_bn: 10.0,
|
free_float_cap_bn: 10.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
@@ -746,7 +746,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
|||||||
vec![
|
vec![
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: first,
|
date: first,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -759,7 +759,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
|||||||
},
|
},
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: second,
|
date: second,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -826,7 +826,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
|||||||
Ok(request
|
Ok(request
|
||||||
.symbols
|
.symbols
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|symbol| IntradayExecutionQuote {
|
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: request.date,
|
date: request.date,
|
||||||
symbol,
|
symbol,
|
||||||
timestamp: request.date.and_time(start_time) - Duration::seconds(1),
|
timestamp: request.date.and_time(start_time) - Duration::seconds(1),
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
|
|||||||
vec![
|
vec![
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -94,7 +94,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
|
|||||||
},
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".into(),
|
||||||
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
||||||
day_open: 5.0,
|
day_open: 5.0,
|
||||||
open: 5.0,
|
open: 5.0,
|
||||||
@@ -117,7 +117,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
|
|||||||
},
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: delist_date,
|
date: delist_date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".into(),
|
||||||
timestamp: Some("2025-01-03 10:18:00".to_string()),
|
timestamp: Some("2025-01-03 10:18:00".to_string()),
|
||||||
day_open: 5.05,
|
day_open: 5.05,
|
||||||
open: 5.05,
|
open: 5.05,
|
||||||
@@ -140,7 +140,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
|
|||||||
},
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".into(),
|
||||||
timestamp: Some("2025-01-06 10:18:00".to_string()),
|
timestamp: Some("2025-01-06 10:18:00".to_string()),
|
||||||
day_open: 5.1,
|
day_open: 5.1,
|
||||||
open: 5.1,
|
open: 5.1,
|
||||||
@@ -165,53 +165,53 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
|
|||||||
vec![
|
vec![
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 20.0,
|
market_cap_bn: 20.0,
|
||||||
free_float_cap_bn: 18.0,
|
free_float_cap_bn: 18.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".into(),
|
||||||
market_cap_bn: 30.0,
|
market_cap_bn: 30.0,
|
||||||
free_float_cap_bn: 28.0,
|
free_float_cap_bn: 28.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: delist_date,
|
date: delist_date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".into(),
|
||||||
market_cap_bn: 30.5,
|
market_cap_bn: 30.5,
|
||||||
free_float_cap_bn: 28.5,
|
free_float_cap_bn: 28.5,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".into(),
|
||||||
market_cap_bn: 31.0,
|
market_cap_bn: 31.0,
|
||||||
free_float_cap_bn: 29.0,
|
free_float_cap_bn: 29.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
vec![
|
vec![
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -224,7 +224,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
|
|||||||
},
|
},
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -237,7 +237,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
|
|||||||
},
|
},
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: delist_date,
|
date: delist_date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -250,7 +250,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
|
|||||||
},
|
},
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -364,7 +364,7 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
|
|||||||
vec![
|
vec![
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -387,7 +387,7 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
|
|||||||
},
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".into(),
|
||||||
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
||||||
day_open: 20.0,
|
day_open: 20.0,
|
||||||
open: 20.0,
|
open: 20.0,
|
||||||
@@ -410,7 +410,7 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
|
|||||||
},
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".into(),
|
||||||
timestamp: Some("2025-01-03 10:18:00".to_string()),
|
timestamp: Some("2025-01-03 10:18:00".to_string()),
|
||||||
day_open: 21.0,
|
day_open: 21.0,
|
||||||
open: 21.0,
|
open: 21.0,
|
||||||
@@ -435,42 +435,42 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
|
|||||||
vec![
|
vec![
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 20.0,
|
market_cap_bn: 20.0,
|
||||||
free_float_cap_bn: 18.0,
|
free_float_cap_bn: 18.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".into(),
|
||||||
market_cap_bn: 30.0,
|
market_cap_bn: 30.0,
|
||||||
free_float_cap_bn: 28.0,
|
free_float_cap_bn: 28.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".into(),
|
||||||
market_cap_bn: 31.0,
|
market_cap_bn: 31.0,
|
||||||
free_float_cap_bn: 29.0,
|
free_float_cap_bn: 29.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
vec![
|
vec![
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -483,7 +483,7 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
|
|||||||
},
|
},
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -496,7 +496,7 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
|
|||||||
},
|
},
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ fn single_day_anchor_data(date: NaiveDate) -> DataSet {
|
|||||||
}],
|
}],
|
||||||
vec![DailyMarketSnapshot {
|
vec![DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -72,18 +72,18 @@ fn single_day_anchor_data(date: NaiveDate) -> DataSet {
|
|||||||
}],
|
}],
|
||||||
vec![DailyFactorSnapshot {
|
vec![DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 100.0,
|
market_cap_bn: 100.0,
|
||||||
free_float_cap_bn: 80.0,
|
free_float_cap_bn: 80.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
}],
|
}],
|
||||||
vec![CandidateEligibility {
|
vec![CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -109,7 +109,7 @@ fn single_day_anchor_data(date: NaiveDate) -> DataSet {
|
|||||||
fn market_row(date: NaiveDate, symbol: &str, open: f64, close: f64) -> DailyMarketSnapshot {
|
fn market_row(date: NaiveDate, symbol: &str, open: f64, close: f64) -> DailyMarketSnapshot {
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
timestamp: Some(format!("{date} 10:18:00")),
|
timestamp: Some(format!("{date} 10:18:00")),
|
||||||
day_open: open,
|
day_open: open,
|
||||||
open,
|
open,
|
||||||
@@ -139,7 +139,7 @@ fn factor_row(
|
|||||||
) -> DailyFactorSnapshot {
|
) -> DailyFactorSnapshot {
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
market_cap_bn: 100.0,
|
market_cap_bn: 100.0,
|
||||||
free_float_cap_bn: 80.0,
|
free_float_cap_bn: 80.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
@@ -153,7 +153,7 @@ fn factor_row(
|
|||||||
fn candidate_row(date: NaiveDate, symbol: &str) -> CandidateEligibility {
|
fn candidate_row(date: NaiveDate, symbol: &str) -> CandidateEligibility {
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -219,7 +219,7 @@ fn two_day_futures_data() -> DataSet {
|
|||||||
("stock_connect_north_bound".into(), 1.0),
|
("stock_connect_north_bound".into(), 1.0),
|
||||||
("industry_citics_l1".into(), 10.0),
|
("industry_citics_l1".into(), 10.0),
|
||||||
("fundamental_net_profit".into(), 99.0),
|
("fundamental_net_profit".into(), 99.0),
|
||||||
]),
|
]).into(),
|
||||||
),
|
),
|
||||||
factor_row(
|
factor_row(
|
||||||
d2,
|
d2,
|
||||||
@@ -232,7 +232,7 @@ fn two_day_futures_data() -> DataSet {
|
|||||||
("stock_connect_north_bound".into(), 1.0),
|
("stock_connect_north_bound".into(), 1.0),
|
||||||
("industry_citics_l1".into(), 10.0),
|
("industry_citics_l1".into(), 10.0),
|
||||||
("fundamental_net_profit".into(), 101.0),
|
("fundamental_net_profit".into(), 101.0),
|
||||||
]),
|
]).into(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
vec![
|
vec![
|
||||||
@@ -1094,7 +1094,7 @@ fn engine_runs_strategy_hooks_in_daily_order() {
|
|||||||
vec![
|
vec![
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -1117,7 +1117,7 @@ fn engine_runs_strategy_hooks_in_daily_order() {
|
|||||||
},
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-03 10:18:00".to_string()),
|
timestamp: Some("2025-01-03 10:18:00".to_string()),
|
||||||
day_open: 10.1,
|
day_open: 10.1,
|
||||||
open: 10.1,
|
open: 10.1,
|
||||||
@@ -1142,31 +1142,31 @@ fn engine_runs_strategy_hooks_in_daily_order() {
|
|||||||
vec![
|
vec![
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 20.0,
|
market_cap_bn: 20.0,
|
||||||
free_float_cap_bn: 18.0,
|
free_float_cap_bn: 18.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 21.0,
|
market_cap_bn: 21.0,
|
||||||
free_float_cap_bn: 19.0,
|
free_float_cap_bn: 19.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
vec![
|
vec![
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -1179,7 +1179,7 @@ fn engine_runs_strategy_hooks_in_daily_order() {
|
|||||||
},
|
},
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -1337,7 +1337,7 @@ fn engine_executes_open_auction_decisions_before_on_day() {
|
|||||||
}],
|
}],
|
||||||
vec![DailyMarketSnapshot {
|
vec![DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-02 09:25:00".to_string()),
|
timestamp: Some("2025-01-02 09:25:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -1360,18 +1360,18 @@ fn engine_executes_open_auction_decisions_before_on_day() {
|
|||||||
}],
|
}],
|
||||||
vec![DailyFactorSnapshot {
|
vec![DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 20.0,
|
market_cap_bn: 20.0,
|
||||||
free_float_cap_bn: 18.0,
|
free_float_cap_bn: 18.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
}],
|
}],
|
||||||
vec![CandidateEligibility {
|
vec![CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -1438,7 +1438,7 @@ fn engine_executes_futures_order_intents_against_future_account() {
|
|||||||
}],
|
}],
|
||||||
vec![DailyMarketSnapshot {
|
vec![DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -1461,18 +1461,18 @@ fn engine_executes_futures_order_intents_against_future_account() {
|
|||||||
}],
|
}],
|
||||||
vec![DailyFactorSnapshot {
|
vec![DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 100.0,
|
market_cap_bn: 100.0,
|
||||||
free_float_cap_bn: 80.0,
|
free_float_cap_bn: 80.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
}],
|
}],
|
||||||
vec![CandidateEligibility {
|
vec![CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -1935,7 +1935,7 @@ fn engine_rejects_futures_orders_when_trading_phase_is_closed() {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
vec![market_row(date, "000001.SZ", 10.0, 10.0), future_market],
|
vec![market_row(date, "000001.SZ", 10.0, 10.0), future_market],
|
||||||
vec![factor_row(date, "000001.SZ", BTreeMap::new())],
|
vec![factor_row(date, "000001.SZ", BTreeMap::new().into())],
|
||||||
vec![candidate_row(date, "000001.SZ")],
|
vec![candidate_row(date, "000001.SZ")],
|
||||||
vec![benchmark_row(date)],
|
vec![benchmark_row(date)],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
@@ -2011,7 +2011,7 @@ fn engine_sweeps_futures_order_book_depth_when_available() {
|
|||||||
market_row(date, "000001.SZ", 10.0, 10.0),
|
market_row(date, "000001.SZ", 10.0, 10.0),
|
||||||
market_row(date, "IF2501", 4000.0, 4000.0),
|
market_row(date, "IF2501", 4000.0, 4000.0),
|
||||||
],
|
],
|
||||||
vec![factor_row(date, "000001.SZ", BTreeMap::new())],
|
vec![factor_row(date, "000001.SZ", BTreeMap::new().into())],
|
||||||
vec![candidate_row(date, "000001.SZ")],
|
vec![candidate_row(date, "000001.SZ")],
|
||||||
vec![benchmark_row(date)],
|
vec![benchmark_row(date)],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
@@ -2154,7 +2154,7 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
|
|||||||
}).collect(),
|
}).collect(),
|
||||||
vec![DailyMarketSnapshot {
|
vec![DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -2177,18 +2177,18 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
|
|||||||
}, market_row(date, "000002.SZ", 20.0, 20.4)],
|
}, market_row(date, "000002.SZ", 20.0, 20.4)],
|
||||||
vec![DailyFactorSnapshot {
|
vec![DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 20.0,
|
market_cap_bn: 20.0,
|
||||||
free_float_cap_bn: 18.0,
|
free_float_cap_bn: 18.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
}],
|
}],
|
||||||
vec![CandidateEligibility {
|
vec![CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -2209,7 +2209,7 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
|
|||||||
}],
|
}],
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp: dt(2025, 1, 2, 10, 18, 0),
|
timestamp: dt(2025, 1, 2, 10, 18, 0),
|
||||||
@@ -2222,7 +2222,7 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
|
|||||||
amount_delta: 10_200.0,
|
amount_delta: 10_200.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".to_string(),
|
||||||
timestamp: dt(2025, 1, 2, 10, 18, 0),
|
timestamp: dt(2025, 1, 2, 10, 18, 0),
|
||||||
@@ -2235,7 +2235,7 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
|
|||||||
amount_delta: 20_400.0,
|
amount_delta: 20_400.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp: dt(2025, 1, 2, 10, 19, 0),
|
timestamp: dt(2025, 1, 2, 10, 19, 0),
|
||||||
@@ -2341,7 +2341,7 @@ fn engine_skips_empty_platform_style_minute_callbacks_between_schedule_times() {
|
|||||||
let date = d(2025, 1, 2);
|
let date = d(2025, 1, 2);
|
||||||
let mut data = single_day_anchor_data(date);
|
let mut data = single_day_anchor_data(date);
|
||||||
data.add_execution_quotes(vec![
|
data.add_execution_quotes(vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp: dt(2025, 1, 2, 10, 18, 0),
|
timestamp: dt(2025, 1, 2, 10, 18, 0),
|
||||||
@@ -2354,7 +2354,7 @@ fn engine_skips_empty_platform_style_minute_callbacks_between_schedule_times() {
|
|||||||
amount_delta: 10_200.0,
|
amount_delta: 10_200.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp: dt(2025, 1, 2, 10, 19, 0),
|
timestamp: dt(2025, 1, 2, 10, 19, 0),
|
||||||
@@ -2446,7 +2446,7 @@ fn strategy_context_exposes_engine_native_data_helpers() {
|
|||||||
.map(
|
.map(
|
||||||
|(date, open, close, prev_close, volume)| DailyMarketSnapshot {
|
|(date, open, close, prev_close, volume)| DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some(format!("{date} 10:18:00")),
|
timestamp: Some(format!("{date} 10:18:00")),
|
||||||
day_open: open,
|
day_open: open,
|
||||||
open,
|
open,
|
||||||
@@ -2473,14 +2473,14 @@ fn strategy_context_exposes_engine_native_data_helpers() {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|date| DailyFactorSnapshot {
|
.map(|date| DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 20.0,
|
market_cap_bn: 20.0,
|
||||||
free_float_cap_bn: 18.0,
|
free_float_cap_bn: 18.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let candidates = [
|
let candidates = [
|
||||||
@@ -2491,7 +2491,7 @@ fn strategy_context_exposes_engine_native_data_helpers() {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(date, is_paused, is_st)| CandidateEligibility {
|
.map(|(date, is_paused, is_st)| CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st,
|
is_st,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -2519,7 +2519,7 @@ fn strategy_context_exposes_engine_native_data_helpers() {
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let quotes = vec![
|
let quotes = vec![
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp: dt(2025, 1, 3, 14, 30, 0),
|
timestamp: dt(2025, 1, 3, 14, 30, 0),
|
||||||
@@ -2532,7 +2532,7 @@ fn strategy_context_exposes_engine_native_data_helpers() {
|
|||||||
amount_delta: 10_150.0,
|
amount_delta: 10_150.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: date3,
|
date: date3,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp: dt(2025, 1, 6, 10, 18, 0),
|
timestamp: dt(2025, 1, 6, 10, 18, 0),
|
||||||
@@ -2545,7 +2545,7 @@ fn strategy_context_exposes_engine_native_data_helpers() {
|
|||||||
amount_delta: 10_250.0,
|
amount_delta: 10_250.0,
|
||||||
trading_phase: Some("continuous".to_string()),
|
trading_phase: Some("continuous".to_string()),
|
||||||
},
|
},
|
||||||
IntradayExecutionQuote {
|
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: date3,
|
date: date3,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
timestamp: dt(2025, 1, 6, 10, 19, 0),
|
timestamp: dt(2025, 1, 6, 10, 19, 0),
|
||||||
@@ -2619,7 +2619,7 @@ fn strategy_context_exposes_final_order_runtime_view() {
|
|||||||
}],
|
}],
|
||||||
vec![DailyMarketSnapshot {
|
vec![DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -2642,18 +2642,18 @@ fn strategy_context_exposes_final_order_runtime_view() {
|
|||||||
}],
|
}],
|
||||||
vec![DailyFactorSnapshot {
|
vec![DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 20.0,
|
market_cap_bn: 20.0,
|
||||||
free_float_cap_bn: 18.0,
|
free_float_cap_bn: 18.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
}],
|
}],
|
||||||
vec![CandidateEligibility {
|
vec![CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -2838,7 +2838,7 @@ fn engine_applies_account_cash_flow_and_financing_intents() {
|
|||||||
vec![
|
vec![
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -2861,7 +2861,7 @@ fn engine_applies_account_cash_flow_and_financing_intents() {
|
|||||||
},
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-03 10:18:00".to_string()),
|
timestamp: Some("2025-01-03 10:18:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -2886,31 +2886,31 @@ fn engine_applies_account_cash_flow_and_financing_intents() {
|
|||||||
vec![
|
vec![
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 20.0,
|
market_cap_bn: 20.0,
|
||||||
free_float_cap_bn: 18.0,
|
free_float_cap_bn: 18.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 20.0,
|
market_cap_bn: 20.0,
|
||||||
free_float_cap_bn: 18.0,
|
free_float_cap_bn: 18.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
vec![
|
vec![
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -2923,7 +2923,7 @@ fn engine_applies_account_cash_flow_and_financing_intents() {
|
|||||||
},
|
},
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -3035,7 +3035,7 @@ fn engine_expires_pending_day_limit_orders_at_market_close() {
|
|||||||
vec![
|
vec![
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -3058,7 +3058,7 @@ fn engine_expires_pending_day_limit_orders_at_market_close() {
|
|||||||
},
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-03 10:18:00".to_string()),
|
timestamp: Some("2025-01-03 10:18:00".to_string()),
|
||||||
day_open: 9.7,
|
day_open: 9.7,
|
||||||
open: 9.7,
|
open: 9.7,
|
||||||
@@ -3083,31 +3083,31 @@ fn engine_expires_pending_day_limit_orders_at_market_close() {
|
|||||||
vec![
|
vec![
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 20.0,
|
market_cap_bn: 20.0,
|
||||||
free_float_cap_bn: 18.0,
|
free_float_cap_bn: 18.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 21.0,
|
market_cap_bn: 21.0,
|
||||||
free_float_cap_bn: 19.0,
|
free_float_cap_bn: 19.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
vec![
|
vec![
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -3120,7 +3120,7 @@ fn engine_expires_pending_day_limit_orders_at_market_close() {
|
|||||||
},
|
},
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -3211,7 +3211,7 @@ fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() {
|
|||||||
vec![
|
vec![
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-30 09:25:00".to_string()),
|
timestamp: Some("2025-01-30 09:25:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -3234,7 +3234,7 @@ fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() {
|
|||||||
},
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-31 09:25:00".to_string()),
|
timestamp: Some("2025-01-31 09:25:00".to_string()),
|
||||||
day_open: 10.1,
|
day_open: 10.1,
|
||||||
open: 10.1,
|
open: 10.1,
|
||||||
@@ -3257,7 +3257,7 @@ fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() {
|
|||||||
},
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: date3,
|
date: date3,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-02-03 09:25:00".to_string()),
|
timestamp: Some("2025-02-03 09:25:00".to_string()),
|
||||||
day_open: 10.2,
|
day_open: 10.2,
|
||||||
open: 10.2,
|
open: 10.2,
|
||||||
@@ -3282,42 +3282,42 @@ fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() {
|
|||||||
vec![
|
vec![
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 20.0,
|
market_cap_bn: 20.0,
|
||||||
free_float_cap_bn: 18.0,
|
free_float_cap_bn: 18.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 21.0,
|
market_cap_bn: 21.0,
|
||||||
free_float_cap_bn: 19.0,
|
free_float_cap_bn: 19.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: date3,
|
date: date3,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 22.0,
|
market_cap_bn: 22.0,
|
||||||
free_float_cap_bn: 20.0,
|
free_float_cap_bn: 20.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
vec![
|
vec![
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -3330,7 +3330,7 @@ fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() {
|
|||||||
},
|
},
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -3343,7 +3343,7 @@ fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() {
|
|||||||
},
|
},
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: date3,
|
date: date3,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -3469,7 +3469,7 @@ fn engine_dispatches_process_events_to_external_bus_listeners() {
|
|||||||
vec![
|
vec![
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-30 09:25:00".to_string()),
|
timestamp: Some("2025-01-30 09:25:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -3492,7 +3492,7 @@ fn engine_dispatches_process_events_to_external_bus_listeners() {
|
|||||||
},
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-31 09:25:00".to_string()),
|
timestamp: Some("2025-01-31 09:25:00".to_string()),
|
||||||
day_open: 10.1,
|
day_open: 10.1,
|
||||||
open: 10.1,
|
open: 10.1,
|
||||||
@@ -3515,7 +3515,7 @@ fn engine_dispatches_process_events_to_external_bus_listeners() {
|
|||||||
},
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: date3,
|
date: date3,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-02-03 09:25:00".to_string()),
|
timestamp: Some("2025-02-03 09:25:00".to_string()),
|
||||||
day_open: 10.2,
|
day_open: 10.2,
|
||||||
open: 10.2,
|
open: 10.2,
|
||||||
@@ -3540,42 +3540,42 @@ fn engine_dispatches_process_events_to_external_bus_listeners() {
|
|||||||
vec![
|
vec![
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 20.0,
|
market_cap_bn: 20.0,
|
||||||
free_float_cap_bn: 18.0,
|
free_float_cap_bn: 18.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 21.0,
|
market_cap_bn: 21.0,
|
||||||
free_float_cap_bn: 19.0,
|
free_float_cap_bn: 19.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: date3,
|
date: date3,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 22.0,
|
market_cap_bn: 22.0,
|
||||||
free_float_cap_bn: 20.0,
|
free_float_cap_bn: 20.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
vec![
|
vec![
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: date1,
|
date: date1,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -3588,7 +3588,7 @@ fn engine_dispatches_process_events_to_external_bus_listeners() {
|
|||||||
},
|
},
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: date2,
|
date: date2,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -3601,7 +3601,7 @@ fn engine_dispatches_process_events_to_external_bus_listeners() {
|
|||||||
},
|
},
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: date3,
|
date: date3,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -3855,7 +3855,7 @@ fn engine_applies_dynamic_universe_and_subscription_directives() {
|
|||||||
[
|
[
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: *date,
|
date: *date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some(format!("{date} 10:18:00")),
|
timestamp: Some(format!("{date} 10:18:00")),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -3878,7 +3878,7 @@ fn engine_applies_dynamic_universe_and_subscription_directives() {
|
|||||||
},
|
},
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: *date,
|
date: *date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".into(),
|
||||||
timestamp: Some(format!("{date} 10:18:00")),
|
timestamp: Some(format!("{date} 10:18:00")),
|
||||||
day_open: 20.0,
|
day_open: 20.0,
|
||||||
open: 20.0,
|
open: 20.0,
|
||||||
@@ -3908,25 +3908,25 @@ fn engine_applies_dynamic_universe_and_subscription_directives() {
|
|||||||
[
|
[
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: *date,
|
date: *date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 10.0,
|
market_cap_bn: 10.0,
|
||||||
free_float_cap_bn: 8.0,
|
free_float_cap_bn: 8.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: *date,
|
date: *date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".into(),
|
||||||
market_cap_bn: 12.0,
|
market_cap_bn: 12.0,
|
||||||
free_float_cap_bn: 10.0,
|
free_float_cap_bn: 10.0,
|
||||||
pe_ttm: 12.0,
|
pe_ttm: 12.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
@@ -3937,7 +3937,7 @@ fn engine_applies_dynamic_universe_and_subscription_directives() {
|
|||||||
[
|
[
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: *date,
|
date: *date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -3950,7 +3950,7 @@ fn engine_applies_dynamic_universe_and_subscription_directives() {
|
|||||||
},
|
},
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: *date,
|
date: *date,
|
||||||
symbol: "000002.SZ".to_string(),
|
symbol: "000002.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -4041,7 +4041,7 @@ fn engine_exposes_current_process_context_to_strategies() {
|
|||||||
}],
|
}],
|
||||||
vec![DailyMarketSnapshot {
|
vec![DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -4064,18 +4064,18 @@ fn engine_exposes_current_process_context_to_strategies() {
|
|||||||
}],
|
}],
|
||||||
vec![DailyFactorSnapshot {
|
vec![DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
market_cap_bn: 20.0,
|
market_cap_bn: 20.0,
|
||||||
free_float_cap_bn: 18.0,
|
free_float_cap_bn: 18.0,
|
||||||
pe_ttm: 10.0,
|
pe_ttm: 10.0,
|
||||||
turnover_ratio: Some(1.0),
|
turnover_ratio: Some(1.0),
|
||||||
effective_turnover_ratio: Some(1.0),
|
effective_turnover_ratio: Some(1.0),
|
||||||
adjustment_factor_backward1: None,
|
adjustment_factor_backward1: None,
|
||||||
extra_factors: BTreeMap::new(),
|
extra_factors: Default::default(),
|
||||||
}],
|
}],
|
||||||
vec![CandidateEligibility {
|
vec![CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -4191,8 +4191,8 @@ fn engine_rejects_an_unexplained_missing_holding_close() {
|
|||||||
market_row(date2, "000001.SZ", 20.0, 20.2),
|
market_row(date2, "000001.SZ", 20.0, 20.2),
|
||||||
],
|
],
|
||||||
vec![
|
vec![
|
||||||
factor_row(date1, "601028.SH", BTreeMap::new()),
|
factor_row(date1, "601028.SH", BTreeMap::new().into()),
|
||||||
factor_row(date2, "000001.SZ", BTreeMap::new()),
|
factor_row(date2, "000001.SZ", BTreeMap::new().into()),
|
||||||
],
|
],
|
||||||
vec![
|
vec![
|
||||||
candidate_row(date1, "601028.SH"),
|
candidate_row(date1, "601028.SH"),
|
||||||
@@ -4269,8 +4269,8 @@ fn platform_strategy_cannot_hide_missing_valuation_by_skipping_stop_take() {
|
|||||||
market_row(date2, "000001.SZ", 20.0, 20.2),
|
market_row(date2, "000001.SZ", 20.0, 20.2),
|
||||||
],
|
],
|
||||||
vec![
|
vec![
|
||||||
factor_row(date1, "601028.SH", BTreeMap::new()),
|
factor_row(date1, "601028.SH", BTreeMap::new().into()),
|
||||||
factor_row(date2, "000001.SZ", BTreeMap::new()),
|
factor_row(date2, "000001.SZ", BTreeMap::new().into()),
|
||||||
],
|
],
|
||||||
vec![
|
vec![
|
||||||
candidate_row(date1, "601028.SH"),
|
candidate_row(date1, "601028.SH"),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@ fn dataset(day_count: usize, bars_per_day: usize) -> (DataSet, Vec<NaiveDate>) {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|date| DailyMarketSnapshot {
|
.map(|date| DailyMarketSnapshot {
|
||||||
date: *date,
|
date: *date,
|
||||||
symbol: SYMBOL.to_string(),
|
symbol: SYMBOL.into(),
|
||||||
timestamp: None,
|
timestamp: None,
|
||||||
day_open: 10.0,
|
day_open: 10.0,
|
||||||
open: 10.0,
|
open: 10.0,
|
||||||
@@ -55,7 +55,7 @@ fn dataset(day_count: usize, bars_per_day: usize) -> (DataSet, Vec<NaiveDate>) {
|
|||||||
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 {
|
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 { observation_kind: Default::default(),
|
||||||
date: *date,
|
date: *date,
|
||||||
symbol: SYMBOL.to_string(),
|
symbol: SYMBOL.to_string(),
|
||||||
timestamp,
|
timestamp,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use fidc_core::{
|
|||||||
PortfolioState, PriceField, StrategyDecision, platform_expr_config_from_value,
|
PortfolioState, PriceField, StrategyDecision, platform_expr_config_from_value,
|
||||||
};
|
};
|
||||||
use rust_decimal::Decimal;
|
use rust_decimal::Decimal;
|
||||||
|
use fidc_core::IntradayExecutionQuote;
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
fn day(n: u32) -> NaiveDate {
|
fn day(n: u32) -> NaiveDate {
|
||||||
@@ -66,7 +67,7 @@ fn data_with_fund_rules(
|
|||||||
};
|
};
|
||||||
market.push(DailyMarketSnapshot {
|
market.push(DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: code(n),
|
symbol: code(n).into(),
|
||||||
timestamp: None,
|
timestamp: None,
|
||||||
day_open: price,
|
day_open: price,
|
||||||
open: price,
|
open: price,
|
||||||
@@ -89,7 +90,7 @@ fn data_with_fund_rules(
|
|||||||
});
|
});
|
||||||
candidates.push(CandidateEligibility {
|
candidates.push(CandidateEligibility {
|
||||||
date,
|
date,
|
||||||
symbol: code(n),
|
symbol: code(n).into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing: false,
|
is_new_listing: false,
|
||||||
@@ -131,7 +132,7 @@ fn data_with_fund_rules(
|
|||||||
.flat_map(|date| {
|
.flat_map(|date| {
|
||||||
(1..=2).map(move |n| DailyFactorSnapshot {
|
(1..=2).map(move |n| DailyFactorSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: code(n),
|
symbol: code(n).into(),
|
||||||
market_cap_bn: 10.,
|
market_cap_bn: 10.,
|
||||||
free_float_cap_bn: 10.,
|
free_float_cap_bn: 10.,
|
||||||
pe_ttm: 10.,
|
pe_ttm: 10.,
|
||||||
@@ -142,8 +143,8 @@ fn data_with_fund_rules(
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let quotes = market.iter().filter(|row| row.symbol != "000300.SH").map(|row| fidc_core::IntradayExecutionQuote {
|
let quotes = market.iter().filter(|row| row.symbol != "000300.SH").map(|row| fidc_core::IntradayExecutionQuote { observation_kind: Default::default(),
|
||||||
date: row.date, symbol: row.symbol.clone(), timestamp: row.date.and_hms_opt(9, 30, 0).unwrap(),
|
date: row.date, symbol: row.symbol.to_string(), timestamp: row.date.and_hms_opt(9, 30, 0).unwrap(),
|
||||||
last_price: row.open, bid1: row.open, ask1: row.open, bid1_volume: 0, ask1_volume: 0,
|
last_price: row.open, bid1: row.open, ask1: row.open, bid1_volume: 0, ask1_volume: 0,
|
||||||
volume_delta: row.volume, amount_delta: row.open * row.volume as f64,
|
volume_delta: row.volume, amount_delta: row.open * row.volume as f64,
|
||||||
trading_phase: Some("synthetic_observation_fixture".into()),
|
trading_phase: Some("synthetic_observation_fixture".into()),
|
||||||
@@ -489,6 +490,72 @@ fn repeating_the_same_partial_exit_generation_does_not_reduce_again() {
|
|||||||
assert_eq!(new_signal.fill_events.iter().filter(|fill|fill.symbol==code(1)).map(|fill|fill.quantity).sum::<u32>(),300);
|
assert_eq!(new_signal.fill_events.iter().filter(|fill|fill.symbol==code(1)).map(|fill|fill.quantity).sum::<u32>(),300);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn daily_execution_price_does_not_satisfy_an_unobserved_order_book_condition() {
|
||||||
|
let data = data(false);
|
||||||
|
for field in ["bid1", "ask1"] {
|
||||||
|
let broker = broker(false);
|
||||||
|
let mut account = PortfolioState::new(30000.);
|
||||||
|
let mut intent = contract(day(2), 1, false);
|
||||||
|
intent.rule.trigger_mode = "condition".into();
|
||||||
|
intent.rule.buy_condition = format!("{field}>0");
|
||||||
|
let result = broker.execute_with_event_dates(day(5), day(2), day(2), &mut account, &data, &decision(intent));
|
||||||
|
assert!(result.unwrap_err().to_string().contains(field));
|
||||||
|
assert!(account.positions().is_empty());
|
||||||
|
assert_eq!(account.cash(), 30000.);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cumulative_conditions_do_not_consume_future_bars_or_inflate_fill_capacity() {
|
||||||
|
let mut data = data(false);
|
||||||
|
let mut quotes = Vec::new();
|
||||||
|
for n in 1..=2 {
|
||||||
|
let price = if n == 1 {20.} else {10.};
|
||||||
|
for (minute, volume) in [(30,600), (31,0), (32,400)] {
|
||||||
|
quotes.push(IntradayExecutionQuote {
|
||||||
|
observation_kind: fidc_core::data::QuoteObservationKind::MinuteBar,
|
||||||
|
date: day(5), symbol: code(n), timestamp: day(5).and_hms_opt(9,minute,0).unwrap(),
|
||||||
|
last_price: price, bid1: 0., ask1: 0., bid1_volume: 0, ask1_volume: 0,
|
||||||
|
volume_delta: volume, amount_delta: volume as f64 * price, trading_phase: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data.replace_execution_quotes(quotes.clone());
|
||||||
|
let at = chrono::NaiveTime::from_hms_opt(9,32,0).unwrap();
|
||||||
|
for condition in ["volume>=1000", "amount>=20000"] {
|
||||||
|
let broker=broker(true).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(at);
|
||||||
|
let mut account=PortfolioState::new(30000.);
|
||||||
|
let mut intent=contract(day(5),1,false);
|
||||||
|
intent.rule.buy_condition=condition.into();intent.rule.trigger_mode="condition".into();
|
||||||
|
let report=broker.execute_with_event_dates(day(5),day(5),day(5),&mut account,&data,&decision(intent)).unwrap();
|
||||||
|
assert_eq!(report.fill_events.iter().map(|fill|fill.quantity).sum::<u32>(),100,"{condition}: {report:?}");
|
||||||
|
assert_eq!(data.execution_quotes_on(day(5),&code(1))[2].volume_delta,400);
|
||||||
|
}
|
||||||
|
let mut future=quotes.last().unwrap().clone();future.symbol=code(1);future.timestamp=day(5).and_hms_opt(9,33,0).unwrap();future.volume_delta=9000;future.amount_delta=180000.;
|
||||||
|
data.add_execution_quotes(vec![future]);
|
||||||
|
let broker=broker(false).with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(at);
|
||||||
|
let mut account=PortfolioState::new(30000.);
|
||||||
|
let mut intent=contract(day(5),1,false);intent.rule.buy_condition="volume>1000".into();intent.rule.trigger_mode="condition".into();
|
||||||
|
let report=broker.execute_with_event_dates(day(5),day(5),day(5),&mut account,&data,&decision(intent)).unwrap();
|
||||||
|
assert!(report.fill_events.is_empty(),"future volume must not satisfy this signal: {report:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn session_total_cache_is_invalidated_without_mutating_other_dataset_clones() {
|
||||||
|
let mut original=data(false);
|
||||||
|
let quote=IntradayExecutionQuote { observation_kind:fidc_core::data::QuoteObservationKind::MinuteBar,date:day(5),symbol:code(1),timestamp:day(5).and_hms_opt(9,30,0).unwrap(),last_price:20.,bid1:0.,ask1:0.,bid1_volume:0,ask1_volume:0,volume_delta:100,amount_delta:2000.,trading_phase:None };
|
||||||
|
original.replace_execution_quotes(vec![quote.clone()]);
|
||||||
|
assert_eq!(original.execution_session_totals(&code(1),quote.timestamp).unwrap().0,Decimal::from(100));
|
||||||
|
let mut changed=original.clone();let mut next=quote.clone();next.timestamp=day(5).and_hms_opt(9,31,0).unwrap();
|
||||||
|
changed.add_execution_quotes(vec![next.clone()]);
|
||||||
|
assert_eq!(changed.execution_session_totals(&code(1),next.timestamp).unwrap().0,Decimal::from(200));
|
||||||
|
assert!(original.execution_session_totals(&code(1),next.timestamp).is_err());
|
||||||
|
changed.remove_execution_quotes_on_date(day(5));
|
||||||
|
assert!(changed.execution_session_totals(&code(1),quote.timestamp).is_err());
|
||||||
|
assert_eq!(original.execution_session_totals(&code(1),quote.timestamp).unwrap().0,Decimal::from(100));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parsed_pool_program_executes_daily_membership_changes_without_legacy_translation() {
|
fn parsed_pool_program_executes_daily_membership_changes_without_legacy_translation() {
|
||||||
let intent = contract(day(2), 1, false);
|
let intent = contract(day(2), 1, false);
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ fn market(
|
|||||||
) -> DailyMarketSnapshot {
|
) -> DailyMarketSnapshot {
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: d(date),
|
date: d(date),
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
timestamp: None,
|
timestamp: None,
|
||||||
day_open: open,
|
day_open: open,
|
||||||
open,
|
open,
|
||||||
@@ -66,7 +66,7 @@ fn factor(
|
|||||||
) -> DailyFactorSnapshot {
|
) -> DailyFactorSnapshot {
|
||||||
DailyFactorSnapshot {
|
DailyFactorSnapshot {
|
||||||
date: d(date),
|
date: d(date),
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
market_cap_bn,
|
market_cap_bn,
|
||||||
free_float_cap_bn,
|
free_float_cap_bn,
|
||||||
pe_ttm: 18.0,
|
pe_ttm: 18.0,
|
||||||
@@ -87,7 +87,7 @@ fn candidate(
|
|||||||
) -> CandidateEligibility {
|
) -> CandidateEligibility {
|
||||||
CandidateEligibility {
|
CandidateEligibility {
|
||||||
date: d(date),
|
date: d(date),
|
||||||
symbol: symbol.to_string(),
|
symbol: symbol.into(),
|
||||||
is_st: false,
|
is_st: false,
|
||||||
is_star_st: false,
|
is_star_st: false,
|
||||||
is_new_listing,
|
is_new_listing,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,109 @@
|
|||||||
|
# 表达式上下文性能验收
|
||||||
|
|
||||||
|
## 范围
|
||||||
|
|
||||||
|
本轮优化 FIDC 引擎的逐股票表达式上下文,不修改策略、因子值、窗口、时间可见性、
|
||||||
|
选股/订单规则、费用、成交价、风控或公司行为。Source 保持 `d5b682c6d097`,
|
||||||
|
研究和信号保持暂停。其他用户任务只读观察,不更改其进程、亲和性或配置。
|
||||||
|
|
||||||
|
已完成编译、回归、正式回放与177发布验收,不能据此关闭整个目标。
|
||||||
|
|
||||||
|
## 重复开销
|
||||||
|
|
||||||
|
1. DataSet 已采用 `NumericFactorMap`,但 `StockExpressionState` 仍把数值因子
|
||||||
|
重建为 `BTreeMap<String, f64>`,增加树节点和字符串分配。
|
||||||
|
2. 每日可用因子名集合先为每个证券复制名称,再由集合丢弃重复名称。
|
||||||
|
|
||||||
|
候选在表达式上下文延续紧凑数值存储。每日名称仍按当日真实字段生成完整集合,
|
||||||
|
仅改为先对借用名称去重,再为唯一名称分配字符串;文本因子同理。
|
||||||
|
没有用全局/未来日期目录替代当日字段,没有缓存选股结果或账户状态。
|
||||||
|
Rhai `factors[...]`、缺失、NaN、负零、别名、覆盖顺序与已完成交易日可见性保持原行为。
|
||||||
|
|
||||||
|
## CPU 计时
|
||||||
|
|
||||||
|
Runner 新增以下运行计时,HTTP benchmark 同样保留:
|
||||||
|
|
||||||
|
- `engineTaskWallSeconds`:实际引擎同步任务的墙钟耗时,包含其同步数据读取等待。
|
||||||
|
- `engineThreadCpuSeconds`:Linux `CLOCK_THREAD_CPUTIME_ID` 实测的引擎调用线程CPU时间。
|
||||||
|
不包含其他并行线程、I/O等待或未被调度的时间;不可当作整个进程总CPU时间。
|
||||||
|
读取不可用、跨线程或时钟异常保持null,不填0。
|
||||||
|
- `engineTaskCount`:实际执行引擎的次数,包含原有修复循环的重放。
|
||||||
|
|
||||||
|
这些是 `engineSeconds` 的子指标,禁止再次加到总耗时。正常耗时与诊断运行分开保存。
|
||||||
|
计时不改写历史制品,旧记录缺少该指标时保持未知。
|
||||||
|
|
||||||
|
## 验收设置
|
||||||
|
|
||||||
|
- 固定引擎基线 `fe7243b`;候选为177的 `07b7b18`,对应本地 `df1862e`。
|
||||||
|
- 两边使用同一计时版service `a9df11a`;`b5d22ff`仅补充benchmark字段读取。
|
||||||
|
- 2021-08-23至2025-11-17、初始1000万、原冻结runtime与策略,1025个执行交易日。
|
||||||
|
- 保留原 `session_capacity_audit`,不能当作实际开盘流动性验证。
|
||||||
|
- 官方benchmark入口、Boris执行、同CPU资源与Source版本、新进程、相同数据缓存副本、
|
||||||
|
新结果目录,不复用回测结果。
|
||||||
|
- 引擎780项、runner408项、API113项、脚本10项通过;9/8/3项手动或外部环境用例分别忽略。
|
||||||
|
- 专项延伸验证紧凑因子的克隆、Rhai映射暴露、缺失、NaN及负零;CPU计时验证睡眠和跨线程边界。
|
||||||
|
|
||||||
|
证据根:`/srv/fidc/canonical/run/research/engine-context-20260913`。
|
||||||
|
|
||||||
|
## 独立进程对照
|
||||||
|
|
||||||
|
| 次序 | 样本 | 完整墙钟 | Source校验 | 数据准备 | 引擎墙钟 | 引擎线程CPU |
|
||||||
|
|---|---|---:|---:|---:|---:|---:|
|
||||||
|
| 1 | control-1 | 31.234s | 11.202s | 8.393s | 10.461s | 10.458s |
|
||||||
|
| 2 | candidate-1 | 17.002s | 0.004s | 8.404s | 7.406s | 7.404s |
|
||||||
|
| 3 | candidate-2 | 18.203s | 0.003s | 8.371s | 7.411s | 7.408s |
|
||||||
|
| 4 | control-2 | 22.983s | 0.004s | 8.323s | 13.401s | 13.398s |
|
||||||
|
| 5 | control-3 | 30.714s | 0.005s | 15.318s | 13.999s | 13.990s |
|
||||||
|
| 6 | candidate-3 | 25.336s | 0.005s | 13.524s | 10.471s | 10.468s |
|
||||||
|
|
||||||
|
首个基线的Source校验等待原样保留,不事后改称预热,不把11.202秒归因于引擎改动。
|
||||||
|
后段样本出现主机负载/缓存竞争变化,数据准备也变慢,不能直接用全组平均墙钟夸大提速。
|
||||||
|
相邻低负载对照的引擎线程CPU为10.458至7.404秒,后段为13.990至10.468秒。
|
||||||
|
CPU计时与任务墙钟非常接近,证明样本主要在执行CPU工作,而不是等待HTTP;
|
||||||
|
这不代表没有SMT、内存带宽或其他用户CPU竞争。
|
||||||
|
|
||||||
|
六次均为21,393笔成交,账户、权益、委托、成交、持仓和风控canonical及结果制品完全一致。
|
||||||
|
每份63个数据缓存文件经完整SHA核对相同,没有复制或读取旧回测结果。
|
||||||
|
|
||||||
|
## HTTP 对照
|
||||||
|
|
||||||
|
| 状态 | 版本 | 运行ID | 总耗时 | 引擎耗时 |
|
||||||
|
|---|---|---|---:|---:|
|
||||||
|
| 清DataSet,磁盘/Source保持 | 原版 | btr_1789232559582_3166774_4 | 21.987s | 11.328s |
|
||||||
|
| 清DataSet,磁盘/Source保持 | 原版 | btr_1789232585690_3166774_5 | 21.684s | 11.259s |
|
||||||
|
| 复用DataSet | 原版 | btr_1789232669598_3166774_6 | 11.820s | 11.031s |
|
||||||
|
| 复用DataSet | 原版 | btr_1789232684861_3166774_7 | 11.857s | 11.067s |
|
||||||
|
| 清DataSet,磁盘/Source保持 | 新版 | btr_1789232818009_3320588_0 | 17.296s | 7.537s |
|
||||||
|
| 清DataSet,磁盘/Source保持 | 新版 | btr_1789232839269_3320588_1 | 17.413s | 7.627s |
|
||||||
|
| 复用DataSet | 新版 | btr_1789232898983_3320588_2 | 8.549s | 7.738s |
|
||||||
|
| 复用DataSet | 新版 | btr_1789232910904_3320588_3 | 8.586s | 7.784s |
|
||||||
|
|
||||||
|
同状态HTTP均值:重建DataSet从21.836至17.355秒,减少约20.5%;
|
||||||
|
复用DataSet从11.839至8.568秒,减少约27.6%。两种状态分开比较,
|
||||||
|
没有把8.568秒当作Source冷启动成绩。与上一轮不同时间的15/17秒样本不作直接百分比对比。
|
||||||
|
|
||||||
|
原版API没有线程CPU字段,保持null;新版本每次实际执行引擎一次,
|
||||||
|
两次重建的线程CPU为7.535/7.624秒。没有用新版本计时回填旧记录。
|
||||||
|
八次HTTP和六次独立回放的canonical及结果制品SHA全部相同,终态审计clean。
|
||||||
|
|
||||||
|
## 发布状态
|
||||||
|
|
||||||
|
177通过官方安装器发布 engine `07b7b181b60138c6ef1c965543c0e3192ac65903`、
|
||||||
|
service `b5d22ffab16f851eced3028e12fa02627ee4c399`。
|
||||||
|
运行身份 `fdd8652a47a5935be4d891beb3b8b0f3e19a468be166a902a2a97b85a9c9e01e`。
|
||||||
|
|
||||||
|
- API SHA:`bf22f58946c3fa495161eb381a400d4e28d7c8d327ee46f5645d83a8308117cf`。
|
||||||
|
- Runner SHA:`7b3849cd8af33d650db242add80c49cfdd32e8cc8686a614da7b3b4016ce2a60`。
|
||||||
|
- 生产在用构建根:`/srv/fidc/canonical/build/engine-context-candidate-20260913`,禁止清理。
|
||||||
|
- 原生因子能力目录发布前后字节相同,SHA为
|
||||||
|
`cec37331a476bc39bdea32c308581b8ac2f86d005d8dd4cc7ba228c5d9dc9a2e`。
|
||||||
|
- API PID3320588,Boris、active、NRestarts=0;Source仍为PID1700096/d5,研究未恢复。
|
||||||
|
|
||||||
|
[完整结构化验收证据](evidence/expression-context-performance-20260913.json),
|
||||||
|
SHA256 `f526950e018354c1305922beebf4063ae3823004f8c5ab20510a452f98b7b7ea`。
|
||||||
|
|
||||||
|
## 边界
|
||||||
|
|
||||||
|
本轮真实长区间案例含一个原生扩展因子,动态映射、缺失及多字段语义另由引擎回归覆盖;
|
||||||
|
不宣称所有策略都具有相同比例提速。Source冷路径仍受独立冻结约束,
|
||||||
|
信号闭环和全部策略/分钟区间/财务PIT不在本轮通过范围内。
|
||||||
Reference in New Issue
Block a user