Compare commits
3 Commits
492ca9d82e
...
cd7f6668af
| Author | SHA1 | Date | |
|---|---|---|---|
| cd7f6668af | |||
| 5453ce1dcf | |||
| c0e338eb23 |
+117
-5
@@ -9,6 +9,7 @@ def fetch_contract_bars(contract_code: str, start_date: str | None = None) -> li
|
||||
"""Fetch daily OHLCV for a single contract from akshare.
|
||||
|
||||
Returns list of {date, open, close, high, low} dicts.
|
||||
Only returns bars on or after start_date when provided.
|
||||
"""
|
||||
import akshare as ak
|
||||
|
||||
@@ -22,6 +23,8 @@ def fetch_contract_bars(contract_code: str, start_date: str | None = None) -> li
|
||||
print(f"[collector] No data returned for {contract_code}")
|
||||
return []
|
||||
|
||||
filter_date = date.fromisoformat(start_date) if start_date else None
|
||||
|
||||
bars = []
|
||||
for _, row in df.iterrows():
|
||||
try:
|
||||
@@ -31,7 +34,7 @@ def fetch_contract_bars(contract_code: str, start_date: str | None = None) -> li
|
||||
else:
|
||||
d = date.fromisoformat(str(val)[:10])
|
||||
|
||||
if start_date and d <= date.fromisoformat(start_date):
|
||||
if filter_date and d < filter_date:
|
||||
continue
|
||||
|
||||
bars.append({
|
||||
@@ -95,6 +98,7 @@ def _sync_one(db, contract_code: str) -> int:
|
||||
return 0
|
||||
|
||||
inserted = 0
|
||||
min_date = None
|
||||
for bar in bars:
|
||||
existing = (
|
||||
db.query(DailyBar)
|
||||
@@ -114,22 +118,130 @@ def _sync_one(db, contract_code: str) -> int:
|
||||
low=bar["low"],
|
||||
))
|
||||
inserted += 1
|
||||
if min_date is None or bar["date"] < min_date:
|
||||
min_date = bar["date"]
|
||||
|
||||
if inserted > 0:
|
||||
db.flush()
|
||||
_recompute_amp(db, contract_code)
|
||||
_recompute_amp(db, contract_code, from_date=min_date)
|
||||
|
||||
return inserted
|
||||
|
||||
|
||||
def _recompute_amp(db, contract_code: str):
|
||||
"""Recompute amp_5d for all bars of a contract."""
|
||||
def fetch_position_rankings(contract_code: str, trade_date: str) -> list[dict]:
|
||||
"""Fetch top-20 position rankings for a contract on a given date from akshare.
|
||||
|
||||
Calls the API 3 times (volume, long, short) and returns a unified list of dicts:
|
||||
{data_type, rank, institution, value, change}
|
||||
"""
|
||||
import akshare as ak
|
||||
|
||||
results = []
|
||||
for sym, dtype in [("成交量", "volume"), ("多单持仓", "long"), ("空单持仓", "short")]:
|
||||
try:
|
||||
df = ak.futures_hold_pos_sina(symbol=sym, contract=contract_code.upper(), date=trade_date)
|
||||
except Exception as e:
|
||||
print(f"[collector] position akshare error for {contract_code} {dtype}: {e}")
|
||||
continue
|
||||
|
||||
if df is None or df.empty:
|
||||
continue
|
||||
|
||||
for _, row in df.iterrows():
|
||||
try:
|
||||
results.append({
|
||||
"data_type": dtype,
|
||||
"rank": int(row["名次"]),
|
||||
"institution": str(row["会员简称"]),
|
||||
"value": int(row.iloc[2]),
|
||||
"change": int(row["比上交易增减"]),
|
||||
})
|
||||
except (KeyError, ValueError, TypeError) as e:
|
||||
print(f"[collector] position skip row: {e}")
|
||||
continue
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def sync_position_rankings() -> dict:
|
||||
"""Sync position rankings for all active contracts. Returns {contract_code: new_rows}."""
|
||||
db = SessionLocal()
|
||||
results = {}
|
||||
|
||||
try:
|
||||
active_contracts = (
|
||||
db.query(Contract).filter(Contract.is_active == True).all()
|
||||
)
|
||||
|
||||
for c in active_contracts:
|
||||
count = _sync_positions_for_contract(db, c.code)
|
||||
results[c.code] = count
|
||||
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def _sync_positions_for_contract(db, contract_code: str) -> int:
|
||||
"""Sync position rankings for all dates that have bars but no position data."""
|
||||
from app.models import PositionRanking
|
||||
|
||||
code = contract_code.upper()
|
||||
|
||||
existing_dates = {
|
||||
r[0] for r in
|
||||
db.query(PositionRanking.date)
|
||||
.filter(PositionRanking.contract_code == code)
|
||||
.distinct()
|
||||
.all()
|
||||
}
|
||||
|
||||
bar_dates = [
|
||||
r[0] for r in
|
||||
db.query(DailyBar.date)
|
||||
.filter(DailyBar.contract == code)
|
||||
.order_by(DailyBar.date)
|
||||
.all()
|
||||
]
|
||||
|
||||
inserted = 0
|
||||
for d in bar_dates:
|
||||
if d in existing_dates:
|
||||
continue
|
||||
date_str = d.strftime("%Y%m%d")
|
||||
rankings = fetch_position_rankings(code, date_str)
|
||||
for r in rankings:
|
||||
db.add(PositionRanking(
|
||||
contract_code=code,
|
||||
institution=r["institution"],
|
||||
data_type=r["data_type"],
|
||||
date=d,
|
||||
rank=r["rank"],
|
||||
value=r["value"],
|
||||
change=r["change"],
|
||||
))
|
||||
inserted += 1
|
||||
db.flush()
|
||||
|
||||
return inserted
|
||||
|
||||
|
||||
def _recompute_amp(db, contract_code: str, from_date: date | None = None):
|
||||
"""Recompute amp_5d for bars of a contract from from_date onwards."""
|
||||
bars = (
|
||||
db.query(DailyBar)
|
||||
.filter(DailyBar.contract == contract_code)
|
||||
.order_by(DailyBar.date)
|
||||
.all()
|
||||
)
|
||||
start_idx = 0
|
||||
if from_date:
|
||||
for i, bar in enumerate(bars):
|
||||
if bar.date >= from_date:
|
||||
start_idx = i
|
||||
break
|
||||
for i, bar in enumerate(bars):
|
||||
if i >= 5:
|
||||
if i >= 5 and i >= start_idx:
|
||||
bar.amp_5d = compute_amp_5d([b.diff for b in bars[i - 5 : i]])
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
"""博弈分析计算引擎"""
|
||||
|
||||
|
||||
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}万"
|
||||
+1
-2
@@ -7,7 +7,7 @@ from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from app.database import engine, Base, SessionLocal
|
||||
from app.models import User
|
||||
from app.seed import seed
|
||||
from app.routers import contracts, analysis, admin, auth
|
||||
from app.routers import contracts, admin, auth
|
||||
|
||||
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
||||
|
||||
@@ -55,7 +55,6 @@ app.add_middleware(AuthMiddleware)
|
||||
|
||||
app.include_router(auth.router)
|
||||
app.include_router(contracts.router)
|
||||
app.include_router(analysis.router)
|
||||
app.include_router(admin.router)
|
||||
|
||||
|
||||
|
||||
@@ -51,18 +51,18 @@ class DailyBar(Base):
|
||||
return self.high - self.low
|
||||
|
||||
|
||||
class PositionSnapshot(Base):
|
||||
__tablename__ = "position_snapshots"
|
||||
__table_args__ = (UniqueConstraint("contract_code", "institution", "direction", "date"),)
|
||||
class PositionRanking(Base):
|
||||
__tablename__ = "position_rankings"
|
||||
__table_args__ = (UniqueConstraint("contract_code", "institution", "data_type", "date"),)
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
contract_code: Mapped[str] = mapped_column(String(10), index=True, default="FG")
|
||||
institution: Mapped[str] = mapped_column(String(20), index=True)
|
||||
direction: Mapped[str] = mapped_column(String(10))
|
||||
contract_code: Mapped[str] = mapped_column(String(10), index=True)
|
||||
institution: Mapped[str] = mapped_column(String(30), index=True)
|
||||
data_type: Mapped[str] = mapped_column(String(10), index=True)
|
||||
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)
|
||||
rank: Mapped[int] = mapped_column(Integer)
|
||||
value: Mapped[int] = mapped_column(Integer)
|
||||
change: Mapped[int] = mapped_column(Integer)
|
||||
|
||||
|
||||
class User(Base):
|
||||
|
||||
@@ -2,8 +2,8 @@ from fastapi import APIRouter, Depends, Form, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.models import Product, Contract, DailyBar, PositionSnapshot
|
||||
from app.collector import sync_active_contracts, sync_one_contract
|
||||
from app.models import Product, Contract, DailyBar, PositionRanking
|
||||
from app.collector import sync_active_contracts, sync_one_contract, sync_position_rankings
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||
|
||||
@@ -113,7 +113,7 @@ def delete_contract(contract_id: int, db: Session = Depends(get_db)):
|
||||
c = db.query(Contract).filter(Contract.id == contract_id).first()
|
||||
if c:
|
||||
db.query(DailyBar).filter(DailyBar.contract == c.code).delete()
|
||||
db.query(PositionSnapshot).filter(PositionSnapshot.contract_code == c.code).delete()
|
||||
db.query(PositionRanking).filter(PositionRanking.contract_code == c.code).delete()
|
||||
db.delete(c)
|
||||
db.commit()
|
||||
return RedirectResponse("/admin/?tab=contract", status_code=303)
|
||||
@@ -125,6 +125,7 @@ def delete_product(product_id: int, db: Session = Depends(get_db)):
|
||||
if p:
|
||||
for c in p.contracts:
|
||||
db.query(DailyBar).filter(DailyBar.contract == c.code).delete()
|
||||
db.query(PositionRanking).filter(PositionRanking.contract_code == c.code).delete()
|
||||
db.delete(p)
|
||||
db.commit()
|
||||
return RedirectResponse("/admin/?tab=product", status_code=303)
|
||||
@@ -144,6 +145,14 @@ def sync_single(contract_code: str):
|
||||
return RedirectResponse(f"/admin/?tab=sync&synced={count}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/sync-positions")
|
||||
def sync_positions(request: Request):
|
||||
results = sync_position_rankings()
|
||||
total = sum(results.values())
|
||||
print(f"[sync] Position rankings: {total} rows across {len(results)} contracts")
|
||||
return RedirectResponse(f"/admin/?tab=sync&pos_synced={total}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/sync/product/{product_id}")
|
||||
def sync_product(product_id: int, request: Request, db: Session = Depends(get_db)):
|
||||
contracts = db.query(Contract).filter(
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
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, Contract, DailyBar
|
||||
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)):
|
||||
# Get active contracts for selector
|
||||
active_contracts = (
|
||||
db.query(Contract.code)
|
||||
.filter(Contract.is_active == True)
|
||||
.order_by(Contract.code)
|
||||
.all()
|
||||
)
|
||||
contract_list = [c[0] for c in active_contracts]
|
||||
|
||||
# Default to first contract, or use query param
|
||||
selected = request.query_params.get("contract", contract_list[0] if contract_list else None)
|
||||
|
||||
if not selected:
|
||||
template = request.app.state.templates.get_template("analysis.html")
|
||||
return HTMLResponse(
|
||||
template.render(request=request, active_nav="analysis", rows=[], latest_date=None)
|
||||
)
|
||||
|
||||
# Get latest date for selected contract
|
||||
latest_snap = (
|
||||
db.query(PositionSnapshot.date)
|
||||
.filter(PositionSnapshot.contract_code == selected)
|
||||
.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, contracts=contract_list, selected=selected,
|
||||
)
|
||||
)
|
||||
|
||||
latest_date = latest_snap[0]
|
||||
|
||||
# Get latest close for current_price reference
|
||||
latest_bar = (
|
||||
db.query(DailyBar)
|
||||
.filter(DailyBar.contract == selected)
|
||||
.order_by(DailyBar.date.desc())
|
||||
.first()
|
||||
)
|
||||
current_price = int(latest_bar.close) if latest_bar else 0
|
||||
|
||||
rows = []
|
||||
total_net_short = 0
|
||||
pnl_sum = 0.0
|
||||
|
||||
for inst in INSTITUTIONS:
|
||||
long = (
|
||||
db.query(PositionSnapshot)
|
||||
.filter(
|
||||
PositionSnapshot.contract_code == selected,
|
||||
PositionSnapshot.institution == inst,
|
||||
PositionSnapshot.direction == "long",
|
||||
PositionSnapshot.date == latest_date,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
short = (
|
||||
db.query(PositionSnapshot)
|
||||
.filter(
|
||||
PositionSnapshot.contract_code == selected,
|
||||
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)
|
||||
|
||||
if np < 0:
|
||||
total_net_short += abs(np)
|
||||
pnl_sum += pnl
|
||||
|
||||
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 "—",
|
||||
"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,
|
||||
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),
|
||||
contracts=contract_list,
|
||||
selected=selected,
|
||||
)
|
||||
)
|
||||
@@ -9,16 +9,6 @@ router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
SESSION_COOKIE = "ft_session"
|
||||
|
||||
|
||||
def get_current_user(request: Request, db: Session = Depends(get_db)) -> User | None:
|
||||
user_id = request.cookies.get(SESSION_COOKIE)
|
||||
if not user_id:
|
||||
return None
|
||||
try:
|
||||
return db.query(User).filter(User.id == int(user_id)).first()
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
@router.get("/login", response_class=HTMLResponse)
|
||||
def login_page(request: Request):
|
||||
template = request.app.state.templates.get_template("login.html")
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import math
|
||||
from datetime import date, timedelta
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi import APIRouter, Depends, Request, Query
|
||||
from fastapi.responses import HTMLResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from app.database import get_db
|
||||
from app.models import DailyBar, Contract
|
||||
from app.models import DailyBar, Contract, PositionRanking
|
||||
|
||||
router = APIRouter(prefix="/contracts", tags=["contracts"])
|
||||
|
||||
WEEKDAY_ZH = {0: "周一", 1: "周二", 2: "周三", 3: "周四", 4: "周五", 5: "周六", 6: "周日"}
|
||||
PAGE_SIZE = 7
|
||||
|
||||
|
||||
def get_active_contracts(db: Session) -> list[str]:
|
||||
@@ -35,11 +37,6 @@ def contract_index(request: Request, db: Session = Depends(get_db)):
|
||||
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(
|
||||
@@ -47,14 +44,18 @@ def contract_index(request: Request, db: Session = Depends(get_db)):
|
||||
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)):
|
||||
def contract_detail(
|
||||
request: Request,
|
||||
contract: str,
|
||||
pos_date: str | None = None,
|
||||
page: int = Query(1, ge=1),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
active_contracts = get_active_contracts(db)
|
||||
bars = (
|
||||
db.query(DailyBar)
|
||||
@@ -63,9 +64,17 @@ def contract_detail(request: Request, contract: str, db: Session = Depends(get_d
|
||||
.all()
|
||||
)
|
||||
|
||||
total = len(bars)
|
||||
total_pages = max(1, math.ceil(total / PAGE_SIZE))
|
||||
page = min(page, total_pages)
|
||||
start = (page - 1) * PAGE_SIZE
|
||||
page_bars = bars[start:start + PAGE_SIZE]
|
||||
|
||||
rows = []
|
||||
for bar in bars:
|
||||
for i, bar in enumerate(page_bars):
|
||||
global_idx = start + i
|
||||
rows.append({
|
||||
"global_idx": global_idx,
|
||||
"date": bar.date.strftime("%Y/%-m/%-d"),
|
||||
"weekday": WEEKDAY_ZH.get(bar.date.weekday(), ""),
|
||||
"open": int(bar.open) if bar.open else "-",
|
||||
@@ -79,6 +88,45 @@ def contract_detail(request: Request, contract: str, db: Session = Depends(get_d
|
||||
|
||||
latest = bars[0] if bars else None
|
||||
|
||||
# Dates that have position data (for date picker)
|
||||
pos_dates = [
|
||||
r[0] for r in
|
||||
db.query(PositionRanking.date)
|
||||
.filter(PositionRanking.contract_code == contract.upper())
|
||||
.distinct()
|
||||
.order_by(PositionRanking.date.desc())
|
||||
.all()
|
||||
]
|
||||
|
||||
# Determine which date to show position rankings for
|
||||
if pos_date:
|
||||
try:
|
||||
selected_date = date.fromisoformat(pos_date)
|
||||
except ValueError:
|
||||
selected_date = pos_dates[0] if pos_dates else None
|
||||
else:
|
||||
selected_date = pos_dates[0] if pos_dates else None
|
||||
|
||||
# Position rankings for the selected date
|
||||
pos_data = {"volume": [], "long": [], "short": []}
|
||||
if selected_date:
|
||||
rankings = (
|
||||
db.query(PositionRanking)
|
||||
.filter(
|
||||
PositionRanking.contract_code == contract.upper(),
|
||||
PositionRanking.date == selected_date,
|
||||
)
|
||||
.order_by(PositionRanking.data_type, PositionRanking.rank)
|
||||
.all()
|
||||
)
|
||||
for r in rankings:
|
||||
pos_data[r.data_type].append({
|
||||
"rank": r.rank,
|
||||
"institution": r.institution,
|
||||
"value": r.value,
|
||||
"change": r.change,
|
||||
})
|
||||
|
||||
# Predict next trading day amplitude: mean of latest 5 diffs
|
||||
# Compute next trading date
|
||||
next_date = latest.date + timedelta(days=1) if latest else None
|
||||
@@ -98,9 +146,17 @@ def contract_detail(request: Request, contract: str, db: Session = Depends(get_d
|
||||
contracts=active_contracts,
|
||||
rows=rows,
|
||||
latest=latest,
|
||||
pos_dates=pos_dates,
|
||||
selected_pos_date=selected_date,
|
||||
pos_data=pos_data,
|
||||
next_amp=next_amp,
|
||||
next_date=next_date.strftime("%Y/%-m/%-d") if next_date else None,
|
||||
next_weekday=WEEKDAY_ZH.get(next_date.weekday(), "") if next_date else "",
|
||||
row_count=len(rows),
|
||||
total_rows=total,
|
||||
page=page,
|
||||
total_pages=total_pages,
|
||||
pos_date=pos_date or "",
|
||||
WEEKDAY_ZH=WEEKDAY_ZH,
|
||||
)
|
||||
)
|
||||
|
||||
+2
-202
@@ -1,7 +1,7 @@
|
||||
"""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, User
|
||||
from app.models import DailyBar, Product, Contract, User
|
||||
from app.engine.lock_strategy import compute_amp_5d
|
||||
|
||||
# --- Seed OHLCV data ---
|
||||
@@ -27,176 +27,6 @@ SEED_BARS: list[dict] = [
|
||||
{"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()
|
||||
@@ -260,38 +90,8 @@ def seed():
|
||||
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.contract_code, r.institution, r.direction, r.date)
|
||||
for r in db.query(
|
||||
PositionSnapshot.contract_code,
|
||||
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 ("FG2609", inst, direction, d) not in existing_pos:
|
||||
snaps.append(
|
||||
PositionSnapshot(
|
||||
contract_code="FG2609",
|
||||
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")
|
||||
print(f"Seeded {len(bars_to_insert)} bars")
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@@ -32,10 +32,16 @@
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px;padding:14px 18px;background:var(--surface);border:1px solid var(--border);border-radius:10px;">
|
||||
<span style="font-size:0.85rem;color:var(--sub);">全部合约</span>
|
||||
<form method="post" action="/admin/sync" style="display:inline;">
|
||||
<button type="submit" class="btn btn-primary" style="font-size:0.82rem;padding:6px 16px;">同步全部</button>
|
||||
<button type="submit" class="btn btn-primary" style="font-size:0.82rem;padding:6px 16px;">同步行情</button>
|
||||
</form>
|
||||
<form method="post" action="/admin/sync-positions" style="display:inline;">
|
||||
<button type="submit" class="btn btn-primary" style="font-size:0.82rem;padding:6px 16px;background:var(--success);">同步持仓</button>
|
||||
</form>
|
||||
{% if request.query_params.get('synced') %}
|
||||
<span style="font-size:0.82rem;color:var(--success-fg);">✓ 已同步 {{ request.query_params.synced }} 条</span>
|
||||
<span style="font-size:0.82rem;color:var(--success-fg);">✓ 行情 {{ request.query_params.synced }} 条</span>
|
||||
{% endif %}
|
||||
{% if request.query_params.get('pos_synced') %}
|
||||
<span style="font-size:0.82rem;color:var(--success-fg);">✓ 持仓 {{ request.query_params.pos_synced }} 条</span>
|
||||
{% endif %}
|
||||
<span style="font-size:0.78rem;color:var(--sub);margin-left:auto;">{{ total_contracts }} 个合约</span>
|
||||
</div>
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}博弈分析{% endblock %}
|
||||
{% block heading %}博弈分析{% endblock %}
|
||||
{% block breadcrumb %}机构持仓{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if contracts %}
|
||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px;">
|
||||
<span style="font-size:0.85rem;color:var(--sub);">合约</span>
|
||||
<select onchange="window.location='?contract='+this.value" style="padding:6px 12px;border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--fg);font-size:0.88rem;">
|
||||
{% for c in contracts %}
|
||||
<option value="{{ c }}"{% if c == selected %} selected{% endif %}>{{ c }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% 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">{{ selected }} 现价</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>
|
||||
</tr>
|
||||
{% for r in rows %}
|
||||
<tr>
|
||||
<td><strong>{{ r.institution }}</strong></td>
|
||||
<td>{{ r.long_pos }}</td>
|
||||
<td>{{ r.short_pos }}</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>—</td>
|
||||
<td>—</td>
|
||||
<td>{{ total_net_short }}</td>
|
||||
<td>{{ total_pnl }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="text-align:center;padding:60px 0;color:var(--sub);">
|
||||
<p style="font-size:1.1rem;">暂无「{{ selected }}」的持仓数据</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -177,15 +177,18 @@
|
||||
.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;
|
||||
input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23475669' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'/%3E%3Cline x1='16' y1='2' x2='16' y2='6'/%3E%3Cline x1='8' y1='2' x2='8' y2='6'/%3E%3Cline x1='3' y1='10' x2='21' y2='10'/%3E%3C/svg%3E");
|
||||
cursor: pointer;
|
||||
}
|
||||
.tag:hover, .tag.active { border-color: var(--accent); color: var(--accent); background: var(--accent-light); }
|
||||
[data-theme="dark"] input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2' ry='2'/%3E%3Cline x1='16' y1='2' x2='16' y2='6'/%3E%3Cline x1='8' y1='2' x2='8' y2='6'/%3E%3Cline x1='3' y1='10' x2='21' y2='10'/%3E%3C/svg%3E");
|
||||
filter: none;
|
||||
}
|
||||
[data-theme="dark"] input[type="date"] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -195,9 +198,6 @@
|
||||
<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="/admin/" class="{% if active_nav == 'admin' %}active{% endif %}">
|
||||
<span class="icon">⚙️</span> 系统管理
|
||||
</a>
|
||||
@@ -236,9 +236,11 @@ themeBtn.addEventListener('click', toggleTheme);
|
||||
function setTheme(dark) {
|
||||
if (dark) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
document.documentElement.style.colorScheme = 'dark';
|
||||
themeBtn.textContent = '☀️ 亮色模式';
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
document.documentElement.style.colorScheme = 'light';
|
||||
themeBtn.textContent = '🌙 暗色模式';
|
||||
}
|
||||
try { localStorage.setItem('ft-theme', dark ? 'dark' : 'light'); } catch(e) {}
|
||||
|
||||
@@ -10,15 +10,13 @@
|
||||
<p style="font-size:0.78rem;color:var(--sub);margin:0;">
|
||||
振幅 = 近 5 日 (最高−最低) 均值取整 · 点击振幅值查看计算过程
|
||||
</p>
|
||||
{% if rows|length > 7 %}
|
||||
<button id="toggle-all" class="btn" style="font-size:0.78rem;padding:4px 14px;background:var(--surface);color:var(--accent);border:1px solid var(--accent);border-radius:5px;cursor:pointer;" onclick="toggleAll()">显示全部 ({{ rows|length }}条)</button>
|
||||
{% endif %}
|
||||
<span style="font-size:0.78rem;color:var(--sub);">共 {{ total_rows }} 条</span>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{% if next_date %}
|
||||
{% if page == 1 and next_date %}
|
||||
<tr style="background:var(--accent-light);font-weight:500;">
|
||||
<td>{{ next_date }}</td>
|
||||
<td>{{ next_weekday }}</td>
|
||||
@@ -31,7 +29,7 @@
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% for row in rows %}
|
||||
<tr data-index="{{ loop.index0 }}" data-diff="{{ row.diff }}" data-date="{{ row.date }}" data-weekday="{{ row.weekday }}"{% if loop.index0 >= 7 %} class="extra-row" style="display:none;"{% endif %}>
|
||||
<tr data-index="{{ row.global_idx }}" data-diff="{{ row.diff }}" data-date="{{ row.date }}" data-weekday="{{ row.weekday }}">
|
||||
<td>{{ row.date }}</td>
|
||||
<td>{{ row.weekday }}</td>
|
||||
<td>{{ row.open }}</td>
|
||||
@@ -51,48 +49,101 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
{% if total_pages > 1 %}
|
||||
<div style="display:flex;align-items:center;justify-content:center;gap:6px;margin-bottom:24px;">
|
||||
{% if page > 1 %}
|
||||
<a class="btn" href="?page={{ page - 1 }}{% if pos_date %}&pos_date={{ pos_date }}{% endif %}" style="font-size:0.82rem;padding:5px 14px;background:var(--surface);color:var(--fg);border:1px solid var(--border);border-radius:5px;text-decoration:none;">← 上一页</a>
|
||||
{% endif %}
|
||||
|
||||
var allShown = false;
|
||||
function toggleAll() {
|
||||
allShown = !allShown;
|
||||
var btn = document.getElementById('toggle-all');
|
||||
var rows = document.querySelectorAll('.extra-row');
|
||||
rows.forEach(function(r) { r.style.display = allShown ? '' : 'none'; });
|
||||
btn.textContent = allShown ? '收起' : '显示全部 ({{ rows|length }}条)';
|
||||
{% for p in range(1, total_pages + 1) %}
|
||||
{% if p == page %}
|
||||
<span style="font-size:0.82rem;padding:5px 12px;background:var(--accent);color:#fff;border-radius:5px;font-weight:600;">{{ p }}</span>
|
||||
{% elif p <= 3 or p > total_pages - 3 or (p >= page - 1 and p <= page + 1) %}
|
||||
<a class="btn" href="?page={{ p }}{% if pos_date %}&pos_date={{ pos_date }}{% endif %}" style="font-size:0.82rem;padding:5px 12px;background:var(--surface);color:var(--fg);border:1px solid var(--border);border-radius:5px;text-decoration:none;">{{ p }}</a>
|
||||
{% elif p == 4 or p == total_pages - 3 %}
|
||||
<span style="color:var(--sub);padding:5px 4px;">…</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page < total_pages %}
|
||||
<a class="btn" href="?page={{ page + 1 }}{% if pos_date %}&pos_date={{ pos_date }}{% endif %}" style="font-size:0.82rem;padding:5px 14px;background:var(--surface);color:var(--fg);border:1px solid var(--border);border-radius:5px;text-decoration:none;">下一页 →</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if pos_dates %}
|
||||
<div class="section-title" style="margin-top:28px;display:flex;align-items:center;gap:12px;">
|
||||
<span>持仓排名 · 前20</span>
|
||||
<input type="date" id="pos-date-select" value="{{ selected_pos_date }}" min="{{ pos_dates[-1] }}" max="{{ pos_dates[0] }}" onchange="changePosDate(this.value)" style="font-size:0.82rem;padding:4px 10px;border:1px solid var(--border);border-radius:5px;background:var(--surface);color:var(--fg);cursor:pointer;">
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-bottom:24px;">
|
||||
|
||||
{% set types = [('volume', '成交量'), ('long', '多单持仓'), ('short', '空单持仓')] %}
|
||||
{% for dtype, dlabel in types %}
|
||||
<div class="table-wrap" style="margin-bottom:0;">
|
||||
<table style="font-size:0.82rem;">
|
||||
<tr><th colspan="4" style="font-size:0.8rem;color:var(--accent);">{{ dlabel }}</th></tr>
|
||||
<tr><th>#</th><th>会员</th><th>持仓</th><th>增减</th></tr>
|
||||
{% for row in pos_data[dtype] %}
|
||||
<tr>
|
||||
<td style="color:var(--sub);">{{ row.rank }}</td>
|
||||
<td style="text-align:left;">{{ row.institution }}</td>
|
||||
<td>{{ row.value }}</td>
|
||||
<td style="color:{% if row.change > 0 %}var(--success-fg){% elif row.change < 0 %}var(--danger-fg){% else %}var(--sub){% endif %};">
|
||||
{% if row.change > 0 %}+{% endif %}{{ row.change }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
function changePosDate(d) {
|
||||
var url = new URL(window.location);
|
||||
url.searchParams.set('pos_date', d);
|
||||
window.location = url;
|
||||
}
|
||||
|
||||
document.querySelectorAll('.amp-cell').forEach(function(cell) {
|
||||
cell.addEventListener('click', function() {
|
||||
var row = cell.parentElement.parentElement;
|
||||
var tbl = document.getElementById('bars-table');
|
||||
var dataRows = tbl.querySelectorAll('tr[data-index]');
|
||||
|
||||
// Next-day row: use latest 5 data rows
|
||||
// Next-day prediction row
|
||||
if (cell.dataset.next) {
|
||||
var dataRows = document.querySelectorAll('#bars-table tr[data-index]');
|
||||
if (dataRows.length < 5) return;
|
||||
showDrawer(cell, '{{ contract }}', dataRows, 0, 4);
|
||||
var rows = Array.from(dataRows).slice(0, 5);
|
||||
showDrawer(cell, '{{ contract }}', rows, true);
|
||||
return;
|
||||
}
|
||||
|
||||
var idx = parseInt(row.dataset.index);
|
||||
// Need 5 older rows (higher index, since newest first)
|
||||
if (idx + 5 >= dataRows.length) return;
|
||||
showDrawer(cell, '{{ contract }}', dataRows, idx + 1, idx + 5);
|
||||
// Collect 5 older rows via DOM navigation (next siblings in the table)
|
||||
var row = cell.parentElement.parentElement;
|
||||
var olderRows = [];
|
||||
var cursor = row;
|
||||
for (var j = 0; j < 5; j++) {
|
||||
cursor = cursor.nextElementSibling;
|
||||
if (!cursor || !cursor.dataset.index) return;
|
||||
olderRows.push(cursor);
|
||||
}
|
||||
showDrawer(cell, '{{ contract }}', olderRows, false);
|
||||
});
|
||||
});
|
||||
|
||||
function showDrawer(cell, product, dataRows, fromIdx, toIdx) {
|
||||
function showDrawer(cell, product, calcRows, isNext) {
|
||||
document.getElementById('drawer-title').textContent = product;
|
||||
var label = cell.dataset.next ? '次日预测' : '目标';
|
||||
var label = isNext ? '次日预测' : '目标';
|
||||
document.getElementById('drawer-date').textContent = label + ' 振幅 ' + cell.textContent.trim();
|
||||
|
||||
var html = '<tr><td>日期</td><td>最高−最低</td></tr>';
|
||||
var sum = 0;
|
||||
for (var j = fromIdx; j <= toIdx; j++) {
|
||||
var r = dataRows[j];
|
||||
calcRows.forEach(function(r) {
|
||||
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>';
|
||||
|
||||
|
||||
@@ -4,27 +4,8 @@
|
||||
{% 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>
|
||||
|
||||
@@ -81,9 +81,11 @@ themeBtn.addEventListener('click', toggleTheme);
|
||||
function setTheme(dark) {
|
||||
if (dark) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
document.documentElement.style.colorScheme = 'dark';
|
||||
themeBtn.textContent = '☀️ 亮色模式';
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
document.documentElement.style.colorScheme = 'light';
|
||||
themeBtn.textContent = '🌙 暗色模式';
|
||||
}
|
||||
try { localStorage.setItem('ft-theme', dark ? 'dark' : 'light'); } catch(e) {}
|
||||
|
||||
Reference in New Issue
Block a user