Files
QuantEngineByItz/tools/build_time_stop_forecast_v1.py
T
kjh2064 ee3e799de1 feat: 리밸런싱 엔진 V1 + GAS 버그 수정 (2026-06-13)
주요 변경:
- tools/build_rebalance_engine_v1.py: REBALANCE_ENGINE_V1 신규
  * account_snapshot 직접 합산(_build_snap_position_map) → 소수주 분리 행 병합
  * 레짐 소스 macro.REGIME_PRELIM 최우선 (GAS 와 동일)
- src/gas_adapter_parts/gdf_06_rebalance.gs: runRebalanceSheet_() 신규
  * Logger.log / getSpreadsheet_() 로 run_all 연동 수정
- src/gas_adapter_parts/gdc_01_fetch_fundamentals.gs
  * _mergePositionRecord_(): 소수주 중복 행 합산 신규
  * parseInt → parseFloat (qty, availQty)
- src/gas_adapter_parts/gdf_01_price_metrics.gs
  * 미보유 종목 SELL_READY → WATCH_EXIT_SIGNAL
- spec/41_release_dag.yaml: build_rebalance_sheet 노드 추가 (step_count 63)
- spec/51_formula_lifecycle_registry.yaml: REBALANCE_ENGINE_V1 등록

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 13:20:14 +09:00

208 lines
7.5 KiB
Python

