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