use std::{fmt, str::FromStr};
use crate::markets::outcomes::coin::PRICE_PLACES;
use crate::utils::MINIMUM_ORDER_VALUE;
use crate::utils::{Dec, Round};
use serde::{Deserialize, Deserializer, de};
pub const TICKS_PER_UNIT: u32 = 100_000;
pub const OBSERVED_PLACES: u32 = 8;
pub const OBSERVED_PER_UNIT: u32 = 100_000_000;
pub const MIN_NOTIONAL: Dec = Dec::lit::<1>(15);
const SHORT_SIDE_FLOOR: Dec = Dec::lit::<2>(1);
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum BadPx {
#[error("`{0}` is not a decimal number")]
NotANumber(String),
#[error("price `{0}` is outside 0 to 1")]
OutOfRange(String),
#[error("price `{0}` is finer than the 0.00001 tick")]
TooFine(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Px(u32);
impl Px {
pub const ZERO: Px = Px(0);
pub const ONE: Px = Px(TICKS_PER_UNIT);
pub const MIN_QUOTE: Px = Px(1);
pub const MAX_QUOTE: Px = Px(TICKS_PER_UNIT - 1);
#[must_use]
pub const fn from_ticks(ticks: u32) -> Option<Px> {
match ticks <= TICKS_PER_UNIT {
true => Some(Px(ticks)),
false => None,
}
}
#[must_use]
pub const fn ticks(self) -> u32 {
self.0
}
#[must_use]
pub const fn complement(self) -> Px {
Px(TICKS_PER_UNIT - self.0)
}
#[must_use]
pub const fn is_zero(self) -> bool {
self.0 == 0
}
#[must_use]
pub const fn is_quotable(self) -> bool {
self.0 >= Px::MIN_QUOTE.0 && self.0 <= Px::MAX_QUOTE.0
}
#[must_use]
pub const fn clamp_to_quotable(self) -> Px {
if self.0 < Px::MIN_QUOTE.0 {
Px::MIN_QUOTE
} else if self.0 > Px::MAX_QUOTE.0 {
Px::MAX_QUOTE
} else {
self
}
}
#[must_use]
pub const fn midpoint(self, other: Px) -> Px {
Px((self.0 + other.0) / 2)
}
#[must_use]
pub const fn checked_add(self, other: Px) -> Option<Px> {
Px::from_ticks(self.0 + other.0)
}
#[must_use]
pub const fn abs_diff(self, other: Px) -> u32 {
self.0.abs_diff(other.0)
}
#[must_use]
pub const fn step(self, ticks: i32) -> Px {
let moved = self.0 as i64 + ticks as i64;
if moved < 0 {
Px::ZERO
} else if moved > TICKS_PER_UNIT as i64 {
Px::ONE
} else {
Px(moved as u32)
}
}
pub fn parse(text: &str) -> Result<Px, BadPx> {
let value = Dec::parse(text).map_err(|_| BadPx::NotANumber(text.to_owned()))?;
if value.is_negative() {
return Err(BadPx::NotANumber(text.to_owned()));
}
Px::exact(value).map_err(|why| why.naming(text))
}
pub fn from_dec(value: Dec) -> Result<Px, BadPx> {
Px::exact(value).map_err(|why| why.naming(&value.to_string()))
}
fn exact(value: Dec) -> Result<Px, BadPx> {
if value.places() > PRICE_PLACES {
return Err(BadPx::TooFine(String::new()));
}
Px::on_grid(value, Round::Down).ok_or(BadPx::OutOfRange(String::new()))
}
#[must_use]
pub fn on_grid(value: Dec, round: Round) -> Option<Px> {
let ticks = value.units_at(PRICE_PLACES, round)?;
u32::try_from(ticks).ok().and_then(Px::from_ticks)
}
#[must_use]
pub fn nearest(value: Dec) -> Option<Px> {
Px::on_grid(value, Round::Nearest)
}
#[must_use]
pub const fn to_dec(self) -> Dec {
Dec::lit::<PRICE_PLACES>(self.0 as i128)
}
#[must_use]
pub fn to_wire(self) -> String {
self.to_dec().to_wire()
}
}
impl BadPx {
fn naming(self, text: &str) -> BadPx {
let text = text.to_owned();
match self {
BadPx::NotANumber(_) => BadPx::NotANumber(text),
BadPx::OutOfRange(_) => BadPx::OutOfRange(text),
BadPx::TooFine(_) => BadPx::TooFine(text),
}
}
}
impl fmt::Display for Px {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_dec())
}
}
impl FromStr for Px {
type Err = BadPx;
fn from_str(text: &str) -> Result<Self, Self::Err> {
Px::parse(text)
}
}
impl<'de> Deserialize<'de> for Px {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let text = String::deserialize(deserializer)?;
Px::parse(&text).map_err(de::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct Mid(u32);
impl Mid {
pub const ZERO: Mid = Mid(0);
pub const ONE: Mid = Mid(OBSERVED_PER_UNIT);
#[must_use]
pub const fn units(self) -> u32 {
self.0
}
#[must_use]
pub const fn from_units(units: u32) -> Option<Mid> {
match units <= OBSERVED_PER_UNIT {
true => Some(Mid(units)),
false => None,
}
}
#[must_use]
pub const fn complement(self) -> Mid {
Mid(OBSERVED_PER_UNIT - self.0)
}
#[must_use]
pub const fn is_zero(self) -> bool {
self.0 == 0
}
#[must_use]
pub const fn of(price: Px) -> Mid {
Mid(price.0 * (OBSERVED_PER_UNIT / TICKS_PER_UNIT))
}
#[must_use]
pub const fn is_orderable(self) -> bool {
self.0.is_multiple_of(OBSERVED_PER_UNIT / TICKS_PER_UNIT)
}
#[must_use]
pub const fn to_px_nearest(self) -> Px {
let per = OBSERVED_PER_UNIT / TICKS_PER_UNIT;
Px((self.0 + per / 2) / per)
}
#[must_use]
pub const fn to_px_floor(self) -> Px {
Px(self.0 / (OBSERVED_PER_UNIT / TICKS_PER_UNIT))
}
#[must_use]
pub const fn to_px_ceil(self) -> Px {
Px(self.0.div_ceil(OBSERVED_PER_UNIT / TICKS_PER_UNIT))
}
pub fn parse(text: &str) -> Result<Mid, BadPx> {
let value = Dec::parse(text).map_err(|_| BadPx::NotANumber(text.to_owned()))?;
if value.is_negative() {
return Err(BadPx::NotANumber(text.to_owned()));
}
if value.places() > OBSERVED_PLACES {
return Err(BadPx::TooFine(text.to_owned()));
}
value
.units_at(OBSERVED_PLACES, Round::Down)
.and_then(|units| u32::try_from(units).ok())
.filter(|units| *units <= OBSERVED_PER_UNIT)
.map(Mid)
.ok_or_else(|| BadPx::OutOfRange(text.to_owned()))
}
#[must_use]
pub const fn to_dec(self) -> Dec {
Dec::lit::<OBSERVED_PLACES>(self.0 as i128)
}
#[must_use]
pub fn to_wire(self) -> String {
self.to_dec().to_wire()
}
}
impl fmt::Display for Mid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_dec())
}
}
impl FromStr for Mid {
type Err = BadPx;
fn from_str(text: &str) -> Result<Self, Self::Err> {
Mid::parse(text)
}
}
impl<'de> Deserialize<'de> for Mid {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let text = String::deserialize(deserializer)?;
Mid::parse(&text).map_err(de::Error::custom)
}
}
#[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)
}
}
#[must_use]
pub fn min_shares(mark: Dec) -> Qty {
let most = Qty(150);
let Some(other) = Dec::ONE.checked_sub(mark) else {
return most;
};
let short = mark.min(other).clamp(SHORT_SIDE_FLOOR, Dec::ONE);
MIN_NOTIONAL.checked_div(short, 0, Round::Up).and_then(whole).unwrap_or(most)
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum BadSignedQty {
#[error(transparent)]
Size(#[from] BadQty),
#[error("holding `{0}` is more contracts than this build can count")]
TooLarge(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct SignedQty(i64);
impl SignedQty {
pub const ZERO: SignedQty = SignedQty(0);
#[must_use]
pub const fn new(contracts: i64) -> SignedQty {
SignedQty(contracts)
}
#[must_use]
pub const fn get(self) -> i64 {
self.0
}
#[must_use]
pub const fn is_zero(self) -> bool {
self.0 == 0
}
#[must_use]
pub const fn is_short(self) -> bool {
self.0 < 0
}
#[must_use]
pub const fn abs(self) -> Qty {
Qty(self.0.unsigned_abs())
}
#[must_use]
pub const fn long(size: Qty) -> SignedQty {
match size.0 > i64::MAX as u64 {
true => SignedQty(i64::MAX),
false => SignedQty(size.0 as i64),
}
}
#[must_use]
pub const fn short(size: Qty) -> SignedQty {
match size.0 > i64::MAX as u64 {
true => SignedQty(i64::MIN),
false => SignedQty(-(size.0 as i64)),
}
}
#[must_use]
pub const fn saturating_add(self, other: SignedQty) -> SignedQty {
SignedQty(self.0.saturating_add(other.0))
}
pub fn parse(text: &str) -> Result<SignedQty, BadSignedQty> {
let raw = text.trim();
let (negative, body) = match raw.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, raw),
};
let size = Qty::parse(body).map_err(|error| match error {
BadQty::NotAnInteger(_) => BadQty::NotAnInteger(raw.to_owned()),
BadQty::Fractional(_) => BadQty::Fractional(raw.to_owned()),
})?;
let contracts = i64::try_from(size.0).map_err(|_| BadSignedQty::TooLarge(raw.to_owned()))?;
Ok(SignedQty(if negative { -contracts } else { contracts }))
}
pub fn from_dec(value: Dec) -> Result<SignedQty, BadSignedQty> {
let size = Qty::from_dec(value.abs())?;
let contracts = i64::try_from(size.0).map_err(|_| BadSignedQty::TooLarge(value.to_string()))?;
Ok(SignedQty(if value.is_negative() { -contracts } else { contracts }))
}
#[must_use]
pub fn to_wire(self) -> String {
self.0.to_string()
}
}
impl fmt::Display for SignedQty {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl FromStr for SignedQty {
type Err = BadSignedQty;
fn from_str(text: &str) -> Result<Self, Self::Err> {
SignedQty::parse(text)
}
}
#[cfg(test)]
#[path = "tests/px.rs"]
mod tests;