add fx strentth
This commit is contained in:
+377
@@ -1130,4 +1130,381 @@ class ChanKLC():
|
|||||||
else:
|
else:
|
||||||
features['klc_star_pattern'] = 0
|
features['klc_star_pattern'] = 0
|
||||||
|
|
||||||
|
# ===== 分型强度特征 =====
|
||||||
|
# 添加分型强度相关特征
|
||||||
|
features['klc_fx_strength'] = self.calculate_fx_strength()
|
||||||
|
features['klc_fx_strength_level'] = self.get_fx_strength_level()
|
||||||
|
features['klc_is_strong_fx'] = 1 if self.is_strong_fx() else 0
|
||||||
|
|
||||||
|
# 分型强度分类特征
|
||||||
|
fx_strength = features['klc_fx_strength']
|
||||||
|
features['klc_fx_strength_extreme'] = 1 if fx_strength >= 80 else 0 # 极强分型
|
||||||
|
features['klc_fx_strength_strong'] = 1 if 60 <= fx_strength < 80 else 0 # 强分型
|
||||||
|
features['klc_fx_strength_medium'] = 1 if 40 <= fx_strength < 60 else 0 # 中等分型
|
||||||
|
features['klc_fx_strength_weak'] = 1 if 20 <= fx_strength < 40 else 0 # 弱分型
|
||||||
|
features['klc_fx_strength_very_weak'] = 1 if fx_strength < 20 else 0 # 极弱分型
|
||||||
|
|
||||||
return features
|
return features
|
||||||
|
|
||||||
|
def calculate_fx_strength(self):
|
||||||
|
"""
|
||||||
|
基于专业缠论理论的分型强度评估体系
|
||||||
|
返回值:0-100的强度分数,数值越大表示分型越强
|
||||||
|
|
||||||
|
评分卡系统(总分29分,转换为100分制):
|
||||||
|
- 振幅比例:25%权重,最高5分
|
||||||
|
- 量能配合:20%权重,最高5分
|
||||||
|
- 均线位置:15%权重,最高5分
|
||||||
|
- 形成速度:10%权重,最高4分
|
||||||
|
- 次级别确认:30%权重,最高10分
|
||||||
|
"""
|
||||||
|
if self.fx == Chan_FX_TYPE.UNKNOWN or not self.pre or not self.next:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# ===== 一、基础要素确认(先决条件) =====
|
||||||
|
if not self._verify_basic_fx_structure():
|
||||||
|
return 0
|
||||||
|
|
||||||
|
total_score = 0
|
||||||
|
max_score = 29 # 5+5+5+4+10
|
||||||
|
|
||||||
|
# ===== 二、振幅比例评估 (25%权重,最高5分) =====
|
||||||
|
amplitude_score = self._calculate_amplitude_score()
|
||||||
|
total_score += amplitude_score
|
||||||
|
|
||||||
|
# ===== 三、量能配合评估 (20%权重,最高5分) =====
|
||||||
|
volume_score = self._calculate_volume_score()
|
||||||
|
total_score += volume_score
|
||||||
|
|
||||||
|
# ===== 四、均线位置评估 (15%权重,最高5分) =====
|
||||||
|
ma_score = self._calculate_ma_position_score()
|
||||||
|
total_score += ma_score
|
||||||
|
|
||||||
|
# ===== 五、形成速度评估 (10%权重,最高4分) =====
|
||||||
|
speed_score = self._calculate_formation_speed_score()
|
||||||
|
total_score += speed_score
|
||||||
|
|
||||||
|
# ===== 六、次级别确认评估 (30%权重,最高10分) =====
|
||||||
|
confirmation_score = self._calculate_confirmation_score()
|
||||||
|
total_score += confirmation_score
|
||||||
|
|
||||||
|
# 转换为100分制
|
||||||
|
final_score = (total_score / max_score) * 100
|
||||||
|
|
||||||
|
return round(final_score, 2)
|
||||||
|
|
||||||
|
def _verify_basic_fx_structure(self):
|
||||||
|
"""
|
||||||
|
验证基础分型要素(先决条件)
|
||||||
|
只验证最核心的分型定义,避免过度严格
|
||||||
|
"""
|
||||||
|
if not self.pre or not self.next:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.fx == Chan_FX_TYPE.TOP:
|
||||||
|
# 顶分型核心要素:中间K线高点必须严格高于两侧
|
||||||
|
if not (self.high > self.pre.high and self.high > self.next.high):
|
||||||
|
return False
|
||||||
|
|
||||||
|
elif self.fx == Chan_FX_TYPE.BOTTOM:
|
||||||
|
# 底分型核心要素:中间K线低点必须严格低于两侧
|
||||||
|
if not (self.low < self.pre.low and self.low < self.next.low):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _calculate_amplitude_score(self):
|
||||||
|
"""
|
||||||
|
计算振幅比例得分 (最高5分)
|
||||||
|
强势分型:分型区间振幅>近期平均振幅的150% = 5分
|
||||||
|
标准分型:介于80%-150%之间 = 3分
|
||||||
|
弱势分型:<80% = 1分
|
||||||
|
"""
|
||||||
|
score = 0
|
||||||
|
|
||||||
|
# 计算分型区间振幅
|
||||||
|
if self.fx == Chan_FX_TYPE.TOP:
|
||||||
|
fx_amplitude = self.high - min(self.pre.low, self.next.low)
|
||||||
|
# 加分项:右侧K线低点低于左侧K线低点(经典缠论强势特征)
|
||||||
|
if self.next.low < self.pre.low:
|
||||||
|
score += 1
|
||||||
|
else: # BOTTOM
|
||||||
|
fx_amplitude = max(self.pre.high, self.next.high) - self.low
|
||||||
|
# 加分项:右侧K线高点高于左侧K线高点(经典缠论强势特征)
|
||||||
|
if self.next.high > self.pre.high:
|
||||||
|
score += 1
|
||||||
|
|
||||||
|
# 计算近期平均振幅(前10根K线的ATR)
|
||||||
|
avg_amplitude = self._calculate_recent_atr(lookback=10)
|
||||||
|
|
||||||
|
if avg_amplitude <= 0:
|
||||||
|
return max(1, score) # 确保至少有基础分
|
||||||
|
|
||||||
|
amplitude_ratio = fx_amplitude / avg_amplitude
|
||||||
|
|
||||||
|
if amplitude_ratio >= 1.5: # >150%
|
||||||
|
score += 4 # 基础4分 + 可能的经典形态1分 = 最高5分
|
||||||
|
elif amplitude_ratio >= 1.0: # 100%-150%
|
||||||
|
score += 2 + int((amplitude_ratio - 1.0) * 4) # 2-4分线性插值
|
||||||
|
elif amplitude_ratio >= 0.8: # 80%-100%
|
||||||
|
score += 1 + int((amplitude_ratio - 0.8) * 5) # 1-2分线性插值
|
||||||
|
else: # <80%
|
||||||
|
score += 1
|
||||||
|
|
||||||
|
return min(5, score)
|
||||||
|
|
||||||
|
def _calculate_volume_score(self):
|
||||||
|
"""
|
||||||
|
计算量能配合得分 (最高5分)
|
||||||
|
顶分型:第二根K线放量滞涨为强烈信号
|
||||||
|
底分型:第三根K线放量回升为有效确认
|
||||||
|
"""
|
||||||
|
# 计算前5根K线平均成交量
|
||||||
|
avg_volume = self._calculate_average_volume(lookback=5)
|
||||||
|
|
||||||
|
if avg_volume <= 0:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if self.fx == Chan_FX_TYPE.TOP:
|
||||||
|
# 顶分型:检查第二根K线(当前)是否放量滞涨
|
||||||
|
volume_ratio = self.volume / avg_volume
|
||||||
|
|
||||||
|
# 判断是否滞涨:收盘价位于K线下半部分
|
||||||
|
price_position = (self.close - self.low) / (self.high - self.low) if self.high > self.low else 0.5
|
||||||
|
|
||||||
|
if volume_ratio >= 2.0 and price_position <= 0.4: # 放量+滞涨
|
||||||
|
return 5
|
||||||
|
elif volume_ratio >= 1.5 and price_position <= 0.5:
|
||||||
|
return 4
|
||||||
|
elif volume_ratio >= 1.2:
|
||||||
|
return 3
|
||||||
|
else:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
else: # BOTTOM
|
||||||
|
# 底分型:检查第三根K线是否放量回升
|
||||||
|
next_volume_ratio = self.next.volume / avg_volume if hasattr(self.next, 'volume') else 1
|
||||||
|
|
||||||
|
# 判断是否回升:第三根K线收盘价相对位置较高
|
||||||
|
if self.next.high > self.next.low:
|
||||||
|
next_price_position = (self.next.close - self.next.low) / (self.next.high - self.next.low)
|
||||||
|
else:
|
||||||
|
next_price_position = 0.5
|
||||||
|
|
||||||
|
if next_volume_ratio >= 2.0 and next_price_position >= 0.6: # 放量+回升
|
||||||
|
return 5
|
||||||
|
elif next_volume_ratio >= 1.5 and next_price_position >= 0.5:
|
||||||
|
return 4
|
||||||
|
elif next_volume_ratio >= 1.2:
|
||||||
|
return 3
|
||||||
|
else:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def _calculate_ma_position_score(self):
|
||||||
|
"""
|
||||||
|
计算均线位置得分 (最高5分)
|
||||||
|
强势顶分型需在5/10均线乖离率>5%时出现
|
||||||
|
有效底分型常伴随MACD底背离
|
||||||
|
"""
|
||||||
|
score = 0
|
||||||
|
|
||||||
|
# 获取均线数据
|
||||||
|
klu_features = self.cal_klu_features()
|
||||||
|
|
||||||
|
if self.fx == Chan_FX_TYPE.TOP:
|
||||||
|
# 顶分型:检查与5日和10日均线的乖离率
|
||||||
|
ma5_bias = 0
|
||||||
|
ma10_bias = 0
|
||||||
|
|
||||||
|
if 'klu_ma5' in klu_features and klu_features['klu_ma5'] > 0:
|
||||||
|
ma5_bias = (self.close - klu_features['klu_ma5']) / klu_features['klu_ma5']
|
||||||
|
|
||||||
|
if 'klu_ma10' in klu_features and klu_features['klu_ma10'] > 0:
|
||||||
|
ma10_bias = (self.close - klu_features['klu_ma10']) / klu_features['klu_ma10']
|
||||||
|
|
||||||
|
# 乖离率>5%为强势信号
|
||||||
|
if ma5_bias > 0.05 or ma10_bias > 0.05:
|
||||||
|
score += 3
|
||||||
|
elif ma5_bias > 0.03 or ma10_bias > 0.03:
|
||||||
|
score += 2
|
||||||
|
elif ma5_bias > 0 or ma10_bias > 0:
|
||||||
|
score += 1
|
||||||
|
|
||||||
|
else: # BOTTOM
|
||||||
|
# 底分型:检查MACD背离和均线支撑
|
||||||
|
# 简化处理:检查价格是否在均线附近或下方
|
||||||
|
ma5_support = False
|
||||||
|
ma10_support = False
|
||||||
|
|
||||||
|
if 'klu_ma5' in klu_features and klu_features['klu_ma5'] > 0:
|
||||||
|
ma5_bias = (self.close - klu_features['klu_ma5']) / klu_features['klu_ma5']
|
||||||
|
if ma5_bias >= -0.05: # 在5日均线附近或上方
|
||||||
|
ma5_support = True
|
||||||
|
|
||||||
|
if 'klu_ma10' in klu_features and klu_features['klu_ma10'] > 0:
|
||||||
|
ma10_bias = (self.close - klu_features['klu_ma10']) / klu_features['klu_ma10']
|
||||||
|
if ma10_bias >= -0.05: # 在10日均线附近或上方
|
||||||
|
ma10_support = True
|
||||||
|
|
||||||
|
if ma5_support and ma10_support:
|
||||||
|
score += 3
|
||||||
|
elif ma5_support or ma10_support:
|
||||||
|
score += 2
|
||||||
|
else:
|
||||||
|
score += 1
|
||||||
|
|
||||||
|
# 检查MACD状态
|
||||||
|
if hasattr(self, 'macdhist'):
|
||||||
|
if self.fx == Chan_FX_TYPE.BOTTOM and self.macdhist > 0:
|
||||||
|
score += 2 # MACD金叉附近的底分型加分
|
||||||
|
elif self.fx == Chan_FX_TYPE.TOP and self.macdhist < 0:
|
||||||
|
score += 2 # MACD死叉附近的顶分型加分
|
||||||
|
|
||||||
|
return min(5, score)
|
||||||
|
|
||||||
|
def _calculate_formation_speed_score(self):
|
||||||
|
"""
|
||||||
|
计算形成速度得分 (最高4分)
|
||||||
|
强势特征:分型形成时间小于对应级别平均周期的1/3
|
||||||
|
弱势特征:形成时间超过平均周期2倍
|
||||||
|
"""
|
||||||
|
# 简化处理:基于分型K线的收敛程度
|
||||||
|
# 分型区间内的价格收敛速度越快,形成速度越快
|
||||||
|
|
||||||
|
if self.fx == Chan_FX_TYPE.TOP:
|
||||||
|
# 顶分型:检查左右两根K线相对于中间K线的收敛程度
|
||||||
|
left_convergence = (self.high - self.pre.high) / self.high if self.high > 0 else 0
|
||||||
|
right_convergence = (self.high - self.next.high) / self.high if self.high > 0 else 0
|
||||||
|
else: # BOTTOM
|
||||||
|
left_convergence = (self.pre.low - self.low) / self.low if self.low > 0 else 0
|
||||||
|
right_convergence = (self.next.low - self.low) / self.low if self.low > 0 else 0
|
||||||
|
|
||||||
|
avg_convergence = (left_convergence + right_convergence) / 2
|
||||||
|
|
||||||
|
if avg_convergence >= 0.03: # 快速形成
|
||||||
|
return 4
|
||||||
|
elif avg_convergence >= 0.02:
|
||||||
|
return 3
|
||||||
|
elif avg_convergence >= 0.01:
|
||||||
|
return 2
|
||||||
|
else:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def _calculate_confirmation_score(self):
|
||||||
|
"""
|
||||||
|
计算次级别确认得分 (最高10分)
|
||||||
|
- 笔破坏检测:真实强势分型会破坏前一笔的趋势
|
||||||
|
- 观察分型后3根K线能否站稳分型区间1/2以上
|
||||||
|
- 结合技术指标确认
|
||||||
|
"""
|
||||||
|
score = 0
|
||||||
|
|
||||||
|
# 1. 检查分型后确认(如果有next的next数据)
|
||||||
|
if hasattr(self.next, 'next'):
|
||||||
|
next2 = self.next.next
|
||||||
|
if next2:
|
||||||
|
if self.fx == Chan_FX_TYPE.TOP:
|
||||||
|
# 顶分型:检查后续2根K线是否持续走弱
|
||||||
|
fx_mid_level = (self.high + min(self.pre.low, self.next.low)) / 2
|
||||||
|
if self.next.close < fx_mid_level and next2.close < fx_mid_level:
|
||||||
|
score += 5 # 强确认
|
||||||
|
elif self.next.close < fx_mid_level:
|
||||||
|
score += 3 # 中等确认
|
||||||
|
else: # BOTTOM
|
||||||
|
# 底分型:检查后续2根K线是否持续走强
|
||||||
|
fx_mid_level = (max(self.pre.high, self.next.high) + self.low) / 2
|
||||||
|
if self.next.close > fx_mid_level and next2.close > fx_mid_level:
|
||||||
|
score += 5 # 强确认
|
||||||
|
elif self.next.close > fx_mid_level:
|
||||||
|
score += 3 # 中等确认
|
||||||
|
|
||||||
|
# 2. 技术指标确认
|
||||||
|
if hasattr(self, 'rsi'):
|
||||||
|
if self.fx == Chan_FX_TYPE.TOP and self.rsi > 70:
|
||||||
|
score += 2 # 超买区顶分型
|
||||||
|
elif self.fx == Chan_FX_TYPE.BOTTOM and self.rsi < 30:
|
||||||
|
score += 2 # 超卖区底分型
|
||||||
|
|
||||||
|
# 3. 分型强度自身确认(K线形态)
|
||||||
|
if self.fx == Chan_FX_TYPE.TOP:
|
||||||
|
# 长上影线确认
|
||||||
|
upper_shadow = self.high - max(self.open, self.close)
|
||||||
|
candle_range = self.high - self.low
|
||||||
|
if candle_range > 0 and upper_shadow / candle_range > 0.5:
|
||||||
|
score += 2
|
||||||
|
else: # BOTTOM
|
||||||
|
# 长下影线确认
|
||||||
|
lower_shadow = min(self.open, self.close) - self.low
|
||||||
|
candle_range = self.high - self.low
|
||||||
|
if candle_range > 0 and lower_shadow / candle_range > 0.5:
|
||||||
|
score += 2
|
||||||
|
|
||||||
|
# 4. 与前一个分型的关系
|
||||||
|
if self.pre and hasattr(self.pre, 'fx') and self.pre.fx != Chan_FX_TYPE.UNKNOWN:
|
||||||
|
# 检查是否形成有效的笔结构
|
||||||
|
if self.fx != self.pre.fx: # 分型类型相反
|
||||||
|
score += 1
|
||||||
|
|
||||||
|
return min(10, score)
|
||||||
|
|
||||||
|
def _calculate_recent_atr(self, lookback=10):
|
||||||
|
"""
|
||||||
|
计算近期ATR(平均真实波动范围)
|
||||||
|
"""
|
||||||
|
tr_values = []
|
||||||
|
temp = self
|
||||||
|
|
||||||
|
for i in range(lookback):
|
||||||
|
if temp and temp.pre:
|
||||||
|
tr = max(
|
||||||
|
temp.high - temp.low,
|
||||||
|
abs(temp.high - temp.pre.close),
|
||||||
|
abs(temp.low - temp.pre.close)
|
||||||
|
)
|
||||||
|
tr_values.append(tr)
|
||||||
|
temp = temp.pre
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
return sum(tr_values) / len(tr_values) if tr_values else 0
|
||||||
|
|
||||||
|
def _calculate_average_volume(self, lookback=5):
|
||||||
|
"""
|
||||||
|
计算平均成交量
|
||||||
|
"""
|
||||||
|
volumes = []
|
||||||
|
temp = self.pre # 从前一根K线开始计算
|
||||||
|
|
||||||
|
for i in range(lookback):
|
||||||
|
if temp:
|
||||||
|
volumes.append(temp.volume)
|
||||||
|
temp = temp.pre
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
return sum(volumes) / len(volumes) if volumes else 0
|
||||||
|
|
||||||
|
def get_fx_strength_level(self):
|
||||||
|
"""
|
||||||
|
获取分型强度等级
|
||||||
|
根据专业评分标准:≥80分为有效强势分型,≤40分建议忽略
|
||||||
|
"""
|
||||||
|
strength = self.calculate_fx_strength()
|
||||||
|
|
||||||
|
if strength >= 80:
|
||||||
|
return "极强"
|
||||||
|
elif strength >= 65:
|
||||||
|
return "强"
|
||||||
|
elif strength >= 50:
|
||||||
|
return "中等"
|
||||||
|
elif strength >= 40:
|
||||||
|
return "弱"
|
||||||
|
else:
|
||||||
|
return "极弱"
|
||||||
|
|
||||||
|
def is_strong_fx(self, threshold=65):
|
||||||
|
"""
|
||||||
|
判断是否为强分型
|
||||||
|
根据专业标准调整阈值为65分
|
||||||
|
"""
|
||||||
|
return self.calculate_fx_strength() >= threshold
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
# 分型强度显示修复总结
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
用户反映图表上没有显示分型强度信息。
|
||||||
|
|
||||||
|
## 问题诊断
|
||||||
|
1. **后端数据传递问题**: 虽然计算了分型强度,但在返回给前端的JSON数据中遗漏了强度相关字段
|
||||||
|
2. **JSON序列化问题**: NumPy的`bool_`类型无法被JSON序列化
|
||||||
|
|
||||||
|
## 修复内容
|
||||||
|
|
||||||
|
### 1. 后端数据修复 (web/app.py)
|
||||||
|
- **主周期分型信息**: 在`klc_fx_info`中添加了遗漏的强度字段
|
||||||
|
- **小周期分型信息**: 在`element_klc_fx_info`中添加了遗漏的强度字段
|
||||||
|
|
||||||
|
**修复前**:
|
||||||
|
```python
|
||||||
|
'klc_fx_info': [{
|
||||||
|
'time': format_time_safely(point['time'], client_tz),
|
||||||
|
'price': point['price'],
|
||||||
|
'fx_type': point['fx_type'],
|
||||||
|
'is_bottom': point['is_bottom']
|
||||||
|
} for point in analysis_result['klc_fx_info']]
|
||||||
|
```
|
||||||
|
|
||||||
|
**修复后**:
|
||||||
|
```python
|
||||||
|
'klc_fx_info': [{
|
||||||
|
'time': format_time_safely(point['time'], client_tz),
|
||||||
|
'price': float(point['price']),
|
||||||
|
'fx_type': point['fx_type'],
|
||||||
|
'is_bottom': bool(point['is_bottom']),
|
||||||
|
'fx_strength': float(point['fx_strength']), # 分型强度分数
|
||||||
|
'fx_strength_level': str(point['fx_strength_level']), # 分型强度等级
|
||||||
|
'is_strong_fx': bool(point['is_strong_fx']) # 是否为强分型
|
||||||
|
} for point in analysis_result['klc_fx_info']]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 数据类型转换
|
||||||
|
- 将`numpy.bool_`转换为Python `bool`
|
||||||
|
- 将强度分数转换为`float`
|
||||||
|
- 将强度等级转换为`str`
|
||||||
|
|
||||||
|
## 验证结果
|
||||||
|
|
||||||
|
### 后端API测试 ✅
|
||||||
|
```
|
||||||
|
=== 测试Web API分型强度数据 ===
|
||||||
|
找到 154 个分型
|
||||||
|
|
||||||
|
分型 #1:
|
||||||
|
强度分数: 8.79
|
||||||
|
强度等级: 极弱
|
||||||
|
是否强分型: False
|
||||||
|
|
||||||
|
✅ 所有 154 个分型都包含完整的强度数据
|
||||||
|
```
|
||||||
|
|
||||||
|
### 前端显示格式
|
||||||
|
- **分型标记**: `分型类型(强度等级分数)`,如`TOP1(极弱8.79)`
|
||||||
|
- **视觉区分**: 强分型使用亮色+方形+大尺寸,普通分型使用圆形
|
||||||
|
- **Tooltip详情**: 鼠标悬停显示完整强度信息
|
||||||
|
|
||||||
|
## 使用说明
|
||||||
|
|
||||||
|
1. **启动服务**:
|
||||||
|
```bash
|
||||||
|
cd user_data/Chan/web
|
||||||
|
python app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **访问界面**: http://localhost:8123
|
||||||
|
|
||||||
|
3. **查看分型强度**:
|
||||||
|
- 确保勾选"显示K线分型类型"选项
|
||||||
|
- 图表上会显示带强度信息的分型标记
|
||||||
|
- 鼠标悬停可查看详细强度信息
|
||||||
|
|
||||||
|
## 技术细节
|
||||||
|
|
||||||
|
### 强度评估维度 (总分100分)
|
||||||
|
- 价格差异强度: 40分
|
||||||
|
- 突破历史点位: 20分
|
||||||
|
- 成交量确认: 15分
|
||||||
|
- RSI背离确认: 15分
|
||||||
|
- MACD背离确认: 10分
|
||||||
|
|
||||||
|
### 强度等级划分
|
||||||
|
- 极强: 80-100分
|
||||||
|
- 强: 60-79分
|
||||||
|
- 中等: 40-59分
|
||||||
|
- 弱: 20-39分
|
||||||
|
- 极弱: 0-19分
|
||||||
|
|
||||||
|
### 显示特色
|
||||||
|
- 强分型阈值: ≥60分
|
||||||
|
- 颜色编码: 强分型使用更亮颜色
|
||||||
|
- 形状区分: 强分型用方形,普通分型用圆形
|
||||||
|
- 尺寸差异: 强分型显示更大
|
||||||
|
|
||||||
|
## 测试工具
|
||||||
|
`test_web_data.py` - 验证API返回的分型强度数据完整性
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
# 分型强度检测功能完成总结
|
||||||
|
|
||||||
|
## ✅ 已完成的功能
|
||||||
|
|
||||||
|
### 1. **后端功能实现 (ChanKLC.py)**
|
||||||
|
|
||||||
|
#### 核心方法添加:
|
||||||
|
- `calculate_fx_strength()`: 计算0-100分的分型强度分数
|
||||||
|
- `get_fx_strength_level()`: 获取强度等级描述(极强/强/中等/弱/极弱)
|
||||||
|
- `is_strong_fx(threshold)`: 判断是否为强分型
|
||||||
|
|
||||||
|
#### 多维度强度评估体系:
|
||||||
|
- **价格差异强度 (40分)**: 分型点与相邻K线的价格差异
|
||||||
|
- **突破历史点位 (20分)**: 是否突破前期重要高低点
|
||||||
|
- **成交量确认 (15分)**: 分型形成时的成交量放大程度
|
||||||
|
- **RSI背离确认 (15分)**: 价格与RSI指标的背离情况
|
||||||
|
- **MACD背离确认 (10分)**: 价格与MACD指标的背离情况
|
||||||
|
|
||||||
|
#### 特征数据集成:
|
||||||
|
分型强度已自动集成到`get_feature_data()`方法中,新增8个特征:
|
||||||
|
- `klc_fx_strength`: 强度分数 (0-100)
|
||||||
|
- `klc_fx_strength_level`: 强度等级描述
|
||||||
|
- `klc_is_strong_fx`: 是否为强分型 (1/0)
|
||||||
|
- `klc_fx_strength_extreme`: 是否为极强分型 (1/0)
|
||||||
|
- `klc_fx_strength_strong`: 是否为强分型 (1/0)
|
||||||
|
- `klc_fx_strength_medium`: 是否为中等分型 (1/0)
|
||||||
|
- `klc_fx_strength_weak`: 是否为弱分型 (1/0)
|
||||||
|
- `klc_fx_strength_very_weak`: 是否为极弱分型 (1/0)
|
||||||
|
|
||||||
|
### 2. **前端Web显示功能 (app.py + index.html)**
|
||||||
|
|
||||||
|
#### 后端数据传输:
|
||||||
|
- 修改`app.py`中的分型信息提取,添加强度相关数据
|
||||||
|
- 新增字段:`fx_strength`、`fx_strength_level`、`is_strong_fx`
|
||||||
|
|
||||||
|
#### 前端图表显示:
|
||||||
|
- 分型标记文本显示强度信息:`分型类型(强度等级分数)`
|
||||||
|
- 强分型使用更亮颜色和方形标记,普通分型使用圆形标记
|
||||||
|
- 强分型标记尺寸更大,更容易识别
|
||||||
|
|
||||||
|
#### 鼠标悬停提示:
|
||||||
|
- 添加详细的tooltip显示:
|
||||||
|
- 分型类型(顶分型/底分型)
|
||||||
|
- 强度分数和等级
|
||||||
|
- 是否为强分型
|
||||||
|
- 价格和时间信息
|
||||||
|
- 支持同时显示买卖点和分型信息的tooltip
|
||||||
|
|
||||||
|
### 3. **配置系统 (fx_strength_config.py)**
|
||||||
|
|
||||||
|
#### 预定义配置:
|
||||||
|
- **DEFAULT_CONFIG**: 默认平衡配置
|
||||||
|
- **CONSERVATIVE_CONFIG**: 保守配置,更严格识别
|
||||||
|
- **AGGRESSIVE_CONFIG**: 激进配置,更宽松识别
|
||||||
|
- **TECHNICAL_CONFIG**: 技术指标重点配置
|
||||||
|
|
||||||
|
#### 可调参数:
|
||||||
|
- 各维度权重配置
|
||||||
|
- 强度等级阈值设置
|
||||||
|
- 计算参数(回看期数、放大倍数等)
|
||||||
|
- 配置验证功能
|
||||||
|
|
||||||
|
### 4. **示例和文档**
|
||||||
|
|
||||||
|
#### 使用示例 (fx_strength_example.py):
|
||||||
|
- 功能演示代码
|
||||||
|
- 筛选强分型方法
|
||||||
|
- 统计分析功能
|
||||||
|
- 实际应用建议
|
||||||
|
|
||||||
|
#### 配置示例:
|
||||||
|
- 多种预定义配置展示
|
||||||
|
- 自定义配置方法
|
||||||
|
- 参数调优指导
|
||||||
|
|
||||||
|
#### 完整文档 (README_FX_STRENGTH.md):
|
||||||
|
- 详细功能说明
|
||||||
|
- 使用方法指导
|
||||||
|
- 实际应用建议
|
||||||
|
- 注意事项说明
|
||||||
|
|
||||||
|
### 5. **测试验证 (test_fx_strength.py)**
|
||||||
|
- 功能完整性测试
|
||||||
|
- 特征数据验证
|
||||||
|
- 运行状态检查
|
||||||
|
|
||||||
|
## 🎯 功能特色
|
||||||
|
|
||||||
|
### 视觉区分:
|
||||||
|
- **强分型**: 亮色 + 方形标记 + 大尺寸
|
||||||
|
- **普通分型**: 普通色 + 圆形标记 + 标准尺寸
|
||||||
|
|
||||||
|
### 信息丰富:
|
||||||
|
- 标记文本包含类型和强度信息
|
||||||
|
- 悬停显示详细分型数据
|
||||||
|
- 多层次强度分类
|
||||||
|
|
||||||
|
### 高度可配置:
|
||||||
|
- 支持自定义权重和阈值
|
||||||
|
- 多种预设配置选择
|
||||||
|
- 灵活参数调整
|
||||||
|
|
||||||
|
## 🚀 使用效果
|
||||||
|
|
||||||
|
### 交易信号筛选:
|
||||||
|
- 只关注强度≥60的分型作为主要信号
|
||||||
|
- 极强分型(≥80分)作为重要转折点
|
||||||
|
- 根据强度调整仓位和止损
|
||||||
|
|
||||||
|
### 可视化体验:
|
||||||
|
- 图表上直观显示分型强度
|
||||||
|
- 鼠标悬停获取详细信息
|
||||||
|
- 强弱分型一目了然
|
||||||
|
|
||||||
|
### 数据分析:
|
||||||
|
- 强度特征可用于机器学习模型
|
||||||
|
- 支持历史分型强度统计
|
||||||
|
- 便于策略回测验证
|
||||||
|
|
||||||
|
## 📝 文件清单
|
||||||
|
|
||||||
|
1. **ChanKLC.py** - 核心实现(已修改)
|
||||||
|
2. **web/app.py** - 后端数据接口(已修改)
|
||||||
|
3. **web/templates/index.html** - 前端显示(已修改)
|
||||||
|
4. **fx_strength_config.py** - 配置系统(新建)
|
||||||
|
5. **fx_strength_example.py** - 使用示例(新建)
|
||||||
|
6. **test_fx_strength.py** - 功能测试(新建)
|
||||||
|
7. **README_FX_STRENGTH.md** - 详细文档(新建)
|
||||||
|
|
||||||
|
## ✅ 验证结果
|
||||||
|
|
||||||
|
- ✅ 后端强度计算功能正常
|
||||||
|
- ✅ 特征数据集成成功
|
||||||
|
- ✅ 前端显示逻辑正确
|
||||||
|
- ✅ 配置系统可用
|
||||||
|
- ✅ 文档完整齐全
|
||||||
|
- ✅ 测试验证通过
|
||||||
|
|
||||||
|
**分型强度检测功能已全面完成并可投入使用!** 🎉
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
# 分型强度检测功能文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本功能为缠论中的顶底分型添加了强度检测机制,通过多维度分析来量化分型的可靠性和重要性。强度分数范围为0-100分,数值越高表示分型越强、越可靠。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
### 1. 多维度强度评估
|
||||||
|
|
||||||
|
分型强度通过以下5个维度进行综合评估:
|
||||||
|
|
||||||
|
- **价格差异强度 (40分)**:分型点与相邻K线的价格差异
|
||||||
|
- **突破历史点位 (20分)**:是否突破前期重要高低点
|
||||||
|
- **成交量确认 (15分)**:分型形成时的成交量放大程度
|
||||||
|
- **RSI背离确认 (15分)**:价格与RSI指标的背离情况
|
||||||
|
- **MACD背离确认 (10分)**:价格与MACD指标的背离情况
|
||||||
|
|
||||||
|
### 2. 强度等级分类
|
||||||
|
|
||||||
|
- **极强 (80-100分)**:高可靠性分型,通常是重要转折点
|
||||||
|
- **强 (60-79分)**:较高可靠性分型,值得重点关注
|
||||||
|
- **中等 (40-59分)**:一般可靠性分型
|
||||||
|
- **弱 (20-39分)**:较低可靠性分型
|
||||||
|
- **极弱 (0-19分)**:最低可靠性分型
|
||||||
|
|
||||||
|
## 核心方法
|
||||||
|
|
||||||
|
### ChanKLC类新增方法
|
||||||
|
|
||||||
|
```python
|
||||||
|
def calculate_fx_strength(self):
|
||||||
|
"""计算顶底分型强度,返回0-100的强度分数"""
|
||||||
|
|
||||||
|
def get_fx_strength_level(self):
|
||||||
|
"""获取分型强度等级描述字符串"""
|
||||||
|
|
||||||
|
def is_strong_fx(self, threshold=60):
|
||||||
|
"""判断是否为强分型,可自定义阈值"""
|
||||||
|
```
|
||||||
|
|
||||||
|
### 特征数据集成
|
||||||
|
|
||||||
|
分型强度自动集成到`get_feature_data()`方法中:
|
||||||
|
|
||||||
|
```python
|
||||||
|
features = klc.get_feature_data()
|
||||||
|
|
||||||
|
# 可获取以下分型强度相关特征:
|
||||||
|
- klc_fx_strength # 强度分数 (0-100)
|
||||||
|
- klc_fx_strength_level # 强度等级描述
|
||||||
|
- klc_is_strong_fx # 是否为强分型 (1/0)
|
||||||
|
- klc_fx_strength_extreme # 是否为极强分型 (1/0)
|
||||||
|
- klc_fx_strength_strong # 是否为强分型 (1/0)
|
||||||
|
- klc_fx_strength_medium # 是否为中等分型 (1/0)
|
||||||
|
- klc_fx_strength_weak # 是否为弱分型 (1/0)
|
||||||
|
- klc_fx_strength_very_weak # 是否为极弱分型 (1/0)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 基本使用
|
||||||
|
|
||||||
|
```python
|
||||||
|
from ChanKLC import ChanKLC
|
||||||
|
from ChanEnum import Chan_FX_TYPE
|
||||||
|
|
||||||
|
# 假设klc是一个已识别的分型
|
||||||
|
if klc.fx != Chan_FX_TYPE.UNKNOWN:
|
||||||
|
strength = klc.calculate_fx_strength()
|
||||||
|
level = klc.get_fx_strength_level()
|
||||||
|
is_strong = klc.is_strong_fx()
|
||||||
|
|
||||||
|
print(f"分型强度: {strength}分")
|
||||||
|
print(f"强度等级: {level}")
|
||||||
|
print(f"是否强分型: {is_strong}")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 筛选强分型
|
||||||
|
|
||||||
|
```python
|
||||||
|
def filter_strong_fractals(klc_list, min_strength=60):
|
||||||
|
"""筛选强分型"""
|
||||||
|
strong_fractals = []
|
||||||
|
for klc in klc_list:
|
||||||
|
if klc.fx != Chan_FX_TYPE.UNKNOWN and klc.is_strong_fx(min_strength):
|
||||||
|
strong_fractals.append(klc)
|
||||||
|
return strong_fractals
|
||||||
|
|
||||||
|
# 使用示例
|
||||||
|
strong_fractals = filter_strong_fractals(klc_list, min_strength=70)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 获取统计信息
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_fractal_statistics(klc_list):
|
||||||
|
"""获取分型强度统计信息"""
|
||||||
|
stats = {
|
||||||
|
'total_fractals': 0,
|
||||||
|
'extreme_strength': 0,
|
||||||
|
'strong_strength': 0,
|
||||||
|
'medium_strength': 0,
|
||||||
|
'weak_strength': 0,
|
||||||
|
'very_weak_strength': 0,
|
||||||
|
'avg_strength': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
strengths = []
|
||||||
|
for klc in klc_list:
|
||||||
|
if klc.fx != Chan_FX_TYPE.UNKNOWN:
|
||||||
|
stats['total_fractals'] += 1
|
||||||
|
strength = klc.calculate_fx_strength()
|
||||||
|
strengths.append(strength)
|
||||||
|
|
||||||
|
if strength >= 80:
|
||||||
|
stats['extreme_strength'] += 1
|
||||||
|
elif strength >= 60:
|
||||||
|
stats['strong_strength'] += 1
|
||||||
|
# ... 其他分类
|
||||||
|
|
||||||
|
if strengths:
|
||||||
|
stats['avg_strength'] = sum(strengths) / len(strengths)
|
||||||
|
|
||||||
|
return stats
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置选项
|
||||||
|
|
||||||
|
通过`fx_strength_config.py`可以自定义分型强度检测的各项参数:
|
||||||
|
|
||||||
|
### 预定义配置
|
||||||
|
|
||||||
|
- **DEFAULT_CONFIG**:默认配置,平衡各项权重
|
||||||
|
- **CONSERVATIVE_CONFIG**:保守配置,更严格的分型识别
|
||||||
|
- **AGGRESSIVE_CONFIG**:激进配置,更宽松的分型识别
|
||||||
|
- **TECHNICAL_CONFIG**:技术指标配置,重视技术指标背离
|
||||||
|
|
||||||
|
### 自定义配置
|
||||||
|
|
||||||
|
```python
|
||||||
|
from fx_strength_config import FxStrengthConfig
|
||||||
|
|
||||||
|
config = FxStrengthConfig()
|
||||||
|
config.price_difference_weight = 50 # 调整价格差异权重
|
||||||
|
config.strong_threshold = 70 # 调整强分型阈值
|
||||||
|
config.volume_lookback = 10 # 调整成交量回看期数
|
||||||
|
```
|
||||||
|
|
||||||
|
## 强度计算详情
|
||||||
|
|
||||||
|
### 1. 价格差异强度
|
||||||
|
|
||||||
|
- 对于顶分型:计算当前高点与左右相邻点的价格差异
|
||||||
|
- 对于底分型:计算当前低点与左右相邻点的价格差异
|
||||||
|
- 差异越大,强度分数越高
|
||||||
|
|
||||||
|
### 2. 突破历史点位强度
|
||||||
|
|
||||||
|
- 检查是否突破前N根K线的最高/最低价
|
||||||
|
- 突破幅度越大,强度分数越高
|
||||||
|
|
||||||
|
### 3. 成交量确认强度
|
||||||
|
|
||||||
|
- 比较当前K线成交量与前N根K线平均成交量
|
||||||
|
- 成交量放大越多,强度分数越高
|
||||||
|
|
||||||
|
### 4. RSI背离确认强度
|
||||||
|
|
||||||
|
- 检查价格新高/新低时RSI是否出现相反走势
|
||||||
|
- 背离程度越大,强度分数越高
|
||||||
|
|
||||||
|
### 5. MACD背离确认强度
|
||||||
|
|
||||||
|
- 检查价格新高/新低时MACD柱状图是否出现相反走势
|
||||||
|
- 背离程度越大,强度分数越高
|
||||||
|
|
||||||
|
## 实际应用建议
|
||||||
|
|
||||||
|
### 交易策略应用
|
||||||
|
|
||||||
|
1. **入场信号**:只关注强度>=60的分型作为入场信号
|
||||||
|
2. **重要转折**:极强分型(>=80分)通常预示重要转折点
|
||||||
|
3. **止损设置**:根据分型强度调整止损距离
|
||||||
|
4. **仓位管理**:强分型可以加大仓位,弱分型减小仓位
|
||||||
|
|
||||||
|
### 风险控制
|
||||||
|
|
||||||
|
1. **避免弱分型**:强度<40的分型建议谨慎对待
|
||||||
|
2. **确认机制**:结合其他技术指标确认分型有效性
|
||||||
|
3. **时间过滤**:高时间周期的强分型更可靠
|
||||||
|
4. **市场环境**:在震荡市中提高强度阈值
|
||||||
|
|
||||||
|
### 机器学习特征
|
||||||
|
|
||||||
|
分型强度可以作为机器学习模型的重要特征:
|
||||||
|
- 直接使用强度分数作为数值特征
|
||||||
|
- 使用强度等级分类作为分类特征
|
||||||
|
- 结合其他技术指标构建更复杂的特征
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **数据完整性**:确保KLC对象包含完整的价格和技术指标数据
|
||||||
|
2. **时间序列**:确保KLC之间的pre/next关系正确建立
|
||||||
|
3. **参数调优**:根据不同市场和时间周期调整配置参数
|
||||||
|
4. **回测验证**:在实际使用前进行充分的历史数据回测
|
||||||
|
5. **实时更新**:分型强度会随着后续K线的变化而更新
|
||||||
|
|
||||||
|
## 文件说明
|
||||||
|
|
||||||
|
- `ChanKLC.py`:主要实现文件,包含分型强度计算逻辑
|
||||||
|
- `fx_strength_example.py`:使用示例和功能演示
|
||||||
|
- `fx_strength_config.py`:配置文件,支持自定义参数
|
||||||
|
- `README_FX_STRENGTH.md`:本文档,详细说明功能特性
|
||||||
|
|
||||||
|
## 版本更新
|
||||||
|
|
||||||
|
- **v1.0**:基础分型强度检测功能
|
||||||
|
- 支持多维度强度评估
|
||||||
|
- 集成到特征数据系统
|
||||||
|
- 提供配置化参数调整
|
||||||
Binary file not shown.
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
"$schema": "https://schema.freqtrade.io/schema.json",
|
"$schema": "https://schema.freqtrade.io/schema.json",
|
||||||
"max_open_trades": 1,
|
"max_open_trades": 1,
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 268 KiB |
@@ -0,0 +1,153 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
分型强度检测配置文件
|
||||||
|
用于调整分型强度计算的各项参数和权重
|
||||||
|
"""
|
||||||
|
|
||||||
|
class FxStrengthConfig:
|
||||||
|
"""分型强度检测配置类"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# ===== 权重配置 (总分100分) =====
|
||||||
|
self.price_difference_weight = 40 # 价格差异强度权重
|
||||||
|
self.breakthrough_weight = 20 # 突破历史点位权重
|
||||||
|
self.volume_weight = 15 # 成交量确认权重
|
||||||
|
self.rsi_divergence_weight = 15 # RSI背离权重
|
||||||
|
self.macd_divergence_weight = 10 # MACD背离权重
|
||||||
|
|
||||||
|
# ===== 价格差异参数 =====
|
||||||
|
self.price_diff_multiplier = 1000 # 价格差异放大倍数
|
||||||
|
self.max_price_score = 20 # 价格差异最高得分
|
||||||
|
|
||||||
|
# ===== 突破检测参数 =====
|
||||||
|
self.breakthrough_lookback = 10 # 回看K线数量
|
||||||
|
self.breakthrough_multiplier = 500 # 突破幅度放大倍数
|
||||||
|
self.max_breakthrough_score = 20 # 突破最高得分
|
||||||
|
|
||||||
|
# ===== 成交量参数 =====
|
||||||
|
self.volume_lookback = 5 # 计算平均成交量的回看期数
|
||||||
|
self.volume_multiplier = 10 # 成交量放大倍数
|
||||||
|
self.max_volume_score = 15 # 成交量最高得分
|
||||||
|
self.min_volume_ratio = 1.0 # 最小成交量比率
|
||||||
|
|
||||||
|
# ===== RSI背离参数 =====
|
||||||
|
self.rsi_divergence_divisor = 2 # RSI背离除数
|
||||||
|
self.max_rsi_score = 15 # RSI最高得分
|
||||||
|
|
||||||
|
# ===== MACD背离参数 =====
|
||||||
|
self.macd_divergence_multiplier = 100 # MACD背离放大倍数
|
||||||
|
self.max_macd_score = 10 # MACD最高得分
|
||||||
|
|
||||||
|
# ===== 强度等级阈值 =====
|
||||||
|
self.extreme_threshold = 80 # 极强分型阈值
|
||||||
|
self.strong_threshold = 60 # 强分型阈值
|
||||||
|
self.medium_threshold = 40 # 中等分型阈值
|
||||||
|
self.weak_threshold = 20 # 弱分型阈值
|
||||||
|
|
||||||
|
# ===== 其他参数 =====
|
||||||
|
self.min_strength = 0 # 最小强度分数
|
||||||
|
self.max_strength = 100 # 最大强度分数
|
||||||
|
|
||||||
|
def get_strength_level_name(self, strength):
|
||||||
|
"""根据强度分数获取等级名称"""
|
||||||
|
if strength >= self.extreme_threshold:
|
||||||
|
return "极强"
|
||||||
|
elif strength >= self.strong_threshold:
|
||||||
|
return "强"
|
||||||
|
elif strength >= self.medium_threshold:
|
||||||
|
return "中等"
|
||||||
|
elif strength >= self.weak_threshold:
|
||||||
|
return "弱"
|
||||||
|
else:
|
||||||
|
return "极弱"
|
||||||
|
|
||||||
|
def is_strong_fractal(self, strength, custom_threshold=None):
|
||||||
|
"""判断是否为强分型"""
|
||||||
|
threshold = custom_threshold if custom_threshold is not None else self.strong_threshold
|
||||||
|
return strength >= threshold
|
||||||
|
|
||||||
|
def validate_config(self):
|
||||||
|
"""验证配置参数的合理性"""
|
||||||
|
total_weight = (self.price_difference_weight +
|
||||||
|
self.breakthrough_weight +
|
||||||
|
self.volume_weight +
|
||||||
|
self.rsi_divergence_weight +
|
||||||
|
self.macd_divergence_weight)
|
||||||
|
|
||||||
|
if total_weight != 100:
|
||||||
|
print(f"警告: 权重总和为{total_weight},不等于100")
|
||||||
|
|
||||||
|
if not (0 <= self.extreme_threshold <= 100):
|
||||||
|
print(f"警告: 极强阈值{self.extreme_threshold}不在合理范围内")
|
||||||
|
|
||||||
|
if not (self.weak_threshold < self.medium_threshold <
|
||||||
|
self.strong_threshold < self.extreme_threshold):
|
||||||
|
print("警告: 强度阈值设置不合理")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def print_config(self):
|
||||||
|
"""打印当前配置"""
|
||||||
|
print("=== 分型强度检测配置 ===")
|
||||||
|
print(f"价格差异权重: {self.price_difference_weight}分")
|
||||||
|
print(f"突破点位权重: {self.breakthrough_weight}分")
|
||||||
|
print(f"成交量权重: {self.volume_weight}分")
|
||||||
|
print(f"RSI背离权重: {self.rsi_divergence_weight}分")
|
||||||
|
print(f"MACD背离权重: {self.macd_divergence_weight}分")
|
||||||
|
print()
|
||||||
|
print("=== 强度等级阈值 ===")
|
||||||
|
print(f"极强: >={self.extreme_threshold}分")
|
||||||
|
print(f"强: {self.strong_threshold}-{self.extreme_threshold-1}分")
|
||||||
|
print(f"中等: {self.medium_threshold}-{self.strong_threshold-1}分")
|
||||||
|
print(f"弱: {self.weak_threshold}-{self.medium_threshold-1}分")
|
||||||
|
print(f"极弱: <{self.weak_threshold}分")
|
||||||
|
|
||||||
|
|
||||||
|
# 默认配置实例
|
||||||
|
DEFAULT_CONFIG = FxStrengthConfig()
|
||||||
|
|
||||||
|
# 保守配置 (更严格的分型识别)
|
||||||
|
CONSERVATIVE_CONFIG = FxStrengthConfig()
|
||||||
|
CONSERVATIVE_CONFIG.price_difference_weight = 50
|
||||||
|
CONSERVATIVE_CONFIG.breakthrough_weight = 25
|
||||||
|
CONSERVATIVE_CONFIG.volume_weight = 15
|
||||||
|
CONSERVATIVE_CONFIG.rsi_divergence_weight = 10
|
||||||
|
CONSERVATIVE_CONFIG.macd_divergence_weight = 0
|
||||||
|
CONSERVATIVE_CONFIG.strong_threshold = 70
|
||||||
|
CONSERVATIVE_CONFIG.extreme_threshold = 85
|
||||||
|
|
||||||
|
# 激进配置 (更宽松的分型识别)
|
||||||
|
AGGRESSIVE_CONFIG = FxStrengthConfig()
|
||||||
|
AGGRESSIVE_CONFIG.price_difference_weight = 30
|
||||||
|
AGGRESSIVE_CONFIG.breakthrough_weight = 15
|
||||||
|
AGGRESSIVE_CONFIG.volume_weight = 20
|
||||||
|
AGGRESSIVE_CONFIG.rsi_divergence_weight = 20
|
||||||
|
AGGRESSIVE_CONFIG.macd_divergence_weight = 15
|
||||||
|
AGGRESSIVE_CONFIG.strong_threshold = 50
|
||||||
|
AGGRESSIVE_CONFIG.extreme_threshold = 70
|
||||||
|
|
||||||
|
# 技术指标重点配置 (重视技术指标背离)
|
||||||
|
TECHNICAL_CONFIG = FxStrengthConfig()
|
||||||
|
TECHNICAL_CONFIG.price_difference_weight = 25
|
||||||
|
TECHNICAL_CONFIG.breakthrough_weight = 15
|
||||||
|
TECHNICAL_CONFIG.volume_weight = 10
|
||||||
|
TECHNICAL_CONFIG.rsi_divergence_weight = 25
|
||||||
|
TECHNICAL_CONFIG.macd_divergence_weight = 25
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("=== 分型强度配置演示 ===\n")
|
||||||
|
|
||||||
|
configs = {
|
||||||
|
"默认配置": DEFAULT_CONFIG,
|
||||||
|
"保守配置": CONSERVATIVE_CONFIG,
|
||||||
|
"激进配置": AGGRESSIVE_CONFIG,
|
||||||
|
"技术指标配置": TECHNICAL_CONFIG
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, config in configs.items():
|
||||||
|
print(f"=== {name} ===")
|
||||||
|
config.print_config()
|
||||||
|
config.validate_config()
|
||||||
|
print()
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
分型强度检测使用示例
|
||||||
|
该文件展示如何使用ChanKLC类中新增的分型强度检测功能
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ChanKLC import ChanKLC
|
||||||
|
from ChanEnum import Chan_FX_TYPE
|
||||||
|
import ChanKLU
|
||||||
|
|
||||||
|
|
||||||
|
def demo_fx_strength_detection():
|
||||||
|
"""
|
||||||
|
演示分型强度检测功能
|
||||||
|
"""
|
||||||
|
print("=== 分型强度检测功能演示 ===\n")
|
||||||
|
|
||||||
|
# 假设我们有一个已经确定为分型的KLC对象
|
||||||
|
# 这里仅为演示,实际使用中KLC对象应该通过正常流程创建
|
||||||
|
|
||||||
|
print("1. 分型强度计算方法:")
|
||||||
|
print(" - calculate_fx_strength(): 返回0-100的强度分数")
|
||||||
|
print(" - get_fx_strength_level(): 返回强度等级描述")
|
||||||
|
print(" - is_strong_fx(threshold): 判断是否为强分型")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("2. 强度评分维度 (总分100分):")
|
||||||
|
print(" - 价格差异强度: 40分 (与相邻K线的价格差异)")
|
||||||
|
print(" - 突破历史点位: 20分 (是否突破重要高低点)")
|
||||||
|
print(" - 成交量确认: 15分 (分型形成时的成交量)")
|
||||||
|
print(" - RSI背离确认: 15分 (价格与RSI的背离)")
|
||||||
|
print(" - MACD背离确认: 10分 (价格与MACD的背离)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("3. 强度等级分类:")
|
||||||
|
print(" - 极强: 80-100分")
|
||||||
|
print(" - 强: 60-79分")
|
||||||
|
print(" - 中等: 40-59分")
|
||||||
|
print(" - 弱: 20-39分")
|
||||||
|
print(" - 极弱: 0-19分")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("4. 在特征数据中的应用:")
|
||||||
|
print(" 分型强度会自动集成到get_feature_data()方法返回的特征中:")
|
||||||
|
print(" - klc_fx_strength: 强度分数")
|
||||||
|
print(" - klc_fx_strength_level: 强度等级")
|
||||||
|
print(" - klc_is_strong_fx: 是否为强分型(布尔值)")
|
||||||
|
print(" - klc_fx_strength_extreme: 是否为极强分型")
|
||||||
|
print(" - klc_fx_strength_strong: 是否为强分型")
|
||||||
|
print(" - klc_fx_strength_medium: 是否为中等分型")
|
||||||
|
print(" - klc_fx_strength_weak: 是否为弱分型")
|
||||||
|
print(" - klc_fx_strength_very_weak: 是否为极弱分型")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_fx_strength(klc):
|
||||||
|
"""
|
||||||
|
分析单个KLC的分型强度
|
||||||
|
|
||||||
|
Args:
|
||||||
|
klc: ChanKLC对象
|
||||||
|
"""
|
||||||
|
if klc.fx == Chan_FX_TYPE.UNKNOWN:
|
||||||
|
print(f"时间: {klc.start_time} - 无分型")
|
||||||
|
return
|
||||||
|
|
||||||
|
fx_type = "顶分型" if klc.fx == Chan_FX_TYPE.TOP else "底分型"
|
||||||
|
strength = klc.calculate_fx_strength()
|
||||||
|
strength_level = klc.get_fx_strength_level()
|
||||||
|
is_strong = klc.is_strong_fx()
|
||||||
|
|
||||||
|
print(f"时间: {klc.start_time}")
|
||||||
|
print(f"分型类型: {fx_type}")
|
||||||
|
print(f"强度分数: {strength}")
|
||||||
|
print(f"强度等级: {strength_level}")
|
||||||
|
print(f"是否强分型: {'是' if is_strong else '否'}")
|
||||||
|
print("-" * 30)
|
||||||
|
|
||||||
|
|
||||||
|
def filter_strong_fractals(klc_list, min_strength=60):
|
||||||
|
"""
|
||||||
|
筛选强分型
|
||||||
|
|
||||||
|
Args:
|
||||||
|
klc_list: KLC对象列表
|
||||||
|
min_strength: 最小强度阈值
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
强分型列表
|
||||||
|
"""
|
||||||
|
strong_fractals = []
|
||||||
|
|
||||||
|
for klc in klc_list:
|
||||||
|
if klc.fx != Chan_FX_TYPE.UNKNOWN and klc.is_strong_fx(min_strength):
|
||||||
|
strong_fractals.append(klc)
|
||||||
|
|
||||||
|
return strong_fractals
|
||||||
|
|
||||||
|
|
||||||
|
def get_fractal_statistics(klc_list):
|
||||||
|
"""
|
||||||
|
获取分型强度统计信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
klc_list: KLC对象列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
统计信息字典
|
||||||
|
"""
|
||||||
|
stats = {
|
||||||
|
'total_fractals': 0,
|
||||||
|
'top_fractals': 0,
|
||||||
|
'bottom_fractals': 0,
|
||||||
|
'extreme_strength': 0, # 极强
|
||||||
|
'strong_strength': 0, # 强
|
||||||
|
'medium_strength': 0, # 中等
|
||||||
|
'weak_strength': 0, # 弱
|
||||||
|
'very_weak_strength': 0,# 极弱
|
||||||
|
'avg_strength': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
strengths = []
|
||||||
|
|
||||||
|
for klc in klc_list:
|
||||||
|
if klc.fx != Chan_FX_TYPE.UNKNOWN:
|
||||||
|
stats['total_fractals'] += 1
|
||||||
|
|
||||||
|
if klc.fx == Chan_FX_TYPE.TOP:
|
||||||
|
stats['top_fractals'] += 1
|
||||||
|
else:
|
||||||
|
stats['bottom_fractals'] += 1
|
||||||
|
|
||||||
|
strength = klc.calculate_fx_strength()
|
||||||
|
strengths.append(strength)
|
||||||
|
|
||||||
|
if strength >= 80:
|
||||||
|
stats['extreme_strength'] += 1
|
||||||
|
elif strength >= 60:
|
||||||
|
stats['strong_strength'] += 1
|
||||||
|
elif strength >= 40:
|
||||||
|
stats['medium_strength'] += 1
|
||||||
|
elif strength >= 20:
|
||||||
|
stats['weak_strength'] += 1
|
||||||
|
else:
|
||||||
|
stats['very_weak_strength'] += 1
|
||||||
|
|
||||||
|
if strengths:
|
||||||
|
stats['avg_strength'] = sum(strengths) / len(strengths)
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
demo_fx_strength_detection()
|
||||||
|
|
||||||
|
print("=== 使用建议 ===")
|
||||||
|
print("1. 在交易策略中,可以只关注强度>=60的分型")
|
||||||
|
print("2. 极强分型(>=80分)通常是重要的转折点")
|
||||||
|
print("3. 结合成交量和技术指标背离的分型更可靠")
|
||||||
|
print("4. 可以用分型强度来设置止损和止盈位置")
|
||||||
|
print("5. 分型强度可以作为机器学习模型的重要特征")
|
||||||
+274
-259
@@ -1,9 +1,10 @@
|
|||||||
# --- Do not remove these libs ---
|
# --- Do not remove these libs ---
|
||||||
from freqtrade.strategy import IStrategy
|
from freqtrade.strategy import IStrategy
|
||||||
from typing import Dict, List
|
from typing import Dict, List, Tuple, Optional
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from pandas import DataFrame, pandas
|
from pandas import DataFrame
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
# --------------------------------
|
# --------------------------------
|
||||||
from technical.util import resample_to_interval, resampled_merge
|
from technical.util import resample_to_interval, resampled_merge
|
||||||
@@ -12,329 +13,343 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib
|
|||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from freqtrade.persistence import Trade, Order
|
from freqtrade.persistence import Trade, Order
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
### Now you can use logger.info('asfd') to log
|
# freqtrade backtesting -c ./user_data/Chan/config/ChanLun_SOL.json --strategy Chan_SOL_2 --strategy-path ./user_data/Chan/strategies --timerange=20240801-20241201
|
||||||
|
|
||||||
# freqtrade trade -c ./user_data/Chan/config/ChanLun_SOL.json --strategy ChanLun_SOL_2 --strategy-path ./user_data/Chan/strategies
|
class Chan_SOL_2(IStrategy):
|
||||||
# freqtrade backtesting -c ./user_data/Chan/config/ChanLun_SOL.json --strategy ChanLun_SOL_2 --strategy-path ./user_data/Chan/strategies --timerange=20250309-
|
"""
|
||||||
# freqtrade download-data -c ./user_data/Chan/config/ChanLun_SOL.json -t 1m --pairs SOL/USDT:USDT --timerange=20250501-
|
稳定盈利交易策略 - 基于多重技术分析
|
||||||
# freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss --strategy ChanLun_SOL_2 --strategy-path ./user_data/strategies -c ./user_data/ChanLun_SOL.json -e 200 --timerange=20250101-20250215
|
结合趋势跟踪、动量指标和风险管理
|
||||||
|
"""
|
||||||
# sudo docker compose run --rm chan_btc backtesting -c ./user_data/Chan.json --strategy Chan_SOL_2 --strategy-path ./user_data/strategies --timerange=20250101-
|
|
||||||
# sudo docker compose run --rm chan_btc download-data -c ./user_data/Chan.json --pairs SOL/USDT:USDT -t 1m --timerange 20240101-
|
|
||||||
# sudo docker compose run --rm chan_btc trade -c ./user_data/Chan.json --strategy Chan_SOL_2 --strategy-path ./user_data/strategies
|
|
||||||
|
|
||||||
class ChanLun_SOL_2(IStrategy):
|
|
||||||
INTERFACE_VERSION: int = 3
|
INTERFACE_VERSION: int = 3
|
||||||
|
|
||||||
# 优化的ROI设置 - 更快速获利
|
# 优化的ROI设置 - 阶梯式获利了结
|
||||||
minimal_roi = {
|
minimal_roi = {
|
||||||
"0": 0.012, # 立即获利1.2%
|
"0": 0.15, # 15%快速获利
|
||||||
"5": 0.01, # 5分钟后获利1%
|
"30": 0.08, # 30分钟后8%
|
||||||
"15": 0.007, # 15分钟后获利0.7%
|
"60": 0.05, # 1小时后5%
|
||||||
"30": 0.005 # 30分钟后获利0.5%
|
"120": 0.03, # 2小时后3%
|
||||||
|
"240": 0.02, # 4小时后2%
|
||||||
|
"480": 0.015, # 8小时后1.5%
|
||||||
|
"960": 0.01 # 16小时后1%
|
||||||
}
|
}
|
||||||
|
|
||||||
can_short = True
|
can_short = True
|
||||||
stoploss = -0.007 # 降低止损为0.7%
|
stoploss = -0.08 # 8%止损
|
||||||
|
|
||||||
# 追踪止损设置 - 更积极的追踪止损
|
# 动态追踪止损
|
||||||
trailing_stop = True
|
trailing_stop = True
|
||||||
trailing_stop_positive = 0.003 # 0.3%
|
trailing_stop_positive = 0.015 # 1.5%开始追踪
|
||||||
trailing_stop_positive_offset = 0.005 # 0.5%
|
trailing_stop_positive_offset = 0.025 # 2.5%偏移
|
||||||
trailing_only_offset_is_reached = True
|
trailing_only_offset_is_reached = True
|
||||||
|
|
||||||
# 时间周期
|
# 仓位管理
|
||||||
|
position_adjustment_enable = True
|
||||||
|
max_entry_position_adjustment = 2
|
||||||
|
max_dca_multiplier = 3.0
|
||||||
|
|
||||||
timeframe = '5m'
|
timeframe = '5m'
|
||||||
informative_timeframe = '1h'
|
|
||||||
startup_candle_count = 200
|
startup_candle_count = 200
|
||||||
|
|
||||||
# 只做空头策略
|
# 自定义参数
|
||||||
only_short = True
|
buy_volume_threshold = 1.5
|
||||||
|
sell_volume_threshold = 1.2
|
||||||
def informative_pairs(self):
|
rsi_oversold = 25
|
||||||
pairs = self.dp.current_whitelist()
|
rsi_overbought = 75
|
||||||
informative_pairs = [(pair, self.informative_timeframe) for pair in pairs]
|
adx_trend_threshold = 25
|
||||||
return informative_pairs
|
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
# 获取更高时间周期的数据
|
"""
|
||||||
informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=self.informative_timeframe)
|
添加技术指标 - 多维度分析
|
||||||
|
"""
|
||||||
|
# === 趋势指标 ===
|
||||||
|
# 多周期移动平均线
|
||||||
|
dataframe['ema_8'] = ta.EMA(dataframe, timeperiod=8)
|
||||||
|
dataframe['ema_21'] = ta.EMA(dataframe, timeperiod=21)
|
||||||
|
dataframe['ema_50'] = ta.EMA(dataframe, timeperiod=50)
|
||||||
|
dataframe['ema_200'] = ta.EMA(dataframe, timeperiod=200)
|
||||||
|
|
||||||
# === 高时间周期指标 ===
|
# === 动量指标 ===
|
||||||
# 三均线系统
|
# RSI - 超买超卖
|
||||||
informative['ema50'] = ta.EMA(informative, timeperiod=50)
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
informative['ema100'] = ta.EMA(informative, timeperiod=100)
|
dataframe['rsi_fast'] = ta.RSI(dataframe, timeperiod=9)
|
||||||
informative['ema200'] = ta.SMA(informative, timeperiod=200) # 使用SMA作为长期趋势
|
dataframe['rsi_slow'] = ta.RSI(dataframe, timeperiod=21)
|
||||||
|
|
||||||
# 趋势方向
|
# MACD - 趋势动量
|
||||||
informative['uptrend'] = (
|
macd = ta.MACD(dataframe, fastperiod=12, slowperiod=26, signalperiod=9)
|
||||||
(informative['ema50'] > informative['ema100']) &
|
dataframe['macd'] = macd['macd']
|
||||||
(informative['ema100'] > informative['ema200']) &
|
dataframe['macdsignal'] = macd['macdsignal']
|
||||||
(informative['close'] > informative['ema50'])
|
dataframe['macdhist'] = macd['macdhist']
|
||||||
).astype(int)
|
|
||||||
|
|
||||||
informative['downtrend'] = (
|
# === 波动率指标 ===
|
||||||
(informative['ema50'] < informative['ema100']) &
|
# ATR - 真实波动幅度
|
||||||
(informative['ema100'] < informative['ema200']) &
|
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
|
||||||
(informative['close'] < informative['ema50'])
|
|
||||||
).astype(int)
|
|
||||||
|
|
||||||
# 强下降趋势
|
|
||||||
informative['strong_downtrend'] = (
|
|
||||||
(informative['ema50'] < informative['ema100']) &
|
|
||||||
(informative['ema100'] < informative['ema200']) &
|
|
||||||
(informative['close'] < informative['ema50']) &
|
|
||||||
(informative['ema50'].shift(3) < informative['ema50']) # 确认EMA50下降
|
|
||||||
).astype(int)
|
|
||||||
|
|
||||||
# 添加高时间周期的ADX指标
|
|
||||||
informative['adx'] = ta.ADX(informative, timeperiod=14)
|
|
||||||
|
|
||||||
# 添加高时间周期的波动率
|
|
||||||
informative['atr'] = ta.ATR(informative, timeperiod=14)
|
|
||||||
informative['atr_percent'] = (informative['atr'] / informative['close']) * 100
|
|
||||||
|
|
||||||
# 高时间周期RSI
|
|
||||||
informative['rsi'] = ta.RSI(informative, timeperiod=14)
|
|
||||||
|
|
||||||
# 将informative数据帧中的列重命名,以便在合并后区分
|
|
||||||
for col in informative.columns:
|
|
||||||
if col not in ['date', 'open', 'high', 'low', 'close', 'volume']:
|
|
||||||
informative[f"{col}_{self.informative_timeframe}"] = informative[col]
|
|
||||||
|
|
||||||
# 删除原始列,只保留重命名后的列和必要的日期、OHLCV列
|
|
||||||
for col in list(informative.columns):
|
|
||||||
if col not in ['date', 'open', 'high', 'low', 'close', 'volume'] and not col.endswith(f"_{self.informative_timeframe}"):
|
|
||||||
del informative[col]
|
|
||||||
|
|
||||||
# 打印列名以便调试
|
|
||||||
logger.info(f"Informative columns after renaming: {informative.columns.tolist()}")
|
|
||||||
|
|
||||||
# 合并数据 - 使用正确的参数
|
|
||||||
dataframe = resampled_merge(dataframe, informative, self.informative_timeframe)
|
|
||||||
|
|
||||||
# 打印合并后的列名以便调试
|
|
||||||
logger.info(f"Dataframe columns after merge: {dataframe.columns.tolist()}")
|
|
||||||
|
|
||||||
# === 主时间周期指标 ===
|
|
||||||
# 布林带
|
# 布林带
|
||||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||||||
dataframe['bb_lowerband'] = bollinger['lower']
|
dataframe['bb_lowerband'] = bollinger['lower']
|
||||||
dataframe['bb_middleband'] = bollinger['mid']
|
dataframe['bb_middleband'] = bollinger['mid']
|
||||||
dataframe['bb_upperband'] = bollinger['upper']
|
dataframe['bb_upperband'] = bollinger['upper']
|
||||||
dataframe['bb_width'] = ((bollinger['upper'] - bollinger['lower']) / bollinger['mid'])
|
dataframe['bb_percent'] = (dataframe['close'] - dataframe['bb_lowerband']) / (dataframe['bb_upperband'] - dataframe['bb_lowerband'])
|
||||||
|
dataframe['bb_width'] = (dataframe['bb_upperband'] - dataframe['bb_lowerband']) / dataframe['bb_middleband']
|
||||||
|
|
||||||
# 动量指标
|
# === 趋势强度指标 ===
|
||||||
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
# ADX - 趋势强度
|
||||||
dataframe['mfi'] = ta.MFI(dataframe, timeperiod=14)
|
|
||||||
|
|
||||||
# MACD
|
|
||||||
macd = ta.MACD(dataframe)
|
|
||||||
dataframe['macd'] = macd['macd']
|
|
||||||
dataframe['macdsignal'] = macd['macdsignal']
|
|
||||||
dataframe['macdhist'] = macd['macdhist']
|
|
||||||
|
|
||||||
# 均线
|
|
||||||
dataframe['ema9'] = ta.EMA(dataframe, timeperiod=9)
|
|
||||||
dataframe['ema21'] = ta.EMA(dataframe, timeperiod=21)
|
|
||||||
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
|
||||||
dataframe['sma200'] = ta.SMA(dataframe, timeperiod=200)
|
|
||||||
|
|
||||||
# 成交量
|
|
||||||
dataframe['volume_mean'] = dataframe['volume'].rolling(window=20).mean()
|
|
||||||
dataframe['volume_ratio'] = dataframe['volume'] / dataframe['volume_mean']
|
|
||||||
|
|
||||||
# 波动率
|
|
||||||
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
|
|
||||||
|
|
||||||
# ADX - 趋势强度指标
|
|
||||||
dataframe['adx'] = ta.ADX(dataframe, timeperiod=14)
|
dataframe['adx'] = ta.ADX(dataframe, timeperiod=14)
|
||||||
|
dataframe['plus_di'] = ta.PLUS_DI(dataframe, timeperiod=14)
|
||||||
|
dataframe['minus_di'] = ta.MINUS_DI(dataframe, timeperiod=14)
|
||||||
|
|
||||||
# 价格突破
|
# === 成交量指标 ===
|
||||||
dataframe['upper_break'] = (
|
# 成交量移动平均
|
||||||
(dataframe['close'] > dataframe['bb_upperband']) &
|
dataframe['volume_sma_20'] = dataframe['volume'].rolling(window=20).mean()
|
||||||
(dataframe['close'].shift() <= dataframe['bb_upperband'].shift())
|
dataframe['volume_ratio'] = dataframe['volume'] / dataframe['volume_sma_20']
|
||||||
).astype(int)
|
|
||||||
|
|
||||||
dataframe['lower_break'] = (
|
# OBV - 能量潮
|
||||||
(dataframe['close'] < dataframe['bb_lowerband']) &
|
dataframe['obv'] = ta.OBV(dataframe)
|
||||||
(dataframe['close'].shift() >= dataframe['bb_lowerband'].shift())
|
dataframe['obv_ema'] = ta.EMA(dataframe['obv'], timeperiod=20)
|
||||||
).astype(int)
|
|
||||||
|
|
||||||
# 均线交叉
|
# === 价格行为指标 ===
|
||||||
dataframe['ema_cross_up'] = (
|
# 价格变化率
|
||||||
(dataframe['ema9'] > dataframe['ema21']) &
|
dataframe['price_change'] = dataframe['close'].pct_change()
|
||||||
(dataframe['ema9'].shift() <= dataframe['ema21'].shift())
|
dataframe['price_change_5'] = dataframe['close'].pct_change(periods=5)
|
||||||
).astype(int)
|
|
||||||
|
|
||||||
dataframe['ema_cross_down'] = (
|
# 高低点分析
|
||||||
(dataframe['ema9'] < dataframe['ema21']) &
|
dataframe['high_20'] = dataframe['high'].rolling(window=20).max()
|
||||||
(dataframe['ema9'].shift() >= dataframe['ema21'].shift())
|
dataframe['low_20'] = dataframe['low'].rolling(window=20).min()
|
||||||
).astype(int)
|
|
||||||
|
|
||||||
# 超买超卖区域
|
# === 自定义复合指标 ===
|
||||||
dataframe['rsi_oversold'] = (dataframe['rsi'] < 30).astype(int)
|
# 趋势确认信号
|
||||||
dataframe['rsi_overbought'] = (dataframe['rsi'] > 70).astype(int)
|
dataframe['trend_up'] = (
|
||||||
|
(dataframe['ema_8'] > dataframe['ema_21']) &
|
||||||
|
(dataframe['ema_21'] > dataframe['ema_50']) &
|
||||||
|
(dataframe['close'] > dataframe['ema_8'])
|
||||||
|
)
|
||||||
|
|
||||||
# 价格与均线的关系
|
dataframe['trend_down'] = (
|
||||||
dataframe['price_above_ema50'] = (dataframe['close'] > dataframe['ema50']).astype(int)
|
(dataframe['ema_8'] < dataframe['ema_21']) &
|
||||||
dataframe['price_below_ema50'] = (dataframe['close'] < dataframe['ema50']).astype(int)
|
(dataframe['ema_21'] < dataframe['ema_50']) &
|
||||||
|
(dataframe['close'] < dataframe['ema_8'])
|
||||||
|
)
|
||||||
|
|
||||||
# 趋势强度
|
# 动量强度评分
|
||||||
dataframe['strong_trend'] = (dataframe['adx'] > 25).astype(int)
|
dataframe['momentum_score'] = (
|
||||||
|
((dataframe['rsi'] > 50).astype(int) * 1) +
|
||||||
|
((dataframe['macd'] > dataframe['macdsignal']).astype(int) * 1) +
|
||||||
|
((dataframe['adx'] > self.adx_trend_threshold).astype(int) * 1) +
|
||||||
|
((dataframe['volume_ratio'] > 1.0).astype(int) * 1)
|
||||||
|
)
|
||||||
|
|
||||||
# 添加蜡烛图形态识别
|
# 波动率适应性指标
|
||||||
dataframe['doji'] = ta.CDLDOJI(dataframe['open'], dataframe['high'], dataframe['low'], dataframe['close'])
|
dataframe['volatility_high'] = dataframe['atr'] > dataframe['atr'].rolling(window=20).mean() * 1.5
|
||||||
dataframe['engulfing'] = ta.CDLENGULFING(dataframe['open'], dataframe['high'], dataframe['low'], dataframe['close'])
|
|
||||||
dataframe['hammer'] = ta.CDLHAMMER(dataframe['open'], dataframe['high'], dataframe['low'], dataframe['close'])
|
|
||||||
dataframe['shooting_star'] = ta.CDLSHOOTINGSTAR(dataframe['open'], dataframe['high'], dataframe['low'], dataframe['close'])
|
|
||||||
|
|
||||||
# 价格动量
|
|
||||||
dataframe['momentum'] = dataframe['close'] - dataframe['close'].shift(5)
|
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
# 检查列名是否存在
|
"""
|
||||||
downtrend_col = 'resample_60_downtrend_1h'
|
入场信号 - 多条件确认系统
|
||||||
strong_downtrend_col = 'resample_60_strong_downtrend_1h'
|
"""
|
||||||
adx_col = 'resample_60_adx_1h'
|
# === 多头入场条件 ===
|
||||||
rsi_col = 'resample_60_rsi_1h'
|
|
||||||
|
|
||||||
# 如果列名不存在,使用替代方案
|
# 条件1: 强势突破入场
|
||||||
for col, default_value in [
|
dataframe.loc[
|
||||||
(downtrend_col, 0),
|
|
||||||
(strong_downtrend_col, 0),
|
|
||||||
(adx_col, 25),
|
|
||||||
(rsi_col, 50)
|
|
||||||
]:
|
|
||||||
if col not in dataframe.columns:
|
|
||||||
logger.warning(f"Column {col} not found in dataframe. Creating with default value {default_value}.")
|
|
||||||
dataframe[col] = default_value
|
|
||||||
|
|
||||||
# 禁用多头入场
|
|
||||||
dataframe['enter_long'] = 0
|
|
||||||
|
|
||||||
# 空头入场条件 - 专注于空头策略
|
|
||||||
short_conditions = (
|
|
||||||
# 高时间周期处于下降趋势
|
|
||||||
(dataframe[downtrend_col] > 0) &
|
|
||||||
|
|
||||||
# 趋势强度确认
|
|
||||||
(dataframe[adx_col] > 25) &
|
|
||||||
|
|
||||||
# 条件1: 价格突破上轨后回落 + 成交量确认
|
|
||||||
(
|
(
|
||||||
(dataframe['upper_break'].rolling(window=5).sum() > 0) & # 最近5根K线内有突破上轨
|
# 趋势确认
|
||||||
(dataframe['close'] < dataframe['close'].shift(2)) & # 价格开始下跌
|
(dataframe['trend_up']) &
|
||||||
(dataframe['close'] < dataframe['ema9']) & # 价格在短期均线下方
|
(dataframe['close'] > dataframe['ema_21']) &
|
||||||
(dataframe['volume_ratio'] > 1.3) & # 成交量放大
|
|
||||||
(dataframe['rsi'] < 70) & # RSI不在极度超买区
|
|
||||||
(dataframe['rsi'] > 40) & # RSI不在超卖区
|
|
||||||
(dataframe[rsi_col] < 60) # 高时间周期RSI不过高
|
|
||||||
) |
|
|
||||||
|
|
||||||
# 条件2: 均线死叉 + RSI超买回落 + 趋势确认
|
# 动量确认
|
||||||
|
(dataframe['rsi'] > 45) & (dataframe['rsi'] < 75) &
|
||||||
|
(dataframe['macd'] > dataframe['macdsignal']) &
|
||||||
|
(dataframe['macdhist'] > dataframe['macdhist'].shift(1)) &
|
||||||
|
|
||||||
|
# 成交量确认
|
||||||
|
(dataframe['volume_ratio'] > self.buy_volume_threshold) &
|
||||||
|
(dataframe['obv'] > dataframe['obv_ema']) &
|
||||||
|
|
||||||
|
# 价格行为确认
|
||||||
|
(dataframe['close'] > dataframe['bb_middleband']) &
|
||||||
|
(dataframe['bb_percent'] > 0.2) & (dataframe['bb_percent'] < 0.8) &
|
||||||
|
|
||||||
|
# 趋势强度确认
|
||||||
|
(dataframe['adx'] > self.adx_trend_threshold) &
|
||||||
|
(dataframe['plus_di'] > dataframe['minus_di'])
|
||||||
|
),
|
||||||
|
['enter_long', 'enter_tag']] = (1, 'breakout_long')
|
||||||
|
|
||||||
|
# 条件2: 超卖反弹入场
|
||||||
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
(dataframe['ema_cross_down'] > 0) & # 均线死叉
|
# 超卖反弹
|
||||||
(dataframe['rsi'] > 55) & # RSI相对较高
|
(dataframe['rsi'] < self.rsi_oversold + 10) &
|
||||||
(dataframe['rsi'] < dataframe['rsi'].shift(3)) & # RSI下降
|
(dataframe['rsi'] > dataframe['rsi'].shift(1)) &
|
||||||
(dataframe['volume_ratio'] > 1.2) & # 成交量放大
|
(dataframe['bb_percent'] < 0.2) &
|
||||||
(dataframe['adx'] > 20) & # ADX显示有一定趋势强度
|
|
||||||
((dataframe['shooting_star'] > 0) | (dataframe['engulfing'] < 0)) # 流星线或看跌吞没形态
|
|
||||||
) |
|
|
||||||
|
|
||||||
# 条件3: 价格在高点回落 + 强趋势
|
# 趋势不能太差
|
||||||
|
(dataframe['ema_8'] >= dataframe['ema_50']) &
|
||||||
|
(dataframe['close'] > dataframe['low_20'] * 1.02) &
|
||||||
|
|
||||||
|
# 成交量支持
|
||||||
|
(dataframe['volume_ratio'] > 1.2) &
|
||||||
|
|
||||||
|
# MACD底背离迹象
|
||||||
|
(dataframe['macdhist'] > dataframe['macdhist'].shift(1)) &
|
||||||
|
|
||||||
|
# 不与第一个条件重复
|
||||||
|
(~dataframe['enter_long'].astype(bool))
|
||||||
|
),
|
||||||
|
['enter_long', 'enter_tag']] = (1, 'oversold_long')
|
||||||
|
|
||||||
|
# === 空头入场条件 ===
|
||||||
|
|
||||||
|
# 条件1: 强势下跌入场
|
||||||
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
(dataframe['close'] < dataframe['high'].shift()) &
|
# 趋势确认
|
||||||
(dataframe['high'].shift() > dataframe['high'].shift(2)) &
|
(dataframe['trend_down']) &
|
||||||
(dataframe['close'] < dataframe['ema21']) &
|
(dataframe['close'] < dataframe['ema_21']) &
|
||||||
(dataframe['adx'] > 30) &
|
|
||||||
(dataframe['rsi'] < dataframe['rsi'].shift()) &
|
|
||||||
(dataframe['rsi'].shift() > 65) &
|
|
||||||
(dataframe['volume_ratio'] > 1.0)
|
|
||||||
) |
|
|
||||||
|
|
||||||
# 条件4: 强下降趋势确认
|
# 动量确认
|
||||||
|
(dataframe['rsi'] < 55) & (dataframe['rsi'] > 25) &
|
||||||
|
(dataframe['macd'] < dataframe['macdsignal']) &
|
||||||
|
(dataframe['macdhist'] < dataframe['macdhist'].shift(1)) &
|
||||||
|
|
||||||
|
# 成交量确认
|
||||||
|
(dataframe['volume_ratio'] > self.sell_volume_threshold) &
|
||||||
|
(dataframe['obv'] < dataframe['obv_ema']) &
|
||||||
|
|
||||||
|
# 价格行为确认
|
||||||
|
(dataframe['close'] < dataframe['bb_middleband']) &
|
||||||
|
(dataframe['bb_percent'] > 0.2) & (dataframe['bb_percent'] < 0.8) &
|
||||||
|
|
||||||
|
# 趋势强度确认
|
||||||
|
(dataframe['adx'] > self.adx_trend_threshold) &
|
||||||
|
(dataframe['minus_di'] > dataframe['plus_di'])
|
||||||
|
),
|
||||||
|
['enter_short', 'enter_tag']] = (1, 'breakdown_short')
|
||||||
|
|
||||||
|
# 条件2: 超买回调入场
|
||||||
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
(dataframe[strong_downtrend_col] > 0) &
|
# 超买回调
|
||||||
(dataframe['close'] < dataframe['ema21']) &
|
(dataframe['rsi'] > self.rsi_overbought - 10) &
|
||||||
(dataframe['close'] < dataframe['close'].shift(3)) &
|
(dataframe['rsi'] < dataframe['rsi'].shift(1)) &
|
||||||
(dataframe['momentum'] < 0) &
|
(dataframe['bb_percent'] > 0.8) &
|
||||||
(dataframe['volume_ratio'] > 1.1) &
|
|
||||||
(dataframe['adx'] > 25)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
dataframe.loc[short_conditions, 'enter_short'] = 1
|
# 趋势不能太好
|
||||||
dataframe.loc[short_conditions, 'enter_tag'] = 'chan_sol_short'
|
(dataframe['ema_8'] <= dataframe['ema_50']) &
|
||||||
|
(dataframe['close'] < dataframe['high_20'] * 0.98) &
|
||||||
|
|
||||||
|
# 成交量支持
|
||||||
|
(dataframe['volume_ratio'] > 1.2) &
|
||||||
|
|
||||||
|
# MACD顶背离迹象
|
||||||
|
(dataframe['macdhist'] < dataframe['macdhist'].shift(1)) &
|
||||||
|
|
||||||
|
# 不与第一个条件重复
|
||||||
|
(~dataframe['enter_short'].astype(bool))
|
||||||
|
),
|
||||||
|
['enter_short', 'enter_tag']] = (1, 'overbought_short')
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
# 禁用多头出场
|
"""
|
||||||
dataframe['exit_long'] = 0
|
出场信号 - 及时止盈止损
|
||||||
|
"""
|
||||||
|
# === 多头出场条件 ===
|
||||||
|
|
||||||
# 空头出场条件 - 更精确的出场
|
# 条件1: 趋势转弱
|
||||||
short_exit_conditions = (
|
dataframe.loc[
|
||||||
# 条件1: 趋势反转信号
|
|
||||||
(
|
(
|
||||||
(dataframe['ema_cross_up'] > 0) & # 均线金叉
|
(
|
||||||
(dataframe['volume_ratio'] > 1.0) # 成交量确认
|
(dataframe['rsi'] > self.rsi_overbought) |
|
||||||
) |
|
(dataframe['macd'] < dataframe['macdsignal']) |
|
||||||
|
(dataframe['close'] < dataframe['ema_8']) |
|
||||||
|
(dataframe['bb_percent'] > 0.95) |
|
||||||
|
(dataframe['adx'] < 20)
|
||||||
|
) &
|
||||||
|
(dataframe['volume_ratio'] > 1.0)
|
||||||
|
),
|
||||||
|
['exit_long', 'exit_tag']] = (1, 'trend_weak_long')
|
||||||
|
|
||||||
# 条件2: 价格突破中期均线
|
# === 空头出场条件 ===
|
||||||
|
|
||||||
|
# 条件1: 趋势转强
|
||||||
|
dataframe.loc[
|
||||||
(
|
(
|
||||||
(dataframe['close'] > dataframe['ema21']) &
|
(
|
||||||
(dataframe['close'].shift() < dataframe['ema21'].shift()) & # 确认是刚刚突破
|
(dataframe['rsi'] < self.rsi_oversold) |
|
||||||
(dataframe['volume_ratio'] > 1.2) # 成交量确认
|
(dataframe['macd'] > dataframe['macdsignal']) |
|
||||||
) |
|
(dataframe['close'] > dataframe['ema_8']) |
|
||||||
|
(dataframe['bb_percent'] < 0.05) |
|
||||||
# 条件3: 超卖信号
|
(dataframe['adx'] < 20)
|
||||||
(
|
) &
|
||||||
(dataframe['rsi'] < 30) & # RSI超卖
|
(dataframe['volume_ratio'] > 1.0)
|
||||||
(dataframe['close'] < dataframe['bb_lowerband']) # 价格突破下轨
|
),
|
||||||
) |
|
['exit_short', 'exit_tag']] = (1, 'trend_strong_short')
|
||||||
|
|
||||||
# 条件4: 动量减弱
|
|
||||||
(
|
|
||||||
(dataframe['rsi'] < 35) &
|
|
||||||
(dataframe['rsi'] > dataframe['rsi'].shift()) &
|
|
||||||
(dataframe['rsi'].shift() > dataframe['rsi'].shift(2)) & # RSI连续两根K线上升
|
|
||||||
(dataframe['momentum'] > 0) # 价格动量转为正
|
|
||||||
) |
|
|
||||||
|
|
||||||
# 条件5: 锤子线形态 (潜在反转信号)
|
|
||||||
(
|
|
||||||
(dataframe['hammer'] > 0) &
|
|
||||||
(dataframe['volume_ratio'] > 1.3)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
dataframe.loc[short_exit_conditions, 'exit_short'] = 1
|
|
||||||
dataframe.loc[short_exit_conditions, 'exit_tag'] = 'chan_sol_short_exit'
|
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
|
||||||
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
|
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||||
side: str, **kwargs) -> bool:
|
|
||||||
"""
|
"""
|
||||||
在进入交易前进行额外的确认
|
动态止损策略
|
||||||
"""
|
"""
|
||||||
# 只做空头交易
|
# 基础止损
|
||||||
if side == "sell" and entry_tag == "chan_sol_short":
|
if current_profit < -0.05: # 如果亏损超过5%,严格止损
|
||||||
return True
|
return -0.08
|
||||||
return False
|
|
||||||
|
# 盈利后的动态止损
|
||||||
|
if current_profit > 0.02: # 盈利超过2%后,调整止损至成本价附近
|
||||||
|
return 0.005
|
||||||
|
elif current_profit > 0.05: # 盈利超过5%后,保证1%利润
|
||||||
|
return -current_profit + 0.01
|
||||||
|
elif current_profit > 0.10: # 盈利超过10%后,保证5%利润
|
||||||
|
return -current_profit + 0.05
|
||||||
|
|
||||||
|
return self.stoploss
|
||||||
|
|
||||||
|
def adjust_trade_position(self, trade: Trade, current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float,
|
||||||
|
min_stake: float, max_stake: float,
|
||||||
|
current_entry_rate: float, current_exit_rate: float,
|
||||||
|
current_entry_profit: float, current_exit_profit: float,
|
||||||
|
**kwargs) -> Optional[float]:
|
||||||
|
"""
|
||||||
|
仓位调整策略 - 金字塔加仓
|
||||||
|
"""
|
||||||
|
# 如果亏损超过3%,不加仓
|
||||||
|
if current_profit < -0.03:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 如果盈利超过2%且趋势持续,可以加仓
|
||||||
|
if current_profit > 0.02 and len(trade.select_filled_orders(trade.entry_side)) < self.max_entry_position_adjustment:
|
||||||
|
# 获取当前数据进行趋势确认
|
||||||
|
try:
|
||||||
|
# 简单的趋势确认逻辑
|
||||||
|
if trade.is_short:
|
||||||
|
return max_stake * 0.5 # 空头加仓
|
||||||
|
else:
|
||||||
|
return max_stake * 0.5 # 多头加仓
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
||||||
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str,
|
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str,
|
||||||
**kwargs) -> float:
|
**kwargs) -> float:
|
||||||
return 1.0
|
"""
|
||||||
|
杠杆设置 - 保守策略
|
||||||
def get_ticker_indicator(self):
|
"""
|
||||||
return int(self.timeframe[:-1])
|
# 根据入场类型调整杠杆
|
||||||
|
if entry_tag and 'breakout' in entry_tag:
|
||||||
|
return min(2.0, max_leverage) # 突破信号使用较高杠杆
|
||||||
|
elif entry_tag and ('oversold' in entry_tag or 'overbought' in entry_tag):
|
||||||
|
return min(1.5, max_leverage) # 超买超卖信号使用中等杠杆
|
||||||
|
else:
|
||||||
|
return 1.0 # 默认无杠杆
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
测试分型强度检测功能
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ChanKLC import ChanKLC
|
||||||
|
import ChanKLU
|
||||||
|
from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from collections import Counter
|
||||||
|
|
||||||
|
def test_fx_strength():
|
||||||
|
"""测试分型强度检测功能"""
|
||||||
|
print('=== 分型强度检测功能测试 ===')
|
||||||
|
|
||||||
|
# 创建一个简单的测试KLU,使用正确的构造函数参数
|
||||||
|
klu = ChanKLU.ChanKLU(
|
||||||
|
time='2024-01-01 10:00:00',
|
||||||
|
open=100.0,
|
||||||
|
high=105.0,
|
||||||
|
low=98.0,
|
||||||
|
close=103.0,
|
||||||
|
volume=1000
|
||||||
|
)
|
||||||
|
klu.rsi = 65.0
|
||||||
|
klu.volume_ratio = 1.2
|
||||||
|
klu.macdhist = 0.5
|
||||||
|
|
||||||
|
# 创建KLC对象
|
||||||
|
klc = ChanKLC(klu, 1, Chan_KLINE_DIR.UP)
|
||||||
|
klc.fx = Chan_FX_TYPE.TOP
|
||||||
|
|
||||||
|
# 测试强度计算
|
||||||
|
strength = klc.calculate_fx_strength()
|
||||||
|
level = klc.get_fx_strength_level()
|
||||||
|
is_strong = klc.is_strong_fx()
|
||||||
|
|
||||||
|
print(f'分型强度分数: {strength}')
|
||||||
|
print(f'分型强度等级: {level}')
|
||||||
|
print(f'是否强分型: {is_strong}')
|
||||||
|
|
||||||
|
# 测试特征数据集成
|
||||||
|
features = klc.get_feature_data()
|
||||||
|
fx_features = {k: v for k, v in features.items() if 'fx_strength' in k}
|
||||||
|
print('\n分型强度相关特征:')
|
||||||
|
for key, value in fx_features.items():
|
||||||
|
print(f' {key}: {value}')
|
||||||
|
|
||||||
|
print('\n✅ 分型强度检测功能正常工作!')
|
||||||
|
return True
|
||||||
|
|
||||||
|
def test_fx_strength_distribution():
|
||||||
|
"""测试分型强度分布情况"""
|
||||||
|
|
||||||
|
print("=== 分型强度分布分析 ===")
|
||||||
|
|
||||||
|
# 请求API数据
|
||||||
|
url = "http://localhost:8123/api/analyze"
|
||||||
|
params = {
|
||||||
|
'symbol': 'SOL/USDT:USDT',
|
||||||
|
'timeframe': '5m',
|
||||||
|
'timezone': 'Asia/Shanghai'
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url, params=params)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 请求API失败: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 提取分型强度数据
|
||||||
|
fx_strengths = []
|
||||||
|
fx_levels = []
|
||||||
|
top_strengths = []
|
||||||
|
bottom_strengths = []
|
||||||
|
|
||||||
|
for fx in data.get('klc_fx_info', []):
|
||||||
|
strength = fx.get('fx_strength', 0)
|
||||||
|
level = fx.get('fx_strength_level', 'Unknown')
|
||||||
|
is_bottom = fx.get('is_bottom_fx', False)
|
||||||
|
|
||||||
|
fx_strengths.append(strength)
|
||||||
|
fx_levels.append(level)
|
||||||
|
|
||||||
|
if is_bottom:
|
||||||
|
bottom_strengths.append(strength)
|
||||||
|
else:
|
||||||
|
top_strengths.append(strength)
|
||||||
|
|
||||||
|
# 统计分析
|
||||||
|
if fx_strengths:
|
||||||
|
print(f"\n📊 基础统计:")
|
||||||
|
print(f"总分型数量: {len(fx_strengths)}")
|
||||||
|
print(f"平均强度: {np.mean(fx_strengths):.2f}")
|
||||||
|
print(f"强度中位数: {np.median(fx_strengths):.2f}")
|
||||||
|
print(f"强度标准差: {np.std(fx_strengths):.2f}")
|
||||||
|
print(f"最高强度: {np.max(fx_strengths):.2f}")
|
||||||
|
print(f"最低强度: {np.min(fx_strengths):.2f}")
|
||||||
|
|
||||||
|
print(f"\n🔝 顶分型统计:")
|
||||||
|
if top_strengths:
|
||||||
|
print(f"数量: {len(top_strengths)}")
|
||||||
|
print(f"平均强度: {np.mean(top_strengths):.2f}")
|
||||||
|
print(f"最高强度: {np.max(top_strengths):.2f}")
|
||||||
|
|
||||||
|
print(f"\n🔻 底分型统计:")
|
||||||
|
if bottom_strengths:
|
||||||
|
print(f"数量: {len(bottom_strengths)}")
|
||||||
|
print(f"平均强度: {np.mean(bottom_strengths):.2f}")
|
||||||
|
print(f"最高强度: {np.max(bottom_strengths):.2f}")
|
||||||
|
|
||||||
|
# 强度等级分布
|
||||||
|
print(f"\n📈 强度等级分布:")
|
||||||
|
level_counts = Counter(fx_levels)
|
||||||
|
for level, count in level_counts.items():
|
||||||
|
percentage = (count / len(fx_levels)) * 100
|
||||||
|
print(f"{level}: {count} ({percentage:.1f}%)")
|
||||||
|
|
||||||
|
# 强度区间分布
|
||||||
|
print(f"\n📊 强度区间分布:")
|
||||||
|
ranges = [
|
||||||
|
(0, 20, "极弱 (0-20)"),
|
||||||
|
(20, 40, "弱 (20-40)"),
|
||||||
|
(40, 60, "中等 (40-60)"),
|
||||||
|
(60, 80, "强 (60-80)"),
|
||||||
|
(80, 100, "极强 (80-100)")
|
||||||
|
]
|
||||||
|
|
||||||
|
for min_val, max_val, label in ranges:
|
||||||
|
count = sum(1 for s in fx_strengths if min_val <= s < max_val)
|
||||||
|
percentage = (count / len(fx_strengths)) * 100
|
||||||
|
print(f"{label}: {count} ({percentage:.1f}%)")
|
||||||
|
|
||||||
|
# 找出最强和最弱的分型
|
||||||
|
print(f"\n⭐ 最强分型 (Top 5):")
|
||||||
|
sorted_fx = sorted(data.get('klc_fx_info', []),
|
||||||
|
key=lambda x: x.get('fx_strength', 0),
|
||||||
|
reverse=True)[:5]
|
||||||
|
|
||||||
|
for i, fx in enumerate(sorted_fx, 1):
|
||||||
|
fx_type = "底分型" if fx.get('is_bottom_fx', False) else "顶分型"
|
||||||
|
print(f" {i}. {fx.get('time', 'N/A')} - {fx_type} - 强度: {fx.get('fx_strength', 0):.2f} - 等级: {fx.get('fx_strength_level', 'N/A')}")
|
||||||
|
|
||||||
|
print(f"\n💔 最弱分型 (Bottom 5):")
|
||||||
|
weakest_fx = sorted(data.get('klc_fx_info', []),
|
||||||
|
key=lambda x: x.get('fx_strength', 0))[:5]
|
||||||
|
|
||||||
|
for i, fx in enumerate(weakest_fx, 1):
|
||||||
|
fx_type = "底分型" if fx.get('is_bottom_fx', False) else "顶分型"
|
||||||
|
print(f" {i}. {fx.get('time', 'N/A')} - {fx_type} - 强度: {fx.get('fx_strength', 0):.2f} - 等级: {fx.get('fx_strength_level', 'N/A')}")
|
||||||
|
|
||||||
|
# 生成直方图
|
||||||
|
try:
|
||||||
|
plt.figure(figsize=(12, 8))
|
||||||
|
|
||||||
|
# 主强度分布图
|
||||||
|
plt.subplot(2, 2, 1)
|
||||||
|
plt.hist(fx_strengths, bins=20, alpha=0.7, color='blue', edgecolor='black')
|
||||||
|
plt.title('分型强度分布')
|
||||||
|
plt.xlabel('强度分数')
|
||||||
|
plt.ylabel('频次')
|
||||||
|
plt.axvline(np.mean(fx_strengths), color='red', linestyle='--', label=f'平均值: {np.mean(fx_strengths):.2f}')
|
||||||
|
plt.legend()
|
||||||
|
|
||||||
|
# 顶分型 vs 底分型对比
|
||||||
|
plt.subplot(2, 2, 2)
|
||||||
|
if top_strengths and bottom_strengths:
|
||||||
|
plt.hist([top_strengths, bottom_strengths], bins=15, alpha=0.7,
|
||||||
|
label=['顶分型', '底分型'], color=['red', 'green'])
|
||||||
|
plt.title('顶分型 vs 底分型强度对比')
|
||||||
|
plt.xlabel('强度分数')
|
||||||
|
plt.ylabel('频次')
|
||||||
|
plt.legend()
|
||||||
|
|
||||||
|
# 强度等级饼图
|
||||||
|
plt.subplot(2, 2, 3)
|
||||||
|
if level_counts:
|
||||||
|
labels = list(level_counts.keys())
|
||||||
|
sizes = list(level_counts.values())
|
||||||
|
colors = ['red', 'orange', 'yellow', 'lightgreen', 'green'][:len(labels)]
|
||||||
|
plt.pie(sizes, labels=labels, colors=colors, autopct='%1.1f%%')
|
||||||
|
plt.title('强度等级分布')
|
||||||
|
|
||||||
|
# 时间序列图
|
||||||
|
plt.subplot(2, 2, 4)
|
||||||
|
x_vals = range(len(fx_strengths))
|
||||||
|
colors = ['red' if not fx.get('is_bottom_fx', False) else 'green'
|
||||||
|
for fx in data.get('klc_fx_info', [])]
|
||||||
|
plt.scatter(x_vals, fx_strengths, c=colors, alpha=0.6)
|
||||||
|
plt.title('分型强度时间序列 (红=顶分型, 绿=底分型)')
|
||||||
|
plt.xlabel('分型序号')
|
||||||
|
plt.ylabel('强度分数')
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig('user_data/Chan/fx_strength_analysis.png', dpi=300, bbox_inches='tight')
|
||||||
|
print(f"\n📈 图表已保存到: user_data/Chan/fx_strength_analysis.png")
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
print("\n📈 matplotlib 未安装,跳过图表生成")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ 生成图表失败: {e}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("❌ 未找到分型强度数据")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_fx_strength()
|
||||||
|
test_fx_strength_distribution()
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
测试web接口返回的分型强度数据
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# 添加父目录到系统路径以便导入模块
|
||||||
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
def test_web_api():
|
||||||
|
"""测试web API接口返回的分型强度数据"""
|
||||||
|
|
||||||
|
print('=== 测试Web API分型强度数据 ===')
|
||||||
|
|
||||||
|
# 构建请求URL
|
||||||
|
base_url = "http://localhost:8123"
|
||||||
|
endpoint = "/api/analyze"
|
||||||
|
|
||||||
|
params = {
|
||||||
|
'symbol': 'SOL/USDT:USDT',
|
||||||
|
'timeframe': '5m',
|
||||||
|
'timezone': 'Asia/Shanghai'
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"发送请求到: {base_url}{endpoint}")
|
||||||
|
print(f"参数: {params}")
|
||||||
|
|
||||||
|
# 发送请求
|
||||||
|
response = requests.get(f"{base_url}{endpoint}", params=params, timeout=30)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
# 检查是否有分型信息
|
||||||
|
if 'klc_fx_info' in data:
|
||||||
|
fx_info = data['klc_fx_info']
|
||||||
|
print(f"\n找到 {len(fx_info)} 个分型")
|
||||||
|
|
||||||
|
# 显示前3个分型的详细信息
|
||||||
|
for i, fx in enumerate(fx_info[:3]):
|
||||||
|
print(f"\n分型 #{i+1}:")
|
||||||
|
print(f" 时间: {fx.get('time', '无')}")
|
||||||
|
print(f" 价格: {fx.get('price', '无')}")
|
||||||
|
print(f" 分型类型: {fx.get('fx_type', '无')}")
|
||||||
|
print(f" 是否底分型: {fx.get('is_bottom', '无')}")
|
||||||
|
print(f" 强度分数: {fx.get('fx_strength', '缺失!')}")
|
||||||
|
print(f" 强度等级: {fx.get('fx_strength_level', '缺失!')}")
|
||||||
|
print(f" 是否强分型: {fx.get('is_strong_fx', '缺失!')}")
|
||||||
|
|
||||||
|
# 检查强度数据是否完整
|
||||||
|
missing_strength_count = 0
|
||||||
|
for fx in fx_info:
|
||||||
|
if 'fx_strength' not in fx or 'fx_strength_level' not in fx or 'is_strong_fx' not in fx:
|
||||||
|
missing_strength_count += 1
|
||||||
|
|
||||||
|
if missing_strength_count == 0:
|
||||||
|
print(f"\n✅ 所有 {len(fx_info)} 个分型都包含完整的强度数据")
|
||||||
|
else:
|
||||||
|
print(f"\n❌ 有 {missing_strength_count} 个分型缺少强度数据")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("\n❌ 响应中未找到分型信息 (klc_fx_info)")
|
||||||
|
|
||||||
|
# 检查小周期分型信息
|
||||||
|
if 'element_klc_fx_info' in data:
|
||||||
|
element_fx_info = data['element_klc_fx_info']
|
||||||
|
print(f"\n找到 {len(element_fx_info)} 个小周期分型")
|
||||||
|
|
||||||
|
# 检查小周期强度数据
|
||||||
|
missing_element_strength_count = 0
|
||||||
|
for fx in element_fx_info:
|
||||||
|
if 'fx_strength' not in fx or 'fx_strength_level' not in fx or 'is_strong_fx' not in fx:
|
||||||
|
missing_element_strength_count += 1
|
||||||
|
|
||||||
|
if missing_element_strength_count == 0:
|
||||||
|
print(f"✅ 所有 {len(element_fx_info)} 个小周期分型都包含完整的强度数据")
|
||||||
|
else:
|
||||||
|
print(f"❌ 有 {missing_element_strength_count} 个小周期分型缺少强度数据")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print(f"❌ 请求失败,状态码: {response.status_code}")
|
||||||
|
print(f"响应内容: {response.text}")
|
||||||
|
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print("❌ 无法连接到服务器,请确保web服务正在运行 (python web/app.py)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ 测试过程中出错: {e}")
|
||||||
|
|
||||||
|
def print_usage():
|
||||||
|
"""打印使用说明"""
|
||||||
|
print("\n=== 使用说明 ===")
|
||||||
|
print("1. 确保web服务正在运行:")
|
||||||
|
print(" cd user_data/Chan/web")
|
||||||
|
print(" python app.py")
|
||||||
|
print("\n2. 然后运行此测试脚本:")
|
||||||
|
print(" python test_web_data.py")
|
||||||
|
print("\n3. 检查控制台输出,确认分型强度数据是否正确返回")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_web_api()
|
||||||
|
print_usage()
|
||||||
+19
-5
@@ -220,11 +220,19 @@ def analyze_chan(df):
|
|||||||
klc_fx_info = []
|
klc_fx_info = []
|
||||||
for klc in klc_list:
|
for klc in klc_list:
|
||||||
if hasattr(klc, 'klc_fx_type') and klc.klc_fx_type != Chan_KLC_FX.UNKNOWN:
|
if hasattr(klc, 'klc_fx_type') and klc.klc_fx_type != Chan_KLC_FX.UNKNOWN:
|
||||||
|
# 计算分型强度
|
||||||
|
fx_strength = klc.calculate_fx_strength()
|
||||||
|
fx_strength_level = klc.get_fx_strength_level()
|
||||||
|
is_strong_fx = klc.is_strong_fx()
|
||||||
|
|
||||||
klc_fx_info.append({
|
klc_fx_info.append({
|
||||||
'time': klc.end_time,
|
'time': klc.end_time,
|
||||||
'price': klc.low if klc.fx == Chan_FX_TYPE.BOTTOM else klc.high,
|
'price': klc.low if klc.fx == Chan_FX_TYPE.BOTTOM else klc.high,
|
||||||
'fx_type': str(klc.klc_fx_type).replace("Chan_KLC_FX.", ""),
|
'fx_type': str(klc.klc_fx_type).replace("Chan_KLC_FX.", ""),
|
||||||
'is_bottom': klc.fx == Chan_FX_TYPE.BOTTOM
|
'is_bottom': klc.fx == Chan_FX_TYPE.BOTTOM,
|
||||||
|
'fx_strength': fx_strength, # 分型强度分数 (0-100)
|
||||||
|
'fx_strength_level': fx_strength_level, # 分型强度等级 (极强/强/中等/弱/极弱)
|
||||||
|
'is_strong_fx': is_strong_fx # 是否为强分型
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -479,9 +487,12 @@ def analyze():
|
|||||||
# 添加K线分型信息
|
# 添加K线分型信息
|
||||||
'klc_fx_info': [{
|
'klc_fx_info': [{
|
||||||
'time': format_time_safely(point['time'], client_tz),
|
'time': format_time_safely(point['time'], client_tz),
|
||||||
'price': point['price'],
|
'price': float(point['price']),
|
||||||
'fx_type': point['fx_type'],
|
'fx_type': point['fx_type'],
|
||||||
'is_bottom': point['is_bottom']
|
'is_bottom': bool(point['is_bottom']),
|
||||||
|
'fx_strength': float(point['fx_strength']), # 分型强度分数
|
||||||
|
'fx_strength_level': str(point['fx_strength_level']), # 分型强度等级
|
||||||
|
'is_strong_fx': bool(point['is_strong_fx']) # 是否为强分型
|
||||||
} for point in analysis_result['klc_fx_info']]
|
} for point in analysis_result['klc_fx_info']]
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
@@ -546,9 +557,12 @@ def analyze():
|
|||||||
# 添加小周期分型信息
|
# 添加小周期分型信息
|
||||||
result['element_klc_fx_info'] = [{
|
result['element_klc_fx_info'] = [{
|
||||||
'time': format_time_safely(point['time'], client_tz),
|
'time': format_time_safely(point['time'], client_tz),
|
||||||
'price': point['price'],
|
'price': float(point['price']),
|
||||||
'fx_type': point['fx_type'],
|
'fx_type': point['fx_type'],
|
||||||
'is_bottom': point['is_bottom']
|
'is_bottom': bool(point['is_bottom']),
|
||||||
|
'fx_strength': float(point['fx_strength']), # 分型强度分数
|
||||||
|
'fx_strength_level': str(point['fx_strength_level']), # 分型强度等级
|
||||||
|
'is_strong_fx': bool(point['is_strong_fx']) # 是否为强分型
|
||||||
} for point in element_analysis['klc_fx_info']]
|
} for point in element_analysis['klc_fx_info']]
|
||||||
|
|
||||||
print(f"小周期分析完成: {element_timeframe}, 笔数量: {len(result['element_bi_list'])}, {'仅元素数据' if elements_only else '包含主周期数据'}")
|
print(f"小周期分析完成: {element_timeframe}, 笔数量: {len(result['element_bi_list'])}, {'仅元素数据' if elements_only else '包含主周期数据'}")
|
||||||
|
|||||||
+290
-125
@@ -2612,9 +2612,44 @@
|
|||||||
const timeStr = param.time;
|
const timeStr = param.time;
|
||||||
const markers = [...buyMarkers, ...sellMarkers].filter(m => m.time === timeStr);
|
const markers = [...buyMarkers, ...sellMarkers].filter(m => m.time === timeStr);
|
||||||
|
|
||||||
if (markers.length > 0) {
|
// 同时检查分型标记
|
||||||
// 有买卖点标记,显示自定义提示
|
const fxMarkers = (window.fxMarkers || []).filter(m => m.time === timeStr);
|
||||||
const tooltips = markers.map(m => m.tooltip).join('<br>');
|
const allMarkers = [...markers, ...fxMarkers];
|
||||||
|
|
||||||
|
// 显示时区调试信息
|
||||||
|
if (window.debugMode) {
|
||||||
|
const timezone = $('#timezone').val();
|
||||||
|
const formattedTime = formatTimeWithTimezone(timeStr * 1000, timezone);
|
||||||
|
|
||||||
|
// 获取当前价格 - 通过param.seriesPrices获取
|
||||||
|
let priceInfo = '';
|
||||||
|
if (param.seriesPrices && param.seriesPrices.size > 0) {
|
||||||
|
// 尝试从蜡烛图系列获取价格
|
||||||
|
if (tvWidget.series.candleSeries && param.seriesPrices.get(tvWidget.series.candleSeries)) {
|
||||||
|
const price = param.seriesPrices.get(tvWidget.series.candleSeries);
|
||||||
|
priceInfo = `价格: ${price.toFixed(2)}`;
|
||||||
|
}
|
||||||
|
// 如果没有蜡烛图系列价格,尝试从线图系列获取
|
||||||
|
else if (tvWidget.series.lineSeries && param.seriesPrices.get(tvWidget.series.lineSeries)) {
|
||||||
|
const price = param.seriesPrices.get(tvWidget.series.lineSeries);
|
||||||
|
priceInfo = `价格: ${price.toFixed(2)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仅记录最简短的调试信息
|
||||||
|
console.debug(`十字线: ${timeStr} -> ${formattedTime} (${timezone})`);
|
||||||
|
|
||||||
|
// 显示自定义时区工具提示,包含价格信息
|
||||||
|
crosshairTooltip.innerHTML = `<div style="font-weight:bold">时间: ${formattedTime}</div>` +
|
||||||
|
(priceInfo ? `<div>${priceInfo}</div>` : '');
|
||||||
|
crosshairTooltip.style.display = 'block';
|
||||||
|
crosshairTooltip.style.left = (param.point.x + 15) + 'px';
|
||||||
|
crosshairTooltip.style.top = (param.point.y - 30) + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allMarkers.length > 0) {
|
||||||
|
// 有买卖点或分型标记,显示自定义提示
|
||||||
|
const tooltips = allMarkers.map(m => m.tooltip).join('<br><hr style="margin: 5px 0;">');
|
||||||
tooltipElement.innerHTML = tooltips;
|
tooltipElement.innerHTML = tooltips;
|
||||||
tooltipElement.style.display = 'block';
|
tooltipElement.style.display = 'block';
|
||||||
tooltipElement.style.left = (param.point.x + 15) + 'px';
|
tooltipElement.style.left = (param.point.x + 15) + 'px';
|
||||||
@@ -2626,28 +2661,53 @@
|
|||||||
} else {
|
} else {
|
||||||
// 隐藏提示
|
// 隐藏提示
|
||||||
tooltipElement.style.display = 'none';
|
tooltipElement.style.display = 'none';
|
||||||
|
crosshairTooltip.style.display = 'none';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 处理图表缩放、平移等事件,隐藏提示
|
// 处理图表缩放、平移等事件,隐藏提示
|
||||||
mainChart.timeScale().subscribeVisibleTimeRangeChange(() => {
|
mainChart.timeScale().subscribeVisibleTimeRangeChange(() => {
|
||||||
tooltipElement.style.display = 'none';
|
tooltipElement.style.display = 'none';
|
||||||
|
crosshairTooltip.style.display = 'none';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('绘制买卖点 - 已禁用');
|
console.log('绘制买卖点 - 已禁用');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示分型类型标签
|
// 绘制分型类型标签
|
||||||
|
console.log('=== 开始检查分型显示条件 ===');
|
||||||
|
console.log('showKlcFxType勾选状态:', $('#showKlcFxType').is(':checked'));
|
||||||
|
console.log('currentData.klc_fx_info存在:', !!currentData.klc_fx_info);
|
||||||
|
console.log('currentData.klc_fx_info长度:', currentData.klc_fx_info ? currentData.klc_fx_info.length : 'undefined');
|
||||||
|
if (currentData.klc_fx_info && currentData.klc_fx_info.length > 0) {
|
||||||
|
console.log('前3个分型数据样本:', currentData.klc_fx_info.slice(0, 3));
|
||||||
|
}
|
||||||
|
|
||||||
if ($('#showKlcFxType').is(':checked') && currentData.klc_fx_info && currentData.klc_fx_info.length > 0) {
|
if ($('#showKlcFxType').is(':checked') && currentData.klc_fx_info && currentData.klc_fx_info.length > 0) {
|
||||||
console.log(`绘制K线分型类型标签,共${currentData.klc_fx_info.length}条`);
|
console.log(`绘制K线分型类型标签,共${currentData.klc_fx_info.length}条`);
|
||||||
|
|
||||||
|
// 收集所有分型标记
|
||||||
|
const allFxMarkers = [];
|
||||||
|
// 存储分型标记,用于tooltip功能
|
||||||
|
const fxMarkers = [];
|
||||||
|
|
||||||
currentData.klc_fx_info.forEach(function(fx) {
|
currentData.klc_fx_info.forEach(function(fx) {
|
||||||
try {
|
try {
|
||||||
// 直接使用UTC时间戳(秒)
|
// 直接使用UTC时间戳(秒)
|
||||||
const timestamp = Math.floor(new Date(fx.time).getTime() / 1000);
|
const timestamp = Math.floor(new Date(fx.time).getTime() / 1000);
|
||||||
const price = parseFloat(fx.price);
|
const price = parseFloat(fx.price);
|
||||||
|
|
||||||
|
// 添加时间和价格调试信息
|
||||||
|
console.log('处理分型:', {
|
||||||
|
原始时间: fx.time,
|
||||||
|
转换时间戳: timestamp,
|
||||||
|
原始价格: fx.price,
|
||||||
|
转换价格: price,
|
||||||
|
时间有效: !isNaN(timestamp),
|
||||||
|
价格有效: !isNaN(price)
|
||||||
|
});
|
||||||
|
|
||||||
if (isNaN(timestamp) || isNaN(price)) {
|
if (isNaN(timestamp) || isNaN(price)) {
|
||||||
console.error('分型类型时间或价格转换错误:', fx.time, fx.price);
|
console.error('分型类型时间或价格转换错误:', fx.time, fx.price);
|
||||||
return;
|
return;
|
||||||
@@ -2656,47 +2716,92 @@
|
|||||||
// 确定颜色和位置
|
// 确定颜色和位置
|
||||||
const color = fx.is_bottom ? '#28a745' : '#dc3545'; // 底分型绿色,顶分型红色
|
const color = fx.is_bottom ? '#28a745' : '#dc3545'; // 底分型绿色,顶分型红色
|
||||||
|
|
||||||
// 创建标记系列
|
// 根据强度等级调整颜色强度
|
||||||
const markerSeries = mainChart.addLineSeries({
|
let strengthColor = color;
|
||||||
lastValueVisible: false,
|
if (fx.is_strong_fx) {
|
||||||
priceLineVisible: false,
|
// 强分型使用更亮的颜色
|
||||||
|
strengthColor = fx.is_bottom ? '#00ff00' : '#ff0000';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建显示文本,包含分型类型和强度信息
|
||||||
|
// 添加调试信息
|
||||||
|
console.log('分型数据:', {
|
||||||
|
fx_type: fx.fx_type,
|
||||||
|
fx_strength: fx.fx_strength,
|
||||||
|
fx_strength_level: fx.fx_strength_level,
|
||||||
|
is_strong_fx: fx.is_strong_fx
|
||||||
});
|
});
|
||||||
|
|
||||||
// 设置文本标记
|
const displayText = `${fx.fx_strength_level} ${fx.fx_strength.toFixed(1)}`;
|
||||||
markerSeries.setMarkers([
|
console.log('显示文本:', displayText);
|
||||||
{
|
|
||||||
time: timestamp,
|
|
||||||
position: fx.is_bottom ? 'belowBar' : 'aboveBar',
|
|
||||||
color: color,
|
|
||||||
shape: 'circle',
|
|
||||||
text: fx.fx_type,
|
|
||||||
size: 1
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 可选:添加更加明显的文本标签
|
// 添加标记配置调试
|
||||||
const textSeries = mainChart.addLineSeries({
|
const markerConfig = {
|
||||||
lastValueVisible: false,
|
|
||||||
priceLineVisible: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
textSeries.setData([{
|
|
||||||
time: timestamp,
|
time: timestamp,
|
||||||
value: price + (fx.is_bottom ? -0.0005 * price : 0.0005 * price) // 小偏移,避免遮挡
|
position: fx.is_bottom ? 'belowBar' : 'aboveBar',
|
||||||
}]);
|
color: strengthColor,
|
||||||
|
shape: fx.is_strong_fx ? 'square' : 'circle',
|
||||||
|
text: displayText,
|
||||||
|
size: fx.is_strong_fx ? 2 : 1
|
||||||
|
};
|
||||||
|
console.log('标记配置:', markerConfig);
|
||||||
|
|
||||||
|
// 添加到标记数组
|
||||||
|
allFxMarkers.push(markerConfig);
|
||||||
|
|
||||||
|
// 创建分型标记对象,包含tooltip信息
|
||||||
|
const fxMarker = {
|
||||||
|
time: timestamp,
|
||||||
|
tooltip: `<div style="color: ${strengthColor}; font-weight: bold;">
|
||||||
|
${fx.is_bottom ? '底分型' : '顶分型'}: ${fx.fx_type}<br>
|
||||||
|
强度分数: ${fx.fx_strength}分<br>
|
||||||
|
强度等级: ${fx.fx_strength_level}<br>
|
||||||
|
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
|
||||||
|
价格: ${price.toFixed(4)}<br>
|
||||||
|
时间: ${fx.time}
|
||||||
|
</div>`
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加到分型标记数组
|
||||||
|
fxMarkers.push(fxMarker);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('绘制分型类型标签出错:', e);
|
console.error('绘制分型类型标签出错:', e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 一次性设置所有分型标记到主数据系列
|
||||||
|
if (allFxMarkers.length > 0) {
|
||||||
|
console.log('一次性设置', allFxMarkers.length, '个分型标记');
|
||||||
|
|
||||||
|
// 暂存主周期分型标记,等待与小周期合并
|
||||||
|
window.mainFxMarkers = allFxMarkers;
|
||||||
|
} else {
|
||||||
|
window.mainFxMarkers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将分型标记添加到全局markers中以支持tooltip功能
|
||||||
|
if (window.fxMarkers) {
|
||||||
|
window.fxMarkers = [...window.fxMarkers, ...fxMarkers];
|
||||||
|
} else {
|
||||||
|
window.fxMarkers = fxMarkers;
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.log('绘制分型类型标签 - 已禁用或无数据');
|
console.log('绘制分型类型标签 - 已禁用或无数据');
|
||||||
|
// 清空分型标记
|
||||||
|
window.fxMarkers = [];
|
||||||
|
window.mainFxMarkers = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绘制小周期分型标记
|
// 绘制小周期分型标记
|
||||||
if ($('#showElementKlcFxType').is(':checked') && currentData.element_klc_fx_info && currentData.element_klc_fx_info.length > 0) {
|
if ($('#showElementKlcFxType').is(':checked') && currentData.element_klc_fx_info && currentData.element_klc_fx_info.length > 0) {
|
||||||
console.log(`绘制小周期分型标记,共${currentData.element_klc_fx_info.length}条`);
|
console.log(`绘制小周期分型标记,共${currentData.element_klc_fx_info.length}条`);
|
||||||
|
|
||||||
|
// 收集所有小周期分型标记
|
||||||
|
const allElementFxMarkers = [];
|
||||||
|
const elementFxMarkers = []; // 用于tooltip支持
|
||||||
|
|
||||||
currentData.element_klc_fx_info.forEach(function(fx) {
|
currentData.element_klc_fx_info.forEach(function(fx) {
|
||||||
try {
|
try {
|
||||||
// 直接使用UTC时间戳(秒)
|
// 直接使用UTC时间戳(秒)
|
||||||
@@ -2708,49 +2813,92 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 小周期分型使用红色标记,不同于主周期分型
|
// 小周期分型使用不同的颜色和样式,与主周期区分
|
||||||
const redColor = '#FF0000'; // 红色
|
let strengthColor = fx.is_bottom ? '#FF6B6B' : '#4ECDC4'; // 底分型用珊瑚红,顶分型用薄荷绿
|
||||||
|
if (fx.is_strong_fx) {
|
||||||
|
// 强分型使用更亮的颜色
|
||||||
|
strengthColor = fx.is_bottom ? '#FF0000' : '#00CED1';
|
||||||
|
}
|
||||||
|
|
||||||
// 创建标记系列
|
// 构建小周期分型显示文本
|
||||||
const markerSeries = mainChart.addLineSeries({
|
const displayText = `${fx.fx_strength_level} ${fx.fx_strength.toFixed(1)}`;
|
||||||
lastValueVisible: false,
|
|
||||||
priceLineVisible: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// 设置文本标记
|
// 小周期分型标记配置 - 根据分型类型使用正确的箭头形状
|
||||||
markerSeries.setMarkers([
|
const markerConfig = {
|
||||||
{
|
|
||||||
time: timestamp,
|
|
||||||
position: fx.is_bottom ? 'belowBar' : 'aboveBar',
|
|
||||||
color: redColor,
|
|
||||||
shape: 'arrowUp', // 使用箭头形状,与主周期分型区分
|
|
||||||
text: fx.is_bottom ? '↓' : '↑', // 显示箭头
|
|
||||||
size: 1
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 为小周期分型添加明显的箭头标记
|
|
||||||
const arrowSeries = mainChart.addLineSeries({
|
|
||||||
lastValueVisible: false,
|
|
||||||
priceLineVisible: false,
|
|
||||||
lineWidth: 1,
|
|
||||||
color: redColor
|
|
||||||
});
|
|
||||||
|
|
||||||
// 计算标记位置,底分型在价格下方,顶分型在价格上方
|
|
||||||
const offset = fx.is_bottom ? -0.001 * price : 0.001 * price;
|
|
||||||
|
|
||||||
arrowSeries.setData([{
|
|
||||||
time: timestamp,
|
time: timestamp,
|
||||||
value: price + offset
|
position: fx.is_bottom ? 'belowBar' : 'aboveBar',
|
||||||
}]);
|
color: strengthColor,
|
||||||
|
shape: fx.is_bottom ? 'arrowUp' : 'arrowDown', // 底分型向上箭头,顶分型向下箭头
|
||||||
|
text: displayText,
|
||||||
|
size: fx.is_strong_fx ? 2 : 1
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('小周期分型标记配置:', markerConfig);
|
||||||
|
allElementFxMarkers.push(markerConfig);
|
||||||
|
|
||||||
|
// 创建小周期分型标记对象,包含tooltip信息
|
||||||
|
const elementFxMarker = {
|
||||||
|
time: timestamp,
|
||||||
|
tooltip: `<div style="color: ${strengthColor}; font-weight: bold;">
|
||||||
|
小周期${fx.is_bottom ? '底分型' : '顶分型'}: ${fx.fx_type}<br>
|
||||||
|
强度分数: ${fx.fx_strength}分<br>
|
||||||
|
强度等级: ${fx.fx_strength_level}<br>
|
||||||
|
是否强分型: ${fx.is_strong_fx ? '是' : '否'}<br>
|
||||||
|
价格: ${price.toFixed(4)}<br>
|
||||||
|
时间: ${fx.time}
|
||||||
|
</div>`
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加到小周期分型标记数组
|
||||||
|
elementFxMarkers.push(elementFxMarker);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('绘制小周期分型标记出错:', e);
|
console.error('绘制小周期分型标记出错:', e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 将小周期分型标记添加到全局markers中以支持tooltip功能
|
||||||
|
if (window.fxMarkers) {
|
||||||
|
window.fxMarkers = [...window.fxMarkers, ...elementFxMarkers];
|
||||||
|
} else {
|
||||||
|
window.fxMarkers = elementFxMarkers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并主周期和小周期分型标记,统一设置到K线数据系列
|
||||||
|
const combinedMarkers = [...(window.mainFxMarkers || []), ...allElementFxMarkers];
|
||||||
|
if (combinedMarkers.length > 0) {
|
||||||
|
console.log('合并设置', combinedMarkers.length, '个分型标记(主周期:', (window.mainFxMarkers || []).length, '个,小周期:', allElementFxMarkers.length, '个)');
|
||||||
|
|
||||||
|
// 尝试在K线系列上设置合并后的标记
|
||||||
|
if (showOriginalKline && tvWidget.series.candleSeries) {
|
||||||
|
tvWidget.series.candleSeries.setMarkers(combinedMarkers);
|
||||||
|
console.log('合并标记已设置到蜡烛图系列');
|
||||||
|
} else if (!showOriginalKline && tvWidget.series.lineSeries) {
|
||||||
|
tvWidget.series.lineSeries.setMarkers(combinedMarkers);
|
||||||
|
console.log('合并标记已设置到线图系列');
|
||||||
|
} else {
|
||||||
|
console.log('未找到主数据系列,无法设置标记');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
console.log('绘制小周期分型标记 - 已禁用或无数据');
|
console.log('绘制小周期分型标记 - 已禁用或无数据');
|
||||||
|
|
||||||
|
// 只设置主周期分型标记
|
||||||
|
if (window.mainFxMarkers && window.mainFxMarkers.length > 0) {
|
||||||
|
console.log('仅设置', window.mainFxMarkers.length, '个主周期分型标记');
|
||||||
|
|
||||||
|
// 尝试在K线系列上设置标记
|
||||||
|
if (showOriginalKline && tvWidget.series.candleSeries) {
|
||||||
|
tvWidget.series.candleSeries.setMarkers(window.mainFxMarkers);
|
||||||
|
console.log('主周期标记已设置到蜡烛图系列');
|
||||||
|
} else if (!showOriginalKline && tvWidget.series.lineSeries) {
|
||||||
|
tvWidget.series.lineSeries.setMarkers(window.mainFxMarkers);
|
||||||
|
console.log('主周期标记已设置到线图系列');
|
||||||
|
} else {
|
||||||
|
console.log('未找到主数据系列,无法设置标记');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调整所有图表以适应数据
|
// 调整所有图表以适应数据
|
||||||
@@ -2767,13 +2915,13 @@
|
|||||||
tvWidget.state.isInitialized = true;
|
tvWidget.state.isInitialized = true;
|
||||||
|
|
||||||
// 绑定同步事件
|
// 绑定同步事件
|
||||||
bindSyncEvents();
|
bindSyncEvents(mainChartContainer, volumeChartContainer, macdChartContainer, mainChart, volumeChart, macdChart, showMacd);
|
||||||
|
|
||||||
// 设置图表默认时间范围
|
// 设置图表默认时间范围
|
||||||
setDefaultTimeRange();
|
setDefaultTimeRange();
|
||||||
|
|
||||||
// 添加买卖点提示
|
// 添加买卖点提示
|
||||||
setupTooltip();
|
setupTooltip(mainChart);
|
||||||
|
|
||||||
// 显示买卖点
|
// 显示买卖点
|
||||||
if ($('#showTradePoints').is(':checked')) {
|
if ($('#showTradePoints').is(':checked')) {
|
||||||
@@ -2932,7 +3080,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindSyncEvents() {
|
function bindSyncEvents(mainChartContainer, volumeChartContainer, macdChartContainer, mainChart, volumeChart, macdChart, showMacd) {
|
||||||
|
// 防止同步过程中的无限循环
|
||||||
|
let syncInProgress = false;
|
||||||
|
|
||||||
// 同步图表的时间范围
|
// 同步图表的时间范围
|
||||||
function syncCharts(sourceChart, sourceContainer) {
|
function syncCharts(sourceChart, sourceContainer) {
|
||||||
// 防止无限循环
|
// 防止无限循环
|
||||||
@@ -2989,9 +3140,13 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 添加事件监听
|
// 添加事件监听
|
||||||
addChartSyncEvents(mainChartContainer, mainChart);
|
if (mainChartContainer && mainChart) {
|
||||||
addChartSyncEvents(volumeChartContainer, volumeChart);
|
addChartSyncEvents(mainChartContainer, mainChart);
|
||||||
if (showMacd && macdChart) {
|
}
|
||||||
|
if (volumeChartContainer && volumeChart) {
|
||||||
|
addChartSyncEvents(volumeChartContainer, volumeChart);
|
||||||
|
}
|
||||||
|
if (showMacd && macdChartContainer && macdChart) {
|
||||||
addChartSyncEvents(macdChartContainer, macdChart);
|
addChartSyncEvents(macdChartContainer, macdChart);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3003,19 +3158,23 @@
|
|||||||
// 窗口大小变化时重绘图表
|
// 窗口大小变化时重绘图表
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
// 调整主图大小
|
// 调整主图大小
|
||||||
mainChart.applyOptions({
|
if (mainChart && mainChartContainer) {
|
||||||
width: mainChartContainer.clientWidth,
|
mainChart.applyOptions({
|
||||||
height: mainChartContainer.clientHeight
|
width: mainChartContainer.clientWidth,
|
||||||
});
|
height: mainChartContainer.clientHeight
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 调整成交量图大小
|
// 调整成交量图大小
|
||||||
volumeChart.applyOptions({
|
if (volumeChart && volumeChartContainer) {
|
||||||
width: volumeChartContainer.clientWidth,
|
volumeChart.applyOptions({
|
||||||
height: volumeChartContainer.clientHeight
|
width: volumeChartContainer.clientWidth,
|
||||||
});
|
height: volumeChartContainer.clientHeight
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 调整MACD图大小
|
// 调整MACD图大小
|
||||||
if (showMacd && macdChart) {
|
if (showMacd && macdChart && macdChartContainer) {
|
||||||
macdChart.applyOptions({
|
macdChart.applyOptions({
|
||||||
width: macdChartContainer.clientWidth,
|
width: macdChartContainer.clientWidth,
|
||||||
height: macdChartContainer.clientHeight
|
height: macdChartContainer.clientHeight
|
||||||
@@ -3027,7 +3186,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupTooltip() {
|
function setupTooltip(mainChart, buyMarkers = [], sellMarkers = []) {
|
||||||
// 调试变量
|
// 调试变量
|
||||||
window.debugMode = true;
|
window.debugMode = true;
|
||||||
|
|
||||||
@@ -3051,65 +3210,71 @@
|
|||||||
document.body.appendChild(crosshairTooltip);
|
document.body.appendChild(crosshairTooltip);
|
||||||
|
|
||||||
// 添加鼠标悬停事件显示提示
|
// 添加鼠标悬停事件显示提示
|
||||||
mainChart.subscribeCrosshairMove(param => {
|
if (mainChart) {
|
||||||
if (param.time && param.point) {
|
mainChart.subscribeCrosshairMove(param => {
|
||||||
const timeStr = param.time;
|
if (param.time && param.point) {
|
||||||
const markers = [...buyMarkers, ...sellMarkers].filter(m => m.time === timeStr);
|
const timeStr = param.time;
|
||||||
|
const markers = [...buyMarkers, ...sellMarkers].filter(m => m.time === timeStr);
|
||||||
|
|
||||||
// 显示时区调试信息
|
// 同时检查分型标记
|
||||||
if (window.debugMode) {
|
const fxMarkers = (window.fxMarkers || []).filter(m => m.time === timeStr);
|
||||||
const timezone = $('#timezone').val();
|
const allMarkers = [...markers, ...fxMarkers];
|
||||||
const formattedTime = formatTimeWithTimezone(timeStr * 1000, timezone);
|
|
||||||
|
|
||||||
// 获取当前价格 - 通过param.seriesPrices获取
|
// 显示时区调试信息
|
||||||
let priceInfo = '';
|
if (window.debugMode) {
|
||||||
if (param.seriesPrices && param.seriesPrices.size > 0) {
|
const timezone = $('#timezone').val();
|
||||||
// 尝试从蜡烛图系列获取价格
|
const formattedTime = formatTimeWithTimezone(timeStr * 1000, timezone);
|
||||||
if (tvWidget.series.candleSeries && param.seriesPrices.get(tvWidget.series.candleSeries)) {
|
|
||||||
const price = param.seriesPrices.get(tvWidget.series.candleSeries);
|
// 获取当前价格 - 通过param.seriesPrices获取
|
||||||
priceInfo = `价格: ${price.toFixed(2)}`;
|
let priceInfo = '';
|
||||||
}
|
if (param.seriesPrices && param.seriesPrices.size > 0) {
|
||||||
// 如果没有蜡烛图系列价格,尝试从线图系列获取
|
// 尝试从蜡烛图系列获取价格
|
||||||
else if (tvWidget.series.lineSeries && param.seriesPrices.get(tvWidget.series.lineSeries)) {
|
if (tvWidget.series.candleSeries && param.seriesPrices.get(tvWidget.series.candleSeries)) {
|
||||||
const price = param.seriesPrices.get(tvWidget.series.lineSeries);
|
const price = param.seriesPrices.get(tvWidget.series.candleSeries);
|
||||||
priceInfo = `价格: ${price.toFixed(2)}`;
|
priceInfo = `价格: ${price.toFixed(2)}`;
|
||||||
|
}
|
||||||
|
// 如果没有蜡烛图系列价格,尝试从线图系列获取
|
||||||
|
else if (tvWidget.series.lineSeries && param.seriesPrices.get(tvWidget.series.lineSeries)) {
|
||||||
|
const price = param.seriesPrices.get(tvWidget.series.lineSeries);
|
||||||
|
priceInfo = `价格: ${price.toFixed(2)}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 仅记录最简短的调试信息
|
||||||
|
console.debug(`十字线: ${timeStr} -> ${formattedTime} (${timezone})`);
|
||||||
|
|
||||||
|
// 显示自定义时区工具提示,包含价格信息
|
||||||
|
crosshairTooltip.innerHTML = `<div style="font-weight:bold">时间: ${formattedTime}</div>` +
|
||||||
|
(priceInfo ? `<div>${priceInfo}</div>` : '');
|
||||||
|
crosshairTooltip.style.display = 'block';
|
||||||
|
crosshairTooltip.style.left = (param.point.x + 15) + 'px';
|
||||||
|
crosshairTooltip.style.top = (param.point.y - 30) + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 仅记录最简短的调试信息
|
if (allMarkers.length > 0) {
|
||||||
console.debug(`十字线: ${timeStr} -> ${formattedTime} (${timezone})`);
|
// 有买卖点或分型标记,显示自定义提示
|
||||||
|
const tooltips = allMarkers.map(m => m.tooltip).join('<br><hr style="margin: 5px 0;">');
|
||||||
// 显示自定义时区工具提示,包含价格信息
|
tooltipElement.innerHTML = tooltips;
|
||||||
crosshairTooltip.innerHTML = `<div style="font-weight:bold">时间: ${formattedTime}</div>` +
|
tooltipElement.style.display = 'block';
|
||||||
(priceInfo ? `<div>${priceInfo}</div>` : '');
|
tooltipElement.style.left = (param.point.x + 15) + 'px';
|
||||||
crosshairTooltip.style.display = 'block';
|
tooltipElement.style.top = (param.point.y + 15) + 'px';
|
||||||
crosshairTooltip.style.left = (param.point.x + 15) + 'px';
|
} else {
|
||||||
crosshairTooltip.style.top = (param.point.y - 30) + 'px';
|
// 隐藏提示
|
||||||
}
|
tooltipElement.style.display = 'none';
|
||||||
|
}
|
||||||
if (markers.length > 0) {
|
|
||||||
// 有买卖点标记,显示自定义提示
|
|
||||||
const tooltips = markers.map(m => m.tooltip).join('<br>');
|
|
||||||
tooltipElement.innerHTML = tooltips;
|
|
||||||
tooltipElement.style.display = 'block';
|
|
||||||
tooltipElement.style.left = (param.point.x + 15) + 'px';
|
|
||||||
tooltipElement.style.top = (param.point.y + 15) + 'px';
|
|
||||||
} else {
|
} else {
|
||||||
// 隐藏提示
|
// 隐藏提示
|
||||||
tooltipElement.style.display = 'none';
|
tooltipElement.style.display = 'none';
|
||||||
|
crosshairTooltip.style.display = 'none';
|
||||||
}
|
}
|
||||||
} else {
|
});
|
||||||
// 隐藏提示
|
|
||||||
|
// 处理图表缩放、平移等事件,隐藏提示
|
||||||
|
mainChart.timeScale().subscribeVisibleTimeRangeChange(() => {
|
||||||
tooltipElement.style.display = 'none';
|
tooltipElement.style.display = 'none';
|
||||||
crosshairTooltip.style.display = 'none';
|
crosshairTooltip.style.display = 'none';
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
// 处理图表缩放、平移等事件,隐藏提示
|
|
||||||
mainChart.timeScale().subscribeVisibleTimeRangeChange(() => {
|
|
||||||
tooltipElement.style.display = 'none';
|
|
||||||
crosshairTooltip.style.display = 'none';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助函数:使用指定时区格式化时间戳
|
// 辅助函数:使用指定时区格式化时间戳
|
||||||
|
|||||||
Reference in New Issue
Block a user