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