Skip to content

Commit f54c6aa

Browse files
Vadim NicolaiVadim Nicolai
authored andcommitted
Change requests applied
1 parent 392744f commit f54c6aa

7 files changed

Lines changed: 247 additions & 407 deletions

File tree

crates/adapters/hyperliquid/src/data/mod.rs

Lines changed: 11 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,9 @@ use nautilus_core::{
4646
};
4747
use nautilus_data::client::DataClient;
4848
use nautilus_model::{
49-
currencies::CURRENCY_MAP,
5049
data::Data,
51-
enums::CurrencyType,
52-
identifiers::{ClientId, InstrumentId, Symbol, Venue},
53-
instruments::{CryptoPerpetual, CurrencyPair, Instrument, InstrumentAny},
54-
types::{Currency, Price, Quantity},
50+
identifiers::{ClientId, InstrumentId, Venue},
51+
instruments::{Instrument, InstrumentAny},
5552
};
5653
use tokio::task::JoinHandle;
5754
use tokio_util::sync::CancellationToken;
@@ -62,102 +59,10 @@ use crate::{
6259
credential::{EvmPrivateKey, Secrets},
6360
},
6461
config::HyperliquidDataClientConfig,
65-
http::{
66-
client::HyperliquidHttpClient,
67-
parse::{HyperliquidInstrumentDef, HyperliquidMarketType},
68-
},
62+
http::client::HyperliquidHttpClient,
6963
websocket::client::HyperliquidWebSocketClient,
7064
};
7165

