目录
双线组合概念
双线组合又称两条均线组合,是由一条时间周期较短的均线和一条时间周期较长的均线组合而成的均线分析系统。
其中时间周期较长的均线,主要用于预测和判断趋势方向。时间周期较短的均线,主要利用其与股价、长周期的均线之间的相互位置和关系,作为确定进出场的依据。为便于解说,时间周期较长的均线,我们称之为定性线;时间周期较短的均线,我们称之为定量线。
PS:以上概念来自书籍《均线技术分析》by 邱立波
双线组合交易原则
1)买入和持仓原则。
A. 股价向上突破定性线,定性线上行,买入。
B. 定量线上穿定性线形成黄金交叉,买入
C. 股价下跌,遇定性线上行支撑止跌回升,买入
D. 定性线上行,股价在定性线上方向上突破定量线,买入
E. 定量线下行,遇定性线上行支撑止跌后再度上升,买入。
F. 股价、定量线、定性线多头排列,持股
2)卖出和空仓原则
A. 股价跌破定性线,定性线走平或已拐头下行,卖出
B. 多头排列期间,股价跌破定量线,减仓
C. 股价急速飙升,远离定性线,减仓
D. 定性线下行,空仓
PS:以上交易原则来自书籍《均线技术分析》by 邱立波
交易原则翻译为代码逻辑
一些概念诸如上行、飙升、下跌等描述要置换成代码逻辑,需要进行开发者做自定义的量化,这部分量化会影响信号发出的频次,先定义好一个默认量,后续策略优化有需要再行修改。
本文策略自定义规则:
描述 | 代码逻辑规则(自定义) |
支撑和阻碍 | 股价与均线价格之间的差值小于等于当前股价的2% |
股价下跌 | 连续三日收盘价涨跌幅为负 |
均线下行 | 连续三日均线涨跌幅为负 |
股价急速飙升 | 连续三日股价涨跌幅都大于等于5% |
远离 | 股价与均线价格之间的差值大于等于当前股价的50% |
下面开始逐句翻译交易原则:
1) 买入和持仓原则:
A. 股价向上突破定性线,定性线上行,买入。
翻译:收盘价涨跌幅为正 & 前一日收盘价小于定性价格 & 收盘价大于定性价格 & 定性线连续三日涨跌幅为正
B. 定量线上穿定性线形成黄金交叉,买入。
翻译:前一日定量价格小于前一日定性价格 & 当日定量价格大于定性价格
C. 股价下跌,遇定性线上行支撑止跌回升,买入。
翻译:连续三日收盘价涨跌幅为负 & 定性线连续三日涨跌幅为正 & 股价被定性线支撑 & 下一日收盘价正跌幅为正
D. 定性线上行,股价在定性线上方向上突破定量线,买入。
翻译:定性线连续三日涨跌幅为正 & 收盘价大于定性线价格 & (前一日收盘价小于前一日定量价格 & 当日收盘价大于定量价格)
E. 定量线下行,遇定性线上行支撑止跌后再度上升,买入。
翻译:连续三日定量线涨跌幅为负 & 连续三日定性线涨跌幅为正 & 定量线被定性线支撑 & 下一日定量线涨跌幅为正
F. 股价、定量线、定性线多头排列,持股
翻译:收盘价大于定量价格大于定性价格
2) 卖出和空仓原则
A. 股价跌破定性线,定性线走平或已拐头下行,卖出
翻译:前一日收盘价大于定性价格 & 当日收盘价小于定性价格 & 定性价格涨跌幅小于等于0
B. 多头排列期间,股价跌破定量线,减仓
翻译:前一日收盘价大于定量价格大于定性价格 & 当日收盘价小于定量价格
C. 股价急速飙升,远离定性线,减仓
翻译:股价飙升 & 收盘价远离定性价格
D. 定性线下行,空仓
翻译:连续三日定性价格涨跌幅为负
策略代码实现
本文策略使用ma5作为定量线,ma20作为定性线
def excute_strategy():
'''
前置自定义规则:
A. 支撑和阻碍,定义为股价与均线价格之间的差值小于等于当前股价的2%
B. 股价下跌,连续三日收盘价涨跌幅为负
C. 均线下行,连续三日均线涨跌幅为负
D. 股价急速飙升,连续三日股价涨跌幅都大于等于5%
E. 远离,定义为股价与均线价格之间的差值大于等于当前股价的50%
1) 买入和持仓原则:
A. 股价向上突破定性线,定性线上行,买入。
翻译:收盘价涨跌幅为正 & 前一日收盘价小于定性价格 & 收盘价大于定性价格 & 定性线连续三日涨跌幅为正
B. 定量线上穿定性线形成黄金交叉,买入。
翻译:前一日定量价格小于前一日定性价格 & 当日定量价格大于定性价格
C. 股价下跌,遇定性线上行支撑止跌回升,买入。
翻译:连续三日收盘价涨跌幅为负 & 定性线连续三日涨跌幅为正 & 股价被定性线支撑 & 下一日收盘价正跌幅为正
D. 定性线上行,股价在定性线上方向上突破定量线,买入。
翻译:定性线连续三日涨跌幅为正 & 收盘价大于定性线价格 & (前一日收盘价小于前一日定量价格 & 当日收盘价大于定量价格)
E. 定量线下行,遇定性线上行支撑止跌后再度上升,买入。
翻译:连续三日定量线涨跌幅为负 & 连续三日定性线涨跌幅为正 & 定量线被定性线支撑 & 下一日定量线涨跌幅为正
F. 股价、定量线、定性线多头排列,持股
翻译:收盘价大于定量价格大于定性价格
2) 卖出和空仓原则
A. 股价跌破定性线,定性线走平或已拐头下行,卖出
翻译:前一日收盘价大于定性价格 & 当日收盘价小于定性价格 & 定性价格涨跌幅小于等于0
B. 多头排列期间,股价跌破定量线,减仓
翻译:前一日收盘价大于定量价格大于定性价格 & 当日收盘价小于定量价格
C. 股价急速飙升,远离定性线,减仓
翻译:股价飙升 & 收盘价远离定性价格
D. 定性线下行,空仓
翻译:连续三日定性价格涨跌幅为负
:return:
'''
import pandas as pd
import talib
df = pd.read_csv('E:/temp005/600660.csv',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']>='2020-01-01'].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']
# 双均线组合
# 定性线:20日均线 定量线:5日均线
df['ma5'] = talib.MA(df['closePrice'],timeperiod=5)
df['ma20'] = talib.MA(df['closePrice'],timeperiod=20)
# 定性线涨跌幅
df['ma20_rate'] = df['ma20']-df['ma20'].shift(1)
# 定性线上行还是下行:连续三天涨跌幅为正,为上行,值为1;连续三天涨跌幅为负,为下行,值为-1
df['ma20_direction'] = 0
df.loc[(df['ma20_rate']>0) & (df['ma20_rate'].shift(1)>0) & (df['ma20_rate'].shift(2)>0),'ma20_direction'] = 1
df.loc[(df['ma20_rate']<0) & (df['ma20_rate'].shift(1)<0) & (df['ma20_rate'].shift(2)<0),'ma20_direction'] = -1
# 定量线涨跌幅
df['ma5_rate'] = df['ma5'] - df['ma5'].shift(1)
df['ma5_direction'] = 0
df.loc[(df['ma5_rate']>0) & (df['ma5_rate'].shift(1)>0) & (df['ma5_rate'].shift(2)>0),'ma5_direction'] = 1
df.loc[(df['ma5_rate']<0) & (df['ma5_rate'].shift(1)<0) & (df['ma5_rate'].shift(2)<0),'ma5_direction'] = -1
# 收盘价涨跌幅
df['close_rate'] = df['closePrice'] - df['closePrice'].shift(1)
df['closePrice_direction'] = 0
df.loc[(df['close_rate'] > 0) & (df['close_rate'].shift(1) > 0) & (df['close_rate'].shift(2) > 0), 'closePrice_direction'] = 1
df.loc[(df['close_rate'] < 0) & (df['close_rate'].shift(1) < 0) & (df['close_rate'].shift(2) < 0), 'closePrice_direction'] = -1
# 收盘价到定性线之间的距离
df['close_ma20_distance'] = (abs(df['closePrice']-df['ma20'])/df['closePrice'])*100
# 收盘价到定量线之间的距离
df['close_ma5_distance'] = (abs(df['closePrice']-df['ma5'])/df['closePrice'])*100
# 定量线到定性线之间的距离
df['ma5_ma20_distance'] = (abs(df['ma5']-df['ma20'])/df['ma5'])*100
# 定性线到定量线之间的距离
df['ma20_ma5_distance'] = (abs(df['ma5']-df['ma20'])/df['ma20'])*100
# 股价是否飙升
df['pre_fast_up'] = 0
df.loc[(df['close_rate']>0) & (df['close_rate']/df['closePrice']>=0.05),'pre_fast_up'] = 1
df['fast_up'] = 0
df.loc[(df['pre_fast_up']==1) & (df['pre_fast_up'].shift(1)==1) & (df['pre_fast_up'].shift(2)==1),'fast_up'] = 1
# 1) 买入和持仓原则:
# 买入=1 持股=2(区间) 卖出=3 减仓=4 空仓=5(区间) 无信号=0
df['signal'] = 0
df['signal_reason'] = ''
# A. 股价向上突破定性线,定性线上行,买入。
# 翻译:收盘价涨跌幅为正 & 前一日收盘价小于定性价格 & 收盘价大于定性价格 & 定性线连续三日涨跌幅为正
df.loc[(df['close_rate']>0) & (df['closePrice'].shift(1)<df['ma20'].shift(1)) & (df['closePrice']>df['ma20']) & (df['ma20_direction']==1),'signal'] = 1
df.loc[(df['close_rate']>0) & (df['closePrice'].shift(1)<df['ma20'].shift(1)) & (df['closePrice']>df['ma20']) & (df['ma20_direction']==1),'signal_reason'] = 'A'
# B.定量线上穿定性线形成黄金交叉,买入。
# 翻译:前一日定量价格小于前一日定性价格 & 当日定量价格大于定性价格
df.loc[(df['ma5'].shift(1)<df['ma20'].shift(1)) & (df['ma5']>df['ma20']),'signal'] = 1
df.loc[(df['ma5'].shift(1)<df['ma20'].shift(1)) & (df['ma5']>df['ma20']),'signal_reason'] = 'B'
# C.股价下跌,遇定性线上行支撑止跌回升,买入。
# 翻译:连续三日收盘价涨跌幅为负 & 定性线连续三日涨跌幅为正 & 股价被定性线支撑 & 下一日收盘价正跌幅为正
df.loc[(df['closePrice_direction']==-1) & (df['ma20_direction']==1) & (df['close_ma20_distance']<=2) & (df['close_rate'].shift(-1)>0),'signal'] = 1
df.loc[(df['closePrice_direction']==-1) & (df['ma20_direction']==1) & (df['close_ma20_distance']<=2) & (df['close_rate'].shift(-1)>0),'signal_reason'] = 'C'
# D.定性线上行,股价在定性线上方向上突破定量线,买入。
# 翻译:定性线连续三日涨跌幅为正 & 收盘价大于定性线价格 & (前一日收盘价小于前一日定量价格 & 当日收盘价大于定量价格)
df.loc[(df['ma20_direction']==1) & (df['closePrice']>df['ma20']) & (df['closePrice'].shift(1)<df['ma5'].shift(1)) & (df['closePrice']>df['ma5']),'signal'] = 1
df.loc[(df['ma20_direction']==1) & (df['closePrice']>df['ma20']) & (df['closePrice'].shift(1)<df['ma5'].shift(1)) & (df['closePrice']>df['ma5']),'signal_reason'] = 'D'
# E.定量线下行,遇定性线上行支撑止跌后再度上升,买入。
# 翻译:连续三日定量线涨跌幅为负 & 连续三日定性线涨跌幅为正 & 定量线被定性线支撑 & 下一日定量线涨跌幅为正
df.loc[(df['ma5_direction']==-1) & (df['ma20_direction']==1) & (df['ma5_ma20_distance']<=2) & (df['ma5_rate'].shift(-1)>0),'signal'] = 1
df.loc[(df['ma5_direction']==-1) & (df['ma20_direction']==1) & (df['ma5_ma20_distance']<=2) & (df['ma5_rate'].shift(-1)>0),'signal_reason'] = 'E'
# F.股价、定量线、定性线多头排列,持股
# 翻译:收盘价大于定量价格大于定性价格
df.loc[(df['closePrice']>df['ma5']) & (df['ma5']>df['ma20']),'signal'] = 2
df.loc[(df['closePrice']>df['ma5']) & (df['ma5']>df['ma20']),'signal_reason'] = 'F'
# 2) 卖出和空仓原则
# A.股价跌破定性线,定性线走平或已拐头下行,卖出
# 翻译:前一日收盘价大于定性价格 & 当日收盘价小于定性价格 & 定性价格涨跌幅小于等于0
df.loc[(df['closePrice'].shift(1)>df['ma20'].shift(1)) & (df['closePrice']<df['ma20']) & (df['ma20_rate']<=0),'signal'] = 3
df.loc[(df['closePrice'].shift(1)>df['ma20'].shift(1)) & (df['closePrice']<df['ma20']) & (df['ma20_rate']<=0),'signal_reason'] = 'A'
# B.多头排列期间,股价跌破定量线,减仓
# 翻译:前一日收盘价大于定量价格大于定性价格 & 当日收盘价小于定量价格
df.loc[(df['closePrice'].shift(1)>df['ma5'].shift(1)) & (df['ma5'].shift(1)>df['ma20'].shift(1)) & (df['closePrice']<df['ma5']),'signal'] = 4
df.loc[(df['closePrice'].shift(1)>df['ma5'].shift(1)) & (df['ma5'].shift(1)>df['ma20'].shift(1)) & (df['closePrice']<df['ma5']),'signal_reason'] = 'B'
# C.股价急速飙升,远离定性线,减仓
# 翻译:股价飙升 & 收盘价远离定性价格
df.loc[(df['fast_up']==1) & (df['close_ma5_distance']>=50),'signal'] = 4
df.loc[(df['fast_up']==1) & (df['close_ma5_distance']>=50),'signal_reason'] = 'C'
# D.定性线下行,空仓
# 翻译:连续三日定性价格涨跌幅为负
df.loc[df['ma20_direction']==-1,'signal'] = 5
df.loc[df['ma20_direction']==-1,'signal_reason'] = 'D'
title_str = '双均线组合信号'
whole_header = ['日期','收盘价','开盘价','最高价','最低价','ma5','ma20']
whole_df = df
whole_pd_header = ['tradeDate','closePrice','openPrice','highestPrice','lowestPrice','ma5','ma20']
line_pd_header = ['ma5','ma20']
line_color_list = [0,2]
line_data = {
'title_str':title_str,
'whole_header':whole_header,
'whole_df':whole_df,
'whole_pd_header':whole_pd_header,
'line_pd_header':line_pd_header,
'line_color_list':line_color_list
}
return line_data
运行工具代码
导入需要的包、K线控件、日期横坐标控件、分页表格控件
import sys,json,os,math,time
from threading import Thread
import numpy as np
import pandas as pd
from datetime import datetime
from dateutil.relativedelta import relativedelta
from typing import Dict,Any,List
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')
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())
# 分页表格控件,单选
class PageTableWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.init_data()
self.init_ui()
pass
def init_data(self):
# 表格全数据 二维数组
self.table_full_data: List[Any] = []
# 表格右键菜单 {菜单名:索引指向}
self.table_right_menus: Dict[str, str] = {}
self.total_page_count: int = 0
self.total_rows_count: int = 0
self.current_page: int = 1
self.single_page_rows: int = 50
pass
def init_ui(self):
pre_page_btn = QtWidgets.QPushButton('上一页')
pre_page_btn.clicked.connect(self.pre_page_btn_clicked)
next_page_btn = QtWidgets.QPushButton('下一页')
next_page_btn.clicked.connect(self.next_page_btn_clicked)
tip_label_0 = QtWidgets.QLabel('第')
self.witch_page_lineedit = QtWidgets.QLineEdit()
self.int_validator = QtGui.QIntValidator()
self.witch_page_lineedit.setValidator(self.int_validator)
self.witch_page_lineedit.setMaximumWidth(20)
tip_label_1 = QtWidgets.QLabel('页')
go_page_btn = QtWidgets.QPushButton('前往')
go_page_btn.clicked.connect(self.go_page_btn_clicked)
layout_witch_page = QtWidgets.QHBoxLayout()
layout_witch_page.addWidget(tip_label_0)
layout_witch_page.addWidget(self.witch_page_lineedit)
layout_witch_page.addWidget(tip_label_1)
layout_witch_page.addWidget(go_page_btn)
layout_witch_page.addStretch(1)
layout_pagechange = QtWidgets.QHBoxLayout()
layout_pagechange.addWidget(pre_page_btn)
layout_pagechange.addWidget(next_page_btn)
layout_pagechange.addLayout(layout_witch_page)
self.total_page_count_label = QtWidgets.QLabel(f"共0页")
self.total_rows_count_label = QtWidgets.QLabel(f"共0行")
self.current_page_label = QtWidgets.QLabel(f"当前第0页")
layout_pagestatus = QtWidgets.QHBoxLayout()
layout_pagestatus.addWidget(self.total_page_count_label)
layout_pagestatus.addWidget(self.total_rows_count_label)
layout_pagestatus.addWidget(self.current_page_label)
layout_pagestatus.addStretch(1)
layout_top = QtWidgets.QVBoxLayout()
layout_top.addLayout(layout_pagechange)
layout_top.addLayout(layout_pagestatus)
self.target_table = QtWidgets.QTableWidget()
self.target_table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.target_table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
layout = QtWidgets.QVBoxLayout()
layout.addLayout(layout_top)
layout.addWidget(self.target_table)
self.setLayout(layout)
pass
def set_table_init_data(self, data: Dict[str, Any]):
'''设置表头、右键菜单等初始化内容'''
headers: List[str] = data['headers']
self.target_table.setColumnCount(len(headers))
self.target_table.setHorizontalHeaderLabels(headers)
pass
def set_table_full_data(self, data: List[Any]):
'''表格全数据'''
self.table_full_data = data
self.total_rows_count = len(data)
self.total_page_count = math.ceil(self.total_rows_count / self.single_page_rows)
self.current_page = 1
self.int_validator.setRange(1, self.total_page_count)
self.total_page_count_label.setText(f"共{self.total_page_count}页")
self.total_rows_count_label.setText(f"共{self.total_rows_count}行")
self.caculate_current_show_data()
pass
def setting_current_pagestatus_label(self):
self.current_page_label.setText(f"当前第{self.current_page}页")
pass
def caculate_current_show_data(self):
'''计算当前要显示的数据'''
start_dot = (self.current_page - 1) * self.single_page_rows
end_dot = start_dot + self.single_page_rows
current_data = self.table_full_data[start_dot:end_dot]
self.fill_table_content(current_data)
self.setting_current_pagestatus_label()
pass
def fill_table_content(self, data: List[Any]):
self.target_table.clearContents()
self.target_table.setRowCount(len(data))
for r_i, r_v in enumerate(data):
for c_i, c_v in enumerate(r_v):
cell = QtWidgets.QTableWidgetItem(str(c_v))
self.target_table.setItem(r_i, c_i, cell)
pass
pass
self.target_table.resizeColumnsToContents()
pass
def go_page_btn_clicked(self):
'''前往按钮点击'''
input_page = self.witch_page_lineedit.text()
input_page = input_page.strip()
if not input_page:
QtWidgets.QMessageBox.information(
self,
'提示',
'请输入要跳转的页码',
QtWidgets.QMessageBox.Yes
)
return
input_page = int(input_page)
if input_page < 0 or input_page > self.total_page_count:
QtWidgets.QMessageBox.information(
self,
'提示',
'输入的页码超出范围',
QtWidgets.QMessageBox.Yes
)
return
self.current_page = input_page
self.caculate_current_show_data()
pass
def pre_page_btn_clicked(self):
'''上一页按钮点击'''
if self.current_page <= 1:
QtWidgets.QMessageBox.information(
self,
'提示',
'已经是首页',
QtWidgets.QMessageBox.Yes
)
return
self.current_page -= 1
self.caculate_current_show_data()
pass
def next_page_btn_clicked(self):
'''下一页按钮点击'''
if self.current_page >= self.total_page_count:
QtWidgets.QMessageBox.information(
self,
'提示',
'已经是最后一页',
QtWidgets.QMessageBox.Yes
)
return
self.current_page += 1
self.caculate_current_show_data()
pass
pass
执行结果图表显示控件
class PyQtGraphRunningWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.init_data()
self.init_ui()
pass
def init_data(self):
self.table_header = ['日期','信号','未复权收盘价','解说']
self.please_select_str = '---请选择---'
self.func_map = {
'标记多头排列区间':'a',
'标记空头排列区间':'b',
'20日线斜率为正':'c'
}
self.func_item_list = []
self.duration_map = {
'今年':'a',
'最近一年':'b',
'最近两年':'c'
}
# https://www.sioe.cn/yingyong/yanse-rgb-16/
self.color_line = (30, 144, 255)
# 0 幽灵的白色; 1 纯黄; 2 紫红色; 3 纯绿; 4 道奇蓝
self.color_list = [(248,248,255),(255,255,0),(255,0,255),(0,128,0),(30,144,255)]
self.buy_color = (220, 20, 60) # 猩红
self.hold_color = (255, 140, 0) # 深橙色
self.sell_color = (50, 205, 50) # 酸橙绿
self.subpos_color = (24, 252, 0) # 草坪绿
self.emptypos_color = (0, 128, 0) # 纯绿
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.line_pd_header = []
self.line_color_list = []
self.signal_show_list = []
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)
self.whole_duration_label = QtWidgets.QLabel('原始最宽边界:左边界~右边界')
self.now_duration_label = QtWidgets.QLabel('当前显示最宽边界:左边界~右边界')
self.buy_checkbox = QtWidgets.QCheckBox('买入')
self.buy_checkbox.stateChanged.connect(lambda: self.signal_checkbox_state(self.buy_checkbox))
self.hold_checkbox = QtWidgets.QCheckBox('持股')
self.hold_checkbox.stateChanged.connect(lambda: self.signal_checkbox_state(self.hold_checkbox))
self.sell_checkbox = QtWidgets.QCheckBox('卖出')
self.sell_checkbox.stateChanged.connect(lambda: self.signal_checkbox_state(self.sell_checkbox))
self.subpos_checkbox = QtWidgets.QCheckBox('减仓')
self.subpos_checkbox.stateChanged.connect(lambda: self.signal_checkbox_state(self.subpos_checkbox))
self.emptypos_checkbox = QtWidgets.QCheckBox('空仓')
self.emptypos_checkbox.stateChanged.connect(lambda: self.signal_checkbox_state(self.emptypos_checkbox))
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.addWidget(self.buy_checkbox)
layout_date.addWidget(self.hold_checkbox)
layout_date.addWidget(self.sell_checkbox)
layout_date.addWidget(self.subpos_checkbox)
layout_date.addWidget(self.emptypos_checkbox)
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=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)
self.table = PageTableWidget()
self.table.set_table_init_data({'headers':self.table_header})
layout_down = QtWidgets.QHBoxLayout()
layout_down.addWidget(self.pw,6)
layout_down.addWidget(self.table,1)
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.title_label)
layout.addLayout(layout_date)
layout.addLayout(layout_duration)
layout.addLayout(layout_down)
self.setLayout(layout)
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']
line_pd_header = data['line_pd_header']
line_color_list = data['line_color_list']
self.whole_header = whole_header
self.whole_df = whole_df
self.whole_pd_header = whole_pd_header
self.line_pd_header = line_pd_header
self.line_color_list = line_color_list
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.pw.clear()
self.func_item_list.clear()
self.nocheck_checkbox()
self.remove_signal_show()
self.now_duration_label.setText(f"当前显示最宽边界:{df.iloc[0]['tradeDate']}~{df.iloc[-1]['tradeDate']}")
xax = self.pw.getAxis('bottom')
xax.setTicks([xTick_show])
candle_fixed_target = CandlestickItem(candle_data)
self.main_fixed_target_list.append(candle_fixed_target)
self.pw.addItem(candle_fixed_target)
# 曲线
if len(self.line_pd_header)>0:
for i,item in enumerate(self.line_pd_header):
line_fixed_target = pg.PlotCurveItem(x=np.array(x), y=np.array(df[item].values.tolist()),
pen=pg.mkPen({'color': self.color_list[self.line_color_list[i]], 'width': 2}),
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)
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 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 nocheck_checkbox(self):
self.buy_checkbox.setChecked(False)
self.hold_checkbox.setChecked(False)
self.sell_checkbox.setChecked(False)
self.subpos_checkbox.setChecked(False)
self.emptypos_checkbox.setChecked(False)
pass
def remove_signal_show(self):
for item in self.signal_show_list:
self.pw.removeItem(item)
self.signal_show_list.clear()
pass
def signal_checkbox_state(self,cb):
self.remove_signal_show()
signal_list = []
if self.buy_checkbox.isChecked():
signal_list.append(1)
if self.hold_checkbox.isChecked():
signal_list.append(2)
if self.sell_checkbox.isChecked():
signal_list.append(3)
if self.subpos_checkbox.isChecked():
signal_list.append(4)
if self.emptypos_checkbox.isChecked():
signal_list.append(5)
if len(signal_list)<=0:
# 删除所有的信号
self.remove_signal_show()
pass
else:
df = self.current_whole_df.copy()
df.reset_index(inplace=True)
if 1 in signal_list:
# 买 220,20,60 猩红
df_buy = df.loc[df['signal']==1].copy()
for i, row in df_buy.iterrows():
buy_fixed_target = pg.InfiniteLine(pos=(i, 0), movable=False, angle=90,
pen=pg.mkPen({'color': self.buy_color, 'width': 2}),
label=str(row['close']),
labelOpts={'position': 0.05, 'color': self.buy_color,
'movable': True, 'fill': (
self.buy_color[0], self.buy_color[1], self.buy_color[2], 30)})
self.signal_show_list.append(buy_fixed_target)
self.pw.addItem(buy_fixed_target)
pass
pass
if 2 in signal_list:
# 持股 255,140,0 深橙色
df['ext_hold_count'] = [i for i in range(len(df))]
df['ext_hold_0'] = 0
df.loc[df['signal'] == 2, 'ext_hold_0'] = 1
# start 值
df['ext_hold_1'] = df['ext_hold_0'] - df['ext_hold_0'].shift(1)
# end 值
df['ext_hold_2'] = df['ext_hold_0'] - df['ext_hold_0'].shift(-1)
df_hold_start = df.loc[df['ext_hold_1'] == 1].copy()
df_hold_end = df.loc[df['ext_hold_2'] == 1].copy()
hold_start_count_list = df_hold_start['ext_hold_count'].values.tolist()
hold_end_count_list = df_hold_end['ext_hold_count'].values.tolist()
if hold_end_count_list[0] < hold_start_count_list[0]:
hold_end_count_list = hold_end_count_list[1:]
if hold_end_count_list[-1] < hold_start_count_list[-1]:
hold_start_count_list = hold_start_count_list[:-1]
for i, item in enumerate(hold_end_count_list):
lr = pg.LinearRegionItem([hold_start_count_list[i], item], movable=False, brush=(
self.hold_color[0], self.hold_color[1], self.hold_color[2], 100))
lr.setZValue(-100)
self.signal_show_list.append(lr)
self.pw.addItem(lr)
pass
pass
if 3 in signal_list:
# 卖出 50,205,50 酸橙绿
df_sell = df.loc[df['signal'] == 3].copy()
for i, row in df_sell.iterrows():
sell_fixed_target = pg.InfiniteLine(pos=(i, 0), movable=False, angle=90,
pen=pg.mkPen({'color': self.sell_color, 'width': 2}),
label=str(row['close']),
labelOpts={'position': 0.05, 'color': self.sell_color,
'movable': True, 'fill': (
self.sell_color[0], self.sell_color[1], self.sell_color[2],
30)})
self.signal_show_list.append(sell_fixed_target)
self.pw.addItem(sell_fixed_target)
pass
pass
if 4 in signal_list:
# 减仓 24,252,0 草坪绿
df_subpos = df.loc[df['signal'] == 4].copy()
for i, row in df_subpos.iterrows():
subpos_fixed_target = pg.InfiniteLine(pos=(i, 0), movable=False, angle=90,
pen=pg.mkPen({'color': self.subpos_color, 'width': 2}),
label=str(row['close']),
labelOpts={'position': 0.05, 'color': self.subpos_color,
'movable': True, 'fill': (
self.subpos_color[0], self.subpos_color[1],
self.subpos_color[2],
30)})
self.signal_show_list.append(subpos_fixed_target)
self.pw.addItem(subpos_fixed_target)
pass
pass
if 5 in signal_list:
# 空仓 0,128,0 纯绿
df['ext_empty_count'] = [i for i in range(len(df))]
df['ext_empty_0'] = 0
df.loc[df['signal']==5,'ext_empty_0'] = 1
# start 值
df['ext_empty_1'] = df['ext_empty_0']-df['ext_empty_0'].shift(1)
# end 值
df['ext_empty_2'] = df['ext_empty_0']-df['ext_empty_0'].shift(-1)
df_empty_start = df.loc[df['ext_empty_1']==1].copy()
df_empty_end = df.loc[df['ext_empty_2']==1].copy()
empty_start_count_list = df_empty_start['ext_empty_count'].values.tolist()
empty_end_count_list = df_empty_end['ext_empty_count'].values.tolist()
if empty_end_count_list[0]<empty_start_count_list[0]:
empty_end_count_list = empty_end_count_list[1:]
if empty_end_count_list[-1]<empty_start_count_list[-1]:
empty_start_count_list = empty_start_count_list[:-1]
for i,item in enumerate(empty_end_count_list):
lr = pg.LinearRegionItem([empty_start_count_list[i],item],movable=False,brush=(self.emptypos_color[0],self.emptypos_color[1],self.emptypos_color[2],100))
lr.setZValue(-100)
self.signal_show_list.append(lr)
self.pw.addItem(lr)
pass
pass
df['signal_name'] = ''
for item in signal_list:
signal_name = ''
if item == 1:
signal_name = '买入'
if item == 2:
signal_name = '持股'
if item == 3:
signal_name = '卖出'
if item == 4:
signal_name = '减仓'
if item == 5:
signal_name = '空仓'
df.loc[df['signal'] == item,'signal_name'] = signal_name
df_table = df.loc[df['signal'].isin(signal_list)].copy()
table_data = df_table.loc[:, ['tradeDate', 'signal_name', 'close','signal_reason']].values.tolist()
self.table.set_table_full_data(table_data)
pass
pass
策略选择并执行控件
class StrategeMainWidget(QtWidgets.QWidget):
signal_runcode = QtCore.pyqtSignal(object)
signal_time = QtCore.pyqtSignal(object)
def __init__(self):
super().__init__()
self.thread_run: Thread = None
self.thread_time: Thread = None
self.running_graph_widget: QtWidgets.QWidget = None
self.init_data()
self.init_ui()
self.register_event()
pass
def init_data(self):
self.pre_output_dir = './'
self.please_select_str: str = '--请选择--'
self.stratege_name_list: List = []
self.tip_msg_0: str = '1.选择策略所在文件夹;2.选择策略;3.点击运行。'
self.stratege_path: str = ''
self.stratege_run_start_time = None
self.stratege_start = False
self.current_stratege_py_str: str = ''
pass
def init_ui(self):
self.setWindowTitle('策略运行小工具')
tip_0 = QtWidgets.QLabel('选择策略所在文件夹:')
self.stratege_dir_lineedit = QtWidgets.QLineEdit()
self.stratege_dir_lineedit.setReadOnly(True)
stratege_choice_btn = QtWidgets.QPushButton('选择文件夹')
stratege_choice_btn.clicked.connect(self.stratege_choice_btn_clicked)
tip_1 = QtWidgets.QLabel('策略:')
self.stratege_combox = QtWidgets.QComboBox()
self.stratege_combox.addItem(self.please_select_str)
self.stratege_combox.currentIndexChanged.connect(self.stratege_combox_currentIndexChanged)
self.run_btn = QtWidgets.QPushButton('运行')
self.run_btn.clicked.connect(self.run_btn_clicked)
self.force_stop_btn = QtWidgets.QPushButton('强制停止')
self.force_stop_btn.clicked.connect(self.force_stop_btn_clicked)
layout_top_left = QtWidgets.QGridLayout()
layout_top_left.addWidget(tip_0,0,0,1,1)
layout_top_left.addWidget(self.stratege_dir_lineedit,0,1,1,3)
layout_top_left.addWidget(stratege_choice_btn,0,4,1,1)
layout_top_left.addWidget(tip_1,1,0,1,1)
layout_top_left.addWidget(self.stratege_combox,1,1,1,2)
layout_top_left.addWidget(self.run_btn,1,3,1,1)
layout_top_left.addWidget(self.force_stop_btn,1,4,1,1)
self.tip_msg_label = QtWidgets.QLabel()
self.tip_msg_label.setWordWrap(True)
self.tip_msg_label.setText(self.tip_msg_0)
layout_top = QtWidgets.QHBoxLayout()
layout_top.addLayout(layout_top_left,3)
layout_top.addWidget(self.tip_msg_label,1)
self.code_textedit = QtWidgets.QTextEdit()
self.code_textedit.setReadOnly(True)
layout = QtWidgets.QVBoxLayout()
layout.addLayout(layout_top)
layout.addWidget(self.code_textedit)
self.setLayout(layout)
pass
def register_event(self):
self.signal_runcode.connect(self.thread_run_excuted)
self.signal_time.connect(self.thread_time_excuted)
pass
def stratege_choice_btn_clicked(self):
'''选择策略所在文件夹'''
path = QtWidgets.QFileDialog.getExistingDirectory(
self,
'打开策略所在文件夹',
self.pre_output_dir
)
if not path:
return
self.stratege_path = path
self.stratege_dir_lineedit.setText(path)
file_list = os.listdir(path)
temp_file_list = set(self.stratege_name_list)
for item in file_list:
if item.endswith('.py'):
temp_file_list.add(item)
self.stratege_name_list = list(temp_file_list)
self.stratege_combox.clear()
self.stratege_combox.addItem(self.please_select_str)
self.stratege_combox.addItems(self.stratege_name_list)
pass
def stratege_combox_currentIndexChanged(self,cur_i:int):
cur_txt = self.stratege_combox.currentText()
if not cur_txt or cur_txt == self.please_select_str:
self.code_textedit.clear()
return
file_path = self.stratege_path + os.path.sep + cur_txt
with open(file_path,'r',encoding='utf-8') as fr:
code_txt = fr.read()
self.code_textedit.setPlainText(code_txt)
pass
def run_btn_clicked(self):
'''运行按钮'''
py_str = self.code_textedit.toPlainText()
if len(py_str)<10:
QtWidgets.QMessageBox.information(
self,
'提示',
'请选择要执行的策略',
QtWidgets.QMessageBox.Yes
)
return
self.current_stratege_py_str = py_str
self.run_btn.setDisabled(True)
self.stratege_combox.setDisabled(True)
self.stratege_run_start_time = datetime.now()
self.stratege_start = True
if self.thread_run:
QtWidgets.QMessageBox.information(
self,
'提示',
'有策略正在运行',
QtWidgets.QMessageBox.Yes
)
return
self.thread_run = Thread(
target=self.running_run_thread,
args=(py_str,)
)
self.thread_run.start()
self.thread_time = Thread(
target=self.running_time_thread
)
self.thread_time.start()
pass
def force_stop_btn_clicked(self):
'''强制停止按钮'''
self.thread_run = None
self.thread_time = None
self.run_btn.setDisabled(False)
self.stratege_combox.setDisabled(False)
self.stratege_start = False
pass
def running_run_thread(self,data:str):
'''执行代码线程'''
namespace = {}
fun_stragegy = compile(data,'<string>','exec')
exec(fun_stragegy,namespace)
ret = namespace['excute_strategy']()
self.signal_runcode.emit(ret)
pass
def running_time_thread(self):
'''计时线程'''
while self.stratege_start:
now = datetime.now()
interval_time = (now-self.stratege_run_start_time).seconds
res_map = {'res':interval_time}
self.signal_time.emit(res_map)
time.sleep(1)
pass
def thread_run_excuted(self,data:Dict):
'''策略代码执行返回结果'''
self.run_btn.setDisabled(False)
self.stratege_combox.setDisabled(False)
if not self.running_graph_widget:
self.running_graph_widget = PyQtGraphRunningWidget()
self.running_graph_widget.set_data(data)
self.running_graph_widget.showMaximized()
self.thread_run = None
self.thread_time = None
self.stratege_start = False
QtWidgets.QMessageBox.information(
self,
'提示',
'当前策略运行完毕',
QtWidgets.QMessageBox.Yes
)
pass
def thread_time_excuted(self,data:Dict):
'''计时返回结果'''
res = data['res']
self.tip_msg_label.setText(f"{res}s")
pass
def closeEvent(self, a0: QtGui.QCloseEvent) -> None:
if self.thread_time:
self.thread_time.join()
if self.thread_run:
self.thread_run.join()
if self.running_graph_widget:
self.running_graph_widget.close()
self.close()
执行
if __name__ == '__main__':
QtCore.QCoreApplication.setAttribute(QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough)
app = QtWidgets.QApplication(sys.argv)
t_win = StrategeMainWidget()
t_win.showMaximized()
app.exec()
pass
运行,选择策略py文件所在目录,选择要执行的策略对应的py文件,点击运行
策略执行完毕后,会弹出新的窗体显示策略执行结果图表,在该图表窗体中可以选择指定日期期间,分别查看各个信号的情况
验证策略思想
由于双均线涉及的交易原则太多,本文以验证“ 1)买入和持仓原则中的 A”为例,讲述工具验证策略思想的过程
从右侧表格中可以看出 2020-09-10 的买入信号是通过买入交易原则 A 获得的,该交易原则的描述是“股价向上突破定性线,定性线上行,买入。”,查看左侧图,可以看出与描述相符
优化策略
优化策略是一个很细的活,这里以“1)买入和持仓原则中的 B”为例,讲述策略优化的过程。
右侧表格中总共出现了两次B, 一次是2020-04-23, 另一次是2020-10-14;放大看左侧图
2020-04-23这次的黄金交叉在次日就变为死亡交叉 ,这种黄金交叉有效时长太短暂,可以认为无效
2020-10-14这次的黄金交叉也和上面的一样,太短暂,可以当做无效
优化策略,增加自定义规则:
描述 | 代码逻辑(自定义) |
黄金交叉 | 前一日定量价格小于定性价格,当日定量价格大于定性价格,后续两日定量价格都大于定性价格 |
死亡交叉 | 前一日定量价格大于定性价格,当日定量价格小于定性价格,后续两日定量价格都小于定性价格 |
修改策略代码:
在实现原则的代码前加入
# 黄金交叉
df['gold_x'] = 0
df.loc[(df['ma5'].shift(1)<df['ma20'].shift(1)) & (df['ma5']>df['ma20']) & (df['ma5'].shift(-1)>df['ma20'].shift(-1)) & (df['ma5'].shift(-2)>df['ma20'].shift(-2)),'gold_x'] = 1
# 死亡交叉
df['dead_x'] = 0
df.loc[(df['ma5'].shift(1)>df['ma20'].shift(1)) & (df['ma5']<df['ma20']) & (df['ma5'].shift(-1)<df['ma20'].shift(-1)) & (df['ma5'].shift(-2)<df['ma20'].shift(-2)),'dead_x'] = 1
将买入B原则的代码修改为
# B.定量线上穿定性线形成黄金交叉,买入。
# 翻译:黄金交叉
df.loc[df['gold_x']==1,'signal'] = 1
df.loc[df['gold_x']==1,'signal_reason'] = 'B'
修正后完整的策略代码为
def excute_strategy():
'''
前置自定义规则:
A. 支撑和阻碍,定义为股价与均线价格之间的差值小于等于当前股价的2%
B. 股价下跌,连续三日收盘价涨跌幅为负
C. 均线下行,连续三日均线涨跌幅为负
D. 股价急速飙升,连续三日股价涨跌幅都大于等于5%
E. 远离,定义为股价与均线价格之间的差值大于等于当前股价的50%
1) 买入和持仓原则:
A. 股价向上突破定性线,定性线上行,买入。
翻译:收盘价涨跌幅为正 & 前一日收盘价小于定性价格 & 收盘价大于定性价格 & 定性线连续三日涨跌幅为正
B. 定量线上穿定性线形成黄金交叉,买入。
翻译:前一日定量价格小于前一日定性价格 & 当日定量价格大于定性价格
C. 股价下跌,遇定性线上行支撑止跌回升,买入。
翻译:连续三日收盘价涨跌幅为负 & 定性线连续三日涨跌幅为正 & 股价被定性线支撑 & 下一日收盘价正跌幅为正
D. 定性线上行,股价在定性线上方向上突破定量线,买入。
翻译:定性线连续三日涨跌幅为正 & 收盘价大于定性线价格 & (前一日收盘价小于前一日定量价格 & 当日收盘价大于定量价格)
E. 定量线下行,遇定性线上行支撑止跌后再度上升,买入。
翻译:连续三日定量线涨跌幅为负 & 连续三日定性线涨跌幅为正 & 定量线被定性线支撑 & 下一日定量线涨跌幅为正
F. 股价、定量线、定性线多头排列,持股
翻译:收盘价大于定量价格大于定性价格
2) 卖出和空仓原则
A. 股价跌破定性线,定性线走平或已拐头下行,卖出
翻译:前一日收盘价大于定性价格 & 当日收盘价小于定性价格 & 定性价格涨跌幅小于等于0
B. 多头排列期间,股价跌破定量线,减仓
翻译:前一日收盘价大于定量价格大于定性价格 & 当日收盘价小于定量价格
C. 股价急速飙升,远离定性线,减仓
翻译:股价飙升 & 收盘价远离定性价格
D. 定性线下行,空仓
翻译:连续三日定性价格涨跌幅为负
:return:
'''
import pandas as pd
import talib
df = pd.read_csv('E:/temp005/600660.csv',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']>='2010-01-01'].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']
# 双均线组合
# 定性线:20日均线 定量线:5日均线
df['ma5'] = talib.MA(df['closePrice'],timeperiod=5)
df['ma20'] = talib.MA(df['closePrice'],timeperiod=20)
# 定性线涨跌幅
df['ma20_rate'] = df['ma20']-df['ma20'].shift(1)
# 定性线上行还是下行:连续三天涨跌幅为正,为上行,值为1;连续三天涨跌幅为负,为下行,值为-1
df['ma20_direction'] = 0
df.loc[(df['ma20_rate']>0) & (df['ma20_rate'].shift(1)>0) & (df['ma20_rate'].shift(2)>0),'ma20_direction'] = 1
df.loc[(df['ma20_rate']<0) & (df['ma20_rate'].shift(1)<0) & (df['ma20_rate'].shift(2)<0),'ma20_direction'] = -1
# 定量线涨跌幅
df['ma5_rate'] = df['ma5'] - df['ma5'].shift(1)
df['ma5_direction'] = 0
df.loc[(df['ma5_rate']>0) & (df['ma5_rate'].shift(1)>0) & (df['ma5_rate'].shift(2)>0),'ma5_direction'] = 1
df.loc[(df['ma5_rate']<0) & (df['ma5_rate'].shift(1)<0) & (df['ma5_rate'].shift(2)<0),'ma5_direction'] = -1
# 收盘价涨跌幅
df['close_rate'] = df['closePrice'] - df['closePrice'].shift(1)
df['closePrice_direction'] = 0
df.loc[(df['close_rate'] > 0) & (df['close_rate'].shift(1) > 0) & (df['close_rate'].shift(2) > 0), 'closePrice_direction'] = 1
df.loc[(df['close_rate'] < 0) & (df['close_rate'].shift(1) < 0) & (df['close_rate'].shift(2) < 0), 'closePrice_direction'] = -1
# 收盘价到定性线之间的距离
df['close_ma20_distance'] = (abs(df['closePrice']-df['ma20'])/df['closePrice'])*100
# 收盘价到定量线之间的距离
df['close_ma5_distance'] = (abs(df['closePrice']-df['ma5'])/df['closePrice'])*100
# 定量线到定性线之间的距离
df['ma5_ma20_distance'] = (abs(df['ma5']-df['ma20'])/df['ma5'])*100
# 定性线到定量线之间的距离
df['ma20_ma5_distance'] = (abs(df['ma5']-df['ma20'])/df['ma20'])*100
# 股价是否飙升
df['pre_fast_up'] = 0
df.loc[(df['close_rate']>0) & (df['close_rate']/df['closePrice']>=0.05),'pre_fast_up'] = 1
df['fast_up'] = 0
df.loc[(df['pre_fast_up']==1) & (df['pre_fast_up'].shift(1)==1) & (df['pre_fast_up'].shift(2)==1),'fast_up'] = 1
# 黄金交叉
df['gold_x'] = 0
df.loc[(df['ma5'].shift(1)<df['ma20'].shift(1)) & (df['ma5']>df['ma20']) & (df['ma5'].shift(-1)>df['ma20'].shift(-1)) & (df['ma5'].shift(-2)>df['ma20'].shift(-2)),'gold_x'] = 1
# 死亡交叉
df['dead_x'] = 0
df.loc[(df['ma5'].shift(1)>df['ma20'].shift(1)) & (df['ma5']<df['ma20']) & (df['ma5'].shift(-1)<df['ma20'].shift(-1)) & (df['ma5'].shift(-2)<df['ma20'].shift(-2)),'dead_x'] = 1
# 1) 买入和持仓原则:
# 买入=1 持股=2(区间) 卖出=3 减仓=4 空仓=5(区间) 无信号=0
df['signal'] = 0
df['signal_reason'] = ''
# A. 股价向上突破定性线,定性线上行,买入。
# 翻译:收盘价涨跌幅为正 & 前一日收盘价小于定性价格 & 收盘价大于定性价格 & 定性线连续三日涨跌幅为正
df.loc[(df['close_rate']>0) & (df['closePrice'].shift(1)<df['ma20'].shift(1)) & (df['closePrice']>df['ma20']) & (df['ma20_direction']==1),'signal'] = 1
df.loc[(df['close_rate']>0) & (df['closePrice'].shift(1)<df['ma20'].shift(1)) & (df['closePrice']>df['ma20']) & (df['ma20_direction']==1),'signal_reason'] = 'A'
# B.定量线上穿定性线形成黄金交叉,买入。
# 翻译:黄金交叉
df.loc[df['gold_x']==1,'signal'] = 1
df.loc[df['gold_x']==1,'signal_reason'] = 'B'
# C.股价下跌,遇定性线上行支撑止跌回升,买入。
# 翻译:连续三日收盘价涨跌幅为负 & 定性线连续三日涨跌幅为正 & 股价被定性线支撑 & 下一日收盘价正跌幅为正
df.loc[(df['closePrice_direction']==-1) & (df['ma20_direction']==1) & (df['close_ma20_distance']<=2) & (df['close_rate'].shift(-1)>0),'signal'] = 1
df.loc[(df['closePrice_direction']==-1) & (df['ma20_direction']==1) & (df['close_ma20_distance']<=2) & (df['close_rate'].shift(-1)>0),'signal_reason'] = 'C'
# D.定性线上行,股价在定性线上方向上突破定量线,买入。
# 翻译:定性线连续三日涨跌幅为正 & 收盘价大于定性线价格 & (前一日收盘价小于前一日定量价格 & 当日收盘价大于定量价格)
df.loc[(df['ma20_direction']==1) & (df['closePrice']>df['ma20']) & (df['closePrice'].shift(1)<df['ma5'].shift(1)) & (df['closePrice']>df['ma5']),'signal'] = 1
df.loc[(df['ma20_direction']==1) & (df['closePrice']>df['ma20']) & (df['closePrice'].shift(1)<df['ma5'].shift(1)) & (df['closePrice']>df['ma5']),'signal_reason'] = 'D'
# E.定量线下行,遇定性线上行支撑止跌后再度上升,买入。
# 翻译:连续三日定量线涨跌幅为负 & 连续三日定性线涨跌幅为正 & 定量线被定性线支撑 & 下一日定量线涨跌幅为正
df.loc[(df['ma5_direction']==-1) & (df['ma20_direction']==1) & (df['ma5_ma20_distance']<=2) & (df['ma5_rate'].shift(-1)>0),'signal'] = 1
df.loc[(df['ma5_direction']==-1) & (df['ma20_direction']==1) & (df['ma5_ma20_distance']<=2) & (df['ma5_rate'].shift(-1)>0),'signal_reason'] = 'E'
# F.股价、定量线、定性线多头排列,持股
# 翻译:收盘价大于定量价格大于定性价格
df.loc[(df['closePrice']>df['ma5']) & (df['ma5']>df['ma20']),'signal'] = 2
df.loc[(df['closePrice']>df['ma5']) & (df['ma5']>df['ma20']),'signal_reason'] = 'F'
# 2) 卖出和空仓原则
# A.股价跌破定性线,定性线走平或已拐头下行,卖出
# 翻译:前一日收盘价大于定性价格 & 当日收盘价小于定性价格 & 定性价格涨跌幅小于等于0
df.loc[(df['closePrice'].shift(1)>df['ma20'].shift(1)) & (df['closePrice']<df['ma20']) & (df['ma20_rate']<=0),'signal'] = 3
df.loc[(df['closePrice'].shift(1)>df['ma20'].shift(1)) & (df['closePrice']<df['ma20']) & (df['ma20_rate']<=0),'signal_reason'] = 'A'
# B.多头排列期间,股价跌破定量线,减仓
# 翻译:前一日收盘价大于定量价格大于定性价格 & 当日收盘价小于定量价格
df.loc[(df['closePrice'].shift(1)>df['ma5'].shift(1)) & (df['ma5'].shift(1)>df['ma20'].shift(1)) & (df['closePrice']<df['ma5']),'signal'] = 4
df.loc[(df['closePrice'].shift(1)>df['ma5'].shift(1)) & (df['ma5'].shift(1)>df['ma20'].shift(1)) & (df['closePrice']<df['ma5']),'signal_reason'] = 'B'
# C.股价急速飙升,远离定性线,减仓
# 翻译:股价飙升 & 收盘价远离定性价格
df.loc[(df['fast_up']==1) & (df['close_ma5_distance']>=50),'signal'] = 4
df.loc[(df['fast_up']==1) & (df['close_ma5_distance']>=50),'signal_reason'] = 'C'
# D.定性线下行,空仓
# 翻译:连续三日定性价格涨跌幅为负
df.loc[df['ma20_direction']==-1,'signal'] = 5
df.loc[df['ma20_direction']==-1,'signal_reason'] = 'D'
title_str = '双均线组合信号'
whole_header = ['日期','收盘价','开盘价','最高价','最低价','ma5','ma20']
whole_df = df
whole_pd_header = ['tradeDate','closePrice','openPrice','highestPrice','lowestPrice','ma5','ma20']
line_pd_header = ['ma5','ma20']
line_color_list = [0,2]
line_data = {
'title_str':title_str,
'whole_header':whole_header,
'whole_df':whole_df,
'whole_pd_header':whole_pd_header,
'line_pd_header':line_pd_header,
'line_color_list':line_color_list
}
return line_data
运行优化后的策略,为方便验证优化结果,把数据的时间往前拉长到10年,执行
2014-05-06这个时间点的黄金交叉就靠谱多了
数据
链接:https://pan.baidu.com/s/1HPkMsDDyXTEgffoAVIhbZw
提取码:h80x