1use crate::trade::error::TradeError;
20use crate::trade::orders::fit::{Fit, worst_price};
21use crate::trade::orders::{MOST_EXPIRY, Order};
22use crate::utils::time::{DAY_MS, MAX_ORDER_EXPIRY_MS, MIN_ORDER_EXPIRY_MS, MINUTE_MS, RESTING_MS};
23use crate::utils::{ClientOrderIndex, Dec, Grid, MarketId, OrderKind, Side, TimeInForce};
24
25pub const DEFAULT_SLIPPAGE_BPS: u32 = 200;
27pub const MAX_SLIPPAGE_BPS: u32 = 1_000;
29pub const TRIGGER_SLIPPAGE_BPS: u32 = 200;
31pub const TWAP_LEAST_MS: u64 = 5 * MINUTE_MS;
33pub const TWAP_MOST_MS: u64 = DAY_MS;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum Size {
39 Base(Dec),
41 Quote(Dec),
43 Position,
46}
47
48impl From<Dec> for Size {
49 fn from(base: Dec) -> Size {
50 Size::Base(base)
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub enum Exit {
57 StopLoss,
59 TakeProfit,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64enum Pricing {
65 Limit(Dec),
66 Market,
68 MarketAt(Dec),
69 Trigger {
70 exit: Exit,
71 trigger: Dec,
72 limit: Option<Dec>,
73 },
74 Twap {
75 worst: Dec,
76 until: u64,
77 },
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub struct OrderIntent {
84 market: MarketId,
85 side: Side,
86 size: Size,
87 pricing: Pricing,
88 tif: Option<TimeInForce>,
89 expiry: Option<u64>,
90 slippage_bps: Option<u32>,
91 reference: Option<Dec>,
92 reduce_only: bool,
93 client_index: Option<ClientOrderIndex>,
94 fit: Fit,
95 least: Option<(Dec, Dec)>,
96}
97
98impl OrderIntent {
99 fn new(market: MarketId, side: Side, size: Size, pricing: Pricing) -> OrderIntent {
100 OrderIntent {
101 market,
102 side,
103 size,
104 pricing,
105 tif: None,
106 expiry: None,
107 slippage_bps: None,
108 reference: None,
109 reduce_only: false,
110 client_index: None,
111 fit: Fit::Exact,
112 least: None,
113 }
114 }
115
116 pub fn limit(market: MarketId, side: Side, size: impl Into<Size>, price: Dec) -> OrderIntent {
118 OrderIntent::new(market, side, size.into(), Pricing::Limit(price))
119 }
120
121 pub fn market(market: MarketId, side: Side, size: impl Into<Size>, reference: Dec) -> OrderIntent {
124 let mut intent = OrderIntent::new(market, side, size.into(), Pricing::Market);
125 intent.reference = Some(reference);
126 intent
127 }
128
129 pub fn market_at(market: MarketId, side: Side, size: impl Into<Size>, worst: Dec) -> OrderIntent {
131 OrderIntent::new(market, side, size.into(), Pricing::MarketAt(worst))
132 }
133
134 pub fn stop_loss(market: MarketId, side: Side, size: impl Into<Size>, trigger: Dec) -> OrderIntent {
137 OrderIntent::trigger(market, side, size.into(), Exit::StopLoss, trigger, None)
138 }
139
140 pub fn stop_loss_limit(
142 market: MarketId,
143 side: Side,
144 size: impl Into<Size>,
145 trigger: Dec,
146 limit: Dec,
147 ) -> OrderIntent {
148 OrderIntent::trigger(market, side, size.into(), Exit::StopLoss, trigger, Some(limit))
149 }
150
151 pub fn take_profit(market: MarketId, side: Side, size: impl Into<Size>, trigger: Dec) -> OrderIntent {
153 OrderIntent::trigger(market, side, size.into(), Exit::TakeProfit, trigger, None)
154 }
155
156 pub fn take_profit_limit(
158 market: MarketId,
159 side: Side,
160 size: impl Into<Size>,
161 trigger: Dec,
162 limit: Dec,
163 ) -> OrderIntent {
164 OrderIntent::trigger(market, side, size.into(), Exit::TakeProfit, trigger, Some(limit))
165 }
166
167 pub fn trigger(
169 market: MarketId,
170 side: Side,
171 size: impl Into<Size>,
172 exit: Exit,
173 trigger: Dec,
174 limit: Option<Dec>,
175 ) -> OrderIntent {
176 OrderIntent::new(market, side, size.into(), Pricing::Trigger { exit, trigger, limit })
177 }
178
179 pub fn twap(market: MarketId, side: Side, size: impl Into<Size>, worst: Dec, until: u64) -> OrderIntent {
182 OrderIntent::new(market, side, size.into(), Pricing::Twap { worst, until })
183 }
184
185 #[must_use]
188 pub const fn post_only(mut self) -> OrderIntent {
189 self.tif = Some(TimeInForce::PostOnly);
190 self
191 }
192
193 #[must_use]
195 pub const fn ioc(mut self) -> OrderIntent {
196 self.tif = Some(TimeInForce::ImmediateOrCancel);
197 self
198 }
199
200 #[must_use]
202 pub const fn good_till_time(mut self) -> OrderIntent {
203 self.tif = Some(TimeInForce::GoodTillTime);
204 self
205 }
206
207 #[must_use]
210 pub const fn expires_at(mut self, at: u64) -> OrderIntent {
211 self.expiry = Some(at);
212 self
213 }
214
215 #[must_use]
217 pub const fn reduce_only(mut self) -> OrderIntent {
218 self.reduce_only = true;
219 self
220 }
221
222 #[must_use]
224 pub const fn client_index(mut self, index: ClientOrderIndex) -> OrderIntent {
225 self.client_index = Some(index);
226 self
227 }
228
229 #[must_use]
232 pub const fn slippage_bps(mut self, bps: u32) -> OrderIntent {
233 self.slippage_bps = Some(bps);
234 self
235 }
236
237 #[must_use]
240 pub const fn reference(mut self, reference: Dec) -> OrderIntent {
241 self.reference = Some(reference);
242 self
243 }
244
245 #[must_use]
247 pub const fn fit(mut self, fit: Fit) -> OrderIntent {
248 self.fit = fit;
249 self
250 }
251
252 #[must_use]
255 pub const fn minimums(mut self, least_size: Dec, least_value: Dec) -> OrderIntent {
256 self.least = Some((least_size, least_value));
257 self
258 }
259
260 #[must_use]
261 pub const fn market_id(&self) -> MarketId {
262 self.market
263 }
264
265 #[must_use]
266 pub const fn side(&self) -> Side {
267 self.side
268 }
269
270 #[must_use]
271 pub const fn size(&self) -> Size {
272 self.size
273 }
274
275 pub fn build(&self, grid: Grid, now_ms: u64) -> Result<Order, TradeError> {
277 let (kind, time_in_force) = self.kind()?;
278 self.check_slippage()?;
279 let fit = self.fit;
280 let (price, trigger) = match self.pricing {
281 Pricing::Limit(price) => (fit.price(grid, price, self.side)?, None),
282 Pricing::Market => {
283 let reference = self.reference.ok_or(TradeError::NoPrice("a market order"))?;
284 let bps = self.slippage_bps.unwrap_or(DEFAULT_SLIPPAGE_BPS);
285 (fit_bound(grid, self.side, reference, bps)?, None)
286 }
287 Pricing::MarketAt(worst) => {
288 let price = fit.price(grid, worst, self.side)?;
289 if let Some(reference) = self.reference {
290 let most = worst_price(grid, self.side, reference, MAX_SLIPPAGE_BPS)?;
291 let worst = grid.price_of(price);
292 let too_far = match self.side {
293 Side::Bid => worst > most,
294 Side::Ask => worst < most,
295 };
296 if too_far {
297 return Err(TradeError::TooFar { worst, reference });
298 }
299 }
300 (price, None)
301 }
302 Pricing::Trigger { trigger, limit, .. } => {
303 let trigger = fit.trigger(grid, trigger)?;
304 let price = match limit {
305 Some(limit) => fit.price(grid, limit, self.side)?,
306 None => {
307 let bps = self.slippage_bps.unwrap_or(TRIGGER_SLIPPAGE_BPS);
308 fit_bound(grid, self.side, grid.price_of(trigger), bps)?
309 }
310 };
311 (price, Some(trigger))
312 }
313 Pricing::Twap { worst, .. } => (fit.price(grid, worst, self.side)?, None),
314 };
315 let base_amount = self.base_amount(grid, price, trigger)?;
316 let order = Order {
317 market: self.market,
318 client_index: self.client_index,
319 base_amount,
320 price,
321 side: self.side,
322 kind,
323 time_in_force,
324 reduce_only: self.reduce_only,
325 trigger_price: trigger.unwrap_or(0),
326 expiry: 0,
327 };
328 let order = Order { expiry: self.expiry(&order, now_ms)?, ..order };
329 self.check_minimums(&order, grid)?;
330 order.check()?;
331 Ok(order)
332 }
333
334 fn kind(&self) -> Result<(OrderKind, TimeInForce), TradeError> {
336 let only = |tif: TimeInForce, why: &'static str| match self.tif {
337 None => Ok(tif),
338 Some(asked) if asked == tif => Ok(tif),
339 Some(_) => Err(TradeError::NotFor(why)),
340 };
341 Ok(match self.pricing {
342 Pricing::Limit(_) => (OrderKind::Limit, self.tif.unwrap_or(TimeInForce::GoodTillTime)),
343 Pricing::Market | Pricing::MarketAt(_) => {
344 (OrderKind::Market, only(TimeInForce::ImmediateOrCancel, "a market order is immediate-or-cancel")?)
345 }
346 Pricing::Trigger { exit, limit: None, .. } => {
347 let kind = match exit {
348 Exit::StopLoss => OrderKind::StopLoss,
349 Exit::TakeProfit => OrderKind::TakeProfit,
350 };
351 (kind, only(TimeInForce::ImmediateOrCancel, "a stop or take-profit at market is immediate-or-cancel")?)
352 }
353 Pricing::Trigger { exit, limit: Some(_), .. } => {
354 let kind = match exit {
355 Exit::StopLoss => OrderKind::StopLossLimit,
356 Exit::TakeProfit => OrderKind::TakeProfitLimit,
357 };
358 (kind, self.tif.unwrap_or(TimeInForce::GoodTillTime))
359 }
360 Pricing::Twap { .. } => (OrderKind::Twap, only(TimeInForce::GoodTillTime, "a TWAP is good-till-time")?),
361 })
362 }
363
364 fn check_slippage(&self) -> Result<(), TradeError> {
366 let Some(bps) = self.slippage_bps else { return Ok(()) };
367 match self.pricing {
368 Pricing::Market | Pricing::Trigger { limit: None, .. } => {}
369 _ => return Err(TradeError::NotFor("slippage is for a market order or a stop or take-profit at market")),
370 }
371 match bps > MAX_SLIPPAGE_BPS {
372 true => Err(TradeError::Slippage(bps)),
373 false => Ok(()),
374 }
375 }
376
377 fn base_amount(&self, grid: Grid, price: u32, trigger: Option<u32>) -> Result<u64, TradeError> {
380 match self.size {
381 Size::Base(size) => self.fit.size(grid, size),
382 Size::Quote(quote) => {
383 let at = match self.pricing {
384 Pricing::Market => self.reference.ok_or(TradeError::NoPrice("a quote-sized market order"))?,
385 Pricing::Trigger { limit: None, .. } => grid.price_of(trigger.unwrap_or(price)),
386 _ => grid.price_of(price),
387 };
388 self.fit.size_for(grid, quote, at)
389 }
390 Size::Position if self.reduce_only => Ok(0),
391 Size::Position => Err(TradeError::NotFor("the whole position is a size for a reduce-only order")),
392 }
393 }
394
395 fn expiry(&self, order: &Order, now_ms: u64) -> Result<u64, TradeError> {
398 let immediate = matches!(order.kind, OrderKind::Market)
399 || matches!((order.kind, order.time_in_force), (OrderKind::Limit, TimeInForce::ImmediateOrCancel));
400 if immediate {
401 return match self.expiry {
402 None => Ok(0),
403 Some(_) => Err(TradeError::NotFor("an order that fills at once has no expiry")),
404 };
405 }
406 if let Pricing::Twap { until, .. } = self.pricing {
407 if self.expiry.is_some() {
408 return Err(TradeError::NotFor("a TWAP lapses when it ends"));
409 }
410 return within("OrderExpiry", "a TWAP's end", until, now_ms, TWAP_LEAST_MS, TWAP_MOST_MS);
411 }
412 match self.expiry {
413 Some(at) => within("OrderExpiry", "an expiry", at, now_ms, MIN_ORDER_EXPIRY_MS, MAX_ORDER_EXPIRY_MS),
414 None => Ok(now_ms.saturating_add(RESTING_MS)),
415 }
416 }
417
418 fn check_minimums(&self, order: &Order, grid: Grid) -> Result<(), TradeError> {
420 let Some((least_size, least_value)) = self.least else { return Ok(()) };
421 if order.takes() || order.base_amount == 0 {
422 return Ok(());
423 }
424 let size = order.size(grid);
425 if size < least_size {
426 return Err(TradeError::BelowLeastSize { size, least: least_size });
427 }
428 let value = size.checked_mul(order.limit(grid)).ok_or(TradeError::NoPrice("the order's value"))?;
429 if value < least_value {
430 return Err(TradeError::BelowLeastValue { value, least: least_value });
431 }
432 Ok(())
433 }
434}
435
436fn fit_bound(grid: Grid, side: Side, reference: Dec, bps: u32) -> Result<u32, TradeError> {
438 let bound = worst_price(grid, side, reference, bps)?;
439 crate::trade::error::units("Price", grid.price_units(bound))
440}
441
442pub(crate) fn within(
444 field: &'static str,
445 what: &'static str,
446 at: u64,
447 now: u64,
448 least_ms: u64,
449 most_ms: u64,
450) -> Result<u64, TradeError> {
451 match at.checked_sub(now) {
452 Some(ahead) if (least_ms..=most_ms).contains(&ahead) && at <= MOST_EXPIRY => Ok(at),
453 _ => Err(TradeError::Expiry { field, what, at, now, least_ms, most_ms }),
454 }
455}
456
457#[cfg(test)]
458#[path = "tests/intent.rs"]
459mod tests;