GT MTF 프로필·캘리브레이션과 04 매칭/시뮬/실거래 파이프라인을 추가한다.

3분~일봉 GT 타점 분석(03c), leg 체결 순서 수정, 총자산 90% 검증 루프,
walk-forward Go/No-Go 시뮬, monitor·live_trader 및 reference 문서를 포함한다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-31 11:27:50 +09:00
parent b52d61b777
commit 2cb67c42b3
47 changed files with 5956 additions and 209 deletions

View File

@@ -0,0 +1,318 @@
"""
규칙 조건 벡터 평가·MTF 스캔 프레임 병합.
"""
from __future__ import annotations
from typing import Any
import numpy as np
import pandas as pd
from config import GENERAL_ANALYSIS_INTERVALS, MATCH_PRIMARY_INTERVAL
from deepcoin.analysis.general_analysis_core import interval_tf_prefix
from deepcoin.analysis.general_analysis_pipeline import general_analysis_enrich_bars
from config import (
ALIGN_RSI_CONFLICT_TIMING_HIGH,
ALIGN_RSI_CONFLICT_TIMING_LOW,
ALIGN_RSI_CONFLICT_TREND_HIGH,
ALIGN_RSI_CONFLICT_TREND_LOW,
ALIGN_RSI_OVERBOUGHT,
ALIGN_RSI_OVERSOLD,
TIMING_INTERVALS,
TREND_INTERVALS,
)
from deepcoin.analysis.general_analysis_core import ga_col
def _add_align_columns_vectorized(frame: pd.DataFrame) -> pd.DataFrame:
"""
스캔 프레임에 ga_align_* 컬럼을 벡터 연산으로 추가합니다.
Args:
frame: TF 접두사 컬럼이 포함된 DataFrame.
Returns:
align 컬럼이 추가된 DataFrame.
"""
out = frame.copy()
rsi_oversold = pd.Series(0, index=out.index, dtype=float)
rsi_overbought = pd.Series(0, index=out.index, dtype=float)
n_timing = 0
for iv in TIMING_INTERVALS:
p = interval_tf_prefix(iv)
rk = f"{p}_RSI"
if rk not in out.columns:
continue
n_timing += 1
rsi = pd.to_numeric(out[rk], errors="coerce")
rsi_oversold += (rsi < ALIGN_RSI_OVERSOLD).astype(int)
rsi_overbought += (rsi > ALIGN_RSI_OVERBOUGHT).astype(int)
trend_up = pd.Series(0, index=out.index, dtype=float)
trend_down = pd.Series(0, index=out.index, dtype=float)
n_trend = 0
for iv in TREND_INTERVALS:
p = interval_tf_prefix(iv)
sk = f"{p}_{ga_col('struct_trend')}"
if sk not in out.columns:
continue
n_trend += 1
st = out[sk].astype(str)
trend_up += (st == "up").astype(int)
trend_down += (st == "down").astype(int)
denom_t = max(n_timing, 1)
denom_r = max(n_trend, 1)
out["ga_align_rsi_oversold_tf"] = rsi_oversold
out["ga_align_rsi_overbought_tf"] = rsi_overbought
out["ga_align_trend_up_tf"] = trend_up
out["ga_align_trend_down_tf"] = trend_down
out["ga_align_timing_buy_score"] = (rsi_oversold / denom_t).round(3)
out["ga_align_timing_sell_score"] = (rsi_overbought / denom_t).round(3)
out["ga_align_trend_score"] = ((trend_up - trend_down) / denom_r).round(3)
conflict = pd.Series(0, index=out.index, dtype=int)
m3_rsi = out.get("m3_RSI")
d1_rsi = out.get("d1_RSI")
if m3_rsi is not None and d1_rsi is not None:
m3v = pd.to_numeric(m3_rsi, errors="coerce")
d1v = pd.to_numeric(d1_rsi, errors="coerce")
conflict = (
((m3v < ALIGN_RSI_CONFLICT_TIMING_LOW) & (d1v > ALIGN_RSI_CONFLICT_TREND_HIGH))
| ((m3v > ALIGN_RSI_CONFLICT_TIMING_HIGH) & (d1v < ALIGN_RSI_CONFLICT_TREND_LOW))
).astype(int)
out["ga_align_mtf_conflict"] = conflict
return out
def _scalar_float(val: Any) -> float:
"""Series/ndarray 스칼라를 float로 변환."""
if isinstance(val, pd.Series):
val = val.iloc[0]
return float(val)
def conditions_columns(rules: list[dict[str, Any]]) -> set[str]:
"""
규칙 목록에서 참조하는 컬럼명 집합을 반환합니다.
Args:
rules: rule_candidates 항목 리스트.
Returns:
컬럼명 set.
"""
cols: set[str] = set()
for rule in rules:
for cond in rule.get("conditions", []):
c = cond.get("col")
if c:
cols.add(c)
return cols
def build_mtf_scan_frame(
frames: dict[int, pd.DataFrame],
needed_cols: set[str],
) -> pd.DataFrame:
"""
주간격(m3) 인덱스에 필요 컬럼만 merge_asof로 붙인 스캔용 DataFrame을 만듭니다.
Args:
frames: interval → OHLCV.
needed_cols: 규칙 평가에 필요한 컬럼명.
Returns:
m3 인덱스 wide DataFrame (close 포함).
"""
primary = MATCH_PRIMARY_INTERVAL
raw = frames.get(primary)
if raw is None or raw.empty:
raise RuntimeError(f"주간격 {primary}분 데이터 없음")
print(f"[04b] Phase A: 8TF enrich (스캔용)...")
enriched: dict[int, pd.DataFrame] = {}
for iv in GENERAL_ANALYSIS_INTERVALS:
r = frames.get(iv)
if r is None or r.empty:
continue
label = interval_tf_prefix(iv)
print(f" enrich {label} ({len(r):,}봉)...")
enriched[iv] = general_analysis_enrich_bars(r, iv, full_context=True)
base = enriched[primary].copy()
if not isinstance(base.index, pd.DatetimeIndex):
base.index = pd.to_datetime(base.index)
base = base.sort_index()
out = pd.DataFrame(index=base.index)
close_col = "close" if "close" in base.columns else "Close"
out["close"] = base[close_col].astype(float)
def _source_col(prefixed: str, prefix: str, ef: pd.DataFrame) -> str | None:
"""m3_RSI → RSI, m60_ga_struct_trend → ga_struct_trend."""
if not prefixed.startswith(f"{prefix}_"):
return None
suffix = prefixed[len(prefix) + 1 :]
if suffix in ef.columns:
return suffix
return None
for iv in GENERAL_ANALYSIS_INTERVALS:
ef = enriched.get(iv)
if ef is None:
continue
p = interval_tf_prefix(iv)
for col in needed_cols:
if col in out.columns or not col.startswith(f"{p}_"):
continue
src = _source_col(col, p, ef)
if src is None:
continue
if iv == primary:
out[col] = ef[src].reindex(out.index)
else:
sub = ef[[src]].copy()
if not isinstance(sub.index, pd.DatetimeIndex):
sub.index = pd.to_datetime(sub.index)
sub = sub.sort_index().rename(columns={src: col})
merged = pd.merge_asof(
out.reset_index(names="_ts"),
sub.reset_index(names="_ts"),
on="_ts",
direction="backward",
).set_index("_ts")
out[col] = merged[col].values
align_needed = [c for c in needed_cols if c.startswith("ga_align_")]
if align_needed:
out = _add_align_columns_vectorized(out)
out = out.loc[:, ~out.columns.duplicated()]
out = out.dropna(subset=["close"])
print(f"[04b] 스캔 프레임: {len(out):,}× {len(out.columns)}")
return out
def _eval_one_condition(
frame: pd.DataFrame,
cond: dict[str, Any],
) -> pd.Series:
"""
단일 조건 boolean Series.
Args:
frame: 평가 대상.
cond: {col, op, ...}.
Returns:
boolean Series.
"""
col = cond.get("col")
if not col or col not in frame.columns:
return pd.Series(False, index=frame.index)
s = frame[col]
op = cond.get("op", "between")
if op == "between":
lo, hi = float(cond["lo"]), float(cond["hi"])
ok = pd.to_numeric(s, errors="coerce")
part = (ok >= lo) & (ok <= hi)
elif op == "gte":
part = pd.to_numeric(s, errors="coerce") >= float(cond["value"])
elif op == "lte":
part = pd.to_numeric(s, errors="coerce") <= float(cond["value"])
elif op == "eq":
val = cond["value"]
if isinstance(val, (int, float)) and pd.api.types.is_numeric_dtype(s):
part = pd.to_numeric(s, errors="coerce") == float(val)
else:
part = s.astype(str) == str(val)
elif op == "eq_int":
part = (
pd.to_numeric(s, errors="coerce").fillna(-999).astype(int)
== int(cond["value"])
)
else:
part = pd.Series(False, index=frame.index)
return part.fillna(False)
def eval_conditions(frame: pd.DataFrame, conditions: list[dict[str, Any]]) -> pd.Series:
"""
단일 규칙의 조건을 모두 AND로 평가합니다.
Args:
frame: 스캔용 DataFrame.
conditions: {col, op, ...} 리스트.
Returns:
boolean Series (인덱스=frame.index).
"""
mask = pd.Series(True, index=frame.index)
for cond in conditions:
mask &= _eval_one_condition(frame, cond)
return mask
def eval_rule_mask(frame: pd.DataFrame, rule: dict[str, Any]) -> pd.Series:
"""
규칙 dict 평가 (logic=and|or).
Args:
frame: 스캔/스냅샷 DataFrame.
rule: conditions, logic 키 포함.
Returns:
boolean Series.
"""
conditions = rule.get("conditions") or []
if not conditions:
return pd.Series(False, index=frame.index)
logic = str(rule.get("logic", "and")).lower()
if logic == "or":
mask = pd.Series(False, index=frame.index)
for cond in conditions:
mask |= _eval_one_condition(frame, cond)
return mask
return eval_conditions(frame, conditions)
def scan_rule_fires(
frame: pd.DataFrame,
rules: list[dict[str, Any]],
) -> pd.DataFrame:
"""
모든 규칙 후보에 대해 발화 시각을 수집합니다.
Args:
frame: build_mtf_scan_frame 결과.
rules: rule_candidates.
Returns:
fire_id, rule_id, side, dt, close 컬럼 DataFrame.
"""
rows: list[dict[str, Any]] = []
fid = 0
for rule in rules:
rid = rule["rule_id"]
side = rule["side"]
mask = eval_rule_mask(frame, rule)
hits = frame.index[mask]
close_s = frame["close"]
if isinstance(close_s, pd.DataFrame):
close_s = close_s.iloc[:, 0]
for ts in hits:
rows.append(
{
"fire_id": fid,
"rule_id": rid,
"side": side,
"dt": ts.strftime("%Y-%m-%d %H:%M:%S"),
"close": _scalar_float(close_s.loc[ts]),
}
)
fid += 1
print(f" 규칙 {rid}: 발화 {len(hits):,}")
if not rows:
return pd.DataFrame(columns=["fire_id", "rule_id", "side", "dt", "close"])
return pd.DataFrame(rows)