TradingAgents/tests/integration/api/test_trade_integration.py

1236 lines
46 KiB
Python

"""Integration tests for Trade model (Issue #6: DB-5).
Tests for Trade model integration with:
- Portfolio model relationships
- CGT calculation workflows (FIFO matching)
- Multi-currency trade scenarios
- Trade lifecycle management
- Complex query scenarios
- Tax year reporting
- Position tracking
Follows TDD principles - tests written BEFORE implementation.
"""
import pytest
from decimal import Decimal
from datetime import datetime, date, timedelta
from sqlalchemy import select, func, and_, or_
from sqlalchemy.exc import IntegrityError
# Mark all tests in this module as asyncio
pytestmark = pytest.mark.asyncio
class TestTradePortfolioIntegration:
"""Integration tests for Trade-Portfolio relationships."""
@pytest.mark.asyncio
async def test_create_trade_for_portfolio(self, db_session, test_portfolio):
"""Should create trade linked to portfolio."""
try:
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
trade = Trade(
portfolio_id=test_portfolio.id,
symbol="AAPL",
side=TradeSide.BUY,
quantity=Decimal("100"),
price=Decimal("150.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.utcnow(),
)
db_session.add(trade)
await db_session.commit()
await db_session.refresh(trade)
await db_session.refresh(test_portfolio, ["trades"])
# Verify both sides of relationship
assert trade.portfolio_id == test_portfolio.id
assert trade in test_portfolio.trades
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
@pytest.mark.asyncio
async def test_portfolio_with_multiple_trades(self, db_session, test_portfolio):
"""Should allow portfolio to have multiple trades."""
try:
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
trades_data = [
("AAPL", Decimal("100"), Decimal("150.00")),
("TSLA", Decimal("50"), Decimal("200.00")),
("GOOGL", Decimal("25"), Decimal("120.00")),
]
for symbol, quantity, price in trades_data:
trade = Trade(
portfolio_id=test_portfolio.id,
symbol=symbol,
side=TradeSide.BUY,
quantity=quantity,
price=price,
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.utcnow(),
)
db_session.add(trade)
await db_session.commit()
# Refresh and verify
await db_session.refresh(test_portfolio, ["trades"])
assert len(test_portfolio.trades) == 3
symbols = {t.symbol for t in test_portfolio.trades}
assert "AAPL" in symbols
assert "TSLA" in symbols
assert "GOOGL" in symbols
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
@pytest.mark.asyncio
async def test_trades_deleted_with_portfolio(self, db_session, test_user):
"""Should cascade delete trades when portfolio is deleted."""
try:
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
# Create a temporary portfolio
portfolio = Portfolio(
user_id=test_user.id,
name="Temp Portfolio",
portfolio_type=PortfolioType.PAPER,
initial_capital=Decimal("10000.00"),
)
db_session.add(portfolio)
await db_session.commit()
await db_session.refresh(portfolio)
# Create trades for portfolio
trade1 = Trade(
portfolio_id=portfolio.id,
symbol="AAPL",
side=TradeSide.BUY,
quantity=Decimal("100"),
price=Decimal("150.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.utcnow(),
)
trade2 = Trade(
portfolio_id=portfolio.id,
symbol="TSLA",
side=TradeSide.BUY,
quantity=Decimal("50"),
price=Decimal("200.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.utcnow(),
)
db_session.add_all([trade1, trade2])
await db_session.commit()
trade1_id = trade1.id
trade2_id = trade2.id
# Delete the portfolio
await db_session.delete(portfolio)
await db_session.commit()
# Verify trades are deleted
result = await db_session.execute(
select(Trade).where(
Trade.id.in_([trade1_id, trade2_id])
)
)
remaining = result.scalars().all()
assert len(remaining) == 0
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
@pytest.mark.asyncio
async def test_multiple_portfolios_separate_trades(self, db_session, test_user, another_user):
"""Should isolate trades between different portfolios."""
try:
from tradingagents.api.models.portfolio import Portfolio, PortfolioType
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
# Create portfolios for different users
portfolio1 = Portfolio(
user_id=test_user.id,
name="User 1 Portfolio",
portfolio_type=PortfolioType.PAPER,
initial_capital=Decimal("10000.00"),
)
portfolio2 = Portfolio(
user_id=another_user.id,
name="User 2 Portfolio",
portfolio_type=PortfolioType.PAPER,
initial_capital=Decimal("10000.00"),
)
db_session.add_all([portfolio1, portfolio2])
await db_session.commit()
# Create trades for each portfolio
trade1 = Trade(
portfolio_id=portfolio1.id,
symbol="AAPL",
side=TradeSide.BUY,
quantity=Decimal("100"),
price=Decimal("150.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.utcnow(),
)
trade2 = Trade(
portfolio_id=portfolio2.id,
symbol="AAPL",
side=TradeSide.BUY,
quantity=Decimal("200"),
price=Decimal("155.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.utcnow(),
)
db_session.add_all([trade1, trade2])
await db_session.commit()
# Query trades for each portfolio
result1 = await db_session.execute(
select(Trade).where(Trade.portfolio_id == portfolio1.id)
)
trades1 = result1.scalars().all()
result2 = await db_session.execute(
select(Trade).where(Trade.portfolio_id == portfolio2.id)
)
trades2 = result2.scalars().all()
assert len(trades1) == 1
assert len(trades2) == 1
assert trades1[0].quantity == Decimal("100")
assert trades2[0].quantity == Decimal("200")
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
class TestTradeCGTEndToEnd:
"""Integration tests for full CGT calculation lifecycle."""
@pytest.mark.asyncio
async def test_simple_buy_sell_cgt_workflow(self, db_session, test_portfolio):
"""Should calculate CGT for simple buy-then-sell scenario."""
try:
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
# Buy 100 shares @ $40
acquisition = date(2023, 1, 15)
buy_trade = Trade(
portfolio_id=test_portfolio.id,
symbol="AAPL",
side=TradeSide.BUY,
quantity=Decimal("100"),
price=Decimal("40.00"),
total_value=Decimal("4000.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.combine(acquisition, datetime.min.time()),
acquisition_date=acquisition,
cost_basis_per_unit=Decimal("40.00"),
cost_basis_total=Decimal("4000.00"),
)
db_session.add(buy_trade)
await db_session.commit()
# Sell 100 shares @ $50 (200 days later, no CGT discount)
sell_date = date(2023, 8, 3)
holding_days = (sell_date - acquisition).days
sell_trade = Trade(
portfolio_id=test_portfolio.id,
symbol="AAPL",
side=TradeSide.SELL,
quantity=Decimal("100"),
price=Decimal("50.00"),
total_value=Decimal("5000.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.combine(sell_date, datetime.min.time()),
acquisition_date=acquisition,
cost_basis_per_unit=Decimal("40.00"),
cost_basis_total=Decimal("4000.00"),
holding_period_days=holding_days,
cgt_discount_eligible=False, # < 367 days
cgt_gross_gain=Decimal("1000.00"), # 100 * ($50 - $40)
cgt_gross_loss=Decimal("0.00"),
cgt_net_gain=Decimal("1000.00"), # No discount
)
db_session.add(sell_trade)
await db_session.commit()
# Verify CGT calculation
await db_session.refresh(sell_trade)
assert sell_trade.holding_period_days == holding_days
assert sell_trade.cgt_discount_eligible is False
assert sell_trade.cgt_gross_gain == Decimal("1000.00")
assert sell_trade.cgt_net_gain == Decimal("1000.00")
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
@pytest.mark.asyncio
async def test_long_term_hold_cgt_discount(self, db_session, test_portfolio):
"""Should apply 50% CGT discount for >367 day hold."""
try:
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
# Buy 100 shares @ $40
acquisition = date(2022, 1, 1)
buy_trade = Trade(
portfolio_id=test_portfolio.id,
symbol="BHP.AX",
side=TradeSide.BUY,
quantity=Decimal("100"),
price=Decimal("40.00"),
total_value=Decimal("4000.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.combine(acquisition, datetime.min.time()),
acquisition_date=acquisition,
cost_basis_per_unit=Decimal("40.00"),
cost_basis_total=Decimal("4000.00"),
)
db_session.add(buy_trade)
await db_session.commit()
# Sell 100 shares @ $50 (400 days later, eligible for discount)
sell_date = date(2023, 2, 5)
holding_days = (sell_date - acquisition).days
sell_trade = Trade(
portfolio_id=test_portfolio.id,
symbol="BHP.AX",
side=TradeSide.SELL,
quantity=Decimal("100"),
price=Decimal("50.00"),
total_value=Decimal("5000.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.combine(sell_date, datetime.min.time()),
acquisition_date=acquisition,
cost_basis_per_unit=Decimal("40.00"),
cost_basis_total=Decimal("4000.00"),
holding_period_days=holding_days,
cgt_discount_eligible=True, # >= 367 days
cgt_gross_gain=Decimal("1000.00"), # 100 * ($50 - $40)
cgt_gross_loss=Decimal("0.00"),
cgt_net_gain=Decimal("500.00"), # 50% discount applied
)
db_session.add(sell_trade)
await db_session.commit()
# Verify CGT discount applied
await db_session.refresh(sell_trade)
assert sell_trade.holding_period_days >= 367
assert sell_trade.cgt_discount_eligible is True
assert sell_trade.cgt_gross_gain == Decimal("1000.00")
assert sell_trade.cgt_net_gain == Decimal("500.00")
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
@pytest.mark.asyncio
async def test_capital_loss_scenario(self, db_session, test_portfolio):
"""Should calculate capital loss correctly."""
try:
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
# Buy 100 shares @ $50
acquisition = date(2023, 3, 1)
buy_trade = Trade(
portfolio_id=test_portfolio.id,
symbol="TSLA",
side=TradeSide.BUY,
quantity=Decimal("100"),
price=Decimal("50.00"),
total_value=Decimal("5000.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.combine(acquisition, datetime.min.time()),
acquisition_date=acquisition,
cost_basis_per_unit=Decimal("50.00"),
cost_basis_total=Decimal("5000.00"),
)
db_session.add(buy_trade)
await db_session.commit()
# Sell 100 shares @ $30 (100 days later, loss)
sell_date = date(2023, 6, 9)
holding_days = (sell_date - acquisition).days
sell_trade = Trade(
portfolio_id=test_portfolio.id,
symbol="TSLA",
side=TradeSide.SELL,
quantity=Decimal("100"),
price=Decimal("30.00"),
total_value=Decimal("3000.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.combine(sell_date, datetime.min.time()),
acquisition_date=acquisition,
cost_basis_per_unit=Decimal("50.00"),
cost_basis_total=Decimal("5000.00"),
holding_period_days=holding_days,
cgt_discount_eligible=False,
cgt_gross_gain=Decimal("0.00"),
cgt_gross_loss=Decimal("2000.00"), # 100 * ($50 - $30)
cgt_net_gain=Decimal("-2000.00"),
)
db_session.add(sell_trade)
await db_session.commit()
# Verify capital loss
await db_session.refresh(sell_trade)
assert sell_trade.cgt_gross_gain == Decimal("0.00")
assert sell_trade.cgt_gross_loss == Decimal("2000.00")
assert sell_trade.cgt_net_gain == Decimal("-2000.00")
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
class TestTradeFIFOMatching:
"""Integration tests for FIFO parcel matching."""
@pytest.mark.asyncio
async def test_fifo_single_parcel_full_sale(self, db_session, test_portfolio):
"""Should match sell to single buy parcel (FIFO)."""
try:
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
# Single buy
buy_date = date(2023, 1, 1)
buy = Trade(
portfolio_id=test_portfolio.id,
symbol="AAPL",
side=TradeSide.BUY,
quantity=Decimal("100"),
price=Decimal("100.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.combine(buy_date, datetime.min.time()),
acquisition_date=buy_date,
cost_basis_per_unit=Decimal("100.00"),
cost_basis_total=Decimal("10000.00"),
)
db_session.add(buy)
await db_session.commit()
# Sell entire position
sell_date = date(2023, 6, 1)
sell = Trade(
portfolio_id=test_portfolio.id,
symbol="AAPL",
side=TradeSide.SELL,
quantity=Decimal("100"),
price=Decimal("120.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.combine(sell_date, datetime.min.time()),
acquisition_date=buy_date, # Matched to first buy
cost_basis_per_unit=Decimal("100.00"),
cost_basis_total=Decimal("10000.00"),
holding_period_days=(sell_date - buy_date).days,
cgt_gross_gain=Decimal("2000.00"),
cgt_net_gain=Decimal("2000.00"),
)
db_session.add(sell)
await db_session.commit()
# Verify FIFO matching
await db_session.refresh(sell)
assert sell.acquisition_date == buy_date
assert sell.cost_basis_per_unit == Decimal("100.00")
assert sell.cgt_gross_gain == Decimal("2000.00")
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
@pytest.mark.asyncio
async def test_fifo_multiple_parcels_oldest_first(self, db_session, test_portfolio):
"""Should match sell to oldest parcel first (FIFO)."""
try:
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
# First buy (oldest)
buy1_date = date(2023, 1, 1)
buy1 = Trade(
portfolio_id=test_portfolio.id,
symbol="BHP.AX",
side=TradeSide.BUY,
quantity=Decimal("50"),
price=Decimal("40.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.combine(buy1_date, datetime.min.time()),
acquisition_date=buy1_date,
cost_basis_per_unit=Decimal("40.00"),
cost_basis_total=Decimal("2000.00"),
)
# Second buy (newer)
buy2_date = date(2023, 3, 1)
buy2 = Trade(
portfolio_id=test_portfolio.id,
symbol="BHP.AX",
side=TradeSide.BUY,
quantity=Decimal("50"),
price=Decimal("45.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.combine(buy2_date, datetime.min.time()),
acquisition_date=buy2_date,
cost_basis_per_unit=Decimal("45.00"),
cost_basis_total=Decimal("2250.00"),
)
db_session.add_all([buy1, buy2])
await db_session.commit()
# Sell 50 shares - should match to buy1 (FIFO)
sell_date = date(2023, 6, 1)
sell = Trade(
portfolio_id=test_portfolio.id,
symbol="BHP.AX",
side=TradeSide.SELL,
quantity=Decimal("50"),
price=Decimal("50.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.combine(sell_date, datetime.min.time()),
acquisition_date=buy1_date, # Matched to oldest buy
cost_basis_per_unit=Decimal("40.00"), # From buy1
cost_basis_total=Decimal("2000.00"),
holding_period_days=(sell_date - buy1_date).days,
cgt_gross_gain=Decimal("500.00"), # 50 * ($50 - $40)
cgt_net_gain=Decimal("500.00"),
)
db_session.add(sell)
await db_session.commit()
# Verify matched to oldest parcel
await db_session.refresh(sell)
assert sell.acquisition_date == buy1_date
assert sell.cost_basis_per_unit == Decimal("40.00")
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
@pytest.mark.asyncio
async def test_fifo_partial_parcel_matching(self, db_session, test_portfolio):
"""Should handle partial parcel matching across multiple buys."""
try:
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
# Buy 100 @ $40
buy1_date = date(2023, 1, 1)
buy1 = Trade(
portfolio_id=test_portfolio.id,
symbol="CBA.AX",
side=TradeSide.BUY,
quantity=Decimal("100"),
price=Decimal("40.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.combine(buy1_date, datetime.min.time()),
acquisition_date=buy1_date,
cost_basis_per_unit=Decimal("40.00"),
cost_basis_total=Decimal("4000.00"),
)
# Buy 100 @ $45
buy2_date = date(2023, 2, 1)
buy2 = Trade(
portfolio_id=test_portfolio.id,
symbol="CBA.AX",
side=TradeSide.BUY,
quantity=Decimal("100"),
price=Decimal("45.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.combine(buy2_date, datetime.min.time()),
acquisition_date=buy2_date,
cost_basis_per_unit=Decimal("45.00"),
cost_basis_total=Decimal("4500.00"),
)
db_session.add_all([buy1, buy2])
await db_session.commit()
# Sell 150 @ $50 - should consume all of buy1 + 50 from buy2
sell_date = date(2023, 6, 1)
# In real implementation, this might be split into 2 trade records
# For this test, we'll use weighted average cost basis
# 100 @ $40 + 50 @ $45 = $6250 / 150 = $41.67 avg
sell = Trade(
portfolio_id=test_portfolio.id,
symbol="CBA.AX",
side=TradeSide.SELL,
quantity=Decimal("150"),
price=Decimal("50.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.combine(sell_date, datetime.min.time()),
acquisition_date=buy1_date, # Earliest acquisition
cost_basis_per_unit=Decimal("41.67"), # Weighted average
cost_basis_total=Decimal("6250.50"),
holding_period_days=(sell_date - buy1_date).days,
cgt_gross_gain=Decimal("1249.50"), # 150*$50 - $6250.50
cgt_net_gain=Decimal("1249.50"),
)
db_session.add(sell)
await db_session.commit()
# Verify FIFO matching
await db_session.refresh(sell)
assert sell.quantity == Decimal("150")
assert sell.acquisition_date == buy1_date
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
class TestTradeMultiCurrency:
"""Integration tests for multi-currency trade scenarios."""
@pytest.mark.asyncio
async def test_foreign_stock_with_fx_conversion(self, db_session, test_portfolio):
"""Should handle foreign stock trades with FX conversion."""
try:
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
# Buy US stock in USD
trade = Trade(
portfolio_id=test_portfolio.id,
symbol="AAPL",
side=TradeSide.BUY,
quantity=Decimal("100"),
price=Decimal("150.00"), # USD
total_value=Decimal("15000.00"), # USD
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.utcnow(),
currency="USD",
fx_rate_to_aud=Decimal("1.50"), # 1 USD = 1.50 AUD
total_value_aud=Decimal("22500.00"), # AUD equivalent
)
db_session.add(trade)
await db_session.commit()
await db_session.refresh(trade)
# Verify currency conversion
assert trade.currency == "USD"
assert trade.total_value == Decimal("15000.00")
assert trade.total_value_aud == Decimal("22500.00")
assert trade.fx_rate_to_aud == Decimal("1.50")
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
@pytest.mark.asyncio
async def test_fx_gain_loss_in_cgt_calculation(self, db_session, test_portfolio):
"""Should account for FX gains/losses in CGT."""
try:
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
# Buy @ 1.50 FX rate
buy_date = date(2023, 1, 1)
buy = Trade(
portfolio_id=test_portfolio.id,
symbol="AAPL",
side=TradeSide.BUY,
quantity=Decimal("100"),
price=Decimal("100.00"), # USD
total_value=Decimal("10000.00"), # USD
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.combine(buy_date, datetime.min.time()),
currency="USD",
fx_rate_to_aud=Decimal("1.50"),
total_value_aud=Decimal("15000.00"), # AUD
acquisition_date=buy_date,
cost_basis_per_unit=Decimal("150.00"), # AUD per share
cost_basis_total=Decimal("15000.00"), # AUD
)
db_session.add(buy)
await db_session.commit()
# Sell @ 1.40 FX rate (AUD strengthened)
sell_date = date(2023, 6, 1)
sell = Trade(
portfolio_id=test_portfolio.id,
symbol="AAPL",
side=TradeSide.SELL,
quantity=Decimal("100"),
price=Decimal("110.00"), # USD
total_value=Decimal("11000.00"), # USD
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.combine(sell_date, datetime.min.time()),
currency="USD",
fx_rate_to_aud=Decimal("1.40"),
total_value_aud=Decimal("15400.00"), # AUD
acquisition_date=buy_date,
cost_basis_per_unit=Decimal("150.00"), # AUD
cost_basis_total=Decimal("15000.00"), # AUD
holding_period_days=(sell_date - buy_date).days,
# Small gain in AUD despite USD price increase due to FX
cgt_gross_gain=Decimal("400.00"),
cgt_net_gain=Decimal("400.00"),
)
db_session.add(sell)
await db_session.commit()
# Verify FX impact on CGT
await db_session.refresh(sell)
assert sell.total_value_aud == Decimal("15400.00")
assert sell.cgt_gross_gain == Decimal("400.00")
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
@pytest.mark.asyncio
async def test_mixed_currency_portfolio(self, db_session, test_portfolio):
"""Should support mixed currency trades in same portfolio."""
try:
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
# AUD trade
aud_trade = Trade(
portfolio_id=test_portfolio.id,
symbol="BHP.AX",
side=TradeSide.BUY,
quantity=Decimal("100"),
price=Decimal("45.00"),
total_value=Decimal("4500.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.utcnow(),
currency="AUD",
fx_rate_to_aud=Decimal("1.0"),
total_value_aud=Decimal("4500.00"),
)
# USD trade
usd_trade = Trade(
portfolio_id=test_portfolio.id,
symbol="AAPL",
side=TradeSide.BUY,
quantity=Decimal("100"),
price=Decimal("150.00"),
total_value=Decimal("15000.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.utcnow(),
currency="USD",
fx_rate_to_aud=Decimal("1.50"),
total_value_aud=Decimal("22500.00"),
)
db_session.add_all([aud_trade, usd_trade])
await db_session.commit()
# Calculate total portfolio value in AUD
result = await db_session.execute(
select(func.sum(Trade.total_value_aud)).where(
and_(
Trade.portfolio_id == test_portfolio.id,
Trade.side == TradeSide.BUY,
Trade.status == TradeStatus.FILLED
)
)
)
total_aud = result.scalar()
assert total_aud == Decimal("27000.00")
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
class TestTradeComplexQueries:
"""Integration tests for complex trade queries."""
@pytest.mark.asyncio
async def test_aggregate_position_by_symbol(self, db_session, test_portfolio):
"""Should calculate current position for a symbol."""
try:
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
# Multiple buys and sells for AAPL
trades = [
(TradeSide.BUY, Decimal("100")),
(TradeSide.BUY, Decimal("50")),
(TradeSide.SELL, Decimal("30")),
(TradeSide.BUY, Decimal("20")),
]
for side, quantity in trades:
trade = Trade(
portfolio_id=test_portfolio.id,
symbol="AAPL",
side=side,
quantity=quantity,
price=Decimal("150.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.utcnow(),
)
db_session.add(trade)
await db_session.commit()
# Calculate net position (buys - sells) using case() from sqlalchemy
from sqlalchemy import case
result = await db_session.execute(
select(
func.sum(
case(
(Trade.side == TradeSide.BUY, Trade.quantity),
else_=-Trade.quantity
)
)
).where(
and_(
Trade.portfolio_id == test_portfolio.id,
Trade.symbol == "AAPL",
Trade.status == TradeStatus.FILLED
)
)
)
net_position = result.scalar()
# 100 + 50 - 30 + 20 = 140
assert net_position == Decimal("140")
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
@pytest.mark.asyncio
async def test_query_trades_by_tax_year(self, db_session, test_portfolio):
"""Should filter trades by Australian tax year."""
try:
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
# FY2024 trades (July 2023 - June 2024)
fy2024_dates = [
datetime(2023, 7, 1),
datetime(2023, 12, 15),
datetime(2024, 6, 30),
]
# FY2025 trade
fy2025_date = datetime(2024, 7, 1)
for exec_date in fy2024_dates:
trade = Trade(
portfolio_id=test_portfolio.id,
symbol="AAPL",
side=TradeSide.BUY,
quantity=Decimal("100"),
price=Decimal("150.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=exec_date,
)
db_session.add(trade)
trade_fy2025 = Trade(
portfolio_id=test_portfolio.id,
symbol="TSLA",
side=TradeSide.BUY,
quantity=Decimal("50"),
price=Decimal("200.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=fy2025_date,
)
db_session.add(trade_fy2025)
await db_session.commit()
# Query FY2024 trades using tax_year property
# Note: This requires the model to have a hybrid_property or similar
# For now, we'll query by date range
fy2024_start = datetime(2023, 7, 1)
fy2024_end = datetime(2024, 6, 30, 23, 59, 59)
result = await db_session.execute(
select(Trade).where(
and_(
Trade.portfolio_id == test_portfolio.id,
Trade.executed_at >= fy2024_start,
Trade.executed_at <= fy2024_end
)
)
)
fy2024_trades = result.scalars().all()
assert len(fy2024_trades) == 3
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
@pytest.mark.asyncio
async def test_query_cgt_eligible_for_discount(self, db_session, test_portfolio):
"""Should filter trades eligible for CGT discount."""
try:
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
# Short hold (no discount)
short_hold = Trade(
portfolio_id=test_portfolio.id,
symbol="AAPL",
side=TradeSide.SELL,
quantity=Decimal("100"),
price=Decimal("150.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.utcnow(),
holding_period_days=200,
cgt_discount_eligible=False,
)
# Long hold (eligible for discount)
long_hold = Trade(
portfolio_id=test_portfolio.id,
symbol="BHP.AX",
side=TradeSide.SELL,
quantity=Decimal("100"),
price=Decimal("45.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.utcnow(),
holding_period_days=400,
cgt_discount_eligible=True,
)
db_session.add_all([short_hold, long_hold])
await db_session.commit()
# Query eligible trades
result = await db_session.execute(
select(Trade).where(
and_(
Trade.portfolio_id == test_portfolio.id,
Trade.cgt_discount_eligible == True
)
)
)
eligible_trades = result.scalars().all()
assert len(eligible_trades) == 1
assert eligible_trades[0].symbol == "BHP.AX"
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
@pytest.mark.asyncio
async def test_calculate_total_cgt_for_year(self, db_session, test_portfolio):
"""Should calculate total CGT for tax year."""
try:
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
# Multiple sales with CGT
trades = [
(Decimal("1000.00"), Decimal("500.00")), # Gain with discount
(Decimal("500.00"), Decimal("500.00")), # Gain no discount
(Decimal("0.00"), Decimal("-300.00")), # Loss
]
for gross_gain, net_gain in trades:
trade = Trade(
portfolio_id=test_portfolio.id,
symbol="AAPL",
side=TradeSide.SELL,
quantity=Decimal("100"),
price=Decimal("150.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime(2024, 3, 15),
cgt_gross_gain=gross_gain,
cgt_net_gain=net_gain,
)
db_session.add(trade)
await db_session.commit()
# Calculate total net CGT
result = await db_session.execute(
select(func.sum(Trade.cgt_net_gain)).where(
and_(
Trade.portfolio_id == test_portfolio.id,
Trade.side == TradeSide.SELL,
Trade.status == TradeStatus.FILLED
)
)
)
total_cgt = result.scalar()
# $500 + $500 - $300 = $700
assert total_cgt == Decimal("700.00")
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
class TestTradeLifecycle:
"""Integration tests for trade lifecycle management."""
@pytest.mark.asyncio
async def test_trade_status_progression(self, db_session, test_portfolio):
"""Should support trade status transitions."""
try:
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
# Create pending order
trade = Trade(
portfolio_id=test_portfolio.id,
symbol="AAPL",
side=TradeSide.BUY,
quantity=Decimal("100"),
price=Decimal("150.00"),
order_type=TradeOrderType.LIMIT,
status=TradeStatus.PENDING,
executed_at=None,
)
db_session.add(trade)
await db_session.commit()
await db_session.refresh(trade)
assert trade.status == TradeStatus.PENDING
# Partially fill
trade.status = TradeStatus.PARTIAL
await db_session.commit()
await db_session.refresh(trade)
assert trade.status == TradeStatus.PARTIAL
# Complete fill
trade.status = TradeStatus.FILLED
trade.executed_at = datetime.utcnow()
await db_session.commit()
await db_session.refresh(trade)
assert trade.status == TradeStatus.FILLED
assert trade.executed_at is not None
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
@pytest.mark.asyncio
async def test_cancel_pending_order(self, db_session, test_portfolio):
"""Should support cancelling pending orders."""
try:
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
trade = Trade(
portfolio_id=test_portfolio.id,
symbol="TSLA",
side=TradeSide.BUY,
quantity=Decimal("50"),
price=Decimal("200.00"),
order_type=TradeOrderType.LIMIT,
status=TradeStatus.PENDING,
executed_at=None,
)
db_session.add(trade)
await db_session.commit()
await db_session.refresh(trade)
# Cancel order
trade.status = TradeStatus.CANCELLED
await db_session.commit()
await db_session.refresh(trade)
assert trade.status == TradeStatus.CANCELLED
assert trade.executed_at is None
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
@pytest.mark.asyncio
async def test_reject_invalid_order(self, db_session, test_portfolio):
"""Should support rejecting invalid orders."""
try:
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
trade = Trade(
portfolio_id=test_portfolio.id,
symbol="INVALID",
side=TradeSide.BUY,
quantity=Decimal("100"),
price=Decimal("0.01"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.REJECTED,
executed_at=None,
)
db_session.add(trade)
await db_session.commit()
await db_session.refresh(trade)
assert trade.status == TradeStatus.REJECTED
assert trade.executed_at is None
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
class TestTradeReporting:
"""Integration tests for trade reporting scenarios."""
@pytest.mark.asyncio
async def test_portfolio_performance_report(self, db_session, test_portfolio):
"""Should generate portfolio performance metrics."""
try:
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
# Create trades with gains and losses
trades = [
(TradeSide.BUY, Decimal("100"), Decimal("40.00"), None, None),
(TradeSide.SELL, Decimal("100"), Decimal("50.00"), Decimal("1000.00"), Decimal("1000.00")),
(TradeSide.BUY, Decimal("50"), Decimal("60.00"), None, None),
(TradeSide.SELL, Decimal("50"), Decimal("55.00"), Decimal("0.00"), Decimal("-250.00")),
]
for side, quantity, price, gross_gain, net_gain in trades:
trade = Trade(
portfolio_id=test_portfolio.id,
symbol="AAPL",
side=side,
quantity=quantity,
price=price,
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=datetime.utcnow(),
cgt_gross_gain=gross_gain or Decimal("0.00"),
cgt_net_gain=net_gain or Decimal("0.00"),
)
db_session.add(trade)
await db_session.commit()
# Calculate metrics
result = await db_session.execute(
select(
func.sum(Trade.cgt_gross_gain),
func.sum(Trade.cgt_net_gain),
func.count(Trade.id)
).where(
and_(
Trade.portfolio_id == test_portfolio.id,
Trade.side == TradeSide.SELL
)
)
)
gross_total, net_total, sell_count = result.one()
assert gross_total == Decimal("1000.00")
assert net_total == Decimal("750.00") # 1000 - 250
assert sell_count == 2
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")
@pytest.mark.asyncio
async def test_symbol_trading_history(self, db_session, test_portfolio):
"""Should retrieve complete trading history for a symbol."""
try:
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
# Create trading history for AAPL
trade_dates = [
datetime(2023, 1, 15),
datetime(2023, 3, 20),
datetime(2023, 6, 10),
datetime(2023, 9, 5),
]
for i, exec_date in enumerate(trade_dates):
trade = Trade(
portfolio_id=test_portfolio.id,
symbol="AAPL",
side=TradeSide.BUY if i % 2 == 0 else TradeSide.SELL,
quantity=Decimal("100"),
price=Decimal(f"{140 + i*10}.00"),
order_type=TradeOrderType.MARKET,
status=TradeStatus.FILLED,
executed_at=exec_date,
)
db_session.add(trade)
await db_session.commit()
# Query trading history ordered by date
result = await db_session.execute(
select(Trade)
.where(
and_(
Trade.portfolio_id == test_portfolio.id,
Trade.symbol == "AAPL"
)
)
.order_by(Trade.executed_at.asc())
)
history = result.scalars().all()
assert len(history) == 4
assert history[0].executed_at == trade_dates[0]
assert history[-1].executed_at == trade_dates[-1]
except ImportError:
pytest.skip("Trade model not yet implemented (TDD RED phase)")