Thanks to visit codestin.com
Credit goes to docs.rs

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