2055 lines
74 KiB
Python
2055 lines
74 KiB
Python
"""Unit tests for Trade model (Issue #6: DB-5).
|
|
|
|
Tests for Trade model fields including:
|
|
- TradeSide enum (BUY, SELL)
|
|
- TradeStatus enum (PENDING, FILLED, PARTIAL, CANCELLED, REJECTED)
|
|
- TradeOrderType enum (MARKET, LIMIT, STOP, STOP_LIMIT)
|
|
- Basic trade fields (symbol, quantity, price, etc.)
|
|
- Signal fields (signal_source, signal_confidence)
|
|
- CGT (Capital Gains Tax) fields and calculations
|
|
- Currency support (currency, fx_rate_to_aud, total_value_aud)
|
|
- Tax year calculation (Australian FY: July-June)
|
|
- Decimal precision for monetary and quantity values
|
|
- Check constraints (quantity > 0, price > 0, etc.)
|
|
- Properties (is_buy, is_sell, is_filled)
|
|
- Relationship with Portfolio
|
|
|
|
Follows TDD principles with comprehensive coverage.
|
|
Tests written BEFORE implementation (RED phase).
|
|
"""
|
|
|
|
import pytest
|
|
from decimal import Decimal
|
|
from datetime import datetime, date, timedelta
|
|
from sqlalchemy import select
|
|
from sqlalchemy.exc import IntegrityError
|
|
|
|
# Mark all tests in this module as asyncio
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
class TestTradeBasicFields:
|
|
"""Tests for basic Trade model fields."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_trade_with_required_fields(self, db_session, test_portfolio):
|
|
"""Should create trade with only required fields."""
|
|
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)
|
|
|
|
# Assert
|
|
assert trade.id is not None
|
|
assert trade.portfolio_id == test_portfolio.id
|
|
assert trade.symbol == "AAPL"
|
|
assert trade.side == TradeSide.BUY
|
|
assert trade.quantity == Decimal("100")
|
|
assert trade.price == Decimal("150.00")
|
|
assert trade.order_type == TradeOrderType.MARKET
|
|
assert trade.status == TradeStatus.FILLED
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trade_defaults(self, db_session, test_portfolio):
|
|
"""Should apply default values to optional fields."""
|
|
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.MARKET,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=datetime.utcnow(),
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
# Check defaults
|
|
assert trade.currency == "AUD"
|
|
assert trade.fx_rate_to_aud == Decimal("1.0000")
|
|
assert trade.created_at is not None
|
|
assert trade.updated_at is not None
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trade_with_all_fields(self, db_session, test_portfolio):
|
|
"""Should create trade with all fields specified."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
executed_time = datetime(2024, 3, 15, 10, 30, 0)
|
|
acquisition = date(2023, 6, 1)
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="BHP.AX",
|
|
side=TradeSide.SELL,
|
|
quantity=Decimal("500"),
|
|
price=Decimal("45.50"),
|
|
total_value=Decimal("22750.00"),
|
|
order_type=TradeOrderType.LIMIT,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=executed_time,
|
|
signal_source="TechnicalAnalysis",
|
|
signal_confidence=Decimal("85.50"),
|
|
acquisition_date=acquisition,
|
|
cost_basis_per_unit=Decimal("40.00"),
|
|
cost_basis_total=Decimal("20000.00"),
|
|
holding_period_days=288,
|
|
cgt_discount_eligible=False,
|
|
cgt_gross_gain=Decimal("2750.00"),
|
|
cgt_gross_loss=Decimal("0.00"),
|
|
cgt_net_gain=Decimal("2750.00"),
|
|
currency="AUD",
|
|
fx_rate_to_aud=Decimal("1.0000"),
|
|
total_value_aud=Decimal("22750.00"),
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
# Assert all fields
|
|
assert trade.id is not None
|
|
assert trade.symbol == "BHP.AX"
|
|
assert trade.side == TradeSide.SELL
|
|
assert trade.quantity == Decimal("500")
|
|
assert trade.price == Decimal("45.50")
|
|
assert trade.total_value == Decimal("22750.00")
|
|
assert trade.signal_source == "TechnicalAnalysis"
|
|
assert trade.signal_confidence == Decimal("85.50")
|
|
assert trade.acquisition_date == acquisition
|
|
assert trade.cgt_discount_eligible is False
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trade_timestamps_auto_populate(self, db_session, test_portfolio):
|
|
"""Should auto-populate created_at and updated_at timestamps."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="CBA.AX",
|
|
side=TradeSide.BUY,
|
|
quantity=Decimal("100"),
|
|
price=Decimal("95.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)
|
|
|
|
# Assert timestamps exist and are recent
|
|
assert trade.created_at is not None
|
|
assert trade.updated_at is not None
|
|
assert isinstance(trade.created_at, datetime)
|
|
assert isinstance(trade.updated_at, datetime)
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestTradeSideEnum:
|
|
"""Tests for TradeSide enum validation."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trade_side_buy(self, db_session, test_portfolio):
|
|
"""Should create trade with BUY side."""
|
|
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)
|
|
|
|
assert trade.side == TradeSide.BUY
|
|
assert trade.side.value == "BUY"
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trade_side_sell(self, db_session, test_portfolio):
|
|
"""Should create trade with SELL side."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="TSLA",
|
|
side=TradeSide.SELL,
|
|
quantity=Decimal("50"),
|
|
price=Decimal("200.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)
|
|
|
|
assert trade.side == TradeSide.SELL
|
|
assert trade.side.value == "SELL"
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trade_side_invalid_value(self, db_session, test_portfolio):
|
|
"""Should reject invalid trade side."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
# Try to create with invalid string
|
|
with pytest.raises((ValueError, AttributeError)):
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="AAPL",
|
|
side="INVALID_SIDE",
|
|
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()
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestTradeStatusEnum:
|
|
"""Tests for TradeStatus enum validation."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trade_status_pending(self, db_session, test_portfolio):
|
|
"""Should create trade with PENDING status."""
|
|
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.LIMIT,
|
|
status=TradeStatus.PENDING,
|
|
executed_at=None, # Not executed yet
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.status == TradeStatus.PENDING
|
|
assert trade.status.value == "PENDING"
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trade_status_filled(self, db_session, test_portfolio):
|
|
"""Should create trade with FILLED status."""
|
|
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)
|
|
|
|
assert trade.status == TradeStatus.FILLED
|
|
assert trade.status.value == "FILLED"
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trade_status_partial(self, db_session, test_portfolio):
|
|
"""Should create trade with PARTIAL status."""
|
|
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.LIMIT,
|
|
status=TradeStatus.PARTIAL,
|
|
executed_at=datetime.utcnow(),
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.status == TradeStatus.PARTIAL
|
|
assert trade.status.value == "PARTIAL"
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trade_status_cancelled(self, db_session, test_portfolio):
|
|
"""Should create trade with CANCELLED status."""
|
|
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.LIMIT,
|
|
status=TradeStatus.CANCELLED,
|
|
executed_at=None,
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.status == TradeStatus.CANCELLED
|
|
assert trade.status.value == "CANCELLED"
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trade_status_rejected(self, db_session, test_portfolio):
|
|
"""Should create trade with REJECTED status."""
|
|
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.REJECTED,
|
|
executed_at=None,
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.status == TradeStatus.REJECTED
|
|
assert trade.status.value == "REJECTED"
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestTradeOrderTypeEnum:
|
|
"""Tests for TradeOrderType enum validation."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_order_type_market(self, db_session, test_portfolio):
|
|
"""Should create trade with MARKET order type."""
|
|
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)
|
|
|
|
assert trade.order_type == TradeOrderType.MARKET
|
|
assert trade.order_type.value == "MARKET"
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_order_type_limit(self, db_session, test_portfolio):
|
|
"""Should create trade with LIMIT order type."""
|
|
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.LIMIT,
|
|
status=TradeStatus.PENDING,
|
|
executed_at=None,
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.order_type == TradeOrderType.LIMIT
|
|
assert trade.order_type.value == "LIMIT"
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_order_type_stop(self, db_session, test_portfolio):
|
|
"""Should create trade with STOP order type."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="AAPL",
|
|
side=TradeSide.SELL,
|
|
quantity=Decimal("100"),
|
|
price=Decimal("140.00"),
|
|
order_type=TradeOrderType.STOP,
|
|
status=TradeStatus.PENDING,
|
|
executed_at=None,
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.order_type == TradeOrderType.STOP
|
|
assert trade.order_type.value == "STOP"
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_order_type_stop_limit(self, db_session, test_portfolio):
|
|
"""Should create trade with STOP_LIMIT order type."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="AAPL",
|
|
side=TradeSide.SELL,
|
|
quantity=Decimal("100"),
|
|
price=Decimal("140.00"),
|
|
order_type=TradeOrderType.STOP_LIMIT,
|
|
status=TradeStatus.PENDING,
|
|
executed_at=None,
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.order_type == TradeOrderType.STOP_LIMIT
|
|
assert trade.order_type.value == "STOP_LIMIT"
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestTradeDecimalPrecision:
|
|
"""Tests for Decimal precision on trade fields."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_quantity_decimal_precision(self, db_session, test_portfolio):
|
|
"""Should store quantity with 4 decimal places (PreciseNumeric standard)."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="BTC",
|
|
side=TradeSide.BUY,
|
|
quantity=Decimal("0.1234"), # 4 decimal places (per model spec)
|
|
price=Decimal("45000.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)
|
|
|
|
# Assert decimal precision maintained (4 decimals per PreciseNumeric)
|
|
assert trade.quantity == Decimal("0.1234")
|
|
assert isinstance(trade.quantity, Decimal)
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_price_decimal_precision(self, db_session, test_portfolio):
|
|
"""Should store price with 4 decimal places."""
|
|
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.1234"), # 4 decimal places
|
|
order_type=TradeOrderType.LIMIT,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=datetime.utcnow(),
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
# Assert decimal precision maintained
|
|
assert trade.price == Decimal("150.1234")
|
|
assert isinstance(trade.price, Decimal)
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_total_value_decimal_precision(self, db_session, test_portfolio):
|
|
"""Should store total_value with 4 decimal places."""
|
|
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"),
|
|
total_value=Decimal("10000.5678"),
|
|
order_type=TradeOrderType.MARKET,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=datetime.utcnow(),
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
# Assert decimal precision maintained
|
|
assert trade.total_value == Decimal("10000.5678")
|
|
assert isinstance(trade.total_value, Decimal)
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cgt_fields_decimal_precision(self, db_session, test_portfolio):
|
|
"""Should store CGT fields with 4 decimal places."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="BHP.AX",
|
|
side=TradeSide.SELL,
|
|
quantity=Decimal("500"),
|
|
price=Decimal("45.50"),
|
|
order_type=TradeOrderType.MARKET,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=datetime.utcnow(),
|
|
cost_basis_per_unit=Decimal("40.1234"),
|
|
cost_basis_total=Decimal("20061.7000"),
|
|
cgt_gross_gain=Decimal("2688.3000"),
|
|
cgt_gross_loss=Decimal("0.0000"),
|
|
cgt_net_gain=Decimal("2688.3000"),
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
# Assert CGT decimal precision
|
|
assert trade.cost_basis_per_unit == Decimal("40.1234")
|
|
assert trade.cost_basis_total == Decimal("20061.7000")
|
|
assert trade.cgt_gross_gain == Decimal("2688.3000")
|
|
assert trade.cgt_net_gain == Decimal("2688.3000")
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fx_rate_decimal_precision(self, db_session, test_portfolio):
|
|
"""Should store fx_rate_to_aud with 6 decimal places."""
|
|
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(),
|
|
currency="USD",
|
|
fx_rate_to_aud=Decimal("1.523456"), # 6 decimal places
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
# Assert FX rate precision
|
|
assert trade.fx_rate_to_aud == Decimal("1.523456")
|
|
assert isinstance(trade.fx_rate_to_aud, Decimal)
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_signal_confidence_decimal_precision(self, db_session, test_portfolio):
|
|
"""Should store signal_confidence with 2 decimal places (0-100)."""
|
|
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(),
|
|
signal_source="ML_Model",
|
|
signal_confidence=Decimal("87.65"), # 2 decimal places
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
# Assert signal confidence precision
|
|
assert trade.signal_confidence == Decimal("87.65")
|
|
assert isinstance(trade.signal_confidence, Decimal)
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestTradeTaxYear:
|
|
"""Tests for Australian tax year calculation (July-June)."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tax_year_fy2024_start(self, db_session, test_portfolio):
|
|
"""Should calculate tax year FY2024 for trade on July 1, 2023."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
# July 1, 2023 starts FY2024
|
|
executed_time = datetime(2023, 7, 1, 10, 0, 0)
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="CBA.AX",
|
|
side=TradeSide.BUY,
|
|
quantity=Decimal("100"),
|
|
price=Decimal("95.00"),
|
|
order_type=TradeOrderType.MARKET,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=executed_time,
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
# FY2024 = July 1, 2023 to June 30, 2024
|
|
assert trade.tax_year == "FY2024"
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tax_year_fy2024_end(self, db_session, test_portfolio):
|
|
"""Should calculate tax year FY2024 for trade on June 30, 2024."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
# June 30, 2024 ends FY2024
|
|
executed_time = datetime(2024, 6, 30, 23, 59, 59)
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="BHP.AX",
|
|
side=TradeSide.BUY,
|
|
quantity=Decimal("200"),
|
|
price=Decimal("45.00"),
|
|
order_type=TradeOrderType.MARKET,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=executed_time,
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.tax_year == "FY2024"
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tax_year_fy2025_start(self, db_session, test_portfolio):
|
|
"""Should calculate tax year FY2025 for trade on July 1, 2024."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
# July 1, 2024 starts FY2025
|
|
executed_time = datetime(2024, 7, 1, 0, 0, 0)
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="WES.AX",
|
|
side=TradeSide.BUY,
|
|
quantity=Decimal("150"),
|
|
price=Decimal("55.00"),
|
|
order_type=TradeOrderType.MARKET,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=executed_time,
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.tax_year == "FY2025"
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tax_year_before_fy_transition(self, db_session, test_portfolio):
|
|
"""Should calculate tax year FY2024 for trade in June 2024."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
# June 15, 2024 is before FY transition
|
|
executed_time = datetime(2024, 6, 15, 14, 30, 0)
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="NAB.AX",
|
|
side=TradeSide.BUY,
|
|
quantity=Decimal("100"),
|
|
price=Decimal("30.00"),
|
|
order_type=TradeOrderType.MARKET,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=executed_time,
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.tax_year == "FY2024"
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tax_year_after_fy_transition(self, db_session, test_portfolio):
|
|
"""Should calculate tax year FY2025 for trade in July 2024."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
# July 15, 2024 is after FY transition
|
|
executed_time = datetime(2024, 7, 15, 9, 0, 0)
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="ANZ.AX",
|
|
side=TradeSide.BUY,
|
|
quantity=Decimal("100"),
|
|
price=Decimal("25.00"),
|
|
order_type=TradeOrderType.MARKET,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=executed_time,
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.tax_year == "FY2025"
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestTradeCGTDiscount:
|
|
"""Tests for CGT discount eligibility (367+ days)."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cgt_discount_not_eligible_short_hold(self, db_session, test_portfolio):
|
|
"""Should not be eligible for CGT discount with <367 days holding."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="AAPL",
|
|
side=TradeSide.SELL,
|
|
quantity=Decimal("100"),
|
|
price=Decimal("160.00"),
|
|
order_type=TradeOrderType.MARKET,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=datetime.utcnow(),
|
|
acquisition_date=date.today() - timedelta(days=200),
|
|
holding_period_days=200,
|
|
cgt_discount_eligible=False,
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.holding_period_days == 200
|
|
assert trade.cgt_discount_eligible is False
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cgt_discount_eligible_367_days(self, db_session, test_portfolio):
|
|
"""Should be eligible for CGT discount with exactly 367 days holding."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="BHP.AX",
|
|
side=TradeSide.SELL,
|
|
quantity=Decimal("500"),
|
|
price=Decimal("45.00"),
|
|
order_type=TradeOrderType.MARKET,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=datetime.utcnow(),
|
|
acquisition_date=date.today() - timedelta(days=367),
|
|
holding_period_days=367,
|
|
cgt_discount_eligible=True,
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.holding_period_days == 367
|
|
assert trade.cgt_discount_eligible is True
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cgt_discount_eligible_long_hold(self, db_session, test_portfolio):
|
|
"""Should be eligible for CGT discount with >367 days holding."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="CBA.AX",
|
|
side=TradeSide.SELL,
|
|
quantity=Decimal("200"),
|
|
price=Decimal("100.00"),
|
|
order_type=TradeOrderType.MARKET,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=datetime.utcnow(),
|
|
acquisition_date=date.today() - timedelta(days=500),
|
|
holding_period_days=500,
|
|
cgt_discount_eligible=True,
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.holding_period_days == 500
|
|
assert trade.cgt_discount_eligible is True
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cgt_discount_boundary_366_days(self, db_session, test_portfolio):
|
|
"""Should not be eligible with 366 days (boundary test)."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="WES.AX",
|
|
side=TradeSide.SELL,
|
|
quantity=Decimal("150"),
|
|
price=Decimal("55.00"),
|
|
order_type=TradeOrderType.MARKET,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=datetime.utcnow(),
|
|
acquisition_date=date.today() - timedelta(days=366),
|
|
holding_period_days=366,
|
|
cgt_discount_eligible=False,
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.holding_period_days == 366
|
|
assert trade.cgt_discount_eligible is False
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestTradeCGTCalculations:
|
|
"""Tests for CGT calculation fields."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cgt_gross_gain_calculation(self, db_session, test_portfolio):
|
|
"""Should calculate gross gain correctly."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
# Sell at $50, cost basis $40 = $10 gain per unit
|
|
# 100 units = $1000 gross gain
|
|
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.utcnow(),
|
|
cost_basis_per_unit=Decimal("40.00"),
|
|
cost_basis_total=Decimal("4000.00"),
|
|
cgt_gross_gain=Decimal("1000.00"),
|
|
cgt_gross_loss=Decimal("0.00"),
|
|
cgt_net_gain=Decimal("1000.00"),
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.cgt_gross_gain == Decimal("1000.00")
|
|
assert trade.cgt_gross_loss == Decimal("0.00")
|
|
assert 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_cgt_gross_loss_calculation(self, db_session, test_portfolio):
|
|
"""Should calculate gross loss correctly."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
# Sell at $30, cost basis $40 = $10 loss per unit
|
|
# 100 units = $1000 gross loss
|
|
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.utcnow(),
|
|
cost_basis_per_unit=Decimal("40.00"),
|
|
cost_basis_total=Decimal("4000.00"),
|
|
cgt_gross_gain=Decimal("0.00"),
|
|
cgt_gross_loss=Decimal("1000.00"),
|
|
cgt_net_gain=Decimal("-1000.00"),
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.cgt_gross_gain == Decimal("0.00")
|
|
assert trade.cgt_gross_loss == Decimal("1000.00")
|
|
assert 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_cgt_net_gain_with_discount(self, db_session, test_portfolio):
|
|
"""Should calculate net gain with CGT discount applied."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
# $1000 gross gain, eligible for 50% discount = $500 net gain
|
|
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.utcnow(),
|
|
acquisition_date=date.today() - timedelta(days=400),
|
|
cost_basis_per_unit=Decimal("40.00"),
|
|
cost_basis_total=Decimal("4000.00"),
|
|
holding_period_days=400,
|
|
cgt_discount_eligible=True,
|
|
cgt_gross_gain=Decimal("1000.00"),
|
|
cgt_gross_loss=Decimal("0.00"),
|
|
cgt_net_gain=Decimal("500.00"), # 50% discount applied
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.cgt_gross_gain == Decimal("1000.00")
|
|
assert trade.cgt_discount_eligible is True
|
|
assert 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_cgt_no_gain_or_loss(self, db_session, test_portfolio):
|
|
"""Should handle breakeven trades (no gain or loss)."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
# Sell at cost basis = no gain or loss
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="CBA.AX",
|
|
side=TradeSide.SELL,
|
|
quantity=Decimal("100"),
|
|
price=Decimal("40.00"),
|
|
total_value=Decimal("4000.00"),
|
|
order_type=TradeOrderType.MARKET,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=datetime.utcnow(),
|
|
cost_basis_per_unit=Decimal("40.00"),
|
|
cost_basis_total=Decimal("4000.00"),
|
|
cgt_gross_gain=Decimal("0.00"),
|
|
cgt_gross_loss=Decimal("0.00"),
|
|
cgt_net_gain=Decimal("0.00"),
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.cgt_gross_gain == Decimal("0.00")
|
|
assert trade.cgt_gross_loss == Decimal("0.00")
|
|
assert trade.cgt_net_gain == Decimal("0.00")
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestTradeCurrencySupport:
|
|
"""Tests for multi-currency support."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_default_currency_aud(self, db_session, test_portfolio):
|
|
"""Should default to AUD currency."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="BHP.AX",
|
|
side=TradeSide.BUY,
|
|
quantity=Decimal("100"),
|
|
price=Decimal("45.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)
|
|
|
|
assert trade.currency == "AUD"
|
|
assert trade.fx_rate_to_aud == Decimal("1.0000")
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_usd_currency_with_fx_rate(self, db_session, test_portfolio):
|
|
"""Should support USD with FX rate conversion."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
# $100 USD @ 1.50 FX rate = $150 AUD
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="AAPL",
|
|
side=TradeSide.BUY,
|
|
quantity=Decimal("100"),
|
|
price=Decimal("150.00"),
|
|
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"),
|
|
total_value_aud=Decimal("22500.00"), # AUD
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.currency == "USD"
|
|
assert trade.fx_rate_to_aud == Decimal("1.50")
|
|
assert trade.total_value == Decimal("15000.00")
|
|
assert trade.total_value_aud == Decimal("22500.00")
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_common_currencies(self, db_session, test_portfolio):
|
|
"""Should accept common 3-letter currency codes."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
currencies = ["USD", "EUR", "GBP", "JPY", "CNY", "AUD"]
|
|
|
|
for currency in currencies:
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol=f"STOCK.{currency}",
|
|
side=TradeSide.BUY,
|
|
quantity=Decimal("100"),
|
|
price=Decimal("100.00"),
|
|
order_type=TradeOrderType.MARKET,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=datetime.utcnow(),
|
|
currency=currency,
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.currency == currency
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_currency_uppercase_enforced(self, db_session, test_portfolio):
|
|
"""Should store currency in uppercase."""
|
|
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(),
|
|
currency="usd", # lowercase input
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
# Should be stored in uppercase
|
|
assert trade.currency == "USD"
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestTradeConstraints:
|
|
"""Tests for CheckConstraints on trade fields."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_quantity_must_be_positive(self, db_session, test_portfolio):
|
|
"""Should reject negative quantity."""
|
|
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"), # Negative!
|
|
price=Decimal("150.00"),
|
|
order_type=TradeOrderType.MARKET,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=datetime.utcnow(),
|
|
)
|
|
|
|
db_session.add(trade)
|
|
|
|
with pytest.raises((IntegrityError, ValueError)):
|
|
await db_session.commit()
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_quantity_cannot_be_zero(self, db_session, test_portfolio):
|
|
"""Should reject zero quantity."""
|
|
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("0"), # Zero!
|
|
price=Decimal("150.00"),
|
|
order_type=TradeOrderType.MARKET,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=datetime.utcnow(),
|
|
)
|
|
|
|
db_session.add(trade)
|
|
|
|
with pytest.raises((IntegrityError, ValueError)):
|
|
await db_session.commit()
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_price_must_be_positive(self, db_session, test_portfolio):
|
|
"""Should reject negative price."""
|
|
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"), # Negative!
|
|
order_type=TradeOrderType.MARKET,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=datetime.utcnow(),
|
|
)
|
|
|
|
db_session.add(trade)
|
|
|
|
with pytest.raises((IntegrityError, ValueError)):
|
|
await db_session.commit()
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_price_cannot_be_zero(self, db_session, test_portfolio):
|
|
"""Should reject zero price."""
|
|
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("0.00"), # Zero!
|
|
order_type=TradeOrderType.MARKET,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=datetime.utcnow(),
|
|
)
|
|
|
|
db_session.add(trade)
|
|
|
|
with pytest.raises((IntegrityError, ValueError)):
|
|
await db_session.commit()
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_signal_confidence_range_0_to_100(self, db_session, test_portfolio):
|
|
"""Should accept signal_confidence between 0 and 100."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
# Test boundary values
|
|
for confidence in [Decimal("0.00"), Decimal("50.00"), Decimal("100.00")]:
|
|
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(),
|
|
signal_source="Test",
|
|
signal_confidence=confidence,
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.signal_confidence == confidence
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_signal_confidence_above_100_rejected(self, db_session, test_portfolio):
|
|
"""Should reject signal_confidence > 100."""
|
|
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(),
|
|
signal_source="Test",
|
|
signal_confidence=Decimal("101.00"), # > 100!
|
|
)
|
|
|
|
db_session.add(trade)
|
|
|
|
with pytest.raises((IntegrityError, ValueError)):
|
|
await db_session.commit()
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_signal_confidence_negative_rejected(self, db_session, test_portfolio):
|
|
"""Should reject negative signal_confidence."""
|
|
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(),
|
|
signal_source="Test",
|
|
signal_confidence=Decimal("-10.00"), # Negative!
|
|
)
|
|
|
|
db_session.add(trade)
|
|
|
|
with pytest.raises((IntegrityError, ValueError)):
|
|
await db_session.commit()
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestTradeSignalFields:
|
|
"""Tests for signal tracking fields."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_signal_source_stored(self, db_session, test_portfolio):
|
|
"""Should store signal source string."""
|
|
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(),
|
|
signal_source="TechnicalAnalysis",
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.signal_source == "TechnicalAnalysis"
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_signal_confidence_stored(self, db_session, test_portfolio):
|
|
"""Should store signal confidence value."""
|
|
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(),
|
|
signal_source="ML_Model",
|
|
signal_confidence=Decimal("92.50"),
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.signal_confidence == Decimal("92.50")
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_signal_fields_optional(self, db_session, test_portfolio):
|
|
"""Should allow trades without signal fields."""
|
|
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(),
|
|
# No signal_source or signal_confidence
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.signal_source is None
|
|
assert trade.signal_confidence is None
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestTradeProperties:
|
|
"""Tests for trade model properties."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_is_buy_property(self, db_session, test_portfolio):
|
|
"""Should return True for BUY side."""
|
|
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)
|
|
|
|
assert trade.is_buy is True
|
|
assert trade.is_sell is False
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_is_sell_property(self, db_session, test_portfolio):
|
|
"""Should return True for SELL side."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="TSLA",
|
|
side=TradeSide.SELL,
|
|
quantity=Decimal("50"),
|
|
price=Decimal("200.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)
|
|
|
|
assert trade.is_buy is False
|
|
assert trade.is_sell is True
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_is_filled_property(self, db_session, test_portfolio):
|
|
"""Should return True for FILLED status."""
|
|
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)
|
|
|
|
assert trade.is_filled is True
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_is_filled_false_for_pending(self, db_session, test_portfolio):
|
|
"""Should return False for PENDING status."""
|
|
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.LIMIT,
|
|
status=TradeStatus.PENDING,
|
|
executed_at=None,
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.is_filled is False
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestTradePortfolioRelationship:
|
|
"""Tests for Trade-Portfolio relationship."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trade_belongs_to_portfolio(self, db_session, test_portfolio):
|
|
"""Should establish relationship from trade 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)
|
|
|
|
# Load the relationship
|
|
await db_session.refresh(trade, ["portfolio"])
|
|
|
|
assert trade.portfolio is not None
|
|
assert trade.portfolio.id == test_portfolio.id
|
|
assert trade.portfolio.name == test_portfolio.name
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_portfolio_has_many_trades(self, db_session, test_portfolio):
|
|
"""Should establish relationship from portfolio to trades."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
trade1 = 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(),
|
|
)
|
|
|
|
trade2 = 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=datetime.utcnow(),
|
|
)
|
|
|
|
db_session.add(trade1)
|
|
db_session.add(trade2)
|
|
await db_session.commit()
|
|
|
|
# Refresh portfolio with trades relationship
|
|
await db_session.refresh(test_portfolio, ["trades"])
|
|
|
|
assert len(test_portfolio.trades) == 2
|
|
symbols = [t.symbol for t in test_portfolio.trades]
|
|
assert "AAPL" in symbols
|
|
assert "TSLA" in symbols
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cascade_delete_when_portfolio_deleted(self, db_session, test_user):
|
|
"""Should delete trades when portfolio is deleted (cascade)."""
|
|
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
|
|
trade = 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(),
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
trade_id = trade.id
|
|
|
|
# Delete the portfolio
|
|
await db_session.delete(portfolio)
|
|
await db_session.commit()
|
|
|
|
# Check trade is also deleted
|
|
result = await db_session.execute(
|
|
select(Trade).where(Trade.id == trade_id)
|
|
)
|
|
deleted_trade = result.scalar_one_or_none()
|
|
|
|
assert deleted_trade is None
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestTradeEdgeCases:
|
|
"""Tests for edge cases and boundary conditions."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_very_long_symbol(self, db_session, test_portfolio):
|
|
"""Should handle symbol names up to 20 chars (model limit)."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
long_symbol = "A" * 20 # 20 char limit per model spec
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol=long_symbol,
|
|
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)
|
|
|
|
assert trade.symbol == long_symbol
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_fractional_shares(self, db_session, test_portfolio):
|
|
"""Should handle fractional share quantities."""
|
|
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("0.5"), # Half a share
|
|
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)
|
|
|
|
assert trade.quantity == Decimal("0.5")
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_very_small_quantity(self, db_session, test_portfolio):
|
|
"""Should handle small quantities within 4 decimal precision."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="BTC",
|
|
side=TradeSide.BUY,
|
|
quantity=Decimal("0.0001"), # Smallest with 4 decimal precision
|
|
price=Decimal("45000.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)
|
|
|
|
assert trade.quantity == Decimal("0.0001")
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_very_large_quantity(self, db_session, test_portfolio):
|
|
"""Should handle large quantities."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="PENNY_STOCK",
|
|
side=TradeSide.BUY,
|
|
quantity=Decimal("1000000"), # 1 million shares
|
|
price=Decimal("0.01"),
|
|
order_type=TradeOrderType.MARKET,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=datetime.utcnow(),
|
|
)
|
|
|
|
db_session.add(trade)
|
|
await db_session.commit()
|
|
await db_session.refresh(trade)
|
|
|
|
assert trade.quantity == Decimal("1000000")
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_trade_repr(self, db_session, test_portfolio):
|
|
"""Should have meaningful string representation."""
|
|
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)
|
|
|
|
repr_str = repr(trade)
|
|
assert "Trade" in repr_str
|
|
assert "AAPL" in repr_str or str(trade.id) in repr_str
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
|
|
class TestTradeQueryOperations:
|
|
"""Tests for querying Trade records."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_query_trade_by_id(self, db_session, test_portfolio):
|
|
"""Should retrieve trade by ID."""
|
|
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()
|
|
trade_id = trade.id
|
|
|
|
# Query by ID
|
|
result = await db_session.execute(
|
|
select(Trade).where(Trade.id == trade_id)
|
|
)
|
|
found = result.scalar_one()
|
|
|
|
assert found.id == trade_id
|
|
assert found.symbol == "AAPL"
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_query_trades_by_symbol(self, db_session, test_portfolio):
|
|
"""Should filter trades by symbol."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
# Create trades for different symbols
|
|
symbols = ["AAPL", "TSLA", "AAPL", "GOOGL"]
|
|
|
|
for symbol in symbols:
|
|
trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol=symbol,
|
|
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()
|
|
|
|
# Query for AAPL trades
|
|
result = await db_session.execute(
|
|
select(Trade).where(Trade.symbol == "AAPL")
|
|
)
|
|
aapl_trades = result.scalars().all()
|
|
|
|
assert len(aapl_trades) == 2
|
|
assert all(t.symbol == "AAPL" for t in aapl_trades)
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_query_trades_by_side(self, db_session, test_portfolio):
|
|
"""Should filter trades by side (BUY/SELL)."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
# Create mix of BUY and SELL trades
|
|
buy_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(),
|
|
)
|
|
|
|
sell_trade = Trade(
|
|
portfolio_id=test_portfolio.id,
|
|
symbol="AAPL",
|
|
side=TradeSide.SELL,
|
|
quantity=Decimal("50"),
|
|
price=Decimal("160.00"),
|
|
order_type=TradeOrderType.MARKET,
|
|
status=TradeStatus.FILLED,
|
|
executed_at=datetime.utcnow(),
|
|
)
|
|
|
|
db_session.add_all([buy_trade, sell_trade])
|
|
await db_session.commit()
|
|
|
|
# Query for BUY trades
|
|
result = await db_session.execute(
|
|
select(Trade).where(Trade.side == TradeSide.BUY)
|
|
)
|
|
buy_trades = result.scalars().all()
|
|
|
|
assert len(buy_trades) == 1
|
|
assert buy_trades[0].side == TradeSide.BUY
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_query_trades_by_status(self, db_session, test_portfolio):
|
|
"""Should filter trades by status."""
|
|
try:
|
|
from tradingagents.api.models.trade import Trade, TradeSide, TradeStatus, TradeOrderType
|
|
|
|
# Create trades with different statuses
|
|
filled = 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(),
|
|
)
|
|
|
|
pending = 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_all([filled, pending])
|
|
await db_session.commit()
|
|
|
|
# Query for PENDING trades
|
|
result = await db_session.execute(
|
|
select(Trade).where(Trade.status == TradeStatus.PENDING)
|
|
)
|
|
pending_trades = result.scalars().all()
|
|
|
|
assert len(pending_trades) == 1
|
|
assert pending_trades[0].status == TradeStatus.PENDING
|
|
|
|
except ImportError:
|
|
pytest.skip("Trade model not yet implemented (TDD RED phase)")
|