Compare commits
116 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 | |||
| 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;
|
||||
@@ -35,6 +43,7 @@ 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;
|
||||
@@ -52,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::{
|
||||
@@ -75,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,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
@@ -0,0 +1,809 @@
|
||||
Compiling fidc-core v0.1.0 (/srv/fidc/programs/src/fidc-share-receivable-validation-20261005-4a41057/crates/fidc-core)
|
||||
Finished `test` profile [unoptimized] target(s) in 42.79s
|
||||
Running unittests src/lib.rs (/tmp/fidc-cash-receivable-build-20261005-af655de/debug/deps/fidc_core-dbea4d4f6cb5d2d7)
|
||||
|
||||
running 789 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::an_explicit_ioc_or_fok_does_not_become_a_persistent_algorithm ... 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::a_clock_slice_does_not_turn_window_twap_into_an_unlimited_instant_order ... 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::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::non_matching_controls_amend_or_cancel_without_filling_a_crossing_quote ... ok
|
||||
test broker::tests::algorithm_clock::partial_algorithm_cancel_releases_reservation_and_never_executes_the_remainder ... ok
|
||||
test broker::tests::algorithm_clock::two_working_algorithms_reserve_only_real_cash_without_starving_the_first ... ok
|
||||
test broker::tests::algorithm_clock::twap_clock_preserves_quantity_prices_fees_budget_and_parent_order ... ok
|
||||
test broker::tests::complete_daily_target_batch_uses_positive_target_count_for_slot_limit ... ok
|
||||
test broker::tests::algorithm_clock::separate_buy_cannot_spend_the_working_algorithms_cash_budget ... ok
|
||||
test broker::tests::daily_capacity_requires_a_timed_observation_instead_of_falling_back_to_total_volume ... 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::algorithm_clock::vwap_clock_preserves_cash_costs_and_does_not_spend_future_volume ... ok
|
||||
test broker::tests::decision_buy_denial_allows_reducing_an_existing_buy ... ok
|
||||
test broker::tests::decision_buy_denial_does_not_rewrite_existing_pending_order ... ok
|
||||
test broker::tests::daily_session_volume_changes_only_audit_not_opening_fills ... ok
|
||||
test broker::tests::decision_buy_denial_blocks_topup_but_allows_sell_and_does_not_leak ... 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::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::independent_target_entry_is_not_subject_to_replacement_slot_limit ... 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_rejects_execution_date_st_even_if_signal_date_was_clean ... 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_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_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::next_open_unpriced_paused_snapshot_prefers_execution_day_pause_risk ... ok
|
||||
test broker::tests::next_open_with_observations_uses_opening_volume_not_daily_totals_or_future_quotes ... ok
|
||||
test broker::tests::open_order_upsert_replaces_in_place_and_preserves_queue_position ... ok
|
||||
test broker::tests::pending_registered_shares_do_not_trigger_duplicate_target_buys ... ok
|
||||
test broker::tests::post_close_phase_is_derived_from_actual_same_day_submission_time ... ok
|
||||
test broker::tests::queued_order_retains_the_real_creation_clock_when_retried ... ok
|
||||
test broker::tests::post_close_order_uses_close_without_slippage_and_waits_until_matching_window ... 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_engine_drives_the_pending_buy_without_a_minute_strategy_callback ... ok
|
||||
test broker::tests::stock_pool_new_signal_supersedes_the_unsubmitted_buy_phase ... ok
|
||||
test broker::tests::stock_pool_partial_sell_waits_for_the_whole_batch_and_never_reissues_buys ... ok
|
||||
test broker::tests::stock_pool_pending_phase_cannot_cross_the_execution_session ... 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_custom_valuation_does_not_create_limit_orders ... ok
|
||||
test broker::tests::target_portfolio_smart_budgets_each_buy_before_cash_optimization ... 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::timed_target_value_zero_sells_full_position_at_requested_time_quote ... ok
|
||||
test broker::tests::failed_resting_order_batch_keeps_accepted_orders_and_unpublished_financial_state ... ok
|
||||
test broker::tests::twap_earlier_fills_do_not_depend_on_later_quote_liquidity_or_count ... 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_process_uses_latest_quote_before_decision_time ... ok
|
||||
test broker::tests::value_buy_rejects_when_slippage_clamps_execution_price_to_upper_limit ... ok
|
||||
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::a_late_corporate_batch_failure_keeps_prior_cash_positions_and_notes ... ok
|
||||
test corporate_book::tests::entitlement_survives_complete_sale_and_a_successor_unit_conversion ... ok
|
||||
test corporate_book::tests::corporate_quantity_overflow_fails_without_saturation_or_a_negative_delta ... ok
|
||||
test corporate_book::tests::flat_position_retirement_does_not_change_issued_lot_state_or_next_purchase_origin ... ok
|
||||
test corporate_book::tests::explicitly_delayed_dividend_remains_receivable_until_actual_payment ... 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::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
|
||||
test corporate_book::tests::successor_without_frozen_instrument_metadata_is_not_an_implicit_new_security ... ok
|
||||
test corporate_book::tests::terminated_security_keeps_paid_cash_and_is_not_recreated_by_reinvestment ... ok
|
||||
test corporate_book::tests::successor_conversion_cannot_discard_unresolved_share_entitlements ... ok
|
||||
test corporate_book::tests::tiny_known_cash_rate_is_preserved_and_cannot_waive_payment_or_record_evidence ... ok
|
||||
test corporate_book::tests::unresolved_delisting_cannot_zero_a_pending_share_asset ... FAILED
|
||||
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 corporate_book::tests::invalid_successor_terms_cannot_be_replaced_with_one_share_or_zero_cash ... ok
|
||||
test corporate_book::tests::verified_conversion_preserves_lots_without_creating_orders_or_fills ... ok
|
||||
test daily_patterns::tests::breakout_retest_needs_a_later_retest_not_the_breakout_candle_itself ... ok
|
||||
test daily_patterns::tests::daily_patterns_adjusts_all_prices_not_volume ... 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_no_missing_data_fallback ... ok
|
||||
test daily_patterns::tests::daily_patterns_all_templates_and_score_absence ... 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::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::additional_terminal_calendar_dates_are_isolated_from_shared_market_data ... ok
|
||||
test data::tests::current_close_average_uses_backward_adjustment_factor_and_current_base ... ok
|
||||
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::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::direct_symbol_id_snapshot_lookups_preserve_alignment_for_sparse_rows ... ok
|
||||
test data::tests::eligible_universe_does_not_require_candidate_risk_state_when_selection_risk_is_disabled ... 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::eligible_universe_uses_decision_market_cap_same_date ... ok
|
||||
test data::tests::execution_quotes_use_stable_k_way_merge_and_release_by_date ... ok
|
||||
test data::tests::factor_snapshot_rejects_legacy_or_invalid_adjustment_storage ... 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::parallel_daily_symbol_indices_match_scalar_for_sparse_and_empty_days ... ok
|
||||
test data::tests::replacing_execution_quotes_preserves_duplicate_timestamp_rows ... ok
|
||||
test data::tests::series_columns_preserve_full_snapshots_and_distinct_price_bits ... ok
|
||||
test data::tests::series_end_position_index_preserves_decision_and_current_boundaries ... 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::source_volume_contract_rejects_windows_containing_missing_values ... ok
|
||||
test data::tests::symbol_price_series_test_constructor_sorts_unsorted_rows ... ok
|
||||
test data::tests::unique_dataset_applies_sparse_intraday_overlay_to_daily_and_symbol_views ... ok
|
||||
test data::tests::parallel_daily_bundle_validation_keeps_earliest_error_and_component_order ... ok
|
||||
test decision_projection::tests::projection_contract_does_not_shadow_core_market_or_typed_fields ... ok
|
||||
test decision_projection::tests::exact_projection_coverage_and_classified_absence_are_receipted ... ok
|
||||
test decision_projection::tests::projection_rejects_future_drift_or_unbound_receipts ... ok
|
||||
test data::tests::volume_rolling_ignores_zero_volume_rows_for_source_and_legacy_data ... 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 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::compact_manual_progress_keeps_exact_applications_and_fallible_delivery_stops_the_run ... ok
|
||||
test engine::tests::decision_projection_failure_never_reaches_strategy_and_does_not_blindly_retry ... ok
|
||||
test engine::tests::decision_projection_rejects_future_response_before_any_strategy_callback ... ok
|
||||
test engine::tests::decision_projection_refuses_static_scores_and_same_session_inputs ... ok
|
||||
test engine::tests::decision_projection_rejects_missing_calendar_inputs_instead_of_dropping_a_session ... ok
|
||||
test engine::tests::decision_projection_is_called_before_callbacks_and_is_not_shared_or_carried_forward ... 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_complete_targets_use_latest_target_and_next_day_zero_supersedes_them ... ok
|
||||
test engine::tests::late_intents_do_not_wait_for_another_selection_snapshot ... ok
|
||||
test engine::tests::late_imperative_orders_execute_once_in_next_session_with_original_dates_and_new_price ... ok
|
||||
test engine::tests::lifecycle_quote_filter_skips_only_dated_legal_absence_before_loading ... ok
|
||||
test engine::tests::late_opening_callback_is_interleaved_with_earlier_quotes_and_manual_observations ... 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::next_bar_open_executes_last_decision_without_execution_day_factor_snapshot ... ok
|
||||
test engine::tests::minute_observer_never_sees_a_later_fill_from_a_coarse_phase ... ok
|
||||
test engine::tests::next_bar_open_execution_risk_ignores_decision_day_paused_state ... 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_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_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_paused_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_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_ignores_decision_day_paused_and_lower_limit_state ... ok
|
||||
test engine::tests::next_bar_open_sell_risk_rejects_execution_day_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_strategy_context_data_helpers_use_decision_date ... ok
|
||||
test engine::tests::next_bar_open_target_percent_freezes_decision_day_equity ... ok
|
||||
test engine::tests::next_bar_open_target_portfolio_smart_sizes_with_execution_day_open ... ok
|
||||
test engine::tests::observed_manual_sale_updates_the_shared_same_day_rebuy_rule ... ok
|
||||
test engine::tests::next_open_observation_loads_quotes_even_when_execution_price_is_open ... ok
|
||||
test engine::tests::opening_and_late_lifecycle_schedules_do_not_read_past_or_future_manual_state ... ok
|
||||
test engine::tests::opening_schedule_cannot_cross_the_after_trading_phase ... ok
|
||||
test engine::tests::physical_on_day_rules_keep_each_actual_submission_time ... ok
|
||||
test engine::tests::opening_subscription_is_loaded_without_replaying_quotes_before_its_activation ... ok
|
||||
test engine::tests::opening_orders_are_submitted_once_at_their_real_clock_even_with_an_earlier_execution_window ... ok
|
||||
test engine::tests::post_close_phase_callbacks_never_rewind_before_completed_fills ... ok
|
||||
test engine::tests::post_close_wait_has_no_position_before_the_first_actual_matching_tick ... ok
|
||||
test engine::tests::pre_trading_orders_wait_for_the_configured_window_and_keep_their_audit ... ok
|
||||
test engine::tests::process_callbacks_observe_the_completed_minute_trade_and_its_clock ... 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::window_algorithm_cannot_publish_future_fill_quantity_to_earlier_callbacks ... ok
|
||||
test etf_execution::tests::latest_generation_overwrites_pending_targets_and_preserves_candidate_order ... ok
|
||||
test engine::tests::wholly_prelisting_universe_retains_cash_days_without_fabricating_prices ... 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::order_event_status_quantity_contract_is_explicit ... ok
|
||||
test events::tests::fill_execution_timestamp_range_is_explicit_and_backward_compatible ... 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 manual_execution::tests::inconsistent_counts_terminals_audits_and_duplicate_facts_are_rejected ... ok
|
||||
test factor_events::tests::parameter_domains_keep_enumeration_labels_without_debug_string_parsing ... 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::observations_follow_durable_receipt_order_and_not_input_array_order ... ok
|
||||
test manual_execution::tests::partial_cancel_is_valid_but_full_fill_cannot_be_reported_as_cancelled ... ok
|
||||
test manual_execution::tests::runtime_position_events_cannot_claim_observations_after_the_source_cutoff ... ok
|
||||
test manual_execution::tests::raw_timezone_and_cutoff_are_required ... 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::missing_duplicate_and_unproven_limit_states_fail ... 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 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 market_event_context::tests::missing_industry_does_not_invent_groups_or_disable_independent_market_facts ... 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::signal_baseline_uses_same_close_for_strategy_and_benchmark ... ok
|
||||
test metrics::tests::risk_adjusted_metrics_use_daily_pit_rates_and_all_period_downside ... ok
|
||||
test numeric_expr_vm::tests::benchmark_numeric_vm_against_rhai ... ignored, manual release-mode performance evidence
|
||||
test metrics::tests::undefined_ratios_remain_null_in_backtest_metrics ... ok
|
||||
test numeric_expr_vm::tests::evaluates_numeric_boolean_and_if_expressions ... ok
|
||||
test numeric_expr_vm::tests::floating_comparisons_match_rhai_epsilon_semantics ... 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_expr_vm::tests::matches_rhai_for_representative_numeric_boolean_corpus ... ok
|
||||
test numeric_factors::tests::wide_batch_merge_matches_tree_and_preserves_old_key_ownership ... 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::completed_session_factor_dates_exclude_intraday_and_preserve_next_open ... ok
|
||||
test platform_expr_strategy::tests::buy_quote_filter_rejects_missing_intraday_quote_not_daily_close ... ok
|
||||
test platform_expr_strategy::tests::completed_session_bound_value_does_not_read_same_day_future_value ... ok
|
||||
test platform_expr_strategy::tests::current_rolling_helpers_do_not_load_factor_maps_or_decision_rollings ... 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::daily_replacement_reference_records_an_explicit_empty_previous_target ... 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_buy_rejection_keeps_candidate_buy_disabled ... 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_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_uses_remaining_slot_budget ... 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_delays_upper_limit_take_profit_to_configured_open_time ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_marked_total_uses_intraday_last_not_close ... 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_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_scheduled_quote_uses_configured_intraday_time ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_sells_down_before_actionable_take_profit_exit ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_skips_positive_target_adjust_when_position_will_close ... 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_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_weak_market_allows_pending_highlimit_target_adjust_after_open ... 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_emits_daily_position_target_adjustment ... 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_threshold_only_sells_overweight_positions ... 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_retargets_same_ratio_every_day ... ok
|
||||
test platform_expr_strategy::tests::platform_calendar_day_rebalance_interval_can_be_enabled_explicitly ... ok
|
||||
test platform_expr_strategy::tests::platform_aiquant_weak_market_waits_pending_highlimit_target_adjust ... ok
|
||||
test platform_expr_strategy::tests::platform_buy_rejection_allows_lower_limit_buy_when_ask_is_available ... ok
|
||||
test platform_expr_strategy::tests::platform_current_rolling_uses_decision_date_row_not_prior_factor_row ... 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_caps_empty_slot_to_equal_weight_target ... 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_does_not_buy_back_same_day_exit ... 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_releases_unsellable_stop_loss_slot ... 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_skips_current_stop_loss_symbol_like_aiquant ... ok
|
||||
test platform_expr_strategy::tests::platform_day_state_uses_decision_benchmark_close ... ok
|
||||
test platform_expr_strategy::tests::platform_daily_top_up_uses_same_day_sell_cash ... 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_explicit_actions_keep_every_declared_trade_time ... ok
|
||||
test platform_expr_strategy::tests::platform_expr_cost_model_honors_zero_minimum_commission ... 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_delayed_open_partial_exit_releases_top_up_slot ... 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_missing_requested_factor_does_not_default_to_zero ... 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_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_max_holding_days_counts_decision_trading_days_not_calendar_days ... ok
|
||||
test platform_expr_strategy::tests::platform_marked_total_value_uses_current_day_close ... 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_max_holding_days_exit_preempts_take_profit_exit ... 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_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_select_symbols_defers_decision_day_limit_state ... 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_order_value_quantity_includes_buy_commission_budget ... ok
|
||||
test platform_expr_strategy::tests::platform_numeric_stop_loss_is_strictly_below_multiplier ... 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_projected_market_fill_caps_volume_limit ... 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_periodic_rebalance_tops_up_underweight_selected_holding ... 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_projection_does_not_consume_cash_from_unfillable_sell ... ok
|
||||
test platform_expr_strategy::tests::platform_rebalance_keeps_unresolved_delisted_position_without_orders_or_replacement ... ok
|
||||
test platform_expr_strategy::tests::platform_selection_does_not_fill_candidates_with_buy_risk_rejections ... ok
|
||||
test platform_expr_strategy::tests::platform_selection_and_buy_respect_allow_flags_are_configurable ... ok
|
||||
test platform_expr_strategy::tests::platform_selection_expands_pct_change_in_filter_and_rank_expr ... ok
|
||||
test platform_expr_strategy::tests::platform_selection_kcb_switch_overrides_legacy_universe_exclude ... 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_ranks_with_previous_factor_date ... ok
|
||||
test broker::tests::simulator_transaction_profile_preserves_successful_output ... 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 platform_expr_strategy::tests::platform_stock_expr_exposes_star_st_and_bjse_fields_without_default_selection_risk ... 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_expr_rejects_when_volume_ma5_exceeds_market_volume_ma100 ... 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_state_uses_factor_date_for_selection_market_fields ... ok
|
||||
test platform_expr_strategy::tests::platform_stop_loss_residual_pending_rechecks_signal_next_day ... 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_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_emits_algo_order_actions ... 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_explicit_actions_when_rotation_is_disabled ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_emits_target_portfolio_smart_algo_order_style ... ok
|
||||
test platform_expr_strategy::tests::platform_stop_take_fraction_values_are_return_thresholds ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_emits_target_shares_explicit_action ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_emits_target_portfolio_smart_explicit_action ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_emits_universe_management_actions ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_exposes_open_order_runtime_fields ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_exposes_advanced_data_runtime_helpers ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_exposes_position_lifecycle_runtime_fields ... 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_filters_target_portfolio_smart_by_subscription_guard ... ok
|
||||
test platform_expr_strategy::tests::platform_strategy_skips_explicit_actions_when_schedule_does_not_match ... 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_strategy_treats_missing_stock_rolling_window_as_filter_reject ... ok
|
||||
test platform_expr_strategy::tests::platform_take_profit_default_uses_position_cost_basis ... ok
|
||||
test platform_expr_strategy::tests::platform_take_profit_false_does_not_track_upper_limit_pending_sell ... ok
|
||||
test platform_expr_strategy::tests::platform_take_profit_sells_position_at_upper_limit ... 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_increase_before_intraday_stop_loss_sell_blocked ... ok
|
||||
test platform_expr_strategy::tests::platform_weak_market_skips_target_adjust_before_actionable_stop_loss ... ok
|
||||
test platform_expr_strategy::tests::selection_buffer_retains_held_symbols_within_buffer_rank ... ok
|
||||
test platform_expr_strategy::tests::platform_weak_market_skips_target_increase_before_lower_limit_blocked_stop_loss ... ok
|
||||
test platform_expr_strategy::tests::projected_historical_slippage_does_not_swallow_missing_calibration ... ok
|
||||
test platform_expr_strategy::tests::selection_quote_plan_filters_daily_part_before_minute_candidates ... ok
|
||||
test platform_expr_strategy::tests::selection_replacement_limit_caps_voluntary_daily_removals ... 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::selection_quote_plan_keeps_configured_static_selection_risk_for_intraday_fields ... ok
|
||||
test platform_expr_strategy::tests::selection_limit_state_requires_intraday_selection_quotes ... 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::signal_date_explicit_actions_are_due_only_on_declared_snapshot_dates ... ok
|
||||
test platform_expr_strategy::tests::portfolio_loss_observes_finalized_nav_after_fees_and_cash_flows ... ok
|
||||
test platform_expr_strategy::tests::stock_pool_suspension_resolution_never_reads_the_future_execution_day ... 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::target_portfolio_smart_exposes_decision_and_execution_dates ... ok
|
||||
test platform_expr_strategy::tests::stock_snapshot_fields_are_loaded_only_when_the_strategy_uses_them ... ok
|
||||
test platform_expr_strategy::tests::target_portfolio_smart_respects_current_date_weight_conditions ... ok
|
||||
test platform_expr_strategy::tests::target_scale_replenishment_preserves_strategy_cash_allocation ... ok
|
||||
test platform_expr_strategy::tests::strategy105_numeric_expressions_compile_to_vm ... 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_strategy_spec::tests::accepts_scoped_blacklist_context_in_runtime_risk_policy ... ok
|
||||
test platform_expr_strategy::tests::timed_rotation_runs_on_its_schedule_instead_of_the_untimed_day_hook ... 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::index_throttle_uses_signal_ma_not_performance_benchmark_ma ... ok
|
||||
test platform_strategy_spec::tests::frozen_candidate_order_is_explicit_and_preserves_source_positions ... ok
|
||||
test platform_strategy_spec::tests::multiple_trade_times_do_not_imply_delayed_limit_exit ... ok
|
||||
test platform_strategy_spec::tests::native_factor_bindings_declare_completed_session_fields ... 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_delayed_limit_open_exit ... ok
|
||||
test platform_strategy_spec::tests::parses_explicit_historical_slippage_into_platform_config ... 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_expr_strategy::tests::timed_current_close_stop_loss_reloads_state_after_quote_arrives ... ok
|
||||
test platform_strategy_spec::tests::parses_max_holding_days_from_engine_and_runtime_trading ... 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_portfolio_drawdown_control_into_platform_config ... ok
|
||||
test platform_strategy_spec::tests::parses_position_average_entry_stop_take_reference_price_mode ... 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_typed_time_in_force_for_explicit_orders ... ok
|
||||
test platform_strategy_spec::tests::parses_st_and_star_st_risk_policy_switches_into_platform_config ... 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_or_duplicate_static_universe_symbols ... ok
|
||||
test platform_strategy_spec::tests::rejects_invalid_portfolio_drawdown_control ... 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::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::platform_spec_accepts_only_supported_matching_types ... 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::runtime_expression_parses_dated_position_exposure_schedule ... 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_expressions_infer_ma_windows_from_literal_strategy_logic ... ok
|
||||
test portfolio::tests::buy_after_partial_sell_continues_moving_average_cost_basis ... ok
|
||||
test platform_strategy_spec::tests::volume_limit_does_not_enable_quote_quantity_limit_for_minute_last ... 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 portfolio_loss::tests::restart_is_exact_and_duplicate_decisions_do_not_consume_cooldown ... ok
|
||||
test portfolio_loss::tests::restored_state_rejects_changed_policy_and_forged_cooldown ... ok
|
||||
test portfolio_loss::tests::nav_serialization_preserves_float_bits ... 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 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 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_buy_switches ... ok
|
||||
test risk_control::tests::missing_risk_state_respects_field_specific_selection_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 platform_expr_strategy::tests::shared_signal_reduction_and_stops_use_each_accounts_cost_quantity_and_sold_state ... 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 signal_contract::tests::rejects_overallocation_nonfinite_and_ambiguous_actions ... 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 session_events::tests::decision_uses_the_previous_completed_label_and_future_prices_do_not_rewrite ... ok
|
||||
test stock_pool_candidates::tests::zero_automatic_day_keeps_manual_members_without_inheriting_old_auto_targets ... 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::equal_thirty_seats_use_full_precision_at_a_board_lot_boundary ... ok
|
||||
test stock_pool_execution::tests::index_cap_respects_ordinary_weight_changes_and_stronger_fill_protection ... 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::native_sell_and_quote_conditions_are_and_but_stop_and_protection_remain_independent ... ok
|
||||
test stock_pool_execution::tests::optional_configuration_and_conflicting_aliases_are_explicit ... 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 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 session_events::tests::all_thirteen_events_return_native_boolean_series ... ok
|
||||
test stock_pool_execution::tests::reserve_slots_keep_requested_and_effective_exposure_distinct ... ok
|
||||
test stock_pool_execution::tests::retained_holdings_reserve_actual_value_and_zero_funding_still_clears ... ok
|
||||
test stock_pool_execution::tests::risk_only_configuration_never_turns_into_an_unconditional_ordinary_exit ... ok
|
||||
test stock_pool_execution::tests::sale_slippage_does_not_prevent_a_marked_value_board_lot_reduction ... 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
|
||||
|
||||
failures:
|
||||
|
||||
---- corporate_book::tests::unresolved_delisting_cannot_zero_a_pending_share_asset stdout ----
|
||||
|
||||
thread 'corporate_book::tests::unresolved_delisting_cannot_zero_a_pending_share_asset' (94174) panicked at crates/fidc-core/src/corporate_book.rs:814:94:
|
||||
called `Result::unwrap()` on an `Err` value: MultipleBenchmarks
|
||||
|
||||
|
||||
failures:
|
||||
corporate_book::tests::unresolved_delisting_cannot_zero_a_pending_share_asset
|
||||
|
||||
test result: FAILED. 781 passed; 1 failed; 7 ignored; 0 measured; 0 filtered out; finished in 0.53s
|
||||
|
||||
error: test failed, to rerun pass `-p fidc-core --lib`
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user