From 074551490f7a53d49ac7db8b64a64c4bf197478c Mon Sep 17 00:00:00 2001 From: dekun Date: Thu, 25 Jun 2026 12:24:10 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=93=81=E7=A7=8D=E6=8E=A8=E8=8D=90?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E6=9C=80=E5=A4=A7=E6=89=8B=E6=95=B0=E5=B9=B6?= =?UTF-8?q?=E8=A1=A5=E7=AE=97=E6=97=A7=E7=BC=93=E5=AD=98=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 最大手数 = floor(权益×保证金上限%÷1手保证金) - 加载与 SSE 推送时实时补算,旧缓存缺字段时自动刷新 Co-authored-by: Cursor --- install_trading.py | 30 ++++++++++++++++---- product_recommend.py | 14 +++++----- recommend_store.py | 62 +++++++++++++++++++++++++++++++++++++++-- recommend_stream.py | 16 ++++++++--- static/js/trade.js | 2 +- templates/settings.html | 2 +- templates/trade.html | 6 ++-- 7 files changed, 109 insertions(+), 23 deletions(-) diff --git a/install_trading.py b/install_trading.py index a6117f4..44c7997 100644 --- a/install_trading.py +++ b/install_trading.py @@ -20,7 +20,12 @@ from position_sizing import ( calc_order_tick_metrics, normalize_sizing_mode, ) -from recommend_store import load_recommend_cache, recommend_payload, refresh_recommend_cache +from recommend_store import ( + load_recommend_cache, + recommend_payload, + refresh_recommend_cache, + rows_missing_max_lots, +) from recommend_stream import recommend_hub, start_recommend_worker from ctp_reconnect import start_ctp_reconnect_worker from ctp_premarket_connect import start_ctp_premarket_connect_worker @@ -400,7 +405,13 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se ).fetchone()["n"] conn.commit() sizing = get_sizing_mode(get_setting) - rec_cache = recommend_payload(conn, live_capital=capital) + max_pct = get_max_margin_pct(get_setting) + rec_loaded = load_recommend_cache(conn) + if rec_loaded.get("stale") or rows_missing_max_lots(rec_loaded.get("rows") or []): + refresh_recommend_cache( + conn, capital, _main_quote, trading_mode=mode, max_margin_pct=max_pct, + ) + rec_cache = recommend_payload(conn, live_capital=capital, max_margin_pct=max_pct) return render_template( "trade.html", trading_mode=mode, @@ -979,7 +990,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se """只读数据库缓存,不在请求时拉行情。""" conn = get_db() try: - payload = recommend_payload(conn, live_capital=_capital(conn)) + payload = recommend_payload( + conn, + live_capital=_capital(conn), + max_margin_pct=get_max_margin_pct(get_setting), + ) return jsonify({"ok": True, **payload}) finally: conn.close() @@ -994,7 +1009,11 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se try: conn = get_db() try: - payload = recommend_payload(conn, live_capital=_capital(conn)) + payload = recommend_payload( + conn, + live_capital=_capital(conn), + max_margin_pct=get_max_margin_pct(get_setting), + ) finally: conn.close() yield sse_format("recommend", {"ok": True, **payload}) @@ -1030,7 +1049,8 @@ def install_trading(app, *, login_required, require_nav, get_db, get_setting, se conn, capital, _main_quote, trading_mode=mode, max_margin_pct=get_max_margin_pct(get_setting), ) - payload = recommend_payload(conn, live_capital=capital) + max_pct = get_max_margin_pct(get_setting) + payload = recommend_payload(conn, live_capital=capital, max_margin_pct=max_pct) recommend_hub.broadcast("recommend", {"ok": True, **payload}) return jsonify({"ok": True, "count": len(rows), **payload}) finally: diff --git a/product_recommend.py b/product_recommend.py index 63c1f0d..c54f344 100644 --- a/product_recommend.py +++ b/product_recommend.py @@ -47,14 +47,14 @@ def assess_product_for_capital( "status_label": "暂无行情", "min_capital_one_lot": None, "margin_one_lot": None, - "recommended_lots": 0, + "max_lots": 0, "risk_one_lot_1pct": None, } margin_one = p * mult * margin_rate min_capital = margin_one / (margin_pct / 100.0) if margin_pct > 0 else margin_one margin_budget = cap * margin_pct / 100.0 if cap > 0 else 0.0 - recommended_lots = int(math.floor(margin_budget / margin_one)) if margin_one > 0 and margin_budget > 0 else 0 + max_lots = int(math.floor(margin_budget / margin_one)) if margin_one > 0 and margin_budget > 0 else 0 stop_dist = tick * default_stop_ticks risk_one_lot = stop_dist * mult risk_pct_1lot = (risk_one_lot / cap * 100) if cap > 0 else 999.0 @@ -65,13 +65,13 @@ def assess_product_for_capital( fee_ths, p, p, 1.0, open_time="", close_time="", trading_mode=trading_mode, ) - can_margin = recommended_lots >= 1 + can_margin = max_lots >= 1 can_risk = cap > 0 and risk_one_lot <= cap * 0.01 if can_margin and can_risk: - status, label = "ok", f"推荐 {recommended_lots} 手" + status, label = "ok", f"最大 {max_lots} 手" elif can_margin: - status, label = "margin_ok", f"可开 {recommended_lots} 手·止损偏宽" + status, label = "margin_ok", f"最大 {max_lots} 手·止损偏宽" else: status, label = "blocked", "资金不足" @@ -84,7 +84,7 @@ def assess_product_for_capital( "tick_size": tick, "margin_one_lot": round(margin_one, 2), "min_capital_one_lot": round(min_capital, 2), - "recommended_lots": recommended_lots, + "max_lots": max_lots, "margin_budget": round(margin_budget, 2), "max_margin_pct": margin_pct, "risk_one_lot_1pct": round(risk_one_lot, 2), @@ -123,5 +123,5 @@ def list_product_recommendations( 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("recommended_lots") or 0))) + rows.sort(key=lambda r: (order.get(r["status"], 9), -(r.get("max_lots") or 0))) return rows diff --git a/recommend_store.py b/recommend_store.py index a841cd4..e8a5bb5 100644 --- a/recommend_store.py +++ b/recommend_store.py @@ -2,6 +2,7 @@ from __future__ import annotations import json +import math from datetime import datetime from typing import Callable, Optional @@ -26,6 +27,50 @@ def filter_affordable_recommendations(rows: list[dict]) -> list[dict]: return [r for r in rows if r.get("status") in ("ok", "margin_ok")] +def rows_missing_max_lots(rows: list[dict]) -> bool: + """缓存是否为旧版(缺少最大手数字段)。""" + if not rows: + return False + return any("max_lots" not in r for r in rows) + + +def enrich_recommend_rows( + rows: list[dict], + capital: float, + *, + max_margin_pct: float = 30.0, +) -> list[dict]: + """用当前权益与保证金比例补算最大可开手数(兼容旧缓存)。""" + cap = float(capital or 0) + pct = max(1.0, min(100.0, float(max_margin_pct or 30.0))) + budget = cap * pct / 100.0 if cap > 0 else 0.0 + enriched: list[dict] = [] + for raw in rows: + row = dict(raw) + try: + margin_one = float(row.get("margin_one_lot") or 0) + except (TypeError, ValueError): + margin_one = 0.0 + if margin_one > 0 and budget > 0: + lots = int(math.floor(budget / margin_one)) + else: + try: + lots = int(row.get("max_lots") or row.get("recommended_lots") or 0) + except (TypeError, ValueError): + lots = 0 + row["max_lots"] = lots + row.pop("recommended_lots", None) + row["margin_budget"] = round(budget, 2) + row["max_margin_pct"] = pct + status = row.get("status") or "" + if lots >= 1 and status in ("ok", "margin_ok"): + row["status_label"] = ( + f"最大 {lots} 手" if status == "ok" else f"最大 {lots} 手·止损偏宽" + ) + enriched.append(row) + return enriched + + def refresh_recommend_cache( conn, capital: float, @@ -85,8 +130,21 @@ def load_recommend_cache(conn) -> dict: } -def recommend_payload(conn, *, live_capital: float) -> dict: +def recommend_payload( + conn, + *, + live_capital: float, + max_margin_pct: float = 30.0, +) -> dict: """读取缓存并附带当前权益(展示用,可能与缓存计算时不同)。""" payload = load_recommend_cache(conn) - payload["capital"] = float(live_capital or 0) + cap = float(live_capital or 0) + pct = max(1.0, min(100.0, float(max_margin_pct or 30.0))) + payload["capital"] = cap + payload["max_margin_pct"] = pct + payload["rows"] = enrich_recommend_rows( + payload.get("rows") or [], + cap, + max_margin_pct=pct, + ) return payload diff --git a/recommend_stream.py b/recommend_stream.py index d842ebb..db6bd62 100644 --- a/recommend_stream.py +++ b/recommend_stream.py @@ -10,7 +10,13 @@ from typing import Callable, Optional from db_conn import connect_db from kline_stream import sse_format -from recommend_store import load_recommend_cache, recommend_cache_stale, refresh_recommend_cache +from recommend_store import ( + load_recommend_cache, + recommend_cache_stale, + recommend_payload, + refresh_recommend_cache, + rows_missing_max_lots, +) logger = logging.getLogger(__name__) @@ -72,13 +78,15 @@ def start_recommend_worker( mode = get_mode_fn() if get_mode_fn else "simulation" max_pct = float(get_max_margin_pct_fn()) if get_max_margin_pct_fn else 30.0 cached = load_recommend_cache(conn) - if recommend_cache_stale(cached.get("updated_at")): + if recommend_cache_stale(cached.get("updated_at")) or rows_missing_max_lots( + cached.get("rows") or [], + ): refresh_recommend_cache( conn, capital, quote_fn, trading_mode=mode, max_margin_pct=max_pct, ) cached = load_recommend_cache(conn) - logger.info("品种推荐每日刷新完成,capital=%.2f rows=%d", capital, len(cached.get("rows") or [])) - payload = {**cached, "capital": capital} + logger.info("品种推荐刷新完成,capital=%.2f rows=%d", capital, len(cached.get("rows") or [])) + payload = recommend_payload(conn, live_capital=capital, max_margin_pct=max_pct) finally: conn.close() recommend_hub.broadcast("recommend", {"ok": True, **payload}) diff --git a/static/js/trade.js b/static/js/trade.js index 70c439b..7ebcca4 100644 --- a/static/js/trade.js +++ b/static/js/trade.js @@ -639,7 +639,7 @@ '' + (r.ref_take_profit != null ? r.ref_take_profit : '—') + '' + '' + (r.margin_one_lot != null ? r.margin_one_lot : '—') + '' + '' + (r.open_fee_one_lot != null ? r.open_fee_one_lot : '—') + '' + - '' + (r.recommended_lots != null && r.recommended_lots > 0 ? r.recommended_lots : '—') + '' + + '' + (r.max_lots != null && r.max_lots > 0 ? r.max_lots : '—') + '' + '' + (r.status_label || '') + '' + '' ); diff --git a/templates/settings.html b/templates/settings.html index 473ae72..55d9ea8 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -70,7 +70,7 @@

