use crate::model::types::UnderlyingAssetType;
use num_traits::ToPrimitive;
use positive::Positive;
use pretty_simple_display::{DebugPretty, DisplaySimple};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct Balance {
pub symbol: String,
pub quantity: Positive,
pub average_premium: Positive,
pub current_premium: Option<Positive>,
pub exchange: String,
pub underlying_asset_type: UnderlyingAssetType,
pub margin_info: Option<MarginInfo>,
}
impl Balance {
pub fn new(
symbol: String,
quantity: Positive,
average_premium: Positive,
current_premium: Option<Positive>,
exchange: String,
underlying_asset_type: UnderlyingAssetType,
margin_info: Option<MarginInfo>,
) -> Self {
Self {
symbol,
quantity,
average_premium,
current_premium,
exchange,
underlying_asset_type,
margin_info,
}
}
pub fn get_total_value(&self) -> Positive {
match self.current_premium {
Some(current_price) => {
Positive::new(
(self.quantity.value() * current_price.value())
.to_f64()
.unwrap_or(0.0),
)
.unwrap_or(Positive::ZERO)
}
None => {
Positive::new(
(self.quantity.value() * self.average_premium.value())
.to_f64()
.unwrap_or(0.0),
)
.unwrap_or(Positive::ZERO)
}
}
}
pub fn get_unrealized_pnl(&self) -> Decimal {
match self.current_premium {
Some(current_price) => {
let current_value = self.quantity.value() * current_price.value();
let cost_basis = self.quantity.value() * self.average_premium.value();
current_value - cost_basis
}
None => Decimal::ZERO,
}
}
pub fn update_current_premium(&mut self, new_premium: Positive) {
self.current_premium = Some(new_premium);
}
pub fn is_profitable(&self) -> bool {
self.get_unrealized_pnl() > Decimal::ZERO
}
pub fn get_cost_basis(&self) -> Positive {
Positive::new(
(self.quantity.value() * self.average_premium.value())
.to_f64()
.unwrap_or(0.0),
)
.unwrap_or(Positive::ZERO)
}
pub fn get_percentage_return(&self) -> Decimal {
match self.current_premium {
Some(_current_price) => {
let pnl = self.get_unrealized_pnl();
let cost_basis = self.get_cost_basis().value();
if cost_basis > Decimal::ZERO {
(pnl / cost_basis) * Decimal::from(100)
} else {
Decimal::ZERO
}
}
None => Decimal::ZERO,
}
}
}
#[derive(
DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize, ToSchema, Default,
)]
pub struct MarginInfo {
pub available_margin: Decimal,
pub used_margin: Decimal,
pub maintenance_margin: Decimal,
pub initial_margin: Option<Decimal>,
pub margin_ratio: Option<Decimal>,
}
#[derive(DebugPretty, DisplaySimple, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
pub struct Portfolio {
pub balances: Vec<Balance>,
pub name: String,
}
impl Portfolio {
pub fn new(name: String) -> Self {
Self {
balances: Vec::new(),
name,
}
}
pub fn add_balance(&mut self, balance: Balance) {
self.balances.push(balance);
}
pub fn remove_balance(&mut self, symbol: &str, exchange: &str) -> bool {
if let Some(pos) = self
.balances
.iter()
.position(|b| b.symbol == symbol && b.exchange == exchange)
{
self.balances.remove(pos);
true
} else {
false
}
}
pub fn get_balance(&self, symbol: &str, exchange: &str) -> Option<&Balance> {
self.balances
.iter()
.find(|b| b.symbol == symbol && b.exchange == exchange)
}
pub fn get_balance_mut(&mut self, symbol: &str, exchange: &str) -> Option<&mut Balance> {
self.balances
.iter_mut()
.find(|b| b.symbol == symbol && b.exchange == exchange)
}
pub fn get_total_value(&self) -> Positive {
let total_value: f64 = self
.balances
.iter()
.map(|balance| balance.get_total_value().value().to_f64().unwrap_or(0.0))
.sum();
Positive::new(total_value).unwrap_or(Positive::ZERO)
}
pub fn get_total_unrealized_pnl(&self) -> Decimal {
self.balances
.iter()
.map(|balance| balance.get_unrealized_pnl())
.sum()
}
pub fn get_balances_by_exchange(&self, exchange: &str) -> Vec<&Balance> {
self.balances
.iter()
.filter(|balance| balance.exchange == exchange)
.collect()
}
pub fn has_profitable_positions(&self) -> bool {
self.balances.iter().any(|balance| balance.is_profitable())
}
pub fn balance_count(&self) -> usize {
self.balances.len()
}
pub fn is_empty(&self) -> bool {
self.balances.is_empty()
}
}
impl Default for Portfolio {
fn default() -> Self {
Self::new("Default Portfolio".to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
#[test]
fn test_balance_creation() {
let balance = Balance::new(
"AAPL240315C00150000".to_string(),
pos_or_panic!(10.0),
pos_or_panic!(5.50),
None,
"CBOE".to_string(),
UnderlyingAssetType::Stock,
None,
);
assert_eq!(balance.symbol, "AAPL240315C00150000");
assert_eq!(balance.quantity, pos_or_panic!(10.0));
assert_eq!(balance.average_premium, pos_or_panic!(5.50));
assert_eq!(balance.exchange, "CBOE");
assert_eq!(balance.underlying_asset_type, UnderlyingAssetType::Stock);
assert!(balance.current_premium.is_none());
assert!(balance.margin_info.is_none());
}
#[test]
fn test_balance_total_value() {
let balance = Balance::new(
"TSLA240315C00200000".to_string(),
pos_or_panic!(5.0),
pos_or_panic!(8.00),
Some(pos_or_panic!(12.50)),
"CBOE".to_string(),
UnderlyingAssetType::Stock,
None,
);
let total_value = balance.get_total_value();
assert_eq!(total_value, pos_or_panic!(62.5)); }
#[test]
fn test_balance_unrealized_pnl() {
let balance = Balance::new(
"AAPL240315C00150000".to_string(),
pos_or_panic!(10.0),
pos_or_panic!(5.50),
Some(pos_or_panic!(7.00)),
"CBOE".to_string(),
UnderlyingAssetType::Stock,
None,
);
let pnl = balance.get_unrealized_pnl();
assert_eq!(pnl, dec!(15.0)); }
#[test]
fn test_balance_is_profitable() {
let profitable_balance = Balance::new(
"GOOGL240315C00200000".to_string(),
pos_or_panic!(5.0),
pos_or_panic!(10.00),
Some(pos_or_panic!(12.00)),
"CBOE".to_string(),
UnderlyingAssetType::Stock,
None,
);
let losing_balance = Balance::new(
"TSLA240315C00080000".to_string(),
pos_or_panic!(3.0),
pos_or_panic!(8.00),
Some(pos_or_panic!(6.50)),
"CBOE".to_string(),
UnderlyingAssetType::Stock,
None,
);
assert!(profitable_balance.is_profitable());
assert!(!losing_balance.is_profitable());
}
#[test]
fn test_portfolio_creation() {
let portfolio = Portfolio::new("My Portfolio".to_string());
assert_eq!(portfolio.name, "My Portfolio");
assert!(portfolio.is_empty());
assert_eq!(portfolio.balance_count(), 0);
}
#[test]
fn test_portfolio_add_balance() {
let mut portfolio = Portfolio::new("Test Portfolio".to_string());
let balance = Balance::new(
"AAPL240315C00150000".to_string(),
pos_or_panic!(10.0),
pos_or_panic!(5.50),
Some(pos_or_panic!(7.00)),
"CBOE".to_string(),
UnderlyingAssetType::Stock,
None,
);
portfolio.add_balance(balance);
assert_eq!(portfolio.balance_count(), 1);
assert!(!portfolio.is_empty());
}
#[test]
fn test_portfolio_total_value() {
let mut portfolio = Portfolio::new("Test Portfolio".to_string());
let balance1 = Balance::new(
"AAPL240315C00150000".to_string(),
pos_or_panic!(10.0),
pos_or_panic!(5.50),
Some(pos_or_panic!(7.00)),
"CBOE".to_string(),
UnderlyingAssetType::Stock,
None,
);
let balance2 = Balance::new(
"TSLA240315C00200000".to_string(),
pos_or_panic!(5.0),
pos_or_panic!(8.00),
Some(pos_or_panic!(12.50)),
"CBOE".to_string(),
UnderlyingAssetType::Stock,
None,
);
portfolio.add_balance(balance1);
portfolio.add_balance(balance2);
let total_value = portfolio.get_total_value();
assert_eq!(total_value, pos_or_panic!(132.5)); }
#[test]
fn test_portfolio_get_balance() {
let mut portfolio = Portfolio::new("Test Portfolio".to_string());
let balance = Balance::new(
"AAPL240315C00150000".to_string(),
pos_or_panic!(10.0),
pos_or_panic!(5.50),
Some(pos_or_panic!(7.00)),
"CBOE".to_string(),
UnderlyingAssetType::Stock,
None,
);
portfolio.add_balance(balance);
let found_balance = portfolio.get_balance("AAPL240315C00150000", "CBOE");
assert!(found_balance.is_some());
assert_eq!(found_balance.unwrap().symbol, "AAPL240315C00150000");
let not_found = portfolio.get_balance("TSLA240315C00200000", "CBOE");
assert!(not_found.is_none());
}
}