wickra_exchange_core/
positions.rs1use crate::options::MarginMode;
10use crate::symbol::Symbol;
11use crate::types::OrderSide;
12use rust_decimal::Decimal;
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17pub enum PositionSide {
18 Long,
20 Short,
22}
23
24impl PositionSide {
25 #[must_use]
27 pub fn sign(self) -> Decimal {
28 match self {
29 PositionSide::Long => Decimal::ONE,
30 PositionSide::Short => Decimal::NEGATIVE_ONE,
31 }
32 }
33
34 #[must_use]
52 pub fn for_order(side: OrderSide, reduce_only: bool) -> Self {
53 match (side, reduce_only) {
54 (OrderSide::Buy, false) | (OrderSide::Sell, true) => PositionSide::Long,
55 (OrderSide::Sell, false) | (OrderSide::Buy, true) => PositionSide::Short,
56 }
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62pub struct Position {
63 pub symbol: Symbol,
65 pub side: PositionSide,
67 pub quantity: Decimal,
69 pub entry_price: Decimal,
71 pub mark_price: Decimal,
73 pub leverage: Decimal,
75 pub unrealized_pnl: Decimal,
77 pub margin_mode: MarginMode,
79}
80
81impl Position {
82 #[must_use]
84 pub fn signed_quantity(&self) -> Decimal {
85 self.quantity * self.side.sign()
86 }
87
88 #[must_use]
90 pub fn notional(&self) -> Decimal {
91 self.quantity * self.mark_price
92 }
93
94 #[must_use]
98 pub fn computed_unrealized_pnl(&self) -> Decimal {
99 (self.mark_price - self.entry_price) * self.signed_quantity()
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use rust_decimal_macros::dec;
107
108 fn position(side: PositionSide, entry: Decimal, mark: Decimal) -> Position {
109 Position {
110 symbol: Symbol::new("BTC", "USDT"),
111 side,
112 quantity: dec!(2),
113 entry_price: entry,
114 mark_price: mark,
115 leverage: dec!(10),
116 unrealized_pnl: dec!(0),
117 margin_mode: MarginMode::Cross,
118 }
119 }
120
121 #[test]
122 fn hedge_side_follows_side_and_reduce_only() {
123 assert_eq!(
125 PositionSide::for_order(OrderSide::Buy, false),
126 PositionSide::Long
127 );
128 assert_eq!(
129 PositionSide::for_order(OrderSide::Sell, false),
130 PositionSide::Short
131 );
132 assert_eq!(
134 PositionSide::for_order(OrderSide::Buy, true),
135 PositionSide::Short
136 );
137 assert_eq!(
138 PositionSide::for_order(OrderSide::Sell, true),
139 PositionSide::Long
140 );
141 }
142
143 #[test]
144 fn side_sign() {
145 assert_eq!(PositionSide::Long.sign(), dec!(1));
146 assert_eq!(PositionSide::Short.sign(), dec!(-1));
147 }
148
149 #[test]
150 fn signed_quantity_and_notional() {
151 let long = position(PositionSide::Long, dec!(100), dec!(110));
152 assert_eq!(long.signed_quantity(), dec!(2));
153 assert_eq!(long.notional(), dec!(220));
154
155 let short = position(PositionSide::Short, dec!(100), dec!(110));
156 assert_eq!(short.signed_quantity(), dec!(-2));
157 assert_eq!(short.notional(), dec!(220));
158 }
159
160 #[test]
161 fn computed_pnl_for_both_sides() {
162 let long = position(PositionSide::Long, dec!(100), dec!(110));
164 assert_eq!(long.computed_unrealized_pnl(), dec!(20));
165 let short = position(PositionSide::Short, dec!(100), dec!(110));
167 assert_eq!(short.computed_unrealized_pnl(), dec!(-20));
168 let short_win = position(PositionSide::Short, dec!(100), dec!(90));
170 assert_eq!(short_win.computed_unrealized_pnl(), dec!(20));
171 }
172}