Files
signal-platform/tests/unit/test_watchlist_service.py
T
dennisthiessen 6e06f51bb6
Deploy / lint (push) Successful in 6s
Deploy / test (push) Successful in 35s
Deploy / deploy (push) Successful in 24s
make watchlist fully manual; add price + day-change, two-block overview
Per design decision: the watchlist is now purely user-curated (no auto-seeding
of the top-10), so the auto_populate/dismissed machinery is removed and removals
are plain deletes. Each entry is enriched with latest close + day-over-day move.

Overview now shows two clear blocks: Top Setups (what to trade) and My Watchlist
(my names with current price and today's %). Market watchlist table drops the
now-meaningless auto/manual Type column in favour of Price and Day columns.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 14:25:04 +02:00

125 lines
3.8 KiB
Python

"""Tests for the user-curated watchlist service.
The watchlist is fully manual: the user adds and removes tickers, capped at
WATCHLIST_MAX. Removals are hard deletes and must stick. Entries are enriched
on read with the latest price and day-over-day move.
"""
from __future__ import annotations
from datetime import date, datetime, timedelta, timezone
import pytest
from app.exceptions import DuplicateError, NotFoundError, ValidationError
from app.models.ohlcv import OHLCVRecord
from app.models.score import CompositeScore
from app.models.ticker import Ticker
from app.models.user import User
from app.services.watchlist_service import (
WATCHLIST_MAX,
add_manual_entry,
get_watchlist,
remove_entry,
)
# Plain (committing) session — the watchlist service commits, so the rolling-back
# db_session fixture would fight it. Reuse the shared in-memory engine.
from tests.conftest import _test_session_factory # type: ignore
@pytest.fixture
async def session():
async with _test_session_factory() as s:
yield s
async def _make_user(session) -> int:
user = User(username="u", password_hash="x", role="user", has_access=True)
session.add(user)
await session.flush()
await session.commit()
return user.id
async def _make_ticker(session, symbol: str, *, score: float | None = None) -> int:
t = Ticker(symbol=symbol)
session.add(t)
await session.flush()
if score is not None:
session.add(
CompositeScore(
ticker_id=t.id,
score=score,
is_stale=False,
weights_json="{}",
computed_at=datetime.now(timezone.utc),
)
)
await session.commit()
return t.id
async def test_add_and_remove_sticks(session):
user_id = await _make_user(session)
await _make_ticker(session, "AAA", score=80.0)
await add_manual_entry(session, user_id, "AAA")
rows = await get_watchlist(session, user_id)
assert {r["symbol"] for r in rows} == {"AAA"}
# Removal is permanent — nothing re-adds it.
await remove_entry(session, user_id, "AAA")
rows = await get_watchlist(session, user_id)
assert rows == []
async def test_duplicate_add_rejected(session):
user_id = await _make_user(session)
await _make_ticker(session, "AAA")
await add_manual_entry(session, user_id, "AAA")
with pytest.raises(DuplicateError):
await add_manual_entry(session, user_id, "AAA")
async def test_remove_absent_raises(session):
user_id = await _make_user(session)
await _make_ticker(session, "AAA")
with pytest.raises(NotFoundError):
await remove_entry(session, user_id, "AAA")
async def test_cap_enforced(session):
user_id = await _make_user(session)
for i in range(WATCHLIST_MAX):
sym = f"T{i:02d}"
await _make_ticker(session, sym)
await add_manual_entry(session, user_id, sym)
await _make_ticker(session, "OVER")
with pytest.raises(ValidationError):
await add_manual_entry(session, user_id, "OVER")
async def test_enrichment_includes_price_and_change(session):
user_id = await _make_user(session)
ticker_id = await _make_ticker(session, "AAA", score=70.0)
today = date.today()
session.add(OHLCVRecord(
ticker_id=ticker_id, date=today - timedelta(days=1),
open=100, high=100, low=100, close=100.0, volume=1,
))
session.add(OHLCVRecord(
ticker_id=ticker_id, date=today,
open=110, high=110, low=110, close=110.0, volume=1,
))
await session.commit()
await add_manual_entry(session, user_id, "AAA")
rows = await get_watchlist(session, user_id)
row = rows[0]
assert row["last_close"] == 110.0
assert row["change_pct"] == pytest.approx(10.0)
assert row["price_date"] == today