Files
QuantEngineByItz/tools/validate_harness_json.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

227 lines
10 KiB
Python

#!/usr/bin/env python3
"""
validate_harness_json.py
harness_context 의도·결과 검증
JSON 경로: data.data._harness_context
"""
import json, pathlib, sys
# Windows cp949 터미널 호환
if sys.stdout.encoding and sys.stdout.encoding.lower() not in ('utf-8','utf8'):
sys.stdout = open(sys.stdout.fileno(), mode='w', encoding='utf-8', buffering=1)
JSON_PATH = pathlib.Path('GatherTradingData.json')
SEP = '=' * 60
SEP2 = '-' * 100
def safe_json(val):
if isinstance(val, str):
try:
return json.loads(val)
except Exception:
return val
return val
def main():
if not JSON_PATH.exists():
print(f'[ERROR] {JSON_PATH} not found')
sys.exit(1)
root = json.loads(JSON_PATH.read_text(encoding='utf-8'))
# harness_context 탐색
hc = None
hc_path = 'unknown'
try:
hc = root['data']['_harness_context']
hc_path = 'data._harness_context'
except (KeyError, TypeError):
pass
if hc is None:
for key in ['_harness_context', 'harness_context']:
if key in root and isinstance(root[key], dict):
hc = root[key]; hc_path = key; break
if hc is None:
print('[ERROR] harness_context 찾을 수 없음')
print('root keys:', list(root.keys())[:10])
sys.exit(1)
print(SEP)
print(f'파일: {JSON_PATH.name}')
print(f'harness_context 경로: {hc_path}')
print(f'키 수: {len(hc)}')
print(SEP)
# ── 1. HARNESS META ──────────────────────────────────────────────────────
print('\n[1] HARNESS META')
for k in ['harness_version', 'computed_at', 'captured_at', 'intraday_lock', 'mrs_score']:
v = hc.get(k, '(missing)')
warn = ''
if k == 'harness_version':
if v == '2026-05-19-X4R1':
warn = ' <- OK (최신 X4R1)'
else:
warn = f' <- [WARN] expected 2026-05-19-X4R1, got {v}'
print(f' {k}: {v}{warn}')
# ── 2. H1 GUARDS ────────────────────────────────────────────────────────
print('\n[2] H1 포트폴리오 가드 (QEH_AUDIT_BLOCK)')
h1 = [
('total_heat_pct', 'TOTAL_HEAT_V1 '),
('heat_gate_status', 'HEAT_GATE '),
('cash_floor_status', 'CASH_RATIOS_V1 '),
('cash_floor_min_pct', 'cash_floor_min_pct '),
('settlement_cash_pct', 'D+2 현금% '),
('immediate_cash_krw', 'immediate_cash_krw '),
('buy_power_krw', 'buy_power_krw '),
('total_asset_krw', 'total_asset_krw '),
('performance_label', 'bayesian_label '),
('performance_multiplier', 'bayesian_mult '),
]
for k, label in h1:
v = hc.get(k, '(missing)')
print(f' [{label}] {v}')
# ── 3. Alpha-Shield ──────────────────────────────────────────────────────
print('\n[3] Alpha-Shield (X1/X3/W1~W4) [신규 2026-05-19-X1W1/X4R1]')
as_keys = [
'alpha_shield_lock',
'alpha_shield_critical_alert_count',
'alpha_shield_critical_alert_flag',
'alpha_shield_formula_ids',
'alpha_shield_computed_at',
]
as_ok = True
for k in as_keys:
v = hc.get(k, '(missing)')
flag = ' <- [FAIL] 누락' if v == '(missing)' else ''
if v == '(missing)': as_ok = False
print(f' {k}: {v}{flag}')
alpha_raw = hc.get('alpha_shield_json')
if alpha_raw is None:
print(' alpha_shield_json: (missing) <- [FAIL]')
as_ok = False
else:
holdings = safe_json(alpha_raw)
if isinstance(holdings, list) and holdings:
print(f'\n per-holding ({len(holdings)}종목):')
cols = f' {"Ticker":<8} {"MRG_Gate":<22} {"RS_Status":<16} {"W1":<20} {"W2":<20} {"W3":<20} {"W4":<24} {"Fires"}'
print(cols)
print(' ' + '-' * 135)
for h in holdings:
t = h.get('ticker', '?')
mrg = h.get('mrg_gate', '?')
rs = h.get('rs_status', '?')
w1 = h.get('w1_status', '?')
w2 = h.get('w2_status', '?')
w3 = h.get('w3_status', '?')
w4 = h.get('w4_status', '?')
fires = h.get('radar_fires', '?')
crit = ' [CRITICAL]' if h.get('critical_alert') == 'CRITICAL_ALERT' else ''
print(f' {t:<8} {mrg:<22} {rs:<16} {w1:<20} {w2:<20} {w3:<20} {w4:<24} {fires}{crit}')
# deviation_ratio / rs_ratio 상세
print()
print(f' {"Ticker":<8} {"deviation_ratio":<18} {"rs_ratio":<12} {"sector":<12} {"sector_rank":<12} {"flow_accel_ratio"}')
print(' ' + '-' * 90)
for h in holdings:
t = h.get('ticker', '?')
dr = h.get('deviation_ratio', 'N/A')
rsr = h.get('rs_ratio', 'N/A')
sec = h.get('sector', 'N/A') or 'N/A'
srk = h.get('sector_rank', 'N/A')
far = h.get('flow_accel_ratio', 'N/A')
print(f' {t:<8} {str(dr):<18} {str(rsr):<12} {str(sec):<12} {str(srk):<12} {far}')
else:
print(f' alpha_shield_json: parse error or empty ({type(holdings).__name__})')
as_ok = False
print()
print(' Alpha-Shield 전체 상태:', 'PASS' if as_ok else '[FAIL] 일부 누락')
# ── 4. X4 Ratchet ────────────────────────────────────────────────────────
print('\n[4] X4 Ratchet -- prices_json [신규 2026-05-19-X4R1]')
prices_raw = hc.get('prices_json')
if prices_raw is None:
print(' prices_json: (missing)')
else:
prices = safe_json(prices_raw)
if isinstance(prices, list):
has_ratchet = any('ratchet_applied' in p for p in prices)
if not has_ratchet:
print(' [FAIL] ratchet_applied 필드 없음 -- X4 미적용')
else:
print(f' {"Ticker":<8} {"stop_price":<12} {"avg_cost":<10} {"atr20":<8} {"ratchet_applied":<32} note[:50]')
print(' ' + '-' * 120)
for p in prices:
if 'error' in p:
print(f' {p.get("ticker","?"):<8} ERROR: {p.get("error")}')
continue
t = p.get('ticker', '?')
sp = p.get('stop_price', '?')
ac = p.get('avg_cost', '?')
atr = p.get('atr20', '?')
ra = p.get('ratchet_applied', '(missing)')
note = str(p.get('ratchet_note', ''))[:50]
flag = ' <- [RATCHET ON]' if 'RATCHET' in str(ra) and 'INACTIVE' not in str(ra) and 'PASS' not in str(ra) else ''
print(f' {t:<8} {str(sp):<12} {str(ac):<10} {str(atr):<8} {str(ra):<32} {note}{flag}')
else:
print(f' prices_json type: {type(prices).__name__}')
# ── 5. Gate 1d -- Decision Trace ─────────────────────────────────────────
print('\n[5] Gate 1d (MEAN_REVERSION_GATE) -- decision_trace [신규 Gate]')
traces_raw = hc.get('decision_trace_json')
if traces_raw is None:
print(' decision_trace_json: (missing)')
else:
traces = safe_json(traces_raw)
if isinstance(traces, list):
mrg = [t for t in traces if t.get('state') == 'MEAN_REVERSION_GATE']
if not mrg:
print(' [INFO] MEAN_REVERSION_GATE trace 없음')
print(' -- 모든 종목이 BUY 아니거나 데이터 부재 (정상 가능)')
else:
print(f' {"Ticker":<8} {"Result":<20} reason')
print(' ' + '-' * 80)
for t in mrg:
print(f' {t.get("ticker","?"):<8} {t.get("result","?"):<20} {t.get("reason","?")}')
else:
print(f' decode error: {type(traces).__name__}')
# ── 6. Sell Priority ─────────────────────────────────────────────────────
print('\n[6] Sell Priority (H2)')
sc_raw = hc.get('sell_candidates_json')
if sc_raw:
cands = safe_json(sc_raw)
if isinstance(cands, list):
print(f' {"Rank":<5} {"Ticker":<8} {"Tier":<5} {"Score":<7} {"Trim_Style":<14} reason[:65]')
print(' ' + '-' * 110)
for c in cands[:12]:
print(f' {str(c.get("rank","?")):<5} {c.get("ticker","?"):<8} '
f'{str(c.get("tier","?")):<5} {str(c.get("score","?")):<7} '
f'{str(c.get("trim_style","?")):<14} {str(c.get("reason",""))[:65]}')
# ── 7. H5 Decision Flow ───────────────────────────────────────────────────
print('\n[7] H5 Decision Flow')
dec_raw = hc.get('decisions_json')
if dec_raw:
decs = safe_json(dec_raw)
if isinstance(decs, list):
print(f' {"Ticker":<8} {"base_action":<20} {"final_action":<20} {"gate_changed":<14} trace gates')
print(' ' + '-' * 100)
for d in decs:
gates = [tr.get('gate','?') + ':' + tr.get('result','?')
for tr in d.get('gate_trace', [])]
gc = str(d.get('gate_changed', '?'))
print(f' {d.get("ticker","?"):<8} {d.get("base_action","?"):<20} '
f'{d.get("final_action","?"):<20} {gc:<14} {", ".join(gates)}')
print()
print(SEP)
print('검증 완료')
if __name__ == '__main__':
main()