df_hyperliquid_core/trade/manage/
modify.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum OrderRef {
17 Oid(u64),
18 Cloid(Cloid),
19}
20
21impl OrderRef {
22 #[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#[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 #[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 #[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#[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 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum Change {
133 Limit { price: Dec, size: Amount, tif: Tif },
135 Trigger { trigger: Dec, price: ExitPrice, size: Covers },
137}
138
139pub 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;