d5648e0b2c
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
240 lines
11 KiB
HTML
240 lines
11 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}{{ contract }}{% endblock %}
|
|
{% block heading %}{{ contract }}{% endblock %}
|
|
{% block breadcrumb %}<a href="/contracts/">行情数据</a> / {{ contract }}{% endblock %}
|
|
|
|
{% block content %}
|
|
|
|
<div class="section-title">日线数据</div>
|
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;">
|
|
<p style="font-size:0.78rem;color:var(--sub);margin:0;">
|
|
振幅 = 近 5 日 (最高−最低) 均值取整 · 点击振幅值查看计算过程
|
|
</p>
|
|
<span style="font-size:0.78rem;color:var(--sub);">共 {{ total_rows }} 条</span>
|
|
</div>
|
|
|
|
{% if page == 1 and next_date %}
|
|
<div style="background:var(--accent-light);border:1px solid var(--accent);border-radius:8px;padding:12px 18px;margin-bottom:14px;display:flex;align-items:center;gap:16px;">
|
|
<span style="font-size:0.78rem;color:var(--sub);">📅 次日预测</span>
|
|
<span style="font-weight:600;">{{ next_date }} {{ next_weekday }}</span>
|
|
<span style="color:var(--sub);">预计振幅</span>
|
|
<span class="amp-cell" style="color:var(--accent);font-weight:700;font-size:1.1rem;cursor:pointer;" data-next="1">{{ next_amp }}</span>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<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>
|
|
{% for row in rows %}
|
|
<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>
|
|
<td>{{ row.close }}</td>
|
|
<td>{{ row.high }}</td>
|
|
<td>{{ row.low }}</td>
|
|
<td>{{ row.diff }}</td>
|
|
<td>
|
|
{% if row.has_amp %}
|
|
<span class="amp-cell">{{ row.amp_5d }}</span>
|
|
{% else %}
|
|
<span class="na">—</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</table>
|
|
</div>
|
|
|
|
{% 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 %}
|
|
|
|
{% 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;">
|
|
<button onclick="openAnalysisDrawer()" style="margin-left:auto;padding:6px 16px;border:1px solid var(--accent);border-radius:5px;background:var(--accent-light);color:var(--accent);cursor:pointer;font-size:0.82rem;font-weight:600;white-space:nowrap;">AI 分析</button>
|
|
</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(--danger-fg){% elif row.change < 0 %}var(--success-fg){% else %}var(--sub){% endif %};">
|
|
{% if row.change > 0 %}+{% endif %}{{ row.change }}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% if dtype != 'volume' %}
|
|
<tr style="background:var(--th-bg);font-weight:600;">
|
|
<td></td><td style="text-align:right;">增减合计</td>
|
|
<td></td>
|
|
<td style="color:{% if pos_totals[dtype] > 0 %}var(--danger-fg){% elif pos_totals[dtype] < 0 %}var(--success-fg){% else %}var(--sub){% endif %};">
|
|
{% if pos_totals[dtype] > 0 %}+{% endif %}{{ pos_totals[dtype] }}
|
|
</td>
|
|
</tr>
|
|
{% endif %}
|
|
</table>
|
|
</div>
|
|
{% endfor %}
|
|
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div id="analysisOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:99;" onclick="closeAnalysisDrawer()"></div>
|
|
<div id="analysisDrawer" style="display:none;position:fixed;top:0;right:0;width:700px;max-width:95vw;height:100%;background:var(--surface);border-left:1px solid var(--border);z-index:100;box-shadow:-4px 0 24px rgba(0,0,0,.12);transform:translateX(100%);transition:transform .25s ease;display:flex;flex-direction:column;">
|
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--border);">
|
|
<h3 style="margin:0;font-size:1rem;">持仓分析 {{ contract }}</h3>
|
|
<div style="display:flex;gap:8px;">
|
|
<button onclick="copyPrompt()" style="padding:4px 12px;border:1px solid var(--border);border-radius:4px;background:var(--surface);color:var(--fg);cursor:pointer;font-size:0.78rem;">复制</button>
|
|
<button onclick="closeAnalysisDrawer()" style="background:none;border:none;font-size:1.3rem;cursor:pointer;color:var(--sub);line-height:1;">×</button>
|
|
</div>
|
|
</div>
|
|
<pre id="analysisContent" style="flex:1;margin:0;padding:16px 20px;overflow-y:auto;font-size:0.78rem;line-height:1.6;white-space:pre-wrap;word-break:break-all;color:var(--fg);background:var(--bg);">加载中...</pre>
|
|
</div>
|
|
|
|
<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() {
|
|
// Next-day prediction row
|
|
if (cell.dataset.next) {
|
|
var dataRows = document.querySelectorAll('#bars-table tr[data-index]');
|
|
if (dataRows.length < 5) return;
|
|
var rows = Array.from(dataRows).slice(0, 5);
|
|
showDrawer(cell, '{{ contract }}', rows, true);
|
|
return;
|
|
}
|
|
|
|
// 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, calcRows, isNext) {
|
|
document.getElementById('drawer-title').textContent = product;
|
|
var label = isNext ? '次日预测' : '目标';
|
|
document.getElementById('drawer-date').textContent = label + ' 振幅 ' + cell.textContent.trim();
|
|
|
|
var html = '<tr><td>日期</td><td>最高−最低</td></tr>';
|
|
var sum = 0;
|
|
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>';
|
|
|
|
document.getElementById('overlay').classList.add('show');
|
|
document.getElementById('drawer').classList.add('show');
|
|
|
|
if (!isNext) {
|
|
var row = cell.parentElement.parentElement;
|
|
if (activeRow) activeRow.classList.remove('active');
|
|
row.classList.add('active');
|
|
activeRow = row;
|
|
}
|
|
}
|
|
|
|
var promptText = '';
|
|
function openAnalysisDrawer() {
|
|
var overlay = document.getElementById('analysisOverlay');
|
|
var drawer = document.getElementById('analysisDrawer');
|
|
drawer.style.display = 'flex';
|
|
overlay.style.display = 'block';
|
|
requestAnimationFrame(function() {
|
|
drawer.style.transform = 'translateX(0)';
|
|
});
|
|
var content = document.getElementById('analysisContent');
|
|
if (promptText) {
|
|
content.textContent = promptText;
|
|
return;
|
|
}
|
|
content.textContent = '加载中...';
|
|
fetch('/contracts/{{ contract }}/analysis-prompt')
|
|
.then(function(r) { return r.text(); })
|
|
.then(function(t) {
|
|
promptText = t;
|
|
content.textContent = t;
|
|
})
|
|
.catch(function() { content.textContent = '加载失败'; });
|
|
}
|
|
function closeAnalysisDrawer() {
|
|
document.getElementById('analysisOverlay').style.display = 'none';
|
|
document.getElementById('analysisDrawer').style.transform = 'translateX(100%)';
|
|
}
|
|
function copyPrompt() {
|
|
navigator.clipboard.writeText(promptText).then(function() {
|
|
showToast('已复制到剪贴板');
|
|
});
|
|
}
|
|
function showToast(msg) {
|
|
var m = document.getElementById('toastMask');
|
|
if (!m) {
|
|
m = document.createElement('div');
|
|
m.id = 'toastMask';
|
|
m.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.3);z-index:999;display:flex;align-items:center;justify-content:center;transition:opacity .25s;opacity:0;';
|
|
m.onclick = function() { hideToast(); };
|
|
var b = document.createElement('div');
|
|
b.id = 'toastBox';
|
|
b.style.cssText = 'background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:32px 40px;text-align:center;box-shadow:0 8px 32px rgba(0,0,0,.18);';
|
|
m.appendChild(b);
|
|
document.body.appendChild(m);
|
|
}
|
|
var box = document.getElementById('toastBox');
|
|
box.innerHTML = '<div style="font-size:2rem;margin-bottom:12px;">✅</div><p style="font-size:1rem;font-weight:600;color:var(--fg);">' + msg + '</p>';
|
|
m.style.display = 'flex';
|
|
requestAnimationFrame(function() { m.style.opacity = '1'; });
|
|
clearTimeout(m._tid);
|
|
m._tid = setTimeout(hideToast, 1800);
|
|
}
|
|
function hideToast() {
|
|
var m = document.getElementById('toastMask');
|
|
if (!m) return;
|
|
m.style.opacity = '0';
|
|
setTimeout(function() { m.style.display = 'none'; }, 250);
|
|
}
|
|
</script>
|
|
{% endblock %}
|