use std::{convert::Infallible, fmt, str::FromStr};
use crate::account::history::{fills, orders};
use crate::account::holdings::spot::SpotState;
use crate::markets::outcomes::coin::Coin;
use crate::markets::outcomes::px::{Px, Qty, SignedQty};
use crate::trade::{Cloid, Side};
use crate::utils::{Dec, Round};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Direction {
Buy,
Sell,
SplitOutcome,
MergeOutcome,
NegateOutcome,
MergeQuestion,
Settlement,
Other(String),
}
impl Direction {
#[must_use]
pub fn read(text: &str) -> Direction {
match text.trim() {
"Buy" => Direction::Buy,
"Sell" => Direction::Sell,
"Split Outcome" => Direction::SplitOutcome,
"Merge Outcome" => Direction::MergeOutcome,
"Negate Outcome" => Direction::NegateOutcome,
"Merge Question" => Direction::MergeQuestion,
"Settlement" => Direction::Settlement,
other => Direction::Other(other.to_owned()),
}
}
#[must_use]
pub fn slug(&self) -> &str {
match self {
Direction::Buy => "buy",
Direction::Sell => "sell",
Direction::SplitOutcome => "split_outcome",
Direction::MergeOutcome => "merge_outcome",
Direction::NegateOutcome => "negate_outcome",
Direction::MergeQuestion => "merge_question",
Direction::Settlement => "settlement",
Direction::Other(raw) => raw,
}
}
#[must_use]
pub const fn is_trade(&self) -> bool {
matches!(self, Direction::Buy | Direction::Sell)
}
#[must_use]
pub const fn is_structural(&self) -> bool {
matches!(
self,
Direction::SplitOutcome | Direction::MergeOutcome | Direction::NegateOutcome | Direction::MergeQuestion
)
}
#[must_use]
pub const fn is_settlement(&self) -> bool {
matches!(self, Direction::Settlement)
}
}
impl FromStr for Direction {
type Err = Infallible;
fn from_str(text: &str) -> Result<Self, Self::Err> {
Ok(Direction::read(text))
}
}
impl fmt::Display for Direction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.slug())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Fill {
pub coin: Coin,
pub side: Side,
pub direction: Direction,
pub price: Dec,
pub size: Qty,
pub at_ms: u64,
pub start_position: SignedQty,
pub position: SignedQty,
pub closed_pnl: Dec,
pub fee: Dec,
pub fee_token: String,
pub builder_fee: Option<Dec>,
pub crossed: bool,
pub oid: u64,
pub tid: u64,
pub hash: String,
pub cloid: Option<String>,
}
impl Fill {
#[must_use]
pub fn read(raw: &fills::Fill) -> Option<Fill> {
if !raw.coin.starts_with('#') {
return None;
}
let coin = Coin::parse(&raw.coin)?;
let size = Qty::from_dec(raw.sz).ok()?;
let start_position = SignedQty::from_dec(raw.start_position).ok()?;
let moved = match raw.side {
Side::Buy => SignedQty::long(size),
Side::Sell => SignedQty::short(size),
};
Some(Fill {
coin,
side: raw.side,
direction: Direction::read(&raw.dir),
price: raw.px,
size,
at_ms: raw.time,
start_position,
position: start_position.saturating_add(moved),
closed_pnl: raw.closed_pnl,
fee: raw.fee,
fee_token: raw.fee_token.clone(),
builder_fee: raw.builder_fee,
crossed: raw.crossed,
oid: raw.oid,
tid: raw.tid,
hash: raw.hash.clone(),
cloid: raw.cloid.clone(),
})
}
#[must_use]
pub fn notional(&self) -> Dec {
self.size.to_dec().checked_mul(self.price).unwrap_or(Dec::ZERO)
}
#[must_use]
pub fn venue_fee(&self) -> Option<Dec> {
self.fee.checked_sub(self.builder_fee.unwrap_or(Dec::ZERO))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OpenOrder {
pub coin: Coin,
pub side: Side,
pub price: Px,
pub size: Qty,
pub original_size: Qty,
pub placed_ms: u64,
pub oid: u64,
pub cloid: Option<Cloid>,
}
impl OpenOrder {
#[must_use]
pub fn read(raw: &orders::OpenOrder) -> Option<OpenOrder> {
if !raw.coin.starts_with('#') {
return None;
}
Some(OpenOrder {
coin: Coin::parse(&raw.coin)?,
side: raw.side,
price: Px::from_dec(raw.limit_px).ok()?,
size: Qty::from_dec(raw.sz).ok()?,
original_size: Qty::from_dec(raw.orig_sz).ok()?,
placed_ms: raw.timestamp,
oid: raw.oid,
cloid: raw.cloid,
})
}
#[must_use]
pub const fn filled(&self) -> Qty {
self.original_size.saturating_sub(self.size)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Holding {
pub coin: Coin,
pub total: Dec,
pub hold: Dec,
pub entry_ntl: Dec,
}
impl Holding {
#[must_use]
pub fn free(&self) -> Dec {
self.total.checked_sub(self.hold).unwrap_or(Dec::ZERO).max(Dec::ZERO)
}
#[must_use]
pub fn average(&self) -> Option<Dec> {
match self.total.is_positive() {
true => self.entry_ntl.checked_div(self.total, 8, Round::Nearest),
false => None,
}
}
}
#[must_use]
pub fn holdings(state: &SpotState) -> Vec<Holding> {
state
.balances
.iter()
.filter(|row| !(row.total.is_zero() && row.hold.is_zero()))
.filter_map(|row| {
Some(Holding { coin: Coin::parse(&row.coin)?, total: row.total, hold: row.hold, entry_ntl: row.entry_ntl })
})
.collect()
}
#[cfg(test)]
#[path = "tests/outcomes.rs"]
mod tests;