Files
finance-talk/ft-app/app/routers/contracts.py
T
2026-07-26 15:06:33 +08:00

208 lines
6.5 KiB
Python

import math
from datetime import date, timedelta
from fastapi import APIRouter, Depends, Request, Query
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from app.database import get_db
from app.models import DailyBar, Contract, PositionRanking
router = APIRouter(prefix="/contracts", tags=["contracts"])
WEEKDAY_ZH = {0: "周一", 1: "周二", 2: "周三", 3: "周四", 4: "周五", 5: "周六", 6: "周日"}
PAGE_SIZE = 7
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]
@router.get("/", response_class=HTMLResponse)
def contract_index(request: Request, db: Session = Depends(get_db)):
from app.models import Product
# Build product → contracts mapping, ordered by product code
products = (
db.query(Product)
.order_by(Product.code)
.all()
)
product_groups = []
all_codes = []
for prod in products:
codes = [
c.code for c in prod.contracts if c.is_active
]
if codes:
product_groups.append({
"code": prod.code,
"name": prod.name,
"exchange": prod.exchange,
"contracts": codes,
})
all_codes.extend(codes)
latest = (
db.query(DailyBar)
.filter(DailyBar.contract.in_(all_codes))
.order_by(DailyBar.date.desc())
.all()
)
contract_bars = {}
for bar in latest:
if bar.contract not in contract_bars:
contract_bars[bar.contract] = bar
# Next-day amplitude: mean of latest 5 diffs per contract
contract_next_amp = {}
for code in all_codes:
diffs = [
r[0] for r in
db.query(DailyBar.high - DailyBar.low)
.filter(DailyBar.contract == code)
.order_by(DailyBar.date.desc())
.limit(5)
.all()
]
if len(diffs) >= 5:
contract_next_amp[code] = round(sum(diffs) / 5)
template = request.app.state.templates.get_template("index.html")
return HTMLResponse(
template.render(
request=request,
active_nav="contracts",
product_groups=product_groups,
contract_bars=contract_bars,
contract_next_amp=contract_next_amp,
)
)
@router.get("/{contract}", response_class=HTMLResponse)
def contract_detail(
request: Request,
contract: str,
pos_date: str | None = None,
page: int = Query(1, ge=1),
db: Session = Depends(get_db),
):
active_contracts = get_active_contracts(db)
bars = (
db.query(DailyBar)
.filter(DailyBar.contract == contract.upper())
.order_by(DailyBar.date.desc())
.all()
)
total = len(bars)
total_pages = max(1, math.ceil(total / PAGE_SIZE))
page = min(page, total_pages)
start = (page - 1) * PAGE_SIZE
page_bars = bars[start:start + PAGE_SIZE]
rows = []
for i, bar in enumerate(page_bars):
global_idx = start + i
rows.append({
"global_idx": global_idx,
"date": bar.date.strftime("%Y/%-m/%-d"),
"weekday": WEEKDAY_ZH.get(bar.date.weekday(), ""),
"open": int(bar.open) if bar.open else "-",
"close": int(bar.close) if bar.close else "-",
"high": int(bar.high) if bar.high else "-",
"low": int(bar.low) if bar.low else "-",
"diff": int(bar.diff) if bar.diff else 0,
"amp_5d": int(bar.amp_5d) if bar.amp_5d is not None else None,
"has_amp": bar.amp_5d is not None,
})
latest = bars[0] if bars else None
# Dates that have position data (for date picker)
pos_dates = [
r[0] for r in
db.query(PositionRanking.date)
.filter(PositionRanking.contract_code == contract.upper())
.distinct()
.order_by(PositionRanking.date.desc())
.all()
]
# Determine which date to show position rankings for
if pos_date:
try:
selected_date = date.fromisoformat(pos_date)
except ValueError:
selected_date = pos_dates[0] if pos_dates else None
else:
selected_date = pos_dates[0] if pos_dates else None
# Position rankings for the selected date
pos_data = {"volume": [], "long": [], "short": []}
if selected_date:
rankings = (
db.query(PositionRanking)
.filter(
PositionRanking.contract_code == contract.upper(),
PositionRanking.date == selected_date,
)
.order_by(PositionRanking.data_type, PositionRanking.rank)
.all()
)
for r in rankings:
pos_data[r.data_type].append({
"rank": r.rank,
"institution": r.institution,
"value": r.value,
"change": r.change,
})
pos_totals = {
"long": sum(r.change for r in rankings if r.data_type == "long"),
"short": sum(r.change for r in rankings if r.data_type == "short"),
}
else:
pos_totals = {"long": 0, "short": 0}
# Predict next trading day amplitude: mean of latest 5 diffs
# Compute next trading date
next_date = latest.date + timedelta(days=1) if latest else None
if next_date and next_date.weekday() >= 5:
next_date += timedelta(days=7 - next_date.weekday())
# Predict next trading day amplitude
next_amp = None
if len(bars) >= 5:
next_amp = round(sum(b.diff for b in bars[:5]) / 5)
template = request.app.state.templates.get_template("contract.html")
return HTMLResponse(
template.render(
request=request,
active_nav="contracts",
contract=contract.upper(),
contracts=active_contracts,
rows=rows,
latest=latest,
pos_dates=pos_dates,
selected_pos_date=selected_date,
pos_data=pos_data,
pos_totals=pos_totals,
next_amp=next_amp,
next_date=next_date.strftime("%Y/%-m/%-d") if next_date else None,
next_weekday=WEEKDAY_ZH.get(next_date.weekday(), "") if next_date else "",
row_count=len(rows),
total_rows=total,
page=page,
total_pages=total_pages,
pos_date=pos_date or "",
WEEKDAY_ZH=WEEKDAY_ZH,
)
)