@@ -4,6 +4,8 @@
let monitorTimer = null ;
let authState = { required : false , logged _in : true } ;
let tpslPending = null ;
let lastMonitorRows = [ ] ;
let expandedExchangeId = sessionStorage . getItem ( "hub_expanded_ex" ) || "" ;
async function apiFetch ( url , opts ) {
const r = await fetch ( url , opts ) ;
@@ -113,62 +115,166 @@
gridEl . style . gridTemplateColumns = ` repeat( ${ cols } , minmax(0, 1fr)) ` ;
}
function normSym ( s ) {
return String ( s || "" )
. toUpperCase ( )
. replace ( /:USDT$/i , "" )
. replace ( /\/USDT:USDT$/i , "" )
. replace ( /\/USDT$/i , "" ) ;
}
function symbolsMatchHub ( a , b ) {
const x = normSym ( a ) ;
const y = normSym ( b ) ;
if ( ! x || ! y ) return false ;
return x === y ;
}
function ordersCollapseKey ( exchangeId , symbol ) {
const sym = normSym ( symbol ) || "unknown" ;
return ` hub_orders_ ${ exchangeId } _ ${ sym } ` ;
}
function isOrdersCollapseOpen ( exchangeId , symbol ) {
return localStorage . getItem ( ordersCollapseKey ( exchangeId , symbol ) ) === "1" ;
}
function findMonitorOrder ( orders , symbol , side ) {
const want = ( side || "" ) . toLowerCase ( ) ;
for ( const o of orders || [ ] ) {
const sym = o . exchange _symbol || o . symbol || "" ;
if ( ! symbolsMatchHub ( sym , symbol ) ) continue ;
const d = ( o . direction || "" ) . toLowerCase ( ) ;
if ( ! d || d === want ) return o ;
}
return null ;
}
function calcRrRatio ( side , entry , sl , tp ) {
const e = Number ( entry ) ;
const s = Number ( sl ) ;
const t = Number ( tp ) ;
if ( ! [ e , s , t ] . every ( ( n ) => Number . isFinite ( n ) && n > 0 ) ) return null ;
if ( ( side || "long" ) . toLowerCase ( ) === "short" ) {
const risk = s - e ;
const reward = e - t ;
if ( risk <= 0 || reward <= 0 ) return null ;
return reward / risk ;
}
const risk = e - s ;
const reward = t - e ;
if ( risk <= 0 || reward <= 0 ) return null ;
return reward / risk ;
}
async function loadMonitorBoard ( ) {
const box = document . getElementById ( "monitor-grid" ) ;
try {
const r = await apiFetch ( "/api/monitor/board" ) ;
const data = await r . json ( ) ;
const rows = data . rows || [ ] ;
const online = rows . filter ( ( x ) => x . http _ok && ( x . agent || { } ) . ok !== false ) . length ;
lastMonitorRows = data . rows || [ ] ;
const online = lastMonitorRows . filter (
( x ) => x . http _ok && ( x . agent || { } ) . ok !== false
) . length ;
const pill = document . getElementById ( "sys-status" ) ;
if ( pill ) {
pill . textContent = rows . length ? ` LINK ${ online } / ${ rows . length } ` : "NO DATA" ;
pill . classList . toggle ( "warn" , rows . length && online < rows . length ) ;
pill . textContent = lastMonitorRows . length
? ` LINK ${ online } / ${ lastMonitorRows . length } `
: "NO DATA" ;
pill . classList . toggle ( "warn" , lastMonitorRows . length && online < lastMonitorRows . length ) ;
}
document . getElementById ( "monitor-updated" ) . textContent =
"UPD " + ( data . updated _at || "" ) . replace ( "T" , " " ) ;
const parts = rows . map ( renderMonitorCard ) ;
box . innerHTML = parts . join ( "" ) || '<div class="err">无已启用账户</div>' ;
syncMonitorGridColumns ( box , rows . length ) ;
box . querySelectorAll ( ".btn-close-ex" ) . forEach ( ( btn ) => {
btn . onclick = ( ) => closeOne ( btn . dataset . id ) ;
} ) ;
box . querySelectorAll ( ".btn-close-pos" ) . forEach ( ( btn ) => {
btn . onclick = ( ) =>
closeOnePosition ( btn . dataset . exId , btn . dataset . symbol , btn . dataset . side ) ;
} ) ;
box . querySelectorAll ( ".btn-cancel-order" ) . forEach ( ( btn ) => {
btn . onclick = ( ) =>
cancelOneOrder (
btn . dataset . exId ,
btn . dataset . symbol ,
btn . dataset . orderId ,
btn . dataset . channel
) ;
} ) ;
box . querySelectorAll ( ".btn-cancel-cond-all" ) . forEach ( ( btn ) => {
btn . onclick = ( ev ) => {
ev . preventDefault ( ) ;
ev . stopPropagation ( ) ;
cancelSymbolOrders ( btn . dataset . exId , btn . dataset . symbol , "conditional" ) ;
} ;
} ) ;
box . querySelectorAll ( ".btn-place-tpsl" ) . forEach ( ( btn ) => {
btn . onclick = ( ) =>
openTpslModal (
btn . dataset . exId ,
btn . dataset . symbol ,
btn . dataset . side ,
btn . dataset . contracts ,
btn . dataset . sl || "" ,
btn . dataset . tp || ""
) ;
} ) ;
renderMonitorGrid ( lastMonitorRows ) ;
} catch ( e ) {
box . innerHTML = ` <div class="err"> ${ esc ( e ) } </div> ` ;
}
}
function renderMonitorGrid ( rows ) {
const box = document . getElementById ( "monitor-grid" ) ;
if ( ! box ) return ;
if ( expandedExchangeId && ! rows . some ( ( r ) => String ( r . id ) === String ( expandedExchangeId ) ) ) {
expandedExchangeId = "" ;
sessionStorage . removeItem ( "hub_expanded_ex" ) ;
}
const visible = expandedExchangeId
? rows . filter ( ( r ) => String ( r . id ) === String ( expandedExchangeId ) )
: rows ;
box . classList . toggle ( "grid-monitor-expanded" , ! ! expandedExchangeId ) ;
const parts = visible . map ( ( r ) => renderMonitorCard ( r , ! ! expandedExchangeId ) ) ;
box . innerHTML = parts . join ( "" ) || '<div class="err">无已启用账户</div>' ;
if ( ! expandedExchangeId ) syncMonitorGridColumns ( box , rows . length ) ;
bindMonitorInteractions ( box ) ;
}
function bindMonitorInteractions ( box ) {
box . querySelectorAll ( ".btn-close-ex" ) . forEach ( ( btn ) => {
btn . onclick = ( ) => closeOne ( btn . dataset . id ) ;
} ) ;
box . querySelectorAll ( ".btn-close-pos" ) . forEach ( ( btn ) => {
btn . onclick = ( ev ) => {
ev . stopPropagation ( ) ;
closeOnePosition ( btn . dataset . exId , btn . dataset . symbol , btn . dataset . side ) ;
} ;
} ) ;
box . querySelectorAll ( ".btn-cancel-order" ) . forEach ( ( btn ) => {
btn . onclick = ( ev ) => {
ev . stopPropagation ( ) ;
cancelOneOrder (
btn . dataset . exId ,
btn . dataset . symbol ,
btn . dataset . orderId ,
btn . dataset . channel
) ;
} ;
} ) ;
box . querySelectorAll ( ".btn-cancel-cond-all" ) . forEach ( ( btn ) => {
btn . onclick = ( ev ) => {
ev . preventDefault ( ) ;
ev . stopPropagation ( ) ;
cancelSymbolOrders ( btn . dataset . exId , btn . dataset . symbol , "conditional" ) ;
} ;
} ) ;
box . querySelectorAll ( ".btn-place-tpsl" ) . forEach ( ( btn ) => {
btn . onclick = ( ev ) => {
ev . stopPropagation ( ) ;
openTpslModal (
btn . dataset . exId ,
btn . dataset . symbol ,
btn . dataset . side ,
btn . dataset . contracts ,
btn . dataset . sl || "" ,
btn . dataset . tp || ""
) ;
} ;
} ) ;
box . querySelectorAll ( ".btn-expand-back" ) . forEach ( ( btn ) => {
btn . onclick = ( ev ) => {
ev . stopPropagation ( ) ;
expandedExchangeId = "" ;
sessionStorage . removeItem ( "hub_expanded_ex" ) ;
renderMonitorGrid ( lastMonitorRows ) ;
} ;
} ) ;
box . querySelectorAll ( ".card-expand-hit" ) . forEach ( ( hit ) => {
hit . onclick = ( ev ) => {
if ( ev . target . closest ( "a, button, input, summary, .pos-orders-collapse" ) ) return ;
const id = hit . closest ( ".card" ) ? . dataset . exId ;
if ( ! id || expandedExchangeId ) return ;
expandedExchangeId = id ;
sessionStorage . setItem ( "hub_expanded_ex" , id ) ;
renderMonitorGrid ( lastMonitorRows ) ;
} ;
} ) ;
box . querySelectorAll ( "details.pos-orders-collapse[data-collapse-key]" ) . forEach ( ( el ) => {
el . addEventListener ( "toggle" , ( ) => {
const k = el . dataset . collapseKey ;
if ( k ) localStorage . setItem ( k , el . open ? "1" : "0" ) ;
} ) ;
} ) ;
}
function renderOrderRows ( exchangeId , symbol , orders , kind ) {
if ( ! orders || ! orders . length ) {
const hint =
@@ -209,38 +315,18 @@
return { sl : triggers [ 0 ] , tp : triggers [ triggers . length - 1 ] } ;
}
function renderPositionBlock ( exchangeId , x ) {
const symAttr = esc ( x . symbol || "" ) . replace ( /"/g , """ ) ;
const sideAttr = esc ( ( x . side || "" ) . toLowerCase ( ) ) . replace ( /"/g , """ ) ;
const contractsAttr = esc ( String ( x . contracts != null ? x . contracts : "" ) ) . replace ( /"/g , """ ) ;
const cond = Array . isArray ( x . conditional _orders ) ? x . conditional _orders : [ ] ;
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 , """ ) ;
function renderOrdersCollapse ( exchangeId , symbol , cond , reg ) {
const symAttr = esc ( symbol || "" ) . replace ( /"/g , """ ) ;
const orderTotal = cond . length + reg . length ;
const collapseKey = ordersCollapseKey ( exchangeId , symbol ) ;
const openAttr = isOrdersCollapseOpen ( exchangeId , symbol ) ? " open" : "" ;
const condAllBtn =
cond . length > 0
? ` <button type="button" class="btn-cancel-cond-all btn-sm ghost" data-ex-id=" ${ esc ( exchangeId ) } " data-symbol=" ${ symAttr } ">撤销条件单</button> `
: "" ;
const condBody = renderOrderRows ( exchangeId , x . symbol , cond , "conditional" ) ;
const regBody = renderOrderRows ( exchangeId , x . symbol , reg , "limit" ) ;
return ` <div class="pos-block ">
< table class = "data-table" > < thead > < tr > < th > 合约 < / t h > < t h > 方 向 < / t h > < t h > 张 数 < / t h > < t h > 浮 盈 < / t h > < t h > 操 作 < / t h > < / t r > < / t h e a d > < t b o d y >
< tr >
< td > $ { esc ( x . symbol ) } < / t d >
< td > $ { esc ( x . side ) } < / t d >
< td > $ { fmt ( x . contracts , 4 ) } < / t d >
< td class = "${pnlCls(x.unrealized_pnl)}" > $ { fmt ( x . unrealized _pnl , 4 ) } < / t d >
< td class = "td-actions" >
< div class = "pos-action-group" >
< button type = "button" class = "btn-place-tpsl btn-sm ghost" data - ex - id = "${esc(exchangeId)}" data - symbol = "${symAttr}" data - side = "${sideAttr}" data - contracts = "${contractsAttr}" data - sl = "${slAttr}" data - tp = "${tpAttr}" > 委托 < / b u t t o n >
< button type = "button" class = "btn-close-pos btn-sm danger" data - ex - id = "${esc(exchangeId)}" data - symbol = "${symAttr}" data - side = "${sideAttr}" > 平仓 < / b u t t o n >
< / d i v >
< / t d >
< / t r >
< / t b o d y > < / t a b l e >
< details class = "pos-orders-collapse" >
const condBody = renderOrderRows ( exchangeId , symbol , cond , "conditional" ) ;
const regBody = renderOrderRows ( exchangeId , symbol , reg , "limit" ) ;
return ` <details class="pos-orders-collapse" ${ openAttr } data-collapse-key=" ${ esc ( collapseKey ) } ">
< summary class = "pos-orders-collapse-summary" >
< span class = "pos-orders-collapse-label" > 委托单 < em > $ { orderTotal } < / e m > < / s p a n >
< span class = "pos-orders-collapse-meta" > 条件 $ { cond . length } · 普通 $ { reg . length } < / s p a n >
@@ -256,10 +342,198 @@
$ { regBody }
< / d i v >
< / d i v >
< / d e t a i l s >
< / d e t a i l s > ` ;
}
function renderExTpslRows ( exchangeId , symbol , cond ) {
const symAttr = esc ( symbol || "" ) . replace ( /"/g , """ ) ;
const sl = cond . find ( ( o ) => ( o . label || "" ) . includes ( "止损" ) ) ;
const tp = cond . find ( ( o ) => ( o . label || "" ) . includes ( "止盈" ) ) ;
function row ( label , o ) {
if ( ! o ) {
return ` <div class="pos-ex-order-row"><span class="pos-ex-order-main"> ${ label } :—</span></div> ` ;
}
const oid = esc ( o . id || "" ) . replace ( /"/g , """ ) ;
const ch = esc ( o . channel || "regular" ) . replace ( /"/g , """ ) ;
const trig = o . trigger _price != null ? fmt ( o . trigger _price , 4 ) : "—" ;
return ` <div class="pos-ex-order-row">
< span class = "pos-ex-order-main" > $ { label } : 触发 $ { trig } · 数量 $ { fmt ( o . amount , 4 ) } < / s p a n >
< button type = "button" class = "pos-ex-cancel-btn btn-cancel-order" data - ex - id = "${esc(exchangeId)}" data - symbol = "${symAttr}" data - order - id = "${oid}" data - channel = "${ch}" > 撤单 < / b u t t o n >
< / d i v > ` ;
}
return row ( "止损" , sl ) + row ( "止盈" , tp ) ;
}
function renderLivePositionCard ( exchangeId , pos , monitorOrder ) {
const symbol = pos . symbol || "" ;
const side = ( pos . side || "long" ) . toLowerCase ( ) ;
const sideCn = side === "long" ? "做多" : "做空" ;
const sideCls = side === "long" ? "pos-side-long" : "pos-side-short" ;
const mo = monitorOrder || { } ;
const cond = Array . isArray ( pos . conditional _orders ) ? pos . conditional _orders : [ ] ;
const reg = Array . isArray ( pos . regular _orders ) ? pos . regular _orders : [ ] ;
const guess = guessTpslFromCondOrders ( side , cond ) ;
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 upnl = pos . unrealized _pnl ;
let pnlText = fmt ( upnl , 2 ) + "U" ;
if ( pos . notional _usdt && upnl != null && Math . abs ( Number ( pos . notional _usdt ) ) > 1e-8 ) {
const pct = ( Number ( upnl ) / Math . abs ( Number ( pos . notional _usdt ) ) ) * 100 ;
pnlText += ` ( ${ pct >= 0 ? "" : "" } ${ pct . toFixed ( 2 ) } %) ` ;
}
const meta = [ ] ;
if ( mo . monitor _type || mo . key _signal _type ) {
meta . push (
` 来源: ${ esc ( mo . monitor _type || "下单监控" ) } ${ mo . key _signal _type ? " · " + esc ( mo . key _signal _type ) : "" } `
) ;
} else {
meta . push ( "来源: 交易所持仓" ) ;
}
if ( mo . trade _style ) meta . push ( ` 风格: ${ esc ( mo . trade _style ) } ` ) ;
else meta . push ( "风格: —" ) ;
if ( mo . risk _percent != null ) {
meta . push ( ` 风险: ${ esc ( mo . risk _percent ) } % ` ) ;
}
const beOn = mo . breakeven _enabled === 1 || mo . breakeven _enabled === true ;
meta . push (
` <span class=" ${ beOn ? "pos-meta-on" : "pos-meta-off" } ">移动保本: ${ beOn ? "开" : "关" } </span> `
) ;
return ` <div class="pos-card hub-pos-card">
< div class = "pos-card-head" >
< div class = "pos-card-symbol" >
< strong > $ { esc ( symbol ) } < / s t r o n g >
< span class = "pos-side-badge ${sideCls}" > $ { sideCn } < / s p a n >
< / d i v >
< div class = "pos-head-actions" >
< button type = "button" class = "pos-entrust-btn btn-place-tpsl" data - ex - id = "${esc(exchangeId)}" data - symbol = "${symAttr}" data - side = "${sideAttr}" data - contracts = "${contractsAttr}" data - sl = "${slAttr}" data - tp = "${tpAttr}" > 委托 < / b u t t o n >
< button type = "button" class = "pos-close-btn btn-close-pos" data - ex - id = "${esc(exchangeId)}" data - symbol = "${symAttr}" data - side = "${sideAttr}" > 平仓 < / b u t t o n >
< / d i v >
< / d i v >
< div class = "pos-meta" > $ { meta . map ( ( m ) => ` <span class="pos-meta-item"> ${ m } </span> ` ) . join ( "" ) } < / d i v >
< div class = "pos-grid" >
< div class = "pos-cell" > < span class = "pos-label" > 成交价 < / s p a n > < s p a n c l a s s = " p o s - v a l u e " > $ { e n t r y ! = n u l l ? f m t ( e n t r y , 4 ) : " — " } < / s p a n > < / d i v >
< div class = "pos-cell" > < span class = "pos-label" > 止损 < / s p a n > < s p a n c l a s s = " p o s - v a l u e " > $ { s l ! = n u l l & & s l ! = = " " ? f m t ( s l , 4 ) : " — " } < / s p a n > < / d i v >
< div class = "pos-cell" > < span class = "pos-label" > 止盈 < / s p a n > < s p a n c l a s s = " p o s - v a l u e " > $ { t p ! = n u l l & & t p ! = = " " ? f m t ( t p , 4 ) : " — " } < / s p a n > < / d i v >
< div class = "pos-cell" > < span class = "pos-label" > 盈亏比 < / s p a n > < s p a n c l a s s = " p o s - v a l u e " > $ { r r ! = n u l l ? f m t ( r r , 2 ) + " : 1 " : " - : 1 " } < / s p a n > < / d i v >
< div class = "pos-cell" > < span class = "pos-label" > 张数 < / s p a n > < s p a n c l a s s = " p o s - v a l u e " > $ { f m t ( p o s . c o n t r a c t s , 4 ) } < / s p a n > < / d i v >
< div class = "pos-cell" > < span class = "pos-label" > 浮盈亏 < / s p a n > < s p a n c l a s s = " p o s - v a l u e $ { p n l C l s ( u p n l ) } " > $ { p n l T e x t } < / s p a n > < / d i v >
< / d i v >
< div class = "pos-footer" >
< span > 杠杆 : $ { mo . leverage != null ? esc ( mo . leverage ) + "x" : "—" } < / s p a n >
< span > 计划基数 : $ { mo . margin _capital != null ? fmt ( mo . margin _capital , 2 ) + "U" : "—" } < / s p a n >
< span > 仓位占比 : $ { mo . position _ratio != null ? esc ( mo . position _ratio ) + "%" : "—" } < / s p a n >
< / d i v >
< div class = "pos-ex-orders" >
< div class = "pos-ex-orders-title" > 交易所止盈止损 < / d i v >
$ { renderExTpslRows ( exchangeId , symbol , cond ) }
< / d i v >
$ { renderOrdersCollapse ( exchangeId , symbol , cond , reg ) }
< / d i v > ` ;
}
function renderHubSectionCard ( title , bodyHtml , emptyHint ) {
const inner = bodyHtml || ` <div class="pos-empty"> ${ esc ( emptyHint || "暂无" ) } </div> ` ;
return ` <div class="hub-section-card">
< div class = "hub-section-head" > $ { esc ( title ) } < / d i v >
< div class = "hub-section-body" > $ { inner } < / d i v >
< / d i v > ` ;
}
function renderKeySection ( keys , kmap ) {
if ( ! keys . length ) return "" ;
return keys
. map ( ( k ) => {
const kp = kmap [ k . id ] || kmap [ String ( k . id ) ] || { } ;
const mt = k . monitor _type || k . type || "" ;
return ` <div class="hub-mini-card">
< div class = "hub-mini-title" > $ { esc ( k . symbol ) } · $ { esc ( mt ) } < / d i v >
< div class = "hub-mini-line" > 上沿 $ { esc ( k . upper ) } / 下沿 $ { esc ( k . lower ) } < / d i v >
< div class = "hub-mini-line" > $ { esc ( kp . gate _summary || kp . price _display || kp . price || "—" ) } < / d i v >
< / d i v > ` ;
} )
. join ( "" ) ;
}
function renderStrategySection ( orders , trends ) {
const parts = [ ] ;
( orders || [ ] ) . forEach ( ( o ) => {
parts . push ( ` <div class="hub-mini-card">
< div class = "hub-mini-title" > 下单监控 # $ { esc ( o . id ) } · $ { esc ( o . symbol || o . exchange _symbol ) } < / d i v >
< div class = "hub-mini-line" > $ { esc ( o . direction ) } · 触发 $ { fmt ( o . trigger _price , 4 ) } · SL $ { fmt ( o . stop _loss , 4 ) } · TP $ { fmt ( o . take _profit , 4 ) } < / d i v >
< / d i v > ` ) ;
} ) ;
( trends || [ ] ) . forEach ( ( t ) => {
parts . push ( ` <div class="hub-mini-card">
< div class = "hub-mini-title" > 趋势计划 # $ { esc ( t . id ) } · $ { esc ( t . symbol ) } < / d i v >
< div class = "hub-mini-line" > $ { esc ( t . direction ) } · SL $ { fmt ( t . stop _loss , 4 ) } · TP $ { fmt ( t . take _profit , 4 ) } < / d i v >
< / d i v > ` ;
} ) ;
return parts . join ( "" ) ;
}
function renderCompactBody ( row , ag , pos , hm , flaskOk , keys , orders , trends , kmap ) {
let inner = ` <div class="stat-row">
< div class = "stat-box" > < div class = "stat-label" > 余额 < / d i v > < d i v c l a s s = " s t a t - v a l u e " > $ { f m t ( a g . b a l a n c e _ u s d t , 2 ) } < s m a l l s t y l e = " f o n t - s i z e : 1 2 p x ; c o l o r : v a r ( - - m u t e d ) " > U < / s m a l l > < / d i v > < / d i v >
< div class = "stat-box" > < div class = "stat-label" > 浮盈合计 < / d i v > < d i v c l a s s = " s t a t - v a l u e $ { p n l C l s ( a g . t o t a l _ u n r e a l i z e d _ p n l ) } " > $ { f m t ( a g . t o t a l _ u n r e a l i z e d _ p n l , 4 ) } < / d i v > < / d i v >
< / d i v > ` ;
inner += ` <div class="section-title">持仓 · ${ pos . length } </div> ` ;
if ( pos . length ) {
inner += '<div class="compact-pos-list">' ;
pos . forEach ( ( p ) => {
inner += ` <div class="compact-pos-line"><span> ${ esc ( p . symbol ) } · ${ esc ( p . side ) } </span><span class=" ${ pnlCls ( p . unrealized _pnl ) } "> ${ fmt ( p . unrealized _pnl , 4 ) } </span></div> ` ;
} ) ;
inner += "</div>" ;
} else {
inner += '<div class="empty-hint">无持仓</div>' ;
}
const keyN = ( row . capabilities || [ ] ) . includes ( "key" ) ? keys . length : 0 ;
const stratN = orders . length + ( ( row . capabilities || [ ] ) . includes ( "trend" ) ? trends . length : 0 ) ;
inner += ` <div class="card-expand-hint">点击卡片展开 · 持仓详情 / 关键位 ${ keyN } / 策略 ${ stratN } </div> ` ;
return inner ;
}
function renderExpandedBody ( row , ag , pos , hm , flaskOk , keys , orders , trends , kmap ) {
let inner = ` <div class="stat-row">
< div class = "stat-box" > < div class = "stat-label" > 余额 < / d i v > < d i v c l a s s = " s t a t - v a l u e " > $ { f m t ( a g . b a l a n c e _ u s d t , 2 ) } < s m a l l s t y l e = " f o n t - s i z e : 1 2 p x ; c o l o r : v a r ( - - m u t e d ) " > U < / s m a l l > < / d i v > < / d i v >
< div class = "stat-box" > < div class = "stat-label" > 浮盈合计 < / d i v > < d i v c l a s s = " s t a t - v a l u e $ { p n l C l s ( a g . t o t a l _ u n r e a l i z e d _ p n l ) } " > $ { f m t ( a g . t o t a l _ u n r e a l i z e d _ p n l , 4 ) } < / d i v > < / d i v >
< / d i v > ` ;
inner += '<div class="hub-pos-list">' ;
if ( pos . length ) {
pos . forEach ( ( p ) => {
const mo = findMonitorOrder ( orders , p . symbol , p . side ) ;
inner += renderLivePositionCard ( row . id , p , mo ) ;
} ) ;
} else {
inner += '<div class="pos-empty">暂无持仓</div>' ;
}
inner += "</div>" ;
if ( ( row . capabilities || [ ] ) . includes ( "key" ) ) {
if ( ! flaskOk ) {
const fe = row . flask _error || hm . msg || hm . error || "策略 Flask 未连通" ;
inner += renderHubSectionCard ( "关键位" , ` <div class="err"> ${ esc ( fe ) } </div> ` , "" ) ;
} else {
inner += renderHubSectionCard ( "关键位" , renderKeySection ( keys , kmap ) , "当前无关键位记录" ) ;
}
}
const showStrategy =
orders . length || ( ( row . capabilities || [ ] ) . includes ( "trend" ) && trends . length ) ;
if ( showStrategy ) {
inner += renderHubSectionCard (
"策略" ,
renderStrategySection ( orders , ( row . capabilities || [ ] ) . includes ( "trend" ) ? trends : [ ] ) ,
"当前无策略记录"
) ;
}
return inner ;
}
function openTpslModal ( exchangeId , symbol , side , contracts , slHint , tpHint ) {
tpslPending = {
exchangeId ,
@@ -398,7 +672,7 @@
}
}
function renderMonitorCard ( row ) {
function renderMonitorCard ( row , expanded ) {
const ag = row . agent || { } ;
const pos = Array . isArray ( ag . positions ) ? ag . positions : [ ] ;
const hm = row . hub _monitor || { } ;
@@ -418,54 +692,10 @@
} else if ( ! agOk ) {
inner = ` <div class="err"> ${ esc ( agErr || "子代理返回失败" ) } </div> ` ;
inner += ` <div class="empty-hint">请检查 PM2 子代理与 <code> ${ esc ( row . agent _url || "" ) } /status</code></div> ` ;
} else if ( expanded ) {
inner = renderExpandedBody ( row , ag , pos , hm , flaskOk , keys , orders , trends , kmap ) ;
} else {
inner = ` <div class="stat-row">
< div class = "stat-box" > < div class = "stat-label" > 余额 < / d i v > < d i v c l a s s = " s t a t - v a l u e " > $ { f m t ( a g . b a l a n c e _ u s d t , 2 ) } < s m a l l s t y l e = " f o n t - s i z e : 1 2 p x ; c o l o r : v a r ( - - m u t e d ) " > U < / s m a l l > < / d i v > < / d i v >
< div class = "stat-box" > < div class = "stat-label" > 浮盈合计 < / d i v > < d i v c l a s s = " s t a t - v a l u e $ { p n l C l s ( a g . t o t a l _ u n r e a l i z e d _ p n l ) } " > $ { f m t ( a g . t o t a l _ u n r e a l i z e d _ p n l , 4 ) } < / d i v > < / d i v >
< / d i v > ` ;
inner += ` <div class="section-title">交易所持仓</div> ` ;
if ( pos . length ) {
inner += pos . map ( ( x ) => renderPositionBlock ( row . id , x ) ) . join ( "" ) ;
} else {
inner += ` <div class="empty-hint">无持仓</div> ` ;
}
if ( orders . length ) {
inner += ` <div class="section-title">机器人单 · ${ orders . length } </div> ` ;
orders . forEach ( ( o ) => {
inner += ` <div class="list-line"> ${ esc ( o . symbol ) } · ${ esc ( o . direction ) } · 触发 ${ o . trigger _price } </div> ` ;
} ) ;
}
if ( ( row . capabilities || [ ] ) . includes ( "key" ) ) {
inner += ` <div class="section-title">关键位</div> ` ;
if ( ! flaskOk ) {
const fe = row . flask _error || hm . msg || hm . error || "" ;
const short =
fe ||
( hm . status === 404
? "HTTP 404:请重启各 crypto_* Flask"
: "策略 Flask 未连通" ) ;
inner += ` <div class="err"> ${ esc ( short ) } </div> ` ;
} else if ( ! keys . length ) {
inner += ` <div class="empty-hint">当前无记录</div> ` ;
} else {
keys . slice ( 0 , 8 ) . forEach ( ( k ) => {
const kp = kmap [ k . id ] || kmap [ String ( k . id ) ] || { } ;
const mt = k . monitor _type || k . type || "" ;
let line = ` ${ esc ( k . symbol ) } · ${ esc ( mt ) } · ${ k . upper } / ${ k . lower } ` ;
if ( kp . price _display != null || kp . price != null ) {
line += ` · ${ esc ( kp . price _display != null ? kp . price _display : kp . price ) } ` ;
}
line += ` · ${ esc ( kp . gate _summary || "-" ) } ` ;
inner += ` <div class="list-line"> ${ line } </div> ` ;
} ) ;
}
}
if ( ( row . capabilities || [ ] ) . includes ( "trend" ) && trends . length ) {
inner += ` <div class="section-title">趋势计划 · ${ trends . length } </div> ` ;
trends . forEach ( ( t ) => {
inner += ` <div class="list-line"># ${ t . id } ${ esc ( t . symbol ) } ${ t . direction } · SL ${ t . stop _loss } · TP ${ t . take _profit } </div> ` ;
} ) ;
}
inner = renderCompactBody ( row , ag , pos , hm , flaskOk , keys , orders , trends , kmap ) ;
}
const online = row . http _ok && agOk ;
const cardCls = online ? "card-online" : "card-offline" ;
@@ -477,7 +707,11 @@
const openFlask = flaskOpen
? ` <a class="btn-link" href=" ${ esc ( flaskOpen ) } " target="_blank" rel="noopener">实例</a> `
: "" ;
return ` <div class="card ${ cardCls } ">
const backBtn = expanded
? ` <button type="button" class="ghost btn-expand-back">返回</button> `
: "" ;
const expandHit = ! expanded && online ? " card-expand-hit" : "" ;
return ` <div class="card ${ cardCls } ${ expanded ? " card-expanded" : "" } " data-ex-id=" ${ esc ( row . id ) } ">
< div class = "card-head" >
< div >
< div class = "card-title-row" >
@@ -487,12 +721,13 @@
< div class = "card-sub" > $ { esc ( flaskOpen || "" ) } < / d i v >
< / d i v >
< div class = "card-actions" >
$ { backBtn }
$ { openFlask }
$ { review }
< button type = "button" class = "danger btn-close-ex" data - id = "${esc(row.id)}" > 全平 < / b u t t o n >
< / d i v >
< / d i v >
< div class = "card-body" > $ { inner } < / d i v >
< div class = "card-body${expandHit} " > $ { inner } < / d i v >
< / d i v > ` ;
}