搭建期货量化管理后台 MVP,支持行情查看、博弈分析、持仓录入、品种合约管理
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
data/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY app/ ./app/
|
||||||
|
|
||||||
|
RUN mkdir -p /app/data
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker, DeclarativeBase
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||||
|
DATA_DIR.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
DATABASE_URL = f"sqlite:///{DATA_DIR / 'ft.db'}"
|
||||||
|
|
||||||
|
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
||||||
|
SessionLocal = sessionmaker(bind=engine)
|
||||||
|
|
||||||
|
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_db():
|
||||||
|
db = SessionLocal()
|
||||||
|
try:
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"""博弈分析计算引擎"""
|
||||||
|
|
||||||
|
|
||||||
|
def net_position(long_pos: int, short_pos: int) -> int:
|
||||||
|
"""净持仓 = 多单 - 空单。正=净多, 负=净空"""
|
||||||
|
return long_pos - short_pos
|
||||||
|
|
||||||
|
|
||||||
|
def net_pnl(net_pos: int, avg_cost: float, current_price: float) -> float:
|
||||||
|
"""净盈亏 = 净持仓 × (现价 - 成本均价) × 20"""
|
||||||
|
return net_pos * (current_price - avg_cost) * 20
|
||||||
|
|
||||||
|
|
||||||
|
def cost_delta(old_cost: float, new_cost: float) -> float:
|
||||||
|
"""成本变化 = 新均价 - 旧均价"""
|
||||||
|
return round(new_cost - old_cost, 2)
|
||||||
|
|
||||||
|
|
||||||
|
def format_pnl(pnl_yuan: float) -> str:
|
||||||
|
"""格式化盈亏为亿/万"""
|
||||||
|
yi = abs(pnl_yuan) / 1e8
|
||||||
|
if yi >= 0.01:
|
||||||
|
sign = "+" if pnl_yuan >= 0 else "-"
|
||||||
|
return f"{sign}{yi:.2f}亿"
|
||||||
|
wan = abs(pnl_yuan) / 1e4
|
||||||
|
return f"{'+' if pnl_yuan >= 0 else '-'}{wan:.1f}万"
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"""振幅锁仓策略计算引擎"""
|
||||||
|
|
||||||
|
|
||||||
|
def compute_amp_5d(diffs: list[float]) -> float:
|
||||||
|
"""近5日均振幅 = round(mean of 5 diffs)"""
|
||||||
|
if len(diffs) < 5:
|
||||||
|
return 0.0
|
||||||
|
return round(sum(diffs[-5:]) / 5)
|
||||||
|
|
||||||
|
|
||||||
|
def check_stop_profit(
|
||||||
|
active_profits: list[float],
|
||||||
|
locked_losses: list[float],
|
||||||
|
daily_hands: int,
|
||||||
|
amp_threshold: float,
|
||||||
|
) -> bool:
|
||||||
|
"""止盈条件: total_floating_profit >= N * A"""
|
||||||
|
total = sum(active_profits) + sum(locked_losses)
|
||||||
|
return total >= daily_hands * amp_threshold
|
||||||
|
|
||||||
|
|
||||||
|
def check_lock(
|
||||||
|
open_price: float,
|
||||||
|
current_price: float,
|
||||||
|
lock_threshold: float,
|
||||||
|
) -> bool:
|
||||||
|
"""锁仓条件: 空单亏损 >= 开仓时的振幅阈值"""
|
||||||
|
return (current_price - open_price) >= lock_threshold
|
||||||
|
|
||||||
|
|
||||||
|
def should_meltdown(lock_count: int) -> bool:
|
||||||
|
"""3锁熔断"""
|
||||||
|
return lock_count >= 3
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
from app.database import engine, Base
|
||||||
|
from app.seed import seed
|
||||||
|
from app.routers import contracts, analysis, data_input, admin
|
||||||
|
|
||||||
|
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
||||||
|
|
||||||
|
|
||||||
|
def setup_jinja(app: FastAPI):
|
||||||
|
env = Environment(loader=FileSystemLoader(str(TEMPLATES_DIR)))
|
||||||
|
app.state.templates = env
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
setup_jinja(app)
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
seed()
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(title="期货量化系统", lifespan=lifespan)
|
||||||
|
|
||||||
|
app.include_router(contracts.router)
|
||||||
|
app.include_router(analysis.router)
|
||||||
|
app.include_router(data_input.router)
|
||||||
|
app.include_router(admin.router)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
def root():
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
|
return RedirectResponse("/contracts")
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
from datetime import date
|
||||||
|
from sqlalchemy import String, Integer, Float, Date, ForeignKey, Boolean, UniqueConstraint
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
from app.database import Base
|
||||||
|
|
||||||
|
|
||||||
|
class Product(Base):
|
||||||
|
__tablename__ = "products"
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
code: Mapped[str] = mapped_column(String(10), unique=True, index=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(20))
|
||||||
|
exchange: Mapped[str] = mapped_column(String(10), default="CZCE")
|
||||||
|
|
||||||
|
contracts: Mapped[list["Contract"]] = relationship(
|
||||||
|
back_populates="product", cascade="all, delete-orphan"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Contract(Base):
|
||||||
|
__tablename__ = "contracts"
|
||||||
|
__table_args__ = (UniqueConstraint("code"),)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
product_id: Mapped[int] = mapped_column(ForeignKey("products.id"), index=True)
|
||||||
|
code: Mapped[str] = mapped_column(String(10), unique=True, index=True)
|
||||||
|
name: Mapped[str] = mapped_column(String(30))
|
||||||
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
|
|
||||||
|
product: Mapped["Product"] = relationship(back_populates="contracts")
|
||||||
|
|
||||||
|
|
||||||
|
class DailyBar(Base):
|
||||||
|
__tablename__ = "daily_bars"
|
||||||
|
__table_args__ = (UniqueConstraint("contract", "date"),)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
contract: Mapped[str] = mapped_column(String(10), index=True)
|
||||||
|
date: Mapped[date] = mapped_column(Date, index=True)
|
||||||
|
open: Mapped[float] = mapped_column(Float)
|
||||||
|
close: Mapped[float] = mapped_column(Float)
|
||||||
|
high: Mapped[float] = mapped_column(Float)
|
||||||
|
low: Mapped[float] = mapped_column(Float)
|
||||||
|
amp_5d: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def diff(self) -> float:
|
||||||
|
"""最高-最低价差"""
|
||||||
|
return self.high - self.low
|
||||||
|
|
||||||
|
|
||||||
|
class PositionSnapshot(Base):
|
||||||
|
__tablename__ = "position_snapshots"
|
||||||
|
__table_args__ = (UniqueConstraint("institution", "direction", "date"),)
|
||||||
|
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
institution: Mapped[str] = mapped_column(String(20), index=True)
|
||||||
|
direction: Mapped[str] = mapped_column(String(10)) # "long" or "short"
|
||||||
|
date: Mapped[date] = mapped_column(Date, index=True)
|
||||||
|
position: Mapped[int] = mapped_column(Integer)
|
||||||
|
delta: Mapped[int] = mapped_column(Integer, default=0)
|
||||||
|
avg_cost: Mapped[float] = mapped_column(Float)
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,289 @@
|
|||||||
|
"""Seed database from existing data files. Run once manually or on first start."""
|
||||||
|
from datetime import date
|
||||||
|
from app.database import engine, Base, SessionLocal
|
||||||
|
from app.models import DailyBar, PositionSnapshot, Product, Contract
|
||||||
|
from app.engine.lock_strategy import compute_amp_5d
|
||||||
|
|
||||||
|
# --- Seed OHLCV data ---
|
||||||
|
SEED_CONTRACT = "FG2609"
|
||||||
|
SEED_BARS: list[dict] = [
|
||||||
|
{"date": "2026-07-01", "open": 972, "close": 961, "high": 972, "low": 956},
|
||||||
|
{"date": "2026-07-02", "open": 960, "close": 966, "high": 972, "low": 957},
|
||||||
|
{"date": "2026-07-03", "open": 965, "close": 973, "high": 979, "low": 962},
|
||||||
|
{"date": "2026-07-06", "open": 977, "close": 967, "high": 982, "low": 964},
|
||||||
|
{"date": "2026-07-07", "open": 966, "close": 952, "high": 966, "low": 951},
|
||||||
|
{"date": "2026-07-08", "open": 954, "close": 962, "high": 966, "low": 951},
|
||||||
|
{"date": "2026-07-09", "open": 963, "close": 957, "high": 964, "low": 949},
|
||||||
|
{"date": "2026-07-10", "open": 956, "close": 965, "high": 974, "low": 952},
|
||||||
|
{"date": "2026-07-13", "open": 965, "close": 952, "high": 967, "low": 950},
|
||||||
|
{"date": "2026-07-14", "open": 954, "close": 951, "high": 956, "low": 944},
|
||||||
|
{"date": "2026-07-15", "open": 950, "close": 949, "high": 958, "low": 944},
|
||||||
|
{"date": "2026-07-16", "open": 949, "close": 929, "high": 952, "low": 927},
|
||||||
|
{"date": "2026-07-17", "open": 931, "close": 900, "high": 931, "low": 899},
|
||||||
|
{"date": "2026-07-20", "open": 902, "close": 890, "high": 908, "low": 889},
|
||||||
|
{"date": "2026-07-21", "open": 891, "close": 904, "high": 910, "low": 891},
|
||||||
|
{"date": "2026-07-22", "open": 908, "close": 913, "high": 918, "low": 900},
|
||||||
|
{"date": "2026-07-23", "open": 914, "close": 899, "high": 917, "low": 892},
|
||||||
|
{"date": "2026-07-24", "open": 900, "close": 908, "high": 912, "low": 891},
|
||||||
|
]
|
||||||
|
|
||||||
|
# --- Position snapshots (from info.txt) ---
|
||||||
|
POSITION_DATA_SHORT: list[dict] = [
|
||||||
|
("中信期货", "short", "2026-07-22", 193030, -22032, 995.26),
|
||||||
|
("中信期货", "short", "2026-07-21", 215062, -1029, 995.26),
|
||||||
|
("中信期货", "short", "2026-07-20", 216091, -18178, 995.26),
|
||||||
|
("中信期货", "short", "2026-07-17", 234269, -31, 995.26),
|
||||||
|
("中信期货", "short", "2026-07-16", 234300, 35258, 995.26),
|
||||||
|
("中信期货", "short", "2026-07-15", 199042, 5294, 1005.40),
|
||||||
|
("中信期货", "short", "2026-07-14", 193748, 4117, 1006.94),
|
||||||
|
("中信期货", "short", "2026-07-13", 189631, 18219, 1008.16),
|
||||||
|
("中信期货", "short", "2026-07-10", 171412, -17319, 1013.49),
|
||||||
|
("中信期货", "short", "2026-07-09", 188731, 9860, 1013.49),
|
||||||
|
("中信期货", "short", "2026-07-08", 178871, -19747, 1016.71),
|
||||||
|
("中信期货", "short", "2026-07-07", 198618, 25262, 1016.71),
|
||||||
|
("中信期货", "short", "2026-07-06", 173356, 2998, 1025.41),
|
||||||
|
("中信期货", "short", "2026-07-03", 170358, -11691, 1026.30),
|
||||||
|
("中信期货", "short", "2026-07-02", 182049, 1284, 1026.30),
|
||||||
|
("中信期货", "long", "2026-07-22", 63971, -10850, 988.16),
|
||||||
|
("中信期货", "long", "2026-07-21", 74821, 2587, 988.16),
|
||||||
|
("中信期货", "long", "2026-07-20", 72234, -84, 991.28),
|
||||||
|
("中信期货", "long", "2026-07-17", 72318, 6928, 991.28),
|
||||||
|
("中信期货", "long", "2026-07-16", 65390, 6950, 999.15),
|
||||||
|
("中信期货", "long", "2026-07-15", 58440, -1227, 1006.42),
|
||||||
|
("中信期货", "long", "2026-07-14", 59667, 887, 1006.42),
|
||||||
|
("中信期货", "long", "2026-07-13", 58780, -4908, 1007.26),
|
||||||
|
("中信期货", "long", "2026-07-10", 63688, 5795, 1007.26),
|
||||||
|
("中信期货", "long", "2026-07-09", 57893, -3193, 1011.59),
|
||||||
|
("中信期货", "long", "2026-07-08", 61086, 2092, 1011.59),
|
||||||
|
("中信期货", "long", "2026-07-07", 58994, 8116, 1013.45),
|
||||||
|
("中信期货", "long", "2026-07-06", 50878, -14738, 1022.46),
|
||||||
|
("中信期货", "long", "2026-07-03", 65616, 9777, 1022.46),
|
||||||
|
("中信期货", "long", "2026-07-02", 55839, -2024, 1031.30),
|
||||||
|
("高盛期货", "short", "2026-07-22", 188460, -14927, 997.89),
|
||||||
|
("高盛期货", "short", "2026-07-21", 203387, -15903, 997.89),
|
||||||
|
("高盛期货", "short", "2026-07-20", 219290, 1220, 997.89),
|
||||||
|
("高盛期货", "short", "2026-07-17", 218070, 14452, 998.44),
|
||||||
|
("高盛期货", "short", "2026-07-16", 203618, 12879, 1004.22),
|
||||||
|
("高盛期货", "short", "2026-07-15", 190739, -5701, 1008.69),
|
||||||
|
("高盛期货", "short", "2026-07-14", 196440, 1711, 1008.69),
|
||||||
|
("高盛期货", "short", "2026-07-13", 194729, 418, 1009.20),
|
||||||
|
("高盛期货", "short", "2026-07-10", 194311, -5203, 1009.31),
|
||||||
|
("高盛期货", "short", "2026-07-09", 199514, 7653, 1009.31),
|
||||||
|
("高盛期货", "short", "2026-07-08", 191861, 1394, 1011.47),
|
||||||
|
("高盛期货", "short", "2026-07-07", 190467, 12046, 1011.86),
|
||||||
|
("高盛期货", "short", "2026-07-06", 178421, 578, 1015.56),
|
||||||
|
("高盛期货", "short", "2026-07-03", 177843, 1049, 1015.69),
|
||||||
|
("高盛期货", "short", "2026-07-02", 176794, 19121, 1015.95),
|
||||||
|
("国泰君安期货", "short", "2026-07-22", 168449, -9289, 1018.54),
|
||||||
|
("国泰君安期货", "short", "2026-07-21", 177738, -10380, 1018.54),
|
||||||
|
("国泰君安期货", "short", "2026-07-20", 188118, -731, 1018.54),
|
||||||
|
("国泰君安期货", "short", "2026-07-17", 188849, -12984, 1018.54),
|
||||||
|
("国泰君安期货", "short", "2026-07-16", 201833, -5144, 1018.54),
|
||||||
|
("国泰君安期货", "short", "2026-07-15", 206977, 2787, 1018.54),
|
||||||
|
("国泰君安期货", "short", "2026-07-14", 204190, -2399, 1019.49),
|
||||||
|
("国泰君安期货", "short", "2026-07-13", 206589, 24344, 1019.49),
|
||||||
|
("国泰君安期货", "short", "2026-07-10", 182245, -4391, 1027.70),
|
||||||
|
("国泰君安期货", "short", "2026-07-09", 186636, -6510, 1027.70),
|
||||||
|
("国泰君安期货", "short", "2026-07-08", 193146, -6957, 1027.70),
|
||||||
|
("国泰君安期货", "short", "2026-07-07", 200103, 9696, 1027.70),
|
||||||
|
("国泰君安期货", "short", "2026-07-06", 190407, 1344, 1031.30),
|
||||||
|
("国泰君安期货", "short", "2026-07-03", 189063, -1588, 1031.70),
|
||||||
|
("国泰君安期货", "short", "2026-07-02", 190651, 4656, 1031.70),
|
||||||
|
("国泰君安期货", "long", "2026-07-22", 144325, -6262, 991.07),
|
||||||
|
("国泰君安期货", "long", "2026-07-21", 150587, 17790, 991.07),
|
||||||
|
("国泰君安期货", "long", "2026-07-20", 132797, 3578, 1003.13),
|
||||||
|
("国泰君安期货", "long", "2026-07-17", 129219, 12066, 1005.99),
|
||||||
|
("国泰君安期货", "long", "2026-07-16", 117153, 8387, 1015.15),
|
||||||
|
("国泰君安期货", "long", "2026-07-15", 108766, 5534, 1021.10),
|
||||||
|
("国泰君安期货", "long", "2026-07-14", 103232, 4815, 1024.97),
|
||||||
|
("国泰君安期货", "long", "2026-07-13", 98417, 4839, 1028.58),
|
||||||
|
("国泰君安期货", "long", "2026-07-10", 93578, -1293, 1032.23),
|
||||||
|
("国泰君安期货", "long", "2026-07-09", 94871, -807, 1032.23),
|
||||||
|
("国泰君安期货", "long", "2026-07-08", 95678, -5169, 1032.23),
|
||||||
|
("国泰君安期货", "long", "2026-07-07", 100847, 3412, 1032.23),
|
||||||
|
("国泰君安期货", "long", "2026-07-06", 97435, -4615, 1034.87),
|
||||||
|
("国泰君安期货", "long", "2026-07-03", 102050, 2169, 1034.87),
|
||||||
|
("国泰君安期货", "long", "2026-07-02", 99881, 9801, 1036.23),
|
||||||
|
("华泰期货", "short", "2026-07-22", 80564, -8762, 976.69),
|
||||||
|
("华泰期货", "short", "2026-07-21", 89326, 9844, 976.69),
|
||||||
|
("华泰期货", "short", "2026-07-20", 79482, 1751, 986.06),
|
||||||
|
("华泰期货", "short", "2026-07-17", 77731, -8367, 988.00),
|
||||||
|
("华泰期货", "short", "2026-07-16", 86098, 11358, 988.00),
|
||||||
|
("华泰期货", "short", "2026-07-15", 74740, 3028, 995.60),
|
||||||
|
("华泰期货", "short", "2026-07-14", 71712, -2199, 997.57),
|
||||||
|
("华泰期货", "short", "2026-07-13", 73911, 10715, 997.57),
|
||||||
|
("华泰期货", "short", "2026-07-10", 63196, -3512, 1004.28),
|
||||||
|
("华泰期货", "short", "2026-07-09", 66708, 3827, 1004.28),
|
||||||
|
("华泰期货", "short", "2026-07-08", 62881, -10077, 1007.27),
|
||||||
|
("华泰期货", "short", "2026-07-07", 72958, 12276, 1007.27),
|
||||||
|
("华泰期货", "short", "2026-07-06", 60682, -6933, 1017.45),
|
||||||
|
("华泰期货", "short", "2026-07-03", 65615, -5963, 1017.45),
|
||||||
|
("华泰期货", "short", "2026-07-02", 71558, -2128, 1017.45),
|
||||||
|
("华泰期货", "long", "2026-07-22", 68791, -5130, 1002.62),
|
||||||
|
("华泰期货", "long", "2026-07-21", 73921, -274, 1002.62),
|
||||||
|
("华泰期货", "long", "2026-07-20", 74195, 840, 1002.62),
|
||||||
|
("华泰期货", "long", "2026-07-17", 73355, 7012, 1003.79),
|
||||||
|
("华泰期货", "long", "2026-07-16", 66343, 4562, 1012.97),
|
||||||
|
("华泰期货", "long", "2026-07-15", 61781, -1562, 1018.50),
|
||||||
|
("华泰期货", "long", "2026-07-14", 63343, 2364, 1018.50),
|
||||||
|
("华泰期货", "long", "2026-07-13", 60979, -1843, 1021.12),
|
||||||
|
("华泰期货", "long", "2026-07-10", 62822, 1161, 1021.12),
|
||||||
|
("华泰期货", "long", "2026-07-09", 61661, 162, 1022.19),
|
||||||
|
("华泰期货", "long", "2026-07-08", 61499, 82, 1022.37),
|
||||||
|
("华泰期货", "long", "2026-07-07", 61417, 6050, 1022.46),
|
||||||
|
("华泰期货", "long", "2026-07-06", 55367, -2699, 1029.61),
|
||||||
|
("华泰期货", "long", "2026-07-03", 58066, -4006, 1029.61),
|
||||||
|
("华泰期货", "long", "2026-07-02", 62072, -2935, 1029.61),
|
||||||
|
("东证期货", "short", "2026-07-22", 112614, -18937, 965.67),
|
||||||
|
("东证期货", "short", "2026-07-21", 131551, -45486, 965.67),
|
||||||
|
("东证期货", "short", "2026-07-20", 177037, -1629, 965.67),
|
||||||
|
("东证期货", "short", "2026-07-17", 178666, -4252, 965.67),
|
||||||
|
("东证期货", "short", "2026-07-16", 182918, 47376, 965.67),
|
||||||
|
("东证期货", "short", "2026-07-15", 135542, -13505, 975.35),
|
||||||
|
("东证期货", "short", "2026-07-14", 149047, -1108, 975.35),
|
||||||
|
("东证期货", "short", "2026-07-13", 150155, 40931, 975.35),
|
||||||
|
("东证期货", "short", "2026-07-10", 109224, -23556, 981.85),
|
||||||
|
("东证期货", "short", "2026-07-09", 132780, 25017, 981.85),
|
||||||
|
("东证期货", "short", "2026-07-08", 107763, -56346, 988.08),
|
||||||
|
("东证期货", "short", "2026-07-07", 164109, 37956, 988.08),
|
||||||
|
("东证期货", "short", "2026-07-06", 126153, 20536, 997.43),
|
||||||
|
("东证期货", "short", "2026-07-03", 105617, -10003, 1001.79),
|
||||||
|
("东证期货", "short", "2026-07-02", 115620, -16409, 1001.79),
|
||||||
|
("东证期货", "long", "2026-07-22", 85630, 6685, 965.35),
|
||||||
|
("东证期货", "long", "2026-07-21", 78945, 8359, 970.20),
|
||||||
|
("东证期货", "long", "2026-07-20", 70586, 2334, 978.40),
|
||||||
|
("东证期货", "long", "2026-07-17", 68252, -5918, 981.08),
|
||||||
|
("东证期货", "long", "2026-07-16", 74170, 10413, 981.08),
|
||||||
|
("东证期货", "long", "2026-07-15", 63757, 1640, 988.12),
|
||||||
|
("东证期货", "long", "2026-07-14", 62117, -2136, 989.15),
|
||||||
|
("东证期货", "long", "2026-07-13", 64253, -14158, 989.15),
|
||||||
|
("东证期货", "long", "2026-07-10", 78411, 7829, 989.15),
|
||||||
|
("东证期货", "long", "2026-07-09", 70582, -9231, 991.94),
|
||||||
|
("东证期货", "long", "2026-07-08", 79813, 11598, 991.94),
|
||||||
|
("东证期货", "long", "2026-07-07", 68215, 10382, 997.54),
|
||||||
|
("东证期货", "long", "2026-07-06", 57833, -30335, 1004.82),
|
||||||
|
("东证期货", "long", "2026-07-03", 88168, 20705, 1004.82),
|
||||||
|
("东证期货", "long", "2026-07-02", 67463, 3862, 1014.89),
|
||||||
|
("银河期货", "short", "2026-07-22", 62130, -3771, 1022.69),
|
||||||
|
("银河期货", "short", "2026-07-21", 65901, -3865, 1022.69),
|
||||||
|
("银河期货", "short", "2026-07-20", 69766, 1572, 1022.69),
|
||||||
|
("银河期货", "short", "2026-07-17", 68194, 431, 1025.51),
|
||||||
|
("银河期货", "short", "2026-07-16", 67763, -936, 1026.20),
|
||||||
|
("银河期货", "short", "2026-07-15", 68699, 372, 1026.20),
|
||||||
|
("银河期货", "short", "2026-07-14", 68327, 5179, 1026.62),
|
||||||
|
("银河期货", "short", "2026-07-13", 63148, -3119, 1032.83),
|
||||||
|
("银河期货", "short", "2026-07-10", 66267, -431, 1032.83),
|
||||||
|
("银河期货", "short", "2026-07-09", 66698, 1995, 1032.83),
|
||||||
|
("银河期货", "short", "2026-07-08", 64703, -5025, 1035.23),
|
||||||
|
("银河期货", "short", "2026-07-07", 69728, 6103, 1035.23),
|
||||||
|
("银河期货", "short", "2026-07-06", 63625, -1703, 1042.73),
|
||||||
|
("银河期货", "short", "2026-07-03", 65328, -3861, 1042.73),
|
||||||
|
("银河期货", "short", "2026-07-02", 69189, 1875, 1042.73),
|
||||||
|
("银河期货", "long", "2026-07-22", 57020, -7565, 1016.17),
|
||||||
|
("银河期货", "long", "2026-07-21", 64585, -4812, 1016.17),
|
||||||
|
("银河期货", "long", "2026-07-20", 69397, -2877, 1016.17),
|
||||||
|
("银河期货", "long", "2026-07-17", 72274, -4410, 1016.17),
|
||||||
|
("银河期货", "long", "2026-07-16", 76684, 4888, 1016.17),
|
||||||
|
("银河期货", "long", "2026-07-15", 71796, -3235, 1021.49),
|
||||||
|
("银河期货", "long", "2026-07-14", 75031, -456, 1021.49),
|
||||||
|
("银河期货", "long", "2026-07-13", 75487, 4361, 1021.49),
|
||||||
|
("银河期货", "long", "2026-07-10", 71126, -6491, 1025.39),
|
||||||
|
("银河期货", "long", "2026-07-09", 77617, 376, 1025.39),
|
||||||
|
("银河期货", "long", "2026-07-08", 77241, -3621, 1025.73),
|
||||||
|
("银河期货", "long", "2026-07-07", 80862, 10307, 1025.73),
|
||||||
|
("银河期货", "long", "2026-07-06", 70555, 2439, 1035.77),
|
||||||
|
("银河期货", "long", "2026-07-03", 68116, -4101, 1037.95),
|
||||||
|
("银河期货", "long", "2026-07-02", 72217, 490, 1037.95),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def seed():
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
db = SessionLocal()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- Seed products and contracts ---
|
||||||
|
if not db.query(Product).first():
|
||||||
|
fg = Product(code="FG", name="玻璃", exchange="CZCE")
|
||||||
|
db.add(fg)
|
||||||
|
db.flush()
|
||||||
|
db.add_all([
|
||||||
|
Contract(product_id=fg.id, code="FG2609", name="玻璃2609", is_active=True),
|
||||||
|
Contract(product_id=fg.id, code="FG2610", name="玻璃2610", is_active=True),
|
||||||
|
Contract(product_id=fg.id, code="FG2611", name="玻璃2611", is_active=True),
|
||||||
|
Contract(product_id=fg.id, code="FG2701", name="玻璃2701", is_active=True),
|
||||||
|
])
|
||||||
|
|
||||||
|
# --- Seed daily bars ---
|
||||||
|
existing_dates = {
|
||||||
|
(r.contract, r.date)
|
||||||
|
for r in db.query(DailyBar.contract, DailyBar.date).all()
|
||||||
|
}
|
||||||
|
|
||||||
|
bars_to_insert = []
|
||||||
|
for bar in SEED_BARS:
|
||||||
|
d = date.fromisoformat(bar["date"])
|
||||||
|
if (SEED_CONTRACT, d) not in existing_dates:
|
||||||
|
bars_to_insert.append(
|
||||||
|
DailyBar(
|
||||||
|
contract=SEED_CONTRACT,
|
||||||
|
date=d,
|
||||||
|
open=bar["open"],
|
||||||
|
close=bar["close"],
|
||||||
|
high=bar["high"],
|
||||||
|
low=bar["low"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if bars_to_insert:
|
||||||
|
db.add_all(bars_to_insert)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
# Compute amp_5d
|
||||||
|
seed_bars = (
|
||||||
|
db.query(DailyBar)
|
||||||
|
.filter(DailyBar.contract == SEED_CONTRACT)
|
||||||
|
.order_by(DailyBar.date)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
for i, bar in enumerate(seed_bars):
|
||||||
|
if i >= 5:
|
||||||
|
bar.amp_5d = compute_amp_5d([b.diff for b in seed_bars[i - 5 : i]])
|
||||||
|
|
||||||
|
# --- Seed position snapshots ---
|
||||||
|
existing_pos = {
|
||||||
|
(r.institution, r.direction, r.date)
|
||||||
|
for r in db.query(
|
||||||
|
PositionSnapshot.institution,
|
||||||
|
PositionSnapshot.direction,
|
||||||
|
PositionSnapshot.date,
|
||||||
|
).all()
|
||||||
|
}
|
||||||
|
|
||||||
|
snaps = []
|
||||||
|
for inst, direction, date_str, pos, delta, cost in POSITION_DATA_SHORT:
|
||||||
|
d = date.fromisoformat(date_str)
|
||||||
|
if (inst, direction, d) not in existing_pos:
|
||||||
|
snaps.append(
|
||||||
|
PositionSnapshot(
|
||||||
|
institution=inst,
|
||||||
|
direction=direction,
|
||||||
|
date=d,
|
||||||
|
position=pos,
|
||||||
|
delta=delta,
|
||||||
|
avg_cost=cost,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if snaps:
|
||||||
|
db.add_all(snaps)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
print(f"Seeded {len(bars_to_insert)} bars + {len(snaps)} position snapshots")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
seed()
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}系统管理{% endblock %}
|
||||||
|
{% block heading %}系统管理{% endblock %}
|
||||||
|
{% block breadcrumb %}品种与合约{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
{# ── New Product ── #}
|
||||||
|
<div class="section-title">新建品种</div>
|
||||||
|
<div class="form-card" style="margin-bottom:24px;">
|
||||||
|
<form method="post" action="/admin/product" style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;">
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:1;min-width:120px;">
|
||||||
|
<label>品种代码</label>
|
||||||
|
<input type="text" name="code" required placeholder="FG" maxlength="6" style="text-transform:uppercase;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:1;min-width:120px;">
|
||||||
|
<label>品种名称</label>
|
||||||
|
<input type="text" name="name" required placeholder="玻璃">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:1;min-width:120px;">
|
||||||
|
<label>交易所</label>
|
||||||
|
<select name="exchange" required>
|
||||||
|
{% for ex in exchanges %}
|
||||||
|
<option value="{{ ex }}">{{ ex }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">新建</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ── Product List ── #}
|
||||||
|
{% for p in products %}
|
||||||
|
<div class="section-title">{{ p.code }} · {{ p.name }} <span style="font-weight:400;color:var(--sub);font-size:0.78rem;">{{ p.exchange }}</span></div>
|
||||||
|
|
||||||
|
{# ── New Contract ── #}
|
||||||
|
<div class="form-card" style="margin-bottom:12px;">
|
||||||
|
<form method="post" action="/admin/contract" style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;">
|
||||||
|
<input type="hidden" name="product_id" value="{{ p.id }}">
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:1;min-width:140px;">
|
||||||
|
<label>合约代码</label>
|
||||||
|
<input type="text" name="code" required placeholder="{{ p.code }}2609" maxlength="10" style="text-transform:uppercase;">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" style="margin-bottom:0;flex:2;min-width:160px;">
|
||||||
|
<label>合约名称</label>
|
||||||
|
<input type="text" name="name" required placeholder="{{ p.name }}2609">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">添加合约</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ── Contract List ── #}
|
||||||
|
{% if p.contracts %}
|
||||||
|
<div class="table-wrap" style="margin-bottom:20px;">
|
||||||
|
<table>
|
||||||
|
<tr><th>合约代码</th><th>名称</th><th>状态</th><th>行情</th><th>操作</th></tr>
|
||||||
|
{% for c in p.contracts %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ c.code }}</strong></td>
|
||||||
|
<td>{{ c.name }}</td>
|
||||||
|
<td>
|
||||||
|
{% if c.is_active %}
|
||||||
|
<span class="badge badge-up">启用</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-down">停用</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td><a href="/contracts/{{ c.code }}" style="color:var(--accent);text-decoration:none;">查看 →</a></td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="/admin/contract/{{ c.id }}/toggle" style="display:inline;">
|
||||||
|
<button style="background:none;border:none;color:var(--sub);cursor:pointer;font-size:0.82rem;">
|
||||||
|
{% if c.is_active %}停用{% else %}启用{% endif %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="/admin/contract/{{ c.id }}/delete" style="display:inline;" onsubmit="return confirm('删除 {{ c.code }}?此操作不可恢复。')">
|
||||||
|
<button style="background:none;border:none;color:var(--danger);cursor:pointer;font-size:0.82rem;">删除</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="/admin/product/{{ p.id }}/delete" onsubmit="return confirm('删除品种 {{ p.code }} 及其所有合约?')" style="text-align:right;margin-bottom:28px;">
|
||||||
|
<button style="background:none;border:none;color:var(--danger);cursor:pointer;font-size:0.82rem;">删除品种 {{ p.code }}</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
<div style="text-align:center;padding:40px;color:var(--sub);">暂无品种,请先新建。</div>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}博弈分析{% endblock %}
|
||||||
|
{% block heading %}博弈分析{% endblock %}
|
||||||
|
{% block breadcrumb %}机构持仓{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if latest_date %}
|
||||||
|
<div class="stat-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">数据日期</div>
|
||||||
|
<div class="value">{{ latest_date }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">参考现价</div>
|
||||||
|
<div class="value">{{ current_price }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">六家净空</div>
|
||||||
|
<div class="value">{{ total_net_short }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">浮动盈亏</div>
|
||||||
|
<div class="value">{{ total_pnl }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-title">机构持仓明细</div>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>机构</th><th>多单</th><th>空单</th><th>多单成本</th><th>空单成本</th><th>净持仓</th><th>净盈亏</th>
|
||||||
|
</tr>
|
||||||
|
{% for r in rows %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ r.institution }}</strong></td>
|
||||||
|
<td>{{ r.long_pos }}</td>
|
||||||
|
<td>{{ r.short_pos }}</td>
|
||||||
|
<td>{{ r.long_cost }}</td>
|
||||||
|
<td>{{ r.short_cost }}</td>
|
||||||
|
<td>{{ r.net_pos }}</td>
|
||||||
|
<td>
|
||||||
|
{% if r.pnl_raw > 0 %}
|
||||||
|
<span class="badge badge-up">{{ r.pnl }}</span>
|
||||||
|
{% elif r.pnl_raw < 0 %}
|
||||||
|
<span class="badge badge-down">{{ r.pnl }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="na">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
<tr style="font-weight:700; background:var(--th-bg);">
|
||||||
|
<td>合计</td>
|
||||||
|
<td>{{ totals.long_pos }}</td>
|
||||||
|
<td>{{ totals.short_pos }}</td>
|
||||||
|
<td>—</td>
|
||||||
|
<td>—</td>
|
||||||
|
<td>{{ totals.net_pos }}</td>
|
||||||
|
<td>{{ totals.pnl }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div style="text-align:center;padding:60px 0;color:var(--sub);">
|
||||||
|
<p style="font-size:1.1rem;">暂无持仓数据</p>
|
||||||
|
<p style="margin-top:8px;"><a href="/input/" style="color:var(--accent);">前往录入 →</a></p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,263 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}期货量化{% endblock %}</title>
|
||||||
|
<style>
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #f8fafc; --surface: #fff; --fg: #1e293b; --sub: #64748b;
|
||||||
|
--border: #e2e8f0; --accent: #3b82f6; --accent-light: #eff6ff;
|
||||||
|
--th-bg: #f1f5f9; --th-fg: #475569;
|
||||||
|
--danger: #ef4444; --danger-bg: #fef2f2; --danger-fg: #dc2626;
|
||||||
|
--success: #10b981; --success-bg: #ecfdf5; --success-fg: #059669;
|
||||||
|
--warn: #f59e0b; --warn-bg: #fffbeb; --warn-fg: #d97706;
|
||||||
|
--na: #94a3b8;
|
||||||
|
--sidebar-w: 220px; --header-h: 56px;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--bg: #0f172a; --surface: #1e293b; --fg: #e2e8f0; --sub: #94a3b8;
|
||||||
|
--border: #334155; --accent: #60a5fa; --accent-light: #1e3a5f;
|
||||||
|
--th-bg: #1e293b; --th-fg: #cbd5e1;
|
||||||
|
--danger: #f87171; --danger-bg: #3b1515; --danger-fg: #fca5a5;
|
||||||
|
--success: #34d399; --success-bg: #0a1f17; --success-fg: #6ee7b7;
|
||||||
|
--warn: #fbbf24; --warn-bg: #2d2110; --warn-fg: #fcd34d;
|
||||||
|
--na: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, 'Segoe UI', system-ui, sans-serif;
|
||||||
|
color: var(--fg); background: var(--bg);
|
||||||
|
display: flex; min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Sidebar ── */
|
||||||
|
.sidebar {
|
||||||
|
width: var(--sidebar-w); min-width: var(--sidebar-w);
|
||||||
|
background: var(--surface); border-right: 1px solid var(--border);
|
||||||
|
display: flex; flex-direction: column; position: fixed;
|
||||||
|
top: 0; left: 0; bottom: 0; z-index: 50;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.sidebar-logo {
|
||||||
|
height: var(--header-h); display: flex; align-items: center;
|
||||||
|
padding: 0 20px; font-size: 1rem; font-weight: 700;
|
||||||
|
letter-spacing: 0.02em; border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.sidebar-nav { flex: 1; padding: 12px 0; }
|
||||||
|
.sidebar-nav a {
|
||||||
|
display: flex; align-items: center; gap: 10px;
|
||||||
|
padding: 10px 20px; color: var(--sub); text-decoration: none;
|
||||||
|
font-size: 0.9rem; transition: background .12s, color .12s;
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
}
|
||||||
|
.sidebar-nav a:hover { background: var(--accent-light); color: var(--fg); }
|
||||||
|
.sidebar-nav a.active {
|
||||||
|
color: var(--accent); background: var(--accent-light);
|
||||||
|
border-left-color: var(--accent); font-weight: 600;
|
||||||
|
}
|
||||||
|
.sidebar-nav .icon { font-size: 1.1rem; width: 22px; text-align: center; }
|
||||||
|
.sidebar-footer {
|
||||||
|
padding: 16px 20px; border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.theme-btn {
|
||||||
|
width: 100%; padding: 8px; border: 1px solid var(--border); border-radius: 6px;
|
||||||
|
background: var(--surface); color: var(--fg); cursor: pointer;
|
||||||
|
font-size: 0.82rem; transition: background .12s;
|
||||||
|
}
|
||||||
|
.theme-btn:hover { background: var(--th-bg); }
|
||||||
|
|
||||||
|
/* ── Main ── */
|
||||||
|
.main {
|
||||||
|
margin-left: var(--sidebar-w); flex: 1;
|
||||||
|
display: flex; flex-direction: column; min-height: 100vh;
|
||||||
|
}
|
||||||
|
.main-header {
|
||||||
|
height: var(--header-h); min-height: var(--header-h);
|
||||||
|
padding: 0 28px; display: flex; align-items: center;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
}
|
||||||
|
.main-header h1 { font-size: 1.15rem; font-weight: 600; }
|
||||||
|
.breadcrumb { color: var(--sub); font-size: 0.82rem; margin-left: 12px; }
|
||||||
|
.breadcrumb a { color: var(--sub); text-decoration: none; }
|
||||||
|
.breadcrumb a:hover { color: var(--accent); }
|
||||||
|
|
||||||
|
.main-content { flex: 1; padding: 24px 28px; }
|
||||||
|
|
||||||
|
/* ── Components ── */
|
||||||
|
.stat-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
|
||||||
|
.stat-card {
|
||||||
|
background: var(--surface); border: 1px solid var(--border);
|
||||||
|
border-radius: 10px; padding: 18px 20px;
|
||||||
|
}
|
||||||
|
.stat-card .label { font-size: 0.78rem; color: var(--sub); margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.04em; }
|
||||||
|
.stat-card .value { font-size: 1.5rem; font-weight: 700; }
|
||||||
|
.stat-card .trend { font-size: 0.8rem; margin-top: 4px; }
|
||||||
|
.trend-up { color: var(--success-fg); }
|
||||||
|
.trend-down { color: var(--danger-fg); }
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 0.85rem; font-weight: 600; color: var(--sub);
|
||||||
|
text-transform: uppercase; letter-spacing: 0.04em;
|
||||||
|
margin-bottom: 12px; padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Table ── */
|
||||||
|
.table-wrap {
|
||||||
|
background: var(--surface); border: 1px solid var(--border);
|
||||||
|
border-radius: 10px; overflow: hidden; margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
table { border-collapse: collapse; width: 100%; font-size: 0.88rem; }
|
||||||
|
th {
|
||||||
|
background: var(--th-bg); color: var(--th-fg); font-weight: 600;
|
||||||
|
padding: 10px 14px; border-bottom: 2px solid var(--border);
|
||||||
|
text-align: center; white-space: nowrap; font-size: 0.8rem;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
td { padding: 9px 14px; border-bottom: 1px solid var(--border); text-align: center; }
|
||||||
|
tr:last-child td { border-bottom: none; }
|
||||||
|
tr:hover td { background: var(--accent-light); }
|
||||||
|
td:first-child { font-weight: 500; }
|
||||||
|
tr.active td { background: #dbeafe !important; }
|
||||||
|
[data-theme="dark"] tr.active td { background: #1e3a5f !important; }
|
||||||
|
.na { color: var(--na); }
|
||||||
|
|
||||||
|
/* ── Badges ── */
|
||||||
|
.badge { display: inline-block; padding: 2px 10px; border-radius: 20px; font-size: 0.78rem; font-weight: 600; }
|
||||||
|
.badge-up { background: var(--success-bg); color: var(--success-fg); }
|
||||||
|
.badge-down { background: var(--danger-bg); color: var(--danger-fg); }
|
||||||
|
.badge-warn { background: var(--warn-bg); color: var(--warn-fg); }
|
||||||
|
|
||||||
|
/* ── Forms ── */
|
||||||
|
.form-card {
|
||||||
|
background: var(--surface); border: 1px solid var(--border);
|
||||||
|
border-radius: 10px; padding: 24px; max-width: 520px;
|
||||||
|
}
|
||||||
|
.form-group { margin-bottom: 16px; }
|
||||||
|
.form-group label {
|
||||||
|
display: block; font-size: 0.8rem; font-weight: 600;
|
||||||
|
color: var(--sub); margin-bottom: 5px;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
.form-group input, .form-group select {
|
||||||
|
width: 100%; padding: 9px 12px; border: 1px solid var(--border);
|
||||||
|
border-radius: 6px; font-size: 0.9rem;
|
||||||
|
background: var(--bg); color: var(--fg);
|
||||||
|
transition: border-color .15s;
|
||||||
|
}
|
||||||
|
.form-group input:focus, .form-group select:focus {
|
||||||
|
outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-light);
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
padding: 9px 22px; border: none; border-radius: 6px;
|
||||||
|
font-size: 0.88rem; font-weight: 600; cursor: pointer;
|
||||||
|
transition: background .12s;
|
||||||
|
}
|
||||||
|
.btn-primary { background: var(--accent); color: #fff; }
|
||||||
|
.btn-primary:hover { opacity: 0.9; }
|
||||||
|
|
||||||
|
/* ── Drawer ── */
|
||||||
|
.drawer-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.35); z-index: 99; display: none; }
|
||||||
|
.drawer-overlay.show { display: block; }
|
||||||
|
.drawer { position: fixed; top: 0; right: 0; width: 360px; height: 100%; background: var(--surface); border-left: 1px solid var(--border); z-index: 100; padding: 28px 24px; transform: translateX(100%); transition: transform .25s ease; overflow-y: auto; box-shadow: -4px 0 24px rgba(0,0,0,.12); }
|
||||||
|
.drawer.show { transform: translateX(0); }
|
||||||
|
.drawer h3 { font-size: 1rem; margin-bottom: 4px; }
|
||||||
|
.drawer .date-label { color: var(--sub); font-size: 0.82rem; margin-bottom: 18px; }
|
||||||
|
.drawer .calc-table { width: 100%; border-collapse: collapse; font-size: 0.82rem; margin-bottom: 16px; }
|
||||||
|
.drawer .calc-table td { padding: 5px 10px; border: 1px solid var(--border); text-align: center; }
|
||||||
|
.drawer .calc-table tr:first-child td { background: var(--th-bg); font-weight: 600; color: var(--th-fg); }
|
||||||
|
.drawer .result { font-size: 0.85rem; line-height: 2; color: var(--fg); }
|
||||||
|
.drawer .result b { font-size: 1.15rem; }
|
||||||
|
.drawer .close-btn { position: absolute; top: 16px; right: 20px; background: none; border: none; font-size: 1.4rem; cursor: pointer; color: var(--sub); line-height: 1; }
|
||||||
|
.drawer .close-btn:hover { color: var(--fg); }
|
||||||
|
.amp-cell { cursor: pointer; color: var(--accent); font-weight: 600; }
|
||||||
|
.amp-cell:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
/* ── Tags / Chips ── */
|
||||||
|
.tag-row { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 20px; }
|
||||||
|
.tag {
|
||||||
|
padding: 5px 12px; border-radius: 20px; font-size: 0.78rem;
|
||||||
|
font-weight: 500; cursor: pointer; text-decoration: none;
|
||||||
|
border: 1px solid var(--border); color: var(--sub);
|
||||||
|
transition: all .12s;
|
||||||
|
}
|
||||||
|
.tag:hover, .tag.active { border-color: var(--accent); color: var(--accent); background: var(--accent-light); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-logo">📊 期货量化</div>
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<a href="/contracts/" class="{% if active_nav == 'contracts' %}active{% endif %}">
|
||||||
|
<span class="icon">📈</span> 行情数据
|
||||||
|
</a>
|
||||||
|
<a href="/analysis/" class="{% if active_nav == 'analysis' %}active{% endif %}">
|
||||||
|
<span class="icon">⚔️</span> 博弈分析
|
||||||
|
</a>
|
||||||
|
<a href="/input/" class="{% if active_nav == 'input' %}active{% endif %}">
|
||||||
|
<span class="icon">📝</span> 数据录入
|
||||||
|
</a>
|
||||||
|
<a href="/admin/" class="{% if active_nav == 'admin' %}active{% endif %}">
|
||||||
|
<span class="icon">⚙️</span> 系统管理
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<button class="theme-btn" id="themeToggle">🌙 暗色模式</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="main">
|
||||||
|
<header class="main-header">
|
||||||
|
<h1>{% block heading %}概览{% endblock %}</h1>
|
||||||
|
<span class="breadcrumb">{% block breadcrumb %}{% endblock %}</span>
|
||||||
|
</header>
|
||||||
|
<div class="main-content">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="drawer-overlay" id="overlay" onclick="closeDrawer()"></div>
|
||||||
|
<div class="drawer" id="drawer">
|
||||||
|
<button class="close-btn" onclick="closeDrawer()">×</button>
|
||||||
|
<h3 id="drawer-title"></h3>
|
||||||
|
<p class="date-label" id="drawer-date"></p>
|
||||||
|
<table class="calc-table" id="drawer-table"></table>
|
||||||
|
<div class="result" id="drawer-result"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var themeBtn = document.getElementById('themeToggle');
|
||||||
|
themeBtn.addEventListener('click', toggleTheme);
|
||||||
|
function setTheme(dark) {
|
||||||
|
if (dark) {
|
||||||
|
document.documentElement.setAttribute('data-theme', 'dark');
|
||||||
|
themeBtn.textContent = '☀️ 亮色模式';
|
||||||
|
} else {
|
||||||
|
document.documentElement.removeAttribute('data-theme');
|
||||||
|
themeBtn.textContent = '🌙 暗色模式';
|
||||||
|
}
|
||||||
|
try { localStorage.setItem('ft-theme', dark ? 'dark' : 'light'); } catch(e) {}
|
||||||
|
}
|
||||||
|
function toggleTheme() {
|
||||||
|
setTheme(document.documentElement.getAttribute('data-theme') !== 'dark');
|
||||||
|
}
|
||||||
|
(function() {
|
||||||
|
try {
|
||||||
|
var s = localStorage.getItem('ft-theme');
|
||||||
|
if (s === 'dark' || (!s && matchMedia('(prefers-color-scheme: dark)').matches)) setTheme(true);
|
||||||
|
} catch(e) {}
|
||||||
|
})();
|
||||||
|
|
||||||
|
var activeRow = null;
|
||||||
|
function closeDrawer() {
|
||||||
|
document.getElementById('overlay').classList.remove('show');
|
||||||
|
document.getElementById('drawer').classList.remove('show');
|
||||||
|
if (activeRow) { activeRow.classList.remove('active'); activeRow = null; }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}{{ contract }}{% endblock %}
|
||||||
|
{% block heading %}{{ contract }}{% endblock %}
|
||||||
|
{% block breadcrumb %}<a href="/contracts/">行情数据</a> / {{ contract }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% if latest %}
|
||||||
|
<div class="stat-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">最新收盘</div>
|
||||||
|
<div class="value">{{ latest.close|int }}</div>
|
||||||
|
<div class="trend">日期 {{ latest.date }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">当日振幅</div>
|
||||||
|
<div class="value">{{ latest.amp_5d|int if latest.amp_5d else '—' }}</div>
|
||||||
|
<div class="trend">日波幅 {{ latest.diff|int }} 点</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">数据条数</div>
|
||||||
|
<div class="value">{{ row_count }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="section-title">日线数据</div>
|
||||||
|
<p style="font-size:0.78rem;color:var(--sub);margin-bottom:12px;">
|
||||||
|
振幅 = 近 5 日 (最高−最低) 均值取整 · 点击振幅值查看计算过程
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table id="bars-table">
|
||||||
|
<tr><th>日期</th><th>星期</th><th>开盘</th><th>收盘</th><th>最高</th><th>最低</th><th>波幅</th><th>5日均振幅</th></tr>
|
||||||
|
{% for row in rows %}
|
||||||
|
<tr data-index="{{ loop.index0 }}" data-diff="{{ row.diff }}" data-date="{{ row.date }}" data-weekday="{{ row.weekday }}">
|
||||||
|
<td>{{ row.date }}</td>
|
||||||
|
<td>{{ row.weekday }}</td>
|
||||||
|
<td>{{ row.open }}</td>
|
||||||
|
<td>{{ row.close }}</td>
|
||||||
|
<td>{{ row.high }}</td>
|
||||||
|
<td>{{ row.low }}</td>
|
||||||
|
<td>{{ row.diff }}</td>
|
||||||
|
<td>
|
||||||
|
{% if row.has_amp %}
|
||||||
|
<span class="amp-cell">{{ row.amp_5d }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="na">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.querySelectorAll('.amp-cell').forEach(function(cell) {
|
||||||
|
cell.addEventListener('click', function() {
|
||||||
|
var row = cell.parentElement.parentElement;
|
||||||
|
var tbl = document.getElementById('bars-table');
|
||||||
|
var rows = tbl.querySelectorAll('tr');
|
||||||
|
var idx = parseInt(row.dataset.index);
|
||||||
|
if (idx < 5) return;
|
||||||
|
|
||||||
|
document.getElementById('drawer-title').textContent = '{{ contract }}';
|
||||||
|
document.getElementById('drawer-date').textContent = '目标: ' + row.dataset.date + ' ' + row.dataset.weekday + ' 振幅 ' + cell.textContent.trim();
|
||||||
|
|
||||||
|
var html = '<tr><td>日期</td><td>最高−最低</td></tr>';
|
||||||
|
var sum = 0;
|
||||||
|
for (var j = idx - 5; j < idx; j++) {
|
||||||
|
var r = rows[j + 1];
|
||||||
|
html += '<tr><td>' + r.dataset.date + ' ' + r.dataset.weekday + '</td><td>' + r.dataset.diff + '</td></tr>';
|
||||||
|
sum += parseInt(r.dataset.diff);
|
||||||
|
}
|
||||||
|
document.getElementById('drawer-table').innerHTML = html;
|
||||||
|
document.getElementById('drawer-result').innerHTML = '合计 <b>' + sum + '</b> ÷ 5 = <b>' + (sum / 5).toFixed(1) + '</b><br>四舍五入 → <b>' + Math.round(sum / 5) + '</b>';
|
||||||
|
|
||||||
|
document.getElementById('overlay').classList.add('show');
|
||||||
|
document.getElementById('drawer').classList.add('show');
|
||||||
|
|
||||||
|
if (activeRow) activeRow.classList.remove('active');
|
||||||
|
row.classList.add('active');
|
||||||
|
activeRow = row;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}行情数据{% endblock %}
|
||||||
|
{% block heading %}行情数据{% endblock %}
|
||||||
|
{% block breadcrumb %}<a href="/contracts/">合约总览</a>{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="stat-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">监控合约</div>
|
||||||
|
<div class="value">{{ contracts|length }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">数据条数</div>
|
||||||
|
<div class="value">{{ total_bars }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="label">最新日期</div>
|
||||||
|
<div class="value">{{ latest_date }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-title">合约列表</div>
|
||||||
|
<div class="tag-row">
|
||||||
|
{% for c in contracts %}
|
||||||
|
<a class="tag" href="/contracts/{{ c }}">{{ c }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table>
|
||||||
|
<tr><th>合约</th><th>日期</th><th>开盘</th><th>收盘</th><th>最高</th><th>最低</th><th>振幅</th><th>操作</th></tr>
|
||||||
|
{% for c in contracts %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ c }}</strong></td>
|
||||||
|
{% if c in contract_bars %}
|
||||||
|
<td>{{ contract_bars[c].date }}</td>
|
||||||
|
<td>{{ contract_bars[c].open|int }}</td>
|
||||||
|
<td>{{ contract_bars[c].close|int }}</td>
|
||||||
|
<td>{{ contract_bars[c].high|int }}</td>
|
||||||
|
<td>{{ contract_bars[c].low|int }}</td>
|
||||||
|
<td>
|
||||||
|
{% if contract_bars[c].amp_5d %}
|
||||||
|
<span class="badge badge-up">{{ contract_bars[c].amp_5d|int }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="na">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% else %}
|
||||||
|
<td colspan="6" class="na">暂无数据</td>
|
||||||
|
{% endif %}
|
||||||
|
<td><a href="/contracts/{{ c }}" style="color:var(--accent);text-decoration:none;">详情 →</a></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}数据录入{% endblock %}
|
||||||
|
{% block heading %}数据录入{% endblock %}
|
||||||
|
{% block breadcrumb %}机构持仓录入{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="form-card">
|
||||||
|
<div class="section-title">持仓快照</div>
|
||||||
|
<form method="post" action="/input">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>机构</label>
|
||||||
|
<select name="institution" required>
|
||||||
|
{% for inst in institutions %}
|
||||||
|
<option value="{{ inst }}">{{ inst }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>方向</label>
|
||||||
|
<select name="direction" required>
|
||||||
|
<option value="long">多单</option>
|
||||||
|
<option value="short">空单</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>日期</label>
|
||||||
|
<input type="date" name="date_str" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>持仓量(手)</label>
|
||||||
|
<input type="number" name="position" required placeholder="例: 193030">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>持仓均价</label>
|
||||||
|
<input type="number" name="avg_cost" step="0.01" required placeholder="例: 995.26">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">提交</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ../日报:/app/output
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fastapi==0.115.6
|
||||||
|
uvicorn==0.34.0
|
||||||
|
sqlalchemy==2.0.36
|
||||||
|
jinja2==3.1.4
|
||||||
|
apscheduler==3.11.0
|
||||||
|
akshare==1.16.72
|
||||||
|
python-multipart==0.0.19
|
||||||
Reference in New Issue
Block a user