"""기술지표 — 순수 함수. 일봉 리스트(시간 오름차순)나 종가 리스트를 받아 계산만 한다. 입력 candle dict 키: open/high/low/close/volume (daily_candles_cache.get_candles 형식). 모든 함수는 데이터 부족 시 None 을 반환한다 (raise X). """ from __future__ import annotations def sma(values: list[float], period: int) -> float | None: """최근 period 개 단순이동평균.""" if period <= 0 or len(values) < period: return None return sum(values[-period:]) / period def rsi(closes: list[float], period: int = 14) -> float | None: """Wilder RSI. closes 오름차순, period+1 개 이상 필요.""" if len(closes) < period + 1: return None gains = losses = 0.0 for i in range(1, period + 1): diff = closes[i] - closes[i - 1] if diff >= 0: gains += diff else: losses -= diff avg_gain = gains / period avg_loss = losses / period for i in range(period + 1, len(closes)): diff = closes[i] - closes[i - 1] gain = diff if diff > 0 else 0.0 loss = -diff if diff < 0 else 0.0 avg_gain = (avg_gain * (period - 1) + gain) / period avg_loss = (avg_loss * (period - 1) + loss) / period if avg_loss == 0: return 100.0 rs = avg_gain / avg_loss return 100.0 - (100.0 / (1.0 + rs)) def atr(candles: list[dict], period: int = 14) -> float | None: """Average True Range (Wilder). candles 오름차순, period+1 개 이상 필요.""" if len(candles) < period + 1: return None trs: list[float] = [] for i in range(1, len(candles)): h = candles[i]['high'] lo = candles[i]['low'] prev_close = candles[i - 1]['close'] trs.append(max(h - lo, abs(h - prev_close), abs(lo - prev_close))) if len(trs) < period: return None atr_val = sum(trs[:period]) / period for tr in trs[period:]: atr_val = (atr_val * (period - 1) + tr) / period return atr_val def recent_high(candles: list[dict], period: int, exclude_last: bool = True) -> float | None: """최근 period 봉 최고가. exclude_last 면 마지막(오늘) 봉 제외 → 돌파 판정용.""" series = candles[:-1] if exclude_last else candles window = series[-period:] if not window: return None return max(c['high'] for c in window) def recent_low(candles: list[dict], period: int, exclude_last: bool = True) -> float | None: """최근 period 봉 최저가 — 손절선 후보.""" series = candles[:-1] if exclude_last else candles window = series[-period:] if not window: return None return min(c['low'] for c in window) def volume_avg(candles: list[dict], period: int, exclude_last: bool = True) -> float | None: """최근 period 봉 평균 거래량. exclude_last 면 오늘 제외.""" series = candles[:-1] if exclude_last else candles window = series[-period:] if len(window) < period: return None return sum(c['volume'] for c in window) / period