architect_api/folio/
mod.rs

1use crate::{
2    orderflow::{AberrantFill, Fill},
3    symbology::*,
4    utils::{half_open_range::ClampSign, messaging::MaybeRequest},
5    AccountId, Dir, HalfOpenRange,
6};
7use chrono::{DateTime, NaiveDate, Utc};
8#[cfg(feature = "netidx")]
9use derive::FromValue;
10#[cfg(feature = "netidx")]
11use netidx_derive::Pack;
12use rust_decimal::Decimal;
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15use std::{
16    collections::{BTreeMap, BTreeSet},
17    sync::Arc,
18};
19use uuid::Uuid;
20
21pub static SCHEMA: &'static str = include_str!("schema.sql");
22
23/// Cpty should implement the following RPCs/messages for Folio integration:
24///
25/// - GetFills/Fills
26/// - GetAccountSummaries/AccountSummaries
27/// - Fills (realtime unsolicited fill dropcopy)
28#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
29#[cfg_attr(feature = "netidx", derive(Pack))]
30#[cfg_attr(feature = "netidx", derive(FromValue))]
31pub enum FolioMessage {
32    GetFillsQuery(MarketFilter, HalfOpenRange<Option<DateTime<Utc>>>),
33    GetFillsQueryResponse(
34        MarketFilter,
35        HalfOpenRange<Option<DateTime<Utc>>>,
36        Arc<Vec<Result<Fill, AberrantFill>>>,
37    ),
38    GetFills(Uuid, GetFills),
39    Fills(Option<Uuid>, CptyId, Result<Fills, GetFillsError>), // None for unsolicited
40    /// Cptys should dropcopy realtime fills to Folio as they become known
41    RealtimeFill(Result<Fill, AberrantFill>),
42    /// Request account summaries snapshot from all cptys, grouped by cpty
43    ///
44    /// - request id
45    /// - account ids (None for all accounts)
46    GetAllAccountSummaries(Uuid, Option<Arc<BTreeSet<AccountId>>>),
47    AllAccountSummaries(Uuid, Vec<(CptyId, Arc<AccountSummaries>)>),
48    /// Request account summaries snapshot; can be called internally
49    /// (as Folio <-> Cpty), or externally (client <-> Folio)
50    ///
51    /// - request id
52    /// - cpty id
53    /// - account ids (None for all accounts)
54    GetAccountSummaries(Uuid, CptyId, Option<Arc<BTreeSet<AccountId>>>),
55    /// Response from cpty with balances snapshot;
56    /// may be unsolicited (response_id = None) from cptys
57    AccountSummaries(Option<Uuid>, CptyId, Option<Arc<AccountSummaries>>),
58    /// Control message to folio to update balances
59    UpdateAccountSummaries,
60    /// Control messages to folio to sync fills
61    SyncFillsForward,
62    SyncFillsBackward(CptyId),
63    InvalidateSyncBefore(CptyId, DateTime<Utc>),
64    InvalidateSyncAfter(CptyId, DateTime<Utc>),
65    GetSyncStatus(Uuid, CptyId),
66    GetSyncStatusResponse(Uuid, FolioSyncStatus),
67    /// Take a snapshot of balances and upsert
68    SnapshotBalances,
69}
70
71#[derive(Copy, Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
72#[cfg_attr(feature = "netidx", derive(Pack))]
73#[cfg_attr(feature = "netidx", derive(FromValue))]
74pub struct MarketFilter {
75    pub venue: Option<VenueId>,
76    pub route: Option<RouteId>,
77    pub base: Option<ProductId>,
78    pub quote: Option<ProductId>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
82#[cfg_attr(feature = "netidx", derive(Pack))]
83#[cfg_attr(feature = "netidx", derive(FromValue))]
84pub struct AccountSummaries {
85    pub snapshot_ts: DateTime<Utc>,
86    pub by_account: BTreeMap<AccountId, AccountSummary>,
87}
88
89impl AccountSummaries {
90    pub fn filter_accounts(&self, accounts: &BTreeSet<AccountId>) -> Self {
91        let by_account = self
92            .by_account
93            .iter()
94            .filter_map(|(account_id, summary)| {
95                if accounts.contains(account_id) {
96                    Some((*account_id, summary.clone()))
97                } else {
98                    None
99                }
100            })
101            .collect();
102        Self { snapshot_ts: self.snapshot_ts, by_account }
103    }
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
107#[cfg_attr(feature = "netidx", derive(Pack))]
108#[cfg_attr(feature = "netidx", derive(FromValue))]
109pub struct AccountSummary {
110    pub balances: BTreeMap<ProductId, Balance>,
111    // There could be multiple open positions for a particular Market,
112    // so this is not a BTreeMap like [balances].
113    pub positions: Vec<Position>,
114    pub profit_loss: Option<Decimal>,
115    pub clearing_venue: Option<VenueId>,
116}
117
118impl AccountSummary {
119    pub fn from_simple_balances(balances: BTreeMap<ProductId, Decimal>) -> Self {
120        Self {
121            balances: balances
122                .into_iter()
123                .map(|(product_id, total)| {
124                    (product_id, Balance { total: Some(total), ..Default::default() })
125                })
126                .collect(),
127            positions: vec![],
128            ..Default::default()
129        }
130    }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
134#[cfg_attr(feature = "netidx", derive(Pack))]
135#[cfg_attr(feature = "netidx", derive(FromValue))]
136pub struct Balance {
137    /// The total amount of this asset held in the account by the
138    /// venue/broker.
139    pub total: Option<Decimal>,
140
141    /// Margin requirement calculated for worst-case based on open positions and working orders.
142    pub total_margin: Option<Decimal>,
143
144    /// Margin requirement calculated for worst-case based on open positions.
145    pub position_margin: Option<Decimal>,
146
147    /// Available account funds including balance, realized profit (or loss), collateral and credits.
148    pub purchasing_power: Option<Decimal>,
149
150    /// Cash available in the account beyond the required margin.
151    pub cash_excess: Option<Decimal>,
152
153    /// Cash balance from the last statement.
154    pub yesterday_balance: Option<Decimal>,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
158#[cfg_attr(feature = "netidx", derive(Pack))]
159#[cfg_attr(feature = "netidx", derive(FromValue))]
160pub struct Position {
161    pub market_id: MarketId,
162
163    pub quantity: Option<Decimal>,
164
165    /// Average price used to open position
166    pub average_price: Option<Decimal>,
167
168    pub trade_time: Option<DateTime<Utc>>,
169
170    /// The trade date according to the exchange
171    /// settlement calendar.
172    pub trade_date: Option<NaiveDate>,
173
174    pub dir: Dir,
175
176    pub break_even_price: Option<Decimal>,
177
178    pub liquidation_price: Option<Decimal>,
179}
180
181/// Request to cpty for a certain range of fills.
182///
183/// Cpty is allowed to reply with a range smaller than requested,
184/// for whatever reason (don't want to paginate, API limit)...
185/// the clamp_sign tells you which side of the range should be
186/// moved when shirking the range.
187#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
188#[cfg_attr(feature = "netidx", derive(Pack))]
189#[cfg_attr(feature = "netidx", derive(FromValue))]
190pub struct GetFills {
191    pub cpty: CptyId,
192    pub range: HalfOpenRange<Option<DateTime<Utc>>>,
193    pub clamp_sign: ClampSign,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
197#[cfg_attr(feature = "netidx", derive(Pack))]
198#[cfg_attr(feature = "netidx", derive(FromValue))]
199pub enum GetFillsError {
200    #[serde(other)]
201    #[cfg_attr(feature = "netidx", pack(other))]
202    Unknown,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
206#[cfg_attr(feature = "netidx", derive(Pack))]
207#[cfg_attr(feature = "netidx", derive(FromValue))]
208pub struct Fills {
209    pub range: HalfOpenRange<Option<DateTime<Utc>>>,
210    pub fills: Arc<Vec<Result<Fill, AberrantFill>>>,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
214#[cfg_attr(feature = "netidx", derive(Pack))]
215#[cfg_attr(feature = "netidx", derive(FromValue))]
216pub struct FolioSyncStatus {
217    pub synced_range: Option<HalfOpenRange<DateTime<Utc>>>,
218    pub beginning_of_time: DateTime<Utc>,
219    pub forward_sync_backoff: Option<DateTime<Utc>>,
220    pub backfill_backoff: Option<DateTime<Utc>>,
221}
222
223impl MaybeRequest for FolioMessage {
224    fn request_id(&self) -> Option<Uuid> {
225        match self {
226            FolioMessage::GetFills(id, ..)
227            | FolioMessage::GetSyncStatus(id, ..)
228            | FolioMessage::GetAccountSummaries(id, ..)
229            | FolioMessage::GetAllAccountSummaries(id, ..) => Some(*id),
230            _ => None,
231        }
232    }
233
234    fn response_id(&self) -> Option<Uuid> {
235        match self {
236            FolioMessage::Fills(id, ..) | FolioMessage::AccountSummaries(id, ..) => *id,
237            FolioMessage::GetSyncStatusResponse(id, ..)
238            | FolioMessage::AllAccountSummaries(id, ..) => Some(*id),
239            _ => None,
240        }
241    }
242}