Phase C dry-run·문서화·DB 증분 저장 및 운영 env 동기화

- 1분봉 다운로드 제외, MONITOR_PERSIST로 05/06 수집 시 coins.db INSERT
- Phase C paper_fires 로그·07 모의 리포트, hybrid 시뮬 산출물·reference 문서 갱신
- .env Phase C(LIVE=0), bootstrap dotenv override=True

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dsyoon
2026-06-01 23:32:47 +09:00
parent 3cbfa40aab
commit b9ee241d14
19 changed files with 877 additions and 333 deletions

7
.env
View File

@@ -25,8 +25,8 @@ DAILY_INTERVAL_MIN=1440
ENTRY_INTERVAL=3 ENTRY_INTERVAL=3
TREND_INTERVAL_1H=60 TREND_INTERVAL_1H=60
TREND_INTERVAL_1D=1440 TREND_INTERVAL_1D=1440
ALL_INTERVALS=1,3,5,10,15,30,60,240,1440 ALL_INTERVALS=3,5,10,15,30,60,240,1440
DOWNLOAD_INTERVALS=1,3,5,10,15,30,60,240,1440 DOWNLOAD_INTERVALS=3,5,10,15,30,60,240,1440
GENERAL_ANALYSIS_INTERVALS=3,5,10,15,30,60,240,1440 GENERAL_ANALYSIS_INTERVALS=3,5,10,15,30,60,240,1440
TIMING_INTERVALS=3,5,10,15 TIMING_INTERVALS=3,5,10,15
TREND_INTERVALS=60,240,1440 TREND_INTERVALS=60,240,1440
@@ -93,7 +93,7 @@ TRADING_FEE_RATE=0.0005
GT_UNLIMITED_CHRONOLOGICAL_DAYS=300 GT_UNLIMITED_CHRONOLOGICAL_DAYS=300
# --- 모니터 --- # --- 모니터 ---
MONITOR_LOOP_SLEEP_SEC=10 MONITOR_LOOP_SLEEP_SEC=180
MONITOR_POOL_WORKERS=12 MONITOR_POOL_WORKERS=12
MONITOR_DEFAULT_INTERVAL=60 MONITOR_DEFAULT_INTERVAL=60
MONITOR_API_RETRIES=3 MONITOR_API_RETRIES=3
@@ -175,6 +175,7 @@ SIM_GO_WF_POSITIVE_RATIO=0.5
SIM_FEE_STRESS_MULT=2.0 SIM_FEE_STRESS_MULT=2.0
MONITOR_ALERT_COOLDOWN_MIN=180 MONITOR_ALERT_COOLDOWN_MIN=180
MONITOR_ALERT_KRW_AMOUNT=100000 MONITOR_ALERT_KRW_AMOUNT=100000
# Phase C: dry-run (~금요일 저녁) → 이후 B-1에서 LIVE_TRADING_ENABLED=1
LIVE_TRADING_ENABLED=0 LIVE_TRADING_ENABLED=0
LIVE_ORDER_KRW=100000 LIVE_ORDER_KRW=100000
LIVE_BUY_PCT_LARGE=1.0 LIVE_BUY_PCT_LARGE=1.0

View File

@@ -8,6 +8,12 @@ COIN_TELEGRAM_CHAT_ID=
SYMBOL=WLD SYMBOL=WLD
CHART_LOOKBACK_DAYS=365 CHART_LOOKBACK_DAYS=365
# 01 다운로드 (1분봉 제외 — 시뮬·MTF는 3분 이상만 사용)
DOWNLOAD_INTERVALS=3,5,10,15,30,60,240,1440
DOWNLOAD_MONTHS=12
# 05/06 루프마다 API 봉을 coins.db에 증분 저장 (01과 동일 append_data)
MONITOR_PERSIST_CANDLES=1
# 02 Ground Truth # 02 Ground Truth
GT_MIN_ORDER_KRW=5000 GT_MIN_ORDER_KRW=5000
GT_BUY_PCT_LARGE_LEG=1.0 GT_BUY_PCT_LARGE_LEG=1.0

View File

@@ -1,17 +1,20 @@
# DeepCoin — WLD MTF 분석·정답·운영 # DeepCoin — WLD MTF 분석·정답·운영
빗썸 KRW-WLD. **1, 3, 5, 10, 15, 30, 60, 240, 1440분** 봉을 적재하고, 빗썸 KRW-WLD. **3, 5, 10, 15, 30, 60, 240, 1440분** 봉을 적재하고 (1분봉은 다운로드 제외, 모니터는 API 최신 1봉만),
Ground Truth·기술적 분석·규칙 매칭·알림·**실거래(선택)**까지 단계별로 관리합니다. Ground Truth·기술적 분석·규칙 매칭·알림·**실거래(선택)**까지 단계별로 관리합니다.
## 남은 작업 순서 ## 남은 작업 순서
| 순서 | 단계 | 실행 | | 순서 | 단계 | 상태 | 실행 |
|------|------|------| |------|------|------|------|
| 1 | 시뮬레이션 | `python scripts/04_simulation_report.py` | | 1 | 시뮬레이션 | 완료 (GO) | `python scripts/04_simulation_report.py` |
| 2 | 문서화 | `docs/reference/SIMULATION.md` 등 | | 2 | 문서화 | 완료 | [docs/reference/SIMULATION.md](docs/reference/SIMULATION.md) 등 |
| 3 | 오픈(실거래) | `python scripts/06_execute_live.py` | | 3 | 오픈(실거래 B-1) | **기동** | `python scripts/06_execute_live.py` (`LIVE=1`) |
| 4 | 1~2주 검증 | 실계좌 기록 | | (C) | dry-run·알림 | 선택 병행 | `05_run_monitor.py` |
| 5 | 지속 거래 | 06 상시 | | 4 | 1~2주 검증 | 대기 | `docs/05_ops/live_verification_*.md` |
| 5 | 지속 거래 | 대기 | 06 상시 + 월간 재시뮬 |
운영 배분: **hybrid primary** (= 시뮬 `sim_causal_hybrid`). [LIVE_TRADING.md](docs/reference/LIVE_TRADING.md)
## 파이프라인 CLI ## 파이프라인 CLI

View File

