e5a586f903
Move business code under modules/, env template to config/, PM2 single qihuo process, and _legacy shims for old imports. Co-authored-by: Cursor <cursoragent@cursor.com>
92 lines
4.3 KiB
JavaScript
92 lines
4.3 KiB
JavaScript
/* Copyright (c) 2025-2026 马建军. All rights reserved.
|
|
* 专有软件 — 未经授权禁止复制、传播、转售。
|
|
* 详见 LICENSE.zh-CN.txt
|
|
*/
|
|
(function () {
|
|
var posTimer = null;
|
|
|
|
function fmtNum(v, digits) {
|
|
if (v === null || v === undefined) return '--';
|
|
return Number(v).toFixed(digits === undefined ? 2 : digits);
|
|
}
|
|
|
|
function buildPosCard(row) {
|
|
var pnlClass = '';
|
|
if (row.float_pnl > 0) pnlClass = 'pnl-pos';
|
|
if (row.float_pnl < 0) pnlClass = 'pnl-neg';
|
|
var pnlText = '--';
|
|
if (row.float_pnl != null) {
|
|
var sign = row.float_pnl >= 0 ? '+' : '';
|
|
pnlText = sign + fmtNum(row.float_pnl) + '元';
|
|
if (row.float_pct != null) {
|
|
pnlText += ' (' + sign + fmtNum(row.float_pct) + '%)';
|
|
}
|
|
}
|
|
var rr = row.rr_ratio != null ? row.rr_ratio + ':1' : '--';
|
|
var openT = (row.open_time || '').replace('T', ' ').slice(0, 16);
|
|
|
|
return (
|
|
'<div class="pos-card" data-pos-id="' + row.id + '">' +
|
|
'<div class="pos-card-head">' +
|
|
'<div><div class="title">' + row.symbol + ' <span class="badge dir">' + row.direction + '</span></div></div>' +
|
|
'<form method="post" action="/close_position/' + row.id + '" style="display:inline" onsubmit="return confirm(\'确认平仓?\')">' +
|
|
'<button type="submit" class="btn-del pos-del">平仓</button></form>' +
|
|
'</div>' +
|
|
'<div class="pos-card-meta">来源 <strong>手动输入</strong> · 风险 <strong>' +
|
|
fmtNum(row.risk_pct) + '%≈' + fmtNum(row.risk_amount) + '元</strong></div>' +
|
|
'<div class="pos-metrics">' +
|
|
'<div class="cell"><label>成交价</label><div>' + fmtNum(row.entry_price) + '</div></div>' +
|
|
'<div class="cell"><label>止损</label><div>' + fmtNum(row.stop_loss) + '</div></div>' +
|
|
'<div class="cell"><label>止盈</label><div>' + fmtNum(row.take_profit) + '</div></div>' +
|
|
'<div class="cell"><label>盈亏比</label><div>' + rr + '</div></div>' +
|
|
'<div class="cell"><label>标记价</label><div>' + (row.mark_price != null ? fmtNum(row.mark_price) : '--') + '</div></div>' +
|
|
'<div class="cell ' + pnlClass + '"><label>浮盈亏</label><div>' + pnlText + '</div></div>' +
|
|
'<div class="cell"><label>预估手续费</label><div>' + fmtNum(row.est_fee) + '元</div></div>' +
|
|
'<div class="cell ' + (row.est_pnl_net > 0 ? 'pnl-pos' : (row.est_pnl_net < 0 ? 'pnl-neg' : '')) + '">' +
|
|
'<label>扣费后</label><div>' + (row.est_pnl_net != null ? fmtNum(row.est_pnl_net) + '元' : '--') + '</div></div>' +
|
|
'</div>' +
|
|
'<div class="pos-footer">' +
|
|
'<span>保证金 ' + fmtNum(row.margin) + '元</span>' +
|
|
'<span>仓位占比 ' + fmtNum(row.position_pct) + '%</span>' +
|
|
'<span>开仓 ' + (openT || '--') + '</span>' +
|
|
'<span>持仓 ' + (row.holding_duration || '--') + '</span>' +
|
|
'<span>张数 ' + row.lots + '</span>' +
|
|
'<span>手续费(估) ' + fmtNum(row.est_fee) + '元 (' + (row.est_fee_close_type || '') + ')</span>' +
|
|
'</div></div>'
|
|
);
|
|
}
|
|
|
|
function pollPositions() {
|
|
var list = document.getElementById('position-live-list');
|
|
if (!list) return;
|
|
|
|
fetch('/api/position_live')
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (rows) {
|
|
if (!rows.length) {
|
|
list.innerHTML = '<div class="empty-hint">暂无持仓,左侧录入后显示</div>';
|
|
return;
|
|
}
|
|
list.innerHTML = rows.map(buildPosCard).join('');
|
|
})
|
|
.catch(function () { /* ignore */ });
|
|
}
|
|
|
|
function stopPolling() {
|
|
if (posTimer) {
|
|
clearInterval(posTimer);
|
|
posTimer = null;
|
|
}
|
|
}
|
|
|
|
function startPolling() {
|
|
stopPolling();
|
|
pollPositions();
|
|
posTimer = setInterval(pollPositions, 1000);
|
|
}
|
|
|
|
if (window.qihuoOnPageLoad) window.qihuoOnPageLoad(startPolling);
|
|
else document.addEventListener('DOMContentLoaded', startPolling);
|
|
if (window.qihuoOnPageLeave) window.qihuoOnPageLeave(stopPolling);
|
|
})();
|