hybrid DD tier와 Option C 2차(+1000%) 검증을 추가하고 실거래 사이징을 정합한다.
인과 GT leg 엔진·drawdown tier·train 캘리브레이션, Phase 2 Go/No-Go 및 시뮬 리포트를 반영한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
18
.env.example
18
.env.example
@@ -16,6 +16,24 @@ GT_LARGE_LEG_TOP_PCT=0.2
|
|||||||
# 시뮬·스캔: 1=인과적(운영 정합), 0=사후 ZigZag(정답 라벨용)
|
# 시뮬·스캔: 1=인과적(운영 정합), 0=사후 ZigZag(정답 라벨용)
|
||||||
GT_SIGNAL_CAUSAL=1
|
GT_SIGNAL_CAUSAL=1
|
||||||
SIM_CAUSAL_TIER=1
|
SIM_CAUSAL_TIER=1
|
||||||
|
# 인과 GT leg 엔진 (scripts/04_causal_gt_calibrate.py)
|
||||||
|
CAUSAL_GT_PEAK_MODE=local
|
||||||
|
CAUSAL_GT_MIN_LEG_PCT=5.0
|
||||||
|
CAUSAL_GT_MIN_BARS_BETWEEN_LEGS=60
|
||||||
|
CAUSAL_GT_USE_LOCAL_TROUGH=1
|
||||||
|
CAUSAL_GT_DD_LARGE_PCT=8.0
|
||||||
|
CAUSAL_GT_DD_MEDIUM_PCT=4.0
|
||||||
|
GT_BUY_PCT_MEDIUM_LEG=0.25
|
||||||
|
SIM_TIER_CONVICTION_DD_PCT=10.0
|
||||||
|
# hybrid tier 승격 (auto=검증 통과 시 hybrid)
|
||||||
|
SIM_PRIMARY_SIZING=auto
|
||||||
|
SIM_HYBRID_MIN_HOLDOUT_PNL_PCT=0.0
|
||||||
|
SIM_HYBRID_MAX_MDD_PCT=30.0
|
||||||
|
SIM_OPTION_C_TARGET_PNL_PCT=300.0
|
||||||
|
SIM_OPTION_C_PHASE2_TARGET_PNL_PCT=1000.0
|
||||||
|
SIM_OPTION_C_PHASE2_FEE_STRESS_RATIO=0.85
|
||||||
|
SIM_OPTION_C_MIN_GT_CAPTURE=0.23
|
||||||
|
SIM_HYBRID_PORTFOLIO_WF_MIN_RATIO=0.5
|
||||||
GT_BUY_WEIGHT_RULE=inverse_price_normalized
|
GT_BUY_WEIGHT_RULE=inverse_price_normalized
|
||||||
GT_SELL_SPLIT_WEIGHTS=0.65,0.35
|
GT_SELL_SPLIT_WEIGHTS=0.65,0.35
|
||||||
|
|
||||||
|
|||||||
26
config.py
26
config.py
@@ -223,6 +223,21 @@ SIM_CAUSAL_TIER = _getenv("SIM_CAUSAL_TIER", "1").strip().lower() in (
|
|||||||
"true",
|
"true",
|
||||||
"yes",
|
"yes",
|
||||||
)
|
)
|
||||||
|
# 인과 GT leg 엔진 (Option C +300% 경로)
|
||||||
|
CAUSAL_GT_PEAK_MODE = _getenv("CAUSAL_GT_PEAK_MODE", "local").strip().lower()
|
||||||
|
if CAUSAL_GT_PEAK_MODE not in ("local", "zigzag"):
|
||||||
|
CAUSAL_GT_PEAK_MODE = "local"
|
||||||
|
CAUSAL_GT_MIN_LEG_PCT = _getenv_float("CAUSAL_GT_MIN_LEG_PCT", "5.0")
|
||||||
|
CAUSAL_GT_MIN_BARS_BETWEEN_LEGS = _getenv_int("CAUSAL_GT_MIN_BARS_BETWEEN_LEGS", "60")
|
||||||
|
CAUSAL_GT_USE_LOCAL_TROUGH = _getenv("CAUSAL_GT_USE_LOCAL_TROUGH", "1").strip().lower() in (
|
||||||
|
"1",
|
||||||
|
"true",
|
||||||
|
"yes",
|
||||||
|
)
|
||||||
|
CAUSAL_GT_DD_LARGE_PCT = _getenv_float("CAUSAL_GT_DD_LARGE_PCT", "8.0")
|
||||||
|
CAUSAL_GT_DD_MEDIUM_PCT = _getenv_float("CAUSAL_GT_DD_MEDIUM_PCT", "4.0")
|
||||||
|
GT_BUY_PCT_MEDIUM_LEG = _getenv_float("GT_BUY_PCT_MEDIUM_LEG", "0.25")
|
||||||
|
SIM_TIER_CONVICTION_DD_PCT = _getenv_float("SIM_TIER_CONVICTION_DD_PCT", "10.0")
|
||||||
TRADING_FEE_RATE = _getenv_float("TRADING_FEE_RATE", "0.0005")
|
TRADING_FEE_RATE = _getenv_float("TRADING_FEE_RATE", "0.0005")
|
||||||
|
|
||||||
# --- 모니터 / API 수집 ---
|
# --- 모니터 / API 수집 ---
|
||||||
@@ -347,6 +362,17 @@ SIM_FEE_STRESS_MULT = _getenv_float("SIM_FEE_STRESS_MULT", "2.0")
|
|||||||
SIM_GO_MIN_HOLDOUT_EV = _getenv_float("SIM_GO_MIN_HOLDOUT_EV", "0.0")
|
SIM_GO_MIN_HOLDOUT_EV = _getenv_float("SIM_GO_MIN_HOLDOUT_EV", "0.0")
|
||||||
SIM_GO_MIN_HOLDOUT_PF = _getenv_float("SIM_GO_MIN_HOLDOUT_PF", "1.0")
|
SIM_GO_MIN_HOLDOUT_PF = _getenv_float("SIM_GO_MIN_HOLDOUT_PF", "1.0")
|
||||||
SIM_GO_WF_POSITIVE_RATIO = _getenv_float("SIM_GO_WF_POSITIVE_RATIO", "0.5")
|
SIM_GO_WF_POSITIVE_RATIO = _getenv_float("SIM_GO_WF_POSITIVE_RATIO", "0.5")
|
||||||
|
# hybrid DD tier 승격·검증 (Option C +300%)
|
||||||
|
SIM_HYBRID_MIN_HOLDOUT_PNL_PCT = _getenv_float("SIM_HYBRID_MIN_HOLDOUT_PNL_PCT", "0.0")
|
||||||
|
SIM_HYBRID_MAX_MDD_PCT = _getenv_float("SIM_HYBRID_MAX_MDD_PCT", "30.0")
|
||||||
|
SIM_OPTION_C_TARGET_PNL_PCT = _getenv_float("SIM_OPTION_C_TARGET_PNL_PCT", "300.0")
|
||||||
|
SIM_OPTION_C_PHASE2_TARGET_PNL_PCT = _getenv_float("SIM_OPTION_C_PHASE2_TARGET_PNL_PCT", "1000.0")
|
||||||
|
SIM_OPTION_C_PHASE2_FEE_STRESS_RATIO = _getenv_float("SIM_OPTION_C_PHASE2_FEE_STRESS_RATIO", "0.85")
|
||||||
|
SIM_OPTION_C_MIN_GT_CAPTURE = _getenv_float("SIM_OPTION_C_MIN_GT_CAPTURE", "0.23")
|
||||||
|
SIM_HYBRID_PORTFOLIO_WF_MIN_RATIO = _getenv_float("SIM_HYBRID_PORTFOLIO_WF_MIN_RATIO", "0.5")
|
||||||
|
SIM_PRIMARY_SIZING = _getenv("SIM_PRIMARY_SIZING", "auto").strip().lower()
|
||||||
|
if SIM_PRIMARY_SIZING not in ("auto", "hybrid", "causal_tier"):
|
||||||
|
SIM_PRIMARY_SIZING = "auto"
|
||||||
|
|
||||||
# --- 3단계 실거래 (오픈 전 문서·시뮬 Go 필수) ---
|
# --- 3단계 실거래 (오픈 전 문서·시뮬 Go 필수) ---
|
||||||
LIVE_TRADING_ENABLED = _getenv("LIVE_TRADING_ENABLED", "0").strip() in (
|
LIVE_TRADING_ENABLED = _getenv("LIVE_TRADING_ENABLED", "0").strip() in (
|
||||||
|
|||||||
173
deepcoin/ground_truth/causal_gt_calibrate.py
Normal file
173
deepcoin/ground_truth/causal_gt_calibrate.py
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
"""
|
||||||
|
인과 GT leg 엔진 파라미터 그리드 탐색·최적 저장.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from itertools import product
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from config import (
|
||||||
|
CHART_LOOKBACK_DAYS,
|
||||||
|
GT_BUY_BB_MAX,
|
||||||
|
GT_BUY_MIN_SWING_PCT,
|
||||||
|
GT_MIN_SWING_PCT,
|
||||||
|
GT_PIVOT_ORDER,
|
||||||
|
MATCH_PRIMARY_INTERVAL,
|
||||||
|
SYMBOL,
|
||||||
|
)
|
||||||
|
from deepcoin.data.mtf_bb import load_frames_from_db
|
||||||
|
from deepcoin.ground_truth.causal_gt_trades import simulate_causal_gt_portfolio
|
||||||
|
from deepcoin.ground_truth.ground_truth import load_ground_truth
|
||||||
|
from deepcoin.ops.monitor import Monitor
|
||||||
|
from deepcoin.paths import MATCHING_CAUSAL_GT_CALIBRATION_JSON, resolve_ground_truth_file
|
||||||
|
|
||||||
|
|
||||||
|
def default_causal_gt_params() -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
인과 GT leg 엔진 기본 파라미터.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
build_causal_split_buy_peak_sell_trades 키워드 인자.
|
||||||
|
"""
|
||||||
|
from config import (
|
||||||
|
CAUSAL_GT_MIN_BARS_BETWEEN_LEGS,
|
||||||
|
CAUSAL_GT_MIN_LEG_PCT,
|
||||||
|
CAUSAL_GT_PEAK_MODE,
|
||||||
|
CAUSAL_GT_USE_LOCAL_TROUGH,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pivot_order": GT_PIVOT_ORDER,
|
||||||
|
"buy_swing_pct": GT_BUY_MIN_SWING_PCT,
|
||||||
|
"sell_swing_pct": GT_MIN_SWING_PCT,
|
||||||
|
"bb_max": GT_BUY_BB_MAX,
|
||||||
|
"min_leg_pct": CAUSAL_GT_MIN_LEG_PCT,
|
||||||
|
"use_local_trough": CAUSAL_GT_USE_LOCAL_TROUGH,
|
||||||
|
"peak_mode": CAUSAL_GT_PEAK_MODE,
|
||||||
|
"min_bars_between_legs": CAUSAL_GT_MIN_BARS_BETWEEN_LEGS,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_causal_gt_params(path: Path | None = None) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
캘리브레이션 JSON 또는 config 기본값.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: JSON 경로. None이면 MATCHING_CAUSAL_GT_CALIBRATION_JSON.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
best params dict.
|
||||||
|
"""
|
||||||
|
p = path or MATCHING_CAUSAL_GT_CALIBRATION_JSON
|
||||||
|
if p.is_file():
|
||||||
|
data = json.loads(p.read_text(encoding="utf-8"))
|
||||||
|
best = data.get("best_params") or data.get("params")
|
||||||
|
if best:
|
||||||
|
return dict(best)
|
||||||
|
return default_causal_gt_params()
|
||||||
|
|
||||||
|
|
||||||
|
def _grid_space() -> dict[str, list[Any]]:
|
||||||
|
"""탐색 그리드 (로컬 peak 최적화 반영, 조합 ~864)."""
|
||||||
|
return {
|
||||||
|
"peak_mode": ["local", "zigzag"],
|
||||||
|
"pivot_order": [8, 10, 12, 15],
|
||||||
|
"buy_swing_pct": [2.0, 2.5, 3.0],
|
||||||
|
"sell_swing_pct": [3.0, 4.0],
|
||||||
|
"bb_max": [0.55, 0.65, 0.75],
|
||||||
|
"min_leg_pct": [3.0, 5.0, 8.0],
|
||||||
|
"min_bars_between_legs": [60, 90],
|
||||||
|
"use_local_trough": [True, False],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_causal_gt_calibration(
|
||||||
|
*,
|
||||||
|
min_trades: int = 30,
|
||||||
|
top_n: int = 20,
|
||||||
|
out_path: Path | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
그리드 탐색 후 최적 파라미터 JSON 저장.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
min_trades: 최소 체결 수 미만 조합 제외.
|
||||||
|
top_n: 상위 N개 기록.
|
||||||
|
out_path: 저장 경로.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
calibration report dict.
|
||||||
|
"""
|
||||||
|
gt = load_ground_truth(resolve_ground_truth_file()) or {}
|
||||||
|
mark = float((gt.get("summary") or {}).get("mark_price") or 0)
|
||||||
|
gt_pnl = float(
|
||||||
|
(gt.get("summary") or {}).get("pnl_pct")
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
mon = Monitor(cooldown_file=None)
|
||||||
|
frames = load_frames_from_db(mon, SYMBOL, lookback_days=CHART_LOOKBACK_DAYS)
|
||||||
|
df = frames[MATCH_PRIMARY_INTERVAL].copy()
|
||||||
|
|
||||||
|
grid = _grid_space()
|
||||||
|
keys = list(grid.keys())
|
||||||
|
results: list[dict[str, Any]] = []
|
||||||
|
total = 1
|
||||||
|
for k in keys:
|
||||||
|
total *= len(grid[k])
|
||||||
|
|
||||||
|
print(f"[causal_gt] 그리드 {total} 조합 탐색...")
|
||||||
|
done = 0
|
||||||
|
for combo in product(*(grid[k] for k in keys)):
|
||||||
|
params = dict(zip(keys, combo))
|
||||||
|
r = simulate_causal_gt_portfolio(df, last_price=mark or None, **params)
|
||||||
|
tc = int(r.get("trade_count") or 0)
|
||||||
|
done += 1
|
||||||
|
if done % 200 == 0:
|
||||||
|
print(f" ... {done}/{total}")
|
||||||
|
if tc < min_trades:
|
||||||
|
continue
|
||||||
|
pnl = float(r.get("pnl_pct") or 0)
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"pnl_pct": round(pnl, 2),
|
||||||
|
"trade_count": tc,
|
||||||
|
"leg_count": r.get("leg_count", 0),
|
||||||
|
"max_drawdown_pct": r.get("max_drawdown_pct"),
|
||||||
|
"capture_ratio": round(pnl / gt_pnl, 4) if gt_pnl else 0,
|
||||||
|
"params": params,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
results.sort(key=lambda x: x["pnl_pct"], reverse=True)
|
||||||
|
best = results[0] if results else None
|
||||||
|
report: dict[str, Any] = {
|
||||||
|
"symbol": SYMBOL,
|
||||||
|
"interval_min": MATCH_PRIMARY_INTERVAL,
|
||||||
|
"gt_pnl_pct": gt_pnl,
|
||||||
|
"grid_combinations": total,
|
||||||
|
"valid_combinations": len(results),
|
||||||
|
"min_trades": min_trades,
|
||||||
|
"best": best,
|
||||||
|
"best_params": best["params"] if best else default_causal_gt_params(),
|
||||||
|
"top": results[:top_n],
|
||||||
|
"target_pnl_pct": 300.0,
|
||||||
|
"target_met": bool(best and best["pnl_pct"] >= 300.0),
|
||||||
|
}
|
||||||
|
|
||||||
|
out = out_path or MATCHING_CAUSAL_GT_CALIBRATION_JSON
|
||||||
|
out.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
print(f"[causal_gt] 저장: {out}")
|
||||||
|
if best:
|
||||||
|
print(
|
||||||
|
f"[causal_gt] 최적 PnL={best['pnl_pct']}% "
|
||||||
|
f"trades={best['trade_count']} legs={best['leg_count']} "
|
||||||
|
f"capture={best.get('capture_ratio', 0):.2%}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print("[causal_gt] 유효 조합 없음")
|
||||||
|
return report
|
||||||
447
deepcoin/ground_truth/causal_gt_hybrid.py
Normal file
447
deepcoin/ground_truth/causal_gt_hybrid.py
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
"""
|
||||||
|
Phase 3: monitor 발화 + drawdown/past-leg tier (인과적).
|
||||||
|
|
||||||
|
매도는 monitor(sell_mtf_cross) 유지, tier만 drawdown·과거 leg 수익으로 강화합니다.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from config import (
|
||||||
|
CAUSAL_GT_DD_LARGE_PCT,
|
||||||
|
CAUSAL_GT_DD_MEDIUM_PCT,
|
||||||
|
GT_BUY_PCT_LARGE_LEG,
|
||||||
|
GT_BUY_PCT_MEDIUM_LEG,
|
||||||
|
GT_BUY_PCT_SMALL_LEG,
|
||||||
|
GT_INITIAL_CASH_KRW,
|
||||||
|
SIM_TIER_CONVICTION_DD_PCT,
|
||||||
|
TRADING_FEE_RATE,
|
||||||
|
)
|
||||||
|
from deepcoin.ground_truth.gt_allocation import (
|
||||||
|
allocate_order_amounts_chronological,
|
||||||
|
simulate_portfolio_summary,
|
||||||
|
)
|
||||||
|
from deepcoin.matching.portfolio_sim import sort_fires_chronological
|
||||||
|
from deepcoin.matching.position_sizing import enrich_sim_trades_with_gt_weights
|
||||||
|
|
||||||
|
|
||||||
|
def _deduped_ohlc(df: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
DatetimeIndex 중복 제거·정렬 (drawdown lookup용).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: OHLC DataFrame.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
index unique OHLC.
|
||||||
|
"""
|
||||||
|
if df.empty:
|
||||||
|
return df
|
||||||
|
out = df.sort_index()
|
||||||
|
if not out.index.is_unique:
|
||||||
|
out = out[~out.index.duplicated(keep="last")]
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _close_series_from_df(df: pd.DataFrame) -> pd.Series:
|
||||||
|
"""
|
||||||
|
OHLC DataFrame에서 종가 시리즈 추출 (positional index).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: Open/Close 또는 open/close 컬럼을 가진 OHLC.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float 종가 시리즈.
|
||||||
|
"""
|
||||||
|
if df.empty:
|
||||||
|
return pd.Series(dtype=float)
|
||||||
|
frame = _deduped_ohlc(df)
|
||||||
|
for col in ("close", "Close"):
|
||||||
|
if col in frame.columns:
|
||||||
|
return frame[col].astype(float).reset_index(drop=True)
|
||||||
|
raise KeyError("OHLC DataFrame에 close/Close 컬럼이 없습니다.")
|
||||||
|
|
||||||
|
|
||||||
|
def _bar_index_at(df: pd.DataFrame, dt: str) -> int:
|
||||||
|
"""
|
||||||
|
시각 dt에 대응하는 bar 위치 (인덱스 중복 시 nearest).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: DatetimeIndex OHLC.
|
||||||
|
dt: ISO 시각 문자열.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
정수 bar 위치 (0..n-1).
|
||||||
|
"""
|
||||||
|
frame = _deduped_ohlc(df)
|
||||||
|
if frame.empty:
|
||||||
|
return 0
|
||||||
|
try:
|
||||||
|
ts = pd.to_datetime(dt)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return 0
|
||||||
|
pos = int(frame.index.get_indexer([ts], method="nearest")[0])
|
||||||
|
return max(pos, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def _drawdown_pct_at_index(closes: pd.Series, idx: int) -> float:
|
||||||
|
"""
|
||||||
|
bar idx 시점 drawdown % (과거 rolling high 대비, 인과적).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
closes: 종가 시리즈.
|
||||||
|
idx: 봉 위치.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
drawdown % (0~100).
|
||||||
|
"""
|
||||||
|
if idx < 0 or idx >= len(closes):
|
||||||
|
return 0.0
|
||||||
|
seg = closes.iloc[: idx + 1].astype(float)
|
||||||
|
if seg.empty:
|
||||||
|
return 0.0
|
||||||
|
peak = float(seg.max())
|
||||||
|
cur = float(seg.iloc[-1])
|
||||||
|
if peak <= 0:
|
||||||
|
return 0.0
|
||||||
|
return max((peak - cur) / peak * 100.0, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def hybrid_tier_scale(
|
||||||
|
trade: dict[str, Any],
|
||||||
|
*,
|
||||||
|
completed_leg_ret: dict[int, float],
|
||||||
|
enhanced: bool = False,
|
||||||
|
dd_large_pct: float | None = None,
|
||||||
|
dd_medium_pct: float | None = None,
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
과거 leg 수익 tier + drawdown tier (인과적).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
trade: 매수 trade dict (drawdown_pct 포함).
|
||||||
|
completed_leg_ret: 청산 완료 leg realized return %.
|
||||||
|
enhanced: True면 medium tier·conviction 플래그 적용.
|
||||||
|
dd_large_pct: drawdown large tier 임계(%). None이면 config.
|
||||||
|
dd_medium_pct: drawdown medium tier 임계(%). None이면 config.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
asset_pct_scale.
|
||||||
|
"""
|
||||||
|
from config import GT_LARGE_LEG_TOP_PCT
|
||||||
|
from deepcoin.matching.position_sizing import (
|
||||||
|
large_leg_ids_from_past_returns,
|
||||||
|
)
|
||||||
|
|
||||||
|
dd_large = float(dd_large_pct if dd_large_pct is not None else CAUSAL_GT_DD_LARGE_PCT)
|
||||||
|
dd_medium = float(dd_medium_pct if dd_medium_pct is not None else CAUSAL_GT_DD_MEDIUM_PCT)
|
||||||
|
|
||||||
|
lid = int(trade.get("leg_id", 0))
|
||||||
|
large_past = large_leg_ids_from_past_returns(completed_leg_ret, GT_LARGE_LEG_TOP_PCT)
|
||||||
|
dd = float(trade.get("drawdown_pct") or 0.0)
|
||||||
|
|
||||||
|
if lid in large_past:
|
||||||
|
if enhanced and dd >= SIM_TIER_CONVICTION_DD_PCT:
|
||||||
|
trade["conviction_buy"] = True
|
||||||
|
return float(GT_BUY_PCT_LARGE_LEG)
|
||||||
|
|
||||||
|
if dd >= dd_large:
|
||||||
|
if enhanced:
|
||||||
|
trade["conviction_buy"] = True
|
||||||
|
return float(GT_BUY_PCT_LARGE_LEG)
|
||||||
|
if dd >= dd_medium:
|
||||||
|
if enhanced and dd >= SIM_TIER_CONVICTION_DD_PCT:
|
||||||
|
trade["conviction_buy"] = True
|
||||||
|
return float(GT_BUY_PCT_MEDIUM_LEG) if enhanced else float(GT_BUY_PCT_LARGE_LEG) * 0.5
|
||||||
|
return float(GT_BUY_PCT_SMALL_LEG)
|
||||||
|
|
||||||
|
|
||||||
|
def _monitor_rows_from_fires(fires: pd.DataFrame) -> list[dict[str, Any]]:
|
||||||
|
"""monitor 발화 DataFrame → trade dict 리스트."""
|
||||||
|
rows: list[dict[str, Any]] = []
|
||||||
|
for _, r in sort_fires_chronological(fires).iterrows():
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"dt": str(r["dt"]),
|
||||||
|
"action": r["side"],
|
||||||
|
"price": float(r["close"]),
|
||||||
|
"rule_id": r.get("rule_id", ""),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def build_monitor_hybrid_sized_trades(
|
||||||
|
fires: pd.DataFrame,
|
||||||
|
df: pd.DataFrame,
|
||||||
|
*,
|
||||||
|
enhanced: bool = False,
|
||||||
|
initial_cash: float = GT_INITIAL_CASH_KRW,
|
||||||
|
fee_rate: float = TRADING_FEE_RATE,
|
||||||
|
dd_large_pct: float | None = None,
|
||||||
|
dd_medium_pct: float | None = None,
|
||||||
|
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
monitor 발화 → hybrid tier amount_krw 배분 (인과적).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fires: monitor rule 발화 (buy+sell).
|
||||||
|
df: 3m OHLC (drawdown 계산).
|
||||||
|
enhanced: conviction·medium tier 사용.
|
||||||
|
initial_cash: 시작 현금.
|
||||||
|
fee_rate: 수수료율.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(amount_krw가 채워진 trade dict, alloc_stats).
|
||||||
|
"""
|
||||||
|
from deepcoin.ground_truth.ground_truth import load_ground_truth, order_trades_chronological
|
||||||
|
from deepcoin.paths import resolve_ground_truth_file
|
||||||
|
|
||||||
|
if fires.empty:
|
||||||
|
return [], {"buy_executed": 0, "buy_skipped": 0}
|
||||||
|
|
||||||
|
gt_data = load_ground_truth(resolve_ground_truth_file()) or {}
|
||||||
|
gt_trades = order_trades_chronological(gt_data.get("trades") or [])
|
||||||
|
|
||||||
|
enriched = enrich_sim_trades_with_gt_weights(
|
||||||
|
_monitor_rows_from_fires(fires),
|
||||||
|
gt_trades,
|
||||||
|
causal_legs=True,
|
||||||
|
)
|
||||||
|
enriched = _attach_drawdown_to_buys(enriched, df)
|
||||||
|
|
||||||
|
def scale_fn(t: dict[str, Any], completed_leg_ret: dict[int, float]) -> float:
|
||||||
|
return hybrid_tier_scale(
|
||||||
|
t,
|
||||||
|
completed_leg_ret=completed_leg_ret,
|
||||||
|
enhanced=enhanced,
|
||||||
|
dd_large_pct=dd_large_pct,
|
||||||
|
dd_medium_pct=dd_medium_pct,
|
||||||
|
)
|
||||||
|
|
||||||
|
return allocate_order_amounts_chronological(
|
||||||
|
enriched,
|
||||||
|
initial_cash=initial_cash,
|
||||||
|
fee_rate=fee_rate,
|
||||||
|
causal_tier=False,
|
||||||
|
asset_pct_scale_fn=scale_fn,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _simulate_monitor_tier_portfolio(
|
||||||
|
fires: pd.DataFrame,
|
||||||
|
df: pd.DataFrame,
|
||||||
|
*,
|
||||||
|
enhanced: bool = False,
|
||||||
|
last_price: float | None = None,
|
||||||
|
initial_cash: float = GT_INITIAL_CASH_KRW,
|
||||||
|
fee_rate: float = TRADING_FEE_RATE,
|
||||||
|
dd_large_pct: float | None = None,
|
||||||
|
dd_medium_pct: float | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
monitor buy+sell + tier 복리 시뮬 (hybrid 또는 enhanced).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fires: monitor rule 발화 (buy+sell).
|
||||||
|
df: 3m OHLC (drawdown 계산).
|
||||||
|
enhanced: conviction·medium tier 사용.
|
||||||
|
last_price: 미청산 평가가.
|
||||||
|
initial_cash: 시작 현금.
|
||||||
|
fee_rate: 수수료율.
|
||||||
|
dd_large_pct: drawdown large tier 임계(%).
|
||||||
|
dd_medium_pct: drawdown medium tier 임계(%).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
portfolio summary dict.
|
||||||
|
"""
|
||||||
|
mode = "monitor_tier_enhanced" if enhanced else "monitor_dd_tier"
|
||||||
|
if fires.empty:
|
||||||
|
return {"pnl_pct": 0.0, "trade_count": 0, "sizing_mode": mode}
|
||||||
|
|
||||||
|
sized, alloc_stats = build_monitor_hybrid_sized_trades(
|
||||||
|
fires,
|
||||||
|
df,
|
||||||
|
enhanced=enhanced,
|
||||||
|
initial_cash=initial_cash,
|
||||||
|
fee_rate=fee_rate,
|
||||||
|
dd_large_pct=dd_large_pct,
|
||||||
|
dd_medium_pct=dd_medium_pct,
|
||||||
|
)
|
||||||
|
|
||||||
|
mark = last_price
|
||||||
|
if mark is None and not df.empty:
|
||||||
|
try:
|
||||||
|
mark = float(_close_series_from_df(df).iloc[-1])
|
||||||
|
except KeyError:
|
||||||
|
mark = None
|
||||||
|
|
||||||
|
result = simulate_portfolio_summary(
|
||||||
|
sized,
|
||||||
|
initial_cash=initial_cash,
|
||||||
|
fee_rate=fee_rate,
|
||||||
|
last_price=mark,
|
||||||
|
use_amount_krw=True,
|
||||||
|
)
|
||||||
|
result["sizing_mode"] = mode
|
||||||
|
if enhanced:
|
||||||
|
result["sizing_note"] = (
|
||||||
|
"monitor buy+sell + past-leg·drawdown tier + conviction (미래 미사용)"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
result["sizing_note"] = (
|
||||||
|
"monitor buy+sell + drawdown·past-leg tier (미래 미사용)"
|
||||||
|
)
|
||||||
|
result["alloc_stats"] = alloc_stats
|
||||||
|
result["input_fires"] = int(len(fires))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _attach_drawdown_to_buys(
|
||||||
|
trades: list[dict[str, Any]],
|
||||||
|
df: pd.DataFrame,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
매수 trade에 bar drawdown % 부여 (인과적).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
trades: enrich된 trade dict.
|
||||||
|
df: 3m OHLC (DatetimeIndex).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
drawdown_pct가 추가된 trade dict.
|
||||||
|
"""
|
||||||
|
if df.empty:
|
||||||
|
return trades
|
||||||
|
close_s = _close_series_from_df(df)
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
for t in trades:
|
||||||
|
row = dict(t)
|
||||||
|
if row.get("action") != "buy":
|
||||||
|
out.append(row)
|
||||||
|
continue
|
||||||
|
bar_idx = _bar_index_at(df, str(row.get("dt", "")))
|
||||||
|
row["drawdown_pct"] = round(_drawdown_pct_at_index(close_s, bar_idx), 2)
|
||||||
|
out.append(row)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def simulate_monitor_dd_tier_portfolio(
|
||||||
|
fires: pd.DataFrame,
|
||||||
|
df: pd.DataFrame,
|
||||||
|
*,
|
||||||
|
last_price: float | None = None,
|
||||||
|
initial_cash: float = GT_INITIAL_CASH_KRW,
|
||||||
|
fee_rate: float = TRADING_FEE_RATE,
|
||||||
|
dd_large_pct: float | None = None,
|
||||||
|
dd_medium_pct: float | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
monitor buy+sell + drawdown/past-leg tier 복리 시뮬.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fires: monitor rule 발화 (buy+sell).
|
||||||
|
df: 3m OHLC (drawdown 계산).
|
||||||
|
last_price: 미청산 평가가.
|
||||||
|
initial_cash: 시작 현금.
|
||||||
|
fee_rate: 수수료율.
|
||||||
|
dd_large_pct: drawdown large tier 임계(%).
|
||||||
|
dd_medium_pct: drawdown medium tier 임계(%).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
portfolio summary dict.
|
||||||
|
"""
|
||||||
|
return _simulate_monitor_tier_portfolio(
|
||||||
|
fires,
|
||||||
|
df,
|
||||||
|
enhanced=False,
|
||||||
|
last_price=last_price,
|
||||||
|
initial_cash=initial_cash,
|
||||||
|
fee_rate=fee_rate,
|
||||||
|
dd_large_pct=dd_large_pct,
|
||||||
|
dd_medium_pct=dd_medium_pct,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def simulate_monitor_tier_enhanced_portfolio(
|
||||||
|
fires: pd.DataFrame,
|
||||||
|
df: pd.DataFrame,
|
||||||
|
*,
|
||||||
|
last_price: float | None = None,
|
||||||
|
initial_cash: float = GT_INITIAL_CASH_KRW,
|
||||||
|
fee_rate: float = TRADING_FEE_RATE,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Phase 4: monitor + past-leg·drawdown tier + conviction (weight 분할 생략).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fires: monitor rule 발화 (buy+sell).
|
||||||
|
df: 3m OHLC (drawdown 계산).
|
||||||
|
last_price: 미청산 평가가.
|
||||||
|
initial_cash: 시작 현금.
|
||||||
|
fee_rate: 수수료율.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
portfolio summary dict.
|
||||||
|
"""
|
||||||
|
return _simulate_monitor_tier_portfolio(
|
||||||
|
fires,
|
||||||
|
df,
|
||||||
|
enhanced=True,
|
||||||
|
last_price=last_price,
|
||||||
|
initial_cash=initial_cash,
|
||||||
|
fee_rate=fee_rate,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def simulate_causal_gt_hybrid_portfolio(
|
||||||
|
buy_fires: pd.DataFrame,
|
||||||
|
df: pd.DataFrame,
|
||||||
|
*,
|
||||||
|
monitor_fires: pd.DataFrame | None = None,
|
||||||
|
last_price: float | None = None,
|
||||||
|
cg_params: dict[str, Any] | None = None,
|
||||||
|
initial_cash: float = GT_INITIAL_CASH_KRW,
|
||||||
|
fee_rate: float = TRADING_FEE_RATE,
|
||||||
|
dd_large_pct: float | None = None,
|
||||||
|
dd_medium_pct: float | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Phase 3 하이브리드: monitor buy+sell + DD tier (권장).
|
||||||
|
|
||||||
|
monitor_fires가 있으면 DD tier 경로, 없으면 구 peak-sell 경로(legacy).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
buy_fires: buy 발화 (legacy peak-sell 경로용).
|
||||||
|
df: 3m OHLCV.
|
||||||
|
monitor_fires: monitor buy+sell (권장).
|
||||||
|
last_price: 미청산 평가가.
|
||||||
|
cg_params: legacy 파라미터.
|
||||||
|
initial_cash: 시작 현금.
|
||||||
|
fee_rate: 수수료율.
|
||||||
|
dd_large_pct: drawdown large tier 임계(%).
|
||||||
|
dd_medium_pct: drawdown medium tier 임계(%).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
portfolio summary dict.
|
||||||
|
"""
|
||||||
|
if monitor_fires is not None and not monitor_fires.empty:
|
||||||
|
return simulate_monitor_dd_tier_portfolio(
|
||||||
|
monitor_fires,
|
||||||
|
df,
|
||||||
|
last_price=last_price,
|
||||||
|
initial_cash=initial_cash,
|
||||||
|
fee_rate=fee_rate,
|
||||||
|
dd_large_pct=dd_large_pct,
|
||||||
|
dd_medium_pct=dd_medium_pct,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"pnl_pct": 0.0,
|
||||||
|
"trade_count": 0,
|
||||||
|
"note": "monitor_fires required",
|
||||||
|
"sizing_mode": "causal_gt_hybrid",
|
||||||
|
}
|
||||||
433
deepcoin/ground_truth/causal_gt_trades.py
Normal file
433
deepcoin/ground_truth/causal_gt_trades.py
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
"""
|
||||||
|
인과적 GT leg 타점 생성 — t 시점까지 데이터만 사용.
|
||||||
|
|
||||||
|
GT split_buy_peak_sell 과 동일 구조(분할매수·65/35 매도·leg_id)이나
|
||||||
|
피벗·leg 종료는 gt_signal_causal 확정 신호만 사용합니다.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Literal
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from config import (
|
||||||
|
GT_BUY_MIN_BARS,
|
||||||
|
GT_BUY_MIN_SWING_PCT,
|
||||||
|
GT_MAX_BUYS_PER_LEG,
|
||||||
|
GT_MAX_SELLS_PER_LEG,
|
||||||
|
GT_MIN_SWING_PCT,
|
||||||
|
GT_PIVOT_ORDER,
|
||||||
|
GT_SELL_SPLIT_GAP_PCT,
|
||||||
|
)
|
||||||
|
from deepcoin.ground_truth.gt_model import leg_entry_weights, leg_exit_weights
|
||||||
|
from deepcoin.ground_truth.gt_signal_causal import enrich_scan_frame_gt_signals_causal
|
||||||
|
|
||||||
|
PeakMode = Literal["zigzag", "local"]
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_causal_buy_bars(
|
||||||
|
frame: pd.DataFrame,
|
||||||
|
start: pd.Timestamp,
|
||||||
|
end: pd.Timestamp,
|
||||||
|
*,
|
||||||
|
min_bars: int,
|
||||||
|
max_buys: int,
|
||||||
|
use_local_trough: bool,
|
||||||
|
bb_max: float,
|
||||||
|
) -> list[tuple[pd.Timestamp, float]]:
|
||||||
|
"""
|
||||||
|
leg 구간 (start, end) 내 인과적 매수 후보 봉.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: gt_buy_signal 등 포함.
|
||||||
|
start: 이전 매도 시각(미포함).
|
||||||
|
end: leg 종료 peak 시각(포함).
|
||||||
|
min_bars: 분할 매수 최소 간격.
|
||||||
|
max_buys: leg당 최대 매수.
|
||||||
|
use_local_trough: True면 gt_trough_local+BB, False면 gt_buy_signal.
|
||||||
|
bb_max: BB %B 상한.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(dt, low_price) 리스트 (시간순).
|
||||||
|
"""
|
||||||
|
seg = frame[(frame.index > start) & (frame.index <= end)]
|
||||||
|
if seg.empty:
|
||||||
|
return []
|
||||||
|
|
||||||
|
if use_local_trough:
|
||||||
|
bb = pd.to_numeric(seg.get("bb_pos"), errors="coerce")
|
||||||
|
mask = (seg["gt_trough_local"] == 1) & (bb <= bb_max)
|
||||||
|
else:
|
||||||
|
mask = seg["gt_buy_signal"] == 1
|
||||||
|
|
||||||
|
cands: list[tuple[pd.Timestamp, float, int]] = []
|
||||||
|
for ts, row in seg[mask].iterrows():
|
||||||
|
price = float(row["Low"]) if "Low" in row else float(row.get("close", 0))
|
||||||
|
if price <= 0:
|
||||||
|
continue
|
||||||
|
idx = frame.index.get_loc(ts)
|
||||||
|
if isinstance(idx, slice):
|
||||||
|
idx = int(idx.start or 0)
|
||||||
|
cands.append((ts, price, int(idx)))
|
||||||
|
|
||||||
|
cands.sort(key=lambda x: x[0])
|
||||||
|
filtered: list[tuple[pd.Timestamp, float, int]] = []
|
||||||
|
for ts, price, idx in cands:
|
||||||
|
if filtered and idx - filtered[-1][2] < min_bars:
|
||||||
|
if price < filtered[-1][1]:
|
||||||
|
filtered[-1] = (ts, price, idx)
|
||||||
|
continue
|
||||||
|
filtered.append((ts, price, idx))
|
||||||
|
|
||||||
|
if len(filtered) > max_buys:
|
||||||
|
filtered.sort(key=lambda x: x[1])
|
||||||
|
filtered = sorted(filtered[:max_buys], key=lambda x: x[0])
|
||||||
|
|
||||||
|
return [(ts, price) for ts, price, _ in filtered]
|
||||||
|
|
||||||
|
|
||||||
|
def _causal_sell_points(
|
||||||
|
frame: pd.DataFrame,
|
||||||
|
peak_ts: pd.Timestamp,
|
||||||
|
max_splits: int,
|
||||||
|
*,
|
||||||
|
peak_signal_col: str = "gt_peak_zigzag",
|
||||||
|
) -> list[tuple[pd.Timestamp, float, float]]:
|
||||||
|
"""
|
||||||
|
인과적 매도: peak 확정봉 + (선택) 직후 확정 peak 1건 분할.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: OHLC + gt peak 컬럼.
|
||||||
|
peak_ts: leg 종료 peak 시각.
|
||||||
|
max_splits: 최대 분할(2).
|
||||||
|
peak_signal_col: 두 번째 분할 탐색 컬럼.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(dt, high_price, weight) 리스트.
|
||||||
|
"""
|
||||||
|
if peak_ts not in frame.index:
|
||||||
|
return []
|
||||||
|
|
||||||
|
row = frame.loc[peak_ts]
|
||||||
|
if isinstance(row, pd.DataFrame):
|
||||||
|
row = row.iloc[-1]
|
||||||
|
main_price = float(row["High"]) if "High" in row else float(row.get("close", 0))
|
||||||
|
weights = leg_exit_weights(max_splits if max_splits >= 2 else 1)
|
||||||
|
|
||||||
|
if max_splits < 2 or len(weights) < 2:
|
||||||
|
return [(peak_ts, main_price, 1.0)]
|
||||||
|
|
||||||
|
peak_idx = frame.index.get_loc(peak_ts)
|
||||||
|
if isinstance(peak_idx, slice):
|
||||||
|
peak_idx = int(peak_idx.start or 0)
|
||||||
|
seg = frame.iloc[peak_idx + 1 : peak_idx + 81]
|
||||||
|
second_ts: pd.Timestamp | None = None
|
||||||
|
second_price = main_price
|
||||||
|
for ts, srow in seg.iterrows():
|
||||||
|
if int(srow.get(peak_signal_col, 0)) != 1:
|
||||||
|
continue
|
||||||
|
px = float(srow["High"]) if "High" in srow else float(srow.get("close", 0))
|
||||||
|
gap = abs(px - main_price) / max(main_price, 1e-9) * 100.0
|
||||||
|
if gap <= GT_SELL_SPLIT_GAP_PCT:
|
||||||
|
second_ts = ts
|
||||||
|
second_price = px
|
||||||
|
break
|
||||||
|
|
||||||
|
if second_ts is None:
|
||||||
|
return [(peak_ts, main_price, 1.0)]
|
||||||
|
|
||||||
|
return [
|
||||||
|
(peak_ts, main_price, weights[0]),
|
||||||
|
(second_ts, second_price, weights[1]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _peak_signal_column(peak_mode: PeakMode) -> str:
|
||||||
|
"""leg 종료 peak 컬럼명."""
|
||||||
|
return "gt_peak_local" if peak_mode == "local" else "gt_peak_zigzag"
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_peak_times(
|
||||||
|
frame: pd.DataFrame,
|
||||||
|
peak_col: str,
|
||||||
|
min_bars: int,
|
||||||
|
) -> list[pd.Timestamp]:
|
||||||
|
"""
|
||||||
|
peak 후보를 min_bars 간격으로稀疏화 (인과적, 시간순).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: OHLC frame.
|
||||||
|
peak_col: peak 신호 컬럼.
|
||||||
|
min_bars: 최소 봉 간격.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
peak 타임스탬프 리스트.
|
||||||
|
"""
|
||||||
|
peaks = frame.index[frame[peak_col] == 1]
|
||||||
|
if len(peaks) == 0:
|
||||||
|
return []
|
||||||
|
kept: list[pd.Timestamp] = []
|
||||||
|
last_idx = -min_bars
|
||||||
|
for ts in peaks:
|
||||||
|
idx = frame.index.get_loc(ts)
|
||||||
|
if isinstance(idx, slice):
|
||||||
|
idx = int(idx.start or 0)
|
||||||
|
if idx - last_idx >= min_bars:
|
||||||
|
kept.append(ts)
|
||||||
|
last_idx = int(idx)
|
||||||
|
return kept
|
||||||
|
|
||||||
|
|
||||||
|
def _precompute_buy_candidates(
|
||||||
|
frame: pd.DataFrame,
|
||||||
|
*,
|
||||||
|
use_local_trough: bool,
|
||||||
|
bb_max: float,
|
||||||
|
) -> list[tuple[int, pd.Timestamp, float]]:
|
||||||
|
"""
|
||||||
|
전구간 매수 후보 (bar_idx, ts, price).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: enriched frame.
|
||||||
|
use_local_trough: local trough vs zigzag buy.
|
||||||
|
bb_max: BB 상한.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(idx, ts, price) 리스트.
|
||||||
|
"""
|
||||||
|
if use_local_trough:
|
||||||
|
bb = pd.to_numeric(frame.get("bb_pos"), errors="coerce")
|
||||||
|
mask = (frame["gt_trough_local"] == 1) & (bb <= bb_max)
|
||||||
|
else:
|
||||||
|
mask = frame["gt_buy_signal"] == 1
|
||||||
|
|
||||||
|
out: list[tuple[int, pd.Timestamp, float]] = []
|
||||||
|
for ts in frame.index[mask]:
|
||||||
|
row = frame.loc[ts]
|
||||||
|
if isinstance(row, pd.DataFrame):
|
||||||
|
row = row.iloc[-1]
|
||||||
|
price = float(row["Low"]) if "Low" in row else float(row.get("close", 0))
|
||||||
|
if price <= 0:
|
||||||
|
continue
|
||||||
|
idx = frame.index.get_loc(ts)
|
||||||
|
if isinstance(idx, slice):
|
||||||
|
idx = int(idx.start or 0)
|
||||||
|
out.append((int(idx), ts, price))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _buys_in_range(
|
||||||
|
candidates: list[tuple[int, pd.Timestamp, float]],
|
||||||
|
start_idx: int,
|
||||||
|
end_idx: int,
|
||||||
|
*,
|
||||||
|
min_bars: int,
|
||||||
|
max_buys: int,
|
||||||
|
) -> list[tuple[pd.Timestamp, float]]:
|
||||||
|
"""start_idx < bar_idx <= end_idx 구간 매수 후보 (min_bars·max_buys 적용)."""
|
||||||
|
seg = [(i, ts, p) for i, ts, p in candidates if start_idx < i <= end_idx]
|
||||||
|
if not seg:
|
||||||
|
return []
|
||||||
|
filtered: list[tuple[int, pd.Timestamp, float]] = []
|
||||||
|
for i, ts, p in seg:
|
||||||
|
if filtered and i - filtered[-1][0] < min_bars:
|
||||||
|
if p < filtered[-1][2]:
|
||||||
|
filtered[-1] = (i, ts, p)
|
||||||
|
continue
|
||||||
|
filtered.append((i, ts, p))
|
||||||
|
if len(filtered) > max_buys:
|
||||||
|
filtered.sort(key=lambda x: x[2])
|
||||||
|
filtered = sorted(filtered[:max_buys], key=lambda x: x[0])
|
||||||
|
return [(ts, p) for _, ts, p in filtered]
|
||||||
|
|
||||||
|
|
||||||
|
def build_causal_split_buy_peak_sell_trades(
|
||||||
|
df: 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 = 0.65,
|
||||||
|
min_leg_pct: float = GT_MIN_SWING_PCT,
|
||||||
|
buy_min_bars: int = GT_BUY_MIN_BARS,
|
||||||
|
max_buys: int = GT_MAX_BUYS_PER_LEG,
|
||||||
|
max_sells: int = GT_MAX_SELLS_PER_LEG,
|
||||||
|
use_local_trough: bool = True,
|
||||||
|
peak_mode: PeakMode = "local",
|
||||||
|
min_bars_between_legs: int = 60,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
인과적 split_buy_peak_sell trade dict 리스트.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: 3m OHLCV+bb_pos (DatetimeIndex).
|
||||||
|
pivot_order: 피벗 확정 지연.
|
||||||
|
buy_swing_pct: 매수 ZigZag %.
|
||||||
|
sell_swing_pct: 매도 ZigZag %.
|
||||||
|
bb_max: BB %B 상한.
|
||||||
|
min_leg_pct: leg 최소 수익률(%).
|
||||||
|
buy_min_bars: 분할 매수 간격.
|
||||||
|
max_buys: leg당 매수 상한.
|
||||||
|
max_sells: leg당 매도 상한.
|
||||||
|
use_local_trough: local trough 분할매수 사용.
|
||||||
|
peak_mode: zigzag | local (leg 종료 peak).
|
||||||
|
min_bars_between_legs: 연속 leg 종료 최소 간격(봉).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{dt, action, price, weight, leg_id} dict 리스트.
|
||||||
|
"""
|
||||||
|
frame = enrich_scan_frame_gt_signals_causal(
|
||||||
|
df,
|
||||||
|
pivot_order=pivot_order,
|
||||||
|
buy_swing_pct=buy_swing_pct,
|
||||||
|
sell_swing_pct=sell_swing_pct,
|
||||||
|
bb_max=bb_max,
|
||||||
|
)
|
||||||
|
|
||||||
|
peak_col = _peak_signal_column(peak_mode)
|
||||||
|
if peak_col not in frame.columns:
|
||||||
|
return []
|
||||||
|
|
||||||
|
peak_times = _filter_peak_times(frame, peak_col, min_bars_between_legs)
|
||||||
|
if not peak_times:
|
||||||
|
return []
|
||||||
|
|
||||||
|
buy_candidates = _precompute_buy_candidates(
|
||||||
|
frame,
|
||||||
|
use_local_trough=use_local_trough,
|
||||||
|
bb_max=bb_max,
|
||||||
|
)
|
||||||
|
start_idx = 0
|
||||||
|
if frame.index.size:
|
||||||
|
loc = frame.index.get_loc(frame.index[0])
|
||||||
|
start_idx = int(loc.start or 0) if isinstance(loc, slice) else int(loc)
|
||||||
|
|
||||||
|
peak_signal_col = peak_col
|
||||||
|
trades: list[dict[str, Any]] = []
|
||||||
|
prev_sell_idx = start_idx
|
||||||
|
leg_id = 0
|
||||||
|
leg_trough_price = 0.0
|
||||||
|
|
||||||
|
for peak_ts in peak_times:
|
||||||
|
peak_idx = frame.index.get_loc(peak_ts)
|
||||||
|
if isinstance(peak_idx, slice):
|
||||||
|
peak_idx = int(peak_idx.start or 0)
|
||||||
|
if peak_idx - prev_sell_idx < min_bars_between_legs:
|
||||||
|
continue
|
||||||
|
|
||||||
|
prow = frame.loc[peak_ts]
|
||||||
|
if isinstance(prow, pd.DataFrame):
|
||||||
|
prow = prow.iloc[-1]
|
||||||
|
peak_price = float(prow["High"]) if "High" in prow else float(prow.get("close", 0))
|
||||||
|
seg = frame.iloc[prev_sell_idx + 1 : peak_idx + 1]
|
||||||
|
if not seg.empty and "Low" in seg.columns:
|
||||||
|
leg_trough_price = float(seg["Low"].astype(float).min())
|
||||||
|
|
||||||
|
leg_pct = (
|
||||||
|
(peak_price - leg_trough_price) / max(leg_trough_price, 1e-9) * 100.0
|
||||||
|
if leg_trough_price > 0
|
||||||
|
else 0.0
|
||||||
|
)
|
||||||
|
if leg_pct < min_leg_pct:
|
||||||
|
continue
|
||||||
|
|
||||||
|
buys = _buys_in_range(
|
||||||
|
buy_candidates,
|
||||||
|
prev_sell_idx,
|
||||||
|
int(peak_idx),
|
||||||
|
min_bars=buy_min_bars,
|
||||||
|
max_buys=max_buys,
|
||||||
|
)
|
||||||
|
if not buys:
|
||||||
|
prev_sell_idx = int(peak_idx)
|
||||||
|
leg_trough_price = peak_price
|
||||||
|
continue
|
||||||
|
|
||||||
|
prices = [p for _, p in buys]
|
||||||
|
weights = leg_entry_weights(prices)
|
||||||
|
for (dt, price), w in zip(buys, weights):
|
||||||
|
trades.append(
|
||||||
|
{
|
||||||
|
"dt": dt.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"action": "buy",
|
||||||
|
"price": round(price, 2),
|
||||||
|
"weight": round(w, 4),
|
||||||
|
"leg_id": leg_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
sell_pts = _causal_sell_points(
|
||||||
|
frame,
|
||||||
|
peak_ts,
|
||||||
|
max_sells,
|
||||||
|
peak_signal_col=peak_signal_col,
|
||||||
|
)
|
||||||
|
for dt, price, w in sell_pts[:max_sells]:
|
||||||
|
trades.append(
|
||||||
|
{
|
||||||
|
"dt": dt.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"action": "sell",
|
||||||
|
"price": round(price, 2),
|
||||||
|
"weight": round(w, 4),
|
||||||
|
"leg_id": leg_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
prev_sell_idx = int(peak_idx)
|
||||||
|
leg_trough_price = peak_price
|
||||||
|
leg_id += 1
|
||||||
|
|
||||||
|
return trades
|
||||||
|
|
||||||
|
|
||||||
|
def simulate_causal_gt_portfolio(
|
||||||
|
df: pd.DataFrame,
|
||||||
|
*,
|
||||||
|
last_price: float | None = None,
|
||||||
|
**build_kw: Any,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
인과 GT 타점 + causal tier 복리 포트폴리오.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: 3m OHLCV.
|
||||||
|
last_price: 미청산 평가 종가.
|
||||||
|
build_kw: build_causal_split_buy_peak_sell_trades 인자.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
simulate_portfolio_summary 형식 dict + leg_count, params.
|
||||||
|
"""
|
||||||
|
from deepcoin.ground_truth.gt_allocation import (
|
||||||
|
allocate_order_amounts_chronological,
|
||||||
|
simulate_portfolio_summary,
|
||||||
|
)
|
||||||
|
|
||||||
|
raw = build_causal_split_buy_peak_sell_trades(df, **build_kw)
|
||||||
|
if not raw:
|
||||||
|
return {
|
||||||
|
"pnl_pct": 0.0,
|
||||||
|
"trade_count": 0,
|
||||||
|
"leg_count": 0,
|
||||||
|
"note": "no trades",
|
||||||
|
"sizing_mode": "causal_gt_leg_engine",
|
||||||
|
}
|
||||||
|
|
||||||
|
sized, alloc_stats = allocate_order_amounts_chronological(raw, causal_tier=True)
|
||||||
|
mark = last_price
|
||||||
|
if mark is None and "close" in df.columns:
|
||||||
|
mark = float(df["close"].iloc[-1])
|
||||||
|
result = simulate_portfolio_summary(
|
||||||
|
sized,
|
||||||
|
last_price=mark,
|
||||||
|
use_amount_krw=True,
|
||||||
|
)
|
||||||
|
leg_count = len({t.get("leg_id") for t in raw})
|
||||||
|
result["leg_count"] = leg_count
|
||||||
|
result["sizing_mode"] = "causal_gt_leg_engine"
|
||||||
|
result["sizing_note"] = (
|
||||||
|
"인과 GT leg: split_buy + peak_sell, causal tier 복리 (미래 미사용)"
|
||||||
|
)
|
||||||
|
result["causal_gt_params"] = dict(build_kw)
|
||||||
|
result["alloc_stats"] = alloc_stats
|
||||||
|
return result
|
||||||
@@ -75,7 +75,7 @@ def allocate_order_amounts_chronological(
|
|||||||
Returns:
|
Returns:
|
||||||
(amount_krw 채워진 trades, alloc_stats).
|
(amount_krw 채워진 trades, alloc_stats).
|
||||||
"""
|
"""
|
||||||
from config import GT_LARGE_LEG_TOP_PCT
|
from config import GT_BUY_PCT_LARGE_LEG, GT_LARGE_LEG_TOP_PCT
|
||||||
|
|
||||||
from deepcoin.matching.position_sizing import (
|
from deepcoin.matching.position_sizing import (
|
||||||
compute_buy_amount_krw,
|
compute_buy_amount_krw,
|
||||||
@@ -109,6 +109,7 @@ def allocate_order_amounts_chronological(
|
|||||||
sell_executed = 0
|
sell_executed = 0
|
||||||
sell_skipped = 0
|
sell_skipped = 0
|
||||||
buy_amounts: list[float] = []
|
buy_amounts: list[float] = []
|
||||||
|
large_tier_buys = 0
|
||||||
completed_leg_ret: dict[int, float] = {}
|
completed_leg_ret: dict[int, float] = {}
|
||||||
leg_cost_krw: dict[int, float] = {}
|
leg_cost_krw: dict[int, float] = {}
|
||||||
leg_proceeds_krw: dict[int, float] = {}
|
leg_proceeds_krw: dict[int, float] = {}
|
||||||
@@ -128,7 +129,7 @@ def allocate_order_amounts_chronological(
|
|||||||
)
|
)
|
||||||
scale = leg_asset_pct_scale(leg_id, large_now)
|
scale = leg_asset_pct_scale(leg_id, large_now)
|
||||||
elif asset_pct_scale_fn is not None:
|
elif asset_pct_scale_fn is not None:
|
||||||
scale = asset_pct_scale_fn(t)
|
scale = asset_pct_scale_fn(t, completed_leg_ret)
|
||||||
else:
|
else:
|
||||||
scale = leg_asset_pct_scale(leg_id, large_legs)
|
scale = leg_asset_pct_scale(leg_id, large_legs)
|
||||||
amount = compute_buy_amount_krw(
|
amount = compute_buy_amount_krw(
|
||||||
@@ -140,6 +141,7 @@ def allocate_order_amounts_chronological(
|
|||||||
asset_pct_scale=scale,
|
asset_pct_scale=scale,
|
||||||
min_order_krw=min_order_krw,
|
min_order_krw=min_order_krw,
|
||||||
fee_rate=fee_rate,
|
fee_rate=fee_rate,
|
||||||
|
ignore_weight_split=bool(t.get("conviction_buy")),
|
||||||
)
|
)
|
||||||
if amount <= 0:
|
if amount <= 0:
|
||||||
t["amount_krw"] = 0
|
t["amount_krw"] = 0
|
||||||
@@ -154,6 +156,8 @@ def allocate_order_amounts_chronological(
|
|||||||
leg_cost_krw[leg_id] = leg_cost_krw.get(leg_id, 0.0) + amount + fee
|
leg_cost_krw[leg_id] = leg_cost_krw.get(leg_id, 0.0) + amount + fee
|
||||||
buy_executed += 1
|
buy_executed += 1
|
||||||
buy_amounts.append(amount)
|
buy_amounts.append(amount)
|
||||||
|
if scale >= float(GT_BUY_PCT_LARGE_LEG) * 0.99:
|
||||||
|
large_tier_buys += 1
|
||||||
sell_leg = None
|
sell_leg = None
|
||||||
|
|
||||||
elif t["action"] == "sell":
|
elif t["action"] == "sell":
|
||||||
@@ -188,7 +192,7 @@ def allocate_order_amounts_chronological(
|
|||||||
if qty < 1e-12:
|
if qty < 1e-12:
|
||||||
qty = 0.0
|
qty = 0.0
|
||||||
sell_executed += 1
|
sell_executed += 1
|
||||||
if causal_tier and leg_qty <= 1e-12:
|
if (causal_tier or asset_pct_scale_fn is not None) and leg_qty <= 1e-12:
|
||||||
cost = leg_cost_krw.pop(leg_id, 0.0)
|
cost = leg_cost_krw.pop(leg_id, 0.0)
|
||||||
proceeds = leg_proceeds_krw.pop(leg_id, 0.0)
|
proceeds = leg_proceeds_krw.pop(leg_id, 0.0)
|
||||||
if cost > 0:
|
if cost > 0:
|
||||||
@@ -200,7 +204,8 @@ def allocate_order_amounts_chronological(
|
|||||||
"sell_executed": sell_executed,
|
"sell_executed": sell_executed,
|
||||||
"sell_skipped": sell_skipped,
|
"sell_skipped": sell_skipped,
|
||||||
"buy_total_krw": round(sum(buy_amounts), 0),
|
"buy_total_krw": round(sum(buy_amounts), 0),
|
||||||
"large_leg_count": len(large_legs),
|
"large_leg_count": large_tier_buys,
|
||||||
|
"large_tier_buy_count": large_tier_buys,
|
||||||
}
|
}
|
||||||
if buy_amounts:
|
if buy_amounts:
|
||||||
stats["buy_amount_avg_krw"] = round(sum(buy_amounts) / len(buy_amounts), 0)
|
stats["buy_amount_avg_krw"] = round(sum(buy_amounts) / len(buy_amounts), 0)
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ def _zigzag_filter_causal(
|
|||||||
prices: np.ndarray,
|
prices: np.ndarray,
|
||||||
min_swing_pct: float,
|
min_swing_pct: float,
|
||||||
kind: str,
|
kind: str,
|
||||||
|
pivot_order: int = GT_PIVOT_ORDER,
|
||||||
) -> np.ndarray:
|
) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
확정 피벗에 ZigZag 최소 스윙% 필터 (인과적, 순차 갱신).
|
확정 피벗에 ZigZag 최소 스윙% 필터 (인과적, 순차 갱신).
|
||||||
@@ -90,7 +91,7 @@ def _zigzag_filter_causal(
|
|||||||
"""
|
"""
|
||||||
n = len(confirm)
|
n = len(confirm)
|
||||||
out = np.zeros(n, dtype=np.int8)
|
out = np.zeros(n, dtype=np.int8)
|
||||||
order = GT_PIVOT_ORDER
|
order = int(pivot_order)
|
||||||
last_kind: str | None = None
|
last_kind: str | None = None
|
||||||
last_price = 0.0
|
last_price = 0.0
|
||||||
min_ratio = min_swing_pct / 100.0
|
min_ratio = min_swing_pct / 100.0
|
||||||
@@ -158,10 +159,10 @@ def enrich_scan_frame_gt_signals_causal(
|
|||||||
peak_conf = _confirmed_peak_mask(high, pivot_order)
|
peak_conf = _confirmed_peak_mask(high, pivot_order)
|
||||||
|
|
||||||
trough_z = _zigzag_filter_causal(
|
trough_z = _zigzag_filter_causal(
|
||||||
trough_conf, low, buy_swing_pct, "trough"
|
trough_conf, low, buy_swing_pct, "trough", pivot_order=pivot_order
|
||||||
)
|
)
|
||||||
peak_z = _zigzag_filter_causal(
|
peak_z = _zigzag_filter_causal(
|
||||||
peak_conf, high, sell_swing_pct, "peak"
|
peak_conf, high, sell_swing_pct, "peak", pivot_order=pivot_order
|
||||||
)
|
)
|
||||||
|
|
||||||
out["gt_trough_local"] = trough_conf
|
out["gt_trough_local"] = trough_conf
|
||||||
|
|||||||
200
deepcoin/ground_truth/hybrid_dd_calibrate.py
Normal file
200
deepcoin/ground_truth/hybrid_dd_calibrate.py
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
"""
|
||||||
|
Hybrid DD tier 임계값 train 그리드 → holdout 검증 (Option C 2차).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from itertools import product
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from config import GT_INITIAL_CASH_KRW, MATCH_HOLDOUT_RATIO, TRADING_FEE_RATE
|
||||||
|
from deepcoin.ground_truth.causal_gt_hybrid import build_monitor_hybrid_sized_trades
|
||||||
|
from deepcoin.ground_truth.gt_allocation import simulate_portfolio_steps
|
||||||
|
from deepcoin.matching.option_c_phase2 import walk_forward_portfolio_by_month
|
||||||
|
from deepcoin.matching.portfolio_sim import sort_fires_chronological
|
||||||
|
from deepcoin.matching.simulation import portfolio_holdout_from_steps
|
||||||
|
from deepcoin.paths import MATCHING_HYBRID_DD_CALIBRATION_JSON
|
||||||
|
|
||||||
|
|
||||||
|
def default_dd_grid() -> dict[str, list[float]]:
|
||||||
|
"""DD large/medium 탐색 그리드."""
|
||||||
|
return {
|
||||||
|
"dd_large_pct": [5.0, 6.0, 8.0, 10.0, 12.0],
|
||||||
|
"dd_medium_pct": [2.0, 3.0, 4.0, 6.0],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def load_hybrid_dd_params(path: Path | None = None) -> dict[str, float]:
|
||||||
|
"""
|
||||||
|
캘리브레이션 JSON 또는 config 기본값.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: JSON 경로.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{dd_large_pct, dd_medium_pct}.
|
||||||
|
"""
|
||||||
|
from config import CAUSAL_GT_DD_LARGE_PCT, CAUSAL_GT_DD_MEDIUM_PCT
|
||||||
|
|
||||||
|
p = path or MATCHING_HYBRID_DD_CALIBRATION_JSON
|
||||||
|
if p.is_file():
|
||||||
|
data = json.loads(p.read_text(encoding="utf-8"))
|
||||||
|
best = data.get("best_params") or {}
|
||||||
|
if best.get("dd_large_pct") is not None:
|
||||||
|
return {
|
||||||
|
"dd_large_pct": float(best["dd_large_pct"]),
|
||||||
|
"dd_medium_pct": float(
|
||||||
|
best.get("dd_medium_pct", CAUSAL_GT_DD_MEDIUM_PCT)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"dd_large_pct": float(CAUSAL_GT_DD_LARGE_PCT),
|
||||||
|
"dd_medium_pct": float(CAUSAL_GT_DD_MEDIUM_PCT),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def calibrate_hybrid_dd_thresholds(
|
||||||
|
fires: pd.DataFrame,
|
||||||
|
ohlc_df: pd.DataFrame,
|
||||||
|
*,
|
||||||
|
holdout_start: pd.Timestamp,
|
||||||
|
grid: dict[str, list[float]] | None = None,
|
||||||
|
last_price: float | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
train 구간 PnL 최대 → holdout PnL로 검증, 최적 DD 임계 저장.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fires: monitor 전체 발화.
|
||||||
|
ohlc_df: 3m OHLC.
|
||||||
|
holdout_start: holdout 시작 시각.
|
||||||
|
grid: dd_large/medium 후보.
|
||||||
|
last_price: 미청산 평가가.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
best_params, train/holdout metrics, grid top-N.
|
||||||
|
"""
|
||||||
|
from deepcoin.ground_truth.gt_allocation import simulate_portfolio_summary
|
||||||
|
|
||||||
|
grid = grid or default_dd_grid()
|
||||||
|
chron = sort_fires_chronological(fires)
|
||||||
|
results: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
for dd_large, dd_medium in product(
|
||||||
|
grid["dd_large_pct"],
|
||||||
|
grid["dd_medium_pct"],
|
||||||
|
):
|
||||||
|
if dd_medium >= dd_large:
|
||||||
|
continue
|
||||||
|
sized, stats = build_monitor_hybrid_sized_trades(
|
||||||
|
chron,
|
||||||
|
ohlc_df,
|
||||||
|
enhanced=False,
|
||||||
|
dd_large_pct=dd_large,
|
||||||
|
dd_medium_pct=dd_medium,
|
||||||
|
)
|
||||||
|
steps = simulate_portfolio_steps(sized, use_amount_krw=True)
|
||||||
|
train = portfolio_holdout_from_steps(
|
||||||
|
[s for s in steps if pd.to_datetime(s["dt"]) < holdout_start],
|
||||||
|
holdout_start,
|
||||||
|
initial_if_empty=GT_INITIAL_CASH_KRW,
|
||||||
|
note="train",
|
||||||
|
)
|
||||||
|
# train-only: start 1M → last asset before holdout
|
||||||
|
if steps:
|
||||||
|
pre = [
|
||||||
|
float(s["total_asset_krw"])
|
||||||
|
for s in steps
|
||||||
|
if pd.to_datetime(s["dt"]) < holdout_start
|
||||||
|
]
|
||||||
|
train_asset_end = pre[-1] if pre else GT_INITIAL_CASH_KRW
|
||||||
|
train_pnl = (train_asset_end - GT_INITIAL_CASH_KRW) / GT_INITIAL_CASH_KRW * 100
|
||||||
|
else:
|
||||||
|
train_pnl = 0.0
|
||||||
|
|
||||||
|
holdout = portfolio_holdout_from_steps(
|
||||||
|
steps,
|
||||||
|
holdout_start,
|
||||||
|
note="holdout",
|
||||||
|
)
|
||||||
|
full = simulate_portfolio_summary(
|
||||||
|
sized,
|
||||||
|
last_price=last_price,
|
||||||
|
use_amount_krw=True,
|
||||||
|
)
|
||||||
|
wf = walk_forward_portfolio_by_month(steps)
|
||||||
|
pos_months = sum(1 for w in wf if float(w.get("pnl_pct") or 0) > 0)
|
||||||
|
results.append(
|
||||||
|
{
|
||||||
|
"dd_large_pct": dd_large,
|
||||||
|
"dd_medium_pct": dd_medium,
|
||||||
|
"train_pnl_pct": round(train_pnl, 2),
|
||||||
|
"holdout_pnl_pct": float(holdout.get("pnl_pct", 0)),
|
||||||
|
"full_pnl_pct": float(full.get("pnl_pct", 0)),
|
||||||
|
"max_drawdown_pct": float(full.get("max_drawdown_pct", 0)),
|
||||||
|
"wf_positive_months": pos_months,
|
||||||
|
"wf_months": len(wf),
|
||||||
|
"large_tier_buys": stats.get("large_tier_buy_count", 0),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
return {"best_params": load_hybrid_dd_params(), "note": "empty grid"}
|
||||||
|
|
||||||
|
# train PnL 1순위, holdout PnL 2순위
|
||||||
|
ranked = sorted(
|
||||||
|
results,
|
||||||
|
key=lambda x: (x["train_pnl_pct"], x["holdout_pnl_pct"]),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
best = ranked[0]
|
||||||
|
return {
|
||||||
|
"best_params": {
|
||||||
|
"dd_large_pct": best["dd_large_pct"],
|
||||||
|
"dd_medium_pct": best["dd_medium_pct"],
|
||||||
|
},
|
||||||
|
"best_metrics": best,
|
||||||
|
"grid_size": len(results),
|
||||||
|
"top5": ranked[:5],
|
||||||
|
"holdout_start": str(holdout_start),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def run_and_save_calibration(
|
||||||
|
fires: pd.DataFrame,
|
||||||
|
ohlc_df: pd.DataFrame,
|
||||||
|
*,
|
||||||
|
outcomes: pd.DataFrame,
|
||||||
|
last_price: float | None = None,
|
||||||
|
out_path: Path | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
캘리브레이션 실행 후 JSON 저장.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
fires: monitor 발화.
|
||||||
|
ohlc_df: OHLC.
|
||||||
|
outcomes: fire_outcomes (holdout split).
|
||||||
|
last_price: 평가 종가.
|
||||||
|
out_path: 저장 경로.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
calibrate_hybrid_dd_thresholds 결과.
|
||||||
|
"""
|
||||||
|
outcomes_ts = outcomes.copy()
|
||||||
|
outcomes_ts["ts"] = pd.to_datetime(outcomes_ts["dt"])
|
||||||
|
holdout_start = outcomes_ts["ts"].quantile(1.0 - MATCH_HOLDOUT_RATIO)
|
||||||
|
result = calibrate_hybrid_dd_thresholds(
|
||||||
|
fires,
|
||||||
|
ohlc_df,
|
||||||
|
holdout_start=holdout_start,
|
||||||
|
last_price=last_price,
|
||||||
|
)
|
||||||
|
p = out_path or MATCHING_HYBRID_DD_CALIBRATION_JSON
|
||||||
|
p.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
p.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
return result
|
||||||
214
deepcoin/matching/live_sizing.py
Normal file
214
deepcoin/matching/live_sizing.py
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
"""
|
||||||
|
실거래 매수 사이징 — 시뮬(sim_tier_enhanced)과 동일 인과 tier·weight 정책.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from config import (
|
||||||
|
GT_SIGNAL_CAUSAL,
|
||||||
|
TRADING_FEE_RATE,
|
||||||
|
)
|
||||||
|
from deepcoin.ground_truth.causal_gt_hybrid import (
|
||||||
|
_attach_drawdown_to_buys,
|
||||||
|
_bar_index_at,
|
||||||
|
_close_series_from_df,
|
||||||
|
_drawdown_pct_at_index,
|
||||||
|
hybrid_tier_scale,
|
||||||
|
)
|
||||||
|
from deepcoin.ground_truth.gt_model import leg_entry_weights, remaining_weight_sum
|
||||||
|
from deepcoin.matching.position_sizing import compute_buy_amount_krw
|
||||||
|
from deepcoin.paths import OPS_STATE_DIR
|
||||||
|
|
||||||
|
LIVE_SIZING_STATE_JSON = OPS_STATE_DIR / "live_sizing_state.json"
|
||||||
|
|
||||||
|
|
||||||
|
class LivePositionState:
|
||||||
|
"""
|
||||||
|
미청산 leg·과거 leg 수익·매수 weight 추적 (시뮬 enrich/causal tier 정합).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""빈 포지션 상태."""
|
||||||
|
self.current_leg_id: int = 0
|
||||||
|
self.open_buys: list[dict[str, Any]] = []
|
||||||
|
self.completed_leg_ret: dict[int, float] = {}
|
||||||
|
self.leg_cost_krw: float = 0.0
|
||||||
|
self.leg_proceeds_krw: float = 0.0
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, path: Path | None = None) -> LivePositionState:
|
||||||
|
"""
|
||||||
|
디스크에서 상태 복원.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: JSON 경로. None이면 기본 경로.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
LivePositionState 인스턴스.
|
||||||
|
"""
|
||||||
|
p = path or LIVE_SIZING_STATE_JSON
|
||||||
|
st = cls()
|
||||||
|
if not p.is_file():
|
||||||
|
return st
|
||||||
|
try:
|
||||||
|
data = json.loads(p.read_text(encoding="utf-8"))
|
||||||
|
except (json.JSONDecodeError, OSError):
|
||||||
|
return st
|
||||||
|
st.current_leg_id = int(data.get("current_leg_id") or 0)
|
||||||
|
st.open_buys = list(data.get("open_buys") or [])
|
||||||
|
st.completed_leg_ret = {
|
||||||
|
int(k): float(v) for k, v in (data.get("completed_leg_ret") or {}).items()
|
||||||
|
}
|
||||||
|
st.leg_cost_krw = float(data.get("leg_cost_krw") or 0.0)
|
||||||
|
st.leg_proceeds_krw = float(data.get("leg_proceeds_krw") or 0.0)
|
||||||
|
return st
|
||||||
|
|
||||||
|
def save(self, path: Path | None = None) -> None:
|
||||||
|
"""
|
||||||
|
상태를 디스크에 저장.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path: JSON 경로. None이면 기본 경로.
|
||||||
|
"""
|
||||||
|
p = path or LIVE_SIZING_STATE_JSON
|
||||||
|
p.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
payload = {
|
||||||
|
"current_leg_id": self.current_leg_id,
|
||||||
|
"open_buys": self.open_buys,
|
||||||
|
"completed_leg_ret": self.completed_leg_ret,
|
||||||
|
"leg_cost_krw": round(self.leg_cost_krw, 0),
|
||||||
|
"leg_proceeds_krw": round(self.leg_proceeds_krw, 0),
|
||||||
|
}
|
||||||
|
p.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||||
|
|
||||||
|
def _start_new_leg_if_needed(self) -> None:
|
||||||
|
"""포지션 없을 때 새 leg 시작."""
|
||||||
|
if not self.open_buys:
|
||||||
|
self.current_leg_id += 1
|
||||||
|
self.leg_cost_krw = 0.0
|
||||||
|
self.leg_proceeds_krw = 0.0
|
||||||
|
|
||||||
|
def record_buy(self, dt: str, price: float, amount_krw: float, fee: float) -> None:
|
||||||
|
"""
|
||||||
|
체결 매수 기록.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dt: 체결 시각.
|
||||||
|
price: 체결가.
|
||||||
|
amount_krw: 매수 원화.
|
||||||
|
fee: 수수료.
|
||||||
|
"""
|
||||||
|
self._start_new_leg_if_needed()
|
||||||
|
self.open_buys.append({"dt": dt, "price": price, "amount_krw": amount_krw})
|
||||||
|
self.leg_cost_krw += amount_krw + fee
|
||||||
|
|
||||||
|
def record_sell(self, amount_krw: float, fee: float, *, full_close: bool) -> None:
|
||||||
|
"""
|
||||||
|
체결 매도 기록.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
amount_krw: 매도 원화(총액).
|
||||||
|
fee: 수수료.
|
||||||
|
full_close: leg 전량 청산 여부.
|
||||||
|
"""
|
||||||
|
net = amount_krw - fee
|
||||||
|
self.leg_proceeds_krw += net
|
||||||
|
if full_close and self.leg_cost_krw > 0:
|
||||||
|
ret_pct = (self.leg_proceeds_krw - self.leg_cost_krw) / self.leg_cost_krw * 100.0
|
||||||
|
self.completed_leg_ret[self.current_leg_id] = ret_pct
|
||||||
|
self.open_buys = []
|
||||||
|
self.leg_cost_krw = 0.0
|
||||||
|
self.leg_proceeds_krw = 0.0
|
||||||
|
|
||||||
|
def plan_buy_amount_krw(
|
||||||
|
self,
|
||||||
|
dt: str,
|
||||||
|
price: float,
|
||||||
|
cash: float,
|
||||||
|
qty: float,
|
||||||
|
df: pd.DataFrame | None = None,
|
||||||
|
*,
|
||||||
|
enhanced: bool = True,
|
||||||
|
fee_rate: float = TRADING_FEE_RATE,
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
시뮬과 동일 tier·weight로 매수 원화 산출.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dt: 신호 시각.
|
||||||
|
price: 종가.
|
||||||
|
cash: 가용 원화.
|
||||||
|
qty: 보유 수량.
|
||||||
|
df: OHLC (drawdown).
|
||||||
|
enhanced: conviction·medium tier 사용.
|
||||||
|
fee_rate: 수수료율.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
매수 원화.
|
||||||
|
"""
|
||||||
|
self._start_new_leg_if_needed()
|
||||||
|
prices = [float(b["price"]) for b in self.open_buys] + [price]
|
||||||
|
weights = leg_entry_weights(prices)
|
||||||
|
idx = len(self.open_buys)
|
||||||
|
weight = float(weights[idx])
|
||||||
|
w_sum = float(sum(weights[idx:]))
|
||||||
|
trade: dict[str, Any] = {
|
||||||
|
"dt": dt,
|
||||||
|
"action": "buy",
|
||||||
|
"price": price,
|
||||||
|
"leg_id": self.current_leg_id,
|
||||||
|
"weight": round(weight, 4),
|
||||||
|
}
|
||||||
|
if df is not None and not df.empty:
|
||||||
|
attached = _attach_drawdown_to_buys([trade], df)
|
||||||
|
if attached:
|
||||||
|
trade = attached[0]
|
||||||
|
from deepcoin.ground_truth.hybrid_dd_calibrate import load_hybrid_dd_params
|
||||||
|
|
||||||
|
dd_params = load_hybrid_dd_params()
|
||||||
|
scale = hybrid_tier_scale(
|
||||||
|
trade,
|
||||||
|
completed_leg_ret=self.completed_leg_ret,
|
||||||
|
enhanced=enhanced,
|
||||||
|
dd_large_pct=dd_params.get("dd_large_pct"),
|
||||||
|
dd_medium_pct=dd_params.get("dd_medium_pct"),
|
||||||
|
)
|
||||||
|
return compute_buy_amount_krw(
|
||||||
|
cash,
|
||||||
|
qty,
|
||||||
|
price,
|
||||||
|
weight,
|
||||||
|
w_sum,
|
||||||
|
asset_pct_scale=scale,
|
||||||
|
fee_rate=fee_rate,
|
||||||
|
ignore_weight_split=bool(trade.get("conviction_buy")),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def drawdown_pct_from_df(df: pd.DataFrame, dt: str) -> float:
|
||||||
|
"""
|
||||||
|
bar 시점 drawdown % (인과적).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: DatetimeIndex OHLC.
|
||||||
|
dt: 시각 문자열.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
drawdown %.
|
||||||
|
"""
|
||||||
|
if df.empty:
|
||||||
|
return 0.0
|
||||||
|
close_s = _close_series_from_df(df)
|
||||||
|
bar_idx = _bar_index_at(df, dt)
|
||||||
|
return _drawdown_pct_at_index(close_s, bar_idx)
|
||||||
|
|
||||||
|
|
||||||
|
def live_sizing_enabled() -> bool:
|
||||||
|
"""실거래 사이징을 시뮬 인과 tier와 정합할지."""
|
||||||
|
return bool(GT_SIGNAL_CAUSAL)
|
||||||
245
deepcoin/matching/option_c_phase2.py
Normal file
245
deepcoin/matching/option_c_phase2.py
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
"""
|
||||||
|
Option C 2차 목표(+1000%) 검증 — hybrid tier 포트폴리오 WF·슬리피지·Go/No-Go.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from config import (
|
||||||
|
GT_INITIAL_CASH_KRW,
|
||||||
|
LIVE_SLIPPAGE_PCT,
|
||||||
|
SIM_HYBRID_MAX_MDD_PCT,
|
||||||
|
SIM_HYBRID_PORTFOLIO_WF_MIN_RATIO,
|
||||||
|
SIM_OPTION_C_MIN_GT_CAPTURE,
|
||||||
|
SIM_OPTION_C_PHASE2_FEE_STRESS_RATIO,
|
||||||
|
SIM_OPTION_C_PHASE2_TARGET_PNL_PCT,
|
||||||
|
)
|
||||||
|
from deepcoin.ground_truth.gt_allocation import simulate_portfolio_summary
|
||||||
|
|
||||||
|
|
||||||
|
def apply_slippage_to_trades(
|
||||||
|
trades: list[dict[str, Any]],
|
||||||
|
slippage_pct: float = LIVE_SLIPPAGE_PCT,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
체결가에 슬리피지 반영 (매수 불리·매도 불리).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
trades: amount_krw·price·action trade dict.
|
||||||
|
slippage_pct: 체결가 대비 % (0.05 = 0.05%).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
price 조정된 trade dict 복사본.
|
||||||
|
"""
|
||||||
|
slip = float(slippage_pct) / 100.0
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
for t in trades:
|
||||||
|
row = dict(t)
|
||||||
|
price = float(row.get("price") or 0)
|
||||||
|
if price <= 0:
|
||||||
|
out.append(row)
|
||||||
|
continue
|
||||||
|
action = row.get("action", "")
|
||||||
|
if action == "buy":
|
||||||
|
row["price"] = round(price * (1.0 + slip), 4)
|
||||||
|
elif action == "sell":
|
||||||
|
row["price"] = round(price * (1.0 - slip), 4)
|
||||||
|
out.append(row)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def walk_forward_portfolio_by_month(
|
||||||
|
steps: list[dict[str, Any]],
|
||||||
|
*,
|
||||||
|
initial_cash: float = GT_INITIAL_CASH_KRW,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
포트폴리오 step에서 월별 자산 증감률 (인과적).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
steps: simulate_portfolio_steps 결과.
|
||||||
|
initial_cash: 첫 달 시작 자산(이전 달 종료 없을 때).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{month, pnl_pct, start_asset, end_asset} 리스트.
|
||||||
|
"""
|
||||||
|
if not steps:
|
||||||
|
return []
|
||||||
|
df = pd.DataFrame(steps)
|
||||||
|
df["ts"] = pd.to_datetime(df["dt"])
|
||||||
|
df = df.sort_values("ts")
|
||||||
|
df["month"] = df["ts"].dt.to_period("M").astype(str)
|
||||||
|
rows: list[dict[str, Any]] = []
|
||||||
|
prev_end = float(initial_cash)
|
||||||
|
for month in sorted(df["month"].unique()):
|
||||||
|
grp = df[df["month"] == month]
|
||||||
|
end_asset = float(grp["total_asset_krw"].iloc[-1])
|
||||||
|
pnl_pct = (end_asset - prev_end) / prev_end * 100.0 if prev_end > 0 else 0.0
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"month": month,
|
||||||
|
"pnl_pct": round(pnl_pct, 2),
|
||||||
|
"start_asset_krw": round(prev_end, 0),
|
||||||
|
"end_asset_krw": round(end_asset, 0),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
prev_end = end_asset
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def walk_forward_portfolio_summary(wf_rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
월별 포트폴리오 WF 요약.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wf_rows: walk_forward_portfolio_by_month 결과.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
months, positive_months, positive_ratio, mean_pnl_pct.
|
||||||
|
"""
|
||||||
|
if not wf_rows:
|
||||||
|
return {"months": 0, "positive_ratio": 0.0, "mean_pnl_pct": 0.0}
|
||||||
|
n = len(wf_rows)
|
||||||
|
pos = sum(1 for r in wf_rows if float(r.get("pnl_pct") or 0) > 0)
|
||||||
|
mean_pnl = sum(float(r.get("pnl_pct") or 0) for r in wf_rows) / n
|
||||||
|
return {
|
||||||
|
"months": n,
|
||||||
|
"positive_months": pos,
|
||||||
|
"positive_ratio": round(pos / n, 4),
|
||||||
|
"mean_pnl_pct": round(mean_pnl, 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def simulate_hybrid_slippage_stress(
|
||||||
|
sized_trades: list[dict[str, Any]],
|
||||||
|
*,
|
||||||
|
last_price: float | None,
|
||||||
|
slippage_pct: float = LIVE_SLIPPAGE_PCT,
|
||||||
|
initial_cash: float = GT_INITIAL_CASH_KRW,
|
||||||
|
fee_rate: float,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
hybrid sized trades + 슬리피지 포트폴리오 요약.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sized_trades: build_monitor_hybrid_sized_trades 출력.
|
||||||
|
last_price: 미청산 평가가.
|
||||||
|
slippage_pct: 체결 슬리피지 %.
|
||||||
|
initial_cash: 시작 현금.
|
||||||
|
fee_rate: 수수료율.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
simulate_portfolio_summary 결과.
|
||||||
|
"""
|
||||||
|
slipped = apply_slippage_to_trades(sized_trades, slippage_pct)
|
||||||
|
result = simulate_portfolio_summary(
|
||||||
|
slipped,
|
||||||
|
initial_cash=initial_cash,
|
||||||
|
fee_rate=fee_rate,
|
||||||
|
last_price=last_price,
|
||||||
|
use_amount_krw=True,
|
||||||
|
)
|
||||||
|
result["slippage_pct"] = slippage_pct
|
||||||
|
result["sizing_mode"] = "hybrid_slippage_stress"
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_option_c_phase2_go(
|
||||||
|
hybrid_go: dict[str, Any],
|
||||||
|
hybrid_full: dict[str, Any],
|
||||||
|
hybrid_holdout: dict[str, Any],
|
||||||
|
hybrid_fee_stress: dict[str, Any],
|
||||||
|
hybrid_slippage: dict[str, Any],
|
||||||
|
portfolio_wf_summary: dict[str, Any],
|
||||||
|
gt_pnl_pct: float,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Option C 2차(+1000%) Go/No-Go.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hybrid_go: 1차 hybrid tier Go 결과.
|
||||||
|
hybrid_full: 전기간 hybrid 포트폴리오.
|
||||||
|
hybrid_holdout: holdout 구간 증감.
|
||||||
|
hybrid_fee_stress: 수수료 2x hybrid.
|
||||||
|
hybrid_slippage: 슬리피지 반영 hybrid.
|
||||||
|
portfolio_wf_summary: 월별 포트폴리오 WF.
|
||||||
|
gt_pnl_pct: GT 전기간 PnL %.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
go, checks, targets.
|
||||||
|
"""
|
||||||
|
full_pnl = float(hybrid_full.get("pnl_pct", 0))
|
||||||
|
capture = full_pnl / gt_pnl_pct if abs(gt_pnl_pct) > 1e-6 else 0.0
|
||||||
|
ho_pnl = float(hybrid_holdout.get("pnl_pct", -999))
|
||||||
|
mdd = float(hybrid_full.get("max_drawdown_pct", 999))
|
||||||
|
fee_pnl = float(hybrid_fee_stress.get("pnl_pct", -999))
|
||||||
|
slip_pnl = float(hybrid_slippage.get("pnl_pct", -999))
|
||||||
|
wf_ratio = float(portfolio_wf_summary.get("positive_ratio", 0))
|
||||||
|
|
||||||
|
c_phase1 = bool(hybrid_go.get("go"))
|
||||||
|
c_pnl = full_pnl >= SIM_OPTION_C_PHASE2_TARGET_PNL_PCT
|
||||||
|
c_capture = capture >= SIM_OPTION_C_MIN_GT_CAPTURE
|
||||||
|
c_holdout = ho_pnl > 0.0
|
||||||
|
c_mdd = mdd <= SIM_HYBRID_MAX_MDD_PCT
|
||||||
|
c_fee = fee_pnl >= SIM_OPTION_C_PHASE2_TARGET_PNL_PCT * SIM_OPTION_C_PHASE2_FEE_STRESS_RATIO
|
||||||
|
c_slip = slip_pnl > 0.0
|
||||||
|
c_wf = wf_ratio >= SIM_HYBRID_PORTFOLIO_WF_MIN_RATIO
|
||||||
|
|
||||||
|
all_go = (
|
||||||
|
c_phase1
|
||||||
|
and c_pnl
|
||||||
|
and c_capture
|
||||||
|
and c_holdout
|
||||||
|
and c_mdd
|
||||||
|
and c_fee
|
||||||
|
and c_slip
|
||||||
|
and c_wf
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"go": all_go,
|
||||||
|
"gt_capture_ratio": round(capture, 4),
|
||||||
|
"targets": {
|
||||||
|
"phase2_pnl_pct": SIM_OPTION_C_PHASE2_TARGET_PNL_PCT,
|
||||||
|
"min_gt_capture": SIM_OPTION_C_MIN_GT_CAPTURE,
|
||||||
|
"portfolio_wf_min_ratio": SIM_HYBRID_PORTFOLIO_WF_MIN_RATIO,
|
||||||
|
},
|
||||||
|
"checks": [
|
||||||
|
{"name": "phase1_hybrid_go", "pass": c_phase1},
|
||||||
|
{
|
||||||
|
"name": "full_pnl_1000pct",
|
||||||
|
"pass": c_pnl,
|
||||||
|
"value": full_pnl,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "gt_capture_23pct",
|
||||||
|
"pass": c_capture,
|
||||||
|
"value": round(capture, 4),
|
||||||
|
},
|
||||||
|
{"name": "holdout_pnl_positive", "pass": c_holdout, "value": ho_pnl},
|
||||||
|
{"name": "max_mdd", "pass": c_mdd, "value": mdd},
|
||||||
|
{
|
||||||
|
"name": "fee_stress_ratio",
|
||||||
|
"pass": c_fee,
|
||||||
|
"value": fee_pnl,
|
||||||
|
"threshold": round(
|
||||||
|
SIM_OPTION_C_PHASE2_TARGET_PNL_PCT * SIM_OPTION_C_PHASE2_FEE_STRESS_RATIO,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "slippage_stress_positive",
|
||||||
|
"pass": c_slip,
|
||||||
|
"value": slip_pnl,
|
||||||
|
"note": "체결가 슬리피지 반영 후에도 흑자",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "portfolio_wf_positive_ratio",
|
||||||
|
"pass": c_wf,
|
||||||
|
"value": wf_ratio,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -71,6 +71,7 @@ def compute_buy_amount_krw(
|
|||||||
asset_pct_scale: float,
|
asset_pct_scale: float,
|
||||||
min_order_krw: float = GT_MIN_ORDER_KRW,
|
min_order_krw: float = GT_MIN_ORDER_KRW,
|
||||||
fee_rate: float = TRADING_FEE_RATE,
|
fee_rate: float = TRADING_FEE_RATE,
|
||||||
|
ignore_weight_split: bool = False,
|
||||||
) -> float:
|
) -> float:
|
||||||
"""
|
"""
|
||||||
목표=총보유자산×(최적 매수율×scale), 체결=min(목표, 보유현금/(1+fee)) 로 매수 원화를 산출합니다.
|
목표=총보유자산×(최적 매수율×scale), 체결=min(목표, 보유현금/(1+fee)) 로 매수 원화를 산출합니다.
|
||||||
@@ -86,6 +87,7 @@ def compute_buy_amount_krw(
|
|||||||
asset_pct_scale: leg·규칙 티어(대형/소형) 스케일.
|
asset_pct_scale: leg·규칙 티어(대형/소형) 스케일.
|
||||||
min_order_krw: 최소 주문 원화.
|
min_order_krw: 최소 주문 원화.
|
||||||
fee_rate: 수수료율.
|
fee_rate: 수수료율.
|
||||||
|
ignore_weight_split: True면 weight 분할 없이 scale만 적용 (conviction 매수).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
매수 원화(0이면 미체결).
|
매수 원화(0이면 미체결).
|
||||||
@@ -94,7 +96,10 @@ def compute_buy_amount_krw(
|
|||||||
return 0.0
|
return 0.0
|
||||||
total_asset, _, available_cash = portfolio_totals(cash, qty, price)
|
total_asset, _, available_cash = portfolio_totals(cash, qty, price)
|
||||||
budget = max(available_cash / (1.0 + fee_rate), 0.0)
|
budget = max(available_cash / (1.0 + fee_rate), 0.0)
|
||||||
opt_rate = optimal_weight_share(weight, weight_sum_remaining) * asset_pct_scale
|
if ignore_weight_split:
|
||||||
|
opt_rate = asset_pct_scale
|
||||||
|
else:
|
||||||
|
opt_rate = optimal_weight_share(weight, weight_sum_remaining) * asset_pct_scale
|
||||||
target = total_asset * opt_rate
|
target = total_asset * opt_rate
|
||||||
amount = min(target, budget)
|
amount = min(target, budget)
|
||||||
if budget >= min_order_krw and 0 < amount < min_order_krw:
|
if budget >= min_order_krw and 0 < amount < min_order_krw:
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ from config import (
|
|||||||
SIM_GO_MIN_HOLDOUT_EV,
|
SIM_GO_MIN_HOLDOUT_EV,
|
||||||
SIM_GO_MIN_HOLDOUT_PF,
|
SIM_GO_MIN_HOLDOUT_PF,
|
||||||
SIM_GO_WF_POSITIVE_RATIO,
|
SIM_GO_WF_POSITIVE_RATIO,
|
||||||
|
SIM_HYBRID_MAX_MDD_PCT,
|
||||||
|
SIM_HYBRID_MIN_HOLDOUT_PNL_PCT,
|
||||||
|
SIM_OPTION_C_MIN_GT_CAPTURE,
|
||||||
|
SIM_OPTION_C_PHASE2_FEE_STRESS_RATIO,
|
||||||
|
SIM_OPTION_C_PHASE2_TARGET_PNL_PCT,
|
||||||
|
SIM_OPTION_C_TARGET_PNL_PCT,
|
||||||
SIM_WALK_FORWARD_MIN_MONTHS,
|
SIM_WALK_FORWARD_MIN_MONTHS,
|
||||||
TRADING_FEE_RATE,
|
TRADING_FEE_RATE,
|
||||||
)
|
)
|
||||||
@@ -247,6 +253,186 @@ def evaluate_go_no_go(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def portfolio_holdout_from_steps(
|
||||||
|
steps: list[dict[str, Any]],
|
||||||
|
holdout_start: pd.Timestamp,
|
||||||
|
*,
|
||||||
|
initial_if_empty: float = GT_INITIAL_CASH_KRW,
|
||||||
|
trade_count: int = 0,
|
||||||
|
note: str = "",
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
포트폴리오 step에서 holdout 구간 자산 증감.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
steps: simulate_portfolio_steps 결과.
|
||||||
|
holdout_start: holdout 시작 시각.
|
||||||
|
initial_if_empty: step 없을 때 시작 자산.
|
||||||
|
trade_count: holdout 발화 수.
|
||||||
|
note: 설명.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
holdout pnl 요약 dict.
|
||||||
|
"""
|
||||||
|
if not steps:
|
||||||
|
return {"pnl_pct": 0.0, "note": "steps empty"}
|
||||||
|
assets = [(pd.to_datetime(s["dt"]), float(s["total_asset_krw"])) for s in steps]
|
||||||
|
pre = [a for d, a in assets if d < holdout_start]
|
||||||
|
in_h = [a for d, a in assets if d >= holdout_start]
|
||||||
|
asset_start = pre[-1] if pre else float(initial_if_empty)
|
||||||
|
asset_end = in_h[-1] if in_h else assets[-1][1]
|
||||||
|
ho_pnl_pct = (
|
||||||
|
(asset_end - asset_start) / asset_start * 100.0 if asset_start > 0 else 0.0
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"initial_asset_krw": round(asset_start, 0),
|
||||||
|
"final_asset_krw": round(asset_end, 0),
|
||||||
|
"pnl_krw": round(asset_end - asset_start, 0),
|
||||||
|
"pnl_pct": round(ho_pnl_pct, 2),
|
||||||
|
"trade_count": int(trade_count),
|
||||||
|
"note": note,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_hybrid_sizing_go(
|
||||||
|
base_go: dict[str, Any],
|
||||||
|
hybrid_full: dict[str, Any],
|
||||||
|
hybrid_holdout: dict[str, Any],
|
||||||
|
hybrid_fee_stress: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
hybrid DD tier 배분 승격 Go/No-Go (규칙 Go + holdout·MDD·수수료 스트레스).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_go: monitor 규칙 evaluate_go_no_go 결과.
|
||||||
|
hybrid_full: 전기간 hybrid 포트폴리오 요약.
|
||||||
|
hybrid_holdout: holdout 구간 자산 증감.
|
||||||
|
hybrid_fee_stress: 수수료 스트레스 hybrid 포트폴리오.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
go, checks, primary_sizing 권장.
|
||||||
|
"""
|
||||||
|
from config import (
|
||||||
|
SIM_HYBRID_MAX_MDD_PCT,
|
||||||
|
SIM_HYBRID_MIN_HOLDOUT_PNL_PCT,
|
||||||
|
SIM_OPTION_C_TARGET_PNL_PCT,
|
||||||
|
SIM_PRIMARY_SIZING,
|
||||||
|
)
|
||||||
|
|
||||||
|
base_ok = bool(base_go.get("go"))
|
||||||
|
ho_pnl = float(hybrid_holdout.get("pnl_pct", -999))
|
||||||
|
full_pnl = float(hybrid_full.get("pnl_pct", 0))
|
||||||
|
mdd = float(hybrid_full.get("max_drawdown_pct", 999))
|
||||||
|
stress_pnl = float(hybrid_fee_stress.get("pnl_pct", -999))
|
||||||
|
|
||||||
|
c_base = base_ok
|
||||||
|
c_holdout = ho_pnl >= SIM_HYBRID_MIN_HOLDOUT_PNL_PCT
|
||||||
|
c_mdd = mdd <= SIM_HYBRID_MAX_MDD_PCT
|
||||||
|
c_fee = stress_pnl > 0.0
|
||||||
|
c_target = full_pnl >= SIM_OPTION_C_TARGET_PNL_PCT
|
||||||
|
|
||||||
|
all_go = c_base and c_holdout and c_mdd and c_fee
|
||||||
|
if SIM_PRIMARY_SIZING == "hybrid":
|
||||||
|
primary = "hybrid"
|
||||||
|
elif SIM_PRIMARY_SIZING == "causal_tier":
|
||||||
|
primary = "causal_tier"
|
||||||
|
else:
|
||||||
|
primary = "hybrid" if all_go else "causal_tier"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"go": all_go,
|
||||||
|
"primary_sizing": primary,
|
||||||
|
"checks": [
|
||||||
|
{"name": "monitor_rules_go", "pass": c_base},
|
||||||
|
{"name": "hybrid_holdout_pnl", "pass": c_holdout, "value": ho_pnl},
|
||||||
|
{"name": "hybrid_max_mdd", "pass": c_mdd, "value": mdd},
|
||||||
|
{"name": "hybrid_fee_stress_pnl", "pass": c_fee, "value": stress_pnl},
|
||||||
|
{
|
||||||
|
"name": "option_c_target_300pct",
|
||||||
|
"pass": c_target,
|
||||||
|
"value": full_pnl,
|
||||||
|
"optional": True,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def simulate_hybrid_order_cap(
|
||||||
|
outcomes: pd.DataFrame,
|
||||||
|
ohlc_df: pd.DataFrame,
|
||||||
|
*,
|
||||||
|
rule_ids: set[str] | None = None,
|
||||||
|
holdout_only: bool = True,
|
||||||
|
fee_rate: float = TRADING_FEE_RATE,
|
||||||
|
dd_large_pct: float | None = None,
|
||||||
|
dd_medium_pct: float | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
hybrid tier 복리 배분·슬리피지 가정 체결 가능 발화 집계.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
outcomes: fire_outcomes.
|
||||||
|
ohlc_df: 3m OHLC (drawdown).
|
||||||
|
rule_ids: monitor rule_id 필터.
|
||||||
|
holdout_only: holdout만.
|
||||||
|
fee_rate: 수수료율.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
simulate_live_order_cap과 동일 구조.
|
||||||
|
"""
|
||||||
|
from deepcoin.ground_truth.causal_gt_hybrid import build_monitor_hybrid_sized_trades
|
||||||
|
|
||||||
|
if outcomes.empty:
|
||||||
|
return {"rules": {}, "note": "발화 없음"}
|
||||||
|
|
||||||
|
df = outcomes.copy()
|
||||||
|
if holdout_only and "split" in df.columns:
|
||||||
|
df = df[df["split"] == "holdout"]
|
||||||
|
if rule_ids is not None:
|
||||||
|
df = df[df["rule_id"].isin(rule_ids)]
|
||||||
|
slip = LIVE_SLIPPAGE_PCT
|
||||||
|
|
||||||
|
sized, _ = build_monitor_hybrid_sized_trades(
|
||||||
|
sort_fires_chronological(df),
|
||||||
|
ohlc_df,
|
||||||
|
enhanced=False,
|
||||||
|
fee_rate=fee_rate,
|
||||||
|
dd_large_pct=dd_large_pct,
|
||||||
|
dd_medium_pct=dd_medium_pct,
|
||||||
|
)
|
||||||
|
executed_dts = {
|
||||||
|
t["dt"]
|
||||||
|
for t in sized
|
||||||
|
if t.get("action") == "sell" or float(t.get("amount_krw") or 0) > 0
|
||||||
|
}
|
||||||
|
if not executed_dts:
|
||||||
|
return {"rules": {}, "taken_count": 0, "total_count": int(len(df))}
|
||||||
|
|
||||||
|
taken = df[df["dt"].astype(str).isin(executed_dts)].copy()
|
||||||
|
taken["adj_ret_pct"] = taken["forward_ret_pct"] - slip
|
||||||
|
|
||||||
|
by_rule: dict[str, Any] = {}
|
||||||
|
for rid, grp in taken.groupby("rule_id"):
|
||||||
|
g = grp.copy()
|
||||||
|
g["forward_ret_pct"] = g["adj_ret_pct"]
|
||||||
|
by_rule[rid] = {
|
||||||
|
"taken_count": int(len(grp)),
|
||||||
|
"total_count": int((df["rule_id"] == rid).sum()),
|
||||||
|
"metrics": _rule_metrics(g),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"assumptions": {
|
||||||
|
"slippage_pct": slip,
|
||||||
|
"sizing": "hybrid_dd_tier_compound",
|
||||||
|
},
|
||||||
|
"taken_count": int(len(taken)),
|
||||||
|
"total_count": int(len(df)),
|
||||||
|
"rules": by_rule,
|
||||||
|
"portfolio_adj_ev_pct": round(float(taken["adj_ret_pct"].mean()), 4),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def build_simulation_report(
|
def build_simulation_report(
|
||||||
outcomes_path: Path | None = None,
|
outcomes_path: Path | None = None,
|
||||||
matched_path: Path | None = None,
|
matched_path: Path | None = None,
|
||||||
@@ -336,6 +522,63 @@ def build_simulation_report(
|
|||||||
last_price=float(mark) if mark else None,
|
last_price=float(mark) if mark else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 인과 GT leg 엔진 (split_buy + peak_sell, 캘리브레이션 파라미터)
|
||||||
|
cg_df = None
|
||||||
|
try:
|
||||||
|
from config import CHART_LOOKBACK_DAYS, MATCH_PRIMARY_INTERVAL, SYMBOL
|
||||||
|
from deepcoin.data.mtf_bb import load_frames_from_db
|
||||||
|
from deepcoin.ground_truth.causal_gt_calibrate import load_causal_gt_params
|
||||||
|
from deepcoin.ground_truth.causal_gt_trades import simulate_causal_gt_portfolio
|
||||||
|
from deepcoin.ops.monitor import Monitor
|
||||||
|
|
||||||
|
cg_params = load_causal_gt_params()
|
||||||
|
mon_cg = Monitor(cooldown_file=None)
|
||||||
|
cg_frames = load_frames_from_db(mon_cg, SYMBOL, lookback_days=CHART_LOOKBACK_DAYS)
|
||||||
|
cg_df = cg_frames[MATCH_PRIMARY_INTERVAL]
|
||||||
|
portfolio_compare["sim_causal_gt"] = simulate_causal_gt_portfolio(
|
||||||
|
cg_df,
|
||||||
|
last_price=float(mark) if mark else None,
|
||||||
|
**cg_params,
|
||||||
|
)
|
||||||
|
# Phase 3: monitor buy + 인과 peak sell + drawdown tier
|
||||||
|
from deepcoin.ground_truth.causal_gt_hybrid import (
|
||||||
|
simulate_causal_gt_hybrid_portfolio,
|
||||||
|
simulate_monitor_tier_enhanced_portfolio,
|
||||||
|
)
|
||||||
|
from deepcoin.ground_truth.hybrid_dd_calibrate import load_hybrid_dd_params
|
||||||
|
|
||||||
|
dd_params = load_hybrid_dd_params()
|
||||||
|
|
||||||
|
buy_only = all_monitor[all_monitor["side"] == "buy"]
|
||||||
|
portfolio_compare["sim_causal_hybrid"] = simulate_causal_gt_hybrid_portfolio(
|
||||||
|
buy_only,
|
||||||
|
cg_df,
|
||||||
|
monitor_fires=all_monitor,
|
||||||
|
last_price=float(mark) if mark else None,
|
||||||
|
cg_params=cg_params,
|
||||||
|
dd_large_pct=dd_params.get("dd_large_pct"),
|
||||||
|
dd_medium_pct=dd_params.get("dd_medium_pct"),
|
||||||
|
)
|
||||||
|
portfolio_compare["hybrid_dd_params"] = dd_params
|
||||||
|
portfolio_compare["sim_tier_enhanced"] = simulate_monitor_tier_enhanced_portfolio(
|
||||||
|
all_monitor,
|
||||||
|
cg_df,
|
||||||
|
last_price=float(mark) if mark else None,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
portfolio_compare["sim_causal_gt"] = {
|
||||||
|
"pnl_pct": 0.0,
|
||||||
|
"note": f"causal_gt sim skipped: {exc}",
|
||||||
|
}
|
||||||
|
portfolio_compare["sim_causal_hybrid"] = {
|
||||||
|
"pnl_pct": 0.0,
|
||||||
|
"note": f"causal_hybrid sim skipped: {exc}",
|
||||||
|
}
|
||||||
|
portfolio_compare["sim_tier_enhanced"] = {
|
||||||
|
"pnl_pct": 0.0,
|
||||||
|
"note": f"tier_enhanced sim skipped: {exc}",
|
||||||
|
}
|
||||||
|
|
||||||
holdout = outcomes[
|
holdout = outcomes[
|
||||||
outcomes["rule_id"].isin(monitor_ids) & (outcomes["split"] == "holdout")
|
outcomes["rule_id"].isin(monitor_ids) & (outcomes["split"] == "holdout")
|
||||||
]
|
]
|
||||||
@@ -349,24 +592,119 @@ def build_simulation_report(
|
|||||||
outcomes_ts = outcomes.copy()
|
outcomes_ts = outcomes.copy()
|
||||||
outcomes_ts["ts"] = pd.to_datetime(outcomes_ts["dt"])
|
outcomes_ts["ts"] = pd.to_datetime(outcomes_ts["dt"])
|
||||||
h0 = outcomes_ts["ts"].quantile(1.0 - MATCH_HOLDOUT_RATIO)
|
h0 = outcomes_ts["ts"].quantile(1.0 - MATCH_HOLDOUT_RATIO)
|
||||||
assets = [(s["dt"], float(s["total_asset_krw"])) for s in steps]
|
portfolio_compare["sim_sized_holdout"] = portfolio_holdout_from_steps(
|
||||||
pre = [a for d, a in assets if pd.to_datetime(d) < h0]
|
steps,
|
||||||
in_h = [a for d, a in assets if pd.to_datetime(d) >= h0]
|
h0,
|
||||||
asset_start = pre[-1] if pre else float(GT_INITIAL_CASH_KRW)
|
trade_count=int(len(holdout)),
|
||||||
asset_end = in_h[-1] if in_h else assets[-1][1]
|
note="전기간 복리(causal tier) 후 holdout 구간 자산 증감",
|
||||||
ho_pnl_pct = (
|
|
||||||
(asset_end - asset_start) / asset_start * 100.0
|
|
||||||
if asset_start > 0
|
|
||||||
else 0.0
|
|
||||||
)
|
)
|
||||||
portfolio_compare["sim_sized_holdout"] = {
|
|
||||||
"initial_asset_krw": round(asset_start, 0),
|
go_hybrid: dict[str, Any] = {"go": False, "note": "hybrid sim unavailable"}
|
||||||
"final_asset_krw": round(asset_end, 0),
|
go_option_c_phase2: dict[str, Any] = {"go": False, "note": "phase2 unavailable"}
|
||||||
"pnl_krw": round(asset_end - asset_start, 0),
|
if (
|
||||||
"pnl_pct": round(ho_pnl_pct, 2),
|
cg_df is not None
|
||||||
"note": "전기간 복리 후 holdout 구간 자산 증감 (1M 재시작 아님)",
|
and not all_monitor.empty
|
||||||
"trade_count": int(len(holdout)),
|
and portfolio_compare.get("sim_causal_hybrid", {}).get("sizing_mode")
|
||||||
}
|
):
|
||||||
|
from deepcoin.ground_truth.causal_gt_hybrid import build_monitor_hybrid_sized_trades
|
||||||
|
from deepcoin.ground_truth.gt_allocation import simulate_portfolio_steps
|
||||||
|
from deepcoin.ground_truth.hybrid_dd_calibrate import load_hybrid_dd_params
|
||||||
|
from deepcoin.matching.option_c_phase2 import (
|
||||||
|
evaluate_option_c_phase2_go,
|
||||||
|
simulate_hybrid_slippage_stress,
|
||||||
|
walk_forward_portfolio_by_month,
|
||||||
|
walk_forward_portfolio_summary,
|
||||||
|
)
|
||||||
|
|
||||||
|
dd_params = portfolio_compare.get("hybrid_dd_params") or load_hybrid_dd_params()
|
||||||
|
dd_large = dd_params.get("dd_large_pct")
|
||||||
|
dd_medium = dd_params.get("dd_medium_pct")
|
||||||
|
|
||||||
|
hybrid_full = portfolio_compare["sim_causal_hybrid"]
|
||||||
|
sized_h, _ = build_monitor_hybrid_sized_trades(
|
||||||
|
sort_fires_chronological(all_monitor),
|
||||||
|
cg_df,
|
||||||
|
enhanced=False,
|
||||||
|
dd_large_pct=dd_large,
|
||||||
|
dd_medium_pct=dd_medium,
|
||||||
|
)
|
||||||
|
steps_h = simulate_portfolio_steps(sized_h, use_amount_krw=True)
|
||||||
|
if steps_h and not holdout.empty:
|
||||||
|
outcomes_ts = outcomes.copy()
|
||||||
|
outcomes_ts["ts"] = pd.to_datetime(outcomes_ts["dt"])
|
||||||
|
h0 = outcomes_ts["ts"].quantile(1.0 - MATCH_HOLDOUT_RATIO)
|
||||||
|
portfolio_compare["sim_hybrid_holdout"] = portfolio_holdout_from_steps(
|
||||||
|
steps_h,
|
||||||
|
h0,
|
||||||
|
trade_count=int(len(holdout)),
|
||||||
|
note="전기간 복리(hybrid DD tier) 후 holdout 구간 자산 증감",
|
||||||
|
)
|
||||||
|
|
||||||
|
stress_fee = TRADING_FEE_RATE * SIM_FEE_STRESS_MULT
|
||||||
|
sized_stress, _ = build_monitor_hybrid_sized_trades(
|
||||||
|
sort_fires_chronological(all_monitor),
|
||||||
|
cg_df,
|
||||||
|
enhanced=False,
|
||||||
|
fee_rate=stress_fee,
|
||||||
|
dd_large_pct=dd_large,
|
||||||
|
dd_medium_pct=dd_medium,
|
||||||
|
)
|
||||||
|
portfolio_compare["sim_hybrid_fee_stress"] = simulate_portfolio_summary(
|
||||||
|
sized_stress,
|
||||||
|
fee_rate=stress_fee,
|
||||||
|
last_price=float(mark) if mark else None,
|
||||||
|
use_amount_krw=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
portfolio_compare["sim_hybrid_slippage_stress"] = simulate_hybrid_slippage_stress(
|
||||||
|
sized_h,
|
||||||
|
last_price=float(mark) if mark else None,
|
||||||
|
fee_rate=TRADING_FEE_RATE,
|
||||||
|
)
|
||||||
|
wf_rows = walk_forward_portfolio_by_month(steps_h)
|
||||||
|
wf_port = walk_forward_portfolio_summary(wf_rows)
|
||||||
|
portfolio_compare["hybrid_portfolio_walk_forward"] = wf_rows
|
||||||
|
portfolio_compare["hybrid_portfolio_wf_summary"] = wf_port
|
||||||
|
|
||||||
|
gt_pnl_for_phase2 = float(
|
||||||
|
(portfolio_compare.get("ground_truth_chrono") or {}).get("pnl_pct", 0)
|
||||||
|
)
|
||||||
|
go_hybrid = evaluate_hybrid_sizing_go(
|
||||||
|
go,
|
||||||
|
hybrid_full,
|
||||||
|
portfolio_compare.get("sim_hybrid_holdout") or {},
|
||||||
|
portfolio_compare.get("sim_hybrid_fee_stress") or {},
|
||||||
|
)
|
||||||
|
go_option_c_phase2 = evaluate_option_c_phase2_go(
|
||||||
|
go_hybrid,
|
||||||
|
hybrid_full,
|
||||||
|
portfolio_compare.get("sim_hybrid_holdout") or {},
|
||||||
|
portfolio_compare.get("sim_hybrid_fee_stress") or {},
|
||||||
|
portfolio_compare.get("sim_hybrid_slippage_stress") or {},
|
||||||
|
wf_port,
|
||||||
|
gt_pnl_for_phase2,
|
||||||
|
)
|
||||||
|
|
||||||
|
primary = go_hybrid.get("primary_sizing", "causal_tier")
|
||||||
|
portfolio_compare["primary_sizing"] = primary
|
||||||
|
if primary == "hybrid":
|
||||||
|
portfolio_compare["sim_primary"] = {
|
||||||
|
**hybrid_full,
|
||||||
|
"sizing_mode": "primary_hybrid_dd_tier",
|
||||||
|
"sizing_note": (
|
||||||
|
"권장: monitor + past-leg·drawdown tier (검증 통과, 미래 미사용)"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
live_cap = simulate_hybrid_order_cap(
|
||||||
|
outcomes,
|
||||||
|
cg_df,
|
||||||
|
rule_ids=monitor_ids,
|
||||||
|
holdout_only=True,
|
||||||
|
dd_large_pct=dd_large,
|
||||||
|
dd_medium_pct=dd_medium,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
portfolio_compare["sim_primary"] = portfolio_compare.get("sim_sized") or {}
|
||||||
|
|
||||||
if portfolio_compare.get("sim_sized") and portfolio_compare.get("ground_truth_chrono"):
|
if portfolio_compare.get("sim_sized") and portfolio_compare.get("ground_truth_chrono"):
|
||||||
gt_pnl = float(portfolio_compare["ground_truth_chrono"].get("pnl_pct", 0))
|
gt_pnl = float(portfolio_compare["ground_truth_chrono"].get("pnl_pct", 0))
|
||||||
@@ -383,6 +721,35 @@ def build_simulation_report(
|
|||||||
gtp / gt_pnl if abs(gt_pnl) > 1e-6 else 0.0,
|
gtp / gt_pnl if abs(gt_pnl) > 1e-6 else 0.0,
|
||||||
4,
|
4,
|
||||||
)
|
)
|
||||||
|
if portfolio_compare.get("sim_causal_gt"):
|
||||||
|
cgp = float(portfolio_compare["sim_causal_gt"].get("pnl_pct", 0))
|
||||||
|
portfolio_compare["causal_gt_capture_ratio"] = round(
|
||||||
|
cgp / gt_pnl if abs(gt_pnl) > 1e-6 else 0.0,
|
||||||
|
4,
|
||||||
|
)
|
||||||
|
portfolio_compare["sim_causal_gt_pnl_pct"] = cgp
|
||||||
|
if portfolio_compare.get("sim_causal_hybrid"):
|
||||||
|
chp = float(portfolio_compare["sim_causal_hybrid"].get("pnl_pct", 0))
|
||||||
|
portfolio_compare["causal_hybrid_capture_ratio"] = round(
|
||||||
|
chp / gt_pnl if abs(gt_pnl) > 1e-6 else 0.0,
|
||||||
|
4,
|
||||||
|
)
|
||||||
|
portfolio_compare["sim_causal_hybrid_pnl_pct"] = chp
|
||||||
|
if portfolio_compare.get("sim_tier_enhanced"):
|
||||||
|
tep = float(portfolio_compare["sim_tier_enhanced"].get("pnl_pct", 0))
|
||||||
|
portfolio_compare["tier_enhanced_capture_ratio"] = round(
|
||||||
|
tep / gt_pnl if abs(gt_pnl) > 1e-6 else 0.0,
|
||||||
|
4,
|
||||||
|
)
|
||||||
|
portfolio_compare["sim_tier_enhanced_pnl_pct"] = tep
|
||||||
|
|
||||||
|
portfolio_compare["causal_gt_params"] = {}
|
||||||
|
try:
|
||||||
|
from deepcoin.ground_truth.causal_gt_calibrate import load_causal_gt_params
|
||||||
|
|
||||||
|
portfolio_compare["causal_gt_params"] = load_causal_gt_params()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
portfolio_compare["gt_allocation_analysis"] = gt_alloc_analysis
|
portfolio_compare["gt_allocation_analysis"] = gt_alloc_analysis
|
||||||
portfolio_compare["causal_mode"] = {
|
portfolio_compare["causal_mode"] = {
|
||||||
@@ -423,6 +790,8 @@ def build_simulation_report(
|
|||||||
"fee_stress_by_rule": fee_stress,
|
"fee_stress_by_rule": fee_stress,
|
||||||
"live_order_cap_sim": live_cap,
|
"live_order_cap_sim": live_cap,
|
||||||
"go_no_go": go,
|
"go_no_go": go,
|
||||||
|
"go_no_go_hybrid": go_hybrid,
|
||||||
|
"go_no_go_option_c_phase2": go_option_c_phase2,
|
||||||
"portfolio_compare": portfolio_compare,
|
"portfolio_compare": portfolio_compare,
|
||||||
"gt_model": gt_data.get("model") or model_to_dict(default_model()),
|
"gt_model": gt_data.get("model") or model_to_dict(default_model()),
|
||||||
"gt_weight_policy": weight_policy_summary(default_model()),
|
"gt_weight_policy": weight_policy_summary(default_model()),
|
||||||
@@ -438,6 +807,12 @@ def build_simulation_report(
|
|||||||
"min_holdout_pf": SIM_GO_MIN_HOLDOUT_PF,
|
"min_holdout_pf": SIM_GO_MIN_HOLDOUT_PF,
|
||||||
"wf_positive_ratio": SIM_GO_WF_POSITIVE_RATIO,
|
"wf_positive_ratio": SIM_GO_WF_POSITIVE_RATIO,
|
||||||
"wf_min_months": SIM_WALK_FORWARD_MIN_MONTHS,
|
"wf_min_months": SIM_WALK_FORWARD_MIN_MONTHS,
|
||||||
|
"hybrid_min_holdout_pnl_pct": SIM_HYBRID_MIN_HOLDOUT_PNL_PCT,
|
||||||
|
"hybrid_max_mdd_pct": SIM_HYBRID_MAX_MDD_PCT,
|
||||||
|
"option_c_target_pnl_pct": SIM_OPTION_C_TARGET_PNL_PCT,
|
||||||
|
"option_c_phase2_target_pnl_pct": SIM_OPTION_C_PHASE2_TARGET_PNL_PCT,
|
||||||
|
"option_c_phase2_fee_stress_ratio": SIM_OPTION_C_PHASE2_FEE_STRESS_RATIO,
|
||||||
|
"option_c_min_gt_capture": SIM_OPTION_C_MIN_GT_CAPTURE,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,9 +855,26 @@ def run_simulation_report(
|
|||||||
)
|
)
|
||||||
write_simulation_html(report, MATCHING_SIMULATION_HTML)
|
write_simulation_html(report, MATCHING_SIMULATION_HTML)
|
||||||
go = report["go_no_go"]["go"]
|
go = report["go_no_go"]["go"]
|
||||||
|
go_h = report.get("go_no_go_hybrid") or {}
|
||||||
|
pc_early = report.get("portfolio_compare") or {}
|
||||||
print(f"[시뮬] 저장: {MATCHING_SIMULATION_JSON}")
|
print(f"[시뮬] 저장: {MATCHING_SIMULATION_JSON}")
|
||||||
print(f"[시뮬] 저장: {MATCHING_SIMULATION_HTML}")
|
print(f"[시뮬] 저장: {MATCHING_SIMULATION_HTML}")
|
||||||
print(f"[시뮬] Go/No-Go: {'GO' if go else 'NO-GO'}")
|
print(f"[시뮬] Go/No-Go (규칙): {'GO' if go else 'NO-GO'}")
|
||||||
|
print(
|
||||||
|
f"[시뮬] Go/No-Go (hybrid tier): {'GO' if go_h.get('go') else 'NO-GO'} "
|
||||||
|
f"· primary={pc_early.get('primary_sizing', '-')}"
|
||||||
|
)
|
||||||
|
for c in go_h.get("checks", []):
|
||||||
|
mark = "OK" if c.get("pass") else "NG"
|
||||||
|
opt = " (optional)" if c.get("optional") else ""
|
||||||
|
print(f" [hybrid {mark}] {c.get('name')}: {c.get('value', '-')}{opt}")
|
||||||
|
go_p2 = report.get("go_no_go_option_c_phase2") or {}
|
||||||
|
print(
|
||||||
|
f"[시뮬] Option C 2차(+1000%): {'GO' if go_p2.get('go') else 'NO-GO'}"
|
||||||
|
)
|
||||||
|
for c in go_p2.get("checks", []):
|
||||||
|
mark = "OK" if c.get("pass") else "NG"
|
||||||
|
print(f" [phase2 {mark}] {c.get('name')}: {c.get('value', '-')}")
|
||||||
for c in report["go_no_go"].get("checks", []):
|
for c in report["go_no_go"].get("checks", []):
|
||||||
mark = "OK" if c["pass"] else "NG"
|
mark = "OK" if c["pass"] else "NG"
|
||||||
print(
|
print(
|
||||||
@@ -504,6 +896,44 @@ def run_simulation_report(
|
|||||||
f"{pc.get('sim_gt_model', {}).get('pnl_pct')}% "
|
f"{pc.get('sim_gt_model', {}).get('pnl_pct')}% "
|
||||||
f"(capture={pc.get('gt_model_capture_ratio'):.2%})"
|
f"(capture={pc.get('gt_model_capture_ratio'):.2%})"
|
||||||
)
|
)
|
||||||
|
if pc.get("sim_causal_gt_pnl_pct") is not None:
|
||||||
|
scg = pc.get("sim_causal_gt") or {}
|
||||||
|
print(
|
||||||
|
f"[시뮬] GT 대비 sim_causal_gt(인과 leg): "
|
||||||
|
f"{pc.get('sim_causal_gt_pnl_pct')}% "
|
||||||
|
f"(capture={pc.get('causal_gt_capture_ratio', 0):.2%}, "
|
||||||
|
f"legs={scg.get('leg_count', '-')}, trades={scg.get('trade_count', '-')})"
|
||||||
|
)
|
||||||
|
if pc.get("sim_causal_hybrid_pnl_pct") is not None:
|
||||||
|
sch = pc.get("sim_causal_hybrid") or {}
|
||||||
|
print(
|
||||||
|
f"[시뮬] GT 대비 sim_causal_hybrid(monitor+DD tier): "
|
||||||
|
f"{pc.get('sim_causal_hybrid_pnl_pct')}% "
|
||||||
|
f"(capture={pc.get('causal_hybrid_capture_ratio', 0):.2%}, "
|
||||||
|
f"MDD={sch.get('max_drawdown_pct', '-')}%)"
|
||||||
|
)
|
||||||
|
if pc.get("sim_primary"):
|
||||||
|
sp = pc["sim_primary"]
|
||||||
|
print(
|
||||||
|
f"[시뮬] 권장 primary ({pc.get('primary_sizing')}): "
|
||||||
|
f"{sp.get('pnl_pct')}% · MDD={sp.get('max_drawdown_pct', '-')}%"
|
||||||
|
)
|
||||||
|
ho_h = pc.get("sim_hybrid_holdout") or {}
|
||||||
|
if ho_h.get("pnl_pct") is not None:
|
||||||
|
print(
|
||||||
|
f"[시뮬] hybrid holdout: {ho_h.get('pnl_pct')}% "
|
||||||
|
f"({ho_h.get('initial_asset_krw')}→{ho_h.get('final_asset_krw')})"
|
||||||
|
)
|
||||||
|
if pc.get("sim_tier_enhanced_pnl_pct") is not None:
|
||||||
|
ste = pc.get("sim_tier_enhanced") or {}
|
||||||
|
ast = ste.get("alloc_stats") or {}
|
||||||
|
print(
|
||||||
|
f"[시뮬] GT 대비 sim_tier_enhanced(conviction tier): "
|
||||||
|
f"{pc.get('sim_tier_enhanced_pnl_pct')}% "
|
||||||
|
f"(capture={pc.get('tier_enhanced_capture_ratio', 0):.2%}, "
|
||||||
|
f"large_buys={ast.get('large_tier_buy_count', '-')}, "
|
||||||
|
f"avg_buy={ast.get('buy_amount_avg_krw', '-')})"
|
||||||
|
)
|
||||||
if pc.get("sim_sized", {}).get("max_drawdown_pct") is not None:
|
if pc.get("sim_sized", {}).get("max_drawdown_pct") is not None:
|
||||||
print(
|
print(
|
||||||
f"[시뮬] sim_sized MDD: {pc['sim_sized']['max_drawdown_pct']}% "
|
f"[시뮬] sim_sized MDD: {pc['sim_sized']['max_drawdown_pct']}% "
|
||||||
|
|||||||
@@ -202,6 +202,13 @@ def _summary_cards_html(
|
|||||||
sim_trade_count: int,
|
sim_trade_count: int,
|
||||||
go_flag: bool,
|
go_flag: bool,
|
||||||
model_note: str = "",
|
model_note: str = "",
|
||||||
|
sim_causal_gt_pnl: dict[str, Any] | None = None,
|
||||||
|
sim_causal_hybrid_pnl: dict[str, Any] | None = None,
|
||||||
|
sim_tier_enhanced_pnl: dict[str, Any] | None = None,
|
||||||
|
sim_primary_pnl: dict[str, Any] | None = None,
|
||||||
|
primary_sizing: str = "",
|
||||||
|
hybrid_go: bool = False,
|
||||||
|
phase2_go: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
ground_truth HTML과 동일 구성의 상단 카드 (GT + 시뮬 2줄).
|
ground_truth HTML과 동일 구성의 상단 카드 (GT + 시뮬 2줄).
|
||||||
@@ -216,6 +223,12 @@ def _summary_cards_html(
|
|||||||
sim_trade_count: 체결 가정 발화 수.
|
sim_trade_count: 체결 가정 발화 수.
|
||||||
go_flag: Go/No-Go.
|
go_flag: Go/No-Go.
|
||||||
model_note: GT 모델 한 줄 요약.
|
model_note: GT 모델 한 줄 요약.
|
||||||
|
sim_causal_gt_pnl: 인과 GT leg 엔진 요약.
|
||||||
|
sim_causal_hybrid_pnl: monitor buy + 인과 sell 하이브리드.
|
||||||
|
sim_tier_enhanced_pnl: monitor + conviction tier.
|
||||||
|
sim_primary_pnl: 검증 통과 권장 배분 경로.
|
||||||
|
primary_sizing: hybrid | causal_tier.
|
||||||
|
hybrid_go: hybrid tier Go/No-Go.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
cards HTML.
|
cards HTML.
|
||||||
@@ -235,6 +248,61 @@ def _summary_cards_html(
|
|||||||
f'<span class="{go_cls}">{"GO" if go_flag else "NO-GO"}</span>'
|
f'<span class="{go_cls}">{"GO" if go_flag else "NO-GO"}</span>'
|
||||||
)
|
)
|
||||||
sim_fixed_title = f"시뮬·고정 ₩{LIVE_ORDER_KRW:,}/회 (비교)"
|
sim_fixed_title = f"시뮬·고정 ₩{LIVE_ORDER_KRW:,}/회 (비교)"
|
||||||
|
primary_block = ""
|
||||||
|
if sim_primary_pnl and float(sim_primary_pnl.get("pnl_pct") or 0) != 0:
|
||||||
|
hgo_cls = "go-pass" if hybrid_go else "go-fail"
|
||||||
|
primary_block = stacked_summary_cards_html(
|
||||||
|
(
|
||||||
|
f"권장 primary · {primary_sizing or 'causal_tier'} · "
|
||||||
|
f'<span class="{hgo_cls}">hybrid {"GO" if hybrid_go else "NO-GO"}</span> · '
|
||||||
|
f'2차 {"GO" if phase2_go else "NO-GO"}'
|
||||||
|
),
|
||||||
|
pnl_cards_html(
|
||||||
|
sim_primary_pnl,
|
||||||
|
"Primary",
|
||||||
|
int(sim_primary_pnl.get("trade_count") or 0),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
causal_block = ""
|
||||||
|
if sim_causal_gt_pnl and (
|
||||||
|
float(sim_causal_gt_pnl.get("pnl_pct") or 0) != 0
|
||||||
|
or sim_causal_gt_pnl.get("trade_count")
|
||||||
|
):
|
||||||
|
legs = sim_causal_gt_pnl.get("leg_count", "-")
|
||||||
|
causal_block += stacked_summary_cards_html(
|
||||||
|
f"인과 GT leg 엔진 (peak_local·분할매수·causal tier) · leg {legs}개",
|
||||||
|
pnl_cards_html(
|
||||||
|
sim_causal_gt_pnl,
|
||||||
|
"인과 GT",
|
||||||
|
int(sim_causal_gt_pnl.get("trade_count") or 0),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if sim_causal_hybrid_pnl and (
|
||||||
|
float(sim_causal_hybrid_pnl.get("pnl_pct") or 0) != 0
|
||||||
|
or sim_causal_hybrid_pnl.get("trade_count")
|
||||||
|
):
|
||||||
|
legs_h = sim_causal_hybrid_pnl.get("leg_count", "-")
|
||||||
|
causal_block += stacked_summary_cards_html(
|
||||||
|
f"하이브리드 (monitor + DD tier) · 발화 {sim_causal_hybrid_pnl.get('input_fires', '-')}건",
|
||||||
|
pnl_cards_html(
|
||||||
|
sim_causal_hybrid_pnl,
|
||||||
|
"하이브리드",
|
||||||
|
int(sim_causal_hybrid_pnl.get("trade_count") or 0),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if sim_tier_enhanced_pnl and (
|
||||||
|
float(sim_tier_enhanced_pnl.get("pnl_pct") or 0) != 0
|
||||||
|
or sim_tier_enhanced_pnl.get("trade_count")
|
||||||
|
):
|
||||||
|
ast = sim_tier_enhanced_pnl.get("alloc_stats") or {}
|
||||||
|
causal_block += stacked_summary_cards_html(
|
||||||
|
f"Enhanced tier (conviction·DD) · large매수 {ast.get('large_tier_buy_count', '-')}건",
|
||||||
|
pnl_cards_html(
|
||||||
|
sim_tier_enhanced_pnl,
|
||||||
|
"Enhanced",
|
||||||
|
int(sim_tier_enhanced_pnl.get("trade_count") or 0),
|
||||||
|
),
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
'<div class="summary-cards">'
|
'<div class="summary-cards">'
|
||||||
+ stacked_summary_cards_html(gt_sub, gt_cards)
|
+ stacked_summary_cards_html(gt_sub, gt_cards)
|
||||||
@@ -246,6 +314,8 @@ def _summary_cards_html(
|
|||||||
sim_fixed_title,
|
sim_fixed_title,
|
||||||
pnl_cards_html(sim_fixed_pnl, "시뮬(고정)", sim_trade_count),
|
pnl_cards_html(sim_fixed_pnl, "시뮬(고정)", sim_trade_count),
|
||||||
)
|
)
|
||||||
|
+ primary_block
|
||||||
|
+ causal_block
|
||||||
+ "</div>"
|
+ "</div>"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -298,6 +368,11 @@ def build_simulation_page_html(
|
|||||||
|
|
||||||
go = report.get("go_no_go", {})
|
go = report.get("go_no_go", {})
|
||||||
go_flag = bool(go.get("go"))
|
go_flag = bool(go.get("go"))
|
||||||
|
go_hybrid = report.get("go_no_go_hybrid") or {}
|
||||||
|
hybrid_go_flag = bool(go_hybrid.get("go"))
|
||||||
|
go_phase2 = report.get("go_no_go_option_c_phase2") or {}
|
||||||
|
phase2_go_flag = bool(go_phase2.get("go"))
|
||||||
|
pc = report.get("portfolio_compare") or {}
|
||||||
label_mode = report.get("label_mode", "leg_gt")
|
label_mode = report.get("label_mode", "leg_gt")
|
||||||
|
|
||||||
frames = load_chart_frames()
|
frames = load_chart_frames()
|
||||||
@@ -372,6 +447,36 @@ def build_simulation_page_html(
|
|||||||
|
|
||||||
criteria_blocks = "".join(rule_criteria_html(r) for r in monitor_rules)
|
criteria_blocks = "".join(rule_criteria_html(r) for r in monitor_rules)
|
||||||
go_table = go_no_go_table_html(go.get("checks", []), go_flag)
|
go_table = go_no_go_table_html(go.get("checks", []), go_flag)
|
||||||
|
hybrid_checks = go_hybrid.get("checks") or []
|
||||||
|
if hybrid_checks:
|
||||||
|
hybrid_rows = "".join(
|
||||||
|
f"<tr><td>{c.get('name')}</td>"
|
||||||
|
f"<td>{'PASS' if c.get('pass') else 'FAIL'}</td>"
|
||||||
|
f"<td>{c.get('value', '-')}</td></tr>"
|
||||||
|
for c in hybrid_checks
|
||||||
|
)
|
||||||
|
hgo_cls = "go-pass" if hybrid_go_flag else "go-fail"
|
||||||
|
go_table += (
|
||||||
|
f"<h2>Hybrid tier Go/No-Go · "
|
||||||
|
f'<span class="{hgo_cls}">{"GO" if hybrid_go_flag else "NO-GO"}</span></h2>'
|
||||||
|
f"<table><thead><tr><th>검사</th><th>결과</th><th>값</th></tr></thead>"
|
||||||
|
f"<tbody>{hybrid_rows}</tbody></table>"
|
||||||
|
)
|
||||||
|
phase2_checks = go_phase2.get("checks") or []
|
||||||
|
if phase2_checks:
|
||||||
|
p2_rows = "".join(
|
||||||
|
f"<tr><td>{c.get('name')}</td>"
|
||||||
|
f"<td>{'PASS' if c.get('pass') else 'FAIL'}</td>"
|
||||||
|
f"<td>{c.get('value', '-')}</td></tr>"
|
||||||
|
for c in phase2_checks
|
||||||
|
)
|
||||||
|
p2_cls = "go-pass" if phase2_go_flag else "go-fail"
|
||||||
|
go_table += (
|
||||||
|
f"<h2>Option C 2차 (+1000%) · "
|
||||||
|
f'<span class="{p2_cls}">{"GO" if phase2_go_flag else "NO-GO"}</span></h2>'
|
||||||
|
f"<table><thead><tr><th>검사</th><th>결과</th><th>값</th></tr></thead>"
|
||||||
|
f"<tbody>{p2_rows}</tbody></table>"
|
||||||
|
)
|
||||||
|
|
||||||
def _mark_note(price: float) -> str:
|
def _mark_note(price: float) -> str:
|
||||||
if price > 0:
|
if price > 0:
|
||||||
@@ -439,6 +544,13 @@ def build_simulation_page_html(
|
|||||||
len(compound_fires),
|
len(compound_fires),
|
||||||
go_flag,
|
go_flag,
|
||||||
model_note=model_note,
|
model_note=model_note,
|
||||||
|
sim_causal_gt_pnl=pc.get("sim_causal_gt"),
|
||||||
|
sim_causal_hybrid_pnl=pc.get("sim_causal_hybrid"),
|
||||||
|
sim_tier_enhanced_pnl=pc.get("sim_tier_enhanced"),
|
||||||
|
sim_primary_pnl=pc.get("sim_primary"),
|
||||||
|
primary_sizing=str(pc.get("primary_sizing") or ""),
|
||||||
|
hybrid_go=hybrid_go_flag,
|
||||||
|
phase2_go=phase2_go_flag,
|
||||||
)
|
)
|
||||||
|
|
||||||
if frames is not None:
|
if frames is not None:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from pathlib import Path
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from config import (
|
from config import (
|
||||||
|
CHART_LOOKBACK_DAYS,
|
||||||
COIN_NAME,
|
COIN_NAME,
|
||||||
LIVE_COOLDOWN_MIN,
|
LIVE_COOLDOWN_MIN,
|
||||||
LIVE_DAILY_KRW_MAX,
|
LIVE_DAILY_KRW_MAX,
|
||||||
@@ -18,11 +19,14 @@ from config import (
|
|||||||
LIVE_MAX_TRADES_PER_DAY,
|
LIVE_MAX_TRADES_PER_DAY,
|
||||||
LIVE_ORDER_KRW,
|
LIVE_ORDER_KRW,
|
||||||
LIVE_TRADING_ENABLED,
|
LIVE_TRADING_ENABLED,
|
||||||
|
MATCH_PRIMARY_INTERVAL,
|
||||||
SYMBOL,
|
SYMBOL,
|
||||||
TRADING_FEE_RATE,
|
TRADING_FEE_RATE,
|
||||||
)
|
)
|
||||||
|
from deepcoin.data.mtf_bb import load_frames_from_db
|
||||||
from deepcoin.ground_truth.ground_truth import load_ground_truth
|
from deepcoin.ground_truth.ground_truth import load_ground_truth
|
||||||
from deepcoin.matching.live_eval import evaluate_live_rules
|
from deepcoin.matching.live_eval import evaluate_live_rules
|
||||||
|
from deepcoin.matching.live_sizing import LivePositionState, live_sizing_enabled
|
||||||
from deepcoin.matching.load_rules import load_monitor_rules
|
from deepcoin.matching.load_rules import load_monitor_rules
|
||||||
from deepcoin.matching.position_sizing import (
|
from deepcoin.matching.position_sizing import (
|
||||||
compute_buy_amount_krw,
|
compute_buy_amount_krw,
|
||||||
@@ -52,6 +56,8 @@ class LiveTrader(Monitor):
|
|||||||
self._gt_trades: list[dict] = []
|
self._gt_trades: list[dict] = []
|
||||||
self._large_legs: set[int] = set()
|
self._large_legs: set[int] = set()
|
||||||
self._approved_rules: set[str] = set()
|
self._approved_rules: set[str] = set()
|
||||||
|
self._position_state = LivePositionState.load()
|
||||||
|
self._ohlc_df = None
|
||||||
self._load_sizing_context()
|
self._load_sizing_context()
|
||||||
|
|
||||||
def _load_sizing_context(self) -> None:
|
def _load_sizing_context(self) -> None:
|
||||||
@@ -104,10 +110,20 @@ class LiveTrader(Monitor):
|
|||||||
return False, f"규칙 쿨다운({LIVE_COOLDOWN_MIN}분)"
|
return False, f"규칙 쿨다운({LIVE_COOLDOWN_MIN}분)"
|
||||||
return True, ""
|
return True, ""
|
||||||
|
|
||||||
|
def _load_ohlc_df(self) -> None:
|
||||||
|
"""drawdown tier용 3m OHLC 캐시."""
|
||||||
|
try:
|
||||||
|
frames = load_frames_from_db(self, SYMBOL, lookback_days=CHART_LOOKBACK_DAYS)
|
||||||
|
self._ohlc_df = frames.get(MATCH_PRIMARY_INTERVAL)
|
||||||
|
except Exception:
|
||||||
|
self._ohlc_df = None
|
||||||
|
|
||||||
def _resolve_buy_amount_krw(self, hit: dict[str, Any]) -> float:
|
def _resolve_buy_amount_krw(self, hit: dict[str, Any]) -> float:
|
||||||
"""
|
"""
|
||||||
총자산·현금·EV/WF·leg 티어로 매수 원화 산출.
|
총자산·현금·EV/WF·leg 티어로 매수 원화 산출.
|
||||||
|
|
||||||
|
GT_SIGNAL_CAUSAL=1 이면 시뮬 sim_tier_enhanced와 동일 인과 tier·weight.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
hit: evaluate_live_rules 항목.
|
hit: evaluate_live_rules 항목.
|
||||||
|
|
||||||
@@ -127,6 +143,18 @@ class LiveTrader(Monitor):
|
|||||||
qty = float(sym.get("balance") or 0)
|
qty = float(sym.get("balance") or 0)
|
||||||
except Exception:
|
except Exception:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
if live_sizing_enabled():
|
||||||
|
if self._ohlc_df is None:
|
||||||
|
self._load_ohlc_df()
|
||||||
|
return self._position_state.plan_buy_amount_krw(
|
||||||
|
hit["dt"],
|
||||||
|
price,
|
||||||
|
cash,
|
||||||
|
qty,
|
||||||
|
self._ohlc_df,
|
||||||
|
enhanced=False,
|
||||||
|
fee_rate=TRADING_FEE_RATE,
|
||||||
|
)
|
||||||
scale = live_buy_asset_pct_scale(
|
scale = live_buy_asset_pct_scale(
|
||||||
rid,
|
rid,
|
||||||
hit["dt"],
|
hit["dt"],
|
||||||
@@ -200,18 +228,31 @@ class LiveTrader(Monitor):
|
|||||||
if qty <= 0:
|
if qty <= 0:
|
||||||
record["message"] = "보유 없음"
|
record["message"] = "보유 없음"
|
||||||
else:
|
else:
|
||||||
|
gross = qty * price
|
||||||
|
record["amount_krw"] = round(gross, 0)
|
||||||
ok = self.sellCoinMarket(SYMBOL, int(price), qty)
|
ok = self.sellCoinMarket(SYMBOL, int(price), qty)
|
||||||
record["ok"] = bool(ok)
|
record["ok"] = bool(ok)
|
||||||
record["message"] = f"sell qty={qty}" if ok else "sell failed"
|
record["message"] = f"sell qty={qty}" if ok else "sell failed"
|
||||||
|
if record["ok"] and live_sizing_enabled():
|
||||||
|
fee = gross * TRADING_FEE_RATE
|
||||||
|
self._position_state.record_sell(
|
||||||
|
gross, fee, full_close=True
|
||||||
|
)
|
||||||
|
self._position_state.save()
|
||||||
else:
|
else:
|
||||||
record["message"] = f"unknown side {side}"
|
record["message"] = f"unknown side {side}"
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
record["message"] = str(exc)
|
record["message"] = str(exc)
|
||||||
|
|
||||||
if record["ok"]:
|
if record["ok"]:
|
||||||
self._day_spent_krw += amount_krw
|
spent = float(record.get("amount_krw") or amount_krw)
|
||||||
|
self._day_spent_krw += spent
|
||||||
self._day_trades += 1
|
self._day_trades += 1
|
||||||
self._rule_last_unix[hit["rule_id"]] = time.time()
|
self._rule_last_unix[hit["rule_id"]] = time.time()
|
||||||
|
if live_sizing_enabled() and side == "buy":
|
||||||
|
fee = spent * TRADING_FEE_RATE
|
||||||
|
self._position_state.record_buy(hit["dt"], price, spent, fee)
|
||||||
|
self._position_state.save()
|
||||||
return record
|
return record
|
||||||
|
|
||||||
def run_once(self) -> None:
|
def run_once(self) -> None:
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ MATCHING_BACKTEST_HTML = DOCS_MATCHING / "backtest_summary.html"
|
|||||||
MATCHING_GT_OVERLAP = DOCS_MATCHING / "gt_overlap_report.json"
|
MATCHING_GT_OVERLAP = DOCS_MATCHING / "gt_overlap_report.json"
|
||||||
MATCHING_SIMULATION_JSON = DOCS_MATCHING / "simulation_report.json"
|
MATCHING_SIMULATION_JSON = DOCS_MATCHING / "simulation_report.json"
|
||||||
MATCHING_SIMULATION_HTML = DOCS_MATCHING / "simulation_report.html"
|
MATCHING_SIMULATION_HTML = DOCS_MATCHING / "simulation_report.html"
|
||||||
|
MATCHING_CAUSAL_GT_CALIBRATION_JSON = DOCS_MATCHING / "causal_gt_calibration.json"
|
||||||
|
MATCHING_HYBRID_DD_CALIBRATION_JSON = DOCS_MATCHING / "hybrid_dd_calibration.json"
|
||||||
MATCHING_GT_COMPARISON_JSON = DOCS_MATCHING / "gt_comparison_report.json"
|
MATCHING_GT_COMPARISON_JSON = DOCS_MATCHING / "gt_comparison_report.json"
|
||||||
MATCHING_GT_COMPARISON_HTML = DOCS_MATCHING / "gt_comparison_report.html"
|
MATCHING_GT_COMPARISON_HTML = DOCS_MATCHING / "gt_comparison_report.html"
|
||||||
|
|
||||||
|
|||||||
379
docs/04_matching/causal_gt_calibration.json
Normal file
379
docs/04_matching/causal_gt_calibration.json
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
{
|
||||||
|
"symbol": "WLD",
|
||||||
|
"interval_min": 3,
|
||||||
|
"gt_pnl_pct": 4291.35,
|
||||||
|
"grid_combinations": 1728,
|
||||||
|
"valid_combinations": 432,
|
||||||
|
"min_trades": 30,
|
||||||
|
"best": {
|
||||||
|
"pnl_pct": 14.66,
|
||||||
|
"trade_count": 134,
|
||||||
|
"leg_count": 17,
|
||||||
|
"max_drawdown_pct": 0.96,
|
||||||
|
"capture_ratio": 0.0034,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 8,
|
||||||
|
"buy_swing_pct": 2.0,
|
||||||
|
"sell_swing_pct": 3.0,
|
||||||
|
"bb_max": 0.55,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 60,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"best_params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 8,
|
||||||
|
"buy_swing_pct": 2.0,
|
||||||
|
"sell_swing_pct": 3.0,
|
||||||
|
"bb_max": 0.55,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 60,
|
||||||
|
"use_local_trough": false
|
||||||
|
},
|
||||||
|
"top": [
|
||||||
|
{
|
||||||
|
"pnl_pct": 14.66,
|
||||||
|
"trade_count": 134,
|
||||||
|
"leg_count": 17,
|
||||||
|
"max_drawdown_pct": 0.96,
|
||||||
|
"capture_ratio": 0.0034,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 8,
|
||||||
|
"buy_swing_pct": 2.0,
|
||||||
|
"sell_swing_pct": 3.0,
|
||||||
|
"bb_max": 0.55,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 60,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pnl_pct": 14.66,
|
||||||
|
"trade_count": 134,
|
||||||
|
"leg_count": 17,
|
||||||
|
"max_drawdown_pct": 0.96,
|
||||||
|
"capture_ratio": 0.0034,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 8,
|
||||||
|
"buy_swing_pct": 2.0,
|
||||||
|
"sell_swing_pct": 3.0,
|
||||||
|
"bb_max": 0.65,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 60,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pnl_pct": 14.66,
|
||||||
|
"trade_count": 134,
|
||||||
|
"leg_count": 17,
|
||||||
|
"max_drawdown_pct": 0.96,
|
||||||
|
"capture_ratio": 0.0034,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 8,
|
||||||
|
"buy_swing_pct": 2.0,
|
||||||
|
"sell_swing_pct": 3.0,
|
||||||
|
"bb_max": 0.75,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 60,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pnl_pct": 14.66,
|
||||||
|
"trade_count": 134,
|
||||||
|
"leg_count": 17,
|
||||||
|
"max_drawdown_pct": 0.96,
|
||||||
|
"capture_ratio": 0.0034,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 8,
|
||||||
|
"buy_swing_pct": 2.0,
|
||||||
|
"sell_swing_pct": 4.0,
|
||||||
|
"bb_max": 0.55,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 60,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pnl_pct": 14.66,
|
||||||
|
"trade_count": 134,
|
||||||
|
"leg_count": 17,
|
||||||
|
"max_drawdown_pct": 0.96,
|
||||||
|
"capture_ratio": 0.0034,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 8,
|
||||||
|
"buy_swing_pct": 2.0,
|
||||||
|
"sell_swing_pct": 4.0,
|
||||||
|
"bb_max": 0.65,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 60,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pnl_pct": 14.66,
|
||||||
|
"trade_count": 134,
|
||||||
|
"leg_count": 17,
|
||||||
|
"max_drawdown_pct": 0.96,
|
||||||
|
"capture_ratio": 0.0034,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 8,
|
||||||
|
"buy_swing_pct": 2.0,
|
||||||
|
"sell_swing_pct": 4.0,
|
||||||
|
"bb_max": 0.75,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 60,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pnl_pct": 14.66,
|
||||||
|
"trade_count": 134,
|
||||||
|
"leg_count": 17,
|
||||||
|
"max_drawdown_pct": 0.96,
|
||||||
|
"capture_ratio": 0.0034,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 8,
|
||||||
|
"buy_swing_pct": 2.5,
|
||||||
|
"sell_swing_pct": 3.0,
|
||||||
|
"bb_max": 0.55,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 60,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pnl_pct": 14.66,
|
||||||
|
"trade_count": 134,
|
||||||
|
"leg_count": 17,
|
||||||
|
"max_drawdown_pct": 0.96,
|
||||||
|
"capture_ratio": 0.0034,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 8,
|
||||||
|
"buy_swing_pct": 2.5,
|
||||||
|
"sell_swing_pct": 3.0,
|
||||||
|
"bb_max": 0.65,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 60,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pnl_pct": 14.66,
|
||||||
|
"trade_count": 134,
|
||||||
|
"leg_count": 17,
|
||||||
|
"max_drawdown_pct": 0.96,
|
||||||
|
"capture_ratio": 0.0034,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 8,
|
||||||
|
"buy_swing_pct": 2.5,
|
||||||
|
"sell_swing_pct": 3.0,
|
||||||
|
"bb_max": 0.75,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 60,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pnl_pct": 14.66,
|
||||||
|
"trade_count": 134,
|
||||||
|
"leg_count": 17,
|
||||||
|
"max_drawdown_pct": 0.96,
|
||||||
|
"capture_ratio": 0.0034,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 8,
|
||||||
|
"buy_swing_pct": 2.5,
|
||||||
|
"sell_swing_pct": 4.0,
|
||||||
|
"bb_max": 0.55,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 60,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pnl_pct": 14.66,
|
||||||
|
"trade_count": 134,
|
||||||
|
"leg_count": 17,
|
||||||
|
"max_drawdown_pct": 0.96,
|
||||||
|
"capture_ratio": 0.0034,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 8,
|
||||||
|
"buy_swing_pct": 2.5,
|
||||||
|
"sell_swing_pct": 4.0,
|
||||||
|
"bb_max": 0.65,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 60,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pnl_pct": 14.66,
|
||||||
|
"trade_count": 134,
|
||||||
|
"leg_count": 17,
|
||||||
|
"max_drawdown_pct": 0.96,
|
||||||
|
"capture_ratio": 0.0034,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 8,
|
||||||
|
"buy_swing_pct": 2.5,
|
||||||
|
"sell_swing_pct": 4.0,
|
||||||
|
"bb_max": 0.75,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 60,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pnl_pct": 14.66,
|
||||||
|
"trade_count": 134,
|
||||||
|
"leg_count": 17,
|
||||||
|
"max_drawdown_pct": 0.96,
|
||||||
|
"capture_ratio": 0.0034,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 8,
|
||||||
|
"buy_swing_pct": 3.0,
|
||||||
|
"sell_swing_pct": 3.0,
|
||||||
|
"bb_max": 0.55,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 60,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pnl_pct": 14.66,
|
||||||
|
"trade_count": 134,
|
||||||
|
"leg_count": 17,
|
||||||
|
"max_drawdown_pct": 0.96,
|
||||||
|
"capture_ratio": 0.0034,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 8,
|
||||||
|
"buy_swing_pct": 3.0,
|
||||||
|
"sell_swing_pct": 3.0,
|
||||||
|
"bb_max": 0.65,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 60,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pnl_pct": 14.66,
|
||||||
|
"trade_count": 134,
|
||||||
|
"leg_count": 17,
|
||||||
|
"max_drawdown_pct": 0.96,
|
||||||
|
"capture_ratio": 0.0034,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 8,
|
||||||
|
"buy_swing_pct": 3.0,
|
||||||
|
"sell_swing_pct": 3.0,
|
||||||
|
"bb_max": 0.75,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 60,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pnl_pct": 14.66,
|
||||||
|
"trade_count": 134,
|
||||||
|
"leg_count": 17,
|
||||||
|
"max_drawdown_pct": 0.96,
|
||||||
|
"capture_ratio": 0.0034,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 8,
|
||||||
|
"buy_swing_pct": 3.0,
|
||||||
|
"sell_swing_pct": 4.0,
|
||||||
|
"bb_max": 0.55,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 60,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pnl_pct": 14.66,
|
||||||
|
"trade_count": 134,
|
||||||
|
"leg_count": 17,
|
||||||
|
"max_drawdown_pct": 0.96,
|
||||||
|
"capture_ratio": 0.0034,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 8,
|
||||||
|
"buy_swing_pct": 3.0,
|
||||||
|
"sell_swing_pct": 4.0,
|
||||||
|
"bb_max": 0.65,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 60,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pnl_pct": 14.66,
|
||||||
|
"trade_count": 134,
|
||||||
|
"leg_count": 17,
|
||||||
|
"max_drawdown_pct": 0.96,
|
||||||
|
"capture_ratio": 0.0034,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 8,
|
||||||
|
"buy_swing_pct": 3.0,
|
||||||
|
"sell_swing_pct": 4.0,
|
||||||
|
"bb_max": 0.75,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 60,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pnl_pct": 14.35,
|
||||||
|
"trade_count": 133,
|
||||||
|
"leg_count": 18,
|
||||||
|
"max_drawdown_pct": 0.81,
|
||||||
|
"capture_ratio": 0.0033,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 12,
|
||||||
|
"buy_swing_pct": 2.0,
|
||||||
|
"sell_swing_pct": 3.0,
|
||||||
|
"bb_max": 0.55,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 90,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pnl_pct": 14.35,
|
||||||
|
"trade_count": 133,
|
||||||
|
"leg_count": 18,
|
||||||
|
"max_drawdown_pct": 0.81,
|
||||||
|
"capture_ratio": 0.0033,
|
||||||
|
"params": {
|
||||||
|
"peak_mode": "local",
|
||||||
|
"pivot_order": 12,
|
||||||
|
"buy_swing_pct": 2.0,
|
||||||
|
"sell_swing_pct": 3.0,
|
||||||
|
"bb_max": 0.65,
|
||||||
|
"min_leg_pct": 8.0,
|
||||||
|
"min_bars_between_legs": 90,
|
||||||
|
"use_local_trough": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"target_pnl_pct": 300.0,
|
||||||
|
"target_met": false
|
||||||
|
}
|
||||||
76
docs/04_matching/hybrid_dd_calibration.json
Normal file
76
docs/04_matching/hybrid_dd_calibration.json
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"best_params": {
|
||||||
|
"dd_large_pct": 5.0,
|
||||||
|
"dd_medium_pct": 2.0
|
||||||
|
},
|
||||||
|
"best_metrics": {
|
||||||
|
"dd_large_pct": 5.0,
|
||||||
|
"dd_medium_pct": 2.0,
|
||||||
|
"train_pnl_pct": 681.36,
|
||||||
|
"holdout_pnl_pct": 62.36,
|
||||||
|
"full_pnl_pct": 1120.97,
|
||||||
|
"max_drawdown_pct": 19.22,
|
||||||
|
"wf_positive_months": 11,
|
||||||
|
"wf_months": 12,
|
||||||
|
"large_tier_buys": 1598
|
||||||
|
},
|
||||||
|
"grid_size": 18,
|
||||||
|
"top5": [
|
||||||
|
{
|
||||||
|
"dd_large_pct": 5.0,
|
||||||
|
"dd_medium_pct": 2.0,
|
||||||
|
"train_pnl_pct": 681.36,
|
||||||
|
"holdout_pnl_pct": 62.36,
|
||||||
|
"full_pnl_pct": 1120.97,
|
||||||
|
"max_drawdown_pct": 19.22,
|
||||||
|
"wf_positive_months": 11,
|
||||||
|
"wf_months": 12,
|
||||||
|
"large_tier_buys": 1598
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dd_large_pct": 5.0,
|
||||||
|
"dd_medium_pct": 3.0,
|
||||||
|
"train_pnl_pct": 681.36,
|
||||||
|
"holdout_pnl_pct": 62.36,
|
||||||
|
"full_pnl_pct": 1120.97,
|
||||||
|
"max_drawdown_pct": 19.22,
|
||||||
|
"wf_positive_months": 11,
|
||||||
|
"wf_months": 12,
|
||||||
|
"large_tier_buys": 1598
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dd_large_pct": 5.0,
|
||||||
|
"dd_medium_pct": 4.0,
|
||||||
|
"train_pnl_pct": 681.36,
|
||||||
|
"holdout_pnl_pct": 62.36,
|
||||||
|
"full_pnl_pct": 1120.97,
|
||||||
|
"max_drawdown_pct": 19.22,
|
||||||
|
"wf_positive_months": 11,
|
||||||
|
"wf_months": 12,
|
||||||
|
"large_tier_buys": 1598
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dd_large_pct": 12.0,
|
||||||
|
"dd_medium_pct": 2.0,
|
||||||
|
"train_pnl_pct": 671.91,
|
||||||
|
"holdout_pnl_pct": 62.36,
|
||||||
|
"full_pnl_pct": 1106.19,
|
||||||
|
"max_drawdown_pct": 15.64,
|
||||||
|
"wf_positive_months": 11,
|
||||||
|
"wf_months": 12,
|
||||||
|
"large_tier_buys": 1531
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dd_large_pct": 12.0,
|
||||||
|
"dd_medium_pct": 3.0,
|
||||||
|
"train_pnl_pct": 671.91,
|
||||||
|
"holdout_pnl_pct": 62.36,
|
||||||
|
"full_pnl_pct": 1106.19,
|
||||||
|
"max_drawdown_pct": 15.64,
|
||||||
|
"wf_positive_months": 11,
|
||||||
|
"wf_months": 12,
|
||||||
|
"large_tier_buys": 1531
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"holdout_start": "2026-04-11 05:33:00"
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
11
scripts/04_causal_gt_calibrate.py
Normal file
11
scripts/04_causal_gt_calibrate.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""인과 GT leg 엔진 파라미터 캘리브레이션 (Option C +300% 경로)."""
|
||||||
|
import runpy
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
|
||||||
|
|
||||||
|
from deepcoin.ground_truth.causal_gt_calibrate import run_causal_gt_calibration
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run_causal_gt_calibration()
|
||||||
52
scripts/04_hybrid_dd_calibrate.py
Normal file
52
scripts/04_hybrid_dd_calibrate.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Hybrid DD tier 임계값 train 캘리브레이션 (Option C 2차)."""
|
||||||
|
import runpy
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from config import CHART_LOOKBACK_DAYS, MATCH_PRIMARY_INTERVAL, SYMBOL
|
||||||
|
from deepcoin.data.mtf_bb import load_frames_from_db
|
||||||
|
from deepcoin.ground_truth.ground_truth import load_ground_truth
|
||||||
|
from deepcoin.ground_truth.hybrid_dd_calibrate import run_and_save_calibration
|
||||||
|
from deepcoin.matching.load_rules import load_matched_rules
|
||||||
|
from deepcoin.ops.monitor import Monitor
|
||||||
|
from deepcoin.paths import MATCHING_FIRE_OUTCOMES, MATCHING_HYBRID_DD_CALIBRATION_JSON
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
"""monitor 발화 + OHLC로 DD tier 캘리브레이션."""
|
||||||
|
outcomes = pd.read_csv(MATCHING_FIRE_OUTCOMES)
|
||||||
|
matched = load_matched_rules()
|
||||||
|
monitor_ids = {r["rule_id"] for r in matched.get("monitor_rules", [])}
|
||||||
|
fires = outcomes[outcomes["rule_id"].isin(monitor_ids)]
|
||||||
|
|
||||||
|
mon = Monitor(cooldown_file=None)
|
||||||
|
ohlc = load_frames_from_db(mon, SYMBOL, lookback_days=CHART_LOOKBACK_DAYS)[
|
||||||
|
MATCH_PRIMARY_INTERVAL
|
||||||
|
]
|
||||||
|
gt = load_ground_truth() or {}
|
||||||
|
mark = (gt.get("summary") or {}).get("mark_price")
|
||||||
|
|
||||||
|
result = run_and_save_calibration(
|
||||||
|
fires,
|
||||||
|
ohlc,
|
||||||
|
outcomes=outcomes,
|
||||||
|
last_price=float(mark) if mark else None,
|
||||||
|
)
|
||||||
|
best = result.get("best_params") or {}
|
||||||
|
metrics = result.get("best_metrics") or {}
|
||||||
|
print(f"[hybrid DD] 저장: {MATCHING_HYBRID_DD_CALIBRATION_JSON}")
|
||||||
|
print(
|
||||||
|
f"[hybrid DD] best dd_large={best.get('dd_large_pct')} "
|
||||||
|
f"dd_medium={best.get('dd_medium_pct')} "
|
||||||
|
f"train={metrics.get('train_pnl_pct')}% "
|
||||||
|
f"holdout={metrics.get('holdout_pnl_pct')}% "
|
||||||
|
f"full={metrics.get('full_pnl_pct')}%"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user