Compare commits

..

9 Commits

8 changed files with 65 additions and 186 deletions
+1 -2
View File
@@ -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, data_input, admin, auth
from app.routers import contracts, analysis, admin, auth
TEMPLATES_DIR = Path(__file__).parent / "templates"
@@ -56,7 +56,6 @@ app.add_middleware(AuthMiddleware)
app.include_router(auth.router)
app.include_router(contracts.router)
app.include_router(analysis.router)
app.include_router(data_input.router)
app.include_router(admin.router)
+10 -4
View File
@@ -76,15 +76,21 @@ def create_product(
def create_contract(
request: Request,
product_id: int = Form(...),
code: str = Form(...),
name: str = Form(...),
month: str = Form(...),
db: Session = Depends(get_db),
):
existing = db.query(Contract).filter(Contract.code == code.upper()).first()
product = db.query(Product).filter(Product.id == product_id).first()
if not product:
return RedirectResponse("/admin/?tab=contract", status_code=303)
code = (product.code + month).upper()
name = month.upper()
existing = db.query(Contract).filter(Contract.code == code).first()
if not existing:
c = Contract(
product_id=product_id,
code=code.upper(),
code=code,
name=name,
is_active=True,
)
-71
View File
@@ -1,71 +0,0 @@
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)
+19 -14
View File
@@ -133,28 +133,34 @@
{# ═══════════════════ Tab: 合约管理 ═══════════════════ #}
<div class="tab-panel{% if tab == 'contract' %} active{% endif %}" id="tab-contract">
{% if products %}
{% for p in products %}
<div class="section-title" style="margin-top:{% if not loop.first %}24px{% else %}0{% endif %};">{{ p.code }} · {{ p.name }}</div>
<div class="form-card" style="margin-bottom:12px;">
{# ── Add contract form ── #}
<div class="form-card" style="margin-bottom:24px;">
<div class="section-title">添加合约</div>
<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;">
<label>品种</label>
<select name="product_id" required>
<option value="">-- 选择品种 --</option>
{% for p in products %}
<option value="{{ p.id }}">{{ p.code }} · {{ p.name }}</option>
{% endfor %}
</select>
</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 class="form-group" style="margin-bottom:0;flex:1;min-width:140px;">
<label>月份 <span style="font-weight:400;color:var(--sub);font-size:0.75rem;">如 2609</span></label>
<input type="text" name="month" required placeholder="2609" maxlength="4" style="text-transform:uppercase;">
</div>
<button type="submit" class="btn btn-primary">添加合约</button>
<button type="submit" class="btn btn-primary">添加</button>
</form>
</div>
{# ── Contract tables per product ── #}
{% for p in products %}
{% if p.contracts %}
<div class="section-title" style="margin-top:{% if not loop.first %}24px{% else %}0{% endif %};">{{ p.code }} · {{ p.name }}</div>
<div class="table-wrap" style="margin-bottom:20px;">
<table>
<tr><th>合约代码</th><th>名称</th><th>状态</th><th>操作</th></tr>
<tr><th>合约代码</th><th>月份</th><th>状态</th><th>操作</th></tr>
{% for c in p.contracts %}
<tr>
<td><strong>{{ c.code }}</strong></td>
@@ -176,8 +182,6 @@
{% endfor %}
</table>
</div>
{% else %}
<div style="color:var(--sub);font-size:0.85rem;margin-bottom:16px;">暂无合约</div>
{% endif %}
{% endfor %}
{% else %}
@@ -207,5 +211,6 @@ function toggleProduct(header) {
arrow.style.transform = 'rotate(-90deg)';
}
}
</script>
{% endblock %}
+1 -6
View File
@@ -27,16 +27,13 @@
<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>
<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 %}
@@ -53,8 +50,6 @@
<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>
-3
View File
@@ -198,9 +198,6 @@
<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>
+34 -41
View File
@@ -4,25 +4,6 @@
{% 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" style="color:var(--accent);">{{ next_amp if next_amp else '—' }}</div>
<div class="trend">近5日均波幅</div>
</div>
<div class="stat-card">
<div class="label">最新波幅</div>
<div class="value">{{ latest.diff|int }} 点</div>
<div class="trend">{{ latest.date }}</div>
</div>
</div>
{% endif %}
<div class="section-title">日线数据</div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;">
@@ -46,7 +27,7 @@
<td class="na"></td>
<td class="na"></td>
<td class="na"></td>
<td><span style="color:var(--accent);font-weight:700;">{{ next_amp }}</span></td>
<td><span class="amp-cell" style="color:var(--accent);font-weight:700;" data-next="1">{{ next_amp }}</span></td>
</tr>
{% endif %}
{% for row in rows %}
@@ -84,32 +65,44 @@ document.querySelectorAll('.amp-cell').forEach(function(cell) {
cell.addEventListener('click', function() {
var row = cell.parentElement.parentElement;
var tbl = document.getElementById('bars-table');
// Only rows with data-index (skip header + next-day row)
var dataRows = tbl.querySelectorAll('tr[data-index]');
// Next-day row: use latest 5 data rows
if (cell.dataset.next) {
if (dataRows.length < 5) return;
showDrawer(cell, '{{ contract }}', dataRows, 0, 4);
return;
}
var idx = parseInt(row.dataset.index);
// Need 5 older rows (higher index, since newest first)
if (idx + 5 >= dataRows.length) 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 + 1; j <= idx + 5; j++) {
var r = dataRows[j];
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> &divide; 5 = <b>' + (sum / 5).toFixed(1) + '</b><br>四舍五入 &rarr; <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;
showDrawer(cell, '{{ contract }}', dataRows, idx + 1, idx + 5);
});
});
function showDrawer(cell, product, dataRows, fromIdx, toIdx) {
document.getElementById('drawer-title').textContent = product;
var label = cell.dataset.next ? '次日预测' : '目标';
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];
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> &divide; 5 = <b>' + (sum / 5).toFixed(1) + '</b><br>四舍五入 &rarr; <b>' + Math.round(sum / 5) + '</b>';
document.getElementById('overlay').classList.add('show');
document.getElementById('drawer').classList.add('show');
var row = cell.parentElement.parentElement;
if (activeRow) activeRow.classList.remove('active');
row.classList.add('active');
activeRow = row;
}
</script>
{% endblock %}
-45
View File
@@ -1,45 +0,0 @@
{% 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 %}