新增平仓汇总页面,按期货/期权/双买分类统计盈亏和胜率

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fish
2026-07-28 15:16:56 +08:00
parent d28ab148f6
commit 7ac9de3663
4 changed files with 305 additions and 1 deletions
+2 -1
View File
@@ -7,7 +7,7 @@ from starlette.middleware.base import BaseHTTPMiddleware
from app.database import engine, Base, SessionLocal from app.database import engine, Base, SessionLocal
from app.models import User from app.models import User
from app.seed import seed 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" TEMPLATES_DIR = Path(__file__).parent / "templates"
@@ -60,6 +60,7 @@ app.include_router(positions.router)
app.include_router(trades.router) app.include_router(trades.router)
app.include_router(option_trades.router) app.include_router(option_trades.router)
app.include_router(dual_options.router) app.include_router(dual_options.router)
app.include_router(summary.router)
@app.get("/") @app.get("/")
+88
View File
@@ -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,
)
)
+3
View File
@@ -215,6 +215,9 @@
<a href="/dual-options/" class="{% if active_nav == 'dual_options' %}active{% endif %}"> <a href="/dual-options/" class="{% if active_nav == 'dual_options' %}active{% endif %}">
<span class="icon">🎯</span> 期权双买 <span class="icon">🎯</span> 期权双买
</a> </a>
<a href="/summary/" class="{% if active_nav == 'summary' %}active{% endif %}">
<span class="icon">📋</span> 平仓汇总
</a>
<a href="/admin/" class="{% if active_nav == 'admin' %}active{% endif %}"> <a href="/admin/" class="{% if active_nav == 'admin' %}active{% endif %}">
<span class="icon">⚙️</span> 系统管理 <span class="icon">⚙️</span> 系统管理
</a> </a>
+212
View File
@@ -0,0 +1,212 @@
{% extends "base.html" %}
{% block title %}平仓汇总{% endblock %}
{% block heading %}平仓汇总{% endblock %}
{% block breadcrumb %}全部已平仓统计{% endblock %}
{% block content %}
<style>
.stat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 14px; margin-bottom: 28px; }
.stat-card {
background: var(--surface); border: 1px solid var(--border);
border-radius: 10px; padding: 18px 20px; text-align: center;
}
.stat-card .label { font-size: 0.75rem; color: var(--sub); margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.04em; }
.stat-card .value { font-size: 1.4rem; font-weight: 700; }
.tabs { display: flex; gap: 0; margin-bottom: 24px; border-bottom: 2px solid var(--border); }
.tab-btn {
padding: 10px 20px; border: none; background: none;
font-size: 0.88rem; font-weight: 500; color: var(--sub);
cursor: pointer; text-decoration: none; border-bottom: 2px solid transparent;
margin-bottom: -2px; transition: color .12s, border-color .12s;
}
.tab-btn:hover { color: var(--fg); }
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
.tab-panel { display: none; }
.tab-panel.active { display: block; }
.sub-stat { display: flex; gap: 18px; margin-bottom: 20px; flex-wrap: wrap; }
.sub-stat span { font-size: 0.82rem; color: var(--sub); }
.sub-stat b { color: var(--fg); }
</style>
{% set view = request.query_params.get('view', '') %}
{# ── 总览卡片 ── #}
<div class="stat-grid">
<div class="stat-card">
<div class="label">总平仓笔数</div>
<div class="value">{{ total_count }}</div>
</div>
<div class="stat-card">
<div class="label">总盈亏</div>
<div class="value" style="{% if total_pnl > 0 %}color:var(--success-fg);{% elif total_pnl < 0 %}color:var(--danger-fg);{% endif %}">
{% if total_pnl > 0 %}+{% endif %}{{ "%.0f"|format(total_pnl) }}
</div>
</div>
<div class="stat-card">
<div class="label">总手续费</div>
<div class="value">{{ "%.0f"|format(total_fee) }}</div>
</div>
<div class="stat-card">
<div class="label">总胜率</div>
<div class="value" style="{% if total_win_rate >= 50 %}color:var(--success-fg);{% else %}color:var(--danger-fg);{% endif %}">
{{ total_win_rate }}%
</div>
</div>
<div class="stat-card">
<div class="label">净盈亏</div>
<div class="value" style="{% if total_pnl - total_fee > 0 %}color:var(--success-fg);{% elif total_pnl - total_fee < 0 %}color:var(--danger-fg);{% endif %}">
{% set net = total_pnl - total_fee %}
{% if net > 0 %}+{% endif %}{{ "%.0f"|format(net) }}
</div>
</div>
</div>
{# ── 按类型分拆 ── #}
<div class="tabs">
<a href="/summary/?view=futures" class="tab-btn{% if view != 'options' and view != 'dual' %} active{% endif %}">📝 期货</a>
<a href="/summary/?view=options" class="tab-btn{% if view == 'options' %} active{% endif %}">📊 期权</a>
<a href="/summary/?view=dual" class="tab-btn{% if view == 'dual' %} active{% endif %}">🎯 双买</a>
</div>
{# ═══════════ 期货 ═══════════ #}
<div class="tab-panel{% if view != 'options' and view != 'dual' %} active{% endif %}">
<div class="sub-stat">
<span>笔数 <b>{{ future_stats.count }}</b></span>
<span>盈亏 <b style="{% if future_stats.pnl > 0 %}color:var(--success-fg);{% elif future_stats.pnl < 0 %}color:var(--danger-fg);{% endif %}">{% if future_stats.pnl > 0 %}+{% endif %}{{ "%.0f"|format(future_stats.pnl) }}</b></span>
<span>手续费 <b>{{ "%.0f"|format(future_stats.total_fee) }}</b></span>
<span>胜率 <b style="{% if future_stats.win_rate >= 50 %}color:var(--success-fg);{% else %}color:var(--danger-fg);{% endif %}">{{ future_stats.win_rate }}%</b></span>
</div>
{% if future_trades %}
<div class="table-wrap">
<table>
<tr><th>品种</th><th>合约</th><th>方向</th><th>开仓日</th><th>平仓日</th><th>持仓</th><th>开仓价</th><th>平仓价</th><th>盈亏</th></tr>
{% for t in future_trades %}
<tr>
<td>{{ t.product_code }}</td>
<td><strong>{{ t.contract_code.replace(t.product_code, '', 1) if t.contract_code.startswith(t.product_code) else t.contract_code }}</strong></td>
<td>
{% if t.direction == 'long' %}
<span class="badge badge-up"></span>
{% else %}
<span class="badge badge-down"></span>
{% endif %}
</td>
<td style="font-size:0.8rem;">{{ t.open_date }} {{ weekdays[t.open_date.weekday()] }}</td>
<td style="font-size:0.8rem;">{{ t.close_date }} {{ weekdays[t.close_date.weekday()] }}</td>
<td>{{ (t.close_date - t.open_date).days }}天</td>
<td>{{ t.open_price }}</td>
<td>{{ t.close_price }}</td>
<td>
{% set p = t.pnl %}
{% if p is not none %}
<span style="font-weight:700;{% if p > 0 %}color:var(--success-fg);{% elif p < 0 %}color:var(--danger-fg);{% endif %}">
{% if p > 0 %}+{% endif %}{{ p }}
</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
{% else %}
<div style="text-align:center;padding:40px;color:var(--sub);">暂无已平仓记录</div>
{% endif %}
</div>
{# ═══════════ 期权 ═══════════ #}
<div class="tab-panel{% if view == 'options' %} active{% endif %}">
<div class="sub-stat">
<span>笔数 <b>{{ option_stats.count }}</b></span>
<span>盈亏 <b style="{% if option_stats.pnl > 0 %}color:var(--success-fg);{% elif option_stats.pnl < 0 %}color:var(--danger-fg);{% endif %}">{% if option_stats.pnl > 0 %}+{% endif %}{{ "%.0f"|format(option_stats.pnl) }}</b></span>
<span>手续费 <b>{{ "%.0f"|format(option_stats.total_fee) }}</b></span>
<span>胜率 <b style="{% if option_stats.win_rate >= 50 %}color:var(--success-fg);{% else %}color:var(--danger-fg);{% endif %}">{{ option_stats.win_rate }}%</b></span>
</div>
{% if option_trades %}
<div class="table-wrap">
<table>
<tr><th>品种</th><th>合约</th><th>类型</th><th>买卖</th><th>行权价</th><th>开仓日</th><th>平仓日</th><th>持仓</th><th>开仓价</th><th>平仓价</th><th>盈亏</th></tr>
{% for t in option_trades %}
<tr>
<td>{{ t.product_code }}</td>
<td><strong>{{ t.contract_code.replace(t.product_code, '', 1) if t.contract_code.startswith(t.product_code) else t.contract_code }}</strong></td>
<td>
{% if t.option_type == 'C' %}
<span class="badge badge-up">C</span>
{% else %}
<span class="badge badge-down">P</span>
{% endif %}
</td>
<td>
{% if t.direction == 'buy' %}
<span class="badge badge-up"></span>
{% else %}
<span class="badge badge-down"></span>
{% endif %}
</td>
<td>{{ t.strike_price }}</td>
<td style="font-size:0.8rem;">{{ t.open_date }} {{ weekdays[t.open_date.weekday()] }}</td>
<td style="font-size:0.8rem;">{{ t.close_date }} {{ weekdays[t.close_date.weekday()] }}</td>
<td>{{ (t.close_date - t.open_date).days }}天</td>
<td>{{ t.open_price }}</td>
<td>{{ t.close_price }}</td>
<td>
{% set p = t.pnl %}
{% if p is not none %}
<span style="font-weight:700;{% if p > 0 %}color:var(--success-fg);{% elif p < 0 %}color:var(--danger-fg);{% endif %}">
{% if p > 0 %}+{% endif %}{{ p }}
</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
{% else %}
<div style="text-align:center;padding:40px;color:var(--sub);">暂无已平仓记录</div>
{% endif %}
</div>
{# ═══════════ 双买 ═══════════ #}
<div class="tab-panel{% if view == 'dual' %} active{% endif %}">
<div class="sub-stat">
<span>笔数 <b>{{ dual_stats.count }}</b></span>
<span>盈亏 <b style="{% if dual_stats.pnl > 0 %}color:var(--success-fg);{% elif dual_stats.pnl < 0 %}color:var(--danger-fg);{% endif %}">{% if dual_stats.pnl > 0 %}+{% endif %}{{ "%.0f"|format(dual_stats.pnl) }}</b></span>
<span>手续费 <b>{{ "%.0f"|format(dual_stats.total_fee) }}</b></span>
<span>胜率 <b style="{% if dual_stats.win_rate >= 50 %}color:var(--success-fg);{% else %}color:var(--danger-fg);{% endif %}">{{ dual_stats.win_rate }}%</b></span>
</div>
{% if dual_trades %}
<div class="table-wrap">
<table>
<tr><th>品种</th><th>合约</th><th>C行权价</th><th>P行权价</th><th>开仓日</th><th>平仓日</th><th>持仓</th><th>看涨(开/平)</th><th>看跌(开/平)</th><th>盈亏</th></tr>
{% for t in dual_trades %}
<tr>
<td>{{ t.product_code }}</td>
<td><strong>{{ t.contract_code.replace(t.product_code, '', 1) if t.contract_code.startswith(t.product_code) else t.contract_code }}</strong></td>
<td>{{ t.call_strike_price }}</td>
<td>{{ t.put_strike_price }}</td>
<td style="font-size:0.8rem;">{{ t.open_date }} {{ weekdays[t.open_date.weekday()] }}</td>
<td style="font-size:0.8rem;">{{ t.close_date }} {{ weekdays[t.close_date.weekday()] }}</td>
<td>{{ (t.close_date - t.open_date).days }}天</td>
<td>{{ t.call_open_price }} / {{ t.call_close_price }}</td>
<td>{{ t.put_open_price }} / {{ t.put_close_price }}</td>
<td>
{% set p = t.pnl %}
{% if p is not none %}
<span style="font-weight:700;{% if p > 0 %}color:var(--success-fg);{% elif p < 0 %}color:var(--danger-fg);{% endif %}">
{% if p > 0 %}+{% endif %}{{ p }}
</span>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
{% else %}
<div style="text-align:center;padding:40px;color:var(--sub);">暂无已平仓记录</div>
{% endif %}
</div>
{% endblock %}