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