Add trailing BE to SL/TP dialog and speed up position refresh.
Use modal for monitor upsert with trailing checkbox, refresh CTP tick every second, and push full snapshot after orders. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+19
-7
@@ -1045,11 +1045,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
tp = float(mon["take_profit"]) if mon and mon.get("take_profit") is not None else None
|
tp = float(mon["take_profit"]) if mon and mon.get("take_profit") is not None else None
|
||||||
holding = _holding_duration(open_time, now_iso) if open_time else ""
|
holding = _holding_duration(open_time, now_iso) if open_time else ""
|
||||||
|
|
||||||
if (mark is None or float(mark or 0) <= 0) and not fast and ctp_status(mode).get("connected"):
|
if ctp_status(mode).get("connected"):
|
||||||
live_mark = ctp_get_tick_price(mode, sym)
|
live_mark = ctp_get_tick_price(mode, sym)
|
||||||
if live_mark and live_mark > 0:
|
if live_mark and live_mark > 0:
|
||||||
mark = live_mark
|
mark = live_mark
|
||||||
if (mark is None or float(mark or 0) <= 0) and not fast and codes:
|
elif (mark is None or float(mark or 0) <= 0) and not fast and codes:
|
||||||
mark = fetch_price(
|
mark = fetch_price(
|
||||||
sym,
|
sym,
|
||||||
codes.get("market_code", ""),
|
codes.get("market_code", ""),
|
||||||
@@ -1780,6 +1780,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
return jsonify({"ok": False, "error": "缺少品种代码"}), 400
|
return jsonify({"ok": False, "error": "缺少品种代码"}), 400
|
||||||
if sl is None and tp is None:
|
if sl is None and tp is None:
|
||||||
return jsonify({"ok": False, "error": "请至少填写止损或止盈"}), 400
|
return jsonify({"ok": False, "error": "请至少填写止损或止盈"}), 400
|
||||||
|
trailing_on = bool(d.get("trailing_be"))
|
||||||
|
if trailing_on and sl is None:
|
||||||
|
return jsonify({"ok": False, "error": "移动保本须填写止损价"}), 400
|
||||||
|
if trailing_on:
|
||||||
|
tp = None
|
||||||
mode = get_trading_mode(get_setting)
|
mode = get_trading_mode(get_setting)
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
try:
|
try:
|
||||||
@@ -1801,7 +1806,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
break
|
break
|
||||||
if not has_pos:
|
if not has_pos:
|
||||||
return jsonify({"ok": False, "error": "未找到对应持仓"}), 400
|
return jsonify({"ok": False, "error": "未找到对应持仓"}), 400
|
||||||
trailing_be = 1 if d.get("trailing_be") else (
|
trailing_be = 1 if trailing_on else (
|
||||||
int(mon.get("trailing_be") or 0) if mon else 0
|
int(mon.get("trailing_be") or 0) if mon else 0
|
||||||
)
|
)
|
||||||
mid = _upsert_open_monitor(
|
mid = _upsert_open_monitor(
|
||||||
@@ -1814,8 +1819,15 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
tp=tp,
|
tp=tp,
|
||||||
trailing_be=trailing_be,
|
trailing_be=trailing_be,
|
||||||
)
|
)
|
||||||
|
if trailing_on and sl is not None:
|
||||||
|
conn.execute(
|
||||||
|
"""UPDATE trade_order_monitors SET
|
||||||
|
take_profit=NULL, initial_stop_loss=?, trailing_r_locked=0
|
||||||
|
WHERE id=?""",
|
||||||
|
(sl, mid),
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
_push_position_snapshot_async()
|
_push_position_snapshot_async(fast=False)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"monitor_id": mid,
|
"monitor_id": mid,
|
||||||
@@ -2419,7 +2431,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
f"{trading_mode_label(get_setting)} {offset} {sym} {direction} {lots}手 @{price}"
|
f"{trading_mode_label(get_setting)} {offset} {sym} {direction} {lots}手 @{price}"
|
||||||
)
|
)
|
||||||
conn.close()
|
conn.close()
|
||||||
_push_position_snapshot_async()
|
_push_position_snapshot_async(fast=False)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"result": result,
|
"result": result,
|
||||||
@@ -3196,8 +3208,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
|
|
||||||
start_position_worker(
|
start_position_worker(
|
||||||
refresh_fn=_position_worker_refresh,
|
refresh_fn=_position_worker_refresh,
|
||||||
interval=2,
|
interval=1,
|
||||||
idle_interval=5,
|
idle_interval=3,
|
||||||
)
|
)
|
||||||
_bootstrap_trading_runtime()
|
_bootstrap_trading_runtime()
|
||||||
start_ctp_fee_worker(
|
start_ctp_fee_worker(
|
||||||
|
|||||||
@@ -116,6 +116,13 @@
|
|||||||
.pos-order-btn:disabled,.pos-order-btn.pos-order-done{opacity:.55;cursor:default;border-color:var(--table-border);background:var(--card-inner);color:var(--text-muted)}
|
.pos-order-btn:disabled,.pos-order-btn.pos-order-done{opacity:.55;cursor:default;border-color:var(--table-border);background:var(--card-inner);color:var(--text-muted)}
|
||||||
.pos-order-btn:disabled:not(.pos-order-done){cursor:wait}
|
.pos-order-btn:disabled:not(.pos-order-done){cursor:wait}
|
||||||
|
|
||||||
|
.sl-tp-modal{max-width:420px;width:100%}
|
||||||
|
.sl-tp-modal-fields{display:flex;flex-direction:column;gap:.75rem;margin-bottom:1rem}
|
||||||
|
.sl-tp-modal-fields .trade-field{margin:0}
|
||||||
|
.sl-tp-modal-trailing{margin-top:.15rem}
|
||||||
|
.sl-tp-modal-actions{display:flex;gap:.5rem;justify-content:flex-end}
|
||||||
|
.sl-tp-modal-actions .btn-secondary,.sl-tp-modal-actions .btn-primary{width:auto;min-width:5rem;padding:.45rem 1rem;font-size:.85rem}
|
||||||
|
|
||||||
@media (min-width:768px) and (max-width:1100px){
|
@media (min-width:768px) and (max-width:1100px){
|
||||||
.trade-split .card{min-height:420px}
|
.trade-split .card{min-height:420px}
|
||||||
.trade-form-line.line-3{grid-template-columns:1fr 1fr}
|
.trade-form-line.line-3{grid-template-columns:1fr 1fr}
|
||||||
|
|||||||
+93
-14
@@ -1061,16 +1061,18 @@
|
|||||||
var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空');
|
var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空');
|
||||||
var openT = (row.open_time || '').replace('T', ' ').slice(0, 16);
|
var openT = (row.open_time || '').replace('T', ' ').slice(0, 16);
|
||||||
var closeAllowed = row.close_allowed !== false && isTradingSession;
|
var closeAllowed = row.close_allowed !== false && isTradingSession;
|
||||||
var slTpBtn = (!row.stop_loss && !row.take_profit && row.can_close) ?
|
var slTpBtn = (!row.stop_loss && !row.take_profit && !row.trailing_be && row.can_close) ?
|
||||||
'<button type="button" class="pos-dismiss-btn pos-sl-btn" data-sl-tp="' +
|
'<button type="button" class="pos-dismiss-btn pos-sl-btn" data-sl-tp="' +
|
||||||
encodeURIComponent(JSON.stringify({
|
encodeURIComponent(JSON.stringify({
|
||||||
symbol_code: row.symbol_code, direction: row.direction,
|
symbol_code: row.symbol_code, direction: row.direction,
|
||||||
lots: row.lots, entry_price: row.entry_price, monitor_id: row.monitor_id || null
|
lots: row.lots, entry_price: row.entry_price, monitor_id: row.monitor_id || null,
|
||||||
|
trailing_be: !!row.trailing_be
|
||||||
})) + '">设置止盈止损</button>' : '';
|
})) + '">设置止盈止损</button>' : '';
|
||||||
var editPayload = encodeURIComponent(JSON.stringify({
|
var editPayload = encodeURIComponent(JSON.stringify({
|
||||||
symbol_code: row.symbol_code, direction: row.direction,
|
symbol_code: row.symbol_code, direction: row.direction,
|
||||||
lots: row.lots, entry_price: row.entry_price, monitor_id: row.monitor_id || null,
|
lots: row.lots, entry_price: row.entry_price, monitor_id: row.monitor_id || null,
|
||||||
stop_loss: row.stop_loss, take_profit: row.take_profit
|
stop_loss: row.stop_loss, take_profit: row.take_profit,
|
||||||
|
trailing_be: !!row.trailing_be
|
||||||
}));
|
}));
|
||||||
var entrustBtn = row.can_close ?
|
var entrustBtn = row.can_close ?
|
||||||
'<button type="button" class="pos-order-btn pos-entrust-btn" data-edit-sl-tp="' + editPayload + '">委托</button>' : '';
|
'<button type="button" class="pos-order-btn pos-entrust-btn" data-edit-sl-tp="' + editPayload + '">委托</button>' : '';
|
||||||
@@ -1172,16 +1174,66 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function promptStopTakeProfit(payload, btn, btnLabel) {
|
var slTpModalState = { payload: null, btn: null, btnLabel: '设置止盈止损' };
|
||||||
btnLabel = btnLabel || '设置止盈止损';
|
|
||||||
var slDefault = payload.stop_loss != null && payload.stop_loss !== '' ? String(payload.stop_loss) : '';
|
function syncSlTpModalTrailingUi() {
|
||||||
var tpDefault = payload.take_profit != null && payload.take_profit !== '' ? String(payload.take_profit) : '';
|
var trailingEl = document.getElementById('sl-tp-modal-trailing');
|
||||||
var slRaw = prompt('止损价(可留空)', slDefault);
|
var tpWrap = document.getElementById('sl-tp-modal-tp-wrap');
|
||||||
if (slRaw === null) return;
|
var hint = document.getElementById('sl-tp-modal-trailing-hint');
|
||||||
var tpRaw = prompt('止盈价(可留空)', tpDefault);
|
var on = !!(trailingEl && trailingEl.checked);
|
||||||
if (tpRaw === null) return;
|
if (tpWrap) tpWrap.hidden = on;
|
||||||
var sl = slRaw.trim() ? parseFloat(slRaw) : null;
|
if (hint) hint.hidden = !on;
|
||||||
var tp = tpRaw.trim() ? parseFloat(tpRaw) : null;
|
if (on) {
|
||||||
|
var tpInput = document.getElementById('sl-tp-modal-tp');
|
||||||
|
if (tpInput) tpInput.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSlTpModal() {
|
||||||
|
var mask = document.getElementById('sl-tp-modal');
|
||||||
|
if (mask) mask.classList.remove('show');
|
||||||
|
slTpModalState.payload = null;
|
||||||
|
slTpModalState.btn = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openSlTpModal(payload, btn, btnLabel) {
|
||||||
|
var mask = document.getElementById('sl-tp-modal');
|
||||||
|
var title = document.getElementById('sl-tp-modal-title');
|
||||||
|
var slInput = document.getElementById('sl-tp-modal-sl');
|
||||||
|
var tpInput = document.getElementById('sl-tp-modal-tp');
|
||||||
|
var trailingEl = document.getElementById('sl-tp-modal-trailing');
|
||||||
|
if (!mask || !slInput) return;
|
||||||
|
slTpModalState.payload = payload;
|
||||||
|
slTpModalState.btn = btn || null;
|
||||||
|
slTpModalState.btnLabel = btnLabel || '设置止盈止损';
|
||||||
|
if (title) title.textContent = slTpModalState.btnLabel;
|
||||||
|
slInput.value = payload.stop_loss != null && payload.stop_loss !== '' ? String(payload.stop_loss) : '';
|
||||||
|
if (tpInput) {
|
||||||
|
tpInput.value = payload.take_profit != null && payload.take_profit !== '' ? String(payload.take_profit) : '';
|
||||||
|
}
|
||||||
|
if (trailingEl) trailingEl.checked = !!payload.trailing_be;
|
||||||
|
syncSlTpModalTrailingUi();
|
||||||
|
mask.classList.add('show');
|
||||||
|
slInput.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSlTpModal() {
|
||||||
|
var payload = slTpModalState.payload;
|
||||||
|
if (!payload) return;
|
||||||
|
var btn = slTpModalState.btn;
|
||||||
|
var btnLabel = slTpModalState.btnLabel;
|
||||||
|
var slInput = document.getElementById('sl-tp-modal-sl');
|
||||||
|
var tpInput = document.getElementById('sl-tp-modal-tp');
|
||||||
|
var trailingEl = document.getElementById('sl-tp-modal-trailing');
|
||||||
|
var trailingOn = !!(trailingEl && trailingEl.checked);
|
||||||
|
var slRaw = slInput && slInput.value ? slInput.value.trim() : '';
|
||||||
|
var tpRaw = trailingOn ? '' : (tpInput && tpInput.value ? tpInput.value.trim() : '');
|
||||||
|
var sl = slRaw ? parseFloat(slRaw) : null;
|
||||||
|
var tp = tpRaw ? parseFloat(tpRaw) : null;
|
||||||
|
if (trailingOn && (sl == null || isNaN(sl))) {
|
||||||
|
alert('移动保本须填写止损价');
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (sl == null && tp == null) {
|
if (sl == null && tp == null) {
|
||||||
alert('请至少填写止损或止盈');
|
alert('请至少填写止损或止盈');
|
||||||
return;
|
return;
|
||||||
@@ -1190,6 +1242,8 @@
|
|||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = '保存中…';
|
btn.textContent = '保存中…';
|
||||||
}
|
}
|
||||||
|
var saveBtn = document.getElementById('sl-tp-modal-save');
|
||||||
|
if (saveBtn) saveBtn.disabled = true;
|
||||||
fetch('/api/trading/monitor/upsert', {
|
fetch('/api/trading/monitor/upsert', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -1201,7 +1255,8 @@
|
|||||||
entry_price: payload.entry_price,
|
entry_price: payload.entry_price,
|
||||||
monitor_id: payload.monitor_id || null,
|
monitor_id: payload.monitor_id || null,
|
||||||
stop_loss: sl,
|
stop_loss: sl,
|
||||||
take_profit: tp
|
take_profit: tp,
|
||||||
|
trailing_be: trailingOn
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.then(function (r) {
|
.then(function (r) {
|
||||||
@@ -1214,6 +1269,7 @@
|
|||||||
})
|
})
|
||||||
.then(function (d) {
|
.then(function (d) {
|
||||||
if (!d.ok) throw new Error(d.error || '保存失败');
|
if (!d.ok) throw new Error(d.error || '保存失败');
|
||||||
|
closeSlTpModal();
|
||||||
pollPositions();
|
pollPositions();
|
||||||
})
|
})
|
||||||
.catch(function (e) {
|
.catch(function (e) {
|
||||||
@@ -1224,9 +1280,31 @@
|
|||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = btnLabel;
|
btn.textContent = btnLabel;
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.finally(function () {
|
||||||
|
if (saveBtn) saveBtn.disabled = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function bindSlTpModal() {
|
||||||
|
var mask = document.getElementById('sl-tp-modal');
|
||||||
|
var trailingEl = document.getElementById('sl-tp-modal-trailing');
|
||||||
|
var cancelBtn = document.getElementById('sl-tp-modal-cancel');
|
||||||
|
var saveBtn = document.getElementById('sl-tp-modal-save');
|
||||||
|
if (trailingEl) trailingEl.addEventListener('change', syncSlTpModalTrailingUi);
|
||||||
|
if (cancelBtn) cancelBtn.addEventListener('click', closeSlTpModal);
|
||||||
|
if (saveBtn) saveBtn.addEventListener('click', saveSlTpModal);
|
||||||
|
if (mask) {
|
||||||
|
mask.addEventListener('click', function (e) {
|
||||||
|
if (e.target === mask) closeSlTpModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function promptStopTakeProfit(payload, btn, btnLabel) {
|
||||||
|
openSlTpModal(payload, btn, btnLabel || '设置止盈止损');
|
||||||
|
}
|
||||||
|
|
||||||
function bindSlTpButtons(root) {
|
function bindSlTpButtons(root) {
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
root.querySelectorAll('[data-sl-tp]').forEach(function (btn) {
|
root.querySelectorAll('[data-sl-tp]').forEach(function (btn) {
|
||||||
@@ -1754,6 +1832,7 @@
|
|||||||
}
|
}
|
||||||
pollPositions();
|
pollPositions();
|
||||||
connectPositionStream();
|
connectPositionStream();
|
||||||
|
bindSlTpModal();
|
||||||
initCtpOnLoad();
|
initCtpOnLoad();
|
||||||
connectRecommendStream();
|
connectRecommendStream();
|
||||||
initRecommendSortControls();
|
initRecommendSortControls();
|
||||||
|
|||||||
@@ -269,6 +269,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="sl-tp-modal" class="modal-mask" role="dialog" aria-labelledby="sl-tp-modal-title">
|
||||||
|
<div class="modal-box sl-tp-modal">
|
||||||
|
<h3 id="sl-tp-modal-title">设置止盈止损</h3>
|
||||||
|
<div class="sl-tp-modal-fields">
|
||||||
|
<div class="trade-field">
|
||||||
|
<label class="text-label" for="sl-tp-modal-sl">止损</label>
|
||||||
|
<input type="number" id="sl-tp-modal-sl" step="any" placeholder="必填(移动保本须填止损)">
|
||||||
|
</div>
|
||||||
|
<div class="trade-field" id="sl-tp-modal-tp-wrap">
|
||||||
|
<label class="text-label" for="sl-tp-modal-tp">止盈</label>
|
||||||
|
<input type="number" id="sl-tp-modal-tp" step="any" placeholder="可留空">
|
||||||
|
</div>
|
||||||
|
<label class="trailing-be-toggle sl-tp-modal-trailing">
|
||||||
|
<input type="checkbox" id="sl-tp-modal-trailing">
|
||||||
|
<span>移动保本</span>
|
||||||
|
</label>
|
||||||
|
<p class="hint" id="sl-tp-modal-trailing-hint" hidden>开启后不设固定止盈;达 1R 后止损移至开仓价 ± 缓冲跳</p>
|
||||||
|
</div>
|
||||||
|
<div class="sl-tp-modal-actions">
|
||||||
|
<button type="button" class="btn-secondary" id="sl-tp-modal-cancel">取消</button>
|
||||||
|
<button type="button" class="btn-primary" id="sl-tp-modal-save">保存</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script type="application/json" id="trade-page-data">{{ {
|
<script type="application/json" id="trade-page-data">{{ {
|
||||||
|
|||||||
Reference in New Issue
Block a user