修复数据采集和清理的bug,新增持仓排名功能,日线数据分页
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user