1pub mod kelly;
22
23pub use kelly::{fractional_kelly, full_kelly, KellyInput, KellyPortfolio, KellyResult};
24
25use crate::error::FinError;
26use crate::types::{NanoTimestamp, Price, Quantity, Side, Symbol};
27use rust_decimal::Decimal;
28use std::collections::HashMap;
29
30#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
32pub struct Fill {
33 pub symbol: Symbol,
35 pub side: Side,
37 pub quantity: Quantity,
39 pub price: Price,
41 pub timestamp: NanoTimestamp,
43 pub commission: Decimal,
45}
46
47impl Fill {
48 pub fn new(
50 symbol: Symbol,
51 side: Side,
52 quantity: Quantity,
53 price: Price,
54 timestamp: NanoTimestamp,
55 ) -> Self {
56 Self {
57 symbol,
58 side,
59 quantity,
60 price,
61 timestamp,
62 commission: Decimal::ZERO,
63 }
64 }
65
66 pub fn with_commission(
68 symbol: Symbol,
69 side: Side,
70 quantity: Quantity,
71 price: Price,
72 timestamp: NanoTimestamp,
73 commission: Decimal,
74 ) -> Self {
75 Self {
76 symbol,
77 side,
78 quantity,
79 price,
80 timestamp,
81 commission,
82 }
83 }
84
85 pub fn notional(&self) -> Decimal {
90 self.price.value() * self.quantity.value()
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
96pub enum PositionDirection {
97 Long,
99 Short,
101 Flat,
103}
104
105#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
107pub struct Position {
108 pub symbol: Symbol,
110 pub quantity: Decimal,
112 pub avg_cost: Decimal,
114 pub realized_pnl: Decimal,
116 #[serde(default)]
118 pub open_bar: usize,
119}
120
121impl Position {
122 pub fn new(symbol: Symbol) -> Self {
124 Self {
125 symbol,
126 quantity: Decimal::ZERO,
127 avg_cost: Decimal::ZERO,
128 realized_pnl: Decimal::ZERO,
129 open_bar: 0,
130 }
131 }
132
133 pub fn set_open_bar(&mut self, bar: usize) {
137 self.open_bar = bar;
138 }
139
140 pub fn position_age_bars(&self, current_bar: usize) -> usize {
144 current_bar.saturating_sub(self.open_bar)
145 }
146
147 pub fn max_favorable_excursion(&self, prices: &[Price]) -> Option<Decimal> {
154 if self.is_flat() || self.avg_cost.is_zero() || prices.is_empty() {
155 return None;
156 }
157 let best = if self.is_long() {
158 prices
159 .iter()
160 .map(|p| (p.value() - self.avg_cost) * self.quantity)
161 .fold(Decimal::MIN, Decimal::max)
162 } else {
163 prices
164 .iter()
165 .map(|p| (self.avg_cost - p.value()) * self.quantity.abs())
166 .fold(Decimal::MIN, Decimal::max)
167 };
168 if best < Decimal::ZERO {
169 Some(Decimal::ZERO)
170 } else {
171 Some(best)
172 }
173 }
174
175 pub fn kelly_fraction(
182 win_rate: Decimal,
183 avg_win: Decimal,
184 avg_loss: Decimal,
185 ) -> Option<Decimal> {
186 if avg_loss.is_zero() || avg_win.is_zero() {
187 return None;
188 }
189 let odds = avg_win / avg_loss;
190 let kelly = win_rate - (Decimal::ONE - win_rate) / odds;
191 Some(kelly.max(Decimal::ZERO).min(Decimal::ONE))
192 }
193
194 pub fn apply_fill(&mut self, fill: &Fill) -> Result<Decimal, FinError> {
202 let fill_qty = match fill.side {
203 Side::Bid => fill.quantity.value(),
204 Side::Ask => -fill.quantity.value(),
205 };
206
207 let realized = if self.quantity != Decimal::ZERO
208 && (self.quantity > Decimal::ZERO) != (fill_qty > Decimal::ZERO)
209 {
210 let closed = fill_qty.abs().min(self.quantity.abs());
211 if self.quantity > Decimal::ZERO {
212 closed * (fill.price.value() - self.avg_cost)
213 } else {
214 closed * (self.avg_cost - fill.price.value())
215 }
216 } else {
217 Decimal::ZERO
218 };
219
220 let new_qty = self.quantity + fill_qty;
221 if new_qty == Decimal::ZERO {
222 self.avg_cost = Decimal::ZERO;
223 } else if (self.quantity >= Decimal::ZERO && fill_qty > Decimal::ZERO)
224 || (self.quantity <= Decimal::ZERO && fill_qty < Decimal::ZERO)
225 {
226 let total_cost =
227 self.avg_cost * self.quantity.abs() + fill.price.value() * fill_qty.abs();
228 self.avg_cost = total_cost
229 .checked_div(new_qty.abs())
230 .ok_or(FinError::ArithmeticOverflow)?;
231 } else if new_qty.abs() <= self.quantity.abs() {
232 } else {
234 self.avg_cost = fill.price.value();
236 }
237
238 self.quantity = new_qty;
239 let net_realized = realized - fill.commission;
240 self.realized_pnl += net_realized;
241 Ok(net_realized)
242 }
243
244 pub fn unrealized_pnl(&self, current_price: Price) -> Decimal {
246 self.quantity * (current_price.value() - self.avg_cost)
247 }
248
249 pub fn checked_unrealized_pnl(&self, current_price: Price) -> Result<Decimal, FinError> {
251 let diff = current_price.value() - self.avg_cost;
252 self.quantity
253 .checked_mul(diff)
254 .ok_or(FinError::ArithmeticOverflow)
255 }
256
257 pub fn unrealized_pnl_pct(&self, current_price: Price) -> Option<Decimal> {
262 if self.is_flat() || self.avg_cost.is_zero() {
263 return None;
264 }
265 let cost_basis = self.quantity.abs() * self.avg_cost;
266 if cost_basis.is_zero() {
267 return None;
268 }
269 let upnl = self.unrealized_pnl(current_price);
270 upnl.checked_div(cost_basis).map(|r| r * Decimal::from(100u32))
271 }
272
273 pub fn total_cost_basis(&self) -> Decimal {
278 self.quantity.abs() * self.avg_cost
279 }
280
281 pub fn market_value(&self, current_price: Price) -> Decimal {
283 self.quantity * current_price.value()
284 }
285
286 pub fn is_flat(&self) -> bool {
288 self.quantity == Decimal::ZERO
289 }
290
291 pub fn is_long(&self) -> bool {
293 self.quantity > Decimal::ZERO
294 }
295
296 pub fn is_short(&self) -> bool {
298 self.quantity < Decimal::ZERO
299 }
300
301 pub fn direction(&self) -> PositionDirection {
303 if self.quantity > Decimal::ZERO {
304 PositionDirection::Long
305 } else if self.quantity < Decimal::ZERO {
306 PositionDirection::Short
307 } else {
308 PositionDirection::Flat
309 }
310 }
311
312 pub fn total_pnl(&self, current_price: Price) -> Decimal {
314 self.realized_pnl + self.unrealized_pnl(current_price)
315 }
316
317 pub fn quantity_abs(&self) -> Decimal {
319 self.quantity.abs()
320 }
321
322 pub fn cost_basis(&self) -> Decimal {
327 self.avg_cost * self.quantity.abs()
328 }
329
330
331 pub fn is_profitable(&self, current_price: Price) -> bool {
333 self.unrealized_pnl(current_price) > Decimal::ZERO
334 }
335
336 pub fn avg_entry_price(&self) -> Option<Price> {
341 Price::new(self.avg_cost).ok()
342 }
343
344 pub fn exposure_pct(&self, current_price: Price, total_portfolio_value: Decimal) -> Option<Decimal> {
351 if total_portfolio_value.is_zero() || self.is_flat() {
352 return None;
353 }
354 let market_value = (self.quantity * current_price.value()).abs();
355 Some(market_value / total_portfolio_value * Decimal::ONE_HUNDRED)
356 }
357
358 pub fn stop_loss_price(&self, stop_pct: Decimal) -> Option<Price> {
371 if self.is_flat() || self.avg_cost.is_zero() {
372 return None;
373 }
374 let factor = stop_pct / Decimal::ONE_HUNDRED;
375 let stop = if self.is_long() {
376 self.avg_cost * (Decimal::ONE - factor)
377 } else {
378 self.avg_cost * (Decimal::ONE + factor)
379 };
380 Price::new(stop).ok()
381 }
382
383 pub fn take_profit_price(&self, tp_pct: Decimal) -> Option<Price> {
389 if self.is_flat() || self.avg_cost.is_zero() {
390 return None;
391 }
392 let factor = tp_pct / Decimal::ONE_HUNDRED;
393 let tp = if self.is_long() {
394 self.avg_cost * (Decimal::ONE + factor)
395 } else {
396 self.avg_cost * (Decimal::ONE - factor)
397 };
398 Price::new(tp).ok()
399 }
400
401 pub fn margin_requirement(&self, margin_pct: Decimal) -> Option<Decimal> {
405 if self.is_flat() || self.avg_cost.is_zero() {
406 return None;
407 }
408 let notional = self.quantity.abs() * self.avg_cost;
409 Some(notional * margin_pct / Decimal::ONE_HUNDRED)
410 }
411
412 pub fn risk_reward_ratio(stop_pct: Decimal, target_pct: Decimal) -> Option<f64> {
417 use rust_decimal::prelude::ToPrimitive;
418 if stop_pct <= Decimal::ZERO {
419 return None;
420 }
421 (target_pct / stop_pct).to_f64()
422 }
423
424 pub fn leverage(&self, portfolio_value: Decimal) -> Option<Decimal> {
428 if self.is_flat() || self.avg_cost.is_zero() || portfolio_value.is_zero() {
429 return None;
430 }
431 let notional = self.quantity.abs() * self.avg_cost;
432 Some(notional / portfolio_value)
433 }
434}
435
436#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
438pub struct PositionLedger {
439 positions: HashMap<Symbol, Position>,
440 cash: Decimal,
441 total_commission_paid: Decimal,
442}
443
444impl PositionLedger {
445 pub fn new(initial_cash: Decimal) -> Self {
447 Self {
448 positions: HashMap::new(),
449 cash: initial_cash,
450 total_commission_paid: Decimal::ZERO,
451 }
452 }
453
454 #[allow(clippy::needless_pass_by_value)]
459 pub fn apply_fill(&mut self, fill: Fill) -> Result<(), FinError> {
460 let cost = match fill.side {
461 Side::Bid => -(fill.quantity.value() * fill.price.value() + fill.commission),
462 Side::Ask => fill.quantity.value() * fill.price.value() - fill.commission,
463 };
464 if fill.side == Side::Bid && self.cash + cost < Decimal::ZERO {
465 return Err(FinError::InsufficientFunds {
466 need: fill.quantity.value() * fill.price.value() + fill.commission,
467 have: self.cash,
468 });
469 }
470 self.cash += cost;
471 self.total_commission_paid += fill.commission;
472 let pos = self
473 .positions
474 .entry(fill.symbol.clone())
475 .or_insert_with(|| Position::new(fill.symbol.clone()));
476 pos.apply_fill(&fill)?;
477 Ok(())
478 }
479
480 pub fn position(&self, symbol: &Symbol) -> Option<&Position> {
482 self.positions.get(symbol)
483 }
484
485 pub fn has_position(&self, symbol: &Symbol) -> bool {
487 self.positions.contains_key(symbol)
488 }
489
490 pub fn positions(&self) -> impl Iterator<Item = &Position> {
492 self.positions.values()
493 }
494
495 pub fn open_positions(&self) -> impl Iterator<Item = &Position> {
497 self.positions.values().filter(|p| !p.is_flat())
498 }
499
500 pub fn flat_positions(&self) -> impl Iterator<Item = &Position> {
502 self.positions.values().filter(|p| p.is_flat())
503 }
504
505 pub fn long_positions(&self) -> impl Iterator<Item = &Position> {
507 self.positions.values().filter(|p| p.is_long())
508 }
509
510 pub fn short_positions(&self) -> impl Iterator<Item = &Position> {
512 self.positions.values().filter(|p| p.is_short())
513 }
514
515 pub fn symbols(&self) -> impl Iterator<Item = &Symbol> {
517 self.positions.keys()
518 }
519
520 pub fn open_symbols(&self) -> impl Iterator<Item = &Symbol> {
522 self.positions
523 .iter()
524 .filter(|(_, p)| !p.is_flat())
525 .map(|(s, _)| s)
526 }
527
528 pub fn total_long_exposure(&self) -> Decimal {
532 self.positions
533 .values()
534 .filter(|p| p.is_long())
535 .map(|p| p.quantity.abs() * p.avg_cost)
536 .sum()
537 }
538
539 pub fn total_short_exposure(&self) -> Decimal {
543 self.positions
544 .values()
545 .filter(|p| p.is_short())
546 .map(|p| p.quantity.abs() * p.avg_cost)
547 .sum()
548 }
549
550 pub fn symbols_sorted(&self) -> Vec<&Symbol> {
554 let mut syms: Vec<&Symbol> = self.positions.keys().collect();
555 syms.sort();
556 syms
557 }
558
559 pub fn position_count(&self) -> usize {
561 self.positions.len()
562 }
563
564 pub fn deposit(&mut self, amount: Decimal) {
569 self.cash += amount;
570 }
571
572 pub fn withdraw(&mut self, amount: Decimal) -> Result<(), FinError> {
577 if amount > self.cash {
578 return Err(FinError::InsufficientFunds {
579 need: amount,
580 have: self.cash,
581 });
582 }
583 self.cash -= amount;
584 Ok(())
585 }
586
587 pub fn open_position_count(&self) -> usize {
589 self.positions.values().filter(|p| !p.is_flat()).count()
590 }
591
592 pub fn long_count(&self) -> usize {
594 self.positions.values().filter(|p| p.quantity > Decimal::ZERO).count()
595 }
596
597 pub fn short_count(&self) -> usize {
599 self.positions.values().filter(|p| p.quantity < Decimal::ZERO).count()
600 }
601
602 pub fn net_exposure(&self) -> Decimal {
607 self.positions.values().map(|p| p.quantity).sum()
608 }
609
610 pub fn net_market_exposure(&self, prices: &std::collections::HashMap<String, Price>) -> Option<Decimal> {
616 let mut found = false;
617 let mut net = Decimal::ZERO;
618 for pos in self.positions.values() {
619 if pos.quantity.is_zero() { continue; }
620 if let Some(&price) = prices.get(pos.symbol.as_str()) {
621 found = true;
622 net += pos.quantity * price.value();
623 }
624 }
625 if found { Some(net) } else { None }
626 }
627
628 pub fn gross_exposure(&self) -> Decimal {
632 self.positions.values().map(|p| p.quantity.abs()).sum()
633 }
634
635 pub fn open_count(&self) -> usize {
640 self.positions.values().filter(|p| !p.is_flat()).count()
641 }
642
643 pub fn largest_position(&self) -> Option<&Position> {
647 self.positions
648 .values()
649 .filter(|p| !p.is_flat())
650 .max_by(|a, b| a.quantity.abs().partial_cmp(&b.quantity.abs()).unwrap_or(std::cmp::Ordering::Equal))
651 }
652
653 pub fn total_market_value(
658 &self,
659 prices: &HashMap<String, Price>,
660 ) -> Result<Decimal, FinError> {
661 let mut total = Decimal::ZERO;
662 for (sym, pos) in &self.positions {
663 if pos.quantity == Decimal::ZERO {
664 continue;
665 }
666 let price = prices
667 .get(sym.as_str())
668 .ok_or_else(|| FinError::PositionNotFound(sym.as_str().to_owned()))?;
669 total += pos.market_value(*price);
670 }
671 Ok(total)
672 }
673
674 pub fn cash(&self) -> Decimal {
676 self.cash
677 }
678
679 pub fn position_weights(&self, prices: &HashMap<String, Price>) -> Vec<(Symbol, Decimal)> {
685 let mut mv_pairs: Vec<(Symbol, Decimal)> = self
686 .positions
687 .iter()
688 .filter(|(_, p)| !p.is_flat())
689 .filter_map(|(sym, pos)| {
690 let price = prices.get(sym.as_str())?;
691 Some((sym.clone(), pos.market_value(*price).abs()))
692 })
693 .collect();
694 let total: Decimal = mv_pairs.iter().map(|(_, v)| *v).sum();
695 if total.is_zero() {
696 return vec![];
697 }
698 mv_pairs.iter_mut().for_each(|(_, v)| *v /= total);
699 mv_pairs
700 }
701
702 pub fn realized_pnl_total(&self) -> Decimal {
704 self.positions.values().map(|p| p.realized_pnl).sum()
705 }
706
707 pub fn unrealized_pnl_total(
712 &self,
713 prices: &HashMap<String, Price>,
714 ) -> Result<Decimal, FinError> {
715 let mut total = Decimal::ZERO;
716 for (sym, pos) in &self.positions {
717 if pos.quantity == Decimal::ZERO {
718 continue;
719 }
720 let price = prices
721 .get(sym.as_str())
722 .ok_or_else(|| FinError::PositionNotFound(sym.as_str().to_owned()))?;
723 total += pos.unrealized_pnl(*price);
724 }
725 Ok(total)
726 }
727
728 pub fn realized_pnl(&self, symbol: &Symbol) -> Option<Decimal> {
730 self.positions.get(symbol).map(|p| p.realized_pnl)
731 }
732
733 pub fn net_pnl(&self, prices: &HashMap<String, Price>) -> Result<Decimal, FinError> {
738 Ok(self.realized_pnl_total() + self.unrealized_pnl_total(prices)?)
739 }
740
741 pub fn equity(&self, prices: &HashMap<String, Price>) -> Result<Decimal, FinError> {
751 Ok(self.cash + self.unrealized_pnl_total(prices)?)
752 }
753
754 pub fn net_liquidation_value(&self, prices: &HashMap<String, Price>) -> Result<Decimal, FinError> {
762 let mut total = self.cash;
763 for (symbol, pos) in &self.positions {
764 if pos.quantity == Decimal::ZERO {
765 continue;
766 }
767 let price = prices
768 .get(symbol.as_str())
769 .ok_or_else(|| FinError::PositionNotFound(symbol.to_string()))?;
770 total += pos.quantity * price.value();
771 }
772 Ok(total)
773 }
774
775 pub fn pnl_by_symbol(&self, prices: &HashMap<String, Price>) -> Result<HashMap<Symbol, Decimal>, FinError> {
782 let mut map = HashMap::new();
783 for (symbol, pos) in &self.positions {
784 if pos.quantity == Decimal::ZERO {
785 continue;
786 }
787 let price = prices
788 .get(symbol.as_str())
789 .ok_or_else(|| FinError::PositionNotFound(symbol.to_string()))?;
790 map.insert(symbol.clone(), pos.unrealized_pnl(*price));
791 }
792 Ok(map)
793 }
794
795 pub fn delta_neutral_check(&self, prices: &HashMap<String, Price>) -> Result<bool, FinError> {
803 let mut net = Decimal::ZERO;
804 let mut gross = Decimal::ZERO;
805 for (symbol, pos) in &self.positions {
806 if pos.quantity == Decimal::ZERO {
807 continue;
808 }
809 let price = prices
810 .get(symbol.as_str())
811 .ok_or_else(|| FinError::PositionNotFound(symbol.to_string()))?;
812 let exposure = pos.quantity * price.value();
813 net += exposure;
814 gross += exposure.abs();
815 }
816 if gross == Decimal::ZERO {
817 return Ok(true);
818 }
819 Ok((net / gross).abs() < Decimal::new(1, 2)) }
821
822 pub fn allocation_pct(
831 &self,
832 symbol: &Symbol,
833 prices: &HashMap<String, Price>,
834 ) -> Result<Option<Decimal>, crate::error::FinError> {
835 let pos = self
836 .positions
837 .get(symbol)
838 .ok_or_else(|| crate::error::FinError::PositionNotFound(symbol.to_string()))?;
839 if pos.quantity == Decimal::ZERO {
840 return Ok(None);
841 }
842 let price = match prices.get(symbol.as_str()) {
843 Some(p) => *p,
844 None => return Ok(None),
845 };
846 let notional = (pos.quantity * price.value()).abs();
847 let total = self.total_market_value(prices)?;
848 if total.is_zero() {
849 return Ok(None);
850 }
851 Ok(Some(notional / total * Decimal::ONE_HUNDRED))
852 }
853
854 pub fn positions_sorted_by_pnl(&self, prices: &HashMap<String, Price>) -> Vec<&Position> {
858 let mut open: Vec<&Position> = self
859 .positions
860 .values()
861 .filter(|p| p.quantity != Decimal::ZERO)
862 .collect();
863 open.sort_by(|a, b| {
864 let pnl_a = prices
865 .get(a.symbol.as_str())
866 .map_or(Decimal::ZERO, |&p| a.unrealized_pnl(p));
867 let pnl_b = prices
868 .get(b.symbol.as_str())
869 .map_or(Decimal::ZERO, |&p| b.unrealized_pnl(p));
870 pnl_b.cmp(&pnl_a)
871 });
872 open
873 }
874
875 pub fn top_n_positions<'a>(&'a self, n: usize, prices: &HashMap<String, Price>) -> Vec<&'a Position> {
879 let mut open: Vec<&Position> = self.positions.values().filter(|p| !p.is_flat()).collect();
880 open.sort_by(|a, b| {
881 let mv_a = prices.get(a.symbol.as_str())
882 .map_or(Decimal::ZERO, |p| (a.quantity * p.value()).abs());
883 let mv_b = prices.get(b.symbol.as_str())
884 .map_or(Decimal::ZERO, |p| (b.quantity * p.value()).abs());
885 mv_b.cmp(&mv_a)
886 });
887 open.into_iter().take(n).collect()
888 }
889
890 pub fn concentration(&self, prices: &HashMap<String, Price>) -> Result<Option<Decimal>, FinError> {
900 let gross = self.gross_exposure();
901 if gross == Decimal::ZERO {
902 return Ok(None);
903 }
904 let mut hhi = Decimal::ZERO;
905 for (symbol, pos) in &self.positions {
906 if pos.quantity == Decimal::ZERO {
907 continue;
908 }
909 let price = prices
910 .get(symbol.as_str())
911 .ok_or_else(|| FinError::PositionNotFound(symbol.to_string()))?;
912 let mv = (pos.quantity * price.value()).abs();
913 let w = mv / gross;
914 hhi += w * w;
915 }
916 Ok(Some(hhi))
917 }
918
919 pub fn margin_used(&self, prices: &HashMap<String, Price>, margin_rate: Decimal) -> Result<Decimal, FinError> {
924 let mut gross = Decimal::ZERO;
925 for (symbol, pos) in &self.positions {
926 if pos.quantity == Decimal::ZERO {
927 continue;
928 }
929 let price = prices
930 .get(symbol.as_str())
931 .ok_or_else(|| FinError::PositionNotFound(symbol.to_string()))?;
932 gross += (pos.quantity * price.value()).abs();
933 }
934 Ok(gross * margin_rate)
935 }
936
937 pub fn flat_count(&self) -> usize {
939 self.positions.values().filter(|p| p.is_flat()).count()
940 }
941
942 pub fn smallest_position(&self) -> Option<&Position> {
946 self.positions
947 .values()
948 .filter(|p| !p.is_flat())
949 .min_by(|a, b| a.quantity.abs().partial_cmp(&b.quantity.abs()).unwrap_or(std::cmp::Ordering::Equal))
950 }
951
952 pub fn most_profitable_symbol(
956 &self,
957 prices: &HashMap<String, Price>,
958 ) -> Option<&Symbol> {
959 self.positions
960 .iter()
961 .filter(|(_, p)| !p.is_flat())
962 .filter_map(|(sym, p)| {
963 let price = prices.get(sym.as_str())?;
964 let pnl = p.unrealized_pnl(*price);
965 Some((sym, pnl))
966 })
967 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
968 .map(|(sym, _)| sym)
969 }
970
971 pub fn least_profitable_symbol(
975 &self,
976 prices: &HashMap<String, Price>,
977 ) -> Option<&Symbol> {
978 self.positions
979 .iter()
980 .filter(|(_, p)| !p.is_flat())
981 .filter_map(|(sym, p)| {
982 let price = prices.get(sym.as_str())?;
983 let pnl = p.unrealized_pnl(*price);
984 Some((sym, pnl))
985 })
986 .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
987 .map(|(sym, _)| sym)
988 }
989
990 pub fn total_commission_paid(&self) -> Decimal {
992 self.total_commission_paid
993 }
994
995 pub fn symbols_with_pnl(
999 &self,
1000 prices: &HashMap<String, Price>,
1001 ) -> Vec<(&Symbol, Decimal)> {
1002 let mut result: Vec<(&Symbol, Decimal)> = self
1003 .positions
1004 .iter()
1005 .filter(|(_, p)| !p.is_flat())
1006 .filter_map(|(sym, p)| {
1007 let price = prices.get(sym.as_str())?;
1008 Some((sym, p.unrealized_pnl(*price)))
1009 })
1010 .collect();
1011 result.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
1012 result
1013 }
1014
1015 pub fn concentration_pct(
1020 &self,
1021 symbol: &Symbol,
1022 prices: &HashMap<String, Price>,
1023 ) -> Option<Decimal> {
1024 let pos = self.positions.get(symbol)?;
1025 let price = prices.get(symbol.as_str())?;
1026 let mv = pos.quantity.abs() * price.value();
1027 let total = self
1028 .positions
1029 .values()
1030 .filter_map(|p| {
1031 let pr = prices.get(p.symbol.as_str())?;
1032 Some(p.quantity.abs() * pr.value())
1033 })
1034 .sum::<Decimal>();
1035 if total.is_zero() {
1036 return None;
1037 }
1038 Some(mv / total * Decimal::ONE_HUNDRED)
1039 }
1040
1041 pub fn all_flat(&self) -> bool {
1043 self.positions.values().all(|p| p.is_flat())
1044 }
1045
1046 pub fn long_exposure(&self, prices: &HashMap<String, Price>) -> Decimal {
1050 self.positions
1051 .iter()
1052 .filter(|(_, p)| p.is_long())
1053 .filter_map(|(sym, p)| {
1054 let price = prices.get(sym.as_str())?;
1055 Some(p.quantity.abs() * price.value())
1056 })
1057 .sum()
1058 }
1059
1060 pub fn short_exposure(&self, prices: &HashMap<String, Price>) -> Decimal {
1064 self.positions
1065 .iter()
1066 .filter(|(_, p)| p.is_short())
1067 .filter_map(|(sym, p)| {
1068 let price = prices.get(sym.as_str())?;
1069 Some(p.quantity.abs() * price.value())
1070 })
1071 .sum()
1072 }
1073
1074 pub fn net_delta(&self, prices: &HashMap<String, Price>) -> Decimal {
1078 self.long_exposure(prices) - self.short_exposure(prices)
1079 }
1080
1081 pub fn avg_cost_basis(&self, symbol: &Symbol) -> Option<Decimal> {
1083 let pos = self.positions.get(symbol)?;
1084 if pos.is_flat() { return None; }
1085 Some(pos.avg_cost)
1086 }
1087
1088 pub fn active_symbols(&self) -> Vec<&Symbol> {
1090 self.positions
1091 .iter()
1092 .filter(|(_, pos)| !pos.is_flat())
1093 .map(|(sym, _)| sym)
1094 .collect()
1095 }
1096
1097 pub fn symbol_count(&self) -> usize {
1099 self.positions.len()
1100 }
1101
1102 pub fn realized_pnl_by_symbol(&self) -> Vec<(Symbol, Decimal)> {
1107 let mut pairs: Vec<(Symbol, Decimal)> = self
1108 .positions
1109 .iter()
1110 .filter_map(|(sym, pos)| {
1111 let r = pos.realized_pnl;
1112 if r != Decimal::ZERO { Some((sym.clone(), r)) } else { None }
1113 })
1114 .collect();
1115 pairs.sort_by(|a, b| b.1.cmp(&a.1));
1116 pairs
1117 }
1118
1119 pub fn top_losers<'a>(
1124 &'a self,
1125 n: usize,
1126 prices: &HashMap<String, Price>,
1127 ) -> Vec<&'a Position> {
1128 if n == 0 {
1129 return vec![];
1130 }
1131 let mut open: Vec<&Position> =
1132 self.positions.values().filter(|p| !p.is_flat()).collect();
1133 open.sort_by(|a, b| {
1134 let pnl_a = prices
1135 .get(a.symbol.as_str())
1136 .map_or(Decimal::ZERO, |&p| a.unrealized_pnl(p));
1137 let pnl_b = prices
1138 .get(b.symbol.as_str())
1139 .map_or(Decimal::ZERO, |&p| b.unrealized_pnl(p));
1140 pnl_a.cmp(&pnl_b) });
1142 open.into_iter().take(n).collect()
1143 }
1144
1145 pub fn flat_symbols(&self) -> Vec<&Symbol> {
1148 let mut syms: Vec<&Symbol> = self.positions
1149 .iter()
1150 .filter_map(|(sym, pos)| if pos.is_flat() { Some(sym) } else { None })
1151 .collect();
1152 syms.sort();
1153 syms
1154 }
1155
1156 pub fn max_unrealized_loss(&self, prices: &HashMap<String, Price>) -> Option<Decimal> {
1160 self.positions
1161 .values()
1162 .filter(|p| !p.is_flat())
1163 .filter_map(|p| {
1164 let price = prices.get(p.symbol.as_str()).copied()?;
1165 let upnl = p.unrealized_pnl(price);
1166 if upnl < Decimal::ZERO { Some(upnl) } else { None }
1167 })
1168 .min_by(|a, b| a.cmp(b))
1169 }
1170
1171 pub fn largest_winner<'a>(&'a self, prices: &HashMap<String, Price>) -> Option<&'a Position> {
1175 self.positions
1176 .values()
1177 .filter(|p| !p.is_flat())
1178 .filter_map(|p| {
1179 let price = prices.get(p.symbol.as_str()).copied()?;
1180 let upnl = p.unrealized_pnl(price);
1181 if upnl > Decimal::ZERO { Some((p, upnl)) } else { None }
1182 })
1183 .max_by(|a, b| a.1.cmp(&b.1))
1184 .map(|(p, _)| p)
1185 }
1186
1187 pub fn largest_loser<'a>(&'a self, prices: &HashMap<String, Price>) -> Option<&'a Position> {
1191 self.positions
1192 .values()
1193 .filter(|p| !p.is_flat())
1194 .filter_map(|p| {
1195 let price = prices.get(p.symbol.as_str()).copied()?;
1196 let upnl = p.unrealized_pnl(price);
1197 if upnl < Decimal::ZERO { Some((p, upnl)) } else { None }
1198 })
1199 .min_by(|a, b| a.1.cmp(&b.1))
1200 .map(|(p, _)| p)
1201 }
1202
1203 pub fn gross_market_exposure(&self, prices: &HashMap<String, Price>) -> Decimal {
1205 self.positions
1206 .values()
1207 .filter(|p| !p.is_flat())
1208 .filter_map(|p| {
1209 let price = prices.get(p.symbol.as_str()).copied()?;
1210 Some(p.market_value(price).abs())
1211 })
1212 .sum()
1213 }
1214
1215 pub fn largest_position_pct(&self, prices: &HashMap<String, Price>) -> Option<Decimal> {
1219 let total = self.gross_market_exposure(prices);
1220 if total.is_zero() { return None; }
1221 let max_mv = self.positions
1222 .values()
1223 .filter(|p| !p.is_flat())
1224 .filter_map(|p| {
1225 let price = prices.get(p.symbol.as_str()).copied()?;
1226 Some(p.market_value(price).abs())
1227 })
1228 .max_by(|a, b| a.cmp(b))?;
1229 Some(max_mv / total * Decimal::from(100u32))
1230 }
1231
1232 pub fn unrealized_pnl_pct(&self, prices: &HashMap<String, Price>) -> Option<Decimal> {
1238 let total_upnl = self.unrealized_pnl_total(prices).ok()?;
1239 let total_cost: Decimal = self.positions
1240 .values()
1241 .filter(|p| !p.is_flat())
1242 .map(|p| p.cost_basis().abs())
1243 .sum();
1244 if total_cost.is_zero() { return None; }
1245 Some(total_upnl / total_cost * Decimal::from(100u32))
1246 }
1247
1248 pub fn symbols_up<'a>(&'a self, prices: &HashMap<String, Price>) -> Vec<&'a Symbol> {
1252 self.positions
1253 .values()
1254 .filter(|p| !p.is_flat())
1255 .filter(|p| {
1256 prices.get(p.symbol.as_str())
1257 .map_or(false, |&price| p.unrealized_pnl(price) > Decimal::ZERO)
1258 })
1259 .map(|p| &p.symbol)
1260 .collect()
1261 }
1262
1263 pub fn symbols_down<'a>(&'a self, prices: &HashMap<String, Price>) -> Vec<&'a Symbol> {
1267 self.positions
1268 .values()
1269 .filter(|p| !p.is_flat())
1270 .filter(|p| {
1271 prices.get(p.symbol.as_str())
1272 .map_or(false, |&price| p.unrealized_pnl(price) < Decimal::ZERO)
1273 })
1274 .map(|p| &p.symbol)
1275 .collect()
1276 }
1277
1278 pub fn largest_unrealized_gain<'a>(&'a self, prices: &HashMap<String, Price>) -> Option<&'a Position> {
1283 self.largest_winner(prices)
1284 }
1285
1286 pub fn avg_realized_pnl_per_symbol(&self) -> Option<Decimal> {
1290 if self.positions.is_empty() { return None; }
1291 let total: Decimal = self.positions.values().map(|p| p.realized_pnl).sum();
1292 #[allow(clippy::cast_possible_truncation)]
1293 Some(total / Decimal::from(self.positions.len() as u32))
1294 }
1295
1296 pub fn win_rate(&self) -> Option<Decimal> {
1303 if self.positions.is_empty() { return None; }
1304 let total = self.positions.len();
1305 let winners = self.positions.values()
1306 .filter(|p| p.realized_pnl > Decimal::ZERO)
1307 .count();
1308 #[allow(clippy::cast_possible_truncation)]
1309 Some(Decimal::from(winners as u32) / Decimal::from(total as u32) * Decimal::from(100u32))
1310 }
1311
1312 pub fn net_pnl_excluding(
1317 &self,
1318 exclude: &Symbol,
1319 prices: &HashMap<String, Price>,
1320 ) -> Result<Decimal, FinError> {
1321 let total = self.net_pnl(prices)?;
1322 let excluded_rpnl = self.realized_pnl(exclude).unwrap_or(Decimal::ZERO);
1323 let excluded_upnl = if let Some(pos) = self.positions.get(exclude) {
1324 if !pos.is_flat() {
1325 let price = prices.get(exclude.as_str())
1326 .copied()
1327 .ok_or_else(|| FinError::InvalidSymbol(exclude.as_str().to_string()))?;
1328 pos.unrealized_pnl(price)
1329 } else {
1330 Decimal::ZERO
1331 }
1332 } else {
1333 Decimal::ZERO
1334 };
1335 Ok(total - excluded_rpnl - excluded_upnl)
1336 }
1337
1338 pub fn long_short_ratio(&self, prices: &HashMap<String, Price>) -> Option<Decimal> {
1344 let long_exp = self.long_exposure(prices);
1345 let short_exp = self.short_exposure(prices).abs();
1346 if short_exp.is_zero() { return None; }
1347 long_exp.checked_div(short_exp)
1348 }
1349
1350 pub fn position_count_by_direction(&self) -> (usize, usize) {
1352 let longs = self.positions.values()
1353 .filter(|p| !p.is_flat() && p.quantity > Decimal::ZERO)
1354 .count();
1355 let shorts = self.positions.values()
1356 .filter(|p| !p.is_flat() && p.quantity < Decimal::ZERO)
1357 .count();
1358 (longs, shorts)
1359 }
1360
1361 pub fn max_position_age_bars(&self, current_bar: usize) -> Option<usize> {
1365 self.positions.values()
1366 .filter(|p| !p.is_flat())
1367 .map(|p| p.position_age_bars(current_bar))
1368 .max()
1369 }
1370
1371 pub fn avg_position_age_bars(&self, current_bar: usize) -> Option<Decimal> {
1375 let ages: Vec<usize> = self.positions.values()
1376 .filter(|p| !p.is_flat())
1377 .map(|p| p.position_age_bars(current_bar))
1378 .collect();
1379 if ages.is_empty() { return None; }
1380 let sum: usize = ages.iter().sum();
1381 Some(Decimal::from(sum as u64) / Decimal::from(ages.len() as u64))
1382 }
1383
1384 pub fn hhi_concentration(&self, prices: &HashMap<String, Price>) -> Option<Decimal> {
1391 let open_positions: Vec<_> = self.positions.values()
1392 .filter(|p| !p.is_flat())
1393 .collect();
1394 if open_positions.is_empty() { return None; }
1395 let mvs: Vec<Decimal> = open_positions.iter()
1396 .filter_map(|p| {
1397 prices.get(p.symbol.as_str())
1398 .map(|&price| p.market_value(price).abs())
1399 })
1400 .collect();
1401 let total: Decimal = mvs.iter().sum();
1402 if total.is_zero() { return None; }
1403 Some(mvs.iter().map(|mv| {
1404 let w = mv / total;
1405 w * w
1406 }).sum())
1407 }
1408
1409 pub fn long_short_pnl_ratio(&self, prices: &HashMap<String, Price>) -> Option<Decimal> {
1414 let long_pnl: Decimal = self.positions.values()
1415 .filter(|p| p.is_long())
1416 .filter_map(|p| prices.get(p.symbol.as_str()).map(|&pr| p.unrealized_pnl(pr)))
1417 .sum();
1418 let short_pnl: Decimal = self.positions.values()
1419 .filter(|p| p.is_short())
1420 .filter_map(|p| prices.get(p.symbol.as_str()).map(|&pr| p.unrealized_pnl(pr)))
1421 .sum();
1422 let short_abs = short_pnl.abs();
1423 if short_abs.is_zero() { return None; }
1424 Some(long_pnl / short_abs)
1425 }
1426
1427 pub fn unrealized_pnl_by_symbol(&self, prices: &HashMap<String, Price>) -> HashMap<String, Decimal> {
1431 self.positions
1432 .iter()
1433 .filter(|(_, p)| !p.is_flat())
1434 .filter_map(|(sym, p)| {
1435 prices.get(sym.as_str())
1436 .map(|&price| (sym.as_str().to_owned(), p.unrealized_pnl(price)))
1437 })
1438 .collect()
1439 }
1440
1441 pub fn portfolio_beta(
1447 &self,
1448 prices: &HashMap<String, Price>,
1449 betas: &HashMap<String, f64>,
1450 ) -> Option<f64> {
1451 use rust_decimal::prelude::ToPrimitive;
1452 let open: Vec<&Position> = self.positions.values().filter(|p| !p.is_flat()).collect();
1453 if open.is_empty() { return None; }
1454 let total_mv: Decimal = open.iter()
1455 .filter_map(|p| prices.get(p.symbol.as_str()).map(|&pr| p.market_value(pr).abs()))
1456 .sum();
1457 if total_mv.is_zero() { return None; }
1458 let total_mv_f64 = total_mv.to_f64()?;
1459 let beta_sum: f64 = open.iter().filter_map(|p| {
1460 let mv = prices.get(p.symbol.as_str()).map(|&pr| p.market_value(pr).abs())?;
1461 let b = betas.get(p.symbol.as_str())?;
1462 let w = mv.to_f64()? / total_mv_f64;
1463 Some(w * b)
1464 }).sum();
1465 Some(beta_sum)
1466 }
1467
1468 pub fn total_notional(&self, prices: &HashMap<String, Price>) -> Option<Decimal> {
1473 let total: Decimal = self.positions.values()
1474 .filter(|p| !p.is_flat())
1475 .filter_map(|p| {
1476 prices.get(p.symbol.as_str())
1477 .map(|&price| p.quantity_abs() * price.value())
1478 })
1479 .sum();
1480 if total.is_zero() { None } else { Some(total) }
1481 }
1482
1483 pub fn max_unrealized_pnl(&self, prices: &HashMap<String, Price>) -> Option<Decimal> {
1488 self.positions.values()
1489 .filter(|p| !p.is_flat())
1490 .filter_map(|p| {
1491 prices.get(p.symbol.as_str())
1492 .map(|&price| p.unrealized_pnl(price))
1493 })
1494 .filter(|&pnl| pnl > Decimal::ZERO)
1495 .max()
1496 }
1497
1498 pub fn realized_pnl_rank(&self, symbol: &Symbol) -> Option<usize> {
1503 let target = self.positions.get(symbol).map(|p| p.realized_pnl)?;
1504 if target == Decimal::ZERO { return None; }
1505 let mut sorted: Vec<Decimal> = self.positions.values()
1506 .map(|p| p.realized_pnl)
1507 .filter(|&r| r != Decimal::ZERO)
1508 .collect();
1509 sorted.sort_by(|a, b| b.cmp(a));
1510 sorted.iter().position(|&r| r == target).map(|i| i + 1)
1511 }
1512
1513 pub fn open_positions_vec(&self) -> Vec<&Position> {
1515 let mut open: Vec<&Position> = self.positions.values()
1516 .filter(|p| !p.is_flat())
1517 .collect();
1518 open.sort_by(|a, b| a.symbol.as_str().cmp(b.symbol.as_str()));
1519 open
1520 }
1521
1522 pub fn symbols_with_pnl_above(&self, threshold: Decimal) -> Vec<Symbol> {
1526 let mut pairs: Vec<(Symbol, Decimal)> = self.positions.iter()
1527 .filter_map(|(sym, pos)| {
1528 if pos.realized_pnl > threshold { Some((sym.clone(), pos.realized_pnl)) } else { None }
1529 })
1530 .collect();
1531 pairs.sort_by(|a, b| b.1.cmp(&a.1));
1532 pairs.into_iter().map(|(s, _)| s).collect()
1533 }
1534
1535 pub fn net_long_short_count(&self) -> (usize, usize) {
1537 let long = self.positions.values().filter(|p| p.is_long()).count();
1538 let short = self.positions.values().filter(|p| p.is_short()).count();
1539 (long, short)
1540 }
1541
1542 pub fn largest_open_position(&self) -> Option<&Symbol> {
1546 self.positions.iter()
1547 .filter(|(_, p)| !p.is_flat())
1548 .max_by(|(_, a), (_, b)| a.quantity.abs().cmp(&b.quantity.abs()))
1549 .map(|(sym, _)| sym)
1550 }
1551
1552 pub fn exposure_by_direction(&self, prices: &HashMap<String, Price>) -> (Decimal, Decimal) {
1556 let long: Decimal = self.positions.values()
1557 .filter(|p| p.is_long())
1558 .filter_map(|p| prices.get(p.symbol.as_str()).map(|&pr| p.market_value(pr)))
1559 .sum();
1560 let short: Decimal = self.positions.values()
1561 .filter(|p| p.is_short())
1562 .filter_map(|p| prices.get(p.symbol.as_str()).map(|&pr| p.market_value(pr).abs()))
1563 .sum();
1564 (long, short)
1565 }
1566
1567 pub fn total_realized_pnl(&self) -> Decimal {
1569 self.positions.values().map(|p| p.realized_pnl).sum()
1570 }
1571
1572 pub fn count_with_pnl_below(&self, threshold: Decimal) -> usize {
1574 self.positions.values().filter(|p| p.realized_pnl < threshold).count()
1575 }
1576
1577 pub fn is_net_long(&self) -> bool {
1579 let net: Decimal = self.positions.values().map(|p| p.quantity).sum();
1580 net > Decimal::ZERO
1581 }
1582
1583 pub fn total_unrealized_pnl(&self, prices: &HashMap<String, Price>) -> Decimal {
1587 self.positions.values()
1588 .filter(|p| !p.is_flat())
1589 .filter_map(|p| prices.get(p.symbol.as_str()).map(|&pr| p.unrealized_pnl(pr)))
1590 .sum()
1591 }
1592
1593 pub fn symbols_flat(&self) -> Vec<&Symbol> {
1595 let mut flat: Vec<&Symbol> = self.positions.iter()
1596 .filter(|(_, p)| p.is_flat())
1597 .map(|(sym, _)| sym)
1598 .collect();
1599 flat.sort_by(|a, b| a.as_str().cmp(b.as_str()));
1600 flat
1601 }
1602
1603 pub fn avg_unrealized_pnl_pct(&self, prices: &HashMap<String, Price>) -> Option<Decimal> {
1608 let pcts: Vec<Decimal> = self.positions.values()
1609 .filter(|p| !p.is_flat())
1610 .filter_map(|p| {
1611 prices.get(p.symbol.as_str()).and_then(|&pr| {
1612 let cost_basis = (p.avg_cost * p.quantity).abs();
1613 if cost_basis.is_zero() { return None; }
1614 Some(p.unrealized_pnl(pr) / cost_basis * Decimal::ONE_HUNDRED)
1615 })
1616 })
1617 .collect();
1618 if pcts.is_empty() { return None; }
1619 Some(pcts.iter().sum::<Decimal>() / Decimal::from(pcts.len()))
1620 }
1621
1622 pub fn max_drawdown_symbol<'a>(&'a self, prices: &HashMap<String, Price>) -> Option<&'a Symbol> {
1626 self.positions.iter()
1627 .filter(|(_, p)| !p.is_flat())
1628 .filter_map(|(sym, p)| {
1629 prices.get(p.symbol.as_str())
1630 .map(|&price| (sym, p.unrealized_pnl(price)))
1631 })
1632 .min_by(|(_, a), (_, b)| a.cmp(b))
1633 .map(|(sym, _)| sym)
1634 }
1635
1636 pub fn avg_unrealized_pnl(&self, prices: &HashMap<String, Price>) -> Option<Decimal> {
1640 let pnls: Vec<Decimal> = self.positions.values()
1641 .filter(|p| !p.is_flat())
1642 .filter_map(|p| prices.get(p.symbol.as_str()).map(|&pr| p.unrealized_pnl(pr)))
1643 .collect();
1644 if pnls.is_empty() { return None; }
1645 #[allow(clippy::cast_possible_truncation)]
1646 Some(pnls.iter().sum::<Decimal>() / Decimal::from(pnls.len() as u32))
1647 }
1648
1649 pub fn position_symbols(&self) -> Vec<&Symbol> {
1651 let mut syms: Vec<&Symbol> = self.positions.keys().collect();
1652 syms.sort_by(|a, b| a.as_str().cmp(b.as_str()));
1653 syms
1654 }
1655
1656 pub fn count_profitable(&self) -> usize {
1658 self.positions.values().filter(|p| p.realized_pnl > Decimal::ZERO).count()
1659 }
1660
1661 pub fn count_losing(&self) -> usize {
1663 self.positions.values().filter(|p| p.realized_pnl < Decimal::ZERO).count()
1664 }
1665
1666 pub fn top_n_by_exposure<'a>(
1669 &'a self,
1670 prices: &HashMap<String, Price>,
1671 n: usize,
1672 ) -> Vec<(&'a Symbol, Decimal)> {
1673 let mut exposures: Vec<(&Symbol, Decimal)> = self.positions.iter()
1674 .filter(|(_, p)| !p.is_flat())
1675 .filter_map(|(sym, p)| {
1676 prices.get(p.symbol.as_str())
1677 .map(|&pr| (sym, (p.quantity * pr.value()).abs()))
1678 })
1679 .collect();
1680 exposures.sort_by(|a, b| b.1.cmp(&a.1));
1681 exposures.truncate(n);
1682 exposures
1683 }
1684
1685 pub fn has_open_positions(&self) -> bool {
1687 self.positions.values().any(|p| !p.is_flat())
1688 }
1689
1690 pub fn long_symbols(&self) -> Vec<&Symbol> {
1692 self.positions.iter()
1693 .filter(|(_, p)| p.quantity > Decimal::ZERO)
1694 .map(|(sym, _)| sym)
1695 .collect()
1696 }
1697
1698 pub fn short_symbols(&self) -> Vec<&Symbol> {
1700 self.positions.iter()
1701 .filter(|(_, p)| p.quantity < Decimal::ZERO)
1702 .map(|(sym, _)| sym)
1703 .collect()
1704 }
1705
1706 pub fn concentration_ratio(&self, prices: &HashMap<String, Price>) -> Option<f64> {
1711 use rust_decimal::prelude::ToPrimitive;
1712 let notionals: Vec<Decimal> = self.positions.values()
1713 .filter(|p| !p.is_flat())
1714 .filter_map(|p| {
1715 prices.get(p.symbol.as_str())
1716 .map(|&pr| (p.quantity * pr.value()).abs())
1717 })
1718 .collect();
1719 if notionals.is_empty() { return None; }
1720 let total: Decimal = notionals.iter().sum();
1721 if total.is_zero() { return None; }
1722 let hhi: f64 = notionals.iter()
1723 .filter_map(|n| (n / total).to_f64())
1724 .map(|w| w * w)
1725 .sum();
1726 Some(hhi)
1727 }
1728
1729 pub fn min_unrealized_pnl(&self, prices: &HashMap<String, Price>) -> Option<Decimal> {
1733 self.positions.values()
1734 .filter(|p| !p.is_flat())
1735 .filter_map(|p| prices.get(p.symbol.as_str()).map(|&pr| p.unrealized_pnl(pr)))
1736 .min_by(|a, b| a.cmp(b))
1737 }
1738
1739 pub fn pct_long(&self) -> Option<Decimal> {
1743 let open: Vec<&Position> = self.positions.values().filter(|p| !p.is_flat()).collect();
1744 if open.is_empty() { return None; }
1745 let longs = open.iter().filter(|p| p.quantity > Decimal::ZERO).count() as u32;
1746 Some(Decimal::from(longs) / Decimal::from(open.len() as u32) * Decimal::ONE_HUNDRED)
1747 }
1748
1749 pub fn pct_short(&self) -> Option<Decimal> {
1753 let open: Vec<&Position> = self.positions.values().filter(|p| !p.is_flat()).collect();
1754 if open.is_empty() { return None; }
1755 let shorts = open.iter().filter(|p| p.quantity < Decimal::ZERO).count() as u32;
1756 Some(Decimal::from(shorts) / Decimal::from(open.len() as u32) * Decimal::ONE_HUNDRED)
1757 }
1758
1759 pub fn realized_pnl_total_abs(&self) -> Decimal {
1761 self.positions.values().map(|p| p.realized_pnl.abs()).sum()
1762 }
1763
1764 pub fn average_entry_price(&self, symbol: &Symbol) -> Option<Price> {
1768 self.positions.get(symbol)?.avg_entry_price()
1769 }
1770
1771 pub fn net_quantity(&self) -> Decimal {
1773 self.positions.values().map(|p| p.quantity).sum()
1774 }
1775
1776 pub fn max_long_notional(&self, prices: &HashMap<String, Price>) -> Option<Decimal> {
1780 self.positions.values()
1781 .filter(|p| p.quantity > Decimal::ZERO)
1782 .filter_map(|p| {
1783 prices.get(p.symbol.as_str()).map(|&pr| (p.quantity * pr.value()).abs())
1784 })
1785 .max_by(|a, b| a.cmp(b))
1786 }
1787
1788 pub fn max_short_notional(&self, prices: &HashMap<String, Price>) -> Option<Decimal> {
1792 self.positions.values()
1793 .filter(|p| p.quantity < Decimal::ZERO)
1794 .filter_map(|p| {
1795 prices.get(p.symbol.as_str()).map(|&pr| (p.quantity * pr.value()).abs())
1796 })
1797 .max_by(|a, b| a.cmp(b))
1798 }
1799
1800 pub fn max_realized_pnl(&self) -> Option<(&Symbol, Decimal)> {
1804 self.positions.iter()
1805 .map(|(sym, p)| (sym, p.realized_pnl))
1806 .max_by(|(_, a), (_, b)| a.cmp(b))
1807 }
1808
1809 pub fn min_realized_pnl(&self) -> Option<(&Symbol, Decimal)> {
1813 self.positions.iter()
1814 .map(|(sym, p)| (sym, p.realized_pnl))
1815 .min_by(|(_, a), (_, b)| a.cmp(b))
1816 }
1817
1818 pub fn avg_holding_bars(&self, current_bar: usize) -> Option<f64> {
1823 let open: Vec<usize> = self.positions.values()
1824 .filter(|p| !p.is_flat())
1825 .map(|p| current_bar.saturating_sub(p.open_bar))
1826 .collect();
1827 if open.is_empty() { return None; }
1828 Some(open.iter().sum::<usize>() as f64 / open.len() as f64)
1829 }
1830
1831 pub fn symbols_with_unrealized_loss(&self, prices: &HashMap<String, Price>) -> Vec<&Symbol> {
1833 self.positions.iter()
1834 .filter(|(_, p)| !p.is_flat())
1835 .filter_map(|(sym, p)| {
1836 prices.get(p.symbol.as_str())
1837 .map(|&pr| (sym, p.unrealized_pnl(pr)))
1838 })
1839 .filter(|(_, pnl)| *pnl < Decimal::ZERO)
1840 .map(|(sym, _)| sym)
1841 .collect()
1842 }
1843
1844 pub fn avg_long_entry_price(&self) -> Option<Decimal> {
1847 let longs: Vec<&Position> = self.positions.values()
1848 .filter(|p| p.is_long())
1849 .collect();
1850 if longs.is_empty() { return None; }
1851 let total_qty: Decimal = longs.iter().map(|p| p.quantity.abs()).sum();
1852 if total_qty.is_zero() { return None; }
1853 let weighted: Decimal = longs.iter().map(|p| p.avg_cost * p.quantity.abs()).sum();
1854 Some(weighted / total_qty)
1855 }
1856
1857 pub fn avg_short_entry_price(&self) -> Option<Decimal> {
1860 let shorts: Vec<&Position> = self.positions.values()
1861 .filter(|p| p.is_short())
1862 .collect();
1863 if shorts.is_empty() { return None; }
1864 let total_qty: Decimal = shorts.iter().map(|p| p.quantity.abs()).sum();
1865 if total_qty.is_zero() { return None; }
1866 let weighted: Decimal = shorts.iter().map(|p| p.avg_cost * p.quantity.abs()).sum();
1867 Some(weighted / total_qty)
1868 }
1869}
1870
1871#[cfg(test)]
1872mod tests {
1873 use super::*;
1874 use rust_decimal_macros::dec;
1875
1876 fn sym(s: &str) -> Symbol {
1877 Symbol::new(s).unwrap()
1878 }
1879
1880 fn make_fill(symbol: &str, side: Side, qty: &str, p: &str, commission: &str) -> Fill {
1881 Fill {
1882 symbol: sym(symbol),
1883 side,
1884 quantity: Quantity::new(qty.parse().unwrap()).unwrap(),
1885 price: Price::new(p.parse().unwrap()).unwrap(),
1886 timestamp: NanoTimestamp::new(0),
1887 commission: commission.parse().unwrap(),
1888 }
1889 }
1890
1891 #[test]
1892 fn test_position_apply_fill_long() {
1893 let mut pos = Position::new(sym("AAPL"));
1894 pos.apply_fill(&make_fill("AAPL", Side::Bid, "10", "100", "0"))
1895 .unwrap();
1896 assert_eq!(pos.quantity, dec!(10));
1897 assert_eq!(pos.avg_cost, dec!(100));
1898 }
1899
1900 #[test]
1901 fn test_position_apply_fill_reduces_position() {
1902 let mut pos = Position::new(sym("AAPL"));
1903 pos.apply_fill(&make_fill("AAPL", Side::Bid, "10", "100", "0"))
1904 .unwrap();
1905 pos.apply_fill(&make_fill("AAPL", Side::Ask, "5", "110", "0"))
1906 .unwrap();
1907 assert_eq!(pos.quantity, dec!(5));
1908 }
1909
1910 #[test]
1911 fn test_position_realized_pnl_on_close() {
1912 let mut pos = Position::new(sym("AAPL"));
1913 pos.apply_fill(&make_fill("AAPL", Side::Bid, "10", "100", "0"))
1914 .unwrap();
1915 let pnl = pos
1916 .apply_fill(&make_fill("AAPL", Side::Ask, "10", "110", "0"))
1917 .unwrap();
1918 assert_eq!(pnl, dec!(100));
1919 assert!(pos.is_flat());
1920 }
1921
1922 #[test]
1923 fn test_position_commission_reduces_realized_pnl() {
1924 let mut pos = Position::new(sym("AAPL"));
1925 pos.apply_fill(&make_fill("AAPL", Side::Bid, "10", "100", "0"))
1926 .unwrap();
1927 let pnl = pos
1928 .apply_fill(&make_fill("AAPL", Side::Ask, "10", "110", "5"))
1929 .unwrap();
1930 assert_eq!(pnl, dec!(95));
1931 }
1932
1933 #[test]
1934 fn test_position_unrealized_pnl() {
1935 let mut pos = Position::new(sym("AAPL"));
1936 pos.apply_fill(&make_fill("AAPL", Side::Bid, "10", "100", "0"))
1937 .unwrap();
1938 let upnl = pos.unrealized_pnl(Price::new(dec!(115)).unwrap());
1939 assert_eq!(upnl, dec!(150));
1940 }
1941
1942 #[test]
1943 fn test_position_market_value() {
1944 let mut pos = Position::new(sym("AAPL"));
1945 pos.apply_fill(&make_fill("AAPL", Side::Bid, "10", "100", "0"))
1946 .unwrap();
1947 assert_eq!(pos.market_value(Price::new(dec!(120)).unwrap()), dec!(1200));
1948 }
1949
1950 #[test]
1951 fn test_position_is_flat_initially() {
1952 let pos = Position::new(sym("X"));
1953 assert!(pos.is_flat());
1954 }
1955
1956 #[test]
1957 fn test_position_is_flat_after_full_close() {
1958 let mut pos = Position::new(sym("AAPL"));
1959 pos.apply_fill(&make_fill("AAPL", Side::Bid, "10", "100", "0"))
1960 .unwrap();
1961 pos.apply_fill(&make_fill("AAPL", Side::Ask, "10", "110", "0"))
1962 .unwrap();
1963 assert!(pos.is_flat());
1964 }
1965
1966 #[test]
1967 fn test_position_avg_cost_weighted_after_two_buys() {
1968 let mut pos = Position::new(sym("X"));
1969 pos.apply_fill(&make_fill("X", Side::Bid, "10", "100", "0"))
1970 .unwrap();
1971 pos.apply_fill(&make_fill("X", Side::Bid, "10", "120", "0"))
1972 .unwrap();
1973 assert_eq!(pos.avg_cost, dec!(110));
1974 }
1975
1976 #[test]
1977 fn test_position_ledger_apply_fill_updates_cash() {
1978 let mut ledger = PositionLedger::new(dec!(10000));
1979 ledger
1980 .apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "1"))
1981 .unwrap();
1982 assert_eq!(ledger.cash(), dec!(8999));
1983 }
1984
1985 #[test]
1986 fn test_position_ledger_insufficient_funds() {
1987 let mut ledger = PositionLedger::new(dec!(100));
1988 let result = ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0"));
1989 assert!(matches!(result, Err(FinError::InsufficientFunds { .. })));
1990 }
1991
1992 #[test]
1993 fn test_position_ledger_equity_calculation() {
1994 let mut ledger = PositionLedger::new(dec!(10000));
1995 ledger
1996 .apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0"))
1997 .unwrap();
1998 let mut prices = HashMap::new();
1999 prices.insert("AAPL".to_owned(), Price::new(dec!(110)).unwrap());
2000 let equity = ledger.equity(&prices).unwrap();
2002 assert_eq!(equity, dec!(9100));
2003 }
2004
2005 #[test]
2006 fn test_position_ledger_net_liquidation_value() {
2007 let mut ledger = PositionLedger::new(dec!(10000));
2009 ledger
2010 .apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0"))
2011 .unwrap();
2012 let mut prices = HashMap::new();
2013 prices.insert("AAPL".to_owned(), Price::new(dec!(110)).unwrap());
2014 let nlv = ledger.net_liquidation_value(&prices).unwrap();
2016 assert_eq!(nlv, dec!(10100));
2017 }
2018
2019 #[test]
2020 fn test_position_ledger_net_liquidation_missing_price() {
2021 let mut ledger = PositionLedger::new(dec!(10000));
2022 ledger
2023 .apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0"))
2024 .unwrap();
2025 let prices: HashMap<String, Price> = HashMap::new();
2026 assert!(ledger.net_liquidation_value(&prices).is_err());
2027 }
2028
2029 #[test]
2030 fn test_position_ledger_pnl_by_symbol() {
2031 let mut ledger = PositionLedger::new(dec!(10000));
2032 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2033 ledger.apply_fill(make_fill("GOOG", Side::Bid, "5", "200", "0")).unwrap();
2034 let mut prices = HashMap::new();
2035 prices.insert("AAPL".to_owned(), Price::new(dec!(110)).unwrap());
2036 prices.insert("GOOG".to_owned(), Price::new(dec!(190)).unwrap());
2037 let pnl = ledger.pnl_by_symbol(&prices).unwrap();
2038 assert_eq!(*pnl.get(&sym("AAPL")).unwrap(), dec!(100)); assert_eq!(*pnl.get(&sym("GOOG")).unwrap(), dec!(-50)); }
2041
2042 #[test]
2043 fn test_position_ledger_pnl_by_symbol_missing_price() {
2044 let mut ledger = PositionLedger::new(dec!(10000));
2045 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2046 let prices: HashMap<String, Price> = HashMap::new();
2047 assert!(ledger.pnl_by_symbol(&prices).is_err());
2048 }
2049
2050 #[test]
2051 fn test_position_ledger_delta_neutral_no_positions() {
2052 let ledger = PositionLedger::new(dec!(10000));
2053 let prices: HashMap<String, Price> = HashMap::new();
2054 assert!(ledger.delta_neutral_check(&prices).unwrap());
2055 }
2056
2057 #[test]
2058 fn test_position_ledger_delta_neutral_long_short_balanced() {
2059 let mut ledger = PositionLedger::new(dec!(10000));
2060 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2061 ledger.apply_fill(make_fill("GOOG", Side::Ask, "10", "100", "0")).unwrap();
2062 let mut prices = HashMap::new();
2063 prices.insert("AAPL".to_owned(), Price::new(dec!(100)).unwrap());
2064 prices.insert("GOOG".to_owned(), Price::new(dec!(100)).unwrap());
2065 assert!(ledger.delta_neutral_check(&prices).unwrap());
2067 }
2068
2069 #[test]
2070 fn test_position_ledger_delta_neutral_one_sided_not_neutral() {
2071 let mut ledger = PositionLedger::new(dec!(10000));
2072 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2073 let mut prices = HashMap::new();
2074 prices.insert("AAPL".to_owned(), Price::new(dec!(100)).unwrap());
2075 assert!(!ledger.delta_neutral_check(&prices).unwrap());
2077 }
2078
2079 #[test]
2080 fn test_position_ledger_open_count_zero_when_empty() {
2081 assert_eq!(PositionLedger::new(dec!(10000)).open_count(), 0);
2082 }
2083
2084 #[test]
2085 fn test_position_ledger_open_count_tracks_positions() {
2086 let mut ledger = PositionLedger::new(dec!(10000));
2087 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2088 assert_eq!(ledger.open_count(), 1);
2089 ledger.apply_fill(make_fill("GOOG", Side::Bid, "5", "200", "0")).unwrap();
2090 assert_eq!(ledger.open_count(), 2);
2091 ledger.apply_fill(make_fill("AAPL", Side::Ask, "10", "105", "0")).unwrap();
2093 assert_eq!(ledger.open_count(), 1);
2094 }
2095
2096 #[test]
2097 fn test_position_ledger_sell_increases_cash() {
2098 let mut ledger = PositionLedger::new(dec!(10000));
2099 ledger
2100 .apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0"))
2101 .unwrap();
2102 ledger
2103 .apply_fill(make_fill("AAPL", Side::Ask, "10", "110", "0"))
2104 .unwrap();
2105 assert_eq!(ledger.cash(), dec!(10100));
2106 }
2107
2108 #[test]
2109 fn test_position_checked_unrealized_pnl_matches() {
2110 let mut pos = Position::new(sym("AAPL"));
2111 pos.apply_fill(&make_fill("AAPL", Side::Bid, "10", "100", "0"))
2112 .unwrap();
2113 let price = Price::new(dec!(115)).unwrap();
2114 let checked = pos.checked_unrealized_pnl(price).unwrap();
2115 let unchecked = pos.unrealized_pnl(price);
2116 assert_eq!(checked, unchecked);
2117 assert_eq!(checked, dec!(150));
2118 }
2119
2120 #[test]
2121 fn test_position_checked_unrealized_pnl_flat_position() {
2122 let pos = Position::new(sym("X"));
2123 let price = Price::new(dec!(100)).unwrap();
2124 assert_eq!(pos.checked_unrealized_pnl(price).unwrap(), dec!(0));
2125 }
2126
2127 #[test]
2128 fn test_position_direction_flat() {
2129 let pos = Position::new(sym("X"));
2130 assert_eq!(pos.direction(), PositionDirection::Flat);
2131 }
2132
2133 #[test]
2134 fn test_position_direction_long() {
2135 let mut pos = Position::new(sym("X"));
2136 pos.apply_fill(&make_fill("X", Side::Bid, "5", "100", "0"))
2137 .unwrap();
2138 assert_eq!(pos.direction(), PositionDirection::Long);
2139 }
2140
2141 #[test]
2142 fn test_position_direction_short() {
2143 let mut pos = Position::new(sym("X"));
2144 pos.apply_fill(&make_fill("X", Side::Ask, "5", "100", "0"))
2146 .unwrap();
2147 assert_eq!(pos.direction(), PositionDirection::Short);
2148 }
2149
2150 #[test]
2151 fn test_position_ledger_positions_iterator() {
2152 let mut ledger = PositionLedger::new(dec!(10000));
2153 ledger
2154 .apply_fill(make_fill("AAPL", Side::Bid, "1", "100", "0"))
2155 .unwrap();
2156 ledger
2157 .apply_fill(make_fill("MSFT", Side::Bid, "1", "200", "0"))
2158 .unwrap();
2159 let count = ledger.positions().count();
2160 assert_eq!(count, 2);
2161 }
2162
2163 #[test]
2164 fn test_position_ledger_total_market_value() {
2165 let mut ledger = PositionLedger::new(dec!(10000));
2166 ledger
2167 .apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0"))
2168 .unwrap();
2169 ledger
2170 .apply_fill(make_fill("MSFT", Side::Bid, "5", "200", "0"))
2171 .unwrap();
2172 let mut prices = HashMap::new();
2173 prices.insert("AAPL".to_owned(), Price::new(dec!(110)).unwrap());
2174 prices.insert("MSFT".to_owned(), Price::new(dec!(210)).unwrap());
2175 let mv = ledger.total_market_value(&prices).unwrap();
2177 assert_eq!(mv, dec!(2150));
2178 }
2179
2180 #[test]
2181 fn test_position_ledger_total_market_value_missing_price() {
2182 let mut ledger = PositionLedger::new(dec!(10000));
2183 ledger
2184 .apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0"))
2185 .unwrap();
2186 let prices: HashMap<String, Price> = HashMap::new();
2187 assert!(matches!(
2188 ledger.total_market_value(&prices),
2189 Err(FinError::PositionNotFound(_))
2190 ));
2191 }
2192
2193 #[test]
2194 fn test_position_ledger_unrealized_pnl_total() {
2195 let mut ledger = PositionLedger::new(dec!(10000));
2196 ledger
2197 .apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0"))
2198 .unwrap();
2199 let mut prices = HashMap::new();
2200 prices.insert("AAPL".to_owned(), Price::new(dec!(105)).unwrap());
2201 let upnl = ledger.unrealized_pnl_total(&prices).unwrap();
2202 assert_eq!(upnl, dec!(50));
2203 }
2204
2205 #[test]
2206 fn test_position_ledger_position_count_includes_flat() {
2207 let mut ledger = PositionLedger::new(dec!(10000));
2208 ledger
2210 .apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0"))
2211 .unwrap();
2212 ledger
2213 .apply_fill(make_fill("AAPL", Side::Ask, "10", "100", "0"))
2214 .unwrap();
2215 ledger
2217 .apply_fill(make_fill("MSFT", Side::Bid, "5", "200", "0"))
2218 .unwrap();
2219 assert_eq!(ledger.position_count(), 2, "both symbols tracked");
2220 assert_eq!(ledger.open_position_count(), 1, "only MSFT open");
2221 }
2222
2223 #[test]
2224 fn test_position_ledger_position_count_zero_on_empty() {
2225 let ledger = PositionLedger::new(dec!(10000));
2226 assert_eq!(ledger.position_count(), 0);
2227 }
2228
2229 #[test]
2230 fn test_position_unrealized_pnl_pct_long_gain() {
2231 let mut pos = Position::new(sym("AAPL"));
2232 pos.apply_fill(&make_fill("AAPL", Side::Bid, "10", "100", "0"))
2233 .unwrap();
2234 let current = Price::new(dec!(110)).unwrap();
2235 let pct = pos.unrealized_pnl_pct(current).unwrap();
2236 assert_eq!(pct, dec!(10));
2237 }
2238
2239 #[test]
2240 fn test_position_unrealized_pnl_pct_flat_returns_none() {
2241 let pos = Position::new(sym("AAPL"));
2242 let current = Price::new(dec!(110)).unwrap();
2243 assert!(pos.unrealized_pnl_pct(current).is_none());
2244 }
2245
2246 #[test]
2247 fn test_position_unrealized_pnl_pct_loss() {
2248 let mut pos = Position::new(sym("AAPL"));
2249 pos.apply_fill(&make_fill("AAPL", Side::Bid, "10", "100", "0"))
2250 .unwrap();
2251 let current = Price::new(dec!(90)).unwrap();
2252 let pct = pos.unrealized_pnl_pct(current).unwrap();
2253 assert_eq!(pct, dec!(-10));
2254 }
2255
2256 #[test]
2257 fn test_position_ledger_open_positions_excludes_flat() {
2258 let mut ledger = PositionLedger::new(dec!(10000));
2259 ledger
2260 .apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0"))
2261 .unwrap();
2262 ledger
2263 .apply_fill(make_fill("AAPL", Side::Ask, "10", "100", "0"))
2264 .unwrap();
2265 ledger
2266 .apply_fill(make_fill("MSFT", Side::Bid, "5", "200", "0"))
2267 .unwrap();
2268 let open: Vec<_> = ledger.open_positions().collect();
2269 assert_eq!(open.len(), 1);
2270 assert_eq!(open[0].symbol.as_str(), "MSFT");
2271 }
2272
2273 #[test]
2274 fn test_position_ledger_open_positions_empty_when_all_flat() {
2275 let mut ledger = PositionLedger::new(dec!(10000));
2276 ledger
2277 .apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0"))
2278 .unwrap();
2279 ledger
2280 .apply_fill(make_fill("AAPL", Side::Ask, "10", "100", "0"))
2281 .unwrap();
2282 let open: Vec<_> = ledger.open_positions().collect();
2283 assert!(open.is_empty());
2284 }
2285
2286 #[test]
2287 fn test_position_is_long() {
2288 let mut pos = Position::new(sym("AAPL"));
2289 pos.apply_fill(&make_fill("AAPL", Side::Bid, "10", "100", "0"))
2290 .unwrap();
2291 assert!(pos.is_long());
2292 assert!(!pos.is_short());
2293 assert!(!pos.is_flat());
2294 }
2295
2296 #[test]
2297 fn test_position_is_short() {
2298 let mut pos = Position::new(sym("AAPL"));
2299 pos.apply_fill(&make_fill("AAPL", Side::Ask, "10", "100", "0"))
2300 .unwrap();
2301 assert!(pos.is_short());
2302 assert!(!pos.is_long());
2303 assert!(!pos.is_flat());
2304 }
2305
2306 #[test]
2307 fn test_position_is_flat_after_close() {
2308 let mut pos = Position::new(sym("AAPL"));
2309 pos.apply_fill(&make_fill("AAPL", Side::Bid, "10", "100", "0"))
2310 .unwrap();
2311 pos.apply_fill(&make_fill("AAPL", Side::Ask, "10", "100", "0"))
2312 .unwrap();
2313 assert!(pos.is_flat());
2314 assert!(!pos.is_long());
2315 assert!(!pos.is_short());
2316 }
2317
2318 #[test]
2319 fn test_position_ledger_flat_positions() {
2320 let mut ledger = PositionLedger::new(dec!(10000));
2321 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2323 ledger.apply_fill(make_fill("AAPL", Side::Ask, "10", "100", "0")).unwrap();
2324 ledger.apply_fill(make_fill("MSFT", Side::Bid, "5", "200", "0")).unwrap();
2326 let flat: Vec<_> = ledger.flat_positions().collect();
2327 assert_eq!(flat.len(), 1);
2328 assert_eq!(flat[0].symbol, sym("AAPL"));
2329 }
2330
2331 #[test]
2332 fn test_position_ledger_flat_positions_empty_when_all_open() {
2333 let mut ledger = PositionLedger::new(dec!(10000));
2334 ledger.apply_fill(make_fill("AAPL", Side::Bid, "1", "100", "0")).unwrap();
2335 assert_eq!(ledger.flat_positions().count(), 0);
2336 }
2337
2338 #[test]
2339 fn test_position_ledger_deposit_increases_cash() {
2340 let mut ledger = PositionLedger::new(dec!(1000));
2341 ledger.deposit(dec!(500));
2342 assert_eq!(ledger.cash(), dec!(1500));
2343 }
2344
2345 #[test]
2346 fn test_position_ledger_withdraw_decreases_cash() {
2347 let mut ledger = PositionLedger::new(dec!(1000));
2348 ledger.withdraw(dec!(300)).unwrap();
2349 assert_eq!(ledger.cash(), dec!(700));
2350 }
2351
2352 #[test]
2353 fn test_position_ledger_withdraw_insufficient_fails() {
2354 let mut ledger = PositionLedger::new(dec!(100));
2355 assert!(matches!(
2356 ledger.withdraw(dec!(200)),
2357 Err(FinError::InsufficientFunds { .. })
2358 ));
2359 assert_eq!(ledger.cash(), dec!(100), "cash unchanged on failure");
2360 }
2361
2362 #[test]
2363 fn test_position_is_profitable_true() {
2364 let mut pos = Position::new(sym("AAPL"));
2365 pos.apply_fill(&make_fill("AAPL", Side::Bid, "10", "100", "0"))
2366 .unwrap();
2367 let current = Price::new(dec!(110)).unwrap();
2368 assert!(pos.is_profitable(current));
2369 }
2370
2371 #[test]
2372 fn test_position_is_profitable_false_when_at_loss() {
2373 let mut pos = Position::new(sym("AAPL"));
2374 pos.apply_fill(&make_fill("AAPL", Side::Bid, "10", "100", "0"))
2375 .unwrap();
2376 let current = Price::new(dec!(90)).unwrap();
2377 assert!(!pos.is_profitable(current));
2378 }
2379
2380 #[test]
2381 fn test_position_ledger_long_positions() {
2382 let mut ledger = PositionLedger::new(dec!(10000));
2383 ledger
2384 .apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0"))
2385 .unwrap();
2386 let longs: Vec<_> = ledger.long_positions().collect();
2387 assert_eq!(longs.len(), 1);
2388 assert_eq!(longs[0].symbol.as_str(), "AAPL");
2389 }
2390
2391 #[test]
2392 fn test_position_ledger_short_positions_empty_for_long_only() {
2393 let mut ledger = PositionLedger::new(dec!(10000));
2394 ledger
2395 .apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0"))
2396 .unwrap();
2397 let shorts: Vec<_> = ledger.short_positions().collect();
2398 assert!(shorts.is_empty());
2399 }
2400
2401 #[test]
2402 fn test_position_ledger_realized_pnl_after_close() {
2403 let mut ledger = PositionLedger::new(dec!(10000));
2404 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2405 ledger.apply_fill(make_fill("AAPL", Side::Ask, "10", "110", "0")).unwrap();
2406 assert_eq!(ledger.realized_pnl(&sym("AAPL")), Some(dec!(100)));
2407 }
2408
2409 #[test]
2410 fn test_position_ledger_realized_pnl_unknown_symbol_returns_none() {
2411 let ledger = PositionLedger::new(dec!(10000));
2412 assert!(ledger.realized_pnl(&sym("AAPL")).is_none());
2413 }
2414
2415 #[test]
2416 fn test_position_ledger_realized_pnl_zero_before_close() {
2417 let mut ledger = PositionLedger::new(dec!(10000));
2418 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2419 assert_eq!(ledger.realized_pnl(&sym("AAPL")), Some(dec!(0)));
2420 }
2421
2422 #[test]
2423 fn test_position_ledger_symbols_sorted_order() {
2424 let mut ledger = PositionLedger::new(dec!(10000));
2425 ledger.apply_fill(make_fill("MSFT", Side::Bid, "1", "100", "0")).unwrap();
2426 ledger.apply_fill(make_fill("AAPL", Side::Bid, "1", "100", "0")).unwrap();
2427 ledger.apply_fill(make_fill("GOOG", Side::Bid, "1", "100", "0")).unwrap();
2428 let sorted = ledger.symbols_sorted();
2429 let names: Vec<&str> = sorted.iter().map(|s| s.as_str()).collect();
2430 assert_eq!(names, vec!["AAPL", "GOOG", "MSFT"]);
2431 }
2432
2433 #[test]
2434 fn test_position_ledger_symbols_sorted_empty() {
2435 let ledger = PositionLedger::new(dec!(10000));
2436 assert!(ledger.symbols_sorted().is_empty());
2437 }
2438
2439 #[test]
2440 fn test_position_avg_entry_price_long() {
2441 let sym = Symbol::new("AAPL").unwrap();
2442 let mut pos = Position::new(sym.clone());
2443 let fill = Fill::new(
2444 sym,
2445 Side::Bid,
2446 Quantity::new(dec!(10)).unwrap(),
2447 Price::new(dec!(150)).unwrap(),
2448 NanoTimestamp::new(0),
2449 );
2450 pos.apply_fill(&fill).unwrap();
2451 assert_eq!(pos.avg_entry_price().unwrap().value(), dec!(150));
2452 }
2453
2454 #[test]
2455 fn test_position_avg_entry_price_flat_returns_none() {
2456 let sym = Symbol::new("AAPL").unwrap();
2457 let pos = Position::new(sym);
2458 assert!(pos.avg_entry_price().is_none());
2459 }
2460
2461 #[test]
2462 fn test_position_avg_entry_price_after_partial_close() {
2463 let sym = Symbol::new("X").unwrap();
2464 let mut pos = Position::new(sym.clone());
2465 pos.apply_fill(&Fill::new(sym.clone(), Side::Bid,
2466 Quantity::new(dec!(10)).unwrap(), Price::new(dec!(100)).unwrap(),
2467 NanoTimestamp::new(0))).unwrap();
2468 pos.apply_fill(&Fill::new(sym.clone(), Side::Ask,
2469 Quantity::new(dec!(5)).unwrap(), Price::new(dec!(100)).unwrap(),
2470 NanoTimestamp::new(1))).unwrap();
2471 assert_eq!(pos.avg_entry_price().unwrap().value(), dec!(100));
2473 }
2474
2475 #[test]
2476 fn test_position_ledger_has_position_true_after_fill() {
2477 let mut ledger = PositionLedger::new(dec!(10000));
2478 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2479 assert!(ledger.has_position(&sym("AAPL")));
2480 }
2481
2482 #[test]
2483 fn test_position_ledger_has_position_false_for_unknown() {
2484 let ledger = PositionLedger::new(dec!(10000));
2485 assert!(!ledger.has_position(&sym("AAPL")));
2486 }
2487
2488 #[test]
2489 fn test_position_ledger_has_position_true_even_when_flat() {
2490 let mut ledger = PositionLedger::new(dec!(10000));
2491 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2492 ledger.apply_fill(make_fill("AAPL", Side::Ask, "10", "100", "0")).unwrap();
2493 assert!(ledger.has_position(&sym("AAPL")));
2495 }
2496
2497 #[test]
2498 fn test_position_ledger_open_symbols_returns_non_flat() {
2499 let mut ledger = PositionLedger::new(dec!(10000));
2500 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2501 ledger.apply_fill(make_fill("MSFT", Side::Bid, "5", "200", "1")).unwrap();
2502 let symbols: Vec<_> = ledger.open_symbols().collect();
2503 assert_eq!(symbols.len(), 2);
2504 }
2505
2506 #[test]
2507 fn test_position_ledger_open_symbols_excludes_flat() {
2508 let mut ledger = PositionLedger::new(dec!(10000));
2509 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2510 ledger.apply_fill(make_fill("AAPL", Side::Ask, "10", "100", "1")).unwrap(); ledger.apply_fill(make_fill("MSFT", Side::Bid, "5", "200", "2")).unwrap();
2512 let symbols: Vec<_> = ledger.open_symbols().collect();
2513 assert_eq!(symbols.len(), 1);
2514 assert_eq!(symbols[0].as_str(), "MSFT");
2515 }
2516
2517 #[test]
2518 fn test_position_ledger_open_symbols_empty_when_all_flat() {
2519 let mut ledger = PositionLedger::new(dec!(10000));
2520 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2521 ledger.apply_fill(make_fill("AAPL", Side::Ask, "10", "100", "1")).unwrap();
2522 let symbols: Vec<_> = ledger.open_symbols().collect();
2523 assert!(symbols.is_empty());
2524 }
2525
2526 #[test]
2527 fn test_position_ledger_total_long_exposure() {
2528 let mut ledger = PositionLedger::new(dec!(100000));
2529 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2530 assert_eq!(ledger.total_long_exposure(), dec!(1000));
2532 }
2533
2534 #[test]
2535 fn test_position_ledger_total_long_exposure_zero_when_flat() {
2536 let ledger = PositionLedger::new(dec!(10000));
2537 assert_eq!(ledger.total_long_exposure(), dec!(0));
2538 }
2539
2540 #[test]
2541 fn test_position_ledger_total_short_exposure_zero_when_no_shorts() {
2542 let mut ledger = PositionLedger::new(dec!(100000));
2543 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2544 assert_eq!(ledger.total_short_exposure(), dec!(0));
2545 }
2546
2547 #[test]
2548 fn test_allocation_pct_single_position() {
2549 let mut ledger = PositionLedger::new(dec!(100000));
2550 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2551 let mut prices = HashMap::new();
2552 let sym = Symbol::new("AAPL").unwrap();
2553 prices.insert("AAPL".to_string(), Price::new(dec!(100)).unwrap());
2554 let pct = ledger.allocation_pct(&sym, &prices).unwrap();
2555 assert_eq!(pct, Some(dec!(100)));
2557 }
2558
2559 #[test]
2560 fn test_allocation_pct_flat_position_returns_none() {
2561 let ledger = PositionLedger::new(dec!(100000));
2562 let mut prices = HashMap::new();
2563 let sym = Symbol::new("AAPL").unwrap();
2564 prices.insert("AAPL".to_string(), Price::new(dec!(100)).unwrap());
2565 assert!(ledger.allocation_pct(&sym, &prices).is_err());
2567 }
2568
2569 #[test]
2570 fn test_positions_sorted_by_pnl_descending() {
2571 let mut ledger = PositionLedger::new(dec!(100000));
2572 ledger.apply_fill(make_fill("AAPL", Side::Bid, "1", "100", "0")).unwrap();
2573 ledger.apply_fill(make_fill("GOOG", Side::Bid, "1", "200", "0")).unwrap();
2574 let mut prices = HashMap::new();
2575 prices.insert("AAPL".to_string(), Price::new(dec!(110)).unwrap());
2577 prices.insert("GOOG".to_string(), Price::new(dec!(250)).unwrap());
2578 let sorted = ledger.positions_sorted_by_pnl(&prices);
2579 assert_eq!(sorted[0].symbol.as_str(), "GOOG");
2581 assert_eq!(sorted[1].symbol.as_str(), "AAPL");
2582 }
2583
2584 #[test]
2585 fn test_positions_sorted_by_pnl_empty_when_all_flat() {
2586 let ledger = PositionLedger::new(dec!(100000));
2587 let prices = HashMap::new();
2588 assert!(ledger.positions_sorted_by_pnl(&prices).is_empty());
2589 }
2590
2591 #[test]
2592 fn test_all_flat_initially() {
2593 let ledger = PositionLedger::new(dec!(100000));
2594 assert!(ledger.all_flat());
2595 }
2596
2597 #[test]
2598 fn test_all_flat_false_after_open_position() {
2599 let mut ledger = PositionLedger::new(dec!(100000));
2600 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "150", "0")).unwrap();
2601 assert!(!ledger.all_flat());
2602 }
2603
2604 #[test]
2605 fn test_all_flat_true_after_close_position() {
2606 let mut ledger = PositionLedger::new(dec!(100000));
2607 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "150", "0")).unwrap();
2608 ledger.apply_fill(make_fill("AAPL", Side::Ask, "10", "155", "0")).unwrap();
2609 assert!(ledger.all_flat());
2610 }
2611
2612 #[test]
2613 fn test_concentration_pct_single_position() {
2614 let mut ledger = PositionLedger::new(dec!(100000));
2615 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "150", "0")).unwrap();
2616 let sym = Symbol::new("AAPL").unwrap();
2617 let mut prices = HashMap::new();
2618 prices.insert("AAPL".to_string(), Price::new(dec!(150)).unwrap());
2619 let pct = ledger.concentration_pct(&sym, &prices).unwrap();
2621 assert_eq!(pct, dec!(100));
2622 }
2623
2624 #[test]
2625 fn test_concentration_pct_two_equal_positions() {
2626 let mut ledger = PositionLedger::new(dec!(100000));
2627 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2628 ledger.apply_fill(make_fill("GOOG", Side::Bid, "10", "100", "0")).unwrap();
2629 let sym = Symbol::new("AAPL").unwrap();
2630 let mut prices = HashMap::new();
2631 prices.insert("AAPL".to_string(), Price::new(dec!(100)).unwrap());
2632 prices.insert("GOOG".to_string(), Price::new(dec!(100)).unwrap());
2633 let pct = ledger.concentration_pct(&sym, &prices).unwrap();
2634 assert_eq!(pct, dec!(50));
2635 }
2636
2637 #[test]
2638 fn test_concentration_pct_missing_price_returns_none() {
2639 let mut ledger = PositionLedger::new(dec!(100000));
2640 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2641 let sym = Symbol::new("AAPL").unwrap();
2642 let prices = HashMap::new(); assert!(ledger.concentration_pct(&sym, &prices).is_none());
2644 }
2645
2646 #[test]
2647 fn test_avg_realized_pnl_per_symbol_none_when_empty() {
2648 let ledger = PositionLedger::new(dec!(100000));
2649 assert!(ledger.avg_realized_pnl_per_symbol().is_none());
2650 }
2651
2652 #[test]
2653 fn test_avg_realized_pnl_per_symbol_with_closed_trade() {
2654 let mut ledger = PositionLedger::new(dec!(100000));
2655 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2657 ledger.apply_fill(make_fill("AAPL", Side::Ask, "10", "110", "0")).unwrap();
2658 let avg = ledger.avg_realized_pnl_per_symbol().unwrap();
2659 assert_eq!(avg, dec!(100));
2660 }
2661
2662 #[test]
2663 fn test_net_exposure_no_prices_returns_none() {
2664 let mut ledger = PositionLedger::new(dec!(100000));
2665 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2666 let prices = HashMap::new();
2667 assert!(ledger.net_market_exposure(&prices).is_none());
2668 }
2669
2670 #[test]
2671 fn test_net_exposure_long_only() {
2672 let mut ledger = PositionLedger::new(dec!(100000));
2673 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2674 let mut prices = HashMap::new();
2675 prices.insert("AAPL".to_string(), Price::new(dec!(110)).unwrap());
2676 assert_eq!(ledger.net_market_exposure(&prices).unwrap(), dec!(1100));
2677 }
2678
2679 #[test]
2680 fn test_win_rate_none_when_empty() {
2681 let ledger = PositionLedger::new(dec!(100000));
2682 assert!(ledger.win_rate().is_none());
2683 }
2684
2685 #[test]
2686 fn test_win_rate_one_winner() {
2687 let mut ledger = PositionLedger::new(dec!(100000));
2688 ledger.apply_fill(make_fill("AAPL", Side::Bid, "10", "100", "0")).unwrap();
2690 ledger.apply_fill(make_fill("AAPL", Side::Ask, "10", "110", "0")).unwrap();
2691 ledger.apply_fill(make_fill("GOOG", Side::Bid, "10", "100", "0")).unwrap();
2693 let rate = ledger.win_rate().unwrap();
2694 assert_eq!(rate, dec!(50));
2696 }
2697}