Route business quotes and K-lines to CTP; keep Sina only for market chart page.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-07-02 15:14:13 +08:00
parent 98d63f38bf
commit 972ab5d08b
9 changed files with 52 additions and 101 deletions
+21 -15
View File
@@ -63,7 +63,12 @@ from stats_engine import (
from kline_store import ensure_kline_tables from kline_store import ensure_kline_tables
from kline_stream import kline_hub, sse_format from kline_stream import kline_hub, sse_format
from kline_chart import generate_review_kline_chart, fetch_market_klines, MARKET_PERIODS 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 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 admin_settings import save_admin_credentials
from db_backup import ( from db_backup import (
@@ -583,10 +588,19 @@ def build_market_quote_payload(
if codes: if codes:
market_code = codes.get("market_code", "") or market_code market_code = codes.get("market_code", "") or market_code
sina_code = codes.get("sina_code", "") or sina_code sina_code = codes.get("sina_code", "") or sina_code
quote_source = "sina" quote_source = "none"
price = None price = None
prev_close = 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: try:
from vnpy_bridge import ctp_status, ctp_get_tick_detail from vnpy_bridge import ctp_status, ctp_get_tick_detail
from trading_context import get_trading_mode from trading_context import get_trading_mode
@@ -601,17 +615,10 @@ def build_market_quote_payload(
prev_close = detail["pre_close"] prev_close = detail["pre_close"]
except Exception: except Exception:
pass pass
if price is None:
price = fetch_price(symbol, market_code, sina_code)
name = symbol name = symbol
codes = ths_to_codes(symbol) codes = ths_to_codes(symbol)
if codes: if codes:
name = codes.get("name", symbol) 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 { return {
"symbol": symbol, "symbol": symbol,
"name": name, "name": name,
@@ -651,8 +658,10 @@ 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]: def fetch_price(ths_code: str, market_code: str = "", sina_code: str = "") -> Optional[float]:
"""业务现价:仅 CTP 柜台 tick,不回退新浪。"""
sym = (ths_code or "").strip() sym = (ths_code or "").strip()
if sym: if not sym:
return None
try: try:
from vnpy_bridge import ctp_status, ctp_get_tick_price from vnpy_bridge import ctp_status, ctp_get_tick_price
from trading_context import get_trading_mode from trading_context import get_trading_mode
@@ -664,10 +673,7 @@ def fetch_price(ths_code: str, market_code: str = "", sina_code: str = "") -> Op
return p return p
except Exception: except Exception:
pass pass
mc, sc = resolve_market_codes(sym, market_code, sina_code)
if not mc and not sc:
return None return None
return market_get_price(mc, sc)
# —————————————— 监控逻辑 —————————————— # —————————————— 监控逻辑 ——————————————
@@ -799,7 +805,7 @@ def start_background_threads():
target=lambda: kline_hub.worker_loop( target=lambda: kline_hub.worker_loop(
DB_PATH, DB_PATH,
lambda sym, mc, sc: build_market_quote_payload( 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), get_mode_fn=lambda: get_trading_mode(get_setting),
), ),
+1 -1
View File
@@ -165,7 +165,7 @@ def fetch_closed_bar(
p, p,
db_path=db_path, db_path=db_path,
trading_mode=trading_mode, trading_mode=trading_mode,
prefer_ctp=False, prefer_ctp=True,
) )
bars = data.get("bars") or [] bars = data.get("bars") or []
return last_closed_bar(bars, bar_period_minutes(p)) return last_closed_bar(bars, bar_period_minutes(p))
+3 -4
View File
@@ -303,8 +303,7 @@ def fetch_market_klines(
except Exception as exc: except Exception as exc:
logger.debug("ctp kline fetch failed %s %s: %s", symbol, p, 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: if ctp_bars and len(ctp_bars) >= MIN_CTP_KLINE_BARS:
bars = ctp_bars bars = ctp_bars
source = "ctp" source = "ctp"
@@ -321,7 +320,7 @@ def fetch_market_klines(
except Exception as exc: except Exception as exc:
logger.warning("kline cache read failed %s %s: %s", chart_sym, p, 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) remote_bars = fetch_sina_klines(symbol, p)
if remote_bars: if remote_bars:
if prefer_ctp and ctp_bars and ctp_connected: if prefer_ctp and ctp_bars and ctp_connected:
@@ -498,7 +497,7 @@ def generate_review_kline_chart(
plotted = False plotted = False
for idx, period in enumerate(valid_periods): for idx, period in enumerate(valid_periods):
ax = axes[idx, 0] 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) bars = _select_bars(bars, cutoff, count)
if not bars: if not bars:
ax.set_facecolor("#12121a") ax.set_facecolor("#12121a")
+2 -9
View File
@@ -43,15 +43,8 @@ def _has_ths_token() -> bool:
def get_quote_source_label(*, ctp_connected: bool = False) -> str: def get_quote_source_label(*, ctp_connected: bool = False) -> str:
"""界面展示用行情源说明。""" """界面展示用行情源说明。"""
if ctp_connected: if ctp_connected:
return "CTP 柜台(已连接)" return "CTP 柜台"
source = _quote_source() return "CTP 未连接"
if source == "sina":
return "新浪(CTP 未连接时备用)"
if source == "ths":
return "同花顺 iFinD" if _has_ths_token() else "同花顺(未配置 token"
if _has_ths_token():
return "同花顺优先,失败回退新浪"
return "新浪(CTP 未连接时备用)"
def _sina_headers() -> dict: def _sina_headers() -> dict:
+2 -2
View File
@@ -182,7 +182,7 @@ def register(deps) -> None:
yield sse_format( yield sse_format(
"quote", "quote",
build_market_quote_payload( build_market_quote_payload(
symbol, market_code, sina_code, prefer_sina=True, symbol, market_code, sina_code, prefer_sina=False,
), ),
) )
while True: while True:
@@ -214,7 +214,7 @@ def register(deps) -> None:
if not symbol and not market_code: if not symbol and not market_code:
return jsonify({"error": "请提供合约"}), 400 return jsonify({"error": "请提供合约"}), 400
return jsonify(build_market_quote_payload( return jsonify(build_market_quote_payload(
symbol, market_code, sina_code, prefer_sina=True, symbol, market_code, sina_code, prefer_sina=False,
)) ))
+6 -53
View File
@@ -9,16 +9,13 @@ from __future__ import annotations
import logging import logging
from typing import Callable, Optional from typing import Callable, Optional
import requests from modules.market.kline_chart import fetch_market_klines
from modules.market.kline_chart import fetch_sina_klines, ths_to_sina_chart_symbol
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
DAILY_LOOKBACK = 7 DAILY_LOOKBACK = 7
OVERLAP_WINDOW = 3 OVERLAP_WINDOW = 3
OVERLAP_RANGE_THRESHOLD = 0.70 OVERLAP_RANGE_THRESHOLD = 0.70
KLINE_FETCH_TIMEOUT = 5
TREND_LONG = "long" TREND_LONG = "long"
TREND_SHORT = "short" TREND_SHORT = "short"
@@ -178,47 +175,12 @@ def analyze_daily_trend(bars: list, *, overlap_threshold: float = OVERLAP_RANGE_
} }
def _normalize_daily_bars(raw: list) -> list: def _fetch_ctp_daily_bars(sym: str) -> 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}"
)
try: try:
resp = requests.get( data = fetch_market_klines(sym, "d", prefer_ctp=True)
url, timeout=KLINE_FETCH_TIMEOUT, return data.get("bars") or []
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
except Exception as exc: except Exception as exc:
logger.debug("quick daily kline failed %s: %s", chart_sym, exc) logger.debug("ctp daily kline failed %s: %s", sym, exc)
return [] return []
@@ -238,16 +200,7 @@ def fetch_week_daily_bars(
return [] return []
return bars[-DAILY_LOOKBACK:] if bars else [] return bars[-DAILY_LOOKBACK:] if bars else []
chart_sym = ths_to_sina_chart_symbol(sym) bars = _fetch_ctp_daily_bars(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 []
return bars[-DAILY_LOOKBACK:] if bars else [] return bars[-DAILY_LOOKBACK:] if bars else []
+3 -3
View File
@@ -544,9 +544,9 @@
src = ' · ' + klineSourceLabel(lastData.source); src = ' · ' + klineSourceLabel(lastData.source);
} }
if (isTradingSession()) { if (isTradingSession()) {
el.textContent = '新浪数据 · 交易中 SSE 推送' + src; el.textContent = 'K线新浪 · 报价CTP · 交易中 SSE 推送' + src;
} else { } 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.count) parts.push('共 ' + data.count + ' 根 · ' + periodLabel(data.period));
if (data.source) parts.push('K线 ' + klineSourceLabel(data.source)); if (data.source) parts.push('K线 ' + klineSourceLabel(data.source));
if (data.quote_source) { if (data.quote_source) {
parts.push('报价 新浪'); parts.push(data.quote_source === 'ctp' ? '报价 CTP' : '报价 新浪');
} }
meta.textContent = parts.join(' · '); meta.textContent = parts.join(' · ');
} }
+1 -1
View File
@@ -55,7 +55,7 @@
<div class="market-chart-empty" id="market-chart-empty">请选择合约并点击「查看」</div> <div class="market-chart-empty" id="market-chart-empty">请选择合约并点击「查看」</div>
<div class="market-chart-loading" id="market-chart-loading">连接中…</div> <div class="market-chart-loading" id="market-chart-loading">连接中…</div>
</div> </div>
<p class="hint">图表引擎:TradingView Lightweight Charts(红跌绿涨)。K 线与报价均使用<strong>新浪</strong>数据。滚轮缩放、拖拽平移;关闭「自动」后拖动查看历史时,推送更新不会重置画面。</p> <p class="hint">图表引擎:TradingView Lightweight Charts(红跌绿涨)。<strong>K 线</strong>使用新浪数据,<strong>报价</strong>使用 CTP 柜台。滚轮缩放、拖拽平移;关闭「自动」后拖动查看历史时,推送更新不会重置画面。</p>
</div> </div>
<style> <style>
+1 -1
View File
@@ -397,7 +397,7 @@
<div class="card-inner"> <div class="card-inner">
<p class="hint" style="font-size:.88rem;line-height:1.6;margin:0"> <p class="hint" style="font-size:.88rem;line-height:1.6;margin:0">
当前行情源:<strong class="text-accent">{{ quote_label }}</strong><br> 当前行情源:<strong class="text-accent">{{ quote_label }}</strong><br>
CTP 已连接时使用<strong>柜台行情</strong>;未连接时回退新浪接口。<br> 现价、浮盈、关键位等业务数据均使用<strong>CTP 柜台行情</strong>(需已连接);仅行情页 K 线图表使用新浪接口。<br>
合约代码按同花顺格式(如 ag2608、IF2606)。 合约代码按同花顺格式(如 ag2608、IF2606)。
</p> </p>
</div> </div>