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}")
|
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")
|
@router.get("/backend/status")
|
||||||
async def backend_status(_admin: Dict[str, Any] = Depends(require_system_admin)) -> Dict[str, Any]:
|
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 [backendStatus, setBackendStatus] = useState(null)
|
||||||
const [recommendationsStatus, setRecommendationsStatus] = useState(null)
|
const [recommendationsStatus, setRecommendationsStatus] = useState(null)
|
||||||
const [servicesSummary, setServicesSummary] = useState(null)
|
const [servicesSummary, setServicesSummary] = useState(null)
|
||||||
|
const [marketOverview, setMarketOverview] = useState(null)
|
||||||
const [systemBusy, setSystemBusy] = useState(false)
|
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(() => {
|
useEffect(() => {
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
|
|
@ -573,7 +583,8 @@ const GlobalConfig = () => {
|
||||||
loadConfigs(),
|
loadConfigs(),
|
||||||
loadSystemStatus(),
|
loadSystemStatus(),
|
||||||
loadBackendStatus(),
|
loadBackendStatus(),
|
||||||
loadRecommendationsStatus()
|
loadRecommendationsStatus(),
|
||||||
|
loadMarketOverview()
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
@ -1155,6 +1166,69 @@ const GlobalConfig = () => {
|
||||||
</div>
|
</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 && (
|
{isAdmin && (
|
||||||
<section className="global-section system-section">
|
<section className="global-section system-section">
|
||||||
|
|
|
||||||
|
|
@ -740,6 +740,16 @@ export const api = {
|
||||||
return response.json()
|
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)
|
// 后端控制(uvicorn)
|
||||||
getBackendStatus: async () => {
|
getBackendStatus: async () => {
|
||||||
const response = await fetch(buildUrl('/api/system/backend/status'), { headers: withAccountHeaders() });
|
const response = await fetch(buildUrl('/api/system/backend/status'), { headers: withAccountHeaders() });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user