nautilus_hyperliquid/common/
converters.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Order type conversion utilities for Hyperliquid adapter.
17//!
18//! This module provides conversion functions between Nautilus core order types
19//! and Hyperliquid-specific order type representations.
20
21use nautilus_model::enums::{OrderType, TimeInForce};
22use rust_decimal::Decimal;
23
24use super::enums::{
25    HyperliquidConditionalOrderType, HyperliquidOrderType, HyperliquidTimeInForce, HyperliquidTpSl,
26};
27
28/// Converts a Nautilus `OrderType` to a Hyperliquid order type configuration.
29///
30/// # Panics
31///
32/// Panics if a conditional order is specified without a trigger price.
33pub fn nautilus_order_type_to_hyperliquid(
34    order_type: OrderType,
35    time_in_force: Option<TimeInForce>,
36    trigger_price: Option<Decimal>,
37) -> HyperliquidOrderType {
38    match order_type {
39        // Regular limit order
40        OrderType::Limit => {
41            let tif = time_in_force.map_or(
42                HyperliquidTimeInForce::Gtc,
43                nautilus_time_in_force_to_hyperliquid,
44            );
45            HyperliquidOrderType::Limit { tif }
46        }
47
48        // Stop market order (stop loss)
49        OrderType::StopMarket => {
50            let trigger_px = trigger_price
51                .expect("Trigger price required for StopMarket order")
52                .to_string();
53            HyperliquidOrderType::Trigger {
54                is_market: true,
55                trigger_px,
56                tpsl: HyperliquidTpSl::Sl,
57            }
58        }
59
60        // Stop limit order (stop loss with limit)
61        OrderType::StopLimit => {
62            let trigger_px = trigger_price
63                .expect("Trigger price required for StopLimit order")
64                .to_string();
65            HyperliquidOrderType::Trigger {
66                is_market: false,
67                trigger_px,
68                tpsl: HyperliquidTpSl::Sl,
69            }
70        }
71
72        // Market if touched (take profit market)
73        OrderType::MarketIfTouched => {
74            let trigger_px = trigger_price
75                .expect("Trigger price required for MarketIfTouched order")
76                .to_string();
77            HyperliquidOrderType::Trigger {
78                is_market: true,
79                trigger_px,
80                tpsl: HyperliquidTpSl::Tp,
81            }
82        }
83
84        // Limit if touched (take profit limit)
85        OrderType::LimitIfTouched => {
86            let trigger_px = trigger_price
87                .expect("Trigger price required for LimitIfTouched order")
88                .to_string();
89            HyperliquidOrderType::Trigger {
90                is_market: false,
91                trigger_px,
92                tpsl: HyperliquidTpSl::Tp,
93            }
94        }
95
96        // Trailing stop market (requires special handling)
97        OrderType::TrailingStopMarket => {
98            let trigger_px = trigger_price
99                .expect("Trigger price required for TrailingStopMarket order")
100                .to_string();
101            HyperliquidOrderType::Trigger {
102                is_market: true,
103                trigger_px,
104                tpsl: HyperliquidTpSl::Sl,
105            }
106        }
107
108        // Trailing stop limit (requires special handling)
109        OrderType::TrailingStopLimit => {
110            let trigger_px = trigger_price
111                .expect("Trigger price required for TrailingStopLimit order")
112                .to_string();
113            HyperliquidOrderType::Trigger {
114                is_market: false,
115                trigger_px,
116                tpsl: HyperliquidTpSl::Sl,
117            }
118        }
119
120        // Market orders are handled elsewhere (not represented in HyperliquidOrderType)
121        OrderType::Market => {
122            panic!("Market orders should be handled separately via immediate execution")
123        }
124
125        // Unsupported order types
126        _ => panic!("Unsupported order type: {order_type:?}"),
127    }
128}
129
130/// Converts a Hyperliquid order type to a Nautilus `OrderType`.
131pub fn hyperliquid_order_type_to_nautilus(hl_order_type: &HyperliquidOrderType) -> OrderType {
132    match hl_order_type {
133        HyperliquidOrderType::Limit { .. } => OrderType::Limit,
134        HyperliquidOrderType::Trigger {
135            is_market, tpsl, ..
136        } => match (is_market, tpsl) {
137            (true, HyperliquidTpSl::Sl) => OrderType::StopMarket,
138            (false, HyperliquidTpSl::Sl) => OrderType::StopLimit,
139            (true, HyperliquidTpSl::Tp) => OrderType::MarketIfTouched,
140            (false, HyperliquidTpSl::Tp) => OrderType::LimitIfTouched,
141        },
142    }
143}
144
145/// Converts a Hyperliquid conditional order type to a Nautilus `OrderType`.
146pub fn hyperliquid_conditional_to_nautilus(
147    conditional_type: HyperliquidConditionalOrderType,
148) -> OrderType {
149    OrderType::from(conditional_type)
150}
151
152/// Converts a Nautilus `OrderType` to a Hyperliquid conditional order type.
153///
154/// # Panics
155///
156/// Panics if the order type is not a conditional order type.
157pub fn nautilus_to_hyperliquid_conditional(
158    order_type: OrderType,
159) -> HyperliquidConditionalOrderType {
160    HyperliquidConditionalOrderType::from(order_type)
161}
162
163/// Converts a Nautilus `TimeInForce` to a Hyperliquid time in force.
164pub fn nautilus_time_in_force_to_hyperliquid(tif: TimeInForce) -> HyperliquidTimeInForce {
165    match tif {
166        TimeInForce::Gtc => HyperliquidTimeInForce::Gtc,
167        TimeInForce::Ioc => HyperliquidTimeInForce::Ioc,
168        TimeInForce::Fok => HyperliquidTimeInForce::Ioc, // FOK maps to IOC in Hyperliquid
169        TimeInForce::Gtd => HyperliquidTimeInForce::Gtc, // GTD maps to GTC
170        TimeInForce::Day => HyperliquidTimeInForce::Gtc, // DAY maps to GTC
171        TimeInForce::AtTheOpen => HyperliquidTimeInForce::Gtc, // ATO maps to GTC
172        TimeInForce::AtTheClose => HyperliquidTimeInForce::Gtc, // ATC maps to GTC
173    }
174}
175
176/// Converts a Hyperliquid time in force to a Nautilus `TimeInForce`.
177pub fn hyperliquid_time_in_force_to_nautilus(hl_tif: HyperliquidTimeInForce) -> TimeInForce {
178    match hl_tif {
179        HyperliquidTimeInForce::Gtc => TimeInForce::Gtc,
180        HyperliquidTimeInForce::Ioc => TimeInForce::Ioc,
181        HyperliquidTimeInForce::Alo => TimeInForce::Gtc, // ALO (post-only) maps to GTC
182    }
183}
184
185/// Determines the TP/SL type based on order type and side.
186///
187/// # Logic
188///
189/// For buy orders:
190/// - Stop orders (trigger below current price) -> Stop Loss
191/// - Take profit orders (trigger above current price) -> Take Profit
192///
193/// For sell orders:
194/// - Stop orders (trigger above current price) -> Stop Loss
195/// - Take profit orders (trigger below current price) -> Take Profit
196pub fn determine_tpsl_type(order_type: OrderType, is_buy: bool) -> HyperliquidTpSl {
197    match order_type {
198        OrderType::StopMarket
199        | OrderType::StopLimit
200        | OrderType::TrailingStopMarket
201        | OrderType::TrailingStopLimit => HyperliquidTpSl::Sl,
202        OrderType::MarketIfTouched | OrderType::LimitIfTouched => HyperliquidTpSl::Tp,
203        _ => {
204            // Default logic based on side if order type is ambiguous
205            if is_buy {
206                HyperliquidTpSl::Sl
207            } else {
208                HyperliquidTpSl::Tp
209            }
210        }
211    }
212}
213
214////////////////////////////////////////////////////////////////////////////////
215// Tests
216////////////////////////////////////////////////////////////////////////////////
217
218#[cfg(test)]
219mod tests {
220    use rstest::rstest;
221
222    use super::*;
223
224    #[rstest]
225    fn test_nautilus_to_hyperliquid_limit_order() {
226        let result =
227            nautilus_order_type_to_hyperliquid(OrderType::Limit, Some(TimeInForce::Gtc), None);
228
229        match result {
230            HyperliquidOrderType::Limit { tif } => {
231                assert_eq!(tif, HyperliquidTimeInForce::Gtc);
232            }
233            _ => panic!("Expected Limit order type"),
234        }
235    }
236
237    #[rstest]
238    fn test_nautilus_to_hyperliquid_stop_market() {
239        let result = nautilus_order_type_to_hyperliquid(
240            OrderType::StopMarket,
241            None,
242            Some(Decimal::new(49000, 0)),
243        );
244
245        match result {
246            HyperliquidOrderType::Trigger {
247                is_market,
248                trigger_px,
249                tpsl,
250            } => {
251                assert!(is_market);
252                assert_eq!(trigger_px, "49000");
253                assert_eq!(tpsl, HyperliquidTpSl::Sl);
254            }
255            _ => panic!("Expected Trigger order type"),
256        }
257    }
258
259    #[rstest]
260    fn test_nautilus_to_hyperliquid_stop_limit() {
261        let result = nautilus_order_type_to_hyperliquid(
262            OrderType::StopLimit,
263            None,
264            Some(Decimal::new(49000, 0)),
265        );
266
267        match result {
268            HyperliquidOrderType::Trigger {
269                is_market,
270                trigger_px,
271                tpsl,
272            } => {
273                assert!(!is_market);
274                assert_eq!(trigger_px, "49000");
275                assert_eq!(tpsl, HyperliquidTpSl::Sl);
276            }
277            _ => panic!("Expected Trigger order type"),
278        }
279    }
280
281    #[rstest]
282    fn test_nautilus_to_hyperliquid_take_profit_market() {
283        let result = nautilus_order_type_to_hyperliquid(
284            OrderType::MarketIfTouched,
285            None,
286            Some(Decimal::new(51000, 0)),
287        );
288
289        match result {
290            HyperliquidOrderType::Trigger {
291                is_market,
292                trigger_px,
293                tpsl,
294            } => {
295                assert!(is_market);
296                assert_eq!(trigger_px, "51000");
297                assert_eq!(tpsl, HyperliquidTpSl::Tp);
298            }
299            _ => panic!("Expected Trigger order type"),
300        }
301    }
302
303    #[rstest]
304    fn test_nautilus_to_hyperliquid_take_profit_limit() {
305        let result = nautilus_order_type_to_hyperliquid(
306            OrderType::LimitIfTouched,
307            None,
308            Some(Decimal::new(51000, 0)),
309        );
310
311        match result {
312            HyperliquidOrderType::Trigger {
313                is_market,
314                trigger_px,
315                tpsl,
316            } => {
317                assert!(!is_market);
318                assert_eq!(trigger_px, "51000");
319                assert_eq!(tpsl, HyperliquidTpSl::Tp);
320            }
321            _ => panic!("Expected Trigger order type"),
322        }
323    }
324
325    #[rstest]
326    fn test_hyperliquid_to_nautilus_limit() {
327        let hl_order = HyperliquidOrderType::Limit {
328            tif: HyperliquidTimeInForce::Gtc,
329        };
330        assert_eq!(
331            hyperliquid_order_type_to_nautilus(&hl_order),
332            OrderType::Limit
333        );
334    }
335
336    #[rstest]
337    fn test_hyperliquid_to_nautilus_stop_market() {
338        let hl_order = HyperliquidOrderType::Trigger {
339            is_market: true,
340            trigger_px: "49000".to_string(),
341            tpsl: HyperliquidTpSl::Sl,
342        };
343        assert_eq!(
344            hyperliquid_order_type_to_nautilus(&hl_order),
345            OrderType::StopMarket
346        );
347    }
348
349    #[rstest]
350    fn test_hyperliquid_to_nautilus_stop_limit() {
351        let hl_order = HyperliquidOrderType::Trigger {
352            is_market: false,
353            trigger_px: "49000".to_string(),
354            tpsl: HyperliquidTpSl::Sl,
355        };
356        assert_eq!(
357            hyperliquid_order_type_to_nautilus(&hl_order),
358            OrderType::StopLimit
359        );
360    }
361
362    #[rstest]
363    fn test_hyperliquid_to_nautilus_take_profit_market() {
364        let hl_order = HyperliquidOrderType::Trigger {
365            is_market: true,
366            trigger_px: "51000".to_string(),
367            tpsl: HyperliquidTpSl::Tp,
368        };
369        assert_eq!(
370            hyperliquid_order_type_to_nautilus(&hl_order),
371            OrderType::MarketIfTouched
372        );
373    }
374
375    #[rstest]
376    fn test_hyperliquid_to_nautilus_take_profit_limit() {
377        let hl_order = HyperliquidOrderType::Trigger {
378            is_market: false,
379            trigger_px: "51000".to_string(),
380            tpsl: HyperliquidTpSl::Tp,
381        };
382        assert_eq!(
383            hyperliquid_order_type_to_nautilus(&hl_order),
384            OrderType::LimitIfTouched
385        );
386    }
387
388    #[rstest]
389    fn test_time_in_force_conversions() {
390        // Test Nautilus to Hyperliquid
391        assert_eq!(
392            nautilus_time_in_force_to_hyperliquid(TimeInForce::Gtc),
393            HyperliquidTimeInForce::Gtc
394        );
395        assert_eq!(
396            nautilus_time_in_force_to_hyperliquid(TimeInForce::Ioc),
397            HyperliquidTimeInForce::Ioc
398        );
399        assert_eq!(
400            nautilus_time_in_force_to_hyperliquid(TimeInForce::Fok),
401            HyperliquidTimeInForce::Ioc
402        );
403
404        // Test Hyperliquid to Nautilus
405        assert_eq!(
406            hyperliquid_time_in_force_to_nautilus(HyperliquidTimeInForce::Gtc),
407            TimeInForce::Gtc
408        );
409        assert_eq!(
410            hyperliquid_time_in_force_to_nautilus(HyperliquidTimeInForce::Ioc),
411            TimeInForce::Ioc
412        );
413        assert_eq!(
414            hyperliquid_time_in_force_to_nautilus(HyperliquidTimeInForce::Alo),
415            TimeInForce::Gtc
416        );
417    }
418
419    #[rstest]
420    fn test_conditional_order_type_conversions() {
421        // Test Hyperliquid conditional to Nautilus
422        assert_eq!(
423            hyperliquid_conditional_to_nautilus(HyperliquidConditionalOrderType::StopMarket),
424            OrderType::StopMarket
425        );
426        assert_eq!(
427            hyperliquid_conditional_to_nautilus(HyperliquidConditionalOrderType::StopLimit),
428            OrderType::StopLimit
429        );
430        assert_eq!(
431            hyperliquid_conditional_to_nautilus(HyperliquidConditionalOrderType::TakeProfitMarket),
432            OrderType::MarketIfTouched
433        );
434        assert_eq!(
435            hyperliquid_conditional_to_nautilus(HyperliquidConditionalOrderType::TakeProfitLimit),
436            OrderType::LimitIfTouched
437        );
438
439        // Test Nautilus to Hyperliquid conditional
440        assert_eq!(
441            nautilus_to_hyperliquid_conditional(OrderType::StopMarket),
442            HyperliquidConditionalOrderType::StopMarket
443        );
444        assert_eq!(
445            nautilus_to_hyperliquid_conditional(OrderType::StopLimit),
446            HyperliquidConditionalOrderType::StopLimit
447        );
448        assert_eq!(
449            nautilus_to_hyperliquid_conditional(OrderType::MarketIfTouched),
450            HyperliquidConditionalOrderType::TakeProfitMarket
451        );
452        assert_eq!(
453            nautilus_to_hyperliquid_conditional(OrderType::LimitIfTouched),
454            HyperliquidConditionalOrderType::TakeProfitLimit
455        );
456    }
457
458    #[rstest]
459    fn test_determine_tpsl_type() {
460        // Stop orders should always be SL
461        assert_eq!(
462            determine_tpsl_type(OrderType::StopMarket, true),
463            HyperliquidTpSl::Sl
464        );
465        assert_eq!(
466            determine_tpsl_type(OrderType::StopLimit, false),
467            HyperliquidTpSl::Sl
468        );
469
470        // Take profit orders should always be TP
471        assert_eq!(
472            determine_tpsl_type(OrderType::MarketIfTouched, true),
473            HyperliquidTpSl::Tp
474        );
475        assert_eq!(
476            determine_tpsl_type(OrderType::LimitIfTouched, false),
477            HyperliquidTpSl::Tp
478        );
479
480        // Trailing stops should be SL
481        assert_eq!(
482            determine_tpsl_type(OrderType::TrailingStopMarket, true),
483            HyperliquidTpSl::Sl
484        );
485        assert_eq!(
486            determine_tpsl_type(OrderType::TrailingStopLimit, false),
487            HyperliquidTpSl::Sl
488        );
489    }
490}