Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06f7fc930a | |||
| 1e6589a39f | |||
| 7bc85b2528 | |||
| 9bab06c1e7 | |||
| fb5cc801ee | |||
| d87bb00208 | |||
| 0684f15859 | |||
| c2a6b99fbd | |||
| c05a1b6c7f | |||
| 8b5fa347cb | |||
| b22123bdfc | |||
| 8076cbbd51 | |||
| c8ed098bfc |
+2
-1
@@ -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
|
from app.routers import contracts, admin, auth, positions, trades, option_trades
|
||||||
|
|
||||||
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
||||||
|
|
||||||
@@ -58,6 +58,7 @@ app.include_router(contracts.router)
|
|||||||
app.include_router(admin.router)
|
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.get("/")
|
@app.get("/")
|
||||||
|
|||||||
@@ -146,4 +146,33 @@ class Trade(Base):
|
|||||||
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)
|
||||||
else:
|
else:
|
||||||
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)
|
||||||
|
return round(result, 2)
|
||||||
|
|
||||||
|
|
||||||
|
class OptionTrade(Base):
|
||||||
|
__tablename__ = "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)
|
||||||
|
option_type: Mapped[str] = mapped_column(String(4))
|
||||||
|
direction: Mapped[str] = mapped_column(String(4))
|
||||||
|
strike_price: Mapped[float] = mapped_column(Float)
|
||||||
|
open_date: Mapped[date] = mapped_column(Date)
|
||||||
|
open_price: Mapped[float] = mapped_column(Float)
|
||||||
|
open_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0)
|
||||||
|
close_date: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||||
|
close_price: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||||
|
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.close_price is None:
|
||||||
|
return None
|
||||||
|
mul = 20 # glass futures point value
|
||||||
|
if self.direction == "sell":
|
||||||
|
result = (self.open_price - self.close_price) * mul - (self.open_fee or 0) - (self.close_fee or 0)
|
||||||
|
else:
|
||||||
|
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)
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
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 OptionTrade, Contract, Product
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/options", tags=["options"])
|
||||||
|
|
||||||
|
OPTION_TYPES = [("C", "C 看涨"), ("P", "P 看跌")]
|
||||||
|
OPTION_DIRECTIONS = [("buy", "买"), ("sell", "卖")]
|
||||||
|
|
||||||
|
|
||||||
|
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 option_page(request: Request, db: Session = Depends(get_db)):
|
||||||
|
view = request.query_params.get("view", "")
|
||||||
|
open_trades = (
|
||||||
|
db.query(OptionTrade).filter(OptionTrade.status == "open")
|
||||||
|
.order_by(OptionTrade.open_date.desc()).all()
|
||||||
|
)
|
||||||
|
closed_trades = (
|
||||||
|
db.query(OptionTrade).filter(OptionTrade.status == "closed")
|
||||||
|
.order_by(OptionTrade.close_date.desc()).limit(50).all()
|
||||||
|
)
|
||||||
|
|
||||||
|
template = request.app.state.templates.get_template("options.html")
|
||||||
|
return HTMLResponse(
|
||||||
|
template.render(
|
||||||
|
request=request,
|
||||||
|
active_nav="options",
|
||||||
|
product_contracts=get_product_contracts(db),
|
||||||
|
option_types=OPTION_TYPES,
|
||||||
|
option_directions=OPTION_DIRECTIONS,
|
||||||
|
open_trades=open_trades,
|
||||||
|
closed_trades=closed_trades,
|
||||||
|
today=today_str(),
|
||||||
|
weekdays=["周一","周二","周三","周四","周五","周六","周日"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/open")
|
||||||
|
def open_trade(
|
||||||
|
request: Request,
|
||||||
|
contract_code: str = Form(...),
|
||||||
|
option_type: str = Form(...),
|
||||||
|
direction: str = Form(...),
|
||||||
|
strike_price: float = Form(...),
|
||||||
|
open_date: str = Form(...),
|
||||||
|
open_price: float = Form(...),
|
||||||
|
open_fee: float = Form(0.0),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
code = contract_code.upper()
|
||||||
|
contract = db.query(Contract).filter(Contract.code == code).first()
|
||||||
|
product_code = contract.product.code if contract else code[:2]
|
||||||
|
|
||||||
|
t = OptionTrade(
|
||||||
|
product_code=product_code,
|
||||||
|
contract_code=code,
|
||||||
|
option_type=option_type,
|
||||||
|
direction=direction,
|
||||||
|
strike_price=strike_price,
|
||||||
|
open_date=date.fromisoformat(open_date),
|
||||||
|
open_price=open_price,
|
||||||
|
open_fee=open_fee,
|
||||||
|
status="open",
|
||||||
|
)
|
||||||
|
db.add(t)
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/options/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{trade_id}/close")
|
||||||
|
def close_trade(
|
||||||
|
request: Request,
|
||||||
|
trade_id: int,
|
||||||
|
close_date: str = Form(...),
|
||||||
|
close_price: float = Form(...),
|
||||||
|
close_fee: float = Form(0.0),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
t = db.query(OptionTrade).filter(OptionTrade.id == trade_id).first()
|
||||||
|
if t and t.status == "open":
|
||||||
|
t.close_date = date.fromisoformat(close_date)
|
||||||
|
t.close_price = close_price
|
||||||
|
t.close_fee = close_fee
|
||||||
|
t.status = "closed"
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/options/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{trade_id}/edit")
|
||||||
|
def edit_trade(
|
||||||
|
request: Request,
|
||||||
|
trade_id: int,
|
||||||
|
contract_code: str = Form(...),
|
||||||
|
option_type: str = Form(...),
|
||||||
|
direction: str = Form(...),
|
||||||
|
strike_price: float = Form(...),
|
||||||
|
open_date: str = Form(...),
|
||||||
|
open_price: float = Form(...),
|
||||||
|
open_fee: float = Form(0.0),
|
||||||
|
close_date: str = Form(""),
|
||||||
|
close_price: float = Form(None),
|
||||||
|
close_fee: float = Form(None),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
t = db.query(OptionTrade).filter(OptionTrade.id == trade_id).first()
|
||||||
|
if t:
|
||||||
|
code = contract_code.upper()
|
||||||
|
contract = db.query(Contract).filter(Contract.code == code).first()
|
||||||
|
t.product_code = contract.product.code if contract else code[:2]
|
||||||
|
t.contract_code = code
|
||||||
|
t.option_type = option_type
|
||||||
|
t.direction = direction
|
||||||
|
t.strike_price = strike_price
|
||||||
|
t.open_date = date.fromisoformat(open_date)
|
||||||
|
t.open_price = open_price
|
||||||
|
t.open_fee = open_fee
|
||||||
|
if close_date and close_price is not None:
|
||||||
|
t.close_date = date.fromisoformat(close_date)
|
||||||
|
t.close_price = close_price
|
||||||
|
t.close_fee = close_fee or 0
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/options/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{trade_id}/delete")
|
||||||
|
def delete_trade(trade_id: int, db: Session = Depends(get_db)):
|
||||||
|
t = db.query(OptionTrade).filter(OptionTrade.id == trade_id).first()
|
||||||
|
if t:
|
||||||
|
db.delete(t)
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/options/", status_code=303)
|
||||||
@@ -57,6 +57,7 @@ def trades_page(request: Request, db: Session = Depends(get_db)):
|
|||||||
open_trades=open_trades,
|
open_trades=open_trades,
|
||||||
closed_trades=closed_trades,
|
closed_trades=closed_trades,
|
||||||
today=today_str(),
|
today=today_str(),
|
||||||
|
weekdays=["周一","周二","周三","周四","周五","周六","周日"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -108,6 +109,38 @@ def close_trade(
|
|||||||
return RedirectResponse("/trades/", status_code=303)
|
return RedirectResponse("/trades/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{trade_id}/edit")
|
||||||
|
def edit_trade(
|
||||||
|
request: Request,
|
||||||
|
trade_id: int,
|
||||||
|
contract_code: str = Form(...),
|
||||||
|
direction: str = Form(...),
|
||||||
|
open_date: str = Form(...),
|
||||||
|
open_price: float = Form(...),
|
||||||
|
open_fee: float = Form(0.0),
|
||||||
|
close_date: str = Form(""),
|
||||||
|
close_price: float = Form(None),
|
||||||
|
close_fee: float = Form(None),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
t = db.query(Trade).filter(Trade.id == trade_id).first()
|
||||||
|
if t:
|
||||||
|
code = contract_code.upper()
|
||||||
|
contract = db.query(Contract).filter(Contract.code == code).first()
|
||||||
|
t.product_code = contract.product.code if contract else code[:2]
|
||||||
|
t.contract_code = code
|
||||||
|
t.direction = direction
|
||||||
|
t.open_date = date.fromisoformat(open_date)
|
||||||
|
t.open_price = open_price
|
||||||
|
t.open_fee = open_fee
|
||||||
|
if close_date and close_price is not None:
|
||||||
|
t.close_date = date.fromisoformat(close_date)
|
||||||
|
t.close_price = close_price
|
||||||
|
t.close_fee = close_fee or 0
|
||||||
|
db.commit()
|
||||||
|
return RedirectResponse("/trades/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{trade_id}/delete")
|
@router.post("/{trade_id}/delete")
|
||||||
def delete_trade(trade_id: int, db: Session = Depends(get_db)):
|
def delete_trade(trade_id: int, db: Session = Depends(get_db)):
|
||||||
t = db.query(Trade).filter(Trade.id == trade_id).first()
|
t = db.query(Trade).filter(Trade.id == trade_id).first()
|
||||||
|
|||||||
@@ -133,8 +133,8 @@
|
|||||||
|
|
||||||
/* ── Badges ── */
|
/* ── Badges ── */
|
||||||
.badge { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: 0.78rem; font-weight: 600; }
|
.badge { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: 0.78rem; font-weight: 600; }
|
||||||
.badge-up { background: var(--success-bg); color: var(--success-fg); }
|
.badge-up { background: var(--danger-bg); color: var(--danger-fg); }
|
||||||
.badge-down { background: var(--danger-bg); color: var(--danger-fg); }
|
.badge-down { background: var(--success-bg); color: var(--success-fg); }
|
||||||
.badge-warn { background: var(--warn-bg); color: var(--warn-fg); }
|
.badge-warn { background: var(--warn-bg); color: var(--warn-fg); }
|
||||||
|
|
||||||
/* ── Forms ── */
|
/* ── Forms ── */
|
||||||
@@ -209,6 +209,9 @@
|
|||||||
<a href="/trades/" class="{% if active_nav == 'trades' %}active{% endif %}">
|
<a href="/trades/" class="{% if active_nav == 'trades' %}active{% endif %}">
|
||||||
<span class="icon">📝</span> 交易记录
|
<span class="icon">📝</span> 交易记录
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/options/" class="{% if active_nav == '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>
|
||||||
|
|||||||
@@ -0,0 +1,415 @@
|
|||||||
|
{% 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="/options/" class="tab-btn{% if view != 'closed' %} active{% endif %}">📋 持仓</a>
|
||||||
|
<a href="/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="/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:70px;">
|
||||||
|
<label>类型</label>
|
||||||
|
<select name="option_type" required>
|
||||||
|
{% for v, label in option_types %}
|
||||||
|
<option value="{{ v }}">{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:60px;">
|
||||||
|
<label>买卖</label>
|
||||||
|
<select name="direction" required>
|
||||||
|
{% for v, label in option_directions %}
|
||||||
|
<option value="{{ v }}">{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</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:80px;">
|
||||||
|
<label>权利金</label>
|
||||||
|
<input type="number" step="any" name="open_price" required placeholder="25" 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>手续费</label>
|
||||||
|
<input type="number" step="any" name="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>开仓日期</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>
|
||||||
|
{% 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 == 'sell' %}
|
||||||
|
<span class="badge badge-down">卖</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-up">买</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ t.strike_price }}</td>
|
||||||
|
<td>{{ t.open_price }}</td>
|
||||||
|
<td>{{ t.open_date }} {{ weekdays[t.open_date.weekday()] }}</td>
|
||||||
|
<td>{{ t.open_fee or 0 }}</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('option', {{ t.id }}, '{{ t.product_code }}', '{{ t.contract_code }}', '{{ t.option_type }}', '{{ t.direction }}', {{ t.strike_price }}, {{ t.open_price }}, {{ t.open_fee or 0 }}, '{{ t.open_date }}', null, null, null, '{{ t.status }}')">编辑</button>
|
||||||
|
<form method="post" action="/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:360px;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="close_price" required style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>手续费</label>
|
||||||
|
<input type="number" step="any" name="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><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>
|
||||||
|
{% 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 == 'sell' %}
|
||||||
|
<span class="badge badge-down">卖</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-up">买</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>{{ t.open_fee or 0 }}</td>
|
||||||
|
<td>{{ t.close_fee or 0 }}</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="showPnlDrawer('{{ t.product_code }} {{ t.contract_code.replace(t.product_code, '', 1) }} {{ t.option_type }}', '{{ t.direction }}', {{ t.open_price }}, {{ t.close_price }}, {{ t.open_fee or 0 }}, {{ t.close_fee or 0 }}, {{ p }})" title="计算过程">ⓘ</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;margin-left:8px;"
|
||||||
|
onclick="showEditForm('option', {{ t.id }}, '{{ t.product_code }}', '{{ t.contract_code }}', '{{ t.option_type }}', '{{ t.direction }}', {{ t.strike_price }}, {{ t.open_price }}, {{ t.open_fee or 0 }}, '{{ t.open_date }}', {{ t.close_price }}, {{ t.close_fee or 0 }}, '{{ t.close_date }}', '{{ t.status }}')">编辑</button>
|
||||||
|
<form method="post" action="/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.open_fee or 0) %}
|
||||||
|
{% set ns.total_close_fee = ns.total_close_fee + (t.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 %}
|
||||||
|
|
||||||
|
{% 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 = '/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:400px;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>
|
||||||
|
<select name="option_type" id="editOptionType" required>
|
||||||
|
{% for v, label in option_types %}
|
||||||
|
<option value="{{ v }}">{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>买卖</label>
|
||||||
|
<select name="direction" id="editDirection" required>
|
||||||
|
{% for v, label in option_directions %}
|
||||||
|
<option value="{{ v }}">{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</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="open_price" id="editOpenPrice" required style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>手续费</label>
|
||||||
|
<input type="number" step="any" name="open_fee" id="editOpenFee" 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="close_price" id="editClosePrice" style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>手续费</label>
|
||||||
|
<input type="number" step="any" name="close_fee" id="editCloseFee" 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="pnlOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:99;" onclick="hidePnlDrawer()"></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:380px;max-width:90vw;">
|
||||||
|
<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:1rem;" id="pnlResult"></div>
|
||||||
|
<button style="position:absolute;top:12px;right:16px;background:none;border:none;font-size:1.3rem;cursor:pointer;color:var(--sub);" onclick="hidePnlDrawer()">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showPnlDrawer(title, direction, openPrice, closePrice, openFee, closeFee, result) {
|
||||||
|
document.getElementById('pnlTitle').textContent = title;
|
||||||
|
var dirLabel = direction === 'sell' ? '卖' : '买';
|
||||||
|
document.getElementById('pnlFormula').textContent = '方向: ' + dirLabel + ' | 每点 20 元';
|
||||||
|
var diff = direction === 'sell' ? (openPrice - closePrice) : (closePrice - openPrice);
|
||||||
|
var gross = diff * 20;
|
||||||
|
var fees = openFee + closeFee;
|
||||||
|
var html = '<tr style="background:var(--th-bg);"><td>项目</td><td>计算</td><td style="text-align:right;">金额</td></tr>';
|
||||||
|
html += '<tr><td>价差</td><td>' + (direction === 'sell' ? openPrice + ' - ' + closePrice : closePrice + ' - ' + openPrice) + '</td><td style="text-align:right;">' + diff.toFixed(2) + ' 点</td></tr>';
|
||||||
|
html += '<tr><td>毛利</td><td>' + diff.toFixed(2) + ' × 20</td><td style="text-align:right;">' + gross.toFixed(2) + '</td></tr>';
|
||||||
|
html += '<tr><td>开仓手续费</td><td></td><td style="text-align:right;color:var(--danger-fg);">-' + openFee.toFixed(2) + '</td></tr>';
|
||||||
|
html += '<tr><td>平仓手续费</td><td></td><td style="text-align:right;color:var(--danger-fg);">-' + closeFee.toFixed(2) + '</td></tr>';
|
||||||
|
document.getElementById('pnlTable').innerHTML = html;
|
||||||
|
document.getElementById('pnlResult').innerHTML = '<b>盈亏 = ' + gross.toFixed(2) + ' - ' + openFee.toFixed(2) + ' - ' + closeFee.toFixed(2) + ' = <span style="color:' + (result > 0 ? 'var(--success-fg)' : result < 0 ? 'var(--danger-fg)' : 'var(--sub)') + ';">' + (result > 0 ? '+' : '') + result.toFixed(2) + '</span></b>';
|
||||||
|
document.getElementById('pnlOverlay').style.display = 'block';
|
||||||
|
document.getElementById('pnlBox').style.display = 'block';
|
||||||
|
}
|
||||||
|
function showEditForm(type, id, productCode, contractCode, optionType, direction, strike, openPrice, openFee, openDate, closePrice, closeFee, closeDate, status) {
|
||||||
|
document.getElementById('editForm').action = '/' + type + 's/' + id + '/edit';
|
||||||
|
document.getElementById('editContract').value = contractCode;
|
||||||
|
document.getElementById('editOptionType').value = optionType;
|
||||||
|
document.getElementById('editDirection').value = direction;
|
||||||
|
document.getElementById('editStrike').value = strike;
|
||||||
|
document.getElementById('editOpenDate').value = openDate;
|
||||||
|
document.getElementById('editOpenPrice').value = openPrice;
|
||||||
|
document.getElementById('editOpenFee').value = openFee || 0;
|
||||||
|
if (status === 'closed' && closeDate) {
|
||||||
|
document.getElementById('editCloseFields').style.display = 'block';
|
||||||
|
document.getElementById('editCloseDate').value = closeDate;
|
||||||
|
document.getElementById('editClosePrice').value = closePrice;
|
||||||
|
document.getElementById('editCloseFee').value = closeFee || 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 hidePnlDrawer() {
|
||||||
|
document.getElementById('pnlOverlay').style.display = 'none';
|
||||||
|
document.getElementById('pnlBox').style.display = 'none';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<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 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 %}
|
||||||
@@ -65,14 +65,14 @@
|
|||||||
<span style="margin-left:auto;display:flex;gap:8px;">
|
<span style="margin-left:auto;display:flex;gap:8px;">
|
||||||
<form method="post" action="/positions/{{ round.id }}/close" style="display:inline;">
|
<form method="post" action="/positions/{{ round.id }}/close" style="display:inline;">
|
||||||
<input type="hidden" name="result" value="profit_taken">
|
<input type="hidden" name="result" value="profit_taken">
|
||||||
<button class="btn" style="background:var(--success);color:#fff;font-size:0.78rem;padding:4px 14px;" onclick="return confirm('确认 {{ round.contract_code }} 止盈清仓?')">止盈</button>
|
<button class="btn" style="background:var(--success);color:#fff;font-size:0.78rem;padding:4px 14px;" onclick="confirmDelete(event, '确认 {{ round.contract_code }} 止盈清仓?', this.closest('form').action)">止盈</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="post" action="/positions/{{ round.id }}/close" style="display:inline;">
|
<form method="post" action="/positions/{{ round.id }}/close" style="display:inline;">
|
||||||
<input type="hidden" name="result" value="meltdown">
|
<input type="hidden" name="result" value="meltdown">
|
||||||
<button class="btn" style="background:var(--danger);color:#fff;font-size:0.78rem;padding:4px 14px;" onclick="return confirm('确认 {{ round.contract_code }} 熔断清仓?')">熔断</button>
|
<button class="btn" style="background:var(--danger);color:#fff;font-size:0.78rem;padding:4px 14px;" onclick="confirmDelete(event, '确认 {{ round.contract_code }} 熔断清仓?', this.closest('form').action)">熔断</button>
|
||||||
</form>
|
</form>
|
||||||
<form method="post" action="/positions/{{ round.id }}/delete" style="display:inline;">
|
<form method="post" action="/positions/{{ round.id }}/delete" style="display:inline;">
|
||||||
<button style="background:none;border:none;color:var(--na);cursor:pointer;font-size:0.78rem;" onclick="return confirm('确认删除 {{ round.contract_code }} 整轮数据?')">删除</button>
|
<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, '确定删除 {{ round.contract_code }} 整轮数据?', this.closest('form').action)">删除</button>
|
||||||
</form>
|
</form>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -182,4 +182,33 @@ function filterContracts() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
|
||||||
|
<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 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 %}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
{% block title %}交易记录{% endblock %}
|
{% block title %}交易记录{% endblock %}
|
||||||
{% block heading %}交易记录{% endblock %}
|
{% block heading %}交易记录{% endblock %}
|
||||||
{% block breadcrumb %}开平仓记录{% endblock %}
|
{% block breadcrumb %}开平仓记录{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{% set view = request.query_params.get('view', '') %}
|
{% set view = request.query_params.get('view', '') %}
|
||||||
@@ -92,14 +91,16 @@
|
|||||||
<span class="badge badge-up">多</span>
|
<span class="badge badge-up">多</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ t.open_date }}</td>
|
<td>{{ t.open_date }} {{ weekdays[t.open_date.weekday()] }}</td>
|
||||||
<td>{{ t.open_price }}</td>
|
<td>{{ t.open_price }}</td>
|
||||||
<td>{{ t.open_fee or 0 }}</td>
|
<td>{{ t.open_fee or 0 }}</td>
|
||||||
<td>
|
<td>
|
||||||
<button style="background:var(--accent);color:#fff;border:none;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;"
|
<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>
|
onclick="showCloseForm({{ t.id }})">平仓</button>
|
||||||
<form method="post" action="/trades/{{ t.id }}/delete" style="display:inline;margin-left:8px;">
|
<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;"
|
||||||
<button style="background:var(--danger-bg);color:var(--danger-fg);border:1px solid var(--danger);padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;" onclick="return confirm('删除此记录?')">删除</button>
|
onclick="showEditForm('trade', {{ t.id }}, '{{ t.product_code }}', '{{ t.contract_code }}', '{{ t.direction }}', {{ t.open_price }}, {{ t.open_fee or 0 }}, '{{ t.open_date }}', null, null, null, '{{ t.status }}')">编辑</button>
|
||||||
|
<form method="post" action="/trades/{{ 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>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -143,7 +144,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></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></tr>
|
||||||
{% for t in closed_trades %}
|
{% for t in closed_trades %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ t.product_code }}</td>
|
<td>{{ t.product_code }}</td>
|
||||||
@@ -155,8 +156,9 @@
|
|||||||
<span class="badge badge-up">多</span>
|
<span class="badge badge-up">多</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td style="font-size:0.8rem;">{{ t.open_date }}</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 }}</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.open_price }}</td>
|
||||||
<td>{{ t.close_price }}</td>
|
<td>{{ t.close_price }}</td>
|
||||||
<td>{{ t.open_fee or 0 }}</td>
|
<td>{{ t.open_fee or 0 }}</td>
|
||||||
@@ -167,11 +169,15 @@
|
|||||||
<span style="font-weight:700;{% if p > 0 %}color:var(--success-fg);{% elif p < 0 %}color:var(--danger-fg);{% else %}color:var(--sub);{% endif %}">
|
<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 }}
|
{% if p > 0 %}+{% endif %}{{ p }}
|
||||||
</span>
|
</span>
|
||||||
|
<span style="color:var(--sub);cursor:pointer;font-size:0.75rem;margin-left:4px;"
|
||||||
|
onclick="showPnlDrawer('{{ t.product_code }} {{ t.contract_code.replace(t.product_code, '', 1) }}', '{{ t.direction }}', {{ t.open_price }}, {{ t.close_price }}, {{ t.open_fee or 0 }}, {{ t.close_fee or 0 }}, {{ p }})" title="计算过程">ⓘ</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<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;margin-left:8px;"
|
||||||
|
onclick="showEditForm('trade', {{ t.id }}, '{{ t.product_code }}', '{{ t.contract_code }}', '{{ t.direction }}', {{ t.open_price }}, {{ t.open_fee or 0 }}, '{{ t.open_date }}', {{ t.close_price }}, {{ t.close_fee or 0 }}, '{{ t.close_date }}', '{{ t.status }}')">编辑</button>
|
||||||
<form method="post" action="/trades/{{ t.id }}/delete" style="display:inline;margin-left:8px;">
|
<form method="post" action="/trades/{{ t.id }}/delete" style="display:inline;margin-left:8px;">
|
||||||
<button style="background:var(--danger-bg);color:var(--danger-fg);border:1px solid var(--danger);padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;" onclick="return confirm('删除此记录?')">删除</button>
|
<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>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -227,4 +233,141 @@ function hideCloseForm() {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
|
||||||
|
<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:400px;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>
|
||||||
|
<select name="direction" id="editDirection" required>
|
||||||
|
{% for v, label in directions %}
|
||||||
|
<option value="{{ v }}">{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</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="open_price" id="editOpenPrice" required style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>开仓手续费</label>
|
||||||
|
<input type="number" step="any" name="open_fee" id="editOpenFee" 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="close_price" id="editClosePrice" style="font-size:0.9rem;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>平仓手续费</label>
|
||||||
|
<input type="number" step="any" name="close_fee" id="editCloseFee" 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="pnlOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:99;" onclick="hidePnlDrawer()"></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:380px;max-width:90vw;">
|
||||||
|
<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:1rem;" id="pnlResult"></div>
|
||||||
|
<button style="position:absolute;top:12px;right:16px;background:none;border:none;font-size:1.3rem;cursor:pointer;color:var(--sub);" onclick="hidePnlDrawer()">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function showPnlDrawer(title, direction, openPrice, closePrice, openFee, closeFee, result) {
|
||||||
|
document.getElementById('pnlTitle').textContent = title;
|
||||||
|
var dirLabel = direction === 'short' ? '空' : '多';
|
||||||
|
document.getElementById('pnlFormula').textContent = '方向: ' + dirLabel + ' | 每点 20 元';
|
||||||
|
var diff = direction === 'short' ? (openPrice - closePrice) : (closePrice - openPrice);
|
||||||
|
var gross = diff * 20;
|
||||||
|
var fees = openFee + closeFee;
|
||||||
|
var html = '<tr style="background:var(--th-bg);"><td>项目</td><td>计算</td><td style="text-align:right;">金额</td></tr>';
|
||||||
|
html += '<tr><td>价差</td><td>' + (direction === 'short' ? openPrice + ' - ' + closePrice : closePrice + ' - ' + openPrice) + '</td><td style="text-align:right;">' + diff.toFixed(2) + ' 点</td></tr>';
|
||||||
|
html += '<tr><td>毛利</td><td>' + diff.toFixed(2) + ' × 20</td><td style="text-align:right;">' + gross.toFixed(2) + '</td></tr>';
|
||||||
|
html += '<tr><td>开仓手续费</td><td></td><td style="text-align:right;color:var(--danger-fg);">-' + openFee.toFixed(2) + '</td></tr>';
|
||||||
|
html += '<tr><td>平仓手续费</td><td></td><td style="text-align:right;color:var(--danger-fg);">-' + closeFee.toFixed(2) + '</td></tr>';
|
||||||
|
document.getElementById('pnlTable').innerHTML = html;
|
||||||
|
document.getElementById('pnlResult').innerHTML = '<b>盈亏 = ' + gross.toFixed(2) + ' - ' + openFee.toFixed(2) + ' - ' + closeFee.toFixed(2) + ' = <span style="color:' + (result > 0 ? 'var(--success-fg)' : result < 0 ? 'var(--danger-fg)' : 'var(--sub)') + ';">' + (result > 0 ? '+' : '') + result.toFixed(2) + '</span></b>';
|
||||||
|
document.getElementById('pnlOverlay').style.display = 'block';
|
||||||
|
document.getElementById('pnlBox').style.display = 'block';
|
||||||
|
}
|
||||||
|
function hidePnlDrawer() {
|
||||||
|
document.getElementById('pnlOverlay').style.display = 'none';
|
||||||
|
document.getElementById('pnlBox').style.display = 'none';
|
||||||
|
}
|
||||||
|
function showEditForm(type, id, productCode, contractCode, direction, openPrice, openFee, openDate, closePrice, closeFee, closeDate, status) {
|
||||||
|
document.getElementById('editForm').action = '/' + type + 's/' + id + '/edit';
|
||||||
|
document.getElementById('editContract').value = contractCode;
|
||||||
|
document.getElementById('editDirection').value = direction;
|
||||||
|
document.getElementById('editOpenDate').value = openDate;
|
||||||
|
document.getElementById('editOpenPrice').value = openPrice;
|
||||||
|
document.getElementById('editOpenFee').value = openFee || 0;
|
||||||
|
if (status === 'closed' && closeDate) {
|
||||||
|
document.getElementById('editCloseFields').style.display = 'block';
|
||||||
|
document.getElementById('editCloseDate').value = closeDate;
|
||||||
|
document.getElementById('editClosePrice').value = closePrice;
|
||||||
|
document.getElementById('editCloseFee').value = closeFee || 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';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<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 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 %}
|
||||||
Binary file not shown.
Reference in New Issue
Block a user