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