Skip to main content

clob_client_rust/
order_builder.rs

1use crate::errors::ClobError;
2use crate::exchange_order_builder::ExchangeOrderBuilder;
3use crate::signing::Eip712Signer;
4use crate::types::{OrderData, Side, SignatureType, SignedOrder};
5use crate::utilities::{decimal_places, round_down, round_normal, round_up};
6use rust_decimal::Decimal;
7use rust_decimal::prelude::*;
8
9#[derive(Debug, Clone)]
10pub struct RoundConfig {
11    pub price: u32,
12    pub size: u32,
13    pub amount: u32,
14}
15
16use std::collections::HashMap;
17
18pub fn rounding_config() -> HashMap<&'static str, RoundConfig> {
19    let mut m = HashMap::new();
20    m.insert(
21        "0.1",
22        RoundConfig {
23            price: 1,
24            size: 2,
25            amount: 3,
26        },
27    );
28    m.insert(
29        "0.01",
30        RoundConfig {
31            price: 2,
32            size: 2,
33            amount: 4,
34        },
35    );
36    m.insert(
37        "0.001",
38        RoundConfig {
39            price: 3,
40            size: 2,
41            amount: 5,
42        },
43    );
44    m.insert(
45        "0.0001",
46        RoundConfig {
47            price: 4,
48            size: 2,
49            amount: 6,
50        },
51    );
52    m
53}
54
55pub struct RawAmounts {
56    pub side: Side,
57    pub raw_maker_amt: Decimal,
58    pub raw_taker_amt: Decimal,
59}
60
61pub fn get_order_raw_amounts(
62    side: Side,
63    size: Decimal,
64    price: Decimal,
65    round_config: &RoundConfig,
66) -> RawAmounts {
67    let raw_price = round_normal(price, round_config.price);
68
69    if let Side::BUY = side {
70        let raw_taker_amt = round_down(size, round_config.size);
71        let mut raw_maker_amt = raw_taker_amt * raw_price;
72        if decimal_places(raw_maker_amt) > round_config.amount {
73            raw_maker_amt = round_up(raw_maker_amt, round_config.amount + 4);
74            if decimal_places(raw_maker_amt) > round_config.amount {
75                raw_maker_amt = round_down(raw_maker_amt, round_config.amount);
76            }
77        }
78        RawAmounts {
79            side: Side::BUY,
80            raw_maker_amt,
81            raw_taker_amt,
82        }
83    } else {
84        let raw_maker_amt = round_down(size, round_config.size);
85        let mut raw_taker_amt = raw_maker_amt * raw_price;
86        if decimal_places(raw_taker_amt) > round_config.amount {
87            raw_taker_amt = round_up(raw_taker_amt, round_config.amount + 4);
88            if decimal_places(raw_taker_amt) > round_config.amount {
89                raw_taker_amt = round_down(raw_taker_amt, round_config.amount);
90            }
91        }
92        RawAmounts {
93            side: Side::SELL,
94            raw_maker_amt,
95            raw_taker_amt,
96        }
97    }
98}
99
100pub(crate) fn parse_units(value: &str, decimals: u32) -> Result<String, ClobError> {
101    let d = value
102        .parse::<Decimal>()
103        .map_err(|e| ClobError::Other(format!("parse decimal error: {}", e)))?;
104    let factor = Decimal::new(10i64.pow(decimals), 0);
105    let scaled = d * factor;
106    let scaled_i = scaled.trunc();
107    match scaled_i.to_i128() {
108        Some(i) => Ok(i.to_string()),
109        None => Err(ClobError::Other("scaled value out of range".to_string())),
110    }
111}
112
113pub async fn build_order_creation_args(
114    signer: &str,
115    maker: &str,
116    signature_type: SignatureType,
117    user_order: &crate::types::UserOrder,
118    round_config: &RoundConfig,
119) -> Result<OrderData, ClobError> {
120    let ra = get_order_raw_amounts(
121        user_order.side,
122        user_order.size,
123        user_order.price,
124        round_config,
125    );
126
127    let maker_amount = parse_units(&ra.raw_maker_amt.to_string(), 6)?;
128    let taker_amount = parse_units(&ra.raw_taker_amt.to_string(), 6)?;
129
130    let taker = if let Some(t) = &user_order.taker {
131        if !t.is_empty() {
132            t.clone()
133        } else {
134            "0x0000000000000000000000000000000000000000".to_string()
135        }
136    } else {
137        "0x0000000000000000000000000000000000000000".to_string()
138    };
139
140    let fee_rate_bps = user_order.fee_rate_bps.to_string();
141    let nonce = user_order
142        .nonce
143        .map(|v| v.to_string())
144        .unwrap_or_else(|| "0".to_string());
145
146    Ok(OrderData {
147        maker: maker.to_string(),
148        taker,
149        token_id: user_order.token_id.clone(),
150        maker_amount,
151        taker_amount,
152        side: ra.side,
153        fee_rate_bps,
154        nonce,
155        signer: signer.to_string(),
156        expiration: user_order
157            .expiration
158            .map(|v| v.to_string())
159            .unwrap_or_else(|| "0".to_string()),
160        signature_type,
161    })
162}
163
164pub async fn build_order(
165    signer: &impl crate::signing::Eip712Signer,
166    exchange_address: &str,
167    chain_id: i32,
168    order_data: OrderData,
169) -> Result<SignedOrder, ClobError> {
170    let builder = ExchangeOrderBuilder::new(exchange_address, chain_id as i64, signer);
171    builder.build_signed_order(order_data).await
172}
173
174pub fn get_market_order_raw_amounts(
175    side: Side,
176    amount: Decimal,
177    price: Decimal,
178    round_config: &RoundConfig,
179) -> RawAmounts {
180    let raw_price = round_down(price, round_config.price);
181
182    if let Side::BUY = side {
183        let raw_maker_amt = round_down(amount, round_config.size);
184        let mut raw_taker_amt = raw_maker_amt / raw_price;
185        if decimal_places(raw_taker_amt) > round_config.amount {
186            raw_taker_amt = round_up(raw_taker_amt, round_config.amount + 4);
187            if decimal_places(raw_taker_amt) > round_config.amount {
188                raw_taker_amt = round_down(raw_taker_amt, round_config.amount);
189            }
190        }
191        RawAmounts {
192            side: Side::BUY,
193            raw_maker_amt,
194            raw_taker_amt,
195        }
196    } else {
197        let raw_maker_amt = round_down(amount, round_config.size);
198        let mut raw_taker_amt = raw_maker_amt * raw_price;
199        if decimal_places(raw_taker_amt) > round_config.amount {
200            raw_taker_amt = round_up(raw_taker_amt, round_config.amount + 4);
201            if decimal_places(raw_taker_amt) > round_config.amount {
202                raw_taker_amt = round_down(raw_taker_amt, round_config.amount);
203            }
204        }
205        RawAmounts {
206            side: Side::SELL,
207            raw_maker_amt,
208            raw_taker_amt,
209        }
210    }
211}
212
213pub struct OrderBuilder<'a, S: Eip712Signer> {
214    signer: &'a S,
215    chain_id: i64,
216    signature_type: SignatureType,
217    funder_address: Option<String>,
218}
219
220#[derive(Debug, Clone)]
221pub struct BuilderConfig {
222    pub tick_size: Option<String>,
223    pub neg_risk: Option<bool>,
224    pub signature_type: SignatureType,
225    pub funder_address: Option<String>,
226}
227
228impl Default for BuilderConfig {
229    fn default() -> Self {
230        Self {
231            tick_size: None,
232            neg_risk: None,
233            signature_type: SignatureType::EOA,
234            funder_address: None,
235        }
236    }
237}
238
239impl<'a, S: Eip712Signer> OrderBuilder<'a, S> {
240    pub fn new(
241        signer: &'a S,
242        chain_id: i64,
243        signature_type: Option<SignatureType>,
244        funder_address: Option<String>,
245    ) -> Self {
246        Self {
247            signer,
248            chain_id,
249            signature_type: signature_type.unwrap_or(SignatureType::EOA),
250            funder_address,
251        }
252    }
253
254    pub fn with_config(signer: &'a S, chain_id: i64, cfg: &BuilderConfig) -> Self {
255        Self {
256            signer,
257            chain_id,
258            signature_type: cfg.signature_type,
259            funder_address: cfg.funder_address.clone(),
260        }
261    }
262
263    pub async fn build_order(
264        &self,
265        exchange_address: &str,
266        user_order: &crate::types::UserOrder,
267        options_tick: &str,
268    ) -> Result<crate::types::SignedOrder, ClobError> {
269        let rc_map = rounding_config();
270        let round_config = rc_map
271            .get(options_tick)
272            .ok_or(ClobError::Other("invalid tick size".to_string()))?;
273        let eoa_addr = self.signer.get_address().await?;
274        let maker = self.funder_address.clone().unwrap_or(eoa_addr.clone());
275
276        let order_data = build_order_creation_args(
277            &eoa_addr,
278            &maker,
279            self.signature_type,
280            user_order,
281            round_config,
282        )
283        .await?;
284
285        let builder = ExchangeOrderBuilder::new(exchange_address, self.chain_id, self.signer);
286        builder.build_signed_order(order_data).await
287    }
288
289    /// 构建限价单(强制指定 salt,用于跨语言/跨路径签名完全一致的验证)
290    pub async fn build_order_with_salt(
291        &self,
292        exchange_address: &str,
293        user_order: &crate::types::UserOrder,
294        options_tick: &str,
295        forced_salt: &str,
296    ) -> Result<crate::types::SignedOrder, ClobError> {
297        let rc_map = rounding_config();
298        let round_config = rc_map
299            .get(options_tick)
300            .ok_or(ClobError::Other("invalid tick size".to_string()))?;
301        let eoa_addr = self.signer.get_address().await?;
302        let maker = self.funder_address.clone().unwrap_or(eoa_addr.clone());
303
304        let order_data = build_order_creation_args(
305            &eoa_addr,
306            &maker,
307            self.signature_type,
308            user_order,
309            round_config,
310        )
311        .await?;
312
313        let builder = ExchangeOrderBuilder::new(exchange_address, self.chain_id, self.signer);
314        builder
315            .build_signed_order_with_salt(order_data, forced_salt)
316            .await
317    }
318
319    pub async fn build_market_order(
320        &self,
321        exchange_address: &str,
322        user_market_order: &crate::types::UserMarketOrder,
323        options_tick: &str,
324    ) -> Result<crate::types::SignedOrder, ClobError> {
325        let rc_map = rounding_config();
326        let round_config = rc_map
327            .get(options_tick)
328            .ok_or(ClobError::Other("invalid tick size".to_string()))?;
329        let eoa_addr = self.signer.get_address().await?;
330        let maker = self.funder_address.clone().unwrap_or(eoa_addr.clone());
331
332        let order_data = build_market_order_creation_args(
333            &eoa_addr,
334            &maker,
335            self.signature_type,
336            user_market_order,
337            round_config,
338        )
339        .await?;
340
341        let builder = ExchangeOrderBuilder::new(exchange_address, self.chain_id, self.signer);
342        builder.build_signed_order(order_data).await
343    }
344}
345
346pub async fn build_market_order_creation_args(
347    signer: &str,
348    maker: &str,
349    signature_type: SignatureType,
350    user_market_order: &crate::types::UserMarketOrder,
351    round_config: &RoundConfig,
352) -> Result<OrderData, ClobError> {
353    let price = user_market_order.price;
354    let ra = get_market_order_raw_amounts(
355        user_market_order.side,
356        user_market_order.amount,
357        price,
358        round_config,
359    );
360
361    let maker_amount = parse_units(&ra.raw_maker_amt.to_string(), 6)?;
362    let taker_amount = parse_units(&ra.raw_taker_amt.to_string(), 6)?;
363
364    let taker = if let Some(t) = &user_market_order.taker {
365        if !t.is_empty() {
366            t.clone()
367        } else {
368            "0x0000000000000000000000000000000000000000".to_string()
369        }
370    } else {
371        "0x0000000000000000000000000000000000000000".to_string()
372    };
373
374    let fee_rate_bps = user_market_order.fee_rate_bps.to_string();
375    let nonce = user_market_order
376        .nonce
377        .map(|v| v.to_string())
378        .unwrap_or_else(|| "0".to_string());
379
380    Ok(OrderData {
381        maker: maker.to_string(),
382        taker,
383        token_id: user_market_order.token_id.clone(),
384        maker_amount,
385        taker_amount,
386        side: ra.side,
387        fee_rate_bps,
388        nonce,
389        signer: signer.to_string(),
390        expiration: "0".to_string(),
391        signature_type,
392    })
393}
394
395pub fn calculate_buy_market_price(
396    positions: &[crate::types::OrderSummary],
397    amount_to_match: Decimal,
398    order_type: crate::types::OrderType,
399) -> Result<Decimal, ClobError> {
400    if positions.is_empty() {
401        return Err(ClobError::Other("no match".to_string()));
402    }
403    let mut sum = Decimal::ZERO;
404    for i in (0..positions.len()).rev() {
405        let p = &positions[i];
406        let price = p.price.parse::<Decimal>().unwrap_or(Decimal::ZERO);
407        let size = p.size.parse::<Decimal>().unwrap_or(Decimal::ZERO);
408        sum += size * price;
409        if sum >= amount_to_match {
410            return Ok(price);
411        }
412    }
413    if let crate::types::OrderType::FOK = order_type {
414        return Err(ClobError::Other("no match".to_string()));
415    }
416    Ok(positions[0]
417        .price
418        .parse::<Decimal>()
419        .unwrap_or(Decimal::ZERO))
420}
421
422pub fn calculate_sell_market_price(
423    positions: &[crate::types::OrderSummary],
424    amount_to_match: Decimal,
425    order_type: crate::types::OrderType,
426) -> Result<Decimal, ClobError> {
427    if positions.is_empty() {
428        return Err(ClobError::Other("no match".to_string()));
429    }
430    let mut sum = Decimal::ZERO;
431    for i in (0..positions.len()).rev() {
432        let p = &positions[i];
433        let size = p.size.parse::<Decimal>().unwrap_or(Decimal::ZERO);
434        sum += size;
435        if sum >= amount_to_match {
436            let price = p.price.parse::<Decimal>().unwrap_or(Decimal::ZERO);
437            return Ok(price);
438        }
439    }
440    if let crate::types::OrderType::FOK = order_type {
441        return Err(ClobError::Other("no match".to_string()));
442    }
443    Ok(positions[0]
444        .price
445        .parse::<Decimal>()
446        .unwrap_or(Decimal::ZERO))
447}
448
449/// 基于外部订单簿快照计算市价单价格(纯函数,无 HTTP)
450pub fn compute_market_price_from_book(
451    book: &crate::types::OrderBookSummary,
452    side: crate::types::Side,
453    amount: Decimal,
454    order_type: crate::types::OrderType,
455) -> Result<Decimal, ClobError> {
456    match side {
457        crate::types::Side::BUY => {
458            if book.asks.is_empty() {
459                return Err(ClobError::Other("no match".to_string()));
460            }
461            calculate_buy_market_price(&book.asks, amount, order_type)
462        }
463        crate::types::Side::SELL => {
464            if book.bids.is_empty() {
465                return Err(ClobError::Other("no match".to_string()));
466            }
467            calculate_sell_market_price(&book.bids, amount, order_type)
468        }
469    }
470}