1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
|
"""Tests for stock selector engine."""
from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock
from strategy_engine.stock_selector import (
SentimentCandidateSource,
StockSelector,
_extract_json_array,
_parse_llm_selections,
)
async def test_sentiment_candidate_source():
mock_db = MagicMock()
mock_db.get_top_symbol_scores = AsyncMock(
return_value=[
{"symbol": "AAPL", "composite": 0.8, "news_count": 5},
{"symbol": "NVDA", "composite": 0.6, "news_count": 3},
]
)
source = SentimentCandidateSource(mock_db)
candidates = await source.get_candidates()
assert len(candidates) == 2
assert candidates[0].symbol == "AAPL"
assert candidates[0].source == "sentiment"
def test_parse_llm_selections_valid():
llm_response = """
[
{"symbol": "NVDA", "side": "BUY", "conviction": 0.85, "reason": "AI demand", "key_news": ["NVDA beats earnings"]},
{"symbol": "XOM", "side": "BUY", "conviction": 0.72, "reason": "Oil surge", "key_news": ["Oil prices up"]}
]
"""
selections = _parse_llm_selections(llm_response)
assert len(selections) == 2
assert selections[0].symbol == "NVDA"
assert selections[0].conviction == 0.85
def test_parse_llm_selections_invalid():
selections = _parse_llm_selections("not json")
assert selections == []
def test_parse_llm_selections_with_markdown():
llm_response = """
Here are my picks:
```json
[
{"symbol": "TSLA", "side": "BUY", "conviction": 0.7, "reason": "Momentum", "key_news": ["Tesla rally"]}
]
```
"""
selections = _parse_llm_selections(llm_response)
assert len(selections) == 1
assert selections[0].symbol == "TSLA"
def test_extract_json_array_from_markdown():
text = '```json\n[{"symbol": "AAPL", "score": 0.9}]\n```'
result = _extract_json_array(text)
assert result == [{"symbol": "AAPL", "score": 0.9}]
def test_extract_json_array_bare():
text = '[{"symbol": "TSLA"}]'
result = _extract_json_array(text)
assert result == [{"symbol": "TSLA"}]
def test_extract_json_array_invalid():
assert _extract_json_array("not json") is None
def test_extract_json_array_filters_non_dicts():
text = '[{"symbol": "AAPL"}, "bad", 42]'
result = _extract_json_array(text)
assert result == [{"symbol": "AAPL"}]
async def test_selector_close():
selector = StockSelector(
db=MagicMock(), broker=MagicMock(), alpaca=MagicMock(), anthropic_api_key="test"
)
# No session yet - close should be safe
await selector.close()
assert selector._http_session is None
async def test_selector_blocks_on_risk_off():
mock_db = MagicMock()
mock_db.get_latest_market_sentiment = AsyncMock(
return_value={
"fear_greed": 15,
"fear_greed_label": "Extreme Fear",
"vix": 35.0,
"fed_stance": "neutral",
"market_regime": "risk_off",
"updated_at": datetime.now(UTC),
}
)
selector = StockSelector(
db=mock_db, broker=MagicMock(), alpaca=MagicMock(), anthropic_api_key="test"
)
result = await selector.select()
assert result == []
|