feat: 品种下拉统一展示推荐列表,与下方品种推荐表一致。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-25 13:01:20 +08:00
parent d127a53870
commit fc425c0e9f
5 changed files with 100 additions and 7 deletions
+27 -1
View File
@@ -24,7 +24,13 @@ from werkzeug.security import check_password_hash, generate_password_hash
from functools import wraps from functools import wraps
from symbols import search_symbols, ths_to_codes, list_main_contracts_grouped, refresh_main_index from symbols import (
search_symbols,
ths_to_codes,
list_main_contracts_grouped,
list_recommended_symbols_grouped,
refresh_main_index,
)
from contract_specs import calc_position_metrics from contract_specs import calc_position_metrics
from fee_specs import ( from fee_specs import (
calc_fee_breakdown, calc_fee_breakdown,
@@ -742,6 +748,26 @@ def api_symbols_mains():
return jsonify(list_main_contracts_grouped()) return jsonify(list_main_contracts_grouped())
@app.route("/api/symbols/recommended")
@login_required
def api_symbols_recommended():
"""品种下拉:仅展示当前资金下推荐的品种(与下方品种推荐表一致)。"""
from recommend_store import recommend_payload
from trading_context import get_account_capital, get_max_margin_pct
conn = get_db()
try:
capital = get_account_capital(conn, get_setting)
payload = recommend_payload(
conn,
live_capital=capital,
max_margin_pct=get_max_margin_pct(get_setting),
)
return jsonify(list_recommended_symbols_grouped(payload.get("rows") or []))
finally:
conn.close()
@app.route("/api/key_prices") @app.route("/api/key_prices")
@login_required @login_required
def api_key_prices(): def api_key_prices():
+14 -4
View File
@@ -1,8 +1,12 @@
(function () { (function () {
function formatSub(item) { function formatSub(item) {
return '同花顺 ' + item.ths_code + var sub = '同花顺 ' + item.ths_code +
(item.market_code ? ' · ' + item.market_code : '') + (item.market_code ? ' · ' + item.market_code : '') +
' · ' + (item.exchange || ''); ' · ' + (item.exchange || '');
if (item.max_lots != null && item.max_lots > 0) {
sub += ' · 最大 ' + item.max_lots + ' 手';
}
return sub;
} }
function formatInputLabel(item) { function formatInputLabel(item) {
@@ -132,17 +136,23 @@
return; return;
} }
if (mainsLoading) { if (mainsLoading) {
dropdown.innerHTML = '<div class="symbol-option">正在识别主力合约…</div>'; dropdown.innerHTML = '<div class="symbol-option">正在加载推荐品种…</div>';
dropdown.classList.add('show'); dropdown.classList.add('show');
return; return;
} }
mainsLoading = true; mainsLoading = true;
dropdown.innerHTML = '<div class="symbol-option">正在识别主力合约…</div>'; dropdown.innerHTML = '<div class="symbol-option">正在加载推荐品种…</div>';
dropdown.classList.add('show'); dropdown.classList.add('show');
fetch('/api/symbols/mains') fetch('/api/symbols/recommended')
.then(function (r) { return r.json(); }) .then(function (r) { return r.json(); })
.then(function (groups) { .then(function (groups) {
mainsCache = groups; mainsCache = groups;
if (!groups.length) {
dropdown.innerHTML =
'<div class="symbol-option">当前资金下暂无推荐品种,可输入合约代码搜索</div>';
dropdown.classList.add('show');
return;
}
showMarketMains(filterQ, onEmpty); showMarketMains(filterQ, onEmpty);
}) })
.catch(function () { .catch(function () {
+57
View File
@@ -419,6 +419,63 @@ def search_symbols(query: str) -> list:
return results return results
_THS_TO_PRODUCT = {p["ths"]: p for p in PRODUCTS}
for _p in PRODUCTS:
_THS_TO_PRODUCT.setdefault(_p["ths"].lower(), _p)
def _product_for_ths(ths: str) -> Optional[dict]:
key = (ths or "").strip()
if not key:
return None
return _THS_TO_PRODUCT.get(key) or _THS_TO_PRODUCT.get(key.lower())
def _main_for_product(product: dict) -> Optional[dict]:
with _main_index_lock:
index = dict(_main_index)
main = index.get(product["sina"])
if not main:
resolved = resolve_main_contract(product)
if resolved:
main = _enrich_item(resolved)
return main
def list_recommended_symbols_grouped(recommend_rows: list[dict]) -> list[dict]:
"""按交易所分类返回推荐品种对应的主力合约(品种选择下拉用)。"""
if not recommend_rows:
return []
buckets: dict[str, list] = defaultdict(list)
seen: set[str] = set()
for row in recommend_rows:
if row.get("status") not in ("ok", "margin_ok"):
continue
ths_key = (row.get("ths") or "").strip()
if not ths_key or ths_key in seen:
continue
product = _product_for_ths(ths_key)
if not product:
continue
seen.add(ths_key)
main = _main_for_product(product)
if not main:
continue
item = dict(main)
max_lots = row.get("max_lots")
if max_lots is not None:
item["max_lots"] = max_lots
buckets[product["exchange"]].append(_enrich_item(item))
groups: list[dict] = []
for cat in EXCHANGE_ORDER:
items = buckets.get(cat)
if items:
groups.append({"category": cat, "items": items})
return groups
def list_main_contracts_grouped() -> list[dict]: def list_main_contracts_grouped() -> list[dict]:
"""按交易所分类返回全部品种主力合约(行情页下拉用)。""" """按交易所分类返回全部品种主力合约(行情页下拉用)。"""
with _main_index_lock: with _main_index_lock:
+1 -1
View File
@@ -7,7 +7,7 @@
<div class="card-body"> <div class="card-body">
<form action="{{ url_for('add_key') }}" method="post" class="form-compact"> <form action="{{ url_for('add_key') }}" method="post" class="form-compact">
<div class="form-line line-3"> <div class="form-line line-3">
<div class="symbol-wrap"> <div class="symbol-wrap symbol-mains">
<input type="text" class="symbol-input" placeholder="主力合约" autocomplete="off" required> <input type="text" class="symbol-input" placeholder="主力合约" autocomplete="off" required>
<input type="hidden" name="symbol" required> <input type="hidden" name="symbol" required>
<input type="hidden" name="symbol_name"> <input type="hidden" name="symbol_name">
+1 -1
View File
@@ -8,7 +8,7 @@
<p class="hint" style="margin-bottom:.75rem">开盘前制定,当日有效;请先选择<strong>主力合约</strong>,下方为进行中计划。</p> <p class="hint" style="margin-bottom:.75rem">开盘前制定,当日有效;请先选择<strong>主力合约</strong>,下方为进行中计划。</p>
<form action="{{ url_for('add_plan') }}" method="post" class="form-compact"> <form action="{{ url_for('add_plan') }}" method="post" class="form-compact">
<div class="form-line line-plan-1"> <div class="form-line line-plan-1">
<div class="symbol-wrap"> <div class="symbol-wrap symbol-mains">
<input type="text" class="symbol-input" placeholder="主力合约" autocomplete="off" required> <input type="text" class="symbol-input" placeholder="主力合约" autocomplete="off" required>
<input type="hidden" name="symbol" required> <input type="hidden" name="symbol" required>
<input type="hidden" name="symbol_name"> <input type="hidden" name="symbol_name">