股票K线图与SAR指标横轴同步缩放显示,SAR显示美式K线图和红绿豆,方便同步查看股票价格与SAR指标之间的关系,在使用代码辅助分析时,通过该控件可以方便所得分析结果可见,协助分析。
目录
效果
代码
需要的包、单K控件、日期横坐标控件、一些日期计算方法
import sys,json
import numpy as np
from datetime import datetime
from dateutil.relativedelta import relativedelta
from typing import Dict,Any
from PyQt5 import QtCore,QtGui,QtWidgets
from PyQt5.QtCore import Qt
import pyqtgraph as pg
import pyqtgraph.examples
pg.setConfigOption('background','k')
pg.setConfigOption('foreground','w')
# 返回今天的日期
def res_date_normal_str():
return datetime.now().strftime('%Y-%m-%d')
# 返回今年第一天
def res_current_year_first_day():
now_day = datetime.today()
current_year = now_day.year
res_str = f"{current_year}-01-01"
return res_str
pass
# 往回推一年的第一天
def res_pre_year_first_day():
# https://blog.csdn.net/weixin_42185136/article/details/108646120
pre_year_day = (datetime.now()-relativedelta(years=1)).strftime('%Y-%m-%d')
return pre_year_day
# 往回推两年的第一天
def res_pre_two_year_first_day():
pre_year_day = (datetime.now() - relativedelta(years=2)).strftime('%Y-%m-%d')
return pre_year_day
class RotateAxisItem(pg.AxisItem):
def drawPicture(self, p, axisSpec, tickSpecs, textSpecs):
p.setRenderHint(p.Antialiasing,False)
p.setRenderHint(p.TextAntialiasing,True)
## draw long line along axis
pen,p1,p2 = axisSpec
p.setPen(pen)
p.drawLine(p1,p2)
p.translate(0.5,0) ## resolves some damn pixel ambiguity
## draw ticks
for pen,p1,p2 in tickSpecs:
p.setPen(pen)
p.drawLine(p1,p2)
## draw all text
# if self.tickFont is not None:
# p.setFont(self.tickFont)
p.setPen(self.pen())
for rect,flags,text in textSpecs:
# this is the important part
p.save()
p.translate(rect.x(),rect.y())
p.rotate(-30)
p.drawText(-rect.width(),rect.height(),rect.width(),rect.height(),flags,text)
# restoring the painter is *required*!!!
p.restore()
class CandlestickItem(pg.GraphicsObject):
def __init__(self, data):
pg.GraphicsObject.__init__(self)
self.data = data ## data must have fields: time, open, close, min, max
self.generatePicture()
def generatePicture(self):
## pre-computing a QPicture object allows paint() to run much more quickly,
## rather than re-drawing the shapes every time.
self.picture = QtGui.QPicture()
p = QtGui.QPainter(self.picture)
p.setPen(pg.mkPen('d'))
w = (self.data[1][0] - self.data[0][0]) / 3.
for (t, open, close, min, max) in self.data:
p.drawLine(QtCore.QPointF(t, min), QtCore.QPointF(t, max))
if open > close:
p.setBrush(pg.mkBrush('r'))
else:
p.setBrush(pg.mkBrush('g'))
p.drawRect(QtCore.QRectF(t - w, open, w * 2, close - open))
p.end()
def paint(self, p, *args):
p.drawPicture(0, 0, self.picture)
def boundingRect(self):
## boundingRect _must_ indicate the entire area that will be drawn on
## or else we will get artifacts and possibly crashing.
## (in this case, QPicture does all the work of computing the bouning rect for us)
return QtCore.QRectF(self.picture.boundingRect())
SAR显示控件
# SAR指标控件
# 当K线运行在SAR曲线的上方时,表明当前股价是处于连续上涨的趋势之中,这时SAR指标的圆圈就是以红色表示
# 当K线运行在SAR曲线的下方时,表明当前股价是处于连续下跌的趋势之中,这时SAR指标的圆圈就是以绿色表示
class SARtickItem(pg.GraphicsObject):
def __init__(self, data):
pg.GraphicsObject.__init__(self)
self.data = data['sar_data'] ## data must have fields: time,close,open,high,low,val
self.min_y = data['min_y']
self.max_y = data['max_y']
self.generatePicture()
def generatePicture(self):
self.picture = QtGui.QPicture()
p = QtGui.QPainter(self.picture)
w = (self.data[1][0] - self.data[0][0]) / 3.
w0 = ((self.max_y-self.min_y)/len(self.data))
for (t,c,o,h,l,v) in self.data:
# 绘制美国K线
if c>o:
# 阳线
p.setPen(pg.mkPen('r'))
p.drawLine(QtCore.QPointF(t, l), QtCore.QPointF(t, h))
p.drawLine(QtCore.QPointF(t, o), QtCore.QPointF((t-w), o))
p.drawLine(QtCore.QPointF(t, c), QtCore.QPointF((t+w), c))
pass
else:
# 阴线
p.setPen(pg.mkPen('g'))
p.drawLine(QtCore.QPointF(t, l), QtCore.QPointF(t, h))
p.drawLine(QtCore.QPointF(t, o), QtCore.QPointF((t - w), o))
p.drawLine(QtCore.QPointF(t, c), QtCore.QPointF((t + w), c))
pass
if c>v:
# 红豆
p.setPen(pg.mkPen('r'))
p.drawEllipse(QtCore.QPointF(t,v),w,w0)
pass
elif c<v:
# 绿豆
p.setPen(pg.mkPen('g'))
p.drawEllipse(QtCore.QPointF(t,v),w,w0)
pass
p.end()
def paint(self, p, *args):
p.drawPicture(0, 0, self.picture)
def boundingRect(self):
return QtCore.QRectF(self.picture.boundingRect())
K线图和SAR指标同步显示控件
class PyQtGraphLineWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.init_data()
self.init_ui()
pass
def init_data(self):
self.please_select_str = '---请选择---'
self.func_map = {
}
self.func_item_list = []
self.duration_map = {
'今年':'a',
'最近一年':'b',
'最近两年':'c'
}
# https://www.sioe.cn/yingyong/yanse-rgb-16/
self.color_line = (30, 144, 255)
self.color_ma_5 = (248,248,255) # 幽灵的白色
self.color_ma_10 = (255,255,0) # 纯黄
self.color_ma_20 = (255,0,255) # 紫红色
self.color_ma_30 = (0,128,0) # 纯绿
self.color_ma_60 = (30,144,255) # 道奇蓝
self.color_up = (220,20,60)
self.color_down = (60,179,113)
self.main_fixed_target_list = [] # 主体固定曲线,不能被删除
self.whole_df = None
self.whole_header = None
self.whole_header2 = None
self.whole_pd_header = None
self.whole_pd_header2 = None
self.current_whole_data = None
self.current_whole_data2 = None
self.current_whole_df = None
pass
def init_ui(self):
# 控制面板 start
left_tip = QtWidgets.QLabel('左边界')
self.left_point = QtWidgets.QDateEdit()
self.left_point.setDisplayFormat('yyyy-MM-dd')
self.left_point.setCalendarPopup(True)
right_tip = QtWidgets.QLabel('右边界')
self.right_point = QtWidgets.QDateEdit()
self.right_point.setDisplayFormat('yyyy-MM-dd')
self.right_point.setCalendarPopup(True)
duration_sel_btn = QtWidgets.QPushButton('确定')
duration_sel_btn.clicked.connect(self.duration_sel_btn_clicked)
duration_reset_btn = QtWidgets.QPushButton('重置')
duration_reset_btn.clicked.connect(self.duration_reset_btn_clicked)
duration_tip = QtWidgets.QLabel('常用时间')
self.duration_combox = QtWidgets.QComboBox()
self.duration_combox.addItem(self.please_select_str)
self.duration_combox.addItems(list(self.duration_map.keys()))
self.duration_combox.currentIndexChanged.connect(self.duration_combox_currentIndexChanged)
combox_tip = QtWidgets.QLabel('功能')
self.func_combox = QtWidgets.QComboBox()
self.func_combox.addItem(self.please_select_str)
self.func_combox.addItems(list(self.func_map.keys()))
self.func_combox.currentIndexChanged.connect(self.func_combox_currentIndexChanged)
clear_func_btn = QtWidgets.QPushButton('清空功能区')
clear_func_btn.clicked.connect(self.clear_func_btn_clicked)
self.whole_duration_label = QtWidgets.QLabel('原始最宽边界:左边界~右边界')
self.now_duration_label = QtWidgets.QLabel('当前显示最宽边界:左边界~右边界')
layout_date = QtWidgets.QHBoxLayout()
layout_date.addWidget(left_tip)
layout_date.addWidget(self.left_point)
layout_date.addWidget(right_tip)
layout_date.addWidget(self.right_point)
layout_date.addWidget(duration_sel_btn)
layout_date.addWidget(duration_reset_btn)
layout_date.addSpacing(30)
layout_date.addWidget(duration_tip)
layout_date.addWidget(self.duration_combox)
layout_date.addSpacing(30)
layout_date.addWidget(combox_tip)
layout_date.addWidget(self.func_combox)
layout_date.addWidget(clear_func_btn)
layout_date.addStretch(1)
layout_duration = QtWidgets.QHBoxLayout()
layout_duration.addWidget(self.whole_duration_label)
layout_duration.addSpacing(30)
layout_duration.addWidget(self.now_duration_label)
layout_duration.addStretch(1)
# 控制面板 end
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=60)
xax2 = RotateAxisItem(orientation='bottom')
xax2.setHeight(h=60)
self.pw = pg.PlotWidget(axisItems={'bottom': xax})
self.pw2 = pg.PlotWidget(axisItems={'bottom': xax2})
layout_pw = QtWidgets.QVBoxLayout()
layout_pw.addWidget(self.pw,1)
layout_pw.addWidget(self.pw2,1)
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.title_label)
layout.addLayout(layout_date)
layout.addLayout(layout_duration)
layout.addLayout(layout_pw)
self.setLayout(layout)
pass
def set_data(self,data:Dict[str,Any]):
title_str = data['title_str']
whole_header = data['whole_header']
whole_header2 = data['whole_header2']
whole_df = data['whole_df']
whole_pd_header = data['whole_pd_header']
whole_pd_header2 = data['whole_pd_header2']
self.whole_header = whole_header
self.whole_header2 = whole_header2
self.whole_df = whole_df
self.whole_pd_header = whole_pd_header
self.whole_pd_header2 = whole_pd_header2
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)
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((i,row['openPrice'],row['closePrice'],row['lowestPrice'],row['highestPrice']))
self.current_whole_data = df.loc[:,self.whole_pd_header].values.tolist()
self.current_whole_data2 = df.loc[:,self.whole_pd_header2].values.tolist()
# 开始配置显示的内容
self.pw.clear()
self.pw2.clear()
self.func_item_list.clear()
self.now_duration_label.setText(f"当前显示最宽边界:{df.iloc[0]['tradeDate']}~{df.iloc[-1]['tradeDate']}")
xax = self.pw.getAxis('bottom')
xax.setTicks([xTick_show])
xax2 = self.pw2.getAxis('bottom')
xax2.setTicks([xTick_show])
candle_fixed_target = CandlestickItem(candle_data)
self.main_fixed_target_list.append(candle_fixed_target)
self.pw.addItem(candle_fixed_target)
# 指标
min_y0 = df['lowestPrice'].min()
max_y0 = df['highestPrice'].max()
min_y1 = df['sar'].min()
max_y1 = df['sar'].max()
if min_y0<min_y1:
min_y = min_y0
else:
min_y = min_y1
if max_y0>max_y1:
max_y = max_y0
else:
max_y = max_y1
sar_data = []
for i, row in df.iterrows():
sar_data.append((i, row['closePrice'], row['openPrice'],row['highestPrice'], row['lowestPrice'], row['sar']))
sar_data00 = {
'sar_data':sar_data,
'max_y':max_y,
'min_y':min_y
}
sar_items = SARtickItem(sar_data00)
self.main_fixed_target_list.append(sar_items)
self.pw2.addItem(sar_items)
self.pw2.setXLink(self.pw)
self.vLine = pg.InfiniteLine(angle=90, movable=False)
self.hLine = pg.InfiniteLine(angle=0, movable=False)
self.label = pg.TextItem()
self.vLine2 = pg.InfiniteLine(angle=90, movable=False)
self.hLine2 = pg.InfiniteLine(angle=0, movable=False)
self.label2 = pg.TextItem()
self.pw.addItem(self.vLine, ignoreBounds=True)
self.pw.addItem(self.hLine, ignoreBounds=True)
self.pw.addItem(self.label, ignoreBounds=True)
self.pw2.addItem(self.vLine2, ignoreBounds=True)
self.pw2.addItem(self.hLine2, ignoreBounds=True)
self.pw2.addItem(self.label2, ignoreBounds=True)
self.vb = self.pw.getViewBox()
self.proxy = pg.SignalProxy(self.pw.scene().sigMouseMoved, rateLimit=60, slot=self.mouseMoved)
self.pw.enableAutoRange()
self.vb2 = self.pw2.getViewBox()
self.proxy2 = pg.SignalProxy(self.pw2.scene().sigMouseMoved, rateLimit=60, slot=self.mouseMoved2)
self.pw2.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())
self.vLine2.setPos(mousePoint.x())
pass
def mouseMoved2(self,evt):
pos = evt[0]
if self.pw2.sceneBoundingRect().contains(pos):
mousePoint = self.vb2.mapSceneToView(pos)
index = int(mousePoint.x())
if index >= 0 and index < len(self.current_whole_data2):
target_data = self.current_whole_data2[index]
html_str = ''
for i, item in enumerate(self.whole_header2):
html_str += f"<br/>{item}:{target_data[i]}"
self.label2.setHtml(html_str)
self.label2.setPos(mousePoint.x(), mousePoint.y())
self.vLine2.setPos(mousePoint.x())
self.hLine2.setPos(mousePoint.y())
self.vLine.setPos(mousePoint.x())
pass
def mouseClicked(self,evt):
pass
def updateViews(self):
pass
# 图形操作之外
def duration_sel_btn_clicked(self):
'''边界选择'''
left_point = self.left_point.date().toString('yyyy-MM-dd')
right_point = self.right_point.date().toString('yyyy-MM-dd')
df = self.whole_df.copy()
df['o_date'] = pd.to_datetime(df['tradeDate'])
self.current_whole_df = df.loc[(df['o_date']>=left_point) & (df['o_date']<=right_point)].copy()
self.caculate_and_show_data()
pass
def duration_reset_btn_clicked(self):
'''边界重置'''
self.current_whole_df = self.whole_df.copy()
self.caculate_and_show_data()
pass
def duration_combox_currentIndexChanged(self,cur_i:int):
cur_txt = self.duration_combox.currentText()
if cur_txt == self.please_select_str:
return
cur_code = self.duration_map[cur_txt]
right_point = res_date_normal_str()
if cur_code == 'a':
left_point = res_current_year_first_day()
elif cur_code == 'b':
left_point = res_pre_year_first_day()
elif cur_code == 'c':
left_point = res_pre_two_year_first_day()
else:
return
df = self.whole_df.copy()
df['o_date'] = pd.to_datetime(df['tradeDate'])
self.current_whole_df = df.loc[(df['o_date'] >= left_point) & (df['o_date'] <= right_point)].copy()
self.caculate_and_show_data()
pass
def func_combox_currentIndexChanged(self,cur_i:int):
pass
def clear_func_btn_clicked(self):
for item in self.func_item_list:
self.pw.removeItem(item)
self.func_item_list.clear()
pass
pass
使用
if __name__ == '__main__':
# 600660 福耀玻璃
import pandas as pd
import math
import talib
df = pd.read_csv('E:/temp005/600660.csv',encoding='utf-8')
# 删除停牌的数据
df = df.loc[df['openPrice']>0].copy()
df['openPrice'] = df['openPrice']*df['accumAdjFactor']
df['closePrice'] = df['closePrice']*df['accumAdjFactor']
df['highestPrice'] = df['highestPrice']*df['accumAdjFactor']
df['lowestPrice'] = df['lowestPrice']*df['accumAdjFactor']
# 计算指标
# sar
df['sar'] = talib.SAR(df['highestPrice'],df['lowestPrice'])
whole_pd_header = ['tradeDate','closePrice','openPrice','highestPrice','lowestPrice']
whole_pd_header2 = ['tradeDate','sar']
line_data = {
'title_str':'福耀玻璃',
'whole_header':['日期','收盘价','开盘价','最高价','最低价'],
'whole_header2':['日期','sar'],
'whole_pd_header':whole_pd_header,
'whole_pd_header2':whole_pd_header2,
'whole_df':df
}
app = QtWidgets.QApplication(sys.argv)
t_win = PyQtGraphLineWidget()
t_win.show()
t_win.set_data(line_data)
sys.exit(app.exec_())
pass
数据
链接:https://pan.baidu.com/s/1HPkMsDDyXTEgffoAVIhbZw
提取码:h80x