修改了很多,明天继续
This commit is contained in:
+5
-1
@@ -61,7 +61,11 @@ class Chan_FX_TYPE(Enum):
|
||||
BB = auto()
|
||||
PTOP = auto()
|
||||
PBOTTOM = auto()
|
||||
|
||||
class Chan_PRICE_TREND(Enum):
|
||||
UP = auto()
|
||||
DOWN = auto()
|
||||
FLAT = auto()
|
||||
UNKNOWN = auto()
|
||||
class Chan_KLC_FX(Enum):
|
||||
TOP1 = auto()
|
||||
TOP2 = auto()
|
||||
|
||||
+7
-1554
File diff suppressed because it is too large
Load Diff
+5
-547
@@ -1,4 +1,4 @@
|
||||
from ChanEnum import Chan_FX_TYPE, Chan_KLU_TYPE, Chan_K_DIR, Chan_MACD_STATE, Chan_MACDHIST_STATE
|
||||
from ChanEnum import Chan_FX_TYPE, Chan_KLU_TYPE, Chan_K_DIR, Chan_MACD_STATE, Chan_MACDHIST_STATE, Chan_PRICE_TREND
|
||||
class ChanKLU:
|
||||
def __init__(self, time, open, high, low, close, volume):
|
||||
# _time, _close, _open, _high, _low, _extra_info={}
|
||||
@@ -42,7 +42,6 @@ class ChanKLU:
|
||||
self.fx_strength = 0 # 分型强度:0-100
|
||||
self.fx_confirmed = False # 分型是否确认
|
||||
self.klu_type = None
|
||||
self.cal_klu_min_max()
|
||||
self.range = self.high - self.low
|
||||
self.body = abs(self.close - self.open)
|
||||
self.upper_shadow = self.high - max(self.close, self.open)
|
||||
@@ -51,7 +50,6 @@ class ChanKLU:
|
||||
self.upper_shadow_ratio = self.upper_shadow / self.body
|
||||
self.lower_shadow_ratio = self.lower_shadow / self.body
|
||||
self.candle_dir = Chan_K_DIR.CROSS if self.close == self.open else Chan_K_DIR.BULL if self.close > self.open else Chan_K_DIR.BEAR
|
||||
self.strength = 0 if self.candle_dir == Chan_K_DIR.CROSS else self.cal_klu_strength()
|
||||
|
||||
self.continue_div = 0
|
||||
self.separate_div = 0
|
||||
@@ -64,6 +62,7 @@ class ChanKLU:
|
||||
self.hist_state = Chan_MACDHIST_STATE.UNKNOWN
|
||||
self.macd_state = Chan_MACD_STATE.UNKNOWN
|
||||
self.macd_hist_gap = 0
|
||||
self.trend = Chan_PRICE_TREND.UNKNOWN
|
||||
# === 归零轴细化与模式/背离 ===
|
||||
self.zero_axis = False # 是否归零轴(穿越或接近)
|
||||
self.zero_axis_state = "none" # {none,crossing,near}
|
||||
@@ -79,49 +78,10 @@ class ChanKLU:
|
||||
#print(self.open, self.close, self.high, self.low, self.candle_dir, self.strength)
|
||||
def set_macd_state(self, state):
|
||||
self.macd_state = state
|
||||
def cal_klu_strength(self):
|
||||
strength = 0
|
||||
if range != 0:
|
||||
if self.candle_dir == Chan_K_DIR.BULL and (self.upper_shadow + self.lower_shadow) != 0:
|
||||
strength += self.body / self.range
|
||||
strength += self.body / (self.upper_shadow + self.lower_shadow)
|
||||
elif self.candle_dir == Chan_K_DIR.BEAR and (self.upper_shadow + self.lower_shadow) != 0:
|
||||
strength -=self.body / self.range
|
||||
strength -= self.body / (self.upper_shadow + self.lower_shadow)
|
||||
#print(self.time, self.body, self.range, self.upper_shadow, self.lower_shadow, self.candle_dir, strength)
|
||||
return strength
|
||||
return strength
|
||||
def cal_klu_min_max(self):
|
||||
"""
|
||||
计算K线类型:大阳线、大阴线、小阳线、小阴线
|
||||
"""
|
||||
if self.open <= 0: # 避免除零错误
|
||||
self.kline_type = None
|
||||
return
|
||||
|
||||
# 计算涨跌幅
|
||||
price_change_ratio = (self.close - self.open) / self.open
|
||||
strength = 0
|
||||
# 判断K线类型
|
||||
if price_change_ratio > 0.005: # 涨幅超过2%
|
||||
self.kline_type = Chan_KLU_TYPE.BigBull
|
||||
strength += abs(price_change_ratio)
|
||||
elif price_change_ratio > 0: # 涨幅0-2%
|
||||
self.kline_type = Chan_KLU_TYPE.SmallBull
|
||||
strength += abs(price_change_ratio)
|
||||
elif price_change_ratio < -0.005: # 跌幅超过2%
|
||||
self.kline_type = Chan_KLU_TYPE.BigBear
|
||||
strength += abs(price_change_ratio)
|
||||
elif price_change_ratio < 0: # 跌幅0-2%
|
||||
self.kline_type = Chan_KLU_TYPE.SmallBear
|
||||
strength += abs(price_change_ratio)
|
||||
else: # 开盘价等于收盘价
|
||||
self.kline_type = Chan_KLU_TYPE.Cross
|
||||
strength += abs(price_change_ratio)
|
||||
return strength
|
||||
def set_trend(self, trend):
|
||||
self.trend = trend
|
||||
def set_next(self, next):
|
||||
self.next = next
|
||||
self.update_realtime_analysis()
|
||||
#if self.fx_type != Chan_FX_TYPE.UNKNOWN and self.fx_strength > 1:
|
||||
#print(self.index, self.time, self.fx_type, self.fx_confirmed, self.fx_strength)
|
||||
def set_pre(self, pre):
|
||||
@@ -139,471 +99,6 @@ class ChanKLU:
|
||||
def set_unittf(self, unittf):
|
||||
"""设置UnitTF关联"""
|
||||
self.unittf = unittf
|
||||
def detect_realtime_fx(self):
|
||||
"""
|
||||
实时检测K线分型(不等待KLC确认)
|
||||
基于原始K线的即时分型识别
|
||||
"""
|
||||
if not self.pre or not self.next:
|
||||
self.fx_type = Chan_FX_TYPE.UNKNOWN
|
||||
return False
|
||||
|
||||
# 顶分型检测
|
||||
if (self.high > self.pre.high and
|
||||
self.high > self.next.high):
|
||||
self.fx_type = Chan_FX_TYPE.TOP
|
||||
self.fx_confirmed = True
|
||||
return True
|
||||
|
||||
# 底分型检测
|
||||
elif (self.low < self.pre.low and
|
||||
self.low < self.next.low):
|
||||
self.fx_type = Chan_FX_TYPE.BOTTOM
|
||||
self.fx_confirmed = True
|
||||
return True
|
||||
|
||||
self.fx_type = Chan_FX_TYPE.UNKNOWN
|
||||
self.fx_confirmed = False
|
||||
return False
|
||||
def cal_fx(self):
|
||||
"""
|
||||
根据缠论经典规则计算分型强弱
|
||||
返回分型强度:3=极强,2=强,1=中等,0=弱,-1=极弱
|
||||
"""
|
||||
if self.fx_type == Chan_FX_TYPE.UNKNOWN or not self.pre or not self.next:
|
||||
return 0
|
||||
|
||||
if self.fx_type == Chan_FX_TYPE.TOP:
|
||||
return self._cal_top_fx_strength()
|
||||
else: # BOTTOM
|
||||
return self._cal_bottom_fx_strength()
|
||||
|
||||
def _check_contain_relation(self, k1, k2):
|
||||
"""检查两根K线是否存在包含关系"""
|
||||
return (k1.high >= k2.high and k1.low <= k2.low) or (k2.high >= k1.high and k2.low <= k1.low)
|
||||
|
||||
def _is_big_yang_line(self, klu):
|
||||
"""判断是否为大阳线"""
|
||||
return klu.close > klu.open and (klu.close - klu.open) / klu.open > 0.02
|
||||
|
||||
def _is_big_yin_line(self, klu):
|
||||
"""判断是否为大阴线"""
|
||||
return klu.close < klu.open and (klu.open - klu.close) / klu.open > 0.02
|
||||
|
||||
def _is_small_line(self, klu):
|
||||
"""判断是否为小K线"""
|
||||
return abs(klu.close - klu.open) / klu.open < 0.01
|
||||
|
||||
def _has_long_upper_shadow(self, klu):
|
||||
"""判断是否有长上影线"""
|
||||
body_size = abs(klu.close - klu.open)
|
||||
upper_shadow = klu.high - max(klu.close, klu.open)
|
||||
return upper_shadow > body_size * 1.5
|
||||
|
||||
def _cal_top_fx_strength(self):
|
||||
"""计算顶分型强度"""
|
||||
strength = 0
|
||||
k1, k2, k3 = self.pre, self, self.next
|
||||
|
||||
# (1) 检查包含关系 - 没有包含关系加分
|
||||
has_contain_12 = self._check_contain_relation(k1, k2)
|
||||
has_contain_23 = self._check_contain_relation(k2, k3)
|
||||
|
||||
if not has_contain_12 and not has_contain_23:
|
||||
strength += 1 # 完全没有包含关系,加1分
|
||||
|
||||
# (2) 检查第1条K线是大阳线,第2、3条是小K线的情况
|
||||
if self._is_big_yang_line(k1) and self._is_small_line(k2) and self._is_small_line(k3):
|
||||
strength -= 2 # 中继顶分型特征,减2分
|
||||
|
||||
# (3) 检查第2条K线有长上影线或大阴线,且第3条K线条件
|
||||
k2_mid = (k2.high + k2.low) / 2
|
||||
k3_is_yang = k3.close > k3.open
|
||||
k3_close_above_mid = k3.close > k2_mid
|
||||
|
||||
if (self._has_long_upper_shadow(k2) or self._is_big_yin_line(k2)) and not (k3_is_yang and k3_close_above_mid):
|
||||
strength += 2 # 力度大的顶分型,加2分
|
||||
|
||||
# (4) 检查第2、3条K线包含关系,第3条为大阴线"吃掉"第2条
|
||||
if has_contain_23 and self._is_big_yin_line(k3) and k3.low < k2.low and k3.high < k2.high:
|
||||
strength += 1 # 最坏包含关系,但对顶分型有利,加1分
|
||||
|
||||
# (5) 第3条K线跌破第1条K线底部且不能高于第1条K线区间一半之上
|
||||
k1_mid = (k1.high + k1.low) / 2
|
||||
if k3.low < k1.low and k3.high < k1_mid:
|
||||
strength -= 1 # 较弱的顶分型,减1分
|
||||
|
||||
# 额外检查:第3条K线收盘价相对第1条K线的位置
|
||||
if k3.close < k1.low:
|
||||
strength += 1 # 强烈下跌确认,加1分
|
||||
|
||||
return max(-1, min(3, strength)) # 限制在-1到3范围内
|
||||
|
||||
def _cal_bottom_fx_strength(self):
|
||||
"""计算底分型强度"""
|
||||
strength = 0
|
||||
k1, k2, k3 = self.pre, self, self.next
|
||||
|
||||
# 底分型上边沿
|
||||
fx_top = max(k1.high, k2.high)
|
||||
|
||||
# (1) 第3条K线高点远高于第1条K线高点
|
||||
if k3.high > k1.high * 1.02: # 高出2%以上认为是"远高于"
|
||||
strength += 2 # 较强走势,加2分
|
||||
|
||||
# (2) 第3条K线高点正好是第1根K线高点,或略微高于底分型上边沿
|
||||
elif k1.high * 0.99 <= k3.high <= fx_top * 1.01: # 在合理范围内
|
||||
strength += 0 # 一般走势,不加分也不减分
|
||||
|
||||
# (3) 第3条K线高点低于第1条K线高点
|
||||
elif k3.high < k1.high:
|
||||
strength -= 1 # 较弱走势,减1分
|
||||
|
||||
# 检查包含关系
|
||||
has_contain_12 = self._check_contain_relation(k1, k2)
|
||||
has_contain_23 = self._check_contain_relation(k2, k3)
|
||||
|
||||
if not has_contain_12 and not has_contain_23:
|
||||
strength += 1 # 完全没有包含关系,加1分
|
||||
|
||||
# 检查第3条K线是否为强阳线
|
||||
if self._is_big_yang_line(k3):
|
||||
strength += 1 # 强阳线确认,加1分
|
||||
|
||||
# (4) 检查后续第1条K线(如果存在)
|
||||
if hasattr(k3, 'next') and k3.next:
|
||||
next_k = k3.next
|
||||
if next_k.low > fx_top:
|
||||
strength += 2 # 后续K线低点高于底分型上边沿,强烈确认,加2分
|
||||
elif next_k.low <= k2.low:
|
||||
strength -= 1 # 后续K线跌破分型低点,减1分
|
||||
|
||||
return max(-1, min(3, strength)) # 限制在-1到3范围内
|
||||
|
||||
def calculate_realtime_fx_strength(self):
|
||||
"""
|
||||
用self.pre和self.next实现分型强弱判断(与KLC中cal_fx_strength一致)
|
||||
|
||||
核心缠论原理:
|
||||
- 强分型:出现在笔的末端,能够终结当前笔,标志着趋势转折
|
||||
- 弱分型:出现在笔的中间,是中继性质,笔还会继续延伸
|
||||
|
||||
返回值:
|
||||
3: 极强分型(笔终结+强确认)
|
||||
2: 强分型(笔终结)
|
||||
1: 偏强分型(可能终结笔)
|
||||
0: 中性分型
|
||||
-1: 偏弱分型(中继特征明显)
|
||||
-2: 弱分型(明显中继)
|
||||
-3: 极弱分型(无效分型)
|
||||
"""
|
||||
# 检查是否为分型,且有前后K线数据
|
||||
if self.fx_type == Chan_FX_TYPE.UNKNOWN:
|
||||
return 0
|
||||
if not self.pre or not self.next:
|
||||
return 100
|
||||
# === 核心判断:分型在笔中的位置 ===
|
||||
|
||||
# 1. 检查这个分型是否能够终结当前笔
|
||||
is_bi_end = self._check_if_bi_ending_fx()
|
||||
|
||||
# 2. 检查分型的后续走势确认
|
||||
post_fx_confirmation = self._check_post_fx_confirmation()
|
||||
|
||||
# 3. 检查分型的标准性和强度
|
||||
fx_quality = self._check_fx_quality()
|
||||
|
||||
# === 综合评分 ===
|
||||
base_score = 0
|
||||
|
||||
# 笔位置是最重要的判断标准
|
||||
if is_bi_end == 2: # 强烈确认笔终结
|
||||
base_score = 2
|
||||
elif is_bi_end == 1: # 可能笔终结
|
||||
base_score = 1
|
||||
elif is_bi_end == -1: # 明显中继
|
||||
base_score = -2
|
||||
elif is_bi_end == -2: # 强烈中继特征
|
||||
base_score = -3
|
||||
else: # 不确定
|
||||
base_score = 0
|
||||
|
||||
# 后续确认调整
|
||||
base_score += post_fx_confirmation
|
||||
|
||||
# 分型质量调整
|
||||
base_score += fx_quality
|
||||
|
||||
# 限制在-3到3范围内
|
||||
final_score = max(-3, min(3, base_score))
|
||||
self.fx_strength = final_score
|
||||
# 转换为0-100分制以保持接口一致性
|
||||
#self.fx_strength = int((final_score + 3) * 100 / 6) # -3到3映射到0-100
|
||||
#if final_score > 1.8:
|
||||
#print(self.time, final_score, is_bi_end, post_fx_confirmation, fx_quality)
|
||||
#print(self.time, final_score, is_bi_end, post_fx_confirmation, fx_quality)
|
||||
#self.fx_strength = self.cal_fx()
|
||||
return self.fx_strength
|
||||
|
||||
def _check_if_bi_ending_fx(self):
|
||||
"""
|
||||
检查分型是否为笔终结分型
|
||||
返回值:
|
||||
2: 强烈确认笔终结
|
||||
1: 可能笔终结
|
||||
0: 不确定
|
||||
-1: 明显中继
|
||||
-2: 强烈中继特征
|
||||
"""
|
||||
# 检查是否有足够的后续数据来判断
|
||||
if not self.next or not hasattr(self.next, 'next'):
|
||||
return 0
|
||||
|
||||
# 获取分型后的几根K线数据
|
||||
subsequent_klus = []
|
||||
temp = self.next
|
||||
for i in range(2): # 检查后续2根K线
|
||||
if temp:
|
||||
subsequent_klus.append(temp)
|
||||
temp = temp.next if hasattr(temp, 'next') else None
|
||||
else:
|
||||
break
|
||||
|
||||
if len(subsequent_klus) < 2:
|
||||
return 0
|
||||
|
||||
if self.fx_type == Chan_FX_TYPE.TOP:
|
||||
return self._check_top_bi_ending(subsequent_klus)
|
||||
else: # BOTTOM
|
||||
return self._check_bottom_bi_ending(subsequent_klus)
|
||||
|
||||
def _check_top_bi_ending(self, subsequent_klus):
|
||||
"""检查顶分型是否为笔终结"""
|
||||
# 强烈笔终结特征:
|
||||
# 1. 后续K线持续下跌,且跌破关键位置
|
||||
# 2. 没有新的更高的高点出现
|
||||
|
||||
broken_key_levels = 0
|
||||
new_highs = 0
|
||||
downward_trend = 0
|
||||
|
||||
# 检查关键价位突破
|
||||
first_low = self.pre.low
|
||||
middle_low = self.low
|
||||
key_support = min(first_low, middle_low)
|
||||
|
||||
for i, klu in enumerate(subsequent_klus):
|
||||
# 检查是否跌破关键支撑
|
||||
if klu.low < key_support:
|
||||
broken_key_levels += 1
|
||||
|
||||
# 检查是否出现新高
|
||||
if klu.high > self.high:
|
||||
new_highs += 1
|
||||
|
||||
# 检查下跌趋势
|
||||
if i > 0 and klu.close < subsequent_klus[i-1].close:
|
||||
downward_trend += 1
|
||||
|
||||
# 强烈笔终结:跌破关键位且无新高
|
||||
if broken_key_levels >= 1 and new_highs == 0 and downward_trend >= 2:
|
||||
return 2
|
||||
|
||||
# 可能笔终结:部分条件满足
|
||||
if (broken_key_levels >= 1 and new_highs <= 1) or (new_highs == 0 and downward_trend >= 3):
|
||||
return 1
|
||||
|
||||
# 明显中继:出现新高且未跌破关键位
|
||||
if new_highs >= 2 and broken_key_levels == 0:
|
||||
return -2
|
||||
|
||||
# 中继倾向:出现新高
|
||||
if new_highs >= 1:
|
||||
return -1
|
||||
|
||||
return 0
|
||||
|
||||
def _check_bottom_bi_ending(self, subsequent_klus):
|
||||
"""检查底分型是否为笔终结"""
|
||||
# 强烈笔终结特征:
|
||||
# 1. 后续K线持续上涨,且突破关键位置
|
||||
# 2. 没有新的更低的低点出现
|
||||
|
||||
broken_key_levels = 0
|
||||
new_lows = 0
|
||||
upward_trend = 0
|
||||
|
||||
# 检查关键价位突破
|
||||
first_high = self.pre.high
|
||||
middle_high = self.high
|
||||
key_resistance = max(first_high, middle_high)
|
||||
|
||||
for i, klu in enumerate(subsequent_klus):
|
||||
# 检查是否突破关键阻力
|
||||
if klu.high > key_resistance:
|
||||
broken_key_levels += 1
|
||||
|
||||
# 检查是否出现新低
|
||||
if klu.low < self.low:
|
||||
new_lows += 1
|
||||
|
||||
# 检查上涨趋势
|
||||
if i > 0 and klu.close > subsequent_klus[i-1].close:
|
||||
upward_trend += 1
|
||||
|
||||
# 强烈笔终结:突破关键位且无新低
|
||||
if broken_key_levels >= 1 and new_lows == 0 and upward_trend >= 2:
|
||||
return 2
|
||||
|
||||
# 可能笔终结:部分条件满足
|
||||
if (broken_key_levels >= 1 and new_lows <= 1) or (new_lows == 0 and upward_trend >= 3):
|
||||
return 1
|
||||
|
||||
# 明显中继:出现新低且未突破关键位
|
||||
if new_lows >= 2 and broken_key_levels == 0:
|
||||
return -2
|
||||
|
||||
# 中继倾向:出现新低
|
||||
if new_lows >= 1:
|
||||
return -1
|
||||
|
||||
return 0
|
||||
|
||||
def _check_post_fx_confirmation(self):
|
||||
"""
|
||||
检查分型后的走势确认
|
||||
返回值:-1到1的调整分数
|
||||
"""
|
||||
if not self.next:
|
||||
return 0
|
||||
|
||||
score = 0
|
||||
|
||||
# 检查第三根K线的确认
|
||||
third_klu = self.next
|
||||
|
||||
if self.fx_type == Chan_FX_TYPE.TOP:
|
||||
# 顶分型:第三根K线应该走弱
|
||||
middle_price = (self.high + self.low) / 2
|
||||
|
||||
if third_klu.close < middle_price:
|
||||
score += 0.5
|
||||
if third_klu.low < self.pre.low: # 跌破第一根K线低点
|
||||
score += 0.5
|
||||
if third_klu.close < third_klu.open and abs(third_klu.close - third_klu.open) > abs(self.close - self.open) * 0.5:
|
||||
score += 0.3 # 明显阴线
|
||||
|
||||
else: # BOTTOM
|
||||
# 底分型:第三根K线应该走强
|
||||
middle_price = (self.high + self.low) / 2
|
||||
|
||||
if third_klu.close > middle_price:
|
||||
score += 0.5
|
||||
if third_klu.high > self.pre.high: # 突破第一根K线高点
|
||||
score += 0.5
|
||||
if third_klu.close > third_klu.open and abs(third_klu.close - third_klu.open) > abs(self.close - self.open) * 0.5:
|
||||
score += 0.3 # 明显阳线
|
||||
|
||||
return min(1, max(-1, score))
|
||||
|
||||
def _check_fx_quality(self):
|
||||
"""
|
||||
检查分型本身的质量
|
||||
返回值:-1到1的调整分数
|
||||
"""
|
||||
score = 0
|
||||
|
||||
# 检查分型的标准性
|
||||
if self.fx_type == Chan_FX_TYPE.TOP:
|
||||
# 高点突出程度
|
||||
high_diff1 = (self.high - self.pre.high) / self.high if self.high > 0 else 0
|
||||
high_diff2 = (self.high - self.next.high) / self.high if self.high > 0 else 0
|
||||
min_diff = min(high_diff1, high_diff2)
|
||||
|
||||
if min_diff > 0.03: # 非常突出
|
||||
score += 0.5
|
||||
elif min_diff > 0.01: # 比较突出
|
||||
score += 0.2
|
||||
elif min_diff < 0.003: # 不够突出
|
||||
score -= 0.5
|
||||
|
||||
else: # BOTTOM
|
||||
# 低点突出程度
|
||||
low_diff1 = (self.pre.low - self.low) / self.pre.low if self.pre.low > 0 else 0
|
||||
low_diff2 = (self.next.low - self.low) / self.next.low if self.next.low > 0 else 0
|
||||
min_diff = min(low_diff1, low_diff2)
|
||||
|
||||
if min_diff > 0.03: # 非常突出
|
||||
score += 0.5
|
||||
elif min_diff > 0.01: # 比较突出
|
||||
score += 0.2
|
||||
elif min_diff < 0.003: # 不够突出
|
||||
score -= 0.5
|
||||
|
||||
# 检查量价配合
|
||||
avg_volume = self._get_avg_volume(lookback=5)
|
||||
if avg_volume > 0:
|
||||
volume_ratio = self.volume / avg_volume
|
||||
if volume_ratio > 1.5:
|
||||
score += 0.3
|
||||
elif volume_ratio < 0.7:
|
||||
score -= 0.2
|
||||
|
||||
return min(1, max(-1, score))
|
||||
|
||||
def _get_avg_volume(self, lookback=5):
|
||||
"""获取前N根K线平均成交量"""
|
||||
volumes = []
|
||||
temp = self.pre
|
||||
|
||||
for i in range(lookback):
|
||||
if temp:
|
||||
volumes.append(temp.volume)
|
||||
temp = temp.pre if hasattr(temp, 'pre') else None
|
||||
else:
|
||||
break
|
||||
|
||||
return sum(volumes) / len(volumes) if volumes else self.volume
|
||||
|
||||
def get_fx_signal(self):
|
||||
"""
|
||||
获取分型交易信号
|
||||
返回: (信号类型, 强度, 建议)
|
||||
"""
|
||||
if not self.fx_confirmed:
|
||||
return ("无信号", 0, "等待分型确认")
|
||||
|
||||
strength_level = "弱"
|
||||
if self.fx_strength >= 80:
|
||||
strength_level = "极强"
|
||||
elif self.fx_strength >= 65:
|
||||
strength_level = "强"
|
||||
elif self.fx_strength >= 50:
|
||||
strength_level = "中等"
|
||||
|
||||
if self.fx_type == Chan_FX_TYPE.TOP:
|
||||
signal_type = f"{strength_level}顶分型"
|
||||
if self.fx_strength >= 65:
|
||||
suggestion = "考虑减仓或止盈"
|
||||
else:
|
||||
suggestion = "谨慎观望"
|
||||
else:
|
||||
signal_type = f"{strength_level}底分型"
|
||||
if self.fx_strength >= 65:
|
||||
suggestion = "考虑建仓或加仓"
|
||||
else:
|
||||
suggestion = "谨慎观望"
|
||||
|
||||
return (signal_type, self.fx_strength, suggestion)
|
||||
|
||||
def update_realtime_analysis(self):
|
||||
"""
|
||||
更新实时分析(在每根K线完成时调用)
|
||||
"""
|
||||
self.detect_realtime_fx()
|
||||
if self.fx_confirmed:
|
||||
self.calculate_realtime_fx_strength()
|
||||
|
||||
def set_idx(self, idx):
|
||||
self.idx = idx
|
||||
self.index = idx
|
||||
@@ -639,9 +134,6 @@ class ChanKLU:
|
||||
self.bblow120 = float(item['bblow120']) if 'bblow120' in item and item['bblow120'] else 0
|
||||
self.bbup365 = float(item['bbup365']) if 'bbup365' in item and item['bbup365'] else 0
|
||||
self.bblow365 = float(item['bblow365']) if 'bblow365' in item and item['bblow365'] else 0
|
||||
# 设置指标后更新实时分析
|
||||
self.update_realtime_analysis()
|
||||
#self.cal_macd_state()
|
||||
def cal_macd_state(self):
|
||||
# 按定义精简实现:优先级 CROSS0 > 位置(HIGH/HE/RETURN_ZERO) > NEAR0 > UNKNOWN
|
||||
# 首条或缺前一根
|
||||
@@ -828,38 +320,4 @@ class ChanKLU:
|
||||
self.macd_state = Chan_MACD_STATE.UNKNOWN
|
||||
return self.macd_state
|
||||
return self.macd_state
|
||||
|
||||
def get_feature_data(self):
|
||||
features = dict()
|
||||
features['klu_close'] = self.close
|
||||
features['klu_open'] = self.open
|
||||
features['klu_high'] = self.high
|
||||
features['klu_low'] = self.low
|
||||
features['klu_volume'] = self.volume
|
||||
features['klu_index'] = self.index
|
||||
features['klu_macd'] = self.macd
|
||||
features['klu_signal'] = self.signal
|
||||
features['klu_macdhist'] = self.macdhist
|
||||
features['klu_ma5'] = self.ma5
|
||||
features['klu_ma10'] = self.ma10
|
||||
features['klu_ma30'] = self.ma30
|
||||
features['klu_ma50'] = self.ma50
|
||||
features['klu_ma200'] = self.ma200
|
||||
features['klu_ma250'] = self.ma250
|
||||
features['klu_rsi'] = self.rsi
|
||||
features['klu_volume_ratio'] = self.volume_ratio
|
||||
|
||||
# === 新增:实时分型特征 ===
|
||||
# 将枚举转换为数值:UNKNOWN=0, TOP=1, BOTTOM=-1
|
||||
if self.fx_type == Chan_FX_TYPE.TOP:
|
||||
fx_type_value = 1
|
||||
elif self.fx_type == Chan_FX_TYPE.BOTTOM:
|
||||
fx_type_value = -1
|
||||
else:
|
||||
fx_type_value = 0
|
||||
|
||||
features['klu_fx_type'] = fx_type_value
|
||||
features['klu_fx_strength'] = self.fx_strength
|
||||
features['klu_fx_confirmed'] = 1 if self.fx_confirmed else 0
|
||||
|
||||
return features
|
||||
|
||||
+131
-1478
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
from datetime import timedelta
|
||||
from pandas import DataFrame
|
||||
from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_SEG_DIR, Chan_ZS_DIR, Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX
|
||||
from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR, Chan_SEG_DIR, Chan_ZS_DIR, Chan_BSP_DIR, Chan_BSP_TYPE, Chan_KLC_FX, Chan_PRICE_TREND
|
||||
from ChanKLU import ChanKLU
|
||||
from ChanKLC import ChanKLC
|
||||
from ChanBI import ChanBI
|
||||
@@ -20,40 +20,213 @@ import numpy as np
|
||||
from ChanMACD import ChanMACD
|
||||
|
||||
class TF_DF():
|
||||
def __init__(self, timeframe, df, ticker_indicator):
|
||||
self.timeframe = timeframe
|
||||
self.dataframe = resample_to_interval(df, ticker_indicator*timeframe)
|
||||
self.ticker_indicator = ticker_indicator
|
||||
self.klu_list = []
|
||||
self.klc_list = []
|
||||
self.bi_list = []
|
||||
self.zs_list = []
|
||||
self.bsp_list = []
|
||||
self.seg_list = []
|
||||
self.init_TF_DF()
|
||||
def init_TF_DF(self):
|
||||
self.klu_list = self.cal_kl_data(self.dataframe)
|
||||
self.klc_list = self.cal_klc_list(self.klu_list)
|
||||
self.bi_list = self.cal_bi_list(self.klc_list)
|
||||
self.seg_list = self.cal_seg_list(self.bi_list)
|
||||
self.zs_list = self.cal_zs_list(self.bi_list, self.seg_list)
|
||||
self.chanmacd = ChanMACD(self.klu_list)
|
||||
self.klu_list = self.chanmacd.cal_macd_state()
|
||||
def check_fx(self, klc):
|
||||
def __init__(self, timeframe, df, ticker_indicator):
|
||||
self.timeframe = timeframe
|
||||
self.dataframe = resample_to_interval(df, ticker_indicator*timeframe)
|
||||
self.dataframe = self.add_indicators(self.dataframe)
|
||||
self.ticker_indicator = ticker_indicator
|
||||
self.klu_list = []
|
||||
self.klc_list = []
|
||||
self.bi_list = []
|
||||
self.zs_list = []
|
||||
self.bsp_list = []
|
||||
self.seg_list = []
|
||||
self.init_TF_DF()
|
||||
def init_TF_DF(self):
|
||||
self.klu_list = self.cal_kl_data(self.dataframe)
|
||||
self.klc_list = self.cal_klc_list(self.klu_list)
|
||||
self.bi_list = self.cal_bi_list(self.klc_list)
|
||||
self.seg_list = self.cal_seg_list(self.bi_list)
|
||||
self.zs_list = self.cal_zs_list(self.bi_list, self.seg_list)
|
||||
self.chanmacd = ChanMACD(self.klu_list)
|
||||
self.klu_list = self.chanmacd.cal_macd_state()
|
||||
def add_indicators(self, df):
|
||||
fast = 12
|
||||
slow = 26
|
||||
period = 9
|
||||
macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period)
|
||||
bb365 = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0)
|
||||
bb120 = ta.BBANDS(df, timeperiod=120, nbdevup=3.0, nbdevdn=3.0, matype=0)
|
||||
bb30 = ta.BBANDS(df, timeperiod=41, nbdevup=2.3, nbdevdn=2.3, matype=0)
|
||||
bb302 = ta.BBANDS(df, timeperiod=41, nbdevup=2.0, nbdevdn=2.0, matype=0)
|
||||
bb30 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0)
|
||||
bb302 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0)
|
||||
# 计算布林带中轨(移动平均线)
|
||||
bb30_middle = ta.SMA(df, timeperiod=90)
|
||||
|
||||
# 手动计算布林带 %B 指标 (BBP)
|
||||
# %B = (Price - Lower Band) / (Upper Band - Lower Band)
|
||||
bbp365 = (df['close'] - bb365['lowerband']) / (bb365['upperband'] - bb365['lowerband'])
|
||||
bbp120 = (df['close'] - bb120['lowerband']) / (bb120['upperband'] - bb120['lowerband'])
|
||||
bbp30 = (df['close'] - bb30['lowerband']) / (bb30['upperband'] - bb30['lowerband'])
|
||||
bbp302 = (df['close'] - bb302['lowerband']) / (bb302['upperband'] - bb302['lowerband'])
|
||||
df['atr'] = ta.ATR(df, timeperiod=14)
|
||||
df['bbup365'] = bb365['upperband']
|
||||
df['bblow365'] = bb365['lowerband']
|
||||
df['bbp365'] = bbp365
|
||||
df['bbup120'] = bb120['upperband']
|
||||
df['bblow120'] = bb120['lowerband']
|
||||
df['bbp120'] = bbp120
|
||||
df['bbup30'] = bb30['upperband']
|
||||
df['bblow30'] = bb30['lowerband']
|
||||
df['bbmiddle30'] = bb30_middle # 添加bb30中轨
|
||||
df['bbp30'] = bbp30
|
||||
df['bbup302'] = bb302['upperband']
|
||||
df['bblow302'] = bb302['lowerband']
|
||||
df['bbp302'] = bbp302
|
||||
df['macd'] = macd['macd']
|
||||
df['macdsignal'] = macd['macdsignal']
|
||||
df['macdhist'] = macd['macdhist']
|
||||
df['ema5'] = ta.EMA(df, timeperiod=5)
|
||||
df['ema10'] = ta.EMA(df, timeperiod=10)
|
||||
df['ema24'] = ta.EMA(df, timeperiod=24)
|
||||
df['ema26'] = ta.EMA(df, timeperiod=26)
|
||||
df['ema52'] = ta.EMA(df, timeperiod=52)
|
||||
df['rsi'] = ta.RSI(df, timeperiod=14)
|
||||
df['volume_ratio'] = self.cal_volume_ratio(df)
|
||||
return df
|
||||
def check_fx(self, klc):
|
||||
if klc.pre and klc.next:
|
||||
if klc.high > klc.pre.high and klc.high > klc.next.high:
|
||||
if klc.macd > 0 and klc.macd > klc.signal and klc.signal > klc.macdhist:
|
||||
klc.set_fx(Chan_FX_TYPE.TOP)
|
||||
#print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time,klc.fx, "TOP")
|
||||
# print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time,klc.fx, "TOP")
|
||||
return Chan_FX_TYPE.TOP
|
||||
elif klc.low < klc.pre.low and klc.low < klc.next.low:
|
||||
if klc.macd < 0 and klc.macd < klc.signal and klc.signal < klc.macdhist:
|
||||
klc.set_fx(Chan_FX_TYPE.BOTTOM)
|
||||
#print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time,klc.fx, "BOTTOM")
|
||||
# print(klc.start_time, klc.end_time,klc.next.start_time, klc.next.end_time,klc.fx, "BOTTOM")
|
||||
return Chan_FX_TYPE.BOTTOM
|
||||
return Chan_FX_TYPE.UNKNOWN
|
||||
|
||||
def cal_kl_data(self, dataframe:DataFrame):
|
||||
def cal_volume_ratio(self, dataframe, window=10):
|
||||
df = dataframe.copy()
|
||||
# 计算过去N根K线的平均成交量
|
||||
df['avg_volume'] = df['volume'].rolling(window=window).mean()
|
||||
# 计算量比
|
||||
df['volume_ratio'] = df['volume'] / df['avg_volume']
|
||||
# 填充缺失值(前N根K线)
|
||||
df['volume_ratio'] = df['volume_ratio'].fillna(1.0)
|
||||
return df['volume_ratio']
|
||||
def cal_trend(self, klc_list):
|
||||
"""
|
||||
基于价格与EMA24/EMA52的位置关系、以及MACD/Signal/Hist的方向,
|
||||
为每个KLC打上趋势标签:'UP' / 'DOWN' / 'FLAT'。
|
||||
仅设置 klc.trend,不影响其它字段。
|
||||
"""
|
||||
if not klc_list:
|
||||
return klc_list
|
||||
last_trend = Chan_PRICE_TREND.UNKNOWN
|
||||
for klc in klc_list:
|
||||
price = getattr(klc, 'close', None)
|
||||
ema24 = getattr(klc, 'ema24', None)
|
||||
ema52 = getattr(klc, 'ema52', None)
|
||||
macd = getattr(klc, 'macd', 0) if getattr(klc, 'macd', None) is not None else 0
|
||||
signal = getattr(klc, 'signal', 0) if getattr(klc, 'signal', None) is not None else 0
|
||||
hist = getattr(klc, 'macdhist', 0) if getattr(klc, 'macdhist', None) is not None else 0
|
||||
rsi = getattr(klc, 'rsi', None)
|
||||
trend = Chan_PRICE_TREND.UNKNOWN
|
||||
try:
|
||||
# 有效性
|
||||
price_valid = price is not None and price != 0
|
||||
ema24_valid = ema24 is not None and ema24 != 0
|
||||
ema52_valid = ema52 is not None and ema52 != 0
|
||||
# 多因子投票
|
||||
score = 0
|
||||
# 1) 均线结构 + 价位
|
||||
if ema24_valid and ema52_valid:
|
||||
score += 1 if ema24 > ema52 else -1
|
||||
if price_valid and ema24_valid:
|
||||
score += 1 if price > ema24 else -1
|
||||
if price_valid and ema52_valid:
|
||||
score += 1 if price > ema52 else -1
|
||||
# 2) MACD结构
|
||||
score += 1 if macd >= signal else -1
|
||||
if hist != 0:
|
||||
score += 1 if hist > 0 else -1
|
||||
# 3) 动量与均线差分斜率
|
||||
pre = getattr(klc, 'pre', None)
|
||||
if pre:
|
||||
pre_close = getattr(pre, 'close', None)
|
||||
if price_valid and pre_close is not None:
|
||||
score += 1 if price >= pre_close else -1
|
||||
pre_ema24 = getattr(pre, 'ema24', None)
|
||||
pre_ema52 = getattr(pre, 'ema52', None)
|
||||
if ema24_valid and ema52_valid and pre_ema24 not in (None, 0) and pre_ema52 not in (None, 0):
|
||||
spread_now = ema24 - ema52
|
||||
spread_pre = pre_ema24 - pre_ema52
|
||||
score += 1 if spread_now >= spread_pre else -1
|
||||
# 4) RSI 辅助
|
||||
if rsi is not None:
|
||||
if rsi >= 55:
|
||||
score += 1
|
||||
elif rsi <= 45:
|
||||
score -= 1
|
||||
# 5) 指标未就绪回退(EMA/MACD缺失时,用动量与RSI辅助,延续趋势)
|
||||
has_full_ind = ema24_valid and ema52_valid and not (macd == 0 and signal == 0 and hist == 0)
|
||||
if not has_full_ind:
|
||||
# 仅根据价动量/RSI做轻量判断,默认延续 last_trend,除非出现强反向
|
||||
strong_up = False
|
||||
strong_down = False
|
||||
pre = getattr(klc, 'pre', None)
|
||||
if pre:
|
||||
pre_close = getattr(pre, 'close', None)
|
||||
if price_valid and pre_close is not None:
|
||||
strong_up = (price >= pre_close)
|
||||
strong_down = (price < pre_close)
|
||||
if rsi is not None:
|
||||
if rsi >= 60:
|
||||
strong_up = True
|
||||
elif rsi <= 40:
|
||||
strong_down = True
|
||||
if last_trend == Chan_PRICE_TREND.UP and not strong_down:
|
||||
trend = Chan_PRICE_TREND.UP
|
||||
elif last_trend == Chan_PRICE_TREND.DOWN and not strong_up:
|
||||
trend = Chan_PRICE_TREND.DOWN
|
||||
else:
|
||||
trend = Chan_PRICE_TREND.UP if strong_up and not strong_down else (Chan_PRICE_TREND.DOWN if strong_down and not strong_up else Chan_PRICE_TREND.FLAT)
|
||||
else:
|
||||
# 6) 震荡过滤(仅当极近EMA52且MACD贴合时判作震荡)
|
||||
near_flat = False
|
||||
if price_valid and ema52_valid:
|
||||
near_ema52 = abs(price - ema52) / abs(ema52) <= 0.0005 # 0.05%
|
||||
near_macd = abs(macd - signal) <= (abs(price) * 0.00005 if price_valid else 0)
|
||||
near_flat = near_ema52 and near_macd
|
||||
# 7) 动态阈值 + 趋势记忆(更强粘滞:趋势中容忍小幅反分)
|
||||
if near_flat:
|
||||
trend = Chan_PRICE_TREND.FLAT
|
||||
else:
|
||||
if last_trend == Chan_PRICE_TREND.UP:
|
||||
# 仅当出现明显反向才翻转,否则维持UP
|
||||
if score <= -2:
|
||||
trend = Chan_PRICE_TREND.DOWN
|
||||
else:
|
||||
trend = Chan_PRICE_TREND.UP
|
||||
elif last_trend == Chan_PRICE_TREND.DOWN:
|
||||
if score >= 2:
|
||||
trend = Chan_PRICE_TREND.UP
|
||||
else:
|
||||
trend = Chan_PRICE_TREND.DOWN
|
||||
else:
|
||||
# 初始无记忆时,降低进入门槛
|
||||
if score >= 1:
|
||||
trend = Chan_PRICE_TREND.UP
|
||||
elif score <= -1:
|
||||
trend = Chan_PRICE_TREND.DOWN
|
||||
else:
|
||||
trend = Chan_PRICE_TREND.FLAT
|
||||
except Exception:
|
||||
trend = Chan_PRICE_TREND.UNKNOWN
|
||||
# 写回趋势
|
||||
if hasattr(klc, 'set_trend'):
|
||||
klc.set_trend(trend)
|
||||
else:
|
||||
setattr(klc, 'trend', trend)
|
||||
last_trend = trend
|
||||
price_diff = klc.close - klc.pre.close if klc.pre else 0
|
||||
print(klc.start_time, klc.end_time, klc.close, klc.ema24, klc.ema52, klc.macd, klc.signal, klc.macdhist, klc.trend, price_diff)
|
||||
#print(klc.start_time, klc.end_time, klc.trend, price_diff)
|
||||
return klc_list
|
||||
def cal_kl_data(self, dataframe:DataFrame):
|
||||
fields = "time,open,high,low,close,volume"
|
||||
klu_list = []
|
||||
last_klu = None
|
||||
@@ -65,8 +238,8 @@ class TF_DF():
|
||||
l = item['low']
|
||||
c = item['close']
|
||||
v = item['volume']
|
||||
#time_obj = date.fromtimestamp(date)
|
||||
#date = date + timedelta(hours=8)
|
||||
# time_obj = date.fromtimestamp(date)
|
||||
# date = date + timedelta(hours=8)
|
||||
time_str = date.strftime('%Y-%m-%d %H:%M:%S')
|
||||
item_data = [
|
||||
time_str,
|
||||
@@ -76,9 +249,9 @@ class TF_DF():
|
||||
c,
|
||||
v
|
||||
]
|
||||
#klu = KLU(self.create_item_dict(item_data, GetColumnNameFromFieldList(fields)))
|
||||
# klu = KLU(self.create_item_dict(item_data, GetColumnNameFromFieldList(fields)))
|
||||
klu = ChanKLU(time_str, o, h, l, c, v)
|
||||
#print(klu.time, klu.open, klu.high, klu.low, klu.close, klu.volume)
|
||||
# print(klu.time, klu.open, klu.high, klu.low, klu.close, klu.volume)
|
||||
klu.set_idx(i)
|
||||
klu_list.append(klu)
|
||||
if last_klu:
|
||||
@@ -89,7 +262,7 @@ class TF_DF():
|
||||
klu.set_indicators(item)
|
||||
return klu_list
|
||||
|
||||
def cal_klc_list(self, klu_list):
|
||||
def cal_klc_list(self, klu_list):
|
||||
klc_list = []
|
||||
last_klu = None
|
||||
macd = ChanMACD(klu_list)
|
||||
@@ -117,9 +290,10 @@ class TF_DF():
|
||||
klc = ChanKLC(klu, 0, ddir)
|
||||
klc_list.append(klc)
|
||||
last_klu = klu
|
||||
klc_list = self.cal_trend(klc_list)
|
||||
return klc_list
|
||||
|
||||
def cal_seg_list(self, bi_list):
|
||||
def cal_seg_list(self, bi_list):
|
||||
seg_list = []
|
||||
up_bi_list = []
|
||||
down_bi_list = []
|
||||
@@ -707,7 +881,7 @@ class TF_DF():
|
||||
#print(bi_list[index].start_time, bi_list[index].start_klc.start_time, bi_list[index].dir)
|
||||
return bi_list
|
||||
|
||||
def get_decimal(self, value):
|
||||
def get_decimal(self, value):
|
||||
return Decimal("{:.2f}".format(value))
|
||||
|
||||
def cal_zs_list(self, bi_list, seg_list):
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
# --- Do not remove these libs ---
|
||||
from statistics import median
|
||||
from freqtrade.strategy import IStrategy, stoploss_from_absolute
|
||||
import sys
|
||||
import os
|
||||
# 添加父目录到系统路径
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
from ChanLun import ChanLun
|
||||
from ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR, Chan_KLC_FX
|
||||
# --------------------------------
|
||||
from technical.util import resample_to_interval, resampled_merge
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
import pandas as pd
|
||||
from datetime import datetime, timedelta
|
||||
from freqtrade.persistence import Trade, Order
|
||||
from typing import Optional
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
### Now you can use logger.info('asfd') to log
|
||||
# freqtrade plot-dataframe --strategy ChanLun_BTC --datadir user_data/data/binance -c ./user_data/ChanLun_SOL_30.json --timerange=20250309-
|
||||
|
||||
# freqtrade trade -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC --strategy-path ./user_data/Chan/strategies
|
||||
# freqtrade backtesting -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC --strategy-path ./user_data/Chan/strategies --timerange=20250901-
|
||||
# freqtrade download-data -c ./user_data/Chan/config/ChanLun_BTC_30.json -t 1m --pairs BTC/USDT:USDT --timerange=20250405-
|
||||
# freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi --strategy ChanLun_BTC --strategy-path ./user_data/Chan/strategies -c ./user_data/Chan/config/ChanLun_BTC_30.json -e 200 --timerange=20250201-20250901
|
||||
# freqtrade edge -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC --strategy-path ./user_data/Chan/strategies --timerange 20250721-20250901
|
||||
# freqtrade plot-dataframe -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC --strategy-path ./user_data/Chan/strategies --timerange 20250721-20250901
|
||||
|
||||
# sudo docker compose run --rm chanlun_btc backtesting -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC --strategy-path ./user_data/Chan/strategies --timerange=20250721-
|
||||
# sudo docker compose run --rm chanlun_btc download-data -c ./user_data/Chan/config/ChanLun_BTC_30.json --pairs BTC/USDT:USDT -t 1m --timerange 20240101-
|
||||
# sudo docker compose run --rm chanlun_btc trade -c ./user_data/Chan/config/ChanLun_BTC_30.json --strategy ChanLun_BTC --strategy-path ./user_data/Chan/strategies
|
||||
|
||||
class ChanLun_BTC(IStrategy):
|
||||
INTERFACE_VERSION: int = 3
|
||||
# Minimal ROI designed for the strategy.
|
||||
# This attribute will be overridden if the config file contains "minimal_roi"
|
||||
# 30m and 1h
|
||||
|
||||
minimal_roi = {
|
||||
"0": 0.15,
|
||||
"360": 0.2,
|
||||
"640": 0.1,
|
||||
"1200": 0
|
||||
}
|
||||
# 5m and 15m
|
||||
minimal_roi_1 = {
|
||||
"0": 0.1,
|
||||
"60": 0.05,
|
||||
"120": 0.02,
|
||||
"240": 0
|
||||
}
|
||||
# 15m and 30m
|
||||
minimal_roi_1 = {
|
||||
"0": 0.1,
|
||||
"240": 0.05,
|
||||
"480": 0.03,
|
||||
"600": 0
|
||||
}
|
||||
minimal_roi_1 = {
|
||||
"0": 1.50,
|
||||
"120": 0.05,
|
||||
"240": 0.025,
|
||||
"360": 0
|
||||
}
|
||||
|
||||
can_short = True
|
||||
lev = 1.0
|
||||
stoploss = -0.3 # 设置为很大的负值,让custom_stoploss来控制
|
||||
use_custom_stoploss = True # 启用自定义止损
|
||||
|
||||
trailing_stop = False
|
||||
trailing_stop_positive = 0.03
|
||||
trailing_stop_positive_offset = 0.06
|
||||
trailing_only_offset_is_reached = False
|
||||
|
||||
# 关闭分批止盈/仓位调整
|
||||
position_adjustment_enable = False
|
||||
startup_candle_count = 2880
|
||||
time3 = 3
|
||||
time5 = 5
|
||||
time15 = 15
|
||||
time30 = 30
|
||||
time60 = 60
|
||||
time2h = 120
|
||||
time4h = 240
|
||||
time1d = 1440
|
||||
last_time = datetime.now()
|
||||
chan = ChanLun()
|
||||
last_order = None
|
||||
last_trade = None
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
|
||||
dataframe = self.add_indicators(dataframe)
|
||||
# 仅保留15m(用于BSP)与60m(用于ATR过滤/止损)两个重采样
|
||||
dataframe_15 = resample_to_interval(dataframe, self.get_ticker_indicator() * 15)
|
||||
dataframe_60 = resample_to_interval(dataframe, self.get_ticker_indicator() * 60)
|
||||
# 计算多周期BSP(以15m为基准),并合并到15m数据上
|
||||
# 先给重采样帧补指标
|
||||
dataframe_15 = self.add_indicators(dataframe_15)
|
||||
dataframe_60 = self.add_indicators(dataframe_60)
|
||||
# 计算15m BSP
|
||||
bsp_15 = self.chan.cal_bsp(dataframe, self.get_ticker_indicator())
|
||||
# 合并15m与60m到主DF,生成 resample_*_* 列
|
||||
dataframe = resampled_merge(dataframe, dataframe_15)
|
||||
dataframe = resampled_merge(dataframe, dataframe_60)
|
||||
return dataframe
|
||||
def add_indicators(self, df):
|
||||
fast = 12
|
||||
slow = 26
|
||||
period = 9
|
||||
macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period)
|
||||
bb365 = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0)
|
||||
bb120 = ta.BBANDS(df, timeperiod=120, nbdevup=3.0, nbdevdn=3.0, matype=0)
|
||||
bb30 = ta.BBANDS(df, timeperiod=41, nbdevup=2.3, nbdevdn=2.3, matype=0)
|
||||
bb302 = ta.BBANDS(df, timeperiod=41, nbdevup=2.0, nbdevdn=2.0, matype=0)
|
||||
bb30 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0)
|
||||
bb302 = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0)
|
||||
# 计算布林带中轨(移动平均线)
|
||||
bb30_middle = ta.SMA(df, timeperiod=90)
|
||||
|
||||
# 手动计算布林带 %B 指标 (BBP)
|
||||
# %B = (Price - Lower Band) / (Upper Band - Lower Band)
|
||||
bbp365 = (df['close'] - bb365['lowerband']) / (bb365['upperband'] - bb365['lowerband'])
|
||||
bbp120 = (df['close'] - bb120['lowerband']) / (bb120['upperband'] - bb120['lowerband'])
|
||||
bbp30 = (df['close'] - bb30['lowerband']) / (bb30['upperband'] - bb30['lowerband'])
|
||||
bbp302 = (df['close'] - bb302['lowerband']) / (bb302['upperband'] - bb302['lowerband'])
|
||||
df['atr'] = ta.ATR(df, timeperiod=14)
|
||||
df['bbup365'] = bb365['upperband']
|
||||
df['bblow365'] = bb365['lowerband']
|
||||
df['bbp365'] = bbp365
|
||||
df['bbup120'] = bb120['upperband']
|
||||
df['bblow120'] = bb120['lowerband']
|
||||
df['bbp120'] = bbp120
|
||||
df['bbup30'] = bb30['upperband']
|
||||
df['bblow30'] = bb30['lowerband']
|
||||
df['bbmiddle30'] = bb30_middle # 添加bb30中轨
|
||||
df['bbp30'] = bbp30
|
||||
df['bbup302'] = bb302['upperband']
|
||||
df['bblow302'] = bb302['lowerband']
|
||||
df['bbp302'] = bbp302
|
||||
df['macd'] = macd['macd']
|
||||
df['macdsignal'] = macd['macdsignal']
|
||||
df['macdhist'] = macd['macdhist']
|
||||
df['ema5'] = ta.EMA(df, timeperiod=5)
|
||||
df['ema10'] = ta.EMA(df, timeperiod=10)
|
||||
df['ema24'] = ta.EMA(df, timeperiod=24)
|
||||
df['ema26'] = ta.EMA(df, timeperiod=26)
|
||||
df['ema52'] = ta.EMA(df, timeperiod=52)
|
||||
df['rsi'] = ta.RSI(df, timeperiod=14)
|
||||
df['volume_ratio'] = self.cal_volume_ratio(df)
|
||||
return df
|
||||
def cal_volume_ratio(self, dataframe, window=10):
|
||||
df = dataframe.copy()
|
||||
# 计算过去N根K线的平均成交量
|
||||
df['avg_volume'] = df['volume'].rolling(window=window).mean()
|
||||
# 计算量比
|
||||
df['volume_ratio'] = df['volume'] / df['avg_volume']
|
||||
# 填充缺失值(前N根K线)
|
||||
df['volume_ratio'] = df['volume_ratio'].fillna(1.0)
|
||||
return df['volume_ratio']
|
||||
def custom_entry_price(self, pair: str, trade: Trade | None, current_time: datetime, proposed_rate: float,
|
||||
entry_tag: str | None, side: str, **kwargs) -> float:
|
||||
new_entryprice = proposed_rate
|
||||
if trade:
|
||||
if trade.is_short:
|
||||
new_entryprice = proposed_rate - 50
|
||||
else:
|
||||
new_entryprice = proposed_rate + 50
|
||||
return new_entryprice
|
||||
|
||||
def custom_exit_price(self, pair: str, trade: Trade,
|
||||
current_time: datetime, proposed_rate: float,
|
||||
current_profit: float, exit_tag: str | None, **kwargs) -> float:
|
||||
new_exitprice = proposed_rate
|
||||
if trade:
|
||||
if trade.is_short:
|
||||
new_exitprice = proposed_rate + 50
|
||||
else:
|
||||
new_exitprice = proposed_rate - 50
|
||||
return new_exitprice
|
||||
|
||||
def adjust_trade_position(self, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float,
|
||||
min_stake: Optional[float], max_stake: float,
|
||||
current_entry_rate: float, current_exit_rate: float,
|
||||
current_entry_profit: float, current_exit_profit: float,
|
||||
**kwargs) -> Optional[float]:
|
||||
# 关闭分批止盈,始终不调整仓位
|
||||
return None
|
||||
|
||||
def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime,
|
||||
current_rate: float, current_profit: float, after_fill: bool,
|
||||
**kwargs) -> float | None:
|
||||
"""
|
||||
止损 = 开仓价 ± 1 * ATR(开仓时的ATR)。
|
||||
多单: 开仓价 - ATR;空单: 开仓价 + ATR。
|
||||
"""
|
||||
# 保本止损:当浮盈达到或超过 1% 时,将止损提至开仓价
|
||||
#if current_profit is not None and current_profit >= 0.14:
|
||||
#return stoploss_from_absolute(trade.open_rate, current_rate, is_short=trade.is_short)
|
||||
|
||||
entry_atr = trade.get_custom_data(key="entry_atr")
|
||||
if entry_atr is None:
|
||||
# 回退:取当前数据的 ATR 估算
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
|
||||
if dataframe is not None and len(dataframe) > 0 and 'atr' in dataframe.columns:
|
||||
entry_atr = float(dataframe.iloc[-1]['atr'])
|
||||
else:
|
||||
# 最保守的回退:5%
|
||||
return -0.05
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
|
||||
last_candle = dataframe.iloc[-1].squeeze()
|
||||
ema52_str = 'resample_{}_ema52'.format(self.get_ticker_indicator()*self.time15)
|
||||
ema52_val = float(last_candle.get(ema52_str, 0) or 0)
|
||||
close_str = 'resample_{}_close'.format(self.get_ticker_indicator()*self.time15)
|
||||
close_val = float(last_candle.get(close_str, 0) or 0)
|
||||
if close_val < ema52_val:
|
||||
return -0.01
|
||||
if trade.is_short:
|
||||
stop_price = trade.open_rate + float(entry_atr)
|
||||
else:
|
||||
stop_price = trade.open_rate - float(entry_atr)
|
||||
return stoploss_from_absolute(stop_price, current_rate, is_short=trade.is_short)
|
||||
|
||||
def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
|
||||
current_profit: float, **kwargs):
|
||||
# 不做分批止盈/最终止盈处理,退出由策略信号/ROI/止损决定
|
||||
return None
|
||||
|
||||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||||
time_in_force: str, current_time: datetime, entry_tag: str | None,
|
||||
side: str, **kwargs) -> bool:
|
||||
"""
|
||||
ATR 过滤:atr < 100 不开单。
|
||||
"""
|
||||
try:
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||
if dataframe is None or len(dataframe) == 0:
|
||||
return False
|
||||
last = dataframe.iloc[-1]
|
||||
atr_str = 'resample_{}_atr'.format(self.get_ticker_indicator()*self.time60)
|
||||
atr_val = float(last.get(atr_str, 0) or 0)
|
||||
if atr_val < 0.001:
|
||||
#logger.info(f"ATR过滤:atr={atr_val:.2f} < 100, 拒绝进场 {pair}")
|
||||
return False
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"confirm_trade_entry 异常: {e}")
|
||||
return True
|
||||
|
||||
def order_filled(self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs) -> None:
|
||||
"""
|
||||
Called right after an order fills.
|
||||
Will be called for all order types (entry, exit, stoploss, position adjustment).
|
||||
:param pair: Pair for trade
|
||||
:param trade: trade object.
|
||||
:param order: Order object.
|
||||
:param current_time: datetime object, containing the current datetime
|
||||
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
|
||||
"""
|
||||
# Obtain pair dataframe (just to show how to access it)
|
||||
dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe)
|
||||
last_candle = dataframe.iloc[-1].squeeze()
|
||||
atr_str = 'resample_{}_atr'.format(self.get_ticker_indicator()*self.time15)
|
||||
# 保存开仓时的ATR值用于止损计算
|
||||
if (trade.nr_of_successful_entries == 1) and (order.ft_order_side == trade.entry_side):
|
||||
entry_atr = last_candle[atr_str] * 4
|
||||
trade.set_custom_data(key="entry_atr", value=entry_atr)
|
||||
#logger.info(f"保存开仓时ATR值: {entry_atr}")
|
||||
return None
|
||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
shift15 = self.time15
|
||||
shift60 = self.time60
|
||||
bsp_col = 'resample_{}_bsp_mtf'.format(self.get_ticker_indicator()*shift15)
|
||||
score_col = 'resample_{}_mtf_score'.format(self.get_ticker_indicator()*shift15)
|
||||
macdh_col = 'resample_{}_macdhist'.format(self.get_ticker_indicator()*shift15)
|
||||
c60_col = 'resample_{}_close'.format(self.get_ticker_indicator()*shift60)
|
||||
e60_col = 'resample_{}_ema52'.format(self.get_ticker_indicator()*shift60)
|
||||
# 强化过滤:15m BSP + 分数阈值 + 60m 趋势同向 + 15m MACD柱同向
|
||||
if all(col in dataframe.columns for col in [bsp_col, score_col, macdh_col, c60_col, e60_col]):
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe[bsp_col].shift(shift15) == 1) &
|
||||
(dataframe[score_col].shift(shift15) >= 1.2) &
|
||||
(dataframe[c60_col].shift(shift60) >= dataframe[e60_col].shift(shift60)) &
|
||||
(dataframe[macdh_col].shift(shift15) > 0)
|
||||
),
|
||||
['enter_long', 'enter_tag']] = (1, 'long_bsp15_v2')
|
||||
dataframe.loc[
|
||||
(
|
||||
(dataframe[bsp_col].shift(shift15) == -1) &
|
||||
(dataframe[score_col].shift(shift15) <= -1.2) &
|
||||
(dataframe[c60_col].shift(shift60) <= dataframe[e60_col].shift(shift60)) &
|
||||
(dataframe[macdh_col].shift(shift15) < 0)
|
||||
),
|
||||
['enter_short', 'enter_tag']] = (1, 'short_bsp15_v2')
|
||||
return dataframe
|
||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
shift15 = self.time15
|
||||
shift60 = self.time60
|
||||
bsp_col = 'resample_{}_bsp_mtf'.format(self.get_ticker_indicator()*shift15)
|
||||
score_col = 'resample_{}_mtf_score'.format(self.get_ticker_indicator()*shift15)
|
||||
c60_col = 'resample_{}_close'.format(self.get_ticker_indicator()*shift60)
|
||||
e60_col = 'resample_{}_ema52'.format(self.get_ticker_indicator()*shift60)
|
||||
# 反向强信号或60m趋势反向时平仓
|
||||
if all(col in dataframe.columns for col in [bsp_col, score_col, c60_col, e60_col]):
|
||||
dataframe.loc[
|
||||
(
|
||||
((dataframe[bsp_col].shift(shift15) == -1) & (dataframe[score_col].shift(shift15) <= -0.8)) |
|
||||
(dataframe[c60_col].shift(shift60) < dataframe[e60_col].shift(shift60))
|
||||
),
|
||||
['exit_long', 'exit_tag']] = (1, 'long_close_bsp15')
|
||||
dataframe.loc[
|
||||
(
|
||||
((dataframe[bsp_col].shift(shift15) == 1) & (dataframe[score_col].shift(shift15) >= 0.8)) |
|
||||
(dataframe[c60_col].shift(shift60) > dataframe[e60_col].shift(shift60))
|
||||
),
|
||||
['exit_short', 'exit_tag']] = (1, 'short_close_bsp15')
|
||||
return dataframe
|
||||
def leverage(self, pair: str, current_time: datetime, current_rate: float,
|
||||
proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str,
|
||||
**kwargs) -> float:
|
||||
return self.lev
|
||||
|
||||
def get_ticker_indicator(self):
|
||||
return int(self.timeframe[:-1])
|
||||
Reference in New Issue
Block a user