use crate::orderbook::fees::FeeSchedule;
use pricelevel::MatchResult;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TradeResult {
pub symbol: String,
pub match_result: MatchResult,
pub total_maker_fees: i128,
pub total_taker_fees: i128,
#[serde(default)]
pub engine_seq: u64,
#[serde(default)]
pub quote_notional: u128,
}
impl TradeResult {
pub fn new(symbol: String, match_result: MatchResult) -> Self {
let quote_notional = compute_quote_notional(&match_result);
Self {
symbol,
match_result,
total_maker_fees: 0,
total_taker_fees: 0,
engine_seq: 0,
quote_notional,
}
}
pub fn with_fees(
symbol: String,
match_result: MatchResult,
fee_schedule: Option<FeeSchedule>,
) -> Self {
let (total_maker_fees, total_taker_fees) = match fee_schedule {
Some(schedule) if !schedule.is_zero_fee() => {
let mut maker_sum: i128 = 0;
let mut taker_sum: i128 = 0;
for tx in match_result.trades().as_vec() {
let notional = tx
.price()
.as_u128()
.saturating_mul(tx.quantity().as_u64() as u128);
maker_sum = maker_sum
.checked_add(schedule.calculate_fee(notional, true))
.unwrap_or(maker_sum);
taker_sum = taker_sum
.checked_add(schedule.calculate_fee(notional, false))
.unwrap_or(taker_sum);
}
(maker_sum, taker_sum)
}
_ => (0, 0),
};
let quote_notional = compute_quote_notional(&match_result);
Self {
symbol,
match_result,
total_maker_fees,
total_taker_fees,
engine_seq: 0,
quote_notional,
}
}
#[must_use]
#[inline]
pub fn total_fees(&self) -> i128 {
self.total_maker_fees
.checked_add(self.total_taker_fees)
.unwrap_or(i128::MAX)
}
}
#[inline]
#[must_use]
fn compute_quote_notional(match_result: &MatchResult) -> u128 {
let mut total: u128 = 0;
for tx in match_result.trades().as_vec() {
let notional = tx
.price()
.as_u128()
.saturating_mul(u128::from(tx.quantity().as_u64()));
total = total.saturating_add(notional);
}
total
}
pub type TradeListener = Arc<dyn Fn(&TradeResult) + Send + Sync>;
#[derive(Debug, Clone)]
pub struct TradeEvent {
pub symbol: String,
pub trade_result: TradeResult,
pub timestamp: u64,
pub engine_seq: u64,
}
#[derive(Debug, Clone)]
pub struct TradeInfo {
pub symbol: String,
pub order_id: String,
pub executed_quantity: u64,
pub remaining_quantity: u64,
pub is_complete: bool,
pub transaction_count: usize,
pub transactions: Vec<TransactionInfo>,
}
#[derive(Debug, Clone)]
pub struct TransactionInfo {
pub price: u128,
pub quantity: u64,
pub transaction_id: String,
pub maker_order_id: String,
pub taker_order_id: String,
pub maker_fee: i128,
pub taker_fee: i128,
}
#[cfg(test)]
mod tests {
use super::*;
use pricelevel::{Id, MatchResult, Price, Quantity, Trade};
fn make_match_result_with_trades(trades: Vec<Trade>) -> MatchResult {
let order_id = Id::new_uuid();
let total_qty: u64 = trades.iter().map(|t| t.quantity().as_u64()).sum();
let initial_qty = if trades.is_empty() { 100 } else { total_qty };
let mut mr = MatchResult::new(order_id, initial_qty);
for trade in trades {
let _ = mr.add_trade(trade);
}
mr
}
fn make_trade(price: u128, quantity: u64) -> Trade {
Trade::new(
Id::new_uuid(),
Id::new_uuid(),
Id::new_uuid(),
Price::new(price),
Quantity::new(quantity),
pricelevel::Side::Buy,
)
}
#[test]
fn test_trade_result_new_has_zero_fees() {
let mr = make_match_result_with_trades(vec![make_trade(1000, 10)]);
let tr = TradeResult::new("BTC/USD".to_string(), mr);
assert_eq!(tr.total_maker_fees, 0);
assert_eq!(tr.total_taker_fees, 0);
assert_eq!(tr.total_fees(), 0);
}
#[test]
fn test_trade_result_with_fees_none_schedule() {
let mr = make_match_result_with_trades(vec![make_trade(1000, 10)]);
let tr = TradeResult::with_fees("BTC/USD".to_string(), mr, None);
assert_eq!(tr.total_maker_fees, 0);
assert_eq!(tr.total_taker_fees, 0);
assert_eq!(tr.total_fees(), 0);
}
#[test]
fn test_trade_result_with_fees_zero_schedule() {
let schedule = FeeSchedule::zero_fee();
let mr = make_match_result_with_trades(vec![make_trade(1000, 10)]);
let tr = TradeResult::with_fees("BTC/USD".to_string(), mr, Some(schedule));
assert_eq!(tr.total_maker_fees, 0);
assert_eq!(tr.total_taker_fees, 0);
assert_eq!(tr.total_fees(), 0);
}
#[test]
fn test_trade_result_with_fees_single_transaction() {
let schedule = FeeSchedule::new(-2, 5);
let mr = make_match_result_with_trades(vec![make_trade(1000, 10)]);
let tr = TradeResult::with_fees("BTC/USD".to_string(), mr, Some(schedule));
assert_eq!(tr.total_maker_fees, -2);
assert_eq!(tr.total_taker_fees, 5);
assert_eq!(tr.total_fees(), 3);
}
#[test]
fn test_trade_result_with_fees_multiple_transactions() {
let schedule = FeeSchedule::new(-2, 5);
let mr = make_match_result_with_trades(vec![
make_trade(1000, 10), make_trade(2000, 20), ]);
let tr = TradeResult::with_fees("BTC/USD".to_string(), mr, Some(schedule));
assert_eq!(tr.total_maker_fees, -10);
assert_eq!(tr.total_taker_fees, 25);
assert_eq!(tr.total_fees(), 15);
}
#[test]
fn test_trade_result_with_fees_no_transactions() {
let schedule = FeeSchedule::new(-2, 5);
let mr = make_match_result_with_trades(vec![]);
let tr = TradeResult::with_fees("BTC/USD".to_string(), mr, Some(schedule));
assert_eq!(tr.total_maker_fees, 0);
assert_eq!(tr.total_taker_fees, 0);
assert_eq!(tr.total_fees(), 0);
}
#[test]
fn test_trade_result_with_maker_rebate() {
let schedule = FeeSchedule::with_maker_rebate(5, 10);
let mr = make_match_result_with_trades(vec![make_trade(100_000, 50)]);
let tr = TradeResult::with_fees("BTC/USD".to_string(), mr, Some(schedule));
assert_eq!(tr.total_maker_fees, -2_500);
assert_eq!(tr.total_taker_fees, 5_000);
assert_eq!(tr.total_fees(), 2_500);
assert!(tr.total_maker_fees < 0); }
#[test]
fn test_trade_result_symbol_preserved() {
let mr = make_match_result_with_trades(vec![]);
let tr = TradeResult::with_fees("ETH/USDT".to_string(), mr, None);
assert_eq!(tr.symbol, "ETH/USDT");
}
#[test]
fn test_transaction_info_fee_fields() {
let info = TransactionInfo {
price: 50_000,
quantity: 10,
transaction_id: "tx-1".to_string(),
maker_order_id: "maker-1".to_string(),
taker_order_id: "taker-1".to_string(),
maker_fee: -25,
taker_fee: 50,
};
assert_eq!(info.maker_fee, -25);
assert_eq!(info.taker_fee, 50);
}
#[test]
fn test_trade_result_engine_seq_default_zero() {
let mr = make_match_result_with_trades(vec![make_trade(1000, 10)]);
let tr = TradeResult::new("BTC/USD".to_string(), mr);
assert_eq!(tr.engine_seq, 0);
}
#[test]
fn test_trade_result_json_roundtrip_preserves_engine_seq() {
let mr = make_match_result_with_trades(vec![make_trade(1000, 10)]);
let mut tr = TradeResult::new("BTC/USD".to_string(), mr);
tr.engine_seq = 42;
let json = serde_json::to_vec(&tr).expect("serialize trade");
let decoded: TradeResult = serde_json::from_slice(&json).expect("deserialize trade");
assert_eq!(decoded.engine_seq, 42);
assert_eq!(decoded.symbol, tr.symbol);
assert_eq!(decoded.total_maker_fees, tr.total_maker_fees);
assert_eq!(decoded.total_taker_fees, tr.total_taker_fees);
}
#[test]
fn test_trade_result_json_missing_engine_seq_defaults_zero() {
let mr = make_match_result_with_trades(vec![make_trade(1000, 10)]);
let mut tr = TradeResult::new("BTC/USD".to_string(), mr);
tr.engine_seq = 99;
let mut value: serde_json::Value =
serde_json::to_value(&tr).expect("serialize trade to value");
if let Some(map) = value.as_object_mut() {
map.remove("engine_seq");
}
let bytes = serde_json::to_vec(&value).expect("serialize stripped value");
let decoded: TradeResult =
serde_json::from_slice(&bytes).expect("deserialize stripped trade");
assert_eq!(
decoded.engine_seq, 0,
"missing engine_seq must default to 0 via #[serde(default)]"
);
}
#[test]
fn test_trade_result_new_populates_quote_notional() {
let mr = make_match_result_with_trades(vec![make_trade(1000, 10)]);
let tr = TradeResult::new("BTC/USD".to_string(), mr);
assert_eq!(tr.quote_notional, 10_000);
}
#[test]
fn test_trade_result_with_fees_populates_quote_notional_multi_trade() {
let mr = make_match_result_with_trades(vec![make_trade(1000, 10), make_trade(2000, 20)]);
let tr = TradeResult::with_fees("BTC/USD".to_string(), mr, None);
assert_eq!(tr.quote_notional, 50_000);
}
#[test]
fn test_trade_result_quote_notional_zero_when_no_trades() {
let mr = make_match_result_with_trades(vec![]);
let tr = TradeResult::new("BTC/USD".to_string(), mr);
assert_eq!(tr.quote_notional, 0);
}
#[test]
fn test_trade_result_json_missing_quote_notional_defaults_zero() {
let mr = make_match_result_with_trades(vec![make_trade(1000, 10)]);
let tr = TradeResult::new("BTC/USD".to_string(), mr);
let mut value: serde_json::Value =
serde_json::to_value(&tr).expect("serialize trade to value");
if let Some(map) = value.as_object_mut() {
map.remove("quote_notional");
}
let bytes = serde_json::to_vec(&value).expect("serialize stripped value");
let decoded: TradeResult =
serde_json::from_slice(&bytes).expect("deserialize stripped trade");
assert_eq!(
decoded.quote_notional, 0,
"missing quote_notional must default to 0 via #[serde(default)]"
);
}
#[test]
fn test_trade_result_json_roundtrip_preserves_quote_notional() {
let mr = make_match_result_with_trades(vec![make_trade(1000, 10), make_trade(2000, 5)]);
let tr = TradeResult::new("BTC/USD".to_string(), mr);
let original = tr.quote_notional;
assert_eq!(original, 20_000);
let json = serde_json::to_vec(&tr).expect("serialize trade");
let decoded: TradeResult = serde_json::from_slice(&json).expect("deserialize trade");
assert_eq!(decoded.quote_notional, original);
}
#[cfg(feature = "bincode")]
#[test]
fn test_trade_result_bincode_roundtrip_preserves_quote_notional() {
use bincode::config::standard;
use bincode::serde::{decode_from_slice, encode_to_vec};
let mr = make_match_result_with_trades(vec![make_trade(1234, 7)]);
let tr = TradeResult::new("BTC/USD".to_string(), mr);
let original = tr.quote_notional;
assert_eq!(original, 8_638);
let bytes = encode_to_vec(&tr, standard()).expect("bincode encode");
let (decoded, consumed): (TradeResult, usize) =
decode_from_slice(&bytes, standard()).expect("bincode decode");
assert_eq!(consumed, bytes.len());
assert_eq!(decoded.quote_notional, original);
}
#[cfg(feature = "bincode")]
#[test]
fn test_trade_result_bincode_roundtrip_preserves_engine_seq() {
use bincode::config::standard;
use bincode::serde::{decode_from_slice, encode_to_vec};
let mr = make_match_result_with_trades(vec![make_trade(1000, 10)]);
let mut tr = TradeResult::new("BTC/USD".to_string(), mr);
tr.engine_seq = 7;
let bytes = encode_to_vec(&tr, standard()).expect("bincode encode");
let (decoded, consumed): (TradeResult, usize) =
decode_from_slice(&bytes, standard()).expect("bincode decode");
assert_eq!(consumed, bytes.len(), "no trailing bytes expected");
assert_eq!(decoded.engine_seq, 7);
assert_eq!(decoded.symbol, tr.symbol);
}
}