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 => Ok(HyperliquidTimeInForce::Gtc), // GTD maps to GTC
176        TimeInForce::Day => Ok(HyperliquidTimeInForce::Gtc), // DAY maps to GTC
177        TimeInForce::AtTheOpen => Ok(HyperliquidTimeInForce::Gtc), // ATO maps to GTC
178        TimeInForce::AtTheClose => Ok(HyperliquidTimeInForce::Gtc), // ATC maps to GTC
179    }
180}
181
182/// Converts a Hyperliquid time in force to a Nautilus `TimeInForce`.
183pub fn hyperliquid_time_in_force_to_nautilus(hl_tif: HyperliquidTimeInForce) -> TimeInForce {
184    match hl_tif {
185        HyperliquidTimeInForce::Gtc => TimeInForce::Gtc,
186        HyperliquidTimeInForce::Ioc => TimeInForce::Ioc,
187        HyperliquidTimeInForce::Alo => TimeInForce::Gtc, // ALO (post-only) maps to GTC
188    }
189}
190
191/// Determines the TP/SL type based on order type and side.
192///
193/// # Logic
194///
195/// For buy orders:
196/// - Stop orders (trigger below current price) -> Stop Loss
197/// - Take profit orders (trigger above current price) -> Take Profit
198///
199/// For sell orders:
200/// - Stop orders (trigger above current price) -> Stop Loss
201/// - Take profit orders (trigger below current price) -> Take Profit
202pub fn determine_tpsl_type(order_type: OrderType, is_buy: bool) -> HyperliquidTpSl {
203    match order_type {
204        OrderType::StopMarket
205        | OrderType::StopLimit
206        | OrderType::TrailingStopMarket
207        | OrderType::TrailingStopLimit => HyperliquidTpSl::Sl,
208        OrderType::MarketIfTouched | OrderType::LimitIfTouched => HyperliquidTpSl::Tp,
209        _ => {
210            // Default logic based on side if order type is ambiguous
211            if is_buy {
212                HyperliquidTpSl::Sl
213            } else {
214                HyperliquidTpSl::Tp
215            }
216        }
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use rstest::rstest;
223
224    use super::*;
225
226    #[rstest]
227    fn test_nautilus_to_hyperliquid_limit_order() {
228        let result =
229            nautilus_order_type_to_hyperliquid(OrderType::Limit, Some(TimeInForce::Gtc), None)
230                .unwrap();
231
232        match result {
233            HyperliquidOrderType::Limit { tif } => {
234                assert_eq!(tif, HyperliquidTimeInForce::Gtc);
235            }
236            _ => panic!("Expected Limit order type"),
237        }
238    }
239
240    #[rstest]
241    fn test_nautilus_to_hyperliquid_stop_market() {
242        let result = nautilus_order_type_to_hyperliquid(
243            OrderType::StopMarket,
244            None,
245            Some(Decimal::new(49000, 0)),
246        )
247        .unwrap();
248
249        match result {
250            HyperliquidOrderType::Trigger {
251                is_market,
252                trigger_px,
253                tpsl,
254            } => {
255                assert!(is_market);
256                assert_eq!(trigger_px, "49000");
257                assert_eq!(tpsl, HyperliquidTpSl::Sl);
258            }
259            _ => panic!("Expected Trigger order type"),
260        }
261    }
262
263    #[rstest]
264    fn test_nautilus_to_hyperliquid_stop_limit() {
265        let result = nautilus_order_type_to_hyperliquid(
266            OrderType::StopLimit,
267            None,
268            Some(Decimal::new(49000, 0)),
269        )
270        .unwrap();
271
272        match result {
273            HyperliquidOrderType::Trigger {
274                is_market,
275                trigger_px,
276                tpsl,
277            } => {
278                assert!(!is_market);
279                assert_eq!(trigger_px, "49000");
280                assert_eq!(tpsl, HyperliquidTpSl::Sl);
281            }
282            _ => panic!("Expected Trigger order type"),
283        }
284    }
285
286    #[rstest]
287    fn test_nautilus_to_hyperliquid_take_profit_market() {
288        let result = nautilus_order_type_to_hyperliquid(
289            OrderType::MarketIfTouched,
290            None,
291            Some(Decimal::new(51000, 0)),
292        )
293        .unwrap();
294
295        match result {
296            HyperliquidOrderType::Trigger {
297                is_market,
298                trigger_px,
299                tpsl,
300            } => {
301                assert!(is_market);
302                assert_eq!(trigger_px, "51000");
303                assert_eq!(tpsl, HyperliquidTpSl::Tp);
304            }
305            _ => panic!("Expected Trigger order type"),
306        }
307    }
308
309    #[rstest]
310    fn test_nautilus_to_hyperliquid_take_profit_limit() {
311        let result = nautilus_order_type_to_hyperliquid(
312            OrderType::LimitIfTouched,
313            None,
314            Some(Decimal::new(51000, 0)),
315        )
316        .unwrap();
317
318        match result {
319            HyperliquidOrderType::Trigger {
320                is_market,
321                trigger_px,
322                tpsl,
323            } => {
324                assert!(!is_market);
325                assert_eq!(trigger_px, "51000");
326                assert_eq!(tpsl, HyperliquidTpSl::Tp);
327            }
328            _ => panic!("Expected Trigger order type"),
329        }
330    }
331
332    #[rstest]
333    fn test_hyperliquid_to_nautilus_limit() {
334        let hl_order = HyperliquidOrderType::Limit {
335            tif: HyperliquidTimeInForce::Gtc,
336        };
337        assert_eq!(
338            hyperliquid_order_type_to_nautilus(&hl_order),
339            OrderType::Limit
340        );
341    }
342
343    #[rstest]
344    fn test_hyperliquid_to_nautilus_stop_market() {
345        let hl_order = HyperliquidOrderType::Trigger {
346            is_market: true,
347            trigger_px: "49000".to_string(),
348            tpsl: HyperliquidTpSl::Sl,
349        };
350        assert_eq!(
351            hyperliquid_order_type_to_nautilus(&hl_order),
352            OrderType::StopMarket
353        );
354    }
355
356    #[rstest]
357    fn test_hyperliquid_to_nautilus_stop_limit() {
358        let hl_order = HyperliquidOrderType::Trigger {
359            is_market: false,
360            trigger_px: "49000".to_string(),
361            tpsl: HyperliquidTpSl::Sl,
362        };
363        assert_eq!(
364            hyperliquid_order_type_to_nautilus(&hl_order),
365            OrderType::StopLimit
366        );
367    }
368
369    #[rstest]
370    fn test_hyperliquid_to_nautilus_take_profit_market() {
371        let hl_order = HyperliquidOrderType::Trigger {
372            is_market: true,
373            trigger_px: "51000".to_string(),
374            tpsl: HyperliquidTpSl::Tp,
375        };
376        assert_eq!(
377            hyperliquid_order_type_to_nautilus(&hl_order),
378            OrderType::MarketIfTouched
379        );
380    }
381
382    #[rstest]
383    fn test_hyperliquid_to_nautilus_take_profit_limit() {
384        let hl_order = HyperliquidOrderType::Trigger {
385            is_market: false,
386            trigger_px: "51000".to_string(),
387            tpsl: HyperliquidTpSl::Tp,
388        };
389        assert_eq!(
390            hyperliquid_order_type_to_nautilus(&hl_order),
391            OrderType::LimitIfTouched
392        );
393    }
394
395    #[rstest]
396    fn test_time_in_force_conversions() {
397        // Test Nautilus to Hyperliquid
398        assert_eq!(
399            nautilus_time_in_force_to_hyperliquid(TimeInForce::Gtc).unwrap(),
400            HyperliquidTimeInForce::Gtc
401        );
402        assert_eq!(
403            nautilus_time_in_force_to_hyperliquid(TimeInForce::Ioc).unwrap(),
404            HyperliquidTimeInForce::Ioc
405        );
406
407        // Test Hyperliquid to Nautilus
408        assert_eq!(
409            hyperliquid_time_in_force_to_nautilus(HyperliquidTimeInForce::Gtc),
410            TimeInForce::Gtc
411        );
412        assert_eq!(
413            hyperliquid_time_in_force_to_nautilus(HyperliquidTimeInForce::Ioc),
414            TimeInForce::Ioc
415        );
416        assert_eq!(
417            hyperliquid_time_in_force_to_nautilus(HyperliquidTimeInForce::Alo),
418            TimeInForce::Gtc
419        );
420    }
421
422    #[rstest]
423    fn test_fok_time_in_force_returns_error() {
424        let result = nautilus_time_in_force_to_hyperliquid(TimeInForce::Fok);
425        assert!(result.is_err());
426        assert!(
427            result
428                .unwrap_err()
429                .to_string()
430                .contains("FOK time in force is not supported")
431        );
432    }
433
434    #[rstest]
435    fn test_conditional_order_type_conversions() {
436        // Test Hyperliquid conditional to Nautilus
437        assert_eq!(
438            hyperliquid_conditional_to_nautilus(HyperliquidConditionalOrderType::StopMarket),
439            OrderType::StopMarket
440        );
441        assert_eq!(
442            hyperliquid_conditional_to_nautilus(HyperliquidConditionalOrderType::StopLimit),
443            OrderType::StopLimit
444        );
445        assert_eq!(
446            hyperliquid_conditional_to_nautilus(HyperliquidConditionalOrderType::TakeProfitMarket),
447            OrderType::MarketIfTouched
448        );
449        assert_eq!(
450            hyperliquid_conditional_to_nautilus(HyperliquidConditionalOrderType::TakeProfitLimit),
451            OrderType::LimitIfTouched
452        );
453
454        // Test Nautilus to Hyperliquid conditional
455        assert_eq!(
456            nautilus_to_hyperliquid_conditional(OrderType::StopMarket),
457            HyperliquidConditionalOrderType::StopMarket
458        );
459        assert_eq!(
460            nautilus_to_hyperliquid_conditional(OrderType::StopLimit),
461            HyperliquidConditionalOrderType::StopLimit
462        );
463        assert_eq!(
464            nautilus_to_hyperliquid_conditional(OrderType::MarketIfTouched),
465            HyperliquidConditionalOrderType::TakeProfitMarket
466        );
467        assert_eq!(
468            nautilus_to_hyperliquid_conditional(OrderType::LimitIfTouched),
469            HyperliquidConditionalOrderType::TakeProfitLimit
470        );
471    }
472
473    #[rstest]
474    fn test_determine_tpsl_type() {
475        // Stop orders should always be SL
476        assert_eq!(
477            determine_tpsl_type(OrderType::StopMarket, true),
478            HyperliquidTpSl::Sl
479        );
480        assert_eq!(
481            determine_tpsl_type(OrderType::StopLimit, false),
482            HyperliquidTpSl::Sl
483        );
484
485        // Take profit orders should always be TP
486        assert_eq!(
487            determine_tpsl_type(OrderType::MarketIfTouched, true),
488            HyperliquidTpSl::Tp
489        );
490        assert_eq!(
491            determine_tpsl_type(OrderType::LimitIfTouched, false),
492            HyperliquidTpSl::Tp
493        );
494
495        // Trailing stops should be SL
496        assert_eq!(
497            determine_tpsl_type(OrderType::TrailingStopMarket, true),
498            HyperliquidTpSl::Sl
499        );
500        assert_eq!(
501            determine_tpsl_type(OrderType::TrailingStopLimit, false),
502            HyperliquidTpSl::Sl
503        );
504    }
505}