This commit is contained in:
aaron 2026-03-31 13:27:02 +08:00
parent 4206258dfe
commit 278015d0b1
4 changed files with 350 additions and 116 deletions

View File

@ -3304,7 +3304,7 @@ class CryptoAgent:
def _validate_data(self, data: Dict[str, pd.DataFrame]) -> bool: def _validate_data(self, data: Dict[str, pd.DataFrame]) -> bool:
"""验证数据完整性""" """验证数据完整性"""
required_intervals = ['1m', '5m', '15m', '30m', '1h', '4h'] required_intervals = ['5m', '15m', '1h', '4h', '1d']
for interval in required_intervals: for interval in required_intervals:
if interval not in data or data[interval].empty: if interval not in data or data[interval].empty:
return False return False

View File

@ -31,8 +31,8 @@ class MarketSignalAnalyzer:
TREND_ANALYSIS_TEMPERATURE = 0.10 TREND_ANALYSIS_TEMPERATURE = 0.10
ANALYSIS_MAX_TOKENS = 1200 ANALYSIS_MAX_TOKENS = 1200
LANE_MIN_CONFIDENCE = { LANE_MIN_CONFIDENCE = {
'short_term': 60, 'short_term': 70,
'medium_term': 65, 'medium_term': 70,
} }
LANE_MIN_RISK_REWARD = { LANE_MIN_RISK_REWARD = {
'short_term': 1.5, 'short_term': 1.5,
@ -51,7 +51,7 @@ class MarketSignalAnalyzer:
INTRADAY_ANALYSIS_PROMPT = """你是一位专业的加密货币日内交易员,只负责生成 short_term 信号。 INTRADAY_ANALYSIS_PROMPT = """你是一位专业的加密货币日内交易员,只负责生成 short_term 信号。
你的任务是基于 5m / 15m / 30m当日开盘VWAP开盘区间关键位Fib 回撤位和衍生品拥挤度判断未来 30 分钟到 4 小时内是否存在可执行 setup 你的任务是基于 5m / 15m当日开盘VWAP开盘区间关键位Fib 回撤位和衍生品拥挤度判断未来 30 分钟到 4 小时内是否存在可执行 setup
执行原则 执行原则
1. 先判断日内 regimetrending / ranging / neutral 1. 先判断日内 regimetrending / ranging / neutral
@ -75,8 +75,8 @@ class MarketSignalAnalyzer:
8. grade / confidence 约束 8. grade / confidence 约束
- A: 80-100结构位置量价时机都对齐 - A: 80-100结构位置量价时机都对齐
- B: 70-79条件较完整但仍有一项次优 - B: 70-79条件较完整但仍有一项次优
- C: 60-69只有轻仓试错级别 - C: 70-71只有轻仓试错级别
- 60 以下不要输出交易信号 - 70 以下不要输出交易信号
9. 止损止盈距离下限 9. 止损止盈距离下限
- short_term 止损距离至少 0.6% - short_term 止损距离至少 0.6%
- short_term 止盈距离至少 1.0% - short_term 止盈距离至少 1.0%
@ -118,13 +118,13 @@ class MarketSignalAnalyzer:
TREND_ANALYSIS_PROMPT = """你是一位专业的加密货币趋势交易员,只负责生成 medium_term 信号。 TREND_ANALYSIS_PROMPT = """你是一位专业的加密货币趋势交易员,只负责生成 medium_term 信号。
你的任务是基于 1h / 4h关键位Fib 回撤/扩展位趋势阶段反转检测衍生品拥挤度和新闻催化判断未来 4 小时到 3 内是否存在趋势 setup 你的任务是基于 1h / 4h / 1d关键位Fib 回撤/扩展位趋势阶段反转检测衍生品拥挤度和新闻催化判断未来 4 小时到 1 内是否存在趋势 setup
执行原则 执行原则
1. 4h 决定大方向1h 决定节奏与入场位置 1. 4h/1d 决定大方向1h 决定节奏与入场位置
2. 只做两类交易 2. 只做两类交易
- 趋势延续4h 趋势明确1h 回踩关键位后确认继续 - 趋势延续4h/1d 趋势明确1h 回踩关键位后确认继续
- 趋势反转4h 结构和 1h 动能同时改善且反转证据充分 - 趋势反转4h/1d 结构和 1h 动能同时改善且反转证据充分
3. 禁止仅凭 15m 噪音逆 4h 开仓 3. 禁止仅凭 15m 噪音逆 4h 开仓
4. 趋势晚期资金费率过热或价格过度偏离关键均线时要显著降低开仓积极性 4. 趋势晚期资金费率过热或价格过度偏离关键均线时要显著降低开仓积极性
5. 没有清晰位置优势就不交易 5. 没有清晰位置优势就不交易
@ -134,16 +134,16 @@ class MarketSignalAnalyzer:
信号要求 信号要求
1. 只允许输出 0 1 medium_term 信号 1. 只允许输出 0 1 medium_term 信号
2. 盈亏比至少 1:1.8 2. 盈亏比至少 1:1.8
3. 如果 4h 1h 明显冲突优先返回空信号 3. 如果 4h/1d 1h 明显冲突优先返回空信号
4. 反转信号必须比延续信号更严格 4. 反转信号必须比延续信号更严格
5. 如果趋势处于晚期且没有回踩确认或反转证据不足必须返回空信号 5. 如果趋势处于晚期且没有回踩确认或反转证据不足必须返回空信号
6. 只有在位置优势和方向一致性都充分时才允许开仓 6. 只有在位置优势和方向一致性都充分时才允许开仓
7. 趋势延续单的 entry 应优先靠近优先支撑/阻力或对应共振区不在远离关键位的位置追价 7. 趋势延续单的 entry 应优先靠近优先支撑/阻力或对应共振区不在远离关键位的位置追价
8. grade / confidence 约束 8. grade / confidence 约束
- A: 82-1004h/1h 同向且位置优 - A: 82-1004h/1d/1h 同向且位置优
- B: 72-81趋势或反转证据较完整 - B: 72-81趋势或反转证据较完整
- C: 65-71仅限早期确认不足的轻仓趋势尝试 - C: 70-71仅限早期确认不足的轻仓趋势尝试
- 65 以下不要输出交易信号 - 70 以下不要输出交易信号
9. 止损止盈距离下限 9. 止损止盈距离下限
- medium_term 止损距离至少 1.0% - medium_term 止损距离至少 1.0%
- medium_term 止盈距离至少 2.0% - medium_term 止盈距离至少 2.0%
@ -270,12 +270,12 @@ class MarketSignalAnalyzer:
feature_5m = self._summarize_timeframe_features(data.get('5m'), '5m') feature_5m = self._summarize_timeframe_features(data.get('5m'), '5m')
feature_15m = self._summarize_timeframe_features(data.get('15m'), '15m') feature_15m = self._summarize_timeframe_features(data.get('15m'), '15m')
feature_30m = self._summarize_timeframe_features(data.get('30m'), '30m')
feature_1h = self._summarize_timeframe_features(data.get('1h'), '1h') feature_1h = self._summarize_timeframe_features(data.get('1h'), '1h')
feature_4h = self._summarize_timeframe_features(data.get('4h'), '4h') feature_4h = self._summarize_timeframe_features(data.get('4h'), '4h')
feature_1d = self._summarize_timeframe_features(data.get('1d'), '1d')
intraday_alignment = self._describe_alignment([feature_5m, feature_15m, feature_30m]) intraday_alignment = self._describe_alignment([feature_5m, feature_15m])
trend_alignment = self._describe_alignment([feature_1h, feature_4h]) trend_alignment = self._describe_alignment([feature_1h, feature_4h, feature_1d])
range_zone = self._detect_range_zone(data) range_zone = self._detect_range_zone(data)
reversal_detection = self._detect_trend_reversal(data) reversal_detection = self._detect_trend_reversal(data)
trend_stage = self._detect_trend_stage(data) trend_stage = self._detect_trend_stage(data)
@ -304,7 +304,6 @@ class MarketSignalAnalyzer:
"## 日内特征", "## 日内特征",
self._format_feature_line(feature_5m), self._format_feature_line(feature_5m),
self._format_feature_line(feature_15m), self._format_feature_line(feature_15m),
self._format_feature_line(feature_30m),
f"- 日内级别一致性: {intraday_alignment}", f"- 日内级别一致性: {intraday_alignment}",
] ]
@ -312,6 +311,7 @@ class MarketSignalAnalyzer:
"## 趋势特征", "## 趋势特征",
self._format_feature_line(feature_1h), self._format_feature_line(feature_1h),
self._format_feature_line(feature_4h), self._format_feature_line(feature_4h),
self._format_feature_line(feature_1d),
f"- 趋势级别一致性: {trend_alignment}", f"- 趋势级别一致性: {trend_alignment}",
] ]
@ -356,11 +356,35 @@ class MarketSignalAnalyzer:
else: else:
trend_parts.append("- 反转检测: 无显著反转信号") trend_parts.append("- 反转检测: 无显著反转信号")
# 震荡市场量化
range_metrics = self._quantify_ranging_state(data)
range_warning = ""
if range_metrics['regime'] in ('ranging', 'transitional'):
range_warning = (
f"\n## 震荡市场警告\n"
f"- 当前市场状态: {range_metrics['regime']} (score {range_metrics['regime_score']}/100)\n"
f"- ATR(1h): {range_metrics['atr_pct']:.3f}%\n"
f"- EMA收敛度: {range_metrics['ema_convergence_pct']:.3f}%\n"
f"- 方向效率: {range_metrics['range_efficiency']:.2f} (0=纯震荡, 1=纯趋势)\n"
f"- ADX: {range_metrics['adx']:.1f}\n"
f"- 建议: {'避免趋势策略,只做区间边界反转' if range_metrics['regime'] == 'ranging' else '谨慎开仓,降低仓位'}\n"
)
intraday_parts.append(
f"- ⚠️ 震荡市场: regime={range_metrics['regime']} (score {range_metrics['regime_score']}) | "
f"ATR={range_metrics['atr_pct']:.2f}% | EMA收敛={range_metrics['ema_convergence_pct']:.2f}% | "
f"效率={range_metrics['range_efficiency']:.2f} | ADX={range_metrics['adx']:.1f}"
)
trend_parts.append(
f"- 市场结构: {range_metrics['regime']} | ADX={range_metrics['adx']:.1f} | "
f"{'BB收口' if range_metrics['bb_squeeze'] else 'BB正常'}"
)
return { return {
'snapshot': "\n".join(snapshot_parts), 'snapshot': "\n".join(snapshot_parts),
'intraday': "\n".join(intraday_parts), 'intraday': "\n".join(intraday_parts),
'trend': "\n".join(trend_parts), 'trend': "\n".join(trend_parts),
'levels': "\n".join(levels_parts), 'levels': "\n".join(levels_parts),
'range_warning': range_warning,
} }
def _get_session_open(self, df: Optional[pd.DataFrame]) -> Optional[float]: def _get_session_open(self, df: Optional[pd.DataFrame]) -> Optional[float]:
@ -449,6 +473,8 @@ class MarketSignalAnalyzer:
'distance_to_recent_high': None, 'distance_to_recent_high': None,
'distance_to_recent_low': None, 'distance_to_recent_low': None,
'is_accelerating': False, 'is_accelerating': False,
'adx': None,
'trend_strength_adx': 'unknown',
} }
if df is None or df.empty or len(df) < 20: if df is None or df.empty or len(df) < 20:
@ -475,6 +501,19 @@ class MarketSignalAnalyzer:
'is_accelerating': self._is_accelerating(df), 'is_accelerating': self._is_accelerating(df),
}) })
# ADX 趋势强度
adx = latest.get('adx')
if pd.notna(adx):
feature['adx'] = float(adx)
if adx >= 40:
feature['trend_strength_adx'] = 'strong'
elif adx >= 25:
feature['trend_strength_adx'] = 'moderate'
elif adx >= 20:
feature['trend_strength_adx'] = 'weak'
else:
feature['trend_strength_adx'] = 'ranging'
if pd.notna(ema5) and pd.notna(ema10) and pd.notna(ema20): if pd.notna(ema5) and pd.notna(ema10) and pd.notna(ema20):
if ema5 > ema10 > ema20: if ema5 > ema10 > ema20:
feature['ema_alignment'] = 'bull' feature['ema_alignment'] = 'bull'
@ -499,15 +538,21 @@ class MarketSignalAnalyzer:
def fmt(value: Optional[float], digits: int = 2) -> str: def fmt(value: Optional[float], digits: int = 2) -> str:
return "N/A" if value is None else f"{value:+.{digits}f}%" return "N/A" if value is None else f"{value:+.{digits}f}%"
def safe(value: Optional[float], digits: int = 1) -> str:
return "N/A" if value is None else f"{value:.{digits}f}"
adx_part = f" | ADX={safe(feature.get('adx'), 1)}({feature['trend_strength_adx']})" if feature.get('adx') else ""
return ( return (
f"- {feature['timeframe']}: 结构={feature['structure']} | EMA={feature['ema_alignment']} | " f"- {feature['timeframe']}: 结构={feature['structure']} | EMA={feature['ema_alignment']} | "
f"3bar={fmt(feature['momentum_3'])} | 12bar={fmt(feature['momentum_12'])} | " f"3bar={fmt(feature['momentum_3'])} | 12bar={fmt(feature['momentum_12'])} | "
f"RSI={feature['rsi']:.1f} | ATR={feature['atr_pct']:.2f}% | " f"RSI={safe(feature['rsi'], 1)} | ATR={safe(feature['atr_pct'], 2)}% | "
f"量比={feature['volume_ratio']:.2f} | " f"量比={safe(feature['volume_ratio'], 2)} | "
f"距EMA20={fmt(feature['distance_to_ema20'])} | " f"距EMA20={fmt(feature['distance_to_ema20'])} | "
f"距20bar高点={fmt(feature['distance_to_recent_high'])} | " f"距20bar高点={fmt(feature['distance_to_recent_high'])} | "
f"距20bar低点={fmt(feature['distance_to_recent_low'])} | " f"距20bar低点={fmt(feature['distance_to_recent_low'])} | "
f"加速={'' if feature['is_accelerating'] else ''}" f"加速={'' if feature['is_accelerating'] else ''}"
f"{adx_part}"
) )
def _describe_alignment(self, features: List[Dict[str, Any]]) -> str: def _describe_alignment(self, features: List[Dict[str, Any]]) -> str:
@ -543,7 +588,7 @@ class MarketSignalAnalyzer:
if range_zone.get('resistance_level'): if range_zone.get('resistance_level'):
resistance_candidates.append(self._make_level_candidate(float(range_zone['resistance_level']), 1.2, "区间上沿")) resistance_candidates.append(self._make_level_candidate(float(range_zone['resistance_level']), 1.2, "区间上沿"))
for timeframe, count, tf_weight in [('30m', 20, 1.0), ('1h', 20, 1.15), ('4h', 12, 1.3)]: for timeframe, count, tf_weight in [('15m', 20, 0.95), ('1h', 20, 1.15), ('4h', 12, 1.3), ('1d', 10, 1.5)]:
df = data.get(timeframe) df = data.get(timeframe)
if df is None or len(df) < count: if df is None or len(df) < count:
continue continue
@ -603,8 +648,9 @@ class MarketSignalAnalyzer:
} }
fib_specs = [ fib_specs = [
('30m', 48, 'intraday', '日内'), ('15m', 48, 'intraday', '日内'),
('4h', 60, 'trend', '趋势'), ('4h', 60, 'trend', '趋势'),
('1d', 30, 'trend', '日线'),
] ]
for timeframe, lookback, key, label in fib_specs: for timeframe, lookback, key, label in fib_specs:
@ -623,6 +669,9 @@ class MarketSignalAnalyzer:
if fib_result.get('trade_zone'): if fib_result.get('trade_zone'):
summary += f" | 可交易区={fib_result['trade_zone']}" summary += f" | 可交易区={fib_result['trade_zone']}"
if key in contexts and contexts[key]:
contexts[key] += f"\n{summary}"
else:
contexts[key] = summary contexts[key] = summary
contexts['support_levels'].extend(fib_result.get('support_levels', [])) contexts['support_levels'].extend(fib_result.get('support_levels', []))
contexts['resistance_levels'].extend(fib_result.get('resistance_levels', [])) contexts['resistance_levels'].extend(fib_result.get('resistance_levels', []))
@ -1187,13 +1236,13 @@ class MarketSignalAnalyzer:
lane_scope = ( lane_scope = (
[ [
"只根据下面提供的日内结构化特征做判断,不要脑补未提供的数据。", "只根据下面提供的日内结构化特征做判断,不要脑补未提供的数据。",
"重点阅读 5m/15m/30m、当日开盘、VWAP、开盘区间、区间状态、关键位、Fib 回撤位和衍生品过热程度。", "重点阅读 5m/15m、当日开盘、VWAP、开盘区间、区间状态、关键位、Fib 回撤位和衍生品过热程度。",
"优先参考“优先支撑/优先阻力”和“可交易多头区/可交易空头区”,不要在远离关键位的位置给 entry。", "优先参考“优先支撑/优先阻力”和“可交易多头区/可交易空头区”,不要在远离关键位的位置给 entry。",
] ]
if lane == "intraday" if lane == "intraday"
else [ else [
"只根据下面提供的趋势结构化特征做判断,不要脑补未提供的数据。", "只根据下面提供的趋势结构化特征做判断,不要脑补未提供的数据。",
"重点阅读 1h/4h、一致性、趋势阶段、反转检测、关键位、Fib 回撤/扩展位、新闻催化和衍生品拥挤度。", "重点阅读 1h/4h/1d、一致性、趋势阶段、反转检测、关键位、Fib 回撤/扩展位、新闻催化和衍生品拥挤度。",
"优先参考“优先支撑/优先阻力”和“可交易多头区/可交易空头区”,趋势单必须体现位置优势,不接受远离关键位追价。", "优先参考“优先支撑/优先阻力”和“可交易多头区/可交易空头区”,趋势单必须体现位置优势,不接受远离关键位追价。",
] ]
) )
@ -1222,6 +1271,10 @@ class MarketSignalAnalyzer:
prompt_parts.append("") prompt_parts.append("")
prompt_parts.append(futures_context) prompt_parts.append(futures_context)
if market_context.get('range_warning'):
prompt_parts.append("")
prompt_parts.append(market_context['range_warning'])
prompt_parts.append("") prompt_parts.append("")
prompt_parts.append("输出要求:只返回 system prompt 定义的 JSON 对象。没有高质量 setup 就返回 signals: []。") prompt_parts.append("输出要求:只返回 system prompt 定义的 JSON 对象。没有高质量 setup 就返回 signals: []。")
@ -1687,16 +1740,16 @@ class MarketSignalAnalyzer:
} }
def _analyze_volatility(self, data: Dict[str, pd.DataFrame]) -> str: def _analyze_volatility(self, data: Dict[str, pd.DataFrame]) -> str:
"""分析波动率变化(使用 30m 作为日内主周期)""" """分析波动率变化(使用 1h 作为日内主周期)"""
df = data.get('30m') df = data.get('1h')
if df is None or len(df) < 24 or 'atr' not in df.columns: if df is None or len(df) < 12 or 'atr' not in df.columns:
return "" return ""
lines = [] lines = []
# ATR 变化趋势 # ATR 变化趋势
recent_atr = df['atr'].iloc[-6:].mean() # 最近 6 根(3小时) recent_atr = df['atr'].iloc[-6:].mean() # 最近 6 根(6小时)
older_atr = df['atr'].iloc[-12:-6].mean() # 之前 6 根 older_atr = df['atr'].iloc[-12:-6].mean() # 之前 6 根6小时
if pd.isna(recent_atr) or pd.isna(older_atr) or older_atr == 0: if pd.isna(recent_atr) or pd.isna(older_atr) or older_atr == 0:
return "" return ""
@ -1707,7 +1760,7 @@ class MarketSignalAnalyzer:
current_price = float(df['close'].iloc[-1]) current_price = float(df['close'].iloc[-1])
atr_percent = current_atr / current_price * 100 atr_percent = current_atr / current_price * 100
lines.append(f"当前 ATR (30m): ${current_atr:.2f} ({atr_percent:.2f}%)") lines.append(f"当前 ATR (1h): ${current_atr:.2f} ({atr_percent:.2f}%)")
if atr_change > 20: if atr_change > 20:
lines.append(f"**波动率扩张**: ATR 上升 {atr_change:.0f}%,日内趋势可能启动") lines.append(f"**波动率扩张**: ATR 上升 {atr_change:.0f}%,日内趋势可能启动")
@ -1728,6 +1781,109 @@ class MarketSignalAnalyzer:
return "\n".join(lines) return "\n".join(lines)
def _quantify_ranging_state(self, data: Dict[str, pd.DataFrame]) -> Dict[str, Any]:
"""
量化震荡市场指标帮助 LLM 在震荡行情中避免追涨杀跌
Returns:
包含 ATR%EMA 收敛度方向效率BB 挤压ADXregime 的字典
"""
result = {
'atr_pct': 0.0,
'atr_ratio_trend': 0.0,
'ema_convergence_pct': 0.0,
'range_efficiency': 0.0,
'bb_squeeze': False,
'adx': 0.0,
'regime': 'unknown',
'regime_score': 0,
}
try:
df_1h = data.get('1h')
if df_1h is None or len(df_1h) < 20:
return result
price = float(df_1h['close'].iloc[-1])
# ATR 占价格百分比
if 'atr' in df_1h.columns:
atr = float(df_1h['atr'].iloc[-1])
result['atr_pct'] = atr / price * 100 if price > 0 else 0
# ATR 趋势(扩张/收缩)
if len(df_1h) >= 18:
atr_recent = df_1h['atr'].iloc[-6:].mean()
atr_older = df_1h['atr'].iloc[-18:-6].mean()
if atr_older > 0:
result['atr_ratio_trend'] = float((atr_recent - atr_older) / atr_older)
# EMA 收敛度
if all(col in df_1h.columns for col in ['ema5', 'ema10', 'ema20']):
ema_short = float(df_1h['ema5'].iloc[-1])
ema_mid = float(df_1h['ema10'].iloc[-1])
ema_long = float(df_1h['ema20'].iloc[-1])
ema_spread = (max(ema_short, ema_mid, ema_long) - min(ema_short, ema_mid, ema_long))
result['ema_convergence_pct'] = ema_spread / price * 100 if price > 0 else 0
# 方向效率Choppiness Index 近似)
lookback = 14
if len(df_1h) >= lookback:
high_low_range = df_1h['high'].iloc[-lookback:].max() - df_1h['low'].iloc[-lookback:].min()
directional_move = abs(df_1h['close'].iloc[-1] - df_1h['close'].iloc[-lookback])
result['range_efficiency'] = float(directional_move / high_low_range) if high_low_range > 0 else 0
# Bollinger Band 挤压
if all(col in df_1h.columns for col in ['bb_upper', 'bb_lower']) and len(df_1h) >= 20:
bb_width_current = (df_1h['bb_upper'].iloc[-1] - df_1h['bb_lower'].iloc[-1]) / price * 100
bb_width_ma = (df_1h['bb_upper'].iloc[-20:] - df_1h['bb_lower'].iloc[-20:]).mean() / \
df_1h['close'].iloc[-20:].mean() * 100
result['bb_squeeze'] = bb_width_ma > 0 and bb_width_current < bb_width_ma * 0.7
# ADX
if 'adx' in df_1h.columns:
adx_val = df_1h['adx'].iloc[-1]
if pd.notna(adx_val):
result['adx'] = float(adx_val)
# Regime 分类
score = 0
if result['ema_convergence_pct'] < 0.5:
score += 30
elif result['ema_convergence_pct'] < 1.0:
score += 20
if result['range_efficiency'] < 0.2:
score += 30
elif result['range_efficiency'] < 0.4:
score += 15
if result['atr_ratio_trend'] < -0.2:
score += 20
if result['bb_squeeze']:
score += 10
if result['adx'] < 20:
score += 20
elif result['adx'] < 25:
score += 10
result['regime_score'] = score
if score >= 75:
result['regime'] = 'ranging'
elif score >= 50:
result['regime'] = 'transitional'
elif score >= 25:
result['regime'] = 'weak_trend'
else:
result['regime'] = 'strong_trend'
except Exception as e:
logger.warning(f"震荡市场量化失败: {e}")
return result
def _detect_range_zone(self, data: Dict[str, pd.DataFrame]) -> Dict[str, Any]: def _detect_range_zone(self, data: Dict[str, pd.DataFrame]) -> Dict[str, Any]:
""" """
检测震荡区间 - 计算明确的支撑位和压力位 检测震荡区间 - 计算明确的支撑位和压力位
@ -1750,23 +1906,22 @@ class MarketSignalAnalyzer:
} }
try: try:
df_30m = data.get('30m')
df_1h = data.get('1h') df_1h = data.get('1h')
df_15m = data.get('15m') df_15m = data.get('15m')
if df_30m is None or len(df_30m) < 48: # 需要至少48根K线24小时 if df_1h is None or len(df_1h) < 24: # 需要至少24根K线24小时
return result return result
current_price = float(df_30m['close'].iloc[-1]) current_price = float(df_1h['close'].iloc[-1])
# ========== 1. 价格通道分析 ========== # ========== 1. 价格通道分析 ==========
# 使用最近24-48根K线12-24小时计算价格通道 # 使用最近12-24根K线12-24小时计算价格通道
lookback_periods = [24, 36, 48] lookback_periods = [12, 18, 24]
price_channels = [] price_channels = []
for period in lookback_periods: for period in lookback_periods:
if len(df_30m) >= period: if len(df_1h) >= period:
period_data = df_30m.iloc[-period:] period_data = df_1h.iloc[-period:]
high = period_data['high'].max() high = period_data['high'].max()
low = period_data['low'].min() low = period_data['low'].min()
price_channels.append({'high': high, 'low': low, 'width': high - low}) price_channels.append({'high': high, 'low': low, 'width': high - low})
@ -1782,38 +1937,32 @@ class MarketSignalAnalyzer:
range_width_pct = (range_width / current_price) * 100 range_width_pct = (range_width / current_price) * 100
# 震荡区间判断标准 # 震荡区间判断标准
# 1. 区间宽度 < 5%(震荡市)
# 2. 价格在区间中位数附近
# 3. EMA 纠缠
is_narrow_range = range_width_pct < 5.0 is_narrow_range = range_width_pct < 5.0
price_in_middle = (current_price - support) / range_width > 0.3 and \ price_in_middle = (current_price - support) / range_width > 0.3 and \
(current_price - support) / range_width < 0.7 (current_price - support) / range_width < 0.7
# EMA 纠缠检查 # EMA 纠缠检查
ema5 = df_30m['ma5'].iloc[-1] if 'ma5' in df_30m.columns else None ema5 = df_1h['ma5'].iloc[-1] if 'ma5' in df_1h.columns else None
ema10 = df_30m['ma10'].iloc[-1] if 'ma10' in df_30m.columns else None ema10 = df_1h['ma10'].iloc[-1] if 'ma10' in df_1h.columns else None
ema20 = df_30m['ma20'].iloc[-1] if 'ma20' in df_30m.columns else None ema20 = df_1h['ma20'].iloc[-1] if 'ma20' in df_1h.columns else None
ema_entangled = False ema_entangled = False
if all([ema5, ema10, ema20]): if all([ema5, ema10, ema20]):
ema_spread = (max(ema5, ema10, ema20) - min(ema5, ema10, ema20)) / current_price * 100 ema_spread = (max(ema5, ema10, ema20) - min(ema5, ema10, ema20)) / current_price * 100
ema_entangled = ema_spread < 1.0 # EMA 排列差距 < 1% ema_entangled = ema_spread < 1.0
# ========== 2. 成交量密集区分析 ========== # ========== 2. 成交量密集区分析 ==========
volume_profile_support = None volume_profile_support = None
volume_profile_resistance = None volume_profile_resistance = None
if len(df_30m) >= 48: if len(df_1h) >= 24:
# 找出成交量最大的价格区间 df_1h_copy = df_1h.iloc[-24:].copy()
df_30m_copy = df_30m.iloc[-48:].copy() df_1h_copy['avg_price'] = (df_1h_copy['high'] + df_1h_copy['low'] + df_1h_copy['close']) / 3
df_30m_copy['avg_price'] = (df_30m_copy['high'] + df_30m_copy['low'] + df_30m_copy['close']) / 3 df_1h_copy['volume_weight'] = df_1h_copy['volume'] * df_1h_copy['avg_price']
df_30m_copy['volume_weight'] = df_30m_copy['volume'] * df_30m_copy['avg_price']
# 按价格分层,找出高成交量区域 price_bins = pd.cut(df_1h_copy['avg_price'], bins=10)
price_bins = pd.cut(df_30m_copy['avg_price'], bins=10) volume_by_price = df_1h_copy.groupby(price_bins, observed=True)['volume'].sum()
volume_by_price = df_30m_copy.groupby(price_bins, observed=True)['volume'].sum()
if len(volume_by_price) > 0: if len(volume_by_price) > 0:
# 高成交量区作为支撑/压力
max_vol_bin = volume_by_price.idxmax() max_vol_bin = volume_by_price.idxmax()
if max_vol_bin is not None: if max_vol_bin is not None:
vp_level = (max_vol_bin.left + max_vol_bin.right) / 2 vp_level = (max_vol_bin.left + max_vol_bin.right) / 2
@ -1825,12 +1974,11 @@ class MarketSignalAnalyzer:
# ========== 3. 布林带支撑/压力 ========== # ========== 3. 布林带支撑/压力 ==========
bb_support = None bb_support = None
bb_resistance = None bb_resistance = None
if 'bb_lower' in df_30m.columns and 'bb_upper' in df_30m.columns: if 'bb_lower' in df_1h.columns and 'bb_upper' in df_1h.columns:
bb_support = float(df_30m['bb_lower'].iloc[-1]) bb_support = float(df_1h['bb_lower'].iloc[-1])
bb_resistance = float(df_30m['bb_upper'].iloc[-1]) bb_resistance = float(df_1h['bb_upper'].iloc[-1])
# ========== 4. 关键价格点综合 ========== # ========== 4. 关键价格点综合 ==========
# 综合多个指标得出最可靠的支撑/压力位
support_candidates = [] support_candidates = []
resistance_candidates = [] resistance_candidates = []
@ -1848,7 +1996,6 @@ class MarketSignalAnalyzer:
if bb_resistance: if bb_resistance:
resistance_candidates.append(bb_resistance) resistance_candidates.append(bb_resistance)
# 取中位数作为最终的支撑/压力位
final_support = np.median(support_candidates) if support_candidates else None final_support = np.median(support_candidates) if support_candidates else None
final_resistance = np.median(resistance_candidates) if resistance_candidates else None final_resistance = np.median(resistance_candidates) if resistance_candidates else None
@ -1868,24 +2015,22 @@ class MarketSignalAnalyzer:
confidence += 25 confidence += 25
reasons.append("EMA纠缠") reasons.append("EMA纠缠")
# 成交量分布检查 - 如果成交量在区间两端较小,说明是有效震荡 # 成交量分布检查
if len(df_30m) >= 24: if len(df_1h) >= 12:
recent_vol = df_30m['volume'].iloc[-12:].mean() recent_vol = df_1h['volume'].iloc[-6:].mean()
older_vol = df_30m['volume'].iloc[-24:-12].mean() older_vol = df_1h['volume'].iloc[-12:-6].mean()
if abs(recent_vol - older_vol) / older_vol < 0.3: if abs(recent_vol - older_vol) / older_vol < 0.3:
confidence += 15 confidence += 15
reasons.append("成交量平稳") reasons.append("成交量平稳")
# 价格反弹次数 - 检查在支撑/压力位附近是否有多次反弹 # 价格反弹次数
if final_support and final_resistance: if final_support and final_resistance:
bounce_count = 0 bounce_count = 0
for i in range(-24, 0): for i in range(-12, 0):
if i >= -len(df_30m): if i >= -len(df_1h):
row = df_30m.iloc[i] row = df_1h.iloc[i]
# 检查是否在支撑位附近反弹
if abs(row['low'] - final_support) / final_support < 0.005 and row['close'] > row['open']: if abs(row['low'] - final_support) / final_support < 0.005 and row['close'] > row['open']:
bounce_count += 1 bounce_count += 1
# 检查是否在压力位附近回落
if abs(row['high'] - final_resistance) / final_resistance < 0.005 and row['close'] < row['open']: if abs(row['high'] - final_resistance) / final_resistance < 0.005 and row['close'] < row['open']:
bounce_count += 1 bounce_count += 1
@ -1932,7 +2077,6 @@ class MarketSignalAnalyzer:
try: try:
df_15m = data.get('15m') df_15m = data.get('15m')
df_30m = data.get('30m')
df_1h = data.get('1h') df_1h = data.get('1h')
if df_15m is None or len(df_15m) < 30: if df_15m is None or len(df_15m) < 30:
@ -2118,7 +2262,6 @@ class MarketSignalAnalyzer:
# ========== 5. 多周期趋势不一致 ========== # ========== 5. 多周期趋势不一致 ==========
trend_15m = self._get_trend_direction(df_15m) trend_15m = self._get_trend_direction(df_15m)
trend_30m = self._get_trend_direction(df_30m)
trend_1h = self._get_trend_direction(df_1h) trend_1h = self._get_trend_direction(df_1h)
# 小周期反转但大周期未反应 # 小周期反转但大周期未反应
@ -2216,13 +2359,12 @@ class MarketSignalAnalyzer:
} }
try: try:
df_30m = data.get('30m')
df_1h = data.get('1h') df_1h = data.get('1h')
if df_30m is None or len(df_30m) < 30: if df_1h is None or len(df_1h) < 30:
return result return result
current_price = float(df_30m['close'].iloc[-1]) current_price = float(df_1h['close'].iloc[-1])
stage_signals = [] stage_signals = []
early_score = 0 early_score = 0
@ -2230,10 +2372,10 @@ class MarketSignalAnalyzer:
late_score = 0 late_score = 0
# ========== 1. EMA 排列状态 ========== # ========== 1. EMA 排列状态 ==========
ema5 = df_30m['ma5'].iloc[-1] if 'ma5' in df_30m.columns else None ema5 = df_1h['ma5'].iloc[-1] if 'ma5' in df_1h.columns else None
ema10 = df_30m['ma10'].iloc[-1] if 'ma10' in df_30m.columns else None ema10 = df_1h['ma10'].iloc[-1] if 'ma10' in df_1h.columns else None
ema20 = df_30m['ma20'].iloc[-1] if 'ma20' in df_30m.columns else None ema20 = df_1h['ma20'].iloc[-1] if 'ma20' in df_1h.columns else None
ema50 = df_30m['ma50'].iloc[-1] if 'ma50' in df_30m.columns else None ema50 = df_1h['ma50'].iloc[-1] if 'ma50' in df_1h.columns else None
if all([ema5, ema10, ema20, ema50]): if all([ema5, ema10, ema20, ema50]):
# 检查EMA排列是否形成 # 检查EMA排列是否形成
@ -2241,11 +2383,11 @@ class MarketSignalAnalyzer:
# 多头排列 # 多头排列
# 检查排列刚刚形成(早期)还是已经稳定(中期/晚期) # 检查排列刚刚形成(早期)还是已经稳定(中期/晚期)
ema5_cross_ma20 = False ema5_cross_ma20 = False
if len(df_30m) >= 10: if len(df_1h) >= 10:
# 检查最近10根内是否发生过金叉 # 检查最近10根内是否发生过金叉
for i in range(-10, 0): for i in range(-10, 0):
if df_30m['ma5'].iloc[i] > df_30m['ma20'].iloc[i]: if df_1h['ma5'].iloc[i] > df_1h['ma20'].iloc[i]:
if i > -10 and df_30m['ma5'].iloc[i-1] <= df_30m['ma20'].iloc[i-1]: if i > -10 and df_1h['ma5'].iloc[i-1] <= df_1h['ma20'].iloc[i-1]:
ema5_cross_ma20 = True ema5_cross_ma20 = True
break break
@ -2265,10 +2407,10 @@ class MarketSignalAnalyzer:
elif ema5 < ema10 < ema20 < ema50: elif ema5 < ema10 < ema20 < ema50:
# 空头排列 # 空头排列
ema5_cross_ma20 = False ema5_cross_ma20 = False
if len(df_30m) >= 10: if len(df_1h) >= 10:
for i in range(-10, 0): for i in range(-10, 0):
if df_30m['ma5'].iloc[i] < df_30m['ma20'].iloc[i]: if df_1h['ma5'].iloc[i] < df_1h['ma20'].iloc[i]:
if i > -10 and df_30m['ma5'].iloc[i-1] >= df_30m['ma20'].iloc[i-1]: if i > -10 and df_1h['ma5'].iloc[i-1] >= df_1h['ma20'].iloc[i-1]:
ema5_cross_ma20 = True ema5_cross_ma20 = True
break break
@ -2285,9 +2427,9 @@ class MarketSignalAnalyzer:
stage_signals.append("EMA排列稳定中期") stage_signals.append("EMA排列稳定中期")
# ========== 2. RSI 状态 ========== # ========== 2. RSI 状态 ==========
if 'rsi' in df_30m.columns: if 'rsi' in df_1h.columns:
rsi_current = df_30m['rsi'].iloc[-1] rsi_current = df_1h['rsi'].iloc[-1]
rsi_prev = df_30m['rsi'].iloc[-5:-1].values rsi_prev = df_1h['rsi'].iloc[-5:-1].values
# RSI极端区 - 晚期信号 # RSI极端区 - 晚期信号
if rsi_current > 70: if rsi_current > 70:
@ -2326,12 +2468,12 @@ class MarketSignalAnalyzer:
stage_signals.append("价格贴近EMA20 - 趋势稳固") stage_signals.append("价格贴近EMA20 - 趋势稳固")
# ========== 4. 量价关系 ========== # ========== 4. 量价关系 ==========
if 'volume' in df_30m.columns and len(df_30m) >= 10: if 'volume' in df_1h.columns and len(df_1h) >= 10:
recent_vol = df_30m['volume'].iloc[-5:].mean() recent_vol = df_1h['volume'].iloc[-5:].mean()
older_vol = df_30m['volume'].iloc[-10:-5].mean() older_vol = df_1h['volume'].iloc[-10:-5].mean()
vol_change = (recent_vol - older_vol) / older_vol * 100 vol_change = (recent_vol - older_vol) / older_vol * 100
price_change_5 = (df_30m['close'].iloc[-1] - df_30m['close'].iloc[-5]) / df_30m['close'].iloc[-5] * 100 price_change_5 = (df_1h['close'].iloc[-1] - df_1h['close'].iloc[-5]) / df_1h['close'].iloc[-5] * 100
# 价格上涨但成交量下降(量价背离)- 晚期信号 # 价格上涨但成交量下降(量价背离)- 晚期信号
if price_change_5 > 1 and vol_change < -20: if price_change_5 > 1 and vol_change < -20:
@ -2348,9 +2490,9 @@ class MarketSignalAnalyzer:
stage_signals.append(f"放量下跌(跌{price_change_5:.1f}%量增{vol_change:.0f}%- 可能早期") stage_signals.append(f"放量下跌(跌{price_change_5:.1f}%量增{vol_change:.0f}%- 可能早期")
# ========== 5. 波动率状态 ========== # ========== 5. 波动率状态 ==========
if 'atr' in df_30m.columns and len(df_30m) >= 20: if 'atr' in df_1h.columns and len(df_1h) >= 20:
recent_atr = df_30m['atr'].iloc[-5:].mean() recent_atr = df_1h['atr'].iloc[-5:].mean()
older_atr = df_30m['atr'].iloc[-15:-5].mean() older_atr = df_1h['atr'].iloc[-15:-5].mean()
atr_change = (recent_atr - older_atr) / older_atr * 100 if older_atr > 0 else 0 atr_change = (recent_atr - older_atr) / older_atr * 100 if older_atr > 0 else 0
if atr_change > 30: if atr_change > 30:
@ -2361,8 +2503,8 @@ class MarketSignalAnalyzer:
stage_signals.append(f"ATR收缩({atr_change:.0f}%) - 动能衰竭") stage_signals.append(f"ATR收缩({atr_change:.0f}%) - 动能衰竭")
# ========== 6. 连续同向K线数量 ========== # ========== 6. 连续同向K线数量 ==========
if len(df_30m) >= 5: if len(df_1h) >= 5:
recent_closes = df_30m['close'].iloc[-5:].values recent_closes = df_1h['close'].iloc[-5:].values
consecutive_up = sum(1 for i in range(1, len(recent_closes)) if recent_closes[i] > recent_closes[i-1]) consecutive_up = sum(1 for i in range(1, len(recent_closes)) if recent_closes[i] > recent_closes[i-1])
consecutive_down = sum(1 for i in range(1, len(recent_closes)) if recent_closes[i] < recent_closes[i-1]) consecutive_down = sum(1 for i in range(1, len(recent_closes)) if recent_closes[i] < recent_closes[i-1])

