"""Liquidity strategy — trade on volume anomalies and volume-price divergence.""" from datetime import datetime, timezone from shared.schemas.trading import MarketSnapshot, SentimentContext, SignalDirection, TradeSignal from shared.strategies.base import BaseStrategy class LiquidityStrategy(BaseStrategy): """Generate signals based on relative volume and volume-price relationships. Requires at least 5 bars of historical data to compute average volume. **No signal** if relative_volume < 1.0 (thin liquidity). **Buy signal** (LONG): relative_volume >= 2.0 AND price is rising (close > open). **Sell signal** (SHORT): - relative_volume >= 2.0 AND price is falling (close < open), or - Price is rising on declining volume (relative_volume < 0.7) — bearish divergence. Signal strength = ``relative_volume / 4.0``, clamped to [0, 1]. """ name: str = "liquidity" async def evaluate( self, ticker: str, market: MarketSnapshot, sentiment: SentimentContext | None = None, ) -> TradeSignal | None: if len(market.bars) < 5: return None # Compute average volume from bars. volumes = [b.get("volume", 0) for b in market.bars if "volume" in b] if not volumes: return None avg_volume = sum(volumes) / len(volumes) if avg_volume <= 0: return None relative_volume = market.volume / avg_volume price_rising = market.close > market.open direction: SignalDirection | None = None # Bearish divergence: price rising on declining volume. if price_rising and relative_volume < 0.7: direction = SignalDirection.SHORT elif relative_volume < 1.0: # Thin liquidity — no signal. return None elif relative_volume >= 2.0: if price_rising: direction = SignalDirection.LONG elif market.close < market.open: direction = SignalDirection.SHORT else: return None else: return None raw_strength = relative_volume / 4.0 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), )