use alloy::primitives::B128;
use rust_decimal::Decimal;
use std::str::FromStr;
use std::sync::Arc;
use crate::types::{
Cloid, OrderRequest, OrderTypePlacement, Side, TIF, TimeInForce, TpSl,
};
#[derive(Debug, Clone)]
pub struct Order {
asset: String,
side: Side,
size: Option<Decimal>,
notional: Option<Decimal>,
price: Option<Decimal>,
tif: TIF,
reduce_only: bool,
cloid: Option<Cloid>,
priority_fee: Option<u64>,
}
impl Order {
pub fn buy(asset: impl Into<String>) -> Self {
Self::new(asset.into(), Side::Buy)
}
pub fn sell(asset: impl Into<String>) -> Self {
Self::new(asset.into(), Side::Sell)
}
pub fn long(asset: impl Into<String>) -> Self {
Self::buy(asset)
}
pub fn short(asset: impl Into<String>) -> Self {
Self::sell(asset)
}
fn new(asset: String, side: Side) -> Self {
Self {
asset,
side,
size: None,
notional: None,
price: None,
tif: TIF::Ioc,
reduce_only: false,
cloid: None,
priority_fee: None,
}
}
pub fn size(mut self, size: f64) -> Self {
self.size = Some(Decimal::from_f64_retain(size).unwrap_or_default());
self
}
pub fn size_decimal(mut self, size: Decimal) -> Self {
self.size = Some(size);
self
}
pub fn notional(mut self, notional: f64) -> Self {
self.notional = Some(Decimal::from_f64_retain(notional).unwrap_or_default());
self
}
pub fn price(mut self, price: f64) -> Self {
self.price = Some(Decimal::from_f64_retain(price).unwrap_or_default());
self
}
pub fn limit(self, price: f64) -> Self {
self.price(price)
}
pub fn market(mut self) -> Self {
self.tif = TIF::Market;
self
}
pub fn ioc(mut self) -> Self {
self.tif = TIF::Ioc;
self
}
pub fn gtc(mut self) -> Self {
self.tif = TIF::Gtc;
self
}
pub fn alo(mut self) -> Self {
self.tif = TIF::Alo;
self
}
pub fn post_only(self) -> Self {
self.alo()
}
pub fn reduce_only(mut self) -> Self {
self.reduce_only = true;
self
}
pub fn cloid(mut self, cloid: impl Into<String>) -> Self {
let cloid_str = cloid.into();
if let Ok(parsed) = cloid_str.parse::<B128>() {
self.cloid = Some(parsed);
}
self
}
pub fn cloid_bytes(mut self, cloid: [u8; 16]) -> Self {
self.cloid = Some(B128::from(cloid));
self
}
pub fn random_cloid(mut self) -> Self {
let mut bytes = [0u8; 16];
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let nanos = now.as_nanos() as u64;
bytes[0..8].copy_from_slice(&nanos.to_le_bytes());
bytes[8..16].copy_from_slice(&(nanos.wrapping_mul(0x517cc1b727220a95)).to_le_bytes());
self.cloid = Some(B128::from(bytes));
self
}
pub fn priority_fee(mut self, p: u64) -> Self {
self.priority_fee = Some(p);
self
}
pub fn get_asset(&self) -> &str {
&self.asset
}
pub fn get_side(&self) -> Side {
self.side
}
pub fn get_size(&self) -> Option<Decimal> {
self.size
}
pub fn get_notional(&self) -> Option<Decimal> {
self.notional
}
pub fn get_price(&self) -> Option<Decimal> {
self.price
}
pub fn get_tif(&self) -> TIF {
self.tif
}
pub fn is_reduce_only(&self) -> bool {
self.reduce_only
}
pub fn is_market(&self) -> bool {
self.tif == TIF::Market || self.price.is_none()
}
pub fn get_cloid(&self) -> Option<Cloid> {
self.cloid
}
pub fn get_priority_fee(&self) -> Option<u64> {
self.priority_fee
}
pub fn validate(&self) -> crate::Result<()> {
if self.size.is_none() && self.notional.is_none() {
return Err(crate::Error::ValidationError(
"Order must have either size or notional".to_string(),
));
}
if self.tif != TIF::Market && self.price.is_none() && self.notional.is_none() {
return Err(crate::Error::ValidationError(
"Non-market orders must have a price".to_string(),
));
}
Ok(())
}
pub fn to_request(&self, asset_index: usize, resolved_price: Decimal) -> OrderRequest {
let cloid = self.cloid.unwrap_or_else(|| {
let mut bytes = [0u8; 16];
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let nanos = now.as_nanos() as u64;
bytes[0..8].copy_from_slice(&nanos.to_le_bytes());
bytes[8..16].copy_from_slice(&(nanos.wrapping_mul(0x517cc1b727220a95)).to_le_bytes());
B128::from(bytes)
});
OrderRequest {
asset: asset_index,
is_buy: self.side.is_buy(),
limit_px: resolved_price,
sz: self.size.unwrap_or_default(),
reduce_only: self.reduce_only,
order_type: OrderTypePlacement::Limit {
tif: TimeInForce::from(self.tif),
},
cloid,
}
}
}
#[derive(Debug, Clone)]
pub struct TriggerOrder {
asset: String,
tpsl: TpSl,
side: Side,
size: Option<Decimal>,
trigger_price: Option<Decimal>,
limit_price: Option<Decimal>,
is_market: bool,
reduce_only: bool,
cloid: Option<Cloid>,
}
impl TriggerOrder {
pub fn stop_loss(asset: impl Into<String>) -> Self {
Self::new(asset.into(), TpSl::Sl)
}
pub fn sl(asset: impl Into<String>) -> Self {
Self::stop_loss(asset)
}
pub fn take_profit(asset: impl Into<String>) -> Self {
Self::new(asset.into(), TpSl::Tp)
}
pub fn tp(asset: impl Into<String>) -> Self {
Self::take_profit(asset)
}
fn new(asset: String, tpsl: TpSl) -> Self {
Self {
asset,
tpsl,
side: Side::Sell, size: None,
trigger_price: None,
limit_price: None,
is_market: true,
reduce_only: true, cloid: None,
}
}
pub fn side(mut self, side: Side) -> Self {
self.side = side;
self
}
pub fn buy(mut self) -> Self {
self.side = Side::Buy;
self
}
pub fn sell(mut self) -> Self {
self.side = Side::Sell;
self
}
pub fn size(mut self, size: f64) -> Self {
self.size = Some(Decimal::from_f64_retain(size).unwrap_or_default());
self
}
pub fn trigger_price(mut self, price: f64) -> Self {
self.trigger_price = Some(Decimal::from_f64_retain(price).unwrap_or_default());
self
}
pub fn trigger(self, price: f64) -> Self {
self.trigger_price(price)
}
pub fn market(mut self) -> Self {
self.is_market = true;
self.limit_price = None;
self
}
pub fn limit(mut self, price: f64) -> Self {
self.is_market = false;
self.limit_price = Some(Decimal::from_f64_retain(price).unwrap_or_default());
self
}
pub fn reduce_only(mut self) -> Self {
self.reduce_only = true;
self
}
pub fn not_reduce_only(mut self) -> Self {
self.reduce_only = false;
self
}
pub fn cloid(mut self, cloid: impl Into<String>) -> Self {
let cloid_str = cloid.into();
if let Ok(parsed) = cloid_str.parse::<B128>() {
self.cloid = Some(parsed);
}
self
}
pub fn get_asset(&self) -> &str {
&self.asset
}
pub fn get_tpsl(&self) -> TpSl {
self.tpsl
}
pub fn get_side(&self) -> Side {
self.side
}
pub fn get_size(&self) -> Option<Decimal> {
self.size
}
pub fn get_trigger_price(&self) -> Option<Decimal> {
self.trigger_price
}
pub fn get_limit_price(&self) -> Option<Decimal> {
self.limit_price
}
pub fn is_market(&self) -> bool {
self.is_market
}
pub fn is_reduce_only(&self) -> bool {
self.reduce_only
}
pub fn validate(&self) -> crate::Result<()> {
if self.size.is_none() {
return Err(crate::Error::ValidationError(
"Trigger order must have a size".to_string(),
));
}
if self.trigger_price.is_none() {
return Err(crate::Error::ValidationError(
"Trigger order must have a trigger price".to_string(),
));
}
Ok(())
}
pub fn to_request(&self, asset_index: usize, execution_price: Decimal) -> OrderRequest {
OrderRequest {
asset: asset_index,
is_buy: self.side.is_buy(),
limit_px: self.limit_price.unwrap_or(execution_price),
sz: self.size.unwrap_or_default(),
reduce_only: self.reduce_only,
order_type: OrderTypePlacement::Trigger {
is_market: self.is_market,
trigger_px: self.trigger_price.unwrap_or_default(),
tpsl: self.tpsl,
},
cloid: self.cloid.unwrap_or(B128::ZERO),
}
}
}
#[derive(Debug, Clone)]
pub struct PlacedOrder {
pub oid: Option<u64>,
pub status: String,
pub asset: String,
pub side: String,
pub size: String,
pub price: Option<String>,
pub filled_size: Option<String>,
pub avg_price: Option<String>,
pub error: Option<String>,
pub raw_response: serde_json::Value,
sdk: Option<Arc<crate::client::HyperliquidSDKInner>>,
}
impl PlacedOrder {
pub(crate) fn from_response(
response: serde_json::Value,
asset: String,
side: Side,
size: Decimal,
price: Option<Decimal>,
sdk: Option<Arc<crate::client::HyperliquidSDKInner>>,
) -> Self {
let status = response
.get("status")
.and_then(|s| s.as_str())
.unwrap_or("unknown")
.to_string();
let mut oid = None;
let mut filled_size = None;
let mut avg_price = None;
let mut error = None;
if status == "ok" {
if let Some(data) = response
.get("response")
.and_then(|r| r.get("data"))
.and_then(|d| d.get("statuses"))
.and_then(|s| s.get(0))
{
if let Some(resting) = data.get("resting") {
oid = resting.get("oid").and_then(|o| o.as_u64());
}
if let Some(filled) = data.get("filled") {
oid = filled.get("oid").and_then(|o| o.as_u64());
filled_size = filled
.get("totalSz")
.and_then(|s| s.as_str())
.map(|s| s.to_string());
avg_price = filled
.get("avgPx")
.and_then(|p| p.as_str())
.map(|s| s.to_string());
}
if let Some(err) = data.get("error") {
error = err.as_str().map(|s| s.to_string());
}
}
} else if status == "err" {
error = response
.get("response")
.and_then(|r| r.as_str())
.map(|s| s.to_string());
}
let status_str = if oid.is_some() {
if filled_size.is_some() {
"filled"
} else {
"resting"
}
} else if error.is_some() {
"error"
} else {
"unknown"
};
Self {
oid,
status: status_str.to_string(),
asset,
side: side.to_string(),
size: size.to_string(),
price: price.map(|p| p.to_string()),
filled_size,
avg_price,
error,
raw_response: response,
sdk,
}
}
#[allow(dead_code)]
pub(crate) fn error(
asset: String,
side: Side,
size: Decimal,
error: String,
) -> Self {
Self {
oid: None,
status: "error".to_string(),
asset,
side: side.to_string(),
size: size.to_string(),
price: None,
filled_size: None,
avg_price: None,
error: Some(error),
raw_response: serde_json::Value::Null,
sdk: None,
}
}
pub fn is_resting(&self) -> bool {
self.status == "resting"
}
pub fn is_filled(&self) -> bool {
self.status == "filled"
}
pub fn is_error(&self) -> bool {
self.status == "error" || self.error.is_some()
}
pub async fn cancel(&self) -> crate::Result<serde_json::Value> {
let oid = self.oid.ok_or_else(|| {
crate::Error::OrderError("Cannot cancel order without OID".to_string())
})?;
let sdk = self.sdk.as_ref().ok_or_else(|| {
crate::Error::OrderError("SDK reference not available for cancel".to_string())
})?;
sdk.cancel_by_oid(oid, &self.asset).await
}
pub async fn modify(
&self,
price: Option<f64>,
size: Option<f64>,
) -> crate::Result<PlacedOrder> {
let oid = self.oid.ok_or_else(|| {
crate::Error::OrderError("Cannot modify order without OID".to_string())
})?;
let sdk = self.sdk.as_ref().ok_or_else(|| {
crate::Error::OrderError("SDK reference not available for modify".to_string())
})?;
let new_price = price
.map(|p| Decimal::from_f64_retain(p).unwrap_or_default())
.or_else(|| self.price.as_ref().and_then(|p| Decimal::from_str(p).ok()));
let new_size = size
.map(|s| Decimal::from_f64_retain(s).unwrap_or_default())
.or_else(|| Decimal::from_str(&self.size).ok());
sdk.modify_by_oid(
oid,
&self.asset,
Side::from_str(&self.side).unwrap_or(Side::Buy),
new_price.unwrap_or_default(),
new_size.unwrap_or_default(),
)
.await
}
}