WLD DeepCoin 단계별 구조 재편 및 설정·문서 통합
로고스/루트 레거시를 제거하고 deepcoin 패키지·scripts 01~05 CLI·docs/reference로 데이터·GT·분석·매칭·운영 단계를 정리했다. config와 .env 기반 설정, trade_anaysis.html 동기화 포함. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
315
deepcoin/common/indicators.py
Normal file
315
deepcoin/common/indicators.py
Normal file
@@ -0,0 +1,315 @@
|
||||
"""
|
||||
볼린저 밴드·일목·MACD·스토캐스틱·RSI·이격도 계산 (모든 봉 간격 공용).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from config import (
|
||||
BB_PERIOD,
|
||||
BB_STD,
|
||||
DISPARITY_OVERBOUGHT,
|
||||
DISPARITY_OVERSOLD,
|
||||
DISPARITY_PERIODS,
|
||||
MACD_FAST,
|
||||
MACD_SIGNAL,
|
||||
MACD_SLOW,
|
||||
RSI_PERIOD,
|
||||
STOCH_D_PERIOD,
|
||||
STOCH_K_PERIOD,
|
||||
STOCH_OVERBOUGHT,
|
||||
STOCH_OVERSOLD,
|
||||
STOCH_SMOOTH_K,
|
||||
TREND_RANGE_MA_GAP_PCT,
|
||||
)
|
||||
|
||||
Trend = str # "up" | "down" | "range"
|
||||
|
||||
|
||||
def add_bollinger(
|
||||
df: pd.DataFrame,
|
||||
period: int = BB_PERIOD,
|
||||
std_mult: float = BB_STD,
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
볼린저 밴드 컬럼을 추가합니다.
|
||||
|
||||
Args:
|
||||
df: OHLCV DataFrame.
|
||||
period: 중심선 기간.
|
||||
std_mult: 표준편차 배수.
|
||||
|
||||
Returns:
|
||||
MA, Upper, Lower, STD, bb_pos, BB_Width 가 추가된 DataFrame.
|
||||
"""
|
||||
out = df.copy()
|
||||
if "MA" not in out.columns:
|
||||
out["MA"] = out["Close"].rolling(period).mean()
|
||||
if "Upper" not in out.columns or "Lower" not in out.columns:
|
||||
std = out["Close"].rolling(period).std()
|
||||
out["STD"] = std
|
||||
out["Upper"] = out["MA"] + std_mult * std
|
||||
out["Lower"] = out["MA"] - std_mult * std
|
||||
ma = out["MA"].replace(0, np.nan)
|
||||
band = (out["Upper"] - out["Lower"]).replace(0, np.nan)
|
||||
out["bb_pos"] = ((out["Close"] - out["Lower"]) / band).clip(0, 1)
|
||||
out["BB_Width"] = band / ma * 100
|
||||
return out
|
||||
|
||||
|
||||
def add_macd(
|
||||
df: pd.DataFrame,
|
||||
fast: int = MACD_FAST,
|
||||
slow: int = MACD_SLOW,
|
||||
signal_period: int = MACD_SIGNAL,
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
MACD(12,26,9) 라인·시그널·히스토그램을 추가합니다.
|
||||
|
||||
Args:
|
||||
df: OHLCV (Close 필요).
|
||||
fast: 단기 EMA 기간.
|
||||
slow: 장기 EMA 기간.
|
||||
signal_period: 시그널 EMA 기간.
|
||||
|
||||
Returns:
|
||||
macd_line, macd_signal, macd_hist 컬럼이 추가된 DataFrame.
|
||||
"""
|
||||
out = df.copy()
|
||||
close = out["Close"].astype(float)
|
||||
ema_fast = close.ewm(span=fast, adjust=False).mean()
|
||||
ema_slow = close.ewm(span=slow, adjust=False).mean()
|
||||
out["macd_line"] = ema_fast - ema_slow
|
||||
out["macd_signal"] = out["macd_line"].ewm(span=signal_period, adjust=False).mean()
|
||||
out["macd_hist"] = out["macd_line"] - out["macd_signal"]
|
||||
return out
|
||||
|
||||
|
||||
def disparity_column(period: int) -> str:
|
||||
"""이격도 컬럼명 (예: disparity_20)."""
|
||||
return f"disparity_{period}"
|
||||
|
||||
|
||||
def add_disparity(
|
||||
df: pd.DataFrame,
|
||||
periods: tuple[int, ...] | None = None,
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
이격도 = (종가 / SMA(n)) × 100. 100이면 이평선과 동일 위치.
|
||||
|
||||
Args:
|
||||
df: OHLCV (Close 필요).
|
||||
periods: SMA 기간 목록. None이면 config.DISPARITY_PERIODS.
|
||||
|
||||
Returns:
|
||||
disparity_{n} 컬럼이 추가된 DataFrame.
|
||||
"""
|
||||
out = df.copy()
|
||||
close = out["Close"].astype(float)
|
||||
for p in periods or DISPARITY_PERIODS:
|
||||
ma = close.rolling(p).mean()
|
||||
out[disparity_column(p)] = (close / ma.replace(0, np.nan)) * 100.0
|
||||
return out
|
||||
|
||||
|
||||
def disparity_zone(value: float | None) -> str:
|
||||
"""이격도 구간 라벨 (oversold / mid / overbought)."""
|
||||
if value is None:
|
||||
return "mid"
|
||||
if value <= DISPARITY_OVERSOLD:
|
||||
return "oversold"
|
||||
if value >= DISPARITY_OVERBOUGHT:
|
||||
return "overbought"
|
||||
return "mid"
|
||||
|
||||
|
||||
def add_stochastic(
|
||||
df: pd.DataFrame,
|
||||
k_period: int = STOCH_K_PERIOD,
|
||||
d_period: int = STOCH_D_PERIOD,
|
||||
smooth_k: int = STOCH_SMOOTH_K,
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
스토캐스틱 %K·%D를 추가합니다 (Slow Stochastic).
|
||||
|
||||
Args:
|
||||
df: OHLCV (High, Low, Close 필요).
|
||||
k_period: %K lookback.
|
||||
d_period: %D SMA 기간.
|
||||
smooth_k: %K SMA 평활 기간.
|
||||
|
||||
Returns:
|
||||
stoch_k, stoch_d 컬럼이 추가된 DataFrame.
|
||||
"""
|
||||
out = df.copy()
|
||||
h = out["High"].astype(float)
|
||||
l = out["Low"].astype(float)
|
||||
c = out["Close"].astype(float)
|
||||
lowest = l.rolling(k_period).min()
|
||||
highest = h.rolling(k_period).max()
|
||||
denom = (highest - lowest).replace(0, np.nan)
|
||||
raw_k = ((c - lowest) / denom) * 100.0
|
||||
out["stoch_k"] = raw_k.rolling(smooth_k).mean()
|
||||
out["stoch_d"] = out["stoch_k"].rolling(d_period).mean()
|
||||
return out
|
||||
|
||||
|
||||
def add_ichimoku(
|
||||
df: pd.DataFrame,
|
||||
tenkan: int = 9,
|
||||
kijun: int = 26,
|
||||
senkou_b_period: int = 52,
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
일목균형표 라인·구름 위치 컬럼 추가 (해당 봉 시점, 미래 데이터 미사용).
|
||||
|
||||
Returns:
|
||||
ichi_tenkan, ichi_kijun, ichi_span_a, ichi_span_b,
|
||||
ichi_cloud_top, ichi_cloud_bottom
|
||||
"""
|
||||
out = df.copy()
|
||||
h = out["High"].astype(float)
|
||||
l = out["Low"].astype(float)
|
||||
c = out["Close"].astype(float)
|
||||
|
||||
out["ichi_tenkan"] = (h.rolling(tenkan).max() + l.rolling(tenkan).min()) / 2
|
||||
out["ichi_kijun"] = (h.rolling(kijun).max() + l.rolling(kijun).min()) / 2
|
||||
out["ichi_span_a"] = (out["ichi_tenkan"] + out["ichi_kijun"]) / 2
|
||||
out["ichi_span_b"] = (h.rolling(senkou_b_period).max() + l.rolling(senkou_b_period).min()) / 2
|
||||
out["ichi_cloud_top"] = np.maximum(out["ichi_span_a"], out["ichi_span_b"])
|
||||
out["ichi_cloud_bottom"] = np.minimum(out["ichi_span_a"], out["ichi_span_b"])
|
||||
return out
|
||||
|
||||
|
||||
def prepare_entry_df(data: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
RSI·거래량 MA·BB 폭 등 보조 컬럼을 추가합니다.
|
||||
|
||||
Args:
|
||||
data: BB(MA/Upper/Lower)가 계산된 OHLCV.
|
||||
|
||||
Returns:
|
||||
RSI 등 컬럼이 추가된 DataFrame.
|
||||
"""
|
||||
df = data.copy()
|
||||
delta = df["Close"].diff()
|
||||
gain = delta.where(delta > 0, 0.0).rolling(RSI_PERIOD).mean()
|
||||
loss = (-delta.where(delta < 0, 0.0)).rolling(RSI_PERIOD).mean()
|
||||
rs = gain / loss.replace(0, np.nan)
|
||||
df["RSI"] = 100 - (100 / (1 + rs))
|
||||
df["VolMA5"] = df["Volume"].rolling(5).mean()
|
||||
if "MA" in df.columns and "Upper" in df.columns and "Lower" in df.columns:
|
||||
ma = df["MA"].replace(0, np.nan)
|
||||
df["BB_Width"] = (df["Upper"] - df["Lower"]) / ma * 100
|
||||
return df
|
||||
|
||||
|
||||
def apply_bar_indicators(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
봉 분석·차트용 표준 지표 일괄 적용 (BB, 일목, RSI, MACD, 스토캐스틱, 이격도).
|
||||
|
||||
Args:
|
||||
df: OHLCV DataFrame (datetime index).
|
||||
|
||||
Returns:
|
||||
모든 지표 컬럼이 붙은 DataFrame.
|
||||
"""
|
||||
out = add_bollinger(df)
|
||||
out = add_ichimoku(out)
|
||||
out = prepare_entry_df(out)
|
||||
out = add_disparity(out)
|
||||
out = add_macd(out)
|
||||
out = add_stochastic(out)
|
||||
return out
|
||||
|
||||
|
||||
def latest_indicator_snapshot(df: pd.DataFrame) -> dict[str, float | str | None]:
|
||||
"""
|
||||
최신 봉의 BB·RSI·MACD·스토캐스틱 요약 (모니터·로그용).
|
||||
|
||||
Args:
|
||||
df: apply_bar_indicators 적용된 DataFrame.
|
||||
|
||||
Returns:
|
||||
지표명→값 dict.
|
||||
"""
|
||||
if df.empty:
|
||||
return {}
|
||||
row = df.iloc[-1]
|
||||
|
||||
def _f(col: str) -> float | None:
|
||||
if col not in row.index or pd.isna(row[col]):
|
||||
return None
|
||||
return round(float(row[col]), 4)
|
||||
|
||||
macd_hist = _f("macd_hist")
|
||||
stoch_k = _f("stoch_k")
|
||||
stoch_d = _f("stoch_d")
|
||||
stoch_zone = "mid"
|
||||
if stoch_k is not None:
|
||||
if stoch_k <= STOCH_OVERSOLD:
|
||||
stoch_zone = "oversold"
|
||||
elif stoch_k >= STOCH_OVERBOUGHT:
|
||||
stoch_zone = "overbought"
|
||||
|
||||
macd_state = "neutral"
|
||||
if macd_hist is not None:
|
||||
macd_state = "bull" if macd_hist > 0 else "bear"
|
||||
|
||||
disp: dict[str, float | None] = {}
|
||||
for p in DISPARITY_PERIODS:
|
||||
col = disparity_column(p)
|
||||
disp[col] = _f(col)
|
||||
primary = disparity_column(DISPARITY_PERIODS[0]) if DISPARITY_PERIODS else None
|
||||
disp_primary = disp.get(primary) if primary else None
|
||||
|
||||
return {
|
||||
"bb_pos": _f("bb_pos"),
|
||||
"rsi": _f("RSI"),
|
||||
"disparity": disp,
|
||||
"disparity_primary": disp_primary,
|
||||
"disparity_zone": disparity_zone(disp_primary),
|
||||
"macd_line": _f("macd_line"),
|
||||
"macd_signal": _f("macd_signal"),
|
||||
"macd_hist": macd_hist,
|
||||
"macd_state": macd_state,
|
||||
"stoch_k": stoch_k,
|
||||
"stoch_d": stoch_d,
|
||||
"stoch_zone": stoch_zone,
|
||||
}
|
||||
|
||||
|
||||
def get_trend(df_1d: pd.DataFrame, df_1h: pd.DataFrame) -> Trend:
|
||||
"""
|
||||
일봉·1시간봉 기준 추세(up/down/range)를 반환합니다.
|
||||
|
||||
Args:
|
||||
df_1d: 일봉 OHLCV+지표.
|
||||
df_1h: 1시간봉 OHLCV+지표.
|
||||
|
||||
Returns:
|
||||
추세 문자열.
|
||||
"""
|
||||
if len(df_1d) < 20 or len(df_1h) < 40:
|
||||
return "range"
|
||||
|
||||
d_close = float(df_1d["Close"].iloc[-1])
|
||||
d_ma20 = float(df_1d["MA20"].iloc[-1])
|
||||
h_close = float(df_1h["Close"].iloc[-1])
|
||||
h_ma20 = float(df_1h["MA20"].iloc[-1])
|
||||
h_ma40 = float(df_1h["MA40"].iloc[-1])
|
||||
|
||||
if h_ma40 == 0:
|
||||
return "range"
|
||||
|
||||
ma_gap_pct = abs(h_ma20 - h_ma40) / h_ma40 * 100
|
||||
if ma_gap_pct < TREND_RANGE_MA_GAP_PCT:
|
||||
return "range"
|
||||
|
||||
if d_close > d_ma20 and h_ma20 > h_ma40 and h_close > h_ma20:
|
||||
return "up"
|
||||
if d_close < d_ma20 and h_ma20 < h_ma40 and h_close < h_ma20:
|
||||
return "down"
|
||||
return "range"
|
||||
Reference in New Issue
Block a user