#!/usr/bin/env python3 from __future__ import annotations import argparse import json import yaml from pathlib import Path ROOT = Path(__file__).resolve().parents[1] def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--snapshot", default="GatherTradingData.json") ap.add_argument("--contract", default="spec/15_account_snapshot_contract.yaml") args = ap.parse_args() snapshot_path = ROOT / args.snapshot contract_path = ROOT / args.contract if not snapshot_path.exists(): print(f"Snapshot file not found: {snapshot_path}") return 1 data_payload = json.loads(snapshot_path.read_text(encoding="utf-8")) data = data_payload.get("data", {}) settings = data.get("settings", {}) # 1. Total Asset & Prev Market Regime total_asset = settings.get("total_asset_krw", 0) prev_regime = settings.get("prev_market_regime", "NORMAL") # Define cash floor ratio based on regime # normal: 10%, overheated_or_event_week: 15%, risk_off: 25% (or min_cash_ratio 7%/10%/15%) # Let's assume standard target is 10% for normal, 15% for overheated, 25% for risk_off # But settings could provide weekly_target_cash_pct target_ratio = settings.get("weekly_target_cash_pct", 10.0) / 100.0 if not settings.get("weekly_target_cash_pct"): if "risk_off" in prev_regime.lower(): target_ratio = 0.15 # min 15% elif "overheated" in prev_regime.lower() or "event" in prev_regime.lower(): target_ratio = 0.10 # min 10% else: target_ratio = 0.07 # min 7% target_cash_floor = total_asset * target_ratio # 2. Gather Cash from snapshot snap = data.get("account_snapshot", []) immediate_cash_map = {} settlement_cash_d2_map = {} # Track accounts by type general_accounts = set() restricted_accounts = set() for item in snap: acc = item.get("account") acc_type = item.get("account_type", "") # Handle broken characters for general account type is_general = False if acc_type: acc_type_str = str(acc_type) if "일반" in acc_type_str or "Ϲ" in acc_type_str: is_general = True elif "isa" in acc_type_str.lower() or "연금" in acc_type_str or "pension" in acc_type_str.lower(): is_general = False else: is_general = True # Default to general if unknown if is_general: if acc: general_accounts.add(acc) else: if acc: restricted_accounts.add(acc) # Retrieve cash values if present imm_cash = item.get("immediate_cash") if imm_cash is not None: immediate_cash_map[acc] = max(immediate_cash_map.get(acc, 0.0), float(imm_cash)) d2_cash = item.get("settlement_cash_d2") if d2_cash is not None: settlement_cash_d2_map[acc] = max(settlement_cash_d2_map.get(acc, 0.0), float(d2_cash)) # Backup from settings for general account D+2 cash if empty settings_d2 = settings.get("settlement_cash_d2_krw") if not settlement_cash_d2_map and settings_d2 is not None: # Assign to a stub general account if no accounts exist acc_stub = list(general_accounts)[0] if general_accounts else "general_stub" general_accounts.add(acc_stub) settlement_cash_d2_map[acc_stub] = float(settings_d2) # 3. Sum up cash by account priority rules general_immediate = sum(immediate_cash_map.get(acc, 0.0) for acc in general_accounts) general_d2 = sum(settlement_cash_d2_map.get(acc, 0.0) for acc in general_accounts) restricted_cash = sum(immediate_cash_map.get(acc, 0.0) + settlement_cash_d2_map.get(acc, 0.0) for acc in restricted_accounts) # Cross account cash leak detection # If restricted cash is added to general cash or general accounts have restricted tags cross_account_cash_leak_count = 0 overlap = general_accounts.intersection(restricted_accounts) if overlap: cross_account_cash_leak_count += len(overlap) # D+2 Cash Defense Rule Applied d2_cash_defense_rule_applied = True eligible_cash = general_immediate + general_d2 cash_shortfall = max(0.0, target_cash_floor - eligible_cash) gate = "PASS" if cross_account_cash_leak_count == 0 else "FAIL" result = { "formula_id": "CASH_LEDGER_V2", "total_asset_krw": total_asset, "target_cash_floor_krw": target_cash_floor, "immediate_cash": general_immediate, "settlement_cash_d2": general_d2, "restricted_cash": restricted_cash, "cross_account_cash_leak_count": cross_account_cash_leak_count, "d2_cash_defense_rule_applied": d2_cash_defense_rule_applied, "cash_shortfall": cash_shortfall, "gate": gate } out_path = ROOT / "Temp" / "cash_ledger_v2.json" out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") print(json.dumps(result, ensure_ascii=True, indent=2)) # Log information separately print(f"Eligible Cash (General Immediate + D+2): {eligible_cash:,.0f} KRW") print(f"Restricted Cash (ISA + Pension): {restricted_cash:,.0f} KRW") print(f"Cash Shortfall against Target Floor ({target_cash_floor:,.0f} KRW): {cash_shortfall:,.0f} KRW") return 0 if gate == "PASS" else 1 if __name__ == "__main__": import sys sys.exit(main())