feat(market_overview): 添加市场行情概览API与前端展示功能
在后端API中新增 `/market-overview` 接口,拉取Binance公开市场数据,并计算策略配置与市场状态的对比。前端组件更新以支持市场行情概览的展示,提供实时市场数据与策略匹配情况,提升用户体验与决策支持。
This commit is contained in:
parent
4ccf067b24
commit
4dd44782c5
|
|
@ -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
182
backend/market_overview.py
Normal 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"
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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() });
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user