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
38 pub time_exchange: DateTime<Utc>,
40}
41
42impl Position {
43 pub fn is_flat(&self) -> bool {
45 self.quantity.is_zero()
46 }
47
48 pub fn is_long(&self) -> bool {
50 self.quantity > Decimal::ZERO
51 }
52
53 pub fn is_short(&self) -> bool {
56 self.quantity < Decimal::ZERO
57 }
58
59 pub fn abs_quantity(&self) -> Decimal {
61 self.quantity.abs()
62 }
63}
64
65#[derive(Debug, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
73pub enum PositionReport {
74 #[default]
78 Unreported,
79 Flat,
81 Open(Position),
84}
85
86impl PositionReport {
87 pub fn from_position(position: Position) -> Self {
90 if position.is_flat() {
91 Self::Flat
92 } else {
93 Self::Open(position)
94 }
95 }
96
97 pub fn open(&self) -> Option<&Position> {
99 match self {
100 Self::Open(position) => Some(position),
101 Self::Unreported | Self::Flat => None,
102 }
103 }
104
105 pub fn quantity(&self) -> Option<Decimal> {
108 match self {
109 Self::Unreported => None,
110 Self::Flat => Some(Decimal::ZERO),
111 Self::Open(position) => Some(position.quantity),
112 }
113 }
114
115 pub fn is_unreported(&self) -> bool {
117 matches!(self, Self::Unreported)
118 }
119}
120
121#[cfg(test)]
122#[allow(clippy::unwrap_used)] mod tests {
124 use super::*;
125 use rust_decimal_macros::dec;
126
127 #[test]
130 fn instrument_snapshot_position_serde() {
131 use crate::InstrumentAccountSnapshot;
132 use rustrade_instrument::{
133 asset::name::AssetNameExchange, exchange::ExchangeId,
134 instrument::name::InstrumentNameExchange,
135 };
136 type Snapshot =
137 InstrumentAccountSnapshot<ExchangeId, AssetNameExchange, InstrumentNameExchange>;
138
139 let open = Position::new(dec!(-2), Some(dec!(10)), None, None, None, None, Utc::now());
140 for (position, expected) in [
141 (PositionReport::Unreported, None),
142 (PositionReport::Flat, Some(serde_json::json!("Flat"))),
143 (
144 PositionReport::Open(open.clone()),
145 Some(serde_json::json!({ "Open": serde_json::to_value(&open).unwrap() })),
146 ),
147 ] {
148 let snapshot = Snapshot::new("x".into(), vec![], false, position, None);
149 let json = serde_json::to_value(&snapshot).unwrap();
150 assert_eq!(json.get("position").cloned(), expected);
151 assert_eq!(serde_json::from_value::<Snapshot>(json).unwrap(), snapshot);
152 }
153 }
154
155 #[test]
156 fn position_report_from_position_reports_zero_as_flat() {
157 let now = Utc::now();
158 let open = Position::new(dec!(-2), None, None, None, None, None, now);
159 assert_eq!(
160 PositionReport::from_position(open.clone()),
161 PositionReport::Open(open.clone())
162 );
163 for zero in [dec!(0), -dec!(0)] {
164 let flat = Position::new(zero, None, None, None, None, None, now);
165 assert_eq!(PositionReport::from_position(flat), PositionReport::Flat);
166 }
167 assert_eq!(PositionReport::Open(open).quantity(), Some(dec!(-2)));
168 assert_eq!(PositionReport::Flat.quantity(), Some(Decimal::ZERO));
169 assert_eq!(PositionReport::Unreported.quantity(), None);
170 }
171
172 #[test]
173 fn test_position_side_detection() {
174 let now = Utc::now();
175
176 let long = Position::new(dec!(1.5), None, None, None, None, None, now);
177 assert!(long.is_long());
178 assert!(!long.is_short());
179 assert!(!long.is_flat());
180
181 let short = Position::new(dec!(-1.5), None, None, None, None, None, now);
182 assert!(!short.is_long());
183 assert!(short.is_short());
184 assert!(!short.is_flat());
185
186 for zero in [dec!(0), -dec!(0)] {
188 let flat = Position::new(zero, None, None, None, None, None, now);
189 assert!(!flat.is_long(), "{zero:?}");
190 assert!(!flat.is_short(), "{zero:?}");
191 assert!(flat.is_flat(), "{zero:?}");
192 }
193 }
194
195 #[test]
196 fn test_abs_quantity() {
197 let now = Utc::now();
198 let short = Position::new(dec!(-2.5), None, None, None, None, None, now);
199 assert_eq!(short.abs_quantity(), dec!(2.5));
200 }
201}