72-
/// Returns a currency, either from the internal currency map or creates a default crypto.
73-
fn get_currency(code: &str) -> Currency {
74-
// SAFETY: Mutex should not be poisoned in normal operation
75-
CURRENCY_MAP
76-
.lock()
77-
.expect("Failed to acquire CURRENCY_MAP lock")
78-
.get(code)
79-
.copied()
80-
.unwrap_or(Currency::new(code, 8, 0, code, CurrencyType::Crypto))
81-
}
82-
83-
/// Creates a Nautilus instrument from a Hyperliquid instrument definition.
84-
fn create_instrument_from_def(def: &HyperliquidInstrumentDef) -> Option<InstrumentAny> {
85-
let ts_event = get_atomic_clock_realtime().get_time_ns();
86-
let ts_init = ts_event;
87-
88-
// Create instrument ID from the symbol
89-
let symbol = Symbol::new(&def.symbol);
90-
let venue = Venue::new("HYPERLIQUID");
91-
let instrument_id = InstrumentId::new(symbol, venue);
92-
93-
let raw_symbol = Symbol::new(&def.symbol);
94-
let base_currency = get_currency(&def.base);
95-
let quote_currency = get_currency(&def.quote);
96-
let price_increment = Price::from(&def.tick_size.to_string());
97-
let size_increment = Quantity::from(&def.lot_size.to_string());
98-
99-
// For now, use minimal parameters - no fees, margins, or lot sizes
100-
match def.market_type {
101-
HyperliquidMarketType::Spot => {
102-
Some(InstrumentAny::CurrencyPair(CurrencyPair::new(
103-
instrument_id,
104-
raw_symbol,
105-
base_currency,
106-
quote_currency,
107-
def.price_decimals as u8,
108-
def.size_decimals as u8,
109-
price_increment,
110-
size_increment,
111-
None, // multiplier
112-
None, // lot_size
113-
None, // max_quantity
114-
None, // min_quantity
115-
None, // max_notional
116-
None, // min_notional
117-
None, // max_price
118-
None, // min_price
119-
None, // margin_init
120-
None, // margin_maint
121-
None, // maker_fee
122-
None, // taker_fee
123-
ts_event,
124-
ts_init,
125-
)))
126-
}
127-
HyperliquidMarketType::Perp => {
128-
// For Hyperliquid, perps are USD-quoted and USDC-settled
129-
let settlement_currency = get_currency("USDC");
130-
131-
Some(InstrumentAny::CryptoPerpetual(CryptoPerpetual::new(
132-
instrument_id,
133-
raw_symbol,
134-
base_currency,
135-
quote_currency,
136-
settlement_currency,
137-
false, // is_inverse - Hyperliquid perps are linear
138-
def.price_decimals as u8,
139-
def.size_decimals as u8,
140-
price_increment,
141-
size_increment,
142-
None, // multiplier
143-
None, // lot_size
144-
None, // max_quantity
145-
None, // min_quantity
146-
None, // max_notional
147-
None, // min_notional
148-
None, // max_price
149-
None, // min_price
150-
None, // margin_init
151-
None, // margin_maint
152-
None, // maker_fee
153-
None, // taker_fee
154-
ts_event,
155-
ts_init,
156-
)))
157-
}
158-
}
159-
}
160-
16166
#[derive(Debug)]
16267
pub struct HyperliquidDataClient {
16368
client_id: ClientId,
@@ -227,24 +132,20 @@ impl HyperliquidDataClient {
227132
}
228133

229134
async fn bootstrap_instruments(&mut self) -> Result<Vec<InstrumentAny>> {
230-
let mut instruments = Vec::new();
231-
232-
match self.http_client.request_instruments().await {
233-
Ok(defs) => {
135+
let instruments = match self.http_client.request_instruments().await {
136+
Ok(mut instruments) => {
234137
tracing::debug!(
235-
count = defs.len(),
236-
"Received Hyperliquid instrument definitions"
138+
count = instruments.len(),
139+
"Received Hyperliquid instruments"
237140
);
238-
for def in defs {
239-
if let Some(instrument) = create_instrument_from_def(&def) {
240-
instruments.push(instrument);
241-
}
242-
}
141+
instruments.sort_by_key(|instrument| instrument.id());
142+
instruments
243143
}
244144
Err(err) => {
245145
tracing::warn!(%err, "Failed to request Hyperliquid instruments");
146+
Vec::new()
246147
}
247-
}
148+
};
248149

249150
tracing::info!(count = instruments.len(), "Loaded Hyperliquid instruments");
250151

crates/adapters/hyperliquid/src/http/client.rs

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use std::{
2929

3030
use anyhow::Context;
3131
use nautilus_core::consts::NAUTILUS_USER_AGENT;
32+
use nautilus_model::instruments::InstrumentAny;
3233
use nautilus_network::{http::HttpClient, ratelimiter::quota::Quota};
3334
use reqwest::{Method, header::USER_AGENT};
3435
use serde_json::Value;
@@ -40,10 +41,12 @@ use crate::{
4041
credential::{Secrets, VaultAddress},
4142
},
4243
http::{
44+
conversion::instruments_from_defs_owned,
4345
error::{Error, Result},
4446
models::{
4547
HyperliquidExchangeRequest, HyperliquidExchangeResponse, HyperliquidFills,
46-
HyperliquidL2Book, HyperliquidMeta, HyperliquidOrderStatus,
48+
HyperliquidL2Book, HyperliquidMeta, HyperliquidOrderStatus, PerpMeta, PerpMetaAndCtxs,
49+
SpotMeta, SpotMetaAndCtxs,
4750
},
4851
parse::{HyperliquidInstrumentDef, parse_perp_instruments, parse_spot_instruments},
4952
query::{ExchangeAction, InfoRequest},
@@ -67,6 +70,10 @@ pub static HYPERLIQUID_REST_QUOTA: LazyLock<Quota> =
6770
/// specific to Hyperliquid, such as request signing (for authenticated endpoints),
6871
/// forming request URLs, and deserializing responses into specific data models.
6972
#[derive(Debug, Clone)]
73+
#[cfg_attr(
74+
feature = "python",
75+
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.adapters")
76+
)]
7077
pub struct HyperliquidHttpClient {
7178
client: HttpClient,
7279
is_testnet: bool,
@@ -188,39 +195,32 @@ impl HyperliquidHttpClient {
188195
serde_json::from_value(response).map_err(Error::Serde)
189196
}
190197

191-
/// Get complete perpetuals metadata.
192-
pub async fn get_perp_meta(&self) -> Result<crate::http::models::PerpMeta> {
193-
let request = InfoRequest::meta();
194-
let response = self.send_info_request(&request).await?;
195-
serde_json::from_value(response).map_err(Error::Serde)
196-
}
197-
198198
/// Get complete spot metadata (tokens and pairs).
199-
pub async fn get_spot_meta(&self) -> Result<crate::http::models::SpotMeta> {
199+
pub async fn get_spot_meta(&self) -> Result<SpotMeta> {
200200
let request = InfoRequest::spot_meta();
201201
let response = self.send_info_request(&request).await?;
202202
serde_json::from_value(response).map_err(Error::Serde)
203203
}
204204

205205
/// Get perpetuals metadata with asset contexts (for price precision refinement).
206-
pub async fn get_perp_meta_and_ctxs(&self) -> Result<crate::http::models::PerpMetaAndCtxs> {
206+
pub async fn get_perp_meta_and_ctxs(&self) -> Result<PerpMetaAndCtxs> {
207207
let request = InfoRequest::meta_and_asset_ctxs();
208208
let response = self.send_info_request(&request).await?;
209209
serde_json::from_value(response).map_err(Error::Serde)
210210
}
211211

212212
/// Get spot metadata with asset contexts (for price precision refinement).
213-
pub async fn get_spot_meta_and_ctxs(&self) -> Result<crate::http::models::SpotMetaAndCtxs> {
213+
pub async fn get_spot_meta_and_ctxs(&self) -> Result<SpotMetaAndCtxs> {
214214
let request = InfoRequest::spot_meta_and_asset_ctxs();
215215
let response = self.send_info_request(&request).await?;
216216
serde_json::from_value(response).map_err(Error::Serde)
217217
}
218218

219219
/// Fetch and parse all available instrument definitions from Hyperliquid.
220-
pub async fn request_instruments(&self) -> Result<Vec<HyperliquidInstrumentDef>> {
221-
let mut defs = Vec::new();
220+
pub async fn request_instruments(&self) -> Result<Vec<InstrumentAny>> {
221+
let mut defs: Vec<HyperliquidInstrumentDef> = Vec::new();
222222

223-
match self.get_perp_meta().await {
223+
match self.load_perp_meta().await {
224224
Ok(perp_meta) => match parse_perp_instruments(&perp_meta) {
225225
Ok(perp_defs) => {
226226
tracing::debug!(
@@ -256,7 +256,13 @@ impl HyperliquidHttpClient {
256256
}
257257
}
258258

259-
Ok(defs)
259+
Ok(instruments_from_defs_owned(defs))
260+
}
261+
262+
pub(crate) async fn load_perp_meta(&self) -> Result<PerpMeta> {
263+
let request = InfoRequest::meta();
264+
let response = self.send_info_request(&request).await?;
265+
serde_json::from_value(response).map_err(Error::Serde)
260266
}
261267

262268
/// Get L2 order book for a coin.
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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+
//! Helpers for converting Hyperliquid instrument definitions into Nautilus instruments.
17+
18+
use nautilus_core::time::get_atomic_clock_realtime;
19+
use nautilus_model::{
20+
currencies::CURRENCY_MAP,
21+
enums::CurrencyType,
22+
identifiers::{InstrumentId, Symbol},
23+
instruments::{CryptoPerpetual, CurrencyPair, InstrumentAny},
24+
types::{Currency, Price, Quantity},
25+
};
26+
27+
use crate::{
28+
common::consts::HYPERLIQUID_VENUE,
29+
http::parse::{HyperliquidInstrumentDef, HyperliquidMarketType},
30+
};
31+
32+
fn get_currency(code: &str) -> Currency {
33+
CURRENCY_MAP
34+
.lock()
35+
.expect("Failed to acquire CURRENCY_MAP lock")
36+
.get(code)
37+
.copied()
38+
.unwrap_or_else(|| Currency::new(code, 8, 0, code, CurrencyType::Crypto))
39+
}
40+
41+
/// Converts a single Hyperliquid instrument definition into a Nautilus `InstrumentAny`.
42+
///
43+
/// Returns `None` if the conversion fails (e.g., unsupported market type).
44+
#[must_use]
45+
pub fn create_instrument_from_def(def: &HyperliquidInstrumentDef) -> Option<InstrumentAny> {
46+
let clock = get_atomic_clock_realtime();
47+
let ts_event = clock.get_time_ns();
48+
let ts_init = ts_event;
49+
50+
let symbol = Symbol::new(&def.symbol);
51+
let venue = *HYPERLIQUID_VENUE;
52+
let instrument_id = InstrumentId::new(symbol, venue);
53+
54+
let raw_symbol = Symbol::new(&def.symbol);
55+
let base_currency = get_currency(&def.base);
56+
let quote_currency = get_currency(&def.quote);
57+
let price_increment = Price::from(&def.tick_size.to_string());
58+
let size_increment = Quantity::from(&def.lot_size.to_string());
59+
60+
match def.market_type {
61+
HyperliquidMarketType::Spot => Some(InstrumentAny::CurrencyPair(CurrencyPair::new(
62+
instrument_id,
63+
raw_symbol,
64+
base_currency,
65+
quote_currency,
66+
def.price_decimals as u8,
67+
def.size_decimals as u8,
68+
price_increment,
69+
size_increment,
70+
None,
71+
None,
72+
None,
73+
None,
74+
None,
75+
None,
76+
None,
77+
None,
78+
None,
79+
None,
80+
None,
81+
None,
82+
ts_event,
83+
ts_init,
84+
))),
85+
HyperliquidMarketType::Perp => {
86+
let settlement_currency = get_currency("USDC");
87+
88+
Some(InstrumentAny::CryptoPerpetual(CryptoPerpetual::new(
89+
instrument_id,
90+
raw_symbol,
91+
base_currency,
92+
quote_currency,
93+
settlement_currency,
94+
false,
95+
def.price_decimals as u8,
96+
def.size_decimals as u8,
97+
price_increment,
98+
size_increment,
99+
None,
100+
None,
101+
None,
102+
None,
103+
None,
104+
None,
105+
None,
106+
None,
107+
None,
108+
None,
109+
None,
110+
None,
111+
ts_event,
112+
ts_init,
113+
)))
114+
}
115+
}
116+
}
117+
118+
/// Convert a collection of Hyperliquid instrument definitions into Nautilus instruments,
119+
/// discarding any definitions that fail to convert.
120+
#[must_use]
121+
pub fn instruments_from_defs(defs: &[HyperliquidInstrumentDef]) -> Vec<InstrumentAny> {
122+
defs.iter().filter_map(create_instrument_from_def).collect()
123+
}
124+
125+
/// Convert owned definitions into Nautilus instruments, consuming the input vector.
126+
#[must_use]
127+
pub fn instruments_from_defs_owned(defs: Vec<HyperliquidInstrumentDef>) -> Vec<InstrumentAny> {
128+
defs.into_iter()
129+
.filter_map(|def| create_instrument_from_def(&def))
130+
.collect()
131+
}

crates/adapters/hyperliquid/src/http/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
// -------------------------------------------------------------------------------------------------
1515

1616
pub mod client;
17+
pub mod conversion;
1718
pub mod error;
1819
pub mod models;
1920
pub mod parse;

0 commit comments

Comments
 (0)