├── .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 |
--------------------------------------------------------------------------------