df_hyperliquid_core/trade/
ticket.rs1use crate::trade::builder::BuilderSetting;
6use crate::trade::error::TradeError;
7use crate::trade::orders::scale::{LEAST_LEVELS, MOST_LEVELS};
8use crate::trade::orders::{Order, PlaceOrder, Tif, Tpsl};
9use crate::utils::rounding::Grid;
10use crate::utils::{Asset, AssetKind, Dec, Round};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct Market {
17 asset: Asset,
18 grid: Grid,
19 max_leverage: Option<u32>,
20 allows_cross: bool,
21}
22
23impl Market {
24 #[must_use]
28 pub const fn perp(asset: Asset, sz_decimals: u32, max_leverage: u32, allows_cross: bool) -> Option<Market> {
29 match asset.kind() {
30 AssetKind::Perp | AssetKind::Hip3Perp => {
31 Some(Market { asset, grid: Grid::perp(sz_decimals), max_leverage: Some(max_leverage), allows_cross })
32 }
33 AssetKind::Spot | AssetKind::Outcome => None,
34 }
35 }
36
37 #[must_use]
40 pub const fn spot(asset: Asset, sz_decimals: u32) -> Option<Market> {
41 match asset.kind() {
42 AssetKind::Spot => {
43 Some(Market { asset, grid: Grid::spot(sz_decimals), max_leverage: None, allows_cross: false })
44 }
45 AssetKind::Perp | AssetKind::Hip3Perp | AssetKind::Outcome => None,
46 }
47 }
48
49 #[must_use]
50 pub const fn asset(&self) -> Asset {
51 self.asset
52 }
53
54 #[must_use]
55 pub const fn grid(&self) -> Grid {
56 self.grid
57 }
58
59 #[must_use]
61 pub const fn max_leverage(&self) -> Option<u32> {
62 self.max_leverage
63 }
64
65 #[must_use]
66 pub const fn allows_cross(&self) -> bool {
67 self.allows_cross
68 }
69
70 #[must_use]
73 pub const fn is_spot(&self) -> bool {
74 self.max_leverage.is_none()
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum Amount {
82 Size(Dec),
83 Notional(Dec),
84}
85
86impl Amount {
87 pub(crate) fn size_on(self, grid: Grid, price: Dec) -> Result<Dec, Refusal> {
89 let raw = match self {
90 Amount::Size(size) => positive(size, "a size")?,
91 Amount::Notional(notional) => positive(notional, "a notional")?
92 .checked_div(price, 12, Round::Down)
93 .ok_or(Refusal::Trade(TradeError::NoPrice(price)))?,
94 };
95 grid.size(raw).ok_or(Refusal::Trade(TradeError::NoSize(raw)))
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
101pub enum Refusal {
102 #[error(transparent)]
103 Trade(#[from] TradeError),
104 #[error("{what} above zero is needed, not {value}")]
105 NotPositive { what: &'static str, value: Dec },
106 #[error("{size} at {price} is worth {}, under the venue's minimum of {minimum}", worth(.size, .price))]
107 UnderMinimum { size: Dec, price: Dec, minimum: Dec },
108 #[error("a limit is never FrontendMarket: a market order is Price::Market, bounded from the mid")]
109 MarketTif,
110 #[error("a scale rests on the book, so Gtc or Alo, not {}", .0.wire())]
111 ScaleTif(Tif),
112 #[error("a scale has {LEAST_LEVELS} to {MOST_LEVELS} levels, not {0}")]
113 Levels(u32),
114 #[error("a modify that does not always place is Alo or Gtc, not {}", .0.wire())]
115 ModifyTif(Tif),
116 #[error("spot takes no {0}")]
117 Spot(&'static str),
118 #[error("a notional needs a reference price to be sized at")]
119 NoReference,
120 #[error("a {} at {trigger} is on the wrong side of an entry at {entry}", tpsl_words(.tpsl))]
121 ExitSide { tpsl: Tpsl, trigger: Dec, entry: Dec },
122 #[error("there is no position here")]
123 NoPosition,
124 #[error("closing {size} is more than the position of {position}")]
125 CloseTooLarge { size: Dec, position: Dec },
126 #[error("leverage is a whole number from 1 to {most}, not {asked}")]
127 Leverage { asked: u32, most: u32 },
128 #[error("this market is isolated only")]
129 IsolatedOnly,
130 #[error("this position is cross; margin moves on isolated ones")]
131 CrossPosition,
132 #[error("order {0} is a trigger: change its trigger instead")]
133 NotALimit(u64),
134 #[error("order {0} is not a take profit or stop loss")]
135 NotATrigger(u64),
136 #[error("order {oid} is a {kind} trigger, which this build cannot modify")]
137 UnknownTrigger { oid: u64, kind: String },
138 #[error("order {0} has a size of its own; only a position's take profit or stop loss covers the whole position")]
139 NotWholePosition(u64),
140}
141
142fn worth(size: &Dec, price: &Dec) -> String {
143 size.checked_mul(*price).map_or_else(|| "too much to count".to_owned(), Dec::to_wire)
144}
145
146const fn tpsl_words(tpsl: &Tpsl) -> &'static str {
147 match tpsl {
148 Tpsl::TakeProfit => "take profit",
149 Tpsl::StopLoss => "stop loss",
150 }
151}
152
153pub(crate) fn positive(value: Dec, what: &'static str) -> Result<Dec, Refusal> {
154 match value.is_positive() {
155 true => Ok(value),
156 false => Err(Refusal::NotPositive { what, value }),
157 }
158}
159
160pub(crate) fn at_least(size: Dec, price: Dec, minimum: Dec) -> Result<(), Refusal> {
161 match price.checked_mul(size).is_some_and(|worth| worth >= minimum) {
162 true => Ok(()),
163 false => Err(Refusal::UnderMinimum { size, price, minimum }),
164 }
165}
166
167pub(crate) fn routed(place: PlaceOrder, builder: Option<&BuilderSetting>) -> Result<PlaceOrder, Refusal> {
169 let Some(setting) = builder else { return Ok(place) };
170 match setting.on_assets(place.orders().iter().map(Order::asset)).map_err(TradeError::from)? {
171 Some(builder) => Ok(place.through(builder)),
172 None => Ok(place),
173 }
174}
175
176#[cfg(test)]
177#[path = "tests/ticket.rs"]
178mod tests;