- Point Ollama to local instance via host.docker.internal, use gemma3 model - Remove Docker Ollama service (using host's Ollama instead) - Add company-name-to-ticker mapping (Apple→AAPL, Tesla→TSLA, etc.) for RSS articles - Lower signal thresholds for faster feedback with paper trading: - FinBERT confidence: 0.6→0.4, signal strength: 0.3→0.15 - News strategy: article_count 2→1, confidence 0.5→0.3, score ±0.3→±0.15 - Fix market data BarSet access bug (BarSet.__contains__ returns False incorrectly) - Fix market data SIP feed error by switching to IEX feed for free Alpaca accounts - Fix nginx proxy routing for /api/auth/* to api-gateway /auth/* - Add seed_sample_data script - Update tests for new thresholds and alpaca mock modules
60 lines
1.7 KiB
Python
60 lines
1.7 KiB
Python
"""News-driven strategy — trade on aggregated news sentiment."""
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
from shared.schemas.trading import MarketSnapshot, SentimentContext, SignalDirection, TradeSignal
|
|
from shared.strategies.base import BaseStrategy
|
|
|
|
|
|
class NewsDrivenStrategy(BaseStrategy):
|
|
"""Generate signals from aggregated news sentiment for a ticker.
|
|
|
|
**Buy signal** (LONG):
|
|
``avg_score > 0.15`` AND ``avg_confidence > 0.3`` AND
|
|
``article_count >= 1``.
|
|
|
|
**Sell signal** (SHORT):
|
|
``avg_score < -0.15`` AND ``avg_confidence > 0.3`` AND
|
|
``article_count >= 1``.
|
|
|
|
Signal strength = ``abs(avg_score) * avg_confidence``, clamped to
|
|
[0, 1].
|
|
"""
|
|
|
|
name: str = "news_driven"
|
|
|
|
async def evaluate(
|
|
self,
|
|
ticker: str,
|
|
market: MarketSnapshot,
|
|
sentiment: SentimentContext | None = None,
|
|
) -> TradeSignal | None:
|
|
if sentiment is None:
|
|
return None
|
|
|
|
# Require at least 1 article.
|
|
if sentiment.article_count < 1:
|
|
return None
|
|
|
|
# Require minimum confidence.
|
|
if sentiment.avg_confidence <= 0.3:
|
|
return None
|
|
|
|
if sentiment.avg_score > 0.15:
|
|
direction = SignalDirection.LONG
|
|
elif sentiment.avg_score < -0.15:
|
|
direction = SignalDirection.SHORT
|
|
else:
|
|
# Sentiment is neutral — no opinion.
|
|
return None
|
|
|
|
raw_strength = abs(sentiment.avg_score) * sentiment.avg_confidence
|
|
strength = max(0.0, min(1.0, raw_strength))
|
|
|
|
return TradeSignal(
|
|
ticker=ticker,
|
|
direction=direction,
|
|
strength=strength,
|
|
strategy_sources=[self.name],
|
|
timestamp=datetime.now(tz=timezone.utc),
|
|
)
|