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.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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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: 合约管理 ═══════════════════ #}
|
{# ═══════════════════ 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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,18 +65,30 @@ 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 }}';
|
function showDrawer(cell, product, dataRows, fromIdx, toIdx) {
|
||||||
document.getElementById('drawer-date').textContent = '目标: ' + row.dataset.date + ' ' + row.dataset.weekday + ' 振幅 ' + cell.textContent.trim();
|
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 html = '<tr><td>日期</td><td>最高−最低</td></tr>';
|
||||||
var sum = 0;
|
var sum = 0;
|
||||||
for (var j = idx + 1; j <= idx + 5; j++) {
|
for (var j = fromIdx; j <= toIdx; j++) {
|
||||||
var r = dataRows[j];
|
var r = dataRows[j];
|
||||||
html += '<tr><td>' + r.dataset.date + ' ' + r.dataset.weekday + '</td><td>' + r.dataset.diff + '</td></tr>';
|
html += '<tr><td>' + r.dataset.date + ' ' + r.dataset.weekday + '</td><td>' + r.dataset.diff + '</td></tr>';
|
||||||
sum += parseInt(r.dataset.diff);
|
sum += parseInt(r.dataset.diff);
|
||||||
@@ -106,10 +99,10 @@ document.querySelectorAll('.amp-cell').forEach(function(cell) {
|
|||||||
document.getElementById('overlay').classList.add('show');
|
document.getElementById('overlay').classList.add('show');
|
||||||
document.getElementById('drawer').classList.add('show');
|
document.getElementById('drawer').classList.add('show');
|
||||||
|
|
||||||
|
var row = cell.parentElement.parentElement;
|
||||||
if (activeRow) activeRow.classList.remove('active');
|
if (activeRow) activeRow.classList.remove('active');
|
||||||
row.classList.add('active');
|
row.classList.add('active');
|
||||||
activeRow = row;
|
activeRow = row;
|
||||||
});
|
}
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% 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