feat(weekly-report): implement weekly report generation with live trade data and performance tracking

- Added functionality to fetch live trade data from the dashboard API.
- Implemented weekly report generation that includes backtest results, live trade statistics, and performance trends.
- Enhanced error handling for API requests and improved logging for better traceability.
- Updated tests to cover new features and ensure reliability of the report generation process.
This commit is contained in:
21in7
2026-03-07 01:13:03 +09:00
parent 6a6740d708
commit 2a767c35d4
11 changed files with 466 additions and 243 deletions

View File

@@ -14,14 +14,16 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
import argparse
import json
import os
import re
import subprocess
from datetime import date, timedelta
import httpx
import numpy as np
from dotenv import load_dotenv
from loguru import logger
load_dotenv()
from src.backtester import WalkForwardBacktester, WalkForwardConfig
from src.notifier import DiscordNotifier
@@ -76,55 +78,30 @@ def run_backtest(
return wf.run()
# ── 로그 파싱 패턴 ────────────────────────────────────────────────
_RE_ENTRY = re.compile(
r"\[(\w+)\]\s+(LONG|SHORT)\s+진입:\s+가격=([\d.]+),\s+수량=([\d.]+),\s+SL=([\d.]+),\s+TP=([\d.]+)"
)
_RE_CLOSE = re.compile(
r"\[(\w+)\]\s+청산 감지\((\w+)\):\s+exit=([\d.]+),\s+rp=([\d.-]+),\s+commission=([\d.]+),\s+net_pnl=([\d.-]+)"
)
_RE_TIMESTAMP = re.compile(r"^(\d{4}-\d{2}-\d{2})\s")
# ── 대시보드 API에서 실전 트레이드 가져오기 ──────────────────────────
DASHBOARD_API_URL = os.getenv("DASHBOARD_API_URL", "http://10.1.10.24:8000")
def parse_live_trades(log_path: str, days: int = 7) -> list[dict]:
"""봇 로그에서 최근 N일간의 진입/청산 기록을 파싱한다."""
path = Path(log_path)
if not path.exists():
def fetch_live_trades(api_url: str = DASHBOARD_API_URL, limit: int = 500) -> list[dict]:
"""운영 LXC 대시보드 API에서 청산된 트레이드 내역을 가져온다."""
try:
resp = httpx.get(f"{api_url}/api/trades", params={"limit": limit}, timeout=10)
resp.raise_for_status()
return resp.json().get("trades", [])
except Exception as e:
logger.warning(f"대시보드 API 트레이드 조회 실패: {e}")
return []
cutoff = (date.today() - timedelta(days=days)).isoformat()
open_trades: dict[str, dict] = {}
closed_trades: list[dict] = []
for line in path.read_text().splitlines():
m_ts = _RE_TIMESTAMP.match(line)
if m_ts and m_ts.group(1) < cutoff:
continue
m = _RE_ENTRY.search(line)
if m:
sym, side, price, qty, sl, tp = m.groups()
open_trades[sym] = {
"symbol": sym, "side": side,
"entry_price": float(price), "quantity": float(qty),
"sl": float(sl), "tp": float(tp),
"entry_time": m_ts.group(1) if m_ts else "",
}
continue
m = _RE_CLOSE.search(line)
if m:
sym, reason, exit_price, rp, commission, net_pnl = m.groups()
trade = open_trades.pop(sym, {"symbol": sym, "side": "UNKNOWN"})
trade.update({
"close_reason": reason, "exit_price": float(exit_price),
"expected_pnl": float(rp), "commission": float(commission),
"net_pnl": float(net_pnl),
"exit_time": m_ts.group(1) if m_ts else "",
})
closed_trades.append(trade)
return closed_trades
def fetch_live_stats(api_url: str = DASHBOARD_API_URL) -> dict:
"""운영 LXC 대시보드 API에서 전체 통계를 가져온다."""
try:
resp = httpx.get(f"{api_url}/api/stats", timeout=10)
resp.raise_for_status()
return resp.json()
except Exception as e:
logger.warning(f"대시보드 API 통계 조회 실패: {e}")
return {}
# ── 추이 추적 ────────────────────────────────────────────────────
@@ -373,11 +350,12 @@ def save_report(report: dict, report_dir: str) -> Path:
def generate_report(
symbols: list[str],
report_dir: str = str(WEEKLY_DIR),
log_path: str = "logs/bot.log",
report_date: date | None = None,
api_url: str | None = None,
) -> dict:
"""전체 주간 리포트를 생성한다."""
today = report_date or date.today()
dashboard_url = api_url or DASHBOARD_API_URL
# 1) Walk-Forward 백테스트 (심볼별)
logger.info("백테스트 실행 중...")
@@ -416,28 +394,31 @@ def generate_report(
"total_pnl": round(combined_pnl, 2),
}
# 2) 실전 트레이드 파싱
logger.info("실전 로그 파싱 중...")
live_trades_list = parse_live_trades(log_path, days=7)
live_wins = sum(1 for t in live_trades_list if t.get("net_pnl", 0) > 0)
live_pnl = sum(t.get("net_pnl", 0) for t in live_trades_list)
# 2) 운영 대시보드 API에서 실전 트레이드 조회
logger.info(f"대시보드 API에서 실전 트레이드 조회 중... ({dashboard_url})")
live_stats = fetch_live_stats(dashboard_url)
live_trades_list = fetch_live_trades(dashboard_url)
live_count = live_stats.get("total_trades", len(live_trades_list))
live_wins = live_stats.get("wins", 0)
live_pnl = live_stats.get("total_pnl", 0)
live_summary = {
"count": len(live_trades_list),
"net_pnl": round(live_pnl, 2),
"win_rate": round(live_wins / len(live_trades_list) * 100, 1) if live_trades_list else 0,
"count": live_count,
"net_pnl": round(float(live_pnl), 2),
"win_rate": round(live_wins / live_count * 100, 1) if live_count > 0 else 0,
}
# 3) 추이 로드
trend = load_trend(report_dir)
# 4) 누적 트레이드 수
cumulative = combined_trades + len(live_trades_list)
# 4) 누적 트레이드 수 (실전 + 이전 리포트)
cumulative = live_count
rdir = Path(report_dir)
if rdir.exists():
for rpath in sorted(rdir.glob("report_*.json")):
try:
prev = json.loads(rpath.read_text())
cumulative += prev.get("live_trades", {}).get("count", 0)
cumulative = max(cumulative, prev.get("live_trades", {}).get("count", 0))
except (json.JSONDecodeError, KeyError):
pass