- 保证金上限用于开仓校验与品种推荐手数(默认 30%)。在 .env 配置 SIMNOW_USER,于「持仓监控」连接 CTP;权益与行情优先来自柜台。 + 保证金上限用于开仓校验与品种最大手数估算(默认 30%)。在 .env 配置 SIMNOW_USER,于「持仓监控」连接 CTP;权益与行情优先来自柜台。

diff --git a/templates/trade.html b/templates/trade.html index 4f575d1..5b59bcc 100644 --- a/templates/trade.html +++ b/templates/trade.html @@ -107,7 +107,7 @@

品种推荐

-

按权益 {{ '%.2f'|format(capital) }} 元 × 保证金上限 {{ max_margin_pct }}% 推荐手数;参考止损/止盈按 20 跳、盈亏比 2:1 估算。 +

最大手数 = floor(权益 × 保证金上限 {{ max_margin_pct }}% ÷ 1手保证金);当前权益 {{ '%.2f'|format(capital) }} 元。参考止损/止盈按 20 跳、盈亏比 2:1 估算。 {% if recommend_updated_at %}每日后台更新 · 最近 {{ recommend_updated_at }}{% else %}等待今日后台刷新…{% endif %}

@@ -116,7 +116,7 @@ 品种交易所参考价 参考止损参考止盈 - 1手保证金1手手续费推荐手数状态 + 1手保证金1手手续费最大手数状态 @@ -130,7 +130,7 @@ {% if r.ref_take_profit %}{{ r.ref_take_profit }}{% else %}—{% endif %} {% if r.margin_one_lot %}{{ r.margin_one_lot }}{% else %}—{% endif %} {% if r.open_fee_one_lot is defined and r.open_fee_one_lot is not none %}{{ r.open_fee_one_lot }}{% else %}—{% endif %} - {% if r.recommended_lots %}{{ r.recommended_lots }}{% else %}—{% endif %} + {% if r.max_lots is not none and r.max_lots > 0 %}{{ r.max_lots }}{% else %}—{% endif %} {{ r.status_label }} {% endfor %}