修复持仓监控页长时间空白:品种推荐改为异步加载。

页面先渲染三卡片,推荐表并行拉行情,持仓与推荐分别通过 API 加载。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-24 10:34:34 +08:00
parent 55d95b4c39
commit 38a38cb51d
4 changed files with 68 additions and 21 deletions
-2
View File
@@ -289,7 +289,6 @@ def install_trading(app, *, login_required, get_db, get_setting, set_setting, fe
capital = _capital(conn)
risk = get_risk_status(conn)
ctp_acc = _ctp_account(mode) if ctp_st.get("connected") else {}
recommend_rows = list_product_recommendations(capital, _main_price)
active_trend = conn.execute(
"SELECT * FROM trend_pullback_plans WHERE status='active' ORDER BY id DESC LIMIT 1"
).fetchone()
@@ -309,7 +308,6 @@ def install_trading(app, *, login_required, get_db, get_setting, set_setting, fe
risk_status=risk,
ctp_status=ctp_st,
ctp_account=ctp_acc,
recommend_rows=recommend_rows,
active_trend=dict(active_trend) if active_trend else None,
monitor_count=monitor_count,
roll_count=roll_count,
+9 -6
View File
@@ -1,6 +1,7 @@
"""按账户资金推荐可交易品种(期货核心筛选)。"""
from __future__ import annotations
from concurrent.futures import ThreadPoolExecutor
from typing import Callable, Optional
from contract_specs import get_contract_spec
@@ -83,14 +84,16 @@ def list_product_recommendations(
max_position_pct: float = 50.0,
) -> list[dict]:
"""扫描全部品种并排序:推荐 > 可开 > 不足。"""
rows = []
for p in PRODUCTS:
ths = p["ths"]
def _one(product: dict) -> dict:
ths = product["ths"]
main_code = price_fn(ths)
row = assess_product_for_capital(
p, capital, main_code, max_position_pct=max_position_pct
return assess_product_for_capital(
product, capital, main_code, max_position_pct=max_position_pct
)
rows.append(row)
with ThreadPoolExecutor(max_workers=10) as pool:
rows = list(pool.map(_one, PRODUCTS))
order = {"ok": 0, "margin_ok": 1, "blocked": 2, "no_price": 3}
rows.sort(key=lambda r: (order.get(r["status"], 9), r.get("min_capital_one_lot") or 1e18))
return rows
+57 -2
View File
@@ -1,7 +1,16 @@
(function () {
var list = document.getElementById('position-live-list');
var recommendList = document.getElementById('recommend-list');
var pollTimer = null;
function runWhenReady(fn) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fn);
} else {
fn();
}
}
function fmtNum(v, digits) {
if (v === null || v === undefined) return '--';
return Number(v).toFixed(digits === undefined ? 2 : digits);
@@ -110,7 +119,10 @@
function pollPositions() {
if (!list) return;
fetch('/api/trading/live')
.then(function (r) { return r.json(); })
.then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function (data) {
var cap = document.getElementById('cap-display');
if (cap && data.capital != null) cap.textContent = Number(data.capital).toFixed(2);
@@ -145,6 +157,48 @@
});
}
function badgeClass(status) {
if (status === 'ok') return 'profit';
if (status === 'blocked') return 'loss';
return 'planned';
}
function buildRecommendRow(r) {
return (
'<tr class="rec-' + (r.status || '') + '">' +
'<td><strong>' + (r.name || '') + '</strong> <span class="text-muted">' + (r.ths || '') + '</span></td>' +
'<td>' + (r.exchange || '') + '</td>' +
'<td>' + (r.price != null ? r.price : '—') + '</td>' +
'<td>' + (r.margin_one_lot != null ? r.margin_one_lot : '—') + '</td>' +
'<td>' + (r.min_capital_one_lot != null ? r.min_capital_one_lot : '—') + '</td>' +
'<td><span class="badge ' + badgeClass(r.status) + '">' + (r.status_label || '') + '</span></td>' +
'</tr>'
);
}
function loadRecommendations() {
if (!recommendList) return;
fetch('/api/recommend/list')
.then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function (data) {
if (!data.ok) throw new Error(data.error || 'load failed');
var recCap = document.getElementById('rec-capital');
if (recCap && data.capital != null) recCap.textContent = Number(data.capital).toFixed(2);
var rows = data.rows || [];
if (!rows.length) {
recommendList.innerHTML = '<tr><td colspan="6" class="empty-hint">暂无推荐数据</td></tr>';
return;
}
recommendList.innerHTML = rows.map(buildRecommendRow).join('');
})
.catch(function () {
recommendList.innerHTML = '<tr><td colspan="6" class="empty-hint text-loss">品种推荐加载失败,请刷新页面</td></tr>';
});
}
var btnConnect = document.getElementById('btn-ctp-connect');
if (btnConnect) {
btnConnect.addEventListener('click', function () {
@@ -163,8 +217,9 @@
});
}
document.addEventListener('DOMContentLoaded', function () {
runWhenReady(function () {
pollPositions();
loadRecommendations();
pollTimer = setInterval(pollPositions, 3000);
});
})();
+2 -11
View File
@@ -80,17 +80,8 @@
<th>品种</th><th>交易所</th><th>参考价</th><th>1手保证金</th><th>建议最低资金</th><th>状态</th>
</tr>
</thead>
<tbody>
{% for r in recommend_rows %}
<tr class="rec-{{ r.status }}">
<td><strong>{{ r.name }}</strong> <span class="text-muted">{{ r.ths }}</span></td>
<td>{{ r.exchange }}</td>
<td>{% if r.price %}{{ r.price }}{% else %}—{% endif %}</td>
<td>{% if r.margin_one_lot %}{{ r.margin_one_lot }}{% else %}—{% endif %}</td>
<td>{% if r.min_capital_one_lot %}{{ r.min_capital_one_lot }}{% else %}—{% endif %}</td>
<td><span class="badge {% if r.status=='ok' %}profit{% elif r.status=='blocked' %}loss{% else %}planned{% endif %}">{{ r.status_label }}</span></td>
</tr>
{% endfor %}
<tbody id="recommend-list">
<tr><td colspan="6" class="empty-hint">品种推荐加载中…</td></tr>
</tbody>
</table>
</div>