"""Seed default trading strategies. Inserts nine strategies with equal initial weights (~0.111 each): - momentum - mean_reversion - news_driven - value - macd_crossover - bollinger_breakout - vwap - liquidity - ma_stack Usage: python -m scripts.seed_strategies """ from __future__ import annotations import asyncio import logging from sqlalchemy import select from shared.config import BaseConfig from shared.db import create_db from shared.models.trading import Strategy logger = logging.getLogger(__name__) # Default strategies to seed DEFAULT_STRATEGIES = [ { "name": "momentum", "description": ( "Buy when price crosses above N-period SMA with increasing volume; " "sell when it crosses below." ), "current_weight": 0.111, "active": True, }, { "name": "mean_reversion", "description": ( "Buy when RSI < 30 (oversold); sell when RSI > 70 (overbought). " "Signal strength proportional to RSI extremity." ), "current_weight": 0.111, "active": True, }, { "name": "news_driven", "description": ( "Buy on strong positive sentiment (score > 0.7, confidence > 0.6); " "sell on strong negative. Decay factor for stale news (> 4 hours)." ), "current_weight": 0.111, "active": True, }, { "name": "value", "description": ( "Fundamental valuation: LONG when PEG < 1.0 and P/E < 25 with positive " "EPS growth; SHORT when PEG > 3.0 or P/E > 50 with negative growth." ), "current_weight": 0.111, "active": True, }, { "name": "macd_crossover", "description": ( "MACD/signal line crossover: LONG on bullish crossover (MACD crosses above " "signal), SHORT on bearish. Strength from histogram magnitude." ), "current_weight": 0.111, "active": True, }, { "name": "bollinger_breakout", "description": ( "Bollinger Band breakout: LONG on upper band break with high volume (momentum) " "or below lower band (mean reversion). SHORT on failed breakout." ), "current_weight": 0.111, "active": True, }, { "name": "vwap", "description": ( "VWAP crossover: LONG when price crosses above VWAP with increasing volume, " "SHORT when below." ), "current_weight": 0.111, "active": True, }, { "name": "liquidity", "description": ( "Volume-based: LONG on high relative volume (>2x) with rising price, " "SHORT on high volume with falling price or bearish divergence." ), "current_weight": 0.112, "active": True, }, { "name": "ma_stack", "description": ( "Moving average alignment: LONG when price > EMA-9 > EMA-21 > SMA-50 > " "SMA-200 (full bull stack). Golden/death cross detection." ), "current_weight": 0.111, "active": True, }, ] async def seed(database_url: str | None = None) -> None: """Insert default strategies if they do not already exist. Parameters ---------- database_url: Override for the database URL. If ``None``, the default from :class:`~shared.config.BaseConfig` is used. """ config = BaseConfig() if database_url: config.database_url = database_url _engine, session_factory = create_db(config) async with session_factory() as session: for strategy_data in DEFAULT_STRATEGIES: # Check if the strategy already exists by name result = await session.execute( select(Strategy).where(Strategy.name == strategy_data["name"]) ) existing = result.scalar_one_or_none() if existing: logger.info( "Strategy '%s' already exists (weight=%.3f), skipping", existing.name, existing.current_weight, ) continue strategy = Strategy(**strategy_data) session.add(strategy) logger.info( "Inserted strategy '%s' with weight %.3f", strategy_data["name"], strategy_data["current_weight"], ) await session.commit() await _engine.dispose() logger.info("Strategy seeding complete") def main() -> None: """CLI entry-point.""" logging.basicConfig(level=logging.INFO) asyncio.run(seed()) if __name__ == "__main__": main()