diff --git a/ft-app/app/routers/summary.py b/ft-app/app/routers/summary.py index b3c5131..eea7758 100644 --- a/ft-app/app/routers/summary.py +++ b/ft-app/app/routers/summary.py @@ -9,31 +9,95 @@ router = APIRouter(prefix="/summary", tags=["summary"]) WEEKDAYS = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] -def _stats(records, pnl_fn=None) -> dict: - """Compute summary stats for a list of closed trade records.""" - if pnl_fn is None: - pnl_fn = lambda r: r.pnl +def _future_stats(records) -> dict: total = len(records) - total_pnl = sum(pnl_fn(r) or 0 for r in records) - wins = sum(1 for r in records if (pnl_fn(r) or 0) > 0) - open_fee = sum((r.open_fee or 0) for r in records) - close_fee = sum((r.close_fee or 0) for r in records) - total_fee = open_fee + close_fee - - # For dual options, also include call/put specific fees - if records and hasattr(records[0], 'call_open_fee'): - open_fee = sum((r.call_open_fee or 0) + (r.put_open_fee or 0) for r in records) - close_fee = sum((r.call_close_fee or 0) + (r.put_close_fee or 0) for r in records) - total_fee = open_fee + close_fee - + gross = 0.0 + open_fee = 0.0 + close_fee = 0.0 + wins = 0 + for t in records: + mul = t.point_value + if t.direction == "short": + g = (t.open_price - t.close_price) * mul + else: + g = (t.close_price - t.open_price) * mul + gross += g + of = t.open_fee or 0 + cf = t.close_fee or 0 + open_fee += of + close_fee += cf + if g - of - cf > 0: + wins += 1 return { "count": total, - "pnl": total_pnl, + "gross": gross, + "net": gross - open_fee - close_fee, "wins": wins, "win_rate": round(wins / total * 100, 1) if total > 0 else 0, "open_fee": open_fee, "close_fee": close_fee, - "total_fee": total_fee, + "total_fee": open_fee + close_fee, + } + + +def _option_stats(records) -> dict: + total = len(records) + gross = 0.0 + open_fee = 0.0 + close_fee = 0.0 + wins = 0 + for t in records: + mul = t.point_value + if t.direction == "sell": + g = (t.open_price - t.close_price) * mul + else: + g = (t.close_price - t.open_price) * mul + gross += g + of = t.open_fee or 0 + cf = t.close_fee or 0 + open_fee += of + close_fee += cf + if g - of - cf > 0: + wins += 1 + return { + "count": total, + "gross": gross, + "net": gross - open_fee - close_fee, + "wins": wins, + "win_rate": round(wins / total * 100, 1) if total > 0 else 0, + "open_fee": open_fee, + "close_fee": close_fee, + "total_fee": open_fee + close_fee, + } + + +def _dual_stats(records) -> dict: + total = len(records) + gross = 0.0 + open_fee = 0.0 + close_fee = 0.0 + wins = 0 + for t in records: + mul = t.point_value + call_g = (t.call_close_price - t.call_open_price) * mul + put_g = (t.put_close_price - t.put_open_price) * mul + g = call_g + put_g + gross += g + of = (t.call_open_fee or 0) + (t.put_open_fee or 0) + cf = (t.call_close_fee or 0) + (t.put_close_fee or 0) + open_fee += of + close_fee += cf + if g - of - cf > 0: + wins += 1 + return { + "count": total, + "gross": gross, + "net": gross - open_fee - close_fee, + "wins": wins, + "win_rate": round(wins / total * 100, 1) if total > 0 else 0, + "open_fee": open_fee, + "close_fee": close_fee, + "total_fee": open_fee + close_fee, } @@ -44,28 +108,29 @@ def summary_page(request: Request, db: Session = Depends(get_db)): db.query(Trade).filter(Trade.status == "closed") .order_by(Trade.close_date.desc()).all() ) - future_stats = _stats(future_trades) + future_stats = _future_stats(future_trades) # Options option_trades = ( db.query(OptionTrade).filter(OptionTrade.status == "closed") .order_by(OptionTrade.close_date.desc()).all() ) - option_stats = _stats(option_trades) + option_stats = _option_stats(option_trades) # Dual options dual_trades = ( db.query(DualOptionTrade).filter(DualOptionTrade.status == "closed") .order_by(DualOptionTrade.close_date.desc()).all() ) - dual_stats = _stats(dual_trades) + dual_stats = _dual_stats(dual_trades) # Aggregate total_count = future_stats["count"] + option_stats["count"] + dual_stats["count"] - total_pnl = future_stats["pnl"] + option_stats["pnl"] + dual_stats["pnl"] + gross_pnl = future_stats["gross"] + option_stats["gross"] + dual_stats["gross"] + net_pnl = future_stats["net"] + option_stats["net"] + dual_stats["net"] + total_fee = future_stats["total_fee"] + option_stats["total_fee"] + dual_stats["total_fee"] total_wins = future_stats["wins"] + option_stats["wins"] + dual_stats["wins"] total_win_rate = round(total_wins / total_count * 100, 1) if total_count > 0 else 0 - total_fee = future_stats["total_fee"] + option_stats["total_fee"] + dual_stats["total_fee"] template = request.app.state.templates.get_template("summary.html") return HTMLResponse( @@ -73,7 +138,8 @@ def summary_page(request: Request, db: Session = Depends(get_db)): request=request, active_nav="summary", total_count=total_count, - total_pnl=total_pnl, + gross_pnl=gross_pnl, + net_pnl=net_pnl, total_wins=total_wins, total_win_rate=total_win_rate, total_fee=total_fee, diff --git a/ft-app/app/templates/summary.html b/ft-app/app/templates/summary.html index 414d8fa..33fbac0 100644 --- a/ft-app/app/templates/summary.html +++ b/ft-app/app/templates/summary.html @@ -37,26 +37,25 @@