在K线图分析中经常需要查看波峰与波谷的情况,鉴别是否后续高点是否高于或低于前一个高点,后续低点是否高于或低于前一个低点;另一种情况是寻找曲线形态,诸如头肩顶(底)、双重顶(底)等。在K线图中直接查找会比较费劲,如果通过代码编写,把比较重要的波峰(高点)、波谷(低点)找出,将这些比较重要的高点低点连线,就可以比较容易地看出高低位置和找出曲线形态。
目录
效果
寻找波峰波谷策略代码
可视化部分代码
使用
效果
左侧是波峰波谷连线后的简易趋势曲线,右侧是原始K线图
寻找波峰波谷策略代码
思路:
1. 出现一个波峰,要出现下一个波峰中间必然会有一个波谷,所以波峰和波谷必然交替出现;
2. pandas中有一个cummax()方法,可以找到当前点之前的最大值,也就是说如果当前点位之前有比当前值更大的值,当前点位即使是新的波峰,这个波峰也无法获取到;波谷的cummin()也是一样的道理;
3. 基于以上两点,可以采用这样的办法:
1) 先找到第一个波峰和波谷,即使用cummax()和cummin()找到第一个波峰和波谷,检查第一个波峰和第一个波谷的位置,在前面的是首先出现的目标点位
2)根据首次出现的目标点位,以该点位的下一个位置为第二次寻找的初始位置,如果第一个目标点位是波峰,那第二次该找波谷,从新的初始位置开始使用cummin()寻找最先出现的波谷;如果第一个目标点位是波谷,那第二次该找波峰,从新的初始位置开始使用cummax()寻找最先出现的波峰;这样第二个目标点位就找到了,以此类推,找到所有的目标点位
3)找到所有的目标点位后,会发现很多波峰与波谷之间只相邻了很少的K线,甚至就是相邻,这种情况根据自己研究的需要,可以把相邻很近的波峰波谷成对删除。
def res_peak_trough_list(df,peak_col,trough_col):
'''
寻找波峰波谷
:param df: 必须要有i_row
:param peak_col: 波峰字段
:param trough_col: 波谷字段
:return:
'''
# 循环寻找波峰和波谷,交替寻找
peak_list = []
trough_list = []
start_up_i_row = 0
peak_yeah = False
trough_yeah = False
while True:
if start_up_i_row >= len(df):
break
df00 = df.loc[df['i_row'] >= start_up_i_row].copy()
df00['peak'] = df00[peak_col].cummax()
df00['ext_00'] = df00['peak'] - df00['peak'].shift(-1)
df00_p = df00.loc[(df00['ext_00'] == 0) & (df00['ext_00'].shift(-1) == 0) & (df00['ext_00'].shift(-2) == 0) & (
df00['ext_00'].shift(-3) == 0) & (df00['ext_00'].shift(-4) == 0)].copy()
df00['trough'] = df00[trough_col].cummin()
df00['ext_10'] = df00['trough'] - df00['trough'].shift(-1)
df00_t = df00.loc[(df00['ext_10'] == 0) & (df00['ext_10'].shift(-1) == 0) & (df00['ext_10'].shift(-2) == 0) & (
df00['ext_10'].shift(-3) == 0) & (df00['ext_10'].shift(-4) == 0)].copy()
if (df00_p is None or len(df00_p) <= 0) and (df00_t is not None and len(df00_t) > 0):
trough_0 = df00_t.iloc[0]['i_row']
if trough_yeah:
trough_list.append(trough_0)
break
elif (df00_t is None or len(df00_t) <= 0) and (df00_p is not None and len(df00_p) > 0):
peak_0 = df00_p.iloc[0]['i_row']
if peak_yeah:
peak_list.append(peak_0)
break
elif (df00_p is None or len(df00_p) <= 0) or (df00_t is None or len(df00_t) <= 0):
break
else:
trough_0 = df00_t.iloc[0]['i_row']
peak_0 = df00_p.iloc[0]['i_row']
if peak_0 == trough_0:
start_up_i_row += 1
continue
if peak_0 == 0:
peak_yeah = False
trough_yeah = True
peak_list.append(peak_0)
start_up_i_row = trough_0
elif trough_0 == 0:
trough_yeah = False
peak_yeah = True
trough_list.append(trough_0)
start_up_i_row = peak_0
else:
if not trough_yeah and not peak_yeah:
# 首次进入
if peak_0 < trough_0:
trough_yeah = True
peak_list.append(peak_0)
start_up_i_row = trough_0
else:
peak_yeah = True
trough_list.append(trough_0)
start_up_i_row = peak_0
else:
if peak_0 < trough_0:
if peak_yeah:
peak_list.append(peak_0)
peak_yeah = False
trough_yeah = True
start_up_i_row = trough_0
else:
if trough_yeah:
trough_list.append(trough_0)
trough_yeah = False
peak_yeah = True
start_up_i_row = peak_0
pass
two_list = peak_list + trough_list
two_list.sort()
i = 0
remove_list = []
while i < len(two_list) - 2:
one_i = two_list[i]
two_i = two_list[i + 1]
if one_i in peak_list and two_i in trough_list:
if abs(one_i - two_i) <= 2:
i += 2
remove_list.append(one_i)
remove_list.append(two_i)
continue
pass
elif one_i in trough_list and two_i in peak_list:
if abs(one_i - two_i) <= 2:
i += 2
remove_list.append(one_i)
remove_list.append(two_i)
continue
pass
else:
pass
i += 1
pass
two_list00 = []
for item in two_list:
if item in remove_list:
continue
two_list00.append(item)
peak_list00 = []
trough_list00 = []
for item in two_list00:
if item in peak_list:
peak_list00.append(item)
if item in trough_list:
trough_list00.append(item)
return peak_list00,trough_list00
可视化部分代码
需要导入的包、日期横坐标控件、K线图控件的代码请查看本人本栏目中其他博文,这里不再赘述。
K线图显示控件
class PyQtGraphScrollKWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.factor_widgets = {}
self.simple_line_widget = None
self.init_data()
self.init_ui()
pass
def init_data(self):
# https://www.sioe.cn/yingyong/yanse-rgb-16/
# self.color_line = (30, 144, 255)
self.color_line = (255, 255, 0)
self.color_highligh = (220,20,60)
# 0 幽灵的白色; 1 纯黄; 2 紫红色; 3 纯绿; 4 道奇蓝
self.color_list = [(248, 248, 255), (255, 255, 0), (255, 0, 255), (0, 128, 0), (30, 144, 255)]
self.color_green = (34,139,34)
self.color_red = (220,20,60)
self.main_fixed_target_list = [] # 主体固定曲线,不能被删除
self.whole_df = None
self.whole_header = None
self.whole_pd_header = None
self.current_whole_data = None
self.current_whole_df = None
self.factor_list = None
self.ma_list = None
self.show_duration = []
self.show_line = []
self.show_segment = []
self.detail_map = None
self.peak_trough_data = None
self.factor_code_widgetname_map = {
'VOL':'VOL_PlotWidget',
'SAR':'SAR_PlotWidget',
'MACD':'MACD_PlotWidget',
'BOLL':'BOLL_PlotWidget',
'KDJ':'KDJ_PlotWidget',
'RSI':'RSI_PlotWidget'
}
pass
def init_ui(self):
self.whole_duration_label = QtWidgets.QLabel('左边界~右边界')
pic_download_btn = QtWidgets.QPushButton('滚动截图')
pic_download_btn.clicked.connect(self.pic_download_btn_clicked)
simple_line_btn = QtWidgets.QPushButton('K波峰波谷图')
simple_line_btn.clicked.connect(self.simple_line_btn_clicked)
layout_top = QtWidgets.QHBoxLayout()
layout_top.addWidget(self.whole_duration_label)
layout_top.addStretch(1)
layout_top.addWidget(pic_download_btn)
layout_top.addWidget(simple_line_btn)
self.title_label = QtWidgets.QLabel('执行过程查看')
self.title_label.setAlignment(Qt.AlignCenter)
self.title_label.setStyleSheet('QLabel{font-size:18px;font-weight:bold}')
# 滚动区域开始
self.pw_layout = QtWidgets.QVBoxLayout()
self.scroll_area = QtWidgets.QScrollArea()
self.scroll_area.setWidgetResizable(True)
# self.scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
layout_right = QtWidgets.QVBoxLayout()
layout_right.addWidget(self.title_label)
layout_right.addLayout(layout_top)
layout_right.addWidget(self.scroll_area)
self.setLayout(layout_right)
pass
def set_data(self, data: Dict[str, Any]):
title_str = data['title_str']
whole_header = data['whole_header']
whole_df = data['whole_df']
whole_pd_header = data['whole_pd_header']
factor_list = data['factor_list']
ma_list = data['ma_list']
detail_map = data['detail_map']
if data.get('peak_trough_data') is not None:
self.peak_trough_data = data['peak_trough_data']
self.whole_header = whole_header
self.whole_df = whole_df
self.whole_pd_header = whole_pd_header
self.factor_list = factor_list
self.ma_list = ma_list
if detail_map.get('duration') is not None:
self.show_duration = detail_map['duration']
if detail_map.get('line') is not None:
self.show_line = detail_map['line']
if detail_map.get('segment') is not None:
self.show_segment = detail_map['segment']
self.detail_map = detail_map
self.title_label.setText(title_str)
self.whole_duration_label.setText(f"{self.whole_df.iloc[0]['tradeDate']}~{self.whole_df.iloc[-1]['tradeDate']}")
self.current_whole_df = self.whole_df.copy()
self.caculate_and_show_data()
pass
def caculate_and_show_data(self):
df = self.current_whole_df.copy()
# df.reset_index(inplace=True)
df['i_count'] = [i for i in range(len(df))]
tradeDate_list = df['tradeDate'].values.tolist()
x = range(len(df))
xTick_show = []
x_dur = math.ceil(len(df) / 20)
for i in range(0, len(df), x_dur):
xTick_show.append((i, tradeDate_list[i]))
if len(df) % 20 != 0:
xTick_show.append((len(df) - 1, tradeDate_list[-1]))
candle_data = []
for i, row in df.iterrows():
candle_data.append(
(row['i_count'], row['openPrice'], row['closePrice'], row['lowestPrice'], row['highestPrice']))
self.current_whole_data = df.loc[:, self.whole_pd_header].values.tolist()
# 开始配置显示的内容
self.create_candle_widget()
self.factor_widgets.clear()
xax = self.pw.getAxis('bottom')
xax.setTicks([xTick_show])
# 标记技术图形 start
if len(self.show_duration)>0:
duration_list = self.show_duration[0]
duration_color = self.show_duration[1]
for i,item in enumerate(duration_list):
for item00 in item:
signal_fiexed_target = pg.LinearRegionItem([item00[0], item00[1]],
movable=False, brush=(
self.color_list[duration_color[i]][0], self.color_list[duration_color[i]][1], self.color_list[duration_color[i]][2], 50))
self.pw.addItem(signal_fiexed_target)
pass
pass
if len(self.show_line)>0:
line_list = self.show_line[0]
line_color = self.show_line[1]
for i,item in enumerate(line_list):
for item00 in item:
signal_fiexed_target = pg.InfiniteLine(pos=(item00, 0), movable=False, angle=90,
pen=pg.mkPen({'color': self.color_list[line_color[i]], 'width': 1})
)
self.pw.addItem(signal_fiexed_target)
pass
if len(self.show_segment)>0:
for item in self.show_segment:
r_target = pg.LineSegmentROI(item,pen={'color':self.color_red,'width':2},movable=False)
self.pw.addItem(r_target)
pass
# 标记技术图形 end
candle_fixed_target = CandlestickItem(candle_data)
self.main_fixed_target_list.append(candle_fixed_target)
self.pw.addItem(candle_fixed_target)
if len(self.ma_list)>0:
for i, item in enumerate(self.ma_list):
item_color = i%len(self.color_list)
line_fixed_target = pg.PlotCurveItem(x=np.array(x), y=np.array(df[item].values.tolist()),
pen=pg.mkPen({'color': self.color_list[item_color],
'width': 1}),
connect='finite')
self.main_fixed_target_list.append(line_fixed_target)
self.pw.addItem(line_fixed_target)
pass
self.vLine = pg.InfiniteLine(angle=90, movable=False)
self.hLine = pg.InfiniteLine(angle=0, movable=False)
self.label = pg.TextItem()
self.pw.addItem(self.vLine, ignoreBounds=True)
self.pw.addItem(self.hLine, ignoreBounds=True)
self.pw.addItem(self.label, ignoreBounds=True)
self.vb = self.pw.getViewBox()
self.proxy = pg.SignalProxy(self.pw.scene().sigMouseMoved, rateLimit=60, slot=self.mouseMoved)
# 其他项
for item in self.factor_list:
item_widget = eval(self.factor_code_widgetname_map[item])()
item_widget.setMinimumHeight(300)
item_widget.set_data({'df':self.current_whole_df.copy()})
item_widget.setXLink(self.pw)
# 标记 start
if self.detail_map.get(item) is not None:
target_detail = self.detail_map[item]
if target_detail.get('duration') is not None:
target_duration = target_detail['duration']
target_list = target_duration[0]
target_color = target_duration[1]
for i,i0 in enumerate(target_list):
for i00 in i0:
signal_fiexed_target = pg.LinearRegionItem([i00[0], i00[1]],
movable=False, brush=(
self.color_list[target_color[i]][0], self.color_list[target_color[i]][1],
self.color_list[target_color[i]][2], 50))
item_widget.addItem(signal_fiexed_target)
pass
if target_detail.get('line') is not None:
target_line = target_detail['line']
target_list = target_line[0]
target_color = target_line[1]
for i, i0 in enumerate(target_list):
for i00 in i0:
signal_fiexed_target = pg.InfiniteLine(pos=(i00, 0), movable=False, angle=90,
pen=pg.mkPen(
{'color': self.color_list[target_color[i]],
'width': 1})
)
item_widget.addItem(signal_fiexed_target)
pass
if target_detail.get('segment') is not None:
target_segment = target_detail['segment']
for i00 in target_segment:
r_target = pg.LineSegmentROI(i00, pen={'color': self.color_red, 'width': 2}, movable=False)
item_widget.addItem(r_target)
pass
# 标记 end
self.factor_widgets[item] = item_widget
self.fill_pw_widget()
self.pw.enableAutoRange()
pass
def mouseMoved(self, evt):
pos = evt[0]
if self.pw.sceneBoundingRect().contains(pos):
mousePoint = self.vb.mapSceneToView(pos)
index = int(mousePoint.x())
if index >= 0 and index < len(self.current_whole_data):
target_data = self.current_whole_data[index]
html_str = ''
for i, item in enumerate(self.whole_header):
html_str += f"<br/>{item}:{target_data[i]}"
self.label.setHtml(html_str)
self.label.setPos(mousePoint.x(), mousePoint.y())
self.vLine.setPos(mousePoint.x())
self.hLine.setPos(mousePoint.y())
pass
def mouseClicked(self, evt):
pass
def updateViews(self):
pass
def fill_pw_widget(self):
# 清空控件
while self.pw_layout.count():
item = self.pw_layout.takeAt(0)
widget = item.widget()
if widget is not None:
widget.deleteLater()
pass
pass
sc_child_widget = self.scroll_area.takeWidget()
if sc_child_widget is not None:
sc_child_widget.deleteLater()
self.pw_layout.addWidget(self.pw)
for item in self.factor_widgets.values():
self.pw_layout.addWidget(item)
one_sc_child_widget = QtWidgets.QWidget()
one_sc_child_widget.setLayout(self.pw_layout)
self.scroll_area.setWidget(one_sc_child_widget)
pass
def create_candle_widget(self):
xax = RotateAxisItem(orientation='bottom')
xax.setHeight(h=60)
self.pw = pg.PlotWidget(axisItems={'bottom': xax})
self.pw.setMinimumHeight(500)
self.pw.setMouseEnabled(x=True, y=True)
# self.pw.enableAutoRange(x=False,y=True)
self.pw.setAutoVisible(x=False, y=True)
pass
def pic_download_btn_clicked(self):
now_str = datetime.now().strftime('%Y%m%d%H%M%S')
path,_ = QtWidgets.QFileDialog.getSaveFileName(
self,
'选择图片保存路径',
f"pic_{now_str}",
'JPG(*.jpg)'
)
if not path:
return
widget = self.scroll_area.widget()
pix = widget.grab()
pix.save(path)
pass
def simple_line_btn_clicked(self):
# PyQtGraphSimpleLineWidget
if self.peak_trough_data is None:
QtWidgets.QMessageBox.information(
self,
'提示',
'没有波峰波谷数据',
QtWidgets.QMessageBox.Yes
)
return
if self.simple_line_widget is None:
self.simple_line_widget = PyQtGraphSimpleLineWidget()
self.simple_line_widget.set_data(self.peak_trough_data)
self.simple_line_widget.show()
pass
def closeEvent(self, a0: QtGui.QCloseEvent) -> None:
if self.simple_line_widget is not None:
self.simple_line_widget.close()
self.close()
pass
简易波峰波谷显示控件
class PyQtGraphSimpleLineWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.init_data()
self.init_ui()
def init_data(self):
# https://www.sioe.cn/yingyong/yanse-rgb-16/
# self.color_line = (30, 144, 255)
self.color_line = (255, 255, 0)
self.color_highligh = (220,20,60)
# 0 幽灵的白色; 1 纯黄; 2 紫红色; 3 纯绿; 4 道奇蓝
self.color_list = [(248, 248, 255), (255, 255, 0), (255, 0, 255), (0, 128, 0), (30, 144, 255)]
pass
def init_ui(self):
self.title_label = QtWidgets.QLabel('简易波峰波谷图')
self.title_label.setAlignment(Qt.AlignCenter)
self.title_label.setStyleSheet('QLabel{font-size:18px;font-weight:bold}')
xax = RotateAxisItem(orientation='bottom')
xax.setHeight(h=80)
self.pw = pg.PlotWidget(axisItems={'bottom': xax})
self.pw.setMouseEnabled(x=True, y=True)
# self.pw.enableAutoRange(x=False,y=True)
self.pw.setAutoVisible(x=False, y=True)
layout_right = QtWidgets.QVBoxLayout()
layout_right.addWidget(self.title_label)
layout_right.addWidget(self.pw)
self.setLayout(layout_right)
pass
def set_data(self, data: Dict[str, Any]):
i_row_list = data['i_row_list']
tradeDate_list = data['tradeDate_list']
y = data['y']
len = data['len']
self.i_row_list = i_row_list
self.tradeDate_list = tradeDate_list
self.y = y
x = range(len)
self.x = x
xTick_show = []
# x_dur = math.ceil(len(tradeDate_list) / 20)
# for i in range(0, len(tradeDate_list), x_dur):
# # xTick_show.append((i, tradeDate_list[i]))
# xTick_show.append((i_row_list[i], tradeDate_list[i]))
# if len(tradeDate_list) % 20 != 0:
# xTick_show.append((len(tradeDate_list) - 1, tradeDate_list[-1]))
for i,item in zip(i_row_list,tradeDate_list):
xTick_show.append((i,item))
# 开始配置显示的内容
self.pw.clear()
xax = self.pw.getAxis('bottom')
xax.setTicks([xTick_show])
fixed_target = pg.PlotCurveItem(x=np.array(i_row_list), y=np.array(y),
pen=pg.mkPen({'color': self.color_line, 'width': 2}),
connect='finite')
self.pw.addItem(fixed_target)
self.vLine = pg.InfiniteLine(angle=90, movable=False)
self.hLine = pg.InfiniteLine(angle=0, movable=False)
self.label = pg.TextItem()
self.pw.addItem(self.vLine, ignoreBounds=True)
self.pw.addItem(self.hLine, ignoreBounds=True)
self.pw.addItem(self.label, ignoreBounds=True)
self.vb = self.pw.getViewBox()
self.proxy = pg.SignalProxy(self.pw.scene().sigMouseMoved, rateLimit=60, slot=self.mouseMoved)
self.pw.enableAutoRange()
pass
def mouseMoved(self, evt):
pos = evt[0]
if self.pw.sceneBoundingRect().contains(pos):
mousePoint = self.vb.mapSceneToView(pos)
index = int(mousePoint.x())
# x = self.x[index]
if index in self.i_row_list:
i0 = self.i_row_list.index(index)
tradeDate = self.tradeDate_list[i0]
y = self.y[i0]
html_str = ''
html_str += f"<br/>日期:{tradeDate}"
html_str += f"<br/>i_row:{index}"
html_str += f"<br/>y:{y}"
self.label.setHtml(html_str)
self.label.setPos(mousePoint.x(), mousePoint.y())
self.vLine.setPos(mousePoint.x())
self.hLine.setPos(mousePoint.y())
pass
def mouseClicked(self, evt):
pass
def updateViews(self):
pass
pass
使用
if __name__ == '__main__':
pre_path = r'E:/temp005/600660.csv'
caculate_start_date_str = '2020-01-01'
df = pd.read_csv(pre_path,encoding='utf-8')
# 删除停牌的数据
df = df.loc[df['openPrice'] > 0].copy()
df['o_date'] = df['tradeDate']
df['o_date'] = pd.to_datetime(df['o_date'])
df = df.loc[df['o_date'] >= caculate_start_date_str].copy()
# 保存未复权收盘价数据
df['close'] = df['closePrice']
# 计算前复权数据
df['openPrice'] = df['openPrice'] * df['accumAdjFactor']
df['closePrice'] = df['closePrice'] * df['accumAdjFactor']
df['highestPrice'] = df['highestPrice'] * df['accumAdjFactor']
df['lowestPrice'] = df['lowestPrice'] * df['accumAdjFactor']
df.reset_index(inplace=True)
df['i_row'] = [i for i in range(len(df))]
k_peak, k_trough = res_peak_trough_list(df,'highestPrice','lowestPrice')
two00 = k_peak + k_trough
two00.sort()
y_list = []
tradeDate_list = []
for item in two00:
if item in k_peak:
y_list.append(df.iloc[item]['highestPrice'])
else:
y_list.append(df.iloc[item]['lowestPrice'])
tradeDate_list.append(df.iloc[item]['tradeDate'])
peak_trough_data = {
'i_row_list': two00,
'tradeDate_list': tradeDate_list,
'y': y_list,
'len': len(df)
}
columns_list = ['日期', '收盘价', '开盘价', '最高价', '最低价']
columns_pd_list = ['tradeDate', 'closePrice', 'openPrice', 'highestPrice', 'lowestPrice']
line_data = {
'title_str': '寻找波峰和波谷',
'whole_header': columns_list,
'whole_df': df,
'whole_pd_header': columns_pd_list,
'detail_map': {},
'factor_list': [],
'ma_list': [],
'peak_trough_data': peak_trough_data
}
QtCore.QCoreApplication.setAttribute(QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
app = QtWidgets.QApplication(sys.argv)
t_win = PyQtGraphScrollKWidget()
t_win.set_data(line_data)
t_win.showMaximized()
app.exec()
pass