Skip to main content

df_hyperliquid_core/trade/
answer.rs

1//! What `/exchange` answers to a trading action, read per item. A count of statuses that does not
2//! match what was sent is unreadable, never a partial success, except an error decided by the
3//! payload alone, which comes back once and stands for every item.
4
5use crate::client::transport::EXCHANGE;
6use crate::client::{Accepted, ApiError, ExchangeResponse, Status, Transport, exchange};
7use crate::trade::order::Cloid;
8use crate::utils::Dec;
9use serde::Deserialize;
10use serde_json::Value;
11
12/// What the venue said of one order.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum OrderAnswer {
15    /// On the book.
16    Resting { oid: u64, cloid: Option<Cloid> },
17    /// Filled `total_sz` at `avg_px` on average: all of it, or as much as
18    /// crossed for an IOC or a market order.
19    Filled { oid: u64, total_sz: Dec, avg_px: Dec, cloid: Option<Cloid> },
20    /// A take profit or stop loss, placed and waiting for its trigger.
21    WaitingForTrigger,
22    /// A take profit or stop loss grouped with an entry that has not filled.
23    WaitingForFill,
24    /// Refused, in the venue's words.
25    Refused(String),
26    /// A status this build does not know, kept as written. Never read as placed.
27    Unknown(String),
28}
29
30#[derive(Deserialize)]
31#[serde(rename_all = "camelCase")]
32struct Rested {
33    oid: u64,
34    #[serde(default)]
35    cloid: Option<Cloid>,
36}
37
38#[derive(Deserialize)]
39#[serde(rename_all = "camelCase")]
40struct Filled {
41    oid: u64,
42    total_sz: Dec,
43    avg_px: Dec,
44    #[serde(default)]
45    cloid: Option<Cloid>,
46}
47
48impl OrderAnswer {
49    /// One order's status. An object this build knows but cannot read (a
50    /// resting order with no `oid`) is an error, not a guess.
51    pub fn read(status: &Status) -> Result<OrderAnswer, String> {
52        let value = match status {
53            Status::Error(reason) => return Ok(OrderAnswer::Refused(reason.clone())),
54            Status::Ok(value) => value,
55        };
56        let shape = |error: serde_json::Error| format!("{error} in {value}");
57        match value {
58            Value::String(word) => Ok(match word.as_str() {
59                "waitingForTrigger" => OrderAnswer::WaitingForTrigger,
60                "waitingForFill" => OrderAnswer::WaitingForFill,
61                other => OrderAnswer::Unknown(other.to_owned()),
62            }),
63            Value::Object(entry) => {
64                if let Some(resting) = entry.get("resting") {
65                    let Rested { oid, cloid } = Rested::deserialize(resting).map_err(shape)?;
66                    return Ok(OrderAnswer::Resting { oid, cloid });
67                }
68                if let Some(filled) = entry.get("filled") {
69                    let Filled { oid, total_sz, avg_px, cloid } = Filled::deserialize(filled).map_err(shape)?;
70                    return Ok(OrderAnswer::Filled { oid, total_sz, avg_px, cloid });
71                }
72                Ok(OrderAnswer::Unknown(value.to_string()))
73            }
74            other => Err(format!("a status that is neither a word nor an object: {other}")),
75        }
76    }
77
78    /// The venue's id for the order, once it has one.
79    #[must_use]
80    pub const fn oid(&self) -> Option<u64> {
81        match self {
82            OrderAnswer::Resting { oid, .. } | OrderAnswer::Filled { oid, .. } => Some(*oid),
83            _ => None,
84        }
85    }
86
87    /// Taken by the venue: resting, filled, or waiting.
88    #[must_use]
89    pub const fn is_placed(&self) -> bool {
90        matches!(
91            self,
92            OrderAnswer::Resting { .. }
93                | OrderAnswer::Filled { .. }
94                | OrderAnswer::WaitingForTrigger
95                | OrderAnswer::WaitingForFill
96        )
97    }
98
99    #[must_use]
100    pub fn refusal(&self) -> Option<&str> {
101        match self {
102            OrderAnswer::Refused(reason) => Some(reason),
103            _ => None,
104        }
105    }
106}
107
108/// What the venue said of one cancel, or of a TWAP's cancel.
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub enum CancelAnswer {
111    Cancelled,
112    /// In the venue's words; any word but `success` is a refusal.
113    Refused(String),
114}
115
116impl CancelAnswer {
117    #[must_use]
118    pub fn read(status: &Status) -> CancelAnswer {
119        match status {
120            Status::Error(reason) => CancelAnswer::Refused(reason.clone()),
121            Status::Ok(Value::String(word)) if word == "success" => CancelAnswer::Cancelled,
122            Status::Ok(Value::String(word)) => CancelAnswer::Refused(word.clone()),
123            Status::Ok(other) => CancelAnswer::Refused(other.to_string()),
124        }
125    }
126}
127
128/// What the venue said of a TWAP.
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub enum TwapAnswer {
131    /// Running under this id, which `twapCancel` names.
132    Running {
133        twap_id: u64,
134    },
135    Refused(String),
136}
137
138fn unreadable(detail: String) -> ApiError {
139    ApiError::Unreadable { path: EXCHANGE, detail }
140}
141
142/// One answer per item sent, the lone error of a whole batch repeated.
143fn per_item<T>(
144    accepted: &Accepted,
145    sent: usize,
146    read: impl Fn(&Status) -> Result<T, String>,
147) -> Result<Vec<T>, ApiError> {
148    let statuses = accepted.statuses.as_slice();
149    if let [lone @ Status::Error(_)] = statuses
150        && sent > 1
151    {
152        return (0..sent).map(|_| read(lone).map_err(unreadable)).collect();
153    }
154    if statuses.len() != sent {
155        return Err(unreadable(format!("{sent} were sent and {} statuses came back", statuses.len())));
156    }
157    statuses.iter().map(|status| read(status).map_err(unreadable)).collect()
158}
159
160/// The one status a TWAP or its cancel answers.
161fn lone(accepted: &Accepted) -> Result<&Status, ApiError> {
162    match accepted.statuses.as_slice() {
163        [status] => Ok(status),
164        many => Err(unreadable(format!("one status was expected and {} came back", many.len()))),
165    }
166}
167
168/// An `order` or `batchModify` answer, `sent` orders in it.
169pub fn orders(accepted: &Accepted, sent: usize) -> Result<Vec<OrderAnswer>, ApiError> {
170    per_item(accepted, sent, OrderAnswer::read)
171}
172
173/// A `cancel` or `cancelByCloid` answer, `sent` cancels in it.
174pub fn cancels(accepted: &Accepted, sent: usize) -> Result<Vec<CancelAnswer>, ApiError> {
175    per_item(accepted, sent, |status| Ok(CancelAnswer::read(status)))
176}
177
178/// A `twapOrder` answer.
179pub fn twap(accepted: &Accepted) -> Result<TwapAnswer, ApiError> {
180    match lone(accepted)? {
181        Status::Error(reason) => Ok(TwapAnswer::Refused(reason.clone())),
182        Status::Ok(value) => value
183            .get("running")
184            .and_then(|running| running.get("twapId"))
185            .and_then(Value::as_u64)
186            .map(|twap_id| TwapAnswer::Running { twap_id })
187            .ok_or_else(|| unreadable(format!("a TWAP status this build cannot read: {value}"))),
188    }
189}
190
191/// A `twapCancel` answer.
192pub fn twap_cancel(accepted: &Accepted) -> Result<CancelAnswer, ApiError> {
193    lone(accepted).map(CancelAnswer::read)
194}
195
196/// An answer that says only that it was taken: a single `modify`, leverage,
197/// margin, `scheduleCancel`, `noop`. A status that is an error refuses it.
198pub fn taken(accepted: &Accepted) -> Result<(), ApiError> {
199    match accepted.errors().next() {
200        Some(reason) => Err(ApiError::Refused(reason.to_owned())),
201        None => Ok(()),
202    }
203}
204
205/// The answer's text, read: `ok`, or the venue's refusal of the whole action.
206pub fn read(text: &str) -> Result<Accepted, ApiError> {
207    match ExchangeResponse::parse(text)? {
208        ExchangeResponse::Ok(accepted) => Ok(accepted),
209        ExchangeResponse::Err(reason) => Err(ApiError::Refused(reason)),
210    }
211}
212
213/// Posts a signed `order` or `batchModify` body once and reads its answer.
214pub async fn send_orders(transport: &impl Transport, body: String, sent: usize) -> Result<Vec<OrderAnswer>, ApiError> {
215    orders(&exchange(transport, body).await?, sent)
216}
217
218/// Posts a signed `cancel` or `cancelByCloid` body once and reads its answer.
219pub async fn send_cancels(
220    transport: &impl Transport,
221    body: String,
222    sent: usize,
223) -> Result<Vec<CancelAnswer>, ApiError> {
224    cancels(&exchange(transport, body).await?, sent)
225}
226
227/// Posts a signed `twapOrder` body once and reads its answer.
228pub async fn send_twap(transport: &impl Transport, body: String) -> Result<TwapAnswer, ApiError> {
229    twap(&exchange(transport, body).await?)
230}
231
232/// Posts a signed `twapCancel` body once and reads its answer.
233pub async fn send_twap_cancel(transport: &impl Transport, body: String) -> Result<CancelAnswer, ApiError> {
234    twap_cancel(&exchange(transport, body).await?)
235}
236
237/// Posts any other signed trading body once: taken, or refused.
238pub async fn send_taken(transport: &impl Transport, body: String) -> Result<(), ApiError> {
239    taken(&exchange(transport, body).await?)
240}
241
242#[cfg(test)]
243#[path = "tests/answer.rs"]
244mod tests;