移除一键保本bug
This commit is contained in:
@@ -72,7 +72,6 @@ from key_sl_tp_lib import (
|
|||||||
sl_tp_mode_label,
|
sl_tp_mode_label,
|
||||||
sl_tp_plan_summary_text,
|
sl_tp_plan_summary_text,
|
||||||
)
|
)
|
||||||
from order_manual_breakeven_lib import apply_order_manual_breakeven
|
|
||||||
from key_monitor_lib import (
|
from key_monitor_lib import (
|
||||||
KEY_DIRECTION_WATCH,
|
KEY_DIRECTION_WATCH,
|
||||||
KEY_MONITOR_ALERT_ONLY_TYPES,
|
KEY_MONITOR_ALERT_ONLY_TYPES,
|
||||||
@@ -6120,80 +6119,6 @@ def api_order_cancel_tpsl(order_id):
|
|||||||
return jsonify({"ok": False, "msg": friendly_exchange_error(e)}), 400
|
return jsonify({"ok": False, "msg": friendly_exchange_error(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/order/<int:order_id>/manual_breakeven", methods=["POST"])
|
|
||||||
@login_required
|
|
||||||
def api_order_manual_breakeven(order_id):
|
|
||||||
data = request.get_json(silent=True) or {}
|
|
||||||
try:
|
|
||||||
offset_pct = float(
|
|
||||||
data.get("offset_pct", os.getenv("MANUAL_BREAKEVEN_OFFSET_PCT", "0.2"))
|
|
||||||
)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return jsonify({"ok": False, "msg": "offset_pct 无效"}), 400
|
|
||||||
if offset_pct < 0 or offset_pct > 10:
|
|
||||||
return jsonify({"ok": False, "msg": "偏移%须在 0~10 之间"}), 400
|
|
||||||
conn = get_db()
|
|
||||||
row = conn.execute(
|
|
||||||
"SELECT * FROM order_monitors WHERE id=? AND status='active'",
|
|
||||||
(order_id,),
|
|
||||||
).fetchone()
|
|
||||||
if not row:
|
|
||||||
conn.close()
|
|
||||||
return jsonify({"ok": False, "msg": "持仓不存在或已结束"}), 404
|
|
||||||
ok, err, new_sl = apply_order_manual_breakeven(
|
|
||||||
row,
|
|
||||||
offset_pct,
|
|
||||||
calc_stop_fn=calc_trend_manual_breakeven_stop,
|
|
||||||
round_price_fn=round_price_to_exchange,
|
|
||||||
resolve_ex_sym_fn=resolve_monitor_exchange_symbol,
|
|
||||||
get_position_fn=get_live_position_contracts,
|
|
||||||
replace_tpsl_fn=replace_active_monitor_tpsl_on_exchange,
|
|
||||||
)
|
|
||||||
if not ok:
|
|
||||||
conn.close()
|
|
||||||
return jsonify({"ok": False, "msg": err or "保本失败"}), 400
|
|
||||||
conn.execute(
|
|
||||||
"UPDATE order_monitors SET stop_loss=?, breakeven_armed=1, breakeven_price=? WHERE id=?",
|
|
||||||
(new_sl, new_sl, order_id),
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
sym = row["symbol"]
|
|
||||||
direction = row["direction"]
|
|
||||||
ex_sym = resolve_monitor_exchange_symbol(row)
|
|
||||||
take_profit = float(row["take_profit"] or 0)
|
|
||||||
slots = fetch_exchange_tpsl_slots(
|
|
||||||
ex_sym, direction, plan_sl=new_sl, plan_tp=take_profit
|
|
||||||
)
|
|
||||||
conn.close()
|
|
||||||
try:
|
|
||||||
entry = float(row["trigger_price"] or 0)
|
|
||||||
send_wechat_msg(
|
|
||||||
"\n".join(
|
|
||||||
[
|
|
||||||
f"# ✅ {sym} 一键保本",
|
|
||||||
f"**账户:{_wechat_account_label()}**",
|
|
||||||
f"- 方向:{'做多' if direction == 'long' else '做空'}",
|
|
||||||
f"- 成交价:{format_price_for_symbol(sym, entry)}",
|
|
||||||
f"- 偏移:{offset_pct}%(相对成交价)",
|
|
||||||
f"- 新止损:{format_price_for_symbol(sym, new_sl)}",
|
|
||||||
"- 交易所:已更新止盈止损",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return jsonify(
|
|
||||||
{
|
|
||||||
"ok": True,
|
|
||||||
"msg": "已一键保本",
|
|
||||||
"stop_loss": new_sl,
|
|
||||||
"stop_loss_display": format_price_for_symbol(sym, new_sl),
|
|
||||||
"breakeven_armed": True,
|
|
||||||
"exchange_tpsl": slots,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/order/<int:order_id>/place_tpsl", methods=["POST"])
|
@app.route("/api/order/<int:order_id>/place_tpsl", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def api_order_place_tpsl(order_id):
|
def api_order_place_tpsl(order_id):
|
||||||
|
|||||||
@@ -194,13 +194,6 @@
|
|||||||
.pos-ex-order-main{flex:1;min-width:0;line-height:1.35}
|
.pos-ex-order-main{flex:1;min-width:0;line-height:1.35}
|
||||||
.pos-ex-cancel-btn{padding:3px 10px;background:#3a3048;color:#d4b8ff;border:none;border-radius:6px;font-size:.74rem;cursor:pointer;flex-shrink:0}
|
.pos-ex-cancel-btn{padding:3px 10px;background:#3a3048;color:#d4b8ff;border:none;border-radius:6px;font-size:.74rem;cursor:pointer;flex-shrink:0}
|
||||||
.pos-ex-cancel-btn:disabled{opacity:.4;cursor:not-allowed}
|
.pos-ex-cancel-btn:disabled{opacity:.4;cursor:not-allowed}
|
||||||
.pos-be-global{font-size:.76rem;font-weight:400;color:#8892b0;margin-left:10px;display:inline-flex;align-items:center;gap:4px}
|
|
||||||
.pos-be-global input{width:52px;padding:3px 6px;background:#0d1119;border:1px solid #3a4460;border-radius:6px;color:#e8ecf5;font-size:.78rem}
|
|
||||||
.pos-be-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px;padding:8px 10px;background:#1a2030;border-radius:8px;border:1px solid #2a3348}
|
|
||||||
.pos-be-label{display:inline-flex;align-items:center;gap:6px;font-size:.78rem;color:#b8c4e8;cursor:pointer;user-select:none}
|
|
||||||
.pos-be-label input{width:15px;height:15px;accent-color:#6eb5ff}
|
|
||||||
.pos-be-offset{width:52px;padding:4px 6px;background:#0d1119;border:1px solid #3a4460;border-radius:6px;color:#e8ecf5;font-size:.78rem}
|
|
||||||
.pos-be-status{font-size:.74rem;color:#6ab88a}
|
|
||||||
.tpsl-modal-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9000;align-items:center;justify-content:center;padding:16px}
|
.tpsl-modal-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9000;align-items:center;justify-content:center;padding:16px}
|
||||||
.tpsl-modal-backdrop.open{display:flex}
|
.tpsl-modal-backdrop.open{display:flex}
|
||||||
.tpsl-modal{background:#1a2030;border:1px solid #3a4a66;border-radius:12px;padding:16px 18px;width:min(440px,100%);max-height:90vh;overflow:auto}
|
.tpsl-modal{background:#1a2030;border:1px solid #3a4a66;border-radius:12px;padding:16px 18px;width:min(440px,100%);max-height:90vh;overflow:auto}
|
||||||
@@ -471,11 +464,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 style="margin-bottom:8px;display:flex;align-items:center;flex-wrap:wrap;gap:6px">实时持仓
|
<h2 style="margin-bottom:8px">实时持仓</h2>
|
||||||
<span class="pos-be-global">一键保本默认偏移
|
|
||||||
<input type="number" id="manual-be-offset-global" min="0" max="10" step="0.01" value="0.2" title="相对成交价,多仓为+、空仓为-">%
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<div class="panel-scroll pos-list pos-list-live">
|
<div class="panel-scroll pos-list pos-list-live">
|
||||||
{% for o in order %}
|
{% for o in order %}
|
||||||
<div class="pos-card" id="order-row-{{ o.id }}"
|
<div class="pos-card" id="order-row-{{ o.id }}"
|
||||||
@@ -503,15 +492,6 @@
|
|||||||
{% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
|
{% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pos-be-row">
|
|
||||||
<label class="pos-be-label" title="将止损移至成交价±偏移%,并同步交易所止盈止损">
|
|
||||||
<input type="checkbox" class="pos-be-switch" data-order-id="{{ o.id }}"
|
|
||||||
{% if o.breakeven_armed %}checked data-armed="1"{% endif %}>
|
|
||||||
一键保本
|
|
||||||
</label>
|
|
||||||
<input type="number" class="pos-be-offset" min="0" max="10" step="0.01" value="0.2" title="相对成交价偏移%">%
|
|
||||||
<span class="pos-be-status" id="pos-be-status-{{ o.id }}">{% if o.breakeven_armed %}已保本{% endif %}</span>
|
|
||||||
</div>
|
|
||||||
<div class="pos-grid">
|
<div class="pos-grid">
|
||||||
<div class="pos-cell">
|
<div class="pos-cell">
|
||||||
<span class="pos-label">成交价</span>
|
<span class="pos-label">成交价</span>
|
||||||
@@ -1753,96 +1733,6 @@ function submitTpslEntrust(){
|
|||||||
post();
|
post();
|
||||||
}).catch(()=>alert('无法校验盈亏比'));
|
}).catch(()=>alert('无法校验盈亏比'));
|
||||||
}
|
}
|
||||||
const MANUAL_BE_OFFSET_KEY = 'manualBreakevenOffsetPct';
|
|
||||||
function getManualBeOffsetPct(){
|
|
||||||
const g = document.getElementById('manual-be-offset-global');
|
|
||||||
if(g && g.value !== ''){
|
|
||||||
const n = parseFloat(g.value);
|
|
||||||
if(Number.isFinite(n)) return n;
|
|
||||||
}
|
|
||||||
const saved = localStorage.getItem(MANUAL_BE_OFFSET_KEY);
|
|
||||||
const n = saved ? parseFloat(saved) : 0.2;
|
|
||||||
return Number.isFinite(n) ? n : 0.2;
|
|
||||||
}
|
|
||||||
function initManualBreakevenUi(){
|
|
||||||
const g = document.getElementById('manual-be-offset-global');
|
|
||||||
if(g){
|
|
||||||
const saved = localStorage.getItem(MANUAL_BE_OFFSET_KEY);
|
|
||||||
if(saved) g.value = saved;
|
|
||||||
g.addEventListener('change', ()=>{
|
|
||||||
localStorage.setItem(MANUAL_BE_OFFSET_KEY, g.value);
|
|
||||||
document.querySelectorAll('.pos-be-offset').forEach(inp=>{
|
|
||||||
if(!inp.dataset.touched) inp.value = g.value;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
document.querySelectorAll('.pos-be-offset').forEach(inp=>{
|
|
||||||
if(!inp.dataset.touched){
|
|
||||||
inp.value = getManualBeOffsetPct();
|
|
||||||
}
|
|
||||||
inp.addEventListener('input', ()=>{ inp.dataset.touched = '1'; });
|
|
||||||
});
|
|
||||||
document.querySelectorAll('.pos-be-switch').forEach(sw=>{
|
|
||||||
if(sw.dataset.bound) return;
|
|
||||||
sw.dataset.bound = '1';
|
|
||||||
sw.addEventListener('change', ()=>{
|
|
||||||
const orderId = sw.getAttribute('data-order-id');
|
|
||||||
const card = sw.closest('.pos-card');
|
|
||||||
const offInp = card && card.querySelector('.pos-be-offset');
|
|
||||||
if(!sw.checked){
|
|
||||||
if(sw.dataset.armed === '1') sw.checked = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
applyManualBreakeven(orderId, sw, offInp, card);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function updateOrderCardSlDisplay(card, slDisplay){
|
|
||||||
if(!card || !slDisplay) return;
|
|
||||||
card.setAttribute('data-plan-sl', slDisplay);
|
|
||||||
const cells = card.querySelectorAll('.pos-grid .pos-cell');
|
|
||||||
if(cells[1]){
|
|
||||||
const val = cells[1].querySelector('.pos-value');
|
|
||||||
if(val) val.textContent = slDisplay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function applyManualBreakeven(orderId, sw, offInp, card){
|
|
||||||
const offset = offInp ? parseFloat(offInp.value) : getManualBeOffsetPct();
|
|
||||||
if(!Number.isFinite(offset) || offset < 0 || offset > 10){
|
|
||||||
alert('偏移%须在 0~10 之间');
|
|
||||||
sw.checked = sw.dataset.armed === '1';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dirHint = card && card.getAttribute('data-direction') === 'short' ? '下' : '上';
|
|
||||||
if(!confirm(`确认一键保本?止损将移至成交价${dirHint}移 ${offset}%(相对成交价)`)){
|
|
||||||
sw.checked = sw.dataset.armed === '1';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sw.disabled = true;
|
|
||||||
fetch(`/api/order/${orderId}/manual_breakeven`, {
|
|
||||||
method:'POST',
|
|
||||||
headers:{'Content-Type':'application/json'},
|
|
||||||
body: JSON.stringify({ offset_pct: offset })
|
|
||||||
}).then(r=>r.json()).then(data=>{
|
|
||||||
if(!data.ok){
|
|
||||||
alert(data.msg || '保本失败');
|
|
||||||
sw.checked = sw.dataset.armed === '1';
|
|
||||||
sw.disabled = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sw.dataset.armed = '1';
|
|
||||||
sw.checked = true;
|
|
||||||
const st = document.getElementById(`pos-be-status-${orderId}`);
|
|
||||||
if(st) st.textContent = '已保本';
|
|
||||||
if(data.stop_loss_display) updateOrderCardSlDisplay(card, data.stop_loss_display);
|
|
||||||
if(data.exchange_tpsl) paintExchangeTpslRow(orderId, data.exchange_tpsl);
|
|
||||||
else refreshPriceSnapshotConditional();
|
|
||||||
}).catch(()=>{
|
|
||||||
alert('保本请求失败');
|
|
||||||
sw.checked = sw.dataset.armed === '1';
|
|
||||||
sw.disabled = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function cancelExchangeTpsl(orderId, role){
|
function cancelExchangeTpsl(orderId, role){
|
||||||
const label = role === 'sl' ? '止损' : '止盈';
|
const label = role === 'sl' ? '止损' : '止盈';
|
||||||
if(!confirm(`确认撤销交易所${label}委托?(不会平仓)`)) return;
|
if(!confirm(`确认撤销交易所${label}委托?(不会平仓)`)) return;
|
||||||
@@ -2182,7 +2072,6 @@ function refreshPriceSnapshotConditional(){
|
|||||||
}
|
}
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
initManualBreakevenUi();
|
|
||||||
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
|
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ GATE_TPSL_USE_POSITION_ORDER=true
|
|||||||
GATE_TPSL_TRIGGER_EXPIRATION=604800
|
GATE_TPSL_TRIGGER_EXPIRATION=604800
|
||||||
# 触发参考价:0=最新成交 1=标记价 2=指数价(非法值按 0)
|
# 触发参考价:0=最新成交 1=标记价 2=指数价(非法值按 0)
|
||||||
GATE_TPSL_PRICE_TYPE=0
|
GATE_TPSL_PRICE_TYPE=0
|
||||||
|
# 仓位类 TP/SL 相对现价的最小间距(%),避免 Gate 1026「触发价须高于/低于现价」
|
||||||
|
GATE_TPSL_LAST_PRICE_GAP_PCT=0.05
|
||||||
# 页面与浏览器标签展示的交易所名称(多环境区分时可改成例如 Gate·模拟)
|
# 页面与浏览器标签展示的交易所名称(多环境区分时可改成例如 Gate·模拟)
|
||||||
# EXCHANGE_DISPLAY_NAME=Gate.io
|
# EXCHANGE_DISPLAY_NAME=Gate.io
|
||||||
|
|
||||||
|
|||||||
+46
-76
@@ -73,7 +73,6 @@ from key_sl_tp_lib import (
|
|||||||
sl_tp_mode_label,
|
sl_tp_mode_label,
|
||||||
sl_tp_plan_summary_text,
|
sl_tp_plan_summary_text,
|
||||||
)
|
)
|
||||||
from order_manual_breakeven_lib import apply_order_manual_breakeven
|
|
||||||
from key_monitor_lib import (
|
from key_monitor_lib import (
|
||||||
KEY_DIRECTION_WATCH,
|
KEY_DIRECTION_WATCH,
|
||||||
KEY_MONITOR_ALERT_ONLY_TYPES,
|
KEY_MONITOR_ALERT_ONLY_TYPES,
|
||||||
@@ -190,6 +189,8 @@ GATE_TPSL_PRICE_TYPE = int(os.getenv("GATE_TPSL_PRICE_TYPE", "0"))
|
|||||||
if GATE_TPSL_PRICE_TYPE < 0 or GATE_TPSL_PRICE_TYPE > 2:
|
if GATE_TPSL_PRICE_TYPE < 0 or GATE_TPSL_PRICE_TYPE > 2:
|
||||||
GATE_TPSL_PRICE_TYPE = 0
|
GATE_TPSL_PRICE_TYPE = 0
|
||||||
GATE_TPSL_USE_POSITION_ORDER = os.getenv("GATE_TPSL_USE_POSITION_ORDER", "true").lower() in ("1", "true", "yes")
|
GATE_TPSL_USE_POSITION_ORDER = os.getenv("GATE_TPSL_USE_POSITION_ORDER", "true").lower() in ("1", "true", "yes")
|
||||||
|
# 仓位类触发单相对 mark/last 的最小间距(%),避免 Gate 1026 AUTO_TRIGGER_PRICE_*_LAST
|
||||||
|
GATE_TPSL_LAST_PRICE_GAP_PCT = float(os.getenv("GATE_TPSL_LAST_PRICE_GAP_PCT", "0.05"))
|
||||||
# 页面展示的交易所名称(多实例/多环境时可按需区分)
|
# 页面展示的交易所名称(多实例/多环境时可按需区分)
|
||||||
EXCHANGE_DISPLAY_NAME = (os.getenv("EXCHANGE_DISPLAY_NAME") or "Gate.io").strip() or "Gate.io"
|
EXCHANGE_DISPLAY_NAME = (os.getenv("EXCHANGE_DISPLAY_NAME") or "Gate.io").strip() or "Gate.io"
|
||||||
_GATE_DEFAULT_MARGIN_MODE = "cross" if GATE_TD_MODE in ("cross", "cross_margin") else "isolated"
|
_GATE_DEFAULT_MARGIN_MODE = "cross" if GATE_TD_MODE in ("cross", "cross_margin") else "isolated"
|
||||||
@@ -2871,6 +2872,40 @@ def _gate_contracts_amount_for_tpsl(order, fallback_amount):
|
|||||||
return float(fallback_amount)
|
return float(fallback_amount)
|
||||||
|
|
||||||
|
|
||||||
|
def _gate_clamp_tpsl_to_last_price(exchange_symbol, direction, stop_loss, take_profit, *, sl_only=False):
|
||||||
|
"""
|
||||||
|
Gate price_orders 规则:空仓止损/多仓止盈 trigger>last;空仓止盈/多仓止损 trigger<last。
|
||||||
|
计划价可能已穿过现价时,按最小间距自动微调并返回说明。
|
||||||
|
"""
|
||||||
|
ensure_markets_loaded()
|
||||||
|
last = get_price(exchange_symbol)
|
||||||
|
if last is None or float(last) <= 0:
|
||||||
|
return float(stop_loss), float(take_profit), None
|
||||||
|
last = float(last)
|
||||||
|
sl = float(stop_loss)
|
||||||
|
tp = float(take_profit)
|
||||||
|
gap = max(0.0, float(GATE_TPSL_LAST_PRICE_GAP_PCT)) / 100.0
|
||||||
|
if gap <= 0:
|
||||||
|
gap = 0.0005
|
||||||
|
notes = []
|
||||||
|
direction = (direction or "long").strip().lower()
|
||||||
|
if direction == "short":
|
||||||
|
if sl <= last:
|
||||||
|
sl = float(exchange.price_to_precision(exchange_symbol, last * (1 + gap)))
|
||||||
|
notes.append(f"止损触发价须高于现价 {last},已调整为 {sl}")
|
||||||
|
if not sl_only and tp >= last:
|
||||||
|
tp = float(exchange.price_to_precision(exchange_symbol, last * (1 - gap)))
|
||||||
|
notes.append(f"止盈触发价须低于现价 {last},已调整为 {tp}")
|
||||||
|
else:
|
||||||
|
if sl >= last:
|
||||||
|
sl = float(exchange.price_to_precision(exchange_symbol, last * (1 - gap)))
|
||||||
|
notes.append(f"止损触发价须低于现价 {last},已调整为 {sl}")
|
||||||
|
if not sl_only and tp <= last:
|
||||||
|
tp = float(exchange.price_to_precision(exchange_symbol, last * (1 + gap)))
|
||||||
|
notes.append(f"止盈触发价须高于现价 {last},已调整为 {tp}")
|
||||||
|
return sl, tp, (";".join(notes) if notes else None)
|
||||||
|
|
||||||
|
|
||||||
def _gate_place_tp_sl_orders_legacy_conditional(exchange_symbol, direction, contracts_amount, stop_loss, take_profit):
|
def _gate_place_tp_sl_orders_legacy_conditional(exchange_symbol, direction, contracts_amount, stop_loss, take_profit):
|
||||||
"""ccxt 市价减仓条件单(两张单分别带 stopLossPrice / takeProfitPrice),与官方仓位类触发单等价逻辑不同路径。"""
|
"""ccxt 市价减仓条件单(两张单分别带 stopLossPrice / takeProfitPrice),与官方仓位类触发单等价逻辑不同路径。"""
|
||||||
ensure_markets_loaded()
|
ensure_markets_loaded()
|
||||||
@@ -2900,6 +2935,9 @@ def _gate_place_tp_sl_orders_position_price_orders(exchange_symbol, direction, s
|
|||||||
order_type=close-long-position / close-short-position,单向全平 close+size=0;双向需 auto_size。
|
order_type=close-long-position / close-short-position,单向全平 close+size=0;双向需 auto_size。
|
||||||
与 App 内展示的「条件委托」一致,平仓后仍需 cancel_gate_swap_trigger_orders 避免残留。
|
与 App 内展示的「条件委托」一致,平仓后仍需 cancel_gate_swap_trigger_orders 避免残留。
|
||||||
"""
|
"""
|
||||||
|
stop_loss, take_profit, _ = _gate_clamp_tpsl_to_last_price(
|
||||||
|
exchange_symbol, direction, stop_loss, take_profit
|
||||||
|
)
|
||||||
ensure_markets_loaded()
|
ensure_markets_loaded()
|
||||||
market = exchange.market(exchange_symbol)
|
market = exchange.market(exchange_symbol)
|
||||||
if not market.get("swap"):
|
if not market.get("swap"):
|
||||||
@@ -2972,6 +3010,9 @@ def _gate_place_tp_sl_orders(exchange_symbol, direction, contracts_amount, stop_
|
|||||||
|
|
||||||
def _gate_place_stop_loss_only_position(exchange_symbol, direction, stop_loss):
|
def _gate_place_stop_loss_only_position(exchange_symbol, direction, stop_loss):
|
||||||
"""Gate 永续:仅挂仓位类止损触发单(趋势回调用)。"""
|
"""Gate 永续:仅挂仓位类止损触发单(趋势回调用)。"""
|
||||||
|
stop_loss, _, _ = _gate_clamp_tpsl_to_last_price(
|
||||||
|
exchange_symbol, direction, stop_loss, stop_loss, sl_only=True
|
||||||
|
)
|
||||||
ensure_markets_loaded()
|
ensure_markets_loaded()
|
||||||
market = exchange.market(exchange_symbol)
|
market = exchange.market(exchange_symbol)
|
||||||
if not market.get("swap"):
|
if not market.get("swap"):
|
||||||
@@ -3298,6 +3339,9 @@ def replace_active_monitor_tpsl_on_exchange(order_row, stop_loss, take_profit):
|
|||||||
raise RuntimeError(reason or "实盘未就绪")
|
raise RuntimeError(reason or "实盘未就绪")
|
||||||
ex_sym = resolve_monitor_exchange_symbol(order_row)
|
ex_sym = resolve_monitor_exchange_symbol(order_row)
|
||||||
direction = order_row["direction"]
|
direction = order_row["direction"]
|
||||||
|
sl, tp, adjust_note = _gate_clamp_tpsl_to_last_price(
|
||||||
|
ex_sym, direction, float(stop_loss), float(take_profit)
|
||||||
|
)
|
||||||
cancel_gate_swap_trigger_orders(ex_sym)
|
cancel_gate_swap_trigger_orders(ex_sym)
|
||||||
contracts = get_live_position_contracts(ex_sym, direction)
|
contracts = get_live_position_contracts(ex_sym, direction)
|
||||||
if contracts is None or float(contracts) <= 0:
|
if contracts is None or float(contracts) <= 0:
|
||||||
@@ -3310,7 +3354,7 @@ def replace_active_monitor_tpsl_on_exchange(order_row, stop_loss, take_profit):
|
|||||||
amt = 0
|
amt = 0
|
||||||
if amt <= 0:
|
if amt <= 0:
|
||||||
raise ValueError("无法确定平仓数量")
|
raise ValueError("无法确定平仓数量")
|
||||||
_gate_place_tp_sl_orders(ex_sym, direction, amt, float(stop_loss), float(take_profit))
|
_gate_place_tp_sl_orders(ex_sym, direction, amt, sl, tp)
|
||||||
|
|
||||||
|
|
||||||
def extract_trade_price_from_order(order):
|
def extract_trade_price_from_order(order):
|
||||||
@@ -6208,80 +6252,6 @@ def api_order_cancel_tpsl(order_id):
|
|||||||
return jsonify({"ok": False, "msg": friendly_exchange_error(e)}), 400
|
return jsonify({"ok": False, "msg": friendly_exchange_error(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/order/<int:order_id>/manual_breakeven", methods=["POST"])
|
|
||||||
@login_required
|
|
||||||
def api_order_manual_breakeven(order_id):
|
|
||||||
data = request.get_json(silent=True) or {}
|
|
||||||
try:
|
|
||||||
offset_pct = float(
|
|
||||||
data.get("offset_pct", os.getenv("MANUAL_BREAKEVEN_OFFSET_PCT", "0.2"))
|
|
||||||
)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return jsonify({"ok": False, "msg": "offset_pct 无效"}), 400
|
|
||||||
if offset_pct < 0 or offset_pct > 10:
|
|
||||||
return jsonify({"ok": False, "msg": "偏移%须在 0~10 之间"}), 400
|
|
||||||
conn = get_db()
|
|
||||||
row = conn.execute(
|
|
||||||
"SELECT * FROM order_monitors WHERE id=? AND status='active'",
|
|
||||||
(order_id,),
|
|
||||||
).fetchone()
|
|
||||||
if not row:
|
|
||||||
conn.close()
|
|
||||||
return jsonify({"ok": False, "msg": "持仓不存在或已结束"}), 404
|
|
||||||
ok, err, new_sl = apply_order_manual_breakeven(
|
|
||||||
row,
|
|
||||||
offset_pct,
|
|
||||||
calc_stop_fn=calc_trend_manual_breakeven_stop,
|
|
||||||
round_price_fn=round_price_to_exchange,
|
|
||||||
resolve_ex_sym_fn=resolve_monitor_exchange_symbol,
|
|
||||||
get_position_fn=get_live_position_contracts,
|
|
||||||
replace_tpsl_fn=replace_active_monitor_tpsl_on_exchange,
|
|
||||||
)
|
|
||||||
if not ok:
|
|
||||||
conn.close()
|
|
||||||
return jsonify({"ok": False, "msg": err or "保本失败"}), 400
|
|
||||||
conn.execute(
|
|
||||||
"UPDATE order_monitors SET stop_loss=?, breakeven_armed=1, breakeven_price=? WHERE id=?",
|
|
||||||
(new_sl, new_sl, order_id),
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
sym = row["symbol"]
|
|
||||||
direction = row["direction"]
|
|
||||||
ex_sym = resolve_monitor_exchange_symbol(row)
|
|
||||||
take_profit = float(row["take_profit"] or 0)
|
|
||||||
slots = fetch_exchange_tpsl_slots(
|
|
||||||
ex_sym, direction, plan_sl=new_sl, plan_tp=take_profit
|
|
||||||
)
|
|
||||||
conn.close()
|
|
||||||
try:
|
|
||||||
entry = float(row["trigger_price"] or 0)
|
|
||||||
send_wechat_msg(
|
|
||||||
"\n".join(
|
|
||||||
[
|
|
||||||
f"# ✅ {sym} 一键保本",
|
|
||||||
f"**账户:{_wechat_account_label()}**",
|
|
||||||
f"- 方向:{'做多' if direction == 'long' else '做空'}",
|
|
||||||
f"- 成交价:{format_price_for_symbol(sym, entry)}",
|
|
||||||
f"- 偏移:{offset_pct}%(相对成交价)",
|
|
||||||
f"- 新止损:{format_price_for_symbol(sym, new_sl)}",
|
|
||||||
"- 交易所:已更新止盈止损",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return jsonify(
|
|
||||||
{
|
|
||||||
"ok": True,
|
|
||||||
"msg": "已一键保本",
|
|
||||||
"stop_loss": new_sl,
|
|
||||||
"stop_loss_display": format_price_for_symbol(sym, new_sl),
|
|
||||||
"breakeven_armed": True,
|
|
||||||
"exchange_tpsl": slots,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/order/<int:order_id>/place_tpsl", methods=["POST"])
|
@app.route("/api/order/<int:order_id>/place_tpsl", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def api_order_place_tpsl(order_id):
|
def api_order_place_tpsl(order_id):
|
||||||
|
|||||||
@@ -194,13 +194,6 @@
|
|||||||
.pos-ex-order-main{flex:1;min-width:0;line-height:1.35}
|
.pos-ex-order-main{flex:1;min-width:0;line-height:1.35}
|
||||||
.pos-ex-cancel-btn{padding:3px 10px;background:#3a3048;color:#d4b8ff;border:none;border-radius:6px;font-size:.74rem;cursor:pointer;flex-shrink:0}
|
.pos-ex-cancel-btn{padding:3px 10px;background:#3a3048;color:#d4b8ff;border:none;border-radius:6px;font-size:.74rem;cursor:pointer;flex-shrink:0}
|
||||||
.pos-ex-cancel-btn:disabled{opacity:.4;cursor:not-allowed}
|
.pos-ex-cancel-btn:disabled{opacity:.4;cursor:not-allowed}
|
||||||
.pos-be-global{font-size:.76rem;font-weight:400;color:#8892b0;margin-left:10px;display:inline-flex;align-items:center;gap:4px}
|
|
||||||
.pos-be-global input{width:52px;padding:3px 6px;background:#0d1119;border:1px solid #3a4460;border-radius:6px;color:#e8ecf5;font-size:.78rem}
|
|
||||||
.pos-be-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px;padding:8px 10px;background:#1a2030;border-radius:8px;border:1px solid #2a3348}
|
|
||||||
.pos-be-label{display:inline-flex;align-items:center;gap:6px;font-size:.78rem;color:#b8c4e8;cursor:pointer;user-select:none}
|
|
||||||
.pos-be-label input{width:15px;height:15px;accent-color:#6eb5ff}
|
|
||||||
.pos-be-offset{width:52px;padding:4px 6px;background:#0d1119;border:1px solid #3a4460;border-radius:6px;color:#e8ecf5;font-size:.78rem}
|
|
||||||
.pos-be-status{font-size:.74rem;color:#6ab88a}
|
|
||||||
.tpsl-modal-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9000;align-items:center;justify-content:center;padding:16px}
|
.tpsl-modal-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9000;align-items:center;justify-content:center;padding:16px}
|
||||||
.tpsl-modal-backdrop.open{display:flex}
|
.tpsl-modal-backdrop.open{display:flex}
|
||||||
.tpsl-modal{background:#1a2030;border:1px solid #3a4a66;border-radius:12px;padding:16px 18px;width:min(440px,100%);max-height:90vh;overflow:auto}
|
.tpsl-modal{background:#1a2030;border:1px solid #3a4a66;border-radius:12px;padding:16px 18px;width:min(440px,100%);max-height:90vh;overflow:auto}
|
||||||
@@ -471,11 +464,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 style="margin-bottom:8px;display:flex;align-items:center;flex-wrap:wrap;gap:6px">实时持仓
|
<h2 style="margin-bottom:8px">实时持仓</h2>
|
||||||
<span class="pos-be-global">一键保本默认偏移
|
|
||||||
<input type="number" id="manual-be-offset-global" min="0" max="10" step="0.01" value="0.2" title="相对成交价,多仓为+、空仓为-">%
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<div class="panel-scroll pos-list pos-list-live">
|
<div class="panel-scroll pos-list pos-list-live">
|
||||||
{% for o in order %}
|
{% for o in order %}
|
||||||
<div class="pos-card" id="order-row-{{ o.id }}"
|
<div class="pos-card" id="order-row-{{ o.id }}"
|
||||||
@@ -503,15 +492,6 @@
|
|||||||
{% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
|
{% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pos-be-row">
|
|
||||||
<label class="pos-be-label" title="将止损移至成交价±偏移%,并同步交易所止盈止损">
|
|
||||||
<input type="checkbox" class="pos-be-switch" data-order-id="{{ o.id }}"
|
|
||||||
{% if o.breakeven_armed %}checked data-armed="1"{% endif %}>
|
|
||||||
一键保本
|
|
||||||
</label>
|
|
||||||
<input type="number" class="pos-be-offset" min="0" max="10" step="0.01" value="0.2" title="相对成交价偏移%">%
|
|
||||||
<span class="pos-be-status" id="pos-be-status-{{ o.id }}">{% if o.breakeven_armed %}已保本{% endif %}</span>
|
|
||||||
</div>
|
|
||||||
<div class="pos-grid">
|
<div class="pos-grid">
|
||||||
<div class="pos-cell">
|
<div class="pos-cell">
|
||||||
<span class="pos-label">成交价</span>
|
<span class="pos-label">成交价</span>
|
||||||
@@ -1753,96 +1733,6 @@ function submitTpslEntrust(){
|
|||||||
post();
|
post();
|
||||||
}).catch(()=>alert('无法校验盈亏比'));
|
}).catch(()=>alert('无法校验盈亏比'));
|
||||||
}
|
}
|
||||||
const MANUAL_BE_OFFSET_KEY = 'manualBreakevenOffsetPct';
|
|
||||||
function getManualBeOffsetPct(){
|
|
||||||
const g = document.getElementById('manual-be-offset-global');
|
|
||||||
if(g && g.value !== ''){
|
|
||||||
const n = parseFloat(g.value);
|
|
||||||
if(Number.isFinite(n)) return n;
|
|
||||||
}
|
|
||||||
const saved = localStorage.getItem(MANUAL_BE_OFFSET_KEY);
|
|
||||||
const n = saved ? parseFloat(saved) : 0.2;
|
|
||||||
return Number.isFinite(n) ? n : 0.2;
|
|
||||||
}
|
|
||||||
function initManualBreakevenUi(){
|
|
||||||
const g = document.getElementById('manual-be-offset-global');
|
|
||||||
if(g){
|
|
||||||
const saved = localStorage.getItem(MANUAL_BE_OFFSET_KEY);
|
|
||||||
if(saved) g.value = saved;
|
|
||||||
g.addEventListener('change', ()=>{
|
|
||||||
localStorage.setItem(MANUAL_BE_OFFSET_KEY, g.value);
|
|
||||||
document.querySelectorAll('.pos-be-offset').forEach(inp=>{
|
|
||||||
if(!inp.dataset.touched) inp.value = g.value;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
document.querySelectorAll('.pos-be-offset').forEach(inp=>{
|
|
||||||
if(!inp.dataset.touched){
|
|
||||||
inp.value = getManualBeOffsetPct();
|
|
||||||
}
|
|
||||||
inp.addEventListener('input', ()=>{ inp.dataset.touched = '1'; });
|
|
||||||
});
|
|
||||||
document.querySelectorAll('.pos-be-switch').forEach(sw=>{
|
|
||||||
if(sw.dataset.bound) return;
|
|
||||||
sw.dataset.bound = '1';
|
|
||||||
sw.addEventListener('change', ()=>{
|
|
||||||
const orderId = sw.getAttribute('data-order-id');
|
|
||||||
const card = sw.closest('.pos-card');
|
|
||||||
const offInp = card && card.querySelector('.pos-be-offset');
|
|
||||||
if(!sw.checked){
|
|
||||||
if(sw.dataset.armed === '1') sw.checked = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
applyManualBreakeven(orderId, sw, offInp, card);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function updateOrderCardSlDisplay(card, slDisplay){
|
|
||||||
if(!card || !slDisplay) return;
|
|
||||||
card.setAttribute('data-plan-sl', slDisplay);
|
|
||||||
const cells = card.querySelectorAll('.pos-grid .pos-cell');
|
|
||||||
if(cells[1]){
|
|
||||||
const val = cells[1].querySelector('.pos-value');
|
|
||||||
if(val) val.textContent = slDisplay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function applyManualBreakeven(orderId, sw, offInp, card){
|
|
||||||
const offset = offInp ? parseFloat(offInp.value) : getManualBeOffsetPct();
|
|
||||||
if(!Number.isFinite(offset) || offset < 0 || offset > 10){
|
|
||||||
alert('偏移%须在 0~10 之间');
|
|
||||||
sw.checked = sw.dataset.armed === '1';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dirHint = card && card.getAttribute('data-direction') === 'short' ? '下' : '上';
|
|
||||||
if(!confirm(`确认一键保本?止损将移至成交价${dirHint}移 ${offset}%(相对成交价)`)){
|
|
||||||
sw.checked = sw.dataset.armed === '1';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sw.disabled = true;
|
|
||||||
fetch(`/api/order/${orderId}/manual_breakeven`, {
|
|
||||||
method:'POST',
|
|
||||||
headers:{'Content-Type':'application/json'},
|
|
||||||
body: JSON.stringify({ offset_pct: offset })
|
|
||||||
}).then(r=>r.json()).then(data=>{
|
|
||||||
if(!data.ok){
|
|
||||||
alert(data.msg || '保本失败');
|
|
||||||
sw.checked = sw.dataset.armed === '1';
|
|
||||||
sw.disabled = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sw.dataset.armed = '1';
|
|
||||||
sw.checked = true;
|
|
||||||
const st = document.getElementById(`pos-be-status-${orderId}`);
|
|
||||||
if(st) st.textContent = '已保本';
|
|
||||||
if(data.stop_loss_display) updateOrderCardSlDisplay(card, data.stop_loss_display);
|
|
||||||
if(data.exchange_tpsl) paintExchangeTpslRow(orderId, data.exchange_tpsl);
|
|
||||||
else refreshPriceSnapshotConditional();
|
|
||||||
}).catch(()=>{
|
|
||||||
alert('保本请求失败');
|
|
||||||
sw.checked = sw.dataset.armed === '1';
|
|
||||||
sw.disabled = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function cancelExchangeTpsl(orderId, role){
|
function cancelExchangeTpsl(orderId, role){
|
||||||
const label = role === 'sl' ? '止损' : '止盈';
|
const label = role === 'sl' ? '止损' : '止盈';
|
||||||
if(!confirm(`确认撤销交易所${label}委托?(不会平仓)`)) return;
|
if(!confirm(`确认撤销交易所${label}委托?(不会平仓)`)) return;
|
||||||
@@ -2182,7 +2072,6 @@ function refreshPriceSnapshotConditional(){
|
|||||||
}
|
}
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
initManualBreakevenUi();
|
|
||||||
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
|
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ from journal_chart_lib import (
|
|||||||
trade_review_fetch_window,
|
trade_review_fetch_window,
|
||||||
trim_rows_for_trade_review,
|
trim_rows_for_trade_review,
|
||||||
)
|
)
|
||||||
from order_manual_breakeven_lib import apply_order_manual_breakeven
|
|
||||||
from hub_auth import request_allowed as hub_request_allowed
|
from hub_auth import request_allowed as hub_request_allowed
|
||||||
from history_window_lib import (
|
from history_window_lib import (
|
||||||
PRESET_CUSTOM,
|
PRESET_CUSTOM,
|
||||||
@@ -5417,74 +5416,6 @@ def api_account_snapshot():
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/order/<int:order_id>/manual_breakeven", methods=["POST"])
|
|
||||||
@login_required
|
|
||||||
def api_order_manual_breakeven(order_id):
|
|
||||||
data = request.get_json(silent=True) or {}
|
|
||||||
try:
|
|
||||||
offset_pct = float(
|
|
||||||
data.get("offset_pct", os.getenv("MANUAL_BREAKEVEN_OFFSET_PCT", "0.2"))
|
|
||||||
)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return jsonify({"ok": False, "msg": "offset_pct 无效"}), 400
|
|
||||||
if offset_pct < 0 or offset_pct > 10:
|
|
||||||
return jsonify({"ok": False, "msg": "偏移%须在 0~10 之间"}), 400
|
|
||||||
conn = get_db()
|
|
||||||
row = conn.execute(
|
|
||||||
"SELECT * FROM order_monitors WHERE id=? AND status='active'",
|
|
||||||
(order_id,),
|
|
||||||
).fetchone()
|
|
||||||
if not row:
|
|
||||||
conn.close()
|
|
||||||
return jsonify({"ok": False, "msg": "持仓不存在或已结束"}), 404
|
|
||||||
ok, err, new_sl = apply_order_manual_breakeven(
|
|
||||||
row,
|
|
||||||
offset_pct,
|
|
||||||
calc_stop_fn=calc_trend_manual_breakeven_stop,
|
|
||||||
round_price_fn=round_price_to_exchange,
|
|
||||||
resolve_ex_sym_fn=resolve_monitor_exchange_symbol,
|
|
||||||
get_position_fn=get_live_position_contracts,
|
|
||||||
replace_tpsl_fn=replace_active_monitor_tpsl_on_exchange,
|
|
||||||
)
|
|
||||||
if not ok:
|
|
||||||
conn.close()
|
|
||||||
return jsonify({"ok": False, "msg": err or "保本失败"}), 400
|
|
||||||
conn.execute(
|
|
||||||
"UPDATE order_monitors SET stop_loss=?, breakeven_armed=1, breakeven_price=? WHERE id=?",
|
|
||||||
(new_sl, new_sl, order_id),
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
sym = row["symbol"]
|
|
||||||
direction = row["direction"]
|
|
||||||
conn.close()
|
|
||||||
try:
|
|
||||||
entry = float(row["trigger_price"] or 0)
|
|
||||||
send_wechat_msg(
|
|
||||||
"\n".join(
|
|
||||||
[
|
|
||||||
f"# ✅ {sym} 一键保本",
|
|
||||||
f"**账户:{_wechat_account_label()}**",
|
|
||||||
f"- 方向:{'做多' if direction == 'long' else '做空'}",
|
|
||||||
f"- 成交价:{format_price_for_symbol(sym, entry)}",
|
|
||||||
f"- 偏移:{offset_pct}%(相对成交价)",
|
|
||||||
f"- 新止损:{format_price_for_symbol(sym, new_sl)}",
|
|
||||||
"- 交易所:已更新止盈止损",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return jsonify(
|
|
||||||
{
|
|
||||||
"ok": True,
|
|
||||||
"msg": "已一键保本",
|
|
||||||
"stop_loss": new_sl,
|
|
||||||
"stop_loss_display": format_price_for_symbol(sym, new_sl),
|
|
||||||
"breakeven_armed": True,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/price_snapshot")
|
@app.route("/api/price_snapshot")
|
||||||
@login_required
|
@login_required
|
||||||
def api_price_snapshot():
|
def api_price_snapshot():
|
||||||
|
|||||||
@@ -131,13 +131,6 @@
|
|||||||
.plan-cell .val.pnl-neutral,.plan-cell .val .pnl-neutral{color:#cfd3ef}
|
.plan-cell .val.pnl-neutral,.plan-cell .val .pnl-neutral{color:#cfd3ef}
|
||||||
.btn-close-plan{padding:7px 14px;background:#5c1e2a;color:#ffb4b4;border:none;border-radius:8px;cursor:pointer;font-size:.82rem;font-weight:600;text-decoration:none;white-space:nowrap}
|
.btn-close-plan{padding:7px 14px;background:#5c1e2a;color:#ffb4b4;border:none;border-radius:8px;cursor:pointer;font-size:.82rem;font-weight:600;text-decoration:none;white-space:nowrap}
|
||||||
.btn-close-plan:hover{filter:brightness(1.08)}
|
.btn-close-plan:hover{filter:brightness(1.08)}
|
||||||
.pos-be-global{font-size:.74rem;font-weight:400;color:#8892b0;margin-left:8px;display:inline-flex;align-items:center;gap:4px}
|
|
||||||
.pos-be-global input{width:52px;padding:3px 6px;background:#0d1119;border:1px solid #3a4460;border-radius:6px;color:#e8ecf5;font-size:.76rem}
|
|
||||||
.pos-be-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px;padding:8px 10px;background:#1a2030;border-radius:8px;border:1px solid #2a3348}
|
|
||||||
.pos-be-label{display:inline-flex;align-items:center;gap:6px;font-size:.76rem;color:#b8c4e8;cursor:pointer}
|
|
||||||
.pos-be-label input{width:15px;height:15px}
|
|
||||||
.pos-be-offset{width:52px;padding:4px 6px;background:#0d1119;border:1px solid #3a4460;border-radius:6px;color:#e8ecf5;font-size:.76rem}
|
|
||||||
.pos-be-status{font-size:.72rem;color:#6ab88a}
|
|
||||||
.records-card{grid-column:1/-1}
|
.records-card{grid-column:1/-1}
|
||||||
.review-card{grid-column:1/-1}
|
.review-card{grid-column:1/-1}
|
||||||
.review-card-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px;flex-wrap:wrap}
|
.review-card-head{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-bottom:10px;flex-wrap:wrap}
|
||||||
@@ -355,15 +348,11 @@
|
|||||||
<button type="submit">开仓(以损定仓)</button>
|
<button type="submit">开仓(以损定仓)</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="order-live-positions">
|
<div class="order-live-positions">
|
||||||
<h3 style="margin:0 0 2px;font-size:.95rem;color:#b8c4ff;display:flex;align-items:center;flex-wrap:wrap;gap:6px">实时持仓
|
<h3 style="margin:0 0 2px;font-size:.95rem;color:#b8c4ff">实时持仓</h3>
|
||||||
<span class="pos-be-global">一键保本默认偏移
|
|
||||||
<input type="number" id="manual-be-offset-global" min="0" max="10" step="0.01" value="0.2">%
|
|
||||||
</span>
|
|
||||||
</h3>
|
|
||||||
<div class="running-plans-stack">
|
<div class="running-plans-stack">
|
||||||
{% for o in order %}
|
{% for o in order %}
|
||||||
{% set osym = o.exchange_symbol or o.symbol %}
|
{% set osym = o.exchange_symbol or o.symbol %}
|
||||||
<div class="plan-position-card" id="order-row-{{ o.id }}" data-order-id="{{ o.id }}" data-direction="{{ o.direction }}">
|
<div class="plan-position-card">
|
||||||
<div class="plan-card-head">
|
<div class="plan-card-head">
|
||||||
<div class="plan-card-title">
|
<div class="plan-card-title">
|
||||||
<span>{{ osym }}</span>
|
<span>{{ osym }}</span>
|
||||||
@@ -371,15 +360,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<a href="/del_order/{{ o.id }}" class="btn-close-plan" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
|
<a href="/del_order/{{ o.id }}" class="btn-close-plan" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="pos-be-row">
|
|
||||||
<label class="pos-be-label" title="将止损移至成交价±偏移%,并同步交易所止盈止损">
|
|
||||||
<input type="checkbox" class="pos-be-switch" data-order-id="{{ o.id }}"
|
|
||||||
{% if o.breakeven_armed %}checked data-armed="1"{% endif %}>
|
|
||||||
一键保本
|
|
||||||
</label>
|
|
||||||
<input type="number" class="pos-be-offset" min="0" max="10" step="0.01" value="0.2">%
|
|
||||||
<span class="pos-be-status" id="pos-be-status-{{ o.id }}">{% if o.breakeven_armed %}已保本{% endif %}</span>
|
|
||||||
</div>
|
|
||||||
<div class="plan-card-meta">
|
<div class="plan-card-meta">
|
||||||
来源: 下单监控 | 风格: {{ o.trade_style or 'trend' }} | 风险: {% if o.risk_percent is not none %}{{ o.risk_percent }}%{% else %}—{% endif %}≈{{ money_fmt(o.risk_amount) }}U
|
来源: 下单监控 | 风格: {{ o.trade_style or 'trend' }} | 风险: {% if o.risk_percent is not none %}{{ o.risk_percent }}%{% else %}—{% endif %}≈{{ money_fmt(o.risk_amount) }}U
|
||||||
| {% if o.breakeven_enabled %}<span class="accent">移动保本: 开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(osym, o.breakeven_price) }}</span>{% else %}移动保本: 关{% endif %}
|
| {% if o.breakeven_enabled %}<span class="accent">移动保本: 开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(osym, o.breakeven_price) }}</span>{% else %}移动保本: 关{% endif %}
|
||||||
@@ -1653,92 +1633,6 @@ if(_journalFormEl){
|
|||||||
syncJournalEntryReasonOtherUi();
|
syncJournalEntryReasonOtherUi();
|
||||||
}
|
}
|
||||||
refreshOrderDefaults();
|
refreshOrderDefaults();
|
||||||
const MANUAL_BE_OFFSET_KEY = 'manualBreakevenOffsetPct';
|
|
||||||
function getManualBeOffsetPct(){
|
|
||||||
const g = document.getElementById('manual-be-offset-global');
|
|
||||||
if(g && g.value !== ''){
|
|
||||||
const n = parseFloat(g.value);
|
|
||||||
if(Number.isFinite(n)) return n;
|
|
||||||
}
|
|
||||||
const saved = localStorage.getItem(MANUAL_BE_OFFSET_KEY);
|
|
||||||
const n = saved ? parseFloat(saved) : 0.2;
|
|
||||||
return Number.isFinite(n) ? n : 0.2;
|
|
||||||
}
|
|
||||||
function initManualBreakevenUi(){
|
|
||||||
const g = document.getElementById('manual-be-offset-global');
|
|
||||||
if(g){
|
|
||||||
const saved = localStorage.getItem(MANUAL_BE_OFFSET_KEY);
|
|
||||||
if(saved) g.value = saved;
|
|
||||||
g.addEventListener('change', ()=>{
|
|
||||||
localStorage.setItem(MANUAL_BE_OFFSET_KEY, g.value);
|
|
||||||
document.querySelectorAll('.pos-be-offset').forEach(inp=>{
|
|
||||||
if(!inp.dataset.touched) inp.value = g.value;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
document.querySelectorAll('.pos-be-offset').forEach(inp=>{
|
|
||||||
if(!inp.dataset.touched) inp.value = getManualBeOffsetPct();
|
|
||||||
inp.addEventListener('input', ()=>{ inp.dataset.touched = '1'; });
|
|
||||||
});
|
|
||||||
document.querySelectorAll('.pos-be-switch').forEach(sw=>{
|
|
||||||
if(sw.dataset.bound) return;
|
|
||||||
sw.dataset.bound = '1';
|
|
||||||
sw.addEventListener('change', ()=>{
|
|
||||||
const orderId = sw.getAttribute('data-order-id');
|
|
||||||
const card = document.getElementById(`order-row-${orderId}`);
|
|
||||||
const offInp = card && card.querySelector('.pos-be-offset');
|
|
||||||
if(!sw.checked){
|
|
||||||
if(sw.dataset.armed === '1') sw.checked = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
applyManualBreakeven(orderId, sw, offInp, card);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function updateOrderCardSlDisplay(card, slDisplay){
|
|
||||||
if(!card || !slDisplay) return;
|
|
||||||
const cells = card.querySelectorAll('.plan-card-grid .plan-cell');
|
|
||||||
if(cells[1]){
|
|
||||||
const val = cells[1].querySelector('.val');
|
|
||||||
if(val) val.textContent = slDisplay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function applyManualBreakeven(orderId, sw, offInp, card){
|
|
||||||
const offset = offInp ? parseFloat(offInp.value) : getManualBeOffsetPct();
|
|
||||||
if(!Number.isFinite(offset) || offset < 0 || offset > 10){
|
|
||||||
alert('偏移%须在 0~10 之间');
|
|
||||||
sw.checked = sw.dataset.armed === '1';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dirHint = card && card.getAttribute('data-direction') === 'short' ? '下' : '上';
|
|
||||||
if(!confirm(`确认一键保本?止损将移至成交价${dirHint}移 ${offset}%`)){
|
|
||||||
sw.checked = sw.dataset.armed === '1';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sw.disabled = true;
|
|
||||||
fetch(`/api/order/${orderId}/manual_breakeven`, {
|
|
||||||
method:'POST',
|
|
||||||
headers:{'Content-Type':'application/json'},
|
|
||||||
body: JSON.stringify({ offset_pct: offset })
|
|
||||||
}).then(r=>r.json()).then(data=>{
|
|
||||||
if(!data.ok){
|
|
||||||
alert(data.msg || '保本失败');
|
|
||||||
sw.checked = sw.dataset.armed === '1';
|
|
||||||
sw.disabled = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sw.dataset.armed = '1';
|
|
||||||
sw.checked = true;
|
|
||||||
const st = document.getElementById(`pos-be-status-${orderId}`);
|
|
||||||
if(st) st.textContent = '已保本';
|
|
||||||
if(data.stop_loss_display) updateOrderCardSlDisplay(card, data.stop_loss_display);
|
|
||||||
}).catch(()=>{
|
|
||||||
alert('保本请求失败');
|
|
||||||
sw.checked = sw.dataset.armed === '1';
|
|
||||||
sw.disabled = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
initManualBreakevenUi();
|
|
||||||
refreshPriceSnapshot();
|
refreshPriceSnapshot();
|
||||||
setInterval(refreshAccountSnapshot, {{ balance_refresh_seconds * 1000 }});
|
setInterval(refreshAccountSnapshot, {{ balance_refresh_seconds * 1000 }});
|
||||||
setInterval(refreshPriceSnapshot, {{ price_refresh_seconds * 1000 }});
|
setInterval(refreshPriceSnapshot, {{ price_refresh_seconds * 1000 }});
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ from key_sl_tp_lib import (
|
|||||||
sl_tp_mode_label,
|
sl_tp_mode_label,
|
||||||
sl_tp_plan_summary_text,
|
sl_tp_plan_summary_text,
|
||||||
)
|
)
|
||||||
from order_manual_breakeven_lib import apply_order_manual_breakeven
|
|
||||||
from key_monitor_lib import (
|
from key_monitor_lib import (
|
||||||
KEY_DIRECTION_WATCH,
|
KEY_DIRECTION_WATCH,
|
||||||
KEY_MONITOR_ALERT_ONLY_TYPES,
|
KEY_MONITOR_ALERT_ONLY_TYPES,
|
||||||
@@ -5847,80 +5846,6 @@ def api_order_cancel_tpsl(order_id):
|
|||||||
return jsonify({"ok": False, "msg": friendly_exchange_error(e)}), 400
|
return jsonify({"ok": False, "msg": friendly_exchange_error(e)}), 400
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/order/<int:order_id>/manual_breakeven", methods=["POST"])
|
|
||||||
@login_required
|
|
||||||
def api_order_manual_breakeven(order_id):
|
|
||||||
data = request.get_json(silent=True) or {}
|
|
||||||
try:
|
|
||||||
offset_pct = float(
|
|
||||||
data.get("offset_pct", os.getenv("MANUAL_BREAKEVEN_OFFSET_PCT", "0.2"))
|
|
||||||
)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return jsonify({"ok": False, "msg": "offset_pct 无效"}), 400
|
|
||||||
if offset_pct < 0 or offset_pct > 10:
|
|
||||||
return jsonify({"ok": False, "msg": "偏移%须在 0~10 之间"}), 400
|
|
||||||
conn = get_db()
|
|
||||||
row = conn.execute(
|
|
||||||
"SELECT * FROM order_monitors WHERE id=? AND status='active'",
|
|
||||||
(order_id,),
|
|
||||||
).fetchone()
|
|
||||||
if not row:
|
|
||||||
conn.close()
|
|
||||||
return jsonify({"ok": False, "msg": "持仓不存在或已结束"}), 404
|
|
||||||
ok, err, new_sl = apply_order_manual_breakeven(
|
|
||||||
row,
|
|
||||||
offset_pct,
|
|
||||||
calc_stop_fn=calc_trend_manual_breakeven_stop,
|
|
||||||
round_price_fn=round_price_to_exchange,
|
|
||||||
resolve_ex_sym_fn=resolve_monitor_exchange_symbol,
|
|
||||||
get_position_fn=get_live_position_contracts,
|
|
||||||
replace_tpsl_fn=replace_active_monitor_tpsl_on_exchange,
|
|
||||||
)
|
|
||||||
if not ok:
|
|
||||||
conn.close()
|
|
||||||
return jsonify({"ok": False, "msg": err or "保本失败"}), 400
|
|
||||||
conn.execute(
|
|
||||||
"UPDATE order_monitors SET stop_loss=?, breakeven_armed=1, breakeven_price=? WHERE id=?",
|
|
||||||
(new_sl, new_sl, order_id),
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
sym = row["symbol"]
|
|
||||||
direction = row["direction"]
|
|
||||||
ex_sym = resolve_monitor_exchange_symbol(row)
|
|
||||||
take_profit = float(row["take_profit"] or 0)
|
|
||||||
slots = fetch_exchange_tpsl_slots(
|
|
||||||
ex_sym, direction, plan_sl=new_sl, plan_tp=take_profit
|
|
||||||
)
|
|
||||||
conn.close()
|
|
||||||
try:
|
|
||||||
entry = float(row["trigger_price"] or 0)
|
|
||||||
send_wechat_msg(
|
|
||||||
"\n".join(
|
|
||||||
[
|
|
||||||
f"# ✅ {sym} 一键保本",
|
|
||||||
f"**账户:{_wechat_account_label()}**",
|
|
||||||
f"- 方向:{'做多' if direction == 'long' else '做空'}",
|
|
||||||
f"- 成交价:{format_price_for_symbol(sym, entry)}",
|
|
||||||
f"- 偏移:{offset_pct}%(相对成交价)",
|
|
||||||
f"- 新止损:{format_price_for_symbol(sym, new_sl)}",
|
|
||||||
"- 交易所:已更新止盈止损",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return jsonify(
|
|
||||||
{
|
|
||||||
"ok": True,
|
|
||||||
"msg": "已一键保本",
|
|
||||||
"stop_loss": new_sl,
|
|
||||||
"stop_loss_display": format_price_for_symbol(sym, new_sl),
|
|
||||||
"breakeven_armed": True,
|
|
||||||
"exchange_tpsl": slots,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/order/<int:order_id>/place_tpsl", methods=["POST"])
|
@app.route("/api/order/<int:order_id>/place_tpsl", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def api_order_place_tpsl(order_id):
|
def api_order_place_tpsl(order_id):
|
||||||
|
|||||||
@@ -194,13 +194,6 @@
|
|||||||
.pos-ex-order-main{flex:1;min-width:0;line-height:1.35}
|
.pos-ex-order-main{flex:1;min-width:0;line-height:1.35}
|
||||||
.pos-ex-cancel-btn{padding:3px 10px;background:#3a3048;color:#d4b8ff;border:none;border-radius:6px;font-size:.74rem;cursor:pointer;flex-shrink:0}
|
.pos-ex-cancel-btn{padding:3px 10px;background:#3a3048;color:#d4b8ff;border:none;border-radius:6px;font-size:.74rem;cursor:pointer;flex-shrink:0}
|
||||||
.pos-ex-cancel-btn:disabled{opacity:.4;cursor:not-allowed}
|
.pos-ex-cancel-btn:disabled{opacity:.4;cursor:not-allowed}
|
||||||
.pos-be-global{font-size:.76rem;font-weight:400;color:#8892b0;margin-left:10px;display:inline-flex;align-items:center;gap:4px}
|
|
||||||
.pos-be-global input{width:52px;padding:3px 6px;background:#0d1119;border:1px solid #3a4460;border-radius:6px;color:#e8ecf5;font-size:.78rem}
|
|
||||||
.pos-be-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:10px;padding:8px 10px;background:#1a2030;border-radius:8px;border:1px solid #2a3348}
|
|
||||||
.pos-be-label{display:inline-flex;align-items:center;gap:6px;font-size:.78rem;color:#b8c4e8;cursor:pointer;user-select:none}
|
|
||||||
.pos-be-label input{width:15px;height:15px;accent-color:#6eb5ff}
|
|
||||||
.pos-be-offset{width:52px;padding:4px 6px;background:#0d1119;border:1px solid #3a4460;border-radius:6px;color:#e8ecf5;font-size:.78rem}
|
|
||||||
.pos-be-status{font-size:.74rem;color:#6ab88a}
|
|
||||||
.tpsl-modal-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9000;align-items:center;justify-content:center;padding:16px}
|
.tpsl-modal-backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:9000;align-items:center;justify-content:center;padding:16px}
|
||||||
.tpsl-modal-backdrop.open{display:flex}
|
.tpsl-modal-backdrop.open{display:flex}
|
||||||
.tpsl-modal{background:#1a2030;border:1px solid #3a4a66;border-radius:12px;padding:16px 18px;width:min(440px,100%);max-height:90vh;overflow:auto}
|
.tpsl-modal{background:#1a2030;border:1px solid #3a4a66;border-radius:12px;padding:16px 18px;width:min(440px,100%);max-height:90vh;overflow:auto}
|
||||||
@@ -480,11 +473,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2 style="margin-bottom:8px;display:flex;align-items:center;flex-wrap:wrap;gap:6px">实时持仓
|
<h2 style="margin-bottom:8px">实时持仓</h2>
|
||||||
<span class="pos-be-global">一键保本默认偏移
|
|
||||||
<input type="number" id="manual-be-offset-global" min="0" max="10" step="0.01" value="0.2" title="相对成交价,多仓为+、空仓为-">%
|
|
||||||
</span>
|
|
||||||
</h2>
|
|
||||||
<div class="panel-scroll pos-list pos-list-live">
|
<div class="panel-scroll pos-list pos-list-live">
|
||||||
{% for o in order %}
|
{% for o in order %}
|
||||||
<div class="pos-card" id="order-row-{{ o.id }}"
|
<div class="pos-card" id="order-row-{{ o.id }}"
|
||||||
@@ -512,15 +501,6 @@
|
|||||||
{% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
|
{% if o.breakeven_enabled %}移动保本:开 {{ o.breakeven_rr_trigger or '-' }}R→{{ price_fmt(o.symbol, o.breakeven_price) }}{% else %}移动保本:关{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pos-be-row">
|
|
||||||
<label class="pos-be-label" title="将止损移至成交价±偏移%,并同步交易所止盈止损">
|
|
||||||
<input type="checkbox" class="pos-be-switch" data-order-id="{{ o.id }}"
|
|
||||||
{% if o.breakeven_armed %}checked data-armed="1"{% endif %}>
|
|
||||||
一键保本
|
|
||||||
</label>
|
|
||||||
<input type="number" class="pos-be-offset" min="0" max="10" step="0.01" value="0.2" title="相对成交价偏移%">%
|
|
||||||
<span class="pos-be-status" id="pos-be-status-{{ o.id }}">{% if o.breakeven_armed %}已保本{% endif %}</span>
|
|
||||||
</div>
|
|
||||||
<div class="pos-grid">
|
<div class="pos-grid">
|
||||||
<div class="pos-cell">
|
<div class="pos-cell">
|
||||||
<span class="pos-label">成交价</span>
|
<span class="pos-label">成交价</span>
|
||||||
@@ -1763,96 +1743,6 @@ function submitTpslEntrust(){
|
|||||||
post();
|
post();
|
||||||
}).catch(()=>alert('无法校验盈亏比'));
|
}).catch(()=>alert('无法校验盈亏比'));
|
||||||
}
|
}
|
||||||
const MANUAL_BE_OFFSET_KEY = 'manualBreakevenOffsetPct';
|
|
||||||
function getManualBeOffsetPct(){
|
|
||||||
const g = document.getElementById('manual-be-offset-global');
|
|
||||||
if(g && g.value !== ''){
|
|
||||||
const n = parseFloat(g.value);
|
|
||||||
if(Number.isFinite(n)) return n;
|
|
||||||
}
|
|
||||||
const saved = localStorage.getItem(MANUAL_BE_OFFSET_KEY);
|
|
||||||
const n = saved ? parseFloat(saved) : 0.2;
|
|
||||||
return Number.isFinite(n) ? n : 0.2;
|
|
||||||
}
|
|
||||||
function initManualBreakevenUi(){
|
|
||||||
const g = document.getElementById('manual-be-offset-global');
|
|
||||||
if(g){
|
|
||||||
const saved = localStorage.getItem(MANUAL_BE_OFFSET_KEY);
|
|
||||||
if(saved) g.value = saved;
|
|
||||||
g.addEventListener('change', ()=>{
|
|
||||||
localStorage.setItem(MANUAL_BE_OFFSET_KEY, g.value);
|
|
||||||
document.querySelectorAll('.pos-be-offset').forEach(inp=>{
|
|
||||||
if(!inp.dataset.touched) inp.value = g.value;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
document.querySelectorAll('.pos-be-offset').forEach(inp=>{
|
|
||||||
if(!inp.dataset.touched){
|
|
||||||
inp.value = getManualBeOffsetPct();
|
|
||||||
}
|
|
||||||
inp.addEventListener('input', ()=>{ inp.dataset.touched = '1'; });
|
|
||||||
});
|
|
||||||
document.querySelectorAll('.pos-be-switch').forEach(sw=>{
|
|
||||||
if(sw.dataset.bound) return;
|
|
||||||
sw.dataset.bound = '1';
|
|
||||||
sw.addEventListener('change', ()=>{
|
|
||||||
const orderId = sw.getAttribute('data-order-id');
|
|
||||||
const card = sw.closest('.pos-card');
|
|
||||||
const offInp = card && card.querySelector('.pos-be-offset');
|
|
||||||
if(!sw.checked){
|
|
||||||
if(sw.dataset.armed === '1') sw.checked = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
applyManualBreakeven(orderId, sw, offInp, card);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function updateOrderCardSlDisplay(card, slDisplay){
|
|
||||||
if(!card || !slDisplay) return;
|
|
||||||
card.setAttribute('data-plan-sl', slDisplay);
|
|
||||||
const cells = card.querySelectorAll('.pos-grid .pos-cell');
|
|
||||||
if(cells[1]){
|
|
||||||
const val = cells[1].querySelector('.pos-value');
|
|
||||||
if(val) val.textContent = slDisplay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function applyManualBreakeven(orderId, sw, offInp, card){
|
|
||||||
const offset = offInp ? parseFloat(offInp.value) : getManualBeOffsetPct();
|
|
||||||
if(!Number.isFinite(offset) || offset < 0 || offset > 10){
|
|
||||||
alert('偏移%须在 0~10 之间');
|
|
||||||
sw.checked = sw.dataset.armed === '1';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const dirHint = card && card.getAttribute('data-direction') === 'short' ? '下' : '上';
|
|
||||||
if(!confirm(`确认一键保本?止损将移至成交价${dirHint}移 ${offset}%(相对成交价)`)){
|
|
||||||
sw.checked = sw.dataset.armed === '1';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sw.disabled = true;
|
|
||||||
fetch(`/api/order/${orderId}/manual_breakeven`, {
|
|
||||||
method:'POST',
|
|
||||||
headers:{'Content-Type':'application/json'},
|
|
||||||
body: JSON.stringify({ offset_pct: offset })
|
|
||||||
}).then(r=>r.json()).then(data=>{
|
|
||||||
if(!data.ok){
|
|
||||||
alert(data.msg || '保本失败');
|
|
||||||
sw.checked = sw.dataset.armed === '1';
|
|
||||||
sw.disabled = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sw.dataset.armed = '1';
|
|
||||||
sw.checked = true;
|
|
||||||
const st = document.getElementById(`pos-be-status-${orderId}`);
|
|
||||||
if(st) st.textContent = '已保本';
|
|
||||||
if(data.stop_loss_display) updateOrderCardSlDisplay(card, data.stop_loss_display);
|
|
||||||
if(data.exchange_tpsl) paintExchangeTpslRow(orderId, data.exchange_tpsl);
|
|
||||||
else refreshPriceSnapshotConditional();
|
|
||||||
}).catch(()=>{
|
|
||||||
alert('保本请求失败');
|
|
||||||
sw.checked = sw.dataset.armed === '1';
|
|
||||||
sw.disabled = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function cancelExchangeTpsl(orderId, role){
|
function cancelExchangeTpsl(orderId, role){
|
||||||
const label = role === 'sl' ? '止损' : '止盈';
|
const label = role === 'sl' ? '止损' : '止盈';
|
||||||
if(!confirm(`确认撤销交易所${label}委托?(不会平仓)`)) return;
|
if(!confirm(`确认撤销交易所${label}委托?(不会平仓)`)) return;
|
||||||
@@ -2224,7 +2114,6 @@ function refreshPriceSnapshotConditional(){
|
|||||||
}
|
}
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
}
|
}
|
||||||
initManualBreakevenUi();
|
|
||||||
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
|
setInterval(refreshPriceSnapshotConditional, {{ price_refresh_seconds * 1000 }});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -336,7 +336,12 @@ def _parse_http_json_body(r: httpx.Response) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
async def _fetch_flask_json(
|
async def _fetch_flask_json(
|
||||||
client: httpx.AsyncClient, ex: dict, path: str, method: str = "GET", data=None
|
client: httpx.AsyncClient,
|
||||||
|
ex: dict,
|
||||||
|
path: str,
|
||||||
|
method: str = "GET",
|
||||||
|
data=None,
|
||||||
|
json_body: dict | None = None,
|
||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
base = (ex.get("flask_url") or "").rstrip("/")
|
base = (ex.get("flask_url") or "").rstrip("/")
|
||||||
if not base:
|
if not base:
|
||||||
@@ -345,7 +350,15 @@ async def _fetch_flask_json(
|
|||||||
if method == "GET":
|
if method == "GET":
|
||||||
r = await client.get(f"{base}{path}", headers=_hub_headers(), timeout=HUB_FLASK_TIMEOUT)
|
r = await client.get(f"{base}{path}", headers=_hub_headers(), timeout=HUB_FLASK_TIMEOUT)
|
||||||
else:
|
else:
|
||||||
r = await client.post(f"{base}{path}", headers=_hub_headers(), data=data, timeout=120.0)
|
headers = {**_hub_headers(), "Content-Type": "application/json"}
|
||||||
|
if json_body is not None:
|
||||||
|
r = await client.post(
|
||||||
|
f"{base}{path}", headers=headers, json=json_body, timeout=120.0
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
r = await client.post(
|
||||||
|
f"{base}{path}", headers=headers, data=data, timeout=120.0
|
||||||
|
)
|
||||||
if r.status_code >= 400:
|
if r.status_code >= 400:
|
||||||
parsed = _parse_http_json_body(r)
|
parsed = _parse_http_json_body(r)
|
||||||
parsed.setdefault("ok", False)
|
parsed.setdefault("ok", False)
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
"""中控台持仓:一键保本(成交价 ± 偏移%)。"""
|
|
||||||
from typing import Any, Callable, Optional, Tuple
|
|
||||||
|
|
||||||
|
|
||||||
def calc_manual_breakeven_stop(direction: str, entry_price: float, offset_pct: float) -> Optional[float]:
|
|
||||||
try:
|
|
||||||
e = float(entry_price)
|
|
||||||
pct = float(offset_pct)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return None
|
|
||||||
if e <= 0 or pct < 0:
|
|
||||||
return None
|
|
||||||
direction = (direction or "long").strip().lower()
|
|
||||||
if direction == "short":
|
|
||||||
return e * (1.0 - pct / 100.0)
|
|
||||||
return e * (1.0 + pct / 100.0)
|
|
||||||
|
|
||||||
|
|
||||||
def apply_order_manual_breakeven(
|
|
||||||
row: Any,
|
|
||||||
offset_pct: float,
|
|
||||||
*,
|
|
||||||
calc_stop_fn: Callable[..., Optional[float]],
|
|
||||||
round_price_fn: Callable[[str, float], Any],
|
|
||||||
resolve_ex_sym_fn: Callable[[Any], str],
|
|
||||||
get_position_fn: Callable[[str, str], Any],
|
|
||||||
replace_tpsl_fn: Callable[[Any, float, float], None],
|
|
||||||
entry_price_key: str = "trigger_price",
|
|
||||||
) -> Tuple[bool, Optional[str], Optional[float]]:
|
|
||||||
if (row["status"] or "").strip() != "active":
|
|
||||||
return False, "持仓已结束", None
|
|
||||||
entry = float(row[entry_price_key] or 0)
|
|
||||||
if entry <= 0:
|
|
||||||
return False, "缺少有效成交价", None
|
|
||||||
direction = (row["direction"] or "long").lower()
|
|
||||||
take_profit = float(row["take_profit"] or 0)
|
|
||||||
if take_profit <= 0:
|
|
||||||
return False, "缺少有效止盈价,无法更新交易所委托", None
|
|
||||||
ex_sym = resolve_ex_sym_fn(row)
|
|
||||||
pos = get_position_fn(ex_sym, direction)
|
|
||||||
if pos is None or float(pos) <= 0:
|
|
||||||
return False, "交易所当前无该方向持仓", None
|
|
||||||
new_sl_raw = calc_stop_fn(direction, entry, offset_pct)
|
|
||||||
if new_sl_raw is None:
|
|
||||||
new_sl_raw = calc_manual_breakeven_stop(direction, entry, offset_pct)
|
|
||||||
if new_sl_raw is None:
|
|
||||||
return False, "保本价计算失败", None
|
|
||||||
new_sl = round_price_fn(ex_sym, new_sl_raw)
|
|
||||||
if new_sl is None:
|
|
||||||
return False, "保本价经交易所精度舍入后无效", None
|
|
||||||
new_sl = float(new_sl)
|
|
||||||
cur_sl = float(row["stop_loss"] or 0)
|
|
||||||
if direction == "long":
|
|
||||||
if cur_sl > 0 and new_sl <= cur_sl:
|
|
||||||
return False, f"新止损 {new_sl} 未高于当前止损 {cur_sl}(多仓需上移)", None
|
|
||||||
else:
|
|
||||||
if cur_sl > 0 and new_sl >= cur_sl:
|
|
||||||
return False, f"新止损 {new_sl} 未低于当前止损 {cur_sl}(空仓需下移)", None
|
|
||||||
try:
|
|
||||||
replace_tpsl_fn(row, new_sl, take_profit)
|
|
||||||
except Exception as e:
|
|
||||||
return False, str(e), None
|
|
||||||
return True, None, new_sl
|
|
||||||
Reference in New Issue
Block a user