hyperliquid_rust_sdk_abrkn/
market_maker.rs

1use ethers::{
2    signers::{LocalWallet, Signer},
3    types::H160,
4};
5use log::{error, info};
6
7use tokio::sync::mpsc::unbounded_channel;
8
9use crate::{
10    bps_diff, truncate_float, BaseUrl, ClientCancelRequest, ClientLimit, ClientOrder,
11    ClientOrderRequest, ExchangeClient, ExchangeDataStatus, ExchangeResponseStatus, InfoClient,
12    Message, Subscription, UserData, EPSILON,
13};
14#[derive(Debug)]
15pub struct MarketMakerRestingOrder {
16    pub oid: u64,
17    pub position: f64,
18    pub price: f64,
19}
20
21pub struct MarketMakerInput {
22    pub asset: String,
23    pub target_liquidity: f64, // Amount of liquidity on both sides to target
24    pub half_spread: u16,      // Half of the spread for our market making (in BPS)
25    pub max_bps_diff: u16, // Max deviation before we cancel and put new orders on the book (in BPS)
26    pub max_absolute_position_size: f64, // Absolute value of the max position we can take on
27    pub decimals: u32,     // Decimals to round to for pricing
28    pub wallet: LocalWallet, // Wallet containing private key
29}
30
31pub struct MarketMaker {
32    pub asset: String,
33    pub target_liquidity: f64,
34    pub half_spread: u16,
35    pub max_bps_diff: u16,
36    pub max_absolute_position_size: f64,
37    pub decimals: u32,
38    pub lower_resting: MarketMakerRestingOrder,
39    pub upper_resting: MarketMakerRestingOrder,
40    pub cur_position: f64,
41    pub latest_mid_price: f64,
42    pub info_client: InfoClient,
43    pub exchange_client: ExchangeClient,
44    pub user_address: H160,
45}
46
47impl MarketMaker {
48    pub async fn new(input: MarketMakerInput) -> MarketMaker {
49        let user_address = input.wallet.address();
50
51        let info_client = InfoClient::new(None, Some(BaseUrl::Testnet)).await.unwrap();
52        let exchange_client =
53            ExchangeClient::new(None, input.wallet, Some(BaseUrl::Testnet), None, None)
54                .await
55                .unwrap();
56
57        MarketMaker {
58            asset: input.asset,
59            target_liquidity: input.target_liquidity,
60            half_spread: input.half_spread,
61            max_bps_diff: input.max_bps_diff,
62            max_absolute_position_size: input.max_absolute_position_size,
63            decimals: input.decimals,
64            lower_resting: MarketMakerRestingOrder {
65                oid: 0,
66                position: 0.0,
67                price: -1.0,
68            },
69            upper_resting: MarketMakerRestingOrder {
70                oid: 0,
71                position: 0.0,
72                price: -1.0,
73            },
74            cur_position: 0.0,
75            latest_mid_price: -1.0,
76            info_client,
77            exchange_client,
78            user_address,
79        }
80    }
81
82    pub async fn start(&mut self) {
83        let (sender, mut receiver) = unbounded_channel();
84
85        // Subscribe to UserEvents for fills
86        self.info_client
87            .subscribe(
88                Subscription::UserEvents {
89                    user: self.user_address,
90                },
91                sender.clone(),
92            )
93            .await
94            .unwrap();
95
96        // Subscribe to AllMids so we can market make around the mid price
97        self.info_client
98            .subscribe(Subscription::AllMids, sender)
99            .await
100            .unwrap();
101
102        loop {
103            let message = receiver.recv().await.unwrap();
104            match message {
105                Message::AllMids(all_mids) => {
106                    let all_mids = all_mids.data.mids;
107                    let mid = all_mids.get(&self.asset);
108                    if let Some(mid) = mid {
109                        let mid: f64 = mid.parse().unwrap();
110                        self.latest_mid_price = mid;
111                        // Check to see if we need to cancel or place any new orders
112                        self.potentially_update().await;
113                    } else {
114                        error!(
115                            "could not get mid for asset {}: {all_mids:?}",
116                            self.asset.clone()
117                        );
118                    }
119                }
120                Message::User(user_events) => {
121                    // We haven't seen the first mid price event yet, so just continue
122                    if self.latest_mid_price < 0.0 {
123                        continue;
124                    }
125                    let user_events = user_events.data;
126                    if let UserData::Fills(fills) = user_events {
127                        for fill in fills {
128                            let amount: f64 = fill.sz.parse().unwrap();
129                            // Update our resting positions whenever we see a fill
130                            if fill.side.eq("B") {
131                                self.cur_position += amount;
132                                self.lower_resting.position -= amount;
133                                info!("Fill: bought {amount} {}", self.asset.clone());
134                            } else {
135                                self.cur_position -= amount;
136                                self.upper_resting.position -= amount;
137                                info!("Fill: sold {amount} {}", self.asset.clone());
138                            }
139                        }
140                    }
141                    // Check to see if we need to cancel or place any new orders
142                    self.potentially_update().await;
143                }
144                _ => {
145                    panic!("Unsupported message type");
146                }
147            }
148        }
149    }
150
151    async fn attempt_cancel(&self, asset: String, oid: u64) -> bool {
152        let cancel = self
153            .exchange_client
154            .cancel(ClientCancelRequest { asset, oid }, None)
155            .await;
156
157        match cancel {
158            Ok(cancel) => match cancel {
159                ExchangeResponseStatus::Ok(cancel) => {
160                    if let Some(cancel) = cancel.data {
161                        if !cancel.statuses.is_empty() {
162                            match cancel.statuses[0].clone() {
163                                ExchangeDataStatus::Success => {
164                                    return true;
165                                }
166                                ExchangeDataStatus::Error(e) => {
167                                    error!("Error with cancelling: {e}")
168                                }
169                                _ => unreachable!(),
170                            }
171                        } else {
172                            error!("Exchange data statuses is empty when cancelling: {cancel:?}")
173                        }
174                    } else {
175                        error!("Exchange response data is empty when cancelling: {cancel:?}")
176                    }
177                }
178                ExchangeResponseStatus::Err(e) => error!("Error with cancelling: {e}"),
179            },
180            Err(e) => error!("Error with cancelling: {e}"),
181        }
182        false
183    }
184
185    async fn place_order(
186        &self,
187        asset: String,
188        amount: f64,
189        price: f64,
190        is_buy: bool,
191    ) -> (f64, u64) {
192        let order = self
193            .exchange_client
194            .order(
195                ClientOrderRequest {
196                    asset,
197                    is_buy,
198                    reduce_only: false,
199                    limit_px: price,
200                    sz: amount,
201                    cloid: None,
202                    order_type: ClientOrder::Limit(ClientLimit {
203                        tif: "Gtc".to_string(),
204                    }),
205                },
206                None,
207            )
208            .await;
209        match order {
210            Ok(order) => match order {
211                ExchangeResponseStatus::Ok(order) => {
212                    if let Some(order) = order.data {
213                        if !order.statuses.is_empty() {
214                            match order.statuses[0].clone() {
215                                ExchangeDataStatus::Filled(order) => {
216                                    return (amount, order.oid);
217                                }
218                                ExchangeDataStatus::Resting(order) => {
219                                    return (amount, order.oid);
220                                }
221                                ExchangeDataStatus::Error(e) => {
222                                    error!("Error with placing order: {e}")
223                                }
224                                _ => unreachable!(),
225                            }
226                        } else {
227                            error!("Exchange data statuses is empty when placing order: {order:?}")
228                        }
229                    } else {
230                        error!("Exchange response data is empty when placing order: {order:?}")
231                    }
232                }
233                ExchangeResponseStatus::Err(e) => {
234                    error!("Error with placing order: {e}")
235                }
236            },
237            Err(e) => error!("Error with placing order: {e}"),
238        }
239        (0.0, 0)
240    }
241
242    async fn potentially_update(&mut self) {
243        let half_spread = (self.latest_mid_price * self.half_spread as f64) / 10000.0;
244        // Determine prices to target from the half spread
245        let (lower_price, upper_price) = (
246            self.latest_mid_price - half_spread,
247            self.latest_mid_price + half_spread,
248        );
249        let (mut lower_price, mut upper_price) = (
250            truncate_float(lower_price, self.decimals, true),
251            truncate_float(upper_price, self.decimals, false),
252        );
253
254        // Rounding optimistically to make our market tighter might cause a weird edge case, so account for that
255        if (lower_price - upper_price).abs() < EPSILON {
256            lower_price = truncate_float(lower_price, self.decimals, false);
257            upper_price = truncate_float(upper_price, self.decimals, true);
258        }
259
260        // Determine amounts we can put on the book without exceeding the max absolute position size
261        let lower_order_amount = (self.max_absolute_position_size - self.cur_position)
262            .min(self.target_liquidity)
263            .max(0.0);
264
265        let upper_order_amount = (self.max_absolute_position_size + self.cur_position)
266            .min(self.target_liquidity)
267            .max(0.0);
268
269        // Determine if we need to cancel the resting order and put a new order up due to deviation
270        let lower_change = (lower_order_amount - self.lower_resting.position).abs() > EPSILON
271            || bps_diff(lower_price, self.lower_resting.price) > self.max_bps_diff;
272        let upper_change = (upper_order_amount - self.upper_resting.position).abs() > EPSILON
273            || bps_diff(upper_price, self.upper_resting.price) > self.max_bps_diff;
274
275        // Consider cancelling
276        // TODO: Don't block on cancels
277        if self.lower_resting.oid != 0 && self.lower_resting.position > EPSILON && lower_change {
278            let cancel = self
279                .attempt_cancel(self.asset.clone(), self.lower_resting.oid)
280                .await;
281            // If we were unable to cancel, it means we got a fill, so wait until we receive that event to do anything
282            if !cancel {
283                return;
284            }
285            info!("Cancelled buy order: {:?}", self.lower_resting);
286        }
287
288        if self.upper_resting.oid != 0 && self.upper_resting.position > EPSILON && upper_change {
289            let cancel = self
290                .attempt_cancel(self.asset.clone(), self.upper_resting.oid)
291                .await;
292            if !cancel {
293                return;
294            }
295            info!("Cancelled sell order: {:?}", self.upper_resting);
296        }
297
298        // Consider putting a new order up
299        if lower_order_amount > EPSILON && lower_change {
300            let (amount_resting, oid) = self
301                .place_order(self.asset.clone(), lower_order_amount, lower_price, true)
302                .await;
303
304            self.lower_resting.oid = oid;
305            self.lower_resting.position = amount_resting;
306            self.lower_resting.price = lower_price;
307
308            if amount_resting > EPSILON {
309                info!(
310                    "Buy for {amount_resting} {} resting at {lower_price}",
311                    self.asset.clone()
312                );
313            }
314        }
315
316        if upper_order_amount > EPSILON && upper_change {
317            let (amount_resting, oid) = self
318                .place_order(self.asset.clone(), upper_order_amount, upper_price, false)
319                .await;
320            self.upper_resting.oid = oid;
321            self.upper_resting.position = amount_resting;
322            self.upper_resting.price = upper_price;
323
324            if amount_resting > EPSILON {
325                info!(
326                    "Sell for {amount_resting} {} resting at {upper_price}",
327                    self.asset.clone()
328                );
329            }
330        }
331    }
332}