修复数据采集和清理的bug,新增持仓排名功能,日线数据分页

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 00:01:41 +08:00
parent 5453ce1dcf
commit cd7f6668af
9 changed files with 309 additions and 60 deletions
+12 -3
View File
@@ -2,8 +2,8 @@ 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, PositionSnapshot
from app.collector import sync_active_contracts, sync_one_contract
from app.models import Product, Contract, DailyBar, PositionRanking
from app.collector import sync_active_contracts, sync_one_contract, sync_position_rankings
router = APIRouter(prefix="/admin", tags=["admin"])
@@ -113,7 +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.query(PositionRanking).filter(PositionRanking.contract_code == c.code).delete()
db.delete(c)
db.commit()
return RedirectResponse("/admin/?tab=contract", status_code=303)
@@ -125,6 +125,7 @@ def delete_product(product_id: int, db: Session = Depends(get_db)):
if p:
for c in p.contracts:
db.query(DailyBar).filter(DailyBar.contract == c.code).delete()
db.query(PositionRanking).filter(PositionRanking.contract_code == c.code).delete()
db.delete(p)
db.commit()
return RedirectResponse("/admin/?tab=product", status_code=303)
@@ -144,6 +145,14 @@ def sync_single(contract_code: str):
return RedirectResponse(f"/admin/?tab=sync&synced={count}", status_code=303)
@router.post("/sync-positions")
def sync_positions(request: Request):
results = sync_position_rankings()
total = sum(results.values())
print(f"[sync] Position rankings: {total} rows across {len(results)} contracts")
return RedirectResponse(f"/admin/?tab=sync&pos_synced={total}", status_code=303)
@router.post("/sync/product/{product_id}")
def sync_product(product_id: int, request: Request, db: Session = Depends(get_db)):
contracts = db.query(Contract).filter(
-10
View File
@@ -9,16 +9,6 @@ router = APIRouter(prefix="/auth", tags=["auth"])
SESSION_COOKIE = "ft_session"
def get_current_user(request: Request, db: Session = Depends(get_db)) -> User | None:
user_id = request.cookies.get(SESSION_COOKIE)
if not user_id:
return None
try:
return db.query(User).filter(User.id == int(user_id)).first()
except (ValueError, TypeError):
return None
@router.get("/login", response_class=HTMLResponse)
def login_page(request: Request):
template = request.app.state.templates.get_template("login.html")
+67 -4
View File
@@ -1,13 +1,15 @@
import math
from datetime import date, timedelta
from fastapi import APIRouter, Depends, Request
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
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]:
@@ -47,7 +49,13 @@ def contract_index(request: Request, db: Session = Depends(get_db)):
@router.get("/{contract}", response_class=HTMLResponse)
def contract_detail(request: Request, contract: str, db: Session = Depends(get_db)):
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)
@@ -56,9 +64,17 @@ def contract_detail(request: Request, contract: str, db: Session = Depends(get_d
.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 bar in bars:
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 "-",
@@ -72,6 +88,45 @@ def contract_detail(request: Request, contract: str, db: Session = Depends(get_d
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,
})
# 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
@@ -91,9 +146,17 @@ def contract_detail(request: Request, contract: str, db: Session = Depends(get_d
contracts=active_contracts,
rows=rows,
latest=latest,
pos_dates=pos_dates,
selected_pos_date=selected_date,
pos_data=pos_data,
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,
)
)