auto_trade_sys/backend/market_overview.py
薇薇安 41e53755ea feat(strategy_overview): 添加策略执行概览功能以优化策略分析
在 `market_overview.py` 中新增 `get_strategy_execution_overview` 函数,生成当前策略执行方案与配置项的易读概览,供全局配置页展示。更新后端API以支持该功能,并在前端组件中展示策略执行概览,提升用户对策略执行标准与机制的理解。此改动增强了系统的可用性与用户体验。
2026-02-25 09:39:33 +08:00

342 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
市场行情概览 - 用于全局配置页展示
拉取 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"
def _g(key: str, default: Any, cfg: dict) -> Any:
"""从配置字典取键,支持 bool/数字/字符串。"""
v = cfg.get(key, default)
if v is None:
return default
if isinstance(default, bool):
return str(v).lower() in ("true", "1", "yes")
return v
def get_strategy_execution_overview() -> Dict[str, Any]:
"""
生成「策略执行概览」:当前执行方案、配置项执行情况,用易读文字描述整体策略执行标准与机制。
供全局配置页「策略执行概览」展示。
返回格式:{ "sections": [ { "title": "小节标题", "content": "正文" } ] }
"""
sections = []
cfg = {}
try:
from config_manager import GlobalStrategyConfigManager
mgr = GlobalStrategyConfigManager()
mgr.reload_from_redis()
for key in (
"AUTO_TRADE_ENABLED", "AUTO_TRADE_ONLY_TRENDING", "AUTO_TRADE_ALLOW_4H_NEUTRAL",
"MIN_SIGNAL_STRENGTH", "LOW_VOLATILITY_MIN_SIGNAL_STRENGTH", "MARKET_REGIME_AUTO",
"TOP_N_SYMBOLS", "SCAN_INTERVAL", "PRIMARY_INTERVAL", "CONFIRM_INTERVAL",
"MAX_OPEN_POSITIONS", "MAX_DAILY_ENTRIES", "FIXED_RISK_PERCENT", "USE_FIXED_RISK_SIZING",
"BETA_FILTER_ENABLED", "BETA_FILTER_THRESHOLD", "MARKET_SCHEME",
"USE_ATR_STOP_LOSS", "ATR_STOP_LOSS_MULTIPLIER", "STOP_LOSS_PERCENT",
"TAKE_PROFIT_1_PERCENT", "TAKE_PROFIT_PERCENT", "USE_TRAILING_STOP",
"TRAILING_STOP_ACTIVATION", "TRAILING_STOP_PROTECT", "PROFIT_PROTECTION_ENABLED",
"SMART_ENTRY_ENABLED", "USE_TREND_ENTRY_FILTER", "MAX_TREND_MOVE_BEFORE_ENTRY",
"MAX_RSI_FOR_LONG", "MIN_RSI_FOR_SHORT", "MAX_CHANGE_PERCENT_FOR_LONG", "MAX_CHANGE_PERCENT_FOR_SHORT",
"MIN_VOLUME_24H", "MIN_VOLATILITY", "MIN_HOLD_TIME_SEC",
):
cfg[key] = mgr.get(key)
except Exception as e:
logger.debug("get_strategy_execution_overview 加载配置失败: %s", e)
def pct(x):
if x is None:
return ""
try:
f = float(x)
if abs(f) < 1 and abs(f) > 0:
return f"{f * 100:.2f}%"
return f"{f}%"
except (TypeError, ValueError):
return str(x)
# ---------- 1. 总开关与自动交易条件 ----------
auto_on = _g("AUTO_TRADE_ENABLED", True, cfg)
only_trending = _g("AUTO_TRADE_ONLY_TRENDING", True, cfg)
allow_4h_neutral = _g("AUTO_TRADE_ALLOW_4H_NEUTRAL", False, cfg)
min_strength = _g("MIN_SIGNAL_STRENGTH", 8, cfg)
low_vol_strength = _g("LOW_VOLATILITY_MIN_SIGNAL_STRENGTH", 9, cfg)
regime_auto = _g("MARKET_REGIME_AUTO", True, cfg)
c1 = []
c1.append("自动交易总开关:" + ("开启" if auto_on else "关闭"))
if not auto_on:
c1.append("关闭时仅生成推荐,不会自动下单。")
else:
c1.append("自动下单条件(需同时满足):")
c1.append("• 信号强度 ≥ " + str(min_strength) + "(技术指标综合评分);低波动期自动提高至 " + str(low_vol_strength) + "" + ("已开启" if regime_auto else "未开启") + "市场节奏识别)。")
c1.append("• 市场状态:仅当「仅做趋势市」开启时,要求市场状态为 trending 才下单ranging/unknown 只生成推荐、不自动下单。当前「仅做趋势市」=" + ("" if only_trending else "") + "")
c1.append("• 4H 趋势:允许 4H 中性时自动交易 = " + ("" if allow_4h_neutral else "") + ";为否时 4H 为中性会跳过自动下单。")
sections.append({
"title": "一、总开关与自动交易条件",
"content": "\n".join(c1),
})
# ---------- 2. 扫描与候选池 ----------
top_n = _g("TOP_N_SYMBOLS", 30, cfg)
scan_interval = _g("SCAN_INTERVAL", 900, cfg)
primary = _g("PRIMARY_INTERVAL", "4h", cfg)
confirm = _g("CONFIRM_INTERVAL", "1d", cfg)
min_vol = _g("MIN_VOLUME_24H", 30000000, cfg)
min_vol_str = f"{min_vol / 1e6:.0f} 万 USDT" if isinstance(min_vol, (int, float)) and min_vol >= 1e6 else str(min_vol)
vol_pct = _g("MIN_VOLATILITY", 0.03, cfg)
vol_pct_str = f"{float(vol_pct) * 100:.1f}%" if isinstance(vol_pct, (int, float)) else str(vol_pct)
c2 = []
c2.append("每次扫描取涨跌幅最大的前 " + str(top_n) + " 个交易对进行详细分析;扫描间隔 " + str(scan_interval) + " 秒。")
c2.append("主周期 " + str(primary) + ",确认周期 " + str(confirm) + "24h 成交额 ≥ " + min_vol_str + ",最小波动率 " + vol_pct_str + "")
sections.append({
"title": "二、扫描与候选池",
"content": "\n".join(c2),
})
# ---------- 3. 仓位与风控 ----------
max_pos = _g("MAX_OPEN_POSITIONS", 4, cfg)
max_daily = _g("MAX_DAILY_ENTRIES", 15, cfg)
fixed_risk = _g("USE_FIXED_RISK_SIZING", True, cfg)
risk_pct = _g("FIXED_RISK_PERCENT", 0.01, cfg)
risk_pct_str = pct(risk_pct) if isinstance(risk_pct, (int, float)) and risk_pct <= 1 else f"{float(risk_pct)}%"
c3 = []
c3.append("同时持仓上限 " + str(max_pos) + " 个,每日最多开仓 " + str(max_daily) + " 笔。")
c3.append("固定风险 sizing" + ("开启" if fixed_risk else "关闭") + ";每笔最大亏损 " + risk_pct_str + " 账户资金。")
sections.append({
"title": "三、仓位与风控",
"content": "\n".join(c3),
})
# ---------- 4. 大盘与市场方案 ----------
beta_on = _g("BETA_FILTER_ENABLED", True, cfg)
beta_th = _g("BETA_FILTER_THRESHOLD", -0.005, cfg)
scheme = str(_g("MARKET_SCHEME", "normal", cfg) or "normal")
c4 = []
c4.append("大盘共振过滤:" + ("开启" if beta_on else "关闭") + "BTC/ETH 短周期跌逾 " + pct(beta_th) + " 时屏蔽多单。")
c4.append("当前市场方案:" + scheme + "(用于参数预设)。")
sections.append({
"title": "四、大盘与市场方案",
"content": "\n".join(c4),
})
# ---------- 5. 止损止盈与保护 ----------
use_atr = _g("USE_ATR_STOP_LOSS", True, cfg)
atr_mult = _g("ATR_STOP_LOSS_MULTIPLIER", 2.0, cfg)
sl_pct = _g("STOP_LOSS_PERCENT", 0.05, cfg)
tp1 = _g("TAKE_PROFIT_1_PERCENT", 0.12, cfg)
tp2 = _g("TAKE_PROFIT_PERCENT", 0.25, cfg)
trail = _g("USE_TRAILING_STOP", True, cfg)
trail_act = _g("TRAILING_STOP_ACTIVATION", 0.10, cfg)
trail_prot = _g("TRAILING_STOP_PROTECT", 0.02, cfg)
profit_prot = _g("PROFIT_PROTECTION_ENABLED", True, cfg)
c5 = []
c5.append("止损ATR 动态止损 " + ("开启" if use_atr else "关闭") + (",倍数 " + str(atr_mult) if use_atr else "") + ";固定止损 " + pct(sl_pct) + "")
c5.append("止盈:第一目标 " + pct(tp1) + ",第二目标 " + pct(tp2) + "")
c5.append("盈利保护总开关:" + ("开启" if profit_prot else "关闭") + ";移动止损 " + ("开启" if trail else "关闭") + (",盈利 " + pct(trail_act) + " 激活、保护 " + pct(trail_prot) + " 利润" if trail else "") + "")
sections.append({
"title": "五、止损止盈与保护",
"content": "\n".join(c5),
})
# ---------- 6. 入场与过滤 ----------
smart = _g("SMART_ENTRY_ENABLED", True, cfg)
trend_filter = _g("USE_TREND_ENTRY_FILTER", True, cfg)
max_trend = _g("MAX_TREND_MOVE_BEFORE_ENTRY", 0.04, cfg)
max_rsi_long = _g("MAX_RSI_FOR_LONG", 65, cfg)
min_rsi_short = _g("MIN_RSI_FOR_SHORT", 30, cfg)
max_ch_long = _g("MAX_CHANGE_PERCENT_FOR_LONG", 25, cfg)
max_ch_short = _g("MAX_CHANGE_PERCENT_FOR_SHORT", 10, cfg)
c6 = []
c6.append("智能入场(限价+追价+市价兜底):" + ("开启" if smart else "关闭") + "")
c6.append("趋势入场过滤:" + ("开启" if trend_filter else "关闭") + ";信号方向已走超 " + pct(max_trend) + " 则不再入场。")
c6.append("做多RSI ≤ " + str(max_rsi_long) + "24h 涨跌幅 ≤ " + str(max_ch_long) + "%。做空RSI ≥ " + str(min_rsi_short) + "24h 涨跌幅 ≤ " + str(max_ch_short) + "%")
sections.append({
"title": "六、入场与过滤",
"content": "\n".join(c6),
})
return {"sections": sections}