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:
0
deepcoin/common/__init__.py
Normal file
0
deepcoin/common/__init__.py
Normal file
362
deepcoin/common/candle_features.py
Normal file
362
deepcoin/common/candle_features.py
Normal file
@@ -0,0 +1,362 @@
|
||||
"""
|
||||
모든 봉(1~1440분)에 BB·일목 위치·캔들 형태 특징을 계산하고
|
||||
기준 타임라인(3분)에 맞춰 정렬합니다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from config import (
|
||||
ALL_INTERVALS,
|
||||
BB_MIN_WIDTH_PCT,
|
||||
DISPARITY_PERIODS,
|
||||
ENTRY_INTERVAL,
|
||||
INTERVAL_PREFIX,
|
||||
STOCH_OVERBOUGHT,
|
||||
STOCH_OVERSOLD,
|
||||
)
|
||||
from deepcoin.common.indicators import apply_bar_indicators, disparity_column
|
||||
|
||||
|
||||
def interval_prefix(interval: int) -> str:
|
||||
"""컬럼 접두사 (예: m3, d1)."""
|
||||
return INTERVAL_PREFIX.get(interval, f"m{interval}")
|
||||
|
||||
|
||||
def interval_display(interval: int) -> str:
|
||||
if interval >= 1440:
|
||||
return "일봉"
|
||||
return f"{interval}분"
|
||||
|
||||
|
||||
# BB 위치 (밴드 내 %B 구간)
|
||||
BB_ZONE_FEATURES: tuple[str, ...] = (
|
||||
"bb_zone_bottom",
|
||||
"bb_zone_low",
|
||||
"bb_zone_mid",
|
||||
"bb_zone_high",
|
||||
"bb_zone_top",
|
||||
)
|
||||
|
||||
# 일목 위치
|
||||
ICHI_FEATURES: tuple[str, ...] = (
|
||||
"ichi_above_cloud",
|
||||
"ichi_below_cloud",
|
||||
"ichi_in_cloud",
|
||||
"ichi_cloud_bull",
|
||||
"ichi_cloud_bear",
|
||||
"ichi_tk_bull",
|
||||
"ichi_tk_bear",
|
||||
"ichi_price_above_tenkan",
|
||||
"ichi_price_below_kijun",
|
||||
"ichi_tk_cross_up",
|
||||
"ichi_tk_cross_down",
|
||||
)
|
||||
|
||||
# BB 이벤트·캔들 형태
|
||||
BB_EVENT_FEATURES: tuple[str, ...] = (
|
||||
"cross_up_lower",
|
||||
"cross_up_upper",
|
||||
"cross_down_lower",
|
||||
"below_lower",
|
||||
"above_upper",
|
||||
"inside_band",
|
||||
"bb_pos_low",
|
||||
"bb_pos_high",
|
||||
"squeeze",
|
||||
)
|
||||
|
||||
MACD_STOCH_FEATURES: tuple[str, ...] = (
|
||||
"macd_hist_positive",
|
||||
"macd_hist_negative",
|
||||
"macd_cross_up",
|
||||
"macd_cross_down",
|
||||
"stoch_oversold",
|
||||
"stoch_overbought",
|
||||
"stoch_cross_up",
|
||||
"stoch_cross_down",
|
||||
)
|
||||
|
||||
|
||||
def _disparity_feature_names() -> tuple[str, ...]:
|
||||
"""기간별 이격도 과매수·과매도 불리언 컬럼명."""
|
||||
names: list[str] = []
|
||||
for p in DISPARITY_PERIODS:
|
||||
names.append(f"disparity_{p}_oversold")
|
||||
names.append(f"disparity_{p}_overbought")
|
||||
return tuple(names)
|
||||
|
||||
|
||||
DISPARITY_FEATURES: tuple[str, ...] = _disparity_feature_names()
|
||||
|
||||
CANDLE_SHAPE_FEATURES: tuple[str, ...] = (
|
||||
"body_strong",
|
||||
"body_weak",
|
||||
"hammer",
|
||||
"shooting_star",
|
||||
"bullish",
|
||||
"bearish",
|
||||
)
|
||||
|
||||
FEATURE_BOOL_COLS: tuple[str, ...] = (
|
||||
BB_EVENT_FEATURES
|
||||
+ BB_ZONE_FEATURES
|
||||
+ ICHI_FEATURES
|
||||
+ MACD_STOCH_FEATURES
|
||||
+ DISPARITY_FEATURES
|
||||
+ CANDLE_SHAPE_FEATURES
|
||||
)
|
||||
|
||||
|
||||
def compute_bar_features(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""단일 봉 DataFrame에 BB·일목·MACD·스토캐스틱·캔들 위치 특징을 추가합니다."""
|
||||
out = apply_bar_indicators(df.copy())
|
||||
if len(out) < 2:
|
||||
return out
|
||||
|
||||
o = out["Open"].astype(float)
|
||||
h = out["High"].astype(float)
|
||||
l = out["Low"].astype(float)
|
||||
c = out["Close"].astype(float)
|
||||
prev_c = c.shift(1)
|
||||
upper = out["Upper"].astype(float)
|
||||
lower = out["Lower"].astype(float)
|
||||
prev_upper = upper.shift(1)
|
||||
prev_lower = lower.shift(1)
|
||||
|
||||
rng = (h - l).replace(0, np.nan)
|
||||
body = (c - o).abs()
|
||||
out["range_pct"] = (rng / c.replace(0, np.nan)) * 100
|
||||
out["body_ratio"] = (body / rng).fillna(0).clip(0, 1)
|
||||
out["upper_wick_ratio"] = ((h - np.maximum(o, c)) / rng).fillna(0).clip(0, 1)
|
||||
out["lower_wick_ratio"] = ((np.minimum(o, c) - l) / rng).fillna(0).clip(0, 1)
|
||||
out["ret_pct"] = ((c - prev_c) / prev_c.replace(0, np.nan)) * 100
|
||||
|
||||
pos = out["bb_pos"].astype(float)
|
||||
out["bb_zone_bottom"] = (pos < 0.15).astype(int)
|
||||
out["bb_zone_low"] = ((pos >= 0.15) & (pos < 0.35)).astype(int)
|
||||
out["bb_zone_mid"] = ((pos >= 0.35) & (pos < 0.65)).astype(int)
|
||||
out["bb_zone_high"] = ((pos >= 0.65) & (pos < 0.85)).astype(int)
|
||||
out["bb_zone_top"] = (pos >= 0.85).astype(int)
|
||||
|
||||
out["cross_up_lower"] = ((prev_c <= prev_lower) & (c > lower)).astype(int)
|
||||
out["cross_up_upper"] = ((prev_c < prev_upper) & (c >= upper)).astype(int)
|
||||
out["cross_down_lower"] = ((prev_c >= prev_lower) & (c < lower)).astype(int)
|
||||
out["below_lower"] = (c < lower).astype(int)
|
||||
out["above_upper"] = (c > upper).astype(int)
|
||||
out["inside_band"] = ((c >= lower) & (c <= upper)).astype(int)
|
||||
out["bb_pos_low"] = (pos < 0.2).astype(int)
|
||||
out["bb_pos_high"] = (pos > 0.8).astype(int)
|
||||
out["squeeze"] = (out["BB_Width"] < BB_MIN_WIDTH_PCT).astype(int)
|
||||
|
||||
ct = out["ichi_cloud_top"].astype(float)
|
||||
cb = out["ichi_cloud_bottom"].astype(float)
|
||||
ten = out["ichi_tenkan"].astype(float)
|
||||
kij = out["ichi_kijun"].astype(float)
|
||||
prev_ten = ten.shift(1)
|
||||
prev_kij = kij.shift(1)
|
||||
|
||||
out["ichi_above_cloud"] = (c > ct).astype(int)
|
||||
out["ichi_below_cloud"] = (c < cb).astype(int)
|
||||
out["ichi_in_cloud"] = ((c >= cb) & (c <= ct)).astype(int)
|
||||
out["ichi_cloud_bull"] = (out["ichi_span_a"] > out["ichi_span_b"]).astype(int)
|
||||
out["ichi_cloud_bear"] = (out["ichi_span_a"] < out["ichi_span_b"]).astype(int)
|
||||
out["ichi_tk_bull"] = (ten > kij).astype(int)
|
||||
out["ichi_tk_bear"] = (ten < kij).astype(int)
|
||||
out["ichi_price_above_tenkan"] = (c > ten).astype(int)
|
||||
out["ichi_price_below_kijun"] = (c < kij).astype(int)
|
||||
out["ichi_tk_cross_up"] = ((prev_ten <= prev_kij) & (ten > kij)).astype(int)
|
||||
out["ichi_tk_cross_down"] = ((prev_ten >= prev_kij) & (ten < kij)).astype(int)
|
||||
|
||||
out["body_strong"] = (out["body_ratio"] > 0.55).astype(int)
|
||||
out["body_weak"] = (out["body_ratio"] < 0.25).astype(int)
|
||||
out["hammer"] = ((out["lower_wick_ratio"] > 0.45) & (out["body_ratio"] < 0.35)).astype(int)
|
||||
out["shooting_star"] = ((out["upper_wick_ratio"] > 0.45) & (out["body_ratio"] < 0.35)).astype(int)
|
||||
out["bullish"] = (c > o).astype(int)
|
||||
out["bearish"] = (c < o).astype(int)
|
||||
|
||||
if "macd_hist" in out.columns:
|
||||
mh = out["macd_hist"].astype(float)
|
||||
prev_mh = mh.shift(1)
|
||||
ml = out["macd_line"].astype(float)
|
||||
ms = out["macd_signal"].astype(float)
|
||||
prev_ml = ml.shift(1)
|
||||
prev_ms = ms.shift(1)
|
||||
out["macd_hist_positive"] = (mh > 0).astype(int)
|
||||
out["macd_hist_negative"] = (mh < 0).astype(int)
|
||||
out["macd_cross_up"] = ((prev_ml <= prev_ms) & (ml > ms)).astype(int)
|
||||
out["macd_cross_down"] = ((prev_ml >= prev_ms) & (ml < ms)).astype(int)
|
||||
|
||||
if "stoch_k" in out.columns:
|
||||
sk = out["stoch_k"].astype(float)
|
||||
sd = out["stoch_d"].astype(float)
|
||||
prev_sk = sk.shift(1)
|
||||
prev_sd = sd.shift(1)
|
||||
out["stoch_oversold"] = (sk <= STOCH_OVERSOLD).astype(int)
|
||||
out["stoch_overbought"] = (sk >= STOCH_OVERBOUGHT).astype(int)
|
||||
out["stoch_cross_up"] = ((prev_sk <= prev_sd) & (sk > sd)).astype(int)
|
||||
out["stoch_cross_down"] = ((prev_sk >= prev_sd) & (sk < sd)).astype(int)
|
||||
|
||||
from config import DISPARITY_OVERBOUGHT, DISPARITY_OVERSOLD
|
||||
|
||||
for p in DISPARITY_PERIODS:
|
||||
col = disparity_column(p)
|
||||
if col not in out.columns:
|
||||
continue
|
||||
d = out[col].astype(float)
|
||||
out[f"disparity_{p}_oversold"] = (d <= DISPARITY_OVERSOLD).astype(int)
|
||||
out[f"disparity_{p}_overbought"] = (d >= DISPARITY_OVERBOUGHT).astype(int)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def describe_latest_position(df: pd.DataFrame, interval: int) -> dict:
|
||||
"""한 봉의 최신 BB·일목 위치 요약."""
|
||||
feat = compute_bar_features(df)
|
||||
if feat.empty:
|
||||
return {"interval": interval, "label": interval_display(interval)}
|
||||
row = feat.iloc[-1]
|
||||
pos = float(row.get("bb_pos", 0.5))
|
||||
bb_zone = "mid"
|
||||
for z in BB_ZONE_FEATURES:
|
||||
if int(row.get(z, 0)) == 1:
|
||||
bb_zone = z.replace("bb_zone_", "")
|
||||
break
|
||||
ichi_pos = "in_cloud"
|
||||
if int(row.get("ichi_above_cloud", 0)):
|
||||
ichi_pos = "above_cloud"
|
||||
elif int(row.get("ichi_below_cloud", 0)):
|
||||
ichi_pos = "below_cloud"
|
||||
|
||||
snap: dict = {
|
||||
"interval": interval,
|
||||
"label": interval_display(interval),
|
||||
"close": float(row["Close"]),
|
||||
"bb_pos": round(pos, 3),
|
||||
"bb_zone": bb_zone,
|
||||
"bb_state": _bb_event_label(row),
|
||||
"ichi_position": ichi_pos,
|
||||
"ichi_tk": "bull" if int(row.get("ichi_tk_bull", 0)) else "bear",
|
||||
"ichi_cloud": "bull" if int(row.get("ichi_cloud_bull", 0)) else "bear",
|
||||
}
|
||||
if "macd_hist" in row.index and pd.notna(row["macd_hist"]):
|
||||
snap["macd_hist"] = round(float(row["macd_hist"]), 4)
|
||||
snap["macd_state"] = "bull" if float(row["macd_hist"]) > 0 else "bear"
|
||||
if "stoch_k" in row.index and pd.notna(row["stoch_k"]):
|
||||
sk = float(row["stoch_k"])
|
||||
snap["stoch_k"] = round(sk, 1)
|
||||
snap["stoch_d"] = round(float(row["stoch_d"]), 1)
|
||||
if sk <= STOCH_OVERSOLD:
|
||||
snap["stoch_zone"] = "oversold"
|
||||
elif sk >= STOCH_OVERBOUGHT:
|
||||
snap["stoch_zone"] = "overbought"
|
||||
else:
|
||||
snap["stoch_zone"] = "mid"
|
||||
disp_vals: dict[int, float] = {}
|
||||
for p in DISPARITY_PERIODS:
|
||||
col = disparity_column(p)
|
||||
if col in row.index and pd.notna(row[col]):
|
||||
disp_vals[p] = round(float(row[col]), 2)
|
||||
if disp_vals:
|
||||
snap["disparity"] = disp_vals
|
||||
primary_p = 20 if 20 in DISPARITY_PERIODS else DISPARITY_PERIODS[0]
|
||||
snap["disparity_primary"] = disp_vals.get(
|
||||
primary_p, next(iter(disp_vals.values()))
|
||||
)
|
||||
return snap
|
||||
|
||||
|
||||
def _bb_event_label(row: pd.Series) -> str:
|
||||
for name in (
|
||||
"cross_up_lower",
|
||||
"cross_up_upper",
|
||||
"cross_down_lower",
|
||||
"below_lower",
|
||||
"above_upper",
|
||||
"squeeze",
|
||||
"inside_band",
|
||||
):
|
||||
if int(row.get(name, 0)) == 1:
|
||||
return name
|
||||
return "neutral"
|
||||
|
||||
|
||||
def _merge_interval_features(
|
||||
master_index: pd.DatetimeIndex,
|
||||
feat: pd.DataFrame,
|
||||
prefix: str,
|
||||
) -> pd.DataFrame:
|
||||
"""master_index 길이와 동일한 간격 특징만 반환."""
|
||||
pick = [c for c in FEATURE_BOOL_COLS if c in feat.columns]
|
||||
numeric_cols = (
|
||||
"bb_pos",
|
||||
"body_ratio",
|
||||
"lower_wick_ratio",
|
||||
"ret_pct",
|
||||
"bb_width_pct",
|
||||
"macd_line",
|
||||
"macd_signal",
|
||||
"macd_hist",
|
||||
"stoch_k",
|
||||
"stoch_d",
|
||||
"RSI",
|
||||
) + tuple(disparity_column(p) for p in DISPARITY_PERIODS)
|
||||
extra = [c for c in numeric_cols if c in feat.columns]
|
||||
if "bb_width_pct" not in feat.columns and "BB_Width" in feat.columns:
|
||||
feat = feat.copy()
|
||||
feat["bb_width_pct"] = feat["BB_Width"]
|
||||
extra.append("bb_width_pct")
|
||||
|
||||
sub = feat[pick + extra].copy()
|
||||
sub.columns = [f"{prefix}_{c}" for c in sub.columns]
|
||||
|
||||
left = pd.DataFrame({"ts": master_index})
|
||||
right = sub.reset_index()
|
||||
time_col = right.columns[0]
|
||||
right = right.rename(columns={time_col: "ts"})
|
||||
|
||||
merged = pd.merge_asof(
|
||||
left.sort_values("ts"),
|
||||
right.sort_values("ts"),
|
||||
on="ts",
|
||||
direction="backward",
|
||||
)
|
||||
merged.index = master_index
|
||||
return merged.drop(columns=["ts"])
|
||||
|
||||
|
||||
def build_master_feature_matrix(frames: dict[int, pd.DataFrame]) -> pd.DataFrame:
|
||||
"""3분 타임라인에 모든 봉의 BB·일목·캔들 특징을 붙인 행렬."""
|
||||
entry = frames.get(ENTRY_INTERVAL)
|
||||
if entry is None or entry.empty:
|
||||
raise ValueError(f"{ENTRY_INTERVAL}분봉(ENTRY_INTERVAL) 데이터가 없습니다.")
|
||||
|
||||
entry_feat = compute_bar_features(entry)
|
||||
entry_feat = entry_feat[~entry_feat.index.duplicated(keep="last")].sort_index()
|
||||
|
||||
p0 = interval_prefix(ENTRY_INTERVAL)
|
||||
ohlc = ["Open", "High", "Low", "Close", "Volume", "Upper", "Lower", "MA"]
|
||||
master = entry_feat[[c for c in ohlc if c in entry_feat.columns]].copy()
|
||||
|
||||
for col in FEATURE_BOOL_COLS:
|
||||
if col in entry_feat.columns:
|
||||
master[f"{p0}_{col}"] = entry_feat[col]
|
||||
for col in ("bb_pos", "body_ratio", "lower_wick_ratio", "ret_pct", "bb_width_pct", "BB_Width"):
|
||||
if col in entry_feat.columns:
|
||||
master[f"{p0}_{col}"] = entry_feat[col]
|
||||
|
||||
for interval in ALL_INTERVALS:
|
||||
if interval == ENTRY_INTERVAL:
|
||||
continue
|
||||
df = frames.get(interval)
|
||||
if df is None or df.empty:
|
||||
continue
|
||||
feat = compute_bar_features(df)
|
||||
feat = feat[~feat.index.duplicated(keep="last")].sort_index()
|
||||
prefix = interval_prefix(interval)
|
||||
merged = _merge_interval_features(master.index, feat, prefix)
|
||||
master = pd.concat([master, merged], axis=1)
|
||||
|
||||
return master.loc[:, ~master.columns.duplicated()]
|
||||
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