Skip to main content

nautilus_hyperliquid/common/
converters.rs

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