1use chrono::{DateTime, Utc};
2use derive_more::Constructor;
3use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5
6#[derive(
12 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, Constructor,
13)]
14pub struct Position {
15 pub quantity: Decimal,
20
21 pub entry_price: Option<Decimal>,
25
26 pub unrealized_pnl: Option<Decimal>,
28
29 pub margin_used: Option<Decimal>,
31
32 pub liquidation_price: Option<Decimal>,
34
35 pub leverage: Option<Decimal>,
37 pub carry: Option<Decimal>,
44
45 pub time_exchange: DateTime<Utc>,
47}
48
49impl Position {
50 pub fn is_flat(&self) -> bool {
52 self.quantity.is_zero()
53 }
54
55 pub fn is_long(&self) -> bool {
57 self.quantity > Decimal::ZERO
58 }
59
60 pub fn is_short(&self) -> bool {
63 self.quantity < Decimal::ZERO
64 }
65
66 pub fn abs_quantity(&self) -> Decimal {
68 self.quantity.abs()
69 }
70}
71
72#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
80pub enum PositionReport {
81 #[default]
85 Unreported,
86 Flat,
88 Open(Position),
91}
92
93impl PositionReport {
94 pub fn from_position(position: Position) -> Self {
97 if position.is_flat() {
98 Self::Flat
99 } else {
100 Self::Open(position)
101 }
102 }
103
104 pub fn open(&self) -> Option<&Position> {
106 match self {
107 Self::Open(position) => Some(position),
108 Self::Unreported | Self::Flat => None,
109 }
110 }
111
112 pub fn quantity(&self) -> Option<Decimal> {
115 match self {
116 Self::Unreported => None,
117 Self::Flat => Some(Decimal::ZERO),
118 Self::Open(position) => Some(position.quantity),
119 }
120 }
121
122 pub fn is_unreported(&self) -> bool {
124 matches!(self, Self::Unreported)
125 }
126}
127
128#[cfg(test)]
129#[allow(clippy::unwrap_used)] mod tests {
131 use super::*;
132 use rust_decimal_macros::dec;
133
134 #[test]
137 fn instrument_snapshot_position_serde() {
138 use crate::InstrumentAccountSnapshot;
139 use rustrade_instrument::{
140 asset::name::AssetNameExchange, exchange::ExchangeId,
141 instrument::name::InstrumentNameExchange,
142 };
143 type Snapshot =
144 InstrumentAccountSnapshot<ExchangeId, AssetNameExchange, InstrumentNameExchange>;
145
146 let open = Position::new(
147 dec!(-2),
148 Some(dec!(10)),
149 None,
150 None,
151 None,
152 None,
153 None,
154 Utc::now(),
155 );
156 for (position, expected) in [
157 (PositionReport::Unreported, None),
158 (PositionReport::Flat, Some(serde_json::json!("Flat"))),
159 (
160 PositionReport::Open(open.clone()),
161 Some(serde_json::json!({ "Open": serde_json::to_value(&open).unwrap() })),
162 ),
163 ] {
164 let snapshot = Snapshot::new("x".into(), vec![], false, position, None);
165 let json = serde_json::to_value(&snapshot).unwrap();
166 assert_eq!(json.get("position").cloned(), expected);
167 assert_eq!(serde_json::from_value::<Snapshot>(json).unwrap(), snapshot);
168 }
169 }
170
171 #[test]
172 fn position_report_from_position_reports_zero_as_flat() {
173 let now = Utc::now();
174 let open = Position::new(dec!(-2), None, None, None, None, None, None, now);
175 assert_eq!(
176 PositionReport::from_position(open.clone()),
177 PositionReport::Open(open.clone())
178 );
179 for zero in [dec!(0), -dec!(0)] {
180 let flat = Position::new(zero, None, None, None, None, None, None, now);
181 assert_eq!(PositionReport::from_position(flat), PositionReport::Flat);
182 }
183 assert_eq!(PositionReport::Open(open).quantity(), Some(dec!(-2)));
184 assert_eq!(PositionReport::Flat.quantity(), Some(Decimal::ZERO));
185 assert_eq!(PositionReport::Unreported.quantity(), None);
186 }
187
188 #[test]
189 fn test_position_side_detection() {
190 let now = Utc::now();
191
192 let long = Position::new(dec!(1.5), None, None, None, None, None, None, now);
193 assert!(long.is_long());
194 assert!(!long.is_short());
195 assert!(!long.is_flat());
196
197 let short = Position::new(dec!(-1.5), None, None, None, None, None, None, now);
198 assert!(!short.is_long());
199 assert!(short.is_short());
200 assert!(!short.is_flat());
201
202 for zero in [dec!(0), -dec!(0)] {
204 let flat = Position::new(zero, None, None, None, None, None, None, now);
205 assert!(!flat.is_long(), "{zero:?}");
206 assert!(!flat.is_short(), "{zero:?}");
207 assert!(flat.is_flat(), "{zero:?}");
208 }
209 }
210
211 #[test]
212 fn test_abs_quantity() {
213 let now = Utc::now();
214 let short = Position::new(dec!(-2.5), None, None, None, None, None, None, now);
215 assert_eq!(short.abs_quantity(), dec!(2.5));
216 }
217}