use kraken_book::OrderbookSnapshot;
use kraken_types::{BalanceData, Decimal, ExecutionData, L3Data, L3Order, Side};
use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DisconnectReason {
ServerClosed,
NetworkError(String),
Timeout,
Shutdown,
AuthFailed,
HeartbeatTimeout,
}
#[derive(Debug, Clone)]
pub enum ConnectionEvent {
Connected {
api_version: String,
connection_id: u64,
},
Disconnected {
reason: DisconnectReason,
},
Reconnecting {
attempt: u32,
delay: Duration,
},
ReconnectFailed {
error: String,
},
SubscriptionsRestored {
count: usize,
},
CircuitBreakerOpen {
trips: u64,
},
}
#[derive(Debug, Clone)]
pub enum SubscriptionEvent {
Subscribed {
channel: String,
symbols: Vec<String>,
},
Rejected {
channel: String,
reason: String,
},
Unsubscribed {
channel: String,
symbols: Vec<String>,
},
}
#[derive(Debug, Clone)]
pub enum MarketEvent {
OrderbookSnapshot {
symbol: String,
snapshot: OrderbookSnapshot,
},
OrderbookUpdate {
symbol: String,
snapshot: OrderbookSnapshot,
},
ChecksumMismatch {
symbol: String,
expected: u32,
computed: u32,
},
Status {
system: String,
version: String,
},
Heartbeat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OrderStatus {
Pending,
New,
PartiallyFilled,
Filled,
Canceled,
Expired,
Rejected,
}
impl OrderStatus {
pub fn parse(s: &str) -> Self {
match s.to_lowercase().as_str() {
"pending" => Self::Pending,
"new" | "open" => Self::New,
"partially_filled" | "partiallyfilled" => Self::PartiallyFilled,
"filled" | "closed" => Self::Filled,
"canceled" | "cancelled" => Self::Canceled,
"expired" => Self::Expired,
_ => Self::Rejected,
}
}
pub fn is_active(&self) -> bool {
matches!(self, Self::Pending | Self::New | Self::PartiallyFilled)
}
pub fn is_terminal(&self) -> bool {
matches!(self, Self::Filled | Self::Canceled | Self::Expired | Self::Rejected)
}
}
#[derive(Debug, Clone)]
pub struct TrackedOrder {
pub order_id: String,
pub symbol: String,
pub side: Side,
pub order_type: String,
pub order_qty: Decimal,
pub limit_price: Option<Decimal>,
pub filled_qty: Decimal,
pub avg_price: Option<Decimal>,
pub status: OrderStatus,
pub total_fees: Decimal,
pub fee_currency: Option<String>,
pub fills: Vec<OrderFill>,
pub last_update: String,
}
impl TrackedOrder {
pub fn from_execution(exec: &ExecutionData) -> Self {
Self {
order_id: exec.order_id.clone(),
symbol: exec.symbol.clone(),
side: exec.side,
order_type: exec.order_type.clone(),
order_qty: exec.order_qty.unwrap_or(Decimal::ZERO),
limit_price: exec.limit_price,
filled_qty: exec.cum_qty.unwrap_or(Decimal::ZERO),
avg_price: exec.avg_price,
status: exec.order_status.as_ref()
.map(|s| OrderStatus::parse(s))
.unwrap_or(OrderStatus::Pending),
total_fees: exec.fee_paid.unwrap_or(Decimal::ZERO),
fee_currency: exec.fee_currency.clone(),
fills: Vec::new(),
last_update: exec.timestamp.clone(),
}
}
pub fn update(&mut self, exec: &ExecutionData) {
if let Some(cum_qty) = exec.cum_qty {
self.filled_qty = cum_qty;
}
if let Some(avg_price) = exec.avg_price {
self.avg_price = Some(avg_price);
}
if let Some(ref status) = exec.order_status {
self.status = OrderStatus::parse(status);
}
if let Some(fee) = exec.fee_paid {
self.total_fees = fee;
}
if exec.fee_currency.is_some() {
self.fee_currency = exec.fee_currency.clone();
}
self.last_update = exec.timestamp.clone();
}
pub fn add_fill(&mut self, fill: OrderFill) {
self.fills.push(fill);
}
pub fn remaining_qty(&self) -> Decimal {
self.order_qty - self.filled_qty
}
pub fn fill_percentage(&self) -> f64 {
if self.order_qty.is_zero() {
return 0.0;
}
(self.filled_qty / self.order_qty)
.to_string()
.parse()
.unwrap_or(0.0)
}
}
#[derive(Debug, Clone)]
pub struct OrderFill {
pub exec_id: Option<String>,
pub trade_id: Option<u64>,
pub qty: Decimal,
pub price: Decimal,
pub fee: Decimal,
pub fee_currency: Option<String>,
pub timestamp: String,
}
impl OrderFill {
pub fn from_execution(exec: &ExecutionData) -> Option<Self> {
let qty = exec.last_qty?;
let price = exec.last_price?;
Some(Self {
exec_id: exec.exec_id.clone(),
trade_id: exec.trade_id,
qty,
price,
fee: exec.fee_paid.unwrap_or(Decimal::ZERO),
fee_currency: exec.fee_currency.clone(),
timestamp: exec.timestamp.clone(),
})
}
}
#[derive(Debug, Clone)]
pub enum PrivateEvent {
Execution {
data: ExecutionData,
exec_type: ExecutionType,
},
OrderUpdate {
order: TrackedOrder,
change: OrderChange,
},
BalanceUpdate {
balances: Vec<BalanceData>,
is_snapshot: bool,
},
BalanceSnapshot {
balances: HashMap<String, BalanceInfo>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExecutionType {
New,
Trade,
Canceled,
Expired,
Amended,
Pending,
Unknown,
}
impl ExecutionType {
pub fn parse(s: &str) -> Self {
match s.to_lowercase().as_str() {
"new" => Self::New,
"trade" | "filled" => Self::Trade,
"canceled" | "cancelled" => Self::Canceled,
"expired" => Self::Expired,
"amended" | "modified" => Self::Amended,
"pending" => Self::Pending,
_ => Self::Unknown,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OrderChange {
Created,
PartialFill,
FullFill,
Canceled,
Expired,
Modified,
}
#[derive(Debug, Clone)]
pub struct BalanceInfo {
pub asset: String,
pub available: Decimal,
pub hold: Decimal,
pub total: Decimal,
}
impl BalanceInfo {
pub fn from_data(data: &BalanceData) -> Self {
let hold = data.hold_trade.unwrap_or(Decimal::ZERO);
Self {
asset: data.asset.clone(),
available: data.balance,
hold,
total: data.balance + hold,
}
}
}
#[derive(Debug, Clone)]
pub enum L3Event {
Snapshot {
symbol: String,
bids: Vec<L3Order>,
asks: Vec<L3Order>,
checksum: Option<u32>,
},
Update {
symbol: String,
bids: Vec<L3Order>,
asks: Vec<L3Order>,
},
}
impl L3Event {
pub fn from_data(data: &L3Data, is_snapshot: bool) -> Self {
if is_snapshot {
Self::Snapshot {
symbol: data.symbol.clone(),
bids: data.bids.clone(),
asks: data.asks.clone(),
checksum: data.checksum,
}
} else {
Self::Update {
symbol: data.symbol.clone(),
bids: data.bids.clone(),
asks: data.asks.clone(),
}
}
}
pub fn symbol(&self) -> &str {
match self {
Self::Snapshot { symbol, .. } | Self::Update { symbol, .. } => symbol,
}
}
pub fn is_snapshot(&self) -> bool {
matches!(self, Self::Snapshot { .. })
}
}
#[derive(Debug, Clone)]
pub enum Event {
Connection(ConnectionEvent),
Subscription(SubscriptionEvent),
Market(MarketEvent),
Private(Box<PrivateEvent>),
L3(L3Event),
}
impl From<ConnectionEvent> for Event {
fn from(event: ConnectionEvent) -> Self {
Event::Connection(event)
}
}
impl From<SubscriptionEvent> for Event {
fn from(event: SubscriptionEvent) -> Self {
Event::Subscription(event)
}
}
impl From<MarketEvent> for Event {
fn from(event: MarketEvent) -> Self {
Event::Market(event)
}
}
impl From<PrivateEvent> for Event {
fn from(event: PrivateEvent) -> Self {
Event::Private(Box::new(event))
}
}
impl From<L3Event> for Event {
fn from(event: L3Event) -> Self {
Event::L3(event)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_order_status_parsing() {
assert_eq!(OrderStatus::parse("pending"), OrderStatus::Pending);
assert_eq!(OrderStatus::parse("new"), OrderStatus::New);
assert_eq!(OrderStatus::parse("open"), OrderStatus::New);
assert_eq!(OrderStatus::parse("filled"), OrderStatus::Filled);
assert_eq!(OrderStatus::parse("closed"), OrderStatus::Filled);
assert_eq!(OrderStatus::parse("canceled"), OrderStatus::Canceled);
assert_eq!(OrderStatus::parse("cancelled"), OrderStatus::Canceled);
}
#[test]
fn test_order_status_states() {
assert!(OrderStatus::Pending.is_active());
assert!(OrderStatus::New.is_active());
assert!(OrderStatus::PartiallyFilled.is_active());
assert!(!OrderStatus::Filled.is_active());
assert!(!OrderStatus::Canceled.is_active());
assert!(OrderStatus::Filled.is_terminal());
assert!(OrderStatus::Canceled.is_terminal());
assert!(OrderStatus::Expired.is_terminal());
assert!(!OrderStatus::New.is_terminal());
}
#[test]
fn test_execution_type_parsing() {
assert_eq!(ExecutionType::parse("new"), ExecutionType::New);
assert_eq!(ExecutionType::parse("trade"), ExecutionType::Trade);
assert_eq!(ExecutionType::parse("filled"), ExecutionType::Trade);
assert_eq!(ExecutionType::parse("canceled"), ExecutionType::Canceled);
assert_eq!(ExecutionType::parse("cancelled"), ExecutionType::Canceled);
}
#[test]
fn test_balance_info_creation() {
let data = BalanceData {
asset: "BTC".to_string(),
balance: Decimal::new(100, 2), hold_trade: Some(Decimal::new(25, 2)), };
let info = BalanceInfo::from_data(&data);
assert_eq!(info.asset, "BTC");
assert_eq!(info.available, Decimal::new(100, 2));
assert_eq!(info.hold, Decimal::new(25, 2));
assert_eq!(info.total, Decimal::new(125, 2));
}
}