refactor: split precheck_core and migrate commands to commands/
- Split 900-line precheck_core.py into 9 focused modules: precheck_constants, time_utils, data_utils, state_manager, market_data, candidate_scoring, snapshot_builder, adaptive_profile, trigger_analyzer - Remove dead auto_trader command and module - Migrate 7 root-level command modules into commands/: check_api, doctor, external_gate, init_user_state, market_probe, paths, rotate_external_gate_log - Keep thin backward-compatible facades in root package - Update cli.py MODULE_MAP to route through commands/ - Verified compileall and smoke tests for all key commands
This commit is contained in:
136
src/coinhunter/services/market_data.py
Normal file
136
src/coinhunter/services/market_data.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""Market data fetching and metric computation for precheck."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import ccxt
|
||||
|
||||
from .data_utils import norm_symbol, to_float
|
||||
from .precheck_constants import BLACKLIST, MAX_PRICE_CAP, MIN_CHANGE_PCT
|
||||
from .time_utils import utc_now
|
||||
|
||||
|
||||
def get_exchange():
|
||||
from ..runtime import load_env_file
|
||||
from .precheck_constants import ENV_FILE
|
||||
|
||||
load_env_file(ENV_FILE)
|
||||
api_key = os.getenv("BINANCE_API_KEY")
|
||||
secret = os.getenv("BINANCE_API_SECRET")
|
||||
if not api_key or not secret:
|
||||
raise RuntimeError("Missing BINANCE_API_KEY or BINANCE_API_SECRET in ~/.hermes/.env")
|
||||
ex = ccxt.binance({
|
||||
"apiKey": api_key,
|
||||
"secret": secret,
|
||||
"options": {"defaultType": "spot"},
|
||||
"enableRateLimit": True,
|
||||
})
|
||||
ex.load_markets()
|
||||
return ex
|
||||
|
||||
|
||||
def fetch_ohlcv_batch(ex, symbols: set, timeframe: str, limit: int):
|
||||
results = {}
|
||||
for sym in sorted(symbols):
|
||||
try:
|
||||
ohlcv = ex.fetch_ohlcv(sym, timeframe=timeframe, limit=limit)
|
||||
if ohlcv and len(ohlcv) >= 2:
|
||||
results[sym] = ohlcv
|
||||
except Exception:
|
||||
pass
|
||||
return results
|
||||
|
||||
|
||||
def compute_ohlcv_metrics(ohlcv_1h, ohlcv_4h, current_price, volume_24h=None):
|
||||
metrics = {}
|
||||
if ohlcv_1h and len(ohlcv_1h) >= 2:
|
||||
closes = [c[4] for c in ohlcv_1h]
|
||||
volumes = [c[5] for c in ohlcv_1h]
|
||||
metrics["change_1h_pct"] = round((closes[-1] - closes[-2]) / closes[-2] * 100, 2) if closes[-2] != 0 else None
|
||||
if len(closes) >= 5:
|
||||
metrics["change_4h_pct"] = round((closes[-1] - closes[-5]) / closes[-5] * 100, 2) if closes[-5] != 0 else None
|
||||
recent_vol = sum(volumes[-4:]) / 4 if len(volumes) >= 4 else None
|
||||
metrics["volume_1h_avg"] = round(recent_vol, 2) if recent_vol else None
|
||||
highs = [c[2] for c in ohlcv_1h[-4:]]
|
||||
lows = [c[3] for c in ohlcv_1h[-4:]]
|
||||
metrics["high_4h"] = round(max(highs), 8) if highs else None
|
||||
metrics["low_4h"] = round(min(lows), 8) if lows else None
|
||||
|
||||
if ohlcv_4h and len(ohlcv_4h) >= 2:
|
||||
closes_4h = [c[4] for c in ohlcv_4h]
|
||||
volumes_4h = [c[5] for c in ohlcv_4h]
|
||||
metrics["change_4h_pct_from_4h"] = round((closes_4h[-1] - closes_4h[-2]) / closes_4h[-2] * 100, 2) if closes_4h[-2] != 0 else None
|
||||
recent_vol_4h = sum(volumes_4h[-2:]) / 2 if len(volumes_4h) >= 2 else None
|
||||
metrics["volume_4h_avg"] = round(recent_vol_4h, 2) if recent_vol_4h else None
|
||||
highs_4h = [c[2] for c in ohlcv_4h]
|
||||
lows_4h = [c[3] for c in ohlcv_4h]
|
||||
metrics["high_24h_calc"] = round(max(highs_4h), 8) if highs_4h else None
|
||||
metrics["low_24h_calc"] = round(min(lows_4h), 8) if lows_4h else None
|
||||
if highs_4h and lows_4h:
|
||||
avg_price = sum(closes_4h) / len(closes_4h)
|
||||
metrics["volatility_4h_pct"] = round((max(highs_4h) - min(lows_4h)) / avg_price * 100, 2)
|
||||
|
||||
if current_price:
|
||||
if metrics.get("high_4h"):
|
||||
metrics["distance_from_4h_high_pct"] = round((metrics["high_4h"] - current_price) / metrics["high_4h"] * 100, 2)
|
||||
if metrics.get("low_4h"):
|
||||
metrics["distance_from_4h_low_pct"] = round((current_price - metrics["low_4h"]) / metrics["low_4h"] * 100, 2)
|
||||
if metrics.get("high_24h_calc"):
|
||||
metrics["distance_from_24h_high_pct"] = round((metrics["high_24h_calc"] - current_price) / metrics["high_24h_calc"] * 100, 2)
|
||||
if metrics.get("low_24h_calc"):
|
||||
metrics["distance_from_24h_low_pct"] = round((current_price - metrics["low_24h_calc"]) / metrics["low_24h_calc"] * 100, 2)
|
||||
|
||||
if volume_24h and volume_24h > 0 and metrics.get("volume_1h_avg"):
|
||||
daily_avg_1h = volume_24h / 24
|
||||
metrics["volume_1h_multiple"] = round(metrics["volume_1h_avg"] / daily_avg_1h, 2)
|
||||
if volume_24h and volume_24h > 0 and metrics.get("volume_4h_avg"):
|
||||
daily_avg_4h = volume_24h / 6
|
||||
metrics["volume_4h_multiple"] = round(metrics["volume_4h_avg"] / daily_avg_4h, 2)
|
||||
|
||||
return metrics
|
||||
|
||||
|
||||
def enrich_candidates_and_positions(global_candidates, candidate_layers, positions_view, tickers, ex):
|
||||
symbols = set()
|
||||
for c in global_candidates:
|
||||
symbols.add(c["symbol"])
|
||||
for p in positions_view:
|
||||
sym = p.get("symbol")
|
||||
if sym:
|
||||
sym_ccxt = norm_symbol(sym)
|
||||
symbols.add(sym_ccxt)
|
||||
|
||||
ohlcv_1h = fetch_ohlcv_batch(ex, symbols, "1h", 24)
|
||||
ohlcv_4h = fetch_ohlcv_batch(ex, symbols, "4h", 12)
|
||||
|
||||
def _apply(target_list):
|
||||
for item in target_list:
|
||||
sym = item.get("symbol")
|
||||
if not sym:
|
||||
continue
|
||||
sym_ccxt = norm_symbol(sym)
|
||||
v24h = to_float(tickers.get(sym_ccxt, {}).get("quoteVolume"))
|
||||
metrics = compute_ohlcv_metrics(
|
||||
ohlcv_1h.get(sym_ccxt),
|
||||
ohlcv_4h.get(sym_ccxt),
|
||||
item.get("price") or item.get("last_price"),
|
||||
volume_24h=v24h,
|
||||
)
|
||||
item["metrics"] = metrics
|
||||
|
||||
_apply(global_candidates)
|
||||
for band_list in candidate_layers.values():
|
||||
_apply(band_list)
|
||||
_apply(positions_view)
|
||||
return global_candidates, candidate_layers, positions_view
|
||||
|
||||
|
||||
def regime_from_pct(pct: float | None) -> str:
|
||||
if pct is None:
|
||||
return "unknown"
|
||||
if pct >= 2.0:
|
||||
return "bullish"
|
||||
if pct <= -2.0:
|
||||
return "bearish"
|
||||
return "neutral"
|
||||
Reference in New Issue
Block a user