feat: wire 6 new strategies and fundamentals into signal generator
This commit is contained in:
parent
4d6bebe6f7
commit
b8eaa20d63
5 changed files with 169 additions and 11 deletions
|
|
@ -10,5 +10,8 @@ class SignalGeneratorConfig(BaseConfig):
|
|||
alpaca_secret_key: str = ""
|
||||
signal_strength_threshold: float = 0.15
|
||||
watchlist: list[str] = []
|
||||
alpha_vantage_api_key: str = ""
|
||||
fmp_api_key: str = ""
|
||||
fundamentals_cache_ttl_hours: int = 24
|
||||
|
||||
model_config = {"env_prefix": "TRADING_"}
|
||||
|
|
|
|||
|
|
@ -25,8 +25,23 @@ from shared.models.trading import Signal as SignalModel
|
|||
from shared.models.trading import SignalDirection as SignalDirectionModel
|
||||
from shared.redis_streams import StreamConsumer, StreamPublisher
|
||||
from shared.schemas.news import ScoredArticle
|
||||
from shared.schemas.trading import MarketSnapshot, SentimentContext
|
||||
from shared.strategies import MeanReversionStrategy, MomentumStrategy, NewsDrivenStrategy
|
||||
from shared.schemas.trading import FundamentalsSnapshot, MarketSnapshot, SentimentContext
|
||||
from shared.strategies import (
|
||||
BollingerBreakoutStrategy,
|
||||
LiquidityStrategy,
|
||||
MACDCrossoverStrategy,
|
||||
MAStackStrategy,
|
||||
MeanReversionStrategy,
|
||||
MomentumStrategy,
|
||||
NewsDrivenStrategy,
|
||||
ValueStrategy,
|
||||
VWAPStrategy,
|
||||
)
|
||||
from shared.fundamentals.alpha_vantage import AlphaVantageProvider
|
||||
from shared.fundamentals.fmp import FMPProvider
|
||||
from shared.fundamentals.yahoo import YahooFinanceProvider
|
||||
from shared.fundamentals.rotating import RotatingProvider
|
||||
from shared.fundamentals.cache import CachedFundamentalsProvider
|
||||
from shared.telemetry import setup_telemetry
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -36,9 +51,15 @@ _MAX_SENTIMENT_SCORES = 50
|
|||
|
||||
# Default strategy weights (equal weighting)
|
||||
_DEFAULT_WEIGHTS: dict[str, float] = {
|
||||
"momentum": 0.333,
|
||||
"mean_reversion": 0.333,
|
||||
"news_driven": 0.334,
|
||||
"momentum": 0.111,
|
||||
"mean_reversion": 0.111,
|
||||
"news_driven": 0.111,
|
||||
"value": 0.111,
|
||||
"macd_crossover": 0.111,
|
||||
"bollinger_breakout": 0.111,
|
||||
"vwap": 0.111,
|
||||
"liquidity": 0.112,
|
||||
"ma_stack": 0.111,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -98,6 +119,7 @@ async def _consume_scored_articles(
|
|||
signals_generated,
|
||||
per_strategy_signal_count,
|
||||
db_session_factory: async_sessionmaker | None = None,
|
||||
fundamentals_cache: dict[str, FundamentalsSnapshot] | None = None,
|
||||
) -> None:
|
||||
"""Consume scored articles from ``news:scored``, run the ensemble, and publish signals.
|
||||
|
||||
|
|
@ -145,6 +167,10 @@ async def _consume_scored_articles(
|
|||
volume=0.0,
|
||||
)
|
||||
|
||||
# Inject fundamentals into snapshot
|
||||
if fundamentals_cache:
|
||||
snapshot.fundamentals = fundamentals_cache.get(ticker)
|
||||
|
||||
# Run ensemble
|
||||
signal_result = await ensemble.evaluate(ticker, snapshot, sentiment, weights)
|
||||
|
||||
|
|
@ -197,6 +223,30 @@ async def _consume_scored_articles(
|
|||
)
|
||||
|
||||
|
||||
async def _refresh_fundamentals(
|
||||
provider: CachedFundamentalsProvider,
|
||||
cache: dict[str, FundamentalsSnapshot],
|
||||
watchlist: list[str],
|
||||
shutdown_event: asyncio.Event,
|
||||
) -> None:
|
||||
"""Periodically refresh fundamental data for all watchlist tickers."""
|
||||
while not shutdown_event.is_set():
|
||||
await asyncio.sleep(3600 * 24) # 24 hours
|
||||
if shutdown_event.is_set():
|
||||
break
|
||||
logger.info("Starting daily fundamentals refresh")
|
||||
for ticker in watchlist:
|
||||
if shutdown_event.is_set():
|
||||
break
|
||||
try:
|
||||
snap = await provider.fetch(ticker)
|
||||
if snap:
|
||||
cache[ticker] = snap
|
||||
except Exception:
|
||||
logger.exception("Failed to refresh fundamentals for %s", ticker)
|
||||
logger.info("Fundamentals refresh complete")
|
||||
|
||||
|
||||
async def run(config: SignalGeneratorConfig | None = None) -> None:
|
||||
"""Main service loop.
|
||||
|
||||
|
|
@ -243,6 +293,12 @@ async def run(config: SignalGeneratorConfig | None = None) -> None:
|
|||
MomentumStrategy(),
|
||||
MeanReversionStrategy(),
|
||||
NewsDrivenStrategy(),
|
||||
ValueStrategy(),
|
||||
MACDCrossoverStrategy(),
|
||||
BollingerBreakoutStrategy(),
|
||||
VWAPStrategy(),
|
||||
LiquidityStrategy(),
|
||||
MAStackStrategy(),
|
||||
]
|
||||
ensemble = WeightedEnsemble(strategies, threshold=config.signal_strength_threshold)
|
||||
|
||||
|
|
@ -257,6 +313,35 @@ async def run(config: SignalGeneratorConfig | None = None) -> None:
|
|||
except Exception:
|
||||
logger.exception("Failed to initialise DB — signals will NOT be persisted")
|
||||
|
||||
# --- Fundamentals ---
|
||||
fundamentals_cache: dict[str, FundamentalsSnapshot] = {}
|
||||
cached_fundamentals_provider: CachedFundamentalsProvider | None = None
|
||||
try:
|
||||
providers = []
|
||||
if config.alpha_vantage_api_key:
|
||||
providers.append(AlphaVantageProvider(api_key=config.alpha_vantage_api_key))
|
||||
if config.fmp_api_key:
|
||||
providers.append(FMPProvider(api_key=config.fmp_api_key))
|
||||
providers.append(YahooFinanceProvider()) # no API key needed
|
||||
|
||||
if providers and db_session_factory is not None:
|
||||
rotating = RotatingProvider(providers)
|
||||
cached_fundamentals_provider = CachedFundamentalsProvider(
|
||||
rotating, db_session_factory, cache_ttl_hours=config.fundamentals_cache_ttl_hours,
|
||||
)
|
||||
# Pre-fetch fundamentals for watchlist
|
||||
for ticker in config.watchlist:
|
||||
try:
|
||||
snap = await cached_fundamentals_provider.fetch(ticker)
|
||||
if snap:
|
||||
fundamentals_cache[ticker] = snap
|
||||
logger.info("Loaded fundamentals for %s", ticker)
|
||||
except Exception:
|
||||
logger.exception("Failed to fetch fundamentals for %s", ticker)
|
||||
logger.info("Fundamentals loaded for %d/%d tickers", len(fundamentals_cache), len(config.watchlist))
|
||||
except Exception:
|
||||
logger.exception("Failed to initialise fundamentals — strategies will run without fundamental data")
|
||||
|
||||
logger.info(
|
||||
"Consuming from news:scored and market:bars, publishing to signals:generated"
|
||||
)
|
||||
|
|
@ -281,6 +366,7 @@ async def run(config: SignalGeneratorConfig | None = None) -> None:
|
|||
signals_generated,
|
||||
per_strategy_signal_count,
|
||||
db_session_factory,
|
||||
fundamentals_cache,
|
||||
)
|
||||
)
|
||||
tg.create_task(
|
||||
|
|
@ -291,6 +377,15 @@ async def run(config: SignalGeneratorConfig | None = None) -> None:
|
|||
bars_received_counter,
|
||||
)
|
||||
)
|
||||
if cached_fundamentals_provider is not None:
|
||||
tg.create_task(
|
||||
_refresh_fundamentals(
|
||||
cached_fundamentals_provider,
|
||||
fundamentals_cache,
|
||||
config.watchlist,
|
||||
shutdown_event,
|
||||
)
|
||||
)
|
||||
finally:
|
||||
await redis.aclose()
|
||||
logger.info("Signal generator stopped gracefully")
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from shared.schemas.trading import MarketSnapshot, OHLCVBar
|
|||
|
||||
|
||||
# Default rolling-window sizes
|
||||
_DEFAULT_MAX_BARS = 100
|
||||
_DEFAULT_MAX_BARS = 250
|
||||
_RSI_PERIOD = 14
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue