From 492ca9d82ed32016f91f33aecccb8c123ba7da7d Mon Sep 17 00:00:00 2001 From: fish Date: Fri, 24 Jul 2026 22:58:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=9A=E5=BC=88=E5=88=86=E6=9E=90=E6=8C=89?= =?UTF-8?q?=E5=90=88=E7=BA=A6=E5=88=86=E7=B1=BB=E5=B1=95=E7=A4=BA=EF=BC=8C?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=90=88=E7=BA=A6=E4=B8=8B=E6=8B=89=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ft-app/app/models.py | 6 ++-- ft-app/app/routers/admin.py | 3 +- ft-app/app/routers/analysis.py | 55 +++++++++++++++++++++--------- ft-app/app/seed.py | 6 ++-- ft-app/app/templates/analysis.html | 25 ++++++++++---- 5 files changed, 66 insertions(+), 29 deletions(-) diff --git a/ft-app/app/models.py b/ft-app/app/models.py index 06214ad..cf292a8 100644 --- a/ft-app/app/models.py +++ b/ft-app/app/models.py @@ -53,18 +53,18 @@ class DailyBar(Base): class PositionSnapshot(Base): __tablename__ = "position_snapshots" - __table_args__ = (UniqueConstraint("institution", "direction", "date"),) + __table_args__ = (UniqueConstraint("contract_code", "institution", "direction", "date"),) id: Mapped[int] = mapped_column(primary_key=True) + contract_code: Mapped[str] = mapped_column(String(10), index=True, default="FG") institution: Mapped[str] = mapped_column(String(20), index=True) - direction: Mapped[str] = mapped_column(String(10)) # "long" or "short" + direction: Mapped[str] = mapped_column(String(10)) date: Mapped[date] = mapped_column(Date, index=True) position: Mapped[int] = mapped_column(Integer) delta: Mapped[int] = mapped_column(Integer, default=0) avg_cost: Mapped[float] = mapped_column(Float) - class User(Base): __tablename__ = "users" diff --git a/ft-app/app/routers/admin.py b/ft-app/app/routers/admin.py index d1ef823..2f61c3f 100644 --- a/ft-app/app/routers/admin.py +++ b/ft-app/app/routers/admin.py @@ -2,7 +2,7 @@ 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 Product, Contract, DailyBar +from app.models import Product, Contract, DailyBar, PositionSnapshot from app.collector import sync_active_contracts, sync_one_contract router = APIRouter(prefix="/admin", tags=["admin"]) @@ -113,6 +113,7 @@ def delete_contract(contract_id: int, db: Session = Depends(get_db)): c = db.query(Contract).filter(Contract.id == contract_id).first() if c: db.query(DailyBar).filter(DailyBar.contract == c.code).delete() + db.query(PositionSnapshot).filter(PositionSnapshot.contract_code == c.code).delete() db.delete(c) db.commit() return RedirectResponse("/admin/?tab=contract", status_code=303) diff --git a/ft-app/app/routers/analysis.py b/ft-app/app/routers/analysis.py index 70cfb9d..55f5814 100644 --- a/ft-app/app/routers/analysis.py +++ b/ft-app/app/routers/analysis.py @@ -2,7 +2,7 @@ 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 PositionSnapshot +from app.models import PositionSnapshot, Contract, DailyBar from app.engine.game_theory import net_position, net_pnl, format_pnl router = APIRouter(prefix="/analysis", tags=["analysis"]) @@ -12,8 +12,28 @@ INSTITUTIONS = ["中信期货", "高盛期货", "国泰君安期货", "华泰期 @router.get("/", response_class=HTMLResponse) def analysis_page(request: Request, db: Session = Depends(get_db)): + # Get active contracts for selector + active_contracts = ( + db.query(Contract.code) + .filter(Contract.is_active == True) + .order_by(Contract.code) + .all() + ) + contract_list = [c[0] for c in active_contracts] + + # Default to first contract, or use query param + selected = request.query_params.get("contract", contract_list[0] if contract_list else None) + + if not selected: + template = request.app.state.templates.get_template("analysis.html") + return HTMLResponse( + template.render(request=request, active_nav="analysis", rows=[], latest_date=None) + ) + + # Get latest date for selected contract latest_snap = ( db.query(PositionSnapshot.date) + .filter(PositionSnapshot.contract_code == selected) .order_by(PositionSnapshot.date.desc()) .first() ) @@ -21,14 +41,24 @@ def analysis_page(request: Request, db: Session = Depends(get_db)): if not latest_snap: template = request.app.state.templates.get_template("analysis.html") return HTMLResponse( - template.render(request=request, active_nav="analysis", rows=[], latest_date=None) + template.render( + request=request, active_nav="analysis", + rows=[], latest_date=None, contracts=contract_list, selected=selected, + ) ) latest_date = latest_snap[0] - current_price = 913 # TODO: fetch from daily_bars or akshare + + # Get latest close for current_price reference + latest_bar = ( + db.query(DailyBar) + .filter(DailyBar.contract == selected) + .order_by(DailyBar.date.desc()) + .first() + ) + current_price = int(latest_bar.close) if latest_bar else 0 rows = [] - totals = {"long_pos": 0, "short_pos": 0, "net_pos": 0, "pnl": 0.0} total_net_short = 0 pnl_sum = 0.0 @@ -36,6 +66,7 @@ def analysis_page(request: Request, db: Session = Depends(get_db)): long = ( db.query(PositionSnapshot) .filter( + PositionSnapshot.contract_code == selected, PositionSnapshot.institution == inst, PositionSnapshot.direction == "long", PositionSnapshot.date == latest_date, @@ -45,6 +76,7 @@ def analysis_page(request: Request, db: Session = Depends(get_db)): short = ( db.query(PositionSnapshot) .filter( + PositionSnapshot.contract_code == selected, PositionSnapshot.institution == inst, PositionSnapshot.direction == "short", PositionSnapshot.date == latest_date, @@ -64,19 +96,14 @@ def analysis_page(request: Request, db: Session = Depends(get_db)): elif np > 0: pnl = net_pnl(np, long_cost, current_price) - totals["long_pos"] += long_pos - totals["short_pos"] += short_pos - totals["net_pos"] += np - pnl_sum += pnl if np < 0: total_net_short += abs(np) + pnl_sum += pnl rows.append({ "institution": inst, "long_pos": f"{long_pos / 10000:.1f}万" if long_pos else "—", "short_pos": f"{short_pos / 10000:.1f}万" if short_pos else "—", - "long_cost": f"{long_cost:.2f}" if long_cost else "—", - "short_cost": f"{short_cost:.2f}" if short_cost else "—", "net_pos": f"净{'多' if np > 0 else '空'} {abs(np) / 10000:.1f}万", "pnl": format_pnl(pnl), "pnl_raw": pnl, @@ -88,15 +115,11 @@ def analysis_page(request: Request, db: Session = Depends(get_db)): request=request, active_nav="analysis", rows=rows, - totals={ - "long_pos": f"{totals['long_pos'] / 10000:.1f}万", - "short_pos": f"{totals['short_pos'] / 10000:.1f}万", - "net_pos": f"净{'多' if totals['net_pos'] > 0 else '空'} {abs(totals['net_pos']) / 10000:.1f}万", - "pnl": format_pnl(pnl_sum), - }, latest_date=latest_date.strftime("%Y-%m-%d"), current_price=current_price, total_net_short=f"{total_net_short / 10000:.1f}万", total_pnl=format_pnl(pnl_sum), + contracts=contract_list, + selected=selected, ) ) diff --git a/ft-app/app/seed.py b/ft-app/app/seed.py index 4ad4a5e..cdb03c9 100644 --- a/ft-app/app/seed.py +++ b/ft-app/app/seed.py @@ -262,8 +262,9 @@ def seed(): # --- Seed position snapshots --- existing_pos = { - (r.institution, r.direction, r.date) + (r.contract_code, r.institution, r.direction, r.date) for r in db.query( + PositionSnapshot.contract_code, PositionSnapshot.institution, PositionSnapshot.direction, PositionSnapshot.date, @@ -273,9 +274,10 @@ def seed(): snaps = [] for inst, direction, date_str, pos, delta, cost in POSITION_DATA_SHORT: d = date.fromisoformat(date_str) - if (inst, direction, d) not in existing_pos: + if ("FG2609", inst, direction, d) not in existing_pos: snaps.append( PositionSnapshot( + contract_code="FG2609", institution=inst, direction=direction, date=d, diff --git a/ft-app/app/templates/analysis.html b/ft-app/app/templates/analysis.html index ce82b9e..8932df2 100644 --- a/ft-app/app/templates/analysis.html +++ b/ft-app/app/templates/analysis.html @@ -4,6 +4,17 @@ {% block breadcrumb %}机构持仓{% endblock %} {% block content %} +{% if contracts %} +
+ 合约 + +
+{% endif %} + {% if latest_date %}
@@ -11,7 +22,7 @@
{{ latest_date }}
-
参考现价
+
{{ selected }} 现价
{{ current_price }}
@@ -27,6 +38,7 @@
机构持仓明细
+ {% for r in rows %} @@ -48,17 +60,16 @@ {% endfor %} - - - - + + + +
机构多单空单净持仓净盈亏
合计{{ totals.long_pos }}{{ totals.short_pos }}{{ totals.net_pos }}{{ totals.pnl }}{{ total_net_short }}{{ total_pnl }}
{% else %}
-

暂无持仓数据

-

前往录入 →

+

暂无「{{ selected }}」的持仓数据

{% endif %} {% endblock %}