Compare commits

..

2 Commits

5 changed files with 69 additions and 29 deletions
+3 -3
View File
@@ -53,18 +53,18 @@ class DailyBar(Base):
class PositionSnapshot(Base): class PositionSnapshot(Base):
__tablename__ = "position_snapshots" __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) 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) 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) date: Mapped[date] = mapped_column(Date, index=True)
position: Mapped[int] = mapped_column(Integer) position: Mapped[int] = mapped_column(Integer)
delta: Mapped[int] = mapped_column(Integer, default=0) delta: Mapped[int] = mapped_column(Integer, default=0)
avg_cost: Mapped[float] = mapped_column(Float) avg_cost: Mapped[float] = mapped_column(Float)
class User(Base): class User(Base):
__tablename__ = "users" __tablename__ = "users"
+5 -1
View File
@@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, Form, Request
from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.database import get_db 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 from app.collector import sync_active_contracts, sync_one_contract
router = APIRouter(prefix="/admin", tags=["admin"]) router = APIRouter(prefix="/admin", tags=["admin"])
@@ -112,6 +112,8 @@ def toggle_contract(contract_id: int, db: Session = Depends(get_db)):
def delete_contract(contract_id: int, db: Session = Depends(get_db)): def delete_contract(contract_id: int, db: Session = Depends(get_db)):
c = db.query(Contract).filter(Contract.id == contract_id).first() c = db.query(Contract).filter(Contract.id == contract_id).first()
if c: 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.delete(c)
db.commit() db.commit()
return RedirectResponse("/admin/?tab=contract", status_code=303) return RedirectResponse("/admin/?tab=contract", status_code=303)
@@ -121,6 +123,8 @@ def delete_contract(contract_id: int, db: Session = Depends(get_db)):
def delete_product(product_id: int, db: Session = Depends(get_db)): def delete_product(product_id: int, db: Session = Depends(get_db)):
p = db.query(Product).filter(Product.id == product_id).first() p = db.query(Product).filter(Product.id == product_id).first()
if p: if p:
for c in p.contracts:
db.query(DailyBar).filter(DailyBar.contract == c.code).delete()
db.delete(p) db.delete(p)
db.commit() db.commit()
return RedirectResponse("/admin/?tab=product", status_code=303) return RedirectResponse("/admin/?tab=product", status_code=303)
+39 -16
View File
@@ -2,7 +2,7 @@ from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app.database import get_db 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 from app.engine.game_theory import net_position, net_pnl, format_pnl
router = APIRouter(prefix="/analysis", tags=["analysis"]) router = APIRouter(prefix="/analysis", tags=["analysis"])
@@ -12,8 +12,28 @@ INSTITUTIONS = ["中信期货", "高盛期货", "国泰君安期货", "华泰期
@router.get("/", response_class=HTMLResponse) @router.get("/", response_class=HTMLResponse)
def analysis_page(request: Request, db: Session = Depends(get_db)): 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 = ( latest_snap = (
db.query(PositionSnapshot.date) db.query(PositionSnapshot.date)
.filter(PositionSnapshot.contract_code == selected)
.order_by(PositionSnapshot.date.desc()) .order_by(PositionSnapshot.date.desc())
.first() .first()
) )
@@ -21,14 +41,24 @@ def analysis_page(request: Request, db: Session = Depends(get_db)):
if not latest_snap: if not latest_snap:
template = request.app.state.templates.get_template("analysis.html") template = request.app.state.templates.get_template("analysis.html")
return HTMLResponse( 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] 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 = [] rows = []
totals = {"long_pos": 0, "short_pos": 0, "net_pos": 0, "pnl": 0.0}
total_net_short = 0 total_net_short = 0
pnl_sum = 0.0 pnl_sum = 0.0
@@ -36,6 +66,7 @@ def analysis_page(request: Request, db: Session = Depends(get_db)):
long = ( long = (
db.query(PositionSnapshot) db.query(PositionSnapshot)
.filter( .filter(
PositionSnapshot.contract_code == selected,
PositionSnapshot.institution == inst, PositionSnapshot.institution == inst,
PositionSnapshot.direction == "long", PositionSnapshot.direction == "long",
PositionSnapshot.date == latest_date, PositionSnapshot.date == latest_date,
@@ -45,6 +76,7 @@ def analysis_page(request: Request, db: Session = Depends(get_db)):
short = ( short = (
db.query(PositionSnapshot) db.query(PositionSnapshot)
.filter( .filter(
PositionSnapshot.contract_code == selected,
PositionSnapshot.institution == inst, PositionSnapshot.institution == inst,
PositionSnapshot.direction == "short", PositionSnapshot.direction == "short",
PositionSnapshot.date == latest_date, PositionSnapshot.date == latest_date,
@@ -64,19 +96,14 @@ def analysis_page(request: Request, db: Session = Depends(get_db)):
elif np > 0: elif np > 0:
pnl = net_pnl(np, long_cost, current_price) 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: if np < 0:
total_net_short += abs(np) total_net_short += abs(np)
pnl_sum += pnl
rows.append({ rows.append({
"institution": inst, "institution": inst,
"long_pos": f"{long_pos / 10000:.1f}" if long_pos else "", "long_pos": f"{long_pos / 10000:.1f}" if long_pos else "",
"short_pos": f"{short_pos / 10000:.1f}" if short_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}", "net_pos": f"{'' if np > 0 else ''} {abs(np) / 10000:.1f}",
"pnl": format_pnl(pnl), "pnl": format_pnl(pnl),
"pnl_raw": pnl, "pnl_raw": pnl,
@@ -88,15 +115,11 @@ def analysis_page(request: Request, db: Session = Depends(get_db)):
request=request, request=request,
active_nav="analysis", active_nav="analysis",
rows=rows, 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"), latest_date=latest_date.strftime("%Y-%m-%d"),
current_price=current_price, current_price=current_price,
total_net_short=f"{total_net_short / 10000:.1f}", total_net_short=f"{total_net_short / 10000:.1f}",
total_pnl=format_pnl(pnl_sum), total_pnl=format_pnl(pnl_sum),
contracts=contract_list,
selected=selected,
) )
) )
+4 -2
View File
@@ -262,8 +262,9 @@ def seed():
# --- Seed position snapshots --- # --- Seed position snapshots ---
existing_pos = { existing_pos = {
(r.institution, r.direction, r.date) (r.contract_code, r.institution, r.direction, r.date)
for r in db.query( for r in db.query(
PositionSnapshot.contract_code,
PositionSnapshot.institution, PositionSnapshot.institution,
PositionSnapshot.direction, PositionSnapshot.direction,
PositionSnapshot.date, PositionSnapshot.date,
@@ -273,9 +274,10 @@ def seed():
snaps = [] snaps = []
for inst, direction, date_str, pos, delta, cost in POSITION_DATA_SHORT: for inst, direction, date_str, pos, delta, cost in POSITION_DATA_SHORT:
d = date.fromisoformat(date_str) d = date.fromisoformat(date_str)
if (inst, direction, d) not in existing_pos: if ("FG2609", inst, direction, d) not in existing_pos:
snaps.append( snaps.append(
PositionSnapshot( PositionSnapshot(
contract_code="FG2609",
institution=inst, institution=inst,
direction=direction, direction=direction,
date=d, date=d,
+18 -7
View File
@@ -4,6 +4,17 @@
{% block breadcrumb %}机构持仓{% endblock %} {% block breadcrumb %}机构持仓{% endblock %}
{% block content %} {% block content %}
{% if contracts %}
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px;">
<span style="font-size:0.85rem;color:var(--sub);">合约</span>
<select onchange="window.location='?contract='+this.value" style="padding:6px 12px;border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--fg);font-size:0.88rem;">
{% for c in contracts %}
<option value="{{ c }}"{% if c == selected %} selected{% endif %}>{{ c }}</option>
{% endfor %}
</select>
</div>
{% endif %}
{% if latest_date %} {% if latest_date %}
<div class="stat-grid"> <div class="stat-grid">
<div class="stat-card"> <div class="stat-card">
@@ -11,7 +22,7 @@
<div class="value">{{ latest_date }}</div> <div class="value">{{ latest_date }}</div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<div class="label">参考现价</div> <div class="label">{{ selected }} 现价</div>
<div class="value">{{ current_price }}</div> <div class="value">{{ current_price }}</div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
@@ -27,6 +38,7 @@
<div class="section-title">机构持仓明细</div> <div class="section-title">机构持仓明细</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>
</tr> </tr>
{% for r in rows %} {% for r in rows %}
@@ -48,17 +60,16 @@
{% endfor %} {% endfor %}
<tr style="font-weight:700; background:var(--th-bg);"> <tr style="font-weight:700; background:var(--th-bg);">
<td>合计</td> <td>合计</td>
<td>{{ totals.long_pos }}</td> <td></td>
<td>{{ totals.short_pos }}</td> <td></td>
<td>{{ totals.net_pos }}</td> <td>{{ total_net_short }}</td>
<td>{{ totals.pnl }}</td> <td>{{ total_pnl }}</td>
</tr> </tr>
</table> </table>
</div> </div>
{% else %} {% else %}
<div style="text-align:center;padding:60px 0;color:var(--sub);"> <div style="text-align:center;padding:60px 0;color:var(--sub);">
<p style="font-size:1.1rem;">暂无持仓数据</p> <p style="font-size:1.1rem;">暂无「{{ selected }}」的持仓数据</p>
<p style="margin-top:8px;"><a href="/input/" style="color:var(--accent);">前往录入 →</a></p>
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}