feat(market_overview): 添加市场行情概览API与前端展示功能

在后端API中新增 `/market-overview` 接口,拉取Binance公开市场数据,并计算策略配置与市场状态的对比。前端组件更新以支持市场行情概览的展示,提供实时市场数据与策略匹配情况,提升用户体验与决策支持。
This commit is contained in:
薇薇安 2026-02-24 15:37:06 +08:00
parent 4ccf067b24
commit 4dd44782c5
4 changed files with 323 additions and 1 deletions

View File

@ -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]:
"""

182
backend/market_overview.py Normal file
View File

@ -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"

View File

@ -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 = () => {
</div>
)}
{/* 市场行情概览(供确认策略方案是否匹配市场) */}
{isAdmin && (
<section className="global-section" style={{ marginBottom: '20px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
<h3>市场行情概览</h3>
<button type="button" className="system-btn primary" onClick={loadMarketOverview}>
刷新
</button>
</div>
{marketOverview ? (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: '12px', fontSize: '14px' }}>
<div style={{ padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
<div style={{ color: '#666', fontSize: '12px' }}>BTC 24h</div>
<div style={{ fontWeight: 600, color: marketOverview.btc_24h_change_pct >= 0 ? '#4caf50' : '#f44336' }}>
{marketOverview.btc_24h_change_pct != null ? `${marketOverview.btc_24h_change_pct}%` : '—'}
</div>
</div>
<div style={{ padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
<div style={{ color: '#666', fontSize: '12px' }}>ETH 24h</div>
<div style={{ fontWeight: 600, color: marketOverview.eth_24h_change_pct >= 0 ? '#4caf50' : '#f44336' }}>
{marketOverview.eth_24h_change_pct != null ? `${marketOverview.eth_24h_change_pct}%` : '—'}
</div>
</div>
<div style={{ padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
<div style={{ color: '#666', fontSize: '12px' }}>BTC 15m / 1h</div>
<div style={{ fontWeight: 500 }}>
{marketOverview.btc_15m_change_pct != null ? marketOverview.btc_15m_change_pct : '—'}% / {marketOverview.btc_1h_change_pct != null ? marketOverview.btc_1h_change_pct : ''}%
</div>
</div>
<div style={{ padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
<div style={{ color: '#666', fontSize: '12px' }}>ETH 15m / 1h</div>
<div style={{ fontWeight: 500 }}>
{marketOverview.eth_15m_change_pct != null ? marketOverview.eth_15m_change_pct : '—'}% / {marketOverview.eth_1h_change_pct != null ? marketOverview.eth_1h_change_pct : ''}%
</div>
</div>
<div style={{ padding: '10px', background: marketOverview.beta_filter_triggered ? '#ffebee' : '#e8f5e9', borderRadius: '6px' }}>
<div style={{ color: '#666', fontSize: '12px' }}>大盘共振过滤</div>
<div style={{ fontWeight: 600, color: marketOverview.beta_filter_triggered ? '#c62828' : '#2e7d32' }}>
{marketOverview.beta_filter_triggered ? '已触发(屏蔽多单)' : '未触发'}
</div>
{marketOverview.config && (
<div style={{ fontSize: '11px', color: '#888', marginTop: '4px' }}>
阈值 {marketOverview.config.BETA_FILTER_THRESHOLD_PCT}% · {marketOverview.config.BETA_FILTER_ENABLED ? '已开启' : '已关闭'}
</div>
)}
</div>
<div style={{ padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
<div style={{ color: '#666', fontSize: '12px' }}>市场状态 / 4H</div>
<div style={{ fontWeight: 500 }}>
{marketOverview.market_regime || '—'} / {marketOverview.btc_trend_4h || '—'}
</div>
</div>
<div style={{ padding: '10px', background: '#f8f9fa', borderRadius: '6px' }}>
<div style={{ color: '#666', fontSize: '12px' }}>当前方案</div>
<div style={{ fontWeight: 600 }}>{marketOverview.config?.MARKET_SCHEME || '—'}</div>
</div>
</div>
) : (
<div style={{ color: '#888', fontSize: '14px' }}>加载中或拉取失败请点击刷新</div>
)}
</section>
)}
{/* 系统控制 */}
{isAdmin && (
<section className="global-section system-section">

View File

@ -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() });