├── .gitignore ├── README.md ├── install.bat ├── main.py ├── pkg └── TA_Lib-0.4.22-cp37-cp37m-win_amd64.whl ├── requirements.txt ├── run.bat └── trade ├── __init__.py ├── chanlog.py ├── chantu ├── __init__.py ├── chart.html ├── cw.ico ├── echarts.min.js ├── jquery-1.10.1.min.js └── widget.py ├── constant.py ├── engine.py ├── jqdata.py ├── object.py ├── strategies ├── __init__.py ├── chan_class.py └── chan_strategy.py ├── template.py ├── ui ├── __init__.py ├── ico │ ├── GitHub.ico │ ├── __init__.py │ ├── about.ico │ ├── app.ico │ ├── cw.ico │ ├── database.ico │ ├── py.ico │ ├── restore.ico │ └── search.ico ├── mainwindow.py └── widget.py └── utility.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .idea 11 | .idea/ 12 | /.idea/ 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | 80 | # Jupyter Notebook 81 | .ipynb_checkpoints 82 | 83 | # IPython 84 | profile_default/ 85 | ipython_config.py 86 | 87 | # pyenv 88 | .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # config 135 | config/ 136 | config1/ 137 | dist/ 138 | .idea/ 139 | __pycache__/ 140 | data/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 缠论图形工具项目 2 | 3 | ## 安装依赖包 4 | 5 | **系统使用win10或者win11** 6 | 7 | **python环境最好使用Anaconda或者python3.7** 8 | 9 | **python一定要使用3.7版本, 并且安装路径不能包含有中文** 10 | 11 | 安装程序依赖 12 | 13 | ````shell 14 | install.bat 15 | ```` 16 | 17 | ## 运行项目 18 | 19 | ````shell 20 | run.bat 21 | ```` 22 | 23 | ## 使用说明 24 | 25 | 1. 通过菜单: 功能->股票缠图,在对话框中选择参数,默认参数仅供参考,参数意义: 26 | 聚宽账号:聚宽账号 27 | 聚宽密码:聚宽密码 28 | 股票代码:股票代码 29 | 开始日期:填写开始日期,以'-'分割 30 | K线类型: 缠论K线(经过包含处理)、普通K线(未经过包含处理) 31 | 中枢类型:笔、线段 32 | 用区间套:区间套对于二三类买卖点可选,第一类买卖点一定使用 33 | 使用共振:共振只针对1分钟级别和5分钟的B1、B2、B3 34 | 展现间隔:如果填写数字>0,则没隔多少根分钟K线渲染缠图 35 | 36 | 2. 聚宽账号需要申请试用API 37 | 38 | ## 缠论规则 39 | 40 | 1. 区间套对于二三类买卖点可选,第一类买卖点一定使用 41 | 2. 共振只针对1分钟级别和5分钟的B1、B2、B3 42 | 43 | ### 缠论实现 处理原则: 44 | 1、三买的类型:强和弱,高于前中枢的GG为强,否则为弱。 45 | 46 | 2、二买的类型:超强、强、中和弱,二买大于前中枢的ZG,反转笔突破前中枢的ZG为强,反转笔突破前中枢的ZD为中,否则为弱。 47 | 48 | 3、买卖点的类型分为:趋势类和盘整类。 49 | 50 | 4、B2失效的判断标准:以B2为起点的笔的顶不大于反转笔的顶。 51 | 52 | 5、S1有效的判断标准:必须有同级别向下的走势(盘整下或趋势下)完成。 53 | 54 | 6、出现第一类买卖点时的中枢处理:如果出现了一买,此时趋势由向下转为向上,以B1为起点的反转笔作为下一个中枢的进入笔;如果出现了一卖,此时趋势由向上转为向下,以S1为起点的反转笔作为下一个中枢的进入笔。 55 | 56 | 7、出现S1后,回撤不碰前中枢也不判断为B3,判断B3的前提是向上离开中枢的笔的顶点不是S1。 57 | 58 | 8、出现B1后,向上不碰前中枢也不判断为S3,判断S3的前提是向下离开中枢的笔的底点不是B1。 59 | -------------------------------------------------------------------------------- /install.bat: -------------------------------------------------------------------------------- 1 | pip install pip -U 2 | pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple 3 | pip install -r requirements.txt -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from trade.engine import MainEngine 2 | from trade.ui import MainWindow, create_qapp 3 | 4 | 5 | def main(): 6 | qapp = create_qapp() 7 | main_engine = MainEngine() 8 | main_window = MainWindow(main_engine) 9 | main_window.showMaximized() 10 | qapp.exec() 11 | 12 | 13 | if __name__ == "__main__": 14 | main() 15 | -------------------------------------------------------------------------------- /pkg/TA_Lib-0.4.22-cp37-cp37m-win_amd64.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dogfun/chanlun/b16708599e4cfefc4c9f8e4e198ddb867a8a4e00/pkg/TA_Lib-0.4.22-cp37-cp37m-win_amd64.whl -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | six==1.13.0 2 | wheel 3 | PyQt5==5.15.4 4 | PyQtWebEngine 5 | PyQtWebEngine-Qt5 6 | pyqtgraph==0.10.0 7 | qdarkstyle~=3.0.3 8 | numpy~=1.21.5 9 | pandas==0.24.2 10 | matplotlib 11 | seaborn 12 | jqdatasdk~=1.8.10 13 | wmi 14 | plotly~=5.5.0 15 | ./pkg/TA_Lib-0.4.22-cp37-cp37m-win_amd64.whl 16 | tzlocal -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | python main.py -------------------------------------------------------------------------------- /trade/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dogfun/chanlun/b16708599e4cfefc4c9f8e4e198ddb867a8a4e00/trade/__init__.py -------------------------------------------------------------------------------- /trade/chanlog.py: -------------------------------------------------------------------------------- 1 | from trade.utility import TEMP_DIR 2 | 3 | 4 | class ChanLog: 5 | 6 | @staticmethod 7 | def log(freq, symbol, data) -> None: 8 | data = str(data) 9 | if len(data) <= 0: 10 | return 11 | chan_path = TEMP_DIR.joinpath('chan_log') 12 | if not chan_path.exists(): 13 | chan_path.mkdir(parents=True) 14 | chan_file = chan_path.joinpath(symbol + '-' + freq + '.txt') 15 | with open(chan_file, 'a') as f: 16 | f.write(data + '\n') 17 | -------------------------------------------------------------------------------- /trade/chantu/__init__.py: -------------------------------------------------------------------------------- 1 | from .widget import ChanTuManager 2 | -------------------------------------------------------------------------------- /trade/chantu/chart.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | echarts 6 | 7 | 8 | 20 | 21 | 22 |
23 | 526 | 527 | -------------------------------------------------------------------------------- /trade/chantu/cw.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dogfun/chanlun/b16708599e4cfefc4c9f8e4e198ddb867a8a4e00/trade/chantu/cw.ico -------------------------------------------------------------------------------- /trade/chantu/widget.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | 3 | from PyQt5.QtWidgets import QMessageBox 4 | 5 | import pandas as pd 6 | from functools import partial 7 | from typing import List 8 | from trade.engine import MainEngine, Event 9 | from trade.ui import QtCore, QtWidgets, QtGui 10 | from PyQt5.QtWebEngineWidgets import QWebEngineView 11 | from pathlib import Path 12 | from trade.constant import FREQS, EVENT_CHANTU, EVENT_RENDER 13 | from trade.strategies.chan_strategy import Chan_Strategy 14 | from trade.object import HistoryRequest, Interval, Exchange 15 | from trade.jqdata import jqdata_client 16 | import talib as tl 17 | from threading import Thread 18 | import json 19 | import time 20 | 21 | ENGINE = 'CHANTU' 22 | 23 | 24 | class ChanTuManager(QtWidgets.QWidget): 25 | signal_chantu = QtCore.pyqtSignal(Event) 26 | signal_render = QtCore.pyqtSignal(Event) 27 | 28 | def __init__(self, main_engine: MainEngine): 29 | super().__init__() 30 | 31 | self.main_engine = main_engine 32 | self.tabWidget = QtWidgets.QTabWidget(self) 33 | self.ROOT_PATH = Path(__file__).parent 34 | self.URL = str(self.ROOT_PATH.joinpath("chart.html")).replace('\\', '/') 35 | 36 | self.ans = None 37 | self.thread = None 38 | self.state = False 39 | self.kline_html_map = {} 40 | self.init_ui() 41 | 42 | def init_ui(self): 43 | self.setWindowTitle("股票缠图") 44 | self.tabWidget.setObjectName("tabWidget") 45 | self.register_event() 46 | self.tabWidget.setMinimumWidth(1800) 47 | self.tabWidget.setMinimumHeight(900) 48 | for freq in FREQS: 49 | freq_widget = QtWidgets.QWidget() 50 | self.tabWidget.addTab(freq_widget, freq) 51 | kline_html = QWebEngineView() 52 | self.kline_html_map[freq] = kline_html 53 | kline_html.page().setBackgroundColor(QtGui.QColor(17, 17, 17)) 54 | kline_html.load(QtCore.QUrl(self.URL)) 55 | qhlayout = QtWidgets.QHBoxLayout() 56 | qhlayout.addWidget(kline_html) 57 | freq_widget.setLayout(qhlayout) 58 | part = partial(self.load, kline_html) 59 | kline_html.loadFinished.connect(part) 60 | 61 | def load(self, kline_html): 62 | kline_html.page().runJavaScript(self.ans) 63 | 64 | def run_chan(self, event: Event): 65 | if self.thread and self.thread.is_alive(): 66 | self.state = True 67 | self.thread.join() 68 | self.state = False 69 | self.thread = Thread( 70 | target=self.run_strategy, 71 | kwargs=(event.data) 72 | ) 73 | self.thread.start() 74 | 75 | def run_strategy(self, strategy_name, vt_symbol, jquser, jqpass, start_time, setting): 76 | # strategy_name = event.data['strategy_name'] 77 | # vt_symbol = event.data['vt_symbol'] 78 | # setting = event.data['setting'] 79 | # include = event.data['include'] 80 | # interval = event.data['interval'] 81 | # include_feature = event.data['include_feature'] 82 | # build_pivot = event.data['build_pivot'] 83 | self.render_interval = setting['time_interval'] 84 | chan_strategy = Chan_Strategy(engine=ENGINE, strategy_name=strategy_name, vt_symbol=vt_symbol, setting=setting) 85 | if len(vt_symbol.strip()) != 6: 86 | self.main_engine.put(event=Event(EVENT_RENDER, '错误的股票代码:' + vt_symbol)) 87 | print('错误的股票代码:' + vt_symbol) 88 | return 89 | exchange = Exchange.SZSE 90 | if vt_symbol[0] == '6': 91 | exchange = Exchange.SSE 92 | now_time = datetime.now() 93 | req = HistoryRequest( 94 | symbol=vt_symbol, 95 | exchange=exchange, 96 | interval=setting['interval'], 97 | start=start_time, 98 | end=now_time.date() 99 | ) 100 | time_start = time.time() 101 | 102 | jqdata_client.init(jquser, jqpass) 103 | if not jqdata_client.inited: 104 | self.main_engine.put(event=Event(EVENT_RENDER, '聚宽账号或者密码错误!')) 105 | return 106 | 107 | rawData, BarDataList = jqdata_client.query_history(req) 108 | time_end = time.time() 109 | if len(BarDataList) <= 0: 110 | self.main_engine.put(event=Event(EVENT_RENDER, '获取K线错误,请检查开始日期')) 111 | print('获取K线错误,请检查开始日期') 112 | return 113 | print('获取k线花费时间:', time_end - time_start) 114 | print('总的1分钟k线数据大小:' + str(len(BarDataList))) 115 | time_start = time_end 116 | i = 1 117 | for bar in BarDataList: 118 | chan_strategy.on_bar(bar) 119 | if self.state: 120 | break 121 | if self.render_interval > 0 and i % self.render_interval == 0: 122 | self.render_html(chan_strategy, setting['include']) 123 | i += 1 124 | self.render_html(chan_strategy, setting['include']) 125 | chan_map = chan_strategy.chan_freq_map 126 | for freq in chan_map: 127 | chan = chan_map[freq] 128 | print(freq) 129 | print(len(chan.chan_k_list)) 130 | # 统计信息 131 | self.sum_bs(chan.buy_list, chan.sell_list, freq) 132 | time_end = time.time() 133 | print('缠论计算 totally cost', time_end - time_start) 134 | 135 | def render_html(self, chan_strategy, include=True): 136 | chan_map = chan_strategy.chan_freq_map 137 | for freq in chan_map: 138 | klist = pd.DataFrame(columns=["date", "open", "close", "low", "high", "volume"], dtype=object) 139 | chan = chan_map[freq] 140 | format_str = '%Y-%m-%d %H:%M' 141 | if freq == FREQS[0]: 142 | format_str = '%Y-%m-%d' 143 | for k in chan.chan_k_list: 144 | klist = klist.append({ 145 | "date": k.datetime.strftime(format_str), "open": k.open_price, "close": k.close_price, 146 | "low": k.low_price, 147 | "high": k.high_price, "volume": k.volume 148 | }, ignore_index=True) 149 | bl = self.reFormatBS(chan.buy_list, format_str) 150 | sl = self.reFormatBS(chan.sell_list, format_str) 151 | if len(klist) <= 0: 152 | continue 153 | dif, dea, macd = tl.MACD(klist['close'].values, fastperiod=12, slowperiod=26, signalperiod=9) 154 | Macd = {} 155 | macd *= 2 156 | Macd['dif'] = dif.tolist() 157 | Macd['dea'] = dea.tolist() 158 | Macd['macd'] = macd.tolist() 159 | Macd = json.dumps(Macd) 160 | self.ans = self.plot( 161 | klist.to_json(orient='split'), 162 | self.reFormatLine(chan.stroke_list, format_str), 163 | self.reFormatLine(chan.line_list, format_str), 164 | self.reFormatPivot(chan.pivot_list, format_str), 165 | [], 166 | [], 167 | [], 168 | Macd, 169 | bl, 170 | sl, 171 | [], 172 | [] 173 | ) 174 | kline_html = self.kline_html_map[freq] 175 | self.load(kline_html) 176 | 177 | def render_event(self, event: Event): 178 | QMessageBox.information(self, self.windowTitle(), event.data, QMessageBox.Yes) 179 | 180 | def sum_bs(self, buy, sell, freq): 181 | b1_valid = set() 182 | b1_invalid = set() 183 | b2_valid = set() 184 | b2_invalid = set() 185 | b3_valid = set() 186 | b3_invalid = set() 187 | 188 | s1_valid = set() 189 | s1_invalid = set() 190 | s2_valid = set() 191 | s2_invalid = set() 192 | s3_valid = set() 193 | s3_invalid = set() 194 | for data in buy: 195 | if data[5] == 1: 196 | if data[2] == 'B1': 197 | b1_valid.add(data[0]) 198 | if data[2] == 'B2': 199 | b2_valid.add(data[0]) 200 | if data[2] == 'B3': 201 | b3_valid.add(data[0]) 202 | else: 203 | if data[2] == 'B1': 204 | b1_invalid.add(data[0]) 205 | if data[2] == 'B2': 206 | b2_invalid.add(data[0]) 207 | if data[2] == 'B3': 208 | b3_invalid.add(data[0]) 209 | 210 | for data in sell: 211 | if data[5] == 1: 212 | if data[2] == 'S1': 213 | s1_valid.add(data[0]) 214 | if data[2] == 'S2': 215 | s2_valid.add(data[0]) 216 | if data[2] == 'S3': 217 | s3_valid.add(data[0]) 218 | else: 219 | if data[2] == 'S1': 220 | s1_invalid.add(data[0]) 221 | if data[2] == 'S2': 222 | s2_invalid.add(data[0]) 223 | if data[2] == 'S3': 224 | s3_invalid.add(data[0]) 225 | print(freq + ':') 226 | print("valid: B1\tB2\tB3") 227 | print(f'{len(b1_valid)}\t{len(b2_valid)}\t{len(b3_valid)}') 228 | print("invalid: B1\tB2\tB3") 229 | print(f'{len(b1_invalid)}\t{len(b2_invalid)}\t{len(b3_invalid)}') 230 | 231 | print("valid: S1\tS2\tS3") 232 | print(f'{len(s1_valid)}\t{len(s2_valid)}\t{len(s3_valid)}') 233 | print("invalid: S1\tS2\tS3") 234 | print(f'{len(s1_invalid)}\t{len(s2_invalid)}\t{len(s3_invalid)}') 235 | 236 | def register_event(self): 237 | self.signal_chantu.connect(self.run_chan) 238 | self.main_engine.register(EVENT_CHANTU, self.signal_chantu.emit) 239 | self.signal_render.connect(self.render_event) 240 | self.main_engine.register(EVENT_RENDER, self.signal_render.emit) 241 | 242 | def plot(self, kline: List, 243 | bi: List = [], 244 | xd: List = [], 245 | zs: List = [], 246 | ma10: List = [], 247 | ma20: List = [], 248 | ma30: List = [], 249 | macd: List = [], 250 | bl: List = [], 251 | sl: List = [], 252 | x_bl: List = [], 253 | x_sl: List = [] 254 | ): 255 | pivot = self.reFormatPivotList(zs) 256 | b1, b2, b3, s1, s2, s3 = self.reFormatBuyAndSell(bl, sl) 257 | js = """ 258 | var data = getData(%s["data"]) 259 | var Macd = %s 260 | myChart.setOption({ 261 | xAxis: [ 262 | {data:data.date}, 263 | {data:data.date}, 264 | {data:data.date}], 265 | series: [{ 266 | name: 'K线', 267 | xAxisIndex: 0, 268 | yAxisIndex: 0, 269 | data: data.values, 270 | },{ 271 | name: 'MA10', 272 | data: %s["data"], 273 | },{ 274 | name: 'MA20', 275 | data: %s["data"], 276 | },{ 277 | name: 'MA30', 278 | data: %s["data"], 279 | },{ 280 | name: 'MACD', 281 | xAxisIndex: 1, 282 | yAxisIndex: 1, 283 | data: Macd["macd"], 284 | itemStyle:{ 285 | normal:{ 286 | color:function(params){ 287 | if(params.value >0){ 288 | return color_red; 289 | }else{ 290 | return color_green; 291 | } 292 | } 293 | } 294 | } 295 | },{ 296 | name: 'DIF', 297 | xAxisIndex: 1, 298 | yAxisIndex: 1, 299 | data: Macd["dif"] 300 | },{ 301 | name: 'DEA', 302 | xAxisIndex: 1, 303 | yAxisIndex: 1, 304 | data: Macd["dea"] 305 | },{ 306 | name: '成交量', 307 | xAxisIndex: 2, 308 | yAxisIndex: 2, 309 | data: data.volumes 310 | },{ 311 | name: '笔', 312 | xAxisIndex: 0, 313 | yAxisIndex: 0, 314 | data: %s 315 | },{ 316 | name: '一买', 317 | data: [], 318 | xAxisIndex: 0, 319 | yAxisIndex: 0, 320 | markPoint: { 321 | data: %s, 322 | symbolSize: 20, 323 | label: { 324 | formatter: function (param) { 325 | return param != null ? param.data.name.split(';').join('\\n') : ''; 326 | }, 327 | show: true, 328 | position: "bottom" 329 | } 330 | } 331 | },{ 332 | name: '二买', 333 | data: [], 334 | xAxisIndex: 0, 335 | yAxisIndex: 0, 336 | markPoint: { 337 | data: %s, 338 | symbolSize: 20, 339 | label: { 340 | formatter: function (param) { 341 | return param != null ? param.data.name.split(';').join('\\n') : ''; 342 | }, 343 | show: true, 344 | position: "bottom" 345 | } 346 | } 347 | },{ 348 | name: '三买', 349 | data: [], 350 | xAxisIndex: 0, 351 | yAxisIndex: 0, 352 | markPoint: { 353 | data: %s, 354 | symbolSize: 20, 355 | label: { 356 | formatter: function (param) { 357 | return param != null ? param.data.name.split(';').join('\\n') : ''; 358 | }, 359 | show: true, 360 | position: "bottom" 361 | } 362 | } 363 | },{ 364 | name: '一卖', 365 | data: [], 366 | xAxisIndex: 0, 367 | yAxisIndex: 0, 368 | markPoint: { 369 | data: %s, 370 | symbolSize: 20, 371 | label: { 372 | formatter: function (param) { 373 | return param != null ? param.data.name.split(';').join('\\n') : ''; 374 | }, 375 | show: true, 376 | position: "top" 377 | } 378 | } 379 | },{ 380 | name: '二卖', 381 | data: [], 382 | xAxisIndex: 0, 383 | yAxisIndex: 0, 384 | markPoint: { 385 | data: %s, 386 | symbolSize: 20, 387 | label: { 388 | formatter: function (param) { 389 | return param != null ? param.data.name.split(';').join('\\n') : ''; 390 | }, 391 | show: true, 392 | position: "top" 393 | } 394 | } 395 | },{ 396 | name: '三卖', 397 | data: [], 398 | xAxisIndex: 0, 399 | yAxisIndex: 0, 400 | markPoint: { 401 | data: %s, 402 | symbolSize: 20, 403 | label: { 404 | formatter: function (param) { 405 | return param != null ? param.data.name.split(';').join('\\n') : ''; 406 | }, 407 | show: true, 408 | position: "top" 409 | } 410 | } 411 | } 412 | ,{ 413 | name: '线段', 414 | xAxisIndex: 0, 415 | yAxisIndex: 0, 416 | data: %s 417 | },{ 418 | name: '中枢', 419 | data: [], 420 | xAxisIndex: 0, 421 | yAxisIndex: 0, 422 | markArea: { 423 | data: %s 424 | }, 425 | }] 426 | }); 427 | """ % (kline, macd, ma10, ma20, ma30, bi, b1, b2, b3, s1, s2, s3, xd, pivot) 428 | return js 429 | 430 | def reFormatPivotList(self, pivotList): 431 | """将中枢列表更改为js指定格式字符串。""" 432 | rePivotList = "[" 433 | for Item in pivotList: 434 | rePivotList += "[{coord: ['%s', %s]},{coord: ['%s', %s]}]," % (Item[0], Item[2], Item[1], Item[3]) 435 | rePivotList += "]" 436 | return rePivotList 437 | 438 | def reFormatPivot(self, pivot, format_str): 439 | reformatpivot = [] 440 | for Item in pivot: 441 | reformatpivot.append( 442 | [Item[0].strftime(format_str), Item[1].strftime(format_str), Item[2], Item[3]]) 443 | return reformatpivot 444 | 445 | def reFormatLine(self, line, format_str): 446 | reformatline = [] 447 | for Item in line: 448 | if Item[3] == 'up': 449 | reformatline.append([Item[2].strftime(format_str), Item[0]]) 450 | else: 451 | reformatline.append([Item[2].strftime(format_str), Item[1]]) 452 | return reformatline 453 | 454 | def reFormatBS(self, BS, format_str): 455 | rebs = [] 456 | for bs in BS: 457 | valid = '有效' 458 | if bs[5] == 0: 459 | valid = '无效' 460 | rebs.append([bs[0].strftime(format_str), bs[1], bs[2], bs[3].strftime(format_str), 461 | valid + ':' + bs[6].strftime(format_str), bs[7], bs[8]]) 462 | else: 463 | rebs.append([bs[0].strftime(format_str), bs[1], bs[2], bs[3].strftime(format_str), '', bs[7], bs[8]]) 464 | return rebs 465 | 466 | def reFormatBuyAndSell(self, buy, sell): 467 | """将买卖点列表更改为js指定格式字符串。""" 468 | reBuyList_1 = "[" 469 | reBuyList_2 = "[" 470 | reBuyList_3 = "[" 471 | if buy: 472 | for Item in buy: 473 | sep = '' 474 | if Item[4]: 475 | sep = ';' 476 | sth = '' 477 | if Item[6]: 478 | sth = ':' + Item[6] 479 | if Item[2] == 'B1': 480 | reBuyList_1 += "{name:'%s', coord: ['%s', %s], value: %s}," % ( 481 | Item[2] + ':' + Item[3] + sep + Item[4] + ';' + Item[5] + sth, Item[0], Item[1], Item[1]) 482 | elif Item[2] == 'B2': 483 | reBuyList_2 += "{name:'%s', coord: ['%s', %s], value: %s}," % ( 484 | Item[2] + ':' + Item[3] + sep + Item[4] + ';' + Item[5] + sth, Item[0], Item[1], Item[1]) 485 | elif Item[2] == 'B3': 486 | reBuyList_3 += "{name:'%s', coord: ['%s', %s], value: %s}," % ( 487 | Item[2] + ':' + Item[3] + sep + Item[4] + ';' + Item[5] + sth, Item[0], Item[1], Item[1]) 488 | reBuyList_1 += "]" 489 | reBuyList_2 += "]" 490 | reBuyList_3 += "]" 491 | 492 | reSellList_1 = "[" 493 | reSellList_2 = "[" 494 | reSellList_3 = "[" 495 | if sell: 496 | for Item in sell: 497 | sep = '' 498 | if Item[4]: 499 | sep = ';' 500 | sth = '' 501 | if Item[6]: 502 | sth = ':' + Item[6] 503 | if Item[2] == 'S1': 504 | reSellList_1 += "{name:'%s', coord: ['%s', %s], value: %s}," % ( 505 | Item[2] + ':' + Item[3] + sep + Item[4] + ';' + Item[5] + sth, Item[0], Item[1], Item[1]) 506 | elif Item[2] == 'S2': 507 | reSellList_2 += "{name:'%s', coord: ['%s', %s], value: %s}," % ( 508 | Item[2] + ':' + Item[3] + sep + Item[4] + ';' + Item[5] + sth, Item[0], Item[1], Item[1]) 509 | elif Item[2] == 'S3': 510 | reSellList_3 += "{name:'%s', coord: ['%s', %s], value: %s}," % ( 511 | Item[2] + ':' + Item[3] + sep + Item[4] + ';' + Item[5] + sth, Item[0], Item[1], Item[1]) 512 | reSellList_1 += "]" 513 | reSellList_2 += "]" 514 | reSellList_3 += "]" 515 | 516 | return reBuyList_1, reBuyList_2, reBuyList_3, reSellList_1, reSellList_2, reSellList_3 517 | -------------------------------------------------------------------------------- /trade/constant.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from datetime import timedelta 3 | 4 | EVENT_TRADE = "eTrade." 5 | EVENT_BAR = "eBar." 6 | EVENT_ORDER = "eOrder." 7 | EVENT_POSITION = "ePosition." 8 | EVENT_ACCOUNT = "eAccount." 9 | EVENT_CONTRACT = "eContract." 10 | EVENT_LOG = "eLog" 11 | EVENT_RENDER = 'eRender' 12 | EVENT_LOAD = "eLoad" 13 | EVENT_STRATEGY = "eStrategy" 14 | EVENT_STRATEGY_LOG = "eStrategyLog" 15 | EVENT_STRATEGY_STOPORDER = "eStopOrder" 16 | EVENT_CHANTU = "eCHANTU" 17 | EVENT_BACKTEST_LOG = "eBacktestLog" 18 | EVENT_BACKTEST_FINISHED = "eBacktestFinished" 19 | EVENT_BACKTEST_OPTIMIZATION_FINISHED = "eBacktestOptimizationFinished" 20 | 21 | 22 | class Direction(Enum): 23 | LONG = "多" # 1 24 | SHORT = "空" # 2 25 | 26 | 27 | class Offset(Enum): 28 | NONE = "" 29 | OPEN = "开" 30 | CLOSE = "平" 31 | CLOSETODAY = "平今" 32 | CLOSEYESTERDAY = "平昨" 33 | 34 | 35 | class Status(Enum): 36 | """ 37 | OrderStatus_Unknown = 0 38 | OrderStatus_New = 1 ## 已报 39 | OrderStatus_PartiallyFilled = 2 ## 部成 40 | OrderStatus_Filled = 3 ## 已成 41 | OrderStatus_Canceled = 5 ## 已撤 42 | OrderStatus_PendingCancel = 6 ## 待撤 43 | OrderStatus_Rejected = 8 ## 已拒绝 44 | OrderStatus_Suspended = 9 ## 挂起 45 | OrderStatus_PendingNew = 10 ## 待报 46 | OrderStatus_Expired = 12 ## 已过期 47 | """ 48 | UNKNOWN = "UNKNOWN" 49 | SUBMITTING = "已提交" 50 | PARTTRADED = "部分成交" 51 | ALLTRADED = "全部成交" 52 | CANCELLED = "已撤销" 53 | WAIT_CANCELLED = "待撤销" 54 | REJECTED = "拒单" 55 | SUSPENDED = "挂起" 56 | PENDINGNEW = "待提交" 57 | EXPIRED = "已过期" 58 | NOTTRADED = "未成交" 59 | 60 | 61 | STATUS_MAP = { 62 | 1: Status.SUBMITTING, 63 | 2: Status.PARTTRADED, 64 | 3: Status.ALLTRADED, 65 | 5: Status.CANCELLED, 66 | 6: Status.WAIT_CANCELLED, 67 | 8: Status.REJECTED, 68 | 9: Status.SUSPENDED, 69 | 10: Status.PENDINGNEW, 70 | 12: Status.EXPIRED, 71 | } 72 | 73 | 74 | class StopOrderStatus(Enum): 75 | WAITING = "等待中" 76 | CANCELLED = "已撤销" 77 | TRIGGERED = "已触发" 78 | 79 | 80 | class EngineType(Enum): 81 | LIVE = "实盘" 82 | BACKTEST = "回测" 83 | 84 | 85 | class OrderType(Enum): 86 | """ 87 | OrderType_Unknown = 0 88 | OrderType_Limit = 1 ## 限价委托 89 | OrderType_Market = 2 ## 市价委托 90 | OrderType_Stop = 3 ## 止损止盈委托 91 | """ 92 | UNKNOWN = "UNKNOWN" 93 | LIMIT = "限价委托" 94 | MARKET = "市价委托" 95 | STOP = "止损止盈委托" 96 | 97 | 98 | ORDERTYPE_MAP = { 99 | 1: OrderType.LIMIT, 100 | 2: OrderType.MARKET, 101 | 3: OrderType.STOP, 102 | } 103 | 104 | 105 | class Exchange(Enum): 106 | SZSE = "SZSE" 107 | SSE = "SSE" 108 | 109 | 110 | class Interval(Enum): 111 | MINUTE = "1m" 112 | MINUTE5 = "5m" 113 | MINUTE30 = "30m" 114 | 115 | HOUR = "1h" 116 | DAILY = "d" 117 | WEEKLY = "w" 118 | 119 | 120 | class METHOD(Enum): 121 | BZ = '标准操作方法' 122 | JJ = '激进操作方法' 123 | DX = '短线反弹操作方法' 124 | 125 | 126 | INTERVAL_DAYS = 30 127 | 128 | INTERVAL_DELTA_MAP = { 129 | Interval.MINUTE: timedelta(minutes=1), 130 | Interval.HOUR: timedelta(hours=1), 131 | Interval.DAILY: timedelta(days=1), 132 | } 133 | 134 | # ['月线', '周线', '日线', '60分钟', '30分钟', '15分钟', '5分钟', '1分钟'] 135 | FREQS = ['日线', '30分钟', '5分钟', '1分钟'] 136 | 137 | FREQS_INV = list(FREQS) 138 | FREQS_INV.reverse() 139 | 140 | FREQS_WINDOW = { 141 | '日线': [240, Interval.MINUTE, Interval.DAILY], 142 | '30分钟': [30, Interval.MINUTE, Interval.MINUTE30], 143 | '5分钟': [5, Interval.MINUTE, Interval.MINUTE5], 144 | '1分钟': [1, Interval.MINUTE, Interval.MINUTE], 145 | } 146 | 147 | INTERVAL_FREQ = { 148 | 'd': '日线', 149 | '30m': '30分钟', 150 | '5m': '5分钟', 151 | '1m': '1分钟' 152 | } 153 | 154 | STOPORDER_PREFIX = 'stop_order' 155 | 156 | PARAM_ZH_MAP = {'method': '交易方法', 'vt_symbol': '股票代码', 'symbol': '股票代码', 'strategy_name': '策略名称', 'include': 'K线包含', 157 | 'build_pivot': '中枢类型', 'qjt': '用区间套', 158 | 'gz': '使用共振', 'jb': '操作级别'} 159 | 160 | PARAM_ZH_MAP_INV = {'股票代码': 'vt_symbol', '策略名称': 'strategy_name', 'K线包含': 'include', '中枢类型': 'build_pivot', 161 | '用区间套': 'qjt', '使用共振': 'gz'} 162 | 163 | SETTING_ZH_MAP = { 164 | 165 | } 166 | 167 | ZH_TRANS_MAP = {'标准操作方法': 'Chan_Strategy_STD', '激进操作方法': 'Chan_Strategy_JJ', '短线反弹操作方法': 'Chan_Strategy_DXFT', 168 | '缠论K线': True, '普通K线': False, '笔中枢': False, '线段中枢': True, '是': True, '否': False 169 | } 170 | -------------------------------------------------------------------------------- /trade/engine.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Sequence, Type, Dict, List, Optional, Set 2 | from trade.constant import ( 3 | Status, 4 | INTERVAL_DELTA_MAP, 5 | StopOrderStatus, 6 | EVENT_BAR, EVENT_RENDER 7 | ) 8 | 9 | from trade.utility import load_json, save_json, extract_vt_symbol, round_to, check_run_time, trans_setting 10 | import os 11 | from trade.jqdata import jqdata_client 12 | 13 | STOP_STATUS_MAP = { 14 | Status.SUBMITTING: StopOrderStatus.WAITING, 15 | Status.NOTTRADED: StopOrderStatus.WAITING, 16 | Status.PARTTRADED: StopOrderStatus.TRIGGERED, 17 | Status.ALLTRADED: StopOrderStatus.TRIGGERED, 18 | Status.CANCELLED: StopOrderStatus.CANCELLED, 19 | Status.REJECTED: StopOrderStatus.CANCELLED 20 | } 21 | from .utility import get_folder_path, TRADER_DIR 22 | from collections import defaultdict 23 | from queue import Empty, Queue 24 | from threading import Thread 25 | from time import sleep 26 | from typing import Any, Callable, List 27 | 28 | EVENT_TIMER = "eTimer" 29 | 30 | 31 | class Event: 32 | def __init__(self, type: str, data: Any = None): 33 | self.type: str = type 34 | self.data: Any = data 35 | 36 | 37 | HandlerType = Callable[[Event], None] 38 | 39 | 40 | class MainEngine: 41 | 42 | def __init__(self, interval: int = 1): 43 | self._interval: int = interval 44 | self._queue: Queue = Queue() 45 | self._active: bool = False 46 | self._thread: Thread = Thread(target=self._run) 47 | self._timer: Thread = Thread(target=self._run_timer) 48 | self._bar: Thread = Thread(target=self._run_bar) 49 | self._bar_map: defaultdict = defaultdict(str) 50 | self._handlers: defaultdict = defaultdict(list) 51 | self._symbol_set: Set[str] = set() 52 | self._general_handlers: List = [] 53 | os.chdir(TRADER_DIR) 54 | self.start() 55 | 56 | def _run(self) -> None: 57 | while self._active: 58 | try: 59 | event = self._queue.get(block=True, timeout=1) 60 | self._process(event) 61 | except Empty: 62 | pass 63 | 64 | def _process(self, event: Event) -> None: 65 | if event.type in self._handlers: 66 | [handler(event) for handler in self._handlers[event.type]] 67 | 68 | if self._general_handlers: 69 | [handler(event) for handler in self._general_handlers] 70 | 71 | def _run_bar(self) -> None: 72 | while self._active: 73 | if check_run_time(): 74 | for vt_symbol in self._symbol_set: 75 | data = jqdata_client.query_bar_xq(vt_symbol) 76 | if data and (not self._bar_map[vt_symbol] or self._bar_map[vt_symbol] != str(data.datetime)): 77 | event = Event(EVENT_BAR, data) 78 | self.put(event) 79 | self._bar_map[vt_symbol] = str(data.datetime) 80 | sleep(5) 81 | 82 | def _run_timer(self) -> None: 83 | while self._active: 84 | sleep(self._interval) 85 | event = Event(EVENT_TIMER) 86 | self.put(event) 87 | 88 | def subscribe(self, vt_sybmol: str) -> None: 89 | self._symbol_set.add(vt_sybmol) 90 | 91 | def unsubscribe(self, vt_sybmol: str) -> None: 92 | self._symbol_set.remove(vt_sybmol) 93 | 94 | def start(self) -> None: 95 | self._active = True 96 | self._thread.start() 97 | self._timer.start() 98 | self._bar.start() 99 | 100 | def stop(self) -> None: 101 | self._active = False 102 | self._timer.join() 103 | self._thread.join() 104 | self._bar.join() 105 | 106 | def put(self, event: Event) -> None: 107 | self._queue.put(event) 108 | 109 | def register(self, type: str, handler: HandlerType) -> None: 110 | handler_list = self._handlers[type] 111 | if handler not in handler_list: 112 | handler_list.append(handler) 113 | 114 | def unregister(self, type: str, handler: HandlerType) -> None: 115 | handler_list = self._handlers[type] 116 | 117 | if handler in handler_list: 118 | handler_list.remove(handler) 119 | 120 | if not handler_list: 121 | self._handlers.pop(type) 122 | 123 | def register_general(self, handler: HandlerType) -> None: 124 | if handler not in self._general_handlers: 125 | self._general_handlers.append(handler) 126 | 127 | def unregister_general(self, handler: HandlerType) -> None: 128 | if handler in self._general_handlers: 129 | self._general_handlers.remove(handler) 130 | 131 | def write_log(self, msg: str, source: str = "") -> None: 132 | print(msg) 133 | 134 | def close(self) -> None: 135 | self.stop() -------------------------------------------------------------------------------- /trade/jqdata.py: -------------------------------------------------------------------------------- 1 | import json 2 | import calendar 3 | from datetime import timedelta, datetime 4 | from typing import List, Optional 5 | import time 6 | import pandas 7 | from pytz import timezone 8 | from numpy import ndarray 9 | import jqdatasdk as jq 10 | from trade.constant import Exchange, Interval 11 | from trade.object import BarData, HistoryRequest 12 | from pathlib import Path 13 | 14 | INTERVAL_VT2RQ = { 15 | Interval.MINUTE: "1m", 16 | Interval.HOUR: "60m", 17 | Interval.DAILY: "1d", 18 | } 19 | 20 | INTERVAL_ADJUSTMENT_MAP = { 21 | Interval.MINUTE: timedelta(minutes=1), 22 | Interval.HOUR: timedelta(hours=1), 23 | Interval.DAILY: timedelta() # no need to adjust for daily bar 24 | } 25 | 26 | CHINA_TZ = timezone("Asia/Shanghai") 27 | 28 | 29 | class JqdataClient: 30 | def __init__(self): 31 | self.username: str = '' 32 | self.password: str = '' 33 | 34 | self.inited: bool = False 35 | self.symbols: ndarray = None 36 | 37 | def init(self, username: str = "", password: str = "") -> bool: 38 | if self.inited: 39 | return True 40 | 41 | if username and password: 42 | self.username = username 43 | self.password = password 44 | 45 | if not self.username or not self.password: 46 | return False 47 | 48 | try: 49 | jq.auth(self.username, self.password) 50 | print("jq auth success.") 51 | self.inited = True 52 | 53 | except Exception as ex: 54 | print("聚宽账号或者密码错误!") 55 | print("jq auth fail:" + repr(ex)) 56 | return 57 | 58 | return True 59 | 60 | def to_jq_symbol(self, symbol: str, exchange: Exchange) -> str: 61 | """ 62 | CZCE product of JQData has symbol like "TA1905" while 63 | vt symbol is "TA905.CZCE" so need to add "1" in symbol. 64 | """ 65 | if exchange in [Exchange.SSE, Exchange.SZSE]: 66 | if exchange == Exchange.SSE: 67 | jq_symbol = f"{symbol}.XSHG" # 上海证券交易所 68 | else: 69 | jq_symbol = f"{symbol}.XSHE" # 深圳证券交易所 70 | elif exchange == Exchange.SHFE: 71 | jq_symbol = f"{symbol}.XSGE" # 上期所 72 | elif exchange == Exchange.CFFEX: 73 | jq_symbol = f"{symbol}.CCFX" # 中金所 74 | elif exchange == Exchange.DCE: 75 | jq_symbol = f"{symbol}.XDCE" # 大商所 76 | elif exchange == Exchange.INE: 77 | jq_symbol = f"{symbol}.XINE" # 上海国际能源期货交易所 78 | elif exchange == Exchange.CZCE: 79 | # 郑商所 的合约代码年份只有三位 需要特殊处理 80 | for count, word in enumerate(symbol): 81 | if word.isdigit(): 82 | break 83 | # Check for index symbol 84 | time_str = symbol[count:] 85 | if time_str in ["88", "888", "99", "8888"]: 86 | return f"{symbol}.XZCE" 87 | # noinspection PyUnboundLocalVariable 88 | product = symbol[:count] 89 | year = symbol[count] 90 | month = symbol[count + 1:] 91 | if year == "9": 92 | year = "1" + year 93 | else: 94 | year = "2" + year 95 | jq_symbol = f"{product}{year}{month}.XZCE" 96 | return jq_symbol.upper() 97 | 98 | def query_history2(self, req: HistoryRequest) -> Optional[List[BarData]]: 99 | # if self.symbols is None: 100 | # return None 101 | 102 | symbol = req.symbol 103 | exchange = req.exchange 104 | interval = req.interval 105 | start = req.start 106 | end = req.end 107 | 108 | jq_symbol = self.to_jq_symbol(symbol, exchange) 109 | # if rq_symbol not in self.symbols: 110 | # return None 111 | 112 | jq_interval = INTERVAL_VT2RQ.get(interval) 113 | if not jq_interval: 114 | return None 115 | 116 | # For adjust timestamp from bar close point (JQData) to open point 117 | adjustment = INTERVAL_ADJUSTMENT_MAP[interval] 118 | 119 | # For querying night trading period data 120 | end += timedelta(1) 121 | 122 | # Only query open interest for futures contract 123 | fields = ["open", "high", "low", "close", "volume"] 124 | 125 | data: List[BarData] = [] 126 | df = self.merge_data() 127 | if df is not None: 128 | for ix, row in df.iterrows(): 129 | bar = BarData( 130 | symbol=symbol, 131 | exchange=exchange, 132 | interval=interval, 133 | datetime=datetime.strptime(row["date"], '%Y-%m-%d %H:%M:%S'), 134 | open_price=row["open"], 135 | high_price=row["high"], 136 | low_price=row["low"], 137 | close_price=row["close"], 138 | volume=row["volume"], 139 | open_interest=row.get("open_interest", 0), 140 | gateway_name="JQ" 141 | ) 142 | 143 | data.append(bar) 144 | 145 | return df, data 146 | 147 | def query_bar(self, vt_symbol: str) -> Optional[BarData]: 148 | # if self.symbols is None: 149 | # return None 150 | symbol_list = vt_symbol.split('.') 151 | symbol = symbol_list[0] 152 | exchange = Exchange.SSE 153 | if symbol_list[1] == 'SZSE': 154 | exchange = Exchange.SZSE 155 | 156 | jq_symbol = self.to_jq_symbol(symbol, exchange) 157 | 158 | df = jq.get_price( 159 | jq_symbol, 160 | frequency=INTERVAL_VT2RQ.get(Interval.MINUTE), 161 | fields=["open", "high", "low", "close", "volume"], 162 | start_date=datetime.now() - timedelta(minutes=1), 163 | end_date=datetime.now(), 164 | skip_paused=True 165 | ) 166 | 167 | data: BarData = None 168 | 169 | if df is not None: 170 | for ix, row in df.iterrows(): 171 | dt = row.name.to_pydatetime() 172 | dt = CHINA_TZ.localize(dt) 173 | 174 | data = BarData( 175 | symbol=symbol, 176 | exchange=exchange, 177 | interval=Interval.MINUTE, 178 | datetime=dt, 179 | open_price=row["open"], 180 | high_price=row["high"], 181 | low_price=row["low"], 182 | close_price=row["close"], 183 | volume=row["volume"], 184 | open_interest=row.get("open_interest", 0) 185 | ) 186 | 187 | return data 188 | # return self.query_bar_xq(vt_symbol) 189 | 190 | 191 | def is_trade_day(self) -> bool: 192 | yd = datetime.now() - timedelta(days=1) 193 | days = jq.get_trade_days(start_date=yd.date(), end_date=None) 194 | if days and len(days) > 0: 195 | return True 196 | return False 197 | 198 | def query_history(self, req: HistoryRequest) -> Optional[List[BarData]]: 199 | # if self.symbols is None: 200 | # return None 201 | symbol = req.symbol 202 | exchange = req.exchange 203 | interval = req.interval 204 | start = req.start 205 | end = req.end 206 | 207 | jq_symbol = self.to_jq_symbol(symbol, exchange) 208 | # if rq_symbol not in self.symbols: 209 | # return None 210 | 211 | jq_interval = INTERVAL_VT2RQ.get(interval) 212 | if not jq_interval: 213 | return None 214 | 215 | # For querying night trading period data 216 | end += timedelta(1) 217 | 218 | # Only query open interest for futures contract 219 | fields = ["open", "high", "low", "close", "volume"] 220 | if not symbol.isdigit(): 221 | fields.append("open_interest") 222 | 223 | df = jq.get_price( 224 | jq_symbol, 225 | frequency=jq_interval, 226 | fields=["open", "high", "low", "close", "volume"], 227 | start_date=start, 228 | end_date=end, 229 | skip_paused=True 230 | ) 231 | 232 | data: List[BarData] = [] 233 | 234 | if df is not None: 235 | for ix, row in df.iterrows(): 236 | dt = row.name.to_pydatetime() 237 | dt = CHINA_TZ.localize(dt) 238 | 239 | bar = BarData( 240 | symbol=symbol, 241 | exchange=exchange, 242 | interval=interval, 243 | datetime=dt, 244 | open_price=row["open"], 245 | high_price=row["high"], 246 | low_price=row["low"], 247 | close_price=row["close"], 248 | volume=row["volume"], 249 | open_interest=row.get("open_interest", 0) 250 | ) 251 | 252 | data.append(bar) 253 | 254 | return df, data 255 | 256 | def merge_data(self): 257 | time_start = time.time() 258 | symbol = '600809' 259 | cwd = Path.cwd() 260 | temp_path = cwd.joinpath('trade/data/' + symbol) 261 | year_start = 2016 262 | year_end = 2022 263 | month_start = 1 264 | month_end = 13 265 | df = pandas.DataFrame(columns=["date", "open", "close", "low", "high", "volume"], dtype=object) 266 | for year in range(year_start, year_end): 267 | for month in range(month_start, month_end): 268 | firstDay, lastDay = getMonthFirstDayAndLastDay(year, month) 269 | file = str(temp_path) + '/' + firstDay.strftime("%Y-%m-%d") + '.csv' 270 | if Path(file).exists(): 271 | df = pandas.concat([df, pandas.read_csv(file)], sort=True) 272 | time_end = time.time() 273 | print('totally cost', time_end - time_start) 274 | return df 275 | 276 | 277 | jqdata_client = JqdataClient() 278 | 279 | 280 | def getMonthFirstDayAndLastDay(year=None, month=None): 281 | """ 282 | :param year: 年份,默认是本年,可传int或str类型 283 | :param month: 月份,默认是本月,可传int或str类型 284 | :return: firstDay: 当月的第一天,datetime.date类型 285 | lastDay: 当月的最后一天,datetime.date类型 286 | """ 287 | if not year or year > 2021 or not month or month > 12 or month < 0: 288 | return None, None 289 | # 获取当月第一天的星期和当月的总天数 290 | firstDayWeekDay, monthRange = calendar.monthrange(year, month) 291 | # 获取当月的第一天 292 | firstDay = datetime(year=year, month=month, day=1) 293 | lastDay = datetime(year=year, month=month, day=monthRange) 294 | 295 | return firstDay, lastDay 296 | 297 | -------------------------------------------------------------------------------- /trade/object.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from datetime import datetime, date 3 | from logging import INFO 4 | from .constant import Direction, Exchange, Interval, Offset, Status, OrderType, StopOrderStatus, STATUS_MAP, \ 5 | ORDERTYPE_MAP 6 | 7 | ACTIVE_STATUSES = set([Status.SUBMITTING, Status.NOTTRADED, Status.PARTTRADED]) 8 | 9 | 10 | @dataclass 11 | class TickData: 12 | symbol: str 13 | exchange: Exchange 14 | datetime: datetime 15 | 16 | name: str = "" 17 | volume: float = 0 18 | open_interest: float = 0 19 | last_price: float = 0 20 | last_volume: float = 0 21 | limit_up: float = 0 22 | limit_down: float = 0 23 | 24 | open_price: float = 0 25 | high_price: float = 0 26 | low_price: float = 0 27 | pre_close: float = 0 28 | 29 | bid_price_1: float = 0 30 | bid_price_2: float = 0 31 | bid_price_3: float = 0 32 | bid_price_4: float = 0 33 | bid_price_5: float = 0 34 | 35 | ask_price_1: float = 0 36 | ask_price_2: float = 0 37 | ask_price_3: float = 0 38 | ask_price_4: float = 0 39 | ask_price_5: float = 0 40 | 41 | bid_volume_1: float = 0 42 | bid_volume_2: float = 0 43 | bid_volume_3: float = 0 44 | bid_volume_4: float = 0 45 | bid_volume_5: float = 0 46 | 47 | ask_volume_1: float = 0 48 | ask_volume_2: float = 0 49 | ask_volume_3: float = 0 50 | ask_volume_4: float = 0 51 | ask_volume_5: float = 0 52 | 53 | def __post_init__(self): 54 | self.vt_symbol = f"{self.symbol}.{self.exchange.value}" 55 | 56 | 57 | @dataclass 58 | class BarData: 59 | symbol: str 60 | exchange: Exchange 61 | datetime: datetime 62 | 63 | interval: Interval = None 64 | volume: float = 0 65 | open_interest: float = 0 66 | open_price: float = 0 67 | high_price: float = 0 68 | low_price: float = 0 69 | close_price: float = 0 70 | 71 | def __post_init__(self): 72 | self.vt_symbol = f"{self.symbol}.{self.exchange.value}" 73 | 74 | 75 | @dataclass 76 | class OrderData: 77 | """ 78 | Order data 79 | """ 80 | 81 | symbol: str 82 | exchange: Exchange 83 | orderid: str 84 | 85 | type: OrderType = OrderType.LIMIT 86 | direction: Direction = None 87 | offset: Offset = Offset.NONE 88 | price: float = 0 89 | volume: float = 0 90 | traded: float = 0 91 | status: Status = Status.SUBMITTING 92 | datetime: datetime = None 93 | 94 | def __post_init__(self): 95 | """""" 96 | self.vt_symbol = f"{self.symbol}.{self.exchange.value}" 97 | self.vt_orderid = f"{self.orderid}" 98 | 99 | def is_active(self) -> bool: 100 | if self.status in ACTIVE_STATUSES: 101 | return True 102 | else: 103 | return False 104 | 105 | def create_cancel_request(self) -> "CancelRequest": 106 | req = CancelRequest( 107 | orderid=self.orderid, symbol=self.symbol, exchange=self.exchange 108 | ) 109 | return req 110 | 111 | 112 | @dataclass 113 | class TradeData: 114 | """ 115 | Trade data 116 | """ 117 | 118 | symbol: str 119 | exchange: Exchange 120 | orderid: str 121 | tradeid: str 122 | direction: Direction = None 123 | 124 | offset: Offset = Offset.NONE 125 | price: float = 0 126 | volume: float = 0 127 | datetime: datetime = None 128 | 129 | def __post_init__(self): 130 | self.vt_symbol = f"{self.symbol}.{self.exchange.value}" 131 | self.vt_orderid = f"{self.orderid}" 132 | self.vt_tradeid = f"{self.tradeid}" 133 | 134 | 135 | @dataclass 136 | class GMOrderData: 137 | """ 138 | GMOrder data 139 | """ 140 | 141 | order_id: str 142 | symbol: str 143 | ord_rej_reason_detail: str 144 | order_type: OrderType = OrderType.LIMIT 145 | side: Direction = None 146 | # offset: Offset = Offset.NONE 147 | price: float = 0 148 | volume: float = 0 149 | filled_vwap: float = 0 150 | status: Status = Status.SUBMITTING 151 | created_at: datetime = None 152 | 153 | def __post_init__(self): 154 | if self.order_type not in ORDERTYPE_MAP.keys(): 155 | self.order_type = ORDERTYPE_MAP.UNKNOWN 156 | else: 157 | self.order_type = STATUS_MAP[self.order_type] 158 | if self.status not in STATUS_MAP.keys(): 159 | self.status = Status.UNKNOWN 160 | else: 161 | self.status = STATUS_MAP[self.status] 162 | if self.side == 1: 163 | self.side = Direction.LONG 164 | else: 165 | self.side = Direction.SHORT 166 | 167 | self.vt_orderid = f"{self.order_id}.{self.symbol}" 168 | 169 | def is_active(self) -> bool: 170 | if self.status in ACTIVE_STATUSES: 171 | return True 172 | else: 173 | return False 174 | 175 | def create_cancel_request(self) -> "CancelRequest": 176 | req = CancelRequest( 177 | order_id=self.order_id, symbol=self.symbol 178 | ) 179 | return req 180 | 181 | 182 | @dataclass 183 | class PositionData: 184 | """ 185 | Positon data 186 | """ 187 | symbol: str 188 | side: Direction 189 | volume: float = 0 190 | volume_today: float = 0 191 | available: float = 0 192 | cost: float = 0 193 | vwap: float = 0 194 | fpnl: float = 0 195 | order_frozen: float = 0 196 | 197 | def __post_init__(self): 198 | self.volume=round(self.volume,2) 199 | self.volume_today=round(self.volume_today,2) 200 | self.available=round(self.available,2) 201 | self.cost=round(self.cost,2) 202 | self.vwap=round(self.vwap,2) 203 | self.fpnl=round(self.fpnl,2) 204 | if self.side == 1: 205 | self.side = Direction.LONG 206 | else: 207 | self.side = Direction.SHORT 208 | self.available = self.volume - self.volume_today - self.order_frozen 209 | self.vt_positionid = f"{self.symbol}" 210 | 211 | 212 | @dataclass 213 | class AccountData: 214 | """ 215 | Account data 216 | """ 217 | 218 | account_id: str 219 | nav: float = 0 220 | pnl: float = 0 221 | available: float = 0 222 | cum_trade: float = 0 223 | cum_commission: float = 0 224 | order_frozen: float = 0 225 | 226 | def __post_init__(self): 227 | self.nav=round(self.nav, 2) 228 | self.pnl=round(self.pnl, 2) 229 | self.available=round(self.available, 2) 230 | self.cum_trade=round(self.cum_trade, 2) 231 | self.cum_commission=round(self.cum_commission, 2) 232 | self.order_frozen=round(self.order_frozen, 2) 233 | self.vt_accountid = f"{self.account_id}" 234 | 235 | 236 | @dataclass 237 | class LogData: 238 | msg: str 239 | level: int = INFO 240 | 241 | def __post_init__(self): 242 | self.time = datetime.now() 243 | 244 | 245 | @dataclass 246 | class OrderRequest: 247 | symbol: str 248 | exchange: Exchange 249 | direction: Direction 250 | type: OrderType 251 | volume: float 252 | price: float = 0 253 | offset: Offset = Offset.NONE 254 | reference: str = "" 255 | 256 | def __post_init__(self): 257 | self.vt_symbol = f"{self.symbol}.{self.exchange.value}" 258 | 259 | def create_order_data(self, order_id: str) -> OrderData: 260 | order = OrderData( 261 | symbol=self.symbol, 262 | price=self.price, 263 | volume=self.volume 264 | ) 265 | return order 266 | 267 | 268 | @dataclass 269 | class CancelRequest: 270 | order_id: str 271 | symbol: str 272 | exchange: Exchange 273 | 274 | def __post_init__(self): 275 | self.vt_symbol = f"{self.symbol}.{self.exchange.value}" 276 | 277 | 278 | @dataclass 279 | class HistoryRequest: 280 | symbol: str 281 | exchange: Exchange 282 | start: date 283 | end: date = None 284 | interval: Interval = None 285 | 286 | def __post_init__(self): 287 | self.vt_symbol = f"{self.symbol}.{self.exchange.value}" 288 | 289 | 290 | @dataclass 291 | class StopOrder: 292 | vt_symbol: str 293 | direction: Direction 294 | offset: Offset 295 | price: float 296 | volume: float 297 | stop_orderid: str 298 | strategy_name: str 299 | lock: bool = False 300 | vt_orderids: list = field(default_factory=list) 301 | status: StopOrderStatus = StopOrderStatus.WAITING 302 | -------------------------------------------------------------------------------- /trade/strategies/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dogfun/chanlun/b16708599e4cfefc4c9f8e4e198ddb867a8a4e00/trade/strategies/__init__.py -------------------------------------------------------------------------------- /trade/strategies/chan_class.py: -------------------------------------------------------------------------------- 1 | import math 2 | import talib as tl 3 | from trade.object import BarData 4 | from copy import copy 5 | import numpy as np 6 | from trade.chanlog import ChanLog 7 | 8 | 9 | class Chan_Class: 10 | 11 | def __init__(self, freq, symbol, sell, buy, include=True, include_feature=False, build_pivot=False, qjt=True, 12 | gz=False, buy1=100, buy2=200, buy3=200, sell1=100, sell2=200, sell3=200): 13 | 14 | self.freq = freq 15 | self.symbol = symbol 16 | self.prev = None 17 | self.next = None 18 | self.k_list = [] 19 | self.chan_k_list = [] 20 | self.fx_list = [] 21 | self.stroke_list = [] 22 | self.stroke_index_in_k = {} 23 | self.line_list = [] 24 | self.line_index = {} 25 | self.line_index_in_k = {} 26 | self.line_feature = [] 27 | self.s_feature = [] 28 | self.x_feature = [] 29 | 30 | self.pivot_list = [] 31 | self.trend_list = [] 32 | self.buy_list = [] 33 | self.sell_list = [] 34 | self.macd = {} 35 | self.buy = buy 36 | self.sell = sell 37 | self.buy1 = buy1 38 | self.buy2 = buy2 39 | self.buy3 = buy3 40 | self.sell1 = sell1 41 | self.sell2 = sell2 42 | self.sell3 = sell3 43 | # 动力减弱最小指标 44 | self.dynamic_reduce = 0 45 | # 笔生成方法,new, old 46 | # 是否进行K线包含处理 47 | self.include = include 48 | # 中枢生成方法,stroke, line 49 | # 使用笔还是线段作为中枢的构成, true使用线段 50 | self.build_pivot = build_pivot 51 | # 线段生成方法 52 | # 是否进行K线包含处理 53 | self.include_feature = include_feature 54 | # 是否使用区间套 55 | self.qjt = qjt 56 | # 是否使用共振 57 | # 采用买卖点共振组合方法,1分钟一类买卖点+5分钟二类买卖点或三类买卖点,都属于共振 58 | self.gz = gz 59 | # 计数 60 | self.gz_delay_k_num = 0 61 | # 最大 62 | self.gz_delay_k_max = 12 63 | # 潜在bs 64 | self.gz_tmp_bs = None 65 | # 高级别bs 66 | self.gz_prev_last_bs = None 67 | 68 | def set_prev(self, chan): 69 | self.prev = chan 70 | 71 | def set_next(self, chan): 72 | self.next = chan 73 | 74 | def on_bar(self, bar: BarData): 75 | self.k_list.append(bar) 76 | if self.gz and self.gz_tmp_bs: 77 | self.gz_delay_k_num += 1 78 | self.on_gz() 79 | if self.include: 80 | self.on_process_k_include(bar) 81 | else: 82 | self.on_process_k_no_include(bar) 83 | 84 | def on_process_k_include(self, bar: BarData): 85 | """合并k线""" 86 | if len(self.chan_k_list) < 2: 87 | self.chan_k_list.append(bar) 88 | else: 89 | pre_bar = self.chan_k_list[-2] 90 | last_bar = self.chan_k_list[-1] 91 | if (last_bar.high_price >= bar.high_price and last_bar.low_price <= bar.low_price) or ( 92 | last_bar.high_price <= bar.high_price and last_bar.low_price >= bar.low_price): 93 | if last_bar.high_price > pre_bar.high_price: 94 | new_bar = copy(bar) 95 | new_bar.high_price = max(last_bar.high_price, new_bar.high_price) 96 | new_bar.low_price = max(last_bar.low_price, new_bar.low_price) 97 | new_bar.open_price = max(last_bar.open_price, new_bar.open_price) 98 | new_bar.close_price = max(last_bar.close_price, new_bar.close_price) 99 | else: 100 | new_bar = copy(bar) 101 | new_bar.high_price = min(last_bar.high_price, new_bar.high_price) 102 | new_bar.low_price = min(last_bar.low_price, new_bar.low_price) 103 | new_bar.open_price = min(last_bar.open_price, new_bar.open_price) 104 | new_bar.close_price = min(last_bar.close_price, new_bar.close_price) 105 | 106 | self.chan_k_list[-1] = new_bar 107 | ChanLog.log(self.freq, self.symbol, "combine k line: " + str(new_bar.datetime)) 108 | else: 109 | self.chan_k_list.append(bar) 110 | # 包含和非包含处理的k线都需要判断是否分型了 111 | self.on_process_fx(self.chan_k_list) 112 | 113 | def on_process_k_no_include(self, bar: BarData): 114 | """不用合并k线""" 115 | self.chan_k_list.append(bar) 116 | self.on_process_fx(self.chan_k_list) 117 | 118 | def on_process_fx(self, data): 119 | if len(data) > 2: 120 | flag = False 121 | if data[-2].high_price >= data[-1].high_price and data[-2].high_price >= data[-3].high_price: 122 | # 形成顶分型 [high_price, low, dt, direction, index of k_list] 123 | self.fx_list.append([data[-2].high_price, data[-2].low_price, data[-2].datetime, 'up', len(data) - 2]) 124 | flag = True 125 | 126 | if data[-2].low_price <= data[-1].low_price and data[-2].low_price <= data[-3].low_price: 127 | # 形成底分型 128 | self.fx_list.append([data[-2].high_price, data[-2].low_price, data[-2].datetime, 'down', len(data) - 2]) 129 | flag = True 130 | 131 | if flag: 132 | self.on_stroke(self.fx_list[-1]) 133 | ChanLog.log(self.freq, self.symbol, "fx_list: ") 134 | ChanLog.log(self.freq, self.symbol, self.fx_list[-1]) 135 | 136 | def on_stroke(self, data): 137 | """生成笔""" 138 | if len(self.stroke_list) < 1: 139 | self.stroke_list.append(data) 140 | ChanLog.log(self.freq, self.symbol, self.stroke_list) 141 | else: 142 | last_fx = self.stroke_list[-1] 143 | cur_fx = data 144 | pivot_flag = False 145 | # 分型之间需要超过三根chank线 146 | # 延申也是需要条件的 147 | if last_fx[3] == cur_fx[3]: 148 | if (last_fx[3] == 'down' and cur_fx[1] < last_fx[1]) or ( 149 | last_fx[3] == 'up' and cur_fx[0] > last_fx[0]): 150 | # 笔延申 151 | self.stroke_list[-1] = cur_fx 152 | pivot_flag = True 153 | 154 | else: 155 | # if (cur_fx[4] - last_fx[4] > 3) and ( 156 | # (cur_fx[3] == 'down' and cur_fx[1] < last_fx[1] and cur_fx[0] < last_fx[0]) or ( 157 | # cur_fx[3] == 'up' and cur_fx[0] > last_fx[0] and cur_fx[1] > last_fx[1])): 158 | if (cur_fx[4] - last_fx[4] > 3) and ( 159 | (cur_fx[3] == 'down' and cur_fx[0] < last_fx[1]) or ( 160 | cur_fx[3] == 'up' and cur_fx[1] > last_fx[0])): 161 | # 笔新增 162 | self.stroke_list.append(cur_fx) 163 | ChanLog.log(self.freq, self.symbol, "stroke_list: ") 164 | ChanLog.log(self.freq, self.symbol, self.stroke_list[-1]) 165 | # ChanLog.log(self.freq, self.symbol, self.stroke_list) 166 | pivot_flag = True 167 | 168 | # 修正倒数第二个分型是否是最高的顶分型或者是否是最低的底分型 169 | # 只修一笔,不修多笔 170 | start = -2 171 | stroke_change = None 172 | if pivot_flag and len(self.stroke_list) > 1: 173 | stroke_change = self.stroke_list[-2] 174 | if cur_fx[3] == 'down': 175 | while len(self.fx_list) > abs(start) and self.fx_list[start][2] > self.stroke_list[-2][2]: 176 | if self.fx_list[start][3] == 'up' and self.fx_list[start][0] > stroke_change[0]: 177 | if len(self.stroke_list) < 3 or (cur_fx[4] - self.fx_list[start][4] > 3): 178 | stroke_change = self.fx_list[start] 179 | start -= 1 180 | else: 181 | while len(self.fx_list) > abs(start) and self.fx_list[start][2] > self.stroke_list[-2][2]: 182 | if self.fx_list[start][3] == 'down' and self.fx_list[start][1] < stroke_change[1]: 183 | if len(self.stroke_list) < 3 or (cur_fx[4] - self.fx_list[start][4] > 3): 184 | stroke_change = self.fx_list[start] 185 | start -= 1 186 | if stroke_change and not stroke_change == self.stroke_list[-2]: 187 | ChanLog.log(self.freq, self.symbol, 'stroke_change') 188 | ChanLog.log(self.freq, self.symbol, stroke_change) 189 | self.stroke_list[-2] = stroke_change 190 | if len(self.stroke_list) > 2: 191 | cur_fx = self.stroke_list[-2] 192 | last_fx = self.stroke_list[-3] 193 | self.macd[cur_fx[2]] = self.cal_macd(last_fx[4], cur_fx[4]) 194 | # if cur_fx[4] - self.stroke_list[-2][4] < 4: 195 | # self.stroke_list.pop() 196 | 197 | if self.build_pivot: 198 | self.on_line(self.stroke_list) 199 | else: 200 | if len(self.stroke_list) > 1: 201 | cur_fx = self.stroke_list[-1] 202 | last_fx = self.stroke_list[-2] 203 | self.macd[cur_fx[2]] = self.cal_macd(last_fx[4], cur_fx[4]) 204 | self.on_line(self.stroke_list) 205 | if pivot_flag: 206 | self.on_pivot(self.stroke_list, None) 207 | 208 | def on_line(self, data): 209 | # line_list保持和stroke_list结构相同,都是由分型构成的 210 | # 特征序列则不同, 211 | if len(data) > 4: 212 | # ChanLog.log(self.freq, self.symbol, 'line_index:') 213 | # ChanLog.log(self.freq, self.symbol, self.line_index) 214 | pivot_flag = False 215 | if data[-1][3] == 'up' and data[-3][0] >= data[-1][0] and data[-3][0] >= data[-5][0]: 216 | if not self.line_list or self.line_list[-1][3] == 'down': 217 | if not self.line_list or ((len(self.stroke_list) - 3) - self.line_index[ 218 | str(self.line_list[-1][2])] > 2 and self.line_list[-1][1] < data[-3][0]): 219 | # 出现顶 220 | self.line_list.append(data[-3]) 221 | self.line_index[str(self.line_list[-1][2])] = len(self.stroke_list) - 3 222 | pivot_flag = True 223 | else: 224 | # 延申顶 225 | if self.line_list[-1][0] < data[-3][0]: 226 | self.line_list[-1] = data[-3] 227 | self.line_index[str(self.line_list[-1][2])] = len(self.stroke_list) - 3 228 | pivot_flag = True 229 | if data[-1][3] == 'down' and data[-3][1] <= data[-1][1] and data[-3][1] <= data[-5][1]: 230 | if not self.line_list or self.line_list[-1][3] == 'up': 231 | if not self.line_list or ((len(self.stroke_list) - 3) - self.line_index[ 232 | str(self.line_list[-1][2])] > 2 and self.line_list[-1][0] > data[-3][1]): 233 | # 出现底 234 | self.line_list.append(data[-3]) 235 | self.line_index[str(self.line_list[-1][2])] = len(self.stroke_list) - 3 236 | pivot_flag = True 237 | else: 238 | # 延申底 239 | if self.line_list[-1][1] > data[-3][1]: 240 | self.line_list[-1] = data[-3] 241 | self.line_index[str(self.line_list[-1][2])] = len(self.stroke_list) - 3 242 | pivot_flag = True 243 | 244 | line_change = None 245 | if pivot_flag and len(self.line_list) > 1: 246 | last_fx = self.line_list[-2] 247 | line_change = last_fx 248 | cur_fx = self.line_list[-1] 249 | cur_index = self.line_index[str(cur_fx[2])] 250 | start = -6 251 | last_index = self.line_index[str(last_fx[2])] 252 | if cur_index - last_index > 3: 253 | while len(self.stroke_list) >= abs(start - 2) and self.stroke_list[start][2] > last_fx[2]: 254 | if cur_fx[3] == 'down' and self.stroke_list[start][0] > self.stroke_list[start + 2][0] and \ 255 | self.stroke_list[start][0] > self.stroke_list[start - 2][0] and self.stroke_list[start][ 256 | 0] > line_change[0]: 257 | line_change = self.stroke_list[start] 258 | if cur_fx[3] == 'up' and self.stroke_list[start][1] < self.stroke_list[start + 2][1] and \ 259 | self.stroke_list[start][1] < self.stroke_list[start - 2][1] and self.stroke_list[start][ 260 | 1] < line_change[1]: 261 | line_change = self.stroke_list[start] 262 | start -= 2 263 | 264 | if line_change and not line_change == self.line_list[-2]: 265 | ChanLog.log(self.freq, self.symbol, 'line_change') 266 | ChanLog.log(self.freq, self.symbol, line_change) 267 | ChanLog.log(self.freq, self.symbol, self.line_list) 268 | self.line_index[str(line_change[2])] = self.line_index[str(self.line_list[-2][2])] 269 | self.line_list[-2] = line_change 270 | if len(self.line_list) > 2: 271 | cur_fx = self.line_list[-2] 272 | last_fx = self.line_list[-3] 273 | self.macd[cur_fx[2]] = self.cal_macd(last_fx[4], cur_fx[4]) 274 | 275 | if self.line_list and self.build_pivot: 276 | if len(self.line_list) > 1: 277 | cur_fx = self.line_list[-1] 278 | last_fx = self.line_list[-2] 279 | self.macd[cur_fx[2]] = self.cal_macd(last_fx[4], cur_fx[4]) 280 | ChanLog.log(self.freq, self.symbol, 'line_list:') 281 | ChanLog.log(self.freq, self.symbol, self.line_list[-1]) 282 | self.on_pivot(self.line_list, None) 283 | 284 | def on_pivot(self, data, type): 285 | # 中枢列表[[日期1,日期2,中枢低点,中枢高点, 中枢类型,中枢进入段,中枢离开段, 形成时间, GG, DD,BS,BS,TS]]] 286 | # 日期1:中枢开始的时间 287 | # 日期2:中枢结束的时间,可能延申 288 | # 中枢类型: ‘up', 'down' 289 | # BS: 买点 290 | # BS: 卖点 291 | # TS: 背驰段 292 | if len(data) > 5: 293 | # 构成笔或者是线段的分型 294 | cur_fx = data[-1] 295 | last_fx = data[-2] 296 | new_pivot = None 297 | flag = False 298 | # 构成新的中枢 299 | # 判断形成新的中枢的可能性 300 | if not self.pivot_list or (len(self.pivot_list) > 0 and len(data) - self.pivot_list[-1][6] > 4): 301 | cur_pivot = [data[-5][2], last_fx[2]] 302 | if cur_fx[3] == 'down' and data[-2][0] > data[-5][1]: 303 | ZD = max(data[-3][1], data[-5][1]) 304 | ZG = min(data[-2][0], data[-4][0]) 305 | DD = min(data[-3][1], data[-5][1]) 306 | GG = max(data[-2][0], data[-4][0]) 307 | if ZG > ZD: 308 | cur_pivot.append(ZD) 309 | cur_pivot.append(ZG) 310 | cur_pivot.append('down') 311 | cur_pivot.append(len(data) - 5) 312 | cur_pivot.append(len(data) - 2) 313 | cur_pivot.append(cur_fx[2]) 314 | cur_pivot.append(GG) 315 | cur_pivot.append(DD) 316 | cur_pivot.append([[], [], []]) 317 | cur_pivot.append([[], [], []]) 318 | cur_pivot.append([]) 319 | new_pivot = cur_pivot 320 | # 中枢形成,判断背驰 321 | if cur_fx[3] == 'up' and data[-2][1] < data[-5][0]: 322 | ZD = max(data[-2][1], data[-4][1]) 323 | ZG = min(data[-3][0], data[-5][0]) 324 | DD = min(data[-2][1], data[-4][1]) 325 | GG = max(data[-3][0], data[-5][0]) 326 | if ZG > ZD: 327 | cur_pivot.append(ZD) 328 | cur_pivot.append(ZG) 329 | cur_pivot.append('up') 330 | cur_pivot.append(len(data) - 5) 331 | cur_pivot.append(len(data) - 2) 332 | cur_pivot.append(cur_fx[2]) 333 | cur_pivot.append(GG) 334 | cur_pivot.append(DD) 335 | cur_pivot.append([[], [], []]) 336 | cur_pivot.append([[], [], []]) 337 | cur_pivot.append([]) 338 | new_pivot = cur_pivot 339 | if not self.pivot_list: 340 | if new_pivot: 341 | flag = True 342 | else: 343 | last_pivot = self.pivot_list[-1] 344 | if new_pivot and ((new_pivot[2] > last_pivot[3] and cur_fx[3] == 'up') or ( 345 | new_pivot[3] < last_pivot[2] and cur_fx[3] == 'down')): 346 | flag = True 347 | if type and new_pivot and type == new_pivot[4]: 348 | flag = True 349 | 350 | if len(self.pivot_list) > 0 and not flag: 351 | last_pivot = self.pivot_list[-1] 352 | ts = last_pivot[12] 353 | # 由于stroke/line_change,不断change中枢 354 | start = last_pivot[5] 355 | # 防止异常 356 | if len(data) <= start: 357 | self.pivot_list.pop() 358 | if not self.pivot_list: 359 | return 360 | last_pivot = self.pivot_list[-1] 361 | start = last_pivot[5] 362 | buy = last_pivot[10] 363 | sell = last_pivot[11] 364 | enter = data[start][2] 365 | exit = cur_fx[2] 366 | ee_data = [[data[start - 1], data[start]], 367 | [data[len(data) - 2], data[len(data) - 1]]] 368 | 369 | if last_pivot[4] == 'up': 370 | # stroke_change导致的笔减少了 371 | if len(data) > start + 3: 372 | last_pivot[2] = max(data[start + 1][1], data[start + 3][1]) 373 | last_pivot[3] = min(data[start][0], data[start + 2][0]) 374 | last_pivot[8] = max(data[start][0], data[start + 2][0]) 375 | last_pivot[9] = min(data[start + 1][1], data[start + 3][1]) 376 | if cur_fx[3] == 'up': 377 | if sell[0]: 378 | # 一卖后的顶分型判断一卖是否有效,无效则将上一个一卖置为无效 379 | if sell[0][1] < cur_fx[0] and len(data) - last_pivot[6] < 3: 380 | # 置一卖无效 381 | sell[0][5] = 0 382 | sell[0][6] = self.k_list[-1].datetime 383 | sell[0] = [] 384 | # 置二卖无效 385 | if sell[1]: 386 | sell[1][5] = 0 387 | sell[1][6] = self.k_list[-1].datetime 388 | sell[1] = [] 389 | # 判断背驰 390 | if self.on_turn(enter, exit, ee_data, last_pivot[4]) and cur_fx[0] > last_pivot[8]: 391 | ts.append([last_fx[2], cur_fx[2]]) 392 | if not sell[0]: 393 | # 形成一卖 394 | ans, qjt_pivot_list = self.qjt_turn(last_fx[2], cur_fx[2], 'up') 395 | if ans: 396 | sell[0] = [cur_fx[2], cur_fx[0], 'S1', self.k_list[-1].datetime, len(data) - 1, 1, 397 | None, self.cal_bs_type(), None, qjt_pivot_list] 398 | self.on_buy_sell(sell[0]) 399 | if sell[0] and not sell[1]: 400 | pos_sell1 = sell[0][4] 401 | if len(data) > pos_sell1 + 2: 402 | pos_fx = data[pos_sell1 + 2] 403 | if pos_fx[3] == 'up': 404 | if pos_fx[1] < sell[0][1]: 405 | # 形成二卖 406 | ans, qjt_pivot_list = self.qjt_trend(last_fx[2], cur_fx[2], 'up') 407 | if ans: 408 | sell[1] = [pos_fx[2], pos_fx[0], 'S2', self.k_list[-1].datetime, 409 | pos_sell1 + 2, 1, None, self.cal_bs_type(), None, qjt_pivot_list] 410 | self.on_buy_sell(sell[1]) 411 | else: 412 | # 一卖无效 413 | sell[0][5] = 0 414 | sell[0][6] = self.k_list[-1].datetime 415 | sell[0] = [] 416 | 417 | if cur_fx[0] < last_pivot[2] and not sell[2] and not buy[0]: 418 | # 形成三卖 419 | ans, qjt_pivot_list = self.qjt_trend(last_fx[2], cur_fx[2], 'up') 420 | if ans: 421 | condition = len(data) > 2 and data[-3][0] < last_pivot[2] and data[-3][2] > last_pivot[ 422 | 1] 423 | if not condition: 424 | sell[2] = [cur_fx[2], cur_fx[0], 'S3', self.k_list[-1].datetime, len(data) - 1, 1, 425 | None, self.cal_bs_type(), None, qjt_pivot_list] 426 | self.on_buy_sell(sell[2]) 427 | 428 | # if (not last_fx[1] > last_pivot[3]) and (not cur_fx[0] < last_pivot[2]): 429 | # last_pivot[1] = cur_fx[2] 430 | # last_pivot[6] = len(data) - 1 431 | 432 | else: 433 | # 判断是否延申 434 | if (not cur_fx[1] > last_pivot[3]) and (not last_fx[0] < last_pivot[2]): 435 | last_pivot[1] = cur_fx[2] 436 | last_pivot[6] = len(data) - 1 437 | else: 438 | # 判断形成第三类买点 439 | if cur_fx[1] > last_pivot[2] and not buy[2] and not sell[0]: 440 | ans, qjt_pivot_list = self.qjt_trend(last_fx[2], cur_fx[2], 'down') 441 | if ans: 442 | condition = len(data) > 2 and data[-3][1] > last_pivot[3] and data[-3][2] > \ 443 | last_pivot[1] 444 | if not condition: 445 | sth_pivot = last_pivot 446 | # if len(self.pivot_list) > 1: 447 | # sth_pivot = self.pivot_list[-2] 448 | buy[2] = [cur_fx[2], cur_fx[1], 'B3', self.k_list[-1].datetime, len(data) - 1, 449 | 1, None, self.cal_bs_type(), 450 | self.cal_b3_strength(cur_fx[1], sth_pivot), qjt_pivot_list] 451 | ChanLog.log(self.freq, self.symbol, 'B3-pivot') 452 | ChanLog.log(self.freq, self.symbol, sth_pivot) 453 | ChanLog.log(self.freq, self.symbol, buy[2]) 454 | self.on_buy_sell(buy[2]) 455 | 456 | 457 | else: 458 | # stroke_change导致的笔减少了 459 | if len(data) > start + 3: 460 | last_pivot[2] = max(data[start][1], data[start + 2][1]) 461 | last_pivot[3] = min(data[start + 1][0], data[start + 3][0]) 462 | last_pivot[8] = max(data[start + 1][0], data[start + 3][0]) 463 | last_pivot[9] = min(data[start][1], data[start + 2][1]) 464 | if cur_fx[3] == 'down': 465 | if buy[0]: 466 | # 一买后的底分型判断一买是否有效,无效则将上一个一买置为无效 467 | if buy[0][1] > cur_fx[1] and len(data) - last_pivot[6] < 3: 468 | # 置一买无效 469 | buy[0][5] = 0 470 | buy[0][6] = self.k_list[-1].datetime 471 | buy[0] = [] 472 | # 置二买无效 473 | if buy[1]: 474 | buy[1][5] = 0 475 | buy[1][6] = self.k_list[-1].datetime 476 | buy[1] = [] 477 | 478 | # 判断背驰 479 | if self.on_turn(enter, exit, ee_data, last_pivot[4]) and cur_fx[1] < last_pivot[9]: 480 | ts.append([last_fx[2], cur_fx[2]]) 481 | if not buy[0]: 482 | # 形成一买 483 | ans, qjt_pivot_list = self.qjt_turn(last_fx[2], cur_fx[2], 'down') 484 | if ans: 485 | buy[0] = [cur_fx[2], cur_fx[1], 'B1', self.k_list[-1].datetime, len(data) - 1, 1, 486 | None, self.cal_bs_type(), None, qjt_pivot_list] 487 | if self.gz: 488 | self.gz_prev_last_bs = self.get_prev_last_bs() 489 | self.gz_tmp_bs = buy 490 | buy[0][5] = 0 491 | else: 492 | self.on_buy_sell(buy[0]) 493 | 494 | if buy[0] and buy[0][5] == 1 and not buy[1]: 495 | pos_buy1 = buy[0][4] 496 | if len(data) > pos_buy1 + 2: 497 | pos_fx = data[pos_buy1 + 2] 498 | if pos_fx[3] == 'down': 499 | if pos_fx[1] > buy[0][1]: 500 | # 形成二买 501 | ans, qjt_pivot_list = self.qjt_trend(last_fx[2], cur_fx[2], 'down') 502 | if ans: 503 | sth_pivot = last_pivot 504 | # if len(self.pivot_list) > 1: 505 | # sth_pivot = self.pivot_list[-2] 506 | buy[1] = [pos_fx[2], pos_fx[1], 'B2', self.k_list[-1].datetime, 507 | pos_buy1 + 2, 1, None, self.cal_bs_type(), 508 | self.cal_b2_strength(pos_fx[1], last_fx, sth_pivot), 509 | qjt_pivot_list] 510 | self.on_buy_sell(buy[1]) 511 | else: 512 | # 一买无效 513 | buy[0][5] = 0 514 | buy[0][6] = self.k_list[-1].datetime 515 | buy[0] = [] 516 | 517 | if cur_fx[1] > last_pivot[3] and not buy[2] and not sell[0]: 518 | # 形成三买 519 | ans, qjt_pivot_list = self.qjt_trend(last_fx[2], cur_fx[2], 'down') 520 | if ans: 521 | condition = len(data) > 2 and data[-3][1] > last_pivot[3] and data[-3][2] > \ 522 | last_pivot[1] 523 | if not condition: 524 | sth_pivot = last_pivot 525 | # if len(self.pivot_list) > 1: 526 | # sth_pivot = self.pivot_list[-2] 527 | buy[2] = [cur_fx[2], cur_fx[1], 'B3', self.k_list[-1].datetime, len(data) - 1, 1, 528 | None, self.cal_bs_type(), self.cal_b3_strength(cur_fx[1], sth_pivot), 529 | qjt_pivot_list] 530 | ChanLog.log(self.freq, self.symbol, 'B3-pivot') 531 | ChanLog.log(self.freq, self.symbol, sth_pivot) 532 | ChanLog.log(self.freq, self.symbol, buy[2]) 533 | self.on_buy_sell(buy[2]) 534 | 535 | # if (not cur_fx[1] > last_pivot[3]) and (not last_fx[0] < last_pivot[2]): 536 | # last_pivot[1] = cur_fx[2] 537 | # last_pivot[6] = len(data) - 1 538 | else: 539 | # 判断是否延申 540 | if (not last_fx[1] > last_pivot[3]) and (not cur_fx[0] < last_pivot[2]): 541 | last_pivot[1] = cur_fx[2] 542 | last_pivot[6] = len(data) - 1 543 | else: 544 | # 判断形成第三类卖点 545 | if cur_fx[1] < last_pivot[3] and not sell[2] and not buy[0]: 546 | ans, qjt_pivot_list = self.qjt_trend(last_fx[2], cur_fx[2], 'up') 547 | if ans: 548 | condition = len(data) > 2 and data[-3][0] < last_pivot[2] and data[-3][2] > \ 549 | last_pivot[1] 550 | if not condition: 551 | sell[2] = [cur_fx[2], cur_fx[0], 'S3', self.k_list[-1].datetime, len(data) - 1, 552 | 1, None, self.cal_bs_type(), None, qjt_pivot_list] 553 | self.on_buy_sell(sell[2]) 554 | 555 | # 判断一二类买卖点失效 556 | if len(self.pivot_list) > 1: 557 | pre = self.pivot_list[-2] 558 | pre_buy = pre[10] 559 | pre_sell = pre[11] 560 | if pre_sell[0] and not pre_sell[1]: 561 | pos_sell1 = pre_sell[0][4] 562 | if len(data) > pos_sell1 + 2: 563 | pos_fx = data[pos_sell1 + 2] 564 | if pos_fx[3] == 'up': 565 | if pos_fx[0] < pre_sell[0][1]: 566 | # 形成二卖 567 | pre_sell[1] = [pos_fx[2], pos_fx[0], 'S2', self.k_list[-1].datetime, pos_sell1 + 2, 568 | 1, None, pre_sell[0][7], None] 569 | self.on_buy_sell(pre_sell[1]) 570 | else: 571 | # 一卖无效 572 | pre_sell[0][5] = 0 573 | pre_sell[0][6] = self.k_list[-1].datetime 574 | pre_sell[0] = [] 575 | 576 | if pre_buy[0] and pre_buy[0][5] == 1 and not pre_buy[1]: 577 | pos_buy1 = pre_buy[0][4] 578 | if len(data) > pos_buy1 + 2: 579 | pos_fx = data[pos_buy1 + 2] 580 | if pos_fx[3] == 'down': 581 | if pos_fx[1] > pre_buy[0][1]: 582 | sth_pivot = None 583 | # if len(self.pivot_list) > 2: 584 | # sth_pivot = self.pivot_list[-3] 585 | if len(self.pivot_list) > 1: 586 | sth_pivot = self.pivot_list[-2] 587 | # 形成二买 588 | pre_buy[1] = [pos_fx[2], pos_fx[1], 'B2', self.k_list[-1].datetime, pos_buy1 + 2, 1, 589 | None, pre_buy[0][7], 590 | self.cal_b2_strength(pos_fx[1], data[pos_buy1 + 1], sth_pivot)] 591 | self.on_buy_sell(pre_buy[1]) 592 | else: 593 | # 一买无效 594 | pre_buy[0][5] = 0 595 | pre_buy[0][6] = self.k_list[-1].datetime 596 | pre_buy[0] = [] 597 | 598 | # B2失效的判断标准:以B2为起点的笔的顶不大于反转笔的顶。 599 | # 判断条件有问题 600 | if pre_buy[1] and len(data) > pre_buy[1][4] + 2: 601 | start = pre_buy[1][4] + 1 602 | if data[start] < data[start - 2]: 603 | if pre_buy[0]: 604 | # 一买无效 605 | pre_buy[0][5] = 0 606 | pre_buy[0][6] = self.k_list[-1].datetime 607 | pre_buy[0] = [] 608 | pre_buy[1][5] = 0 609 | pre_buy[1][6] = self.k_list[-1].datetime 610 | pre_buy[1] = [] 611 | 612 | sth_pivot = None 613 | # if len(self.pivot_list) > 2: 614 | # sth_pivot = self.pivot_list[-3] 615 | if len(self.pivot_list) > 1: 616 | sth_pivot = self.pivot_list[-2] 617 | self.x_bs_pos(data, pre_buy, pre_sell, pre, sth_pivot) 618 | 619 | if len(self.pivot_list) > 2: 620 | pre2 = self.pivot_list[-3] 621 | pre_buy = pre2[10] 622 | pre_sell = pre2[11] 623 | pre1 = self.pivot_list[-2] 624 | if pre1[3] < last_pivot[2] and pre2[3] < pre1[2]: 625 | # 上升趋势 626 | if pre_sell[0]: 627 | # 置一卖无效 628 | pre_sell[0][5] = 0 629 | pre_sell[0][6] = self.k_list[-1].datetime 630 | pre_sell[0] = [] 631 | 632 | if pre_sell[1]: 633 | # 置二卖无效 634 | pre_sell[1][5] = 0 635 | pre_sell[1][6] = self.k_list[-1].datetime 636 | pre_sell[1] = [] 637 | # if pre1[2] > last_pivot[3]: 638 | # # 下降趋势 639 | # if pre_buy[0]: 640 | # # 置一买无效 641 | # pre_buy[0][5] = 0 642 | # pre_buy[0][6] = self.k_list[-1].datetime 643 | # pre_buy[0] = [] 644 | # 645 | # if pre_buy[1]: 646 | # # 置二买无效 647 | # pre_buy[1][5] = 0 648 | # pre_buy[1][6] = self.k_list[-1].datetime 649 | # pre_buy[1] = [] 650 | # 判断三类买卖点失效 651 | if sell[2] and sell[2][0] < last_pivot[1]: 652 | sell[2][5] = 0 653 | sell[2][6] = self.k_list[-1].datetime 654 | sell[2] = [] 655 | 656 | if buy[2] and buy[2][0] < last_pivot[1]: 657 | buy[2][5] = 0 658 | buy[2][6] = self.k_list[-1].datetime 659 | buy[2] = [] 660 | sth_pivot = last_pivot 661 | # if len(self.pivot_list) > 1: 662 | # sth_pivot = self.pivot_list[-2] 663 | self.x_bs_pos(data, buy, sell, last_pivot, sth_pivot) 664 | 665 | if flag: 666 | if new_pivot: 667 | self.pivot_list.append(new_pivot) 668 | # 中枢形成,判断背驰 669 | ts = new_pivot[12] 670 | buy = new_pivot[10] 671 | sell = new_pivot[11] 672 | enter = data[new_pivot[5]][2] 673 | exit = data[new_pivot[6]][2] 674 | ee_data = [[data[new_pivot[5] - 1], data[new_pivot[5]]], 675 | [data[new_pivot[6] - 1], data[new_pivot[6]]]] 676 | if new_pivot[4] == 'up': 677 | if self.on_turn(enter, exit, ee_data, new_pivot[4]) and cur_fx[0] > new_pivot[8]: 678 | ts.append([last_fx[2], cur_fx[2]]) 679 | if not sell[0]: 680 | # 形成一卖 681 | ans, qjt_pivot_list = self.qjt_turn(last_fx[2], cur_fx[2], 'up') 682 | if ans: 683 | sell[0] = [cur_fx[2], cur_fx[0], 'S1', self.k_list[-1].datetime, len(data) - 1, 1, 684 | None, self.cal_bs_type(), None, qjt_pivot_list] 685 | self.on_buy_sell(sell[0]) 686 | 687 | if new_pivot[4] == 'down': 688 | if self.on_turn(enter, exit, ee_data, new_pivot[4]) and cur_fx[1] < new_pivot[9]: 689 | ts.append([last_fx[2], cur_fx[2]]) 690 | if not buy[0]: 691 | # 形成一买 692 | ans, qjt_pivot_list = self.qjt_turn(last_fx[2], cur_fx[2], 'down') 693 | if ans: 694 | buy[0] = [cur_fx[2], cur_fx[1], 'B1', self.k_list[-1].datetime, len(data) - 1, 1, 695 | None, self.cal_bs_type(), None, qjt_pivot_list] 696 | if self.gz: 697 | self.gz_prev_last_bs = self.get_prev_last_bs() 698 | self.gz_tmp_bs = buy 699 | buy[0][5] = 0 700 | else: 701 | self.on_buy_sell(buy[0]) 702 | 703 | ChanLog.log(self.freq, self.symbol, "pivot_list:") 704 | ChanLog.log(self.freq, self.symbol, new_pivot) 705 | self.on_trend(new_pivot, data) 706 | 707 | def x_bs_pos(self, data, buy, sell, last_pivot, sth_pivot): 708 | if not self.gz: 709 | if buy[0] and len(data) > buy[0][4] and data[buy[0][4]][2] != buy[0][0]: 710 | pos_fx = data[buy[0][4]] 711 | buy[0][5] = 0 712 | buy[0][6] = self.k_list[-1].datetime 713 | # B1
sell[0][4] and data[sell[0][4]][2] != sell[0][0]: 718 | pos_fx = data[sell[0][4]] 719 | sell[0][5] = 0 720 | sell[0][6] = self.k_list[-1].datetime 721 | # S1>GG 722 | sell[0] = [pos_fx[2], pos_fx[0], 'S1', self.k_list[-1].datetime, sell[0][4], 1, None, sell[0][7], None] 723 | self.on_buy_sell(sell[0]) 724 | 725 | if buy[1] and len(data) > buy[1][4] and data[buy[1][4]][2] != buy[1][0]: 726 | pos_fx = data[buy[1][4]] 727 | buy[1][5] = 0 728 | buy[1][6] = self.k_list[-1].datetime 729 | if buy[0]: 730 | if pos_fx[1] > buy[0][1]: 731 | # todo 笔延申重新判断为强弱 732 | buy[1] = [pos_fx[2], pos_fx[1], 'B2', self.k_list[-1].datetime, buy[1][4], 1, None, buy[1][7], 733 | self.cal_b2_strength(pos_fx[1], data[buy[1][4]], sth_pivot)] 734 | self.on_buy_sell(buy[1]) 735 | else: 736 | # 一买无效 737 | buy[0][5] = 0 738 | buy[0][6] = self.k_list[-1].datetime 739 | 740 | if sell[1] and len(data) > sell[1][4] and data[sell[1][4]][2] != sell[1][0]: 741 | pos_fx = data[sell[1][4]] 742 | sell[1][5] = 0 743 | sell[1][6] = self.k_list[-1].datetime 744 | 745 | if pos_fx[0] < sell[0][1]: 746 | sell[1] = [pos_fx[2], pos_fx[0], 'S2', self.k_list[-1].datetime, sell[1][4], 1, None, sell[1][7], None] 747 | self.on_buy_sell(sell[1]) 748 | else: 749 | # 一卖无效 750 | sell[0][5] = 0 751 | sell[0][6] = self.k_list[-1].datetime 752 | 753 | if buy[2] and len(data) > buy[2][4] and data[buy[2][4]][2] != buy[2][0] and buy[2][0] > last_pivot[1]: 754 | pos_fx = data[buy[2][4]] 755 | buy[2][5] = 0 756 | buy[2][6] = self.k_list[-1].datetime 757 | if pos_fx[1] > last_pivot[3]: 758 | buy[2] = [pos_fx[2], pos_fx[1], 'B3', self.k_list[-1].datetime, buy[2][4], 1, None, buy[2][7], 759 | self.cal_b3_strength(pos_fx[1], sth_pivot)] 760 | ChanLog.log(self.freq, self.symbol, 'B3-pivot') 761 | ChanLog.log(self.freq, self.symbol, sth_pivot) 762 | ChanLog.log(self.freq, self.symbol, buy[2]) 763 | self.on_buy_sell(buy[2]) 764 | if sell[2] and len(data) > sell[2][4] and data[sell[2][4]][2] != sell[2][0] and sell[2][0] > last_pivot[1]: 765 | pos_fx = data[sell[2][4]] 766 | sell[2][5] = 0 767 | sell[2][6] = self.k_list[-1].datetime 768 | if pos_fx[0] < last_pivot[2]: 769 | sell[2] = [pos_fx[2], pos_fx[0], 'S3', self.k_list[-1].datetime, sell[2][4], 1, None, sell[2][7], None] 770 | self.on_buy_sell(sell[2]) 771 | 772 | def cal_bs_type(self): 773 | if len(self.pivot_list) > 1 and self.pivot_list[-1][4] == self.pivot_list[-2][4]: 774 | return '趋势' 775 | return '盘整' 776 | 777 | def cal_b3_strength(self, price, last_pivot): 778 | if last_pivot: 779 | if price > last_pivot[8]: 780 | return '强' 781 | return '弱' 782 | 783 | def cal_b2_strength(self, price, fx, last_pivot): 784 | if last_pivot: 785 | if price > last_pivot[3]: 786 | return '超强' 787 | if fx[0] > last_pivot[3]: 788 | return '强' 789 | if fx[0] > last_pivot[2]: 790 | return '中' 791 | return '弱' 792 | 793 | def cal_macd(self, start, end): 794 | sum = 0 795 | if start >= end: 796 | return sum 797 | if self.include: 798 | close_list = np.array([x.close_price for x in self.chan_k_list], dtype=np.double) 799 | else: 800 | close_list = np.array([x.close_price for x in self.k_list], dtype=np.double) 801 | dif, dea, macd = tl.MACD(close_list, fastperiod=12, 802 | slowperiod=26, signalperiod=9) 803 | for i, v in enumerate(macd.tolist()): 804 | if start <= i <= end: 805 | sum += abs(round(v, 4)) 806 | return round(sum, 4) 807 | 808 | def on_turn(self, start, end, ee_data, type): 809 | # ee_data: 笔/段列表 [[start, end]] 810 | # 判断背驰 811 | start_macd = None 812 | if start in self.macd: 813 | start_macd = self.macd[start] 814 | end_macd = None 815 | if end in self.macd: 816 | end_macd = self.macd[end] 817 | if start_macd and end_macd: 818 | if math.isnan(start_macd) or math.isnan(end_macd): 819 | if len(ee_data) > 1: 820 | if type == 'down': 821 | enter_slope = (ee_data[0][0][0] - ee_data[0][1][1]) / (ee_data[0][1][4] - ee_data[0][0][4] + 1) 822 | exit_slope = (ee_data[1][0][0] - ee_data[1][1][1]) / (ee_data[1][1][4] - ee_data[1][0][4] + 1) 823 | return abs(enter_slope) > abs(exit_slope) 824 | else: 825 | enter_slope = (ee_data[0][0][1] - ee_data[0][1][0]) / (ee_data[0][1][4] - ee_data[0][0][4] + 1) 826 | exit_slope = (ee_data[1][0][1] - ee_data[1][1][0]) / (ee_data[1][1][4] - ee_data[1][0][4] + 1) 827 | return abs(enter_slope) > abs(exit_slope) 828 | else: 829 | return start_macd > end_macd 830 | return False 831 | 832 | def qjt_turn0(self, start, end, type): 833 | # 区间套判断背驰:判断有无中枢和qjt_trend相同 834 | qjt_pivot_list = [] 835 | if not self.qjt: 836 | return True, qjt_pivot_list 837 | chan = self.next 838 | if not chan: 839 | return True, qjt_pivot_list 840 | ans = False 841 | ChanLog.log(self.freq, self.symbol, '区间套判断背驰:') 842 | ChanLog.log(self.freq, self.symbol, self.freq) 843 | ChanLog.log(self.freq, self.symbol, str(self.pivot_list[-1]) + ':' + str(start)) 844 | while chan: 845 | last_pivot = chan.pivot_list[-1] 846 | tmp = False 847 | if last_pivot[1] > start: 848 | if last_pivot[11][0]: 849 | tmp = True 850 | start = chan.stroke_list[last_pivot[11][0][4] - 1][2] 851 | if chan.build_pivot: 852 | start = chan.stroke_list[last_pivot[11][0][4] - 1][2] 853 | if last_pivot[10][0]: 854 | tmp = True 855 | start = chan.stroke_list[last_pivot[10][0][4] - 1][2] 856 | if chan.build_pivot: 857 | start = chan.stroke_list[last_pivot[10][0][4] - 1][2] 858 | ChanLog.log(self.freq, self.symbol, chan.freq + ':' + str(tmp)) 859 | ChanLog.log(self.freq, self.symbol, str(last_pivot) + ':' + str(start)) 860 | ans = ans or tmp 861 | chan = chan.next 862 | return ans, qjt_pivot_list 863 | 864 | def qjt_turn1(self, start, end, type): 865 | # 区间套判断背驰: 利用低级别的买卖点 866 | qjt_pivot_list = [] 867 | if not self.qjt: 868 | return True, qjt_pivot_list 869 | chan = self.next 870 | if not chan: 871 | return True, qjt_pivot_list 872 | ans = False 873 | ChanLog.log(self.freq, self.symbol, '区间套判断背驰:') 874 | ChanLog.log(self.freq, self.symbol, self.freq) 875 | ChanLog.log(self.freq, self.symbol, str(self.pivot_list[-1]) + ':' + str(start)) 876 | while chan: 877 | tmp = False 878 | for i in range(-1, -len(chan.buy_list), -1): 879 | buy_dt = chan.buy_list[i] 880 | if buy_dt >= end and buy_dt < start: 881 | tmp = True 882 | break 883 | tmp = False 884 | for i in range(-1, -len(chan.sell_list), -1): 885 | sell_dt = chan.sell_list[i] 886 | if sell_dt >= end and sell_dt < start: 887 | tmp = True 888 | break 889 | ans = ans or tmp 890 | chan = chan.next 891 | return ans, qjt_pivot_list 892 | 893 | def qjt_pivot(self, data, type): 894 | chan_pivot = Chan_Class(freq=self.freq, symbol=self.symbol, sell=None, buy=None, include=self.include, 895 | include_feature=self.include_feature, build_pivot=self.build_pivot, qjt=False) 896 | chan_pivot.macd = self.macd 897 | chan_pivot.k_list = self.chan_k_list 898 | new_data = [] 899 | for d in data: 900 | new_data.append(d) 901 | chan_pivot.on_pivot(new_data, type) 902 | return chan_pivot.pivot_list 903 | 904 | def qjt_turn(self, start, end, type): 905 | # 区间套判断背驰:重新形成新的中枢和买卖点 906 | qjt_pivot_list = [] 907 | # if not self.qjt: 908 | # return True, qjt_pivot_list 909 | chan = self.next 910 | if not chan: 911 | return True, qjt_pivot_list 912 | ans = True 913 | ChanLog.log(self.freq, self.symbol, '区间套判断背驰:') 914 | ChanLog.log(self.freq, self.symbol, self.freq) 915 | 916 | while chan: 917 | tmp = False 918 | data = [] 919 | if chan.build_pivot: 920 | for i in range(-1, -len(chan.line_list), -1): 921 | d = chan.line_list[i] 922 | if d[2] >= start: 923 | if d[2] <= end: 924 | data.append(d) 925 | else: 926 | if type == 'up' and d[3] == 'down': 927 | data.append(d) 928 | if type == 'down' and d[3] == 'up': 929 | data.append(d) 930 | break 931 | else: 932 | for i in range(-1, -len(chan.stroke_list), -1): 933 | d = chan.stroke_list[i] 934 | if d[2] >= start: 935 | if d[2] <= end: 936 | data.append(d) 937 | else: 938 | if type == 'up' and d[3] == 'down': 939 | data.append(d) 940 | if type == 'down' and d[3] == 'up': 941 | data.append(d) 942 | break 943 | data.reverse() 944 | chan_pivot_list = chan.qjt_pivot(data, type) 945 | ChanLog.log(self.freq, self.symbol, str(self.pivot_list[-1]) + ':' + str(start)) 946 | ChanLog.log(self.freq, self.symbol, chan_pivot_list) 947 | qjt_pivot_list.append(chan_pivot_list) 948 | if chan_pivot_list and len(chan_pivot_list[-1][12]) > 0: 949 | ts_item = chan_pivot_list[-1][12][-1] 950 | start = ts_item[0] 951 | end = ts_item[1] 952 | tmp = True 953 | chan = chan.next 954 | 955 | ans = tmp and ans 956 | if not ans: 957 | break 958 | 959 | return ans, qjt_pivot_list 960 | 961 | def qjt_trend0(self, start, end, type): 962 | # 区间套判断有无走势:判断有无中枢 963 | qjt_pivot_list = [] 964 | if not self.qjt: 965 | return True, qjt_pivot_list 966 | ChanLog.log(self.freq, self.symbol, '区间套判断有无走势:') 967 | ChanLog.log(self.freq, self.symbol, str(start) + '--' + str(end)) 968 | ChanLog.log(self.freq, self.symbol, str(self.pivot_list[-1])) 969 | chan = self.next 970 | if not chan: 971 | return True, qjt_pivot_list 972 | ans = False 973 | while chan: 974 | tmp = False 975 | for i in range(-1, -len(chan.pivot_list), -1): 976 | last_pivot = chan.pivot_list[i] 977 | if last_pivot[1] <= end and last_pivot[0] >= start: 978 | tmp = True 979 | break 980 | ans = ans or tmp 981 | ChanLog.log(self.freq, self.symbol, chan.freq + ':' + str(tmp)) 982 | chan = chan.next 983 | return ans, qjt_pivot_list 984 | 985 | def qjt_trend(self, start, end, type): 986 | # 区间套判断有无走势:重新形成中枢 987 | qjt_pivot_list = [] 988 | if not self.qjt: 989 | return True, qjt_pivot_list 990 | chan = self.next 991 | if not chan: 992 | return True, qjt_pivot_list 993 | ans = False 994 | ChanLog.log(self.freq, self.symbol, '区间套判断背驰:') 995 | ChanLog.log(self.freq, self.symbol, self.freq) 996 | 997 | while chan: 998 | tmp = False 999 | data = [] 1000 | if chan.build_pivot: 1001 | for i in range(-1, -len(chan.line_list), -1): 1002 | d = chan.line_list[i] 1003 | if d[2] >= start: 1004 | if d[2] <= end: 1005 | data.append(d) 1006 | else: 1007 | if type == 'up' and d[3] == 'down': 1008 | data.append(d) 1009 | if type == 'down' and d[3] == 'up': 1010 | data.append(d) 1011 | break 1012 | else: 1013 | for i in range(-1, -len(chan.stroke_list), -1): 1014 | d = chan.stroke_list[i] 1015 | if d[2] >= start: 1016 | if d[2] <= end: 1017 | data.append(d) 1018 | else: 1019 | if type == 'up' and d[3] == 'down': 1020 | data.append(d) 1021 | if type == 'down' and d[3] == 'up': 1022 | data.append(d) 1023 | break 1024 | data.reverse() 1025 | chan_pivot_list = chan.qjt_pivot(data, type) 1026 | ChanLog.log(self.freq, self.symbol, str(self.pivot_list[-1]) + ':' + str(start)) 1027 | ChanLog.log(self.freq, self.symbol, chan_pivot_list) 1028 | qjt_pivot_list.append(chan_pivot_list) 1029 | if not len(chan_pivot_list) > 0: 1030 | chan = chan.next 1031 | else: 1032 | tmp = True 1033 | ans = tmp or ans 1034 | if ans: 1035 | break 1036 | 1037 | return ans, qjt_pivot_list 1038 | 1039 | def on_gz(self): 1040 | """共振处理:只关联上一个级别""" 1041 | # 暂时 只处理买点B1 1042 | chan = self.prev 1043 | if not chan: 1044 | return 1045 | last_bs = None 1046 | if len(chan.buy_list) > 0: 1047 | last_bs = chan.buy_list[-1] 1048 | # B1不成立 1049 | if self.gz_delay_k_num >= self.gz_delay_k_max or (len(self.gz_tmp_bs) > 4 and self.gz_tmp_bs[0][5] == 0) or not \ 1050 | self.gz_tmp_bs[0]: 1051 | self.gz_delay_k_num = 0 1052 | self.gz_prev_last_bs = None 1053 | self.gz_tmp_bs[0] = [] 1054 | self.gz_tmp_bs = None 1055 | else: 1056 | if last_bs and last_bs != self.gz_prev_last_bs and ( 1057 | last_bs[1] == 'B2' or last_bs[2] == 'B3' or last_bs[2] == 'B1'): 1058 | ChanLog.log(self.freq, self.symbol, 'gz:' + str(self.gz_delay_k_num) + ':') 1059 | ChanLog.log(self.freq, self.symbol, last_bs) 1060 | ChanLog.log(self.freq, self.symbol, self.gz_prev_last_bs) 1061 | ChanLog.log(self.freq, self.symbol, self.gz_tmp_bs[0]) 1062 | if self.gz_tmp_bs[0]: 1063 | self.gz_tmp_bs[0][3] = self.k_list[-1].datetime 1064 | self.gz_tmp_bs[0][5] = 1 1065 | self.on_buy_sell(self.gz_tmp_bs[0]) 1066 | self.gz_delay_k_num = 0 1067 | self.gz_prev_last_bs = None 1068 | self.gz_tmp_bs = None 1069 | 1070 | def get_prev_last_bs(self): 1071 | chan = self.prev 1072 | if not chan or len(chan.buy_list) < 1: 1073 | return None 1074 | return chan.buy_list[-1] 1075 | 1076 | def on_trend(self, new_pivot, data): 1077 | # 走势列表[[日期1,日期2,走势类型,[背驰点], [中枢]]] 1078 | if not self.trend_list: 1079 | type = 'pzup' 1080 | if new_pivot[4] == 'down': 1081 | type = 'pzdown' 1082 | self.trend_list.append([new_pivot[0], new_pivot[1], type, [], [len(self.pivot_list) - 1]]) 1083 | else: 1084 | last_trend = self.trend_list[-1] 1085 | if last_trend[2] == 'up': 1086 | if new_pivot[4] == 'up': 1087 | last_trend[1] = new_pivot[1] 1088 | last_trend[4].append(len(self.pivot_list) - 1) 1089 | else: 1090 | self.trend_list.append([new_pivot[0], new_pivot[1], 'pzdown', [], [len(self.pivot_list) - 1]]) 1091 | if last_trend[2] == 'down': 1092 | if new_pivot[4] == 'down': 1093 | last_trend[1] = new_pivot[1] 1094 | last_trend[4].append(len(self.pivot_list) - 1) 1095 | else: 1096 | self.trend_list.append([new_pivot[0], new_pivot[1], 'pzup', [], [len(self.pivot_list) - 1]]) 1097 | if last_trend[2] == 'pzup': 1098 | if new_pivot[4] == 'up': 1099 | last_trend[1] = new_pivot[1] 1100 | last_trend[4].append(len(self.pivot_list) - 1) 1101 | last_trend[2] = 'up' 1102 | else: 1103 | self.trend_list.append([new_pivot[0], new_pivot[1], 'pzdown', [], [len(self.pivot_list) - 1]]) 1104 | if last_trend[2] == 'pzdown': 1105 | if new_pivot[4] == 'down': 1106 | last_trend[1] = new_pivot[1] 1107 | last_trend[4].append(len(self.pivot_list) - 1) 1108 | last_trend[2] = 'down' 1109 | else: 1110 | self.trend_list.append([new_pivot[0], new_pivot[1], 'pzup', [], [len(self.pivot_list) - 1]]) 1111 | 1112 | def on_buy_sell(self, data, valid=True): 1113 | if not data: 1114 | return 1115 | # 买点列表[[日期,值,类型, evaluation_time, 买点位置=index of stroke/line, valid, invalid_time, 类型, 强弱, qjt_pivot_list]] 1116 | # 卖点列表[[日期,值,类型, evaluation_time, 买点位置=index of stroke/line, valid, invalid_time, 类型, 强弱, qjt_pivot_list]] 1117 | if valid: 1118 | if data[2].startswith('B'): 1119 | ChanLog.log(self.freq, self.symbol, 'buy:') 1120 | ChanLog.log(self.freq, self.symbol, data) 1121 | self.buy_list.append(data) 1122 | if self.buy: 1123 | self.buy(self.k_list[-1].close_price, 100, self.freq) 1124 | else: 1125 | ChanLog.log(self.freq, self.symbol, 'sell:') 1126 | ChanLog.log(self.freq, self.symbol, data) 1127 | self.sell_list.append(data) 1128 | if self.sell: 1129 | self.sell(self.k_list[-1].close_price, 100, self.freq) 1130 | else: 1131 | pass 1132 | -------------------------------------------------------------------------------- /trade/strategies/chan_strategy.py: -------------------------------------------------------------------------------- 1 | from trade.utility import BarGenerator 2 | from trade.template import Template 3 | from trade.object import ( 4 | StopOrder, 5 | TickData, 6 | BarData, 7 | TradeData, 8 | OrderData, 9 | ) 10 | from trade.constant import FREQS, INTERVAL_FREQ, Interval, FREQS_WINDOW, METHOD, Direction, Offset 11 | from .chan_class import Chan_Class 12 | 13 | 14 | class Chan_Strategy(Template): 15 | """首页展示行情""" 16 | 17 | method = METHOD.BZ 18 | symbol = '' 19 | include = True 20 | build_pivot = False 21 | qjt = False 22 | gz = False 23 | jb = Interval.MINUTE 24 | 25 | parameters = ['method', 'symbol', 'include', 'build_pivot', 'qjt', 'gz', 'jb'] 26 | buy1 = 100 27 | buy2 = 200 28 | buy3 = 200 29 | sell1 = 100 30 | sell2 = 200 31 | sell3 = 200 32 | variables = ['buy1', 'buy2', 'buy3', 'sell1', 'sell2', 'sell3'] 33 | 34 | # parameters = ["period", "stroke_type", "pivot_type", "buy1", "buy2", "buy3", "sell1", "sell2", "sell3", 35 | # "dynamic_reduce"] 36 | # variables = ["stroke_list", "line_list", "pivot_list", "trend_list", "buy_list", "sell_list"] 37 | 38 | def __init__(self, engine, strategy_name, vt_symbol, setting): 39 | """ 40 | 从1分钟->5->30->1d 41 | 先做一个级别,之后再做其他的级别 42 | """ 43 | if setting: 44 | if 'method' in setting.keys(): 45 | self.method = setting['method'] 46 | if 'symbol' in setting.keys(): 47 | self.symbol = setting['symbol'] 48 | # 笔生成方法,new, old 49 | # 是否进行K线包含处理 50 | if 'include' in setting.keys(): 51 | self.include = setting['include'] 52 | # 中枢生成方法,stroke, line 53 | # 使用笔还是线段作为中枢的构成, true使用线段 54 | if 'build_pivot' in setting.keys(): 55 | self.build_pivot = setting['build_pivot'] 56 | if 'qjt' in setting.keys(): 57 | self.qjt = setting['qjt'] 58 | if 'gz' in setting.keys(): 59 | self.gz = setting['gz'] 60 | # 买卖的级别 61 | if 'jb' in setting.keys(): 62 | self.jb = setting['jb'] 63 | # 线段生成方法 64 | # if 'include_feature' in setting.keys(): 65 | # self.include_feature = setting['include_feature'] 66 | 67 | super().__init__(engine, strategy_name, vt_symbol, setting) 68 | self.engine = engine 69 | self.strategy_name = strategy_name 70 | self.vt_symbol = vt_symbol 71 | self.include_feature = False 72 | 73 | # map 74 | self.chan_freq_map = {} 75 | self.bg_freq_map = {} 76 | 77 | # 初始化缠论类和bg 78 | self.bg = BarGenerator(on_bar=self.on_bar, interval=Interval.MINUTE) 79 | i = 0 80 | prev = None 81 | 82 | for freq in FREQS: 83 | chan = Chan_Class(freq=freq, symbol=self.vt_symbol, sell=self.sell, buy=self.buy, include=self.include, 84 | include_feature=self.include_feature, build_pivot=self.build_pivot, qjt=self.qjt, 85 | gz=self.gz) 86 | self.chan_freq_map[freq] = chan 87 | if prev: 88 | prev.set_next(chan) 89 | chan.set_prev(prev) 90 | prev = chan 91 | # 限定共振作用级别 92 | if chan.prev == None or chan.freq != FREQS[-1]: 93 | chan.gz = False 94 | if i > 0: 95 | wlist = FREQS_WINDOW[FREQS[i - 1]] 96 | self.bg_freq_map[freq] = BarGenerator(on_bar=self.on_pass, on_window_bar=self.on_bar, window=wlist[0], 97 | interval=wlist[1], target=wlist[2]) 98 | i += 1 99 | 100 | def on_start(self): 101 | self.write_log("chan策略启动") 102 | 103 | self.put_event() 104 | 105 | def on_stop(self): 106 | self.write_log("chan策略停止") 107 | self.put_event() 108 | 109 | def on_tick(self, tick: TickData): 110 | self.bg.update_tick(tick) 111 | 112 | def on_bar(self, bar: BarData): 113 | # print(bar) 114 | freq = INTERVAL_FREQ[bar.interval.value] 115 | if bar.interval.value == Interval.MINUTE.value: 116 | for freq in self.bg_freq_map: 117 | self.bg_freq_map[freq].update_bar(bar) 118 | # self.put_render_event() 119 | self.chan_freq_map[freq].on_bar(bar) 120 | 121 | def buy(self, price: float, volume: float, freq: str = '', stop: bool = False, lock: bool = False): 122 | return self.send_order(Direction.LONG, Offset.OPEN, price, volume, freq, stop, lock) 123 | 124 | def sell(self, price: float, volume: float, freq: str = '', stop: bool = False, lock: bool = False): 125 | return self.send_order(Direction.SHORT, Offset.CLOSE, price, volume, freq, stop, lock) 126 | 127 | def send_order( 128 | self, 129 | direction: Direction, 130 | offset: Offset, 131 | price: float, 132 | volume: float, 133 | freq: str, 134 | stop: bool = False, 135 | lock: bool = False 136 | ): 137 | if self.trading and self.jb == freq: 138 | vt_orderids = self.engine.send_order( 139 | self, direction, offset, price, volume, stop, lock 140 | ) 141 | return vt_orderids 142 | return [] 143 | 144 | def on_order(self, order: OrderData): 145 | pass 146 | 147 | def on_trade(self, trade: TradeData): 148 | pass 149 | 150 | def on_stop_order(self, stop_order: StopOrder): 151 | pass 152 | 153 | def on_pass(self): 154 | pass 155 | -------------------------------------------------------------------------------- /trade/template.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from copy import copy 3 | from typing import Any, Callable 4 | 5 | from trade.constant import Interval, Direction, Offset 6 | from trade.object import BarData, TickData, OrderData, TradeData, StopOrder 7 | from trade.utility import virtual 8 | 9 | 10 | class Template(ABC): 11 | parameters = [] 12 | variables = [] 13 | 14 | def __init__( 15 | self, 16 | engine: Any, 17 | strategy_name: str, 18 | vt_symbol: str, 19 | setting: dict, 20 | ): 21 | self.engine = engine 22 | self.strategy_name = strategy_name 23 | self.vt_symbol = vt_symbol 24 | self.setting = setting 25 | self.trading = False 26 | self.pos = 0 27 | 28 | # Copy a new variables list here to avoid duplicate insert when multiple 29 | # strategy instances are created with the same strategy class. 30 | self.variables = copy(self.variables) 31 | self.variables.insert(0, "trading") 32 | self.variables.insert(1, "pos") 33 | 34 | self.update_setting(setting) 35 | 36 | def update_setting(self, setting: dict): 37 | for name in self.parameters: 38 | if name in setting: 39 | setattr(self, name, setting[name]) 40 | 41 | def get_parameters(self): 42 | strategy_parameters = {} 43 | for name in self.parameters: 44 | strategy_parameters[name] = getattr(self, name) 45 | return strategy_parameters 46 | 47 | def get_variables(self): 48 | strategy_variables = {} 49 | for name in self.variables: 50 | strategy_variables[name] = getattr(self, name) 51 | return strategy_variables 52 | 53 | def get_data(self): 54 | 55 | strategy_data = { 56 | "strategy_name": self.strategy_name, 57 | "vt_symbol": self.vt_symbol, 58 | "class_name": self.__class__.__name__, 59 | "parameters": self.get_parameters(), 60 | "variables": self.get_variables(), 61 | } 62 | return strategy_data 63 | 64 | @virtual 65 | def on_start(self): 66 | pass 67 | 68 | @virtual 69 | def on_stop(self): 70 | pass 71 | 72 | @virtual 73 | def on_tick(self, tick: TickData): 74 | pass 75 | 76 | @virtual 77 | def on_bar(self, bar: BarData): 78 | pass 79 | 80 | @virtual 81 | def on_trade(self, trade: TradeData): 82 | pass 83 | 84 | @virtual 85 | def on_order(self, order: OrderData): 86 | pass 87 | 88 | @virtual 89 | def on_stop_order(self, stop_order: StopOrder): 90 | pass 91 | 92 | def buy(self, price: float, volume: float, freq: str = '', stop: bool = False, lock: bool = False): 93 | return self.send_order(Direction.LONG, Offset.OPEN, price, volume, freq, stop, lock) 94 | 95 | def sell(self, price: float, volume: float, freq: str = '', stop: bool = False, lock: bool = False): 96 | return self.send_order(Direction.SHORT, Offset.CLOSE, price, volume, freq, stop, lock) 97 | 98 | def send_order( 99 | self, 100 | direction: Direction, 101 | offset: Offset, 102 | price: float, 103 | volume: float, 104 | freq: str, 105 | stop: bool = False, 106 | lock: bool = False 107 | ): 108 | if self.trading: 109 | vt_orderids = self.engine.send_order( 110 | self, direction, offset, price, volume, stop, lock 111 | ) 112 | return vt_orderids 113 | else: 114 | return [] 115 | 116 | def cancel_order(self, vt_orderid: str): 117 | if self.trading: 118 | self.engine.cancel_order(self, vt_orderid) 119 | 120 | def cancel_all(self): 121 | if self.trading: 122 | self.engine.cancel_all(self) 123 | 124 | def write_log(self, msg: str): 125 | self.engine.write_log(msg, self) 126 | 127 | def get_engine_type(self): 128 | return self.engine.get_engine_type() 129 | 130 | def put_event(self): 131 | self.engine.put_strategy_event(self) 132 | 133 | def put_render_event(self): 134 | self.engine.put_render_event(self) 135 | 136 | def send_msg(self, msg): 137 | self.engine.send_msg(msg, self) 138 | 139 | def sync_data(self): 140 | if self.trading: 141 | self.engine.sync_strategy_data(self) 142 | -------------------------------------------------------------------------------- /trade/ui/__init__.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import platform 3 | import sys 4 | import traceback 5 | import webbrowser 6 | import types 7 | 8 | import qdarkstyle 9 | from PyQt5 import QtGui, QtWidgets, QtCore 10 | 11 | from .mainwindow import MainWindow 12 | from ..utility import get_icon_path 13 | 14 | 15 | def excepthook(exctype: type, value: Exception, tb: types.TracebackType) -> None: 16 | """ 17 | Raise exception under debug mode, otherwise 18 | show exception detail with QMessageBox. 19 | """ 20 | sys.__excepthook__(exctype, value, tb) 21 | 22 | msg = "".join(traceback.format_exception(exctype, value, tb)) 23 | dialog = ExceptionDialog(msg) 24 | dialog.exec_() 25 | 26 | 27 | def create_qapp(app_name: str = "pychan") -> QtWidgets.QApplication: 28 | sys.excepthook = excepthook 29 | 30 | QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling) 31 | 32 | qapp = QtWidgets.QApplication([]) 33 | qapp.setStyleSheet(qdarkstyle.load_stylesheet_pyqt5()) 34 | 35 | font = QtGui.QFont('Arial', 12) 36 | qapp.setFont(font) 37 | 38 | icon = QtGui.QIcon(get_icon_path(__file__, "app.ico")) 39 | qapp.setWindowIcon(icon) 40 | 41 | if "Windows" in platform.uname(): 42 | ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( 43 | app_name 44 | ) 45 | 46 | return qapp 47 | 48 | 49 | class ExceptionDialog(QtWidgets.QDialog): 50 | def __init__(self, msg: str): 51 | super().__init__() 52 | self.msg: str = msg 53 | self.init_ui() 54 | 55 | def init_ui(self) -> None: 56 | self.setWindowTitle("触发异常") 57 | self.setFixedSize(600, 600) 58 | 59 | self.msg_edit = QtWidgets.QTextEdit() 60 | self.msg_edit.setText(self.msg) 61 | self.msg_edit.setReadOnly(True) 62 | 63 | copy_button = QtWidgets.QPushButton("复制") 64 | copy_button.clicked.connect(self._copy_text) 65 | 66 | close_button = QtWidgets.QPushButton("关闭") 67 | close_button.clicked.connect(self.close) 68 | 69 | hbox = QtWidgets.QHBoxLayout() 70 | hbox.addWidget(copy_button) 71 | hbox.addWidget(close_button) 72 | 73 | vbox = QtWidgets.QVBoxLayout() 74 | vbox.addWidget(self.msg_edit) 75 | vbox.addLayout(hbox) 76 | 77 | self.setLayout(vbox) 78 | 79 | def _copy_text(self) -> None: 80 | self.msg_edit.selectAll() 81 | self.msg_edit.copy() 82 | -------------------------------------------------------------------------------- /trade/ui/ico/GitHub.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dogfun/chanlun/b16708599e4cfefc4c9f8e4e198ddb867a8a4e00/trade/ui/ico/GitHub.ico -------------------------------------------------------------------------------- /trade/ui/ico/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dogfun/chanlun/b16708599e4cfefc4c9f8e4e198ddb867a8a4e00/trade/ui/ico/__init__.py -------------------------------------------------------------------------------- /trade/ui/ico/about.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dogfun/chanlun/b16708599e4cfefc4c9f8e4e198ddb867a8a4e00/trade/ui/ico/about.ico -------------------------------------------------------------------------------- /trade/ui/ico/app.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dogfun/chanlun/b16708599e4cfefc4c9f8e4e198ddb867a8a4e00/trade/ui/ico/app.ico -------------------------------------------------------------------------------- /trade/ui/ico/cw.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dogfun/chanlun/b16708599e4cfefc4c9f8e4e198ddb867a8a4e00/trade/ui/ico/cw.ico -------------------------------------------------------------------------------- /trade/ui/ico/database.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dogfun/chanlun/b16708599e4cfefc4c9f8e4e198ddb867a8a4e00/trade/ui/ico/database.ico -------------------------------------------------------------------------------- /trade/ui/ico/py.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dogfun/chanlun/b16708599e4cfefc4c9f8e4e198ddb867a8a4e00/trade/ui/ico/py.ico -------------------------------------------------------------------------------- /trade/ui/ico/restore.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dogfun/chanlun/b16708599e4cfefc4c9f8e4e198ddb867a8a4e00/trade/ui/ico/restore.ico -------------------------------------------------------------------------------- /trade/ui/ico/search.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dogfun/chanlun/b16708599e4cfefc4c9f8e4e198ddb867a8a4e00/trade/ui/ico/search.ico -------------------------------------------------------------------------------- /trade/ui/mainwindow.py: -------------------------------------------------------------------------------- 1 | import webbrowser 2 | from functools import partial 3 | from importlib import import_module 4 | from typing import Callable, Dict, Tuple 5 | 6 | from PyQt5 import QtCore, QtGui, QtWidgets 7 | from .widget import ( 8 | ChanTuWidget, 9 | AboutDialog 10 | ) 11 | from trade.chantu import ChanTuManager 12 | from ..constant import EVENT_CHANTU, Interval 13 | from ..engine import MainEngine, Event 14 | from ..utility import get_icon_path 15 | 16 | 17 | class MainWindow(QtWidgets.QMainWindow): 18 | 19 | def __init__(self, main_engine: MainEngine): 20 | super(MainWindow, self).__init__() 21 | self.main_engine: MainEngine = main_engine 22 | 23 | self.window_title: str = f"缠论图形工具项目" 24 | 25 | self.widgets: Dict[str, QtWidgets.QWidget] = {} 26 | 27 | self.init_ui() 28 | 29 | def init_ui(self) -> None: 30 | self.setWindowTitle(self.window_title) 31 | self.init_dock() 32 | self.init_toolbar() 33 | self.init_menu() 34 | self.load_window_setting("custom") 35 | event = Event(type=EVENT_CHANTU, data={ 36 | 'strategy_name': 'chantu', 37 | 'vt_symbol': '600519', 38 | 'setting': { 39 | 'start_time': "2021-11-01", 40 | 'include': True, 41 | 'interval': Interval.MINUTE, 42 | 'include_feature': False, 43 | 'qjt': True, 44 | 'gz': True, 45 | 'build_pivot': False, 46 | 'time_interval': 0 47 | }, 48 | }) 49 | # self.main_engine.put(event) 50 | 51 | 52 | def init_dock(self) -> None: 53 | market_widget, market_dock = self.create_dock( 54 | ChanTuManager, "行情", QtCore.Qt.LeftDockWidgetArea 55 | ) 56 | market_dock.raise_() 57 | 58 | self.save_window_setting("default") 59 | 60 | def init_menu(self) -> None: 61 | bar = self.menuBar() 62 | 63 | # System menu 64 | sys_menu = bar.addMenu("功能") 65 | self.add_menu_action( 66 | sys_menu, 67 | "股票缠图", 68 | "cw.ico", 69 | partial(self.open_widget, ChanTuWidget, "股票缠图"), 70 | ) 71 | sys_menu.addSeparator() 72 | 73 | self.add_menu_action(sys_menu, "退出", "exit.ico", self.close) 74 | 75 | # Help menu 76 | help_menu = bar.addMenu("帮助") 77 | 78 | self.add_menu_action( 79 | help_menu, "还原窗口", "restore.ico", self.restore_window_setting 80 | ) 81 | 82 | self.add_menu_action( 83 | help_menu, "系统源码", "GitHub.ico", self.open_github 84 | ) 85 | 86 | self.add_menu_action( 87 | help_menu, 88 | "关于", 89 | "about.ico", 90 | partial(self.open_widget, AboutDialog, "about"), 91 | ) 92 | 93 | def init_toolbar(self) -> None: 94 | self.toolbar = QtWidgets.QToolBar(self) 95 | self.toolbar.setObjectName("工具栏") 96 | self.toolbar.setFloatable(False) 97 | self.toolbar.setMovable(False) 98 | 99 | # Set button size 100 | w = 40 101 | size = QtCore.QSize(w, w) 102 | self.toolbar.setIconSize(size) 103 | 104 | # Set button spacing 105 | self.toolbar.layout().setSpacing(10) 106 | 107 | self.addToolBar(QtCore.Qt.LeftToolBarArea, self.toolbar) 108 | 109 | def add_menu_action( 110 | self, 111 | menu: QtWidgets.QMenu, 112 | action_name: str, 113 | icon_name: str, 114 | func: Callable, 115 | ) -> None: 116 | icon = QtGui.QIcon(get_icon_path(__file__, icon_name)) 117 | 118 | action = QtWidgets.QAction(action_name, self) 119 | action.triggered.connect(func) 120 | action.setIcon(icon) 121 | 122 | menu.addAction(action) 123 | 124 | def add_toolbar_action( 125 | self, 126 | action_name: str, 127 | icon_name: str, 128 | func: Callable, 129 | ) -> None: 130 | icon = QtGui.QIcon(get_icon_path(__file__, icon_name)) 131 | 132 | action = QtWidgets.QAction(action_name, self) 133 | action.triggered.connect(func) 134 | action.setIcon(icon) 135 | 136 | self.toolbar.addAction(action) 137 | 138 | def create_dock( 139 | self, 140 | widget_class: QtWidgets.QWidget, 141 | name: str, 142 | area: int 143 | ) -> Tuple[QtWidgets.QWidget, QtWidgets.QDockWidget]: 144 | widget = widget_class(self.main_engine) 145 | 146 | dock = QtWidgets.QDockWidget(name) 147 | dock.setWidget(widget) 148 | dock.setObjectName(name) 149 | dock.setFeatures(dock.DockWidgetFloatable | dock.DockWidgetMovable) 150 | self.addDockWidget(area, dock) 151 | return widget, dock 152 | 153 | def closeEvent(self, event: QtGui.QCloseEvent) -> None: 154 | reply = QtWidgets.QMessageBox.question( 155 | self, 156 | "退出", 157 | "确认退出?", 158 | QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, 159 | QtWidgets.QMessageBox.No, 160 | ) 161 | 162 | if reply == QtWidgets.QMessageBox.Yes: 163 | for widget in self.widgets.values(): 164 | widget.close() 165 | self.save_window_setting("custom") 166 | 167 | self.main_engine.close() 168 | 169 | event.accept() 170 | else: 171 | event.ignore() 172 | 173 | def open_widget(self, widget_class: QtWidgets.QWidget, name: str) -> None: 174 | widget = self.widgets.get(name, None) 175 | if not widget: 176 | widget = widget_class(self.main_engine) 177 | self.widgets[name] = widget 178 | 179 | if isinstance(widget, QtWidgets.QDialog): 180 | widget.exec_() 181 | else: 182 | widget.show() 183 | 184 | def save_window_setting(self, name: str): 185 | settings = QtCore.QSettings(self.window_title, name) 186 | settings.setValue("state", self.saveState()) 187 | settings.setValue("geometry", self.saveGeometry()) 188 | 189 | def load_window_setting(self, name: str) -> None: 190 | settings = QtCore.QSettings(self.window_title, name) 191 | state = settings.value("state") 192 | geometry = settings.value("geometry") 193 | 194 | if isinstance(state, QtCore.QByteArray): 195 | self.restoreState(state) 196 | self.restoreGeometry(geometry) 197 | 198 | def restore_window_setting(self) -> None: 199 | self.load_window_setting("default") 200 | self.showMaximized() 201 | 202 | def open_github(self) -> None: 203 | webbrowser.open("https://github.com/dogfun/chan") 204 | -------------------------------------------------------------------------------- /trade/ui/widget.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import platform 3 | from datetime import datetime, timedelta 4 | from typing import Any, Dict 5 | from copy import copy 6 | from tzlocal import get_localzone 7 | from PyQt5 import QtCore, QtGui, QtWidgets, Qt 8 | import numpy as np 9 | from trade.constant import ( 10 | EVENT_CHANTU, 11 | EVENT_TRADE, 12 | EVENT_ORDER, 13 | EVENT_POSITION, 14 | EVENT_ACCOUNT, 15 | EVENT_LOG, Direction, Interval 16 | ) 17 | from ..engine import MainEngine, Event 18 | 19 | COLOR_LONG = QtGui.QColor("red") 20 | COLOR_SHORT = QtGui.QColor("green") 21 | COLOR_BID = QtGui.QColor(255, 174, 201) 22 | COLOR_ASK = QtGui.QColor(160, 255, 160) 23 | COLOR_BLACK = QtGui.QColor("black") 24 | 25 | 26 | class BaseCell(QtWidgets.QTableWidgetItem): 27 | 28 | def __init__(self, content: Any, data: Any): 29 | super(BaseCell, self).__init__() 30 | self.setTextAlignment(QtCore.Qt.AlignCenter) 31 | self.set_content(content, data) 32 | 33 | def set_content(self, content: Any, data: Any) -> None: 34 | self.setText(str(content)) 35 | self._data = data 36 | 37 | def get_data(self) -> Any: 38 | return self._data 39 | 40 | 41 | class EnumCell(BaseCell): 42 | """ 43 | Cell used for showing enum data. 44 | """ 45 | 46 | def __init__(self, content: str, data: Any): 47 | super(EnumCell, self).__init__(content, data) 48 | 49 | def set_content(self, content: Any, data: Any) -> None: 50 | """ 51 | Set text using enum.constant.value. 52 | """ 53 | if content: 54 | super(EnumCell, self).set_content(content.value, data) 55 | 56 | 57 | class DirectionCell(EnumCell): 58 | """ 59 | Cell used for showing direction data. 60 | """ 61 | 62 | def __init__(self, content: str, data: Any): 63 | """""" 64 | super(DirectionCell, self).__init__(content, data) 65 | 66 | def set_content(self, content: Any, data: Any) -> None: 67 | """ 68 | Cell color is set according to direction. 69 | """ 70 | super(DirectionCell, self).set_content(content, data) 71 | if content is Direction.SHORT: 72 | self.setForeground(COLOR_SHORT) 73 | else: 74 | self.setForeground(COLOR_LONG) 75 | 76 | 77 | class BidCell(BaseCell): 78 | """ 79 | Cell used for showing bid price and volume. 80 | """ 81 | 82 | def __init__(self, content: Any, data: Any): 83 | super(BidCell, self).__init__(content, data) 84 | 85 | self.setForeground(COLOR_BID) 86 | 87 | 88 | class AskCell(BaseCell): 89 | """ 90 | Cell used for showing ask price and volume. 91 | """ 92 | 93 | def __init__(self, content: Any, data: Any): 94 | super(AskCell, self).__init__(content, data) 95 | 96 | self.setForeground(COLOR_ASK) 97 | 98 | 99 | class PnlCell(BaseCell): 100 | """ 101 | Cell used for showing pnl data. 102 | """ 103 | 104 | def __init__(self, content: Any, data: Any): 105 | """""" 106 | super(PnlCell, self).__init__(content, data) 107 | 108 | def set_content(self, content: Any, data: Any) -> None: 109 | """ 110 | Cell color is set based on whether pnl is 111 | positive or negative. 112 | """ 113 | super(PnlCell, self).set_content(content, data) 114 | 115 | if str(content).startswith("-"): 116 | self.setForeground(COLOR_SHORT) 117 | else: 118 | self.setForeground(COLOR_LONG) 119 | 120 | 121 | class TimeCell(BaseCell): 122 | """ 123 | Cell used for showing time string from datetime object. 124 | """ 125 | 126 | local_tz = get_localzone() 127 | 128 | def __init__(self, content: Any, data: Any): 129 | super(TimeCell, self).__init__(content, data) 130 | 131 | def set_content(self, content: Any, data: Any) -> None: 132 | if content is None: 133 | return 134 | 135 | content = content.astimezone(self.local_tz) 136 | timestamp = content.strftime("%H:%M:%S") 137 | 138 | millisecond = int(content.microsecond / 1000) 139 | if millisecond: 140 | timestamp = f"{timestamp}.{millisecond}" 141 | 142 | self.setText(timestamp) 143 | self._data = data 144 | 145 | 146 | class MsgCell(BaseCell): 147 | """ 148 | Cell used for showing msg data. 149 | """ 150 | 151 | def __init__(self, content: str, data: Any): 152 | super(MsgCell, self).__init__(content, data) 153 | self.setTextAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) 154 | 155 | 156 | class BaseMonitor(QtWidgets.QTableWidget): 157 | event_type: str = "" 158 | data_key: str = "" 159 | sorting: bool = False 160 | headers: Dict[str, dict] = {} 161 | 162 | signal: QtCore.pyqtSignal = QtCore.pyqtSignal(Event) 163 | 164 | def __init__(self, main_engine: MainEngine): 165 | super(BaseMonitor, self).__init__() 166 | 167 | self.main_engine: MainEngine = main_engine 168 | self.cells: Dict[str, dict] = {} 169 | 170 | self.init_ui() 171 | self.register_event() 172 | 173 | def init_ui(self) -> None: 174 | self.init_table() 175 | self.init_menu() 176 | 177 | def init_table(self) -> None: 178 | self.setColumnCount(len(self.headers)) 179 | 180 | labels = [d["display"] for d in self.headers.values()] 181 | self.setHorizontalHeaderLabels(labels) 182 | 183 | self.verticalHeader().setVisible(False) 184 | self.setEditTriggers(self.NoEditTriggers) 185 | self.setAlternatingRowColors(True) 186 | self.setSortingEnabled(self.sorting) 187 | 188 | def init_menu(self) -> None: 189 | """ 190 | Create right click menu. 191 | """ 192 | self.menu = QtWidgets.QMenu(self) 193 | 194 | resize_action = QtWidgets.QAction("调整列宽", self) 195 | resize_action.triggered.connect(self.resize_columns) 196 | self.menu.addAction(resize_action) 197 | 198 | save_action = QtWidgets.QAction("保存数据", self) 199 | save_action.triggered.connect(self.save_csv) 200 | self.menu.addAction(save_action) 201 | 202 | def register_event(self) -> None: 203 | """ 204 | Register event handler into event engine. 205 | """ 206 | if self.event_type: 207 | self.signal.connect(self.process_event) 208 | self.main_engine.register(self.event_type, self.signal.emit) 209 | 210 | def process_event(self, event: Event) -> None: 211 | """ 212 | Process new data from event and update into table. 213 | """ 214 | # Disable sorting to prevent unwanted error. 215 | if self.sorting: 216 | self.setSortingEnabled(False) 217 | 218 | # Update data into table. 219 | data = event.data 220 | 221 | if not self.data_key: 222 | self.insert_new_row(data) 223 | else: 224 | key = data.__getattribute__(self.data_key) 225 | 226 | if key in self.cells: 227 | self.update_old_row(data) 228 | else: 229 | self.insert_new_row(data) 230 | 231 | # Enable sorting 232 | if self.sorting: 233 | self.setSortingEnabled(True) 234 | 235 | def insert_new_row(self, data: Any): 236 | self.insertRow(0) 237 | 238 | row_cells = {} 239 | for column, header in enumerate(self.headers.keys()): 240 | setting = self.headers[header] 241 | 242 | content = data.__getattribute__(header) 243 | cell = setting["cell"](content, data) 244 | self.setItem(0, column, cell) 245 | 246 | if setting["update"]: 247 | row_cells[header] = cell 248 | 249 | if self.data_key: 250 | key = data.__getattribute__(self.data_key) 251 | self.cells[key] = row_cells 252 | 253 | def update_old_row(self, data: Any) -> None: 254 | """ 255 | Update an old row in table. 256 | """ 257 | key = data.__getattribute__(self.data_key) 258 | row_cells = self.cells[key] 259 | 260 | for header, cell in row_cells.items(): 261 | content = data.__getattribute__(header) 262 | cell.set_content(content, data) 263 | 264 | def resize_columns(self) -> None: 265 | """ 266 | Resize all columns according to contents. 267 | """ 268 | self.horizontalHeader().resizeSections(QtWidgets.QHeaderView.ResizeToContents) 269 | 270 | def save_csv(self) -> None: 271 | """ 272 | Save table data into a csv file 273 | """ 274 | path, _ = QtWidgets.QFileDialog.getSaveFileName( 275 | self, "保存数据", "", "CSV(*.csv)") 276 | 277 | if not path: 278 | return 279 | 280 | with open(path, "w") as f: 281 | writer = csv.writer(f, lineterminator="\n") 282 | 283 | writer.writerow(self.headers.keys()) 284 | 285 | for row in range(self.rowCount()): 286 | row_data = [] 287 | for column in range(self.columnCount()): 288 | item = self.item(row, column) 289 | if item: 290 | row_data.append(str(item.text())) 291 | else: 292 | row_data.append("") 293 | writer.writerow(row_data) 294 | 295 | def contextMenuEvent(self, event: QtGui.QContextMenuEvent) -> None: 296 | """ 297 | Show menu with right click. 298 | """ 299 | self.menu.popup(QtGui.QCursor.pos()) 300 | 301 | 302 | class LogMonitor(BaseMonitor): 303 | event_type = EVENT_LOG 304 | data_key = "" 305 | sorting = False 306 | 307 | headers = { 308 | "time": {"display": "时间", "cell": TimeCell, "update": False}, 309 | "msg": {"display": "信息", "cell": MsgCell, "update": False} 310 | } 311 | 312 | 313 | class TradeMonitor(BaseMonitor): 314 | event_type = EVENT_TRADE 315 | data_key = "" 316 | sorting = True 317 | 318 | headers: Dict[str, dict] = { 319 | "tradeid": {"display": "成交号 ", "cell": BaseCell, "update": False}, 320 | "orderid": {"display": "委托号", "cell": BaseCell, "update": False}, 321 | "symbol": {"display": "代码", "cell": BaseCell, "update": False}, 322 | "exchange": {"display": "交易所", "cell": EnumCell, "update": False}, 323 | "direction": {"display": "方向", "cell": DirectionCell, "update": False}, 324 | "offset": {"display": "开平", "cell": EnumCell, "update": False}, 325 | "price": {"display": "价格", "cell": BaseCell, "update": False}, 326 | "volume": {"display": "数量", "cell": BaseCell, "update": False}, 327 | "datetime": {"display": "时间", "cell": TimeCell, "update": False} 328 | } 329 | 330 | 331 | class OrderMonitor(BaseMonitor): 332 | """ 333 | Monitor for order data. 334 | """ 335 | 336 | event_type = EVENT_ORDER 337 | data_key = "vt_orderid" 338 | sorting = True 339 | 340 | headers: Dict[str, dict] = { 341 | "order_id": {"display": "委托ID", "cell": BaseCell, "update": False}, 342 | "symbol": {"display": "股票代码", "cell": BaseCell, "update": False}, 343 | # "exchange": {"display": "交易所", "cell": EnumCell, "update": False}, 344 | "order_type": {"display": "委托类型", "cell": EnumCell, "update": False}, 345 | "side": {"display": "方向", "cell": DirectionCell, "update": False}, 346 | # "offset": {"display": "开平", "cell": EnumCell, "update": False}, 347 | "price": {"display": "委托价格", "cell": BaseCell, "update": False}, 348 | "volume": {"display": "委托量", "cell": BaseCell, "update": True}, 349 | "filled_vwap": {"display": "成交均价", "cell": BaseCell, "update": True}, 350 | "status": {"display": "委托状态", "cell": EnumCell, "update": True}, 351 | "created_at": {"display": "委托时间", "cell": TimeCell, "update": True}, 352 | "ord_rej_reason_detail": {"display": "委托拒绝原因", "cell": BaseCell, "update": True} 353 | } 354 | 355 | def init_ui(self): 356 | """ 357 | Connect signal. 358 | """ 359 | super(OrderMonitor, self).init_ui() 360 | 361 | self.setToolTip("双击单元格撤单") 362 | self.itemDoubleClicked.connect(self.cancel_order) 363 | 364 | def cancel_order(self, cell: BaseCell) -> None: 365 | """ 366 | Cancel order if cell double clicked. 367 | """ 368 | order = cell.get_data() 369 | req = order.create_cancel_request() 370 | self.main_engine.cancel_order(req, order.gateway_name) 371 | 372 | 373 | class PositionMonitor(BaseMonitor): 374 | """ 375 | Monitor for position data. 376 | """ 377 | 378 | event_type = EVENT_POSITION 379 | data_key = "vt_positionid" 380 | sorting = True 381 | 382 | headers = { 383 | "symbol": {"display": "股票代码", "cell": BaseCell, "update": False}, 384 | "side": {"display": "方向", "cell": DirectionCell, "update": False}, 385 | "volume": {"display": "总持仓量", "cell": BaseCell, "update": True}, 386 | "volume_today": {"display": "今日持仓量", "cell": BaseCell, "update": True}, 387 | "available": {"display": "可平仓位", "cell": BaseCell, "update": False}, 388 | "order_frozen": {"display": "冻结仓位", "cell": BaseCell, "update": False}, 389 | "cost": {"display": "持仓成本", "cell": BaseCell, "update": True}, 390 | "vwap": {"display": "持仓均价", "cell": BaseCell, "update": True}, 391 | "fpnl": {"display": "盈亏", "cell": PnlCell, "update": True} 392 | } 393 | 394 | 395 | class AccountMonitor(BaseMonitor): 396 | event_type = EVENT_ACCOUNT 397 | data_key = "vt_accountid" 398 | sorting = True 399 | 400 | headers = { 401 | "account_id": {"display": "账号", "cell": BaseCell, "update": False}, 402 | "nav": {"display": "净值", "cell": BaseCell, "update": True}, 403 | "pnl": {"display": "净收益", "cell": BaseCell, "update": True}, 404 | "available": {"display": "可用资金", "cell": BaseCell, "update": True}, 405 | "cum_trade": {"display": "累计交易额", "cell": BaseCell, "update": True}, 406 | "cum_commission": {"display": "累计手续费", "cell": BaseCell, "update": True}, 407 | "order_frozen": {"display": "冻结资金", "cell": BaseCell, "update": True} 408 | } 409 | 410 | 411 | class ChanTuWidget(QtWidgets.QDialog): 412 | 413 | def __init__(self, main_engine: MainEngine): 414 | super().__init__() 415 | 416 | self.main_engine: MainEngine = main_engine 417 | 418 | self.vt_symbol: str = "" 419 | self.price_digits: int = 0 420 | 421 | self.init_ui() 422 | 423 | def init_ui(self) -> None: 424 | self.setWindowTitle("股票缠图") 425 | self.setFixedWidth(400) 426 | self.jquser_line = QtWidgets.QLineEdit() 427 | self.jqpass_line = QtWidgets.QLineEdit() 428 | self.symbol_line = QtWidgets.QLineEdit() 429 | self.symbol_line.setText("600809") 430 | self.k_line_include_combo = QtWidgets.QComboBox() 431 | self.k_line_include_combo.addItems(['缠论K线', '普通K线']) 432 | self.k_line_include_combo.setCurrentIndex(0) 433 | self.xd_zs_combo = QtWidgets.QComboBox() 434 | self.xd_zs_combo.addItems(['笔中枢', '线段中枢']) 435 | self.xd_zs_combo.setCurrentIndex(0) 436 | # self.feature_include_combo = QtWidgets.QComboBox() 437 | # self.feature_include_combo.addItems(['True', 'False']) 438 | # self.feature_include_combo.setCurrentIndex(1) 439 | self.qjt_combo = QtWidgets.QComboBox() 440 | self.qjt_combo.addItems(['是', '否']) 441 | self.qjt_combo.setCurrentIndex(0) 442 | self.gz_combo = QtWidgets.QComboBox() 443 | self.gz_combo.addItems(['是', '否']) 444 | self.gz_combo.setCurrentIndex(0) 445 | self.time_interval_line = QtWidgets.QLineEdit() 446 | self.time_interval_line.setText("10") 447 | self.start_time_line = QtWidgets.QLineEdit() 448 | self.start_time_line.setText("2022-01-01") 449 | 450 | chan_button = QtWidgets.QPushButton("确定") 451 | chan_button.clicked.connect(self.show_chan) 452 | 453 | grid = QtWidgets.QGridLayout() 454 | grid.addWidget(QtWidgets.QLabel("聚宽账号"), 0, 0) 455 | grid.addWidget(QtWidgets.QLabel("聚宽密码"), 1, 0) 456 | grid.addWidget(QtWidgets.QLabel("股票代码"), 2, 0) 457 | grid.addWidget(QtWidgets.QLabel("开始日期"), 3, 0) 458 | grid.addWidget(QtWidgets.QLabel("K线类型"), 4, 0) 459 | grid.addWidget(QtWidgets.QLabel("中枢类型"), 5, 0) 460 | # grid.addWidget(QtWidgets.QLabel("特序包含"), 3, 0) 461 | grid.addWidget(QtWidgets.QLabel("用区间套"), 6, 0) 462 | grid.addWidget(QtWidgets.QLabel("使用共振"), 7, 0) 463 | grid.addWidget(QtWidgets.QLabel("展现间隔"), 8, 0) 464 | grid.addWidget(self.jquser_line, 0, 1, 1, 2) 465 | grid.addWidget(self.jqpass_line, 1, 1, 1, 2) 466 | grid.addWidget(self.symbol_line, 2, 1, 1, 2) 467 | grid.addWidget(self.start_time_line, 3, 1, 1, 2) 468 | grid.addWidget(self.k_line_include_combo, 4, 1, 1, 2) 469 | grid.addWidget(self.xd_zs_combo, 5, 1, 1, 2) 470 | # grid.addWidget(self.feature_include_combo, 3, 1, 1, 2) 471 | grid.addWidget(self.qjt_combo, 6, 1, 1, 2) 472 | grid.addWidget(self.gz_combo, 7, 1, 1, 2) 473 | grid.addWidget(self.time_interval_line, 8, 1, 1, 2) 474 | grid.addWidget(chan_button, 9, 0, 1, 3) 475 | 476 | vbox = QtWidgets.QVBoxLayout() 477 | vbox.addLayout(grid) 478 | self.setLayout(vbox) 479 | 480 | def show_chan(self): 481 | jquser = self.jquser_line.text() 482 | jqpass = self.jqpass_line.text() 483 | vt_symbol = self.symbol_line.text() 484 | start_time = self.start_time_line.text() 485 | k_line_include = self.k_line_include_combo.currentText() 486 | if k_line_include == '缠论K线': 487 | k_line_include = True 488 | else: 489 | k_line_include = False 490 | xd_zs = self.xd_zs_combo.currentText() 491 | if xd_zs == '线段中枢': 492 | xd_zs = True 493 | else: 494 | xd_zs = False 495 | # feature_include = self.feature_include_combo.currentText() 496 | # if feature_include == 'True': 497 | # feature_include=True 498 | # else: 499 | # feature_include=False 500 | qjt = self.qjt_combo.currentText() 501 | if qjt == '是': 502 | qjt = True 503 | else: 504 | qjt = False 505 | gz = self.gz_combo.currentText() 506 | if gz == '是': 507 | gz = True 508 | else: 509 | gz = False 510 | time_interval = self.time_interval_line.text() 511 | self.bt_engine = self.main_engine 512 | event = Event(type=EVENT_CHANTU, data={ 513 | 'strategy_name': 'chantu', 514 | 'jquser': jquser, 515 | 'jqpass': jqpass, 516 | 'vt_symbol': vt_symbol, 517 | 'start_time': start_time, 518 | 'setting': { 519 | 'include': k_line_include, 520 | 'interval': Interval.MINUTE, 521 | # 'include_feature': feature_include, 522 | 'include_feature': False, 523 | 'qjt': qjt, 524 | 'gz': gz, 525 | 'build_pivot': xd_zs, 526 | 'time_interval': int(time_interval) 527 | }, 528 | 529 | }) 530 | self.main_engine.put(event) 531 | self.accept() 532 | 533 | def set_vt_symbol(self) -> None: 534 | """ 535 | Set the tick depth data to monitor by vt_symbol. 536 | """ 537 | symbol = str(self.symbol_line.text()) 538 | if not symbol: 539 | return 540 | 541 | # Generate vt_symbol from symbol and exchange 542 | exchange_value = str(self.exchange_combo.currentText()) 543 | vt_symbol = f"{symbol}.{exchange_value}" 544 | 545 | if vt_symbol == self.vt_symbol: 546 | return 547 | self.vt_symbol = vt_symbol 548 | 549 | # Update name line widget and clear all labels 550 | contract = self.main_engine.get_contract(vt_symbol) 551 | if not contract: 552 | self.name_line.setText("") 553 | gateway_name = self.gateway_combo.currentText() 554 | else: 555 | self.name_line.setText(contract.name) 556 | gateway_name = contract.gateway_name 557 | ix = self.gateway_combo.findText(gateway_name) 558 | self.gateway_combo.setCurrentIndex(ix) 559 | self.price_digits = 2 560 | 561 | self.clear_label_text() 562 | self.volume_line.setText("") 563 | self.price_line.setText("") 564 | 565 | def clear_label_text(self) -> None: 566 | self.lp_label.setText("") 567 | self.return_label.setText("") 568 | 569 | self.bv1_label.setText("") 570 | self.bv2_label.setText("") 571 | self.bv3_label.setText("") 572 | self.bv4_label.setText("") 573 | self.bv5_label.setText("") 574 | 575 | self.av1_label.setText("") 576 | self.av2_label.setText("") 577 | self.av3_label.setText("") 578 | self.av4_label.setText("") 579 | self.av5_label.setText("") 580 | 581 | self.bp1_label.setText("") 582 | self.bp2_label.setText("") 583 | self.bp3_label.setText("") 584 | self.bp4_label.setText("") 585 | self.bp5_label.setText("") 586 | 587 | self.ap1_label.setText("") 588 | self.ap2_label.setText("") 589 | self.ap3_label.setText("") 590 | self.ap4_label.setText("") 591 | self.ap5_label.setText("") 592 | 593 | 594 | class AboutDialog(QtWidgets.QDialog): 595 | def __init__(self, main_engine: MainEngine): 596 | super().__init__() 597 | self.main_engine: MainEngine = main_engine 598 | self.init_ui() 599 | 600 | def init_ui(self) -> None: 601 | self.setWindowTitle(f"关于<缠论图形工具项目>") 602 | 603 | text = f""" 604 | License:MIT 605 | URL:https://github.com/dogfun/chan 606 | 607 | Python - {platform.python_version()} 608 | PyQt5 - {Qt.PYQT_VERSION_STR} 609 | Numpy - {np.__version__} 610 | """ 611 | 612 | label = QtWidgets.QLabel() 613 | label.setText(text) 614 | label.setMinimumWidth(500) 615 | 616 | vbox = QtWidgets.QVBoxLayout() 617 | vbox.addWidget(label) 618 | self.setLayout(vbox) 619 | 620 | -------------------------------------------------------------------------------- /trade/utility.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import logging 4 | import sys 5 | from pathlib import Path 6 | from typing import Callable, Dict, Tuple, Union, Optional 7 | from decimal import Decimal 8 | from math import floor, ceil 9 | 10 | import numpy as np 11 | import talib 12 | 13 | from trade.object import BarData, TickData 14 | from trade.constant import Exchange, Interval, ZH_TRANS_MAP 15 | 16 | log_formatter = logging.Formatter('[%(asctime)s] %(message)s') 17 | 18 | 19 | def extract_vt_symbol(vt_symbol: str) -> Tuple[str, Exchange]: 20 | symbol, exchange_str = vt_symbol.split(".") 21 | return symbol, Exchange(exchange_str) 22 | 23 | 24 | def generate_vt_symbol(symbol: str, exchange: Exchange) -> str: 25 | """ 26 | return vt_symbol 27 | """ 28 | return f"{symbol}.{exchange.value}" 29 | 30 | 31 | def _get_trader_dir(temp_name: str) -> Tuple[Path, Path]: 32 | cwd = Path.cwd() 33 | temp_path = cwd.joinpath(temp_name) 34 | if not temp_path.exists(): 35 | temp_path.mkdir(parents=True) 36 | return cwd, temp_path 37 | 38 | 39 | TRADER_DIR, TEMP_DIR = _get_trader_dir("config") 40 | sys.path.append(str(TRADER_DIR)) 41 | 42 | 43 | def get_file_path(filename: str) -> Path: 44 | return TEMP_DIR.joinpath(filename) 45 | 46 | 47 | def get_folder_path(folder_name: str) -> Path: 48 | folder_path = TEMP_DIR.joinpath(folder_name) 49 | if not folder_path.exists(): 50 | folder_path.mkdir() 51 | return folder_path 52 | 53 | 54 | def get_icon_path(filepath: str, ico_name: str) -> str: 55 | ui_path = Path(filepath).parent 56 | icon_path = ui_path.joinpath("ico", ico_name) 57 | return str(icon_path) 58 | 59 | 60 | def load_json(filename: str) -> dict: 61 | filepath = get_file_path(filename) 62 | 63 | if filepath.exists(): 64 | with open(filepath, mode="r", encoding="UTF-8") as f: 65 | data = json.load(f) 66 | return data 67 | else: 68 | save_json(filename, {}) 69 | return {} 70 | 71 | 72 | def save_json(filename: str, data: dict) -> None: 73 | filepath = get_file_path(filename) 74 | with open(filepath, mode="w+", encoding="UTF-8") as f: 75 | json.dump( 76 | data, 77 | f, 78 | indent=4, 79 | ensure_ascii=False 80 | ) 81 | 82 | 83 | def round_to(value: float, target: float) -> float: 84 | """ 85 | Round price to price tick value. 86 | """ 87 | value = Decimal(str(value)) 88 | target = Decimal(str(target)) 89 | rounded = float(int(round(value / target)) * target) 90 | return rounded 91 | 92 | 93 | def floor_to(value: float, target: float) -> float: 94 | """ 95 | Similar to math.floor function, but to target float number. 96 | """ 97 | value = Decimal(str(value)) 98 | target = Decimal(str(target)) 99 | result = float(int(floor(value / target)) * target) 100 | return result 101 | 102 | 103 | def ceil_to(value: float, target: float) -> float: 104 | """ 105 | Similar to math.ceil function, but to target float number. 106 | """ 107 | value = Decimal(str(value)) 108 | target = Decimal(str(target)) 109 | result = float(int(ceil(value / target)) * target) 110 | return result 111 | 112 | 113 | def get_digits(value: float) -> int: 114 | value_str = str(value) 115 | 116 | if "e-" in value_str: 117 | _, buf = value_str.split("e-") 118 | return int(buf) 119 | elif "." in value_str: 120 | _, buf = value_str.split(".") 121 | return len(buf) 122 | else: 123 | return 0 124 | 125 | 126 | def check_run_time() -> bool: 127 | dtime = datetime.datetime 128 | now = dtime.now() 129 | now_str = str(now.date()) 130 | start_sw = dtime.strptime(now_str + " 9:30", '%Y-%m-%d %H:%M') 131 | end_sw = dtime.strptime(now_str + " 11:32", '%Y-%m-%d %H:%M') 132 | start_xw = dtime.strptime(now_str + " 13:00", '%Y-%m-%d %H:%M') 133 | end_xw = dtime.strptime(now_str + " 15:02", '%Y-%m-%d %H:%M') 134 | if (start_sw <= now <= end_sw) or (start_xw <= now <= end_xw): 135 | return True 136 | else: 137 | return False 138 | 139 | 140 | class BarGenerator: 141 | """ 142 | Target: 143 | 1. generating 1 minute bar data from tick data 144 | 2. generateing x minute bar/x hour bar data from 1 minute data 145 | 146 | Notice: 147 | 1. for x minute bar, x must be able to divide 60: 2, 3, 5, 6, 10, 15, 20, 30 148 | 2. for x hour bar, x can be any number 149 | """ 150 | 151 | def __init__( 152 | self, 153 | on_bar: Callable, 154 | window: int = 0, 155 | on_window_bar: Callable = None, 156 | interval: Interval = Interval.MINUTE, 157 | target: Interval = Interval.MINUTE 158 | ): 159 | self.bar: BarData = None 160 | self.on_bar: Callable = on_bar 161 | 162 | self.interval: Interval = interval 163 | self.interval_count: int = 0 164 | 165 | self.window: int = window 166 | self.window_bar: BarData = None 167 | self.on_window_bar: Callable = on_window_bar 168 | 169 | self.last_tick: TickData = None 170 | self.last_bar: BarData = None 171 | 172 | self.target = target 173 | 174 | def update_tick(self, tick: TickData) -> None: 175 | """ 176 | Update new tick data into generator. 177 | """ 178 | new_minute = False 179 | 180 | # Filter tick data with 0 last price 181 | if not tick.last_price: 182 | return 183 | 184 | # Filter tick data with less intraday trading volume (i.e. older timestamp) 185 | # if self.last_tick and tick.volume and tick.volume < self.last_tick.volume: 186 | # return 187 | # 过滤掉收到的过去的tick 188 | if self.last_tick and tick.datetime < self.last_tick.datetime: 189 | return 190 | 191 | if not self.bar: 192 | new_minute = True 193 | elif self.bar.datetime.minute != tick.datetime.minute: 194 | self.bar.datetime = self.bar.datetime.replace( 195 | second=0, microsecond=0 196 | ) 197 | self.on_bar(self.bar) 198 | 199 | new_minute = True 200 | 201 | if new_minute: 202 | self.bar = BarData( 203 | symbol=tick.symbol, 204 | exchange=tick.exchange, 205 | interval=Interval.MINUTE, 206 | datetime=tick.datetime, 207 | open_price=tick.last_price, 208 | high_price=tick.last_price, 209 | low_price=tick.last_price, 210 | close_price=tick.last_price, 211 | open_interest=tick.open_interest 212 | ) 213 | else: 214 | self.bar.high_price = max(self.bar.high_price, tick.last_price) 215 | self.bar.low_price = min(self.bar.low_price, tick.last_price) 216 | self.bar.close_price = tick.last_price 217 | self.bar.open_interest = tick.open_interest 218 | self.bar.datetime = tick.datetime 219 | 220 | if self.last_tick: 221 | volume_change = tick.volume - self.last_tick.volume 222 | self.bar.volume += max(volume_change, 0) 223 | 224 | self.last_tick = tick 225 | 226 | def update_bar(self, bar: BarData) -> None: 227 | """ 228 | Update 1 minute bar into generator 229 | """ 230 | # If not inited, creaate window bar object 231 | if not self.window_bar: 232 | # Generate timestamp for bar data 233 | if self.interval == Interval.MINUTE: 234 | dt = bar.datetime.replace(second=0, microsecond=0) 235 | else: 236 | dt = bar.datetime.replace(minute=0, second=0, microsecond=0) 237 | 238 | self.window_bar = BarData( 239 | symbol=bar.symbol, 240 | exchange=bar.exchange, 241 | datetime=dt, 242 | open_price=bar.open_price, 243 | high_price=bar.high_price, 244 | low_price=bar.low_price 245 | ) 246 | # Otherwise, update high/low price into window bar 247 | else: 248 | dt = bar.datetime.replace(second=0, microsecond=0) 249 | if not self.interval == Interval.MINUTE: 250 | dt = bar.datetime.replace(minute=0, second=0, microsecond=0) 251 | self.window_bar.datetime = dt 252 | self.window_bar.high_price = max( 253 | self.window_bar.high_price, bar.high_price) 254 | self.window_bar.low_price = min( 255 | self.window_bar.low_price, bar.low_price) 256 | 257 | # Update close price/volume into window bar 258 | self.window_bar.close_price = bar.close_price 259 | self.window_bar.volume += int(bar.volume) 260 | self.window_bar.open_interest = bar.open_interest 261 | self.window_bar.interval = self.target 262 | # Check if window bar completed 263 | finished = False 264 | 265 | if self.interval == Interval.MINUTE: 266 | # x-minute bar 267 | self.interval_count += 1 268 | 269 | if not self.interval_count % self.window: 270 | finished = True 271 | self.interval_count = 0 272 | elif self.interval == Interval.HOUR: 273 | if self.last_bar and bar.datetime.hour != self.last_bar.datetime.hour: 274 | # 1-hour bar 275 | if self.window == 1: 276 | finished = True 277 | # x-hour bar 278 | else: 279 | self.interval_count += 1 280 | 281 | if not self.interval_count % self.window: 282 | finished = True 283 | self.interval_count = 0 284 | 285 | if finished: 286 | self.on_window_bar(self.window_bar) 287 | self.window_bar = None 288 | 289 | # Cache last bar object 290 | self.last_bar = bar 291 | 292 | def generate(self) -> Optional[BarData]: 293 | """ 294 | Generate the bar data and call callback immediately. 295 | """ 296 | bar = self.bar 297 | 298 | if self.bar: 299 | bar.datetime = bar.datetime.replace(second=0, microsecond=0) 300 | self.on_bar(bar) 301 | 302 | self.bar = None 303 | return bar 304 | 305 | 306 | class ArrayManager(object): 307 | """ 308 | For: 309 | 1. time series container of bar data 310 | 2. calculating technical indicator value 311 | """ 312 | 313 | def __init__(self, size: int = 100): 314 | """Constructor""" 315 | self.count: int = 0 316 | self.size: int = size 317 | self.inited: bool = False 318 | 319 | self.open_array: np.ndarray = np.zeros(size) 320 | self.high_array: np.ndarray = np.zeros(size) 321 | self.low_array: np.ndarray = np.zeros(size) 322 | self.close_array: np.ndarray = np.zeros(size) 323 | self.volume_array: np.ndarray = np.zeros(size) 324 | self.open_interest_array: np.ndarray = np.zeros(size) 325 | 326 | def update_bar(self, bar: BarData) -> None: 327 | """ 328 | Update new bar data into array manager. 329 | """ 330 | self.count += 1 331 | if not self.inited and self.count >= self.size: 332 | self.inited = True 333 | 334 | self.open_array[:-1] = self.open_array[1:] 335 | self.high_array[:-1] = self.high_array[1:] 336 | self.low_array[:-1] = self.low_array[1:] 337 | self.close_array[:-1] = self.close_array[1:] 338 | self.volume_array[:-1] = self.volume_array[1:] 339 | self.open_interest_array[:-1] = self.open_interest_array[1:] 340 | 341 | self.open_array[-1] = bar.open_price 342 | self.high_array[-1] = bar.high_price 343 | self.low_array[-1] = bar.low_price 344 | self.close_array[-1] = bar.close_price 345 | self.volume_array[-1] = bar.volume 346 | self.open_interest_array[-1] = bar.open_interest 347 | 348 | @property 349 | def open(self) -> np.ndarray: 350 | """ 351 | Get open price time series. 352 | """ 353 | return self.open_array 354 | 355 | @property 356 | def high(self) -> np.ndarray: 357 | """ 358 | Get high price time series. 359 | """ 360 | return self.high_array 361 | 362 | @property 363 | def low(self) -> np.ndarray: 364 | """ 365 | Get low price time series. 366 | """ 367 | return self.low_array 368 | 369 | @property 370 | def close(self) -> np.ndarray: 371 | """ 372 | Get close price time series. 373 | """ 374 | return self.close_array 375 | 376 | @property 377 | def volume(self) -> np.ndarray: 378 | """ 379 | Get trading volume time series. 380 | """ 381 | return self.volume_array 382 | 383 | @property 384 | def open_interest(self) -> np.ndarray: 385 | """ 386 | Get trading volume time series. 387 | """ 388 | return self.open_interest_array 389 | 390 | def sma(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 391 | """ 392 | Simple moving average. 393 | """ 394 | result = talib.SMA(self.close, n) 395 | if array: 396 | return result 397 | return result[-1] 398 | 399 | def ema(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 400 | """ 401 | Exponential moving average. 402 | """ 403 | result = talib.EMA(self.close, n) 404 | if array: 405 | return result 406 | return result[-1] 407 | 408 | def kama(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 409 | """ 410 | KAMA. 411 | """ 412 | result = talib.KAMA(self.close, n) 413 | if array: 414 | return result 415 | return result[-1] 416 | 417 | def wma(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 418 | """ 419 | WMA. 420 | """ 421 | result = talib.WMA(self.close, n) 422 | if array: 423 | return result 424 | return result[-1] 425 | 426 | def apo(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 427 | """ 428 | APO. 429 | """ 430 | result = talib.APO(self.close, n) 431 | if array: 432 | return result 433 | return result[-1] 434 | 435 | def cmo(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 436 | """ 437 | CMO. 438 | """ 439 | result = talib.CMO(self.close, n) 440 | if array: 441 | return result 442 | return result[-1] 443 | 444 | def mom(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 445 | """ 446 | MOM. 447 | """ 448 | result = talib.MOM(self.close, n) 449 | if array: 450 | return result 451 | return result[-1] 452 | 453 | def ppo(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 454 | """ 455 | PPO. 456 | """ 457 | result = talib.PPO(self.close, n) 458 | if array: 459 | return result 460 | return result[-1] 461 | 462 | def roc(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 463 | """ 464 | ROC. 465 | """ 466 | result = talib.ROC(self.close, n) 467 | if array: 468 | return result 469 | return result[-1] 470 | 471 | def rocr(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 472 | """ 473 | ROCR. 474 | """ 475 | result = talib.ROCR(self.close, n) 476 | if array: 477 | return result 478 | return result[-1] 479 | 480 | def rocp(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 481 | """ 482 | ROCP. 483 | """ 484 | result = talib.ROCP(self.close, n) 485 | if array: 486 | return result 487 | return result[-1] 488 | 489 | def rocr_100(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 490 | """ 491 | ROCR100. 492 | """ 493 | result = talib.ROCR100(self.close, n) 494 | if array: 495 | return result 496 | return result[-1] 497 | 498 | def trix(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 499 | """ 500 | TRIX. 501 | """ 502 | result = talib.TRIX(self.close, n) 503 | if array: 504 | return result 505 | return result[-1] 506 | 507 | def std(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 508 | """ 509 | Standard deviation. 510 | """ 511 | result = talib.STDDEV(self.close, n) 512 | if array: 513 | return result 514 | return result[-1] 515 | 516 | def obv(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 517 | """ 518 | OBV. 519 | """ 520 | result = talib.OBV(self.close, self.volume) 521 | if array: 522 | return result 523 | return result[-1] 524 | 525 | def cci(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 526 | """ 527 | Commodity Channel Index (CCI). 528 | """ 529 | result = talib.CCI(self.high, self.low, self.close, n) 530 | if array: 531 | return result 532 | return result[-1] 533 | 534 | def atr(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 535 | """ 536 | Average True Range (ATR). 537 | """ 538 | result = talib.ATR(self.high, self.low, self.close, n) 539 | if array: 540 | return result 541 | return result[-1] 542 | 543 | def natr(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 544 | """ 545 | NATR. 546 | """ 547 | result = talib.NATR(self.high, self.low, self.close, n) 548 | if array: 549 | return result 550 | return result[-1] 551 | 552 | def rsi(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 553 | """ 554 | Relative Strenght Index (RSI). 555 | """ 556 | result = talib.RSI(self.close, n) 557 | if array: 558 | return result 559 | return result[-1] 560 | 561 | def macd( 562 | self, 563 | fast_period: int, 564 | slow_period: int, 565 | signal_period: int, 566 | array: bool = False 567 | ) -> Union[ 568 | Tuple[np.ndarray, np.ndarray, np.ndarray], 569 | Tuple[float, float, float] 570 | ]: 571 | """ 572 | MACD. 573 | """ 574 | macd, signal, hist = talib.MACD( 575 | self.close, fast_period, slow_period, signal_period 576 | ) 577 | if array: 578 | return macd, signal, hist 579 | return macd[-1], signal[-1], hist[-1] 580 | 581 | def adx(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 582 | """ 583 | ADX. 584 | """ 585 | result = talib.ADX(self.high, self.low, self.close, n) 586 | if array: 587 | return result 588 | return result[-1] 589 | 590 | def adxr(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 591 | """ 592 | ADXR. 593 | """ 594 | result = talib.ADXR(self.high, self.low, self.close, n) 595 | if array: 596 | return result 597 | return result[-1] 598 | 599 | def dx(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 600 | """ 601 | DX. 602 | """ 603 | result = talib.DX(self.high, self.low, self.close, n) 604 | if array: 605 | return result 606 | return result[-1] 607 | 608 | def minus_di(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 609 | """ 610 | MINUS_DI. 611 | """ 612 | result = talib.MINUS_DI(self.high, self.low, self.close, n) 613 | if array: 614 | return result 615 | return result[-1] 616 | 617 | def plus_di(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 618 | """ 619 | PLUS_DI. 620 | """ 621 | result = talib.PLUS_DI(self.high, self.low, self.close, n) 622 | if array: 623 | return result 624 | return result[-1] 625 | 626 | def willr(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 627 | """ 628 | WILLR. 629 | """ 630 | result = talib.WILLR(self.high, self.low, self.close, n) 631 | if array: 632 | return result 633 | return result[-1] 634 | 635 | def ultosc(self, array: bool = False) -> Union[float, np.ndarray]: 636 | """ 637 | Ultimate Oscillator. 638 | """ 639 | result = talib.ULTOSC(self.high, self.low, self.close) 640 | if array: 641 | return result 642 | return result[-1] 643 | 644 | def trange(self, array: bool = False) -> Union[float, np.ndarray]: 645 | """ 646 | TRANGE. 647 | """ 648 | result = talib.TRANGE(self.high, self.low, self.close) 649 | if array: 650 | return result 651 | return result[-1] 652 | 653 | def boll( 654 | self, 655 | n: int, 656 | dev: float, 657 | array: bool = False 658 | ) -> Union[ 659 | Tuple[np.ndarray, np.ndarray], 660 | Tuple[float, float] 661 | ]: 662 | """ 663 | Bollinger Channel. 664 | """ 665 | mid = self.sma(n, array) 666 | std = self.std(n, array) 667 | 668 | up = mid + std * dev 669 | down = mid - std * dev 670 | 671 | return up, down 672 | 673 | def keltner( 674 | self, 675 | n: int, 676 | dev: float, 677 | array: bool = False 678 | ) -> Union[ 679 | Tuple[np.ndarray, np.ndarray], 680 | Tuple[float, float] 681 | ]: 682 | """ 683 | Keltner Channel. 684 | """ 685 | mid = self.sma(n, array) 686 | atr = self.atr(n, array) 687 | 688 | up = mid + atr * dev 689 | down = mid - atr * dev 690 | 691 | return up, down 692 | 693 | def donchian( 694 | self, n: int, array: bool = False 695 | ) -> Union[ 696 | Tuple[np.ndarray, np.ndarray], 697 | Tuple[float, float] 698 | ]: 699 | """ 700 | Donchian Channel. 701 | """ 702 | up = talib.MAX(self.high, n) 703 | down = talib.MIN(self.low, n) 704 | 705 | if array: 706 | return up, down 707 | return up[-1], down[-1] 708 | 709 | def aroon( 710 | self, 711 | n: int, 712 | array: bool = False 713 | ) -> Union[ 714 | Tuple[np.ndarray, np.ndarray], 715 | Tuple[float, float] 716 | ]: 717 | """ 718 | Aroon indicator. 719 | """ 720 | aroon_up, aroon_down = talib.AROON(self.high, self.low, n) 721 | 722 | if array: 723 | return aroon_up, aroon_down 724 | return aroon_up[-1], aroon_down[-1] 725 | 726 | def aroonosc(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 727 | """ 728 | Aroon Oscillator. 729 | """ 730 | result = talib.AROONOSC(self.high, self.low, n) 731 | 732 | if array: 733 | return result 734 | return result[-1] 735 | 736 | def minus_dm(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 737 | """ 738 | MINUS_DM. 739 | """ 740 | result = talib.MINUS_DM(self.high, self.low, n) 741 | 742 | if array: 743 | return result 744 | return result[-1] 745 | 746 | def plus_dm(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 747 | """ 748 | PLUS_DM. 749 | """ 750 | result = talib.PLUS_DM(self.high, self.low, n) 751 | 752 | if array: 753 | return result 754 | return result[-1] 755 | 756 | def mfi(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 757 | """ 758 | Money Flow Index. 759 | """ 760 | result = talib.MFI(self.high, self.low, self.close, self.volume, n) 761 | if array: 762 | return result 763 | return result[-1] 764 | 765 | def ad(self, array: bool = False) -> Union[float, np.ndarray]: 766 | """ 767 | AD. 768 | """ 769 | result = talib.AD(self.high, self.low, self.close, self.volume) 770 | if array: 771 | return result 772 | return result[-1] 773 | 774 | def adosc(self, n: int, array: bool = False) -> Union[float, np.ndarray]: 775 | """ 776 | ADOSC. 777 | """ 778 | result = talib.ADOSC(self.high, self.low, self.close, self.volume, n) 779 | if array: 780 | return result 781 | return result[-1] 782 | 783 | def bop(self, array: bool = False) -> Union[float, np.ndarray]: 784 | """ 785 | BOP. 786 | """ 787 | result = talib.BOP(self.open, self.high, self.low, self.close) 788 | 789 | if array: 790 | return result 791 | return result[-1] 792 | 793 | 794 | def virtual(func: Callable) -> Callable: 795 | """ 796 | mark a function as "virtual", which means that this function can be override. 797 | any base class should use this or @abstractmethod to decorate all functions 798 | that can be (re)implemented by subclasses. 799 | """ 800 | return func 801 | 802 | 803 | file_handlers: Dict[str, logging.FileHandler] = {} 804 | 805 | 806 | def _get_file_logger_handler(filename: str) -> logging.FileHandler: 807 | handler = file_handlers.get(filename, None) 808 | if handler is None: 809 | handler = logging.FileHandler(filename) 810 | file_handlers[filename] = handler # Am i need a lock? 811 | return handler 812 | 813 | 814 | def get_file_logger(filename: str) -> logging.Logger: 815 | """ 816 | return a logger that writes records into a file. 817 | """ 818 | logger = logging.getLogger(filename) 819 | handler = _get_file_logger_handler(filename) # get singleton handler. 820 | handler.setFormatter(log_formatter) 821 | logger.addHandler(handler) # each handler will be added only once. 822 | return logger 823 | 824 | 825 | def trans_setting(setting: dict): 826 | new_setting = {} 827 | for name, value in setting.items(): 828 | if value in ZH_TRANS_MAP.keys(): 829 | new_setting[name] = ZH_TRANS_MAP[value] 830 | else: 831 | new_setting[name] = value 832 | return new_setting 833 | --------------------------------------------------------------------------------