1use crate::account::orders::FrontendOrder;
6use crate::trade::builder::BuilderSetting;
7use crate::trade::error::TradeError;
8use crate::trade::leverage::{MarginMode, UpdateIsolatedMargin, UpdateLeverage};
9use crate::trade::modify::{BatchModify, Modify, OrderRef};
10use crate::trade::order::{Cloid, Grouping, Order, PlaceOrder, Side, Tif, Tpsl};
11use crate::trade::twap::Twap;
12use crate::utils::rounding::{Grid, MINIMUM_ORDER_VALUE};
13use crate::utils::{Asset, AssetKind, Dec, Round};
14
15pub const TRIGGER_SLIPPAGE: Dec = Dec::lit::<1>(1);
18
19pub const MINIMUM_TWAP_VALUE: Dec = Dec::from_int(100);
21
22pub const LEAST_LEVELS: u32 = 2;
24
25pub const MOST_LEVELS: u32 = 50;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub struct Market {
33 asset: Asset,
34 grid: Grid,
35 max_leverage: Option<u32>,
36 allows_cross: bool,
37}
38
39impl Market {
40 #[must_use]
44 pub const fn perp(asset: Asset, sz_decimals: u32, max_leverage: u32, allows_cross: bool) -> Option<Market> {
45 match asset.kind() {
46 AssetKind::Perp | AssetKind::Hip3Perp => {
47 Some(Market { asset, grid: Grid::perp(sz_decimals), max_leverage: Some(max_leverage), allows_cross })
48 }
49 AssetKind::Spot | AssetKind::Outcome => None,
50 }
51 }
52
53 #[must_use]
56 pub const fn spot(asset: Asset, sz_decimals: u32) -> Option<Market> {
57 match asset.kind() {
58 AssetKind::Spot => {
59 Some(Market { asset, grid: Grid::spot(sz_decimals), max_leverage: None, allows_cross: false })
60 }
61 AssetKind::Perp | AssetKind::Hip3Perp | AssetKind::Outcome => None,
62 }
63 }
64
65 #[must_use]
66 pub const fn asset(&self) -> Asset {
67 self.asset
68 }
69
70 #[must_use]
71 pub const fn grid(&self) -> Grid {
72 self.grid
73 }
74
75 #[must_use]
77 pub const fn max_leverage(&self) -> Option<u32> {
78 self.max_leverage
79 }
80
81 #[must_use]
82 pub const fn allows_cross(&self) -> bool {
83 self.allows_cross
84 }
85
86 #[must_use]
89 pub const fn is_spot(&self) -> bool {
90 self.max_leverage.is_none()
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum Amount {
98 Size(Dec),
99 Notional(Dec),
100}
101
102impl Amount {
103 fn size_on(self, grid: Grid, price: Dec) -> Result<Dec, Refusal> {
105 let raw = match self {
106 Amount::Size(size) => positive(size, "a size")?,
107 Amount::Notional(notional) => positive(notional, "a notional")?
108 .checked_div(price, 12, Round::Down)
109 .ok_or(Refusal::Trade(TradeError::NoPrice(price)))?,
110 };
111 grid.size(raw).ok_or(Refusal::Trade(TradeError::NoSize(raw)))
112 }
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum Price {
118 Market { reference: Dec, slippage: Dec },
121 Limit { price: Dec, tif: Tif },
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum ExitPrice {
128 Market { slippage: Dec },
130 Limit(Dec),
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub struct Exit {
137 pub trigger: Dec,
138 pub price: ExitPrice,
139}
140
141impl Exit {
142 #[must_use]
144 pub const fn market(trigger: Dec) -> Exit {
145 Exit::market_within(trigger, TRIGGER_SLIPPAGE)
146 }
147
148 #[must_use]
150 pub const fn market_within(trigger: Dec, slippage: Dec) -> Exit {
151 Exit { trigger, price: ExitPrice::Market { slippage } }
152 }
153
154 #[must_use]
156 pub const fn limit(trigger: Dec, price: Dec) -> Exit {
157 Exit { trigger, price: ExitPrice::Limit(price) }
158 }
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub struct OrderIntent {
165 pub side: Side,
166 pub amount: Amount,
167 pub price: Price,
168 pub reduce_only: bool,
169 pub cloid: Option<Cloid>,
170 pub take_profit: Option<Exit>,
171 pub stop_loss: Option<Exit>,
172}
173
174impl OrderIntent {
175 #[must_use]
176 pub const fn market(side: Side, amount: Amount, reference: Dec, slippage: Dec) -> OrderIntent {
177 OrderIntent::at(side, amount, Price::Market { reference, slippage })
178 }
179
180 #[must_use]
181 pub const fn limit(side: Side, amount: Amount, price: Dec, tif: Tif) -> OrderIntent {
182 OrderIntent::at(side, amount, Price::Limit { price, tif })
183 }
184
185 const fn at(side: Side, amount: Amount, price: Price) -> OrderIntent {
186 OrderIntent { side, amount, price, reduce_only: false, cloid: None, take_profit: None, stop_loss: None }
187 }
188
189 #[must_use]
190 pub const fn with_cloid(mut self, cloid: Cloid) -> OrderIntent {
191 self.cloid = Some(cloid);
192 self
193 }
194
195 #[must_use]
197 pub const fn take_profit(mut self, exit: Exit) -> OrderIntent {
198 self.take_profit = Some(exit);
199 self
200 }
201
202 #[must_use]
204 pub const fn stop_loss(mut self, exit: Exit) -> OrderIntent {
205 self.stop_loss = Some(exit);
206 self
207 }
208}
209
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
212pub enum Covers {
213 Size(Amount),
215 Position,
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub struct TriggerIntent {
224 pub side: Side,
225 pub size: Covers,
226 pub tpsl: Tpsl,
227 pub exit: Exit,
228 pub cloid: Option<Cloid>,
229}
230
231impl TriggerIntent {
232 #[must_use]
233 pub const fn new(side: Side, size: Covers, tpsl: Tpsl, exit: Exit) -> TriggerIntent {
234 TriggerIntent { side, size, tpsl, exit, cloid: None }
235 }
236
237 #[must_use]
238 pub const fn with_cloid(mut self, cloid: Cloid) -> TriggerIntent {
239 self.cloid = Some(cloid);
240 self
241 }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247pub struct Scale {
248 pub side: Side,
249 pub size: Dec,
251 pub from: Dec,
252 pub to: Dec,
253 pub levels: u32,
254 pub tif: Tif,
255 pub reduce_only: bool,
256}
257
258impl Scale {
259 #[must_use]
260 pub const fn new(side: Side, size: Dec, from: Dec, to: Dec, levels: u32, tif: Tif) -> Scale {
261 Scale { side, size, from, to, levels, tif, reduce_only: false }
262 }
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
268pub struct TwapIntent {
269 pub side: Side,
270 pub amount: Amount,
271 pub minutes: u32,
272 pub randomize: bool,
273 pub reduce_only: bool,
274 pub reference: Option<Dec>,
275}
276
277impl TwapIntent {
278 #[must_use]
279 pub const fn new(side: Side, amount: Amount, minutes: u32) -> TwapIntent {
280 TwapIntent { side, amount, minutes, randomize: false, reduce_only: false, reference: None }
281 }
282
283 #[must_use]
285 pub const fn at(mut self, reference: Dec) -> TwapIntent {
286 self.reference = Some(reference);
287 self
288 }
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
293pub struct Close {
294 pub position: Dec,
297 pub size: Option<Dec>,
298 pub reference: Dec,
299 pub slippage: Dec,
300 pub cloid: Option<Cloid>,
301}
302
303impl Close {
304 #[must_use]
306 pub const fn new(position: Dec, reference: Dec, slippage: Dec) -> Close {
307 Close { position, size: None, reference, slippage, cloid: None }
308 }
309
310 #[must_use]
311 pub const fn with_cloid(mut self, cloid: Cloid) -> Close {
312 self.cloid = Some(cloid);
313 self
314 }
315}
316
317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
319pub enum Change {
320 Limit { price: Dec, size: Amount, tif: Tif },
322 Trigger { trigger: Dec, price: ExitPrice, size: Covers },
324}
325
326#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
328pub enum Refusal {
329 #[error(transparent)]
330 Trade(#[from] TradeError),
331 #[error("{what} above zero is needed, not {value}")]
332 NotPositive { what: &'static str, value: Dec },
333 #[error("{size} at {price} is worth {}, under the venue's minimum of {minimum}", worth(.size, .price))]
334 UnderMinimum { size: Dec, price: Dec, minimum: Dec },
335 #[error("a limit is never FrontendMarket: a market order is Price::Market, bounded from the mid")]
336 MarketTif,
337 #[error("a scale rests on the book, so Gtc or Alo, not {}", .0.wire())]
338 ScaleTif(Tif),
339 #[error("a scale has {LEAST_LEVELS} to {MOST_LEVELS} levels, not {0}")]
340 Levels(u32),
341 #[error("a modify that does not always place is Alo or Gtc, not {}", .0.wire())]
342 ModifyTif(Tif),
343 #[error("spot takes no {0}")]
344 Spot(&'static str),
345 #[error("a notional needs a reference price to be sized at")]
346 NoReference,
347 #[error("a {} at {trigger} is on the wrong side of an entry at {entry}", tpsl_words(.tpsl))]
348 ExitSide { tpsl: Tpsl, trigger: Dec, entry: Dec },
349 #[error("there is no position here")]
350 NoPosition,
351 #[error("closing {size} is more than the position of {position}")]
352 CloseTooLarge { size: Dec, position: Dec },
353 #[error("leverage is a whole number from 1 to {most}, not {asked}")]
354 Leverage { asked: u32, most: u32 },
355 #[error("this market is isolated only")]
356 IsolatedOnly,
357 #[error("this position is cross; margin moves on isolated ones")]
358 CrossPosition,
359 #[error("order {0} is a trigger: change its trigger instead")]
360 NotALimit(u64),
361 #[error("order {0} is not a take profit or stop loss")]
362 NotATrigger(u64),
363 #[error("order {oid} is a {kind} trigger, which this build cannot modify")]
364 UnknownTrigger { oid: u64, kind: String },
365 #[error("order {0} has a size of its own; only a position's take profit or stop loss covers the whole position")]
366 NotWholePosition(u64),
367}
368
369fn worth(size: &Dec, price: &Dec) -> String {
370 size.checked_mul(*price).map_or_else(|| "too much to count".to_owned(), Dec::to_wire)
371}
372
373const fn tpsl_words(tpsl: &Tpsl) -> &'static str {
374 match tpsl {
375 Tpsl::TakeProfit => "take profit",
376 Tpsl::StopLoss => "stop loss",
377 }
378}
379
380fn positive(value: Dec, what: &'static str) -> Result<Dec, Refusal> {
381 match value.is_positive() {
382 true => Ok(value),
383 false => Err(Refusal::NotPositive { what, value }),
384 }
385}
386
387fn at_least(size: Dec, price: Dec, minimum: Dec) -> Result<(), Refusal> {
388 match price.checked_mul(size).is_some_and(|worth| worth >= minimum) {
389 true => Ok(()),
390 false => Err(Refusal::UnderMinimum { size, price, minimum }),
391 }
392}
393
394fn trigger_on(grid: Grid, trigger: Dec) -> Result<Dec, Refusal> {
397 let trigger = positive(trigger, "a trigger price")?;
398 grid.tick(trigger, Round::Nearest).ok_or(Refusal::Trade(TradeError::NoPrice(trigger)))
399}
400
401fn limit_on(grid: Grid, price: Dec, side: Side, what: &'static str) -> Result<Dec, Refusal> {
403 let price = positive(price, what)?;
404 grid.price(price, side).ok_or(Refusal::Trade(TradeError::NoPrice(price)))
405}
406
407fn fill_price(grid: Grid, trigger: Dec, price: ExitPrice, side: Side) -> Result<(Dec, bool), Refusal> {
410 match price {
411 ExitPrice::Market { slippage } => Ok((grid.market_bound(trigger, slippage, side)?, true)),
412 ExitPrice::Limit(price) => Ok((limit_on(grid, price, side, "a limit price")?, false)),
413 }
414}
415
416fn exit_order(market: &Market, side: Side, size: Dec, tpsl: Tpsl, exit: Exit) -> Result<Order, Refusal> {
418 let trigger = trigger_on(market.grid, exit.trigger)?;
419 let (price, is_market) = fill_price(market.grid, trigger, exit.price, side)?;
420 Ok(Order::trigger(market.asset, side, price, size, trigger, tpsl, is_market).reduce_only())
421}
422
423fn routed(place: PlaceOrder, builder: Option<&BuilderSetting>) -> Result<PlaceOrder, Refusal> {
425 let Some(setting) = builder else { return Ok(place) };
426 match setting.on_assets(place.orders().iter().map(Order::asset)).map_err(TradeError::from)? {
427 Some(builder) => Ok(place.through(builder)),
428 None => Ok(place),
429 }
430}
431
432fn on_its_side(side: Side, tpsl: Tpsl, trigger: Dec, entry: Dec) -> bool {
435 match (side, tpsl) {
436 (Side::Buy, Tpsl::TakeProfit) | (Side::Sell, Tpsl::StopLoss) => trigger > entry,
437 (Side::Buy, Tpsl::StopLoss) | (Side::Sell, Tpsl::TakeProfit) => trigger < entry,
438 }
439}
440
441pub fn order(market: &Market, intent: &OrderIntent, builder: Option<&BuilderSetting>) -> Result<PlaceOrder, Refusal> {
445 let has_exits = intent.take_profit.is_some() || intent.stop_loss.is_some();
446 if market.is_spot() && intent.reduce_only {
447 return Err(Refusal::Spot("reduce only"));
448 }
449 if market.is_spot() && has_exits {
450 return Err(Refusal::Spot("a take profit or stop loss"));
451 }
452 let grid = market.grid;
453 let side = intent.side;
454 let (price, valued_at, tif) = match intent.price {
455 Price::Market { reference, slippage } => {
456 let reference = positive(reference, "a reference price")?;
457 (grid.market_bound(reference, slippage, side)?, reference, Tif::FrontendMarket)
458 }
459 Price::Limit { tif: Tif::FrontendMarket, .. } => return Err(Refusal::MarketTif),
460 Price::Limit { price, tif } => {
461 let price = limit_on(grid, price, side, "a price")?;
462 (price, price, tif)
463 }
464 };
465 let size = intent.amount.size_on(grid, valued_at)?;
466 at_least(size, valued_at, MINIMUM_ORDER_VALUE)?;
467 let mut entry = match tif {
468 Tif::FrontendMarket => Order::market(market.asset, side, price, size),
469 tif => Order::limit(market.asset, side, price, size, tif),
470 };
471 if intent.reduce_only {
472 entry = entry.reduce_only();
473 }
474 if let Some(cloid) = intent.cloid {
475 entry = entry.with_cloid(cloid);
476 }
477 let mut orders = vec![entry];
478 for (tpsl, exit) in [(Tpsl::TakeProfit, intent.take_profit), (Tpsl::StopLoss, intent.stop_loss)] {
479 let Some(exit) = exit else { continue };
480 if !on_its_side(side, tpsl, exit.trigger, valued_at) {
481 return Err(Refusal::ExitSide { tpsl, trigger: exit.trigger, entry: valued_at });
482 }
483 at_least(size, trigger_on(grid, exit.trigger)?, MINIMUM_ORDER_VALUE)?;
484 orders.push(exit_order(market, side.other(), size, tpsl, exit)?);
485 }
486 let grouping = match has_exits {
487 true => Grouping::NormalTpsl,
488 false => Grouping::Na,
489 };
490 routed(PlaceOrder::new(orders).grouped(grouping), builder)
491}
492
493pub fn trigger(
496 market: &Market,
497 intent: &TriggerIntent,
498 builder: Option<&BuilderSetting>,
499) -> Result<PlaceOrder, Refusal> {
500 if market.is_spot() {
501 return Err(Refusal::Spot("a take profit or stop loss"));
502 }
503 let (size, grouping) = match intent.size {
504 Covers::Position => (Dec::ZERO, Grouping::PositionTpsl),
505 Covers::Size(amount) => {
506 let trigger = trigger_on(market.grid, intent.exit.trigger)?;
507 let size = amount.size_on(market.grid, trigger)?;
508 at_least(size, trigger, MINIMUM_ORDER_VALUE)?;
509 (size, Grouping::Na)
510 }
511 };
512 let mut order = exit_order(market, intent.side, size, intent.tpsl, intent.exit)?;
513 if let Some(cloid) = intent.cloid {
514 order = order.with_cloid(cloid);
515 }
516 routed(PlaceOrder::one(order).grouped(grouping), builder)
517}
518
519pub fn scale(market: &Market, intent: &Scale, builder: Option<&BuilderSetting>) -> Result<PlaceOrder, Refusal> {
521 if market.is_spot() && intent.reduce_only {
522 return Err(Refusal::Spot("reduce only"));
523 }
524 if !matches!(intent.tif, Tif::Gtc | Tif::Alo) {
525 return Err(Refusal::ScaleTif(intent.tif));
526 }
527 let levels = intent.levels;
528 if !(LEAST_LEVELS..=MOST_LEVELS).contains(&levels) {
529 return Err(Refusal::Levels(levels));
530 }
531 let grid = market.grid;
532 let total = positive(intent.size, "a size")?;
533 let from = positive(intent.from, "a from price")?;
534 let to = positive(intent.to, "a to price")?;
535 let count = Dec::from_int(i64::from(levels));
536 let raw = total.checked_div(count, 12, Round::Down).ok_or(Refusal::Trade(TradeError::NoSize(total)))?;
537 let each = grid.size(raw).ok_or(Refusal::Trade(TradeError::NoSize(raw)))?;
538 let span = to.checked_sub(from).ok_or(Refusal::Trade(TradeError::NoPrice(to)))?;
539 let step = span
540 .checked_div(Dec::from_int(i64::from(levels - 1)), 12, Round::Down)
541 .ok_or(Refusal::Trade(TradeError::NoPrice(to)))?;
542 let mut orders = Vec::new();
543 for at in 0..levels {
544 let raw = match at + 1 == levels {
545 true => to,
546 false => step
547 .checked_mul(Dec::from_int(i64::from(at)))
548 .and_then(|moved| from.checked_add(moved))
549 .ok_or(Refusal::Trade(TradeError::NoPrice(from)))?,
550 };
551 let price = limit_on(grid, raw, intent.side, "a level's price")?;
552 at_least(each, price, MINIMUM_ORDER_VALUE)?;
553 let order = Order::limit(market.asset, intent.side, price, each, intent.tif);
554 orders.push(match intent.reduce_only {
555 true => order.reduce_only(),
556 false => order,
557 });
558 }
559 routed(PlaceOrder::new(orders), builder)
560}
561
562pub fn twap(market: &Market, intent: &TwapIntent) -> Result<Twap, Refusal> {
564 if market.is_spot() && intent.reduce_only {
565 return Err(Refusal::Spot("reduce only"));
566 }
567 let size = match (intent.amount, intent.reference) {
568 (Amount::Notional(_), None) => return Err(Refusal::NoReference),
569 (amount, Some(reference)) => amount.size_on(market.grid, positive(reference, "a reference price")?)?,
570 (Amount::Size(size), None) => {
571 let size = positive(size, "a size")?;
572 market.grid.size(size).ok_or(Refusal::Trade(TradeError::NoSize(size)))?
573 }
574 };
575 if let Some(reference) = intent.reference {
576 at_least(size, reference, MINIMUM_TWAP_VALUE)?;
577 }
578 let twap = Twap::new(market.asset, intent.side, size, intent.minutes)?;
579 let twap = match intent.reduce_only {
580 true => twap.reduce_only(),
581 false => twap,
582 };
583 Ok(match intent.randomize {
584 true => twap.randomized(),
585 false => twap,
586 })
587}
588
589pub fn modify(market: &Market, order: &FrontendOrder, change: &Change) -> Result<BatchModify, Refusal> {
593 let grid = market.grid;
594 let side = order.side;
595 let new = match *change {
596 Change::Limit { .. } if order.is_trigger => return Err(Refusal::NotALimit(order.oid)),
597 Change::Limit { tif, .. } if !matches!(tif, Tif::Alo | Tif::Gtc) => return Err(Refusal::ModifyTif(tif)),
598 Change::Limit { price, size, tif } => {
599 let price = limit_on(grid, price, side, "a new price")?;
600 let size = size.size_on(grid, price)?;
601 at_least(size, price, MINIMUM_ORDER_VALUE)?;
602 Order::limit(market.asset, side, price, size, tif)
603 }
604 Change::Trigger { .. } if !order.is_trigger => return Err(Refusal::NotATrigger(order.oid)),
605 Change::Trigger { trigger, price, size } => {
606 let Some(tpsl) = order.tpsl() else {
607 return Err(Refusal::UnknownTrigger { oid: order.oid, kind: order.order_type.word().to_lowercase() });
608 };
609 let trigger = trigger_on(grid, trigger)?;
610 let size = match size {
611 Covers::Position if order.sz.is_zero() => Dec::ZERO,
612 Covers::Position => return Err(Refusal::NotWholePosition(order.oid)),
613 Covers::Size(amount) => {
614 let size = amount.size_on(grid, trigger)?;
615 at_least(size, trigger, MINIMUM_ORDER_VALUE)?;
616 size
617 }
618 };
619 let (price, is_market) = fill_price(grid, trigger, price, side)?;
620 Order::trigger(market.asset, side, price, size, trigger, tpsl, is_market)
621 }
622 };
623 let new = match order.reduce_only {
624 true => new.reduce_only(),
625 false => new,
626 };
627 Ok(BatchModify::new(vec![Modify::new(OrderRef::Oid(order.oid), new)]))
628}
629
630pub fn close(market: &Market, intent: &Close, builder: Option<&BuilderSetting>) -> Result<PlaceOrder, Refusal> {
634 if market.is_spot() {
635 return Err(Refusal::Spot("a position"));
636 }
637 if intent.position.is_zero() {
638 return Err(Refusal::NoPosition);
639 }
640 let side = match intent.position.is_positive() {
641 true => Side::Sell,
642 false => Side::Buy,
643 };
644 let held = intent.position.abs();
645 let wanted = match intent.size {
646 None => held,
647 Some(size) if positive(size, "a size")? > held => {
648 return Err(Refusal::CloseTooLarge { size, position: held });
649 }
650 Some(size) => size,
651 };
652 let size = market.grid.size(wanted).ok_or(Refusal::Trade(TradeError::NoSize(wanted)))?;
653 let reference = positive(intent.reference, "a reference price")?;
654 let bound = market.grid.market_bound(reference, intent.slippage, side)?;
655 if size != held {
656 at_least(size, reference, MINIMUM_ORDER_VALUE)?;
657 }
658 let order = Order::market(market.asset, side, bound, size).reduce_only();
659 let order = match intent.cloid {
660 Some(cloid) => order.with_cloid(cloid),
661 None => order,
662 };
663 routed(PlaceOrder::one(order), builder)
664}
665
666pub fn leverage(market: &Market, mode: MarginMode, leverage: u32) -> Result<UpdateLeverage, Refusal> {
668 let Some(most) = market.max_leverage else {
669 return Err(Refusal::Spot("leverage"));
670 };
671 if leverage == 0 || leverage > most {
672 return Err(Refusal::Leverage { asked: leverage, most });
673 }
674 if mode == MarginMode::Cross && !market.allows_cross {
675 return Err(Refusal::IsolatedOnly);
676 }
677 Ok(UpdateLeverage::new(market.asset, mode, leverage)?)
678}
679
680pub fn margin(market: &Market, position: Dec, mode: MarginMode, usdc: Dec) -> Result<UpdateIsolatedMargin, Refusal> {
683 if market.is_spot() {
684 return Err(Refusal::Spot("margin"));
685 }
686 if position.is_zero() {
687 return Err(Refusal::NoPosition);
688 }
689 if mode == MarginMode::Cross {
690 return Err(Refusal::CrossPosition);
691 }
692 let side = match position.is_positive() {
693 true => Side::Buy,
694 false => Side::Sell,
695 };
696 Ok(UpdateIsolatedMargin::new(market.asset, side, usdc)?)
697}
698
699#[cfg(test)]
700#[path = "tests/ticket.rs"]
701mod tests;