Skip to main content

df_hyperliquid_core/markets/
candle.rs

1//! Candles and their intervals, the same shape from the `candleSnapshot` read and from the `candle`
2//! feed.
3
4use std::{borrow::Cow, fmt, str::FromStr};
5
6use serde::{Deserialize, Deserializer, de};
7
8use crate::client::{ApiError, Transport, info, info_request};
9use crate::utils::Json;
10use crate::utils::dec::Dec;
11
12/// `{"type":"candleSnapshot","req":{"coin","interval","startTime","endTime"}}`,
13/// the times unix milliseconds. Only the 5000 most recent candles are kept.
14#[must_use]
15pub fn candle_snapshot_request(coin: &str, interval: Interval, start: u64, end: u64) -> String {
16    let req = Json::Object(vec![
17        ("coin".to_owned(), Json::String(coin.to_owned())),
18        ("interval".to_owned(), Json::String(interval.wire().to_owned())),
19        ("startTime".to_owned(), Json::Number(start.to_string())),
20        ("endTime".to_owned(), Json::Number(end.to_string())),
21    ]);
22    info_request("candleSnapshot", &[("req", req)])
23}
24
25/// The candles in the window, oldest first; none when the venue answers `null`.
26pub async fn candle_snapshot(
27    transport: &impl Transport,
28    coin: &str,
29    interval: Interval,
30    start: u64,
31    end: u64,
32) -> Result<Vec<Candle>, ApiError> {
33    let candles: Option<Vec<Candle>> = info(transport, candle_snapshot_request(coin, interval, start, end)).await?;
34    Ok(candles.unwrap_or_default())
35}
36
37/// A candle's width; every interval the venue accepts.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
39pub enum Interval {
40    OneMinute,
41    ThreeMinutes,
42    FiveMinutes,
43    FifteenMinutes,
44    ThirtyMinutes,
45    OneHour,
46    TwoHours,
47    FourHours,
48    EightHours,
49    TwelveHours,
50    OneDay,
51    ThreeDays,
52    OneWeek,
53    OneMonth,
54}
55
56const MINUTE_MS: u64 = 60_000;
57
58impl Interval {
59    /// Every interval the venue accepts, finest first.
60    pub const ALL: [Interval; 14] = [
61        Interval::OneMinute,
62        Interval::ThreeMinutes,
63        Interval::FiveMinutes,
64        Interval::FifteenMinutes,
65        Interval::ThirtyMinutes,
66        Interval::OneHour,
67        Interval::TwoHours,
68        Interval::FourHours,
69        Interval::EightHours,
70        Interval::TwelveHours,
71        Interval::OneDay,
72        Interval::ThreeDays,
73        Interval::OneWeek,
74        Interval::OneMonth,
75    ];
76
77    /// As the venue spells it: `1m` is a minute, `1M` a month.
78    #[must_use]
79    pub const fn wire(self) -> &'static str {
80        match self {
81            Interval::OneMinute => "1m",
82            Interval::ThreeMinutes => "3m",
83            Interval::FiveMinutes => "5m",
84            Interval::FifteenMinutes => "15m",
85            Interval::ThirtyMinutes => "30m",
86            Interval::OneHour => "1h",
87            Interval::TwoHours => "2h",
88            Interval::FourHours => "4h",
89            Interval::EightHours => "8h",
90            Interval::TwelveHours => "12h",
91            Interval::OneDay => "1d",
92            Interval::ThreeDays => "3d",
93            Interval::OneWeek => "1w",
94            Interval::OneMonth => "1M",
95        }
96    }
97
98    /// Accepts only the venue's exact spellings.
99    #[must_use]
100    pub fn parse(text: &str) -> Option<Interval> {
101        Interval::ALL.into_iter().find(|interval| interval.wire() == text)
102    }
103
104    /// Its width in milliseconds; a month counts thirty days.
105    #[must_use]
106    pub const fn ms(self) -> u64 {
107        let minutes = match self {
108            Interval::OneMinute => 1,
109            Interval::ThreeMinutes => 3,
110            Interval::FiveMinutes => 5,
111            Interval::FifteenMinutes => 15,
112            Interval::ThirtyMinutes => 30,
113            Interval::OneHour => 60,
114            Interval::TwoHours => 120,
115            Interval::FourHours => 240,
116            Interval::EightHours => 480,
117            Interval::TwelveHours => 720,
118            Interval::OneDay => 1_440,
119            Interval::ThreeDays => 4_320,
120            Interval::OneWeek => 10_080,
121            Interval::OneMonth => 43_200,
122        };
123        minutes * MINUTE_MS
124    }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
128#[error("`{0}` is not an interval; the venue takes 1m 3m 5m 15m 30m 1h 2h 4h 8h 12h 1d 3d 1w 1M")]
129pub struct UnknownInterval(pub String);
130
131impl FromStr for Interval {
132    type Err = UnknownInterval;
133
134    fn from_str(text: &str) -> Result<Self, Self::Err> {
135        Interval::parse(text).ok_or_else(|| UnknownInterval(text.to_owned()))
136    }
137}
138
139impl fmt::Display for Interval {
140    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141        f.write_str(self.wire())
142    }
143}
144
145impl<'de> Deserialize<'de> for Interval {
146    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
147        let text = <Cow<'de, str>>::deserialize(deserializer)?;
148        text.parse().map_err(de::Error::custom)
149    }
150}
151
152/// One candle, `t` to `T` inclusive. The feed resends it as it forms.
153#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
154pub struct Candle {
155    /// Open time, milliseconds.
156    #[serde(rename = "t")]
157    pub open_time: u64,
158    /// The last millisecond inside it, not the next one's open.
159    #[serde(rename = "T")]
160    pub close_time: u64,
161    #[serde(rename = "s")]
162    pub coin: String,
163    #[serde(rename = "i")]
164    pub interval: Interval,
165    #[serde(rename = "o")]
166    pub open: Dec,
167    #[serde(rename = "c")]
168    pub close: Dec,
169    #[serde(rename = "h")]
170    pub high: Dec,
171    #[serde(rename = "l")]
172    pub low: Dec,
173    /// In the base asset.
174    #[serde(rename = "v")]
175    pub volume: Dec,
176    /// Number of trades in the bar.
177    #[serde(rename = "n")]
178    pub trades: u64,
179}
180
181#[cfg(test)]
182#[path = "tests/candle.rs"]
183mod tests;