use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use super::Money;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Position {
symbol: String,
exchange: String,
instrument_id: Option<String>,
quantity: Decimal,
lot_size: u32,
current_price: Money,
average_cost: Option<Money>,
blocked: bool,
blocked_lots: u32,
}
impl Position {
#[must_use]
pub fn new(
symbol: impl Into<String>,
exchange: impl Into<String>,
quantity: Decimal,
current_price: Money,
) -> Self {
Self {
symbol: symbol.into(),
exchange: exchange.into(),
instrument_id: None,
quantity,
lot_size: 1,
current_price,
average_cost: None,
blocked: false,
blocked_lots: 0,
}
}
#[must_use]
pub fn builder(symbol: impl Into<String>, exchange: impl Into<String>) -> PositionBuilder {
PositionBuilder::new(symbol, exchange)
}
#[must_use]
pub fn symbol(&self) -> &str {
&self.symbol
}
#[must_use]
pub fn exchange(&self) -> &str {
&self.exchange
}
#[must_use]
pub fn instrument_id(&self) -> Option<&str> {
self.instrument_id.as_deref()
}
#[must_use]
pub const fn quantity(&self) -> Decimal {
self.quantity
}
#[must_use]
pub const fn lot_size(&self) -> u32 {
self.lot_size
}
#[must_use]
pub const fn current_price(&self) -> &Money {
&self.current_price
}
#[must_use]
pub const fn average_cost(&self) -> Option<&Money> {
self.average_cost.as_ref()
}
#[must_use]
pub const fn is_blocked(&self) -> bool {
self.blocked
}
#[must_use]
pub const fn blocked_lots(&self) -> u32 {
self.blocked_lots
}
#[must_use]
pub fn market_value(&self) -> Money {
self.current_price.multiply(self.quantity)
}
#[must_use]
pub fn lots_held(&self) -> u32 {
let lot_size = Decimal::from(self.lot_size);
(self.quantity / lot_size)
.floor()
.to_string()
.parse::<u32>()
.unwrap_or(0)
}
#[must_use]
pub fn tradeable_lots(&self) -> u32 {
self.lots_held().saturating_sub(self.blocked_lots)
}
#[must_use]
pub fn unrealized_pnl(&self) -> Option<Money> {
let avg_cost = self.average_cost.as_ref()?;
let cost_basis = avg_cost.multiply(self.quantity);
let market_value = self.market_value();
market_value.checked_sub(&cost_basis)
}
#[must_use]
pub fn unrealized_pnl_percent(&self) -> Option<Decimal> {
let avg_cost = self.average_cost.as_ref()?;
if avg_cost.is_zero() {
return None;
}
let pnl = self.unrealized_pnl()?;
let cost_basis = avg_cost.multiply(self.quantity);
Some((pnl.amount() / cost_basis.amount()) * Decimal::from(100))
}
pub fn update_price(&mut self, price: Money) {
self.current_price = price;
}
pub fn update_quantity(&mut self, quantity: Decimal) {
self.quantity = quantity;
}
}
pub struct PositionBuilder {
symbol: String,
exchange: String,
instrument_id: Option<String>,
quantity: Decimal,
lot_size: u32,
current_price: Option<Money>,
average_cost: Option<Money>,
blocked: bool,
blocked_lots: u32,
}
impl PositionBuilder {
fn new(symbol: impl Into<String>, exchange: impl Into<String>) -> Self {
Self {
symbol: symbol.into(),
exchange: exchange.into(),
instrument_id: None,
quantity: Decimal::ZERO,
lot_size: 1,
current_price: None,
average_cost: None,
blocked: false,
blocked_lots: 0,
}
}
#[must_use]
pub fn instrument_id(mut self, id: impl Into<String>) -> Self {
self.instrument_id = Some(id.into());
self
}
#[must_use]
pub const fn quantity(mut self, qty: Decimal) -> Self {
self.quantity = qty;
self
}
#[must_use]
pub const fn lot_size(mut self, size: u32) -> Self {
self.lot_size = size;
self
}
#[must_use]
pub fn current_price(mut self, price: Money) -> Self {
self.current_price = Some(price);
self
}
#[must_use]
pub fn average_cost(mut self, cost: Money) -> Self {
self.average_cost = Some(cost);
self
}
#[must_use]
pub const fn blocked(mut self, is_blocked: bool) -> Self {
self.blocked = is_blocked;
self
}
#[must_use]
pub const fn blocked_lots(mut self, lots: u32) -> Self {
self.blocked_lots = lots;
self
}
#[must_use]
pub fn build(self) -> Position {
Position {
symbol: self.symbol,
exchange: self.exchange,
instrument_id: self.instrument_id,
quantity: self.quantity,
lot_size: self.lot_size,
current_price: self.current_price.expect("current_price is required"),
average_cost: self.average_cost,
blocked: self.blocked,
blocked_lots: self.blocked_lots,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
fn make_usd(amount: Decimal) -> Money {
Money::new(amount, "USD")
}
#[test]
fn test_position_creation() {
let pos = Position::new("AAPL", "NASDAQ", dec!(10), make_usd(dec!(150)));
assert_eq!(pos.symbol(), "AAPL");
assert_eq!(pos.exchange(), "NASDAQ");
assert_eq!(pos.quantity(), dec!(10));
}
#[test]
fn test_position_builder() {
let pos = Position::builder("SBER", "MOEX")
.instrument_id("BBG004730N88")
.quantity(dec!(100))
.lot_size(10)
.current_price(make_usd(dec!(250)))
.average_cost(make_usd(dec!(200)))
.build();
assert_eq!(pos.symbol(), "SBER");
assert_eq!(pos.instrument_id(), Some("BBG004730N88"));
assert_eq!(pos.lot_size(), 10);
assert_eq!(pos.lots_held(), 10); }
#[test]
fn test_market_value() {
let pos = Position::new("BTC", "BINANCE", dec!(2.5), make_usd(dec!(40000)));
let value = pos.market_value();
assert_eq!(value.amount(), dec!(100000));
}
#[test]
fn test_unrealized_pnl() {
let pos = Position::builder("AAPL", "NASDAQ")
.quantity(dec!(10))
.current_price(make_usd(dec!(150)))
.average_cost(make_usd(dec!(100)))
.build();
let pnl = pos.unrealized_pnl().unwrap();
assert_eq!(pnl.amount(), dec!(500));
let pnl_pct = pos.unrealized_pnl_percent().unwrap();
assert_eq!(pnl_pct, dec!(50)); }
#[test]
fn test_lots_calculation() {
let pos = Position::builder("SBER", "MOEX")
.quantity(dec!(105))
.lot_size(10)
.current_price(make_usd(dec!(250)))
.blocked_lots(3)
.build();
assert_eq!(pos.lots_held(), 10); assert_eq!(pos.tradeable_lots(), 7); }
}