refactor: Git에서 데이터 제거, 설정·코드만 유지

파이프라인 산출물(data/, docs/)을 Git 추적에서 제외하고
히스토리를 단일 커밋으로 재구성해 저장소 용량을 경량화한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-12 10:01:43 +09:00
commit 741c949470
92 changed files with 12230 additions and 0 deletions

View File

@@ -0,0 +1,303 @@
"""RSI·MACD 다이버전스 매수·매도 타점 (Ground Truth, 사후 검증)."""
from __future__ import annotations
from dataclasses import dataclass
import pandas as pd
from deepcoin.techniques.indicators import macd, rsi
@dataclass(frozen=True)
class DivergenceSignal:
"""다이버전스 신호."""
side: str # buy | sell
bar_index: int
price: float
datetime: pd.Timestamp
indicator: str # rsi | macd_hist
price_prev: float
price_curr: float
ind_prev: float
ind_curr: float
def find_divergence_signals(
df: pd.DataFrame,
local_order: int = 15,
min_bars_between: int = 100,
max_pair_lookback_bars: int = 5000,
rsi_period: int = 14,
min_rsi_diff: float = 2.0,
min_macd_hist_diff: float = 0.0,
min_price_move_pct: float = 1.5,
future_bars: int = 2000,
min_future_move_pct: float = 2.0,
) -> tuple[list[DivergenceSignal], list[DivergenceSignal]]:
"""가격·지표 다이버전스 매수·매도 후보를 찾는다.
- 상승 다이버전스(매수): 가격 LL + RSI/MACD HL
- 하락 다이버전스(매도): 가격 HH + RSI/MACD LH
- 미래 데이터로 이후 유의미한 반등·하락을 사후 검증
Args:
df: OHLCV DataFrame.
local_order: 국소 극값 반경(봉).
min_bars_between: 연속 다이버전스 최소 간격(봉).
max_pair_lookback_bars: 비교할 이전 극값 최대 거리(봉).
rsi_period: RSI 기간.
min_rsi_diff: RSI 다이버전스 최소 차이(포인트).
min_macd_hist_diff: MACD 히스토그램 최소 차이.
min_price_move_pct: 극값 간 최소 가격 변동(%).
future_bars: 사후 검증 구간(봉).
min_future_move_pct: 사후 최소 가격 변동(%).
Returns:
(매수 신호, 매도 신호) 리스트.
"""
if len(df) < local_order * 4 + rsi_period:
return [], []
close = df["close"].astype(float)
low = df["low"].astype(float)
high = df["high"].astype(float)
rsi_vals = rsi(close, period=rsi_period)
_, _, macd_hist = macd(close)
lows = _find_local_extrema(df, low, rsi_vals, macd_hist, local_order, "low")
highs = _find_local_extrema(df, high, rsi_vals, macd_hist, local_order, "high")
bull_rsi = _detect_bullish(
df, lows, rsi_vals, "rsi", min_rsi_diff, min_price_move_pct,
max_pair_lookback_bars, future_bars, min_future_move_pct, high,
)
bear_rsi = _detect_bearish(
df, highs, rsi_vals, "rsi", min_rsi_diff, min_price_move_pct,
max_pair_lookback_bars, future_bars, min_future_move_pct, low,
)
bull_macd: list[DivergenceSignal] = []
bear_macd: list[DivergenceSignal] = []
if min_macd_hist_diff > 0:
bull_macd = _detect_bullish(
df, lows, macd_hist, "macd_hist", min_macd_hist_diff, min_price_move_pct,
max_pair_lookback_bars, future_bars, min_future_move_pct, high,
)
bear_macd = _detect_bearish(
df, highs, macd_hist, "macd_hist", min_macd_hist_diff, min_price_move_pct,
max_pair_lookback_bars, future_bars, min_future_move_pct, low,
)
buys = _dedupe_signals(
_merge_same_bar(bull_rsi + bull_macd), min_bars_between, prefer_lower=True
)
sells = _dedupe_signals(
_merge_same_bar(bear_rsi + bear_macd), min_bars_between, prefer_lower=False
)
return buys, sells
@dataclass
class _ExtremePoint:
"""국소 극값."""
bar_index: int
price: float
rsi: float
macd_hist: float
datetime: pd.Timestamp
def _find_local_extrema(
df: pd.DataFrame,
series: pd.Series,
rsi_vals: pd.Series,
macd_hist: pd.Series,
order: int,
side: str,
) -> list[_ExtremePoint]:
"""국소 저점·고점을 수집한다."""
points: list[_ExtremePoint] = []
values = series.values
for i in range(order, len(df) - order):
window = values[i - order : i + order + 1]
val = float(values[i])
if side == "low" and val > window.min():
continue
if side == "high" and val < window.max():
continue
if pd.isna(rsi_vals.iloc[i]) or pd.isna(macd_hist.iloc[i]):
continue
points.append(
_ExtremePoint(
bar_index=i,
price=val,
rsi=float(rsi_vals.iloc[i]),
macd_hist=float(macd_hist.iloc[i]),
datetime=pd.Timestamp(df.iloc[i]["datetime"]),
)
)
return points
def _detect_bullish(
df: pd.DataFrame,
lows: list[_ExtremePoint],
indicator: pd.Series,
indicator_name: str,
min_ind_diff: float,
min_price_move_pct: float,
max_lookback: int,
future_bars: int,
min_future_move_pct: float,
highs,
) -> list[DivergenceSignal]:
"""상승 다이버전스 매수를 탐지한다."""
signals: list[DivergenceSignal] = []
if len(lows) < 2:
return signals
for idx in range(1, len(lows)):
curr = lows[idx]
for prev in reversed(lows[:idx]):
if curr.bar_index - prev.bar_index > max_lookback:
break
if curr.bar_index - prev.bar_index < 20:
continue
price_move = (prev.price - curr.price) / prev.price * 100.0
if price_move < min_price_move_pct:
continue
ind_prev = float(indicator.iloc[prev.bar_index])
ind_curr = float(indicator.iloc[curr.bar_index])
if pd.isna(ind_prev) or pd.isna(ind_curr):
continue
if not (curr.price < prev.price and ind_curr > ind_prev + min_ind_diff):
continue
end = min(len(df), curr.bar_index + future_bars + 1)
future_high = float(highs[curr.bar_index:end].max())
rally = (future_high - curr.price) / curr.price * 100.0
if rally < min_future_move_pct:
continue
signals.append(
DivergenceSignal(
side="buy",
bar_index=curr.bar_index,
price=round(curr.price, 2),
datetime=curr.datetime,
indicator=indicator_name,
price_prev=round(prev.price, 2),
price_curr=round(curr.price, 2),
ind_prev=round(ind_prev, 4),
ind_curr=round(ind_curr, 4),
)
)
break
return signals
def _detect_bearish(
df: pd.DataFrame,
highs: list[_ExtremePoint],
indicator: pd.Series,
indicator_name: str,
min_ind_diff: float,
min_price_move_pct: float,
max_lookback: int,
future_bars: int,
min_future_move_pct: float,
lows,
) -> list[DivergenceSignal]:
"""하락 다이버전스 매도를 탐지한다."""
signals: list[DivergenceSignal] = []
if len(highs) < 2:
return signals
for idx in range(1, len(highs)):
curr = highs[idx]
for prev in reversed(highs[:idx]):
if curr.bar_index - prev.bar_index > max_lookback:
break
if curr.bar_index - prev.bar_index < 20:
continue
price_move = (curr.price - prev.price) / prev.price * 100.0
if price_move < min_price_move_pct:
continue
ind_prev = float(indicator.iloc[prev.bar_index])
ind_curr = float(indicator.iloc[curr.bar_index])
if pd.isna(ind_prev) or pd.isna(ind_curr):
continue
if not (curr.price > prev.price and ind_curr < ind_prev - min_ind_diff):
continue
end = min(len(df), curr.bar_index + future_bars + 1)
future_low = float(lows[curr.bar_index:end].min())
drop = (curr.price - future_low) / curr.price * 100.0
if drop < min_future_move_pct:
continue
signals.append(
DivergenceSignal(
side="sell",
bar_index=curr.bar_index,
price=round(curr.price, 2),
datetime=curr.datetime,
indicator=indicator_name,
price_prev=round(prev.price, 2),
price_curr=round(curr.price, 2),
ind_prev=round(ind_prev, 4),
ind_curr=round(ind_curr, 4),
)
)
break
return signals
def _merge_same_bar(signals: list[DivergenceSignal]) -> list[DivergenceSignal]:
"""동일 봉·동일 방향 신호를 하나로 합친다."""
by_bar: dict[int, DivergenceSignal] = {}
for signal in signals:
prev = by_bar.get(signal.bar_index)
if prev is None:
by_bar[signal.bar_index] = signal
continue
prev_diff = abs(prev.ind_curr - prev.ind_prev)
curr_diff = abs(signal.ind_curr - signal.ind_prev)
if curr_diff > prev_diff:
by_bar[signal.bar_index] = signal
return sorted(by_bar.values(), key=lambda s: s.bar_index)
def _dedupe_signals(
signals: list[DivergenceSignal],
min_bars: int,
prefer_lower: bool,
) -> list[DivergenceSignal]:
"""근접 신호를 병합한다."""
if not signals:
return []
sorted_signals = sorted(signals, key=lambda s: s.bar_index)
merged: list[DivergenceSignal] = [sorted_signals[0]]
for signal in sorted_signals[1:]:
last = merged[-1]
if signal.bar_index - last.bar_index < min_bars:
if prefer_lower and signal.price < last.price:
merged[-1] = signal
elif not prefer_lower and signal.price > last.price:
merged[-1] = signal
else:
merged.append(signal)
return merged