验证待交付股份的目标仓位与证券转换保护
This commit is contained in:
@@ -10740,6 +10740,52 @@ mod tests {
|
||||
assert_eq!(portfolio.position("000001.SZ").unwrap().quantity, 1_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pending_registered_shares_do_not_trigger_duplicate_target_buys() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).unwrap();
|
||||
let broker = BrokerSimulator::new_with_execution_price(
|
||||
ChinaAShareCostModel::default(), ChinaEquityRuleHooks, PriceField::Open,
|
||||
).with_matching_type(MatchingType::NextBarOpen)
|
||||
.with_volume_limit(false).with_liquidity_limit(false).with_inactive_limit(false);
|
||||
let mut snapshot = limit_test_snapshot();
|
||||
snapshot.date = date;
|
||||
snapshot.prev_close = 10.0;
|
||||
snapshot.open = 10.0;
|
||||
snapshot.close = 10.0;
|
||||
let data = DataSet::from_components_with_actions_and_quotes(
|
||||
vec![limit_test_instrument()], vec![snapshot], Vec::new(),
|
||||
vec![limit_test_candidate(true, true)], vec![limit_test_benchmark()],
|
||||
Vec::new(), Vec::new(),
|
||||
).unwrap();
|
||||
let mut portfolio = PortfolioState::new(20_000.0);
|
||||
portfolio.position_mut("000001.SZ").buy(date.pred_opt().unwrap(), 1_000, 10.0);
|
||||
portfolio.apply_cash_delta(-10_000.0).unwrap();
|
||||
// Isolated broker fixture: these shares are an asset, but not delivered stock.
|
||||
portfolio.position_mut("000001.SZ").share_receivables.push(crate::portfolio::ShareReceivable {
|
||||
ex_date: date, record_date: date.pred_opt().unwrap(),
|
||||
effective_date: date.succ_opt().unwrap(), quantity: 500,
|
||||
holding_origin_date: date.pred_opt().unwrap(),
|
||||
});
|
||||
let mut report = BrokerExecutionReport::default();
|
||||
broker.process_target_value(date, &mut portfolio, &data, "000001.SZ", 15_000.0,
|
||||
"registered shares already meet target value", &mut BTreeMap::new(),
|
||||
&mut IntradayExecutionLedger::default(), &mut None, &mut BTreeMap::new(), &mut report).unwrap();
|
||||
broker.process_timed_target_value(date, &mut portfolio, &data, "000001.SZ", 15_000.0,
|
||||
AlgoOrderStyle::Twap, None, None, "registered shares already meet timed target",
|
||||
&mut BTreeMap::new(), &mut IntradayExecutionLedger::default(), &mut None,
|
||||
&mut BTreeMap::new(), &mut report).unwrap();
|
||||
broker.process_target_shares(date, &mut portfolio, &data, "000001.SZ", 1_500,
|
||||
"registered shares already meet quantity target", &mut BTreeMap::new(),
|
||||
&mut IntradayExecutionLedger::default(), &mut None, &mut BTreeMap::new(), &mut report).unwrap();
|
||||
assert!(report.order_events.is_empty());
|
||||
assert!(report.fill_events.is_empty());
|
||||
assert_eq!(portfolio.cash(), 10_000.0);
|
||||
let position = portfolio.position("000001.SZ").unwrap();
|
||||
assert_eq!(position.quantity, 1_000);
|
||||
assert_eq!(position.share_receivable_quantity(), 500);
|
||||
assert_eq!(position.sellable_qty(date), 1_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_value_delta_below_order_step_is_audited_without_creating_zero_quantity_order() {
|
||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||
|
||||
@@ -820,6 +820,30 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successor_conversion_cannot_discard_unresolved_share_entitlements() {
|
||||
for already_pending in [false, true] {
|
||||
let mut account = conversion_book();
|
||||
let action = if already_pending {
|
||||
let input = conversion_data(vec![shares(Some(date().succ_opt().unwrap()))], false);
|
||||
apply(date(), &input, &mut account, &mut Vec::new(), true, true).unwrap();
|
||||
conversion()
|
||||
} else {
|
||||
let mut action = conversion();
|
||||
action.share_bonus = 0.5;
|
||||
action.share_effective_date = Some(date().succ_opt().unwrap());
|
||||
action
|
||||
};
|
||||
let input = conversion_data(vec![action], true);
|
||||
let before = account.financial_replay_identity();
|
||||
let mut notes = vec!["original".into()];
|
||||
let error = apply(date(), &input, &mut account, &mut notes, true, true).unwrap_err();
|
||||
assert!(error.to_string().contains("pending_share_successor_terms_required"));
|
||||
assert_eq!(account.financial_replay_identity(), before);
|
||||
assert_eq!(notes, ["original"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_or_backward_cash_payment_date_never_updates_the_account() {
|
||||
for payment in [None, date().pred_opt()] {
|
||||
|
||||
Reference in New Issue
Block a user