perpl_sdk/types/trade.rs
1use fastnum::UD64;
2
3use super::BuilderAttribution;
4
5/// A single maker fill within a taker trade.
6#[derive(Clone, derive_more::Debug)]
7pub struct MakerFill {
8 /// Log index of this maker fill event.
9 pub log_index: u64,
10
11 /// Maker account ID.
12 pub maker_account_id: super::AccountId,
13
14 /// Maker order ID.
15 pub maker_order_id: super::OrderId,
16
17 /// Maker client order ID, if known.
18 ///
19 /// Available only when the order placement was observed in processed
20 /// events, not for orders loaded from the initial snapshot.
21 pub maker_client_order_id: Option<super::RequestId>,
22
23 /// Fill price (normalized decimal).
24 #[debug("{price}")]
25 pub price: UD64,
26
27 /// Fill size (normalized decimal).
28 #[debug("{size}")]
29 pub size: UD64,
30
31 /// Maker fee paid (normalized decimal, in collateral token).
32 #[debug("{fee}")]
33 pub fee: UD64,
34
35 /// Builder the maker order is attributed to, with the fee rate it charges,
36 /// if any.
37 ///
38 /// Available only when the order placement was observed in processed events
39 /// or recovered from the initial snapshot, on contract v1.1.7.4+.
40 pub builder: Option<super::BuilderAttribution>,
41
42 /// Builder fee earned on this fill (normalized decimal, in collateral
43 /// token).
44 ///
45 /// Included in [`Self::fee`], so consumers must not add it on top. Zero on
46 /// contracts without builder attribution, and on close/decrease fills
47 /// before contract v1.1.7.5 (which charges every size-changing fill,
48 /// exits included) - even when [`Self::builder`] is set.
49 #[debug("{builder_fee}")]
50 pub builder_fee: UD64,
51}
52
53/// A complete trade event: one taker matched against one or more makers.
54///
55/// Each `TakerTrade` represents a single taker order execution that may have
56/// matched against multiple maker orders. The `maker_fills` vector contains
57/// all individual maker fills that occurred as part of this trade.
58#[derive(Clone, derive_more::Debug)]
59pub struct Trade {
60 /// Perpetual contract ID.
61 pub perpetual_id: super::PerpetualId,
62
63 /// Taker account ID.
64 pub taker_account_id: super::AccountId,
65
66 /// Taker request ID.
67 pub taker_request_id: super::RequestId,
68
69 /// Taker side (Bid = buying, Ask = selling).
70 pub taker_side: super::OrderSide,
71
72 /// Taker fee paid (normalized decimal, in collateral token).
73 #[debug("{taker_fee}")]
74 pub taker_fee: UD64,
75
76 /// Builder the taker order is attributed to, with the fee rate it charges,
77 /// if any.
78 pub taker_builder: Option<BuilderAttribution>,
79
80 /// Builder fee earned on the taker side (normalized decimal, in collateral
81 /// token).
82 ///
83 /// Included in [`Self::taker_fee`], so consumers must not add it on top.
84 #[debug("{taker_builder_fee}")]
85 pub taker_builder_fee: UD64,
86
87 /// All maker fills matched by this taker order.
88 pub maker_fills: Vec<MakerFill>,
89}
90
91impl Trade {
92 /// Total size filled across all makers.
93 pub fn total_size(&self) -> UD64 { self.maker_fills.iter().map(|f| f.size).sum() }
94
95 /// Volume-weighted average price across all maker fills.
96 ///
97 /// Returns `None` if there are no fills.
98 pub fn avg_price(&self) -> Option<UD64> {
99 if self.maker_fills.is_empty() {
100 return None;
101 }
102 let total_value: UD64 = self.maker_fills.iter().map(|f| f.price * f.size).sum();
103 let total_size = self.total_size();
104 if total_size == UD64::ZERO {
105 return None;
106 }
107 Some(total_value / total_size)
108 }
109
110 /// Total maker fees paid across all fills.
111 pub fn total_maker_fees(&self) -> UD64 { self.maker_fills.iter().map(|f| f.fee).sum() }
112
113 /// Total builder fees earned on this trade, taker and maker sides combined.
114 ///
115 /// Part of [`Self::taker_fee`] and the maker fees, not additional to them.
116 pub fn total_builder_fees(&self) -> UD64 {
117 self.taker_builder_fee + self.maker_fills.iter().map(|f| f.builder_fee).sum::<UD64>()
118 }
119
120 /// Total builder fees earned by a specific builder on this trade.
121 pub fn builder_total(&self, builder_id: super::BuilderId) -> UD64 {
122 let taker = self
123 .taker_builder
124 .filter(|b| b.builder_id() == builder_id)
125 .map(|_| self.taker_builder_fee)
126 .unwrap_or(UD64::ZERO);
127 taker
128 + self
129 .maker_fills
130 .iter()
131 .filter(|f| f.builder.is_some_and(|b| b.builder_id() == builder_id))
132 .map(|f| f.builder_fee)
133 .sum::<UD64>()
134 }
135
136 /// Volume-weighted average price, total size and total fees for a specific
137 /// maker.
138 ///
139 /// Returns `None` if the maker has no fills in this trade.
140 pub fn maker_total(&self, account_id: super::AccountId) -> Option<(UD64, UD64, UD64)> {
141 let mut total_value = UD64::ZERO;
142 let mut total_size = UD64::ZERO;
143 let mut total_fee = UD64::ZERO;
144 for fill in self
145 .maker_fills
146 .iter()
147 .filter(|f| f.maker_account_id == account_id)
148 {
149 total_value += fill.price * fill.size;
150 total_size += fill.size;
151 total_fee += fill.fee
152 }
153 if total_size == UD64::ZERO {
154 return None;
155 }
156 Some((total_value / total_size, total_size, total_fee))
157 }
158}