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}