# Trade Model Test Summary (Issue #6: DB-5) ## Overview Comprehensive test suite for Trade model covering: - Basic trade fields and enums - CGT (Capital Gains Tax) calculations - Multi-currency support - Tax year handling (Australian FY) - FIFO parcel matching - Trade lifecycle management ## Test Files ### Unit Tests: `tests/unit/api/test_trade_model.py` **65 tests** covering: #### TestTradeBasicFields (4 tests) - Create trade with required fields - Default values (currency=AUD, fx_rate=1.0) - All fields specified - Timestamp auto-population #### TestTradeSideEnum (3 tests) - BUY side - SELL side - Invalid side rejection #### TestTradeStatusEnum (5 tests) - PENDING status - FILLED status - PARTIAL status - CANCELLED status - REJECTED status #### TestTradeOrderTypeEnum (4 tests) - MARKET order type - LIMIT order type - STOP order type - STOP_LIMIT order type #### TestTradeDecimalPrecision (7 tests) - Quantity: Decimal(19,8) - supports crypto - Price: Decimal(19,4) - Total value: Decimal(19,4) - CGT fields: Decimal(19,4) - FX rate: Decimal(12,6) - Signal confidence: Decimal(5,2) - range 0-100 #### TestTradeTaxYear (5 tests) - FY2024 start (July 1, 2023) - FY2024 end (June 30, 2024) - FY2025 start (July 1, 2024) - Before FY transition (June) - After FY transition (July) #### TestTradeCGTDiscount (4 tests) - Not eligible: <367 days - Eligible: exactly 367 days - Eligible: >367 days - Boundary: 366 days (not eligible) #### TestTradeCGTCalculations (4 tests) - Gross gain calculation - Gross loss calculation - Net gain with 50% discount - Breakeven (no gain/loss) #### TestTradeCurrencySupport (4 tests) - Default AUD currency - USD with FX rate conversion - Common currency codes - Currency uppercase enforcement #### TestTradeConstraints (7 tests) - Quantity must be > 0 - Quantity cannot be zero - Price must be > 0 - Price cannot be zero - Signal confidence: 0-100 range - Signal confidence: >100 rejected - Signal confidence: negative rejected #### TestTradeSignalFields (3 tests) - Signal source stored - Signal confidence stored - Signal fields optional #### TestTradeProperties (4 tests) - is_buy property (True for BUY) - is_sell property (True for SELL) - is_filled property (True for FILLED) - is_filled False for PENDING #### TestTradePortfolioRelationship (3 tests) - Trade belongs to portfolio - Portfolio has many trades - Cascade delete with portfolio #### TestTradeEdgeCases (6 tests) - Very long symbol names - Fractional shares (0.5) - Very small quantities (crypto satoshis) - Very large quantities (millions) - Trade repr() #### TestTradeQueryOperations (4 tests) - Query by ID - Filter by symbol - Filter by side (BUY/SELL) - Filter by status ### Integration Tests: `tests/integration/api/test_trade_integration.py` **22 tests** covering: #### TestTradePortfolioIntegration (4 tests) - Create trade for portfolio - Portfolio with multiple trades - Cascade delete trades - Multiple portfolios isolation #### TestTradeCGTEndToEnd (3 tests) - Simple buy-sell workflow - Long-term hold with CGT discount - Capital loss scenario #### TestTradeFIFOMatching (3 tests) - Single parcel full sale - Multiple parcels - oldest first - Partial parcel matching across buys #### TestTradeMultiCurrency (3 tests) - Foreign stock with FX conversion - FX gain/loss in CGT calculation - Mixed currency portfolio #### TestTradeComplexQueries (5 tests) - Aggregate position by symbol - Query by tax year - CGT discount eligibility filter - Total CGT for year - Order by date/value #### TestTradeLifecycle (3 tests) - Status progression (PENDING→PARTIAL→FILLED) - Cancel pending order - Reject invalid order #### TestTradeReporting (2 tests) - Portfolio performance metrics - Symbol trading history ## Test Statistics - **Total Tests**: 87 (65 unit + 22 integration) - **All Tests**: SKIPPED (RED phase - implementation pending) - **Expected Coverage**: 80%+ when implemented ## Key Test Patterns ### 1. TDD RED Phase ```python try: from spektiv.api.models.trade import Trade, TradeSide # Test implementation except ImportError: pytest.skip("Trade model not yet implemented (TDD RED phase)") ``` ### 2. Async Database Operations ```python @pytest.mark.asyncio async def test_create_trade(db_session, test_portfolio): trade = Trade(portfolio_id=test_portfolio.id, ...) db_session.add(trade) await db_session.commit() await db_session.refresh(trade) ``` ### 3. Foreign Key Storage Pattern ```python # Store foreign keys BEFORE async operations portfolio_id = test_portfolio.id # ... async operations ... # Use stored ID to avoid lazy load after rollback ``` ### 4. Constraint Testing ```python with pytest.raises((IntegrityError, ValueError)): trade = Trade(quantity=Decimal("-100")) # Invalid await db_session.commit() ``` ## Model Requirements (From Tests) ### Enums ```python class TradeSide(Enum): BUY = "BUY" SELL = "SELL" class TradeStatus(Enum): PENDING = "PENDING" FILLED = "FILLED" PARTIAL = "PARTIAL" CANCELLED = "CANCELLED" REJECTED = "REJECTED" class TradeOrderType(Enum): MARKET = "MARKET" LIMIT = "LIMIT" STOP = "STOP" STOP_LIMIT = "STOP_LIMIT" ``` ### Required Fields - portfolio_id (ForeignKey) - symbol (String) - side (TradeSide enum) - quantity (Decimal(19,8)) - price (Decimal(19,4)) - order_type (TradeOrderType enum) - status (TradeStatus enum) - executed_at (DateTime, nullable for pending) ### Optional Fields - total_value (Decimal(19,4)) - signal_source (String, nullable) - signal_confidence (Decimal(5,2), 0-100 range, nullable) - acquisition_date (Date, nullable) - cost_basis_per_unit (Decimal(19,4), nullable) - cost_basis_total (Decimal(19,4), nullable) - holding_period_days (Integer, nullable) - cgt_discount_eligible (Boolean, nullable) - cgt_gross_gain (Decimal(19,4), nullable) - cgt_gross_loss (Decimal(19,4), nullable) - cgt_net_gain (Decimal(19,4), nullable) - currency (String(3), default="AUD") - fx_rate_to_aud (Decimal(12,6), default=1.0) - total_value_aud (Decimal(19,4), nullable) ### Properties - tax_year: String - Calculated from executed_at (Australian FY) - is_buy: Boolean - True if side == BUY - is_sell: Boolean - True if side == SELL - is_filled: Boolean - True if status == FILLED ### Constraints - quantity > 0 - price > 0 - signal_confidence: 0 <= value <= 100 (when not null) - currency: uppercase, 3 letters ### Relationships - portfolio: Many-to-One with Portfolio - Cascade delete: trades deleted when portfolio deleted - Back-populates: portfolio.trades ## Australian Tax Year Calculation ```python # FY2024 = July 1, 2023 to June 30, 2024 if executed_at.month >= 7: fy_year = executed_at.year + 1 else: fy_year = executed_at.year tax_year = f"FY{fy_year}" ``` ## CGT Discount Eligibility - **Eligible**: holding_period_days >= 367 - **Discount**: 50% of gross gain - **Formula**: net_gain = gross_gain * 0.5 if eligible else gross_gain ## FIFO Matching Rules 1. Sell trades matched to oldest buy (by acquisition_date) 2. Partial parcel matching supported 3. Weighted average cost basis for multi-parcel sales 4. Holding period calculated from earliest acquisition ## Multi-Currency Support - All trades stored in original currency - FX rate at execution time stored - AUD equivalent calculated for reporting - CGT calculated in AUD (tax reporting currency) ## Next Steps (Implementation Phase) 1. Create `spektiv/api/models/trade.py` 2. Define enums (TradeSide, TradeStatus, TradeOrderType) 3. Create Trade model with all fields 4. Add check constraints 5. Add properties (tax_year, is_buy, is_sell, is_filled) 6. Add relationship to Portfolio 7. Create migration: `alembic revision --autogenerate -m "Add Trade model"` 8. Run tests: `pytest tests/unit/api/test_trade_model.py -v` 9. Run integration tests: `pytest tests/integration/api/test_trade_integration.py -v` 10. Verify 80%+ coverage ## Test Execution ```bash # Run all trade tests pytest tests/unit/api/test_trade_model.py tests/integration/api/test_trade_integration.py -v # Run with coverage pytest tests/unit/api/test_trade_model.py tests/integration/api/test_trade_integration.py --cov=spektiv/api/models/trade --cov-report=term-missing # Run specific test class pytest tests/unit/api/test_trade_model.py::TestTradeCGTCalculations -v # Run with minimal verbosity (avoid pipe deadlock) pytest tests/unit/api/test_trade_model.py --tb=line -q ``` ## Coverage Goals - **Unit Tests**: Basic CRUD, enums, constraints, properties - **Integration Tests**: Relationships, CGT workflows, FIFO, multi-currency - **Edge Cases**: Fractional shares, crypto quantities, long symbols - **Boundary Tests**: CGT discount threshold (367 days), signal confidence (0-100) ## Related Issues - **Issue #4 (DB-3)**: Portfolio model - parent relationship - **Issue #6 (DB-5)**: Trade model - this test suite - **Issue #7 (DB-6)**: Position model - will use trade data - **Issue #8 (DB-7)**: Tax report - will aggregate CGT data --- **Status**: ✅ Tests Complete (RED phase) **Created**: 2025-12-26 **Tests**: 87 total (65 unit + 22 integration) **Coverage**: Comprehensive (all requirements from spec)