├── .dockeringore ├── .gitignore ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── docker-compose.yml ├── hfpy ├── __init__.py ├── atp.py ├── bar.py ├── config.py ├── data.py ├── order.py ├── report_stra.py ├── strategy.py ├── structs.py └── template.html ├── main.py ├── readme.md ├── requirements.txt ├── setup.py ├── strategies ├── SMACross.py └── SMACross.yml └── ta-lib-0.4.0-src.tar.gz /.dockeringore: -------------------------------------------------------------------------------- 1 | .git/ 2 | .vs*/ 3 | dist/ 4 | __py*/ 5 | log*/ 6 | hfpy.*/ 7 | *.tgz 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # PyCharm 2 | # http://www.jetbrains.com/pycharm/webhelp/project.html 3 | .idea 4 | .iml 5 | 6 | #ignore thumbnails created by windows 7 | Thumbs.db 8 | #Ignore files build by Visual Studio 9 | *.obj 10 | *.exe 11 | *.pdb 12 | *.user 13 | *.aps 14 | *.pch 15 | *.vspscc 16 | *_i.c 17 | *_p.c 18 | *.ncb 19 | *.suo 20 | *.tlb 21 | *.tlh 22 | *.bak 23 | *.cache 24 | *.ilk 25 | *.log 26 | [Bb]in 27 | [Dd]ebug*/ 28 | *.lib 29 | *.sbr 30 | obj/ 31 | [Rr]elease*/ 32 | _ReSharper*/ 33 | [Tt]est[Rr]esult* 34 | __pycache__* 35 | log*/ 36 | .vscode*/ 37 | .git/ 38 | render.html 39 | hfpy.egg-info/ 40 | dist/ 41 | config_real.yml 42 | config.yml 43 | Pik* 44 | hfpy/atp_pg.py 45 | zeromq-4.2.2.tar.gz 46 | indic*/ 47 | *.html 48 | *.tgz 49 | *.pyc 50 | 51 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6.12-slim 2 | 3 | # 换源到ali 4 | RUN echo "deb http://mirrors.aliyun.com/debian/ buster main non-free contrib" > /etc/apt/sources.list; \ 5 | echo "deb-src http://mirrors.aliyun.com/debian/ buster main non-free contrib" >> /etc/apt/sources.list; \ 6 | echo "deb http://mirrors.aliyun.com/debian-security buster/updates main" >> /etc/apt/sources.list; \ 7 | echo "deb-src http://mirrors.aliyun.com/debian-security buster/updates main" >> /etc/apt/sources.list; \ 8 | echo "deb http://mirrors.aliyun.com/debian/ buster-updates main non-free contrib" >> /etc/apt/sources.list; \ 9 | echo "deb-src http://mirrors.aliyun.com/debian/ buster-updates main non-free contrib" >> /etc/apt/sources.list; \ 10 | echo "deb http://mirrors.aliyun.com/debian/ buster-backports main non-free contrib" >> /etc/apt/sources.list; \ 11 | echo "deb-src http://mirrors.aliyun.com/debian/ buster-backports main non-free contrib" >> /etc/apt/sources.list; 12 | 13 | RUN set -ex; \ 14 | apt-get update; \ 15 | # 安装talib依赖 16 | apt-get install -y python3-dev libssl-dev libffi-dev build-essential libxml2-dev libxslt1-dev; 17 | 18 | WORKDIR / 19 | # ta-lib 20 | ADD ta-lib-0.4.0-src.tar.gz . 21 | RUN cd ta-lib/; \ 22 | ./configure --prefix=/usr; \ 23 | make && make install; \ 24 | cd .. && rm -f ta-lib-0.4.0-src.tar.gz && rm ta-lib -rf; \ 25 | apt-get clean; \ 26 | # numpy 要先安装 27 | pip install --upgrade pip; \ 28 | pip install --no-cache-dir numpy; \ 29 | # 支持将order写入pg 30 | pip install --no-cache-dir ta-lib pyyaml color_log psycopg2-binary redis sqlalchemy; 31 | 32 | WORKDIR /hfpy 33 | COPY hfpy ./hfpy/ 34 | COPY strategies/SMA* ./strategies/ 35 | COPY main.py . 36 | # 添加默认策略 37 | ENV strategy_names SMACross 38 | 39 | ENTRYPOINT [ "python", "main.py" ] 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "{}" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright 2016 海风 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.in 2 | include *.ini 3 | include readme.md 4 | include *.txt 5 | exclude *.yml 6 | recursive-include hfpy *.* 7 | global-exclude strategies 8 | global-exclude __pycache__ *.py[cod] -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | hf_py: 5 | image: haifengat/hfpy 6 | container_name: hf_py 7 | restart: always 8 | environment: 9 | - TZ=Asia/Shanghai 10 | - strategy_names="SMACross" 11 | # 当日分钟与实时分钟 12 | - redis_addr="172.19.129.98:16379" 13 | # 分钟数据,没配置zmq时使用 14 | - pg_min=postgresql://postgres:123456@172.19.129.98:25432/postgres 15 | # 策略信号入库使用 16 | - pg_order=postgresql://postgres:12345@172.19.129.98:20032/postgres 17 | volumes: 18 | # 个人策略文件夹 19 | - ./strategies:/hfpy/strategies 20 | 21 | # docker build -t haifengat/hfpy:`date '+%m%d'` . && docker push haifengat/hfpy:`date '+%m%d'` 22 | -------------------------------------------------------------------------------- /hfpy/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | __title__ = '' 5 | __author__ = 'HaiFeng' 6 | __mtime__ = '2016/9/23' 7 | """ 8 | -------------------------------------------------------------------------------- /hfpy/atp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2016/05/31 9:16 4 | # @Author : HaiFeng 5 | # @Email : 24918700@qq.com 6 | # @File : atp.py 7 | # @Software: PyCharm 8 | 9 | import os, json, yaml 10 | from sqlalchemy import log 11 | 12 | from yaml import loader # 可能前面的import模块对time有影响,故放在最后 13 | 14 | from .bar import Bar 15 | from .data import Data 16 | from .order import OrderItem 17 | from .structs import DirectType, OffsetType 18 | from .strategy import Strategy 19 | # from .report_stra import Report 20 | from .config import Config 21 | 22 | from sqlalchemy.engine import ResultProxy 23 | 24 | 25 | class ATP(object): 26 | """""" 27 | 28 | def __init__(self): 29 | self.trading_days: list = [] 30 | '''交易日序列''' 31 | self.real = False 32 | '''是否实时行情''' 33 | self.cfg = Config() 34 | self.stra_instances = [] 35 | 36 | def on_order(self, stra: Strategy, data: Data, order: OrderItem): 37 | """此处调用ctp接口即可实现实际下单""" 38 | 39 | # 可通过环境配置作为开关 40 | if self.cfg.pg_order is not None: 41 | # 为app提供 42 | if 'app' in os.environ: 43 | color = 'red' if order.Direction == DirectType.Buy else 'green' 44 | sign = f'{{"color": "{color}", "price": "{order.Price:.4f}"}}' 45 | sql = f"""INSERT INTO public.strategy_sign 46 | (tradingday, order_time, instrument, "period", strategy_id, strategy_group, sign, remark, insert_time) 47 | VALUES('{data.Bars[-1].Tradingday}', '{stra.D[-1]}', '{data.Instrument}', {data.Interval}, '{stra.ID}', '{type(stra).__name__}','{sign}', '', now())""" 48 | else: 49 | js = json.dumps({ 50 | 'Direction': str(order.Direction).split('.')[1], 51 | 'Offset': str(order.Offset).split('.')[1], 52 | 'Price': round(order.Price, 4), 53 | 'Volume': order.Volume 54 | }) 55 | sql = f"""INSERT INTO public.strategy_sign 56 | (tradingday, order_time, instrument, "period", strategy_id, strategy_group, sign, remark, insert_time) 57 | VALUES('{data.Bars[-1].Tradingday}', '{stra.D[-1]}', '{data.Instrument}', {data.Interval}, '{stra.ID}', '{type(stra).__name__}', '{js}', '', now())""" 58 | self.cfg.pg_order.execute(sql) 59 | if self.real and self.cfg.rds is not None: 60 | js = json.dumps({ 61 | 'Instrument': order.Instrument, 62 | 'Direction': str(order.Direction).split('.')[1], 63 | 'Offset': str(order.Offset).split('.')[1], 64 | 'Price': round(order.Price, 4), 65 | 'Volume': order.Volume, 66 | "ID": stra.ID * 1000 + len(stra.Orders) + 1 67 | }) 68 | self.cfg.rds.publish(f'order.{type(stra).__name__}', js) 69 | self.cfg.log.war(js) 70 | 71 | def load_strategy(self): 72 | """加载../strategy目录下的策略""" 73 | """通过文件名取到对应的继承Data的类并实例""" 74 | for stra_name in self.cfg.strategy_name: 75 | f = os.path.join('./strategies', f'{stra_name}.py') 76 | # 只处理对应的 py文件 77 | if not os.path.exists(f): 78 | self.cfg.log.info(f'{f} is not exists!') 79 | continue 80 | # 以目录结构作为 namespace 81 | module_name = f"{os.path.split(os.path.dirname(f))[1]}.{stra_name}" 82 | module = __import__(module_name) # import module 83 | 84 | c = getattr(getattr(module, stra_name), stra_name) # 双层调用才是class,单层是为module 85 | if not issubclass(c, Strategy): # 类c是Data的子类 86 | continue 87 | 88 | # 与策略文件同名的 yaml 作为配置文件处理 89 | cfg_file = os.path.join(f'./strategies/{stra_name}.yml') 90 | if os.path.exists(cfg_file): 91 | # 清除策略信号 92 | if self.cfg.pg_order is not None: 93 | self.cfg.pg_order.execute(f"DELETE FROM public.strategy_sign WHERE strategy_group='{stra_name}'") 94 | with open(cfg_file, encoding='utf-8') as stra_cfg_file: 95 | params = yaml.load(stra_cfg_file, loader.FullLoader) 96 | for param in [p for p in params if p is not None]: # 去除None的配置 97 | stra: Strategy = c(param) 98 | stra.ID = param['ID'] 99 | self.cfg.log.info(f"load strategy:{stra_name}, id:{param['ID']}") 100 | 101 | for data in stra.Datas: 102 | data.SingleOrderOneBar = self.cfg.single_order_one_bar 103 | # 由atp处理策略指令 104 | stra._data_order = self.on_order 105 | self.stra_instances.append(stra) 106 | else: 107 | self.cfg.log.error(f"缺少对应的 yaml 配置文件{cfg_file}") 108 | 109 | def read_bars_his(self, stra: Strategy) -> list: 110 | """PG""" 111 | bars = [] 112 | if self.cfg.pg_min is not None: 113 | res = self.cfg.pg_min.execute("select count(1) from pg_catalog.pg_tables where schemaname='future' and tablename = 'future_min'") 114 | if res.fetchone()[0] == 0: 115 | self.cfg.log.error('future.future_min 不存在') 116 | return bars 117 | res:ResultProxy = self.cfg.pg_min.execute(f"""SELECT to_char("DateTime", 'YYYY-MM-DD HH24:MI:SS') AS datetime, "Instrument", "Open", "High", "Low","Close","Volume", "OpenInterest", "TradingDay" 118 | FROM future.future_min 119 | WHERE "TradingDay" >= '{stra.BeginDate}' 120 | AND "Instrument" IN ('{"','".join([d.Instrument for d in stra.Datas])}') 121 | ORDER BY "DateTime" 122 | """) 123 | row = res.fetchone() 124 | while row is not None: 125 | bars.append( { 126 | 'DateTime':row[0], 127 | 'Instrument':row[1], 128 | 'Open':row[2], 129 | 'High':row[3], 130 | 'Low':row[4], 131 | 'Close':row[5], 132 | 'Volume':row[6], 133 | 'OpenInterest':row[7], 134 | 'Tradingday':row[8] 135 | }) 136 | row = res.fetchone() 137 | bars.sort(key=lambda bar: bar['DateTime']) # 按时间升序 138 | return bars 139 | 140 | def read_bars_cur(self, stra:Strategy): 141 | """取当日数据""" 142 | bars = [] 143 | # 取实时数据 144 | if self.cfg.rds is not None: 145 | for inst in [d.Instrument for d in stra.Datas]: 146 | json_mins = self.cfg.rds.lrange(inst, 0, -1) 147 | for min in json_mins: 148 | bar = json.loads(min) 149 | bar['Instrument'] = inst 150 | bar['DateTime'] = bar.pop('_id') 151 | bar['Tradingday'] = bar.pop('TradingDay') 152 | bars.append(bar) 153 | return bars 154 | 155 | def read_data_test(self): 156 | """取历史和实时K线数据,并执行策略回测""" 157 | for stra in self.stra_instances: 158 | self.cfg.log.info(f'策略 {type(stra).__name__}.{stra.ID} 加载历史数据...') 159 | listBar = [] 160 | bars = self.read_bars_his(stra) 161 | listBar = [Bar(b['DateTime'], b['Instrument'], b['High'], b['Low'], b['Open'], b['Close'], b['Volume'], b['OpenInterest'], b['Tradingday']) for b in bars] 162 | 163 | for bar in listBar: 164 | for data in stra.Datas: 165 | if data.Instrument == bar.Instrument: 166 | data.__new_min_bar__(bar) # 调Data的onbar 167 | # 生成策略的测试报告 168 | # if len(stra.Orders) > 0: 169 | # Report(stra) 170 | self.cfg.log.war("test history is end.") 171 | # 加载当日数据 172 | for stra in self.stra_instances: 173 | self.cfg.log.info(f'策略 {type(stra).__name__}.{stra.ID} 加载当日数据...') 174 | listBar = [] 175 | bars = self.read_bars_cur(stra) 176 | listBar = [Bar(b['DateTime'], b['Instrument'], b['High'], b['Low'], b['Open'], b['Close'], b['Volume'], b['OpenInterest'], b['Tradingday']) for b in bars] 177 | 178 | for bar in listBar: 179 | for data in stra.Datas: 180 | if data.Instrument == bar.Instrument: 181 | data.__new_min_bar__(bar) # 调Data的onbar 182 | self.cfg.log.war("today test is end.") 183 | 184 | def Run(self): 185 | """""" 186 | ########### 信号入库 ######################## 187 | if self.cfg.pg_order is not None: 188 | # 清除策略信号 189 | res = self.cfg.pg_order.execute("select count(1) from pg_catalog.pg_tables where schemaname='public' and tablename = 'strategy_sign'") 190 | if res.fetchone()[0] == 0: 191 | sqls = f""" 192 | CREATE TABLE public.strategy_sign ( 193 | id serial NOT NULL, -- 自增序列 194 | tradingday varchar(8) NOT NULL, -- 交易日 195 | order_time varchar(20) NOT NULL, -- 信号时间:yyyy-MM-dd HH:mm:ss 196 | strategy_group varchar(255) NULL, -- 策略组(名) 197 | strategy_id varchar(32) NOT NULL, -- 策略标识 198 | instrument varchar(32) NOT NULL, -- 合约 199 | "period" int4 NOT NULL, -- 周期(单位-分钟) 200 | sign varchar(512) NOT NULL, -- 信号内容:json 201 | remark varchar(512) NULL, -- 备注 202 | insert_time timestamp NULL DEFAULT now() -- 入库时间 203 | ); 204 | CREATE INDEX newtable_instrument_idx ON public.strategy_sign USING btree (instrument, period); 205 | CREATE INDEX newtable_strategy_id_idx ON public.strategy_sign USING btree (strategy_id); 206 | CREATE INDEX newtable_tradingday_idx ON public.strategy_sign USING btree (tradingday); 207 | COMMENT ON TABLE public.strategy_sign IS '策略信号'; 208 | 209 | -- Column comments 210 | 211 | COMMENT ON COLUMN public.strategy_sign.tradingday IS '交易日'; 212 | COMMENT ON COLUMN public.strategy_sign.order_time IS '信号时间:yyyy-MM-dd HH:mm:ss'; 213 | COMMENT ON COLUMN public.strategy_sign.instrument IS '合约'; 214 | COMMENT ON COLUMN public.strategy_sign."period" IS '周期(单位-分钟)'; 215 | COMMENT ON COLUMN public.strategy_sign.strategy_id IS '策略标识'; 216 | COMMENT ON COLUMN public.strategy_sign.sign IS '信号内容:json'; 217 | COMMENT ON COLUMN public.strategy_sign.remark IS '备注'; 218 | COMMENT ON COLUMN public.strategy_sign.insert_time IS '入库时间'; 219 | COMMENT ON COLUMN public.strategy_sign.id IS '自增序列'; 220 | COMMENT ON COLUMN public.strategy_sign.strategy_group IS '策略组(名)'; 221 | 222 | -- Permissions 223 | 224 | ALTER TABLE public.strategy_sign OWNER TO postgres; 225 | GRANT ALL ON TABLE public.strategy_sign TO postgres; 226 | """ 227 | for sql in sqls.split(';'): 228 | if sql.strip('\n') == "": 229 | continue 230 | self.cfg.pg_order.execute(sql.strip('\n')) 231 | self.cfg.log.info('加载策略...') 232 | self.load_strategy() 233 | self.cfg.log.info('历史数据回测...') 234 | self.read_data_test() 235 | # 订阅行情 236 | if self.cfg.rds is not None: 237 | self.real = True 238 | ps = self.cfg.rds.pubsub() 239 | for datas in [stra.Datas for stra in self.stra_instances]: 240 | for data in datas: 241 | ps.psubscribe(f'md.{data.Instrument}') 242 | for tick in ps.listen(): 243 | if tick['type'] == 'pmessage': 244 | dic = json.loads(tick['data']) 245 | bar = Bar(dic['_id'],tick['channel'][3:],dic['High'],dic['Low'],dic['Open'],dic['Close'],dic['Volume'],dic['OpenInterest'],dic['TradingDay']) 246 | # 分钟数据后,传给各个策略 247 | for datas in [stra.Datas for stra in self.stra_instances]: 248 | for data in datas: 249 | if data.Instrument == bar.Instrument: 250 | data.on_min(bar) 251 | 252 | -------------------------------------------------------------------------------- /hfpy/bar.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | __title__ = '' 5 | __author__ = 'HaiFeng' 6 | __mtime__ = '2016/8/16' 7 | """ 8 | 9 | 10 | class Bar(object): 11 | """K线数据""" 12 | 13 | def __init__(self, datetime: str, ins: str, h: float, l: float, o: float, c: float, v: int, i: float, tradingday: str): 14 | """初始化""" 15 | """时间 16 | yyyyMMdd HH:mm:ss""" 17 | self.D = datetime 18 | """时间 19 | yyyyMMdd HH:mm:ss""" 20 | """合约""" 21 | self.Instrument = ins 22 | """合约""" 23 | """交易日""" 24 | self.Tradingday = tradingday 25 | """交易日""" 26 | """最高价""" 27 | self.H = h 28 | """最高价""" 29 | """最低价""" 30 | self.L = l 31 | """最低价""" 32 | """开仓价""" 33 | self.O = o 34 | """开仓价""" 35 | """收盘价""" 36 | self.C = c 37 | """收盘价""" 38 | """成交量""" 39 | self.V = v 40 | """成交量""" 41 | """持仓价""" 42 | self.I = i 43 | """持仓价""" 44 | self._pre_volume = 0 45 | 46 | def __str__(self): 47 | """""" 48 | return '{self.D}, {self.H}, {self.L}, {self.O}, {self.C}, {self.V}, {self.I}'.format( 49 | self=self) 50 | -------------------------------------------------------------------------------- /hfpy/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | __title__ = '项目配置信息' 4 | __author__ = 'HaiFeng' 5 | __mtime__ = '20180820' 6 | 7 | from sqlalchemy import create_engine 8 | from sqlalchemy.engine import Engine 9 | from color_log.logger import Logger 10 | import redis, os 11 | 12 | 13 | class Config(object): 14 | """""" 15 | 16 | def __init__(self): 17 | self.log = Logger() 18 | 19 | self.strategy_name = [] 20 | '''策略配置json格式:stra_name:[stra_id]''' 21 | if 'strategy_names' in os.environ: 22 | self.strategy_name = [n.strip().strip('"') for n in os.environ['strategy_names'].split(',')] 23 | 24 | self.single_order_one_bar = True 25 | '''是否每根K线只发一次指令''' 26 | if 'single_order_one_bar' in os.environ: 27 | self.single_order_one_bar = os.environ['single_order_one_bar'] 28 | 29 | self.pg_min:Engine = None 30 | if 'pg_min' in os.environ: 31 | self.pg_min = create_engine(os.environ['pg_min']) 32 | print(f"connecting pg min: {os.environ['pg_min']}") 33 | 34 | self.pg_order:Engine = None 35 | if 'pg_order' in os.environ: 36 | self.pg_order = create_engine(os.environ['pg_order']) 37 | print(f"connecting pg min: {os.environ['pg_order']}") 38 | 39 | self.rds:redis.Redis = None 40 | '''实时行情库&实时order''' 41 | if 'redis_addr' in os.environ: 42 | host, port = os.environ['redis_addr'].split(':') 43 | self.log.info(f'connecting redis: {host}:{port}') 44 | pool = redis.ConnectionPool(host=host, port=port, db=0, decode_responses=True) 45 | self.rds = redis.StrictRedis(connection_pool=pool) 46 | -------------------------------------------------------------------------------- /hfpy/data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | __title__ = '' 5 | __author__ = 'HaiFeng' 6 | __mtime__ = '2016/8/16 ' 7 | """ 8 | 9 | import time 10 | import copy 11 | from .structs import IntervalType 12 | from .order import OrderItem 13 | from .bar import Bar 14 | from .structs import DirectType, OffsetType 15 | 16 | 17 | class Data(object): 18 | '''数据类, 策略继承此类''' 19 | 20 | def __init__(self, stra_barupdate=None, stra_onorder=None): 21 | '''初始所有变量''' 22 | self.stra_uppdate = stra_barupdate 23 | self.stra_onorder = stra_onorder 24 | '''每bar只执行一次交易''' 25 | self.SingleOrderOneBar = False 26 | '''每bar只执行一 次交易''' 27 | '''K线序列''' 28 | self.Bars = [] 29 | '''K线序列''' 30 | '''合约''' 31 | self.Instrument = '' 32 | '''合约''' 33 | '''周期''' 34 | self.Interval = 1 35 | '''周期''' 36 | '''周期类型''' 37 | self.IntervalType = IntervalType.Minute 38 | '''周期类型''' 39 | 40 | '''买卖信号''' 41 | self.Orders = [] 42 | '''买卖信号''' 43 | 44 | '''指标字典 45 | 策略使用的指标保存在此字典中 46 | 以便管理程序显示和处理''' 47 | self.IndexDict = {} 48 | '''指标字典 49 | 策略使用的指标保存在此字典中 50 | 以便管理程序显示和处理''' 51 | '''时间''' 52 | self.D = [] 53 | '''时间''' 54 | '''最高价''' 55 | self.H = [] 56 | '''最高价''' 57 | '''最低价''' 58 | self.L = [] 59 | '''最低价''' 60 | '''开盘价''' 61 | self.O = [] 62 | '''开盘价''' 63 | '''收盘价''' 64 | self.C = [] 65 | '''收盘价''' 66 | '''交易量''' 67 | self.V = [] 68 | '''交易量''' 69 | '''持仓量''' 70 | self.I = [] 71 | '''持仓量''' 72 | 73 | self._lastOrder = OrderItem() 74 | 75 | @property 76 | def AvgEntryPriceShort(self): 77 | '''开仓均价-空''' 78 | return self._lastOrder.AvgEntryPriceShort 79 | 80 | @property 81 | def AvgEntryPriceLong(self): 82 | '''开仓均价-多''' 83 | return self._lastOrder.AvgEntryPriceLong 84 | 85 | @property 86 | def PositionLong(self): 87 | '''持仓-多''' 88 | return self._lastOrder.PositionLong 89 | 90 | @property 91 | def PositionShort(self): 92 | '''持仓-空''' 93 | return self._lastOrder.PositionShort 94 | 95 | @property 96 | def EntryDateLong(self): 97 | '''开仓时间-多''' 98 | return self._lastOrder.EntryDateLong 99 | 100 | @property 101 | def EntryPriceLong(self): 102 | '''开仓价格-多''' 103 | return self._lastOrder.EntryPriceLong 104 | 105 | @property 106 | def ExitDateShort(self): 107 | '''平仓时间-空''' 108 | return self._lastOrder.ExitDateShort 109 | 110 | @property 111 | def ExitPriceShort(self): 112 | '''平仓价-空''' 113 | return self._lastOrder.ExitPriceShort 114 | 115 | @property 116 | def EntryDateShort(self): 117 | '''开仓时间-空''' 118 | return self._lastOrder.EntryDateShort 119 | 120 | @property 121 | def EntryPriceShort(self): 122 | '''开仓价-空''' 123 | return self._lastOrder.EntryPriceShort 124 | 125 | @property 126 | def ExitDateLong(self): 127 | '''平仓时间-多''' 128 | return self._lastOrder.ExitDateLong 129 | 130 | @property 131 | def ExitPriceLong(self): 132 | '''平仓价-多''' 133 | return self._lastOrder.ExitPriceLong 134 | 135 | @property 136 | def LastEntryDateShort(self): 137 | '''最后开仓时间-空''' 138 | return self._lastOrder.LastEntryDateShort 139 | 140 | @property 141 | def LastEntryPriceShort(self): 142 | '''最后开仓价-空''' 143 | return self._lastOrder.LastEntryPriceShort 144 | 145 | @property 146 | def LastEntryDateLong(self): 147 | '''最后开仓时间-多''' 148 | return self._lastOrder.LastEntryDateLong 149 | 150 | @property 151 | def LastEntryPriceLong(self): 152 | '''最后开仓价-多''' 153 | return self._lastOrder.LastEntryPriceLong 154 | 155 | @property 156 | def IndexEntryLong(self): 157 | '''开仓到当前K线数量-多''' 158 | return self._lastOrder.IndexEntryLong 159 | 160 | @property 161 | def IndexEntryShort(self): 162 | '''开仓到当前K线数量-空''' 163 | return self._lastOrder.IndexEntryShort 164 | 165 | @property 166 | def IndexLastEntryLong(self): 167 | '''最后开仓到当前K线的数量-多''' 168 | return self._lastOrder.IndexLastEntryLong 169 | 170 | @property 171 | def IndexLastEntryShort(self): 172 | '''最后开仓到当前K线的数量-空''' 173 | return self._lastOrder.IndexLastEntryShort 174 | 175 | @property 176 | def IndexExitLong(self): 177 | '''平仓到当前K线数量-多''' 178 | return self._lastOrder.IndexExitLong 179 | 180 | @property 181 | def IndexExitShort(self): 182 | '''平仓到当前K线数量-空''' 183 | return self._lastOrder.IndexExitShort 184 | 185 | @property 186 | def Position(self): 187 | '''持仓净头寸''' 188 | return self.PositionLong - self.PositionShort 189 | 190 | @property 191 | def CurrentBar(self): 192 | '''当前K线序号(0开始)''' 193 | return max(len(self.Bars) - 1, 0) 194 | 195 | def on_min(self, bar:Bar): 196 | '''分笔数据处理''' 197 | if len(self.Bars) == 0 or self.Bars[-1].D != bar.D: # 新数据 198 | self.__new_min_bar__(bar) # 新K线数据插入 199 | else: 200 | self.__update_bar__(bar) 201 | 202 | def __new_min_bar__(self, bar2): 203 | """有新min_bar添加""" 204 | bar = copy.copy(bar2) 205 | bar_time = time.strptime(bar.D, "%Y-%m-%d %H:%M:%S") 206 | year = bar_time.tm_year 207 | mon = bar_time.tm_mon 208 | day = bar_time.tm_mday 209 | hour = bar_time.tm_hour 210 | mins = bar_time.tm_min 211 | if self.IntervalType == IntervalType.Minute: 212 | mins = bar_time.tm_min // self.Interval * self.Interval 213 | elif self.IntervalType == IntervalType.Hour: 214 | hour = hour // self.Interval 215 | mins = 0 216 | elif self.IntervalType == IntervalType.Day: 217 | hour = 0 218 | mins = 0 219 | elif self.IntervalType == IntervalType.Month: 220 | hour = 0 221 | mins = 0 222 | day = 1 223 | elif self.IntervalType == IntervalType.Year: 224 | hour = 0 225 | mins = 0 226 | day = 1 227 | mon = 1 228 | elif self.IntervalType == IntervalType.Week: 229 | hour = 0 230 | mins = 0 231 | # 用周号替换日期 232 | day = time.strftime('%W', bar_time) 233 | 234 | # time -> str 235 | bar_time = f'{year}-{mon:02d}-{day:02d} {hour:02d}:{mins:02d}:00' 236 | if len(self.Bars) == 0 or self.Bars[-1].D != bar_time: 237 | bar.D = bar_time 238 | self.Bars.append(bar) 239 | 240 | self.D.append(bar.D) 241 | self.H.append(bar.H) 242 | self.L.append(bar.L) 243 | self.O.append(bar.O) 244 | self.C.append(bar.C) 245 | self.V.append(bar.V) 246 | self.I.append(bar.I) 247 | else: 248 | old_bar = self.Bars[-1] 249 | self.H[-1] = old_bar.H = max(bar.H, old_bar.H) 250 | self.L[-1] = old_bar.L = min(bar.L, old_bar.L) 251 | self.C[-1] = old_bar.C = bar.C 252 | old_bar.V += bar.V 253 | self.V[-1] = old_bar.V 254 | self.I[-1] = old_bar.I = bar.I 255 | # bar.A = tick.AveragePrice 256 | 257 | self.stra_uppdate(self, bar) 258 | 259 | def __update_bar__(self, bar): 260 | """更新当前数据序列""" 261 | 262 | self.D[-1] = bar.D 263 | self.H[-1] = bar.H 264 | self.L[-1] = bar.L 265 | self.O[-1] = bar.O 266 | self.C[-1] = bar.C 267 | self.V[-1] = bar.V 268 | self.I[-1] = bar.I 269 | self.stra_uppdate(self, bar) 270 | 271 | def __order__(self, direction, offset, price, volume, remark): 272 | """策略执行信号""" 273 | if self.SingleOrderOneBar: 274 | # 平仓后允许开仓 275 | if self.ExitDateShort == self.D[-1] and (not (direction == DirectType.Buy and offset == OffsetType.Open)): 276 | return 277 | if self.ExitDateLong == self.D[-1] and (not (direction == DirectType.Sell and offset == OffsetType.Open)): 278 | return 279 | if self.LastEntryDateLong == self.D[-1] or self.LastEntryDateShort == self.D[-1]: 280 | return 281 | # if self.SingleOrderOneBar and (self.LastEntryDateLong == self.D[-1] 282 | # or self.LastEntryDateShort == self.D[-1] 283 | # or self.ExitDateLong == self.D[-1] 284 | # or self.ExitDateShort == self.D[-1]): 285 | # return 286 | order = OrderItem() 287 | order.Instrument = self.Instrument 288 | order.DateTime = self.D[-1] 289 | order.Direction = direction 290 | order.Offset = offset 291 | order.Price = price 292 | order.Volume = volume 293 | order.Remark = remark 294 | 295 | self.Orders.append(order) 296 | 297 | # 处理策略相关属性 298 | order.IndexEntryLong = self._lastOrder.IndexEntryLong 299 | order.IndexExitLong = self._lastOrder.IndexExitLong 300 | order.IndexEntryShort = self._lastOrder.IndexEntryShort 301 | order.IndexExitShort = self._lastOrder.IndexExitShort 302 | order.IndexLastEntryLong = self._lastOrder.IndexLastEntryLong 303 | order.IndexLastEntryShort = self._lastOrder.IndexLastEntryShort 304 | order.AvgEntryPriceLong = self._lastOrder.AvgEntryPriceLong 305 | order.AvgEntryPriceShort = self._lastOrder.AvgEntryPriceShort 306 | order.PositionLong = self._lastOrder.PositionLong 307 | order.PositionShort = self._lastOrder.PositionShort 308 | order.EntryDateLong = self._lastOrder.EntryDateLong 309 | order.EntryDateShort = self._lastOrder.EntryDateShort 310 | order.EntryPriceLong = self._lastOrder.EntryPriceLong 311 | order.EntryPriceShort = self._lastOrder.EntryPriceShort 312 | order.ExitDateLong = self._lastOrder.ExitDateLong 313 | order.ExitDateShort = self._lastOrder.ExitDateShort 314 | order.ExitPriceLong = self._lastOrder.ExitPriceLong 315 | order.ExitPriceShort = self._lastOrder.ExitPriceShort 316 | order.LastEntryDateLong = self._lastOrder.LastEntryDateLong 317 | order.LastEntryDateShort = self._lastOrder.LastEntryDateShort 318 | order.LastEntryPriceLong = self._lastOrder.LastEntryPriceLong 319 | order.LastEntryPriceShort = self._lastOrder.LastEntryPriceShort 320 | 321 | diroff = '{0}-{1}'.format(order.Direction.name, order.Offset.name) 322 | if diroff == 'Buy-Open': 323 | if self._lastOrder.PositionLong == 0: 324 | order.IndexEntryLong = len(self.Bars) - 1 325 | order.EntryDateLong = self.D[-1] # str '20160630 21:25:00' 326 | order.EntryPriceLong = order.Price 327 | order.PositionLong = order.Volume 328 | order.AvgEntryPriceLong = order.Price 329 | else: 330 | order.PositionLong += order.Volume 331 | order.AvgEntryPriceLong = (self._lastOrder.PositionLong * self._lastOrder.AvgEntryPriceLong + order.Volume * order.Price) / (self._lastOrder.PositionLong + order.Volume) 332 | order.IndexLastEntryLong = len(self.Bars) - 1 333 | order.LastEntryPriceLong = order.Price 334 | order.LastEntryDateLong = self.D[-1] 335 | elif diroff == 'Buy-Close': 336 | c_lots = min(self._lastOrder.PositionShort, order.Volume) # 能够平掉的数量 337 | if c_lots > 0: # 避免无仓可平 338 | order.PositionShort -= c_lots 339 | 340 | order.IndexExitShort = len(self.Bars) - 1 341 | order.ExitDateShort = self.D[-1] 342 | order.ExitPriceShort = order.Price 343 | # if order.PositionShort == 0: 344 | # order.AvgEntryPriceShort = 0 # 20180906注销方便后期计算盈利 345 | elif diroff == 'Sell-Open': 346 | if self._lastOrder.PositionShort == 0: 347 | order.IndexEntryShort = len(self.Bars) - 1 348 | order.EntryDateShort = self.D[-1] # time or double or str ??? 349 | order.EntryPriceShort = order.Price 350 | order.AvgEntryPriceShort = order.Price 351 | order.PositionShort = order.Volume 352 | else: 353 | order.PositionShort += order.Volume 354 | order.AvgEntryPriceShort = (self._lastOrder.PositionShort * self._lastOrder.AvgEntryPriceShort + order.Volume * order.Price) / (self._lastOrder.PositionShort + order.Volume) 355 | order.IndexLastEntryShort = len(self.Bars) - 1 356 | order.LastEntryPriceShort = order.Price 357 | order.LastEntryDateShort = self.D[-1] 358 | elif diroff == 'Sell-Close': 359 | c_lots = min(self._lastOrder.PositionLong, order.Volume) # 能够平掉的数量 360 | if c_lots > 0: # 避免无仓可平 361 | order.PositionLong -= c_lots 362 | 363 | order.IndexExitLong = len(self.Bars) - 1 364 | order.ExitDateLong = self.D[-1] 365 | order.ExitPriceLong = order.Price 366 | # if order.PositionLong == 0: 367 | # order.AvgEntryPriceLong = 0 # 20180906注销方便后期计算盈利 368 | 369 | self._lastOrder = order 370 | self.stra_onorder(self, order) 371 | 372 | def Buy(self, price: float, volume: int, remark: str = ''): 373 | """买开""" 374 | self.__order__(DirectType.Buy, OffsetType.Open, price, volume, remark) 375 | 376 | def Sell(self, price, volume, remark: str = ''): 377 | """买平""" 378 | self.__order__(DirectType.Sell, OffsetType.Close, price, volume, 379 | remark) 380 | 381 | def SellShort(self, price, volume, remark: str = ''): 382 | """卖开""" 383 | self.__order__(DirectType.Sell, OffsetType.Open, price, volume, remark) 384 | 385 | def BuyToCover(self, price, volume, remark: str = ''): 386 | """买平""" 387 | self.__order__(DirectType.Buy, OffsetType.Close, price, volume, remark) 388 | -------------------------------------------------------------------------------- /hfpy/order.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | __title__ = '' 5 | __author__ = 'HaiFeng' 6 | __mtime__ = '2016/8/16 ' 7 | """ 8 | 9 | import time 10 | from .structs import DirectType, OffsetType 11 | 12 | class OrderItem(object): 13 | """策略信号""" 14 | 15 | def __init__(self): 16 | """Constructor""" 17 | '''合约''' 18 | self.Instrument = '' 19 | '''合约''' 20 | '''时间 yyyyMMdd HH:mm:ss''' 21 | self.DateTime = time.strftime('%Y%m%d %H:%Mm:%S', time.localtime(time.time())) 22 | '''时间 yyyyMMdd HH:mm:ss''' 23 | '''买卖''' 24 | self.Direction = DirectType.Buy 25 | '''买卖''' 26 | '''开平''' 27 | self.Offset = OffsetType.Open 28 | '''开平''' 29 | '''价格''' 30 | self.Price = 0.0 31 | '''价格''' 32 | '''数量''' 33 | self.Volume = 0 34 | '''数量''' 35 | '''备注''' 36 | self.Remark = '' 37 | '''备注''' 38 | '''关联的开仓指令''' 39 | self.RelationOpenOrders = [] 40 | '''关联的开仓指令''' 41 | '''开仓均价-空''' 42 | self.AvgEntryPriceShort = 0.0 43 | '''开仓均价-空''' 44 | '''开仓均价-多''' 45 | self.AvgEntryPriceLong = 0.0 46 | '''开仓均价-多''' 47 | '''持仓-多''' 48 | self.PositionLong = 0 49 | '''持仓-多''' 50 | '''持仓-空''' 51 | self.PositionShort = 0 52 | '''持仓-空''' 53 | '''开仓时间-多''' 54 | self.EntryDateLong = '' 55 | '''开仓时间-多''' 56 | '''开仓时间-空''' 57 | self.EntryDateShort = '' 58 | '''开仓时间-空''' 59 | '''开仓价格-多''' 60 | self.EntryPriceLong = 0.0 61 | '''开仓价格-多''' 62 | '''开仓价格-空''' 63 | self.EntryPriceShort = 0.0 64 | '''开仓价格-空''' 65 | '''平仓时间-多''' 66 | self.ExitDateLong = '' 67 | '''平仓时间-多''' 68 | '''平仓时间-空''' 69 | self.ExitDateShort = '' 70 | '''平仓时间-空''' 71 | '''平仓价格-多''' 72 | self.ExitPriceLong = 0.0 73 | '''平仓价格-多''' 74 | '''平仓价格-空''' 75 | self.ExitPriceShort = 0.0 76 | '''平仓价格-空''' 77 | '''最后一次开仓时间-多''' 78 | self.LastEntryDateLong = '' 79 | '''最后一次开仓时间-多''' 80 | '''最后一次开仓时间-空''' 81 | self.LastEntryDateShort = '' 82 | '''最后一次开仓时间-空''' 83 | '''最后一次开仓价格-多''' 84 | self.LastEntryPriceLong = 0.0 85 | '''最后一次开仓价格-多''' 86 | '''最后一次开仓价格-空''' 87 | self.LastEntryPriceShort = 0.0 88 | '''最后一次开仓价格-空''' 89 | '''开仓到当前K线的数量(0开始)-多''' 90 | self.IndexEntryLong = 0.0 91 | '''开仓到当前K线的数量(0开始)-多''' 92 | '''开仓到当前K线的数量(0开始)-空''' 93 | self.IndexEntryShort = 0.0 94 | '''开仓到当前K线的数量(0开始)-空''' 95 | '''最后开仓到当前K线的数量(0开始)-多''' 96 | self.IndexLastEntryLong = -1 97 | '''最后开仓到当前K线的数量(0开始)-多''' 98 | '''最后开仓到当前K线的数量(0开始)-空''' 99 | self.IndexLastEntryShort = -1 100 | '''最后开仓到当前K线的数量(0开始)-空''' 101 | '''平仓到当前K线的数量(0开始)-多''' 102 | self.IndexExitLong = -1 103 | '''平仓到当前K线的数量(0开始)-多''' 104 | '''平仓到当前K线的数量(0开始)-空''' 105 | self.IndexExitShort = -1 106 | '''平仓到当前K线的数量(0开始)-空''' 107 | 108 | def __str__(self): 109 | """""" 110 | return '{self.Instrument}, {self.DateTime}, {self.Direction}, {self.Offset}, {self.Price}, {self.Volume}, {self.Remark}'.format( 111 | self=self) 112 | -------------------------------------------------------------------------------- /hfpy/report_stra.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | __title__ = '策略报告' 4 | __author__ = 'HaiFeng' 5 | __mtime__ = '20180831' 6 | 7 | from .strategy import Strategy 8 | from .data import Data 9 | from .bar import Bar 10 | import os 11 | from .order import OrderItem 12 | import pandas as pd 13 | from pandas import DataFrame, Grouper 14 | import webbrowser 15 | import json 16 | from py_ctp.enums import DirectType, OffsetType 17 | import numpy as np 18 | 19 | 20 | class Report(object): 21 | """""" 22 | 23 | def __init__(self, stra: Strategy, base_fund: float = 1000.0): 24 | """初始化""" 25 | # self.yi_、_an_Bar_ji_suan = .0 # 一、按Bar计算 26 | self.chu_shi_zi_jin = base_fund # 初始资金 27 | self.zui_da_hui_che_bi_lv = .0 # 最大回撤比率 28 | self.zui_da_hui_che_qu_jian = .0 # 最大回撤区间 29 | # self.er_、_an_jiao_yi_bi_shu_ji_suan = .0 # 二、按交易笔数计算 30 | self.jiao_yi_ci_shu = .0 # 交易次数 31 | self.ying_li_ci_shu = .0 # 盈利次数 32 | self.kui_sun_ci_shu = .0 # 亏损次数 33 | 34 | self.zong_ying_li = .0 # 总盈利 35 | self.zong_kui_sun = .0 # 总亏损 36 | self.ying_li_yin_zi = .0 # 盈利因子 37 | 38 | self.ping_jun_ying_li = .0 # 平均盈利 39 | self.ping_jun_kui_sun = .0 # 平均亏损 40 | self.ping_jun_ying_kui_bi = .0 # 平均盈亏比 41 | 42 | self.jing_li_run = .0 # 净利润 43 | self.zui_da_ying_li = .0 # 最大盈利 44 | self.zui_da_kui_sun = .0 # 最大亏损 45 | 46 | self.sheng_lv = .0 # 胜率 47 | self.zui_da_ying_li_zhan_bi = .0 # 最大盈利占比 48 | self.zui_da_kui_sun_zhan_bi = .0 # 最大亏损占比 49 | 50 | # self.san_、_an_ri_ji_suan = .0 # 三、按日计算 51 | self.zong_jiao_yi_tian_shu = .0 # 总交易天数 52 | self.ying_li_tian_shu = .0 # 盈利天数 53 | self.kui_sun_tian_shu = .0 # 亏损天数 54 | 55 | self.ping_jun_mei_tian_kui_sun = .0 # 平均每天亏损 56 | self.ping_jun_mei_tian_ying_li = .0 # 平均每天盈利 57 | self.ri_jun_ying_kui_bi_bi_lv = .0 # 日均盈亏比比率 58 | 59 | self.zui_da_lian_xu_ying_li_ci_shu = .0 # 最大连续盈利次数 60 | self.zui_da_lian_xu_kui_sun_ci_shu = .0 # 最大连续亏损次数 61 | # self.zui_da_lian_xu_ying_li_tian_shu = .0 # 最大连续盈利天数 62 | 63 | self.zui_da_lian_xu_ying_li_jin_e = .0 # 最大连续盈利金额 64 | self.zui_da_lian_xu_kui_sun_jin_e = .0 # 最大连续亏损金额 65 | # self.zui_da_lian_xu_kui_sun_tian_shu = .0 # 最大连续亏损天数 66 | 67 | self.zong_shou_yi_lv = .0 # 总收益率 68 | self.ping_jun_ri_shou_yi = .0 # 平均日收益 69 | self.nian_hua_shou_yi_lv = .0 # 年化收益率 70 | 71 | self.bo_dong_lv = .0 # 波动率 72 | self.xia_pu_bi_lv = .0 # 夏普比率 73 | self.MAR_bi_lv = .0 # MAR比率 74 | 75 | self.zui_da_jing_zhi_bu_chuang_xin_gao_tian_shu = .0 # 最大净值不创新高天数 76 | # self.zui_da_jing_zhi_bu_chuang_xin_gao_qu_jian = .0 # 最大净值不创新高区间 77 | 78 | # self.wu_、_an_yue_ji_suan = .0 # 五、按月计算 79 | # self.zong_jiao_yi_yue_shu = .0 # 总交易月数 80 | # self.ying_li_yue_shu = .0 # 盈利月数 81 | # self.kui_sun_yue_shu = .0 # 亏损月数 82 | # self.chi_ping_yue_shu = .0 # 持平月数 83 | # self.zui_da_lian_xu_ying_li_yue_shu = .0 # 最大连续盈利月数 84 | # self.zui_da_lian_xu_kui_sun_yue_shu = .0 # 最大连续亏损月数 85 | # self.ping_jun_mei_yue_ying_li = .0 # 平均每月盈利 86 | # self.ping_jun_mei_yue_kui_sun = .0 # 平均每月亏损 87 | 88 | self.longwinnlist = [] 89 | self.shortwinnlist = [] 90 | self.winnlist = [] 91 | 92 | self.index_description = {'chu_shi_zi_jin': '初始资金', 93 | 'zui_da_hui_che_bi_lv': '最大回撤比率', 94 | 'zui_da_hui_che_qu_jian': '最大回撤区间', 95 | 96 | 'jing_li_run': '净利润', 97 | 'jiao_yi_ci_shu': '交易次数', 98 | 'ying_li_ci_shu': '盈利次数', 99 | 'kui_sun_ci_shu': '亏损次数', 100 | 'chi_ping_ci_shu': '持平次数', 101 | 'zong_ying_li': '总盈利', 102 | 'zong_kui_sun': '总亏损', 103 | 'ying_li_yin_zi': '盈利因子', 104 | 'sheng_lv': '胜率', 105 | 'ping_jun_ying_li': '平均盈利', 106 | 'ping_jun_kui_sun': '平均亏损', 107 | 'ping_jun_ying_kui_bi': '平均盈亏比', 108 | 'zui_da_ying_li': '最大单次盈利', 109 | 'zui_da_kui_sun': '最大单次亏损', 110 | 'zui_da_ying_li_zhan_bi': '最大盈利占比', 111 | 'zui_da_kui_sun_zhan_bi': '最大亏损占比', 112 | 'zui_da_lian_xu_ying_li_ci_shu': '最大连续盈利次数', 113 | 'zui_da_lian_xu_kui_sun_ci_shu': '最大连续亏损次数', 114 | 'zui_da_lian_xu_ying_li_jin_e': '最大连续盈利金额', 115 | 'zui_da_lian_xu_kui_sun_jin_e': '最大连续亏损金额', 116 | 117 | 'zong_jiao_yi_tian_shu': '总交易天数', 118 | 'ying_li_tian_shu': '盈利天数', 119 | 'kui_sun_tian_shu': '亏损天数', 120 | 'chi_ping_tian_shu': '持平天数', 121 | 'ping_jun_ri_shou_yi': '平均日收益', 122 | 'ping_jun_mei_tian_kui_sun': '平均每天亏损', 123 | 'ping_jun_mei_tian_ying_li': '平均每天盈利', 124 | 'ri_jun_ying_kui_bi_bi_lv': '日均盈亏比比率', 125 | # 'zui_da_lian_xu_ying_li_tian_shu': '最大连续盈利天数', 126 | # 'zui_da_lian_xu_kui_sun_tian_shu': '最大连续亏损天数', 127 | 'zui_da_jing_zhi_bu_chuang_xin_gao_tian_shu': '最大净值不创新高天数', 128 | # 'zui_da_jing_zhi_bu_chuang_xin_gao_qu_jian': '最大净值不创新高区间', 129 | 'zong_shou_yi_lv': '总收益率', 130 | 'nian_hua_shou_yi_lv': '年化收益率', 131 | 'bo_dong_lv': '波动率', 132 | 'xia_pu_bi_lv': '夏普比率', 133 | 'MAR_bi_lv': 'MAR比率', 134 | 135 | # 'zong_jiao_yi_zhou_shu': '总交易周数', 136 | # 'ying_li_zhou_shu': '盈利周数', 137 | # 'kui_sun_zhou_shu': '亏损周数', 138 | # 'chi_ping_zhou_shu': '持平周数', 139 | # 'zhou_sheng_lv': '周胜率', 140 | # 'ping_jun_mei_zhou_kui_sun': '平均每周亏损', 141 | # 'ping_jun_mei_zhou_ying_li': '平均每周盈利', 142 | # 'zui_da_lian_xu_ying_li_zhou_shu': '最大连续盈利周数', 143 | # 'zui_da_lian_xu_kui_sun_zhou_shu': '最大连续亏损周数', 144 | 145 | # 'zong_jiao_yi_yue_shu': '总交易月数', 146 | # 'ying_li_yue_shu': '盈利月数', 147 | # 'kui_sun_yue_shu': '亏损月数', 148 | # 'chi_ping_yue_shu': '持平月数', 149 | # 'zui_da_lian_xu_ying_li_yue_shu': '最大连续盈利月数', 150 | # 'zui_da_lian_xu_kui_sun_yue_shu': '最大连续亏损月数', 151 | # 'ping_jun_mei_yue_kui_sun': '平均每月亏损', 152 | # 'ping_jun_mei_yue_ying_li': '平均每月盈利', 153 | 154 | # 'zong_jiao_yi_nian_shu': '总交易年数', 155 | # 'ying_li_nian_shu': '盈利年数', 156 | # 'kui_sun_nian_shu': '亏损年数', 157 | # 'chi_ping_nian_shu': '持平年数', 158 | # 'zui_da_lian_xu_ying_li_nian_shu': '最大连续盈利年数', 159 | # 'zui_da_lian_xu_kui_sun_nian_shu': '最大连续亏损年数', 160 | # 'ping_jun_mei_nian_kui_sun': '平均每年亏损', 161 | # 'ping_jun_mei_nian_ying_li': '平均每年盈利' 162 | 163 | } 164 | 165 | data: Data = stra.Datas[0] 166 | if len(data.Orders) == 0: 167 | return 168 | j = '' 169 | bar: Bar = None 170 | for bar in data.Bars: 171 | js = '"D":"{}",'.format(bar.D) 172 | js += '"TD":{},'.format(bar.Tradingday) 173 | js += '"O":{},'.format(bar.O) 174 | js += '"H":{},'.format(bar.H) 175 | js += '"L":{},'.format(bar.L) 176 | js += '"C":{},'.format(bar.C) 177 | js += '"V":{},'.format(bar.V) 178 | js += '"I":{}'.format(bar.I) 179 | j += '{{{}}},'.format(js) 180 | j = '[{}]'.format(j[:-1]) 181 | df_data: DataFrame = pd.read_json(j) 182 | df_data['D'] = pd.to_datetime(df_data['D'], format='%Y%m%d %H:%M:%S') 183 | df_data = df_data.set_index('D', drop=True) 184 | df_data = df_data[['O', 'H', 'L', 'C', 'V', 'I', 'TD']] 185 | # print(df_data) 186 | j = '' 187 | o: OrderItem = None 188 | for o in data.Orders: 189 | js = '"D":"{}",'.format(o.DateTime) 190 | # js += '"OP":"{}",'.format(o.Direction.name[0] + ('K' if o.Offset.name[0] == 'O' else 'P')) 191 | # js += '"Price":{},'.format(o.Price) 192 | # js += '"Volume":{}'.format(o.Volume) 193 | js += '"AvgEntryPriceLong":{},'.format(o.AvgEntryPriceLong) 194 | js += '"AvgEntryPriceShort":{},'.format(o.AvgEntryPriceShort) 195 | js += '"PositionLong":{},'.format(o.PositionLong) 196 | js += '"PositionShort":{},'.format(o.PositionShort) 197 | # AvgEntryPriceLong 无仓时为0 198 | js += '"CloseProfit":{},'.format((o.Price - o.AvgEntryPriceLong if o.Direction.name[0] + o.Offset.name[0] == 'SC' else o.AvgEntryPriceShort - o.Price if o.Direction.name[0] + o.Offset.name[0] == 'BC' else 0) * o.Volume) 199 | j += '{{{}}},'.format(js) 200 | j = '[{}]'.format(j[:-1]) 201 | df_order: DataFrame = pd.read_json(j) 202 | df_order['D'] = pd.to_datetime(df_order['D'], format='%Y%m%d %H:%M:%S') 203 | df_order = df_order.set_index('D', drop=True) 204 | # df_order = df_order.ix[:, ['AvgEntryPriceLong', 'AvgEntryPriceShort', 'PositionLong', 'PositionShort']] 205 | # 把交易日和交易合并 206 | df_data = df_data.join(df_order, on='D', how='left') 207 | # df_data = df_data.fillna(method='ffill') 208 | # df_data = df_data.fillna(value=0) 209 | self.df_data = df_data 210 | self.stra = stra 211 | self.get_report(stra) 212 | self.show(stra) 213 | 214 | def get_report(self, stra: Strategy): 215 | """按日统计""" 216 | # 插入一行序号 217 | nr = {'nr': range(self.df_data.shape[0])} 218 | self.df_data.insert(12, 'nr', nr['nr']) 219 | self.df_data['profit10'] = self.df_data['CloseProfit'].notnull() 220 | self.df_data.insert(13, 'Profit', nr['nr']) 221 | 222 | self.daywinnlist = [] 223 | daysumm = 0 224 | longpos = 0 225 | shortpos = 0 226 | profit = 0.0 227 | longprice = 0.0 228 | shortprice = 0.0 229 | 230 | # df_data的循环 231 | for index, row in self.df_data.iterrows(): 232 | 233 | # 没有交易的日子进行填补 234 | if not row['profit10']: 235 | self.df_data.at[index, 'PositionShort'] = shortpos 236 | self.df_data.at[index, 'PositionLong'] = longpos 237 | self.df_data.at[index, 'CloseProfit'] = profit 238 | self.df_data.at[index, 'AvgEntryPriceLong'] = longprice 239 | self.df_data.at[index, 'AvgEntryPriceShort'] = shortprice 240 | # 多空及无持仓时,profit 的填补 241 | if self.df_data.at[index, 'PositionLong'] > 0 and self.df_data.at[index, 'PositionShort'] == 0: 242 | self.df_data.at[index, 'Profit'] = daysumm + (self.df_data.at[index, 'C'] - self.df_data.at[index, 'AvgEntryPriceLong']) * self.df_data.at[index, 'PositionLong'] 243 | elif self.df_data.at[index, 'PositionShort'] > 0 and self.df_data.at[index, 'PositionLong'] == 0: 244 | self.df_data.at[index, 'Profit'] = daysumm + (self.df_data.at[index, 'AvgEntryPriceShort'] - self.df_data.at[index, 'C']) * self.df_data.at[index, 'PositionShort'] 245 | else: 246 | self.df_data.at[index, 'Profit'] = daysumm 247 | 248 | # 有交易的日子进行赋值 249 | if row['profit10'] and row['PositionShort'] == 0 and row['PositionLong'] == 0: 250 | daysumm = daysumm + row['CloseProfit'] 251 | self.df_data.at[index, 'Profit'] = daysumm 252 | longpos = 0 253 | shortpos = 0 254 | longprice = 0.0 255 | shortprice = 0.0 256 | profit = 0.0 257 | elif row['profit10'] and row['PositionShort'] == 1 and row['PositionLong'] == 0: 258 | daysumm = daysumm + row['CloseProfit'] 259 | self.df_data.at[index, 'Profit'] = daysumm 260 | longpos = 0 261 | shortpos = row['PositionShort'] 262 | longprice = 0 263 | shortprice = row['AvgEntryPriceShort'] 264 | profit = row['CloseProfit'] 265 | elif row['profit10'] and row['PositionShort'] == 0 and row['PositionLong'] == 1: 266 | daysumm = daysumm + row['CloseProfit'] 267 | self.df_data.at[index, 'Profit'] = daysumm 268 | longpos = row['PositionLong'] 269 | shortpos = 0 270 | longprice = row['AvgEntryPriceLong'] 271 | shortprice = 0 272 | profit = row['CloseProfit'] 273 | 274 | elif row['profit10'] and row['PositionShort'] == 1 and row['PositionLong'] == 1: 275 | daysumm = daysumm + row['CloseProfit'] 276 | self.df_data.at[index, 'Profit'] = daysumm 277 | longpos = row['PositionLong'] 278 | shortpos = row['PositionShort'] 279 | longprice = row['AvgEntryPriceLong'] 280 | shortprice = row['AvgEntryPriceLong'] 281 | profit = row['CloseProfit'] 282 | 283 | # # self.san_、_an_ri_ji_suan = .0 # 三、按日计算 284 | self.df_data['post_net'] = self.df_data['PositionLong'] + self.df_data['PositionShort'] 285 | # self.df_data['ProfitLong'] = (self.df_data['C'] - self.df_data['AvgEntryPriceLong']) * self.df_data['PositionLong'] 286 | # self.df_data['ProfitShort'] = (self.df_data['AvgEntryPriceShort'] - self.df_data['C']) * self.df_data['PositionShort'] 287 | # self.df_data['Profit'] = self.df_data['ProfitLong'] + self.df_data['ProfitShort'] 288 | # # self.df_data['Profit_'] = 1 if self.df_data['Profit'] > 0 else -1 if self.df_data['Profit'] < 0 else 0 289 | # self.df_data['Profit'] = self.df_data['profit2'] 290 | 291 | g = self.df_data.groupby('TD', axis=0, sort=True) # Grouper(freq='1B', axis=0, sort=True)) 292 | df_day: DataFrame = DataFrame() 293 | df_day['D'] = g.indices 294 | 295 | # 收益累加 按日计算,累计的盈亏 296 | df_day['Profit'] = g['Profit'].last() 297 | # 平仓收益按,当日无平仓为零,每日得盈亏 298 | df_day['CloseProfit'] = g['CloseProfit'].sum() 299 | 300 | # ===================================== 笔统计 =================== 301 | 302 | # 计算单笔收益 303 | 304 | longinprice = 0 305 | shortinprice = 0 306 | for o in stra.Datas[0].Orders: 307 | if o.Offset == OffsetType.Open and o.Direction == DirectType.Buy: 308 | longinprice = o.Price 309 | elif o.Offset == OffsetType.Open and o.Direction == DirectType.Sell: 310 | shortinprice = o.Price 311 | elif o.Offset == OffsetType.Close and o.Direction == DirectType.Sell: 312 | winn = o.Price - longinprice 313 | self.longwinnlist.append(winn) 314 | self.winnlist.append(winn) 315 | elif o.Offset == OffsetType.Close and o.Direction == DirectType.Buy: 316 | winn = shortinprice - o.Price 317 | self.shortwinnlist.append(winn) 318 | self.winnlist.append(winn) 319 | 320 | # self.jing_li_run = .0 # 净利润 321 | self.jing_li_run = self.df_data['Profit'][-1] 322 | # self.jiao_yi_ci_shu = .0 # 交易次数::当前持仓比下一持仓大,则表示有平仓,即视为一次交易 323 | self.jiao_yi_ci_shu = len(self.df_data[self.df_data['CloseProfit'] != 0]) 324 | if self.jiao_yi_ci_shu == 0: 325 | print("没有成交") 326 | return 327 | # self.ying_li_ci_shu = .0 # 盈利次数 328 | self.ying_li_ci_shu = len(self.df_data[self.df_data['CloseProfit'] > 0]) 329 | # self.kui_sun_ci_shu = .0 # 亏损次数 330 | self.kui_sun_ci_shu = len(self.df_data[self.df_data['CloseProfit'] < 0]) 331 | # self.zong_ying_li = .0 # 总盈利 332 | self.zong_ying_li = self.df_data[self.df_data['CloseProfit'] > 0]['CloseProfit'].sum() 333 | # self.zong_kui_sun = .0 # 总亏损 334 | self.zong_kui_sun = self.df_data[self.df_data['CloseProfit'] < 0]['CloseProfit'].sum() 335 | # self.sheng_lv = .0 # 胜率 336 | self.sheng_lv = round(self.ying_li_ci_shu / self.jiao_yi_ci_shu, 4) 337 | 338 | # 计算最大回撤区间和比例 最大不创新高天数 339 | maxprofit = 0 340 | nowback = 0 341 | nowbacklist = [] 342 | nowbackperc = 0 343 | nowbackperclist = [] 344 | 345 | maxmany = 0 # 最大净值 346 | notmaxmanyday = 0 347 | maxnotmaxmanyday = 0 348 | for index, row in df_day.iterrows(): 349 | maxprofit = max(row["Profit"], maxprofit) 350 | nowback = - (row["Profit"] - maxprofit) 351 | if nowback != 0 and maxprofit != 0: 352 | nowbackperc = nowback / (maxprofit + self.chu_shi_zi_jin) 353 | else: 354 | nowbackperc = 0 355 | nowbacklist.append(nowback) 356 | nowbackperclist.append(nowbackperc) 357 | 358 | maxmany = max(row["Profit"], maxmany) 359 | if row["Profit"] == maxmany: 360 | notmaxmanyday = 0 361 | else: 362 | notmaxmanyday = notmaxmanyday + 1 363 | maxnotmaxmanyday = max(maxnotmaxmanyday, notmaxmanyday) 364 | 365 | # 最大净值不创新高天数 366 | self.zui_da_jing_zhi_bu_chuang_xin_gao_tian_shu = maxnotmaxmanyday 367 | # 最大回撤比率 368 | self.zui_da_hui_che_bi_lv = round(max(nowbackperclist), 4) 369 | 370 | # 最大回撤区间 371 | self.zui_da_hui_che_qu_jian = max(nowbacklist) 372 | 373 | if self.zong_ying_li > 0: 374 | # self.ping_jun_ying_li = .0 # 平均盈利 375 | self.ping_jun_ying_li = round(self.zong_ying_li / self.ying_li_ci_shu, 2) 376 | else: 377 | # self.ping_jun_ying_li = .0 # 平均盈利 378 | self.ping_jun_ying_li = 0 379 | 380 | if self.zong_kui_sun < 0: 381 | # self.ping_jun_kui_sun = .0 # 平均亏损 382 | self.ping_jun_kui_sun = round(self.zong_kui_sun / self.kui_sun_ci_shu, 2) 383 | # self.ping_jun_ying_kui_bi = .0 # 平均盈亏比 384 | self.ping_jun_ying_kui_bi = - round(self.ping_jun_ying_li / self.ping_jun_kui_sun, 2) 385 | 386 | else: 387 | # self.ping_jun_kui_sun = .0 # 平均亏损 388 | self.ping_jun_kui_sun = 0 389 | # self.ping_jun_ying_kui_bi = .0 # 平均盈亏比 390 | self.ping_jun_ying_kui_bi = 0 391 | 392 | # self.zui_da_ying_li = .0 # 最大盈利 393 | self.zui_da_ying_li = self.df_data[self.df_data['CloseProfit'] > 0]['CloseProfit'].max() 394 | # self.zui_da_kui_sun = .0 # 最大亏损 395 | self.zui_da_kui_sun = self.df_data[self.df_data['CloseProfit'] < 0]['CloseProfit'].min() 396 | # self.zui_da_ying_li_zhan_bi = .0 # 最大盈利占比 397 | if self.zong_ying_li > 0: 398 | self.zui_da_ying_li_zhan_bi = round(self.zui_da_ying_li / self.zong_ying_li, 2) 399 | else: 400 | self.zui_da_ying_li_zhan_bi = 0 401 | # self.zui_da_kui_sun_zhan_bi = .0 # 最大亏损占比 402 | if self.zong_kui_sun < 0: 403 | self.zui_da_kui_sun_zhan_bi = round(self.zui_da_kui_sun / self.zong_kui_sun, 2) 404 | else: 405 | self.zui_da_kui_sun_zhan_bi = 0 406 | # # self.zui_da_lian_xu_ying_li_ci_shu = .0 # 最大连续盈利次数 407 | # self.df_data['CloseProfit_cnt'] = (self.df_data['CloseProfit'] > 0).astype(int) 408 | # self.df_data['CloseProfit_times'] = self.df_data.groupby((self.df_data['CloseProfit_cnt'] != self.df_data['CloseProfit_cnt'].shift(1)).cumsum()).cumcount() + 1 409 | # self.zui_da_lian_xu_ying_li_ci_shu = self.df_data['CloseProfit_times'].max() 410 | # # self.zui_da_lian_xu_kui_sun_ci_shu = .0 # 最大连续亏损次数 411 | # self.df_data['CloseLoss_cnt'] = (self.df_data['CloseProfit'] < 0).astype(int) 412 | # self.df_data['CloseLoss_times'] = self.df_data.groupby((self.df_data['CloseLoss_cnt'] != self.df_data['CloseLoss_cnt'].shift(1)).cumsum()).cumcount() + 1 413 | # self.zui_da_lian_xu_kui_sun_ci_shu = self.df_data['CloseLoss_times'].max() 414 | # self.ying_li_yin_zi = .0 # 盈利因子 415 | self.ying_li_yin_zi = round(self.zong_ying_li / self.zong_kui_sun, 2) 416 | # # self.zui_da_lian_xu_ying_li_jin_e = .0 # 最大连续盈利金额 417 | # self.df_data['CloseProfit_money'] = self.df_data.groupby((self.df_data['CloseProfit_cnt'] != self.df_data['CloseProfit_cnt'].shift(1)).cumsum()).cumsum() 418 | # self.zui_da_lian_xu_ying_li_jin_e = self.df_data['CloseProfit_money'].max() 419 | # # self.zui_da_lian_xu_kui_sun_jin_e = .0 # 最大连续亏损金额 420 | # self.df_data['CloseLoss_money'] = self.df_data.groupby((self.df_data['CloseLoss_cnt'] != self.df_data['CloseLoss_cnt'].shift(1)).cumsum()).cumsum() 421 | # self.zui_da_lian_xu_kui_sun_jin_e = self.df_data['CloseLoss_money'].max() 422 | 423 | # =============================== 日统计 ===================== 424 | # self.zui_da_jing_zhi_bu_chuang_xin_gao_tian_shu = .0 # 最大净值不创新高天数 425 | # self.zui_da_jing_zhi_bu_chuang_xin_gao_qu_jian = .0 # 最大净值不创新高区间 426 | # self.bo_dong_lv = .0 # 波动率 427 | self.bo_dong_lv = round((self.df_data['Profit'] / 1000).std(), 2) # mean收益率 428 | 429 | # self.zong_jiao_yi_tian_shu = .0 # 总交易天数 430 | self.zong_jiao_yi_tian_shu = len(df_day['Profit']) 431 | 432 | # self.ying_li_tian_shu = .0 # 盈利天数 433 | self.ying_li_tian_shu = len([p for p in df_day['Profit'] if p > 0]) 434 | 435 | # self.kui_sun_tian_shu = .0 # 亏损天数 436 | self.kui_sun_tian_shu = len([p for p in df_day['Profit'] if p < 0]) 437 | 438 | # self.ping_jun_ri_shou_yi = .0 # 平均日收益 439 | self.ping_jun_ri_shou_yi = round(df_day['Profit'] / self.zong_jiao_yi_tian_shu, 2) 440 | 441 | # self.ping_jun_mei_tian_kui_sun = .0 # 平均每天亏损 442 | self.ping_jun_mei_tian_kui_sun = round(sum([p for p in df_day['Profit'] if p < 0]) / self.zong_jiao_yi_tian_shu, 2) 443 | 444 | # self.ping_jun_mei_tian_ying_li = .0 # 平均每天盈利 445 | self.ping_jun_mei_tian_ying_li = round(sum([p for p in df_day['Profit'] if p > 0]) / self.zong_jiao_yi_tian_shu, 2) 446 | 447 | # self.ri_jun_ying_kui_bi_bi_lv = .0 # 日均盈亏比比率 448 | self.ri_jun_ying_kui_bi_bi_lv = 0 if self.ping_jun_mei_tian_kui_sun == 0 else round(self.ping_jun_mei_tian_ying_li / self.ping_jun_mei_tian_kui_sun, 4) 449 | 450 | # self.zui_da_lian_xu_ying_li_tian_shu = .0 # 最大连续盈利天数 451 | 452 | # self.zui_da_lian_xu_kui_sun_tian_shu = .0 # 最大连续亏损天数 453 | # 计算最大盈利和亏损次数 金额 454 | winntime = 0 455 | losstime = 0 456 | winnmany = 0 457 | lossmany = 0 458 | 459 | maxwinntime = 0 460 | maxlosstime = 0 461 | maxwinnmany = 0 462 | maxlossmany = 0 463 | 464 | for winns in self.winnlist: 465 | if winns > 0: 466 | maxlosstime = max(maxlosstime, losstime) 467 | losstime = 0 468 | winntime = winntime + 1 469 | maxlossmany = min(maxlossmany, lossmany) 470 | lossmany = 0 471 | winnmany = winnmany + winns 472 | elif winns < 0: 473 | maxwinntime = max(winntime, maxwinntime) 474 | winntime = 0 475 | losstime = losstime + 1 476 | maxwinnmany = max(winnmany, maxwinnmany) 477 | winnmany = 0 478 | lossmany = lossmany + winns 479 | 480 | self.zui_da_lian_xu_ying_li_ci_shu = maxwinntime 481 | self.zui_da_lian_xu_kui_sun_ci_shu = maxlosstime 482 | self.zui_da_lian_xu_ying_li_jin_e = maxwinnmany 483 | self.zui_da_lian_xu_kui_sun_jin_e = maxlossmany 484 | 485 | ''' 486 | # self.zui_da_lian_xu_ying_li_tian_shu = .0 # 最大连续盈利天数 487 | df_day['Profit_cnt'] = (df_day['Profit'] > 0).astype(int) 488 | df_day['Profit_days'] = df_day.groupby((df_day['Profit_cnt'] != df_day['Profit_cnt'].shift(1)).cumsum()).cumcount() + 1 489 | self.zui_da_lian_xu_ying_li_tian_shu = df_day['Profit_days'].max() 490 | 491 | # self.zui_da_lian_xu_kui_sun_tian_shu = .0 # 最大连续亏损天数 492 | df_day['Loss_cnt'] = (df_day['Profit'] < 0).astype(int) 493 | df_day['Loss_days'] = df_day.groupby((df_day['Loss_cnt'] != df_day['Loss_cnt'].shift(1)).cumsum()).cumcount() + 1 494 | self.zui_da_lian_xu_kui_sun_tian_shu = df_day['Loss_days'].max() 495 | ''' 496 | # self.zong_shou_yi_lv = .0 # 总收益率 497 | self.zong_shou_yi_lv = round(self.df_data['Profit'][-1] / self.chu_shi_zi_jin, 2) 498 | 499 | # self.xia_pu_bi_lv = .0 # 夏普比率 500 | self.xia_pu_bi_lv = round((self.zong_shou_yi_lv - 0.04) / self.bo_dong_lv, 4) 501 | 502 | # self.MAR_bi_lv = .0 # MAR比率 503 | self.MAR_bi_lv = round(self.zong_shou_yi_lv / self.zui_da_hui_che_bi_lv, 4) 504 | 505 | # self.nian_hua_shou_yi_lv = .0 # 年化收益率 506 | g_year = self.df_data.groupby(Grouper(freq='12M', axis=0, sort=True)) 507 | self.nian_hua_shou_yi_lv = round(self.df_data['Profit'].sum() / len(g_year) / self.chu_shi_zi_jin, 2) 508 | 509 | def show(self, stra: Strategy): 510 | report = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'template.html'), 'r', encoding='utf-8').read() 511 | tempData = [] 512 | reportData = [] 513 | for k, v in vars(self).items(): 514 | if type(v) == float or type(v) == int or type(v) == np.float64: 515 | tempData.append({ 516 | 'item': self.index_description[k], 517 | 'value': v 518 | }) 519 | 520 | if len(tempData) % 3 == 0: 521 | rownum = int(len(tempData) / 3) 522 | else: 523 | rownum = int(len(tempData) / 3) + 1 524 | tempData.append({ 525 | 'item': '', 526 | 'value': '' 527 | }) 528 | if len(tempData) % 3 == 2: 529 | tempData.append({ 530 | 'item': '', 531 | 'value': '' 532 | }) 533 | 534 | for i in range(rownum): 535 | reportData.append({ 536 | 'item_1': tempData[i]['item'], 537 | 'value_1': tempData[i]['value'], 538 | 'item_2': tempData[rownum * 1 + i]['item'], 539 | 'value_2': tempData[rownum * 1 + i]['value'], 540 | 'item_3': tempData[rownum * 2 + i]['item'], 541 | 'value_3': tempData[rownum * 2 + i]['value'] 542 | }) 543 | 544 | # items_per_row = 3 545 | # table = '' 546 | # for i in range(items_per_row): 547 | # table += '项目值' 548 | # table += '' 549 | # idx = 0 550 | # for k, v in vars(self).items(): 551 | # print(self.index_description[k], v) 552 | # if str(type(v)).find('int') > 0: 553 | # table += '{}{}'.format(self.index_description[k], v) 554 | # # print('{0:{2}<12}:{1:>9d}. |'.format(self.index_description[k], v, chr(12288)), end='\t') 555 | # elif str(type(v)).find('float') > 0: 556 | # table += '{}{}'.format(self.index_description[k], v) 557 | # # print('{0:{2}<12}:{1:>13.3f}|'.format(self.index_description[k], v, chr(12288)), end='\t') 558 | # else: 559 | # continue 560 | # idx += 1 561 | # if idx % items_per_row == 0: 562 | # table += '' 563 | # # print('') 564 | # table += '' 565 | report = report.replace('$report_table$', str(json.dumps(reportData))) 566 | 567 | bars_json = [] 568 | for bar in self.stra.Bars: 569 | bars_json.append([bar.D, bar.O, bar.C, bar.L, bar.H]) 570 | report = report.replace('$bars$', str(bars_json)) 571 | 572 | orders_json = [] 573 | for ord in self.stra.Orders: 574 | # 遇到diction=Diction.Buy转换后:diction: 后面报错 575 | # orders_str.append(ord.__dict__) 576 | orders_json.append([ord.DateTime, (0 if ord.Direction == DirectType.Buy else 1), (0 if ord.Offset == OffsetType.Open else 1), ord.Price, ord.Volume]) 577 | report = report.replace('$orders$', json.dumps(orders_json)) 578 | 579 | g = self.df_data.groupby('TD', axis=0, sort=True) # Grouper(freq='1B', axis=0, sort=True)) 580 | df_day: DataFrame = DataFrame() 581 | df_day['D'] = g.indices 582 | 583 | # 收益累加 按日计算,累计的盈亏 584 | df_day['Profit'] = g['Profit'].last() 585 | # 平仓收益按,当日无平仓为零,每日得盈亏 586 | df_day['CloseProfit'] = g['CloseProfit'].sum() 587 | 588 | lei_ji_shou_yi = df_day['Profit'] 589 | quanyi = lei_ji_shou_yi.to_dict().items() 590 | quanyi = [[k, v] for k, v in quanyi] 591 | report = report.replace('$quanyi$', str(quanyi)) 592 | 593 | # 计算累计收益率 做多和做空的 594 | 595 | winnsumlist = np.array(self.winnlist, dtype=float).cumsum() 596 | longwinnsumlist = np.array(self.longwinnlist, dtype=float).cumsum() 597 | shortwinnsumlist = np.array(self.shortwinnlist, dtype=float).cumsum() 598 | 599 | leijishouyi = [] 600 | longleijishouyi = [] 601 | shortleijishouyi = [] 602 | for x in range(len(winnsumlist)): 603 | leijishouyi.append([x, winnsumlist[x]]) 604 | 605 | for x in range(len(longwinnsumlist)): 606 | longleijishouyi.append([x, longwinnsumlist[x]]) 607 | 608 | for x in range(len(shortwinnsumlist)): 609 | shortleijishouyi.append([x, shortwinnsumlist[x]]) 610 | 611 | report = report.replace('$leijishouyi$', str(leijishouyi)) 612 | report = report.replace('$longleijishouyi$', str(longleijishouyi)) 613 | report = report.replace('$shortleijishouyi$', str(shortleijishouyi)) 614 | 615 | with open('r.html', 'w', encoding='utf-8') as w: 616 | w.write(report) 617 | webbrowser.open("r.html") 618 | -------------------------------------------------------------------------------- /hfpy/strategy.py: -------------------------------------------------------------------------------- 1 | # !/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | __title__ = '' 5 | __author__ = 'HaiFeng' 6 | __mtime__ = '2017/11/13' 7 | """ 8 | import time 9 | from .structs import IntervalType 10 | from .bar import Bar 11 | from .data import Data 12 | from .order import OrderItem 13 | 14 | 15 | class Strategy(object): 16 | '''策略类 ''' 17 | 18 | def __init__(self, dict_cfg): 19 | '''初始化''' 20 | '''策略标识''' 21 | self.ID = 0 22 | '''数据序列''' 23 | self.Datas = [] 24 | """起始测试时间 25 | 格式:yyyyMMdd[%Y%m%d] 26 | 默认:20170101""" 27 | self.BeginDate = '20170101' 28 | 29 | '''参数''' 30 | self.Params = [] 31 | '''分笔测试''' 32 | self.TickTest = False 33 | 34 | if dict_cfg == '': 35 | return 36 | else: 37 | self.ID = dict_cfg['ID'] 38 | self.Params = dict_cfg['Params'] 39 | self.BeginDate = str(dict_cfg['BeginDate']) 40 | if 'TickTest' in dict_cfg: 41 | self.TickTest = dict_cfg['TickTest'] 42 | for data in dict_cfg['Datas']: 43 | newdata = Data(self.__BarUpdate, self.__OnOrder) 44 | newdata.Instrument = data['Instrument'] 45 | newdata.Interval = data['Interval'] 46 | newdata.IntervalType = IntervalType[data['IntervalType']] 47 | self.Datas.append(newdata) 48 | 49 | @property 50 | def Bars(self): 51 | '''k''' 52 | return self.Datas[0].Bars 53 | 54 | @property 55 | def Instrument(self): 56 | '''合约''' 57 | return self.Datas[0].Instrument 58 | 59 | @property 60 | def Interval(self): 61 | '''周期''' 62 | return self.Datas[0].Interval 63 | 64 | @property 65 | def IntervalType(self): 66 | '''周期类型''' 67 | return self.Datas[0].IntervalType 68 | 69 | @property 70 | def Orders(self): 71 | '''买卖信号''' 72 | return self.Datas[0].Orders 73 | 74 | @property 75 | def IndexDict(self): 76 | '''指标字典 77 | 策略使用的指标保存在此字典中 78 | 以便管理程序显示和处理''' 79 | return self.Datas[0].IndexDict 80 | 81 | @property 82 | def D(self): 83 | '''时间''' 84 | return self.Datas[0].D 85 | 86 | @property 87 | def H(self): 88 | '''最高价''' 89 | return self.Datas[0].H 90 | 91 | @property 92 | def L(self): 93 | '''最低价''' 94 | return self.Datas[0].L 95 | 96 | @property 97 | def O(self): 98 | '''开盘价''' 99 | return self.Datas[0].O 100 | 101 | @property 102 | def C(self): 103 | '''收盘价''' 104 | return self.Datas[0].C 105 | 106 | @property 107 | def V(self): 108 | '''交易量''' 109 | return self.Datas[0].V 110 | 111 | @property 112 | def I(self): 113 | '''持仓量''' 114 | return self.Datas[0].I 115 | 116 | @property 117 | def AvgEntryPriceShort(self): 118 | '''开仓均价-空''' 119 | return self.Datas[0].AvgEntryPriceShort 120 | 121 | @property 122 | def AvgEntryPriceLong(self): 123 | '''开仓均价-多''' 124 | return self.Datas[0].AvgEntryPriceLong 125 | 126 | @property 127 | def PositionLong(self): 128 | '''持仓-多''' 129 | return self.Datas[0].PositionLong 130 | 131 | @property 132 | def PositionShort(self): 133 | '''持仓-空''' 134 | return self.Datas[0].PositionShort 135 | 136 | @property 137 | def EntryDateLong(self): 138 | '''开仓时间-多''' 139 | return self.Datas[0].EntryDateLong 140 | 141 | @property 142 | def EntryPriceLong(self): 143 | '''开仓价格-多''' 144 | return self.Datas[0].EntryPriceLong 145 | 146 | @property 147 | def ExitDateShort(self): 148 | '''平仓时间-空''' 149 | return self.Datas[0].ExitDateShort 150 | 151 | @property 152 | def ExitPriceShort(self): 153 | '''平仓价-空''' 154 | return self.Datas[0].ExitPriceShort 155 | 156 | @property 157 | def EntryDateShort(self): 158 | '''开仓时间-空''' 159 | return self.Datas[0].EntryDateShort 160 | 161 | @property 162 | def EntryPriceShort(self): 163 | '''开仓价-空''' 164 | return self.Datas[0].EntryPriceShort 165 | 166 | @property 167 | def ExitDateLong(self): 168 | '''平仓时间-多''' 169 | return self.Datas[0].ExitDateLong 170 | 171 | @property 172 | def ExitPriceLong(self): 173 | '''平仓价-多''' 174 | return self.Datas[0].ExitPriceLong 175 | 176 | @property 177 | def LastEntryDateShort(self): 178 | '''最后开仓时间-空''' 179 | return self.Datas[0].LastEntryDateShort 180 | 181 | @property 182 | def LastEntryPriceShort(self): 183 | '''最后开仓价-空''' 184 | return self.Datas[0].LastEntryPriceShort 185 | 186 | @property 187 | def LastEntryDateLong(self): 188 | '''最后开仓时间-多''' 189 | return self.Datas[0].LastEntryDateLong 190 | 191 | @property 192 | def LastEntryPriceLong(self): 193 | '''最后开仓价-多''' 194 | return self.Datas[0].LastEntryPriceLong 195 | 196 | @property 197 | def IndexEntryLong(self): 198 | '''开仓到当前K线数量-多''' 199 | return self.Datas[0].IndexEntryLong 200 | 201 | @property 202 | def IndexEntryShort(self): 203 | '''开仓到当前K线数量-空''' 204 | return self.Datas[0].IndexEntryShort 205 | 206 | @property 207 | def IndexLastEntryLong(self): 208 | '''最后平仓到当前K线数量-多''' 209 | return self.Datas[0].IndexLastEntryLong 210 | 211 | @property 212 | def IndexLastEntryShort(self): 213 | '''最后平仓到当前K线数量-空''' 214 | return self.Datas[0].IndexLastEntryShort 215 | 216 | @property 217 | def IndexExitLong(self): 218 | '''平仓到当前K线数量-多''' 219 | return self.Datas[0].IndexExitLong 220 | 221 | @property 222 | def IndexExitShort(self): 223 | '''平仓到当前K线数量-空''' 224 | return self.Datas[0].IndexExitShort 225 | 226 | @property 227 | def Position(self): 228 | '''持仓净头寸''' 229 | return self.Datas[0].Position 230 | 231 | @property 232 | def CurrentBar(self): 233 | '''当前K线序号(0开始)''' 234 | return self.Datas[0].CurrentBar 235 | 236 | def Buy(self, price: float, volume: int, remark: str = ''): 237 | """买开""" 238 | self.Datas[0].Buy(price, volume, remark) 239 | 240 | def Sell(self, price, volume, remark): 241 | """买平""" 242 | self.Datas[0].Sell(price, volume, remark) 243 | 244 | def SellShort(self, price, volume, remark): 245 | """卖开""" 246 | self.Datas[0].SellShort(price, volume, remark) 247 | 248 | def BuyToCover(self, price, volume, remark): 249 | """买平""" 250 | self.Datas[0].BuyToCover(price, volume, remark) 251 | 252 | def OnBarUpdate(self, data: Data, bar: Bar): 253 | """行情触发 254 | 历史行情:每分钟触发一次 255 | 实时行情:每分钟触发一次""" 256 | pass 257 | 258 | def __BarUpdate(self, data: Data, bar: Bar): 259 | """调用策略的逻辑部分""" 260 | # self.OnBarUpdate(data, bar) 261 | if data.Interval == self.Interval and data.IntervalType == self.IntervalType: 262 | self.OnBarUpdate(data, bar) 263 | 264 | def __OnOrder(self, data: Data, order: OrderItem): 265 | """调用外部接口的reqorder""" 266 | # 同时接口发单可不注释 267 | self._data_order(self, data, order) 268 | 269 | # 外层接口调用 270 | def _data_order(self, stra, data: Data, order: OrderItem): 271 | """继承类中实现此函数,有策略信号产生时调用""" 272 | pass 273 | -------------------------------------------------------------------------------- /hfpy/structs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | __title__ = '' 5 | __author__ = 'HaiFeng' 6 | __mtime__ = '2016/9/21' 7 | """ 8 | 9 | from enum import Enum 10 | 11 | 12 | class IntervalType(Enum): 13 | """时间类型:秒,分,时,日,周,月,年""" 14 | 15 | Second = 0 16 | '''秒''' 17 | 18 | Minute = 1 19 | '''分''' 20 | 21 | Hour = 2 22 | '''时''' 23 | 24 | Day = 3 25 | '''日''' 26 | 27 | Week = 4 28 | '''周''' 29 | 30 | Month = 5 31 | '''月''' 32 | 33 | Year = 6 34 | '''年''' 35 | 36 | def __int__(self): 37 | """return int value""" 38 | return self.value 39 | 40 | class DirectType(Enum): 41 | """买卖""" 42 | Buy = 0 43 | """买""" 44 | Sell = 1 45 | """卖""" 46 | 47 | class OffsetType(Enum): 48 | """开平""" 49 | Open = 0 50 | '''开''' 51 | Close = 1 52 | '''平''' -------------------------------------------------------------------------------- /hfpy/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 交易策略绩效报告 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

