ee3e799de1
주요 변경: - 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>
227 lines
10 KiB
Python
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()
|