1
This commit is contained in:
parent
4206258dfe
commit
278015d0b1
@ -3304,7 +3304,7 @@ class CryptoAgent:
|
||||
|
||||
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:
|
||||
if interval not in data or data[interval].empty:
|
||||
return False
|
||||
|
||||
@ -31,8 +31,8 @@ class MarketSignalAnalyzer:
|
||||
TREND_ANALYSIS_TEMPERATURE = 0.10
|
||||
ANALYSIS_MAX_TOKENS = 1200
|
||||
LANE_MIN_CONFIDENCE = {
|
||||
'short_term': 60,
|
||||
'medium_term': 65,
|
||||
'short_term': 70,
|
||||
'medium_term': 70,
|
||||
}
|
||||
LANE_MIN_RISK_REWARD = {
|
||||
'short_term': 1.5,
|
||||
@ -51,7 +51,7 @@ class MarketSignalAnalyzer:
|
||||
|
||||
INTRADAY_ANALYSIS_PROMPT = """你是一位专业的加密货币日内交易员,只负责生成 short_term 信号。
|
||||
|
||||
你的任务是基于 5m / 15m / 30m、当日开盘、VWAP、开盘区间、关键位、Fib 回撤位和衍生品拥挤度,判断未来 30 分钟到 4 小时内是否存在可执行 setup。
|
||||
你的任务是基于 5m / 15m、当日开盘、VWAP、开盘区间、关键位、Fib 回撤位和衍生品拥挤度,判断未来 30 分钟到 4 小时内是否存在可执行 setup。
|
||||
|
||||
执行原则:
|
||||
1. 先判断日内 regime:trending / ranging / neutral。
|
||||
@ -75,8 +75,8 @@ class MarketSignalAnalyzer:
|
||||
8. grade / confidence 约束:
|
||||
- A: 80-100,结构、位置、量价、时机都对齐
|
||||
- B: 70-79,条件较完整但仍有一项次优
|
||||
- C: 60-69,只有轻仓试错级别
|
||||
- 60 以下不要输出交易信号
|
||||
- C: 70-71,只有轻仓试错级别
|
||||
- 70 以下不要输出交易信号
|
||||
9. 止损止盈距离下限:
|
||||
- short_term 止损距离至少 0.6%
|
||||
- short_term 止盈距离至少 1.0%
|
||||
@ -118,13 +118,13 @@ class MarketSignalAnalyzer:
|
||||
|
||||
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. 只做两类交易:
|
||||
- 趋势延续:4h 趋势明确,1h 回踩关键位后确认继续
|
||||
- 趋势反转:4h 结构和 1h 动能同时改善,且反转证据充分
|
||||
- 趋势延续:4h/1d 趋势明确,1h 回踩关键位后确认继续
|
||||
- 趋势反转:4h/1d 结构和 1h 动能同时改善,且反转证据充分
|
||||
3. 禁止仅凭 15m 噪音逆 4h 开仓。
|
||||
4. 趋势晚期、资金费率过热或价格过度偏离关键均线时,要显著降低开仓积极性。
|
||||
5. 没有清晰位置优势就不交易。
|
||||
@ -134,16 +134,16 @@ class MarketSignalAnalyzer:
|
||||
信号要求:
|
||||
1. 只允许输出 0 或 1 个 medium_term 信号。
|
||||
2. 盈亏比至少 1:1.8。
|
||||
3. 如果 4h 与 1h 明显冲突,优先返回空信号。
|
||||
3. 如果 4h/1d 与 1h 明显冲突,优先返回空信号。
|
||||
4. 反转信号必须比延续信号更严格。
|
||||
5. 如果趋势处于晚期且没有回踩确认,或反转证据不足,必须返回空信号。
|
||||
6. 只有在位置优势和方向一致性都充分时才允许开仓。
|
||||
7. 趋势延续单的 entry 应优先靠近优先支撑/阻力或对应共振区,不在远离关键位的位置追价。
|
||||
8. grade / confidence 约束:
|
||||
- A: 82-100,4h/1h 同向且位置优
|
||||
- A: 82-100,4h/1d/1h 同向且位置优
|
||||
- B: 72-81,趋势或反转证据较完整
|
||||
- C: 65-71,仅限早期确认不足的轻仓趋势尝试
|
||||
- 65 以下不要输出交易信号
|
||||
- C: 70-71,仅限早期确认不足的轻仓趋势尝试
|
||||
- 70 以下不要输出交易信号
|
||||
9. 止损止盈距离下限:
|
||||
- medium_term 止损距离至少 1.0%
|
||||
- medium_term 止盈距离至少 2.0%
|
||||
@ -270,12 +270,12 @@ class MarketSignalAnalyzer:
|
||||
|
||||
feature_5m = self._summarize_timeframe_features(data.get('5m'), '5m')
|
||||
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_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])
|
||||
trend_alignment = self._describe_alignment([feature_1h, feature_4h])
|
||||
intraday_alignment = self._describe_alignment([feature_5m, feature_15m])
|
||||
trend_alignment = self._describe_alignment([feature_1h, feature_4h, feature_1d])
|
||||
range_zone = self._detect_range_zone(data)
|
||||
reversal_detection = self._detect_trend_reversal(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_15m),
|
||||
self._format_feature_line(feature_30m),
|
||||
f"- 日内级别一致性: {intraday_alignment}",
|
||||
]
|
||||
|
||||
@ -312,6 +311,7 @@ class MarketSignalAnalyzer:
|
||||
"## 趋势特征",
|
||||
self._format_feature_line(feature_1h),
|
||||
self._format_feature_line(feature_4h),
|
||||
self._format_feature_line(feature_1d),
|
||||
f"- 趋势级别一致性: {trend_alignment}",
|
||||
]
|
||||
|
||||
@ -356,11 +356,35 @@ class MarketSignalAnalyzer:
|
||||
else:
|
||||
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 {
|
||||
'snapshot': "\n".join(snapshot_parts),
|
||||
'intraday': "\n".join(intraday_parts),
|
||||
'trend': "\n".join(trend_parts),
|
||||
'levels': "\n".join(levels_parts),
|
||||
'range_warning': range_warning,
|
||||
}
|
||||
|
||||
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_low': None,
|
||||
'is_accelerating': False,
|
||||
'adx': None,
|
||||
'trend_strength_adx': 'unknown',
|
||||
}
|
||||
|
||||
if df is None or df.empty or len(df) < 20:
|
||||
@ -475,6 +501,19 @@ class MarketSignalAnalyzer:
|
||||
'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 ema5 > ema10 > ema20:
|
||||
feature['ema_alignment'] = 'bull'
|
||||
@ -499,15 +538,21 @@ class MarketSignalAnalyzer:
|
||||
def fmt(value: Optional[float], digits: int = 2) -> str:
|
||||
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 (
|
||||
f"- {feature['timeframe']}: 结构={feature['structure']} | EMA={feature['ema_alignment']} | "
|
||||
f"3bar={fmt(feature['momentum_3'])} | 12bar={fmt(feature['momentum_12'])} | "
|
||||
f"RSI={feature['rsi']:.1f} | ATR={feature['atr_pct']:.2f}% | "
|
||||
f"量比={feature['volume_ratio']:.2f} | "
|
||||
f"RSI={safe(feature['rsi'], 1)} | ATR={safe(feature['atr_pct'], 2)}% | "
|
||||
f"量比={safe(feature['volume_ratio'], 2)} | "
|
||||
f"距EMA20={fmt(feature['distance_to_ema20'])} | "
|
||||
f"距20bar高点={fmt(feature['distance_to_recent_high'])} | "
|
||||
f"距20bar低点={fmt(feature['distance_to_recent_low'])} | "
|
||||
f"加速={'是' if feature['is_accelerating'] else '否'}"
|
||||
f"{adx_part}"
|
||||
)
|
||||
|
||||
def _describe_alignment(self, features: List[Dict[str, Any]]) -> str:
|
||||
@ -543,7 +588,7 @@ class MarketSignalAnalyzer:
|
||||
if range_zone.get('resistance_level'):
|
||||
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)
|
||||
if df is None or len(df) < count:
|
||||
continue
|
||||
@ -603,8 +648,9 @@ class MarketSignalAnalyzer:
|
||||
}
|
||||
|
||||
fib_specs = [
|
||||
('30m', 48, 'intraday', '日内'),
|
||||
('15m', 48, 'intraday', '日内'),
|
||||
('4h', 60, 'trend', '趋势'),
|
||||
('1d', 30, 'trend', '日线'),
|
||||
]
|
||||
|
||||
for timeframe, lookback, key, label in fib_specs:
|
||||
@ -623,7 +669,10 @@ class MarketSignalAnalyzer:
|
||||
if fib_result.get('trade_zone'):
|
||||
summary += f" | 可交易区={fib_result['trade_zone']}"
|
||||
|
||||
contexts[key] = summary
|
||||
if key in contexts and contexts[key]:
|
||||
contexts[key] += f"\n{summary}"
|
||||
else:
|
||||
contexts[key] = summary
|
||||
contexts['support_levels'].extend(fib_result.get('support_levels', []))
|
||||
contexts['resistance_levels'].extend(fib_result.get('resistance_levels', []))
|
||||
contexts['support_details'].extend(fib_result.get('support_details', []))
|
||||
@ -1187,13 +1236,13 @@ class MarketSignalAnalyzer:
|
||||
lane_scope = (
|
||||
[
|
||||
"只根据下面提供的日内结构化特征做判断,不要脑补未提供的数据。",
|
||||
"重点阅读 5m/15m/30m、当日开盘、VWAP、开盘区间、区间状态、关键位、Fib 回撤位和衍生品过热程度。",
|
||||
"重点阅读 5m/15m、当日开盘、VWAP、开盘区间、区间状态、关键位、Fib 回撤位和衍生品过热程度。",
|
||||
"优先参考“优先支撑/优先阻力”和“可交易多头区/可交易空头区”,不要在远离关键位的位置给 entry。",
|
||||
]
|
||||
if lane == "intraday"
|
||||
else [
|
||||
"只根据下面提供的趋势结构化特征做判断,不要脑补未提供的数据。",
|
||||
"重点阅读 1h/4h、一致性、趋势阶段、反转检测、关键位、Fib 回撤/扩展位、新闻催化和衍生品拥挤度。",
|
||||
"重点阅读 1h/4h/1d、一致性、趋势阶段、反转检测、关键位、Fib 回撤/扩展位、新闻催化和衍生品拥挤度。",
|
||||
"优先参考“优先支撑/优先阻力”和“可交易多头区/可交易空头区”,趋势单必须体现位置优势,不接受远离关键位追价。",
|
||||
]
|
||||
)
|
||||
@ -1222,6 +1271,10 @@ class MarketSignalAnalyzer:
|
||||
prompt_parts.append("")
|
||||
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("输出要求:只返回 system prompt 定义的 JSON 对象。没有高质量 setup 就返回 signals: []。")
|
||||
|
||||
@ -1687,16 +1740,16 @@ class MarketSignalAnalyzer:
|
||||
}
|
||||
|
||||
def _analyze_volatility(self, data: Dict[str, pd.DataFrame]) -> str:
|
||||
"""分析波动率变化(使用 30m 作为日内主周期)"""
|
||||
df = data.get('30m')
|
||||
if df is None or len(df) < 24 or 'atr' not in df.columns:
|
||||
"""分析波动率变化(使用 1h 作为日内主周期)"""
|
||||
df = data.get('1h')
|
||||
if df is None or len(df) < 12 or 'atr' not in df.columns:
|
||||
return ""
|
||||
|
||||
lines = []
|
||||
|
||||
# ATR 变化趋势
|
||||
recent_atr = df['atr'].iloc[-6:].mean() # 最近 6 根(3小时)
|
||||
older_atr = df['atr'].iloc[-12:-6].mean() # 之前 6 根
|
||||
recent_atr = df['atr'].iloc[-6:].mean() # 最近 6 根(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:
|
||||
return ""
|
||||
@ -1707,7 +1760,7 @@ class MarketSignalAnalyzer:
|
||||
current_price = float(df['close'].iloc[-1])
|
||||
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:
|
||||
lines.append(f"**波动率扩张**: ATR 上升 {atr_change:.0f}%,日内趋势可能启动")
|
||||
@ -1728,6 +1781,109 @@ class MarketSignalAnalyzer:
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _quantify_ranging_state(self, data: Dict[str, pd.DataFrame]) -> Dict[str, Any]:
|
||||
"""
|
||||
量化震荡市场指标,帮助 LLM 在震荡行情中避免追涨杀跌
|
||||
|
||||
Returns:
|
||||
包含 ATR%、EMA 收敛度、方向效率、BB 挤压、ADX、regime 的字典
|
||||
"""
|
||||
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]:
|
||||
"""
|
||||
检测震荡区间 - 计算明确的支撑位和压力位
|
||||
@ -1750,23 +1906,22 @@ class MarketSignalAnalyzer:
|
||||
}
|
||||
|
||||
try:
|
||||
df_30m = data.get('30m')
|
||||
df_1h = data.get('1h')
|
||||
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
|
||||
|
||||
current_price = float(df_30m['close'].iloc[-1])
|
||||
current_price = float(df_1h['close'].iloc[-1])
|
||||
|
||||
# ========== 1. 价格通道分析 ==========
|
||||
# 使用最近24-48根K线(12-24小时)计算价格通道
|
||||
lookback_periods = [24, 36, 48]
|
||||
# 使用最近12-24根K线(12-24小时)计算价格通道
|
||||
lookback_periods = [12, 18, 24]
|
||||
price_channels = []
|
||||
|
||||
for period in lookback_periods:
|
||||
if len(df_30m) >= period:
|
||||
period_data = df_30m.iloc[-period:]
|
||||
if len(df_1h) >= period:
|
||||
period_data = df_1h.iloc[-period:]
|
||||
high = period_data['high'].max()
|
||||
low = period_data['low'].min()
|
||||
price_channels.append({'high': high, 'low': low, 'width': high - low})
|
||||
@ -1782,38 +1937,32 @@ class MarketSignalAnalyzer:
|
||||
range_width_pct = (range_width / current_price) * 100
|
||||
|
||||
# 震荡区间判断标准
|
||||
# 1. 区间宽度 < 5%(震荡市)
|
||||
# 2. 价格在区间中位数附近
|
||||
# 3. EMA 纠缠
|
||||
is_narrow_range = range_width_pct < 5.0
|
||||
price_in_middle = (current_price - support) / range_width > 0.3 and \
|
||||
(current_price - support) / range_width < 0.7
|
||||
|
||||
# EMA 纠缠检查
|
||||
ema5 = df_30m['ma5'].iloc[-1] if 'ma5' in df_30m.columns else None
|
||||
ema10 = df_30m['ma10'].iloc[-1] if 'ma10' in df_30m.columns else None
|
||||
ema20 = df_30m['ma20'].iloc[-1] if 'ma20' in df_30m.columns else None
|
||||
ema5 = df_1h['ma5'].iloc[-1] if 'ma5' in df_1h.columns else None
|
||||
ema10 = df_1h['ma10'].iloc[-1] if 'ma10' in df_1h.columns else None
|
||||
ema20 = df_1h['ma20'].iloc[-1] if 'ma20' in df_1h.columns else None
|
||||
ema_entangled = False
|
||||
if all([ema5, ema10, ema20]):
|
||||
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. 成交量密集区分析 ==========
|
||||
volume_profile_support = None
|
||||
volume_profile_resistance = None
|
||||
|
||||
if len(df_30m) >= 48:
|
||||
# 找出成交量最大的价格区间
|
||||
df_30m_copy = df_30m.iloc[-48:].copy()
|
||||
df_30m_copy['avg_price'] = (df_30m_copy['high'] + df_30m_copy['low'] + df_30m_copy['close']) / 3
|
||||
df_30m_copy['volume_weight'] = df_30m_copy['volume'] * df_30m_copy['avg_price']
|
||||
if len(df_1h) >= 24:
|
||||
df_1h_copy = df_1h.iloc[-24:].copy()
|
||||
df_1h_copy['avg_price'] = (df_1h_copy['high'] + df_1h_copy['low'] + df_1h_copy['close']) / 3
|
||||
df_1h_copy['volume_weight'] = df_1h_copy['volume'] * df_1h_copy['avg_price']
|
||||
|
||||
# 按价格分层,找出高成交量区域
|
||||
price_bins = pd.cut(df_30m_copy['avg_price'], bins=10)
|
||||
volume_by_price = df_30m_copy.groupby(price_bins, observed=True)['volume'].sum()
|
||||
price_bins = pd.cut(df_1h_copy['avg_price'], bins=10)
|
||||
volume_by_price = df_1h_copy.groupby(price_bins, observed=True)['volume'].sum()
|
||||
|
||||
if len(volume_by_price) > 0:
|
||||
# 高成交量区作为支撑/压力
|
||||
max_vol_bin = volume_by_price.idxmax()
|
||||
if max_vol_bin is not None:
|
||||
vp_level = (max_vol_bin.left + max_vol_bin.right) / 2
|
||||
@ -1825,12 +1974,11 @@ class MarketSignalAnalyzer:
|
||||
# ========== 3. 布林带支撑/压力 ==========
|
||||
bb_support = None
|
||||
bb_resistance = None
|
||||
if 'bb_lower' in df_30m.columns and 'bb_upper' in df_30m.columns:
|
||||
bb_support = float(df_30m['bb_lower'].iloc[-1])
|
||||
bb_resistance = float(df_30m['bb_upper'].iloc[-1])
|
||||
if 'bb_lower' in df_1h.columns and 'bb_upper' in df_1h.columns:
|
||||
bb_support = float(df_1h['bb_lower'].iloc[-1])
|
||||
bb_resistance = float(df_1h['bb_upper'].iloc[-1])
|
||||
|
||||
# ========== 4. 关键价格点综合 ==========
|
||||
# 综合多个指标得出最可靠的支撑/压力位
|
||||
support_candidates = []
|
||||
resistance_candidates = []
|
||||
|
||||
@ -1848,7 +1996,6 @@ class MarketSignalAnalyzer:
|
||||
if bb_resistance:
|
||||
resistance_candidates.append(bb_resistance)
|
||||
|
||||
# 取中位数作为最终的支撑/压力位
|
||||
final_support = np.median(support_candidates) if support_candidates else None
|
||||
final_resistance = np.median(resistance_candidates) if resistance_candidates else None
|
||||
|
||||
@ -1868,24 +2015,22 @@ class MarketSignalAnalyzer:
|
||||
confidence += 25
|
||||
reasons.append("EMA纠缠")
|
||||
|
||||
# 成交量分布检查 - 如果成交量在区间两端较小,说明是有效震荡
|
||||
if len(df_30m) >= 24:
|
||||
recent_vol = df_30m['volume'].iloc[-12:].mean()
|
||||
older_vol = df_30m['volume'].iloc[-24:-12].mean()
|
||||
# 成交量分布检查
|
||||
if len(df_1h) >= 12:
|
||||
recent_vol = df_1h['volume'].iloc[-6:].mean()
|
||||
older_vol = df_1h['volume'].iloc[-12:-6].mean()
|
||||
if abs(recent_vol - older_vol) / older_vol < 0.3:
|
||||
confidence += 15
|
||||
reasons.append("成交量平稳")
|
||||
|
||||
# 价格反弹次数 - 检查在支撑/压力位附近是否有多次反弹
|
||||
# 价格反弹次数
|
||||
if final_support and final_resistance:
|
||||
bounce_count = 0
|
||||
for i in range(-24, 0):
|
||||
if i >= -len(df_30m):
|
||||
row = df_30m.iloc[i]
|
||||
# 检查是否在支撑位附近反弹
|
||||
for i in range(-12, 0):
|
||||
if i >= -len(df_1h):
|
||||
row = df_1h.iloc[i]
|
||||
if abs(row['low'] - final_support) / final_support < 0.005 and row['close'] > row['open']:
|
||||
bounce_count += 1
|
||||
# 检查是否在压力位附近回落
|
||||
if abs(row['high'] - final_resistance) / final_resistance < 0.005 and row['close'] < row['open']:
|
||||
bounce_count += 1
|
||||
|
||||
@ -1932,7 +2077,6 @@ class MarketSignalAnalyzer:
|
||||
|
||||
try:
|
||||
df_15m = data.get('15m')
|
||||
df_30m = data.get('30m')
|
||||
df_1h = data.get('1h')
|
||||
|
||||
if df_15m is None or len(df_15m) < 30:
|
||||
@ -2118,7 +2262,6 @@ class MarketSignalAnalyzer:
|
||||
|
||||
# ========== 5. 多周期趋势不一致 ==========
|
||||
trend_15m = self._get_trend_direction(df_15m)
|
||||
trend_30m = self._get_trend_direction(df_30m)
|
||||
trend_1h = self._get_trend_direction(df_1h)
|
||||
|
||||
# 小周期反转但大周期未反应
|
||||
@ -2216,13 +2359,12 @@ class MarketSignalAnalyzer:
|
||||
}
|
||||
|
||||
try:
|
||||
df_30m = data.get('30m')
|
||||
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
|
||||
|
||||
current_price = float(df_30m['close'].iloc[-1])
|
||||
current_price = float(df_1h['close'].iloc[-1])
|
||||
|
||||
stage_signals = []
|
||||
early_score = 0
|
||||
@ -2230,10 +2372,10 @@ class MarketSignalAnalyzer:
|
||||
late_score = 0
|
||||
|
||||
# ========== 1. EMA 排列状态 ==========
|
||||
ema5 = df_30m['ma5'].iloc[-1] if 'ma5' in df_30m.columns else None
|
||||
ema10 = df_30m['ma10'].iloc[-1] if 'ma10' in df_30m.columns else None
|
||||
ema20 = df_30m['ma20'].iloc[-1] if 'ma20' in df_30m.columns else None
|
||||
ema50 = df_30m['ma50'].iloc[-1] if 'ma50' in df_30m.columns else None
|
||||
ema5 = df_1h['ma5'].iloc[-1] if 'ma5' in df_1h.columns else None
|
||||
ema10 = df_1h['ma10'].iloc[-1] if 'ma10' in df_1h.columns else None
|
||||
ema20 = df_1h['ma20'].iloc[-1] if 'ma20' in df_1h.columns else None
|
||||
ema50 = df_1h['ma50'].iloc[-1] if 'ma50' in df_1h.columns else None
|
||||
|
||||
if all([ema5, ema10, ema20, ema50]):
|
||||
# 检查EMA排列是否形成
|
||||
@ -2241,11 +2383,11 @@ class MarketSignalAnalyzer:
|
||||
# 多头排列
|
||||
# 检查排列刚刚形成(早期)还是已经稳定(中期/晚期)
|
||||
ema5_cross_ma20 = False
|
||||
if len(df_30m) >= 10:
|
||||
if len(df_1h) >= 10:
|
||||
# 检查最近10根内是否发生过金叉
|
||||
for i in range(-10, 0):
|
||||
if df_30m['ma5'].iloc[i] > df_30m['ma20'].iloc[i]:
|
||||
if i > -10 and df_30m['ma5'].iloc[i-1] <= df_30m['ma20'].iloc[i-1]:
|
||||
if df_1h['ma5'].iloc[i] > df_1h['ma20'].iloc[i]:
|
||||
if i > -10 and df_1h['ma5'].iloc[i-1] <= df_1h['ma20'].iloc[i-1]:
|
||||
ema5_cross_ma20 = True
|
||||
break
|
||||
|
||||
@ -2265,10 +2407,10 @@ class MarketSignalAnalyzer:
|
||||
elif ema5 < ema10 < ema20 < ema50:
|
||||
# 空头排列
|
||||
ema5_cross_ma20 = False
|
||||
if len(df_30m) >= 10:
|
||||
if len(df_1h) >= 10:
|
||||
for i in range(-10, 0):
|
||||
if df_30m['ma5'].iloc[i] < df_30m['ma20'].iloc[i]:
|
||||
if i > -10 and df_30m['ma5'].iloc[i-1] >= df_30m['ma20'].iloc[i-1]:
|
||||
if df_1h['ma5'].iloc[i] < df_1h['ma20'].iloc[i]:
|
||||
if i > -10 and df_1h['ma5'].iloc[i-1] >= df_1h['ma20'].iloc[i-1]:
|
||||
ema5_cross_ma20 = True
|
||||
break
|
||||
|
||||
@ -2285,9 +2427,9 @@ class MarketSignalAnalyzer:
|
||||
stage_signals.append("EMA排列稳定(中期)")
|
||||
|
||||
# ========== 2. RSI 状态 ==========
|
||||
if 'rsi' in df_30m.columns:
|
||||
rsi_current = df_30m['rsi'].iloc[-1]
|
||||
rsi_prev = df_30m['rsi'].iloc[-5:-1].values
|
||||
if 'rsi' in df_1h.columns:
|
||||
rsi_current = df_1h['rsi'].iloc[-1]
|
||||
rsi_prev = df_1h['rsi'].iloc[-5:-1].values
|
||||
|
||||
# RSI极端区 - 晚期信号
|
||||
if rsi_current > 70:
|
||||
@ -2326,12 +2468,12 @@ class MarketSignalAnalyzer:
|
||||
stage_signals.append("价格贴近EMA20 - 趋势稳固")
|
||||
|
||||
# ========== 4. 量价关系 ==========
|
||||
if 'volume' in df_30m.columns and len(df_30m) >= 10:
|
||||
recent_vol = df_30m['volume'].iloc[-5:].mean()
|
||||
older_vol = df_30m['volume'].iloc[-10:-5].mean()
|
||||
if 'volume' in df_1h.columns and len(df_1h) >= 10:
|
||||
recent_vol = df_1h['volume'].iloc[-5:].mean()
|
||||
older_vol = df_1h['volume'].iloc[-10:-5].mean()
|
||||
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:
|
||||
@ -2348,9 +2490,9 @@ class MarketSignalAnalyzer:
|
||||
stage_signals.append(f"放量下跌(跌{price_change_5:.1f}%量增{vol_change:.0f}%)- 可能早期")
|
||||
|
||||
# ========== 5. 波动率状态 ==========
|
||||
if 'atr' in df_30m.columns and len(df_30m) >= 20:
|
||||
recent_atr = df_30m['atr'].iloc[-5:].mean()
|
||||
older_atr = df_30m['atr'].iloc[-15:-5].mean()
|
||||
if 'atr' in df_1h.columns and len(df_1h) >= 20:
|
||||
recent_atr = df_1h['atr'].iloc[-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
|
||||
|
||||
if atr_change > 30:
|
||||
@ -2361,8 +2503,8 @@ class MarketSignalAnalyzer:
|
||||
stage_signals.append(f"ATR收缩({atr_change:.0f}%) - 动能衰竭")
|
||||
|
||||
# ========== 6. 连续同向K线数量 ==========
|
||||
if len(df_30m) >= 5:
|
||||
recent_closes = df_30m['close'].iloc[-5:].values
|
||||
if len(df_1h) >= 5:
|
||||
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_down = sum(1 for i in range(1, len(recent_closes)) if recent_closes[i] < recent_closes[i-1])
|
||||
|
||||
|
||||
@ -14,12 +14,11 @@ class BitgetService:
|
||||
|
||||
# K线周期映射 - 注意 Bitget 使用大写 H
|
||||
INTERVALS = {
|
||||
'1m': '1m',
|
||||
'5m': '5m',
|
||||
'15m': '15m',
|
||||
'30m': '30m',
|
||||
'1h': '1H', # Bitget 大写
|
||||
'4h': '4H', # Bitget 大写
|
||||
'1d': '1D', # Bitget 大写
|
||||
}
|
||||
|
||||
# Bitget API 基础 URL
|
||||
@ -105,26 +104,24 @@ class BitgetService:
|
||||
category: 产品类型,默认 USDT-FUTURES
|
||||
|
||||
Returns:
|
||||
包含 1m, 5m, 15m, 30m, 1h, 4h 数据的字典
|
||||
包含 5m, 15m, 1h, 4h, 1d 数据的字典
|
||||
"""
|
||||
# 不同周期使用不同的数据量,平衡分析深度和性能
|
||||
# 1m: 200根 = 3.3小时(超短线精确入场)
|
||||
# 5m: 200根 = 16.7小时(日内分析)
|
||||
# 15m: 200根 = 2.1天(短线分析)
|
||||
# 30m: 200根 = 4.2天(日内趋势)
|
||||
# 1h: 300根 = 12.5天(日内主趋势)
|
||||
# 4h: 180根 = 30天(趋势判断)
|
||||
# 5m: 200根 = 16.7小时(日内微结构)
|
||||
# 15m: 200根 = 2.1天(日内主时间级别)
|
||||
# 1h: 300根 = 12.5天(短期趋势 + 24h 涨跌)
|
||||
# 4h: 180根 = 30天(趋势确认)
|
||||
# 1d: 120根 = 120天(宏观趋势,日线 S/R)
|
||||
limits = {
|
||||
'1m': 200,
|
||||
'5m': 200,
|
||||
'15m': 200,
|
||||
'30m': 200,
|
||||
'1h': 300,
|
||||
'4h': 180
|
||||
'4h': 180,
|
||||
'1d': 120,
|
||||
}
|
||||
|
||||
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),
|
||||
category=category)
|
||||
if not df.empty:
|
||||
@ -179,14 +176,13 @@ class BitgetService:
|
||||
if df.empty:
|
||||
return df
|
||||
|
||||
# 根据周期调整 MA 参数
|
||||
# 根据周期调整 MA 参数(每个时间级别有不同的覆盖范围)
|
||||
ma_config = {
|
||||
'1m': {'ma_short': 5, 'ma_mid': 10, 'ma_long': 20, 'ma_extra': 50},
|
||||
'5m': {'ma_short': 5, 'ma_mid': 10, 'ma_long': 20, 'ma_extra': 50},
|
||||
'15m': {'ma_short': 5, 'ma_mid': 10, 'ma_long': 20, 'ma_extra': 50},
|
||||
'30m': {'ma_short': 5, 'ma_mid': 10, 'ma_long': 20, 'ma_extra': 50},
|
||||
'1h': {'ma_short': 5, 'ma_mid': 10, 'ma_long': 20, 'ma_extra': 50},
|
||||
'4h': {'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},
|
||||
'15m': {'ma_short': 7, 'ma_mid': 20, 'ma_long': 50, 'ma_extra': 100},
|
||||
'1h': {'ma_short': 7, 'ma_mid': 25, 'ma_long': 60, 'ma_extra': 99},
|
||||
'4h': {'ma_short': 7, 'ma_mid': 25, 'ma_long': 50, 'ma_extra': 100},
|
||||
'1d': {'ma_short': 5, 'ma_mid': 20, 'ma_long': 60, 'ma_extra': 120},
|
||||
}
|
||||
|
||||
config = ma_config.get(interval, ma_config['1h'])
|
||||
@ -222,6 +218,11 @@ class BitgetService:
|
||||
# ATR
|
||||
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_ma20'] = self._calculate_ma(df['volume'], 20)
|
||||
@ -305,6 +306,40 @@ class BitgetService:
|
||||
|
||||
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]:
|
||||
"""
|
||||
获取当前价格
|
||||
|
||||
@ -144,6 +144,63 @@ def calculate_boll(
|
||||
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:
|
||||
"""
|
||||
计算成交量移动平均
|
||||
|
||||
Loading…
Reference in New Issue
Block a user