use rust_decimal::Decimal;
use crate::types::{Market, Side, Timestamp};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Balance {
pub asset: String,
pub available: Decimal,
pub locked: Decimal,
}
impl Balance {
pub fn total(&self) -> Decimal {
self.available + self.locked
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum OrderType {
Market,
Limit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum TimeInForce {
GoodTilCancelled,
ImmediateOrCancel,
FillOrKill,
PostOnly,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Size {
Base(Decimal),
Quote(Decimal),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum OrderStatus {
Accepted,
Open,
PartiallyFilled,
Filled,
Cancelled,
Rejected,
Unknown,
}
impl OrderStatus {
pub const fn is_live(self) -> bool {
matches!(self, Self::Accepted | Self::Open | Self::PartiallyFilled)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Order {
pub id: String,
pub market: Market,
pub side: Side,
pub status: OrderStatus,
pub filled_quantity: Decimal,
pub remaining_quantity: Decimal,
pub price: Option<Decimal>,
pub created_at: Option<Timestamp>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Position {
pub market: Market,
pub side: Option<Side>,
pub quantity: Decimal,
pub entry_price: Option<Decimal>,
pub mark_price: Option<Decimal>,
pub notional: Option<Decimal>,
pub unrealized_pnl: Option<Decimal>,
pub leverage: Option<Decimal>,
pub margin_mode: Option<MarginMode>,
}
impl Position {
pub fn is_flat(&self) -> bool {
self.quantity.is_zero()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum MarginMode {
Cross,
Isolated,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MarginSummary {
pub asset: String,
pub equity: Option<Decimal>,
pub margin_balance: Option<Decimal>,
pub available_balance: Option<Decimal>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FundingRate {
pub market: Market,
pub timestamp: Timestamp,
pub rate: Decimal,
pub mark_price: Option<Decimal>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FundingPayment {
pub market: Market,
pub timestamp: Timestamp,
pub amount: Decimal,
pub rate: Option<Decimal>,
pub id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cursor(pub(crate) String);
impl Cursor {
pub fn new(cursor: impl Into<String>) -> Self {
Self(cursor.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Page<T> {
pub items: Vec<T>,
pub next: Option<Cursor>,
}
impl<T> Page<T> {
pub fn has_more(&self) -> bool {
self.next.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Exchange, Market};
#[test]
fn balance_total_counts_locked_funds() {
let balance = Balance {
asset: "KRW".to_string(),
available: Decimal::from(1_000),
locked: Decimal::from(500),
};
assert_eq!(balance.total(), Decimal::from(1_500));
}
#[test]
fn only_unfinished_orders_are_live() {
for status in [
OrderStatus::Accepted,
OrderStatus::Open,
OrderStatus::PartiallyFilled,
] {
assert!(status.is_live(), "{status:?}");
}
for status in [
OrderStatus::Filled,
OrderStatus::Cancelled,
OrderStatus::Rejected,
OrderStatus::Unknown,
] {
assert!(!status.is_live(), "{status:?}");
}
}
#[test]
fn base_and_quote_sizing_are_not_interchangeable() {
let ten_thousand_krw = Size::Quote(Decimal::from(10_000));
let ten_thousand_btc = Size::Base(Decimal::from(10_000));
assert_ne!(ten_thousand_krw, ten_thousand_btc);
}
#[test]
fn a_zero_size_position_is_flat() {
let mut position = Position {
market: Market::perpetual(Exchange::Binance, "BTC", "USDT"),
side: None,
quantity: Decimal::ZERO,
entry_price: None,
mark_price: None,
notional: None,
unrealized_pnl: None,
leverage: None,
margin_mode: None,
};
assert!(position.is_flat());
position.quantity = Decimal::ONE;
assert!(!position.is_flat());
}
#[test]
fn the_last_page_reports_no_more() {
let last = Page::<u8> {
items: vec![],
next: None,
};
let more = Page::<u8> {
items: vec![],
next: Some(Cursor("next".to_string())),
};
assert!(!last.has_more());
assert!(more.has_more());
assert_eq!(more.next.unwrap().as_str(), "next");
}
}