搭建期货量化管理后台 MVP,支持行情查看、博弈分析、持仓录入、品种合约管理
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
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
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||
|
||||
EXCHANGES = ["CZCE", "DCE", "SHFE", "CFFEX", "INE"]
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
def admin_page(request: Request, db: Session = Depends(get_db)):
|
||||
products = db.query(Product).order_by(Product.code).all()
|
||||
|
||||
product_data = []
|
||||
for p in products:
|
||||
contracts = (
|
||||
db.query(Contract)
|
||||
.filter(Contract.product_id == p.id)
|
||||
.order_by(Contract.code)
|
||||
.all()
|
||||
)
|
||||
product_data.append({
|
||||
"id": p.id,
|
||||
"code": p.code,
|
||||
"name": p.name,
|
||||
"exchange": p.exchange,
|
||||
"contracts": [
|
||||
{"id": c.id, "code": c.code, "name": c.name, "is_active": c.is_active}
|
||||
for c in contracts
|
||||
],
|
||||
})
|
||||
|
||||
template = request.app.state.templates.get_template("admin.html")
|
||||
return HTMLResponse(
|
||||
template.render(
|
||||
request=request,
|
||||
active_nav="admin",
|
||||
products=product_data,
|
||||
exchanges=EXCHANGES,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/product")
|
||||
def create_product(
|
||||
request: Request,
|
||||
code: str = Form(...),
|
||||
name: str = Form(...),
|
||||
exchange: str = Form(...),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
existing = db.query(Product).filter(Product.code == code.upper()).first()
|
||||
if not existing:
|
||||
p = Product(code=code.upper(), name=name, exchange=exchange)
|
||||
db.add(p)
|
||||
db.commit()
|
||||
return RedirectResponse("/admin/", status_code=303)
|
||||
|
||||
|
||||
@router.post("/contract")
|
||||
def create_contract(
|
||||
request: Request,
|
||||
product_id: int = Form(...),
|
||||
code: str = Form(...),
|
||||
name: str = Form(...),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
existing = db.query(Contract).filter(Contract.code == code.upper()).first()
|
||||
if not existing:
|
||||
c = Contract(
|
||||
product_id=product_id,
|
||||
code=code.upper(),
|
||||
name=name,
|
||||
is_active=True,
|
||||
)
|
||||
db.add(c)
|
||||
db.commit()
|
||||
return RedirectResponse("/admin/", status_code=303)
|
||||
|
||||
|
||||
@router.post("/contract/{contract_id}/toggle")
|
||||
def toggle_contract(contract_id: int, db: Session = Depends(get_db)):
|
||||
c = db.query(Contract).filter(Contract.id == contract_id).first()
|
||||
if c:
|
||||
c.is_active = not c.is_active
|
||||
db.commit()
|
||||
return RedirectResponse("/admin/", status_code=303)
|
||||
|
||||
|
||||
@router.post("/contract/{contract_id}/delete")
|
||||
def delete_contract(contract_id: int, db: Session = Depends(get_db)):
|
||||
c = db.query(Contract).filter(Contract.id == contract_id).first()
|
||||
if c:
|
||||
db.delete(c)
|
||||
db.commit()
|
||||
return RedirectResponse("/admin/", status_code=303)
|
||||
|
||||
|
||||
@router.post("/product/{product_id}/delete")
|
||||
def delete_product(product_id: int, db: Session = Depends(get_db)):
|
||||
p = db.query(Product).filter(Product.id == product_id).first()
|
||||
if p:
|
||||
db.delete(p)
|
||||
db.commit()
|
||||
return RedirectResponse("/admin/", status_code=303)
|
||||
@@ -0,0 +1,102 @@
|
||||
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.engine.game_theory import net_position, net_pnl, format_pnl
|
||||
|
||||
router = APIRouter(prefix="/analysis", tags=["analysis"])
|
||||
|
||||
INSTITUTIONS = ["中信期货", "高盛期货", "国泰君安期货", "华泰期货", "东证期货", "银河期货"]
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
def analysis_page(request: Request, db: Session = Depends(get_db)):
|
||||
latest_snap = (
|
||||
db.query(PositionSnapshot.date)
|
||||
.order_by(PositionSnapshot.date.desc())
|
||||
.first()
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
latest_date = latest_snap[0]
|
||||
current_price = 913 # TODO: fetch from daily_bars or akshare
|
||||
|
||||
rows = []
|
||||
totals = {"long_pos": 0, "short_pos": 0, "net_pos": 0, "pnl": 0.0}
|
||||
total_net_short = 0
|
||||
pnl_sum = 0.0
|
||||
|
||||
for inst in INSTITUTIONS:
|
||||
long = (
|
||||
db.query(PositionSnapshot)
|
||||
.filter(
|
||||
PositionSnapshot.institution == inst,
|
||||
PositionSnapshot.direction == "long",
|
||||
PositionSnapshot.date == latest_date,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
short = (
|
||||
db.query(PositionSnapshot)
|
||||
.filter(
|
||||
PositionSnapshot.institution == inst,
|
||||
PositionSnapshot.direction == "short",
|
||||
PositionSnapshot.date == latest_date,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
long_pos = long.position if long else 0
|
||||
short_pos = short.position if short else 0
|
||||
long_cost = long.avg_cost if long else 0
|
||||
short_cost = short.avg_cost if short else 0
|
||||
|
||||
np = net_position(long_pos, short_pos)
|
||||
pnl = 0.0
|
||||
if np < 0:
|
||||
pnl = net_pnl(np, short_cost, current_price)
|
||||
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)
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
template = request.app.state.templates.get_template("analysis.html")
|
||||
return HTMLResponse(
|
||||
template.render(
|
||||
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),
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,93 @@
|
||||
from datetime import date
|
||||
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 DailyBar, Contract
|
||||
|
||||
router = APIRouter(prefix="/contracts", tags=["contracts"])
|
||||
|
||||
WEEKDAY_ZH = {0: "周一", 1: "周二", 2: "周三", 3: "周四", 4: "周五", 5: "周六", 6: "周日"}
|
||||
|
||||
|
||||
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)):
|
||||
active_contracts = get_active_contracts(db)
|
||||
|
||||
latest = (
|
||||
db.query(DailyBar)
|
||||
.filter(DailyBar.contract.in_(active_contracts))
|
||||
.order_by(DailyBar.date.desc())
|
||||
.all()
|
||||
)
|
||||
contract_bars = {}
|
||||
for bar in latest:
|
||||
if bar.contract not in contract_bars:
|
||||
contract_bars[bar.contract] = bar
|
||||
|
||||
total_bars = db.query(DailyBar).count()
|
||||
latest_bar = (
|
||||
db.query(DailyBar).order_by(DailyBar.date.desc()).first()
|
||||
)
|
||||
|
||||
template = request.app.state.templates.get_template("index.html")
|
||||
return HTMLResponse(
|
||||
template.render(
|
||||
request=request,
|
||||
active_nav="contracts",
|
||||
contracts=active_contracts,
|
||||
contract_bars=contract_bars,
|
||||
total_bars=total_bars,
|
||||
latest_date=latest_bar.date.strftime("%Y-%m-%d") if latest_bar else "—",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{contract}", response_class=HTMLResponse)
|
||||
def contract_detail(request: Request, contract: str, db: Session = Depends(get_db)):
|
||||
active_contracts = get_active_contracts(db)
|
||||
bars = (
|
||||
db.query(DailyBar)
|
||||
.filter(DailyBar.contract == contract.upper())
|
||||
.order_by(DailyBar.date)
|
||||
.all()
|
||||
)
|
||||
|
||||
rows = []
|
||||
for bar in bars:
|
||||
rows.append({
|
||||
"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[-1] if bars else None
|
||||
|
||||
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,
|
||||
row_count=len(rows),
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
from datetime import date
|
||||
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 PositionSnapshot
|
||||
|
||||
router = APIRouter(prefix="/input", tags=["input"])
|
||||
|
||||
INSTITUTIONS = ["中信期货", "高盛期货", "国泰君安期货", "华泰期货", "东证期货", "银河期货"]
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
def input_page(request: Request):
|
||||
template = request.app.state.templates.get_template("input.html")
|
||||
return HTMLResponse(
|
||||
template.render(request=request, active_nav="input", institutions=INSTITUTIONS)
|
||||
)
|
||||
|
||||
|
||||
@router.post("/")
|
||||
def submit_position(
|
||||
request: Request,
|
||||
institution: str = Form(...),
|
||||
direction: str = Form(...),
|
||||
date_str: str = Form(...),
|
||||
position: int = Form(...),
|
||||
avg_cost: float = Form(...),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
d = date.fromisoformat(date_str)
|
||||
|
||||
prev = (
|
||||
db.query(PositionSnapshot)
|
||||
.filter(
|
||||
PositionSnapshot.institution == institution,
|
||||
PositionSnapshot.direction == direction,
|
||||
PositionSnapshot.date < d,
|
||||
)
|
||||
.order_by(PositionSnapshot.date.desc())
|
||||
.first()
|
||||
)
|
||||
delta = position - prev.position if prev else 0
|
||||
|
||||
existing = (
|
||||
db.query(PositionSnapshot)
|
||||
.filter(
|
||||
PositionSnapshot.institution == institution,
|
||||
PositionSnapshot.direction == direction,
|
||||
PositionSnapshot.date == d,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if existing:
|
||||
existing.position = position
|
||||
existing.delta = delta
|
||||
existing.avg_cost = avg_cost
|
||||
else:
|
||||
snap = PositionSnapshot(
|
||||
institution=institution,
|
||||
direction=direction,
|
||||
date=d,
|
||||
position=position,
|
||||
delta=delta,
|
||||
avg_cost=avg_cost,
|
||||
)
|
||||
db.add(snap)
|
||||
|
||||
db.commit()
|
||||
return RedirectResponse("/analysis", status_code=303)
|
||||
Reference in New Issue
Block a user