Skip to main content

df_hyperliquid_core/trade/orders/
tpsl.rs

1//! Take profit and stop loss: on their own for a fixed size (`na`) or the whole position
2//! (`positionTpsl`), or attached to an entry (`normalTpsl`).
3
4use crate::trade::builder::BuilderSetting;
5use crate::trade::orders::trigger::{Exit, exit_order, trigger_on};
6use crate::trade::orders::{Cloid, Grouping, Order, PlaceOrder, Side, Tpsl};
7use crate::trade::ticket::{Amount, Market, Refusal, at_least, routed};
8use crate::utils::Dec;
9use crate::utils::rounding::MINIMUM_ORDER_VALUE;
10
11/// How much of a position a take profit or stop loss closes.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Covers {
14    /// A fixed size.
15    Size(Amount),
16    /// The whole position, whatever it is when the trigger fires: size 0,
17    /// grouped `positionTpsl`.
18    Position,
19}
20
21/// A take profit or stop loss on its own. `side` is the side that closes:
22/// a sell for a long.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct TriggerIntent {
25    pub side: Side,
26    pub size: Covers,
27    pub tpsl: Tpsl,
28    pub exit: Exit,
29    pub cloid: Option<Cloid>,
30}
31
32impl TriggerIntent {
33    #[must_use]
34    pub const fn new(side: Side, size: Covers, tpsl: Tpsl, exit: Exit) -> TriggerIntent {
35        TriggerIntent { side, size, tpsl, exit, cloid: None }
36    }
37
38    #[must_use]
39    pub const fn with_cloid(mut self, cloid: Cloid) -> TriggerIntent {
40        self.cloid = Some(cloid);
41        self
42    }
43}
44
45/// Whether `exit` fires on the side of `entry` it should: above it for a
46/// long's take profit and a short's stop, below it otherwise.
47fn on_its_side(side: Side, tpsl: Tpsl, trigger: Dec, entry: Dec) -> bool {
48    match (side, tpsl) {
49        (Side::Buy, Tpsl::TakeProfit) | (Side::Sell, Tpsl::StopLoss) => trigger > entry,
50        (Side::Buy, Tpsl::StopLoss) | (Side::Sell, Tpsl::TakeProfit) => trigger < entry,
51    }
52}
53
54/// An entry's take profit and stop loss: each on its side of the entry, worth the minimum at its
55/// trigger, reduce only and the other way.
56pub(crate) fn attach(
57    market: &Market,
58    side: Side,
59    size: Dec,
60    entry: Dec,
61    take_profit: Option<Exit>,
62    stop_loss: Option<Exit>,
63) -> Result<Vec<Order>, Refusal> {
64    let mut exits = Vec::new();
65    for (tpsl, exit) in [(Tpsl::TakeProfit, take_profit), (Tpsl::StopLoss, stop_loss)] {
66        let Some(exit) = exit else { continue };
67        if !on_its_side(side, tpsl, exit.trigger, entry) {
68            return Err(Refusal::ExitSide { tpsl, trigger: exit.trigger, entry });
69        }
70        at_least(size, trigger_on(market.grid(), exit.trigger)?, MINIMUM_ORDER_VALUE)?;
71        exits.push(exit_order(market, side.other(), size, tpsl, exit)?);
72    }
73    Ok(exits)
74}
75
76/// A take profit or stop loss on its own: reduce only, a fixed size grouped
77/// `na`, or the whole position at size 0 grouped `positionTpsl`.
78pub fn trigger(
79    market: &Market,
80    intent: &TriggerIntent,
81    builder: Option<&BuilderSetting>,
82) -> Result<PlaceOrder, Refusal> {
83    if market.is_spot() {
84        return Err(Refusal::Spot("a take profit or stop loss"));
85    }
86    let (size, grouping) = match intent.size {
87        Covers::Position => (Dec::ZERO, Grouping::PositionTpsl),
88        Covers::Size(amount) => {
89            let trigger = trigger_on(market.grid(), intent.exit.trigger)?;
90            let size = amount.size_on(market.grid(), trigger)?;
91            at_least(size, trigger, MINIMUM_ORDER_VALUE)?;
92            (size, Grouping::Na)
93        }
94    };
95    let mut order = exit_order(market, intent.side, size, intent.tpsl, intent.exit)?;
96    if let Some(cloid) = intent.cloid {
97        order = order.with_cloid(cloid);
98    }
99    routed(PlaceOrder::one(order).grouped(grouping), builder)
100}
101
102#[cfg(test)]
103#[path = "tests/tpsl.rs"]
104mod tests;