Skip to main content

df_hyperliquid_core/trade/manage/
modify.rs

1//! `modify` and `batchModify`: a resting order replaced in place.
2
3use crate::account::FrontendOrder;
4use crate::trade::orders::Tif;
5use crate::trade::orders::limit::limit_on;
6use crate::trade::orders::tpsl::Covers;
7use crate::trade::orders::trigger::{ExitPrice, fill_price, trigger_on};
8use crate::trade::orders::{Cloid, Order};
9use crate::trade::ticket::{Amount, Market, Refusal, at_least};
10use crate::utils::Dec;
11use crate::utils::rounding::MINIMUM_ORDER_VALUE;
12use crate::utils::sign::{L1Action, Map, Value};
13
14/// The order a modify replaces: the venue's id, or the client's.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum OrderRef {
17    Oid(u64),
18    Cloid(Cloid),
19}
20
21impl OrderRef {
22    /// An oid as a number, a cloid as its hex.
23    #[must_use]
24    pub fn wire(self) -> Value {
25        match self {
26            OrderRef::Oid(oid) => Value::Uint(oid),
27            OrderRef::Cloid(cloid) => Value::from(cloid.to_hex()),
28        }
29    }
30}
31
32/// `modify`: `{type, oid, order, a?}`, one order. `a` (always place) is written last and only
33/// when set, and set whenever the new order is a trigger: the venue refuses a trigger without it
34/// and an action hashed with `a: false`. A `batchModify` entry never carries it; the batch does.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct Modify {
37    target: OrderRef,
38    order: Order,
39    always_place: bool,
40}
41
42impl Modify {
43    #[must_use]
44    pub const fn new(target: OrderRef, order: Order) -> Modify {
45        Modify { target, order, always_place: order.is_trigger() }
46    }
47
48    /// Places the new order even where the old one is gone.
49    #[must_use]
50    pub const fn always_place(mut self) -> Modify {
51        self.always_place = true;
52        self
53    }
54
55    #[must_use]
56    pub const fn is_always_place(&self) -> bool {
57        self.always_place
58    }
59
60    #[must_use]
61    pub const fn target(&self) -> OrderRef {
62        self.target
63    }
64
65    #[must_use]
66    pub const fn order(&self) -> &Order {
67        &self.order
68    }
69
70    /// `{oid, order}`, one entry of a `batchModify`.
71    #[must_use]
72    pub fn entry(&self) -> Map {
73        Map::new().put("oid", self.target.wire()).put("order", self.order.wire())
74    }
75}
76
77impl L1Action for Modify {
78    fn action(&self) -> Map {
79        Map::new()
80            .put("type", "modify")
81            .put("oid", self.target.wire())
82            .put("order", self.order.wire())
83            .put_some("a", self.always_place.then_some(true))
84    }
85}
86
87/// `batchModify`: `{type, modifies: [{oid, order}], a?}`. `a` (always place: the new order goes on
88/// whether or not the old one was still there) is written last and only when set, and set
89/// whenever a new order is a trigger, since the venue refuses a trigger without it.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct BatchModify {
92    modifies: Vec<Modify>,
93    always_place: bool,
94}
95
96impl BatchModify {
97    #[must_use]
98    pub fn new(modifies: Vec<Modify>) -> BatchModify {
99        let always_place = modifies.iter().any(|modify| modify.order.is_trigger());
100        BatchModify { modifies, always_place }
101    }
102
103    /// Places the new orders even where the old ones are gone.
104    #[must_use]
105    pub fn always_place(mut self) -> BatchModify {
106        self.always_place = true;
107        self
108    }
109
110    #[must_use]
111    pub fn modifies(&self) -> &[Modify] {
112        &self.modifies
113    }
114
115    #[must_use]
116    pub const fn is_always_place(&self) -> bool {
117        self.always_place
118    }
119}
120
121impl L1Action for BatchModify {
122    fn action(&self) -> Map {
123        Map::new()
124            .put("type", "batchModify")
125            .put("modifies", Value::array(self.modifies.iter().map(Modify::entry)))
126            .put_some("a", self.always_place.then_some(true))
127    }
128}
129
130/// What a resting order becomes.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum Change {
133    /// A limit order's new price, size and time in force.
134    Limit { price: Dec, size: Amount, tif: Tif },
135    /// A take profit's or stop loss's new trigger, price and size.
136    Trigger { trigger: Dec, price: ExitPrice, size: Covers },
137}
138
139/// A resting order replaced in place, as `frontendOpenOrders` lists it. Its
140/// side, reduce only and client order id stay; a take profit or stop loss
141/// always places, as the venue requires of a trigger.
142pub fn modify(market: &Market, order: &FrontendOrder, change: &Change) -> Result<BatchModify, Refusal> {
143    let grid = market.grid();
144    let side = order.side;
145    let new = match *change {
146        Change::Limit { .. } if order.is_trigger => return Err(Refusal::NotALimit(order.oid)),
147        Change::Limit { tif, .. } if !matches!(tif, Tif::Alo | Tif::Gtc) => return Err(Refusal::ModifyTif(tif)),
148        Change::Limit { price, size, tif } => {
149            let price = limit_on(grid, price, side, "a new price")?;
150            let size = size.size_on(grid, price)?;
151            at_least(size, price, MINIMUM_ORDER_VALUE)?;
152            Order::limit(market.asset(), side, price, size, tif)
153        }
154        Change::Trigger { .. } if !order.is_trigger => return Err(Refusal::NotATrigger(order.oid)),
155        Change::Trigger { trigger, price, size } => {
156            let Some(tpsl) = order.tpsl() else {
157                return Err(Refusal::UnknownTrigger { oid: order.oid, kind: order.order_type.word().to_lowercase() });
158            };
159            let trigger = trigger_on(grid, trigger)?;
160            let size = match size {
161                Covers::Position if order.sz.is_zero() => Dec::ZERO,
162                Covers::Position => return Err(Refusal::NotWholePosition(order.oid)),
163                Covers::Size(amount) => {
164                    let size = amount.size_on(grid, trigger)?;
165                    at_least(size, trigger, MINIMUM_ORDER_VALUE)?;
166                    size
167                }
168            };
169            let (price, is_market) = fill_price(grid, trigger, price, side)?;
170            Order::trigger(market.asset(), side, price, size, trigger, tpsl, is_market)
171        }
172    };
173    let new = match order.reduce_only {
174        true => new.reduce_only(),
175        false => new,
176    };
177    let new = match order.cloid {
178        Some(cloid) => new.with_cloid(cloid),
179        None => new,
180    };
181    Ok(BatchModify::new(vec![Modify::new(OrderRef::Oid(order.oid), new)]))
182}
183
184#[cfg(test)]
185#[path = "tests/modify.rs"]
186mod tests;