1use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5
6use super::Money;
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10pub struct Position {
11 symbol: String,
13 exchange: String,
15 instrument_id: Option<String>,
17 quantity: Decimal,
19 lot_size: u32,
21 current_price: Money,
23 average_cost: Option<Money>,
25 blocked: bool,
27 blocked_lots: u32,
29}
30
31impl Position {
32 #[must_use]
34 pub fn new(
35 symbol: impl Into<String>,
36 exchange: impl Into<String>,
37 quantity: Decimal,
38 current_price: Money,
39 ) -> Self {
40 Self {
41 symbol: symbol.into(),
42 exchange: exchange.into(),
43 instrument_id: None,
44 quantity,
45 lot_size: 1,
46 current_price,
47 average_cost: None,
48 blocked: false,
49 blocked_lots: 0,
50 }
51 }
52
53 #[must_use]
55 pub fn builder(symbol: impl Into<String>, exchange: impl Into<String>) -> PositionBuilder {
56 PositionBuilder::new(symbol, exchange)
57 }
58
59 #[must_use]
61 pub fn symbol(&self) -> &str {
62 &self.symbol
63 }
64
65 #[must_use]
67 pub fn exchange(&self) -> &str {
68 &self.exchange
69 }
70
71 #[must_use]
73 pub fn instrument_id(&self) -> Option<&str> {
74 self.instrument_id.as_deref()
75 }
76
77 #[must_use]
79 pub const fn quantity(&self) -> Decimal {
80 self.quantity
81 }
82
83 #[must_use]
85 pub const fn lot_size(&self) -> u32 {
86 self.lot_size
87 }
88
89 #[must_use]
91 pub const fn current_price(&self) -> &Money {
92 &self.current_price
93 }
94
95 #[must_use]
97 pub const fn average_cost(&self) -> Option<&Money> {
98 self.average_cost.as_ref()
99 }
100
101 #[must_use]
103 pub const fn is_blocked(&self) -> bool {
104 self.blocked
105 }
106
107 #[must_use]
109 pub const fn blocked_lots(&self) -> u32 {
110 self.blocked_lots
111 }
112
113 #[must_use]
115 pub fn market_value(&self) -> Money {
116 self.current_price.multiply(self.quantity)
117 }
118
119 #[must_use]
121 pub fn lots_held(&self) -> u32 {
122 let lot_size = Decimal::from(self.lot_size);
123 (self.quantity / lot_size)
124 .floor()
125 .to_string()
126 .parse::<u32>()
127 .unwrap_or(0)
128 }
129
130 #[must_use]
132 pub fn tradeable_lots(&self) -> u32 {
133 self.lots_held().saturating_sub(self.blocked_lots)
134 }
135
136 #[must_use]
138 pub fn unrealized_pnl(&self) -> Option<Money> {
139 let avg_cost = self.average_cost.as_ref()?;
140 let cost_basis = avg_cost.multiply(self.quantity);
141 let market_value = self.market_value();
142 market_value.checked_sub(&cost_basis)
143 }
144
145 #[must_use]
147 pub fn unrealized_pnl_percent(&self) -> Option<Decimal> {
148 let avg_cost = self.average_cost.as_ref()?;
149 if avg_cost.is_zero() {
150 return None;
151 }
152 let pnl = self.unrealized_pnl()?;
153 let cost_basis = avg_cost.multiply(self.quantity);
154 Some((pnl.amount() / cost_basis.amount()) * Decimal::from(100))
155 }
156
157 pub fn update_price(&mut self, price: Money) {
159 self.current_price = price;
160 }
161
162 pub fn update_quantity(&mut self, quantity: Decimal) {
164 self.quantity = quantity;
165 }
166}
167
168pub struct PositionBuilder {
170 symbol: String,
171 exchange: String,
172 instrument_id: Option<String>,
173 quantity: Decimal,
174 lot_size: u32,
175 current_price: Option<Money>,
176 average_cost: Option<Money>,
177 blocked: bool,
178 blocked_lots: u32,
179}
180
181impl PositionBuilder {
182 fn new(symbol: impl Into<String>, exchange: impl Into<String>) -> Self {
183 Self {
184 symbol: symbol.into(),
185 exchange: exchange.into(),
186 instrument_id: None,
187 quantity: Decimal::ZERO,
188 lot_size: 1,
189 current_price: None,
190 average_cost: None,
191 blocked: false,
192 blocked_lots: 0,
193 }
194 }
195
196 #[must_use]
198 pub fn instrument_id(mut self, id: impl Into<String>) -> Self {
199 self.instrument_id = Some(id.into());
200 self
201 }
202
203 #[must_use]
205 pub const fn quantity(mut self, qty: Decimal) -> Self {
206 self.quantity = qty;
207 self
208 }
209
210 #[must_use]
212 pub const fn lot_size(mut self, size: u32) -> Self {
213 self.lot_size = size;
214 self
215 }
216
217 #[must_use]
219 pub fn current_price(mut self, price: Money) -> Self {
220 self.current_price = Some(price);
221 self
222 }
223
224 #[must_use]
226 pub fn average_cost(mut self, cost: Money) -> Self {
227 self.average_cost = Some(cost);
228 self
229 }
230
231 #[must_use]
233 pub const fn blocked(mut self, is_blocked: bool) -> Self {
234 self.blocked = is_blocked;
235 self
236 }
237
238 #[must_use]
240 pub const fn blocked_lots(mut self, lots: u32) -> Self {
241 self.blocked_lots = lots;
242 self
243 }
244
245 #[must_use]
251 pub fn build(self) -> Position {
252 Position {
253 symbol: self.symbol,
254 exchange: self.exchange,
255 instrument_id: self.instrument_id,
256 quantity: self.quantity,
257 lot_size: self.lot_size,
258 current_price: self.current_price.expect("current_price is required"),
259 average_cost: self.average_cost,
260 blocked: self.blocked,
261 blocked_lots: self.blocked_lots,
262 }
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use rust_decimal_macros::dec;
270
271 fn make_usd(amount: Decimal) -> Money {
272 Money::new(amount, "USD")
273 }
274
275 #[test]
276 fn test_position_creation() {
277 let pos = Position::new("AAPL", "NASDAQ", dec!(10), make_usd(dec!(150)));
278 assert_eq!(pos.symbol(), "AAPL");
279 assert_eq!(pos.exchange(), "NASDAQ");
280 assert_eq!(pos.quantity(), dec!(10));
281 }
282
283 #[test]
284 fn test_position_builder() {
285 let pos = Position::builder("SBER", "MOEX")
286 .instrument_id("BBG004730N88")
287 .quantity(dec!(100))
288 .lot_size(10)
289 .current_price(make_usd(dec!(250)))
290 .average_cost(make_usd(dec!(200)))
291 .build();
292
293 assert_eq!(pos.symbol(), "SBER");
294 assert_eq!(pos.instrument_id(), Some("BBG004730N88"));
295 assert_eq!(pos.lot_size(), 10);
296 assert_eq!(pos.lots_held(), 10); }
298
299 #[test]
300 fn test_market_value() {
301 let pos = Position::new("BTC", "BINANCE", dec!(2.5), make_usd(dec!(40000)));
302 let value = pos.market_value();
303 assert_eq!(value.amount(), dec!(100000));
304 }
305
306 #[test]
307 fn test_unrealized_pnl() {
308 let pos = Position::builder("AAPL", "NASDAQ")
309 .quantity(dec!(10))
310 .current_price(make_usd(dec!(150)))
311 .average_cost(make_usd(dec!(100)))
312 .build();
313
314 let pnl = pos.unrealized_pnl().unwrap();
315 assert_eq!(pnl.amount(), dec!(500)); let pnl_pct = pos.unrealized_pnl_percent().unwrap();
318 assert_eq!(pnl_pct, dec!(50)); }
320
321 #[test]
322 fn test_lots_calculation() {
323 let pos = Position::builder("SBER", "MOEX")
324 .quantity(dec!(105))
325 .lot_size(10)
326 .current_price(make_usd(dec!(250)))
327 .blocked_lots(3)
328 .build();
329
330 assert_eq!(pos.lots_held(), 10); assert_eq!(pos.tradeable_lots(), 7); }
333}