diff --git a/ft-app/app/main.py b/ft-app/app/main.py index e2915c5..ee0896a 100644 --- a/ft-app/app/main.py +++ b/ft-app/app/main.py @@ -7,7 +7,7 @@ from starlette.middleware.base import BaseHTTPMiddleware from app.database import engine, Base, SessionLocal from app.models import User from app.seed import seed -from app.routers import contracts, admin, auth, positions +from app.routers import contracts, admin, auth, positions, trades TEMPLATES_DIR = Path(__file__).parent / "templates" @@ -57,6 +57,7 @@ app.include_router(auth.router) app.include_router(contracts.router) app.include_router(admin.router) app.include_router(positions.router) +app.include_router(trades.router) @app.get("/") diff --git a/ft-app/app/models.py b/ft-app/app/models.py index 45a1eb8..d8b1018 100644 --- a/ft-app/app/models.py +++ b/ft-app/app/models.py @@ -119,4 +119,30 @@ class Position(Base): locked_count: Mapped[int] = mapped_column(Integer, default=0) opened_at: Mapped[date] = mapped_column(Date) - round: Mapped["Round"] = relationship(back_populates="positions") \ No newline at end of file + round: Mapped["Round"] = relationship(back_populates="positions") + + +class Trade(Base): + __tablename__ = "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) + direction: Mapped[str] = mapped_column(String(4)) + open_date: Mapped[date] = mapped_column(Date) + open_price: Mapped[int] = mapped_column(Integer) + open_fee: Mapped[int | None] = mapped_column(Integer, nullable=True, default=0) + close_date: Mapped[date | None] = mapped_column(Date, nullable=True) + close_price: Mapped[int | None] = mapped_column(Integer, nullable=True) + close_fee: Mapped[int | None] = mapped_column(Integer, nullable=True, default=0) + status: Mapped[str] = mapped_column(String(10), default="open") + + @property + def pnl(self) -> int | None: + if self.close_price is None: + return None + mul = 20 # glass futures point value + if self.direction == "long": + return (self.close_price - self.open_price) * mul - (self.open_fee or 0) - (self.close_fee or 0) + else: + return (self.open_price - self.close_price) * mul - (self.open_fee or 0) - (self.close_fee or 0) \ No newline at end of file diff --git a/ft-app/app/routers/trades.py b/ft-app/app/routers/trades.py new file mode 100644 index 0000000..73bce37 --- /dev/null +++ b/ft-app/app/routers/trades.py @@ -0,0 +1,108 @@ +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 Trade, Product, Contract + +router = APIRouter(prefix="/trades", tags=["trades"]) + +DIRECTIONS = [("short", "ç©ș"), ("long", "〚")] + + +def get_active_contracts(db: Session) -> list[str]: + contracts = ( + db.query(Contract.code) + .filter(Contract.is_active == True) + .order_by(Contract.code) + .all() + ) + return [c[0] for c in contracts] + + +def get_products(db: Session) -> list[str]: + products = db.query(Product.code).order_by(Product.code).all() + return [p[0] for p in products] + + +def today_str() -> str: + return date.today().isoformat() + + +@router.get("/", response_class=HTMLResponse) +def trades_page(request: Request, db: Session = Depends(get_db)): + open_trades = ( + db.query(Trade).filter(Trade.status == "open") + .order_by(Trade.open_date.desc()).all() + ) + closed_trades = ( + db.query(Trade).filter(Trade.status == "closed") + .order_by(Trade.close_date.desc()).limit(50).all() + ) + + template = request.app.state.templates.get_template("trades.html") + return HTMLResponse( + template.render( + request=request, + active_nav="trades", + contracts=get_active_contracts(db), + products=get_products(db), + directions=DIRECTIONS, + open_trades=open_trades, + closed_trades=closed_trades, + today=today_str(), + ) + ) + + +@router.post("/open") +def open_trade( + request: Request, + product_code: str = Form(...), + contract_code: str = Form(...), + direction: str = Form(...), + open_date: str = Form(...), + open_price: int = Form(...), + open_fee: int = Form(0), + db: Session = Depends(get_db), +): + t = Trade( + product_code=product_code.upper(), + contract_code=contract_code.upper(), + direction=direction, + open_date=date.fromisoformat(open_date), + open_price=open_price, + open_fee=open_fee, + status="open", + ) + db.add(t) + db.commit() + return RedirectResponse("/trades/", status_code=303) + + +@router.post("/{trade_id}/close") +def close_trade( + request: Request, + trade_id: int, + close_date: str = Form(...), + close_price: int = Form(...), + close_fee: int = Form(0), + db: Session = Depends(get_db), +): + t = db.query(Trade).filter(Trade.id == trade_id).first() + if t and t.status == "open": + t.close_date = date.fromisoformat(close_date) + t.close_price = close_price + t.close_fee = close_fee + t.status = "closed" + db.commit() + return RedirectResponse("/trades/", status_code=303) + + +@router.post("/{trade_id}/delete") +def delete_trade(trade_id: int, db: Session = Depends(get_db)): + t = db.query(Trade).filter(Trade.id == trade_id).first() + if t: + db.delete(t) + db.commit() + return RedirectResponse("/trades/", status_code=303) diff --git a/ft-app/app/templates/base.html b/ft-app/app/templates/base.html index 933556f..715ff77 100644 --- a/ft-app/app/templates/base.html +++ b/ft-app/app/templates/base.html @@ -201,6 +201,9 @@ 📐 ä»“äœçźĄç† + + 📝 äș€æ˜“èź°ćœ• + ⚙ çł»ç»ŸçźĄç† diff --git a/ft-app/app/templates/trades.html b/ft-app/app/templates/trades.html new file mode 100644 index 0000000..1315bf7 --- /dev/null +++ b/ft-app/app/templates/trades.html @@ -0,0 +1,167 @@ +{% extends "base.html" %} +{% block title %}äș€æ˜“èź°ćœ•{% endblock %} +{% block heading %}äș€æ˜“èź°ćœ•{% endblock %} +{% block breadcrumb %}ćŒ€ćčłä»“èź°ćœ• · 盈äșç»ŸèźĄ{% endblock %} + +{% block content %} + +{# ═══════════════ 新ć»șćŒ€ä»“ ═══════════════ #} +
新ć»șćŒ€ä»“
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+ +{# ═══════════════ 持仓䞭 ═══════════════ #} +
持仓䞭 · {{ open_trades|length }} 笔
+ +{% if open_trades %} +
+ + + {% for t in open_trades %} + + + + + + + + + + {% endfor %} +
擁种搈çșŠæ–čć‘ćŒ€ä»“æ—„æœŸćŒ€ä»“ä»·æ‰‹ç»­èŽč操䜜
{{ t.product_code }}{{ t.contract_code }} + {% if t.direction == 'short' %} + ç©ș + {% else %} + 〚 + {% endif %} + {{ t.open_date }}{{ t.open_price }}{{ t.open_fee or 0 }} + +
+ +
+
+
+{% else %} +
暂无持仓
+{% endif %} + +{# ═══════════════ ćčłä»“èĄšć•ïŒˆéšè—ïŒ‰ ═══════════════ #} + + + +{# ═══════════════ ć·Čćčłä»“ ═══════════════ #} +{% if closed_trades %} +
ć·Čćčłä»“ · {{ closed_trades|length }} 笔
+
+ + + {% for t in closed_trades %} + + + + + + + + + + + + {% endfor %} +
擁种搈çșŠæ–čć‘ćŒ€ä»“ćčłä»“ćŒ€ä»“ä»·ćčłä»“价盈äșæ“äœœ
{{ t.product_code }}{{ t.contract_code }} + {% if t.direction == 'short' %} + ç©ș + {% else %} + 〚 + {% endif %} + {{ t.open_date }}{{ t.close_date }}{{ t.open_price }}{{ t.close_price }} + {% set p = t.pnl %} + {% if p is not none %} + + {% if p > 0 %}+{% endif %}{{ p }} + + {% endif %} + +
+ +
+
+
+{% endif %} + + +{% endblock %}