Files
Bithumb/src/deepcoin/techniques/indicators.py
dsyoon 741c949470 refactor: Git에서 데이터 제거, 설정·코드만 유지
파이프라인 산출물(data/, docs/)을 Git 추적에서 제외하고
히스토리를 단일 커밋으로 재구성해 저장소 용량을 경량화한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-12 10:01:43 +09:00

296 lines
8.8 KiB
Python

"""기술적 지표 계산 (인과 신호용)."""
from __future__ import annotations
import pandas as pd
def ema(series: pd.Series, span: int) -> pd.Series:
"""지수이동평균을 계산한다."""
return series.ewm(span=span, adjust=False).mean()
def sma(series: pd.Series, window: int) -> pd.Series:
"""단순이동평균을 계산한다."""
return series.rolling(window=window, min_periods=window).mean()
def bollinger_bands(
close: pd.Series,
window: int = 20,
num_std: float = 2.0,
) -> tuple[pd.Series, pd.Series, pd.Series]:
"""볼린저 밴드 (중심, 상단, 하단)를 계산한다."""
mid = sma(close, window)
std = close.rolling(window=window, min_periods=window).std()
upper = mid + num_std * std
lower = mid - num_std * std
return mid, upper, lower
def rsi(close: pd.Series, period: int = 14) -> pd.Series:
"""RSI(상대강도지수)를 계산한다."""
delta = close.diff()
gain = delta.clip(lower=0.0)
loss = -delta.clip(upper=0.0)
avg_gain = gain.ewm(alpha=1 / period, min_periods=period, adjust=False).mean()
avg_loss = loss.ewm(alpha=1 / period, min_periods=period, adjust=False).mean()
rs = avg_gain / avg_loss.replace(0, pd.NA)
return 100 - (100 / (1 + rs))
def macd(
close: pd.Series,
fast: int = 12,
slow: int = 26,
signal: int = 9,
) -> tuple[pd.Series, pd.Series, pd.Series]:
"""MACD, 시그널, 히스토그램을 계산한다."""
ema_fast = ema(close, fast)
ema_slow = ema(close, slow)
macd_line = ema_fast - ema_slow
signal_line = ema(macd_line, signal)
hist = macd_line - signal_line
return macd_line, signal_line, hist
def atr(
high: pd.Series,
low: pd.Series,
close: pd.Series,
period: int = 14,
) -> pd.Series:
"""Average True Range (ATR)를 계산한다.
Args:
high: 고가 시리즈.
low: 저가 시리즈.
close: 종가 시리즈.
period: ATR 기간.
Returns:
ATR 시리즈.
"""
prev_close = close.shift(1)
tr = pd.concat(
[
(high - low).abs(),
(high - prev_close).abs(),
(low - prev_close).abs(),
],
axis=1,
).max(axis=1)
return tr.ewm(alpha=1 / period, min_periods=period, adjust=False).mean()
def stochastic(
high: pd.Series,
low: pd.Series,
close: pd.Series,
k_period: int = 14,
d_period: int = 3,
) -> tuple[pd.Series, pd.Series]:
"""Stochastic %K, %D를 계산한다."""
import numpy as np
close_f = close.astype(float)
lowest = low.astype(float).rolling(window=k_period, min_periods=k_period).min()
highest = high.astype(float).rolling(window=k_period, min_periods=k_period).max()
range_hl = highest - lowest
pct_k = pd.Series(
np.where(range_hl > 0, 100.0 * (close_f - lowest) / range_hl, np.nan),
index=close.index,
dtype=float,
)
pct_d = pct_k.rolling(window=d_period, min_periods=d_period).mean()
return pct_k, pct_d
def cci(
high: pd.Series,
low: pd.Series,
close: pd.Series,
period: int = 20,
) -> pd.Series:
"""Commodity Channel Index를 계산한다."""
tp = (high + low + close) / 3.0
sma_tp = sma(tp, period)
mean_dev = (tp - sma_tp).abs().rolling(window=period, min_periods=period).mean()
return (tp - sma_tp) / (0.015 * mean_dev.replace(0, pd.NA))
def roc(close: pd.Series, period: int = 12) -> pd.Series:
"""Rate of Change(%)를 계산한다."""
prev = close.shift(period)
return (close - prev) / prev.replace(0, pd.NA) * 100.0
def adx(
high: pd.Series,
low: pd.Series,
close: pd.Series,
period: int = 14,
) -> tuple[pd.Series, pd.Series, pd.Series]:
"""ADX, +DI, -DI를 계산한다."""
up_move = high.diff()
down_move = -low.diff()
plus_dm = up_move.where((up_move > down_move) & (up_move > 0), 0.0)
minus_dm = down_move.where((down_move > up_move) & (down_move > 0), 0.0)
atr_vals = atr(high, low, close, period=period)
plus_di = 100 * ema(plus_dm, period) / atr_vals.replace(0, pd.NA)
minus_di = 100 * ema(minus_dm, period) / atr_vals.replace(0, pd.NA)
dx = (plus_di - minus_di).abs() / (plus_di + minus_di).replace(0, pd.NA) * 100
adx_line = ema(dx, period)
return adx_line, plus_di, minus_di
def keltner_channels(
high: pd.Series,
low: pd.Series,
close: pd.Series,
ema_span: int = 20,
atr_period: int = 10,
atr_mult: float = 2.0,
) -> tuple[pd.Series, pd.Series, pd.Series]:
"""Keltner 채널 (중심, 상단, 하단)을 계산한다."""
mid = ema(close, ema_span)
atr_vals = atr(high, low, close, period=atr_period)
upper = mid + atr_mult * atr_vals
lower = mid - atr_mult * atr_vals
return mid, upper, lower
def obv(close: pd.Series, volume: pd.Series) -> pd.Series:
"""On-Balance Volume을 계산한다."""
direction = close.diff().apply(lambda x: 1 if x > 0 else (-1 if x < 0 else 0))
return (direction * volume.astype(float)).cumsum()
def supertrend(
high: pd.Series,
low: pd.Series,
close: pd.Series,
period: int = 10,
multiplier: float = 3.0,
) -> tuple[pd.Series, pd.Series]:
"""Supertrend 라인과 방향(1=상승, -1=하락)을 계산한다."""
atr_vals = atr(high, low, close, period=period)
hl2 = (high + low) / 2.0
basic_upper = hl2 + multiplier * atr_vals
basic_lower = hl2 - multiplier * atr_vals
final_upper = basic_upper.copy()
final_lower = basic_lower.copy()
direction = pd.Series(1, index=close.index, dtype=float)
st_line = pd.Series(index=close.index, dtype=float)
for i in range(1, len(close)):
if pd.isna(final_upper.iloc[i]) or pd.isna(final_lower.iloc[i]):
continue
if basic_upper.iloc[i] < final_upper.iloc[i - 1] or close.iloc[i - 1] > final_upper.iloc[i - 1]:
final_upper.iloc[i] = basic_upper.iloc[i]
else:
final_upper.iloc[i] = final_upper.iloc[i - 1]
if basic_lower.iloc[i] > final_lower.iloc[i - 1] or close.iloc[i - 1] < final_lower.iloc[i - 1]:
final_lower.iloc[i] = basic_lower.iloc[i]
else:
final_lower.iloc[i] = final_lower.iloc[i - 1]
if direction.iloc[i - 1] == 1:
if close.iloc[i] < final_lower.iloc[i]:
direction.iloc[i] = -1
else:
direction.iloc[i] = 1
else:
if close.iloc[i] > final_upper.iloc[i]:
direction.iloc[i] = 1
else:
direction.iloc[i] = -1
st_line.iloc[i] = final_lower.iloc[i] if direction.iloc[i] == 1 else final_upper.iloc[i]
return st_line, direction
def parabolic_sar(
high: pd.Series,
low: pd.Series,
close: pd.Series,
af_step: float = 0.02,
af_max: float = 0.2,
) -> pd.Series:
"""Parabolic SAR 근사값을 계산한다."""
length = len(close)
sar = pd.Series(index=close.index, dtype=float)
if length < 2:
return sar
bull = True
af = af_step
ep = float(high.iloc[0])
sar.iloc[0] = float(low.iloc[0])
for i in range(1, length):
prev_sar = float(sar.iloc[i - 1]) if not pd.isna(sar.iloc[i - 1]) else float(low.iloc[0])
curr_sar = prev_sar + af * (ep - prev_sar)
h = float(high.iloc[i])
low_i = float(low.iloc[i])
if bull:
curr_sar = min(curr_sar, float(low.iloc[i - 1]), low_i)
if low_i < curr_sar:
bull = False
curr_sar = ep
ep = low_i
af = af_step
else:
if h > ep:
ep = h
af = min(af + af_step, af_max)
else:
curr_sar = max(curr_sar, float(high.iloc[i - 1]), h)
if h > curr_sar:
bull = True
curr_sar = ep
ep = h
af = af_step
else:
if low_i < ep:
ep = low_i
af = min(af + af_step, af_max)
sar.iloc[i] = curr_sar
return sar
def ichimoku(
high: pd.Series,
low: pd.Series,
tenkan: int = 9,
kijun: int = 26,
) -> tuple[pd.Series, pd.Series]:
"""일목 전환선·기준선을 계산한다 (인과 신호용 간소 버전)."""
tenkan_sen = (high.rolling(tenkan).max() + low.rolling(tenkan).min()) / 2.0
kijun_sen = (high.rolling(kijun).max() + low.rolling(kijun).min()) / 2.0
return tenkan_sen, kijun_sen
def rolling_pivot_points(
high: pd.Series,
low: pd.Series,
close: pd.Series,
window: int = 60,
) -> tuple[pd.Series, pd.Series, pd.Series]:
"""롤링 피벗 P, S1, R1을 계산한다."""
prev_high = high.shift(1).rolling(window).max()
prev_low = low.shift(1).rolling(window).min()
prev_close = close.shift(1)
pivot = (prev_high + prev_low + prev_close) / 3.0
s1 = 2 * pivot - prev_high
r1 = 2 * pivot - prev_low
return pivot, s1, r1