@@ -8,7 +8,7 @@ import os
from deepcoin.env_loader import load_project_env from deepcoin.env_loader import load_project_env
load_project_env() load_project_env(override=True)
def _getenv(key: str, default: str = "") -> str: def _getenv(key: str, default: str = "") -> str:
@@ -85,11 +85,12 @@ TREND_INTERVAL_1H = _getenv_int("TREND_INTERVAL_1H", "60")
TREND_INTERVAL_1D = _getenv_int("TREND_INTERVAL_1D", "1440") TREND_INTERVAL_1D = _getenv_int("TREND_INTERVAL_1D", "1440")
ALL_INTERVALS: tuple[int, ...] = _parse_int_tuple( ALL_INTERVALS: tuple[int, ...] = _parse_int_tuple(
"ALL_INTERVALS", "1,3,5,10,15,30,60,240,1440" "ALL_INTERVALS", "3,5,10,15,30,60,240,1440"
) )
# 1분봉은 시뮬·MTF 분석 미사용. 실시간 모니터는 API로 최신 1봉만 조회.
DOWNLOAD_INTERVALS: tuple[int, ...] = _parse_int_tuple( DOWNLOAD_INTERVALS: tuple[int, ...] = _parse_int_tuple(
"DOWNLOAD_INTERVALS", "DOWNLOAD_INTERVALS",
",".join(str(x) for x in ALL_INTERVALS), "3,5,10,15,30,60,240,1440",
) )
GENERAL_ANALYSIS_INTERVALS: tuple[int, ...] = _parse_int_tuple( GENERAL_ANALYSIS_INTERVALS: tuple[int, ...] = _parse_int_tuple(
"GENERAL_ANALYSIS_INTERVALS", "3,5,10,15,30,60,240,1440" "GENERAL_ANALYSIS_INTERVALS", "3,5,10,15,30,60,240,1440"
@@ -257,6 +258,12 @@ MONITOR_NORM_WINDOW = _getenv_int("MONITOR_NORM_WINDOW", "20")
MONITOR_TELEGRAM_BATCH_SIZE = _getenv_int("MONITOR_TELEGRAM_BATCH_SIZE", "20") MONITOR_TELEGRAM_BATCH_SIZE = _getenv_int("MONITOR_TELEGRAM_BATCH_SIZE", "20")
# 규칙 알림 참고 금액(매수 시 수량=금액/가격). 매도 시에는 보유 수량 우선. # 규칙 알림 참고 금액(매수 시 수량=금액/가격). 매도 시에는 보유 수량 우선.
MONITOR_ALERT_KRW_AMOUNT = _getenv_int("MONITOR_ALERT_KRW_AMOUNT", "100000") MONITOR_ALERT_KRW_AMOUNT = _getenv_int("MONITOR_ALERT_KRW_AMOUNT", "100000")
# 05/06·live_eval API 수집 시 coins.db 증분 INSERT (01_download와 동일 append_data)
MONITOR_PERSIST_CANDLES = _getenv("MONITOR_PERSIST_CANDLES", "1").strip().lower() in (
"1",
"true",
"yes",
)
# --- general_analysis --- # --- general_analysis ---
GA_COL_PREFIX = _getenv("GA_COL_PREFIX", "ga_") GA_COL_PREFIX = _getenv("GA_COL_PREFIX", "ga_")

View File

@@ -37,7 +37,7 @@ from deepcoin.matching.position_sizing import (
from deepcoin.paths import resolve_ground_truth_file from deepcoin.paths import resolve_ground_truth_file
from deepcoin.ops.alert_message import build_rule_alert_message from deepcoin.ops.alert_message import build_rule_alert_message
from deepcoin.ops.monitor import Monitor from deepcoin.ops.monitor import Monitor
from deepcoin.paths import LIVE_TRADES_LOG from deepcoin.paths import LIVE_TRADES_LOG, PAPER_FIRES_LOG
class LiveTrader(Monitor): class LiveTrader(Monitor):
@@ -87,6 +87,35 @@ class LiveTrader(Monitor):
with LIVE_TRADES_LOG.open("a", encoding="utf-8") as f: with LIVE_TRADES_LOG.open("a", encoding="utf-8") as f:
f.write(json.dumps(record, ensure_ascii=False) + "\n") f.write(json.dumps(record, ensure_ascii=False) + "\n")
def _append_paper_fire(
self,
hit: dict[str, Any],
planned_krw: float,
would_trade: bool,
skip_reason: str = "",
order_log: dict[str, Any] | None = None,
) -> None:
"""
Phase C dry-run: 모든 발화·스킵 사유·모의 금액을 paper_fires.jsonl에 기록.
금요일 `07_phase_c_paper_report.py`로 forward 수익률(참고) 집계.
"""
PAPER_FIRES_LOG.parent.mkdir(parents=True, exist_ok=True)
row = {
"ts": datetime.now().isoformat(timespec="seconds"),
"signal_dt": hit.get("dt"),
"rule_id": hit.get("rule_id"),
"side": hit.get("side"),
"close": float(hit.get("close") or 0),
"planned_krw": round(float(planned_krw), 0),
"would_trade": bool(would_trade),
"skip_reason": skip_reason or "",
"live_enabled": bool(LIVE_TRADING_ENABLED),
"order_message": (order_log or {}).get("message", ""),
}
with PAPER_FIRES_LOG.open("a", encoding="utf-8") as f:
f.write(json.dumps(row, ensure_ascii=False) + "\n")
def _can_trade(self, rule_id: str, planned_krw: float | None = None) -> tuple[bool, str]: def _can_trade(self, rule_id: str, planned_krw: float | None = None) -> tuple[bool, str]:
""" """
일·쿨다운·손실 한도 검사. 일·쿨다운·손실 한도 검사.
@@ -283,6 +312,9 @@ class LiveTrader(Monitor):
if hit["side"] == "buy" and hit["rule_id"] not in self._approved_rules: if hit["side"] == "buy" and hit["rule_id"] not in self._approved_rules:
print(f" [{hit['side']}] {rid} @ {hit['dt']}") print(f" [{hit['side']}] {rid} @ {hit['dt']}")
print(" skip: EV/WF 미통과 규칙") print(" skip: EV/WF 미통과 규칙")
self._append_paper_fire(
hit, 0.0, False, "EV/WF 미통과 규칙"
)
continue continue
planned = ( planned = (
self._resolve_buy_amount_krw(hit) self._resolve_buy_amount_krw(hit)
@@ -293,11 +325,14 @@ class LiveTrader(Monitor):
print(f" [{hit['side']}] {rid} @ {hit['dt']}") print(f" [{hit['side']}] {rid} @ {hit['dt']}")
if not ok: if not ok:
print(f" skip: {reason}") print(f" skip: {reason}")
self._append_paper_fire(hit, planned, False, reason)
continue continue
if hit["side"] == "buy" and planned <= 0: if hit["side"] == "buy" and planned <= 0:
print(" skip: 매수금액 0") print(" skip: 매수금액 0")
self._append_paper_fire(hit, 0.0, False, "매수금액 0")
continue continue
log = self._execute_order(hit) log = self._execute_order(hit)
self._append_paper_fire(hit, planned, True, "", log)
self._append_log(log) self._append_log(log)
print(f" order: {log['message']} ok={log['ok']}") print(f" order: {log['message']} ok={log['ok']}")
msg = build_rule_alert_message(hit, balances) msg = build_rule_alert_message(hit, balances)

View File

@@ -415,18 +415,56 @@ class Monitor(HTS):
bars_per_day = max((24 * 60) // max(interval, 1), 1) bars_per_day = max((24 * 60) // max(interval, 1), 1)
return bars_per_day * lookback_days + DB_ROW_WARMUP_BARS return bars_per_day * lookback_days + DB_ROW_WARMUP_BARS
def get_coin_saved_data( def persist_api_candles_to_db(
self, self,
symbol: str, symbol: str,
interval: int, interval: int,
data: pd.DataFrame, data: pd.DataFrame,
db_path: str = DB_PATH, db_path: str = DB_PATH,
) -> tuple[int, int]:
"""
API로 받은 봉을 coins.db에 증분 INSERT합니다 (01_download.append_data와 동일).
dry-run·05·06·live_eval이 load_frames_from_db 할 때마다 최신 봉이 쌓입니다.
Returns:
(추가 행 수, 스킵 행 수)
"""
if not MONITOR_PERSIST_CANDLES or data is None or data.empty:
return 0, 0
from deepcoin.data.downloader import (
append_data,
get_last_timestamp,
months_for_interval,
prune_before_cutoff,
)
if not isinstance(data.index, pd.DatetimeIndex):
data = data.copy()
data.index = pd.to_datetime(data.index)
data = data.sort_index()
last_ts = get_last_timestamp(symbol, interval, db_path=db_path)
inserted, skipped = append_data(
symbol, interval, data, last_ts=last_ts, db_path=db_path
)
if inserted > 0:
months = months_for_interval(interval, DOWNLOAD_MONTHS)
prune_before_cutoff(symbol, interval, months, db_path=db_path)
return inserted, skipped
def read_candles_from_db(
self,
symbol: str,
interval: int,
db_path: str = DB_PATH,
max_rows: int = DB_READ_LIMIT_DEFAULT, max_rows: int = DB_READ_LIMIT_DEFAULT,
) -> pd.DataFrame: ) -> pd.DataFrame:
""" """
coins.db에서 저장된 봉을 읽고, API로 받은 최신 봉을 DB에 반영합니다. coins.db에서 저장된 봉을 읽니다.
scripts/01_download.py로 미리 적재해 두면 장기 MA 계산에 유리합니다. scripts/01_download.py 또는 persist_api_candles_to_db로 적재된 데이터.
""" """
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
cursor = conn.cursor() cursor = conn.cursor()
@@ -440,33 +478,6 @@ class Monitor(HTS):
f"CREATE INDEX IF NOT EXISTS {table_name}_idx ON {table_name}(CODE, ymdhms)" f"CREATE INDEX IF NOT EXISTS {table_name}_idx ON {table_name}(CODE, ymdhms)"
) )
for i in range(1, len(data)):
ymdhms = data["datetime"].iloc[-i].strftime("%Y-%m-%d %H:%M:%S")
cursor.execute(
f"SELECT 1 FROM {table_name} WHERE CODE = ? AND ymdhms = ?",
(symbol, ymdhms),
)
if not cursor.fetchone():
cursor.execute(
f"INSERT INTO {table_name} "
"(CODE, NAME, ymdhms, ymd, hms, Close, Open, High, Low, Volume) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
(
symbol,
KR_COINS[symbol],
ymdhms,
data["datetime"].iloc[-i].strftime("%Y%m%d"),
data["datetime"].iloc[-i].strftime("%H%M%S"),
data["Close"].iloc[-i],
data["Open"].iloc[-i],
data["High"].iloc[-i],
data["Low"].iloc[-i],
data["Volume"].iloc[-i],
),
)
else:
break
cursor.execute( cursor.execute(
f"SELECT Open, Close, High, Low, Volume, ymdhms AS datetime " f"SELECT Open, Close, High, Low, Volume, ymdhms AS datetime "
f"FROM (SELECT Open, Close, High, Low, Volume, ymdhms " f"FROM (SELECT Open, Close, High, Low, Volume, ymdhms "
@@ -491,26 +502,42 @@ class Monitor(HTS):
df["datetime"] = df.index df["datetime"] = df.index
return df return df
def get_coin_saved_data(
self,
symbol: str,
interval: int,
data: pd.DataFrame,
db_path: str = DB_PATH,
max_rows: int = DB_READ_LIMIT_DEFAULT,
) -> pd.DataFrame:
"""하위 호환: API 봉 저장 후 DB에서 읽기."""
self.persist_api_candles_to_db(symbol, interval, data, db_path=db_path)
return self.read_candles_from_db(
symbol, interval, db_path=db_path, max_rows=max_rows
)
def get_coin_some_data( def get_coin_some_data(
self, symbol: str, interval: int, db_max_rows: int | None = None self, symbol: str, interval: int, db_max_rows: int | None = None
) -> pd.DataFrame: ) -> pd.DataFrame:
""" """
WLD 시세: API 최신 봉 + coins.db 과거 봉 + 1분봉 최신 1개를 합칩니다. WLD 시세: API 최신 봉 + coins.db 과거 봉 + 1분봉 최신 1개를 합칩니다.
DB가 비어 있으면 API·1분봉만 사용합니다. 과거 적재는 scripts/01_download.py 실행. MONITOR_PERSIST_CANDLES=1 이면 API 청크를 즉시 coins.db에 INSERT합니다.
""" """
data = self.get_coin_data(symbol, interval) data = self.get_coin_data(symbol, interval)
if data is None or data.empty: if data is None or data.empty:
return pd.DataFrame() return pd.DataFrame()
self.persist_api_candles_to_db(symbol, interval, data)
data_1 = self.get_coin_data(symbol, interval=1) data_1 = self.get_coin_data(symbol, interval=1)
if data_1 is not None and not data_1.empty: if data_1 is not None and not data_1.empty:
data_1 = data_1.copy() data_1 = data_1.copy()
data_1.at[data_1.index[-1], "Volume"] = data_1["Volume"].iloc[-1] * 60 data_1.at[data_1.index[-1], "Volume"] = data_1["Volume"].iloc[-1] * 60
row_limit = DB_READ_LIMIT_DEFAULT if db_max_rows is None else int(db_max_rows) row_limit = DB_READ_LIMIT_DEFAULT if db_max_rows is None else int(db_max_rows)
saved_data = self.get_coin_saved_data( saved_data = self.read_candles_from_db(
symbol, interval, data, max_rows=row_limit symbol, interval, max_rows=row_limit
) )
parts = [data] parts = [data]
if saved_data is not None and not saved_data.empty: if saved_data is not None and not saved_data.empty:

View File

@@ -53,6 +53,8 @@ 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"
LIVE_TRADES_LOG = OPS_STATE_DIR / "live_trades.jsonl" LIVE_TRADES_LOG = OPS_STATE_DIR / "live_trades.jsonl"
PAPER_FIRES_LOG = OPS_STATE_DIR / "paper_fires.jsonl"
PAPER_WEEKLY_REPORT_JSON = DOCS_OPS / "phase_c_paper_report.json"
CHART_BB_HTML = DOCS_CHARTS / "wld_bb_chart.html" CHART_BB_HTML = DOCS_CHARTS / "wld_bb_chart.html"
CHART_TRUTH_HTML = DOCS_GROUND_TRUTH / "wld_ground_truth_chart.html" CHART_TRUTH_HTML = DOCS_GROUND_TRUTH / "wld_ground_truth_chart.html"

File diff suppressed because one or more lines are too long

View File

@@ -6,75 +6,75 @@
"walk_forward": [ "walk_forward": [
{ {
"month": "2025-06", "month": "2025-06",
"pnl_pct": 10.39, "pnl_pct": 12.78,
"start_asset_krw": 1000000.0, "start_asset_krw": 1000000.0,
"end_asset_krw": 1103874.0 "end_asset_krw": 1127825.0
}, },
{ {
"month": "2025-07", "month": "2025-07",
"pnl_pct": 60.89, "pnl_pct": 60.89,
"start_asset_krw": 1103874.0, "start_asset_krw": 1127825.0,
"end_asset_krw": 1776051.0 "end_asset_krw": 1814584.0
}, },
{ {
"month": "2025-08", "month": "2025-08",
"pnl_pct": 22.9, "pnl_pct": 22.9,
"start_asset_krw": 1776051.0, "start_asset_krw": 1814584.0,
"end_asset_krw": 2182728.0 "end_asset_krw": 2230083.0
}, },
{ {
"month": "2025-09", "month": "2025-09",
"pnl_pct": 57.63, "pnl_pct": 57.63,
"start_asset_krw": 2182728.0, "start_asset_krw": 2230083.0,
"end_asset_krw": 3440648.0 "end_asset_krw": 3515283.0
}, },
{ {
"month": "2025-10", "month": "2025-10",
"pnl_pct": 9.29, "pnl_pct": 9.29,
"start_asset_krw": 3440648.0, "start_asset_krw": 3515283.0,
"end_asset_krw": 3760442.0 "end_asset_krw": 3842010.0
}, },
{ {
"month": "2025-11", "month": "2025-11",
"pnl_pct": 11.24, "pnl_pct": 11.24,
"start_asset_krw": 3760442.0, "start_asset_krw": 3842010.0,
"end_asset_krw": 4183047.0 "end_asset_krw": 4273771.0
}, },
{ {
"month": "2025-12", "month": "2025-12",
"pnl_pct": -0.87, "pnl_pct": -0.87,
"start_asset_krw": 4183047.0, "start_asset_krw": 4273771.0,
"end_asset_krw": 4146498.0 "end_asset_krw": 4236421.0
}, },
{ {
"month": "2026-01", "month": "2026-01",
"pnl_pct": 33.77, "pnl_pct": 33.77,
"start_asset_krw": 4146498.0, "start_asset_krw": 4236421.0,
"end_asset_krw": 5546639.0 "end_asset_krw": 5666889.0
}, },
{ {
"month": "2026-02", "month": "2026-02",
"pnl_pct": 15.61, "pnl_pct": 15.61,
"start_asset_krw": 5546639.0, "start_asset_krw": 5666889.0,
"end_asset_krw": 6412274.0 "end_asset_krw": 6551242.0
}, },
{ {
"month": "2026-03", "month": "2026-03",
"pnl_pct": 9.12, "pnl_pct": 9.12,
"start_asset_krw": 6412274.0, "start_asset_krw": 6551242.0,
"end_asset_krw": 6996857.0 "end_asset_krw": 7148390.0
}, },
{ {
"month": "2026-04", "month": "2026-04",
"pnl_pct": 22.86, "pnl_pct": 22.85,
"start_asset_krw": 6996857.0, "start_asset_krw": 7148390.0,
"end_asset_krw": 8596129.0 "end_asset_krw": 8782116.0
}, },
{ {
"month": "2026-05", "month": "2026-05",
"pnl_pct": 47.58, "pnl_pct": 47.57,
"start_asset_krw": 8596129.0, "start_asset_krw": 8782116.0,
"end_asset_krw": 12686054.0 "end_asset_krw": 12959660.0
} }
], ],
"walk_forward_summary": { "walk_forward_summary": {
@@ -292,7 +292,7 @@
{ {
"name": "hybrid_holdout_pnl", "name": "hybrid_holdout_pnl",
"pass": true, "pass": true,
"value": 62.36 "value": 62.35
}, },
{ {
"name": "hybrid_max_mdd", "name": "hybrid_max_mdd",
@@ -302,19 +302,19 @@
{ {
"name": "hybrid_fee_stress_pnl", "name": "hybrid_fee_stress_pnl",
"pass": true, "pass": true,
"value": 952.77 "value": 975.74
}, },
{ {
"name": "option_c_target_300pct", "name": "option_c_target_300pct",
"pass": true, "pass": true,
"value": 1120.97, "value": 1147.3,
"optional": true "optional": true
} }
] ]
}, },
"go_no_go_option_c_phase2": { "go_no_go_option_c_phase2": {
"go": true, "go": true,
"gt_capture_ratio": 0.2612, "gt_capture_ratio": 0.2674,
"targets": { "targets": {
"phase2_pnl_pct": 1000.0, "phase2_pnl_pct": 1000.0,
"min_gt_capture": 0.23, "min_gt_capture": 0.23,
@@ -328,17 +328,17 @@
{ {
"name": "full_pnl_1000pct", "name": "full_pnl_1000pct",
"pass": true, "pass": true,
"value": 1120.97 "value": 1147.3
}, },
{ {
"name": "gt_capture_23pct", "name": "gt_capture_23pct",
"pass": true, "pass": true,
"value": 0.2612 "value": 0.2674
}, },
{ {
"name": "holdout_pnl_positive", "name": "holdout_pnl_positive",
"pass": true, "pass": true,
"value": 62.36 "value": 62.35
}, },
{ {
"name": "max_mdd", "name": "max_mdd",
@@ -348,13 +348,13 @@
{ {
"name": "fee_stress_ratio", "name": "fee_stress_ratio",
"pass": true, "pass": true,
"value": 952.77, "value": 975.74,
"threshold": 850.0 "threshold": 850.0
}, },
{ {
"name": "slippage_stress_positive", "name": "slippage_stress_positive",
"pass": true, "pass": true,
"value": 27.66, "value": 31.58,
"note": "체결가 슬리피지 반영 후에도 흑자" "note": "체결가 슬리피지 반영 후에도 흑자"
}, },
{ {
@@ -434,19 +434,19 @@
}, },
"sim_causal_gt": { "sim_causal_gt": {
"initial_cash_krw": 1000000, "initial_cash_krw": 1000000,
"final_asset_krw": 1146560.0, "final_asset_krw": 1147944.0,
"pnl_krw": 146560.0, "pnl_krw": 147944.0,
"pnl_pct": 14.66, "pnl_pct": 14.79,
"total_fees_krw": 2101.0, "total_fees_krw": 2025.0,
"cash_krw": 1146560.0, "cash_krw": 1147944.0,
"holding_qty": 0.0, "holding_qty": 0.0,
"holding_value_krw": 0.0, "holding_value_krw": 0.0,
"mark_price": 487.0, "mark_price": 487.0,
"fee_rate": 0.0005, "fee_rate": 0.0005,
"trade_count": 134, "trade_count": 124,
"max_drawdown_pct": 0.96, "max_drawdown_pct": 0.96,
"peak_asset_krw": 1117188.0, "peak_asset_krw": 1118535.0,
"trough_asset_krw": 1106457.0, "trough_asset_krw": 1107793.0,
"leg_count": 17, "leg_count": 17,
"sizing_mode": "causal_gt_leg_engine", "sizing_mode": "causal_gt_leg_engine",
"sizing_note": "인과 GT leg: split_buy + peak_sell, causal tier 복리 (미래 미사용)", "sizing_note": "인과 GT leg: split_buy + peak_sell, causal tier 복리 (미래 미사용)",
@@ -461,46 +461,46 @@
"use_local_trough": false "use_local_trough": false
}, },
"alloc_stats": { "alloc_stats": {
"buy_executed": 101, "buy_executed": 91,
"buy_skipped": 0, "buy_skipped": 0,
"sell_executed": 33, "sell_executed": 33,
"sell_skipped": 0, "sell_skipped": 0,
"buy_total_krw": 2026492.0, "buy_total_krw": 1950020.0,
"large_leg_count": 0, "large_leg_count": 0,
"large_tier_buy_count": 0, "large_tier_buy_count": 0,
"buy_amount_avg_krw": 20064.0, "buy_amount_avg_krw": 21429.0,
"buy_amount_min_krw": 5000, "buy_amount_min_krw": 5000,
"buy_amount_max_krw": 57147.0 "buy_amount_max_krw": 57216.0
} }
}, },
"sim_causal_hybrid": { "sim_causal_hybrid": {
"initial_cash_krw": 1000000, "initial_cash_krw": 1000000,
"final_asset_krw": 12209700.0, "final_asset_krw": 12473032.0,
"pnl_krw": 11209700.0, "pnl_krw": 11473032.0,
"pnl_pct": 1120.97, "pnl_pct": 1147.3,
"total_fees_krw": 695439.0, "total_fees_krw": 710297.0,
"cash_krw": 0.0, "cash_krw": 0.0,
"holding_qty": 25071.252612, "holding_qty": 25611.975633,
"holding_value_krw": 12209700.0, "holding_value_krw": 12473032.0,
"mark_price": 487.0, "mark_price": 487.0,
"fee_rate": 0.0005, "fee_rate": 0.0005,
"trade_count": 5225, "trade_count": 5225,
"max_drawdown_pct": 19.22, "max_drawdown_pct": 19.22,
"peak_asset_krw": 1149472.0, "peak_asset_krw": 1174413.0,
"trough_asset_krw": 928596.0, "trough_asset_krw": 948744.0,
"sizing_mode": "monitor_dd_tier", "sizing_mode": "monitor_dd_tier",
"sizing_note": "monitor buy+sell + drawdown·past-leg tier (미래 미사용)", "sizing_note": "monitor buy+sell + drawdown·past-leg tier (미래 미사용)",
"alloc_stats": { "alloc_stats": {
"buy_executed": 1608, "buy_executed": 1632,
"buy_skipped": 679, "buy_skipped": 655,
"sell_executed": 2938, "sell_executed": 2938,
"sell_skipped": 0, "sell_skipped": 0,
"buy_total_krw": 695591657.0, "buy_total_krw": 710442254.0,
"large_leg_count": 1598, "large_leg_count": 1535,
"large_tier_buy_count": 1598, "large_tier_buy_count": 1535,
"buy_amount_avg_krw": 432582.0, "buy_amount_avg_krw": 435320.0,
"buy_amount_min_krw": 814.0, "buy_amount_min_krw": 828.0,
"buy_amount_max_krw": 8437092.0 "buy_amount_max_krw": 8620153.0
}, },
"input_fires": 5225 "input_fires": 5225
}, },
@@ -510,32 +510,32 @@
}, },
"sim_tier_enhanced": { "sim_tier_enhanced": {
"initial_cash_krw": 1000000, "initial_cash_krw": 1000000,
"final_asset_krw": 485326.0, "final_asset_krw": 487437.0,
"pnl_krw": -514674.0, "pnl_krw": -512563.0,
"pnl_pct": -51.47, "pnl_pct": -51.26,
"total_fees_krw": 111808.0, "total_fees_krw": 107068.0,
"cash_krw": 0.0, "cash_krw": 1.0,
"holding_qty": 996.562036, "holding_qty": 1000.895401,
"holding_value_krw": 485326.0, "holding_value_krw": 487436.0,
"mark_price": 487.0, "mark_price": 487.0,
"fee_rate": 0.0005, "fee_rate": 0.0005,
"trade_count": 5225, "trade_count": 5225,
"max_drawdown_pct": 74.08, "max_drawdown_pct": 74.08,
"peak_asset_krw": 1534299.0, "peak_asset_krw": 1540984.0,
"trough_asset_krw": 397703.0, "trough_asset_krw": 399432.0,
"sizing_mode": "monitor_tier_enhanced", "sizing_mode": "monitor_tier_enhanced",
"sizing_note": "monitor buy+sell + past-leg·drawdown tier + conviction (미래 미사용)", "sizing_note": "monitor buy+sell + past-leg·drawdown tier + conviction (미래 미사용)",
"alloc_stats": { "alloc_stats": {
"buy_executed": 183, "buy_executed": 279,
"buy_skipped": 2104, "buy_skipped": 2008,
"sell_executed": 2938, "sell_executed": 2938,
"sell_skipped": 0, "sell_skipped": 0,
"buy_total_krw": 112251943.0, "buy_total_krw": 107514607.0,
"large_leg_count": 144, "large_leg_count": 136,
"large_tier_buy_count": 144, "large_tier_buy_count": 136,
"buy_amount_avg_krw": 613399.0, "buy_amount_avg_krw": 385357.0,
"buy_amount_min_krw": 5000, "buy_amount_min_krw": 5000,
"buy_amount_max_krw": 1533530.0 "buy_amount_max_krw": 1540213.0
}, },
"input_fires": 5225 "input_fires": 5225
}, },
@@ -548,156 +548,156 @@
"note": "전기간 복리(causal tier) 후 holdout 구간 자산 증감" "note": "전기간 복리(causal tier) 후 holdout 구간 자산 증감"
}, },
"sim_hybrid_holdout": { "sim_hybrid_holdout": {
"initial_asset_krw": 7813594.0, "initial_asset_krw": 7982769.0,
"final_asset_krw": 12686054.0, "final_asset_krw": 12959660.0,
"pnl_krw": 4872460.0, "pnl_krw": 4976891.0,
"pnl_pct": 62.36, "pnl_pct": 62.35,
"trade_count": 845, "trade_count": 845,
"note": "전기간 복리(hybrid DD tier) 후 holdout 구간 자산 증감" "note": "전기간 복리(hybrid DD tier) 후 holdout 구간 자산 증감"
}, },
"sim_hybrid_fee_stress": { "sim_hybrid_fee_stress": {
"initial_cash_krw": 1000000, "initial_cash_krw": 1000000,
"final_asset_krw": 10527724.0, "final_asset_krw": 10757384.0,
"pnl_krw": 9527724.0, "pnl_krw": 9757384.0,
"pnl_pct": 952.77, "pnl_pct": 975.74,
"total_fees_krw": 1261948.0, "total_fees_krw": 1289111.0,
"cash_krw": -0.0, "cash_krw": -0.0,
"holding_qty": 21617.503526, "holding_qty": 22089.084224,
"holding_value_krw": 10527724.0, "holding_value_krw": 10757384.0,
"mark_price": 487.0, "mark_price": 487.0,
"fee_rate": 0.001, "fee_rate": 0.001,
"trade_count": 5225, "trade_count": 5225,
"max_drawdown_pct": 19.37, "max_drawdown_pct": 16.02,
"peak_asset_krw": 4363286.0, "peak_asset_krw": 7104706.0,
"trough_asset_krw": 3518190.0 "trough_asset_krw": 5966238.0
}, },
"sim_hybrid_slippage_stress": { "sim_hybrid_slippage_stress": {
"initial_cash_krw": 1000000, "initial_cash_krw": 1000000,
"final_asset_krw": 1276559.0, "final_asset_krw": 1315755.0,
"pnl_krw": 276559.0, "pnl_krw": 315755.0,
"pnl_pct": 27.66, "pnl_pct": 31.58,
"total_fees_krw": 695439.0, "total_fees_krw": 710297.0,
"cash_krw": 0.0, "cash_krw": -0.0,
"holding_qty": 2621.271075, "holding_qty": 2701.755603,
"holding_value_krw": 1276559.0, "holding_value_krw": 1315755.0,
"mark_price": 487.0, "mark_price": 487.0,
"fee_rate": 0.0005, "fee_rate": 0.0005,
"trade_count": 5225, "trade_count": 5225,
"max_drawdown_pct": 59.37, "max_drawdown_pct": 59.08,
"peak_asset_krw": 3404596.0, "peak_asset_krw": 3483599.0,
"trough_asset_krw": 1383204.0, "trough_asset_krw": 1425625.0,
"slippage_pct": 0.05, "slippage_pct": 0.05,
"sizing_mode": "hybrid_slippage_stress" "sizing_mode": "hybrid_slippage_stress"
}, },
"hybrid_portfolio_walk_forward": [ "hybrid_portfolio_walk_forward": [
{ {
"month": "2025-06", "month": "2025-06",
"pnl_pct": 10.39, "pnl_pct": 12.78,
"start_asset_krw": 1000000.0, "start_asset_krw": 1000000.0,
"end_asset_krw": 1103874.0 "end_asset_krw": 1127825.0
}, },
{ {
"month": "2025-07", "month": "2025-07",
"pnl_pct": 60.89, "pnl_pct": 60.89,
"start_asset_krw": 1103874.0, "start_asset_krw": 1127825.0,
"end_asset_krw": 1776051.0 "end_asset_krw": 1814584.0
}, },
{ {
"month": "2025-08", "month": "2025-08",
"pnl_pct": 22.9, "pnl_pct": 22.9,
"start_asset_krw": 1776051.0, "start_asset_krw": 1814584.0,
"end_asset_krw": 2182728.0 "end_asset_krw": 2230083.0
}, },
{ {
"month": "2025-09", "month": "2025-09",
"pnl_pct": 57.63, "pnl_pct": 57.63,
"start_asset_krw": 2182728.0, "start_asset_krw": 2230083.0,
"end_asset_krw": 3440648.0 "end_asset_krw": 3515283.0
}, },
{ {
"month": "2025-10", "month": "2025-10",
"pnl_pct": 9.29, "pnl_pct": 9.29,
"start_asset_krw": 3440648.0, "start_asset_krw": 3515283.0,
"end_asset_krw": 3760442.0 "end_asset_krw": 3842010.0
}, },
{ {
"month": "2025-11", "month": "2025-11",
"pnl_pct": 11.24, "pnl_pct": 11.24,
"start_asset_krw": 3760442.0, "start_asset_krw": 3842010.0,
"end_asset_krw": 4183047.0 "end_asset_krw": 4273771.0
}, },
{ {
"month": "2025-12", "month": "2025-12",
"pnl_pct": -0.87, "pnl_pct": -0.87,
"start_asset_krw": 4183047.0, "start_asset_krw": 4273771.0,
"end_asset_krw": 4146498.0 "end_asset_krw": 4236421.0
}, },
{ {
"month": "2026-01", "month": "2026-01",
"pnl_pct": 33.77, "pnl_pct": 33.77,
"start_asset_krw": 4146498.0, "start_asset_krw": 4236421.0,
"end_asset_krw": 5546639.0 "end_asset_krw": 5666889.0
}, },
{ {
"month": "2026-02", "month": "2026-02",
"pnl_pct": 15.61, "pnl_pct": 15.61,
"start_asset_krw": 5546639.0, "start_asset_krw": 5666889.0,
"end_asset_krw": 6412274.0 "end_asset_krw": 6551242.0
}, },
{ {
"month": "2026-03", "month": "2026-03",
"pnl_pct": 9.12, "pnl_pct": 9.12,
"start_asset_krw": 6412274.0, "start_asset_krw": 6551242.0,
"end_asset_krw": 6996857.0 "end_asset_krw": 7148390.0
}, },
{ {
"month": "2026-04", "month": "2026-04",
"pnl_pct": 22.86, "pnl_pct": 22.85,
"start_asset_krw": 6996857.0, "start_asset_krw": 7148390.0,
"end_asset_krw": 8596129.0 "end_asset_krw": 8782116.0
}, },
{ {
"month": "2026-05", "month": "2026-05",
"pnl_pct": 47.58, "pnl_pct": 47.57,
"start_asset_krw": 8596129.0, "start_asset_krw": 8782116.0,
"end_asset_krw": 12686054.0 "end_asset_krw": 12959660.0
} }
], ],
"hybrid_portfolio_wf_summary": { "hybrid_portfolio_wf_summary": {
"months": 12, "months": 12,
"positive_months": 11, "positive_months": 11,
"positive_ratio": 0.9167, "positive_ratio": 0.9167,
"mean_pnl_pct": 25.03 "mean_pnl_pct": 25.23
}, },
"primary_sizing": "hybrid", "primary_sizing": "hybrid",
"sim_primary": { "sim_primary": {
"initial_cash_krw": 1000000, "initial_cash_krw": 1000000,
"final_asset_krw": 12209700.0, "final_asset_krw": 12473032.0,
"pnl_krw": 11209700.0, "pnl_krw": 11473032.0,
"pnl_pct": 1120.97, "pnl_pct": 1147.3,
"total_fees_krw": 695439.0, "total_fees_krw": 710297.0,
"cash_krw": 0.0, "cash_krw": 0.0,
"holding_qty": 25071.252612, "holding_qty": 25611.975633,
"holding_value_krw": 12209700.0, "holding_value_krw": 12473032.0,
"mark_price": 487.0, "mark_price": 487.0,
"fee_rate": 0.0005, "fee_rate": 0.0005,
"trade_count": 5225, "trade_count": 5225,
"max_drawdown_pct": 19.22, "max_drawdown_pct": 19.22,
"peak_asset_krw": 1149472.0, "peak_asset_krw": 1174413.0,
"trough_asset_krw": 928596.0, "trough_asset_krw": 948744.0,
"sizing_mode": "primary_hybrid_dd_tier", "sizing_mode": "primary_hybrid_dd_tier",
"sizing_note": "권장: monitor + past-leg·drawdown tier (검증 통과, 미래 미사용)", "sizing_note": "권장: monitor + past-leg·drawdown tier (검증 통과, 미래 미사용)",
"alloc_stats": { "alloc_stats": {
"buy_executed": 1608, "buy_executed": 1632,
"buy_skipped": 679, "buy_skipped": 655,
"sell_executed": 2938, "sell_executed": 2938,
"sell_skipped": 0, "sell_skipped": 0,
"buy_total_krw": 695591657.0, "buy_total_krw": 710442254.0,
"large_leg_count": 1598, "large_leg_count": 1535,
"large_tier_buy_count": 1598, "large_tier_buy_count": 1535,
"buy_amount_avg_krw": 432582.0, "buy_amount_avg_krw": 435320.0,
"buy_amount_min_krw": 814.0, "buy_amount_min_krw": 828.0,
"buy_amount_max_krw": 8437092.0 "buy_amount_max_krw": 8620153.0
}, },
"input_fires": 5225 "input_fires": 5225
}, },
@@ -706,11 +706,11 @@
"sim_sized_pnl_pct": 74.69, "sim_sized_pnl_pct": 74.69,
"gt_model_capture_ratio": 0.0019, "gt_model_capture_ratio": 0.0019,
"causal_gt_capture_ratio": 0.0034, "causal_gt_capture_ratio": 0.0034,
"sim_causal_gt_pnl_pct": 14.66, "sim_causal_gt_pnl_pct": 14.79,
"causal_hybrid_capture_ratio": 0.2612, "causal_hybrid_capture_ratio": 0.2674,
"sim_causal_hybrid_pnl_pct": 1120.97, "sim_causal_hybrid_pnl_pct": 1147.3,
"tier_enhanced_capture_ratio": -0.012, "tier_enhanced_capture_ratio": -0.0119,
"sim_tier_enhanced_pnl_pct": -51.47, "sim_tier_enhanced_pnl_pct": -51.26,
"causal_gt_params": { "causal_gt_params": {
"peak_mode": "local", "peak_mode": "local",
"pivot_order": 8, "pivot_order": 8,

View File

@@ -1,10 +1,7 @@
# Live Phase A — dry-run 검증 # Live Phase A — dry-run 검증
- 일시: 2026-06-01 16:37:13 - 일시: 2026-06-01 23:17:30
- 결과: **PASS** - 결과: **PASS**
- **배포 일정:** Phase C ~6/5 (금) → Phase B-1 ~6/6 (토)
- **체크리스트:** [DEPLOYMENT_CHECKLIST.md](./DEPLOYMENT_CHECKLIST.md)
- **`.env` 권장:** [env.recommended.md](./env.recommended.md)
## Plan (목적) ## Plan (목적)
@@ -21,35 +18,59 @@ python scripts/06_execute_live.py --once
## Check (점검 결과) ## Check (점검 결과)
- GT_SIGNAL_CAUSAL=True · live_sizing=ON · LIVE_TRADING_ENABLED=False - GT_SIGNAL_CAUSAL=True
- LIVE_TRADING_ENABLED=False
- monitor_rules: buy_compound_tight, sell_mtf_cross_all_tf - monitor_rules: buy_compound_tight, sell_mtf_cross_all_tf
- hybrid DD: dd_large=5.0%, dd_medium=2.0% - hybrid DD: {'dd_large_pct': 5.0, 'dd_medium_pct': 2.0}
- 현재 발화: 없음 (신호 대기)
- 06 dry-run: 주문 없음, live=OFF
### hybrid tier 사이징 (시나리오)
| 시나리오 | hybrid | conviction (배포 금지) |
|----------|--------|------------------------|
| 신규·DD 1% | ₩50,000 | ₩50,000 |
| 신규·DD 6% | ₩999,500 | ₩999,500 |
| 복리 500만·large leg | ₩4,997,501 | ₩4,997,501 |
- live_trader는 `enhanced=False` → hybrid primary와 동일 경로
- conviction은 DD 10% 이상·weight 분할 생략 시 시뮬과 크게 괴리 (별도 경로)
### 실거래 한도 (중요)
- 현재 시장 DD 구간에서 hybrid 1회 매수액 ≈ **₩999,500** (현금 100만 기준)
- `LIVE_DAILY_KRW_MAX=300,000`**일 주문 한도 초과로 매수 스킵**
- 파일럿 단계에서는 한도가 의도적으로 시뮬(+1121%)보다 보수적임
## Act (다음 단계) ## Act (다음 단계)
1. **Phase C (월~6/5 금):** `05_run_monitor.py` 상시 · `06_verify_live_dryrun.py` 1일 1회 1. ~~`LIVE_TRADING_ENABLED=1`~~ **적용 완료 (Phase B-1, 2026-06-01)**
2. **금요일 C Go/No-Go** — [DEPLOYMENT_CHECKLIST.md](./DEPLOYMENT_CHECKLIST.md) §4.4 2. `06_execute_live.py` 상시 루프 기동 (180초 주기)
3. **토 6/6~ Phase B-1:** [env.recommended.md](./env.recommended.md) 적용 후 `LIVE_TRADING_ENABLED=1` 3. 1~2주 실계좌 PnL·슬리피지 기록 (본 문서 갱신)
4. 2~4주 후 B-2 승격 검토
## Phase C dry-run (지금 ~ 금요일 저녁)
| 항목 | 값 |
|------|-----|
| `LIVE_TRADING_ENABLED` | **0** (실주문 없음) |
| 실행 | `python scripts/06_execute_live.py` (180초 주기) |
| 발화 로그 | `data/ops/paper_fires.jsonl` |
| 금요일 집계 | `python scripts/07_phase_c_paper_report.py` |
### 수익률 확인 (중요)
| 종류 | 가능? |
|------|-------|
| **실계좌 수익률** | **불가** (주문 없음) |
| **모의 forward %** | **가능** (07 스크립트, 발화 후 N봉 가격 기준 **참고용**) |
| **시뮬 hybrid +1,147%** | 과거 1년 백테스트, 이번 주 결과와 **별개** |
금요일 C Go 후 토요일~ **B-1**: `.env`에서 `LIVE_TRADING_ENABLED=1`, `LIVE_DAILY_KRW_MAX=1000000`
### 일별 기록
| 날짜 | download | verify | 06 dry | buy 발화 | sell 발화 | 메모 |
|------|----------|--------|--------|----------|-----------|------|
| 6/1 | | PASS | | | | |
| 6/2 | | | | | | |
| 6/3 | | | | | | |
| 6/4 | | | | | | |
| 6/5 | | | | | | **C Go → B-1** |
---
## Phase B-1 (금요일 이후 예정)
| 항목 | 값 |
|------|-----|
| `LIVE_TRADING_ENABLED` | 1 |
| `LIVE_DAILY_KRW_MAX` | 1,000,000 |
| `LIVE_DAILY_LOSS_LIMIT_KRW` | 100,000 |
| `06 --once` | live=ON, 발화 없음 |
| 배분 | hybrid primary (`enhanced=False`) |
**선행 조치:** `coin` 환경에 `pip install python-dotenv` (`.env` 미적용 방지). `scripts/_bootstrap.py`·`config.py``load_project_env(override=True)`.
## Kill switch ## Kill switch

View File

@@ -2,7 +2,7 @@
| 경로 | 용도 | Git | | 경로 | 용도 | Git |
|------|------|-----| |------|------|-----|
| [reference/](reference/) | 로드맵·구조·GT 가이드·기법 명세 (`trade_anaysis.html`) | 추적 | | [reference/](reference/) | 로드맵·**시뮬/LIVE/RISK/OPERATIONS**·GT·기법 명세 | 추적 |
| [02_ground_truth/](02_ground_truth/) | 정답 차트 HTML | 추적 | | [02_ground_truth/](02_ground_truth/) | 정답 차트 HTML | 추적 |
| [03_analysis/](03_analysis/) | enrich CSV·GT 스냅샷·점검 HTML | 추적 | | [03_analysis/](03_analysis/) | enrich CSV·GT 스냅샷·점검 HTML | 추적 |
| [04_matching/](04_matching/) | 규칙·시뮬·유사도 산출물 | 추적 | | [04_matching/](04_matching/) | 규칙·시뮬·유사도 산출물 | 추적 |

View File

@@ -1,48 +1,99 @@
# 3단계 — 오픈 (실거래) # 2~3단계 — dry-run·실거래 (hybrid primary)
## 정의 ## 운영 모델 (시뮬과 동일)
**실제 KRW가 빗썸 주문으로 나가는 단계**입니다. 05 텔레그램 알림만으로는 3단계가 아닙니다. dry-run(Phase C)과 실거래(Phase B) 모두 **시뮬 `sim_primary` = `sim_causal_hybrid`** 와 같은 경로이다.
## 선행 조건 | 구분 | 내용 |
|------|------|
| 신호 | `matched_rules.json``monitor_rules` 2개 |
| 매수 규칙 | `buy_compound_tight` |
| 매도 규칙 | `sell_mtf_cross_all_tf` |
| 배분 | hybrid DD tier + past-leg, **`enhanced=False`** |
| 금지 | `sim_tier_enhanced` (conviction), GT oracle 타점 |
1. `python scripts/04_simulation_report.py`**Go/No-Go: GO** 코드: `deepcoin/ops/live_trader.py` (`GT_SIGNAL_CAUSAL=1``plan_buy_amount_krw(..., enhanced=False)`)
2. [DEPLOYMENT_CHECKLIST.md](../05_ops/DEPLOYMENT_CHECKLIST.md) — Phase C 완료 후 B-1
3. [env.recommended.md](../05_ops/env.recommended.md) — Phase별 `.env`
4. 본 문서·`RISK.md`·`OPERATIONS.md` 숙지
## 실행 ## Phase C — dry-run (주문 없음, 3단계 전)
**정의:** 빗썸 **시장가 주문 없음**. 신호·tier·알림·로그만 검증.
| 항목 | 설정 |
|------|------|
| `LIVE_TRADING_ENABLED` | **0** |
| 스크립트 | `05_run_monitor.py`, `06_verify_live_dryrun.py`, `06_execute_live.py --once` |
| 기간 | 배포 체크리스트 기준 ~Phase C 종료(금요일 Go/No-Go) |
```bash ```bash
# Phase A: hybrid tier·한도 점검 (주문 없음) # .env: LIVE_TRADING_ENABLED=0, GT_SIGNAL_CAUSAL=1, SIM_PRIMARY_SIZING=auto
python scripts/06_verify_live_dryrun.py python scripts/01_download.py # 1일 1회
python scripts/06_verify_live_dryrun.py # 설정·tier·규칙 점검
python scripts/05_run_monitor.py # 텔레그램 알림 (상시)
python scripts/06_execute_live.py --once # dry_run 로그만 (선택)
```
# 반드시 LIVE_TRADING_ENABLED=1 일 때만 주문 - 06은 발화 시 `dry_run (LIVE_TRADING_ENABLED=0)` 만 기록, **API 매수·매도 호출 없음**
python scripts/06_execute_live.py --once # 1회 점검 - 잔고 조회는 알림·tier 계산용으로 API를 쓸 수 있음
python scripts/06_execute_live.py # 상시 (알림+주문)
상세: [DEPLOYMENT_CHECKLIST.md](../05_ops/DEPLOYMENT_CHECKLIST.md) §4 · [env.recommended.md](../05_ops/env.recommended.md) Phase C
## Phase B — 실거래 (3단계 오픈)
**정의:** 실제 KRW가 빗썸 주문으로 나간다. 05 알림만으로는 3단계가 아니다.
### 선행 조건
1. [SIMULATION.md](SIMULATION.md) — 시뮬 **GO** (완료)
2. Phase C **GO** (5일 모니터·verify·알림 안정)
3. [env.recommended.md](../05_ops/env.recommended.md) Phase B-1 `.env`
4. [RISK.md](RISK.md), [OPERATIONS.md](OPERATIONS.md) 숙지
### 실행
```bash
python scripts/06_verify_live_dryrun.py # B-1 당일 재확인
# LIVE_TRADING_ENABLED=1 확인 후
python scripts/06_execute_live.py --once
python scripts/06_execute_live.py # 상시
``` ```
## 환경 변수 ## 환경 변수
| 변수 | 기본 | 설명 | | 변수 | Phase C | Phase B-1 (예) | 설명 |
|------|------|------| |------|---------|----------------|------|
| `LIVE_TRADING_ENABLED` | 0 | **1**일 때만 실주문 | | `LIVE_TRADING_ENABLED` | 0 | 1 | 1일 때만 실주문 |
| `LIVE_ORDER_KRW` | 100000 | 1회 주문 금액(원) | | `GT_SIGNAL_CAUSAL` | 1 | 1 | hybrid sizing |
| `LIVE_DAILY_KRW_MAX` | 300000 | 일일 총 주문 한도 | | `SIM_PRIMARY_SIZING` | auto | auto | primary=hybrid |
| `LIVE_COOLDOWN_MIN` | 180 | 동일 규칙 재주문 최소 간격(분) | | `LIVE_ORDER_KRW` | 100000 | 100000 | 매도·비-hybrid fallback 참고 |
| `LIVE_MAX_TRADES_PER_DAY` | 10 | 일일 최대 체결 시도 | | `LIVE_DAILY_KRW_MAX` | 300000 | 1,000,000 | **sim 대비 체결 상한** |
| `LIVE_DAILY_LOSS_LIMIT_KRW` | 50000 | 일 손실 한도(추가 주문 중단) | | `LIVE_COOLDOWN_MIN` | 180 | 180 | 규칙별 재주문 간격 |
| `LIVE_MAX_TRADES_PER_DAY` | 10 | 10 | 일 최대 시도 |
| `LIVE_DAILY_LOSS_LIMIT_KRW` | 50000 | 50000 | 일 손실 한도 |
| `MONITOR_LOOP_SLEEP_SEC` | 180 | 180 | 05/06 루프 주기 |
| `MONITOR_ALERT_COOLDOWN_MIN` | 180 | 180 | 텔레그램 중복 방지 |
## 주문 규칙 Phase별 전체 블록: [env.recommended.md](../05_ops/env.recommended.md)
- `matched_rules.json`**`monitor_rules`** 만 사용 (매수·매도 각 1개) ## 주문·배분 동작
- 매수: 시장가 매수 (`buyCoinMarket`)
- 매도: 보유 수량 기준 시장가 매도 (`sellCoinMarket`) - **매수:** EV/WF 통과 규칙만 hybrid tier 원화 산출 → `_can_trade` (일한도·쿨다운) → 시장가 매수
- **매도:** 보유 WLD 전량 기준 시장가 매도 (`LIVE_ORDER_KRW`는 매도 경로에서 수량 우선)
- **스킵:** 현금 부족, 일한도, hybrid planned > `LIVE_DAILY_KRW_MAX`, 미승인 규칙
## 로그 ## 로그
- `data/ops/live_trades.jsonl` — 주문 시도·결과 | 경로 | 내용 |
|------|------|
| `data/ops/live_trades.jsonl` | 06 주문·dry-run 시도 (JSONL) |
| `docs/05_ops/live_verification_*.md` | Phase C/B 일별 기록 |
## 4단계 연결 ## 4단계 연결
오픈 후 **1~2주** 실계좌 PnL·슬리피지·장애를 `docs/05_ops/live_verification_*.md`에 기록합니다. 오픈 후 **1~2주** 실계좌 PnL·슬리피지·장애를 verification 문서에 기록 → B-2 한도 검토.
## 관련 문서
- [SIMULATION.md](SIMULATION.md) — Go/No-Go·`sim_primary`
- [OPERATIONS.md](OPERATIONS.md) — 일상 루틴·장애
- [RISK.md](RISK.md) — Kill switch·한도

View File

@@ -1,42 +1,143 @@
# 운영 가이드 # 운영 가이드
## 로드맵과 현재 위치 (2026-06-01)
| 순서 | 단계 | 상태 |
|------|------|------|
| 1 | 시뮬레이션 | **완료** (GO) — [SIMULATION.md](SIMULATION.md) |
| 2 | 문서화 | **본 문서군** — SIMULATION / LIVE / RISK / OPERATIONS |
| 3 | 오픈 (실거래) | Phase C 후 B-1 — [LIVE_TRADING.md](LIVE_TRADING.md) |
| 4 | 실계좌 검증 | 1~2주 |
| 5 | 지속 운영 | 06 상시 + 월간 재시뮬 |
배포 모델: **hybrid primary** (= 시뮬 `sim_causal_hybrid`). 상세: [LIVE_TRADING.md](LIVE_TRADING.md)
---
## 단계별 스크립트 ## 단계별 스크립트
| 단계 | 스크립트 | | 단계 | 스크립트 | 산출·역할 |
|------|----------| |------|----------|-----------|
| 데이터 | `01_download.py` | | 01 | `01_download.py` | `coins.db` (3~1440분, 1분 제외) |
| GT | `02_ground_truth.py` | | (자동) | 05/06·`load_frames_from_db` | API 수집 시 `MONITOR_PERSIST_CANDLES=1`이면 **즉시 DB INSERT** |
| 분석 | `03_analyze_enrich.py`, `03_analyze_trades.py` | | 02 | `02_ground_truth.py` | GT JSON·차트 |
| 매칭 | `04_match_rules.py` | | 03 | `03_analyze_enrich.py` | `docs/03_analysis/latest/` |
| 시뮬 | `04_simulation_report.py` | | 03b | `03_analyze_trades.py` | GT MTF 스냅샷 CSV |
| 알림 | `05_run_monitor.py` | | 03c | `03_gt_mtf_profile.py` | GT 프로필 (04 입력) |
| 실거래 | `06_execute_live.py` | | 04 | `04_match_rules.py` | `matched_rules.json` |
| 04 시뮬 | `04_simulation_report.py` | Go/No-Go 리포트 |
| 05 | `05_run_monitor.py` | 알림 (주문 없음) |
| 06 | `06_execute_live.py` | 알림+주문 (LIVE=1) |
| 점검 | `06_verify_live_dryrun.py` | hybrid·한도·규칙 PASS |
| 환경 | `verify_env.py` | `.env`·경로 검증 |
## 일상 운영 (5단계 이후) 구조: [STRUCTURE.md](STRUCTURE.md)
1. `01_download.py` — 일 1회 권장 ---
2. `06_execute_live.py` — 상시 (`LIVE_TRADING_ENABLED=1`)
3. 주간 — `04_simulation_report.py`로 EV·Go 재확인 ## Phase C — 지금~금요일 (dry-run, 주문 없음)
### `.env` 핵심
```env
LIVE_TRADING_ENABLED=0
GT_SIGNAL_CAUSAL=1
SIM_PRIMARY_SIZING=auto
MONITOR_LOOP_SLEEP_SEC=180
MONITOR_ALERT_COOLDOWN_MIN=180
```
전체: [env.recommended.md](../05_ops/env.recommended.md)
### 매일 루틴
| 순서 | 명령 | 빈도 |
|------|------|------|
| 1 | `python scripts/01_download.py` | 1일 1회 |
| 2 | `python scripts/06_verify_live_dryrun.py` | 1일 1회 |
| 3 | `python scripts/06_execute_live.py` | 상시 (`LIVE=0`, 발화→`paper_fires.jsonl`) |
| 4 | 금요일 | `python scripts/07_phase_c_paper_report.py` (모의 forward % 참고) |
실계좌 수익률은 dry-run에서 나오지 않음. B-1 전 C Go 판정 후 `LIVE_TRADING_ENABLED=1`.
### 일별 기록
- 파일: `docs/05_ops/live_verification_20260601.md`
- 금요일: [DEPLOYMENT_CHECKLIST.md](../05_ops/DEPLOYMENT_CHECKLIST.md) §4.4 C Go/No-Go
---
## Phase B-1 — 실거래 시작 (C GO 이후)
1. [env.recommended.md](../05_ops/env.recommended.md) Phase B-1 블록 적용
2. `LIVE_TRADING_ENABLED=1` 확인
3. `06_verify_live_dryrun.py` → PASS
4. `06_execute_live.py --once``live_trades.jsonl` 확인
5. `06_execute_live.py` 상시
체크리스트: [DEPLOYMENT_CHECKLIST.md](../05_ops/DEPLOYMENT_CHECKLIST.md) §5
---
## Phase B-2 이후 (검증 통과 시)
- `LIVE_DAILY_KRW_MAX` 등 한도 상향 (리스크 문서·본인 판단)
- 주간 `04_simulation_report.py`로 EV·hybrid Go 재확인
---
## 텔레그램 ## 텔레그램
- 05/06: 규칙 발화·**체결 결과** (06) | 스크립트 | 내용 |
- `MONITOR_ALERT_COOLDOWN_MIN` / `LIVE_COOLDOWN_MIN` 으로 중복 완화 |----------|------|
| 05 | 규칙 발화·MTF 요약 (주문 없음) |
| 06 | 발화 + 체결/dry-run 결과 (`LIVE=1` 시 체결) |
중복 완화: `MONITOR_ALERT_COOLDOWN_MIN`, `LIVE_COOLDOWN_MIN`
---
## 장애 대응 ## 장애 대응
| 증상 | 조치 | | 증상 | 조치 |
|------|------| |------|------|
| 주문 실패 | 로그·빗썸 API 키·잔고 확인, `LIVE_TRADING_ENABLED=0` | | 주문 실패 | `live_trades.jsonl`, API 키·잔고 `LIVE_TRADING_ENABLED=0` |
| 알림만 오고 주문 없음 | `LIVE_TRADING_ENABLED` 확인 | | 알림만, 주문 없음 | C: 정상. B: `LIVE_TRADING_ENABLED` 확인 |
| 과다 알림 | 쿨다운 증가·`monitor_rules` 축소 | | verify FAIL | `.env`, `matched_rules` 2개, `GT_SIGNAL_CAUSAL=1` |
| hybrid 금액 0 | 현금·EV/WF·tier 스킵 로그 확인 |
| 과다 알림 | 쿨다운 증가 |
| 시뮬과 수익 괴리 | 일한도·슬리피지 — [RISK.md](RISK.md) |
## 오픈 체크리스트 (3단계 당일) ---
- [ ] [DEPLOYMENT_CHECKLIST.md](../05_ops/DEPLOYMENT_CHECKLIST.md) Phase C Go ## 데이터·산출물 경로
- [ ] [env.recommended.md](../05_ops/env.recommended.md) Phase B-1 적용
- [ ] 시뮬 Go/No-Go **GO** | 경로 | 용도 |
- [ ] `.env` 한도 확인 |------|------|
- [ ] `LIVE_TRADING_ENABLED=1` 의도적 설정 | `data/coins.db` 또는 루트 `coins.db` | OHLCV |
- [ ] `--once` 1회 테스트 | `data/ground_truth/ground_truth_trades.json` | GT |
- [ ] `live_trades.jsonl` 기록 확인 | `docs/04_matching/matched_rules.json` | 운영 규칙 |
| `docs/04_matching/simulation_report.html` | 시뮬 리포트 |
| `data/ops/live_trades.jsonl` | 06 로그 |
---
## 오픈 당일 체크리스트 (3단계)
- [ ] Phase C **GO**
- [ ] 시뮬·hybrid Go/No-Go **GO**
- [ ] Phase B-1 `.env`
- [ ] `06_verify_live_dryrun.py` PASS
- [ ] `LIVE_TRADING_ENABLED=1` 의도 확인
- [ ] `--once``live_trades.jsonl`
- [ ] [RISK.md](RISK.md) Kill switch 숙지
---
## 관련 문서
- [ROADMAP.md](ROADMAP.md)
- [SIMULATION.md](SIMULATION.md)
- [LIVE_TRADING.md](LIVE_TRADING.md)
- [RISK.md](RISK.md)
- [DEPLOYMENT_CHECKLIST.md](../05_ops/DEPLOYMENT_CHECKLIST.md)

View File

@@ -1,25 +1,62 @@
# 리스크 — 실거래 # 리스크 — dry-run·실거래
## 원칙 ## 모델 리스크 (배포 경로)
- 소액 파일럿만 허용 (`LIVE_ORDER_KRW`, `LIVE_DAILY_KRW_MAX`) | 경로 | 리스크 수준 | 조치 |
- 손실 한도 초과 시 **당일 추가 주문 중단** |------|-------------|------|
- API·네트워크 오류 시 주문 중단·로그 기록 | **hybrid primary** (`sim_causal_hybrid`) | 배포 허용 | Phase C 검증 후 B-1 소액 |
| GT oracle (+4,291%) | **운영 금지** | 벤치마크만 참고 (미래 정보) |
| conviction (`sim_tier_enhanced`) | **코드·env 모두 금지** | `enhanced=False` 고정 |
| sim 고수익 ≠ 실현 | **구조적 갭** | `LIVE_DAILY_KRW_MAX`·슬리피지·부분 체결 |
시뮬 Option C **GO**는 과거 데이터·가정 하의 결과이며, 실계좌 수익을 보장하지 않는다.
## 자금·한도
### 원칙
- 파일럿만 허용: Phase B-1은 **소액** (`LIVE_ORDER_KRW`, `LIVE_DAILY_KRW_MAX` 보수적)
- hybrid 1회 planned 매수가 `LIVE_DAILY_KRW_MAX`를 넘으면 **주문 스킵** (시뮬은 제한 없음)
- 일 손실 한도 초과 시 **당일 추가 주문 중단**
### Phase별 한도 예 (`.env` 조정 필수)
| Phase | `LIVE_TRADING_ENABLED` | 일한도 예 | 비고 |
|-------|------------------------|-----------|------|
| C (dry-run) | 0 | 30만 (dry-run 참고) | 주문 없음 |
| B-1 | 1 | 100만 | sim 대비 보수 |
| B-2 | 1 | 500만+ | B-1 검증 후만 |
본인 자금·위험 성향에 맞게 **반드시** 낮춰 시작한다.
## 시장·기술 리스크
| 리스크 | 영향 | 완화 |
|--------|------|------|
| API 장애·레이트리밋 | 시세·주문 실패 | 재시도·`LIVE_TRADING_ENABLED=0` |
| 슬리피지·호가 | sim 대비 수익 하락 | B-1 소액·verification 기록 |
| DB·봉 지연 | 규칙 오판 | `01_download` 일 1회 |
| 과다 발화 | 수수료·알림 피로 | 쿨다운·규칙 수(`MATCH_MONITOR_MAX_PER_SIDE`) |
| 단일 종목(WLD) | 집중 리스크 | 포지션·일한도 상한 |
## Kill switch ## Kill switch
| 방법 | 동작 | | 방법 | 동작 |
|------|------| |------|------|
| `.env` | `LIVE_TRADING_ENABLED=0` 설정 후 프로세스 재시작 | | `.env` | `LIVE_TRADING_ENABLED=0` 05/06 프로세스 재시작 |
| 프로세스 | `06_execute_live.py` 중지 | | 프로세스 | `06_execute_live.py` 중지 (05만 남기면 알림만) |
| 빗썸 | 앱/웹에서 수동 청산 | | 빗썸 | 앱/웹 수동 청산 |
## 한도 (기본값 예시) 긴급 시 **주문 프로세스 중지 → LIVE_TRADING_ENABLED=0** 순서를 권장한다.
- 1회 10만 원 · 일 30만 원 · 일 손실 5만 원 초과 시 중단 ## 검증·재평가
운영 전 본인 자금에 맞게 **반드시** 조정하세요. | 주기 | 작업 |
|------|------|
| Phase C (~5일) | 발화·알림·verify PASS — [DEPLOYMENT_CHECKLIST](../05_ops/DEPLOYMENT_CHECKLIST.md) |
| B-1 (1~2주) | `live_verification_*.md` PnL·MDD·슬리피지 |
| 월 1회 | `04_simulation_report.py` Go 재확인 |
## 면책 ## 면책
실거래 손익은 전적으로 운영자 책임입니다. 본 저장소는 투자 자문이 아니다. 실거래·dry-run 관찰 손익은 전적으로 운영자 책임다. 본 저장소는 투자 자문이 아니다.

View File

@@ -4,21 +4,33 @@
| 단계 | 내용 | 실행 | | 단계 | 내용 | 실행 |
|------|------|------| |------|------|------|
| 01~03c | DB, GT, enrich, GT MTF 스냅샷, **전 TF 프로필(매수/매도 대조)** | `01`~`03_gt_mtf_profile.py` | | 01~03c | DB, GT, enrich, GT MTF 스냅샷, 전 TF 프로필 | `01`~`03_gt_mtf_profile.py` |
| 04 | GT 프로필 + leg_gt EV + holdout | `04_match_rules.py` | | 04 | GT 프로필 + leg_gt EV + holdout | `04_match_rules.py` |
| 05 | 텔레그램 알림 (주문 없음) | `05_run_monitor.py` | | 05 (기능) | 텔레그램 알림 스크립트 | `05_run_monitor.py` |
| **1** | **시뮬레이션** walk-forward·Go/No-Go | `04_simulation_report.py`**GO** |
| **2** | **문서화** | [SIMULATION.md](SIMULATION.md), [LIVE_TRADING.md](LIVE_TRADING.md), [RISK.md](RISK.md), [OPERATIONS.md](OPERATIONS.md) |
## 남은 작업 (합의 순서) ## 남은 작업 (합의 순서)
| 순서 | 단계 | 내용 | 실행 | | 순서 | 단계 | 내용 | 실행 |
|------|------|------|------| |------|------|------|------|
| **1** | 시뮬레이션 | walk-forward·민감도·Go/No-Go | `04_simulation_report.py` | | **3** | 오픈 (B-1) | 실거래 소액 · hybrid | `06_execute_live.py`**기동** (`LIVE_TRADING_ENABLED=1`) |
| **2** | 문서화 | SIMULATION, LIVE, RISK, OPERATIONS | `docs/reference/` | | **(병행)** | Phase C 알림 | 선택 | `05_run_monitor.py` |
| **3** | 오픈 | **실거래** (소액) | `06_execute_live.py` |
| **4** | 검증 | 실계좌 1~2주 | `docs/05_ops/live_verification_*.md` | | **4** | 검증 | 실계좌 1~2주 | `docs/05_ops/live_verification_*.md` |
| **5** | 지속 | 실거래 유지·월간 재시뮬 | 06 상시 | | **5** | 지속 | 실거래·월간 재시뮬 | 06 상시 |
가이드: [SIMULATION.md](SIMULATION.md) · [LIVE_TRADING.md](LIVE_TRADING.md) 운영 모델: **sim_primary = hybrid** (`sim_causal_hybrid`). [LIVE_TRADING.md](LIVE_TRADING.md)
## 가이드 맵
| 문서 | 용도 |
|------|------|
| [SIMULATION.md](SIMULATION.md) | 1단계 결과·Go/No-Go·portfolio_compare |
| [LIVE_TRADING.md](LIVE_TRADING.md) | Phase C dry-run · Phase B live |
| [RISK.md](RISK.md) | 한도·Kill switch·sim vs 실현 갭 |
| [OPERATIONS.md](OPERATIONS.md) | 일상 루틴·장애·체크리스트 |
| [DEPLOYMENT_CHECKLIST.md](../05_ops/DEPLOYMENT_CHECKLIST.md) | C→B 일정·Go 기준 |
| [env.recommended.md](../05_ops/env.recommended.md) | Phase별 `.env` |
## 디렉터리 ## 디렉터리

View File

@@ -1,8 +1,11 @@
# 1단계 — 시뮬레이션 # 1단계 — 시뮬레이션
**상태 (2026-06-01):** `04_simulation_report.py` 실행 완료 · 규칙·hybrid·Option C 2차 **GO**
## 목적 ## 목적
`monitor_rules`과적합이 아닌지 검증하고, **Ground Truth와 동일한 자본 배분 원칙**으로 holdout 체결 수익을 비교합니다. 1. `monitor_rules`holdout·walk-forward·수수료 스트레스를 통과하는지 검증한다.
2. 운영에 쓸 **배분 경로(primary)** 를 정한다. (실전 = **hybrid**, GT oracle 아님)
## 실행 ## 실행
@@ -15,40 +18,95 @@ python scripts/04_simulation_report.py
| 파일 | 내용 | | 파일 | 내용 |
|------|------| |------|------|
| `docs/04_matching/simulation_report.json` | Go/No-Go, `portfolio_compare`, `gt_model` | | `docs/04_matching/simulation_report.json` | Go/No-Go, `portfolio_compare`, walk-forward |
| `docs/04_matching/simulation_report.html` | 카드 3줄: **GT · 시뮬(총자산%) · 시뮬(고정₩/회)** | | `docs/04_matching/simulation_report.html` | GT·시뮬 경로별 카드·차트 |
## 포트폴리오 비교 (`portfolio_compare`) ## 배포 모델 (primary)
| 경로 | `portfolio_compare` 키 | 전기간 PnL (최근 실행) | 운영 |
|------|------------------------|------------------------|------|
| GT oracle (사후 ZigZag) | `ground_truth_chrono` | +4,291% | **미사용** (미래 허용 벤치마크) |
| **권장 primary** | `sim_primary` = `sim_causal_hybrid` | **+1,147%** | **dry-run·live 배분** |
| causal tier only | `sim_sized` | +75% | 미사용 |
| 인과 GT leg 엔진 | `sim_causal_gt` | +15% | 미사용 |
| conviction tier | `sim_tier_enhanced` | -51% | **금지** |
| 고정 금액 baseline | `sim_fixed_order` | -94% | 비교용 |
- `primary_sizing`: **hybrid** (`go_no_go_hybrid.primary_sizing`)
- hybrid: monitor 발화 + **DD tier + past-leg tier**, `enhanced=False`
- 코드: `deepcoin/ground_truth/causal_gt_hybrid.py`, `deepcoin/matching/simulation.py`
## Go/No-Go (최근 실행 요약)
### 규칙 (`go_no_go`)
| rule_id | holdout EV | WF+ 비율 | fee 2× EV | 결과 |
|---------|------------|----------|-----------|------|
| `buy_compound_tight` | 5.66 | 0.75 | 4.99 | PASS |
| `sell_mtf_cross_all_tf` | 7.13 | 1.00 | 7.12 | PASS |
**GO**
### hybrid primary (`go_no_go_hybrid`)
| 검사 | 값 | 결과 |
|------|-----|------|
| monitor_rules_go | - | PASS |
| hybrid_holdout_pnl | +62.35% | PASS |
| hybrid_max_mdd | 19.22% | PASS |
| hybrid_fee_stress_pnl | +975.74% | PASS |
| option_c_target_300pct (optional) | +1,147% | PASS |
**GO** · `primary_sizing=hybrid`
### Option C 2차 (`go_no_go_option_c_phase2`)
- 전기간 +1,000% 목표, GT capture ≥23%, WF 양수 월 비율, 슬리피지 스트레스 등 → **GO**
재실행 후 수치는 `simulation_report.json`을 기준으로 한다.
## 포트폴리오 비교 (`portfolio_compare`) — 읽는 법
| 키 | 설명 | | 키 | 설명 |
|----|------| |----|------|
| `ground_truth_chrono` | GT 타점 + `amount_krw` 시각순 체결 | | `ground_truth_chrono` | GT 타점·`amount_krw` 시각순 체결 (상한 벤치마크) |
| `sim_sized` | holdout 발화 + **총자산×비중×EV/WF·leg상위** (`position_sizing`) | | `sim_primary` / `sim_causal_hybrid` | **운영 배분과 동일** (monitor + hybrid tier) |
| `sim_fixed_order` | 동일 발화 + **고정 `LIVE_ORDER_KRW`/회** (baseline) | | `sim_sized` | EV/WF·leg 가중 복리 (구 경로) |
| `sim_hybrid_holdout` | hybrid 전기간 복리 후 **holdout 구간** 자산 증감 |
## 시뮬 매수 배분 (GT와 동일 원칙) | `sim_hybrid_fee_stress` | 수수료 스트레스 hybrid |
| `hybrid_dd_params` | `dd_large_pct`, `dd_medium_pct` (캘리브 JSON과 동기) |
- **통과 규칙만** 대형: holdout EV·PF, walk-forward, 수수료 2× 스트레스 (`load_ev_wf_approved_rule_ids`)
- **leg 상위** `GT_LARGE_LEG_TOP_PCT` + 근접 GT leg 매칭 → `LIVE_BUY_PCT_LARGE`
- 그 외 → `LIVE_BUY_PCT_SMALL`
- 일한도·일최대거래: `select_capped_fires` (동적 planned 원화로 `LIVE_DAILY_KRW_MAX` 적용)
## 검증 항목 ## 검증 항목
| 항목 | 설명 | | 항목 | 설명 |
|------|------| |------|------|
| Holdout | EV≥0, PF≥1 | | Holdout | 규칙별 EV≥0, PF≥1 |
| Walk-forward | 양수 월 비율 ≥ `SIM_GO_WF_POSITIVE_RATIO` | | Walk-forward | 월별 EV·`SIM_GO_WF_POSITIVE_RATIO` |
| 수수료 스트레스 | 수수료 2× 후 EV≥0 | | 수수료 스트레스 | `SIM_FEE_STRESS_MULT` (기본 2×) |
| 실거래 한도 | 동적 매수액 기준 일한도 시뮬 | | hybrid | holdout PnL, MDD, fee stress, (선택) +300% |
| 슬리피지 | Option C 2차 — 체결가 불리 가정 후 흑자 여부 |
## Go/No-Go ## 시뮬 vs 실운영 (기대 갭)
- **GO**: monitor_rules 전 규칙 checks 통과 시뮬 hybrid는 **일한도 없이** 복리·전액 배분을 가정한다. 실거래는 `LIVE_DAILY_KRW_MAX` 등으로 체결이 잘리므로 **수익률이 sim_primary와 같지 않을 수 있다**. (배포 체크리스트 D6: 실현=sim 미달)
- **NO-GO**: 04 재선별 후 재실행
## 환경 변수 ## 환경 변수
- `SIM_GO_*`, `SIM_WALK_FORWARD_MIN_MONTHS`, `SIM_FEE_STRESS_MULT` | 변수 | 용도 |
- `LIVE_ORDER_KRW`, `LIVE_DAILY_KRW_MAX` (고정 baseline·한도) |------|------|
- `LIVE_BUY_PCT_LARGE`, `LIVE_BUY_PCT_SMALL` (시뮬·실거래 비율 매수) | `SIM_GO_*`, `SIM_FEE_STRESS_MULT`, `SIM_WALK_FORWARD_MIN_MONTHS` | 규칙 Go/No-Go |
| `SIM_PRIMARY_SIZING` | `auto` \| `hybrid` \| `causal_tier` (권장: **auto** → hybrid) |
| `GT_SIGNAL_CAUSAL` | 1 — hybrid live sizing 활성 |
| `CAUSAL_GT_DD_LARGE_PCT`, `CAUSAL_GT_DD_MEDIUM_PCT` | hybrid tier (캘리브와 동기) |
| `GT_BUY_PCT_*`, `GT_LARGE_LEG_TOP_PCT` | tier 비중 |
## NO-GO 시
1. `04_match_rules.py` 재실행 (프로필·선별)
2. `04_simulation_report.py` 재실행
3. `go_no_go` / `go_no_go_hybrid` checks 확인
## 다음 단계
- **2단계 문서화:** 본 문서 + [LIVE_TRADING.md](LIVE_TRADING.md), [RISK.md](RISK.md), [OPERATIONS.md](OPERATIONS.md)
- **3단계 이전:** [DEPLOYMENT_CHECKLIST.md](../05_ops/DEPLOYMENT_CHECKLIST.md) Phase C (dry-run)

View File

@@ -0,0 +1,165 @@
#!/usr/bin/env python3
"""
Phase C dry-run 종료 후 모의 수익률(참고) 집계.
- 입력: data/ops/paper_fires.jsonl (06 dry-run 발화 로그)
- 출력: docs/05_ops/phase_c_paper_report.json + 콘솔 요약
주의: 실계좌 수익이 아님. 발화가 N봉 후 가격으로 계산한 forward % 합산(참고).
hybrid 복리 PnL은 simulation_report.html 과 다릅니다.
"""
from __future__ import annotations
import json
import runpy
from datetime import datetime
from pathlib import Path
import numpy as np
import pandas as pd
runpy.run_path(str(Path(__file__).resolve().parent / "_bootstrap.py"))
from config import ( # noqa: E402
MATCH_FORWARD_BARS,
MATCH_PRIMARY_INTERVAL,
SYMBOL,
TRADING_FEE_RATE,
)
from deepcoin.matching.label_outcomes import _forward_ret_vectorized # noqa: E402
from deepcoin.ops.monitor import Monitor # noqa: E402
from deepcoin.paths import PAPER_FIRES_LOG, PAPER_WEEKLY_REPORT_JSON # noqa: E402
_FEE_PCT = TRADING_FEE_RATE * 2 * 100
def load_paper_fires(path: Path) -> pd.DataFrame:
"""paper_fires.jsonl → DataFrame."""
if not path.is_file():
return pd.DataFrame()
rows = []
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if line:
rows.append(json.loads(line))
if not rows:
return pd.DataFrame()
return pd.DataFrame(rows)
def attach_forward_returns(fires: pd.DataFrame, close_df: pd.DataFrame) -> pd.DataFrame:
"""
would_trade=True 발화에 MATCH_FORWARD_BARS 기준 forward 수익률(%) 부여.
Args:
fires: paper 발화.
close_df: 3분 종가 (datetime index).
Returns:
forward_ret_pct 컬럼 추가.
"""
if fires.empty or close_df.empty:
fires["forward_ret_pct"] = np.nan
return fires
close_df = close_df.sort_index()
if not isinstance(close_df.index, pd.DatetimeIndex):
close_df.index = pd.to_datetime(close_df.index)
close_ts_ns = close_df.index.astype(np.int64).values
close_px = close_df["Close"].astype(float).values
sub = fires[fires["would_trade"] == True].copy() # noqa: E712
if sub.empty:
fires["forward_ret_pct"] = np.nan
return fires
sig = pd.to_datetime(sub["signal_dt"])
fire_ns = sig.astype(np.int64).values
c0 = sub["close"].astype(float).values
side = sub["side"].astype(str).values
ret, valid = _forward_ret_vectorized(
fire_ns, c0, close_ts_ns, close_px, side, MATCH_FORWARD_BARS, _FEE_PCT
)
fires = fires.copy()
fires["forward_ret_pct"] = np.nan
idx = sub.index
fires.loc[idx, "forward_ret_pct"] = np.where(valid, ret, np.nan)
return fires
def summarize(fires: pd.DataFrame) -> dict:
"""집계 dict."""
traded = fires[fires["would_trade"] == True] # noqa: E712
with_ret = traded[traded["forward_ret_pct"].notna()]
out: dict = {
"generated_at": datetime.now().isoformat(timespec="seconds"),
"symbol": SYMBOL,
"forward_bars": MATCH_FORWARD_BARS,
"fee_round_trip_pct": _FEE_PCT,
"total_signals": int(len(fires)),
"would_trade_count": int(len(traded)),
"skipped_count": int(len(fires) - len(traded)),
"labeled_count": int(len(with_ret)),
"note": (
"모의 forward 수익률. 실계좌·hybrid 복리 PnL 아님. "
"매수·매도 leg 미결합 단순 합산."
),
}
if not with_ret.empty:
out["mean_forward_ret_pct"] = round(float(with_ret["forward_ret_pct"].mean()), 4)
out["sum_forward_ret_pct"] = round(float(with_ret["forward_ret_pct"].sum()), 4)
by_side = (
with_ret.groupby("side")["forward_ret_pct"]
.agg(["count", "mean", "sum"])
.round(4)
)
out["by_side"] = {k: v.to_dict() for k, v in by_side.iterrows()}
by_rule = (
traded.groupby("rule_id")
.size()
.to_dict()
if not traded.empty
else {}
)
out["fires_by_rule"] = by_rule
return out
def main() -> None:
"""paper_fires 로드 → forward % → 리포트 저장."""
fires = load_paper_fires(PAPER_FIRES_LOG)
if fires.empty:
print(f"[07] 발화 로그 없음: {PAPER_FIRES_LOG}")
print(" Phase C 기간 06_execute_live.py (LIVE=0) 상시 실행 후 재시도")
return
mon = Monitor(cooldown_file=None)
df = mon.read_candles_from_db(SYMBOL, MATCH_PRIMARY_INTERVAL, max_rows=50000)
if df.empty:
df = mon.get_coin_some_data(SYMBOL, MATCH_PRIMARY_INTERVAL)
if not isinstance(df.index, pd.DatetimeIndex):
df = df.set_index(pd.to_datetime(df["datetime"]))
fires = attach_forward_returns(fires, df)
report = summarize(fires)
PAPER_WEEKLY_REPORT_JSON.parent.mkdir(parents=True, exist_ok=True)
PAPER_WEEKLY_REPORT_JSON.write_text(
json.dumps(report, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(f"[07] 저장: {PAPER_WEEKLY_REPORT_JSON}")
print(f" 기간 로그: {fires['ts'].min()} ~ {fires['ts'].max()}")
print(f" 발화 {report['total_signals']} · 체결가정(would_trade) {report['would_trade_count']}")
if "sum_forward_ret_pct" in report:
print(
f" 모의 forward 합산: {report['sum_forward_ret_pct']}% "
f"(평균 {report['mean_forward_ret_pct']}%, "
f"{MATCH_FORWARD_BARS}봉 후, 참고용)"
)
else:
print(" forward 라벨 가능 건 없음 (봉 데이터 부족 또는 발화 없음)")
if __name__ == "__main__":
main()

View File

@@ -8,4 +8,5 @@ if str(ROOT) not in sys.path:
# .env → config 일관 로드 # .env → config 일관 로드
from deepcoin.env_loader import load_project_env # noqa: E402 from deepcoin.env_loader import load_project_env # noqa: E402
load_project_env() # 프로젝트 .env가 OS에 남은 LIVE_* 등보다 우선 (실거래 설정 일치)
load_project_env(override=True)

17
scripts/run_phase_c.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/bin/bash
# Phase C dry-run (~금요일 저녁). 프로젝트 루트에서 실행.
set -e
cd "$(dirname "$0")/.."
PY="${PY:-/Users/dsyoon/opt/anaconda3/envs/coin/bin/python}"
echo "=== Phase C: LIVE_TRADING_ENABLED=0 확인 ==="
$PY -c "import runpy; from pathlib import Path; runpy.run_path('scripts/_bootstrap.py'); import config; assert not config.LIVE_TRADING_ENABLED, 'LIVE must be 0'"
echo "=== 01 봉 갱신 (1일 1회 권장) ==="
$PY scripts/01_download.py
echo "=== verify ==="
$PY scripts/06_verify_live_dryrun.py
echo "=== 06 dry-run 상시 (Ctrl+C 종료) ==="
exec $PY scripts/06_execute_live.py