Files
finance-talk/ft-app/app/routers/positions.py
T
2026-07-28 18:10:28 +08:00

193 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 Round, Position, Contract, Product
router = APIRouter(prefix="/positions", tags=["positions"])
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]
def get_product_contracts(db: Session) -> dict:
result: dict[str, list[str]] = {}
products = db.query(Product).order_by(Product.code).all()
for p in products:
codes = [c.code for c in p.contracts if c.is_active]
if codes:
result[p.code] = codes
return result
@router.get("/", response_class=HTMLResponse)
def positions_page(request: Request, db: Session = Depends(get_db)):
active_rounds = (
db.query(Round)
.filter(Round.status == "active")
.order_by(Round.started_at.desc())
.all()
)
# Compute stop-profit threshold: latest position A × point_value per round
round_thresholds: dict[int, dict] = {}
for r in active_rounds:
if r.positions:
latest_a = r.positions[-1].amp_threshold
else:
latest_a = 0
contract = db.query(Contract).filter(Contract.code == r.contract_code).first()
pv = contract.product.point_value if contract and contract.product else 20
round_thresholds[r.id] = {
"amp": latest_a,
"point_value": pv,
"daily_hands": r.daily_hands,
"threshold": latest_a * pv * r.daily_hands,
}
template = request.app.state.templates.get_template("positions.html")
return HTMLResponse(
template.render(
request=request,
active_nav="positions",
contracts=get_active_contracts(db),
product_contracts=get_product_contracts(db),
active_rounds=active_rounds,
round_thresholds=round_thresholds,
)
)
@router.post("/round")
def create_round(
request: Request,
contract_code: str = Form(...),
daily_hands: int = Form(1),
db: Session = Depends(get_db),
):
code = contract_code.upper()
existing = db.query(Round).filter(
Round.status == "active", Round.contract_code == code
).first()
if existing:
return RedirectResponse(f"/positions/?error={code} 已有活跃轮次", status_code=303)
r = Round(
contract_code=code,
daily_hands=daily_hands,
status="active",
started_at=date.today(),
)
db.add(r)
db.commit()
return RedirectResponse("/positions/", status_code=303)
@router.post("/{round_id}/add")
def add_position(
request: Request,
round_id: int,
open_price: int = Form(...),
amp_threshold: int = Form(...),
lock_price: str = Form(""),
hands: int = Form(1),
db: Session = Depends(get_db),
):
r = db.query(Round).filter(Round.id == round_id).first()
if not r or r.status != "active":
return RedirectResponse("/positions/?error=轮次不存在或已结束", status_code=303)
final_lock = int(lock_price) if lock_price else open_price + amp_threshold
p = Position(
round_id=r.id,
open_price=open_price,
amp_threshold=amp_threshold,
lock_price=final_lock,
hands=hands,
opened_at=date.today(),
)
db.add(p)
db.commit()
return RedirectResponse("/positions/", status_code=303)
@router.post("/{position_id}/lock")
def lock_position(
position_id: int,
lock_price: str = Form(""),
db: Session = Depends(get_db),
):
p = db.query(Position).filter(Position.id == position_id).first()
if p and p.locked_count < p.hands:
if lock_price:
p.lock_price = int(lock_price)
p.locked_count += 1
db.commit()
return RedirectResponse("/positions/", status_code=303)
@router.post("/{position_id}/unlock")
def unlock_position(position_id: int, db: Session = Depends(get_db)):
p = db.query(Position).filter(Position.id == position_id).first()
if p and p.locked_count > 0:
p.locked_count -= 1
db.commit()
return RedirectResponse("/positions/", status_code=303)
@router.post("/{position_id}/update-lock-price")
def update_lock_price(
position_id: int,
lock_price: int = Form(...),
db: Session = Depends(get_db),
):
p = db.query(Position).filter(Position.id == position_id).first()
if p and p.locked_count == 0:
p.lock_price = lock_price
db.commit()
return RedirectResponse("/positions/", status_code=303)
@router.post("/{round_id}/max-locks")
def update_max_locks(
round_id: int,
max_locks: int = Form(...),
db: Session = Depends(get_db),
):
r = db.query(Round).filter(Round.id == round_id).first()
if r and r.status == "active" and 1 <= max_locks <= 10:
r.max_locks = max_locks
db.commit()
return RedirectResponse("/positions/", status_code=303)
@router.post("/{round_id}/close")
def close_round(
request: Request,
round_id: int,
result: str = Form(...),
db: Session = Depends(get_db),
):
r = db.query(Round).filter(Round.id == round_id).first()
if r and r.status == "active":
r.status = result
db.commit()
return RedirectResponse("/positions/", status_code=303)
@router.post("/{round_id}/delete")
def delete_round(round_id: int, db: Session = Depends(get_db)):
r = db.query(Round).filter(Round.id == round_id).first()
if r:
db.delete(r)
db.commit()
return RedirectResponse("/positions/", status_code=303)