파이프라인 산출물(data/, docs/)을 Git 추적에서 제외하고 히스토리를 단일 커밋으로 재구성해 저장소 용량을 경량화한다. Co-authored-by: Cursor <cursoragent@cursor.com>
296 lines
8.8 KiB
Python
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
|
|
|