df_hyperliquid_core/trade/orders/
tpsl.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Covers {
14 Size(Amount),
16 Position,
19}
20
21#[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
45fn 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
54pub(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
76pub 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;