use crate::trade::builder::BuilderSetting;
use crate::trade::error::TradeError;
use crate::trade::orders::scale::{LEAST_LEVELS, MOST_LEVELS};
use crate::trade::orders::{Order, PlaceOrder, Tif, Tpsl};
use crate::utils::rounding::Grid;
use crate::utils::{Asset, AssetKind, Dec, Round};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Market {
asset: Asset,
grid: Grid,
max_leverage: Option<u32>,
allows_cross: bool,
}
impl Market {
#[must_use]
pub const fn perp(asset: Asset, sz_decimals: u32, max_leverage: u32, allows_cross: bool) -> Option<Market> {
match asset.kind() {
AssetKind::Perp | AssetKind::Hip3Perp => {
Some(Market { asset, grid: Grid::perp(sz_decimals), max_leverage: Some(max_leverage), allows_cross })
}
AssetKind::Spot | AssetKind::Outcome => None,
}
}
#[must_use]
pub const fn spot(asset: Asset, sz_decimals: u32) -> Option<Market> {
match asset.kind() {
AssetKind::Spot => {
Some(Market { asset, grid: Grid::spot(sz_decimals), max_leverage: None, allows_cross: false })
}
AssetKind::Perp | AssetKind::Hip3Perp | AssetKind::Outcome => None,
}
}
#[must_use]
pub const fn asset(&self) -> Asset {
self.asset
}
#[must_use]
pub const fn grid(&self) -> Grid {
self.grid
}
#[must_use]
pub const fn max_leverage(&self) -> Option<u32> {
self.max_leverage
}
#[must_use]
pub const fn allows_cross(&self) -> bool {
self.allows_cross
}
#[must_use]
pub const fn is_spot(&self) -> bool {
self.max_leverage.is_none()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Amount {
Size(Dec),
Notional(Dec),
}
impl Amount {
pub(crate) fn size_on(self, grid: Grid, price: Dec) -> Result<Dec, Refusal> {
let raw = match self {
Amount::Size(size) => positive(size, "a size")?,
Amount::Notional(notional) => positive(notional, "a notional")?
.checked_div(price, 12, Round::Down)
.ok_or(Refusal::Trade(TradeError::NoPrice(price)))?,
};
grid.size(raw).ok_or(Refusal::Trade(TradeError::NoSize(raw)))
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum Refusal {
#[error(transparent)]
Trade(#[from] TradeError),
#[error("{what} above zero is needed, not {value}")]
NotPositive { what: &'static str, value: Dec },
#[error("{size} at {price} is worth {}, under the venue's minimum of {minimum}", worth(.size, .price))]
UnderMinimum { size: Dec, price: Dec, minimum: Dec },
#[error("a limit is never FrontendMarket: a market order is Price::Market, bounded from the mid")]
MarketTif,
#[error("a scale rests on the book, so Gtc or Alo, not {}", .0.wire())]
ScaleTif(Tif),
#[error("a scale has {LEAST_LEVELS} to {MOST_LEVELS} levels, not {0}")]
Levels(u32),
#[error("a modify that does not always place is Alo or Gtc, not {}", .0.wire())]
ModifyTif(Tif),
#[error("spot takes no {0}")]
Spot(&'static str),
#[error("a notional needs a reference price to be sized at")]
NoReference,
#[error("a {} at {trigger} is on the wrong side of an entry at {entry}", tpsl_words(.tpsl))]
ExitSide { tpsl: Tpsl, trigger: Dec, entry: Dec },
#[error("there is no position here")]
NoPosition,
#[error("closing {size} is more than the position of {position}")]
CloseTooLarge { size: Dec, position: Dec },
#[error("leverage is a whole number from 1 to {most}, not {asked}")]
Leverage { asked: u32, most: u32 },
#[error("this market is isolated only")]
IsolatedOnly,
#[error("this position is cross; margin moves on isolated ones")]
CrossPosition,
#[error("order {0} is a trigger: change its trigger instead")]
NotALimit(u64),
#[error("order {0} is not a take profit or stop loss")]
NotATrigger(u64),
#[error("order {oid} is a {kind} trigger, which this build cannot modify")]
UnknownTrigger { oid: u64, kind: String },
#[error("order {0} has a size of its own; only a position's take profit or stop loss covers the whole position")]
NotWholePosition(u64),
}
fn worth(size: &Dec, price: &Dec) -> String {
size.checked_mul(*price).map_or_else(|| "too much to count".to_owned(), Dec::to_wire)
}
const fn tpsl_words(tpsl: &Tpsl) -> &'static str {
match tpsl {
Tpsl::TakeProfit => "take profit",
Tpsl::StopLoss => "stop loss",
}
}
pub(crate) fn positive(value: Dec, what: &'static str) -> Result<Dec, Refusal> {
match value.is_positive() {
true => Ok(value),
false => Err(Refusal::NotPositive { what, value }),
}
}
pub(crate) fn at_least(size: Dec, price: Dec, minimum: Dec) -> Result<(), Refusal> {
match price.checked_mul(size).is_some_and(|worth| worth >= minimum) {
true => Ok(()),
false => Err(Refusal::UnderMinimum { size, price, minimum }),
}
}
pub(crate) fn routed(place: PlaceOrder, builder: Option<&BuilderSetting>) -> Result<PlaceOrder, Refusal> {
let Some(setting) = builder else { return Ok(place) };
match setting.on_assets(place.orders().iter().map(Order::asset)).map_err(TradeError::from)? {
Some(builder) => Ok(place.through(builder)),
None => Ok(place),
}
}
#[cfg(test)]
#[path = "tests/ticket.rs"]
mod tests;