Skip to main content

df_hyperliquid_core/trade/orders/
scale.rs

1//! Scale orders: resting limits evenly spaced between two prices, the size split evenly between
2//! them, in one action.
3
4use crate::trade::builder::BuilderSetting;
5use crate::trade::error::TradeError;
6use crate::trade::orders::limit::limit_on;
7use crate::trade::orders::{Order, PlaceOrder, Side, Tif};
8use crate::trade::ticket::{Market, Refusal, at_least, positive, routed};
9use crate::utils::rounding::MINIMUM_ORDER_VALUE;
10use crate::utils::{Dec, Round};
11
12/// The fewest levels a scale has.
13pub const LEAST_LEVELS: u32 = 2;
14
15/// The most levels a scale has.
16pub const MOST_LEVELS: u32 = 50;
17
18/// A scale: `levels` resting orders evenly spaced from `from` to `to`, the
19/// size split evenly between them.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct Scale {
22    pub side: Side,
23    /// The total, in the base.
24    pub size: Dec,
25    pub from: Dec,
26    pub to: Dec,
27    pub levels: u32,
28    pub tif: Tif,
29    pub reduce_only: bool,
30}
31
32impl Scale {
33    #[must_use]
34    pub const fn new(side: Side, size: Dec, from: Dec, to: Dec, levels: u32, tif: Tif) -> Scale {
35        Scale { side, size, from, to, levels, tif, reduce_only: false }
36    }
37}
38
39/// A scale: one `order` with an order a level. The last level is `to` itself.
40pub fn scale(market: &Market, intent: &Scale, builder: Option<&BuilderSetting>) -> Result<PlaceOrder, Refusal> {
41    if market.is_spot() && intent.reduce_only {
42        return Err(Refusal::Spot("reduce only"));
43    }
44    if !matches!(intent.tif, Tif::Gtc | Tif::Alo) {
45        return Err(Refusal::ScaleTif(intent.tif));
46    }
47    let levels = intent.levels;
48    if !(LEAST_LEVELS..=MOST_LEVELS).contains(&levels) {
49        return Err(Refusal::Levels(levels));
50    }
51    let grid = market.grid();
52    let total = positive(intent.size, "a size")?;
53    let from = positive(intent.from, "a from price")?;
54    let to = positive(intent.to, "a to price")?;
55    let count = Dec::from_int(i64::from(levels));
56    let raw = total.checked_div(count, 12, Round::Down).ok_or(Refusal::Trade(TradeError::NoSize(total)))?;
57    let each = grid.size(raw).ok_or(Refusal::Trade(TradeError::NoSize(raw)))?;
58    let span = to.checked_sub(from).ok_or(Refusal::Trade(TradeError::NoPrice(to)))?;
59    let step = span
60        .checked_div(Dec::from_int(i64::from(levels - 1)), 12, Round::Down)
61        .ok_or(Refusal::Trade(TradeError::NoPrice(to)))?;
62    let mut orders = Vec::new();
63    for at in 0..levels {
64        let raw = match at + 1 == levels {
65            true => to,
66            false => step
67                .checked_mul(Dec::from_int(i64::from(at)))
68                .and_then(|moved| from.checked_add(moved))
69                .ok_or(Refusal::Trade(TradeError::NoPrice(from)))?,
70        };
71        let price = limit_on(grid, raw, intent.side, "a level's price")?;
72        at_least(each, price, MINIMUM_ORDER_VALUE)?;
73        let order = Order::limit(market.asset(), intent.side, price, each, intent.tif);
74        orders.push(match intent.reduce_only {
75            true => order.reduce_only(),
76            false => order,
77        });
78    }
79    routed(PlaceOrder::new(orders), builder)
80}
81
82#[cfg(test)]
83#[path = "tests/scale.rs"]
84mod tests;