Files
qihuo/templates/strategy.html
T
dekun 9cd81a3ea7 Isolate CTP in worker process and improve strategy roll UX.
Split vn.py into qihuo-ctp worker with IPC client bridge, keep CTP connected during breaks with cached account fallback, speed up strategy page loads, and allow off-session breakout roll submissions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 12:37:44 +08:00

216 lines
13 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 %}
<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 '' }}"
{% if not m.roll_eligible %}disabled{% endif %}>
{{ 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>
{% endif %}
</form>
{% else %}
<p class="empty-hint">暂无可用持仓监控</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 src="{{ url_for('static', filename='js/strategy.js') }}"></script>
{% endblock %}