Skip to main content

df_hyperliquid_core/live/store/
state.rs

1//! The store: one network's live state, the interests that keep it live, and its views.
2
3use std::collections::BTreeMap;
4
5use crate::account::ClearinghouseState;
6use crate::account::FrontendOrder;
7use crate::account::positions::Position;
8use crate::account::{Abstraction, Beside, Collateral, Prices, Standing, Surface, Usable, Worth, combined};
9use crate::client::{Network, USDC_INDEX};
10use crate::live::store::account::{Account, Keep};
11use crate::live::store::convert;
12use crate::live::store::interest::{Changes, Handle, Interest, Interests};
13use crate::live::store::market::{BARS, Context, Markets, fold_bar, fold_prints};
14use crate::live::store::stamp::Stamped;
15use crate::live::{Book, Candle, Frame, Interval, LedgerUpdate, Level, Limits, Refused, Session, Subscription, Trade};
16use crate::markets::perps::{MetaAndAssetCtxs, PerpCtx, UserFunding};
17use crate::markets::split_coin;
18use crate::markets::spot::{SpotCtx, SpotMeta, SpotMetaAndAssetCtxs};
19use crate::markets::{L2Book, MarketTrade, Mids};
20use crate::utils::{Address, Dec};
21
22/// A typed `/info` answer for the store to fold.
23#[derive(Debug, Clone, Copy)]
24pub enum Read<'a> {
25    /// `l2Book`, ungrouped.
26    Book(&'a L2Book),
27    /// `recentTrades`, newest first.
28    Trades(&'a [MarketTrade]),
29    /// `candleSnapshot`, oldest first: what the socket already sent of the same candles stays.
30    Candles(&'a [crate::markets::Candle]),
31    /// `allMids` of one perp dex, `""` the first.
32    Mids { dex: &'a str, mids: &'a Mids },
33    /// `metaAndAssetCtxs`: the first dex's universe, collateral and contexts.
34    Perps(&'a MetaAndAssetCtxs),
35    /// `metaAndAssetCtxs` with a HIP-3 `dex`.
36    Dex { dex: &'a str, read: &'a crate::markets::dexes::DexMetaAndAssetCtxs },
37    /// `perpDexs` with `allPerpMetas`: every dex's universe and collateral.
38    Dexes(&'a crate::markets::dexes::Dexes),
39    /// `spotMetaAndAssetCtxs`.
40    Spot(&'a SpotMetaAndAssetCtxs),
41    /// `spotMeta`: the tokens and pairs that price spot balances.
42    SpotMeta(&'a SpotMeta),
43    /// `clearinghouseState` of one dex, at the account's own address.
44    Clearinghouse { user: Address, dex: &'a str, state: &'a ClearinghouseState },
45    /// `spotClearinghouseState`.
46    SpotState { user: Address, state: &'a crate::account::SpotState },
47    /// `userAbstraction`.
48    Mode { user: Address, mode: &'a Abstraction },
49    /// `frontendOpenOrders` of one dex.
50    Orders { user: Address, dex: &'a str, orders: &'a [FrontendOrder] },
51    /// `userFills`: the latest, newest first, which replace what is held.
52    Fills { user: Address, fills: &'a [crate::account::fills::Fill] },
53    /// `userFillsByTime`: a window, oldest first, added to what is held.
54    MoreFills { user: Address, fills: &'a [crate::account::fills::Fill] },
55    /// `userFunding`: a window, oldest first, added to what is held.
56    Fundings { user: Address, fundings: &'a [UserFunding] },
57    /// `userNonFundingLedgerUpdates`: a window, oldest first, added to what is held, in the
58    /// socket's row type.
59    Ledger { user: Address, rows: &'a [LedgerUpdate] },
60}
61
62/// What a frame or a read did to the store.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum Folded {
65    /// Something a view reads changed.
66    Changed,
67    /// Told again, and nothing changed but when it was heard.
68    Same,
69    /// Not kept: nothing wants it, it is older than what is held, or it is not state.
70    Dropped,
71}
72
73/// What [`Store::sync`] gives the socket's owner.
74#[derive(Debug, Clone, Default, PartialEq, Eq)]
75pub struct Synced {
76    /// The frames to send, in order.
77    pub frames: Vec<String>,
78    /// What the session would not take; asked for again on the next sync.
79    pub refused: Vec<(Subscription, Refused)>,
80}
81
82/// A store and a session on two networks: one store is one network and never feeds a session on another.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
84#[error("this store is on {store} and the session on {session}; one store is one network")]
85pub struct OtherNetwork {
86    pub store: Network,
87    pub session: Network,
88}
89
90/// One network's live state.
91#[derive(Debug)]
92pub struct Store {
93    network: Network,
94    interests: Interests,
95    markets: Markets,
96    accounts: BTreeMap<Address, Account>,
97}
98
99impl Store {
100    /// An empty store on `network`, within the venue's whole per-IP limits.
101    #[must_use]
102    pub fn new(network: Network) -> Store {
103        Store {
104            network,
105            interests: Interests::new(Limits::VENUE),
106            markets: Markets::default(),
107            accounts: BTreeMap::new(),
108        }
109    }
110
111    /// Within a part of the IP's limits, for a store sharing it with other sessions.
112    #[must_use]
113    pub fn with_limits(mut self, limits: Limits) -> Store {
114        self.interests.set_limits(limits);
115        self
116    }
117
118    #[must_use]
119    pub const fn network(&self) -> Network {
120        self.network
121    }
122
123    /// Needs `interest` kept live until the handle is dropped or released. Refused past the
124    /// venue's limits, or when the venue would not take a subscription it needs.
125    pub fn register(&mut self, interest: Interest) -> Result<Handle, Refused> {
126        self.settle();
127        self.interests.register(interest)
128    }
129
130    /// Ends the interest `handle` holds, as dropping it does, and lets go at once of what
131    /// nothing needs any more.
132    pub fn release(&mut self, handle: Handle) {
133        drop(handle);
134        self.settle();
135    }
136
137    /// Every subscription wanted now, in the order first wanted.
138    #[must_use]
139    pub fn wanted(&self) -> &[Subscription] {
140        self.interests.wanted()
141    }
142
143    /// The accounts followed, in the order first followed; at most the limits' users.
144    #[must_use]
145    pub fn followed(&self) -> &[Address] {
146        self.interests.users()
147    }
148
149    /// What to tell the socket since it was last told: unsubscribe what nothing needs, then
150    /// subscribe what is newly needed.
151    pub fn changes(&mut self) -> Changes {
152        self.settle();
153        self.interests.changes()
154    }
155
156    /// [`Store::changes`] into `session`: the frames to send now. A session on another network
157    /// is refused before anything changes.
158    pub fn sync(&mut self, session: &mut Session) -> Result<Synced, OtherNetwork> {
159        if session.network() != self.network {
160            return Err(OtherNetwork { store: self.network, session: session.network() });
161        }
162        let changes = self.changes();
163        let mut synced = Synced::default();
164        for subscription in &changes.unsubscribe {
165            synced.frames.extend(session.unsubscribe(subscription));
166        }
167        for subscription in changes.subscribe {
168            match session.subscribe(subscription.clone()) {
169                Ok(frame) => synced.frames.extend(frame),
170                Err(refused) => {
171                    self.interests.refused(&subscription);
172                    synced.refused.push((subscription, refused));
173                }
174            }
175        }
176        Ok(synced)
177    }
178
179    /// A frame from the socket, received at `now` milliseconds. `userEvents` and `orderUpdates`
180    /// frames name no account, so with several followed they are not placed: fills, fundings and
181    /// resting orders come from their own channels.
182    pub fn apply(&mut self, now: u64, frame: &Frame) -> Folded {
183        self.settle();
184        match frame {
185            Frame::AllMids(mids) => self.fold_mids(now, &mids.dex, &mids.mids),
186            Frame::Book(book) => self.fold_book(now, book.clone()),
187            Frame::Trades(prints) => self.fold_prints(now, prints.iter().rev().cloned()),
188            Frame::Candle(candle) => self.fold_candles(now, [candle.clone()]),
189            Frame::AssetCtx { coin, ctx } => self.fold_perp_ctxs(now, [(coin.clone(), ctx.clone())]),
190            Frame::SpotAssetCtx { coin, ctx } => self.fold_spot_ctxs(now, [(coin.clone(), ctx.clone())]),
191            Frame::AllDexsAssetCtxs(dexes) => dexes
192                .iter()
193                .map(|(dex, ctxs)| self.fold_dex_ctxs(now, dex, ctxs.iter().cloned()))
194                .fold(Folded::Dropped, Folded::or),
195            Frame::AssetCtxs { dex, ctxs } => self.fold_dex_ctxs(now, dex, ctxs.iter().cloned()),
196            Frame::SpotAssetCtxs(ctxs) => {
197                self.fold_spot_ctxs(now, ctxs.iter().map(|ctx| (ctx.coin.clone(), ctx.clone())))
198            }
199            Frame::ClearinghouseState(one) => {
200                let state = one.state.clone();
201                self.with_state(one.user, |account| account.set_dex(now, &one.dex, state))
202            }
203            Frame::AllDexsClearinghouseState(all) => {
204                let states = all.states.clone();
205                self.with_state(all.user, |account| account.set_dexes(now, states))
206            }
207            Frame::SpotState(spot) => {
208                let wanted = Subscription::SpotState { user: spot.user, ignore_portfolio_margin: false };
209                self.with(spot.user, &wanted, |account| {
210                    let spot = convert::spot_state(spot, account.spot().map(|held| &held.value));
211                    account.set_spot(now, spot)
212                })
213            }
214            Frame::WebData3(web) => match web.user_state.abstraction.as_deref() {
215                Some(mode) => self.fold_mode(now, web.user_state.user, Abstraction::parse(mode)),
216                None => Folded::Dropped,
217            },
218            Frame::OpenOrders(open) => {
219                let orders = open.orders.clone();
220                self.fold_orders(now, open.user, &open.dex, orders)
221            }
222            Frame::UserFills(fills) => {
223                let newest = fills.fills.iter().rev().cloned().collect();
224                self.fold_fills(now, fills.user, newest, fills.is_snapshot)
225            }
226            Frame::UserFundings(fundings) => {
227                let wanted = Subscription::UserFundings { user: fundings.user };
228                let newest = fundings.fundings.iter().rev().cloned().collect();
229                let whole = fundings.is_snapshot;
230                self.with(fundings.user, &wanted, |account| account.fold_fundings(now, newest, whole))
231            }
232            Frame::Ledger(ledger) => {
233                let newest = ledger.updates.iter().rev().cloned().collect();
234                self.fold_ledger(now, ledger.user, newest, ledger.is_snapshot)
235            }
236            _ => Folded::Dropped,
237        }
238    }
239
240    /// A typed `/info` answer, read at `now` milliseconds. A read of something no interest wants
241    /// (a book, an account) is not kept; mids, contexts and the metas are kept whatever reads
242    /// them.
243    pub fn read(&mut self, now: u64, read: Read<'_>) -> Folded {
244        self.settle();
245        match read {
246            Read::Book(book) => self.fold_book(now, (*book).clone()),
247            Read::Trades(prints) => self.fold_prints(now, prints.iter().cloned()),
248            Read::Candles(candles) => self.fold_history(now, candles.to_vec()),
249            Read::Mids { dex, mids } => self.fold_mids(now, dex, mids),
250            Read::Perps(read) => {
251                let coins: Vec<String> = read.meta.universe.iter().map(|perp| perp.name.clone()).collect();
252                let listed = self.list("", coins, read.meta.collateral_token);
253                listed.or(self.fold_dex_ctxs(now, "", read.ctxs.iter().cloned()))
254            }
255            Read::Dex { dex, read } => {
256                let coins: Vec<String> = read.meta.universe.iter().map(|perp| perp.name.clone()).collect();
257                let listed = self.list(dex, coins, read.meta.collateral_token);
258                listed.or(self.fold_dex_ctxs(now, dex, read.ctxs.iter().cloned()))
259            }
260            Read::Dexes(dexes) => dexes
261                .list
262                .0
263                .iter()
264                .zip(&dexes.metas)
265                .map(|(dex, meta)| {
266                    let name = dex.as_ref().map_or("", |dex| dex.name.as_str());
267                    let coins = meta.universe.iter().map(|perp| perp.name.clone()).collect();
268                    self.list(name, coins, meta.collateral_token)
269                })
270                .fold(Folded::Dropped, Folded::or),
271            Read::Spot(read) => {
272                let listed = self.fold_spot_meta(&read.meta);
273                listed.or(self.fold_spot_ctxs(now, read.ctxs.iter().map(|ctx| (ctx.coin.clone(), ctx.clone()))))
274            }
275            Read::SpotMeta(meta) => self.fold_spot_meta(meta),
276            Read::Clearinghouse { user, dex, state } => {
277                self.with_state(user, |account| account.set_dex(now, dex, state.clone()))
278            }
279            Read::SpotState { user, state } => {
280                let wanted = Subscription::SpotState { user, ignore_portfolio_margin: false };
281                self.with(user, &wanted, |account| account.set_spot(now, state.clone()))
282            }
283            Read::Mode { user, mode } => self.fold_mode(now, user, mode.clone()),
284            Read::Orders { user, dex, orders } => self.fold_orders(now, user, dex, orders.to_vec()),
285            Read::Fills { user, fills } => self.fold_fills(now, user, fills.to_vec(), true),
286            Read::MoreFills { user, fills } => self.fold_fills(now, user, fills.iter().rev().cloned().collect(), false),
287            Read::Fundings { user, fundings } => {
288                let wanted = Subscription::UserFundings { user };
289                let newest = fundings.iter().rev().map(convert::funding).collect();
290                self.with(user, &wanted, |account| account.fold_fundings(now, newest, false))
291            }
292            Read::Ledger { user, rows } => self.fold_ledger(now, user, rows.iter().rev().cloned().collect(), false),
293        }
294    }
295
296    /// Ends the interests whose handles were dropped, and lets go of what nothing wants.
297    fn settle(&mut self) {
298        if !self.interests.drain() {
299            return;
300        }
301        let interests = &self.interests;
302        self.markets.books.retain(|coin, _| interests.wants(&book(coin)));
303        self.markets.tapes.retain(|coin, _| interests.wants(&Subscription::Trades { coin: coin.clone() }));
304        self.markets.candles.retain(|(coin, interval), _| {
305            interests.wants(&Subscription::Candle { coin: coin.clone(), interval: *interval })
306        });
307        let users = interests.users();
308        self.accounts.retain(|user, _| users.contains(user));
309        for (user, account) in &mut self.accounts {
310            let user = *user;
311            let orders = |dex: &str| interests.wants(&Subscription::OpenOrders { user, dex: dex.to_owned() });
312            account.keep(&Keep {
313                state: interests.wants(&Subscription::AllDexsClearinghouseState { user }),
314                orders: &orders,
315                fills: interests.wants(&Subscription::UserFills { user, aggregate_by_time: false }),
316                fundings: interests.wants(&Subscription::UserFundings { user }),
317                ledger: interests.wants(&Subscription::UserNonFundingLedgerUpdates { user }),
318            });
319        }
320    }
321
322    fn fold_mids(&mut self, now: u64, dex: &str, mids: &Mids) -> Folded {
323        let mids: BTreeMap<String, Dec> = mids.iter().map(|(coin, mid)| (coin.to_owned(), mid)).collect();
324        match self.markets.mids.get_mut(dex) {
325            Some(held) => Folded::of(held.set(now, mids)),
326            None => {
327                self.markets.mids.insert(dex.to_owned(), Stamped::new(now, mids));
328                Folded::Changed
329            }
330        }
331    }
332
333    /// A book replaces the one held unless it is older by its own time. A grouped book (one
334    /// with a spread) is not the full book the store asks for.
335    fn fold_book(&mut self, now: u64, sent: Book) -> Folded {
336        if sent.spread.is_some() || !self.interests.wants(&book(&sent.coin)) {
337            return Folded::Dropped;
338        }
339        match self.markets.books.get_mut(&sent.coin) {
340            Some(held) if held.time > sent.time => Folded::Dropped,
341            Some(held) => Folded::of(held.set(now, sent)),
342            None => {
343                self.markets.books.insert(sent.coin.clone(), Stamped::new(now, sent));
344                Folded::Changed
345            }
346        }
347    }
348
349    /// Prints, newest first, onto each wanted coin's tape.
350    fn fold_prints(&mut self, now: u64, prints: impl IntoIterator<Item = Trade>) -> Folded {
351        let mut by_coin: BTreeMap<String, Vec<Trade>> = BTreeMap::new();
352        for print in prints {
353            by_coin.entry(print.coin.clone()).or_default().push(print);
354        }
355        let mut folded = Folded::Dropped;
356        for (coin, prints) in by_coin {
357            if !self.interests.wants(&Subscription::Trades { coin: coin.clone() }) {
358                continue;
359            }
360            let tape = self.markets.tapes.entry(coin).or_insert_with(|| Stamped::new(now, Vec::new()));
361            let changed = fold_prints(&mut tape.value, prints);
362            tape.touch(now, changed);
363            folded = folded.or(Folded::of(changed));
364        }
365        folded
366    }
367
368    /// Candles from the socket, each onto its wanted market and interval.
369    fn fold_candles(&mut self, now: u64, candles: impl IntoIterator<Item = Candle>) -> Folded {
370        let mut folded = Folded::Dropped;
371        for candle in candles {
372            let key = (candle.coin.clone(), candle.interval);
373            if !self.interests.wants(&Subscription::Candle { coin: key.0.clone(), interval: key.1 }) {
374                continue;
375            }
376            let bars = self.markets.candles.entry(key).or_insert_with(|| Stamped::new(now, Vec::new()));
377            let changed = fold_bar(&mut bars.value, candle);
378            bars.touch(now, changed);
379            folded = folded.or(Folded::of(changed));
380        }
381        folded
382    }
383
384    /// Candles read, oldest first: the history goes under what the socket already sent, and
385    /// the socket's win.
386    fn fold_history(&mut self, now: u64, history: Vec<Candle>) -> Folded {
387        let mut by_market: BTreeMap<(String, Interval), Vec<Candle>> = BTreeMap::new();
388        for candle in history {
389            by_market.entry((candle.coin.clone(), candle.interval)).or_default().push(candle);
390        }
391        let mut folded = Folded::Dropped;
392        for (key, mut history) in by_market {
393            if !self.interests.wants(&Subscription::Candle { coin: key.0.clone(), interval: key.1 }) {
394                continue;
395            }
396            let bars = self.markets.candles.entry(key).or_insert_with(|| Stamped::new(now, Vec::new()));
397            history.sort_by_key(|candle| candle.open_time);
398            let live = std::mem::take(&mut bars.value);
399            let mut merged = history;
400            for bar in live.iter().cloned() {
401                let _ = fold_bar(&mut merged, bar);
402            }
403            if merged.len() > BARS {
404                let extra = merged.len() - BARS;
405                merged.drain(..extra);
406            }
407            let changed = merged != live;
408            bars.value = merged;
409            bars.touch(now, changed);
410            folded = folded.or(Folded::of(changed));
411        }
412        folded
413    }
414
415    fn fold_perp_ctxs(&mut self, now: u64, ctxs: impl IntoIterator<Item = (String, PerpCtx)>) -> Folded {
416        let mut folded = Folded::Dropped;
417        for (coin, ctx) in ctxs {
418            let changed = match self.markets.perps.get_mut(&coin) {
419                Some(held) => held.set(now, ctx),
420                None => {
421                    self.markets.perps.insert(coin, Stamped::new(now, ctx));
422                    true
423                }
424            };
425            folded = folded.or(Folded::of(changed));
426        }
427        folded
428    }
429
430    fn fold_spot_ctxs(&mut self, now: u64, ctxs: impl IntoIterator<Item = (String, SpotCtx)>) -> Folded {
431        let mut folded = Folded::Dropped;
432        for (coin, ctx) in ctxs {
433            let changed = match self.markets.spots.get_mut(&coin) {
434                Some(held) => held.set(now, ctx),
435                None => {
436                    self.markets.spots.insert(coin, Stamped::new(now, ctx));
437                    true
438                }
439            };
440            folded = folded.or(Folded::of(changed));
441        }
442        folded
443    }
444
445    /// One dex's contexts in its universe's order. With no universe read for the dex there is
446    /// nothing to place them on; past its end (a perp listed since) they wait for the next read.
447    fn fold_dex_ctxs(&mut self, now: u64, dex: &str, ctxs: impl IntoIterator<Item = PerpCtx>) -> Folded {
448        let Some(coins) = self.markets.universes.get(dex) else { return Folded::Dropped };
449        let placed: Vec<(String, PerpCtx)> = coins.iter().cloned().zip(ctxs).collect();
450        self.fold_perp_ctxs(now, placed)
451    }
452
453    /// A dex's universe and collateral, from its meta.
454    fn list(&mut self, dex: &str, coins: Vec<String>, collateral: Option<u32>) -> Folded {
455        let token = collateral.unwrap_or(USDC_INDEX);
456        let changed = self.markets.universes.get(dex) != Some(&coins) || self.markets.collateral.of(dex) != token;
457        self.markets.universes.insert(dex.to_owned(), coins);
458        self.markets.collateral = std::mem::take(&mut self.markets.collateral).with(dex, token);
459        Folded::of(changed)
460    }
461
462    fn fold_spot_meta(&mut self, meta: &SpotMeta) -> Folded {
463        let changed = self.markets.spot_meta.as_ref() != Some(meta);
464        self.markets.spot_meta = Some(meta.clone());
465        Folded::of(changed)
466    }
467
468    fn fold_mode(&mut self, now: u64, user: Address, mode: Abstraction) -> Folded {
469        self.with(user, &Subscription::WebData3 { user }, |account| account.set_mode(now, mode))
470    }
471
472    fn fold_orders(&mut self, now: u64, user: Address, dex: &str, orders: Vec<FrontendOrder>) -> Folded {
473        let wanted = Subscription::OpenOrders { user, dex: dex.to_owned() };
474        self.with(user, &wanted, |account| account.set_orders(now, dex, orders))
475    }
476
477    fn fold_fills(&mut self, now: u64, user: Address, fills: Vec<crate::live::Fill>, whole: bool) -> Folded {
478        let wanted = Subscription::UserFills { user, aggregate_by_time: false };
479        self.with(user, &wanted, |account| account.fold_fills(now, fills, whole))
480    }
481
482    fn fold_ledger(&mut self, now: u64, user: Address, rows: Vec<LedgerUpdate>, whole: bool) -> Folded {
483        let wanted = Subscription::UserNonFundingLedgerUpdates { user };
484        self.with(user, &wanted, |account| account.fold_ledger(now, rows, whole))
485    }
486
487    /// Changes `user`'s clearinghouse, while its state is wanted.
488    fn with_state(&mut self, user: Address, change: impl FnOnce(&mut Account) -> bool) -> Folded {
489        self.with(user, &Subscription::AllDexsClearinghouseState { user }, change)
490    }
491
492    /// Changes `user`'s account while `wanted` is: what no interest asked for is not kept.
493    fn with(&mut self, user: Address, wanted: &Subscription, change: impl FnOnce(&mut Account) -> bool) -> Folded {
494        if !self.interests.wants(wanted) {
495            return Folded::Dropped;
496        }
497        Folded::of(change(self.accounts.entry(user).or_default()))
498    }
499
500    // Views: pure reads of the state.
501
502    /// A market's book, as last told.
503    #[must_use]
504    pub fn book(&self, coin: &str) -> Option<&Stamped<Book>> {
505        self.markets.books.get(coin)
506    }
507
508    /// The best bid and the best ask of the book held.
509    #[must_use]
510    pub fn best(&self, coin: &str) -> (Option<Level>, Option<Level>) {
511        self.book(coin).map_or((None, None), |book| (book.bids.first().copied(), book.asks.first().copied()))
512    }
513
514    /// A coin's mid: halfway between the touch where a book with both sides is held (sent on
515    /// every change), else what `allMids` said, else the context's.
516    #[must_use]
517    pub fn mid(&self, coin: &str) -> Option<Dec> {
518        if let Some(mid) = self.book(coin).and_then(|book| book.mid()) {
519            return Some(mid);
520        }
521        let dex = split_coin(coin).map_or("", |(dex, _)| dex);
522        if let Some(mid) = self.markets.mids.get(dex).and_then(|mids| mids.get(coin)) {
523            return Some(*mid);
524        }
525        self.context(coin).and_then(|ctx| ctx.mid())
526    }
527
528    /// One perp dex's mids, `""` the first.
529    #[must_use]
530    pub fn mids(&self, dex: &str) -> Option<&Stamped<BTreeMap<String, Dec>>> {
531        self.markets.mids.get(dex)
532    }
533
534    /// A market's context.
535    #[must_use]
536    pub fn context(&self, coin: &str) -> Option<Stamped<Context<'_>>> {
537        if let Some(ctx) = self.markets.perps.get(coin) {
538            return Some(Stamped { value: Context::Perp(&ctx.value), heard: ctx.heard, changed: ctx.changed });
539        }
540        let ctx = self.markets.spots.get(coin)?;
541        Some(Stamped { value: Context::Spot(&ctx.value), heard: ctx.heard, changed: ctx.changed })
542    }
543
544    /// A market's prints, newest first.
545    #[must_use]
546    pub fn trades(&self, coin: &str) -> Option<&Stamped<Vec<Trade>>> {
547        self.markets.tapes.get(coin)
548    }
549
550    /// A market's candles at `interval`, oldest first, the last one perhaps still forming.
551    #[must_use]
552    pub fn candles(&self, coin: &str, interval: Interval) -> Option<&Stamped<Vec<Candle>>> {
553        self.markets.candles.get(&(coin.to_owned(), interval))
554    }
555
556    /// Each perp dex's collateral token, as the metas read say.
557    #[must_use]
558    pub const fn collateral(&self) -> &Collateral {
559        &self.markets.collateral
560    }
561
562    #[must_use]
563    pub fn account(&self, user: Address) -> Option<&Account> {
564        self.accounts.get(&user)
565    }
566
567    /// Every open position of `user`, dex by dex.
568    #[must_use]
569    pub fn positions(&self, user: Address) -> Vec<&Position> {
570        self.account(user).map_or_else(Vec::new, |account| account.positions().collect())
571    }
572
573    /// Every resting order of `user`, dex by dex.
574    #[must_use]
575    pub fn orders(&self, user: Address) -> Vec<&FrontendOrder> {
576        self.account(user).map_or_else(Vec::new, |account| account.all_orders().collect())
577    }
578
579    /// The kind of market `coin` is, and so what an order on it draws on: an outcome side, a
580    /// spot pair (its quote as spot meta lists it; `None` until spot meta is read), or a perp on
581    /// its dex.
582    #[must_use]
583    pub fn surface<'a>(&self, coin: &'a str) -> Option<Surface<'a>> {
584        if coin.starts_with(['#', '+']) {
585            return Some(Surface::Outcomes);
586        }
587        if coin.starts_with('@') || coin.contains('/') {
588            let pair = self.markets.spot_meta.as_ref()?.pair_named(coin)?;
589            return Some(Surface::Spot { quote: pair.quote() });
590        }
591        Some(Surface::Perps { dex: split_coin(coin).map_or("", |(dex, _)| dex) })
592    }
593
594    /// What `user` can put on an order in `coin`, read the way its mode says (portfolio margin's
595    /// lending priced at [`Store::price`]). `None` until the account's state has been told.
596    #[must_use]
597    pub fn usable(&self, user: Address, coin: &str) -> Option<Usable> {
598        let surface = self.surface(coin)?;
599        let account = self.account(user).filter(|account| account.spot.is_some() || !account.dexes.is_empty())?;
600        Some(account.standing().usable(surface, &self.markets.collateral, |name| self.price(name)))
601    }
602
603    /// What one of a spot balance's rows is worth in USDC, by the name the row carries: USDC at
604    /// one, a token at the mid of its pair with USDC (spot meta names the pair), an outcome side
605    /// `+N` at the mid quoted as `#N`. A mid of nothing is no price.
606    #[must_use]
607    pub fn price(&self, name: &str) -> Option<Dec> {
608        if name == "USDC" {
609            return Some(Dec::ONE);
610        }
611        let mids = self.markets.mids.get("")?;
612        let mid = |coin: &str| mids.get(coin).copied().filter(|mid| mid.is_positive());
613        if let Some(side) = name.strip_prefix('+') {
614            return mid(&format!("#{side}"));
615        }
616        let meta = self.markets.spot_meta.as_ref()?;
617        mid(&meta.pair_of(meta.token_named(name)?.index, USDC_INDEX)?.name)
618    }
619
620    /// Every price [`Store::price`] knows, as [`Prices`]: each token's first pair with USDC (as
621    /// spot meta's `pair_of` finds it) and the first dex's mids.
622    #[must_use]
623    pub fn prices(&self) -> Prices {
624        let Some(mids) = self.markets.mids.get("") else { return Prices::new() };
625        let mut pairs: BTreeMap<String, String> = BTreeMap::new();
626        if let Some(meta) = self.markets.spot_meta.as_ref() {
627            let names: BTreeMap<u32, &str> =
628                meta.tokens.iter().map(|token| (token.index, token.name.as_str())).collect();
629            for pair in meta.universe.iter().filter(|pair| pair.quote() == USDC_INDEX) {
630                if let Some(name) = names.get(&pair.base()) {
631                    pairs.entry((*name).to_owned()).or_insert_with(|| pair.name.clone());
632                }
633            }
634        }
635        Prices::of(&pairs, mids)
636    }
637
638    /// What `user` is worth, counted once. `beside` is what is read of it elsewhere: staking,
639    /// vaults, lending.
640    #[must_use]
641    pub fn worth(&self, user: Address, beside: &Beside) -> Option<Worth> {
642        self.account(user)?.standing().worth(&self.prices(), beside)
643    }
644
645    /// Several accounts summed, each counted once: a deposit one of them holds in a vault that
646    /// is itself one of them is counted as the vault. An account not held is left out.
647    #[must_use]
648    pub fn combined(&self, accounts: &[(Address, Beside)]) -> Option<Worth> {
649        let held: Vec<(Address, &Standing, &Beside)> = accounts
650            .iter()
651            .filter_map(|(user, beside)| Some((*user, self.account(*user)?.standing(), beside)))
652            .collect();
653        combined(&held, &self.prices())
654    }
655}
656
657impl Folded {
658    const fn of(changed: bool) -> Folded {
659        match changed {
660            true => Folded::Changed,
661            false => Folded::Same,
662        }
663    }
664
665    /// The more of the two: a change over a repeat over nothing kept.
666    const fn or(self, other: Folded) -> Folded {
667        match (self, other) {
668            (Folded::Changed, _) | (_, Folded::Changed) => Folded::Changed,
669            (Folded::Same, _) | (_, Folded::Same) => Folded::Same,
670            _ => Folded::Dropped,
671        }
672    }
673}
674
675/// The full book's subscription, the one [`Interest::Book`] asks for.
676fn book(coin: &str) -> Subscription {
677    Subscription::L2Book { coin: coin.to_owned(), n_sig_figs: None, mantissa: None, fast: false }
678}