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', '') %} + +{# ── 总览卡片 ── #} +
| 品种 | 合约 | 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 %} + | +