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.database import engine, Base, SessionLocal
from app.models import User from app.models import User
from app.seed import seed 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" TEMPLATES_DIR = Path(__file__).parent / "templates"
@@ -56,7 +56,6 @@ app.add_middleware(AuthMiddleware)
app.include_router(auth.router) app.include_router(auth.router)
app.include_router(contracts.router) app.include_router(contracts.router)
app.include_router(analysis.router) app.include_router(analysis.router)
app.include_router(data_input.router)
app.include_router(admin.router) app.include_router(admin.router)
+10 -4
View File
@@ -76,15 +76,21 @@ def create_product(
def create_contract( def create_contract(
request: Request, request: Request,
product_id: int = Form(...), product_id: int = Form(...),
code: str = Form(...), month: str = Form(...),
name: str = Form(...),
db: Session = Depends(get_db), 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: if not existing:
c = Contract( c = Contract(
product_id=product_id, product_id=product_id,
code=code.upper(), code=code,
name=name, name=name,
is_active=True, 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: 合约管理 ═══════════════════ #} {# ═══════════════════ Tab: 合约管理 ═══════════════════ #}
<div class="tab-panel{% if tab == 'contract' %} active{% endif %}" id="tab-contract"> <div class="tab-panel{% if tab == 'contract' %} active{% endif %}" id="tab-contract">
{% if products %} {% if products %}
{% for p in products %} {# ── Add contract form ── #}
<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:24px;">
<div class="section-title">添加合约</div>
<div class="form-card" style="margin-bottom:12px;">
<form method="post" action="/admin/contract" style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;"> <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;"> <div class="form-group" style="margin-bottom:0;flex:1;min-width:140px;">
<label>合约代码</label> <label>品种</label>
<input type="text" name="code" required placeholder="{{ p.code }}2609" maxlength="10" style="text-transform:uppercase;"> <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>
<div class="form-group" style="margin-bottom:0;flex:2;min-width:160px;"> <div class="form-group" style="margin-bottom:0;flex:1;min-width:140px;">
<label>合约名称</label> <label>月份 <span style="font-weight:400;color:var(--sub);font-size:0.75rem;">如 2609</span></label>
<input type="text" name="name" required placeholder="{{ p.name }}2609"> <input type="text" name="month" required placeholder="2609" maxlength="4" style="text-transform:uppercase;">
</div> </div>
<button type="submit" class="btn btn-primary">添加合约</button> <button type="submit" class="btn btn-primary">添加</button>
</form> </form>
</div> </div>
{# ── Contract tables per product ── #}
{% for p in products %}
{% if p.contracts %} {% 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;"> <div class="table-wrap" style="margin-bottom:20px;">
<table> <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 %} {% for c in p.contracts %}
<tr> <tr>
<td><strong>{{ c.code }}</strong></td> <td><strong>{{ c.code }}</strong></td>
@@ -176,8 +182,6 @@
{% endfor %} {% endfor %}
</table> </table>
</div> </div>
{% else %}
<div style="color:var(--sub);font-size:0.85rem;margin-bottom:16px;">暂无合约</div>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% else %} {% else %}
@@ -207,5 +211,6 @@ function toggleProduct(header) {
arrow.style.transform = 'rotate(-90deg)'; arrow.style.transform = 'rotate(-90deg)';
} }
} }
</script> </script>
{% endblock %} {% endblock %}
+1 -6
View File
@@ -27,16 +27,13 @@
<div class="section-title">机构持仓明细</div> <div class="section-title">机构持仓明细</div>
<div class="table-wrap"> <div class="table-wrap">
<table> <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> </tr>
{% for r in rows %} {% for r in rows %}
<tr> <tr>
<td><strong>{{ r.institution }}</strong></td> <td><strong>{{ r.institution }}</strong></td>
<td>{{ r.long_pos }}</td> <td>{{ r.long_pos }}</td>
<td>{{ r.short_pos }}</td> <td>{{ r.short_pos }}</td>
<td>{{ r.long_cost }}</td>
<td>{{ r.short_cost }}</td>
<td>{{ r.net_pos }}</td> <td>{{ r.net_pos }}</td>
<td> <td>
{% if r.pnl_raw > 0 %} {% if r.pnl_raw > 0 %}
@@ -53,8 +50,6 @@
<td>合计</td> <td>合计</td>
<td>{{ totals.long_pos }}</td> <td>{{ totals.long_pos }}</td>
<td>{{ totals.short_pos }}</td> <td>{{ totals.short_pos }}</td>
<td></td>
<td></td>
<td>{{ totals.net_pos }}</td> <td>{{ totals.net_pos }}</td>
<td>{{ totals.pnl }}</td> <td>{{ totals.pnl }}</td>
</tr> </tr>
-3
View File
@@ -198,9 +198,6 @@
<a href="/analysis/" class="{% if active_nav == 'analysis' %}active{% endif %}"> <a href="/analysis/" class="{% if active_nav == 'analysis' %}active{% endif %}">
<span class="icon">⚔️</span> 博弈分析 <span class="icon">⚔️</span> 博弈分析
</a> </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 %}"> <a href="/admin/" class="{% if active_nav == 'admin' %}active{% endif %}">
<span class="icon">⚙️</span> 系统管理 <span class="icon">⚙️</span> 系统管理
</a> </a>
+34 -41
View File
@@ -4,25 +4,6 @@
{% block breadcrumb %}<a href="/contracts/">行情数据</a> / {{ contract }}{% endblock %} {% block breadcrumb %}<a href="/contracts/">行情数据</a> / {{ contract }}{% endblock %}
{% block content %} {% 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 class="section-title">日线数据</div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;"> <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 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> </tr>
{% endif %} {% endif %}
{% for row in rows %} {% for row in rows %}
@@ -84,32 +65,44 @@ document.querySelectorAll('.amp-cell').forEach(function(cell) {
cell.addEventListener('click', function() { cell.addEventListener('click', function() {
var row = cell.parentElement.parentElement; var row = cell.parentElement.parentElement;
var tbl = document.getElementById('bars-table'); var tbl = document.getElementById('bars-table');
// Only rows with data-index (skip header + next-day row)
var dataRows = tbl.querySelectorAll('tr[data-index]'); 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); var idx = parseInt(row.dataset.index);
// Need 5 older rows (higher index, since newest first) // Need 5 older rows (higher index, since newest first)
if (idx + 5 >= dataRows.length) return; if (idx + 5 >= dataRows.length) return;
showDrawer(cell, '{{ contract }}', dataRows, idx + 1, idx + 5);
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;
}); });
}); });
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> </script>
{% endblock %} {% 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 %}