Skip to main content

rustrade_execution/
cash_flow.rs

1//! Cash that moves into or out of an account other than by a trade: perpetual funding, margin
2//! interest, stock borrow fees and rebates.
3//!
4//! An account stream reports each such movement the venue posts as
5//! [`AccountEventKind::CashFlow`](crate::AccountEventKind::CashFlow), carrying a [`CashFlow`]. The
6//! library delivers the venue's own figures; rates, day counts and what to do with the flows stay
7//! the consumer's.
8
9use chrono::{DateTime, Utc};
10use derive_more::From;
11use rust_decimal::Decimal;
12use serde::{Deserialize, Serialize};
13use smol_str::SmolStr;
14use std::fmt::{Display, Formatter};
15
16/// One movement of cash into or out of an account that is not a trade, as the venue posted it.
17///
18/// Sent as [`AccountEventKind::CashFlow`](crate::AccountEventKind::CashFlow). A flow is a delta:
19/// apply each one once. The engine adds a flow attributed to an instrument to the `carry` of that
20/// instrument's open position (see `Position::carry` in `rustrade`), and applies none to
21/// balances. A venue's balances already include the flows it has posted, so adding a flow to a
22/// balance read after it counts the flow twice. Carry a venue has accrued but not yet posted is no
23/// flow: a venue that reports it puts it in
24/// [`Balance::accrued`](crate::balance::Balance::accrued).
25///
26/// # Delivery
27///
28/// Each producer says how it avoids sending one flow twice, and what identifies a flow when the
29/// venue gives it no [`id`](Self::id). Known producers, as of writing:
30/// - Hyperliquid perpetuals: funding, as [`CashFlowKind::Funding`]. See the client's rustdoc.
31/// - Alpaca: margin interest, fees and pass-through charges and rebates, read on request by
32///   `AlpacaClient::fetch_cash_flows` rather than streamed, with Alpaca's activity id. The caller
33///   drops what it has already read.
34/// - Binance margin: interest, as [`CashFlowKind::MarginInterest`], read on request by
35///   `BinanceMargin::fetch_margin_interest` rather than streamed, with Binance's transaction id.
36///   The caller drops what it has already read.
37/// - The simulated venue: whatever its configured
38///   [`HoldingCostModel`](crate::holding_cost::HoldingCostModel)s and
39///   [`MarginInterestModel`](crate::margin::MarginInterestModel) charge, and the
40///   [`LiquidationFee`](CashFlowKind::LiquidationFee) of a configured
41///   [`SpotMarginLiquidation`](crate::margin::SpotMarginLiquidation), each flow once, with an id
42///   from its own sequence.
43#[non_exhaustive]
44#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
45pub struct CashFlow<AssetKey, InstrumentKey> {
46    /// What the flow is for.
47    pub kind: CashFlowKind,
48    /// The asset the cash moved in.
49    pub asset: AssetKey,
50    /// How much moved, signed: positive when the account received it, negative when it paid.
51    pub amount: Decimal,
52    /// The instrument whose position caused the flow, when the venue attributes it to one, as it
53    /// does funding. `None` for a flow on the account as a whole, such as cross-margin interest.
54    ///
55    /// The `rustrade` engine does not guess an instrument for a `None` flow: it asks the
56    /// strategy's `CashFlowAttributionStrategy`, and applies the flow to no position when that
57    /// names none. See `EngineState::update_from_account` in `rustrade` for why.
58    pub instrument: Option<InstrumentKey>,
59    /// The time the venue stamps the flow with: when it posted the flow, unless its producer
60    /// documents otherwise. A venue may stamp a charge at the start of the period it is for, as
61    /// Binance stamps its margin interest; such a producer also sets [`period`](Self::period).
62    pub time_exchange: DateTime<Utc>,
63    /// The venue's identifier for the flow, when it gives one. See [`CashFlowId`].
64    pub id: Option<CashFlowId>,
65    /// The period the flow is for, when its producer knows it, such as the charging period a
66    /// margin interest charge was made in. `None` when the producer does not say. Set with
67    /// [`with_period`](Self::with_period).
68    ///
69    /// [`time_exchange`](Self::time_exchange) stays the venue's own stamp, which a producer
70    /// setting a period puts in the period or at its start. Nothing checks that. The `rustrade`
71    /// engine reads the period of an [`InterestTiming::OnBorrow`] charge only: a venue that stamps
72    /// the charge on a new loan at the start of its period stamps it before the position the loan
73    /// funds opened, and the period tells the engine the position may still own it. See
74    /// `InstrumentState::update_from_cash_flow` in `rustrade`.
75    ///
76    /// `#[serde(default)]` so a flow serialised before this field existed still loads.
77    #[serde(default)]
78    pub period: Option<CashFlowPeriod>,
79}
80
81impl<AssetKey, InstrumentKey> CashFlow<AssetKey, InstrumentKey> {
82    /// A flow with no [`period`](Self::period). Add one with [`with_period`](Self::with_period).
83    pub fn new(
84        kind: CashFlowKind,
85        asset: AssetKey,
86        amount: Decimal,
87        instrument: Option<InstrumentKey>,
88        time_exchange: DateTime<Utc>,
89        id: Option<CashFlowId>,
90    ) -> Self {
91        Self {
92            kind,
93            asset,
94            amount,
95            instrument,
96            time_exchange,
97            id,
98            period: None,
99        }
100    }
101
102    /// Sets [`period`](Self::period).
103    pub fn with_period(mut self, period: Option<CashFlowPeriod>) -> Self {
104        self.period = period;
105        self
106    }
107}
108
109/// The period a [`CashFlow`] is for: from [`start`](Self::start), inclusive, to
110/// [`end`](Self::end), exclusive. `end` is after `start`.
111///
112/// Deserialising checks the order too, so a period read back is as valid as one built.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
114#[serde(try_from = "CashFlowPeriodFields")]
115pub struct CashFlowPeriod {
116    start: DateTime<Utc>,
117    end: DateTime<Utc>,
118}
119
120impl CashFlowPeriod {
121    /// The period from `start` to `end`, or `None` when `end` is not after `start`.
122    pub fn new(start: DateTime<Utc>, end: DateTime<Utc>) -> Option<Self> {
123        (start < end).then_some(Self { start, end })
124    }
125
126    /// When the period starts: the first instant in it.
127    pub fn start(&self) -> DateTime<Utc> {
128        self.start
129    }
130
131    /// When the period ends: the first instant after it.
132    pub fn end(&self) -> DateTime<Utc> {
133        self.end
134    }
135
136    /// Whether `time` is in the period.
137    pub fn contains(&self, time: DateTime<Utc>) -> bool {
138        (self.start..self.end).contains(&time)
139    }
140}
141
142/// The serialised form of a [`CashFlowPeriod`], before its order is checked.
143#[derive(Deserialize)]
144struct CashFlowPeriodFields {
145    start: DateTime<Utc>,
146    end: DateTime<Utc>,
147}
148
149impl TryFrom<CashFlowPeriodFields> for CashFlowPeriod {
150    type Error = String;
151
152    fn try_from(fields: CashFlowPeriodFields) -> Result<Self, Self::Error> {
153        Self::new(fields.start, fields.end).ok_or_else(|| {
154            format!(
155                "CashFlowPeriod end {} is not after its start {}",
156                fields.end, fields.start
157            )
158        })
159    }
160}
161
162/// What a [`CashFlow`] is for.
163///
164/// Each variant carries the venue's own figures where it reports them, and `None` where it does
165/// not.
166#[non_exhaustive]
167#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
168pub enum CashFlowKind {
169    /// A perpetual's funding payment, paid or received on a position held at a funding time.
170    Funding {
171        /// The funding rate the payment was charged at, per funding interval, in the venue's
172        /// sign convention.
173        rate: Option<Decimal>,
174        /// The signed position the payment was charged on: positive long, negative short.
175        position_quantity: Option<Decimal>,
176    },
177    /// A fee for borrowing a security to sell it short.
178    BorrowFee {
179        /// The fee rate charged, as the venue quotes it: usually annualised.
180        rate: Option<Decimal>,
181        /// The quantity borrowed that the fee was charged on.
182        quantity: Option<Decimal>,
183    },
184    /// Interest on borrowed cash or assets.
185    MarginInterest {
186        /// The interest rate charged, as its source quotes it: per the venue's own period from a
187        /// live venue (daily from Binance), annualised from the simulated venue's
188        /// [`PeriodicMarginInterest`](crate::margin::PeriodicMarginInterest).
189        rate: Option<Decimal>,
190        /// The amount borrowed that the interest was charged on.
191        principal: Option<Decimal>,
192        /// When in the loan's life the charge falls, where the venue says.
193        timing: Option<InterestTiming>,
194    },
195    /// The financing of a leveraged position held past a venue's daily cutoff, such as a CFD's
196    /// overnight financing.
197    ///
198    /// Charged on the position rather than on a loan: a long and a short in one instrument are
199    /// charged at different rates, and a short may receive rather than pay. Interest on a loan of
200    /// cash or assets is [`MarginInterest`](Self::MarginInterest).
201    Financing {
202        /// The financing rate charged, as the venue quotes it: usually annualised.
203        rate: Option<Decimal>,
204        /// The notional value of the position the financing was charged on.
205        notional: Option<Decimal>,
206    },
207    /// A rebate paid to the account, such as on cash collateral for a short sale.
208    Rebate,
209    /// The fee a venue charged for liquidating the account, on the debt the liquidation repaid.
210    ///
211    /// Charged on the debt a liquidation repaid rather than on each forced fill, so it is not
212    /// part of any trade's fees. The forced fills themselves carry
213    /// [`TradeOrigin::Liquidation`](crate::trade::TradeOrigin::Liquidation).
214    LiquidationFee {
215        /// The fee rate, as a fraction of `repaid`.
216        rate: Option<Decimal>,
217        /// The value of the debt the liquidation repaid, in the flow's asset, which the fee was
218        /// charged on.
219        repaid: Option<Decimal>,
220    },
221    /// A flow of a kind no other variant names, with the venue's own name for it.
222    Other(String),
223}
224
225/// When in a loan's life a [`CashFlowKind::MarginInterest`] charge falls.
226///
227/// A venue charges the interest on a new loan when it is taken and again at each of its charging
228/// boundaries. A charge on a new loan can carry a time before the position it belongs to opened,
229/// since a venue may stamp it at the start of the period the loan was taken in. Such a producer
230/// gives the flow that period as its [`CashFlow::period`], which lets the `rustrade` engine still
231/// give the charge to that position.
232#[non_exhaustive]
233#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
234pub enum InterestTiming {
235    /// The charge for holding the loan over a charging period, at one of the venue's boundaries.
236    Periodic,
237    /// The first charge, made when the loan is taken.
238    OnBorrow,
239    /// A timing neither variant names, with the venue's own name for it. Like an absent timing
240    /// (`None`), it is unknown: do not treat it as either named variant.
241    Other(SmolStr),
242}
243
244/// A venue's own identifier for one [`CashFlow`], carried through opaquely.
245///
246/// Like [`TradeId`](crate::trade::TradeId), its uniqueness is the venue's to define and may be
247/// scoped to an asset or instrument rather than global. A consumer that needs a key should
248/// qualify it by exchange and asset or instrument. `Ord` is byte order, not time order.
249#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, From)]
250pub struct CashFlowId(pub SmolStr);
251
252impl Display for CashFlowId {
253    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
254        Display::fmt(&self.0, f)
255    }
256}
257
258impl CashFlowId {
259    pub fn new<S: AsRef<str>>(id: S) -> Self {
260        Self(SmolStr::new(id))
261    }
262}
263
264/// What one read of a venue's cash flows over a span returned.
265///
266/// `resume` says whether the read stopped before the span's end:
267/// - `None`: every flow in the span the venue still holds, and the client reports, is in `flows`.
268///   Each client documents the records it leaves out.
269/// - `Some(t)`: the read stopped at its bound, and flows from `t` on may be missing from `flows`.
270///   Read on from `start = t` with the same end. That read can return again flows this one
271///   returned at or after `t`, so match them by [`CashFlowId`].
272///
273/// A venue also keeps its records for a limited time. `held_since` says when that limit cuts
274/// into the span, so a caller can tell flows the venue no longer has from flows that never
275/// happened.
276#[non_exhaustive]
277#[must_use = "`resume` and `held_since` say whether the span was read in full"]
278#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
279pub struct CashFlowsRead<AssetKey, InstrumentKey> {
280    /// The flows read, in the span.
281    pub flows: Vec<CashFlow<AssetKey, InstrumentKey>>,
282    /// Where to read on from, or `None` when the read reached the span's end.
283    pub resume: Option<DateTime<Utc>>,
284    /// The earliest moment the read covers, when the span starts before it; flows before it may
285    /// exist but cannot be read. It is after the span's end when none of the span can be read.
286    /// `None` when the venue's limit is not known to cut into the span, which is no claim that
287    /// the venue holds flows without limit.
288    ///
289    /// A caller that needs every flow in the span, such as one adding up a position's whole
290    /// interest, treats the part before `held_since` as a gap: `resume: None` covers what the
291    /// venue holds, not what it held. The limit cuts into the span from its start, so only the
292    /// first read of a span can report it; a caller reading on from `resume` has nothing to learn
293    /// from a later read's.
294    pub held_since: Option<DateTime<Utc>>,
295}
296
297impl<AssetKey, InstrumentKey> CashFlowsRead<AssetKey, InstrumentKey> {
298    /// A read that stopped at `resume` (`None`: it reached the span's end), with no venue limit
299    /// cutting into the span.
300    pub fn new(
301        flows: Vec<CashFlow<AssetKey, InstrumentKey>>,
302        resume: Option<DateTime<Utc>>,
303    ) -> Self {
304        Self {
305            flows,
306            resume,
307            held_since: None,
308        }
309    }
310
311    /// A read that reached the span's end.
312    pub fn complete(flows: Vec<CashFlow<AssetKey, InstrumentKey>>) -> Self {
313        Self::new(flows, None)
314    }
315
316    /// Sets [`held_since`](Self::held_since).
317    pub fn with_held_since(mut self, held_since: Option<DateTime<Utc>>) -> Self {
318        self.held_since = held_since;
319        self
320    }
321}
322
323#[cfg(test)]
324#[allow(clippy::unwrap_used, clippy::expect_used)] // test code: panics acceptable
325mod tests {
326    use super::*;
327    use chrono::TimeZone;
328
329    fn at(hour: u32) -> DateTime<Utc> {
330        Utc.with_ymd_and_hms(2026, 11, 2, hour, 0, 0).unwrap()
331    }
332
333    #[test]
334    fn a_period_must_end_after_it_starts() {
335        assert!(CashFlowPeriod::new(at(10), at(10)).is_none());
336        assert!(CashFlowPeriod::new(at(11), at(10)).is_none());
337        let period = CashFlowPeriod::new(at(10), at(11)).unwrap();
338        assert_eq!((period.start(), period.end()), (at(10), at(11)));
339    }
340
341    #[test]
342    fn a_period_holds_its_start_and_not_its_end() {
343        let period = CashFlowPeriod::new(at(10), at(11)).unwrap();
344        assert!(period.contains(at(10)));
345        assert!(period.contains(at(10) + chrono::Duration::minutes(59)));
346        assert!(!period.contains(at(11)));
347        assert!(!period.contains(at(9)));
348    }
349
350    #[test]
351    fn a_period_round_trips_and_a_reversed_one_does_not_deserialise() {
352        let period = CashFlowPeriod::new(at(10), at(11)).unwrap();
353        let json = serde_json::to_string(&period).unwrap();
354        assert_eq!(
355            serde_json::from_str::<CashFlowPeriod>(&json).unwrap(),
356            period
357        );
358
359        let reversed = json.replace("10:00:00Z", "12:00:00Z");
360        assert!(serde_json::from_str::<CashFlowPeriod>(&reversed).is_err());
361    }
362
363    #[test]
364    fn a_flow_serialised_without_a_period_loads_with_none() {
365        let flow: CashFlow<String, String> = CashFlow::new(
366            CashFlowKind::Rebate,
367            "USD".to_owned(),
368            Decimal::ONE,
369            None,
370            at(10),
371            None,
372        );
373        let mut value = serde_json::to_value(&flow).unwrap();
374        value.as_object_mut().unwrap().remove("period");
375        let loaded: CashFlow<String, String> = serde_json::from_value(value).unwrap();
376        assert_eq!(loaded, flow);
377    }
378}