ui: 顶栏透明、设置两列、下单与持仓监控优化
导航栏与页面背景一致;系统设置双列布局;下单三行表单与开仓状态反馈;持仓卡片增加平仓与止盈止损挂单展示。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -48,6 +48,7 @@ from ctp_symbol import ths_to_vnpy_symbol
|
|||||||
from vnpy_bridge import (
|
from vnpy_bridge import (
|
||||||
ctp_connect,
|
ctp_connect,
|
||||||
ctp_get_account,
|
ctp_get_account,
|
||||||
|
ctp_list_active_orders,
|
||||||
ctp_list_positions,
|
ctp_list_positions,
|
||||||
ctp_status,
|
ctp_status,
|
||||||
execute_order,
|
execute_order,
|
||||||
@@ -110,6 +111,68 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
except Exception:
|
except Exception:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
def _build_pending_orders(conn, mode: str) -> list[dict]:
|
||||||
|
pending: list[dict] = []
|
||||||
|
for r in conn.execute(
|
||||||
|
"SELECT * FROM trade_order_monitors WHERE status='active' ORDER BY id DESC"
|
||||||
|
).fetchall():
|
||||||
|
mon = dict(r)
|
||||||
|
sym = mon.get("symbol") or ""
|
||||||
|
direction = mon.get("direction") or "long"
|
||||||
|
lots = int(mon.get("lots") or 0)
|
||||||
|
base = {
|
||||||
|
"symbol_code": sym,
|
||||||
|
"symbol": mon.get("symbol_name") or sym,
|
||||||
|
"direction": direction,
|
||||||
|
"direction_label": "做多" if direction == "long" else "做空",
|
||||||
|
"lots": lots,
|
||||||
|
"source": "monitor",
|
||||||
|
}
|
||||||
|
sl = mon.get("stop_loss")
|
||||||
|
tp = mon.get("take_profit")
|
||||||
|
if sl is not None:
|
||||||
|
pending.append({
|
||||||
|
**base,
|
||||||
|
"order_kind": "stop_loss",
|
||||||
|
"label": "止损挂单",
|
||||||
|
"price": float(sl),
|
||||||
|
})
|
||||||
|
if tp is not None:
|
||||||
|
pending.append({
|
||||||
|
**base,
|
||||||
|
"order_kind": "take_profit",
|
||||||
|
"label": "止盈挂单",
|
||||||
|
"price": float(tp),
|
||||||
|
})
|
||||||
|
ctp_st = ctp_status(mode)
|
||||||
|
if ctp_st.get("connected"):
|
||||||
|
for o in _ctp_active_orders(mode):
|
||||||
|
sym = o.get("symbol") or ""
|
||||||
|
offset_s = (o.get("offset") or "").upper()
|
||||||
|
kind = "limit"
|
||||||
|
label = "委托挂单"
|
||||||
|
if "CLOSE" in offset_s:
|
||||||
|
label = "平仓委托"
|
||||||
|
pending.append({
|
||||||
|
"symbol_code": sym,
|
||||||
|
"symbol": sym,
|
||||||
|
"direction": o.get("direction") or "long",
|
||||||
|
"direction_label": "做多" if o.get("direction") == "long" else "做空",
|
||||||
|
"lots": int(o.get("lots") or 0),
|
||||||
|
"price": float(o.get("price") or 0),
|
||||||
|
"order_kind": kind,
|
||||||
|
"label": label,
|
||||||
|
"source": "ctp",
|
||||||
|
"order_id": o.get("order_id"),
|
||||||
|
})
|
||||||
|
return pending
|
||||||
|
|
||||||
|
def _ctp_active_orders(mode: str) -> list:
|
||||||
|
try:
|
||||||
|
return ctp_list_active_orders(mode)
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
def _build_trading_live_rows(conn) -> list[dict]:
|
def _build_trading_live_rows(conn) -> list[dict]:
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
tz = ZoneInfo("Asia/Shanghai")
|
tz = ZoneInfo("Asia/Shanghai")
|
||||||
@@ -150,6 +213,23 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
break
|
break
|
||||||
sl = float(mon["stop_loss"]) if mon and mon.get("stop_loss") is not None else None
|
sl = float(mon["stop_loss"]) if mon and mon.get("stop_loss") is not None else None
|
||||||
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
|
||||||
|
pending_for_row: list[dict] = []
|
||||||
|
if sl is not None:
|
||||||
|
pending_for_row.append({
|
||||||
|
"order_kind": "stop_loss",
|
||||||
|
"label": "止损挂单",
|
||||||
|
"price": sl,
|
||||||
|
"lots": lots,
|
||||||
|
"source": "monitor",
|
||||||
|
})
|
||||||
|
if tp is not None:
|
||||||
|
pending_for_row.append({
|
||||||
|
"order_kind": "take_profit",
|
||||||
|
"label": "止盈挂单",
|
||||||
|
"price": tp,
|
||||||
|
"lots": lots,
|
||||||
|
"source": "monitor",
|
||||||
|
})
|
||||||
rows.append({
|
rows.append({
|
||||||
"key": f"ctp:{sym.lower()}:{direction}",
|
"key": f"ctp:{sym.lower()}:{direction}",
|
||||||
"source": "ctp",
|
"source": "ctp",
|
||||||
@@ -169,6 +249,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
"price_precision": tick.get("price_precision"),
|
"price_precision": tick.get("price_precision"),
|
||||||
"tick_size": tick.get("tick_size"),
|
"tick_size": tick.get("tick_size"),
|
||||||
"can_close": True,
|
"can_close": True,
|
||||||
|
"pending_orders": pending_for_row,
|
||||||
})
|
})
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
@@ -234,11 +315,13 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se
|
|||||||
mode = get_trading_mode(get_setting)
|
mode = get_trading_mode(get_setting)
|
||||||
ctp_st = ctp_status(mode)
|
ctp_st = ctp_status(mode)
|
||||||
rows = _build_trading_live_rows(conn)
|
rows = _build_trading_live_rows(conn)
|
||||||
|
pending_orders = _build_pending_orders(conn, mode)
|
||||||
capital = _capital(conn)
|
capital = _capital(conn)
|
||||||
risk = get_risk_status(conn)
|
risk = get_risk_status(conn)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"rows": rows,
|
"rows": rows,
|
||||||
|
"pending_orders": pending_orders,
|
||||||
"capital": capital,
|
"capital": capital,
|
||||||
"ctp_status": ctp_st,
|
"ctp_status": ctp_st,
|
||||||
"trading_mode_label": trading_mode_label(get_setting),
|
"trading_mode_label": trading_mode_label(get_setting),
|
||||||
|
|||||||
+2
-2
@@ -56,8 +56,8 @@
|
|||||||
|
|
||||||
.site-header{
|
.site-header{
|
||||||
border-bottom:1px solid var(--border-header);
|
border-bottom:1px solid var(--border-header);
|
||||||
background:var(--header-bg);
|
background:transparent;
|
||||||
backdrop-filter:blur(12px);
|
backdrop-filter:none;
|
||||||
}
|
}
|
||||||
.site-header::after{
|
.site-header::after{
|
||||||
content:"";display:block;height:1px;margin-top:-1px;
|
content:"";display:block;height:1px;margin-top:-1px;
|
||||||
|
|||||||
+19
-7
@@ -10,26 +10,38 @@
|
|||||||
.trade-order-status{display:grid;gap:.55rem;margin:.5rem 0 .75rem;padding:.65rem .85rem;background:var(--card-inner);border:1px solid var(--card-border);border-radius:8px;font-size:.82rem}
|
.trade-order-status{display:grid;gap:.55rem;margin:.5rem 0 .75rem;padding:.65rem .85rem;background:var(--card-inner);border:1px solid var(--card-border);border-radius:8px;font-size:.82rem}
|
||||||
.trade-order-status-compact{margin-top:0}
|
.trade-order-status-compact{margin-top:0}
|
||||||
.trade-order-status .status-row{display:flex;flex-wrap:wrap;align-items:center;gap:.35rem .65rem}
|
.trade-order-status .status-row{display:flex;flex-wrap:wrap;align-items:center;gap:.35rem .65rem}
|
||||||
.trade-form-grid{display:grid;grid-template-columns:1fr 1fr;gap:.75rem .65rem;margin-bottom:.85rem}
|
.trade-form-rows{display:flex;flex-direction:column;gap:.75rem;margin-bottom:.85rem}
|
||||||
.trade-form-grid .span-2{grid-column:span 2}
|
.trade-form-line{display:grid;gap:.65rem;align-items:end}
|
||||||
|
.trade-form-line.line-3{grid-template-columns:1.4fr 0.8fr 0.8fr}
|
||||||
.trade-field label{display:block;font-size:.72rem;margin-bottom:.28rem;color:var(--text-label)}
|
.trade-field label{display:block;font-size:.72rem;margin-bottom:.28rem;color:var(--text-label)}
|
||||||
.trade-field select,.trade-field input{width:100%;box-sizing:border-box}
|
.trade-field select,.trade-field input{width:100%;box-sizing:border-box}
|
||||||
.trade-field .lots-auto{color:var(--accent);font-weight:600;background:var(--card-inner);cursor:default}
|
.trade-field .lots-auto{color:var(--accent);font-weight:600;background:var(--card-inner);cursor:default}
|
||||||
.price-type-tabs{display:flex;gap:.35rem;margin-bottom:.35rem}
|
.price-type-tabs{display:flex;gap:.35rem;margin-bottom:.35rem}
|
||||||
.price-tab{border:1px solid var(--card-border);background:var(--card-inner);color:var(--text-muted);padding:.28rem .7rem;border-radius:6px;font-size:.75rem;cursor:pointer;flex:1;text-align:center}
|
.price-tab{border:1px solid var(--card-border);background:var(--card-inner);color:var(--text-muted);padding:.28rem .7rem;border-radius:6px;font-size:.75rem;cursor:pointer;flex:1;text-align:center;width:auto}
|
||||||
.price-tab.active{border-color:var(--accent);color:var(--accent);font-weight:600;background:rgba(56,189,248,.08)}
|
.price-tab.active{border-color:var(--accent);color:var(--accent);font-weight:600;background:rgba(56,189,248,.08)}
|
||||||
.market-hint{font-size:.7rem;margin-top:.25rem}
|
.market-hint{font-size:.7rem;margin-top:.25rem}
|
||||||
.trade-action-row{display:grid;grid-template-columns:1fr 1fr;gap:.65rem;margin:.85rem 0 .55rem}
|
.trade-action-row{display:flex;flex-direction:column;gap:.45rem;margin:.85rem 0 .55rem}
|
||||||
.trade-action-row .btn-open,.trade-action-row .btn-secondary{padding:.6rem .75rem;font-size:.88rem;width:100%}
|
.trade-action-row .btn-open{padding:.65rem .75rem;font-size:.9rem;width:100%}
|
||||||
|
.trade-action-row .btn-open:disabled{opacity:.65;cursor:wait}
|
||||||
|
.trade-order-msg{font-size:.82rem;text-align:center;margin:0;padding:.35rem}
|
||||||
|
.trade-order-msg.ok{color:var(--profit)}
|
||||||
|
.trade-order-msg.err{color:var(--loss)}
|
||||||
.trade-footer{background:var(--card-inner);border-radius:8px;padding:.65rem .85rem;font-size:.78rem;line-height:1.5;border:1px solid var(--card-border);margin-top:.5rem}
|
.trade-footer{background:var(--card-inner);border-radius:8px;padding:.65rem .85rem;font-size:.78rem;line-height:1.5;border:1px solid var(--card-border);margin-top:.5rem}
|
||||||
.trade-footer strong{color:var(--accent)}
|
.trade-footer strong{color:var(--accent)}
|
||||||
.rec-blocked td{opacity:.55}
|
.rec-blocked td{opacity:.55}
|
||||||
.rec-ok td:first-child{font-weight:600}
|
.rec-ok td:first-child{font-weight:600}
|
||||||
#positions .card-body{max-height:460px;overflow-y:auto}
|
#positions .card-body{max-height:460px;overflow-y:auto}
|
||||||
|
.pos-pending-orders{margin-top:.55rem;padding-top:.55rem;border-top:1px dashed var(--table-border)}
|
||||||
|
.pos-pending-orders .pending-title{font-size:.68rem;color:var(--text-muted);margin-bottom:.35rem}
|
||||||
|
.pos-pending-item{display:flex;justify-content:space-between;align-items:center;gap:.5rem;font-size:.75rem;padding:.35rem .5rem;border-radius:6px;margin-bottom:.25rem;background:var(--list-item-bg)}
|
||||||
|
.pos-pending-item.sl{border-left:3px solid var(--loss)}
|
||||||
|
.pos-pending-item.tp{border-left:3px solid var(--profit)}
|
||||||
|
.pos-pending-item.ctp{border-left:3px solid var(--accent)}
|
||||||
|
.pos-close-btn{padding:.4rem .85rem;font-size:.78rem;border-radius:8px;border:1px solid var(--loss);background:var(--loss-bg);color:var(--loss);cursor:pointer;white-space:nowrap;width:auto;flex-shrink:0}
|
||||||
|
.pos-close-btn:disabled{opacity:.55;cursor:wait}
|
||||||
|
|
||||||
@media (max-width:900px){
|
@media (max-width:900px){
|
||||||
.trade-row-split{grid-template-columns:1fr}
|
.trade-row-split{grid-template-columns:1fr}
|
||||||
#positions .card-body{max-height:360px}
|
#positions .card-body{max-height:360px}
|
||||||
.trade-form-grid{grid-template-columns:1fr}
|
.trade-form-line.line-3{grid-template-columns:1fr}
|
||||||
.trade-form-grid .span-2{grid-column:span 1}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-19
@@ -177,28 +177,46 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showOrderMsg(text, ok) {
|
||||||
|
var el = document.getElementById('order-msg');
|
||||||
|
if (!el) return;
|
||||||
|
if (!text) {
|
||||||
|
el.hidden = true;
|
||||||
|
el.textContent = '';
|
||||||
|
el.className = 'trade-order-msg';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.hidden = false;
|
||||||
|
el.textContent = text;
|
||||||
|
el.className = 'trade-order-msg ' + (ok ? 'ok' : 'err');
|
||||||
|
}
|
||||||
|
|
||||||
function postOrder(offset) {
|
function postOrder(offset) {
|
||||||
var sym = selectedSymbol();
|
var sym = selectedSymbol();
|
||||||
if (!sym) { alert('请选择品种'); return; }
|
if (!sym) { showOrderMsg('请选择品种', false); return; }
|
||||||
var direction = dirSelect ? dirSelect.value : 'long';
|
var direction = dirSelect ? dirSelect.value : 'long';
|
||||||
var price = entryPrice();
|
var price = entryPrice();
|
||||||
if (!price || price <= 0) {
|
if (!price || price <= 0) {
|
||||||
alert('无法获取有效价格,请先填写或刷新行情');
|
showOrderMsg('无法获取有效价格,请先填写或刷新行情', false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var lots = effectiveLots();
|
var lots = effectiveLots();
|
||||||
if (offset === 'open') {
|
if (offset === 'open') {
|
||||||
if (isRiskMode() && lots <= 0) {
|
if (isRiskMode() && lots <= 0) {
|
||||||
alert('请填写止损,系统将自动计算手数');
|
showOrderMsg('请填写止损,系统将自动计算手数', false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isRiskMode() && lots <= 0) {
|
if (!isRiskMode() && lots <= 0) {
|
||||||
alert('请填写手数');
|
showOrderMsg('请填写手数', false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
lots = parseInt(lotsInput && lotsInput.value, 10) || 1;
|
|
||||||
}
|
}
|
||||||
|
var btnOpen = document.getElementById('btn-open');
|
||||||
|
if (btnOpen) {
|
||||||
|
btnOpen.disabled = true;
|
||||||
|
btnOpen.textContent = '开仓中…';
|
||||||
|
}
|
||||||
|
showOrderMsg('开仓中…', true);
|
||||||
var body = {
|
var body = {
|
||||||
symbol: sym,
|
symbol: sym,
|
||||||
offset: offset,
|
offset: offset,
|
||||||
@@ -214,22 +232,48 @@
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(body)
|
body: JSON.stringify(body)
|
||||||
}).then(function (r) { return r.json(); }).then(function (data) {
|
}).then(function (r) { return r.json(); }).then(function (data) {
|
||||||
if (!data.ok) { alert(data.error || '下单失败'); return; }
|
if (!data.ok) {
|
||||||
alert((offset === 'open' ? '开仓' : '平仓') + '已提交 ' + (data.lots || lots) + ' 手');
|
showOrderMsg(data.error || '下单失败', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showOrderMsg('开仓成功 · ' + (data.lots || lots) + ' 手', true);
|
||||||
pollPositions();
|
pollPositions();
|
||||||
refreshQuote();
|
refreshQuote();
|
||||||
|
setTimeout(function () { showOrderMsg(''); }, 4000);
|
||||||
|
}).catch(function () {
|
||||||
|
showOrderMsg('网络错误,请重试', false);
|
||||||
|
}).finally(function () {
|
||||||
|
if (btnOpen) {
|
||||||
|
btnOpen.disabled = false;
|
||||||
|
btnOpen.textContent = '开仓';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildPendingHtml(items) {
|
||||||
|
if (!items || !items.length) return '';
|
||||||
|
var rows = items.map(function (p) {
|
||||||
|
var cls = p.order_kind === 'stop_loss' ? 'sl' : (p.order_kind === 'take_profit' ? 'tp' : 'ctp');
|
||||||
|
return (
|
||||||
|
'<div class="pos-pending-item ' + cls + '">' +
|
||||||
|
'<span>' + (p.label || '挂单') + '</span>' +
|
||||||
|
'<span><strong>' + fmtNum(p.price) + '</strong> · ' + (p.lots || 1) + ' 手</span>' +
|
||||||
|
'</div>'
|
||||||
|
);
|
||||||
|
}).join('');
|
||||||
|
return '<div class="pos-pending-orders"><div class="pending-title">止盈止损挂单</div>' + rows + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
function buildPosCard(row) {
|
function buildPosCard(row) {
|
||||||
var pnlClass = row.float_pnl > 0 ? 'pnl-pos' : (row.float_pnl < 0 ? 'pnl-neg' : '');
|
var pnlClass = row.float_pnl > 0 ? 'pnl-pos' : (row.float_pnl < 0 ? 'pnl-neg' : '');
|
||||||
var pnlText = row.float_pnl != null ? ((row.float_pnl >= 0 ? '+' : '') + fmtNum(row.float_pnl) + ' 元') : '--';
|
var pnlText = row.float_pnl != null ? ((row.float_pnl >= 0 ? '+' : '') + fmtNum(row.float_pnl) + ' 元') : '--';
|
||||||
var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空');
|
var dirBadge = row.direction_label || (row.direction === 'long' ? '做多' : '做空');
|
||||||
|
var closePayload = encodeURIComponent(JSON.stringify({
|
||||||
|
source: row.source, symbol_code: row.symbol_code, direction: row.direction,
|
||||||
|
lots: row.lots, mark_price: row.mark_price, monitor_id: row.monitor_id || null
|
||||||
|
}));
|
||||||
var closeBtn = row.can_close ?
|
var closeBtn = row.can_close ?
|
||||||
'<button type="button" class="btn-del pos-del" data-close=\'' + JSON.stringify({
|
'<button type="button" class="pos-close-btn" data-close="' + closePayload + '">平仓</button>' : '';
|
||||||
source: row.source, symbol_code: row.symbol_code, direction: row.direction,
|
|
||||||
lots: row.lots, mark_price: row.mark_price, monitor_id: row.monitor_id || null
|
|
||||||
}) + '\'>平仓</button>' : '';
|
|
||||||
return (
|
return (
|
||||||
'<div class="pos-card">' +
|
'<div class="pos-card">' +
|
||||||
'<div class="pos-card-head"><div><div class="title">' + row.symbol + ' <span class="badge dir">' + dirBadge + '</span></div>' +
|
'<div class="pos-card-head"><div><div class="title">' + row.symbol + ' <span class="badge dir">' + dirBadge + '</span></div>' +
|
||||||
@@ -240,21 +284,39 @@
|
|||||||
'<div class="cell"><label>止损</label><div>' + (row.stop_loss != null ? fmtNum(row.stop_loss) : '--') + '</div></div>' +
|
'<div class="cell"><label>止损</label><div>' + (row.stop_loss != null ? fmtNum(row.stop_loss) : '--') + '</div></div>' +
|
||||||
'<div class="cell"><label>止盈</label><div>' + (row.take_profit != null ? fmtNum(row.take_profit) : '--') + '</div></div>' +
|
'<div class="cell"><label>止盈</label><div>' + (row.take_profit != null ? fmtNum(row.take_profit) : '--') + '</div></div>' +
|
||||||
'<div class="cell ' + pnlClass + '"><label>浮盈亏</label><div>' + pnlText + '</div></div>' +
|
'<div class="cell ' + pnlClass + '"><label>浮盈亏</label><div>' + pnlText + '</div></div>' +
|
||||||
'</div><div class="pos-footer"><span>' + row.lots + ' 手</span></div></div>'
|
'</div>' + buildPendingHtml(row.pending_orders) +
|
||||||
|
'<div class="pos-footer"><span>' + row.lots + ' 手</span></div></div>'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closePosition(payload) {
|
function closePosition(payload, btn) {
|
||||||
function doClose(price) {
|
function doClose(price) {
|
||||||
if (!price || price <= 0) { alert('无法获取现价'); return; }
|
if (!price || price <= 0) { alert('无法获取现价'); return; }
|
||||||
if (!confirm('确认平仓 ' + payload.lots + ' 手?')) return;
|
if (!confirm('确认平仓 ' + payload.lots + ' 手?')) return;
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = '平仓中…';
|
||||||
|
}
|
||||||
fetch('/api/trading/close', {
|
fetch('/api/trading/close', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(Object.assign({}, payload, { price: price }))
|
body: JSON.stringify(Object.assign({}, payload, { price: price }))
|
||||||
}).then(function (r) { return r.json(); }).then(function (d) {
|
}).then(function (r) { return r.json(); }).then(function (d) {
|
||||||
if (!d.ok) { alert(d.error || '平仓失败'); return; }
|
if (!d.ok) {
|
||||||
|
alert(d.error || '平仓失败');
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = '平仓';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (btn) btn.textContent = '已平仓';
|
||||||
pollPositions();
|
pollPositions();
|
||||||
|
}).catch(function () {
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = '平仓';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (payload.mark_price > 0) {
|
if (payload.mark_price > 0) {
|
||||||
@@ -285,13 +347,26 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
list.innerHTML = '<div class="empty-hint">柜台暂无持仓。</div>';
|
var pendingOnly = data.pending_orders || [];
|
||||||
|
if (pendingOnly.length) {
|
||||||
|
list.innerHTML = '<div class="empty-hint" style="margin-bottom:.75rem">柜台暂无持仓</div>' +
|
||||||
|
pendingOnly.map(function (p) {
|
||||||
|
return (
|
||||||
|
'<div class="pos-pending-item ' +
|
||||||
|
(p.order_kind === 'stop_loss' ? 'sl' : (p.order_kind === 'take_profit' ? 'tp' : 'ctp')) +
|
||||||
|
'"><span>' + (p.label || '挂单') + ' · ' + (p.symbol || p.symbol_code) + '</span>' +
|
||||||
|
'<span><strong>' + fmtNum(p.price) + '</strong> · ' + (p.lots || 1) + ' 手</span></div>'
|
||||||
|
);
|
||||||
|
}).join('');
|
||||||
|
} else {
|
||||||
|
list.innerHTML = '<div class="empty-hint">柜台暂无持仓。</div>';
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
list.innerHTML = rows.map(buildPosCard).join('');
|
list.innerHTML = rows.map(buildPosCard).join('');
|
||||||
list.querySelectorAll('[data-close]').forEach(function (btn) {
|
list.querySelectorAll('[data-close]').forEach(function (btn) {
|
||||||
btn.addEventListener('click', function () {
|
btn.addEventListener('click', function () {
|
||||||
closePosition(JSON.parse(btn.getAttribute('data-close')));
|
closePosition(JSON.parse(decodeURIComponent(btn.getAttribute('data-close'))), btn);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
@@ -357,9 +432,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var btnOpen = document.getElementById('btn-open');
|
var btnOpen = document.getElementById('btn-open');
|
||||||
var btnClose = document.getElementById('btn-close-pos');
|
|
||||||
if (btnOpen) btnOpen.addEventListener('click', function () { postOrder('open'); });
|
if (btnOpen) btnOpen.addEventListener('click', function () { postOrder('open'); });
|
||||||
if (btnClose) btnClose.addEventListener('click', function () { postOrder('close'); });
|
|
||||||
|
|
||||||
var btnConnect = document.getElementById('btn-ctp-connect');
|
var btnConnect = document.getElementById('btn-ctp-connect');
|
||||||
if (btnConnect) {
|
if (btnConnect) {
|
||||||
|
|||||||
+6
-6
@@ -25,7 +25,7 @@
|
|||||||
--bg-page:#050508;
|
--bg-page:#050508;
|
||||||
--bg-grid:rgba(76,194,255,.045);
|
--bg-grid:rgba(76,194,255,.045);
|
||||||
--border-header:rgba(76,194,255,.12);
|
--border-header:rgba(76,194,255,.12);
|
||||||
--header-bg:rgba(8,10,18,.75);
|
--header-bg:transparent;
|
||||||
--text-primary:#e8eaf6;
|
--text-primary:#e8eaf6;
|
||||||
--text-title:#ffffff;
|
--text-title:#ffffff;
|
||||||
--text-muted:#7a82a0;
|
--text-muted:#7a82a0;
|
||||||
@@ -79,8 +79,8 @@
|
|||||||
[data-theme="light"]{
|
[data-theme="light"]{
|
||||||
--bg-page:#e8eef8;
|
--bg-page:#e8eef8;
|
||||||
--bg-grid:rgba(37,99,235,.07);
|
--bg-grid:rgba(37,99,235,.07);
|
||||||
--border-header:rgba(37,99,235,.15);
|
--border-header:rgba(37,99,235,.12);
|
||||||
--header-bg:rgba(255,255,255,.82);
|
--header-bg:transparent;
|
||||||
--text-primary:#1a2233;
|
--text-primary:#1a2233;
|
||||||
--text-title:#0a1628;
|
--text-title:#0a1628;
|
||||||
--text-muted:#5c6578;
|
--text-muted:#5c6578;
|
||||||
@@ -162,13 +162,13 @@
|
|||||||
.site-nav{display:flex;justify-content:center;gap:.45rem;flex-wrap:wrap}
|
.site-nav{display:flex;justify-content:center;gap:.45rem;flex-wrap:wrap}
|
||||||
.site-nav a{
|
.site-nav a{
|
||||||
padding:.55rem 1.15rem;border-radius:8px;
|
padding:.55rem 1.15rem;border-radius:8px;
|
||||||
border:1px solid var(--nav-border);
|
border:1px solid transparent;
|
||||||
background:var(--nav-bg);
|
background:transparent;
|
||||||
color:var(--text-primary);
|
color:var(--text-primary);
|
||||||
text-decoration:none;font-size:.88rem;
|
text-decoration:none;font-size:.88rem;
|
||||||
transition:.2s;white-space:nowrap;
|
transition:.2s;white-space:nowrap;
|
||||||
}
|
}
|
||||||
.site-nav a:hover{background:var(--nav-hover);border-color:var(--accent);color:var(--text-title)}
|
.site-nav a:hover{background:var(--nav-hover);border-color:var(--nav-border);color:var(--text-title)}
|
||||||
.site-nav a.active{background:var(--nav-active);border-color:var(--nav-active-border);color:#fff}
|
.site-nav a.active{background:var(--nav-active);border-color:var(--nav-active-border);color:#fff}
|
||||||
.user-bar{position:absolute;top:1rem;right:1.5rem;font-size:.8rem;color:var(--text-muted);white-space:nowrap}
|
.user-bar{position:absolute;top:1rem;right:1.5rem;font-size:.8rem;color:var(--text-muted);white-space:nowrap}
|
||||||
.user-bar a{color:var(--danger);text-decoration:none;margin-left:.5rem}
|
.user-bar a{color:var(--danger);text-decoration:none;margin-left:.5rem}
|
||||||
|
|||||||
+19
-5
@@ -1,15 +1,26 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}系统设置 - 国内期货监控系统{% endblock %}
|
{% block title %}系统设置 - 国内期货监控系统{% endblock %}
|
||||||
|
{% block extra_css %}
|
||||||
|
<style>
|
||||||
|
.settings-page{max-width:1100px;margin:0 auto}
|
||||||
|
.settings-grid{display:grid;grid-template-columns:1fr 1fr;gap:1.25rem;align-items:start}
|
||||||
|
.settings-grid .card{margin-bottom:0;height:100%}
|
||||||
|
.settings-grid .settings-span-2{grid-column:1/-1}
|
||||||
|
@media(max-width:900px){.settings-grid{grid-template-columns:1fr}}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="settings-page">
|
||||||
|
<div class="settings-grid">
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>导航显示</h2>
|
<h2>导航显示</h2>
|
||||||
<form action="{{ url_for('settings') }}" method="post">
|
<form action="{{ url_for('settings') }}" method="post">
|
||||||
<input type="hidden" name="action" value="nav">
|
<input type="hidden" name="action" value="nav">
|
||||||
<p class="hint" style="margin-bottom:.75rem">关闭后顶栏隐藏对应入口,直接访问 URL 也会跳转回持仓监控。</p>
|
<p class="hint" style="margin-bottom:.75rem">关闭后顶栏隐藏对应入口,直接访问 URL 也会跳转回持仓监控。</p>
|
||||||
<div class="form-grid" style="max-width:640px;grid-template-columns:1fr 1fr">
|
<div class="check-row">
|
||||||
{% for key, label in nav_toggles.items() %}
|
{% for key, label in nav_toggles.items() %}
|
||||||
<label class="field" style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
|
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer;white-space:nowrap">
|
||||||
<input type="checkbox" name="nav_{{ key }}" {% if nav_items[key] %}checked{% endif %}>
|
<input type="checkbox" name="nav_{{ key }}" {% if nav_items[key] %}checked{% endif %}>
|
||||||
<span>{{ label }}</span>
|
<span>{{ label }}</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -23,7 +34,7 @@
|
|||||||
<h2>交易模式</h2>
|
<h2>交易模式</h2>
|
||||||
<form action="{{ url_for('settings') }}" method="post">
|
<form action="{{ url_for('settings') }}" method="post">
|
||||||
<input type="hidden" name="action" value="trading">
|
<input type="hidden" name="action" value="trading">
|
||||||
<div class="form-grid" style="max-width:640px">
|
<div class="form-grid">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>交易通道</label>
|
<label>交易通道</label>
|
||||||
<select name="trading_mode">
|
<select name="trading_mode">
|
||||||
@@ -84,9 +95,9 @@
|
|||||||
<p class="hint" style="margin-top:.75rem">在企业微信群中添加机器人后,将 Webhook 地址粘贴到上方保存即可。</p>
|
<p class="hint" style="margin-top:.75rem">在企业微信群中添加机器人后,将 Webhook 地址粘贴到上方保存即可。</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card settings-span-2">
|
||||||
<h2>修改密码</h2>
|
<h2>修改密码</h2>
|
||||||
<form action="{{ url_for('settings') }}" method="post" style="max-width:400px">
|
<form action="{{ url_for('settings') }}" method="post" style="max-width:480px">
|
||||||
<input type="hidden" name="action" value="password">
|
<input type="hidden" name="action" value="password">
|
||||||
<div style="margin-bottom:.75rem">
|
<div style="margin-bottom:.75rem">
|
||||||
<label class="text-label" style="font-size:.85rem;display:block;margin-bottom:.35rem">当前账号</label>
|
<label class="text-label" style="font-size:.85rem;display:block;margin-bottom:.35rem">当前账号</label>
|
||||||
@@ -107,4 +118,7 @@
|
|||||||
<button type="submit" class="btn-primary">修改密码</button>
|
<button type="submit" class="btn-primary">修改密码</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+38
-36
@@ -32,50 +32,52 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="trade-form-grid">
|
<div class="trade-form-rows">
|
||||||
<div class="symbol-wrap trade-field span-2">
|
<div class="trade-form-line line-3">
|
||||||
<label class="text-label">品种</label>
|
<div class="symbol-wrap trade-field">
|
||||||
<input type="text" id="trade-symbol" class="symbol-input" placeholder="主力合约 rb2610" autocomplete="off">
|
<label class="text-label">品种</label>
|
||||||
<div class="symbol-dropdown"></div>
|
<input type="text" id="trade-symbol" class="symbol-input" placeholder="主力合约 rb2610" autocomplete="off">
|
||||||
<div class="symbol-selected" id="sym-selected"></div>
|
<div class="symbol-dropdown"></div>
|
||||||
</div>
|
<div class="symbol-selected" id="sym-selected"></div>
|
||||||
<div class="trade-field">
|
</div>
|
||||||
<label class="text-label">方向</label>
|
<div class="trade-field">
|
||||||
<select id="trade-direction">
|
<label class="text-label">方向</label>
|
||||||
<option value="long">做多</option>
|
<select id="trade-direction">
|
||||||
<option value="short">做空</option>
|
<option value="long">做多</option>
|
||||||
</select>
|
<option value="short">做空</option>
|
||||||
</div>
|
</select>
|
||||||
|
</div>
|
||||||
<div class="trade-field" id="field-lots">
|
<div class="trade-field" id="field-lots">
|
||||||
<label class="text-label">手数</label>
|
<label class="text-label">手数</label>
|
||||||
<input type="number" id="trade-lots" min="1" step="1" value="1" {% if sizing_mode == 'risk' %}hidden{% endif %}>
|
<input type="number" id="trade-lots" min="1" step="1" value="1" {% if sizing_mode == 'risk' %}hidden{% endif %}>
|
||||||
<input type="text" id="trade-lots-calc" class="lots-auto" readonly placeholder="填写止损后自动计算" {% if sizing_mode != 'risk' %}hidden{% endif %}>
|
<input type="text" id="trade-lots-calc" class="lots-auto" readonly placeholder="填写止损后自动计算" {% if sizing_mode != 'risk' %}hidden{% endif %}>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="trade-field span-2">
|
|
||||||
<label class="text-label">入场价</label>
|
|
||||||
<div class="price-type-tabs">
|
|
||||||
<button type="button" class="price-tab active" data-type="limit">限价</button>
|
|
||||||
<button type="button" class="price-tab" data-type="market">市价</button>
|
|
||||||
</div>
|
</div>
|
||||||
<input type="number" id="trade-price" step="any" placeholder="限价">
|
|
||||||
<p class="hint market-hint" id="market-hint" hidden>市价将按最新行情价报单</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="trade-field">
|
<div class="trade-form-line line-3">
|
||||||
<label class="text-label">止损</label>
|
<div class="trade-field">
|
||||||
<input type="number" id="trade-sl" step="any">
|
<label class="text-label">入场价</label>
|
||||||
</div>
|
<div class="price-type-tabs">
|
||||||
<div class="trade-field">
|
<button type="button" class="price-tab active" data-type="limit">限价</button>
|
||||||
<label class="text-label">止盈</label>
|
<button type="button" class="price-tab" data-type="market">市价</button>
|
||||||
<input type="number" id="trade-tp" step="any">
|
</div>
|
||||||
|
<input type="number" id="trade-price" step="any" placeholder="限价">
|
||||||
|
<p class="hint market-hint" id="market-hint" hidden>市价将按最新行情价报单</p>
|
||||||
|
</div>
|
||||||
|
<div class="trade-field">
|
||||||
|
<label class="text-label">止盈</label>
|
||||||
|
<input type="number" id="trade-tp" step="any">
|
||||||
|
</div>
|
||||||
|
<div class="trade-field">
|
||||||
|
<label class="text-label">止损</label>
|
||||||
|
<input type="number" id="trade-sl" step="any">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="trade-action-row">
|
<div class="trade-action-row">
|
||||||
<button type="button" class="btn-primary btn-open" id="btn-open">开仓</button>
|
<button type="button" class="btn-primary btn-open" id="btn-open">开仓</button>
|
||||||
<button type="button" class="btn-secondary" id="btn-close-pos">平仓</button>
|
<p class="trade-order-msg" id="order-msg" hidden></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="trade-footer" id="trade-footer">
|
<div class="trade-footer" id="trade-footer">
|
||||||
|
|||||||
@@ -324,6 +324,45 @@ class CtpBridge:
|
|||||||
})
|
})
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
def list_active_orders(self) -> list[dict[str, Any]]:
|
||||||
|
if not self._engine:
|
||||||
|
return []
|
||||||
|
out: list[dict[str, Any]] = []
|
||||||
|
try:
|
||||||
|
orders = self._engine.get_all_active_orders()
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
for order in orders or []:
|
||||||
|
status = getattr(order, "status", None)
|
||||||
|
status_s = str(status)
|
||||||
|
if status_s and not any(x in status_s for x in ("NOTTRADED", "PARTTRADED", "SUBMITTING")):
|
||||||
|
continue
|
||||||
|
vol = int(getattr(order, "volume", 0) or 0)
|
||||||
|
traded = int(getattr(order, "traded", 0) or 0)
|
||||||
|
remain = max(0, vol - traded)
|
||||||
|
if remain <= 0:
|
||||||
|
continue
|
||||||
|
direction = getattr(order, "direction", None)
|
||||||
|
d = "long"
|
||||||
|
if direction is not None and str(direction).endswith("SHORT"):
|
||||||
|
d = "short"
|
||||||
|
offset = getattr(order, "offset", None)
|
||||||
|
offset_s = str(offset or "")
|
||||||
|
sym = getattr(order, "symbol", "") or ""
|
||||||
|
exchange = getattr(order, "exchange", None)
|
||||||
|
ex_name = str(exchange.value if hasattr(exchange, "value") else exchange or "")
|
||||||
|
out.append({
|
||||||
|
"symbol": sym,
|
||||||
|
"exchange": ex_name,
|
||||||
|
"direction": d,
|
||||||
|
"lots": remain,
|
||||||
|
"price": float(getattr(order, "price", 0) or 0),
|
||||||
|
"offset": offset_s,
|
||||||
|
"order_id": str(getattr(order, "orderid", "") or ""),
|
||||||
|
"status": status_s,
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
def send_order(
|
def send_order(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
@@ -433,6 +472,12 @@ def ctp_list_positions(mode: str) -> list[dict[str, Any]]:
|
|||||||
return b.list_positions()
|
return b.list_positions()
|
||||||
|
|
||||||
|
|
||||||
|
def ctp_list_active_orders(mode: str) -> list[dict[str, Any]]:
|
||||||
|
b = get_bridge()
|
||||||
|
b.ensure_connected(mode)
|
||||||
|
return b.list_active_orders()
|
||||||
|
|
||||||
|
|
||||||
def get_ctp_balance(mode: str) -> Optional[float]:
|
def get_ctp_balance(mode: str) -> Optional[float]:
|
||||||
try:
|
try:
|
||||||
acc = ctp_get_account(mode)
|
acc = ctp_get_account(mode)
|
||||||
|
|||||||
Reference in New Issue
Block a user