9.0 KiB
9.0 KiB
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
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
@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
# 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
with pytest.raises((IntegrityError, ValueError)):
trade = Trade(quantity=Decimal("-100")) # Invalid
await db_session.commit()
Model Requirements (From Tests)
Enums
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
# 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
- Sell trades matched to oldest buy (by acquisition_date)
- Partial parcel matching supported
- Weighted average cost basis for multi-parcel sales
- 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)
- Create
spektiv/api/models/trade.py - Define enums (TradeSide, TradeStatus, TradeOrderType)
- Create Trade model with all fields
- Add check constraints
- Add properties (tax_year, is_buy, is_sell, is_filled)
- Add relationship to Portfolio
- Create migration:
alembic revision --autogenerate -m "Add Trade model" - Run tests:
pytest tests/unit/api/test_trade_model.py -v - Run integration tests:
pytest tests/integration/api/test_trade_integration.py -v - Verify 80%+ coverage
Test Execution
# 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)