use crate::order::id::{ClientOrderId, OrderId};
use chrono::{DateTime, Utc};
use derive_more::From;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
use std::fmt::{Display, Formatter};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
pub struct CashFlow<AssetKey, InstrumentKey> {
pub kind: CashFlowKind,
pub asset: AssetKey,
pub amount: Decimal,
pub instrument: Option<InstrumentKey>,
pub time_exchange: DateTime<Utc>,
pub id: Option<CashFlowId>,
#[serde(default)]
pub period: Option<CashFlowPeriod>,
#[serde(default)]
pub loan: Option<CashFlowLoan>,
}
impl<AssetKey, InstrumentKey> CashFlow<AssetKey, InstrumentKey> {
pub fn new(
kind: CashFlowKind,
asset: AssetKey,
amount: Decimal,
instrument: Option<InstrumentKey>,
time_exchange: DateTime<Utc>,
id: Option<CashFlowId>,
) -> Self {
Self {
kind,
asset,
amount,
instrument,
time_exchange,
id,
period: None,
loan: None,
}
}
pub fn with_period(mut self, period: Option<CashFlowPeriod>) -> Self {
self.period = period;
self
}
pub fn with_loan(mut self, loan: Option<CashFlowLoan>) -> Self {
self.loan = loan;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
#[non_exhaustive]
pub struct CashFlowLoan {
pub time: DateTime<Utc>,
pub order_id: OrderId,
pub cid: ClientOrderId,
}
impl CashFlowLoan {
pub fn new(time: DateTime<Utc>, order_id: OrderId, cid: ClientOrderId) -> Self {
Self {
time,
order_id,
cid,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(try_from = "CashFlowPeriodFields")]
pub struct CashFlowPeriod {
start: DateTime<Utc>,
end: DateTime<Utc>,
}
impl CashFlowPeriod {
pub fn new(start: DateTime<Utc>, end: DateTime<Utc>) -> Option<Self> {
(start < end).then_some(Self { start, end })
}
pub fn start(&self) -> DateTime<Utc> {
self.start
}
pub fn end(&self) -> DateTime<Utc> {
self.end
}
pub fn contains(&self, time: DateTime<Utc>) -> bool {
(self.start..self.end).contains(&time)
}
}
#[derive(Deserialize)]
struct CashFlowPeriodFields {
start: DateTime<Utc>,
end: DateTime<Utc>,
}
impl TryFrom<CashFlowPeriodFields> for CashFlowPeriod {
type Error = String;
fn try_from(fields: CashFlowPeriodFields) -> Result<Self, Self::Error> {
Self::new(fields.start, fields.end).ok_or_else(|| {
format!(
"CashFlowPeriod end {} is not after its start {}",
fields.end, fields.start
)
})
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
pub enum CashFlowKind {
Funding {
rate: Option<Decimal>,
position_quantity: Option<Decimal>,
},
BorrowFee {
rate: Option<Decimal>,
quantity: Option<Decimal>,
},
MarginInterest {
rate: Option<Decimal>,
principal: Option<Decimal>,
timing: Option<InterestTiming>,
},
Financing {
rate: Option<Decimal>,
notional: Option<Decimal>,
},
Rebate,
LiquidationFee {
rate: Option<Decimal>,
repaid: Option<Decimal>,
},
Other(String),
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
pub enum InterestTiming {
Periodic,
OnBorrow,
Other(SmolStr),
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, From)]
pub struct CashFlowId(pub SmolStr);
impl Display for CashFlowId {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.0, f)
}
}
impl CashFlowId {
pub fn new<S: AsRef<str>>(id: S) -> Self {
Self(SmolStr::new(id))
}
}
#[non_exhaustive]
#[must_use = "`resume` and `held_since` say whether the span was read in full"]
#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
pub struct CashFlowsRead<AssetKey, InstrumentKey> {
pub flows: Vec<CashFlow<AssetKey, InstrumentKey>>,
pub resume: Option<DateTime<Utc>>,
pub held_since: Option<DateTime<Utc>>,
}
impl<AssetKey, InstrumentKey> CashFlowsRead<AssetKey, InstrumentKey> {
pub fn new(
flows: Vec<CashFlow<AssetKey, InstrumentKey>>,
resume: Option<DateTime<Utc>>,
) -> Self {
Self {
flows,
resume,
held_since: None,
}
}
pub fn complete(flows: Vec<CashFlow<AssetKey, InstrumentKey>>) -> Self {
Self::new(flows, None)
}
pub fn with_held_since(mut self, held_since: Option<DateTime<Utc>>) -> Self {
self.held_since = held_since;
self
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)] mod tests {
use super::*;
use chrono::TimeZone;
fn at(hour: u32) -> DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 11, 2, hour, 0, 0).unwrap()
}
#[test]
fn a_period_must_end_after_it_starts() {
assert!(CashFlowPeriod::new(at(10), at(10)).is_none());
assert!(CashFlowPeriod::new(at(11), at(10)).is_none());
let period = CashFlowPeriod::new(at(10), at(11)).unwrap();
assert_eq!((period.start(), period.end()), (at(10), at(11)));
}
#[test]
fn a_period_holds_its_start_and_not_its_end() {
let period = CashFlowPeriod::new(at(10), at(11)).unwrap();
assert!(period.contains(at(10)));
assert!(period.contains(at(10) + chrono::Duration::minutes(59)));
assert!(!period.contains(at(11)));
assert!(!period.contains(at(9)));
}
#[test]
fn a_period_round_trips_and_a_reversed_one_does_not_deserialise() {
let period = CashFlowPeriod::new(at(10), at(11)).unwrap();
let json = serde_json::to_string(&period).unwrap();
assert_eq!(
serde_json::from_str::<CashFlowPeriod>(&json).unwrap(),
period
);
let reversed = json.replace("10:00:00Z", "12:00:00Z");
assert!(serde_json::from_str::<CashFlowPeriod>(&reversed).is_err());
}
#[test]
fn a_flow_serialised_without_a_period_loads_with_none() {
let flow: CashFlow<String, String> = CashFlow::new(
CashFlowKind::Rebate,
"USD".to_owned(),
Decimal::ONE,
None,
at(10),
None,
);
let mut value = serde_json::to_value(&flow).unwrap();
value.as_object_mut().unwrap().remove("period");
let loaded: CashFlow<String, String> = serde_json::from_value(value).unwrap();
assert_eq!(loaded, flow);
}
#[test]
fn a_flow_serialised_without_a_loan_loads_with_none_and_one_with_a_loan_round_trips() {
let flow: CashFlow<String, String> = CashFlow::new(
CashFlowKind::Rebate,
"USD".to_owned(),
Decimal::ONE,
None,
at(10),
None,
);
let mut value = serde_json::to_value(&flow).unwrap();
value.as_object_mut().unwrap().remove("loan");
let loaded: CashFlow<String, String> = serde_json::from_value(value).unwrap();
assert_eq!(loaded, flow);
let lent = flow.with_loan(Some(CashFlowLoan::new(
at(9),
OrderId::new("7"),
ClientOrderId::new("borrow"),
)));
let json = serde_json::to_string(&lent).unwrap();
assert_eq!(
serde_json::from_str::<CashFlow<String, String>>(&json).unwrap(),
lent
);
}
}