trading/shared/strategies/liquidity.py

77 lines
2.4 KiB
Python

"""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),
)