增加一键保本

This commit is contained in:
dekun
2026-05-28 12:31:16 +08:00
parent 6184e7a11f
commit 96dd4a041c
9 changed files with 801 additions and 5 deletions
+108 -2
View File
@@ -131,6 +131,13 @@
.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: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}
.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}
@@ -348,11 +355,15 @@
<button type="submit">开仓(以损定仓)</button>
</form>
<div class="order-live-positions">
<h3 style="margin:0 0 2px;font-size:.95rem;color:#b8c4ff">实时持仓</h3>
<h3 style="margin:0 0 2px;font-size:.95rem;color:#b8c4ff;display:flex;align-items:center;flex-wrap:wrap;gap:6px">实时持仓
<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">
{% for o in order %}
{% set osym = o.exchange_symbol or o.symbol %}
<div class="plan-position-card">
<div class="plan-position-card" id="order-row-{{ o.id }}" data-order-id="{{ o.id }}" data-direction="{{ o.direction }}">
<div class="plan-card-head">
<div class="plan-card-title">
<span>{{ osym }}</span>
@@ -360,6 +371,15 @@
</div>
<a href="/del_order/{{ o.id }}" class="btn-close-plan" onclick="return confirm('删除会触发手动平仓,继续?')">平仓</a>
</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">
来源: 下单监控 风格: {{ 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 %}
@@ -1633,6 +1653,92 @@ if(_journalFormEl){
syncJournalEntryReasonOtherUi();
}
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('偏移%须在 010 之间');
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();
setInterval(refreshAccountSnapshot, {{ balance_refresh_seconds * 1000 }});
setInterval(refreshPriceSnapshot, {{ price_refresh_seconds * 1000 }});