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/// # Arguments
31///
32/// * `order_type` - The Nautilus order type to convert
33/// * `time_in_force` - The time in force for limit orders
34/// * `trigger_price` - Optional trigger price for conditional orders
35///
36/// # Returns
37///
38/// A `HyperliquidOrderType` variant configured for the specified order type.
39///
40/// # Panics
41///
42/// Panics if a conditional order is specified without a trigger price.
43pub fn nautilus_order_type_to_hyperliquid(
44    order_type: OrderType,
45    time_in_force: Option<TimeInForce>,
46    trigger_price: Option<Decimal>,
47) -> HyperliquidOrderType {
48    match order_type {
49        // Regular limit order
50        OrderType::Limit => {
51            let tif = time_in_force
52                .map(nautilus_time_in_force_to_hyperliquid)
53                .unwrap_or(HyperliquidTimeInForce::Gtc);
54            HyperliquidOrderType::Limit { tif }
55        }
56
57        // Stop market order (stop loss)
58        OrderType::StopMarket => {
59            let trigger_px = trigger_price
60                .expect("Trigger price required for StopMarket order")
61                .to_string();
62            HyperliquidOrderType::Trigger {
63                is_market: true,
64                trigger_px,
65                tpsl: HyperliquidTpSl::Sl,
66            }
67        }
68
69        // Stop limit order (stop loss with limit)
70        OrderType::StopLimit => {
71            let trigger_px = trigger_price
72                .expect("Trigger price required for StopLimit order")
73                .to_string();
74            HyperliquidOrderType::Trigger {
75                is_market: false,
76                trigger_px,
77                tpsl: HyperliquidTpSl::Sl,
78            }
79        }
80
81        // Market if touched (take profit market)
82        OrderType::MarketIfTouched => {
83            let trigger_px = trigger_price
84                .expect("Trigger price required for MarketIfTouched order")
85                .to_string();
86            HyperliquidOrderType::Trigger {
87                is_market: true,
88                trigger_px,
89                tpsl: HyperliquidTpSl::Tp,
90            }
91        }
92
93        // Limit if touched (take profit limit)
94        OrderType::LimitIfTouched => {
95            let trigger_px = trigger_price
96                .expect("Trigger price required for LimitIfTouched order")
97                .to_string();
98            HyperliquidOrderType::Trigger {
99                is_market: false,
100                trigger_px,
101                tpsl: HyperliquidTpSl::Tp,
102            }
103        }
104
105        // Trailing stop market (requires special handling)
106        OrderType::TrailingStopMarket => {
107            let trigger_px = trigger_price
108                .expect("Trigger price required for TrailingStopMarket order")
109                .to_string();
110            HyperliquidOrderType::Trigger {
111                is_market: true,
112                trigger_px,
113                tpsl: HyperliquidTpSl::Sl,
114            }
115        }
116
117        // Trailing stop limit (requires special handling)
118        OrderType::TrailingStopLimit => {
119            let trigger_px = trigger_price
120                .expect("Trigger price required for TrailingStopLimit order")
121                .to_string();
122            HyperliquidOrderType::Trigger {
123                is_market: false,
124                trigger_px,
125                tpsl: HyperliquidTpSl::Sl,
126            }
127        }
128
129        // Market orders are handled elsewhere (not represented in HyperliquidOrderType)
130        OrderType::Market => {
131            panic!("Market orders should be handled separately via immediate execution")
132        }
133
134        // Unsupported order types
135        _ => panic!("Unsupported order type: {order_type:?}"),
136    }
137}
138
139/// Converts a Hyperliquid order type to a Nautilus `OrderType`.
140///
141/// # Arguments
142///
143/// * `hl_order_type` - The Hyperliquid order type to convert
144///
145/// # Returns
146///
147/// The corresponding Nautilus `OrderType`.
148pub fn hyperliquid_order_type_to_nautilus(hl_order_type: &HyperliquidOrderType) -> OrderType {
149    match hl_order_type {
150        HyperliquidOrderType::Limit { .. } => OrderType::Limit,
151        HyperliquidOrderType::Trigger {
152            is_market, tpsl, ..
153        } => match (is_market, tpsl) {
154            (true, HyperliquidTpSl::Sl) => OrderType::StopMarket,
155            (false, HyperliquidTpSl::Sl) => OrderType::StopLimit,
156            (true, HyperliquidTpSl::Tp) => OrderType::MarketIfTouched,
157            (false, HyperliquidTpSl::Tp) => OrderType::LimitIfTouched,
158        },
159    }
160}
161
162/// Converts a Hyperliquid conditional order type to a Nautilus `OrderType`.
163///
164/// # Arguments
165///
166/// * `conditional_type` - The Hyperliquid conditional order type
167///
168/// # Returns
169///
170/// The corresponding Nautilus `OrderType`.
171pub fn hyperliquid_conditional_to_nautilus(
172    conditional_type: HyperliquidConditionalOrderType,
173) -> OrderType {
174    OrderType::from(conditional_type)
175}
176
177/// Converts a Nautilus `OrderType` to a Hyperliquid conditional order type.
178///
179/// # Arguments
180///
181/// * `order_type` - The Nautilus order type
182///
183/// # Returns
184///
185/// The corresponding Hyperliquid conditional order type.
186///
187/// # Panics
188///
189/// Panics if the order type is not a conditional order type.
190pub fn nautilus_to_hyperliquid_conditional(
191    order_type: OrderType,
192) -> HyperliquidConditionalOrderType {
193    HyperliquidConditionalOrderType::from(order_type)
194}
195
196/// Converts a Nautilus `TimeInForce` to a Hyperliquid time in force.
197///
198/// # Arguments
199///
200/// * `tif` - The Nautilus time in force
201///
202/// # Returns
203///
204/// The corresponding Hyperliquid time in force.
205pub fn nautilus_time_in_force_to_hyperliquid(tif: TimeInForce) -> HyperliquidTimeInForce {
206    match tif {
207        TimeInForce::Gtc => HyperliquidTimeInForce::Gtc,
208        TimeInForce::Ioc => HyperliquidTimeInForce::Ioc,
209        TimeInForce::Fok => HyperliquidTimeInForce::Ioc, // FOK maps to IOC in Hyperliquid
210        TimeInForce::Gtd => HyperliquidTimeInForce::Gtc, // GTD maps to GTC
211        TimeInForce::Day => HyperliquidTimeInForce::Gtc, // DAY maps to GTC
212        TimeInForce::AtTheOpen => HyperliquidTimeInForce::Gtc, // ATO maps to GTC
213        TimeInForce::AtTheClose => HyperliquidTimeInForce::Gtc, // ATC maps to GTC
214    }
215}
216
217/// Converts a Hyperliquid time in force to a Nautilus `TimeInForce`.
218///
219/// # Arguments
220///
221/// * `hl_tif` - The Hyperliquid time in force
222///
223/// # Returns
224///
225/// The corresponding Nautilus time in force.
226pub fn hyperliquid_time_in_force_to_nautilus(hl_tif: HyperliquidTimeInForce) -> TimeInForce {
227    match hl_tif {
228        HyperliquidTimeInForce::Gtc => TimeInForce::Gtc,
229        HyperliquidTimeInForce::Ioc => TimeInForce::Ioc,
230        HyperliquidTimeInForce::Alo => TimeInForce::Gtc, // ALO (post-only) maps to GTC
231    }
232}
233
234/// Determines the TP/SL type based on order type and side.
235///
236/// # Arguments
237///
238/// * `order_type` - The Nautilus order type
239/// * `is_buy` - Whether this is a buy order
240///
241/// # Returns
242///
243/// The appropriate `HyperliquidTpSl` type.
244///
245/// # Logic
246///
247/// For buy orders:
248/// - Stop orders (trigger below current price) -> Stop Loss
249/// - Take profit orders (trigger above current price) -> Take Profit
250///
251/// For sell orders:
252/// - Stop orders (trigger above current price) -> Stop Loss
253/// - Take profit orders (trigger below current price) -> Take Profit
254pub fn determine_tpsl_type(order_type: OrderType, is_buy: bool) -> HyperliquidTpSl {
255    match order_type {
256        OrderType::StopMarket
257        | OrderType::StopLimit
258        | OrderType::TrailingStopMarket
259        | OrderType::TrailingStopLimit => HyperliquidTpSl::Sl,
260        OrderType::MarketIfTouched | OrderType::LimitIfTouched => HyperliquidTpSl::Tp,
261        _ => {
262            // Default logic based on side if order type is ambiguous
263            if is_buy {
264                HyperliquidTpSl::Sl
265            } else {
266                HyperliquidTpSl::Tp
267            }
268        }
269    }
270}
271
272////////////////////////////////////////////////////////////////////////////////
273// Tests
274////////////////////////////////////////////////////////////////////////////////
275
276#[cfg(test)]
277mod tests {
278    use rstest::rstest;
279
280    use super::*;
281
282    #[rstest]
283    fn test_nautilus_to_hyperliquid_limit_order() {
284        let result =
285            nautilus_order_type_to_hyperliquid(OrderType::Limit, Some(TimeInForce::Gtc), None);
286
287        match result {
288            HyperliquidOrderType::Limit { tif } => {
289                assert_eq!(tif, HyperliquidTimeInForce::Gtc);
290            }
291            _ => panic!("Expected Limit order type"),
292        }
293    }
294
295    #[rstest]
296    fn test_nautilus_to_hyperliquid_stop_market() {
297        let result = nautilus_order_type_to_hyperliquid(
298            OrderType::StopMarket,
299            None,
300            Some(Decimal::new(49000, 0)),
301        );
302
303        match result {
304            HyperliquidOrderType::Trigger {
305                is_market,
306                trigger_px,
307                tpsl,
308            } => {
309                assert!(is_market);
310                assert_eq!(trigger_px, "49000");
311                assert_eq!(tpsl, HyperliquidTpSl::Sl);
312            }
313            _ => panic!("Expected Trigger order type"),
314        }
315    }
316
317    #[rstest]
318    fn test_nautilus_to_hyperliquid_stop_limit() {
319        let result = nautilus_order_type_to_hyperliquid(
320            OrderType::StopLimit,
321            None,
322            Some(Decimal::new(49000, 0)),
323        );
324
325        match result {
326            HyperliquidOrderType::Trigger {
327                is_market,
328                trigger_px,
329                tpsl,
330            } => {
331                assert!(!is_market);
332                assert_eq!(trigger_px, "49000");
333                assert_eq!(tpsl, HyperliquidTpSl::Sl);
334            }
335            _ => panic!("Expected Trigger order type"),
336        }
337    }
338
339    #[rstest]
340    fn test_nautilus_to_hyperliquid_take_profit_market() {
341        let result = nautilus_order_type_to_hyperliquid(
342            OrderType::MarketIfTouched,
343            None,
344            Some(Decimal::new(51000, 0)),
345        );
346
347        match result {
348            HyperliquidOrderType::Trigger {
349                is_market,
350                trigger_px,
351                tpsl,
352            } => {
353                assert!(is_market);
354                assert_eq!(trigger_px, "51000");
355                assert_eq!(tpsl, HyperliquidTpSl::Tp);
356            }
357            _ => panic!("Expected Trigger order type"),
358        }
359    }
360
361    #[rstest]
362    fn test_nautilus_to_hyperliquid_take_profit_limit() {
363        let result = nautilus_order_type_to_hyperliquid(
364            OrderType::LimitIfTouched,
365            None,
366            Some(Decimal::new(51000, 0)),
367        );
368
369        match result {
370            HyperliquidOrderType::Trigger {
371                is_market,
372                trigger_px,
373                tpsl,
374            } => {
375                assert!(!is_market);
376                assert_eq!(trigger_px, "51000");
377                assert_eq!(tpsl, HyperliquidTpSl::Tp);
378            }
379            _ => panic!("Expected Trigger order type"),
380        }
381    }
382
383    #[rstest]
384    fn test_hyperliquid_to_nautilus_limit() {
385        let hl_order = HyperliquidOrderType::Limit {
386            tif: HyperliquidTimeInForce::Gtc,
387        };
388        assert_eq!(
389            hyperliquid_order_type_to_nautilus(&hl_order),
390            OrderType::Limit
391        );
392    }
393
394    #[rstest]
395    fn test_hyperliquid_to_nautilus_stop_market() {
396        let hl_order = HyperliquidOrderType::Trigger {
397            is_market: true,
398            trigger_px: "49000".to_string(),
399            tpsl: HyperliquidTpSl::Sl,
400        };
401        assert_eq!(
402            hyperliquid_order_type_to_nautilus(&hl_order),
403            OrderType::StopMarket
404        );
405    }
406
407    #[rstest]
408    fn test_hyperliquid_to_nautilus_stop_limit() {
409        let hl_order = HyperliquidOrderType::Trigger {
410            is_market: false,
411            trigger_px: "49000".to_string(),
412            tpsl: HyperliquidTpSl::Sl,
413        };
414        assert_eq!(
415            hyperliquid_order_type_to_nautilus(&hl_order),
416            OrderType::StopLimit
417        );
418    }
419
420    #[rstest]
421    fn test_hyperliquid_to_nautilus_take_profit_market() {
422        let hl_order = HyperliquidOrderType::Trigger {
423            is_market: true,
424            trigger_px: "51000".to_string(),
425            tpsl: HyperliquidTpSl::Tp,
426        };
427        assert_eq!(
428            hyperliquid_order_type_to_nautilus(&hl_order),
429            OrderType::MarketIfTouched
430        );
431    }
432
433    #[rstest]
434    fn test_hyperliquid_to_nautilus_take_profit_limit() {
435        let hl_order = HyperliquidOrderType::Trigger {
436            is_market: false,
437            trigger_px: "51000".to_string(),
438            tpsl: HyperliquidTpSl::Tp,
439        };
440        assert_eq!(
441            hyperliquid_order_type_to_nautilus(&hl_order),
442            OrderType::LimitIfTouched
443        );
444    }
445
446    #[rstest]
447    fn test_time_in_force_conversions() {
448        // Test Nautilus to Hyperliquid
449        assert_eq!(
450            nautilus_time_in_force_to_hyperliquid(TimeInForce::Gtc),
451            HyperliquidTimeInForce::Gtc
452        );
453        assert_eq!(
454            nautilus_time_in_force_to_hyperliquid(TimeInForce::Ioc),
455            HyperliquidTimeInForce::Ioc
456        );
457        assert_eq!(
458            nautilus_time_in_force_to_hyperliquid(TimeInForce::Fok),
459            HyperliquidTimeInForce::Ioc
460        );
461
462        // Test Hyperliquid to Nautilus
463        assert_eq!(
464            hyperliquid_time_in_force_to_nautilus(HyperliquidTimeInForce::Gtc),
465            TimeInForce::Gtc
466        );
467        assert_eq!(
468            hyperliquid_time_in_force_to_nautilus(HyperliquidTimeInForce::Ioc),
469            TimeInForce::Ioc
470        );
471        assert_eq!(
472            hyperliquid_time_in_force_to_nautilus(HyperliquidTimeInForce::Alo),
473            TimeInForce::Gtc
474        );
475    }
476
477    #[rstest]
478    fn test_conditional_order_type_conversions() {
479        // Test Hyperliquid conditional to Nautilus
480        assert_eq!(
481            hyperliquid_conditional_to_nautilus(HyperliquidConditionalOrderType::StopMarket),
482            OrderType::StopMarket
483        );
484        assert_eq!(
485            hyperliquid_conditional_to_nautilus(HyperliquidConditionalOrderType::StopLimit),
486            OrderType::StopLimit
487        );
488        assert_eq!(
489            hyperliquid_conditional_to_nautilus(HyperliquidConditionalOrderType::TakeProfitMarket),
490            OrderType::MarketIfTouched
491        );
492        assert_eq!(
493            hyperliquid_conditional_to_nautilus(HyperliquidConditionalOrderType::TakeProfitLimit),
494            OrderType::LimitIfTouched
495        );
496
497        // Test Nautilus to Hyperliquid conditional
498        assert_eq!(
499            nautilus_to_hyperliquid_conditional(OrderType::StopMarket),
500            HyperliquidConditionalOrderType::StopMarket
501        );
502        assert_eq!(
503            nautilus_to_hyperliquid_conditional(OrderType::StopLimit),
504            HyperliquidConditionalOrderType::StopLimit
505        );
506        assert_eq!(
507            nautilus_to_hyperliquid_conditional(OrderType::MarketIfTouched),
508            HyperliquidConditionalOrderType::TakeProfitMarket
509        );
510        assert_eq!(
511            nautilus_to_hyperliquid_conditional(OrderType::LimitIfTouched),
512            HyperliquidConditionalOrderType::TakeProfitLimit
513        );
514    }
515
516    #[rstest]
517    fn test_determine_tpsl_type() {
518        // Stop orders should always be SL
519        assert_eq!(
520            determine_tpsl_type(OrderType::StopMarket, true),
521            HyperliquidTpSl::Sl
522        );
523        assert_eq!(
524            determine_tpsl_type(OrderType::StopLimit, false),
525            HyperliquidTpSl::Sl
526        );
527
528        // Take profit orders should always be TP
529        assert_eq!(
530            determine_tpsl_type(OrderType::MarketIfTouched, true),
531            HyperliquidTpSl::Tp
532        );
533        assert_eq!(
534            determine_tpsl_type(OrderType::LimitIfTouched, false),
535            HyperliquidTpSl::Tp
536        );
537
538        // Trailing stops should be SL
539        assert_eq!(
540            determine_tpsl_type(OrderType::TrailingStopMarket, true),
541            HyperliquidTpSl::Sl
542        );
543        assert_eq!(
544            determine_tpsl_type(OrderType::TrailingStopLimit, false),
545            HyperliquidTpSl::Sl
546        );
547    }
548}