diff --git a/app.py b/app.py index 2e22f7a..90d91f5 100644 --- a/app.py +++ b/app.py @@ -63,7 +63,12 @@ from stats_engine import ( from kline_store import ensure_kline_tables from kline_stream import kline_hub, sse_format from kline_chart import generate_review_kline_chart, fetch_market_klines, MARKET_PERIODS -from market import get_price as market_get_price, set_ths_refresh_token, get_quote_source_label +from market import ( + fetch_raw_for_volume, + get_price as market_get_price, + set_ths_refresh_token, + get_quote_source_label, +) from db_conn import OperationalError, connect_db, database_label, is_benign_migration_error, is_db_contention_error, is_schema_migration_error, rollback_if_postgres from admin_settings import save_admin_credentials from db_backup import ( @@ -583,10 +588,19 @@ def build_market_quote_payload( if codes: market_code = codes.get("market_code", "") or market_code sina_code = codes.get("sina_code", "") or sina_code - quote_source = "sina" + quote_source = "none" price = None prev_close = None - if not prefer_sina: + if prefer_sina: + mc, sc = resolve_market_codes(symbol, market_code, sina_code) + if mc or sc: + price = market_get_price(mc, sc) + quote_source = "sina" + if prev_close is None and sc: + raw = fetch_raw_for_volume(sc) + if raw and raw.get("prev_close") is not None: + prev_close = raw["prev_close"] + else: try: from vnpy_bridge import ctp_status, ctp_get_tick_detail from trading_context import get_trading_mode @@ -601,17 +615,10 @@ def build_market_quote_payload( prev_close = detail["pre_close"] except Exception: pass - if price is None: - price = fetch_price(symbol, market_code, sina_code) name = symbol codes = ths_to_codes(symbol) if codes: name = codes.get("name", symbol) - if prev_close is None and sina_code: - from market import fetch_raw_for_volume - raw = fetch_raw_for_volume(sina_code) - if raw and raw.get("prev_close") is not None: - prev_close = raw["prev_close"] return { "symbol": symbol, "name": name, @@ -651,23 +658,22 @@ def resolve_market_codes(ths_code: str, market_code: str = "", sina_code: str = def fetch_price(ths_code: str, market_code: str = "", sina_code: str = "") -> Optional[float]: + """业务现价:仅 CTP 柜台 tick,不回退新浪。""" sym = (ths_code or "").strip() - if sym: - try: - from vnpy_bridge import ctp_status, ctp_get_tick_price - from trading_context import get_trading_mode - - mode = get_trading_mode(get_setting) - if ctp_status(mode).get("connected"): - p = ctp_get_tick_price(mode, sym) - if p and p > 0: - return p - except Exception: - pass - mc, sc = resolve_market_codes(sym, market_code, sina_code) - if not mc and not sc: + if not sym: return None - return market_get_price(mc, sc) + try: + from vnpy_bridge import ctp_status, ctp_get_tick_price + from trading_context import get_trading_mode + + mode = get_trading_mode(get_setting) + if ctp_status(mode).get("connected"): + p = ctp_get_tick_price(mode, sym) + if p and p > 0: + return p + except Exception: + pass + return None # —————————————— 监控逻辑 —————————————— @@ -799,7 +805,7 @@ def start_background_threads(): target=lambda: kline_hub.worker_loop( DB_PATH, lambda sym, mc, sc: build_market_quote_payload( - sym, mc, sc, prefer_sina=True, + sym, mc, sc, prefer_sina=False, ), get_mode_fn=lambda: get_trading_mode(get_setting), ), diff --git a/modules/keys/key_monitor_lib.py b/modules/keys/key_monitor_lib.py index 1f0f4d6..5f5407c 100644 --- a/modules/keys/key_monitor_lib.py +++ b/modules/keys/key_monitor_lib.py @@ -165,7 +165,7 @@ def fetch_closed_bar( p, db_path=db_path, trading_mode=trading_mode, - prefer_ctp=False, + prefer_ctp=True, ) bars = data.get("bars") or [] return last_closed_bar(bars, bar_period_minutes(p)) diff --git a/modules/market/kline_chart.py b/modules/market/kline_chart.py index 0259926..f495364 100644 --- a/modules/market/kline_chart.py +++ b/modules/market/kline_chart.py @@ -303,8 +303,7 @@ def fetch_market_klines( except Exception as exc: logger.debug("ctp kline fetch failed %s %s: %s", symbol, p, exc) - need_sina = force_remote or not prefer_ctp or not ctp_bars or len(ctp_bars) < MIN_CTP_KLINE_BARS - + need_sina = not prefer_ctp or force_remote if ctp_bars and len(ctp_bars) >= MIN_CTP_KLINE_BARS: bars = ctp_bars source = "ctp" @@ -321,7 +320,7 @@ def fetch_market_klines( except Exception as exc: logger.warning("kline cache read failed %s %s: %s", chart_sym, p, exc) - if not bars or len(ctp_bars) < MIN_CTP_KLINE_BARS or not prefer_ctp: + if need_sina and (not bars or len(ctp_bars) < MIN_CTP_KLINE_BARS or not prefer_ctp): remote_bars = fetch_sina_klines(symbol, p) if remote_bars: if prefer_ctp and ctp_bars and ctp_connected: @@ -498,7 +497,7 @@ def generate_review_kline_chart( plotted = False for idx, period in enumerate(valid_periods): ax = axes[idx, 0] - bars = fetch_sina_klines(symbol, period) + bars = fetch_market_klines(symbol, period, prefer_ctp=True).get("bars") or [] bars = _select_bars(bars, cutoff, count) if not bars: ax.set_facecolor("#12121a") diff --git a/modules/market/market.py b/modules/market/market.py index 9a5b0ad..0fea3a7 100644 --- a/modules/market/market.py +++ b/modules/market/market.py @@ -43,15 +43,8 @@ def _has_ths_token() -> bool: def get_quote_source_label(*, ctp_connected: bool = False) -> str: """界面展示用行情源说明。""" if ctp_connected: - return "CTP 柜台(已连接)" - source = _quote_source() - if source == "sina": - return "新浪(CTP 未连接时备用)" - if source == "ths": - return "同花顺 iFinD" if _has_ths_token() else "同花顺(未配置 token)" - if _has_ths_token(): - return "同花顺优先,失败回退新浪" - return "新浪(CTP 未连接时备用)" + return "CTP 柜台" + return "CTP 未连接" def _sina_headers() -> dict: diff --git a/modules/market/routes.py b/modules/market/routes.py index b9a69e9..1be2c5e 100644 --- a/modules/market/routes.py +++ b/modules/market/routes.py @@ -182,7 +182,7 @@ def register(deps) -> None: yield sse_format( "quote", build_market_quote_payload( - symbol, market_code, sina_code, prefer_sina=True, + symbol, market_code, sina_code, prefer_sina=False, ), ) while True: @@ -214,7 +214,7 @@ def register(deps) -> None: if not symbol and not market_code: return jsonify({"error": "请提供合约"}), 400 return jsonify(build_market_quote_payload( - symbol, market_code, sina_code, prefer_sina=True, + symbol, market_code, sina_code, prefer_sina=False, )) diff --git a/modules/trading/recommend_trend.py b/modules/trading/recommend_trend.py index b260a4d..87ca83a 100644 --- a/modules/trading/recommend_trend.py +++ b/modules/trading/recommend_trend.py @@ -9,16 +9,13 @@ from __future__ import annotations import logging from typing import Callable, Optional -import requests - -from modules.market.kline_chart import fetch_sina_klines, ths_to_sina_chart_symbol +from modules.market.kline_chart import fetch_market_klines logger = logging.getLogger(__name__) DAILY_LOOKBACK = 7 OVERLAP_WINDOW = 3 OVERLAP_RANGE_THRESHOLD = 0.70 -KLINE_FETCH_TIMEOUT = 5 TREND_LONG = "long" TREND_SHORT = "short" @@ -178,48 +175,13 @@ def analyze_daily_trend(bars: list, *, overlap_threshold: float = OVERLAP_RANGE_ } -def _normalize_daily_bars(raw: list) -> list: - out = [] - for row in raw: - if isinstance(row, list) and len(row) >= 5: - out.append({ - "d": str(row[0]), - "o": float(row[1]), - "h": float(row[2]), - "l": float(row[3]), - "c": float(row[4]), - "v": float(row[5]) if len(row) > 5 and row[5] else 0.0, - }) - elif isinstance(row, dict) and row.get("d"): - out.append({ - "d": str(row["d"]), - "o": float(row.get("o", 0) or 0), - "h": float(row.get("h", 0) or 0), - "l": float(row.get("l", 0) or 0), - "c": float(row.get("c", 0) or 0), - "v": float(row.get("v", 0) or 0), - }) - return out - - -def _fetch_sina_daily_quick(chart_sym: str) -> list: - url = ( - "https://stock2.finance.sina.com.cn/futures/api/json.php/" - f"IndexService.getInnerFuturesDailyKLine?symbol={chart_sym}" - ) +def _fetch_ctp_daily_bars(sym: str) -> list: try: - resp = requests.get( - url, timeout=KLINE_FETCH_TIMEOUT, - headers={"Referer": "https://finance.sina.com.cn"}, - ) - raw = resp.json() - if raw and isinstance(raw, list): - bars = _normalize_daily_bars(raw) - if bars: - return bars + data = fetch_market_klines(sym, "d", prefer_ctp=True) + return data.get("bars") or [] except Exception as exc: - logger.debug("quick daily kline failed %s: %s", chart_sym, exc) - return [] + logger.debug("ctp daily kline failed %s: %s", sym, exc) + return [] def fetch_week_daily_bars( @@ -238,16 +200,7 @@ def fetch_week_daily_bars( return [] return bars[-DAILY_LOOKBACK:] if bars else [] - chart_sym = ths_to_sina_chart_symbol(sym) - if not chart_sym: - return [] - bars = _fetch_sina_daily_quick(chart_sym) - if not bars: - try: - bars = fetch_sina_klines(sym, "d") or [] - except Exception as exc: - logger.debug("fetch week daily fallback failed %s: %s", sym, exc) - return [] + bars = _fetch_ctp_daily_bars(sym) return bars[-DAILY_LOOKBACK:] if bars else [] diff --git a/modules/web/static/js/market.js b/modules/web/static/js/market.js index d524129..3b50dda 100644 --- a/modules/web/static/js/market.js +++ b/modules/web/static/js/market.js @@ -544,9 +544,9 @@ src = ' · ' + klineSourceLabel(lastData.source); } if (isTradingSession()) { - el.textContent = '新浪数据 · 交易中 SSE 推送' + src; + el.textContent = 'K线新浪 · 报价CTP · 交易中 SSE 推送' + src; } else { - el.textContent = '新浪数据 · 非交易时段低频刷新' + src; + el.textContent = 'K线新浪 · 报价CTP · 非交易时段低频刷新' + src; } } @@ -652,7 +652,7 @@ if (data.count) parts.push('共 ' + data.count + ' 根 · ' + periodLabel(data.period)); if (data.source) parts.push('K线 ' + klineSourceLabel(data.source)); if (data.quote_source) { - parts.push('报价 新浪'); + parts.push(data.quote_source === 'ctp' ? '报价 CTP' : '报价 新浪'); } meta.textContent = parts.join(' · '); } diff --git a/modules/web/templates/market.html b/modules/web/templates/market.html index 45d3ab7..ed4acd2 100644 --- a/modules/web/templates/market.html +++ b/modules/web/templates/market.html @@ -55,7 +55,7 @@
图表引擎:TradingView Lightweight Charts(红跌绿涨)。K 线与报价均使用新浪数据。滚轮缩放、拖拽平移;关闭「自动」后拖动查看历史时,推送更新不会重置画面。
+图表引擎:TradingView Lightweight Charts(红跌绿涨)。K 线使用新浪数据,报价使用 CTP 柜台。滚轮缩放、拖拽平移;关闭「自动」后拖动查看历史时,推送更新不会重置画面。