Files
qihuo/modules/web/templates/strategy.html
T
dekun 3c53b2063f Sync CTP monitors on strategy page load for roll trading.
Revive and sync active monitors from live positions before listing roll candidates, show ineligible monitors with reasons, and validate eligibility on preview.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 21:55:50 +08:00

227 lines
14 KiB
HTML

{# Copyright (c) 2025-2026 马建军. All rights reserved. 专有软件,详见 LICENSE.zh-CN.txt #}
{% extends "base.html" %}
{% block title %}策略交易 - 国内期货 · 交易复盘系统{% endblock %}
{% block extra_css %}
<style>
.strategy-page .split-grid .card{min-height:420px;display:flex;flex-direction:column}
.strategy-page .split-grid .card-body{flex:1}
.strategy-preview{background:var(--card-inner);border:1px solid var(--card-border);border-radius:8px;padding:.65rem .85rem;font-size:.78rem;line-height:1.5;margin-top:.75rem;max-height:360px;overflow:auto}
.strategy-preview .trend-summary{margin-bottom:.45rem;color:var(--text-title);font-size:.8rem;line-height:1.55}
.strategy-preview .trend-detail{margin-bottom:.55rem;color:var(--text-muted);font-size:.75rem;line-height:1.5}
.strategy-preview-table{width:100%;border-collapse:collapse;font-size:.72rem;min-width:520px}
.strategy-preview-table th,.strategy-preview-table td{padding:.35rem .4rem;border-bottom:1px solid var(--table-border);text-align:right;white-space:nowrap}
.strategy-preview-table th:first-child,.strategy-preview-table td:first-child{text-align:left}
.strategy-preview-table thead th{color:var(--text-muted);font-weight:600;background:var(--list-item-bg)}
.strategy-page .form-line.line-trend-head{grid-template-columns:1.5fr .75fr .85fr}
.strategy-steps{margin:.75rem 0 0;padding-left:1.1rem;font-size:.82rem;color:var(--text-muted);line-height:1.6}
.strategy-steps a{color:var(--accent)}
.strategy-active-roll{margin-top:.65rem;padding:.55rem .75rem;background:var(--card-inner);border-radius:8px;font-size:.8rem;border:1px solid var(--card-border)}
</style>
{% endblock %}
{% block content %}
<div class="strategy-page">
<details class="module-rules">
<summary>规则说明</summary>
<div class="module-rules-body">
<p><strong>趋势回调</strong></p>
<ul>
<li>填写品种、方向、周期、止损、补仓边界、止盈 → <strong>预览计划</strong> → 确认后<strong>市价开首仓</strong></li>
<li>须 CTP 已连接、交易时段内、账户风控允许开仓</li>
<li>运行中:价格触及<strong>止盈</strong> → 全部平仓结案;回落至<strong>网格档位</strong> → 自动分档补仓</li>
<li>可手动「结束计划」;记录归档至策略交易记录</li>
</ul>
<p><strong>顺势加仓(滚仓)</strong></p>
<ul>
<li><strong>固定金额(以损定仓)</strong>模式;<strong>移动保本</strong>持仓不可滚仓</li>
<li>须「下单监控」有 active 持仓;与趋势回调互斥</li>
<li>风险预算 = 系统设置<strong>固定金额</strong>;合并持仓打到新止损总亏损 ≤ B</li>
<li>最多 3 腿(已成交);同一组最多 1 条 pending 监控腿</li>
<li>止盈锁定首仓;突破由程序监控标记价穿越后市价加仓</li>
</ul>
</div>
</details>
<div class="split-grid">
<div class="card">
<h2>趋势回调</h2>
<div class="card-body">
{% if active_trend %}
<p class="hint">运行中 #{{ active_trend.id }} · {{ active_trend.symbol_name or active_trend.symbol }} · {{ '做多' if active_trend.direction == 'long' else '做空' }} · {{ active_trend.period_label or '15分' }}</p>
<p class="hint">已开 <strong>{{ active_trend.lots_open or 0 }}</strong> / {{ active_trend.target_lots }} 手 · 止损 {{ active_trend.stop_loss }} · 止盈 {{ active_trend.take_profit }}</p>
<form id="trend-stop-form" class="form-row" style="margin-top:.75rem">
<input type="hidden" name="plan_id" value="{{ active_trend.id }}">
<button type="button" class="btn-primary" id="btn-trend-stop">结束计划</button>
</form>
<p class="hint" style="margin-top:.75rem;font-size:.75rem">后台按档位自动补仓,触及止盈或手动结束。</p>
{% else %}
<p class="hint" style="margin-bottom:.65rem">设置止损/补仓边界/止盈 → 预览 → 确认执行首仓;后续自动分档加仓。</p>
<form id="trend-form" class="form-compact">
<div class="form-line line-trend-head">
<div class="symbol-wrap symbol-mains">
<input type="text" class="symbol-input" placeholder="品种,输入中文或代码" autocomplete="off" required>
<input type="hidden" name="symbol" class="symbol-ths-code">
<input type="hidden" name="symbol_name">
<div class="symbol-dropdown"></div>
<div class="symbol-selected"></div>
</div>
<select name="direction"><option value="long">做多</option><option value="short">做空</option></select>
<select name="period" title="参考 K 线周期">
{% for p in trend_periods %}
<option value="{{ p.key }}"{% if p.key == '15m' %} selected{% endif %}>{{ p.label }}</option>
{% endfor %}
</select>
</div>
<div class="form-line line-3">
<input name="stop_loss" type="number" step="any" placeholder="止损" required>
<input name="add_upper" type="number" step="any" placeholder="补仓边界" required>
<input name="take_profit" type="number" step="any" placeholder="止盈" required>
</div>
<div class="form-line line-2">
<input name="risk_percent" type="number" step="0.1" value="{{ risk_percent }}" placeholder="单笔风险 %" title="单笔风险%">
<button type="button" class="btn-primary" id="btn-trend-preview">预览计划</button>
</div>
</form>
<div id="trend-preview" class="strategy-preview" hidden></div>
<button type="button" class="btn-primary" id="btn-trend-exec" hidden style="margin-top:.65rem;width:100%">确认执行首仓</button>
{% endif %}
</div>
</div>
<div class="card">
<h2>顺势加仓(滚仓)</h2>
<div class="card-body">
<details class="module-rules" style="margin-bottom:.65rem">
<summary>顺势加仓规则说明</summary>
<div class="module-rules-body" style="font-size:.78rem;line-height:1.55">
<ul>
<li>手动提交;须实盘已有同向持仓与 active 监控单</li>
<li>计仓模式须为<strong>固定金额</strong>;移动保本不可滚仓</li>
<li>做多/做空各最多 3 次滚仓(仅计已成交);止盈为首仓 TP 不变</li>
<li>风险预算 B = 系统设置中的<strong>固定金额</strong>;打到新止损 S 时合并持仓总亏损 ≤ B</li>
<li>突破:标记价穿越触发价后按当时持仓重算手数再市价加仓</li>
<li>pending 腿不可改,只能删除;手动平仓后滚仓组关闭</li>
</ul>
</div>
</details>
{% if not roll_allowed %}
<p class="hint text-muted">当前为「{{ sizing_mode_label }}」模式,滚仓不可用。请在系统设置切换为<strong>固定金额</strong></p>
{% endif %}
{% if monitors %}
{% set roll_eligible_count = monitors|selectattr('roll_eligible')|list|length %}
{% if roll_eligible_count == 0 %}
<p class="hint text-muted">检测到 {{ monitors|length }} 条持仓监控,但当前均不可滚仓(见下拉项说明)。</p>
{% endif %}
<p class="hint" id="roll-risk-hint">风险预算(固定金额):<strong id="roll-risk-budget">{{ '%.0f'|format(fixed_amount) }} 元</strong></p>
<form id="roll-form" class="form-compact">
<div class="form-line line-2">
<select name="monitor_id" id="roll-monitor-select" required {% if not roll_allowed %}disabled{% endif %}>
{% for m in monitors %}
<option value="{{ m.id }}"
data-direction="{{ m.direction }}"
data-eligible="{{ '1' if m.roll_eligible else '0' }}"
data-block="{{ m.roll_block_reason or '' }}">
{{ m.symbol_name or m.symbol }} {{ m.symbol }} · {{ '多' if m.direction == 'long' else '空' }} #{{ m.id }}
· {{ m.lots }}手 · SL {{ m.stop_loss or '—' }}
{% if not m.roll_eligible %} · {{ m.roll_block_reason }}{% endif %}
</option>
{% endfor %}
</select>
<select name="add_mode" id="roll-add-mode" {% if not roll_allowed %}disabled{% endif %}>
<option value="market">市价加仓</option>
<option value="breakout">突破加仓</option>
</select>
</div>
<div class="form-line line-2">
<input name="new_stop_loss" id="roll-new-sl" type="number" step="any" placeholder="新统一止损" required {% if not roll_allowed %}disabled{% endif %}>
<input name="breakthrough_price" id="roll-break-price" type="number" step="any" placeholder="突破价" hidden>
</div>
<div class="form-line line-2">
<button type="button" class="btn-primary" id="btn-roll-preview" {% if not roll_allowed %}disabled{% endif %}>预览</button>
<button type="button" class="btn-primary" id="btn-roll-exec" hidden {% if not roll_allowed %}disabled{% endif %}>执行滚仓</button>
</div>
<div id="roll-preview" class="strategy-preview" hidden></div>
<p class="hint" id="roll-exec-hint" hidden style="font-size:.75rem;margin-top:.45rem">市价加仓:须交易时段内确认,10 秒倒计时执行;突破加仓:休盘也可提交,开盘后再监控触价</p>
{% if not trading_session %}
<p class="hint text-muted" id="roll-off-session-hint" style="font-size:.75rem;margin-top:.35rem">当前{{ session_clock.status_label or '休盘' }}:市价加仓须交易时段内执行;休盘可先预览,或选「突破加仓」提交监控。</p>
{% else %}
<p class="hint text-muted" id="roll-off-session-hint" hidden style="font-size:.75rem;margin-top:.35rem"></p>
{% endif %}
</form>
{% else %}
<p class="empty-hint">暂无可用持仓监控</p>
<p class="hint text-muted" style="font-size:.78rem">若「委托与持仓」中已有持仓,请先在持仓页连接 CTP 并刷新,再回到本页。</p>
<ol class="strategy-steps">
<li>打开 <a href="{{ url_for('positions') }}">持仓监控</a>,连接 CTP</li>
<li>系统设置为<strong>固定金额</strong>,在「期货下单」开仓(勿开移动保本)</li>
<li>开仓成功后生成本页可选监控,即可滚仓</li>
</ol>
{% endif %}
<h3 style="font-size:.85rem;margin:1rem 0 .45rem">活跃滚仓组</h3>
{% if roll_groups %}
<div class="table-responsive">
<table class="strategy-preview-table">
<thead><tr>
<th>ID</th><th>品种</th><th>方向</th><th>腿数</th><th>首仓手数</th><th>当前总手数</th><th>首仓TP</th><th>当前SL</th><th>当前均价</th><th>止盈盈利(元)</th>
</tr></thead>
<tbody>
{% for g in roll_groups %}
<tr>
<td>#{{ g.id }}</td>
<td>{{ g.symbol_name or g.symbol }}</td>
<td>{{ '多' if g.direction == 'long' else '空' }}</td>
<td>{{ g.leg_count or 0 }}/3</td>
<td>{{ g.first_lots if g.first_lots is not none else '—' }}</td>
<td>{{ g.total_lots if g.total_lots is not none else '—' }}</td>
<td>{{ g.initial_take_profit or '—' }}</td>
<td>{{ g.current_stop_loss or '—' }}</td>
<td>{{ g.avg_entry or '—' }}</td>
<td>{{ g.reward_at_tp if g.reward_at_tp is not none else '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="hint text-muted">暂无</p>
{% endif %}
<h3 style="font-size:.85rem;margin:1rem 0 .45rem">正在滚仓</h3>
{% if roll_legs %}
<div class="table-responsive">
<table class="strategy-preview-table">
<thead><tr>
<th>#</th><th></th><th>方式</th><th>手数</th><th>触发/限价</th><th>新SL</th><th>当前价</th><th>状态</th><th>操作</th>
</tr></thead>
<tbody>
{% for l in roll_legs %}
<tr>
<td>{{ l.id }}</td>
<td>#{{ l.roll_group_id }}</td>
<td>{{ add_mode_labels.get(l.add_mode, l.add_mode) }}</td>
<td>{{ l.lots or '—' }}</td>
<td>{{ l.breakthrough_price or l.limit_price or l.fill_price or '—' }}</td>
<td>{{ l.new_stop_loss or '—' }}</td>
<td>{{ l.current_price if l.current_price is not none else '—' }}</td>
<td title="{{ l.invalidated_reason or '' }}">{{ roll_leg_status_labels.get(l.status, l.status) }}{% if l.status == 'invalidated' and l.invalidated_reason %} · {{ l.invalidated_reason[:24] }}{% endif %}</td>
<td>{% if l.status == 'pending' %}<button type="button" class="btn-link roll-cancel-leg" data-leg-id="{{ l.id }}">删除</button>{% else %}—{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="hint text-muted">暂无</p>
{% endif %}
</div>
</div>
</div>
<p class="hint" style="margin-top:1rem"><a href="{{ url_for('strategy_records_page') }}">策略交易记录 →</a></p>
</div>
{% endblock %}
{% block extra_js %}
<script>
window.STRATEGY_PAGE = {
inTradingSession: {{ 'true' if trading_session else 'false' }}
};
</script>
<script src="{{ url_for('static', filename='js/strategy.js') }}"></script>
{% endblock %}