Compare commits

..

41 Commits

Author SHA1 Message Date
fish 9c04ad642e 更新数据库文件
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 18:17:51 +08:00
fish 0d0bfb8c6f 轮次仓位表格新增行号
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 18:14:06 +08:00
fish a0ab47040d 添加仓位手数字段改为只读,取轮次每日手数
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 18:12:34 +08:00
fish ef9238041b 止盈阈值改为A×跳点价×每日手数
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 18:10:28 +08:00
fish 6afec84a2d 交易记录已平仓底部统计新增手续费合计
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 15:38:41 +08:00
fish 51792ce4b3 全胜时盈亏比显示∞
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 15:32:34 +08:00
fish 6609b39f7b 平仓汇总新增盈亏比数据
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 15:31:16 +08:00
fish 0d01faf464 平仓汇总按原始价差计算毛利和净利,红涨绿跌配色
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 15:30:04 +08:00
fish 7ac9de3663 新增平仓汇总页面,按期货/期权/双买分类统计盈亏和胜率
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 15:16:56 +08:00
fish d28ab148f6 更新数据库文件
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 15:07:43 +08:00
fish c109598e4e 合约管理支持按交易所筛选品种;品种列表按交易所分组展示
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 14:59:19 +08:00
fish 3d2c8e13db 品种管理交易所字段支持中英双语展示,加宽表单面板
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 14:54:49 +08:00
fish 54ce2c97ce 更新数据库文件
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 14:43:56 +08:00
fish fcaf2dc2d4 双买持仓页面新增分析按钮,展示盈亏平衡点和盈利区间
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 14:42:39 +08:00
fish 39c5e22ed6 双买交易行权价拆分为看涨行权价和看跌行权价,支持不同价位
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 14:37:53 +08:00
fish af3e359d24 期权交易"权利金"改为"开仓价";新增期权双买交易功能,支持跨式策略开平仓及组合盈亏分析
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 14:24:49 +08:00
fish 6228c139ef 更新数据库文件
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 10:10:49 +08:00
fish 6c66edb507 无持仓的品种tab不展示全平按钮
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 10:08:43 +08:00
fish 662deacf04 全平按钮右对齐
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 10:07:44 +08:00
fish 014a8de964 持仓页面品种tab增加全平按钮,支持按品种批量平仓
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 10:06:58 +08:00
fish 9e566935d5 PNL弹窗毛利金额按正负显示红绿色及正负号
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 10:02:57 +08:00
fish a25cfb906a PNL弹窗手续费金额颜色改为绿色
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 10:00:56 +08:00
fish ca30d05ebf PNL弹窗底部盈亏文案改为收益
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 09:59:43 +08:00
fish 5f4df50fe7 已平仓表格盈亏列标题改为收益
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 09:58:21 +08:00
fish 41c149a6ee 调整平仓盈亏列位置至平仓价与开仓费之间
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 09:57:12 +08:00
fish 0fcc7eea03 已平仓记录增加平仓盈亏列,展示价差毛利
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 09:56:03 +08:00
fish 621ee8c32d 已平仓底部统计改为平仓盈亏、开平手续费、收益四个指标
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 09:54:59 +08:00
fish 833491b8d0 品种增加每点价值字段,PNL计算支持不同品种差异化配置
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 09:49:37 +08:00
fish 5018eb2083 已平仓页面切换品种时底部统计跟随当前品种汇总
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 09:45:55 +08:00
fish 50328f49ce 修复止盈确认弹窗缺少参数导致的报错,已平仓页面按品种分组展示
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-28 09:44:42 +08:00
fish 05d0b7524f 更新数据库文件
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 17:39:13 +08:00
fish 8ed87a23e9 更新数据库文件
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 17:15:34 +08:00
fish c9a7dee7ff 持仓全部视图卡片标题恢复分割线
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 15:11:46 +08:00
fish 794ca61358 持仓全部视图卡片标题间距优化
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 15:11:07 +08:00
fish 11aff71415 更新数据库文件
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 15:09:09 +08:00
fish e1dc4595a2 持仓列表增加品种子tab切换
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 15:08:34 +08:00
fish fa763bd968 更新数据库文件
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 15:03:12 +08:00
fish 9bce381497 已平仓盈亏颜色改为红涨绿跌
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 15:01:25 +08:00
fish 227d8b515f 对冲管理轮次移除日期显示
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 14:59:33 +08:00
fish c40b4b919c 更新数据库文件
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 13:00:31 +08:00
fish 434acb0470 锁仓上限改为可配置,默认3次不变,止盈按钮左侧增加修改入口
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-27 12:57:34 +08:00
18 changed files with 1522 additions and 95 deletions
+3 -3
View File
@@ -28,6 +28,6 @@ def check_lock(
return (current_price - open_price) >= lock_threshold return (current_price - open_price) >= lock_threshold
def should_meltdown(lock_count: int) -> bool: def should_meltdown(lock_count: int, max_locks: int = 3) -> bool:
"""3锁熔断""" """仓次数达到上限时熔断"""
return lock_count >= 3 return lock_count >= max_locks
+3 -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, summary
TEMPLATES_DIR = Path(__file__).parent / "templates" TEMPLATES_DIR = Path(__file__).parent / "templates"
@@ -59,6 +59,8 @@ 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.include_router(summary.router)
@app.get("/") @app.get("/")
+58 -3
View File
@@ -13,6 +13,7 @@ class Product(Base):
code: Mapped[str] = mapped_column(String(10), unique=True, index=True) code: Mapped[str] = mapped_column(String(10), unique=True, index=True)
name: Mapped[str] = mapped_column(String(20)) name: Mapped[str] = mapped_column(String(20))
exchange: Mapped[str] = mapped_column(String(10), default="CZCE") exchange: Mapped[str] = mapped_column(String(10), default="CZCE")
point_value: Mapped[int] = mapped_column(Integer, default=20)
contracts: Mapped[list["Contract"]] = relationship( contracts: Mapped[list["Contract"]] = relationship(
back_populates="product", cascade="all, delete-orphan" back_populates="product", cascade="all, delete-orphan"
@@ -95,6 +96,7 @@ class Round(Base):
id: Mapped[int] = mapped_column(primary_key=True) id: Mapped[int] = mapped_column(primary_key=True)
contract_code: Mapped[str] = mapped_column(String(10), index=True) contract_code: Mapped[str] = mapped_column(String(10), index=True)
daily_hands: Mapped[int] = mapped_column(Integer, default=1) daily_hands: Mapped[int] = mapped_column(Integer, default=1)
max_locks: Mapped[int] = mapped_column(Integer, default=3)
status: Mapped[str] = mapped_column(String(20), default="active") status: Mapped[str] = mapped_column(String(20), default="active")
started_at: Mapped[date] = mapped_column(Date) started_at: Mapped[date] = mapped_column(Date)
@@ -136,12 +138,13 @@ class Trade(Base):
close_price: Mapped[float | None] = mapped_column(Float, nullable=True) close_price: Mapped[float | None] = mapped_column(Float, nullable=True)
close_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0) close_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0)
status: Mapped[str] = mapped_column(String(10), default="open") status: Mapped[str] = mapped_column(String(10), default="open")
point_value: Mapped[int] = mapped_column(Integer, default=20)
@property @property
def pnl(self) -> float | None: def pnl(self) -> float | None:
if self.close_price is None: if self.close_price is None:
return None return None
mul = 20 # glass futures point value mul = self.point_value
if self.direction == "long": if self.direction == "long":
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:
@@ -165,14 +168,66 @@ class OptionTrade(Base):
close_price: Mapped[float | None] = mapped_column(Float, nullable=True) close_price: Mapped[float | None] = mapped_column(Float, nullable=True)
close_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0) close_fee: Mapped[float | None] = mapped_column(Float, nullable=True, default=0)
status: Mapped[str] = mapped_column(String(10), default="open") status: Mapped[str] = mapped_column(String(10), default="open")
point_value: Mapped[int] = mapped_column(Integer, default=20)
@property @property
def pnl(self) -> float | None: def pnl(self) -> float | None:
if self.close_price is None: if self.close_price is None:
return None return None
mul = 20 # glass futures point value mul = self.point_value
if self.direction == "sell": if self.direction == "sell":
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)
call_strike_price: Mapped[float] = mapped_column(Float)
put_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.call_strike_price + self.total_premium
@property
def breakeven_lower(self) -> float:
return self.put_strike_price - self.total_premium
+10 -2
View File
@@ -9,7 +9,13 @@ from app.collector import (
router = APIRouter(prefix="/admin", tags=["admin"]) router = APIRouter(prefix="/admin", tags=["admin"])
EXCHANGES = ["CZCE", "DCE", "SHFE", "CFFEX", "INE"] EXCHANGES = [
("CZCE", "郑州商品交易所 CZCE"),
("DCE", "大连商品交易所 DCE"),
("SHFE", "上海期货交易所 SHFE"),
("CFFEX", "中国金融期货交易所 CFFEX"),
("INE", "上海国际能源交易中心 INE"),
]
@router.get("/", response_class=HTMLResponse) @router.get("/", response_class=HTMLResponse)
@@ -48,6 +54,7 @@ def admin_page(request: Request, db: Session = Depends(get_db)):
"code": p.code, "code": p.code,
"name": p.name, "name": p.name,
"exchange": p.exchange, "exchange": p.exchange,
"point_value": p.point_value,
"contracts": contract_list, "contracts": contract_list,
}) })
@@ -71,11 +78,12 @@ def create_product(
code: str = Form(...), code: str = Form(...),
name: str = Form(...), name: str = Form(...),
exchange: str = Form(...), exchange: str = Form(...),
point_value: int = Form(20),
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
existing = db.query(Product).filter(Product.code == code.upper()).first() existing = db.query(Product).filter(Product.code == code.upper()).first()
if not existing: if not existing:
p = Product(code=code.upper(), name=name, exchange=exchange) p = Product(code=code.upper(), name=name, exchange=exchange, point_value=point_value)
db.add(p) db.add(p)
db.commit() db.commit()
return RedirectResponse("/admin/?tab=product", status_code=303) return RedirectResponse("/admin/?tab=product", status_code=303)
+167
View File
@@ -0,0 +1,167 @@
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(...),
call_strike_price: float = Form(...),
put_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,
call_strike_price=call_strike_price,
put_strike_price=put_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(...),
call_strike_price: float = Form(...),
put_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.call_strike_price = call_strike_price
t.put_strike_price = put_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)
+13 -2
View File
@@ -67,7 +67,12 @@ def open_trade(
): ):
code = contract_code.upper() code = contract_code.upper()
contract = db.query(Contract).filter(Contract.code == code).first() contract = db.query(Contract).filter(Contract.code == code).first()
product_code = contract.product.code if contract else code[:2] 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 = OptionTrade( t = OptionTrade(
product_code=product_code, product_code=product_code,
@@ -78,6 +83,7 @@ def open_trade(
open_date=date.fromisoformat(open_date), open_date=date.fromisoformat(open_date),
open_price=open_price, open_price=open_price,
open_fee=open_fee, open_fee=open_fee,
point_value=point_value,
status="open", status="open",
) )
db.add(t) db.add(t)
@@ -124,7 +130,12 @@ def edit_trade(
if t: if t:
code = contract_code.upper() code = contract_code.upper()
contract = db.query(Contract).filter(Contract.code == code).first() contract = db.query(Contract).filter(Contract.code == code).first()
t.product_code = contract.product.code if contract else code[:2] 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.contract_code = code
t.option_type = option_type t.option_type = option_type
t.direction = direction t.direction = direction
+30
View File
@@ -37,6 +37,22 @@ def positions_page(request: Request, db: Session = Depends(get_db)):
.all() .all()
) )
# Compute stop-profit threshold: latest position A × point_value per round
round_thresholds: dict[int, dict] = {}
for r in active_rounds:
if r.positions:
latest_a = r.positions[-1].amp_threshold
else:
latest_a = 0
contract = db.query(Contract).filter(Contract.code == r.contract_code).first()
pv = contract.product.point_value if contract and contract.product else 20
round_thresholds[r.id] = {
"amp": latest_a,
"point_value": pv,
"daily_hands": r.daily_hands,
"threshold": latest_a * pv * r.daily_hands,
}
template = request.app.state.templates.get_template("positions.html") template = request.app.state.templates.get_template("positions.html")
return HTMLResponse( return HTMLResponse(
template.render( template.render(
@@ -45,6 +61,7 @@ def positions_page(request: Request, db: Session = Depends(get_db)):
contracts=get_active_contracts(db), contracts=get_active_contracts(db),
product_contracts=get_product_contracts(db), product_contracts=get_product_contracts(db),
active_rounds=active_rounds, active_rounds=active_rounds,
round_thresholds=round_thresholds,
) )
) )
@@ -139,6 +156,19 @@ def update_lock_price(
return RedirectResponse("/positions/", status_code=303) return RedirectResponse("/positions/", status_code=303)
@router.post("/{round_id}/max-locks")
def update_max_locks(
round_id: int,
max_locks: int = Form(...),
db: Session = Depends(get_db),
):
r = db.query(Round).filter(Round.id == round_id).first()
if r and r.status == "active" and 1 <= max_locks <= 10:
r.max_locks = max_locks
db.commit()
return RedirectResponse("/positions/", status_code=303)
@router.post("/{round_id}/close") @router.post("/{round_id}/close")
def close_round( def close_round(
request: Request, request: Request,
+184
View File
@@ -0,0 +1,184 @@
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 _future_stats(records) -> dict:
total = len(records)
gross = 0.0
open_fee = 0.0
close_fee = 0.0
wins = 0
profit_sum = 0.0
loss_sum = 0.0
for t in records:
mul = t.point_value
if t.direction == "short":
g = (t.open_price - t.close_price) * mul
else:
g = (t.close_price - t.open_price) * mul
gross += g
of = t.open_fee or 0
cf = t.close_fee or 0
open_fee += of
close_fee += cf
net = g - of - cf
if net > 0:
wins += 1
profit_sum += net
elif net < 0:
loss_sum += net
return {
"count": total,
"gross": gross,
"net": gross - open_fee - close_fee,
"wins": wins,
"win_rate": round(wins / total * 100, 1) if total > 0 else 0,
"open_fee": open_fee,
"close_fee": close_fee,
"total_fee": open_fee + close_fee,
"profit_sum": profit_sum,
"loss_sum": loss_sum,
}
def _option_stats(records) -> dict:
total = len(records)
gross = 0.0
open_fee = 0.0
close_fee = 0.0
wins = 0
profit_sum = 0.0
loss_sum = 0.0
for t in records:
mul = t.point_value
if t.direction == "sell":
g = (t.open_price - t.close_price) * mul
else:
g = (t.close_price - t.open_price) * mul
gross += g
of = t.open_fee or 0
cf = t.close_fee or 0
open_fee += of
close_fee += cf
net = g - of - cf
if net > 0:
wins += 1
profit_sum += net
elif net < 0:
loss_sum += net
return {
"count": total,
"gross": gross,
"net": gross - open_fee - close_fee,
"wins": wins,
"win_rate": round(wins / total * 100, 1) if total > 0 else 0,
"open_fee": open_fee,
"close_fee": close_fee,
"total_fee": open_fee + close_fee,
"profit_sum": profit_sum,
"loss_sum": loss_sum,
}
def _dual_stats(records) -> dict:
total = len(records)
gross = 0.0
open_fee = 0.0
close_fee = 0.0
wins = 0
profit_sum = 0.0
loss_sum = 0.0
for t in records:
mul = t.point_value
call_g = (t.call_close_price - t.call_open_price) * mul
put_g = (t.put_close_price - t.put_open_price) * mul
g = call_g + put_g
gross += g
of = (t.call_open_fee or 0) + (t.put_open_fee or 0)
cf = (t.call_close_fee or 0) + (t.put_close_fee or 0)
open_fee += of
close_fee += cf
net = g - of - cf
if net > 0:
wins += 1
profit_sum += net
elif net < 0:
loss_sum += net
return {
"count": total,
"gross": gross,
"net": gross - open_fee - close_fee,
"wins": wins,
"win_rate": round(wins / total * 100, 1) if total > 0 else 0,
"open_fee": open_fee,
"close_fee": close_fee,
"total_fee": open_fee + close_fee,
"profit_sum": profit_sum,
"loss_sum": loss_sum,
}
@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 = _future_stats(future_trades)
# Options
option_trades = (
db.query(OptionTrade).filter(OptionTrade.status == "closed")
.order_by(OptionTrade.close_date.desc()).all()
)
option_stats = _option_stats(option_trades)
# Dual options
dual_trades = (
db.query(DualOptionTrade).filter(DualOptionTrade.status == "closed")
.order_by(DualOptionTrade.close_date.desc()).all()
)
dual_stats = _dual_stats(dual_trades)
# Aggregate
total_count = future_stats["count"] + option_stats["count"] + dual_stats["count"]
gross_pnl = future_stats["gross"] + option_stats["gross"] + dual_stats["gross"]
net_pnl = future_stats["net"] + option_stats["net"] + dual_stats["net"]
total_fee = future_stats["total_fee"] + option_stats["total_fee"] + dual_stats["total_fee"]
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_profit = future_stats["profit_sum"] + option_stats["profit_sum"] + dual_stats["profit_sum"]
total_loss = future_stats["loss_sum"] + option_stats["loss_sum"] + dual_stats["loss_sum"]
profit_ratio = round(total_profit / abs(total_loss), 2) if total_loss != 0 else 0
template = request.app.state.templates.get_template("summary.html")
return HTMLResponse(
template.render(
request=request,
active_nav="summary",
total_count=total_count,
gross_pnl=gross_pnl,
net_pnl=net_pnl,
total_wins=total_wins,
total_win_rate=total_win_rate,
total_fee=total_fee,
total_profit=total_profit,
total_loss=total_loss,
profit_ratio=profit_ratio,
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,
)
)
+36 -2
View File
@@ -74,7 +74,12 @@ def open_trade(
): ):
code = contract_code.upper() code = contract_code.upper()
contract = db.query(Contract).filter(Contract.code == code).first() contract = db.query(Contract).filter(Contract.code == code).first()
product_code = contract.product.code if contract else code[:2] 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 = Trade( t = Trade(
product_code=product_code, product_code=product_code,
@@ -83,6 +88,7 @@ def open_trade(
open_date=date.fromisoformat(open_date), open_date=date.fromisoformat(open_date),
open_price=open_price, open_price=open_price,
open_fee=open_fee, open_fee=open_fee,
point_value=point_value,
status="open", status="open",
) )
db.add(t) db.add(t)
@@ -127,7 +133,12 @@ def edit_trade(
if t: if t:
code = contract_code.upper() code = contract_code.upper()
contract = db.query(Contract).filter(Contract.code == code).first() contract = db.query(Contract).filter(Contract.code == code).first()
t.product_code = contract.product.code if contract else code[:2] 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.contract_code = code
t.direction = direction t.direction = direction
t.open_date = date.fromisoformat(open_date) t.open_date = date.fromisoformat(open_date)
@@ -141,6 +152,29 @@ def edit_trade(
return RedirectResponse("/trades/", status_code=303) return RedirectResponse("/trades/", status_code=303)
@router.post("/bulk-close")
def bulk_close(
request: Request,
product_code: str = Form(...),
close_date: str = Form(...),
close_price: float = Form(...),
close_fee: float = Form(0.0),
db: Session = Depends(get_db),
):
trades = (
db.query(Trade)
.filter(Trade.product_code == product_code.upper(), Trade.status == "open")
.all()
)
for t in trades:
t.close_date = date.fromisoformat(close_date)
t.close_price = close_price
t.close_fee = close_fee
t.status = "closed"
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()
+41
View File
@@ -27,8 +27,49 @@ SEED_BARS: list[dict] = [
{"date": "2026-07-24", "open": 900, "close": 908, "high": 912, "low": 891}, {"date": "2026-07-24", "open": 900, "close": 908, "high": 912, "low": 891},
] ]
def _migrate(engine):
"""Add missing columns to existing tables (SQLite doesn't auto-migrate)."""
import sqlite3
conn = engine.raw_connection()
try:
cur = conn.cursor()
cur.execute("PRAGMA table_info(rounds)")
cols = {row[1] for row in cur.fetchall()}
if "max_locks" not in cols:
cur.execute("ALTER TABLE rounds ADD COLUMN max_locks INTEGER DEFAULT 3")
cur.execute("PRAGMA table_info(products)")
cols = {row[1] for row in cur.fetchall()}
if "point_value" not in cols:
cur.execute("ALTER TABLE products ADD COLUMN point_value INTEGER DEFAULT 20")
cur.execute("PRAGMA table_info(trades)")
cols = {row[1] for row in cur.fetchall()}
if "point_value" not in cols:
cur.execute("ALTER TABLE trades ADD COLUMN point_value INTEGER DEFAULT 20")
cur.execute("PRAGMA table_info(option_trades)")
cols = {row[1] for row in cur.fetchall()}
if "point_value" not in cols:
cur.execute("ALTER TABLE option_trades ADD COLUMN point_value INTEGER DEFAULT 20")
cur.execute("PRAGMA table_info(dual_option_trades)")
cols = {row[1] for row in cur.fetchall()}
if "call_strike_price" not in cols:
cur.execute("ALTER TABLE dual_option_trades ADD COLUMN call_strike_price FLOAT DEFAULT 0")
if "put_strike_price" not in cols:
cur.execute("ALTER TABLE dual_option_trades ADD COLUMN put_strike_price FLOAT DEFAULT 0")
if "strike_price" in cols:
cur.execute("ALTER TABLE dual_option_trades DROP COLUMN strike_price")
conn.commit()
finally:
conn.close()
def seed(): def seed():
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
_migrate(engine)
db = SessionLocal() db = SessionLocal()
try: try:
+75 -27
View File
@@ -20,6 +20,10 @@
</style> </style>
{% set tab = request.query_params.get('tab', 'sync') %} {% set tab = request.query_params.get('tab', 'sync') %}
{% set exchange_names = {} %}
{% for ex in exchanges %}
{% set _ = exchange_names.update({ex[0]: ex[1]}) %}
{% endfor %}
<div class="tabs"> <div class="tabs">
<button class="tab-btn{% if tab == 'sync' %} active{% endif %}" onclick="switchTab('sync')">📡 数据同步</button> <button class="tab-btn{% if tab == 'sync' %} active{% endif %}" onclick="switchTab('sync')">📡 数据同步</button>
@@ -51,7 +55,7 @@
onclick="toggleProduct(this)"> onclick="toggleProduct(this)">
<span class="collapse-arrow" style="font-size:0.75rem;transition:transform .2s;display:inline-block;transform:rotate(-90deg);"></span> <span class="collapse-arrow" style="font-size:0.75rem;transition:transform .2s;display:inline-block;transform:rotate(-90deg);"></span>
<strong style="font-size:0.92rem;">{{ p.code }} · {{ p.name }}</strong> <strong style="font-size:0.92rem;">{{ p.code }} · {{ p.name }}</strong>
<span style="font-size:0.78rem;color:var(--sub);">{{ p.exchange }}</span> <span style="font-size:0.78rem;color:var(--sub);">{{ exchange_names.get(p.exchange, p.exchange) }}</span>
<span style="font-size:0.78rem;color:var(--sub);margin-left:8px;">{{ p.contracts|length }} 合约</span> <span style="font-size:0.78rem;color:var(--sub);margin-left:8px;">{{ p.contracts|length }} 合约</span>
<form method="post" action="/admin/sync/product/{{ p.id }}" style="display:inline;margin-left:auto;" onclick="event.stopPropagation()"> <form method="post" action="/admin/sync/product/{{ p.id }}" style="display:inline;margin-left:auto;" onclick="event.stopPropagation()">
<button type="submit" class="btn" style="font-size:0.78rem;padding:4px 14px;background:var(--accent);color:#fff;border-radius:5px;">同步行情</button> <button type="submit" class="btn" style="font-size:0.78rem;padding:4px 14px;background:var(--accent);color:#fff;border-radius:5px;">同步行情</button>
@@ -86,7 +90,7 @@
{# ═══════════════════ Tab: 品种管理 ═══════════════════ #} {# ═══════════════════ Tab: 品种管理 ═══════════════════ #}
<div class="tab-panel{% if tab == 'product' %} active{% endif %}" id="tab-product"> <div class="tab-panel{% if tab == 'product' %} active{% endif %}" id="tab-product">
<div class="form-card" style="margin-bottom:24px;"> <div class="form-card" style="margin-bottom:24px;max-width:720px;">
<div class="section-title">新建品种</div> <div class="section-title">新建品种</div>
<form method="post" action="/admin/product" style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;"> <form method="post" action="/admin/product" style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;">
<div class="form-group" style="margin-bottom:0;flex:1;min-width:120px;"> <div class="form-group" style="margin-bottom:0;flex:1;min-width:120px;">
@@ -97,37 +101,55 @@
<label>品种名称</label> <label>品种名称</label>
<input type="text" name="name" required placeholder="玻璃"> <input type="text" name="name" required placeholder="玻璃">
</div> </div>
<div class="form-group" style="margin-bottom:0;flex:1;min-width:120px;"> <div class="form-group" style="margin-bottom:0;flex:1;min-width:200px;">
<label>交易所</label> <label>交易所</label>
<select name="exchange" required> <select name="exchange" required>
{% for ex in exchanges %} {% for ex in exchanges %}
<option value="{{ ex }}">{{ ex }}</option> <option value="{{ ex[0] }}">{{ ex[1] }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="form-group" style="margin-bottom:0;flex:0 0 auto;min-width:80px;">
<label>每点价值</label>
<input type="number" name="point_value" value="20" min="1" required style="font-size:0.9rem;">
</div>
<button type="submit" class="btn btn-primary">新建</button> <button type="submit" class="btn btn-primary">新建</button>
</form> </form>
</div> </div>
{% if products %} {% if products %}
<div class="table-wrap"> {% for ex in exchanges %}
<table> {% set ex_products = products | selectattr('exchange', 'equalto', ex[0]) | list %}
<tr><th>代码</th><th>名称</th><th>交易所</th><th>合约数</th><th>操作</th></tr> {% if ex_products %}
{% for p in products %} <div style="margin-bottom:16px;border:1px solid var(--border);border-radius:10px;overflow:hidden;">
<tr> <div class="product-header"
<td><strong>{{ p.code }}</strong></td> style="display:flex;align-items:center;gap:12px;padding:12px 18px;background:var(--th-bg);cursor:pointer;user-select:none;"
<td>{{ p.name }}</td> onclick="toggleProduct(this)">
<td>{{ p.exchange }}</td> <span class="collapse-arrow" style="font-size:0.75rem;transition:transform .2s;display:inline-block;transform:rotate(-90deg);"></span>
<td>{{ p.contracts|length }}</td> <strong style="font-size:0.92rem;">{{ ex[1] }}</strong>
<td> <span style="font-size:0.78rem;color:var(--sub);">{{ ex_products|length }} 个品种</span>
<form method="post" action="/admin/product/{{ p.id }}/delete" onsubmit="return confirm('删除品种 {{ p.code }} 及其所有合约?')" style="display:inline;"> </div>
<button style="background:none;border:none;color:var(--danger);cursor:pointer;font-size:0.82rem;">删除</button> <div class="product-body" style="display:none;">
</form> <table style="font-size:0.88rem;">
</td> <tr><th>代码</th><th>名称</th><th>每点</th><th>合约数</th><th>操作</th></tr>
</tr> {% for p in ex_products %}
{% endfor %} <tr>
</table> <td><strong>{{ p.code }}</strong></td>
<td>{{ p.name }}</td>
<td>{{ p.point_value }}</td>
<td>{{ p.contracts|length }}</td>
<td>
<form method="post" action="/admin/product/{{ p.id }}/delete" onsubmit="return confirm('删除品种 {{ p.code }} 及其所有合约?')" style="display:inline;">
<button style="background:none;border:none;color:var(--danger);cursor:pointer;font-size:0.82rem;">删除</button>
</form>
</td>
</tr>
{% endfor %}
</table>
</div>
</div> </div>
{% endif %}
{% endfor %}
{% else %} {% else %}
<div style="text-align:center;padding:40px;color:var(--sub);">暂无品种,请先新建。</div> <div style="text-align:center;padding:40px;color:var(--sub);">暂无品种,请先新建。</div>
{% endif %} {% endif %}
@@ -137,16 +159,22 @@
<div class="tab-panel{% if tab == 'contract' %} active{% endif %}" id="tab-contract"> <div class="tab-panel{% if tab == 'contract' %} active{% endif %}" id="tab-contract">
{% if products %} {% if products %}
{# ── Add contract form ── #} {# ── Add contract form ── #}
<div class="form-card" style="margin-bottom:24px;"> <div class="form-card" style="margin-bottom:24px;max-width:720px;">
<div class="section-title">添加合约</div> <div class="section-title">添加合约</div>
<form method="post" action="/admin/contract" style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;"> <form method="post" action="/admin/contract" style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;">
<div class="form-group" style="margin-bottom:0;flex:1;min-width:200px;">
<label>交易所</label>
<select id="contractExchange" required onchange="filterProductsByExchange()">
<option value="">-- 选择交易所 --</option>
{% for ex in exchanges %}
<option value="{{ ex[0] }}">{{ ex[1] }}</option>
{% endfor %}
</select>
</div>
<div class="form-group" style="margin-bottom:0;flex:1;min-width:140px;"> <div class="form-group" style="margin-bottom:0;flex:1;min-width:140px;">
<label>品种</label> <label>品种</label>
<select name="product_id" required> <select name="product_id" id="contractProduct" required disabled>
<option value="">-- 选择品种 --</option> <option value="">-- 先选交易所 --</option>
{% for p in products %}
<option value="{{ p.id }}">{{ p.code }} · {{ p.name }}</option>
{% endfor %}
</select> </select>
</div> </div>
<div class="form-group" style="margin-bottom:0;flex:1;min-width:140px;"> <div class="form-group" style="margin-bottom:0;flex:1;min-width:140px;">
@@ -199,6 +227,26 @@
</div> </div>
<script> <script>
var productList = [
{% for p in products %}
{id: {{ p.id }}, code: '{{ p.code }}', name: '{{ p.name }}', exchange: '{{ p.exchange }}'}{% if not loop.last %},{% endif %}
{% endfor %}
];
function filterProductsByExchange() {
var exchange = document.getElementById('contractExchange').value;
var sel = document.getElementById('contractProduct');
sel.innerHTML = '<option value="">-- 选择品种 --</option>';
sel.disabled = !exchange;
if (exchange) {
productList.forEach(function(p) {
if (p.exchange === exchange) {
sel.innerHTML += '<option value="' + p.id + '">' + p.code + ' · ' + p.name + '</option>';
}
});
}
}
function escapeHtml(s) { function escapeHtml(s) {
var d = document.createElement('div'); var d = document.createElement('div');
d.appendChild(document.createTextNode(s)); d.appendChild(document.createTextNode(s));
+6
View File
@@ -212,6 +212,12 @@
<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="/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>
+463
View File
@@ -0,0 +1,463 @@
{% 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:100px;">
<label>看涨行权价</label>
<input type="number" step="any" name="call_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="put_strike_price" required placeholder="1280" 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>C行权价</th><th>P行权价</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.call_strike_price }}</td>
<td>{{ t.put_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(--warn);color:#fff;border:none;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;"
onclick="showOpenAnalysis('{{ t.product_code }} {{ t.contract_code.replace(t.product_code, '', 1) }}', {{ t.call_strike_price }}, {{ t.put_strike_price }}, {{ t.call_open_price }}, {{ t.put_open_price }}, {{ t.call_open_fee or 0 }}, {{ t.put_open_fee or 0 }}, {{ t.point_value }})">分析</button>
<button style="background:var(--accent);color:#fff;border:none;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;margin-left:8px;"
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.call_strike_price }}, {{ t.put_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>C行权价</th><th>P行权价</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.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>{{ "%.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.call_strike_price }}, {{ t.put_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.call_strike_price }}, {{ t.put_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="call_strike_price" id="editCallStrike" required style="font-size:0.9rem;">
</div>
<div class="form-group">
<label>看跌行权价</label>
<input type="number" step="any" name="put_strike_price" id="editPutStrike" 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 showOpenAnalysis(title, callStrike, putStrike, callOpen, putOpen, callOpenFee, putOpenFee, pointValue) {
document.getElementById('pnlTitle').textContent = title + ' 持仓分析';
document.getElementById('pnlFormula').textContent = 'C行权价: ' + callStrike + ' P行权价: ' + putStrike + ' | 每点 ' + pointValue + ' 元';
var totalPremium = callOpen + putOpen;
var totalOpenFee = callOpenFee + putOpenFee;
var premiumRmb = totalPremium * pointValue;
var totalCost = premiumRmb + totalOpenFee;
var beUpper = callStrike + totalPremium;
var beLower = putStrike - totalPremium;
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 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 style="background:var(--accent-light);"><td><b>盈亏平衡点(上)</b></td><td>' + callStrike + ' + ' + totalPremium.toFixed(2) + '</td><td style="text-align:right;font-weight:600;">' + beUpper.toFixed(2) + '</td></tr>';
html += '<tr style="background:var(--accent-light);"><td><b>盈亏平衡点(下)</b></td><td>' + putStrike + ' - ' + totalPremium.toFixed(2) + '</td><td style="text-align:right;font-weight:600;">' + beLower.toFixed(2) + '</td></tr>';
document.getElementById('pnlTable').innerHTML = html;
var insight = '<b>最大亏损 = ' + totalCost.toFixed(2) + ' 元</b>(标的价格在 ' + putStrike + ' ~ ' + callStrike + ' 区间时)<br>';
insight += '盈利区间: 标的价格 &gt; <b style="color:var(--success-fg);">' + beUpper.toFixed(2) + '</b> 或 &lt; <b style="color:var(--success-fg);">' + beLower.toFixed(2) + '</b><br>';
insight += '<span style="color:var(--sub);">平仓后可在已平仓页查看实际盈亏明细</span>';
document.getElementById('pnlInsight').innerHTML = insight;
document.getElementById('pnlOverlay').style.display = 'block';
document.getElementById('pnlBox').style.display = 'block';
}
function showDualPnlDrawer(title, callStrike, putStrike, callOpen, putOpen, callClose, putClose, callOpenFee, putOpenFee, callCloseFee, putCloseFee, pnl, pointValue) {
document.getElementById('pnlTitle').textContent = title + ' 双买分析';
document.getElementById('pnlFormula').textContent = 'C行权价: ' + callStrike + ' P行权价: ' + putStrike + ' | 每点 ' + 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 = callStrike + totalPremium;
var beLower = putStrike - 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>' + callStrike + ' + ' + totalPremium.toFixed(2) + '</td><td style="text-align:right;">' + beUpper.toFixed(2) + '</td></tr>';
html += '<tr><td>盈亏平衡点(下)</td><td>' + putStrike + ' - ' + 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) + ' 元(标的价格在 ' + putStrike + ' ~ ' + callStrike + ' 区间时)<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, callStrike, putStrike, 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('editCallStrike').value = callStrike;
document.getElementById('editPutStrike').value = putStrike;
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 %}
+9 -9
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>
@@ -198,7 +198,7 @@
{% 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;" <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="计算过程">&#9432;</span> 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 }}, {{ t.point_value }})" title="计算过程">&#9432;</span>
{% endif %} {% endif %}
</td> </td>
<td> <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">
@@ -333,16 +333,16 @@ function hideCloseForm() {
</div> </div>
<script> <script>
function showPnlDrawer(title, direction, openPrice, closePrice, openFee, closeFee, result) { function showPnlDrawer(title, direction, openPrice, closePrice, openFee, closeFee, result, pointValue) {
document.getElementById('pnlTitle').textContent = title; document.getElementById('pnlTitle').textContent = title;
var dirLabel = direction === 'sell' ? '卖' : '买'; var dirLabel = direction === 'sell' ? '卖' : '买';
document.getElementById('pnlFormula').textContent = '方向: ' + dirLabel + ' | 每点 20 元'; document.getElementById('pnlFormula').textContent = '方向: ' + dirLabel + ' | 每点 ' + pointValue + ' 元';
var diff = direction === 'sell' ? (openPrice - closePrice) : (closePrice - openPrice); var diff = direction === 'sell' ? (openPrice - closePrice) : (closePrice - openPrice);
var gross = diff * 20; var gross = diff * pointValue;
var fees = openFee + closeFee; var fees = openFee + closeFee;
var html = '<tr style="background:var(--th-bg);"><td>项目</td><td>计算</td><td style="text-align:right;">金额</td></tr>'; 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>' + (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>' + diff.toFixed(2) + ' × ' + pointValue + '</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);">-' + 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>'; 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('pnlTable').innerHTML = html;
+59 -16
View File
@@ -56,23 +56,23 @@
<div style="display:flex;align-items:center;gap:16px;padding:14px 20px;background:var(--th-bg);flex-wrap:wrap;"> <div style="display:flex;align-items:center;gap:16px;padding:14px 20px;background:var(--th-bg);flex-wrap:wrap;">
<strong style="font-size:1rem;">{{ round.contract_code }}</strong> <strong style="font-size:1rem;">{{ round.contract_code }}</strong>
<span class="badge badge-up">{{ round.daily_hands }} 手/天</span> <span class="badge badge-up">{{ round.daily_hands }} 手/天</span>
<span style="font-size:0.82rem;color:var(--sub);">已开 {{ round.positions|length }} 天</span> <span style="font-size:0.82rem;font-weight:600;{% if locks >= round.max_locks %}color:var(--danger-fg);{% endif %}">
<span style="font-size:0.82rem;color:var(--sub);">开始 {{ round.started_at }}</span> 锁仓 {{ locks }}/{{ round.max_locks }}
<span style="font-size:0.82rem;font-weight:600;{% if locks >= 3 %}color:var(--danger-fg);{% endif %}"> {% if locks >= round.max_locks %} ⚠ 熔断{% endif %}
锁仓 {{ locks }}/3
{% if locks >= 3 %} ⚠ 熔断{% endif %}
</span> </span>
<span style="margin-left:auto;display:flex;gap:8px;"> <span style="margin-left:auto;display:flex;gap:8px;">
<button style="background:var(--surface);color:var(--fg);border:1px solid var(--border);padding:3px 12px;border-radius:4px;cursor:pointer;font-size:0.78rem;"
onclick="showMaxLocksModal({{ round.id }}, {{ round.max_locks }})">修改锁仓数</button>
<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="confirmDelete(event, '确认 {{ round.contract_code }} 止盈清仓?', this.closest('form').action)">止盈</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'))">止盈</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="confirmDelete(event, '确认 {{ round.contract_code }} 熔断清仓?', this.closest('form').action)">熔断</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'))">熔断</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: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> <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'))">删除</button>
</form> </form>
</span> </span>
</div> </div>
@@ -81,12 +81,14 @@
{# ── 止盈阈值 ── #} {# ── 止盈阈值 ── #}
{% if round.positions %} {% if round.positions %}
{% set latest = round.positions[-1] %} {% set th = round_thresholds.get(round.id, {}) %}
<div style="background:var(--accent-light);border:1px solid var(--accent);border-radius:6px;padding:10px 16px;margin-bottom:16px;display:flex;align-items:center;gap:20px;flex-wrap:wrap;"> <div style="background:var(--accent-light);border:1px solid var(--accent);border-radius:6px;padding:10px 16px;margin-bottom:16px;display:flex;align-items:center;gap:20px;flex-wrap:wrap;">
<span style="font-size:0.78rem;color:var(--sub);">止盈阈值</span> <span style="font-size:0.78rem;color:var(--sub);">止盈阈值</span>
<span style="font-weight:700;">{{ round.daily_hands }} × {{ latest.amp_threshold }} = {{ round.daily_hands * latest.amp_threshold }} </span> <span style="font-weight:700;">{{ th.get('amp', 0) }} × {{ th.get('point_value', 0) }} × {{ th.get('daily_hands', 0) }} = {{ "%.0f"|format(th.get('threshold', 0)) }} </span>
<span style="color:var(--sub);">|</span> <span style="color:var(--sub);">|</span>
<span style="font-size:0.78rem;color:var(--sub);">当前 A = {{ latest.amp_threshold }}</span> <span style="font-size:0.78rem;color:var(--sub);">当前 A = {{ th.get('amp', 0) }}</span>
<span style="color:var(--sub);">|</span>
<span style="font-size:0.78rem;color:var(--sub);">跳点价 {{ th.get('point_value', 0) }} 元/点</span>
<span style="color:var(--sub);">|</span> <span style="color:var(--sub);">|</span>
<span style="font-size:0.78rem;color:var(--sub);">总手数 {{ round.daily_hands * round.positions|length }}</span> <span style="font-size:0.78rem;color:var(--sub);">总手数 {{ round.daily_hands * round.positions|length }}</span>
</div> </div>
@@ -106,7 +108,7 @@
</div> </div>
<div class="form-group" style="margin-bottom:0;flex:1;min-width:70px;"> <div class="form-group" style="margin-bottom:0;flex:1;min-width:70px;">
<label>手数</label> <label>手数</label>
<input type="number" name="hands" value="{{ round.daily_hands }}" min="1" max="10" required style="font-size:0.9rem;"> <input type="number" name="hands" value="{{ round.daily_hands }}" readonly style="font-size:0.9rem;background:var(--th-bg);cursor:default;">
</div> </div>
<button type="submit" class="btn btn-primary" style="font-size:0.82rem;padding:8px 18px;">+ 添加</button> <button type="submit" class="btn btn-primary" style="font-size:0.82rem;padding:8px 18px;">+ 添加</button>
</div> </div>
@@ -118,11 +120,11 @@
<div class="table-wrap" style="margin-bottom:0;"> <div class="table-wrap" style="margin-bottom:0;">
<table style="font-size:0.84rem;"> <table style="font-size:0.84rem;">
<tr> <tr>
<th>日期</th><th>开仓价</th><th>A</th><th>锁仓价</th><th>手数</th><th>已锁</th><th>状态</th><th>操作</th> <th>#</th><th>开仓价</th><th>A</th><th>锁仓价</th><th>手数</th><th>已锁</th><th>状态</th><th>操作</th>
</tr> </tr>
{% for p in round.positions|reverse %} {% for p in round.positions|reverse %}
<tr> <tr>
<td><strong>{{ p.opened_at }}</strong></td> <td style="color:var(--sub);font-size:0.78rem;">{{ loop.index }}</td>
<td>{{ p.open_price }}</td> <td>{{ p.open_price }}</td>
<td>{{ p.amp_threshold }}</td> <td>{{ p.amp_threshold }}</td>
<td style="color:var(--danger-fg);font-weight:600;">{{ p.lock_price }}</td> <td style="color:var(--danger-fg);font-weight:600;">{{ p.lock_price }}</td>
@@ -180,6 +182,24 @@ function filterContracts() {
}); });
} }
} }
function showMaxLocksModal(rid, currentMax) {
document.getElementById('maxLocksRid').value = rid;
document.getElementById('maxLocksInput').value = currentMax;
document.getElementById('maxLocksModal').style.display = 'block';
document.getElementById('maxLocksModalOverlay').style.display = 'block';
document.getElementById('maxLocksInput').focus();
}
function hideMaxLocksModal() {
document.getElementById('maxLocksModal').style.display = 'none';
document.getElementById('maxLocksModalOverlay').style.display = 'none';
}
function doUpdateMaxLocks() {
var rid = document.getElementById('maxLocksRid').value;
var fd = new FormData();
fd.append('max_locks', document.getElementById('maxLocksInput').value);
fetch('/positions/' + rid + '/max-locks', {method:'POST', body:fd})
.then(function() { location.reload(); });
}
function showLockConfirm(pid, lockPrice) { function showLockConfirm(pid, lockPrice) {
document.getElementById('lockPid').value = pid; document.getElementById('lockPid').value = pid;
document.getElementById('lockPriceInput').value = lockPrice; document.getElementById('lockPriceInput').value = lockPrice;
@@ -200,6 +220,20 @@ function filterContracts() {
} }
</script> </script>
<div id="maxLocksModalOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:159;" onclick="hideMaxLocksModal()"></div>
<div id="maxLocksModal" 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:160;width:300px;max-width:90vw;">
<h3 style="margin-bottom:16px;">修改锁仓上限</h3>
<input type="hidden" id="maxLocksRid">
<div class="form-group">
<label>锁仓次数上限(熔断阈值)</label>
<input type="number" id="maxLocksInput" min="1" max="10" required style="font-size:1rem;text-align:center;font-weight:600;">
</div>
<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:8px;">
<button class="btn" style="background:var(--th-bg);color:var(--fg);" onclick="hideMaxLocksModal()">取消</button>
<button class="btn" style="background:var(--accent);color:#fff;" onclick="doUpdateMaxLocks()">确认修改</button>
</div>
</div>
<div id="lockModalOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:149;" onclick="hideLockModal()"></div> <div id="lockModalOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:149;" onclick="hideLockModal()"></div>
<div id="lockModal" 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:150;width:300px;max-width:90vw;"> <div id="lockModal" 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:150;width:300px;max-width:90vw;">
<h3 style="margin-bottom:16px;">确认锁仓</h3> <h3 style="margin-bottom:16px;">确认锁仓</h3>
@@ -223,13 +257,22 @@ function filterContracts() {
</div> </div>
</div> </div>
<script> <script>
function confirmDelete(e, msg, url) { function confirmDelete(e, msg, formEl) {
e.preventDefault(); e.preventDefault();
document.getElementById('confirmMsg').textContent = msg; document.getElementById('confirmMsg').textContent = msg;
document.getElementById('confirmBtn').onclick = function() { document.getElementById('confirmBtn').onclick = function() {
var f = document.createElement('form'); var f = document.createElement('form');
f.method = 'POST'; f.method = 'POST';
f.action = url; f.action = formEl.action;
// copy hidden inputs from the original form
var inputs = formEl.querySelectorAll('input[type="hidden"]');
for (var i = 0; i < inputs.length; i++) {
var clone = document.createElement('input');
clone.type = 'hidden';
clone.name = inputs[i].name;
clone.value = inputs[i].value;
f.appendChild(clone);
}
document.body.appendChild(f); document.body.appendChild(f);
f.submit(); f.submit();
}; };
+217
View File
@@ -0,0 +1,217 @@
{% 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 gross_pnl > 0 %}color:var(--danger-fg);{% elif gross_pnl < 0 %}color:var(--success-fg);{% endif %}">
{% if gross_pnl > 0 %}+{% endif %}{{ "%.2f"|format(gross_pnl) }}
</div>
</div>
<div class="stat-card">
<div class="label">总手续费</div>
<div class="value">{{ "%.2f"|format(total_fee) }}</div>
</div>
<div class="stat-card">
<div class="label">总胜率</div>
<div class="value" style="{% if total_win_rate >= 40 %}color:var(--danger-fg);{% else %}color:var(--success-fg);{% endif %}">
{{ total_win_rate }}%
</div>
</div>
<div class="stat-card">
<div class="label">盈亏比</div>
<div class="value" style="{% if profit_ratio <= 0 %}color:var(--sub);{% elif profit_ratio >= 1 %}color:var(--danger-fg);{% else %}color:var(--success-fg);{% endif %}">
{% if total_loss == 0 and total_profit > 0 %}∞{% else %}{{ "%.2f"|format(profit_ratio) }}{% endif %}
</div>
</div>
<div class="stat-card">
<div class="label">净利润</div>
<div class="value" style="{% if net_pnl > 0 %}color:var(--danger-fg);{% elif net_pnl < 0 %}color:var(--success-fg);{% endif %}">
{% if net_pnl > 0 %}+{% endif %}{{ "%.2f"|format(net_pnl) }}
</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.gross > 0 %}color:var(--danger-fg);{% elif future_stats.gross < 0 %}color:var(--success-fg);{% endif %}">{% if future_stats.gross > 0 %}+{% endif %}{{ "%.2f"|format(future_stats.gross) }}</b></span>
<span>手续费 <b>{{ "%.2f"|format(future_stats.total_fee) }}</b></span>
<span>净利 <b style="{% if future_stats.net > 0 %}color:var(--danger-fg);{% elif future_stats.net < 0 %}color:var(--success-fg);{% endif %}">{% if future_stats.net > 0 %}+{% endif %}{{ "%.2f"|format(future_stats.net) }}</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(--danger-fg);{% elif p < 0 %}color:var(--success-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.gross > 0 %}color:var(--danger-fg);{% elif option_stats.gross < 0 %}color:var(--success-fg);{% endif %}">{% if option_stats.gross > 0 %}+{% endif %}{{ "%.2f"|format(option_stats.gross) }}</b></span>
<span>手续费 <b>{{ "%.2f"|format(option_stats.total_fee) }}</b></span>
<span>净利 <b style="{% if option_stats.net > 0 %}color:var(--danger-fg);{% elif option_stats.net < 0 %}color:var(--success-fg);{% endif %}">{% if option_stats.net > 0 %}+{% endif %}{{ "%.2f"|format(option_stats.net) }}</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(--danger-fg);{% elif p < 0 %}color:var(--success-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.gross > 0 %}color:var(--danger-fg);{% elif dual_stats.gross < 0 %}color:var(--success-fg);{% endif %}">{% if dual_stats.gross > 0 %}+{% endif %}{{ "%.2f"|format(dual_stats.gross) }}</b></span>
<span>手续费 <b>{{ "%.2f"|format(dual_stats.total_fee) }}</b></span>
<span>净利 <b style="{% if dual_stats.net > 0 %}color:var(--danger-fg);{% elif dual_stats.net < 0 %}color:var(--success-fg);{% endif %}">{% if dual_stats.net > 0 %}+{% endif %}{{ "%.2f"|format(dual_stats.net) }}</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(--danger-fg);{% elif p < 0 %}color:var(--success-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 %}
+148 -30
View File
@@ -5,6 +5,12 @@
{% block content %} {% block content %}
{% set view = request.query_params.get('view', '') %} {% set view = request.query_params.get('view', '') %}
{% set product_tab = request.query_params.get('tab', '全部') %}
{% set product_codes = product_contracts.keys()|list %}
{% set grouped_open = {} %}
{% for t in open_trades %}
{% set _ = grouped_open.setdefault(t.product_code, []).append(t) %}
{% endfor %}
<style> <style>
.tabs { display: flex; gap: 0; margin-bottom: 24px; border-bottom: 2px solid var(--border); } .tabs { display: flex; gap: 0; margin-bottom: 24px; border-bottom: 2px solid var(--border); }
@@ -18,6 +24,14 @@
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; } .tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
.tab-panel { display: none; } .tab-panel { display: none; }
.tab-panel.active { display: block; } .tab-panel.active { display: block; }
.sub-tabs { display: flex; gap: 6px; margin-bottom: 16px; flex-wrap: wrap; }
.sub-tab-btn {
padding: 4px 14px; border: 1px solid var(--border); background: var(--surface);
font-size: 0.78rem; color: var(--sub); cursor: pointer; text-decoration: none;
border-radius: 4px; transition: color .12s, border-color .12s;
}
.sub-tab-btn:hover { color: var(--fg); border-color: var(--accent); }
.sub-tab-btn.active { color: var(--accent); border-color: var(--accent); font-weight: 600; background: var(--accent-light); }
</style> </style>
<div class="tabs"> <div class="tabs">
@@ -76,13 +90,27 @@
{# ── 持仓列表 ── #} {# ── 持仓列表 ── #}
<div class="section-title">持仓中 · {{ open_trades|length }} 笔</div> <div class="section-title">持仓中 · {{ open_trades|length }} 笔</div>
<div class="sub-tabs">
<a href="/trades/?tab=全部" class="sub-tab-btn{% if product_tab == '全部' %} active{% endif %}">全部</a>
{% for pc in product_codes %}
{% set count = grouped_open.get(pc, [])|length %}
<a href="/trades/?tab={{ pc }}" class="sub-tab-btn{% if product_tab == pc %} active{% endif %}">{{ pc }}{% if count %} · {{ count }}{% endif %}</a>
{% endfor %}
</div>
{% if open_trades %} {% if open_trades %}
<div class="table-wrap"> {% for pc in product_codes %}
{% if product_tab == '全部' or product_tab == pc %}
{% set trades = grouped_open.get(pc, []) %}
{% if trades %}
<div class="table-wrap" style="margin-bottom:24px;">
{% if product_tab == '全部' %}
<div class="section-title" style="font-size:0.88rem;margin-bottom:0;padding:6px 12px;">{{ pc }} · {{ trades|length }} 笔</div>
{% endif %}
<table> <table>
<tr><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></tr>
{% for t in open_trades %} {% for t in trades %}
<tr> <tr>
<td>{{ t.product_code }}</td>
<td><strong>{{ t.contract_code.replace(t.product_code, '', 1) }}</strong></td> <td><strong>{{ t.contract_code.replace(t.product_code, '', 1) }}</strong></td>
<td> <td>
{% if t.direction == 'short' %} {% if t.direction == 'short' %}
@@ -99,7 +127,7 @@
onclick="showCloseForm({{ t.id }})">平仓</button> 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;" <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 }}', null, null, null, '{{ t.status }}')">编辑</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;"> <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> <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>
@@ -107,6 +135,17 @@
{% endfor %} {% endfor %}
</table> </table>
</div> </div>
{% endif %}
{% endif %}
{% endfor %}
{% if product_tab != '全部' and grouped_open.get(product_tab, []) %}
<div style="margin-bottom:24px;text-align:right;">
<button class="btn" style="background:var(--danger);color:#fff;font-size:0.88rem;padding:8px 24px;" onclick="showBulkClose('{{ product_tab }}')">
全平 {{ product_tab }}
</button>
</div>
{% endif %}
{% else %} {% else %}
<div style="text-align:center;padding:40px;color:var(--sub);">暂无持仓</div> <div style="text-align:center;padding:40px;color:var(--sub);">暂无持仓</div>
{% endif %} {% endif %}
@@ -135,19 +174,65 @@
</form> </form>
</div> </div>
{# ── 全平弹窗 ── #}
<div id="bulkCloseOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:99;" onclick="hideBulkClose()"></div>
<div id="bulkCloseBox" 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;">全平 <span id="bulkCloseLabel"></span></h3>
<form method="post" id="bulkCloseForm" action="/trades/bulk-close">
<input type="hidden" name="product_code" id="bulkCloseProduct">
<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="hideBulkClose()">取消</button>
<button type="submit" class="btn" style="background:var(--danger);color:#fff;">确认全平</button>
</div>
</form>
</div>
</div> </div>
{% else %} {% else %}
<div class="tab-panel active"> <div class="tab-panel active">
{# ═══════════════ 已平仓 ═══════════════ #} {# ═══════════════ 已平仓 ═══════════════ #}
{% set closed_tab = request.query_params.get('tab', '全部') %}
{% set grouped_closed = {} %}
{% for t in closed_trades %}
{% set _ = grouped_closed.setdefault(t.product_code, []).append(t) %}
{% endfor %}
{% if closed_trades %} {% if closed_trades %}
<div class="section-title">已平仓 · {{ closed_trades|length }} 笔</div> <div class="section-title">已平仓 · {{ closed_trades|length }} 笔</div>
<div class="table-wrap">
<div class="sub-tabs">
<a href="/trades/?view=closed&tab=全部" class="sub-tab-btn{% if closed_tab == '全部' %} active{% endif %}">全部</a>
{% for pc in product_codes %}
{% set count = grouped_closed.get(pc, [])|length %}
<a href="/trades/?view=closed&tab={{ pc }}" class="sub-tab-btn{% if closed_tab == pc %} active{% endif %}">{{ pc }}{% if count %} · {{ count }}{% endif %}</a>
{% endfor %}
</div>
{% for pc in product_codes %}
{% if closed_tab == '全部' or closed_tab == pc %}
{% set trades = grouped_closed.get(pc, []) %}
{% if trades %}
<div class="table-wrap" style="margin-bottom:24px;">
{% if closed_tab == '全部' %}
<div class="section-title" style="font-size:0.88rem;margin-bottom:0;padding:6px 12px;">{{ pc }} · {{ trades|length }} 笔</div>
{% endif %}
<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></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 trades %}
<tr> <tr>
<td>{{ t.product_code }}</td>
<td><strong>{{ t.contract_code.replace(t.product_code, '', 1) }}</strong></td> <td><strong>{{ t.contract_code.replace(t.product_code, '', 1) }}</strong></td>
<td> <td>
{% if t.direction == 'short' %} {% if t.direction == 'short' %}
@@ -161,20 +246,30 @@
<td>{{ (t.close_date - t.open_date).days }}天</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>
{% if t.direction == 'short' %}
{% set gross_pnl = (t.open_price - t.close_price) * t.point_value %}
{% else %}
{% set gross_pnl = (t.close_price - t.open_price) * t.point_value %}
{% endif %}
<span style="font-weight:700;{% if gross_pnl > 0 %}color:var(--danger-fg);{% elif gross_pnl < 0 %}color:var(--success-fg);{% else %}color:var(--sub);{% endif %}">
{% if gross_pnl > 0 %}+{% endif %}{{ '%.2f'|format(gross_pnl) }}
</span>
</td>
<td>{{ t.open_fee or 0 }}</td> <td>{{ t.open_fee or 0 }}</td>
<td>{{ t.close_fee or 0 }}</td> <td>{{ t.close_fee or 0 }}</td>
<td> <td>
{% set p = t.pnl %} {% set p = t.pnl %}
{% if p is not none %} {% 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 %}"> <span style="font-weight:700;{% if p > 0 %}color:var(--danger-fg);{% elif p < 0 %}color:var(--success-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;" <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="计算过程">&#9432;</span> 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 }}, {{ t.point_value }})" title="计算过程">&#9432;</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;" <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('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> 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: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> <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>
@@ -184,21 +279,32 @@
{% endfor %} {% endfor %}
</table> </table>
</div> </div>
{% endif %}
{% set ns = namespace(total_pnl=0, total_open_fee=0, total_close_fee=0) %} {% endif %}
{% 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 %} {% endfor %}
{% set summary_trades = closed_trades if closed_tab == '全部' else grouped_closed.get(closed_tab, []) %}
{% set ns = namespace(gross=0, open_fee=0, close_fee=0) %}
{% for t in summary_trades %}
{% if t.direction == 'short' %}
{% set ns.gross = ns.gross + (t.open_price - t.close_price) * t.point_value %}
{% else %}
{% set ns.gross = ns.gross + (t.close_price - t.open_price) * t.point_value %}
{% endif %}
{% set ns.open_fee = ns.open_fee + (t.open_fee or 0) %}
{% set ns.close_fee = ns.close_fee + (t.close_fee or 0) %}
{% endfor %}
{% set net = ns.gross - ns.open_fee - ns.close_fee %}
<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;"> <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>平仓盈亏 <b style="{% if ns.gross > 0 %}color:var(--danger-fg);{% elif ns.gross < 0 %}color:var(--success-fg);{% endif %}">{% if ns.gross > 0 %}+{% endif %}{{ '%.2f'|format(ns.gross) }}</b></span>
<span style="color:var(--sub);">|</span> <span style="color:var(--sub);">|</span>
<span>开仓手续费 <b>{{ '%.2f'|format(ns.total_open_fee) }}</b></span> <span>开仓手续费 <b>{{ '%.2f'|format(ns.open_fee) }}</b></span>
<span style="color:var(--sub);">|</span> <span style="color:var(--sub);">|</span>
<span>平仓手续费 <b>{{ '%.2f'|format(ns.total_close_fee) }}</b></span> <span>平仓手续费 <b>{{ '%.2f'|format(ns.close_fee) }}</b></span>
<span style="color:var(--sub);">|</span> <span style="color:var(--sub);">|</span>
<span>手续费合计 <b>{{ '%.2f'|format(ns.total_open_fee + ns.total_close_fee) }}</b></span> <span>手续费合计 <b>{{ '%.2f'|format(ns.open_fee + ns.close_fee) }}</b></span>
<span style="color:var(--sub);">|</span>
<span>收益 <b style="{% if net > 0 %}color:var(--danger-fg);{% elif net < 0 %}color:var(--success-fg);{% endif %}">{% if net > 0 %}+{% endif %}{{ '%.2f'|format(net) }}</b></span>
</div> </div>
{% else %} {% else %}
<div style="text-align:center;padding:60px;color:var(--sub);">暂无已平仓记录</div> <div style="text-align:center;padding:60px;color:var(--sub);">暂无已平仓记录</div>
@@ -231,6 +337,16 @@ function hideCloseForm() {
document.getElementById('closeFormOverlay').style.display = 'none'; document.getElementById('closeFormOverlay').style.display = 'none';
document.getElementById('closeFormBox').style.display = 'none'; document.getElementById('closeFormBox').style.display = 'none';
} }
function showBulkClose(productCode) {
document.getElementById('bulkCloseLabel').textContent = productCode;
document.getElementById('bulkCloseProduct').value = productCode;
document.getElementById('bulkCloseOverlay').style.display = 'block';
document.getElementById('bulkCloseBox').style.display = 'block';
}
function hideBulkClose() {
document.getElementById('bulkCloseOverlay').style.display = 'none';
document.getElementById('bulkCloseBox').style.display = 'none';
}
</script> </script>
{% endif %} {% endif %}
@@ -293,20 +409,22 @@ function hideCloseForm() {
</div> </div>
<script> <script>
function showPnlDrawer(title, direction, openPrice, closePrice, openFee, closeFee, result) { function showPnlDrawer(title, direction, openPrice, closePrice, openFee, closeFee, result, pointValue) {
document.getElementById('pnlTitle').textContent = title; document.getElementById('pnlTitle').textContent = title;
var dirLabel = direction === 'short' ? '空' : '多'; var dirLabel = direction === 'short' ? '空' : '多';
document.getElementById('pnlFormula').textContent = '方向: ' + dirLabel + ' | 每点 20 元'; document.getElementById('pnlFormula').textContent = '方向: ' + dirLabel + ' | 每点 ' + pointValue + ' 元';
var diff = direction === 'short' ? (openPrice - closePrice) : (closePrice - openPrice); var diff = direction === 'short' ? (openPrice - closePrice) : (closePrice - openPrice);
var gross = diff * 20; var gross = diff * pointValue;
var fees = openFee + closeFee; var fees = openFee + closeFee;
var html = '<tr style="background:var(--th-bg);"><td>项目</td><td>计算</td><td style="text-align:right;">金额</td></tr>'; 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>' + (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>'; var grossColor = gross > 0 ? 'var(--danger-fg)' : gross < 0 ? 'var(--success-fg)' : 'var(--danger-fg)';
html += '<tr><td>开仓手续费</td><td></td><td style="text-align:right;color:var(--danger-fg);">-' + openFee.toFixed(2) + '</td></tr>'; var grossPrefix = gross > 0 ? '+' : gross < 0 ? '' : '';
html += '<tr><td>平仓手续费</td><td></td><td style="text-align:right;color:var(--danger-fg);">-' + closeFee.toFixed(2) + '</td></tr>'; html += '<tr><td>毛利</td><td>' + diff.toFixed(2) + ' × ' + pointValue + '</td><td style="text-align:right;color:' + grossColor + ';">' + grossPrefix + gross.toFixed(2) + '</td></tr>';
html += '<tr><td>开仓手续费</td><td></td><td style="text-align:right;color:var(--success-fg);">-' + openFee.toFixed(2) + '</td></tr>';
html += '<tr><td>平仓手续费</td><td></td><td style="text-align:right;color:var(--success-fg);">-' + closeFee.toFixed(2) + '</td></tr>';
document.getElementById('pnlTable').innerHTML = html; 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('pnlResult').innerHTML = '<b>收益 = ' + gross.toFixed(2) + ' - ' + openFee.toFixed(2) + ' - ' + closeFee.toFixed(2) + ' = <span style="color:' + (result > 0 ? 'var(--danger-fg)' : result < 0 ? 'var(--success-fg)' : 'var(--sub)') + ';">' + (result > 0 ? '+' : '') + result.toFixed(2) + '</span></b>';
document.getElementById('pnlOverlay').style.display = 'block'; document.getElementById('pnlOverlay').style.display = 'block';
document.getElementById('pnlBox').style.display = 'block'; document.getElementById('pnlBox').style.display = 'block';
} }
@@ -370,4 +488,4 @@ function hideConfirm() {
} }
</script> </script>
{% endblock %} {% endblock %}
BIN
View File
Binary file not shown.