feat: split portfolio and opportunity decision models
This commit is contained in:
78
src/coinhunter/services/signal_service.py
Normal file
78
src/coinhunter/services/signal_service.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""Shared market signal scoring."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from statistics import mean
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _safe_pct(new: float, old: float) -> float:
|
||||
if old == 0:
|
||||
return 0.0
|
||||
return (new - old) / old
|
||||
|
||||
|
||||
def get_signal_weights(config: dict[str, Any]) -> dict[str, float]:
|
||||
signal_config = config.get("signal", {})
|
||||
return {
|
||||
"trend": float(signal_config.get("trend", 1.0)),
|
||||
"momentum": float(signal_config.get("momentum", 1.0)),
|
||||
"breakout": float(signal_config.get("breakout", 0.8)),
|
||||
"volume": float(signal_config.get("volume", 0.7)),
|
||||
"volatility_penalty": float(signal_config.get("volatility_penalty", 0.5)),
|
||||
}
|
||||
|
||||
|
||||
def get_signal_interval(config: dict[str, Any]) -> str:
|
||||
signal_config = config.get("signal", {})
|
||||
if signal_config.get("lookback_interval"):
|
||||
return str(signal_config["lookback_interval"])
|
||||
return "1h"
|
||||
|
||||
|
||||
def score_market_signal(
|
||||
closes: list[float],
|
||||
volumes: list[float],
|
||||
ticker: dict[str, Any],
|
||||
weights: dict[str, float],
|
||||
) -> tuple[float, dict[str, float]]:
|
||||
if len(closes) < 2 or not volumes:
|
||||
return 0.0, {
|
||||
"trend": 0.0,
|
||||
"momentum": 0.0,
|
||||
"breakout": 0.0,
|
||||
"volume_confirmation": 1.0,
|
||||
"volatility": 0.0,
|
||||
}
|
||||
|
||||
current = closes[-1]
|
||||
sma_short = mean(closes[-5:]) if len(closes) >= 5 else current
|
||||
sma_long = mean(closes[-20:]) if len(closes) >= 20 else mean(closes)
|
||||
trend = 1.0 if current >= sma_short >= sma_long else -1.0 if current < sma_short < sma_long else 0.0
|
||||
momentum = (
|
||||
_safe_pct(closes[-1], closes[-2]) * 0.5
|
||||
+ (_safe_pct(closes[-1], closes[-5]) * 0.3 if len(closes) >= 5 else 0.0)
|
||||
+ float(ticker.get("price_change_pct", 0.0)) / 100.0 * 0.2
|
||||
)
|
||||
recent_high = max(closes[-20:]) if len(closes) >= 20 else max(closes)
|
||||
breakout = 1.0 - max((recent_high - current) / recent_high, 0.0)
|
||||
avg_volume = mean(volumes[:-1]) if len(volumes) > 1 else volumes[-1]
|
||||
volume_confirmation = volumes[-1] / avg_volume if avg_volume else 1.0
|
||||
volume_score = min(max(volume_confirmation - 1.0, -1.0), 2.0)
|
||||
volatility = (max(closes[-10:]) - min(closes[-10:])) / current if len(closes) >= 10 and current else 0.0
|
||||
|
||||
score = (
|
||||
weights.get("trend", 1.0) * trend
|
||||
+ weights.get("momentum", 1.0) * momentum
|
||||
+ weights.get("breakout", 0.8) * breakout
|
||||
+ weights.get("volume", 0.7) * volume_score
|
||||
- weights.get("volatility_penalty", 0.5) * volatility
|
||||
)
|
||||
metrics = {
|
||||
"trend": round(trend, 4),
|
||||
"momentum": round(momentum, 4),
|
||||
"breakout": round(breakout, 4),
|
||||
"volume_confirmation": round(volume_confirmation, 4),
|
||||
"volatility": round(volatility, 4),
|
||||
}
|
||||
return score, metrics
|
||||
Reference in New Issue
Block a user