diff --git a/install_trading.py b/install_trading.py index 8ff57a1..cb12bb4 100644 --- a/install_trading.py +++ b/install_trading.py @@ -48,6 +48,7 @@ from ctp_symbol import ths_to_vnpy_symbol from vnpy_bridge import ( ctp_connect, ctp_get_account, + ctp_list_active_orders, ctp_list_positions, ctp_status, execute_order, @@ -110,6 +111,68 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se except Exception: 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]: from zoneinfo import ZoneInfo tz = ZoneInfo("Asia/Shanghai") @@ -150,6 +213,23 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se break 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 + 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({ "key": f"ctp:{sym.lower()}:{direction}", "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"), "tick_size": tick.get("tick_size"), "can_close": True, + "pending_orders": pending_for_row, }) 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) ctp_st = ctp_status(mode) rows = _build_trading_live_rows(conn) + pending_orders = _build_pending_orders(conn, mode) capital = _capital(conn) risk = get_risk_status(conn) conn.commit() return jsonify({ "rows": rows, + "pending_orders": pending_orders, "capital": capital, "ctp_status": ctp_st, "trading_mode_label": trading_mode_label(get_setting), diff --git a/static/css/tech.css b/static/css/tech.css index d32c145..c439bec 100644 --- a/static/css/tech.css +++ b/static/css/tech.css @@ -56,8 +56,8 @@ .site-header{ border-bottom:1px solid var(--border-header); - background:var(--header-bg); - backdrop-filter:blur(12px); + background:transparent; + backdrop-filter:none; } .site-header::after{ content:"";display:block;height:1px;margin-top:-1px; diff --git a/static/css/trade.css b/static/css/trade.css index dc3141c..66105f2 100644 --- a/static/css/trade.css +++ b/static/css/trade.css @@ -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-compact{margin-top:0} .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-grid .span-2{grid-column:span 2} +.trade-form-rows{display:flex;flex-direction:column;gap:.75rem;margin-bottom:.85rem} +.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 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} .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)} .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 .btn-open,.trade-action-row .btn-secondary{padding:.6rem .75rem;font-size:.88rem;width:100%} +.trade-action-row{display:flex;flex-direction:column;gap:.45rem;margin:.85rem 0 .55rem} +.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 strong{color:var(--accent)} .rec-blocked td{opacity:.55} .rec-ok td:first-child{font-weight:600} #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){ .trade-row-split{grid-template-columns:1fr} #positions .card-body{max-height:360px} - .trade-form-grid{grid-template-columns:1fr} - .trade-form-grid .span-2{grid-column:span 1} + .trade-form-line.line-3{grid-template-columns:1fr} } diff --git a/static/js/trade.js b/static/js/trade.js index ff261f8..6a63b22 100644 --- a/static/js/trade.js +++ b/static/js/trade.js @@ -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) { var sym = selectedSymbol(); - if (!sym) { alert('请选择品种'); return; } + if (!sym) { showOrderMsg('请选择品种', false); return; } var direction = dirSelect ? dirSelect.value : 'long'; var price = entryPrice(); if (!price || price <= 0) { - alert('无法获取有效价格,请先填写或刷新行情'); + showOrderMsg('无法获取有效价格,请先填写或刷新行情', false); return; } var lots = effectiveLots(); if (offset === 'open') { if (isRiskMode() && lots <= 0) { - alert('请填写止损,系统将自动计算手数'); + showOrderMsg('请填写止损,系统将自动计算手数', false); return; } if (!isRiskMode() && lots <= 0) { - alert('请填写手数'); + showOrderMsg('请填写手数', false); 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 = { symbol: sym, offset: offset, @@ -214,22 +232,48 @@ headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }).then(function (r) { return r.json(); }).then(function (data) { - if (!data.ok) { alert(data.error || '下单失败'); return; } - alert((offset === 'open' ? '开仓' : '平仓') + '已提交 ' + (data.lots || lots) + ' 手'); + if (!data.ok) { + showOrderMsg(data.error || '下单失败', false); + return; + } + showOrderMsg('开仓成功 · ' + (data.lots || lots) + ' 手', true); pollPositions(); 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 ( + '