#!/usr/bin/env python3 """stock.briefing 폴백·강제 재실행. 메일은 발송하지 않는다 (2026-08-12 관리자님 결정 — 주식 리포트 메일 중지, 스냅샷만 유지). 목적은 자산웹 '당일 평가손익' 기준값이 되는 오늘 스냅샷을 확보하는 것. retry/final: 오늘 portfolio_daily_snapshot.json에 오늘 키가 이미 있으면 skip (idempotent). 없으면 stock_portfolio_report.py send --no-mail 재실행. final 모드에선 그래도 실패 시 레이 텔레그램 알림. force: 스냅샷 존재 여부 무관하게 21:00 fresh fetch로 데이터 덮어쓰기. 20:10 데이터가 부정확할 때 21:00 안정된 데이터로 갱신용. 스냅샷 있으면 run 모드, 없으면 send --no-mail 폴백 + 실패 시 알림. usage: briefing_fallback.py retry # 20:30 — 스냅샷 없으면 재실행, 실패해도 알림 없음 briefing_fallback.py final # 스냅샷 없으면 재실행 + 그래도 없으면 알림 (전통적 폴백) briefing_fallback.py force # 21:00 — 무조건 fresh fetch (스냅샷 갱신만, 노이즈 없음) """ from __future__ import annotations import json import subprocess import sys from datetime import datetime from pathlib import Path from zoneinfo import ZoneInfo KST = ZoneInfo('Asia/Seoul') WORKSPACE = Path('/Users/snowoyh/.openclaw/agents/stock/workspace') SNAPSHOT = WORKSPACE / 'state' / 'portfolio_daily_snapshot.json' REPORT = WORKSPACE / 'scripts' / 'stock_portfolio_report.py' sys.path.insert(0, str(WORKSPACE / 'scripts')) def today_snapshot_exists() -> bool: if not SNAPSHOT.exists(): return False try: data = json.loads(SNAPSHOT.read_text()) except Exception: return False today = datetime.now(KST).strftime('%Y-%m-%d') return today in data def run_briefing() -> int: # 2026-08-12부터 주식 리포트 메일은 발송하지 않는다 (스냅샷만). --no-mail 은 send 경로의 # 휴장일 가드·실패 알림을 유지하면서 메일만 건너뛴다. return subprocess.call(['/usr/bin/python3', str(REPORT), 'send', '--no-mail']) def alert(text: str) -> None: from send_balance_to_budget import send_telegram send_telegram(text) def main() -> int: if len(sys.argv) < 2 or sys.argv[1] not in ('retry', 'final', 'force'): print('usage: briefing_fallback.py {retry|final|force}', file=sys.stderr) return 2 mode = sys.argv[1] now = datetime.now(KST).strftime('%Y-%m-%d %H:%M:%S') # 휴장일/주말은 메인 stock.briefing이 self-skip하므로 폴백도 의미 없음. # 특히 final 모드의 거짓 "스냅샷 없음" 텔레그램 경보를 차단한다. try: from holiday_sync import is_market_day_today if not is_market_day_today(): print(f'[{mode}] {now} KRX 휴장일/주말 — 폴백 스킵') return 0 except Exception as e: print(f'[{mode}] holiday-check-fail: {e} — 평소대로 진행', file=sys.stderr) if mode == 'force': # 21:00 무조건 재실행 — 20:10 데이터가 부정확할 때 fresh fetch로 덮어씀. # 스냅샷 있으면 run(스냅샷만), 없으면 send --no-mail(스냅샷 + 실패 시 알림). if today_snapshot_exists(): print(f'[force] {now} 스냅샷 갱신 (run 모드, 메일·텔레그램 없음).') code = subprocess.call(['/usr/bin/python3', str(REPORT), 'run']) print(f'[force] {now} run exit={code}') return 0 if code == 0 else 1 print(f'[force] {now} 스냅샷 없음 → send --no-mail 재실행 (메일 없음).') code = run_briefing() print(f'[force] {now} send exit={code}') if today_snapshot_exists(): return 0 alert( f'⚠️ stock.briefing {now} 폴백 모두 실패\n' f'오늘({datetime.now(KST):%Y-%m-%d}) 스냅샷이 생성되지 않았습니다.\n' f'수동 실행: python3 {REPORT} send --no-mail' ) return 1 if today_snapshot_exists(): print(f'[{mode}] {now} 오늘 스냅샷 있음, skip.') return 0 print(f'[{mode}] {now} 오늘 스냅샷 없음 → stock.briefing 재실행.') code = run_briefing() print(f'[{mode}] {now} stock.briefing exit={code}') if today_snapshot_exists(): print(f'[{mode}] {now} 재실행으로 스냅샷 생성됨.') return 0 if mode == 'final': alert( f'⚠️ stock.briefing {now} 폴백 모두 실패\n' f'오늘({datetime.now(KST):%Y-%m-%d}) 스냅샷이 생성되지 않았습니다.\n' f'수동 실행: python3 {REPORT} send --no-mail' ) return 1 if mode == 'final' else 0 if __name__ == '__main__': sys.exit(main())