From 0d01faf464371fc824078949a1db099c5119893e Mon Sep 17 00:00:00 2001 From: fish Date: Tue, 28 Jul 2026 15:30:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B9=B3=E4=BB=93=E6=B1=87=E6=80=BB=E6=8C=89?= =?UTF-8?q?=E5=8E=9F=E5=A7=8B=E4=BB=B7=E5=B7=AE=E8=AE=A1=E7=AE=97=E6=AF=9B?= =?UTF-8?q?=E5=88=A9=E5=92=8C=E5=87=80=E5=88=A9=EF=BC=8C=E7=BA=A2=E6=B6=A8?= =?UTF-8?q?=E7=BB=BF=E8=B7=8C=E9=85=8D=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- ft-app/app/routers/summary.py | 114 +++++++++++++++++++++++------- ft-app/app/templates/summary.html | 41 ++++++----- 2 files changed, 110 insertions(+), 45 deletions(-) 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 @@
{{ total_count }}
-
总盈亏
-
- {% if total_pnl > 0 %}+{% endif %}{{ "%.0f"|format(total_pnl) }} +
毛利
+
+ {% if gross_pnl > 0 %}+{% endif %}{{ "%.2f"|format(gross_pnl) }}
总手续费
-
{{ "%.0f"|format(total_fee) }}
+
{{ "%.2f"|format(total_fee) }}
总胜率
-
+
{{ total_win_rate }}%
-
净盈亏
-
- {% set net = total_pnl - total_fee %} - {% if net > 0 %}+{% endif %}{{ "%.0f"|format(net) }} +
净利润
+
+ {% if net_pnl > 0 %}+{% endif %}{{ "%.2f"|format(net_pnl) }}
@@ -72,9 +71,9 @@
笔数 {{ future_stats.count }} - 盈亏 {% if future_stats.pnl > 0 %}+{% endif %}{{ "%.0f"|format(future_stats.pnl) }} - 手续费 {{ "%.0f"|format(future_stats.total_fee) }} - 胜率 {{ future_stats.win_rate }}% + 毛利 {% if future_stats.gross > 0 %}+{% endif %}{{ "%.2f"|format(future_stats.gross) }} + 手续费 {{ "%.2f"|format(future_stats.total_fee) }} + 净利 {% if future_stats.net > 0 %}+{% endif %}{{ "%.2f"|format(future_stats.net) }}
{% if future_trades %} @@ -100,7 +99,7 @@ {% set p = t.pnl %} {% if p is not none %} - + {% if p > 0 %}+{% endif %}{{ p }} {% endif %} @@ -118,9 +117,9 @@
笔数 {{ option_stats.count }} - 盈亏 {% if option_stats.pnl > 0 %}+{% endif %}{{ "%.0f"|format(option_stats.pnl) }} - 手续费 {{ "%.0f"|format(option_stats.total_fee) }} - 胜率 {{ option_stats.win_rate }}% + 毛利 {% if option_stats.gross > 0 %}+{% endif %}{{ "%.2f"|format(option_stats.gross) }} + 手续费 {{ "%.2f"|format(option_stats.total_fee) }} + 净利 {% if option_stats.net > 0 %}+{% endif %}{{ "%.2f"|format(option_stats.net) }}
{% if option_trades %} @@ -154,7 +153,7 @@ {% set p = t.pnl %} {% if p is not none %} - + {% if p > 0 %}+{% endif %}{{ p }} {% endif %} @@ -172,9 +171,9 @@
笔数 {{ dual_stats.count }} - 盈亏 {% if dual_stats.pnl > 0 %}+{% endif %}{{ "%.0f"|format(dual_stats.pnl) }} - 手续费 {{ "%.0f"|format(dual_stats.total_fee) }} - 胜率 {{ dual_stats.win_rate }}% + 毛利 {% if dual_stats.gross > 0 %}+{% endif %}{{ "%.2f"|format(dual_stats.gross) }} + 手续费 {{ "%.2f"|format(dual_stats.total_fee) }} + 净利 {% if dual_stats.net > 0 %}+{% endif %}{{ "%.2f"|format(dual_stats.net) }}
{% if dual_trades %} @@ -195,7 +194,7 @@ {% set p = t.pnl %} {% if p is not none %} - + {% if p > 0 %}+{% endif %}{{ p }} {% endif %}