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>
This commit is contained in:
dekun
2026-07-02 21:55:50 +08:00
parent 4657d26f5e
commit 3c53b2063f
3 changed files with 59 additions and 3 deletions
+40 -1
View File
@@ -3190,6 +3190,44 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
return val return val
return None return None
def _ensure_strategy_monitors(conn, mode: str) -> int:
"""策略页加载前:恢复误关监控并同步柜台,与持仓页逻辑一致。"""
if not _cached_ctp_status(mode).get("connected"):
return 0
capital = _capital(conn)
synced = 0
seen: set[tuple[str, str]] = set()
positions = list(trading_state.get_positions() or [])
if not positions:
positions = list(_ctp_positions(mode, refresh_if_empty=False) or [])
for p in positions:
lots = int(p.get("lots") or 0)
if lots <= 0:
continue
ths = _ctp_pos_to_ths_code(p) or (p.get("symbol") or "")
direction = (p.get("direction") or "long").strip().lower()
key = (ths.lower(), direction)
if key in seen:
continue
seen.add(key)
mon = _find_or_revive_monitor(conn, ths, direction)
if not mon:
continue
mon = _restore_monitor_sl_tp_if_missing(conn, mon, ths, direction) or mon
_sync_monitor_from_ctp(
conn,
int(mon["id"]),
mon.get("symbol") or ths,
mon.get("direction") or direction,
mode,
ctp=p,
capital=capital,
)
synced += 1
if synced:
commit_retry(conn)
return synced
@app.route("/strategy") @app.route("/strategy")
@login_required @login_required
@_nav("strategy") @_nav("strategy")
@@ -3199,13 +3237,14 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
init_strategy_tables(conn) init_strategy_tables(conn)
ensure_monitor_order_columns(conn) ensure_monitor_order_columns(conn)
capital = _capital(conn) capital = _capital(conn)
mode = get_trading_mode(get_setting)
_ensure_strategy_monitors(conn, mode)
active_trend = conn.execute( active_trend = conn.execute(
"SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC LIMIT 1" "SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC LIMIT 1"
).fetchone() ).fetchone()
monitors_raw = conn.execute( monitors_raw = conn.execute(
"SELECT * FROM trade_order_monitors WHERE status='active' ORDER BY id DESC" "SELECT * FROM trade_order_monitors WHERE status='active' ORDER BY id DESC"
).fetchall() ).fetchall()
mode = get_trading_mode(get_setting)
roll_ctx = _build_roll_context(conn) roll_ctx = _build_roll_context(conn)
roll_groups = conn.execute( roll_groups = conn.execute(
"""SELECT g.*, m.symbol_name, m.lots AS mon_lots, m.entry_price AS mon_entry, """SELECT g.*, m.symbol_name, m.lots AS mon_lots, m.entry_price AS mon_entry,
+13
View File
@@ -221,6 +221,19 @@
if (btnRollE) btnRollE.hidden = true; if (btnRollE) btnRollE.hidden = true;
return; return;
} }
if (rollMonitorSel) {
var opt = rollMonitorSel.options[rollMonitorSel.selectedIndex];
if (opt && opt.getAttribute('data-eligible') === '0') {
showPreview(
rollPrev,
opt.getAttribute('data-block') || '当前监控不可滚仓',
false,
false
);
if (btnRollE) btnRollE.hidden = true;
return;
}
}
btnRollP.disabled = true; btnRollP.disabled = true;
jsonPost('/api/strategy/roll/preview', rollPayload).then(function (d) { jsonPost('/api/strategy/roll/preview', rollPayload).then(function (d) {
if (!d.ok) { if (!d.ok) {
+6 -2
View File
@@ -106,6 +106,10 @@
<p class="hint text-muted">当前为「{{ sizing_mode_label }}」模式,滚仓不可用。请在系统设置切换为<strong>固定金额</strong></p> <p class="hint text-muted">当前为「{{ sizing_mode_label }}」模式,滚仓不可用。请在系统设置切换为<strong>固定金额</strong></p>
{% endif %} {% endif %}
{% if monitors %} {% 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> <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"> <form id="roll-form" class="form-compact">
<div class="form-line line-2"> <div class="form-line line-2">
@@ -114,8 +118,7 @@
<option value="{{ m.id }}" <option value="{{ m.id }}"
data-direction="{{ m.direction }}" data-direction="{{ m.direction }}"
data-eligible="{{ '1' if m.roll_eligible else '0' }}" data-eligible="{{ '1' if m.roll_eligible else '0' }}"
data-block="{{ m.roll_block_reason or '' }}" 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.symbol_name or m.symbol }} {{ m.symbol }} · {{ '多' if m.direction == 'long' else '空' }} #{{ m.id }}
· {{ m.lots }}手 · SL {{ m.stop_loss or '—' }} · {{ m.lots }}手 · SL {{ m.stop_loss or '—' }}
{% if not m.roll_eligible %} · {{ m.roll_block_reason }}{% endif %} {% if not m.roll_eligible %} · {{ m.roll_block_reason }}{% endif %}
@@ -145,6 +148,7 @@
</form> </form>
{% else %} {% else %}
<p class="empty-hint">暂无可用持仓监控</p> <p class="empty-hint">暂无可用持仓监控</p>
<p class="hint text-muted" style="font-size:.78rem">若「委托与持仓」中已有持仓,请先在持仓页连接 CTP 并刷新,再回到本页。</p>
<ol class="strategy-steps"> <ol class="strategy-steps">
<li>打开 <a href="{{ url_for('positions') }}">持仓监控</a>,连接 CTP</li> <li>打开 <a href="{{ url_for('positions') }}">持仓监控</a>,连接 CTP</li>
<li>系统设置为<strong>固定金额</strong>,在「期货下单」开仓(勿开移动保本)</li> <li>系统设置为<strong>固定金额</strong>,在「期货下单」开仓(勿开移动保本)</li>