Restructure into modules/ with single-process CTP and config/ layout.
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>
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
/* Copyright (c) 2025-2026 马建军. All rights reserved.
|
||||
* 专有软件 — 未经授权禁止复制、传播、转售。
|
||||
* 详见 LICENSE.zh-CN.txt
|
||||
*/
|
||||
(function () {
|
||||
var cache = null;
|
||||
|
||||
function fmtNum(v, suffix) {
|
||||
if (v === null || v === undefined || v === '') return '-';
|
||||
var n = Number(v);
|
||||
if (isNaN(n)) return String(v);
|
||||
var s = Number.isInteger(n) ? String(n) : n.toFixed(2);
|
||||
return suffix ? s + suffix : s;
|
||||
}
|
||||
|
||||
function fmtMoney(v) {
|
||||
if (v === null || v === undefined) return '-';
|
||||
return fmtNum(v) + ' 元';
|
||||
}
|
||||
|
||||
function fmtPct(v) {
|
||||
if (v === null || v === undefined) return '-';
|
||||
return fmtNum(v) + '%';
|
||||
}
|
||||
|
||||
function setSummary(s) {
|
||||
var map = {
|
||||
total_trades: function () { return fmtNum(s.total_trades); },
|
||||
win_rate: function () { return fmtPct(s.win_rate); },
|
||||
avg_profit: function () { return fmtMoney(s.avg_profit); },
|
||||
avg_loss: function () { return fmtMoney(s.avg_loss); },
|
||||
profit_loss_ratio: function () { return fmtNum(s.profit_loss_ratio); },
|
||||
consecutive_losses: function () { return fmtNum(s.consecutive_losses); },
|
||||
max_drawdown: function () {
|
||||
var amt = fmtMoney(s.max_drawdown);
|
||||
var pct = s.max_drawdown_pct ? ' (' + fmtPct(s.max_drawdown_pct) + ')' : '';
|
||||
return amt + pct;
|
||||
},
|
||||
max_loss_amount: function () { return fmtMoney(s.max_loss_amount); },
|
||||
max_loss_pct: function () { return fmtPct(s.max_loss_pct); },
|
||||
max_profit_amount: function () { return fmtMoney(s.max_profit_amount); },
|
||||
max_profit_pct: function () { return fmtPct(s.max_profit_pct); },
|
||||
total_fee: function () { return fmtMoney(s.total_fee); },
|
||||
emotion_count: function () { return fmtNum(s.emotion_count); },
|
||||
emotion_ratio: function () { return fmtPct(s.emotion_ratio); },
|
||||
};
|
||||
document.querySelectorAll('#stats-summary [data-k]').forEach(function (el) {
|
||||
var key = el.getAttribute('data-k');
|
||||
el.textContent = map[key] ? map[key]() : '-';
|
||||
});
|
||||
}
|
||||
|
||||
function fillViewSelect(views, selected) {
|
||||
var sel = document.getElementById('stats-view-select');
|
||||
if (!sel) return;
|
||||
sel.innerHTML = '';
|
||||
views.forEach(function (v) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = v.key;
|
||||
opt.textContent = v.label;
|
||||
if (v.key === selected) opt.selected = true;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function cellClass(key, val) {
|
||||
if (key === 'max_loss' || key === 'max_profit') {
|
||||
if (val === 0) return 'text-muted';
|
||||
}
|
||||
if (key === 'total_net' || key === 'max_profit' || key === 'avg_profit') {
|
||||
if (val > 0) return 'text-profit';
|
||||
if (val < 0) return 'text-loss';
|
||||
}
|
||||
if (key === 'max_loss' || key === 'avg_loss' || key === 'total_fee') {
|
||||
if (val < 0) return 'text-loss';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function fmtBreakdownVal(key, val) {
|
||||
if ((key === 'max_loss' || key === 'max_profit') && (val === 0 || val === 0.0)) {
|
||||
return '-';
|
||||
}
|
||||
return fmtNum(val);
|
||||
}
|
||||
|
||||
function renderBreakdown(key) {
|
||||
if (!cache || !cache.breakdowns) return;
|
||||
var block = cache.breakdowns[key];
|
||||
var head = document.getElementById('stats-breakdown-head');
|
||||
var body = document.getElementById('stats-breakdown-body');
|
||||
if (!block || !head || !body) return;
|
||||
|
||||
head.innerHTML = '';
|
||||
block.columns.forEach(function (col) {
|
||||
var th = document.createElement('th');
|
||||
th.textContent = col.label;
|
||||
head.appendChild(th);
|
||||
});
|
||||
|
||||
body.innerHTML = '';
|
||||
if (!block.rows || !block.rows.length) {
|
||||
var tr = document.createElement('tr');
|
||||
var td = document.createElement('td');
|
||||
td.colSpan = block.columns.length;
|
||||
td.className = 'text-muted';
|
||||
td.textContent = '暂无数据';
|
||||
tr.appendChild(td);
|
||||
body.appendChild(tr);
|
||||
return;
|
||||
}
|
||||
|
||||
block.rows.forEach(function (row) {
|
||||
var tr = document.createElement('tr');
|
||||
block.columns.forEach(function (col) {
|
||||
var td = document.createElement('td');
|
||||
var val = row[col.key];
|
||||
if (col.key === 'win_rate') {
|
||||
td.textContent = fmtPct(val);
|
||||
} else if (col.key === 'label') {
|
||||
td.textContent = val || '-';
|
||||
} else if (typeof val === 'number') {
|
||||
td.textContent = fmtBreakdownVal(col.key, val);
|
||||
td.className = cellClass(col.key, val);
|
||||
} else {
|
||||
td.textContent = val != null ? val : '-';
|
||||
}
|
||||
tr.appendChild(td);
|
||||
});
|
||||
body.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function applyData(data) {
|
||||
cache = data;
|
||||
setSummary(data.summary || {});
|
||||
var views = data.views || [];
|
||||
var sel = document.getElementById('stats-view-select');
|
||||
var current = sel && sel.value ? sel.value : (views[0] && views[0].key);
|
||||
fillViewSelect(views, current);
|
||||
renderBreakdown(current);
|
||||
var updated = document.getElementById('stats-updated');
|
||||
if (updated) {
|
||||
updated.textContent = data.updated_at
|
||||
? '统计更新于 ' + data.updated_at.replace('T', ' ')
|
||||
: '统计已加载';
|
||||
}
|
||||
}
|
||||
|
||||
function loadStats() {
|
||||
fetch('/api/stats', { credentials: 'same-origin' })
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
return r.json();
|
||||
})
|
||||
.then(applyData)
|
||||
.catch(function () {
|
||||
var updated = document.getElementById('stats-updated');
|
||||
if (updated) updated.textContent = '加载失败,请刷新页面';
|
||||
});
|
||||
}
|
||||
|
||||
function bootStatsPage() {
|
||||
if (!document.getElementById('stats-summary')) return;
|
||||
var viewSel = document.getElementById('stats-view-select');
|
||||
if (viewSel && !viewSel.dataset.statsBound) {
|
||||
viewSel.dataset.statsBound = '1';
|
||||
viewSel.addEventListener('change', function () {
|
||||
renderBreakdown(this.value);
|
||||
});
|
||||
}
|
||||
loadStats();
|
||||
}
|
||||
|
||||
if (window.qihuoPageBoot) window.qihuoPageBoot(bootStatsPage, '#stats-summary');
|
||||
else if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootStatsPage);
|
||||
else bootStatsPage();
|
||||
})();
|
||||
Reference in New Issue
Block a user