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 %}
+
+ | {{ t.product_code }} |
+ {{ t.contract_code }} |
+
+ {% if t.direction == 'short' %}
+ ç©ș
+ {% else %}
+ ć€
+ {% endif %}
+ |
+ {{ t.open_date }} |
+ {{ t.open_price }} |
+ {{ t.open_fee or 0 }} |
+
+
+
+ |
+
+ {% endfor %}
+
+
+{% else %}
+ææ æä»
+{% endif %}
+
+{# âââââââââââââââ ćčłä»èĄšćïŒéèïŒ âââââââââââââââ #}
+
+
+
+{# âââââââââââââââ ć·Čćčłä» âââââââââââââââ #}
+{% if closed_trades %}
+ć·Čćčłä» · {{ closed_trades|length }} çŹ
+
+
+ | ćç§ | ćçșŠ | æčć | ćŒä» | ćčłä» | ćŒä»ä»· | ćčłä»ä»· | çäș | æäœ |
+ {% for t in closed_trades %}
+
+ | {{ 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 %}
+ |
+
+
+ |
+
+ {% endfor %}
+
+
+{% endif %}
+
+
+{% endblock %}