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#[cfg(test)]
215mod tests {
216    use rstest::rstest;
217
218    use super::*;
219
220    #[rstest]
221    fn test_nautilus_to_hyperliquid_limit_order() {
222        let result =
223            nautilus_order_type_to_hyperliquid(OrderType::Limit, Some(TimeInForce::Gtc), None);
224
225        match result {
226            HyperliquidOrderType::Limit { tif } => {
227                assert_eq!(tif, HyperliquidTimeInForce::Gtc);
228            }
229            _ => panic!("Expected Limit order type"),
230        }
231    }
232
233    #[rstest]
234    fn test_nautilus_to_hyperliquid_stop_market() {
235        let result = nautilus_order_type_to_hyperliquid(
236            OrderType::StopMarket,
237            None,
238            Some(Decimal::new(49000, 0)),
239        );
240
241        match result {
242            HyperliquidOrderType::Trigger {
243                is_market,
244                trigger_px,
245                tpsl,
246            } => {
247                assert!(is_market);
248                assert_eq!(trigger_px, "49000");
249                assert_eq!(tpsl, HyperliquidTpSl::Sl);
250            }
251            _ => panic!("Expected Trigger order type"),
252        }
253    }
254
255    #[rstest]
256    fn test_nautilus_to_hyperliquid_stop_limit() {
257        let result = nautilus_order_type_to_hyperliquid(
258            OrderType::StopLimit,
259            None,
260            Some(Decimal::new(49000, 0)),
261        );
262
263        match result {
264            HyperliquidOrderType::Trigger {
265                is_market,
266                trigger_px,
267                tpsl,
268            } => {
269                assert!(!is_market);
270                assert_eq!(trigger_px, "49000");
271                assert_eq!(tpsl, HyperliquidTpSl::Sl);
272            }
273            _ => panic!("Expected Trigger order type"),
274        }
275    }
276
277    #[rstest]
278    fn test_nautilus_to_hyperliquid_take_profit_market() {
279        let result = nautilus_order_type_to_hyperliquid(
280            OrderType::MarketIfTouched,
281            None,
282            Some(Decimal::new(51000, 0)),
283        );
284
285        match result {
286            HyperliquidOrderType::Trigger {
287                is_market,
288                trigger_px,
289                tpsl,
290            } => {
291                assert!(is_market);
292                assert_eq!(trigger_px, "51000");
293                assert_eq!(tpsl, HyperliquidTpSl::Tp);
294            }
295            _ => panic!("Expected Trigger order type"),
296        }
297    }
298
299    #[rstest]
300    fn test_nautilus_to_hyperliquid_take_profit_limit() {
301        let result = nautilus_order_type_to_hyperliquid(
302            OrderType::LimitIfTouched,
303            None,
304            Some(Decimal::new(51000, 0)),
305        );
306
307        match result {
308            HyperliquidOrderType::Trigger {
309                is_market,
310                trigger_px,
311                tpsl,
312            } => {
313                assert!(!is_market);
314                assert_eq!(trigger_px, "51000");
315                assert_eq!(tpsl, HyperliquidTpSl::Tp);
316            }
317            _ => panic!("Expected Trigger order type"),
318        }
319    }
320
321    #[rstest]
322    fn test_hyperliquid_to_nautilus_limit() {
323        let hl_order = HyperliquidOrderType::Limit {
324            tif: HyperliquidTimeInForce::Gtc,
325        };
326        assert_eq!(
327            hyperliquid_order_type_to_nautilus(&hl_order),
328            OrderType::Limit
329        );
330    }
331
332    #[rstest]
333    fn test_hyperliquid_to_nautilus_stop_market() {
334        let hl_order = HyperliquidOrderType::Trigger {
335            is_market: true,
336            trigger_px: "49000".to_string(),
337            tpsl: HyperliquidTpSl::Sl,
338        };
339        assert_eq!(
340            hyperliquid_order_type_to_nautilus(&hl_order),
341            OrderType::StopMarket
342        );
343    }
344
345    #[rstest]
346    fn test_hyperliquid_to_nautilus_stop_limit() {
347        let hl_order = HyperliquidOrderType::Trigger {
348            is_market: false,
349            trigger_px: "49000".to_string(),
350            tpsl: HyperliquidTpSl::Sl,
351        };
352        assert_eq!(
353            hyperliquid_order_type_to_nautilus(&hl_order),
354            OrderType::StopLimit
355        );
356    }
357
358    #[rstest]
359    fn test_hyperliquid_to_nautilus_take_profit_market() {
360        let hl_order = HyperliquidOrderType::Trigger {
361            is_market: true,
362            trigger_px: "51000".to_string(),
363            tpsl: HyperliquidTpSl::Tp,
364        };
365        assert_eq!(
366            hyperliquid_order_type_to_nautilus(&hl_order),
367            OrderType::MarketIfTouched
368        );
369    }
370
371    #[rstest]
372    fn test_hyperliquid_to_nautilus_take_profit_limit() {
373        let hl_order = HyperliquidOrderType::Trigger {
374            is_market: false,
375            trigger_px: "51000".to_string(),
376            tpsl: HyperliquidTpSl::Tp,
377        };
378        assert_eq!(
379            hyperliquid_order_type_to_nautilus(&hl_order),
380            OrderType::LimitIfTouched
381        );
382    }
383
384    #[rstest]
385    fn test_time_in_force_conversions() {
386        // Test Nautilus to Hyperliquid
387        assert_eq!(
388            nautilus_time_in_force_to_hyperliquid(TimeInForce::Gtc),
389            HyperliquidTimeInForce::Gtc
390        );
391        assert_eq!(
392            nautilus_time_in_force_to_hyperliquid(TimeInForce::Ioc),
393            HyperliquidTimeInForce::Ioc
394        );
395        assert_eq!(
396            nautilus_time_in_force_to_hyperliquid(TimeInForce::Fok),
397            HyperliquidTimeInForce::Ioc
398        );
399
400        // Test Hyperliquid to Nautilus
401        assert_eq!(
402            hyperliquid_time_in_force_to_nautilus(HyperliquidTimeInForce::Gtc),
403            TimeInForce::Gtc
404        );
405        assert_eq!(
406            hyperliquid_time_in_force_to_nautilus(HyperliquidTimeInForce::Ioc),
407            TimeInForce::Ioc
408        );
409        assert_eq!(
410            hyperliquid_time_in_force_to_nautilus(HyperliquidTimeInForce::Alo),
411            TimeInForce::Gtc
412        );
413    }
414
415    #[rstest]
416    fn test_conditional_order_type_conversions() {
417        // Test Hyperliquid conditional to Nautilus
418        assert_eq!(
419            hyperliquid_conditional_to_nautilus(HyperliquidConditionalOrderType::StopMarket),
420            OrderType::StopMarket
421        );
422        assert_eq!(
423            hyperliquid_conditional_to_nautilus(HyperliquidConditionalOrderType::StopLimit),
424            OrderType::StopLimit
425        );
426        assert_eq!(
427            hyperliquid_conditional_to_nautilus(HyperliquidConditionalOrderType::TakeProfitMarket),
428            OrderType::MarketIfTouched
429        );
430        assert_eq!(
431            hyperliquid_conditional_to_nautilus(HyperliquidConditionalOrderType::TakeProfitLimit),
432            OrderType::LimitIfTouched
433        );
434
435        // Test Nautilus to Hyperliquid conditional
436        assert_eq!(
437            nautilus_to_hyperliquid_conditional(OrderType::StopMarket),
438            HyperliquidConditionalOrderType::StopMarket
439        );
440        assert_eq!(
441            nautilus_to_hyperliquid_conditional(OrderType::StopLimit),
442            HyperliquidConditionalOrderType::StopLimit
443        );
444        assert_eq!(
445            nautilus_to_hyperliquid_conditional(OrderType::MarketIfTouched),
446            HyperliquidConditionalOrderType::TakeProfitMarket
447        );
448        assert_eq!(
449            nautilus_to_hyperliquid_conditional(OrderType::LimitIfTouched),
450            HyperliquidConditionalOrderType::TakeProfitLimit
451        );
452    }
453
454    #[rstest]
455    fn test_determine_tpsl_type() {
456        // Stop orders should always be SL
457        assert_eq!(
458            determine_tpsl_type(OrderType::StopMarket, true),
459            HyperliquidTpSl::Sl
460        );
461        assert_eq!(
462            determine_tpsl_type(OrderType::StopLimit, false),
463            HyperliquidTpSl::Sl
464        );
465
466        // Take profit orders should always be TP
467        assert_eq!(
468            determine_tpsl_type(OrderType::MarketIfTouched, true),
469            HyperliquidTpSl::Tp
470        );
471        assert_eq!(
472            determine_tpsl_type(OrderType::LimitIfTouched, false),
473            HyperliquidTpSl::Tp
474        );
475
476        // Trailing stops should be SL
477        assert_eq!(
478            determine_tpsl_type(OrderType::TrailingStopMarket, true),
479            HyperliquidTpSl::Sl
480        );
481        assert_eq!(
482            determine_tpsl_type(OrderType::TrailingStopLimit, false),
483            HyperliquidTpSl::Sl
484        );
485    }
486}