use std::{
collections::{BTreeMap, BTreeSet},
convert::Infallible,
fmt,
str::FromStr,
};
use crate::account::order::Cloid;
use crate::markets::outcomes::coin::Coin;
use crate::markets::outcomes::px::{Px, Qty, SignedQty};
use crate::trade::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())
}
}
pub type RawFill = crate::account::fill::Fill;
#[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: &RawFill) -> 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))
}
}
pub type RawOrder = crate::account::order::OpenOrder;
#[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: &RawOrder) -> 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 Position {
pub coin: Coin,
pub size: SignedQty,
pub fixed_at_ms: u64,
pub realized_pnl: Dec,
pub fees: Dec,
pub fills: usize,
}
#[must_use]
pub fn positions(fills: &[Fill]) -> Vec<Position> {
let mut newest_first: Vec<&Fill> = fills.iter().collect();
newest_first.sort_by_key(|fill| std::cmp::Reverse(fill.at_ms));
let mut found: BTreeMap<Coin, Position> = BTreeMap::new();
for fill in newest_first {
let position = found.entry(fill.coin).or_insert_with(|| Position {
coin: fill.coin,
size: fill.position,
fixed_at_ms: fill.at_ms,
realized_pnl: Dec::ZERO,
fees: Dec::ZERO,
fills: 0,
});
position.realized_pnl = position.realized_pnl.checked_add(fill.closed_pnl).unwrap_or(position.realized_pnl);
position.fees = position.fees.checked_add(fill.fee).unwrap_or(position.fees);
position.fills += 1;
}
let mut positions: Vec<Position> = found.into_values().collect();
positions.sort_by(|a, b| b.size.abs().cmp(&a.size.abs()).then_with(|| a.coin.cmp(&b.coin)));
positions
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Summary {
pub fills: usize,
pub markets: usize,
pub trades: usize,
pub traded_volume: Dec,
pub structural_volume: Dec,
pub settled_volume: Dec,
pub realized_pnl: Dec,
pub fees: Dec,
pub made: usize,
pub taken: usize,
pub by_direction: BTreeMap<String, usize>,
pub first_ms: Option<u64>,
pub last_ms: Option<u64>,
}
#[must_use]
pub fn summarize(fills: &[Fill]) -> Summary {
let add = |sum: Dec, more: Dec| sum.checked_add(more).unwrap_or(sum);
let mut summary = Summary {
fills: fills.len(),
markets: 0,
trades: 0,
traded_volume: Dec::ZERO,
structural_volume: Dec::ZERO,
settled_volume: Dec::ZERO,
realized_pnl: Dec::ZERO,
fees: Dec::ZERO,
made: 0,
taken: 0,
by_direction: BTreeMap::new(),
first_ms: None,
last_ms: None,
};
let mut outcomes = BTreeSet::new();
for fill in fills {
outcomes.insert(fill.coin.outcome());
summary.realized_pnl = add(summary.realized_pnl, fill.closed_pnl);
summary.fees = add(summary.fees, fill.fee);
if fill.direction.is_trade() {
summary.trades += 1;
summary.traded_volume = add(summary.traded_volume, fill.notional());
match fill.crossed {
true => summary.taken += 1,
false => summary.made += 1,
}
} else if fill.direction.is_structural() {
summary.structural_volume = add(summary.structural_volume, fill.notional());
} else if fill.direction.is_settlement() {
summary.settled_volume = add(summary.settled_volume, fill.notional());
}
*summary.by_direction.entry(fill.direction.slug().to_owned()).or_default() += 1;
summary.first_ms = Some(summary.first_ms.map_or(fill.at_ms, |first| first.min(fill.at_ms)));
summary.last_ms = Some(summary.last_ms.map_or(fill.at_ms, |last| last.max(fill.at_ms)));
}
summary.markets = outcomes.len();
summary
}
pub type SpotBalances = crate::account::clearinghouse::SpotState;
pub type SpotRow = crate::account::clearinghouse::SpotBalance;
#[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: &SpotBalances) -> 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()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Check {
Agrees,
Disagrees {
from_fills: SignedQty,
},
Unseen,
}
#[must_use]
pub fn corroborate<'a>(holdings: &'a [Holding], positions: &[Position]) -> Vec<(&'a Holding, Check)> {
holdings
.iter()
.map(|holding| {
let check = match positions.iter().find(|position| position.coin == holding.coin) {
None => Check::Unseen,
Some(position) if !position.size.is_short() && position.size.abs().to_dec() == holding.total => {
Check::Agrees
}
Some(position) => Check::Disagrees { from_fills: position.size },
};
(holding, check)
})
.collect()
}
#[cfg(test)]
#[path = "tests/outcomes.rs"]
mod tests;