Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f789866d33 | |||
| 1092cd0b80 | |||
| 4a41057ec1 | |||
| 4a829fc0b9 | |||
| ce46c13f37 | |||
| 8d90c983ec | |||
| 7d7cce8b91 | |||
| a267052f7b | |||
| 05315f05d7 | |||
| 4eb4c17724 | |||
| 0caac44d52 | |||
| 0bbeed4ad0 | |||
| 371c573454 | |||
| d3f49a17ef | |||
| af655de0c1 | |||
| cd82492a89 | |||
| a435154108 | |||
| b331d41c31 | |||
| 591a71bf43 | |||
| b3491da6ef | |||
| dbc2ccbfef | |||
| 6d531e3d96 | |||
| 2bc033b8cc | |||
| 7485d4ceb9 | |||
| 4a349ca3c7 | |||
| 98732bbc46 | |||
| 0ac07ac0f1 | |||
| 689f93f1f1 | |||
| 4d731f9c14 | |||
| 175b319d47 | |||
| 5924e43f3d | |||
| 3c70a9273b | |||
| 695fdee4b8 | |||
| 4c6147e2ee | |||
| 818552bc96 | |||
| 534ab42906 | |||
| ba4b77fd74 | |||
| ad76bdb6ae | |||
| 59a0c95aae | |||
| d4e7cdd5b5 | |||
| 984f9d308d | |||
| fbc4233dcd | |||
| 05f1cbbe00 | |||
| ef9cc39882 | |||
| b4c68be29b | |||
| e9c9ecbd48 | |||
| b2eaaa0d26 | |||
| 13c89e8d59 | |||
| 232e9ae154 | |||
| f8955bfb18 | |||
| fb8192a286 | |||
| 7f0c6a008a | |||
| 93de28d369 | |||
| 665653c3fe | |||
| a29c434be9 | |||
| 4c96d0c31f | |||
| 5e11f3da22 | |||
| 8e7ae69b0b | |||
| 81acc54228 | |||
| 600808b171 | |||
| d2f1b64af1 | |||
| 9a54156df9 | |||
| 237ee15a51 | |||
| 996b909608 | |||
| c62ae1206f | |||
| 3a3091a2cf | |||
| d2aa16a2f0 | |||
| 0576cf9b6d | |||
| 636e0dfd05 | |||
| c98bcc3eb2 | |||
| 53af3a6a85 | |||
| 70c6f7e90b | |||
| 0ed6752a73 | |||
| 3e8cc63b9a | |||
| be171683c9 | |||
| 0a6fab9038 | |||
| e8abf43cd4 | |||
| 2286bfa757 | |||
| 93809eea1b | |||
| f7f0ff2951 | |||
| effa0c6456 | |||
| b1ca2dfada | |||
| d15abc18ae | |||
| 9370dfe6e9 | |||
| b19108558f | |||
| fe05384f80 | |||
| 0ff90c4329 | |||
| c85daae608 | |||
| f73513e2d4 | |||
| 4e953b6e98 | |||
| b232847e40 | |||
| e3b3929578 | |||
| 20e73d567b | |||
| cf4498668b | |||
| 3f39943ee4 | |||
| 5c65e65c6f | |||
| f3c70ea566 | |||
| 07b7b181b6 | |||
| fe7243bbc3 | |||
| 875e31f71f | |||
| 61bd14d001 | |||
| 531df39911 | |||
| e912e2a6f5 | |||
| b1f2fcb85c | |||
| 828690a51e | |||
| ffd23b9920 | |||
| 4ac9ee5058 | |||
| 848c1a514a | |||
| 099759ae67 | |||
| d646ca455d | |||
| 3b2a97fa84 | |||
| 2f2258f208 | |||
| 7ac87a90c4 | |||
| 5949d4cc69 | |||
| 24cb4805a7 | |||
| 053f880e34 | |||
| 4d3a9e0e5b |
Generated
+30
@@ -145,6 +145,15 @@ version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "castaway"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||
dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.59"
|
||||
@@ -192,6 +201,20 @@ dependencies = [
|
||||
"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]]
|
||||
name = "const-random"
|
||||
version = "0.1.18"
|
||||
@@ -316,6 +339,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"chrono",
|
||||
"compact_str",
|
||||
"indexmap",
|
||||
"rayon",
|
||||
"rhai",
|
||||
@@ -2044,3 +2068,9 @@ dependencies = [
|
||||
"quote",
|
||||
"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
|
||||
|
||||
[dependencies]
|
||||
compact_str = { version = "=0.10.0", features = ["serde"] }
|
||||
rust_decimal.workspace = true
|
||||
ahash.workspace = true
|
||||
chrono.workspace = true
|
||||
|
||||
+1099
-260
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,778 @@
|
||||
use super::*;
|
||||
|
||||
fn time(minute: u32) -> NaiveTime {
|
||||
NaiveTime::from_hms_opt(10, minute, 0).unwrap()
|
||||
}
|
||||
|
||||
fn data(quotes: &[(u32, f64, u32)]) -> DataSet {
|
||||
data_with_snapshot(quotes, limit_test_snapshot())
|
||||
}
|
||||
|
||||
fn data_with_snapshot(quotes: &[(u32, f64, u32)], snapshot: DailyMarketSnapshot) -> DataSet {
|
||||
DataSet::from_components_with_actions_and_quotes(
|
||||
vec![limit_test_instrument()],
|
||||
vec![snapshot],
|
||||
vec![],
|
||||
vec![limit_test_candidate(true, true)],
|
||||
vec![limit_test_benchmark()],
|
||||
vec![],
|
||||
quotes
|
||||
.iter()
|
||||
.map(|&(minute, price, volume)| {
|
||||
let mut quote = limit_test_quote(price, price, price);
|
||||
quote.timestamp = quote.date.and_time(time(minute));
|
||||
quote.volume_delta = u64::from(volume);
|
||||
quote.amount_delta = price * f64::from(volume);
|
||||
quote.bid1_volume = u64::from(volume / 100);
|
||||
quote.ask1_volume = u64::from(volume / 100);
|
||||
quote
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn broker() -> BrokerSimulator<ChinaAShareCostModel, ChinaEquityRuleHooks> {
|
||||
BrokerSimulator::new(
|
||||
ChinaAShareCostModel::default()
|
||||
.with_commission_rate(0.0003)
|
||||
.with_minimum_commission(5.),
|
||||
ChinaEquityRuleHooks,
|
||||
)
|
||||
.with_matching_type(MatchingType::MinuteLast)
|
||||
.with_execution_price_field(PriceField::Last)
|
||||
.with_intraday_execution_start_time(time(0))
|
||||
.with_volume_limit(true)
|
||||
.with_volume_percent(0.25)
|
||||
.with_liquidity_limit(false)
|
||||
.with_inactive_limit(false)
|
||||
.with_strict_value_budget(true)
|
||||
}
|
||||
|
||||
fn intent(style: AlgoOrderStyle, value: f64) -> StrategyDecision {
|
||||
StrategyDecision {
|
||||
order_intents: vec![OrderIntent::AlgoValue {
|
||||
symbol: "000001.SZ".into(),
|
||||
value,
|
||||
style,
|
||||
start_time: Some(time(0)),
|
||||
end_time: Some(time(10)),
|
||||
reason: "clock-algorithm".into(),
|
||||
}],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn step(
|
||||
broker: &BrokerSimulator<ChinaAShareCostModel, ChinaEquityRuleHooks>,
|
||||
portfolio: &mut PortfolioState,
|
||||
data: &DataSet,
|
||||
minute: u32,
|
||||
decision: &StrategyDecision,
|
||||
) -> BrokerExecutionReport {
|
||||
broker
|
||||
.execute_between(
|
||||
limit_test_snapshot().date,
|
||||
portfolio,
|
||||
data,
|
||||
decision,
|
||||
Some(time(minute)),
|
||||
Some(time(minute)),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn twap_clock_preserves_quantity_prices_fees_budget_and_parent_order() {
|
||||
let data = data(&[
|
||||
(0, 10., 4_000),
|
||||
(2, 10.1, 4_000),
|
||||
(5, 10.2, 4_000),
|
||||
(10, 10.3, 4_000),
|
||||
]);
|
||||
let decision = intent(AlgoOrderStyle::Twap, 10_000.);
|
||||
let mut synchronous_account = PortfolioState::new(20_000.);
|
||||
let reference = broker()
|
||||
.execute(
|
||||
limit_test_snapshot().date,
|
||||
&mut synchronous_account,
|
||||
&data,
|
||||
&decision,
|
||||
)
|
||||
.unwrap();
|
||||
let broker = broker();
|
||||
let mut account = PortfolioState::new(20_000.);
|
||||
let mut fills = Vec::new();
|
||||
let mut events = Vec::new();
|
||||
let empty = StrategyDecision::default();
|
||||
for minute in [0, 2, 5, 10] {
|
||||
let batch = step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
minute,
|
||||
if minute == 0 { &decision } else { &empty },
|
||||
);
|
||||
assert!(
|
||||
batch
|
||||
.fill_events
|
||||
.iter()
|
||||
.all(|fill| fill.execution_timestamp.unwrap().time() <= time(minute))
|
||||
);
|
||||
fills.extend(batch.fill_events);
|
||||
events.extend(batch.order_events);
|
||||
}
|
||||
let canonical = |rows: &[crate::events::FillEvent]| {
|
||||
rows.iter()
|
||||
.map(|fill| {
|
||||
(
|
||||
fill.quantity,
|
||||
fill.price.to_bits(),
|
||||
fill.commission.to_bits(),
|
||||
fill.stamp_tax.to_bits(),
|
||||
fill.transfer_fee.to_bits(),
|
||||
fill.execution_timestamp,
|
||||
fill.order_id,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
assert_eq!(canonical(&fills), canonical(&reference.fill_events));
|
||||
assert_eq!(account.cash(), synchronous_account.cash());
|
||||
assert_eq!(fills.iter().map(|fill| fill.quantity).sum::<u32>(), 900);
|
||||
assert_eq!(fills.iter().map(|fill| fill.commission).sum::<f64>(), 5.);
|
||||
assert!(fills.iter().map(|fill| -fill.net_cash_flow).sum::<f64>() <= 10_000.);
|
||||
assert!(events.iter().all(|event| event.order_id == Some(1)));
|
||||
assert_eq!(events.last().unwrap().status, OrderStatus::Filled);
|
||||
assert!(broker.open_order_views().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_algorithm_cancel_releases_reservation_and_never_executes_the_remainder() {
|
||||
let data = data(&[
|
||||
(0, 10., 4_000),
|
||||
(2, 10., 4_000),
|
||||
(5, 10., 4_000),
|
||||
(10, 10., 4_000),
|
||||
]);
|
||||
let broker = broker();
|
||||
let mut account = PortfolioState::new(20_000.);
|
||||
step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
0,
|
||||
&intent(AlgoOrderStyle::Twap, 10_000.),
|
||||
);
|
||||
assert_eq!(broker.open_order_views()[0].reserved_cash, Some(10_000.));
|
||||
let partial = step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
2,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
partial
|
||||
.fill_events
|
||||
.iter()
|
||||
.map(|fill| fill.quantity)
|
||||
.sum::<u32>(),
|
||||
100
|
||||
);
|
||||
let working = broker.open_order_views();
|
||||
assert_eq!(working[0].order_id, 1);
|
||||
assert_eq!(working[0].filled_quantity, 100);
|
||||
assert_eq!(
|
||||
working[0].reserved_cash,
|
||||
Some(10_000. + partial.fill_events[0].net_cash_flow)
|
||||
);
|
||||
let cancel = step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
3,
|
||||
&StrategyDecision {
|
||||
order_intents: vec![OrderIntent::CancelAll {
|
||||
reason: "explicit-user-cancel".into(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(cancel.fill_events.is_empty());
|
||||
assert_eq!(
|
||||
cancel.order_events.last().unwrap().status,
|
||||
OrderStatus::Canceled
|
||||
);
|
||||
assert_eq!(cancel.order_events.last().unwrap().filled_quantity, 100);
|
||||
assert!(broker.open_order_views().is_empty());
|
||||
assert!(
|
||||
step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
10,
|
||||
&StrategyDecision::default()
|
||||
)
|
||||
.fill_events
|
||||
.is_empty()
|
||||
);
|
||||
assert_eq!(account.position("000001.SZ").unwrap().quantity, 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn algorithm_expiry_without_a_quote_does_not_reuse_old_liquidity() {
|
||||
let data = data(&[(0, 10., 4_000), (2, 10., 4_000)]);
|
||||
let broker = broker();
|
||||
let mut account = PortfolioState::new(20_000.);
|
||||
step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
0,
|
||||
&intent(AlgoOrderStyle::Twap, 10_000.),
|
||||
);
|
||||
step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
2,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
broker.next_day_order_expiry(limit_test_snapshot().date),
|
||||
Some(time(10))
|
||||
);
|
||||
let terminal = step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
10,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
assert!(terminal.fill_events.is_empty());
|
||||
assert_eq!(
|
||||
terminal.order_events.last().unwrap().status,
|
||||
OrderStatus::Expired
|
||||
);
|
||||
assert_eq!(terminal.order_events.last().unwrap().filled_quantity, 100);
|
||||
assert!(
|
||||
terminal
|
||||
.process_events
|
||||
.iter()
|
||||
.any(|event| event.detail.contains("Expired")),
|
||||
"{:?}",
|
||||
terminal.process_events
|
||||
);
|
||||
assert!(broker.open_order_views().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn separate_buy_cannot_spend_the_working_algorithms_cash_budget() {
|
||||
let data = data(&[
|
||||
(0, 10., 4_000),
|
||||
(1, 10., 4_000),
|
||||
(2, 10., 4_000),
|
||||
(10, 10., 4_000),
|
||||
]);
|
||||
let broker = broker();
|
||||
let mut account = PortfolioState::new(11_000.);
|
||||
step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
0,
|
||||
&intent(AlgoOrderStyle::Twap, 10_000.),
|
||||
);
|
||||
let other = step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
1,
|
||||
&StrategyDecision {
|
||||
order_intents: vec![OrderIntent::Shares {
|
||||
symbol: "000001.SZ".into(),
|
||||
quantity: 1_000,
|
||||
reason: "separate-buy".into(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert!(
|
||||
other.fill_events.is_empty(),
|
||||
"cash reserved for order 1 was spent: {:?}",
|
||||
other.fill_events
|
||||
);
|
||||
let final_batch = step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
10,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
assert!(
|
||||
final_batch
|
||||
.fill_events
|
||||
.iter()
|
||||
.all(|fill| fill.order_id == Some(1))
|
||||
);
|
||||
assert_eq!(account.position("000001.SZ").unwrap().quantity, 900);
|
||||
assert!(account.cash() >= 1_000.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changing_the_later_daily_close_does_not_resize_an_algorithm_submitted_now() {
|
||||
let quotes = [(0, 10., 4_000), (2, 10.1, 4_000), (10, 10.2, 4_000)];
|
||||
let mut changed = limit_test_snapshot();
|
||||
changed.close = 100.;
|
||||
changed.last_price = 100.;
|
||||
let run = |data: DataSet| {
|
||||
let broker = broker();
|
||||
let mut account = PortfolioState::new(20_000.);
|
||||
let initial = step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
0,
|
||||
&intent(AlgoOrderStyle::Twap, 10_000.),
|
||||
);
|
||||
assert!(initial.fill_events.is_empty());
|
||||
let quantity = broker.open_order_views()[0].requested_quantity;
|
||||
let final_batch = step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
10,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
(
|
||||
quantity,
|
||||
final_batch
|
||||
.fill_events
|
||||
.iter()
|
||||
.map(|fill| {
|
||||
(
|
||||
fill.quantity,
|
||||
fill.price.to_bits(),
|
||||
fill.net_cash_flow.to_bits(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
};
|
||||
assert_eq!(
|
||||
run(data("es)),
|
||||
run(data_with_snapshot("es, changed))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vwap_clock_preserves_cash_costs_and_does_not_spend_future_volume() {
|
||||
let data = data(&[
|
||||
(0, 10., 400),
|
||||
(2, 10., 800),
|
||||
(5, 10., 1_200),
|
||||
(10, 10., 4_000),
|
||||
]);
|
||||
let decision = intent(AlgoOrderStyle::Vwap, 10_000.);
|
||||
let mut synchronous_account = PortfolioState::new(20_000.);
|
||||
let reference = broker()
|
||||
.execute(
|
||||
limit_test_snapshot().date,
|
||||
&mut synchronous_account,
|
||||
&data,
|
||||
&decision,
|
||||
)
|
||||
.unwrap();
|
||||
let broker = broker();
|
||||
let mut account = PortfolioState::new(20_000.);
|
||||
let empty = StrategyDecision::default();
|
||||
let mut filled = 0;
|
||||
let mut commission = 0.;
|
||||
for (minute, expected) in [(0, 100), (2, 300), (5, 600), (10, 900)] {
|
||||
let batch = step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
minute,
|
||||
if minute == 0 { &decision } else { &empty },
|
||||
);
|
||||
filled += batch
|
||||
.fill_events
|
||||
.iter()
|
||||
.map(|fill| fill.quantity)
|
||||
.sum::<u32>();
|
||||
commission += batch
|
||||
.fill_events
|
||||
.iter()
|
||||
.map(|fill| fill.commission)
|
||||
.sum::<f64>();
|
||||
assert_eq!(filled, expected);
|
||||
assert!(batch.fill_events.iter().all(|fill| fill.order_id == Some(1)
|
||||
&& fill.execution_timestamp.unwrap().time() <= time(minute)));
|
||||
}
|
||||
assert_eq!(account.cash(), synchronous_account.cash());
|
||||
assert_eq!(
|
||||
commission,
|
||||
reference
|
||||
.fill_events
|
||||
.iter()
|
||||
.map(|fill| fill.commission)
|
||||
.sum::<f64>()
|
||||
);
|
||||
assert!(broker.open_order_views().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_vwap_matching_keeps_the_same_working_order_between_clock_ticks() {
|
||||
let data = data(&[(0, 10., 400), (2, 10., 400), (10, 10., 4_000)]);
|
||||
let broker = broker().with_matching_type(MatchingType::Vwap);
|
||||
let mut account = PortfolioState::new(20_000.);
|
||||
let first = step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
0,
|
||||
&StrategyDecision {
|
||||
order_intents: vec![OrderIntent::Shares {
|
||||
symbol: "000001.SZ".into(),
|
||||
quantity: 900,
|
||||
reason: "configured-vwap".into(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
first
|
||||
.fill_events
|
||||
.iter()
|
||||
.map(|fill| fill.quantity)
|
||||
.sum::<u32>(),
|
||||
100
|
||||
);
|
||||
assert_eq!(
|
||||
broker.open_order_views().len(),
|
||||
1,
|
||||
"{:?}",
|
||||
first.order_events
|
||||
);
|
||||
let second = step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
2,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
assert_eq!(second.fill_events[0].quantity, 100);
|
||||
assert_eq!(second.fill_events[0].order_id, Some(1));
|
||||
let final_batch = step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
10,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
assert_eq!(final_batch.fill_events[0].quantity, 700);
|
||||
assert_eq!(final_batch.fill_events[0].order_id, Some(1));
|
||||
assert!(broker.open_order_views().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn algorithm_sell_honors_t_plus_one_and_keeps_original_quantity_after_partial_fills() {
|
||||
let data = data(&[(0, 10., 400), (2, 10., 800), (10, 10., 4_000)]);
|
||||
let date = limit_test_snapshot().date;
|
||||
for acquired_today in [false, true] {
|
||||
let broker = broker();
|
||||
let mut account = PortfolioState::new(20_000.);
|
||||
account.position_mut("000001.SZ").buy(
|
||||
if acquired_today {
|
||||
date
|
||||
} else {
|
||||
date.pred_opt().unwrap()
|
||||
},
|
||||
1_000,
|
||||
10.,
|
||||
);
|
||||
let decision = intent(AlgoOrderStyle::Vwap, -10_000.);
|
||||
let mut fills = Vec::new();
|
||||
let mut events = Vec::new();
|
||||
let empty = StrategyDecision::default();
|
||||
for minute in [0, 2, 10] {
|
||||
let batch = step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
minute,
|
||||
if minute == 0 { &decision } else { &empty },
|
||||
);
|
||||
fills.extend(batch.fill_events);
|
||||
events.extend(batch.order_events);
|
||||
}
|
||||
assert_eq!(
|
||||
fills.iter().map(|fill| fill.quantity).sum::<u32>(),
|
||||
if acquired_today { 0 } else { 1_000 }
|
||||
);
|
||||
assert!(events.iter().all(|event| event.order_id == Some(1)));
|
||||
if !acquired_today {
|
||||
assert_eq!(events.last().unwrap().status, OrderStatus::Filled);
|
||||
assert_eq!(events.last().unwrap().requested_quantity, 1_000);
|
||||
assert_eq!(events.last().unwrap().filled_quantity, 1_000);
|
||||
}
|
||||
assert!(broker.open_order_views().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_explicit_ioc_or_fok_does_not_become_a_persistent_algorithm() {
|
||||
let data = data(&[(0, 10., 400), (2, 10., 4_000), (10, 10., 4_000)]);
|
||||
for tif in [
|
||||
OrderTimeInForce::Ioc,
|
||||
OrderTimeInForce::Fok,
|
||||
OrderTimeInForce::Day,
|
||||
OrderTimeInForce::Gtc,
|
||||
] {
|
||||
let broker = broker();
|
||||
let mut account = PortfolioState::new(20_000.);
|
||||
let mut decision = intent(AlgoOrderStyle::Vwap, 10_000.);
|
||||
if !decision.order_intents[0].supports_time_in_force(tif) {
|
||||
decision.order_intents = decision
|
||||
.order_intents
|
||||
.into_iter()
|
||||
.map(|intent| intent.with_time_in_force(tif))
|
||||
.collect();
|
||||
let error = broker
|
||||
.execute_between(
|
||||
limit_test_snapshot().date,
|
||||
&mut account,
|
||||
&data,
|
||||
&decision,
|
||||
Some(time(0)),
|
||||
Some(time(0)),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("is not supported for this order intent")
|
||||
);
|
||||
assert_eq!(account.cash(), 20_000.);
|
||||
assert!(broker.open_order_views().is_empty());
|
||||
continue;
|
||||
}
|
||||
decision.order_intents = decision
|
||||
.order_intents
|
||||
.into_iter()
|
||||
.map(|intent| intent.with_time_in_force(tif))
|
||||
.collect();
|
||||
let first = step(&broker, &mut account, &data, 0, &decision);
|
||||
let persists = matches!(tif, OrderTimeInForce::Day | OrderTimeInForce::Gtc);
|
||||
assert_eq!(
|
||||
!broker.open_order_views().is_empty(),
|
||||
persists,
|
||||
"{tif:?}: {:?}",
|
||||
first.order_events
|
||||
);
|
||||
if !persists {
|
||||
assert!(
|
||||
step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
10,
|
||||
&StrategyDecision::default()
|
||||
)
|
||||
.fill_events
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_working_algorithms_reserve_only_real_cash_without_starving_the_first() {
|
||||
let data = data(&[(0, 10., 40_000), (10, 10., 40_000)]);
|
||||
let broker = broker();
|
||||
let mut account = PortfolioState::new(15_000.);
|
||||
let mut decision = intent(AlgoOrderStyle::Twap, 10_000.);
|
||||
decision
|
||||
.order_intents
|
||||
.extend(intent(AlgoOrderStyle::Twap, 10_000.).order_intents);
|
||||
step(&broker, &mut account, &data, 0, &decision);
|
||||
assert_eq!(
|
||||
broker
|
||||
.open_order_views()
|
||||
.iter()
|
||||
.map(|order| order.reserved_cash.unwrap())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![10_000., 5_000.]
|
||||
);
|
||||
let report = step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
10,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
report
|
||||
.fill_events
|
||||
.iter()
|
||||
.map(|fill| (fill.order_id, fill.quantity))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![(Some(1), 900), (Some(2), 500)]
|
||||
);
|
||||
assert!(account.cash() >= 0.);
|
||||
assert!(broker.open_order_views().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_clock_slice_does_not_turn_window_twap_into_an_unlimited_instant_order() {
|
||||
let data = data(&[(0, 10., 100), (2, 10., 100), (10, 10.1, 100)]);
|
||||
let broker = broker()
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(false);
|
||||
let mut account = PortfolioState::new(20_000.);
|
||||
step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
0,
|
||||
&intent(AlgoOrderStyle::Twap, 10_000.),
|
||||
);
|
||||
let first = step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
2,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
let last = step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
10,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
first
|
||||
.fill_events
|
||||
.iter()
|
||||
.map(|fill| fill.quantity)
|
||||
.sum::<u32>(),
|
||||
100
|
||||
);
|
||||
assert_eq!(
|
||||
last.fill_events
|
||||
.iter()
|
||||
.map(|fill| fill.quantity)
|
||||
.sum::<u32>(),
|
||||
100
|
||||
);
|
||||
assert_eq!(
|
||||
last.order_events.last().unwrap().status,
|
||||
OrderStatus::Expired
|
||||
);
|
||||
assert_eq!(last.order_events.last().unwrap().filled_quantity, 200);
|
||||
assert!(broker.open_order_views().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_matching_controls_amend_or_cancel_without_filling_a_crossing_quote() {
|
||||
let data = data(&[(0, 10., 4_000), (2, 9.4, 4_000)]);
|
||||
let broker = broker();
|
||||
let mut account = PortfolioState::new(20_000.);
|
||||
step(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
0,
|
||||
&StrategyDecision {
|
||||
order_intents: vec![
|
||||
OrderIntent::LimitShares {
|
||||
symbol: "000001.SZ".into(),
|
||||
quantity: 100,
|
||||
limit_price: 9.5,
|
||||
reason: "resting".into(),
|
||||
}
|
||||
.with_time_in_force(OrderTimeInForce::Gtc),
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(broker.open_order_views().len(), 1);
|
||||
let modify = broker
|
||||
.execute_controls_without_matching(
|
||||
limit_test_snapshot().date,
|
||||
limit_test_snapshot().date,
|
||||
&mut account,
|
||||
&data,
|
||||
&StrategyDecision {
|
||||
order_intents: vec![OrderIntent::ModifyOrder {
|
||||
order_id: 1,
|
||||
new_total_quantity: Some(200),
|
||||
new_limit_price: Some(9.3),
|
||||
reason: "pre-open-amend".into(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
Some(time(2)),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(modify.fill_events.is_empty());
|
||||
assert_eq!(broker.open_order_views()[0].limit_price, 9.3);
|
||||
assert_eq!(broker.open_order_views()[0].requested_quantity, 200);
|
||||
let cancel = broker
|
||||
.execute_controls_without_matching(
|
||||
limit_test_snapshot().date,
|
||||
limit_test_snapshot().date,
|
||||
&mut account,
|
||||
&data,
|
||||
&StrategyDecision {
|
||||
order_intents: vec![OrderIntent::CancelAll {
|
||||
reason: "pre-open-cancel".into(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
Some(time(2)),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(cancel.fill_events.is_empty());
|
||||
assert_eq!(
|
||||
cancel.order_events.last().unwrap().status,
|
||||
OrderStatus::Canceled
|
||||
);
|
||||
assert_eq!(account.cash(), 20_000.);
|
||||
assert!(broker.open_order_views().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_only_phase_cannot_be_used_to_submit_an_order_or_leave_matching_disabled() {
|
||||
let data = data(&[(0, 10., 4_000)]);
|
||||
let broker = broker();
|
||||
let mut account = PortfolioState::new(20_000.);
|
||||
let submit = StrategyDecision {
|
||||
order_intents: vec![OrderIntent::Shares {
|
||||
symbol: "000001.SZ".into(),
|
||||
quantity: 100,
|
||||
reason: "normal-order".into(),
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
broker
|
||||
.execute_controls_without_matching(
|
||||
limit_test_snapshot().date,
|
||||
limit_test_snapshot().date,
|
||||
&mut account,
|
||||
&data,
|
||||
&submit,
|
||||
Some(time(0))
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(account.cash(), 20_000.);
|
||||
assert_eq!(
|
||||
step(&broker, &mut account, &data, 0, &submit).fill_events[0].quantity,
|
||||
100
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,516 @@
|
||||
// Kept inside broker::tests to inspect internal accepted-order identity as
|
||||
// well as the public report. These are simulator states, never GT requests.
|
||||
fn recovery_test_data(missing_previous: Option<usize>, intraday: bool) -> DataSet {
|
||||
let previous = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
|
||||
let mut instruments = Vec::new();
|
||||
let mut rows = Vec::new();
|
||||
let mut candidates = Vec::new();
|
||||
let mut quotes = Vec::new();
|
||||
for index in 1..=2 {
|
||||
let symbol = format!("{index:06}.SZ");
|
||||
let mut instrument = limit_test_instrument();
|
||||
instrument.symbol = symbol.clone();
|
||||
instruments.push(instrument);
|
||||
for day in [previous, date] {
|
||||
if day == previous && missing_previous == Some(index) {
|
||||
continue;
|
||||
}
|
||||
let mut row = dated_limit_test_snapshot(day);
|
||||
row.symbol = symbol.clone().into();
|
||||
rows.push(row);
|
||||
let mut candidate = dated_limit_test_candidate(day, false, false, true, true);
|
||||
candidate.symbol = symbol.clone().into();
|
||||
candidates.push(candidate);
|
||||
}
|
||||
if intraday {
|
||||
let mut quote = limit_test_quote(10., 10., 10.);
|
||||
quote.symbol = symbol;
|
||||
quote.date = date;
|
||||
quote.timestamp = date.and_hms_opt(9, 33, 0).unwrap();
|
||||
quotes.push(quote);
|
||||
}
|
||||
}
|
||||
DataSet::from_components_with_actions_and_quotes(
|
||||
instruments,
|
||||
rows,
|
||||
vec![],
|
||||
candidates,
|
||||
vec![
|
||||
dated_limit_test_benchmark(previous),
|
||||
dated_limit_test_benchmark(date),
|
||||
],
|
||||
vec![],
|
||||
quotes,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn recovery_test_broker(
|
||||
intraday: bool,
|
||||
first_side: OrderSide,
|
||||
) -> (
|
||||
BrokerSimulator<ChinaAShareCostModel, ChinaEquityRuleHooks>,
|
||||
PortfolioState,
|
||||
) {
|
||||
let mut broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_matching_type(if intraday {
|
||||
MatchingType::MinuteLast
|
||||
} else {
|
||||
MatchingType::CurrentBarClose
|
||||
})
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(false)
|
||||
.with_slippage_model(SlippageModel::HistoricalVolumeVolatility(
|
||||
super::DynamicSlippageConfig::new(0., 0., 0.1),
|
||||
));
|
||||
if intraday {
|
||||
broker =
|
||||
broker.with_intraday_execution_start_time(NaiveTime::from_hms_opt(9, 33, 0).unwrap());
|
||||
}
|
||||
let mut first = test_open_order(1);
|
||||
first.filled_quantity = 100;
|
||||
first.remaining_quantity = 100;
|
||||
first.commission_remaining = Some(0.);
|
||||
first.side = first_side;
|
||||
let mut second = test_open_order(2);
|
||||
second.symbol = "000002.SZ".into();
|
||||
broker.upsert_open_order(first);
|
||||
broker.upsert_open_order(second);
|
||||
broker.next_order_id.set(3);
|
||||
let mut account = PortfolioState::new(9000.);
|
||||
account.position_mut("000001.SZ").buy(
|
||||
chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(),
|
||||
if first_side == OrderSide::Buy {
|
||||
100
|
||||
} else {
|
||||
200
|
||||
},
|
||||
10.,
|
||||
);
|
||||
account.begin_trading_day();
|
||||
(broker, account)
|
||||
}
|
||||
|
||||
fn recovery_report_value(report: &BrokerExecutionReport) -> serde_json::Value {
|
||||
serde_json::json!({"orders":report.order_events,"fills":report.fill_events,
|
||||
"positions":report.position_events,"accounts":report.account_events,
|
||||
"events":report.process_events,"diagnostics":report.diagnostics})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_resting_order_batch_keeps_accepted_orders_and_unpublished_financial_state() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
|
||||
for intraday in [false, true] {
|
||||
for first_side in [OrderSide::Buy, OrderSide::Sell] {
|
||||
for missing in [1, 2] {
|
||||
let (broker, mut account) = recovery_test_broker(intraday, first_side);
|
||||
let orders = format!("{:?}", broker.open_orders.borrow());
|
||||
let ledger = account.financial_replay_identity();
|
||||
let error = broker
|
||||
.execute(
|
||||
date,
|
||||
&mut account,
|
||||
&recovery_test_data(Some(missing), intraday),
|
||||
&StrategyDecision::default(),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("historical_slippage_calibration_missing")
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{:?}", broker.open_orders.borrow()),
|
||||
orders,
|
||||
"intraday={intraday} first={first_side:?} missing={missing}"
|
||||
);
|
||||
assert_eq!(account.financial_replay_identity(), ledger);
|
||||
assert!(broker.same_day_sold_symbols.borrow().is_empty());
|
||||
let recovered = broker
|
||||
.execute(
|
||||
date,
|
||||
&mut account,
|
||||
&recovery_test_data(None, intraday),
|
||||
&StrategyDecision::default(),
|
||||
)
|
||||
.unwrap();
|
||||
let (clean, mut clean_account) = recovery_test_broker(intraday, first_side);
|
||||
let reference = clean
|
||||
.execute(
|
||||
date,
|
||||
&mut clean_account,
|
||||
&recovery_test_data(None, intraday),
|
||||
&StrategyDecision::default(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
recovery_report_value(&recovered),
|
||||
recovery_report_value(&reference)
|
||||
);
|
||||
assert_eq!(
|
||||
account.financial_replay_identity(),
|
||||
clean_account.financial_replay_identity()
|
||||
);
|
||||
assert!(broker.open_order_views().is_empty());
|
||||
assert_eq!(recovered.fill_events.len(), 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_new_batch_does_not_erase_prior_success_or_double_charge_on_retry() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
|
||||
let (broker, mut account) = recovery_test_broker(true, OrderSide::Buy);
|
||||
let good = recovery_test_data(None, true);
|
||||
let prior = broker
|
||||
.execute(date, &mut account, &good, &StrategyDecision::default())
|
||||
.unwrap();
|
||||
assert_eq!(prior.fill_events.len(), 2);
|
||||
let initial = account.financial_replay_identity();
|
||||
let id = broker.next_order_id.get();
|
||||
let decision = StrategyDecision {
|
||||
order_intents: vec![
|
||||
OrderIntent::Shares {
|
||||
symbol: "000001.SZ".into(),
|
||||
quantity: 100,
|
||||
reason: "next-batch-a".into(),
|
||||
},
|
||||
OrderIntent::Shares {
|
||||
symbol: "000002.SZ".into(),
|
||||
quantity: 100,
|
||||
reason: "next-batch-b".into(),
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
// A later quote lets this batch execute independently of the prior fills.
|
||||
let mut parts = good.snapshot_components();
|
||||
for quote in &mut parts.execution_quotes {
|
||||
quote.timestamp += chrono::Duration::minutes(1);
|
||||
}
|
||||
broker
|
||||
.runtime_execution_clock
|
||||
.set(Some(NaiveTime::from_hms_opt(9, 34, 0).unwrap()));
|
||||
let restored = DataSet::from_components_with_actions_and_quotes(
|
||||
parts.instruments.clone(),
|
||||
parts.market.clone(),
|
||||
parts.factors.clone(),
|
||||
parts.candidates.clone(),
|
||||
parts.benchmarks.clone(),
|
||||
vec![],
|
||||
parts.execution_quotes.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
parts
|
||||
.market
|
||||
.retain(|row| !(row.symbol.as_str() == "000002.SZ" && row.date < date));
|
||||
let broken = DataSet::from_components_with_actions_and_quotes(
|
||||
parts.instruments,
|
||||
parts.market,
|
||||
parts.factors,
|
||||
parts.candidates,
|
||||
parts.benchmarks,
|
||||
vec![],
|
||||
parts.execution_quotes,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
broker
|
||||
.execute(date, &mut account, &broken, &decision)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(account.financial_replay_identity(), initial);
|
||||
assert_eq!(broker.next_order_id.get(), id);
|
||||
assert!(broker.open_orders.borrow().is_empty());
|
||||
let result = broker
|
||||
.execute(date, &mut account, &restored, &decision)
|
||||
.unwrap();
|
||||
assert_eq!(result.fill_events.len(), 2);
|
||||
assert_eq!(result.fill_events[0].order_id, Some(id));
|
||||
assert_eq!(result.fill_events[1].order_id, Some(id + 1));
|
||||
assert_eq!(account.position("000001.SZ").unwrap().quantity, 300);
|
||||
assert_eq!(account.position("000002.SZ").unwrap().quantity, 300);
|
||||
assert_eq!(
|
||||
prior.fill_events.len(),
|
||||
2,
|
||||
"previously returned report remains intact"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unwinding_an_unpublished_simulator_transaction_restores_its_state() {
|
||||
let (broker, mut account) = recovery_test_broker(false, OrderSide::Sell);
|
||||
let initial = account.financial_replay_identity();
|
||||
let orders = format!("{:?}", broker.open_orders.borrow());
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
let _ = broker.execution_transaction(&mut account, true, None, |account| {
|
||||
account.apply_cash_delta(500.).unwrap();
|
||||
broker.open_orders.borrow_mut().clear();
|
||||
panic!("isolated simulator callback unwind");
|
||||
});
|
||||
}));
|
||||
assert!(result.is_err());
|
||||
assert_eq!(account.financial_replay_identity(), initial);
|
||||
assert_eq!(format!("{:?}", broker.open_orders.borrow()), orders);
|
||||
assert_eq!(broker.execution_transaction_depth.get(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_etf_batch_failure_keeps_both_targets_and_prior_generation_progress() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
|
||||
let previous = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
let mut parts = recovery_test_data(None, false).snapshot_components();
|
||||
for instrument in &mut parts.instruments {
|
||||
instrument.board = "ETF".into();
|
||||
}
|
||||
let good = DataSet::from_components_with_actions_and_quotes(
|
||||
parts.instruments.clone(),
|
||||
parts.market.clone(),
|
||||
parts.factors.clone(),
|
||||
parts.candidates.clone(),
|
||||
parts.benchmarks.clone(),
|
||||
vec![],
|
||||
vec![],
|
||||
)
|
||||
.unwrap();
|
||||
parts
|
||||
.market
|
||||
.retain(|row| !(row.date == date && row.symbol.as_str() == "000002.SZ"));
|
||||
let bad = DataSet::from_components_with_actions_and_quotes(
|
||||
parts.instruments,
|
||||
parts.market,
|
||||
parts.factors,
|
||||
parts.candidates,
|
||||
parts.benchmarks,
|
||||
vec![],
|
||||
vec![],
|
||||
)
|
||||
.unwrap();
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(false);
|
||||
let members = std::sync::Arc::new(
|
||||
(1..=2)
|
||||
.map(|index| crate::stock_pool_execution::StockPoolMemberSpec {
|
||||
symbol: format!("{index:06}.SZ"),
|
||||
requested_order: index,
|
||||
recommendation_reason: String::new(),
|
||||
target_weight_bps: None,
|
||||
stop_loss: None,
|
||||
take_profit: None,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
broker
|
||||
.deferred_etf_targets
|
||||
.borrow_mut()
|
||||
.replace_generation("pool", "latest");
|
||||
for index in 1..=2 {
|
||||
broker
|
||||
.deferred_etf_targets
|
||||
.borrow_mut()
|
||||
.upsert(crate::etf_execution::DeferredEtfTarget {
|
||||
pool_id: "pool".into(),
|
||||
generation: "latest".into(),
|
||||
symbol: format!("{index:06}.SZ"),
|
||||
signal_date: previous,
|
||||
signal_at: previous.and_hms_opt(13, 0, 0).unwrap(),
|
||||
execute_on: Some(date),
|
||||
target_value: 1000.into(),
|
||||
target_weight_bps: 5000,
|
||||
target_weight_ratio: None,
|
||||
side: crate::stock_pool_execution::OrderSide::Buy,
|
||||
max_positions: 2,
|
||||
rule: Default::default(),
|
||||
members: std::sync::Arc::clone(&members),
|
||||
reason: "deferred recovery fixture".into(),
|
||||
});
|
||||
}
|
||||
let queue = format!("{:?}", broker.deferred_etf_targets.borrow());
|
||||
let mut account = PortfolioState::new(10000.);
|
||||
let state = account.stock_pool_execution_state("pool");
|
||||
assert!(
|
||||
broker
|
||||
.execute_deferred_etf_targets(date, &mut account, &bad)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(account.cash(), 10000.);
|
||||
assert!(account.positions().is_empty());
|
||||
assert_eq!(account.stock_pool_execution_state("pool"), state);
|
||||
assert_eq!(format!("{:?}", broker.deferred_etf_targets.borrow()), queue);
|
||||
assert_eq!(broker.next_order_id.get(), 1);
|
||||
assert_eq!(broker.execution_transaction_depth.get(), 0);
|
||||
let result = broker
|
||||
.execute_deferred_etf_targets(date, &mut account, &good)
|
||||
.unwrap();
|
||||
assert_eq!(result.fill_events.len(), 2, "{result:?}");
|
||||
assert_eq!(broker.pending_etf_target_count(), 0);
|
||||
assert_eq!(account.position("000001.SZ").unwrap().quantity, 100);
|
||||
assert_eq!(account.position("000002.SZ").unwrap().quantity, 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_callback_unwind_does_not_leak_order_context_or_authoritative_prior_state() {
|
||||
struct PanicRules;
|
||||
impl crate::rules::EquityRuleHooks for PanicRules {
|
||||
fn can_buy(
|
||||
&self,
|
||||
_: chrono::NaiveDate,
|
||||
_: &DailyMarketSnapshot,
|
||||
_: &CandidateEligibility,
|
||||
_: PriceField,
|
||||
) -> crate::rules::RuleCheck {
|
||||
panic!("isolated rule callback panic")
|
||||
}
|
||||
fn can_sell(
|
||||
&self,
|
||||
_: chrono::NaiveDate,
|
||||
_: &DailyMarketSnapshot,
|
||||
_: &CandidateEligibility,
|
||||
_: &crate::portfolio::Position,
|
||||
_: PriceField,
|
||||
) -> crate::rules::RuleCheck {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
|
||||
let prior = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), PanicRules)
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(false);
|
||||
broker.runtime_decision_date.set(Some(prior));
|
||||
broker
|
||||
.runtime_buy_denials
|
||||
.borrow_mut()
|
||||
.insert("unrelated".into(), "prior".into());
|
||||
let mut account = PortfolioState::new(10000.);
|
||||
let decision = StrategyDecision {
|
||||
buy_denials: BTreeMap::from([("another".into(), "temporary".into())]),
|
||||
order_intents: vec![
|
||||
OrderIntent::LimitShares {
|
||||
symbol: "000001.SZ".into(),
|
||||
quantity: 100,
|
||||
limit_price: 10.,
|
||||
reason: "panic fixture".into(),
|
||||
}
|
||||
.with_time_in_force(OrderTimeInForce::Gtc),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
let _ = broker.execute(
|
||||
date,
|
||||
&mut account,
|
||||
&recovery_test_data(None, false),
|
||||
&decision,
|
||||
);
|
||||
}))
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(account.cash(), 10000.);
|
||||
assert!(account.positions().is_empty());
|
||||
assert!(broker.open_order_views().is_empty());
|
||||
assert_eq!(broker.runtime_decision_date.get(), Some(prior));
|
||||
assert_eq!(
|
||||
*broker.runtime_buy_denials.borrow(),
|
||||
BTreeMap::from([("unrelated".into(), "prior".into())])
|
||||
);
|
||||
assert_eq!(broker.runtime_time_in_force.get(), None);
|
||||
assert_eq!(broker.runtime_target_position_limit.get(), None);
|
||||
assert_eq!(broker.execution_transaction_depth.get(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simulator_transaction_profile_preserves_successful_output() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
|
||||
let previous = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
let mut instruments = Vec::new();
|
||||
let mut market = Vec::new();
|
||||
let mut candidates = Vec::new();
|
||||
for index in 1..=30 {
|
||||
let symbol = format!("{index:06}.SZ");
|
||||
let mut instrument = limit_test_instrument();
|
||||
instrument.symbol = symbol.clone();
|
||||
instruments.push(instrument);
|
||||
for day in [previous, date] {
|
||||
let mut row = dated_limit_test_snapshot(day);
|
||||
row.symbol = symbol.clone().into();
|
||||
market.push(row);
|
||||
let mut row = dated_limit_test_candidate(day, false, false, true, true);
|
||||
row.symbol = symbol.clone().into();
|
||||
candidates.push(row);
|
||||
}
|
||||
}
|
||||
let data = DataSet::from_components(
|
||||
instruments,
|
||||
market,
|
||||
vec![],
|
||||
candidates,
|
||||
vec![
|
||||
dated_limit_test_benchmark(previous),
|
||||
dated_limit_test_benchmark(date),
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
let mut reference = None;
|
||||
let mut samples = Vec::new();
|
||||
for protected in [false, true, true, false] {
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_matching_type(MatchingType::CurrentBarClose)
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(false);
|
||||
// Private comparison only: no runtime option can disable protection.
|
||||
if !protected {
|
||||
broker.execution_transaction_depth.set(1);
|
||||
}
|
||||
let mut account = PortfolioState::new(10_000_000.);
|
||||
for index in 1..=30 {
|
||||
for _ in 0..20 {
|
||||
account
|
||||
.position_mut(&format!("{index:06}.SZ"))
|
||||
.buy(previous, 100, 10.);
|
||||
}
|
||||
}
|
||||
account.begin_trading_day();
|
||||
let mut orders = Vec::new();
|
||||
let mut fills = Vec::new();
|
||||
let start = std::time::Instant::now();
|
||||
for index in 0..500 {
|
||||
let report = broker
|
||||
.execute(
|
||||
date,
|
||||
&mut account,
|
||||
&data,
|
||||
&StrategyDecision {
|
||||
order_intents: vec![OrderIntent::Shares {
|
||||
symbol: format!("{:06}.SZ", index % 30 + 1),
|
||||
quantity: 100,
|
||||
reason: "transaction profile".into(),
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
orders.extend(report.order_events);
|
||||
fills.extend(report.fill_events);
|
||||
}
|
||||
samples.push(
|
||||
serde_json::json!({"protected":protected,"microseconds":start.elapsed().as_micros()}),
|
||||
);
|
||||
assert_eq!(fills.len(), 500);
|
||||
let outcome = serde_json::json!({"orders":orders,"fills":fills,"ledger":account.financial_replay_identity()});
|
||||
if let Some(reference) = &reference {
|
||||
assert_eq!(&outcome, reference);
|
||||
} else {
|
||||
reference = Some(outcome);
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"simulator_transaction_profile={}",
|
||||
serde_json::json!({"securities":30,"initial_lots_per_security":20,"calls":500,"samples":samples,
|
||||
"scope":"isolated broker only; not Source or full backtest throughput"})
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,35 @@ use super::*;
|
||||
use crate::holding_policy::HoldingLifecycleEvidence;
|
||||
use crate::stock_pool_execution as pool;
|
||||
use rust_decimal::{Decimal, prelude::ToPrimitive};
|
||||
use chrono::Timelike;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct DeferredStockPoolExecution {
|
||||
date: NaiveDate,
|
||||
contract: Box<pool::FrozenStockPoolIntent>,
|
||||
buy_only: bool,
|
||||
symbols: BTreeSet<String>,
|
||||
initial_holdings: BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl<C, R> BrokerSimulator<C, R> {
|
||||
pub(crate) fn pending_stock_pool_symbols(&self) -> BTreeSet<String> {
|
||||
self.deferred_stock_pools.borrow().values().flat_map(|pending| pending.symbols.iter().cloned()).collect()
|
||||
}
|
||||
|
||||
pub(crate) fn has_pending_stock_pool_execution(&self) -> bool {
|
||||
!self.deferred_stock_pools.borrow().is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn finish_stock_pool_session(&self, date: NaiveDate, report: &mut BrokerExecutionReport) {
|
||||
self.deferred_stock_pools.borrow_mut().retain(|_, pending| {
|
||||
if pending.date <= date {
|
||||
report.diagnostics.push(format!("stock_pool_unsubmitted_phase_expired generation={} date={date} no_buy_order_created=true",pending.contract.generation));
|
||||
false
|
||||
} else { true }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn decimal(value: f64, label: &str) -> Result<Decimal, BacktestError> {
|
||||
if !value.is_finite() {
|
||||
@@ -16,6 +45,11 @@ fn decimal(value: f64, label: &str) -> Result<Decimal, BacktestError> {
|
||||
.map_err(|_| BacktestError::Execution(format!("stock_pool_decimal_range_{label}")))
|
||||
}
|
||||
|
||||
fn etf_activity(report:&mut BrokerExecutionReport,date:NaiveDate,symbol:&str,side:pool::OrderSide,detail:String) {
|
||||
report.process_events.push(ProcessEvent {date,kind:ProcessEventKind::EtfExecutionFallback,order_id:None,
|
||||
symbol:Some(symbol.into()),side:Some(if side==pool::OrderSide::Buy {OrderSide::Buy} else {OrderSide::Sell}),detail});
|
||||
}
|
||||
|
||||
fn pool_positions(
|
||||
portfolio: &PortfolioState,
|
||||
date: NaiveDate,
|
||||
@@ -35,15 +69,141 @@ fn pool_positions(
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod successor_protection_tests {
|
||||
use super::*;
|
||||
use crate::holding_policy::{AutomaticTradeLock, AutomaticTradeProtection};
|
||||
fn day(n: u32) -> NaiveDate { NaiveDate::from_ymd_opt(2026, 9, n).unwrap() }
|
||||
|
||||
#[test]
|
||||
fn deferred_etf_open_rechecks_inherited_locks_and_cooldown_before_any_order() {
|
||||
let old = "159915.SZ";
|
||||
let new = "159999.SZ";
|
||||
let data = DataSet::from_components(
|
||||
[old, new].into_iter().map(|symbol| crate::Instrument {
|
||||
symbol: symbol.into(), name: "isolated ETF fixture".into(), board: "ETF".into(), round_lot: 100,
|
||||
listed_at: Some(day(1)), delisted_at: None, status: "active".into(),
|
||||
}).collect(), vec![crate::DailyMarketSnapshot {
|
||||
date: day(15), symbol: new.into(), timestamp: None, day_open: 5., open: 5., high: 5., low: 5.,
|
||||
close: 5., last_price: 5., bid1: 5., ask1: 5., prev_close: 5., volume: 100000,
|
||||
minute_volume: 0, bid1_volume: 100000, ask1_volume: 100000, trading_phase: None,
|
||||
paused: false, upper_limit: 5.5, lower_limit: 4.5, price_tick: 0.001,
|
||||
}], vec![], vec![crate::CandidateEligibility {
|
||||
date: day(15), symbol: new.into(), is_st: false, is_star_st: false, is_new_listing: false,
|
||||
is_paused: false, allow_buy: true, allow_sell: true, is_kcb: false, is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
}], [11,14,15].into_iter().map(|n| crate::BenchmarkSnapshot {
|
||||
date: day(n), benchmark: "000300.SH".into(), open: 100., close: 100., prev_close: 100., volume: 10000,
|
||||
}).collect()).unwrap();
|
||||
for mode in ["lock", "cooldown", "expired"] {
|
||||
let broker = BrokerSimulator::new(crate::ChinaAShareCostModel::default(), crate::ChinaEquityRuleHooks)
|
||||
.with_volume_limit(false).with_liquidity_limit(false);
|
||||
let mut portfolio = PortfolioState::new(10000.);
|
||||
portfolio.position_mut(old).buy(day(11), 200, 10.);
|
||||
portfolio.position_mut(old).sell(100, 10.).unwrap();
|
||||
broker.mark_same_day_sold(day(11), old);
|
||||
portfolio.apply_successor_conversion(old, new, 2., 0.).unwrap();
|
||||
let policy = AutomaticTradeProtection {
|
||||
sell_cooldown_days: if mode == "cooldown" { 3 } else { 0 },
|
||||
locks: if mode != "cooldown" { vec![AutomaticTradeLock {
|
||||
symbol: old.into(), start_date: day(11), end_date: Some(day(if mode == "expired" {14} else {15})),
|
||||
}] } else { vec![] }, ..Default::default()
|
||||
};
|
||||
let rule = pool::StockPoolExecutionRule { automatic_trade_protection: policy, ..Default::default() };
|
||||
broker.deferred_etf_targets.borrow_mut().replace_generation("pool", "latest");
|
||||
broker.deferred_etf_targets.borrow_mut().upsert(crate::etf_execution::DeferredEtfTarget {
|
||||
pool_id: "pool".into(), generation: "latest".into(), symbol: new.into(),
|
||||
signal_date: day(14), signal_at: day(14).and_hms_opt(13,0,0).unwrap(), execute_on: Some(day(15)),
|
||||
target_value: 5000.into(), target_weight_bps: 10000, target_weight_ratio:None, side: pool::OrderSide::Buy, max_positions: 1,
|
||||
rule: std::sync::Arc::new(rule), members: std::sync::Arc::new(vec![pool::StockPoolMemberSpec {
|
||||
symbol: new.into(), requested_order: 0, recommendation_reason: String::new(),
|
||||
target_weight_bps: None, stop_loss: None, take_profit: None,
|
||||
}]), reason: "isolated deferred ETF target".into(),
|
||||
});
|
||||
let report = broker.execute_deferred_etf_targets(day(15), &mut portfolio, &data).unwrap();
|
||||
if mode == "expired" {
|
||||
assert_eq!(report.fill_events.len(), 1, "{report:?}");
|
||||
assert_eq!(portfolio.position(new).unwrap().quantity, 1000);
|
||||
} else {
|
||||
assert!(report.order_events.is_empty(), "{mode}: {report:?}");
|
||||
assert!(report.fill_events.is_empty());
|
||||
assert_eq!(portfolio.position(new).unwrap().quantity, 200);
|
||||
assert!(report.diagnostics.iter().any(|text| text.contains(if mode == "lock" {"automatic_trade_locked"} else {"sell_fill_cooldown"})));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
||||
fn pool_automatic_permission(&self, symbol: &str, date: NaiveDate,
|
||||
policy: &crate::holding_policy::AutomaticTradeProtection,
|
||||
portfolio: &PortfolioState, data: &DataSet)
|
||||
-> Result<crate::holding_policy::AutomaticTradePermission, BacktestError> {
|
||||
let position = portfolio.position(symbol).filter(|position| position.quantity > 0);
|
||||
let sold = self.same_day_sold_symbols.borrow().iter().rev()
|
||||
.find(|(day, symbols)| **day <= date && (symbols.contains(symbol)
|
||||
|| portfolio.corporate_predecessors(symbol).any(|previous| symbols.contains(previous))))
|
||||
.map(|(day, _)| *day);
|
||||
let evidence = HoldingLifecycleEvidence {
|
||||
has_position: position.is_some(), opened_date: position.and_then(|position| position.opened_date()),
|
||||
last_buy_date: position.and_then(|position| position.last_buy_date()), last_sell_date: sold,
|
||||
};
|
||||
policy.evaluate_with_predecessors(symbol, date, &evidence, data.calendar(),
|
||||
portfolio.corporate_predecessors(symbol)).map_err(BacktestError::Execution)
|
||||
}
|
||||
|
||||
pub(super) fn resume_stock_pool_executions(&self, date: NaiveDate, portfolio: &mut PortfolioState, data: &DataSet,
|
||||
session: &mut BrokerExecutionSession, report: &mut BrokerExecutionReport) -> Result<(), BacktestError> {
|
||||
let clock = self.runtime_intraday_start_time.get().or(self.intraday_execution_start_time);
|
||||
let mut expired = Vec::new();
|
||||
for (id, pending) in self.deferred_stock_pools.borrow().iter() {
|
||||
let end = NaiveTime::parse_from_str(&pending.contract.rule.window_end, "%H:%M")
|
||||
.map_err(|_| BacktestError::Execution("stock_pool_execution_window_invalid".into()))?;
|
||||
if pending.date != date || clock.is_some_and(|clock| clock >= end) { expired.push(id.clone()); }
|
||||
}
|
||||
for id in expired {
|
||||
if let Some(pending) = self.deferred_stock_pools.borrow_mut().remove(&id) {
|
||||
report.diagnostics.push(format!("stock_pool_unsubmitted_phase_expired generation={} date={date} no_buy_order_created=true",pending.contract.generation));
|
||||
}
|
||||
}
|
||||
if self.has_open_orders() || clock.is_none() { return Ok(()); }
|
||||
let pending = std::mem::take(&mut *self.deferred_stock_pools.borrow_mut());
|
||||
for (id, pending) in pending {
|
||||
let now = clock.expect("clock checked above");
|
||||
let start = NaiveTime::parse_from_str(&pending.contract.rule.window_start, "%H:%M")
|
||||
.map_err(|_| BacktestError::Execution("stock_pool_execution_window_invalid".into()))?;
|
||||
if now < start || !pool::stock_pool_is_trading_minute(now.hour() * 60 + now.minute()) {
|
||||
self.deferred_stock_pools.borrow_mut().insert(id, pending);
|
||||
continue;
|
||||
}
|
||||
let prior_followup = self.runtime_stock_pool_followup.replace(true);
|
||||
let prior_decision = self.runtime_decision_date.replace(Some(pending.contract.signal_date));
|
||||
let prior_created = self.runtime_order_created_date.replace(Some(date));
|
||||
let order_start = report.order_events.len();
|
||||
let fill_start = report.fill_events.len();
|
||||
report.diagnostics.push(format!("stock_pool_resume_after_order_reports generation={} clock={} cash={}",pending.contract.generation,clock.unwrap(),portfolio.cash()));
|
||||
let result = self.process_stock_pool_contract_phase(date, portfolio, data, &pending.contract,
|
||||
&mut session.intraday_turnover, &mut session.execution_cursors, &mut session.global_execution_cursor,
|
||||
&mut session.commission_state, report, pending.buy_only, Some(&pending.initial_holdings));
|
||||
self.runtime_stock_pool_followup.set(prior_followup);
|
||||
self.runtime_decision_date.set(prior_decision);
|
||||
self.runtime_order_created_date.set(prior_created);
|
||||
result?;
|
||||
Self::annotate_report_range(report, order_start, fill_start, pending.contract.signal_date, date, date);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pool_quote_inputs(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
data: &DataSet,
|
||||
symbols: &BTreeSet<String>,
|
||||
execution_clock: Option<NaiveDateTime>,
|
||||
) -> Result<Vec<pool::MarketSnapshot>, BacktestError> {
|
||||
symbols
|
||||
cumulative_conditions: bool,
|
||||
) -> Result<(Vec<pool::MarketSnapshot>, Vec<String>), BacktestError> {
|
||||
let mut unavailable = Vec::new();
|
||||
let quotes = symbols
|
||||
.iter()
|
||||
.map(|symbol| {
|
||||
let snapshot = data.market(date, symbol).ok_or_else(|| {
|
||||
@@ -54,7 +214,13 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
||||
let instrument = data.instruments().get(symbol).ok_or_else(|| {
|
||||
BacktestError::Execution(format!("stock_pool_instrument_missing:{symbol}"))
|
||||
})?;
|
||||
let (price, prev, volume, amount, bid, ask, buy_price, sell_price) = if self
|
||||
let fallback = self.pool_etf_fallback_reference(date, data, symbol, execution_clock)?;
|
||||
let (price, prev, volume, amount, bid, ask, buy_price, sell_price) = if let Some(reference) = fallback {
|
||||
let calibration = self.slippage_calibration(data, snapshot)?;
|
||||
(reference.price, snapshot.prev_close, None, None, None, None,
|
||||
self.quote_execution_price(snapshot, OrderSide::Buy, reference.price, None, calibration.as_ref())?,
|
||||
self.quote_execution_price(snapshot, OrderSide::Sell, reference.price, None, calibration.as_ref())?)
|
||||
} else if self
|
||||
.matching_type_uses_intraday_quotes()
|
||||
{
|
||||
let time = self
|
||||
@@ -89,7 +255,7 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
||||
snapshot,
|
||||
quote,
|
||||
OrderSide::Buy,
|
||||
self.matching_type,
|
||||
self.matching_type_for_algo_request(None),
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
BacktestError::Execution(format!(
|
||||
@@ -101,7 +267,7 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
||||
snapshot,
|
||||
quote,
|
||||
OrderSide::Sell,
|
||||
self.matching_type,
|
||||
self.matching_type_for_algo_request(None),
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
BacktestError::Execution(format!(
|
||||
@@ -123,11 +289,17 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
||||
None,
|
||||
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,
|
||||
snapshot.prev_close,
|
||||
Some(quote.volume_delta as f64),
|
||||
Some(quote.amount_delta),
|
||||
totals.map(|total| total.0),
|
||||
totals.map(|total| total.1),
|
||||
Some(quote.bid1),
|
||||
Some(quote.ask1),
|
||||
buy,
|
||||
@@ -142,13 +314,24 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
||||
}
|
||||
// A daily open does not reveal the session's volume/turnover.
|
||||
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,
|
||||
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,
|
||||
Some(price),
|
||||
Some(price),
|
||||
self.snapshot_execution_price(data, snapshot, OrderSide::Buy, None)?,
|
||||
self.snapshot_execution_price(data, snapshot, OrderSide::Sell, None)?,
|
||||
)
|
||||
@@ -157,8 +340,8 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
||||
symbol: symbol.clone(),
|
||||
last_price: decimal(price, "price")?,
|
||||
prev_close: Some(decimal(prev, "prev_close")?),
|
||||
volume: volume.map(|v| decimal(v, "volume")).transpose()?,
|
||||
turnover: amount.map(|v| decimal(v, "amount")).transpose()?,
|
||||
volume,
|
||||
turnover: amount,
|
||||
bid_price_1: bid.map(|v| decimal(v, "bid")).transpose()?,
|
||||
ask_price_1: ask.map(|v| decimal(v, "ask")).transpose()?,
|
||||
is_kcb: Some(instrument.board.eq_ignore_ascii_case("KSH")),
|
||||
@@ -171,7 +354,18 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
||||
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> {
|
||||
if !self.matching_type_uses_intraday_quotes() || !self.has_verified_etf_minute_absence(date, symbol) {
|
||||
return Ok(None);
|
||||
}
|
||||
let time = self.runtime_intraday_start_time.get().or(self.intraday_execution_start_time)
|
||||
.ok_or_else(|| BacktestError::Execution("etf_daily_open_fallback: execution clock missing".into()))?;
|
||||
let at = clock.unwrap_or(date.and_time(time)).max(date.and_time(time));
|
||||
crate::etf_execution::reference(data, symbol, at).map(Some)
|
||||
}
|
||||
|
||||
pub(super) fn process_stock_pool_contract(
|
||||
@@ -185,6 +379,17 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
||||
global_execution_cursor: &mut Option<NaiveDateTime>,
|
||||
commission_state: &mut BTreeMap<u64, f64>,
|
||||
report: &mut BrokerExecutionReport,
|
||||
) -> Result<(), BacktestError> {
|
||||
self.process_stock_pool_contract_phase(date, portfolio, data, contract, intraday_turnover,
|
||||
execution_cursors, global_execution_cursor, commission_state, report, false, None)
|
||||
}
|
||||
|
||||
fn process_stock_pool_contract_phase(
|
||||
&self, date: NaiveDate, portfolio: &mut PortfolioState, data: &DataSet,
|
||||
contract: &pool::FrozenStockPoolIntent, intraday_turnover: &mut BTreeMap<String, u32>,
|
||||
execution_cursors: &mut IntradayExecutionLedger, global_execution_cursor: &mut Option<NaiveDateTime>,
|
||||
commission_state: &mut BTreeMap<u64, f64>, report: &mut BrokerExecutionReport, buy_only: bool,
|
||||
initial_holdings: Option<&BTreeSet<String>>,
|
||||
) -> Result<(), BacktestError> {
|
||||
if contract.signal_date > date
|
||||
|| contract.frozen_equity < Decimal::ZERO
|
||||
@@ -225,6 +430,7 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
scope.extend(portfolio.positions().keys().cloned());
|
||||
let before_positions = initial_holdings.cloned().unwrap_or_else(|| portfolio.positions().keys().cloned().collect());
|
||||
let official_dates = data.calendar().iter().collect::<Vec<_>>();
|
||||
let initial_positions = pool_positions(portfolio, date)?;
|
||||
let state = portfolio
|
||||
@@ -240,7 +446,12 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
||||
portfolio
|
||||
.set_stock_pool_execution_state(&contract.pool_id, state)
|
||||
.map_err(BacktestError::Execution)?;
|
||||
let superseded = self.deferred_etf_targets.borrow_mut().replace_generation(&contract.pool_id, &contract.generation);
|
||||
if superseded > 0 { report.diagnostics.push(format!("etf_daily_open_fallback:superseded pool={} generation={} targets={superseded}", contract.pool_id, contract.generation)); }
|
||||
if self.has_open_orders() {
|
||||
self.deferred_stock_pools.borrow_mut().insert(contract.pool_id.clone(), DeferredStockPoolExecution {
|
||||
date, contract: Box::new(contract.clone()), buy_only, symbols: scope, initial_holdings: before_positions,
|
||||
});
|
||||
report
|
||||
.diagnostics
|
||||
.push("stock_pool_waiting_for_active_orders no_new_intent=true".into());
|
||||
@@ -286,14 +497,31 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
||||
.push("paused".into());
|
||||
}
|
||||
}
|
||||
let before_positions = portfolio
|
||||
.positions()
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect::<BTreeSet<_>>();
|
||||
// All delayed symbols in a generation share immutable configuration.
|
||||
// Do not duplicate an N-member pool N times in a large mixed pool.
|
||||
let mut deferred_configuration = None;
|
||||
for side in [pool::OrderSide::Sell, pool::OrderSide::Buy] {
|
||||
let quotes =
|
||||
self.pool_quote_inputs(date, data, "e_scope, *global_execution_cursor)?;
|
||||
if buy_only && side == pool::OrderSide::Sell { continue; }
|
||||
if side == pool::OrderSide::Buy && self.has_open_orders()
|
||||
&& self.effective_rebalance_cash_mode() == RebalanceCashMode::SellThenBuy {
|
||||
self.deferred_stock_pools.borrow_mut().insert(contract.pool_id.clone(), DeferredStockPoolExecution {
|
||||
date, contract: Box::new(contract.clone()), buy_only: true, symbols: quote_scope.clone(), initial_holdings: before_positions.clone(),
|
||||
});
|
||||
report.diagnostics.push(format!("stock_pool_waiting_for_sell_reports generation={} no_buy_order_created=true",contract.generation));
|
||||
break;
|
||||
}
|
||||
let mut fallback_references = BTreeMap::new();
|
||||
for symbol in "e_scope {
|
||||
if let Some(reference) = self.pool_etf_fallback_reference(date, data, symbol, *global_execution_cursor)? {
|
||||
let condition = if side == pool::OrderSide::Buy { &contract.rule.buy_condition } else { &contract.rule.sell_condition };
|
||||
if !condition.trim().is_empty() {
|
||||
return Err(BacktestError::Execution(format!("etf_daily_open_fallback: intraday condition evidence unavailable symbol={symbol} side={side:?}; daily reference is not a minute or tick signal")));
|
||||
}
|
||||
fallback_references.insert(symbol.clone(), reference);
|
||||
}
|
||||
}
|
||||
let (quotes, unavailable) = 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 execution_state = portfolio
|
||||
.stock_pool_execution_state(&contract.pool_id)
|
||||
@@ -307,6 +535,8 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
||||
.map_err(BacktestError::Execution)?;
|
||||
constraints.pending_entry_symbols = execution_state.pending_symbols();
|
||||
constraints.prior_target_weights = execution_state.last_target_weights.clone();
|
||||
constraints.prior_target_weight_ratios = execution_state.last_target_weight_ratios.clone();
|
||||
constraints.position_action_bases = execution_state.position_action_bases_for(&contract.generation);
|
||||
constraints.next_day_outside_exit_symbols = execution_state.next_day_exit_symbols(date);
|
||||
let account = pool::AccountSnapshot {
|
||||
total_equity: contract.frozen_equity,
|
||||
@@ -316,30 +546,24 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
||||
constraints.automatic_permissions.clear();
|
||||
if contract.rule.automatic_trade_protection.enabled() {
|
||||
for symbol in &scope {
|
||||
let position = portfolio.position(symbol).filter(|p| p.quantity > 0);
|
||||
let sold = self
|
||||
.same_day_sold_symbols
|
||||
.borrow()
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|(day, symbols)| **day <= date && symbols.contains(symbol))
|
||||
.map(|(day, _)| *day);
|
||||
let evidence = HoldingLifecycleEvidence {
|
||||
has_position: position.is_some(),
|
||||
opened_date: position.and_then(|p| p.opened_date()),
|
||||
last_buy_date: position.and_then(|p| p.last_buy_date()),
|
||||
last_sell_date: sold,
|
||||
};
|
||||
let permission = contract
|
||||
.rule
|
||||
.automatic_trade_protection
|
||||
.evaluate(symbol, date, &evidence, data.calendar())
|
||||
.map_err(BacktestError::Execution)?;
|
||||
let permission = self.pool_automatic_permission(symbol, date,
|
||||
&contract.rule.automatic_trade_protection, portfolio, data)?;
|
||||
constraints
|
||||
.automatic_permissions
|
||||
.insert(symbol.clone(), permission);
|
||||
}
|
||||
}
|
||||
if side == pool::OrderSide::Buy {
|
||||
for (symbol, reference) in &fallback_references {
|
||||
if !reference.immediate {
|
||||
// The sell leg was queued, not filled. Keep its real
|
||||
// holdings/slots and do not finance buys with proceeds
|
||||
// from the following session.
|
||||
constraints.automatic_permissions.entry(symbol.clone()).or_default()
|
||||
.sell_denial.get_or_insert("etf_daily_open_deferred");
|
||||
}
|
||||
}
|
||||
}
|
||||
if self
|
||||
.risk_config
|
||||
.static_rules
|
||||
@@ -400,10 +624,20 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
||||
Decimal::ZERO,
|
||||
Some(&fee),
|
||||
)
|
||||
.map_err(BacktestError::Execution)?;
|
||||
let updated = execution_state
|
||||
.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
|
||||
.record_plan(contract.signal_date, &contract.generation, &plan)
|
||||
.map_err(BacktestError::Execution)?;
|
||||
for (symbol, reference) in &fallback_references {
|
||||
if !reference.immediate && let Some(entry) = updated.entries.get_mut(symbol) {
|
||||
// The signal only fixes money, not shares at a stale close.
|
||||
entry.completion_quantity = None;
|
||||
}
|
||||
}
|
||||
portfolio
|
||||
.set_stock_pool_execution_state(&contract.pool_id, updated)
|
||||
.map_err(BacktestError::Execution)?;
|
||||
@@ -425,6 +659,26 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
||||
if row.side != Some(side) {
|
||||
continue;
|
||||
}
|
||||
if let Some(reference) = fallback_references.get(&row.symbol) {
|
||||
let time = self.runtime_intraday_start_time.get().or(self.intraday_execution_start_time).expect("fallback clock validated");
|
||||
let at = global_execution_cursor.unwrap_or(date.and_time(time)).max(date.and_time(time));
|
||||
if !reference.immediate {
|
||||
report.diagnostics.push(format!("etf_daily_open_fallback:deferred symbol={} signal_at={at} reference_date={} reference_price={} target_value={} execute_on={:?}", row.symbol, reference.reference_date, reference.price, row.target_value, reference.execute_on));
|
||||
let deferred = deferred_configuration.get_or_insert_with(|| (
|
||||
std::sync::Arc::new(contract.rule.clone()), std::sync::Arc::new(members.clone()),
|
||||
));
|
||||
let opening_date=reference.execute_on.map(|day|day.to_string()).unwrap_or_else(||"回测区间外(后续日历未加载)".into());
|
||||
etf_activity(report,date,&row.symbol,side,format!("ETF 顺延执行:信号 {at},参考 {} 收盘 {},目标金额 {},下一正式开盘日 {opening_date};未生成成交。",reference.reference_date,reference.price,row.target_value));
|
||||
self.deferred_etf_targets.borrow_mut().upsert(crate::etf_execution::DeferredEtfTarget {
|
||||
pool_id:contract.pool_id.clone(), generation:contract.generation.clone(), symbol:row.symbol.clone(),
|
||||
signal_date:contract.signal_date, signal_at:at, execute_on:reference.execute_on,
|
||||
target_value:row.target_value, target_weight_bps:row.target_weight_bps, target_weight_ratio:plan.target_weight_ratios.get(&row.symbol).copied(), side,
|
||||
max_positions, rule:std::sync::Arc::clone(&deferred.0), members:std::sync::Arc::clone(&deferred.1),
|
||||
reason:row.source_intent.clone().unwrap_or_else(||"stock_pool_target".into()),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if side == pool::OrderSide::Buy
|
||||
&& portfolio
|
||||
.position(&row.symbol)
|
||||
@@ -440,8 +694,14 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
||||
let target = row.target_quantity.to_i32().ok_or_else(|| {
|
||||
BacktestError::Execution("stock_pool_target_quantity_out_of_range".into())
|
||||
})?;
|
||||
let reason = row.source_intent.as_deref().unwrap_or("stock_pool_target");
|
||||
if let Some(price) = row.limit_price {
|
||||
let fallback_reason = fallback_references.contains_key(&row.symbol).then(|| format!("{}: etf_daily_open_fallback signal_date={} execution_date={date}", row.source_intent.as_deref().unwrap_or("stock_pool_target"), contract.signal_date));
|
||||
let reason = fallback_reason.as_deref().unwrap_or_else(|| row.source_intent.as_deref().unwrap_or("stock_pool_target"));
|
||||
let first_fill = report.fill_events.len();
|
||||
if fallback_references.contains_key(&row.symbol) {
|
||||
report.diagnostics.push(format!("etf_daily_open_fallback:opening symbol={} signal_date={} execution_date={date}", row.symbol, contract.signal_date));
|
||||
etf_activity(report,date,&row.symbol,side,format!("ETF 日线开盘回退:信号日 {},执行日 {date},使用正式日线开盘价;不是分钟成交行情。",contract.signal_date));
|
||||
}
|
||||
let mut execute = || if let Some(price) = row.limit_price {
|
||||
self.process_limit_target_shares(
|
||||
date,
|
||||
portfolio,
|
||||
@@ -457,7 +717,7 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
||||
global_execution_cursor,
|
||||
commission_state,
|
||||
report,
|
||||
)?;
|
||||
)
|
||||
} else {
|
||||
self.process_target_shares(
|
||||
date,
|
||||
@@ -471,10 +731,127 @@ impl<C: CostModel, R: EquityRuleHooks> BrokerSimulator<C, R> {
|
||||
global_execution_cursor,
|
||||
commission_state,
|
||||
report,
|
||||
)?;
|
||||
}
|
||||
)
|
||||
};
|
||||
if fallback_references.contains_key(&row.symbol) {
|
||||
self.with_etf_daily_open(execute)?;
|
||||
for fill in &mut report.fill_events[first_fill..] {
|
||||
fill.execution_start_timestamp.get_or_insert(date.and_time(crate::etf_execution::opening_time()));
|
||||
fill.execution_timestamp.get_or_insert(date.and_time(crate::etf_execution::opening_time()));
|
||||
}
|
||||
} else { execute()?; }
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn pending_etf_target_count(&self) -> usize {
|
||||
self.deferred_etf_targets.borrow().len()
|
||||
}
|
||||
|
||||
/// Called at the opening clock, after settlement/corporate actions and
|
||||
/// auction callbacks. It never sends a stock order or replays a strategy.
|
||||
pub(crate) fn execute_deferred_etf_targets(&self, date: NaiveDate, portfolio: &mut PortfolioState, data: &DataSet) -> Result<BrokerExecutionReport, BacktestError> {
|
||||
self.execution_transaction(portfolio, !self.has_open_orders() && self.pending_etf_target_count() > 0, None,
|
||||
|portfolio| self.execute_deferred_etf_targets_inner(date, portfolio, data))
|
||||
}
|
||||
|
||||
fn execute_deferred_etf_targets_inner(&self, date: NaiveDate, portfolio: &mut PortfolioState, data: &DataSet) -> Result<BrokerExecutionReport, BacktestError> {
|
||||
let mut report = BrokerExecutionReport::default();
|
||||
if self.has_open_orders() {
|
||||
if self.pending_etf_target_count() > 0 {
|
||||
report.diagnostics.push("etf_daily_open_fallback:waiting_for_active_orders".into());
|
||||
}
|
||||
return Ok(report);
|
||||
}
|
||||
let due = self.deferred_etf_targets.borrow_mut().take_due(date);
|
||||
let dates = data.calendar().iter().collect::<Vec<_>>();
|
||||
for target in due {
|
||||
let instrument = data.instrument(&target.symbol).ok_or_else(|| BacktestError::Execution("etf_daily_open_fallback: instrument identity missing at execution".into()))?;
|
||||
if !instrument.is_exchange_traded_fund() { return Err(BacktestError::Execution("etf_daily_open_fallback: instrument identity changed".into())); }
|
||||
if let Some(reason) = instrument.dated_market_absence_reason(date) {
|
||||
report.diagnostics.push(format!("etf_daily_open_fallback:blocked symbol={} date={date} reason={reason}", target.symbol));
|
||||
continue;
|
||||
}
|
||||
let snapshot = data.market(date, &target.symbol).ok_or_else(|| BacktestError::Execution(format!("etf_daily_open_fallback: daily_open_missing symbol={} date={date}", target.symbol)))?;
|
||||
if !snapshot.open.is_finite() || snapshot.open <= 0.0 {
|
||||
return Err(BacktestError::Execution(format!("etf_daily_open_fallback: daily_open_invalid symbol={} date={date}", target.symbol)));
|
||||
}
|
||||
let position = portfolio.position(&target.symbol).filter(|p| p.quantity > 0);
|
||||
let before_quantity = position.map_or(0, |p| p.quantity);
|
||||
let permission = self.pool_automatic_permission(&target.symbol, date,
|
||||
&target.rule.automatic_trade_protection, portfolio, data)?;
|
||||
let denial = if target.side == pool::OrderSide::Buy {
|
||||
permission.buy_denial.or(permission.max_holding_exit.then_some("max_holding_exit_pending"))
|
||||
} else { permission.sell_denial };
|
||||
if let Some(denial) = denial {
|
||||
report.diagnostics.push(format!("etf_daily_open_fallback:protected symbol={} date={date} reason={denial}", target.symbol));
|
||||
etf_activity(&mut report,date,&target.symbol,target.side,format!("ETF 顺延目标受持有保护限制:{denial};未提交委托。"));
|
||||
continue;
|
||||
}
|
||||
if target.side == pool::OrderSide::Buy && before_quantity == 0 && Self::positive_position_count(portfolio) >= target.max_positions {
|
||||
report.diagnostics.push(format!("etf_daily_open_fallback:blocked symbol={} reason=occupied_position_slots", target.symbol));
|
||||
continue;
|
||||
}
|
||||
let value = target.target_value.to_f64().ok_or_else(|| BacktestError::Execution("etf_daily_open_fallback: target value out of range".into()))?;
|
||||
let current_value = snapshot.open * f64::from(before_quantity);
|
||||
let satisfied = (target.side == pool::OrderSide::Buy && value <= current_value)
|
||||
|| (target.side == pool::OrderSide::Sell && value >= current_value);
|
||||
let reason = format!("{}: etf_daily_open_fallback signal_at={} execution_at={} target_value={}", target.reason, target.signal_at, date.and_time(crate::etf_execution::opening_time()), target.target_value);
|
||||
let mut sub = BrokerExecutionReport::default();
|
||||
if !satisfied {
|
||||
let (_, limit) = pool::resolve_stock_pool_order_price(&target.rule, &target.symbol, decimal(snapshot.open, "etf_open")?, target.side, decimal(snapshot.price_tick, "etf_tick")?).map_err(BacktestError::Execution)?;
|
||||
let intent = match limit {
|
||||
Some(limit) => OrderIntent::LimitTargetValue { symbol:target.symbol.clone(), target_value:value, limit_price:limit.to_f64().ok_or_else(|| BacktestError::Execution("ETF limit out of range".into()))?, reason:reason.clone() },
|
||||
None => OrderIntent::TargetValue { symbol:target.symbol.clone(), target_value:value, reason:reason.clone() },
|
||||
};
|
||||
let old_time = self.runtime_intraday_start_time.replace(Some(crate::etf_execution::opening_time()));
|
||||
let old_origin = self.runtime_resting_order_origin.replace(Some(RestingOrderOrigin { created_date:Some(target.signal_at.date()), submission_time:Some(target.signal_at.time()), accepted_date:date }));
|
||||
let outcome = self.with_etf_daily_open(|| self.execute_with_event_dates(date, target.signal_date, target.signal_at.date(), portfolio, data, &StrategyDecision {
|
||||
order_intents:vec![OrderIntent::WithTimeInForce { intent:Box::new(intent), time_in_force:OrderTimeInForce::Day }], ..Default::default()
|
||||
}));
|
||||
self.runtime_intraday_start_time.set(old_time);
|
||||
self.runtime_resting_order_origin.set(old_origin);
|
||||
sub = outcome?;
|
||||
}
|
||||
// The actual open determines the full requested shares. A clipped
|
||||
// or rejected execution must not be recorded as completed entry.
|
||||
let order = sub.order_events.iter().rev().find(|order| order.symbol == target.symbol);
|
||||
let goal_quantity = order.map_or(before_quantity, |order| match order.side {
|
||||
OrderSide::Buy => before_quantity.saturating_add(order.requested_quantity),
|
||||
OrderSide::Sell => before_quantity.saturating_sub(order.requested_quantity),
|
||||
});
|
||||
let status = if satisfied || (order.is_none() && !self.has_open_orders()) { "BELOW_MINIMUM_TRADE_UNIT_ALREADY_SATISFIED" } else { "READY" };
|
||||
let positions = pool_positions(portfolio, date)?;
|
||||
let state = portfolio.stock_pool_execution_state(&target.pool_id)
|
||||
.observe(target.signal_date, date, &dates, &target.members, &positions).map_err(BacktestError::Execution)?
|
||||
.record_targets(target.signal_date, &target.generation, [crate::stock_pool_state::StockPoolGoalObservation {
|
||||
symbol:&target.symbol, target_weight_bps:target.target_weight_bps, target_weight_ratio:target.target_weight_ratio, target_value:target.target_value,
|
||||
current_quantity:before_quantity.into(), target_quantity:goal_quantity.into(), status,
|
||||
}]).map_err(BacktestError::Execution)?
|
||||
.observe(target.signal_date, date, &dates, &target.members, &positions).map_err(BacktestError::Execution)?;
|
||||
portfolio.set_stock_pool_execution_state(&target.pool_id, state).map_err(BacktestError::Execution)?;
|
||||
for fill in &mut sub.fill_events {
|
||||
fill.decision_date.get_or_insert(target.signal_date);
|
||||
fill.order_created_date.get_or_insert(target.signal_at.date());
|
||||
fill.execution_date.get_or_insert(date);
|
||||
fill.execution_start_timestamp.get_or_insert(date.and_time(crate::etf_execution::opening_time()));
|
||||
fill.execution_timestamp.get_or_insert(date.and_time(crate::etf_execution::opening_time()));
|
||||
}
|
||||
for order in &mut sub.order_events {
|
||||
order.decision_date.get_or_insert(target.signal_date);
|
||||
order.order_created_date.get_or_insert(target.signal_at.date());
|
||||
order.execution_date.get_or_insert(date);
|
||||
}
|
||||
report.diagnostics.push(reason);
|
||||
etf_activity(&mut report,date,&target.symbol,target.side,format!("ETF 顺延目标开盘处理:原信号 {},本次 {date} 09:30,冻结目标金额 {},持仓 {before_quantity} → {};按本日开盘价、资金与风控重新定量。",target.signal_at,target.target_value,portfolio.position(&target.symbol).map_or(0,|position|position.quantity)));
|
||||
report.order_events.extend(sub.order_events);
|
||||
report.fill_events.extend(sub.fill_events);
|
||||
report.position_events.extend(sub.position_events);
|
||||
report.account_events.extend(sub.account_events);
|
||||
report.process_events.extend(sub.process_events);
|
||||
report.diagnostics.extend(sub.diagnostics);
|
||||
}
|
||||
Ok(report)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,684 @@
|
||||
fn pool_batch_data() -> DataSet {
|
||||
pool_batch_data_with(|_| true)
|
||||
}
|
||||
|
||||
fn pool_batch_data_with(change: impl Fn(&mut IntradayExecutionQuote) -> bool) -> DataSet {
|
||||
let symbols = ["000001.SZ", "000002.SZ", "000003.SZ"];
|
||||
let instruments = symbols
|
||||
.iter()
|
||||
.map(|symbol| Instrument {
|
||||
symbol: (*symbol).into(),
|
||||
..limit_test_instrument()
|
||||
})
|
||||
.collect();
|
||||
let snapshots = symbols
|
||||
.iter()
|
||||
.map(|symbol| DailyMarketSnapshot {
|
||||
symbol: (*symbol).into(),
|
||||
..limit_test_snapshot()
|
||||
})
|
||||
.collect();
|
||||
let candidates = symbols
|
||||
.iter()
|
||||
.map(|symbol| CandidateEligibility {
|
||||
symbol: (*symbol).into(),
|
||||
..limit_test_candidate(true, true)
|
||||
})
|
||||
.collect();
|
||||
let mut quotes = Vec::new();
|
||||
for minute in [30, 31, 32, 33, 34, 36] {
|
||||
for symbol in symbols {
|
||||
let price = if symbol == "000001.SZ" && minute > 30 {
|
||||
10.5
|
||||
} else {
|
||||
10.0
|
||||
};
|
||||
let mut quote = limit_test_quote(price, price, price);
|
||||
quote.symbol = symbol.into();
|
||||
quote.timestamp = quote.date.and_hms_opt(9, minute, 0).unwrap();
|
||||
quote.volume_delta = 200;
|
||||
quote.bid1_volume = 200;
|
||||
quote.ask1_volume = 200;
|
||||
quote.amount_delta = price * 200.0;
|
||||
if change(&mut quote) {
|
||||
quotes.push(quote);
|
||||
}
|
||||
}
|
||||
}
|
||||
DataSet::from_components_with_actions_and_quotes(
|
||||
instruments,
|
||||
snapshots,
|
||||
Vec::new(),
|
||||
candidates,
|
||||
vec![limit_test_benchmark()],
|
||||
Vec::new(),
|
||||
quotes,
|
||||
)
|
||||
.unwrap()
|
||||
.with_additional_trading_dates([chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap()])
|
||||
}
|
||||
|
||||
fn pool_batch_decision(symbol: &str, generation: &str, end: &str) -> StrategyDecision {
|
||||
use crate::stock_pool_execution as pool;
|
||||
let signal = chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
|
||||
let symbols = vec![symbol.to_owned()];
|
||||
let rule = pool::StockPoolExecutionRule {
|
||||
pricing_mode: pool::POOL_PRICE_FIXED_LIMIT.into(),
|
||||
fixed_prices: [
|
||||
("000001.SZ".into(), rust_decimal::Decimal::new(104, 1)),
|
||||
("000002.SZ".into(), 10.into()),
|
||||
("000003.SZ".into(), 10.into()),
|
||||
]
|
||||
.into(),
|
||||
window_end: end.into(),
|
||||
..Default::default()
|
||||
};
|
||||
StrategyDecision {
|
||||
order_intents: vec![OrderIntent::StockPool {
|
||||
contract: Box::new(pool::FrozenStockPoolIntent {
|
||||
pool_id: "batch-test".into(),
|
||||
signal_date: signal,
|
||||
frozen_equity: 2000.into(),
|
||||
selection: pool::StockPoolSelection {
|
||||
trade_date: signal,
|
||||
requested_symbols: symbols.clone(),
|
||||
normal_trading_symbols: symbols.clone(),
|
||||
risk_eligible_symbols: symbols.clone(),
|
||||
final_symbols: symbols,
|
||||
exclusion_reasons: Default::default(),
|
||||
inherited_from_generation: None,
|
||||
explicit_empty: false,
|
||||
generation: Some(generation.into()),
|
||||
},
|
||||
members: vec![pool::StockPoolMemberSpec {
|
||||
symbol: symbol.into(),
|
||||
recommendation_reason: String::new(),
|
||||
requested_order: 0,
|
||||
target_weight_bps: None,
|
||||
stop_loss: None,
|
||||
take_profit: None,
|
||||
}],
|
||||
rule,
|
||||
constraints: pool::StockPoolDecisionConstraints {
|
||||
target_holding_count: Some(1),
|
||||
..Default::default()
|
||||
},
|
||||
invest_ratio_bps: 10000,
|
||||
reserve_cash: 0.into(),
|
||||
out_of_pool_policy: "reduce_to_zero_when_sellable".into(),
|
||||
generation: generation.into(),
|
||||
}),
|
||||
}],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn pool_batch_broker(partial: bool) -> BrokerSimulator<ChinaAShareCostModel, ChinaEquityRuleHooks> {
|
||||
let cost = ChinaAShareCostModel::from_trading_constraints(
|
||||
crate::risk_control::TradingConstraintConfig {
|
||||
commission_rate: 0.0,
|
||||
minimum_commission: 0.0,
|
||||
stamp_tax_rate_before_change: 0.0,
|
||||
stamp_tax_rate_after_change: 0.0,
|
||||
transfer_fee_rate: 0.0,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let broker =
|
||||
BrokerSimulator::new_with_execution_price(cost, ChinaEquityRuleHooks, PriceField::Open)
|
||||
.with_matching_type(if partial {
|
||||
MatchingType::MinuteLast
|
||||
} else {
|
||||
MatchingType::NextBarOpen
|
||||
})
|
||||
.with_volume_limit(partial)
|
||||
.with_volume_percent(0.5)
|
||||
.with_liquidity_limit(false)
|
||||
.with_inactive_limit(false);
|
||||
if partial {
|
||||
broker
|
||||
.with_intraday_execution_start_time(chrono::NaiveTime::from_hms_opt(9, 30, 0).unwrap())
|
||||
} else {
|
||||
broker
|
||||
}
|
||||
}
|
||||
|
||||
fn pool_batch_account() -> PortfolioState {
|
||||
let mut account = PortfolioState::new(0.0);
|
||||
account.position_mut("000001.SZ").buy(
|
||||
chrono::NaiveDate::from_ymd_opt(2024, 12, 30).unwrap(),
|
||||
200,
|
||||
10.0,
|
||||
);
|
||||
account
|
||||
}
|
||||
|
||||
fn pool_batch_tick(
|
||||
broker: &BrokerSimulator<ChinaAShareCostModel, ChinaEquityRuleHooks>,
|
||||
account: &mut PortfolioState,
|
||||
data: &DataSet,
|
||||
minute: u32,
|
||||
decision: &StrategyDecision,
|
||||
) -> BrokerExecutionReport {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
broker
|
||||
.runtime_intraday_start_time
|
||||
.set(Some(chrono::NaiveTime::from_hms_opt(9, minute, 0).unwrap()));
|
||||
broker
|
||||
.runtime_intraday_end_time
|
||||
.set(Some(chrono::NaiveTime::from_hms_opt(9, minute, 0).unwrap()));
|
||||
broker.execute(date, account, data, decision).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stock_pool_pending_sell_continues_buy_after_actual_fill_without_strategy_rerun() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
let signal = chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
|
||||
let data = pool_batch_data();
|
||||
let broker = pool_batch_broker(false);
|
||||
let mut account = pool_batch_account();
|
||||
let initial = broker
|
||||
.execute_with_event_dates(
|
||||
date,
|
||||
signal,
|
||||
signal,
|
||||
&mut account,
|
||||
&data,
|
||||
&pool_batch_decision("000002.SZ", "first", "09:35"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(initial.fill_events.is_empty());
|
||||
assert_eq!(broker.open_order_views().len(), 1);
|
||||
assert_eq!(broker.open_order_views()[0].side, OrderSide::Sell);
|
||||
let done = pool_batch_tick(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
31,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
assert!(
|
||||
done.fill_events
|
||||
.iter()
|
||||
.any(|fill| fill.symbol == "000001.SZ" && fill.side == OrderSide::Sell)
|
||||
);
|
||||
assert_eq!(
|
||||
account.position("000002.SZ").map(|p| p.quantity),
|
||||
Some(200),
|
||||
"sell proceeds must trigger the retained buy phase: {:?}",
|
||||
done.diagnostics
|
||||
);
|
||||
assert!(
|
||||
account
|
||||
.position("000001.SZ")
|
||||
.is_none_or(|p| p.quantity == 0)
|
||||
);
|
||||
let repeated = pool_batch_tick(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
32,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
assert!(repeated.order_events.is_empty() && repeated.fill_events.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stock_pool_partial_sell_waits_for_the_whole_batch_and_never_reissues_buys() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
let signal = chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
|
||||
let data = pool_batch_data();
|
||||
let broker = pool_batch_broker(true);
|
||||
let mut account = pool_batch_account();
|
||||
broker
|
||||
.execute_with_event_dates(
|
||||
date,
|
||||
signal,
|
||||
signal,
|
||||
&mut account,
|
||||
&data,
|
||||
&pool_batch_decision("000002.SZ", "partial", "09:35"),
|
||||
)
|
||||
.unwrap();
|
||||
let first = pool_batch_tick(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
31,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
assert_eq!(account.position("000001.SZ").unwrap().quantity, 100);
|
||||
assert!(account.position("000002.SZ").is_none());
|
||||
assert!(
|
||||
first
|
||||
.order_events
|
||||
.iter()
|
||||
.all(|event| event.side == OrderSide::Sell)
|
||||
);
|
||||
let second = pool_batch_tick(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
32,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
let third = pool_batch_tick(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
33,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
assert_eq!(account.position("000002.SZ").unwrap().quantity, 200);
|
||||
let ids = second
|
||||
.order_events
|
||||
.iter()
|
||||
.chain(&third.order_events)
|
||||
.filter(|event| event.side == OrderSide::Buy)
|
||||
.filter_map(|event| event.order_id)
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
ids.len(),
|
||||
1,
|
||||
"one buy intention; partial reports must keep its ID"
|
||||
);
|
||||
assert!(
|
||||
pool_batch_tick(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
34,
|
||||
&StrategyDecision::default()
|
||||
)
|
||||
.order_events
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stock_pool_delayed_sell_does_not_start_buys_after_the_configured_window() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
let signal = chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
|
||||
let data = pool_batch_data();
|
||||
let broker = pool_batch_broker(true);
|
||||
let mut account = pool_batch_account();
|
||||
broker
|
||||
.execute_with_event_dates(
|
||||
date,
|
||||
signal,
|
||||
signal,
|
||||
&mut account,
|
||||
&data,
|
||||
&pool_batch_decision("000002.SZ", "expired", "09:32"),
|
||||
)
|
||||
.unwrap();
|
||||
pool_batch_tick(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
31,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
let last = pool_batch_tick(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
32,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
assert!(account.position("000002.SZ").is_none());
|
||||
assert!(
|
||||
last.order_events
|
||||
.iter()
|
||||
.all(|event| event.side == OrderSide::Sell)
|
||||
);
|
||||
assert!(
|
||||
last.diagnostics
|
||||
.iter()
|
||||
.any(|event| event.contains("unsubmitted_phase_expired"))
|
||||
);
|
||||
assert!(!broker.has_pending_stock_pool_execution());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stock_pool_new_signal_supersedes_the_unsubmitted_buy_phase() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
let signal = chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
|
||||
let data = pool_batch_data();
|
||||
let broker = pool_batch_broker(true);
|
||||
let mut account = pool_batch_account();
|
||||
broker
|
||||
.execute_with_event_dates(
|
||||
date,
|
||||
signal,
|
||||
signal,
|
||||
&mut account,
|
||||
&data,
|
||||
&pool_batch_decision("000002.SZ", "old", "09:35"),
|
||||
)
|
||||
.unwrap();
|
||||
pool_batch_tick(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
31,
|
||||
&pool_batch_decision("000003.SZ", "new", "09:35"),
|
||||
);
|
||||
pool_batch_tick(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
32,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
pool_batch_tick(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
33,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
assert!(account.position("000002.SZ").is_none());
|
||||
assert_eq!(account.position("000003.SZ").unwrap().quantity, 200);
|
||||
assert!(!broker.has_pending_stock_pool_execution());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stock_pool_after_sell_uses_fresh_quotes_and_actual_submission_clock() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
let signal = chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
|
||||
let data = pool_batch_data_with(|quote| {
|
||||
if quote.symbol == "000002.SZ" {
|
||||
quote.last_price = 10.2;
|
||||
quote.bid1 = 10.2;
|
||||
quote.ask1 = 10.2;
|
||||
quote.amount_delta = 2040.0;
|
||||
}
|
||||
true
|
||||
});
|
||||
let broker = pool_batch_broker(false);
|
||||
let mut account = pool_batch_account();
|
||||
let mut decision = pool_batch_decision("000002.SZ", "fresh", "09:35");
|
||||
if let OrderIntent::StockPool { contract } = &mut decision.order_intents[0] {
|
||||
contract.rule.pricing_mode = crate::stock_pool_execution::POOL_PRICE_FORMULA_LIMIT.into();
|
||||
contract.rule.sell_offset_bps = 400;
|
||||
}
|
||||
broker
|
||||
.execute_with_event_dates(date, signal, signal, &mut account, &data, &decision)
|
||||
.unwrap();
|
||||
let result = pool_batch_tick(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
31,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
account.position("000002.SZ").unwrap().quantity,
|
||||
100,
|
||||
"2000/10.2 rounds to one 100-share lot, not 200 at stale open"
|
||||
);
|
||||
let fill = result
|
||||
.fill_events
|
||||
.iter()
|
||||
.find(|fill| fill.symbol == "000002.SZ")
|
||||
.unwrap();
|
||||
assert_eq!(fill.price, 10.2);
|
||||
assert_eq!(
|
||||
fill.execution_start_timestamp,
|
||||
Some(date.and_hms_opt(9, 31, 0).unwrap())
|
||||
);
|
||||
let event = result
|
||||
.order_events
|
||||
.iter()
|
||||
.find(|event| event.side == OrderSide::Buy)
|
||||
.unwrap();
|
||||
assert_eq!(event.decision_date, Some(signal));
|
||||
assert_eq!(event.order_created_date, Some(date));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stock_pool_after_sell_rejects_missing_quote_instead_of_reusing_daily_open() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
let signal = chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
|
||||
let data = pool_batch_data_with(|quote| quote.symbol != "000002.SZ");
|
||||
let broker = pool_batch_broker(false);
|
||||
let mut account = pool_batch_account();
|
||||
broker
|
||||
.execute_with_event_dates(
|
||||
date,
|
||||
signal,
|
||||
signal,
|
||||
&mut account,
|
||||
&data,
|
||||
&pool_batch_decision("000002.SZ", "missing", "09:35"),
|
||||
)
|
||||
.unwrap();
|
||||
broker
|
||||
.runtime_intraday_start_time
|
||||
.set(Some(chrono::NaiveTime::from_hms_opt(9, 31, 0).unwrap()));
|
||||
broker
|
||||
.runtime_intraday_end_time
|
||||
.set(Some(chrono::NaiveTime::from_hms_opt(9, 31, 0).unwrap()));
|
||||
let error = broker
|
||||
.execute(date, &mut account, &data, &StrategyDecision::default())
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("stock_pool_execution_quote_missing:000002.SZ"),
|
||||
"{error}"
|
||||
);
|
||||
assert!(account.position("000002.SZ").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stock_pool_delayed_take_profit_does_not_rebuy_the_same_generation_exit() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
let signal = chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
|
||||
let data = pool_batch_data();
|
||||
let broker = pool_batch_broker(false);
|
||||
let mut account = PortfolioState::new(0.0);
|
||||
account.position_mut("000001.SZ").buy(
|
||||
chrono::NaiveDate::from_ymd_opt(2024, 12, 30).unwrap(),
|
||||
200,
|
||||
9.0,
|
||||
);
|
||||
let mut decision = pool_batch_decision("000002.SZ", "take-profit", "09:35");
|
||||
if let OrderIntent::StockPool { contract } = &mut decision.order_intents[0] {
|
||||
let symbols = vec!["000001.SZ".to_owned(), "000002.SZ".to_owned()];
|
||||
contract.selection.requested_symbols = symbols.clone();
|
||||
contract.selection.normal_trading_symbols = symbols.clone();
|
||||
contract.selection.risk_eligible_symbols = symbols.clone();
|
||||
contract.selection.final_symbols = symbols;
|
||||
contract.constraints.target_holding_count = Some(2);
|
||||
contract.members.insert(
|
||||
0,
|
||||
crate::stock_pool_execution::StockPoolMemberSpec {
|
||||
symbol: "000001.SZ".into(),
|
||||
recommendation_reason: String::new(),
|
||||
requested_order: 0,
|
||||
target_weight_bps: None,
|
||||
stop_loss: None,
|
||||
take_profit: Some(rust_decimal::Decimal::new(5, 2)),
|
||||
},
|
||||
);
|
||||
contract.members[1].requested_order = 1;
|
||||
}
|
||||
broker
|
||||
.execute_with_event_dates(date, signal, signal, &mut account, &data, &decision)
|
||||
.unwrap();
|
||||
let result = pool_batch_tick(
|
||||
&broker,
|
||||
&mut account,
|
||||
&data,
|
||||
31,
|
||||
&StrategyDecision::default(),
|
||||
);
|
||||
assert!(
|
||||
account
|
||||
.position("000001.SZ")
|
||||
.is_none_or(|p| p.quantity == 0)
|
||||
);
|
||||
assert_eq!(account.position("000002.SZ").unwrap().quantity, 200);
|
||||
assert!(
|
||||
!result
|
||||
.order_events
|
||||
.iter()
|
||||
.any(|event| event.symbol == "000001.SZ" && event.side == OrderSide::Buy)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stock_pool_pending_phase_cannot_cross_the_execution_session() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
let signal = chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap();
|
||||
let data = pool_batch_data();
|
||||
let broker = pool_batch_broker(false);
|
||||
let mut account = pool_batch_account();
|
||||
let mut report = broker
|
||||
.execute_with_event_dates(
|
||||
date,
|
||||
signal,
|
||||
signal,
|
||||
&mut account,
|
||||
&data,
|
||||
&pool_batch_decision("000002.SZ", "end", "09:35"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(broker.has_pending_stock_pool_execution());
|
||||
broker.finish_stock_pool_session(date, &mut report);
|
||||
assert!(!broker.has_pending_stock_pool_execution());
|
||||
assert!(
|
||||
report
|
||||
.diagnostics
|
||||
.iter()
|
||||
.any(|event| event.contains("unsubmitted_phase_expired"))
|
||||
);
|
||||
assert_eq!(
|
||||
broker.open_order_views().len(),
|
||||
1,
|
||||
"session cleanup preserves broker order history and remainder"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stock_pool_engine_drives_the_pending_buy_without_a_minute_strategy_callback() {
|
||||
use crate::{BacktestConfig, BacktestEngine, BacktestError, Strategy, StrategyContext};
|
||||
struct DailyPool;
|
||||
impl Strategy for DailyPool {
|
||||
fn name(&self) -> &str {
|
||||
"daily-pool-batch"
|
||||
}
|
||||
fn requires_minute_callbacks(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn schedule_rules(&self) -> Vec<crate::ScheduleRule> {
|
||||
vec![
|
||||
crate::ScheduleRule::daily("open", crate::ScheduleStage::OnDay)
|
||||
.with_time_rule(crate::ScheduleTimeRule::physical_time(9, 30)),
|
||||
]
|
||||
}
|
||||
fn on_scheduled(
|
||||
&mut self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
_: &crate::ScheduleRule,
|
||||
) -> Result<StrategyDecision, BacktestError> {
|
||||
if ctx.execution_date.day() == 2 {
|
||||
Ok(StrategyDecision {
|
||||
order_intents: vec![OrderIntent::LimitTargetShares {
|
||||
symbol: "000001.SZ".into(),
|
||||
target_quantity: 200,
|
||||
limit_price: 10.0,
|
||||
reason: "initial-entry".into(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
} else {
|
||||
Ok(pool_batch_decision("000002.SZ", "rotation", "09:35"))
|
||||
}
|
||||
}
|
||||
fn on_minute(
|
||||
&mut self,
|
||||
_: &StrategyContext<'_>,
|
||||
_: &IntradayExecutionQuote,
|
||||
) -> Result<StrategyDecision, BacktestError> {
|
||||
panic!("this daily strategy must not be rerun to continue a pending batch")
|
||||
}
|
||||
}
|
||||
use chrono::Datelike;
|
||||
let first = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
let last = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).unwrap();
|
||||
let base = pool_batch_data();
|
||||
let mut market = Vec::new();
|
||||
let mut factors = Vec::new();
|
||||
let mut candidates = Vec::new();
|
||||
let mut benchmarks = Vec::new();
|
||||
let mut quotes = Vec::new();
|
||||
for date in [first, last] {
|
||||
for symbol in ["000001.SZ", "000002.SZ", "000003.SZ"] {
|
||||
let mut row = base.market(first, symbol).unwrap().clone();
|
||||
row.date = date;
|
||||
market.push(row);
|
||||
let mut row = base.candidate(first, symbol).unwrap().clone();
|
||||
row.date = date;
|
||||
candidates.push(row);
|
||||
factors.push(crate::data::DailyFactorSnapshot {
|
||||
date,
|
||||
symbol: symbol.into(),
|
||||
market_cap_bn: 10.,
|
||||
free_float_cap_bn: 10.,
|
||||
pe_ttm: 10.,
|
||||
turnover_ratio: None,
|
||||
effective_turnover_ratio: None,
|
||||
adjustment_factor_backward1: Some(1.),
|
||||
extra_factors: Default::default(),
|
||||
});
|
||||
for original in base.execution_quotes_on(first, symbol) {
|
||||
let mut quote = original.clone();
|
||||
quote.date = date;
|
||||
quote.timestamp = date.and_time(original.timestamp.time());
|
||||
quotes.push(quote);
|
||||
}
|
||||
}
|
||||
let mut row = limit_test_benchmark();
|
||||
row.date = date;
|
||||
benchmarks.push(row);
|
||||
}
|
||||
let data = DataSet::from_components_with_actions_and_quotes(
|
||||
base.instruments().values().cloned().collect(),
|
||||
market,
|
||||
factors,
|
||||
candidates,
|
||||
benchmarks,
|
||||
Vec::new(),
|
||||
quotes,
|
||||
)
|
||||
.unwrap()
|
||||
.with_additional_trading_dates([chrono::NaiveDate::from_ymd_opt(2024, 12, 31).unwrap()]);
|
||||
let config = BacktestConfig {
|
||||
initial_cash: 2000.0,
|
||||
benchmark_code: "000852.SH".into(),
|
||||
start_date: Some(first),
|
||||
end_date: Some(last),
|
||||
decision_lag_trading_days: 0,
|
||||
execution_price_field: PriceField::Open,
|
||||
};
|
||||
let result = BacktestEngine::new(data, DailyPool, pool_batch_broker(false), config)
|
||||
.run()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
result.fills.len(),
|
||||
3,
|
||||
"initial buy, delayed sell, resumed buy: orders={:?} equity={:?}",
|
||||
result.order_events,
|
||||
result.equity_curve
|
||||
);
|
||||
assert_eq!(result.fills[2].symbol, "000002.SZ");
|
||||
assert_eq!(result.fills[2].quantity, 200);
|
||||
assert_eq!(
|
||||
result.fills[2].execution_timestamp,
|
||||
Some(last.and_hms_opt(9, 31, 0).unwrap())
|
||||
);
|
||||
assert_eq!(result.holdings_summary.len(), 1);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+590
-215
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,272 @@
|
||||
//! Run-local, receipt-bound model projections for a completed decision session.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::{Component, Path};
|
||||
|
||||
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::engine::BacktestError;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DecisionProjectionContract {
|
||||
pub contract_sha256: String,
|
||||
pub fields: BTreeSet<String>,
|
||||
pub input_cutoff_time: NaiveTime,
|
||||
}
|
||||
|
||||
impl DecisionProjectionContract {
|
||||
pub fn validate(&self) -> Result<(), BacktestError> {
|
||||
require_sha256(&self.contract_sha256)?;
|
||||
if self.fields.is_empty() || self.fields.iter().any(|field| !valid_projection_field(field)) {
|
||||
return Err(error("projection fields must be explicit canonical numeric identifiers, not market or typed factors"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DecisionProjectionRequest {
|
||||
pub contract_sha256: String,
|
||||
pub decision_date: NaiveDate,
|
||||
pub execution_date: NaiveDate,
|
||||
pub as_of: NaiveDateTime,
|
||||
pub symbols: BTreeSet<String>,
|
||||
pub fields: BTreeSet<String>,
|
||||
}
|
||||
|
||||
impl DecisionProjectionRequest {
|
||||
pub fn sha256(&self) -> Result<String, BacktestError> {
|
||||
json_sha256(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DecisionProjectionRow {
|
||||
pub symbol: String,
|
||||
pub values: BTreeMap<String, f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DecisionProjectionExclusion {
|
||||
pub symbol: String,
|
||||
pub classification: String,
|
||||
pub reason: String,
|
||||
pub source_sha256: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DecisionProjectionReference {
|
||||
pub path: String,
|
||||
pub sha256: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DecisionProjectionBatch {
|
||||
pub contract_sha256: String,
|
||||
pub decision_date: NaiveDate,
|
||||
pub execution_date: NaiveDate,
|
||||
pub input_available_at: NaiveDateTime,
|
||||
pub rows: Vec<DecisionProjectionRow>,
|
||||
pub exclusions: Vec<DecisionProjectionExclusion>,
|
||||
pub inference_reference: DecisionProjectionReference,
|
||||
pub provider_request_sha256: String,
|
||||
pub provider_response_sha256: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DecisionProjectionReceipt {
|
||||
pub schema_version: String,
|
||||
pub request: DecisionProjectionRequest,
|
||||
pub request_sha256: String,
|
||||
pub input_available_at: NaiveDateTime,
|
||||
pub inference_reference: DecisionProjectionReference,
|
||||
pub provider_request_sha256: String,
|
||||
pub provider_response_sha256: String,
|
||||
pub values_sha256: String,
|
||||
pub row_count: usize,
|
||||
pub excluded_count: usize,
|
||||
}
|
||||
|
||||
impl DecisionProjectionBatch {
|
||||
pub(crate) fn validate(
|
||||
&self,
|
||||
request: &DecisionProjectionRequest,
|
||||
) -> Result<DecisionProjectionReceipt, BacktestError> {
|
||||
if self.contract_sha256 != request.contract_sha256
|
||||
|| self.decision_date != request.decision_date
|
||||
|| self.execution_date != request.execution_date
|
||||
|| request.as_of.date() != request.decision_date
|
||||
|| request.decision_date >= request.execution_date
|
||||
|| self.input_available_at > request.as_of
|
||||
{
|
||||
return Err(error("projection identity or causal availability differs from the decision request"));
|
||||
}
|
||||
require_sha256(&self.inference_reference.sha256)?;
|
||||
require_sha256(&self.provider_request_sha256)?;
|
||||
require_sha256(&self.provider_response_sha256)?;
|
||||
let path = Path::new(&self.inference_reference.path);
|
||||
if !path.is_absolute() || path.components().any(|part| matches!(part, Component::ParentDir)) {
|
||||
return Err(error("projection inference receipt requires an absolute immutable artifact reference"));
|
||||
}
|
||||
if request.symbols.is_empty() || request.fields.is_empty() {
|
||||
return Err(error("projection request population and fields must be explicit"));
|
||||
}
|
||||
let mut covered = BTreeSet::new();
|
||||
for row in &self.rows {
|
||||
if !request.symbols.contains(&row.symbol) || !covered.insert(row.symbol.clone()) {
|
||||
return Err(error("projection returned an unexpected or duplicate symbol"));
|
||||
}
|
||||
if row.values.keys().collect::<BTreeSet<_>>() != request.fields.iter().collect()
|
||||
|| row.values.values().any(|value| !value.is_finite())
|
||||
{
|
||||
return Err(error("projection numeric field coverage is incomplete or non-finite"));
|
||||
}
|
||||
}
|
||||
for exclusion in &self.exclusions {
|
||||
if !request.symbols.contains(&exclusion.symbol) || !covered.insert(exclusion.symbol.clone()) {
|
||||
return Err(error("projection exclusion overlaps a value or repeats an unexpected symbol"));
|
||||
}
|
||||
if exclusion.classification != "valid_market_absence" || exclusion.reason.trim().is_empty() {
|
||||
return Err(error("projection missing data is unclassified; no order skip is permitted"));
|
||||
}
|
||||
require_sha256(&exclusion.source_sha256)?;
|
||||
}
|
||||
if covered != request.symbols {
|
||||
return Err(error("projection did not explain every requested symbol"));
|
||||
}
|
||||
// Digest the actual normalized values consumed by this engine, not a
|
||||
// provider's summary flag. Stable ordering preserves same-input replay.
|
||||
let rows = self.rows.iter().map(|row| (&row.symbol, &row.values)).collect::<BTreeMap<_, _>>();
|
||||
let exclusions = self.exclusions.iter().map(|item| (&item.symbol, item)).collect::<BTreeMap<_, _>>();
|
||||
Ok(DecisionProjectionReceipt {
|
||||
schema_version: "fidc.decision-projection-receipt/v1".into(),
|
||||
request: request.clone(),
|
||||
request_sha256: request.sha256()?,
|
||||
input_available_at: self.input_available_at,
|
||||
inference_reference: self.inference_reference.clone(),
|
||||
provider_request_sha256: self.provider_request_sha256.clone(),
|
||||
provider_response_sha256: self.provider_response_sha256.clone(),
|
||||
values_sha256: json_sha256(&(rows, exclusions))?,
|
||||
row_count: self.rows.len(),
|
||||
excluded_count: self.exclusions.len(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn valid_projection_field(field: &str) -> bool {
|
||||
let mut bytes = field.bytes();
|
||||
let identifier = bytes.next().is_some_and(|c| c.is_ascii_lowercase() || c == b'_')
|
||||
&& bytes.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'_');
|
||||
identifier && !matches!(field,
|
||||
"market_cap" | "market_cap_bn" | "free_float_cap" | "free_float_cap_bn"
|
||||
| "pe" | "pe_ttm" | "turnover_ratio" | "effective_turnover_ratio"
|
||||
| "adjustment_factor_backward1" | "open" | "day_open" | "close"
|
||||
| "high" | "low" | "last" | "last_price" | "volume" | "amount"
|
||||
| "prev_close" | "bid1" | "ask1" | "upper_limit" | "lower_limit")
|
||||
}
|
||||
|
||||
fn require_sha256(value: &str) -> Result<(), BacktestError> {
|
||||
if value.len() != 64 || !value.bytes().all(|c| c.is_ascii_digit() || (b'a'..=b'f').contains(&c)) {
|
||||
return Err(error("projection requires canonical SHA256 identities"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn json_sha256(value: &impl Serialize) -> Result<String, BacktestError> {
|
||||
let bytes = serde_json::to_vec(value).map_err(|_| error("projection receipt could not be serialized"))?;
|
||||
Ok(format!("{:x}", Sha256::digest(bytes)))
|
||||
}
|
||||
|
||||
fn error(message: &str) -> BacktestError {
|
||||
BacktestError::Execution(message.into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn request() -> DecisionProjectionRequest {
|
||||
let decision_date = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
DecisionProjectionRequest {
|
||||
contract_sha256: "1".repeat(64), decision_date,
|
||||
execution_date: NaiveDate::from_ymd_opt(2025, 1, 3).unwrap(),
|
||||
as_of: decision_date.and_hms_opt(16, 0, 0).unwrap(),
|
||||
symbols: BTreeSet::from(["000001.SZ".into(), "000002.SZ".into()]),
|
||||
fields: BTreeSet::from(["model_score_a".into()]),
|
||||
}
|
||||
}
|
||||
|
||||
fn batch() -> DecisionProjectionBatch {
|
||||
let request = request();
|
||||
DecisionProjectionBatch {
|
||||
contract_sha256: request.contract_sha256,
|
||||
decision_date: request.decision_date, execution_date: request.execution_date,
|
||||
input_available_at: request.as_of,
|
||||
rows: vec![DecisionProjectionRow { symbol: "000001.SZ".into(), values: BTreeMap::from([("model_score_a".into(), 0.5)]) }],
|
||||
exclusions: vec![DecisionProjectionExclusion { symbol: "000002.SZ".into(), classification: "valid_market_absence".into(), reason: "dated suspension evidence".into(), source_sha256: "2".repeat(64) }],
|
||||
inference_reference: DecisionProjectionReference { path: "/private/inference.json".into(), sha256: "3".repeat(64) },
|
||||
provider_request_sha256: "4".repeat(64), provider_response_sha256: "5".repeat(64),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_projection_coverage_and_classified_absence_are_receipted() {
|
||||
let receipt = batch().validate(&request()).unwrap();
|
||||
assert_eq!((receipt.row_count, receipt.excluded_count), (1, 1));
|
||||
assert_eq!(receipt.request_sha256, request().sha256().unwrap());
|
||||
let mut changed = batch(); changed.rows[0].values.insert("model_score_a".into(), 0.6);
|
||||
assert_ne!(changed.validate(&request()).unwrap().values_sha256, receipt.values_sha256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_contract_does_not_shadow_core_market_or_typed_fields() {
|
||||
for field in ["", "Score", "model-score", "close", "market_cap_bn", "adjustment_factor_backward1"] {
|
||||
let contract = DecisionProjectionContract { contract_sha256: "1".repeat(64), fields: BTreeSet::from([field.into()]), input_cutoff_time: NaiveTime::from_hms_opt(16, 0, 0).unwrap() };
|
||||
assert!(contract.validate().is_err(), "{field}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_rejects_incomplete_duplicate_extra_and_overlapping_population() {
|
||||
let mut examples = vec![];
|
||||
let mut item = batch(); item.exclusions.clear(); examples.push(item);
|
||||
let mut item = batch(); item.rows.push(item.rows[0].clone()); examples.push(item);
|
||||
let mut item = batch(); item.rows[0].symbol = "999999.SZ".into(); examples.push(item);
|
||||
let mut item = batch(); item.exclusions[0].symbol = item.rows[0].symbol.clone(); examples.push(item);
|
||||
for item in examples { assert!(item.validate(&request()).is_err()); }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_rejects_nonfinite_wrong_fields_and_unproven_absence() {
|
||||
let mut examples = vec![];
|
||||
for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
|
||||
let mut item = batch(); item.rows[0].values.insert("model_score_a".into(), value); examples.push(item);
|
||||
}
|
||||
let mut item = batch(); item.rows[0].values.clear(); examples.push(item);
|
||||
let mut item = batch(); item.rows[0].values.insert("extra".into(), 1.0); examples.push(item);
|
||||
let mut item = batch(); item.exclusions[0].classification = "unknown".into(); examples.push(item);
|
||||
let mut item = batch(); item.exclusions[0].reason.clear(); examples.push(item);
|
||||
let mut item = batch(); item.exclusions[0].source_sha256.clear(); examples.push(item);
|
||||
for item in examples { assert!(item.validate(&request()).is_err()); }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projection_rejects_future_drift_or_unbound_receipts() {
|
||||
let mut examples = vec![];
|
||||
let mut item = batch(); item.input_available_at += chrono::Duration::seconds(1); examples.push(item);
|
||||
let mut item = batch(); item.decision_date = item.execution_date; examples.push(item);
|
||||
let mut item = batch(); item.contract_sha256 = "0".repeat(64); examples.push(item);
|
||||
let mut item = batch(); item.inference_reference.path = "/private/../inference.json".into(); examples.push(item);
|
||||
let mut item = batch(); item.provider_response_sha256 = "A".repeat(64); examples.push(item);
|
||||
for item in examples { assert!(item.validate(&request()).is_err()); }
|
||||
}
|
||||
}
|
||||
+4146
-1018
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,129 @@
|
||||
//! Historical ETF execution fallback. Never manufactures an intraday bar.
|
||||
use chrono::{NaiveDate, NaiveDateTime, NaiveTime};
|
||||
use rust_decimal::Decimal;
|
||||
use crate::{BacktestError, DataSet};
|
||||
|
||||
pub(crate) fn opening_time() -> NaiveTime {
|
||||
NaiveTime::from_hms_opt(9, 30, 0).expect("valid exchange opening time")
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct EtfFallbackReference {
|
||||
pub price: f64,
|
||||
pub reference_date: NaiveDate,
|
||||
/// None means the next official session is outside the loaded calendar.
|
||||
/// No natural-day guess or price from beyond the requested run is used.
|
||||
pub execute_on: Option<NaiveDate>,
|
||||
pub immediate: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn reference(data: &DataSet, symbol: &str, at: NaiveDateTime) -> Result<EtfFallbackReference, BacktestError> {
|
||||
let fail = |reason: &str| BacktestError::Execution(format!(
|
||||
"etf_daily_open_fallback:{reason} symbol={symbol} signal_at={at}"
|
||||
));
|
||||
let instrument = data.instrument(symbol).ok_or_else(|| fail("instrument_identity_missing"))?;
|
||||
if !instrument.is_exchange_traded_fund() || instrument.listed_at.is_none() {
|
||||
return Err(fail("verified_etf_identity_required"));
|
||||
}
|
||||
if instrument.dated_market_absence_reason(at.date()).is_some() {
|
||||
return Err(fail("outside_instrument_lifecycle"));
|
||||
}
|
||||
if at.time() == opening_time() {
|
||||
let row = data.market(at.date(), symbol).ok_or_else(|| fail("daily_open_missing"))?;
|
||||
if !row.open.is_finite() || row.open <= 0.0 { return Err(fail("daily_open_invalid")); }
|
||||
return Ok(EtfFallbackReference { price: row.open, reference_date: at.date(), execute_on: Some(at.date()), immediate: true });
|
||||
}
|
||||
let previous = data.previous_trading_date(at.date(), 1).ok_or_else(|| fail("previous_official_session_missing"))?;
|
||||
let close = data.market(previous, symbol).map(|row| row.close).ok_or_else(|| fail("previous_completed_close_missing"))?;
|
||||
if !close.is_finite() || close <= 0.0 { return Err(fail("previous_completed_close_invalid")); }
|
||||
Ok(EtfFallbackReference {
|
||||
price: close, reference_date: previous, immediate: false,
|
||||
execute_on: if at.time() < opening_time() { Some(at.date()) } else { data.next_trading_date(at.date(), 1) },
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct DeferredEtfTarget {
|
||||
pub pool_id: String,
|
||||
pub generation: String,
|
||||
pub symbol: String,
|
||||
pub signal_date: NaiveDate,
|
||||
pub signal_at: NaiveDateTime,
|
||||
pub execute_on: Option<NaiveDate>,
|
||||
pub target_value: Decimal,
|
||||
pub target_weight_bps: i32,
|
||||
pub target_weight_ratio: Option<Decimal>,
|
||||
pub side: crate::stock_pool_execution::OrderSide,
|
||||
pub max_positions: usize,
|
||||
pub rule: std::sync::Arc<crate::stock_pool_execution::StockPoolExecutionRule>,
|
||||
pub members: std::sync::Arc<Vec<crate::stock_pool_execution::StockPoolMemberSpec>>,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
/// Owned by one broker/run. Replacing a full pool generation supersedes older
|
||||
/// queued targets; order of the latest candidate list is retained.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub(crate) struct DeferredEtfTargets {
|
||||
generations: std::collections::BTreeMap<String, String>,
|
||||
rows: Vec<DeferredEtfTarget>,
|
||||
}
|
||||
|
||||
impl DeferredEtfTargets {
|
||||
pub fn replace_generation(&mut self, pool_id: &str, generation: &str) -> usize {
|
||||
if self.generations.get(pool_id).is_some_and(|old| old == generation) { return 0; }
|
||||
self.generations.insert(pool_id.into(), generation.into());
|
||||
let before = self.rows.len();
|
||||
self.rows.retain(|row| row.pool_id != pool_id);
|
||||
before - self.rows.len()
|
||||
}
|
||||
pub fn upsert(&mut self, row: DeferredEtfTarget) {
|
||||
if let Some(existing) = self.rows.iter_mut().find(|v| v.pool_id == row.pool_id && v.symbol == row.symbol) {
|
||||
*existing = row;
|
||||
} else { self.rows.push(row); }
|
||||
}
|
||||
pub fn take_due(&mut self, date: NaiveDate) -> Vec<DeferredEtfTarget> {
|
||||
let mut due = Vec::new();
|
||||
self.rows.retain(|row| {
|
||||
if row.execute_on.is_some_and(|day| day <= date) { due.push(row.clone()); false } else { true }
|
||||
});
|
||||
due.sort_by_key(|row| match row.side { crate::stock_pool_execution::OrderSide::Sell => 0, crate::stock_pool_execution::OrderSide::Buy => 1 });
|
||||
due
|
||||
}
|
||||
pub fn len(&self) -> usize { self.rows.len() }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
fn target(symbol:&str,side:crate::stock_pool_execution::OrderSide,generation:&str)->DeferredEtfTarget {
|
||||
let date=NaiveDate::from_ymd_opt(2026,1,2).unwrap();
|
||||
DeferredEtfTarget {pool_id:"pool".into(),generation:generation.into(),symbol:symbol.into(),signal_date:date,signal_at:date.and_hms_opt(13,0,0).unwrap(),execute_on:NaiveDate::from_ymd_opt(2026,1,5),target_value:1000.into(),target_weight_bps:5000,target_weight_ratio:None,side,max_positions:2,rule:Default::default(),members:std::sync::Arc::new(vec![]),reason:"fixture".into()}
|
||||
}
|
||||
#[test]
|
||||
fn latest_generation_overwrites_pending_targets_and_preserves_candidate_order() {
|
||||
use crate::stock_pool_execution::OrderSide::{Buy,Sell};
|
||||
let mut queue=DeferredEtfTargets::default();
|
||||
queue.replace_generation("pool","v1");
|
||||
queue.upsert(target("510300.SH",Buy,"v1"));
|
||||
queue.upsert(target("159915.SZ",Buy,"v1"));
|
||||
assert_eq!(queue.replace_generation("pool","v1"),0);
|
||||
assert_eq!(queue.replace_generation("pool","v2"),2);
|
||||
queue.upsert(target("560450.SH",Buy,"v2"));
|
||||
queue.upsert(target("159915.SZ",Sell,"v2"));
|
||||
queue.upsert(target("510300.SH",Buy,"v2"));
|
||||
assert!(queue.take_due(NaiveDate::from_ymd_opt(2026,1,2).unwrap()).is_empty());
|
||||
let due=queue.take_due(NaiveDate::from_ymd_opt(2026,1,5).unwrap());
|
||||
assert_eq!(due.iter().map(|v|v.symbol.as_str()).collect::<Vec<_>>(),vec!["159915.SZ","560450.SH","510300.SH"]);
|
||||
assert!(due.iter().all(|v|v.generation=="v2"));
|
||||
assert_eq!(queue.len(),0);
|
||||
}
|
||||
#[test]
|
||||
fn no_loaded_next_session_is_not_guessed_from_natural_days() {
|
||||
let mut queue=DeferredEtfTargets::default();
|
||||
let mut item=target("510300.SH",crate::stock_pool_execution::OrderSide::Buy,"v1");
|
||||
item.execute_on=None;
|
||||
queue.upsert(item);
|
||||
assert!(queue.take_due(NaiveDate::from_ymd_opt(2026,2,1).unwrap()).is_empty());
|
||||
assert_eq!(queue.len(),1);
|
||||
}
|
||||
}
|
||||
@@ -181,8 +181,22 @@ impl OrderEvent {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FillOrigin {
|
||||
#[default]
|
||||
MarketExecution,
|
||||
DividendReinvestment,
|
||||
}
|
||||
|
||||
impl FillOrigin {
|
||||
pub fn is_market_execution(&self) -> bool { *self == Self::MarketExecution }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FillEvent {
|
||||
#[serde(default, skip_serializing_if = "FillOrigin::is_market_execution")]
|
||||
pub origin: FillOrigin,
|
||||
#[serde(with = "date_format")]
|
||||
pub date: NaiveDate,
|
||||
#[serde(default, with = "optional_date_format")]
|
||||
@@ -219,6 +233,14 @@ pub struct FillEvent {
|
||||
|
||||
impl FillEvent {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.origin == FillOrigin::DividendReinvestment && (
|
||||
self.order_id.is_some() || self.side != OrderSide::Buy
|
||||
|| self.commission != 0. || self.stamp_tax != 0. || self.transfer_fee != 0.
|
||||
|| self.execution_timestamp != self.date.and_hms_opt(0, 0, 0)
|
||||
|| self.execution_start_timestamp != self.execution_timestamp
|
||||
) {
|
||||
return Err("dividend accounting allocation cannot carry an exchange order, fees, or a market clock".into());
|
||||
}
|
||||
if self.symbol.trim().is_empty()
|
||||
|| self.quantity == 0
|
||||
|| !self.price.is_finite()
|
||||
@@ -311,12 +333,15 @@ pub enum ProcessEventKind {
|
||||
OrderUpdateReject,
|
||||
OrderUnsolicitedUpdate,
|
||||
Trade,
|
||||
ManualExecutionObserved,
|
||||
UniverseUpdated,
|
||||
UniverseSubscribed,
|
||||
UniverseUnsubscribed,
|
||||
AccountDepositWithdraw,
|
||||
AccountFinanceRepay,
|
||||
AccountManagementFee,
|
||||
SessionCapacityAudit,
|
||||
EtfExecutionFallback,
|
||||
}
|
||||
|
||||
impl ProcessEventKind {
|
||||
@@ -356,12 +381,15 @@ impl ProcessEventKind {
|
||||
Self::OrderUpdateReject => "order_update_reject",
|
||||
Self::OrderUnsolicitedUpdate => "order_unsolicited_update",
|
||||
Self::Trade => "trade",
|
||||
Self::ManualExecutionObserved => "manual_execution_observed",
|
||||
Self::UniverseUpdated => "universe_updated",
|
||||
Self::UniverseSubscribed => "universe_subscribed",
|
||||
Self::UniverseUnsubscribed => "universe_unsubscribed",
|
||||
Self::AccountDepositWithdraw => "account_deposit_withdraw",
|
||||
Self::AccountFinanceRepay => "account_finance_repay",
|
||||
Self::AccountManagementFee => "account_management_fee",
|
||||
Self::SessionCapacityAudit => "session_capacity_audit",
|
||||
Self::EtfExecutionFallback => "etf_execution_fallback",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,12 +415,15 @@ impl ProcessEventKind {
|
||||
| Self::OrderUpdateReject
|
||||
| Self::OrderUnsolicitedUpdate
|
||||
| Self::Trade
|
||||
| Self::ManualExecutionObserved
|
||||
| Self::UniverseUpdated
|
||||
| Self::UniverseSubscribed
|
||||
| Self::UniverseUnsubscribed
|
||||
| Self::AccountDepositWithdraw
|
||||
| Self::AccountFinanceRepay
|
||||
| Self::AccountManagementFee
|
||||
| Self::SessionCapacityAudit
|
||||
| Self::EtfExecutionFallback
|
||||
| Self::Settlement
|
||||
)
|
||||
}
|
||||
@@ -416,7 +447,7 @@ pub struct ProcessEvent {
|
||||
mod tests {
|
||||
use chrono::{NaiveDate, NaiveDateTime};
|
||||
|
||||
use super::{FillEvent, OrderEvent, OrderSide, OrderStatus, ProcessEventKind};
|
||||
use super::{FillEvent, FillOrigin, OrderEvent, OrderSide, OrderStatus, ProcessEventKind};
|
||||
|
||||
fn order_event(status: OrderStatus, filled_quantity: u32) -> OrderEvent {
|
||||
OrderEvent {
|
||||
@@ -460,6 +491,7 @@ mod tests {
|
||||
|
||||
fn fill_event(start: Option<NaiveDateTime>, end: Option<NaiveDateTime>) -> FillEvent {
|
||||
FillEvent {
|
||||
origin: crate::events::FillOrigin::MarketExecution,
|
||||
date: NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(),
|
||||
decision_date: None,
|
||||
order_created_date: None,
|
||||
@@ -480,6 +512,27 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accounting_origin_cannot_disguise_an_exchange_order_or_fee() {
|
||||
let at = NaiveDate::from_ymd_opt(2025, 1, 2).unwrap().and_hms_opt(0, 0, 0).unwrap();
|
||||
let mut fill = fill_event(Some(at), Some(at));
|
||||
fill.origin = FillOrigin::DividendReinvestment;
|
||||
fill.order_id = None;
|
||||
fill.commission = 0.;
|
||||
fill.net_cash_flow = -1000.;
|
||||
assert!(fill.validate().is_ok());
|
||||
for kind in 0..3 {
|
||||
let mut invalid = fill.clone();
|
||||
match kind {
|
||||
0 => invalid.order_id = Some(1),
|
||||
1 => invalid.commission = 1.,
|
||||
_ => { invalid.execution_timestamp = Some(at + chrono::Duration::hours(9)); invalid.execution_start_timestamp = invalid.execution_timestamp; }
|
||||
}
|
||||
assert!(invalid.validate().is_err());
|
||||
}
|
||||
assert!(serde_json::to_value(fill_event(None, None)).unwrap().get("origin").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_execution_timestamp_range_is_explicit_and_backward_compatible() {
|
||||
let start = NaiveDate::from_ymd_opt(2025, 1, 2)
|
||||
|
||||
@@ -12,6 +12,19 @@ pub enum VolumeCapacityMode {
|
||||
SessionCapacityAudit,
|
||||
}
|
||||
|
||||
impl VolumeCapacityMode {
|
||||
pub fn validate(self, enabled: bool, has_execution_observations: bool) -> Result<(), CapacityError> {
|
||||
if !enabled { return Ok(()); }
|
||||
match self {
|
||||
Self::ExecutionObservation if !has_execution_observations => Err(CapacityError::MissingObservation),
|
||||
Self::CompletedBar => Err(CapacityError::MissingCompletedBar),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn limits_execution_quantity(self) -> bool { self != Self::SessionCapacityAudit }
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
|
||||
pub enum CapacityError {
|
||||
#[error("execution capacity ratio must be finite and in (0, 1]")]
|
||||
@@ -26,6 +39,28 @@ pub enum CapacityError {
|
||||
WrongSession,
|
||||
#[error("execution-time capacity is missing; daily session volume cannot size an earlier fill")]
|
||||
MissingObservation,
|
||||
#[error("completed_bar capacity requires declared bar end and availability; an undated daily total is not a completed observation")]
|
||||
MissingCompletedBar,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CapacityAuditSummary {
|
||||
pub mode: VolumeCapacityMode,
|
||||
pub enabled: bool,
|
||||
pub participation_rate: f64,
|
||||
pub audited_symbol_sessions: usize,
|
||||
pub failed_symbol_sessions: usize,
|
||||
pub audit_passed: Option<bool>,
|
||||
pub execution_time_capacity_proven: bool,
|
||||
}
|
||||
|
||||
impl CapacityAuditSummary {
|
||||
pub fn observe(&mut self, audit: &SessionCapacityAudit) {
|
||||
self.audited_symbol_sessions += 1;
|
||||
self.failed_symbol_sessions += usize::from(!audit.passed);
|
||||
self.audit_passed = Some(self.failed_symbol_sessions == 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Decimal semantics of the frozen JSON rate, evaluated without a float product.
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
//! Check typed pending intent numbers before JSON could replace NaN/Inf with null.
|
||||
//! This traverses the original Serialize representation without materializing it.
|
||||
use serde::{Serialize, Serializer, ser};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct Finite;
|
||||
|
||||
pub(crate) fn validate(value: &impl Serialize) -> Result<(), serde_json::Error> {
|
||||
value.serialize(Finite)
|
||||
}
|
||||
|
||||
macro_rules! scalar {
|
||||
($($method:ident: $ty:ty),* $(,)?) => {$(
|
||||
fn $method(self, _: $ty) -> Result<(), Self::Error> { Ok(()) }
|
||||
)*};
|
||||
}
|
||||
|
||||
impl Serializer for Finite {
|
||||
type Ok = ();
|
||||
type Error = serde_json::Error;
|
||||
type SerializeSeq = Self;
|
||||
type SerializeTuple = Self;
|
||||
type SerializeTupleStruct = Self;
|
||||
type SerializeTupleVariant = Self;
|
||||
type SerializeMap = Self;
|
||||
type SerializeStruct = Self;
|
||||
type SerializeStructVariant = Self;
|
||||
|
||||
scalar!(serialize_bool: bool, serialize_i8: i8, serialize_i16: i16,
|
||||
serialize_i32: i32, serialize_i64: i64, serialize_i128: i128,
|
||||
serialize_u8: u8, serialize_u16: u16, serialize_u32: u32,
|
||||
serialize_u64: u64, serialize_u128: u128, serialize_char: char,
|
||||
serialize_str: &str, serialize_bytes: &[u8]);
|
||||
fn serialize_f32(self, value: f32) -> Result<(), Self::Error> {
|
||||
self.serialize_f64(f64::from(value))
|
||||
}
|
||||
fn serialize_f64(self, value: f64) -> Result<(), Self::Error> {
|
||||
if value.is_finite() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ser::Error::custom(
|
||||
"pending strategy intent contains a non-finite number",
|
||||
))
|
||||
}
|
||||
}
|
||||
fn serialize_none(self) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<(), Self::Error> {
|
||||
value.serialize(self)
|
||||
}
|
||||
fn serialize_unit(self) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
fn serialize_unit_struct(self, _: &'static str) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
fn serialize_unit_variant(
|
||||
self,
|
||||
_: &'static str,
|
||||
_: u32,
|
||||
_: &'static str,
|
||||
) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
fn serialize_newtype_struct<T: ?Sized + Serialize>(
|
||||
self,
|
||||
_: &'static str,
|
||||
value: &T,
|
||||
) -> Result<(), Self::Error> {
|
||||
value.serialize(self)
|
||||
}
|
||||
fn serialize_newtype_variant<T: ?Sized + Serialize>(
|
||||
self,
|
||||
_: &'static str,
|
||||
_: u32,
|
||||
_: &'static str,
|
||||
value: &T,
|
||||
) -> Result<(), Self::Error> {
|
||||
value.serialize(self)
|
||||
}
|
||||
fn serialize_seq(self, _: Option<usize>) -> Result<Self, Self::Error> {
|
||||
Ok(self)
|
||||
}
|
||||
fn serialize_tuple(self, _: usize) -> Result<Self, Self::Error> {
|
||||
Ok(self)
|
||||
}
|
||||
fn serialize_tuple_struct(self, _: &'static str, _: usize) -> Result<Self, Self::Error> {
|
||||
Ok(self)
|
||||
}
|
||||
fn serialize_tuple_variant(
|
||||
self,
|
||||
_: &'static str,
|
||||
_: u32,
|
||||
_: &'static str,
|
||||
_: usize,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(self)
|
||||
}
|
||||
fn serialize_map(self, _: Option<usize>) -> Result<Self, Self::Error> {
|
||||
Ok(self)
|
||||
}
|
||||
fn serialize_struct(self, _: &'static str, _: usize) -> Result<Self, Self::Error> {
|
||||
Ok(self)
|
||||
}
|
||||
fn serialize_struct_variant(
|
||||
self,
|
||||
_: &'static str,
|
||||
_: u32,
|
||||
_: &'static str,
|
||||
_: usize,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! sequence {
|
||||
($trait:ident, $method:ident) => {
|
||||
impl ser::$trait for Finite {
|
||||
type Ok = ();
|
||||
type Error = serde_json::Error;
|
||||
fn $method<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
|
||||
value.serialize(*self)
|
||||
}
|
||||
fn end(self) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
sequence!(SerializeSeq, serialize_element);
|
||||
sequence!(SerializeTuple, serialize_element);
|
||||
sequence!(SerializeTupleStruct, serialize_field);
|
||||
sequence!(SerializeTupleVariant, serialize_field);
|
||||
|
||||
impl ser::SerializeMap for Finite {
|
||||
type Ok = ();
|
||||
type Error = serde_json::Error;
|
||||
fn serialize_key<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
|
||||
value.serialize(*self)
|
||||
}
|
||||
fn serialize_value<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
|
||||
value.serialize(*self)
|
||||
}
|
||||
fn end(self) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! structure {
|
||||
($trait:ident) => {
|
||||
impl ser::$trait for Finite {
|
||||
type Ok = ();
|
||||
type Error = serde_json::Error;
|
||||
fn serialize_field<T: ?Sized + Serialize>(
|
||||
&mut self,
|
||||
_: &'static str,
|
||||
value: &T,
|
||||
) -> Result<(), Self::Error> {
|
||||
value.serialize(*self)
|
||||
}
|
||||
fn end(self) -> Result<(), Self::Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
structure!(SerializeStruct);
|
||||
structure!(SerializeStructVariant);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::strategy::{OrderIntent, StrategyDecision};
|
||||
|
||||
#[test]
|
||||
fn pending_numbers_cannot_be_silently_serialized_as_optional_nulls() {
|
||||
for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
|
||||
let decision = StrategyDecision {
|
||||
order_intents: vec![
|
||||
OrderIntent::LimitTargetPercent {
|
||||
symbol: "000001.SZ".into(),
|
||||
target_percent: 0.5,
|
||||
limit_price: value,
|
||||
reason: "test".into(),
|
||||
}
|
||||
.with_time_in_force(crate::strategy::OrderTimeInForce::Day),
|
||||
],
|
||||
..Default::default()
|
||||
};
|
||||
assert!(validate(&decision).is_err());
|
||||
assert!(validate(&vec![Some(value)]).is_err());
|
||||
}
|
||||
assert!(validate(&(None::<f64>, vec![0., -0., 0.123456789], "NaN")).is_ok());
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,17 @@ impl FixedMoney {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn to_decimal_string(self) -> String {
|
||||
let magnitude = self.0.unsigned_abs();
|
||||
let scale = MONEY_SCALE as u128;
|
||||
let sign = if self.0 < 0 { "-" } else { "" };
|
||||
let width = MONEY_SCALE.ilog10() as usize;
|
||||
format!("{sign}{}.{:0width$}", magnitude / scale, magnitude % scale)
|
||||
.trim_end_matches('0')
|
||||
.trim_end_matches('.')
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn from_decimal_str(value: &str) -> Result<Self, String> {
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
|
||||
@@ -26,7 +26,7 @@ fn sum_futures_money(values: impl IntoIterator<Item = FixedMoney>, label: &str)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
|
||||
pub enum FuturesDirection {
|
||||
Long,
|
||||
Short,
|
||||
@@ -62,7 +62,7 @@ impl FuturesDirection {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
pub enum FuturesPositionEffect {
|
||||
Open,
|
||||
Close,
|
||||
@@ -81,7 +81,7 @@ impl FuturesPositionEffect {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
pub struct FuturesContractSpec {
|
||||
pub contract_multiplier: f64,
|
||||
pub long_margin_rate: f64,
|
||||
@@ -190,7 +190,7 @@ impl FuturesTransactionCostModel {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct FuturesOrderIntent {
|
||||
pub symbol: String,
|
||||
pub direction: FuturesDirection,
|
||||
@@ -1048,6 +1048,7 @@ impl FuturesAccountState {
|
||||
)
|
||||
.to_f64();
|
||||
report.fill_events.push(FillEvent {
|
||||
origin: crate::events::FillOrigin::MarketExecution,
|
||||
date,
|
||||
decision_date: None,
|
||||
order_created_date: None,
|
||||
|
||||
@@ -63,7 +63,7 @@ pub struct HoldingLifecycleEvidence {
|
||||
pub last_sell_date: Option<NaiveDate>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct AutomaticTradePermission {
|
||||
pub buy_denial: Option<&'static str>,
|
||||
pub sell_denial: Option<&'static str>,
|
||||
@@ -120,10 +120,25 @@ impl AutomaticTradeProtection {
|
||||
execution_date: NaiveDate,
|
||||
evidence: &HoldingLifecycleEvidence,
|
||||
calendar: &TradingCalendar,
|
||||
) -> Result<AutomaticTradePermission, String> {
|
||||
self.evaluate_with_predecessors(symbol, execution_date, evidence, calendar, std::iter::empty())
|
||||
}
|
||||
|
||||
/// Only accept predecessors from validated, actually applied holding
|
||||
/// conversions. Catalog aliases or requested strategy symbols are not
|
||||
/// evidence that a configured lock covers another security.
|
||||
pub fn evaluate_with_predecessors<'a>(
|
||||
&self,
|
||||
symbol: &str,
|
||||
execution_date: NaiveDate,
|
||||
evidence: &HoldingLifecycleEvidence,
|
||||
calendar: &TradingCalendar,
|
||||
verified_predecessors: impl IntoIterator<Item = &'a str>,
|
||||
) -> Result<AutomaticTradePermission, String> {
|
||||
self.validate()?;
|
||||
let predecessors = verified_predecessors.into_iter().collect::<std::collections::BTreeSet<_>>();
|
||||
if self.locks.iter().any(|lock| {
|
||||
lock.symbol == symbol
|
||||
(lock.symbol == symbol || predecessors.contains(lock.symbol.as_str()))
|
||||
&& lock.start_date <= execution_date
|
||||
&& lock.end_date.is_none_or(|end| execution_date <= end)
|
||||
}) {
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
pub mod broker;
|
||||
pub mod calendar;
|
||||
pub mod cost;
|
||||
mod corporate_book;
|
||||
pub mod data;
|
||||
pub mod decision_projection;
|
||||
mod numeric_factors;
|
||||
pub mod daily_patterns;
|
||||
pub mod pattern_context;
|
||||
pub mod session_events;
|
||||
pub mod factor_events;
|
||||
pub mod execution_capacity;
|
||||
mod etf_execution;
|
||||
mod execution_schedule;
|
||||
mod factor_event_catalog;
|
||||
pub mod factor_cross_section;
|
||||
@@ -15,15 +19,19 @@ pub mod engine;
|
||||
pub mod event_bus;
|
||||
pub mod events;
|
||||
pub mod fixed_point;
|
||||
mod finite_serialization;
|
||||
pub mod futures;
|
||||
pub mod instrument;
|
||||
pub mod metrics;
|
||||
pub mod manual_execution;
|
||||
mod manual_corporate_replay;
|
||||
mod numeric_expr_vm;
|
||||
pub mod platform_expr_strategy;
|
||||
pub mod platform_runtime_schema;
|
||||
pub mod platform_strategy_spec;
|
||||
pub mod portfolio;
|
||||
pub mod portfolio_loss;
|
||||
pub mod position_exposure;
|
||||
pub mod risk_control;
|
||||
pub mod rules;
|
||||
pub mod scheduler;
|
||||
@@ -33,7 +41,9 @@ pub mod stock_pool_candidates;
|
||||
pub mod stock_pool_indicators;
|
||||
pub mod stock_pool_execution;
|
||||
pub mod stock_pool_index_policy;
|
||||
pub mod stock_pool_market_cap;
|
||||
pub mod stock_pool_state;
|
||||
pub mod stock_pool_quote_facts;
|
||||
pub mod signal_contract;
|
||||
pub mod strategy_ai;
|
||||
pub mod universe;
|
||||
@@ -51,16 +61,21 @@ pub use data::{
|
||||
IntradayMarketSnapshotOverlay, IntradayOrderBookDepthLevel, NumericFactorMap, PriceBar,
|
||||
PriceField, SecuritiesMarginRecord, SplitRecord, YieldCurvePoint,
|
||||
};
|
||||
pub use decision_projection::{
|
||||
DecisionProjectionBatch, DecisionProjectionContract, DecisionProjectionExclusion,
|
||||
DecisionProjectionReceipt, DecisionProjectionReference, DecisionProjectionRequest,
|
||||
DecisionProjectionRow,
|
||||
};
|
||||
pub use engine::{
|
||||
AnalyzerMonthlyReturnRow, AnalyzerPositionRow, AnalyzerReport, AnalyzerRiskSummary,
|
||||
AnalyzerTradeRow, BacktestConfig, BacktestDayProgress, BacktestEngine, BacktestError,
|
||||
BacktestResult, BacktestTerminalAssetClass, BacktestTerminalAudit, BacktestTerminalOpenOrder,
|
||||
BacktestTerminalStatus, DailyEquityPoint, ExecutionQuoteRequest, FuturesValidationConfig,
|
||||
ProcessEventRetention, backtest_execution_dates,
|
||||
ProcessEventRetention, backtest_execution_dates, backtest_execution_dates_with_rules,
|
||||
};
|
||||
pub use event_bus::{BacktestProcessMod, BacktestProcessModLoader, ProcessEventBus};
|
||||
pub use events::{
|
||||
AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
|
||||
AccountEvent, FillEvent, FillOrigin, OrderEvent, OrderSide, OrderStatus, PositionEvent, ProcessEvent,
|
||||
ProcessEventKind,
|
||||
};
|
||||
pub use fixed_point::{
|
||||
@@ -74,7 +89,8 @@ pub use futures::{
|
||||
};
|
||||
pub use instrument::Instrument;
|
||||
pub use metrics::{
|
||||
BacktestMetrics, RiskFreeRateContract, RiskFreeRateObservation, compute_backtest_metrics,
|
||||
BacktestMetrics, RiskFreeRateContract, RiskFreeRateObservation, RiskAdjustedStatistics,
|
||||
compute_backtest_metrics, risk_adjusted_statistics,
|
||||
};
|
||||
pub use platform_expr_strategy::{
|
||||
PlatformAccountActionKind, PlatformExplicitActionStage, PlatformExplicitCancelKind,
|
||||
|
||||
@@ -0,0 +1,444 @@
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, Utc};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::manual_execution::{
|
||||
AppliedManualFill, ManualCorporateActionReference, ManualCorporateAdjustment,
|
||||
ManualCorporatePositionChange, ManualExecutionReplay, ManualFillObservation,
|
||||
};
|
||||
use crate::{DataSet, FillEvent, FixedMoney, MatchingType, OrderSide, PortfolioState, PriceField};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CashEffect {
|
||||
at: NaiveDateTime,
|
||||
amount: FixedMoney,
|
||||
}
|
||||
|
||||
/// Replays issued financial facts, never strategy callbacks or hypothetical orders.
|
||||
/// The ordinary book remains observable until an actual receipt is delivered.
|
||||
pub(crate) struct ManualCorporateReplay {
|
||||
first_date: NaiveDate,
|
||||
sessions: RefCell<BTreeSet<NaiveDate>>,
|
||||
closed: RefCell<BTreeSet<NaiveDate>>,
|
||||
cash: RefCell<Vec<CashEffect>>,
|
||||
reconciled_count: Cell<usize>,
|
||||
cash_dividends: bool,
|
||||
adjust_cost: bool,
|
||||
reinvest: bool,
|
||||
matching: MatchingType,
|
||||
daily_price: PriceField,
|
||||
same_day_mark_at_fill: bool,
|
||||
}
|
||||
|
||||
impl ManualCorporateReplay {
|
||||
pub(crate) fn new(
|
||||
first_date: NaiveDate,
|
||||
cash_dividends: bool,
|
||||
adjust_cost: bool,
|
||||
reinvest: bool,
|
||||
matching: MatchingType,
|
||||
daily_price: PriceField,
|
||||
same_day_mark_at_fill: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
first_date,
|
||||
sessions: RefCell::new(BTreeSet::new()),
|
||||
closed: RefCell::new(BTreeSet::new()),
|
||||
cash: RefCell::new(Vec::new()),
|
||||
reconciled_count: Cell::new(0),
|
||||
cash_dividends,
|
||||
adjust_cost,
|
||||
reinvest,
|
||||
matching,
|
||||
daily_price,
|
||||
same_day_mark_at_fill,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn record_session(&self, date: NaiveDate) {
|
||||
self.sessions.borrow_mut().insert(date);
|
||||
}
|
||||
pub(crate) fn record_close(&self, date: NaiveDate) {
|
||||
self.closed.borrow_mut().insert(date);
|
||||
}
|
||||
pub(crate) fn committed(&self, count: usize) {
|
||||
self.reconciled_count.set(count);
|
||||
}
|
||||
|
||||
pub(crate) fn record_cash(
|
||||
&self,
|
||||
at: Option<NaiveDateTime>,
|
||||
before: FixedMoney,
|
||||
after: FixedMoney,
|
||||
) -> Result<(), String> {
|
||||
let amount = after
|
||||
.checked_sub(before)
|
||||
.ok_or("manual corporate cash observation overflow")?;
|
||||
if amount != FixedMoney::ZERO {
|
||||
self.cash.borrow_mut().push(CashEffect {
|
||||
at: at.ok_or("manual corporate cash observation has no execution clock")?,
|
||||
amount,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn action_references(
|
||||
&self,
|
||||
observation: ManualFillObservation<'_>,
|
||||
data: &DataSet,
|
||||
) -> Result<Vec<ManualCorporateActionReference>, String> {
|
||||
let mut symbols = BTreeSet::from([observation.order.symbol.clone()]);
|
||||
let observed_date = local(observation.fill.observed_at).date();
|
||||
let mut actions = Vec::new();
|
||||
for date in self.sessions.borrow().range((
|
||||
std::ops::Bound::Excluded(observation.fill.trade_date),
|
||||
std::ops::Bound::Included(observed_date),
|
||||
)) {
|
||||
for action in data.corporate_actions_on(*date) {
|
||||
if !symbols.contains(&action.symbol) {
|
||||
continue;
|
||||
}
|
||||
let successor_terms = crate::corporate_book::validate_action(action, data)?;
|
||||
let effective = (action.split_ratio() - 1.).abs() > f64::EPSILON
|
||||
|| action.has_successor_conversion()
|
||||
|| (self.cash_dividends && action.share_cash.abs() > f64::EPSILON);
|
||||
if !effective {
|
||||
continue;
|
||||
}
|
||||
if let Some((successor, _, _)) = successor_terms {
|
||||
symbols.insert(successor.to_owned());
|
||||
}
|
||||
actions.push(ManualCorporateActionReference {
|
||||
date: *date,
|
||||
symbol: action.symbol.clone(),
|
||||
successor_symbol: action.successor_symbol.clone(),
|
||||
share_cash: action.share_cash.to_string(),
|
||||
split_ratio: action.split_ratio().to_string(),
|
||||
successor_ratio: action.successor_ratio.map(|value| value.to_string()),
|
||||
successor_cash: action.successor_cash.map(|value| value.to_string()),
|
||||
sha256: digest(
|
||||
&serde_json::to_value(action).map_err(|error| error.to_string())?,
|
||||
)?,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(actions)
|
||||
}
|
||||
|
||||
pub(crate) fn required(
|
||||
&self,
|
||||
observation: ManualFillObservation<'_>,
|
||||
data: &DataSet,
|
||||
) -> Result<bool, String> {
|
||||
Ok(!self.action_references(observation, data)?.is_empty())
|
||||
}
|
||||
|
||||
pub(crate) fn project(
|
||||
&self,
|
||||
source: &ManualExecutionReplay,
|
||||
applied_count: usize,
|
||||
observation: ManualFillObservation<'_>,
|
||||
current: &mut PortfolioState,
|
||||
data: &DataSet,
|
||||
fills: &[FillEvent],
|
||||
has_pending: bool,
|
||||
) -> Result<(AppliedManualFill, ManualCorporateAdjustment), String> {
|
||||
if has_pending {
|
||||
return Err("manual observation conflicts with pending shadow orders".into());
|
||||
}
|
||||
let all = source.observations()?;
|
||||
if all.get(applied_count).is_none_or(|next| {
|
||||
next.fill.observation_event_id != observation.fill.observation_event_id
|
||||
}) {
|
||||
return Err(
|
||||
"manual corporate observation prefix differs from the immutable trace".into(),
|
||||
);
|
||||
}
|
||||
let actions = self.action_references(observation, data)?;
|
||||
if actions.is_empty() {
|
||||
return Err("manual corporate projection has no processed corporate action".into());
|
||||
}
|
||||
let at = local(observation.fill.observed_at);
|
||||
let reference = self.replay(
|
||||
source,
|
||||
current.initial_cash_fixed(),
|
||||
&all[..applied_count],
|
||||
self.reconciled_count.get(),
|
||||
data,
|
||||
fills,
|
||||
at,
|
||||
)?;
|
||||
let expected = current.financial_replay_identity();
|
||||
let reference_identity = reference.financial_replay_identity();
|
||||
if reference_identity != expected {
|
||||
let differing = expected.as_object().into_iter().flat_map(|value| value.keys())
|
||||
.chain(reference_identity.as_object().into_iter().flat_map(|value| value.keys()))
|
||||
.collect::<BTreeSet<_>>().into_iter().filter(|key| expected.get(*key)!=reference_identity.get(*key))
|
||||
.cloned().collect::<Vec<_>>().join(",");
|
||||
return Err(format!(
|
||||
"manual corporate ledger coverage mismatch: symbol={} observed_at={} expected={} replayed={} differing_fields={}",
|
||||
observation.order.symbol,
|
||||
observation.fill.observed_at,
|
||||
digest(&expected)?,
|
||||
digest(&reference_identity)?, differing
|
||||
));
|
||||
}
|
||||
let replayed = self.replay(
|
||||
source,
|
||||
current.initial_cash_fixed(),
|
||||
&all[..=applied_count],
|
||||
applied_count + 1,
|
||||
data,
|
||||
fills,
|
||||
at,
|
||||
)?;
|
||||
let replayed_identity = replayed.financial_replay_identity();
|
||||
let gross = FixedMoney::from_decimal_str(&observation.fill.gross_amount()?.to_string())?;
|
||||
let fees = FixedMoney::from_decimal_str(&observation.fill.total_fees()?.to_string())?;
|
||||
let cash_delta = match observation.order.side {
|
||||
OrderSide::Buy => gross.checked_add(fees).and_then(FixedMoney::checked_neg),
|
||||
OrderSide::Sell => gross.checked_sub(fees),
|
||||
}
|
||||
.ok_or("manual corporate trade cash overflow")?;
|
||||
let before = current.cash_fixed();
|
||||
let after = replayed.cash_fixed();
|
||||
let corporate_cash = after
|
||||
.checked_sub(before)
|
||||
.and_then(|delta| delta.checked_sub(cash_delta))
|
||||
.ok_or("manual corporate adjustment overflow")?;
|
||||
let symbols = current
|
||||
.positions()
|
||||
.keys()
|
||||
.chain(replayed.positions().keys())
|
||||
.cloned()
|
||||
.chain(std::iter::once(observation.order.symbol.clone()))
|
||||
.collect::<BTreeSet<_>>();
|
||||
let mut positions = BTreeMap::new();
|
||||
for symbol in symbols {
|
||||
let change = ManualCorporatePositionChange {
|
||||
quantity_before: current
|
||||
.position(&symbol)
|
||||
.map_or(0, |position| position.quantity),
|
||||
quantity_after: replayed
|
||||
.position(&symbol)
|
||||
.map_or(0, |position| position.quantity),
|
||||
cost_basis_before: current
|
||||
.financial_position_basis(&symbol)
|
||||
.to_decimal_string(),
|
||||
cost_basis_after: replayed
|
||||
.financial_position_basis(&symbol)
|
||||
.to_decimal_string(),
|
||||
};
|
||||
if change.quantity_before != change.quantity_after
|
||||
|| change.cost_basis_before != change.cost_basis_after
|
||||
{
|
||||
positions.insert(symbol, change);
|
||||
}
|
||||
}
|
||||
let quantity_after = replayed
|
||||
.position(&observation.order.symbol)
|
||||
.map_or(0, |position| position.quantity);
|
||||
let adjustment = ManualCorporateAdjustment {
|
||||
schema: "fidc.manual-corporate-adjustment/v1".into(),
|
||||
observed_at: observation.fill.observed_at,
|
||||
cash_dividends_enabled: self.cash_dividends,
|
||||
dividend_cost_basis_adjustment: self.adjust_cost,
|
||||
dividend_reinvestment: self.reinvest,
|
||||
actions,
|
||||
cash_before: before.to_decimal_string(),
|
||||
cash_after: after.to_decimal_string(),
|
||||
corporate_cash_delta: corporate_cash.to_decimal_string(),
|
||||
positions,
|
||||
reference_sha256: digest(&reference_identity)?,
|
||||
replayed_sha256: digest(&replayed_identity)?,
|
||||
};
|
||||
current.replace_replayed_financial_book(replayed)?;
|
||||
Ok((
|
||||
AppliedManualFill {
|
||||
gross,
|
||||
fees,
|
||||
cash_delta,
|
||||
quantity_after,
|
||||
},
|
||||
adjustment,
|
||||
))
|
||||
}
|
||||
|
||||
fn replay(
|
||||
&self,
|
||||
runtime_input: &ManualExecutionReplay,
|
||||
initial_cash: FixedMoney,
|
||||
manual: &[ManualFillObservation<'_>],
|
||||
economic_count: usize,
|
||||
data: &DataSet,
|
||||
fills: &[FillEvent],
|
||||
at: NaiveDateTime,
|
||||
) -> Result<PortfolioState, String> {
|
||||
enum Event<'a> {
|
||||
Session,
|
||||
Cash(&'a CashEffect),
|
||||
Corporate(NaiveDate),
|
||||
Settle(NaiveDate),
|
||||
Manual(ManualFillObservation<'a>),
|
||||
Simulated(&'a FillEvent),
|
||||
Close(NaiveDate),
|
||||
}
|
||||
let mut events = Vec::new();
|
||||
let sessions = self.sessions.borrow();
|
||||
let closed = self.closed.borrow();
|
||||
let cash = self.cash.borrow();
|
||||
for date in &*sessions {
|
||||
let clock = date.and_hms_opt(0, 0, 0).unwrap();
|
||||
events.push((clock, 0, 0, Event::Session));
|
||||
events.push((clock, 2, 0, Event::Corporate(*date)));
|
||||
events.push((clock, 3, 0, Event::Settle(*date)));
|
||||
}
|
||||
for (index, effect) in cash.iter().enumerate() {
|
||||
events.push((effect.at, 1, index, Event::Cash(effect)));
|
||||
}
|
||||
for (index, observation) in manual.iter().enumerate() {
|
||||
let clock = if index < economic_count {
|
||||
local(observation.fill.executed_at)
|
||||
} else {
|
||||
local(observation.fill.observed_at)
|
||||
};
|
||||
if clock.date() < self.first_date {
|
||||
return Err("manual corporate execution precedes the represented initial ledger; opening facts are required".into());
|
||||
}
|
||||
events.push((clock, 4, fills.len() + index, Event::Manual(*observation)));
|
||||
}
|
||||
for (index, fill) in fills.iter().enumerate() {
|
||||
fill.validate()?;
|
||||
if fill.origin == crate::events::FillOrigin::DividendReinvestment {
|
||||
// The declared accounting model is recalculated from the
|
||||
// corrected entitlements; this was never a submitted order.
|
||||
continue;
|
||||
}
|
||||
let date = fill.execution_date.unwrap_or(fill.date);
|
||||
// This is the frozen daily matching model, not a broker timestamp.
|
||||
// Intraday contracts must supply their actual execution clock.
|
||||
let clock = match fill.execution_timestamp {
|
||||
Some(clock) => clock,
|
||||
None if matches!(
|
||||
self.matching,
|
||||
MatchingType::OpenAuction | MatchingType::NextBarOpen
|
||||
) =>
|
||||
{
|
||||
date.and_hms_opt(9, 30, 0).unwrap()
|
||||
}
|
||||
None if self.matching == MatchingType::CurrentBarClose
|
||||
&& self.daily_price == PriceField::Close =>
|
||||
{
|
||||
date.and_hms_opt(15, 0, 0).unwrap()
|
||||
}
|
||||
None => {
|
||||
return Err(
|
||||
"manual corporate replay lacks an intraday fill execution clock".into(),
|
||||
);
|
||||
}
|
||||
};
|
||||
events.push((clock, 4, index, Event::Simulated(fill)));
|
||||
}
|
||||
for date in &*closed {
|
||||
events.push((
|
||||
date.and_hms_nano_opt(23, 59, 59, 999_999_999)
|
||||
.unwrap()
|
||||
.min(at),
|
||||
5,
|
||||
0,
|
||||
Event::Close(*date),
|
||||
));
|
||||
}
|
||||
events.sort_by_key(|(clock, priority, sequence, _)| (*clock, *priority, *sequence));
|
||||
let mut book = PortfolioState::from_fixed_initial_cash(initial_cash);
|
||||
book.record_empty_simulation_origin(self.first_date)?;
|
||||
for (clock, _, _, event) in events {
|
||||
if clock > at {
|
||||
return Err("manual corporate replay contains a future financial fact".into());
|
||||
}
|
||||
match event {
|
||||
Event::Session => book.begin_trading_day_at(clock.date()),
|
||||
Event::Cash(effect) => {
|
||||
book.apply_cash_delta_fixed(effect.amount)?;
|
||||
if book.cash_fixed() < FixedMoney::ZERO {
|
||||
return Err(
|
||||
"manual corporate replay conflicts with prior cash facts".into()
|
||||
);
|
||||
}
|
||||
}
|
||||
Event::Corporate(date) => {
|
||||
crate::corporate_book::apply(
|
||||
date,
|
||||
data,
|
||||
&mut book,
|
||||
&mut Vec::new(),
|
||||
self.cash_dividends,
|
||||
self.adjust_cost,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
Event::Settle(date) => {
|
||||
crate::corporate_book::settle_shares(date, &mut book, &mut Vec::new())
|
||||
.map_err(|error| error.to_string())?;
|
||||
crate::corporate_book::settle_receivables(date, data, &mut book, &mut Vec::new(), self.reinvest, Some(runtime_input))
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
Event::Manual(observation) => {
|
||||
observation.apply(&mut book, data, false)?;
|
||||
}
|
||||
Event::Simulated(fill) => {
|
||||
let gross = FixedMoney::from_f64(fill.gross_amount)
|
||||
.ok_or("invalid simulated gross amount")?;
|
||||
let fees = FixedMoney::checked_sum_f64([
|
||||
fill.commission,
|
||||
fill.stamp_tax,
|
||||
fill.transfer_fee,
|
||||
])
|
||||
.ok_or("invalid simulated fee amount")?;
|
||||
book.apply_observed_manual_fill(
|
||||
fill.execution_date.unwrap_or(fill.date),
|
||||
&fill.symbol,
|
||||
fill.side,
|
||||
fill.quantity,
|
||||
fill.price,
|
||||
fill.price,
|
||||
gross,
|
||||
fees,
|
||||
)?;
|
||||
book.prune_flat_positions();
|
||||
}
|
||||
Event::Close(date) => {
|
||||
book.update_prices_with_options(
|
||||
date,
|
||||
data,
|
||||
PriceField::Close,
|
||||
self.same_day_mark_at_fill,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
if date < at.date() || at.time() == chrono::NaiveTime::from_hms_nano_opt(23,59,59,999_999_999).unwrap() {
|
||||
book.record_closing_holdings(date)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
if book.cash_fixed() < FixedMoney::ZERO {
|
||||
return Err("manual corporate replay would borrow unobserved cash".into());
|
||||
}
|
||||
}
|
||||
Ok(book)
|
||||
}
|
||||
}
|
||||
|
||||
fn local(value: DateTime<Utc>) -> NaiveDateTime {
|
||||
value
|
||||
.with_timezone(&FixedOffset::east_opt(8 * 3600).unwrap())
|
||||
.naive_local()
|
||||
}
|
||||
fn digest(value: &serde_json::Value) -> Result<String, String> {
|
||||
Ok(format!(
|
||||
"{:x}",
|
||||
Sha256::digest(serde_json::to_vec(value).map_err(|error| error.to_string())?)
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,748 @@
|
||||
//! Confirmed manual fills are external observations, not simulated broker fills.
|
||||
//! The producer must bind these records to the runtime's durable order/audit facts.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use chrono::{DateTime, FixedOffset, NaiveDate, Timelike, Utc};
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::events::OrderSide;
|
||||
use crate::{DataSet, FixedMoney, PortfolioState};
|
||||
use rust_decimal::prelude::ToPrimitive;
|
||||
|
||||
pub const MANUAL_REPLAY_SCHEMA: &str = "fidc.observed-manual-executions/v3";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub struct ManualExecutionReplay {
|
||||
pub schema: String,
|
||||
pub runtime_id: String,
|
||||
pub account_id: String,
|
||||
pub source_contract_sha256: String,
|
||||
pub content_sha256: String,
|
||||
pub observation_cutoff: DateTime<Utc>,
|
||||
pub actions: Vec<ManualExecutionAction>,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub position_exposure_events: Vec<crate::position_exposure::PositionExposureEvent>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub legacy_position_exposure_bps: BTreeMap<NaiveDate, i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub struct ManualExecutionAction {
|
||||
pub action_id: String,
|
||||
pub source: ManualExecutionSource,
|
||||
pub audit_event_ids: Vec<String>,
|
||||
pub confirmed_at: DateTime<Utc>,
|
||||
pub confirmation_observed_at: DateTime<Utc>,
|
||||
pub outcome: ManualActionOutcome,
|
||||
pub orders: Vec<ManualExecutionOrder>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ManualActionOutcome {
|
||||
NoOrdersNeeded,
|
||||
NotExecuted,
|
||||
OrdersTerminal,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ManualExecutionSource {
|
||||
ManualSecurityTrade,
|
||||
ManualPositionAction,
|
||||
ManualRebalance,
|
||||
StockPoolAllocation,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub struct ManualExecutionOrder {
|
||||
pub order_id: String,
|
||||
pub broker_order_id: Option<String>,
|
||||
pub source_adapter: Option<String>,
|
||||
pub symbol: String,
|
||||
pub side: OrderSide,
|
||||
pub quantity: u32,
|
||||
pub order_created_at: DateTime<Utc>,
|
||||
pub terminal_observed_at: DateTime<Utc>,
|
||||
pub terminal_status: ManualOrderTerminalStatus,
|
||||
pub fills: Vec<ManualExecutionFill>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ManualOrderTerminalStatus {
|
||||
Filled,
|
||||
Cancelled,
|
||||
Rejected,
|
||||
Expired,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub struct ManualExecutionFill {
|
||||
pub trade_id: String,
|
||||
pub observation_event_id: String,
|
||||
pub observation_sequence: u64,
|
||||
pub fee_observation_event_id: String,
|
||||
pub fee_observation_sequence: u64,
|
||||
pub fee_observed_at: DateTime<Utc>,
|
||||
pub trade_date: NaiveDate,
|
||||
pub executed_at: DateTime<Utc>,
|
||||
pub observed_at: DateTime<Utc>,
|
||||
pub timestamp_precision: ManualTimestampPrecision,
|
||||
pub quantity: u32,
|
||||
#[serde(with = "rust_decimal::serde::str")]
|
||||
pub price: Decimal,
|
||||
#[serde(default, with = "rust_decimal::serde::str_option")]
|
||||
pub commission: Option<Decimal>,
|
||||
#[serde(default, with = "rust_decimal::serde::str_option")]
|
||||
pub stamp_tax: Option<Decimal>,
|
||||
#[serde(default, with = "rust_decimal::serde::str_option")]
|
||||
pub transfer_fee: Option<Decimal>,
|
||||
/// Full observed charge, including any venue fees not itemized above.
|
||||
#[serde(with = "rust_decimal::serde::str")]
|
||||
pub total_fee: Decimal,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ManualTimestampPrecision {
|
||||
Second,
|
||||
Millisecond,
|
||||
Microsecond,
|
||||
Nanosecond,
|
||||
}
|
||||
|
||||
impl ManualTimestampPrecision {
|
||||
fn nanoseconds(self) -> i64 {
|
||||
match self {
|
||||
Self::Second => 1_000_000_000,
|
||||
Self::Millisecond => 1_000_000,
|
||||
Self::Microsecond => 1_000,
|
||||
Self::Nanosecond => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ManualExecutionFill {
|
||||
pub fn gross_amount(&self) -> Result<Decimal, String> {
|
||||
self.price
|
||||
.checked_mul(Decimal::from(self.quantity))
|
||||
.ok_or_else(|| "manual fill gross amount overflow".into())
|
||||
}
|
||||
|
||||
pub fn total_fees(&self) -> Result<Decimal, String> {
|
||||
let known = [self.commission, self.stamp_tax, self.transfer_fee]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.try_fold(Decimal::ZERO, |sum, fee| {
|
||||
if fee < Decimal::ZERO {
|
||||
return Err("manual fill fee component is negative");
|
||||
}
|
||||
sum.checked_add(fee).ok_or("manual fill fees overflow")
|
||||
})?;
|
||||
if self.total_fee < known {
|
||||
return Err("manual total fee is below its known components".into());
|
||||
}
|
||||
Ok(self.total_fee)
|
||||
}
|
||||
}
|
||||
|
||||
fn identifier(value: &str) -> Result<(), String> {
|
||||
if value.is_empty()
|
||||
|| value.trim() != value
|
||||
|| value.len() > 256
|
||||
|| value.chars().any(char::is_control)
|
||||
{
|
||||
return Err("manual execution identity is empty, untrimmed or invalid".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl ManualExecutionReplay {
|
||||
/// Market/indicator data is needed for securities whose observed fills
|
||||
/// change the portfolio. A rejected, never-filled order is not data demand.
|
||||
pub fn required_data_symbols(&self) -> Result<BTreeSet<String>, String> {
|
||||
self.validate()?;
|
||||
Ok(self
|
||||
.actions
|
||||
.iter()
|
||||
.flat_map(|action| &action.orders)
|
||||
.filter(|order| !order.fills.is_empty())
|
||||
.map(|order| order.symbol.clone())
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn observations(&self) -> Result<Vec<ManualFillObservation<'_>>, String> {
|
||||
self.validate()?;
|
||||
let mut observations = Vec::new();
|
||||
for action in &self.actions {
|
||||
for order in &action.orders {
|
||||
for fill in &order.fills {
|
||||
observations.push(ManualFillObservation {
|
||||
action,
|
||||
order,
|
||||
fill,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
observations.sort_by_key(|entry| (entry.fill.observed_at, entry.fill.observation_sequence));
|
||||
Ok(observations)
|
||||
}
|
||||
pub fn content_digest(&self) -> Result<String, String> {
|
||||
let mut value = serde_json::to_value(self).map_err(|error| error.to_string())?;
|
||||
value
|
||||
.as_object_mut()
|
||||
.ok_or("manual replay is not an object")?
|
||||
.remove("contentSha256");
|
||||
let bytes = serde_json::to_vec(&value).map_err(|error| error.to_string())?;
|
||||
Ok(format!("{:x}", Sha256::digest(bytes)))
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.schema != MANUAL_REPLAY_SCHEMA
|
||||
&& self.schema != "fidc.observed-manual-executions/v2"
|
||||
{
|
||||
return Err("unsupported manual replay schema".into());
|
||||
}
|
||||
if self.schema == "fidc.observed-manual-executions/v2"
|
||||
&& (!self.position_exposure_events.is_empty()
|
||||
|| !self.legacy_position_exposure_bps.is_empty())
|
||||
{
|
||||
return Err("runtime configuration requires manual replay v3".into());
|
||||
}
|
||||
crate::position_exposure::PositionExposureTimeline::from_events(
|
||||
&self.position_exposure_events,
|
||||
)?;
|
||||
if self.position_exposure_events.iter().any(|event| event.effective_at > self.observation_cutoff) {
|
||||
return Err("observed runtime position event is after the evidence cutoff".into());
|
||||
}
|
||||
if self
|
||||
.legacy_position_exposure_bps
|
||||
.values()
|
||||
.any(|value| !(0..=10000).contains(value))
|
||||
{
|
||||
return Err("legacy manual exposure is outside 0..10000 bps".into());
|
||||
}
|
||||
identifier(&self.runtime_id)?;
|
||||
identifier(&self.account_id)?;
|
||||
if self.source_contract_sha256.len() != 64
|
||||
|| !self
|
||||
.source_contract_sha256
|
||||
.bytes()
|
||||
.all(|v| v.is_ascii_hexdigit())
|
||||
{
|
||||
return Err("manual replay source contract hash is invalid".into());
|
||||
}
|
||||
if self.content_digest()? != self.content_sha256 {
|
||||
return Err("manual replay content digest mismatch".into());
|
||||
}
|
||||
if self.actions.len() > 100_000 {
|
||||
return Err("manual replay action limit exceeded; trace was not truncated".into());
|
||||
}
|
||||
let shanghai = FixedOffset::east_opt(8 * 3600).unwrap();
|
||||
let mut actions = BTreeSet::new();
|
||||
let mut audits = BTreeSet::new();
|
||||
let mut orders = BTreeSet::new();
|
||||
let mut broker_orders = BTreeSet::new();
|
||||
let mut trades = BTreeSet::new();
|
||||
let mut observation_events = BTreeSet::new();
|
||||
let mut observation_sequences = BTreeSet::new();
|
||||
let mut fee_observations = BTreeSet::new();
|
||||
let mut receipt_ids = BTreeMap::new();
|
||||
let mut receipt_sequences = BTreeMap::new();
|
||||
for action in &self.actions {
|
||||
identifier(&action.action_id)?;
|
||||
if !actions.insert(action.action_id.as_str())
|
||||
|| action.confirmed_at > self.observation_cutoff
|
||||
|| action.confirmation_observed_at < action.confirmed_at
|
||||
|| action.confirmation_observed_at > self.observation_cutoff
|
||||
{
|
||||
return Err("duplicate manual action or confirmation after cutoff".into());
|
||||
}
|
||||
if action.audit_event_ids.is_empty() {
|
||||
return Err("manual action has no immutable audit binding".into());
|
||||
}
|
||||
if (action.outcome != ManualActionOutcome::OrdersTerminal) != action.orders.is_empty() {
|
||||
return Err("manual action outcome does not prove its order coverage".into());
|
||||
}
|
||||
for id in &action.audit_event_ids {
|
||||
identifier(id)?;
|
||||
if !audits.insert(id.as_str()) {
|
||||
return Err("manual audit event is bound more than once".into());
|
||||
}
|
||||
}
|
||||
for order in &action.orders {
|
||||
identifier(&order.order_id)?;
|
||||
if let Some(adapter) = &order.source_adapter {
|
||||
identifier(adapter)?;
|
||||
}
|
||||
identifier(&order.symbol)?;
|
||||
if let Some(id) = &order.broker_order_id {
|
||||
identifier(id)?;
|
||||
if !broker_orders.insert((
|
||||
order
|
||||
.source_adapter
|
||||
.as_deref()
|
||||
.ok_or("broker identity requires its source adapter")?,
|
||||
order.order_created_at.with_timezone(&shanghai).date_naive(),
|
||||
id.as_str(),
|
||||
)) {
|
||||
return Err("manual local orders share one broker order identity".into());
|
||||
}
|
||||
}
|
||||
if !order.fills.is_empty() && order.source_adapter.is_none() {
|
||||
return Err("manual fills require a known source adapter".into());
|
||||
}
|
||||
if !order.fills.is_empty()
|
||||
&& order.source_adapter.as_deref() != Some("paper")
|
||||
&& order.broker_order_id.is_none()
|
||||
{
|
||||
return Err(
|
||||
"manual broker fills require their original broker order identity".into(),
|
||||
);
|
||||
}
|
||||
if !orders.insert(order.order_id.as_str())
|
||||
|| order.quantity == 0
|
||||
|| order.quantity > i32::MAX as u32
|
||||
{
|
||||
return Err("duplicate manual order or invalid quantity".into());
|
||||
}
|
||||
if order.order_created_at < action.confirmed_at
|
||||
|| order.terminal_observed_at < order.order_created_at
|
||||
|| order.terminal_observed_at > self.observation_cutoff
|
||||
{
|
||||
return Err(
|
||||
"manual order confirmation/submission/terminal time is inconsistent".into(),
|
||||
);
|
||||
}
|
||||
let mut filled = 0_u32;
|
||||
for fill in &order.fills {
|
||||
identifier(&fill.trade_id)?;
|
||||
identifier(&fill.observation_event_id)?;
|
||||
identifier(&fill.fee_observation_event_id)?;
|
||||
if fill.observation_sequence == 0
|
||||
|| fill.observation_sequence > i64::MAX as u64
|
||||
|| !observation_events.insert(fill.observation_event_id.as_str())
|
||||
|| !observation_sequences.insert(fill.observation_sequence)
|
||||
{
|
||||
return Err(
|
||||
"manual fill requires a unique durable observation event and sequence"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
if fill.fee_observation_sequence == 0
|
||||
|| fill.fee_observation_sequence > i64::MAX as u64
|
||||
|| fill.fee_observed_at < fill.observed_at
|
||||
|| fill.fee_observed_at > self.observation_cutoff
|
||||
|| !fee_observations.insert((
|
||||
fill.fee_observation_event_id.as_str(),
|
||||
fill.fee_observation_sequence,
|
||||
))
|
||||
{
|
||||
return Err("manual finalized fees require their own unique observation within the cutoff".into());
|
||||
}
|
||||
if (fill.fee_observation_event_id == fill.observation_event_id)
|
||||
!= (fill.fee_observation_sequence == fill.observation_sequence)
|
||||
|| (fill.fee_observation_event_id == fill.observation_event_id
|
||||
&& fill.fee_observed_at != fill.observed_at)
|
||||
{
|
||||
return Err("manual fill and fee observation identities disagree".into());
|
||||
}
|
||||
if !trades.insert((fill.trade_date, fill.trade_id.as_str()))
|
||||
|| fill.quantity == 0
|
||||
{
|
||||
return Err("duplicate manual trade or zero fill quantity".into());
|
||||
}
|
||||
for (id, sequence) in [
|
||||
(&fill.observation_event_id, fill.observation_sequence),
|
||||
(
|
||||
&fill.fee_observation_event_id,
|
||||
fill.fee_observation_sequence,
|
||||
),
|
||||
] {
|
||||
if receipt_ids
|
||||
.insert(id, (&fill.trade_id, sequence))
|
||||
.is_some_and(|owner| owner != (&fill.trade_id, sequence))
|
||||
|| receipt_sequences
|
||||
.insert(sequence, (&fill.trade_id, id))
|
||||
.is_some_and(|owner| owner != (&fill.trade_id, id))
|
||||
{
|
||||
return Err("manual observation identity is reused by a different trade or sequence".into());
|
||||
}
|
||||
}
|
||||
if fill.executed_at.with_timezone(&shanghai).date_naive() != fill.trade_date
|
||||
|| fill.observed_at > self.observation_cutoff
|
||||
|| fill.observed_at < order.order_created_at
|
||||
|| fill.observed_at < action.confirmation_observed_at
|
||||
|| fill.observed_at < fill.executed_at
|
||||
|| fill.executed_at > order.terminal_observed_at
|
||||
{
|
||||
return Err("manual fill execution/observation time is inconsistent".into());
|
||||
}
|
||||
if i64::from(fill.executed_at.nanosecond())
|
||||
% fill.timestamp_precision.nanoseconds()
|
||||
!= 0
|
||||
{
|
||||
return Err(
|
||||
"broker timestamp contains digits finer than its declared precision"
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
let upper = fill
|
||||
.executed_at
|
||||
.checked_add_signed(chrono::Duration::nanoseconds(
|
||||
fill.timestamp_precision.nanoseconds(),
|
||||
))
|
||||
.ok_or("manual execution timestamp overflow")?;
|
||||
let earliest = order.order_created_at.max(action.confirmation_observed_at);
|
||||
if fill.executed_at < earliest && earliest >= upper {
|
||||
return Err("manual fill predates its order or durable confirmation".into());
|
||||
}
|
||||
if fill.price <= Decimal::ZERO {
|
||||
return Err("manual fill requires a positive price".into());
|
||||
}
|
||||
fill.gross_amount()?
|
||||
.checked_add(fill.total_fees()?)
|
||||
.ok_or("manual fill cash amount overflow")?;
|
||||
filled = filled
|
||||
.checked_add(fill.quantity)
|
||||
.ok_or("manual cumulative fill quantity overflow")?;
|
||||
}
|
||||
if filled > order.quantity
|
||||
|| (order.terminal_status == ManualOrderTerminalStatus::Filled
|
||||
&& filled != order.quantity)
|
||||
|| (order.terminal_status == ManualOrderTerminalStatus::Rejected && filled != 0)
|
||||
|| (matches!(
|
||||
order.terminal_status,
|
||||
ManualOrderTerminalStatus::Cancelled | ManualOrderTerminalStatus::Expired
|
||||
) && filled == order.quantity)
|
||||
{
|
||||
return Err("manual terminal status disagrees with cumulative fills".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ManualFillObservation<'a> {
|
||||
pub action: &'a ManualExecutionAction,
|
||||
pub order: &'a ManualExecutionOrder,
|
||||
pub fill: &'a ManualExecutionFill,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct AppliedManualFill {
|
||||
pub gross: FixedMoney,
|
||||
pub fees: FixedMoney,
|
||||
pub cash_delta: FixedMoney,
|
||||
pub quantity_after: u32,
|
||||
}
|
||||
|
||||
/// One replay owns its immutable trace and progress. Advancing is atomic even
|
||||
/// if a later receipt in the same step disagrees with the shadow account.
|
||||
pub struct ManualReplayCursor {
|
||||
replay: std::sync::Arc<ManualExecutionReplay>,
|
||||
indices: Vec<(usize, usize, usize)>,
|
||||
cursor: usize,
|
||||
clock: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ManualReplayApplication {
|
||||
pub action_id: String,
|
||||
pub order_id: String,
|
||||
pub trade_id: String,
|
||||
pub observation_event_id: String,
|
||||
pub observation_sequence: u64,
|
||||
pub observed_at: DateTime<Utc>,
|
||||
pub fee_observation_event_id: String,
|
||||
pub fee_observed_at: DateTime<Utc>,
|
||||
pub executed_at: DateTime<Utc>,
|
||||
pub symbol: String,
|
||||
pub side: OrderSide,
|
||||
pub quantity: u32,
|
||||
pub quantity_after: u32,
|
||||
pub price: String,
|
||||
pub commission: Option<String>,
|
||||
pub stamp_tax: Option<String>,
|
||||
pub transfer_fee: Option<String>,
|
||||
pub source_total_fee: String,
|
||||
pub source_gross_amount: String,
|
||||
pub ledger_gross_amount: String,
|
||||
pub ledger_fees: String,
|
||||
pub cash_delta: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub corporate_adjustment: Option<ManualCorporateAdjustment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub struct ManualCorporateAdjustment {
|
||||
pub schema: String,
|
||||
pub observed_at: DateTime<Utc>,
|
||||
pub cash_dividends_enabled: bool,
|
||||
pub dividend_cost_basis_adjustment: bool,
|
||||
#[serde(default, skip_serializing_if = "disabled_flag")]
|
||||
pub dividend_reinvestment: bool,
|
||||
pub actions: Vec<ManualCorporateActionReference>,
|
||||
pub cash_before: String,
|
||||
pub cash_after: String,
|
||||
pub corporate_cash_delta: String,
|
||||
pub positions: BTreeMap<String, ManualCorporatePositionChange>,
|
||||
pub reference_sha256: String,
|
||||
pub replayed_sha256: String,
|
||||
}
|
||||
|
||||
fn disabled_flag(value: &bool) -> bool { !value }
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub struct ManualCorporateActionReference {
|
||||
pub date: NaiveDate,
|
||||
pub symbol: String,
|
||||
pub successor_symbol: Option<String>,
|
||||
pub share_cash: String,
|
||||
pub split_ratio: String,
|
||||
pub successor_ratio: Option<String>,
|
||||
pub successor_cash: Option<String>,
|
||||
pub sha256: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub struct ManualCorporatePositionChange {
|
||||
pub quantity_before: u32,
|
||||
pub quantity_after: u32,
|
||||
pub cost_basis_before: String,
|
||||
pub cost_basis_after: String,
|
||||
}
|
||||
|
||||
impl ManualReplayCursor {
|
||||
pub(crate) fn frozen_source(&self) -> std::sync::Arc<ManualExecutionReplay> {
|
||||
self.replay.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn next_observation(&self) -> Option<ManualFillObservation<'_>> {
|
||||
self.indices.get(self.cursor).map(|&(a, o, f)| ManualFillObservation {
|
||||
action: &self.replay.actions[a], order: &self.replay.actions[a].orders[o],
|
||||
fill: &self.replay.actions[a].orders[o].fills[f],
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn advance_next_projected<F>(
|
||||
&mut self, portfolio: &mut PortfolioState, project: F,
|
||||
) -> Result<Option<ManualReplayApplication>, String>
|
||||
where F: FnOnce(ManualFillObservation<'_>, &mut PortfolioState) -> Result<(AppliedManualFill, ManualCorporateAdjustment), String> {
|
||||
let Some(observation) = self.next_observation() else { return Ok(None); };
|
||||
let at = observation.fill.observed_at;
|
||||
if at > self.replay.observation_cutoff || self.clock.is_some_and(|clock| at < clock) {
|
||||
return Err("manual projected observation clock violates the frozen trace".into());
|
||||
}
|
||||
let mut next = portfolio.clone();
|
||||
let (applied, adjustment) = project(observation, &mut next)?;
|
||||
let mut application = observation.application(applied)?;
|
||||
application.corporate_adjustment = Some(adjustment);
|
||||
crate::finite_serialization::validate(&application).map_err(|error| error.to_string())?;
|
||||
*portfolio = next;
|
||||
self.cursor += 1;
|
||||
self.clock = Some(at);
|
||||
Ok(Some(application))
|
||||
}
|
||||
|
||||
pub fn new(replay: ManualExecutionReplay) -> Result<Self, String> {
|
||||
Self::from_shared(std::sync::Arc::new(replay))
|
||||
}
|
||||
|
||||
pub fn from_shared(replay: std::sync::Arc<ManualExecutionReplay>) -> Result<Self, String> {
|
||||
replay.validate()?;
|
||||
let mut indices = Vec::new();
|
||||
for (a, action) in replay.actions.iter().enumerate() {
|
||||
for (o, order) in action.orders.iter().enumerate() {
|
||||
for f in 0..order.fills.len() {
|
||||
indices.push((a, o, f));
|
||||
}
|
||||
}
|
||||
}
|
||||
indices.sort_by_key(|&(a, o, f)| {
|
||||
let fill = &replay.actions[a].orders[o].fills[f];
|
||||
(fill.observed_at, fill.observation_sequence)
|
||||
});
|
||||
Ok(Self {
|
||||
replay,
|
||||
indices,
|
||||
cursor: 0,
|
||||
clock: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn next_observation_at(&self) -> Option<DateTime<Utc>> {
|
||||
self.indices
|
||||
.get(self.cursor)
|
||||
.map(|&(a, o, f)| self.replay.actions[a].orders[o].fills[f].observed_at)
|
||||
}
|
||||
|
||||
pub fn applied_count(&self) -> usize {
|
||||
self.cursor
|
||||
}
|
||||
|
||||
pub fn advance(
|
||||
&mut self,
|
||||
at: DateTime<Utc>,
|
||||
portfolio: &mut PortfolioState,
|
||||
data: &DataSet,
|
||||
has_pending_orders: bool,
|
||||
) -> Result<Vec<ManualReplayApplication>, String> {
|
||||
let end = self.cursor
|
||||
+ self.indices[self.cursor..].iter().take_while(|&&(a, o, f)| {
|
||||
self.replay.actions[a].orders[o].fills[f].observed_at <= at
|
||||
}).count();
|
||||
self.advance_through(at, end, portfolio, data, has_pending_orders)
|
||||
}
|
||||
|
||||
/// One receipt at a time lets callbacks observe the intermediate state
|
||||
/// when multiple fills share a timestamp but have distinct durable sequences.
|
||||
pub fn advance_next(
|
||||
&mut self, portfolio: &mut PortfolioState, data: &DataSet, has_pending_orders: bool,
|
||||
) -> Result<Option<ManualReplayApplication>, String> {
|
||||
let Some(at) = self.next_observation_at() else { return Ok(None); };
|
||||
let mut applications = self.advance_through(at, self.cursor + 1, portfolio, data, has_pending_orders)?;
|
||||
Ok(applications.pop())
|
||||
}
|
||||
|
||||
fn advance_through(
|
||||
&mut self, at: DateTime<Utc>, end: usize, portfolio: &mut PortfolioState,
|
||||
data: &DataSet, has_pending_orders: bool,
|
||||
) -> Result<Vec<ManualReplayApplication>, String> {
|
||||
if at > self.replay.observation_cutoff {
|
||||
return Err("manual observation clock exceeds the frozen evidence cutoff".into());
|
||||
}
|
||||
if self.clock.is_some_and(|clock| at < clock) {
|
||||
return Err("manual observation clock moved backwards".into());
|
||||
}
|
||||
if end == self.cursor {
|
||||
self.clock = Some(at);
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let mut next = portfolio.clone();
|
||||
let mut applications = Vec::with_capacity(end - self.cursor);
|
||||
for &(a, o, f) in &self.indices[self.cursor..end] {
|
||||
let action = &self.replay.actions[a];
|
||||
let order = &action.orders[o];
|
||||
let fill = &order.fills[f];
|
||||
let applied = ManualFillObservation {
|
||||
action,
|
||||
order,
|
||||
fill,
|
||||
}
|
||||
.apply(&mut next, data, has_pending_orders)?;
|
||||
applications.push(ManualReplayApplication {
|
||||
action_id: action.action_id.clone(),
|
||||
order_id: order.order_id.clone(),
|
||||
trade_id: fill.trade_id.clone(),
|
||||
observation_event_id: fill.observation_event_id.clone(),
|
||||
observation_sequence: fill.observation_sequence,
|
||||
observed_at: fill.observed_at,
|
||||
fee_observation_event_id: fill.fee_observation_event_id.clone(),
|
||||
fee_observed_at: fill.fee_observed_at,
|
||||
executed_at: fill.executed_at,
|
||||
symbol: order.symbol.clone(),
|
||||
side: order.side,
|
||||
quantity: fill.quantity,
|
||||
quantity_after: applied.quantity_after,
|
||||
price: fill.price.to_string(),
|
||||
commission: fill.commission.map(|fee| fee.to_string()),
|
||||
stamp_tax: fill.stamp_tax.map(|fee| fee.to_string()),
|
||||
transfer_fee: fill.transfer_fee.map(|fee| fee.to_string()),
|
||||
source_total_fee: fill.total_fee.to_string(),
|
||||
source_gross_amount: fill.gross_amount()?.to_string(),
|
||||
ledger_gross_amount: applied.gross.to_decimal_string(),
|
||||
ledger_fees: applied.fees.to_decimal_string(),
|
||||
cash_delta: applied.cash_delta.to_decimal_string(),
|
||||
corporate_adjustment: None,
|
||||
});
|
||||
}
|
||||
*portfolio = next;
|
||||
self.cursor = end;
|
||||
self.clock = Some(at);
|
||||
Ok(applications)
|
||||
}
|
||||
}
|
||||
|
||||
impl ManualFillObservation<'_> {
|
||||
fn application(&self, applied: AppliedManualFill) -> Result<ManualReplayApplication, String> {
|
||||
Ok(ManualReplayApplication {
|
||||
action_id: self.action.action_id.clone(), order_id: self.order.order_id.clone(),
|
||||
trade_id: self.fill.trade_id.clone(), observation_event_id: self.fill.observation_event_id.clone(),
|
||||
observation_sequence: self.fill.observation_sequence, observed_at: self.fill.observed_at,
|
||||
fee_observation_event_id: self.fill.fee_observation_event_id.clone(), fee_observed_at: self.fill.fee_observed_at,
|
||||
executed_at: self.fill.executed_at, symbol: self.order.symbol.clone(), side: self.order.side,
|
||||
quantity: self.fill.quantity, quantity_after: applied.quantity_after, price: self.fill.price.to_string(),
|
||||
commission: self.fill.commission.map(|fee| fee.to_string()), stamp_tax: self.fill.stamp_tax.map(|fee| fee.to_string()),
|
||||
transfer_fee: self.fill.transfer_fee.map(|fee| fee.to_string()), source_total_fee: self.fill.total_fee.to_string(),
|
||||
source_gross_amount: self.fill.gross_amount()?.to_string(), ledger_gross_amount: applied.gross.to_decimal_string(),
|
||||
ledger_fees: applied.fees.to_decimal_string(), cash_delta: applied.cash_delta.to_decimal_string(), corporate_adjustment: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn apply(
|
||||
&self,
|
||||
portfolio: &mut PortfolioState,
|
||||
data: &DataSet,
|
||||
has_pending_orders: bool,
|
||||
) -> Result<AppliedManualFill, String> {
|
||||
if has_pending_orders {
|
||||
return Err("manual observation conflicts with pending shadow orders".into());
|
||||
}
|
||||
let instrument = data
|
||||
.instrument(&self.order.symbol)
|
||||
.ok_or("manual observation instrument is absent from frozen source data")?;
|
||||
if instrument
|
||||
.dated_market_absence_reason(self.fill.trade_date)
|
||||
.is_some()
|
||||
{
|
||||
return Err("manual execution contradicts the frozen instrument lifecycle".into());
|
||||
}
|
||||
let gross = FixedMoney::from_decimal_str(&self.fill.gross_amount()?.to_string())?;
|
||||
let fees = FixedMoney::from_decimal_str(&self.fill.total_fees()?.to_string())?;
|
||||
let price = self
|
||||
.fill
|
||||
.price
|
||||
.to_f64()
|
||||
.filter(|price| price.is_finite() && *price > 0.)
|
||||
.ok_or("manual execution price cannot be represented for valuation")?;
|
||||
// This is the real observed trade price, not a fabricated quote. The
|
||||
// normal market clock remains responsible for subsequent marks.
|
||||
let cash_delta = portfolio.apply_observed_manual_fill(
|
||||
self.fill.trade_date,
|
||||
&self.order.symbol,
|
||||
self.order.side,
|
||||
self.fill.quantity,
|
||||
price,
|
||||
price,
|
||||
gross,
|
||||
fees,
|
||||
)?;
|
||||
Ok(AppliedManualFill {
|
||||
gross,
|
||||
fees,
|
||||
cash_delta,
|
||||
quantity_after: portfolio
|
||||
.position(&self.order.symbol)
|
||||
.map_or(0, |position| position.quantity),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,761 @@
|
||||
use super::*;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn sample() -> ManualExecutionReplay {
|
||||
let fill = json!({"tradeId":"trade-1","observationEventId":"received-1","observationSequence":1,"tradeDate":"2026-09-14","executedAt":"2026-09-14T01:30:00Z",
|
||||
"feeObservationEventId":"received-1","feeObservationSequence":1,"feeObservedAt":"2026-09-14T01:30:01Z",
|
||||
"observedAt":"2026-09-14T01:30:01Z","timestampPrecision":"second","quantity":100,
|
||||
"price":"10.1234567891","commission":"0.1000001","stampTax":"0","transferFee":"0.02","totalFee":"0.1200001"});
|
||||
let mut input:ManualExecutionReplay=serde_json::from_value(json!({
|
||||
"schema":MANUAL_REPLAY_SCHEMA,"runtimeId":"runtime-1","accountId":"account-1",
|
||||
"sourceContractSha256":"a".repeat(64),"contentSha256":"", "observationCutoff":"2026-09-14T08:00:00Z",
|
||||
"actions":[{"actionId":"action-1","source":"manual_security_trade","auditEventIds":["audit-1"],
|
||||
"confirmedAt":"2026-09-14T01:30:00.500Z","confirmationObservedAt":"2026-09-14T01:30:00.550Z","outcome":"orders_terminal","orders":[{
|
||||
"orderId":"order-1","brokerOrderId":"broker-1","sourceAdapter":"gt-api","symbol":"000001.SZ","side":"Buy","quantity":100,
|
||||
"orderCreatedAt":"2026-09-14T01:30:00.600Z","terminalObservedAt":"2026-09-14T01:30:00.900Z","terminalStatus":"filled",
|
||||
"fills":[fill]
|
||||
}]}]
|
||||
})).unwrap();
|
||||
reseal(&mut input);
|
||||
input
|
||||
}
|
||||
|
||||
fn reseal(input: &mut ManualExecutionReplay) {
|
||||
input.content_sha256 = input.content_digest().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_corporate_projection_does_not_change_the_book_or_receipt_cursor() {
|
||||
let mut cursor = ManualReplayCursor::new(sample()).unwrap();
|
||||
let mut portfolio = PortfolioState::new(10000.);
|
||||
let before = portfolio.financial_replay_identity();
|
||||
let error = cursor.advance_next_projected(&mut portfolio, |_, next| {
|
||||
next.apply_cash_delta(-50.)?;
|
||||
next.position_mut("000001.SZ").buy(NaiveDate::from_ymd_opt(2026, 9, 11).unwrap(), 100, 10.);
|
||||
Err("financial coverage mismatch".into())
|
||||
}).unwrap_err();
|
||||
assert_eq!(error, "financial coverage mismatch");
|
||||
assert_eq!(portfolio.financial_replay_identity(), before);
|
||||
assert_eq!(cursor.applied_count(), 0);
|
||||
}
|
||||
|
||||
fn delayed_buy_replay() -> ManualExecutionReplay {
|
||||
let mut input = sample();
|
||||
let template = input.actions[0].clone();
|
||||
input.actions.clear();
|
||||
for (index, side, executed, observed, price, fee) in [
|
||||
(
|
||||
0,
|
||||
OrderSide::Buy,
|
||||
"2026-09-14T01:30:00Z",
|
||||
"2026-09-14T01:30:01Z",
|
||||
"20",
|
||||
"0.25",
|
||||
),
|
||||
(
|
||||
1,
|
||||
OrderSide::Buy,
|
||||
"2026-09-11T06:00:00Z",
|
||||
"2026-09-14T01:30:02Z",
|
||||
"10",
|
||||
"0.75",
|
||||
),
|
||||
(
|
||||
2,
|
||||
OrderSide::Sell,
|
||||
"2026-09-14T01:31:00Z",
|
||||
"2026-09-14T01:31:01Z",
|
||||
"10",
|
||||
"0.5",
|
||||
),
|
||||
(
|
||||
3,
|
||||
OrderSide::Sell,
|
||||
"2026-09-14T01:32:00Z",
|
||||
"2026-09-14T01:32:01Z",
|
||||
"10",
|
||||
"0.5",
|
||||
),
|
||||
] {
|
||||
let executed: DateTime<Utc> = executed.parse().unwrap();
|
||||
let observed: DateTime<Utc> = observed.parse().unwrap();
|
||||
let mut action = template.clone();
|
||||
action.action_id = format!("action-{index}");
|
||||
action.audit_event_ids = vec![format!("audit-{index}")];
|
||||
action.confirmed_at = executed - chrono::Duration::seconds(2);
|
||||
action.confirmation_observed_at = action.confirmed_at;
|
||||
let order = &mut action.orders[0];
|
||||
order.order_id = format!("order-{index}");
|
||||
order.broker_order_id = Some(format!("broker-{index}"));
|
||||
order.side = side;
|
||||
order.order_created_at = executed - chrono::Duration::seconds(1);
|
||||
order.terminal_observed_at = observed;
|
||||
let fill = &mut order.fills[0];
|
||||
fill.trade_id = format!("trade-{index}");
|
||||
fill.observation_event_id = format!("receipt-{index}");
|
||||
fill.observation_sequence = index + 1;
|
||||
fill.fee_observation_event_id = fill.observation_event_id.clone();
|
||||
fill.fee_observation_sequence = fill.observation_sequence;
|
||||
fill.trade_date = executed
|
||||
.with_timezone(&FixedOffset::east_opt(8 * 3600).unwrap())
|
||||
.date_naive();
|
||||
fill.executed_at = executed;
|
||||
fill.observed_at = observed;
|
||||
fill.fee_observed_at = observed;
|
||||
fill.price = price.parse().unwrap();
|
||||
fill.commission = None;
|
||||
fill.stamp_tax = None;
|
||||
fill.transfer_fee = None;
|
||||
fill.total_fee = fee.parse().unwrap();
|
||||
input.actions.push(action);
|
||||
}
|
||||
reseal(&mut input);
|
||||
input.validate().unwrap();
|
||||
input
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn late_buy_retains_the_earliest_opening_and_latest_buy_dates() {
|
||||
let mut cursor = ManualReplayCursor::new(delayed_buy_replay()).unwrap();
|
||||
let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
|
||||
let mut portfolio = PortfolioState::new(10000.);
|
||||
let applications = cursor
|
||||
.advance(
|
||||
"2026-09-14T01:30:02Z".parse().unwrap(),
|
||||
&mut portfolio,
|
||||
&data,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
applications
|
||||
.iter()
|
||||
.map(|row| row.trade_id.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
["trade-0", "trade-1"]
|
||||
);
|
||||
let position = portfolio.position("000001.SZ").unwrap();
|
||||
assert_eq!(position.opened_date(), NaiveDate::from_ymd_opt(2026, 9, 11));
|
||||
assert_eq!(
|
||||
position.last_buy_date(),
|
||||
NaiveDate::from_ymd_opt(2026, 9, 14)
|
||||
);
|
||||
assert_eq!(position.quantity, 200);
|
||||
let calendar = crate::TradingCalendar::new(
|
||||
[11, 14, 15, 16, 17, 18]
|
||||
.map(|day| NaiveDate::from_ymd_opt(2026, 9, day).unwrap())
|
||||
.into(),
|
||||
);
|
||||
let evidence = crate::holding_policy::HoldingLifecycleEvidence {
|
||||
has_position: true,
|
||||
opened_date: position.opened_date(),
|
||||
last_buy_date: position.last_buy_date(),
|
||||
last_sell_date: None,
|
||||
};
|
||||
let mut policy = crate::holding_policy::AutomaticTradeProtection {
|
||||
max_holding_days: 1,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(
|
||||
policy
|
||||
.evaluate(
|
||||
"000001.SZ",
|
||||
NaiveDate::from_ymd_opt(2026, 9, 14).unwrap(),
|
||||
&evidence,
|
||||
&calendar
|
||||
)
|
||||
.unwrap()
|
||||
.max_holding_exit
|
||||
);
|
||||
policy.buy_protection_days = 3;
|
||||
for day in [14, 15, 16, 17] {
|
||||
let permission = policy
|
||||
.evaluate(
|
||||
"000001.SZ",
|
||||
NaiveDate::from_ymd_opt(2026, 9, day).unwrap(),
|
||||
&evidence,
|
||||
&calendar,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(permission.sell_denial, Some("buy_fill_protection"));
|
||||
assert!(!permission.max_holding_exit);
|
||||
}
|
||||
assert!(
|
||||
policy
|
||||
.evaluate(
|
||||
"000001.SZ",
|
||||
NaiveDate::from_ymd_opt(2026, 9, 18).unwrap(),
|
||||
&evidence,
|
||||
&calendar
|
||||
)
|
||||
.unwrap()
|
||||
.max_holding_exit
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn late_buy_fifo_depletion_preserves_costs_and_cannot_unlock_today_lots() {
|
||||
let mut cursor = ManualReplayCursor::new(delayed_buy_replay()).unwrap();
|
||||
let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
|
||||
let mut portfolio = PortfolioState::new(10000.);
|
||||
let applications = cursor
|
||||
.advance(
|
||||
"2026-09-14T01:31:01Z".parse().unwrap(),
|
||||
&mut portfolio,
|
||||
&data,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(applications.len(), 3);
|
||||
let position = portfolio.position("000001.SZ").unwrap();
|
||||
assert_eq!(position.quantity, 100);
|
||||
assert_eq!(position.unrealized_pnl(), -1000.25);
|
||||
assert_eq!(
|
||||
position.sellable_qty(NaiveDate::from_ymd_opt(2026, 9, 14).unwrap()),
|
||||
0
|
||||
);
|
||||
assert_eq!(position.realized_pnl(), -0.75);
|
||||
assert_eq!(portfolio.cash(), 7998.5);
|
||||
assert_eq!(portfolio.external_cash_flow_total(), 0.);
|
||||
assert!(
|
||||
cursor
|
||||
.advance(
|
||||
"2026-09-14T01:32:01Z".parse().unwrap(),
|
||||
&mut portfolio,
|
||||
&data,
|
||||
false
|
||||
)
|
||||
.unwrap_err()
|
||||
.contains("T+1")
|
||||
);
|
||||
assert_eq!(cursor.applied_count(), 3);
|
||||
assert_eq!(portfolio.cash(), 7998.5);
|
||||
assert_eq!(portfolio.position("000001.SZ").unwrap().quantity, 100);
|
||||
}
|
||||
fn semantic_result(input: &ManualExecutionReplay) -> Result<(), String> {
|
||||
let mut input = input.clone();
|
||||
reseal(&mut input);
|
||||
input.validate()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_exact_decimal_evidence_allows_later_observation_and_retains_source_digits() {
|
||||
let input = sample();
|
||||
input.validate().unwrap();
|
||||
let fill = &input.actions[0].orders[0].fills[0];
|
||||
assert_eq!(fill.gross_amount().unwrap().to_string(), "1012.3456789100");
|
||||
assert_eq!(fill.total_fees().unwrap().to_string(), "0.1200001");
|
||||
assert_eq!(
|
||||
serde_json::to_value(&input).unwrap()["actions"][0]["orders"][0]["fills"][0]["price"],
|
||||
"10.1234567891"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_scope_only_contains_actual_filled_securities_and_validates_the_source() {
|
||||
let mut input = sample();
|
||||
let mut rejected = input.actions[0].orders[0].clone();
|
||||
rejected.order_id = "rejected-order".into();
|
||||
rejected.broker_order_id = None;
|
||||
rejected.source_adapter = None;
|
||||
rejected.symbol = "510300.SH".into();
|
||||
rejected.terminal_status = ManualOrderTerminalStatus::Rejected;
|
||||
rejected.fills.clear();
|
||||
input.actions[0].orders.push(rejected);
|
||||
reseal(&mut input);
|
||||
assert_eq!(
|
||||
input.required_data_symbols().unwrap(),
|
||||
BTreeSet::from(["000001.SZ".into()])
|
||||
);
|
||||
input.actions[0].orders[0].symbol = "600000.SH".into();
|
||||
assert!(input.required_data_symbols().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_facts_keep_their_encoding_but_cannot_silently_carry_new_runtime_settings() {
|
||||
let mut input = sample();
|
||||
input.schema = "fidc.observed-manual-executions/v2".into();
|
||||
reseal(&mut input);
|
||||
input.validate().unwrap();
|
||||
let old = serde_json::to_value(&input).unwrap();
|
||||
assert!(old.get("positionExposureEvents").is_none());
|
||||
assert!(old.get("legacyPositionExposureBps").is_none());
|
||||
input
|
||||
.legacy_position_exposure_bps
|
||||
.insert(NaiveDate::from_ymd_opt(2026, 9, 14).unwrap(), 5000);
|
||||
reseal(&mut input);
|
||||
assert!(input.validate().is_err());
|
||||
input.schema = MANUAL_REPLAY_SCHEMA.into();
|
||||
reseal(&mut input);
|
||||
input.validate().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_position_events_cannot_claim_observations_after_the_source_cutoff() {
|
||||
let mut input = sample();
|
||||
input.position_exposure_events.push(serde_json::from_value(json!({
|
||||
"eventId": "position-event", "sequence": 1, "effectiveAt": input.observation_cutoff,
|
||||
"action": "scale", "requestedBps": 5000
|
||||
})).unwrap());
|
||||
semantic_result(&input).unwrap();
|
||||
input.position_exposure_events[0].effective_at += chrono::Duration::nanoseconds(1);
|
||||
assert!(semantic_result(&input).unwrap_err().contains("after the evidence cutoff"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_required_money_and_binding_fields_reject_missing_or_wrong_values() {
|
||||
let original = serde_json::to_value(sample()).unwrap();
|
||||
for field in ["price", "totalFee"] {
|
||||
let mut missing = original.clone();
|
||||
missing["actions"][0]["orders"][0]["fills"][0]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.remove(field);
|
||||
assert!(
|
||||
serde_json::from_value::<ManualExecutionReplay>(missing).is_err(),
|
||||
"{field}"
|
||||
);
|
||||
let mut numeric = original.clone();
|
||||
numeric["actions"][0]["orders"][0]["fills"][0][field] = json!(1.1);
|
||||
assert!(
|
||||
serde_json::from_value::<ManualExecutionReplay>(numeric).is_err(),
|
||||
"numeric {field}"
|
||||
);
|
||||
}
|
||||
for mutate in [
|
||||
("schema", json!("unknown")),
|
||||
("sourceContractSha256", json!("broken")),
|
||||
("accountId", json!(" ")),
|
||||
] {
|
||||
let mut value = original.clone();
|
||||
value[mutate.0] = mutate.1;
|
||||
assert!(
|
||||
semantic_result(&serde_json::from_value::<ManualExecutionReplay>(value).unwrap())
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inconsistent_counts_terminals_audits_and_duplicate_facts_are_rejected() {
|
||||
let original = sample();
|
||||
let mut invalid = original.clone();
|
||||
invalid.actions[0].orders[0].quantity = 200;
|
||||
assert!(semantic_result(&invalid).is_err());
|
||||
let mut invalid = original.clone();
|
||||
invalid.actions[0].orders[0].terminal_status = ManualOrderTerminalStatus::Rejected;
|
||||
assert!(semantic_result(&invalid).is_err());
|
||||
let mut invalid = original.clone();
|
||||
invalid.actions[0].audit_event_ids.clear();
|
||||
assert!(semantic_result(&invalid).is_err());
|
||||
let mut invalid = original.clone();
|
||||
invalid.actions.push(invalid.actions[0].clone());
|
||||
assert!(semantic_result(&invalid).is_err());
|
||||
let mut invalid = original.clone();
|
||||
let duplicate = invalid.actions[0].orders[0].fills[0].clone();
|
||||
invalid.actions[0].orders[0].fills.push(duplicate);
|
||||
assert!(semantic_result(&invalid).is_err());
|
||||
let mut invalid = original.clone();
|
||||
invalid.actions[0].orders[0].broker_order_id = None;
|
||||
assert!(semantic_result(&invalid).is_err());
|
||||
invalid.actions[0].orders[0].source_adapter = Some("paper".into());
|
||||
reseal(&mut invalid);
|
||||
invalid.validate().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_time_precision_is_not_invented_and_submitted_time_must_fit_the_interval() {
|
||||
let mut input = sample();
|
||||
input.actions[0].orders[0].order_created_at = "2026-09-14T01:30:00.999999Z".parse().unwrap();
|
||||
input.actions[0].orders[0].terminal_observed_at = "2026-09-14T01:30:01.500Z".parse().unwrap();
|
||||
input.actions[0].orders[0].fills[0].observed_at = "2026-09-14T01:30:02Z".parse().unwrap();
|
||||
input.actions[0].orders[0].fills[0].fee_observed_at =
|
||||
input.actions[0].orders[0].fills[0].observed_at;
|
||||
reseal(&mut input);
|
||||
input.validate().unwrap();
|
||||
input.actions[0].orders[0].order_created_at = "2026-09-14T01:30:01Z".parse().unwrap();
|
||||
assert!(semantic_result(&input).is_err());
|
||||
let mut input = sample();
|
||||
input.actions[0].orders[0].fills[0].executed_at = "2026-09-14T01:30:00.800Z".parse().unwrap();
|
||||
assert!(semantic_result(&input).is_err());
|
||||
input.actions[0].orders[0].fills[0].timestamp_precision = ManualTimestampPrecision::Millisecond;
|
||||
reseal(&mut input);
|
||||
input.validate().unwrap();
|
||||
input.actions[0].orders[0].fills[0].executed_at =
|
||||
"2026-09-14T01:30:00.800001Z".parse().unwrap();
|
||||
assert!(semantic_result(&input).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirmed_no_order_outcome_is_distinct_from_unconfirmed_or_unknown_work() {
|
||||
let mut input = sample();
|
||||
input.actions[0].orders.clear();
|
||||
assert!(semantic_result(&input).is_err());
|
||||
input.actions[0].outcome = ManualActionOutcome::NoOrdersNeeded;
|
||||
reseal(&mut input);
|
||||
input.validate().unwrap();
|
||||
input.actions[0].outcome = ManualActionOutcome::NotExecuted;
|
||||
reseal(&mut input);
|
||||
input.validate().unwrap();
|
||||
let mut value = serde_json::to_value(input).unwrap();
|
||||
value["actions"][0]["outcome"] = json!("result_unknown");
|
||||
assert!(serde_json::from_value::<ManualExecutionReplay>(value).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_timezone_and_cutoff_are_required() {
|
||||
let mut value = serde_json::to_value(sample()).unwrap();
|
||||
value["actions"][0]["orders"][0]["fills"][0]["executedAt"] = json!("2026-09-14T09:30:00");
|
||||
assert!(serde_json::from_value::<ManualExecutionReplay>(value).is_err());
|
||||
let mut input = sample();
|
||||
input.observation_cutoff = "2026-09-14T01:30:00.700Z".parse().unwrap();
|
||||
assert!(semantic_result(&input).is_err());
|
||||
let mut value = serde_json::to_value(sample()).unwrap();
|
||||
value["actions"][0]["orders"][0]["fills"][0]["totalFee"] = Value::Null;
|
||||
assert!(serde_json::from_value::<ManualExecutionReplay>(value).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authoritative_total_fee_does_not_require_inventing_unknown_components() {
|
||||
let mut input = sample();
|
||||
let fill = &mut input.actions[0].orders[0].fills[0];
|
||||
fill.commission = None;
|
||||
fill.stamp_tax = None;
|
||||
fill.transfer_fee = None;
|
||||
assert_eq!(
|
||||
fill.total_fees().unwrap(),
|
||||
"0.1200001".parse::<Decimal>().unwrap()
|
||||
);
|
||||
assert!(semantic_result(&input).is_ok());
|
||||
let value = serde_json::to_value(&input).unwrap();
|
||||
assert!(value["actions"][0]["orders"][0]["fills"][0]["commission"].is_null());
|
||||
assert_eq!(
|
||||
value["actions"][0]["orders"][0]["fills"][0]["totalFee"],
|
||||
"0.1200001"
|
||||
);
|
||||
for field in ["commission", "stampTax", "transferFee"] {
|
||||
let mut numeric = value.clone();
|
||||
numeric["actions"][0]["orders"][0]["fills"][0][field] = json!(0.1);
|
||||
assert!(serde_json::from_value::<ManualExecutionReplay>(numeric).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manual_fee_total_includes_extra_charges_and_rejects_inconsistent_components() {
|
||||
let mut input = sample();
|
||||
input.actions[0].orders[0].fills[0].total_fee = "0.15".parse().unwrap();
|
||||
assert!(semantic_result(&input).is_ok());
|
||||
assert_eq!(
|
||||
input.actions[0].orders[0].fills[0]
|
||||
.total_fees()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
"0.15"
|
||||
);
|
||||
input.actions[0].orders[0].fills[0].total_fee = "0.1".parse().unwrap();
|
||||
assert!(semantic_result(&input).is_err());
|
||||
input.actions[0].orders[0].fills[0].total_fee = "0.15".parse().unwrap();
|
||||
input.actions[0].orders[0].fills[0].commission = Some(Decimal::NEGATIVE_ONE);
|
||||
assert!(semantic_result(&input).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn late_fee_evidence_keeps_the_original_fill_observation_clock() {
|
||||
let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
|
||||
let mut input = sample();
|
||||
let fill = &mut input.actions[0].orders[0].fills[0];
|
||||
let original = fill.observed_at;
|
||||
fill.fee_observation_event_id = "fee-receipt-1".into();
|
||||
fill.fee_observation_sequence = 2;
|
||||
fill.fee_observed_at = original + chrono::Duration::hours(1);
|
||||
let fee_time = fill.fee_observed_at;
|
||||
reseal(&mut input);
|
||||
let mut cursor = ManualReplayCursor::new(input).unwrap();
|
||||
assert_eq!(cursor.next_observation_at(), Some(original));
|
||||
let mut portfolio = PortfolioState::new(10_000.);
|
||||
let result = cursor
|
||||
.advance(original, &mut portfolio, &data, false)
|
||||
.unwrap();
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].observed_at, original);
|
||||
assert_eq!(result[0].fee_observed_at, fee_time);
|
||||
assert_eq!(result[0].source_total_fee, "0.1200001");
|
||||
assert!(
|
||||
cursor
|
||||
.advance(fee_time, &mut portfolio, &data, false)
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changing_any_external_price_or_identity_invalidates_the_frozen_trace() {
|
||||
let input = sample();
|
||||
let original = input.content_sha256.clone();
|
||||
let mut changed = input.clone();
|
||||
changed.actions[0].orders[0].fills[0].price += Decimal::ONE;
|
||||
assert_ne!(changed.content_digest().unwrap(), original);
|
||||
assert_eq!(
|
||||
changed.validate().unwrap_err(),
|
||||
"manual replay content digest mismatch"
|
||||
);
|
||||
let mut changed = input;
|
||||
changed.account_id = "another-account".into();
|
||||
assert_ne!(changed.content_digest().unwrap(), original);
|
||||
assert!(changed.validate().is_err());
|
||||
}
|
||||
|
||||
fn identity_data(listed: NaiveDate) -> DataSet {
|
||||
DataSet::from_components(
|
||||
vec![crate::Instrument {
|
||||
symbol: "000001.SZ".into(),
|
||||
name: "test".into(),
|
||||
board: "SZ".into(),
|
||||
round_lot: 100,
|
||||
listed_at: Some(listed),
|
||||
delisted_at: None,
|
||||
status: "active".into(),
|
||||
}],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![crate::BenchmarkSnapshot {
|
||||
date: listed,
|
||||
benchmark: "000300.SH".into(),
|
||||
open: 100.,
|
||||
close: 100.,
|
||||
prev_close: 100.,
|
||||
volume: 0,
|
||||
}],
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirmed_manual_fill_changes_cash_and_lots_but_not_external_cash_flow_units() {
|
||||
let input = sample();
|
||||
let observations = input.observations().unwrap();
|
||||
let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
|
||||
let mut account = PortfolioState::new(10_000.);
|
||||
let applied = observations[0].apply(&mut account, &data, false).unwrap();
|
||||
assert_eq!(
|
||||
applied.gross,
|
||||
FixedMoney::from_decimal_str("1012.345679").unwrap()
|
||||
);
|
||||
assert_eq!(applied.fees, FixedMoney::from_decimal_str("0.12").unwrap());
|
||||
assert_eq!(account.cash(), 8987.534321);
|
||||
assert_eq!(account.position("000001.SZ").unwrap().quantity, 100);
|
||||
assert_eq!(
|
||||
account
|
||||
.position("000001.SZ")
|
||||
.unwrap()
|
||||
.sellable_qty(input.actions[0].orders[0].fills[0].trade_date),
|
||||
0
|
||||
);
|
||||
assert_eq!(account.external_cash_flow_total(), 0.);
|
||||
assert_eq!(account.starting_cash(), 10_000.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manual_mismatches_are_atomic_and_do_not_borrow_shares_cash_or_override_pending_orders() {
|
||||
let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
|
||||
let input = sample();
|
||||
let observations = input.observations().unwrap();
|
||||
let mut poor = PortfolioState::new(10.);
|
||||
assert!(observations[0].apply(&mut poor, &data, false).is_err());
|
||||
assert_eq!(poor.cash(), 10.);
|
||||
assert!(poor.positions().is_empty());
|
||||
let mut account = PortfolioState::new(10_000.);
|
||||
assert!(observations[0].apply(&mut account, &data, true).is_err());
|
||||
assert_eq!(account.cash(), 10_000.);
|
||||
assert!(account.positions().is_empty());
|
||||
observations[0].apply(&mut account, &data, false).unwrap();
|
||||
let before = account.cash();
|
||||
let mut sell = input.clone();
|
||||
sell.actions[0].orders[0].side = OrderSide::Sell;
|
||||
reseal(&mut sell);
|
||||
assert!(
|
||||
sell.observations().unwrap()[0]
|
||||
.apply(&mut account, &data, false)
|
||||
.unwrap_err()
|
||||
.contains("T+1")
|
||||
);
|
||||
assert_eq!(account.cash(), before);
|
||||
assert_eq!(account.position("000001.SZ").unwrap().quantity, 100);
|
||||
let unlisted = identity_data(NaiveDate::from_ymd_opt(2027, 1, 1).unwrap());
|
||||
assert!(
|
||||
observations[0]
|
||||
.apply(&mut account, &unlisted, false)
|
||||
.unwrap_err()
|
||||
.contains("lifecycle")
|
||||
);
|
||||
assert_eq!(account.cash(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_next_day_manual_sale_keeps_the_actual_quantity_and_fee_contract() {
|
||||
let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
|
||||
let input = sample();
|
||||
let mut account = PortfolioState::new(10_000.);
|
||||
input.observations().unwrap()[0]
|
||||
.apply(&mut account, &data, false)
|
||||
.unwrap();
|
||||
let mut sell = input.clone();
|
||||
let order = &mut sell.actions[0].orders[0];
|
||||
order.side = OrderSide::Sell;
|
||||
order.order_created_at += chrono::Duration::days(1);
|
||||
order.terminal_observed_at += chrono::Duration::days(1);
|
||||
order.fills[0].trade_date = order.fills[0].trade_date.succ_opt().unwrap();
|
||||
order.fills[0].executed_at += chrono::Duration::days(1);
|
||||
order.fills[0].observed_at += chrono::Duration::days(1);
|
||||
order.fills[0].fee_observed_at += chrono::Duration::days(1);
|
||||
sell.observation_cutoff += chrono::Duration::days(1);
|
||||
reseal(&mut sell);
|
||||
let applied = sell.observations().unwrap()[0]
|
||||
.apply(&mut account, &data, false)
|
||||
.unwrap();
|
||||
assert_eq!(applied.quantity_after, 0);
|
||||
assert_eq!(account.cash(), 9999.76);
|
||||
assert_eq!(account.external_cash_flow_total(), 0.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observations_follow_durable_receipt_order_and_not_input_array_order() {
|
||||
let mut input = sample();
|
||||
let mut second = input.actions[0].orders[0].fills[0].clone();
|
||||
second.trade_id = "trade-2".into();
|
||||
second.observation_event_id = "received-2".into();
|
||||
second.observation_sequence = 2;
|
||||
second.fee_observation_event_id = "received-2".into();
|
||||
second.fee_observation_sequence = 2;
|
||||
input.actions[0].orders[0].quantity = 200;
|
||||
input.actions[0].orders[0].fills.insert(0, second);
|
||||
reseal(&mut input);
|
||||
assert_eq!(
|
||||
input
|
||||
.observations()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|row| row.fill.observation_sequence)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![1, 2]
|
||||
);
|
||||
let mut invalid = input.clone();
|
||||
invalid.actions[0].orders[0].fills[0].observation_sequence = 1;
|
||||
assert!(
|
||||
semantic_result(&invalid)
|
||||
.unwrap_err()
|
||||
.contains("observation")
|
||||
);
|
||||
let mut invalid = input;
|
||||
invalid.actions[0].orders[0].fills[0].observation_event_id = "received-1".into();
|
||||
assert!(
|
||||
semantic_result(&invalid)
|
||||
.unwrap_err()
|
||||
.contains("observation")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_cancel_is_valid_but_full_fill_cannot_be_reported_as_cancelled() {
|
||||
let mut input = sample();
|
||||
input.actions[0].orders[0].quantity = 200;
|
||||
input.actions[0].orders[0].terminal_status = ManualOrderTerminalStatus::Cancelled;
|
||||
semantic_result(&input).unwrap();
|
||||
input.actions[0].orders[0].quantity = 100;
|
||||
assert!(
|
||||
semantic_result(&input)
|
||||
.unwrap_err()
|
||||
.contains("terminal status")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_waits_for_observation_and_never_reapplies_or_rewinds() {
|
||||
let input = sample();
|
||||
let at = input.actions[0].orders[0].fills[0].observed_at;
|
||||
let mut replay = ManualReplayCursor::new(input).unwrap();
|
||||
let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
|
||||
let mut account = PortfolioState::new(10_000.);
|
||||
assert_eq!(replay.next_observation_at(), Some(at));
|
||||
assert!(
|
||||
replay
|
||||
.advance(
|
||||
at - chrono::Duration::milliseconds(1),
|
||||
&mut account,
|
||||
&data,
|
||||
false
|
||||
)
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
assert_eq!(account.cash(), 10_000.);
|
||||
let records = replay.advance(at, &mut account, &data, false).unwrap();
|
||||
assert_eq!(records.len(), 1);
|
||||
assert_eq!(records[0].cash_delta, "-1012.465679");
|
||||
assert_eq!(replay.applied_count(), 1);
|
||||
assert_eq!(replay.next_observation_at(), None);
|
||||
let cash = account.cash();
|
||||
assert!(
|
||||
replay
|
||||
.advance(at, &mut account, &data, false)
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
assert_eq!(account.cash(), cash);
|
||||
assert!(
|
||||
replay
|
||||
.advance(
|
||||
at - chrono::Duration::seconds(1),
|
||||
&mut account,
|
||||
&data,
|
||||
false
|
||||
)
|
||||
.unwrap_err()
|
||||
.contains("backwards")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_multi_receipt_advance_keeps_both_progress_and_portfolio_unchanged() {
|
||||
let mut input = sample();
|
||||
let mut next = input.actions[0].orders[0].fills[0].clone();
|
||||
next.trade_id = "trade-2".into();
|
||||
next.observation_event_id = "received-2".into();
|
||||
next.observation_sequence = 2;
|
||||
next.fee_observation_event_id = "received-2".into();
|
||||
next.fee_observation_sequence = 2;
|
||||
input.actions[0].orders[0].quantity = 200;
|
||||
input.actions[0].orders[0].fills.push(next);
|
||||
reseal(&mut input);
|
||||
let at = input.actions[0].orders[0].fills[0].observed_at;
|
||||
let mut replay = ManualReplayCursor::new(input).unwrap();
|
||||
let data = identity_data(NaiveDate::from_ymd_opt(2020, 1, 1).unwrap());
|
||||
let mut account = PortfolioState::new(1_500.);
|
||||
assert!(replay.advance(at, &mut account, &data, false).is_err());
|
||||
assert_eq!(account.cash(), 1_500.);
|
||||
assert!(account.positions().is_empty());
|
||||
assert_eq!(replay.applied_count(), 0);
|
||||
assert_eq!(replay.next_observation_at(), Some(at));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_money_decimal_text_preserves_micro_units_without_float_conversion() {
|
||||
for text in [
|
||||
"0",
|
||||
"100",
|
||||
"-100",
|
||||
"0.000001",
|
||||
"-0.000001",
|
||||
"12345678901234567890123456.123456",
|
||||
] {
|
||||
assert_eq!(
|
||||
FixedMoney::from_decimal_str(text)
|
||||
.unwrap()
|
||||
.to_decimal_string(),
|
||||
text
|
||||
);
|
||||
}
|
||||
let min = FixedMoney::from_raw(i128::MIN);
|
||||
assert!(min.to_decimal_string().starts_with('-'));
|
||||
}
|
||||
+189
-100
@@ -9,6 +9,75 @@ use crate::portfolio::HoldingSummary;
|
||||
|
||||
const TRADING_DAYS_PER_YEAR: f64 = 252.0;
|
||||
const MONTHS_PER_YEAR: f64 = 12.0;
|
||||
pub const RISK_STATISTICS_VERSION: &str = "fidc-risk-statistics/v2";
|
||||
|
||||
/// Shared by historical backtests and observed paper/live account returns.
|
||||
/// Undefined ratios remain None; callers must not invent a risk-free rate.
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
pub struct RiskAdjustedStatistics {
|
||||
pub sharpe: Option<f64>,
|
||||
pub sortino: Option<f64>,
|
||||
pub downside_volatility: Option<f64>,
|
||||
}
|
||||
|
||||
pub fn risk_adjusted_statistics(
|
||||
returns: &[f64], rates: &[f64], periods_per_year: f64,
|
||||
) -> Result<RiskAdjustedStatistics, &'static str> {
|
||||
if returns.len() != rates.len() || !periods_per_year.is_finite() || periods_per_year <= 0.0
|
||||
|| returns.iter().chain(rates).any(|value| !value.is_finite()) {
|
||||
return Err("risk-adjusted statistics require finite aligned returns and rates");
|
||||
}
|
||||
if returns.is_empty() { return Ok(RiskAdjustedStatistics::default()); }
|
||||
let adjusted: Vec<_> = returns.iter().zip(rates).map(|(value, rate)| value-rate).collect();
|
||||
if adjusted.iter().any(|value| !value.is_finite()) { return Err("risk-adjusted return overflow"); }
|
||||
let mean_return = mean(&adjusted);
|
||||
let deviation = std_dev(&adjusted);
|
||||
let downside = (adjusted.iter().map(|value| value.min(0.0).powi(2)).sum::<f64>() / adjusted.len() as f64).sqrt();
|
||||
let annual = periods_per_year.sqrt();
|
||||
Ok(RiskAdjustedStatistics {
|
||||
sharpe: (adjusted.len() > 1 && deviation > f64::EPSILON).then_some(mean_return/deviation*annual).filter(|value|value.is_finite()),
|
||||
sortino: (downside > f64::EPSILON).then_some(mean_return/downside*annual).filter(|value|value.is_finite()),
|
||||
downside_volatility: Some(downside*annual).filter(|value|value.is_finite()),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod risk_adjusted_contract_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn changing_daily_rates_adjusts_each_return_before_variance_and_downside() {
|
||||
let returns = [0.02, -0.01, 0.005];
|
||||
let rates = [0.0001, 0.0002, 0.0003];
|
||||
let values: Vec<f64> = returns.iter().zip(rates).map(|(r,f)| r-f).collect();
|
||||
let stats = risk_adjusted_statistics(&returns,&rates,252.0).unwrap();
|
||||
let average = values.iter().sum::<f64>()/3.0;
|
||||
let deviation = (values.iter().map(|r|(r-average).powi(2)).sum::<f64>()/2.0).sqrt();
|
||||
let downside = (values.iter().map(|r|r.min(0.0).powi(2)).sum::<f64>()/3.0).sqrt();
|
||||
assert!((stats.sharpe.unwrap()-average/deviation*252.0_f64.sqrt()).abs()<1e-12);
|
||||
assert!((stats.sortino.unwrap()-average/downside*252.0_f64.sqrt()).abs()<1e-12);
|
||||
assert_ne!(stats.sharpe, risk_adjusted_statistics(&returns,&[0.0;3],252.0).unwrap().sharpe);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn incomplete_or_invalid_rates_are_not_zero_rate_observations() {
|
||||
for rates in [vec![],vec![0.0],vec![0.0,f64::NAN],vec![0.0,f64::INFINITY]] {
|
||||
assert!(risk_adjusted_statistics(&[0.01,-0.01],&rates,252.0).is_err());
|
||||
}
|
||||
assert!(risk_adjusted_statistics(&[f64::NAN],&[0.0],252.0).is_err());
|
||||
assert!(risk_adjusted_statistics(&[0.0],&[0.0],0.0).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_deviation_and_single_sample_ratios_remain_undefined() {
|
||||
let flat=risk_adjusted_statistics(&[0.001,0.001],&[0.001,0.001],252.0).unwrap();
|
||||
assert!(flat.sharpe.is_none() && flat.sortino.is_none());
|
||||
assert_eq!(flat.downside_volatility,Some(0.0));
|
||||
let one=risk_adjusted_statistics(&[-0.01],&[0.001],252.0).unwrap();
|
||||
assert_eq!(one.sharpe,None);
|
||||
assert!((one.sortino.unwrap()+252.0_f64.sqrt()).abs()<1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -34,13 +103,15 @@ pub struct RiskFreeRateContract {
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct BacktestMetrics {
|
||||
#[serde(default)]
|
||||
pub risk_statistics_version: String,
|
||||
pub total_return: f64,
|
||||
pub annual_return: f64,
|
||||
pub sharpe: f64,
|
||||
pub sharpe: Option<f64>,
|
||||
pub max_drawdown: f64,
|
||||
pub win_rate: f64,
|
||||
pub alpha: f64,
|
||||
pub beta: f64,
|
||||
pub alpha: Option<f64>,
|
||||
pub beta: Option<f64>,
|
||||
pub benchmark_cumulative_return: f64,
|
||||
pub benchmark_net_value: f64,
|
||||
pub risk_free_rate: f64,
|
||||
@@ -49,14 +120,14 @@ pub struct BacktestMetrics {
|
||||
pub excess_annual_return: f64,
|
||||
pub max_drawdown_duration_days: usize,
|
||||
pub total_trade_days: usize,
|
||||
pub sortino: f64,
|
||||
pub downside_risk: f64,
|
||||
pub information_ratio: f64,
|
||||
pub tracking_error: f64,
|
||||
pub volatility: f64,
|
||||
pub sortino: Option<f64>,
|
||||
pub downside_risk: Option<f64>,
|
||||
pub information_ratio: Option<f64>,
|
||||
pub tracking_error: Option<f64>,
|
||||
pub volatility: Option<f64>,
|
||||
pub excess_return: f64,
|
||||
pub excess_sharpe: f64,
|
||||
pub excess_volatility: f64,
|
||||
pub excess_sharpe: Option<f64>,
|
||||
pub excess_volatility: Option<f64>,
|
||||
pub excess_max_drawdown: f64,
|
||||
pub holding_count: usize,
|
||||
pub average_weight: f64,
|
||||
@@ -75,8 +146,8 @@ pub struct BacktestMetrics {
|
||||
#[serde(default)]
|
||||
pub external_cash_flow_total: f64,
|
||||
pub excess_win_rate: f64,
|
||||
pub monthly_sharpe: f64,
|
||||
pub monthly_volatility: f64,
|
||||
pub monthly_sharpe: Option<f64>,
|
||||
pub monthly_volatility: Option<f64>,
|
||||
pub risk_free_rate_contract_version: String,
|
||||
pub risk_free_rate_source: String,
|
||||
pub risk_free_rate_tenor: String,
|
||||
@@ -93,15 +164,26 @@ pub fn compute_backtest_metrics(
|
||||
account_events: &[AccountEvent],
|
||||
initial_cash: f64,
|
||||
risk_free_contract: Option<&RiskFreeRateContract>,
|
||||
) -> Result<BacktestMetrics, String> {
|
||||
compute_backtest_metrics_with_manual(equity_curve, fills, &[], daily_holdings, account_events, initial_cash, risk_free_contract)
|
||||
}
|
||||
|
||||
pub fn compute_backtest_metrics_with_manual(
|
||||
equity_curve: &[DailyEquityPoint], fills: &[FillEvent],
|
||||
manual_executions: &[crate::manual_execution::ManualReplayApplication],
|
||||
daily_holdings: &[HoldingSummary], account_events: &[AccountEvent], initial_cash: f64,
|
||||
risk_free_contract: Option<&RiskFreeRateContract>,
|
||||
) -> Result<BacktestMetrics, String> {
|
||||
let Some(first_point) = equity_curve.first() else {
|
||||
return Ok(BacktestMetrics {
|
||||
risk_statistics_version: RISK_STATISTICS_VERSION.into(),
|
||||
initial_cash,
|
||||
..BacktestMetrics::default()
|
||||
});
|
||||
};
|
||||
let Some(last_point) = equity_curve.last() else {
|
||||
return Ok(BacktestMetrics {
|
||||
risk_statistics_version: RISK_STATISTICS_VERSION.into(),
|
||||
initial_cash,
|
||||
..BacktestMetrics::default()
|
||||
});
|
||||
@@ -124,6 +206,10 @@ pub fn compute_backtest_metrics(
|
||||
flow_neutral_nav_series(equity_curve, account_events, initial_cash)
|
||||
};
|
||||
let mut returns = Vec::with_capacity(portfolio_nav.len());
|
||||
// A zero terminal NAV is a real -100% return. A later return starting
|
||||
// from zero has no denominator and must not become another zero return.
|
||||
let risk_periods_valid = portfolio_nav.iter().all(|nav|nav.is_finite() && *nav >= 0.0)
|
||||
&& portfolio_nav.windows(2).all(|pair| pair[0] > 0.0);
|
||||
if let Some(first_nav) = portfolio_nav.first().copied() {
|
||||
returns.push(pct_change(1.0, first_nav));
|
||||
}
|
||||
@@ -167,24 +253,23 @@ pub fn compute_backtest_metrics(
|
||||
aligned_daily_risk_free_rates(equity_curve, risk_free_contract)?;
|
||||
let risk_free_rate =
|
||||
effective_annual_risk_free_rate(&daily_risk_free_rates, TRADING_DAYS_PER_YEAR);
|
||||
let sharpe = annualized_sharpe(&returns, &daily_risk_free_rates, TRADING_DAYS_PER_YEAR);
|
||||
let sortino = annualized_sortino(&returns, &daily_risk_free_rates, TRADING_DAYS_PER_YEAR);
|
||||
let downside_risk =
|
||||
annualized_downside_risk(&returns, &daily_risk_free_rates, TRADING_DAYS_PER_YEAR);
|
||||
let information_ratio = annualized_sharpe(
|
||||
let risk_stats = if risk_periods_valid {
|
||||
risk_adjusted_statistics(&returns, &daily_risk_free_rates, TRADING_DAYS_PER_YEAR)?
|
||||
} else { RiskAdjustedStatistics::default() };
|
||||
let sharpe = risk_stats.sharpe;
|
||||
let sortino = risk_stats.sortino;
|
||||
let downside_risk = risk_stats.downside_volatility;
|
||||
let excess_stats = if risk_periods_valid { risk_adjusted_statistics(
|
||||
&excess_returns,
|
||||
&zero_risk_free_rates,
|
||||
TRADING_DAYS_PER_YEAR,
|
||||
);
|
||||
let tracking_error = annualized_std(&excess_returns, TRADING_DAYS_PER_YEAR);
|
||||
let volatility = annualized_std(&returns, TRADING_DAYS_PER_YEAR);
|
||||
let excess_volatility = annualized_std(&excess_returns, TRADING_DAYS_PER_YEAR);
|
||||
let excess_sharpe = annualized_sharpe(
|
||||
&excess_returns,
|
||||
&zero_risk_free_rates,
|
||||
TRADING_DAYS_PER_YEAR,
|
||||
);
|
||||
let (alpha, beta) = alpha_beta(&returns, &benchmark_returns, &daily_risk_free_rates);
|
||||
)? } else { RiskAdjustedStatistics::default() };
|
||||
let information_ratio = excess_stats.sharpe;
|
||||
let tracking_error = risk_periods_valid.then(||annualized_std(&excess_returns, TRADING_DAYS_PER_YEAR)).flatten();
|
||||
let volatility = risk_periods_valid.then(||annualized_std(&returns, TRADING_DAYS_PER_YEAR)).flatten();
|
||||
let excess_volatility = tracking_error;
|
||||
let excess_sharpe = excess_stats.sharpe;
|
||||
let (alpha, beta) = if risk_periods_valid { alpha_beta(&returns, &benchmark_returns, &daily_risk_free_rates) } else { (None,None) };
|
||||
|
||||
let equity_nav = portfolio_nav;
|
||||
let benchmark_nav_series = equity_curve
|
||||
@@ -222,19 +307,27 @@ pub fn compute_backtest_metrics(
|
||||
.count(),
|
||||
monthly_excess_returns.len(),
|
||||
);
|
||||
let monthly_sharpe = annualized_sharpe(
|
||||
let monthly_sharpe = if risk_periods_valid { risk_adjusted_statistics(
|
||||
&monthly_portfolio_returns,
|
||||
&monthly_risk_free_returns,
|
||||
MONTHS_PER_YEAR,
|
||||
);
|
||||
let monthly_volatility = annualized_std(&monthly_portfolio_returns, MONTHS_PER_YEAR);
|
||||
)?.sharpe } else { None };
|
||||
let monthly_volatility = risk_periods_valid.then(||annualized_std(&monthly_portfolio_returns, MONTHS_PER_YEAR)).flatten();
|
||||
|
||||
let turnover_by_date = fills
|
||||
let mut turnover_by_date = fills
|
||||
.iter()
|
||||
.fold(BTreeMap::<NaiveDate, f64>::new(), |mut acc, fill| {
|
||||
*acc.entry(fill.date).or_default() += fill.gross_amount.abs();
|
||||
acc
|
||||
});
|
||||
for execution in manual_executions {
|
||||
use rust_decimal::prelude::ToPrimitive;
|
||||
let gross = execution.ledger_gross_amount.parse::<rust_decimal::Decimal>()
|
||||
.ok().and_then(|value| value.to_f64()).filter(|value| value.is_finite() && *value >= 0.)
|
||||
.ok_or("manual turnover requires its validated ledger gross amount")?;
|
||||
let date = execution.observed_at.with_timezone(&chrono::FixedOffset::east_opt(8 * 3600).unwrap()).date_naive();
|
||||
*turnover_by_date.entry(date).or_default() += gross;
|
||||
}
|
||||
let equity_by_date = equity_curve
|
||||
.iter()
|
||||
.map(|point| (point.date, point.total_equity))
|
||||
@@ -277,6 +370,7 @@ pub fn compute_backtest_metrics(
|
||||
let total_trade_days = equity_by_date.len();
|
||||
|
||||
Ok(BacktestMetrics {
|
||||
risk_statistics_version: RISK_STATISTICS_VERSION.into(),
|
||||
total_return,
|
||||
annual_return,
|
||||
sharpe,
|
||||
@@ -453,80 +547,21 @@ fn effective_annual_risk_free_rate(daily_rates: &[f64], periods_per_year: f64) -
|
||||
(mean_log * periods_per_year).exp_m1()
|
||||
}
|
||||
|
||||
fn annualized_sharpe(returns: &[f64], daily_risk_free_rates: &[f64], periods_per_year: f64) -> f64 {
|
||||
if returns.len() < 2 || returns.len() != daily_risk_free_rates.len() {
|
||||
return 0.0;
|
||||
}
|
||||
let adjusted = returns
|
||||
.iter()
|
||||
.zip(daily_risk_free_rates)
|
||||
.map(|(value, risk_free)| value - risk_free)
|
||||
.collect::<Vec<_>>();
|
||||
let mean_ret = mean(&adjusted);
|
||||
let std = std_dev(&adjusted);
|
||||
if std <= f64::EPSILON {
|
||||
0.0
|
||||
} else {
|
||||
mean_ret / std * periods_per_year.sqrt()
|
||||
}
|
||||
}
|
||||
|
||||
fn annualized_sortino(
|
||||
returns: &[f64],
|
||||
daily_risk_free_rates: &[f64],
|
||||
periods_per_year: f64,
|
||||
) -> f64 {
|
||||
if returns.is_empty() || returns.len() != daily_risk_free_rates.len() {
|
||||
return 0.0;
|
||||
}
|
||||
let adjusted = returns
|
||||
.iter()
|
||||
.zip(daily_risk_free_rates)
|
||||
.map(|(value, risk_free)| value - risk_free)
|
||||
.collect::<Vec<_>>();
|
||||
let downside = adjusted
|
||||
.iter()
|
||||
.map(|value| value.min(0.0).powi(2))
|
||||
.sum::<f64>();
|
||||
let downside_dev = (downside / adjusted.len() as f64).sqrt();
|
||||
if downside_dev <= f64::EPSILON {
|
||||
0.0
|
||||
} else {
|
||||
mean(&adjusted) / downside_dev * periods_per_year.sqrt()
|
||||
}
|
||||
}
|
||||
|
||||
fn annualized_downside_risk(
|
||||
returns: &[f64],
|
||||
daily_risk_free_rates: &[f64],
|
||||
periods_per_year: f64,
|
||||
) -> f64 {
|
||||
if returns.is_empty() || returns.len() != daily_risk_free_rates.len() {
|
||||
return 0.0;
|
||||
}
|
||||
let downside_mean_square = returns
|
||||
.iter()
|
||||
.zip(daily_risk_free_rates)
|
||||
.map(|(value, risk_free)| (value - risk_free).min(0.0).powi(2))
|
||||
.sum::<f64>()
|
||||
/ returns.len() as f64;
|
||||
downside_mean_square.sqrt() * periods_per_year.sqrt()
|
||||
}
|
||||
|
||||
fn annualized_std(values: &[f64], periods_per_year: f64) -> f64 {
|
||||
std_dev(values) * periods_per_year.sqrt()
|
||||
fn annualized_std(values: &[f64], periods_per_year: f64) -> Option<f64> {
|
||||
(values.len() > 1).then(|| std_dev(values) * periods_per_year.sqrt())
|
||||
.filter(|value| value.is_finite())
|
||||
}
|
||||
|
||||
fn alpha_beta(
|
||||
returns: &[f64],
|
||||
benchmark_returns: &[f64],
|
||||
daily_risk_free_rates: &[f64],
|
||||
) -> (f64, f64) {
|
||||
) -> (Option<f64>, Option<f64>) {
|
||||
if returns.len() < 2
|
||||
|| returns.len() != benchmark_returns.len()
|
||||
|| returns.len() != daily_risk_free_rates.len()
|
||||
{
|
||||
return (0.0, 0.0);
|
||||
return (None, None);
|
||||
}
|
||||
let strategy_excess = returns
|
||||
.iter()
|
||||
@@ -544,7 +579,7 @@ fn alpha_beta(
|
||||
let mean_raw_benchmark = mean(benchmark_returns);
|
||||
let variance_benchmark = variance(benchmark_returns);
|
||||
if variance_benchmark <= f64::EPSILON {
|
||||
return (0.0, 0.0);
|
||||
return (None, None);
|
||||
}
|
||||
let covariance = returns
|
||||
.iter()
|
||||
@@ -554,7 +589,7 @@ fn alpha_beta(
|
||||
/ (strategy_excess.len() - 1) as f64;
|
||||
let beta = covariance / variance_benchmark;
|
||||
let alpha = (mean_strategy - beta * mean_benchmark) * TRADING_DAYS_PER_YEAR;
|
||||
(alpha, beta)
|
||||
(Some(alpha).filter(|value| value.is_finite()), Some(beta).filter(|value| value.is_finite()))
|
||||
}
|
||||
|
||||
fn drawdown_stats(nav: &[f64]) -> (f64, usize) {
|
||||
@@ -911,12 +946,66 @@ mod tests {
|
||||
/ adjusted.len() as f64)
|
||||
.sqrt();
|
||||
let expected_sortino = mean(&adjusted) / downside * TRADING_DAYS_PER_YEAR.sqrt();
|
||||
assert!((metrics.sharpe - expected_sharpe).abs() < 1e-12);
|
||||
assert!((metrics.sortino - expected_sortino).abs() < 1e-12);
|
||||
assert!((metrics.downside_risk - downside * TRADING_DAYS_PER_YEAR.sqrt()).abs() < 1e-12);
|
||||
assert!((metrics.sharpe.unwrap() - expected_sharpe).abs() < 1e-12);
|
||||
assert!((metrics.sortino.unwrap() - expected_sortino).abs() < 1e-12);
|
||||
assert!((metrics.downside_risk.unwrap() - downside * TRADING_DAYS_PER_YEAR.sqrt()).abs() < 1e-12);
|
||||
assert_eq!(metrics.risk_free_rate_source, "test");
|
||||
assert_eq!(metrics.risk_free_rate_tenor, "3M");
|
||||
assert_eq!(metrics.risk_free_rate_observation_count, 4);
|
||||
assert_ne!(metrics.risk_free_rate, 0.022);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undefined_ratios_remain_null_in_backtest_metrics() {
|
||||
for curve in [vec![], vec![equity_point("2026-01-02", 100.0, 100.0, 100.0)],
|
||||
vec![equity_point("2026-01-02", 100.0, 100.0, 100.0),
|
||||
equity_point("2026-01-05", 100.0, 100.0, 100.0)]] {
|
||||
let metrics = compute_backtest_metrics(&curve, &[], &[], &[], 100.0, None).unwrap();
|
||||
let json = serde_json::to_value(&metrics).unwrap();
|
||||
assert_eq!(json["risk_statistics_version"], RISK_STATISTICS_VERSION);
|
||||
for field in ["sharpe", "sortino", "alpha", "beta", "information_ratio", "excess_sharpe", "monthly_sharpe"] {
|
||||
assert!(json[field].is_null(), "{field}: {json}");
|
||||
}
|
||||
if curve.len() < 2 { assert_eq!(metrics.volatility, None); }
|
||||
else { assert_eq!(metrics.volatility, Some(0.0)); }
|
||||
assert_eq!(metrics.downside_risk, (!curve.is_empty()).then_some(0.0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_defined_zero_ratio_is_not_missing() {
|
||||
let stats = risk_adjusted_statistics(&[0.01,-0.01], &[0.0,0.0], 252.0).unwrap();
|
||||
assert_eq!(stats.sharpe, Some(0.0));
|
||||
assert_eq!(stats.sortino, Some(0.0));
|
||||
let (alpha, beta) = alpha_beta(&[0.0,0.0], &[0.01,-0.01], &[0.0,0.0]);
|
||||
assert_eq!((alpha,beta), (Some(0.0),Some(0.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gains_without_downside_have_no_sortino_or_monthly_sharpe() {
|
||||
let curve = vec![equity_point("2026-01-02", 101.0, 101.0, 100.0),
|
||||
equity_point("2026-01-05", 103.02, 103.02, 101.0)];
|
||||
let metrics = compute_backtest_metrics(&curve,&[],&[],&[],100.0,None).unwrap();
|
||||
assert!(metrics.sharpe.unwrap() > 0.0);
|
||||
assert_eq!(metrics.sortino,None);
|
||||
assert_eq!(metrics.downside_risk,Some(0.0));
|
||||
assert_eq!(metrics.information_ratio,None);
|
||||
assert_eq!(metrics.excess_sharpe,None);
|
||||
assert_eq!(metrics.tracking_error,Some(0.0));
|
||||
assert_eq!(metrics.monthly_sharpe,None);
|
||||
assert_eq!(metrics.monthly_volatility,None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_risk_ratio_is_fabricated_after_nav_has_reached_zero() {
|
||||
let curve = vec![equity_point("2026-01-02", 0.0, 101.0, 100.0),
|
||||
equity_point("2026-01-05", 0.0, 102.0, 101.0)];
|
||||
let metrics=compute_backtest_metrics(&curve,&[],&[],&[],100.0,None).unwrap();
|
||||
assert_eq!(metrics.total_return,-1.0);
|
||||
for value in [metrics.sharpe,metrics.sortino,metrics.alpha,metrics.beta,metrics.information_ratio,metrics.volatility,metrics.monthly_sharpe] {
|
||||
assert_eq!(value,None);
|
||||
}
|
||||
let loss=compute_backtest_metrics(&curve[..1],&[],&[],&[],100.0,None).unwrap();
|
||||
assert!(loss.sortino.unwrap()<0.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,415 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::ops::Index;
|
||||
|
||||
use compact_str::CompactString;
|
||||
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<(CompactString, f64)>,
|
||||
}
|
||||
|
||||
fn compact_key(key: Cow<'static, str>) -> CompactString {
|
||||
match key {
|
||||
Cow::Borrowed(value) => CompactString::const_new(value),
|
||||
Cow::Owned(value) => CompactString::from(value),
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/// Reserve known new fields without geometric spare capacity per snapshot.
|
||||
pub fn reserve_exact(&mut self, additional: usize) {
|
||||
self.entries.reserve_exact(additional);
|
||||
}
|
||||
|
||||
pub fn get(&self, key: &str) -> Option<&f64> {
|
||||
self.entries
|
||||
.binary_search_by(|(name, _)| name.as_str().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_str().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> {
|
||||
self.insert_compact(compact_key(key), value)
|
||||
}
|
||||
|
||||
pub fn insert_compact(&mut self, key: CompactString, value: f64) -> Option<f64> {
|
||||
if self
|
||||
.entries
|
||||
.last()
|
||||
.is_none_or(|(last, _)| last.as_str() < key.as_str())
|
||||
{
|
||||
self.entries.push((key, value));
|
||||
return None;
|
||||
}
|
||||
match self
|
||||
.entries
|
||||
.binary_search_by(|(name, _)| name.as_str().cmp(key.as_str()))
|
||||
{
|
||||
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_str().cmp(key))
|
||||
.ok()
|
||||
.map(|index| self.entries.remove(index).1)
|
||||
}
|
||||
|
||||
pub fn retain(&mut self, mut keep: impl FnMut(&CompactString, &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 = &CompactString> + 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, (CompactString, f64)>);
|
||||
impl<'a> Iterator for Iter<'a> {
|
||||
type Item = (&'a CompactString, &'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 CompactString, &'a f64);
|
||||
type IntoIter = Iter<'a>;
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.iter()
|
||||
}
|
||||
}
|
||||
impl IntoIterator for NumericFactorMap {
|
||||
type Item = (CompactString, 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 {
|
||||
iter.into_iter().map(|(key, value)| (compact_key(key), value)).collect()
|
||||
}
|
||||
}
|
||||
impl FromIterator<(CompactString, f64)> for NumericFactorMap {
|
||||
fn from_iter<T: IntoIterator<Item = (CompactString, 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) {
|
||||
self.extend(iter.into_iter().map(|(key, value)| (compact_key(key), value)));
|
||||
}
|
||||
}
|
||||
impl Extend<(CompactString, f64)> for NumericFactorMap {
|
||||
fn extend<T: IntoIterator<Item = (CompactString, 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 {
|
||||
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::<CompactString, f64>()? {
|
||||
entries.push((key, value));
|
||||
}
|
||||
Ok(entries.into_iter().collect())
|
||||
}
|
||||
}
|
||||
deserializer.deserialize_map(Fields)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn exact_reservation_preserves_values_and_avoids_growth_during_known_inserts() {
|
||||
let mut map = NumericFactorMap::from([
|
||||
(Cow::Borrowed("amount"), 125.25),
|
||||
(Cow::Borrowed("nullable_value"), f64::from_bits(0x7ff8000000000042)),
|
||||
(Cow::Borrowed("signal"), -0.0),
|
||||
]);
|
||||
let original = map.iter().map(|(key, value)| (key.to_string(), value.to_bits())).collect::<Vec<_>>();
|
||||
map.reserve_exact(2);
|
||||
assert_eq!(map.iter().map(|(key, value)| (key.to_string(), value.to_bits())).collect::<Vec<_>>(), original);
|
||||
let buffer = map.entries.as_ptr();
|
||||
map.insert(Cow::Borrowed("pre_close"), 12.5);
|
||||
map.insert(Cow::Borrowed("no_limit"), 0.0);
|
||||
assert_eq!(map.entries.as_ptr(), buffer);
|
||||
assert_eq!(map.len(), 5);
|
||||
assert_eq!(map["signal"].to_bits(), (-0.0_f64).to_bits());
|
||||
assert_eq!(map["nullable_value"].to_bits(), 0x7ff8000000000042);
|
||||
let before = map.entries.as_ptr();
|
||||
map.reserve_exact(0);
|
||||
assert_eq!(map.entries.as_ptr(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compact_keys_inline_dynamic_names_and_keep_long_static_storage() {
|
||||
const LONG: &str = "a_long_static_factor_identifier_that_must_remain_borrowed";
|
||||
let map = NumericFactorMap::from([
|
||||
(Cow::Owned("dynamic_factor_20".to_owned()), -0.0),
|
||||
(Cow::Borrowed(LONG), 1.0),
|
||||
]);
|
||||
let cloned = map.clone();
|
||||
let short = cloned.keys().find(|key| key.as_str() == "dynamic_factor_20").unwrap();
|
||||
assert!(!short.is_heap_allocated());
|
||||
let long = cloned.keys().find(|key| key.as_str() == LONG).unwrap();
|
||||
assert_eq!(long.as_static_str(), Some(LONG));
|
||||
assert_eq!(cloned["dynamic_factor_20"].to_bits(), (-0.0_f64).to_bits());
|
||||
assert_eq!(std::mem::size_of::<(CompactString, f64)>(), std::mem::size_of::<(Cow<'static, str>, f64)>());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_dynamic_unicode_and_short_keys_keep_the_same_json_map() {
|
||||
let entries = ["", "a", "a_field_longer_than_the_inline_string_capacity", "价格因子", "ths_up_days_stock"]
|
||||
.into_iter().enumerate().map(|(index, key)| (Cow::Owned(key.to_string()), index as f64 + 0.25))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
let map = NumericFactorMap::from(entries.clone());
|
||||
assert_eq!(serde_json::to_string(&map).unwrap(), serde_json::to_string(&entries).unwrap());
|
||||
let decoded: NumericFactorMap = serde_json::from_str(&serde_json::to_string(&map).unwrap()).unwrap();
|
||||
assert_eq!(decoded, map);
|
||||
assert!(!decoded.keys().find(|key| key.as_str() == "ths_up_days_stock").unwrap().is_heap_allocated());
|
||||
}
|
||||
|
||||
#[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().map(|(key, value)| (key.as_str(), value)).collect::<Vec<_>>(),
|
||||
tree.iter().map(|(key, value)| (key.as_ref(), value)).collect::<Vec<_>>()
|
||||
);
|
||||
flat.retain(|_, value| *value > 100.0);
|
||||
tree.retain(|_, value| *value > 100.0);
|
||||
assert_eq!(
|
||||
flat.iter().map(|(key, value)| (key.as_str(), value)).collect::<Vec<_>>(),
|
||||
tree.iter().map(|(key, value)| (key.as_ref(), value)).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().map(|(key, value)| (key.as_str(), value)).collect::<Vec<_>>(),
|
||||
tree.iter().map(|(key, value)| (key.as_ref(), value)).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().map(|(key, value)| (key.as_str(), value)).collect::<Vec<_>>(),
|
||||
tree.iter().map(|(key, value)| (key.as_ref(), value)).collect::<Vec<_>>()
|
||||
);
|
||||
assert_eq!(flat.keys().last().map(CompactString::as_str), Some("shared"));
|
||||
assert!(!flat.keys().last().unwrap().is_heap_allocated());
|
||||
flat.extend([(Cow::Borrowed("zz"), f64::NAN)]);
|
||||
assert!(flat["zz"].is_nan());
|
||||
flat.extend(std::iter::empty::<(CompactString, f64)>());
|
||||
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> {
|
||||
// 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();
|
||||
match value {
|
||||
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) => {
|
||||
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)?);
|
||||
}
|
||||
}
|
||||
@@ -248,6 +261,31 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{BenchmarkSnapshot, DailyFactorSnapshot, DailyMarketSnapshot, Instrument};
|
||||
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]
|
||||
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();
|
||||
@@ -298,7 +336,7 @@ mod tests {
|
||||
][n][i];
|
||||
market.push(DailyMarketSnapshot {
|
||||
date: *d,
|
||||
symbol: s.to_string(),
|
||||
symbol: (*s).into(),
|
||||
timestamp: None,
|
||||
day_open: c,
|
||||
open: c,
|
||||
@@ -321,7 +359,7 @@ mod tests {
|
||||
});
|
||||
factors.push(DailyFactorSnapshot {
|
||||
date: *d,
|
||||
symbol: s.to_string(),
|
||||
symbol: (*s).into(),
|
||||
market_cap_bn: 1.,
|
||||
free_float_cap_bn: 1.,
|
||||
pe_ttm: 10.,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,14 @@ impl PlatformExprStrategy {
|
||||
.as_ref()
|
||||
.ok_or_else(|| BacktestError::Execution("stock_pool_program_missing".into()))?
|
||||
.clone();
|
||||
if !self.config.stop_loss_expr.trim().is_empty() || !self.config.take_profit_expr.trim().is_empty()
|
||||
|| self.config.position_target_rules.len() != program.exit_signals.len()
|
||||
|| self.config.position_target_rules.iter().zip(&program.exit_signals).any(|(compiled, frozen)|
|
||||
compiled.when_expr != frozen.when_expr || compiled.remaining_position_bps != frozen.remaining_position_bps
|
||||
|| compiled.reason != frozen.reason || compiled.stock_pool_role != frozen.role)
|
||||
{
|
||||
return Err(BacktestError::Execution("stock_pool_exit_roles_required: exit rules must remain bound to the frozen stock_pool program".into()));
|
||||
}
|
||||
let mut constraints = pool::stock_pool_constraints_from_configuration(
|
||||
&program.allocation_policy,
|
||||
&program.stop_take_policy,
|
||||
@@ -78,17 +86,29 @@ impl PlatformExprStrategy {
|
||||
closes,
|
||||
});
|
||||
}
|
||||
let rule = pool::normalize_stock_pool_execution_rule(
|
||||
let rule = pool::normalize_stock_pool_execution_rule_with_exit_roles(
|
||||
Some(&program.timing_policy),
|
||||
!self.config.buy_filter_expr.trim().is_empty(),
|
||||
!self.config.stop_loss_expr.trim().is_empty()
|
||||
|| !self.config.take_profit_expr.trim().is_empty()
|
||||
|| !self.config.position_target_rules.is_empty(),
|
||||
self.config.position_target_rules.iter().any(|rule| rule.stock_pool_role == pool::StockPoolExitRole::OrdinarySell),
|
||||
self.config.position_target_rules.iter().any(|rule| rule.stock_pool_role == pool::StockPoolExitRole::RiskExit),
|
||||
)
|
||||
.map_err(BacktestError::Execution)?;
|
||||
if self.config.in_skip_window(ctx.decision_date) {
|
||||
return Ok(StrategyDecision::default());
|
||||
}
|
||||
let explicit_quote_condition = self.selection_quote_usage != StockFilterQuoteUsage::DailyOnly
|
||||
|| [self.config.buy_filter_expr.as_str(), self.config.stop_loss_expr.as_str(), self.config.take_profit_expr.as_str()]
|
||||
.into_iter().chain(self.config.position_target_rules.iter().map(|rule|rule.when_expr.as_str()))
|
||||
.any(|expression|Self::stock_filter_quote_usage_for_expr(expression)!=StockFilterQuoteUsage::DailyOnly);
|
||||
if explicit_quote_condition && ctx.active_datetime.is_some_and(|at|at.time()<NaiveTime::from_hms_opt(15,0,0).unwrap()) {
|
||||
for symbol in program.members.iter().map(|member|&member.symbol).chain(ctx.portfolio.positions().keys()) {
|
||||
if ctx.data.instrument(symbol).is_some_and(|instrument|instrument.is_exchange_traded_fund()&&instrument.dated_market_absence_reason(ctx.execution_date).is_none())
|
||||
&& self.scheduled_quote_at_time(ctx,ctx.execution_date,symbol,None).is_none()
|
||||
{
|
||||
return Err(BacktestError::Execution(format!("etf_intraday_condition_evidence_missing:{symbol}; completed daily references cannot make minute/tick conditions true")));
|
||||
}
|
||||
}
|
||||
}
|
||||
let day = self.day_state(ctx, ctx.decision_date)?;
|
||||
let (market_date, universe_date, factor_date) = self.selection_dates(ctx);
|
||||
let (low, high) = self.market_cap_band(ctx, &day)?;
|
||||
@@ -106,7 +126,7 @@ impl PlatformExprStrategy {
|
||||
.portfolio
|
||||
.positions()
|
||||
.values()
|
||||
.filter(|p| p.quantity > 0)
|
||||
.filter(|p| p.economic_quantity() > 0)
|
||||
.map(|p| p.symbol.clone())
|
||||
.collect::<BTreeSet<_>>();
|
||||
if !self.config.buy_filter_expr.trim().is_empty() {
|
||||
@@ -120,23 +140,13 @@ impl PlatformExprStrategy {
|
||||
}
|
||||
}
|
||||
}
|
||||
let native_exits = self.current_stop_take_exit_symbols(ctx, ctx.decision_date, &day)?;
|
||||
for symbol in native_exits {
|
||||
constraints.position_target_bps.insert(symbol, 0);
|
||||
}
|
||||
for (symbol, (bps, _)) in
|
||||
self.current_position_target_rules(ctx, ctx.decision_date, factor_date, &day)?
|
||||
{
|
||||
constraints
|
||||
.position_target_bps
|
||||
.entry(symbol)
|
||||
.and_modify(|old| *old = (*old).min(bps))
|
||||
.or_insert(bps);
|
||||
for (role, targets) in self.current_position_target_rules_by_role(ctx, ctx.decision_date, factor_date, &day)? {
|
||||
let output = match role { pool::StockPoolExitRole::OrdinarySell => &mut constraints.position_target_bps, pool::StockPoolExitRole::RiskExit => &mut constraints.independent_position_target_bps };
|
||||
for (symbol, (bps, _)) in targets { output.insert(symbol, bps); }
|
||||
}
|
||||
let limit = constraints.target_holding_count.unwrap_or(ranked.len());
|
||||
let final_symbols = ranked
|
||||
.iter()
|
||||
.filter(|symbol| !constraints.position_target_bps.contains_key(*symbol))
|
||||
.take(limit)
|
||||
.cloned()
|
||||
.collect();
|
||||
@@ -172,6 +182,15 @@ impl PlatformExprStrategy {
|
||||
scope.push(symbol)
|
||||
}
|
||||
}
|
||||
let allocation_weights = self
|
||||
.config
|
||||
.runtime_position_exposure_timeline
|
||||
.allocation_weights_at(portfolio_loss_decision_at(ctx))
|
||||
.or_else(|| {
|
||||
self.config
|
||||
.position_exposure_timeline
|
||||
.allocation_weights_at(portfolio_loss_decision_at(ctx))
|
||||
});
|
||||
let members = scope
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -188,18 +207,36 @@ impl PlatformExprStrategy {
|
||||
take_profit: constraints.default_take_profit,
|
||||
});
|
||||
member.requested_order = index as i32;
|
||||
if let Some(weights) = allocation_weights {
|
||||
member.target_weight_bps = Some(*weights.get(symbol).unwrap_or(&0));
|
||||
}
|
||||
member
|
||||
})
|
||||
.collect();
|
||||
let (base_ratio, reserve_cash) =
|
||||
pool::stock_pool_funding_from_configuration(&program.allocation_policy)
|
||||
.map_err(BacktestError::Execution)?;
|
||||
let base_exposure = self
|
||||
.config
|
||||
.position_exposure_timeline
|
||||
.exposure_at(
|
||||
portfolio_loss_decision_at(ctx),
|
||||
ctx.execution_date,
|
||||
&self.config.position_exposure_schedule,
|
||||
f64::from(base_ratio) / 10000.,
|
||||
)
|
||||
.unwrap_or(f64::from(base_ratio) / 10000.);
|
||||
let ratio = self
|
||||
.config
|
||||
.position_exposure_schedule
|
||||
.range(..=ctx.decision_date)
|
||||
.next_back()
|
||||
.map(|(_, value)| (*value * 10000.).round() as i64)
|
||||
.runtime_position_exposure_timeline
|
||||
.exposure_at(
|
||||
portfolio_loss_decision_at(ctx),
|
||||
ctx.execution_date,
|
||||
&self.config.runtime_position_exposure_schedule,
|
||||
base_exposure,
|
||||
)
|
||||
.or(Some(base_exposure))
|
||||
.map(|value| (value * 10000.).round() as i64)
|
||||
.unwrap_or(i64::from(base_ratio));
|
||||
let invest_ratio_bps = i32::try_from(ratio)
|
||||
.ok()
|
||||
|
||||
@@ -91,6 +91,8 @@ pub struct StrategyRebalanceSpec {
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StrategyExecutionSpec {
|
||||
#[serde(default, alias = "volume_capacity_mode")]
|
||||
pub volume_capacity_mode: Option<crate::execution_capacity::VolumeCapacityMode>,
|
||||
#[serde(default)]
|
||||
pub frequency: Option<String>,
|
||||
#[serde(default, alias = "matching_type")]
|
||||
@@ -164,9 +166,22 @@ pub struct StrategyExecutionSpec {
|
||||
pub sell_then_buy_delay_slippage_rate: Option<f64>,
|
||||
}
|
||||
|
||||
impl StrategyRuntimeSpec {
|
||||
pub fn volume_capacity_mode(&self) -> Result<crate::execution_capacity::VolumeCapacityMode, String> {
|
||||
let engine = self.engine_config.as_ref().and_then(|config| config.volume_capacity_mode);
|
||||
let execution = self.execution.as_ref().and_then(|config| config.volume_capacity_mode);
|
||||
if engine.zip(execution).is_some_and(|(a, b)| a != b) {
|
||||
return Err("conflicting engine/execution volumeCapacityMode".into());
|
||||
}
|
||||
Ok(execution.or(engine).unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct StrategyEngineConfig {
|
||||
#[serde(default, alias = "volume_capacity_mode")]
|
||||
pub volume_capacity_mode: Option<crate::execution_capacity::VolumeCapacityMode>,
|
||||
#[serde(default)]
|
||||
pub frequency: Option<String>,
|
||||
#[serde(default, alias = "template_id")]
|
||||
@@ -934,6 +949,8 @@ pub struct StrategyExpressionRiskConfig {
|
||||
pub exposure_expr: Option<String>,
|
||||
#[serde(default, alias = "position_exposure_schedule")]
|
||||
pub position_exposure_schedule: Vec<StrategyPositionExposureSchedulePoint>,
|
||||
#[serde(default, alias = "position_exposure_events")]
|
||||
pub position_exposure_events: Vec<crate::position_exposure::PositionExposureEvent>,
|
||||
#[serde(default)]
|
||||
pub portfolio_drawdown_control: Option<StrategyPortfolioDrawdownControlConfig>,
|
||||
#[serde(default)]
|
||||
@@ -1822,6 +1839,7 @@ pub fn platform_expr_config_from_spec(
|
||||
strategy_spec: Option<&StrategyRuntimeSpec>,
|
||||
) -> Result<PlatformExprStrategyConfig, String> {
|
||||
let mut cfg = PlatformExprStrategyConfig::generic();
|
||||
cfg.volume_capacity_mode = strategy_spec.map(StrategyRuntimeSpec::volume_capacity_mode).transpose()?.unwrap_or_default();
|
||||
cfg.strategy_name = strategy_id.to_string();
|
||||
if !signal_symbol.trim().is_empty() {
|
||||
cfg.signal_symbol = signal_symbol.trim().to_string();
|
||||
@@ -2212,6 +2230,7 @@ pub fn platform_expr_config_from_spec(
|
||||
expr.clone()
|
||||
};
|
||||
}
|
||||
cfg.position_exposure_timeline = crate::position_exposure::PositionExposureTimeline::from_events(&risk.position_exposure_events)?;
|
||||
for point in &risk.position_exposure_schedule {
|
||||
let effective_date = NaiveDate::parse_from_str(
|
||||
point.effective_date.trim(),
|
||||
@@ -2323,6 +2342,7 @@ pub fn platform_expr_config_from_spec(
|
||||
));
|
||||
}
|
||||
cfg.position_target_rules.push(PlatformPositionTargetRule {
|
||||
stock_pool_role: crate::stock_pool_execution::StockPoolExitRole::OrdinarySell,
|
||||
when_expr: when_expr.to_string(),
|
||||
remaining_position_bps: rule.remaining_position_bps,
|
||||
reason: rule
|
||||
@@ -2696,9 +2716,14 @@ pub fn platform_expr_config_from_spec(
|
||||
}
|
||||
if let Some(pool)=&spec.stock_pool {
|
||||
if cfg.signal_book.is_some() || spec.signal_book_ref.is_some() || !cfg.explicit_actions.is_empty(){return Err("stock_pool_program_cannot_mix_other_order_programs".into())}
|
||||
let legacy_exit = !cfg.stop_loss_expr.trim().is_empty() || !cfg.take_profit_expr.trim().is_empty() || !cfg.position_target_rules.is_empty();
|
||||
if legacy_exit { return Err("stock_pool_exit_roles_required: regenerate this historical stock-pool strategy from its saved configuration; legacy risk expressions do not preserve ordinary/risk exit roles".into()); }
|
||||
let secondary_buy=!cfg.buy_filter_expr.trim().is_empty();
|
||||
let secondary_sell=spec.runtime_expressions.as_ref().and_then(|runtime|runtime.risk.as_ref()).is_some_and(|risk|risk.stop_loss_expr.is_some()||risk.take_profit_expr.is_some()) || !cfg.position_target_rules.is_empty();
|
||||
pool.validate(secondary_buy,secondary_sell)?;
|
||||
pool.validate(secondary_buy,false)?;
|
||||
cfg.position_target_rules.extend(pool.exit_signals.iter().map(|signal| PlatformPositionTargetRule {
|
||||
when_expr: signal.when_expr.clone(), remaining_position_bps: signal.remaining_position_bps,
|
||||
reason: signal.reason.clone(), stock_pool_role: signal.role,
|
||||
}));
|
||||
cfg.stock_pool=Some(pool.clone());
|
||||
cfg.hold_until_exit_enabled=false;
|
||||
cfg.daily_top_up_enabled=false;
|
||||
@@ -3440,6 +3465,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
cfg.position_target_rules,
|
||||
vec![PlatformPositionTargetRule {
|
||||
stock_pool_role: crate::stock_pool_execution::StockPoolExitRole::OrdinarySell,
|
||||
when_expr: "factors[\"reduce_signal\"] == 1".to_string(),
|
||||
remaining_position_bps: 5000,
|
||||
reason: "factor_reduce_position".to_string(),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,536 @@
|
||||
//! Dated manual adjustments are ordered facts; restoring is not a 100% target.
|
||||
use chrono::{DateTime, NaiveDate, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(tag = "action", rename_all = "snake_case", deny_unknown_fields)]
|
||||
pub enum PositionExposureAction {
|
||||
Scale {
|
||||
#[serde(rename = "requestedBps", alias = "requested_bps")]
|
||||
requested_bps: i32,
|
||||
},
|
||||
Set {
|
||||
#[serde(rename = "targetExposureBps", alias = "target_exposure_bps")]
|
||||
target_exposure_bps: i32,
|
||||
},
|
||||
Restore,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PositionExposureEvent {
|
||||
#[serde(alias = "event_id")]
|
||||
pub event_id: String,
|
||||
pub sequence: u64,
|
||||
#[serde(alias = "effective_at")]
|
||||
pub effective_at: DateTime<Utc>,
|
||||
#[serde(
|
||||
default,
|
||||
skip_serializing_if = "Option::is_none",
|
||||
alias = "allocation_weights_bps"
|
||||
)]
|
||||
pub allocation_weights_bps: Option<BTreeMap<String, i32>>,
|
||||
#[serde(flatten)]
|
||||
pub action: PositionExposureAction,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PositionExposureTimeline {
|
||||
events: BTreeMap<(DateTime<Utc>, u64), (PositionExposureAction, Option<BTreeMap<String, i32>>)>,
|
||||
}
|
||||
|
||||
impl PositionExposureTimeline {
|
||||
pub fn from_events(events: &[PositionExposureEvent]) -> Result<Self, String> {
|
||||
let mut result = Self::default();
|
||||
let mut ids = BTreeSet::new();
|
||||
let mut sequences = BTreeSet::new();
|
||||
for event in events {
|
||||
if event.event_id.trim().is_empty() || !ids.insert(event.event_id.as_str()) {
|
||||
return Err("position exposure event id is missing or duplicated".into());
|
||||
}
|
||||
if event.sequence == 0 || !sequences.insert(event.sequence) {
|
||||
return Err("position exposure event sequence must be positive and unique".into());
|
||||
}
|
||||
if let PositionExposureAction::Scale { requested_bps } = event.action
|
||||
&& !(0..=10000).contains(&requested_bps)
|
||||
{
|
||||
return Err("position exposure scale must be between 0 and 10000 bps".into());
|
||||
}
|
||||
if let PositionExposureAction::Set {
|
||||
target_exposure_bps,
|
||||
} = event.action
|
||||
&& !(0..=10_000).contains(&target_exposure_bps)
|
||||
{
|
||||
return Err("position exposure target must be between 0 and 10000 bps".into());
|
||||
}
|
||||
if let Some(weights) = &event.allocation_weights_bps {
|
||||
let target = match event.action {
|
||||
PositionExposureAction::Set {
|
||||
target_exposure_bps,
|
||||
} => target_exposure_bps,
|
||||
PositionExposureAction::Scale { requested_bps } => requested_bps,
|
||||
PositionExposureAction::Restore => {
|
||||
return Err(
|
||||
"restoring strategy allocation cannot carry manual weights".into()
|
||||
);
|
||||
}
|
||||
};
|
||||
validate_allocation_weights(weights, target)?;
|
||||
}
|
||||
result.events.insert(
|
||||
(event.effective_at, event.sequence),
|
||||
(event.action.clone(), event.allocation_weights_bps.clone()),
|
||||
);
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Legacy day-level contracts remain day-level; never invent intraday times.
|
||||
pub fn exposure_at(
|
||||
&self,
|
||||
at: DateTime<Utc>,
|
||||
execution_date: NaiveDate,
|
||||
legacy: &BTreeMap<NaiveDate, f64>,
|
||||
strategy_exposure: f64,
|
||||
) -> Option<f64> {
|
||||
match self
|
||||
.events
|
||||
.range(..=(at, u64::MAX))
|
||||
.next_back()
|
||||
.map(|(_, (action, _))| action)
|
||||
{
|
||||
Some(PositionExposureAction::Scale { requested_bps }) => {
|
||||
Some(strategy_exposure * f64::from(*requested_bps) / 10000.)
|
||||
}
|
||||
Some(PositionExposureAction::Set {
|
||||
target_exposure_bps,
|
||||
}) => Some(f64::from(*target_exposure_bps) / 10_000.),
|
||||
Some(PositionExposureAction::Restore) => None,
|
||||
None => legacy
|
||||
.range(..=execution_date)
|
||||
.next_back()
|
||||
.map(|(_, value)| *value),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scale_at(&self, at: DateTime<Utc>) -> Option<i32> {
|
||||
match self
|
||||
.events
|
||||
.range(..=(at, u64::MAX))
|
||||
.next_back()
|
||||
.map(|(_, (action, _))| action)
|
||||
{
|
||||
Some(PositionExposureAction::Scale { requested_bps }) => Some(*requested_bps),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn allocation_weights_at(&self, at: DateTime<Utc>) -> Option<&BTreeMap<String, i32>> {
|
||||
self.events
|
||||
.range(..=(at, u64::MAX))
|
||||
.next_back()
|
||||
.and_then(|(_, (_, weights))| weights.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_allocation_weights(
|
||||
weights: &BTreeMap<String, i32>,
|
||||
exposure_bps: i32,
|
||||
) -> Result<(), String> {
|
||||
if !(0..=10000).contains(&exposure_bps) || weights.len() > 10000 {
|
||||
return Err("invalid allocation exposure or weight count".into());
|
||||
}
|
||||
for (symbol, weight) in weights {
|
||||
if !(0..=10000).contains(weight)
|
||||
|| !symbol.rsplit_once('.').is_some_and(|(code, exchange)| {
|
||||
code.len() == 6
|
||||
&& code.bytes().all(|byte| byte.is_ascii_digit())
|
||||
&& matches!(exchange, "SH" | "SZ" | "BJ")
|
||||
})
|
||||
{
|
||||
return Err(
|
||||
"allocation weights require canonical stock/ETF symbols and 0..10000 bps".into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (weights.is_empty() && exposure_bps != 0)
|
||||
|| (!weights.is_empty() && weights.values().sum::<i32>() != 10000)
|
||||
{
|
||||
return Err("manual allocation weights must total 10000 bps; only a zero exposure may have no weights".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Scale new buys and desired targets without weakening sell/reduction or
|
||||
/// cancellation instructions. Prices, subscriptions and cash flows are intact.
|
||||
pub fn scale_explicit_intent(
|
||||
mut intent: crate::OrderIntent,
|
||||
bps: i32,
|
||||
open_orders: &[crate::OpenOrderView],
|
||||
) -> Result<Option<crate::OrderIntent>, String> {
|
||||
use crate::OrderIntent as I;
|
||||
if !(0..=10000).contains(&bps) {
|
||||
return Err("position scale out of range".into());
|
||||
}
|
||||
if bps == 10000 {
|
||||
return Ok(Some(intent));
|
||||
}
|
||||
if let I::WithTimeInForce {
|
||||
intent: inner,
|
||||
time_in_force,
|
||||
} = intent
|
||||
{
|
||||
return Ok(
|
||||
scale_explicit_intent(*inner, bps, open_orders)?.map(|intent| I::WithTimeInForce {
|
||||
intent: Box::new(intent),
|
||||
time_in_force,
|
||||
}),
|
||||
);
|
||||
}
|
||||
let integer = |value: i32| ((i64::from(value) * i64::from(bps)) / 10000) as i32;
|
||||
let amount = |value: f64, target: bool| -> Result<f64, String> {
|
||||
if !value.is_finite() || (target && value < 0.) {
|
||||
return Err("position override received an invalid original amount".into());
|
||||
}
|
||||
Ok(if value > 0. {
|
||||
value * f64::from(bps) / 10000.
|
||||
} else {
|
||||
value
|
||||
})
|
||||
};
|
||||
match &mut intent {
|
||||
I::Shares { quantity, .. }
|
||||
| I::LimitShares { quantity, .. }
|
||||
| I::Lots { lots: quantity, .. }
|
||||
| I::LimitLots { lots: quantity, .. } => {
|
||||
if *quantity > 0 {
|
||||
*quantity = integer(*quantity);
|
||||
if *quantity == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
I::TargetShares {
|
||||
target_quantity, ..
|
||||
}
|
||||
| I::LimitTargetShares {
|
||||
target_quantity, ..
|
||||
} => {
|
||||
if *target_quantity < 0 {
|
||||
return Err("position override received a negative target quantity".into());
|
||||
}
|
||||
*target_quantity = integer(*target_quantity);
|
||||
}
|
||||
I::Value { value, .. }
|
||||
| I::LimitValue { value, .. }
|
||||
| I::AlgoValue { value, .. }
|
||||
| I::Percent { percent: value, .. }
|
||||
| I::LimitPercent { percent: value, .. }
|
||||
| I::AlgoPercent { percent: value, .. } => {
|
||||
*value = amount(*value, false)?;
|
||||
if *value == 0. {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
I::TargetValue { target_value, .. }
|
||||
| I::LimitTargetValue { target_value, .. }
|
||||
| I::TimedTargetValue { target_value, .. }
|
||||
| I::TargetPercent {
|
||||
target_percent: target_value,
|
||||
..
|
||||
}
|
||||
| I::LimitTargetPercent {
|
||||
target_percent: target_value,
|
||||
..
|
||||
} => {
|
||||
*target_value = amount(*target_value, true)?;
|
||||
}
|
||||
I::TargetPortfolioSmart { target_weights, .. } => {
|
||||
for value in target_weights.values_mut() {
|
||||
*value = amount(*value, true)?;
|
||||
}
|
||||
}
|
||||
I::ModifyOrder {
|
||||
order_id,
|
||||
new_total_quantity: Some(quantity),
|
||||
..
|
||||
} => {
|
||||
let order = open_orders
|
||||
.iter()
|
||||
.find(|order| order.order_id == *order_id)
|
||||
.ok_or("position override cannot resolve the order being modified")?;
|
||||
if order.side == crate::OrderSide::Buy && *quantity > order.requested_quantity {
|
||||
let extra = u64::from(*quantity - order.requested_quantity) * bps as u64 / 10000;
|
||||
*quantity = order.requested_quantity + extra as u32;
|
||||
}
|
||||
}
|
||||
I::Futures { .. } | I::StockPool { .. } => {
|
||||
return Err("manual equity scaling cannot transform this intent kind".into());
|
||||
}
|
||||
I::ModifyOrder { .. }
|
||||
| I::CancelOrder { .. }
|
||||
| I::CancelSymbol { .. }
|
||||
| I::CancelAll { .. }
|
||||
| I::UpdateUniverse { .. }
|
||||
| I::Subscribe { .. }
|
||||
| I::Unsubscribe { .. }
|
||||
| I::DepositWithdraw { .. }
|
||||
| I::FinanceRepay { .. }
|
||||
| I::SetManagementFeeRate { .. } => {}
|
||||
I::WithTimeInForce { .. } => unreachable!("wrapper handled first"),
|
||||
}
|
||||
Ok(Some(intent))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn scalar_preserves_strategy_risk_off_and_restore_keeps_original_exposure() {
|
||||
let at = DateTime::parse_from_rfc3339("2026-01-05T09:30:00+08:00")
|
||||
.unwrap()
|
||||
.with_timezone(&Utc);
|
||||
let event = PositionExposureEvent {
|
||||
event_id: "scale".into(),
|
||||
sequence: 1,
|
||||
effective_at: at,
|
||||
allocation_weights_bps: None,
|
||||
action: PositionExposureAction::Scale {
|
||||
requested_bps: 5000,
|
||||
},
|
||||
};
|
||||
let timeline = PositionExposureTimeline::from_events(&[event.clone()]).unwrap();
|
||||
assert_eq!(
|
||||
timeline.exposure_at(at, at.date_naive(), &BTreeMap::new(), 0.),
|
||||
Some(0.)
|
||||
);
|
||||
assert_eq!(
|
||||
timeline.exposure_at(at, at.date_naive(), &BTreeMap::new(), 0.2),
|
||||
Some(0.1)
|
||||
);
|
||||
let restored = PositionExposureEvent {
|
||||
event_id: "restore".into(),
|
||||
sequence: 2,
|
||||
effective_at: at,
|
||||
allocation_weights_bps: None,
|
||||
action: PositionExposureAction::Restore,
|
||||
};
|
||||
let timeline = PositionExposureTimeline::from_events(&[event, restored]).unwrap();
|
||||
assert_eq!(
|
||||
timeline
|
||||
.exposure_at(
|
||||
at,
|
||||
at.date_naive(),
|
||||
&BTreeMap::from([(at.date_naive(), 1.)]),
|
||||
0.2
|
||||
)
|
||||
.unwrap_or(0.2),
|
||||
0.2
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allocation_is_dated_and_any_later_scalar_or_restore_clears_it() {
|
||||
let at = DateTime::parse_from_rfc3339("2026-09-14T10:00:00+08:00")
|
||||
.unwrap()
|
||||
.with_timezone(&Utc);
|
||||
let weights = BTreeMap::from([("000001.SZ".into(), 3000), ("510300.SH".into(), 7000)]);
|
||||
let event = PositionExposureEvent {
|
||||
event_id: "allocation".into(),
|
||||
sequence: 1,
|
||||
effective_at: at,
|
||||
action: PositionExposureAction::Set {
|
||||
target_exposure_bps: 8000,
|
||||
},
|
||||
allocation_weights_bps: Some(weights.clone()),
|
||||
};
|
||||
let timeline = PositionExposureTimeline::from_events(&[event.clone()]).unwrap();
|
||||
assert!(
|
||||
timeline
|
||||
.allocation_weights_at(at - chrono::Duration::seconds(1))
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(timeline.allocation_weights_at(at), Some(&weights));
|
||||
for action in [
|
||||
PositionExposureAction::Set {
|
||||
target_exposure_bps: 5000,
|
||||
},
|
||||
PositionExposureAction::Restore,
|
||||
] {
|
||||
let next = PositionExposureEvent {
|
||||
event_id: "new".into(),
|
||||
sequence: 2,
|
||||
effective_at: at + chrono::Duration::seconds(1),
|
||||
action,
|
||||
allocation_weights_bps: None,
|
||||
};
|
||||
assert!(
|
||||
PositionExposureTimeline::from_events(&[event.clone(), next])
|
||||
.unwrap()
|
||||
.allocation_weights_at(at + chrono::Duration::seconds(1))
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
validate_allocation_weights(&BTreeMap::from([("000001.SZ".into(), 9000)]), 5000)
|
||||
.is_err()
|
||||
);
|
||||
assert!(validate_allocation_weights(&BTreeMap::new(), 1).is_err());
|
||||
assert!(validate_allocation_weights(&BTreeMap::new(), 0).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_equity_targets_and_buys_scale_but_sales_cashflows_and_prices_do_not() {
|
||||
use crate::OrderIntent as I;
|
||||
let symbol = "000001.SZ".to_string();
|
||||
let reason = "fixture".to_string();
|
||||
for bps in [0, 3000, 5000, 10000] {
|
||||
let ratio = f64::from(bps) / 10000.;
|
||||
let buy = I::LimitShares {
|
||||
symbol: symbol.clone(),
|
||||
quantity: 1000,
|
||||
limit_price: 12.345,
|
||||
reason: reason.clone(),
|
||||
};
|
||||
let scaled = scale_explicit_intent(buy, bps, &[]).unwrap();
|
||||
if bps == 0 {
|
||||
assert!(scaled.is_none())
|
||||
} else if let Some(I::LimitShares {
|
||||
quantity,
|
||||
limit_price,
|
||||
..
|
||||
}) = scaled
|
||||
{
|
||||
assert_eq!(quantity, (1000. * ratio) as i32);
|
||||
assert_eq!(limit_price, 12.345);
|
||||
} else {
|
||||
panic!("wrong intent")
|
||||
}
|
||||
let sell = I::Shares {
|
||||
symbol: symbol.clone(),
|
||||
quantity: -1000,
|
||||
reason: reason.clone(),
|
||||
};
|
||||
assert!(matches!(
|
||||
scale_explicit_intent(sell, bps, &[]).unwrap(),
|
||||
Some(I::Shares {
|
||||
quantity: -1000,
|
||||
..
|
||||
})
|
||||
));
|
||||
let clear = I::TargetShares {
|
||||
symbol: symbol.clone(),
|
||||
target_quantity: 0,
|
||||
reason: reason.clone(),
|
||||
};
|
||||
assert!(matches!(
|
||||
scale_explicit_intent(clear, bps, &[]).unwrap(),
|
||||
Some(I::TargetShares {
|
||||
target_quantity: 0,
|
||||
..
|
||||
})
|
||||
));
|
||||
let target = I::TargetPercent {
|
||||
symbol: symbol.clone(),
|
||||
target_percent: 0.2,
|
||||
reason: reason.clone(),
|
||||
};
|
||||
if let Some(I::TargetPercent { target_percent, .. }) =
|
||||
scale_explicit_intent(target, bps, &[]).unwrap()
|
||||
{
|
||||
assert!((target_percent - 0.2 * ratio).abs() < 1e-12)
|
||||
} else {
|
||||
panic!("wrong target")
|
||||
}
|
||||
let deposit = I::DepositWithdraw {
|
||||
amount: 123.456,
|
||||
receiving_days: 2,
|
||||
reason: reason.clone(),
|
||||
};
|
||||
assert!(matches!(
|
||||
scale_explicit_intent(deposit, bps, &[]).unwrap(),
|
||||
Some(I::DepositWithdraw {
|
||||
amount: 123.456,
|
||||
receiving_days: 2,
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
assert!(
|
||||
scale_explicit_intent(
|
||||
I::TargetValue {
|
||||
symbol,
|
||||
target_value: f64::NAN,
|
||||
reason
|
||||
},
|
||||
0,
|
||||
&[]
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_day_adjustments_restore_and_future_events_keep_their_own_times() {
|
||||
let events: Vec<PositionExposureEvent> = serde_json::from_value(json!([
|
||||
{"eventId":"first","sequence":1,"effectiveAt":"2026-09-10T10:00:00+08:00","action":"set","targetExposureBps":0},
|
||||
{"eventId":"second","sequence":2,"effectiveAt":"2026-09-10T13:00:00+08:00","action":"set","targetExposureBps":5000},
|
||||
{"eventId":"restore","sequence":3,"effectiveAt":"2026-09-10T14:00:00+08:00","action":"restore"},
|
||||
{"eventId":"future","sequence":4,"effectiveAt":"2026-09-11T10:00:00+08:00","action":"set","targetExposureBps":1000}
|
||||
])).unwrap();
|
||||
let timeline = PositionExposureTimeline::from_events(&events).unwrap();
|
||||
let date = NaiveDate::from_ymd_opt(2026, 9, 10).unwrap();
|
||||
let legacy = BTreeMap::from([(date.pred_opt().unwrap(), 0.8)]);
|
||||
for (time, expected) in [
|
||||
("09:30:00", Some(0.8)),
|
||||
("10:00:00", Some(0.)),
|
||||
("12:59:59", Some(0.)),
|
||||
("13:00:00", Some(0.5)),
|
||||
("14:00:00", None),
|
||||
("15:00:00", None),
|
||||
] {
|
||||
let at = DateTime::parse_from_rfc3339(&format!("2026-09-10T{time}+08:00"))
|
||||
.unwrap()
|
||||
.with_timezone(&Utc);
|
||||
assert_eq!(
|
||||
timeline.exposure_at(at, date, &legacy, 0.2),
|
||||
expected,
|
||||
"{time}"
|
||||
);
|
||||
}
|
||||
let next_open = DateTime::parse_from_rfc3339("2026-09-11T09:30:00+08:00")
|
||||
.unwrap()
|
||||
.with_timezone(&Utc);
|
||||
assert_eq!(
|
||||
timeline.exposure_at(next_open, date.succ_opt().unwrap(), &legacy, 0.2),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_actions_duplicate_identity_and_invalid_bps() {
|
||||
let valid = json!({"eventId":"one","sequence":1,"effectiveAt":"2026-09-10T09:30:00+08:00","action":"set","targetExposureBps":5000});
|
||||
for (key, value) in [
|
||||
("action", json!("other")),
|
||||
("effectiveAt", json!("2026-09-10 09:30:00")),
|
||||
("targetExposureBps", json!(null)),
|
||||
] {
|
||||
let mut invalid = valid.clone();
|
||||
invalid[key] = value;
|
||||
assert!(serde_json::from_value::<PositionExposureEvent>(invalid).is_err());
|
||||
}
|
||||
let event: PositionExposureEvent = serde_json::from_value(valid).unwrap();
|
||||
assert!(PositionExposureTimeline::from_events(&[event.clone(), event.clone()]).is_err());
|
||||
let mut invalid = event.clone();
|
||||
invalid.action = PositionExposureAction::Set {
|
||||
target_exposure_bps: 10001,
|
||||
};
|
||||
assert!(PositionExposureTimeline::from_events(&[invalid]).is_err());
|
||||
let mut duplicate = event.clone();
|
||||
duplicate.event_id = "two".into();
|
||||
assert!(PositionExposureTimeline::from_events(&[event, duplicate]).is_err());
|
||||
}
|
||||
}
|
||||
@@ -76,6 +76,26 @@ impl Default for StaticRiskRuleConfig {
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticRiskRuleConfig {
|
||||
pub(crate) fn selection_checks_enabled(&self) -> bool {
|
||||
(self.blacklist_enabled && !self.blacklisted_symbols.is_empty())
|
||||
|| self.selection_state_checks_enabled()
|
||||
}
|
||||
|
||||
fn selection_state_checks_enabled(&self) -> bool {
|
||||
self.reject_st_selection
|
||||
|| self.reject_star_st_selection
|
||||
|| self.reject_paused_selection
|
||||
|| self.reject_inactive_selection
|
||||
|| self.reject_new_listing_selection
|
||||
|| self.reject_kcb_selection
|
||||
|| self.reject_bjse_selection
|
||||
|| self.reject_one_yuan_selection
|
||||
|| self.reject_upper_limit_selection
|
||||
|| self.reject_lower_limit_selection
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct TradingConstraintConfig {
|
||||
/// Shared execution limits. These fields intentionally use the same
|
||||
@@ -654,16 +674,7 @@ fn missing_risk_state_fields(code: &str) -> Vec<String> {
|
||||
fn missing_selection_risk_state_rejected(code: &str, config: &FidcRiskControlConfig) -> bool {
|
||||
let fields = missing_risk_state_fields(code);
|
||||
if fields.is_empty() {
|
||||
return config.static_rules.reject_st_selection
|
||||
|| config.static_rules.reject_star_st_selection
|
||||
|| config.static_rules.reject_paused_selection
|
||||
|| config.static_rules.reject_inactive_selection
|
||||
|| config.static_rules.reject_new_listing_selection
|
||||
|| config.static_rules.reject_kcb_selection
|
||||
|| config.static_rules.reject_bjse_selection
|
||||
|| config.static_rules.reject_one_yuan_selection
|
||||
|| config.static_rules.reject_upper_limit_selection
|
||||
|| config.static_rules.reject_lower_limit_selection;
|
||||
return config.static_rules.selection_state_checks_enabled();
|
||||
}
|
||||
missing_field_rejected(&fields, config, RiskCheckScope::Selection)
|
||||
}
|
||||
@@ -778,18 +789,7 @@ fn missing_single_field_rejected(
|
||||
RiskCheckScope::Sell => config.static_rules.reject_lower_limit_sell,
|
||||
},
|
||||
_ => match scope {
|
||||
RiskCheckScope::Selection => {
|
||||
config.static_rules.reject_st_selection
|
||||
|| config.static_rules.reject_star_st_selection
|
||||
|| config.static_rules.reject_paused_selection
|
||||
|| config.static_rules.reject_inactive_selection
|
||||
|| config.static_rules.reject_new_listing_selection
|
||||
|| config.static_rules.reject_kcb_selection
|
||||
|| config.static_rules.reject_bjse_selection
|
||||
|| config.static_rules.reject_one_yuan_selection
|
||||
|| config.static_rules.reject_upper_limit_selection
|
||||
|| config.static_rules.reject_lower_limit_selection
|
||||
}
|
||||
RiskCheckScope::Selection => config.static_rules.selection_state_checks_enabled(),
|
||||
RiskCheckScope::Buy => {
|
||||
config.static_rules.reject_st_buy
|
||||
|| config.static_rules.reject_star_st_buy
|
||||
@@ -869,7 +869,7 @@ mod tests {
|
||||
fn candidate(date: NaiveDate) -> CandidateEligibility {
|
||||
CandidateEligibility {
|
||||
date,
|
||||
symbol: "002633.SZ".to_string(),
|
||||
symbol: "002633.SZ".into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
@@ -885,7 +885,7 @@ mod tests {
|
||||
fn market(date: NaiveDate, last_price: f64, lower_limit: f64) -> DailyMarketSnapshot {
|
||||
DailyMarketSnapshot {
|
||||
date,
|
||||
symbol: "002633.SZ".to_string(),
|
||||
symbol: "002633.SZ".into(),
|
||||
timestamp: Some(format!("{date} 10:18:00")),
|
||||
day_open: last_price,
|
||||
open: last_price,
|
||||
@@ -914,6 +914,69 @@ mod tests {
|
||||
position
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_check_activation_covers_every_configured_flag_and_blacklist_state() {
|
||||
let fields = [
|
||||
"reject_st_selection", "reject_star_st_selection", "reject_paused_selection",
|
||||
"reject_inactive_selection", "reject_new_listing_selection", "reject_kcb_selection",
|
||||
"reject_bjse_selection", "reject_one_yuan_selection", "reject_upper_limit_selection",
|
||||
"reject_lower_limit_selection",
|
||||
];
|
||||
let base = serde_json::to_value(StaticRiskRuleConfig::default()).unwrap();
|
||||
let declared = base.as_object().unwrap().keys()
|
||||
.filter(|key| key.ends_with("_selection"))
|
||||
.map(String::as_str).collect::<BTreeSet<_>>();
|
||||
assert_eq!(declared, fields.into_iter().collect());
|
||||
for mask in 0..(1_u32 << fields.len()) {
|
||||
for (blacklist_enabled, populated) in [(false, false), (false, true), (true, false), (true, true)] {
|
||||
let mut value = base.clone();
|
||||
for (bit, field) in fields.iter().enumerate() {
|
||||
value[*field] = serde_json::json!(mask & (1 << bit) != 0);
|
||||
}
|
||||
value["blacklist_enabled"] = serde_json::json!(blacklist_enabled);
|
||||
value["blacklisted_symbols"] = if populated {
|
||||
serde_json::json!(["002633.SZ"])
|
||||
} else { serde_json::json!([]) };
|
||||
let config: StaticRiskRuleConfig = serde_json::from_value(value).unwrap();
|
||||
assert_eq!(config.selection_checks_enabled(), mask != 0 || (blacklist_enabled && populated));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inactive_selection_checks_preserve_missing_facts_and_execution_rejections() {
|
||||
let date = d(2025, 2, 6);
|
||||
let mut candidate = candidate(date);
|
||||
candidate.is_st = true;
|
||||
candidate.is_star_st = true;
|
||||
candidate.is_paused = true;
|
||||
candidate.is_new_listing = true;
|
||||
candidate.is_kcb = true;
|
||||
candidate.is_one_yuan = true;
|
||||
candidate.allow_buy = false;
|
||||
let snapshot = market(date, 0.9, 0.9);
|
||||
let config = FidcRiskControlConfig::default();
|
||||
assert!(!config.static_rules.selection_checks_enabled());
|
||||
let instrument = instrument("delisted", Some(date));
|
||||
for code in [None, Some("not_listed"), Some("inactive_or_delisted"),
|
||||
Some("missing_risk_state"), Some("missing_risk_state:is_st;is_kcb|allow_buy"),
|
||||
Some("missing_risk_state:unknown_fact"), Some("missing_risk_state:IS_PAUSED")] {
|
||||
candidate.risk_level_code = code.map(str::to_owned);
|
||||
assert_eq!(ChinaAShareRiskControl::selection_rejection_decision_with_config(
|
||||
date, &candidate, &snapshot, Some(&instrument), &config), None);
|
||||
}
|
||||
candidate.risk_level_code = None;
|
||||
assert_eq!(ChinaAShareRiskControl::buy_rejection_reason_with_config(
|
||||
date, &candidate, &snapshot, None, 0.9, &config), Some("paused"));
|
||||
assert_eq!(ChinaAShareRiskControl::sell_rejection_reason_with_config(
|
||||
date, &candidate, &snapshot, None, None, 0.9, &config), Some("paused"));
|
||||
let mut blacklist_only = config;
|
||||
blacklist_only.static_rules.blacklisted_symbols.insert(candidate.symbol.to_string());
|
||||
assert!(blacklist_only.static_rules.selection_checks_enabled());
|
||||
assert_eq!(ChinaAShareRiskControl::selection_rejection_reason_with_config(
|
||||
date, &candidate, &snapshot, None, &blacklist_only), Some("blacklisted"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_yuan_buy_rule_uses_execution_price_not_later_close_or_earlier_open() {
|
||||
let day = d(2025, 2, 6);
|
||||
@@ -944,7 +1007,7 @@ mod tests {
|
||||
let mut candidate=candidate(day);
|
||||
let mut snapshot=market(day,1.2,0.5);
|
||||
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();
|
||||
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"));
|
||||
@@ -1047,7 +1110,7 @@ mod tests {
|
||||
config
|
||||
.static_rules
|
||||
.blacklisted_symbols
|
||||
.insert(candidate.symbol.clone());
|
||||
.insert(candidate.symbol.to_string());
|
||||
|
||||
let selection_reason = ChinaAShareRiskControl::selection_rejection_reason_with_config(
|
||||
date, &candidate, &market, None, &config,
|
||||
@@ -1092,7 +1155,7 @@ mod tests {
|
||||
let mut candidate = candidate(date);
|
||||
let config = FidcRiskControlConfig::default();
|
||||
for symbol in ["688001.SH", "689001.SH", "000001.SZ"] {
|
||||
candidate.symbol = symbol.to_string();
|
||||
candidate.symbol = symbol.into();
|
||||
for is_kcb in [false, true] {
|
||||
candidate.is_kcb = is_kcb;
|
||||
let reason = ChinaAShareRiskControl::buy_rejection_reason_with_config(
|
||||
@@ -1200,7 +1263,7 @@ mod tests {
|
||||
fn configurable_bjse_filter_can_be_disabled() {
|
||||
let date = d(2025, 1, 2);
|
||||
let mut candidate = candidate(date);
|
||||
candidate.symbol = "430047.BJ".to_string();
|
||||
candidate.symbol = "430047.BJ".into();
|
||||
candidate.allow_sell = true;
|
||||
let market = market(date, 6.27, 5.63);
|
||||
let default_selection =
|
||||
@@ -1232,7 +1295,7 @@ mod tests {
|
||||
fn concrete_kcb_reason_wins_over_generic_missing_risk_state() {
|
||||
let date = d(2025, 1, 2);
|
||||
let mut candidate = candidate(date);
|
||||
candidate.symbol = "688506.SH".to_string();
|
||||
candidate.symbol = "688506.SH".into();
|
||||
candidate.is_kcb = true;
|
||||
candidate.risk_level_code = Some("missing_risk_state".to_string());
|
||||
let market = market(date, 6.27, 5.63);
|
||||
|
||||
@@ -233,7 +233,7 @@ impl<'a> Scheduler<'a> {
|
||||
pub fn default_stage_time(stage: ScheduleStage) -> Option<NaiveTime> {
|
||||
match stage {
|
||||
ScheduleStage::BeforeTrading => Some(NaiveTime::from_hms_opt(9, 0, 0).expect("valid time")),
|
||||
ScheduleStage::OpenAuction => Some(NaiveTime::from_hms_opt(9, 31, 0).expect("valid time")),
|
||||
ScheduleStage::OpenAuction => Some(NaiveTime::from_hms_opt(9, 25, 0).expect("valid time")),
|
||||
ScheduleStage::Bar => Some(NaiveTime::from_hms_opt(10, 18, 0).expect("valid time")),
|
||||
ScheduleStage::Minute => None,
|
||||
ScheduleStage::OnDay => Some(NaiveTime::from_hms_opt(10, 18, 0).expect("valid time")),
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
use std::ops::Index;
|
||||
|
||||
use super::prefix_sums;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) enum ReferenceMatchedValues {
|
||||
Identical,
|
||||
Owned(Vec<f64>),
|
||||
}
|
||||
|
||||
impl ReferenceMatchedValues {
|
||||
pub(super) fn push(&mut self, value: f64, reference: &[f64], capacity: usize) {
|
||||
let previous_len = reference.len().checked_sub(1).expect("reference row is missing");
|
||||
match self {
|
||||
Self::Identical if value.to_bits() == reference[previous_len].to_bits() => {}
|
||||
Self::Identical => {
|
||||
let mut values = Vec::with_capacity(capacity);
|
||||
values.extend_from_slice(&reference[..previous_len]);
|
||||
values.push(value);
|
||||
*self = Self::Owned(values);
|
||||
}
|
||||
Self::Owned(values) => {
|
||||
debug_assert_eq!(values.len(), previous_len);
|
||||
values.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn values<'a>(&'a self, reference: &'a [f64]) -> &'a [f64] {
|
||||
match self {
|
||||
Self::Identical => reference,
|
||||
Self::Owned(values) => {
|
||||
debug_assert_eq!(values.len(), reference.len());
|
||||
values
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set(&mut self, index: usize, value: f64, reference: &[f64]) {
|
||||
assert!(index < reference.len(), "series index out of bounds");
|
||||
match self {
|
||||
Self::Owned(values) => values[index] = value,
|
||||
Self::Identical if value.to_bits() == reference[index].to_bits() => {}
|
||||
Self::Identical => {
|
||||
let mut values = reference.to_vec();
|
||||
values[index] = value;
|
||||
*self = Self::Owned(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn prefix(&self) -> Self {
|
||||
match self {
|
||||
Self::Identical => Self::Identical,
|
||||
Self::Owned(values) => Self::Owned(prefix_sums(values)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct RepeatedValues<T> {
|
||||
repeated: T,
|
||||
values: Option<Vec<T>>,
|
||||
len: usize,
|
||||
}
|
||||
|
||||
impl<T: Default + Clone + Eq> RepeatedValues<T> {
|
||||
pub(super) fn new() -> Self {
|
||||
Self { repeated: T::default(), values: None, len: 0 }
|
||||
}
|
||||
|
||||
pub(super) fn push(&mut self, value: &T, capacity: usize) {
|
||||
if let Some(values) = &mut self.values {
|
||||
values.push(value.clone());
|
||||
} else if self.len == 0 {
|
||||
self.repeated = value.clone();
|
||||
} else if *value != self.repeated {
|
||||
let mut values = Vec::with_capacity(capacity);
|
||||
values.resize(self.len, std::mem::take(&mut self.repeated));
|
||||
values.push(value.clone());
|
||||
self.values = Some(values);
|
||||
}
|
||||
self.len += 1;
|
||||
}
|
||||
|
||||
pub(super) fn set(&mut self, index: usize, value: T) {
|
||||
assert!(index < self.len, "series index out of bounds");
|
||||
if let Some(values) = &mut self.values {
|
||||
values[index] = value;
|
||||
} else if value != self.repeated {
|
||||
let mut values = vec![std::mem::take(&mut self.repeated); self.len];
|
||||
values[index] = value;
|
||||
self.values = Some(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Index<usize> for RepeatedValues<T> {
|
||||
type Output = T;
|
||||
|
||||
fn index(&self, index: usize) -> &T {
|
||||
assert!(index < self.len, "series index out of bounds");
|
||||
match &self.values {
|
||||
Some(values) => &values[index],
|
||||
None => &self.repeated,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn bits(values: &[f64]) -> Vec<u64> {
|
||||
values.iter().map(|value| value.to_bits()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identical_prices_share_only_after_exact_bit_comparison() {
|
||||
let reference = [10., -0., f64::from_bits(0x7ff8_0000_0000_0042), f64::INFINITY];
|
||||
let mut column = ReferenceMatchedValues::Identical;
|
||||
for (index, value) in reference.iter().copied().enumerate() {
|
||||
column.push(value, &reference[..=index], reference.len());
|
||||
}
|
||||
assert!(matches!(column, ReferenceMatchedValues::Identical));
|
||||
assert_eq!(column.values(&reference).as_ptr(), reference.as_ptr());
|
||||
let prefix = prefix_sums(&reference);
|
||||
assert_eq!(bits(column.prefix().values(&prefix)), bits(&prefix));
|
||||
|
||||
let original = column.clone();
|
||||
column.set(1, 0., &reference);
|
||||
assert!(matches!(column, ReferenceMatchedValues::Owned(_)));
|
||||
assert_eq!(column.values(&reference)[1].to_bits(), 0_f64.to_bits());
|
||||
assert_eq!(bits(original.values(&reference)), bits(&reference));
|
||||
assert_eq!(bits(column.prefix().values(&prefix)), bits(&prefix_sums(column.values(&reference))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn differing_prices_preserve_zero_nan_payloads_and_prior_rows() {
|
||||
let reference = [10., 11., f64::from_bits(0x7ff8_0000_0000_0042), 13.];
|
||||
for actual in [
|
||||
[10., 0., reference[2], 13.],
|
||||
[10., 11., f64::from_bits(0x7ff8_0000_0000_0043), 13.],
|
||||
] {
|
||||
let mut column = ReferenceMatchedValues::Identical;
|
||||
for (index, value) in actual.iter().copied().enumerate() {
|
||||
column.push(value, &reference[..=index], actual.len());
|
||||
}
|
||||
assert!(matches!(column, ReferenceMatchedValues::Owned(_)));
|
||||
assert_eq!(bits(column.values(&reference)), bits(&actual));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_values_preserve_nonzero_values_and_copy_on_change() {
|
||||
let mut column = RepeatedValues::new();
|
||||
for _ in 0..128 { column.push(&7_u64, 128); }
|
||||
assert!(column.values.is_none());
|
||||
assert_eq!(column[127], 7);
|
||||
column.set(0, 7);
|
||||
assert!(column.values.is_none());
|
||||
let mut changed = column.clone();
|
||||
changed.set(64, 9);
|
||||
assert_eq!(changed[64], 9);
|
||||
assert_eq!(changed[63], 7);
|
||||
assert_eq!(column[64], 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_values_keep_none_distinct_from_empty_and_repeated_text() {
|
||||
for repeated in [None, Some(String::new()), Some("continuous".to_string())] {
|
||||
let mut column = RepeatedValues::new();
|
||||
for _ in 0..12 { column.push(&repeated, 16); }
|
||||
assert!(column.values.is_none());
|
||||
assert_eq!(column[0], repeated);
|
||||
column.push(&Some("closing".to_string()), 16);
|
||||
assert_eq!(column[11], repeated);
|
||||
assert_eq!(column[12].as_deref(), Some("closing"));
|
||||
column.set(5, None);
|
||||
assert_eq!(column[5], None);
|
||||
assert_eq!(column[4], repeated);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "series index out of bounds")]
|
||||
fn repeated_values_reject_out_of_range_access() {
|
||||
let column = RepeatedValues::<u64>::new();
|
||||
let _ = column[0];
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,22 @@ pub enum QuoteConditionScope {
|
||||
AnyTarget,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum StockPoolExitRole {
|
||||
OrdinarySell,
|
||||
RiskExit,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct StockPoolExitSignal {
|
||||
pub role: StockPoolExitRole,
|
||||
pub when_expr: String,
|
||||
pub remaining_position_bps: u32,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
pub fn stock_pool_target_holding_count(policy: &Value) -> Result<Option<usize>, String> {
|
||||
let object = policy
|
||||
.as_object()
|
||||
@@ -399,6 +415,8 @@ pub struct StockPoolExecutionRule {
|
||||
pub sell_condition_scope: Option<QuoteConditionScope>,
|
||||
#[serde(skip)]
|
||||
pub secondary_sell_condition: bool,
|
||||
#[serde(skip)]
|
||||
pub independent_sell_condition: bool,
|
||||
#[serde(
|
||||
default,
|
||||
deserialize_with = "crate::holding_policy::deserialize_optional_policy"
|
||||
@@ -461,11 +479,14 @@ pub struct StockPoolSelection {
|
||||
pub generation: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
|
||||
pub struct StockPoolDecisionConstraints {
|
||||
pub execution_date: Option<NaiveDate>,
|
||||
pub frozen_positions: BTreeMap<String, FrozenStockPoolPosition>,
|
||||
pub prior_target_weights: BTreeMap<String, i32>,
|
||||
/// Sizing ratios from prior plans; integer bps are display/legacy only.
|
||||
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub prior_target_weight_ratios: BTreeMap<String, Decimal>,
|
||||
pub pending_entry_symbols: BTreeSet<String>,
|
||||
pub next_day_outside_exit_symbols: BTreeSet<String>,
|
||||
pub market_timing_policy: Option<crate::stock_pool_index_policy::MarketTimingPolicy>,
|
||||
@@ -476,6 +497,10 @@ pub struct StockPoolDecisionConstraints {
|
||||
pub default_stop_loss: Option<Decimal>,
|
||||
pub default_take_profit: Option<Decimal>,
|
||||
pub position_target_bps: BTreeMap<String, u32>,
|
||||
pub independent_position_target_bps: BTreeMap<String, u32>,
|
||||
/// First actually planned holding quantity for this generation. Retries
|
||||
/// apply percentages to this basis, never to the remaining holding.
|
||||
pub position_action_bases: BTreeMap<String, Decimal>,
|
||||
pub buy_denials: BTreeMap<String, Vec<String>>,
|
||||
pub same_day_sold_symbols: BTreeSet<String>,
|
||||
pub automatic_permissions: BTreeMap<String, crate::holding_policy::AutomaticTradePermission>,
|
||||
@@ -508,6 +533,10 @@ pub struct StockPoolPlanRow {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct StockPoolPlan {
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub target_weight_ratios: BTreeMap<String, Decimal>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub position_action_bases: BTreeMap<String, Decimal>,
|
||||
pub market_timing: Option<crate::stock_pool_index_policy::MarketTimingEvaluation>,
|
||||
pub rows: Vec<StockPoolPlanRow>,
|
||||
pub budget: Decimal,
|
||||
@@ -521,7 +550,7 @@ pub struct StockPoolPlan {
|
||||
|
||||
/// A signal-time contract. Only the broker/execution adapter supplies later
|
||||
/// prices, actual cash and holdings; strategy code never sees those inputs.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct FrozenStockPoolIntent {
|
||||
pub pool_id: String,
|
||||
pub signal_date: NaiveDate,
|
||||
@@ -549,6 +578,8 @@ pub struct StockPoolProgram {
|
||||
pub timing_policy: Value,
|
||||
pub stop_take_policy: Value,
|
||||
pub out_of_pool_policy: String,
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub exit_signals: Vec<StockPoolExitSignal>,
|
||||
}
|
||||
|
||||
impl StockPoolProgram {
|
||||
@@ -562,10 +593,19 @@ impl StockPoolProgram {
|
||||
normalize_stock_pool_members(&self.members)?;
|
||||
stock_pool_funding_from_configuration(&self.allocation_policy)?;
|
||||
stock_pool_constraints_from_configuration(&self.allocation_policy, &self.stop_take_policy)?;
|
||||
normalize_stock_pool_execution_rule(
|
||||
let mut identities = BTreeSet::new();
|
||||
for signal in &self.exit_signals {
|
||||
if signal.when_expr.trim().is_empty() || signal.reason.trim().is_empty() || signal.remaining_position_bps >= 10000 {
|
||||
return Err("stock_pool_exit_signal_invalid".into());
|
||||
}
|
||||
let identity = serde_json::to_string(signal).map_err(|error| error.to_string())?;
|
||||
if !identities.insert(identity) { return Err("stock_pool_exit_signal_duplicate".into()); }
|
||||
}
|
||||
normalize_stock_pool_execution_rule_with_exit_roles(
|
||||
Some(&self.timing_policy),
|
||||
secondary_buy,
|
||||
secondary_sell,
|
||||
secondary_sell || self.exit_signals.iter().any(|signal| signal.role == StockPoolExitRole::OrdinarySell),
|
||||
self.exit_signals.iter().any(|signal| signal.role == StockPoolExitRole::RiskExit),
|
||||
)?;
|
||||
if !matches!(
|
||||
self.out_of_pool_policy.as_str(),
|
||||
@@ -583,6 +623,7 @@ impl Default for StockPoolExecutionRule {
|
||||
buy_condition_scope: None,
|
||||
sell_condition_scope: None,
|
||||
secondary_sell_condition: false,
|
||||
independent_sell_condition: false,
|
||||
automatic_trade_protection: Default::default(),
|
||||
schema_version: STOCK_POOL_SCHEMA_VERSION,
|
||||
auto_execute: true,
|
||||
@@ -669,7 +710,17 @@ pub fn build_stock_pool_target_plan_with_fee_model(
|
||||
}
|
||||
}
|
||||
}
|
||||
// Validate source targets before a stronger stop/expiry can replace them.
|
||||
// Otherwise an invalid ratio could be hidden by target consolidation.
|
||||
for (symbol, target) in constraints.position_target_bps.iter().chain(constraints.independent_position_target_bps.iter()) {
|
||||
if *target >= 10_000 {
|
||||
return Err(format!("factor position target for {symbol} must be below 10000 bps"));
|
||||
}
|
||||
}
|
||||
let mut effective_position_targets = constraints.position_target_bps.clone();
|
||||
for (symbol, target) in &constraints.independent_position_target_bps {
|
||||
effective_position_targets.entry(symbol.clone()).and_modify(|current| *current = (*current).min(*target)).or_insert(*target);
|
||||
}
|
||||
for (symbol, permission) in &constraints.automatic_permissions {
|
||||
if permission.max_holding_exit {
|
||||
effective_position_targets.insert(symbol.clone(), 0);
|
||||
@@ -764,6 +815,19 @@ pub fn build_stock_pool_target_plan_with_fee_model(
|
||||
if quote_map.len() != quotes.len() {
|
||||
return Err("duplicate or invalid stock pool execution quotes".into());
|
||||
}
|
||||
let declared_symbols = normalized_members.iter().map(|member| member.symbol.as_str()).collect::<BTreeSet<_>>();
|
||||
for (symbol, quantity) in &constraints.position_action_bases {
|
||||
if normalize_stock_symbol(symbol).as_ref() != Some(symbol) || *quantity <= Decimal::ZERO {
|
||||
return Err(format!("invalid stock pool position-action basis:{symbol}"));
|
||||
}
|
||||
}
|
||||
for (symbol, _) in constraints.position_target_bps.iter().chain(constraints.independent_position_target_bps.iter()) {
|
||||
if normalize_stock_symbol(symbol).as_deref() != Some(symbol.as_str()) || (!declared_symbols.contains(symbol.as_str()) && !current.contains_key(symbol)) {
|
||||
return Err(format!("position action is outside declared candidates and managed holdings:{symbol}"));
|
||||
}
|
||||
}
|
||||
// Exit rules act on managed holdings, not on an unheld candidate's entry.
|
||||
effective_position_targets.retain(|symbol, _| current.get(symbol).is_some_and(|position| position.0 > Decimal::ZERO));
|
||||
frozen::validate(selection.trade_date, constraints, ¤t)?;
|
||||
for symbol in constraints.frozen_positions.keys() {
|
||||
effective_position_targets.remove(symbol);
|
||||
@@ -863,37 +927,49 @@ pub fn build_stock_pool_target_plan_with_fee_model(
|
||||
.then(|| symbol.clone())
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
// A full stop is stricter than a simultaneous relative reduction. Merge
|
||||
// the target before selecting its single owner, never emit a second exit.
|
||||
for symbol in &global_stop_hits {
|
||||
if let Some(target) = effective_position_targets.get_mut(symbol) {
|
||||
*target = 0;
|
||||
}
|
||||
}
|
||||
let mut quote_sell_exits = BTreeSet::new();
|
||||
let mut sell_condition_denials = BTreeSet::new();
|
||||
if rule.sell_trigger_mode == POOL_SELL_CONDITION {
|
||||
let ordinary_enabled = !rule.sell_condition.trim().is_empty() || rule.secondary_sell_condition;
|
||||
// Ordinary sell predicates only depend on positions participating in
|
||||
// that stage. Independent stops/expiry and protected holdings were
|
||||
// already decided above; unrelated quote fields must not block them.
|
||||
let held = current
|
||||
.iter()
|
||||
.filter(|(symbol, row)| {
|
||||
row.0 > Decimal::ZERO && !constraints.frozen_positions.contains_key(*symbol)
|
||||
row.0 > Decimal::ZERO
|
||||
&& !protected_positions.contains(*symbol)
|
||||
&& !global_stop_hits.contains(*symbol)
|
||||
&& constraints.independent_position_target_bps.get(*symbol) != Some(&0)
|
||||
&& !constraints.automatic_permissions.get(*symbol)
|
||||
.is_some_and(|permission| permission.max_holding_exit)
|
||||
})
|
||||
.map(|(symbol, _)| symbol.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let qualified = quote_condition_results(
|
||||
let qualified = if ordinary_enabled { quote_condition_results(
|
||||
&rule.sell_condition,
|
||||
rule.sell_condition_scope,
|
||||
&held,
|
||||
"e_map,
|
||||
)?;
|
||||
)? } else { BTreeMap::new() };
|
||||
for symbol in held {
|
||||
if global_stop_hits.contains(&symbol)
|
||||
|| constraints
|
||||
.automatic_permissions
|
||||
.get(&symbol)
|
||||
.is_some_and(|permission| permission.max_holding_exit)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let permitted = qualified.get(&symbol) == Some(&true)
|
||||
let permitted = ordinary_enabled && qualified.get(&symbol) == Some(&true)
|
||||
&& (!rule.secondary_sell_condition
|
||||
|| constraints.position_target_bps.contains_key(&symbol));
|
||||
if !permitted {
|
||||
sell_condition_denials.insert(symbol.clone());
|
||||
effective_position_targets.remove(&symbol);
|
||||
if let Some(target) = constraints.independent_position_target_bps.get(&symbol) {
|
||||
effective_position_targets.insert(symbol.clone(), *target);
|
||||
} else {
|
||||
sell_condition_denials.insert(symbol.clone());
|
||||
}
|
||||
} else if !rule.secondary_sell_condition {
|
||||
quote_sell_exits.insert(symbol.clone());
|
||||
effective_position_targets.insert(symbol, 0);
|
||||
@@ -944,6 +1020,7 @@ pub fn build_stock_pool_target_plan_with_fee_model(
|
||||
let normalized_same_day_sold =
|
||||
normalize_symbol_set(&same_day_sold_symbols.iter().cloned().collect::<Vec<_>>())?;
|
||||
let mut rebuy_exclusions = stop_take_exits.clone();
|
||||
rebuy_exclusions.extend(effective_position_targets.keys().cloned());
|
||||
rebuy_exclusions.extend(
|
||||
normalized_same_day_sold
|
||||
.iter()
|
||||
@@ -1024,6 +1101,11 @@ pub fn build_stock_pool_target_plan_with_fee_model(
|
||||
target_count,
|
||||
)?
|
||||
};
|
||||
let target_weight_ratios = frozen::sizing_ratios(
|
||||
&original_final_symbols, &active_symbols, &explicit_weights,
|
||||
constraints, reserved_protected_slots, &weights,
|
||||
)?;
|
||||
let sizing_ratio = |symbol: &str| target_weight_ratios.get(symbol).copied().unwrap_or(Decimal::ZERO);
|
||||
for symbol in &rebuy_exclusions {
|
||||
if original_final_symbols.contains(symbol) || current.contains_key(symbol) {
|
||||
weights.insert(symbol.clone(), 0);
|
||||
@@ -1031,7 +1113,12 @@ pub fn build_stock_pool_target_plan_with_fee_model(
|
||||
}
|
||||
let mut planning_symbols = active_symbols;
|
||||
for symbol in &original_final_symbols {
|
||||
if rebuy_exclusions.contains(symbol) && !planning_symbols.contains(symbol) {
|
||||
// An explicit quote/expiry position action owns its single target row.
|
||||
// Keep it excluded from entry sizing without adding a second stop row.
|
||||
if rebuy_exclusions.contains(symbol)
|
||||
&& !factor_position_target_bps.contains_key(symbol)
|
||||
&& !planning_symbols.contains(symbol)
|
||||
{
|
||||
planning_symbols.push(symbol.clone());
|
||||
}
|
||||
}
|
||||
@@ -1158,8 +1245,7 @@ pub fn build_stock_pool_target_plan_with_fee_model(
|
||||
.map(|symbol| {
|
||||
let current_value = current[symbol].0
|
||||
* frozen::valuation(symbol, "e_map, &constraints.frozen_positions)?;
|
||||
let desired =
|
||||
budget * Decimal::from(*weights.get(symbol).unwrap_or(&0)) / Decimal::from(10_000);
|
||||
let desired = budget * sizing_ratio(symbol);
|
||||
if constraints.frozen_positions.contains_key(symbol) {
|
||||
return Ok((symbol.clone(), desired));
|
||||
}
|
||||
@@ -1200,7 +1286,7 @@ pub fn build_stock_pool_target_plan_with_fee_model(
|
||||
let free_desired = weights
|
||||
.iter()
|
||||
.filter(|(symbol, _)| !protected_values.contains_key(*symbol))
|
||||
.map(|(_, weight)| budget * Decimal::from(*weight) / Decimal::from(10_000))
|
||||
.map(|(symbol, _)| budget * sizing_ratio(symbol))
|
||||
.sum::<Decimal>();
|
||||
let free_budget = (budget * Decimal::from(requested_weight_total) / Decimal::from(10_000)
|
||||
- protected_values.values().copied().sum::<Decimal>())
|
||||
@@ -1219,7 +1305,7 @@ pub fn build_stock_pool_target_plan_with_fee_model(
|
||||
rows.push(StockPoolPlanRow {
|
||||
symbol: symbol.clone(),
|
||||
target_weight_bps: weight,
|
||||
target_value: budget * Decimal::from(weight) / Decimal::from(10_000),
|
||||
target_value: budget * sizing_ratio(symbol),
|
||||
current_quantity: quantity,
|
||||
target_quantity: quantity,
|
||||
delta_quantity: Decimal::ZERO,
|
||||
@@ -1323,24 +1409,11 @@ pub fn build_stock_pool_target_plan_with_fee_model(
|
||||
}
|
||||
|
||||
for (symbol, target_bps) in factor_position_target_bps {
|
||||
if *target_bps >= 10_000 {
|
||||
return Err(format!(
|
||||
"factor position target for {symbol} must be below 10000 bps"
|
||||
));
|
||||
}
|
||||
if !member_map.contains_key(symbol) && !current.contains_key(symbol) {
|
||||
return Err(format!(
|
||||
"factor position-action symbol {symbol} is outside candidates and managed holdings"
|
||||
));
|
||||
}
|
||||
if selection.final_symbols.contains(symbol)
|
||||
&& !maximum_holding_exits.contains(symbol)
|
||||
&& !quote_sell_exits.contains(symbol)
|
||||
{
|
||||
return Err(format!(
|
||||
"factor position-action symbol {symbol} cannot remain in final selection"
|
||||
));
|
||||
}
|
||||
let current_quantity = current
|
||||
.get(symbol)
|
||||
.map(|value| value.0)
|
||||
@@ -1362,10 +1435,11 @@ pub fn build_stock_pool_target_plan_with_fee_model(
|
||||
Decimal::ZERO
|
||||
} else {
|
||||
floor_step(
|
||||
current_quantity * Decimal::from(*target_bps) / Decimal::from(10_000),
|
||||
constraints.position_action_bases.get(symbol).copied().unwrap_or(current_quantity)
|
||||
* Decimal::from(*target_bps) / Decimal::from(10_000),
|
||||
step,
|
||||
)
|
||||
};
|
||||
}.min(current_quantity);
|
||||
let desired_reduction = (current_quantity - requested_target).max(Decimal::ZERO);
|
||||
let executable = if *target_bps == 0 {
|
||||
closable_quantity.min(current_quantity).max(Decimal::ZERO)
|
||||
@@ -1379,13 +1453,33 @@ pub fn build_stock_pool_target_plan_with_fee_model(
|
||||
if current_quantity == Decimal::ZERO {
|
||||
(
|
||||
"FACTOR_EXIT_ALREADY_SATISFIED",
|
||||
"生产因子持仓动作命中,当前无持仓",
|
||||
"持仓退出规则命中,当前无持仓",
|
||||
Decimal::ZERO,
|
||||
Decimal::ZERO,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
} else if desired_reduction == Decimal::ZERO {
|
||||
(
|
||||
"FACTOR_EXIT_ALREADY_SATISFIED",
|
||||
"本次信号的持仓退出目标已达到,不重复减仓",
|
||||
Decimal::ZERO,
|
||||
current_quantity,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
} else if executable == Decimal::ZERO && closable_quantity >= desired_reduction {
|
||||
(
|
||||
"BELOW_MINIMUM_TRADE_UNIT_ALREADY_SATISFIED",
|
||||
"目标持仓差额不足最小交易单位,无需重复委托",
|
||||
Decimal::ZERO,
|
||||
current_quantity,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
} else if executable == Decimal::ZERO {
|
||||
(
|
||||
"DEFERRED_T_PLUS_ONE",
|
||||
@@ -1414,6 +1508,10 @@ pub fn build_stock_pool_target_plan_with_fee_model(
|
||||
"达到最长持有期,按配置退出"
|
||||
} else if quote_sell_exits.contains(symbol) {
|
||||
"卖出行情条件命中"
|
||||
} else if stop_take_exits.contains(symbol) {
|
||||
"止损/止盈触发,覆盖较弱的减仓目标"
|
||||
} else if constraints.independent_position_target_bps.get(symbol) == Some(target_bps) {
|
||||
"独立风险退出条件命中"
|
||||
} else if *target_bps == 0 {
|
||||
"生产因子退出条件命中"
|
||||
} else {
|
||||
@@ -1481,7 +1579,7 @@ pub fn build_stock_pool_target_plan_with_fee_model(
|
||||
let target_value = protected_values
|
||||
.get(symbol)
|
||||
.copied()
|
||||
.unwrap_or(budget * Decimal::from(weight) / Decimal::from(10_000) * free_scale)
|
||||
.unwrap_or(if weight == 0 { Decimal::ZERO } else { budget * sizing_ratio(symbol) * free_scale })
|
||||
.round_dp_with_strategy(2, RoundingStrategy::MidpointNearestEven);
|
||||
let sizing_price = if target_value >= current_quantity * quote.last_price {
|
||||
quote.buy_sizing_price.unwrap_or(quote.last_price)
|
||||
@@ -1491,7 +1589,17 @@ pub fn build_stock_pool_target_plan_with_fee_model(
|
||||
if sizing_price <= Decimal::ZERO {
|
||||
return Err(format!("{symbol} execution sizing price is invalid"));
|
||||
}
|
||||
let raw_target = (target_value / sizing_price).floor();
|
||||
// Existing shares are marked at the observed market price. Only the
|
||||
// new buy leg pays its executable/slippage price; repricing the whole
|
||||
// position would charge fictitious slippage and miss a board lot.
|
||||
let current_value = current_quantity * quote.last_price;
|
||||
let raw_target = if target_value >= current_value {
|
||||
current_quantity + ((target_value - current_value) / sizing_price).floor()
|
||||
} else {
|
||||
// Sale slippage changes proceeds, not the marked shares we must
|
||||
// remove to reach a market-value target.
|
||||
(target_value / quote.last_price).floor()
|
||||
};
|
||||
let (step, minimum_buy) = order_quantity_rules(quote)?;
|
||||
let mut target_quantity = current_quantity;
|
||||
let mut delta = Decimal::ZERO;
|
||||
@@ -1688,6 +1796,14 @@ pub fn build_stock_pool_target_plan_with_fee_model(
|
||||
}
|
||||
}
|
||||
}
|
||||
// Verify disjoint planning ownership before an index cap can address rows
|
||||
// by symbol. Never deduplicate emitted intentions or count proceeds twice.
|
||||
let mut owners = BTreeSet::new();
|
||||
for row in &rows {
|
||||
if !owners.insert(row.symbol.as_str()) {
|
||||
return Err(format!("stock_pool_target_owner_conflict:{}", row.symbol));
|
||||
}
|
||||
}
|
||||
if market_timing.is_some() {
|
||||
let caps = index_cap::remaining_index_targets(
|
||||
¤t,
|
||||
@@ -1806,7 +1922,7 @@ pub fn build_stock_pool_target_plan_with_fee_model(
|
||||
let cost = |quantity: Decimal| {
|
||||
Ok(quantity * price + fee_for(&row.symbol, OrderSide::Buy, quantity * price)?)
|
||||
};
|
||||
let own_budget = (row.target_value - row.current_quantity * price).max(Decimal::ZERO);
|
||||
let own_budget = (row.target_value - row.current_quantity * quote.last_price).max(Decimal::ZERO);
|
||||
let allocation_quantity = max_affordable_buy_quantity_with_cost(
|
||||
own_budget,
|
||||
row.delta_quantity,
|
||||
@@ -1890,7 +2006,16 @@ pub fn build_stock_pool_target_plan_with_fee_model(
|
||||
.into_iter()
|
||||
.sum();
|
||||
let estimated_cash_after = available_cash - estimated_buy_amount + estimated_sell_amount;
|
||||
let position_action_bases = rows.iter()
|
||||
.filter(|row| effective_position_targets.get(&row.symbol).is_some_and(|bps| *bps > 0)
|
||||
&& row.current_quantity > Decimal::ZERO
|
||||
&& row.status != "AUTOMATIC_TRADE_PROTECTED"
|
||||
&& !constraints.frozen_positions.contains_key(&row.symbol))
|
||||
.map(|row| (row.symbol.clone(), constraints.position_action_bases.get(&row.symbol).copied().unwrap_or(row.current_quantity)))
|
||||
.collect();
|
||||
Ok(StockPoolPlan {
|
||||
target_weight_ratios,
|
||||
position_action_bases,
|
||||
market_timing,
|
||||
rows,
|
||||
budget,
|
||||
@@ -2167,6 +2292,15 @@ pub fn normalize_stock_pool_execution_rule(
|
||||
raw: Option<&Value>,
|
||||
secondary_buy_condition: bool,
|
||||
secondary_sell_condition: bool,
|
||||
) -> Result<StockPoolExecutionRule, String> {
|
||||
normalize_stock_pool_execution_rule_with_exit_roles(raw, secondary_buy_condition, secondary_sell_condition, false)
|
||||
}
|
||||
|
||||
pub fn normalize_stock_pool_execution_rule_with_exit_roles(
|
||||
raw: Option<&Value>,
|
||||
secondary_buy_condition: bool,
|
||||
secondary_sell_condition: bool,
|
||||
independent_sell_condition: bool,
|
||||
) -> Result<StockPoolExecutionRule, String> {
|
||||
let mut rule = match raw {
|
||||
None | Some(Value::Null) => StockPoolExecutionRule::default(),
|
||||
@@ -2174,6 +2308,7 @@ pub fn normalize_stock_pool_execution_rule(
|
||||
.map_err(|err| format!("stock pool execution_rule is invalid: {err}"))?,
|
||||
};
|
||||
rule.secondary_sell_condition = secondary_sell_condition;
|
||||
rule.independent_sell_condition = independent_sell_condition;
|
||||
rule.automatic_trade_protection.validate()?;
|
||||
if rule.schema_version != STOCK_POOL_SCHEMA_VERSION {
|
||||
return Err(format!(
|
||||
@@ -2260,7 +2395,7 @@ pub fn normalize_stock_pool_execution_rule(
|
||||
return Err("stock pool buy_condition is not supported".to_string());
|
||||
}
|
||||
if rule.sell_trigger_mode == POOL_SELL_CONDITION {
|
||||
if (rule.sell_condition.trim().is_empty() && !secondary_sell_condition)
|
||||
if (rule.sell_condition.trim().is_empty() && !secondary_sell_condition && !independent_sell_condition)
|
||||
|| (!rule.sell_condition.trim().is_empty()
|
||||
&& parse_stock_pool_condition(&rule.sell_condition).is_none())
|
||||
{
|
||||
|
||||
@@ -1,4 +1,56 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn legacy_constraints_do_not_gain_an_empty_precision_field() {
|
||||
let value = serde_json::to_value(StockPoolDecisionConstraints::default()).unwrap();
|
||||
assert!(value.get("prior_target_weight_ratios").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_thirty_seats_use_full_precision_at_a_board_lot_boundary() {
|
||||
for (equity, price, executable, held) in [("999377.147617", "3.07", "3.070307", 2000), ("995624.8819", "6.42", "6.420642", 900)] {
|
||||
let pool = members(30);
|
||||
let mut market = quotes(30);
|
||||
let last = market.last_mut().unwrap();
|
||||
last.last_price = price.parse().unwrap();
|
||||
last.buy_sizing_price = Some(executable.parse().unwrap());
|
||||
let positions = vec![Position { symbol: last.symbol.clone(), quantity: held.into(), closable_quantity: held.into(), average_cost: last.last_price }];
|
||||
let mut selection = selection(30, 30);
|
||||
let constraints = StockPoolDecisionConstraints { target_holding_count: Some(30), reserve_cash_slots: 1, ..Default::default() };
|
||||
let total: Decimal = equity.parse().unwrap();
|
||||
let account = AccountSnapshot { total_equity: total, cash: total - positions[0].quantity * last.last_price, frozen_cash: Decimal::ZERO };
|
||||
let build = |selection: &StockPoolSelection| build_stock_pool_target_plan_with_constraints(selection, &pool, &StockPoolExecutionRule::default(), &account, &positions, &market,
|
||||
2000, Decimal::ZERO, "hold", "full_rebalance", &constraints, "exact-shares", Decimal::new(2,4), Decimal::ZERO, Decimal::ZERO).unwrap();
|
||||
let plan = build(&selection);
|
||||
let target = plan.rows.iter().find(|row| row.symbol == positions[0].symbol).unwrap();
|
||||
assert_eq!(target.delta_quantity, Decimal::from(100), "{equity}: {target:?}");
|
||||
let expected = (total * Decimal::new(2,1) / Decimal::from(31)).round_dp_with_strategy(2, RoundingStrategy::MidpointNearestEven);
|
||||
assert!(plan.rows.iter().all(|row| row.target_value == expected));
|
||||
selection.final_symbols.reverse();
|
||||
selection.requested_symbols.reverse();
|
||||
assert_eq!(build(&selection).rows.iter().find(|row| row.symbol == positions[0].symbol).unwrap().delta_quantity, Decimal::from(100));
|
||||
let date = selection.trade_date;
|
||||
let state = crate::stock_pool_state::StockPoolExecutionState::default().observe(date,date,&[date],&pool,&positions).unwrap().record_plan(date,"exact-shares",&plan).unwrap();
|
||||
assert_eq!(state.schema_version, 2);
|
||||
assert_eq!(state.last_target_weight_ratios[&positions[0].symbol], Decimal::ONE / Decimal::from(30));
|
||||
let restored: crate::stock_pool_state::StockPoolExecutionState = serde_json::from_value(serde_json::to_value(&state).unwrap()).unwrap();
|
||||
restored.validate().unwrap();
|
||||
assert_eq!(state, restored);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sale_slippage_does_not_prevent_a_marked_value_board_lot_reduction() {
|
||||
let pool = members(1);
|
||||
let mut market = quotes(1);
|
||||
market[0].sell_sizing_price = Some(Decimal::new(99,1));
|
||||
let positions = vec![Position { symbol:symbol(1),quantity:200.into(),closable_quantity:200.into(),average_cost:10.into() }];
|
||||
let plan = build_stock_pool_target_plan_with_constraints(&selection(1,1),&pool,&StockPoolExecutionRule::default(),
|
||||
&AccountSnapshot {total_equity:2000.into(),cash:Decimal::ZERO,frozen_cash:Decimal::ZERO},&positions,&market,
|
||||
5000,Decimal::ZERO,"hold","full_rebalance",&StockPoolDecisionConstraints::default(),"reduce",Decimal::ZERO,Decimal::ZERO,Decimal::ZERO).unwrap();
|
||||
assert_eq!(plan.rows[0].target_value,Decimal::from(1000));
|
||||
assert_eq!(plan.rows[0].delta_quantity,Decimal::from(-100));
|
||||
}
|
||||
use serde_json::json;
|
||||
|
||||
fn symbol(index: usize) -> String {
|
||||
@@ -509,6 +561,16 @@ fn condition_plan(
|
||||
quotes: &[MarketSnapshot],
|
||||
constraints: &StockPoolDecisionConstraints,
|
||||
) -> StockPoolPlan {
|
||||
condition_plan_result(selection, rule, positions, quotes, constraints).unwrap()
|
||||
}
|
||||
|
||||
fn condition_plan_result(
|
||||
selection: &StockPoolSelection,
|
||||
rule: &StockPoolExecutionRule,
|
||||
positions: &[Position],
|
||||
quotes: &[MarketSnapshot],
|
||||
constraints: &StockPoolDecisionConstraints,
|
||||
) -> Result<StockPoolPlan, String> {
|
||||
let held_value = positions
|
||||
.iter()
|
||||
.map(|position| {
|
||||
@@ -541,7 +603,6 @@ fn condition_plan(
|
||||
Decimal::ZERO,
|
||||
Decimal::ZERO,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -704,6 +765,221 @@ fn native_sell_and_quote_conditions_are_and_but_stop_and_protection_remain_indep
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn independent_stop_does_not_require_unused_ordinary_sell_quote_facts() {
|
||||
let mut market = quotes(1);
|
||||
market[0].last_price = 9.into();
|
||||
market[0].volume = None;
|
||||
let rule = normalize_stock_pool_execution_rule(
|
||||
Some(&json!({"sell_trigger_mode":"condition","sell_condition":"volume>1000"})),
|
||||
false,
|
||||
false,
|
||||
).unwrap();
|
||||
let constraints = StockPoolDecisionConstraints {
|
||||
default_stop_loss: Some(Decimal::new(5, 2)),
|
||||
..Default::default()
|
||||
};
|
||||
let plan = condition_plan(&selection(1, 1), &rule, &[position(1)], &market, &constraints);
|
||||
let exit = plan.rows.iter().find(|row| row.symbol == symbol(1)).unwrap();
|
||||
assert_eq!(exit.side, Some(OrderSide::Sell), "{plan:?}");
|
||||
assert_eq!(exit.target_quantity, Decimal::ZERO, "{plan:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_sell_scope_excludes_independent_exits_and_protected_positions() {
|
||||
for scope in [QuoteConditionScope::PerSymbol, QuoteConditionScope::AllTargets, QuoteConditionScope::AnyTarget] {
|
||||
for cause in ["stop_loss", "take_profit", "maximum_holding_exit", "automatic_trade_locked", "buy_fill_protection"] {
|
||||
let mut market = quotes(2);
|
||||
market[0].volume = None;
|
||||
let mut constraints = StockPoolDecisionConstraints::default();
|
||||
match cause {
|
||||
"stop_loss" => { market[0].last_price = 9.into(); constraints.default_stop_loss = Some(Decimal::new(5, 2)); },
|
||||
"take_profit" => { market[0].last_price = 12.into(); constraints.default_take_profit = Some(Decimal::new(10, 2)); },
|
||||
"maximum_holding_exit" => { constraints.automatic_permissions.insert(symbol(1), crate::holding_policy::AutomaticTradePermission { max_holding_exit: true, ..Default::default() }); },
|
||||
_ => { constraints.automatic_permissions.insert(symbol(1), crate::holding_policy::AutomaticTradePermission { sell_denial: Some(cause), buy_denial: Some(cause), ..Default::default() }); },
|
||||
}
|
||||
let rule = normalize_stock_pool_execution_rule(
|
||||
Some(&json!({"sell_trigger_mode":"condition","sell_condition":"volume>1000","sell_condition_scope":scope})),
|
||||
false, false,
|
||||
).unwrap();
|
||||
let plan = condition_plan(&selection(2, 2), &rule, &[position(1), position(2)], &market, &constraints);
|
||||
let protected = plan.rows.iter().find(|row| row.symbol == symbol(1)).unwrap();
|
||||
let normal = plan.rows.iter().find(|row| row.symbol == symbol(2)).unwrap();
|
||||
assert_eq!(normal.side, Some(OrderSide::Sell), "{scope:?}/{cause}: {plan:?}");
|
||||
if cause == "automatic_trade_locked" || cause == "buy_fill_protection" {
|
||||
assert_eq!(protected.side, None, "{scope:?}/{cause}: {plan:?}");
|
||||
assert_eq!(protected.target_quantity, 1000.into(), "{scope:?}/{cause}: {plan:?}");
|
||||
assert_eq!(protected.status, "AUTOMATIC_TRADE_PROTECTED", "{scope:?}/{cause}: {plan:?}");
|
||||
} else {
|
||||
assert_eq!(protected.side, Some(OrderSide::Sell), "{scope:?}/{cause}: {plan:?}");
|
||||
assert_eq!(protected.target_quantity, Decimal::ZERO, "{scope:?}/{cause}: {plan:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn independent_exit_quote_priority_does_not_bypass_t_plus_one_or_price_validation() {
|
||||
let rule = normalize_stock_pool_execution_rule(Some(&json!({"sell_trigger_mode":"condition","sell_condition":"volume>1000"})), false, false).unwrap();
|
||||
let mut market = quotes(1);
|
||||
market[0].last_price = 9.into(); market[0].volume = None;
|
||||
let constraints = StockPoolDecisionConstraints { default_stop_loss: Some(Decimal::new(5,2)), ..Default::default() };
|
||||
for closable in [0, 400, 1000] {
|
||||
let mut held = position(1); held.closable_quantity = Decimal::from(closable);
|
||||
let plan = condition_plan(&selection(1,1), &rule, &[held], &market, &constraints);
|
||||
let row = plan.rows.iter().find(|row|row.symbol==symbol(1)).unwrap();
|
||||
assert_eq!(row.delta_quantity, -Decimal::from(closable), "{plan:?}");
|
||||
assert_eq!(row.target_quantity, Decimal::from(1000-closable), "{plan:?}");
|
||||
}
|
||||
market[0].last_price = Decimal::ZERO;
|
||||
assert!(condition_plan_result(&selection(1,1), &rule, &[position(1)], &market, &constraints).unwrap_err().contains("execution quote is invalid"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_stop_overrides_a_simultaneous_factor_reduction_without_a_second_target() {
|
||||
let mut selected = selection(2, 1);
|
||||
selected.final_symbols = vec![symbol(2)];
|
||||
let mut market = quotes(2);
|
||||
market[0].last_price = 9.into();
|
||||
let constraints = StockPoolDecisionConstraints {
|
||||
default_stop_loss: Some(Decimal::new(5, 2)),
|
||||
position_target_bps: BTreeMap::from([(symbol(1), 5000)]),
|
||||
..Default::default()
|
||||
};
|
||||
let plan = condition_plan(&selected, &StockPoolExecutionRule::default(), &[position(1)], &market, &constraints);
|
||||
let rows = plan.rows.iter().filter(|row|row.symbol==symbol(1)).collect::<Vec<_>>();
|
||||
assert_eq!(rows.len(),1,"{plan:?}");
|
||||
assert_eq!(rows[0].target_quantity,Decimal::ZERO,"a full stop must not be weakened by a 50% reduction: {plan:?}");
|
||||
assert_eq!(rows[0].delta_quantity,Decimal::from(-1000),"{plan:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stop_reduction_merge_matrix_preserves_protection_t_plus_one_and_invalid_config_errors() {
|
||||
for take_profit in [false,true] {
|
||||
for reduction in [0,2500,5000,9999] {
|
||||
for closable in [0,400,1000] {
|
||||
for locked in [false,true] {
|
||||
let mut selected=selection(2,1);selected.final_symbols=vec![symbol(2)];
|
||||
let mut market=quotes(2);market[0].last_price=if take_profit {12.into()} else {9.into()};market[0].volume=None;
|
||||
let mut held=position(1);held.closable_quantity=Decimal::from(closable);
|
||||
let mut constraints=StockPoolDecisionConstraints {position_target_bps:BTreeMap::from([(symbol(1),reduction)]),..Default::default()};
|
||||
if take_profit {constraints.default_take_profit=Some(Decimal::new(10,2))} else {constraints.default_stop_loss=Some(Decimal::new(5,2))}
|
||||
if locked {constraints.automatic_permissions.insert(symbol(1),crate::holding_policy::AutomaticTradePermission {sell_denial:Some("automatic_trade_locked"),buy_denial:Some("automatic_trade_locked"),..Default::default()});}
|
||||
let rule=normalize_stock_pool_execution_rule(Some(&json!({"sell_trigger_mode":"condition","sell_condition":"volume>1000"})),false,true).unwrap();
|
||||
let plan=condition_plan(&selected,&rule,&[held],&market,&constraints);
|
||||
let rows=plan.rows.iter().filter(|row|row.symbol==symbol(1)).collect::<Vec<_>>();
|
||||
assert_eq!(rows.len(),1,"{plan:?}");
|
||||
let sold=if locked {0} else {closable};
|
||||
assert_eq!(rows[0].delta_quantity,-Decimal::from(sold),"{plan:?}");
|
||||
assert_eq!(rows[0].target_quantity,Decimal::from(1000-sold),"{plan:?}");
|
||||
assert_eq!(plan.estimated_sell_amount,Decimal::from(sold)*market[0].last_price,"{plan:?}");
|
||||
if locked {assert_eq!(rows[0].status,"AUTOMATIC_TRADE_PROTECTED","{plan:?}")}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut invalid=StockPoolDecisionConstraints {default_stop_loss:Some(Decimal::new(5,2)),position_target_bps:BTreeMap::from([(symbol(1),10000)]),..Default::default()};
|
||||
invalid.automatic_permissions.insert(symbol(1),crate::holding_policy::AutomaticTradePermission {max_holding_exit:true,..Default::default()});
|
||||
assert!(condition_plan_result(&selection(1,1),&StockPoolExecutionRule::default(),&[position(1)],"es(1),&invalid).unwrap_err().contains("must be below 10000"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_sell_keeps_required_quote_failures_and_zero_stop_is_not_an_exit() {
|
||||
let rule = normalize_stock_pool_execution_rule(Some(&json!({"sell_trigger_mode":"condition","sell_condition":"volume>1000"})), false, false).unwrap();
|
||||
let mut market = quotes(1); market[0].last_price = 9.into(); market[0].volume = None;
|
||||
for stop in [None, Some(Decimal::ZERO)] {
|
||||
let constraints = StockPoolDecisionConstraints { default_stop_loss: stop, ..Default::default() };
|
||||
assert_eq!(condition_plan_result(&selection(1,1), &rule, &[position(1)], &market, &constraints).unwrap_err(), "condition requires volume");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quote_field_operator_side_and_scope_matrix_matches_the_configured_predicate() {
|
||||
let market = quotes(2);
|
||||
for (field, threshold) in [("price", "10"), ("last", "10"), ("change_pct", "0"), ("volume", "1000000"), ("amount", "10000000"), ("bid1", "10"), ("ask1", "10")] {
|
||||
for (operator, matched) in [(">",false), (">=",true), ("<",false), ("<=",true), ("==",true), ("!=",false)] {
|
||||
for scope in [QuoteConditionScope::PerSymbol, QuoteConditionScope::AllTargets, QuoteConditionScope::AnyTarget] {
|
||||
for side in [OrderSide::Buy, OrderSide::Sell] {
|
||||
let condition = format!("{field}{operator}{threshold}");
|
||||
let config = if side == OrderSide::Buy {
|
||||
json!({"trigger_mode":"condition","buy_condition":condition,"buy_condition_scope":scope})
|
||||
} else {
|
||||
json!({"sell_trigger_mode":"condition","sell_condition":condition,"sell_condition_scope":scope})
|
||||
};
|
||||
let rule = normalize_stock_pool_execution_rule(Some(&config), false, false).unwrap();
|
||||
let held = if side == OrderSide::Sell { vec![position(1),position(2)] } else { vec![] };
|
||||
let plan = condition_plan(&selection(2,2), &rule, &held, &market, &StockPoolDecisionConstraints::default());
|
||||
assert_eq!(plan.rows.iter().map(|row| &row.symbol).collect::<BTreeSet<_>>().len(), plan.rows.len(), "duplicate target ownership: {plan:?}");
|
||||
let orders = plan.rows.iter().filter(|row|row.side==Some(side)).count();
|
||||
assert_eq!(orders, if matched {2} else {0}, "{side:?}/{scope:?}/{condition}: {plan:?}");
|
||||
if side == OrderSide::Sell && matched {
|
||||
assert_eq!(plan.estimated_sell_amount, Decimal::from(20000), "{plan:?}");
|
||||
assert_eq!(plan.estimated_cash_after, Decimal::from(40000), "{plan:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_exit_roles_merge_only_satisfied_ordinary_actions_with_independent_risk() {
|
||||
for risk in [None,Some(0),Some(5000)] {
|
||||
for ordinary in [None,Some(0),Some(7500)] {
|
||||
for quote in ["","price<9","price>9"] {
|
||||
for locked in [false,true] {
|
||||
for closable in [0,400,1000] {
|
||||
let rule=normalize_stock_pool_execution_rule_with_exit_roles(Some(&json!({"sell_trigger_mode":"condition","sell_condition":quote})),false,true,true).unwrap();
|
||||
let mut constraints=StockPoolDecisionConstraints {portfolio_policy:Some(StockPoolPortfolioPolicy{schema_version:1,membership:MembershipPolicy::RetainHoldings,rebalance_weights:false}),..Default::default()};
|
||||
if let Some(target)=ordinary {constraints.position_target_bps.insert(symbol(1),target);}
|
||||
if let Some(target)=risk {constraints.independent_position_target_bps.insert(symbol(1),target);}
|
||||
if locked {constraints.automatic_permissions.insert(symbol(1),crate::holding_policy::AutomaticTradePermission{sell_denial:Some("automatic_trade_locked"),buy_denial:Some("automatic_trade_locked"),..Default::default()});}
|
||||
let mut held=position(1);held.closable_quantity=closable.into();
|
||||
let plan=condition_plan(&selection(1,1),&rule,&[held],"es(1),&constraints);
|
||||
assert_eq!(plan.rows.len(),1,"{risk:?}/{ordinary:?}/{quote}: {plan:?}");
|
||||
let ordinary=if quote=="price<9" {None} else {ordinary};
|
||||
let target_bps=risk.into_iter().chain(ordinary).min().unwrap_or(10000);
|
||||
let desired=if target_bps==0 {0} else {(1000*target_bps/10000)/100*100};
|
||||
let sold=if locked {0} else {(1000-desired).min(closable)};
|
||||
assert_eq!(plan.rows[0].delta_quantity,-Decimal::from(sold),"{risk:?}/{ordinary:?}/{quote}: {plan:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn risk_only_configuration_never_turns_into_an_unconditional_ordinary_exit() {
|
||||
let rule=normalize_stock_pool_execution_rule_with_exit_roles(Some(&json!({"sell_trigger_mode":"condition"})),false,false,true).unwrap();
|
||||
let mut constraints=StockPoolDecisionConstraints::default();
|
||||
let hold=condition_plan(&selection(1,1),&rule,&[position(1)],"es(1),&constraints);
|
||||
assert_ne!(hold.rows[0].side,Some(OrderSide::Sell),"a risk-only configuration must not manufacture an exit: {hold:?}");
|
||||
constraints.independent_position_target_bps.insert(symbol(1),5000);
|
||||
let exit=condition_plan(&selection(1,1),&rule,&[position(1)],"es(1),&constraints);
|
||||
assert_eq!(exit.rows[0].delta_quantity,Decimal::from(-500),"{exit:?}");
|
||||
let unheld=condition_plan(&selection(1,1),&rule,&[],"es(1),&constraints);
|
||||
assert_eq!(unheld.rows[0].side,Some(OrderSide::Buy),"an exit-only rule must not secretly become a selection/buy filter: {unheld:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quote_only_exit_still_works_when_independent_risk_rules_are_configured() {
|
||||
let rule=normalize_stock_pool_execution_rule_with_exit_roles(Some(&json!({"sell_trigger_mode":"condition","sell_condition":"price>9"})),false,false,true).unwrap();
|
||||
let plan=condition_plan(&selection(1,1),&rule,&[position(1)],"es(1),&StockPoolDecisionConstraints::default());
|
||||
assert_eq!(plan.rows.len(),1);assert_eq!(plan.rows[0].delta_quantity,Decimal::from(-1000),"{plan:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn independent_full_exit_has_no_ordinary_quote_dependency_but_partial_risk_does_not_fake_missing_facts() {
|
||||
let rule=normalize_stock_pool_execution_rule_with_exit_roles(Some(&json!({"sell_trigger_mode":"condition","sell_condition":"volume>100"})),false,true,true).unwrap();
|
||||
let mut market=quotes(1);market[0].volume=None;
|
||||
let mut constraints=StockPoolDecisionConstraints {position_target_bps:BTreeMap::from([(symbol(1),0)]),independent_position_target_bps:BTreeMap::from([(symbol(1),0)]),..Default::default()};
|
||||
let complete=condition_plan(&selection(1,1),&rule,&[position(1)],&market,&constraints);
|
||||
assert_eq!(complete.rows[0].delta_quantity,Decimal::from(-1000));
|
||||
constraints.independent_position_target_bps.insert(symbol(1),5000);
|
||||
assert!(condition_plan_result(&selection(1,1),&rule,&[position(1)],&market,&constraints).unwrap_err().contains("requires volume"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_sell_cooldown_restricts_increases_without_clearing_the_remainder() {
|
||||
let mut constraints = StockPoolDecisionConstraints::default();
|
||||
|
||||
@@ -26,9 +26,58 @@ pub(super) fn validate(
|
||||
{
|
||||
return Err("stock_pool_prior_target_weights_invalid".into());
|
||||
}
|
||||
if constraints.prior_target_weight_ratios.iter().any(|(symbol, ratio)| {
|
||||
normalize_stock_symbol(symbol).as_ref() != Some(symbol) || *ratio < Decimal::ZERO || *ratio > Decimal::ONE
|
||||
}) { return Err("stock_pool_prior_target_weight_ratios_invalid".into()); }
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Never size cash with the rounded display bps. Paused holdings keep the
|
||||
/// actually recorded prior ratio. Legacy bps are preserved, not guessed as 1/N.
|
||||
pub(super) fn sizing_ratios(
|
||||
original: &[String], active: &[String], explicit: &BTreeMap<String, i32>,
|
||||
constraints: &StockPoolDecisionConstraints, reserved_slots: usize,
|
||||
display: &BTreeMap<String, i32>,
|
||||
) -> Result<BTreeMap<String, Decimal>, String> {
|
||||
if !explicit.is_empty() {
|
||||
return Ok(display.iter().map(|(symbol, weight)| (symbol.clone(), Decimal::from(*weight) / Decimal::from(10_000))).collect());
|
||||
}
|
||||
if constraints.frozen_positions.is_empty() {
|
||||
let count = active.len() + reserved_slots;
|
||||
let share = if count == 0 { Decimal::ZERO } else { Decimal::ONE / Decimal::from(count as u64) };
|
||||
return Ok(active.iter().map(|symbol| (symbol.clone(), share)).collect());
|
||||
}
|
||||
let count = original.len() + reserved_slots;
|
||||
let base = if count == 0 { Decimal::ZERO } else { Decimal::ONE / Decimal::from(count as u64) };
|
||||
let mut result = BTreeMap::new();
|
||||
for symbol in constraints.frozen_positions.keys() {
|
||||
let ratio = constraints.prior_target_weight_ratios.get(symbol).copied()
|
||||
.or_else(|| constraints.prior_target_weights.get(symbol).map(|bps| Decimal::from(*bps) / Decimal::from(10_000)))
|
||||
.unwrap_or(base);
|
||||
result.insert(symbol.clone(), ratio);
|
||||
}
|
||||
let frozen_total = result.values().copied().sum::<Decimal>();
|
||||
// Only allow last-digit residue from Decimal division, never a meaningful
|
||||
// over-allocation. All actual cash/fee checks remain downstream.
|
||||
if frozen_total > Decimal::ONE + Decimal::new(1, 24) {
|
||||
return Err("stock_pool_frozen_position_ratios_exceed_budget".into());
|
||||
}
|
||||
let free_original = original.iter().filter(|symbol| !result.contains_key(*symbol)).collect::<BTreeSet<_>>();
|
||||
let free_total = (base * Decimal::from(free_original.len() as u64)).min((Decimal::ONE - frozen_total).max(Decimal::ZERO));
|
||||
let share = if free_original.is_empty() { Decimal::ZERO } else { free_total / Decimal::from(free_original.len() as u64) };
|
||||
let free = active.iter().filter(|symbol| !constraints.frozen_positions.contains_key(*symbol)).collect::<Vec<_>>();
|
||||
let promoted = free.iter().filter(|symbol| !free_original.contains(**symbol)).copied().collect::<Vec<_>>();
|
||||
for symbol in &free { result.insert((*symbol).clone(), if free_original.contains(*symbol) { share } else { Decimal::ZERO }); }
|
||||
let assigned = free.iter().map(|symbol| result[*symbol]).sum::<Decimal>();
|
||||
let missing = (free_total - assigned).max(Decimal::ZERO);
|
||||
let recipients = if promoted.is_empty() { &free } else { &promoted };
|
||||
if !recipients.is_empty() {
|
||||
let addition = missing / Decimal::from(recipients.len() as u64);
|
||||
for symbol in recipients { *result.entry((*symbol).clone()).or_default() += addition; }
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub(super) fn valuation(
|
||||
symbol: &str,
|
||||
quotes: &HashMap<String, &MarketSnapshot>,
|
||||
@@ -42,6 +91,48 @@ pub(super) fn valuation(
|
||||
.ok_or_else(|| format!("{symbol} confirmed holding valuation missing"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod ratio_tests {
|
||||
use super::*;
|
||||
fn configuration() -> (Vec<String>, StockPoolDecisionConstraints, BTreeMap<String,i32>) {
|
||||
let symbols: Vec<String> = vec!["000001.SZ".into(),"000002.SZ".into(),"000003.SZ".into()];
|
||||
let paused = FrozenStockPoolPosition { trade_date: NaiveDate::from_ymd_opt(2026,9,3).unwrap(), reason:"paused".into(), valuation_price:Decimal::from(10) };
|
||||
let constraints = StockPoolDecisionConstraints { frozen_positions:BTreeMap::from([(symbols[0].clone(),paused)]),
|
||||
prior_target_weights:BTreeMap::from([(symbols[0].clone(),3334)]), ..Default::default() };
|
||||
let display = BTreeMap::from([(symbols[0].clone(),3334),(symbols[1].clone(),3333),(symbols[2].clone(),3333)]);
|
||||
(symbols,constraints,display)
|
||||
}
|
||||
#[test]
|
||||
fn precise_paused_budget_survives_replacement_and_zero_targets() {
|
||||
let (symbols,mut constraints,display)=configuration();
|
||||
let third=Decimal::ONE/Decimal::from(3);
|
||||
constraints.prior_target_weight_ratios.insert(symbols[0].clone(),third);
|
||||
let mut active=symbols.clone();active[2]="000004.SZ".into();
|
||||
let ratios=sizing_ratios(&symbols,&active,&BTreeMap::new(),&constraints,0,&display).unwrap();
|
||||
assert_eq!(ratios[&symbols[0]],third);
|
||||
assert_eq!(ratios[&symbols[1]],third);
|
||||
assert!((ratios["000004.SZ"]-third).abs()<Decimal::new(1,24));
|
||||
assert!(!ratios.contains_key(&symbols[2]));
|
||||
assert!((ratios.values().copied().sum::<Decimal>()-Decimal::ONE).abs()<Decimal::new(1,24));
|
||||
}
|
||||
#[test]
|
||||
fn legacy_paused_and_explicit_partial_budgets_are_not_reinterpreted() {
|
||||
let (symbols,constraints,display)=configuration();
|
||||
let ratios=sizing_ratios(&symbols,&symbols,&BTreeMap::new(),&constraints,0,&display).unwrap();
|
||||
assert_eq!(ratios[&symbols[0]],Decimal::new(3334,4));
|
||||
assert!((ratios[&symbols[1]]-Decimal::new(3333,4)).abs()<Decimal::new(1,24));
|
||||
let partial=BTreeMap::from([(symbols[0].clone(),2000),(symbols[1].clone(),0)]);
|
||||
assert_eq!(sizing_ratios(&symbols,&symbols,&partial,&constraints,0,&partial).unwrap(),
|
||||
BTreeMap::from([(symbols[0].clone(),Decimal::new(2,1)),(symbols[1].clone(),Decimal::ZERO)]));
|
||||
}
|
||||
#[test]
|
||||
fn precision_is_checked_before_frozen_budget_is_allocated() {
|
||||
let (symbols,mut constraints,display)=configuration();
|
||||
constraints.prior_target_weight_ratios.insert(symbols[0].clone(),Decimal::new(1001,3));
|
||||
assert!(sizing_ratios(&symbols,&symbols,&BTreeMap::new(),&constraints,0,&display).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn weights(
|
||||
original: &[String],
|
||||
active: &[String],
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
//! Configurable index-to-market-cap band. Values are CNY, not implicit yi.
|
||||
use chrono::NaiveDate;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct IndexMarketCapPolicy {
|
||||
pub schema_version: u32,
|
||||
pub index_code: String,
|
||||
pub field: String,
|
||||
pub value_unit: String,
|
||||
pub index_low: f64,
|
||||
pub index_high: f64,
|
||||
pub lower_at_low: f64,
|
||||
pub lower_at_high: f64,
|
||||
pub upper_at_low: f64,
|
||||
pub upper_at_high: f64,
|
||||
}
|
||||
|
||||
impl IndexMarketCapPolicy {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.schema_version != 1 || self.value_unit != "CNY"
|
||||
|| !matches!(self.field.as_str(), "market_cap" | "float_market_cap")
|
||||
{ return Err("index_market_cap_contract_invalid".into()); }
|
||||
let index = self.index_code.split_once('.').is_some_and(|(code, exchange)| {
|
||||
(6..=12).contains(&code.len())
|
||||
&& code.bytes().all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit())
|
||||
&& matches!(exchange, "SH" | "SZ" | "CSI" | "CNI")
|
||||
});
|
||||
if !index { return Err("index_market_cap_index_invalid".into()); }
|
||||
if [self.index_low,self.index_high,self.lower_at_low,self.lower_at_high,self.upper_at_low,self.upper_at_high]
|
||||
.iter().any(|value| !value.is_finite() || *value <= 0.)
|
||||
|| self.index_low >= self.index_high || self.lower_at_low > self.upper_at_low
|
||||
|| self.lower_at_high > self.upper_at_high
|
||||
{ return Err("index_market_cap_bounds_invalid".into()); }
|
||||
Ok(())
|
||||
}
|
||||
pub fn band(&self, close: f64) -> Result<(f64, f64), String> {
|
||||
self.validate()?;
|
||||
if !close.is_finite() || close <= 0. { return Err("index_market_cap_close_invalid".into()); }
|
||||
let t = (close.clamp(self.index_low,self.index_high) - self.index_low) / (self.index_high-self.index_low);
|
||||
Ok((self.lower_at_low + t*(self.lower_at_high-self.lower_at_low),
|
||||
self.upper_at_low + t*(self.upper_at_high-self.upper_at_low)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct IndexMarketCapRow { pub date: NaiveDate, pub close: f64 }
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Request {
|
||||
pub policy: IndexMarketCapPolicy,
|
||||
pub official_dates: Vec<NaiveDate>,
|
||||
pub index_code: String,
|
||||
pub closes: Vec<IndexMarketCapRow>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||
pub struct Band { pub date: NaiveDate, pub index_close: f64, pub lower: f64, pub upper: f64 }
|
||||
|
||||
pub fn implementation_sha256() -> String { format!("{:x}", Sha256::digest(include_bytes!("stock_pool_market_cap.rs"))) }
|
||||
|
||||
pub fn evaluate(input: &Request) -> Result<Vec<Band>, String> {
|
||||
input.policy.validate()?;
|
||||
if input.index_code != input.policy.index_code || input.official_dates.is_empty()
|
||||
|| input.official_dates.len() > 4000 || input.official_dates.len() != input.closes.len()
|
||||
|| input.official_dates.windows(2).any(|pair| pair[0]>=pair[1])
|
||||
|| input.closes.iter().zip(&input.official_dates).any(|(row, day)| row.date != *day)
|
||||
{ return Err("index_market_cap_calendar_or_identity_mismatch".into()); }
|
||||
input.closes.iter().map(|row| {
|
||||
let (lower,upper)=input.policy.band(row.close)?;
|
||||
Ok(Band{date:row.date,index_close:row.close,lower,upper})
|
||||
}).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
fn policy()->IndexMarketCapPolicy {
|
||||
serde_json::from_value(serde_json::json!({"schema_version":1,"index_code":"000300.SH","field":"market_cap","value_unit":"CNY",
|
||||
"index_low":4000,"index_high":6000,"lower_at_low":2000000000_f64,"lower_at_high":3000000000_f64,
|
||||
"upper_at_low":5000000000_f64,"upper_at_high":8000000000_f64})).unwrap()
|
||||
}
|
||||
#[test]
|
||||
fn interpolates_declared_endpoints_and_clamps_without_business_defaults(){
|
||||
assert_eq!(policy().band(3000.).unwrap(),(2e9,5e9));
|
||||
assert_eq!(policy().band(5000.).unwrap(),(2.5e9,6.5e9));
|
||||
assert_eq!(policy().band(7000.).unwrap(),(3e9,8e9));
|
||||
let mut decreasing=policy();decreasing.lower_at_low=3e9;decreasing.lower_at_high=2e9;
|
||||
assert_eq!(decreasing.band(5000.).unwrap(),(2.5e9,6.5e9));
|
||||
assert!(policy().band(f64::NAN).is_err());
|
||||
let mut invalid=policy();invalid.value_unit="亿元".into();assert!(invalid.validate().is_err());
|
||||
invalid=policy();invalid.index_high=4000.;assert!(invalid.validate().is_err());
|
||||
invalid=policy();invalid.lower_at_low=9e9;assert!(invalid.validate().is_err());
|
||||
}
|
||||
#[test]
|
||||
fn missing_duplicate_or_mismatched_index_inputs_do_not_shrink_the_calendar(){
|
||||
let day=NaiveDate::from_ymd_opt(2026,9,11).unwrap();
|
||||
let mut input=Request{policy:policy(),official_dates:vec![day],index_code:"000300.SH".into(),closes:vec![IndexMarketCapRow{date:day,close:5000.}]};
|
||||
assert_eq!(evaluate(&input).unwrap()[0].lower,2.5e9);
|
||||
input.official_dates.push(day);assert!(evaluate(&input).is_err());input.official_dates.pop();
|
||||
input.index_code="932000.CSI".into();assert!(evaluate(&input).is_err());
|
||||
input.index_code="000300.SH".into();input.closes.clear();assert!(evaluate(&input).is_err());
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,14 @@ pub struct StockPoolEntryProgress {
|
||||
pub completion_quantity: Option<Decimal>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct StockPoolPositionActionBasis {
|
||||
pub generation: String,
|
||||
pub first_execution_date: NaiveDate,
|
||||
pub quantity: Decimal,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct StockPoolExecutionState {
|
||||
@@ -32,13 +40,20 @@ pub struct StockPoolExecutionState {
|
||||
pub entries: BTreeMap<String, StockPoolEntryProgress>,
|
||||
#[serde(default)]
|
||||
pub last_target_weights: BTreeMap<String, i32>,
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub last_target_weight_ratios: BTreeMap<String, Decimal>,
|
||||
/// First signal excluding an actually held member; not an acquisition date.
|
||||
pub removed_since: BTreeMap<String, NaiveDate>,
|
||||
/// Signal progress, not a fill or holding-period fact. Kept across retries
|
||||
/// and later execution sessions until a new generation supersedes it.
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub position_action_bases: BTreeMap<String, StockPoolPositionActionBasis>,
|
||||
}
|
||||
|
||||
pub struct StockPoolGoalObservation<'a> {
|
||||
pub symbol: &'a str,
|
||||
pub target_weight_bps: i32,
|
||||
pub target_weight_ratio: Option<Decimal>,
|
||||
pub target_value: Decimal,
|
||||
pub current_quantity: Decimal,
|
||||
pub target_quantity: Decimal,
|
||||
@@ -52,16 +67,20 @@ impl Default for StockPoolExecutionState {
|
||||
last_execution_date: None,
|
||||
entries: BTreeMap::new(),
|
||||
last_target_weights: BTreeMap::new(),
|
||||
last_target_weight_ratios: BTreeMap::new(),
|
||||
removed_since: BTreeMap::new(),
|
||||
position_action_bases: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StockPoolExecutionState {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.schema_version != 1
|
||||
if !matches!(self.schema_version, 1 | 2)
|
||||
|| (self.schema_version == 1 && !self.last_target_weight_ratios.is_empty())
|
||||
|| self.entries.len() > 10000
|
||||
|| self.removed_since.len() > 10000
|
||||
|| self.position_action_bases.len() > 10000
|
||||
{
|
||||
return Err("stock_pool_execution_state_invalid_schema_or_size".into());
|
||||
}
|
||||
@@ -70,6 +89,8 @@ impl StockPoolExecutionState {
|
||||
.keys()
|
||||
.chain(self.removed_since.keys())
|
||||
.chain(self.last_target_weights.keys())
|
||||
.chain(self.last_target_weight_ratios.keys())
|
||||
.chain(self.position_action_bases.keys())
|
||||
{
|
||||
if normalize_stock_symbol(symbol).as_ref() != Some(symbol) {
|
||||
return Err("stock_pool_execution_state_invalid_symbol".into());
|
||||
@@ -83,6 +104,9 @@ impl StockPoolExecutionState {
|
||||
{
|
||||
return Err("stock_pool_execution_state_invalid_weights".into());
|
||||
}
|
||||
if self.last_target_weight_ratios.len() > 10000 || self.last_target_weight_ratios.iter().any(|(symbol, ratio)| {
|
||||
*ratio < Decimal::ZERO || *ratio > Decimal::ONE || !self.last_target_weights.contains_key(symbol)
|
||||
}) { return Err("stock_pool_execution_state_invalid_weight_ratios".into()); }
|
||||
if self.entries.values().any(|entry| {
|
||||
entry.latest_target_value < Decimal::ZERO
|
||||
|| entry.completion_quantity.is_some_and(|quantity| quantity <= Decimal::ZERO)
|
||||
@@ -97,6 +121,12 @@ impl StockPoolExecutionState {
|
||||
{
|
||||
return Err("stock_pool_execution_state_invalid_goal_or_clock".into());
|
||||
}
|
||||
if self.position_action_bases.values().any(|basis| {
|
||||
basis.generation.trim().is_empty() || basis.quantity <= Decimal::ZERO
|
||||
|| self.last_execution_date.is_none_or(|date| basis.first_execution_date > date)
|
||||
}) {
|
||||
return Err("stock_pool_execution_state_invalid_action_basis".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -138,6 +168,7 @@ impl StockPoolExecutionState {
|
||||
});
|
||||
next.last_target_weights
|
||||
.retain(|symbol, _| members.contains(symbol) || held.contains(symbol));
|
||||
next.last_target_weight_ratios.retain(|symbol, _| members.contains(symbol) || held.contains(symbol));
|
||||
for (symbol, entry) in &mut next.entries {
|
||||
entry.observed_holding |= held.contains(symbol);
|
||||
if entry.pending
|
||||
@@ -186,15 +217,76 @@ impl StockPoolExecutionState {
|
||||
self.record_targets(
|
||||
decision_date,
|
||||
generation,
|
||||
plan.rows.iter().map(|row| StockPoolGoalObservation {
|
||||
plan.rows.iter().filter(|row| !plan.position_action_bases.contains_key(&row.symbol)).map(|row| StockPoolGoalObservation {
|
||||
symbol: &row.symbol,
|
||||
target_weight_bps: row.target_weight_bps,
|
||||
target_weight_ratio: plan.target_weight_ratios.get(&row.symbol).copied(),
|
||||
target_value: row.target_value,
|
||||
current_quantity: row.current_quantity,
|
||||
target_quantity: row.target_quantity,
|
||||
status: &row.status,
|
||||
}),
|
||||
)
|
||||
)?.record_position_action_bases(generation, &plan.position_action_bases)
|
||||
}
|
||||
|
||||
pub fn position_action_bases_for(&self, generation: &str) -> BTreeMap<String, Decimal> {
|
||||
self.position_action_bases.iter()
|
||||
.filter(|(_, basis)| basis.generation == generation)
|
||||
.map(|(symbol, basis)| (symbol.clone(), basis.quantity))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// A verified split changes the share unit, not the intended reduction or
|
||||
/// entry completion. Never infer a split from a changed holding quantity.
|
||||
pub fn adjust_for_split(&self, symbol: &str, ratio: Decimal) -> Result<Self, String> {
|
||||
self.validate()?;
|
||||
if ratio <= Decimal::ZERO || normalize_stock_symbol(symbol).as_deref() != Some(symbol) {
|
||||
return Err("stock_pool_execution_state_split_invalid".into());
|
||||
}
|
||||
let scale = |quantity: Decimal| quantity.checked_mul(ratio)
|
||||
.map(|value| value.round_dp_with_strategy(0, rust_decimal::RoundingStrategy::MidpointAwayFromZero))
|
||||
.ok_or_else(|| "stock_pool_execution_state_split_overflow".to_string());
|
||||
let mut next = self.clone();
|
||||
if let Some(entry) = next.entries.get_mut(symbol) {
|
||||
if let Some(quantity) = entry.completion_quantity {
|
||||
let quantity = scale(quantity)?;
|
||||
entry.completion_quantity = (quantity > Decimal::ZERO).then_some(quantity);
|
||||
}
|
||||
}
|
||||
if let Some(basis) = next.position_action_bases.get_mut(symbol) {
|
||||
basis.quantity = scale(basis.quantity)?;
|
||||
if basis.quantity == Decimal::ZERO { next.position_action_bases.remove(symbol); }
|
||||
}
|
||||
next.validate()?;
|
||||
Ok(next)
|
||||
}
|
||||
|
||||
pub fn record_position_action_bases(
|
||||
&self,
|
||||
generation: &str,
|
||||
quantities: &BTreeMap<String, Decimal>,
|
||||
) -> Result<Self, String> {
|
||||
self.validate()?;
|
||||
if generation.trim().is_empty() {
|
||||
return Err("stock_pool_execution_state_action_generation_missing".into());
|
||||
}
|
||||
let first_execution_date = self.last_execution_date
|
||||
.ok_or("stock_pool_execution_state_action_clock_missing")?;
|
||||
let mut next = self.clone();
|
||||
next.position_action_bases.retain(|_, basis| basis.generation == generation);
|
||||
for (symbol, quantity) in quantities {
|
||||
if let Some(basis) = next.position_action_bases.get(symbol) {
|
||||
if basis.quantity != *quantity {
|
||||
return Err(format!("stock_pool_execution_state_action_basis_changed:{symbol}"));
|
||||
}
|
||||
} else {
|
||||
next.position_action_bases.insert(symbol.clone(), StockPoolPositionActionBasis {
|
||||
generation: generation.into(), first_execution_date, quantity: *quantity,
|
||||
});
|
||||
}
|
||||
}
|
||||
next.validate()?;
|
||||
Ok(next)
|
||||
}
|
||||
|
||||
pub fn record_targets<'a>(
|
||||
@@ -213,9 +305,18 @@ impl StockPoolExecutionState {
|
||||
}
|
||||
let mut next = self.clone();
|
||||
for row in rows {
|
||||
if row.status == "AUTOMATIC_TRADE_PROTECTED" {
|
||||
continue;
|
||||
}
|
||||
if row.target_weight_bps > 0 {
|
||||
next.last_target_weights
|
||||
.insert(row.symbol.into(), row.target_weight_bps);
|
||||
if let Some(ratio) = row.target_weight_ratio {
|
||||
next.schema_version = 2;
|
||||
next.last_target_weight_ratios.insert(row.symbol.into(), ratio);
|
||||
} else {
|
||||
next.last_target_weight_ratios.remove(row.symbol);
|
||||
}
|
||||
}
|
||||
let eligible = row.target_weight_bps > 0 && row.target_value > Decimal::ZERO;
|
||||
let completion_quantity = (row.status == "READY"
|
||||
|
||||
@@ -20,6 +20,18 @@ use crate::universe::{DynamicMarketCapBandSelector, SelectionContext, UniverseSe
|
||||
|
||||
pub trait Strategy {
|
||||
fn name(&self) -> &str;
|
||||
fn bind_runtime_position_configuration(
|
||||
&mut self,
|
||||
events: &[crate::position_exposure::PositionExposureEvent],
|
||||
legacy: &BTreeMap<NaiveDate, i32>,
|
||||
) -> Result<(), BacktestError> {
|
||||
if !events.is_empty() || !legacy.is_empty() {
|
||||
return Err(BacktestError::Execution(
|
||||
"strategy does not implement runtime position configuration".into(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn initial_subscriptions(&self) -> BTreeSet<String> {
|
||||
BTreeSet::new()
|
||||
}
|
||||
@@ -40,6 +52,12 @@ pub trait Strategy {
|
||||
) -> Result<(), BacktestError> {
|
||||
Ok(())
|
||||
}
|
||||
/// External, already executed manual activity. It is not a new strategy
|
||||
/// order and must not be run through order generation or transaction costs.
|
||||
fn on_observed_manual_execution(
|
||||
&mut self,
|
||||
_execution: &crate::manual_execution::ManualReplayApplication,
|
||||
) -> Result<(), BacktestError> { Ok(()) }
|
||||
fn schedule_rules(&self) -> Vec<ScheduleRule> {
|
||||
Vec::new()
|
||||
}
|
||||
@@ -102,6 +120,7 @@ pub struct OpenOrderView {
|
||||
pub avg_price: f64,
|
||||
pub transaction_cost: f64,
|
||||
pub limit_price: f64,
|
||||
pub reserved_cash: Option<f64>,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
@@ -497,6 +516,7 @@ impl StrategyContext<'_> {
|
||||
.iter()
|
||||
.filter(|order| order.side == OrderSide::Buy)
|
||||
.map(|order| {
|
||||
if let Some(reserved) = order.reserved_cash { return reserved; }
|
||||
let price = if order.limit_price.is_finite() {
|
||||
order.limit_price.max(0.0)
|
||||
} else {
|
||||
@@ -685,7 +705,7 @@ impl StrategyContext<'_> {
|
||||
self.data.get_price(symbol, start, end, frequency)
|
||||
}
|
||||
|
||||
pub fn get_dividend(&self, symbol: &str, start: NaiveDate) -> Vec<DividendRecord> {
|
||||
pub fn get_dividend(&self, symbol: &str, start: NaiveDate) -> Result<Vec<DividendRecord>, BacktestError> {
|
||||
let current_data_date = self.current_data_date();
|
||||
let end = self
|
||||
.data
|
||||
@@ -975,7 +995,7 @@ fn safe_ratio(numerator: f64, denominator: f64) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[derive(Debug, Clone, Default, serde::Serialize)]
|
||||
pub struct StrategyDecision {
|
||||
pub buy_denials: BTreeMap<String, String>,
|
||||
pub rebalance: bool,
|
||||
@@ -988,6 +1008,15 @@ pub struct StrategyDecision {
|
||||
}
|
||||
|
||||
impl StrategyDecision {
|
||||
pub(crate) fn is_portfolio_target_only(&self) -> bool {
|
||||
(self.rebalance && self.order_intents.is_empty())
|
||||
|| (self.order_intents.len() == 1
|
||||
&& matches!(
|
||||
self.order_intents[0].unwrapped(),
|
||||
OrderIntent::StockPool { .. } | OrderIntent::TargetPortfolioSmart { .. }
|
||||
))
|
||||
}
|
||||
|
||||
pub fn potential_buy_symbols(&self, open_orders: &[OpenOrderView]) -> BTreeSet<String> {
|
||||
let mut symbols = BTreeSet::new();
|
||||
if self.rebalance {
|
||||
@@ -1001,9 +1030,24 @@ impl StrategyDecision {
|
||||
}
|
||||
|
||||
pub fn merge_from(&mut self, mut other: StrategyDecision) {
|
||||
if self.is_portfolio_target_only() && other.is_portfolio_target_only() {
|
||||
let mut previous = std::mem::replace(self, other);
|
||||
previous
|
||||
.diagnostics
|
||||
.push("unsubmitted_portfolio_target_superseded".into());
|
||||
self.notes.splice(0..0, previous.notes);
|
||||
self.diagnostics.splice(0..0, previous.diagnostics);
|
||||
return;
|
||||
}
|
||||
self.buy_denials.append(&mut other.buy_denials);
|
||||
self.rebalance |= other.rebalance;
|
||||
self.target_weights.append(&mut other.target_weights);
|
||||
if other.rebalance {
|
||||
// Rebalance targets are a complete portfolio, not an additive
|
||||
// list. A newer unsent target replaces the earlier allocation.
|
||||
self.rebalance = true;
|
||||
self.target_weights = std::mem::take(&mut other.target_weights);
|
||||
} else {
|
||||
self.target_weights.append(&mut other.target_weights);
|
||||
}
|
||||
self.exit_symbols.append(&mut other.exit_symbols);
|
||||
self.order_intents.append(&mut other.order_intents);
|
||||
self.notes.append(&mut other.notes);
|
||||
@@ -1023,13 +1067,59 @@ impl StrategyDecision {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[cfg(test)]
|
||||
mod decision_merge_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn newer_complete_target_replaces_old_symbols_without_discarding_explicit_actions() {
|
||||
let mut earlier = StrategyDecision {
|
||||
rebalance: true,
|
||||
target_weights: BTreeMap::from([("A".into(), 0.5), ("B".into(), 0.5)]),
|
||||
exit_symbols: BTreeSet::from(["risk_exit".into()]),
|
||||
order_intents: vec![OrderIntent::Shares {
|
||||
symbol: "explicit".into(),
|
||||
quantity: 100,
|
||||
reason: "explicit action".into(),
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
earlier.merge_from(StrategyDecision {
|
||||
rebalance: true,
|
||||
target_weights: BTreeMap::from([("C".into(), 1.)]),
|
||||
..Default::default()
|
||||
});
|
||||
assert_eq!(earlier.target_weights, BTreeMap::from([("C".into(), 1.)]));
|
||||
assert!(earlier.rebalance);
|
||||
assert!(earlier.exit_symbols.contains("risk_exit"));
|
||||
assert_eq!(earlier.order_intents.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_empty_complete_target_replaces_old_allocation_but_empty_callback_does_not() {
|
||||
let mut decision = StrategyDecision {
|
||||
rebalance: true,
|
||||
target_weights: BTreeMap::from([("A".into(), 1.)]),
|
||||
..Default::default()
|
||||
};
|
||||
decision.merge_from(StrategyDecision::default());
|
||||
assert_eq!(decision.target_weights.len(), 1);
|
||||
decision.merge_from(StrategyDecision {
|
||||
rebalance: true,
|
||||
..Default::default()
|
||||
});
|
||||
assert!(decision.target_weights.is_empty());
|
||||
assert!(decision.rebalance);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
|
||||
pub enum AlgoOrderStyle {
|
||||
Vwap,
|
||||
Twap,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
|
||||
pub enum OrderTimeInForce {
|
||||
Day,
|
||||
Ioc,
|
||||
@@ -1058,7 +1148,7 @@ impl OrderTimeInForce {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub enum TargetPortfolioOrderPricing {
|
||||
LimitPrices(BTreeMap<String, f64>),
|
||||
AlgoOrder {
|
||||
@@ -1068,7 +1158,7 @@ pub enum TargetPortfolioOrderPricing {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub enum OrderIntent {
|
||||
StockPool {
|
||||
contract: Box<crate::stock_pool_execution::FrozenStockPoolIntent>,
|
||||
@@ -3184,7 +3274,7 @@ mod tests {
|
||||
.enumerate()
|
||||
.map(|(index, date)| DailyMarketSnapshot {
|
||||
date: *date,
|
||||
symbol: symbol.to_string(),
|
||||
symbol: symbol.into(),
|
||||
timestamp: Some(format!("{date} 10:18:00")),
|
||||
day_open: 10.0 + index as f64,
|
||||
open: 10.0 + index as f64,
|
||||
@@ -3210,21 +3300,21 @@ mod tests {
|
||||
.iter()
|
||||
.map(|date| DailyFactorSnapshot {
|
||||
date: *date,
|
||||
symbol: symbol.to_string(),
|
||||
symbol: symbol.into(),
|
||||
market_cap_bn: 10.0,
|
||||
free_float_cap_bn: 9.0,
|
||||
pe_ttm: 12.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
extra_factors: Default::default(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let candidate_rows = dates
|
||||
.iter()
|
||||
.map(|date| CandidateEligibility {
|
||||
date: *date,
|
||||
symbol: symbol.to_string(),
|
||||
symbol: symbol.into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
|
||||
@@ -254,7 +254,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
},
|
||||
ManualSection {
|
||||
title: "corporate_actions.dividend_reinvestment".to_string(),
|
||||
detail: "支持 corporate_actions.dividend_reinvestment(true)。开启后,现金分红到账会优先按 round lot 回补成同一只股票,零头保留为现金。".to_string(),
|
||||
detail: "历史兼容的回测账务再投模型:corporate_actions.dividend_reinvestment(true) 在分红结算时按调整后的参考价分配整手股数,零头留现金,费用为0;来源标记为 dividend_reinvestment,不是交易所委托或真实自动买入。新策略不应手工处理公司行为。".to_string(),
|
||||
},
|
||||
ManualSection {
|
||||
title: "execution.matching_type / execution.slippage".to_string(),
|
||||
@@ -362,7 +362,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
|
||||
ManualFunction { name: "get_trading_dates/get_previous_trading_date/get_next_trading_date".to_string(), signature: "ctx.get_previous_trading_date(date, n)".to_string(), detail: "交易日历 API。get_trading_dates 返回闭区间交易日;previous/next 返回相对某日向前或向后的第 n 个交易日,当前日自身不计入。".to_string() },
|
||||
ManualFunction { name: "is_suspended/is_st_stock".to_string(), signature: "ctx.is_suspended(symbol, count)".to_string(), detail: "读取指定证券截至当前信号日最近 count 个交易日的停牌或 ST 标记,返回 bool 序列,顺序从旧到新;对应平台内核的 is_suspended/is_st_stock 数据能力。执行日停牌或 ST 只能由撮合风控判断,不能在 next_bar_open 的 T 日提前固化。".to_string() },
|
||||
ManualFunction { name: "get_price".to_string(), signature: "ctx.get_price(symbol, start_date, end_date, \"1d\" | \"1m\")".to_string(), detail: "按日期区间读取统一 PriceBar 序列。日线返回 open/high/low/close/last/volume/盘口字段;分钟线返回按 timestamp 排序的 last/bid1/ask1/volume_delta/amount_delta 映射,便于服务层转成表格或前端明细。".to_string() },
|
||||
ManualFunction { name: "get_dividend / dividend_cash / has_dividend".to_string(), signature: "dividend_cash(lookback) / has_dividend(lookback)".to_string(), detail: "高级数据 风格分红 API。Rust Context 可用 ctx.get_dividend(symbol, start_date) 读取明细;平台表达式可用 dividend_cash(lookback) 汇总当前股票最近 N 个交易日现金分红,用 has_dividend(lookback) 判断是否发生分红,也支持 dividend_cash(\"600000.SH\", lookback)。".to_string() },
|
||||
ManualFunction { name: "get_dividend / dividend_cash / has_dividend".to_string(), signature: "dividend_cash(lookback) / has_dividend(lookback)".to_string(), detail: "高级数据 风格分红 API。Rust Context 可用 ctx.get_dividend(symbol, start_date)? 读取明细;缺少真实付款日或付款日早于除息日时返回错误,不推定到账日期。平台表达式可用 dividend_cash(lookback) 汇总当前股票最近 N 个交易日现金分红,用 has_dividend(lookback) 判断是否发生分红,也支持 dividend_cash(\"600000.SH\", lookback)。".to_string() },
|
||||
ManualFunction { name: "get_split / split_ratio / has_split".to_string(), signature: "split_ratio(lookback) / has_split(lookback)".to_string(), detail: "高级数据 风格拆分/送转 API。Rust Context 可用 ctx.get_split(symbol, start_date) 读取明细;平台表达式可用 split_ratio(lookback) 计算当前股票最近 N 个交易日累计拆分比例,has_split(lookback) 判断是否发生送转。".to_string() },
|
||||
ManualFunction { name: "get_factor / factor_value".to_string(), signature: "factor_value(\"field\", lookback=1)".to_string(), detail: "数值因子 API。factor(\"field\") 读取当前股票当日因子;factor_value(\"field\", lookback) 会在最近 N 个交易日内取该字段最新数值,适合读取任意可用指标或自定义数值因子。Rust Context 可用 ctx.get_factor(symbol, start, end, field) 读取完整数值序列。".to_string() },
|
||||
ManualFunction { name: "get_factor_text / factor_text".to_string(), signature: "factor_text(\"field\", lookback=1)".to_string(), detail: "字符串因子 API。读取最近 N 个交易日内指定字段的最新字符串值,适合行业名称、概念、标签、风格分类、自定义文本信号等字段。示例:factor_text(\"concept\") == \"ai_chip\" 或 factor(\"industry_name\") == \"电子\"。Rust Context 可用 ctx.get_factor_text(symbol, start, end, field) 读取完整字符串序列。".to_string() },
|
||||
|
||||
@@ -83,7 +83,7 @@ impl SelectionContext<'_> {
|
||||
for factor in self.data.factor_snapshot_rows_on(self.decision_date) {
|
||||
if self
|
||||
.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;
|
||||
}
|
||||
@@ -307,7 +307,7 @@ mod tests {
|
||||
fn market(symbol: &str, price: f64) -> DailyMarketSnapshot {
|
||||
DailyMarketSnapshot {
|
||||
date: d(),
|
||||
symbol: symbol.to_string(),
|
||||
symbol: symbol.into(),
|
||||
timestamp: Some("2025-01-02 10:00:00".to_string()),
|
||||
day_open: price,
|
||||
open: price,
|
||||
@@ -333,7 +333,7 @@ mod tests {
|
||||
fn factor(symbol: &str, market_cap_bn: f64) -> DailyFactorSnapshot {
|
||||
DailyFactorSnapshot {
|
||||
date: d(),
|
||||
symbol: symbol.to_string(),
|
||||
symbol: symbol.into(),
|
||||
market_cap_bn,
|
||||
free_float_cap_bn: market_cap_bn,
|
||||
pe_ttm: 10.0,
|
||||
@@ -347,7 +347,7 @@ mod tests {
|
||||
fn candidate(symbol: &str, is_st: bool, is_kcb: bool) -> CandidateEligibility {
|
||||
CandidateEligibility {
|
||||
date: d(),
|
||||
symbol: symbol.to_string(),
|
||||
symbol: symbol.into(),
|
||||
is_st,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
|
||||
@@ -105,8 +105,164 @@ fn action(quantity: &str, when: &str) -> PlatformTradeAction {
|
||||
reason: "configured_strategy_action".into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observed_manual_trades_then_split_keep_real_fill_protection_and_lock_dates() {
|
||||
for sell_during_lock in [false, true] {
|
||||
let sale = if sell_during_lock {
|
||||
("manual-sell", "Sell", "2026-09-16T01:31:00Z", "2026-09-16T01:31:01Z", "5", "0.5", 200)
|
||||
} else {
|
||||
("manual-sell", "Sell", "2026-09-14T01:31:00Z", "2026-09-14T01:31:01Z", "10", "0.5", 100)
|
||||
};
|
||||
let actions = [
|
||||
("new-buy", "Buy", "2026-09-14T01:30:00Z", "2026-09-14T01:30:01Z", "10", "0.25", 100),
|
||||
("late-buy", "Buy", "2026-09-11T06:00:00Z", "2026-09-14T01:30:02Z", "10", "0.75", 100),
|
||||
sale,
|
||||
].into_iter().enumerate().map(|(index, (id, side, executed, observed, price, fee, quantity))| {
|
||||
let executed: chrono::DateTime<chrono::Utc> = executed.parse().unwrap();
|
||||
let observed: chrono::DateTime<chrono::Utc> = observed.parse().unwrap();
|
||||
let created = executed - chrono::Duration::seconds(1);
|
||||
serde_json::json!({"actionId":id,"source":"manual_security_trade","auditEventIds":[format!("audit-{id}")],
|
||||
"confirmedAt":created,"confirmationObservedAt":created,"outcome":"orders_terminal","orders":[{
|
||||
"orderId":id,"brokerOrderId":id,"sourceAdapter":"paper","symbol":"000001.SZ","side":side,"quantity":quantity,
|
||||
"orderCreatedAt":created,"terminalObservedAt":observed,"terminalStatus":"filled","fills":[{
|
||||
"tradeId":id,"observationEventId":id,"observationSequence":index+1,
|
||||
"tradeDate":executed.date_naive(),"executedAt":executed,"observedAt":observed,
|
||||
"feeObservationEventId":id,"feeObservationSequence":index+1,"feeObservedAt":observed,
|
||||
"timestampPrecision":"second","quantity":quantity,"price":price,"totalFee":fee
|
||||
}]
|
||||
}]})
|
||||
}).collect::<Vec<_>>();
|
||||
let mut replay: fidc_core::manual_execution::ManualExecutionReplay = serde_json::from_value(serde_json::json!({
|
||||
"schema":fidc_core::manual_execution::MANUAL_REPLAY_SCHEMA,"runtimeId":"r","accountId":"a",
|
||||
"sourceContractSha256":"a".repeat(64),"contentSha256":"","observationCutoff":"2026-09-18T08:00:00Z","actions":actions,
|
||||
})).unwrap();
|
||||
replay.content_sha256 = replay.content_digest().unwrap();
|
||||
let mut parts = data().snapshot_components();
|
||||
for row in &mut parts.market {
|
||||
if row.date >= d(15) {
|
||||
row.day_open = 5.;
|
||||
row.open = 5.;
|
||||
row.high = 5.;
|
||||
row.low = 5.;
|
||||
row.close = 5.;
|
||||
row.last_price = 5.;
|
||||
row.bid1 = 5.;
|
||||
row.ask1 = 5.;
|
||||
row.prev_close = 5.;
|
||||
row.upper_limit = 5.5;
|
||||
row.lower_limit = 4.5;
|
||||
}
|
||||
}
|
||||
parts.corporate_actions.push(fidc_core::CorporateAction {
|
||||
date: d(15),
|
||||
symbol: "000001.SZ".into(),
|
||||
payable_date: None,
|
||||
record_date: Some(d(14)),
|
||||
share_effective_date: Some(d(15)),
|
||||
share_rounding_rule: None,
|
||||
share_cash: 0.,
|
||||
share_bonus: 1.,
|
||||
share_gift: 0.,
|
||||
issue_quantity: 0.,
|
||||
issue_price: 0.,
|
||||
reform: false,
|
||||
adjust_factor: None,
|
||||
successor_symbol: None,
|
||||
successor_ratio: None,
|
||||
successor_cash: None,
|
||||
});
|
||||
let data = DataSet::from_components_with_actions(
|
||||
parts.instruments,
|
||||
parts.market,
|
||||
parts.factors,
|
||||
parts.candidates,
|
||||
parts.benchmarks,
|
||||
parts.corporate_actions,
|
||||
)
|
||||
.unwrap();
|
||||
let mut config = PlatformExprStrategyConfig::generic();
|
||||
config.signal_symbol = "000001.SZ".into();
|
||||
config.benchmark_symbol = "000300.SH".into();
|
||||
config.rotation_enabled = false;
|
||||
config.matching_type = MatchingType::CurrentBarClose;
|
||||
config.volume_capacity_mode =
|
||||
fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
|
||||
config.automatic_trade_protection = AutomaticTradeProtection {
|
||||
buy_protection_days: 3,
|
||||
sell_cooldown_days: 3,
|
||||
max_holding_days: 1,
|
||||
locks: vec![AutomaticTradeLock {
|
||||
symbol: "000001.SZ".into(),
|
||||
start_date: d(16),
|
||||
end_date: Some(d(17)),
|
||||
}],
|
||||
};
|
||||
config.explicit_actions = vec![action("-200", "decision_date >= \"2026-09-14\"")];
|
||||
let result = BacktestEngine::new(
|
||||
data,
|
||||
PlatformExprStrategy::new(config),
|
||||
BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_matching_type(MatchingType::CurrentBarClose)
|
||||
.with_volume_capacity_mode(
|
||||
fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit,
|
||||
),
|
||||
BacktestConfig {
|
||||
initial_cash: 10000.,
|
||||
benchmark_code: "000300.SH".into(),
|
||||
start_date: Some(d(11)),
|
||||
end_date: Some(d(18)),
|
||||
decision_lag_trading_days: 0,
|
||||
execution_price_field: PriceField::Close,
|
||||
},
|
||||
)
|
||||
.with_observed_manual_executions(replay)
|
||||
.unwrap()
|
||||
.run()
|
||||
.unwrap();
|
||||
assert_eq!(result.manual_executions.len(), 3);
|
||||
assert_eq!(result.manual_executions[2].quantity_after, if sell_during_lock { 200 } else { 100 });
|
||||
assert_eq!(result.fills.len(), 1, "{:?}", result.fills);
|
||||
assert_eq!(
|
||||
(
|
||||
result.fills[0].date,
|
||||
result.fills[0].side,
|
||||
result.fills[0].quantity,
|
||||
result.fills[0].price
|
||||
),
|
||||
(d(18), OrderSide::Sell, 200, 5.)
|
||||
);
|
||||
assert!(result.fills[0].reason.contains("max_holding_days_exit"));
|
||||
for day in [14, 15] {
|
||||
for rule in ["buy_fill_protection", "sell_fill_cooldown"] {
|
||||
if rule == "sell_fill_cooldown" && sell_during_lock { continue; }
|
||||
assert!(result.risk_decisions.iter().any(|audit| audit.date == d(day)
|
||||
&& audit.symbol == "000001.SZ" && audit.rule_code == rule && !audit.accepted), "day={day} rule={rule}");
|
||||
}
|
||||
}
|
||||
for day in [16, 17] {
|
||||
assert!(result.risk_decisions.iter().any(|audit| audit.date == d(day)
|
||||
&& audit.symbol == "000001.SZ" && audit.rule_code == "automatic_trade_locked" && !audit.accepted));
|
||||
}
|
||||
assert!(
|
||||
result
|
||||
.daily_holdings
|
||||
.iter()
|
||||
.any(|row| row.date == d(15) && row.quantity == if sell_during_lock { 400 } else { 200 })
|
||||
);
|
||||
assert!(result.holdings_summary.is_empty());
|
||||
assert!(
|
||||
result
|
||||
.equity_curve
|
||||
.iter()
|
||||
.all(|point| point.external_cash_flow == 0.)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn run(policy: AutomaticTradeProtection) -> fidc_core::BacktestResult {
|
||||
let mut config = PlatformExprStrategyConfig::generic();
|
||||
config.volume_capacity_mode = fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
|
||||
config.signal_symbol = "000001.SZ".into();
|
||||
config.benchmark_symbol = "000300.SH".into();
|
||||
config.rotation_enabled = false;
|
||||
@@ -119,7 +275,7 @@ fn run(policy: AutomaticTradeProtection) -> fidc_core::BacktestResult {
|
||||
action("-100", "decision_date >= \"2026-09-14\""),
|
||||
];
|
||||
config.matching_type = MatchingType::CurrentBarClose;
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_matching_type(MatchingType::CurrentBarClose);
|
||||
BacktestEngine::new(
|
||||
data(),
|
||||
@@ -276,6 +432,7 @@ fn locked_holding_keeps_its_slot_even_when_cash_can_buy_the_next_candidate() {
|
||||
)
|
||||
.unwrap();
|
||||
let mut config = PlatformExprStrategyConfig::generic();
|
||||
config.volume_capacity_mode = fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
|
||||
config.signal_symbol = "000001.SZ".into();
|
||||
config.benchmark_symbol = "000300.SH".into();
|
||||
config.strategy_name = "protection_test".into();
|
||||
@@ -294,7 +451,7 @@ fn locked_holding_keeps_its_slot_even_when_cash_can_buy_the_next_candidate() {
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_matching_type(MatchingType::CurrentBarClose);
|
||||
let result = BacktestEngine::new(
|
||||
dataset,
|
||||
|
||||
@@ -15,7 +15,7 @@ fn d(year: i32, month: u32, day: u32) -> NaiveDate {
|
||||
fn candidate() -> CandidateEligibility {
|
||||
CandidateEligibility {
|
||||
date: d(2024, 1, 3),
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
@@ -31,7 +31,7 @@ fn candidate() -> CandidateEligibility {
|
||||
fn snapshot(open: f64, upper_limit: f64, lower_limit: f64) -> DailyMarketSnapshot {
|
||||
DailyMarketSnapshot {
|
||||
date: d(2024, 1, 3),
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
timestamp: Some("2024-01-03 10:18:00".to_string()),
|
||||
day_open: open,
|
||||
open,
|
||||
@@ -256,7 +256,7 @@ fn china_rule_hooks_allow_sell_when_last_price_is_above_lower_limit() {
|
||||
|
||||
let snapshot = DailyMarketSnapshot {
|
||||
date: d(2024, 4, 7),
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
timestamp: Some("2024-04-07 10:18:00".to_string()),
|
||||
day_open: 2.53,
|
||||
open: 2.53,
|
||||
|
||||
@@ -113,7 +113,7 @@ impl Strategy for BuyAndHoldStrategy {
|
||||
fn stock_market_snapshot(date: NaiveDate) -> DailyMarketSnapshot {
|
||||
DailyMarketSnapshot {
|
||||
date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
timestamp: Some(format!("{date} 10:18:00")),
|
||||
day_open: 10.0,
|
||||
open: 10.0,
|
||||
@@ -139,21 +139,21 @@ fn stock_market_snapshot(date: NaiveDate) -> DailyMarketSnapshot {
|
||||
fn stock_factor_snapshot(date: NaiveDate) -> DailyFactorSnapshot {
|
||||
DailyFactorSnapshot {
|
||||
date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
market_cap_bn: 20.0,
|
||||
free_float_cap_bn: 18.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
extra_factors: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn stock_candidate(date: NaiveDate) -> CandidateEligibility {
|
||||
CandidateEligibility {
|
||||
date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
@@ -177,6 +177,160 @@ fn benchmark_snapshot(date: NaiveDate) -> BenchmarkSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successor_conversion_depletes_older_source_lots_before_newer_successor_buys() {
|
||||
struct ConvertedSale {
|
||||
dates: [NaiveDate; 3],
|
||||
seen: std::rc::Rc<std::cell::RefCell<Option<(Option<NaiveDate>, Option<NaiveDate>)>>>,
|
||||
}
|
||||
impl Strategy for ConvertedSale {
|
||||
fn name(&self) -> &str {
|
||||
"successor FIFO"
|
||||
}
|
||||
fn on_day(
|
||||
&mut self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
) -> Result<StrategyDecision, fidc_core::BacktestError> {
|
||||
let (symbol, quantity) = if ctx.execution_date == self.dates[0] {
|
||||
("000001.SZ", 100)
|
||||
} else if ctx.execution_date == self.dates[1] {
|
||||
("000002.SZ", 100)
|
||||
} else {
|
||||
let holding = ctx.portfolio.position("000002.SZ").unwrap();
|
||||
*self.seen.borrow_mut() = Some((holding.opened_date(), holding.last_buy_date()));
|
||||
("000002.SZ", -200)
|
||||
};
|
||||
Ok(StrategyDecision {
|
||||
order_intents: vec![fidc_core::OrderIntent::Shares {
|
||||
symbol: symbol.into(),
|
||||
quantity,
|
||||
reason: "dated lot test".into(),
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
let dates = [d(2026, 9, 11), d(2026, 9, 14), d(2026, 9, 15)];
|
||||
let symbols = ["000001.SZ", "000002.SZ"];
|
||||
let mut market = Vec::new();
|
||||
let mut factors = Vec::new();
|
||||
let mut candidates = Vec::new();
|
||||
for date in dates {
|
||||
for symbol in symbols {
|
||||
let price = if symbol == symbols[0] {
|
||||
10.
|
||||
} else if date == dates[2] {
|
||||
6.
|
||||
} else {
|
||||
20.
|
||||
};
|
||||
let mut quote = stock_market_snapshot(date);
|
||||
quote.symbol = symbol.into();
|
||||
quote.day_open = price;
|
||||
quote.open = price;
|
||||
quote.high = price;
|
||||
quote.low = price;
|
||||
quote.close = price;
|
||||
quote.last_price = price;
|
||||
quote.bid1 = price;
|
||||
quote.ask1 = price;
|
||||
quote.prev_close = price;
|
||||
quote.upper_limit = price * 1.1;
|
||||
quote.lower_limit = price * 0.9;
|
||||
market.push(quote);
|
||||
let mut factor = stock_factor_snapshot(date);
|
||||
factor.symbol = symbol.into();
|
||||
factors.push(factor);
|
||||
let mut candidate = stock_candidate(date);
|
||||
candidate.symbol = symbol.into();
|
||||
candidates.push(candidate);
|
||||
}
|
||||
}
|
||||
let data = DataSet::from_components_with_actions(
|
||||
symbols
|
||||
.into_iter()
|
||||
.map(|symbol| Instrument {
|
||||
symbol: symbol.into(),
|
||||
name: symbol.into(),
|
||||
board: "SZ".into(),
|
||||
round_lot: 100,
|
||||
listed_at: Some(d(2020, 1, 1)),
|
||||
delisted_at: None,
|
||||
status: "active".into(),
|
||||
})
|
||||
.collect(),
|
||||
market,
|
||||
factors,
|
||||
candidates,
|
||||
dates.map(benchmark_snapshot).into(),
|
||||
vec![CorporateAction {
|
||||
date: dates[2],
|
||||
symbol: symbols[0].into(),
|
||||
payable_date: None,
|
||||
record_date: Some(dates[1]),
|
||||
share_effective_date: None,
|
||||
share_rounding_rule: None,
|
||||
share_cash: 0.,
|
||||
share_bonus: 0.,
|
||||
share_gift: 0.,
|
||||
issue_quantity: 0.,
|
||||
issue_price: 0.,
|
||||
reform: false,
|
||||
adjust_factor: None,
|
||||
successor_symbol: Some(symbols[1].into()),
|
||||
successor_ratio: Some(2.),
|
||||
successor_cash: Some(0.),
|
||||
}],
|
||||
)
|
||||
.unwrap();
|
||||
let seen = std::rc::Rc::new(std::cell::RefCell::new(None));
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks,
|
||||
PriceField::Open,
|
||||
)
|
||||
.with_matching_type(fidc_core::MatchingType::NextBarOpen)
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(false);
|
||||
let result = BacktestEngine::new(
|
||||
data,
|
||||
ConvertedSale {
|
||||
dates,
|
||||
seen: seen.clone(),
|
||||
},
|
||||
broker,
|
||||
BacktestConfig {
|
||||
initial_cash: 10000.,
|
||||
benchmark_code: "000300.SH".into(),
|
||||
start_date: Some(dates[0]),
|
||||
end_date: Some(dates[2]),
|
||||
decision_lag_trading_days: 0,
|
||||
execution_price_field: PriceField::Open,
|
||||
},
|
||||
)
|
||||
.run()
|
||||
.unwrap();
|
||||
assert_eq!(*seen.borrow(), Some((Some(dates[0]), Some(dates[1]))));
|
||||
assert_eq!(result.fills.len(), 3);
|
||||
assert_eq!(result.fills[2].quantity, 200);
|
||||
assert_eq!(result.fills[2].symbol, symbols[1]);
|
||||
let remaining = result
|
||||
.holdings_summary
|
||||
.iter()
|
||||
.find(|row| row.symbol == symbols[1])
|
||||
.unwrap();
|
||||
assert_eq!(remaining.quantity, 100);
|
||||
assert_eq!(remaining.realized_pnl, 200.);
|
||||
assert!(
|
||||
result
|
||||
.position_events
|
||||
.iter()
|
||||
.any(|event| event.symbol == symbols[0]
|
||||
&& event.quantity_after == 0
|
||||
&& event.reason.starts_with("successor_conversion"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_reinvests_dividend_receivable_in_round_lots() {
|
||||
let buy_date = d(2025, 1, 1);
|
||||
@@ -195,7 +349,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
||||
vec![
|
||||
DailyMarketSnapshot {
|
||||
date: buy_date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
timestamp: Some("2025-01-01 10:18:00".to_string()),
|
||||
day_open: 10.0,
|
||||
open: 10.0,
|
||||
@@ -218,7 +372,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
||||
},
|
||||
DailyMarketSnapshot {
|
||||
date: ex_date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
||||
day_open: 10.0,
|
||||
open: 10.0,
|
||||
@@ -241,7 +395,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
||||
},
|
||||
DailyMarketSnapshot {
|
||||
date: payable_date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
timestamp: Some("2025-01-03 10:18:00".to_string()),
|
||||
day_open: 10.0,
|
||||
open: 10.0,
|
||||
@@ -266,42 +420,42 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
||||
vec![
|
||||
DailyFactorSnapshot {
|
||||
date: buy_date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
market_cap_bn: 20.0,
|
||||
free_float_cap_bn: 18.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
extra_factors: Default::default(),
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
date: ex_date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
market_cap_bn: 20.0,
|
||||
free_float_cap_bn: 18.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
extra_factors: Default::default(),
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
date: payable_date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
market_cap_bn: 20.0,
|
||||
free_float_cap_bn: 18.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
extra_factors: Default::default(),
|
||||
},
|
||||
],
|
||||
vec![
|
||||
CandidateEligibility {
|
||||
date: buy_date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
@@ -314,7 +468,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
||||
},
|
||||
CandidateEligibility {
|
||||
date: ex_date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
@@ -327,7 +481,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
||||
},
|
||||
CandidateEligibility {
|
||||
date: payable_date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
@@ -369,6 +523,9 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
||||
date: ex_date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
payable_date: Some(payable_date),
|
||||
record_date: Some(buy_date),
|
||||
share_effective_date: None,
|
||||
share_rounding_rule: None,
|
||||
share_cash: 1.05,
|
||||
share_bonus: 0.0,
|
||||
share_gift: 0.0,
|
||||
@@ -394,7 +551,7 @@ fn engine_reinvests_dividend_receivable_in_round_lots() {
|
||||
.with_minimum_commission(0.0),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
),
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit),
|
||||
BacktestConfig {
|
||||
initial_cash: 11_008.0,
|
||||
benchmark_code: "000300.SH".to_string(),
|
||||
@@ -452,6 +609,9 @@ fn engine_settles_same_day_dividend_after_split_for_aiquant_semantics() {
|
||||
date: ex_date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
payable_date: Some(ex_date),
|
||||
record_date: Some(buy_date),
|
||||
share_effective_date: Some(ex_date),
|
||||
share_rounding_rule: None,
|
||||
share_cash: 1.05,
|
||||
share_bonus: 0.2,
|
||||
share_gift: 0.0,
|
||||
@@ -475,7 +635,7 @@ fn engine_settles_same_day_dividend_after_split_for_aiquant_semantics() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
),
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit),
|
||||
BacktestConfig {
|
||||
initial_cash: 11_008.0,
|
||||
benchmark_code: "000300.SH".to_string(),
|
||||
@@ -507,7 +667,7 @@ fn engine_settles_same_day_dividend_after_split_for_aiquant_semantics() {
|
||||
result
|
||||
.position_events
|
||||
.iter()
|
||||
.any(|event| event.reason == "stock_split 1.200000" && event.quantity_after == 1_200)
|
||||
.any(|event| event.reason == "registered_share_distribution_delivered" && event.quantity_after == 1_200)
|
||||
);
|
||||
assert!(
|
||||
result
|
||||
|
||||
@@ -5,7 +5,7 @@ use fidc_core::{
|
||||
Instrument, IntradayExecutionQuote, MatchingType, OrderIntent, PriceField, Strategy, StrategyContext,
|
||||
StrategyDecision,
|
||||
};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
|
||||
@@ -105,7 +105,7 @@ fn single_day_quote_plan_data(date: NaiveDate) -> DataSet {
|
||||
fixture_instruments(),
|
||||
vec![DailyMarketSnapshot {
|
||||
date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
timestamp: Some(format!("{date} 15:00:00")),
|
||||
day_open: 10.0,
|
||||
open: 10.0,
|
||||
@@ -128,7 +128,7 @@ fn single_day_quote_plan_data(date: NaiveDate) -> DataSet {
|
||||
}],
|
||||
vec![DailyFactorSnapshot {
|
||||
date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
market_cap_bn: 10.0,
|
||||
free_float_cap_bn: 10.0,
|
||||
pe_ttm: 10.0,
|
||||
@@ -139,7 +139,7 @@ fn single_day_quote_plan_data(date: NaiveDate) -> DataSet {
|
||||
}],
|
||||
vec![CandidateEligibility {
|
||||
date,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
@@ -163,14 +163,55 @@ fn single_day_quote_plan_data(date: NaiveDate) -> DataSet {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_uses_preplanned_decision_symbols_without_recomputing_strategy_plan() {
|
||||
fn runtime_account_dependent_quote_scope_uses_the_actual_account() {
|
||||
struct AccountDependentQuoteReader;
|
||||
impl Strategy for AccountDependentQuoteReader {
|
||||
fn name(&self) -> &str { "account_dependent_quote_reader" }
|
||||
fn decision_quote_times(&self) -> Vec<NaiveTime> { vec![t(10, 18, 0)] }
|
||||
fn decision_quote_symbols(&mut self, ctx: &StrategyContext<'_>) -> Result<BTreeSet<String>, fidc_core::BacktestError> {
|
||||
Ok(if ctx.portfolio.cash() < 50_000.0 {
|
||||
BTreeSet::from(["000001.SZ".into()])
|
||||
} else { BTreeSet::new() })
|
||||
}
|
||||
fn on_day(&mut self, ctx: &StrategyContext<'_>) -> Result<StrategyDecision, fidc_core::BacktestError> {
|
||||
let loaded = ctx.data.execution_quotes_on(ctx.execution_date, "000001.SZ").iter().any(|quote|
|
||||
quote.timestamp.time()==t(10,17,59) && quote.last_price==10.0);
|
||||
assert_eq!(loaded, ctx.portfolio.cash() < 50_000.0,
|
||||
"quote scope must match this account, not a fixed-capital planning account");
|
||||
Ok(StrategyDecision::default())
|
||||
}
|
||||
}
|
||||
let date = d(2026, 1, 5);
|
||||
for initial_cash in [10_000.0, 100_000.0] {
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(), ChinaEquityRuleHooks, PriceField::Close,
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_matching_type(MatchingType::CurrentBarClose);
|
||||
let config = BacktestConfig {
|
||||
initial_cash, benchmark_code:"000852.SH".into(),
|
||||
start_date:Some(date), end_date:Some(date), decision_lag_trading_days:0,
|
||||
execution_price_field:PriceField::Close,
|
||||
};
|
||||
let mut engine = BacktestEngine::new(single_day_quote_plan_data(date), AccountDependentQuoteReader, broker, config)
|
||||
.with_execution_quote_loader(move |request| Ok(request.symbols.into_iter().map(|symbol| IntradayExecutionQuote {
|
||||
observation_kind:Default::default(), date:request.date, symbol,
|
||||
timestamp:request.date.and_time(t(10,17,59)), last_price:10.0,bid1:10.0,ask1:10.0,
|
||||
bid1_volume:10_000,ask1_volume:10_000,volume_delta:10_000,amount_delta:100_000.0,
|
||||
trading_phase:Some("continuous".into()),
|
||||
}).collect()));
|
||||
engine.run().expect("account-dependent quote planning");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn engine_resolves_the_runtime_strategy_scope_when_a_loader_exists() {
|
||||
let date = d(2026, 1, 5);
|
||||
let data = single_day_quote_plan_data(date);
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks,
|
||||
PriceField::Close,
|
||||
)
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_matching_type(MatchingType::CurrentBarClose);
|
||||
let config = BacktestConfig {
|
||||
initial_cash: 10_000.0,
|
||||
@@ -186,17 +227,13 @@ fn engine_uses_preplanned_decision_symbols_without_recomputing_strategy_plan() {
|
||||
symbol_plan_calls: Arc::clone(&symbol_plan_calls),
|
||||
};
|
||||
let captured_loader_calls = Arc::clone(&loader_calls);
|
||||
let preplanned = Arc::new(BTreeMap::from([(
|
||||
date,
|
||||
BTreeSet::from(["000001.SZ".to_string()]),
|
||||
)]));
|
||||
let mut engine = BacktestEngine::new(data, strategy, broker, config)
|
||||
.with_execution_quote_loader(move |request| {
|
||||
*captured_loader_calls.lock().expect("loader counter mutex") += 1;
|
||||
Ok(request
|
||||
.symbols
|
||||
.into_iter()
|
||||
.map(|symbol| IntradayExecutionQuote {
|
||||
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||
date: request.date,
|
||||
symbol,
|
||||
timestamp: request.date.and_time(t(10, 17, 59)),
|
||||
@@ -210,20 +247,19 @@ fn engine_uses_preplanned_decision_symbols_without_recomputing_strategy_plan() {
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
.with_preplanned_decision_quote_symbols_by_date(preplanned);
|
||||
});
|
||||
|
||||
engine.run().expect("backtest should run");
|
||||
|
||||
assert_eq!(
|
||||
*symbol_plan_calls.lock().expect("symbol plan counter mutex"),
|
||||
0,
|
||||
"the strategy plan must not be recomputed after a complete plan is supplied"
|
||||
1,
|
||||
"quote planning must use the actual run context"
|
||||
);
|
||||
assert_eq!(
|
||||
*loader_calls.lock().expect("loader counter mutex"),
|
||||
1,
|
||||
"the supplied symbols must still pass through the normal quote loader"
|
||||
0,
|
||||
"an empty runtime scope must not fetch unrequested symbols"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -235,7 +271,7 @@ fn engine_skips_decision_quote_symbol_plan_without_loader() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks,
|
||||
PriceField::Close,
|
||||
)
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_matching_type(MatchingType::CurrentBarClose);
|
||||
let config = BacktestConfig {
|
||||
initial_cash: 10_000.0,
|
||||
@@ -269,7 +305,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
||||
vec![
|
||||
DailyMarketSnapshot {
|
||||
date: first,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
timestamp: Some("2026-01-05 15:00:00".to_string()),
|
||||
day_open: 10.0,
|
||||
open: 10.0,
|
||||
@@ -292,7 +328,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
||||
},
|
||||
DailyMarketSnapshot {
|
||||
date: second,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
timestamp: Some("2026-01-06 15:00:00".to_string()),
|
||||
day_open: 10.5,
|
||||
open: 10.5,
|
||||
@@ -317,7 +353,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
||||
vec![
|
||||
DailyFactorSnapshot {
|
||||
date: first,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
market_cap_bn: 10.0,
|
||||
free_float_cap_bn: 10.0,
|
||||
pe_ttm: 10.0,
|
||||
@@ -328,7 +364,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
date: second,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
market_cap_bn: 10.0,
|
||||
free_float_cap_bn: 10.0,
|
||||
pe_ttm: 10.0,
|
||||
@@ -341,7 +377,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
||||
vec![
|
||||
CandidateEligibility {
|
||||
date: first,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
@@ -354,7 +390,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
||||
},
|
||||
CandidateEligibility {
|
||||
date: second,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
@@ -391,7 +427,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks,
|
||||
PriceField::Last,
|
||||
)
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_matching_type(MatchingType::MinuteLast)
|
||||
.with_intraday_execution_start_time(t(10, 40, 0));
|
||||
let config = BacktestConfig {
|
||||
@@ -411,7 +447,7 @@ fn engine_preloads_declared_decision_quotes_for_current_positions() {
|
||||
Ok(request
|
||||
.symbols
|
||||
.into_iter()
|
||||
.map(|symbol| IntradayExecutionQuote {
|
||||
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||
date: request.date,
|
||||
symbol,
|
||||
timestamp: request.date.and_time(t(10, 39, 59)),
|
||||
@@ -439,7 +475,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
||||
vec![
|
||||
DailyMarketSnapshot {
|
||||
date: first,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
timestamp: Some("2026-01-05 15:00:00".to_string()),
|
||||
day_open: 10.0,
|
||||
open: 10.0,
|
||||
@@ -462,7 +498,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
||||
},
|
||||
DailyMarketSnapshot {
|
||||
date: second,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
timestamp: Some("2026-01-06 15:00:00".to_string()),
|
||||
day_open: 10.5,
|
||||
open: 10.5,
|
||||
@@ -487,7 +523,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
||||
vec![
|
||||
DailyFactorSnapshot {
|
||||
date: first,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
market_cap_bn: 10.0,
|
||||
free_float_cap_bn: 10.0,
|
||||
pe_ttm: 10.0,
|
||||
@@ -498,7 +534,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
date: second,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
market_cap_bn: 10.0,
|
||||
free_float_cap_bn: 10.0,
|
||||
pe_ttm: 10.0,
|
||||
@@ -511,7 +547,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
||||
vec![
|
||||
CandidateEligibility {
|
||||
date: first,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
@@ -524,7 +560,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
||||
},
|
||||
CandidateEligibility {
|
||||
date: second,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
@@ -556,7 +592,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
||||
],
|
||||
Vec::new(),
|
||||
vec![
|
||||
IntradayExecutionQuote {
|
||||
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||
date: first,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
timestamp: first.and_time(t(10, 39, 59)),
|
||||
@@ -569,7 +605,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
||||
amount_delta: 100_000.0,
|
||||
trading_phase: Some("continuous".to_string()),
|
||||
},
|
||||
IntradayExecutionQuote {
|
||||
IntradayExecutionQuote { observation_kind: Default::default(),
|
||||
date: second,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
timestamp: second.and_time(t(10, 39, 59)),
|
||||
@@ -590,7 +626,7 @@ fn engine_reuses_preloaded_decision_quotes_without_loader_call() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks,
|
||||
PriceField::Last,
|
||||
)
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_matching_type(MatchingType::MinuteLast)
|
||||
.with_intraday_execution_start_time(t(10, 40, 0));
|
||||
let config = BacktestConfig {
|
||||
@@ -674,7 +710,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
||||
vec![
|
||||
DailyMarketSnapshot {
|
||||
date: first,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
timestamp: Some("2026-01-05 15:00:00".to_string()),
|
||||
day_open: 10.0,
|
||||
open: 10.0,
|
||||
@@ -697,7 +733,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
||||
},
|
||||
DailyMarketSnapshot {
|
||||
date: second,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
timestamp: Some("2026-01-06 15:00:00".to_string()),
|
||||
day_open: 10.5,
|
||||
open: 10.5,
|
||||
@@ -722,7 +758,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
||||
vec![
|
||||
DailyFactorSnapshot {
|
||||
date: first,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
market_cap_bn: 10.0,
|
||||
free_float_cap_bn: 10.0,
|
||||
pe_ttm: 10.0,
|
||||
@@ -733,7 +769,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
date: second,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
market_cap_bn: 10.0,
|
||||
free_float_cap_bn: 10.0,
|
||||
pe_ttm: 10.0,
|
||||
@@ -746,7 +782,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
||||
vec![
|
||||
CandidateEligibility {
|
||||
date: first,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
@@ -759,7 +795,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
||||
},
|
||||
CandidateEligibility {
|
||||
date: second,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
@@ -796,7 +832,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks,
|
||||
PriceField::Last,
|
||||
)
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit)
|
||||
.with_matching_type(MatchingType::MinuteLast)
|
||||
.with_intraday_execution_start_time(t(10, 40, 0));
|
||||
let config = BacktestConfig {
|
||||
@@ -826,7 +862,7 @@ fn engine_loads_distinct_decision_quote_times_on_same_day() {
|
||||
Ok(request
|
||||
.symbols
|
||||
.into_iter()
|
||||
.map(|symbol| IntradayExecutionQuote {
|
||||
.map(|symbol| IntradayExecutionQuote { observation_kind: Default::default(),
|
||||
date: request.date,
|
||||
symbol,
|
||||
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![
|
||||
DailyMarketSnapshot {
|
||||
date: date1,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
||||
day_open: 10.0,
|
||||
open: 10.0,
|
||||
@@ -94,7 +94,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
|
||||
},
|
||||
DailyMarketSnapshot {
|
||||
date: date1,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
symbol: "000002.SZ".into(),
|
||||
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
||||
day_open: 5.0,
|
||||
open: 5.0,
|
||||
@@ -117,7 +117,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
|
||||
},
|
||||
DailyMarketSnapshot {
|
||||
date: delist_date,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
symbol: "000002.SZ".into(),
|
||||
timestamp: Some("2025-01-03 10:18:00".to_string()),
|
||||
day_open: 5.05,
|
||||
open: 5.05,
|
||||
@@ -140,7 +140,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
|
||||
},
|
||||
DailyMarketSnapshot {
|
||||
date: date2,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
symbol: "000002.SZ".into(),
|
||||
timestamp: Some("2025-01-06 10:18:00".to_string()),
|
||||
day_open: 5.1,
|
||||
open: 5.1,
|
||||
@@ -165,53 +165,53 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
|
||||
vec![
|
||||
DailyFactorSnapshot {
|
||||
date: date1,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
market_cap_bn: 20.0,
|
||||
free_float_cap_bn: 18.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
extra_factors: Default::default(),
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
date: date1,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
symbol: "000002.SZ".into(),
|
||||
market_cap_bn: 30.0,
|
||||
free_float_cap_bn: 28.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
extra_factors: Default::default(),
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
date: delist_date,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
symbol: "000002.SZ".into(),
|
||||
market_cap_bn: 30.5,
|
||||
free_float_cap_bn: 28.5,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
extra_factors: Default::default(),
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
date: date2,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
symbol: "000002.SZ".into(),
|
||||
market_cap_bn: 31.0,
|
||||
free_float_cap_bn: 29.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
extra_factors: Default::default(),
|
||||
},
|
||||
],
|
||||
vec![
|
||||
CandidateEligibility {
|
||||
date: date1,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
@@ -224,7 +224,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
|
||||
},
|
||||
CandidateEligibility {
|
||||
date: date1,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
symbol: "000002.SZ".into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
@@ -237,7 +237,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
|
||||
},
|
||||
CandidateEligibility {
|
||||
date: delist_date,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
symbol: "000002.SZ".into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
@@ -250,7 +250,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
|
||||
},
|
||||
CandidateEligibility {
|
||||
date: date2,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
symbol: "000002.SZ".into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
@@ -295,7 +295,7 @@ fn engine_keeps_unresolved_delisted_position_without_fabricating_a_fill() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
);
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit);
|
||||
let mut engine = BacktestEngine::new(
|
||||
data,
|
||||
BuyThenHoldStrategy,
|
||||
@@ -364,7 +364,7 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
|
||||
vec![
|
||||
DailyMarketSnapshot {
|
||||
date: date1,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
||||
day_open: 10.0,
|
||||
open: 10.0,
|
||||
@@ -387,7 +387,7 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
|
||||
},
|
||||
DailyMarketSnapshot {
|
||||
date: date1,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
symbol: "000002.SZ".into(),
|
||||
timestamp: Some("2025-01-02 10:18:00".to_string()),
|
||||
day_open: 20.0,
|
||||
open: 20.0,
|
||||
@@ -410,7 +410,7 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
|
||||
},
|
||||
DailyMarketSnapshot {
|
||||
date: date2,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
symbol: "000002.SZ".into(),
|
||||
timestamp: Some("2025-01-03 10:18:00".to_string()),
|
||||
day_open: 21.0,
|
||||
open: 21.0,
|
||||
@@ -435,42 +435,42 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
|
||||
vec![
|
||||
DailyFactorSnapshot {
|
||||
date: date1,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
market_cap_bn: 20.0,
|
||||
free_float_cap_bn: 18.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
extra_factors: Default::default(),
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
date: date1,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
symbol: "000002.SZ".into(),
|
||||
market_cap_bn: 30.0,
|
||||
free_float_cap_bn: 28.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
extra_factors: Default::default(),
|
||||
},
|
||||
DailyFactorSnapshot {
|
||||
date: date2,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
symbol: "000002.SZ".into(),
|
||||
market_cap_bn: 31.0,
|
||||
free_float_cap_bn: 29.0,
|
||||
pe_ttm: 10.0,
|
||||
turnover_ratio: Some(1.0),
|
||||
effective_turnover_ratio: Some(1.0),
|
||||
adjustment_factor_backward1: None,
|
||||
extra_factors: BTreeMap::new(),
|
||||
extra_factors: Default::default(),
|
||||
},
|
||||
],
|
||||
vec![
|
||||
CandidateEligibility {
|
||||
date: date1,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
symbol: "000001.SZ".into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
@@ -483,7 +483,7 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
|
||||
},
|
||||
CandidateEligibility {
|
||||
date: date1,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
symbol: "000002.SZ".into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
@@ -496,7 +496,7 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
|
||||
},
|
||||
CandidateEligibility {
|
||||
date: date2,
|
||||
symbol: "000002.SZ".to_string(),
|
||||
symbol: "000002.SZ".into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
@@ -530,6 +530,9 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
|
||||
date: date2,
|
||||
symbol: "000001.SZ".to_string(),
|
||||
payable_date: None,
|
||||
record_date: Some(date1),
|
||||
share_effective_date: None,
|
||||
share_rounding_rule: None,
|
||||
share_cash: 0.0,
|
||||
share_bonus: 0.0,
|
||||
share_gift: 0.0,
|
||||
@@ -548,7 +551,7 @@ fn engine_applies_successor_conversion_before_unresolved_delisting_audit() {
|
||||
ChinaAShareCostModel::default(),
|
||||
ChinaEquityRuleHooks::default(),
|
||||
PriceField::Open,
|
||||
);
|
||||
).with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit);
|
||||
let mut engine = BacktestEngine::new(
|
||||
data,
|
||||
BuyThenHoldStrategy,
|
||||
|
||||
@@ -0,0 +1,478 @@
|
||||
use chrono::{NaiveDate, NaiveTime};
|
||||
use fidc_core::{
|
||||
BacktestConfig, BacktestEngine, BrokerSimulator, ChinaAShareCostModel, ChinaEquityRuleHooks,
|
||||
DataSet, MatchingType, OrderIntent, PriceField, Strategy, StrategyContext, StrategyDecision,
|
||||
};
|
||||
use std::{cell::RefCell, collections::BTreeSet, rc::Rc};
|
||||
|
||||
const SYMBOL: &str = "000001.SZ";
|
||||
fn day(value: u32) -> NaiveDate {
|
||||
NaiveDate::from_ymd_opt(2026, 9, value).unwrap()
|
||||
}
|
||||
|
||||
fn data() -> DataSet {
|
||||
let days = [11, 14, 15].map(day);
|
||||
DataSet::from_components_with_actions_and_quotes(
|
||||
vec![fidc_core::Instrument {
|
||||
symbol: SYMBOL.into(),
|
||||
name: "fixture".into(),
|
||||
board: "SZ".into(),
|
||||
round_lot: 100,
|
||||
listed_at: Some(day(1)),
|
||||
delisted_at: None,
|
||||
status: "active".into(),
|
||||
}],
|
||||
days.iter()
|
||||
.map(|&date| {
|
||||
let price = if date == day(11) { 10. } else { 8.95 };
|
||||
fidc_core::DailyMarketSnapshot {
|
||||
date,
|
||||
symbol: SYMBOL.into(),
|
||||
timestamp: Some(format!("{date} 15:00:00")),
|
||||
day_open: price,
|
||||
open: price,
|
||||
high: price,
|
||||
low: price,
|
||||
close: price,
|
||||
last_price: price,
|
||||
bid1: price,
|
||||
ask1: price,
|
||||
prev_close: price,
|
||||
volume: 100000,
|
||||
minute_volume: 100000,
|
||||
bid1_volume: 100000,
|
||||
ask1_volume: 100000,
|
||||
trading_phase: Some("continuous".into()),
|
||||
paused: false,
|
||||
upper_limit: price * 1.1,
|
||||
lower_limit: price * 0.9,
|
||||
price_tick: 0.01,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
days.iter()
|
||||
.map(|&date| fidc_core::DailyFactorSnapshot {
|
||||
date,
|
||||
symbol: SYMBOL.into(),
|
||||
market_cap_bn: 10.,
|
||||
free_float_cap_bn: 10.,
|
||||
pe_ttm: 10.,
|
||||
turnover_ratio: None,
|
||||
effective_turnover_ratio: None,
|
||||
adjustment_factor_backward1: Some(1.),
|
||||
extra_factors: Default::default(),
|
||||
})
|
||||
.collect(),
|
||||
days.iter()
|
||||
.map(|&date| fidc_core::CandidateEligibility {
|
||||
date,
|
||||
symbol: SYMBOL.into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
})
|
||||
.collect(),
|
||||
days.iter()
|
||||
.map(|&date| fidc_core::BenchmarkSnapshot {
|
||||
date,
|
||||
benchmark: "000300.SH".into(),
|
||||
open: 100.,
|
||||
close: 100.,
|
||||
prev_close: 100.,
|
||||
volume: 100000,
|
||||
})
|
||||
.collect(),
|
||||
vec![fidc_core::CorporateAction {
|
||||
date: day(14),
|
||||
symbol: SYMBOL.into(),
|
||||
payable_date: Some(day(14)),
|
||||
record_date: Some(day(11)),
|
||||
share_effective_date: None,
|
||||
share_rounding_rule: None,
|
||||
share_cash: 1.05,
|
||||
share_bonus: 0.,
|
||||
share_gift: 0.,
|
||||
issue_quantity: 0.,
|
||||
issue_price: 0.,
|
||||
reform: false,
|
||||
adjust_factor: None,
|
||||
successor_symbol: None,
|
||||
successor_ratio: None,
|
||||
successor_cash: None,
|
||||
}],
|
||||
[(9, 15), (9, 31)]
|
||||
.into_iter()
|
||||
.map(|(hour, minute)| fidc_core::IntradayExecutionQuote {
|
||||
observation_kind: fidc_core::data::QuoteObservationKind::MinuteBar,
|
||||
date: day(14),
|
||||
symbol: SYMBOL.into(),
|
||||
timestamp: day(14).and_hms_opt(hour, minute, 0).unwrap(),
|
||||
last_price: 8.95,
|
||||
bid1: 8.95,
|
||||
ask1: 8.95,
|
||||
bid1_volume: 100000,
|
||||
ask1_volume: 100000,
|
||||
volume_delta: 10000,
|
||||
amount_delta: 89500.,
|
||||
trading_phase: Some("continuous".into()),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
struct Hold {
|
||||
seen: Rc<RefCell<Vec<(NaiveTime, u32)>>>,
|
||||
}
|
||||
impl Strategy for Hold {
|
||||
fn name(&self) -> &str {
|
||||
"accounting reinvestment contract"
|
||||
}
|
||||
fn initial_subscriptions(&self) -> BTreeSet<String> {
|
||||
[SYMBOL.into()].into()
|
||||
}
|
||||
fn on_day(
|
||||
&mut self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
) -> Result<StrategyDecision, fidc_core::BacktestError> {
|
||||
Ok(if ctx.execution_date == day(11) {
|
||||
StrategyDecision {
|
||||
order_intents: vec![OrderIntent::Shares {
|
||||
symbol: SYMBOL.into(),
|
||||
quantity: 1000,
|
||||
reason: "initial".into(),
|
||||
}],
|
||||
..Default::default()
|
||||
}
|
||||
} else {
|
||||
StrategyDecision::default()
|
||||
})
|
||||
}
|
||||
fn on_minute(
|
||||
&mut self,
|
||||
ctx: &StrategyContext<'_>,
|
||||
_: &fidc_core::IntradayExecutionQuote,
|
||||
) -> Result<StrategyDecision, fidc_core::BacktestError> {
|
||||
self.seen.borrow_mut().push((
|
||||
ctx.current_time().unwrap(),
|
||||
ctx.portfolio
|
||||
.position(SYMBOL)
|
||||
.map_or(0, |position| position.quantity),
|
||||
));
|
||||
Ok(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
fn engine() -> BacktestEngine<Hold, ChinaAShareCostModel, ChinaEquityRuleHooks> {
|
||||
BacktestEngine::new(
|
||||
data(),
|
||||
Hold {
|
||||
seen: Rc::new(RefCell::new(Vec::new())),
|
||||
},
|
||||
BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default()
|
||||
.with_commission_rate(0.0008)
|
||||
.with_minimum_commission(0.),
|
||||
ChinaEquityRuleHooks,
|
||||
PriceField::Open,
|
||||
)
|
||||
.with_matching_type(MatchingType::NextBarOpen)
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(false),
|
||||
BacktestConfig {
|
||||
initial_cash: 50000.,
|
||||
benchmark_code: "000300.SH".into(),
|
||||
start_date: Some(day(11)),
|
||||
end_date: Some(day(15)),
|
||||
decision_lag_trading_days: 0,
|
||||
execution_price_field: PriceField::Open,
|
||||
},
|
||||
)
|
||||
.with_dividend_reinvestment(true)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accounting_reinvestment_has_an_explicit_origin_clock_and_progress_delivery() {
|
||||
let mut progress = Vec::new();
|
||||
let result = engine()
|
||||
.run_with_progress(|event| progress.push(event.clone()))
|
||||
.unwrap();
|
||||
let reinvest = result
|
||||
.fills
|
||||
.iter()
|
||||
.find(|fill| fill.reason == "dividend_reinvestment")
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
(
|
||||
reinvest.quantity,
|
||||
reinvest.price,
|
||||
reinvest.commission,
|
||||
reinvest.order_id
|
||||
),
|
||||
(100, 8.95, 0., None)
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(reinvest).unwrap()["origin"],
|
||||
"dividend_reinvestment"
|
||||
);
|
||||
assert_eq!(reinvest.execution_timestamp, day(14).and_hms_opt(0, 0, 0));
|
||||
let received = progress.iter().find(|event| event.date == day(14)).unwrap();
|
||||
assert!(
|
||||
received
|
||||
.fills
|
||||
.iter()
|
||||
.any(|fill| fill.reason == "dividend_reinvestment")
|
||||
);
|
||||
assert_eq!(
|
||||
progress
|
||||
.iter()
|
||||
.map(|event| event.daily_fill_count)
|
||||
.sum::<usize>(),
|
||||
result.fills.len()
|
||||
);
|
||||
}
|
||||
|
||||
fn manual_source(delayed: bool) -> fidc_core::manual_execution::ManualExecutionReplay {
|
||||
let observed = if delayed {
|
||||
"2026-09-14T01:15:00Z"
|
||||
} else {
|
||||
"2026-09-11T06:00:01Z"
|
||||
};
|
||||
let mut source: fidc_core::manual_execution::ManualExecutionReplay = serde_json::from_value(serde_json::json!({
|
||||
"schema":fidc_core::manual_execution::MANUAL_REPLAY_SCHEMA,"runtimeId":"r","accountId":"a","sourceContractSha256":"a".repeat(64),"contentSha256":"",
|
||||
"observationCutoff":"2026-09-15T08:00:00Z","actions":[{"actionId":"manual","source":"manual_security_trade","auditEventIds":["audit"],
|
||||
"confirmedAt":"2026-09-11T05:59:59Z","confirmationObservedAt":"2026-09-11T05:59:59Z","outcome":"orders_terminal","orders":[{
|
||||
"orderId":"manual-order","sourceAdapter":"paper","symbol":SYMBOL,"side":"Buy","quantity":1000,
|
||||
"orderCreatedAt":"2026-09-11T05:59:59Z","terminalObservedAt":observed,"terminalStatus":"filled","fills":[{
|
||||
"tradeId":"manual-fill","observationEventId":"receipt","observationSequence":1,"feeObservationEventId":"receipt","feeObservationSequence":1,
|
||||
"feeObservedAt":observed,"tradeDate":"2026-09-11","executedAt":"2026-09-11T06:00:00Z","observedAt":observed,
|
||||
"timestampPrecision":"second","quantity":1000,"price":"10","totalFee":"1"
|
||||
}]
|
||||
}]}]
|
||||
})).unwrap();
|
||||
source.content_sha256 = source.content_digest().unwrap();
|
||||
source
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delayed_receipt_before_market_open_reconciles_accounting_not_future_market_fills() {
|
||||
let timely = engine()
|
||||
.with_observed_manual_executions(manual_source(false))
|
||||
.unwrap()
|
||||
.run()
|
||||
.unwrap();
|
||||
let delayed = engine()
|
||||
.with_observed_manual_executions(manual_source(true))
|
||||
.unwrap()
|
||||
.run()
|
||||
.unwrap();
|
||||
assert_eq!(delayed.holdings_summary[0].quantity, 2200);
|
||||
assert_eq!(
|
||||
delayed.holdings_summary[0].quantity,
|
||||
timely.holdings_summary[0].quantity
|
||||
);
|
||||
assert_eq!(
|
||||
delayed.equity_curve.last().unwrap().cash,
|
||||
timely.equity_curve.last().unwrap().cash
|
||||
);
|
||||
assert_eq!(
|
||||
delayed.manual_executions[0]
|
||||
.corporate_adjustment
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.corporate_cash_delta,
|
||||
"155"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn weekend_receipts_and_morning_allocations_are_in_the_next_progress_batch() {
|
||||
let mut source = manual_source(true);
|
||||
let observed = "2026-09-12T02:00:00Z".parse().unwrap();
|
||||
let order = &mut source.actions[0].orders[0];
|
||||
order.terminal_observed_at = observed;
|
||||
order.fills[0].observed_at = observed;
|
||||
order.fills[0].fee_observed_at = observed;
|
||||
source.content_sha256 = source.content_digest().unwrap();
|
||||
let mut progress = Vec::new();
|
||||
let result = engine()
|
||||
.with_observed_manual_executions(source)
|
||||
.unwrap()
|
||||
.run_with_progress(|event| progress.push(event.clone()))
|
||||
.unwrap();
|
||||
let monday = progress.iter().find(|event| event.date == day(14)).unwrap();
|
||||
assert_eq!(monday.daily_manual_fill_count, 1);
|
||||
assert_eq!(monday.manual_executions[0].observed_at, observed);
|
||||
assert!(
|
||||
monday
|
||||
.fills
|
||||
.iter()
|
||||
.any(|fill| fill.origin == fidc_core::FillOrigin::DividendReinvestment)
|
||||
);
|
||||
assert!(
|
||||
monday
|
||||
.process_events
|
||||
.iter()
|
||||
.any(|event| event.kind == fidc_core::ProcessEventKind::ManualExecutionObserved)
|
||||
);
|
||||
assert_eq!(
|
||||
progress
|
||||
.iter()
|
||||
.map(|event| event.daily_fill_count)
|
||||
.sum::<usize>(),
|
||||
result.fills.len() + result.manual_executions.len()
|
||||
);
|
||||
}
|
||||
|
||||
fn exposure_event(id: &str, sequence: u64, at: &str, action: fidc_core::position_exposure::PositionExposureAction)
|
||||
-> fidc_core::position_exposure::PositionExposureEvent {
|
||||
fidc_core::position_exposure::PositionExposureEvent {
|
||||
event_id: id.into(), sequence, effective_at: at.parse().unwrap(), allocation_weights_bps: None, action,
|
||||
}
|
||||
}
|
||||
|
||||
fn cleared_reinvestment_case(
|
||||
events: Vec<fidc_core::position_exposure::PositionExposureEvent>,
|
||||
legacy: std::collections::BTreeMap<NaiveDate, i32>,
|
||||
extra_buy_delayed: Option<bool>,
|
||||
) -> fidc_core::BacktestResult {
|
||||
let mut parts = data().snapshot_components();
|
||||
parts.corporate_actions[0].payable_date = Some(day(15));
|
||||
let data = DataSet::from_components_with_actions_and_quotes(parts.instruments, parts.market,
|
||||
parts.factors, parts.candidates, parts.benchmarks, parts.corporate_actions, parts.execution_quotes).unwrap();
|
||||
let mut source = manual_source(false);
|
||||
let mut sale = source.actions[0].clone();
|
||||
sale.action_id = "clear".into(); sale.audit_event_ids = vec!["clear-audit".into()];
|
||||
sale.confirmed_at = "2026-09-14T05:59:59Z".parse().unwrap();
|
||||
sale.confirmation_observed_at = sale.confirmed_at;
|
||||
let order = &mut sale.orders[0];
|
||||
order.order_id = "clear-order".into(); order.side = fidc_core::OrderSide::Sell;
|
||||
order.order_created_at = sale.confirmed_at;
|
||||
order.terminal_observed_at = "2026-09-14T06:00:01Z".parse().unwrap();
|
||||
let fill = &mut order.fills[0];
|
||||
fill.trade_id = "clear-fill".into(); fill.observation_event_id = "clear-receipt".into();
|
||||
fill.observation_sequence = 2; fill.fee_observation_event_id = "clear-receipt".into();
|
||||
fill.fee_observation_sequence = 2; fill.trade_date = day(14);
|
||||
fill.executed_at = "2026-09-14T06:00:00Z".parse().unwrap();
|
||||
fill.observed_at = order.terminal_observed_at; fill.fee_observed_at = order.terminal_observed_at;
|
||||
fill.price = "8.95".parse().unwrap();
|
||||
source.actions.push(sale);
|
||||
if let Some(delayed) = extra_buy_delayed {
|
||||
let mut extra = source.actions[0].clone();
|
||||
extra.action_id = "extra".into(); extra.audit_event_ids = vec!["extra-audit".into()];
|
||||
extra.confirmed_at = "2026-09-11T06:00:59Z".parse().unwrap();
|
||||
extra.confirmation_observed_at = extra.confirmed_at;
|
||||
let order = &mut extra.orders[0];
|
||||
order.order_id = "extra-order".into(); order.order_created_at = extra.confirmed_at;
|
||||
order.terminal_observed_at = if delayed { "2026-09-15T01:15:00Z" } else { "2026-09-11T06:01:01Z" }.parse().unwrap();
|
||||
let fill = &mut order.fills[0];
|
||||
fill.trade_id = "extra-fill".into(); fill.observation_event_id = "extra-receipt".into();
|
||||
fill.observation_sequence = if delayed { 3 } else { 2 };
|
||||
fill.fee_observation_event_id = "extra-receipt".into(); fill.fee_observation_sequence = fill.observation_sequence;
|
||||
fill.executed_at = "2026-09-11T06:01:00Z".parse().unwrap(); fill.observed_at = order.terminal_observed_at;
|
||||
fill.fee_observed_at = order.terminal_observed_at;
|
||||
if !delayed {
|
||||
source.actions[1].orders[0].fills[0].observation_sequence = 3;
|
||||
source.actions[1].orders[0].fills[0].fee_observation_sequence = 3;
|
||||
}
|
||||
source.actions.push(extra);
|
||||
}
|
||||
source.position_exposure_events = events;
|
||||
source.legacy_position_exposure_bps = legacy;
|
||||
source.content_sha256 = source.content_digest().unwrap();
|
||||
let mut config = fidc_core::PlatformExprStrategyConfig::generic();
|
||||
config.signal_symbol = SYMBOL.into(); config.benchmark_symbol = "000300.SH".into();
|
||||
config.rotation_enabled = false; config.matching_type = MatchingType::CurrentBarClose;
|
||||
BacktestEngine::new(data, fidc_core::PlatformExprStrategy::new(config),
|
||||
BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_matching_type(MatchingType::CurrentBarClose).with_volume_limit(false).with_liquidity_limit(false),
|
||||
BacktestConfig { initial_cash: 50000., benchmark_code: "000300.SH".into(), start_date: Some(day(11)),
|
||||
end_date: Some(day(15)), decision_lag_trading_days: 0, execution_price_field: PriceField::Close })
|
||||
.with_dividend_reinvestment(true).with_observed_manual_executions(source).unwrap().run().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_effective_manual_zero_must_not_recreate_a_cleared_position_on_dividend_payment() {
|
||||
use fidc_core::position_exposure::PositionExposureAction as Action;
|
||||
let zero = exposure_event("zero", 1, "2026-09-14T07:00:00Z", Action::Set { target_exposure_bps: 0 });
|
||||
let result = cleared_reinvestment_case(vec![zero], Default::default(), None);
|
||||
assert!(result.fills.is_empty(), "{:?}", result.fills);
|
||||
assert!(result.holdings_summary.is_empty());
|
||||
assert_eq!(result.equity_curve.last().unwrap().cash, 49998.);
|
||||
assert!(result.equity_curve.last().unwrap().notes.contains("runtime_zero_exposure"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_clear_without_a_manual_zero_keeps_the_declared_legacy_reinvestment_model() {
|
||||
let result = cleared_reinvestment_case(vec![], Default::default(), None);
|
||||
assert_eq!(result.fills.len(), 1);
|
||||
assert_eq!((result.fills[0].quantity, result.fills[0].price, result.fills[0].commission), (100, 8.95, 0.));
|
||||
assert_eq!(result.fills[0].gross_amount, 895.);
|
||||
assert_eq!(result.fills[0].net_cash_flow, -895.);
|
||||
assert!(result.order_events.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reinvestment_respects_zero_restore_same_instant_sequence_and_legacy_granularity() {
|
||||
use fidc_core::position_exposure::PositionExposureAction as Action;
|
||||
let before = "2026-09-14T07:00:00Z";
|
||||
let settlement = "2026-09-14T16:00:00Z";
|
||||
let later = "2026-09-15T01:31:00Z";
|
||||
let zero = || exposure_event("zero", 1, before, Action::Scale { requested_bps: 0 });
|
||||
let cases = vec![
|
||||
(vec![zero()], Default::default(), false),
|
||||
(vec![exposure_event("zero-at-settlement", 1, settlement, Action::Set { target_exposure_bps: 0 })], Default::default(), false),
|
||||
(vec![exposure_event("later-zero", 1, later, Action::Set { target_exposure_bps: 0 })], Default::default(), true),
|
||||
(vec![zero(), exposure_event("restore", 2, before, Action::Restore)], Default::default(), true),
|
||||
(vec![exposure_event("restore", 1, before, Action::Restore), exposure_event("last-zero", 2, before, Action::Set { target_exposure_bps: 0 })], Default::default(), false),
|
||||
(vec![exposure_event("restore", 2, before, Action::Restore), zero()], Default::default(), true),
|
||||
(vec![zero(), exposure_event("later-restore", 2, later, Action::Restore)], Default::default(), false),
|
||||
(vec![], std::collections::BTreeMap::from([(day(14), 0)]), false),
|
||||
(vec![exposure_event("restore-legacy", 1, before, Action::Restore)], std::collections::BTreeMap::from([(day(14), 0)]), true),
|
||||
(vec![exposure_event("positive", 1, before, Action::Set { target_exposure_bps: 3000 })], Default::default(), true),
|
||||
];
|
||||
for (events, legacy, allowed) in cases {
|
||||
let result = cleared_reinvestment_case(events, legacy, None);
|
||||
assert_eq!(result.fills.len(), usize::from(allowed));
|
||||
assert_eq!(result.equity_curve.last().unwrap().cash, if allowed { 49103. } else { 49998. });
|
||||
assert_eq!(result.holdings_summary.iter().map(|holding| holding.quantity).sum::<u32>(), if allowed {100} else {0});
|
||||
assert!(result.order_events.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_explicit_zero_member_weight_or_omission_blocks_only_that_reinvestment() {
|
||||
use fidc_core::position_exposure::PositionExposureAction as Action;
|
||||
for included in [false, true] {
|
||||
for weight in [0, 10000] {
|
||||
let mut event = exposure_event("allocation", 1, "2026-09-14T07:00:00Z", Action::Set { target_exposure_bps: 5000 });
|
||||
let mut weights = std::collections::BTreeMap::from([("000002.SZ".into(), if included {10000-weight} else {10000})]);
|
||||
if included { weights.insert(SYMBOL.into(), weight); }
|
||||
event.allocation_weights_bps = Some(weights);
|
||||
let allowed = included && weight > 0;
|
||||
let result = cleared_reinvestment_case(vec![event], Default::default(), None);
|
||||
assert_eq!(result.fills.len(), usize::from(allowed));
|
||||
if !allowed { assert!(result.equity_curve.last().unwrap().notes.contains("runtime_zero_allocation")); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn late_corporate_replay_uses_the_same_zero_policy_and_retains_actual_manual_shares() {
|
||||
use fidc_core::position_exposure::PositionExposureAction as Action;
|
||||
let event = exposure_event("zero", 1, "2026-09-14T07:00:00Z", Action::Set { target_exposure_bps: 0 });
|
||||
let timely = cleared_reinvestment_case(vec![event.clone()], Default::default(), Some(false));
|
||||
let late = cleared_reinvestment_case(vec![event], Default::default(), Some(true));
|
||||
assert!(timely.fills.is_empty()); assert!(late.fills.is_empty());
|
||||
assert_eq!(timely.equity_curve.last().unwrap().cash, 41047.);
|
||||
assert_eq!(late.equity_curve.last().unwrap().cash, timely.equity_curve.last().unwrap().cash);
|
||||
assert_eq!(late.holdings_summary[0].quantity, 1000);
|
||||
assert_eq!(late.holdings_summary[0].quantity, timely.holdings_summary[0].quantity);
|
||||
assert_eq!(late.manual_executions.last().unwrap().corporate_adjustment.as_ref().unwrap().corporate_cash_delta, "1050");
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
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()
|
||||
.map(|date| DailyMarketSnapshot {
|
||||
date: *date,
|
||||
symbol: SYMBOL.to_string(),
|
||||
symbol: SYMBOL.into(),
|
||||
timestamp: None,
|
||||
day_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");
|
||||
for offset in 0..bars_per_day {
|
||||
let timestamp = session_start + Duration::minutes(offset as i64);
|
||||
quotes.push(IntradayExecutionQuote {
|
||||
quotes.push(IntradayExecutionQuote { observation_kind: Default::default(),
|
||||
date: *date,
|
||||
symbol: SYMBOL.to_string(),
|
||||
timestamp,
|
||||
|
||||
@@ -0,0 +1,737 @@
|
||||
use chrono::{DateTime, NaiveDate, Utc};
|
||||
use fidc_core::manual_execution::{MANUAL_REPLAY_SCHEMA, ManualExecutionReplay};
|
||||
use fidc_core::{
|
||||
BacktestConfig, BacktestEngine, BenchmarkSnapshot, BrokerSimulator, CandidateEligibility,
|
||||
ChinaAShareCostModel, ChinaEquityRuleHooks, CorporateAction, DailyFactorSnapshot,
|
||||
DailyMarketSnapshot, DataSet, Instrument, MatchingType, PriceField, Strategy,
|
||||
};
|
||||
|
||||
fn date(day: u32) -> NaiveDate {
|
||||
NaiveDate::from_ymd_opt(2026, 9, day).unwrap()
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Action {
|
||||
Split,
|
||||
Dividend,
|
||||
Successor,
|
||||
}
|
||||
|
||||
fn data(action: Action) -> DataSet {
|
||||
data_with_successor_metadata(action, true)
|
||||
}
|
||||
|
||||
fn data_with_successor_metadata(action: Action, include_successor: bool) -> DataSet {
|
||||
let days = [10, 11, 14, 15].map(date);
|
||||
let mut market = Vec::new();
|
||||
let mut factors = Vec::new();
|
||||
let mut candidates = Vec::new();
|
||||
for day in days {
|
||||
for symbol in ["000001.SZ", "000002.SZ"] {
|
||||
if matches!(action, Action::Successor) && symbol == "000001.SZ" && day >= date(14) {
|
||||
continue;
|
||||
}
|
||||
let price = if day < date(14)
|
||||
|| (symbol == "000002.SZ" && !matches!(action, Action::Successor))
|
||||
{
|
||||
10.
|
||||
} else if matches!(action, Action::Dividend) {
|
||||
9.
|
||||
} else {
|
||||
5.
|
||||
};
|
||||
market.push(DailyMarketSnapshot {
|
||||
date: day,
|
||||
symbol: symbol.into(),
|
||||
timestamp: Some(format!("{day} 15:00:00")),
|
||||
day_open: price,
|
||||
open: price,
|
||||
high: price,
|
||||
low: price,
|
||||
close: price,
|
||||
last_price: price,
|
||||
bid1: price,
|
||||
ask1: price,
|
||||
prev_close: price,
|
||||
volume: 100000,
|
||||
minute_volume: 100000,
|
||||
bid1_volume: 100000,
|
||||
ask1_volume: 100000,
|
||||
trading_phase: Some("continuous".into()),
|
||||
paused: false,
|
||||
upper_limit: price * 1.1,
|
||||
lower_limit: price * 0.9,
|
||||
price_tick: 0.01,
|
||||
});
|
||||
factors.push(DailyFactorSnapshot {
|
||||
date: day,
|
||||
symbol: symbol.into(),
|
||||
market_cap_bn: 10.,
|
||||
free_float_cap_bn: 10.,
|
||||
pe_ttm: 10.,
|
||||
turnover_ratio: None,
|
||||
effective_turnover_ratio: None,
|
||||
adjustment_factor_backward1: Some(1.),
|
||||
extra_factors: Default::default(),
|
||||
});
|
||||
candidates.push(CandidateEligibility {
|
||||
date: day,
|
||||
symbol: symbol.into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing: false,
|
||||
is_paused: false,
|
||||
allow_buy: true,
|
||||
allow_sell: true,
|
||||
is_kcb: false,
|
||||
is_one_yuan: false,
|
||||
risk_level_code: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
DataSet::from_components_with_actions(
|
||||
["000001.SZ", "000002.SZ"]
|
||||
.into_iter()
|
||||
.filter(|symbol| include_successor || *symbol != "000002.SZ")
|
||||
.map(|symbol| Instrument {
|
||||
symbol: symbol.into(),
|
||||
name: symbol.into(),
|
||||
board: "SZ".into(),
|
||||
round_lot: 100,
|
||||
listed_at: Some(date(1)),
|
||||
delisted_at: (matches!(action, Action::Successor) && symbol == "000001.SZ")
|
||||
.then_some(date(14)),
|
||||
status: "active".into(),
|
||||
})
|
||||
.collect(),
|
||||
market,
|
||||
factors,
|
||||
candidates,
|
||||
days.map(|day| BenchmarkSnapshot {
|
||||
date: day,
|
||||
benchmark: "000300.SH".into(),
|
||||
open: 100.,
|
||||
close: 100.,
|
||||
prev_close: 100.,
|
||||
volume: 100000,
|
||||
})
|
||||
.into(),
|
||||
vec![CorporateAction {
|
||||
date: date(14),
|
||||
symbol: "000001.SZ".into(),
|
||||
payable_date: Some(date(14)),
|
||||
record_date: Some(date(11)),
|
||||
share_effective_date: Some(date(14)),
|
||||
share_rounding_rule: Some("aggregate_half_up".into()),
|
||||
share_cash: if matches!(action, Action::Dividend) {
|
||||
1.
|
||||
} else {
|
||||
0.
|
||||
},
|
||||
share_bonus: if matches!(action, Action::Split) {
|
||||
1.
|
||||
} else {
|
||||
0.
|
||||
},
|
||||
share_gift: 0.,
|
||||
issue_quantity: 0.,
|
||||
issue_price: 0.,
|
||||
reform: false,
|
||||
adjust_factor: None,
|
||||
successor_symbol: matches!(action, Action::Successor).then(|| "000002.SZ".into()),
|
||||
successor_ratio: matches!(action, Action::Successor).then_some(2.),
|
||||
successor_cash: matches!(action, Action::Successor).then_some(0.5),
|
||||
}],
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn source(delayed: bool, sell: bool) -> ManualExecutionReplay {
|
||||
let trades = if sell {
|
||||
vec![
|
||||
("initial-buy", "Buy", 10, 200, false),
|
||||
("sale", "Sell", 11, 100, delayed),
|
||||
]
|
||||
} else {
|
||||
vec![("buy", "Buy", 11, 100, delayed)]
|
||||
};
|
||||
let actions = trades.into_iter().enumerate().map(|(index, (id, side, day, quantity, late))| {
|
||||
let executed = format!("2026-09-{day:02}T06:00:00Z").parse::<DateTime<Utc>>().unwrap();
|
||||
let observed = if late { "2026-09-15T05:00:00Z".parse().unwrap() } else { executed + chrono::Duration::seconds(1) };
|
||||
let created = executed - chrono::Duration::seconds(1);
|
||||
serde_json::json!({"actionId":id,"source":"manual_security_trade","auditEventIds":[format!("audit-{id}")],
|
||||
"confirmedAt":created,"confirmationObservedAt":created,"outcome":"orders_terminal","orders":[{
|
||||
"orderId":id,"brokerOrderId":id,"sourceAdapter":"paper","symbol":"000001.SZ","side":side,"quantity":quantity,
|
||||
"orderCreatedAt":created,"terminalObservedAt":observed,"terminalStatus":"filled","fills":[{
|
||||
"tradeId":id,"observationEventId":id,"observationSequence":index+1,"tradeDate":date(day),
|
||||
"executedAt":executed,"observedAt":observed,"feeObservationEventId":id,"feeObservationSequence":index+1,
|
||||
"feeObservedAt":observed,"timestampPrecision":"second","quantity":quantity,"price":"10","totalFee":"1"
|
||||
}]
|
||||
}]})
|
||||
}).collect::<Vec<_>>();
|
||||
let mut source: ManualExecutionReplay = serde_json::from_value(serde_json::json!({
|
||||
"schema":MANUAL_REPLAY_SCHEMA,"runtimeId":"r","accountId":"a","sourceContractSha256":"a".repeat(64),
|
||||
"contentSha256":"","observationCutoff":"2026-09-15T08:00:00Z","actions":actions,
|
||||
})).unwrap();
|
||||
source.content_sha256 = source.content_digest().unwrap();
|
||||
source.validate().unwrap();
|
||||
source
|
||||
}
|
||||
|
||||
struct Hold;
|
||||
impl Strategy for Hold {
|
||||
fn name(&self) -> &str {
|
||||
"manual corporate observation"
|
||||
}
|
||||
fn requires_minute_callbacks(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn run_custom<S: Strategy>(
|
||||
data: DataSet,
|
||||
source: ManualExecutionReplay,
|
||||
strategy: S,
|
||||
cash_dividends: bool,
|
||||
adjust_cost: bool,
|
||||
) -> Result<fidc_core::BacktestResult, fidc_core::BacktestError> {
|
||||
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_matching_type(MatchingType::NextBarOpen)
|
||||
.with_volume_limit(false)
|
||||
.with_liquidity_limit(false);
|
||||
BacktestEngine::new(
|
||||
data,
|
||||
strategy,
|
||||
broker,
|
||||
BacktestConfig {
|
||||
initial_cash: 10000.,
|
||||
benchmark_code: "000300.SH".into(),
|
||||
start_date: Some(date(10)),
|
||||
end_date: Some(date(15)),
|
||||
decision_lag_trading_days: 0,
|
||||
execution_price_field: PriceField::Open,
|
||||
},
|
||||
)
|
||||
.with_cash_dividends(cash_dividends)
|
||||
.with_cash_dividend_cost_basis_adjustment(adjust_cost)
|
||||
.with_observed_manual_executions(source)
|
||||
.unwrap()
|
||||
.run()
|
||||
}
|
||||
|
||||
fn run(
|
||||
action: Action,
|
||||
delayed: bool,
|
||||
sell: bool,
|
||||
) -> Result<fidc_core::BacktestResult, fidc_core::BacktestError> {
|
||||
run_custom(data(action), source(delayed, sell), Hold, true, true)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delayed_buy_does_not_lose_corporate_entitlements() {
|
||||
for action in [Action::Split, Action::Dividend, Action::Successor] {
|
||||
let timely = run(action, false, false).unwrap();
|
||||
let delayed = run(action, true, false).unwrap();
|
||||
let project = |result: &fidc_core::BacktestResult| {
|
||||
(
|
||||
result.equity_curve.last().unwrap().cash,
|
||||
result.equity_curve.last().unwrap().total_equity,
|
||||
result
|
||||
.holdings_summary
|
||||
.iter()
|
||||
.map(|row| (row.symbol.clone(), row.quantity))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
};
|
||||
assert_eq!(project(&delayed), project(&timely), "{action:?}");
|
||||
assert_eq!(delayed.manual_executions.len(), 1);
|
||||
assert!(delayed.fills.is_empty());
|
||||
if matches!(action, Action::Successor)
|
||||
&& let Ok(directory) = std::env::var("FIDC_CORPORATE_QA_OUTPUT")
|
||||
{
|
||||
use std::io::Write;
|
||||
let path = std::path::Path::new(&directory).join("corporate-successor-result.json");
|
||||
let mut options = std::fs::OpenOptions::new();
|
||||
options.write(true).create_new(true);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
options.mode(0o600);
|
||||
}
|
||||
let mut file = options.open(path).unwrap();
|
||||
file.write_all(&serde_json::to_vec(&serde_json::json!({
|
||||
"source":delayed.manual_execution_source.as_deref(), "applications":delayed.manual_executions,
|
||||
})).unwrap()).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delayed_sale_does_not_keep_unearned_corporate_entitlements() {
|
||||
for action in [Action::Split, Action::Dividend, Action::Successor] {
|
||||
let timely = run(action, false, true).unwrap();
|
||||
let delayed = run(action, true, true).unwrap();
|
||||
let project = |result: &fidc_core::BacktestResult| {
|
||||
(
|
||||
result.equity_curve.last().unwrap().cash,
|
||||
result.equity_curve.last().unwrap().total_equity,
|
||||
result
|
||||
.holdings_summary
|
||||
.iter()
|
||||
.map(|row| (row.symbol.clone(), row.quantity))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
};
|
||||
assert_eq!(project(&delayed), project(&timely), "{action:?}");
|
||||
assert_eq!(delayed.manual_executions.len(), 2);
|
||||
assert!(delayed.fills.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paper_and_broker_observations_require_the_same_frozen_successor_scope() {
|
||||
for adapter in ["paper", "gt", "qmt"] {
|
||||
for delayed in [false, true] {
|
||||
for sell in [false, true] {
|
||||
let mut replay = source(delayed, sell);
|
||||
for action in &mut replay.actions {
|
||||
for order in &mut action.orders { order.source_adapter = Some(adapter.into()); }
|
||||
}
|
||||
replay.content_sha256 = replay.content_digest().unwrap();
|
||||
replay.validate().unwrap();
|
||||
let complete = run_custom(data(Action::Successor), replay.clone(), Hold, true, true).unwrap();
|
||||
assert_eq!(complete.holdings_summary[0].symbol, "000002.SZ");
|
||||
assert_eq!(complete.holdings_summary[0].quantity, 200);
|
||||
assert!(complete.fills.is_empty());
|
||||
assert!(complete.order_events.is_empty());
|
||||
let error = run_custom(data_with_successor_metadata(Action::Successor, false),
|
||||
replay, Hold, true, true).unwrap_err();
|
||||
assert!(error.to_string().contains("successor_instrument_missing"),
|
||||
"{adapter} delayed={delayed} sell={sell}: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn protected_successor_run(delayed: bool, locked: bool, amount: i32)
|
||||
-> fidc_core::BacktestResult {
|
||||
let policy = fidc_core::holding_policy::AutomaticTradeProtection {
|
||||
sell_cooldown_days: if locked { 0 } else { 3 },
|
||||
locks: if locked { vec![fidc_core::holding_policy::AutomaticTradeLock {
|
||||
symbol: "000001.SZ".into(), start_date: date(11), end_date: Some(date(15)),
|
||||
}] } else { vec![] }, ..Default::default()
|
||||
};
|
||||
protected_successor_case(delayed, policy, amount, "partial")
|
||||
}
|
||||
|
||||
fn protected_successor_case(delayed: bool,
|
||||
policy: fidc_core::holding_policy::AutomaticTradeProtection, amount: i32, scenario: &str)
|
||||
-> fidc_core::BacktestResult {
|
||||
let mut config = fidc_core::PlatformExprStrategyConfig::generic();
|
||||
config.signal_symbol = "000002.SZ".into();
|
||||
config.benchmark_symbol = "000300.SH".into();
|
||||
config.rotation_enabled = false;
|
||||
config.matching_type = MatchingType::CurrentBarClose;
|
||||
config.volume_capacity_mode = fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit;
|
||||
config.explicit_action_schedule = Some(fidc_core::PlatformRebalanceSchedule {
|
||||
frequency: fidc_core::PlatformScheduleFrequency::Daily,
|
||||
time_rule: Some(fidc_core::ScheduleTimeRule::physical_time(14, 30)),
|
||||
});
|
||||
config.automatic_trade_protection = policy;
|
||||
config.explicit_actions = vec![fidc_core::PlatformTradeAction::Order {
|
||||
kind: fidc_core::PlatformExplicitOrderKind::Shares, symbol: "000002.SZ".into(),
|
||||
amount_expr: amount.to_string(), when_expr: Some("decision_date == \"2026-09-15\"".into()),
|
||||
limit_price_expr: None, time_in_force: None, start_time_expr: None, end_time_expr: None,
|
||||
reason: "configured_successor_action".into(),
|
||||
}];
|
||||
let data = successor_execution_data();
|
||||
let mut replay = source(delayed, true);
|
||||
if scenario == "sold_before" {
|
||||
let order = &mut replay.actions[1].orders[0];
|
||||
order.quantity = 200; order.fills[0].quantity = 200;
|
||||
} else if scenario == "cleared_after" {
|
||||
let mut row = serde_json::to_value(&replay.actions[1]).unwrap();
|
||||
let at = "2026-09-15T05:30:00Z";
|
||||
let receipt = "2026-09-15T05:30:01Z";
|
||||
row["actionId"] = "clear".into(); row["auditEventIds"] = serde_json::json!(["audit-clear"]);
|
||||
row["confirmedAt"] = at.into(); row["confirmationObservedAt"] = at.into();
|
||||
let order = &mut row["orders"][0];
|
||||
order["orderId"] = "clear-order".into(); order["brokerOrderId"] = "clear-order".into();
|
||||
order["symbol"] = "000002.SZ".into(); order["quantity"] = 200.into();
|
||||
order["orderCreatedAt"] = at.into(); order["terminalObservedAt"] = receipt.into();
|
||||
let fill = &mut order["fills"][0];
|
||||
fill["tradeId"] = "clear-trade".into(); fill["observationEventId"] = "clear-receipt".into();
|
||||
fill["observationSequence"] = 3.into(); fill["tradeDate"] = "2026-09-15".into();
|
||||
fill["executedAt"] = at.into(); fill["observedAt"] = receipt.into();
|
||||
fill["feeObservationEventId"] = "clear-receipt".into(); fill["feeObservationSequence"] = 3.into();
|
||||
fill["feeObservedAt"] = receipt.into(); fill["price"] = "5".into(); fill["quantity"] = 200.into();
|
||||
replay.actions.push(serde_json::from_value(row).unwrap());
|
||||
}
|
||||
replay.content_sha256 = replay.content_digest().unwrap();
|
||||
BacktestEngine::new(data, fidc_core::PlatformExprStrategy::new(config),
|
||||
BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_matching_type(MatchingType::CurrentBarClose)
|
||||
.with_volume_capacity_mode(fidc_core::execution_capacity::VolumeCapacityMode::SessionCapacityAudit),
|
||||
BacktestConfig { initial_cash: 10000., benchmark_code: "000300.SH".into(),
|
||||
start_date: Some(date(10)), end_date: Some(date(15)), decision_lag_trading_days: 0,
|
||||
execution_price_field: PriceField::Close,
|
||||
}).with_observed_manual_executions(replay).unwrap().run().unwrap()
|
||||
}
|
||||
|
||||
fn successor_execution_data() -> DataSet {
|
||||
let parts = data(Action::Successor).snapshot_components();
|
||||
DataSet::from_components_with_actions_and_quotes(parts.instruments, parts.market,
|
||||
parts.factors, parts.candidates, parts.benchmarks, parts.corporate_actions,
|
||||
[30, 31].into_iter().map(|minute| fidc_core::IntradayExecutionQuote {
|
||||
observation_kind: fidc_core::data::QuoteObservationKind::MinuteBar,
|
||||
date: date(15), symbol: "000002.SZ".into(), timestamp: date(15).and_hms_opt(14,minute,0).unwrap(),
|
||||
last_price: 5., bid1: 5., ask1: 5., bid1_volume: 100000, ask1_volume: 100000,
|
||||
volume_delta: 100000, amount_delta: 500000., trading_phase: Some("continuous".into()),
|
||||
}).collect()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_pool_rebalance_applies_inherited_protection_without_rewriting_its_target() {
|
||||
use fidc_core::stock_pool_execution as pool;
|
||||
struct NativePool { locked: bool, expires: u32, exposure: i32 }
|
||||
impl Strategy for NativePool {
|
||||
fn name(&self) -> &str { "native pool successor protection" }
|
||||
fn requires_minute_callbacks(&self) -> bool { false }
|
||||
fn schedule_rules(&self) -> Vec<fidc_core::ScheduleRule> {
|
||||
vec![fidc_core::ScheduleRule::daily("pool", fidc_core::ScheduleStage::OnDay)
|
||||
.with_time_rule(fidc_core::ScheduleTimeRule::physical_time(14,30))]
|
||||
}
|
||||
fn on_scheduled(&mut self, ctx: &fidc_core::StrategyContext<'_>, _: &fidc_core::ScheduleRule)
|
||||
-> Result<fidc_core::StrategyDecision, fidc_core::BacktestError> {
|
||||
if ctx.execution_date != date(15) { return Ok(Default::default()); }
|
||||
let symbols = vec!["000002.SZ".to_owned()];
|
||||
let policy = fidc_core::holding_policy::AutomaticTradeProtection {
|
||||
sell_cooldown_days: if self.locked { 0 } else { 3 },
|
||||
locks: if self.locked { vec![fidc_core::holding_policy::AutomaticTradeLock {
|
||||
symbol: "000001.SZ".into(), start_date: date(11), end_date: Some(date(self.expires)),
|
||||
}] } else { vec![] }, ..Default::default()
|
||||
};
|
||||
let contract = pool::FrozenStockPoolIntent {
|
||||
pool_id: "pool".into(), signal_date: date(15), frozen_equity: 10000.into(),
|
||||
selection: pool::StockPoolSelection { trade_date: date(15), requested_symbols: symbols.clone(),
|
||||
normal_trading_symbols: symbols.clone(), risk_eligible_symbols: symbols.clone(), final_symbols: symbols,
|
||||
exclusion_reasons: Default::default(), inherited_from_generation: None, explicit_empty: false,
|
||||
generation: Some("latest".into()),
|
||||
}, members: vec![pool::StockPoolMemberSpec { symbol: "000002.SZ".into(), requested_order: 0,
|
||||
recommendation_reason: String::new(), target_weight_bps: None, stop_loss: None, take_profit: None }],
|
||||
rule: pool::StockPoolExecutionRule { pricing_mode: pool::POOL_PRICE_FIRST_TICK.into(),
|
||||
window_start: "14:30".into(), window_end: "15:00".into(), automatic_trade_protection: policy,
|
||||
..Default::default() }, constraints: pool::StockPoolDecisionConstraints {
|
||||
target_holding_count: Some(1), ..Default::default() },
|
||||
invest_ratio_bps: self.exposure, reserve_cash: 0.into(), out_of_pool_policy: "hold".into(), generation: "latest".into(),
|
||||
};
|
||||
Ok(fidc_core::StrategyDecision { order_intents: vec![fidc_core::OrderIntent::StockPool { contract: Box::new(contract) }], ..Default::default() })
|
||||
}
|
||||
}
|
||||
for delayed in [false, true] {
|
||||
for (locked, expires, exposure) in [(false, 15, 10000), (true, 15, 10000), (true, 15, 0), (true, 14, 10000)] {
|
||||
let result = BacktestEngine::new(successor_execution_data(), NativePool { locked, expires, exposure },
|
||||
BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
|
||||
.with_matching_type(MatchingType::MinuteLast).with_intraday_execution_start_time(chrono::NaiveTime::from_hms_opt(14,30,0).unwrap())
|
||||
.with_volume_limit(false).with_liquidity_limit(false),
|
||||
BacktestConfig { initial_cash: 10000., benchmark_code: "000300.SH".into(), start_date: Some(date(10)),
|
||||
end_date: Some(date(15)), decision_lag_trading_days: 0, execution_price_field: PriceField::Last })
|
||||
.with_observed_manual_executions(source(delayed, true)).unwrap().run().unwrap();
|
||||
if expires == 14 {
|
||||
assert!(!result.fills.is_empty(), "positive control {:?}", result.order_events);
|
||||
} else {
|
||||
assert!(result.fills.is_empty(), "delayed={delayed} locked={locked} exposure={exposure}: {:?}", result.fills);
|
||||
assert!(result.order_events.is_empty());
|
||||
assert_eq!(result.holdings_summary[0].quantity, 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successor_keeps_sell_cooldown_for_timely_and_delayed_receipts() {
|
||||
for delayed in [false, true] {
|
||||
let result = protected_successor_run(delayed, false, 100);
|
||||
assert!(result.fills.is_empty(), "delayed={delayed}: {:?}", result.fills);
|
||||
assert!(result.order_events.is_empty(), "delayed={delayed}: {:?}", result.order_events);
|
||||
assert!(result.risk_decisions.iter().any(|row| row.symbol == "000002.SZ"
|
||||
&& row.date == date(15) && !row.accepted && row.rule_code == "sell_fill_cooldown"),
|
||||
"orders={:?} risk={:?} notes={:?}", result.order_events, result.risk_decisions,
|
||||
result.equity_curve.iter().map(|row| (&row.date, &row.notes)).collect::<Vec<_>>());
|
||||
assert_eq!(result.holdings_summary[0].quantity, 200);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converted_holding_does_not_lose_its_configured_date_lock() {
|
||||
for delayed in [false, true] {
|
||||
for amount in [-100, 100] {
|
||||
let result = protected_successor_run(delayed, true, amount);
|
||||
assert!(result.fills.is_empty(), "delayed={delayed} amount={amount}: {:?}", result.fills);
|
||||
assert!(result.order_events.is_empty(), "delayed={delayed} amount={amount}: {:?}", result.order_events);
|
||||
assert!(result.risk_decisions.iter().any(|row| row.symbol == "000002.SZ"
|
||||
&& row.date == date(15) && !row.accepted && row.rule_code == "automatic_trade_locked"),
|
||||
"orders={:?} risk={:?} notes={:?}", result.order_events, result.risk_decisions,
|
||||
result.equity_curve.iter().map(|row| (&row.date, &row.notes)).collect::<Vec<_>>());
|
||||
assert_eq!(result.holdings_summary[0].quantity, 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successor_lock_expires_on_the_original_configured_date_not_the_conversion_date() {
|
||||
for delayed in [false, true] {
|
||||
let policy = fidc_core::holding_policy::AutomaticTradeProtection {
|
||||
locks: vec![fidc_core::holding_policy::AutomaticTradeLock {
|
||||
symbol: "000001.SZ".into(), start_date: date(11), end_date: Some(date(14)),
|
||||
}], ..Default::default()
|
||||
};
|
||||
let result = protected_successor_case(delayed, policy, 100, "partial");
|
||||
assert_eq!(result.fills.len(), 1);
|
||||
assert_eq!(result.fills[0].quantity, 100);
|
||||
assert_eq!(result.holdings_summary[0].quantity, 300);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_survives_a_manual_clear_after_conversion_but_not_a_disproved_conversion() {
|
||||
for delayed in [false, true] {
|
||||
let policy = fidc_core::holding_policy::AutomaticTradeProtection {
|
||||
locks: vec![fidc_core::holding_policy::AutomaticTradeLock {
|
||||
symbol: "000001.SZ".into(), start_date: date(11), end_date: Some(date(15)),
|
||||
}], ..Default::default()
|
||||
};
|
||||
let cleared = protected_successor_case(delayed, policy.clone(), 100, "cleared_after");
|
||||
assert!(cleared.fills.is_empty());
|
||||
assert!(cleared.order_events.is_empty());
|
||||
assert!(cleared.holdings_summary.is_empty());
|
||||
assert_eq!(cleared.manual_executions.len(), 3);
|
||||
let unconverted = protected_successor_case(delayed, policy, 100, "sold_before");
|
||||
assert_eq!(unconverted.fills.len(), 1);
|
||||
assert_eq!(unconverted.holdings_summary[0].quantity, 100);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corporate_replay_preserves_issued_orders_cash_flows_financing_and_charged_fees() {
|
||||
struct ExistingActivity {
|
||||
receiving_days: usize,
|
||||
}
|
||||
impl Strategy for ExistingActivity {
|
||||
fn name(&self) -> &str {
|
||||
"corporate replay with original activity"
|
||||
}
|
||||
fn requires_minute_callbacks(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn on_day(
|
||||
&mut self,
|
||||
ctx: &fidc_core::StrategyContext<'_>,
|
||||
) -> Result<fidc_core::StrategyDecision, fidc_core::BacktestError> {
|
||||
use fidc_core::OrderIntent;
|
||||
let order_intents = if ctx.execution_date == date(10) {
|
||||
vec![
|
||||
OrderIntent::DepositWithdraw {
|
||||
amount: 500.,
|
||||
receiving_days: self.receiving_days,
|
||||
reason: "original deposit".into(),
|
||||
},
|
||||
OrderIntent::FinanceRepay {
|
||||
amount: 200.,
|
||||
reason: "original financing".into(),
|
||||
},
|
||||
OrderIntent::SetManagementFeeRate {
|
||||
rate: 0.001,
|
||||
reason: "original fee policy".into(),
|
||||
},
|
||||
]
|
||||
} else if ctx.execution_date == date(11) {
|
||||
vec![OrderIntent::Shares {
|
||||
symbol: "000002.SZ".into(),
|
||||
quantity: 100,
|
||||
reason: "unrelated stock".into(),
|
||||
}]
|
||||
} else if ctx.execution_date == date(14) {
|
||||
vec![OrderIntent::Shares {
|
||||
symbol: "000001.SZ".into(),
|
||||
quantity: 100,
|
||||
reason: "already issued after corporate action".into(),
|
||||
}]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
Ok(fidc_core::StrategyDecision {
|
||||
order_intents,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
fn management_fee(
|
||||
&mut self,
|
||||
_: &fidc_core::StrategyContext<'_>,
|
||||
_: f64,
|
||||
) -> Result<Option<f64>, fidc_core::BacktestError> {
|
||||
Ok(Some(0.25))
|
||||
}
|
||||
}
|
||||
for receiving_days in [0, 1] {
|
||||
for sell in [false, true] {
|
||||
let timely = run_custom(
|
||||
data(Action::Split),
|
||||
source(false, sell),
|
||||
ExistingActivity { receiving_days },
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
let delayed = run_custom(
|
||||
data(Action::Split),
|
||||
source(true, sell),
|
||||
ExistingActivity { receiving_days },
|
||||
true,
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
serde_json::to_value(&timely.fills).unwrap(),
|
||||
serde_json::to_value(&delayed.fills).unwrap()
|
||||
);
|
||||
assert_eq!(delayed.fills.len(), 2);
|
||||
assert_eq!(
|
||||
delayed.equity_curve.last().unwrap().cash,
|
||||
timely.equity_curve.last().unwrap().cash
|
||||
);
|
||||
assert_eq!(
|
||||
delayed.equity_curve.last().unwrap().total_equity,
|
||||
timely.equity_curve.last().unwrap().total_equity
|
||||
);
|
||||
assert_eq!(
|
||||
delayed
|
||||
.equity_curve
|
||||
.iter()
|
||||
.map(|row| row.external_cash_flow)
|
||||
.sum::<f64>(),
|
||||
500.
|
||||
);
|
||||
assert_eq!(delayed.manual_executions.len(), if sell { 2 } else { 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pending_and_disabled_dividends_keep_the_configured_cash_and_cost_contract() {
|
||||
for paid in [false, true] {
|
||||
for enabled in [false, true] {
|
||||
for adjust_cost in [false, true] {
|
||||
let fixture = || {
|
||||
let mut parts = data(Action::Dividend).snapshot_components();
|
||||
parts.corporate_actions[0].payable_date =
|
||||
Some(date(if paid { 14 } else { 16 }));
|
||||
DataSet::from_components_with_actions(
|
||||
parts.instruments,
|
||||
parts.market,
|
||||
parts.factors,
|
||||
parts.candidates,
|
||||
parts.benchmarks,
|
||||
parts.corporate_actions,
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
let timely =
|
||||
run_custom(fixture(), source(false, false), Hold, enabled, adjust_cost)
|
||||
.unwrap();
|
||||
let delayed =
|
||||
run_custom(fixture(), source(true, false), Hold, enabled, adjust_cost).unwrap();
|
||||
assert_eq!(
|
||||
delayed.equity_curve.last().unwrap().cash,
|
||||
timely.equity_curve.last().unwrap().cash
|
||||
);
|
||||
let financial = |result: &fidc_core::BacktestResult| {
|
||||
result
|
||||
.holdings_summary
|
||||
.iter()
|
||||
.map(|row| {
|
||||
(
|
||||
row.symbol.clone(),
|
||||
row.quantity,
|
||||
row.average_cost,
|
||||
row.last_price,
|
||||
row.market_value,
|
||||
row.unrealized_pnl,
|
||||
row.realized_pnl,
|
||||
row.pnl,
|
||||
row.dividend_receivable,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
// Receipt-day turnover is deliberately different when the
|
||||
// dividend option is disabled and no adjustment is required.
|
||||
assert_eq!(financial(&delayed), financial(&timely));
|
||||
assert_eq!(
|
||||
delayed.manual_executions[0].corporate_adjustment.is_some(),
|
||||
enabled
|
||||
);
|
||||
if enabled && !paid {
|
||||
assert_eq!(delayed.terminal_audit.cash_receivable_count, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn late_fill_replays_aggregate_split_rounding_not_an_independent_rounded_fragment() {
|
||||
let fixture = || {
|
||||
let mut parts = data(Action::Split).snapshot_components();
|
||||
parts.corporate_actions[0].share_bonus = 0.125;
|
||||
for row in &mut parts.market {
|
||||
if row.symbol == "000001.SZ" && row.date >= date(14) {
|
||||
row.day_open = 8.89;
|
||||
row.open = 8.89;
|
||||
row.close = 8.89;
|
||||
row.last_price = 8.89;
|
||||
row.high = 8.89;
|
||||
row.low = 8.89;
|
||||
row.prev_close = 8.89;
|
||||
row.bid1 = 8.89;
|
||||
row.ask1 = 8.89;
|
||||
row.upper_limit = 9.78;
|
||||
row.lower_limit = 8.;
|
||||
}
|
||||
}
|
||||
DataSet::from_components_with_actions(
|
||||
parts.instruments,
|
||||
parts.market,
|
||||
parts.factors,
|
||||
parts.candidates,
|
||||
parts.benchmarks,
|
||||
parts.corporate_actions,
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
let input = |delayed| {
|
||||
let mut value = source(delayed, true);
|
||||
value.actions[0].orders[0].quantity = 100;
|
||||
value.actions[0].orders[0].fills[0].quantity = 100;
|
||||
value.actions[1].orders[0].side = fidc_core::OrderSide::Buy;
|
||||
value.content_sha256 = value.content_digest().unwrap();
|
||||
value
|
||||
};
|
||||
let timely = run_custom(fixture(), input(false), Hold, true, true).unwrap();
|
||||
let delayed = run_custom(fixture(), input(true), Hold, true, true).unwrap();
|
||||
assert_eq!(timely.holdings_summary[0].quantity, 225);
|
||||
assert_eq!(delayed.holdings_summary[0].quantity, 225);
|
||||
assert_eq!(
|
||||
delayed.equity_curve.last().unwrap().total_equity,
|
||||
timely.equity_curve.last().unwrap().total_equity
|
||||
);
|
||||
assert_eq!(
|
||||
delayed.manual_executions[1]
|
||||
.corporate_adjustment
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.positions["000001.SZ"]
|
||||
.quantity_before,
|
||||
113
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -158,6 +158,42 @@ fn legacy_state_without_quantity_keeps_its_serialized_identity() {
|
||||
assert_eq!(serde_json::to_value(state).unwrap(), original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_exit_basis_is_immutable_restart_safe_and_scoped_to_the_signal() {
|
||||
let original = StockPoolExecutionState::default()
|
||||
.observe(day(11), day(11), &[day(11), day(14)], &[member()], &[held(1000, 1000)]).unwrap();
|
||||
let basis = BTreeMap::from([("000001.SZ".into(), Decimal::from(1000))]);
|
||||
let saved = original.record_position_action_bases("sell-signal", &basis).unwrap();
|
||||
assert!(original.position_action_bases.is_empty(), "a preview must not mutate its input");
|
||||
let restored: StockPoolExecutionState = serde_json::from_slice(&serde_json::to_vec(&saved).unwrap()).unwrap();
|
||||
let next_day = restored.observe(day(11), day(14), &[day(11), day(14)], &[member()], &[held(500, 500)]).unwrap();
|
||||
assert_eq!(next_day.position_action_bases_for("sell-signal"), basis);
|
||||
assert!(next_day.position_action_bases_for("new-signal").is_empty());
|
||||
assert!(next_day.record_position_action_bases("sell-signal", &BTreeMap::from([("000001.SZ".into(), Decimal::from(500))])).unwrap_err().contains("basis_changed"));
|
||||
let new_signal = next_day.record_position_action_bases("new-signal", &BTreeMap::from([("000001.SZ".into(), Decimal::from(500))])).unwrap();
|
||||
assert!(new_signal.position_action_bases_for("sell-signal").is_empty());
|
||||
assert_eq!(new_signal.position_action_bases_for("new-signal")["000001.SZ"], Decimal::from(500));
|
||||
for invalid in [Decimal::ZERO, Decimal::NEGATIVE_ONE] {
|
||||
assert!(original.record_position_action_bases("signal", &BTreeMap::from([("000001.SZ".into(), invalid)])).is_err());
|
||||
}
|
||||
assert!(original.record_position_action_bases(" ", &basis).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verified_split_adjusts_exit_basis_and_entry_completion_not_generation() {
|
||||
let initial = StockPoolExecutionState::default()
|
||||
.observe(day(11), day(11), &[day(11)], &[member()], &[]).unwrap();
|
||||
let entry_plan = plan(&initial, day(11), &[member()], &[], 10000, "hold");
|
||||
let entered = initial.record_plan(day(11), "entry", &entry_plan).unwrap();
|
||||
let saved = entered.record_position_action_bases("sell", &BTreeMap::from([("000001.SZ".into(), Decimal::from(1000))])).unwrap();
|
||||
let adjusted = saved.adjust_for_split("000001.SZ", Decimal::new(15,1)).unwrap();
|
||||
assert_eq!(adjusted.position_action_bases_for("sell")["000001.SZ"], Decimal::from(1500));
|
||||
assert_eq!(adjusted.entries["000001.SZ"].completion_quantity, Some(Decimal::from(1500)));
|
||||
assert_eq!(adjusted.position_action_bases["000001.SZ"].first_execution_date, day(11));
|
||||
assert_eq!(saved.position_action_bases_for("sell")["000001.SZ"], Decimal::from(1000));
|
||||
assert!(saved.adjust_for_split("000001.SZ", Decimal::ZERO).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_entry_continues_after_restart_then_completed_holdings_are_preserved() {
|
||||
let members = vec![member()];
|
||||
|
||||
@@ -35,7 +35,7 @@ fn market(
|
||||
) -> DailyMarketSnapshot {
|
||||
DailyMarketSnapshot {
|
||||
date: d(date),
|
||||
symbol: symbol.to_string(),
|
||||
symbol: symbol.into(),
|
||||
timestamp: None,
|
||||
day_open: open,
|
||||
open,
|
||||
@@ -66,7 +66,7 @@ fn factor(
|
||||
) -> DailyFactorSnapshot {
|
||||
DailyFactorSnapshot {
|
||||
date: d(date),
|
||||
symbol: symbol.to_string(),
|
||||
symbol: symbol.into(),
|
||||
market_cap_bn,
|
||||
free_float_cap_bn,
|
||||
pe_ttm: 18.0,
|
||||
@@ -87,7 +87,7 @@ fn candidate(
|
||||
) -> CandidateEligibility {
|
||||
CandidateEligibility {
|
||||
date: d(date),
|
||||
symbol: symbol.to_string(),
|
||||
symbol: symbol.into(),
|
||||
is_st: false,
|
||||
is_star_st: false,
|
||||
is_new_listing,
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
- 成交日及后续N个完整正式交易日均受保护。例如周五成交、N=3,保护到下周三结束,周四恢复;不按72小时或自然日替代。
|
||||
- 最长持有从连续持仓第一次实际买入开始,跨正式交易日计数;加仓、部分卖出和有证据的证券转换不重置,完全卖出再买入开启新周期。锁定和买入保护优先于最长持有退出。
|
||||
- 日期锁定禁止自动买卖,已接受的挂单不自动撤销;手工路径只绕过自动策略保护,不绕过账户授权、T+1、券商和风控。
|
||||
- 已校验且实际发生的持仓换股继承原保护期限与日期锁,不因改代码解锁或重计时;无实际转换的目录映射不继承。换股后手工清仓仍受原有效日期锁;确认换股前已清仓时不保留推定关系。原生策略、股票池与ETF顺延消费的修复及未发布边界见 `successor-protection-20260914.md`。
|
||||
- 保留的真实持仓继续占用资金与席位,不把未完成卖出当现金。最长持有退出先形成唯一最终目标,不能叠加一笔策略部分卖出和一笔框架全量卖出。
|
||||
- 在线上下文重建必须注入已经校验的真实成交/持仓快照,不能把重建日或旧行情日当建仓日。期货或股票期货混合账户未纳入本合同,显式拒绝。
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
# 回报上下文、盘前意图与尚未提交的目标
|
||||
|
||||
2026-09-14。本轮已配套发布177,annotated tag `v2026.9.14.5`。Engine81acc54 / Service e81bf47 / Trading94f99d2;完整股票池Goal继续,不据本阶段关闭。
|
||||
|
||||
## 已复现问题
|
||||
|
||||
1. `on_process_event`总是收到`active_datetime=None`及空委托/成交数组。10:00账本已有100股,但Trade/PostMinute回调的成交数量仍为0;不能靠普通`on_minute`已修复就认为通知链也完整。
|
||||
2. 15:05盘后成交后,PreAfterTrading仍被标为15:00;跨日模式的PostOnDay又使用信号日描述执行日已发生的成交。
|
||||
3. BeforeTrading调度只处理订阅、账户和期货指令,剩余股票买卖/撤改意图没有后续消费。简单在开盘调用普通broker执行还会让旧挂单先成交再撤单。
|
||||
4. 合并完整目标时只追加权重会保留旧证券;更重要的是,不能先提交盘前旧组合,之后才计算同一窗口的新目标,否则T+1可能使错误买入无法纠正。
|
||||
5. 策略计算前的空broker调用也会恢复上一目标的未提交买入腿。反例中原持仓100股,09:30卖25股、13:00卖剩余75股;若此时先恢复旧买入,已经准备将新目标设为0%的策略仍会买入另一股票3000股。
|
||||
|
||||
## 本轮处理
|
||||
|
||||
- 事件通知显式携带当前可见的委托、成交与回调时钟,移动已完成记录后再通知,不按每个回调复制整段历史。上下文是通知时已完成批次的最新状态,不冒充每一历史通知发生瞬间的账本快照。
|
||||
- 信号计算回调保留信号日;账户/委托通知使用实际执行日与物理时钟。默认收盘和结算不早于已处理时刻及当前适用的盘后结束点,管理费回调沿用同一完成时钟。
|
||||
- 盘前撤改走明确的非撮合控制阶段,保持原订单ID和实际已成交量;该入口拒绝买卖目标,不会顺带撮合旧单。普通显式买卖按原配置窗口执行,后续回调读取撤改后的真实活动订单。
|
||||
- 盘前与集合竞价的显式命令保留各自批次及约束。纯完整组合(完整rebalance或单一StockPool/TargetPortfolioSmart)可以被更新的完整意图替换;空回调不等于清仓,显式空完整目标才清仓。被替换意图的旧买入限制不能污染新完整目标。
|
||||
- 尚未提交的完整目标保留到当前窗口日度策略算完;新执行意图优先,只有没有新执行意图时才使用前面的目标。已提交挂单可以先更新实际成交,但策略计算前不恢复旧的未提交买入腿,之后再由正常执行路径处理当前意图。
|
||||
- 订阅/账户/直接期货指令通知同样获得完成后的历史;本轮不改变期货成交、会话或费用规则。
|
||||
|
||||
## 回归证据
|
||||
|
||||
- 通知链:09:30为空、10:00/10:01均看到100股及1笔实际成交,Trade通知可找到相同订单。
|
||||
- 盘后:15:05成交后的默认收盘/结算和管理费通知不倒退;next-open保持独立信号日和执行日。
|
||||
- 盘前:09:00生成100股命令,分别只在09:30/13:00配置窗口成交;保留备注/诊断。跨日撤销原GTC订单后,新订单只成交100股,未让旧单先成交。
|
||||
- 完整目标:盘前A、集合竞价B、日度A或显式空目标,最终只采用有效最新目标;日度无新信号时保持B。显式逐股命令不会被目标合并丢弃。
|
||||
- 恢复顺序:开启正常旧恢复的单点负向对照确实多买3000股;恢复BeforeStrategy阶段后,只有原股票同一卖单的25+75股成交,无新增买入,最终持仓为空。
|
||||
- 本机Core834项通过(9项原有ignore),Trading613、最新main Runner446/API119通过。外部数据库及平台ignore不当作通过。
|
||||
|
||||
精确只读快照在Linux通过Core834及Trading613。旧二进制先独立归档,构建保持1GiB磁盘余量;本轮未再次删除缓存或业务文件。
|
||||
|
||||
## 发布与真实历史复验
|
||||
|
||||
已推送annotated tag `v2026.9.14.5`对应Engine `81acc5422878abc855fca72b35766ffad6159200`、Service `e81bf47806f5ac4ae4798bb5f5955a56638f754c`、Trading `94f99d20f49f6cd1810996706cb94f610c302385`。回测API/Runner于06:15:53 CST切换,五交易单元06:21:09切换,06:22实际运行文件和业务事实复核通过。
|
||||
|
||||
三组冻结合同共六次独立原生A/B,完整Canonical及equity/orders/trades/holdings逐行一致;再通过生产HTTP各提交一次,结果分别匹配原生候选,旧记录未改写:
|
||||
|
||||
| 案例 | 生产回测ID | 成交 / 持仓 | 期末权益 |
|
||||
| --- | --- | --- | ---: |
|
||||
| 手选优先四证券 | btr_req_44f1bb067559946ef22941a0c425ed53e47515e04b399e20 | 10 / 4 | 9706248.648662 |
|
||||
| 自动优先四证券 | btr_req_078f129ed46b55ba72b47605983a00ae3eef14b1995e1237 | 10 / 4 | 9706248.648662 |
|
||||
| 许总24只原v3 | btr_req_ef37b8ea403f489e4798d2878e0ef85966ace550f6b4f3c4 | 51 / 21 | 9685563.876924999 |
|
||||
|
||||
重复目标委托0。三条新记录各5个交易日事件落库,持久事件27/18/32条,唯一键计数分别相同;旧流式样本仍27条/5日。仍为原合同下的日终容量审计,不外推实时盘口成交能力;首次Source准备和后续快速返回也不作为性能优化证明。
|
||||
|
||||
API SHA `dea170902d77734d0a77c4da7dad71a70b33f76467e0608675dfbcc9d35d67fc`,Runner SHA `b1d93215deb275fbec6217c6b9afbf717d5649600716bf4f3a1bf5d1cfa69731`,运行实现身份 `fed10e9fa61836aa271921f5d58490054210d83da935cad5de11cfacab45c13e`。API发布目录`/srv/fidc/canonical/run/backtest-api/releases/callback-81acc54-7w1zx9fb`,回退目录`/srv/fidc/canonical/run/build/callback-rollback-j7oje2tz`;交易回退`holding-protection-rollback-czuric4r`。
|
||||
|
||||
六服务实际SHA与manifest吻合,新增ERROR0。3Paper/0Live、配置、旧活动单、3个未确认Paper预览、迁移、影子配置0及disabled未变;发布后Paper/Live新订单0,未发送真实通知、委托或撤单。Source d5/PID1700096与UI6a2/PID3089476未重启,研究/信号暂停不变。177维护中的Engine9a54156完整保留,实际构建使用81acc54/e81bf47及81acc54/94f99d2的只读Git快照。
|
||||
|
||||
原始回放/HTTP证据`/srv/fidc/canonical/run/research/stock-pool-callback-20260914/`;发布和最终审计`/tmp/fidc-callback-{candidate,api-release,trading-release,final-audit}-20260914.json`;非敏感汇总在`docs/evidence/callback-target-20260914/acceptance.json`。
|
||||
|
||||
## 继续范围
|
||||
|
||||
显式逐笔手工影子回放仍未完成,四类手工来源继续拒绝纯比例影子;原始撤单意图时刻不能用网关回报时刻冒充。还需继续检查会话外调度产生的未提交意图、完整阶段日历与其余参数/生命周期/适配器矩阵。Source冻结、研究/信号暂停、现有任务配置和真实路由不改。
|
||||
@@ -0,0 +1,25 @@
|
||||
# 应收股息资产与支付日净值守恒修复
|
||||
|
||||
2026-10-05。发现总权益只计算现金、持仓市值和负债,遗漏已经确认但尚未支付的应收股息。这会在除息后的持仓计价下降时低估资产,再在真正支付时把资产转移误呈现成收益。本次修复账簿计价;没有部署活动运行版本或改写历史研究结果,生产策略仍为 0/10。
|
||||
|
||||
## 行为更正
|
||||
|
||||
`PortfolioState::total_equity` 现在加入独立应收股息资产。应收金额逐项转换为固定精度货币并检查非负及溢出,既不变成可用现金,也不计入证券市值。真实支付通过既有结算链路将同一资产从应收转为现金,不凭空新增权益;已卖光或被清理的持仓不能让应收资产消失。
|
||||
|
||||
具体回归:账户现金 1,000、登记日持有 100 股、价格 10、税前每股股息 0.5。除息后持仓市值 950、应收 50,账户权益仍为 2,000;支付前现金仍为 1,000,支付后现金 1,050、应收零,总权益保持 2,000。没有实际到账前,提现仍受现金余额约束。独立无持仓案例验证应收 50 不因空持仓清理而丢失。
|
||||
|
||||
同时移除对原始股息率的 `f64::EPSILON` 过滤。明确的极小非零值仍出现在股息查询中,也必须具备支付日、登记日和实际登记持仓证据;不能因为最终乘积可能按货币精度舍入为零,就在校验前抹掉原始率或绕过门禁。新增 1e-16 原始率的保留及未知支付日拒绝回归。
|
||||
|
||||
## 代码、运行与证据
|
||||
|
||||
权益计价提交为 `af655de0c169cd92ea8bf9bbc77c34a5c157bdf3`,极小非零率提交为 `d3f49a17ef284dba587607ad0ac160cc394bf677`,已推送既有工作分支。没有重写正式历史输入、模型、评分、旧日志或旧报告。
|
||||
|
||||
重构建及测试在 177 的 Boris UID 1003 私有 checkout 中执行,显式使用批准的 `stable-x86_64-unknown-linux-gnu` / Rust 1.98.0、锁文件和离线依赖。编译输出写入已核验可执行的 `/tmp` tmpfs,单元 CPUQuota=400%、MemoryMax=16GiB、MemorySwapMax=0、4 个构建 worker;不使用低于保留线的 SSD 进行批量构建,不改系统工具链。
|
||||
|
||||
首次权益计价版本通过 18 项定向测试及全工作区 963 项测试。最终版本 `cargo test --locked --offline -p fidc-core --all-targets` 通过 **964 项测试、9 项既有忽略项**,日志 SHA256 为 `7e25d69e1adec3183394a90a6c3dfab01c56982f9a4aa64afd261af69be72551`。新改动后复核完整核心回归,未跳过失败样本。
|
||||
|
||||
验证回执 SHA256 为 `c92576e6ecb36727444baf4f0eeebd5a1d9f401cc60cc88f541482e7c2645640`,PG18 artifact 为 `strategy_research_fcae25fd6b283a74016bd99e`,完整 JSONB 回读通过。回执、PG 原记录、首次及最终测试日志见 `docs/evidence/cash-receivable-equity-20261005/`。真实冻结研究执行器仍为 PID 1396112、InvocationID `0eb28e3a45694a00951aa3aec0e13465`,原数值研究预算未重开。
|
||||
|
||||
## 未完成门禁
|
||||
|
||||
此次结果证明应收现金计入权益及支付转移的账簿回归,不等同于完整实盘或策略验收。股份应收、登记日送转权益、真实股份交付与可卖日仍需继续实现;Rust 接收端对股份交付日的承接、AiQuant 同条件运行、完整 Source 事件/无事件与修订证据、样本外、容量、模拟盘和人工审批仍未闭环。`production_runtime_deployed=false`、`production_ready=false`、`trading_allowed=false` 保留。
|
||||
@@ -0,0 +1,191 @@
|
||||
{
|
||||
"verified_at": "2026-09-13T22:22:38.597836+00:00",
|
||||
"tag": "v2026.9.14.5",
|
||||
"processes": {
|
||||
"fidc-backtest-service-highmem177.service": {
|
||||
"pid": 3692551,
|
||||
"sha256": "dea170902d77734d0a77c4da7dad71a70b33f76467e0608675dfbcc9d35d67fc",
|
||||
"journal_since": "2026-09-13T22:15:53.225719+00:00",
|
||||
"journal_lines": 54,
|
||||
"error_lines": 0
|
||||
},
|
||||
"fidc-trading-control-highmem177.service": {
|
||||
"pid": 3697497,
|
||||
"sha256": "a8f62ba74caf7ce2f5ba9cc6f67f41c844dee3747852611051c8dfb7b36295a3",
|
||||
"journal_since": "2026-09-13T22:21:09.122548+00:00",
|
||||
"journal_lines": 5,
|
||||
"error_lines": 0
|
||||
},
|
||||
"fidc-market-data-highmem177.service": {
|
||||
"pid": 3697498,
|
||||
"sha256": "89395ab4c9e11274f171f4386f88949ce616fe45d15aae9a829216d53ed11db7",
|
||||
"journal_since": "2026-09-13T22:21:09.122548+00:00",
|
||||
"journal_lines": 5,
|
||||
"error_lines": 0
|
||||
},
|
||||
"fidc-strategy-runtime-highmem177.service": {
|
||||
"pid": 3697688,
|
||||
"sha256": "041a0103d2c46c55221d169965ece9fdacee3905abc0d46edd9c8a2a86f6cd54",
|
||||
"journal_since": "2026-09-13T22:21:09.122548+00:00",
|
||||
"journal_lines": 5,
|
||||
"error_lines": 0
|
||||
},
|
||||
"fidc-paper-trading-highmem177.service": {
|
||||
"pid": 3697792,
|
||||
"sha256": "8460008f712810f7d3876b9f2274aef88f82d46cd33e1593dbd0361f6d158b75",
|
||||
"journal_since": "2026-09-13T22:21:09.122548+00:00",
|
||||
"journal_lines": 6,
|
||||
"error_lines": 0
|
||||
},
|
||||
"fidc-live-trading-highmem177.service": {
|
||||
"pid": 3697762,
|
||||
"sha256": "e89d6ba68655a5d4a164793ad67a9003cf2c49733063018497339930feafaac3",
|
||||
"journal_since": "2026-09-13T22:21:09.122548+00:00",
|
||||
"journal_lines": 6,
|
||||
"error_lines": 0
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"commit": "d5b682c6d097",
|
||||
"pid": 1700096,
|
||||
"loaded_at": "2026-09-12T03:57:06.665536+00:00",
|
||||
"source_stale": false,
|
||||
"loaded_server_sha256": "ef827ce6b95e0ea63047a0068af2677633716e0a5d63cf350de6c91a3413e352"
|
||||
},
|
||||
"source_checkouts": {
|
||||
"fidc-backtest-engine": {
|
||||
"head": "9a54156df94cfbf11a1e6335ec6ef5449bd6ac17",
|
||||
"runtime_commit": "81acc5422878abc855fca72b35766ffad6159200",
|
||||
"tracked_dirty": false
|
||||
},
|
||||
"fidc-backtest-service": {
|
||||
"head": "5ec8dc86d99736a0c0140440bd039d11e118c1c6",
|
||||
"runtime_commit": "e81bf47806f5ac4ae4798bb5f5955a56638f754c",
|
||||
"tracked_dirty": false
|
||||
},
|
||||
"fidc-trading-platform": {
|
||||
"head": "94f99d20f49f6cd1810996706cb94f610c302385",
|
||||
"runtime_commit": "94f99d20f49f6cd1810996706cb94f610c302385",
|
||||
"tracked_dirty": false
|
||||
},
|
||||
"omniquant": {
|
||||
"head": "6a2b2604b40505fa754453307c517fef60743426",
|
||||
"runtime_commit": "6a2b2604b40505fa754453307c517fef60743426",
|
||||
"tracked_dirty": false
|
||||
}
|
||||
},
|
||||
"ui_unchanged": {
|
||||
"commit": "6a2b2604b40505fa754453307c517fef60743426",
|
||||
"pid": 3089476
|
||||
},
|
||||
"http_cases": [
|
||||
{
|
||||
"name": "manual_first",
|
||||
"run_id": "btr_req_44f1bb067559946ef22941a0c425ed53e47515e04b399e20",
|
||||
"status": "succeeded",
|
||||
"canonical_sha256": "0830216850b64d6e83291e072b31a9989f179915ee3341a75a77c73d1f9081a3",
|
||||
"trade_count": 10,
|
||||
"holding_count": 4,
|
||||
"final_equity": 9706248.648662,
|
||||
"old_result_unchanged": true
|
||||
},
|
||||
{
|
||||
"name": "automatic_first",
|
||||
"run_id": "btr_req_078f129ed46b55ba72b47605983a00ae3eef14b1995e1237",
|
||||
"status": "succeeded",
|
||||
"canonical_sha256": "c75cabcc03760f415bb664d20060e81c620d7a0201dd348ea71f75c932571de7",
|
||||
"trade_count": 10,
|
||||
"holding_count": 4,
|
||||
"final_equity": 9706248.648662,
|
||||
"old_result_unchanged": true
|
||||
},
|
||||
{
|
||||
"name": "stock24",
|
||||
"run_id": "btr_req_ef37b8ea403f489e4798d2878e0ef85966ace550f6b4f3c4",
|
||||
"status": "succeeded",
|
||||
"canonical_sha256": "270b403542ab41290c3d6e027b89cdab24dd41a8e2851d8786b33daa51e0051f",
|
||||
"trade_count": 51,
|
||||
"holding_count": 21,
|
||||
"final_equity": 9685563.876924999,
|
||||
"old_result_unchanged": true
|
||||
}
|
||||
],
|
||||
"durable_events": [
|
||||
{
|
||||
"run_id": "btr_req_44f1bb067559946ef22941a0c425ed53e47515e04b399e20",
|
||||
"count": 27,
|
||||
"unique_keys": 27,
|
||||
"days": 5
|
||||
},
|
||||
{
|
||||
"run_id": "btr_req_078f129ed46b55ba72b47605983a00ae3eef14b1995e1237",
|
||||
"count": 18,
|
||||
"unique_keys": 18,
|
||||
"days": 5
|
||||
},
|
||||
{
|
||||
"run_id": "btr_req_ef37b8ea403f489e4798d2878e0ef85966ace550f6b4f3c4",
|
||||
"count": 32,
|
||||
"unique_keys": 32,
|
||||
"days": 5
|
||||
},
|
||||
{
|
||||
"run_id": "btr_req_a3c3dfe5cd81e27e565064a65665f561c60adebdc6c9c9b4",
|
||||
"count": 27,
|
||||
"unique_keys": 27,
|
||||
"days": 5
|
||||
}
|
||||
],
|
||||
"trading_state": {
|
||||
"paper": {
|
||||
"configuration": {
|
||||
"count": 3,
|
||||
"hash": "93f3224edef59c381164e0236529dacc"
|
||||
},
|
||||
"active": {
|
||||
"claims": 0,
|
||||
"orders": 0
|
||||
}
|
||||
},
|
||||
"live": {
|
||||
"configuration": {
|
||||
"count": 0,
|
||||
"hash": "d41d8cd98f00b204e9800998ecf8427e"
|
||||
},
|
||||
"active": {
|
||||
"claims": 0,
|
||||
"orders": 1,
|
||||
"today_orders": 0,
|
||||
"orders_hash": "d4b56fbf3a541a41a383ad4e48891bb8",
|
||||
"route_mode": "disabled"
|
||||
}
|
||||
}
|
||||
},
|
||||
"manual_facts_unchanged": {
|
||||
"paper": {
|
||||
"shadow_configurations": 0,
|
||||
"shadow_runs": 0,
|
||||
"manual_count": 3,
|
||||
"manual_hash": "82572901ac0b5fdb4d8b984f71e1763d",
|
||||
"migrations_hash": "21d711b2ee52d2d66a8be4e99b179190",
|
||||
"new_orders": 0
|
||||
},
|
||||
"live": {
|
||||
"shadow_configurations": 0,
|
||||
"shadow_runs": 0,
|
||||
"manual_count": 0,
|
||||
"manual_hash": "d41d8cd98f00b204e9800998ecf8427e",
|
||||
"migrations_hash": "610528d4f350309379c9398c4ea43f66",
|
||||
"new_orders": 0
|
||||
}
|
||||
},
|
||||
"broker_submission": false,
|
||||
"linux_core_tests": {
|
||||
"passed": 834,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"log": "/srv/fidc/canonical/run/fidc-private/evidence/callback-candidate-6qumwky7/linux-core-tests.log"
|
||||
},
|
||||
"scope": "Callback and pending-target release verification; historical simulation only. Full manual shadow replay remains incomplete.",
|
||||
"native_replays": 6
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"full-linux-tests.log": {
|
||||
"original_path": "/data/fidc-cold/autonomous-research/fidc-cash-receivable-validation-20261005-d3f49a1/full-linux-tests.log",
|
||||
"bytes": 98059,
|
||||
"sha256": "7e25d69e1adec3183394a90a6c3dfab01c56982f9a4aa64afd261af69be72551"
|
||||
},
|
||||
"validation-receipt.json": {
|
||||
"original_path": "/data/fidc-cold/autonomous-research/fidc-cash-receivable-validation-20261005-d3f49a1/validation-receipt.json",
|
||||
"bytes": 1728,
|
||||
"sha256": "c92576e6ecb36727444baf4f0eeebd5a1d9f401cc60cc88f541482e7c2645640"
|
||||
},
|
||||
"validation-pg-receipt.json": {
|
||||
"original_path": "/data/fidc-cold/autonomous-research/fidc-cash-receivable-validation-20261005-d3f49a1/validation-pg-receipt.json",
|
||||
"bytes": 2771,
|
||||
"sha256": "eb2e367fc18a43297b70f9bd7967b317463369365291644d1383aaebb012eb7c"
|
||||
},
|
||||
"first-targeted-linux-tests.log": {
|
||||
"original_path": "/data/fidc-cold/autonomous-research/fidc-cash-receivable-validation-20261005-af655de/targeted-linux-tests.log",
|
||||
"bytes": 8284,
|
||||
"sha256": "10e4936faf4ac2955d2e3d9038f5d56c43e7eaccb0b62b6fcaa701bb68a4b1fd"
|
||||
},
|
||||
"first-full-linux-tests.log": {
|
||||
"original_path": "/data/fidc-cold/autonomous-research/fidc-cash-receivable-validation-20261005-af655de/full-linux-tests.log",
|
||||
"bytes": 101035,
|
||||
"sha256": "6e90211e4df5c91d81e55f3de4cbd6ed9b24b20cdf7c4927d7cdcadcdda41467"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,177 @@
|
||||
Compiling version_check v0.9.5
|
||||
Compiling proc-macro2 v1.0.106
|
||||
Compiling unicode-ident v1.0.24
|
||||
Compiling quote v1.0.45
|
||||
Compiling autocfg v1.5.0
|
||||
Compiling serde_core v1.0.228
|
||||
Compiling libc v0.2.184
|
||||
Compiling cfg-if v1.0.4
|
||||
Compiling crunchy v0.2.4
|
||||
Compiling generic-array v0.14.7
|
||||
Compiling tiny-keccak v2.0.2
|
||||
Compiling crossbeam-utils v0.8.22
|
||||
Compiling num-traits v0.2.19
|
||||
Compiling syn v2.0.117
|
||||
Compiling typenum v1.20.1
|
||||
Compiling serde v1.0.228
|
||||
Compiling portable-atomic v1.13.1
|
||||
Compiling crossbeam-epoch v0.9.20
|
||||
Compiling getrandom v0.2.17
|
||||
Compiling getrandom v0.3.4
|
||||
Compiling once_cell v1.21.4
|
||||
Compiling rustversion v1.0.22
|
||||
Compiling crossbeam-deque v0.8.7
|
||||
Compiling zerocopy v0.8.48
|
||||
Compiling const-random-macro v0.1.16
|
||||
Compiling smartstring v1.0.1
|
||||
Compiling ahash v0.8.12
|
||||
Compiling static_assertions v1.1.0
|
||||
Compiling rayon-core v1.13.0
|
||||
Compiling zmij v1.0.23
|
||||
Compiling const-random v0.1.18
|
||||
Compiling crypto-common v0.1.7
|
||||
Compiling block-buffer v0.10.4
|
||||
Compiling itoa v1.0.18
|
||||
Compiling thiserror v2.0.18
|
||||
Compiling serde_json v1.0.145
|
||||
Compiling rust_decimal v1.39.0
|
||||
Compiling castaway v0.2.4
|
||||
Compiling digest v0.10.7
|
||||
Compiling iana-time-zone v0.1.65
|
||||
Compiling ryu v1.0.23
|
||||
Compiling hashbrown v0.16.1
|
||||
Compiling equivalent v1.0.2
|
||||
Compiling bitflags v2.11.1
|
||||
Compiling smallvec v1.15.1
|
||||
Compiling either v1.17.0
|
||||
Compiling thin-vec v0.2.16
|
||||
Compiling serde_derive v1.0.228
|
||||
Compiling thiserror-impl v2.0.18
|
||||
Compiling rhai_codegen v3.1.0
|
||||
Compiling arrayvec v0.7.8
|
||||
Compiling memchr v2.8.0
|
||||
Compiling ta-lib-dispatch v0.1.2 (https://github.com/TA-Lib/ta-lib.git?rev=dd5a90259a3f9e04e2da9f38bf0719a841b40108#dd5a9025)
|
||||
Compiling cpufeatures v0.2.17
|
||||
Compiling sha2 v0.10.9
|
||||
Compiling rhai v1.23.6
|
||||
Compiling ta-lib v0.8.1 (https://github.com/TA-Lib/ta-lib.git?rev=dd5a90259a3f9e04e2da9f38bf0719a841b40108#dd5a9025)
|
||||
Compiling indexmap v2.11.4
|
||||
Compiling chrono v0.4.44
|
||||
Compiling compact_str v0.10.0
|
||||
Compiling rayon v1.12.0
|
||||
Compiling fidc-core v0.1.0 (/srv/fidc/programs/src/fidc-cash-receivable-validation-20261005-af655de/crates/fidc-core)
|
||||
Finished `test` profile [unoptimized] target(s) in 56.65s
|
||||
Running unittests src/lib.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/fidc_core-dbea4d4f6cb5d2d7)
|
||||
|
||||
running 18 tests
|
||||
test corporate_book::tests::closing_register_cannot_be_replaced_by_later_holdings ... ok
|
||||
test corporate_book::tests::dividend_receivable_survives_a_flat_position_without_becoming_spendable_cash ... ok
|
||||
test corporate_book::tests::entitlement_survives_complete_sale_and_a_successor_unit_conversion ... ok
|
||||
test corporate_book::tests::explicitly_delayed_dividend_remains_receivable_until_actual_payment ... ok
|
||||
test corporate_book::tests::a_late_corporate_batch_failure_keeps_prior_cash_positions_and_notes ... ok
|
||||
test corporate_book::tests::corporate_quantity_overflow_fails_without_saturation_or_a_negative_delta ... ok
|
||||
test corporate_book::tests::missing_accounting_reference_is_atomic_not_a_silent_cash_only_success ... ok
|
||||
test corporate_book::tests::manual_zero_skips_only_unused_allocation_facts_not_invalid_cash_evidence ... ok
|
||||
test corporate_book::tests::flat_position_retirement_does_not_change_issued_lot_state_or_next_purchase_origin ... ok
|
||||
test corporate_book::tests::per_share_precision_is_retained_until_the_registered_cash_amount_is_rounded ... ok
|
||||
test corporate_book::tests::missing_or_backward_cash_payment_date_never_updates_the_account ... ok
|
||||
test corporate_book::tests::missing_record_date_or_unobserved_closing_book_is_atomic ... ok
|
||||
test corporate_book::tests::post_record_buy_cannot_receive_the_earlier_dividend ... ok
|
||||
test corporate_book::tests::terminated_security_keeps_paid_cash_and_is_not_recreated_by_reinvestment ... ok
|
||||
test corporate_book::tests::sale_after_registration_preserves_cash_and_excludes_new_lots_from_cost_adjustment ... ok
|
||||
test corporate_book::tests::successor_without_frozen_instrument_metadata_is_not_an_implicit_new_security ... ok
|
||||
test corporate_book::tests::verified_conversion_preserves_lots_without_creating_orders_or_fills ... ok
|
||||
test corporate_book::tests::invalid_successor_terms_cannot_be_replaced_with_one_share_or_zero_cash ... ok
|
||||
|
||||
test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 764 filtered out; finished in 0.01s
|
||||
|
||||
Running unittests src/bin/dump_platform_runtime_schema.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/dump_platform_runtime_schema-c4b52627aec309c9)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/automatic_trade_protection.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/automatic_trade_protection-e76e4284ad200e69)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 6 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/core_rules.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/core_rules-6365eded07d721e8)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/corporate_actions.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/corporate_actions-05d8a8628f15e674)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 6 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/decision_quote_preload.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/decision_quote_preload-f63700247a753869)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 6 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/delisting.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/delisting-4a43fc52b1f023c5)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/dividend_reinvestment_contract.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/dividend_reinvestment_contract-69149bb471610f44)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/engine_hooks.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/engine_hooks-3b06d8e9ecb68f45)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 30 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/explicit_order_flow.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/explicit_order_flow-fe395fa4711b4010)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 58 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/futures_account.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/futures_account-47b777d1d163df6f)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 12 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/intraday_history_performance.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/intraday_history_performance-7ce6b1e7b0291409)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/manual_corporate_observation.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/manual_corporate_observation-8dc2a04a201f24a0)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 11 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/stock_pool_execution_contract.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/stock_pool_execution_contract-a11a2ea3e2392446)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 30 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/stock_pool_execution_state.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/stock_pool_execution_state-a8b965ddcfd186e1)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 8 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/strategy_selection.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/strategy_selection-bf5a72b2003c833f)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"artifact_bytes": 1728,
|
||||
"artifact_id": "strategy_research_fcae25fd6b283a74016bd99e",
|
||||
"artifact_path": "/data/fidc-cold/autonomous-research/fidc-cash-receivable-validation-20261005-d3f49a1/validation-receipt.json",
|
||||
"artifact_sha256": "c92576e6ecb36727444baf4f0eeebd5a1d9f401cc60cc88f541482e7c2645640",
|
||||
"artifact_type": "research_source_scope_probe",
|
||||
"contract_sha256": "39388b1bb63377e170145fcff03cee9bb3996283e5fa7c2c44453bbc82525e36",
|
||||
"created_at": "2026-10-04T19:06:15Z",
|
||||
"created_by": "boris",
|
||||
"department_id": null,
|
||||
"organization_id": "local_org",
|
||||
"persisted_at": "2026-10-04T19:06:15Z",
|
||||
"production_ready": false,
|
||||
"record_json": {
|
||||
"cash_entitlement_asset_included_without_spendable_cash_inference": true,
|
||||
"component": "fidc-backtest-engine",
|
||||
"contract_sha256": "31d7028d123a9abd2004d93534f3d8768704b2904529a3666d6146d67de36018",
|
||||
"engine_commit": "d3f49a17ef284dba587607ad0ac160cc394bf677",
|
||||
"ignored_benchmarks": 9,
|
||||
"log_reference": {
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-cash-receivable-validation-20261005-d3f49a1/full-linux-tests.log",
|
||||
"sha256": "7e25d69e1adec3183394a90a6c3dfab01c56982f9a4aa64afd261af69be72551"
|
||||
},
|
||||
"numeric_research_budget_reopened": false,
|
||||
"observed_at": "2026-10-04T19:06:11.362993+00:00",
|
||||
"original_numerical_research_executor": {
|
||||
"ActiveState": "active",
|
||||
"InvocationID": "0eb28e3a45694a00951aa3aec0e13465",
|
||||
"MainPID": "1396112"
|
||||
},
|
||||
"owner_uid": 1003,
|
||||
"payment_is_receivable_to_cash_asset_transfer_not_new_income": true,
|
||||
"production_ready": false,
|
||||
"production_runtime_deployed": false,
|
||||
"production_strategy_count": 0,
|
||||
"resource_contract": {
|
||||
"build_jobs": 4,
|
||||
"cpu_percent": 400,
|
||||
"memory_max_bytes": 17179869184,
|
||||
"memory_swap_max_bytes": 0,
|
||||
"rust_toolchain": "stable-x86_64-unknown-linux-gnu",
|
||||
"rustc": "1.98.0",
|
||||
"target_filesystem": "executable_tmpfs",
|
||||
"target_path": "/tmp/fidc-cash-receivable-build-20261005-af655de"
|
||||
},
|
||||
"schema_version": "fidc.linux-cash-receivable-equity-validation/v1",
|
||||
"sharedelivery_ledger_implemented": false,
|
||||
"source_path": "/srv/fidc/programs/src/fidc-cash-receivable-validation-20261005-d3f49a1",
|
||||
"status": "full_engine_regression_passed_not_runtime_or_strategy_acceptance",
|
||||
"tests_passed": 964,
|
||||
"tiny_nonzero_original_rate_preserved_and_dates_still_required": true,
|
||||
"trading_allowed": false
|
||||
},
|
||||
"research_key": "fidc-cash-receivable-equity-validation-20261005-d3f49a1",
|
||||
"schema_table": "strategy_research_artifact",
|
||||
"schema_version": "fidc.linux-cash-receivable-equity-validation/v1",
|
||||
"status": "full_engine_regression_passed_not_runtime_or_strategy_acceptance",
|
||||
"storage": "postgres"
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"cash_entitlement_asset_included_without_spendable_cash_inference": true,
|
||||
"component": "fidc-backtest-engine",
|
||||
"contract_sha256": "31d7028d123a9abd2004d93534f3d8768704b2904529a3666d6146d67de36018",
|
||||
"engine_commit": "d3f49a17ef284dba587607ad0ac160cc394bf677",
|
||||
"ignored_benchmarks": 9,
|
||||
"log_reference": {
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-cash-receivable-validation-20261005-d3f49a1/full-linux-tests.log",
|
||||
"sha256": "7e25d69e1adec3183394a90a6c3dfab01c56982f9a4aa64afd261af69be72551"
|
||||
},
|
||||
"numeric_research_budget_reopened": false,
|
||||
"observed_at": "2026-10-04T19:06:11.362993+00:00",
|
||||
"original_numerical_research_executor": {
|
||||
"ActiveState": "active",
|
||||
"InvocationID": "0eb28e3a45694a00951aa3aec0e13465",
|
||||
"MainPID": "1396112"
|
||||
},
|
||||
"owner_uid": 1003,
|
||||
"payment_is_receivable_to_cash_asset_transfer_not_new_income": true,
|
||||
"production_ready": false,
|
||||
"production_runtime_deployed": false,
|
||||
"production_strategy_count": 0,
|
||||
"resource_contract": {
|
||||
"build_jobs": 4,
|
||||
"cpu_percent": 400,
|
||||
"memory_max_bytes": 17179869184,
|
||||
"memory_swap_max_bytes": 0,
|
||||
"rust_toolchain": "stable-x86_64-unknown-linux-gnu",
|
||||
"rustc": "1.98.0",
|
||||
"target_filesystem": "executable_tmpfs",
|
||||
"target_path": "/tmp/fidc-cash-receivable-build-20261005-af655de"
|
||||
},
|
||||
"schema_version": "fidc.linux-cash-receivable-equity-validation/v1",
|
||||
"sharedelivery_ledger_implemented": false,
|
||||
"source_path": "/srv/fidc/programs/src/fidc-cash-receivable-validation-20261005-d3f49a1",
|
||||
"status": "full_engine_regression_passed_not_runtime_or_strategy_acceptance",
|
||||
"tests_passed": 964,
|
||||
"tiny_nonzero_original_rate_preserved_and_dates_still_required": true,
|
||||
"trading_allowed": false
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,258 @@
|
||||
{
|
||||
"verified_at": "2026-09-13T20:22:24.750379+00:00",
|
||||
"tag": "v2026.9.14.4",
|
||||
"processes": {
|
||||
"fidc-backtest-service-highmem177.service": {
|
||||
"pid": 3612875,
|
||||
"sha256": "4e9f142be0ae3f9ca8e1c126507d4a9905cde4b69859df4544472afd1bda1ff2",
|
||||
"journal_since": "2026-09-13T20:14:17.444770+00:00",
|
||||
"journal_lines": 54,
|
||||
"error_lines": 0
|
||||
},
|
||||
"fidc-trading-control-highmem177.service": {
|
||||
"pid": 3617963,
|
||||
"sha256": "cd587928591fef952f2e98b47aa338a1702def5edf016a7da9751296667f6674",
|
||||
"journal_since": "2026-09-13T20:19:57.821843+00:00",
|
||||
"journal_lines": 5,
|
||||
"error_lines": 0
|
||||
},
|
||||
"fidc-market-data-highmem177.service": {
|
||||
"pid": 3617964,
|
||||
"sha256": "2efb1d3ad7d510cf85e6047dd6d1981d0a30d768ff3d33c842adc52211002bdc",
|
||||
"journal_since": "2026-09-13T20:19:57.821843+00:00",
|
||||
"journal_lines": 5,
|
||||
"error_lines": 0
|
||||
},
|
||||
"fidc-strategy-runtime-highmem177.service": {
|
||||
"pid": 3618140,
|
||||
"sha256": "3d7f3f2e8756e7f3439075344fe9c8bc0b55df33e7e6f251282712274c00339d",
|
||||
"journal_since": "2026-09-13T20:19:57.821843+00:00",
|
||||
"journal_lines": 5,
|
||||
"error_lines": 0
|
||||
},
|
||||
"fidc-paper-trading-highmem177.service": {
|
||||
"pid": 3618260,
|
||||
"sha256": "0383b1d6cc7b3c48c6902dd7fd4760a38698c1916e0eaff63be26fe3f6b1a2ab",
|
||||
"journal_since": "2026-09-13T20:19:57.821843+00:00",
|
||||
"journal_lines": 6,
|
||||
"error_lines": 0
|
||||
},
|
||||
"fidc-live-trading-highmem177.service": {
|
||||
"pid": 3618246,
|
||||
"sha256": "583f52e204aeb416574ee17daa20cebed49e0659a194c81e8072a9249824e48e",
|
||||
"journal_since": "2026-09-13T20:19:57.821843+00:00",
|
||||
"journal_lines": 6,
|
||||
"error_lines": 0
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"commit": "d5b682c6d097",
|
||||
"pid": 1700096,
|
||||
"loaded_at": "2026-09-12T03:57:06.665536+00:00",
|
||||
"source_stale": false,
|
||||
"loaded_server_sha256": "ef827ce6b95e0ea63047a0068af2677633716e0a5d63cf350de6c91a3413e352"
|
||||
},
|
||||
"source_checkouts": {
|
||||
"fidc-backtest-engine": {
|
||||
"head": "9a54156df94cfbf11a1e6335ec6ef5449bd6ac17",
|
||||
"runtime_commit": "237ee15a518a668297959509daffc4b88995f310",
|
||||
"tracked_dirty": false
|
||||
},
|
||||
"fidc-backtest-service": {
|
||||
"head": "5ec8dc86d99736a0c0140440bd039d11e118c1c6",
|
||||
"runtime_commit": "e81bf47806f5ac4ae4798bb5f5955a56638f754c",
|
||||
"tracked_dirty": false
|
||||
},
|
||||
"fidc-trading-platform": {
|
||||
"head": "dab98e0cc09793df15b8c72841a6dc7e9a58a208",
|
||||
"runtime_commit": "dab98e0cc09793df15b8c72841a6dc7e9a58a208",
|
||||
"tracked_dirty": false
|
||||
},
|
||||
"omniquant": {
|
||||
"head": "6a2b2604b40505fa754453307c517fef60743426",
|
||||
"runtime_commit": "6a2b2604b40505fa754453307c517fef60743426",
|
||||
"tracked_dirty": false
|
||||
}
|
||||
},
|
||||
"ui_unchanged": {
|
||||
"commit": "6a2b2604b40505fa754453307c517fef60743426",
|
||||
"pid": 3089476
|
||||
},
|
||||
"http_cases": [
|
||||
{
|
||||
"name": "manual_first",
|
||||
"run_id": "btr_req_60612ec2af9f97df26a81c13448aec7d95b23a93f824c303",
|
||||
"status": "succeeded",
|
||||
"canonical_sha256": "0830216850b64d6e83291e072b31a9989f179915ee3341a75a77c73d1f9081a3",
|
||||
"trade_count": 10,
|
||||
"holding_count": 4,
|
||||
"final_equity": 9706248.648662,
|
||||
"old_result_unchanged": true
|
||||
},
|
||||
{
|
||||
"name": "automatic_first",
|
||||
"run_id": "btr_req_373da23c5ea5aaf4e59c38fbe37d663ae37731aeed8cc054",
|
||||
"status": "succeeded",
|
||||
"canonical_sha256": "c75cabcc03760f415bb664d20060e81c620d7a0201dd348ea71f75c932571de7",
|
||||
"trade_count": 10,
|
||||
"holding_count": 4,
|
||||
"final_equity": 9706248.648662,
|
||||
"old_result_unchanged": true
|
||||
},
|
||||
{
|
||||
"name": "stock24",
|
||||
"run_id": "btr_req_5bb965ea83e047c998ec16be656f40ec28a4e5d870aa6d74",
|
||||
"status": "succeeded",
|
||||
"canonical_sha256": "270b403542ab41290c3d6e027b89cdab24dd41a8e2851d8786b33daa51e0051f",
|
||||
"trade_count": 51,
|
||||
"holding_count": 21,
|
||||
"final_equity": 9685563.876924999,
|
||||
"old_result_unchanged": true
|
||||
}
|
||||
],
|
||||
"durable_events": [
|
||||
{
|
||||
"run_id": "btr_req_60612ec2af9f97df26a81c13448aec7d95b23a93f824c303",
|
||||
"count": 27,
|
||||
"unique_keys": 27,
|
||||
"days": 5
|
||||
},
|
||||
{
|
||||
"run_id": "btr_req_373da23c5ea5aaf4e59c38fbe37d663ae37731aeed8cc054",
|
||||
"count": 18,
|
||||
"unique_keys": 18,
|
||||
"days": 5
|
||||
},
|
||||
{
|
||||
"run_id": "btr_req_5bb965ea83e047c998ec16be656f40ec28a4e5d870aa6d74",
|
||||
"count": 32,
|
||||
"unique_keys": 32,
|
||||
"days": 5
|
||||
},
|
||||
{
|
||||
"run_id": "btr_req_a3c3dfe5cd81e27e565064a65665f561c60adebdc6c9c9b4",
|
||||
"count": 27,
|
||||
"unique_keys": 27,
|
||||
"days": 5
|
||||
}
|
||||
],
|
||||
"trading_state": {
|
||||
"paper": {
|
||||
"configuration": {
|
||||
"count": 3,
|
||||
"hash": "93f3224edef59c381164e0236529dacc"
|
||||
},
|
||||
"active": {
|
||||
"claims": 0,
|
||||
"orders": 0
|
||||
}
|
||||
},
|
||||
"live": {
|
||||
"configuration": {
|
||||
"count": 0,
|
||||
"hash": "d41d8cd98f00b204e9800998ecf8427e"
|
||||
},
|
||||
"active": {
|
||||
"claims": 0,
|
||||
"orders": 1,
|
||||
"today_orders": 0,
|
||||
"orders_hash": "d4b56fbf3a541a41a383ad4e48891bb8",
|
||||
"route_mode": "disabled"
|
||||
}
|
||||
}
|
||||
},
|
||||
"manual_facts_unchanged": {
|
||||
"paper": {
|
||||
"shadow_configurations": 0,
|
||||
"shadow_runs": 0,
|
||||
"manual_count": 3,
|
||||
"manual_hash": "82572901ac0b5fdb4d8b984f71e1763d",
|
||||
"migrations_hash": "21d711b2ee52d2d66a8be4e99b179190",
|
||||
"new_orders": 0
|
||||
},
|
||||
"live": {
|
||||
"shadow_configurations": 0,
|
||||
"shadow_runs": 0,
|
||||
"manual_count": 0,
|
||||
"manual_hash": "d41d8cd98f00b204e9800998ecf8427e",
|
||||
"migrations_hash": "610528d4f350309379c9398c4ea43f66",
|
||||
"new_orders": 0
|
||||
}
|
||||
},
|
||||
"broker_submission": false,
|
||||
"linux_core_tests": {
|
||||
"passed": 822,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"log": "/srv/fidc/canonical/run/fidc-private/evidence/clock-candidate-gqx8g70l/linux-core-tests.log"
|
||||
},
|
||||
"cleanup": {
|
||||
"apply": true,
|
||||
"deleted": [
|
||||
{
|
||||
"path": "/srv/fidc/canonical/build/holding-protection-stage-wywd2682/fidc-trading-platform/debug/incremental",
|
||||
"kind": "incremental_compiler_state",
|
||||
"bytes": 9553190912,
|
||||
"device": 2101,
|
||||
"inode": 39877787,
|
||||
"mtime_ns": 1789318996850462500,
|
||||
"links": 176,
|
||||
"size_bytes": 12288
|
||||
},
|
||||
{
|
||||
"path": "/srv/fidc/canonical/build/target-backtest/debug/incremental",
|
||||
"kind": "incremental_compiler_state",
|
||||
"bytes": 2103459840,
|
||||
"device": 2101,
|
||||
"inode": 29904450,
|
||||
"mtime_ns": 1789318494701447400,
|
||||
"links": 46,
|
||||
"size_bytes": 4096
|
||||
},
|
||||
{
|
||||
"path": "/srv/fidc/canonical/build/target-backtest/debug/deps/fidc_backtest_service-46310ff8aeeb4040",
|
||||
"kind": "superseded_test_binary",
|
||||
"bytes": 398401536,
|
||||
"device": 2101,
|
||||
"inode": 29934330,
|
||||
"mtime_ns": 1789117975169420000,
|
||||
"links": 1,
|
||||
"size_bytes": 399242488
|
||||
},
|
||||
{
|
||||
"path": "/srv/fidc/canonical/build/target-backtest/debug/deps/fidc_core-18c9b2429fdf6026",
|
||||
"kind": "superseded_test_binary",
|
||||
"bytes": 207015936,
|
||||
"device": 2101,
|
||||
"inode": 29918792,
|
||||
"mtime_ns": 1789166943885717800,
|
||||
"links": 1,
|
||||
"size_bytes": 207144208
|
||||
},
|
||||
{
|
||||
"path": "/srv/fidc/canonical/build/target-backtest/debug/deps/fidc_core-42f704a330411730",
|
||||
"kind": "superseded_test_binary",
|
||||
"bytes": 191205376,
|
||||
"device": 2101,
|
||||
"inode": 29933759,
|
||||
"mtime_ns": 1789117542401109800,
|
||||
"links": 1,
|
||||
"size_bytes": 191333360
|
||||
}
|
||||
],
|
||||
"reclaimed_allocated_bytes": 12453273600,
|
||||
"before": {
|
||||
"total": 1749269057536,
|
||||
"used": 1659299954688,
|
||||
"free": 1035599872
|
||||
},
|
||||
"after": {
|
||||
"total": 1749269057536,
|
||||
"used": 1647166930944,
|
||||
"free": 13168623616
|
||||
},
|
||||
"observed_free_change": 12133023744
|
||||
},
|
||||
"scope": "Intraday clock release verification; historical simulation only, not a performance or real broker liquidity acceptance.",
|
||||
"native_replays": 6
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"source": "/srv/fidc/programs/runtime-snapshots/lot-lifecycle-20260914-AEGDYL",
|
||||
"build": "/srv/fidc/canonical/build/lot-lifecycle-20260914-AEGDYL",
|
||||
"commits": {
|
||||
"fidc-backtest-engine": "b4c68be29b98dc814fa75641f629dd18e1dd803b",
|
||||
"fidc-backtest-service": "49f280075e6b9dce2ef149fc190cfe663411b905",
|
||||
"fidc-trading-platform": "ae83fd30f56a5420a235022b0a169aaf0bae55cf"
|
||||
},
|
||||
"source_unchanged": true,
|
||||
"results": [
|
||||
{
|
||||
"repository": "fidc-backtest-engine",
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 60.008,
|
||||
"passed": 889,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"log_sha256": "7d0760a024a564cd2c18b7bf3b41ab8ba6f95d7e259d49a43191f0ebe666989e"
|
||||
},
|
||||
{
|
||||
"repository": "fidc-trading-platform",
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 102.011,
|
||||
"passed": 625,
|
||||
"failed": 0,
|
||||
"ignored": 63,
|
||||
"log_sha256": "ab55da8d47425a0acc6c33dc4bc352f463ad4e1aac887b0169a90f18cb6763cb"
|
||||
},
|
||||
{
|
||||
"repository": "fidc-backtest-service",
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 118.011,
|
||||
"passed": 589,
|
||||
"failed": 0,
|
||||
"ignored": 16,
|
||||
"log_sha256": "ff6946b18567dad6dab41b6c36f65ea1181d7ee6b7d53b6ceb568b1dedb7abae"
|
||||
}
|
||||
],
|
||||
"available_bytes": 94029492224
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"observed_date": "2026-09-14",
|
||||
"status": "tested_not_deployed",
|
||||
"source_snapshot_manifest_sha256": "5560614fa2ce2b0c905c44011ff4147df1fe7c04be93cd3c1410cd16fca025de",
|
||||
"immutable_source_file_count": 962,
|
||||
"test_scope": "fidc-lot-lifecycle-check-20260914-AEGDYL.scope",
|
||||
"test_cpu_quota_percent": 400,
|
||||
"test_memory_max_gib": 12,
|
||||
"source_runtime_commit": "d5b682c6d09704ff23d725a8dd8b155db3eb6967",
|
||||
"source_runtime_pid": 1700096,
|
||||
"source_runtime_changed": false,
|
||||
"runtime_services": [
|
||||
{
|
||||
"service": "omniquant-highmem177.service",
|
||||
"pid": 987464,
|
||||
"state": "active",
|
||||
"version": "v2026.9.14.8"
|
||||
},
|
||||
{
|
||||
"service": "fidc-backtest-service-highmem177.service",
|
||||
"pid": 4108679,
|
||||
"state": "active",
|
||||
"exe_sha256": "5933b7423c6722e5fe644a604c915a27ceb89900f690b092d1689bfe0fe389b7"
|
||||
},
|
||||
{
|
||||
"service": "fidc-paper-trading-highmem177.service",
|
||||
"pid": 3765829,
|
||||
"state": "active",
|
||||
"exe_sha256": "3ebd7ce815e325ff0bb38632ef5b5e5cd5aff7a4e0601ba0229feb3201cdefe3"
|
||||
},
|
||||
{
|
||||
"service": "fidc-live-trading-highmem177.service",
|
||||
"pid": 3765783,
|
||||
"state": "active",
|
||||
"exe_sha256": "1f4b2d697accdbb76d89cd7cb9c3ae4dad942d86e3dde2e7d211eee5879b9144"
|
||||
},
|
||||
{
|
||||
"service": "fidc-strategy-runtime-highmem177.service",
|
||||
"pid": 3765705,
|
||||
"state": "active",
|
||||
"exe_sha256": "5a160ba9588f0ba9275f7d33152667be176cece310fc3026d4c043d897d44b4c"
|
||||
},
|
||||
{
|
||||
"service": "fidc-trading-control-highmem177.service",
|
||||
"pid": 3765532,
|
||||
"state": "active",
|
||||
"exe_sha256": "9feade5eed04007595f6ef88e076a3f501631b06cb6dc9d9b6d7f5be379b0a20"
|
||||
}
|
||||
],
|
||||
"engine_development_checkout": "b4c68be29b98dc814fa75641f629dd18e1dd803b",
|
||||
"preserved_service_development_checkout": "5991e7733d9e2a770f740e971924b12cc5f7d29d",
|
||||
"production_order_or_cancel_requests_by_this_verification": 0,
|
||||
"production_task_configuration_writes_by_this_verification": 0,
|
||||
"remaining": [
|
||||
"Source freeze authorization and physical partition validation",
|
||||
"Actual Source and Runner shadow joint acceptance",
|
||||
"Unaccepted Arrow performance candidate gate",
|
||||
"Delayed observations spanning already applied corporate actions and remaining cross-mode matrix"
|
||||
],
|
||||
"source_runtime_tracked_clean": true
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"source": "/srv/fidc/programs/runtime-snapshots/corporate-replay-20260914-ZAnvlO",
|
||||
"build": "/srv/fidc/canonical/build/corporate-replay-20260914-ZAnvlO",
|
||||
"commits": {
|
||||
"fidc-backtest-engine": "05f1cbbe00ffe75b72de8a24cea6a4e40e8142b0",
|
||||
"fidc-backtest-service": "879a5a7fe07620415b00023f4628cc81e45903fb",
|
||||
"fidc-trading-platform": "559f5b1d24d98a67606e859a1f2105bee4c0bc01"
|
||||
},
|
||||
"source_unchanged": true,
|
||||
"results": [
|
||||
{
|
||||
"repository": "fidc-backtest-engine",
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 58.007,
|
||||
"passed": 895,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"log_sha256": "d50fc04699eaa39399016e3b9f470e9adf3e8da0d968ada2e212ef606bdfb37a"
|
||||
},
|
||||
{
|
||||
"repository": "fidc-trading-platform",
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 94.009,
|
||||
"passed": 625,
|
||||
"failed": 0,
|
||||
"ignored": 63,
|
||||
"log_sha256": "eafd3d792dc54ff90efaae9aa1e9e0c15bb191f8b679585c982b8c1bab3211ad"
|
||||
},
|
||||
{
|
||||
"repository": "fidc-backtest-service",
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 148.015,
|
||||
"passed": 591,
|
||||
"failed": 0,
|
||||
"ignored": 16,
|
||||
"log_sha256": "2ae31905c43697177af50825270d4f7830361d6e1d5e3d22a26179dd726abe36"
|
||||
}
|
||||
],
|
||||
"available_bytes": 90635833344
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"observed_date": "2026-09-14",
|
||||
"status": "tested_not_deployed",
|
||||
"source_snapshot_manifest_sha256": "9b61a3586248c80bac30ce2107c9323a4d9a1ac7bdf29a42d3c423bd5c615ec4",
|
||||
"immutable_source_file_count": 972,
|
||||
"test_scope": "fidc-corporate-replay-check-20260914-ZAnvlO.scope",
|
||||
"test_cpu_quota_percent": 400,
|
||||
"test_memory_max_gib": 12,
|
||||
"source_runtime_commit": "d5b682c6d09704ff23d725a8dd8b155db3eb6967",
|
||||
"source_runtime_pid": 1700096,
|
||||
"source_runtime_changed": false,
|
||||
"runtime_services": [
|
||||
{
|
||||
"service": "omniquant-highmem177.service",
|
||||
"pid": 987464,
|
||||
"state": "active",
|
||||
"version": "v2026.9.14.8"
|
||||
},
|
||||
{
|
||||
"service": "fidc-backtest-service-highmem177.service",
|
||||
"pid": 4108679,
|
||||
"state": "active",
|
||||
"exe_sha256": "5933b7423c6722e5fe644a604c915a27ceb89900f690b092d1689bfe0fe389b7"
|
||||
},
|
||||
{
|
||||
"service": "fidc-paper-trading-highmem177.service",
|
||||
"pid": 3765829,
|
||||
"state": "active",
|
||||
"exe_sha256": "3ebd7ce815e325ff0bb38632ef5b5e5cd5aff7a4e0601ba0229feb3201cdefe3"
|
||||
},
|
||||
{
|
||||
"service": "fidc-live-trading-highmem177.service",
|
||||
"pid": 3765783,
|
||||
"state": "active",
|
||||
"exe_sha256": "1f4b2d697accdbb76d89cd7cb9c3ae4dad942d86e3dde2e7d211eee5879b9144"
|
||||
},
|
||||
{
|
||||
"service": "fidc-strategy-runtime-highmem177.service",
|
||||
"pid": 3765705,
|
||||
"state": "active",
|
||||
"exe_sha256": "5a160ba9588f0ba9275f7d33152667be176cece310fc3026d4c043d897d44b4c"
|
||||
},
|
||||
{
|
||||
"service": "fidc-trading-control-highmem177.service",
|
||||
"pid": 3765532,
|
||||
"state": "active",
|
||||
"exe_sha256": "9feade5eed04007595f6ef88e076a3f501631b06cb6dc9d9b6d7f5be379b0a20"
|
||||
}
|
||||
],
|
||||
"engine_development_checkout": "05f1cbbe00ffe75b72de8a24cea6a4e40e8142b0",
|
||||
"preserved_service_development_checkout": "5991e7733d9e2a770f740e971924b12cc5f7d29d",
|
||||
"production_order_or_cancel_requests_by_this_verification": 0,
|
||||
"production_task_configuration_writes_by_this_verification": 0,
|
||||
"remaining": [
|
||||
"Source freeze authorization and physical partition validation",
|
||||
"Actual Source and Runner shadow joint acceptance",
|
||||
"Unaccepted Arrow performance candidate gate",
|
||||
"Dividend reinvestment execution clocks, successor data scope, target state and cross-mode ETF matrix"
|
||||
],
|
||||
"source_runtime_tracked_clean": true
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"source": "/srv/fidc/programs/runtime-snapshots/zero-reinvestment-20260915-Yovt6m",
|
||||
"build": "/srv/fidc/canonical/build/zero-reinvestment-20260915-Yovt6m",
|
||||
"commits": {
|
||||
"fidc-backtest-engine": "818552bc969e0ea40081f0770c1d97db574a1981",
|
||||
"fidc-backtest-service": "234d85bdb8ed12b552ba8e9eff47fddc5e683acc",
|
||||
"fidc-trading-platform": "d7d492541e60eb16e58dbff74b664b8ac85f6ccf"
|
||||
},
|
||||
"source_unchanged": true,
|
||||
"results": [
|
||||
{
|
||||
"repository": "fidc-backtest-engine",
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 56.008,
|
||||
"passed": 919,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"log_sha256": "87a4daa4db1c0f735399ce243feff8e514e5f75f72b0dbfdb69da0c21d02f2b4"
|
||||
},
|
||||
{
|
||||
"repository": "fidc-trading-platform",
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 90.029,
|
||||
"passed": 625,
|
||||
"failed": 0,
|
||||
"ignored": 63,
|
||||
"log_sha256": "e7f15eb7463f1cc33b7122020549af082d2070ad38392d0918c9c7309a68cc26"
|
||||
},
|
||||
{
|
||||
"repository": "fidc-backtest-service",
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 112.014,
|
||||
"passed": 594,
|
||||
"failed": 0,
|
||||
"ignored": 16,
|
||||
"log_sha256": "4faa52e9a87056c192341622d0273942f3daf963cd75a381ad913c76d47aed29"
|
||||
}
|
||||
],
|
||||
"available_bytes": 77131395072
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"date": "2026-09-15",
|
||||
"baseline_engine": "534ab42906c7e7a4e253b7791cc143dec894a7f2",
|
||||
"candidate_engine": "818552bc969e0ea40081f0770c1d97db574a1981",
|
||||
"reproduced": {
|
||||
"manual_zero_effective_before_payment": true,
|
||||
"original_position_cleared": true,
|
||||
"unexpected_accounting_allocation": {
|
||||
"symbol": "000001.SZ",
|
||||
"quantity": 100,
|
||||
"price": 8.95
|
||||
},
|
||||
"original_gross_output": 894.9999999999999,
|
||||
"actual_posted_amount": 895
|
||||
},
|
||||
"verified": {
|
||||
"global_set_and_scale_zero_leave_cash": true,
|
||||
"zero_or_excluded_manual_member_leaves_cash": true,
|
||||
"same_instant_uses_sequence": true,
|
||||
"input_array_order_does_not_change_result": true,
|
||||
"future_settings_are_not_backdated": true,
|
||||
"restore_does_not_mean_forced_full_exposure": true,
|
||||
"legacy_day_granularity_preserved": true,
|
||||
"normal_settlement_and_late_replay_share_control": true,
|
||||
"actual_manual_fills_and_cash_entitlements_not_erased": true,
|
||||
"invalid_cash_is_rejected_atomically": true,
|
||||
"unused_reinvestment_reference_not_read_under_zero": true,
|
||||
"no_control_keeps_legacy_price_quantity_and_fees": true
|
||||
},
|
||||
"exact_outcomes": {
|
||||
"cleared_with_zero": {
|
||||
"cash": 49998,
|
||||
"quantity": 0
|
||||
},
|
||||
"cleared_without_zero": {
|
||||
"cash": 49103,
|
||||
"quantity": 100,
|
||||
"price": 8.95,
|
||||
"fee": 0,
|
||||
"gross": 895
|
||||
},
|
||||
"actual_extra_buy_timely_and_late": {
|
||||
"cash": 41047,
|
||||
"quantity": 1000,
|
||||
"corporate_cash_adjustment": 1050,
|
||||
"optional_allocations": 0
|
||||
}
|
||||
},
|
||||
"local_tests": {
|
||||
"core": {
|
||||
"passed": 919,
|
||||
"ignored": 9
|
||||
},
|
||||
"trading": {
|
||||
"passed": 625,
|
||||
"ignored": 63
|
||||
},
|
||||
"runner": {
|
||||
"passed": 463,
|
||||
"ignored": 9
|
||||
},
|
||||
"api": {
|
||||
"passed": 129,
|
||||
"ignored": 7
|
||||
}
|
||||
},
|
||||
"new_test_functions": 6,
|
||||
"production_runtime_modified": false,
|
||||
"production_order_or_cancel_requests": 0,
|
||||
"limits": [
|
||||
"isolated replay not actual Source/Runner acceptance",
|
||||
"does not evaluate arbitrary strategy expressions at settlement",
|
||||
"Source runtime freeze unchanged",
|
||||
"no new production UI or broker validation"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"observed_at": "2026-09-14T16:34:44.320112+00:00",
|
||||
"status": "tested_not_deployed",
|
||||
"runtime_services": [
|
||||
{
|
||||
"service": "omniquant-highmem177.service",
|
||||
"pid": 987464,
|
||||
"state": "active"
|
||||
},
|
||||
{
|
||||
"service": "fidc-backtest-service-highmem177.service",
|
||||
"pid": 4108679,
|
||||
"state": "active",
|
||||
"exe_sha256": "5933b7423c6722e5fe644a604c915a27ceb89900f690b092d1689bfe0fe389b7"
|
||||
},
|
||||
{
|
||||
"service": "fidc-paper-trading-highmem177.service",
|
||||
"pid": 3765829,
|
||||
"state": "active",
|
||||
"exe_sha256": "3ebd7ce815e325ff0bb38632ef5b5e5cd5aff7a4e0601ba0229feb3201cdefe3"
|
||||
},
|
||||
{
|
||||
"service": "fidc-live-trading-highmem177.service",
|
||||
"pid": 3765783,
|
||||
"state": "active",
|
||||
"exe_sha256": "1f4b2d697accdbb76d89cd7cb9c3ae4dad942d86e3dde2e7d211eee5879b9144"
|
||||
},
|
||||
{
|
||||
"service": "fidc-strategy-runtime-highmem177.service",
|
||||
"pid": 3765705,
|
||||
"state": "active",
|
||||
"exe_sha256": "5a160ba9588f0ba9275f7d33152667be176cece310fc3026d4c043d897d44b4c"
|
||||
},
|
||||
{
|
||||
"service": "fidc-trading-control-highmem177.service",
|
||||
"pid": 3765532,
|
||||
"state": "active",
|
||||
"exe_sha256": "9feade5eed04007595f6ef88e076a3f501631b06cb6dc9d9b6d7f5be379b0a20"
|
||||
}
|
||||
],
|
||||
"source_runtime_commit": "d5b682c6d09704ff23d725a8dd8b155db3eb6967",
|
||||
"source_runtime_tracked_clean": true,
|
||||
"engine_development_checkout": "818552bc969e0ea40081f0770c1d97db574a1981",
|
||||
"preserved_service_development_checkout": "5991e7733d9e2a770f740e971924b12cc5f7d29d",
|
||||
"immutable_source_file_count": 989,
|
||||
"source_snapshot_manifest_sha256": "9ffdc1bc86e1b4b085caa694e921fc230ce0f09c57d34254058ae28e631d5bb3",
|
||||
"test_scope": "fidc-zero-reinvestment-check-20260915-Yovt6m.scope",
|
||||
"test_cpu_quota_percent": 400,
|
||||
"test_memory_max_gib": 12,
|
||||
"source_runtime_modified": false,
|
||||
"production_order_or_cancel_requests_by_this_verification": 0,
|
||||
"production_task_configuration_writes_by_this_verification": 0
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"source": "/srv/fidc/programs/runtime-snapshots/opening-clock-20260914-UUx5ru",
|
||||
"build": "/srv/fidc/canonical/build/opening-clock-20260914-UUx5ru",
|
||||
"commits": {
|
||||
"fidc-backtest-engine": "13c89e8d59f21df6280d722f369d0ec8d9a6457e",
|
||||
"fidc-backtest-service": "49f280075e6b9dce2ef149fc190cfe663411b905",
|
||||
"fidc-trading-platform": "ae83fd30f56a5420a235022b0a169aaf0bae55cf"
|
||||
},
|
||||
"source_unchanged": true,
|
||||
"results": [
|
||||
{
|
||||
"repository": "fidc-backtest-engine",
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 56.007,
|
||||
"passed": 885,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"log_sha256": "9a887a23fc656d0f448502ecb2fca5fcfd82db15346f567e6a8b0bcaa88180d7"
|
||||
},
|
||||
{
|
||||
"repository": "fidc-trading-platform",
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 90.054,
|
||||
"passed": 625,
|
||||
"failed": 0,
|
||||
"ignored": 63,
|
||||
"log_sha256": "048a7690ce874bccc58e3111decd183c3f88f649397c59cdfbaa3e4533662589"
|
||||
},
|
||||
{
|
||||
"repository": "fidc-backtest-service",
|
||||
"exit_code": 0,
|
||||
"elapsed_seconds": 118.011,
|
||||
"passed": 589,
|
||||
"failed": 0,
|
||||
"ignored": 16,
|
||||
"log_sha256": "ec4f51b30dd69d9c86812cbb1b1e310c277fdfcf29465978b8d87eb83f40204c"
|
||||
}
|
||||
],
|
||||
"available_bytes": 97373085696
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"observed_date": "2026-09-14",
|
||||
"status": "tested_not_deployed",
|
||||
"source_snapshot_manifest_sha256": "bf01cd040bdcbeaa0c81a93153eb8e505dd7dcb8c761731f7f3ed81b1b2b6b67",
|
||||
"immutable_source_file_count": 959,
|
||||
"test_scope": "fidc-opening-clock-check-20260914-UUx5ru.scope",
|
||||
"test_cpu_quota_percent": 400,
|
||||
"test_memory_max_gib": 12,
|
||||
"source_runtime_commit": "d5b682c6d09704ff23d725a8dd8b155db3eb6967",
|
||||
"source_runtime_pid": 1700096,
|
||||
"source_runtime_changed": false,
|
||||
"runtime_services": [
|
||||
{
|
||||
"service": "omniquant-highmem177.service",
|
||||
"pid": 987464,
|
||||
"state": "active",
|
||||
"version": "v2026.9.14.8"
|
||||
},
|
||||
{
|
||||
"service": "fidc-backtest-service-highmem177.service",
|
||||
"pid": 4108679,
|
||||
"state": "active",
|
||||
"exe_sha256": "5933b7423c6722e5fe644a604c915a27ceb89900f690b092d1689bfe0fe389b7"
|
||||
},
|
||||
{
|
||||
"service": "fidc-paper-trading-highmem177.service",
|
||||
"pid": 3765829,
|
||||
"state": "active",
|
||||
"exe_sha256": "3ebd7ce815e325ff0bb38632ef5b5e5cd5aff7a4e0601ba0229feb3201cdefe3"
|
||||
},
|
||||
{
|
||||
"service": "fidc-live-trading-highmem177.service",
|
||||
"pid": 3765783,
|
||||
"state": "active",
|
||||
"exe_sha256": "1f4b2d697accdbb76d89cd7cb9c3ae4dad942d86e3dde2e7d211eee5879b9144"
|
||||
},
|
||||
{
|
||||
"service": "fidc-strategy-runtime-highmem177.service",
|
||||
"pid": 3765705,
|
||||
"state": "active",
|
||||
"exe_sha256": "5a160ba9588f0ba9275f7d33152667be176cece310fc3026d4c043d897d44b4c"
|
||||
},
|
||||
{
|
||||
"service": "fidc-trading-control-highmem177.service",
|
||||
"pid": 3765532,
|
||||
"state": "active",
|
||||
"exe_sha256": "9feade5eed04007595f6ef88e076a3f501631b06cb6dc9d9b6d7f5be379b0a20"
|
||||
}
|
||||
],
|
||||
"engine_development_checkout": "13c89e8d59f21df6280d722f369d0ec8d9a6457e",
|
||||
"preserved_service_development_checkout": "5991e7733d9e2a770f740e971924b12cc5f7d29d",
|
||||
"production_order_or_cancel_requests_by_this_verification": 0,
|
||||
"production_task_configuration_writes_by_this_verification": 0,
|
||||
"remaining": [
|
||||
"Source freeze authorization and physical partition validation",
|
||||
"Actual Source and Runner shadow joint acceptance",
|
||||
"Unaccepted Arrow performance candidate gate",
|
||||
"Remaining corporate-action, holding-protection and full parameter matrix"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"bytes": 827,
|
||||
"file": "validation-probe-assembly-failure.json",
|
||||
"sha256": "7e648d3fc3d3e77b6ce5c30f1c7dc49035d0a0467e433ea2afd0ed2256fe451f"
|
||||
}
|
||||
],
|
||||
"original_manifest_sha256": "e26c6bb619b9d0f0183983da3e8c030dfc61e610cf0b3a4c2311d3525cc85355",
|
||||
"schema_version": "fidc.additional-share-delivery-evidence/v1"
|
||||
}
|
||||
@@ -0,0 +1,853 @@
|
||||
Compiling fidc-core v0.1.0 (/srv/fidc/programs/src/fidc-share-receivable-validation-20261005-0caac44/crates/fidc-core)
|
||||
Finished `test` profile [unoptimized] target(s) in 41.35s
|
||||
Running unittests src/lib.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/fidc_core-dbea4d4f6cb5d2d7)
|
||||
|
||||
running 786 tests
|
||||
test broker::tests::aiquant_rules_keep_structured_buy_risk_while_using_aiquant_limit_price ... ok
|
||||
test broker::tests::aiquant_sell_rule_still_blocks_real_allow_sell_false ... ok
|
||||
test broker::tests::aiquant_sell_rule_uses_intraday_quote_when_daily_snapshot_is_stale_lower_limit ... ok
|
||||
test broker::tests::aiquant_target_portfolio_smart_keeps_batch_semantics_after_failed_full_close_sell ... ok
|
||||
test broker::tests::aiquant_target_portfolio_smart_rejects_deferred_buy_risk_at_order_stage ... ok
|
||||
test broker::stock_pool::successor_protection_tests::deferred_etf_open_rechecks_inherited_locks_and_cooldown_before_any_order ... ok
|
||||
test broker::tests::aiquant_target_value_delta_uses_scheduled_mark_price ... ok
|
||||
test broker::tests::algorithm_clock::algorithm_expiry_without_a_quote_does_not_reuse_old_liquidity ... ok
|
||||
test broker::tests::algorithm_clock::a_clock_slice_does_not_turn_window_twap_into_an_unlimited_instant_order ... ok
|
||||
test broker::tests::algorithm_clock::algorithm_sell_honors_t_plus_one_and_keeps_original_quantity_after_partial_fills ... ok
|
||||
test broker::tests::algorithm_clock::an_explicit_ioc_or_fok_does_not_become_a_persistent_algorithm ... ok
|
||||
test broker::tests::algorithm_clock::control_only_phase_cannot_be_used_to_submit_an_order_or_leave_matching_disabled ... ok
|
||||
test broker::tests::algorithm_clock::non_matching_controls_amend_or_cancel_without_filling_a_crossing_quote ... ok
|
||||
test broker::tests::algorithm_clock::changing_the_later_daily_close_does_not_resize_an_algorithm_submitted_now ... ok
|
||||
test broker::tests::algorithm_clock::global_vwap_matching_keeps_the_same_working_order_between_clock_ticks ... ok
|
||||
test broker::tests::algorithm_clock::partial_algorithm_cancel_releases_reservation_and_never_executes_the_remainder ... ok
|
||||
test broker::tests::algorithm_clock::separate_buy_cannot_spend_the_working_algorithms_cash_budget ... ok
|
||||
test broker::tests::algorithm_clock::two_working_algorithms_reserve_only_real_cash_without_starving_the_first ... ok
|
||||
test broker::tests::complete_daily_target_batch_uses_positive_target_count_for_slot_limit ... ok
|
||||
test broker::tests::daily_capacity_requires_a_timed_observation_instead_of_falling_back_to_total_volume ... ok
|
||||
test broker::tests::algorithm_clock::twap_clock_preserves_quantity_prices_fees_budget_and_parent_order ... ok
|
||||
test broker::tests::daily_matching_does_not_require_intraday_quote_quantity ... ok
|
||||
test broker::tests::custom_valuation_missing_with_current_market_still_fails_closed ... ok
|
||||
test broker::tests::decision_buy_denial_allows_reducing_an_existing_buy ... ok
|
||||
test broker::tests::algorithm_clock::vwap_clock_preserves_cash_costs_and_does_not_spend_future_volume ... ok
|
||||
test broker::tests::decision_buy_denial_does_not_rewrite_existing_pending_order ... ok
|
||||
test broker::tests::decision_buy_denial_blocks_topup_but_allows_sell_and_does_not_leak ... ok
|
||||
test broker::tests::daily_session_volume_changes_only_audit_not_opening_fills ... ok
|
||||
test broker::tests::decision_buy_denial_rejects_increasing_amendments_without_mutation ... ok
|
||||
test broker::tests::each_execution_leg_rechecks_one_yuan_including_slippage_and_limit_price ... ok
|
||||
test broker::tests::explicit_quote_price_models_do_not_replace_missing_prices_or_reuse_older_ones ... ok
|
||||
test broker::tests::decision_buy_denial_uses_actual_next_open_target_delta ... ok
|
||||
test broker::tests::failed_target_exit_blocks_replacement_entry_when_no_position_slot_is_released ... ok
|
||||
test broker::tests::gtc_resting_order_keeps_its_session_and_original_dates_across_days ... ok
|
||||
test broker::tests::deferred_etf_batch_failure_keeps_both_targets_and_prior_generation_progress ... ok
|
||||
test broker::tests::failed_new_batch_does_not_erase_prior_success_or_double_charge_on_retry ... ok
|
||||
test broker::tests::historical_slippage_rejects_missing_future_or_invalid_calibration_without_raw_price_fallback ... ok
|
||||
test broker::tests::instantaneous_twap_without_limits_does_not_cap_quote_quantity ... ok
|
||||
test broker::tests::intraday_execution_rejects_buy_at_upper_limit_price ... ok
|
||||
test broker::tests::independent_target_entry_is_not_subject_to_replacement_slot_limit ... ok
|
||||
test broker::tests::intraday_execution_rejects_sell_at_lower_limit_price ... ok
|
||||
test broker::tests::later_execution_clocks_do_not_replenish_the_same_observed_volume ... ok
|
||||
test broker::tests::minute_last_exact_time_does_not_fallback_to_older_liquid_quote ... ok
|
||||
test broker::tests::minute_last_execution_uses_latest_quote_before_decision_time ... ok
|
||||
test broker::tests::market_share_buy_skips_quantity_that_rounds_below_one_lot ... ok
|
||||
test broker::tests::minute_last_prefilter_does_not_use_snapshot_minute_volume ... ok
|
||||
test broker::tests::minute_last_quote_quantity_cap_depends_on_liquidity_limit ... ok
|
||||
test broker::tests::minute_last_uses_volume_delta_when_level1_depth_missing ... ok
|
||||
test broker::tests::minute_last_volume_limit_caps_quote_volume_delta_without_liquidity_limit ... ok
|
||||
test broker::tests::minute_last_volume_limit_rejects_sub_lot_quote_volume ... ok
|
||||
test broker::tests::minute_last_without_volume_or_liquidity_limit_does_not_cap_quote_quantity ... ok
|
||||
test broker::tests::historical_slippage_does_not_read_later_daily_fields_for_open_or_minute_fills ... ok
|
||||
test broker::tests::next_open_buy_limit_risk_rejects_open_upper_limit_even_when_close_is_clean ... ok
|
||||
test broker::tests::next_open_buy_limit_risk_uses_open_not_close ... ok
|
||||
test broker::tests::next_open_buy_risk_rejects_execution_date_delisted_even_without_market_snapshot ... ok
|
||||
test broker::tests::next_open_buy_risk_uses_execution_date_not_signal_date ... ok
|
||||
test broker::tests::next_open_day_limit_remainder_matches_intraday_with_execution_day_ttl ... ok
|
||||
test broker::tests::next_open_sell_limit_risk_rejects_open_lower_limit_even_when_close_is_clean ... ok
|
||||
test broker::tests::next_open_buy_risk_rejects_execution_date_st_even_if_signal_date_was_clean ... ok
|
||||
test broker::tests::next_open_sell_limit_risk_uses_open_not_close ... ok
|
||||
test broker::tests::next_open_sell_risk_rejects_execution_date_lower_limit_even_if_signal_date_was_clean ... ok
|
||||
test broker::tests::next_open_sell_risk_uses_execution_date_not_signal_date ... ok
|
||||
test broker::tests::next_open_target_portfolio_unpriced_symbol_does_not_abort_batch ... ok
|
||||
test broker::tests::next_open_target_value_valuation_uses_execution_open ... ok
|
||||
test broker::tests::next_open_target_value_recomputes_quantity_from_execution_open ... ok
|
||||
test broker::tests::next_open_unpriced_active_snapshot_rejects_without_historical_fill ... ok
|
||||
test broker::tests::open_order_upsert_replaces_in_place_and_preserves_queue_position ... ok
|
||||
test broker::tests::next_open_with_observations_uses_opening_volume_not_daily_totals_or_future_quotes ... ok
|
||||
test broker::tests::next_open_unpriced_paused_snapshot_prefers_execution_day_pause_risk ... ok
|
||||
test broker::tests::post_close_phase_is_derived_from_actual_same_day_submission_time ... ok
|
||||
test broker::tests::post_close_order_uses_close_without_slippage_and_waits_until_matching_window ... ok
|
||||
test broker::tests::queued_order_retains_the_real_creation_clock_when_retried ... ok
|
||||
test broker::tests::session_audit_does_not_infer_an_opening_suspension_from_future_zero_volume ... ok
|
||||
test broker::tests::scheduled_target_value_valuation_and_sizing_use_same_intraday_price ... ok
|
||||
test broker::tests::session_capacity_audit_never_caps_an_early_odd_lot_sell ... ok
|
||||
test broker::tests::public_callback_unwind_does_not_leak_order_context_or_authoritative_prior_state ... ok
|
||||
test broker::tests::stock_pool_after_sell_rejects_missing_quote_instead_of_reusing_daily_open ... ok
|
||||
test broker::tests::stock_pool_after_sell_uses_fresh_quotes_and_actual_submission_clock ... ok
|
||||
test broker::tests::stock_pool_delayed_sell_does_not_start_buys_after_the_configured_window ... ok
|
||||
test broker::tests::stock_pool_delayed_take_profit_does_not_rebuy_the_same_generation_exit ... ok
|
||||
test broker::tests::stock_pool_new_signal_supersedes_the_unsubmitted_buy_phase ... ok
|
||||
test broker::tests::stock_pool_engine_drives_the_pending_buy_without_a_minute_strategy_callback ... ok
|
||||
test broker::tests::stock_pool_pending_phase_cannot_cross_the_execution_session ... ok
|
||||
test broker::tests::stock_pool_partial_sell_waits_for_the_whole_batch_and_never_reissues_buys ... ok
|
||||
test broker::tests::strict_value_buy_budget_includes_commission_at_execution_price ... ok
|
||||
test broker::tests::successful_target_exit_releases_position_slot_for_replacement_entry ... ok
|
||||
test broker::tests::stock_pool_pending_sell_continues_buy_after_actual_fill_without_strategy_rerun ... ok
|
||||
test broker::tests::target_percent_reduction_from_full_to_twenty_percent_sells_eighty_percent ... ok
|
||||
test broker::tests::target_portfolio_smart_budgets_each_buy_before_cash_optimization ... ok
|
||||
test broker::tests::target_portfolio_smart_custom_valuation_does_not_create_limit_orders ... ok
|
||||
test broker::tests::target_portfolio_smart_defers_buy_risk_until_order_validation ... ok
|
||||
test broker::tests::target_portfolio_smart_ignores_zero_weight_symbols_without_market_snapshot ... ok
|
||||
test broker::tests::target_portfolio_smart_keeps_position_when_sell_snapshot_missing ... ok
|
||||
test broker::tests::target_portfolio_smart_open_auction_uses_day_open_for_valuation ... ok
|
||||
test broker::tests::target_portfolio_smart_records_buy_rejection_when_target_is_blacklisted ... ok
|
||||
test broker::tests::target_portfolio_smart_pre_open_cash_does_not_spend_same_batch_sell_proceeds ... ok
|
||||
test broker::tests::target_portfolio_smart_uses_prior_close_only_for_missing_day_valuation ... ok
|
||||
test broker::tests::target_value_delta_below_order_step_is_audited_without_creating_zero_quantity_order ... ok
|
||||
test broker::tests::target_value_batch_executes_reductions_before_increases_when_sell_cash_is_reusable ... ok
|
||||
test broker::tests::target_value_reduction_from_full_to_twenty_percent_sells_eighty_percent ... ok
|
||||
test broker::tests::target_value_zero_rejects_sell_when_market_snapshot_missing ... ok
|
||||
test broker::tests::target_weight_buy_quantity_respects_per_symbol_budget_after_slippage_and_fees ... ok
|
||||
test broker::tests::twap_earlier_fills_do_not_depend_on_later_quote_liquidity_or_count ... ok
|
||||
test broker::tests::timed_target_value_zero_sells_full_position_at_requested_time_quote ... ok
|
||||
test broker::tests::unchanged_targets_do_not_create_zero_quantity_orders ... ok
|
||||
test broker::tests::unwinding_an_unpublished_simulator_transaction_restores_its_state ... ok
|
||||
test broker::tests::value_buy_rejects_when_slippage_clamps_execution_price_to_upper_limit ... ok
|
||||
test broker::tests::value_buy_process_uses_latest_quote_before_decision_time ... ok
|
||||
test corporate_book::tests::closing_register_cannot_be_replaced_by_later_holdings ... ok
|
||||
test broker::tests::failed_resting_order_batch_keeps_accepted_orders_and_unpublished_financial_state ... ok
|
||||
test corporate_book::tests::a_late_corporate_batch_failure_keeps_prior_cash_positions_and_notes ... ok
|
||||
test corporate_book::tests::dividend_receivable_survives_a_flat_position_without_becoming_spendable_cash ... ok
|
||||
test corporate_book::tests::corporate_quantity_overflow_fails_without_saturation_or_a_negative_delta ... ok
|
||||
test corporate_book::tests::explicitly_delayed_dividend_remains_receivable_until_actual_payment ... ok
|
||||
test corporate_book::tests::entitlement_survives_complete_sale_and_a_successor_unit_conversion ... ok
|
||||
test corporate_book::tests::flat_position_retirement_does_not_change_issued_lot_state_or_next_purchase_origin ... ok
|
||||
test corporate_book::tests::manual_zero_skips_only_unused_allocation_facts_not_invalid_cash_evidence ... ok
|
||||
test corporate_book::tests::missing_accounting_reference_is_atomic_not_a_silent_cash_only_success ... ok
|
||||
test corporate_book::tests::invalid_delivery_negative_rates_and_unresolved_fractional_claims_are_atomic ... ok
|
||||
test corporate_book::tests::missing_or_backward_cash_payment_date_never_updates_the_account ... ok
|
||||
test corporate_book::tests::missing_record_date_or_unobserved_closing_book_is_atomic ... ok
|
||||
test corporate_book::tests::per_share_precision_is_retained_until_the_registered_cash_amount_is_rounded ... ok
|
||||
test corporate_book::tests::post_record_buy_cannot_receive_the_earlier_dividend ... ok
|
||||
test corporate_book::tests::post_record_purchase_does_not_inherit_registered_shares ... ok
|
||||
test corporate_book::tests::registered_shares_are_valued_but_not_delivered_or_sellable_before_the_original_date ... ok
|
||||
test corporate_book::tests::sale_after_registration_preserves_cash_and_excludes_new_lots_from_cost_adjustment ... ok
|
||||
test corporate_book::tests::terminated_security_keeps_paid_cash_and_is_not_recreated_by_reinvestment ... ok
|
||||
test corporate_book::tests::successor_without_frozen_instrument_metadata_is_not_an_implicit_new_security ... ok
|
||||
test corporate_book::tests::verified_conversion_preserves_lots_without_creating_orders_or_fills ... ok
|
||||
test corporate_book::tests::tiny_known_cash_rate_is_preserved_and_cannot_waive_payment_or_record_evidence ... ok
|
||||
test cost::tests::cost_model_can_use_configurable_stamp_tax_change_date ... ok
|
||||
test cost::tests::default_quantizes_fees_to_micro_yuan ... ok
|
||||
test cost::tests::fund_fees_use_admitted_instrument_type_and_share_the_order_commission_budget ... ok
|
||||
test daily_patterns::tests::daily_patterns_adjusts_all_prices_not_volume ... ok
|
||||
test daily_patterns::tests::breakout_retest_needs_a_later_retest_not_the_breakout_candle_itself ... ok
|
||||
test corporate_book::tests::invalid_successor_terms_cannot_be_replaced_with_one_share_or_zero_cash ... ok
|
||||
test daily_patterns::tests::daily_patterns_flat_decimal_prices_do_not_create_a_sell_signal ... ok
|
||||
test daily_patterns::tests::daily_patterns_helper_literal_preserves_parameters ... ok
|
||||
test daily_patterns::tests::daily_patterns_rejects_future_and_duplicate_bars ... ok
|
||||
test daily_patterns::tests::daily_patterns_all_templates_and_score_absence ... ok
|
||||
test daily_patterns::tests::daily_patterns_no_missing_data_fallback ... ok
|
||||
test daily_patterns::tests::limit_consolidation_requires_real_limit_and_never_infers_ten_percent ... ok
|
||||
test daily_patterns::tests::mean_volume_followup_requires_bullish_breakout_and_shrink ... ok
|
||||
test daily_patterns::tests::mean_volume_is_not_prior_max_and_excludes_current_bar ... ok
|
||||
test daily_patterns::tests::expression_condition_preserves_native_types_and_rejects_numeric_as_signal ... ok
|
||||
test daily_patterns::tests::research_index_and_ranking_context_never_unlock_strategy_mapping ... ok
|
||||
test data::series_columns::tests::differing_prices_preserve_zero_nan_payloads_and_prior_rows ... ok
|
||||
test data::series_columns::tests::identical_prices_share_only_after_exact_bit_comparison ... ok
|
||||
test daily_patterns::tests::research_isolates_missing_listing_day_without_weakening_execution ... ok
|
||||
test data::series_columns::tests::optional_values_keep_none_distinct_from_empty_and_repeated_text ... ok
|
||||
test data::series_columns::tests::repeated_values_preserve_nonzero_values_and_copy_on_change ... ok
|
||||
test daily_patterns::tests::research_numeric_output_keeps_warmup_unknown_without_a_false_signal ... ok
|
||||
test data::series_columns::tests::repeated_values_reject_out_of_range_access - should panic ... ok
|
||||
test data::tests::adjusted_close_average_normalization_prevents_strict_crossover_drift ... ok
|
||||
test data::tests::baseline_selection_uses_dated_lifecycle_not_latest_undated_status ... ok
|
||||
test data::tests::benchmark_current_rolling_reuses_symbol_boundary ... ignored, manual release-mode current rolling boundary benchmark
|
||||
test data::tests::benchmark_daily_snapshot_view_lookup ... ignored, manual component benchmark
|
||||
test data::tests::additional_terminal_calendar_dates_are_isolated_from_shared_market_data ... ok
|
||||
test data::tests::benchmark_decision_close_windows_exclude_current_close ... ok
|
||||
test data::tests::benchmark_instrument_symbol_id_lookup ... ignored, manual release-mode instrument lookup benchmark
|
||||
test data::tests::benchmark_series_boundary_reuses_calendar_index ... ignored, manual release-mode calendar index reuse benchmark
|
||||
test data::tests::batched_standard_rolling_means_match_scalar_lookups ... ok
|
||||
test data::tests::daily_bundle_constructor_rejects_duplicate_or_mismatched_dates ... ok
|
||||
test data::tests::current_close_average_uses_backward_adjustment_factor_and_current_base ... ok
|
||||
test data::tests::day_open_moving_average_uses_its_own_historical_column ... ok
|
||||
test data::tests::dataset_clone_shares_immutable_base_and_isolates_execution_quotes ... ok
|
||||
test data::tests::decision_close_average_ignores_current_day_close ... ok
|
||||
test data::tests::decision_market_cap_uses_factor_date_snapshot_without_price_reconstruction ... ok
|
||||
test data::tests::decision_volume_average_ignores_paused_zero_volume_days ... ok
|
||||
test data::tests::decision_volume_average_uses_previous_completed_days_only ... ok
|
||||
test data::tests::daily_bundle_constructor_matches_flat_component_constructor ... ok
|
||||
test data::tests::eligible_universe_can_use_configured_risk_policy ... ok
|
||||
test data::tests::eligible_universe_does_not_require_candidate_risk_state_when_selection_risk_is_disabled ... ok
|
||||
test data::tests::direct_symbol_id_snapshot_lookups_preserve_alignment_for_sparse_rows ... ok
|
||||
test data::tests::eligible_universe_uses_decision_market_cap_same_date ... ok
|
||||
test data::tests::factor_numeric_value_normalizes_fields_without_changing_aliases ... ok
|
||||
test data::tests::factor_snapshot_normalization_moves_clean_maps_and_repairs_dirty_maps ... ok
|
||||
test data::tests::factor_snapshot_rejects_legacy_or_invalid_adjustment_storage ... ok
|
||||
test data::tests::execution_quotes_use_stable_k_way_merge_and_release_by_date ... ok
|
||||
test data::tests::intraday_overlay_fails_closed_when_daily_panel_is_shared ... ok
|
||||
test data::tests::future_missing_adjustment_factor_does_not_invalidate_historical_window ... ok
|
||||
test data::tests::parallel_dense_index_rejects_invalid_days_without_publishing_partial_index ... ok
|
||||
test data::tests::replacing_execution_quotes_preserves_duplicate_timestamp_rows ... ok
|
||||
test data::tests::parallel_daily_symbol_indices_match_scalar_for_sparse_and_empty_days ... ok
|
||||
test data::tests::series_end_position_index_preserves_decision_and_current_boundaries ... ok
|
||||
test data::tests::series_columns_preserve_full_snapshots_and_distinct_price_bits ... ok
|
||||
test data::tests::series_overlay_materializes_only_changed_values_and_preserves_history_cutoff ... ok
|
||||
test data::tests::shared_execution_quote_release_does_not_clone_the_base_map ... ok
|
||||
test data::tests::symbol_id_union_preserves_lexical_order_and_all_component_sources ... ok
|
||||
test data::tests::symbol_price_series_test_constructor_sorts_unsorted_rows ... ok
|
||||
test data::tests::source_volume_contract_rejects_windows_containing_missing_values ... ok
|
||||
test data::tests::unique_dataset_applies_sparse_intraday_overlay_to_daily_and_symbol_views ... ok
|
||||
test decision_projection::tests::exact_projection_coverage_and_classified_absence_are_receipted ... ok
|
||||
test decision_projection::tests::projection_contract_does_not_shadow_core_market_or_typed_fields ... ok
|
||||
test data::tests::volume_rolling_ignores_zero_volume_rows_for_source_and_legacy_data ... ok
|
||||
test decision_projection::tests::projection_rejects_future_drift_or_unbound_receipts ... ok
|
||||
test decision_projection::tests::projection_rejects_incomplete_duplicate_extra_and_overlapping_population ... ok
|
||||
test decision_projection::tests::projection_rejects_nonfinite_wrong_fields_and_unproven_absence ... ok
|
||||
test data::tests::parallel_daily_bundle_validation_keeps_earliest_error_and_component_order ... ok
|
||||
test engine::tests::backtest_execution_dates_match_sparse_lagged_equity_schedule ... ok
|
||||
test engine::tests::after_trading_cancel_is_executed_as_a_control_not_a_next_session_order ... ok
|
||||
test engine::tests::compact_progress_keeps_counts_without_event_payload_clones ... ok
|
||||
test engine::tests::current_bar_close_uses_decision_day_close_for_fill ... ok
|
||||
test engine::tests::decision_projection_does_not_silently_shift_a_start_without_prior_inputs ... ok
|
||||
test engine::tests::current_close_order_at_1500_loads_and_uses_post_close_matching_window ... ok
|
||||
test engine::tests::decision_projection_failure_never_reaches_strategy_and_does_not_blindly_retry ... ok
|
||||
test engine::tests::compact_manual_progress_keeps_exact_applications_and_fallible_delivery_stops_the_run ... ok
|
||||
test engine::tests::decision_projection_rejects_future_response_before_any_strategy_callback ... ok
|
||||
test engine::tests::decision_projection_is_called_before_callbacks_and_is_not_shared_or_carried_forward ... ok
|
||||
test engine::tests::decision_projection_rejects_missing_calendar_inputs_instead_of_dropping_a_session ... ok
|
||||
test engine::tests::decision_projection_refuses_static_scores_and_same_session_inputs ... ok
|
||||
test engine::tests::decision_projection_requested_first_session_uses_visible_preperiod_inputs ... ok
|
||||
test engine::tests::default_close_phase_waits_for_close_and_callbacks_receive_completed_trade_history ... ok
|
||||
test engine::tests::early_pre_open_schedules_observe_manual_receipts_in_chronological_order ... ok
|
||||
test engine::tests::empty_manual_source_does_not_retime_natural_strategy_execution ... ok
|
||||
test engine::tests::join_text_parts_matches_vec_join_contract ... ok
|
||||
test engine::tests::final_day_late_intents_are_retained_without_orders_or_fills_past_the_end ... ok
|
||||
test engine::tests::full_minute_coverage_rejects_missing_active_bars_but_allows_paused_or_zero_volume ... ok
|
||||
test engine::tests::lagged_signal_callbacks_keep_signal_date_but_process_notifications_use_execution_date ... ok
|
||||
test engine::tests::late_day_callbacks_read_the_actual_earlier_trade_and_keep_their_current_clock ... ok
|
||||
test engine::tests::late_intents_do_not_wait_for_another_selection_snapshot ... ok
|
||||
test engine::tests::late_complete_targets_use_latest_target_and_next_day_zero_supersedes_them ... ok
|
||||
test engine::tests::late_imperative_orders_execute_once_in_next_session_with_original_dates_and_new_price ... ok
|
||||
test engine::tests::late_opening_callback_is_interleaved_with_earlier_quotes_and_manual_observations ... ok
|
||||
test engine::tests::lifecycle_quote_filter_skips_only_dated_legal_absence_before_loading ... ok
|
||||
test engine::tests::minute_event_clock_uses_timestamp_order_and_coalesces_equal_heads ... ok
|
||||
test engine::tests::latest_unsent_complete_portfolio_does_not_union_an_older_pre_market_target ... ok
|
||||
test engine::tests::minute_schedules_fire_at_each_declared_time_without_market_rows_at_those_times ... ok
|
||||
test engine::tests::next_bar_open_eligible_universe_helper_does_not_block_on_decision_day_risk ... ok
|
||||
test engine::tests::minute_observer_never_sees_a_later_fill_from_a_coarse_phase ... ok
|
||||
test engine::tests::next_bar_open_executes_last_decision_without_execution_day_factor_snapshot ... ok
|
||||
test engine::tests::manual_observations_are_visible_at_their_real_phase_and_each_receipt_sequence ... ok
|
||||
test engine::tests::next_bar_open_execution_risk_ignores_decision_day_st_state ... ok
|
||||
test engine::tests::next_bar_open_execution_risk_ignores_decision_day_paused_state ... ok
|
||||
test engine::tests::next_bar_open_execution_risk_ignores_decision_day_star_st_state ... ok
|
||||
test engine::tests::next_bar_open_execution_risk_ignores_decision_day_upper_limit_state ... ok
|
||||
test engine::tests::next_bar_open_execution_risk_ignores_later_one_yuan_close ... ok
|
||||
test engine::tests::next_bar_open_execution_risk_rejects_execution_day_paused_state ... ok
|
||||
test engine::tests::next_bar_open_execution_risk_rejects_execution_day_delisted_state ... ok
|
||||
test engine::tests::next_bar_open_execution_risk_rejects_blacklisted_buy_on_execution_day ... ok
|
||||
test engine::tests::next_bar_open_execution_risk_rejects_execution_day_st_state ... ok
|
||||
test engine::tests::next_bar_open_execution_risk_rejects_execution_day_star_st_state ... ok
|
||||
test engine::tests::next_bar_open_execution_risk_rejects_execution_day_upper_limit_buy ... ok
|
||||
test engine::tests::next_bar_open_execution_risk_rejects_one_yuan_open_despite_higher_close ... ok
|
||||
test engine::tests::next_bar_open_execution_risk_uses_open_not_close_for_upper_limit_buy ... ok
|
||||
test engine::tests::next_bar_open_same_day_rebuy_uses_actual_execution_date ... ok
|
||||
test engine::tests::next_bar_open_scheduled_signal_uses_decision_date_not_execution_date ... ok
|
||||
test engine::tests::next_bar_open_sell_respects_allow_sell_policy_on_execution_day ... ok
|
||||
test engine::tests::next_bar_open_sell_risk_rejects_execution_day_lower_limit_state ... ok
|
||||
test engine::tests::next_bar_open_sell_risk_ignores_decision_day_paused_and_lower_limit_state ... ok
|
||||
test engine::tests::next_bar_open_sell_risk_uses_open_not_close_for_lower_limit_sell ... ok
|
||||
test engine::tests::next_bar_open_sell_volume_limit_ignores_decision_day_zero_volume ... ok
|
||||
test engine::tests::next_bar_open_session_audit_flags_zero_volume_without_rewriting_fills ... ok
|
||||
test engine::tests::next_bar_open_skips_unavailable_lag_day_and_fills_next_open ... ok
|
||||
test engine::tests::next_bar_open_target_percent_freezes_decision_day_equity ... ok
|
||||
test engine::tests::next_bar_open_strategy_context_data_helpers_use_decision_date ... ok
|
||||
test engine::tests::next_bar_open_target_portfolio_smart_sizes_with_execution_day_open ... ok
|
||||
test engine::tests::next_open_observation_loads_quotes_even_when_execution_price_is_open ... ok
|
||||
test engine::tests::observed_manual_sale_updates_the_shared_same_day_rebuy_rule ... ok
|
||||
test engine::tests::opening_schedule_cannot_cross_the_after_trading_phase ... ok
|
||||
test engine::tests::opening_and_late_lifecycle_schedules_do_not_read_past_or_future_manual_state ... ok
|
||||
test engine::tests::opening_orders_are_submitted_once_at_their_real_clock_even_with_an_earlier_execution_window ... ok
|
||||
test engine::tests::physical_on_day_rules_keep_each_actual_submission_time ... ok
|
||||
test engine::tests::post_close_phase_callbacks_never_rewind_before_completed_fills ... ok
|
||||
test engine::tests::opening_subscription_is_loaded_without_replaying_quotes_before_its_activation ... ok
|
||||
test engine::tests::post_close_wait_has_no_position_before_the_first_actual_matching_tick ... ok
|
||||
test engine::tests::process_callbacks_observe_the_completed_minute_trade_and_its_clock ... ok
|
||||
test engine::tests::pre_trading_orders_wait_for_the_configured_window_and_keep_their_audit ... ok
|
||||
test engine::tests::scheduled_event_detail_records_actual_time_only_for_timed_rules ... ok
|
||||
test engine::tests::pre_market_cancel_is_observed_before_the_next_open_and_new_signal ... ok
|
||||
test engine::tests::unknown_identity_or_missing_candidate_does_not_waive_quote_coverage ... ok
|
||||
test engine::tests::wholly_prelisting_universe_retains_cash_days_without_fabricating_prices ... ok
|
||||
test etf_execution::tests::latest_generation_overwrites_pending_targets_and_preserves_candidate_order ... ok
|
||||
test engine::tests::window_algorithm_cannot_publish_future_fill_quantity_to_earlier_callbacks ... ok
|
||||
test etf_execution::tests::no_loaded_next_session_is_not_guessed_from_natural_days ... ok
|
||||
test events::tests::accounting_origin_cannot_disguise_an_exchange_order_or_fee ... ok
|
||||
test events::tests::fill_execution_timestamp_range_is_explicit_and_backward_compatible ... ok
|
||||
test events::tests::order_event_status_quantity_contract_is_explicit ... ok
|
||||
test events::tests::process_event_business_lifecycle_filter_keeps_audit_events_only ... ok
|
||||
test execution_capacity::tests::completed_volume_cannot_be_used_for_an_earlier_open ... ok
|
||||
test execution_capacity::tests::decimal_participation_never_rounds_a_fractional_share_up_or_overflows ... ok
|
||||
test execution_capacity::tests::delayed_publication_and_invalid_bounds_are_not_treated_as_zero_volume ... ok
|
||||
test execution_capacity::tests::session_audit_changes_verdict_not_executed_quantity ... ok
|
||||
test execution_schedule::tests::clock_entitlements_are_exact_and_allow_backlog_without_future_quote_counts ... ok
|
||||
test factor_cross_section::tests::neutralization_preserves_input_order ... ok
|
||||
test factor_cross_section::tests::ties_keep_equal_rank_and_missing_universe_rejects ... ok
|
||||
test factor_cross_section::tests::historical_ranks_keep_ties_and_unknown_full_cross_sections ... ok
|
||||
test factor_events::tests::cross_is_event_not_state_and_never_uses_future ... ok
|
||||
test factor_events::tests::every_parameter_domain_is_structured_and_matches_native_defaults ... ok
|
||||
test factor_events::tests::missing_is_not_zero_and_breakout_excludes_current ... ok
|
||||
test factor_events::tests::no_event_has_no_bars_since_and_type_errors_reject ... ok
|
||||
test engine::tests::scheduled_day_limit_order_loads_later_quotes_without_strategy_minute_subscription ... ok
|
||||
test factor_events::tests::ta_rewarms_after_gap_and_const_zscore_is_unknown ... ok
|
||||
test factor_events::tests::ta_sma_real_values_and_parameter_validation ... ok
|
||||
test finite_serialization::tests::pending_numbers_cannot_be_silently_serialized_as_optional_nulls ... ok
|
||||
test fixed_point::tests::decimal_parser_rounds_only_beyond_money_scale ... ok
|
||||
test fixed_point::tests::fixed_budget_comparison_rejects_one_micro_yuan_overrun ... ok
|
||||
test fixed_point::tests::fixed_budget_never_exceeds_cash_after_cost ... ok
|
||||
test fixed_point::tests::fixed_fifo_pnl_and_external_flow_are_deterministic ... ok
|
||||
test fixed_point::tests::runtime_cost_model_matches_fixed_execution_primitive ... ok
|
||||
test fixed_point::tests::runtime_split_commission_matches_fixed_execution_primitive ... ok
|
||||
test holding_policy::tests::absolute_locks_are_inclusive_and_override_timed_exit_without_changing_other_symbols ... ok
|
||||
test holding_policy::tests::manual_origin_only_bypasses_automatic_policy_not_an_order_or_broker_permission ... ok
|
||||
test holding_policy::tests::opening_date_follows_fills_not_partial_sales_or_corporate_conversions ... ok
|
||||
test holding_policy::tests::three_complete_sessions_protect_through_wednesday_not_72_hours ... ok
|
||||
test holding_policy::tests::zero_disabled_and_missing_calendar_or_opened_date_are_not_inferred ... ok
|
||||
test instrument::tests::lifecycle_is_dated_and_latest_undated_terminal_status_is_not_historical_evidence ... ok
|
||||
test instrument::tests::listing_sector_is_explicit_and_unknown_stays_unknown ... ok
|
||||
test instrument::tests::order_quantity_rules_are_case_insensitive_without_allocating_normalized_boards ... ok
|
||||
test manual_execution::tests::a_failed_corporate_projection_does_not_change_the_book_or_receipt_cursor ... ok
|
||||
test manual_execution::tests::all_required_money_and_binding_fields_reject_missing_or_wrong_values ... ok
|
||||
test manual_execution::tests::authoritative_total_fee_does_not_require_inventing_unknown_components ... ok
|
||||
test manual_execution::tests::changing_any_external_price_or_identity_invalidates_the_frozen_trace ... ok
|
||||
test manual_execution::tests::complete_exact_decimal_evidence_allows_later_observation_and_retains_source_digits ... ok
|
||||
test manual_execution::tests::confirmed_manual_fill_changes_cash_and_lots_but_not_external_cash_flow_units ... ok
|
||||
test manual_execution::tests::confirmed_no_order_outcome_is_distinct_from_unconfirmed_or_unknown_work ... ok
|
||||
test manual_execution::tests::cursor_waits_for_observation_and_never_reapplies_or_rewinds ... ok
|
||||
test manual_execution::tests::data_scope_only_contains_actual_filled_securities_and_validates_the_source ... ok
|
||||
test manual_execution::tests::failed_multi_receipt_advance_keeps_both_progress_and_portfolio_unchanged ... ok
|
||||
test manual_execution::tests::fixed_money_decimal_text_preserves_micro_units_without_float_conversion ... ok
|
||||
test factor_events::tests::literal_unknown_fields_reject_and_catalog_is_not_trading_permission ... ok
|
||||
test factor_events::tests::parameter_domains_keep_enumeration_labels_without_debug_string_parsing ... ok
|
||||
test manual_execution::tests::inconsistent_counts_terminals_audits_and_duplicate_facts_are_rejected ... ok
|
||||
test manual_execution::tests::late_buy_fifo_depletion_preserves_costs_and_cannot_unlock_today_lots ... ok
|
||||
test manual_execution::tests::late_fee_evidence_keeps_the_original_fill_observation_clock ... ok
|
||||
test manual_execution::tests::late_buy_retains_the_earliest_opening_and_latest_buy_dates ... ok
|
||||
test manual_execution::tests::manual_fee_total_includes_extra_charges_and_rejects_inconsistent_components ... ok
|
||||
test manual_execution::tests::manual_mismatches_are_atomic_and_do_not_borrow_shares_cash_or_override_pending_orders ... ok
|
||||
test manual_execution::tests::partial_cancel_is_valid_but_full_fill_cannot_be_reported_as_cancelled ... ok
|
||||
test manual_execution::tests::observations_follow_durable_receipt_order_and_not_input_array_order ... ok
|
||||
test manual_execution::tests::raw_timezone_and_cutoff_are_required ... ok
|
||||
test manual_execution::tests::runtime_position_events_cannot_claim_observations_after_the_source_cutoff ... ok
|
||||
test manual_execution::tests::the_next_day_manual_sale_keeps_the_actual_quantity_and_fee_contract ... ok
|
||||
test manual_execution::tests::v2_facts_keep_their_encoding_but_cannot_silently_carry_new_runtime_settings ... ok
|
||||
test market_event_context::tests::formulas_use_real_limits_and_full_denominators ... ok
|
||||
test market_event_context::tests::chunking_and_future_append_preserve_history ... ok
|
||||
test manual_execution::tests::source_time_precision_is_not_invented_and_submitted_time_must_fit_the_interval ... ok
|
||||
test market_event_context::tests::missing_duplicate_and_unproven_limit_states_fail ... ok
|
||||
test market_event_context::tests::missing_industry_does_not_invent_groups_or_disable_independent_market_facts ... ok
|
||||
test metrics::risk_adjusted_contract_tests::changing_daily_rates_adjusts_each_return_before_variance_and_downside ... ok
|
||||
test metrics::risk_adjusted_contract_tests::incomplete_or_invalid_rates_are_not_zero_rate_observations ... ok
|
||||
test metrics::risk_adjusted_contract_tests::zero_deviation_and_single_sample_ratios_remain_undefined ... ok
|
||||
test metrics::tests::a_defined_zero_ratio_is_not_missing ... ok
|
||||
test metrics::tests::drawdown_includes_initial_nav_without_adding_a_trading_day ... ok
|
||||
test metrics::tests::benchmark_cumulative_return_uses_first_day_previous_close ... ok
|
||||
test metrics::tests::external_cash_flow_is_excluded_from_return_and_reported_separately ... ok
|
||||
test metrics::tests::first_day_loss_is_preserved_in_shared_backtest_metrics ... ok
|
||||
test metrics::tests::gains_without_downside_have_no_sortino_or_monthly_sharpe ... ok
|
||||
test metrics::tests::no_risk_ratio_is_fabricated_after_nav_has_reached_zero ... ok
|
||||
test metrics::tests::risk_adjusted_metrics_use_daily_pit_rates_and_all_period_downside ... ok
|
||||
test metrics::tests::signal_baseline_uses_same_close_for_strategy_and_benchmark ... ok
|
||||
test numeric_expr_vm::tests::benchmark_numeric_vm_against_rhai ... ignored, manual release-mode performance evidence
|
||||
test numeric_expr_vm::tests::floating_comparisons_match_rhai_epsilon_semantics ... ok
|
||||
test numeric_expr_vm::tests::evaluates_numeric_boolean_and_if_expressions ... ok
|
||||
test metrics::tests::undefined_ratios_remain_null_in_backtest_metrics ... ok
|
||||
test numeric_expr_vm::tests::invalid_clamp_returns_error_without_panicking ... ok
|
||||
test numeric_expr_vm::tests::nullable_boolean_truth_table_preserves_unknown_under_negation ... ok
|
||||
test numeric_expr_vm::tests::nullable_short_circuit_and_explicit_conditional_branches_are_lazy ... ok
|
||||
test numeric_expr_vm::tests::missing_numeric_operands_do_not_become_boolean_false_or_zero ... ok
|
||||
test numeric_expr_vm::tests::rejects_dynamic_language_features ... ok
|
||||
test numeric_expr_vm::tests::scratch_reuses_allocations_across_program_runs ... ok
|
||||
test numeric_expr_vm::tests::short_circuit_does_not_resolve_unused_variable ... ok
|
||||
test numeric_expr_vm::tests::supports_numeric_builtins_and_local_reuse ... ok
|
||||
test numeric_factors::tests::bulk_load_is_sorted_and_keeps_last_value_for_each_field ... ok
|
||||
test numeric_factors::tests::clone_does_not_share_mutable_values ... ok
|
||||
test numeric_factors::tests::compact_keys_inline_dynamic_names_and_keep_long_static_storage ... ok
|
||||
test numeric_factors::tests::exact_reservation_preserves_values_and_avoids_growth_during_known_inserts ... ok
|
||||
test numeric_factors::tests::long_dynamic_unicode_and_short_keys_keep_the_same_json_map ... ok
|
||||
test numeric_factors::tests::serialization_keeps_the_map_contract_and_precise_numbers ... ok
|
||||
test numeric_factors::tests::wide_batch_merge_matches_tree_and_preserves_old_key_ownership ... ok
|
||||
test numeric_expr_vm::tests::matches_rhai_for_representative_numeric_boolean_corpus ... ok
|
||||
test pattern_context::tests::normalized_rule_does_not_turn_an_omitted_window_into_explicit_null ... ok
|
||||
test pattern_context::tests::reference_index_never_defaults_to_performance_benchmark ... ok
|
||||
test pattern_context::tests::runtime_contract_rejects_missing_range_and_recursive_ranks ... ok
|
||||
test numeric_factors::tests::updates_order_removal_and_values_match_tree_map ... ok
|
||||
test pattern_context::tests::dataset_rank_is_full_scope_causal_and_equal_to_pure_cross_section ... ok
|
||||
test pattern_context::tests::structured_pool_conditions_are_not_rescanned_inside_serialized_source_code ... ok
|
||||
test platform_expr_strategy::tests::allocation_buy_scale_preserves_explicit_overweight_multiplier ... ok
|
||||
test platform_expr_strategy::tests::benchmark_compiled_runtime_helper_arguments ... ignored, manual release-mode runtime helper binding benchmark
|
||||
test platform_expr_strategy::tests::bjse_symbol_detection_is_case_insensitive_without_normalizing_all_symbols ... ok
|
||||
test platform_expr_strategy::tests::available_factor_name_collection_preserves_sparse_and_repeated_fields ... ok
|
||||
test platform_expr_strategy::tests::buy_filter_attaches_denials_without_rewriting_selection ... ok
|
||||
test platform_expr_strategy::tests::available_factor_name_collection_preserves_wide_dynamic_field_identity ... ok
|
||||
test platform_expr_strategy::tests::buy_filter_uses_active_schedule_time_instead_of_first_configured_time ... ok
|
||||
test platform_expr_strategy::tests::buy_quote_filter_rejects_missing_intraday_quote_not_daily_close ... ok
|
||||
test platform_expr_strategy::tests::completed_session_factor_dates_exclude_intraday_and_preserve_next_open ... ok
|
||||
test platform_expr_strategy::tests::completed_session_bound_value_does_not_read_same_day_future_value ... ok
|
||||
test platform_expr_strategy::tests::daily_replacement_reference_records_an_explicit_empty_previous_target ... ok
|
||||
test platform_expr_strategy::tests::daily_pattern_runtime_uses_the_shared_kernel_and_rejects_early_visibility ... ok
|
||||
test platform_expr_strategy::tests::dated_position_exposure_uses_the_latest_effective_point ... ok
|
||||
test platform_expr_strategy::tests::current_rolling_helpers_do_not_load_factor_maps_or_decision_rollings ... ok
|
||||
test platform_expr_strategy::tests::daily_replacement_reference_uses_previous_target_before_lagged_orders_fill ... ok
|
||||
test platform_expr_strategy::tests::expression_plan_only_executes_prelude_when_expression_depends_on_it ... ok
|
||||
test platform_expr_strategy::tests::expression_plan_skips_unreferenced_market_scope_maps ... ok
|
||||
test platform_expr_strategy::tests::framework_rolling_factor_aliases_resolve_to_raw_market_series ... ok
|
||||
test platform_expr_strategy::tests::lagged_rebalance_emits_target_value_even_when_projection_is_unchanged ... ok
|
||||
test platform_expr_strategy::tests::factor_position_target_reduces_once_without_daily_rebalance_override ... ok
|
||||
test platform_expr_strategy::tests::market_cap_ordered_selection_range_is_inclusive_and_field_specific ... ok
|
||||
test platform_expr_strategy::tests::model_target_lifecycle_removes_exits_and_appends_replacements ... ok
|
||||
test platform_expr_strategy::tests::numeric_vm_compiles_static_runtime_helper_arguments ... ok
|
||||
test platform_expr_strategy::tests::numeric_vm_reuses_compiled_program_across_invocations ... ok
|
||||
test platform_expr_strategy::tests::numeric_vm_rewrites_only_valid_iso_date_literals ... ok
|
||||
test platform_expr_strategy::tests::numeric_vm_reuses_rolling_helper_program_across_dates ... ok
|
||||
test platform_expr_strategy::tests::market_cap_ascending_rank_reuses_only_equivalent_universe_order ... ok
|
||||
test platform_expr_strategy::tests::periodic_selected_bjse_buy_rejection_is_audited_without_creating_an_order ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_blocks_target_increase_when_stop_loss_sell_is_lower_limit_blocked ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_buy_rejection_keeps_candidate_buy_disabled ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_buy_rejection_uses_scheduled_price_not_open_limit ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_daily_top_up_releases_slot_after_take_profit_exit ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_daily_top_up_releases_second_slot_after_prior_top_up ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_daily_top_up_uses_remaining_slot_budget ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_marked_total_uses_intraday_last_not_close ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_delays_upper_limit_take_profit_to_configured_open_time ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_marked_total_value_uses_scheduled_quote ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_minute_last_projection_uses_last_quote_for_buy_budget ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_marks_highlimit_before_cptrade_time ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_minute_stop_loss_requires_intraday_quote ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_next_open_stop_loss_uses_signal_close_not_execution_last_price ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_missing_risk_state_does_not_force_existing_position_exit ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_next_open_target_delta_uses_previous_close_and_sizes_at_open ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_projection_allows_signal_day_market_fallback_for_next_open ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_projection_does_not_release_sell_slot_without_execution_quote ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_projection_rejects_missing_symbol_execution_quote ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_projection_reserves_sell_cost_without_same_callback_proceeds ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_periodic_rebalance_uses_total_equity_equal_weight_buy_budget ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_remaining_buy_cash_uses_strategy_visible_price_and_projected_quantity ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_risk_level_forces_existing_position_exit ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_scheduled_quote_uses_configured_intraday_time ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_selection_uses_decision_market_cap ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_sell_projection_uses_signal_day_market_for_lagged_execution ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_sells_down_before_actionable_take_profit_exit ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_stock_expr_uses_scheduled_minute_price_for_limit_check ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_skips_positive_target_adjust_when_position_will_close ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_target_adjust_uses_scheduled_quote_for_position_value ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_target_value_uses_scheduled_mark_for_delta ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_stop_loss_uses_fee_cost_basis_avg_cost ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_upper_limit_buy_disabled_does_not_force_exit_existing_position ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_weak_market_allows_pending_highlimit_target_adjust_after_open ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_weak_market_marks_upper_limit_but_allows_target_adjust ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_weak_market_emits_daily_position_target_adjustment ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_weak_market_threshold_top_up_uses_projected_remaining_budget ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_weak_market_waits_pending_highlimit_target_adjust ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_weak_market_threshold_only_sells_overweight_positions ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_weak_market_retargets_same_ratio_every_day ... ok
|
||||
test platform_expr_strategy::tests::platform_buy_rejection_allows_lower_limit_buy_when_ask_is_available ... ok
|
||||
test platform_expr_strategy::tests::platform_calendar_day_rebalance_interval_can_be_enabled_explicitly ... ok
|
||||
test platform_expr_strategy::tests::platform_current_rolling_uses_decision_date_row_not_prior_factor_row ... ok
|
||||
test platform_expr_strategy::tests::platform_daily_top_up_caps_empty_slot_to_equal_weight_target ... ok
|
||||
test platform_expr_strategy::tests::platform_can_sell_position_uses_configured_lower_limit_policy ... ok
|
||||
test platform_expr_strategy::tests::platform_daily_top_up_counts_submitted_zero_fill_candidate_as_strategy_slot ... ok
|
||||
test platform_expr_strategy::tests::platform_daily_top_up_keeps_selection_limited_to_target_count ... ok
|
||||
test platform_expr_strategy::tests::platform_daily_top_up_does_not_buy_back_same_day_exit ... ok
|
||||
test platform_expr_strategy::tests::platform_daily_top_up_release_slot_flag_matches_aiquant_exit_signal_state ... ok
|
||||
test platform_expr_strategy::tests::platform_daily_top_up_releases_unsellable_stop_loss_slot ... ok
|
||||
test platform_expr_strategy::tests::platform_daily_top_up_skips_current_stop_loss_symbol_like_aiquant ... ok
|
||||
test platform_expr_strategy::tests::platform_daily_top_up_uses_same_day_sell_cash ... ok
|
||||
test platform_expr_strategy::tests::platform_day_state_uses_decision_benchmark_close ... ok
|
||||
test platform_expr_strategy::tests::platform_delayed_open_day_carried_full_close_releases_second_top_up_slot ... ok
|
||||
test platform_expr_strategy::tests::platform_delayed_open_exit_uses_delayed_time_for_slot_projection ... ok
|
||||
test platform_expr_strategy::tests::platform_delayed_open_day_take_profit_releases_second_top_up_slot ... ok
|
||||
test platform_expr_strategy::tests::platform_expr_code_number_extracts_stock_digits ... ok
|
||||
test platform_expr_strategy::tests::platform_delayed_open_partial_exit_releases_top_up_slot ... ok
|
||||
test platform_expr_strategy::tests::platform_explicit_actions_keep_every_declared_trade_time ... ok
|
||||
test platform_expr_strategy::tests::platform_expr_expands_day_factor_in_prelude ... ok
|
||||
test platform_expr_strategy::tests::platform_expr_limit_flags_are_directional ... ok
|
||||
test platform_expr_strategy::tests::platform_expr_cost_model_honors_zero_minimum_commission ... ok
|
||||
test platform_expr_strategy::tests::platform_expr_normalizes_python_style_integer_division ... ok
|
||||
test platform_expr_strategy::tests::platform_expr_normalizes_signal_field_alias_in_prelude ... ok
|
||||
test platform_expr_strategy::tests::platform_expr_cost_model_uses_commission_override ... ok
|
||||
test platform_expr_strategy::tests::platform_expr_preflight_rejects_malformed_expression_with_field_name ... ok
|
||||
test platform_expr_strategy::tests::platform_expr_preflight_accepts_all_configured_expression_categories ... ok
|
||||
test platform_expr_strategy::tests::platform_expr_rewrites_nested_ternary ... ok
|
||||
test platform_expr_strategy::tests::platform_expr_rewrites_prelude_assignment_ternary ... ok
|
||||
test platform_expr_strategy::tests::platform_expr_missing_requested_factor_does_not_default_to_zero ... ok
|
||||
test platform_expr_strategy::tests::platform_expr_rewrites_multiple_nested_ternaries_inside_function_calls ... ok
|
||||
test platform_expr_strategy::tests::platform_expr_safe_div_supports_two_and_three_arg_forms ... ok
|
||||
test platform_expr_strategy::tests::platform_expr_supports_python_style_string_aliases ... ok
|
||||
test platform_expr_strategy::tests::platform_expr_tracks_requested_dynamic_factor_identifiers ... ok
|
||||
test platform_expr_strategy::tests::platform_full_exposure_carried_full_close_releases_second_top_up_slot ... ok
|
||||
test platform_expr_strategy::tests::platform_lagged_exit_does_not_reserve_replacement_slot_before_execution ... ok
|
||||
test platform_expr_strategy::tests::platform_full_exposure_partial_stop_loss_releases_second_top_up_slot ... ok
|
||||
test platform_expr_strategy::tests::platform_helpers_support_generic_rolling_stats_and_normalized_factors ... ok
|
||||
test platform_expr_strategy::tests::platform_marked_total_value_uses_current_day_close ... ok
|
||||
test platform_expr_strategy::tests::platform_max_holding_days_counts_decision_trading_days_not_calendar_days ... ok
|
||||
test platform_expr_strategy::tests::platform_max_holding_days_exit_preempts_take_profit_exit ... ok
|
||||
test platform_expr_strategy::tests::platform_market_cap_field_uses_storage_unit_without_extra_scaling ... ok
|
||||
test platform_expr_strategy::tests::platform_minute_last_volume_limit_does_not_require_l1_quantity ... ok
|
||||
test platform_expr_strategy::tests::platform_next_open_defers_complete_static_selection_risk_before_universe_output ... ok
|
||||
test platform_expr_strategy::tests::platform_next_open_select_symbols_defers_decision_day_limit_state ... ok
|
||||
test platform_expr_strategy::tests::platform_next_open_risk_level_exit_ignores_decision_day_sellability ... ok
|
||||
test platform_expr_strategy::tests::platform_next_open_projected_execution_price_uses_open_not_close ... ok
|
||||
test platform_expr_strategy::tests::platform_next_open_selection_applies_explicit_signal_day_risk_filter ... ok
|
||||
test platform_expr_strategy::tests::platform_next_open_selection_ignores_execution_day_limit_state ... ok
|
||||
test platform_expr_strategy::tests::platform_next_open_sell_risk_uses_open_not_close_for_lower_limit ... ok
|
||||
test platform_expr_strategy::tests::platform_numeric_stop_loss_is_strictly_below_multiplier ... ok
|
||||
test platform_expr_strategy::tests::platform_order_value_quantity_includes_buy_commission_budget ... ok
|
||||
test platform_expr_strategy::tests::platform_periodic_rebalance_caps_empty_slot_to_equal_weight_target ... ok
|
||||
test platform_expr_strategy::tests::platform_periodic_rebalance_skips_unsellable_stop_loss_symbol ... ok
|
||||
test platform_expr_strategy::tests::platform_projected_execution_limit_checks_respect_risk_policy ... ok
|
||||
test platform_expr_strategy::tests::platform_periodic_rebalance_uses_same_day_sell_cash_for_later_buys ... ok
|
||||
test platform_expr_strategy::tests::platform_periodic_rebalance_tops_up_underweight_selected_holding ... ok
|
||||
test platform_expr_strategy::tests::platform_rebalance_date_due_does_not_use_the_on_day_default_clock ... ok
|
||||
test platform_expr_strategy::tests::platform_projection_does_not_consume_cash_from_unfillable_sell ... ok
|
||||
test platform_expr_strategy::tests::platform_rebalance_schedule_matches_monthly_tradingday ... ok
|
||||
test platform_expr_strategy::tests::platform_rebalance_schedule_matches_physical_time ... ok
|
||||
test platform_expr_strategy::tests::platform_rebalance_schedule_matches_weekly_weekday ... ok
|
||||
test platform_expr_strategy::tests::platform_rebalance_keeps_unresolved_delisted_position_without_orders_or_replacement ... ok
|
||||
test platform_expr_strategy::tests::platform_projected_market_fill_caps_volume_limit ... ok
|
||||
test platform_expr_strategy::tests::platform_selection_and_buy_respect_allow_flags_are_configurable ... ok
|
||||
test platform_expr_strategy::tests::platform_selection_does_not_fill_candidates_with_buy_risk_rejections ... ok
|
||||
test platform_expr_strategy::tests::platform_selection_expands_pct_change_in_filter_and_rank_expr ... ok
|
||||
test platform_expr_strategy::tests::platform_refresh_rate_uses_stateful_trading_day_counter ... ok
|
||||
test platform_expr_strategy::tests::platform_selection_missing_candidate_risk_state_does_not_change_default_universe ... ok
|
||||
test platform_expr_strategy::tests::platform_selection_kcb_switch_overrides_legacy_universe_exclude ... ok
|
||||
test platform_expr_strategy::tests::platform_selection_ranks_with_previous_factor_date ... ok
|
||||
test platform_expr_strategy::tests::platform_signal_price_model_lifecycle_survives_unfilled_buy_and_exits ... ok
|
||||
test platform_expr_strategy::tests::platform_selection_uses_complete_static_risk_policy_before_universe_output ... ok
|
||||
test platform_expr_strategy::tests::platform_skip_window_keeps_pending_highlimit_until_open ... ok
|
||||
test platform_expr_strategy::tests::platform_skip_window_runs_delayed_open_exit_before_seasonal_liquidation ... ok
|
||||
test broker::tests::simulator_transaction_profile_preserves_successful_output ... ok
|
||||
test platform_expr_strategy::tests::platform_stock_expr_exposes_star_st_and_bjse_fields_without_default_selection_risk ... ok
|
||||
test platform_expr_strategy::tests::platform_stock_expr_handles_positive_volume_guard ... ok
|
||||
test platform_expr_strategy::tests::platform_stock_state_uses_completed_day_high_and_low_for_limit_touch ... ok
|
||||
test platform_expr_strategy::tests::platform_stock_expr_rejects_when_volume_ma5_exceeds_market_volume_ma100 ... ok
|
||||
test platform_expr_strategy::tests::platform_selection_ranks_by_candidate_market_cap_with_current_precomputed_factors ... ok
|
||||
test platform_expr_strategy::tests::platform_stock_state_uses_factor_date_for_selection_market_fields ... ok
|
||||
test platform_expr_strategy::tests::platform_stock_state_uses_market_series_when_factor_map_has_no_rolling ... ok
|
||||
test platform_expr_strategy::tests::platform_stock_state_uses_framework_rolling_series ... ok
|
||||
test platform_expr_strategy::tests::platform_stop_loss_residual_pending_rechecks_signal_next_day ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_baseline_risk_rejects_new_listing_buy_without_selection_replacement ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_can_cancel_latest_open_order_by_runtime_id ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_can_filter_with_string_factors ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_emits_explicit_actions_in_open_auction_stage ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_emits_algo_order_actions ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_emits_explicit_actions_when_rotation_is_disabled ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_emits_target_portfolio_smart_explicit_action ... ok
|
||||
test platform_expr_strategy::tests::platform_stop_take_fraction_values_are_return_thresholds ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_emits_target_portfolio_smart_algo_order_style ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_emits_target_shares_explicit_action ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_emits_universe_management_actions ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_exposes_advanced_data_runtime_helpers ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_exposes_open_order_runtime_fields ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_exposes_position_lifecycle_runtime_fields ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_filters_target_portfolio_smart_by_subscription_guard ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_empty_exclude_keeps_buy_risk_without_selection_filtering ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_exposes_process_event_runtime_fields ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_skips_explicit_actions_when_schedule_does_not_match ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_treats_missing_stock_rolling_window_as_filter_reject ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_suppresses_explicit_target_for_blocked_stop_loss_exit ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_uses_decision_date_for_next_bar_open_signals ... ok
|
||||
test platform_expr_strategy::tests::platform_take_profit_default_uses_position_cost_basis ... ok
|
||||
test platform_expr_strategy::tests::platform_take_profit_sells_position_at_upper_limit ... ok
|
||||
test platform_expr_strategy::tests::platform_take_profit_false_does_not_track_upper_limit_pending_sell ... ok
|
||||
test platform_expr_strategy::tests::platform_weak_exposure_carried_full_close_keeps_residual_slot ... ok
|
||||
test platform_expr_strategy::tests::platform_weak_exposure_partial_stop_loss_keeps_residual_slot ... ok
|
||||
test platform_expr_strategy::tests::portfolio_drawdown_control_is_idempotent_and_rearms_after_cooldown ... ok
|
||||
test platform_expr_strategy::tests::platform_weak_market_skips_target_adjust_before_actionable_stop_loss ... ok
|
||||
test platform_expr_strategy::tests::platform_weak_market_skips_target_increase_before_intraday_stop_loss_sell_blocked ... ok
|
||||
test platform_expr_strategy::tests::platform_weak_market_skips_target_increase_before_lower_limit_blocked_stop_loss ... ok
|
||||
test platform_expr_strategy::tests::selection_buffer_retains_held_symbols_within_buffer_rank ... ok
|
||||
test platform_expr_strategy::tests::selection_quote_plan_filters_daily_part_before_minute_candidates ... ok
|
||||
test platform_expr_strategy::tests::selection_quote_plan_keeps_configured_static_selection_risk_for_intraday_fields ... ok
|
||||
test platform_expr_strategy::tests::selection_replacement_limit_caps_voluntary_daily_removals ... ok
|
||||
test platform_expr_strategy::tests::projected_historical_slippage_does_not_swallow_missing_calibration ... ok
|
||||
test platform_expr_strategy::tests::session_event_uses_previous_bar_and_recomputes_at_each_minute_without_becoming_a_quote ... ok
|
||||
test platform_expr_strategy::tests::portfolio_loss_observes_finalized_nav_after_fees_and_cash_flows ... ok
|
||||
test platform_expr_strategy::tests::stock_pool_exit_weight_redistribution_is_explicit_and_uses_integer_bps ... ok
|
||||
test platform_expr_strategy::tests::stock_pool_suspension_keeps_budget_and_exit_redistributes_only_available_weights ... ok
|
||||
test platform_expr_strategy::tests::stock_pool_suspension_resolution_never_reads_the_future_execution_day ... ok
|
||||
test platform_expr_strategy::tests::signal_date_explicit_actions_are_due_only_on_declared_snapshot_dates ... ok
|
||||
test platform_expr_strategy::tests::selection_limit_state_requires_intraday_selection_quotes ... ok
|
||||
test platform_expr_strategy::tests::stock_pool_target_lifecycle_keeps_exits_for_next_day_reentry ... ok
|
||||
test platform_expr_strategy::tests::stock_state_cache_resets_before_reusing_compact_keys_on_another_date ... ok
|
||||
test platform_expr_strategy::tests::strategy_specific_labels_are_not_framework_rolling_factors ... ok
|
||||
test platform_expr_strategy::tests::stock_snapshot_fields_are_loaded_only_when_the_strategy_uses_them ... ok
|
||||
test platform_expr_strategy::tests::strategy105_numeric_expressions_compile_to_vm ... ok
|
||||
test platform_expr_strategy::tests::target_scale_replenishment_preserves_strategy_cash_allocation ... ok
|
||||
test platform_expr_strategy::tests::target_portfolio_smart_exposes_decision_and_execution_dates ... ok
|
||||
test platform_expr_strategy::tests::target_portfolio_smart_respects_current_date_weight_conditions ... ok
|
||||
test platform_expr_strategy::tests::typed_runtime_numbers_preserve_legacy_rhai_formatting ... ok
|
||||
test platform_expr_strategy::tests::universe_exclude_matches_explicit_symbols_and_bjse_alias ... ok
|
||||
test platform_runtime_schema::tests::runtime_schema_includes_known_identifiers ... ok
|
||||
test platform_runtime_schema::tests::runtime_schema_serializes_to_json_object ... ok
|
||||
test platform_strategy_spec::tests::accepts_equivalent_risk_policy_alias_values ... ok
|
||||
test platform_expr_strategy::tests::timed_rotation_runs_on_its_schedule_instead_of_the_untimed_day_hook ... ok
|
||||
test platform_strategy_spec::tests::accepts_scoped_blacklist_context_in_runtime_risk_policy ... ok
|
||||
test platform_strategy_spec::tests::canonicalizes_duplicate_execution_aliases_without_changing_strategy_intent ... ok
|
||||
test platform_strategy_spec::tests::current_close_respects_explicitly_disabled_liquidity_limit ... ok
|
||||
test platform_strategy_spec::tests::engine_config_parses_weak_market_shrink_overweight_threshold ... ok
|
||||
test platform_strategy_spec::tests::engine_stock_ma_filter_generates_price_and_volume_expr ... ok
|
||||
test platform_strategy_spec::tests::explicit_actions_inherit_top_level_runtime_schedule ... ok
|
||||
test platform_strategy_spec::tests::execution_limit_aliases_normalize_to_one_serde_field_without_touching_policy ... ok
|
||||
test platform_strategy_spec::tests::explicit_delayed_limit_open_exit_false_overrides_trade_times ... ok
|
||||
test platform_strategy_spec::tests::explicit_trading_schedule_overrides_rebalance_trade_times ... ok
|
||||
test platform_strategy_spec::tests::frozen_candidate_order_is_explicit_and_preserves_source_positions ... ok
|
||||
test platform_strategy_spec::tests::index_throttle_uses_signal_ma_not_performance_benchmark_ma ... ok
|
||||
test platform_strategy_spec::tests::native_factor_bindings_declare_completed_session_fields ... ok
|
||||
test platform_strategy_spec::tests::multiple_trade_times_do_not_imply_delayed_limit_exit ... ok
|
||||
test platform_strategy_spec::tests::normalize_board_does_not_infer_kcb_from_security_code ... ok
|
||||
test platform_strategy_spec::tests::numeric_strategy_aliases_accept_exact_zero_but_never_hide_rounding_or_conflicts ... ok
|
||||
test platform_strategy_spec::tests::parses_and_rejects_invalid_position_target_rules ... ok
|
||||
test platform_strategy_spec::tests::parses_buy_filter_as_a_separate_trading_condition ... ok
|
||||
test platform_strategy_spec::tests::parses_daily_schedule_time_for_execution_quotes ... ok
|
||||
test platform_strategy_spec::tests::parses_delayed_deposit_receiving_days_expression ... ok
|
||||
test platform_strategy_spec::tests::parses_dynamic_model_score_dates_into_platform_config ... ok
|
||||
test platform_strategy_spec::tests::parses_execution_cost_overrides_into_platform_config ... ok
|
||||
test platform_strategy_spec::tests::parses_execution_slippage_overrides_into_platform_config ... ok
|
||||
test platform_strategy_spec::tests::parses_explicit_historical_slippage_into_platform_config ... ok
|
||||
test platform_strategy_spec::tests::parses_explicit_delayed_limit_open_exit ... ok
|
||||
test platform_strategy_spec::tests::parses_lifecycle_target_portfolio_contract ... ok
|
||||
test platform_strategy_spec::tests::parses_generic_futures_actions_and_rejects_incomplete_contracts ... ok
|
||||
test platform_strategy_spec::tests::parses_max_holding_days_from_engine_and_runtime_trading ... ok
|
||||
test platform_expr_strategy::tests::timed_current_close_stop_loss_reloads_state_after_quote_arrives ... ok
|
||||
test platform_strategy_spec::tests::parses_percent_slippage_alias_into_platform_config ... ok
|
||||
test platform_strategy_spec::tests::parses_minute_stage_schedule_and_initial_subscriptions ... ok
|
||||
test platform_strategy_spec::tests::parses_position_average_entry_stop_take_reference_price_mode ... ok
|
||||
test platform_strategy_spec::tests::parses_portfolio_drawdown_control_into_platform_config ... ok
|
||||
test platform_strategy_spec::tests::parses_rebalance_cash_mode_and_forces_minute_to_actual_sequence ... ok
|
||||
test platform_strategy_spec::tests::parses_risk_policy_into_platform_config ... ok
|
||||
test platform_strategy_spec::tests::parses_runtime_expression_spec_into_platform_config ... ok
|
||||
test platform_strategy_spec::tests::parses_signal_dates_rebalance_into_platform_config ... ok
|
||||
test platform_strategy_spec::tests::parses_signal_day_post_adjusted_stop_take_reference_price_mode ... ok
|
||||
test platform_strategy_spec::tests::parses_st_and_star_st_risk_policy_switches_into_platform_config ... ok
|
||||
test platform_strategy_spec::tests::parses_typed_time_in_force_for_explicit_orders ... ok
|
||||
test platform_strategy_spec::tests::rejects_conflicting_non_policy_strategy_aliases ... ok
|
||||
test platform_strategy_spec::tests::rejects_conflicting_risk_policy_alias_values ... ok
|
||||
test platform_strategy_spec::tests::rejects_invalid_explicit_volume_percent_in_platform_config ... ok
|
||||
test platform_strategy_spec::tests::portfolio_loss_contract_is_explicit_and_validated ... ok
|
||||
test platform_strategy_spec::tests::rejects_invalid_portfolio_drawdown_control ... ok
|
||||
test platform_strategy_spec::tests::rejects_invalid_or_duplicate_static_universe_symbols ... ok
|
||||
test platform_strategy_spec::tests::rejects_removed_compatibility_profile ... ok
|
||||
test platform_strategy_spec::tests::rejects_removed_engine_profile_name ... ok
|
||||
test platform_strategy_spec::tests::rejects_unknown_stop_take_reference_price_mode ... ok
|
||||
test platform_strategy_spec::tests::platform_spec_accepts_only_supported_matching_types ... ok
|
||||
test platform_strategy_spec::tests::rejects_non_positive_shared_order_limits ... ok
|
||||
test platform_strategy_spec::tests::rejects_unknown_strategy_risk_policy_fields_before_deserialization ... ok
|
||||
test platform_strategy_spec::tests::rejects_unknown_time_in_force_in_runtime_contract ... ok
|
||||
test platform_strategy_spec::tests::runtime_expression_parser_does_not_inherit_microcap_template_defaults ... ok
|
||||
test platform_strategy_spec::tests::retired_or_unknown_slippage_models_do_not_fall_back_to_fixed_or_none ... ok
|
||||
test platform_strategy_spec::tests::runtime_expression_parses_dated_position_exposure_schedule ... ok
|
||||
test platform_strategy_spec::tests::runtime_expressions_infer_ma_windows_from_literal_strategy_logic ... ok
|
||||
test platform_strategy_spec::tests::volume_limit_does_not_enable_quote_quantity_limit_for_minute_last ... ok
|
||||
test portfolio::tests::buy_after_partial_sell_continues_moving_average_cost_basis ... ok
|
||||
test portfolio::tests::cash_dividend_can_preserve_avg_cost_for_aiquant_rules ... ok
|
||||
test portfolio::tests::delayed_withdrawals_are_reserved_and_settled_atomically ... ok
|
||||
test portfolio::tests::external_cash_flow_rebases_units_without_changing_nav ... ok
|
||||
test portfolio::tests::fifo_fixed_pnl_conserves_value_while_alv_average_cost_stays_stable ... ok
|
||||
test portfolio::tests::fixed_lot_allocation_rounds_nearest_and_conserves_total ... ok
|
||||
test portfolio::tests::holdings_summary_reports_entry_price_pnl_excluding_buy_commission ... ok
|
||||
test portfolio::tests::partial_sell_preserves_remaining_average_cost ... ok
|
||||
test portfolio::tests::portfolio_exposes_engine_native_account_metrics ... ok
|
||||
test portfolio::tests::portfolio_marks_same_day_buy_at_fill_until_next_trading_day ... ok
|
||||
test portfolio::tests::portfolio_missing_market_requires_formal_suspension_before_carrying_price ... ok
|
||||
test portfolio::tests::portfolio_tracks_dividend_receivable_and_day_pnl ... ok
|
||||
test portfolio::tests::position_tracks_day_lifecycle_fields ... ok
|
||||
test portfolio::tests::positions_preserve_insertion_order ... ok
|
||||
test portfolio::tests::scoped_checkpoint_restores_order_flat_rows_and_progress_without_copying_untouched_lots ... ok
|
||||
test portfolio::tests::strategy_entry_price_excludes_buy_commission_cost_basis ... ok
|
||||
test portfolio_loss::tests::cash_sessions_preserve_continuity_without_inventing_returns ... ok
|
||||
test platform_strategy_spec::tests::session_rotation_keeps_every_declared_clock_not_only_the_last_one ... ok
|
||||
test portfolio_loss::tests::refuses_future_missing_corrected_and_incomplete_accounting ... ok
|
||||
test platform_expr_strategy::tests::shared_signal_reduction_and_stops_use_each_accounts_cost_quantity_and_sold_state ... ok
|
||||
test portfolio_loss::tests::restored_state_rejects_changed_policy_and_forged_cooldown ... ok
|
||||
test position_exposure::tests::allocation_is_dated_and_any_later_scalar_or_restore_clears_it ... ok
|
||||
test position_exposure::tests::explicit_equity_targets_and_buys_scale_but_sales_cashflows_and_prices_do_not ... ok
|
||||
test position_exposure::tests::rejects_unknown_actions_duplicate_identity_and_invalid_bps ... ok
|
||||
test position_exposure::tests::same_day_adjustments_restore_and_future_events_keep_their_own_times ... ok
|
||||
test position_exposure::tests::scalar_preserves_strategy_risk_off_and_restore_keeps_original_exposure ... ok
|
||||
test risk_control::tests::concrete_kcb_reason_wins_over_generic_missing_risk_state ... ok
|
||||
test risk_control::tests::configurable_bjse_filter_can_be_disabled ... ok
|
||||
test risk_control::tests::configurable_blacklist_rejects_selection_and_buy ... ok
|
||||
test portfolio_loss::tests::restart_is_exact_and_duplicate_decisions_do_not_consume_cooldown ... ok
|
||||
test risk_control::tests::configurable_kcb_filter_can_be_disabled ... ok
|
||||
test risk_control::tests::configurable_lower_limit_selection_filter_can_be_disabled ... ok
|
||||
test risk_control::tests::configurable_upper_limit_buy_filter_can_be_disabled ... ok
|
||||
test risk_control::tests::configurable_upper_limit_selection_filter_can_be_disabled ... ok
|
||||
test risk_control::tests::delisting_period_status_is_not_terminal_without_delisted_date ... ok
|
||||
test risk_control::tests::execution_quote_covers_missing_one_yuan_flag_but_not_other_risk_facts ... ok
|
||||
test risk_control::tests::explicit_candidate_inactive_status_is_not_treated_as_missing_risk_state ... ok
|
||||
test risk_control::tests::explicit_one_yuan_selection_policy_still_uses_selection_facts ... ok
|
||||
test portfolio_loss::tests::nav_serialization_preserves_float_bits ... ok
|
||||
test risk_control::tests::fund_identity_excludes_stock_one_yuan_rule_but_not_actual_price_and_pause_checks ... ok
|
||||
test risk_control::tests::inactive_selection_checks_preserve_missing_facts_and_execution_rejections ... ok
|
||||
test risk_control::tests::kcb_filter_uses_classification_instead_of_security_code ... ok
|
||||
test risk_control::tests::missing_risk_state_can_be_relaxed_by_disabling_static_filters ... ok
|
||||
test risk_control::tests::missing_risk_state_default_selection_ignores_allow_flags_but_buy_rejects ... ok
|
||||
test risk_control::tests::missing_risk_state_rejects_sell_when_sell_risk_facts_are_missing ... ok
|
||||
test risk_control::tests::missing_risk_state_respects_field_specific_selection_switches ... ok
|
||||
test risk_control::tests::missing_risk_state_respects_field_specific_buy_switches ... ok
|
||||
test risk_control::tests::missing_risk_state_selection_audit_keeps_missing_fields_in_reason ... ok
|
||||
test risk_control::tests::missing_risk_state_selection_still_respects_configured_static_filters ... ok
|
||||
test risk_control::tests::missing_risk_state_sell_can_be_relaxed_by_disabling_sell_static_filters ... ok
|
||||
test risk_control::tests::one_yuan_buy_rule_uses_execution_price_not_later_close_or_earlier_open ... ok
|
||||
test risk_control::tests::sell_rejection_blocks_execution_price_at_lower_limit ... ok
|
||||
test risk_control::tests::sell_rejection_respects_allow_sell_policy_on_execution_day ... ok
|
||||
test risk_control::tests::sell_rejects_explicit_candidate_inactive_status_without_instrument_row ... ok
|
||||
test risk_control::tests::shared_order_size_limits_apply_to_both_sides_and_buy_position ... ok
|
||||
test risk_control::tests::st_and_star_st_filters_are_independent ... ok
|
||||
test scheduler::tests::date_due_is_independent_from_the_order_execution_clock ... ok
|
||||
test scheduler::tests::scheduler_matches_before_trading_and_minute_level_rules ... ok
|
||||
test scheduler::tests::scheduler_matches_daily_weekly_and_monthly_rules ... ok
|
||||
test portfolio::tests::cash_ledger_accumulates_micro_yuan_exactly ... ok
|
||||
test session_events::tests::gaps_and_stale_last_bars_do_not_become_false_or_repeated_signals ... ok
|
||||
test session_events::tests::opening_range_is_unavailable_before_the_range_has_completed ... ok
|
||||
test signal_contract::tests::changed_valid_contents_must_not_reuse_a_version_hash ... ok
|
||||
test signal_contract::tests::completed_daily_inputs_may_be_published_after_market_close ... ok
|
||||
test signal_contract::tests::coverage_and_duplicate_actions_fail_closed ... ok
|
||||
test signal_contract::tests::empty_complete_snapshot_clears_only_that_accounts_holdings ... ok
|
||||
test signal_contract::tests::historical_reconstruction_is_not_online_publication ... ok
|
||||
test signal_contract::tests::next_open_uses_decision_session_and_never_nearest_signal ... ok
|
||||
test signal_contract::tests::observed_next_open_never_backdates_a_morning_publication_into_yesterdays_orders ... ok
|
||||
test signal_contract::tests::platform_spec_consumes_book_without_running_another_selection ... ok
|
||||
test signal_contract::tests::reconstruction_ignores_research_wall_clock_but_never_early_input_availability ... ok
|
||||
test signal_contract::tests::reduction_is_resolved_from_each_accounts_actual_position ... ok
|
||||
test signal_contract::tests::rejects_future_inputs_and_model_knowledge ... ok
|
||||
test session_events::tests::decision_uses_the_previous_completed_label_and_future_prices_do_not_rewrite ... ok
|
||||
test signal_contract::tests::rejects_quantities_prices_and_unknown_signal_fields ... ok
|
||||
test stock_pool_candidates::tests::list_order_reuses_explicit_prefix_and_appends_new_candidates ... ok
|
||||
test stock_pool_candidates::tests::missing_snapshot_duplicate_input_and_empty_filtered_scope_fail ... ok
|
||||
test stock_pool_candidates::tests::mixed_sources_preserve_priority_and_both_provenances ... ok
|
||||
test stock_pool_candidates::tests::zero_automatic_day_keeps_manual_members_without_inheriting_old_auto_targets ... ok
|
||||
test signal_contract::tests::rejects_overallocation_nonfinite_and_ambiguous_actions ... ok
|
||||
test stock_pool_execution::frozen::ratio_tests::legacy_paused_and_explicit_partial_budgets_are_not_reinterpreted ... ok
|
||||
test stock_pool_execution::frozen::ratio_tests::precise_paused_budget_survives_replacement_and_zero_targets ... ok
|
||||
test stock_pool_execution::frozen::ratio_tests::precision_is_checked_before_frozen_budget_is_allocated ... ok
|
||||
test stock_pool_execution::tests::confirmed_holdings_not_unsettled_sells_determine_remaining_position_budget ... ok
|
||||
test stock_pool_execution::tests::all_membership_and_weight_combinations_use_the_same_target_planner ... ok
|
||||
test stock_pool_execution::tests::frozen_valuation_is_dated_and_cannot_turn_an_unknown_gap_into_a_pause ... ok
|
||||
test stock_pool_execution::tests::full_stop_overrides_a_simultaneous_factor_reduction_without_a_second_target ... ok
|
||||
test stock_pool_execution::tests::independent_exit_quote_priority_does_not_bypass_t_plus_one_or_price_validation ... ok
|
||||
test stock_pool_execution::tests::independent_full_exit_has_no_ordinary_quote_dependency_but_partial_risk_does_not_fake_missing_facts ... ok
|
||||
test stock_pool_execution::tests::independent_stop_does_not_require_unused_ordinary_sell_quote_facts ... ok
|
||||
test stock_pool_execution::tests::index_cap_cannot_sell_a_paused_holding_or_use_its_budget_as_free_cash ... ok
|
||||
test stock_pool_execution::tests::index_cap_keeps_non_sellable_quantity_and_defers_the_remaining_risk_target ... ok
|
||||
test stock_pool_execution::tests::index_cap_reduces_existing_proportions_without_enabling_weight_rebalancing ... ok
|
||||
test stock_pool_execution::tests::index_cap_respects_ordinary_weight_changes_and_stronger_fill_protection ... ok
|
||||
test stock_pool_execution::tests::equal_thirty_seats_use_full_precision_at_a_board_lot_boundary ... ok
|
||||
test stock_pool_execution::tests::instrument_price_tick_and_quantity_units_are_not_a_two_decimal_stock_assumption ... ok
|
||||
test stock_pool_execution::tests::legacy_constraints_do_not_gain_an_empty_precision_field ... ok
|
||||
test stock_pool_execution::tests::market_quantity_budget_includes_execution_slippage_and_fees ... ok
|
||||
test stock_pool_execution::tests::index_exposure_and_reserved_seats_multiply_without_rounding_the_budget_early ... ok
|
||||
test stock_pool_execution::tests::optional_configuration_and_conflicting_aliases_are_explicit ... ok
|
||||
test stock_pool_execution::tests::native_sell_and_quote_conditions_are_and_but_stop_and_protection_remain_independent ... ok
|
||||
test stock_pool_execution::tests::ordinary_sell_keeps_required_quote_failures_and_zero_stop_is_not_an_exit ... ok
|
||||
test stock_pool_execution::tests::partial_sell_cooldown_restricts_increases_without_clearing_the_remainder ... ok
|
||||
test stock_pool_execution::tests::ordinary_sell_scope_excludes_independent_exits_and_protected_positions ... ok
|
||||
test stock_pool_execution::tests::paused_outside_holding_retains_prior_budget_without_an_execution_quote ... ok
|
||||
test stock_pool_execution::tests::quote_condition_number_contract_rejects_invalid_facts_and_fractional_shares ... ok
|
||||
test session_events::tests::all_thirteen_events_return_native_boolean_series ... ok
|
||||
test stock_pool_execution::tests::paused_held_weight_is_not_replenished_after_another_stock_exits ... ok
|
||||
test stock_pool_execution::tests::quote_condition_scopes_are_evaluated_before_target_orders ... ok
|
||||
test stock_pool_execution::tests::quote_only_exit_still_works_when_independent_risk_rules_are_configured ... ok
|
||||
test stock_pool_execution::tests::retained_holdings_reserve_actual_value_and_zero_funding_still_clears ... ok
|
||||
test stock_pool_execution::tests::reserve_slots_keep_requested_and_effective_exposure_distinct ... ok
|
||||
test stock_pool_execution::tests::sale_slippage_does_not_prevent_a_marked_value_board_lot_reduction ... ok
|
||||
test stock_pool_execution::tests::risk_only_configuration_never_turns_into_an_unconditional_ordinary_exit ... ok
|
||||
test stock_pool_execution::tests::sell_cooldown_does_not_turn_disabled_weight_rebalancing_back_on ... ok
|
||||
test stock_pool_execution::tests::sell_quote_condition_creates_exit_and_unmet_holding_keeps_its_slot ... ok
|
||||
test stock_pool_execution::tests::selection_excluding_paused_stock_keeps_the_same_24_stock_budget_as_broker_execution ... ok
|
||||
test stock_pool_execution::tests::stop_reduction_merge_matrix_preserves_protection_t_plus_one_and_invalid_config_errors ... ok
|
||||
test stock_pool_index_policy::tests::disabled_is_optional_but_enabled_parameters_are_not_invented ... ok
|
||||
test stock_pool_index_policy::tests::missing_or_duplicate_or_future_rows_never_shorten_the_window ... ok
|
||||
test stock_pool_index_policy::tests::trend_and_caps_match_the_declared_math_and_keep_zero_meaningful ... ok
|
||||
test stock_pool_indicators::tests::kdj_uses_the_same_frozen_native_kernel_as_event_conditions ... ok
|
||||
test stock_pool_indicators::tests::missing_inputs_require_source_admission_and_future_inputs_fail ... ok
|
||||
test stock_pool_indicators::tests::uses_shared_ma_ema_and_preserves_recursive_prefix_before_output_window ... ok
|
||||
test stock_pool_market_cap::tests::interpolates_declared_endpoints_and_clamps_without_business_defaults ... ok
|
||||
test stock_pool_market_cap::tests::missing_duplicate_or_mismatched_index_inputs_do_not_shrink_the_calendar ... ok
|
||||
test stock_pool_quote_facts::tests::lunch_and_post_close_gaps_follow_the_minute_feed_segments ... ok
|
||||
test stock_pool_quote_facts::tests::real_source_session_prefix_matches_observed_checkpoints ... ignored, requires FIDC_SESSION_PREFIX_SOURCE_JSON from the frozen Source minute response
|
||||
test stock_pool_quote_facts::tests::sparse_unverified_and_overflowing_quotes_cannot_be_called_session_totals ... ok
|
||||
test stock_pool_quote_facts::tests::totals_use_only_the_complete_observed_prefix_and_keep_decimal_amounts ... ok
|
||||
test strategy::decision_merge_tests::explicit_empty_complete_target_replaces_old_allocation_but_empty_callback_does_not ... ok
|
||||
test strategy::decision_merge_tests::newer_complete_target_replaces_old_symbols_without_discarding_explicit_actions ... ok
|
||||
test strategy::tests::omni_microcap_projection_uses_configured_trading_cost ... ok
|
||||
test strategy::tests::omni_microcap_selection_uses_configured_risk_policy ... ok
|
||||
test strategy::tests::order_time_in_force_parsing_and_order_type_contract_are_explicit ... ok
|
||||
test strategy_ai::tests::generation_prompt_uses_explicit_performance_acceptance_contract ... ok
|
||||
test strategy_ai::tests::manual_separates_explicit_business_selection_from_framework_risk_policy ... ok
|
||||
test strategy_ai::tests::optimization_prompt_uses_explicit_performance_acceptance_contract ... ok
|
||||
test universe::tests::selector_applies_configured_selection_risk_on_decision_date ... ok
|
||||
test universe::tests::selector_can_defer_configured_selection_risk_without_losing_diagnostics ... ok
|
||||
test universe::tests::selector_records_structured_selection_risk_decisions ... ok
|
||||
test stock_pool_execution::tests::typed_exit_roles_merge_only_satisfied_ordinary_actions_with_independent_risk ... ok
|
||||
test risk_control::tests::selection_check_activation_covers_every_configured_flag_and_blacklist_state ... ok
|
||||
test stock_pool_execution::tests::quote_field_operator_side_and_scope_matrix_matches_the_configured_predicate ... ok
|
||||
|
||||
test result: ok. 779 passed; 0 failed; 7 ignored; 0 measured; 0 filtered out; finished in 0.54s
|
||||
|
||||
Running unittests src/bin/dump_platform_runtime_schema.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/dump_platform_runtime_schema-c4b52627aec309c9)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/automatic_trade_protection.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/automatic_trade_protection-e76e4284ad200e69)
|
||||
|
||||
running 6 tests
|
||||
test absolute_lock_blocks_initial_strategy_buy_without_a_rejected_order ... ok
|
||||
test framework_protection_uses_fills_and_covers_explicit_strategy_orders ... ok
|
||||
test maximum_holding_policy_applies_to_discrete_strategies_and_yields_to_buy_protection ... ok
|
||||
test serialized_framework_policy_survives_shared_alias_normalization_and_rejects_conflicts ... ok
|
||||
test locked_holding_keeps_its_slot_even_when_cash_can_buy_the_next_candidate ... ok
|
||||
test observed_manual_trades_then_split_keep_real_fill_protection_and_lock_dates ... ok
|
||||
|
||||
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
|
||||
|
||||
Running tests/core_rules.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/core_rules-6365eded07d721e8)
|
||||
|
||||
running 8 tests
|
||||
test china_cost_model_applies_minimum_commission_and_stamp_tax ... ok
|
||||
test china_cost_model_tracks_minimum_commission_per_order_id ... ok
|
||||
test china_cost_model_switches_stamp_tax_rate_after_2023_08_28 ... ok
|
||||
test china_rule_hooks_allow_sell_when_last_price_is_above_lower_limit ... ok
|
||||
test china_rule_hooks_block_same_day_sell_under_t_plus_one ... ok
|
||||
test china_rule_hooks_block_buy_at_limit_up_and_sell_at_limit_down ... ok
|
||||
test china_rule_hooks_use_strict_price_limits ... ok
|
||||
test configured_cost_model_matches_declared_run_options ... ok
|
||||
|
||||
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/corporate_actions.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/corporate_actions-05d8a8628f15e674)
|
||||
|
||||
running 6 tests
|
||||
test cash_dividend_adjusts_cost_basis_and_returns_cash_delta ... ok
|
||||
test stock_split_scales_lots_quantity_and_average_cost ... ok
|
||||
test portfolio_settles_cash_receivable_on_payable_date ... ok
|
||||
test engine_settles_same_day_dividend_after_split_for_aiquant_semantics ... FAILED
|
||||
test engine_reinvests_dividend_receivable_in_round_lots ... ok
|
||||
test successor_conversion_depletes_older_source_lots_before_newer_successor_buys ... ok
|
||||
|
||||
failures:
|
||||
|
||||
---- engine_settles_same_day_dividend_after_split_for_aiquant_semantics stdout ----
|
||||
|
||||
thread 'engine_settles_same_day_dividend_after_split_for_aiquant_semantics' (4173472) panicked at crates/fidc-core/tests/corporate_actions.rs:663:5:
|
||||
assertion failed: result.position_events.iter().any(|event|
|
||||
event.reason == "stock_split 1.200000" &&
|
||||
event.quantity_after == 1_200)
|
||||
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||||
|
||||
|
||||
failures:
|
||||
engine_settles_same_day_dividend_after_split_for_aiquant_semantics
|
||||
|
||||
test result: FAILED. 5 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
error: test failed, to rerun pass `-p fidc-core --test corporate_actions`
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
Compiling fidc-core v0.1.0 (/srv/fidc/programs/src/fidc-share-receivable-validation-20261005-0bbeed4/crates/fidc-core)
|
||||
Finished `test` profile [unoptimized] target(s) in 45.20s
|
||||
Running unittests src/lib.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/fidc_core-dbea4d4f6cb5d2d7)
|
||||
|
||||
running 22 tests
|
||||
test corporate_book::tests::closing_register_cannot_be_replaced_by_later_holdings ... ok
|
||||
test corporate_book::tests::dividend_receivable_survives_a_flat_position_without_becoming_spendable_cash ... ok
|
||||
test corporate_book::tests::entitlement_survives_complete_sale_and_a_successor_unit_conversion ... ok
|
||||
test corporate_book::tests::a_late_corporate_batch_failure_keeps_prior_cash_positions_and_notes ... ok
|
||||
|
||||
thread 'corporate_book::tests::corporate_quantity_overflow_fails_without_saturation_or_a_negative_delta' (4165594) panicked at crates/fidc-core/src/corporate_book.rs:1098:13:
|
||||
assertion failed: error.to_string().contains("corporate_action_quantity_overflow")
|
||||
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||||
test corporate_book::tests::explicitly_delayed_dividend_remains_receivable_until_actual_payment ... ok
|
||||
test corporate_book::tests::flat_position_retirement_does_not_change_issued_lot_state_or_next_purchase_origin ... ok
|
||||
test corporate_book::tests::corporate_quantity_overflow_fails_without_saturation_or_a_negative_delta ... FAILED
|
||||
test corporate_book::tests::missing_accounting_reference_is_atomic_not_a_silent_cash_only_success ... ok
|
||||
test corporate_book::tests::manual_zero_skips_only_unused_allocation_facts_not_invalid_cash_evidence ... ok
|
||||
test corporate_book::tests::invalid_delivery_negative_rates_and_unresolved_fractional_claims_are_atomic ... ok
|
||||
test corporate_book::tests::missing_or_backward_cash_payment_date_never_updates_the_account ... ok
|
||||
test corporate_book::tests::per_share_precision_is_retained_until_the_registered_cash_amount_is_rounded ... ok
|
||||
test corporate_book::tests::missing_record_date_or_unobserved_closing_book_is_atomic ... ok
|
||||
test corporate_book::tests::post_record_buy_cannot_receive_the_earlier_dividend ... ok
|
||||
test corporate_book::tests::post_record_purchase_does_not_inherit_registered_shares ... ok
|
||||
test corporate_book::tests::registered_shares_are_valued_but_not_delivered_or_sellable_before_the_original_date ... ok
|
||||
test corporate_book::tests::sale_after_registration_preserves_cash_and_excludes_new_lots_from_cost_adjustment ... ok
|
||||
|
||||
thread 'corporate_book::tests::successor_without_frozen_instrument_metadata_is_not_an_implicit_new_security' (4165614) panicked at crates/fidc-core/src/corporate_book.rs:998:9:
|
||||
assertion failed: error.to_string().contains("successor_instrument_missing")
|
||||
test corporate_book::tests::terminated_security_keeps_paid_cash_and_is_not_recreated_by_reinvestment ... ok
|
||||
test corporate_book::tests::successor_without_frozen_instrument_metadata_is_not_an_implicit_new_security ... FAILED
|
||||
test corporate_book::tests::verified_conversion_preserves_lots_without_creating_orders_or_fills ... ok
|
||||
test corporate_book::tests::tiny_known_cash_rate_is_preserved_and_cannot_waive_payment_or_record_evidence ... ok
|
||||
test corporate_book::tests::invalid_successor_terms_cannot_be_replaced_with_one_share_or_zero_cash ... ok
|
||||
|
||||
failures:
|
||||
|
||||
failures:
|
||||
corporate_book::tests::corporate_quantity_overflow_fails_without_saturation_or_a_negative_delta
|
||||
corporate_book::tests::successor_without_frozen_instrument_metadata_is_not_an_implicit_new_security
|
||||
|
||||
test result: FAILED. 20 passed; 2 failed; 0 ignored; 0 measured; 764 filtered out; finished in 0.01s
|
||||
|
||||
error: test failed, to rerun pass `-p fidc-core --lib`
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,140 @@
|
||||
{
|
||||
"engine_commit": "ce46c13f37f028393e689babbe5c348d482d3a32",
|
||||
"files": [
|
||||
{
|
||||
"bytes": 3625,
|
||||
"file": "core-initial-targeted-failure.log",
|
||||
"sha256": "9684e4e1c10aabe426c15cd408bb61b198ada8eed1258231e4b81fc30716ac49",
|
||||
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-0bbeed4/initial-targeted-linux-tests.log",
|
||||
"source_uid": 1003
|
||||
},
|
||||
{
|
||||
"bytes": 82467,
|
||||
"file": "core-attempt1-failure.log",
|
||||
"sha256": "9535151bd7a29a5f56265ef35dbff89b3e48729688cb34c32aa8a8fc9347e41c",
|
||||
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-0caac44/full-linux-tests-attempt1.log",
|
||||
"source_uid": 1003
|
||||
},
|
||||
{
|
||||
"bytes": 95343,
|
||||
"file": "core-attempt2-failure.log",
|
||||
"sha256": "db18ad5e876ce482dc1f76ac244e170a6095c2ead8a991af5dce45e72af8e9ac",
|
||||
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-4eb4c17/full-linux-tests-attempt2.log",
|
||||
"source_uid": 1003
|
||||
},
|
||||
{
|
||||
"bytes": 98381,
|
||||
"file": "core-attempt3-success.log",
|
||||
"sha256": "66879f838ec21001cb3e6c284f497cfb6a80c2d3a724ee9cb04e09476afc101b",
|
||||
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-05315f0/full-linux-tests-attempt3.log",
|
||||
"source_uid": 1003
|
||||
},
|
||||
{
|
||||
"bytes": 98381,
|
||||
"file": "core-sell-availability-success.log",
|
||||
"sha256": "b565d8b983fb87881974da37ad5bd0721db3f51acda3af0cbe1b62406f18371c",
|
||||
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-a267052/final-full-linux-tests.log",
|
||||
"source_uid": 1003
|
||||
},
|
||||
{
|
||||
"bytes": 98381,
|
||||
"file": "core-successor-guard-success.log",
|
||||
"sha256": "b8f7a20a93335801950e40ce3a7124ae039e84fbb1db7dd821c314d2b05b4447",
|
||||
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-7d7cce8/final-full-linux-tests.log",
|
||||
"source_uid": 1003
|
||||
},
|
||||
{
|
||||
"bytes": 98572,
|
||||
"file": "core-final-full-linux-tests.log",
|
||||
"sha256": "cfd1cb81fde9f34991a54a54e2a734844bd8fab52292e043856b08d30090d19a",
|
||||
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-ce46c13/final-full-linux-tests.log",
|
||||
"source_uid": 1003
|
||||
},
|
||||
{
|
||||
"bytes": 10543,
|
||||
"file": "runner-initial-compile-failure.log",
|
||||
"sha256": "e758be915373f707a460e7d08ab4162f3b80d86b98091c555cb3351c2bb4d7b8",
|
||||
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1fa73bb/full-linux-tests-attempt1.log",
|
||||
"source_uid": 1003
|
||||
},
|
||||
{
|
||||
"bytes": 65133,
|
||||
"file": "runner-attempt2-success.log",
|
||||
"sha256": "d053e4e432cbe0b198f85ceee349b241eac4cf90b8e6a75289c322c59538b2b7",
|
||||
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ffc6e1f/full-linux-tests-attempt2.log",
|
||||
"source_uid": 1003
|
||||
},
|
||||
{
|
||||
"bytes": 65133,
|
||||
"file": "runner-successor-pair-success.log",
|
||||
"sha256": "6f2e8974a90d5cdbce788e2744a1276dff9fe94b79958869cfb0730c096d1a42",
|
||||
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/final-full-linux-tests.log",
|
||||
"source_uid": 1003
|
||||
},
|
||||
{
|
||||
"bytes": 7207,
|
||||
"file": "prior-validation-receipt.json",
|
||||
"sha256": "5240dbf5f1d7382531a4c0dce5fcdb722f4d1137a41854269159b61f0236d412",
|
||||
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/validation-receipt.json",
|
||||
"source_uid": 1003
|
||||
},
|
||||
{
|
||||
"bytes": 8550,
|
||||
"file": "prior-validation-pg-receipt.json",
|
||||
"sha256": "45b483a748c5354239dd678e66b652f53d9225108f2ab41f152a06250e69bd25",
|
||||
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/validation-pg-receipt.json",
|
||||
"source_uid": 1003
|
||||
},
|
||||
{
|
||||
"bytes": 2275,
|
||||
"file": "prior-operational-verification.json",
|
||||
"sha256": "32e634849aecbd2b634c6dcdbad68013f2c5eb603c8d586a43cc4698d8b18a4a",
|
||||
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/operational-verification.json",
|
||||
"source_uid": 1003
|
||||
},
|
||||
{
|
||||
"bytes": 762,
|
||||
"file": "receipt-construction-failure.json",
|
||||
"sha256": "e62a4819bd08599a3af797588211c79add8867bb8b3dfb67c28497bfe5780445",
|
||||
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/receipt-construction-failure.json",
|
||||
"source_uid": 1003
|
||||
},
|
||||
{
|
||||
"bytes": 65258,
|
||||
"file": "runner-final-full-linux-tests.log",
|
||||
"sha256": "2ba99fb0198a68ff5bc3d6341b1cdeb3da1a4d0ca5496b2faa3e79e3494ae434",
|
||||
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/final-full-linux-tests.log",
|
||||
"source_uid": 1003
|
||||
},
|
||||
{
|
||||
"bytes": 8192,
|
||||
"file": "validation-receipt.json",
|
||||
"sha256": "097101988e5f6875c8933012534cfa1ca72f9e2742c04d002aba7d00a69fc346",
|
||||
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/validation-receipt.json",
|
||||
"source_uid": 1003
|
||||
},
|
||||
{
|
||||
"bytes": 9573,
|
||||
"file": "validation-pg-receipt.json",
|
||||
"sha256": "04207632e68efaea406e1357937a5bb75d23fa0d74e5461ccfa7993b07e302e8",
|
||||
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/validation-pg-receipt.json",
|
||||
"source_uid": 1003
|
||||
},
|
||||
{
|
||||
"bytes": 2275,
|
||||
"file": "operational-verification.json",
|
||||
"sha256": "3ce2ba78ed28f549ddc675429565e578fe61eee1db4374c375e5049761c8fd06",
|
||||
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/operational-verification.json",
|
||||
"source_uid": 1003
|
||||
},
|
||||
{
|
||||
"bytes": 839,
|
||||
"file": "live-test-resource-verification.json",
|
||||
"sha256": "1686248392f29ae93f023ff147fcc0abb06b6b2fb15a9806b5718e320ed75a40",
|
||||
"source_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/live-test-resource-verification.json",
|
||||
"source_uid": 1003
|
||||
}
|
||||
],
|
||||
"schema_version": "fidc.local-share-delivery-evidence-manifest/v1",
|
||||
"service_commit": "1d4b32e18d3836b5844130ae0c95d90247768882"
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"contract_sha256": "fed292d158989d58f63b478d266675e294b1f156f20796406615f9fc4eb7b398",
|
||||
"observed_at": "2026-10-04T22:06:48.117070+00:00",
|
||||
"observed_process_owners": [
|
||||
{
|
||||
"pid": 83961,
|
||||
"uid": 1003
|
||||
},
|
||||
{
|
||||
"pid": 84064,
|
||||
"uid": 1003
|
||||
},
|
||||
{
|
||||
"pid": 84092,
|
||||
"uid": 1003
|
||||
}
|
||||
],
|
||||
"owner_uid": 1003,
|
||||
"production_ready": false,
|
||||
"properties": {
|
||||
"ActiveState": "active",
|
||||
"CPUQuotaPerSecUSec": "4s",
|
||||
"ControlGroup": "/user.slice/user-1003.slice/user@1003.service/app.slice/fidc-share-delivery-quantity-validation-20261005.scope",
|
||||
"MemoryMax": "17179869184",
|
||||
"MemorySwapMax": "0",
|
||||
"TasksMax": "256"
|
||||
},
|
||||
"schema_version": "fidc.live-share-delivery-test-scope/v1",
|
||||
"trading_allowed": false,
|
||||
"unit": "fidc-share-delivery-quantity-validation-20261005.scope"
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"authorized_incremental_cache_root": "/srv/fidc/programs/src/fidc-backtest-service/target/debug/incremental",
|
||||
"contract_sha256": "4696cb0eb31095275a0440c3c25e605502c2b6b6f07acf46032eb552d3547e7e",
|
||||
"health": [
|
||||
{
|
||||
"body_sha256": "3d4310bd55460dc1d83c95d0420b5e2c979a15b90c6d98f4b45378b645f6cd96",
|
||||
"http_status": 200,
|
||||
"port": 18095
|
||||
},
|
||||
{
|
||||
"body_sha256": "897da12d1075bd042696ef9b39e0aec42706985e9880887be0daceeab21afb17",
|
||||
"http_status": 200,
|
||||
"port": 18096
|
||||
}
|
||||
],
|
||||
"incremental_files_present": 0,
|
||||
"numeric_research_budget_reopened": false,
|
||||
"observed_at": "2026-10-04T22:08:10.206192+00:00",
|
||||
"original_research_plan_sha256": "6aa6932b8b7d4538543e7baf48b1b0ab305e684b36003faeaa7f0f4db4067c0c",
|
||||
"original_research_source_commit": "d847b82de3f62a369291c13160b31fcfc0075b8b",
|
||||
"owner_uid": 1003,
|
||||
"production_ready": false,
|
||||
"production_runtime_deployed": false,
|
||||
"production_strategy_count": 0,
|
||||
"schema_version": "fidc.share-delivery-operational-verification/v1",
|
||||
"ssd_free_bytes": 9045954560,
|
||||
"trading_allowed": false,
|
||||
"units": [
|
||||
{
|
||||
"ActiveState": "active",
|
||||
"InvocationID": "b6bd3c6c3a6d4f729291b969590d66a0",
|
||||
"MainPID": "3571059",
|
||||
"NRestarts": "0",
|
||||
"process_uid": 1003,
|
||||
"unit": "fidc-private-agent-runtime.service"
|
||||
},
|
||||
{
|
||||
"ActiveState": "active",
|
||||
"InvocationID": "851771deedc44b78bea58e18c74f9421",
|
||||
"MainPID": "1073961",
|
||||
"NRestarts": "1",
|
||||
"process_uid": 1003,
|
||||
"unit": "fidc-private-source-lake-worker.service"
|
||||
},
|
||||
{
|
||||
"ActiveState": "active",
|
||||
"InvocationID": "ce19e205006945e888c42ff385f96a50",
|
||||
"MainPID": "2371183",
|
||||
"NRestarts": "0",
|
||||
"process_uid": 1003,
|
||||
"unit": "fidc-private-source-lake.service"
|
||||
},
|
||||
{
|
||||
"ActiveState": "active",
|
||||
"InvocationID": "386b6478a0384505a8c9704e2d2a1478",
|
||||
"MainPID": "3571320",
|
||||
"NRestarts": "0",
|
||||
"process_uid": 1003,
|
||||
"unit": "fidc-private-strategy-ai.service"
|
||||
},
|
||||
{
|
||||
"ActiveState": "active",
|
||||
"InvocationID": "0eb28e3a45694a00951aa3aec0e13465",
|
||||
"MainPID": "1396112",
|
||||
"NRestarts": "0",
|
||||
"process_uid": 1003,
|
||||
"unit": "fidc-projection-research-autopilot.service"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"authorized_incremental_cache_root": "/srv/fidc/programs/src/fidc-backtest-service/target/debug/incremental",
|
||||
"contract_sha256": "66d0cf1d1a93b7960777c494da152af837f1c86b49bccf5591ba3f295f7383b6",
|
||||
"health": [
|
||||
{
|
||||
"body_sha256": "0dc4f9d9af47e41507d9267ee3e08d4cce2027b0b757f0f87193bf27c73abd91",
|
||||
"http_status": 200,
|
||||
"port": 18095
|
||||
},
|
||||
{
|
||||
"body_sha256": "245cfc3e4dd1b3301167429fa8cdfd53e49f7f6a5d8ad754047a6bc1645f5f93",
|
||||
"http_status": 200,
|
||||
"port": 18096
|
||||
}
|
||||
],
|
||||
"incremental_files_present": 0,
|
||||
"numeric_research_budget_reopened": false,
|
||||
"observed_at": "2026-10-04T22:02:08.949482+00:00",
|
||||
"original_research_plan_sha256": "6aa6932b8b7d4538543e7baf48b1b0ab305e684b36003faeaa7f0f4db4067c0c",
|
||||
"original_research_source_commit": "d847b82de3f62a369291c13160b31fcfc0075b8b",
|
||||
"owner_uid": 1003,
|
||||
"production_ready": false,
|
||||
"production_runtime_deployed": false,
|
||||
"production_strategy_count": 0,
|
||||
"schema_version": "fidc.share-delivery-operational-verification/v1",
|
||||
"ssd_free_bytes": 9070112768,
|
||||
"trading_allowed": false,
|
||||
"units": [
|
||||
{
|
||||
"ActiveState": "active",
|
||||
"InvocationID": "b6bd3c6c3a6d4f729291b969590d66a0",
|
||||
"MainPID": "3571059",
|
||||
"NRestarts": "0",
|
||||
"process_uid": 1003,
|
||||
"unit": "fidc-private-agent-runtime.service"
|
||||
},
|
||||
{
|
||||
"ActiveState": "active",
|
||||
"InvocationID": "851771deedc44b78bea58e18c74f9421",
|
||||
"MainPID": "1073961",
|
||||
"NRestarts": "1",
|
||||
"process_uid": 1003,
|
||||
"unit": "fidc-private-source-lake-worker.service"
|
||||
},
|
||||
{
|
||||
"ActiveState": "active",
|
||||
"InvocationID": "ce19e205006945e888c42ff385f96a50",
|
||||
"MainPID": "2371183",
|
||||
"NRestarts": "0",
|
||||
"process_uid": 1003,
|
||||
"unit": "fidc-private-source-lake.service"
|
||||
},
|
||||
{
|
||||
"ActiveState": "active",
|
||||
"InvocationID": "386b6478a0384505a8c9704e2d2a1478",
|
||||
"MainPID": "3571320",
|
||||
"NRestarts": "0",
|
||||
"process_uid": 1003,
|
||||
"unit": "fidc-private-strategy-ai.service"
|
||||
},
|
||||
{
|
||||
"ActiveState": "active",
|
||||
"InvocationID": "0eb28e3a45694a00951aa3aec0e13465",
|
||||
"MainPID": "1396112",
|
||||
"NRestarts": "0",
|
||||
"process_uid": 1003,
|
||||
"unit": "fidc-projection-research-autopilot.service"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
{
|
||||
"artifact_bytes": 7207,
|
||||
"artifact_id": "strategy_research_cb32517064d7558f69713001",
|
||||
"artifact_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/validation-receipt.json",
|
||||
"artifact_sha256": "5240dbf5f1d7382531a4c0dce5fcdb722f4d1137a41854269159b61f0236d412",
|
||||
"artifact_type": "research_source_scope_probe",
|
||||
"contract_sha256": "35ecf44697bf2e35967221e9f6dd9b330f3445137a3b00dadd3da59a01db2720",
|
||||
"created_at": "2026-10-04T22:03:46Z",
|
||||
"created_by": "boris",
|
||||
"department_id": null,
|
||||
"organization_id": "local_org",
|
||||
"persisted_at": "2026-10-04T22:03:46Z",
|
||||
"production_ready": false,
|
||||
"record_json": {
|
||||
"all_attempt_log_references": [
|
||||
{
|
||||
"bytes": 3625,
|
||||
"compiler_failure": false,
|
||||
"complete_success": false,
|
||||
"failed": 2,
|
||||
"ignored": 0,
|
||||
"passed": 20,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-0bbeed4/initial-targeted-linux-tests.log",
|
||||
"sha256": "9684e4e1c10aabe426c15cd408bb61b198ada8eed1258231e4b81fc30716ac49"
|
||||
},
|
||||
{
|
||||
"bytes": 82467,
|
||||
"compiler_failure": false,
|
||||
"complete_success": false,
|
||||
"failed": 1,
|
||||
"ignored": 7,
|
||||
"passed": 798,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-0caac44/full-linux-tests-attempt1.log",
|
||||
"sha256": "9535151bd7a29a5f56265ef35dbff89b3e48729688cb34c32aa8a8fc9347e41c"
|
||||
},
|
||||
{
|
||||
"bytes": 95343,
|
||||
"compiler_failure": false,
|
||||
"complete_success": false,
|
||||
"failed": 4,
|
||||
"ignored": 9,
|
||||
"passed": 923,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-4eb4c17/full-linux-tests-attempt2.log",
|
||||
"sha256": "db18ad5e876ce482dc1f76ac244e170a6095c2ead8a991af5dce45e72af8e9ac"
|
||||
},
|
||||
{
|
||||
"bytes": 98381,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"passed": 967,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-05315f0/full-linux-tests-attempt3.log",
|
||||
"sha256": "66879f838ec21001cb3e6c284f497cfb6a80c2d3a724ee9cb04e09476afc101b"
|
||||
},
|
||||
{
|
||||
"bytes": 98381,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"passed": 967,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-a267052/final-full-linux-tests.log",
|
||||
"sha256": "b565d8b983fb87881974da37ad5bd0721db3f51acda3af0cbe1b62406f18371c"
|
||||
},
|
||||
{
|
||||
"bytes": 98381,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"passed": 967,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-7d7cce8/final-full-linux-tests.log",
|
||||
"sha256": "b8f7a20a93335801950e40ce3a7124ae039e84fbb1db7dd821c314d2b05b4447"
|
||||
},
|
||||
{
|
||||
"bytes": 98572,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"passed": 969,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-ce46c13/final-full-linux-tests.log",
|
||||
"sha256": "cfd1cb81fde9f34991a54a54e2a734844bd8fab52292e043856b08d30090d19a"
|
||||
},
|
||||
{
|
||||
"bytes": 10543,
|
||||
"compiler_failure": true,
|
||||
"complete_success": false,
|
||||
"failed": 0,
|
||||
"ignored": 0,
|
||||
"passed": 0,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1fa73bb/full-linux-tests-attempt1.log",
|
||||
"sha256": "e758be915373f707a460e7d08ab4162f3b80d86b98091c555cb3351c2bb4d7b8"
|
||||
},
|
||||
{
|
||||
"bytes": 65133,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 17,
|
||||
"passed": 613,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ffc6e1f/full-linux-tests-attempt2.log",
|
||||
"sha256": "d053e4e432cbe0b198f85ceee349b241eac4cf90b8e6a75289c322c59538b2b7"
|
||||
},
|
||||
{
|
||||
"bytes": 65133,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 17,
|
||||
"passed": 613,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/final-full-linux-tests.log",
|
||||
"sha256": "6f2e8974a90d5cdbce788e2744a1276dff9fe94b79958869cfb0730c096d1a42"
|
||||
}
|
||||
],
|
||||
"contract_sha256": "b4885b28b8076c61977dbe24948d2c0eabc7ac32f6e8ad0591b2011f1b9b2c11",
|
||||
"core_test_result": {
|
||||
"bytes": 98572,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"passed": 969,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-ce46c13/final-full-linux-tests.log",
|
||||
"sha256": "cfd1cb81fde9f34991a54a54e2a734844bd8fab52292e043856b08d30090d19a"
|
||||
},
|
||||
"corporate_action_cache_schema_version": 5,
|
||||
"engine_commit": "ce46c13f37f028393e689babbe5c348d482d3a32",
|
||||
"fractional_quantity_requires_explicit_settlement_rule": true,
|
||||
"full_source_event_and_revision_scope_verified": false,
|
||||
"new_runtime_release_performed": false,
|
||||
"numeric_research_budget_reopened": false,
|
||||
"observed_at": "2026-10-04T22:02:08.963623+00:00",
|
||||
"operational_verification_reference": {
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/operational-verification.json",
|
||||
"sha256": "32e634849aecbd2b634c6dcdbad68013f2c5eb603c8d586a43cc4698d8b18a4a"
|
||||
},
|
||||
"owner_uid": 1003,
|
||||
"pending_share_successor_conversion_without_carry_terms_rejected": true,
|
||||
"performance_speedup_claimed": false,
|
||||
"production_ready": false,
|
||||
"production_runtime_deployed": false,
|
||||
"production_strategy_count": 0,
|
||||
"registered_share_entitlements_separate_from_delivered_and_sellable_quantity": true,
|
||||
"resource_contract_requested": {
|
||||
"build_jobs": 4,
|
||||
"core_target_path": "/tmp/fidc-cash-receivable-build-20261005-af655de",
|
||||
"cpu_quota_percent": 400,
|
||||
"incremental": false,
|
||||
"locked": true,
|
||||
"memory_max_bytes": 17179869184,
|
||||
"memory_swap_max_bytes": 0,
|
||||
"offline": true,
|
||||
"resource_properties_after_transient_scope_exit_are_not_live_validation": true,
|
||||
"runner_target_path": "/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb",
|
||||
"rustc": "rustc 1.98.0 (88d9e12ae 2026-08-18)",
|
||||
"target_filesystem": "executable_tmpfs",
|
||||
"tasks_max": 256,
|
||||
"toolchain": "stable-x86_64-unknown-linux-gnu"
|
||||
},
|
||||
"runner_api_test_result": {
|
||||
"bytes": 65133,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 17,
|
||||
"passed": 613,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/final-full-linux-tests.log",
|
||||
"sha256": "6f2e8974a90d5cdbce788e2744a1276dff9fe94b79958869cfb0730c096d1a42"
|
||||
},
|
||||
"schema_version": "fidc.linux-registered-share-delivery-validation/v1",
|
||||
"service_commit": "ffc6e1f5b8b1d6c9b342a661c7a25ebfc3d8d4c5",
|
||||
"share_receivable_assets_included_in_equity_and_target_sizing": true,
|
||||
"simulation_rounding_fixture_not_official_settlement_evidence": true,
|
||||
"source_pair_root": "/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ce46c13",
|
||||
"source_publication_times_admitted_as_PIT": false,
|
||||
"source_rows_semantic_sha256": "eec65df828f97a3e51827cb6b0e9ec2c86079a333481d55ec9eb6ab2845d73c5",
|
||||
"source_rows_semantic_version": "v10",
|
||||
"source_transport_commit": "12eeae5d570004499a30f7faa42c0da93c1578cc",
|
||||
"status": "core_and_runner_api_regressions_passed_not_source_runtime_or_strategy_acceptance",
|
||||
"trading_allowed": false,
|
||||
"two_framework_financial_reconciliation_completed": false,
|
||||
"unknown_delivery_date_not_defaulted_to_ex_date": true,
|
||||
"unknown_distribution_quantities_not_defaulted_to_zero": true
|
||||
},
|
||||
"research_key": "fidc-registered-share-delivery-validation-20261005-ce46c13-ffc6e1f",
|
||||
"schema_table": "strategy_research_artifact",
|
||||
"schema_version": "fidc.linux-registered-share-delivery-validation/v1",
|
||||
"status": "core_and_runner_api_regressions_passed_not_source_runtime_or_strategy_acceptance",
|
||||
"storage": "postgres"
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"all_attempt_log_references": [
|
||||
{
|
||||
"bytes": 3625,
|
||||
"compiler_failure": false,
|
||||
"complete_success": false,
|
||||
"failed": 2,
|
||||
"ignored": 0,
|
||||
"passed": 20,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-0bbeed4/initial-targeted-linux-tests.log",
|
||||
"sha256": "9684e4e1c10aabe426c15cd408bb61b198ada8eed1258231e4b81fc30716ac49"
|
||||
},
|
||||
{
|
||||
"bytes": 82467,
|
||||
"compiler_failure": false,
|
||||
"complete_success": false,
|
||||
"failed": 1,
|
||||
"ignored": 7,
|
||||
"passed": 798,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-0caac44/full-linux-tests-attempt1.log",
|
||||
"sha256": "9535151bd7a29a5f56265ef35dbff89b3e48729688cb34c32aa8a8fc9347e41c"
|
||||
},
|
||||
{
|
||||
"bytes": 95343,
|
||||
"compiler_failure": false,
|
||||
"complete_success": false,
|
||||
"failed": 4,
|
||||
"ignored": 9,
|
||||
"passed": 923,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-4eb4c17/full-linux-tests-attempt2.log",
|
||||
"sha256": "db18ad5e876ce482dc1f76ac244e170a6095c2ead8a991af5dce45e72af8e9ac"
|
||||
},
|
||||
{
|
||||
"bytes": 98381,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"passed": 967,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-05315f0/full-linux-tests-attempt3.log",
|
||||
"sha256": "66879f838ec21001cb3e6c284f497cfb6a80c2d3a724ee9cb04e09476afc101b"
|
||||
},
|
||||
{
|
||||
"bytes": 98381,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"passed": 967,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-a267052/final-full-linux-tests.log",
|
||||
"sha256": "b565d8b983fb87881974da37ad5bd0721db3f51acda3af0cbe1b62406f18371c"
|
||||
},
|
||||
{
|
||||
"bytes": 98381,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"passed": 967,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-7d7cce8/final-full-linux-tests.log",
|
||||
"sha256": "b8f7a20a93335801950e40ce3a7124ae039e84fbb1db7dd821c314d2b05b4447"
|
||||
},
|
||||
{
|
||||
"bytes": 98572,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"passed": 969,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-ce46c13/final-full-linux-tests.log",
|
||||
"sha256": "cfd1cb81fde9f34991a54a54e2a734844bd8fab52292e043856b08d30090d19a"
|
||||
},
|
||||
{
|
||||
"bytes": 10543,
|
||||
"compiler_failure": true,
|
||||
"complete_success": false,
|
||||
"failed": 0,
|
||||
"ignored": 0,
|
||||
"passed": 0,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1fa73bb/full-linux-tests-attempt1.log",
|
||||
"sha256": "e758be915373f707a460e7d08ab4162f3b80d86b98091c555cb3351c2bb4d7b8"
|
||||
},
|
||||
{
|
||||
"bytes": 65133,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 17,
|
||||
"passed": 613,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ffc6e1f/full-linux-tests-attempt2.log",
|
||||
"sha256": "d053e4e432cbe0b198f85ceee349b241eac4cf90b8e6a75289c322c59538b2b7"
|
||||
},
|
||||
{
|
||||
"bytes": 65133,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 17,
|
||||
"passed": 613,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/final-full-linux-tests.log",
|
||||
"sha256": "6f2e8974a90d5cdbce788e2744a1276dff9fe94b79958869cfb0730c096d1a42"
|
||||
}
|
||||
],
|
||||
"contract_sha256": "b4885b28b8076c61977dbe24948d2c0eabc7ac32f6e8ad0591b2011f1b9b2c11",
|
||||
"core_test_result": {
|
||||
"bytes": 98572,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"passed": 969,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-ce46c13/final-full-linux-tests.log",
|
||||
"sha256": "cfd1cb81fde9f34991a54a54e2a734844bd8fab52292e043856b08d30090d19a"
|
||||
},
|
||||
"corporate_action_cache_schema_version": 5,
|
||||
"engine_commit": "ce46c13f37f028393e689babbe5c348d482d3a32",
|
||||
"fractional_quantity_requires_explicit_settlement_rule": true,
|
||||
"full_source_event_and_revision_scope_verified": false,
|
||||
"new_runtime_release_performed": false,
|
||||
"numeric_research_budget_reopened": false,
|
||||
"observed_at": "2026-10-04T22:02:08.963623+00:00",
|
||||
"operational_verification_reference": {
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/operational-verification.json",
|
||||
"sha256": "32e634849aecbd2b634c6dcdbad68013f2c5eb603c8d586a43cc4698d8b18a4a"
|
||||
},
|
||||
"owner_uid": 1003,
|
||||
"pending_share_successor_conversion_without_carry_terms_rejected": true,
|
||||
"performance_speedup_claimed": false,
|
||||
"production_ready": false,
|
||||
"production_runtime_deployed": false,
|
||||
"production_strategy_count": 0,
|
||||
"registered_share_entitlements_separate_from_delivered_and_sellable_quantity": true,
|
||||
"resource_contract_requested": {
|
||||
"build_jobs": 4,
|
||||
"core_target_path": "/tmp/fidc-cash-receivable-build-20261005-af655de",
|
||||
"cpu_quota_percent": 400,
|
||||
"incremental": false,
|
||||
"locked": true,
|
||||
"memory_max_bytes": 17179869184,
|
||||
"memory_swap_max_bytes": 0,
|
||||
"offline": true,
|
||||
"resource_properties_after_transient_scope_exit_are_not_live_validation": true,
|
||||
"runner_target_path": "/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb",
|
||||
"rustc": "rustc 1.98.0 (88d9e12ae 2026-08-18)",
|
||||
"target_filesystem": "executable_tmpfs",
|
||||
"tasks_max": 256,
|
||||
"toolchain": "stable-x86_64-unknown-linux-gnu"
|
||||
},
|
||||
"runner_api_test_result": {
|
||||
"bytes": 65133,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 17,
|
||||
"passed": 613,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/final-full-linux-tests.log",
|
||||
"sha256": "6f2e8974a90d5cdbce788e2744a1276dff9fe94b79958869cfb0730c096d1a42"
|
||||
},
|
||||
"schema_version": "fidc.linux-registered-share-delivery-validation/v1",
|
||||
"service_commit": "ffc6e1f5b8b1d6c9b342a661c7a25ebfc3d8d4c5",
|
||||
"share_receivable_assets_included_in_equity_and_target_sizing": true,
|
||||
"simulation_rounding_fixture_not_official_settlement_evidence": true,
|
||||
"source_pair_root": "/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ce46c13",
|
||||
"source_publication_times_admitted_as_PIT": false,
|
||||
"source_rows_semantic_sha256": "eec65df828f97a3e51827cb6b0e9ec2c86079a333481d55ec9eb6ab2845d73c5",
|
||||
"source_rows_semantic_version": "v10",
|
||||
"source_transport_commit": "12eeae5d570004499a30f7faa42c0da93c1578cc",
|
||||
"status": "core_and_runner_api_regressions_passed_not_source_runtime_or_strategy_acceptance",
|
||||
"trading_allowed": false,
|
||||
"two_framework_financial_reconciliation_completed": false,
|
||||
"unknown_delivery_date_not_defaulted_to_ex_date": true,
|
||||
"unknown_distribution_quantities_not_defaulted_to_zero": true
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"contract_sha256": "6d6cb3b513abce58634f24ed2cc4d691d3c542d0944b2a3408298772806dca67",
|
||||
"error_type": "TypeError",
|
||||
"failed_before_pg_persist_call": true,
|
||||
"failure_function": "build_record",
|
||||
"missing_required_argument": "organization_id",
|
||||
"observed_at": "2026-10-04T22:03:37.554100+00:00",
|
||||
"owner_uid": 1003,
|
||||
"production_ready": false,
|
||||
"schema_version": "fidc.share-delivery-receipt-construction-failure/v1",
|
||||
"test_results_and_frozen_validation_receipt_preserved": true,
|
||||
"trading_allowed": false,
|
||||
"validation_reference": {
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/validation-receipt.json",
|
||||
"sha256": "5240dbf5f1d7382531a4c0dce5fcdb722f4d1137a41854269159b61f0236d412"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,678 @@
|
||||
Compiling fidc-core v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ffc6e1f/fidc-backtest-engine/crates/fidc-core)
|
||||
Compiling fidc-common v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ffc6e1f/fidc-backtest-service/crates/fidc-common)
|
||||
Compiling fidc-signal-client v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ffc6e1f/fidc-backtest-engine/crates/fidc-signal-client)
|
||||
Compiling fidc-backtest-runner v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ffc6e1f/fidc-backtest-service/services/backtest_runner)
|
||||
Compiling fidc-backtest-service v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ffc6e1f/fidc-backtest-service/services/api)
|
||||
Finished `test` profile [unoptimized] target(s) in 40.73s
|
||||
Running unittests src/lib.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/fidc_backtest_runner-f9173de301b7a940)
|
||||
|
||||
running 483 tests
|
||||
test backtest::columnar_factor_overlay_tests::cache_range_rejects_date_disorder_outside_the_binary_search_slice ... ok
|
||||
test backtest::columnar_factor_overlay_tests::cache_range_rejects_symbol_disorder_outside_the_requested_date ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_real_cache_value_equivalence ... ignored, requires a frozen private Boris real-data cache directory on 177
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_missing_or_stale_cache_does_not_mutate_base ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_duplicate_window_boundary ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_duplicate_order_and_count_mismatches ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_market_projection_and_invalid_cached_column_type ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_late_key_mismatch_before_any_base_mutation ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_unknown_keys_are_not_hidden_as_empty_values ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_replaced_file_invalidates_view_without_mutating_pinned_input ... ok
|
||||
test backtest::columnar_factor_overlay_tests::row_visitor_preserves_selected_offsets_and_late_validation_errors ... ok
|
||||
test backtest::compact_storage_tests::batch_bound_factor_columns_preserve_types_nulls_and_nonfinite_values ... ok
|
||||
test backtest::columnar_factor_overlay_tests::valid_cache_reuses_its_key_index_and_failed_indexes_are_not_published ... ok
|
||||
test backtest::compact_storage_tests::inline_symbols_and_factor_maps_preserve_binary_and_json_contracts ... ok
|
||||
test backtest::day_snapshot_builder::tests::a_failed_builder_cannot_publish_earlier_valid_rows ... ok
|
||||
test backtest::day_snapshot_builder::tests::factor_reservation_keeps_missing_and_overwrite_semantics ... ok
|
||||
test backtest::day_snapshot_builder::tests::late_source_validation_failure_invalidates_the_streamed_day ... ok
|
||||
test backtest::equity_lifecycle_metadata_tests::empty_quotes_require_full_interval_lifecycle_proof_not_a_latest_status ... ok
|
||||
test backtest::equity_lifecycle_metadata_tests::lifecycle_cash_days_keep_real_benchmark_without_fabricated_security_rows ... ok
|
||||
test backtest::equity_lifecycle_metadata_tests::missing_identity_and_dates_do_not_become_active_securities ... ok
|
||||
test backtest::execution_config_tests::capacity_preflight_requires_explicit_daily_assumptions_without_changing_risk_switches ... ok
|
||||
test backtest::execution_config_tests::component_execution_quotes_seed_uses_latest_intraday_quote_before_target_time ... ok
|
||||
test backtest::execution_config_tests::current_close_respects_explicitly_disabled_liquidity_limit ... ok
|
||||
test backtest::execution_config_tests::daily_current_close_preserves_the_strategy_decision_clock ... ok
|
||||
test backtest::execution_config_tests::daily_execution_price_request_serializes_date_symbol_filter_scope ... ok
|
||||
test backtest::execution_config_tests::daily_execution_projection_keeps_benchmark_narrow_and_trade_risk_complete ... ok
|
||||
test backtest::execution_config_tests::data_intraday_snapshot_time_is_strategy_spec_driven ... ok
|
||||
test backtest::execution_config_tests::dataset_signature_rejects_source_lake_without_frozen_backtest_bundle ... ok
|
||||
test backtest::day_snapshot_builder::tests::arrow_rows_build_the_same_final_snapshots_without_a_row_collection ... ok
|
||||
test backtest::execution_config_tests::dataset_signature_scopes_dynamic_model_score_source_rows ... ok
|
||||
test backtest::execution_config_tests::default_matching_type_follows_backtest_frequency ... ok
|
||||
test backtest::execution_config_tests::dataset_signature_scopes_backtest_data_bundle_source_rows ... ok
|
||||
test backtest::execution_config_tests::dynamic_model_score_execution_quotes_are_lazy_by_default ... ok
|
||||
test backtest::execution_config_tests::dynamic_model_score_rank_does_not_extract_fixed_signal_symbols ... ok
|
||||
test backtest::execution_config_tests::execution_quote_prefilter_keeps_previous_factor_rows_in_current_band ... ok
|
||||
test backtest::execution_config_tests::execution_quote_prefilter_prefers_runtime_market_cap_slope ... ok
|
||||
test backtest::execution_config_tests::execution_quote_query_times_follow_strategy_trade_times ... ok
|
||||
test backtest::execution_config_tests::execution_quote_prefilter_keeps_candidates_beyond_legacy_limit ... ok
|
||||
test backtest::execution_config_tests::execution_top_level_fee_overrides_nested_risk_policy_cost ... ok
|
||||
test backtest::execution_config_tests::extract_score_context_does_not_extract_fixed_signal_symbols ... ok
|
||||
test backtest::execution_config_tests::execution_top_level_volume_percent_overrides_nested_risk_policy ... ok
|
||||
test backtest::execution_config_tests::factor_keys_use_compact_storage_without_changing_names ... ok
|
||||
test backtest::execution_config_tests::fixed_signal_model_score_scope_does_not_validate_candidate_limit ... ok
|
||||
test backtest::execution_config_tests::fixed_signal_symbol_scope_preserves_explicit_empty_targets ... ok
|
||||
test backtest::execution_config_tests::minute_session_bounds_extend_only_after_post_close_rule_effective_date ... ok
|
||||
test backtest::execution_config_tests::full_minute_event_stream_uses_subscriptions_instead_of_point_snapshot ... ok
|
||||
test backtest::execution_config_tests::model_score_source_rows_scope_evidence_verifies_candidate_limit ... ok
|
||||
test backtest::execution_config_tests::model_score_source_rows_scope_rejects_unbounded_rows ... ok
|
||||
test backtest::execution_config_tests::native_next_open_pool_loads_causal_quotes_without_exposing_them_to_selection ... ok
|
||||
test backtest::execution_config_tests::narrow_overlay_projection_does_not_retain_a_wide_value_buffer ... ok
|
||||
test backtest::execution_config_tests::owned_date_group_producers_drop_every_input_exactly_once_on_failure ... ok
|
||||
test backtest::execution_config_tests::owned_trade_day_preparation_matches_borrowed_rows ... ok
|
||||
test backtest::execution_config_tests::parse_matching_type_limits_global_backtest_matching_modes ... ok
|
||||
test backtest::execution_config_tests::parse_slippage_model_supports_explicit_historical_model ... ok
|
||||
test backtest::execution_config_tests::parse_slippage_model_supports_limit_price ... ok
|
||||
test backtest::execution_config_tests::platform_daily_factor_strategy_uses_lazy_execution_quotes_by_default ... ok
|
||||
test backtest::execution_config_tests::platform_execution_quote_carry_can_include_recent_candidates ... ok
|
||||
test backtest::execution_config_tests::platform_execution_quote_carry_does_not_let_candidates_evict_recent_orders ... ok
|
||||
test backtest::execution_config_tests::platform_execution_quote_carry_keeps_today_candidates_without_candidate_carry ... ok
|
||||
test backtest::execution_config_tests::platform_execution_quote_carry_prefers_recent_candidates_before_today_candidates ... ok
|
||||
test backtest::execution_config_tests::platform_execution_quote_carry_prioritizes_order_symbols ... ok
|
||||
test backtest::execution_config_tests::platform_execution_quote_default_carry_keeps_large_recent_candidate_set ... ok
|
||||
test backtest::execution_config_tests::parallel_owned_date_groups_keep_exact_rows_and_owned_factor_allocations ... ok
|
||||
test backtest::execution_config_tests::platform_execution_route_requires_runtime_contract ... ok
|
||||
test backtest::execution_config_tests::platform_strategy_config_rejects_loose_value_budget_without_panicking ... ok
|
||||
test backtest::execution_config_tests::platform_runtime_snapshot_preserves_typed_time_in_force ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_candidate_market_cap_falls_back_to_total_market_cap ... ok
|
||||
test backtest::execution_config_tests::platform_strategy_config_uses_same_intraday_time_as_broker ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_daily_selection_price_uses_close_without_limit_allow_flags ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_does_not_build_execution_quote_from_daily_fallback ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_keeps_allow_buy_when_missing_field_is_not_buy_relevant ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_keeps_daily_volume_when_snapshot_has_intraday_volume ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_keeps_external_signal_warmup_day ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_keeps_missing_risk_facts_when_runtime_filters_are_disabled ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_keeps_st_and_star_st_flags_independent ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_preserves_missing_daily_volume_contract ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_preserves_zero_ask_from_intraday_snapshot ... ok
|
||||
test backtest::execution_config_tests::raw_yuan_market_cap_normalizes_to_100m_units ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_records_all_missing_risk_facts_independent_of_policy ... ok
|
||||
test backtest::execution_config_tests::resolve_dividend_reinvestment_defaults_false_and_reads_engine_flag ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_does_not_allow_overspending_value_budget ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_enables_strict_value_budget_by_default ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_keeps_explicit_liquidity_quote_limit ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_distinguishes_timed_daily_current_close ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_keeps_explicit_same_day_buy_mark_override ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_keeps_explicit_snapshot_volume_limit ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_keeps_selection_risk_off_by_default ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_keeps_volume_limit_for_snapshot_minute_last_by_default ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_maps_daily_next_open ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_maps_minute_last_to_minute_price ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_maps_rebalance_cash_mode_and_forces_minute ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_preserves_zero_minimum_commission ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_reads_nested_risk_policy ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_rejects_delay_slippage_outside_sell_then_buy ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_rejects_vwap_as_global_matching_type ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_uses_explicit_market_rules_without_profile ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_uses_latest_trade_time_as_main_schedule ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_uses_runtime_schedule_time_for_minute_execution ... ok
|
||||
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_preserves_static_universe_contract ... ok
|
||||
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_preserves_weak_market_threshold ... ok
|
||||
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_delayed_limit_open_exit ... ok
|
||||
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_release_slot_on_exit_signal ... ok
|
||||
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_trading_stage ... ok
|
||||
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_trading_schedule ... ok
|
||||
test backtest::execution_config_tests::retired_slippage_and_invalid_coefficients_never_become_no_slippage ... ok
|
||||
test backtest::execution_config_tests::risk_policy_alias_normalization_preserves_matching_values ... ok
|
||||
test backtest::execution_config_tests::risk_policy_alias_normalization_rejects_conflicting_values ... ok
|
||||
test backtest::execution_config_tests::reused_candidate_plan_panel_matches_full_intraday_market_materialization ... ok
|
||||
test backtest::execution_config_tests::runtime_selection_intraday_quote_detection_reads_quote_fields_only ... ok
|
||||
test backtest::execution_config_tests::sorted_overlay_keeps_unchanged_zero_bits_and_counts_only_changed_rows ... ok
|
||||
test backtest::execution_config_tests::sorted_overlay_transfers_owned_factor_storage_without_changing_bits ... ok
|
||||
test backtest::execution_config_tests::sorted_source_row_insertion_preserves_existing_duplicate_order ... ok
|
||||
test backtest::execution_config_tests::sorted_sparse_overlay_merge_falls_back_for_duplicate_keys ... ok
|
||||
test backtest::execution_config_tests::sorted_sparse_overlay_merge_preserves_order_and_unmatched_rows ... ok
|
||||
test backtest::execution_config_tests::source_lake_execution_price_requests_keep_compact_schema ... ok
|
||||
test backtest::execution_config_tests::source_lake_backtest_bundle_context_requires_complete_metadata ... ok
|
||||
test backtest::execution_config_tests::source_lake_market_cap_keeps_current_decision_date_value ... ok
|
||||
test backtest::execution_config_tests::source_lake_market_cap_uses_current_decision_date_snapshot ... ok
|
||||
test backtest::execution_config_tests::source_lake_model_score_context_allows_plain_rotation_without_model_score ... ok
|
||||
test backtest::execution_config_tests::source_lake_model_score_context_accepts_complete_inferred_scope ... ok
|
||||
test backtest::execution_config_tests::source_lake_model_score_context_rejects_dynamic_mode_without_source_label_or_scope ... ok
|
||||
test backtest::execution_config_tests::source_lake_model_score_context_rejects_partial_scope ... ok
|
||||
test backtest::execution_config_tests::source_lake_model_score_context_rejects_sparse_ref_without_scope ... ok
|
||||
test backtest::execution_config_tests::source_lake_request_keeps_dynamic_model_score_as_all_market_with_score_context ... ok
|
||||
test backtest::execution_config_tests::source_lake_request_keeps_explicit_empty_symbols_empty ... ok
|
||||
test backtest::execution_config_tests::source_lake_request_scopes_fixed_signal_symbols_and_keeps_compact_schema ... ok
|
||||
test backtest::execution_config_tests::source_lake_source_rows_uses_current_row_decision_safe_prev_factor_mode ... ok
|
||||
test backtest::execution_config_tests::source_lake_request_serializes_date_symbol_filter_scope ... ok
|
||||
test backtest::execution_config_tests::source_row_adjustment_factor_decode_accepts_positive_finite_values ... ok
|
||||
test backtest::execution_config_tests::source_row_adjustment_factor_decode_allows_paused_rows_without_close ... ok
|
||||
test backtest::execution_config_tests::source_row_adjustment_factor_decode_rejects_missing_or_invalid_values ... ok
|
||||
test backtest::execution_config_tests::source_row_adjustment_factor_decode_rejects_paused_rows_with_close ... ok
|
||||
test backtest::execution_config_tests::source_row_cache_and_filter_distinguish_all_from_explicit_empty_symbols ... ok
|
||||
test backtest::execution_config_tests::source_row_adjustment_factor_decode_skips_unrequested_projection ... ok
|
||||
test backtest::execution_config_tests::source_row_daily_capacity_is_scoped_to_actual_insertion_dates ... ok
|
||||
test backtest::execution_config_tests::source_row_grouping_copies_date_keys_only_at_boundaries ... ok
|
||||
test backtest::execution_config_tests::source_row_missing_open_does_not_mark_candidate_as_one_yuan ... ok
|
||||
test backtest::execution_config_tests::source_row_model_score_context_accepts_extract_only_score_metadata ... ok
|
||||
test backtest::execution_config_tests::source_row_model_score_context_can_disable_candidate_limit_inference ... ok
|
||||
test backtest::execution_config_tests::source_row_model_score_context_infers_candidate_limit_from_selection_limit ... ok
|
||||
test backtest::execution_config_tests::source_row_daily_capacity_keeps_supplemental_inserts_in_place ... ok
|
||||
test backtest::execution_config_tests::source_row_model_score_context_merges_bundle_extract_and_scopes_cache ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_does_not_require_adjustment_factor_for_volume_only_rolling ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_includes_amount_for_stock_amount_expressions ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_keeps_select_all_for_generic_empty_factor_refs ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_keeps_source_lake_empty_factor_refs_narrow ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_keeps_source_lake_without_strategy_spec_narrow ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_uses_framework_rolling_inputs ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_splits_sparse_model_score_factor_refs ... ok
|
||||
test backtest::execution_config_tests::source_row_sort_preserves_published_order_and_stably_repairs_merges ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_uses_raw_market_columns_for_framework_rollings ... ok
|
||||
test backtest::execution_config_tests::source_rows_query_engine_is_validated_and_scopes_cache_epoch ... ok
|
||||
test backtest::execution_config_tests::source_rows_scope_evidence_preserves_unordered_date_counts ... ok
|
||||
test backtest::execution_config_tests::sparse_model_score_overlay_dates_exclude_warmup_rows ... ok
|
||||
test backtest::execution_config_tests::sparse_model_score_overlay_keeps_named_score_when_context_uses_default_score ... ok
|
||||
test backtest::execution_config_tests::sparse_model_score_overlay_merges_only_requested_extra_factors ... ok
|
||||
test backtest::execution_config_tests::sparse_model_score_overlay_query_windows_are_chunked_by_date_span ... ok
|
||||
test backtest::execution_config_tests::source_warmup_uses_required_trading_windows ... ok
|
||||
test backtest::execution_config_tests::sparse_model_score_overlay_skips_primary_score_columns ... ok
|
||||
test backtest::execution_config_tests::sparse_model_score_overlay_reuses_source_row_cache_without_http ... ok
|
||||
test backtest::execution_config_tests::stock_cost_model_for_execution_uses_explicit_fee_overrides ... ok
|
||||
test backtest::execution_config_tests::sparse_overlay_skips_source_lake_when_scoped_date_symbol_filter_is_empty ... ok
|
||||
test backtest::execution_config_tests::strategy_execution_top_level_risk_enabled_aliases_are_applied ... ok
|
||||
test backtest::execution_config_tests::strategy_risk_policy_accepts_camel_case_fields ... ok
|
||||
test backtest::execution_config_tests::supplemental_execution_key_index_only_materializes_requested_symbols ... ok
|
||||
test backtest::execution_config_tests::strategy_factory_model_rank_rotation_keeps_explicit_execution_contract ... ok
|
||||
test backtest::execution_config_tests::timed_daily_current_close_loads_the_intraday_decision_snapshot ... ok
|
||||
test backtest::execution_config_tests::top_level_execution_limits_override_nested_risk_policy_defaults ... ok
|
||||
test backtest::execution_config_tests::unique_source_row_trade_dates_preserves_sorted_and_unordered_inputs ... ok
|
||||
test backtest::execution_config_tests::unsupported_backtest_frequency_is_rejected ... ok
|
||||
test backtest::execution_config_tests::validated_source_limit_prices_allows_explicit_no_limit_stock ... ok
|
||||
test backtest::execution_config_tests::validated_source_limit_prices_allows_index_without_limits ... ok
|
||||
test backtest::execution_config_tests::validated_source_limit_prices_allows_paused_stock_without_limits ... ok
|
||||
test backtest::execution_config_tests::validated_source_limit_prices_rejects_missing_stock_limit ... ok
|
||||
test backtest::execution_config_tests::validated_source_limit_prices_trusts_source_limits ... ok
|
||||
test backtest::factor_history_calendar_tests::history_uses_actual_sessions_and_rejects_short_or_future_calendars ... ok
|
||||
test backtest::factor_history_calendar_tests::index_policy_preloads_its_reference_without_widening_the_candidate_universe ... ok
|
||||
test backtest::factor_history_calendar_tests::contextual_pattern_expands_data_scope_but_not_candidate_members ... ok
|
||||
test backtest::factor_history_calendar_tests::selection_buy_permission_carries_its_complete_pattern_window ... ok
|
||||
test backtest::fund_metadata::tests::day_preparation_distinguishes_etf_optional_risk_from_missing_prices ... ok
|
||||
test backtest::fund_metadata::tests::etf_metadata_binds_identity_dates_and_units ... ok
|
||||
test backtest::fund_metadata::tests::real_etf_fallback_fills_match_source_opens_dates_and_fee_budgets ... ignored, requires exact frontend-compiled frozen Source Lake ETF replay artifacts
|
||||
test backtest::fund_metadata::tests::real_mixed_prices_preserve_holdings_until_explicit_zero_allocation ... ignored, requires FIDC_FUND_REPLAY_PREFIX containing read-only real Source Arrow fixtures
|
||||
test backtest::fund_metadata::tests::real_source_arrow_preserves_etf_identity_and_detects_tampering ... ignored, requires FIDC_FUND_METADATA_TEST_ARROW from the read-only real Source probe
|
||||
test backtest::fund_metadata::tests::preliminary_candidate_panel_uses_verified_etf_rules_without_inventing_limits ... ok
|
||||
test backtest::manual_data_scope::tests::explicit_data_scope_can_expand_without_mutating_the_strategy_or_empty_pool ... ok
|
||||
test backtest::manual_data_scope::tests::dataset_cache_tracks_extra_market_demand_not_manual_price_or_execution_identity ... ok
|
||||
test backtest::prepared_source_row::tests::all_supported_numeric_types_keep_the_existing_conversion_and_null_policy ... ok
|
||||
test backtest::prepared_source_row::tests::arbitrary_slices_and_supplemental_rows_keep_symbol_order ... ok
|
||||
test backtest::prepared_source_row::tests::finite_values_keep_exact_float_bits_and_metadata ... ok
|
||||
test backtest::prepared_source_row::tests::actual_arrow_day_uses_columnar_rows_and_matches_owned_final_snapshots ... ok
|
||||
test backtest::prepared_source_row::tests::invalid_json_is_not_hidden_by_a_consumer_that_needs_no_factors ... ok
|
||||
test backtest::prepared_source_row::tests::nonfinite_price_policy_does_not_mutate_original_arrow_values ... ok
|
||||
test backtest::prepared_source_row::tests::overlay_cannot_relax_base_adjustment_validation ... ok
|
||||
test backtest::prepared_source_row::tests::json_typed_duplicates_and_projected_overlay_preserve_overwrite_order ... ok
|
||||
test backtest::risk_free_rate_cache_identity_includes_bundle_and_full_calendar ... ok
|
||||
test backtest::prepared_source_row::tests::schema_error_precedence_and_null_keys_are_preserved ... ok
|
||||
test backtest::risk_free_rate_cache_round_trips_validated_contract ... ok
|
||||
test backtest::saved_risk_cache::tests::saved_reference_cancelled_reader_does_not_leave_a_claim ... ok
|
||||
test backtest::saved_risk_cache::tests::saved_reference_identity_includes_endpoint_hash_and_calendar ... ok
|
||||
test backtest::saved_risk_reference_requires_original_content_not_only_server_claimed_hash ... ok
|
||||
test backtest::source_query_windows::tests::actual_cached_boundaries_are_kept_and_gaps_are_covered ... ok
|
||||
test backtest::source_query_windows::tests::all_days_remain_once_and_scope_count_is_bounded ... ok
|
||||
test backtest::source_query_windows::tests::discovery_never_crosses_source_keys_schemas_or_file_types ... ok
|
||||
test backtest::source_query_windows::tests::overlap_prefers_more_covered_days_then_fewer_windows ... ok
|
||||
test backtest::source_query_windows::tests::projection_and_symbol_scope_have_distinct_boundary_keys ... ok
|
||||
test backtest::source_query_windows::tests::uncached_gaps_keep_the_existing_calendar_anchor ... ok
|
||||
test backtest::source_row_key_index::tests::indexed_date_ranges_cover_days_split_across_batches ... ok
|
||||
test backtest::saved_risk_cache::tests::saved_reference_coalesces_only_validated_original_contracts ... ok
|
||||
test backtest::source_row_key_index::tests::normalized_keys_cannot_duplicate_across_batches ... ok
|
||||
test backtest::source_row_key_index::tests::null_identity_and_declared_count_mismatch_are_rejected ... ok
|
||||
test backtest::source_row_query_purpose_tests::factor_overlay_cache_identity_cannot_be_reused_as_market_facts ... ok
|
||||
test backtest::source_row_query_purpose_tests::factor_overlay_merge_preserves_every_base_market_and_risk_fact ... ok
|
||||
test backtest::source_row_query_purpose_tests::factor_overlay_requires_an_explicit_non_market_projection ... ok
|
||||
test backtest::source_row_query_purpose_tests::only_explicit_factor_overlays_omit_redundant_risk_columns ... ok
|
||||
test backtest::source_rows_columnar::tests::cache_write_failure_does_not_replace_the_current_columnar_input ... ok
|
||||
test backtest::source_rows_columnar::tests::actual_selection_and_empty_outputs_do_not_keep_unused_factor_columns ... ok
|
||||
test backtest::source_rows_columnar::tests::columnar_normalization_matches_mixed_values_json_aliases_and_sorting ... ok
|
||||
test backtest::source_rows_columnar::tests::invalid_keys_and_adjustment_are_rejected_before_selection ... ok
|
||||
test backtest::source_rows_columnar::tests::malformed_json_and_dependency_identity_are_not_accepted ... ok
|
||||
test backtest::source_rows_columnar::tests::non_cacheable_json_factor_names_keep_the_real_record_path ... ok
|
||||
test backtest::source_rows_columnar::tests::normalized_duplicate_symbols_are_not_deduplicated ... ok
|
||||
test backtest::source_rows_input_tests::cache_cannot_include_rows_outside_its_declared_date_range ... ok
|
||||
test backtest::source_rows_input_tests::cache_write_failure_retains_the_real_window_and_empty_windows_do_not_read_old_rows ... ok
|
||||
test backtest::source_rows_input_tests::failed_materialization_does_not_turn_the_source_into_an_empty_success ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_crosses_record_batches_and_preserves_key_normalization ... ok
|
||||
test backtest::source_rows_input_tests::independent_windows_stay_columnar_until_daily_snapshot_construction ... ok
|
||||
test backtest::source_rows_input_tests::invalid_cache_keys_and_overlay_coverage_never_publish_source_input ... ok
|
||||
test backtest::source_rows_input_tests::mapped_input_can_be_materialized_for_mutating_model_and_publication_overlays ... ok
|
||||
test backtest::source_rows_input_tests::mapped_input_preserves_empty_scopes_and_filters_only_base_rows ... ok
|
||||
test backtest::source_rows_input_tests::mixed_window_storage_and_duplicate_boundaries_keep_original_semantics ... ok
|
||||
test backtest::source_rows_input_tests::mapped_inventory_and_final_days_match_owned_rows_with_factor_overlays ... ok
|
||||
test backtest::source_rows_input_tests::unsuccessful_window_publication_cannot_read_a_previous_cache_file ... ok
|
||||
test backtest::terminal_audit_summary_tests::deferred_strategy_warning_is_not_presented_as_a_submitted_order ... ok
|
||||
test backtest::terminal_audit_summary_tests::pending_terminal_state_is_explicit_in_summary_warning ... ok
|
||||
test backtest::terminal_calendar_tests::delayed_deposit_requires_terminal_calendar_but_zero_day_does_not ... ok
|
||||
test backtest::terminal_calendar_tests::scheduled_terminal_calendar_requires_strict_order_and_range ... ok
|
||||
test backtest::tests::accounting_allocation_details_do_not_require_or_fabricate_an_order ... ok
|
||||
test backtest::tests::all_source_lake_runtime_caches_are_scoped_by_backtest_bundle ... ok
|
||||
test backtest::tests::arrow_reader_accepts_lz4_compressed_stream ... ok
|
||||
test backtest::tests::audit_instrument_cache_values_from_request ... ignored, read-only Boris cache comparison with an explicit audit request
|
||||
test backtest::tests::benchmark_borrowed_numeric_factor_keys_against_owned_strings ... ignored, manual release-mode allocation benchmark
|
||||
test backtest::source_rows_input_tests::separate_arrow_factor_input_preserves_sparse_overlay_values_and_existing_layers ... ok
|
||||
test backtest::tests::benchmark_date_symbol_filter_query_range_preserves_index_symbols ... ok
|
||||
test backtest::tests::benchmark_query_windows_split_long_source_lake_ranges ... ok
|
||||
test backtest::tests::benchmark_row_binary_cache_rejects_metadata_mismatch ... ok
|
||||
test backtest::tests::benchmark_row_binary_cache_roundtrips_rows ... ok
|
||||
test backtest::tests::benchmark_row_cache_ignores_legacy_csv_cache ... ok
|
||||
test backtest::tests::benchmark_row_cache_path_uses_binary_cache_file ... ok
|
||||
test backtest::tests::benchmark_row_cache_path_scopes_date_symbol_filter ... ok
|
||||
test backtest::tests::benchmark_row_candidate_scan_removes_stale_schema_cache ... ok
|
||||
test backtest::tests::benchmark_row_covering_cache_reuses_superset_symbols_and_dates ... ok
|
||||
test backtest::tests::benchmark_row_partial_cache_rejects_missing_symbol_segment ... ok
|
||||
test backtest::tests::benchmark_row_to_source_row_preserves_false_paused_state ... ok
|
||||
test backtest::tests::benchmark_row_partial_cache_reports_missing_ranges ... ok
|
||||
test backtest::tests::benchmark_row_window_cache_scope_ignores_out_of_window_filter_dates ... ok
|
||||
test backtest::tests::benchmark_rows_dedup_after_window_merge ... ok
|
||||
test backtest::tests::benchmark_source_row_arrow_bounded_materialization ... ignored, requires FIDC_BT_SOURCE_ROW_VIEW_PROBE_PATH and explicit release-mode invocation
|
||||
test backtest::tests::benchmark_source_row_arrow_view_cache ... ignored, requires FIDC_BT_SOURCE_ROW_VIEW_PROBE_PATH and explicit release-mode invocation
|
||||
test backtest::tests::bootstrap_missing_quote_repair_keys_keeps_all_actual_order_gaps_without_fills ... ok
|
||||
test backtest::tests::bootstrap_missing_quote_repair_keys_keeps_all_after_fills_exist ... ok
|
||||
test backtest::tests::benchmark_rows_empty_date_symbol_filter_does_not_reuse_existing_cache ... ok
|
||||
test backtest::tests::build_trade_detail_rows_expands_split_fills_by_order_id ... ok
|
||||
test backtest::tests::build_trade_detail_rows_omits_canceled_orders_without_fills ... ok
|
||||
test backtest::tests::build_trade_detail_rows_matches_fill_when_order_reason_changes ... ok
|
||||
test backtest::tests::build_trade_detail_rows_preserves_decision_and_execution_dates ... ok
|
||||
test backtest::tests::carry_forward_target_snapshot_keeps_transition_day_then_replaces_positions ... ok
|
||||
test backtest::tests::cn_index_symbols_are_not_stock_execution_quote_repair_symbols ... ok
|
||||
test backtest::tests::collect_missing_execution_quote_keys_uses_canceled_stock_orders_only ... ok
|
||||
test backtest::tests::compact_progress_retains_manual_counts_and_rejects_a_missing_source ... ok
|
||||
test backtest::tests::compacted_carry_forward_filter_keeps_window_semantics ... ok
|
||||
test backtest::tests::corporate_action_arrow_preserves_the_independent_delivery_date_and_unknown_quantities ... ok
|
||||
test backtest::tests::corporate_action_binary_cache_roundtrips_rows ... ok
|
||||
test backtest::tests::corporate_action_cache_ignores_legacy_csv_cache ... ok
|
||||
test backtest::tests::corporate_action_cache_path_uses_source_lake_provider_epoch ... ok
|
||||
test backtest::tests::bundle_validation_observes_dependency_semantic_change_and_revocation_without_ttl ... ok
|
||||
test backtest::tests::corporate_action_covering_cache_reuses_superset_scope ... ok
|
||||
test backtest::tests::daily_execution_price_arrow_decode_preserves_unpriced_paused_risk_rows ... ok
|
||||
test backtest::tests::daily_execution_price_arrow_requires_execution_contract_fields ... ok
|
||||
test backtest::tests::daily_execution_scope_membership_is_normalized_once_and_isolated_by_date ... ok
|
||||
test backtest::tests::dataset_signature_scopes_fixed_signal_source_rows ... ok
|
||||
test backtest::tests::dataset_signature_scopes_fixed_signal_date_symbol_filter_with_model_score ... ok
|
||||
test backtest::tests::date_symbol_filter_query_range_carry_forward_keeps_history_prefix ... ok
|
||||
test backtest::tests::date_symbol_filter_query_range_exact_keeps_only_window_dates ... ok
|
||||
test backtest::tests::date_symbol_filter_query_range_preserves_empty_request ... ok
|
||||
test backtest::tests::dataset_signature_uses_data_requirements_not_strategy_identity_or_thresholds ... ok
|
||||
test backtest::tests::default_cache_epochs_ignore_local_manifest_directory ... ok
|
||||
test backtest::tests::dense_execution_quote_plan_uses_requested_trade_dates_only ... ok
|
||||
test backtest::tests::dynamic_model_score_execution_only_queries_next_date_gaps ... ok
|
||||
test backtest::tests::dynamic_model_score_execution_skips_rows_present_on_next_full_calendar_date ... ok
|
||||
test backtest::tests::dynamic_model_score_execution_symbols_include_candidate_universe_for_lagged_execution ... ok
|
||||
test backtest::tests::dynamic_symbol_expression_does_not_enable_fixed_source_scope ... ok
|
||||
test backtest::tests::empty_date_symbol_filter_is_empty_request_not_all_market ... ok
|
||||
test backtest::tests::equity_artifact_keeps_external_deposit_out_of_return ... ok
|
||||
test backtest::tests::equity_projection_orders_facts_and_includes_the_initial_nav_peak ... ok
|
||||
test backtest::tests::execution_arrow_does_not_turn_nonfinite_numbers_into_optional_missing_values ... ok
|
||||
test backtest::tests::execution_arrow_requires_the_strict_source_contract_even_when_empty ... ok
|
||||
test backtest::tests::execution_price_date_symbol_filter_keeps_benchmark_and_prunes_other_targets ... ok
|
||||
test backtest::tests::execution_price_only_source_row_marker_round_trips ... ok
|
||||
test backtest::tests::execution_quantities_are_exact_integers_not_rounded_floats ... ok
|
||||
test backtest::tests::exact_source_row_windows_restore_in_parallel_and_keep_order ... ok
|
||||
test backtest::tests::execution_quote_day_binary_cache_invalidates_duplicate_date_symbol ... ok
|
||||
test backtest::tests::execution_quote_day_binary_cache_rejects_metadata_mismatch ... ok
|
||||
test backtest::tests::execution_quote_day_cache_ignores_legacy_csv_cache ... ok
|
||||
test backtest::tests::execution_quote_day_cache_does_not_persist_empty_negative_result ... ok
|
||||
test backtest::tests::execution_quote_day_cache_loads_empty_without_provider_url ... ok
|
||||
test backtest::tests::execution_quote_day_cache_path_includes_minute_lookback_and_price_rule ... ok
|
||||
test backtest::tests::execution_quote_day_cache_path_uses_quote_epoch ... ok
|
||||
test backtest::tests::execution_quote_day_cache_path_uses_source_lake_provider_tag ... ok
|
||||
test backtest::tests::execution_quote_day_cache_tracks_returned_symbols_only ... ok
|
||||
test backtest::tests::execution_quote_prefetch_seed_tracks_negative_symbols ... ok
|
||||
test backtest::tests::execution_quote_query_tasks_keep_target_time_symbol_scopes_narrow ... ok
|
||||
test backtest::tests::execution_quote_query_tasks_merge_identical_symbol_scopes_for_same_day ... ok
|
||||
test backtest::tests::execution_quote_rows_duplicate_date_symbol_is_rejected ... ok
|
||||
test backtest::tests::execution_quote_rows_normalize_base_stock_code_symbols ... ok
|
||||
test backtest::tests::execution_quote_loader_reads_a_bounded_minute_window_instead_of_a_point_snapshot ... ok
|
||||
test backtest::tests::explicit_cli_source_cache_epoch_is_stable_and_provider_scoped ... ok
|
||||
test backtest::tests::external_daily_execution_symbols_without_signals_only_queries_benchmark ... ok
|
||||
test backtest::tests::external_daily_execution_symbols_skip_signals_already_in_source_rows ... ok
|
||||
test backtest::tests::factor_values_arrow_decode_validates_metadata_and_merges_aliases ... ok
|
||||
test backtest::tests::fidc_runner_rejects_ficlaw_data_source_rows_table ... ok
|
||||
test backtest::tests::fixed_minute_subscriptions_expand_static_daily_data_scope ... ok
|
||||
test backtest::tests::factor_values_arrow_decode_rejects_future_data_and_semantic_drift ... ok
|
||||
test backtest::tests::fixed_signal_date_symbol_filter_is_disabled_when_static_symbols_are_mixed_in ... ok
|
||||
test backtest::tests::fixed_signal_date_symbol_filter_survives_model_score_rank_metadata ... ok
|
||||
test backtest::tests::fixed_signal_date_symbol_filter_seeds_static_symbols_at_query_start ... ok
|
||||
test backtest::tests::fixed_signal_next_open_exit_scope_keeps_all_prior_unsold_symbols ... ok
|
||||
test backtest::tests::fixed_signal_source_row_date_symbol_filter_extracts_dated_weights_only ... ok
|
||||
test backtest::tests::fixed_signal_source_row_symbols_extracts_runtime_target_weights_expr ... ok
|
||||
test backtest::tests::fixed_signal_source_row_date_symbol_filter_extracts_target_holding_rows ... ok
|
||||
test backtest::tests::fixed_signal_source_row_symbols_extracts_single_quoted_runtime_target_weights_expr ... ok
|
||||
test backtest::tests::fixed_signal_source_row_symbols_extracts_strategy_metadata_targets ... ok
|
||||
test backtest::tests::fixed_signal_source_row_symbols_extracts_target_weight_codes ... ok
|
||||
test backtest::tests::fixed_symbol_order_scope_requires_rotation_disabled ... ok
|
||||
test backtest::tests::fixed_symbol_orders_seed_exact_intraday_snapshot_keys ... ok
|
||||
test backtest::tests::frozen_bundle_identity_http_failure_is_not_a_mutable_epoch_fallback ... ok
|
||||
test backtest::tests::full_minute_prefetch_days_respect_total_bar_budget ... ok
|
||||
test backtest::tests::futures_source_lake_catalog_requires_all_truth_datasets ... ok
|
||||
test backtest::tests::futures_only_runtime_contract_keeps_stock_scope_empty_and_allocates_cash ... ok
|
||||
test backtest::tests::instrument_metadata_binary_cache_rejects_metadata_mismatch ... ok
|
||||
test backtest::tests::instrument_metadata_binary_cache_roundtrips_rows ... ok
|
||||
test backtest::tests::instrument_metadata_cache_merges_missing_symbols_incrementally ... ok
|
||||
test backtest::tests::instrument_metadata_cache_ignores_legacy_csv_cache ... ok
|
||||
test backtest::tests::instrument_metadata_cache_path_is_scoped_by_historical_window ... ok
|
||||
test backtest::tests::instrument_metadata_cache_path_uses_binary_format ... ok
|
||||
test backtest::tests::instrument_metadata_request_serializes_historical_source_window_when_present ... ok
|
||||
test backtest::tests::intraday_snapshot_binary_cache_rejects_metadata_mismatch ... ok
|
||||
test backtest::tests::intraday_snapshot_binary_cache_roundtrips_rows ... ok
|
||||
test backtest::tests::intraday_snapshot_cache_ignores_legacy_csv_cache ... ok
|
||||
test backtest::tests::intraday_snapshot_cache_path_uses_source_lake_provider_tag ... ok
|
||||
test backtest::tests::intraday_snapshot_seeds_execution_quotes_and_negative_gaps ... ok
|
||||
test backtest::tests::kcb_board_and_quantity_use_source_classification ... ok
|
||||
test backtest::tests::layered_execution_quote_loader_cache_preserves_shared_seed_and_isolates_overlay ... ok
|
||||
test backtest::tests::lazy_execution_quote_window_backfill_keeps_future_missing_symbols ... ok
|
||||
test backtest::tests::lazy_execution_quote_window_dates_use_trading_calendar_only ... ok
|
||||
test backtest::tests::load_strategy_spec_rejects_removed_compatibility_profile ... ok
|
||||
test backtest::tests::load_strategy_spec_rejects_removed_engine_profile_name ... ok
|
||||
test backtest::tests::malformed_execution_observations_are_errors_not_empty_successes ... ok
|
||||
test backtest::tests::malformed_versioned_factor_ref_fails_closed ... ok
|
||||
test backtest::tests::managed_source_rows_route_by_exact_table ... ok
|
||||
test backtest::tests::minute_bar_arrow_decode_accepts_bounded_multi_date_stream ... ok
|
||||
test backtest::tests::load_strategy_spec_accepts_snake_case_bundle_field ... ok
|
||||
test backtest::tests::minute_bar_arrow_decode_preserves_timestamp_symbol_order ... ok
|
||||
test backtest::tests::minute_bar_arrow_decode_rejects_retired_string_time_schema ... ok
|
||||
test backtest::tests::minute_bar_arrow_decode_rejects_duplicate_or_unordered_rows ... ok
|
||||
test backtest::tests::mixed_candidate_book_binds_complete_dates_raw_sources_and_resolved_order ... ok
|
||||
test backtest::tests::mixed_stock_futures_contract_requires_explicit_futures_cash ... ok
|
||||
test backtest::tests::normalize_symbol_maps_bse_exchange_to_beijing_board ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_matches_row_decoder_for_wide_sparse_null_and_signed_zero_values ... ok
|
||||
test backtest::tests::minute_bars_retries_transient_http_failure_and_preserves_caller_header ... ok
|
||||
test backtest::tests::partial_source_row_window_cache_crops_covering_segments_in_arrow ... ok
|
||||
test backtest::tests::partial_source_row_window_cache_ignores_all_market_cache_for_symbol_scope ... ok
|
||||
test backtest::tests::partial_source_row_window_cache_ignores_wide_projection_segment ... ok
|
||||
test backtest::tests::partial_source_row_window_cache_reports_only_missing_gaps ... ok
|
||||
test backtest::tests::progress_callback_is_forwarded_to_blocking_engine_thread ... ok
|
||||
test backtest::tests::published_factor_binding_normalizes_ref_and_builds_arrow_request ... ok
|
||||
test backtest::tests::query_scope_batch_preserves_authoritative_window_identities ... ok
|
||||
test backtest::tests::platform_quote_plan_uses_current_decision_date_source_rows ... ok
|
||||
test backtest::tests::query_scope_identity_reuses_physical_window_across_distinct_bundles ... ok
|
||||
test backtest::tests::real_source_quote_json_and_arrow_match_strict_reader ... ignored, requires a bounded real Source JSON/Arrow probe directory
|
||||
test backtest::tests::repeated_query_views_share_only_identical_execution_observations ... ok
|
||||
test backtest::tests::partial_source_row_window_cache_reuses_exact_projection_with_null_dynamic_columns ... ok
|
||||
test backtest::tests::query_scope_http_carries_rolling_dependencies_and_rejects_bad_contracts ... ok
|
||||
test backtest::tests::select_instrument_metadata_rejects_missing_symbols ... ok
|
||||
test backtest::tests::shared_base_source_projection_keeps_raw_adjustment_contract ... ok
|
||||
test backtest::tests::shared_base_source_rows_cache_is_enabled_by_default ... ok
|
||||
test backtest::tests::reusable_source_row_window_cache_does_not_reuse_covering_cache ... ok
|
||||
test backtest::tests::shared_base_source_rows_cache_is_only_for_unscoped_raw_rows ... ok
|
||||
test backtest::tests::sorted_source_row_validator_rejects_unordered_or_duplicate_keys ... ok
|
||||
test backtest::tests::source_cache_epoch_default_is_independent_from_quote_plan_epoch ... ok
|
||||
test backtest::tests::source_dependency_contract_invalidates_all_authoritative_cache_scopes ... ok
|
||||
test backtest::tests::segmented_source_row_cache_reads_only_uncovered_overlap_ranges ... ok
|
||||
test backtest::tests::short_range_source_query_window_is_explicit_and_keeps_long_ranges_bounded ... ok
|
||||
test backtest::tests::source_lake_contract_epoch_ignores_runtime_build_but_tracks_catalog ... ok
|
||||
test backtest::tests::source_lake_health_contract_epoch_ignores_runtime_build ... ok
|
||||
test backtest::tests::source_lake_arrow_request_retries_bad_gateway ... ok
|
||||
test backtest::tests::source_lake_busy_retry_honors_retryable_capacity_response ... ok
|
||||
test backtest::tests::source_lake_source_query_retries_are_bounded_and_transient_only ... ok
|
||||
test backtest::tests::source_lake_source_query_timeout_accepts_highmem_alias ... ok
|
||||
test backtest::tests::source_query_concurrency_defaults_and_clamps ... ok
|
||||
test backtest::tests::source_lake_run_waits_for_manifest_publication_and_holds_shared_lease ... ok
|
||||
test backtest::tests::source_row_arrow_cache_rejects_and_does_not_write_empty_rows ... ok
|
||||
test backtest::tests::source_row_arrow_cache_rejects_duplicate_date_symbol ... ok
|
||||
test backtest::tests::source_row_arrow_decode_keeps_compact_base_rows_without_extra_json ... ok
|
||||
test backtest::tests::source_row_arrow_decode_preserves_dynamic_projection_columns ... ok
|
||||
test backtest::tests::source_row_arrow_extra_factor_helpers_preserve_projected_columns ... ok
|
||||
test backtest::tests::source_lake_uses_wider_windows_for_narrow_queries ... ok
|
||||
test backtest::tests::source_row_arrow_dependency_contract_rejects_invalid_metadata ... ok
|
||||
test backtest::tests::source_row_cache_candidate_scan_removes_previous_schema_files ... ok
|
||||
test backtest::tests::source_row_cache_directory_index_tracks_files_published_by_other_workers ... ok
|
||||
test backtest::tests::source_row_arrow_view_keeps_mmap_alive_until_materialization ... ok
|
||||
test backtest::tests::source_row_arrow_view_cache_reuses_identity_and_invalidates_replacement ... ok
|
||||
test backtest::tests::source_row_cache_file_range_parses_source_cache_dates ... ok
|
||||
test backtest::tests::source_row_cache_path_hashes_long_projection_columns ... ok
|
||||
test backtest::tests::source_row_cache_path_normalizes_projection_column_order ... ok
|
||||
test backtest::tests::source_row_cache_path_scopes_explicit_source_symbols ... ok
|
||||
test backtest::tests::source_row_cache_projection_check_allows_source_lake_fixed_aliases ... ok
|
||||
test backtest::tests::source_row_cache_projection_check_requires_extra_factor_keys ... ok
|
||||
test backtest::tests::source_row_cache_rejects_legacy_raw_bincode_without_magic_header ... ok
|
||||
test backtest::tests::source_row_cache_rejects_metadata_mismatch ... ok
|
||||
test backtest::tests::source_row_cache_segment_selection_prefers_small_complete_windows ... ok
|
||||
test backtest::tests::source_row_cache_segment_selection_rejects_incomplete_windows ... ok
|
||||
test backtest::tests::source_row_cache_writes_arrow_file ... ok
|
||||
test backtest::tests::source_row_date_windows_keep_interior_buckets_stable_across_ranges ... ok
|
||||
test backtest::tests::source_row_date_windows_split_long_ranges ... ok
|
||||
test backtest::tests::source_row_date_windows_stay_within_requested_range ... ok
|
||||
test backtest::tests::source_row_primary_keys_are_inline_without_growing_the_row_header ... ok
|
||||
test backtest::tests::source_row_request_cache_epoch_scopes_runtime_query_context ... ok
|
||||
test backtest::tests::source_row_segment_boundary_validator_preserves_fail_closed_integrity ... ok
|
||||
test backtest::tests::source_row_symbol_normalization_preserves_canonical_and_maps_board ... ok
|
||||
test backtest::tests::source_row_symbol_scope_epoch_changes_with_fixed_signal_symbols ... ok
|
||||
test backtest::tests::source_row_window_cache_scope_ignores_out_of_window_filter_dates ... ok
|
||||
test backtest::tests::source_rows_duplicate_date_symbol_is_rejected ... ok
|
||||
test backtest::tests::source_rows_request_keeps_rolling_enabled_for_rolling_projection_columns ... ok
|
||||
test backtest::tests::source_rows_unique_date_symbol_passes_without_reordering ... ok
|
||||
test backtest::tests::source_semantic_change_invalidates_bundle_and_window_cache ... ok
|
||||
test backtest::tests::static_universe_include_is_an_explicit_source_row_scope ... ok
|
||||
test backtest::tests::stock_pool_screen_snapshot_uses_exact_selection_dates_and_full_symbol_history ... ok
|
||||
test backtest::tests::strategy_bundle_path_loads_nested_spec_and_signal_scope ... ok
|
||||
test backtest::tests::strategy_factor_overlay_only_queries_keys_and_missing_factor_fields ... ok
|
||||
test backtest::tests::strategy_factor_overlay_rejects_duplicate_and_unordered_keys ... ok
|
||||
test backtest::tests::strategy_factor_overlay_requires_exact_base_key_coverage ... ok
|
||||
test backtest::tests::strategy_factory_source_lake_provider_name_is_exact ... ok
|
||||
test backtest::tests::source_row_cache_lease_serializes_concurrent_misses ... ok
|
||||
test backtest::tests::source_row_cache_path_separates_projection_columns ... ok
|
||||
test backtest::tests::strategy_factory_source_lake_requires_url_and_matching_provider ... ok
|
||||
test backtest::tests::strategy_factory_source_lake_uses_arrow_sized_query_windows_by_default ... ok
|
||||
test backtest::tests::trade_display_metadata_cache_rejects_rows_outside_exact_request ... ok
|
||||
test backtest::tests::trade_display_metadata_cache_is_bundle_scoped_and_covers_missing_rows ... ok
|
||||
test backtest::tests::typed_equity_result_emits_service_schema ... ok
|
||||
test backtest::tests::trade_display_metadata_disk_cache_roundtrips_frozen_bundle_rows ... ok
|
||||
test backtest::tests::typed_order_result_keeps_zero_fill_orders ... ok
|
||||
test backtest::tests::typed_trade_result_preserves_sub_cent_execution_price ... ok
|
||||
test backtest::tests::validated_manifest_scope_reuses_cache_across_distinct_bundle_ids ... ok
|
||||
test backtest::tests::versioned_factor_binding_rejects_research_scope ... ok
|
||||
test backtest::tests::versioned_factor_binding_requires_declared_semantics ... ok
|
||||
test backtest::tests::versioned_factor_ref_requires_a_binding ... ok
|
||||
test backtest::typed_exit_dependency_tests::ordinary_strategy_partial_exit_also_contributes_its_window ... ok
|
||||
test backtest::typed_exit_dependency_tests::typed_stock_pool_exits_contribute_the_full_rolling_window ... ok
|
||||
test backtest::tests::timed_quote_dataset_signature_includes_daily_selection_plan ... ok
|
||||
test compute_pool_tests::concurrent_embedded_runs_do_not_change_each_others_pool_budget ... ok
|
||||
test compute_pool_tests::current_thread_async_runtime_keeps_the_compute_context ... ok
|
||||
test backtest::tests::unused_daily_market_cap_prefilter_does_not_split_identical_input_cache ... ok
|
||||
test cache_directory_watch::tests::notifications_cover_publication_rename_delete_and_ignore_unrelated_files ... ok
|
||||
test decision_projection::tests::projection_metadata_alias_empty_fields_and_credential_paths_fail_closed ... ok
|
||||
test decision_projection::tests::projection_native_request_is_complete_and_never_truncated_to_top_n ... ok
|
||||
test compute_pool_tests::embedded_runner_uses_its_declared_budget_and_preserves_results ... ok
|
||||
test manual_ledger::corporate_tests::actual_core_corporate_projection_is_preserved_in_the_shared_trade_row ... ok
|
||||
test result_store::tests::canonical_first_divergence_reports_json_pointer ... ok
|
||||
test result_store::tests::canonical_manifest_is_stable_and_transport_independent ... ok
|
||||
test decision_projection::tests::projection_response_is_bound_to_source_model_clock_and_exact_population ... ok
|
||||
test manual_ledger::corporate_tests::corporate_adjustments_reject_forged_money_dates_and_reference_chains ... ok
|
||||
test result_store::tests::lazy_registration_defers_payload_hashing_to_section_read ... ok
|
||||
test result_store::tests::fact_manifest_publish_is_idempotent_under_concurrency ... ok
|
||||
test result_store::tests::process_event_store_is_content_addressed_and_round_trips ... ok
|
||||
test result_store::tests::run_store_identity_keeps_different_data_epochs_separate ... ok
|
||||
test result_store::tests::manual_source_and_applications_are_both_hashed_and_persisted ... ok
|
||||
test result_store::tests::run_store_is_content_addressed_and_round_trips ... ok
|
||||
test result_store::tests::run_store_reads_only_date_overlapping_blocks ... ok
|
||||
test result_store::tests::run_store_rejects_corrupt_manifest ... ok
|
||||
test thread_cpu::tests::thread_cpu_clock_keeps_unavailable_or_cross_thread_samples_unknown ... ok
|
||||
test thread_cpu::tests::sleeping_time_is_not_reported_as_cpu_execution ... ok
|
||||
test backtest::source_rows_columnar::tests::columnar_cache_keeps_original_output_batch_boundaries ... ok
|
||||
|
||||
test result: ok. 474 passed; 0 failed; 9 ignored; 0 measured; 0 filtered out; finished in 4.12s
|
||||
|
||||
Running unittests src/main.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/fidc_backtest_runner-95d90db6c05a9877)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/allocation_profile.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/allocation_profile-33a6f9f5570d3138)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running unittests examples/export_manual_corporate_evidence.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/examples/export_manual_corporate_evidence-877623d86b4ef06d)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running unittests src/main.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/fidc_backtest_service-11e588cf5d2c9d39)
|
||||
|
||||
running 142 tests
|
||||
test daily_patterns::tests::arrow_rejects_truncation_and_forged_lengths_before_decoding ... ok
|
||||
test daily_patterns::tests::column_transport_is_lossless_and_rejects_ambiguous_rows ... ok
|
||||
test event_stream::tests::effective_initial_cash_is_projected_without_guessing_or_losing_source_precision ... ok
|
||||
test daily_patterns::tests::numeric_mode_requires_the_explicit_read_only_context ... ok
|
||||
test daily_patterns::tests::daily_patterns_read_only_transport_binds_exact_input ... ok
|
||||
test event_stream::tests::ordered_batch_is_idempotent_and_changed_facts_do_not_replace_history ... ignored, requires local FIDC_EVENT_TEST_POSTGRES_DSN on the private fidc-auth-pg cluster
|
||||
test event_stream::tests::memory_tail_and_durable_prefix_replay_in_publish_order ... ok
|
||||
test daily_patterns::tests::arrow_uses_same_kernel_and_exact_byte_proof_with_partial_scope ... ok
|
||||
test event_stream::tests::live_sequence_recent_replay_and_concurrent_runs_remain_isolated ... ok
|
||||
test execution_ledger::tests::dated_fact_store_rejects_reverse_order_before_projection ... ok
|
||||
test execution_ledger::tests::actual_http_routes_return_both_sources_and_precise_manual_values ... ok
|
||||
test execution_ledger::tests::serve_browser_fixture ... ignored, starts a local GET-only API for browser acceptance; set FIDC_LEDGER_UI_QA_PORT explicitly
|
||||
test execution_ledger::tests::serve_stream_browser_fixture ... ignored, starts isolated real WebSocket/GET handlers; FIDC_LEDGER_STREAM_QA_PORT required
|
||||
test execution_ledger::tests::source_precision_ids_and_later_status_are_not_rewritten ... ok
|
||||
test execution_ledger::tests::streamed_manual_facts_equal_final_ledger_without_publishing_a_future_terminal ... ok
|
||||
test execution_ledger::tests::typed_accounting_rows_keep_origin_without_order_identity ... ok
|
||||
test execution_ledger::tests::performance_counts_both_origins_and_sums_authoritative_fees_exactly ... ok
|
||||
test factor_events::tests::market_history_binds_kernel_version_and_preserves_unknown_streak ... ok
|
||||
test factor_events::tests::no_account_or_execution_payload_is_accepted ... ok
|
||||
test factor_events::tests::rank_history_binds_exact_scope_and_input_hash ... ok
|
||||
test factor_events::tests::transport_preserves_input_identity_and_rejects_future_values ... ok
|
||||
test idempotency::tests::canonical_json_order_does_not_change_request_identity ... ok
|
||||
test idempotency::tests::identity_binds_owner_and_rejects_changed_request ... ok
|
||||
test idempotency::tests::malformed_keys_and_unbound_records_are_rejected ... ok
|
||||
test performance::tests::accounting_allocations_are_not_unidentified_exchange_fills ... ok
|
||||
test performance::tests::builds_daily_weight_diagnostics_from_persisted_holdings ... ok
|
||||
test performance::tests::counts_open_underwater_duration_from_the_latest_peak ... ok
|
||||
test performance::tests::does_not_fake_risk_ratios_for_one_period ... ok
|
||||
test performance::tests::emits_true_nav_and_multiple_recovery_episodes ... ok
|
||||
test performance::tests::explicit_frozen_initial_capital_survives_a_first_day_deposit ... ok
|
||||
test performance::tests::first_period_loss_has_an_initial_peak_episode_without_a_fake_date ... ok
|
||||
test performance::tests::missing_benchmark_does_not_compact_dates_or_invent_a_drawdown_path ... ok
|
||||
test performance::tests::missing_order_ids_and_fees_remain_unknown_not_zero_or_report_counts ... ok
|
||||
test performance::tests::order_metrics_count_identities_separately_from_reports_and_partial_fills ... ok
|
||||
test performance::tests::preserves_a_first_day_strategy_return_and_initial_capital ... ok
|
||||
test performance::tests::preserves_the_canonical_pre_period_benchmark_baseline ... ok
|
||||
test performance::tests::projects_all_canonical_risk_metrics_and_contract_metadata ... ok
|
||||
test performance::tests::separates_longest_underwater_episode_from_deepest_drawdown ... ok
|
||||
test performance::tests::zero_nav_is_a_full_loss_not_missing_data ... ok
|
||||
test performance::tests::zero_orders_are_known_but_conflicting_order_identity_is_not_a_count ... ok
|
||||
test result_analysis_tests::batched_history_bindings_preserve_validation_and_survive_cache_eviction ... ignored, requires isolated FIDC_BACKTEST_TEST_POSTGRES_DSN
|
||||
test factor_events::tests::catalog_validator_binds_the_complete_body_and_preserves_contract ... ok
|
||||
test result_analysis_tests::copied_real_result_stores_and_browser_fixture ... ignored, requires FIDC_RESULT_QA_FIXTURES containing copied immutable result stores
|
||||
test result_analysis_tests::failed_optional_classification_keeps_unmodified_facts ... ok
|
||||
test result_analysis_tests::historical_text_search_filters_before_count_and_pagination ... ignored, requires isolated FIDC_BACKTEST_TEST_POSTGRES_DSN
|
||||
test result_analysis_tests::history_filters_use_corrected_drawdown_and_never_treat_unknown_as_zero ... ok
|
||||
test result_analysis_tests::owned_analysis_reference_does_not_require_a_cached_run_binding ... ok
|
||||
test result_analysis_tests::projected_history_filter_sort_and_pagination_on_postgres ... ignored, requires FIDC_BACKTEST_TEST_POSTGRES_DSN; creates an isolated test schema
|
||||
test result_analysis_tests::sector_filter_cannot_disable_its_classification_dependency ... ok
|
||||
test risk_projection::tests::absent_original_rates_preserve_independent_metrics_without_network_or_zero_rate_claim ... ok
|
||||
test risk_projection::tests::canonical_null_overrides_all_legacy_aliases_but_real_zero_survives ... ok
|
||||
test risk_projection::tests::original_contract_reprojects_legacy_zero_and_preserves_cash_flow_facts ... ok
|
||||
test risk_projection::tests::unavailable_contract_does_not_relabel_legacy_zero_as_a_verified_statistic ... ok
|
||||
test stock_pool_candidates::tests::resolve_binds_input_and_preserves_empty_automatic_days ... ok
|
||||
test submission_pg_tests::atomic_submission_claim_replay_and_rollback_on_postgres ... ignored, requires FIDC_BACKTEST_TEST_POSTGRES_DSN and creates only an isolated schema
|
||||
test tests::backtest_create_body_limit_accepts_bounded_signal_replays ... ok
|
||||
test tests::backtest_event_stream_exposes_a_websocket_transport ... ok
|
||||
test tests::backtest_list_cache_key_separates_summary_only_runtime_shape ... ok
|
||||
test tests::backtest_list_filters_and_sorting_use_generated_summary ... ok
|
||||
test tests::backtest_list_reports_cache_hit_and_effective_page_limit ... ok
|
||||
test tests::backtest_list_summary_only_uses_compact_query ... ok
|
||||
test tests::backtest_performance_contract_records_total_and_runner_seconds ... ok
|
||||
test tests::backtest_run_compact_cache_key_separates_strategy_detail_shape ... ok
|
||||
test tests::backtest_runner_data_env_accepts_strategy_factory_source_lake_provider ... ok
|
||||
test tests::backtest_runner_data_env_infers_strategy_factory_source_lake_from_runtime_source_table ... ok
|
||||
test tests::backtest_runner_data_env_rejects_legacy_ficlaw_data_source_table ... ok
|
||||
test tests::backtest_runner_data_env_rejects_legacy_source_provider ... ok
|
||||
test tests::backtest_runner_data_env_rejects_non_source_lake_provider ... ok
|
||||
test tests::backtest_runner_data_env_rejects_source_lake_provider_aliases ... ok
|
||||
test tests::backtest_runner_data_env_requires_strategy_factory_source_lake_url ... ok
|
||||
test tests::backtest_runner_data_env_uses_strategy_factory_source_lake_only ... ok
|
||||
test tests::backtest_schema_initialization_never_deletes_run_data ... ok
|
||||
test tests::backtest_schema_materializes_implementation_identity_for_compact_reads ... ok
|
||||
test tests::build_run_payload_does_not_mark_empty_running_summary_as_available ... ok
|
||||
test tests::build_run_payload_exposes_only_canonical_strategy_detail ... ok
|
||||
test tests::build_run_payload_marks_failed_error_summary_as_available ... ok
|
||||
test tests::build_run_payload_marks_terminal_metric_summary_as_available ... ok
|
||||
test tests::build_run_summary_preview_preserves_strategy_runtime_rebalance_signal ... ok
|
||||
test tests::build_run_summary_preview_preserves_terminal_audit ... ok
|
||||
test tests::canonical_result_is_recovered_from_runner_summary ... ok
|
||||
test tests::capacity_override_is_frozen_once_without_changing_execution_risk ... ok
|
||||
test tests::clear_backtest_list_cache_clears_compact_run_cache ... ok
|
||||
test tests::compact_result_handler_uses_single_pg_lookup ... ok
|
||||
test tests::compact_summary_only_runtime_select_avoids_config_json ... ok
|
||||
test tests::compact_summary_sql_removes_heavy_result_artifacts ... ok
|
||||
test tests::daily_execution_matching_type_normalizes_next_open_aliases_and_rejects_other_aliases ... ok
|
||||
test tests::default_backtest_concurrency_scales_with_available_cpu_threads ... ok
|
||||
test tests::default_backtest_source_table_uses_strategy_factory_source_lake ... ok
|
||||
test tests::detail_rows_query_page_size_maps_to_offset_limit ... ok
|
||||
test tests::equity_rows_hide_diagnostics_unless_explicitly_requested ... ok
|
||||
test tests::execution_cost_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::execution_dynamic_slippage_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::execution_matching_type_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::execution_percent_slippage_alias_normalizes_to_price_ratio ... ok
|
||||
test tests::execution_rebalance_cash_mode_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::execution_risk_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::execution_risk_policy_rejects_conflicting_alias_values ... ok
|
||||
test tests::execution_risk_policy_rejects_non_object ... ok
|
||||
test tests::execution_risk_policy_rejects_unknown_fields_before_queueing ... ok
|
||||
test tests::execution_sell_then_buy_delay_slippage_requires_sell_then_buy_mode ... ok
|
||||
test tests::execution_slippage_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::execution_top_level_risk_enabled_aliases_are_frozen_into_strategy_spec ... ok
|
||||
test tests::explicit_cost_override_removes_old_aliases_and_preserves_zero_minimum ... ok
|
||||
test tests::history_query_cache_has_a_bounded_entry_budget ... ok
|
||||
test tests::history_search_is_bounded_and_retains_literal_characters ... ok
|
||||
test tests::implementation_identity_freezes_source_and_binary_generation ... ok
|
||||
test tests::manual_observations_are_run_scoped_and_private_in_the_runner_bundle ... ok
|
||||
test tests::minute_execution_matching_type_uses_minute_price_and_rejects_daily_modes ... ok
|
||||
test tests::persisted_runtime_projection_never_guesses_missing_frequency ... ok
|
||||
test tests::persisted_runtime_projection_retains_minute_mode_and_frozen_bundle ... ok
|
||||
test tests::process_event_projection_uses_stable_kind_and_filters ... ok
|
||||
test tests::projected_fact_block_cache_key_is_shared_across_run_manifests ... ok
|
||||
test tests::public_backtest_projection_removes_internal_paths_recursively ... ok
|
||||
test tests::public_event_message_removes_internal_paths ... ok
|
||||
test tests::public_result_store_manifest_hides_internal_fact_blocks ... ok
|
||||
test tests::public_run_projection_does_not_reintroduce_internal_paths ... ok
|
||||
test tests::result_artifact_cache_keeps_shared_fact_blocks_after_run_eviction ... ok
|
||||
test tests::result_artifact_cache_rejects_conflicting_reference_for_same_sha ... ok
|
||||
test tests::result_block_page_plan_reads_only_overlapping_blocks ... ok
|
||||
test tests::runner_strategy_spec_path_prefers_bundle ... ok
|
||||
test tests::runtime_frequency_normalizes_and_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::runtime_frozen_bundle_is_written_to_strategy_metadata_for_runner ... ok
|
||||
test tests::runtime_projection_migration_preserves_frozen_config ... ok
|
||||
test tests::runtime_symbols_override_strategy_spec_for_runner ... ok
|
||||
test tests::source_lake_preflight_rejects_unsupported_minute_cash_mode ... ok
|
||||
test tests::source_lake_run_contract_preflight_freezes_runtime_and_execution ... ok
|
||||
test tests::source_lake_runtime_requires_frozen_backtest_bundle_contract ... ok
|
||||
test tests::start_backtest_run_rejects_unready_data_source_before_persistence ... ok
|
||||
test tests::startup_reconciliation_closes_orphaned_runs_and_records_audit_event ... ok
|
||||
test tests::strategy_detail_keeps_strategy_body_without_full_config ... ok
|
||||
test tests::strategy_detail_returns_none_without_strategy_body ... ok
|
||||
test tests::strategy_expression_preflight_rejects_invalid_dsl_before_data_load ... ok
|
||||
test tests::summary_list_projection_keeps_table_metrics_and_removes_detail_payloads ... ok
|
||||
test tests::terminal_audit_accepts_deferred_intents_and_checks_the_retained_count ... ok
|
||||
test tests::terminal_audit_contract_rejects_missing_or_inconsistent_state ... ok
|
||||
test tests::top_level_limits_replace_all_nested_execution_aliases_before_preflight ... ok
|
||||
test tests::typed_holding_projection_derives_weight_and_final_snapshot ... ok
|
||||
test tests::typed_result_rows_filter_and_paginate_without_json_table_roundtrip ... ok
|
||||
test tests::typed_risk_filter_applies_execution_fields ... ok
|
||||
test event_stream::tests::replay_buffer_is_bounded_and_completed_streams_are_evicted_without_closing_subscribers ... ok
|
||||
test result_analysis_tests::complete_performance_and_paged_equity_share_corrected_drawdown ... ok
|
||||
test execution_ledger::tests::combined_pages_filter_before_count_and_keep_native_pages_bounded ... ok
|
||||
|
||||
test result: ok. 134 passed; 0 failed; 8 ignored; 0 measured; 0 filtered out; finished in 9.28s
|
||||
|
||||
Running unittests examples/risk_projection_readonly.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/examples/risk_projection_readonly-70003ce8ab818e58)
|
||||
|
||||
running 4 tests
|
||||
test risk_projection::tests::original_contract_reprojects_legacy_zero_and_preserves_cash_flow_facts ... ok
|
||||
test risk_projection::tests::absent_original_rates_preserve_independent_metrics_without_network_or_zero_rate_claim ... ok
|
||||
test risk_projection::tests::canonical_null_overrides_all_legacy_aliases_but_real_zero_survives ... ok
|
||||
test risk_projection::tests::unavailable_contract_does_not_relabel_legacy_zero_as_a_verified_statistic ... ok
|
||||
|
||||
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running unittests examples/stock_pool_contract_probe.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/examples/stock_pool_contract_probe-d3a42410cb4f098c)
|
||||
|
||||
running 1 test
|
||||
test stock_pool_candidates::tests::resolve_binds_input_and_preserves_empty_automatic_days ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
@@ -0,0 +1,679 @@
|
||||
Compiling fidc-core v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1d4b32e/fidc-backtest-engine/crates/fidc-core)
|
||||
Compiling fidc-common v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1d4b32e/fidc-backtest-service/crates/fidc-common)
|
||||
Compiling fidc-signal-client v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1d4b32e/fidc-backtest-engine/crates/fidc-signal-client)
|
||||
Compiling fidc-backtest-runner v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1d4b32e/fidc-backtest-service/services/backtest_runner)
|
||||
Compiling fidc-backtest-service v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1d4b32e/fidc-backtest-service/services/api)
|
||||
Finished `test` profile [unoptimized] target(s) in 40.48s
|
||||
Running unittests src/lib.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/fidc_backtest_runner-e8f8f9d86cad5791)
|
||||
|
||||
running 484 tests
|
||||
test backtest::columnar_factor_overlay_tests::cache_range_rejects_symbol_disorder_outside_the_requested_date ... ok
|
||||
test backtest::columnar_factor_overlay_tests::cache_range_rejects_date_disorder_outside_the_binary_search_slice ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_real_cache_value_equivalence ... ignored, requires a frozen private Boris real-data cache directory on 177
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_missing_or_stale_cache_does_not_mutate_base ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_duplicate_order_and_count_mismatches ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_duplicate_window_boundary ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_late_key_mismatch_before_any_base_mutation ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_market_projection_and_invalid_cached_column_type ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_unknown_keys_are_not_hidden_as_empty_values ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_replaced_file_invalidates_view_without_mutating_pinned_input ... ok
|
||||
test backtest::columnar_factor_overlay_tests::row_visitor_preserves_selected_offsets_and_late_validation_errors ... ok
|
||||
test backtest::compact_storage_tests::batch_bound_factor_columns_preserve_types_nulls_and_nonfinite_values ... ok
|
||||
test backtest::compact_storage_tests::inline_symbols_and_factor_maps_preserve_binary_and_json_contracts ... ok
|
||||
test backtest::day_snapshot_builder::tests::a_failed_builder_cannot_publish_earlier_valid_rows ... ok
|
||||
test backtest::columnar_factor_overlay_tests::valid_cache_reuses_its_key_index_and_failed_indexes_are_not_published ... ok
|
||||
test backtest::day_snapshot_builder::tests::corporate_distribution_quantities_must_be_known_before_a_day_is_published ... ok
|
||||
test backtest::day_snapshot_builder::tests::factor_reservation_keeps_missing_and_overwrite_semantics ... ok
|
||||
test backtest::day_snapshot_builder::tests::late_source_validation_failure_invalidates_the_streamed_day ... ok
|
||||
test backtest::equity_lifecycle_metadata_tests::empty_quotes_require_full_interval_lifecycle_proof_not_a_latest_status ... ok
|
||||
test backtest::equity_lifecycle_metadata_tests::lifecycle_cash_days_keep_real_benchmark_without_fabricated_security_rows ... ok
|
||||
test backtest::equity_lifecycle_metadata_tests::missing_identity_and_dates_do_not_become_active_securities ... ok
|
||||
test backtest::execution_config_tests::capacity_preflight_requires_explicit_daily_assumptions_without_changing_risk_switches ... ok
|
||||
test backtest::execution_config_tests::component_execution_quotes_seed_uses_latest_intraday_quote_before_target_time ... ok
|
||||
test backtest::execution_config_tests::current_close_respects_explicitly_disabled_liquidity_limit ... ok
|
||||
test backtest::execution_config_tests::daily_current_close_preserves_the_strategy_decision_clock ... ok
|
||||
test backtest::execution_config_tests::daily_execution_price_request_serializes_date_symbol_filter_scope ... ok
|
||||
test backtest::execution_config_tests::daily_execution_projection_keeps_benchmark_narrow_and_trade_risk_complete ... ok
|
||||
test backtest::execution_config_tests::data_intraday_snapshot_time_is_strategy_spec_driven ... ok
|
||||
test backtest::execution_config_tests::dataset_signature_rejects_source_lake_without_frozen_backtest_bundle ... ok
|
||||
test backtest::day_snapshot_builder::tests::arrow_rows_build_the_same_final_snapshots_without_a_row_collection ... ok
|
||||
test backtest::execution_config_tests::dataset_signature_scopes_dynamic_model_score_source_rows ... ok
|
||||
test backtest::execution_config_tests::default_matching_type_follows_backtest_frequency ... ok
|
||||
test backtest::execution_config_tests::dataset_signature_scopes_backtest_data_bundle_source_rows ... ok
|
||||
test backtest::execution_config_tests::dynamic_model_score_execution_quotes_are_lazy_by_default ... ok
|
||||
test backtest::execution_config_tests::dynamic_model_score_rank_does_not_extract_fixed_signal_symbols ... ok
|
||||
test backtest::execution_config_tests::execution_quote_prefilter_keeps_previous_factor_rows_in_current_band ... ok
|
||||
test backtest::execution_config_tests::execution_quote_prefilter_prefers_runtime_market_cap_slope ... ok
|
||||
test backtest::execution_config_tests::execution_quote_query_times_follow_strategy_trade_times ... ok
|
||||
test backtest::execution_config_tests::execution_top_level_fee_overrides_nested_risk_policy_cost ... ok
|
||||
test backtest::execution_config_tests::execution_quote_prefilter_keeps_candidates_beyond_legacy_limit ... ok
|
||||
test backtest::execution_config_tests::execution_top_level_volume_percent_overrides_nested_risk_policy ... ok
|
||||
test backtest::execution_config_tests::factor_keys_use_compact_storage_without_changing_names ... ok
|
||||
test backtest::execution_config_tests::extract_score_context_does_not_extract_fixed_signal_symbols ... ok
|
||||
test backtest::execution_config_tests::fixed_signal_model_score_scope_does_not_validate_candidate_limit ... ok
|
||||
test backtest::execution_config_tests::fixed_signal_symbol_scope_preserves_explicit_empty_targets ... ok
|
||||
test backtest::execution_config_tests::full_minute_event_stream_uses_subscriptions_instead_of_point_snapshot ... ok
|
||||
test backtest::execution_config_tests::minute_session_bounds_extend_only_after_post_close_rule_effective_date ... ok
|
||||
test backtest::execution_config_tests::model_score_source_rows_scope_evidence_verifies_candidate_limit ... ok
|
||||
test backtest::execution_config_tests::model_score_source_rows_scope_rejects_unbounded_rows ... ok
|
||||
test backtest::execution_config_tests::native_next_open_pool_loads_causal_quotes_without_exposing_them_to_selection ... ok
|
||||
test backtest::execution_config_tests::narrow_overlay_projection_does_not_retain_a_wide_value_buffer ... ok
|
||||
test backtest::execution_config_tests::owned_date_group_producers_drop_every_input_exactly_once_on_failure ... ok
|
||||
test backtest::execution_config_tests::owned_trade_day_preparation_matches_borrowed_rows ... ok
|
||||
test backtest::execution_config_tests::parse_matching_type_limits_global_backtest_matching_modes ... ok
|
||||
test backtest::execution_config_tests::parse_slippage_model_supports_explicit_historical_model ... ok
|
||||
test backtest::execution_config_tests::parse_slippage_model_supports_limit_price ... ok
|
||||
test backtest::execution_config_tests::platform_daily_factor_strategy_uses_lazy_execution_quotes_by_default ... ok
|
||||
test backtest::execution_config_tests::platform_execution_quote_carry_can_include_recent_candidates ... ok
|
||||
test backtest::execution_config_tests::platform_execution_quote_carry_does_not_let_candidates_evict_recent_orders ... ok
|
||||
test backtest::execution_config_tests::platform_execution_quote_carry_keeps_today_candidates_without_candidate_carry ... ok
|
||||
test backtest::execution_config_tests::platform_execution_quote_carry_prefers_recent_candidates_before_today_candidates ... ok
|
||||
test backtest::execution_config_tests::platform_execution_quote_carry_prioritizes_order_symbols ... ok
|
||||
test backtest::execution_config_tests::platform_execution_quote_default_carry_keeps_large_recent_candidate_set ... ok
|
||||
test backtest::execution_config_tests::platform_execution_route_requires_runtime_contract ... ok
|
||||
test backtest::execution_config_tests::platform_runtime_snapshot_preserves_typed_time_in_force ... ok
|
||||
test backtest::execution_config_tests::platform_strategy_config_rejects_loose_value_budget_without_panicking ... ok
|
||||
test backtest::execution_config_tests::parallel_owned_date_groups_keep_exact_rows_and_owned_factor_allocations ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_candidate_market_cap_falls_back_to_total_market_cap ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_daily_selection_price_uses_close_without_limit_allow_flags ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_does_not_build_execution_quote_from_daily_fallback ... ok
|
||||
test backtest::execution_config_tests::platform_strategy_config_uses_same_intraday_time_as_broker ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_keeps_allow_buy_when_missing_field_is_not_buy_relevant ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_keeps_daily_volume_when_snapshot_has_intraday_volume ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_keeps_external_signal_warmup_day ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_keeps_st_and_star_st_flags_independent ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_keeps_missing_risk_facts_when_runtime_filters_are_disabled ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_preserves_missing_daily_volume_contract ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_preserves_zero_ask_from_intraday_snapshot ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_records_all_missing_risk_facts_independent_of_policy ... ok
|
||||
test backtest::execution_config_tests::raw_yuan_market_cap_normalizes_to_100m_units ... ok
|
||||
test backtest::execution_config_tests::resolve_dividend_reinvestment_defaults_false_and_reads_engine_flag ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_does_not_allow_overspending_value_budget ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_enables_strict_value_budget_by_default ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_keeps_explicit_liquidity_quote_limit ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_distinguishes_timed_daily_current_close ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_keeps_explicit_same_day_buy_mark_override ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_keeps_explicit_snapshot_volume_limit ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_keeps_selection_risk_off_by_default ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_keeps_volume_limit_for_snapshot_minute_last_by_default ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_maps_daily_next_open ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_maps_minute_last_to_minute_price ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_maps_rebalance_cash_mode_and_forces_minute ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_preserves_zero_minimum_commission ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_reads_nested_risk_policy ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_rejects_delay_slippage_outside_sell_then_buy ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_rejects_vwap_as_global_matching_type ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_uses_explicit_market_rules_without_profile ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_uses_latest_trade_time_as_main_schedule ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_uses_runtime_schedule_time_for_minute_execution ... ok
|
||||
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_preserves_static_universe_contract ... ok
|
||||
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_preserves_weak_market_threshold ... ok
|
||||
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_delayed_limit_open_exit ... ok
|
||||
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_release_slot_on_exit_signal ... ok
|
||||
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_trading_schedule ... ok
|
||||
test backtest::execution_config_tests::retired_slippage_and_invalid_coefficients_never_become_no_slippage ... ok
|
||||
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_trading_stage ... ok
|
||||
test backtest::execution_config_tests::risk_policy_alias_normalization_preserves_matching_values ... ok
|
||||
test backtest::execution_config_tests::reused_candidate_plan_panel_matches_full_intraday_market_materialization ... ok
|
||||
test backtest::execution_config_tests::risk_policy_alias_normalization_rejects_conflicting_values ... ok
|
||||
test backtest::execution_config_tests::runtime_selection_intraday_quote_detection_reads_quote_fields_only ... ok
|
||||
test backtest::execution_config_tests::sorted_overlay_keeps_unchanged_zero_bits_and_counts_only_changed_rows ... ok
|
||||
test backtest::execution_config_tests::sorted_overlay_transfers_owned_factor_storage_without_changing_bits ... ok
|
||||
test backtest::execution_config_tests::sorted_source_row_insertion_preserves_existing_duplicate_order ... ok
|
||||
test backtest::execution_config_tests::sorted_sparse_overlay_merge_falls_back_for_duplicate_keys ... ok
|
||||
test backtest::execution_config_tests::sorted_sparse_overlay_merge_preserves_order_and_unmatched_rows ... ok
|
||||
test backtest::execution_config_tests::source_lake_backtest_bundle_context_requires_complete_metadata ... ok
|
||||
test backtest::execution_config_tests::source_lake_execution_price_requests_keep_compact_schema ... ok
|
||||
test backtest::execution_config_tests::source_lake_market_cap_keeps_current_decision_date_value ... ok
|
||||
test backtest::execution_config_tests::source_lake_market_cap_uses_current_decision_date_snapshot ... ok
|
||||
test backtest::execution_config_tests::source_lake_model_score_context_accepts_complete_inferred_scope ... ok
|
||||
test backtest::execution_config_tests::source_lake_model_score_context_allows_plain_rotation_without_model_score ... ok
|
||||
test backtest::execution_config_tests::source_lake_model_score_context_rejects_dynamic_mode_without_source_label_or_scope ... ok
|
||||
test backtest::execution_config_tests::source_lake_model_score_context_rejects_partial_scope ... ok
|
||||
test backtest::execution_config_tests::source_lake_model_score_context_rejects_sparse_ref_without_scope ... ok
|
||||
test backtest::execution_config_tests::source_lake_request_keeps_dynamic_model_score_as_all_market_with_score_context ... ok
|
||||
test backtest::execution_config_tests::source_lake_request_keeps_explicit_empty_symbols_empty ... ok
|
||||
test backtest::execution_config_tests::source_lake_request_scopes_fixed_signal_symbols_and_keeps_compact_schema ... ok
|
||||
test backtest::execution_config_tests::source_lake_request_serializes_date_symbol_filter_scope ... ok
|
||||
test backtest::execution_config_tests::source_lake_source_rows_uses_current_row_decision_safe_prev_factor_mode ... ok
|
||||
test backtest::execution_config_tests::source_row_adjustment_factor_decode_accepts_positive_finite_values ... ok
|
||||
test backtest::execution_config_tests::source_row_adjustment_factor_decode_allows_paused_rows_without_close ... ok
|
||||
test backtest::execution_config_tests::source_row_adjustment_factor_decode_rejects_paused_rows_with_close ... ok
|
||||
test backtest::execution_config_tests::source_row_adjustment_factor_decode_rejects_missing_or_invalid_values ... ok
|
||||
test backtest::execution_config_tests::source_row_cache_and_filter_distinguish_all_from_explicit_empty_symbols ... ok
|
||||
test backtest::execution_config_tests::source_row_daily_capacity_is_scoped_to_actual_insertion_dates ... ok
|
||||
test backtest::execution_config_tests::source_row_adjustment_factor_decode_skips_unrequested_projection ... ok
|
||||
test backtest::execution_config_tests::source_row_grouping_copies_date_keys_only_at_boundaries ... ok
|
||||
test backtest::execution_config_tests::source_row_missing_open_does_not_mark_candidate_as_one_yuan ... ok
|
||||
test backtest::execution_config_tests::source_row_model_score_context_accepts_extract_only_score_metadata ... ok
|
||||
test backtest::execution_config_tests::source_row_model_score_context_can_disable_candidate_limit_inference ... ok
|
||||
test backtest::execution_config_tests::source_row_model_score_context_infers_candidate_limit_from_selection_limit ... ok
|
||||
test backtest::execution_config_tests::source_row_daily_capacity_keeps_supplemental_inserts_in_place ... ok
|
||||
test backtest::execution_config_tests::source_row_model_score_context_merges_bundle_extract_and_scopes_cache ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_does_not_require_adjustment_factor_for_volume_only_rolling ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_keeps_select_all_for_generic_empty_factor_refs ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_includes_amount_for_stock_amount_expressions ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_keeps_source_lake_empty_factor_refs_narrow ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_keeps_source_lake_without_strategy_spec_narrow ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_splits_sparse_model_score_factor_refs ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_uses_framework_rolling_inputs ... ok
|
||||
test backtest::execution_config_tests::source_row_sort_preserves_published_order_and_stably_repairs_merges ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_uses_raw_market_columns_for_framework_rollings ... ok
|
||||
test backtest::execution_config_tests::source_rows_query_engine_is_validated_and_scopes_cache_epoch ... ok
|
||||
test backtest::execution_config_tests::source_rows_scope_evidence_preserves_unordered_date_counts ... ok
|
||||
test backtest::execution_config_tests::sparse_model_score_overlay_dates_exclude_warmup_rows ... ok
|
||||
test backtest::execution_config_tests::sparse_model_score_overlay_keeps_named_score_when_context_uses_default_score ... ok
|
||||
test backtest::execution_config_tests::sparse_model_score_overlay_merges_only_requested_extra_factors ... ok
|
||||
test backtest::execution_config_tests::sparse_model_score_overlay_query_windows_are_chunked_by_date_span ... ok
|
||||
test backtest::execution_config_tests::source_warmup_uses_required_trading_windows ... ok
|
||||
test backtest::execution_config_tests::sparse_model_score_overlay_skips_primary_score_columns ... ok
|
||||
test backtest::execution_config_tests::sparse_model_score_overlay_reuses_source_row_cache_without_http ... ok
|
||||
test backtest::execution_config_tests::stock_cost_model_for_execution_uses_explicit_fee_overrides ... ok
|
||||
test backtest::execution_config_tests::sparse_overlay_skips_source_lake_when_scoped_date_symbol_filter_is_empty ... ok
|
||||
test backtest::execution_config_tests::strategy_execution_top_level_risk_enabled_aliases_are_applied ... ok
|
||||
test backtest::execution_config_tests::strategy_risk_policy_accepts_camel_case_fields ... ok
|
||||
test backtest::execution_config_tests::supplemental_execution_key_index_only_materializes_requested_symbols ... ok
|
||||
test backtest::execution_config_tests::timed_daily_current_close_loads_the_intraday_decision_snapshot ... ok
|
||||
test backtest::execution_config_tests::strategy_factory_model_rank_rotation_keeps_explicit_execution_contract ... ok
|
||||
test backtest::execution_config_tests::top_level_execution_limits_override_nested_risk_policy_defaults ... ok
|
||||
test backtest::execution_config_tests::unique_source_row_trade_dates_preserves_sorted_and_unordered_inputs ... ok
|
||||
test backtest::execution_config_tests::unsupported_backtest_frequency_is_rejected ... ok
|
||||
test backtest::execution_config_tests::validated_source_limit_prices_allows_explicit_no_limit_stock ... ok
|
||||
test backtest::execution_config_tests::validated_source_limit_prices_allows_index_without_limits ... ok
|
||||
test backtest::execution_config_tests::validated_source_limit_prices_allows_paused_stock_without_limits ... ok
|
||||
test backtest::execution_config_tests::validated_source_limit_prices_rejects_missing_stock_limit ... ok
|
||||
test backtest::execution_config_tests::validated_source_limit_prices_trusts_source_limits ... ok
|
||||
test backtest::factor_history_calendar_tests::history_uses_actual_sessions_and_rejects_short_or_future_calendars ... ok
|
||||
test backtest::factor_history_calendar_tests::contextual_pattern_expands_data_scope_but_not_candidate_members ... ok
|
||||
test backtest::factor_history_calendar_tests::index_policy_preloads_its_reference_without_widening_the_candidate_universe ... ok
|
||||
test backtest::factor_history_calendar_tests::selection_buy_permission_carries_its_complete_pattern_window ... ok
|
||||
test backtest::fund_metadata::tests::day_preparation_distinguishes_etf_optional_risk_from_missing_prices ... ok
|
||||
test backtest::fund_metadata::tests::etf_metadata_binds_identity_dates_and_units ... ok
|
||||
test backtest::fund_metadata::tests::real_etf_fallback_fills_match_source_opens_dates_and_fee_budgets ... ignored, requires exact frontend-compiled frozen Source Lake ETF replay artifacts
|
||||
test backtest::fund_metadata::tests::real_mixed_prices_preserve_holdings_until_explicit_zero_allocation ... ignored, requires FIDC_FUND_REPLAY_PREFIX containing read-only real Source Arrow fixtures
|
||||
test backtest::fund_metadata::tests::real_source_arrow_preserves_etf_identity_and_detects_tampering ... ignored, requires FIDC_FUND_METADATA_TEST_ARROW from the read-only real Source probe
|
||||
test backtest::fund_metadata::tests::preliminary_candidate_panel_uses_verified_etf_rules_without_inventing_limits ... ok
|
||||
test backtest::manual_data_scope::tests::explicit_data_scope_can_expand_without_mutating_the_strategy_or_empty_pool ... ok
|
||||
test backtest::manual_data_scope::tests::dataset_cache_tracks_extra_market_demand_not_manual_price_or_execution_identity ... ok
|
||||
test backtest::prepared_source_row::tests::all_supported_numeric_types_keep_the_existing_conversion_and_null_policy ... ok
|
||||
test backtest::prepared_source_row::tests::arbitrary_slices_and_supplemental_rows_keep_symbol_order ... ok
|
||||
test backtest::prepared_source_row::tests::finite_values_keep_exact_float_bits_and_metadata ... ok
|
||||
test backtest::prepared_source_row::tests::invalid_json_is_not_hidden_by_a_consumer_that_needs_no_factors ... ok
|
||||
test backtest::prepared_source_row::tests::actual_arrow_day_uses_columnar_rows_and_matches_owned_final_snapshots ... ok
|
||||
test backtest::prepared_source_row::tests::json_typed_duplicates_and_projected_overlay_preserve_overwrite_order ... ok
|
||||
test backtest::prepared_source_row::tests::nonfinite_price_policy_does_not_mutate_original_arrow_values ... ok
|
||||
test backtest::prepared_source_row::tests::overlay_cannot_relax_base_adjustment_validation ... ok
|
||||
test backtest::risk_free_rate_cache_identity_includes_bundle_and_full_calendar ... ok
|
||||
test backtest::prepared_source_row::tests::schema_error_precedence_and_null_keys_are_preserved ... ok
|
||||
test backtest::risk_free_rate_cache_round_trips_validated_contract ... ok
|
||||
test backtest::saved_risk_cache::tests::saved_reference_cancelled_reader_does_not_leave_a_claim ... ok
|
||||
test backtest::saved_risk_cache::tests::saved_reference_identity_includes_endpoint_hash_and_calendar ... ok
|
||||
test backtest::saved_risk_reference_requires_original_content_not_only_server_claimed_hash ... ok
|
||||
test backtest::source_query_windows::tests::actual_cached_boundaries_are_kept_and_gaps_are_covered ... ok
|
||||
test backtest::source_query_windows::tests::all_days_remain_once_and_scope_count_is_bounded ... ok
|
||||
test backtest::source_query_windows::tests::discovery_never_crosses_source_keys_schemas_or_file_types ... ok
|
||||
test backtest::source_query_windows::tests::overlap_prefers_more_covered_days_then_fewer_windows ... ok
|
||||
test backtest::source_query_windows::tests::projection_and_symbol_scope_have_distinct_boundary_keys ... ok
|
||||
test backtest::source_query_windows::tests::uncached_gaps_keep_the_existing_calendar_anchor ... ok
|
||||
test backtest::source_row_key_index::tests::indexed_date_ranges_cover_days_split_across_batches ... ok
|
||||
test backtest::saved_risk_cache::tests::saved_reference_coalesces_only_validated_original_contracts ... ok
|
||||
test backtest::source_row_key_index::tests::normalized_keys_cannot_duplicate_across_batches ... ok
|
||||
test backtest::source_row_key_index::tests::null_identity_and_declared_count_mismatch_are_rejected ... ok
|
||||
test backtest::source_row_query_purpose_tests::factor_overlay_cache_identity_cannot_be_reused_as_market_facts ... ok
|
||||
test backtest::source_row_query_purpose_tests::factor_overlay_merge_preserves_every_base_market_and_risk_fact ... ok
|
||||
test backtest::source_row_query_purpose_tests::only_explicit_factor_overlays_omit_redundant_risk_columns ... ok
|
||||
test backtest::source_row_query_purpose_tests::factor_overlay_requires_an_explicit_non_market_projection ... ok
|
||||
test backtest::source_rows_columnar::tests::cache_write_failure_does_not_replace_the_current_columnar_input ... ok
|
||||
test backtest::source_rows_columnar::tests::actual_selection_and_empty_outputs_do_not_keep_unused_factor_columns ... ok
|
||||
test backtest::source_rows_columnar::tests::columnar_normalization_matches_mixed_values_json_aliases_and_sorting ... ok
|
||||
test backtest::source_rows_columnar::tests::invalid_keys_and_adjustment_are_rejected_before_selection ... ok
|
||||
test backtest::source_rows_columnar::tests::malformed_json_and_dependency_identity_are_not_accepted ... ok
|
||||
test backtest::source_rows_columnar::tests::non_cacheable_json_factor_names_keep_the_real_record_path ... ok
|
||||
test backtest::source_rows_columnar::tests::normalized_duplicate_symbols_are_not_deduplicated ... ok
|
||||
test backtest::source_rows_input_tests::cache_cannot_include_rows_outside_its_declared_date_range ... ok
|
||||
test backtest::source_rows_input_tests::cache_write_failure_retains_the_real_window_and_empty_windows_do_not_read_old_rows ... ok
|
||||
test backtest::source_rows_input_tests::failed_materialization_does_not_turn_the_source_into_an_empty_success ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_crosses_record_batches_and_preserves_key_normalization ... ok
|
||||
test backtest::source_rows_input_tests::independent_windows_stay_columnar_until_daily_snapshot_construction ... ok
|
||||
test backtest::source_rows_input_tests::mapped_input_can_be_materialized_for_mutating_model_and_publication_overlays ... ok
|
||||
test backtest::source_rows_input_tests::invalid_cache_keys_and_overlay_coverage_never_publish_source_input ... ok
|
||||
test backtest::source_rows_input_tests::mapped_input_preserves_empty_scopes_and_filters_only_base_rows ... ok
|
||||
test backtest::source_rows_input_tests::mixed_window_storage_and_duplicate_boundaries_keep_original_semantics ... ok
|
||||
test backtest::source_rows_input_tests::mapped_inventory_and_final_days_match_owned_rows_with_factor_overlays ... ok
|
||||
test backtest::source_rows_input_tests::separate_arrow_factor_input_preserves_sparse_overlay_values_and_existing_layers ... ok
|
||||
test backtest::terminal_audit_summary_tests::deferred_strategy_warning_is_not_presented_as_a_submitted_order ... ok
|
||||
test backtest::terminal_audit_summary_tests::pending_terminal_state_is_explicit_in_summary_warning ... ok
|
||||
test backtest::terminal_calendar_tests::delayed_deposit_requires_terminal_calendar_but_zero_day_does_not ... ok
|
||||
test backtest::source_rows_input_tests::unsuccessful_window_publication_cannot_read_a_previous_cache_file ... ok
|
||||
test backtest::terminal_calendar_tests::scheduled_terminal_calendar_requires_strict_order_and_range ... ok
|
||||
test backtest::tests::accounting_allocation_details_do_not_require_or_fabricate_an_order ... ok
|
||||
test backtest::tests::all_source_lake_runtime_caches_are_scoped_by_backtest_bundle ... ok
|
||||
test backtest::tests::audit_instrument_cache_values_from_request ... ignored, read-only Boris cache comparison with an explicit audit request
|
||||
test backtest::tests::benchmark_borrowed_numeric_factor_keys_against_owned_strings ... ignored, manual release-mode allocation benchmark
|
||||
test backtest::tests::benchmark_date_symbol_filter_query_range_preserves_index_symbols ... ok
|
||||
test backtest::tests::benchmark_query_windows_split_long_source_lake_ranges ... ok
|
||||
test backtest::tests::arrow_reader_accepts_lz4_compressed_stream ... ok
|
||||
test backtest::tests::benchmark_row_binary_cache_rejects_metadata_mismatch ... ok
|
||||
test backtest::tests::benchmark_row_binary_cache_roundtrips_rows ... ok
|
||||
test backtest::tests::benchmark_row_cache_ignores_legacy_csv_cache ... ok
|
||||
test backtest::tests::benchmark_row_cache_path_scopes_date_symbol_filter ... ok
|
||||
test backtest::tests::benchmark_row_cache_path_uses_binary_cache_file ... ok
|
||||
test backtest::tests::benchmark_row_candidate_scan_removes_stale_schema_cache ... ok
|
||||
test backtest::tests::benchmark_row_covering_cache_reuses_superset_symbols_and_dates ... ok
|
||||
test backtest::tests::benchmark_row_partial_cache_rejects_missing_symbol_segment ... ok
|
||||
test backtest::tests::benchmark_row_to_source_row_preserves_false_paused_state ... ok
|
||||
test backtest::tests::benchmark_row_partial_cache_reports_missing_ranges ... ok
|
||||
test backtest::tests::benchmark_row_window_cache_scope_ignores_out_of_window_filter_dates ... ok
|
||||
test backtest::tests::benchmark_rows_dedup_after_window_merge ... ok
|
||||
test backtest::tests::benchmark_source_row_arrow_bounded_materialization ... ignored, requires FIDC_BT_SOURCE_ROW_VIEW_PROBE_PATH and explicit release-mode invocation
|
||||
test backtest::tests::benchmark_source_row_arrow_view_cache ... ignored, requires FIDC_BT_SOURCE_ROW_VIEW_PROBE_PATH and explicit release-mode invocation
|
||||
test backtest::tests::bootstrap_missing_quote_repair_keys_keeps_all_actual_order_gaps_without_fills ... ok
|
||||
test backtest::tests::bootstrap_missing_quote_repair_keys_keeps_all_after_fills_exist ... ok
|
||||
test backtest::tests::benchmark_rows_empty_date_symbol_filter_does_not_reuse_existing_cache ... ok
|
||||
test backtest::tests::build_trade_detail_rows_expands_split_fills_by_order_id ... ok
|
||||
test backtest::tests::build_trade_detail_rows_matches_fill_when_order_reason_changes ... ok
|
||||
test backtest::tests::build_trade_detail_rows_omits_canceled_orders_without_fills ... ok
|
||||
test backtest::tests::build_trade_detail_rows_preserves_decision_and_execution_dates ... ok
|
||||
test backtest::tests::carry_forward_target_snapshot_keeps_transition_day_then_replaces_positions ... ok
|
||||
test backtest::tests::cn_index_symbols_are_not_stock_execution_quote_repair_symbols ... ok
|
||||
test backtest::tests::collect_missing_execution_quote_keys_uses_canceled_stock_orders_only ... ok
|
||||
test backtest::tests::compact_progress_retains_manual_counts_and_rejects_a_missing_source ... ok
|
||||
test backtest::tests::compacted_carry_forward_filter_keeps_window_semantics ... ok
|
||||
test backtest::tests::corporate_action_arrow_preserves_the_independent_delivery_date_and_unknown_quantities ... ok
|
||||
test backtest::tests::corporate_action_binary_cache_roundtrips_rows ... ok
|
||||
test backtest::tests::corporate_action_cache_ignores_legacy_csv_cache ... ok
|
||||
test backtest::tests::corporate_action_cache_path_uses_source_lake_provider_epoch ... ok
|
||||
test backtest::tests::bundle_validation_observes_dependency_semantic_change_and_revocation_without_ttl ... ok
|
||||
test backtest::tests::corporate_action_covering_cache_reuses_superset_scope ... ok
|
||||
test backtest::tests::daily_execution_price_arrow_decode_preserves_unpriced_paused_risk_rows ... ok
|
||||
test backtest::tests::daily_execution_scope_membership_is_normalized_once_and_isolated_by_date ... ok
|
||||
test backtest::tests::daily_execution_price_arrow_requires_execution_contract_fields ... ok
|
||||
test backtest::tests::dataset_signature_scopes_fixed_signal_source_rows ... ok
|
||||
test backtest::tests::dataset_signature_scopes_fixed_signal_date_symbol_filter_with_model_score ... ok
|
||||
test backtest::tests::date_symbol_filter_query_range_carry_forward_keeps_history_prefix ... ok
|
||||
test backtest::tests::date_symbol_filter_query_range_exact_keeps_only_window_dates ... ok
|
||||
test backtest::tests::date_symbol_filter_query_range_preserves_empty_request ... ok
|
||||
test backtest::tests::dataset_signature_uses_data_requirements_not_strategy_identity_or_thresholds ... ok
|
||||
test backtest::tests::default_cache_epochs_ignore_local_manifest_directory ... ok
|
||||
test backtest::tests::dense_execution_quote_plan_uses_requested_trade_dates_only ... ok
|
||||
test backtest::tests::dynamic_model_score_execution_only_queries_next_date_gaps ... ok
|
||||
test backtest::tests::dynamic_model_score_execution_skips_rows_present_on_next_full_calendar_date ... ok
|
||||
test backtest::tests::dynamic_model_score_execution_symbols_include_candidate_universe_for_lagged_execution ... ok
|
||||
test backtest::tests::dynamic_symbol_expression_does_not_enable_fixed_source_scope ... ok
|
||||
test backtest::tests::empty_date_symbol_filter_is_empty_request_not_all_market ... ok
|
||||
test backtest::tests::equity_artifact_keeps_external_deposit_out_of_return ... ok
|
||||
test backtest::tests::equity_projection_orders_facts_and_includes_the_initial_nav_peak ... ok
|
||||
test backtest::tests::execution_arrow_does_not_turn_nonfinite_numbers_into_optional_missing_values ... ok
|
||||
test backtest::tests::execution_arrow_requires_the_strict_source_contract_even_when_empty ... ok
|
||||
test backtest::tests::execution_price_date_symbol_filter_keeps_benchmark_and_prunes_other_targets ... ok
|
||||
test backtest::tests::execution_price_only_source_row_marker_round_trips ... ok
|
||||
test backtest::tests::execution_quantities_are_exact_integers_not_rounded_floats ... ok
|
||||
test backtest::tests::exact_source_row_windows_restore_in_parallel_and_keep_order ... ok
|
||||
test backtest::tests::execution_quote_day_binary_cache_invalidates_duplicate_date_symbol ... ok
|
||||
test backtest::tests::execution_quote_day_binary_cache_rejects_metadata_mismatch ... ok
|
||||
test backtest::tests::execution_quote_day_cache_does_not_persist_empty_negative_result ... ok
|
||||
test backtest::tests::execution_quote_day_cache_ignores_legacy_csv_cache ... ok
|
||||
test backtest::tests::execution_quote_day_cache_loads_empty_without_provider_url ... ok
|
||||
test backtest::tests::execution_quote_day_cache_path_includes_minute_lookback_and_price_rule ... ok
|
||||
test backtest::tests::execution_quote_day_cache_path_uses_quote_epoch ... ok
|
||||
test backtest::tests::execution_quote_day_cache_path_uses_source_lake_provider_tag ... ok
|
||||
test backtest::tests::execution_quote_day_cache_tracks_returned_symbols_only ... ok
|
||||
test backtest::tests::execution_quote_prefetch_seed_tracks_negative_symbols ... ok
|
||||
test backtest::tests::execution_quote_query_tasks_keep_target_time_symbol_scopes_narrow ... ok
|
||||
test backtest::tests::execution_quote_query_tasks_merge_identical_symbol_scopes_for_same_day ... ok
|
||||
test backtest::tests::execution_quote_rows_duplicate_date_symbol_is_rejected ... ok
|
||||
test backtest::tests::execution_quote_rows_normalize_base_stock_code_symbols ... ok
|
||||
test backtest::tests::execution_quote_loader_reads_a_bounded_minute_window_instead_of_a_point_snapshot ... ok
|
||||
test backtest::tests::explicit_cli_source_cache_epoch_is_stable_and_provider_scoped ... ok
|
||||
test backtest::tests::external_daily_execution_symbols_without_signals_only_queries_benchmark ... ok
|
||||
test backtest::tests::external_daily_execution_symbols_skip_signals_already_in_source_rows ... ok
|
||||
test backtest::tests::factor_values_arrow_decode_validates_metadata_and_merges_aliases ... ok
|
||||
test backtest::tests::fidc_runner_rejects_ficlaw_data_source_rows_table ... ok
|
||||
test backtest::tests::factor_values_arrow_decode_rejects_future_data_and_semantic_drift ... ok
|
||||
test backtest::tests::fixed_minute_subscriptions_expand_static_daily_data_scope ... ok
|
||||
test backtest::tests::fixed_signal_date_symbol_filter_is_disabled_when_static_symbols_are_mixed_in ... ok
|
||||
test backtest::tests::fixed_signal_date_symbol_filter_seeds_static_symbols_at_query_start ... ok
|
||||
test backtest::tests::fixed_signal_date_symbol_filter_survives_model_score_rank_metadata ... ok
|
||||
test backtest::tests::fixed_signal_next_open_exit_scope_keeps_all_prior_unsold_symbols ... ok
|
||||
test backtest::tests::fixed_signal_source_row_date_symbol_filter_extracts_dated_weights_only ... ok
|
||||
test backtest::tests::fixed_signal_source_row_symbols_extracts_runtime_target_weights_expr ... ok
|
||||
test backtest::tests::fixed_signal_source_row_date_symbol_filter_extracts_target_holding_rows ... ok
|
||||
test backtest::tests::fixed_signal_source_row_symbols_extracts_single_quoted_runtime_target_weights_expr ... ok
|
||||
test backtest::tests::fixed_signal_source_row_symbols_extracts_strategy_metadata_targets ... ok
|
||||
test backtest::tests::fixed_signal_source_row_symbols_extracts_target_weight_codes ... ok
|
||||
test backtest::tests::fixed_symbol_order_scope_requires_rotation_disabled ... ok
|
||||
test backtest::tests::fixed_symbol_orders_seed_exact_intraday_snapshot_keys ... ok
|
||||
test backtest::tests::frozen_bundle_identity_http_failure_is_not_a_mutable_epoch_fallback ... ok
|
||||
test backtest::tests::full_minute_prefetch_days_respect_total_bar_budget ... ok
|
||||
test backtest::tests::futures_source_lake_catalog_requires_all_truth_datasets ... ok
|
||||
test backtest::tests::futures_only_runtime_contract_keeps_stock_scope_empty_and_allocates_cash ... ok
|
||||
test backtest::tests::instrument_metadata_binary_cache_rejects_metadata_mismatch ... ok
|
||||
test backtest::tests::instrument_metadata_binary_cache_roundtrips_rows ... ok
|
||||
test backtest::tests::instrument_metadata_cache_merges_missing_symbols_incrementally ... ok
|
||||
test backtest::tests::instrument_metadata_cache_ignores_legacy_csv_cache ... ok
|
||||
test backtest::tests::instrument_metadata_cache_path_is_scoped_by_historical_window ... ok
|
||||
test backtest::tests::instrument_metadata_cache_path_uses_binary_format ... ok
|
||||
test backtest::tests::instrument_metadata_request_serializes_historical_source_window_when_present ... ok
|
||||
test backtest::tests::intraday_snapshot_binary_cache_rejects_metadata_mismatch ... ok
|
||||
test backtest::tests::intraday_snapshot_binary_cache_roundtrips_rows ... ok
|
||||
test backtest::tests::intraday_snapshot_cache_ignores_legacy_csv_cache ... ok
|
||||
test backtest::tests::intraday_snapshot_cache_path_uses_source_lake_provider_tag ... ok
|
||||
test backtest::tests::kcb_board_and_quantity_use_source_classification ... ok
|
||||
test backtest::tests::intraday_snapshot_seeds_execution_quotes_and_negative_gaps ... ok
|
||||
test backtest::tests::layered_execution_quote_loader_cache_preserves_shared_seed_and_isolates_overlay ... ok
|
||||
test backtest::tests::lazy_execution_quote_window_dates_use_trading_calendar_only ... ok
|
||||
test backtest::tests::lazy_execution_quote_window_backfill_keeps_future_missing_symbols ... ok
|
||||
test backtest::tests::load_strategy_spec_rejects_removed_compatibility_profile ... ok
|
||||
test backtest::tests::load_strategy_spec_rejects_removed_engine_profile_name ... ok
|
||||
test backtest::tests::malformed_execution_observations_are_errors_not_empty_successes ... ok
|
||||
test backtest::tests::malformed_versioned_factor_ref_fails_closed ... ok
|
||||
test backtest::tests::managed_source_rows_route_by_exact_table ... ok
|
||||
test backtest::tests::minute_bar_arrow_decode_accepts_bounded_multi_date_stream ... ok
|
||||
test backtest::tests::load_strategy_spec_accepts_snake_case_bundle_field ... ok
|
||||
test backtest::tests::minute_bar_arrow_decode_preserves_timestamp_symbol_order ... ok
|
||||
test backtest::tests::minute_bar_arrow_decode_rejects_retired_string_time_schema ... ok
|
||||
test backtest::tests::minute_bar_arrow_decode_rejects_duplicate_or_unordered_rows ... ok
|
||||
test backtest::tests::mixed_candidate_book_binds_complete_dates_raw_sources_and_resolved_order ... ok
|
||||
test backtest::tests::mixed_stock_futures_contract_requires_explicit_futures_cash ... ok
|
||||
test backtest::tests::normalize_symbol_maps_bse_exchange_to_beijing_board ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_matches_row_decoder_for_wide_sparse_null_and_signed_zero_values ... ok
|
||||
test backtest::tests::minute_bars_retries_transient_http_failure_and_preserves_caller_header ... ok
|
||||
test backtest::tests::partial_source_row_window_cache_crops_covering_segments_in_arrow ... ok
|
||||
test backtest::tests::partial_source_row_window_cache_ignores_all_market_cache_for_symbol_scope ... ok
|
||||
test backtest::tests::partial_source_row_window_cache_ignores_wide_projection_segment ... ok
|
||||
test backtest::tests::partial_source_row_window_cache_reports_only_missing_gaps ... ok
|
||||
test backtest::tests::progress_callback_is_forwarded_to_blocking_engine_thread ... ok
|
||||
test backtest::tests::published_factor_binding_normalizes_ref_and_builds_arrow_request ... ok
|
||||
test backtest::tests::query_scope_batch_preserves_authoritative_window_identities ... ok
|
||||
test backtest::tests::partial_source_row_window_cache_reuses_exact_projection_with_null_dynamic_columns ... ok
|
||||
test backtest::tests::query_scope_identity_reuses_physical_window_across_distinct_bundles ... ok
|
||||
test backtest::tests::real_source_quote_json_and_arrow_match_strict_reader ... ignored, requires a bounded real Source JSON/Arrow probe directory
|
||||
test backtest::tests::repeated_query_views_share_only_identical_execution_observations ... ok
|
||||
test backtest::tests::platform_quote_plan_uses_current_decision_date_source_rows ... ok
|
||||
test backtest::tests::query_scope_http_carries_rolling_dependencies_and_rejects_bad_contracts ... ok
|
||||
test backtest::tests::select_instrument_metadata_rejects_missing_symbols ... ok
|
||||
test backtest::tests::shared_base_source_projection_keeps_raw_adjustment_contract ... ok
|
||||
test backtest::tests::shared_base_source_rows_cache_is_enabled_by_default ... ok
|
||||
test backtest::tests::shared_base_source_rows_cache_is_only_for_unscoped_raw_rows ... ok
|
||||
test backtest::tests::reusable_source_row_window_cache_does_not_reuse_covering_cache ... ok
|
||||
test backtest::tests::sorted_source_row_validator_rejects_unordered_or_duplicate_keys ... ok
|
||||
test backtest::tests::source_cache_epoch_default_is_independent_from_quote_plan_epoch ... ok
|
||||
test backtest::tests::source_dependency_contract_invalidates_all_authoritative_cache_scopes ... ok
|
||||
test backtest::tests::segmented_source_row_cache_reads_only_uncovered_overlap_ranges ... ok
|
||||
test backtest::tests::short_range_source_query_window_is_explicit_and_keeps_long_ranges_bounded ... ok
|
||||
test backtest::tests::source_lake_contract_epoch_ignores_runtime_build_but_tracks_catalog ... ok
|
||||
test backtest::tests::source_lake_health_contract_epoch_ignores_runtime_build ... ok
|
||||
test backtest::tests::source_lake_arrow_request_retries_bad_gateway ... ok
|
||||
test backtest::tests::source_lake_busy_retry_honors_retryable_capacity_response ... ok
|
||||
test backtest::tests::source_lake_source_query_retries_are_bounded_and_transient_only ... ok
|
||||
test backtest::tests::source_lake_source_query_timeout_accepts_highmem_alias ... ok
|
||||
test backtest::tests::source_lake_run_waits_for_manifest_publication_and_holds_shared_lease ... ok
|
||||
test backtest::tests::source_row_arrow_cache_rejects_and_does_not_write_empty_rows ... ok
|
||||
test backtest::tests::source_lake_uses_wider_windows_for_narrow_queries ... ok
|
||||
test backtest::tests::source_query_concurrency_defaults_and_clamps ... ok
|
||||
test backtest::tests::source_row_arrow_decode_preserves_dynamic_projection_columns ... ok
|
||||
test backtest::tests::source_row_arrow_cache_rejects_duplicate_date_symbol ... ok
|
||||
test backtest::tests::source_row_arrow_dependency_contract_rejects_invalid_metadata ... ok
|
||||
test backtest::tests::source_row_arrow_extra_factor_helpers_preserve_projected_columns ... ok
|
||||
test backtest::tests::source_row_arrow_decode_keeps_compact_base_rows_without_extra_json ... ok
|
||||
test backtest::tests::source_row_arrow_view_keeps_mmap_alive_until_materialization ... ok
|
||||
test backtest::tests::source_row_arrow_view_cache_reuses_identity_and_invalidates_replacement ... ok
|
||||
test backtest::tests::source_row_cache_file_range_parses_source_cache_dates ... ok
|
||||
test backtest::tests::source_row_cache_candidate_scan_removes_previous_schema_files ... ok
|
||||
test backtest::tests::source_row_cache_directory_index_tracks_files_published_by_other_workers ... ok
|
||||
test backtest::tests::source_row_cache_path_hashes_long_projection_columns ... ok
|
||||
test backtest::tests::source_row_cache_path_scopes_explicit_source_symbols ... ok
|
||||
test backtest::tests::source_row_cache_lease_serializes_concurrent_misses ... ok
|
||||
test backtest::tests::source_row_cache_path_normalizes_projection_column_order ... ok
|
||||
test backtest::tests::source_row_cache_projection_check_requires_extra_factor_keys ... ok
|
||||
test backtest::tests::source_row_cache_projection_check_allows_source_lake_fixed_aliases ... ok
|
||||
test backtest::tests::source_row_cache_path_separates_projection_columns ... ok
|
||||
test backtest::tests::source_row_cache_rejects_legacy_raw_bincode_without_magic_header ... ok
|
||||
test backtest::tests::source_row_cache_segment_selection_prefers_small_complete_windows ... ok
|
||||
test backtest::tests::source_row_cache_segment_selection_rejects_incomplete_windows ... ok
|
||||
test backtest::tests::source_row_date_windows_keep_interior_buckets_stable_across_ranges ... ok
|
||||
test backtest::tests::source_row_date_windows_split_long_ranges ... ok
|
||||
test backtest::tests::source_row_cache_rejects_metadata_mismatch ... ok
|
||||
test backtest::tests::source_row_date_windows_stay_within_requested_range ... ok
|
||||
test backtest::tests::source_row_primary_keys_are_inline_without_growing_the_row_header ... ok
|
||||
test backtest::tests::source_row_cache_writes_arrow_file ... ok
|
||||
test backtest::tests::source_row_segment_boundary_validator_preserves_fail_closed_integrity ... ok
|
||||
test backtest::tests::source_row_symbol_normalization_preserves_canonical_and_maps_board ... ok
|
||||
test backtest::tests::source_row_request_cache_epoch_scopes_runtime_query_context ... ok
|
||||
test backtest::tests::source_row_symbol_scope_epoch_changes_with_fixed_signal_symbols ... ok
|
||||
test backtest::tests::source_rows_duplicate_date_symbol_is_rejected ... ok
|
||||
test backtest::tests::source_rows_request_keeps_rolling_enabled_for_rolling_projection_columns ... ok
|
||||
test backtest::tests::source_row_window_cache_scope_ignores_out_of_window_filter_dates ... ok
|
||||
test backtest::tests::source_rows_unique_date_symbol_passes_without_reordering ... ok
|
||||
test backtest::tests::source_semantic_change_invalidates_bundle_and_window_cache ... ok
|
||||
test backtest::tests::static_universe_include_is_an_explicit_source_row_scope ... ok
|
||||
test backtest::tests::stock_pool_screen_snapshot_uses_exact_selection_dates_and_full_symbol_history ... ok
|
||||
test backtest::tests::strategy_bundle_path_loads_nested_spec_and_signal_scope ... ok
|
||||
test backtest::tests::strategy_factor_overlay_only_queries_keys_and_missing_factor_fields ... ok
|
||||
test backtest::tests::strategy_factor_overlay_requires_exact_base_key_coverage ... ok
|
||||
test backtest::tests::strategy_factor_overlay_rejects_duplicate_and_unordered_keys ... ok
|
||||
test backtest::tests::strategy_factory_source_lake_provider_name_is_exact ... ok
|
||||
test backtest::tests::strategy_factory_source_lake_requires_url_and_matching_provider ... ok
|
||||
test backtest::tests::strategy_factory_source_lake_uses_arrow_sized_query_windows_by_default ... ok
|
||||
test backtest::tests::trade_display_metadata_cache_is_bundle_scoped_and_covers_missing_rows ... ok
|
||||
test backtest::tests::trade_display_metadata_cache_rejects_rows_outside_exact_request ... ok
|
||||
test backtest::tests::typed_equity_result_emits_service_schema ... ok
|
||||
test backtest::tests::trade_display_metadata_disk_cache_roundtrips_frozen_bundle_rows ... ok
|
||||
test backtest::tests::typed_order_result_keeps_zero_fill_orders ... ok
|
||||
test backtest::tests::typed_trade_result_preserves_sub_cent_execution_price ... ok
|
||||
test backtest::tests::validated_manifest_scope_reuses_cache_across_distinct_bundle_ids ... ok
|
||||
test backtest::tests::versioned_factor_binding_rejects_research_scope ... ok
|
||||
test backtest::tests::versioned_factor_binding_requires_declared_semantics ... ok
|
||||
test backtest::tests::versioned_factor_ref_requires_a_binding ... ok
|
||||
test backtest::typed_exit_dependency_tests::ordinary_strategy_partial_exit_also_contributes_its_window ... ok
|
||||
test backtest::typed_exit_dependency_tests::typed_stock_pool_exits_contribute_the_full_rolling_window ... ok
|
||||
test cache_directory_watch::tests::notifications_cover_publication_rename_delete_and_ignore_unrelated_files ... ok
|
||||
test compute_pool_tests::concurrent_embedded_runs_do_not_change_each_others_pool_budget ... ok
|
||||
test compute_pool_tests::current_thread_async_runtime_keeps_the_compute_context ... ok
|
||||
test backtest::tests::timed_quote_dataset_signature_includes_daily_selection_plan ... ok
|
||||
test decision_projection::tests::projection_metadata_alias_empty_fields_and_credential_paths_fail_closed ... ok
|
||||
test decision_projection::tests::projection_native_request_is_complete_and_never_truncated_to_top_n ... ok
|
||||
test decision_projection::tests::projection_response_is_bound_to_source_model_clock_and_exact_population ... ok
|
||||
test compute_pool_tests::embedded_runner_uses_its_declared_budget_and_preserves_results ... ok
|
||||
test manual_ledger::corporate_tests::actual_core_corporate_projection_is_preserved_in_the_shared_trade_row ... ok
|
||||
test result_store::tests::canonical_first_divergence_reports_json_pointer ... ok
|
||||
test result_store::tests::canonical_manifest_is_stable_and_transport_independent ... ok
|
||||
test manual_ledger::corporate_tests::corporate_adjustments_reject_forged_money_dates_and_reference_chains ... ok
|
||||
test backtest::tests::unused_daily_market_cap_prefilter_does_not_split_identical_input_cache ... ok
|
||||
test result_store::tests::lazy_registration_defers_payload_hashing_to_section_read ... ok
|
||||
test result_store::tests::fact_manifest_publish_is_idempotent_under_concurrency ... ok
|
||||
test result_store::tests::process_event_store_is_content_addressed_and_round_trips ... ok
|
||||
test result_store::tests::run_store_identity_keeps_different_data_epochs_separate ... ok
|
||||
test result_store::tests::run_store_is_content_addressed_and_round_trips ... ok
|
||||
test result_store::tests::manual_source_and_applications_are_both_hashed_and_persisted ... ok
|
||||
test result_store::tests::run_store_rejects_corrupt_manifest ... ok
|
||||
test result_store::tests::run_store_reads_only_date_overlapping_blocks ... ok
|
||||
test thread_cpu::tests::thread_cpu_clock_keeps_unavailable_or_cross_thread_samples_unknown ... ok
|
||||
test thread_cpu::tests::sleeping_time_is_not_reported_as_cpu_execution ... ok
|
||||
test backtest::source_rows_columnar::tests::columnar_cache_keeps_original_output_batch_boundaries ... ok
|
||||
|
||||
test result: ok. 475 passed; 0 failed; 9 ignored; 0 measured; 0 filtered out; finished in 4.31s
|
||||
|
||||
Running unittests src/main.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/fidc_backtest_runner-b8208f20c65c40bc)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/allocation_profile.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/allocation_profile-1ab90294b6f34b33)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running unittests examples/export_manual_corporate_evidence.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/examples/export_manual_corporate_evidence-400d24419df6725a)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running unittests src/main.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/fidc_backtest_service-e322f82562f9dee9)
|
||||
|
||||
running 142 tests
|
||||
test daily_patterns::tests::column_transport_is_lossless_and_rejects_ambiguous_rows ... ok
|
||||
test daily_patterns::tests::arrow_rejects_truncation_and_forged_lengths_before_decoding ... ok
|
||||
test event_stream::tests::effective_initial_cash_is_projected_without_guessing_or_losing_source_precision ... ok
|
||||
test daily_patterns::tests::daily_patterns_read_only_transport_binds_exact_input ... ok
|
||||
test daily_patterns::tests::numeric_mode_requires_the_explicit_read_only_context ... ok
|
||||
test event_stream::tests::ordered_batch_is_idempotent_and_changed_facts_do_not_replace_history ... ignored, requires local FIDC_EVENT_TEST_POSTGRES_DSN on the private fidc-auth-pg cluster
|
||||
test event_stream::tests::memory_tail_and_durable_prefix_replay_in_publish_order ... ok
|
||||
test daily_patterns::tests::arrow_uses_same_kernel_and_exact_byte_proof_with_partial_scope ... ok
|
||||
test event_stream::tests::live_sequence_recent_replay_and_concurrent_runs_remain_isolated ... ok
|
||||
test execution_ledger::tests::actual_http_routes_return_both_sources_and_precise_manual_values ... ok
|
||||
test execution_ledger::tests::dated_fact_store_rejects_reverse_order_before_projection ... ok
|
||||
test execution_ledger::tests::serve_browser_fixture ... ignored, starts a local GET-only API for browser acceptance; set FIDC_LEDGER_UI_QA_PORT explicitly
|
||||
test execution_ledger::tests::serve_stream_browser_fixture ... ignored, starts isolated real WebSocket/GET handlers; FIDC_LEDGER_STREAM_QA_PORT required
|
||||
test execution_ledger::tests::source_precision_ids_and_later_status_are_not_rewritten ... ok
|
||||
test execution_ledger::tests::streamed_manual_facts_equal_final_ledger_without_publishing_a_future_terminal ... ok
|
||||
test execution_ledger::tests::typed_accounting_rows_keep_origin_without_order_identity ... ok
|
||||
test execution_ledger::tests::performance_counts_both_origins_and_sums_authoritative_fees_exactly ... ok
|
||||
test factor_events::tests::market_history_binds_kernel_version_and_preserves_unknown_streak ... ok
|
||||
test factor_events::tests::no_account_or_execution_payload_is_accepted ... ok
|
||||
test factor_events::tests::rank_history_binds_exact_scope_and_input_hash ... ok
|
||||
test factor_events::tests::transport_preserves_input_identity_and_rejects_future_values ... ok
|
||||
test idempotency::tests::canonical_json_order_does_not_change_request_identity ... ok
|
||||
test idempotency::tests::identity_binds_owner_and_rejects_changed_request ... ok
|
||||
test idempotency::tests::malformed_keys_and_unbound_records_are_rejected ... ok
|
||||
test performance::tests::accounting_allocations_are_not_unidentified_exchange_fills ... ok
|
||||
test performance::tests::builds_daily_weight_diagnostics_from_persisted_holdings ... ok
|
||||
test performance::tests::counts_open_underwater_duration_from_the_latest_peak ... ok
|
||||
test performance::tests::does_not_fake_risk_ratios_for_one_period ... ok
|
||||
test performance::tests::emits_true_nav_and_multiple_recovery_episodes ... ok
|
||||
test performance::tests::explicit_frozen_initial_capital_survives_a_first_day_deposit ... ok
|
||||
test performance::tests::first_period_loss_has_an_initial_peak_episode_without_a_fake_date ... ok
|
||||
test performance::tests::missing_benchmark_does_not_compact_dates_or_invent_a_drawdown_path ... ok
|
||||
test performance::tests::missing_order_ids_and_fees_remain_unknown_not_zero_or_report_counts ... ok
|
||||
test performance::tests::order_metrics_count_identities_separately_from_reports_and_partial_fills ... ok
|
||||
test performance::tests::preserves_a_first_day_strategy_return_and_initial_capital ... ok
|
||||
test performance::tests::preserves_the_canonical_pre_period_benchmark_baseline ... ok
|
||||
test performance::tests::projects_all_canonical_risk_metrics_and_contract_metadata ... ok
|
||||
test performance::tests::separates_longest_underwater_episode_from_deepest_drawdown ... ok
|
||||
test performance::tests::zero_nav_is_a_full_loss_not_missing_data ... ok
|
||||
test performance::tests::zero_orders_are_known_but_conflicting_order_identity_is_not_a_count ... ok
|
||||
test result_analysis_tests::batched_history_bindings_preserve_validation_and_survive_cache_eviction ... ignored, requires isolated FIDC_BACKTEST_TEST_POSTGRES_DSN
|
||||
test factor_events::tests::catalog_validator_binds_the_complete_body_and_preserves_contract ... ok
|
||||
test result_analysis_tests::copied_real_result_stores_and_browser_fixture ... ignored, requires FIDC_RESULT_QA_FIXTURES containing copied immutable result stores
|
||||
test result_analysis_tests::failed_optional_classification_keeps_unmodified_facts ... ok
|
||||
test result_analysis_tests::historical_text_search_filters_before_count_and_pagination ... ignored, requires isolated FIDC_BACKTEST_TEST_POSTGRES_DSN
|
||||
test result_analysis_tests::history_filters_use_corrected_drawdown_and_never_treat_unknown_as_zero ... ok
|
||||
test result_analysis_tests::owned_analysis_reference_does_not_require_a_cached_run_binding ... ok
|
||||
test result_analysis_tests::projected_history_filter_sort_and_pagination_on_postgres ... ignored, requires FIDC_BACKTEST_TEST_POSTGRES_DSN; creates an isolated test schema
|
||||
test result_analysis_tests::sector_filter_cannot_disable_its_classification_dependency ... ok
|
||||
test risk_projection::tests::absent_original_rates_preserve_independent_metrics_without_network_or_zero_rate_claim ... ok
|
||||
test risk_projection::tests::canonical_null_overrides_all_legacy_aliases_but_real_zero_survives ... ok
|
||||
test risk_projection::tests::original_contract_reprojects_legacy_zero_and_preserves_cash_flow_facts ... ok
|
||||
test risk_projection::tests::unavailable_contract_does_not_relabel_legacy_zero_as_a_verified_statistic ... ok
|
||||
test stock_pool_candidates::tests::resolve_binds_input_and_preserves_empty_automatic_days ... ok
|
||||
test submission_pg_tests::atomic_submission_claim_replay_and_rollback_on_postgres ... ignored, requires FIDC_BACKTEST_TEST_POSTGRES_DSN and creates only an isolated schema
|
||||
test tests::backtest_create_body_limit_accepts_bounded_signal_replays ... ok
|
||||
test tests::backtest_event_stream_exposes_a_websocket_transport ... ok
|
||||
test tests::backtest_list_cache_key_separates_summary_only_runtime_shape ... ok
|
||||
test tests::backtest_list_filters_and_sorting_use_generated_summary ... ok
|
||||
test tests::backtest_list_reports_cache_hit_and_effective_page_limit ... ok
|
||||
test tests::backtest_list_summary_only_uses_compact_query ... ok
|
||||
test tests::backtest_performance_contract_records_total_and_runner_seconds ... ok
|
||||
test tests::backtest_run_compact_cache_key_separates_strategy_detail_shape ... ok
|
||||
test tests::backtest_runner_data_env_accepts_strategy_factory_source_lake_provider ... ok
|
||||
test tests::backtest_runner_data_env_infers_strategy_factory_source_lake_from_runtime_source_table ... ok
|
||||
test tests::backtest_runner_data_env_rejects_legacy_ficlaw_data_source_table ... ok
|
||||
test tests::backtest_runner_data_env_rejects_legacy_source_provider ... ok
|
||||
test tests::backtest_runner_data_env_rejects_non_source_lake_provider ... ok
|
||||
test tests::backtest_runner_data_env_rejects_source_lake_provider_aliases ... ok
|
||||
test tests::backtest_runner_data_env_requires_strategy_factory_source_lake_url ... ok
|
||||
test tests::backtest_runner_data_env_uses_strategy_factory_source_lake_only ... ok
|
||||
test tests::backtest_schema_initialization_never_deletes_run_data ... ok
|
||||
test tests::backtest_schema_materializes_implementation_identity_for_compact_reads ... ok
|
||||
test tests::build_run_payload_does_not_mark_empty_running_summary_as_available ... ok
|
||||
test tests::build_run_payload_exposes_only_canonical_strategy_detail ... ok
|
||||
test tests::build_run_payload_marks_failed_error_summary_as_available ... ok
|
||||
test tests::build_run_payload_marks_terminal_metric_summary_as_available ... ok
|
||||
test tests::build_run_summary_preview_preserves_strategy_runtime_rebalance_signal ... ok
|
||||
test tests::build_run_summary_preview_preserves_terminal_audit ... ok
|
||||
test tests::canonical_result_is_recovered_from_runner_summary ... ok
|
||||
test tests::capacity_override_is_frozen_once_without_changing_execution_risk ... ok
|
||||
test tests::clear_backtest_list_cache_clears_compact_run_cache ... ok
|
||||
test tests::compact_result_handler_uses_single_pg_lookup ... ok
|
||||
test tests::compact_summary_only_runtime_select_avoids_config_json ... ok
|
||||
test tests::compact_summary_sql_removes_heavy_result_artifacts ... ok
|
||||
test tests::daily_execution_matching_type_normalizes_next_open_aliases_and_rejects_other_aliases ... ok
|
||||
test tests::default_backtest_concurrency_scales_with_available_cpu_threads ... ok
|
||||
test tests::default_backtest_source_table_uses_strategy_factory_source_lake ... ok
|
||||
test tests::detail_rows_query_page_size_maps_to_offset_limit ... ok
|
||||
test tests::equity_rows_hide_diagnostics_unless_explicitly_requested ... ok
|
||||
test tests::execution_cost_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::execution_dynamic_slippage_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::execution_matching_type_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::execution_percent_slippage_alias_normalizes_to_price_ratio ... ok
|
||||
test tests::execution_rebalance_cash_mode_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::execution_risk_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::execution_risk_policy_rejects_conflicting_alias_values ... ok
|
||||
test tests::execution_risk_policy_rejects_non_object ... ok
|
||||
test tests::execution_risk_policy_rejects_unknown_fields_before_queueing ... ok
|
||||
test tests::execution_sell_then_buy_delay_slippage_requires_sell_then_buy_mode ... ok
|
||||
test tests::execution_slippage_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::execution_top_level_risk_enabled_aliases_are_frozen_into_strategy_spec ... ok
|
||||
test tests::explicit_cost_override_removes_old_aliases_and_preserves_zero_minimum ... ok
|
||||
test tests::history_query_cache_has_a_bounded_entry_budget ... ok
|
||||
test tests::history_search_is_bounded_and_retains_literal_characters ... ok
|
||||
test tests::implementation_identity_freezes_source_and_binary_generation ... ok
|
||||
test tests::manual_observations_are_run_scoped_and_private_in_the_runner_bundle ... ok
|
||||
test tests::minute_execution_matching_type_uses_minute_price_and_rejects_daily_modes ... ok
|
||||
test tests::persisted_runtime_projection_never_guesses_missing_frequency ... ok
|
||||
test tests::persisted_runtime_projection_retains_minute_mode_and_frozen_bundle ... ok
|
||||
test tests::process_event_projection_uses_stable_kind_and_filters ... ok
|
||||
test tests::projected_fact_block_cache_key_is_shared_across_run_manifests ... ok
|
||||
test tests::public_backtest_projection_removes_internal_paths_recursively ... ok
|
||||
test tests::public_event_message_removes_internal_paths ... ok
|
||||
test tests::public_result_store_manifest_hides_internal_fact_blocks ... ok
|
||||
test tests::public_run_projection_does_not_reintroduce_internal_paths ... ok
|
||||
test tests::result_artifact_cache_keeps_shared_fact_blocks_after_run_eviction ... ok
|
||||
test tests::result_artifact_cache_rejects_conflicting_reference_for_same_sha ... ok
|
||||
test tests::result_block_page_plan_reads_only_overlapping_blocks ... ok
|
||||
test tests::runner_strategy_spec_path_prefers_bundle ... ok
|
||||
test tests::runtime_frequency_normalizes_and_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::runtime_frozen_bundle_is_written_to_strategy_metadata_for_runner ... ok
|
||||
test tests::runtime_projection_migration_preserves_frozen_config ... ok
|
||||
test tests::runtime_symbols_override_strategy_spec_for_runner ... ok
|
||||
test tests::source_lake_preflight_rejects_unsupported_minute_cash_mode ... ok
|
||||
test tests::source_lake_run_contract_preflight_freezes_runtime_and_execution ... ok
|
||||
test tests::source_lake_runtime_requires_frozen_backtest_bundle_contract ... ok
|
||||
test tests::start_backtest_run_rejects_unready_data_source_before_persistence ... ok
|
||||
test tests::startup_reconciliation_closes_orphaned_runs_and_records_audit_event ... ok
|
||||
test tests::strategy_detail_keeps_strategy_body_without_full_config ... ok
|
||||
test tests::strategy_detail_returns_none_without_strategy_body ... ok
|
||||
test tests::strategy_expression_preflight_rejects_invalid_dsl_before_data_load ... ok
|
||||
test tests::summary_list_projection_keeps_table_metrics_and_removes_detail_payloads ... ok
|
||||
test tests::terminal_audit_accepts_deferred_intents_and_checks_the_retained_count ... ok
|
||||
test tests::terminal_audit_contract_rejects_missing_or_inconsistent_state ... ok
|
||||
test tests::top_level_limits_replace_all_nested_execution_aliases_before_preflight ... ok
|
||||
test tests::typed_holding_projection_derives_weight_and_final_snapshot ... ok
|
||||
test tests::typed_result_rows_filter_and_paginate_without_json_table_roundtrip ... ok
|
||||
test tests::typed_risk_filter_applies_execution_fields ... ok
|
||||
test event_stream::tests::replay_buffer_is_bounded_and_completed_streams_are_evicted_without_closing_subscribers ... ok
|
||||
test result_analysis_tests::complete_performance_and_paged_equity_share_corrected_drawdown ... ok
|
||||
test execution_ledger::tests::combined_pages_filter_before_count_and_keep_native_pages_bounded ... ok
|
||||
|
||||
test result: ok. 134 passed; 0 failed; 8 ignored; 0 measured; 0 filtered out; finished in 9.37s
|
||||
|
||||
Running unittests examples/risk_projection_readonly.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/examples/risk_projection_readonly-5f8d5aa4ff515b03)
|
||||
|
||||
running 4 tests
|
||||
test risk_projection::tests::canonical_null_overrides_all_legacy_aliases_but_real_zero_survives ... ok
|
||||
test risk_projection::tests::absent_original_rates_preserve_independent_metrics_without_network_or_zero_rate_claim ... ok
|
||||
test risk_projection::tests::unavailable_contract_does_not_relabel_legacy_zero_as_a_verified_statistic ... ok
|
||||
test risk_projection::tests::original_contract_reprojects_legacy_zero_and_preserves_cash_flow_facts ... ok
|
||||
|
||||
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running unittests examples/stock_pool_contract_probe.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/examples/stock_pool_contract_probe-e8a8af6fd36ebc70)
|
||||
|
||||
running 1 test
|
||||
test stock_pool_candidates::tests::resolve_binds_input_and_preserves_empty_automatic_days ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
Compiling proc-macro2 v1.0.106
|
||||
Compiling unicode-ident v1.0.24
|
||||
Compiling quote v1.0.45
|
||||
Compiling libc v0.2.185
|
||||
Compiling cfg-if v1.0.4
|
||||
Compiling bytes v1.11.1
|
||||
Compiling pin-project-lite v0.2.17
|
||||
Compiling portable-atomic v1.13.1
|
||||
Compiling version_check v0.9.5
|
||||
Compiling autocfg v1.5.0
|
||||
Compiling syn v2.0.117
|
||||
Compiling typenum v1.20.0
|
||||
Compiling libm v0.2.16
|
||||
Compiling num-traits v0.2.19
|
||||
Compiling once_cell v1.21.4
|
||||
Compiling serde_core v1.0.228
|
||||
Compiling zerocopy v0.8.48
|
||||
Compiling errno v0.3.14
|
||||
Compiling memchr v2.8.0
|
||||
Compiling futures-core v0.3.32
|
||||
Compiling signal-hook-registry v1.4.8
|
||||
Compiling socket2 v0.6.3
|
||||
Compiling mio v1.2.0
|
||||
Compiling itoa v1.0.18
|
||||
Compiling smallvec v1.15.1
|
||||
Compiling serde v1.0.228
|
||||
Compiling log v0.4.29
|
||||
Compiling shlex v1.3.0
|
||||
Compiling find-msvc-tools v0.1.9
|
||||
Compiling futures-sink v0.3.32
|
||||
Compiling stable_deref_trait v1.2.1
|
||||
Compiling generic-array v0.14.7
|
||||
Compiling subtle v2.6.1
|
||||
Compiling cc v1.2.60
|
||||
Compiling futures-channel v0.3.32
|
||||
Compiling tracing-core v0.1.36
|
||||
Compiling getrandom v0.3.4
|
||||
Compiling synstructure v0.13.2
|
||||
Compiling http v1.4.0
|
||||
Compiling slab v0.4.12
|
||||
Compiling futures-io v0.3.32
|
||||
Compiling futures-task v0.3.32
|
||||
Compiling crypto-common v0.1.7
|
||||
Compiling block-buffer v0.10.4
|
||||
Compiling getrandom v0.2.17
|
||||
Compiling httparse v1.10.1
|
||||
Compiling const-oid v0.9.6
|
||||
Compiling percent-encoding v2.3.2
|
||||
Compiling crunchy v0.2.4
|
||||
Compiling digest v0.10.7
|
||||
Compiling http-body v1.0.1
|
||||
Compiling ring v0.17.14
|
||||
Compiling zerocopy-derive v0.8.48
|
||||
Compiling tokio-macros v2.7.0
|
||||
Compiling zerofrom-derive v0.1.7
|
||||
Compiling yoke-derive v0.8.2
|
||||
Compiling tokio v1.52.1
|
||||
Compiling zerofrom v0.1.7
|
||||
Compiling serde_derive v1.0.228
|
||||
Compiling yoke v0.8.2
|
||||
Compiling zerovec-derive v0.11.3
|
||||
Compiling zerovec v0.11.6
|
||||
Compiling tracing-attributes v0.1.31
|
||||
Compiling futures-macro v0.3.32
|
||||
Compiling displaydoc v0.2.5
|
||||
Compiling futures-util v0.3.32
|
||||
Compiling tracing v0.1.44
|
||||
Compiling litemap v0.8.2
|
||||
Compiling tinystr v0.8.3
|
||||
Compiling writeable v0.6.3
|
||||
Compiling tiny-keccak v2.0.2
|
||||
Compiling base64 v0.22.1
|
||||
Compiling icu_locale_core v2.2.0
|
||||
Compiling zerotrie v0.2.4
|
||||
Compiling potential_utf v0.1.5
|
||||
Compiling icu_properties_data v2.2.0
|
||||
Compiling icu_normalizer_data v2.2.0
|
||||
Compiling utf8_iter v1.0.4
|
||||
Compiling zeroize v1.8.2
|
||||
Compiling tower-service v0.3.3
|
||||
Compiling rustls-pki-types v1.14.0
|
||||
Compiling icu_collections v2.2.0
|
||||
Compiling icu_provider v2.2.0
|
||||
Compiling ppv-lite86 v0.2.21
|
||||
Compiling num-integer v0.1.46
|
||||
Compiling rand_core v0.10.1
|
||||
Compiling bitflags v2.11.1
|
||||
Compiling getrandom v0.4.2
|
||||
Compiling try-lock v0.2.5
|
||||
Compiling crc32fast v1.5.0
|
||||
Compiling cpufeatures v0.2.17
|
||||
Compiling untrusted v0.9.0
|
||||
Compiling const-random-macro v0.1.16
|
||||
Compiling want v0.3.1
|
||||
Compiling sync_wrapper v1.0.2
|
||||
Compiling ahash v0.8.12
|
||||
Compiling crossbeam-utils v0.8.21
|
||||
Compiling simd-adler32 v0.3.9
|
||||
Compiling rustls v0.23.38
|
||||
Compiling tower-layer v0.3.3
|
||||
Compiling adler2 v2.0.1
|
||||
Compiling ryu v1.0.23
|
||||
Compiling httpdate v1.0.3
|
||||
Compiling serde_json v1.0.145
|
||||
Compiling atomic-waker v1.1.2
|
||||
Compiling iana-time-zone v0.1.65
|
||||
Compiling hyper v1.9.0
|
||||
Compiling miniz_oxide v0.8.9
|
||||
Compiling chrono v0.4.44
|
||||
Compiling icu_properties v2.2.0
|
||||
Compiling rustls-webpki v0.103.13
|
||||
Compiling const-random v0.1.18
|
||||
Compiling icu_normalizer v2.2.0
|
||||
Compiling num-bigint v0.4.6
|
||||
Compiling form_urlencoded v1.2.2
|
||||
Compiling thiserror v2.0.18
|
||||
Compiling ipnet v2.12.0
|
||||
Compiling idna_adapter v1.2.1
|
||||
Compiling flate2 v1.1.9
|
||||
Compiling hyper-util v0.1.20
|
||||
Compiling num-rational v0.4.2
|
||||
Compiling tower v0.5.3
|
||||
Compiling num-iter v0.1.45
|
||||
Compiling tokio-util v0.7.18
|
||||
Compiling http-body-util v0.1.3
|
||||
Compiling thiserror-impl v2.0.18
|
||||
Compiling num-complex v0.4.6
|
||||
Compiling compression-core v0.4.32
|
||||
Compiling rustversion v1.0.22
|
||||
Compiling hashbrown v0.16.1
|
||||
Compiling num v0.4.3
|
||||
Compiling compression-codecs v0.4.38
|
||||
Compiling crossbeam-epoch v0.9.18
|
||||
Compiling idna v1.1.0
|
||||
Compiling serde_urlencoded v0.7.1
|
||||
Compiling sha1 v0.10.6
|
||||
Compiling tokio-rustls v0.26.4
|
||||
Compiling webpki-roots v1.0.7
|
||||
Compiling half v2.7.1
|
||||
Compiling smartstring v1.0.1
|
||||
Compiling rayon-core v1.13.0
|
||||
Compiling zmij v1.0.23
|
||||
Compiling static_assertions v1.1.0
|
||||
Compiling iri-string v0.7.12
|
||||
Compiling semver v1.0.28
|
||||
Compiling lazy_static v1.5.0
|
||||
Compiling arrow-buffer v56.2.1
|
||||
Compiling rustc_version v0.4.1
|
||||
Compiling hyper-rustls v0.27.9
|
||||
Compiling url v2.5.8
|
||||
Compiling crossbeam-deque v0.8.6
|
||||
Compiling async-compression v0.4.42
|
||||
Compiling tower-http v0.6.8
|
||||
Compiling sha2 v0.10.9
|
||||
Compiling async-trait v0.1.89
|
||||
Compiling rand_core v0.6.4
|
||||
Compiling hybrid-array v0.4.10
|
||||
Compiling rust_decimal v1.39.0
|
||||
Compiling anyhow v1.0.102
|
||||
Compiling utf8parse v0.2.2
|
||||
Compiling arrow-schema v56.2.1
|
||||
Compiling regex-syntax v0.8.10
|
||||
Compiling anstyle-parse v1.0.0
|
||||
Compiling reqwest v0.12.24
|
||||
Compiling rand_chacha v0.3.1
|
||||
Compiling arrow-data v56.2.1
|
||||
Compiling castaway v0.2.4
|
||||
Compiling regex-automata v0.4.14
|
||||
Compiling flatbuffers v25.12.19
|
||||
Compiling rhai_codegen v3.1.0
|
||||
Compiling tikv-jemalloc-sys v0.7.1+5.3.1-0-g81034ce1f1373e37dc865038e1bc8eeecf559ce8
|
||||
Compiling is_terminal_polyfill v1.70.2
|
||||
Compiling arrayvec v0.7.8
|
||||
Compiling anstyle v1.0.14
|
||||
Compiling colorchoice v1.0.5
|
||||
Compiling cmov v0.5.3
|
||||
Compiling either v1.15.0
|
||||
Compiling equivalent v1.0.2
|
||||
Compiling anstyle-query v1.1.5
|
||||
Compiling ta-lib-dispatch v0.1.2 (https://github.com/TA-Lib/ta-lib.git?rev=dd5a90259a3f9e04e2da9f38bf0719a841b40108#dd5a9025)
|
||||
Compiling thin-vec v0.2.16
|
||||
Compiling anstream v1.0.0
|
||||
Compiling ta-lib v0.8.1 (https://github.com/TA-Lib/ta-lib.git?rev=dd5a90259a3f9e04e2da9f38bf0719a841b40108#dd5a9025)
|
||||
Compiling rhai v1.23.6
|
||||
Compiling matchers v0.2.0
|
||||
Compiling indexmap v2.11.4
|
||||
Compiling rayon v1.12.0
|
||||
Compiling ctutils v0.4.2
|
||||
Compiling compact_str v0.10.0
|
||||
Compiling arrow-array v56.2.1
|
||||
Compiling rand v0.8.6
|
||||
Compiling crypto-common v0.2.1
|
||||
Compiling block-buffer v0.12.0
|
||||
Compiling sharded-slab v0.1.7
|
||||
Compiling hmac v0.12.1
|
||||
Compiling tracing-log v0.2.0
|
||||
Compiling thread_local v1.1.9
|
||||
Compiling nu-ansi-term v0.50.3
|
||||
Compiling const-oid v0.10.2
|
||||
Compiling fastrand v2.4.1
|
||||
Compiling clap_lex v1.1.0
|
||||
Compiling twox-hash v2.1.3
|
||||
Compiling hex v0.4.3
|
||||
Compiling strsim v0.11.1
|
||||
Compiling home v0.5.12
|
||||
Compiling heck v0.5.0
|
||||
Compiling clap_derive v4.6.1
|
||||
Compiling reqsign v0.16.5
|
||||
Compiling clap_builder v4.6.0
|
||||
Compiling lz4_flex v0.11.6
|
||||
Compiling backon v1.6.0
|
||||
Compiling digest v0.11.2
|
||||
Compiling tracing-subscriber v0.3.23
|
||||
Compiling fidc-core v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1fa73bb/fidc-backtest-engine/crates/fidc-core)
|
||||
Compiling arrow-select v56.2.1
|
||||
Compiling uuid v1.23.1
|
||||
Compiling futures v0.3.32
|
||||
Compiling quick-xml v0.36.2
|
||||
Compiling md-5 v0.10.6
|
||||
Compiling rmp v0.8.15
|
||||
Compiling flagset v0.4.7
|
||||
Compiling dotenvy v0.15.7
|
||||
Compiling cpufeatures v0.3.0
|
||||
Compiling tinyvec_macros v0.1.1
|
||||
Compiling tinyvec v1.11.0
|
||||
Compiling fidc-common v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1fa73bb/fidc-backtest-service/crates/fidc-common)
|
||||
Compiling opendal v0.50.2
|
||||
Compiling rmp-serde v1.3.1
|
||||
Compiling arrow-ipc v56.2.1
|
||||
Compiling clap v4.6.1
|
||||
Compiling bincode v1.3.3
|
||||
Compiling memmap2 v0.9.11
|
||||
Compiling unicode-normalization v0.1.25
|
||||
Compiling chacha20 v0.10.0
|
||||
Compiling rand_core v0.9.5
|
||||
Compiling unicode-properties v0.1.4
|
||||
Compiling parking_lot_core v0.9.12
|
||||
Compiling unicode-bidi v0.3.18
|
||||
Compiling stringprep v0.1.5
|
||||
Compiling rand_chacha v0.9.0
|
||||
Compiling rand v0.10.1
|
||||
Compiling sha2 v0.11.0
|
||||
Compiling hmac v0.13.0
|
||||
Compiling md-5 v0.11.0
|
||||
Compiling fallible-iterator v0.2.0
|
||||
Compiling scopeguard v1.2.0
|
||||
Compiling byteorder v1.5.0
|
||||
Compiling siphasher v1.0.2
|
||||
Compiling phf_shared v0.13.1
|
||||
Compiling postgres-protocol v0.6.11
|
||||
Compiling lock_api v0.4.14
|
||||
Compiling rand v0.9.4
|
||||
Compiling data-encoding v2.11.1
|
||||
Compiling parking_lot v0.12.5
|
||||
Compiling tungstenite v0.29.0
|
||||
Compiling postgres-types v0.2.13
|
||||
Compiling phf v0.13.1
|
||||
Compiling deadpool-runtime v0.1.4
|
||||
Compiling num_cpus v1.17.0
|
||||
Compiling whoami v2.1.1
|
||||
Compiling mime v0.3.17
|
||||
Compiling axum-core v0.5.6
|
||||
Compiling tokio-postgres v0.7.17
|
||||
Compiling fidc-signal-client v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1fa73bb/fidc-backtest-engine/crates/fidc-signal-client)
|
||||
Compiling deadpool v0.12.3
|
||||
Compiling tokio-tungstenite v0.29.0
|
||||
Compiling async-stream-impl v0.3.6
|
||||
Compiling serde_path_to_error v0.1.20
|
||||
Compiling matchit v0.8.4
|
||||
Compiling async-stream v0.3.6
|
||||
Compiling axum v0.8.9
|
||||
Compiling deadpool-postgres v0.14.1
|
||||
Compiling tikv-jemallocator v0.7.0
|
||||
Compiling fidc-backtest-runner v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1fa73bb/fidc-backtest-service/services/backtest_runner)
|
||||
error[E0425]: cannot find function `decode_source_lake_corporate_actions_arrow` in this scope
|
||||
--> services/backtest_runner/src/backtest.rs:25124:20
|
||||
|
|
||||
25124 | let rows = decode_source_lake_corporate_actions_arrow(&body).unwrap();
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
...
|
||||
39780 | / fn decode_source_lake_source_rows_arrow(
|
||||
39781 | | body: &[u8],
|
||||
39782 | | validate_adjustment_factor: bool,
|
||||
39783 | | ) -> Result<Vec<SourceRowRecord>> {
|
||||
... |
|
||||
39801 | | Ok(rows)
|
||||
39802 | | }
|
||||
| |_- similarly named function `decode_source_lake_source_rows_arrow` defined here
|
||||
|
|
||||
help: a function with a similar name exists
|
||||
|
|
||||
25124 - let rows = decode_source_lake_corporate_actions_arrow(&body).unwrap();
|
||||
25124 + let rows = decode_source_lake_source_rows_arrow(&body).unwrap();
|
||||
|
|
||||
help: consider importing this function
|
||||
|
|
||||
20351 + use crate::backtest::decode_source_lake_corporate_actions_arrow;
|
||||
|
|
||||
|
||||
For more information about this error, try `rustc --explain E0425`.
|
||||
error: could not compile `fidc-backtest-runner` (lib test) due to 1 previous error
|
||||
warning: build failed, waiting for other jobs to finish...
|
||||
@@ -0,0 +1,678 @@
|
||||
Compiling fidc-core v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ce46c13/fidc-backtest-engine/crates/fidc-core)
|
||||
Compiling fidc-common v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ce46c13/fidc-backtest-service/crates/fidc-common)
|
||||
Compiling fidc-signal-client v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ce46c13/fidc-backtest-engine/crates/fidc-signal-client)
|
||||
Compiling fidc-backtest-runner v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ce46c13/fidc-backtest-service/services/backtest_runner)
|
||||
Compiling fidc-backtest-service v0.1.0 (/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-ce46c13/fidc-backtest-service/services/api)
|
||||
Finished `test` profile [unoptimized] target(s) in 41.16s
|
||||
Running unittests src/lib.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/fidc_backtest_runner-01cc3db11a686fbb)
|
||||
|
||||
running 483 tests
|
||||
test backtest::columnar_factor_overlay_tests::cache_range_rejects_date_disorder_outside_the_binary_search_slice ... ok
|
||||
test backtest::columnar_factor_overlay_tests::cache_range_rejects_symbol_disorder_outside_the_requested_date ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_real_cache_value_equivalence ... ignored, requires a frozen private Boris real-data cache directory on 177
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_missing_or_stale_cache_does_not_mutate_base ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_duplicate_window_boundary ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_duplicate_order_and_count_mismatches ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_market_projection_and_invalid_cached_column_type ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_rejects_late_key_mismatch_before_any_base_mutation ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_replaced_file_invalidates_view_without_mutating_pinned_input ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_unknown_keys_are_not_hidden_as_empty_values ... ok
|
||||
test backtest::columnar_factor_overlay_tests::row_visitor_preserves_selected_offsets_and_late_validation_errors ... ok
|
||||
test backtest::compact_storage_tests::batch_bound_factor_columns_preserve_types_nulls_and_nonfinite_values ... ok
|
||||
test backtest::columnar_factor_overlay_tests::valid_cache_reuses_its_key_index_and_failed_indexes_are_not_published ... ok
|
||||
test backtest::compact_storage_tests::inline_symbols_and_factor_maps_preserve_binary_and_json_contracts ... ok
|
||||
test backtest::day_snapshot_builder::tests::a_failed_builder_cannot_publish_earlier_valid_rows ... ok
|
||||
test backtest::day_snapshot_builder::tests::factor_reservation_keeps_missing_and_overwrite_semantics ... ok
|
||||
test backtest::day_snapshot_builder::tests::late_source_validation_failure_invalidates_the_streamed_day ... ok
|
||||
test backtest::equity_lifecycle_metadata_tests::empty_quotes_require_full_interval_lifecycle_proof_not_a_latest_status ... ok
|
||||
test backtest::equity_lifecycle_metadata_tests::lifecycle_cash_days_keep_real_benchmark_without_fabricated_security_rows ... ok
|
||||
test backtest::equity_lifecycle_metadata_tests::missing_identity_and_dates_do_not_become_active_securities ... ok
|
||||
test backtest::execution_config_tests::capacity_preflight_requires_explicit_daily_assumptions_without_changing_risk_switches ... ok
|
||||
test backtest::execution_config_tests::component_execution_quotes_seed_uses_latest_intraday_quote_before_target_time ... ok
|
||||
test backtest::execution_config_tests::current_close_respects_explicitly_disabled_liquidity_limit ... ok
|
||||
test backtest::execution_config_tests::daily_current_close_preserves_the_strategy_decision_clock ... ok
|
||||
test backtest::execution_config_tests::daily_execution_price_request_serializes_date_symbol_filter_scope ... ok
|
||||
test backtest::execution_config_tests::daily_execution_projection_keeps_benchmark_narrow_and_trade_risk_complete ... ok
|
||||
test backtest::execution_config_tests::data_intraday_snapshot_time_is_strategy_spec_driven ... ok
|
||||
test backtest::execution_config_tests::dataset_signature_rejects_source_lake_without_frozen_backtest_bundle ... ok
|
||||
test backtest::day_snapshot_builder::tests::arrow_rows_build_the_same_final_snapshots_without_a_row_collection ... ok
|
||||
test backtest::execution_config_tests::dataset_signature_scopes_dynamic_model_score_source_rows ... ok
|
||||
test backtest::execution_config_tests::default_matching_type_follows_backtest_frequency ... ok
|
||||
test backtest::execution_config_tests::dataset_signature_scopes_backtest_data_bundle_source_rows ... ok
|
||||
test backtest::execution_config_tests::dynamic_model_score_execution_quotes_are_lazy_by_default ... ok
|
||||
test backtest::execution_config_tests::dynamic_model_score_rank_does_not_extract_fixed_signal_symbols ... ok
|
||||
test backtest::execution_config_tests::execution_quote_prefilter_keeps_previous_factor_rows_in_current_band ... ok
|
||||
test backtest::execution_config_tests::execution_quote_prefilter_prefers_runtime_market_cap_slope ... ok
|
||||
test backtest::execution_config_tests::execution_quote_query_times_follow_strategy_trade_times ... ok
|
||||
test backtest::execution_config_tests::execution_quote_prefilter_keeps_candidates_beyond_legacy_limit ... ok
|
||||
test backtest::execution_config_tests::execution_top_level_fee_overrides_nested_risk_policy_cost ... ok
|
||||
test backtest::execution_config_tests::execution_top_level_volume_percent_overrides_nested_risk_policy ... ok
|
||||
test backtest::execution_config_tests::extract_score_context_does_not_extract_fixed_signal_symbols ... ok
|
||||
test backtest::execution_config_tests::factor_keys_use_compact_storage_without_changing_names ... ok
|
||||
test backtest::execution_config_tests::fixed_signal_model_score_scope_does_not_validate_candidate_limit ... ok
|
||||
test backtest::execution_config_tests::fixed_signal_symbol_scope_preserves_explicit_empty_targets ... ok
|
||||
test backtest::execution_config_tests::full_minute_event_stream_uses_subscriptions_instead_of_point_snapshot ... ok
|
||||
test backtest::execution_config_tests::minute_session_bounds_extend_only_after_post_close_rule_effective_date ... ok
|
||||
test backtest::execution_config_tests::model_score_source_rows_scope_evidence_verifies_candidate_limit ... ok
|
||||
test backtest::execution_config_tests::model_score_source_rows_scope_rejects_unbounded_rows ... ok
|
||||
test backtest::execution_config_tests::native_next_open_pool_loads_causal_quotes_without_exposing_them_to_selection ... ok
|
||||
test backtest::execution_config_tests::narrow_overlay_projection_does_not_retain_a_wide_value_buffer ... ok
|
||||
test backtest::execution_config_tests::owned_date_group_producers_drop_every_input_exactly_once_on_failure ... ok
|
||||
test backtest::execution_config_tests::owned_trade_day_preparation_matches_borrowed_rows ... ok
|
||||
test backtest::execution_config_tests::parse_matching_type_limits_global_backtest_matching_modes ... ok
|
||||
test backtest::execution_config_tests::parse_slippage_model_supports_explicit_historical_model ... ok
|
||||
test backtest::execution_config_tests::parse_slippage_model_supports_limit_price ... ok
|
||||
test backtest::execution_config_tests::platform_daily_factor_strategy_uses_lazy_execution_quotes_by_default ... ok
|
||||
test backtest::execution_config_tests::platform_execution_quote_carry_can_include_recent_candidates ... ok
|
||||
test backtest::execution_config_tests::platform_execution_quote_carry_does_not_let_candidates_evict_recent_orders ... ok
|
||||
test backtest::execution_config_tests::platform_execution_quote_carry_keeps_today_candidates_without_candidate_carry ... ok
|
||||
test backtest::execution_config_tests::platform_execution_quote_carry_prefers_recent_candidates_before_today_candidates ... ok
|
||||
test backtest::execution_config_tests::platform_execution_quote_carry_prioritizes_order_symbols ... ok
|
||||
test backtest::execution_config_tests::platform_execution_quote_default_carry_keeps_large_recent_candidate_set ... ok
|
||||
test backtest::execution_config_tests::platform_execution_route_requires_runtime_contract ... ok
|
||||
test backtest::execution_config_tests::platform_runtime_snapshot_preserves_typed_time_in_force ... ok
|
||||
test backtest::execution_config_tests::parallel_owned_date_groups_keep_exact_rows_and_owned_factor_allocations ... ok
|
||||
test backtest::execution_config_tests::platform_strategy_config_rejects_loose_value_budget_without_panicking ... ok
|
||||
test backtest::execution_config_tests::platform_strategy_config_uses_same_intraday_time_as_broker ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_candidate_market_cap_falls_back_to_total_market_cap ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_daily_selection_price_uses_close_without_limit_allow_flags ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_does_not_build_execution_quote_from_daily_fallback ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_keeps_allow_buy_when_missing_field_is_not_buy_relevant ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_keeps_daily_volume_when_snapshot_has_intraday_volume ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_keeps_external_signal_warmup_day ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_keeps_missing_risk_facts_when_runtime_filters_are_disabled ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_keeps_st_and_star_st_flags_independent ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_preserves_missing_daily_volume_contract ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_preserves_zero_ask_from_intraday_snapshot ... ok
|
||||
test backtest::execution_config_tests::prepare_trade_day_records_all_missing_risk_facts_independent_of_policy ... ok
|
||||
test backtest::execution_config_tests::raw_yuan_market_cap_normalizes_to_100m_units ... ok
|
||||
test backtest::execution_config_tests::resolve_dividend_reinvestment_defaults_false_and_reads_engine_flag ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_does_not_allow_overspending_value_budget ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_enables_strict_value_budget_by_default ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_keeps_explicit_liquidity_quote_limit ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_distinguishes_timed_daily_current_close ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_keeps_explicit_same_day_buy_mark_override ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_keeps_explicit_snapshot_volume_limit ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_keeps_selection_risk_off_by_default ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_keeps_volume_limit_for_snapshot_minute_last_by_default ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_maps_daily_next_open ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_maps_minute_last_to_minute_price ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_maps_rebalance_cash_mode_and_forces_minute ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_preserves_zero_minimum_commission ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_reads_nested_risk_policy ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_rejects_delay_slippage_outside_sell_then_buy ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_rejects_vwap_as_global_matching_type ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_uses_explicit_market_rules_without_profile ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_uses_latest_trade_time_as_main_schedule ... ok
|
||||
test backtest::execution_config_tests::resolve_execution_config_uses_runtime_schedule_time_for_minute_execution ... ok
|
||||
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_preserves_static_universe_contract ... ok
|
||||
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_preserves_weak_market_threshold ... ok
|
||||
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_delayed_limit_open_exit ... ok
|
||||
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_release_slot_on_exit_signal ... ok
|
||||
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_trading_schedule ... ok
|
||||
test backtest::execution_config_tests::resolve_platform_expr_strategy_config_reads_trading_stage ... ok
|
||||
test backtest::execution_config_tests::retired_slippage_and_invalid_coefficients_never_become_no_slippage ... ok
|
||||
test backtest::execution_config_tests::risk_policy_alias_normalization_preserves_matching_values ... ok
|
||||
test backtest::execution_config_tests::risk_policy_alias_normalization_rejects_conflicting_values ... ok
|
||||
test backtest::execution_config_tests::runtime_selection_intraday_quote_detection_reads_quote_fields_only ... ok
|
||||
test backtest::execution_config_tests::reused_candidate_plan_panel_matches_full_intraday_market_materialization ... ok
|
||||
test backtest::execution_config_tests::sorted_overlay_keeps_unchanged_zero_bits_and_counts_only_changed_rows ... ok
|
||||
test backtest::execution_config_tests::sorted_overlay_transfers_owned_factor_storage_without_changing_bits ... ok
|
||||
test backtest::execution_config_tests::sorted_source_row_insertion_preserves_existing_duplicate_order ... ok
|
||||
test backtest::execution_config_tests::sorted_sparse_overlay_merge_falls_back_for_duplicate_keys ... ok
|
||||
test backtest::execution_config_tests::sorted_sparse_overlay_merge_preserves_order_and_unmatched_rows ... ok
|
||||
test backtest::execution_config_tests::source_lake_backtest_bundle_context_requires_complete_metadata ... ok
|
||||
test backtest::execution_config_tests::source_lake_execution_price_requests_keep_compact_schema ... ok
|
||||
test backtest::execution_config_tests::source_lake_market_cap_keeps_current_decision_date_value ... ok
|
||||
test backtest::execution_config_tests::source_lake_market_cap_uses_current_decision_date_snapshot ... ok
|
||||
test backtest::execution_config_tests::source_lake_model_score_context_allows_plain_rotation_without_model_score ... ok
|
||||
test backtest::execution_config_tests::source_lake_model_score_context_accepts_complete_inferred_scope ... ok
|
||||
test backtest::execution_config_tests::source_lake_model_score_context_rejects_dynamic_mode_without_source_label_or_scope ... ok
|
||||
test backtest::execution_config_tests::source_lake_model_score_context_rejects_partial_scope ... ok
|
||||
test backtest::execution_config_tests::source_lake_model_score_context_rejects_sparse_ref_without_scope ... ok
|
||||
test backtest::execution_config_tests::source_lake_request_keeps_dynamic_model_score_as_all_market_with_score_context ... ok
|
||||
test backtest::execution_config_tests::source_lake_request_keeps_explicit_empty_symbols_empty ... ok
|
||||
test backtest::execution_config_tests::source_lake_request_scopes_fixed_signal_symbols_and_keeps_compact_schema ... ok
|
||||
test backtest::execution_config_tests::source_lake_request_serializes_date_symbol_filter_scope ... ok
|
||||
test backtest::execution_config_tests::source_lake_source_rows_uses_current_row_decision_safe_prev_factor_mode ... ok
|
||||
test backtest::execution_config_tests::source_row_adjustment_factor_decode_accepts_positive_finite_values ... ok
|
||||
test backtest::execution_config_tests::source_row_adjustment_factor_decode_allows_paused_rows_without_close ... ok
|
||||
test backtest::execution_config_tests::source_row_adjustment_factor_decode_rejects_missing_or_invalid_values ... ok
|
||||
test backtest::execution_config_tests::source_row_adjustment_factor_decode_rejects_paused_rows_with_close ... ok
|
||||
test backtest::execution_config_tests::source_row_cache_and_filter_distinguish_all_from_explicit_empty_symbols ... ok
|
||||
test backtest::execution_config_tests::source_row_adjustment_factor_decode_skips_unrequested_projection ... ok
|
||||
test backtest::execution_config_tests::source_row_daily_capacity_is_scoped_to_actual_insertion_dates ... ok
|
||||
test backtest::execution_config_tests::source_row_grouping_copies_date_keys_only_at_boundaries ... ok
|
||||
test backtest::execution_config_tests::source_row_missing_open_does_not_mark_candidate_as_one_yuan ... ok
|
||||
test backtest::execution_config_tests::source_row_model_score_context_accepts_extract_only_score_metadata ... ok
|
||||
test backtest::execution_config_tests::source_row_model_score_context_can_disable_candidate_limit_inference ... ok
|
||||
test backtest::execution_config_tests::source_row_model_score_context_infers_candidate_limit_from_selection_limit ... ok
|
||||
test backtest::execution_config_tests::source_row_model_score_context_merges_bundle_extract_and_scopes_cache ... ok
|
||||
test backtest::execution_config_tests::source_row_daily_capacity_keeps_supplemental_inserts_in_place ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_does_not_require_adjustment_factor_for_volume_only_rolling ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_includes_amount_for_stock_amount_expressions ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_keeps_select_all_for_generic_empty_factor_refs ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_keeps_source_lake_empty_factor_refs_narrow ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_keeps_source_lake_without_strategy_spec_narrow ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_splits_sparse_model_score_factor_refs ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_uses_framework_rolling_inputs ... ok
|
||||
test backtest::execution_config_tests::source_row_sort_preserves_published_order_and_stably_repairs_merges ... ok
|
||||
test backtest::execution_config_tests::source_row_projection_uses_raw_market_columns_for_framework_rollings ... ok
|
||||
test backtest::execution_config_tests::source_rows_query_engine_is_validated_and_scopes_cache_epoch ... ok
|
||||
test backtest::execution_config_tests::source_rows_scope_evidence_preserves_unordered_date_counts ... ok
|
||||
test backtest::execution_config_tests::sparse_model_score_overlay_dates_exclude_warmup_rows ... ok
|
||||
test backtest::execution_config_tests::sparse_model_score_overlay_keeps_named_score_when_context_uses_default_score ... ok
|
||||
test backtest::execution_config_tests::sparse_model_score_overlay_merges_only_requested_extra_factors ... ok
|
||||
test backtest::execution_config_tests::sparse_model_score_overlay_query_windows_are_chunked_by_date_span ... ok
|
||||
test backtest::execution_config_tests::source_warmup_uses_required_trading_windows ... ok
|
||||
test backtest::execution_config_tests::sparse_model_score_overlay_skips_primary_score_columns ... ok
|
||||
test backtest::execution_config_tests::sparse_model_score_overlay_reuses_source_row_cache_without_http ... ok
|
||||
test backtest::execution_config_tests::stock_cost_model_for_execution_uses_explicit_fee_overrides ... ok
|
||||
test backtest::execution_config_tests::sparse_overlay_skips_source_lake_when_scoped_date_symbol_filter_is_empty ... ok
|
||||
test backtest::execution_config_tests::strategy_execution_top_level_risk_enabled_aliases_are_applied ... ok
|
||||
test backtest::execution_config_tests::strategy_risk_policy_accepts_camel_case_fields ... ok
|
||||
test backtest::execution_config_tests::supplemental_execution_key_index_only_materializes_requested_symbols ... ok
|
||||
test backtest::execution_config_tests::strategy_factory_model_rank_rotation_keeps_explicit_execution_contract ... ok
|
||||
test backtest::execution_config_tests::timed_daily_current_close_loads_the_intraday_decision_snapshot ... ok
|
||||
test backtest::execution_config_tests::top_level_execution_limits_override_nested_risk_policy_defaults ... ok
|
||||
test backtest::execution_config_tests::unique_source_row_trade_dates_preserves_sorted_and_unordered_inputs ... ok
|
||||
test backtest::execution_config_tests::unsupported_backtest_frequency_is_rejected ... ok
|
||||
test backtest::execution_config_tests::validated_source_limit_prices_allows_explicit_no_limit_stock ... ok
|
||||
test backtest::execution_config_tests::validated_source_limit_prices_allows_index_without_limits ... ok
|
||||
test backtest::execution_config_tests::validated_source_limit_prices_allows_paused_stock_without_limits ... ok
|
||||
test backtest::execution_config_tests::validated_source_limit_prices_rejects_missing_stock_limit ... ok
|
||||
test backtest::execution_config_tests::validated_source_limit_prices_trusts_source_limits ... ok
|
||||
test backtest::factor_history_calendar_tests::history_uses_actual_sessions_and_rejects_short_or_future_calendars ... ok
|
||||
test backtest::factor_history_calendar_tests::contextual_pattern_expands_data_scope_but_not_candidate_members ... ok
|
||||
test backtest::factor_history_calendar_tests::index_policy_preloads_its_reference_without_widening_the_candidate_universe ... ok
|
||||
test backtest::factor_history_calendar_tests::selection_buy_permission_carries_its_complete_pattern_window ... ok
|
||||
test backtest::fund_metadata::tests::day_preparation_distinguishes_etf_optional_risk_from_missing_prices ... ok
|
||||
test backtest::fund_metadata::tests::etf_metadata_binds_identity_dates_and_units ... ok
|
||||
test backtest::fund_metadata::tests::real_etf_fallback_fills_match_source_opens_dates_and_fee_budgets ... ignored, requires exact frontend-compiled frozen Source Lake ETF replay artifacts
|
||||
test backtest::fund_metadata::tests::real_mixed_prices_preserve_holdings_until_explicit_zero_allocation ... ignored, requires FIDC_FUND_REPLAY_PREFIX containing read-only real Source Arrow fixtures
|
||||
test backtest::fund_metadata::tests::real_source_arrow_preserves_etf_identity_and_detects_tampering ... ignored, requires FIDC_FUND_METADATA_TEST_ARROW from the read-only real Source probe
|
||||
test backtest::fund_metadata::tests::preliminary_candidate_panel_uses_verified_etf_rules_without_inventing_limits ... ok
|
||||
test backtest::manual_data_scope::tests::explicit_data_scope_can_expand_without_mutating_the_strategy_or_empty_pool ... ok
|
||||
test backtest::manual_data_scope::tests::dataset_cache_tracks_extra_market_demand_not_manual_price_or_execution_identity ... ok
|
||||
test backtest::prepared_source_row::tests::all_supported_numeric_types_keep_the_existing_conversion_and_null_policy ... ok
|
||||
test backtest::prepared_source_row::tests::arbitrary_slices_and_supplemental_rows_keep_symbol_order ... ok
|
||||
test backtest::prepared_source_row::tests::finite_values_keep_exact_float_bits_and_metadata ... ok
|
||||
test backtest::prepared_source_row::tests::invalid_json_is_not_hidden_by_a_consumer_that_needs_no_factors ... ok
|
||||
test backtest::prepared_source_row::tests::json_typed_duplicates_and_projected_overlay_preserve_overwrite_order ... ok
|
||||
test backtest::prepared_source_row::tests::actual_arrow_day_uses_columnar_rows_and_matches_owned_final_snapshots ... ok
|
||||
test backtest::prepared_source_row::tests::nonfinite_price_policy_does_not_mutate_original_arrow_values ... ok
|
||||
test backtest::prepared_source_row::tests::schema_error_precedence_and_null_keys_are_preserved ... ok
|
||||
test backtest::prepared_source_row::tests::overlay_cannot_relax_base_adjustment_validation ... ok
|
||||
test backtest::risk_free_rate_cache_identity_includes_bundle_and_full_calendar ... ok
|
||||
test backtest::risk_free_rate_cache_round_trips_validated_contract ... ok
|
||||
test backtest::saved_risk_cache::tests::saved_reference_cancelled_reader_does_not_leave_a_claim ... ok
|
||||
test backtest::saved_risk_cache::tests::saved_reference_identity_includes_endpoint_hash_and_calendar ... ok
|
||||
test backtest::saved_risk_reference_requires_original_content_not_only_server_claimed_hash ... ok
|
||||
test backtest::source_query_windows::tests::actual_cached_boundaries_are_kept_and_gaps_are_covered ... ok
|
||||
test backtest::source_query_windows::tests::all_days_remain_once_and_scope_count_is_bounded ... ok
|
||||
test backtest::source_query_windows::tests::discovery_never_crosses_source_keys_schemas_or_file_types ... ok
|
||||
test backtest::source_query_windows::tests::overlap_prefers_more_covered_days_then_fewer_windows ... ok
|
||||
test backtest::source_query_windows::tests::projection_and_symbol_scope_have_distinct_boundary_keys ... ok
|
||||
test backtest::saved_risk_cache::tests::saved_reference_coalesces_only_validated_original_contracts ... ok
|
||||
test backtest::source_query_windows::tests::uncached_gaps_keep_the_existing_calendar_anchor ... ok
|
||||
test backtest::source_row_key_index::tests::indexed_date_ranges_cover_days_split_across_batches ... ok
|
||||
test backtest::source_row_key_index::tests::normalized_keys_cannot_duplicate_across_batches ... ok
|
||||
test backtest::source_row_key_index::tests::null_identity_and_declared_count_mismatch_are_rejected ... ok
|
||||
test backtest::source_row_query_purpose_tests::factor_overlay_cache_identity_cannot_be_reused_as_market_facts ... ok
|
||||
test backtest::source_row_query_purpose_tests::factor_overlay_merge_preserves_every_base_market_and_risk_fact ... ok
|
||||
test backtest::source_row_query_purpose_tests::factor_overlay_requires_an_explicit_non_market_projection ... ok
|
||||
test backtest::source_row_query_purpose_tests::only_explicit_factor_overlays_omit_redundant_risk_columns ... ok
|
||||
test backtest::source_rows_columnar::tests::cache_write_failure_does_not_replace_the_current_columnar_input ... ok
|
||||
test backtest::source_rows_columnar::tests::actual_selection_and_empty_outputs_do_not_keep_unused_factor_columns ... ok
|
||||
test backtest::source_rows_columnar::tests::columnar_normalization_matches_mixed_values_json_aliases_and_sorting ... ok
|
||||
test backtest::source_rows_columnar::tests::invalid_keys_and_adjustment_are_rejected_before_selection ... ok
|
||||
test backtest::source_rows_columnar::tests::malformed_json_and_dependency_identity_are_not_accepted ... ok
|
||||
test backtest::source_rows_columnar::tests::non_cacheable_json_factor_names_keep_the_real_record_path ... ok
|
||||
test backtest::source_rows_columnar::tests::normalized_duplicate_symbols_are_not_deduplicated ... ok
|
||||
test backtest::source_rows_input_tests::cache_cannot_include_rows_outside_its_declared_date_range ... ok
|
||||
test backtest::source_rows_input_tests::cache_write_failure_retains_the_real_window_and_empty_windows_do_not_read_old_rows ... ok
|
||||
test backtest::source_rows_input_tests::failed_materialization_does_not_turn_the_source_into_an_empty_success ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_crosses_record_batches_and_preserves_key_normalization ... ok
|
||||
test backtest::source_rows_input_tests::independent_windows_stay_columnar_until_daily_snapshot_construction ... ok
|
||||
test backtest::source_rows_input_tests::mapped_input_can_be_materialized_for_mutating_model_and_publication_overlays ... ok
|
||||
test backtest::source_rows_input_tests::invalid_cache_keys_and_overlay_coverage_never_publish_source_input ... ok
|
||||
test backtest::source_rows_input_tests::mapped_input_preserves_empty_scopes_and_filters_only_base_rows ... ok
|
||||
test backtest::source_rows_input_tests::mixed_window_storage_and_duplicate_boundaries_keep_original_semantics ... ok
|
||||
test backtest::source_rows_input_tests::mapped_inventory_and_final_days_match_owned_rows_with_factor_overlays ... ok
|
||||
test backtest::source_rows_input_tests::unsuccessful_window_publication_cannot_read_a_previous_cache_file ... ok
|
||||
test backtest::terminal_audit_summary_tests::deferred_strategy_warning_is_not_presented_as_a_submitted_order ... ok
|
||||
test backtest::terminal_audit_summary_tests::pending_terminal_state_is_explicit_in_summary_warning ... ok
|
||||
test backtest::terminal_calendar_tests::delayed_deposit_requires_terminal_calendar_but_zero_day_does_not ... ok
|
||||
test backtest::terminal_calendar_tests::scheduled_terminal_calendar_requires_strict_order_and_range ... ok
|
||||
test backtest::source_rows_input_tests::separate_arrow_factor_input_preserves_sparse_overlay_values_and_existing_layers ... ok
|
||||
test backtest::tests::accounting_allocation_details_do_not_require_or_fabricate_an_order ... ok
|
||||
test backtest::tests::all_source_lake_runtime_caches_are_scoped_by_backtest_bundle ... ok
|
||||
test backtest::tests::audit_instrument_cache_values_from_request ... ignored, read-only Boris cache comparison with an explicit audit request
|
||||
test backtest::tests::benchmark_borrowed_numeric_factor_keys_against_owned_strings ... ignored, manual release-mode allocation benchmark
|
||||
test backtest::tests::benchmark_date_symbol_filter_query_range_preserves_index_symbols ... ok
|
||||
test backtest::tests::benchmark_query_windows_split_long_source_lake_ranges ... ok
|
||||
test backtest::tests::arrow_reader_accepts_lz4_compressed_stream ... ok
|
||||
test backtest::tests::benchmark_row_binary_cache_rejects_metadata_mismatch ... ok
|
||||
test backtest::tests::benchmark_row_binary_cache_roundtrips_rows ... ok
|
||||
test backtest::tests::benchmark_row_cache_ignores_legacy_csv_cache ... ok
|
||||
test backtest::tests::benchmark_row_cache_path_scopes_date_symbol_filter ... ok
|
||||
test backtest::tests::benchmark_row_cache_path_uses_binary_cache_file ... ok
|
||||
test backtest::tests::benchmark_row_candidate_scan_removes_stale_schema_cache ... ok
|
||||
test backtest::tests::benchmark_row_covering_cache_reuses_superset_symbols_and_dates ... ok
|
||||
test backtest::tests::benchmark_row_partial_cache_rejects_missing_symbol_segment ... ok
|
||||
test backtest::tests::benchmark_row_to_source_row_preserves_false_paused_state ... ok
|
||||
test backtest::tests::benchmark_row_partial_cache_reports_missing_ranges ... ok
|
||||
test backtest::tests::benchmark_row_window_cache_scope_ignores_out_of_window_filter_dates ... ok
|
||||
test backtest::tests::benchmark_rows_dedup_after_window_merge ... ok
|
||||
test backtest::tests::benchmark_source_row_arrow_bounded_materialization ... ignored, requires FIDC_BT_SOURCE_ROW_VIEW_PROBE_PATH and explicit release-mode invocation
|
||||
test backtest::tests::benchmark_source_row_arrow_view_cache ... ignored, requires FIDC_BT_SOURCE_ROW_VIEW_PROBE_PATH and explicit release-mode invocation
|
||||
test backtest::tests::bootstrap_missing_quote_repair_keys_keeps_all_actual_order_gaps_without_fills ... ok
|
||||
test backtest::tests::bootstrap_missing_quote_repair_keys_keeps_all_after_fills_exist ... ok
|
||||
test backtest::tests::benchmark_rows_empty_date_symbol_filter_does_not_reuse_existing_cache ... ok
|
||||
test backtest::tests::build_trade_detail_rows_expands_split_fills_by_order_id ... ok
|
||||
test backtest::tests::build_trade_detail_rows_omits_canceled_orders_without_fills ... ok
|
||||
test backtest::tests::build_trade_detail_rows_matches_fill_when_order_reason_changes ... ok
|
||||
test backtest::tests::build_trade_detail_rows_preserves_decision_and_execution_dates ... ok
|
||||
test backtest::tests::carry_forward_target_snapshot_keeps_transition_day_then_replaces_positions ... ok
|
||||
test backtest::tests::cn_index_symbols_are_not_stock_execution_quote_repair_symbols ... ok
|
||||
test backtest::tests::collect_missing_execution_quote_keys_uses_canceled_stock_orders_only ... ok
|
||||
test backtest::tests::compact_progress_retains_manual_counts_and_rejects_a_missing_source ... ok
|
||||
test backtest::tests::compacted_carry_forward_filter_keeps_window_semantics ... ok
|
||||
test backtest::tests::corporate_action_arrow_preserves_the_independent_delivery_date_and_unknown_quantities ... ok
|
||||
test backtest::tests::corporate_action_binary_cache_roundtrips_rows ... ok
|
||||
test backtest::tests::corporate_action_cache_ignores_legacy_csv_cache ... ok
|
||||
test backtest::tests::corporate_action_cache_path_uses_source_lake_provider_epoch ... ok
|
||||
test backtest::tests::bundle_validation_observes_dependency_semantic_change_and_revocation_without_ttl ... ok
|
||||
test backtest::tests::corporate_action_covering_cache_reuses_superset_scope ... ok
|
||||
test backtest::tests::daily_execution_price_arrow_decode_preserves_unpriced_paused_risk_rows ... ok
|
||||
test backtest::tests::daily_execution_price_arrow_requires_execution_contract_fields ... ok
|
||||
test backtest::tests::daily_execution_scope_membership_is_normalized_once_and_isolated_by_date ... ok
|
||||
test backtest::tests::dataset_signature_scopes_fixed_signal_source_rows ... ok
|
||||
test backtest::tests::dataset_signature_scopes_fixed_signal_date_symbol_filter_with_model_score ... ok
|
||||
test backtest::tests::date_symbol_filter_query_range_carry_forward_keeps_history_prefix ... ok
|
||||
test backtest::tests::date_symbol_filter_query_range_exact_keeps_only_window_dates ... ok
|
||||
test backtest::tests::date_symbol_filter_query_range_preserves_empty_request ... ok
|
||||
test backtest::tests::dataset_signature_uses_data_requirements_not_strategy_identity_or_thresholds ... ok
|
||||
test backtest::tests::default_cache_epochs_ignore_local_manifest_directory ... ok
|
||||
test backtest::tests::dense_execution_quote_plan_uses_requested_trade_dates_only ... ok
|
||||
test backtest::tests::dynamic_model_score_execution_only_queries_next_date_gaps ... ok
|
||||
test backtest::tests::dynamic_model_score_execution_skips_rows_present_on_next_full_calendar_date ... ok
|
||||
test backtest::tests::dynamic_model_score_execution_symbols_include_candidate_universe_for_lagged_execution ... ok
|
||||
test backtest::tests::dynamic_symbol_expression_does_not_enable_fixed_source_scope ... ok
|
||||
test backtest::tests::empty_date_symbol_filter_is_empty_request_not_all_market ... ok
|
||||
test backtest::tests::equity_artifact_keeps_external_deposit_out_of_return ... ok
|
||||
test backtest::tests::equity_projection_orders_facts_and_includes_the_initial_nav_peak ... ok
|
||||
test backtest::tests::execution_arrow_does_not_turn_nonfinite_numbers_into_optional_missing_values ... ok
|
||||
test backtest::tests::execution_arrow_requires_the_strict_source_contract_even_when_empty ... ok
|
||||
test backtest::tests::execution_price_date_symbol_filter_keeps_benchmark_and_prunes_other_targets ... ok
|
||||
test backtest::tests::execution_price_only_source_row_marker_round_trips ... ok
|
||||
test backtest::tests::execution_quantities_are_exact_integers_not_rounded_floats ... ok
|
||||
test backtest::tests::exact_source_row_windows_restore_in_parallel_and_keep_order ... ok
|
||||
test backtest::tests::execution_quote_day_binary_cache_invalidates_duplicate_date_symbol ... ok
|
||||
test backtest::tests::execution_quote_day_binary_cache_rejects_metadata_mismatch ... ok
|
||||
test backtest::tests::execution_quote_day_cache_does_not_persist_empty_negative_result ... ok
|
||||
test backtest::tests::execution_quote_day_cache_ignores_legacy_csv_cache ... ok
|
||||
test backtest::tests::execution_quote_day_cache_loads_empty_without_provider_url ... ok
|
||||
test backtest::tests::execution_quote_day_cache_path_includes_minute_lookback_and_price_rule ... ok
|
||||
test backtest::tests::execution_quote_day_cache_path_uses_quote_epoch ... ok
|
||||
test backtest::tests::execution_quote_day_cache_path_uses_source_lake_provider_tag ... ok
|
||||
test backtest::tests::execution_quote_day_cache_tracks_returned_symbols_only ... ok
|
||||
test backtest::tests::execution_quote_prefetch_seed_tracks_negative_symbols ... ok
|
||||
test backtest::tests::execution_quote_query_tasks_keep_target_time_symbol_scopes_narrow ... ok
|
||||
test backtest::tests::execution_quote_query_tasks_merge_identical_symbol_scopes_for_same_day ... ok
|
||||
test backtest::tests::execution_quote_rows_duplicate_date_symbol_is_rejected ... ok
|
||||
test backtest::tests::execution_quote_rows_normalize_base_stock_code_symbols ... ok
|
||||
test backtest::tests::execution_quote_loader_reads_a_bounded_minute_window_instead_of_a_point_snapshot ... ok
|
||||
test backtest::tests::explicit_cli_source_cache_epoch_is_stable_and_provider_scoped ... ok
|
||||
test backtest::tests::external_daily_execution_symbols_without_signals_only_queries_benchmark ... ok
|
||||
test backtest::tests::external_daily_execution_symbols_skip_signals_already_in_source_rows ... ok
|
||||
test backtest::tests::factor_values_arrow_decode_validates_metadata_and_merges_aliases ... ok
|
||||
test backtest::tests::fidc_runner_rejects_ficlaw_data_source_rows_table ... ok
|
||||
test backtest::tests::factor_values_arrow_decode_rejects_future_data_and_semantic_drift ... ok
|
||||
test backtest::tests::fixed_minute_subscriptions_expand_static_daily_data_scope ... ok
|
||||
test backtest::tests::fixed_signal_date_symbol_filter_is_disabled_when_static_symbols_are_mixed_in ... ok
|
||||
test backtest::tests::fixed_signal_date_symbol_filter_seeds_static_symbols_at_query_start ... ok
|
||||
test backtest::tests::fixed_signal_date_symbol_filter_survives_model_score_rank_metadata ... ok
|
||||
test backtest::tests::fixed_signal_next_open_exit_scope_keeps_all_prior_unsold_symbols ... ok
|
||||
test backtest::tests::fixed_signal_source_row_date_symbol_filter_extracts_dated_weights_only ... ok
|
||||
test backtest::tests::fixed_signal_source_row_date_symbol_filter_extracts_target_holding_rows ... ok
|
||||
test backtest::tests::fixed_signal_source_row_symbols_extracts_runtime_target_weights_expr ... ok
|
||||
test backtest::tests::fixed_signal_source_row_symbols_extracts_single_quoted_runtime_target_weights_expr ... ok
|
||||
test backtest::tests::fixed_signal_source_row_symbols_extracts_strategy_metadata_targets ... ok
|
||||
test backtest::tests::fixed_signal_source_row_symbols_extracts_target_weight_codes ... ok
|
||||
test backtest::tests::fixed_symbol_order_scope_requires_rotation_disabled ... ok
|
||||
test backtest::tests::fixed_symbol_orders_seed_exact_intraday_snapshot_keys ... ok
|
||||
test backtest::tests::frozen_bundle_identity_http_failure_is_not_a_mutable_epoch_fallback ... ok
|
||||
test backtest::tests::full_minute_prefetch_days_respect_total_bar_budget ... ok
|
||||
test backtest::tests::futures_source_lake_catalog_requires_all_truth_datasets ... ok
|
||||
test backtest::tests::futures_only_runtime_contract_keeps_stock_scope_empty_and_allocates_cash ... ok
|
||||
test backtest::tests::instrument_metadata_binary_cache_rejects_metadata_mismatch ... ok
|
||||
test backtest::tests::instrument_metadata_binary_cache_roundtrips_rows ... ok
|
||||
test backtest::tests::instrument_metadata_cache_ignores_legacy_csv_cache ... ok
|
||||
test backtest::tests::instrument_metadata_cache_merges_missing_symbols_incrementally ... ok
|
||||
test backtest::tests::instrument_metadata_cache_path_is_scoped_by_historical_window ... ok
|
||||
test backtest::tests::instrument_metadata_cache_path_uses_binary_format ... ok
|
||||
test backtest::tests::instrument_metadata_request_serializes_historical_source_window_when_present ... ok
|
||||
test backtest::tests::intraday_snapshot_binary_cache_rejects_metadata_mismatch ... ok
|
||||
test backtest::tests::intraday_snapshot_binary_cache_roundtrips_rows ... ok
|
||||
test backtest::tests::intraday_snapshot_cache_ignores_legacy_csv_cache ... ok
|
||||
test backtest::tests::intraday_snapshot_cache_path_uses_source_lake_provider_tag ... ok
|
||||
test backtest::tests::kcb_board_and_quantity_use_source_classification ... ok
|
||||
test backtest::tests::intraday_snapshot_seeds_execution_quotes_and_negative_gaps ... ok
|
||||
test backtest::tests::layered_execution_quote_loader_cache_preserves_shared_seed_and_isolates_overlay ... ok
|
||||
test backtest::tests::lazy_execution_quote_window_dates_use_trading_calendar_only ... ok
|
||||
test backtest::tests::lazy_execution_quote_window_backfill_keeps_future_missing_symbols ... ok
|
||||
test backtest::tests::load_strategy_spec_rejects_removed_compatibility_profile ... ok
|
||||
test backtest::tests::load_strategy_spec_rejects_removed_engine_profile_name ... ok
|
||||
test backtest::tests::malformed_execution_observations_are_errors_not_empty_successes ... ok
|
||||
test backtest::tests::malformed_versioned_factor_ref_fails_closed ... ok
|
||||
test backtest::tests::managed_source_rows_route_by_exact_table ... ok
|
||||
test backtest::tests::minute_bar_arrow_decode_accepts_bounded_multi_date_stream ... ok
|
||||
test backtest::tests::load_strategy_spec_accepts_snake_case_bundle_field ... ok
|
||||
test backtest::tests::minute_bar_arrow_decode_preserves_timestamp_symbol_order ... ok
|
||||
test backtest::tests::minute_bar_arrow_decode_rejects_retired_string_time_schema ... ok
|
||||
test backtest::tests::minute_bar_arrow_decode_rejects_duplicate_or_unordered_rows ... ok
|
||||
test backtest::tests::mixed_candidate_book_binds_complete_dates_raw_sources_and_resolved_order ... ok
|
||||
test backtest::tests::mixed_stock_futures_contract_requires_explicit_futures_cash ... ok
|
||||
test backtest::tests::normalize_symbol_maps_bse_exchange_to_beijing_board ... ok
|
||||
test backtest::columnar_factor_overlay_tests::columnar_overlay_matches_row_decoder_for_wide_sparse_null_and_signed_zero_values ... ok
|
||||
test backtest::tests::minute_bars_retries_transient_http_failure_and_preserves_caller_header ... ok
|
||||
test backtest::tests::partial_source_row_window_cache_crops_covering_segments_in_arrow ... ok
|
||||
test backtest::tests::partial_source_row_window_cache_ignores_all_market_cache_for_symbol_scope ... ok
|
||||
test backtest::tests::partial_source_row_window_cache_ignores_wide_projection_segment ... ok
|
||||
test backtest::tests::partial_source_row_window_cache_reports_only_missing_gaps ... ok
|
||||
test backtest::tests::progress_callback_is_forwarded_to_blocking_engine_thread ... ok
|
||||
test backtest::tests::published_factor_binding_normalizes_ref_and_builds_arrow_request ... ok
|
||||
test backtest::tests::query_scope_batch_preserves_authoritative_window_identities ... ok
|
||||
test backtest::tests::partial_source_row_window_cache_reuses_exact_projection_with_null_dynamic_columns ... ok
|
||||
test backtest::tests::query_scope_identity_reuses_physical_window_across_distinct_bundles ... ok
|
||||
test backtest::tests::real_source_quote_json_and_arrow_match_strict_reader ... ignored, requires a bounded real Source JSON/Arrow probe directory
|
||||
test backtest::tests::repeated_query_views_share_only_identical_execution_observations ... ok
|
||||
test backtest::tests::platform_quote_plan_uses_current_decision_date_source_rows ... ok
|
||||
test backtest::tests::query_scope_http_carries_rolling_dependencies_and_rejects_bad_contracts ... ok
|
||||
test backtest::tests::select_instrument_metadata_rejects_missing_symbols ... ok
|
||||
test backtest::tests::shared_base_source_projection_keeps_raw_adjustment_contract ... ok
|
||||
test backtest::tests::shared_base_source_rows_cache_is_enabled_by_default ... ok
|
||||
test backtest::tests::shared_base_source_rows_cache_is_only_for_unscoped_raw_rows ... ok
|
||||
test backtest::tests::reusable_source_row_window_cache_does_not_reuse_covering_cache ... ok
|
||||
test backtest::tests::sorted_source_row_validator_rejects_unordered_or_duplicate_keys ... ok
|
||||
test backtest::tests::source_cache_epoch_default_is_independent_from_quote_plan_epoch ... ok
|
||||
test backtest::tests::source_dependency_contract_invalidates_all_authoritative_cache_scopes ... ok
|
||||
test backtest::tests::segmented_source_row_cache_reads_only_uncovered_overlap_ranges ... ok
|
||||
test backtest::tests::short_range_source_query_window_is_explicit_and_keeps_long_ranges_bounded ... ok
|
||||
test backtest::tests::source_lake_contract_epoch_ignores_runtime_build_but_tracks_catalog ... ok
|
||||
test backtest::tests::source_lake_health_contract_epoch_ignores_runtime_build ... ok
|
||||
test backtest::tests::source_lake_arrow_request_retries_bad_gateway ... ok
|
||||
test backtest::tests::source_lake_busy_retry_honors_retryable_capacity_response ... ok
|
||||
test backtest::tests::source_lake_source_query_retries_are_bounded_and_transient_only ... ok
|
||||
test backtest::tests::source_lake_source_query_timeout_accepts_highmem_alias ... ok
|
||||
test backtest::tests::source_query_concurrency_defaults_and_clamps ... ok
|
||||
test backtest::tests::source_lake_run_waits_for_manifest_publication_and_holds_shared_lease ... ok
|
||||
test backtest::tests::source_row_arrow_cache_rejects_and_does_not_write_empty_rows ... ok
|
||||
test backtest::tests::source_row_arrow_cache_rejects_duplicate_date_symbol ... ok
|
||||
test backtest::tests::source_lake_uses_wider_windows_for_narrow_queries ... ok
|
||||
test backtest::tests::source_row_arrow_decode_preserves_dynamic_projection_columns ... ok
|
||||
test backtest::tests::source_row_arrow_decode_keeps_compact_base_rows_without_extra_json ... ok
|
||||
test backtest::tests::source_row_arrow_dependency_contract_rejects_invalid_metadata ... ok
|
||||
test backtest::tests::source_row_arrow_extra_factor_helpers_preserve_projected_columns ... ok
|
||||
test backtest::tests::source_row_arrow_view_keeps_mmap_alive_until_materialization ... ok
|
||||
test backtest::tests::source_row_arrow_view_cache_reuses_identity_and_invalidates_replacement ... ok
|
||||
test backtest::tests::source_row_cache_file_range_parses_source_cache_dates ... ok
|
||||
test backtest::tests::source_row_cache_candidate_scan_removes_previous_schema_files ... ok
|
||||
test backtest::tests::source_row_cache_directory_index_tracks_files_published_by_other_workers ... ok
|
||||
test backtest::tests::source_row_cache_path_hashes_long_projection_columns ... ok
|
||||
test backtest::tests::source_row_cache_path_scopes_explicit_source_symbols ... ok
|
||||
test backtest::tests::source_row_cache_lease_serializes_concurrent_misses ... ok
|
||||
test backtest::tests::source_row_cache_path_normalizes_projection_column_order ... ok
|
||||
test backtest::tests::source_row_cache_path_separates_projection_columns ... ok
|
||||
test backtest::tests::source_row_cache_projection_check_allows_source_lake_fixed_aliases ... ok
|
||||
test backtest::tests::source_row_cache_projection_check_requires_extra_factor_keys ... ok
|
||||
test backtest::tests::source_row_cache_rejects_legacy_raw_bincode_without_magic_header ... ok
|
||||
test backtest::tests::source_row_cache_segment_selection_prefers_small_complete_windows ... ok
|
||||
test backtest::tests::source_row_cache_segment_selection_rejects_incomplete_windows ... ok
|
||||
test backtest::tests::source_row_date_windows_keep_interior_buckets_stable_across_ranges ... ok
|
||||
test backtest::tests::source_row_date_windows_split_long_ranges ... ok
|
||||
test backtest::tests::source_row_date_windows_stay_within_requested_range ... ok
|
||||
test backtest::tests::source_row_cache_rejects_metadata_mismatch ... ok
|
||||
test backtest::tests::source_row_primary_keys_are_inline_without_growing_the_row_header ... ok
|
||||
test backtest::tests::source_row_segment_boundary_validator_preserves_fail_closed_integrity ... ok
|
||||
test backtest::tests::source_row_symbol_normalization_preserves_canonical_and_maps_board ... ok
|
||||
test backtest::tests::source_row_symbol_scope_epoch_changes_with_fixed_signal_symbols ... ok
|
||||
test backtest::tests::source_row_request_cache_epoch_scopes_runtime_query_context ... ok
|
||||
test backtest::tests::source_row_cache_writes_arrow_file ... ok
|
||||
test backtest::tests::source_rows_duplicate_date_symbol_is_rejected ... ok
|
||||
test backtest::tests::source_row_window_cache_scope_ignores_out_of_window_filter_dates ... ok
|
||||
test backtest::tests::source_rows_request_keeps_rolling_enabled_for_rolling_projection_columns ... ok
|
||||
test backtest::tests::source_rows_unique_date_symbol_passes_without_reordering ... ok
|
||||
test backtest::tests::static_universe_include_is_an_explicit_source_row_scope ... ok
|
||||
test backtest::tests::source_semantic_change_invalidates_bundle_and_window_cache ... ok
|
||||
test backtest::tests::strategy_factor_overlay_only_queries_keys_and_missing_factor_fields ... ok
|
||||
test backtest::tests::stock_pool_screen_snapshot_uses_exact_selection_dates_and_full_symbol_history ... ok
|
||||
test backtest::tests::strategy_bundle_path_loads_nested_spec_and_signal_scope ... ok
|
||||
test backtest::tests::strategy_factor_overlay_rejects_duplicate_and_unordered_keys ... ok
|
||||
test backtest::tests::strategy_factor_overlay_requires_exact_base_key_coverage ... ok
|
||||
test backtest::tests::strategy_factory_source_lake_provider_name_is_exact ... ok
|
||||
test backtest::tests::strategy_factory_source_lake_requires_url_and_matching_provider ... ok
|
||||
test backtest::tests::strategy_factory_source_lake_uses_arrow_sized_query_windows_by_default ... ok
|
||||
test backtest::tests::trade_display_metadata_cache_rejects_rows_outside_exact_request ... ok
|
||||
test backtest::tests::trade_display_metadata_cache_is_bundle_scoped_and_covers_missing_rows ... ok
|
||||
test backtest::tests::typed_equity_result_emits_service_schema ... ok
|
||||
test backtest::tests::trade_display_metadata_disk_cache_roundtrips_frozen_bundle_rows ... ok
|
||||
test backtest::tests::typed_order_result_keeps_zero_fill_orders ... ok
|
||||
test backtest::tests::typed_trade_result_preserves_sub_cent_execution_price ... ok
|
||||
test backtest::tests::validated_manifest_scope_reuses_cache_across_distinct_bundle_ids ... ok
|
||||
test backtest::tests::versioned_factor_binding_rejects_research_scope ... ok
|
||||
test backtest::tests::versioned_factor_binding_requires_declared_semantics ... ok
|
||||
test backtest::tests::versioned_factor_ref_requires_a_binding ... ok
|
||||
test backtest::typed_exit_dependency_tests::ordinary_strategy_partial_exit_also_contributes_its_window ... ok
|
||||
test backtest::typed_exit_dependency_tests::typed_stock_pool_exits_contribute_the_full_rolling_window ... ok
|
||||
test cache_directory_watch::tests::notifications_cover_publication_rename_delete_and_ignore_unrelated_files ... ok
|
||||
test compute_pool_tests::concurrent_embedded_runs_do_not_change_each_others_pool_budget ... ok
|
||||
test compute_pool_tests::current_thread_async_runtime_keeps_the_compute_context ... ok
|
||||
test backtest::tests::timed_quote_dataset_signature_includes_daily_selection_plan ... ok
|
||||
test decision_projection::tests::projection_metadata_alias_empty_fields_and_credential_paths_fail_closed ... ok
|
||||
test decision_projection::tests::projection_native_request_is_complete_and_never_truncated_to_top_n ... ok
|
||||
test compute_pool_tests::embedded_runner_uses_its_declared_budget_and_preserves_results ... ok
|
||||
test decision_projection::tests::projection_response_is_bound_to_source_model_clock_and_exact_population ... ok
|
||||
test manual_ledger::corporate_tests::actual_core_corporate_projection_is_preserved_in_the_shared_trade_row ... ok
|
||||
test result_store::tests::canonical_first_divergence_reports_json_pointer ... ok
|
||||
test result_store::tests::canonical_manifest_is_stable_and_transport_independent ... ok
|
||||
test manual_ledger::corporate_tests::corporate_adjustments_reject_forged_money_dates_and_reference_chains ... ok
|
||||
test backtest::tests::unused_daily_market_cap_prefilter_does_not_split_identical_input_cache ... ok
|
||||
test result_store::tests::fact_manifest_publish_is_idempotent_under_concurrency ... ok
|
||||
test result_store::tests::lazy_registration_defers_payload_hashing_to_section_read ... ok
|
||||
test result_store::tests::process_event_store_is_content_addressed_and_round_trips ... ok
|
||||
test result_store::tests::run_store_identity_keeps_different_data_epochs_separate ... ok
|
||||
test result_store::tests::manual_source_and_applications_are_both_hashed_and_persisted ... ok
|
||||
test result_store::tests::run_store_is_content_addressed_and_round_trips ... ok
|
||||
test result_store::tests::run_store_reads_only_date_overlapping_blocks ... ok
|
||||
test thread_cpu::tests::thread_cpu_clock_keeps_unavailable_or_cross_thread_samples_unknown ... ok
|
||||
test result_store::tests::run_store_rejects_corrupt_manifest ... ok
|
||||
test thread_cpu::tests::sleeping_time_is_not_reported_as_cpu_execution ... ok
|
||||
test backtest::source_rows_columnar::tests::columnar_cache_keeps_original_output_batch_boundaries ... ok
|
||||
|
||||
test result: ok. 474 passed; 0 failed; 9 ignored; 0 measured; 0 filtered out; finished in 4.24s
|
||||
|
||||
Running unittests src/main.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/fidc_backtest_runner-86f3f46fa8dc66e0)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running tests/allocation_profile.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/allocation_profile-a941e54820726a92)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running unittests examples/export_manual_corporate_evidence.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/examples/export_manual_corporate_evidence-0a5023c51d949afa)
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running unittests src/main.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/deps/fidc_backtest_service-4c9243df6eb45c3d)
|
||||
|
||||
running 142 tests
|
||||
test daily_patterns::tests::column_transport_is_lossless_and_rejects_ambiguous_rows ... ok
|
||||
test daily_patterns::tests::arrow_rejects_truncation_and_forged_lengths_before_decoding ... ok
|
||||
test event_stream::tests::effective_initial_cash_is_projected_without_guessing_or_losing_source_precision ... ok
|
||||
test daily_patterns::tests::daily_patterns_read_only_transport_binds_exact_input ... ok
|
||||
test daily_patterns::tests::numeric_mode_requires_the_explicit_read_only_context ... ok
|
||||
test event_stream::tests::ordered_batch_is_idempotent_and_changed_facts_do_not_replace_history ... ignored, requires local FIDC_EVENT_TEST_POSTGRES_DSN on the private fidc-auth-pg cluster
|
||||
test event_stream::tests::memory_tail_and_durable_prefix_replay_in_publish_order ... ok
|
||||
test daily_patterns::tests::arrow_uses_same_kernel_and_exact_byte_proof_with_partial_scope ... ok
|
||||
test event_stream::tests::live_sequence_recent_replay_and_concurrent_runs_remain_isolated ... ok
|
||||
test execution_ledger::tests::dated_fact_store_rejects_reverse_order_before_projection ... ok
|
||||
test execution_ledger::tests::actual_http_routes_return_both_sources_and_precise_manual_values ... ok
|
||||
test execution_ledger::tests::serve_browser_fixture ... ignored, starts a local GET-only API for browser acceptance; set FIDC_LEDGER_UI_QA_PORT explicitly
|
||||
test execution_ledger::tests::serve_stream_browser_fixture ... ignored, starts isolated real WebSocket/GET handlers; FIDC_LEDGER_STREAM_QA_PORT required
|
||||
test execution_ledger::tests::source_precision_ids_and_later_status_are_not_rewritten ... ok
|
||||
test execution_ledger::tests::streamed_manual_facts_equal_final_ledger_without_publishing_a_future_terminal ... ok
|
||||
test execution_ledger::tests::typed_accounting_rows_keep_origin_without_order_identity ... ok
|
||||
test execution_ledger::tests::performance_counts_both_origins_and_sums_authoritative_fees_exactly ... ok
|
||||
test factor_events::tests::market_history_binds_kernel_version_and_preserves_unknown_streak ... ok
|
||||
test factor_events::tests::no_account_or_execution_payload_is_accepted ... ok
|
||||
test factor_events::tests::rank_history_binds_exact_scope_and_input_hash ... ok
|
||||
test factor_events::tests::transport_preserves_input_identity_and_rejects_future_values ... ok
|
||||
test idempotency::tests::canonical_json_order_does_not_change_request_identity ... ok
|
||||
test idempotency::tests::identity_binds_owner_and_rejects_changed_request ... ok
|
||||
test idempotency::tests::malformed_keys_and_unbound_records_are_rejected ... ok
|
||||
test performance::tests::accounting_allocations_are_not_unidentified_exchange_fills ... ok
|
||||
test performance::tests::builds_daily_weight_diagnostics_from_persisted_holdings ... ok
|
||||
test performance::tests::counts_open_underwater_duration_from_the_latest_peak ... ok
|
||||
test performance::tests::does_not_fake_risk_ratios_for_one_period ... ok
|
||||
test performance::tests::emits_true_nav_and_multiple_recovery_episodes ... ok
|
||||
test performance::tests::explicit_frozen_initial_capital_survives_a_first_day_deposit ... ok
|
||||
test performance::tests::first_period_loss_has_an_initial_peak_episode_without_a_fake_date ... ok
|
||||
test performance::tests::missing_benchmark_does_not_compact_dates_or_invent_a_drawdown_path ... ok
|
||||
test performance::tests::missing_order_ids_and_fees_remain_unknown_not_zero_or_report_counts ... ok
|
||||
test performance::tests::order_metrics_count_identities_separately_from_reports_and_partial_fills ... ok
|
||||
test performance::tests::preserves_a_first_day_strategy_return_and_initial_capital ... ok
|
||||
test performance::tests::preserves_the_canonical_pre_period_benchmark_baseline ... ok
|
||||
test performance::tests::projects_all_canonical_risk_metrics_and_contract_metadata ... ok
|
||||
test performance::tests::separates_longest_underwater_episode_from_deepest_drawdown ... ok
|
||||
test performance::tests::zero_nav_is_a_full_loss_not_missing_data ... ok
|
||||
test performance::tests::zero_orders_are_known_but_conflicting_order_identity_is_not_a_count ... ok
|
||||
test result_analysis_tests::batched_history_bindings_preserve_validation_and_survive_cache_eviction ... ignored, requires isolated FIDC_BACKTEST_TEST_POSTGRES_DSN
|
||||
test factor_events::tests::catalog_validator_binds_the_complete_body_and_preserves_contract ... ok
|
||||
test result_analysis_tests::copied_real_result_stores_and_browser_fixture ... ignored, requires FIDC_RESULT_QA_FIXTURES containing copied immutable result stores
|
||||
test result_analysis_tests::failed_optional_classification_keeps_unmodified_facts ... ok
|
||||
test result_analysis_tests::historical_text_search_filters_before_count_and_pagination ... ignored, requires isolated FIDC_BACKTEST_TEST_POSTGRES_DSN
|
||||
test result_analysis_tests::history_filters_use_corrected_drawdown_and_never_treat_unknown_as_zero ... ok
|
||||
test result_analysis_tests::owned_analysis_reference_does_not_require_a_cached_run_binding ... ok
|
||||
test result_analysis_tests::projected_history_filter_sort_and_pagination_on_postgres ... ignored, requires FIDC_BACKTEST_TEST_POSTGRES_DSN; creates an isolated test schema
|
||||
test result_analysis_tests::sector_filter_cannot_disable_its_classification_dependency ... ok
|
||||
test risk_projection::tests::absent_original_rates_preserve_independent_metrics_without_network_or_zero_rate_claim ... ok
|
||||
test risk_projection::tests::canonical_null_overrides_all_legacy_aliases_but_real_zero_survives ... ok
|
||||
test risk_projection::tests::original_contract_reprojects_legacy_zero_and_preserves_cash_flow_facts ... ok
|
||||
test risk_projection::tests::unavailable_contract_does_not_relabel_legacy_zero_as_a_verified_statistic ... ok
|
||||
test stock_pool_candidates::tests::resolve_binds_input_and_preserves_empty_automatic_days ... ok
|
||||
test submission_pg_tests::atomic_submission_claim_replay_and_rollback_on_postgres ... ignored, requires FIDC_BACKTEST_TEST_POSTGRES_DSN and creates only an isolated schema
|
||||
test tests::backtest_create_body_limit_accepts_bounded_signal_replays ... ok
|
||||
test tests::backtest_event_stream_exposes_a_websocket_transport ... ok
|
||||
test tests::backtest_list_cache_key_separates_summary_only_runtime_shape ... ok
|
||||
test tests::backtest_list_filters_and_sorting_use_generated_summary ... ok
|
||||
test tests::backtest_list_reports_cache_hit_and_effective_page_limit ... ok
|
||||
test tests::backtest_list_summary_only_uses_compact_query ... ok
|
||||
test tests::backtest_performance_contract_records_total_and_runner_seconds ... ok
|
||||
test tests::backtest_run_compact_cache_key_separates_strategy_detail_shape ... ok
|
||||
test tests::backtest_runner_data_env_accepts_strategy_factory_source_lake_provider ... ok
|
||||
test tests::backtest_runner_data_env_infers_strategy_factory_source_lake_from_runtime_source_table ... ok
|
||||
test tests::backtest_runner_data_env_rejects_legacy_ficlaw_data_source_table ... ok
|
||||
test tests::backtest_runner_data_env_rejects_legacy_source_provider ... ok
|
||||
test tests::backtest_runner_data_env_rejects_non_source_lake_provider ... ok
|
||||
test tests::backtest_runner_data_env_rejects_source_lake_provider_aliases ... ok
|
||||
test tests::backtest_runner_data_env_requires_strategy_factory_source_lake_url ... ok
|
||||
test tests::backtest_runner_data_env_uses_strategy_factory_source_lake_only ... ok
|
||||
test tests::backtest_schema_initialization_never_deletes_run_data ... ok
|
||||
test tests::backtest_schema_materializes_implementation_identity_for_compact_reads ... ok
|
||||
test tests::build_run_payload_does_not_mark_empty_running_summary_as_available ... ok
|
||||
test tests::build_run_payload_exposes_only_canonical_strategy_detail ... ok
|
||||
test tests::build_run_payload_marks_failed_error_summary_as_available ... ok
|
||||
test tests::build_run_payload_marks_terminal_metric_summary_as_available ... ok
|
||||
test tests::build_run_summary_preview_preserves_strategy_runtime_rebalance_signal ... ok
|
||||
test tests::build_run_summary_preview_preserves_terminal_audit ... ok
|
||||
test tests::canonical_result_is_recovered_from_runner_summary ... ok
|
||||
test tests::capacity_override_is_frozen_once_without_changing_execution_risk ... ok
|
||||
test tests::clear_backtest_list_cache_clears_compact_run_cache ... ok
|
||||
test tests::compact_result_handler_uses_single_pg_lookup ... ok
|
||||
test tests::compact_summary_only_runtime_select_avoids_config_json ... ok
|
||||
test tests::compact_summary_sql_removes_heavy_result_artifacts ... ok
|
||||
test tests::daily_execution_matching_type_normalizes_next_open_aliases_and_rejects_other_aliases ... ok
|
||||
test tests::default_backtest_concurrency_scales_with_available_cpu_threads ... ok
|
||||
test tests::default_backtest_source_table_uses_strategy_factory_source_lake ... ok
|
||||
test tests::detail_rows_query_page_size_maps_to_offset_limit ... ok
|
||||
test tests::equity_rows_hide_diagnostics_unless_explicitly_requested ... ok
|
||||
test tests::execution_cost_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::execution_dynamic_slippage_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::execution_matching_type_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::execution_percent_slippage_alias_normalizes_to_price_ratio ... ok
|
||||
test tests::execution_rebalance_cash_mode_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::execution_risk_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::execution_risk_policy_rejects_conflicting_alias_values ... ok
|
||||
test tests::execution_risk_policy_rejects_non_object ... ok
|
||||
test tests::execution_risk_policy_rejects_unknown_fields_before_queueing ... ok
|
||||
test tests::execution_sell_then_buy_delay_slippage_requires_sell_then_buy_mode ... ok
|
||||
test tests::execution_slippage_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::execution_top_level_risk_enabled_aliases_are_frozen_into_strategy_spec ... ok
|
||||
test tests::explicit_cost_override_removes_old_aliases_and_preserves_zero_minimum ... ok
|
||||
test tests::history_query_cache_has_a_bounded_entry_budget ... ok
|
||||
test tests::history_search_is_bounded_and_retains_literal_characters ... ok
|
||||
test tests::implementation_identity_freezes_source_and_binary_generation ... ok
|
||||
test tests::manual_observations_are_run_scoped_and_private_in_the_runner_bundle ... ok
|
||||
test tests::minute_execution_matching_type_uses_minute_price_and_rejects_daily_modes ... ok
|
||||
test tests::persisted_runtime_projection_never_guesses_missing_frequency ... ok
|
||||
test tests::persisted_runtime_projection_retains_minute_mode_and_frozen_bundle ... ok
|
||||
test tests::process_event_projection_uses_stable_kind_and_filters ... ok
|
||||
test tests::projected_fact_block_cache_key_is_shared_across_run_manifests ... ok
|
||||
test tests::public_backtest_projection_removes_internal_paths_recursively ... ok
|
||||
test tests::public_event_message_removes_internal_paths ... ok
|
||||
test tests::public_result_store_manifest_hides_internal_fact_blocks ... ok
|
||||
test tests::public_run_projection_does_not_reintroduce_internal_paths ... ok
|
||||
test tests::result_artifact_cache_keeps_shared_fact_blocks_after_run_eviction ... ok
|
||||
test tests::result_artifact_cache_rejects_conflicting_reference_for_same_sha ... ok
|
||||
test tests::result_block_page_plan_reads_only_overlapping_blocks ... ok
|
||||
test tests::runner_strategy_spec_path_prefers_bundle ... ok
|
||||
test tests::runtime_frequency_normalizes_and_overrides_strategy_spec_for_runner ... ok
|
||||
test tests::runtime_frozen_bundle_is_written_to_strategy_metadata_for_runner ... ok
|
||||
test tests::runtime_projection_migration_preserves_frozen_config ... ok
|
||||
test tests::runtime_symbols_override_strategy_spec_for_runner ... ok
|
||||
test tests::source_lake_preflight_rejects_unsupported_minute_cash_mode ... ok
|
||||
test tests::source_lake_run_contract_preflight_freezes_runtime_and_execution ... ok
|
||||
test tests::source_lake_runtime_requires_frozen_backtest_bundle_contract ... ok
|
||||
test tests::start_backtest_run_rejects_unready_data_source_before_persistence ... ok
|
||||
test tests::startup_reconciliation_closes_orphaned_runs_and_records_audit_event ... ok
|
||||
test tests::strategy_detail_keeps_strategy_body_without_full_config ... ok
|
||||
test tests::strategy_detail_returns_none_without_strategy_body ... ok
|
||||
test tests::strategy_expression_preflight_rejects_invalid_dsl_before_data_load ... ok
|
||||
test tests::summary_list_projection_keeps_table_metrics_and_removes_detail_payloads ... ok
|
||||
test tests::terminal_audit_accepts_deferred_intents_and_checks_the_retained_count ... ok
|
||||
test tests::terminal_audit_contract_rejects_missing_or_inconsistent_state ... ok
|
||||
test tests::top_level_limits_replace_all_nested_execution_aliases_before_preflight ... ok
|
||||
test tests::typed_holding_projection_derives_weight_and_final_snapshot ... ok
|
||||
test tests::typed_result_rows_filter_and_paginate_without_json_table_roundtrip ... ok
|
||||
test tests::typed_risk_filter_applies_execution_fields ... ok
|
||||
test event_stream::tests::replay_buffer_is_bounded_and_completed_streams_are_evicted_without_closing_subscribers ... ok
|
||||
test result_analysis_tests::complete_performance_and_paged_equity_share_corrected_drawdown ... ok
|
||||
test execution_ledger::tests::combined_pages_filter_before_count_and_keep_native_pages_bounded ... ok
|
||||
|
||||
test result: ok. 134 passed; 0 failed; 8 ignored; 0 measured; 0 filtered out; finished in 9.15s
|
||||
|
||||
Running unittests examples/risk_projection_readonly.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/examples/risk_projection_readonly-b7328e36ec4d5bbd)
|
||||
|
||||
running 4 tests
|
||||
test risk_projection::tests::canonical_null_overrides_all_legacy_aliases_but_real_zero_survives ... ok
|
||||
test risk_projection::tests::unavailable_contract_does_not_relabel_legacy_zero_as_a_verified_statistic ... ok
|
||||
test risk_projection::tests::original_contract_reprojects_legacy_zero_and_preserves_cash_flow_facts ... ok
|
||||
test risk_projection::tests::absent_original_rates_preserve_independent_metrics_without_network_or_zero_rate_claim ... ok
|
||||
|
||||
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running unittests examples/stock_pool_contract_probe.rs (/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb/debug/examples/stock_pool_contract_probe-58c8f70095cabf04)
|
||||
|
||||
running 1 test
|
||||
test stock_pool_candidates::tests::resolve_binds_input_and_preserves_empty_automatic_days ... ok
|
||||
|
||||
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
{
|
||||
"artifact_bytes": 8192,
|
||||
"artifact_id": "strategy_research_59f5a031d3cc7e0e5080c91e",
|
||||
"artifact_path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/validation-receipt.json",
|
||||
"artifact_sha256": "097101988e5f6875c8933012534cfa1ca72f9e2742c04d002aba7d00a69fc346",
|
||||
"artifact_type": "research_source_scope_probe",
|
||||
"contract_sha256": "054241b3df4e522159691c6ee27506667790f73a04c42989b87b5504cf587ee4",
|
||||
"created_at": "2026-10-04T22:08:19Z",
|
||||
"created_by": "boris",
|
||||
"department_id": null,
|
||||
"organization_id": "local_org",
|
||||
"persisted_at": "2026-10-04T22:08:19Z",
|
||||
"production_ready": false,
|
||||
"record_json": {
|
||||
"all_attempt_log_references": [
|
||||
{
|
||||
"bytes": 3625,
|
||||
"compiler_failure": false,
|
||||
"complete_success": false,
|
||||
"failed": 2,
|
||||
"ignored": 0,
|
||||
"passed": 20,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-0bbeed4/initial-targeted-linux-tests.log",
|
||||
"sha256": "9684e4e1c10aabe426c15cd408bb61b198ada8eed1258231e4b81fc30716ac49"
|
||||
},
|
||||
{
|
||||
"bytes": 82467,
|
||||
"compiler_failure": false,
|
||||
"complete_success": false,
|
||||
"failed": 1,
|
||||
"ignored": 7,
|
||||
"passed": 798,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-0caac44/full-linux-tests-attempt1.log",
|
||||
"sha256": "9535151bd7a29a5f56265ef35dbff89b3e48729688cb34c32aa8a8fc9347e41c"
|
||||
},
|
||||
{
|
||||
"bytes": 95343,
|
||||
"compiler_failure": false,
|
||||
"complete_success": false,
|
||||
"failed": 4,
|
||||
"ignored": 9,
|
||||
"passed": 923,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-4eb4c17/full-linux-tests-attempt2.log",
|
||||
"sha256": "db18ad5e876ce482dc1f76ac244e170a6095c2ead8a991af5dce45e72af8e9ac"
|
||||
},
|
||||
{
|
||||
"bytes": 98381,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"passed": 967,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-05315f0/full-linux-tests-attempt3.log",
|
||||
"sha256": "66879f838ec21001cb3e6c284f497cfb6a80c2d3a724ee9cb04e09476afc101b"
|
||||
},
|
||||
{
|
||||
"bytes": 98381,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"passed": 967,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-a267052/final-full-linux-tests.log",
|
||||
"sha256": "b565d8b983fb87881974da37ad5bd0721db3f51acda3af0cbe1b62406f18371c"
|
||||
},
|
||||
{
|
||||
"bytes": 98381,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"passed": 967,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-7d7cce8/final-full-linux-tests.log",
|
||||
"sha256": "b8f7a20a93335801950e40ce3a7124ae039e84fbb1db7dd821c314d2b05b4447"
|
||||
},
|
||||
{
|
||||
"bytes": 98572,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"passed": 969,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-ce46c13/final-full-linux-tests.log",
|
||||
"sha256": "cfd1cb81fde9f34991a54a54e2a734844bd8fab52292e043856b08d30090d19a"
|
||||
},
|
||||
{
|
||||
"bytes": 10543,
|
||||
"compiler_failure": true,
|
||||
"complete_success": false,
|
||||
"failed": 0,
|
||||
"ignored": 0,
|
||||
"passed": 0,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1fa73bb/full-linux-tests-attempt1.log",
|
||||
"sha256": "e758be915373f707a460e7d08ab4162f3b80d86b98091c555cb3351c2bb4d7b8"
|
||||
},
|
||||
{
|
||||
"bytes": 65133,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 17,
|
||||
"passed": 613,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ffc6e1f/full-linux-tests-attempt2.log",
|
||||
"sha256": "d053e4e432cbe0b198f85ceee349b241eac4cf90b8e6a75289c322c59538b2b7"
|
||||
},
|
||||
{
|
||||
"bytes": 65133,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 17,
|
||||
"passed": 613,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/final-full-linux-tests.log",
|
||||
"sha256": "6f2e8974a90d5cdbce788e2744a1276dff9fe94b79958869cfb0730c096d1a42"
|
||||
},
|
||||
{
|
||||
"bytes": 65258,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 17,
|
||||
"passed": 614,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/final-full-linux-tests.log",
|
||||
"sha256": "2ba99fb0198a68ff5bc3d6341b1cdeb3da1a4d0ca5496b2faa3e79e3494ae434"
|
||||
}
|
||||
],
|
||||
"contract_sha256": "5b4389afc3bbda8f30cbe141cc2a05af75bc0e69724aa04c09b258b98326105f",
|
||||
"core_test_result": {
|
||||
"bytes": 98572,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"passed": 969,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-ce46c13/final-full-linux-tests.log",
|
||||
"sha256": "cfd1cb81fde9f34991a54a54e2a734844bd8fab52292e043856b08d30090d19a"
|
||||
},
|
||||
"corporate_action_cache_schema_version": 5,
|
||||
"engine_commit": "ce46c13f37f028393e689babbe5c348d482d3a32",
|
||||
"fractional_quantity_requires_explicit_settlement_rule": true,
|
||||
"full_source_event_and_revision_scope_verified": false,
|
||||
"new_runtime_release_performed": false,
|
||||
"numeric_research_budget_reopened": false,
|
||||
"observed_at": "2026-10-04T22:08:10.219296+00:00",
|
||||
"operational_verification_reference": {
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/operational-verification.json",
|
||||
"sha256": "3ce2ba78ed28f549ddc675429565e578fe61eee1db4374c375e5049761c8fd06"
|
||||
},
|
||||
"owner_uid": 1003,
|
||||
"pending_share_successor_conversion_without_carry_terms_rejected": true,
|
||||
"performance_speedup_claimed": false,
|
||||
"prior_validation_pg_artifact": "strategy_research_cb32517064d7558f69713001",
|
||||
"prior_validation_reference": {
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/validation-receipt.json",
|
||||
"sha256": "5240dbf5f1d7382531a4c0dce5fcdb722f4d1137a41854269159b61f0236d412"
|
||||
},
|
||||
"production_ready": false,
|
||||
"production_runtime_deployed": false,
|
||||
"production_strategy_count": 0,
|
||||
"registered_share_entitlements_separate_from_delivered_and_sellable_quantity": true,
|
||||
"resource_contract_live_verification_reference": {
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/live-test-resource-verification.json",
|
||||
"sha256": "1686248392f29ae93f023ff147fcc0abb06b6b2fb15a9806b5718e320ed75a40"
|
||||
},
|
||||
"resource_contract_requested": {
|
||||
"build_jobs": 4,
|
||||
"core_target_path": "/tmp/fidc-cash-receivable-build-20261005-af655de",
|
||||
"cpu_quota_percent": 400,
|
||||
"incremental": false,
|
||||
"locked": true,
|
||||
"memory_max_bytes": 17179869184,
|
||||
"memory_swap_max_bytes": 0,
|
||||
"offline": true,
|
||||
"resource_properties_after_transient_scope_exit_are_not_live_validation": true,
|
||||
"runner_target_path": "/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb",
|
||||
"rustc": "rustc 1.98.0 (88d9e12ae 2026-08-18)",
|
||||
"target_filesystem": "executable_tmpfs",
|
||||
"tasks_max": 256,
|
||||
"toolchain": "stable-x86_64-unknown-linux-gnu"
|
||||
},
|
||||
"runner_api_test_result": {
|
||||
"bytes": 65258,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 17,
|
||||
"passed": 614,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/final-full-linux-tests.log",
|
||||
"sha256": "2ba99fb0198a68ff5bc3d6341b1cdeb3da1a4d0ca5496b2faa3e79e3494ae434"
|
||||
},
|
||||
"schema_version": "fidc.linux-registered-share-delivery-validation/v1",
|
||||
"service_commit": "1d4b32e18d3836b5844130ae0c95d90247768882",
|
||||
"share_receivable_assets_included_in_equity_and_target_sizing": true,
|
||||
"simulation_rounding_fixture_not_official_settlement_evidence": true,
|
||||
"source_pair_root": "/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1d4b32e",
|
||||
"source_publication_times_admitted_as_PIT": false,
|
||||
"source_rows_semantic_sha256": "eec65df828f97a3e51827cb6b0e9ec2c86079a333481d55ec9eb6ab2845d73c5",
|
||||
"source_rows_semantic_version": "v10",
|
||||
"source_transport_commit": "12eeae5d570004499a30f7faa42c0da93c1578cc",
|
||||
"status": "core_and_runner_api_regressions_passed_not_source_runtime_or_strategy_acceptance",
|
||||
"trading_allowed": false,
|
||||
"two_framework_financial_reconciliation_completed": false,
|
||||
"unknown_delivery_date_not_defaulted_to_ex_date": true,
|
||||
"unknown_distribution_quantities_not_defaulted_to_zero": true
|
||||
},
|
||||
"research_key": "fidc-registered-share-delivery-validation-20261005-ce46c13-1d4b32e",
|
||||
"schema_table": "strategy_research_artifact",
|
||||
"schema_version": "fidc.linux-registered-share-delivery-validation/v1",
|
||||
"status": "core_and_runner_api_regressions_passed_not_source_runtime_or_strategy_acceptance",
|
||||
"storage": "postgres"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"schema_version": "fidc.validation-probe-assembly-failure/v1",
|
||||
"recorded_on": "2026-10-05",
|
||||
"error_type": "SyntaxError",
|
||||
"reported_frames": [
|
||||
{
|
||||
"function": "<module>",
|
||||
"line": 6
|
||||
}
|
||||
],
|
||||
"request_path": "/tmp/fidc-share-delivery-quantity-request-20261005.json",
|
||||
"request_sha256": "2a91810a4d30ae811fa8dfb010ac7ce761d9bdfd144af608318886617292fed7",
|
||||
"cause": "probe string replacement left a dot immediately before an equality operator",
|
||||
"remote_probe_code_execution_started": false,
|
||||
"remote_source_or_test_outcome_unknown": false,
|
||||
"correction": "assembled probe compiled locally before executing corrected validation",
|
||||
"corrected_test_source_pair": "/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1d4b32e",
|
||||
"production_ready": false,
|
||||
"trading_allowed": false
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
{
|
||||
"all_attempt_log_references": [
|
||||
{
|
||||
"bytes": 3625,
|
||||
"compiler_failure": false,
|
||||
"complete_success": false,
|
||||
"failed": 2,
|
||||
"ignored": 0,
|
||||
"passed": 20,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-0bbeed4/initial-targeted-linux-tests.log",
|
||||
"sha256": "9684e4e1c10aabe426c15cd408bb61b198ada8eed1258231e4b81fc30716ac49"
|
||||
},
|
||||
{
|
||||
"bytes": 82467,
|
||||
"compiler_failure": false,
|
||||
"complete_success": false,
|
||||
"failed": 1,
|
||||
"ignored": 7,
|
||||
"passed": 798,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-0caac44/full-linux-tests-attempt1.log",
|
||||
"sha256": "9535151bd7a29a5f56265ef35dbff89b3e48729688cb34c32aa8a8fc9347e41c"
|
||||
},
|
||||
{
|
||||
"bytes": 95343,
|
||||
"compiler_failure": false,
|
||||
"complete_success": false,
|
||||
"failed": 4,
|
||||
"ignored": 9,
|
||||
"passed": 923,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-4eb4c17/full-linux-tests-attempt2.log",
|
||||
"sha256": "db18ad5e876ce482dc1f76ac244e170a6095c2ead8a991af5dce45e72af8e9ac"
|
||||
},
|
||||
{
|
||||
"bytes": 98381,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"passed": 967,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-05315f0/full-linux-tests-attempt3.log",
|
||||
"sha256": "66879f838ec21001cb3e6c284f497cfb6a80c2d3a724ee9cb04e09476afc101b"
|
||||
},
|
||||
{
|
||||
"bytes": 98381,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"passed": 967,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-a267052/final-full-linux-tests.log",
|
||||
"sha256": "b565d8b983fb87881974da37ad5bd0721db3f51acda3af0cbe1b62406f18371c"
|
||||
},
|
||||
{
|
||||
"bytes": 98381,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"passed": 967,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-7d7cce8/final-full-linux-tests.log",
|
||||
"sha256": "b8f7a20a93335801950e40ce3a7124ae039e84fbb1db7dd821c314d2b05b4447"
|
||||
},
|
||||
{
|
||||
"bytes": 98572,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"passed": 969,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-ce46c13/final-full-linux-tests.log",
|
||||
"sha256": "cfd1cb81fde9f34991a54a54e2a734844bd8fab52292e043856b08d30090d19a"
|
||||
},
|
||||
{
|
||||
"bytes": 10543,
|
||||
"compiler_failure": true,
|
||||
"complete_success": false,
|
||||
"failed": 0,
|
||||
"ignored": 0,
|
||||
"passed": 0,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1fa73bb/full-linux-tests-attempt1.log",
|
||||
"sha256": "e758be915373f707a460e7d08ab4162f3b80d86b98091c555cb3351c2bb4d7b8"
|
||||
},
|
||||
{
|
||||
"bytes": 65133,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 17,
|
||||
"passed": 613,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ffc6e1f/full-linux-tests-attempt2.log",
|
||||
"sha256": "d053e4e432cbe0b198f85ceee349b241eac4cf90b8e6a75289c322c59538b2b7"
|
||||
},
|
||||
{
|
||||
"bytes": 65133,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 17,
|
||||
"passed": 613,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/final-full-linux-tests.log",
|
||||
"sha256": "6f2e8974a90d5cdbce788e2744a1276dff9fe94b79958869cfb0730c096d1a42"
|
||||
},
|
||||
{
|
||||
"bytes": 65258,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 17,
|
||||
"passed": 614,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/final-full-linux-tests.log",
|
||||
"sha256": "2ba99fb0198a68ff5bc3d6341b1cdeb3da1a4d0ca5496b2faa3e79e3494ae434"
|
||||
}
|
||||
],
|
||||
"contract_sha256": "5b4389afc3bbda8f30cbe141cc2a05af75bc0e69724aa04c09b258b98326105f",
|
||||
"core_test_result": {
|
||||
"bytes": 98572,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 9,
|
||||
"passed": 969,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-receivable-validation-20261005-ce46c13/final-full-linux-tests.log",
|
||||
"sha256": "cfd1cb81fde9f34991a54a54e2a734844bd8fab52292e043856b08d30090d19a"
|
||||
},
|
||||
"corporate_action_cache_schema_version": 5,
|
||||
"engine_commit": "ce46c13f37f028393e689babbe5c348d482d3a32",
|
||||
"fractional_quantity_requires_explicit_settlement_rule": true,
|
||||
"full_source_event_and_revision_scope_verified": false,
|
||||
"new_runtime_release_performed": false,
|
||||
"numeric_research_budget_reopened": false,
|
||||
"observed_at": "2026-10-04T22:08:10.219296+00:00",
|
||||
"operational_verification_reference": {
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/operational-verification.json",
|
||||
"sha256": "3ce2ba78ed28f549ddc675429565e578fe61eee1db4374c375e5049761c8fd06"
|
||||
},
|
||||
"owner_uid": 1003,
|
||||
"pending_share_successor_conversion_without_carry_terms_rejected": true,
|
||||
"performance_speedup_claimed": false,
|
||||
"prior_validation_pg_artifact": "strategy_research_cb32517064d7558f69713001",
|
||||
"prior_validation_reference": {
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-ce46c13/validation-receipt.json",
|
||||
"sha256": "5240dbf5f1d7382531a4c0dce5fcdb722f4d1137a41854269159b61f0236d412"
|
||||
},
|
||||
"production_ready": false,
|
||||
"production_runtime_deployed": false,
|
||||
"production_strategy_count": 0,
|
||||
"registered_share_entitlements_separate_from_delivered_and_sellable_quantity": true,
|
||||
"resource_contract_live_verification_reference": {
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/live-test-resource-verification.json",
|
||||
"sha256": "1686248392f29ae93f023ff147fcc0abb06b6b2fb15a9806b5718e320ed75a40"
|
||||
},
|
||||
"resource_contract_requested": {
|
||||
"build_jobs": 4,
|
||||
"core_target_path": "/tmp/fidc-cash-receivable-build-20261005-af655de",
|
||||
"cpu_quota_percent": 400,
|
||||
"incremental": false,
|
||||
"locked": true,
|
||||
"memory_max_bytes": 17179869184,
|
||||
"memory_swap_max_bytes": 0,
|
||||
"offline": true,
|
||||
"resource_properties_after_transient_scope_exit_are_not_live_validation": true,
|
||||
"runner_target_path": "/tmp/fidc-share-delivery-runner-build-20261005-1fa73bb",
|
||||
"rustc": "rustc 1.98.0 (88d9e12ae 2026-08-18)",
|
||||
"target_filesystem": "executable_tmpfs",
|
||||
"tasks_max": 256,
|
||||
"toolchain": "stable-x86_64-unknown-linux-gnu"
|
||||
},
|
||||
"runner_api_test_result": {
|
||||
"bytes": 65258,
|
||||
"compiler_failure": false,
|
||||
"complete_success": true,
|
||||
"failed": 0,
|
||||
"ignored": 17,
|
||||
"passed": 614,
|
||||
"path": "/data/fidc-cold/autonomous-research/fidc-share-delivery-pair-validation-20261005-1d4b32e/final-full-linux-tests.log",
|
||||
"sha256": "2ba99fb0198a68ff5bc3d6341b1cdeb3da1a4d0ca5496b2faa3e79e3494ae434"
|
||||
},
|
||||
"schema_version": "fidc.linux-registered-share-delivery-validation/v1",
|
||||
"service_commit": "1d4b32e18d3836b5844130ae0c95d90247768882",
|
||||
"share_receivable_assets_included_in_equity_and_target_sizing": true,
|
||||
"simulation_rounding_fixture_not_official_settlement_evidence": true,
|
||||
"source_pair_root": "/srv/fidc/programs/src/fidc-share-delivery-pair-validation-20261005-1d4b32e",
|
||||
"source_publication_times_admitted_as_PIT": false,
|
||||
"source_rows_semantic_sha256": "eec65df828f97a3e51827cb6b0e9ec2c86079a333481d55ec9eb6ab2845d73c5",
|
||||
"source_rows_semantic_version": "v10",
|
||||
"source_transport_commit": "12eeae5d570004499a30f7faa42c0da93c1578cc",
|
||||
"status": "core_and_runner_api_regressions_passed_not_source_runtime_or_strategy_acceptance",
|
||||
"trading_allowed": false,
|
||||
"two_framework_financial_reconciliation_completed": false,
|
||||
"unknown_delivery_date_not_defaulted_to_ex_date": true,
|
||||
"unknown_distribution_quantities_not_defaulted_to_zero": true
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user