18 |

绩效报告

19 | 37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |

下单记录

50 | 83 |
84 | 85 | 752 | 753 | 754 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | __title__ = '主程序' 4 | __author__ = 'HaiFeng' 5 | __mtime__ = '20180822' 6 | 7 | from hfpy.atp import ATP 8 | from time import sleep 9 | 10 | if __name__ == '__main__': 11 | atp = ATP() 12 | 13 | # 测试 14 | # from hfpy.structs import ReqPackage, BarType 15 | # import sys 16 | # req = ReqPackage() 17 | # # req.Type = BarType.Real 18 | # req.Instrument = 'rb2005' 19 | # req.Type = BarType.Min 20 | # req.Begin = '20190301' 21 | # req.End = '20200701' 22 | # atp.get_data_zmq(req) 23 | # sys.exit() 24 | 25 | atp.Run() 26 | while True: 27 | sleep(60*10) 28 | # atp.close_api() 29 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 | ![海风](http://git.oschina.net/uploads/2/330302_haifengat.png?1484575602) 4 | 5 | ## HFpy 6 | 7 | 一款开源的策略开发平台.为用户提供方便易用的策略开发工具. 8 | 9 | ## 海风AT的功能 10 | 11 | - 策略编写 12 | - 提供常用指标 13 | - 采用HLOC调用K线数据 14 | - 历史数据 15 | - 提供每日数据 16 | - 提供实时数据分钟级服务 17 | 18 | ## 运行环境 19 | 20 | ### talab 指标库 21 | [https://www.ta-lib.org/function.html](https://www.ta-lib.org/function.html) 22 | 23 | ### 生成镜像 24 | ```bash 25 | docker build -t haifengat/hfpy:`date +%Y%m%d` . && docker push haifengat/hfpy:`date +%Y%m%d` 26 | docker tag haifengat/hfpy:`date +%Y%m%d` haifengat/hfpy && docker push haifengat/hfpy 27 | ``` 28 | 29 | ### 配置docker-compose.yml 30 | #### 环境变量 31 | * strategy_names 32 | * 策略名列表,用","分隔 33 | * 对应的strategies目录下同名策略文件 34 | * single_order_one_bar 35 | * 是否K线只发一个委托,默认 True 36 | * pg_min 37 | * postgresql://postgres:123456@hf_pg:5432/postgres?sslmode=disable 38 | * 分钟数据库 39 | * pg_order 40 | * postgresql://postgres:123456@pg_order:5432/postgres?sslmode=disable 41 | * 策略信号数据库 42 | * redis_addr 43 | * ip:port 44 | * 实时分钟数据库 [md.{instrument}] 读取 45 | * 实时order [order.{stra_name}.{stra_id}] 写入 46 | 47 | ### 示例 docker-compose.yml 48 | ```yaml 49 | version: "3.7" 50 | 51 | services: 52 | hf_py: 53 | image: haifengat/hfpy 54 | container_name: hf_py 55 | restart: always 56 | environment: 57 | - TZ=Asia/Shanghai 58 | - strategy_names="SMACross" 59 | # 当日分钟与实时分钟 60 | - redis_addr="172.19.129.98:16379" 61 | # 分钟数据,没配置zmq时使用 62 | - pg_min=postgresql://postgres:12345@hf_py_pg:5432/postgres 63 | # 策略信号入库使用 64 | - pg_order=postgresql://postgres:12345@hf_py_pg:5432/postgres 65 | volumes: 66 | # 个人策略文件夹 67 | - ./strategies:/hfpy/strategies 68 | ``` 69 | 70 | ## 策略信号 71 | ### 策略生成的信号会插件到postgres的public.strategy_sign中 72 | ```python 73 | js = json.dumps({ 74 | 'Direction': str(order.Direction).split('.')[1], 75 | 'Offset': str(order.Offset).split('.')[1], 76 | 'Price': round(order.Price, 4), 77 | 'Volume': order.Volume 78 | }) 79 | sql = f"""INSERT INTO public.strategy_sign 80 | (tradingday, order_time, instrument, "period", strategy_id, strategy_group, sign, remark, insert_time) 81 | VALUES('{data.Bars[-1].Tradingday}', '{stra.D[-1]}', '{data.Instrument}', {data.Interval}, '{stra.ID}', '{type(stra).__name__}', '{js}', '', now())""" 82 | ``` 83 | ### 实时信号会发布到redis 84 | ```python 85 | js = json.dumps({ 86 | 'Instrument': order.Instrument, 87 | 'Direction': str(order.Direction).split('.')[1], 88 | 'Offset': str(order.Offset).split('.')[1], 89 | 'Price': round(order.Price, 4), 90 | 'Volume': order.Volume, 91 | "ID": stra.ID * 1000 + len(stra.Orders) + 1 92 | }) 93 | self.cfg.rds.publish(f'order.{type(stra).__name__}', js) 94 | ``` 95 | 96 | ## 测试报告 97 | 因报告使用了pandas所以被注释掉了,如需要则可以自行安装pandas并注释掉atp.py的5行和252行。 98 | 99 | ## 策略配置 100 | - 与策略文件名同名的.yml文件 101 | - 配置参数组 102 | - 必须有ID标识(int) 103 | ```yml 104 | --- 105 | - 106 | # ID用于区分不同策略实例的委托不可重复 107 | "ID": 901 108 | # 回测开始日期 109 | "BeginDate": 20200101 110 | # 可通过增加Data实现多合约多周期引用 111 | "Datas": 112 | - 113 | # 合约/周期/周期数 114 | "Instrument": "ag2012" 115 | "IntervalType": "Minute" 116 | "Interval": 5 117 | "Params": 118 | # 突破轨道的长度 119 | "LENGTH1": 46 120 | "OPENPARAM": 0.54 121 | ``` 122 | 123 | ### 策略编写 124 | 125 | **策略文件名与类名、配置文件名要一致(区分大小写)** 126 | 127 | #### SMACross.py 128 | ```python 129 | #!/usr/bin/env python 130 | # -*- coding: utf-8 -*- 131 | """ 132 | __title__ = '' 133 | __author__ = 'HaiFeng' 134 | __mtime__ = '2016/8/16' 135 | """ 136 | # import talib._ta_lib as talib 137 | from hfpy.data import Data 138 | from hfpy.bar import Bar 139 | from hfpy.strategy import Strategy 140 | import numpy as np 141 | import talib as ta 142 | 143 | class SMACross(Strategy): 144 | 145 | def __init__(self, jsonfile): 146 | super().__init__(jsonfile) 147 | self.p_ma1 = self.Params['MA1'] 148 | self.p_ma2 = self.Params['MA2'] 149 | self.p_lots = self.Params['Lots'] 150 | 151 | def OnBarUpdate(self, data=Data, bar=Bar): 152 | if len(self.C) < self.p_ma2: 153 | return 154 | # if len(data.Instrument) > 0: 155 | # print(f'{data.Tick.Instrument},{data.Tick.Volume}') 156 | 157 | # print('{0}-{1}'.format(self.D[-1], self.C[-1])) 158 | ma1 = ta.SMA(np.array(self.C, dtype=float), self.p_ma1) 159 | ma2 = ta.SMA(np.array(self.C, dtype=float), self.p_ma2) 160 | 161 | self.IndexDict['ma5'] = ma1 162 | self.IndexDict['ma10'] = ma2 163 | 164 | if len(ma2) < 2 or len(ma1) < 2: 165 | return 166 | if self.PositionLong == 0: 167 | if ma1[-1] >= ma2[-1] and ma1[-2] < ma2[-2]: 168 | if self.PositionShort > 0: 169 | self.BuyToCover(self.O[-1], self.p_lots, '买平') 170 | self.Buy(self.O[-1], self.p_lots, '买开') 171 | elif self.PositionShort == 0: 172 | if ma1[-1] <= ma2[-1] and ma1[-2] > ma2[-2]: 173 | if self.PositionLong > 0: 174 | self.Sell(self.O[-1], self.p_lots, '卖平') 175 | self.SellShort(self.O[-1], self.p_lots, '卖开') 176 | ``` 177 | 178 | #### SMACross.yml 179 | 180 | ```yaml 181 | --- 182 | # ID用于区分不同策略实例的委托 183 | - 184 | ID: 119 185 | BeginDate: 20191101 186 | TickTest: false 187 | # 可通过增加Data实现多合约多周期引用 188 | Datas: 189 | - 190 | Instrument: p2105 191 | IntervalType: Minute 192 | Interval: 5 193 | - 194 | Instrument: rb2105 195 | IntervalType: Minute 196 | Interval: 5 197 | Params: 198 | Lots: 1 199 | MA1: 10 200 | MA2: 20 201 | - 202 | ID: 120 203 | BeginDate: 20180901 204 | Datas: 205 | - 206 | Instrument: rb2105 207 | IntervalType: Minute 208 | Interval: 5 209 | Params: 210 | Lots: 1 211 | MA1: 5 212 | MA2: 60 213 | ``` 214 | 215 | ## 附 216 | ### talib安装 217 | 报错:#include "Python.h 218 | 解决: 219 | apt: apt-get install python3-dev 220 | yum: yum install python3-devel 221 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyhamcrest 2 | Cython 3 | setuptools 4 | PyYAML 5 | pyzmq 6 | color_log 7 | 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # @Time : 2018/11/20 8:15 4 | # @Author : HaiFeng 5 | # @Email : 24918700@qq.com 6 | 7 | 8 | from setuptools import setup 9 | import os 10 | 11 | this_directory = os.path.abspath(os.path.dirname(__file__)) 12 | 13 | 14 | # 读取文件内容 15 | def read_file(filename): 16 | with open(os.path.join(this_directory, filename), encoding='utf-8') as f: 17 | desc = f.read() 18 | return desc 19 | 20 | 21 | # 获取依赖 22 | def read_requirements(filename): 23 | return [line.strip() for line in read_file(filename).splitlines() 24 | if not line.startswith('#')] 25 | 26 | 27 | long_description = read_file('readme.md') 28 | long_description_content_type = 'text/markdown' # 指定包文档格式为markdown 29 | 30 | # talib无需加入 os.system('pipreqs . --encoding=utf8 --force') # 生成 requirements.txt 31 | 32 | setup( 33 | name='hfpy', # 包名 34 | python_requires='>=3.6.0', # python环境 35 | version='0.2.2', # 包的版本 36 | description="Hai Feng Future Trading Platform with SE", # 包简介,显示在PyPI 37 | long_description=long_description, # 读取的Readme文档内容 38 | long_description_content_type=long_description_content_type, # 指定包文档格式为markdown 39 | author="HaiFeng", # 作者相关信息 40 | author_email='haifengat@vip.qq.com', 41 | url='https://github.com/haifengat/hf_at_py', 42 | # 指定包信息,还可以用find_packages()函数 43 | # packages=find_packages(), 44 | packages=['hfpy'], 45 | install_requires=read_requirements('requirements.txt'), # 指定需要安装的依赖 46 | include_package_data=True, 47 | license="MIT License", 48 | platforms="any", 49 | classifiers=[ 50 | "Programming Language :: Python :: 3.6", 51 | "Programming Language :: Python :: 3.7", 52 | "License :: OSI Approved :: MIT License", 53 | "Operating System :: OS Independent", 54 | ], 55 | ) 56 | -------------------------------------------------------------------------------- /strategies/SMACross.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | """ 4 | __title__ = '' 5 | __author__ = 'HaiFeng' 6 | __mtime__ = '2016/8/16' 7 | """ 8 | # import talib._ta_lib as talib 9 | from hfpy.data import Data 10 | from hfpy.bar import Bar 11 | from hfpy.strategy import Strategy 12 | import numpy as np 13 | import talib as ta 14 | 15 | class SMACross(Strategy): 16 | 17 | def __init__(self, jsonfile): 18 | super().__init__(jsonfile) 19 | self.p_ma1 = self.Params['MA1'] 20 | self.p_ma2 = self.Params['MA2'] 21 | self.p_lots = self.Params['Lots'] 22 | 23 | def OnBarUpdate(self, data=Data, bar=Bar): 24 | if len(self.C) < self.p_ma2: 25 | return 26 | # if len(data.Instrument) > 0: 27 | # print(f'{data.Tick.Instrument},{data.Tick.Volume}') 28 | 29 | # print('{0}-{1}'.format(self.D[-1], self.C[-1])) 30 | ma1 = ta.SMA(np.array(self.C, dtype=float), self.p_ma1) 31 | ma2 = ta.SMA(np.array(self.C, dtype=float), self.p_ma2) 32 | 33 | self.IndexDict['ma5'] = ma1 34 | self.IndexDict['ma10'] = ma2 35 | 36 | if len(ma2) < 2 or len(ma1) < 2: 37 | return 38 | if self.PositionLong == 0: 39 | if ma1[-1] >= ma2[-1] and ma1[-2] < ma2[-2]: 40 | if self.PositionShort > 0: 41 | self.BuyToCover(self.O[-1], self.p_lots, '买平') 42 | self.Buy(self.O[-1], self.p_lots, '买开') 43 | elif self.PositionShort == 0: 44 | if ma1[-1] <= ma2[-1] and ma1[-2] > ma2[-2]: 45 | if self.PositionLong > 0: 46 | self.Sell(self.O[-1], self.p_lots, '卖平') 47 | self.SellShort(self.O[-1], self.p_lots, '卖开') 48 | -------------------------------------------------------------------------------- /strategies/SMACross.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # ID用于区分不同策略实例的委托 3 | - 4 | ID: 119 5 | BeginDate: 20201001 6 | TickTest: false 7 | # 可通过增加Data实现多合约多周期引用 8 | Datas: 9 | - 10 | Instrument: p2101 11 | IntervalType: Minute 12 | Interval: 5 13 | - 14 | Instrument: rb2101 15 | IntervalType: Minute 16 | Interval: 5 17 | Params: 18 | Lots: 1 19 | MA1: 10 20 | MA2: 20 21 | - 22 | ID: 120 23 | BeginDate: 20180901 24 | Datas: 25 | - 26 | Instrument: rb2101 27 | IntervalType: Minute 28 | Interval: 5 29 | Params: 30 | Lots: 1 31 | MA1: 5 32 | MA2: 60 33 | -------------------------------------------------------------------------------- /ta-lib-0.4.0-src.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haifengat/hfpy/edbbefc7dd1d476ed7fd62ad9635888cfc5fcb44/ta-lib-0.4.0-src.tar.gz --------------------------------------------------------------------------------