diff --git a/ft-app/.gitignore b/ft-app/.gitignore
new file mode 100644
index 0000000..ac481ac
--- /dev/null
+++ b/ft-app/.gitignore
@@ -0,0 +1,3 @@
+data/
+__pycache__/
+*.pyc
diff --git a/ft-app/Dockerfile b/ft-app/Dockerfile
new file mode 100644
index 0000000..2f2e976
--- /dev/null
+++ b/ft-app/Dockerfile
@@ -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"]
diff --git a/ft-app/app/database.py b/ft-app/app/database.py
new file mode 100644
index 0000000..635f100
--- /dev/null
+++ b/ft-app/app/database.py
@@ -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()
diff --git a/ft-app/app/engine/__init__.py b/ft-app/app/engine/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/ft-app/app/engine/game_theory.py b/ft-app/app/engine/game_theory.py
new file mode 100644
index 0000000..e6824fa
--- /dev/null
+++ b/ft-app/app/engine/game_theory.py
@@ -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}万"
diff --git a/ft-app/app/engine/lock_strategy.py b/ft-app/app/engine/lock_strategy.py
new file mode 100644
index 0000000..ce4b2d6
--- /dev/null
+++ b/ft-app/app/engine/lock_strategy.py
@@ -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
diff --git a/ft-app/app/main.py b/ft-app/app/main.py
new file mode 100644
index 0000000..bd67ecc
--- /dev/null
+++ b/ft-app/app/main.py
@@ -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")
diff --git a/ft-app/app/models.py b/ft-app/app/models.py
new file mode 100644
index 0000000..40e25a5
--- /dev/null
+++ b/ft-app/app/models.py
@@ -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)
diff --git a/ft-app/app/routers/__init__.py b/ft-app/app/routers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/ft-app/app/routers/admin.py b/ft-app/app/routers/admin.py
new file mode 100644
index 0000000..81c5e03
--- /dev/null
+++ b/ft-app/app/routers/admin.py
@@ -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)
diff --git a/ft-app/app/routers/analysis.py b/ft-app/app/routers/analysis.py
new file mode 100644
index 0000000..70cfb9d
--- /dev/null
+++ b/ft-app/app/routers/analysis.py
@@ -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),
+ )
+ )
diff --git a/ft-app/app/routers/contracts.py b/ft-app/app/routers/contracts.py
new file mode 100644
index 0000000..042f07f
--- /dev/null
+++ b/ft-app/app/routers/contracts.py
@@ -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),
+ )
+ )
diff --git a/ft-app/app/routers/data_input.py b/ft-app/app/routers/data_input.py
new file mode 100644
index 0000000..b98c0a3
--- /dev/null
+++ b/ft-app/app/routers/data_input.py
@@ -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)
diff --git a/ft-app/app/seed.py b/ft-app/app/seed.py
new file mode 100644
index 0000000..06a00e2
--- /dev/null
+++ b/ft-app/app/seed.py
@@ -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()
diff --git a/ft-app/app/templates/admin.html b/ft-app/app/templates/admin.html
new file mode 100644
index 0000000..465ff40
--- /dev/null
+++ b/ft-app/app/templates/admin.html
@@ -0,0 +1,93 @@
+{% extends "base.html" %}
+{% block title %}系统管理{% endblock %}
+{% block heading %}系统管理{% endblock %}
+{% block breadcrumb %}品种与合约{% endblock %}
+
+{% block content %}
+
+{# ── New Product ── #}
+
新建品种
+
+
+{# ── Product List ── #}
+{% for p in products %}
+{{ p.code }} · {{ p.name }} {{ p.exchange }}
+
+{# ── New Contract ── #}
+
+
+{# ── Contract List ── #}
+{% if p.contracts %}
+
+
+ | 合约代码 | 名称 | 状态 | 行情 | 操作 |
+ {% for c in p.contracts %}
+
+ | {{ c.code }} |
+ {{ c.name }} |
+
+ {% if c.is_active %}
+ 启用
+ {% else %}
+ 停用
+ {% endif %}
+ |
+ 查看 → |
+
+
+
+ |
+
+ {% endfor %}
+
+
+{% endif %}
+
+
+
+{% else %}
+暂无品种,请先新建。
+{% endfor %}
+
+{% endblock %}
diff --git a/ft-app/app/templates/analysis.html b/ft-app/app/templates/analysis.html
new file mode 100644
index 0000000..08f580c
--- /dev/null
+++ b/ft-app/app/templates/analysis.html
@@ -0,0 +1,69 @@
+{% extends "base.html" %}
+{% block title %}博弈分析{% endblock %}
+{% block heading %}博弈分析{% endblock %}
+{% block breadcrumb %}机构持仓{% endblock %}
+
+{% block content %}
+{% if latest_date %}
+
+
+
数据日期
+
{{ latest_date }}
+
+
+
参考现价
+
{{ current_price }}
+
+
+
六家净空
+
{{ total_net_short }}
+
+
+
浮动盈亏
+
{{ total_pnl }}
+
+
+
+机构持仓明细
+
+
+
+ | 机构 | 多单 | 空单 | 多单成本 | 空单成本 | 净持仓 | 净盈亏 |
+
+ {% for r in rows %}
+
+ | {{ r.institution }} |
+ {{ r.long_pos }} |
+ {{ r.short_pos }} |
+ {{ r.long_cost }} |
+ {{ r.short_cost }} |
+ {{ r.net_pos }} |
+
+ {% if r.pnl_raw > 0 %}
+ {{ r.pnl }}
+ {% elif r.pnl_raw < 0 %}
+ {{ r.pnl }}
+ {% else %}
+ —
+ {% endif %}
+ |
+
+ {% endfor %}
+
+ | 合计 |
+ {{ totals.long_pos }} |
+ {{ totals.short_pos }} |
+ — |
+ — |
+ {{ totals.net_pos }} |
+ {{ totals.pnl }} |
+
+
+
+{% else %}
+
+{% endif %}
+{% endblock %}
diff --git a/ft-app/app/templates/base.html b/ft-app/app/templates/base.html
new file mode 100644
index 0000000..cbeaea6
--- /dev/null
+++ b/ft-app/app/templates/base.html
@@ -0,0 +1,263 @@
+
+
+
+
+
+{% block title %}期货量化{% endblock %}
+
+
+
+
+
+
+
+ {% block heading %}概览{% endblock %}
+ {% block breadcrumb %}{% endblock %}
+
+
+ {% block content %}{% endblock %}
+
+
+
+
+
+
+
+
+
diff --git a/ft-app/app/templates/contract.html b/ft-app/app/templates/contract.html
new file mode 100644
index 0000000..5803618
--- /dev/null
+++ b/ft-app/app/templates/contract.html
@@ -0,0 +1,86 @@
+{% extends "base.html" %}
+{% block title %}{{ contract }}{% endblock %}
+{% block heading %}{{ contract }}{% endblock %}
+{% block breadcrumb %}行情数据 / {{ contract }}{% endblock %}
+
+{% block content %}
+{% if latest %}
+
+
+
最新收盘
+
{{ latest.close|int }}
+
日期 {{ latest.date }}
+
+
+
当日振幅
+
{{ latest.amp_5d|int if latest.amp_5d else '—' }}
+
日波幅 {{ latest.diff|int }} 点
+
+
+
数据条数
+
{{ row_count }}
+
+
+{% endif %}
+
+日线数据
+
+ 振幅 = 近 5 日 (最高−最低) 均值取整 · 点击振幅值查看计算过程
+
+
+
+
+ | 日期 | 星期 | 开盘 | 收盘 | 最高 | 最低 | 波幅 | 5日均振幅 |
+ {% for row in rows %}
+
+ | {{ row.date }} |
+ {{ row.weekday }} |
+ {{ row.open }} |
+ {{ row.close }} |
+ {{ row.high }} |
+ {{ row.low }} |
+ {{ row.diff }} |
+
+ {% if row.has_amp %}
+ {{ row.amp_5d }}
+ {% else %}
+ —
+ {% endif %}
+ |
+
+ {% endfor %}
+
+
+
+
+{% endblock %}
diff --git a/ft-app/app/templates/index.html b/ft-app/app/templates/index.html
new file mode 100644
index 0000000..b6fbec9
--- /dev/null
+++ b/ft-app/app/templates/index.html
@@ -0,0 +1,56 @@
+{% extends "base.html" %}
+{% block title %}行情数据{% endblock %}
+{% block heading %}行情数据{% endblock %}
+{% block breadcrumb %}合约总览{% endblock %}
+
+{% block content %}
+
+
+
监控合约
+
{{ contracts|length }}
+
+
+
数据条数
+
{{ total_bars }}
+
+
+
最新日期
+
{{ latest_date }}
+
+
+
+合约列表
+
+ {% for c in contracts %}
+
{{ c }}
+ {% endfor %}
+
+
+
+
+ | 合约 | 日期 | 开盘 | 收盘 | 最高 | 最低 | 振幅 | 操作 |
+ {% for c in contracts %}
+
+ | {{ c }} |
+ {% if c in contract_bars %}
+ {{ contract_bars[c].date }} |
+ {{ contract_bars[c].open|int }} |
+ {{ contract_bars[c].close|int }} |
+ {{ contract_bars[c].high|int }} |
+ {{ contract_bars[c].low|int }} |
+
+ {% if contract_bars[c].amp_5d %}
+ {{ contract_bars[c].amp_5d|int }}
+ {% else %}
+ —
+ {% endif %}
+ |
+ {% else %}
+ 暂无数据 |
+ {% endif %}
+ 详情 → |
+
+ {% endfor %}
+
+
+{% endblock %}
diff --git a/ft-app/app/templates/input.html b/ft-app/app/templates/input.html
new file mode 100644
index 0000000..449363b
--- /dev/null
+++ b/ft-app/app/templates/input.html
@@ -0,0 +1,45 @@
+{% extends "base.html" %}
+{% block title %}数据录入{% endblock %}
+{% block heading %}数据录入{% endblock %}
+{% block breadcrumb %}机构持仓录入{% endblock %}
+
+{% block content %}
+
+{% endblock %}
diff --git a/ft-app/docker-compose.yml b/ft-app/docker-compose.yml
new file mode 100644
index 0000000..c86e186
--- /dev/null
+++ b/ft-app/docker-compose.yml
@@ -0,0 +1,9 @@
+services:
+ app:
+ build: .
+ ports:
+ - "8000:8000"
+ volumes:
+ - ./data:/app/data
+ - ../日报:/app/output
+ restart: unless-stopped
diff --git a/ft-app/requirements.txt b/ft-app/requirements.txt
new file mode 100644
index 0000000..b3b2e5f
--- /dev/null
+++ b/ft-app/requirements.txt
@@ -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