summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--services/news-collector/src/news_collector/collectors/fear_greed.py5
-rw-r--r--services/news-collector/src/news_collector/collectors/fed.py23
-rw-r--r--services/news-collector/src/news_collector/collectors/reddit.py4
-rw-r--r--services/news-collector/src/news_collector/collectors/sec_edgar.py43
-rw-r--r--services/news-collector/src/news_collector/collectors/truth_social.py4
-rw-r--r--services/news-collector/tests/test_fed.py16
-rw-r--r--services/news-collector/tests/test_finnhub.py1
-rw-r--r--services/news-collector/tests/test_main.py8
-rw-r--r--services/news-collector/tests/test_reddit.py38
-rw-r--r--services/news-collector/tests/test_rss.py1
-rw-r--r--services/news-collector/tests/test_sec_edgar.py4
-rw-r--r--services/news-collector/tests/test_truth_social.py13
-rw-r--r--services/strategy-engine/src/strategy_engine/main.py6
-rw-r--r--services/strategy-engine/src/strategy_engine/stock_selector.py13
-rw-r--r--services/strategy-engine/tests/test_stock_selector.py40
-rw-r--r--shared/alembic/versions/002_news_sentiment_tables.py8
-rw-r--r--shared/src/shared/db.py10
-rw-r--r--shared/src/shared/notifier.py3
-rw-r--r--shared/src/shared/sentiment.py6
-rw-r--r--shared/tests/test_db_news.py2
-rw-r--r--shared/tests/test_sentiment_aggregator.py12
-rw-r--r--shared/tests/test_sentiment_models.py1
22 files changed, 178 insertions, 83 deletions
diff --git a/services/news-collector/src/news_collector/collectors/fear_greed.py b/services/news-collector/src/news_collector/collectors/fear_greed.py
index 305d416..f79f716 100644
--- a/services/news-collector/src/news_collector/collectors/fear_greed.py
+++ b/services/news-collector/src/news_collector/collectors/fear_greed.py
@@ -7,7 +7,6 @@ from typing import Optional
import aiohttp
from news_collector.collectors.base import BaseCollector
-from shared.models import NewsItem
logger = logging.getLogger(__name__)
@@ -31,7 +30,9 @@ class FearGreedCollector(BaseCollector):
headers = {"User-Agent": "Mozilla/5.0"}
try:
async with aiohttp.ClientSession() as session:
- async with session.get(FEAR_GREED_URL, headers=headers, timeout=aiohttp.ClientTimeout(total=10)) as resp:
+ async with session.get(
+ FEAR_GREED_URL, headers=headers, timeout=aiohttp.ClientTimeout(total=10)
+ ) as resp:
if resp.status != 200:
return None
return await resp.json()
diff --git a/services/news-collector/src/news_collector/collectors/fed.py b/services/news-collector/src/news_collector/collectors/fed.py
index 47b70f5..fce4842 100644
--- a/services/news-collector/src/news_collector/collectors/fed.py
+++ b/services/news-collector/src/news_collector/collectors/fed.py
@@ -17,12 +17,27 @@ logger = logging.getLogger(__name__)
_FED_RSS_URL = "https://www.federalreserve.gov/feeds/press_all.xml"
_HAWKISH_KEYWORDS = [
- "rate hike", "interest rate increase", "tighten", "tightening", "inflation",
- "hawkish", "restrictive", "raise rates", "hike rates",
+ "rate hike",
+ "interest rate increase",
+ "tighten",
+ "tightening",
+ "inflation",
+ "hawkish",
+ "restrictive",
+ "raise rates",
+ "hike rates",
]
_DOVISH_KEYWORDS = [
- "rate cut", "interest rate decrease", "easing", "ease", "stimulus",
- "dovish", "accommodative", "lower rates", "cut rates", "quantitative easing",
+ "rate cut",
+ "interest rate decrease",
+ "easing",
+ "ease",
+ "stimulus",
+ "dovish",
+ "accommodative",
+ "lower rates",
+ "cut rates",
+ "quantitative easing",
]
diff --git a/services/news-collector/src/news_collector/collectors/reddit.py b/services/news-collector/src/news_collector/collectors/reddit.py
index 11b855c..226a2f9 100644
--- a/services/news-collector/src/news_collector/collectors/reddit.py
+++ b/services/news-collector/src/news_collector/collectors/reddit.py
@@ -39,7 +39,9 @@ class RedditCollector(BaseCollector):
headers = {"User-Agent": "TradingPlatform/1.0 (research@example.com)"}
try:
async with aiohttp.ClientSession() as session:
- async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=10)) as resp:
+ async with session.get(
+ url, headers=headers, timeout=aiohttp.ClientTimeout(total=10)
+ ) as resp:
if resp.status == 200:
data = await resp.json()
return data.get("data", {}).get("children", [])
diff --git a/services/news-collector/src/news_collector/collectors/sec_edgar.py b/services/news-collector/src/news_collector/collectors/sec_edgar.py
index a00abb5..ca1d070 100644
--- a/services/news-collector/src/news_collector/collectors/sec_edgar.py
+++ b/services/news-collector/src/news_collector/collectors/sec_edgar.py
@@ -12,9 +12,16 @@ from news_collector.collectors.base import BaseCollector
logger = logging.getLogger(__name__)
TRACKED_CIKS = {
- "0000320193": "AAPL", "0000789019": "MSFT", "0001652044": "GOOGL",
- "0001018724": "AMZN", "0001318605": "TSLA", "0001045810": "NVDA",
- "0001326801": "META", "0000019617": "JPM", "0000078003": "PFE", "0000021344": "KO",
+ "0000320193": "AAPL",
+ "0000789019": "MSFT",
+ "0001652044": "GOOGL",
+ "0001018724": "AMZN",
+ "0001318605": "TSLA",
+ "0001045810": "NVDA",
+ "0001326801": "META",
+ "0000019617": "JPM",
+ "0000078003": "PFE",
+ "0000021344": "KO",
}
SEC_USER_AGENT = "TradingPlatform research@example.com"
@@ -37,7 +44,9 @@ class SecEdgarCollector(BaseCollector):
for cik, ticker in TRACKED_CIKS.items():
try:
url = f"https://data.sec.gov/submissions/CIK{cik}.json"
- async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=10)) as resp:
+ async with session.get(
+ url, headers=headers, timeout=aiohttp.ClientTimeout(total=10)
+ ) as resp:
if resp.status == 200:
data = await resp.json()
data["tickers"] = [{"ticker": ticker}]
@@ -72,16 +81,20 @@ class SecEdgarCollector(BaseCollector):
accession = accessions[i] if i < len(accessions) else ""
headline = f"{company_name} ({', '.join(tickers)}): {form} - {desc}"
- items.append(NewsItem(
- source=self.name,
- headline=headline,
- summary=desc,
- url=f"https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&accession={accession}",
- published_at=datetime.strptime(filing_date, "%Y-%m-%d").replace(tzinfo=timezone.utc),
- symbols=tickers,
- sentiment=self._vader.polarity_scores(headline)["compound"],
- category=NewsCategory.FILING,
- raw_data={"form": form, "accession": accession},
- ))
+ items.append(
+ NewsItem(
+ source=self.name,
+ headline=headline,
+ summary=desc,
+ url=f"https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&accession={accession}",
+ published_at=datetime.strptime(filing_date, "%Y-%m-%d").replace(
+ tzinfo=timezone.utc
+ ),
+ symbols=tickers,
+ sentiment=self._vader.polarity_scores(headline)["compound"],
+ category=NewsCategory.FILING,
+ raw_data={"form": form, "accession": accession},
+ )
+ )
return items
diff --git a/services/news-collector/src/news_collector/collectors/truth_social.py b/services/news-collector/src/news_collector/collectors/truth_social.py
index 2205257..33ebc86 100644
--- a/services/news-collector/src/news_collector/collectors/truth_social.py
+++ b/services/news-collector/src/news_collector/collectors/truth_social.py
@@ -37,7 +37,9 @@ class TruthSocialCollector(BaseCollector):
headers = {"User-Agent": "TradingPlatform/1.0 (research@example.com)"}
try:
async with aiohttp.ClientSession() as session:
- async with session.get(_API_URL, headers=headers, timeout=aiohttp.ClientTimeout(total=10)) as resp:
+ async with session.get(
+ _API_URL, headers=headers, timeout=aiohttp.ClientTimeout(total=10)
+ ) as resp:
if resp.status == 200:
return await resp.json()
except Exception as exc:
diff --git a/services/news-collector/tests/test_fed.py b/services/news-collector/tests/test_fed.py
index 8acea5f..d1a736b 100644
--- a/services/news-collector/tests/test_fed.py
+++ b/services/news-collector/tests/test_fed.py
@@ -1,24 +1,36 @@
"""Tests for Federal Reserve collector."""
+
import pytest
from unittest.mock import AsyncMock, patch
from news_collector.collectors.fed import FedCollector
+
@pytest.fixture
def collector():
return FedCollector()
+
def test_collector_name(collector):
assert collector.name == "fed"
assert collector.poll_interval == 3600
+
async def test_is_available(collector):
assert await collector.is_available() is True
+
async def test_collect_parses_rss(collector):
mock_entries = [
- {"title": "Federal Reserve issues FOMC statement", "link": "https://www.federalreserve.gov/newsevents/pressreleases/monetary20260402a.htm", "published_parsed": (2026, 4, 2, 14, 0, 0, 0, 0, 0), "summary": "The Federal Open Market Committee decided to maintain the target range..."},
+ {
+ "title": "Federal Reserve issues FOMC statement",
+ "link": "https://www.federalreserve.gov/newsevents/pressreleases/monetary20260402a.htm",
+ "published_parsed": (2026, 4, 2, 14, 0, 0, 0, 0, 0),
+ "summary": "The Federal Open Market Committee decided to maintain the target range...",
+ },
]
- with patch.object(collector, "_fetch_fed_rss", new_callable=AsyncMock, return_value=mock_entries):
+ with patch.object(
+ collector, "_fetch_fed_rss", new_callable=AsyncMock, return_value=mock_entries
+ ):
items = await collector.collect()
assert len(items) == 1
assert items[0].source == "fed"
diff --git a/services/news-collector/tests/test_finnhub.py b/services/news-collector/tests/test_finnhub.py
index 74bd5e6..a4cf169 100644
--- a/services/news-collector/tests/test_finnhub.py
+++ b/services/news-collector/tests/test_finnhub.py
@@ -2,7 +2,6 @@
import pytest
from unittest.mock import AsyncMock, patch
-from datetime import datetime, timezone
from news_collector.collectors.finnhub import FinnhubCollector
diff --git a/services/news-collector/tests/test_main.py b/services/news-collector/tests/test_main.py
index 3ebb094..66190dc 100644
--- a/services/news-collector/tests/test_main.py
+++ b/services/news-collector/tests/test_main.py
@@ -1,5 +1,5 @@
"""Tests for news collector scheduler."""
-import pytest
+
from unittest.mock import AsyncMock, MagicMock
from datetime import datetime, timezone
from shared.models import NewsCategory, NewsItem
@@ -8,9 +8,11 @@ from news_collector.main import run_collector_once
async def test_run_collector_once_stores_and_publishes():
mock_item = NewsItem(
- source="test", headline="Test news",
+ source="test",
+ headline="Test news",
published_at=datetime(2026, 4, 2, tzinfo=timezone.utc),
- sentiment=0.5, category=NewsCategory.MACRO,
+ sentiment=0.5,
+ category=NewsCategory.MACRO,
)
mock_collector = MagicMock()
mock_collector.name = "test"
diff --git a/services/news-collector/tests/test_reddit.py b/services/news-collector/tests/test_reddit.py
index 3626c0a..440b173 100644
--- a/services/news-collector/tests/test_reddit.py
+++ b/services/news-collector/tests/test_reddit.py
@@ -1,33 +1,63 @@
"""Tests for Reddit collector."""
+
import pytest
from unittest.mock import AsyncMock, patch
from news_collector.collectors.reddit import RedditCollector
+
@pytest.fixture
def collector():
return RedditCollector()
+
def test_collector_name(collector):
assert collector.name == "reddit"
assert collector.poll_interval == 900
+
async def test_is_available(collector):
assert await collector.is_available() is True
+
async def test_collect_parses_posts(collector):
mock_posts = [
- {"data": {"title": "NVDA to the moon! AI demand is insane", "selftext": "Just loaded up on NVDA calls", "url": "https://reddit.com/r/wallstreetbets/123", "created_utc": 1711929600, "score": 500, "num_comments": 200, "subreddit": "wallstreetbets"}},
+ {
+ "data": {
+ "title": "NVDA to the moon! AI demand is insane",
+ "selftext": "Just loaded up on NVDA calls",
+ "url": "https://reddit.com/r/wallstreetbets/123",
+ "created_utc": 1711929600,
+ "score": 500,
+ "num_comments": 200,
+ "subreddit": "wallstreetbets",
+ }
+ },
]
- with patch.object(collector, "_fetch_subreddit", new_callable=AsyncMock, return_value=mock_posts):
+ with patch.object(
+ collector, "_fetch_subreddit", new_callable=AsyncMock, return_value=mock_posts
+ ):
items = await collector.collect()
assert len(items) >= 1
assert items[0].source == "reddit"
assert items[0].category.value == "social"
+
async def test_collect_filters_low_score(collector):
mock_posts = [
- {"data": {"title": "Random question", "selftext": "", "url": "https://reddit.com/456", "created_utc": 1711929600, "score": 3, "num_comments": 1, "subreddit": "stocks"}},
+ {
+ "data": {
+ "title": "Random question",
+ "selftext": "",
+ "url": "https://reddit.com/456",
+ "created_utc": 1711929600,
+ "score": 3,
+ "num_comments": 1,
+ "subreddit": "stocks",
+ }
+ },
]
- with patch.object(collector, "_fetch_subreddit", new_callable=AsyncMock, return_value=mock_posts):
+ with patch.object(
+ collector, "_fetch_subreddit", new_callable=AsyncMock, return_value=mock_posts
+ ):
items = await collector.collect()
assert items == []
diff --git a/services/news-collector/tests/test_rss.py b/services/news-collector/tests/test_rss.py
index 58c5f7c..e03250a 100644
--- a/services/news-collector/tests/test_rss.py
+++ b/services/news-collector/tests/test_rss.py
@@ -2,7 +2,6 @@
import pytest
from unittest.mock import AsyncMock, patch
-from datetime import datetime, timezone
from news_collector.collectors.rss import RSSCollector
diff --git a/services/news-collector/tests/test_sec_edgar.py b/services/news-collector/tests/test_sec_edgar.py
index a10b47a..5d4f69f 100644
--- a/services/news-collector/tests/test_sec_edgar.py
+++ b/services/news-collector/tests/test_sec_edgar.py
@@ -40,7 +40,9 @@ async def test_collect_parses_filings(collector):
mock_datetime.now.return_value = datetime(2026, 4, 2, tzinfo=timezone.utc)
mock_datetime.strptime = datetime.strptime
- with patch.object(collector, "_fetch_recent_filings", new_callable=AsyncMock, return_value=[mock_response]):
+ with patch.object(
+ collector, "_fetch_recent_filings", new_callable=AsyncMock, return_value=[mock_response]
+ ):
with patch("news_collector.collectors.sec_edgar.datetime", mock_datetime):
items = await collector.collect()
diff --git a/services/news-collector/tests/test_truth_social.py b/services/news-collector/tests/test_truth_social.py
index bcf8a8c..91ddb9d 100644
--- a/services/news-collector/tests/test_truth_social.py
+++ b/services/news-collector/tests/test_truth_social.py
@@ -1,22 +1,32 @@
"""Tests for Truth Social collector."""
+
import pytest
from unittest.mock import AsyncMock, patch
from news_collector.collectors.truth_social import TruthSocialCollector
+
@pytest.fixture
def collector():
return TruthSocialCollector()
+
def test_collector_name(collector):
assert collector.name == "truth_social"
assert collector.poll_interval == 900
+
async def test_is_available(collector):
assert await collector.is_available() is True
+
async def test_collect_parses_posts(collector):
mock_posts = [
- {"content": "<p>We are imposing 25% tariffs on all steel imports!</p>", "created_at": "2026-04-02T12:00:00.000Z", "url": "https://truthsocial.com/@realDonaldTrump/12345", "id": "12345"},
+ {
+ "content": "<p>We are imposing 25% tariffs on all steel imports!</p>",
+ "created_at": "2026-04-02T12:00:00.000Z",
+ "url": "https://truthsocial.com/@realDonaldTrump/12345",
+ "id": "12345",
+ },
]
with patch.object(collector, "_fetch_posts", new_callable=AsyncMock, return_value=mock_posts):
items = await collector.collect()
@@ -24,6 +34,7 @@ async def test_collect_parses_posts(collector):
assert items[0].source == "truth_social"
assert items[0].category.value == "policy"
+
async def test_collect_handles_empty(collector):
with patch.object(collector, "_fetch_posts", new_callable=AsyncMock, return_value=[]):
items = await collector.collect()
diff --git a/services/strategy-engine/src/strategy_engine/main.py b/services/strategy-engine/src/strategy_engine/main.py
index e9c96b2..5a30766 100644
--- a/services/strategy-engine/src/strategy_engine/main.py
+++ b/services/strategy-engine/src/strategy_engine/main.py
@@ -126,9 +126,9 @@ async def run() -> None:
anthropic_model=config.anthropic_model,
max_picks=config.selector_max_picks,
)
- tasks.append(asyncio.create_task(
- run_stock_selector(selector, notifier, db, config, log)
- ))
+ tasks.append(
+ asyncio.create_task(run_stock_selector(selector, notifier, db, config, log))
+ )
log.info("stock_selector_enabled", time=config.selector_final_time)
await asyncio.gather(*tasks)
diff --git a/services/strategy-engine/src/strategy_engine/stock_selector.py b/services/strategy-engine/src/strategy_engine/stock_selector.py
index e1f2fe7..268d557 100644
--- a/services/strategy-engine/src/strategy_engine/stock_selector.py
+++ b/services/strategy-engine/src/strategy_engine/stock_selector.py
@@ -4,17 +4,14 @@ import json
import logging
import re
from datetime import datetime, timezone
-from decimal import Decimal
-from typing import Optional
import aiohttp
-import pandas as pd
from shared.alpaca import AlpacaClient
from shared.broker import RedisBroker
from shared.db import Database
from shared.models import OrderSide
-from shared.sentiment_models import Candidate, MarketSentiment, SelectedStock, SymbolScore
+from shared.sentiment_models import Candidate, MarketSentiment, SelectedStock
logger = logging.getLogger(__name__)
@@ -325,7 +322,9 @@ class StockSelector:
ema20 = sum(closes[-20:]) / 20 # simple approximation
current_price = closes[-1]
if current_price <= ema20:
- logger.debug("%s price %.2f <= EMA20 %.2f", candidate.symbol, current_price, ema20)
+ logger.debug(
+ "%s price %.2f <= EMA20 %.2f", candidate.symbol, current_price, ema20
+ )
continue
avg_volume = sum(volumes[:-1]) / max(len(volumes) - 1, 1)
@@ -333,7 +332,9 @@ class StockSelector:
if current_volume <= 0.5 * avg_volume:
logger.debug(
"%s volume %.0f <= 50%% avg %.0f",
- candidate.symbol, current_volume, avg_volume,
+ candidate.symbol,
+ current_volume,
+ avg_volume,
)
continue
diff --git a/services/strategy-engine/tests/test_stock_selector.py b/services/strategy-engine/tests/test_stock_selector.py
index a2f5bca..ff9d09c 100644
--- a/services/strategy-engine/tests/test_stock_selector.py
+++ b/services/strategy-engine/tests/test_stock_selector.py
@@ -1,12 +1,8 @@
"""Tests for stock selector engine."""
-import pytest
-from unittest.mock import AsyncMock, MagicMock, patch
+from unittest.mock import AsyncMock, MagicMock
from datetime import datetime, timezone
-from decimal import Decimal
-from shared.models import OrderSide
-from shared.sentiment_models import SymbolScore, MarketSentiment, SelectedStock, Candidate
from strategy_engine.stock_selector import (
SentimentCandidateSource,
@@ -17,10 +13,12 @@ from strategy_engine.stock_selector import (
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},
- ])
+ 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()
@@ -64,15 +62,19 @@ def test_parse_llm_selections_with_markdown():
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(timezone.utc),
- })
-
- selector = StockSelector(db=mock_db, broker=MagicMock(), alpaca=MagicMock(), anthropic_api_key="test")
+ 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(timezone.utc),
+ }
+ )
+
+ selector = StockSelector(
+ db=mock_db, broker=MagicMock(), alpaca=MagicMock(), anthropic_api_key="test"
+ )
result = await selector.select()
assert result == []
diff --git a/shared/alembic/versions/002_news_sentiment_tables.py b/shared/alembic/versions/002_news_sentiment_tables.py
index b57f1d6..402ff87 100644
--- a/shared/alembic/versions/002_news_sentiment_tables.py
+++ b/shared/alembic/versions/002_news_sentiment_tables.py
@@ -29,7 +29,9 @@ def upgrade() -> None:
sa.Column("sentiment", sa.Float, nullable=False),
sa.Column("category", sa.Text, nullable=False),
sa.Column("raw_data", sa.Text),
- sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
+ sa.Column(
+ "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()
+ ),
)
op.create_index("idx_news_items_published", "news_items", ["published_at"])
op.create_index("idx_news_items_source", "news_items", ["source"])
@@ -68,7 +70,9 @@ def upgrade() -> None:
sa.Column("reason", sa.Text, nullable=False),
sa.Column("key_news", sa.Text),
sa.Column("sentiment_snapshot", sa.Text),
- sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
+ sa.Column(
+ "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()
+ ),
)
op.create_index("idx_stock_selections_date", "stock_selections", ["trade_date"])
diff --git a/shared/src/shared/db.py b/shared/src/shared/db.py
index 55f93b4..9cc8686 100644
--- a/shared/src/shared/db.py
+++ b/shared/src/shared/db.py
@@ -299,11 +299,7 @@ class Database:
async def get_top_symbol_scores(self, limit: int = 20) -> list[dict]:
"""Retrieve top symbol scores ordered by composite descending."""
- stmt = (
- select(SymbolScoreRow)
- .order_by(SymbolScoreRow.composite.desc())
- .limit(limit)
- )
+ stmt = select(SymbolScoreRow).order_by(SymbolScoreRow.composite.desc()).limit(limit)
async with self._session_factory() as session:
try:
result = await session.execute(stmt)
@@ -431,7 +427,9 @@ class Database:
"conviction": r.conviction,
"reason": r.reason,
"key_news": json.loads(r.key_news) if r.key_news else [],
- "sentiment_snapshot": json.loads(r.sentiment_snapshot) if r.sentiment_snapshot else {},
+ "sentiment_snapshot": json.loads(r.sentiment_snapshot)
+ if r.sentiment_snapshot
+ else {},
"created_at": r.created_at,
}
for r in rows
diff --git a/shared/src/shared/notifier.py b/shared/src/shared/notifier.py
index 9630a18..3d7b6cf 100644
--- a/shared/src/shared/notifier.py
+++ b/shared/src/shared/notifier.py
@@ -137,8 +137,7 @@ class TelegramNotifier:
for i, s in enumerate(selections, 1):
emoji = side_emoji.get(s.side.value, "⚪")
lines.append(
- f"{i}. <b>{s.symbol}</b> {emoji} {s.side.value} "
- f"(conviction: {s.conviction:.0%})"
+ f"{i}. <b>{s.symbol}</b> {emoji} {s.side.value} (conviction: {s.conviction:.0%})"
)
lines.append(f" {s.reason}")
if s.key_news:
diff --git a/shared/src/shared/sentiment.py b/shared/src/shared/sentiment.py
index a20227e..449eb76 100644
--- a/shared/src/shared/sentiment.py
+++ b/shared/src/shared/sentiment.py
@@ -2,7 +2,7 @@
import logging
from dataclasses import dataclass, field
-from datetime import datetime, timedelta, timezone
+from datetime import datetime, timezone
from shared.sentiment_models import SymbolScore
@@ -82,9 +82,7 @@ class SentimentAggregator:
+ filing_score * self.WEIGHTS["filing"]
)
- def aggregate(
- self, news_items: list[dict], now: datetime
- ) -> dict[str, SymbolScore]:
+ def aggregate(self, news_items: list[dict], now: datetime) -> dict[str, SymbolScore]:
"""Aggregate news items into per-symbol scores.
Each dict needs: symbols, sentiment, category, published_at.
diff --git a/shared/tests/test_db_news.py b/shared/tests/test_db_news.py
index f13cf1e..a2c9140 100644
--- a/shared/tests/test_db_news.py
+++ b/shared/tests/test_db_news.py
@@ -1,7 +1,5 @@
"""Tests for database news/sentiment methods. Uses in-memory SQLite."""
-import json
-import uuid
import pytest
from datetime import datetime, date, timezone
diff --git a/shared/tests/test_sentiment_aggregator.py b/shared/tests/test_sentiment_aggregator.py
index f9277e7..a99c711 100644
--- a/shared/tests/test_sentiment_aggregator.py
+++ b/shared/tests/test_sentiment_aggregator.py
@@ -1,4 +1,5 @@
"""Tests for sentiment aggregator."""
+
import pytest
from datetime import datetime, timezone, timedelta
from shared.sentiment import SentimentAggregator
@@ -35,7 +36,9 @@ def test_freshness_decay_old():
def test_compute_composite():
a = SentimentAggregator()
- composite = a._compute_composite(news_score=0.5, social_score=0.3, policy_score=0.8, filing_score=0.2)
+ composite = a._compute_composite(
+ news_score=0.5, social_score=0.3, policy_score=0.8, filing_score=0.2
+ )
expected = 0.5 * 0.3 + 0.3 * 0.2 + 0.8 * 0.3 + 0.2 * 0.2
assert abs(composite - expected) < 0.001
@@ -44,7 +47,12 @@ def test_aggregate_news_by_symbol(aggregator):
now = datetime.now(timezone.utc)
news_items = [
{"symbols": ["AAPL"], "sentiment": 0.8, "category": "earnings", "published_at": now},
- {"symbols": ["AAPL"], "sentiment": 0.3, "category": "macro", "published_at": now - timedelta(hours=2)},
+ {
+ "symbols": ["AAPL"],
+ "sentiment": 0.3,
+ "category": "macro",
+ "published_at": now - timedelta(hours=2),
+ },
{"symbols": ["MSFT"], "sentiment": -0.5, "category": "policy", "published_at": now},
]
scores = aggregator.aggregate(news_items, now)
diff --git a/shared/tests/test_sentiment_models.py b/shared/tests/test_sentiment_models.py
index 74f1acd..25fc371 100644
--- a/shared/tests/test_sentiment_models.py
+++ b/shared/tests/test_sentiment_models.py
@@ -1,6 +1,5 @@
"""Tests for news and sentiment models."""
-import pytest
from datetime import datetime, timezone
from shared.models import NewsCategory, NewsItem, OrderSide