Compare commits
41 Commits
1a70a60dba
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c04ad642e | |||
| 0d0bfb8c6f | |||
| a0ab47040d | |||
| ef9238041b | |||
| 6afec84a2d | |||
| 51792ce4b3 | |||
| 6609b39f7b | |||
| 0d01faf464 | |||
| 7ac9de3663 | |||
| d28ab148f6 | |||
| c109598e4e | |||
| 3d2c8e13db | |||
| 54ce2c97ce | |||
| fcaf2dc2d4 | |||
| 39c5e22ed6 | |||
| af3e359d24 | |||
| 6228c139ef | |||
| 6c66edb507 | |||
| 662deacf04 | |||
| 014a8de964 | |||
| 9e566935d5 | |||
| a25cfb906a | |||
| ca30d05ebf | |||
| 5f4df50fe7 | |||
| 41c149a6ee | |||
| 0fcc7eea03 | |||
| 621ee8c32d | |||
| 833491b8d0 | |||
| 5018eb2083 | |||
| 50328f49ce | |||
| 05d0b7524f | |||
| 8ed87a23e9 | |||
| c9a7dee7ff | |||
| 794ca61358 | |||
| 11aff71415 | |||
| e1dc4595a2 | |||
| fa763bd968 | |||
| 9bce381497 | |||
| 227d8b515f | |||
| c40b4b919c | |||
| 434acb0470 |
@@ -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
@@ -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
@@ -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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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="盈亏分析">ⓘ</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()">×</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 += '盈利区间: 标的价格 > <b style="color:var(--success-fg);">' + beUpper.toFixed(2) + '</b> 或 < <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 += '盈利区间: 标的价格 > ' + beUpper.toFixed(2) + ' 或 < ' + 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 %}
|
||||||
@@ -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="计算过程">ⓘ</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="计算过程">ⓘ</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;
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 %}
|
||||||
@@ -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="计算过程">ⓘ</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="计算过程">ⓘ</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 %}
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user