From 38a38cb51da1f89b54303505a40a0b61e78f8d2f Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 24 Jun 2026 10:34:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=8C=81=E4=BB=93=E7=9B=91?= =?UTF-8?q?=E6=8E=A7=E9=A1=B5=E9=95=BF=E6=97=B6=E9=97=B4=E7=A9=BA=E7=99=BD?= =?UTF-8?q?=EF=BC=9A=E5=93=81=E7=A7=8D=E6=8E=A8=E8=8D=90=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=BC=82=E6=AD=A5=E5=8A=A0=E8=BD=BD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 页面先渲染三卡片,推荐表并行拉行情,持仓与推荐分别通过 API 加载。 Co-authored-by: Cursor --- install_trading.py | 2 -- product_recommend.py | 15 ++++++----- static/js/trade.js | 59 ++++++++++++++++++++++++++++++++++++++++++-- templates/trade.html | 13 ++-------- 4 files changed, 68 insertions(+), 21 deletions(-) diff --git a/install_trading.py b/install_trading.py index 1e48da1..bb74e11 100644 --- a/install_trading.py +++ b/install_trading.py @@ -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, diff --git a/product_recommend.py b/product_recommend.py index 9a7a3bf..2c5eff4 100644 --- a/product_recommend.py +++ b/product_recommend.py @@ -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 diff --git a/static/js/trade.js b/static/js/trade.js index af6c584..45fbe6c 100644 --- a/static/js/trade.js +++ b/static/js/trade.js @@ -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 ( + '' + + '' + (r.name || '') + ' ' + (r.ths || '') + '' + + '' + (r.exchange || '') + '' + + '' + (r.price != null ? r.price : '—') + '' + + '' + (r.margin_one_lot != null ? r.margin_one_lot : '—') + '' + + '' + (r.min_capital_one_lot != null ? r.min_capital_one_lot : '—') + '' + + '' + (r.status_label || '') + '' + + '' + ); + } + + 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 = '暂无推荐数据'; + return; + } + recommendList.innerHTML = rows.map(buildRecommendRow).join(''); + }) + .catch(function () { + recommendList.innerHTML = '品种推荐加载失败,请刷新页面'; + }); + } + 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); }); })(); diff --git a/templates/trade.html b/templates/trade.html index e9793eb..c9dc98f 100644 --- a/templates/trade.html +++ b/templates/trade.html @@ -80,17 +80,8 @@ 品种交易所参考价1手保证金建议最低资金状态 - - {% for r in recommend_rows %} - - {{ r.name }} {{ r.ths }} - {{ r.exchange }} - {% if r.price %}{{ r.price }}{% else %}—{% endif %} - {% if r.margin_one_lot %}{{ r.margin_one_lot }}{% else %}—{% endif %} - {% if r.min_capital_one_lot %}{{ r.min_capital_one_lot }}{% else %}—{% endif %} - {{ r.status_label }} - - {% endfor %} + + 品种推荐加载中…