227d8b515f
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
276 lines
14 KiB
HTML
276 lines
14 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}对冲管理{% endblock %}
|
||
{% block heading %}对冲管理{% endblock %}
|
||
{% block breadcrumb %}振幅锁仓 · 仓位跟踪{% endblock %}
|
||
|
||
{% block content %}
|
||
|
||
{% set error = request.query_params.get('error', '') %}
|
||
|
||
{% if error %}
|
||
<div style="background:var(--danger-bg);color:var(--danger-fg);padding:10px 16px;border-radius:6px;margin-bottom:16px;font-size:0.88rem;">{{ error }}</div>
|
||
{% endif %}
|
||
|
||
{# ═══════════════ 新建轮次 ═══════════════ #}
|
||
<div class="section-title">新建轮次</div>
|
||
<div class="form-card" style="margin-bottom:28px;">
|
||
<form method="post" action="/positions/round">
|
||
<div style="display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap;">
|
||
<div class="form-group" style="margin-bottom:0;flex:1;min-width:100px;">
|
||
<label>品种</label>
|
||
<select id="productSelect" required onchange="filterContracts()">
|
||
<option value="">--</option>
|
||
{% for pcode in product_contracts %}
|
||
<option value="{{ pcode }}">{{ pcode }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div class="form-group" style="margin-bottom:0;flex:1;min-width:120px;">
|
||
<label>合约</label>
|
||
<select name="contract_code" id="contractSelect" required disabled>
|
||
<option value="">-- 先选品种 --</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group" style="margin-bottom:0;flex:1;min-width:100px;">
|
||
<label>每日手数 N</label>
|
||
<input type="number" name="daily_hands" value="1" min="1" max="10" required>
|
||
</div>
|
||
<button type="submit" class="btn btn-primary">开始新轮</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
{% if not active_rounds %}
|
||
<div style="text-align:center;padding:60px;color:var(--sub);">
|
||
<p style="font-size:1.1rem;margin-bottom:8px;">暂无活跃轮次</p>
|
||
<p style="font-size:0.82rem;">创建一轮新的交易,开始跟踪仓位和锁仓状态</p>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% for round in active_rounds %}
|
||
{# ═══════════════ 轮次卡片 ═══════════════ #}
|
||
{% set locks = round.total_locks %}
|
||
<div style="margin-bottom:32px;border:1px solid var(--border);border-radius:10px;overflow:hidden;">
|
||
|
||
{# ── 轮次头部 ── #}
|
||
<div style="display:flex;align-items:center;gap:16px;padding:14px 20px;background:var(--th-bg);flex-wrap:wrap;">
|
||
<strong style="font-size:1rem;">{{ round.contract_code }}</strong>
|
||
<span class="badge badge-up">{{ round.daily_hands }} 手/天</span>
|
||
<span style="font-size:0.82rem;font-weight:600;{% if locks >= round.max_locks %}color:var(--danger-fg);{% endif %}">
|
||
锁仓 {{ locks }}/{{ round.max_locks }}
|
||
{% if locks >= round.max_locks %} ⚠ 熔断{% endif %}
|
||
</span>
|
||
<span style="margin-left:auto;display:flex;gap:8px;">
|
||
<button style="background:var(--surface);color:var(--fg);border:1px solid var(--border);padding:3px 12px;border-radius:4px;cursor:pointer;font-size:0.78rem;"
|
||
onclick="showMaxLocksModal({{ round.id }}, {{ round.max_locks }})">修改锁仓数</button>
|
||
<form method="post" action="/positions/{{ round.id }}/close" style="display:inline;">
|
||
<input type="hidden" name="result" value="profit_taken">
|
||
<button class="btn" style="background:var(--success);color:#fff;font-size:0.78rem;padding:4px 14px;" onclick="confirmDelete(event, '确认 {{ round.contract_code }} 止盈清仓?', this.closest('form').action)">止盈</button>
|
||
</form>
|
||
<form method="post" action="/positions/{{ round.id }}/close" style="display:inline;">
|
||
<input type="hidden" name="result" value="meltdown">
|
||
<button class="btn" style="background:var(--danger);color:#fff;font-size:0.78rem;padding:4px 14px;" onclick="confirmDelete(event, '确认 {{ round.contract_code }} 熔断清仓?', this.closest('form').action)">熔断</button>
|
||
</form>
|
||
<form method="post" action="/positions/{{ round.id }}/delete" style="display:inline;">
|
||
<button style="background:transparent;color:#dc2626;border:1px solid #ef4444;padding:3px 10px;border-radius:4px;cursor:pointer;font-size:0.78rem;" onclick="confirmDelete(event, '确定删除 {{ round.contract_code }} 整轮数据?', this.closest('form').action)">删除</button>
|
||
</form>
|
||
</span>
|
||
</div>
|
||
|
||
<div style="padding:16px 20px;">
|
||
|
||
{# ── 止盈阈值 ── #}
|
||
{% if round.positions %}
|
||
{% set latest = round.positions[-1] %}
|
||
<div style="background:var(--accent-light);border:1px solid var(--accent);border-radius:6px;padding:10px 16px;margin-bottom:16px;display:flex;align-items:center;gap:20px;flex-wrap:wrap;">
|
||
<span style="font-size:0.78rem;color:var(--sub);">止盈阈值</span>
|
||
<span style="font-weight:700;">{{ round.daily_hands }} × {{ latest.amp_threshold }} = {{ round.daily_hands * latest.amp_threshold }} 点</span>
|
||
<span style="color:var(--sub);">|</span>
|
||
<span style="font-size:0.78rem;color:var(--sub);">当前 A = {{ latest.amp_threshold }}</span>
|
||
<span style="color:var(--sub);">|</span>
|
||
<span style="font-size:0.78rem;color:var(--sub);">总手数 {{ round.daily_hands * round.positions|length }}</span>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{# ── 添加仓位 ── #}
|
||
<div style="margin-bottom:16px;">
|
||
<form method="post" action="/positions/{{ round.id }}/add">
|
||
<div style="display:flex;gap:10px;align-items:flex-end;flex-wrap:wrap;">
|
||
<div class="form-group" style="margin-bottom:0;flex:1;min-width:100px;">
|
||
<label>开仓价</label>
|
||
<input type="number" name="open_price" required placeholder="如 1300" style="font-size:0.9rem;">
|
||
</div>
|
||
<div class="form-group" style="margin-bottom:0;flex:1;min-width:100px;">
|
||
<label>振幅 A</label>
|
||
<input type="number" name="amp_threshold" required placeholder="如 18" style="font-size:0.9rem;">
|
||
</div>
|
||
<div class="form-group" style="margin-bottom:0;flex:1;min-width:70px;">
|
||
<label>手数</label>
|
||
<input type="number" name="hands" value="{{ round.daily_hands }}" min="1" max="10" required style="font-size:0.9rem;">
|
||
</div>
|
||
<button type="submit" class="btn btn-primary" style="font-size:0.82rem;padding:8px 18px;">+ 添加</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
{# ── 仓位表格 ── #}
|
||
{% if round.positions %}
|
||
<div class="table-wrap" style="margin-bottom:0;">
|
||
<table style="font-size:0.84rem;">
|
||
<tr>
|
||
<th>开仓价</th><th>A</th><th>锁仓价</th><th>手数</th><th>已锁</th><th>状态</th><th>操作</th>
|
||
</tr>
|
||
{% for p in round.positions|reverse %}
|
||
<tr>
|
||
<td>{{ p.open_price }}</td>
|
||
<td>{{ p.amp_threshold }}</td>
|
||
<td style="color:var(--danger-fg);font-weight:600;">{{ p.lock_price }}</td>
|
||
<td>{{ p.hands }}</td>
|
||
<td>
|
||
{% if p.locked_count > 0 %}
|
||
<span class="badge badge-warn">{{ p.locked_count }}/{{ p.hands }}</span>
|
||
{% else %}
|
||
<span class="na">0/{{ p.hands }}</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
{% if p.locked_count >= p.hands %}
|
||
<span class="badge badge-down">已全锁</span>
|
||
{% elif p.locked_count > 0 %}
|
||
<span class="badge badge-warn">部分锁</span>
|
||
{% else %}
|
||
<span class="badge badge-up">活跃</span>
|
||
{% endif %}
|
||
</td>
|
||
<td>
|
||
<div style="display:flex;gap:6px;justify-content:center;">
|
||
<button style="background:var(--warn-bg);color:var(--warn-fg);border:1px solid var(--warn);padding:2px 8px;border-radius:4px;cursor:pointer;font-size:0.75rem;font-weight:600;"
|
||
{% if p.locked_count >= p.hands %}disabled style="opacity:0.4;cursor:default;"{% endif %}
|
||
onclick="showLockConfirm({{ p.id }}, {{ p.lock_price }})">锁</button>
|
||
<form method="post" action="/positions/{{ p.id }}/unlock" style="display:inline;">
|
||
<button style="background:var(--surface);color:var(--sub);border:1px solid var(--border);padding:2px 8px;border-radius:4px;cursor:pointer;font-size:0.75rem;"
|
||
{% if p.locked_count == 0 %}disabled style="opacity:0.4;cursor:default;"{% endif %}>撤</button>
|
||
</form>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</table>
|
||
</div>
|
||
{% else %}
|
||
<div style="text-align:center;padding:24px;color:var(--sub);font-size:0.84rem;">暂无仓位,请添加。</div>
|
||
{% endif %}
|
||
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
|
||
<script>
|
||
var productContracts = {{ product_contracts | tojson }};
|
||
function filterContracts() {
|
||
var prod = document.getElementById('productSelect').value;
|
||
var sel = document.getElementById('contractSelect');
|
||
sel.innerHTML = '<option value="">-- 选择合约 --</option>';
|
||
sel.disabled = !prod;
|
||
if (prod && productContracts[prod]) {
|
||
productContracts[prod].forEach(function(c) {
|
||
var display = c.startsWith(prod) ? c.substring(prod.length) : c;
|
||
sel.innerHTML += '<option value="' + c + '">' + display + '</option>';
|
||
});
|
||
}
|
||
}
|
||
function showMaxLocksModal(rid, currentMax) {
|
||
document.getElementById('maxLocksRid').value = rid;
|
||
document.getElementById('maxLocksInput').value = currentMax;
|
||
document.getElementById('maxLocksModal').style.display = 'block';
|
||
document.getElementById('maxLocksModalOverlay').style.display = 'block';
|
||
document.getElementById('maxLocksInput').focus();
|
||
}
|
||
function hideMaxLocksModal() {
|
||
document.getElementById('maxLocksModal').style.display = 'none';
|
||
document.getElementById('maxLocksModalOverlay').style.display = 'none';
|
||
}
|
||
function doUpdateMaxLocks() {
|
||
var rid = document.getElementById('maxLocksRid').value;
|
||
var fd = new FormData();
|
||
fd.append('max_locks', document.getElementById('maxLocksInput').value);
|
||
fetch('/positions/' + rid + '/max-locks', {method:'POST', body:fd})
|
||
.then(function() { location.reload(); });
|
||
}
|
||
function showLockConfirm(pid, lockPrice) {
|
||
document.getElementById('lockPid').value = pid;
|
||
document.getElementById('lockPriceInput').value = lockPrice;
|
||
document.getElementById('lockModal').style.display = 'block';
|
||
document.getElementById('lockModalOverlay').style.display = 'block';
|
||
document.getElementById('lockPriceInput').focus();
|
||
}
|
||
function hideLockModal() {
|
||
document.getElementById('lockModal').style.display = 'none';
|
||
document.getElementById('lockModalOverlay').style.display = 'none';
|
||
}
|
||
function doLock() {
|
||
var pid = document.getElementById('lockPid').value;
|
||
var fd = new FormData();
|
||
fd.append('lock_price', document.getElementById('lockPriceInput').value);
|
||
fetch('/positions/' + pid + '/lock', {method:'POST', body:fd})
|
||
.then(function() { location.reload(); });
|
||
}
|
||
</script>
|
||
|
||
<div id="maxLocksModalOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:159;" onclick="hideMaxLocksModal()"></div>
|
||
<div id="maxLocksModal" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;z-index:160;width:300px;max-width:90vw;">
|
||
<h3 style="margin-bottom:16px;">修改锁仓上限</h3>
|
||
<input type="hidden" id="maxLocksRid">
|
||
<div class="form-group">
|
||
<label>锁仓次数上限(熔断阈值)</label>
|
||
<input type="number" id="maxLocksInput" min="1" max="10" required style="font-size:1rem;text-align:center;font-weight:600;">
|
||
</div>
|
||
<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:8px;">
|
||
<button class="btn" style="background:var(--th-bg);color:var(--fg);" onclick="hideMaxLocksModal()">取消</button>
|
||
<button class="btn" style="background:var(--accent);color:#fff;" onclick="doUpdateMaxLocks()">确认修改</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="lockModalOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:149;" onclick="hideLockModal()"></div>
|
||
<div id="lockModal" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:24px;z-index:150;width:300px;max-width:90vw;">
|
||
<h3 style="margin-bottom:16px;">确认锁仓</h3>
|
||
<input type="hidden" id="lockPid">
|
||
<div class="form-group">
|
||
<label>锁仓价位</label>
|
||
<input type="number" id="lockPriceInput" required style="font-size:1rem;text-align:center;font-weight:600;color:var(--danger-fg);">
|
||
</div>
|
||
<div style="display:flex;gap:10px;justify-content:flex-end;margin-top:8px;">
|
||
<button class="btn" style="background:var(--th-bg);color:var(--fg);" onclick="hideLockModal()">取消</button>
|
||
<button class="btn" style="background:var(--warn);color:#fff;" onclick="doLock()">确认锁仓</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="confirmOverlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.35);z-index:199;" onclick="hideConfirm()"></div>
|
||
<div id="confirmBox" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:28px 32px;z-index:200;text-align:center;max-width:90vw;">
|
||
<p id="confirmMsg" style="font-size:0.95rem;margin-bottom:20px;"></p>
|
||
<div style="display:flex;gap:10px;justify-content:center;">
|
||
<button class="btn" style="background:var(--th-bg);color:var(--fg);" onclick="hideConfirm()">取消</button>
|
||
<button class="btn" style="background:var(--danger);color:#fff;" id="confirmBtn">确认</button>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
function confirmDelete(e, msg, url) {
|
||
e.preventDefault();
|
||
document.getElementById('confirmMsg').textContent = msg;
|
||
document.getElementById('confirmBtn').onclick = function() {
|
||
var f = document.createElement('form');
|
||
f.method = 'POST';
|
||
f.action = url;
|
||
document.body.appendChild(f);
|
||
f.submit();
|
||
};
|
||
document.getElementById('confirmOverlay').style.display = 'block';
|
||
document.getElementById('confirmBox').style.display = 'block';
|
||
}
|
||
function hideConfirm() {
|
||
document.getElementById('confirmOverlay').style.display = 'none';
|
||
document.getElementById('confirmBox').style.display = 'none';
|
||
}
|
||
</script>
|
||
|
||
{% endblock %} |