|
|
"""Tests for data model serialization and deserialization. |
|
|
|
|
|
This module tests the serialization and deserialization of data model classes, |
|
|
ensuring that objects can be converted to dictionaries and back without losing |
|
|
information. |
|
|
""" |
|
|
|
|
|
import unittest |
|
|
|
|
|
from src.folio.data_model import ( |
|
|
ExposureBreakdown, |
|
|
OptionPosition, |
|
|
PortfolioGroup, |
|
|
PortfolioSummary, |
|
|
StockPosition, |
|
|
) |
|
|
|
|
|
|
|
|
class TestDataModelSerialization(unittest.TestCase): |
|
|
"""Test serialization and deserialization of data model classes.""" |
|
|
|
|
|
def test_stock_position_serialization(self): |
|
|
"""Test that StockPosition objects can be serialized and deserialized.""" |
|
|
|
|
|
stock = StockPosition( |
|
|
ticker="AAPL", |
|
|
quantity=100, |
|
|
beta=1.2, |
|
|
market_exposure=15000.0, |
|
|
beta_adjusted_exposure=18000.0, |
|
|
price=150.0, |
|
|
cost_basis=140.0, |
|
|
) |
|
|
|
|
|
|
|
|
stock_dict = stock.to_dict() |
|
|
|
|
|
|
|
|
stock2 = StockPosition.from_dict(stock_dict) |
|
|
|
|
|
|
|
|
self.assertEqual(stock.ticker, stock2.ticker) |
|
|
self.assertEqual(stock.quantity, stock2.quantity) |
|
|
self.assertEqual(stock.beta, stock2.beta) |
|
|
self.assertEqual(stock.market_exposure, stock2.market_exposure) |
|
|
self.assertEqual(stock.beta_adjusted_exposure, stock2.beta_adjusted_exposure) |
|
|
self.assertEqual(stock.price, stock2.price) |
|
|
self.assertEqual(stock.cost_basis, stock2.cost_basis) |
|
|
self.assertEqual(stock.market_value, stock2.market_value) |
|
|
|
|
|
def test_stock_position_serialization_without_market_value(self): |
|
|
"""Test that StockPosition objects can be deserialized without market_value.""" |
|
|
|
|
|
stock_dict = { |
|
|
"ticker": "AAPL", |
|
|
"quantity": 100, |
|
|
"beta": 1.2, |
|
|
"market_exposure": 15000.0, |
|
|
"beta_adjusted_exposure": 18000.0, |
|
|
"price": 150.0, |
|
|
"position_type": "stock", |
|
|
"cost_basis": 140.0, |
|
|
} |
|
|
|
|
|
|
|
|
stock = StockPosition.from_dict(stock_dict) |
|
|
|
|
|
|
|
|
self.assertEqual( |
|
|
stock.market_value, stock_dict["price"] * stock_dict["quantity"] |
|
|
) |
|
|
|
|
|
def test_option_position_serialization(self): |
|
|
"""Test that OptionPosition objects can be serialized and deserialized.""" |
|
|
|
|
|
option = OptionPosition( |
|
|
ticker="AAPL", |
|
|
position_type="option", |
|
|
quantity=10, |
|
|
beta=1.2, |
|
|
beta_adjusted_exposure=18000.0, |
|
|
strike=150.0, |
|
|
expiry="2023-12-15", |
|
|
option_type="CALL", |
|
|
delta=0.7, |
|
|
delta_exposure=10500.0, |
|
|
notional_value=15000.0, |
|
|
underlying_beta=1.2, |
|
|
market_exposure=10500.0, |
|
|
price=15.0, |
|
|
cost_basis=14.0, |
|
|
) |
|
|
|
|
|
|
|
|
option_dict = option.to_dict() |
|
|
|
|
|
|
|
|
option2 = OptionPosition.from_dict(option_dict) |
|
|
|
|
|
|
|
|
self.assertEqual(option.ticker, option2.ticker) |
|
|
self.assertEqual(option.position_type, option2.position_type) |
|
|
self.assertEqual(option.quantity, option2.quantity) |
|
|
self.assertEqual(option.beta, option2.beta) |
|
|
self.assertEqual(option.beta_adjusted_exposure, option2.beta_adjusted_exposure) |
|
|
self.assertEqual(option.strike, option2.strike) |
|
|
self.assertEqual(option.expiry, option2.expiry) |
|
|
self.assertEqual(option.option_type, option2.option_type) |
|
|
self.assertEqual(option.delta, option2.delta) |
|
|
self.assertEqual(option.delta_exposure, option2.delta_exposure) |
|
|
self.assertEqual(option.notional_value, option2.notional_value) |
|
|
self.assertEqual(option.underlying_beta, option2.underlying_beta) |
|
|
self.assertEqual(option.market_exposure, option2.market_exposure) |
|
|
self.assertEqual(option.price, option2.price) |
|
|
self.assertEqual(option.cost_basis, option2.cost_basis) |
|
|
self.assertEqual(option.market_value, option2.market_value) |
|
|
|
|
|
def test_option_position_serialization_without_market_value(self): |
|
|
"""Test that OptionPosition objects can be deserialized without market_value.""" |
|
|
|
|
|
option_dict = { |
|
|
"ticker": "AAPL", |
|
|
"position_type": "option", |
|
|
"quantity": 10, |
|
|
"beta": 1.2, |
|
|
"beta_adjusted_exposure": 18000.0, |
|
|
"strike": 150.0, |
|
|
"expiry": "2023-12-15", |
|
|
"option_type": "CALL", |
|
|
"delta": 0.7, |
|
|
"delta_exposure": 10500.0, |
|
|
"notional_value": 15000.0, |
|
|
"underlying_beta": 1.2, |
|
|
"market_exposure": 10500.0, |
|
|
"price": 15.0, |
|
|
"cost_basis": 14.0, |
|
|
} |
|
|
|
|
|
|
|
|
option = OptionPosition.from_dict(option_dict) |
|
|
|
|
|
|
|
|
self.assertEqual( |
|
|
option.market_value, option_dict["price"] * option_dict["quantity"] * 100 |
|
|
) |
|
|
|
|
|
def test_portfolio_group_serialization(self): |
|
|
"""Test that PortfolioGroup objects can be serialized and deserialized.""" |
|
|
|
|
|
stock = StockPosition( |
|
|
ticker="AAPL", |
|
|
quantity=100, |
|
|
beta=1.2, |
|
|
market_exposure=15000.0, |
|
|
beta_adjusted_exposure=18000.0, |
|
|
price=150.0, |
|
|
cost_basis=140.0, |
|
|
) |
|
|
|
|
|
|
|
|
option = OptionPosition( |
|
|
ticker="AAPL", |
|
|
position_type="option", |
|
|
quantity=10, |
|
|
beta=1.2, |
|
|
beta_adjusted_exposure=18000.0, |
|
|
strike=150.0, |
|
|
expiry="2023-12-15", |
|
|
option_type="CALL", |
|
|
delta=0.7, |
|
|
delta_exposure=10500.0, |
|
|
notional_value=15000.0, |
|
|
underlying_beta=1.2, |
|
|
market_exposure=10500.0, |
|
|
price=15.0, |
|
|
cost_basis=14.0, |
|
|
) |
|
|
|
|
|
|
|
|
group = PortfolioGroup( |
|
|
ticker="AAPL", |
|
|
stock_position=stock, |
|
|
option_positions=[option], |
|
|
net_exposure=25500.0, |
|
|
beta=1.2, |
|
|
beta_adjusted_exposure=36000.0, |
|
|
total_delta_exposure=10500.0, |
|
|
options_delta_exposure=10500.0, |
|
|
) |
|
|
|
|
|
|
|
|
group_dict = group.to_dict() |
|
|
|
|
|
|
|
|
group2 = PortfolioGroup.from_dict(group_dict) |
|
|
|
|
|
|
|
|
self.assertEqual(group.ticker, group2.ticker) |
|
|
self.assertEqual(group.net_exposure, group2.net_exposure) |
|
|
self.assertEqual(group.beta, group2.beta) |
|
|
self.assertEqual(group.beta_adjusted_exposure, group2.beta_adjusted_exposure) |
|
|
self.assertEqual(group.total_delta_exposure, group2.total_delta_exposure) |
|
|
self.assertEqual(group.options_delta_exposure, group2.options_delta_exposure) |
|
|
|
|
|
|
|
|
self.assertEqual(group.stock_position.ticker, group2.stock_position.ticker) |
|
|
self.assertEqual(group.stock_position.quantity, group2.stock_position.quantity) |
|
|
self.assertEqual(group.stock_position.beta, group2.stock_position.beta) |
|
|
self.assertEqual( |
|
|
group.stock_position.market_exposure, group2.stock_position.market_exposure |
|
|
) |
|
|
self.assertEqual( |
|
|
group.stock_position.beta_adjusted_exposure, |
|
|
group2.stock_position.beta_adjusted_exposure, |
|
|
) |
|
|
self.assertEqual(group.stock_position.price, group2.stock_position.price) |
|
|
self.assertEqual( |
|
|
group.stock_position.cost_basis, group2.stock_position.cost_basis |
|
|
) |
|
|
self.assertEqual( |
|
|
group.stock_position.market_value, group2.stock_position.market_value |
|
|
) |
|
|
|
|
|
|
|
|
self.assertEqual( |
|
|
group.option_positions[0].ticker, group2.option_positions[0].ticker |
|
|
) |
|
|
self.assertEqual( |
|
|
group.option_positions[0].position_type, |
|
|
group2.option_positions[0].position_type, |
|
|
) |
|
|
self.assertEqual( |
|
|
group.option_positions[0].quantity, group2.option_positions[0].quantity |
|
|
) |
|
|
self.assertEqual( |
|
|
group.option_positions[0].beta, group2.option_positions[0].beta |
|
|
) |
|
|
self.assertEqual( |
|
|
group.option_positions[0].beta_adjusted_exposure, |
|
|
group2.option_positions[0].beta_adjusted_exposure, |
|
|
) |
|
|
self.assertEqual( |
|
|
group.option_positions[0].strike, group2.option_positions[0].strike |
|
|
) |
|
|
self.assertEqual( |
|
|
group.option_positions[0].expiry, group2.option_positions[0].expiry |
|
|
) |
|
|
self.assertEqual( |
|
|
group.option_positions[0].option_type, |
|
|
group2.option_positions[0].option_type, |
|
|
) |
|
|
self.assertEqual( |
|
|
group.option_positions[0].delta, group2.option_positions[0].delta |
|
|
) |
|
|
self.assertEqual( |
|
|
group.option_positions[0].delta_exposure, |
|
|
group2.option_positions[0].delta_exposure, |
|
|
) |
|
|
self.assertEqual( |
|
|
group.option_positions[0].notional_value, |
|
|
group2.option_positions[0].notional_value, |
|
|
) |
|
|
self.assertEqual( |
|
|
group.option_positions[0].underlying_beta, |
|
|
group2.option_positions[0].underlying_beta, |
|
|
) |
|
|
self.assertEqual( |
|
|
group.option_positions[0].market_exposure, |
|
|
group2.option_positions[0].market_exposure, |
|
|
) |
|
|
self.assertEqual( |
|
|
group.option_positions[0].price, group2.option_positions[0].price |
|
|
) |
|
|
self.assertEqual( |
|
|
group.option_positions[0].cost_basis, group2.option_positions[0].cost_basis |
|
|
) |
|
|
self.assertEqual( |
|
|
group.option_positions[0].market_value, |
|
|
group2.option_positions[0].market_value, |
|
|
) |
|
|
|
|
|
def test_portfolio_summary_serialization(self): |
|
|
"""Test that PortfolioSummary objects can be serialized and deserialized.""" |
|
|
|
|
|
long_exposure = ExposureBreakdown( |
|
|
stock_exposure=15000.0, |
|
|
stock_beta_adjusted=18000.0, |
|
|
option_delta_exposure=10500.0, |
|
|
option_beta_adjusted=12600.0, |
|
|
total_exposure=25500.0, |
|
|
total_beta_adjusted=30600.0, |
|
|
description="Long exposure", |
|
|
formula="Long stocks + Long calls + Short puts", |
|
|
components={ |
|
|
"Long stocks": 15000.0, |
|
|
"Long calls": 10500.0, |
|
|
"Short puts": 0.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
short_exposure = ExposureBreakdown( |
|
|
stock_exposure=-5000.0, |
|
|
stock_beta_adjusted=-6000.0, |
|
|
option_delta_exposure=-3500.0, |
|
|
option_beta_adjusted=-4200.0, |
|
|
total_exposure=-8500.0, |
|
|
total_beta_adjusted=-10200.0, |
|
|
description="Short exposure", |
|
|
formula="Short stocks + Short calls + Long puts", |
|
|
components={ |
|
|
"Short stocks": -5000.0, |
|
|
"Short calls": -3500.0, |
|
|
"Long puts": 0.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
options_exposure = ExposureBreakdown( |
|
|
stock_exposure=0.0, |
|
|
stock_beta_adjusted=0.0, |
|
|
option_delta_exposure=7000.0, |
|
|
option_beta_adjusted=8400.0, |
|
|
total_exposure=7000.0, |
|
|
total_beta_adjusted=8400.0, |
|
|
description="Options exposure", |
|
|
formula="Long calls + Short calls + Long puts + Short puts", |
|
|
components={ |
|
|
"Long calls": 10500.0, |
|
|
"Short calls": -3500.0, |
|
|
"Long puts": 0.0, |
|
|
"Short puts": 0.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
|
|
|
cash = StockPosition( |
|
|
ticker="CASH", |
|
|
quantity=1, |
|
|
beta=0.0, |
|
|
market_exposure=0.0, |
|
|
beta_adjusted_exposure=0.0, |
|
|
price=10000.0, |
|
|
cost_basis=10000.0, |
|
|
) |
|
|
|
|
|
|
|
|
summary = PortfolioSummary( |
|
|
net_market_exposure=17000.0, |
|
|
portfolio_beta=1.2, |
|
|
long_exposure=long_exposure, |
|
|
short_exposure=short_exposure, |
|
|
options_exposure=options_exposure, |
|
|
short_percentage=33.33, |
|
|
cash_like_positions=[cash], |
|
|
cash_like_value=10000.0, |
|
|
cash_like_count=1, |
|
|
cash_percentage=20.0, |
|
|
stock_value=20000.0, |
|
|
option_value=15000.0, |
|
|
pending_activity_value=5000.0, |
|
|
portfolio_estimate_value=50000.0, |
|
|
price_updated_at="2023-12-15T12:00:00Z", |
|
|
) |
|
|
|
|
|
|
|
|
summary_dict = summary.to_dict() |
|
|
|
|
|
|
|
|
summary2 = PortfolioSummary.from_dict(summary_dict) |
|
|
|
|
|
|
|
|
self.assertEqual(summary.net_market_exposure, summary2.net_market_exposure) |
|
|
self.assertEqual(summary.portfolio_beta, summary2.portfolio_beta) |
|
|
self.assertEqual(summary.short_percentage, summary2.short_percentage) |
|
|
self.assertEqual(summary.cash_like_value, summary2.cash_like_value) |
|
|
self.assertEqual(summary.cash_like_count, summary2.cash_like_count) |
|
|
self.assertEqual(summary.cash_percentage, summary2.cash_percentage) |
|
|
self.assertEqual(summary.stock_value, summary2.stock_value) |
|
|
self.assertEqual(summary.option_value, summary2.option_value) |
|
|
self.assertEqual( |
|
|
summary.pending_activity_value, summary2.pending_activity_value |
|
|
) |
|
|
self.assertEqual( |
|
|
summary.portfolio_estimate_value, summary2.portfolio_estimate_value |
|
|
) |
|
|
self.assertEqual(summary.price_updated_at, summary2.price_updated_at) |
|
|
|
|
|
|
|
|
self.assertEqual( |
|
|
summary.long_exposure.stock_exposure, summary2.long_exposure.stock_exposure |
|
|
) |
|
|
self.assertEqual( |
|
|
summary.long_exposure.stock_beta_adjusted, |
|
|
summary2.long_exposure.stock_beta_adjusted, |
|
|
) |
|
|
self.assertEqual( |
|
|
summary.long_exposure.option_delta_exposure, |
|
|
summary2.long_exposure.option_delta_exposure, |
|
|
) |
|
|
self.assertEqual( |
|
|
summary.long_exposure.option_beta_adjusted, |
|
|
summary2.long_exposure.option_beta_adjusted, |
|
|
) |
|
|
self.assertEqual( |
|
|
summary.long_exposure.total_exposure, summary2.long_exposure.total_exposure |
|
|
) |
|
|
self.assertEqual( |
|
|
summary.long_exposure.total_beta_adjusted, |
|
|
summary2.long_exposure.total_beta_adjusted, |
|
|
) |
|
|
|
|
|
def test_portfolio_summary_serialization_without_pending_activity(self): |
|
|
"""Test that PortfolioSummary objects can be deserialized without pending_activity_value.""" |
|
|
|
|
|
long_exposure = ExposureBreakdown( |
|
|
stock_exposure=15000.0, |
|
|
stock_beta_adjusted=18000.0, |
|
|
option_delta_exposure=10500.0, |
|
|
option_beta_adjusted=12600.0, |
|
|
total_exposure=25500.0, |
|
|
total_beta_adjusted=30600.0, |
|
|
description="Long exposure", |
|
|
formula="Long stocks + Long calls + Short puts", |
|
|
components={ |
|
|
"Long stocks": 15000.0, |
|
|
"Long calls": 10500.0, |
|
|
"Short puts": 0.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
short_exposure = ExposureBreakdown( |
|
|
stock_exposure=-5000.0, |
|
|
stock_beta_adjusted=-6000.0, |
|
|
option_delta_exposure=-3500.0, |
|
|
option_beta_adjusted=-4200.0, |
|
|
total_exposure=-8500.0, |
|
|
total_beta_adjusted=-10200.0, |
|
|
description="Short exposure", |
|
|
formula="Short stocks + Short calls + Long puts", |
|
|
components={ |
|
|
"Short stocks": -5000.0, |
|
|
"Short calls": -3500.0, |
|
|
"Long puts": 0.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
options_exposure = ExposureBreakdown( |
|
|
stock_exposure=0.0, |
|
|
stock_beta_adjusted=0.0, |
|
|
option_delta_exposure=7000.0, |
|
|
option_beta_adjusted=8400.0, |
|
|
total_exposure=7000.0, |
|
|
total_beta_adjusted=8400.0, |
|
|
description="Options exposure", |
|
|
formula="Long calls + Short calls + Long puts + Short puts", |
|
|
components={ |
|
|
"Long calls": 10500.0, |
|
|
"Short calls": -3500.0, |
|
|
"Long puts": 0.0, |
|
|
"Short puts": 0.0, |
|
|
}, |
|
|
) |
|
|
|
|
|
|
|
|
summary_dict = { |
|
|
"net_market_exposure": 17000.0, |
|
|
"portfolio_beta": 1.2, |
|
|
"long_exposure": long_exposure.to_dict(), |
|
|
"short_exposure": short_exposure.to_dict(), |
|
|
"options_exposure": options_exposure.to_dict(), |
|
|
"short_percentage": 33.33, |
|
|
"cash_like_positions": [], |
|
|
"cash_like_value": 10000.0, |
|
|
"cash_like_count": 1, |
|
|
"cash_percentage": 20.0, |
|
|
"stock_value": 20000.0, |
|
|
"option_value": 15000.0, |
|
|
"portfolio_estimate_value": 50000.0, |
|
|
"help_text": {}, |
|
|
"price_updated_at": "2023-12-15T12:00:00Z", |
|
|
} |
|
|
|
|
|
|
|
|
summary = PortfolioSummary.from_dict(summary_dict) |
|
|
|
|
|
|
|
|
self.assertEqual(summary.pending_activity_value, 0.0) |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
unittest.main() |
|
|
|