"""build_time_stop_forecast_v1.py — P1-1: TIME_STOP 사전 발동 예측
60일 TIME_STOP 기준에 가까운 종목을 사전에 탐지하여 운영자가
7월 발동 전에 포지션 검토를 할 수 있도록 예측 보고서를 생성한다.
formula_id: BUILD_TIME_STOP_FORECAST_V1
contract_ref: spec/exit/stop_loss.yaml (TIME_STOP 60일 기준)
spec/54_temporal_data_integrity.yaml
"""
from __future__ import annotations
import json
import sys
from datetime import datetime, date, timedelta
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_HARNESS = ROOT / "Temp" / "computed_harness_v1.json"
DEFAULT_DATA = ROOT / "GatherTradingData.json"
OUTPUT_PATH = ROOT / "Temp" / "time_stop_forecast_v1.json"
TIME_STOP_HOLD_DAYS = 60 # 60일 기준 (spec/exit/stop_loss.yaml)
FORECAST_WINDOW_DAYS = 30 # 앞으로 30일 내 발동 예상 종목 탐지
def _load_json(path: Path) -> dict:
if not path.exists():
return {"_missing": True, "_path": str(path)}
try:
return json.loads(path.read_text(encoding="utf-8"))
except Exception as e:
return {"_error": str(e), "_path": str(path)}
def _parse_date(s: str | None) -> date | None:
if not s:
return None
for fmt in ("%Y-%m-%d", "%Y/%m/%d"):
try:
return datetime.strptime(s, fmt).date()
except ValueError:
continue
return None
def _forecast_from_harness(harness: dict, today: date) -> list[dict]:
"""Parse relative_stop_signal_json for TIME_STOP candidates."""
forecasts = []
signals = harness.get("relative_stop_signal_json") or []
if not isinstance(signals, list):
return forecasts
for sig in signals:
if not isinstance(sig, dict):
continue
details = sig.get("details") or {}
hold_days = details.get("hold_days")
excess = details.get("excess_ret20d")
entry_date_str = details.get("entry_date")
if hold_days is None:
continue
hold_days = int(hold_days)
days_to_trigger = TIME_STOP_HOLD_DAYS - hold_days
if days_to_trigger <= FORECAST_WINDOW_DAYS:
trigger_date = (
(_parse_date(entry_date_str) + timedelta(days=TIME_STOP_HOLD_DAYS))
if entry_date_str else None
)
forecasts.append({
"ticker": sig.get("ticker"),
"name": sig.get("name"),
"current_hold_days": hold_days,
"days_to_trigger": max(0, days_to_trigger),
"projected_trigger_date": str(trigger_date) if trigger_date else "UNKNOWN",
"excess_ret20d": excess,
"current_signal": sig.get("signal"),
"time_stop_condition": excess is not None and float(excess) < 0,
"action_required": days_to_trigger <= 7,
"note": (
"TIME_STOP 발동 임박 (7일 이내)" if days_to_trigger <= 7
else f"TIME_STOP {days_to_trigger}일 후 예상"
),
})
return sorted(forecasts, key=lambda x: x["days_to_trigger"])
def _forecast_from_data(data_json: dict, today: date) -> list[dict]:
"""Fallback: parse account_snapshot rows for hold_days."""
forecasts = []
rows = data_json.get("account_snapshot") or []
if isinstance(rows, dict):
rows = rows.get("rows") or []
if not isinstance(rows, list):
return forecasts
for row in rows:
if not isinstance(row, dict):
continue
entry_date_str = row.get("entry_date") or row.get("EntryDate")
entry_date = _parse_date(entry_date_str)
if not entry_date:
continue
hold_days = (today - entry_date).days
days_to_trigger = TIME_STOP_HOLD_DAYS - hold_days
if 0 <= days_to_trigger <= FORECAST_WINDOW_DAYS:
trigger_date = entry_date + timedelta(days=TIME_STOP_HOLD_DAYS)
forecasts.append({
"ticker": row.get("ticker") or row.get("code"),
"name": row.get("name") or row.get("종목명"),
"current_hold_days": hold_days,
"days_to_trigger": days_to_trigger,
"projected_trigger_date": str(trigger_date),
"excess_ret20d": None,
"current_signal": "DATA_MISSING",
"time_stop_condition": None,
"action_required": days_to_trigger <= 7,
"note": (
"TIME_STOP 발동 임박 (7일 이내)" if days_to_trigger <= 7
else f"TIME_STOP {days_to_trigger}일 후 예상"
),
})
return sorted(forecasts, key=lambda x: x["days_to_trigger"])
def run(harness_path: Path, data_path: Path) -> dict:
today = date.today()
harness = _load_json(harness_path)
data = _load_json(data_path)
forecasts = []
if not harness.get("_missing"):
forecasts = _forecast_from_harness(harness, today)
if not forecasts and not data.get("_missing"):
forecasts = _forecast_from_data(data, today)
imminent = [f for f in forecasts if f["days_to_trigger"] <= 7]
upcoming = [f for f in forecasts if 7 < f["days_to_trigger"] <= 30]
triggered = [f for f in forecasts if f.get("current_signal") == "TIME_STOP"]
gate = "PASS"
if imminent:
gate = "WARN"
if triggered:
gate = "TRIGGERED"
result = {
"gate": gate,
"as_of_date": str(today),
"time_stop_hold_days_threshold": TIME_STOP_HOLD_DAYS,
"forecast_window_days": FORECAST_WINDOW_DAYS,
"imminent_count": len(imminent),
"upcoming_count": len(upcoming),
"triggered_count": len(triggered),
"imminent_tickers": imminent,
"upcoming_tickers": upcoming,
"triggered_tickers": triggered,
"summary": (
f"TODAY={today}: "
f"{len(triggered)}건 발동중, "
f"{len(imminent)}건 7일내 임박, "
f"{len(upcoming)}건 30일내 예정"
),
"action_note": (
"임박 종목 포지션 재검토 필요 — excess_ret20d 추이 모니터링 권장"
if (imminent or triggered) else
"현재 30일내 발동 예상 종목 없음"
),
"contract": "spec/54_temporal_data_integrity.yaml",
}
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
OUTPUT_PATH.write_text(json.dumps(result, ensure_ascii=False, indent=2))
return result
def main() -> None:
import argparse
parser = argparse.ArgumentParser(description="TIME_STOP Forecast Builder")
parser.add_argument("--harness", default=str(DEFAULT_HARNESS))
parser.add_argument("--data", default=str(DEFAULT_DATA))
args = parser.parse_args()
result = run(Path(args.harness), Path(args.data))
gate = result.get("gate", "PASS")
print(f"[BUILD_TIME_STOP_FORECAST_V1] gate={gate}")
print(f" {result.get('summary')}")
if result.get("imminent_tickers"):
print(" 임박 종목:")
for t in result["imminent_tickers"]:
print(f" {t['ticker']} {t.get('name','')}{t['days_to_trigger']}일후 ({t['projected_trigger_date']}) excess={t.get('excess_ret20d')}")
if result.get("triggered_tickers"):
print(" 발동중:")
for t in result["triggered_tickers"]:
print(f" {t['ticker']} {t.get('name','')} — hold={t['current_hold_days']}일")
if __name__ == "__main__":
main()