View File

@ -14,12 +14,11 @@ class BitgetService:
# K线周期映射 - 注意 Bitget 使用大写 H # K线周期映射 - 注意 Bitget 使用大写 H
INTERVALS = { INTERVALS = {
'1m': '1m',
'5m': '5m', '5m': '5m',
'15m': '15m', '15m': '15m',
'30m': '30m',
'1h': '1H', # Bitget 大写 '1h': '1H', # Bitget 大写
'4h': '4H', # Bitget 大写 '4h': '4H', # Bitget 大写
'1d': '1D', # Bitget 大写
} }
# Bitget API 基础 URL # Bitget API 基础 URL
@ -105,26 +104,24 @@ class BitgetService:
category: 产品类型默认 USDT-FUTURES category: 产品类型默认 USDT-FUTURES
Returns: Returns:
包含 1m, 5m, 15m, 30m, 1h, 4h 数据的字典 包含 5m, 15m, 1h, 4h, 1d 数据的字典
""" """
# 不同周期使用不同的数据量,平衡分析深度和性能 # 不同周期使用不同的数据量,平衡分析深度和性能
# 1m: 200根 = 3.3小时(超短线精确入场) # 5m: 200根 = 16.7小时(日内微结构)
# 5m: 200根 = 16.7小时(日内分析) # 15m: 200根 = 2.1天(日内主时间级别)
# 15m: 200根 = 2.1天(短线分析) # 1h: 300根 = 12.5天(短期趋势 + 24h 涨跌)
# 30m: 200根 = 4.2天(日内趋势) # 4h: 180根 = 30天趋势确认
# 1h: 300根 = 12.5天(日内主趋势) # 1d: 120根 = 120天宏观趋势日线 S/R
# 4h: 180根 = 30天趋势判断
limits = { limits = {
'1m': 200,
'5m': 200, '5m': 200,
'15m': 200, '15m': 200,
'30m': 200,
'1h': 300, '1h': 300,
'4h': 180 '4h': 180,
'1d': 120,
} }
data = {} data = {}
for interval in ['1m', '5m', '15m', '30m', '1h', '4h']: for interval in ['5m', '15m', '1h', '4h', '1d']:
df = self.get_klines(symbol, interval, limit=limits.get(interval, 100), df = self.get_klines(symbol, interval, limit=limits.get(interval, 100),
category=category) category=category)
if not df.empty: if not df.empty:
@ -179,14 +176,13 @@ class BitgetService:
if df.empty: if df.empty:
return df return df
# 根据周期调整 MA 参数 # 根据周期调整 MA 参数(每个时间级别有不同的覆盖范围)
ma_config = { ma_config = {
'1m': {'ma_short': 5, 'ma_mid': 10, 'ma_long': 20, 'ma_extra': 50}, '5m': {'ma_short': 8, 'ma_mid': 21, 'ma_long': 55, 'ma_extra': 89},
'5m': {'ma_short': 5, 'ma_mid': 10, 'ma_long': 20, 'ma_extra': 50}, '15m': {'ma_short': 7, 'ma_mid': 20, 'ma_long': 50, 'ma_extra': 100},
'15m': {'ma_short': 5, 'ma_mid': 10, 'ma_long': 20, 'ma_extra': 50}, '1h': {'ma_short': 7, 'ma_mid': 25, 'ma_long': 60, 'ma_extra': 99},
'30m': {'ma_short': 5, 'ma_mid': 10, 'ma_long': 20, 'ma_extra': 50}, '4h': {'ma_short': 7, 'ma_mid': 25, 'ma_long': 50, 'ma_extra': 100},
'1h': {'ma_short': 5, 'ma_mid': 10, 'ma_long': 20, 'ma_extra': 50}, '1d': {'ma_short': 5, 'ma_mid': 20, 'ma_long': 60, 'ma_extra': 120},
'4h': {'ma_short': 5, 'ma_mid': 10, 'ma_long': 20, 'ma_extra': 50},
} }
config = ma_config.get(interval, ma_config['1h']) config = ma_config.get(interval, ma_config['1h'])
@ -222,6 +218,11 @@ class BitgetService:
# ATR # ATR
df['atr'] = self._calculate_atr(df['high'], df['low'], df['close']) df['atr'] = self._calculate_atr(df['high'], df['low'], df['close'])
# ADX (趋势强度)
df['adx'], df['plus_di'], df['minus_di'] = self._calculate_adx(
df['high'], df['low'], df['close']
)
# 成交量均线 # 成交量均线
df['volume_ma5'] = self._calculate_ma(df['volume'], 5) df['volume_ma5'] = self._calculate_ma(df['volume'], 5)
df['volume_ma20'] = self._calculate_ma(df['volume'], 20) df['volume_ma20'] = self._calculate_ma(df['volume'], 20)
@ -305,6 +306,40 @@ class BitgetService:
return atr return atr
@staticmethod
def _calculate_adx(high: pd.Series, low: pd.Series, close: pd.Series, period: int = 14):
"""ADX 趋势强度指标"""
# True Range
tr1 = high - low
tr2 = abs(high - close.shift(1))
tr3 = abs(low - close.shift(1))
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
# Directional Movement
up_move = high - high.shift(1)
down_move = low.shift(1) - low
plus_dm = pd.Series(
np.where((up_move > down_move) & (up_move > 0), up_move, 0.0),
index=high.index
)
minus_dm = pd.Series(
np.where((down_move > up_move) & (down_move > 0), down_move, 0.0),
index=high.index
)
# Wilder's smoothing
atr = tr.ewm(alpha=1/period, adjust=False).mean()
plus_di = 100 * (plus_dm.ewm(alpha=1/period, adjust=False).mean() / atr)
minus_di = 100 * (minus_dm.ewm(alpha=1/period, adjust=False).mean() / atr)
di_sum = plus_di + minus_di
di_sum = di_sum.replace(0, np.nan)
dx = 100 * abs(plus_di - minus_di) / di_sum
adx = dx.ewm(alpha=1/period, adjust=False).mean()
return adx, plus_di, minus_di
def get_current_price(self, symbol: str, category: str = None) -> Optional[float]: def get_current_price(self, symbol: str, category: str = None) -> Optional[float]:
""" """
获取当前价格 获取当前价格

