将核心结构、指标与分析拆到 chan/{core,indicators,analysis,pipeline};
根目录保留兼容 shim;strategies 改为从 chan 包导入;买卖点经 bsp_macd 与 MACD 接合。
Co-authored-by: Cursor <cursoragent@cursor.com>
479 lines
14 KiB
Python
479 lines
14 KiB
Python
import sys
|
|
import os
|
|
from typing import Dict, List
|
|
from functools import reduce
|
|
from pandas import DataFrame
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
# 外部 chan.py 与本仓库包名 `chan` 在 macOS 大小写不敏感磁盘上冲突。
|
|
# 临时卸下本包后再导入上游,再恢复本包。
|
|
_EXT_ROOT = os.path.abspath("/Users/jack/Project/chan.py")
|
|
|
|
|
|
def _load_external_chan():
|
|
saved = {}
|
|
for key in list(sys.modules):
|
|
low = key.lower()
|
|
if low == "chan" or low.startswith("chan."):
|
|
saved[key] = sys.modules.pop(key)
|
|
inserted = False
|
|
if _EXT_ROOT not in sys.path:
|
|
sys.path.insert(0, _EXT_ROOT)
|
|
inserted = True
|
|
try:
|
|
from Chan import CChan as _CChan
|
|
from BuySellPoint.BS_Point import CBS_Point as _CBS_Point
|
|
from ChanConfig import CChanConfig as _CChanConfig
|
|
from Common.CEnum import (
|
|
AUTYPE as _AUTYPE,
|
|
DATA_SRC as _DATA_SRC,
|
|
KL_TYPE as _KL_TYPE,
|
|
DATA_FIELD as _DATA_FIELD,
|
|
BSP_TYPE as _BSP_TYPE,
|
|
FX_TYPE as _FX_TYPE,
|
|
BI_DIR as _BI_DIR,
|
|
KLINE_DIR as _KLINE_DIR,
|
|
SEG_DIR as _SEG_DIR,
|
|
)
|
|
from KLine.KLine_Unit import CKLine_Unit as _CKLine_Unit
|
|
from Common.CTime import CTime as _CTime
|
|
from Common.func_util import kltype_lt_day as _kltype_lt_day, str2float as _str2float
|
|
from Bi.Bi import CBi as _CBi
|
|
return {
|
|
"CChan": _CChan,
|
|
"CBS_Point": _CBS_Point,
|
|
"CChanConfig": _CChanConfig,
|
|
"AUTYPE": _AUTYPE,
|
|
"DATA_SRC": _DATA_SRC,
|
|
"KL_TYPE": _KL_TYPE,
|
|
"DATA_FIELD": _DATA_FIELD,
|
|
"BSP_TYPE": _BSP_TYPE,
|
|
"FX_TYPE": _FX_TYPE,
|
|
"BI_DIR": _BI_DIR,
|
|
"KLINE_DIR": _KLINE_DIR,
|
|
"SEG_DIR": _SEG_DIR,
|
|
"CKLine_Unit": _CKLine_Unit,
|
|
"CTime": _CTime,
|
|
"kltype_lt_day": _kltype_lt_day,
|
|
"str2float": _str2float,
|
|
"CBi": _CBi,
|
|
}
|
|
finally:
|
|
# 清除上游以 Chan/chan 注册的模块,避免污染本包
|
|
for key in list(sys.modules):
|
|
low = key.lower()
|
|
if low == "chan" or low.startswith("chan."):
|
|
sys.modules.pop(key, None)
|
|
sys.modules.update(saved)
|
|
if inserted and _EXT_ROOT in sys.path:
|
|
try:
|
|
sys.path.remove(_EXT_ROOT)
|
|
except ValueError:
|
|
pass
|
|
|
|
|
|
_ext = _load_external_chan()
|
|
CChan = _ext["CChan"]
|
|
CBS_Point = _ext["CBS_Point"]
|
|
CChanConfig = _ext["CChanConfig"]
|
|
AUTYPE = _ext["AUTYPE"]
|
|
DATA_SRC = _ext["DATA_SRC"]
|
|
KL_TYPE = _ext["KL_TYPE"]
|
|
DATA_FIELD = _ext["DATA_FIELD"]
|
|
BSP_TYPE = _ext["BSP_TYPE"]
|
|
FX_TYPE = _ext["FX_TYPE"]
|
|
BI_DIR = _ext["BI_DIR"]
|
|
KLINE_DIR = _ext["KLINE_DIR"]
|
|
SEG_DIR = _ext["SEG_DIR"]
|
|
CKLine_Unit = _ext["CKLine_Unit"]
|
|
CTime = _ext["CTime"]
|
|
kltype_lt_day = _ext["kltype_lt_day"]
|
|
str2float = _ext["str2float"]
|
|
CBi = _ext["CBi"]
|
|
|
|
|
|
def GetColumnNameFromFieldList(fileds: str):
|
|
_dict = {
|
|
"time": DATA_FIELD.FIELD_TIME,
|
|
"open": DATA_FIELD.FIELD_OPEN,
|
|
"high": DATA_FIELD.FIELD_HIGH,
|
|
"low": DATA_FIELD.FIELD_LOW,
|
|
"close": DATA_FIELD.FIELD_CLOSE,
|
|
"volume": DATA_FIELD.FIELD_VOLUME
|
|
}
|
|
return [_dict[x] for x in fileds.split(",")]
|
|
class ChanPY():
|
|
k_type = KL_TYPE.K_5M
|
|
config = CChanConfig({
|
|
"bi_strict": True,
|
|
"bi_algo": "normal",
|
|
"trigger_step": True,
|
|
"skip_step": 0,
|
|
"divergence_rate": float("inf"),
|
|
"bsp2_follow_1": False,
|
|
"bsp3_follow_1": False,
|
|
"min_zs_cnt": 1,
|
|
"bs1_peak": False,
|
|
"macd_algo": "peak",
|
|
"bs_type": '1,2,3a,1p,2s,3b',
|
|
"print_warning": True,
|
|
"zs_algo": "normal",
|
|
})
|
|
chan = CChan(
|
|
code="BTC/USDT:USDT",
|
|
data_src=DATA_SRC.CCXT,
|
|
lv_list=[k_type],
|
|
config=config,
|
|
autype=AUTYPE.QFQ,
|
|
)
|
|
klu_list = []
|
|
bsps = []
|
|
chanIn = True
|
|
#def __init__(self, dataframe):
|
|
#self.klu_list = self.get_kl_data(dataframe)
|
|
#for klu in self.klu_list:
|
|
#self.chan.trigger_load({self.k_type: [klu]})
|
|
def add_klu(self, klu):
|
|
if klu:
|
|
self.chan.trigger_load({self.k_type: [klu]})
|
|
self.klu_list.append(klu)
|
|
def add_klu_from_dataframe(self, dataframe):
|
|
if len(dataframe) > len(self.klu_list) and len(dataframe) - len(self.klu_list) == 1:
|
|
klu = self.get_last_klu(dataframe)
|
|
self.chan.trigger_load({self.k_type: [klu]})
|
|
self.klu_list.append(klu)
|
|
def parse_time_column(self, inp):
|
|
if len(inp) == 10:
|
|
year = int(inp[:4])
|
|
month = int(inp[5:7])
|
|
day = int(inp[8:10])
|
|
hour = minute = 0
|
|
elif len(inp) == 17:
|
|
year = int(inp[:4])
|
|
month = int(inp[4:6])
|
|
day = int(inp[6:8])
|
|
hour = int(inp[8:10])
|
|
minute = int(inp[10:12])
|
|
elif len(inp) == 19:
|
|
year = int(inp[:4])
|
|
month = int(inp[5:7])
|
|
day = int(inp[8:10])
|
|
hour = int(inp[11:13])
|
|
minute = int(inp[14:16])
|
|
else:
|
|
raise Exception(f"unknown time column from TradingView:{inp}")
|
|
return CTime(year, month, day, hour, minute, auto=not kltype_lt_day(self.k_type))
|
|
|
|
def create_item_dict(self, data, column_name):
|
|
for i in range(len(data)):
|
|
data[i] = self.parse_time_column(data[i]) if i == 0 else str2float(data[i])
|
|
return dict(zip(column_name, data))
|
|
def get_last_klu(self, dataframe:DataFrame):
|
|
fields = "time,open,high,low,close,volume"
|
|
item = dataframe.iloc[-1]
|
|
date = item['date']
|
|
o = item['open']
|
|
h = item['high']
|
|
l = item['low']
|
|
c = item['close']
|
|
v = item['volume']
|
|
#time_obj = date.fromtimestamp(date)
|
|
time_str = date.strftime('%Y-%m-%d %H:%M:%S')
|
|
item_data = [
|
|
time_str,
|
|
o,
|
|
h,
|
|
l,
|
|
c,
|
|
v
|
|
]
|
|
klu = CKLine_Unit(self.create_item_dict(item_data, GetColumnNameFromFieldList(fields)), autofix=True)
|
|
klu.set_idx(len(dataframe)-1)
|
|
return klu
|
|
def get_kl_data(self, dataframe:DataFrame):
|
|
fields = "time,open,high,low,close,volume"
|
|
klu_list = []
|
|
for i in range(0, len(dataframe)):
|
|
item = dataframe.iloc[i]
|
|
date = item['date']
|
|
o = item['open']
|
|
h = item['high']
|
|
l = item['low']
|
|
c = item['close']
|
|
v = item['volume']
|
|
#time_obj = date.fromtimestamp(date)
|
|
time_str = date.strftime('%Y-%m-%d %H:%M:%S')
|
|
item_data = [
|
|
time_str,
|
|
o,
|
|
h,
|
|
l,
|
|
c,
|
|
v
|
|
]
|
|
klu = CKLine_Unit(self.create_item_dict(item_data, GetColumnNameFromFieldList(fields)), autofix=True)
|
|
klu.set_idx(i)
|
|
klu_list.append(klu)
|
|
return klu_list
|
|
def get_bsp_type(self, bsp_type, is_buy):
|
|
if is_buy:
|
|
if bsp_type == BSP_TYPE.T1:
|
|
return 1
|
|
if bsp_type == BSP_TYPE.T1P:
|
|
return 2
|
|
if bsp_type == BSP_TYPE.T2:
|
|
return 3
|
|
if bsp_type == BSP_TYPE.T2S:
|
|
return 4
|
|
if bsp_type == BSP_TYPE.T3A:
|
|
return 5
|
|
if bsp_type == BSP_TYPE.T3B:
|
|
return 6
|
|
else:
|
|
if bsp_type == BSP_TYPE.T1:
|
|
return -1
|
|
if bsp_type == BSP_TYPE.T1P:
|
|
return -2
|
|
if bsp_type == BSP_TYPE.T2:
|
|
return -3
|
|
if bsp_type == BSP_TYPE.T2S:
|
|
return -4
|
|
if bsp_type == BSP_TYPE.T3A:
|
|
return -5
|
|
if bsp_type == BSP_TYPE.T3B:
|
|
return -6
|
|
def get_bsps(self, dataframe:DataFrame):
|
|
fields = "time,open,high,low,close,volume"
|
|
bsps = []
|
|
updown = []
|
|
bi_sure = []
|
|
if self.chanIn:
|
|
kl_data = self.get_kl_data(dataframe)
|
|
bsp_list = []
|
|
bsp_list_pre_len = 0
|
|
last_bsp_value = 0
|
|
last_updown = -1
|
|
bi_list_pre_len = 0
|
|
pre_bi = None
|
|
zs_list_pre_len = 0
|
|
pre_zs = None
|
|
for klu in kl_data: # 获取单根K线
|
|
self.chan.trigger_load({self.k_type: [klu]}) # 喂给CChan新增k线
|
|
self.last_kline = klu
|
|
bsp_list = self.chan.get_bsp()
|
|
kl_datas = self.chan.kl_datas[self.k_type]
|
|
bi_list = kl_datas.bi_list
|
|
lst = kl_datas.lst
|
|
if len(bsp_list) > 0:
|
|
last_bsp = bsp_list[-1]
|
|
#print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, lst[-2].fx, bi_list[-1].dir, bi_list[-1].is_sure,klu.close)
|
|
if bsp_list_pre_len > len(bsp_list):
|
|
if abs(last_bsp_value) == 1 or abs(last_bsp_value) == 2:
|
|
bsps.append(1)
|
|
#print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, 98)
|
|
else:
|
|
bsps.append(99)
|
|
else:
|
|
if bsp_list_pre_len == len(bsp_list):
|
|
if klu.idx == last_bsp.klu.idx:
|
|
last_bsp_value = self.get_bsp_type(last_bsp.type[0], last_bsp.is_buy)
|
|
bsps.append(last_bsp_value)
|
|
#print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value)
|
|
else:
|
|
bsps.append(0)
|
|
else:
|
|
last_bsp_value = self.get_bsp_type(last_bsp.type[0], last_bsp.is_buy)
|
|
bsps.append(last_bsp_value)
|
|
#print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value)
|
|
else:
|
|
bsps.append(0)
|
|
bsp_list_pre_len = len(bsp_list)
|
|
|
|
#Check zs -----------------------------------
|
|
zs_list = kl_datas.zs_list
|
|
if len(zs_list) > 0:
|
|
zs = zs_list[-1]
|
|
#if zs_list_pre_len > len(zs_list):
|
|
#print("No zs", zs.begin.time)
|
|
#if len(zs_list) > zs_list_pre_len:
|
|
#print(zs.begin.time, zs.end.time, zs.end.idx, zs.high, zs.low, zs.peak_high, zs.peak_low)
|
|
zs_list_pre_len = len(zs_list)
|
|
pre_zs = zs
|
|
|
|
#Check Bi -----------------------------------
|
|
if len(bi_list) > 0:
|
|
last_bi = bi_list[-1]
|
|
if len(bi_list) == 1:
|
|
if last_bi.dir == BI_DIR.UP:
|
|
updown.append(1)
|
|
last_updown = 1
|
|
else:
|
|
updown.append(-1)
|
|
last_updown = -1
|
|
else:
|
|
if last_updown == 1:
|
|
if last_bi.dir == BI_DIR.UP:
|
|
updown.append(0)
|
|
else:
|
|
updown.append(-1)
|
|
last_updown = -1
|
|
else:
|
|
if last_bi.dir == BI_DIR.DOWN:
|
|
updown.append(0)
|
|
else:
|
|
updown.append(1)
|
|
last_updown = 1
|
|
else:
|
|
updown.append(0)
|
|
bi_list = kl_datas.bi_list
|
|
if len(bi_list) > 0:
|
|
last_bi = bi_list[-1]
|
|
#if bi_list_pre_len > len(bi_list):
|
|
#print("Bi ", klu.time, pre_bi.idx, pre_bi.is_sure, bi_list[-1].idx, bi_list[-1].is_sure)
|
|
if last_bi.is_sure:
|
|
bi_sure.append(1)
|
|
#print(klu.time, last_bi.is_sure)
|
|
else:
|
|
bi_sure.append(0)
|
|
pre_bi = bi_list[-1]
|
|
bi_list_pre_len = len(bi_list)
|
|
else:
|
|
bi_sure.append(0)
|
|
#if bsps[-1] != 0 or updown[-1] != 0:
|
|
#print(klu.time, bsps[-1], updown[-1], bi_list[-1].is_sure)
|
|
self.chanIn = False
|
|
else:
|
|
klu = self.get_last_klu(dataframe)
|
|
if self.last_kline.time < klu.time:
|
|
self.chan.trigger_load({self.k_type: [klu]}) # 喂给CChan新增k线
|
|
self.last_kline = klu
|
|
for index in range(0, len(bsps)):
|
|
if not (abs(bsps[index]) == 1 or abs(bsps[index]) == 2):
|
|
bsps[index] = 0
|
|
else:
|
|
if bsps[index] == 2:
|
|
bsps[index] = 1
|
|
else:
|
|
if bsps[index] == -2:
|
|
bsps[index] = -1
|
|
else:
|
|
bsps[index] = 0
|
|
#print(bsps)
|
|
#print(updown)
|
|
kl_datas = self.chan.kl_datas[self.k_type]
|
|
#for zs in kl_datas.zs_list:
|
|
#print(zs.begin.time, zs.end.time)
|
|
return bsps, updown, bi_sure
|
|
|
|
def get_bsp_state1(self, dataframe:DataFrame):
|
|
fields = "time,open,high,low,close,volume"
|
|
bsps = []
|
|
if self.chanIn:
|
|
kl_data = self.get_kl_data(dataframe)
|
|
self.chan.trigger_load({self.k_type: kl_data})
|
|
bsp_list = self.chan.get_bsp()
|
|
bsp_index = 0
|
|
for klu in kl_data:
|
|
if bsp_index >= len(bsp_list):
|
|
bsp_index = len(bsp_list) - 1
|
|
bsp = bsp_list[bsp_index]
|
|
if klu.idx == bsp.klu.idx:
|
|
bsp_type = self.get_bsp_type(bsp.type[0], bsp.is_buy)
|
|
if abs(bsp_type) == 1 or abs(bsp_type) == 10:
|
|
bsps.append(1)
|
|
else:
|
|
bsps.append(0)
|
|
bsp_index = bsp_index + 1
|
|
else:
|
|
bsps.append(0)
|
|
self.chanIn = False
|
|
else:
|
|
klu = CKLine_Unit(self.create_item_dict(self.get_last_item_data(dataframe), GetColumnNameFromFieldList(fields)), autofix=True)
|
|
if self.last_kline.time < klu.time:
|
|
self.chan.trigger_load({self.k_type: [klu]}) # 喂给CChan新增k线
|
|
self.last_kline = klu
|
|
return bsps
|
|
def get_bsp_state(self, dataframe:DataFrame):
|
|
fields = "time,open,high,low,close,volume"
|
|
if self.chanIn:
|
|
kl_data = self.get_kl_data(dataframe)
|
|
bsp_list = []
|
|
bsp_list_pre_len = 0
|
|
last_bsp_value = 0
|
|
last_bsp_index = 0
|
|
for klu in kl_data: # 获取单根K线
|
|
self.chan.trigger_load({self.k_type: [klu]}) # 喂给CChan新增k线
|
|
self.last_kline = klu
|
|
bsp_list = self.chan.get_bsp()
|
|
kl_datas = self.chan.kl_datas[self.k_type]
|
|
bi_list = kl_datas.bi_list
|
|
lst = kl_datas.lst
|
|
if len(bsp_list) > 0:
|
|
last_bsp = bsp_list[-1]
|
|
#print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, lst[-2].fx, bi_list[-1].dir, bi_list[-1].is_sure,klu.close)
|
|
if bsp_list_pre_len > len(bsp_list):
|
|
if abs(last_bsp_value) == 1:
|
|
self.bsps.append(1)
|
|
#print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, 98)
|
|
else:
|
|
self.bsps.append(99)
|
|
else:
|
|
if bsp_list_pre_len == len(bsp_list):
|
|
if klu.idx == last_bsp.klu.idx:
|
|
if last_bsp.klu.idx - last_bsp_index > 3:
|
|
last_bsp_value = self.get_bsp_type(last_bsp.type[0], last_bsp.is_buy)
|
|
self.bsps.append(last_bsp_value)
|
|
else:
|
|
self.bsps.append(0)
|
|
last_bsp_index = last_bsp.klu.idx
|
|
#if abs(last_bsp_value) == 1 or abs(last_bsp_value) == 2:
|
|
#print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, "Knonw")
|
|
else:
|
|
self.bsps.append(0)
|
|
else:
|
|
if klu.idx == last_bsp.klu.idx:
|
|
if last_bsp.klu.idx - last_bsp_index > 3:
|
|
last_bsp_value = self.get_bsp_type(last_bsp.type[0], last_bsp.is_buy)
|
|
self.bsps.append(last_bsp_value)
|
|
else:
|
|
self.bsps.append(0)
|
|
last_bsp_index = last_bsp.klu.idx
|
|
#if abs(last_bsp_value) == 1 or abs(last_bsp_value) == 2:
|
|
#print(klu.time, klu.idx, last_bsp.klu.time, last_bsp.klu.idx, last_bsp_value, "Knonw")
|
|
else:
|
|
self.bsps.append(0)
|
|
else:
|
|
self.bsps.append(0)
|
|
bsp_list_pre_len = len(bsp_list)
|
|
self.chanIn = False
|
|
else:
|
|
klu = self.get_last_klu(dataframe)
|
|
if self.last_kline.time < klu.time:
|
|
self.chan.trigger_load({self.k_type: [klu]}) # 喂给CChan新增k线
|
|
self.last_kline = klu
|
|
bsp_list = self.chan.get_bsp()
|
|
last_bsp = bsp_list[-1]
|
|
if last_bsp.klu.idx == klu.idx:
|
|
self.bsps.append(self.get_bsp_type(last_bsp.type[0], last_bsp.is_buy))
|
|
else:
|
|
self.bsps.append(0)
|
|
for index in range(0, len(self.bsps)):
|
|
if not (abs(self.bsps[index]) == 1 or abs(self.bsps[index]) == 2):
|
|
self.bsps[index] = 0
|
|
else:
|
|
if self.bsps[index] == 2:
|
|
self.bsps[index] = 10
|
|
else:
|
|
if self.bsps[index] == -2:
|
|
self.bsps[index] = -10
|
|
else:
|
|
if self.bsps[index] == 1:
|
|
self.bsps[index] = 1
|
|
else:
|
|
if self.bsps[index] == -1:
|
|
self.bsps[index] = -1
|
|
else:
|
|
self.bsps[index] = 0
|
|
return self.bsps
|
|
|