use serde::Deserialize;
use crate::client::{ApiError, Call, Transport, read};
use crate::markets::{Trade, TradeKind};
use crate::utils::lenient::list;
use crate::utils::units::COLLATERAL_DECIMALS;
use crate::utils::{AccountIndex, ClientOrderIndex, Dec, MarketId, MarketKind, OrderIndex, Round, Side};
use super::order_reads::kinds_filter;
pub const MOST_TRADES: u8 = 100;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum TradeSort {
#[default]
Timestamp,
BlockHeight,
TradeId,
}
impl TradeSort {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
TradeSort::Timestamp => "timestamp",
TradeSort::BlockHeight => "block_height",
TradeSort::TradeId => "trade_id",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Role {
Maker,
Taker,
}
impl Role {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Role::Maker => "maker",
Role::Taker => "taker",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TradeQuery {
pub account: AccountIndex,
pub market: Option<MarketId>,
pub kinds: Vec<MarketKind>,
pub sort: TradeSort,
pub cursor: Option<String>,
pub side: Option<Side>,
pub role: Option<Role>,
pub kind: Option<TradeKind>,
pub order: Option<OrderIndex>,
pub limit: u8,
}
impl TradeQuery {
#[must_use]
pub const fn new(account: AccountIndex) -> TradeQuery {
TradeQuery {
account,
market: None,
kinds: Vec::new(),
sort: TradeSort::Timestamp,
cursor: None,
side: None,
role: None,
kind: None,
order: None,
limit: MOST_TRADES,
}
}
#[must_use]
pub fn after(&self, page: &TradePage) -> Option<TradeQuery> {
let cursor = page.next()?;
Some(TradeQuery { cursor: Some(cursor.to_owned()), ..self.clone() })
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct TradePage {
#[serde(default, deserialize_with = "list")]
pub trades: Vec<Trade>,
#[serde(default)]
pub next_cursor: Option<String>,
}
impl TradePage {
#[must_use]
pub fn next(&self) -> Option<&str> {
self.next_cursor.as_deref().filter(|cursor| !cursor.is_empty())
}
}
pub(crate) const fn kind_filter(kind: TradeKind) -> Option<&'static str> {
match kind {
TradeKind::Trade => Some("trade"),
TradeKind::Liquidation => Some("liquidation"),
TradeKind::Deleverage => Some("deleverage"),
TradeKind::MarketSettlement => Some("market-settlement"),
TradeKind::Other => None,
}
}
#[must_use]
pub fn account_trades_call(query: &TradeQuery) -> Call {
Call::get("trades")
.field("account_index", query.account)
.maybe("market_id", query.market)
.maybe("market_type", kinds_filter(&query.kinds))
.field("sort_by", query.sort.name())
.maybe("cursor", query.cursor.as_deref())
.maybe("ask_filter", query.side.map(|side| u8::from(side.is_ask())))
.maybe("role", query.role.map(Role::name))
.maybe("type", query.kind.and_then(kind_filter))
.maybe("order_index", query.order)
.field("limit", query.limit.clamp(1, MOST_TRADES))
.authed()
}
pub async fn account_trades(transport: &impl Transport, query: &TradeQuery) -> Result<TradePage, ApiError> {
read(transport, account_trades_call(query)).await
}
pub async fn fills_since(
transport: &impl Transport,
account: AccountIndex,
kinds: &[MarketKind],
since_ms: u64,
pages: usize,
) -> Result<Vec<Fill>, ApiError> {
let mut query = TradeQuery { kinds: kinds.to_vec(), ..TradeQuery::new(account) };
let mut fills = Vec::new();
for _ in 0..pages {
let page = account_trades(transport, &query).await?;
let reached = page.trades.iter().any(|trade| trade.timestamp < since_ms);
fills.extend(
page.trades.iter().filter(|trade| trade.timestamp >= since_ms).filter_map(|trade| Fill::of(trade, account)),
);
match query.after(&page) {
Some(next) if !reached && !page.trades.is_empty() => query = next,
_ => break,
}
}
Ok(fills)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Fill {
pub trade: Trade,
pub account: AccountIndex,
pub side: Side,
pub is_maker: bool,
pub fee_rate: u32,
pub fee: Dec,
pub realized_pnl: Option<Dec>,
}
impl Fill {
#[must_use]
pub fn of(trade: &Trade, account: AccountIndex) -> Option<Fill> {
let side = trade.side_of(account)?;
let is_maker = trade.maker_side() == side;
let (rate, before, entry_quote) = match is_maker {
true => (trade.maker_fee, trade.maker_position_size_before, trade.maker_entry_quote_before),
false => (trade.taker_fee, trade.taker_position_size_before, trade.taker_entry_quote_before),
};
let fee_rate = rate.unwrap_or(0);
Some(Fill {
trade: trade.clone(),
account,
side,
is_maker,
fee_rate,
fee: fee(trade.usd_amount, fee_rate),
realized_pnl: realized(side, trade.size, trade.price, before, entry_quote),
})
}
#[must_use]
pub const fn market(&self) -> MarketId {
self.trade.market_id
}
#[must_use]
pub const fn price(&self) -> Dec {
self.trade.price
}
#[must_use]
pub const fn size(&self) -> Dec {
self.trade.size
}
#[must_use]
pub const fn timestamp_ms(&self) -> u64 {
self.trade.timestamp
}
#[must_use]
pub const fn order_index(&self) -> u64 {
match self.side {
Side::Bid => self.trade.bid_id,
Side::Ask => self.trade.ask_id,
}
}
#[must_use]
pub fn client_order_index(&self) -> Option<ClientOrderIndex> {
let index = match self.side {
Side::Bid => self.trade.bid_client_id,
Side::Ask => self.trade.ask_client_id,
};
ClientOrderIndex::new(index).ok()
}
#[must_use]
pub const fn is_liquidation(&self) -> bool {
matches!(self.trade.kind, TradeKind::Liquidation | TradeKind::Deleverage)
}
#[must_use]
pub const fn venue_pnl(&self) -> Option<Dec> {
match self.side {
Side::Bid => self.trade.bid_account_pnl,
Side::Ask => self.trade.ask_account_pnl,
}
}
}
#[must_use]
pub fn fee(notional: Dec, rate: u32) -> Dec {
let fraction = Dec::new(i128::from(rate), 6).unwrap_or(Dec::ZERO);
notional.checked_mul(fraction).map_or(Dec::ZERO, |fee| fee.round(COLLATERAL_DECIMALS, Round::Nearest))
}
#[must_use]
pub fn realized(side: Side, size: Dec, price: Dec, before: Option<Dec>, entry_quote: Option<Dec>) -> Option<Dec> {
let (before, entry_quote) = (before?, entry_quote?);
let long = before.is_positive();
if before.is_zero() || (long && side == Side::Bid) || (!long && side == Side::Ask) {
return Some(Dec::ZERO);
}
let held = before.abs();
let closed = size.min(held);
let top = closed.checked_mul(price)?.checked_mul(held)?.checked_sub(closed.checked_mul(entry_quote.abs())?)?;
let pnl = top.checked_div(held, COLLATERAL_DECIMALS, Round::Nearest)?;
Some(match long {
true => pnl,
false => pnl.negate(),
})
}
#[cfg(test)]
#[path = "tests/fills.rs"]
mod tests;