""" 인과적(미래 미사용) GT 스타일 신호 — t봉 시점에 t 이하 데이터만 사용. ZigZag/국소극값: pivot bar i-order 는 bar i 에서 확정 (i-order..i 구간만 관측). """ from __future__ import annotations import numpy as np import pandas as pd from config import ( GT_BUY_BB_MAX, GT_BUY_MIN_SWING_PCT, GT_MIN_SWING_PCT, GT_PIVOT_ORDER, ) def _confirmed_trough_mask( low: np.ndarray, order: int, ) -> np.ndarray: """ bar i 에서 i-order 봉이 저점임을 확정 (low[i-order:i+1] 만 사용). Args: low: Low 가격 배열. order: pivot 반경(봉). Returns: 길이 n, i 에 1이면 i 시점 매수 확인 신호. """ n = len(low) out = np.zeros(n, dtype=np.int8) for i in range(2 * order, n): p = i - order seg = low[p - order : i + 1] if len(seg) == 0: continue if low[p] <= seg.min() + 1e-12: out[i] = 1 return out def _confirmed_peak_mask( high: np.ndarray, order: int, ) -> np.ndarray: """ bar i 에서 i-order 봉이 고점임을 확정. Args: high: High 가격 배열. order: pivot 반경. Returns: i 시점 매도 확인 신호. """ n = len(high) out = np.zeros(n, dtype=np.int8) for i in range(2 * order, n): p = i - order seg = high[p - order : i + 1] if len(seg) == 0: continue if high[p] >= seg.max() - 1e-12: out[i] = 1 return out def _zigzag_filter_causal( confirm: np.ndarray, prices: np.ndarray, min_swing_pct: float, kind: str, ) -> np.ndarray: """ 확정 피벗에 ZigZag 최소 스윙% 필터 (인과적, 순차 갱신). Args: confirm: bar i 에 확정 플래그. prices: pivot 가격 (i-order 위치의 low/high). pivot_indices: confirm==1 인 bar index. min_swing_pct: 최소 스윙 %. kind: trough | peak. Returns: zigzag 통과 시점에 1. """ n = len(confirm) out = np.zeros(n, dtype=np.int8) order = GT_PIVOT_ORDER last_kind: str | None = None last_price = 0.0 min_ratio = min_swing_pct / 100.0 for i in range(n): if confirm[i] != 1: continue p = i - order if p < 0: continue price = float(prices[p]) if last_kind is None: out[i] = 1 last_kind = kind last_price = price continue if kind == last_kind: if kind == "trough" and price < last_price: out[i - 1] = 0 out[i] = 1 last_price = price elif kind == "peak" and price > last_price: out[i - 1] = 0 out[i] = 1 last_price = price continue move = abs(price - last_price) / max(last_price, 1e-9) if move >= min_ratio: out[i] = 1 last_kind = kind last_price = price return out def enrich_scan_frame_gt_signals_causal( frame: pd.DataFrame, *, pivot_order: int = GT_PIVOT_ORDER, buy_swing_pct: float = GT_BUY_MIN_SWING_PCT, sell_swing_pct: float = GT_MIN_SWING_PCT, bb_max: float = GT_BUY_BB_MAX, ) -> pd.DataFrame: """ 인과적 GT 신호 컬럼 (gt_*). t 시점 신호는 데이터 index<=t 만 사용. Args: frame: m3 스캔 프레임. pivot_order: 확정 지연(봉). buy_swing_pct: 매수 ZigZag 스윙%. sell_swing_pct: 매도 ZigZag 스윙%. bb_max: BB 하단 필터. Returns: gt_* 컬럼 추가 DataFrame. """ out = frame.copy() if "Low" not in out.columns or "High" not in out.columns: return out low = out["Low"].astype(float).values high = out["High"].astype(float).values n = len(low) trough_conf = _confirmed_trough_mask(low, pivot_order) peak_conf = _confirmed_peak_mask(high, pivot_order) trough_z = _zigzag_filter_causal( trough_conf, low, buy_swing_pct, "trough" ) peak_z = _zigzag_filter_causal( peak_conf, high, sell_swing_pct, "peak" ) out["gt_trough_local"] = trough_conf out["gt_peak_local"] = peak_conf out["gt_trough_zigzag"] = trough_z out["gt_peak_zigzag"] = peak_z bb_ok = pd.Series(True, index=out.index) if "bb_pos" in out.columns: bb = pd.to_numeric(out["bb_pos"], errors="coerce") bb_ok = bb <= bb_max out["gt_buy_signal"] = (pd.Series(trough_z, index=out.index) == 1) & bb_ok out["gt_buy_signal"] = out["gt_buy_signal"].astype(int) out["gt_sell_signal"] = pd.Series(peak_z, index=out.index).astype(int) out["gt_signal_causal"] = 1 return out