diff --git a/manual_trading_hub/exchange_orders.py b/manual_trading_hub/exchange_orders.py
index ca3b4f7..55cdbd0 100644
--- a/manual_trading_hub/exchange_orders.py
+++ b/manual_trading_hub/exchange_orders.py
@@ -166,7 +166,7 @@ def _okx_normalize_orders(raw: dict, channel: str) -> list[dict[str, Any]]:
info = {}
sl_trig = _coerce_float(info.get("slTriggerPx"), raw.get("stopLossPrice"))
tp_trig = _coerce_float(info.get("tpTriggerPx"), raw.get("takeProfitPrice"))
- if sl_trig is None or tp_trig is None:
+ if sl_trig is None or tp_trig is None or sl_trig == tp_trig:
return [n]
base_id = n["id"]
rows: list[dict[str, Any]] = []
diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css
index 485d6a0..a3dd809 100644
--- a/manual_trading_hub/static/app.css
+++ b/manual_trading_hub/static/app.css
@@ -1969,14 +1969,19 @@ body.login-page {
.market-chart-wrap {
display: flex;
flex-direction: column;
- height: min(72vh, 640px);
- min-height: 360px;
+ height: min(76vh, 680px);
+ min-height: 380px;
border: 1px solid var(--border-soft);
border-radius: var(--radius);
background: #0a1018;
overflow: hidden;
}
+.market-chart-wrap.has-pos-panel {
+ height: min(80vh, 740px);
+ min-height: 440px;
+}
+
.market-ohlcv-bar {
flex: 0 0 auto;
padding: 8px 12px;
@@ -2147,6 +2152,11 @@ body.login-page {
font-size: 0.68rem;
}
+.market-pos-tp-monitored {
+ color: #8fc8ff;
+ font-size: 0.72rem;
+}
+
.sym-link {
background: none;
border: none;
diff --git a/manual_trading_hub/static/app.js b/manual_trading_hub/static/app.js
index aea33a8..dae32ff 100644
--- a/manual_trading_hub/static/app.js
+++ b/manual_trading_hub/static/app.js
@@ -506,14 +506,146 @@
return (row && (row.key || row.id)) || exchangeId;
}
- function buildPositionMarketContext(pos, monitorOrder) {
+ function findTrendPlan(trends, symbol, side) {
+ const want = (side || "").toLowerCase();
+ for (const t of trends || []) {
+ const sym = t.symbol || t.exchange_symbol || "";
+ if (!symbolsMatchHub(sym, symbol)) continue;
+ const d = (t.direction || "").toLowerCase();
+ if (!d || d === want) return t;
+ }
+ return null;
+ }
+
+ function inferTpslFromCondOrders(side, cond, entry) {
+ const picked = pickExTpslOrders(cond);
+ let sl = picked.sl && picked.sl.trigger_price != null ? picked.sl.trigger_price : "";
+ let tp = picked.tp && picked.tp.trigger_price != null ? picked.tp.trigger_price : "";
+ if (sl !== "" && tp !== "" && Number(sl) !== Number(tp)) {
+ return { sl, tp };
+ }
+
+ const triggers = (cond || [])
+ .map(function (o) {
+ return { price: Number(o.trigger_price), label: o.label || "" };
+ })
+ .filter(function (o) {
+ return o.price != null && !Number.isNaN(o.price) && o.price > 0;
+ });
+ if (!triggers.length) return { sl: sl || "", tp: tp || "" };
+
+ const s = (side || "long").toLowerCase();
+ const e = entry != null && Number.isFinite(Number(entry)) ? Number(entry) : null;
+
+ if (e != null) {
+ const below = triggers.filter(function (t) {
+ return t.price < e;
+ });
+ const above = triggers.filter(function (t) {
+ return t.price > e;
+ });
+ if (s === "long") {
+ if (sl === "" && below.length) {
+ sl = Math.max.apply(
+ null,
+ below.map(function (t) {
+ return t.price;
+ })
+ );
+ }
+ if (tp === "" && above.length) {
+ tp = Math.min.apply(
+ null,
+ above.map(function (t) {
+ return t.price;
+ })
+ );
+ }
+ } else {
+ if (sl === "" && above.length) {
+ sl = Math.min.apply(
+ null,
+ above.map(function (t) {
+ return t.price;
+ })
+ );
+ }
+ if (tp === "" && below.length) {
+ tp = Math.max.apply(
+ null,
+ below.map(function (t) {
+ return t.price;
+ })
+ );
+ }
+ }
+ }
+
+ if (triggers.length === 1 && sl === "" && tp === "") {
+ const one = triggers[0];
+ const p = one.price;
+ const lbl = one.label;
+ if (e != null) {
+ if (s === "long") {
+ if (p < e) sl = p;
+ else if (p > e) tp = p;
+ } else if (p > e) sl = p;
+ else if (p < e) tp = p;
+ } else if (/止损/.test(lbl)) sl = p;
+ else if (/止盈/.test(lbl) && !/止盈止损/.test(lbl)) tp = p;
+ }
+
+ if (sl !== "" && tp !== "" && Number(sl) === Number(tp)) tp = "";
+ return { sl: sl || "", tp: tp || "" };
+ }
+
+ function resolvePositionTpsl(pos, monitorOrder, trendPlan) {
const mo = monitorOrder || {};
+ const tp = trendPlan || {};
+ const cond = condOrdersFromPosition(pos);
+ const entryRaw =
+ pos.entry_price != null
+ ? pos.entry_price
+ : mo.trigger_price != null
+ ? mo.trigger_price
+ : tp.avg_entry_price;
+ const entryN = entryRaw != null && entryRaw !== "" ? Number(entryRaw) : null;
+ const isTrend =
+ !!(trendPlan && trendPlan.id) || String(mo.monitor_type || "").trim() === "趋势回调";
+
+ let sl = mo.stop_loss != null && mo.stop_loss !== "" ? mo.stop_loss : "";
+ let takeProfit = mo.take_profit != null && mo.take_profit !== "" ? mo.take_profit : "";
+ let tpMonitored = false;
+
+ if (isTrend) {
+ tpMonitored = true;
+ takeProfit = "";
+ if (trendPlan && trendPlan.stop_loss != null && trendPlan.stop_loss !== "") {
+ sl = trendPlan.stop_loss;
+ }
+ }
+
+ const inferred = inferTpslFromCondOrders(pos.side, cond, entryN);
+ if (sl === "" || sl == null) sl = inferred.sl;
+ if (!tpMonitored && (takeProfit === "" || takeProfit == null)) takeProfit = inferred.tp;
+
+ if (sl !== "" && takeProfit !== "" && Number(sl) === Number(takeProfit)) {
+ takeProfit = "";
+ }
+
+ return {
+ entry: entryRaw,
+ sl,
+ tp: takeProfit,
+ tp_monitored: tpMonitored,
+ is_trend: isTrend,
+ };
+ }
+
+ function buildPositionMarketContext(pos, monitorOrder, trendPlan) {
+ const tpsl = resolvePositionTpsl(pos, monitorOrder, trendPlan);
const cond = condOrdersFromPosition(pos);
const reg = Array.isArray(pos.regular_orders) ? pos.regular_orders : [];
- const guess = guessTpslFromCondOrders(pos.side, cond);
- const entry = pos.entry_price != null ? pos.entry_price : mo.trigger_price;
- const sl = mo.stop_loss != null ? mo.stop_loss : guess.sl;
- const tp = mo.take_profit != null ? mo.take_profit : guess.tp;
const num = function (v) {
if (v == null || v === "") return null;
const n = Number(v);
@@ -538,9 +670,11 @@
});
return {
side: (pos.side || "long").toLowerCase(),
- entry: num(entry),
- stop_loss: num(sl),
- take_profit: num(tp),
+ entry: num(tpsl.entry),
+ stop_loss: num(tpsl.sl),
+ take_profit: num(tpsl.tp),
+ tp_monitored: !!tpsl.tp_monitored,
+ is_trend: !!tpsl.is_trend,
contracts: num(pos.contracts),
orders: orders,
};
@@ -565,10 +699,13 @@
}
}
- function marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder) {
+ function marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder, trendPlan) {
const symAttr = esc(symbol || "").replace(/"/g, """);
const exKeyAttr = esc(exchangeKey || exchangeId || "").replace(/"/g, """);
- const ctxEnc = esc(encodePosCtx(buildPositionMarketContext(pos, monitorOrder))).replace(/"/g, """);
+ const ctxEnc = esc(encodePosCtx(buildPositionMarketContext(pos, monitorOrder, trendPlan))).replace(
+ /"/g,
+ """
+ );
return (
'data-ex-id="' +
esc(exchangeId) +
@@ -706,18 +843,8 @@
return `
`;
}
- function guessTpslFromCondOrders(side, cond) {
- const triggers = (cond || [])
- .map((o) => o.trigger_price)
- .filter((v) => v != null && !Number.isNaN(Number(v)))
- .map(Number);
- if (!triggers.length) return { sl: "", tp: "" };
- triggers.sort((a, b) => a - b);
- const s = (side || "long").toLowerCase();
- if (s === "short") {
- return { sl: triggers[triggers.length - 1], tp: triggers[0] };
- }
- return { sl: triggers[0], tp: triggers[triggers.length - 1] };
+ function guessTpslFromCondOrders(side, cond, entry) {
+ return inferTpslFromCondOrders(side, cond, entry);
}
function renderOrdersCollapse(exchangeId, symbol, cond, reg) {
@@ -786,7 +913,7 @@
return row("止损", sl) + row("止盈", tp);
}
- function renderLivePositionCard(exchangeId, exchangeKey, pos, monitorOrder) {
+ function renderLivePositionCard(exchangeId, exchangeKey, pos, monitorOrder, trendPlan) {
const symbol = pos.symbol || "";
const exKeyAttr = esc(exchangeKey || exchangeId || "").replace(/"/g, """);
const side = (pos.side || "long").toLowerCase();
@@ -795,16 +922,17 @@
const mo = monitorOrder || {};
const cond = condOrdersFromPosition(pos);
const reg = Array.isArray(pos.regular_orders) ? pos.regular_orders : [];
- const guess = guessTpslFromCondOrders(side, cond);
+ const tpsl = resolvePositionTpsl(pos, mo, trendPlan);
const symAttr = esc(symbol).replace(/"/g, """);
const sideAttr = esc(side).replace(/"/g, """);
const contractsAttr = esc(String(pos.contracts != null ? pos.contracts : "")).replace(/"/g, """);
- const slAttr = esc(String(mo.stop_loss != null ? mo.stop_loss : guess.sl)).replace(/"/g, """);
- const tpAttr = esc(String(mo.take_profit != null ? mo.take_profit : guess.tp)).replace(/"/g, """);
- const entry = pos.entry_price != null ? pos.entry_price : mo.trigger_price;
- const sl = mo.stop_loss != null ? mo.stop_loss : guess.sl;
- const tp = mo.take_profit != null ? mo.take_profit : guess.tp;
- const rr = calcRrRatio(side, entry, sl, tp);
+ const slAttr = esc(String(tpsl.sl)).replace(/"/g, """);
+ const tpAttr = esc(String(tpsl.tp)).replace(/"/g, """);
+ const entry = tpsl.entry;
+ const sl = tpsl.sl;
+ const tp = tpsl.tp;
+ const tpMonitored = tpsl.tp_monitored;
+ const rr = tpMonitored ? null : calcRrRatio(side, entry, sl, tp);
const upnl = pos.unrealized_pnl;
let pnlText = fmt(upnl, 2) + "U";
if (pos.notional_usdt && upnl != null && Math.abs(Number(pos.notional_usdt)) > 1e-8) {
@@ -828,7 +956,7 @@
meta.push(
`移动保本:${beOn ? "开" : "关"}`
);
- const mktAttrs = marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder);
+ const mktAttrs = marketOpenBtnAttrs(exchangeId, exchangeKey, symbol, pos, monitorOrder, trendPlan);
return `
@@ -844,8 +972,8 @@
成交价${entry != null ? fmt(entry, 4) : "—"}
止损${sl != null && sl !== "" ? fmt(sl, 4) : "—"}
-
止盈${tp != null && tp !== "" ? fmt(tp, 4) : "—"}
-
盈亏比${rr != null ? fmt(rr, 2) + ":1" : "-:1"}
+
止盈${tpMonitored ? "程序监控" : tp != null && tp !== "" ? fmt(tp, 4) : "—"}
+
盈亏比${tpMonitored ? "—" : rr != null ? fmt(rr, 2) + ":1" : "-:1"}
张数${fmt(pos.contracts, 4)}
浮盈亏${pnlText}
@@ -933,17 +1061,17 @@
.join("");
}
- function renderPositionBlock(exchangeId, exchangeKey, x, monitorOrder) {
+ function renderPositionBlock(exchangeId, exchangeKey, x, monitorOrder, trendPlan) {
const symAttr = esc(x.symbol || "").replace(/"/g, """);
const exKeyAttr = esc(exchangeKey || exchangeId || "").replace(/"/g, """);
const sideAttr = esc((x.side || "").toLowerCase()).replace(/"/g, """);
const contractsAttr = esc(String(x.contracts != null ? x.contracts : "")).replace(/"/g, """);
const cond = condOrdersFromPosition(x);
const reg = Array.isArray(x.regular_orders) ? x.regular_orders : [];
- const guess = guessTpslFromCondOrders(x.side, cond);
- const slAttr = esc(String(guess.sl)).replace(/"/g, """);
- const tpAttr = esc(String(guess.tp)).replace(/"/g, """);
- const mktAttrs = marketOpenBtnAttrs(exchangeId, exchangeKey, x.symbol, x, monitorOrder);
+ const tpsl = resolvePositionTpsl(x, monitorOrder, trendPlan);
+ const slAttr = esc(String(tpsl.sl)).replace(/"/g, """);
+ const tpAttr = esc(String(tpsl.tp)).replace(/"/g, """);
+ const mktAttrs = marketOpenBtnAttrs(exchangeId, exchangeKey, x.symbol, x, monitorOrder, trendPlan);
return `