1use crate::prelude::*;
3
4use super::config::Config;
5
6impl Config {
7 pub fn calculate_trade_fee(
11 &self,
12 old_notional_size_in_collateral: Signed<Collateral>,
13 new_notional_size_in_collateral: Signed<Collateral>,
14 old_counter_collateral: Collateral,
15 new_counter_collateral: Collateral,
16 ) -> Result<Collateral> {
17 debug_assert!(
18 old_notional_size_in_collateral.is_zero()
19 || (old_notional_size_in_collateral.is_negative()
20 == new_notional_size_in_collateral.is_negative())
21 );
22 let old_notional_size_in_collateral = old_notional_size_in_collateral.abs_unsigned();
23 let new_notional_size_in_collateral = new_notional_size_in_collateral.abs_unsigned();
24 let notional_size_fee = match new_notional_size_in_collateral
25 .checked_sub(old_notional_size_in_collateral)
26 .ok()
27 {
28 Some(delta) => {
29 debug_assert!(old_notional_size_in_collateral <= new_notional_size_in_collateral);
30 delta.checked_mul_dec(self.trading_fee_notional_size)?
31 }
32 None => {
33 debug_assert!(old_notional_size_in_collateral > new_notional_size_in_collateral);
34 Collateral::zero()
35 }
36 };
37 let counter_collateral_fee = match new_counter_collateral
38 .checked_sub(old_counter_collateral)
39 .ok()
40 {
41 Some(delta) => {
42 debug_assert!(old_counter_collateral <= new_counter_collateral);
43 delta.checked_mul_dec(self.trading_fee_counter_collateral)?
44 }
45 None => {
46 debug_assert!(old_counter_collateral > new_counter_collateral);
47 Collateral::zero()
48 }
49 };
50 notional_size_fee
51 .checked_add(counter_collateral_fee)
52 .context("Overflow when calculating trading fee")
53 }
54
55 pub fn calculate_trade_fee_open(
57 &self,
58 notional_size_in_collateral: Signed<Collateral>,
59 counter_collateral: Collateral,
60 ) -> Result<Collateral> {
61 self.calculate_trade_fee(
62 Signed::zero(),
63 notional_size_in_collateral,
64 Collateral::zero(),
65 counter_collateral,
66 )
67 }
68}
69
70pub mod events {
72 use super::*;
73 use crate::contracts::market::order::OrderId;
74 use crate::contracts::market::position::PositionId;
75 use crate::{constants::event_key, contracts::market::deferred_execution::DeferredExecId};
76 use cosmwasm_std::{Decimal256, Event};
77
78 #[derive(Debug, Clone)]
80 pub enum TradeId {
81 Position(PositionId),
83 LimitOrder(OrderId),
85 Deferred(DeferredExecId),
87 }
88
89 #[derive(Debug, Clone, Copy)]
91 pub enum FeeSource {
92 Trading,
94 Borrow,
96 DeltaNeutrality,
98 }
99
100 impl FeeSource {
101 fn as_str(self) -> &'static str {
102 match self {
103 FeeSource::Trading => "trading",
104 FeeSource::Borrow => "borrow",
105 FeeSource::DeltaNeutrality => "delta-neutrality",
106 }
107 }
108 }
109
110 impl FromStr for FeeSource {
111 type Err = anyhow::Error;
112
113 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
114 match s {
115 "trading" => Ok(FeeSource::Trading),
116 "borrow" => Ok(FeeSource::Borrow),
117 "delta-neutrality" => Ok(FeeSource::DeltaNeutrality),
118 _ => Err(anyhow::anyhow!("Unknown FeeSource {s}")),
119 }
120 }
121 }
122
123 #[derive(Debug, Clone)]
125 pub struct FeeEvent {
126 pub trade_id: TradeId,
128 pub fee_source: FeeSource,
130 pub lp_amount: Collateral,
132 pub lp_amount_usd: Usd,
134 pub xlp_amount: Collateral,
136 pub xlp_amount_usd: Usd,
138 pub protocol_amount: Collateral,
140 pub protocol_amount_usd: Usd,
142 }
143
144 impl From<FeeEvent> for Event {
145 fn from(
146 FeeEvent {
147 trade_id,
148 fee_source,
149 lp_amount,
150 lp_amount_usd,
151 xlp_amount,
152 xlp_amount_usd,
153 protocol_amount,
154 protocol_amount_usd,
155 }: FeeEvent,
156 ) -> Self {
157 let (trade_id_key, trade_id_val) = match trade_id {
158 TradeId::Position(pos_id) => ("pos-id", pos_id.to_string()),
159 TradeId::LimitOrder(order_id) => ("order-id", order_id.to_string()),
160 TradeId::Deferred(deferred_id) => ("deferred-id", deferred_id.to_string()),
161 };
162
163 Event::new("fee")
164 .add_attribute(trade_id_key, trade_id_val)
165 .add_attribute("source", fee_source.as_str())
166 .add_attribute("lp-amount", lp_amount.to_string())
167 .add_attribute("lp-amount-usd", lp_amount_usd.to_string())
168 .add_attribute("xlp-amount", xlp_amount.to_string())
169 .add_attribute("xlp-amount-usd", xlp_amount_usd.to_string())
170 .add_attribute("protocol-amount", protocol_amount.to_string())
171 .add_attribute("protocol-amount-usd", protocol_amount_usd.to_string())
172 }
173 }
174 impl TryFrom<Event> for FeeEvent {
175 type Error = anyhow::Error;
176
177 fn try_from(evt: Event) -> anyhow::Result<Self> {
178 let trade_id = match evt.try_u64_attr("pos-id")? {
179 Some(pos_id) => TradeId::Position(PositionId::new(pos_id)),
180 None => match evt.try_u64_attr("order-id")? {
181 Some(order_id) => TradeId::LimitOrder(OrderId::new(order_id)),
182 None => {
183 let deferred_id = evt.u64_attr("deferred-id")?;
184 TradeId::Deferred(DeferredExecId::from_u64(deferred_id))
185 }
186 },
187 };
188
189 Ok(FeeEvent {
190 trade_id,
191 fee_source: evt.string_attr("source")?.parse()?,
192 lp_amount: evt.decimal_attr("lp-amount")?,
193 lp_amount_usd: evt.decimal_attr("lp-amount-usd")?,
194 xlp_amount: evt.decimal_attr("xlp-amount")?,
195 xlp_amount_usd: evt.decimal_attr("xlp-amount-usd")?,
196 protocol_amount: evt.decimal_attr("protocol-amount")?,
197 protocol_amount_usd: evt.decimal_attr("protocol-amount-usd")?,
198 })
199 }
200 }
201
202 pub struct FundingPaymentEvent {
204 pub pos_id: PositionId,
206 pub amount: Signed<Collateral>,
208 pub amount_usd: Signed<Usd>,
210 pub direction: DirectionToBase,
212 }
213
214 impl From<&FundingPaymentEvent> for Event {
215 fn from(src: &FundingPaymentEvent) -> Self {
216 Event::new("funding-payment")
217 .add_attribute("pos-id", src.pos_id.to_string())
218 .add_attribute("amount", src.amount.to_string())
219 .add_attribute("amount-usd", src.amount_usd.to_string())
220 .add_attribute(event_key::DIRECTION, src.direction.as_str())
221 }
222 }
223 impl From<FundingPaymentEvent> for Event {
224 fn from(src: FundingPaymentEvent) -> Self {
225 (&src).into()
226 }
227 }
228
229 impl TryFrom<Event> for FundingPaymentEvent {
230 type Error = anyhow::Error;
231
232 fn try_from(evt: Event) -> anyhow::Result<Self> {
233 Ok(FundingPaymentEvent {
234 pos_id: PositionId::new(evt.u64_attr("pos-id")?),
235 amount: evt.number_attr("amount")?,
236 amount_usd: evt.number_attr("amount-usd")?,
237 direction: evt.direction_attr(event_key::DIRECTION)?,
238 })
239 }
240 }
241
242 pub struct FundingRateChangeEvent {
244 pub time: Timestamp,
246 pub long_rate_base: Number,
248 pub short_rate_base: Number,
250 }
251
252 impl From<FundingRateChangeEvent> for Event {
253 fn from(
254 FundingRateChangeEvent {
255 time,
256 long_rate_base,
257 short_rate_base,
258 }: FundingRateChangeEvent,
259 ) -> Self {
260 Event::new("funding-rate-change")
261 .add_attribute("time", time.to_string())
262 .add_attribute("long-rate", long_rate_base.to_string())
263 .add_attribute("short-rate", short_rate_base.to_string())
264 }
265 }
266
267 impl TryFrom<Event> for FundingRateChangeEvent {
268 type Error = anyhow::Error;
269
270 fn try_from(evt: Event) -> anyhow::Result<Self> {
271 Ok(FundingRateChangeEvent {
272 time: evt.timestamp_attr("time")?,
273 long_rate_base: evt.number_attr("long-rate-base")?,
274 short_rate_base: evt.number_attr("short-rate-base")?,
275 })
276 }
277 }
278
279 pub struct BorrowFeeChangeEvent {
281 pub time: Timestamp,
283 pub total_rate: Decimal256,
285 pub lp_rate: Decimal256,
287 pub xlp_rate: Decimal256,
289 }
290
291 impl From<BorrowFeeChangeEvent> for Event {
292 fn from(
293 BorrowFeeChangeEvent {
294 time,
295 total_rate,
296 lp_rate,
297 xlp_rate,
298 }: BorrowFeeChangeEvent,
299 ) -> Self {
300 Event::new("borrow-fee-change")
301 .add_attribute("time", time.to_string())
302 .add_attribute("total", total_rate.to_string())
303 .add_attribute("lp", lp_rate.to_string())
304 .add_attribute("xlp", xlp_rate.to_string())
305 }
306 }
307
308 impl TryFrom<Event> for BorrowFeeChangeEvent {
309 type Error = anyhow::Error;
310
311 fn try_from(evt: Event) -> Result<Self, Self::Error> {
312 Ok(BorrowFeeChangeEvent {
313 time: evt.timestamp_attr("time")?,
314 total_rate: evt.decimal_attr("total")?,
315 lp_rate: evt.decimal_attr("lp")?,
316 xlp_rate: evt.decimal_attr("xlp")?,
317 })
318 }
319 }
320
321 pub struct CrankFeeEvent {
323 pub trade_id: TradeId,
325 pub amount: Collateral,
327 pub amount_usd: Usd,
329 pub old_balance: Collateral,
331 pub new_balance: Collateral,
333 }
334
335 impl From<CrankFeeEvent> for Event {
336 fn from(
337 CrankFeeEvent {
338 trade_id,
339 amount,
340 amount_usd,
341 old_balance,
342 new_balance,
343 }: CrankFeeEvent,
344 ) -> Self {
345 let (trade_id_key, trade_id_val) = match trade_id {
346 TradeId::Position(pos_id) => ("pos-id", pos_id.to_string()),
347 TradeId::LimitOrder(order_id) => ("order-id", order_id.to_string()),
348 TradeId::Deferred(deferred_id) => ("deferred-id", deferred_id.to_string()),
349 };
350
351 Event::new("crank-fee")
352 .add_attribute(trade_id_key, trade_id_val)
353 .add_attribute("amount", amount.to_string())
354 .add_attribute("amount-usd", amount_usd.to_string())
355 .add_attribute("old-balance", old_balance.to_string())
356 .add_attribute("new-balance", new_balance.to_string())
357 }
358 }
359 impl TryFrom<Event> for CrankFeeEvent {
360 type Error = anyhow::Error;
361
362 fn try_from(evt: Event) -> anyhow::Result<Self> {
363 let trade_id = match evt.try_u64_attr("pos-id")? {
364 Some(pos_id) => TradeId::Position(PositionId::new(pos_id)),
365 None => match evt.try_u64_attr("order-id")? {
366 Some(order_id) => TradeId::LimitOrder(OrderId::new(order_id)),
367 None => {
368 let deferred_id = evt.u64_attr("deferred-id")?;
369 TradeId::Deferred(DeferredExecId::from_u64(deferred_id))
370 }
371 },
372 };
373
374 Ok(CrankFeeEvent {
375 trade_id,
376 amount: evt.decimal_attr("amount")?,
377 amount_usd: evt.decimal_attr("amount-usd")?,
378 old_balance: evt.decimal_attr("old-balance")?,
379 new_balance: evt.decimal_attr("new-balance")?,
380 })
381 }
382 }
383
384 pub struct CrankFeeEarnedEvent {
386 pub recipient: Addr,
388 pub amount: NonZero<Collateral>,
390 pub amount_usd: NonZero<Usd>,
392 }
393
394 impl From<CrankFeeEarnedEvent> for Event {
395 fn from(
396 CrankFeeEarnedEvent {
397 recipient,
398 amount,
399 amount_usd,
400 }: CrankFeeEarnedEvent,
401 ) -> Self {
402 Event::new("crank-fee-claimed")
403 .add_attribute("recipient", recipient.to_string())
404 .add_attribute("amount", amount.to_string())
405 .add_attribute("amount-usd", amount_usd.to_string())
406 }
407 }
408 impl TryFrom<Event> for CrankFeeEarnedEvent {
409 type Error = anyhow::Error;
410
411 fn try_from(evt: Event) -> anyhow::Result<Self> {
412 Ok(CrankFeeEarnedEvent {
413 recipient: evt.unchecked_addr_attr("recipient")?,
414 amount: evt.non_zero_attr("amount")?,
415 amount_usd: evt.non_zero_attr("amount-usd")?,
416 })
417 }
418 }
419
420 pub struct InsufficientMarginEvent {
422 pub pos: PositionId,
424 pub fee_type: FeeType,
426 pub available: Signed<Collateral>,
428 pub requested: Signed<Collateral>,
430 pub desc: Option<String>,
432 }
433 impl From<&InsufficientMarginEvent> for Event {
434 fn from(
435 InsufficientMarginEvent {
436 pos,
437 fee_type,
438 available,
439 requested,
440 desc,
441 }: &InsufficientMarginEvent,
442 ) -> Self {
443 let evt = Event::new(event_key::INSUFFICIENT_MARGIN)
444 .add_attribute(event_key::POS_ID, pos.to_string())
445 .add_attribute(event_key::FEE_TYPE, fee_type.as_str())
446 .add_attribute(event_key::AVAILABLE, available.to_string())
447 .add_attribute(event_key::REQUESTED, requested.to_string());
448 match desc {
449 Some(desc) => evt.add_attribute(event_key::DESC, desc),
450 None => evt,
451 }
452 }
453 }
454 impl From<InsufficientMarginEvent> for Event {
455 fn from(event: InsufficientMarginEvent) -> Self {
456 (&event).into()
457 }
458 }
459
460 #[derive(Clone, Copy, PartialEq, Eq, Debug)]
462 pub enum FeeType {
463 Overall,
465 Borrow,
467 DeltaNeutrality,
469 Funding,
471 Crank,
473 FundingTotal,
478 }
479
480 impl FeeType {
481 pub fn as_str(self) -> &'static str {
483 match self {
484 FeeType::Overall => "overall",
485 FeeType::Borrow => "borrow",
486 FeeType::DeltaNeutrality => "delta-neutrality",
487 FeeType::Funding => "funding",
488 FeeType::FundingTotal => "funding-total",
489 FeeType::Crank => "crank",
490 }
491 }
492 }
493}
494
495#[cfg(test)]
496mod tests {
497 use crate::contracts::market::spot_price::SpotPriceConfig;
498
499 use super::*;
500
501 #[test]
502 fn trade_fee_open() {
503 let config = Config {
504 trading_fee_notional_size: "0.01".parse().unwrap(),
505 trading_fee_counter_collateral: "0.02".parse().unwrap(),
506 ..Config::new(SpotPriceConfig::Manual {
507 admin: Addr::unchecked("foo"),
508 })
509 };
510 assert_eq!(
511 config
512 .calculate_trade_fee_open("-500".parse().unwrap(), "200".parse().unwrap())
513 .unwrap(),
514 "9".parse().unwrap()
515 )
516 }
517
518 #[test]
519 fn trade_fee_update() {
520 let config = Config {
521 trading_fee_notional_size: "0.01".parse().unwrap(),
522 trading_fee_counter_collateral: "0.02".parse().unwrap(),
523 ..Config::new(SpotPriceConfig::Manual {
524 admin: Addr::unchecked("foo"),
525 })
526 };
527 assert_eq!(
528 config
529 .calculate_trade_fee(
530 "-100".parse().unwrap(),
531 "-500".parse().unwrap(),
532 "100".parse().unwrap(),
533 "200".parse().unwrap()
534 )
535 .unwrap(),
536 "6".parse().unwrap()
537 );
538 assert_eq!(
539 config
540 .calculate_trade_fee(
541 "-100".parse().unwrap(),
542 "-500".parse().unwrap(),
543 "300".parse().unwrap(),
544 "200".parse().unwrap()
545 )
546 .unwrap(),
547 "4".parse().unwrap()
548 );
549 assert_eq!(
550 config
551 .calculate_trade_fee(
552 "-600".parse().unwrap(),
553 "-500".parse().unwrap(),
554 "300".parse().unwrap(),
555 "200".parse().unwrap()
556 )
557 .unwrap(),
558 "0".parse().unwrap()
559 );
560 assert_eq!(
561 config
562 .calculate_trade_fee(
563 "-600".parse().unwrap(),
564 "-500".parse().unwrap(),
565 "100".parse().unwrap(),
566 "200".parse().unwrap()
567 )
568 .unwrap(),
569 "2".parse().unwrap()
570 );
571 }
572}