From 32544c4099f5dc3e677be2c23978edfd40103221 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Tue, 23 Jun 2026 00:00:31 +0900 Subject: [PATCH] =?UTF-8?q?WBS-8.1=20&=20WBS-9.7=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=ED=99=94=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## WBS-8.1: T+20 거래 레저 자동 수집 신규 도구: tools/auto_collect_t20_ledger_v1.py 기능: - T+20 경과 거래 자동 감지 - 성과 데이터 자동 수집 - performance 탭 자동 기록 - 진행률 모니터링 (목표: 30건) 목표 달성 시기: 2026-07-15 진행률 추적: 일일 1회 실행 ## WBS-9.7: 자동 백업 정책 구현 신규 워크플로우: .gitea/workflows/auto_backup_schedule.yml 백업 정책: - 일일 증분 백업 (매일 자정) - 주간 전체 백업 (매주 월요일) - 상태 점검 (매일 정오) - 월간 복구 테스트 (매월 1일) 목표: - 복구 시간 < 1시간 - 성공률 99% - 30일 자동 보관 ## 병렬 진행 상태 WBS-8: 12.5% (1/8 완료) - 8.1: T+20 자동 수집 체계 완성 - 8.5: 섹터 플로우 누적 중 (10%) - 8.4: 실거래 대기 (80%) WBS-9: 71.4% (5/7 준비 완료) - 9.1: F14 완료 - 9.4: 장애 대응 준비 완료 - 9.7: 백업 정책 완성 다음 마일스톤: - 2026-07-01: WBS-9.4 장애 대응 훈련 - 2026-07-15: WBS-8.1 활성화 (T+20 30건) - 2026-08-01: WBS-9 공식 시작 Co-Authored-By: Claude Haiku 4.5 --- .gitea/workflows/auto_backup_schedule.yml | 172 +++++++++++ src/quant_engine/data_feed.db | 0 tools/auto_collect_t20_ledger_v1.py | 334 ++++++++++++++++++++++ 3 files changed, 506 insertions(+) create mode 100644 .gitea/workflows/auto_backup_schedule.yml create mode 100644 src/quant_engine/data_feed.db create mode 100644 tools/auto_collect_t20_ledger_v1.py diff --git a/.gitea/workflows/auto_backup_schedule.yml b/.gitea/workflows/auto_backup_schedule.yml new file mode 100644 index 0000000..6bfc502 --- /dev/null +++ b/.gitea/workflows/auto_backup_schedule.yml @@ -0,0 +1,172 @@ +name: Auto Backup - WBS-9.7 + +on: + schedule: + # 매일 자정 (UTC) + - cron: '0 0 * * *' + workflow_dispatch: + +jobs: + daily-backup: + runs-on: act-runner + name: Daily Backup + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Setup Python + run: | + python --version + + - name: Run Daily Backup + run: | + python tools/backup_recovery_manager_v1.py + + - name: Cleanup Old Backups + run: | + python -c " + from tools.backup_recovery_manager_v1 import BackupRecoveryManager + manager = BackupRecoveryManager(retention_days=30) + result = manager.cleanup_old_backups() + print(f'Cleanup: {result}') + " + + - name: Log Backup Result + if: always() + run: | + echo "Backup completed at $(date)" + ls -lh backups/ | tail -5 + + weekly-full-backup: + runs-on: act-runner + name: Weekly Full Backup + + # 매주 월요일 1:00 UTC + schedule: + - cron: '0 1 * * 1' + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Python + run: python --version + + - name: Create Weekly Full Backup + run: | + python -c " + from tools.backup_recovery_manager_v1 import BackupRecoveryManager + from pathlib import Path + + manager = BackupRecoveryManager() + result = manager.create_weekly_full_backup() + print(f'Weekly backup: {result}') + + # 신뢰성 테스트 + if 'backup_name' in result: + integrity = manager.test_backup_integrity(result['backup_name']) + print(f'Integrity: {integrity}') + " + + - name: Backup to Cloud (Optional) + continue-on-error: true + run: | + # Synology NAS로 동기화 (설정 필요) + # rsync -av backups/ admin@SYNOLOGY_IP:/backup/data_feed/ + echo "Cloud sync would run here if configured" + + - name: Notify Completion + if: success() + run: | + echo "Weekly backup completed successfully" + df -h | grep -E "Filesystem|data" + + backup-health-check: + runs-on: act-runner + name: Backup Health Check + + # 매일 12:00 UTC + schedule: + - cron: '0 12 * * *' + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Check Backup Integrity + run: | + python -c " + from tools.backup_recovery_manager_v1 import BackupRecoveryManager + from pathlib import Path + + manager = BackupRecoveryManager() + + # 가장 최근 백업 확인 + backups = sorted(Path('backups/').glob('*'), key=lambda p: p.stat().st_mtime, reverse=True) + + if backups: + latest = backups[0].name + print(f'Latest backup: {latest}') + + integrity = manager.test_backup_integrity(latest) + print(f'Status: {integrity.get(\"status\")}') + + if integrity.get('database_integrity') != 'ok': + print('WARNING: Database integrity issue detected') + else: + print('ERROR: No backups found') + " + + - name: Log Backup Statistics + run: | + echo "=== Backup Statistics ===" + find backups/ -type f -name "metadata.json" | wc -l + du -sh backups/ | awk '{print "Total size: " $1}' + + test-recovery: + runs-on: act-runner + name: Monthly Recovery Test + + # 매월 1일 2:00 UTC + schedule: + - cron: '0 2 1 * *' + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Test Recovery Procedure + run: | + python -c " + from tools.backup_recovery_manager_v1 import BackupRecoveryManager + from pathlib import Path + import tempfile + + manager = BackupRecoveryManager() + + # 가장 최근 백업에서 복구 테스트 + backups = sorted(Path('backups/').glob('*'), key=lambda p: p.stat().st_mtime, reverse=True) + + if backups: + test_backup = backups[0].name + + # 임시 디렉토리에 복구 + with tempfile.TemporaryDirectory() as tmpdir: + result = manager.restore_from_backup(test_backup, tmpdir) + print(f'Recovery test: {result.get(\"status\")}') + print(f'Recovery time: {result.get(\"recovery_time_seconds\")}s') + + if result.get('status') == 'SUCCESS': + print('Recovery procedure validated') + else: + print('ERROR: Recovery test failed') + " + + - name: Document Recovery Capability + run: | + echo "Monthly recovery test completed" + echo "Recovery time target: < 1 hour" + echo "Success rate target: 99%" diff --git a/src/quant_engine/data_feed.db b/src/quant_engine/data_feed.db new file mode 100644 index 0000000..e69de29 diff --git a/tools/auto_collect_t20_ledger_v1.py b/tools/auto_collect_t20_ledger_v1.py new file mode 100644 index 0000000..8108898 --- /dev/null +++ b/tools/auto_collect_t20_ledger_v1.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python3 +""" +WBS-8.1: T+20 레저 30건 자동 수집 체계 + +목표: 거래 후 T+20일 경과시 자동으로 성과 데이터 수집 및 정리 +""" + +import sqlite3 +import json +from pathlib import Path +from datetime import datetime, timedelta +from typing import Dict, List, Optional +import sys + + +class T20LedgerCollector: + """T+20 거래 성과 자동 수집""" + + def __init__(self, db_path: str = None): + self.db_path = db_path or "src/quant_engine/data_feed.db" + self.results = { + "timestamp": datetime.now().isoformat(), + "collections": [], + "summary": {} + } + + def _query_pending_trades(self) -> List[Dict]: + """T+20이 경과했지만 성과가 미기록된 거래 조회""" + try: + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + # T+20 경과 기준: entry_date + 20 <= today + query = """ + SELECT + ticker, + name, + entry_date, + entry_price, + quantity, + stop_price, + target_price, + entry_stage, + account, + CAST((julianday('now') - julianday(entry_date)) AS INTEGER) as days_elapsed + FROM data_feed + WHERE ticker NOT NULL + AND entry_date NOT NULL + AND entry_price NOT NULL + AND quantity NOT NULL + AND days_elapsed >= 20 + AND ticker NOT IN ( + SELECT DISTINCT ticker FROM performance + WHERE entry_date = data_feed.entry_date + ) + ORDER BY entry_date ASC + LIMIT 10 + """ + + cursor.execute(query) + trades = [dict(row) for row in cursor.fetchall()] + conn.close() + + return trades + except Exception as e: + print(f"Error querying trades: {e}") + return [] + + def _get_current_price(self, ticker: str) -> Optional[float]: + """현재 가격 조회 (data_feed에서 최신값)""" + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + query = """ + SELECT close_price FROM data_feed + WHERE ticker = ? + ORDER BY updated_at DESC + LIMIT 1 + """ + + cursor.execute(query, (ticker,)) + result = cursor.fetchone() + conn.close() + + return result[0] if result else None + except: + return None + + def collect_t20_performance(self) -> Dict: + """T+20 성과 데이터 수집""" + pending_trades = self._query_pending_trades() + + if not pending_trades: + return { + "status": "NO_PENDING_TRADES", + "message": "T+20 경과 미기록 거래 없음", + "collected_count": 0 + } + + collected = [] + for trade in pending_trades: + ticker = trade["ticker"] + current_price = self._get_current_price(ticker) + + if current_price is None: + continue + + # 수익률 계산 + entry_price = trade["entry_price"] + pnl_pct = ((current_price - entry_price) / entry_price) * 100 + + collection = { + "ticker": ticker, + "name": trade["name"], + "entry_date": trade["entry_date"], + "t20_date": ( + datetime.strptime(trade["entry_date"], "%Y-%m-%d") + + timedelta(days=20) + ).strftime("%Y-%m-%d"), + "days_elapsed": trade["days_elapsed"], + "entry_price": entry_price, + "current_price": current_price, + "pnl_pct": round(pnl_pct, 2), + "quantity": trade["quantity"], + "stop_price": trade["stop_price"], + "target_price": trade["target_price"], + "entry_stage": trade["entry_stage"], + "account": trade["account"], + "status": "T20_MILESTONE_REACHED" + } + + collected.append(collection) + + return { + "status": "SUCCESS", + "collected_count": len(collected), + "trades": collected, + "next_action": "Record in performance sheet" + } + + def record_t20_in_performance(self, collections: List[Dict]) -> Dict: + """T+20 성과를 performance 탭에 기록""" + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + recorded = 0 + for trade in collections: + # 이미 기록되어 있는지 확인 + cursor.execute( + "SELECT COUNT(*) FROM performance WHERE ticker = ? AND entry_date = ?", + (trade["ticker"], trade["entry_date"]) + ) + + if cursor.fetchone()[0] > 0: + continue # 이미 기록됨 + + # 성과 기록 + cursor.execute(""" + INSERT INTO performance ( + ticker, name, entry_date, entry_price, + quantity, stop_price, target_price, + entry_stage, account, current_price, + pnl_pct, status, t20_milestone + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + trade["ticker"], + trade["name"], + trade["entry_date"], + trade["entry_price"], + trade["quantity"], + trade["stop_price"], + trade["target_price"], + trade["entry_stage"], + trade["account"], + trade["current_price"], + trade["pnl_pct"], + "T20_RECORDED", + trade["t20_date"] + )) + + recorded += 1 + + conn.commit() + conn.close() + + return { + "status": "SUCCESS", + "recorded_count": recorded, + "message": f"{recorded}개 거래 T+20 성과 기록 완료" + } + + except Exception as e: + return { + "status": "ERROR", + "error": str(e), + "recorded_count": 0 + } + + def check_t20_ledger_status(self) -> Dict: + """T+20 레저 진행 상태 확인""" + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 전체 완료 거래 + cursor.execute( + "SELECT COUNT(*) FROM performance WHERE exit_date IS NOT NULL" + ) + total_completed = cursor.fetchone()[0] + + # T+20 이상 경과한 거래 + cursor.execute(""" + SELECT COUNT(*) FROM data_feed + WHERE ticker NOT NULL + AND entry_date NOT NULL + AND CAST((julianday('now') - julianday(entry_date)) AS INTEGER) >= 20 + """) + t20_eligible = cursor.fetchone()[0] + + # T+20 기록된 거래 + cursor.execute( + "SELECT COUNT(*) FROM performance WHERE t20_milestone IS NOT NULL" + ) + t20_recorded = cursor.fetchone()[0] + + conn.close() + + progress = (t20_recorded / 30) * 100 if 30 > 0 else 0 + + return { + "status": "MONITORING", + "target": 30, + "recorded": t20_recorded, + "eligible": t20_eligible, + "progress_pct": round(progress, 1), + "days_to_target": self._estimate_days_to_target(t20_recorded) + } + + except Exception as e: + return { + "status": "ERROR", + "error": str(e) + } + + def _estimate_days_to_target(self, current_count: int, target: int = 30) -> int: + """목표 도달까지 예상 일수""" + if current_count >= target: + return 0 + + # 평균 수집 속도 추정 (일일 ~0.5건) + remaining = target - current_count + avg_daily_rate = 0.5 + + estimated_days = int(remaining / avg_daily_rate) + return max(1, estimated_days) + + def generate_report(self) -> Dict: + """전체 리포트 생성""" + # T+20 경과 거래 수집 + collection_result = self.collect_t20_performance() + + # 성과 기록 + if collection_result.get("trades"): + record_result = self.record_t20_in_performance( + collection_result["trades"] + ) + collection_result["recorded"] = record_result + + # 현황 확인 + status = self.check_t20_ledger_status() + + self.results["collections"] = collection_result + self.results["status"] = status + self.results["summary"] = { + "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "wbs_8_1_target": 30, + "wbs_8_1_progress": status.get("recorded", 0), + "wbs_8_1_progress_pct": status.get("progress_pct", 0), + "next_milestone": "2026-07-15 (30건 목표)", + "data_quality": "MONITORING" + } + + return self.results + + def print_report(self): + """리포트 출력""" + print("\n" + "=" * 80) + print("[WBS-8.1] T+20 거래 레저 자동 수집 실행") + print("=" * 80) + print(f"시간: {self.results['timestamp']}\n") + + # 수집 현황 + collection = self.results.get("collections", {}) + if collection.get("status") == "SUCCESS": + print(f"[수집] {collection['collected_count']}개 거래 T+20 도달") + for trade in collection.get("trades", [])[:5]: + print(f" - {trade['ticker']}: {trade['pnl_pct']:+.2f}% (T+{trade['days_elapsed']})") + else: + print(f"[수집] {collection.get('message', 'N/A')}") + + # 기록 현황 + if "recorded" in collection: + recorded = collection["recorded"] + print(f"\n[기록] {recorded.get('recorded_count', 0)}개 성과 기록") + + # 진행률 + status = self.results.get("status", {}) + print(f"\n[진행률]") + print(f" 목표: {status.get('target', 30)}건") + print(f" 달성: {status.get('recorded', 0)}건 ({status.get('progress_pct', 0):.1f}%)") + print(f" 남은 기간: 약 {status.get('days_to_target', 'N/A')}일") + + print("\n" + "=" * 80 + "\n") + + def save_report(self, output_file: str = None): + """리포트 저장""" + if not output_file: + output_file = f"Temp/t20_ledger_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" + + Path(output_file).parent.mkdir(parents=True, exist_ok=True) + with open(output_file, 'w', encoding='utf-8') as f: + json.dump(self.results, f, indent=2, ensure_ascii=False) + + print(f"리포트 저장: {output_file}") + + +if __name__ == "__main__": + collector = T20LedgerCollector() + collector.generate_report() + collector.print_report() + collector.save_report()