Initial commit

This commit is contained in:
jackyu66git
2025-04-22 10:04:30 +08:00
commit 626d3a6f26
43 changed files with 8607 additions and 0 deletions
Vendored
BIN
View File
Binary file not shown.
+2
View File
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
+67
View File
@@ -0,0 +1,67 @@
import ChanKLC
from ChanEnum import Chan_BI_DIR
class ChanBI():
def __init__(self, klc: ChanKLC, index, ddir=Chan_BI_DIR.UP):
self.start_klc = klc
self.end_klc = None
self.next = None
self.pre = None
self.dir = ddir
self.index = index
self.is_sure = False
self.high = klc.high
self.low = klc.low
self.sure_time = None
self.klc_list = []
self.klc_list.append(klc)
self.end_time = None
self.start_time = klc.start_time
self.macd_hist = 0
self.macd_div = 0
def set_macd_hist(self, macd_hist):
self.macd_hist = macd_hist
def set_macd_div(self, macd_div):
self.macd_div = macd_div
def check_overlap(self):
if self.next and self.next.next:
if self.dir == Chan_BI_DIR.UP:
return self.high > self.next.low and self.high < self.next.next.high
else:
return self.high > self.next.high and self.low > self.next.next.low
else:
return False
def set_end_klc(self, klc, sure_klc):
if self.dir == Chan_BI_DIR.UP and klc.high > self.high:
self.high = klc.high
if self.dir == Chan_BI_DIR.DOWN and klc.low < self.low:
self.low = klc.low
self.end_klc = klc
self.set_is_sure(True, sure_klc.end_time)
self.end_time = klc.end_time
def set_is_sure(self, is_sure, time):
self.is_sure = is_sure
self.sure_time = time
def set_start_klc(self, klc, ddir):
self.start_klc = klc
self.klc_list = []
self.klc_list.append(klc)
self.high = klc.high
self.low = klc.low
self.dir = ddir
def set_pre(self, bi):
self.pre = bi
def set_next(self, bi):
self.next = bi
def add_klc(self, klc):
self.klc_list.append(klc)
def append_klc_list(self, klc_list):
self.klc_list.append(klc_list)
def update_bi(self, klc):
self.end_klc = None
if self.dir == Chan_BI_DIR.UP and klc.high > self.high:
self.high = klc.high
if self.dir == Chan_BI_DIR.DOWN and klc.low < self.low:
self.low = klc.low
self.is_sure = False
self.sure_time = None
#print(self.start_klc.start_time, klc.start_time, klc.fx, "This bi is extended")
+55
View File
@@ -0,0 +1,55 @@
from ChanEnum import Chan_ZS_DIR
import ChanBI
# 中枢
class ChanBIZS():
def __init__(self, start_bi: ChanBI, index, ddir: Chan_ZS_DIR):
self.start_klc = start_bi.start_klc
self.start_time = self.start_klc.start_time
self.end_time = None
self.index = index
self.next = None
self.pre = None
self.start_bi = start_bi
self.bi_list = []
self.bi_list.append(start_bi)
self.end_bi = None
self.last_bi_in = None
self.bi_out = None
self.is_sure = False
self.zg = 0
self.zd = 0
self.dir = ddir
self.sure_time = None
self.end_klc = None
self.bi_out_count = 0
self.bi_out_list = []
def set_last_bi_in(self, last_bi_in):
self.last_bi_in = last_bi_in
def set_bi_out(self, bi_out):
if bi_out:
#print(bi_out.start_klc.start_time, bi_out.sure_time, bi_out.dir, bi_out_seg.dir, len(self.bi_out_list))
if len(self.bi_out_list) > 0:
last_bi = self.bi_out_list[-1]
if last_bi.index != bi_out.index:
self.bi_out_list.append(bi_out)
else:
self.bi_out_list.append(bi_out)
self.bi_out = bi_out
def set_end_klc(self, end_klc, sure_time, bi_out_count):
self.end_klc = end_klc
self.set_end_time(end_klc.end_time)
self.is_sure = True
self.sure_time = sure_time
self.bi_out_count = bi_out_count
def set_pre(self, pre):
self.pre = pre
def set_next(self, next):
self.next = next
def set_end_time(self, end_time):
self.end_time = end_time
def add_klc(self, klc):
self.klc_list.append(klc)
def set_zg(self, zg):
self.zg = zg
def set_zd(self, zd):
self.zd = zd
+24
View File
@@ -0,0 +1,24 @@
import ChanBI
from ChanEnum import Chan_BSP_TYPE, Chan_BSP_DIR
class ChanBSP():
def __init__(self, bi: ChanBI, index, type: Chan_BSP_TYPE, ddir: Chan_BSP_DIR, sure_time, zs_count, zs, seg):
self.bi = bi
self.klc = bi.start_klc
self.index = index
self.type = type
self.start_time = bi.end_klc.start_time
self.end_time = bi.end_klc.end_time
if sure_time:
self.is_sure = True
self.sure_time = sure_time
else:
self.is_sure = False
self.sure_time = None
self.dir = ddir
self.zs_count = zs_count
self.zs = zs
self.seg = seg
def set_sure_time(self, sure_time):
self.is_sure = True
self.sure_time = sure_time
+44
View File
@@ -0,0 +1,44 @@
from datetime import datetime
class ChanCTime:
def __init__(self, year, month, day, hour, minute, second=0, auto=True):
self.year = year
self.month = month
self.day = day
self.hour = hour
self.minute = minute
self.second = second
self.auto = auto # 自适应对天的理解
self.set_timestamp() # set self.ts
def __str__(self):
if self.hour == 0 and self.minute == 0:
return f"{self.year:04}/{self.month:02}/{self.day:02}"
else:
return f"{self.year:04}/{self.month:02}/{self.day:02} {self.hour:02}:{self.minute:02}"
def to_str(self):
if self.hour == 0 and self.minute == 0:
return f"{self.year:04}/{self.month:02}/{self.day:02}"
else:
return f"{self.year:04}/{self.month:02}/{self.day:02} {self.hour:02}:{self.minute:02}"
def toDateStr(self, splt=''):
return f"{self.year:04}{splt}{self.month:02}{splt}{self.day:02}"
def toDate(self):
return ChanCTime(self.year, self.month, self.day, 0, 0, auto=False)
def set_timestamp(self):
if self.hour == 0 and self.minute == 0 and self.auto:
date = datetime(self.year, self.month, self.day, 23, 59, self.second)
else:
date = datetime(self.year, self.month, self.day, self.hour, self.minute, self.second)
self.ts = date.timestamp()
def __gt__(self, t2):
return self.ts > t2.ts
def __ge__(self, t2):
return self.ts >= t2.ts
+147
View File
@@ -0,0 +1,147 @@
from enum import Enum, auto
from typing import Literal
class Chan_DATA_SRC(Enum):
BAO_STOCK = auto()
CCXT = auto()
CSV = auto()
class Chan_ZS_DIR(Enum):
UP = auto()
DOWN = auto()
class Chan_KL_TYPE(Enum):
K_1M = auto()
K_DAY = auto()
K_WEEK = auto()
K_MON = auto()
K_YEAR = auto()
K_5M = auto()
K_15M = auto()
K_30M = auto()
K_60M = auto()
K_3M = auto()
K_QUARTER = auto()
class Chan_KLINE_DIR(Enum):
UP = auto()
DOWN = auto()
COMBINE = auto()
INCLUDED = auto()
class Chan_FX_TYPE(Enum):
BOTTOM = auto()
TOP = auto()
UNKNOWN = auto()
UP = auto()
DOWN = auto()
TT = auto()
BB = auto()
class Chan_BI_DIR(Enum):
UP = auto()
DOWN = auto()
class Chan_SEG_DIR(Enum):
UP = auto()
DOWN = auto()
class Chan_BI_TYPE(Enum):
UNKNOWN = auto()
STRICT = auto()
SUB_VALUE = auto() # 次高低点成笔
TIAOKONG_THRED = auto()
DAHENG = auto()
TUIBI = auto()
UNSTRICT = auto()
TIAOKONG_VALUE = auto()
Chan_BSP_MAIN_TYPE = Literal['1', '2', '3']
class Chan_BSP_DIR(Enum):
BUY = auto()
SELL = auto()
class Chan_BSP_TYPE(Enum):
T1 = '1'
T1P = '1p'
T2 = '2'
T2S = '2s'
T3A = '3a' # 中枢在1类后面
T3B = '3b' # 中枢在1类前面
T3 = '3'
T3E ='3e' # T3退出点
QJT = 'qjt' # 区间套突破
QJT1 = 'qjt1' # 区间套一类买点
QJT2 = 'qjt2' # 区间套一类卖点
QJT3 = 'qjt3' # 区间套三类买点
def main_type(self) -> Chan_BSP_MAIN_TYPE:
return self.value[0] # type: ignore
class Chan_AUTYPE(Enum):
QFQ = auto()
HFQ = auto()
NONE = auto()
class Chan_TREND_TYPE(Enum):
MEAN = "mean"
MAX = "max"
MIN = "min"
class Chan_TREND_LINE_SIDE(Enum):
INSIDE = auto()
OUTSIDE = auto()
class Chan_LEFT_SEG_METHOD(Enum):
ALL = auto()
PEAK = auto()
class Chan_FX_CHECK_METHOD(Enum):
STRICT = auto()
LOSS = auto()
HALF = auto()
TOTALLY = auto()
class Chan_SEG_TYPE(Enum):
BI = auto()
SEG = auto()
class Chan_MACD_ALGO(Enum):
AREA = auto()
PEAK = auto()
FULL_AREA = auto()
DIFF = auto()
SLOPE = auto()
AMP = auto()
VOLUMN = auto()
AMOUNT = auto()
VOLUMN_AVG = auto()
AMOUNT_AVG = auto()
TURNRATE_AVG = auto()
RSI = auto()
class Chan_DATA_FIELD:
FIELD_TIME = "time_key"
FIELD_OPEN = "open"
FIELD_HIGH = "high"
FIELD_LOW = "low"
FIELD_CLOSE = "close"
FIELD_VOLUME = "volume" # 成交量
FIELD_TURNOVER = "turnover" # 成交额
FIELD_TURNRATE = "turnover_rate" # 换手率
Chan_TRADE_INFO_LST = [Chan_DATA_FIELD.FIELD_VOLUME, Chan_DATA_FIELD.FIELD_TURNOVER, Chan_DATA_FIELD.FIELD_TURNRATE]
+440
View File
@@ -0,0 +1,440 @@
import copy
from typing import Dict, Optional
from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR
import ChanKLU
import ChanCTime
# 根据结合律合并K线后的K线
class ChanKLC():
def __init__(self, klu: ChanKLU, index, ddir=Chan_KLINE_DIR.UP):
self.start_time = klu.time
self.end_time = None
self.high = klu.high
self.low = klu.low
self.dir = ddir
self.index = index
self.klus = []
self.add_klu(klu)
self.fx = Chan_FX_TYPE.UNKNOWN
self.next = None
self.pre = None
self.start_klu = klu
self.end_klu = None
self.state = "00"
self.open = klu.open
self.close = klu.close
self.volume = klu.volume
def add_klu(self, klu):
self.klus.append(klu)
def set_end_klu(self, klu):
self.end_klu = klu
self.end_time = klu.time
self.close = klu.close
for index in range(1, len(self.klus)):
self.volume += self.klus[index].volume
def set_next(self, klc):
self.next = klc
def set_pre(self, klc):
self.pre = klc
def set_state(self, state):
self.state = state
def check_klu_included(self, klu):
if self.high >= klu.high:
# high大于,low小于,左包含
if self.low <= klu.low:
self.add_klu(klu=klu)
# gn>gn-1
if self.dir == Chan_KLINE_DIR.UP:
# UP -> max(dn)
self.low = klu.low
else:
# DOWN -> min(gn)
self.high = klu.high
#self.print(klu, "Z")
return True
# high大于,low大于,不包含
else:
# if self.low > klu.low
# high相等,右包含
if self.high == klu.high:
self.add_klu(klu=klu)
# UP -> max(gn)
if self.dir == Chan_KLINE_DIR.UP:
self.high = klu.high
else:
# DOWN -> min(dn)
self.low = klu.low
return True
else:
return False
else:
# high小于,low大于,右包含
if self.low >= klu.low:
self.add_klu(klu=klu)
# gn>gn-1
if self.dir == Chan_KLINE_DIR.UP:
# UP -> max(gn)
self.high = klu.high
else:
# DOWN -> min(dn)
self.low = klu.low
#self.print(klu, "Y")
return True
else:
# high小于,low小于,不包含
return False
def set_fx(self, fx: Chan_FX_TYPE):
self.fx = fx
def print(self):
print(self.time, self.high, self.low, self.start_time, self.end_time, self.fx, self.index)
def copy(self):
"""创建KLC对象的浅拷贝, 避免循环引用"""
new_klc = ChanKLC(self.start_klu, self.index, self.dir)
new_klc.high = self.high
new_klc.low = self.low
new_klc.state = self.state
new_klc.fx = self.fx
# 不复制 next 和 pre 引用,避免循环引用
return new_klc
def set_pre_fx(self):
if self.pre and self.pre.pre:
self.pre.fx = self.check_fx(self.pre.pre, self.pre)
def check_fx(self, k1, k2):
if k2.high > k1.high and k2.high > self.high:
return Chan_FX_TYPE.TOP
elif k2.low < k1.low and k2.low < self.low:
return Chan_FX_TYPE.BOTTOM
else:
return Chan_FX_TYPE.UNKNOWN
def set_bi_data(self, bi):
self.bi = bi
def cal_klu_features(self):
features = dict()
feature_sums = dict()
feature_counts = dict()
# 遍历所有klu,累计每个特征的总和和计数
for klu in self.klus:
for key, value in klu.get_feature_data().items():
if key not in feature_sums:
feature_sums[key] = 0
feature_counts[key] = 0
feature_sums[key] += value
feature_counts[key] += 1
# 计算每个特征的平均值
for key in feature_sums:
features[key] = feature_sums[key] / feature_counts[key]
return features
def get_feature_data(self):
features = dict()
# 原有基础特征
features['klc_close'] = self.close #0
features['klc_open'] = self.open #1
features['klc_high'] = self.high #2
features['klc_low'] = self.low #3
features['klc_index'] = self.index #4
features['klc_dir'] = 0 if self.dir == Chan_KLINE_DIR.UP else 1 #5
features['klc_state'] = self.state #6
features['klc_fx'] = 0 if self.fx == Chan_FX_TYPE.UNKNOWN else 1 if self.fx == Chan_FX_TYPE.TOP else 2 #7
features['klc_klus'] = len(self.klus) #8
features['klc_volume'] = self.volume #9
features['klc_pre_fx'] = (0 if self.pre.fx == Chan_FX_TYPE.UNKNOWN else 1 if self.pre.fx == Chan_FX_TYPE.TOP else 2) if self.pre else 0
# ===== 2.1 K线形态因子 =====
# K线实体大小
if self.open != 0: # 避免除以零
features['klc_body_size_rel'] = abs(self.close - self.open) / self.open # 相对实体大小
else:
features['klc_body_size_rel'] = 0
features['klc_body_size_abs'] = abs(self.close - self.open) # 绝对实体大小
# 上下影线长度
max_oc = max(self.open, self.close)
min_oc = min(self.open, self.close)
high_low_range = self.high - self.low
if high_low_range != 0: # 避免除以零
features['klc_upper_shadow'] = (self.high - max_oc) / high_low_range # 上影线相对长度
features['klc_lower_shadow'] = (min_oc - self.low) / high_low_range # 下影线相对长度
else:
features['klc_upper_shadow'] = 0
features['klc_lower_shadow'] = 0
# K线波动范围
if self.close != 0: # 避免除以零
features['klc_range'] = (self.high - self.low) / self.close
else:
features['klc_range'] = 0
# 与前K线的价格关系
if self.pre:
# 当前K线最高价与前一根K线最高价的比较
if self.pre.high != 0: # 避免除以零
features['klc_high_ratio'] = self.high / self.pre.high
else:
features['klc_high_ratio'] = 1
# 当前K线最低价与前一根K线最低价的比较
if self.pre.low != 0: # 避免除以零
features['klc_low_ratio'] = self.low / self.pre.low
else:
features['klc_low_ratio'] = 1
# 当前K线收盘价与前一根K线收盘价的相对位置
if self.pre.close != 0: # 避免除以零
features['klc_close_change_1'] = (self.close - self.pre.close) / self.pre.close
else:
features['klc_close_change_1'] = 0
# 如果有前两根K线
if self.pre.pre:
if self.pre.pre.close != 0: # 避免除以零
features['klc_close_change_2'] = (self.close - self.pre.pre.close) / self.pre.pre.close
else:
features['klc_close_change_2'] = 0
else:
features['klc_close_change_2'] = 0
else:
# 如果没有前K线,设置默认值
features['klc_high_ratio'] = 1
features['klc_low_ratio'] = 1
features['klc_close_change_1'] = 0
features['klc_close_change_2'] = 0
# 分型特征编码
# 这里直接使用现有的fx字段,不重复计算
# ===== 2.2 价格关系因子 =====
# 价格与均线的关系 (从KLU中获取)
klu_features = self.cal_klu_features()
# MA5与收盘价的关系
if 'klu_ma5' in klu_features and klu_features['klu_ma5'] != 0:
features['klc_close_to_ma5'] = (self.close - klu_features['klu_ma5']) / klu_features['klu_ma5']
else:
features['klc_close_to_ma5'] = 0
# MA10与收盘价的关系
if 'klu_ma10' in klu_features and klu_features['klu_ma10'] != 0:
features['klc_close_to_ma10'] = (self.close - klu_features['klu_ma10']) / klu_features['klu_ma10']
else:
features['klc_close_to_ma10'] = 0
# MA30与收盘价的关系
if 'klu_ma30' in klu_features and klu_features['klu_ma30'] != 0:
features['klc_close_to_ma30'] = (self.close - klu_features['klu_ma30']) / klu_features['klu_ma30']
else:
features['klc_close_to_ma30'] = 0
# 短期均线与长期均线的差异
if 'klu_ma5' in klu_features and 'klu_ma30' in klu_features and klu_features['klu_ma30'] != 0:
features['klc_ma_diff'] = (klu_features['klu_ma5'] - klu_features['klu_ma30']) / klu_features['klu_ma30']
else:
features['klc_ma_diff'] = 0
# 价格突破特征
# 检查当前K线是否突破前3根K线的最高/最低价
if self.pre:
max_high = self.pre.high
min_low = self.pre.low
temp = self.pre
count = 1
while temp.pre and count < 3:
temp = temp.pre
max_high = max(max_high, temp.high)
min_low = min(min_low, temp.low)
count += 1
features['klc_break_high'] = 1 if self.high > max_high else 0
features['klc_break_low'] = 1 if self.low < min_low else 0
else:
features['klc_break_high'] = 0
features['klc_break_low'] = 0
# ===== 2.3 技术指标因子 =====
# 获取技术指标
# RSI (从KLU中获取)
if 'klu_rsi' in klu_features:
features['klc_rsi'] = klu_features['klu_rsi']
else:
features['klc_rsi'] = 50 # 默认中性值
# MACD (从KLU中获取)
if 'klu_macd' in klu_features:
features['klc_macd'] = klu_features['klu_macd']
else:
features['klc_macd'] = 0
if 'klu_signal' in klu_features:
features['klc_macd_signal'] = klu_features['klu_signal']
else:
features['klc_macd_signal'] = 0
if 'klu_macdhist' in klu_features:
features['klc_macd_hist'] = klu_features['klu_macdhist']
else:
features['klc_macd_hist'] = 0
# 成交量变化
if self.pre:
vol_sum = 0
count = 0
temp = self.pre
# 计算前5根K线的平均成交量
while temp and count < 5:
vol_sum += temp.volume
count += 1
temp = temp.pre
avg_vol = vol_sum / count if count > 0 else self.volume
if avg_vol != 0: # 避免除以零
features['klc_vol_ratio'] = self.volume / avg_vol
else:
features['klc_vol_ratio'] = 1
else:
features['klc_vol_ratio'] = 1
# ===== 2.4 市场环境因子 =====
# 价格波动率 (前5根K线收盘价的标准差)
if self.pre:
close_vals = [self.close]
temp = self.pre
count = 0
while temp and count < 5:
close_vals.append(temp.close)
count += 1
temp = temp.pre
if len(close_vals) > 1:
import numpy as np
std_dev = np.std(close_vals)
avg_close = np.mean(close_vals)
if avg_close != 0: # 避免除以零
features['klc_volatility'] = std_dev / avg_close
else:
features['klc_volatility'] = 0
else:
features['klc_volatility'] = 0
else:
features['klc_volatility'] = 0
# 前5根K线的价格趋势 (简单线性回归斜率)
if self.pre:
price_vals = [self.close]
temp = self.pre
count = 0
while temp and count < 5:
price_vals.append(temp.close)
count += 1
temp = temp.pre
if len(price_vals) > 2:
import numpy as np
y = np.array(price_vals)
x = np.arange(len(y))
# 简单线性回归
slope = np.polyfit(x, y, 1)[0]
# 归一化斜率
if abs(np.mean(y)) > 0: # 避免除以零
features['klc_trend_slope'] = slope / abs(np.mean(y))
else:
features['klc_trend_slope'] = 0
else:
features['klc_trend_slope'] = 0
else:
features['klc_trend_slope'] = 0
# ===== 2.5 其他衍生因子 =====
# K线组合形态
# 十字星 (实体非常小)
body_pct = abs(self.close - self.open) / (self.high - self.low) if (self.high - self.low) > 0 else 0
features['klc_is_doji'] = 1 if body_pct < 0.1 else 0 # 实体小于10%算十字星
# 锤子线/上吊线 (下影线长,上影线短,实体小)
if high_low_range > 0:
lower_shadow_pct = (min_oc - self.low) / high_low_range
upper_shadow_pct = (self.high - max_oc) / high_low_range
features['klc_is_hammer'] = 1 if (lower_shadow_pct > 0.6 and upper_shadow_pct < 0.1) else 0
else:
features['klc_is_hammer'] = 0
# 吞没形态
if self.pre:
prev_body_size = abs(self.pre.close - self.pre.open)
curr_body_size = abs(self.close - self.open)
# 看涨吞没
if (self.pre.close < self.pre.open # 前一根是阴线
and self.close > self.open # 当前是阳线
and self.open <= self.pre.close # 当前开盘低于前收盘
and self.close >= self.pre.open # 当前收盘高于前开盘
and curr_body_size > prev_body_size): # 当前实体大于前实体
features['klc_is_bullish_engulfing'] = 1
else:
features['klc_is_bullish_engulfing'] = 0
# 看跌吞没
if (self.pre.close > self.pre.open # 前一根是阳线
and self.close < self.open # 当前是阴线
and self.open >= self.pre.close # 当前开盘高于前收盘
and self.close <= self.pre.open # 当前收盘低于前开盘
and curr_body_size > prev_body_size): # 当前实体大于前实体
features['klc_is_bearish_engulfing'] = 1
else:
features['klc_is_bearish_engulfing'] = 0
else:
features['klc_is_bullish_engulfing'] = 0
features['klc_is_bearish_engulfing'] = 0
# 包含关系
if self.pre:
# 向上包含
if (self.high >= self.pre.high and self.low >= self.pre.low):
features['klc_is_up_inclusive'] = 1
else:
features['klc_is_up_inclusive'] = 0
# 向下包含
if (self.high <= self.pre.high and self.low <= self.pre.low):
features['klc_is_down_inclusive'] = 1
else:
features['klc_is_down_inclusive'] = 0
# 完全包含
if (self.high >= self.pre.high and self.low <= self.pre.low):
features['klc_is_full_inclusive'] = 1
else:
features['klc_is_full_inclusive'] = 0
# 被完全包含
if (self.high <= self.pre.high and self.low >= self.pre.low):
features['klc_is_inner_inclusive'] = 1
else:
features['klc_is_inner_inclusive'] = 0
else:
features['klc_is_up_inclusive'] = 0
features['klc_is_down_inclusive'] = 0
features['klc_is_full_inclusive'] = 0
features['klc_is_inner_inclusive'] = 0
# 从KLU获取其他特征
features.update(self.cal_klu_features())
return features
+40
View File
@@ -0,0 +1,40 @@
class ChanKLU:
def __init__(self, time, open, high, low, close, volume):
# _time, _close, _open, _high, _low, _extra_info={}
self.kl_type = None
self.time = time
self.close = close
self.open = open
self.high = high
self.low = low
self.volume = volume
self.idx = 0
self.index = 0
self.macd = 0
self.signal = 0
self.macdhist = 0
self.ma5 = 0
self.ma10 = 0
self.ma30 = 0
self.ma250 = 0
self.rsi = 0
def set_idx(self, idx):
self.idx = idx
self.index = idx
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_ma250'] = self.ma250
features['klu_rsi'] = self.rsi
return features
+2236
View File
File diff suppressed because it is too large Load Diff
+435
View File
@@ -0,0 +1,435 @@
import sys
import os
#sys.setrecursionlimit(1000000) #例如这里设置为一百万
#sys.path.append(os.path.abspath("/freqtrade/user_data/Chan"))
sys.path.append(os.path.abspath("/Users/jack/Project/freqtrade/user_data/Chan"))
import numpy as np
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
from ChanKLU import ChanKLU
from ChanKLC import ChanKLC
from ChanBI import ChanBI
from ChanSBI import ChanSBI
from ChanSEG import ChanSEG
from ChanZS import ChanZS
from ChanBSP import ChanBSP
import talib.abstract as ta
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.dates import DateFormatter, date2num
import matplotlib.patches as patches
from technical.util import resample_to_interval
from decimal import Decimal
from ChanLun import ChanLun
import xgboost as xgb
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, classification_report
class ChanLunClassifier:
def __init__(self, dataframe: DataFrame):
self.dataframe = dataframe
self.model = None
chan = ChanLun()
def train_model(self, dataframe=None, data_file_path=None, model_file_path='chan_xgb_model.json', use_cv=False, custom_params=None, model_name=None):
"""
使用dataframe前80%的数据训练XGBoost模型
:param dataframe: 输入的DataFrame,如果为None则使用初始化时的dataframe
:param data_file_path: 特征数据保存路径,可选
:param model_file_path: 模型保存路径
:param use_cv: 是否使用交叉验证寻找最佳参数
:param custom_params: 自定义模型参数
:return: 训练好的模型
"""
if dataframe is None:
dataframe = self.dataframe
# 分割数据集,前80%用于训练
train_size = int(len(dataframe) * 0.8)
train_df = dataframe.iloc[:train_size].copy()
# 获取训练集特征和标签
X_train, y_train = self.get_feature_data(train_df)
if len(X_train) == 0:
print("没有提取到足够的特征数据进行训练")
return None
# 保存特征数据(可选)
if data_file_path:
feature_df = pd.DataFrame(X_train)
feature_df['label'] = y_train
feature_df.to_csv(data_file_path, index=False)
#{'eta': 0.03, 'max_depth': 4, 'subsample': 0.8, 'colsample_bytree': 0.8, 'gamma': 0.1, 'min_child_weight': 3, 'alpha': 1, 'lambda': 3},
# 默认XGBoost参数
default_params = {
'objective': 'binary:logistic',
'max_depth': 4,
'eta': 0.03,
'subsample': 0.8,
'colsample_bytree': 0.8,
'eval_metric': 'auc',
'gamma': 0.1,
'min_child_weight': 3,
'alpha': 1, # L1正则化
'lambda': 3, # L2正则化
'scale_pos_weight': 1
}
# 使用自定义参数覆盖默认参数
if custom_params:
for key, value in custom_params.items():
default_params[key] = value
params = default_params
dtrain = xgb.DMatrix(X_train, label=y_train)
# 如果使用交叉验证寻找最佳参数
if use_cv:
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
from sklearn.metrics import make_scorer, accuracy_score, f1_score
import numpy as np
# 转换为sklearn兼容格式
xgb_model = xgb.XGBClassifier(
objective=params['objective'],
max_depth=params['max_depth'],
learning_rate=params['eta'],
subsample=params['subsample'],
colsample_bytree=params['colsample_bytree'],
gamma=params['gamma'],
min_child_weight=params['min_child_weight'],
reg_alpha=params['alpha'],
reg_lambda=params['lambda'],
scale_pos_weight=params['scale_pos_weight'],
use_label_encoder=False,
eval_metric='auc'
)
# 参数网格
param_grid = {
'max_depth': [3, 5, 7, 9],
'learning_rate': [0.01, 0.05, 0.1, 0.2],
'subsample': [0.6, 0.8, 1.0],
'colsample_bytree': [0.6, 0.8, 1.0],
'min_child_weight': [1, 3, 5],
'gamma': [0, 0.1, 0.2],
'n_estimators': [50, 100, 200]
}
# 使用随机搜索寻找最佳参数(比网格搜索快)
random_search = RandomizedSearchCV(
estimator=xgb_model,
param_distributions=param_grid,
n_iter=10, # 随机尝试的参数组合数
scoring=make_scorer(f1_score),
cv=5,
verbose=1,
n_jobs=-1,
random_state=42
)
print("进行交叉验证参数搜索...")
random_search.fit(X_train, y_train)
# 获取最佳参数
best_params = random_search.best_params_
print(f"最佳参数: {best_params}")
# 使用最佳参数更新模型参数
params['max_depth'] = best_params['max_depth']
params['eta'] = best_params['learning_rate']
params['subsample'] = best_params['subsample']
params['colsample_bytree'] = best_params['colsample_bytree']
params['min_child_weight'] = best_params['min_child_weight']
params['gamma'] = best_params['gamma']
num_round = best_params['n_estimators']
# 使用最佳参数训练最终模型
self.model = xgb.train(params, dtrain, num_round)
else:
# 标准训练(不使用交叉验证)
# 使用早停机制避免过拟合
# 分割训练集为训练和验证
eval_size = int(len(X_train) * 0.2)
X_eval = X_train[-eval_size:]
y_eval = y_train[-eval_size:]
X_train_part = X_train[:-eval_size]
y_train_part = y_train[:-eval_size]
dtrain_part = xgb.DMatrix(X_train_part, label=y_train_part)
deval = xgb.DMatrix(X_eval, label=y_eval)
# 评估列表
evallist = [(dtrain_part, 'train'), (deval, 'eval')]
# 训练模型,使用早停
num_round = 1000 # 设置较大的轮数,让早停机制决定何时停止
self.model = xgb.train(
params,
dtrain_part,
num_round,
evallist,
early_stopping_rounds=50, # 50轮内评估指标无改善则停止
verbose_eval=True
)
# 使用全部训练数据重新训练最终模型,使用最佳轮数
# best_rounds = self.model.best_ntree_limit
# 兼容新版本的XGBoost
if hasattr(self.model, 'best_ntree_limit'):
best_rounds = self.model.best_ntree_limit
elif hasattr(self.model, 'best_iteration'):
best_rounds = self.model.best_iteration
elif hasattr(self.model, 'best_ntree_idx'):
best_rounds = self.model.best_ntree_idx
else:
# 如果都不存在,使用默认值
best_rounds = num_round
print(f"最佳轮数: {best_rounds}")
# 使用全部训练数据和最佳轮数训练最终模型
self.model = xgb.train(params, dtrain, best_rounds)
# 保存模型
if model_file_path:
self.model.save_model(model_name + model_file_path)
# 特征重要性分析
if hasattr(self.model, 'get_score'):
importance = self.model.get_score(importance_type='gain')
print("\n特征重要性 (gain):")
for key, value in sorted(importance.items(), key=lambda x: x[1], reverse=True):
print(f"{key}: {value}")
return self.model
def load_model(self, model_name=None, model_file_path='chan_xgb_model.json'):
if model_name:
self.model = xgb.Booster()
self.model.load_model(model_name + model_file_path)
else:
self.model = xgb.Booster()
self.model.load_model(model_file_path)
def find_best_params(self, dataframe=None):
"""
寻找最佳参数组合
:param dataframe: 输入的DataFrame,如果为None则使用初始化时的dataframe
:return: 最佳参数
"""
# 不同参数组合
param_combinations = [
# 低学习率,深树
{'eta': 0.01, 'max_depth': 8, 'subsample': 0.8, 'colsample_bytree': 0.8, 'gamma': 0, 'min_child_weight': 1},
# 中等学习率,中等树深度
{'eta': 0.05, 'max_depth': 5, 'subsample': 0.7, 'colsample_bytree': 0.7, 'gamma': 0.1, 'min_child_weight': 3},
# 高学习率,浅树
{'eta': 0.1, 'max_depth': 3, 'subsample': 0.6, 'colsample_bytree': 0.6, 'gamma': 0.2, 'min_child_weight': 5},
# 正则化较强 best here
{'eta': 0.03, 'max_depth': 4, 'subsample': 0.8, 'colsample_bytree': 0.8, 'gamma': 0.1, 'min_child_weight': 3, 'alpha': 1, 'lambda': 3},
# 正则化较弱
{'eta': 0.08, 'max_depth': 6, 'subsample': 0.9, 'colsample_bytree': 0.9, 'gamma': 0, 'min_child_weight': 1, 'alpha': 0, 'lambda': 0.5},
]
best_score = 0
best_params = None
best_model = None
for params in param_combinations:
print(f"\n尝试参数组合: {params}")
model = self.train_model(dataframe=dataframe, custom_params=params)
# 分割数据集,后20%用于测试
if dataframe is None:
dataframe = self.dataframe
train_size = int(len(dataframe) * 0.8)
test_df = dataframe.iloc[train_size:].copy()
# 获取测试集特征和标签
X_test, y_test = self.get_validate_feature_data(test_df)
if len(X_test) == 0:
print("没有提取到足够的测试特征数据")
continue
# 预测
dtest = xgb.DMatrix(X_test)
y_pred_prob = model.predict(dtest)
y_pred = [1 if p > 0.5 else 0 for p in y_pred_prob]
# 计算F1分数
f1 = f1_score(y_test, y_pred, zero_division=0)
print(f"F1分数: {f1:.4f}")
if f1 > best_score:
best_score = f1
best_params = params
best_model = model
print(f"\n最佳参数组合 (F1={best_score:.4f}):")
print(best_params)
self.model = best_model
return best_params
def get_feature_data(self, dataframe):
"""
从dataframe提取特征数据
:param dataframe: 输入的DataFrame
:return: 特征矩阵X和标签y
"""
# 使用ChanLun获取bi_list
bi_list = self.chan.cal_bi_list(self.chan.get_klc_list(dataframe))
klc_list = self.chan.get_klc_list(dataframe)
# 筛选方向为UP的bi的起始klc
feature_data = []
labels = []
bi_index = 0
for klc in klc_list:
if bi_index == len(bi_list):
bi_index = len(bi_list) - 1
bi = bi_list[bi_index]
# 提取特征
features = klc.get_feature_data()
# 将特征转换为模型可用的格式
feature_vec = []
for key, value in features.items():
if isinstance(value, (int, float)):
feature_vec.append(value)
else:
feature_vec.append(0)
# 判断这个bi是否赚钱(这里简单定义为:如果bi的结束价格高于起始价格,则标记为1,否则为0)
# 这个标签定义可以根据实际需求修改
if bi.start_klc.index == klc.index:
label = 1
bi_index += 1
else:
label = 0
feature_data.append(feature_vec)
labels.append(label)
print("Trainning data: ", klc_list[-1].start_time, klc_list[-1].fx)
return np.array(feature_data), np.array(labels)
def get_validate_feature_data(self, dataframe):
"""
从dataframe提取特征数据
:param dataframe: 输入的DataFrame
:return: 特征矩阵X和标签y
"""
# 使用ChanLun获取bi_list
bi_list = self.chan.cal_bi_list(self.chan.get_klc_list(dataframe))
klc_list = self.chan.get_klc_list(dataframe)
# 筛选方向为UP的bi的起始klc
feature_data = []
labels = []
bi_index = 0
for klc in klc_list:
if bi_index == len(bi_list):
bi_index = len(bi_list) - 1
bi = bi_list[bi_index]
# 提取特征
features = klc.get_feature_data()
# 将特征转换为模型可用的格式
feature_vec = []
# 与get_feature_data保持一致,只使用相同的特征集
for key, value in features.items():
if isinstance(value, (int, float)):
feature_vec.append(value)
else:
feature_vec.append(0)
if bi.start_klc.index == klc.index:
label = 1
bi_index += 1
else:
label = 0
feature_data.append(feature_vec)
labels.append(label)
return np.array(feature_data), np.array(labels)
def validate_model(self, dataframe=None):
"""
使用dataframe后20%的数据验证模型
:param dataframe: 输入的DataFrame,如果为None则使用初始化时的dataframe
:return: 验证结果
"""
if self.model is None:
print("模型尚未训练,请先调用train_model方法")
return None
if dataframe is None:
dataframe = self.dataframe
# 分割数据集,后20%用于测试
train_size = int(len(dataframe) * 0.8)
test_df = dataframe.iloc[train_size:].copy()
# 获取测试集特征和标签
X_test, y_test = self.get_validate_feature_data(test_df)
if len(X_test) == 0:
print("没有提取到足够的测试特征数据")
return None
# 预测
dtest = xgb.DMatrix(X_test)
y_pred_prob = self.model.predict(dtest)
y_pred = [1 if p > 0.5 else 0 for p in y_pred_prob]
# 计算评估指标
accuracy = accuracy_score(y_test, y_pred)
precision = precision_score(y_test, y_pred, zero_division=0)
recall = recall_score(y_test, y_pred, zero_division=0)
f1 = f1_score(y_test, y_pred, zero_division=0)
# 打印评估报告
print("模型评估结果:")
print(f"准确率: {accuracy:.4f}")
print(f"精确率: {precision:.4f}")
print(f"召回率: {recall:.4f}")
print(f"F1分数: {f1:.4f}")
print("\n分类报告:")
print(classification_report(y_test, y_pred, zero_division=0))
return {
'accuracy': accuracy,
'precision': precision,
'recall': recall,
'f1': f1,
'y_test': y_test,
'y_pred': y_pred,
'y_pred_prob': y_pred_prob
}
def predict(self, klc):
"""
使用训练好的模型预测单个KLC
:param klc: 需要预测的ChanKLC对象
:return: 预测结果(概率值)
"""
if self.model is None:
print("模型尚未训练,请先调用train_model方法")
return None
# 提取特征
features = klc.get_feature_data()
feature_vec = []
# 与get_feature_data保持一致,只使用相同的特征集
for key, value in features.items():
if isinstance(value, (int, float)):
feature_vec.append(value)
else:
feature_vec.append(0)
# 转换为模型输入格式
dtest = xgb.DMatrix(np.array([feature_vec]))
# 预测
return self.model.predict(dtest)[0]
+78
View File
@@ -0,0 +1,78 @@
import copy
from typing import Dict, Optional
from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_BI_DIR
import ChanKLU
from ChanBI import ChanBI
class ChanSBI():
def __init__(self, start_bi: ChanBI, index, dir=Chan_BI_DIR.UP):
self.start_bi = start_bi
self.end_bi = None
self.index = index
self.dir = dir
self.high = start_bi.high
self.low = start_bi.low
self.pre = None
self.next = None
self.fx = Chan_FX_TYPE.UNKNOWN
self.bi_list = []
self.bi_list.append(start_bi)
self.has_fx_gap = False
def set_fx(self, fx):
self.fx = fx
def set_end_bi(self, bi):
self.end_bi = bi
def set_pre(self, sbi):
self.pre = sbi
def set_next(self, sbi):
self.next = sbi
def add_bi(self, bi):
self.bi_list.append(bi)
def check_fx(self):
if self.pre and self.next:
#print(self.pre.start_bi.start_time, self.start_bi.start_time, self.end_bi.end_time, self.next.start_bi.start_time, self.pre.high, self.high, self.next.high, self.pre.low, self.low, self.next.low, self.dir)
if self.high > self.pre.high and self.high > self.next.high:
self.fx = Chan_FX_TYPE.TOP
#print(self.start_bi.start_time, self.pre.start_bi.start_time, self.next.start_bi.start_time, self.fx)
if self.low > self.pre.high:
self.has_fx_gap = True
#print(self.start_bi.start_time, self.end_bi.end_time, self.pre.start_bi.start_time, self.next.start_bi.start_time, self.dir, self.has_fx_gap, self.fx)
return Chan_FX_TYPE.TOP
else:
if self.low < self.pre.low and self.low < self.next.low:
self.fx = Chan_FX_TYPE.BOTTOM
#print(self.start_bi.start_time, self.pre.start_bi.start_time, self.next.start_bi.start_time, self.fx)
if self.high < self.pre.low:
self.has_fx_gap = True
#print(self.start_bi.start_time, self.end_bi.end_time, self.pre.start_bi.start_time, self.next.start_bi.start_time, self.dir, self.has_fx_gap, self.fx)
return Chan_FX_TYPE.BOTTOM
return Chan_FX_TYPE.UNKNOWN
def check_bi_included(self, bi):
included = False
if self.high > bi.high:
# high大于,low小于,左包含
if self.low < bi.low:
included = True
# high大于,low大于,不包含
else:
# if self.low > bi.low
# high相等,右包含
included = False
else:
included = False
if included:
if self.pre:
if self.high > self.pre.high and self.low < self.pre.low:
included = True
if included:
self.add_bi(bi)
# gn>gn-1
if self.dir == Chan_BI_DIR.DOWN:
# UP -> max(dn)
self.low = bi.low
else:
# DOWN -> min(gn)
self.high = bi.high
#self.print(bi, "Z")
return included
+60
View File
@@ -0,0 +1,60 @@
import copy
from typing import Dict, Optional
from ChanEnum import Chan_FX_TYPE, Chan_KLINE_DIR, Chan_SEG_DIR
import ChanKLU
import ChanCTime
from ChanBI import ChanBI
class ChanSEG():
def __init__(self, start_bi: ChanBI, index, ddir=Chan_SEG_DIR.UP):
self.start_bi = start_bi
self.end_bi = None
self.dir = ddir
self.low = 0
self.high = 0
if self.dir == Chan_SEG_DIR.UP and start_bi:
self.low = start_bi.low
else:
if start_bi:
self.high = start_bi.high
self.index = index
self.pre = None
self.next = None
self.bi_list = []
self.bi_list.append(start_bi)
self.is_sure = False
self.sure_time = None
self.macd_hist = 0
self.macd_div = 0
def set_macd_hist(self, macd_hist):
self.macd_hist = macd_hist
def set_macd_div(self, macd_div):
self.macd_div = macd_div
def set_end_bi(self, bi: ChanBI, sure_bi: ChanBI):
self.end_bi = bi
if bi:
if self.dir == Chan_SEG_DIR.UP:
self.high = bi.high
else:
self.low = bi.low
self.is_sure = True
if sure_bi.is_sure:
self.sure_time = sure_bi.end_klc.end_time
def pre_set_end_bi(self, bi: ChanBI):
self.end_bi = bi
if bi:
if self.dir == Chan_SEG_DIR.UP:
self.high = bi.high
else:
self.low = bi.low
def set_pre(self, seg):
self.pre = seg
def set_next(self, seg):
self.next = seg
def set_sure(self, sure_bi):
if sure_bi.is_sure:
self.sure_time = sure_bi.end_klc.end_time
self.is_sure = True
def add_bi(self, bi: ChanBI):
if len(self.bi_list) > 1:
self.bi_list.append(bi)
+66
View File
@@ -0,0 +1,66 @@
from typing import Dict, Optional
import ChanKLC, ChanSEG
import ChanCTime
from ChanEnum import Chan_ZS_DIR
# 中枢
class ChanZS():
def __init__(self, start_seg: ChanSEG, index, ddir: Chan_ZS_DIR):
self.start_klc = start_seg.start_bi.start_klc
self.start_time = self.start_klc.start_time
self.end_time = None
self.index = index
self.next = None
self.pre = None
self.start_seg = start_seg
self.seg_list = []
self.seg_list.append(start_seg)
self.end_seg = None
self.last_bi_in = None
self.bi_out = None
self.is_sure = False
self.zg = 0
self.zd = 0
self.dir = ddir
self.sure_time = None
self.end_klc = None
self.bi_out_count = 0
self.bi_out_list = []
self.bi_out_seg_list = []
self.bi_out_seg = None
def set_last_bi_in(self, last_bi_in):
self.last_bi_in = last_bi_in
def set_bi_out(self, bi_out, bi_out_seg):
if bi_out:
#print(bi_out.start_klc.start_time, bi_out.sure_time, bi_out.dir, bi_out_seg.dir, len(self.bi_out_list))
if len(self.bi_out_list) > 0:
last_bi = self.bi_out_list[-1]
if last_bi.index != bi_out.index:
self.bi_out_list.append(bi_out)
self.bi_out_seg_list.append(bi_out_seg)
else:
self.bi_out_list.append(bi_out)
self.bi_out_seg_list.append(bi_out_seg)
self.bi_out = bi_out
self.bi_out_seg = bi_out_seg
def set_end_klc(self, end_klc, sure_time, bi_out_count, seg):
self.end_klc = end_klc
self.set_end_time(end_klc.end_time)
self.is_sure = True
self.sure_time = sure_time
self.bi_out_count = bi_out_count
self.end_seg = seg
def set_end_seg(self, end_seg):
self.end_seg = end_seg
def set_pre(self, pre):
self.pre = pre
def set_next(self, next):
self.next = next
def set_end_time(self, end_time):
self.end_time = end_time
def add_klc(self, klc):
self.klc_list.append(klc)
def set_zg(self, zg):
self.zg = zg
def set_zd(self, zd):
self.zd = zd
+448
View File
@@ -0,0 +1,448 @@
import ccxt
import pandas as pd
import numpy as np
import mplfinance as mpf
from talib import MACD, SMA
from datetime import datetime, timedelta
import logging
import datetime as dt
# Configure logging
logging.basicConfig(
filename='chanlun_trading.log',
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
# Configuration (user to modify)
BINANCE_API_KEY = 'your_api_key' # Replace with your Binance API key
BINANCE_API_SECRET = 'your_api_secret' # Replace with your Binance API secret
SIMULATION_MODE = True # Set to False for live trading
# 1. Fetch K-line data from Binance (multi-timeframe support)
def fetch_binance_data(symbol='BTC/USDT', timeframe='5m', limit=500):
try:
exchange = ccxt.binance({
'apiKey': BINANCE_API_KEY if not SIMULATION_MODE else '',
'secret': BINANCE_API_SECRET if not SIMULATION_MODE else '',
'enableRateLimit': True,
'options': {'defaultType': 'spot'}
})
since = exchange.parse8601((datetime.now(dt.UTC) - timedelta(days=7)).isoformat())
ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since, limit)
df = pd.DataFrame(ohlcv, columns=['Date', 'Open', 'High', 'Low', 'Close', 'Volume'])
df['Date'] = pd.to_datetime(df['Date'], unit='ms')
df.set_index('Date', inplace=True)
logging.info(f"Fetched {len(df)} K-lines for {symbol} ({timeframe})")
return df
except Exception as e:
logging.error(f"Failed to fetch data: {e}")
raise
# 2. K-line merging (vectorized)
def merge_kline(df):
try:
df = df.copy()
merged_data = []
trend = np.sign(df['Close'].diff().shift(-1)) # 1: up, -1: down, 0: neutral
# Detect inclusion
is_included = ((df['High'].shift(-1) <= df['High']) & (df['Low'].shift(-1) >= df['Low'])) | \
((df['High'].shift(-1) >= df['High']) & (df['Low'].shift(-1) <= df['Low']))
i = 0
while i < len(df) - 1:
if is_included.iloc[i]:
current_k = df.iloc[i]
next_k = df.iloc[i + 1]
high = max(current_k['High'], next_k['High'])
low = min(current_k['Low'], next_k['Low'])
open_price = current_k['Open']
close_price = next_k['Close'] if trend.iloc[i] >= 0 else next_k['Close']
volume = current_k['Volume'] + next_k['Volume']
merged_data.append({
'Date': next_k.name,
'Open': open_price,
'High': high,
'Low': low,
'Close': close_price,
'Volume': volume
})
i += 2
else:
current_k = df.iloc[i]
merged_data.append({
'Date': current_k.name,
'Open': current_k['Open'],
'High': current_k['High'],
'Low': current_k['Low'],
'Close': current_k['Close'],
'Volume': current_k['Volume']
})
i += 1
if i == len(df) - 1:
last_k = df.iloc[i]
merged_data.append({
'Date': last_k.name,
'Open': last_k['Open'],
'High': last_k['High'],
'Low': last_k['Low'],
'Close': last_k['Close'],
'Volume': last_k['Volume']
})
merged_df = pd.DataFrame(merged_data)
merged_df['Date'] = pd.to_datetime(merged_df['Date'])
merged_df.set_index('Date', inplace=True)
logging.info(f"Merged K-lines: {len(df)} -> {len(merged_df)}")
return merged_df
except Exception as e:
logging.error(f"K-line merging failed: {e}")
raise
# 3. Detect fractals (vectorized)
def detect_fractals(df):
try:
df = df.copy()
df['is_top'] = (df['High'] > df['High'].shift(1)) & (df['High'] > df['High'].shift(-1)) & \
(df['High'] > df['High'].shift(2)) & (df['High'] > df['High'].shift(-2))
df['is_bottom'] = (df['Low'] < df['Low'].shift(1)) & (df['Low'] < df['Low'].shift(-1)) & \
(df['Low'] < df['Low'].shift(2)) & (df['Low'] < df['Low'].shift(-2))
df['is_top'] = df['is_top'].fillna(False)
df['is_bottom'] = df['is_bottom'].fillna(False)
logging.info(f"Detected {df['is_top'].sum()} top fractals and {df['is_bottom'].sum()} bottom fractals")
return df
except Exception as e:
logging.error(f"Fractal detection failed: {e}")
raise
# 4. Detect strokes
def detect_strokes(df):
try:
strokes = []
last_fractal = None
last_price = None
last_index = None
for i in range(len(df)):
if df['is_top'].iloc[i] or df['is_bottom'].iloc[i]:
current_fractal = 'top' if df['is_top'].iloc[i] else 'bottom'
current_price = df['High'].iloc[i] if current_fractal == 'top' else df['Low'].iloc[i]
if last_fractal is None:
last_fractal = current_fractal
last_price = current_price
last_index = df.index[i]
continue
if (last_fractal == 'top' and current_fractal == 'bottom' and current_price < last_price) or \
(last_fractal == 'bottom' and current_fractal == 'top' and current_price > last_price):
strokes.append({
'start_time': last_index,
'end_time': df.index[i],
'start_price': last_price,
'end_price': current_price,
'type': 'down' if current_fractal == 'bottom' else 'up',
'volume': df['Volume'].loc[last_index:df.index[i]].sum()
})
last_fractal = current_fractal
last_price = current_price
last_index = df.index[i]
logging.info(f"Detected {len(strokes)} strokes")
return strokes
except Exception as e:
logging.error(f"Stroke detection failed: {e}")
raise
# 5. Detect segments
def detect_segments(strokes):
try:
segments = []
if len(strokes) < 3:
return segments
i = 0
while i < len(strokes) - 2:
stroke1, stroke2, stroke3 = strokes[i], strokes[i+1], strokes[i+2]
if stroke1['type'] == 'up' and stroke2['type'] == 'down' and stroke3['type'] == 'up':
if stroke3['end_price'] > stroke1['end_price']:
segments.append({
'start_time': stroke1['start_time'],
'end_time': stroke3['end_time'],
'start_price': stroke1['start_price'],
'end_price': stroke3['end_price'],
'type': 'up'
})
i += 3
else:
i += 1
elif stroke1['type'] == 'down' and stroke2['type'] == 'up' and stroke3['type'] == 'down':
if stroke3['end_price'] < stroke1['end_price']:
segments.append({
'start_time': stroke1['start_time'],
'end_time': stroke3['end_time'],
'start_price': stroke1['start_price'],
'end_price': stroke3['end_price'],
'type': 'down'
})
i += 3
else:
i += 1
else:
i += 1
logging.info(f"Detected {len(segments)} segments")
return segments
except Exception as e:
logging.error(f"Segment detection failed: {e}")
raise
# 6. Detect pivots (midlines)
def detect_pivots(strokes):
try:
pivots = []
if len(strokes) < 3:
return pivots
for i in range(len(strokes) - 2):
s1, s2, s3 = strokes[i:i+3]
high = min(s1['start_price'], s1['end_price'], s2['start_price'], s2['end_price'],
s3['start_price'], s3['end_price'])
low = max(s1['start_price'], s1['end_price'], s2['start_price'], s2['end_price'],
s3['start_price'], s3['end_price'])
if high > low:
pivots.append({
'start_time': s1['start_time'],
'end_time': s3['end_time'],
'high': high,
'low': low
})
logging.info(f"Detected {len(pivots)} pivots")
return pivots
except Exception as e:
logging.error(f"Pivot detection failed: {e}")
raise
# 7. Analyze higher timeframe (30m)
def analyze_higher_timeframe(df_30m):
try:
df_30m = detect_fractals(df_30m)
strokes_30m = detect_strokes(df_30m)
if not strokes_30m:
return 'neutral'
last_stroke = strokes_30m[-1]
logging.info(f"30m trend: {last_stroke['type']}")
return last_stroke['type']
except Exception as e:
logging.error(f"Higher timeframe analysis failed: {e}")
raise
# 8. Back-divergence detection (enhanced)
def detect_back_divergence(df, strokes, higher_trend):
try:
macd, signal, hist = MACD(df['Close'], fastperiod=12, slowperiod=26, signalperiod=9)
sma20 = SMA(df['Close'], timeperiod=20)
df['macd'] = macd
df['hist'] = hist
df['sma20'] = sma20
df['buy_signal'] = False
df['sell_signal'] = False
stroke_metrics = []
for stroke in strokes:
start_idx = df.index.get_loc(stroke['start_time'])
end_idx = df.index.get_loc(stroke['end_time'])
hist_segment = df['hist'].iloc[start_idx:end_idx+1]
price_change = abs(stroke['end_price'] - stroke['start_price'])
hist_area = sum(abs(h) for h in hist_segment if not np.isnan(h))
volume = stroke['volume']
stroke_metrics.append({
'start_time': stroke['start_time'],
'end_time': stroke['end_time'],
'type': stroke['type'],
'price_change': price_change,
'hist_area': hist_area,
'volume': volume
})
for i in range(2, len(stroke_metrics)):
current_stroke = stroke_metrics[i]
prev_stroke = stroke_metrics[i-2]
if current_stroke['type'] != prev_stroke['type']:
continue
current_end_idx = df.index.get_loc(current_stroke['end_time'])
# Uptrend back-divergence (sell signal)
if current_stroke['type'] == 'up':
price_increase = df['High'].loc[current_stroke['end_time']] > df['High'].loc[prev_stroke['end_time']]
hist_decrease = current_stroke['hist_area'] < prev_stroke['hist_area']
volume_decrease = current_stroke['volume'] < prev_stroke['volume']
is_top_fractal = df['is_top'].loc[current_stroke['end_time']]
hist_positive = df['hist'].iloc[current_end_idx] > 0 or \
(df['hist'].iloc[current_end_idx] < 0 and df['hist'].iloc[current_end_idx-1] > 0)
sma_trend = df['Close'].iloc[current_end_idx] > df['sma20'].iloc[current_end_idx]
trend_match = higher_trend in ['up', 'neutral']
if price_increase and hist_decrease and volume_decrease and is_top_fractal and \
hist_positive and sma_trend and trend_match:
df.loc[df.index[current_end_idx], 'sell_signal'] = True
# Downtrend back-divergence (buy signal)
elif current_stroke['type'] == 'down':
price_decrease = df['Low'].loc[current_stroke['end_time']] < df['Low'].loc[prev_stroke['end_time']]
hist_decrease = current_stroke['hist_area'] < prev_stroke['hist_area']
volume_decrease = current_stroke['volume'] < prev_stroke['volume']
is_bottom_fractal = df['is_bottom'].loc[current_stroke['end_time']]
hist_negative = df['hist'].iloc[current_end_idx] < 0 or \
(df['hist'].iloc[current_end_idx] > 0 and df['hist'].iloc[current_end_idx-1] < 0)
sma_trend = df['Close'].iloc[current_end_idx] < df['sma20'].iloc[current_end_idx]
trend_match = higher_trend in ['down', 'neutral']
if price_decrease and hist_decrease and volume_decrease and is_bottom_fractal and \
hist_negative and sma_trend and trend_match:
df.loc[df.index[current_end_idx], 'buy_signal'] = True
logging.info(f"Detected {df['buy_signal'].sum()} buy signals and {df['sell_signal'].sum()} sell signals")
return df
except Exception as e:
logging.error(f"Back-divergence detection failed: {e}")
raise
# 9. Execute trade
def execute_trade(exchange, symbol, signal, amount=0.001):
try:
if SIMULATION_MODE:
msg = f"[SIMULATION] {'Buy' if signal == 'buy' else 'Sell'} {amount} {symbol} at {datetime.now(dt.UTC)}"
print(msg)
logging.info(msg)
return
if signal == 'buy':
order = exchange.create_market_buy_order(symbol, amount)
msg = f"Buy order executed: {order}"
print(msg)
logging.info(msg)
elif signal == 'sell':
order = exchange.create_market_sell_order(symbol, amount)
msg = f"Sell order executed: {order}"
print(msg)
logging.info(msg)
except Exception as e:
msg = f"Trade execution failed: {e}"
print(msg)
logging.error(msg)
# 10. Plot chart
def plot_chart(df, strokes, segments, pivots):
try:
# Initialize additional plots
apds = []
alines = [] # For line segments
# Plot strokes as line segments
for stroke in strokes:
alines.append([(stroke['start_time'], stroke['start_price']),
(stroke['end_time'], stroke['end_price'])])
# Plot segments as line segments
for segment in segments:
alines.append([(segment['start_time'], segment['start_price']),
(segment['end_time'], segment['end_price'])])
# Plot pivots as horizontal lines
for pivot in pivots:
alines.append([(pivot['start_time'], pivot['high']),
(pivot['end_time'], pivot['high'])])
alines.append([(pivot['start_time'], pivot['low']),
(pivot['end_time'], pivot['low'])])
# Add alines to plot (single color for simplicity, can customize)
if alines:
apds.append(mpf.make_addplot(
None, # No y-data needed for alines
alines=alines,
type='line',
color=['blue' if i < len(strokes) else 'purple' if i < len(strokes) + len(segments) else 'orange'
for i in range(len(alines))],
linestyle=['--' if i < len(strokes) else '-' if i < len(strokes) + len(segments) else ':'
for i in range(len(alines))]
))
# Plot buy/sell signals
buy_signals = df[df['buy_signal']]['Close']
sell_signals = df[df['sell_signal']]['Close']
apds.append(mpf.make_addplot(buy_signals, type='scatter', markersize=100, marker='^', color='green'))
apds.append(mpf.make_addplot(sell_signals, type='scatter', markersize=100, marker='v', color='red'))
# Plot K-line chart
mpf.plot(df, type='candle', addplot=apds, title='Chanlun Advanced Analysis', style='yahoo')
logging.info("Chart plotted successfully")
except Exception as e:
logging.error(f"Chart plotting failed: {e}")
raise
# 11. Main function
def main():
try:
# Initialize exchange
exchange = ccxt.binance({
'apiKey': BINANCE_API_KEY if not SIMULATION_MODE else '',
'secret': BINANCE_API_SECRET if not SIMULATION_MODE else '',
'enableRateLimit': True,
'options': {'defaultType': 'spot'}
})
# Fetch data
df_5m = fetch_binance_data(symbol='BTC/USDT', timeframe='5m', limit=500)
df_30m = fetch_binance_data(symbol='BTC/USDT', timeframe='30m', limit=200)
# Merge 5m K-lines
df_5m = merge_kline(df_5m)
# Detect fractals, strokes, segments, pivots
df_5m = detect_fractals(df_5m)
strokes = detect_strokes(df_5m)
segments = detect_segments(strokes)
pivots = detect_pivots(strokes)
# Analyze 30m trend
higher_trend = analyze_higher_timeframe(df_30m)
print(f"30m Trend: {higher_trend}")
# Detect back-divergence
df_5m = detect_back_divergence(df_5m, strokes, higher_trend)
# Plot chart
plot_chart(df_5m, strokes, segments, pivots)
# Output and execute trades
print("Buy Signals:")
buy_signals = df_5m[df_5m['buy_signal']][['Close']]
print(buy_signals)
for idx, row in buy_signals.iterrows():
execute_trade(exchange, 'BTC/USDT', 'buy', amount=0.001)
print("Sell Signals:")
sell_signals = df_5m[df_5m['sell_signal']][['Close']]
print(sell_signals)
for idx, row in sell_signals.iterrows():
execute_trade(exchange, 'BTC/USDT', 'sell', amount=0.001)
logging.info("Main function completed successfully")
except Exception as e:
logging.error(f"Main function failed: {e}")
raise
if __name__ == "__main__":
main()
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+13
View File
@@ -0,0 +1,13 @@
1. **第一类买卖点**
- 定义:趋势反转的起始点,即在下跌趋势结束时形成的买点(第一类买点),或在上涨趋势结束时形成的卖点(第一类卖点)。这是市场多空力量发生根本性转变的位置。
- 与MACD背驰的关系:Macd背驰是指出中枢后形成的Macd的红绿柱面积比进入中枢时的面积绝对值小,背驰比较的黄白线和柱子面积都在0轴的一个方向上。第一类买点都是在0轴之下背驰形成的,第一类卖点都是在0轴之上的背驰形成的。
2. **第二类买卖点**
- 定义:趋势确认后的回调点。在第一类买卖点之后,价格会回调或反弹,形成第二类买点(回调不破前低)或第二类卖点(反弹不破前高),是对第一类买卖点的确认。第二类买点都是第一次上0轴后回抽确认形成的。第二类卖点都是第一次0轴之下上涨确认形成的。第二类买卖点只会在趋势确认后,第一类买卖点出现之后出现一次,不会重复出现,除非趋势反转之后。
3. **第三类买卖点**
- 定义:趋势延续的确认点。价格突破回调或反弹的中枢区间后,回踩不破关键位置(如中枢上沿或下沿),形成第三类买点(上升趋势延续)或第三类卖点(下降趋势延续)。第三类买卖点只会在中枢确认之后出现。
我们交易的是币安的比特币合约, 数据格式是json, 数据包括现有的持仓, 仓位历史, 账户余额, 你用缠论分析之后, 给出以下分析, 最近的一个中枢在哪里,现在的趋势是什么,现在是否是买卖点,如果是,是那一类买卖点,应该进行何种操作。
+39
View File
@@ -0,0 +1,39 @@
2025-04-18 20:25:03,767 - INFO - Fetched 500 K-lines for BTC/USDT (5m)
2025-04-18 20:25:06,074 - INFO - Fetched 200 K-lines for BTC/USDT (30m)
2025-04-18 20:25:06,104 - INFO - Merged K-lines: 500 -> 404
2025-04-18 20:25:06,105 - INFO - Detected 49 top fractals and 48 bottom fractals
2025-04-18 20:25:06,111 - INFO - Detected 80 strokes
2025-04-18 20:25:06,111 - INFO - Detected 22 segments
2025-04-18 20:25:06,111 - INFO - Detected 0 pivots
2025-04-18 20:25:06,112 - INFO - Detected 27 top fractals and 29 bottom fractals
2025-04-18 20:25:06,115 - INFO - Detected 45 strokes
2025-04-18 20:25:06,115 - INFO - 30m trend: down
2025-04-18 20:25:06,119 - INFO - Detected 4 buy signals and 0 sell signals
2025-04-18 20:25:06,233 - ERROR - Chart plotting failed: x and y must have same first dimension, but have shapes (404,) and (2,)
2025-04-18 20:25:06,233 - ERROR - Main function failed: x and y must have same first dimension, but have shapes (404,) and (2,)
2025-04-18 20:27:05,455 - INFO - Fetched 500 K-lines for BTC/USDT (5m)
2025-04-18 20:27:08,491 - INFO - Fetched 200 K-lines for BTC/USDT (30m)
2025-04-18 20:27:08,521 - INFO - Merged K-lines: 500 -> 404
2025-04-18 20:27:08,522 - INFO - Detected 49 top fractals and 48 bottom fractals
2025-04-18 20:27:08,528 - INFO - Detected 80 strokes
2025-04-18 20:27:08,528 - INFO - Detected 22 segments
2025-04-18 20:27:08,528 - INFO - Detected 0 pivots
2025-04-18 20:27:08,529 - INFO - Detected 27 top fractals and 29 bottom fractals
2025-04-18 20:27:08,532 - INFO - Detected 45 strokes
2025-04-18 20:27:08,532 - INFO - 30m trend: down
2025-04-18 20:27:08,537 - INFO - Detected 4 buy signals and 0 sell signals
2025-04-18 20:27:08,647 - ERROR - Chart plotting failed: x and y must have same first dimension, but have shapes (404,) and (2,)
2025-04-18 20:27:08,647 - ERROR - Main function failed: x and y must have same first dimension, but have shapes (404,) and (2,)
2025-04-18 20:28:51,970 - INFO - Fetched 500 K-lines for BTC/USDT (5m)
2025-04-18 20:28:53,861 - INFO - Fetched 200 K-lines for BTC/USDT (30m)
2025-04-18 20:28:53,897 - INFO - Merged K-lines: 500 -> 404
2025-04-18 20:28:53,898 - INFO - Detected 49 top fractals and 48 bottom fractals
2025-04-18 20:28:53,904 - INFO - Detected 80 strokes
2025-04-18 20:28:53,904 - INFO - Detected 22 segments
2025-04-18 20:28:53,904 - INFO - Detected 0 pivots
2025-04-18 20:28:53,905 - INFO - Detected 27 top fractals and 29 bottom fractals
2025-04-18 20:28:53,908 - INFO - Detected 45 strokes
2025-04-18 20:28:53,908 - INFO - 30m trend: down
2025-04-18 20:28:53,913 - INFO - Detected 4 buy signals and 0 sell signals
2025-04-18 20:28:53,913 - ERROR - Chart plotting failed: Wrong type for data, in make_addplot()
2025-04-18 20:28:53,913 - ERROR - Main function failed: Wrong type for data, in make_addplot()
BIN
View File
Binary file not shown.
+2
View File
@@ -0,0 +1,2 @@
pip install -r requirements.txt
python app.py
+539
View File
@@ -0,0 +1,539 @@
from flask import Flask, render_template, jsonify, request
import ccxt
import pandas as pd
from datetime import datetime, timedelta
import sys
import os
import matplotlib
matplotlib.use('Agg') # 设置使用非GUI后端,必须在导入pyplot之前设置
import matplotlib.pyplot as plt
import io
import base64
import time
import traceback
from pytz import timezone
# 添加父目录到系统路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from ChanLun import ChanLun
from ChanEnum import Chan_BI_DIR, Chan_SEG_DIR
# 添加买卖点枚举类型
class TRADE_POINT_TYPE:
BUY1 = 1 # 一类买点
BUY2 = 2 # 二类买点
BUY3 = 3 # 三类买点
SELL1 = -1 # 一类卖点
SELL2 = -2 # 二类卖点
SELL3 = -3 # 三类卖点
app = Flask(__name__)
# 初始化交易所
exchange = ccxt.binance({
'enableRateLimit': True,
})
# 时间周期映射
TIMEFRAMES = {
'1m': '1分钟',
'5m': '5分钟',
'15m': '15分钟',
'30m': '30分钟',
'1h': '1小时',
'4h': '4小时',
'1d': '日线',
'1w': '周线',
'1M': '月线',
}
# 常见交易对
SYMBOLS = [
'SOL/USDT:USDT', 'BTC/USDT:USDT', 'ETH/USDT:USDT', 'BNB/USDT:USDT', 'XRP/USDT:USDT',
'ADA/USDT:USDT', 'DOGE/USDT:USDT', 'AVAX/USDT:USDT', 'DOT/USDT:USDT', 'MATIC/USDT:USDT'
]
def get_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time=None):
"""获取K线数据,支持分页加载确保获取指定时间范围内的所有数据"""
try:
# 初始化参数
since = None
if start_time:
try:
since = int(start_time)
except ValueError:
print(f"无效的起始时间: {start_time}")
# 结束时间处理
until = None
if end_time:
try:
until = int(end_time)
except ValueError:
print(f"无效的结束时间: {end_time}")
# 初始化存储所有K线数据的列表
all_ohlcv = []
# 初始化当前查询的开始时间
current_since = since
# 分页加载数据
while True:
print(f"获取数据: {symbol}, {timeframe}, limit={limit}, since={current_since}")
# 获取当前页的数据
ohlcv = exchange.fetch_ohlcv(symbol, timeframe, since=current_since, limit=limit)
# 如果没有获取到数据,结束循环
if not ohlcv or len(ohlcv) == 0:
break
# 将获取到的数据添加到总列表中
all_ohlcv.extend(ohlcv)
# 获取最后一条数据的时间戳
last_timestamp = ohlcv[-1][0]
# 如果已达到结束时间,结束循环
if until and last_timestamp >= until:
break
# 如果获取的数据条数小于限制数,说明已经获取完所有数据
if len(ohlcv) < limit:
break
# 更新下一页的开始时间(加1毫秒避免重复)
current_since = last_timestamp + 1
# 防止API请求过于频繁
time.sleep(0.5) # 等待0.5秒
# 数据为空的情况
if not all_ohlcv or len(all_ohlcv) == 0:
print(f"未获取到数据: {symbol}, {timeframe}")
return None
# 转换为DataFrame
df = pd.DataFrame(all_ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
df['date'] = pd.to_datetime(df['timestamp'], unit='ms').dt.tz_localize('UTC').dt.tz_convert('Asia/Shanghai')
# 在客户端进行结束时间过滤
if until:
df = df[df['timestamp'] <= until]
# 去除重复数据
df = df.drop_duplicates(subset=['timestamp'])
# 按时间排序
df = df.sort_values('timestamp')
# 如果过滤后没有数据,返回None
if len(df) == 0:
print("过滤后无数据")
return None
print(f"获取到总共 {len(df)} 条数据")
return df
except Exception as e:
print(f"获取数据错误: {e}")
traceback.print_exc()
return None
def calculate_macd(df):
"""计算MACD指标"""
exp1 = df['close'].ewm(span=12, adjust=False).mean()
exp2 = df['close'].ewm(span=26, adjust=False).mean()
macd = exp1 - exp2
signal = macd.ewm(span=9, adjust=False).mean()
histogram = macd - signal
return {
'macd': macd.tolist(),
'signal': signal.tolist(),
'histogram': histogram.tolist()
}
def analyze_chan(df):
"""进行缠论分析"""
chan = ChanLun()
# 获取分析结果
klc_list = chan.get_klc_list(df)
bi_list = chan.cal_bi_list(klc_list)
seg_list = chan.get_seg_list(bi_list)
zs_list = chan.calculate_zs(bi_list, seg_list)
# 获取笔中枢列表
bi_zs_list = chan.get_bi_zs_list(bi_list)
# 添加买卖点识别
buy_sell_points = identify_trade_points(bi_list, seg_list, zs_list)
return {
'klc_list': klc_list,
'bi_list': bi_list,
'seg_list': seg_list,
'zs_list': zs_list,
'bi_zs_list': bi_zs_list, # 添加笔中枢数据
'trade_points': buy_sell_points
}
def identify_trade_points(bi_list, seg_list, zs_list):
"""识别缠论买卖点 - 只保留最重要的一类买卖点,减少标记干扰"""
trade_points = []
# 输出调试信息
print(f"识别买卖点:总共 {len(bi_list)} 个笔, {len(seg_list)} 个线段, {len(zs_list)} 个中枢")
# 只识别一类买卖点:线段向上或向下突破
if len(seg_list) >= 3:
for i in range(2, len(seg_list)):
# 确保线段已完成
if seg_list[i].end_bi and seg_list[i-1].end_bi and seg_list[i-2].end_bi:
# 一类买点:向下-向上-向下的底分型,第三段结束点为买点
if (convert_direction(seg_list[i-2].dir) == -1 and
convert_direction(seg_list[i-1].dir) == 1 and
convert_direction(seg_list[i].dir) == -1):
print(f"发现一类买点:线段方向 {convert_direction(seg_list[i-2].dir)}-{convert_direction(seg_list[i-1].dir)}-{convert_direction(seg_list[i].dir)}")
trade_points.append({
'type': TRADE_POINT_TYPE.BUY1,
'time': seg_list[i].end_bi.end_klc.end_time,
'price': seg_list[i].end_bi.end_klc.low,
'desc': '一类买点'
})
# 一类卖点:向上-向下-向上的顶分型,第三段结束点为卖点
if (convert_direction(seg_list[i-2].dir) == 1 and
convert_direction(seg_list[i-1].dir) == -1 and
convert_direction(seg_list[i].dir) == 1):
print(f"发现一类卖点:线段方向 {convert_direction(seg_list[i-2].dir)}-{convert_direction(seg_list[i-1].dir)}-{convert_direction(seg_list[i].dir)}")
trade_points.append({
'type': TRADE_POINT_TYPE.SELL1,
'time': seg_list[i].end_bi.end_klc.end_time,
'price': seg_list[i].end_bi.end_klc.high,
'desc': '一类卖点'
})
print(f"总共识别出 {len(trade_points)} 个买卖点")
return trade_points
# 辅助函数,转换缠论方向枚举为整数
def convert_direction(direction):
"""将缠论方向枚举转换为整数"""
if direction == Chan_BI_DIR.UP:
return 1
elif direction == Chan_BI_DIR.DOWN:
return -1
elif direction == Chan_SEG_DIR.UP:
return 1
elif direction == Chan_SEG_DIR.DOWN:
return -1
else:
return 0
def format_time_safely(time_obj, client_tz):
"""安全地格式化时间对象,处理字符串和datetime两种情况"""
if time_obj is None:
return None
if isinstance(time_obj, str):
# 尝试将字符串解析为datetime
try:
from dateutil import parser
time_obj = parser.parse(time_obj)
return time_obj.astimezone(client_tz).isoformat()
except:
return time_obj
else:
# 已经是datetime对象
return time_obj.astimezone(client_tz).isoformat()
def is_smaller_timeframe(tf1, tf2):
"""判断时间周期tf1是否小于tf2"""
# 定义时间周期的分钟数映射
tf_values = {
'1m': 1,
'3m': 3,
'5m': 5,
'15m': 15,
'30m': 30,
'1h': 60,
'2h': 120,
'4h': 240,
'6h': 360,
'8h': 480,
'12h': 720,
'1d': 1440,
'3d': 4320,
'1w': 10080,
'1M': 43200
}
# 获取时间周期对应的分钟数
tf1_value = tf_values.get(tf1)
tf2_value = tf_values.get(tf2)
# 如果某个时间周期不在映射中,返回False
if tf1_value is None or tf2_value is None:
return False
# 返回tf1是否小于tf2
return tf1_value < tf2_value
def is_smaller_or_equal_timeframe(tf1, tf2):
"""判断时间周期tf1是否小于等于tf2"""
# 定义时间周期的分钟数映射
tf_values = {
'1m': 1,
'3m': 3,
'5m': 5,
'15m': 15,
'30m': 30,
'1h': 60,
'2h': 120,
'4h': 240,
'6h': 360,
'8h': 480,
'12h': 720,
'1d': 1440,
'3d': 4320,
'1w': 10080,
'1M': 43200
}
# 获取时间周期对应的分钟数
tf1_value = tf_values.get(tf1)
tf2_value = tf_values.get(tf2)
# 如果某个时间周期不在映射中,返回False
if tf1_value is None or tf2_value is None:
return False
# 返回tf1是否小于等于tf2
return tf1_value <= tf2_value
@app.route('/')
def index():
"""主页"""
return render_template('index.html', timeframes=TIMEFRAMES, symbols=SYMBOLS)
@app.route('/api/analyze')
def analyze():
"""分析接口"""
symbol = request.args.get('symbol', 'SOL/USDT:USDT')
timeframe = request.args.get('timeframe', '5m')
# 验证交易对不为空
if not symbol or symbol.strip() == '':
print(f"错误: 空交易对")
return jsonify({'error': '交易对不能为空'})
# 获取时间范围参数
start_time = request.args.get('start_time')
end_time = request.args.get('end_time')
# 获取客户端请求的时区
client_timezone = request.args.get('timezone', 'Asia/Shanghai')
# 获取分形元素时间周期
element_timeframe = request.args.get('element_timeframe')
# 获取是否只需要分形元素数据的参数
elements_only_param = request.args.get('elements_only')
elements_only = elements_only_param == 'true'
print(f"API请求参数: symbol={symbol}, timeframe={timeframe}, element_timeframe={element_timeframe}")
print(f"时间范围: start_time={start_time}, end_time={end_time}")
print(f"elements_only参数: 原始值={elements_only_param}, 处理后={elements_only}")
# 验证小周期是否小于主周期
if element_timeframe and not is_smaller_or_equal_timeframe(element_timeframe, timeframe):
print(f"错误: 元素周期 {element_timeframe} 大于主周期 {timeframe}")
return jsonify({'error': '分形元素时间周期必须小于或等于主图表时间周期'})
# 获取数据
df = get_kl_data(symbol, timeframe, start_time=start_time, end_time=end_time)
if df is None:
print(f"错误: 获取数据失败 - symbol={symbol}, timeframe={timeframe}")
return jsonify({'error': '获取数据失败'})
if len(df) == 0:
print(f"错误: 所选时间范围内没有数据 - symbol={symbol}, timeframe={timeframe}")
return jsonify({'error': '所选时间范围内没有数据'})
# 使用客户端指定的时区
client_tz = timezone(client_timezone)
# 如果只需要分形元素数据而不需要主周期数据,则初始化一个空结果
result = {
'timezone': client_timezone
}
# 如果不是只需要分形元素数据,则添加主周期数据
if not elements_only:
print(f"处理主周期数据 (elements_only={elements_only})")
# 进行缠论分析
analysis_result = analyze_chan(df)
# 计算MACD
macd_data = calculate_macd(df)
# 添加主周期分析结果到返回数据
result.update({
'kline_data': df.to_dict('records'),
'bi_list': [{
'start_time': bi.start_klc.start_time if isinstance(bi.start_klc.start_time, str) else bi.start_klc.start_time.astimezone(client_tz).isoformat(),
'end_time': (bi.end_klc.end_time if isinstance(bi.end_klc.end_time, str) else bi.end_klc.end_time.astimezone(client_tz).isoformat()) if bi.end_klc else None,
'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high,
'end_price': bi.end_klc.high if convert_direction(bi.dir) == 1 else bi.end_klc.low if bi.end_klc else None,
'direction': convert_direction(bi.dir)
} for bi in analysis_result['bi_list'] if bi.end_klc],
'seg_list': [{
'start_time': seg.start_bi.start_klc.start_time if isinstance(seg.start_bi.start_klc.start_time, str) else seg.start_bi.start_klc.start_time.astimezone(client_tz).isoformat(),
'end_time': (seg.end_bi.end_klc.end_time if isinstance(seg.end_bi.end_klc.end_time, str) else seg.end_bi.end_klc.end_time.astimezone(client_tz).isoformat()) if seg.end_bi else None,
'start_price': seg.start_bi.start_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.start_klc.high,
'end_price': seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low if seg.end_bi else None,
'direction': convert_direction(seg.dir)
} for seg in analysis_result['seg_list'] if seg.end_bi],
'zs_list': [{
'start_time': zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat(),
'end_time': (zs.end_klc.end_time if isinstance(zs.end_klc.end_time, str) else zs.end_klc.end_time.astimezone(client_tz).isoformat()) if zs.end_klc else None,
'zg': zs.zg,
'zd': zs.zd,
'is_sure': zs.is_sure, # 添加中枢是否完成的标志
'type': getattr(zs, 'type', 'SEG_ZS') # 中枢类型,默认为线段中枢
} for zs in analysis_result['zs_list'] if zs.end_klc],
# 添加笔中枢列表
'bi_zs_list': [{
'start_time': zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat(),
'end_time': (zs.end_klc.end_time if isinstance(zs.end_klc.end_time, str) else zs.end_klc.end_time.astimezone(client_tz).isoformat()) if zs.end_klc else None,
'zg': zs.zg,
'zd': zs.zd,
'is_sure': zs.is_sure, # 添加中枢是否完成的标志
'type': 'BI_ZS' # 标记为笔中枢
} for zs in analysis_result['bi_zs_list'] if zs.end_klc],
# 添加未完成中枢列表
'uncompleted_zs_list': [{
'start_time': zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat(),
'end_time': None, # 未完成中枢没有结束时间
'zg': zs.zg,
'zd': zs.zd,
'is_sure': zs.is_sure, # 未完成中枢的is_sure为False
'type': getattr(zs, 'type', 'SEG_ZS') # 中枢类型,默认为线段中枢
} for zs in analysis_result['zs_list'] if not zs.is_sure],
# 添加未完成笔中枢列表
'uncompleted_bi_zs_list': [{
'start_time': zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat(),
'end_time': None, # 未完成中枢没有结束时间
'zg': zs.zg,
'zd': zs.zd,
'is_sure': zs.is_sure, # 未完成中枢的is_sure为False
'type': 'BI_ZS' # 标记为笔中枢
} for zs in analysis_result['bi_zs_list'] if not zs.is_sure],
'trade_points': [{
'type': point['type'],
'time': format_time_safely(point['time'], client_tz),
'price': point['price'],
'desc': point['desc']
} for point in analysis_result['trade_points']],
'macd': macd_data
})
else:
print(f"只请求元素数据,跳过主周期数据处理 (elements_only={elements_only})")
# 如果有指定分形元素时间周期,获取小周期数据
if element_timeframe:
print(f"处理元素周期数据: {element_timeframe}")
# 获取小周期数据,使用与主周期相同的时间范围
element_df = get_kl_data(symbol, element_timeframe, start_time=start_time, end_time=end_time)
if element_df is not None and len(element_df) > 0:
# 对小周期数据进行缠论分析
element_analysis = analyze_chan(element_df)
# 添加小周期分析结果到返回数据
result['element_timeframe'] = element_timeframe
result['element_bi_list'] = [{
'start_time': bi.start_klc.start_time if isinstance(bi.start_klc.start_time, str) else bi.start_klc.start_time.astimezone(client_tz).isoformat(),
'end_time': (bi.end_klc.end_time if isinstance(bi.end_klc.end_time, str) else bi.end_klc.end_time.astimezone(client_tz).isoformat()) if bi.end_klc else None,
'start_price': bi.start_klc.low if convert_direction(bi.dir) == 1 else bi.start_klc.high,
'end_price': bi.end_klc.high if convert_direction(bi.dir) == 1 else bi.end_klc.low if bi.end_klc else None,
'direction': convert_direction(bi.dir)
} for bi in element_analysis['bi_list'] if bi.end_klc]
result['element_seg_list'] = [{
'start_time': seg.start_bi.start_klc.start_time if isinstance(seg.start_bi.start_klc.start_time, str) else seg.start_bi.start_klc.start_time.astimezone(client_tz).isoformat(),
'end_time': (seg.end_bi.end_klc.end_time if isinstance(seg.end_bi.end_klc.end_time, str) else seg.end_bi.end_klc.end_time.astimezone(client_tz).isoformat()) if seg.end_bi else None,
'start_price': seg.start_bi.start_klc.low if convert_direction(seg.dir) == 1 else seg.start_bi.start_klc.high,
'end_price': seg.end_bi.end_klc.high if convert_direction(seg.dir) == 1 else seg.end_bi.end_klc.low if seg.end_bi else None,
'direction': convert_direction(seg.dir)
} for seg in element_analysis['seg_list'] if seg.end_bi]
result['element_zs_list'] = [{
'start_time': zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat(),
'end_time': (zs.end_klc.end_time if isinstance(zs.end_klc.end_time, str) else zs.end_klc.end_time.astimezone(client_tz).isoformat()) if zs.end_klc else None,
'zg': zs.zg,
'zd': zs.zd,
'is_sure': zs.is_sure, # 添加中枢是否完成的标志
'type': getattr(zs, 'type', 'SEG_ZS') # 中枢类型,默认为线段中枢
} for zs in element_analysis['zs_list'] if zs.end_klc]
# 添加小周期笔中枢
result['element_bi_zs_list'] = [{
'start_time': zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat(),
'end_time': (zs.end_klc.end_time if isinstance(zs.end_klc.end_time, str) else zs.end_klc.end_time.astimezone(client_tz).isoformat()) if zs.end_klc else None,
'zg': zs.zg,
'zd': zs.zd,
'is_sure': zs.is_sure, # 添加中枢是否完成的标志
'type': 'BI_ZS' # 标记为笔中枢
} for zs in element_analysis['bi_zs_list'] if zs.end_klc]
# 添加小周期未完成中枢列表
result['element_uncompleted_zs_list'] = [{
'start_time': zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat(),
'end_time': None, # 未完成中枢没有结束时间
'zg': zs.zg,
'zd': zs.zd,
'is_sure': zs.is_sure, # 未完成中枢的is_sure为False
'type': getattr(zs, 'type', 'SEG_ZS') # 中枢类型,默认为线段中枢
} for zs in element_analysis['zs_list'] if not zs.is_sure]
# 添加小周期未完成笔中枢列表
result['element_uncompleted_bi_zs_list'] = [{
'start_time': zs.start_klc.start_time if isinstance(zs.start_klc.start_time, str) else zs.start_klc.start_time.astimezone(client_tz).isoformat(),
'end_time': None, # 未完成中枢没有结束时间
'zg': zs.zg,
'zd': zs.zd,
'is_sure': zs.is_sure, # 未完成中枢的is_sure为False
'type': 'BI_ZS' # 标记为笔中枢
} for zs in element_analysis['bi_zs_list'] if not zs.is_sure]
result['element_trade_points'] = [{
'type': point['type'],
'time': format_time_safely(point['time'], client_tz),
'price': point['price'],
'desc': point['desc']
} for point in element_analysis['trade_points']]
print(f"小周期分析完成: {element_timeframe}, 笔数量: {len(result['element_bi_list'])}, {'仅元素数据' if elements_only else '包含主周期数据'}")
else:
print(f"无法获取小周期数据: {element_timeframe}")
return jsonify(result)
@app.route('/api/symbols')
def get_symbols():
"""获取可用交易对"""
try:
markets = exchange.load_markets()
# 合约交易对通常是以USDT结尾的永续合约
symbols = [symbol for symbol in markets.keys() if '/USDT' in symbol and ':USDT' in symbol]
return jsonify(symbols)
except Exception as e:
return jsonify({'error': str(e)})
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=8124)
+5
View File
@@ -0,0 +1,5 @@
flask==2.0.1
ccxt==4.4.70
pandas==1.3.3
numpy==1.21.2
plotly==5.3.1
+131
View File
@@ -0,0 +1,131 @@
/* 缠论分析系统样式 */
body {
font-family: "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
margin: 0;
padding: 20px;
background-color: #f8f9fa;
color: #333;
}
.container {
max-width: 1400px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.header {
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.controls {
margin-bottom: 20px;
padding: 15px;
background-color: #f1f3f5;
border-radius: 6px;
}
.chart-container {
width: 100%;
height: 600px;
margin-top: 20px;
border: 1px solid #eee;
border-radius: 6px;
padding: 10px;
background-color: #fff;
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
}
.data-container {
margin-top: 30px;
}
.nav-tabs {
margin-bottom: 15px;
}
.table-container {
overflow-x: auto;
}
.refresh-btn {
background-color: #0d6efd;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.refresh-btn:hover {
background-color: #0b5ed7;
}
#loadingIndicator {
display: none;
text-align: center;
padding: 20px;
font-size: 18px;
color: #666;
}
/* 表格样式定制 */
.dataTables_wrapper .dataTables_length,
.dataTables_wrapper .dataTables_filter {
margin-bottom: 15px;
}
table.dataTable {
border-collapse: collapse;
width: 100%;
}
table.dataTable thead th {
background-color: #f8f9fa;
border-bottom: 2px solid #dee2e6;
font-weight: 600;
}
table.dataTable tbody tr:hover {
background-color: #f1f3f5;
}
/* 方向列颜色 */
.direction-up {
color: #dc3545; /* 红色 */
font-weight: bold;
}
.direction-down {
color: #28a745; /* 绿色 */
font-weight: bold;
}
/* MACD列颜色 */
.positive {
color: #dc3545;
}
.negative {
color: #28a745;
}
/* 响应式调整 */
@media (max-width: 768px) {
.container {
padding: 10px;
}
.chart-container {
height: 400px;
}
.controls .row {
flex-direction: column;
}
}
File diff suppressed because it is too large Load Diff