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>
This commit is contained in:
@@ -0,0 +1,226 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user