From 7ac9de36633d9da3b2c57f4cecc91451f3073563 Mon Sep 17 00:00:00 2001 From: fish Date: Tue, 28 Jul 2026 15:16:56 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=B9=B3=E4=BB=93=E6=B1=87?= =?UTF-8?q?=E6=80=BB=E9=A1=B5=E9=9D=A2=EF=BC=8C=E6=8C=89=E6=9C=9F=E8=B4=A7?= =?UTF-8?q?/=E6=9C=9F=E6=9D=83/=E5=8F=8C=E4=B9=B0=E5=88=86=E7=B1=BB?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E7=9B=88=E4=BA=8F=E5=92=8C=E8=83=9C=E7=8E=87?= 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/main.py | 3 +- ft-app/app/routers/summary.py | 88 +++++++++++++ ft-app/app/templates/base.html | 3 + ft-app/app/templates/summary.html | 212 ++++++++++++++++++++++++++++++ 4 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 ft-app/app/routers/summary.py create mode 100644 ft-app/app/templates/summary.html diff --git a/ft-app/app/main.py b/ft-app/app/main.py index 920c843..92fa375 100644 --- a/ft-app/app/main.py +++ b/ft-app/app/main.py @@ -7,7 +7,7 @@ from starlette.middleware.base import BaseHTTPMiddleware from app.database import engine, Base, SessionLocal from app.models import User from app.seed import seed -from app.routers import contracts, admin, auth, positions, trades, option_trades, dual_options +from app.routers import contracts, admin, auth, positions, trades, option_trades, dual_options, summary TEMPLATES_DIR = Path(__file__).parent / "templates" @@ -60,6 +60,7 @@ app.include_router(positions.router) app.include_router(trades.router) app.include_router(option_trades.router) app.include_router(dual_options.router) +app.include_router(summary.router) @app.get("/") diff --git a/ft-app/app/routers/summary.py b/ft-app/app/routers/summary.py new file mode 100644 index 0000000..b3c5131 --- /dev/null +++ b/ft-app/app/routers/summary.py @@ -0,0 +1,88 @@ +from fastapi import APIRouter, Depends, Request +from fastapi.responses import HTMLResponse +from sqlalchemy.orm import Session +from app.database import get_db +from app.models import Trade, OptionTrade, DualOptionTrade + +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 + 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 + + return { + "count": total, + "pnl": total_pnl, + "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, + } + + +@router.get("/", response_class=HTMLResponse) +def summary_page(request: Request, db: Session = Depends(get_db)): + # Futures + future_trades = ( + db.query(Trade).filter(Trade.status == "closed") + .order_by(Trade.close_date.desc()).all() + ) + future_stats = _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) + + # Dual options + dual_trades = ( + db.query(DualOptionTrade).filter(DualOptionTrade.status == "closed") + .order_by(DualOptionTrade.close_date.desc()).all() + ) + dual_stats = _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"] + 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( + template.render( + request=request, + active_nav="summary", + total_count=total_count, + total_pnl=total_pnl, + total_wins=total_wins, + total_win_rate=total_win_rate, + total_fee=total_fee, + future_stats=future_stats, + future_trades=future_trades, + option_stats=option_stats, + option_trades=option_trades, + dual_stats=dual_stats, + dual_trades=dual_trades, + weekdays=WEEKDAYS, + ) + ) diff --git a/ft-app/app/templates/base.html b/ft-app/app/templates/base.html index 401c481..c1178e6 100644 --- a/ft-app/app/templates/base.html +++ b/ft-app/app/templates/base.html @@ -215,6 +215,9 @@ 🎯 期权双买 + + 📋 平仓汇总 + ⚙️ 系统管理 diff --git a/ft-app/app/templates/summary.html b/ft-app/app/templates/summary.html new file mode 100644 index 0000000..414d8fa --- /dev/null +++ b/ft-app/app/templates/summary.html @@ -0,0 +1,212 @@ +{% extends "base.html" %} +{% block title %}平仓汇总{% endblock %} +{% block heading %}平仓汇总{% endblock %} +{% block breadcrumb %}全部已平仓统计{% endblock %} +{% block content %} + + + +{% set view = request.query_params.get('view', '') %} + +{# ── 总览卡片 ── #} +
+
+
总平仓笔数
+
{{ total_count }}
+
+
+
总盈亏
+
+ {% if total_pnl > 0 %}+{% endif %}{{ "%.0f"|format(total_pnl) }} +
+
+
+
总手续费
+
{{ "%.0f"|format(total_fee) }}
+
+
+
总胜率
+
+ {{ total_win_rate }}% +
+
+
+
净盈亏
+
+ {% set net = total_pnl - total_fee %} + {% if net > 0 %}+{% endif %}{{ "%.0f"|format(net) }} +
+
+
+ +{# ── 按类型分拆 ── #} + + +{# ═══════════ 期货 ═══════════ #} +
+
+ 笔数 {{ 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_trades %} +
+ + + {% for t in future_trades %} + + + + + + + + + + + + {% endfor %} +
品种合约方向开仓日平仓日持仓开仓价平仓价盈亏
{{ t.product_code }}{{ t.contract_code.replace(t.product_code, '', 1) if t.contract_code.startswith(t.product_code) else t.contract_code }} + {% if t.direction == 'long' %} + + {% else %} + + {% endif %} + {{ t.open_date }} {{ weekdays[t.open_date.weekday()] }}{{ t.close_date }} {{ weekdays[t.close_date.weekday()] }}{{ (t.close_date - t.open_date).days }}天{{ t.open_price }}{{ t.close_price }} + {% set p = t.pnl %} + {% if p is not none %} + + {% if p > 0 %}+{% endif %}{{ p }} + + {% endif %} +
+
+ {% else %} +
暂无已平仓记录
+ {% endif %} +
+ +{# ═══════════ 期权 ═══════════ #} +
+
+ 笔数 {{ 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_trades %} +
+ + + {% for t in option_trades %} + + + + + + + + + + + + + + {% endfor %} +
品种合约类型买卖行权价开仓日平仓日持仓开仓价平仓价盈亏
{{ t.product_code }}{{ t.contract_code.replace(t.product_code, '', 1) if t.contract_code.startswith(t.product_code) else t.contract_code }} + {% if t.option_type == 'C' %} + C + {% else %} + P + {% endif %} + + {% if t.direction == 'buy' %} + + {% else %} + + {% endif %} + {{ t.strike_price }}{{ t.open_date }} {{ weekdays[t.open_date.weekday()] }}{{ t.close_date }} {{ weekdays[t.close_date.weekday()] }}{{ (t.close_date - t.open_date).days }}天{{ t.open_price }}{{ t.close_price }} + {% set p = t.pnl %} + {% if p is not none %} + + {% if p > 0 %}+{% endif %}{{ p }} + + {% endif %} +
+
+ {% else %} +
暂无已平仓记录
+ {% endif %} +
+ +{# ═══════════ 双买 ═══════════ #} +
+
+ 笔数 {{ 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_trades %} +
+ + + {% for t in dual_trades %} + + + + + + + + + + + + + {% endfor %} +
品种合约C行权价P行权价开仓日平仓日持仓看涨(开/平)看跌(开/平)盈亏
{{ t.product_code }}{{ t.contract_code.replace(t.product_code, '', 1) if t.contract_code.startswith(t.product_code) else t.contract_code }}{{ t.call_strike_price }}{{ t.put_strike_price }}{{ t.open_date }} {{ weekdays[t.open_date.weekday()] }}{{ t.close_date }} {{ weekdays[t.close_date.weekday()] }}{{ (t.close_date - t.open_date).days }}天{{ t.call_open_price }} / {{ t.call_close_price }}{{ t.put_open_price }} / {{ t.put_close_price }} + {% set p = t.pnl %} + {% if p is not none %} + + {% if p > 0 %}+{% endif %}{{ p }} + + {% endif %} +
+
+ {% else %} +
暂无已平仓记录
+ {% endif %} +
+ +{% endblock %}