"""분봉 OHLCV → 5-level 모멘텀 분류.""" from __future__ import annotations from collections import deque # 분류 카테고리 STRONG_UP = "strong_up" WEAK_UP = "weak_up" NEUTRAL = "neutral" WEAK_DOWN = "weak_down" STRONG_DOWN = "strong_down" _BARS_PER_5MIN = 5 _LOOKBACK_5MIN_BARS = 5 _VOLUME_AVG_WINDOW = 12 # 60분 = 5분봉 12개 def aggregate_1min_to_5min(minute_bars: list[dict]) -> list[dict]: """1분봉 N개 → 5분봉 floor(N/5) 개. 시간 오름차순. 각 5분봉: open=첫 1분봉 open, high=max, low=min, close=마지막 close, volume=sum. """ bars_5min = [] chunks = len(minute_bars) // _BARS_PER_5MIN for i in range(chunks): chunk = minute_bars[i * _BARS_PER_5MIN : (i + 1) * _BARS_PER_5MIN] bars_5min.append({ "datetime": chunk[0]["datetime"], "open": chunk[0]["open"], "high": max(b["high"] for b in chunk), "low": min(b["low"] for b in chunk), "close": chunk[-1]["close"], "volume": sum(b["volume"] for b in chunk), }) return bars_5min def classify_minute_momentum(minute_bars: deque) -> str: """1분봉 deque → 5-level 모멘텀 분류. Returns: STRONG_UP / WEAK_UP / NEUTRAL / WEAK_DOWN / STRONG_DOWN """ minute_list = list(minute_bars) if len(minute_list) < _BARS_PER_5MIN * _LOOKBACK_5MIN_BARS: return NEUTRAL # 데이터 부족 bars_5min = aggregate_1min_to_5min(minute_list) if len(bars_5min) < _LOOKBACK_5MIN_BARS: return NEUTRAL recent = bars_5min[-_LOOKBACK_5MIN_BARS:] up_count = sum(1 for b in recent if b["close"] > b["open"]) # 거래량 multiplier: recent 5 avg vs 60분 avg recent_vol_avg = sum(b["volume"] for b in recent) / len(recent) long_window = bars_5min[-_VOLUME_AVG_WINDOW:] long_vol_avg = sum(b["volume"] for b in long_window) / len(long_window) vol_mult = recent_vol_avg / long_vol_avg if long_vol_avg > 0 else 1.0 # 5-level 분류 if up_count == 5 and vol_mult >= 1.5: return STRONG_UP elif up_count >= 3 and vol_mult >= 1.0: return WEAK_UP elif up_count == 0 and vol_mult >= 1.5: return STRONG_DOWN elif up_count <= 2 and vol_mult < 1.0: return WEAK_DOWN else: return NEUTRAL