Skip to main content

df_hyperliquid_core/trade/orders/
trigger.rs

1//! Trigger orders: they rest off the book until the mark crosses the trigger, then fill at
2//! market within a slippage or as a limit. Every take profit and stop loss is one.
3
4use crate::trade::error::TradeError;
5use crate::trade::orders::limit::limit_on;
6use crate::trade::orders::{Order, Side, Tpsl};
7use crate::trade::ticket::{Market, Refusal, positive};
8use crate::utils::rounding::Grid;
9use crate::utils::{Dec, Round};
10
11/// The slippage a market take profit or stop loss takes past its trigger:
12/// the venue's TP/SL page gives market TP/SL orders a tolerance of 10%.
13pub const TRIGGER_SLIPPAGE: Dec = Dec::lit::<1>(1);
14
15/// How a take profit or stop loss fills once its trigger fires.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ExitPrice {
18    /// At market, no worse than `slippage` past the trigger.
19    Market { slippage: Dec },
20    /// A limit at this price.
21    Limit(Dec),
22}
23
24/// A take profit or stop loss: where it fires and how it fills.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct Exit {
27    pub trigger: Dec,
28    pub price: ExitPrice,
29}
30
31impl Exit {
32    /// At market, within the venue's own [`TRIGGER_SLIPPAGE`].
33    #[must_use]
34    pub const fn market(trigger: Dec) -> Exit {
35        Exit::market_within(trigger, TRIGGER_SLIPPAGE)
36    }
37
38    /// At market, within `slippage`.
39    #[must_use]
40    pub const fn market_within(trigger: Dec, slippage: Dec) -> Exit {
41        Exit { trigger, price: ExitPrice::Market { slippage } }
42    }
43
44    /// A limit at `price` once `trigger` fires.
45    #[must_use]
46    pub const fn limit(trigger: Dec, price: Dec) -> Exit {
47        Exit { trigger, price: ExitPrice::Limit(price) }
48    }
49}
50
51/// A trigger on the grid: to the nearest tick, since where it fires favours
52/// neither side.
53pub(crate) fn trigger_on(grid: Grid, trigger: Dec) -> Result<Dec, Refusal> {
54    let trigger = positive(trigger, "a trigger price")?;
55    grid.tick(trigger, Round::Nearest).ok_or(Refusal::Trade(TradeError::NoPrice(trigger)))
56}
57
58/// The price a trigger order carries: its bound past the trigger at market,
59/// or its limit.
60pub(crate) fn fill_price(grid: Grid, trigger: Dec, price: ExitPrice, side: Side) -> Result<(Dec, bool), Refusal> {
61    match price {
62        ExitPrice::Market { slippage } => Ok((grid.market_bound(trigger, slippage, side)?, true)),
63        ExitPrice::Limit(price) => Ok((limit_on(grid, price, side, "a limit price")?, false)),
64    }
65}
66
67/// A reduce-only trigger closing `size` on `side`.
68pub(crate) fn exit_order(market: &Market, side: Side, size: Dec, tpsl: Tpsl, exit: Exit) -> Result<Order, Refusal> {
69    let trigger = trigger_on(market.grid(), exit.trigger)?;
70    let (price, is_market) = fill_price(market.grid(), trigger, exit.price, side)?;
71    Ok(Order::trigger(market.asset(), side, price, size, trigger, tpsl, is_market).reduce_only())
72}
73
74#[cfg(test)]
75#[path = "tests/trigger.rs"]
76mod tests;