merge: retain audited signal identity and cross-language semantic hashing
This commit is contained in:
@@ -3375,6 +3375,12 @@ impl DataSet {
|
|||||||
.unwrap_or(&[])
|
.unwrap_or(&[])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_reference_only_benchmark(&self, symbol: &str) -> bool {
|
||||||
|
if symbol != self.benchmark_code() { return false; }
|
||||||
|
let Some(symbol_id) = self.symbol_id(symbol) else { return true; };
|
||||||
|
!self.candidate_symbol_ids_by_date.values().any(|ids| ids.contains(&symbol_id))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn bundle_on(&self, date: NaiveDate) -> Result<DailySnapshotBundle, DataSetError> {
|
pub fn bundle_on(&self, date: NaiveDate) -> Result<DailySnapshotBundle, DataSetError> {
|
||||||
let benchmark = self
|
let benchmark = self
|
||||||
.benchmark(date)
|
.benchmark(date)
|
||||||
|
|||||||
@@ -474,7 +474,9 @@ pub struct BacktestEngine<S, C, R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn all_instruments_have_dated_absence(data: &DataSet, date: NaiveDate) -> bool {
|
fn all_instruments_have_dated_absence(data: &DataSet, date: NaiveDate) -> bool {
|
||||||
!data.instruments().is_empty() && data.instruments().values().all(|instrument| instrument.dated_market_absence_reason(date).is_some())
|
let mut instruments = data.instruments().values()
|
||||||
|
.filter(|instrument| !data.is_reference_only_benchmark(&instrument.symbol)).peekable();
|
||||||
|
instruments.peek().is_some() && instruments.all(|instrument| instrument.dated_market_absence_reason(date).is_some())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn backtest_execution_schedule(
|
fn backtest_execution_schedule(
|
||||||
@@ -5580,9 +5582,11 @@ mod tests {
|
|||||||
let dates = [d(2025, 1, 2), d(2025, 1, 3), d(2025, 1, 6)];
|
let dates = [d(2025, 1, 2), d(2025, 1, 3), d(2025, 1, 6)];
|
||||||
let mut engine = engine_with_matching(MatchingType::CurrentBarClose, PriceField::Close, 0);
|
let mut engine = engine_with_matching(MatchingType::CurrentBarClose, PriceField::Close, 0);
|
||||||
engine.config.end_date = Some(dates[2]);
|
engine.config.end_date = Some(dates[2]);
|
||||||
|
let mut markets = vec![market(dates[2], 10.0, 10.0)];
|
||||||
|
markets.extend(dates.iter().map(|date| DailyMarketSnapshot { symbol: "000852.SH".into(), ..market(*date, 1000.0, 1000.0) }));
|
||||||
engine.data = DataSet::from_components(
|
engine.data = DataSet::from_components(
|
||||||
vec![Instrument { listed_at: Some(dates[2]), ..default_instrument() }],
|
vec![Instrument { listed_at: Some(dates[2]), ..default_instrument() }, Instrument { symbol: "000852.SH".into(), listed_at: None, ..default_instrument() }],
|
||||||
vec![market(dates[2], 10.0, 10.0)], vec![factor(dates[2])], vec![candidate(dates[2])],
|
markets, vec![factor(dates[2])], vec![candidate(dates[2])],
|
||||||
dates.iter().map(|date| benchmark(*date)).collect(),
|
dates.iter().map(|date| benchmark(*date)).collect(),
|
||||||
).unwrap();
|
).unwrap();
|
||||||
assert_eq!(super::backtest_execution_dates(&engine.data, Some(dates[0]), Some(dates[2]), 0), dates);
|
assert_eq!(super::backtest_execution_dates(&engine.data, Some(dates[0]), Some(dates[2]), 0), dates);
|
||||||
|
|||||||
@@ -3857,16 +3857,9 @@ impl PlatformExprStrategy {
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !defer_execution_risk
|
if !defer_execution_risk && self.buy_rejection_reason(
|
||||||
&& self
|
ctx, execution_date, symbol, self.stock_state(ctx, execution_date, symbol)?.as_ref(),
|
||||||
.buy_rejection_reason(
|
)?.is_some() {
|
||||||
ctx,
|
|
||||||
execution_date,
|
|
||||||
symbol,
|
|
||||||
self.stock_state(ctx, execution_date, symbol)?.as_ref(),
|
|
||||||
)?
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let decision_stock = self.stock_state_with_factor_date(
|
let decision_stock = self.stock_state_with_factor_date(
|
||||||
@@ -14036,16 +14029,10 @@ impl PlatformExprStrategy {
|
|||||||
if target_value <= 0.0 {
|
if target_value <= 0.0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !defer_execution_risk
|
if !defer_execution_risk && let Some(reason) = self.buy_rejection_reason(
|
||||||
&& self
|
ctx, execution_date, symbol, self.stock_state(ctx, execution_date, symbol)?.as_ref(),
|
||||||
.buy_rejection_reason(
|
)? {
|
||||||
ctx,
|
risk_decisions.push(FidcRiskDecisionAudit::rejected_buy_plan(execution_date, symbol, &reason));
|
||||||
execution_date,
|
|
||||||
symbol,
|
|
||||||
self.stock_state(ctx, execution_date, symbol)?.as_ref(),
|
|
||||||
)?
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !self.stock_passes_expr(ctx, &day, &decision_stock)? {
|
if !self.stock_passes_expr(ctx, &day, &decision_stock)? {
|
||||||
@@ -14322,6 +14309,37 @@ mod tests {
|
|||||||
assert_eq!(strategy.selection_quote_usage, StockFilterQuoteUsage::DailyOnly);
|
assert_eq!(strategy.selection_quote_usage, StockFilterQuoteUsage::DailyOnly);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn periodic_selected_bjse_buy_rejection_is_audited_without_creating_an_order() {
|
||||||
|
let dates = [d(2026, 8, 5), d(2026, 8, 6)];
|
||||||
|
let symbol = "920038.BJ";
|
||||||
|
let data = single_symbol_platform_data(&dates, symbol);
|
||||||
|
let portfolio = PortfolioState::new(100_000.0);
|
||||||
|
let subscriptions = BTreeSet::new();
|
||||||
|
let ctx = StrategyContext {
|
||||||
|
execution_date: dates[1], decision_date: dates[1], decision_index: 1, data: &data,
|
||||||
|
portfolio: &portfolio, futures_account: None, open_orders: &[], dynamic_universe: None,
|
||||||
|
subscriptions: &subscriptions, process_events: &[], active_process_event: None,
|
||||||
|
active_datetime: None, order_events: &[], fills: &[],
|
||||||
|
};
|
||||||
|
let mut cfg = PlatformExprStrategyConfig::generic();
|
||||||
|
cfg.signal_symbol = symbol.into();
|
||||||
|
cfg.stock_filter_expr = "close > 0".into();
|
||||||
|
cfg.hold_until_exit_enabled = true;
|
||||||
|
cfg.target_portfolio_daily_enabled = true;
|
||||||
|
cfg.daily_top_up_enabled = true;
|
||||||
|
cfg.daily_position_target_adjust_enabled = true;
|
||||||
|
cfg.rebalance_existing_positions = true;
|
||||||
|
cfg.risk_config.static_rules.reject_bjse_selection = false;
|
||||||
|
cfg.risk_config.static_rules.reject_bjse_buy = true;
|
||||||
|
let decision = PlatformExprStrategy::new(cfg.clone()).on_day(&ctx).unwrap();
|
||||||
|
assert!(decision.order_intents.is_empty());
|
||||||
|
assert!(decision.risk_decisions.iter().any(|audit| audit.symbol == symbol && audit.stage == "buy_planning" && audit.rule_code == "bjse" && !audit.accepted));
|
||||||
|
cfg.risk_config.static_rules.reject_bjse_buy = false;
|
||||||
|
let allowed = PlatformExprStrategy::new(cfg).on_day(&ctx).unwrap();
|
||||||
|
assert!(!allowed.order_intents.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn daily_pattern_runtime_uses_the_shared_kernel_and_rejects_early_visibility() {
|
fn daily_pattern_runtime_uses_the_shared_kernel_and_rejects_early_visibility() {
|
||||||
let dates=(0..21).map(|n|d(2025,1,1)+chrono::Duration::days(n)).collect::<Vec<_>>();
|
let dates=(0..21).map(|n|d(2025,1,1)+chrono::Duration::days(n)).collect::<Vec<_>>();
|
||||||
|
|||||||
@@ -138,6 +138,16 @@ pub struct FidcRiskDecisionAudit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FidcRiskDecisionAudit {
|
impl FidcRiskDecisionAudit {
|
||||||
|
pub fn rejected_buy_plan(date: NaiveDate, symbol: &str, reason: &str) -> Self {
|
||||||
|
Self {
|
||||||
|
date, symbol: symbol.into(), scope: RiskCheckScope::Buy,
|
||||||
|
stage: "buy_planning".into(), accepted: false,
|
||||||
|
rule_code: reason.into(), reason: reason.into(),
|
||||||
|
config_version: Some("inline_risk_policy".into()), data_epoch: date.to_string(),
|
||||||
|
selection_batch_id: None, order_id: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn rejected_selection(
|
pub fn rejected_selection(
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
symbol: impl Into<String>,
|
symbol: impl Into<String>,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use std::sync::{Arc, Mutex, OnceLock, Weak};
|
|||||||
|
|
||||||
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc};
|
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Utc};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
use crate::strategy::{OrderIntent, StrategyContext};
|
use crate::strategy::{OrderIntent, StrategyContext};
|
||||||
use crate::portfolio::PortfolioState;
|
use crate::portfolio::PortfolioState;
|
||||||
@@ -150,6 +151,39 @@ fn shanghai(value: DateTime<Utc>) -> NaiveDateTime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SignalBook {
|
impl SignalBook {
|
||||||
|
pub fn content_sha256(&self) -> Result<String, String> {
|
||||||
|
let mut value=serde_json::to_value(self).map_err(|error|error.to_string())?;
|
||||||
|
value.as_object_mut().ok_or("signal_book_object_required")?.remove("versionSha256");
|
||||||
|
value["knowledgeCutoff"]=self.knowledge_cutoff.map(|at|serde_json::json!(at.timestamp_micros())).unwrap_or(serde_json::Value::Null);
|
||||||
|
value["expectedDecisions"]=serde_json::json!(self.expected_decisions.iter().map(DateTime::timestamp_micros).collect::<Vec<_>>());
|
||||||
|
for (raw,snapshot) in value["snapshots"].as_array_mut().ok_or("signal_snapshots_required")?.iter_mut().zip(&self.snapshots) {
|
||||||
|
let object=raw.as_object_mut().ok_or("signal_snapshot_required")?;
|
||||||
|
object.remove("generatedAt");
|
||||||
|
object.remove("publishedAt");
|
||||||
|
for (key,at) in [("signalAt",snapshot.signal_at),("decisionAt",snapshot.decision_at),
|
||||||
|
("inputAsOf",snapshot.input_as_of),("inputAvailableAt",snapshot.input_available_at)] {
|
||||||
|
object.insert(key.into(),serde_json::json!(at.timestamp_micros()));
|
||||||
|
}
|
||||||
|
for (raw,action) in object.get_mut("actions").and_then(serde_json::Value::as_array_mut).ok_or("signal_actions_required")?.iter_mut().zip(&snapshot.actions) {
|
||||||
|
match action {
|
||||||
|
SignalAction::TargetWeight{weight,..}=>raw["weight"]=serde_json::json!(format!("{:016x}",weight.to_bits())),
|
||||||
|
SignalAction::Reduce{remaining_ratio,..}=>raw["remaining_ratio"]=serde_json::json!(format!("{:016x}",remaining_ratio.to_bits())),
|
||||||
|
_=>{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn sorted(value:serde_json::Value)->serde_json::Value {
|
||||||
|
match value {
|
||||||
|
serde_json::Value::Object(map)=>serde_json::Value::Object(map.into_iter().map(|(key,value)|(key,sorted(value)))
|
||||||
|
.collect::<BTreeMap<_,_>>().into_iter().collect()),
|
||||||
|
serde_json::Value::Array(rows)=>serde_json::Value::Array(rows.into_iter().map(sorted).collect()),
|
||||||
|
other=>other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let raw=serde_json::to_vec(&sorted(value)).map_err(|error|error.to_string())?;
|
||||||
|
Ok(format!("{:x}",Sha256::digest(raw)))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn validate(self) -> Result<ValidatedSignalBook, String> {
|
pub fn validate(self) -> Result<ValidatedSignalBook, String> {
|
||||||
if self.schema != SIGNAL_BOOK_SCHEMA || !valid_sha(&self.version_sha256)
|
if self.schema != SIGNAL_BOOK_SCHEMA || !valid_sha(&self.version_sha256)
|
||||||
|| !valid_sha(&self.generator_sha256)
|
|| !valid_sha(&self.generator_sha256)
|
||||||
@@ -168,6 +202,10 @@ impl SignalBook {
|
|||||||
let mut previous = None;
|
let mut previous = None;
|
||||||
let mut total_actions = 0usize;
|
let mut total_actions = 0usize;
|
||||||
for (number, (expected, snapshot)) in self.expected_decisions.iter().zip(&self.snapshots).enumerate() {
|
for (number, (expected, snapshot)) in self.expected_decisions.iter().zip(&self.snapshots).enumerate() {
|
||||||
|
if [*expected,snapshot.signal_at,snapshot.input_as_of,snapshot.input_available_at,snapshot.generated_at,snapshot.published_at]
|
||||||
|
.iter().any(|at|at.timestamp_subsec_nanos()%1000!=0) || self.knowledge_cutoff.is_some_and(|at|at.timestamp_subsec_nanos()%1000!=0) {
|
||||||
|
return Err("signal_timestamp_requires_microsecond_precision".into());
|
||||||
|
}
|
||||||
if snapshot.decision_at != *expected || previous.is_some_and(|value| value >= *expected) {
|
if snapshot.decision_at != *expected || previous.is_some_and(|value| value >= *expected) {
|
||||||
return Err("signal_book_decisions_duplicate_or_unordered".into());
|
return Err("signal_book_decisions_duplicate_or_unordered".into());
|
||||||
}
|
}
|
||||||
@@ -221,6 +259,9 @@ impl SignalBook {
|
|||||||
}
|
}
|
||||||
index.insert(shanghai(*expected), number);
|
index.insert(shanghai(*expected), number);
|
||||||
}
|
}
|
||||||
|
if self.content_sha256()? != self.version_sha256 {
|
||||||
|
return Err("signal_book_content_hash_mismatch".into());
|
||||||
|
}
|
||||||
Ok(ValidatedSignalBook { book: self, index })
|
Ok(ValidatedSignalBook { book: self, index })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,7 +289,9 @@ impl ValidatedSignalBook {
|
|||||||
let snapshot = self.snapshot_at(ctx.execution_date, ctx.current_time(), ctx.is_lagged_execution())?;
|
let snapshot = self.snapshot_at(ctx.execution_date, ctx.current_time(), ctx.is_lagged_execution())?;
|
||||||
let logical_clock=ctx.current_datetime().filter(|at|at.date()==ctx.decision_date)
|
let logical_clock=ctx.current_datetime().filter(|at|at.date()==ctx.decision_date)
|
||||||
.unwrap_or(ctx.decision_date.and_hms_opt(15,0,0).expect("completed decision session"));
|
.unwrap_or(ctx.decision_date.and_hms_opt(15,0,0).expect("completed decision session"));
|
||||||
if shanghai(snapshot.signal_at)>logical_clock || (ctx.is_lagged_execution() && shanghai(snapshot.input_as_of).date()>ctx.decision_date) {
|
let lagged_daily=ctx.is_lagged_execution() && self.book.frequency==SignalFrequency::Daily;
|
||||||
|
if (lagged_daily && shanghai(snapshot.input_as_of).date()>ctx.decision_date)
|
||||||
|
|| (!lagged_daily && shanghai(snapshot.signal_at)>logical_clock) {
|
||||||
return Err("next_open_signal_contains_execution_session_inputs".into());
|
return Err("next_open_signal_contains_execution_session_inputs".into());
|
||||||
}
|
}
|
||||||
Ok(snapshot)
|
Ok(snapshot)
|
||||||
@@ -329,7 +372,7 @@ mod tests {
|
|||||||
fn book() -> SignalBook {
|
fn book() -> SignalBook {
|
||||||
let decision: DateTime<Utc> = "2025-01-07T09:30:00+08:00".parse().unwrap();
|
let decision: DateTime<Utc> = "2025-01-07T09:30:00+08:00".parse().unwrap();
|
||||||
let source: DateTime<Utc> = "2025-01-06T15:00:00+08:00".parse().unwrap();
|
let source: DateTime<Utc> = "2025-01-06T15:00:00+08:00".parse().unwrap();
|
||||||
SignalBook {
|
seal(SignalBook {
|
||||||
schema: SIGNAL_BOOK_SCHEMA.into(), version_sha256: "a".repeat(64), generator_sha256: "b".repeat(64),
|
schema: SIGNAL_BOOK_SCHEMA.into(), version_sha256: "a".repeat(64), generator_sha256: "b".repeat(64),
|
||||||
model_sha256: Some("d".repeat(64)),
|
model_sha256: Some("d".repeat(64)),
|
||||||
knowledge_cutoff: Some("2024-12-31T15:00:00+08:00".parse().unwrap()),
|
knowledge_cutoff: Some("2024-12-31T15:00:00+08:00".parse().unwrap()),
|
||||||
@@ -341,7 +384,12 @@ mod tests {
|
|||||||
input_sha256: "c".repeat(64), complete_targets: true,
|
input_sha256: "c".repeat(64), complete_targets: true,
|
||||||
actions: vec![SignalAction::TargetWeight { symbol: "000001.SZ".into(), weight: 0.5 }],
|
actions: vec![SignalAction::TargetWeight { symbol: "000001.SZ".into(), weight: 0.5 }],
|
||||||
}],
|
}],
|
||||||
}
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn seal(mut book:SignalBook)->SignalBook {
|
||||||
|
book.version_sha256=book.content_sha256().unwrap();
|
||||||
|
book
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -353,7 +401,7 @@ mod tests {
|
|||||||
assert!(observed.clone().validate().unwrap_err().contains("not_available"));
|
assert!(observed.clone().validate().unwrap_err().contains("not_available"));
|
||||||
observed.snapshots[0].generated_at = observed.snapshots[0].decision_at;
|
observed.snapshots[0].generated_at = observed.snapshots[0].decision_at;
|
||||||
observed.snapshots[0].published_at = observed.snapshots[0].decision_at;
|
observed.snapshots[0].published_at = observed.snapshots[0].decision_at;
|
||||||
observed.validate().unwrap().require_observed().unwrap();
|
seal(observed).validate().unwrap().require_observed().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -423,7 +471,7 @@ mod tests {
|
|||||||
let mut raw = book();
|
let mut raw = book();
|
||||||
raw.snapshots[0].complete_targets = false;
|
raw.snapshots[0].complete_targets = false;
|
||||||
raw.snapshots[0].actions = vec![SignalAction::Reduce {symbol:"000001.SZ".into(),remaining_ratio:0.5}];
|
raw.snapshots[0].actions = vec![SignalAction::Reduce {symbol:"000001.SZ".into(),remaining_ratio:0.5}];
|
||||||
let value = raw.validate().unwrap();
|
let value = seal(raw).validate().unwrap();
|
||||||
let day = NaiveDate::from_ymd_opt(2025,1,3).unwrap();
|
let day = NaiveDate::from_ymd_opt(2025,1,3).unwrap();
|
||||||
for (held, expected) in [(1000,500),(3000,1500)] {
|
for (held, expected) in [(1000,500),(3000,1500)] {
|
||||||
let mut portfolio = PortfolioState::new(100_000.0);
|
let mut portfolio = PortfolioState::new(100_000.0);
|
||||||
@@ -439,7 +487,7 @@ mod tests {
|
|||||||
fn empty_complete_snapshot_clears_only_that_accounts_holdings() {
|
fn empty_complete_snapshot_clears_only_that_accounts_holdings() {
|
||||||
let mut raw = book();
|
let mut raw = book();
|
||||||
raw.snapshots[0].actions.clear();
|
raw.snapshots[0].actions.clear();
|
||||||
let value = raw.validate().unwrap();
|
let value = seal(raw).validate().unwrap();
|
||||||
let day = NaiveDate::from_ymd_opt(2025,1,3).unwrap();
|
let day = NaiveDate::from_ymd_opt(2025,1,3).unwrap();
|
||||||
let mut portfolio = PortfolioState::new(100_000.0);
|
let mut portfolio = PortfolioState::new(100_000.0);
|
||||||
portfolio.position_mut("000002.SZ").buy(day,200,10.0);
|
portfolio.position_mut("000002.SZ").buy(day,200,10.0);
|
||||||
@@ -454,4 +502,26 @@ mod tests {
|
|||||||
assert!(!config.rotation_enabled && config.signal_book.is_some());
|
assert!(!config.rotation_enabled && config.signal_book.is_some());
|
||||||
assert!(matches!(config.explicit_actions.as_slice(),[crate::PlatformTradeAction::ConsumeSignal]));
|
assert!(matches!(config.explicit_actions.as_slice(),[crate::PlatformTradeAction::ConsumeSignal]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn changed_valid_contents_must_not_reuse_a_version_hash() {
|
||||||
|
let mut raw=book();
|
||||||
|
raw.snapshots[0].actions=vec![SignalAction::TargetWeight{symbol:"000001.SZ".into(),weight:0.4}];
|
||||||
|
assert_eq!(raw.clone().validate().unwrap_err(),"signal_book_content_hash_mismatch");
|
||||||
|
seal(raw).validate().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completed_daily_inputs_may_be_published_after_market_close() {
|
||||||
|
let mut raw=book();
|
||||||
|
raw.expected_decisions=vec!["2026-07-07T09:30:00+08:00".parse().unwrap()];
|
||||||
|
raw.snapshots[0].decision_at=raw.expected_decisions[0];
|
||||||
|
raw.snapshots[0].input_as_of="2026-07-06T15:30:00+08:00".parse().unwrap();
|
||||||
|
raw.snapshots[0].input_available_at="2026-07-06T16:00:00+08:00".parse().unwrap();
|
||||||
|
raw.snapshots[0].signal_at=raw.snapshots[0].input_available_at;
|
||||||
|
raw.snapshots[0].generated_at=raw.snapshots[0].input_available_at;
|
||||||
|
raw.snapshots[0].published_at=raw.snapshots[0].generated_at;
|
||||||
|
raw.provenance=SignalProvenance::Observed;
|
||||||
|
seal(raw).validate().unwrap().require_observed().unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use chrono::{Duration, NaiveDate, NaiveTime};
|
|||||||
use fidc_core::{
|
use fidc_core::{
|
||||||
BacktestConfig, BacktestEngine, BenchmarkSnapshot, BrokerSimulator, CandidateEligibility,
|
BacktestConfig, BacktestEngine, BenchmarkSnapshot, BrokerSimulator, CandidateEligibility,
|
||||||
ChinaAShareCostModel, ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet,
|
ChinaAShareCostModel, ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet,
|
||||||
IntradayExecutionQuote, MatchingType, OrderIntent, PriceField, Strategy, StrategyContext,
|
Instrument, IntradayExecutionQuote, MatchingType, OrderIntent, PriceField, Strategy, StrategyContext,
|
||||||
StrategyDecision,
|
StrategyDecision,
|
||||||
};
|
};
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
@@ -16,6 +16,18 @@ fn t(hour: u32, minute: u32, second: u32) -> NaiveTime {
|
|||||||
NaiveTime::from_hms_opt(hour, minute, second).expect("valid time")
|
NaiveTime::from_hms_opt(hour, minute, second).expect("valid time")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fixture_instruments() -> Vec<Instrument> {
|
||||||
|
vec![Instrument {
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
name: "quote-plan-fixture".to_string(),
|
||||||
|
board: "SZ".to_string(),
|
||||||
|
round_lot: 100,
|
||||||
|
listed_at: Some(d(2020, 1, 1)),
|
||||||
|
delisted_at: None,
|
||||||
|
status: "active".to_string(),
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct DecisionQuoteReader {
|
struct DecisionQuoteReader {
|
||||||
day_count: usize,
|
day_count: usize,
|
||||||
@@ -90,7 +102,7 @@ impl Strategy for NoLoaderDecisionQuoteStrategy {
|
|||||||
|
|
||||||
fn single_day_quote_plan_data(date: NaiveDate) -> DataSet {
|
fn single_day_quote_plan_data(date: NaiveDate) -> DataSet {
|
||||||
DataSet::from_components(
|
DataSet::from_components(
|
||||||
Vec::new(),
|
fixture_instruments(),
|
||||||
vec![DailyMarketSnapshot {
|
vec![DailyMarketSnapshot {
|
||||||
date,
|
date,
|
||||||
symbol: "000001.SZ".to_string(),
|
symbol: "000001.SZ".to_string(),
|
||||||
@@ -253,7 +265,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
|||||||
let first = d(2026, 1, 5);
|
let first = d(2026, 1, 5);
|
||||||
let second = d(2026, 1, 6);
|
let second = d(2026, 1, 6);
|
||||||
let data = DataSet::from_components(
|
let data = DataSet::from_components(
|
||||||
Vec::new(),
|
fixture_instruments(),
|
||||||
vec![
|
vec![
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: first,
|
date: first,
|
||||||
@@ -423,7 +435,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
|||||||
let first = d(2026, 1, 5);
|
let first = d(2026, 1, 5);
|
||||||
let second = d(2026, 1, 6);
|
let second = d(2026, 1, 6);
|
||||||
let data = DataSet::from_components_with_actions_and_quotes(
|
let data = DataSet::from_components_with_actions_and_quotes(
|
||||||
Vec::new(),
|
fixture_instruments(),
|
||||||
vec![
|
vec![
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: first,
|
date: first,
|
||||||
@@ -658,7 +670,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
|||||||
let first = d(2026, 1, 5);
|
let first = d(2026, 1, 5);
|
||||||
let second = d(2026, 1, 6);
|
let second = d(2026, 1, 6);
|
||||||
let data = DataSet::from_components(
|
let data = DataSet::from_components(
|
||||||
Vec::new(),
|
fixture_instruments(),
|
||||||
vec![
|
vec![
|
||||||
DailyMarketSnapshot {
|
DailyMarketSnapshot {
|
||||||
date: first,
|
date: first,
|
||||||
|
|||||||
@@ -7,3 +7,9 @@
|
|||||||
整个明确证券范围尚未上市时保留官方日历内现金净值点,不缩短回测范围,不伪造成交或 OHLCV。基准只在首个基线点归一,后续无交易日不反复重置。
|
整个明确证券范围尚未上市时保留官方日历内现金净值点,不缩短回测范围,不伪造成交或 OHLCV。基准只在首个基线点归一,后续无交易日不反复重置。
|
||||||
|
|
||||||
513 项核心测试通过,6 项原有测试忽略。新增验证包含沪深北股票和 ETF 上市前、实际摘牌日、未知证券身份、候选缺失、正式停牌和普通价格缺口、全池上市前现金期间。对单个正式分区的数据缺口仍需数据源修复,不从这些测试外推全市场完整性。
|
513 项核心测试通过,6 项原有测试忽略。新增验证包含沪深北股票和 ETF 上市前、实际摘牌日、未知证券身份、候选缺失、正式停牌和普通价格缺口、全池上市前现金期间。对单个正式分区的数据缺口仍需数据源修复,不从这些测试外推全市场完整性。
|
||||||
|
|
||||||
|
## 真实边界回放补充
|
||||||
|
|
||||||
|
177 回测 `btr_1789041425783_797911_1`:920038.BJ,2026-08-04 至 08-07。真实上市日08-05,原结果只保留08-05至08-07三个净值点。原因是准备面同时加载基准000300.SH,基准不是交易候选但参与了“全部证券生命周期外”的判定。现在只排除已声明且没有交易候选记录的基准,不按代码或名称猜测指数,也不把真实候选排除;补充真实准备结构的回归后,4日现金区间完整保留。
|
||||||
|
|
||||||
|
该草稿沿用源池 `rejectBjseSelection=false`、`rejectBjseBuy=true`,所以选中北交所但不下单符合其买入政策;原规划阶段没有记录拒绝原因则是审计缺项。新增 `scope=buy, stage=buy_planning` 审计,不伪造订单ID,不把买入否决改写成选股排除。测试验证禁止时无订单且有bjse原因,放开买入政策时正常生成意图。最新核心514项通过、6项原有忽略。
|
||||||
|
|||||||
Reference in New Issue
Block a user