refactor: Git에서 데이터 제거, 설정·코드만 유지
파이프라인 산출물(data/, docs/)을 Git 추적에서 제외하고 히스토리를 단일 커밋으로 재구성해 저장소 용량을 경량화한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
295
src/deepcoin/techniques/indicators.py
Normal file
295
src/deepcoin/techniques/indicators.py
Normal file
@@ -0,0 +1,295 @@
|
||||
"""기술적 지표 계산 (인과 신호용)."""
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user