use std::{fmt, str::FromStr};
use crate::markets::outcomes::coin::PRICE_PLACES;
use crate::markets::outcomes::px::Px;
use crate::utils::MINIMUM_ORDER_VALUE;
use crate::utils::{Dec, Round};
use serde::{Deserialize, Deserializer, de};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum BadQty {
#[error("`{0}` is not a whole number of contracts")]
NotAnInteger(String),
#[error("size `{0}` is fractional; outcome books trade whole contracts")]
Fractional(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Qty(u64);
impl Qty {
pub const ZERO: Qty = Qty(0);
#[must_use]
pub const fn new(contracts: u64) -> Qty {
Qty(contracts)
}
#[must_use]
pub const fn get(self) -> u64 {
self.0
}
#[must_use]
pub const fn is_zero(self) -> bool {
self.0 == 0
}
#[must_use]
pub const fn min(self, other: Qty) -> Qty {
if self.0 < other.0 { self } else { other }
}
#[must_use]
pub const fn saturating_sub(self, other: Qty) -> Qty {
Qty(self.0.saturating_sub(other.0))
}
pub fn parse(text: &str) -> Result<Qty, BadQty> {
let raw = text.trim();
let body = raw.strip_prefix('+').unwrap_or(raw);
let (whole, fraction) = body.split_once('.').unwrap_or((body, ""));
let digits = |part: &str| part.bytes().all(|byte| byte.is_ascii_digit());
if whole.is_empty() || !digits(whole) || !digits(fraction) {
return Err(BadQty::NotAnInteger(raw.to_owned()));
}
if fraction.bytes().any(|byte| byte != b'0') {
return Err(BadQty::Fractional(raw.to_owned()));
}
whole.parse().map(Qty).map_err(|_| BadQty::NotAnInteger(raw.to_owned()))
}
pub fn from_dec(value: Dec) -> Result<Qty, BadQty> {
if !value.is_integer() {
return Err(BadQty::Fractional(value.to_string()));
}
value
.units_at(0, Round::Down)
.and_then(|whole| u64::try_from(whole).ok())
.map(Qty)
.ok_or_else(|| BadQty::NotAnInteger(value.to_string()))
}
#[must_use]
pub const fn to_dec(self) -> Dec {
Dec::lit::<0>(self.0 as i128)
}
#[must_use]
pub fn to_wire(self) -> String {
self.0.to_string()
}
#[must_use]
pub const fn notional(self, price: Px) -> Dec {
Dec::lit::<PRICE_PLACES>(self.0 as i128 * price.0 as i128)
}
#[must_use]
pub fn worth_minimum(self, price: Px) -> bool {
self.notional(price) >= MINIMUM_ORDER_VALUE
}
#[must_use]
pub fn least_at(price: Px) -> Option<Qty> {
if price.is_zero() {
return None;
}
whole(MINIMUM_ORDER_VALUE.checked_div(price.to_dec(), 0, Round::Up)?)
}
#[must_use]
pub fn spend(amount: Dec, price: Px) -> Option<Qty> {
if price.is_zero() || amount.is_negative() {
return None;
}
whole(amount.checked_div(price.to_dec(), 0, Round::Down)?)
}
}
fn whole(value: Dec) -> Option<Qty> {
value.units_at(0, Round::Down).and_then(|whole| u64::try_from(whole).ok()).map(Qty)
}
impl fmt::Display for Qty {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for Qty {
type Err = BadQty;
fn from_str(text: &str) -> Result<Self, Self::Err> {
Qty::parse(text)
}
}
impl<'de> Deserialize<'de> for Qty {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let text = String::deserialize(deserializer)?;
Qty::parse(&text).map_err(de::Error::custom)
}
}
#[cfg(test)]
#[path = "tests/qty.rs"]
mod tests;