Compare commits
9 Commits
aaf7d0afd1
...
57f792fbe5
| Author | SHA1 | Date | |
|---|---|---|---|
| 57f792fbe5 | |||
| 2854eb97bb | |||
| 1488fc42e1 | |||
| 557c7dbda0 | |||
| 2bcae30378 | |||
| 970bea5222 | |||
| 6718e5e2cc | |||
| 30900dc3ad | |||
| 0947b73a1a |
+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, 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)
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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> ÷ 5 = <b>' + (sum / 5).toFixed(1) + '</b><br>四舍五入 → <b>' + Math.round(sum / 5) + '</b>';
|
||||
|
||||
document.getElementById('overlay').classList.add('show');
|
||||
document.getElementById('drawer').classList.add('show');
|
||||
|
||||
if (activeRow) activeRow.classList.remove('active');
|
||||
row.classList.add('active');
|
||||
activeRow = row;
|
||||
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> ÷ 5 = <b>' + (sum / 5).toFixed(1) + '</b><br>四舍五入 → <b>' + Math.round(sum / 5) + '</b>';
|
||||
|
||||
document.getElementById('overlay').classList.add('show');
|
||||
document.getElementById('drawer').classList.add('show');
|
||||
|
||||
var row = cell.parentElement.parentElement;
|
||||
if (activeRow) activeRow.classList.remove('active');
|
||||
row.classList.add('active');
|
||||
activeRow = row;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user