diff --git a/install_trading.py b/install_trading.py index cb12bb4..a3d7738 100644 --- a/install_trading.py +++ b/install_trading.py @@ -3,7 +3,7 @@ from __future__ import annotations import json from datetime import datetime -from typing import Any, Callable +from typing import Any, Callable, Optional from flask import flash, jsonify, redirect, render_template, request, url_for, Response, stream_with_context @@ -69,7 +69,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se def _capital(conn) -> float: return get_account_capital(conn, get_setting) - def _main_price(product_ths: str): + def _main_quote(product_ths: str) -> Optional[dict]: for p in PRODUCTS: if p["ths"] == product_ths: main = resolve_main_contract(p) @@ -77,8 +77,19 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se return None sym = main.get("ths_code") or "" codes = ths_to_codes(sym) + price = None if codes: - return fetch_price(sym, codes.get("market_code", ""), codes.get("sina_code", "")) + price = fetch_price( + sym, + codes.get("market_code", ""), + codes.get("sina_code", ""), + ) + return { + "ths_code": sym, + "price": price, + "display": main.get("display") or sym, + "name": main.get("name") or p.get("name"), + } return None def _ctp_account(mode: str) -> dict: @@ -670,7 +681,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se try: init_strategy_tables(conn) capital = _capital(conn) - rows = refresh_recommend_cache(conn, capital, _main_price) + rows = refresh_recommend_cache(conn, capital, _main_quote) payload = load_recommend_cache(conn) recommend_hub.broadcast("recommend", {"ok": True, **payload}) return jsonify({"ok": True, "count": len(rows), **payload}) @@ -1001,7 +1012,7 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se start_recommend_worker( db_path=DB_PATH, get_capital_fn=_capital, - price_fn=_main_price, + quote_fn=_main_quote, init_tables_fn=_init_tables, ) start_ctp_reconnect_worker(get_mode_fn=lambda: get_trading_mode(get_setting)) diff --git a/product_recommend.py b/product_recommend.py index 2c5eff4..13b0630 100644 --- a/product_recommend.py +++ b/product_recommend.py @@ -79,18 +79,22 @@ def assess_product_for_capital( def list_product_recommendations( capital: float, - price_fn: Callable[[str], Optional[float]], + quote_fn: Callable[[str], Optional[dict]], *, max_position_pct: float = 50.0, ) -> list[dict]: - """扫描全部品种并排序:推荐 > 可开 > 不足。""" + """扫描全部品种并排序:推荐 > 可开 > 不足。quote_fn(品种代码) -> {price, ths_code, ...}""" def _one(product: dict) -> dict: ths = product["ths"] - main_code = price_fn(ths) - return assess_product_for_capital( - product, capital, main_code, max_position_pct=max_position_pct + quote = quote_fn(ths) or {} + price = quote.get("price") + row = assess_product_for_capital( + product, capital, price, max_position_pct=max_position_pct ) + main_code = (quote.get("ths_code") or "").strip() + row["main_code"] = main_code + return row with ThreadPoolExecutor(max_workers=10) as pool: rows = list(pool.map(_one, PRODUCTS)) diff --git a/recommend_store.py b/recommend_store.py index 38a6532..51b9308 100644 --- a/recommend_store.py +++ b/recommend_store.py @@ -29,11 +29,11 @@ def filter_affordable_recommendations(rows: list[dict]) -> list[dict]: def refresh_recommend_cache( conn, capital: float, - price_fn: Callable[[str], Optional[float]], + quote_fn: Callable[[str], Optional[dict]], ) -> list[dict]: """后台拉行情、筛选并写入数据库。""" ensure_recommend_tables(conn) - all_rows = list_product_recommendations(capital, price_fn) + all_rows = list_product_recommendations(capital, quote_fn) rows = filter_affordable_recommendations(all_rows) now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") conn.execute( diff --git a/recommend_stream.py b/recommend_stream.py index df9589c..56a517b 100644 --- a/recommend_stream.py +++ b/recommend_stream.py @@ -53,7 +53,7 @@ def start_recommend_worker( *, db_path: str, get_capital_fn: Callable, - price_fn: Callable[[str], Optional[float]], + price_fn: Callable[[str], Optional[dict]], init_tables_fn: Callable | None = None, interval: int = REFRESH_INTERVAL_SEC, ) -> None: diff --git a/static/js/symbol.js b/static/js/symbol.js index c4c4518..6e37c78 100644 --- a/static/js/symbol.js +++ b/static/js/symbol.js @@ -19,15 +19,26 @@ return hay.indexOf(qLower) >= 0; } + function groupedHasMatch(groups, qLower) { + if (!qLower) return true; + return groups.some(function (group) { + return group.items.some(function (item) { + return itemMatchesQuery(item, qLower); + }); + }); + } + function initSymbolInput(wrapper) { const input = wrapper.querySelector('.symbol-input'); - const hiddenThs = wrapper.querySelector('input[name="symbol"]'); + const hiddenThs = wrapper.querySelector('input[name="symbol"]') + || wrapper.querySelector('.symbol-ths-code'); const hiddenName = wrapper.querySelector('input[name="symbol_name"]'); const hiddenMarket = wrapper.querySelector('input[name="market_code"]'); const hiddenSina = wrapper.querySelector('input[name="sina_code"]'); const dropdown = wrapper.querySelector('.symbol-dropdown'); const selectedEl = wrapper.querySelector('.symbol-selected'); const isMarketPicker = wrapper.classList.contains('market-symbol-wrap'); + const useMainsPicker = isMarketPicker || wrapper.classList.contains('symbol-mains'); let timer = null; let abortCtrl = null; const cache = new Map(); @@ -41,15 +52,13 @@ function selectItem(item) { const label = formatInputLabel(item); input.value = label; - hiddenThs.value = item.ths_code; - hiddenName.value = item.name; + if (hiddenThs) hiddenThs.value = item.ths_code; + if (hiddenName) hiddenName.value = item.name; if (hiddenMarket) hiddenMarket.value = item.market_code || ''; if (hiddenSina) hiddenSina.value = item.sina_code || ''; - selectedEl.textContent = formatSub(item); + if (selectedEl) selectedEl.textContent = formatSub(item); hideDropdown(); - if (isMarketPicker) { - input.dispatchEvent(new CustomEvent('symbol-selected', { detail: item })); - } + input.dispatchEvent(new CustomEvent('symbol-selected', { detail: item, bubbles: true })); } function buildOptionEl(item) { @@ -107,9 +116,19 @@ dropdown.classList.add('show'); } - function showMarketMains(filterQ) { + function showMarketMains(filterQ, onEmpty) { + const q = (filterQ || '').trim(); + const qLower = q.toLowerCase(); if (mainsCache) { - renderGrouped(mainsCache, filterQ); + if (!q || groupedHasMatch(mainsCache, qLower)) { + renderGrouped(mainsCache, q); + return; + } + if (typeof onEmpty === 'function') { + onEmpty(q); + return; + } + renderGrouped(mainsCache, q); return; } if (mainsLoading) { @@ -124,7 +143,7 @@ .then(function (r) { return r.json(); }) .then(function (groups) { mainsCache = groups; - renderGrouped(groups, filterQ); + showMarketMains(filterQ, onEmpty); }) .catch(function () { hideDropdown(); @@ -157,15 +176,25 @@ }); } + function handleQuery(q) { + if (useMainsPicker) { + showMarketMains(q, function (query) { + search(query); + }); + } else { + search(q); + } + } + input.addEventListener('input', function () { - hiddenThs.value = ''; - hiddenName.value = ''; + if (hiddenThs) hiddenThs.value = ''; + if (hiddenName) hiddenName.value = ''; if (hiddenMarket) hiddenMarket.value = ''; if (hiddenSina) hiddenSina.value = ''; - selectedEl.textContent = ''; + if (selectedEl) selectedEl.textContent = ''; const q = input.value.trim(); if (!q) { - if (isMarketPicker) { + if (useMainsPicker) { showMarketMains(''); } else { hideDropdown(); @@ -174,11 +203,7 @@ } clearTimeout(timer); timer = setTimeout(function () { - if (isMarketPicker) { - showMarketMains(q); - } else { - search(q); - } + handleQuery(q); }, 120); }); @@ -188,11 +213,13 @@ input.addEventListener('focus', function () { const q = input.value.trim(); - if (isMarketPicker) { - showMarketMains(q); + if (useMainsPicker) { + showMarketMains(q, function (query) { + if (query) search(query); + }); return; } - if (q && !hiddenThs.value) { + if (q && hiddenThs && !hiddenThs.value) { search(q); } }); @@ -205,7 +232,8 @@ if (!form.querySelector('.symbol-wrap')) return; if (form.id === 'market-form') return; form.addEventListener('submit', function (e) { - const ths = form.querySelector('input[name="symbol"]'); + const ths = form.querySelector('input[name="symbol"]') + || form.querySelector('.symbol-ths-code'); const market = form.querySelector('input[name="market_code"]'); if (ths && !ths.value.trim()) { e.preventDefault(); diff --git a/static/js/trade.js b/static/js/trade.js index 6a63b22..d718d9f 100644 --- a/static/js/trade.js +++ b/static/js/trade.js @@ -34,6 +34,9 @@ } function selectedSymbol() { + var codeEl = document.getElementById('trade-symbol-code'); + var code = codeEl && codeEl.value ? codeEl.value.trim() : ''; + if (code) return code; return (symInput && symInput.value || '').trim(); } @@ -389,7 +392,7 @@ recommendList.innerHTML = rows.map(function (r) { return ( '' + - '' + (r.name || '') + ' ' + (r.ths || '') + '' + + '' + (r.name || '') + ' ' + (r.main_code || r.ths || '') + '' + '' + (r.exchange || '') + '' + '' + (r.price != null ? r.price : '—') + '' + '' + (r.margin_one_lot != null ? r.margin_one_lot : '—') + '' + @@ -419,7 +422,13 @@ }); }); - if (symInput) symInput.addEventListener('input', function () { scheduleQuote(); scheduleAutoCalc(); }); + if (symInput) { + symInput.addEventListener('input', function () { scheduleQuote(); scheduleAutoCalc(); }); + symInput.addEventListener('symbol-selected', function () { + scheduleQuote(); + scheduleAutoCalc(); + }); + } if (lotsInput) lotsInput.addEventListener('input', scheduleQuote); if (slInput) slInput.addEventListener('input', scheduleAutoCalc); if (tpInput) tpInput.addEventListener('input', scheduleAutoCalc); diff --git a/templates/trade.html b/templates/trade.html index 412ee09..ed3a1a6 100644 --- a/templates/trade.html +++ b/templates/trade.html @@ -37,9 +37,10 @@
-
+
- + +
@@ -120,7 +121,7 @@ {% if recommend_rows %} {% for r in recommend_rows %} - {{ r.name }} {{ r.ths }} + {{ r.name }} {{ r.main_code or r.ths }} {{ r.exchange }} {% if r.price %}{{ r.price }}{% else %}—{% endif %} {% if r.margin_one_lot %}{{ r.margin_one_lot }}{% else %}—{% endif %}