View File

@ -144,6 +144,63 @@ def calculate_boll(
return upper, middle, lower return upper, middle, lower
def calculate_adx(
high: pd.Series,
low: pd.Series,
close: pd.Series,
period: int = 14
) -> Tuple[pd.Series, pd.Series, pd.Series]:
"""
计算 ADX (Average Directional Index) 趋势强度指标
ADX 衡量趋势强度非方向
- ADX < 20: 震荡 / 无趋势
- ADX 20-25: 过渡期
- ADX 25-50: 趋势中
- ADX > 50: 强趋势
Args:
high: 最高价
low: 最低价
close: 收盘价
period: 回溯周期 (默认 14)
Returns:
(adx, plus_di, minus_di) 元组
"""
# True Range
tr1 = high - low
tr2 = abs(high - close.shift(1))
tr3 = abs(low - close.shift(1))
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
# Directional Movement
up_move = high - high.shift(1)
down_move = low.shift(1) - low
plus_dm = pd.Series(
np.where((up_move > down_move) & (up_move > 0), up_move, 0.0),
index=high.index
)
minus_dm = pd.Series(
np.where((down_move > up_move) & (down_move > 0), down_move, 0.0),
index=high.index
)
# Wilder's smoothing (EWM alpha=1/period)
atr = tr.ewm(alpha=1/period, adjust=False).mean()
plus_di = 100 * (plus_dm.ewm(alpha=1/period, adjust=False).mean() / atr)
minus_di = 100 * (minus_dm.ewm(alpha=1/period, adjust=False).mean() / atr)
# DX and ADX
di_sum = plus_di + minus_di
di_sum = di_sum.replace(0, np.nan)
dx = 100 * abs(plus_di - minus_di) / di_sum
adx = dx.ewm(alpha=1/period, adjust=False).mean()
return adx, plus_di, minus_di
def calculate_volume_ma(volume: pd.Series, period: int = 5) -> pd.Series: def calculate_volume_ma(volume: pd.Series, period: int = 5) -> pd.Series:
""" """
计算成交量移动平均 计算成交量移动平均