hyperliquid_rust_sdk_abrkn/
market_maker.rs1use 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, pub half_spread: u16, pub max_bps_diff: u16, pub max_absolute_position_size: f64, pub decimals: u32, pub wallet: LocalWallet, }
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 self.info_client
87 .subscribe(
88 Subscription::UserEvents {
89 user: self.user_address,
90 },
91 sender.clone(),
92 )
93 .await
94 .unwrap();
95
96 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 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 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 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 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 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 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 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 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 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 !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 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}