From 4dd44782c5448cd6e03d280b20808b5fc7a10ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Tue, 24 Feb 2026 15:37:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(market=5Foverview):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=B8=82=E5=9C=BA=E8=A1=8C=E6=83=85=E6=A6=82=E8=A7=88API?= =?UTF-8?q?=E4=B8=8E=E5=89=8D=E7=AB=AF=E5=B1=95=E7=A4=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在后端API中新增 `/market-overview` 接口,拉取Binance公开市场数据,并计算策略配置与市场状态的对比。前端组件更新以支持市场行情概览的展示,提供实时市场数据与策略匹配情况,提升用户体验与决策支持。 --- backend/api/routes/system.py | 56 +++++++ backend/market_overview.py | 182 +++++++++++++++++++++++ frontend/src/components/GlobalConfig.jsx | 76 +++++++++- frontend/src/services/api.js | 10 ++ 4 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 backend/market_overview.py diff --git a/backend/api/routes/system.py b/backend/api/routes/system.py index b470acc..a8e413a 100644 --- a/backend/api/routes/system.py +++ b/backend/api/routes/system.py @@ -1245,6 +1245,62 @@ async def trading_restart_all( raise HTTPException(status_code=500, detail=f"批量重启失败: {e}") +@router.get("/market-overview") +async def market_overview(_admin: Dict[str, Any] = Depends(require_system_admin)) -> Dict[str, Any]: + """ + 市场行情概览:拉取 Binance 公开接口,展示与策略过滤对应的数据。 + 供全局配置页展示,帮助用户确认当前策略方案是否匹配市场。 + """ + try: + from market_overview import get_market_overview + except ImportError: + try: + from backend.market_overview import get_market_overview + except ImportError: + import sys + backend_dir = Path(__file__).parent.parent.parent + if str(backend_dir) not in sys.path: + sys.path.insert(0, str(backend_dir)) + from market_overview import get_market_overview + + data = get_market_overview() + + # 获取当前策略配置,用于对比 + beta_enabled = False + beta_threshold = -0.005 + market_scheme = "normal" + try: + from config_manager import GlobalStrategyConfigManager + mgr = GlobalStrategyConfigManager() + beta_enabled = str(mgr.get("BETA_FILTER_ENABLED", "true")).lower() in ("true", "1", "yes") + try: + beta_threshold = float(mgr.get("BETA_FILTER_THRESHOLD", -0.005)) + except (TypeError, ValueError): + pass + market_scheme = str(mgr.get("MARKET_SCHEME", "normal")).strip().lower() or "normal" + except Exception: + pass + + # 计算大盘共振是否触发(与 strategy._check_beta_filter 一致) + threshold_pct = beta_threshold * 100 + triggered = False + if beta_enabled: + for key in ["btc_15m_change_pct", "btc_1h_change_pct", "eth_15m_change_pct", "eth_1h_change_pct"]: + val = data.get(key) + if val is not None and val < threshold_pct: + triggered = True + break + + data["config"] = { + "BETA_FILTER_ENABLED": beta_enabled, + "BETA_FILTER_THRESHOLD": beta_threshold, + "BETA_FILTER_THRESHOLD_PCT": round(threshold_pct, 2), + "MARKET_SCHEME": market_scheme, + } + data["beta_filter_triggered"] = triggered + return data + + @router.get("/backend/status") async def backend_status(_admin: Dict[str, Any] = Depends(require_system_admin)) -> Dict[str, Any]: """ diff --git a/backend/market_overview.py b/backend/market_overview.py new file mode 100644 index 0000000..e32f1b4 --- /dev/null +++ b/backend/market_overview.py @@ -0,0 +1,182 @@ +""" +市场行情概览 - 用于全局配置页展示 +拉取 Binance 公开接口(无需 API Key),与策略过滤逻辑对应的数据。 +""" +import json +import logging +import ssl +import urllib.request +from typing import Any, Dict, Optional + +logger = logging.getLogger(__name__) + +BINANCE_FUTURES_BASE = "https://fapi.binance.com" +BINANCE_FUTURES_DATA = "https://fapi.binance.com/futures/data" +REQUEST_TIMEOUT = 10 + + +def _http_get(url: str, params: Optional[dict] = None) -> Optional[Any]: + """发起 GET 请求,返回 JSON 或 None。""" + if params: + qs = "&".join(f"{k}={v}" for k, v in params.items()) + url = f"{url}?{qs}" + try: + req = urllib.request.Request(url, headers={"Accept": "application/json"}) + ctx = ssl.create_default_context() + with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT, context=ctx) as resp: + return json.loads(resp.read().decode("utf-8")) + except Exception as e: + logger.debug("market_overview HTTP GET 失败 %s: %s", url[:80], e) + return None + + +def _fetch_klines(symbol: str, interval: str, limit: int) -> Optional[list]: + """获取 K 线数据。""" + data = _http_get( + f"{BINANCE_FUTURES_BASE}/fapi/v1/klines", + {"symbol": symbol, "interval": interval, "limit": limit}, + ) + return data if isinstance(data, list) else None + + +def _compute_change_from_klines(klines: list, periods: int) -> Optional[float]: + """根据 K 线计算最近 N 根的总涨跌幅(比例,如 -0.0167 表示 -1.67%)。""" + if not klines or len(klines) < periods + 1: + return None + first_close = float(klines[0][4]) + last_close = float(klines[-1][4]) + return (last_close - first_close) / first_close if first_close else None + + +def fetch_symbol_change_period(symbol: str, interval: str, periods: int) -> Optional[float]: + """获取指定交易对在指定周期内的涨跌幅(比例)。""" + klines = _fetch_klines(symbol, interval, periods + 1) + return _compute_change_from_klines(klines, periods) if klines else None + + +def fetch_ticker_24h(symbol: str) -> Optional[Dict]: + """获取 24h ticker。""" + data = _http_get(f"{BINANCE_FUTURES_BASE}/fapi/v1/ticker/24hr", {"symbol": symbol}) + return data if isinstance(data, dict) else None + + +def fetch_premium_index(symbol: str) -> Optional[Dict]: + """获取资金费率等。""" + data = _http_get(f"{BINANCE_FUTURES_BASE}/fapi/v1/premiumIndex", {"symbol": symbol}) + return data if isinstance(data, dict) else None + + +def fetch_long_short_ratio(symbol: str = "BTCUSDT", period: str = "1d", limit: int = 1) -> Optional[float]: + """获取大户多空比。""" + data = _http_get( + f"{BINANCE_FUTURES_DATA}/topLongShortPositionRatio", + {"symbol": symbol, "period": period, "limit": limit}, + ) + if not isinstance(data, list) or len(data) == 0: + return None + try: + return float(data[-1].get("longShortRatio", 1)) + except (TypeError, ValueError): + return None + + +def get_market_overview() -> Dict[str, Any]: + """ + 获取市场行情概览,与策略过滤逻辑对应的数据。 + 供全局配置页展示,帮助用户确认当前策略方案是否匹配市场。 + """ + result = { + "btc_24h_change_pct": None, + "eth_24h_change_pct": None, + "btc_15m_change_pct": None, + "btc_1h_change_pct": None, + "eth_15m_change_pct": None, + "eth_1h_change_pct": None, + "btc_funding_rate": None, + "eth_funding_rate": None, + "btc_long_short_ratio": None, + "btc_trend_4h": None, + "market_regime": None, + "beta_filter_triggered": None, + } + + # 24h 涨跌幅 + btc_ticker = fetch_ticker_24h("BTCUSDT") + if btc_ticker is not None: + try: + result["btc_24h_change_pct"] = round(float(btc_ticker.get("priceChangePercent", 0)), 2) + except (TypeError, ValueError): + pass + eth_ticker = fetch_ticker_24h("ETHUSDT") + if eth_ticker is not None: + try: + result["eth_24h_change_pct"] = round(float(eth_ticker.get("priceChangePercent", 0)), 2) + except (TypeError, ValueError): + pass + + # 15m / 1h 涨跌幅(大盘共振过滤用) + btc_15m = fetch_symbol_change_period("BTCUSDT", "15m", 5) + btc_1h = fetch_symbol_change_period("BTCUSDT", "1h", 3) + eth_15m = fetch_symbol_change_period("ETHUSDT", "15m", 5) + eth_1h = fetch_symbol_change_period("ETHUSDT", "1h", 3) + if btc_15m is not None: + result["btc_15m_change_pct"] = round(btc_15m * 100, 2) + if btc_1h is not None: + result["btc_1h_change_pct"] = round(btc_1h * 100, 2) + if eth_15m is not None: + result["eth_15m_change_pct"] = round(eth_15m * 100, 2) + if eth_1h is not None: + result["eth_1h_change_pct"] = round(eth_1h * 100, 2) + + # 资金费率 + btc_prem = fetch_premium_index("BTCUSDT") + if btc_prem is not None: + try: + result["btc_funding_rate"] = round(float(btc_prem.get("lastFundingRate", 0)), 6) + except (TypeError, ValueError): + pass + eth_prem = fetch_premium_index("ETHUSDT") + if eth_prem is not None: + try: + result["eth_funding_rate"] = round(float(eth_prem.get("lastFundingRate", 0)), 6) + except (TypeError, ValueError): + pass + + # 大户多空比 + lsr = fetch_long_short_ratio("BTCUSDT", "1d", 1) + if lsr is not None: + result["btc_long_short_ratio"] = round(lsr, 4) + + # 4H 趋势 + klines_4h = _fetch_klines("BTCUSDT", "4h", 60) + if klines_4h and len(klines_4h) >= 21: + try: + from trading_system.market_regime_detector import compute_trend_4h_from_klines + result["btc_trend_4h"] = compute_trend_4h_from_klines(klines_4h) + except Exception: + result["btc_trend_4h"] = _simple_trend_4h(klines_4h) + + # 市场状态(bull/bear/normal) + try: + from trading_system.market_regime_detector import detect_market_regime + regime, details = detect_market_regime() + result["market_regime"] = regime + result["market_regime_details"] = details + except Exception as e: + logger.debug("market_overview 获取市场状态失败: %s", e) + + return result + + +def _simple_trend_4h(klines: list) -> str: + """简化 4H 趋势:价格 vs 最近一根 K 线前 20 根均价。""" + if len(klines) < 21: + return "neutral" + closes = [float(k[4]) for k in klines] + price = closes[-1] + avg20 = sum(closes[-21:-1]) / 20 + if price > avg20 * 1.002: + return "up" + if price < avg20 * 0.998: + return "down" + return "neutral" diff --git a/frontend/src/components/GlobalConfig.jsx b/frontend/src/components/GlobalConfig.jsx index fa9b6e9..5f16f43 100644 --- a/frontend/src/components/GlobalConfig.jsx +++ b/frontend/src/components/GlobalConfig.jsx @@ -201,6 +201,7 @@ const GlobalConfig = () => { const [backendStatus, setBackendStatus] = useState(null) const [recommendationsStatus, setRecommendationsStatus] = useState(null) const [servicesSummary, setServicesSummary] = useState(null) + const [marketOverview, setMarketOverview] = useState(null) const [systemBusy, setSystemBusy] = useState(false) // 预设方案相关 @@ -565,6 +566,15 @@ const GlobalConfig = () => { } } + const loadMarketOverview = async () => { + try { + const res = await api.getMarketOverview() + setMarketOverview(res) + } catch (error) { + setMarketOverview(null) + } + } + useEffect(() => { const init = async () => { if (isAdmin) { @@ -573,7 +583,8 @@ const GlobalConfig = () => { loadConfigs(), loadSystemStatus(), loadBackendStatus(), - loadRecommendationsStatus() + loadRecommendationsStatus(), + loadMarketOverview() ]) } setLoading(false) @@ -1155,6 +1166,69 @@ const GlobalConfig = () => { )} + {/* 市场行情概览(供确认策略方案是否匹配市场) */} + {isAdmin && ( +
+
+

市场行情概览

+ +
+ {marketOverview ? ( +
+
+
BTC 24h
+
= 0 ? '#4caf50' : '#f44336' }}> + {marketOverview.btc_24h_change_pct != null ? `${marketOverview.btc_24h_change_pct}%` : '—'} +
+
+
+
ETH 24h
+
= 0 ? '#4caf50' : '#f44336' }}> + {marketOverview.eth_24h_change_pct != null ? `${marketOverview.eth_24h_change_pct}%` : '—'} +
+
+
+
BTC 15m / 1h
+
+ {marketOverview.btc_15m_change_pct != null ? marketOverview.btc_15m_change_pct : '—'}% / {marketOverview.btc_1h_change_pct != null ? marketOverview.btc_1h_change_pct : '—'}% +
+
+
+
ETH 15m / 1h
+
+ {marketOverview.eth_15m_change_pct != null ? marketOverview.eth_15m_change_pct : '—'}% / {marketOverview.eth_1h_change_pct != null ? marketOverview.eth_1h_change_pct : '—'}% +
+
+
+
大盘共振过滤
+
+ {marketOverview.beta_filter_triggered ? '已触发(屏蔽多单)' : '未触发'} +
+ {marketOverview.config && ( +
+ 阈值 {marketOverview.config.BETA_FILTER_THRESHOLD_PCT}% · {marketOverview.config.BETA_FILTER_ENABLED ? '已开启' : '已关闭'} +
+ )} +
+
+
市场状态 / 4H
+
+ {marketOverview.market_regime || '—'} / {marketOverview.btc_trend_4h || '—'} +
+
+
+
当前方案
+
{marketOverview.config?.MARKET_SCHEME || '—'}
+
+
+ ) : ( +
加载中或拉取失败,请点击刷新
+ )} +
+ )} + {/* 系统控制 */} {isAdmin && (
diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 3453c26..a6f5d95 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -740,6 +740,16 @@ export const api = { return response.json() }, + // 市场行情概览(用于确认策略方案是否匹配市场) + getMarketOverview: async () => { + const response = await fetch(buildUrl('/api/system/market-overview'), { headers: withAccountHeaders() }); + if (!response.ok) { + const error = await response.json().catch(() => ({ detail: '获取市场行情失败' })); + throw new Error(error.detail || '获取市场行情失败'); + } + return response.json(); + }, + // 后端控制(uvicorn) getBackendStatus: async () => { const response = await fetch(buildUrl('/api/system/backend/status'), { headers: withAccountHeaders() });