"""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)")