Add futures roll strategy with breakout monitoring and fixed-amount sizing.

Replace percent-based risk with system fixed amount, support market/breakout add modes only, allow pending submission outside trading hours, and fix short breakout geometry plus route registration.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-29 12:05:21 +08:00
parent 7ce59d2d71
commit 44bec23296
8 changed files with 982 additions and 160 deletions
+91 -21
View File
@@ -32,9 +32,11 @@
</ul>
<p><strong>顺势加仓(滚仓)</strong></p>
<ul>
<li>在「下单监控」已有 <strong>active 持仓监控</strong> 上扩大仓位,统一抬高止损</li>
<li>预览后执行;<strong>仓位上限冻结时仍可滚仓</strong>,但受滚仓保证金上限约束</li>
<li>最多 3 腿;止盈锁定首仓逻辑</li>
<li><strong>固定金额(以损定仓)</strong>模式;<strong>移动保本</strong>持仓不可滚仓</li>
<li>须「下单监控」有 active 持仓;与趋势回调互斥</li>
<li>风险预算 = 系统设置<strong>固定金额</strong>;合并持仓打到新止损总亏损 ≤ B</li>
<li>最多 3 腿(已成交);同一组最多 1 条 pending 监控腿</li>
<li>止盈锁定首仓;突破由程序监控标记价穿越后市价加仓</li>
</ul>
</div>
</details>
@@ -87,43 +89,111 @@
<div class="card">
<h2>顺势加仓(滚仓)</h2>
<div class="card-body">
<p class="hint" style="margin-bottom:.65rem">在已有持仓上扩大仓位,统一抬高止损;最多 3 腿,止盈锁定首仓。</p>
{% if roll_groups %}
{% for g in roll_groups %}
<div class="strategy-active-roll">
运行中 · 监控 #{{ g.order_monitor_id }} · {{ g.leg_count or 1 }} 腿 · 止损 {{ g.current_stop_loss }}
</div>
{% endfor %}
<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="field" style="margin-bottom:.5rem">
<label class="text-label" style="font-size:.72rem">选择下单监控(开仓后生成)</label>
<select name="monitor_id" required>
<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 }}">{{ m.symbol_name or m.symbol }} {{ m.symbol }} · {{ '多' if m.direction == 'long' else '空' }} {{ m.lots }}手 · SL {{ m.stop_loss or '—' }}</option>
<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" type="number" step="any" placeholder="新统一止损" required>
<input name="risk_percent" type="number" step="0.1" value="2" placeholder="总风险 %" title="总风险%">
<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">
<input name="add_price" type="number" step="any" placeholder="加仓参考价(可选)">
<button type="button" class="btn-primary" id="btn-roll-preview">预览滚仓</button>
<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>
<button type="button" class="btn-primary" id="btn-roll-exec" hidden style="margin-top:.65rem;width:100%">执行滚仓</button>
<p class="hint" id="roll-exec-hint" hidden style="font-size:.75rem;margin-top:.45rem">市价加仓:须交易时段内确认,10 秒倒计时执行;突破加仓:任意时间可提交,开盘后再监控触价</p>
</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>
<li>系统设置为<strong>固定金额</strong>,在「期货下单」开仓(勿开移动保本)</li>
<li>开仓成功后生成本页可选监控,即可滚仓</li>
</ol>
{% endif %}
<h3 style="font-size:.85rem;margin:1rem 0 .45rem">活跃滚仓组</h3>
{% if roll_groups %}
<table class="strategy-preview-table">
<thead><tr>
<th>ID</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.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>
{% else %}
<p class="hint text-muted">暂无</p>
{% endif %}
<h3 style="font-size:.85rem;margin:1rem 0 .45rem">最近滚仓腿</h3>
{% if roll_legs %}
<table class="strategy-preview-table">
<thead><tr>
<th>#</th><th></th><th>方式</th><th>手数</th><th>触发/限价</th><th>新SL</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 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>
{% else %}
<p class="hint text-muted">暂无</p>
{% endif %}
</div>
</div>
</div>