Expand recommend table with gap, daily stats, and client-side sorting
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+145
-12
@@ -30,6 +30,11 @@
|
||||
var selectedMaxLots = null;
|
||||
var recommendMaxByProduct = {};
|
||||
var recommendMaxByCode = {};
|
||||
var recRowsRaw = [];
|
||||
var recSortKey = 'trend';
|
||||
var recSortDesc = true;
|
||||
var REC_SORT_CACHE = 'qihuo_rec_sort_v1';
|
||||
var REC_COLSPAN = 14;
|
||||
var POS_CACHE_KEY = 'qihuo_trading_live_v3';
|
||||
|
||||
function runWhenReady(fn) {
|
||||
@@ -979,6 +984,84 @@
|
||||
};
|
||||
}
|
||||
|
||||
function loadRecSortPrefs() {
|
||||
try {
|
||||
var raw = sessionStorage.getItem(REC_SORT_CACHE);
|
||||
if (!raw) return;
|
||||
var p = JSON.parse(raw);
|
||||
if (p.key) recSortKey = p.key;
|
||||
if (typeof p.desc === 'boolean') recSortDesc = p.desc;
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function saveRecSortPrefs() {
|
||||
try {
|
||||
sessionStorage.setItem(REC_SORT_CACHE, JSON.stringify({ key: recSortKey, desc: recSortDesc }));
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function syncRecSortUi() {
|
||||
var sel = document.getElementById('rec-sort-key');
|
||||
var btn = document.getElementById('rec-sort-dir');
|
||||
if (sel) sel.value = recSortKey;
|
||||
if (btn) btn.textContent = recSortDesc ? '↓' : '↑';
|
||||
}
|
||||
|
||||
var TREND_SORT_RANK = { break_long: 0, break_short: 0, long: 1, short: 2, range: 3, '': 9 };
|
||||
var GAP_SORT_RANK = { up: 2, down: 1, none: 0, '': -1 };
|
||||
|
||||
function sortRecommendRows(rows) {
|
||||
var list = (rows || []).slice();
|
||||
var key = recSortKey || 'trend';
|
||||
var desc = recSortDesc;
|
||||
list.sort(function (a, b) {
|
||||
var av, bv, as, bs;
|
||||
if (key === 'gap') {
|
||||
av = GAP_SORT_RANK[a.gap || ''] !== undefined ? GAP_SORT_RANK[a.gap || ''] : -1;
|
||||
bv = GAP_SORT_RANK[b.gap || ''] !== undefined ? GAP_SORT_RANK[b.gap || ''] : -1;
|
||||
as = Math.abs(Number(a.gap_pct) || 0);
|
||||
bs = Math.abs(Number(b.gap_pct) || 0);
|
||||
} else if (key === 'volume') {
|
||||
av = Number(a.volume) || 0;
|
||||
bv = Number(b.volume) || 0;
|
||||
as = bs = 0;
|
||||
} else if (key === 'amplitude') {
|
||||
av = Number(a.yesterday_amplitude_pct) || 0;
|
||||
bv = Number(b.yesterday_amplitude_pct) || 0;
|
||||
as = bs = 0;
|
||||
} else {
|
||||
av = TREND_SORT_RANK[a.trend || ''] !== undefined ? TREND_SORT_RANK[a.trend || ''] : 9;
|
||||
bv = TREND_SORT_RANK[b.trend || ''] !== undefined ? TREND_SORT_RANK[b.trend || ''] : 9;
|
||||
as = Number(a.max_lots) || 0;
|
||||
bs = Number(b.max_lots) || 0;
|
||||
}
|
||||
if (av !== bv) return desc ? bv - av : av - bv;
|
||||
if (as !== bs) return desc ? bs - as : as - bs;
|
||||
return String(a.name || '').localeCompare(String(b.name || ''), 'zh-CN');
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
function fmtRecVolume(v) {
|
||||
if (v === null || v === undefined) return '—';
|
||||
var n = Number(v);
|
||||
if (!isFinite(n)) return '—';
|
||||
if (n >= 10000) return (n / 10000).toFixed(1) + '万';
|
||||
return String(Math.round(n));
|
||||
}
|
||||
|
||||
function changeCellHtml(r) {
|
||||
if (r.yesterday_change == null) return '—';
|
||||
var ch = Number(r.yesterday_change);
|
||||
var cls = ch > 0 ? 'rec-change-up' : (ch < 0 ? 'rec-change-down' : '');
|
||||
var txt = (ch > 0 ? '+' : '') + ch;
|
||||
if (r.yesterday_change_pct != null) {
|
||||
var pct = Number(r.yesterday_change_pct);
|
||||
txt += ' (' + (pct > 0 ? '+' : '') + pct + '%)';
|
||||
}
|
||||
return '<span class="' + cls + '">' + txt + '</span>';
|
||||
}
|
||||
|
||||
function trendBadgeHtml(r) {
|
||||
var label = r.trend_label || '';
|
||||
if (!label || label === '—') return '—';
|
||||
@@ -992,18 +1075,23 @@
|
||||
return '<span class="badge trend-badge ' + cls + '"' + title + '>' + prefix + label + '</span>';
|
||||
}
|
||||
|
||||
function renderRecommendations(data) {
|
||||
if (!recommendList || !data) return;
|
||||
updateRecommendMaxMaps(data);
|
||||
var recCap = document.getElementById('rec-capital');
|
||||
if (recCap && data.capital != null) recCap.textContent = Number(data.capital).toFixed(2);
|
||||
var recUpdated = document.getElementById('rec-updated');
|
||||
if (recUpdated && data.updated_at) {
|
||||
recUpdated.textContent = '每日后台更新 · 最近 ' + data.updated_at;
|
||||
function gapBadgeHtml(r) {
|
||||
var label = r.gap_label || '';
|
||||
if (!label || label === '—') return '—';
|
||||
var cls = 'planned';
|
||||
if (r.gap === 'up') cls = 'profit';
|
||||
else if (r.gap === 'down') cls = 'loss';
|
||||
var title = '';
|
||||
if (r.gap_pct != null && r.gap !== 'none') {
|
||||
title = ' title="跳空 ' + (Number(r.gap_pct) > 0 ? '+' : '') + r.gap_pct + '%"';
|
||||
}
|
||||
var rows = data.rows || [];
|
||||
return '<span class="badge gap-badge ' + cls + '"' + title + '>' + label + '</span>';
|
||||
}
|
||||
|
||||
function renderRecommendRows(rows) {
|
||||
if (!recommendList) return;
|
||||
if (!rows.length) {
|
||||
recommendList.innerHTML = '<tr><td colspan="10" class="empty-hint">当前资金下暂无推荐品种(每日后台刷新)</td></tr>';
|
||||
recommendList.innerHTML = '<tr><td colspan="' + REC_COLSPAN + '" class="empty-hint">当前资金下暂无推荐品种(每日后台刷新)</td></tr>';
|
||||
return;
|
||||
}
|
||||
recommendList.innerHTML = rows.map(function (r) {
|
||||
@@ -1015,9 +1103,13 @@
|
||||
'<td><strong' + nameCls + '>' + (r.name || '') + '</strong> <span class="text-accent">' + (r.main_code || r.ths || '') + '</span></td>' +
|
||||
'<td>' + (r.exchange || '') + '</td>' +
|
||||
'<td>' + trendBadgeHtml(r) + '</td>' +
|
||||
'<td>' + gapBadgeHtml(r) + '</td>' +
|
||||
'<td>' + (r.price != null ? r.price : '—') + '</td>' +
|
||||
'<td>' + (r.ref_stop_loss != null ? r.ref_stop_loss : '—') + '</td>' +
|
||||
'<td>' + (r.ref_take_profit != null ? r.ref_take_profit : '—') + '</td>' +
|
||||
'<td>' + (r.prev_close != null ? r.prev_close : '—') + '</td>' +
|
||||
'<td>' + (r.today_open != null ? r.today_open : '—') + '</td>' +
|
||||
'<td>' + changeCellHtml(r) + '</td>' +
|
||||
'<td>' + (r.yesterday_amplitude_pct != null ? r.yesterday_amplitude_pct + '%' : '—') + '</td>' +
|
||||
'<td>' + fmtRecVolume(r.volume) + '</td>' +
|
||||
'<td>' + (r.margin_one_lot != null ? r.margin_one_lot + (r.margin_source === 'ctp' ? ' <span class="text-muted">(柜台)</span>' : '') : '—') + '</td>' +
|
||||
'<td>' + (r.open_fee_one_lot != null ? r.open_fee_one_lot : '—') + '</td>' +
|
||||
'<td>' + (r.max_lots != null && r.max_lots > 0 ? r.max_lots : '—') + '</td>' +
|
||||
@@ -1027,6 +1119,46 @@
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderRecommendations(data) {
|
||||
if (!recommendList || !data) return;
|
||||
updateRecommendMaxMaps(data);
|
||||
var recCap = document.getElementById('rec-capital');
|
||||
if (recCap && data.capital != null) recCap.textContent = Number(data.capital).toFixed(2);
|
||||
var recUpdated = document.getElementById('rec-updated');
|
||||
if (recUpdated && data.updated_at) {
|
||||
recUpdated.textContent = '每日后台更新 · 最近 ' + data.updated_at;
|
||||
}
|
||||
var rows = data.rows || [];
|
||||
recRowsRaw = rows.slice();
|
||||
if (!rows.length) {
|
||||
recommendList.innerHTML = '<tr><td colspan="' + REC_COLSPAN + '" class="empty-hint">当前资金下暂无推荐品种(每日后台刷新)</td></tr>';
|
||||
return;
|
||||
}
|
||||
renderRecommendRows(sortRecommendRows(recRowsRaw));
|
||||
}
|
||||
|
||||
function initRecommendSortControls() {
|
||||
loadRecSortPrefs();
|
||||
syncRecSortUi();
|
||||
var sel = document.getElementById('rec-sort-key');
|
||||
var btn = document.getElementById('rec-sort-dir');
|
||||
if (sel) {
|
||||
sel.addEventListener('change', function () {
|
||||
recSortKey = sel.value || 'trend';
|
||||
saveRecSortPrefs();
|
||||
renderRecommendRows(sortRecommendRows(recRowsRaw));
|
||||
});
|
||||
}
|
||||
if (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
recSortDesc = !recSortDesc;
|
||||
saveRecSortPrefs();
|
||||
syncRecSortUi();
|
||||
renderRecommendRows(sortRecommendRows(recRowsRaw));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function connectRecommendStream() {
|
||||
if (recommendSource) { recommendSource.close(); recommendSource = null; }
|
||||
recommendSource = new EventSource('/api/recommend/stream');
|
||||
@@ -1125,6 +1257,7 @@
|
||||
connectPositionStream();
|
||||
initCtpOnLoad();
|
||||
connectRecommendStream();
|
||||
initRecommendSortControls();
|
||||
fetch('/api/recommend/list')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) { if (data.ok) renderRecommendations(data); })
|
||||
|
||||
Reference in New Issue
Block a user