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::{Constructor, 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, Constructor)]
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    /// When the venue posted the flow.
60    pub time_exchange: DateTime<Utc>,
61    /// The venue's identifier for the flow, when it gives one. See [`CashFlowId`].
62    pub id: Option<CashFlowId>,
63}
64
65/// What a [`CashFlow`] is for.
66///
67/// Each variant carries the venue's own figures where it reports them, and `None` where it does
68/// not.
69#[non_exhaustive]
70#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
71pub enum CashFlowKind {
72    /// A perpetual's funding payment, paid or received on a position held at a funding time.
73    Funding {
74        /// The funding rate the payment was charged at, per funding interval, in the venue's
75        /// sign convention.
76        rate: Option<Decimal>,
77        /// The signed position the payment was charged on: positive long, negative short.
78        position_quantity: Option<Decimal>,
79    },
80    /// A fee for borrowing a security to sell it short.
81    BorrowFee {
82        /// The fee rate charged, as the venue quotes it: usually annualised.
83        rate: Option<Decimal>,
84        /// The quantity borrowed that the fee was charged on.
85        quantity: Option<Decimal>,
86    },
87    /// Interest on borrowed cash or assets.
88    MarginInterest {
89        /// The interest rate charged, as its source quotes it: per the venue's own period from a
90        /// live venue (daily from Binance), annualised from the simulated venue's
91        /// [`PeriodicMarginInterest`](crate::margin::PeriodicMarginInterest).
92        rate: Option<Decimal>,
93        /// The amount borrowed that the interest was charged on.
94        principal: Option<Decimal>,
95        /// When in the loan's life the charge falls, where the venue says.
96        timing: Option<InterestTiming>,
97    },
98    /// The financing of a leveraged position held past a venue's daily cutoff, such as a CFD's
99    /// overnight financing.
100    ///
101    /// Charged on the position rather than on a loan: a long and a short in one instrument are
102    /// charged at different rates, and a short may receive rather than pay. Interest on a loan of
103    /// cash or assets is [`MarginInterest`](Self::MarginInterest).
104    Financing {
105        /// The financing rate charged, as the venue quotes it: usually annualised.
106        rate: Option<Decimal>,
107        /// The notional value of the position the financing was charged on.
108        notional: Option<Decimal>,
109    },
110    /// A rebate paid to the account, such as on cash collateral for a short sale.
111    Rebate,
112    /// The fee a venue charged for liquidating the account, on the debt the liquidation repaid.
113    ///
114    /// Charged on the debt a liquidation repaid rather than on each forced fill, so it is not
115    /// part of any trade's fees. The forced fills themselves carry
116    /// [`TradeOrigin::Liquidation`](crate::trade::TradeOrigin::Liquidation).
117    LiquidationFee {
118        /// The fee rate, as a fraction of `repaid`.
119        rate: Option<Decimal>,
120        /// The value of the debt the liquidation repaid, in the flow's asset, which the fee was
121        /// charged on.
122        repaid: Option<Decimal>,
123    },
124    /// A flow of a kind no other variant names, with the venue's own name for it.
125    Other(String),
126}
127
128/// When in a loan's life a [`CashFlowKind::MarginInterest`] charge falls.
129///
130/// A venue charges the interest on a new loan when it is taken and again at each of its charging
131/// boundaries. A charge on a new loan can carry a time before the position it belongs to opened,
132/// since a venue may stamp it at the start of the period the loan was taken in.
133#[non_exhaustive]
134#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
135pub enum InterestTiming {
136    /// The charge for holding the loan over a charging period, at one of the venue's boundaries.
137    Periodic,
138    /// The first charge, made when the loan is taken.
139    OnBorrow,
140    /// A timing neither variant names, with the venue's own name for it. Like an absent timing
141    /// (`None`), it is unknown: do not treat it as either named variant.
142    Other(SmolStr),
143}
144
145/// A venue's own identifier for one [`CashFlow`], carried through opaquely.
146///
147/// Like [`TradeId`](crate::trade::TradeId), its uniqueness is the venue's to define and may be
148/// scoped to an asset or instrument rather than global. A consumer that needs a key should
149/// qualify it by exchange and asset or instrument. `Ord` is byte order, not time order.
150#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Deserialize, Serialize, From)]
151pub struct CashFlowId(pub SmolStr);
152
153impl Display for CashFlowId {
154    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
155        Display::fmt(&self.0, f)
156    }
157}
158
159impl CashFlowId {
160    pub fn new<S: AsRef<str>>(id: S) -> Self {
161        Self(SmolStr::new(id))
162    }
163}
164
165/// What one read of a venue's cash flows over a span returned.
166///
167/// `resume` says whether the read stopped before the span's end:
168/// - `None`: every flow in the span the venue still holds, and the client reports, is in `flows`.
169///   Each client documents the records it leaves out.
170/// - `Some(t)`: the read stopped at its bound, and flows from `t` on may be missing from `flows`.
171///   Read on from `start = t` with the same end. That read can return again flows this one
172///   returned at or after `t`, so match them by [`CashFlowId`].
173///
174/// A venue also keeps its records for a limited time. `held_since` says when that limit cuts
175/// into the span, so a caller can tell flows the venue no longer has from flows that never
176/// happened.
177#[non_exhaustive]
178#[must_use = "`resume` and `held_since` say whether the span was read in full"]
179#[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize)]
180pub struct CashFlowsRead<AssetKey, InstrumentKey> {
181    /// The flows read, in the span.
182    pub flows: Vec<CashFlow<AssetKey, InstrumentKey>>,
183    /// Where to read on from, or `None` when the read reached the span's end.
184    pub resume: Option<DateTime<Utc>>,
185    /// The earliest moment the read covers, when the span starts before it; flows before it may
186    /// exist but cannot be read. It is after the span's end when none of the span can be read.
187    /// `None` when the venue's limit is not known to cut into the span, which is no claim that
188    /// the venue holds flows without limit.
189    ///
190    /// A caller that needs every flow in the span, such as one adding up a position's whole
191    /// interest, treats the part before `held_since` as a gap: `resume: None` covers what the
192    /// venue holds, not what it held. The limit cuts into the span from its start, so only the
193    /// first read of a span can report it; a caller reading on from `resume` has nothing to learn
194    /// from a later read's.
195    pub held_since: Option<DateTime<Utc>>,
196}
197
198impl<AssetKey, InstrumentKey> CashFlowsRead<AssetKey, InstrumentKey> {
199    /// A read that stopped at `resume` (`None`: it reached the span's end), with no venue limit
200    /// cutting into the span.
201    pub fn new(
202        flows: Vec<CashFlow<AssetKey, InstrumentKey>>,
203        resume: Option<DateTime<Utc>>,
204    ) -> Self {
205        Self {
206            flows,
207            resume,
208            held_since: None,
209        }
210    }
211
212    /// A read that reached the span's end.
213    pub fn complete(flows: Vec<CashFlow<AssetKey, InstrumentKey>>) -> Self {
214        Self::new(flows, None)
215    }
216
217    /// Sets [`held_since`](Self::held_since).
218    pub fn with_held_since(mut self, held_since: Option<DateTime<Utc>>) -> Self {
219        self.held_since = held_since;
220        self
221    }
222}