102 lines
2.9 KiB
Python
102 lines
2.9 KiB
Python
|
|
"""Tests for the DeferredSignalQueue.
|
||
|
|
|
||
|
|
Kevin's signals are mid/long-term — a signal that lands outside US market
|
||
|
|
hours should be held in a Redis sorted-set and replayed at the next market
|
||
|
|
open instead of dropped.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from datetime import datetime, timedelta, timezone
|
||
|
|
from decimal import Decimal
|
||
|
|
from uuid import uuid4
|
||
|
|
|
||
|
|
import fakeredis.aioredis
|
||
|
|
import pytest
|
||
|
|
|
||
|
|
from services.trade_executor.deferred_queue import DeferredSignalQueue
|
||
|
|
from shared.schemas.trading import SignalDirection, TradeSignal
|
||
|
|
|
||
|
|
|
||
|
|
def _signal(ticker: str = "NVDA") -> TradeSignal:
|
||
|
|
return TradeSignal(
|
||
|
|
signal_id=uuid4(),
|
||
|
|
ticker=ticker,
|
||
|
|
direction=SignalDirection.LONG,
|
||
|
|
strength=0.85,
|
||
|
|
strategy_sources=["meet_kevin:buy:0.85"],
|
||
|
|
timestamp=datetime.now(timezone.utc),
|
||
|
|
target_dollars=Decimal("2000"),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
async def redis():
|
||
|
|
r = fakeredis.aioredis.FakeRedis()
|
||
|
|
try:
|
||
|
|
yield r
|
||
|
|
finally:
|
||
|
|
await r.aclose()
|
||
|
|
|
||
|
|
|
||
|
|
async def test_defer_then_pop_due_returns_signal(redis):
|
||
|
|
q = DeferredSignalQueue(redis)
|
||
|
|
s = _signal()
|
||
|
|
past = datetime.now(timezone.utc) - timedelta(minutes=1)
|
||
|
|
await q.defer(s, target_ts=past)
|
||
|
|
|
||
|
|
due = await q.pop_due()
|
||
|
|
assert len(due) == 1
|
||
|
|
popped_signal, _queued_at = due[0]
|
||
|
|
assert popped_signal.ticker == "NVDA"
|
||
|
|
assert popped_signal.signal_id == s.signal_id
|
||
|
|
|
||
|
|
|
||
|
|
async def test_pop_due_skips_future_targets(redis):
|
||
|
|
q = DeferredSignalQueue(redis)
|
||
|
|
s_now = _signal("NVDA")
|
||
|
|
s_future = _signal("AMD")
|
||
|
|
now = datetime.now(timezone.utc)
|
||
|
|
await q.defer(s_now, target_ts=now - timedelta(seconds=10))
|
||
|
|
await q.defer(s_future, target_ts=now + timedelta(hours=12))
|
||
|
|
|
||
|
|
due = await q.pop_due()
|
||
|
|
assert [s.ticker for s, _ in due] == ["NVDA"]
|
||
|
|
|
||
|
|
remaining = await q.size()
|
||
|
|
assert remaining == 1
|
||
|
|
|
||
|
|
|
||
|
|
async def test_pop_due_is_atomic_pop(redis):
|
||
|
|
"""Once popped, a signal must NOT come back on the next pop."""
|
||
|
|
q = DeferredSignalQueue(redis)
|
||
|
|
await q.defer(_signal(), target_ts=datetime.now(timezone.utc) - timedelta(seconds=1))
|
||
|
|
|
||
|
|
first = await q.pop_due()
|
||
|
|
assert len(first) == 1
|
||
|
|
second = await q.pop_due()
|
||
|
|
assert second == []
|
||
|
|
|
||
|
|
|
||
|
|
async def test_size_zero_when_empty(redis):
|
||
|
|
q = DeferredSignalQueue(redis)
|
||
|
|
assert await q.size() == 0
|
||
|
|
|
||
|
|
|
||
|
|
async def test_defer_preserves_signal_age(redis):
|
||
|
|
"""The original TradeSignal.timestamp (which the strategy uses for
|
||
|
|
mention-age cutoffs) must survive the round-trip."""
|
||
|
|
q = DeferredSignalQueue(redis)
|
||
|
|
orig_ts = datetime(2026, 6, 1, 15, 0, tzinfo=timezone.utc)
|
||
|
|
s = TradeSignal(
|
||
|
|
signal_id=uuid4(),
|
||
|
|
ticker="MSFT",
|
||
|
|
direction=SignalDirection.LONG,
|
||
|
|
strength=0.7,
|
||
|
|
strategy_sources=["meet_kevin"],
|
||
|
|
timestamp=orig_ts,
|
||
|
|
)
|
||
|
|
await q.defer(s, target_ts=datetime.now(timezone.utc) - timedelta(seconds=1))
|
||
|
|
[(popped, _)] = await q.pop_due()
|
||
|
|
assert popped.timestamp == orig_ts
|