77 lines
2.4 KiB
Python
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),
|
|
)
|