明确红利账务再投来源并修复流式遗漏与校正时钟

This commit is contained in:
boris
2026-09-14 22:30:05 +08:00
parent fbc4233dcd
commit 984f9d308d
11 changed files with 681 additions and 140 deletions
+45 -1
View File
@@ -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()
@@ -425,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 {
@@ -469,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,
@@ -489,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)