Initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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)
|
||||
@@ -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
@@ -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()
|
||||
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
@@ -0,0 +1,13 @@
|
||||
1. **第一类买卖点**:
|
||||
- 定义:趋势反转的起始点,即在下跌趋势结束时形成的买点(第一类买点),或在上涨趋势结束时形成的卖点(第一类卖点)。这是市场多空力量发生根本性转变的位置。
|
||||
- 与MACD背驰的关系:Macd背驰是指出中枢后形成的Macd的红绿柱面积比进入中枢时的面积绝对值小,背驰比较的黄白线和柱子面积都在0轴的一个方向上。第一类买点都是在0轴之下背驰形成的,第一类卖点都是在0轴之上的背驰形成的。
|
||||
|
||||
2. **第二类买卖点**:
|
||||
- 定义:趋势确认后的回调点。在第一类买卖点之后,价格会回调或反弹,形成第二类买点(回调不破前低)或第二类卖点(反弹不破前高),是对第一类买卖点的确认。第二类买点都是第一次上0轴后回抽确认形成的。第二类卖点都是第一次0轴之下上涨确认形成的。第二类买卖点只会在趋势确认后,第一类买卖点出现之后出现一次,不会重复出现,除非趋势反转之后。
|
||||
|
||||
3. **第三类买卖点**:
|
||||
- 定义:趋势延续的确认点。价格突破回调或反弹的中枢区间后,回踩不破关键位置(如中枢上沿或下沿),形成第三类买点(上升趋势延续)或第三类卖点(下降趋势延续)。第三类买卖点只会在中枢确认之后出现。
|
||||
|
||||
|
||||
|
||||
我们交易的是币安的比特币合约, 数据格式是json, 数据包括现有的持仓, 仓位历史, 账户余额, 你用缠论分析之后, 给出以下分析, 最近的一个中枢在哪里,现在的趋势是什么,现在是否是买卖点,如果是,是那一类买卖点,应该进行何种操作。
|
||||
@@ -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()
|
||||
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
pip install -r requirements.txt
|
||||
python app.py
|
||||
+539
@@ -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)
|
||||
@@ -0,0 +1,5 @@
|
||||
flask==2.0.1
|
||||
ccxt==4.4.70
|
||||
pandas==1.3.3
|
||||
numpy==1.21.2
|
||||
plotly==5.3.1
|
||||
@@ -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
Reference in New Issue
Block a user