158 lines
4.9 KiB
Python
158 lines
4.9 KiB
Python
"""Shared pytest fixtures for portfolio tests.
|
|
|
|
Fixtures provided:
|
|
|
|
- ``tmp_reports`` -- temporary directory used as ReportStore base_dir
|
|
- ``sample_portfolio`` -- a Portfolio instance for testing (not persisted)
|
|
- ``sample_holding`` -- a Holding instance for testing (not persisted)
|
|
- ``sample_trade`` -- a Trade instance for testing (not persisted)
|
|
- ``sample_snapshot`` -- a PortfolioSnapshot instance for testing
|
|
- ``report_store`` -- a ReportStore instance backed by tmp_reports
|
|
- ``mock_supabase_client`` -- MagicMock of SupabaseClient for unit tests
|
|
|
|
Supabase integration tests use ``pytest.mark.skipif`` to auto-skip when
|
|
``SUPABASE_CONNECTION_STRING`` is not set in the environment.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from tradingagents.portfolio.models import (
|
|
Holding,
|
|
Portfolio,
|
|
PortfolioSnapshot,
|
|
Trade,
|
|
)
|
|
from tradingagents.portfolio.report_store import ReportStore
|
|
from tradingagents.portfolio.supabase_client import SupabaseClient
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Skip marker for Supabase integration tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
requires_supabase = pytest.mark.skipif(
|
|
not os.getenv("SUPABASE_CONNECTION_STRING"),
|
|
reason="SUPABASE_CONNECTION_STRING not set -- skipping integration tests",
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Data fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_portfolio_id() -> str:
|
|
"""Return a fixed UUID for deterministic testing."""
|
|
return "11111111-1111-1111-1111-111111111111"
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_holding_id() -> str:
|
|
"""Return a fixed UUID for deterministic testing."""
|
|
return "22222222-2222-2222-2222-222222222222"
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_portfolio(sample_portfolio_id: str) -> Portfolio:
|
|
"""Return an unsaved Portfolio instance for testing."""
|
|
return Portfolio(
|
|
portfolio_id=sample_portfolio_id,
|
|
name="Test Portfolio",
|
|
cash=50_000.0,
|
|
initial_cash=100_000.0,
|
|
currency="USD",
|
|
created_at="2026-03-20T00:00:00Z",
|
|
updated_at="2026-03-20T00:00:00Z",
|
|
report_path="reports/daily/2026-03-20/portfolio",
|
|
metadata={"strategy": "test"},
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_holding(sample_portfolio_id: str, sample_holding_id: str) -> Holding:
|
|
"""Return an unsaved Holding instance for testing."""
|
|
return Holding(
|
|
holding_id=sample_holding_id,
|
|
portfolio_id=sample_portfolio_id,
|
|
ticker="AAPL",
|
|
shares=100.0,
|
|
avg_cost=150.0,
|
|
sector="Technology",
|
|
industry="Consumer Electronics",
|
|
created_at="2026-03-20T00:00:00Z",
|
|
updated_at="2026-03-20T00:00:00Z",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_trade(sample_portfolio_id: str) -> Trade:
|
|
"""Return an unsaved Trade instance for testing."""
|
|
return Trade(
|
|
trade_id="33333333-3333-3333-3333-333333333333",
|
|
portfolio_id=sample_portfolio_id,
|
|
ticker="AAPL",
|
|
action="BUY",
|
|
shares=100.0,
|
|
price=150.0,
|
|
total_value=15_000.0,
|
|
trade_date="2026-03-20T10:00:00Z",
|
|
rationale="Strong momentum signal",
|
|
signal_source="scanner",
|
|
metadata={"confidence": 0.85},
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_snapshot(sample_portfolio_id: str) -> PortfolioSnapshot:
|
|
"""Return an unsaved PortfolioSnapshot instance for testing."""
|
|
return PortfolioSnapshot(
|
|
snapshot_id="44444444-4444-4444-4444-444444444444",
|
|
portfolio_id=sample_portfolio_id,
|
|
snapshot_date="2026-03-20",
|
|
total_value=115_000.0,
|
|
cash=50_000.0,
|
|
equity_value=65_000.0,
|
|
num_positions=2,
|
|
holdings_snapshot=[
|
|
{"ticker": "AAPL", "shares": 100.0, "avg_cost": 150.0},
|
|
{"ticker": "MSFT", "shares": 50.0, "avg_cost": 300.0},
|
|
],
|
|
metadata={"note": "end of day snapshot"},
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Filesystem fixtures
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def tmp_reports(tmp_path: Path) -> Path:
|
|
"""Temporary reports directory, cleaned up after each test."""
|
|
reports_dir = tmp_path / "reports"
|
|
reports_dir.mkdir()
|
|
return reports_dir
|
|
|
|
|
|
@pytest.fixture
|
|
def report_store(tmp_reports: Path) -> ReportStore:
|
|
"""ReportStore instance backed by a temporary directory."""
|
|
return ReportStore(base_dir=tmp_reports)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Mock Supabase client fixture
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_supabase_client() -> MagicMock:
|
|
"""MagicMock of SupabaseClient for unit tests that don't hit the DB."""
|
|
return MagicMock(spec=SupabaseClient)
|