Skip to main content

df_hyperliquid_core/trade/orders/
entry.rs

1//! An entry: one order at market or at a limit, with its take profit and stop loss grouped
2//! `normalTpsl` when it has them. Each order is worth at least [`MINIMUM_ORDER_VALUE`].
3
4use crate::trade::builder::BuilderSetting;
5use crate::trade::orders::trigger::Exit;
6use crate::trade::orders::{Cloid, Grouping, Order, PlaceOrder, Side, Tif, limit, market, tpsl};
7use crate::trade::ticket::{Amount, Market, Refusal, at_least, routed};
8use crate::utils::Dec;
9use crate::utils::rounding::MINIMUM_ORDER_VALUE;
10
11/// At what price an order goes.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Price {
14    /// `FrontendMarket`, bounded `slippage` (a fraction, 0.01 for 1%) past
15    /// `reference`: the mid, or else the mark.
16    Market { reference: Dec, slippage: Dec },
17    /// A limit at `price`, resting as `tif` says. Never `FrontendMarket`.
18    Limit { price: Dec, tif: Tif },
19}
20
21/// An order to place: a market or limit entry, with its take profit and stop
22/// loss when it has them.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct OrderIntent {
25    pub side: Side,
26    pub amount: Amount,
27    pub price: Price,
28    pub reduce_only: bool,
29    pub cloid: Option<Cloid>,
30    pub take_profit: Option<Exit>,
31    pub stop_loss: Option<Exit>,
32}
33
34impl OrderIntent {
35    pub(super) const fn at(side: Side, amount: Amount, price: Price) -> OrderIntent {
36        OrderIntent { side, amount, price, reduce_only: false, cloid: None, take_profit: None, stop_loss: None }
37    }
38
39    #[must_use]
40    pub const fn with_cloid(mut self, cloid: Cloid) -> OrderIntent {
41        self.cloid = Some(cloid);
42        self
43    }
44
45    /// Placed with the entry, the other way, once it fills.
46    #[must_use]
47    pub const fn take_profit(mut self, exit: Exit) -> OrderIntent {
48        self.take_profit = Some(exit);
49        self
50    }
51
52    /// Placed with the entry, the other way, once it fills.
53    #[must_use]
54    pub const fn stop_loss(mut self, exit: Exit) -> OrderIntent {
55        self.stop_loss = Some(exit);
56        self
57    }
58}
59
60/// An `order`: the entry, and its take profit and stop loss grouped `normalTpsl` when it has them.
61/// Each is worth at least [`MINIMUM_ORDER_VALUE`] in the quote, valued at its limit, at the
62/// reference at market, or at the trigger.
63pub fn order(market: &Market, intent: &OrderIntent, builder: Option<&BuilderSetting>) -> Result<PlaceOrder, Refusal> {
64    let has_exits = intent.take_profit.is_some() || intent.stop_loss.is_some();
65    if market.is_spot() && intent.reduce_only {
66        return Err(Refusal::Spot("reduce only"));
67    }
68    if market.is_spot() && has_exits {
69        return Err(Refusal::Spot("a take profit or stop loss"));
70    }
71    let grid = market.grid();
72    let side = intent.side;
73    let (price, valued_at, tif) = match intent.price {
74        Price::Market { reference, slippage } => market::bound(grid, reference, slippage, side)?,
75        Price::Limit { price, tif } => limit::resting(grid, price, tif, side)?,
76    };
77    let size = intent.amount.size_on(grid, valued_at)?;
78    at_least(size, valued_at, MINIMUM_ORDER_VALUE)?;
79    let mut entry = match tif {
80        Tif::FrontendMarket => Order::market(market.asset(), side, price, size),
81        tif => Order::limit(market.asset(), side, price, size, tif),
82    };
83    if intent.reduce_only {
84        entry = entry.reduce_only();
85    }
86    if let Some(cloid) = intent.cloid {
87        entry = entry.with_cloid(cloid);
88    }
89    let mut orders = vec![entry];
90    orders.extend(tpsl::attach(market, side, size, valued_at, intent.take_profit, intent.stop_loss)?);
91    let grouping = match has_exits {
92        true => Grouping::NormalTpsl,
93        false => Grouping::Na,
94    };
95    routed(PlaceOrder::new(orders).grouped(grouping), builder)
96}
97
98#[cfg(test)]
99#[path = "tests/entry.rs"]
100mod tests;