期权交易"权利金"改为"开仓价";新增期权双买交易功能,支持跨式策略开平仓及组合盈亏分析

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fish
2026-07-28 14:24:49 +08:00
parent 6228c139ef
commit af3e359d24
6 changed files with 642 additions and 6 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 from app.routers import contracts, admin, auth, positions, trades, option_trades, dual_options
TEMPLATES_DIR = Path(__file__).parent / "templates" TEMPLATES_DIR = Path(__file__).parent / "templates"
@@ -59,6 +59,7 @@ app.include_router(admin.router)
app.include_router(positions.router) 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.get("/") @app.get("/")
+51 -1
View File
@@ -179,4 +179,54 @@ class OptionTrade(Base):
result = (self.open_price - self.close_price) * mul - (self.open_fee or 0) - (self.close_fee or 0) result = (self.open_price - self.close_price) * mul - (self.open_fee or 0) - (self.close_fee or 0)
else: else:
result = (self.close_price - self.open_price) * mul - (self.open_fee or 0) - (self.close_fee or 0) result = (self.close_price - self.open_price) * mul - (self.open_fee or 0) - (self.close_fee or 0)
return round(result, 2) return round(result, 2)
class DualOptionTrade(Base):
__tablename__ = "dual_option_trades"
id: Mapped[int] = mapped_column(primary_key=True)
product_code: Mapped[str] = mapped_column(String(10))
contract_code: Mapped[str] = mapped_column(String(10), index=True)
strike_price: Mapped[float] = mapped_column(Float)
call_open_price: Mapped[float] = mapped_column(Float)
put_open_price: Mapped[float] = mapped_column(Float)
call_open_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0)
put_open_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0)
open_date: Mapped[date] = mapped_column(Date)
point_value: Mapped[int] = mapped_column(Integer, default=20)
close_date: Mapped[date | None] = mapped_column(Date, nullable=True)
call_close_price: Mapped[float | None] = mapped_column(Float, nullable=True)
put_close_price: Mapped[float | None] = mapped_column(Float, nullable=True)
call_close_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0)
put_close_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0)
status: Mapped[str] = mapped_column(String(10), default="open")
@property
def pnl(self) -> float | None:
if self.call_close_price is None or self.put_close_price is None:
return None
mul = self.point_value
call_pnl = (self.call_close_price - self.call_open_price) * mul - (self.call_open_fee or 0) - (self.call_close_fee or 0)
put_pnl = (self.put_close_price - self.put_open_price) * mul - (self.put_open_fee or 0) - (self.put_close_fee or 0)
return round(call_pnl + put_pnl, 2)
@property
def total_premium(self) -> float:
return self.call_open_price + self.put_open_price
@property
def total_open_fee(self) -> float:
return (self.call_open_fee or 0) + (self.put_open_fee or 0)
@property
def total_cost(self) -> float:
return self.total_premium * self.point_value + self.total_open_fee
@property
def breakeven_upper(self) -> float:
return self.strike_price + self.total_premium
@property
def breakeven_lower(self) -> float:
return self.strike_price - self.total_premium
+163
View File
@@ -0,0 +1,163 @@
from datetime import date
from fastapi import APIRouter, Depends, Form, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import DualOptionTrade, Contract, Product
router = APIRouter(prefix="/dual-options", tags=["dual_options"])
def get_product_contracts(db: Session) -> dict:
result: dict[str, list[str]] = {}
products = db.query(Product).order_by(Product.code).all()
for p in products:
codes = [c.code for c in p.contracts if c.is_active]
if codes:
result[p.code] = codes
return result
def today_str() -> str:
return date.today().isoformat()
@router.get("/", response_class=HTMLResponse)
def dual_option_page(request: Request, db: Session = Depends(get_db)):
view = request.query_params.get("view", "")
open_trades = (
db.query(DualOptionTrade).filter(DualOptionTrade.status == "open")
.order_by(DualOptionTrade.open_date.desc()).all()
)
closed_trades = (
db.query(DualOptionTrade).filter(DualOptionTrade.status == "closed")
.order_by(DualOptionTrade.close_date.desc()).limit(50).all()
)
template = request.app.state.templates.get_template("dual_options.html")
return HTMLResponse(
template.render(
request=request,
active_nav="dual_options",
product_contracts=get_product_contracts(db),
open_trades=open_trades,
closed_trades=closed_trades,
today=today_str(),
weekdays=["周一","周二","周三","周四","周五","周六","周日"],
)
)
@router.post("/open")
def open_trade(
request: Request,
contract_code: str = Form(...),
strike_price: float = Form(...),
call_open_price: float = Form(...),
put_open_price: float = Form(...),
call_open_fee: float = Form(0.0),
put_open_fee: float = Form(0.0),
open_date: str = Form(...),
db: Session = Depends(get_db),
):
code = contract_code.upper()
contract = db.query(Contract).filter(Contract.code == code).first()
if contract:
product = contract.product
else:
product = db.query(Product).filter(Product.code == code[:2]).first()
product_code = product.code if product else code[:2]
point_value = product.point_value if product else 20
t = DualOptionTrade(
product_code=product_code,
contract_code=code,
strike_price=strike_price,
call_open_price=call_open_price,
put_open_price=put_open_price,
call_open_fee=call_open_fee,
put_open_fee=put_open_fee,
open_date=date.fromisoformat(open_date),
point_value=point_value,
status="open",
)
db.add(t)
db.commit()
return RedirectResponse("/dual-options/", status_code=303)
@router.post("/{trade_id}/close")
def close_trade(
request: Request,
trade_id: int,
close_date: str = Form(...),
call_close_price: float = Form(...),
put_close_price: float = Form(...),
call_close_fee: float = Form(0.0),
put_close_fee: float = Form(0.0),
db: Session = Depends(get_db),
):
t = db.query(DualOptionTrade).filter(DualOptionTrade.id == trade_id).first()
if t and t.status == "open":
t.close_date = date.fromisoformat(close_date)
t.call_close_price = call_close_price
t.put_close_price = put_close_price
t.call_close_fee = call_close_fee
t.put_close_fee = put_close_fee
t.status = "closed"
db.commit()
return RedirectResponse("/dual-options/", status_code=303)
@router.post("/{trade_id}/edit")
def edit_trade(
request: Request,
trade_id: int,
contract_code: str = Form(...),
strike_price: float = Form(...),
call_open_price: float = Form(...),
put_open_price: float = Form(...),
call_open_fee: float = Form(0.0),
put_open_fee: float = Form(0.0),
open_date: str = Form(...),
close_date: str = Form(""),
call_close_price: str = Form(""),
put_close_price: str = Form(""),
call_close_fee: str = Form(""),
put_close_fee: str = Form(""),
db: Session = Depends(get_db),
):
t = db.query(DualOptionTrade).filter(DualOptionTrade.id == trade_id).first()
if t:
code = contract_code.upper()
contract = db.query(Contract).filter(Contract.code == code).first()
if contract:
product = contract.product
else:
product = db.query(Product).filter(Product.code == code[:2]).first()
t.product_code = product.code if product else code[:2]
t.point_value = product.point_value if product else 20
t.contract_code = code
t.strike_price = strike_price
t.call_open_price = call_open_price
t.put_open_price = put_open_price
t.call_open_fee = call_open_fee
t.put_open_fee = put_open_fee
t.open_date = date.fromisoformat(open_date)
if close_date and call_close_price and put_close_price:
t.close_date = date.fromisoformat(close_date)
t.call_close_price = float(call_close_price)
t.put_close_price = float(put_close_price)
t.call_close_fee = float(call_close_fee) if call_close_fee else 0.0
t.put_close_fee = float(put_close_fee) if put_close_fee else 0.0
db.commit()
return RedirectResponse("/dual-options/", status_code=303)
@router.post("/{trade_id}/delete")
def delete_trade(trade_id: int, db: Session = Depends(get_db)):
t = db.query(DualOptionTrade).filter(DualOptionTrade.id == trade_id).first()
if t:
db.delete(t)
db.commit()
return RedirectResponse("/dual-options/", status_code=303)
+3
View File
@@ -212,6 +212,9 @@
<a href="/options/" class="{% if active_nav == 'options' %}active{% endif %}"> <a href="/options/" class="{% if active_nav == 'options' %}active{% endif %}">
<span class="icon">📊</span> 期权交易 <span class="icon">📊</span> 期权交易
</a> </a>
<a href="/dual-options/" class="{% if active_nav == 'dual_options' %}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>
+419
View File
@@ -0,0 +1,419 @@
{% extends "base.html" %}
{% block title %}期权双买交易{% endblock %}
{% block heading %}期权双买交易{% endblock %}
{% block breadcrumb %}跨式策略{% endblock %}
{% block content %}
{% set view = request.query_params.get('view', '') %}
<style>
.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; }
</style>
<div class="tabs">
<a href="/dual-options/" class="tab-btn{% if view != 'closed' %} active{% endif %}">📋 持仓</a>
<a href="/dual-options/?view=closed" class="tab-btn{% if view == 'closed' %} active{% endif %}">📊 已平仓</a>
</div>
{% if view != 'closed' %}
<div class="tab-panel active">
{# ── 新建双买开仓 ── #}
<div class="section-title">新建双买开仓(同时买入看涨 + 看跌)</div>
<div class="form-card" style="margin-bottom:28px;max-width:100%;">
<form method="post" action="/dual-options/open">
<div style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;">
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:100px;">
<label>品种</label>
<select id="productSelect" required onchange="filterContracts()">
<option value="">--</option>
{% for pcode in product_contracts %}
<option value="{{ pcode }}">{{ pcode }}</option>
{% endfor %}
</select>
</div>
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:120px;">
<label>合约</label>
<select name="contract_code" id="contractSelect" required disabled>
<option value="">-- 先选品种 --</option>
</select>
</div>
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:90px;">
<label>行权价</label>
<input type="number" step="any" name="strike_price" required placeholder="1300" style="font-size:0.9rem;">
</div>
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:100px;">
<label>看涨开仓价</label>
<input type="number" step="any" name="call_open_price" required placeholder="C权利金" style="font-size:0.9rem;">
</div>
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:100px;">
<label>看跌开仓价</label>
<input type="number" step="any" name="put_open_price" required placeholder="P权利金" style="font-size:0.9rem;">
</div>
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:130px;">
<label>开仓日期</label>
<input type="date" name="open_date" value="{{ today }}" required style="font-size:0.85rem;">
</div>
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:80px;">
<label>C手续费</label>
<input type="number" step="any" name="call_open_fee" value="0" style="font-size:0.9rem;">
</div>
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:80px;">
<label>P手续费</label>
<input type="number" step="any" name="put_open_fee" value="0" style="font-size:0.9rem;">
</div>
<button type="submit" class="btn btn-primary" style="font-size:0.82rem;padding:8px 18px;">双买开仓</button>
</div>
</form>
</div>
{# ── 持仓列表 ── #}
<div class="section-title">持仓中 · {{ open_trades|length }} 笔</div>
{% if open_trades %}
<div class="table-wrap">
<table>
<tr><th>品种</th><th>合约</th><th>行权价</th><th>开仓日期</th><th>看涨开仓价</th><th>看跌开仓价</th><th>C手续费</th><th>P手续费</th><th>总成本</th><th>操作</th></tr>
{% for t in open_trades %}
<tr>
<td>{{ t.product_code }}</td>
<td><strong>{{ t.contract_code.replace(t.product_code, '', 1) }}</strong></td>
<td>{{ t.strike_price }}</td>
<td>{{ t.open_date }} {{ weekdays[t.open_date.weekday()] }}</td>
<td>{{ t.call_open_price }}</td>
<td>{{ t.put_open_price }}</td>
<td>{{ t.call_open_fee or 0 }}</td>
<td>{{ t.put_open_fee or 0 }}</td>
<td><b>{{ "%.0f"|format(t.total_cost) }}</b></td>
<td>
<button style="background:var(--accent);color:#fff;border:none;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;"
onclick="showCloseForm({{ t.id }})">平仓</button>
<button style="background:var(--success-bg);color:var(--success-fg);border:1px solid var(--success);padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;margin-left:8px;"
onclick="showEditForm({{ t.id }}, '{{ t.product_code }}', '{{ t.contract_code }}', {{ t.strike_price }}, {{ t.call_open_price }}, {{ t.put_open_price }}, {{ t.call_open_fee or 0 }}, {{ t.put_open_fee or 0 }}, '{{ t.open_date }}', null, null, null, null, null, 'open')">编辑</button>
<form method="post" action="/dual-options/{{ t.id }}/delete" style="display:inline;margin-left:8px;">
<button style="background:transparent;color:#dc2626;border:1px solid #ef4444;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;" onclick="confirmDelete(event, '确定删除此记录?', this.closest('form').action)">删除</button>
</form>
</td>
</tr>
{% endfor %}
</table>
</div>
{% else %}
<div style="text-align:center;padding:40px;color:var(--sub);">暂无持仓</div>
{% endif %}
{# ── 平仓弹窗 ── #}
<div id="closeFormOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:99;" onclick="hideCloseForm()"></div>
<div id="closeFormBox" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;z-index:100;width:400px;max-width:90vw;">
<h3 style="margin-bottom:16px;">双买平仓</h3>
<form method="post" id="closeForm">
<div class="form-group">
<label>平仓日期</label>
<input type="date" name="close_date" value="{{ today }}" required style="font-size:0.85rem;">
</div>
<div class="form-group">
<label>看涨平仓价</label>
<input type="number" step="any" name="call_close_price" required style="font-size:0.9rem;">
</div>
<div class="form-group">
<label>看跌平仓价</label>
<input type="number" step="any" name="put_close_price" required style="font-size:0.9rem;">
</div>
<div class="form-group">
<label>看涨手续费</label>
<input type="number" step="any" name="call_close_fee" value="0" style="font-size:0.9rem;">
</div>
<div class="form-group">
<label>看跌手续费</label>
<input type="number" step="any" name="put_close_fee" value="0" style="font-size:0.9rem;">
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button type="button" class="btn" style="background:var(--th-bg);color:var(--fg);" onclick="hideCloseForm()">取消</button>
<button type="submit" class="btn btn-primary">确认平仓</button>
</div>
</form>
</div>
</div>
{% else %}
<div class="tab-panel active">
{# ═══════════════ 已平仓 ═══════════════ #}
{% if closed_trades %}
<div class="section-title">已平仓 · {{ closed_trades|length }} 笔</div>
<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 closed_trades %}
<tr>
<td>{{ t.product_code }}</td>
<td><strong>{{ t.contract_code.replace(t.product_code, '', 1) }}</strong></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.call_open_price }} / {{ t.call_close_price }}</td>
<td>{{ t.put_open_price }} / {{ t.put_close_price }}</td>
<td>{{ "%.0f"|format(t.total_cost) }}</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);{% else %}color:var(--sub);{% endif %}">
{% if p > 0 %}+{% endif %}{{ p }}
</span>
<span style="color:var(--sub);cursor:pointer;font-size:0.75rem;margin-left:4px;"
onclick="showDualPnlDrawer('{{ t.product_code }} {{ t.contract_code.replace(t.product_code, '', 1) }} {{ t.strike_price }}', {{ t.strike_price }}, {{ t.call_open_price }}, {{ t.put_open_price }}, {{ t.call_close_price }}, {{ t.put_close_price }}, {{ t.call_open_fee or 0 }}, {{ t.put_open_fee or 0 }}, {{ t.call_close_fee or 0 }}, {{ t.put_close_fee or 0 }}, {{ p }}, {{ t.point_value }})" title="盈亏分析">&#9432;</span>
{% endif %}
</td>
<td>
<button style="background:var(--success-bg);color:var(--success-fg);border:1px solid var(--success);padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;"
onclick="showEditForm({{ t.id }}, '{{ t.product_code }}', '{{ t.contract_code }}', {{ t.strike_price }}, {{ t.call_open_price }}, {{ t.put_open_price }}, {{ t.call_open_fee or 0 }}, {{ t.put_open_fee or 0 }}, '{{ t.open_date }}', {{ t.call_close_price }}, {{ t.put_close_price }}, {{ t.call_close_fee or 0 }}, {{ t.put_close_fee or 0 }}, '{{ t.close_date }}', 'closed')">编辑</button>
<form method="post" action="/dual-options/{{ t.id }}/delete" style="display:inline;margin-left:8px;">
<button style="background:transparent;color:#dc2626;border:1px solid #ef4444;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;" onclick="confirmDelete(event, '确定删除此记录?', this.closest('form').action)">删除</button>
</form>
</td>
</tr>
{% endfor %}
</table>
</div>
{% set ns = namespace(total_pnl=0, total_open_fee=0, total_close_fee=0) %}
{% for t in closed_trades %}
{% set ns.total_pnl = ns.total_pnl + (t.pnl or 0) %}
{% set ns.total_open_fee = ns.total_open_fee + (t.call_open_fee or 0) + (t.put_open_fee or 0) %}
{% set ns.total_close_fee = ns.total_close_fee + (t.call_close_fee or 0) + (t.put_close_fee or 0) %}
{% endfor %}
<div style="display:flex;gap:24px;margin-bottom:24px;padding:12px 18px;background:var(--surface);border:1px solid var(--border);border-radius:8px;font-size:0.88rem;flex-wrap:wrap;">
<span>盈亏合计 <b style="{% if ns.total_pnl > 0 %}color:var(--success-fg);{% elif ns.total_pnl < 0 %}color:var(--danger-fg);{% endif %}">{% if ns.total_pnl > 0 %}+{% endif %}{{ '%.2f'|format(ns.total_pnl) }}</b></span>
<span style="color:var(--sub);">|</span>
<span>开仓手续费 <b>{{ '%.2f'|format(ns.total_open_fee) }}</b></span>
<span style="color:var(--sub);">|</span>
<span>平仓手续费 <b>{{ '%.2f'|format(ns.total_close_fee) }}</b></span>
<span style="color:var(--sub);">|</span>
<span>手续费合计 <b>{{ '%.2f'|format(ns.total_open_fee + ns.total_close_fee) }}</b></span>
</div>
{% else %}
<div style="text-align:center;padding:60px;color:var(--sub);">暂无已平仓记录</div>
{% endif %}
</div>
{% endif %}
{# ── 盈亏分析抽屉 ── #}
<div id="pnlOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:99;" onclick="hideDualPnlDrawer()"></div>
<div id="pnlBox" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;z-index:100;width:480px;max-width:90vw;max-height:90vh;overflow-y:auto;">
<h3 style="margin-bottom:4px;" id="pnlTitle"></h3>
<p style="font-size:0.8rem;color:var(--sub);margin-bottom:14px;" id="pnlFormula"></p>
<table class="calc-table" style="width:100%;border-collapse:collapse;font-size:0.84rem;margin-bottom:12px;" id="pnlTable"></table>
<div style="font-size:0.85rem;color:var(--fg);line-height:1.8;" id="pnlInsight"></div>
<button style="position:absolute;top:12px;right:16px;background:none;border:none;font-size:1.3rem;cursor:pointer;color:var(--sub);" onclick="hideDualPnlDrawer()">&times;</button>
</div>
{% if view != 'closed' %}
<script>
var productContracts = {{ product_contracts | tojson }};
function filterContracts() {
var prod = document.getElementById('productSelect').value;
var sel = document.getElementById('contractSelect');
sel.innerHTML = '<option value="">-- 选择合约 --</option>';
sel.disabled = !prod;
if (prod && productContracts[prod]) {
productContracts[prod].forEach(function(c) {
var display = c.startsWith(prod) ? c.substring(prod.length) : c;
sel.innerHTML += '<option value="' + c + '">' + display + '</option>';
});
}
}
function showCloseForm(tradeId) {
document.getElementById('closeForm').action = '/dual-options/' + tradeId + '/close';
document.getElementById('closeFormOverlay').style.display = 'block';
document.getElementById('closeFormBox').style.display = 'block';
}
function hideCloseForm() {
document.getElementById('closeFormOverlay').style.display = 'none';
document.getElementById('closeFormBox').style.display = 'none';
}
</script>
{% endif %}
{# ── 编辑弹窗 ── #}
<div id="editFormOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:98;" onclick="hideEditForm()"></div>
<div id="editFormBox" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;z-index:99;width:440px;max-width:90vw;max-height:90vh;overflow-y:auto;">
<h3 style="margin-bottom:16px;">编辑双买记录</h3>
<form method="post" id="editForm">
<div class="form-group">
<label>合约</label>
<input type="text" name="contract_code" id="editContract" required style="font-size:0.9rem;">
</div>
<div class="form-group">
<label>行权价</label>
<input type="number" step="any" name="strike_price" id="editStrike" required style="font-size:0.9rem;">
</div>
<div class="form-group">
<label>开仓日期</label>
<input type="date" name="open_date" id="editOpenDate" required style="font-size:0.85rem;">
</div>
<div class="form-group">
<label>看涨开仓价</label>
<input type="number" step="any" name="call_open_price" id="editCallOpenPrice" required style="font-size:0.9rem;">
</div>
<div class="form-group">
<label>看跌开仓价</label>
<input type="number" step="any" name="put_open_price" id="editPutOpenPrice" required style="font-size:0.9rem;">
</div>
<div class="form-group">
<label>看涨手续费</label>
<input type="number" step="any" name="call_open_fee" id="editCallOpenFee" value="0" style="font-size:0.9rem;">
</div>
<div class="form-group">
<label>看跌手续费</label>
<input type="number" step="any" name="put_open_fee" id="editPutOpenFee" value="0" style="font-size:0.9rem;">
</div>
<div id="editCloseFields" style="display:none;">
<div class="form-group">
<label>平仓日期</label>
<input type="date" name="close_date" id="editCloseDate" style="font-size:0.85rem;">
</div>
<div class="form-group">
<label>看涨平仓价</label>
<input type="number" step="any" name="call_close_price" id="editCallClosePrice" style="font-size:0.9rem;">
</div>
<div class="form-group">
<label>看跌平仓价</label>
<input type="number" step="any" name="put_close_price" id="editPutClosePrice" style="font-size:0.9rem;">
</div>
<div class="form-group">
<label>看涨平仓手续费</label>
<input type="number" step="any" name="call_close_fee" id="editCallCloseFee" value="0" style="font-size:0.9rem;">
</div>
<div class="form-group">
<label>看跌平仓手续费</label>
<input type="number" step="any" name="put_close_fee" id="editPutCloseFee" value="0" style="font-size:0.9rem;">
</div>
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;">
<button type="button" class="btn" style="background:var(--th-bg);color:var(--fg);" onclick="hideEditForm()">取消</button>
<button type="submit" class="btn btn-primary">保存</button>
</div>
</form>
</div>
{# ── 通用确认弹窗 ── #}
<div id="confirmOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:199;" onclick="hideConfirm()"></div>
<div id="confirmBox" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:28px 32px;z-index:200;text-align:center;max-width:90vw;">
<p id="confirmMsg" style="font-size:0.95rem;margin-bottom:20px;"></p>
<div style="display:flex;gap:10px;justify-content:center;">
<button class="btn" style="background:var(--th-bg);color:var(--fg);" onclick="hideConfirm()">取消</button>
<button class="btn" style="background:var(--danger);color:#fff;" id="confirmBtn">确认删除</button>
</div>
</div>
<script>
function showDualPnlDrawer(title, strikePrice, callOpen, putOpen, callClose, putClose, callOpenFee, putOpenFee, callCloseFee, putCloseFee, pnl, pointValue) {
document.getElementById('pnlTitle').textContent = title + ' 双买分析';
document.getElementById('pnlFormula').textContent = '行权价: ' + strikePrice + ' | 每点 ' + pointValue + ' 元';
var totalPremium = callOpen + putOpen;
var totalOpenFee = callOpenFee + putOpenFee;
var totalCloseFee = callCloseFee + putCloseFee;
var totalFee = totalOpenFee + totalCloseFee;
var premiumRmb = totalPremium * pointValue;
var totalCost = premiumRmb + totalOpenFee;
var beUpper = strikePrice + totalPremium;
var beLower = strikePrice - totalPremium;
var callPnl = (callClose - callOpen) * pointValue - callOpenFee - callCloseFee;
var putPnl = (putClose - putOpen) * pointValue - putOpenFee - putCloseFee;
var colorFn = function(v) {
if (v > 0) return 'color:var(--success-fg);';
if (v < 0) return 'color:var(--danger-fg);';
return 'color:var(--sub);';
};
var signed = function(v) { return (v > 0 ? '+' : '') + v.toFixed(2); };
var html = '<tr style="background:var(--th-bg);"><td>项目</td><td>计算</td><td style="text-align:right;">金额</td></tr>';
html += '<tr><td>总权利金(/点)</td><td>' + callOpen + ' + ' + putOpen + '</td><td style="text-align:right;">' + totalPremium.toFixed(2) + ' 点</td></tr>';
html += '<tr><td>总权利金(元)</td><td>' + totalPremium.toFixed(2) + ' × ' + pointValue + '</td><td style="text-align:right;">' + premiumRmb.toFixed(2) + '</td></tr>';
html += '<tr><td>开仓手续费</td><td>C:' + callOpenFee + ' + P:' + putOpenFee + '</td><td style="text-align:right;color:var(--danger-fg);">-' + totalOpenFee.toFixed(2) + '</td></tr>';
html += '<tr><td>平仓手续费</td><td>C:' + callCloseFee + ' + P:' + putCloseFee + '</td><td style="text-align:right;color:var(--danger-fg);">-' + totalCloseFee.toFixed(2) + '</td></tr>';
html += '<tr style="font-weight:600;"><td>总成本</td><td>' + premiumRmb.toFixed(2) + ' + ' + totalOpenFee.toFixed(2) + '</td><td style="text-align:right;">' + totalCost.toFixed(2) + '</td></tr>';
html += '<tr><td>盈亏平衡点(上)</td><td>' + strikePrice + ' + ' + totalPremium.toFixed(2) + '</td><td style="text-align:right;">' + beUpper.toFixed(2) + '</td></tr>';
html += '<tr><td>盈亏平衡点(下)</td><td>' + strikePrice + ' - ' + totalPremium.toFixed(2) + '</td><td style="text-align:right;">' + beLower.toFixed(2) + '</td></tr>';
html += '<tr style="background:var(--th-bg);"><td colspan="3"></td></tr>';
html += '<tr><td>看涨盈亏</td><td>(' + callClose + ' - ' + callOpen + ') × ' + pointValue + ' - ' + callOpenFee + ' - ' + callCloseFee + '</td><td style="text-align:right;font-weight:600;' + colorFn(callPnl) + '">' + signed(callPnl) + '</td></tr>';
html += '<tr><td>看跌盈亏</td><td>(' + putClose + ' - ' + putOpen + ') × ' + pointValue + ' - ' + putOpenFee + ' - ' + putCloseFee + '</td><td style="text-align:right;font-weight:600;' + colorFn(putPnl) + '">' + signed(putPnl) + '</td></tr>';
document.getElementById('pnlTable').innerHTML = html;
var insight = '<b style="' + colorFn(pnl) + '">组合盈亏 = ' + signed(pnl) + '</b><br>';
insight += '最大亏损 = ' + totalCost.toFixed(2) + ' 元(标的价格 = ' + strikePrice + ' 时)<br>';
insight += '盈利区间: 标的价格 &gt; ' + beUpper.toFixed(2) + ' 或 &lt; ' + beLower.toFixed(2);
document.getElementById('pnlInsight').innerHTML = insight;
document.getElementById('pnlOverlay').style.display = 'block';
document.getElementById('pnlBox').style.display = 'block';
}
function showEditForm(id, productCode, contractCode, strike, callOpen, putOpen, callOpenFee, putOpenFee, openDate, callClose, putClose, callCloseFee, putCloseFee, closeDate, status) {
document.getElementById('editForm').action = '/dual-options/' + id + '/edit';
document.getElementById('editContract').value = contractCode;
document.getElementById('editStrike').value = strike;
document.getElementById('editOpenDate').value = openDate;
document.getElementById('editCallOpenPrice').value = callOpen;
document.getElementById('editPutOpenPrice').value = putOpen;
document.getElementById('editCallOpenFee').value = callOpenFee || 0;
document.getElementById('editPutOpenFee').value = putOpenFee || 0;
if (status === 'closed' && closeDate) {
document.getElementById('editCloseFields').style.display = 'block';
document.getElementById('editCloseDate').value = closeDate;
document.getElementById('editCallClosePrice').value = callClose;
document.getElementById('editPutClosePrice').value = putClose;
document.getElementById('editCallCloseFee').value = callCloseFee || 0;
document.getElementById('editPutCloseFee').value = putCloseFee || 0;
} else {
document.getElementById('editCloseFields').style.display = 'none';
}
document.getElementById('editFormOverlay').style.display = 'block';
document.getElementById('editFormBox').style.display = 'block';
}
function hideEditForm() {
document.getElementById('editFormOverlay').style.display = 'none';
document.getElementById('editFormBox').style.display = 'none';
}
function hideDualPnlDrawer() {
document.getElementById('pnlOverlay').style.display = 'none';
document.getElementById('pnlBox').style.display = 'none';
}
function confirmDelete(e, msg, url) {
e.preventDefault();
document.getElementById('confirmMsg').textContent = msg;
document.getElementById('confirmBtn').onclick = function() {
var f = document.createElement('form');
f.method = 'POST';
f.action = url;
document.body.appendChild(f);
f.submit();
};
document.getElementById('confirmOverlay').style.display = 'block';
document.getElementById('confirmBox').style.display = 'block';
}
function hideConfirm() {
document.getElementById('confirmOverlay').style.display = 'none';
document.getElementById('confirmBox').style.display = 'none';
}
</script>
{% endblock %}
+4 -4
View File
@@ -69,7 +69,7 @@
<input type="number" step="any" name="strike_price" required placeholder="1300" style="font-size:0.9rem;"> <input type="number" step="any" name="strike_price" required placeholder="1300" style="font-size:0.9rem;">
</div> </div>
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:80px;"> <div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:80px;">
<label>权利金</label> <label>开仓价</label>
<input type="number" step="any" name="open_price" required placeholder="25" style="font-size:0.9rem;"> <input type="number" step="any" name="open_price" required placeholder="25" style="font-size:0.9rem;">
</div> </div>
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:130px;"> <div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:130px;">
@@ -91,7 +91,7 @@
{% if open_trades %} {% if open_trades %}
<div class="table-wrap"> <div class="table-wrap">
<table> <table>
<tr><th>品种</th><th>合约</th><th>类型</th><th>买卖</th><th>行权价</th><th>权利金</th><th>开仓日期</th><th>手续费</th><th>操作</th></tr> <tr><th>品种</th><th>合约</th><th>类型</th><th>买卖</th><th>行权价</th><th>开仓价</th><th>开仓日期</th><th>手续费</th><th>操作</th></tr>
{% for t in open_trades %} {% for t in open_trades %}
<tr> <tr>
<td>{{ t.product_code }}</td> <td>{{ t.product_code }}</td>
@@ -164,7 +164,7 @@
<div class="section-title">已平仓 · {{ closed_trades|length }} 笔</div> <div class="section-title">已平仓 · {{ closed_trades|length }} 笔</div>
<div class="table-wrap"> <div class="table-wrap">
<table> <table>
<tr><th>品种</th><th>合约</th><th>类型</th><th>买卖</th><th>行权价</th><th>开仓</th><th>平仓</th><th>持仓</th><th>权利金</th><th>平仓价</th><th>开仓费</th><th>平仓费</th><th>盈亏</th><th>操作</th></tr> <tr><th>品种</th><th>合约</th><th>类型</th><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 closed_trades %} {% for t in closed_trades %}
<tr> <tr>
<td>{{ t.product_code }}</td> <td>{{ t.product_code }}</td>
@@ -295,7 +295,7 @@ function hideCloseForm() {
<input type="date" name="open_date" id="editOpenDate" required style="font-size:0.85rem;"> <input type="date" name="open_date" id="editOpenDate" required style="font-size:0.85rem;">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>权利金</label> <label>开仓价</label>
<input type="number" step="any" name="open_price" id="editOpenPrice" required style="font-size:0.9rem;"> <input type="number" step="any" name="open_price" id="editOpenPrice" required style="font-size:0.9rem;">
</div> </div>
<div class="form-group"> <div class="form-group">