From de6815d4817e3a4e5616b19de89367687f911c42 Mon Sep 17 00:00:00 2001 From: dekun Date: Wed, 24 Jun 2026 13:26:53 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20K=E7=BA=BF=E6=96=B0=E6=B5=AA=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E8=A1=A5=E9=BD=90=E4=B8=8E=E6=89=8B=E7=BB=AD=E8=B4=B9?= =?UTF-8?q?=E9=A1=B5=E5=B8=83=E5=B1=80=E5=8F=8ACTP=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- app.py | 6 +++- ctp_fee_sync.py | 28 +++++++++++++++++-- fee_specs.py | 25 +++++++++++++++++ kline_chart.py | 49 +++++++++++++++++++++++++++------ static/js/market.js | 1 + templates/fees.html | 64 +++++++++++++++++++++++++++++-------------- templates/market.html | 2 +- vnpy_bridge.py | 50 +++++++++++++++++++++++---------- 8 files changed, 178 insertions(+), 47 deletions(-) diff --git a/app.py b/app.py index f8fd82a..3f5420e 100644 --- a/app.py +++ b/app.py @@ -32,6 +32,8 @@ from fee_specs import ( get_fee_multiplier, get_fee_source_mode, list_all_fee_rates, + list_fee_rates_for_ui, + count_fee_rates_by_source, load_fee_rates_from_json, upsert_fee_rate, ) @@ -1612,13 +1614,15 @@ def fees(): flash(f"已保存 {product} 费率") return redirect(url_for("fees")) - rates = list_all_fee_rates() + rates = list_fee_rates_for_ui() + fee_counts = count_fee_rates_by_source() multiplier = get_setting("fee_multiplier", "2") fee_source_mode = get_fee_source_mode() ctp_st = ctp_status(mode) return render_template( "fees.html", rates=rates, + fee_counts=fee_counts, multiplier=multiplier, fee_source_mode=fee_source_mode, ctp_connected=bool(ctp_st.get("connected")), diff --git a/ctp_fee_sync.py b/ctp_fee_sync.py index 0b43e30..0737744 100644 --- a/ctp_fee_sync.py +++ b/ctp_fee_sync.py @@ -67,14 +67,36 @@ def sync_fees_from_ctp(mode: str, *, max_symbols: int = 80) -> tuple[int, str]: if not bridge.ping(): return 0, "CTP 连接无效,请重连" + seen: set[str] = set() + ok = 0 + errors = 0 + + batch = bridge.query_all_commissions(mode=mode) + if batch: + for raw in batch: + inst = str(raw.get("InstrumentID") or "").strip() + product = _product_from_instrument(inst) + if not product or product in seen: + continue + seen.add(product) + try: + fields = ctp_commission_to_fee_fields(raw, inst or product) + upsert_fee_rate(product, fields) + ok += 1 + except Exception as exc: + logger.debug("CTP fee batch %s: %s", inst, exc) + errors += 1 + if ok > 0: + msg = f"已从 CTP 批量同步 {ok} 个品种手续费" + if errors: + msg += f"({errors} 个跳过)" + return ok, msg + symbols = _collect_main_ths_codes()[:max_symbols] if not symbols: return 0, "无主力合约列表" - seen: set[str] = set() - ok = 0 - errors = 0 for ths in symbols: product = _product_from_instrument(ths) if not product or product in seen: diff --git a/fee_specs.py b/fee_specs.py index 520dbc6..56f0ae1 100644 --- a/fee_specs.py +++ b/fee_specs.py @@ -296,6 +296,31 @@ def list_all_fee_rates() -> list: return [dict(r) for r in rows] +def list_fee_rates_for_ui() -> list: + """手续费页展示:CTP 模式下 ctp 来源优先排前。""" + rows = list_all_fee_rates() + if get_fee_source_mode() == "ctp": + rows.sort( + key=lambda r: ( + 0 if (r.get("source") or "") == "ctp" else 1, + r.get("product") or "", + ) + ) + return rows + + +def count_fee_rates_by_source() -> dict[str, int]: + conn = _get_db() + rows = conn.execute( + "SELECT source, COUNT(*) AS n FROM fee_rates GROUP BY source" + ).fetchall() + conn.close() + out: dict[str, int] = {} + for row in rows: + out[str(row["source"] or "local")] = int(row["n"] or 0) + return out + + def upsert_fee_rate(product: str, fields: dict) -> None: product = product.lower().strip() conn = _get_db() diff --git a/kline_chart.py b/kline_chart.py index fe94dc4..0a6952a 100644 --- a/kline_chart.py +++ b/kline_chart.py @@ -17,6 +17,9 @@ from kline_store import ensure_kline_tables, get_cached_entry, save_bars logger = logging.getLogger(__name__) TZ = ZoneInfo("Asia/Shanghai") +# CTP tick 聚合 bar 少于此数时,用新浪历史补齐走势 +MIN_CTP_KLINE_BARS = 15 + PERIOD_MINUTES = { "1m": "1", "3m": "3", @@ -165,6 +168,24 @@ def _merge_bars(chunk: list) -> dict: } +def _merge_kline_bars(history: list, live: list) -> list: + """新浪历史 + CTP 实时尾部(去重叠)。""" + if not history: + return list(live or []) + if not live: + return list(history) + first_live = _bar_datetime(live[0]) + if not first_live: + return history + live + trimmed = [] + for bar in history: + dt = _bar_datetime(bar) + if dt and dt < first_live: + trimmed.append(bar) + merged = trimmed + list(live) + return merged if merged else list(history) + + def _weekly_from_daily(daily: list) -> list: if not daily: return [] @@ -236,6 +257,7 @@ def fetch_market_klines( source = "remote" cached_at = None ctp_connected = False + ctp_bars: list = [] if prefer_ctp: try: @@ -253,14 +275,21 @@ def fetch_market_klines( mode = "simulation" ctp_connected = bool(ctp_status(mode).get("connected")) if ctp_connected: - ctp_bars = fetch_ctp_klines(symbol, p, mode) - if ctp_bars: - bars = ctp_bars - source = "ctp" + ctp_bars = fetch_ctp_klines(symbol, p, mode) or [] except Exception as exc: logger.debug("ctp kline fetch failed %s %s: %s", symbol, p, exc) - if not bars and db_path and chart_sym and not force_remote: + need_sina = ( + force_remote + or not ctp_bars + or len(ctp_bars) < MIN_CTP_KLINE_BARS + ) + + if ctp_bars and len(ctp_bars) >= MIN_CTP_KLINE_BARS: + bars = ctp_bars + source = "ctp" + + if not bars and db_path and chart_sym and not force_remote and need_sina: try: conn = connect_db(db_path) cached = get_cached_entry(conn, chart_sym, p) @@ -272,11 +301,15 @@ def fetch_market_klines( except Exception as exc: logger.warning("kline cache read failed %s %s: %s", chart_sym, p, exc) - if force_remote or not bars: + if not bars or len(ctp_bars) < MIN_CTP_KLINE_BARS: remote_bars = fetch_sina_klines(symbol, p) if remote_bars: - bars = remote_bars - source = "remote" + if ctp_bars and ctp_connected: + bars = _merge_kline_bars(remote_bars, ctp_bars) + source = "ctp+remote" + else: + bars = remote_bars + source = "remote" if db_path and chart_sym and not ctp_connected: try: conn = connect_db(db_path) diff --git a/static/js/market.js b/static/js/market.js index aa29c02..692f218 100644 --- a/static/js/market.js +++ b/static/js/market.js @@ -576,6 +576,7 @@ function klineSourceLabel(src) { if (src === 'ctp') return 'CTP'; + if (src === 'ctp+remote') return '新浪+CTP'; if (src === 'local') return '本地缓存'; return '新浪'; } diff --git a/templates/fees.html b/templates/fees.html index 29d0b08..87b2d24 100644 --- a/templates/fees.html +++ b/templates/fees.html @@ -4,6 +4,15 @@ {% endblock %} {% block content %} @@ -36,6 +45,15 @@ CTP 未连接 {% endif %} + {% if fee_counts %} +

+ 已缓存: + {% if fee_counts.get('ctp') %}CTP {{ fee_counts.ctp }}{% endif %} + {% if fee_counts.get('local') %}local {{ fee_counts.local }}{% endif %} + {% if fee_counts.get('json') %}json {{ fee_counts.json }}{% endif %} + {% if fee_counts.get('manual') %}manual {{ fee_counts.manual }}{% endif %} +

+ {% endif %} @@ -63,10 +81,10 @@ -
+

品种费率表

-
-
+
+
@@ -79,23 +97,20 @@ {% for r in rates %} + {% set fid = 'fee-row-' ~ r.product %} - - - - - - - - - - - - - - - - + + + + + + + + + + + + {% else %} @@ -103,9 +118,18 @@
{{ r.product }}{{ r.source or 'local' }}{{ (r.updated_at or '')[:16] }}{{ r.product }}{{ r.source or 'local' }}{{ (r.updated_at or '')[:16] }}
暂无费率,请连接 CTP 后同步
+ {% for r in rates %} + + {% endfor %}
-

+

公式:单边 = 固定(元/手)×手数 + 比例×价格×乘数×手数;往返 = 开仓 + 平仓(平今/平昨自动判断)。 + {% if fee_source_mode == 'ctp' and ctp_connected and not fee_counts.get('ctp') %} +
当前无 CTP 费率缓存,请点击「从 CTP 同步费率」。 + {% endif %}

{% endblock %} diff --git a/templates/market.html b/templates/market.html index 96f54bc..a9cd180 100644 --- a/templates/market.html +++ b/templates/market.html @@ -45,7 +45,7 @@
请选择合约并点击「查看」
连接中…
-

数据来源:{% if ctp_connected %}CTP 柜台 tick 聚合(实时价与 K 线){% else %}CTP 未连接时 K 线与报价回退新浪{% endif %}。拖拽左右平移、滚轮缩放;按住图表上下拖动可平移价格轴。可视区内自动标注最高/最低价。

+

数据来源:{% if ctp_connected %}报价来自 CTP;K 线历史由新浪补齐、最新 bar 由 CTP tick 更新{% else %}CTP 未连接时 K 线与报价回退新浪{% endif %}。拖拽左右平移、滚轮缩放;按住图表上下拖动可平移价格轴。可视区内自动标注最高/最低价。