├── .gitignore ├── .idea ├── .gitignore ├── UniversalRotation.iml ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── vcs.xml └── workspace.xml ├── LICENSE ├── README.md ├── UniversalRotation.py ├── UniversalRotation.xlsm └── res ├── LSJZ.png ├── author.png ├── download.png └── premium_rate.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | log* 132 | ~$UniversalRotation.xlsm 133 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | # /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/UniversalRotation.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 31 | 33 | { 34 | "associatedIndex": 5 35 | } 36 | 37 | 38 | 39 | 42 | { 43 | "keyToString": { 44 | "Python.UniversalRotation.executor": "Run", 45 | "RunOnceActivity.OpenProjectViewOnStart": "true", 46 | "RunOnceActivity.ShowReadmeOnStart": "true", 47 | "RunOnceActivity.git.unshallow": "true", 48 | "git-widget-placeholder": "<unknown>", 49 | "last_opened_file_path": "C:/Users/Administrator/Desktop/wgb/UniversalRotation/res", 50 | "settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable" 51 | } 52 | } 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 82 | 83 | 84 | 85 | 86 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 1652152425490 95 | 99 | 100 | 101 | 102 | 111 | 112 | -------------------------------------------------------------------------------- /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, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 原贴地址:https://www.jisilu.cn/question/457389 2 | 3 | 下载源码的方式:点击GitHub网页上的Code按钮,选择自己喜欢的下载方式即可: 4 | ![](res/download.png) 5 | 6 | ### 联系作者 7 | 如果想讨论该tool的用法或者新需求,以及量化轮动策略等相关的内容,可以关注公众号“多策略再平衡小杠杆对冲”(wangbaobaoyzy),共同学习和进步! 8 | 9 | ![](res/author.png) 10 | 11 | ### 使用前准备 12 | 1. 安装Python3:https://www.python.org/ftp/python/3.8.7/python-3.8.7-amd64.exe 13 | 2. 打开cmd窗口输入:pip install schedule xlwings pandas requests pysnowball browser-cookie3 chinese_calendar 14 | 3. 启用Microsoft Office(不要使用WPS)的Excel中的xlwings宏: 15 | (1)、命令行安装加载项:xlwings addin install。 16 | (2)、在excel中启用加载项: 文件>选项>信任中心>信任中心设置>宏设置 中,选择“启用所有并勾选”并勾选“对VBA对象模型的信任访问”。 17 | 4. 设置Firefox(强烈推荐)或Chrome(我用的版本是108.0.5359.125,建议使用最新版,因为太多网友因为版本太低导致无法使用browser-cookie3)或Opera或Edge或Chromium为默认浏览器,使用他们的任意一个访问任意一个雪球网站以便获取token(不需要登录雪球账号)。 18 | 19 | 注1:如遇到如下报错就代表token过期了: 20 | Exception: b'{"error_description":"\xe9\x81\x87","error_uri":"/v5/stock/quote.json","error_data":null,"error_code":"400016"}'。 21 | 由于我们在Python调用雪球API前需要设置xq_a_token,但它大约只有20天的有效期,之前都是使用F12各种查找xq_a_token然后复制粘贴到我们的Python程序, 22 | 比较繁琐,所以现在改用browser-cookie3工具库来自动化获取电脑浏览器已缓存的cookies(当然啦,浏览器的cookies肯定也会过期,我们只需要坚持每20天左右使用电脑浏览器访问任意一个雪球网站即可刷新电脑浏览器的cookies)。 23 | 但是我发现网友经常遇到Chrome浏览器无法使用browser-cookie3的问题,是因为Chrome浏览器的版本太低导致的。所以建议大家使用Firefox为默认浏览器或者更新到最新版Chrome浏览器。 24 | 25 | 注2:如遇到如下报错就代表chinese_calendar过期了: 26 | no available data for year 2023, only year between [2004, 2022] supported. 27 | 需要升级:pip install -U chinesecalendar 28 | 29 | 注3:作为一个Android程序员,从2022.2月份开始边学边练第一次写Python项目进行量化投资,语法格式肯定不完美,勿喷,仅仅是为了解放调仓的苦恼而写的小玩意,分享出来仅用于学习研究,不可用于商业用途! 30 | 31 | ### (一)、使用Python+Excel可视化你的持仓和最新排名的差异,解决买什么卖什么的烦恼 32 | 1. 这里以"低溢价可转债策略"( https://xueqiu.com/P/ZH3128720 )、"双低可转债策略"( https://xueqiu.com/P/ZH2982722 )为例子,我做了2个轮动的sheet表格:《低溢价可转债轮动》和《双低可转债轮动》。 33 | 2. 点击《可转债实时数据》里面的“点击更新”按钮。 34 | 3. 分别点击《低溢价可转债轮动》和《双低可转债轮动》里面的“点击更新”按钮,如果有其他任何自定义排名也可以手动更新”最新多因子1/2可转债排名“。 35 | 4. 从券商PC端软件导出最新的低溢价可转债持仓和双低可转债持仓的Excel表格,并分别更新到“我的低溢价可转债持仓”、“我的双低可转债持仓”,这时候就可以看到需要轮动的结果了。 36 | 37 | 注3:这里依赖的是Excel自带的各种公式,忽然发现Excel解放手脚的强大了吧?神不神奇惊不惊喜,意不意外? 38 | 39 | 注4:每个sheet页我都做了双排名(即自定义的”最新多因子1/2可转债排名“),满足用户自定义的需求。 40 | 41 | 注5:这里的2个sheet页仅是2个可转债的例子,填充了持仓和最新排名。用户可以将它应用于任何的“依据各种因子进行排名的量化轮动策略”。 42 | 大家可以将这两个sheet作为壳子,替换成任何你们自己的轮动品种,比如小市值低价股票策略、低估股票策略、A/H轮动等等。 43 | 44 | 45 | ### (二)、关于《20天净值增长率和溢价率轮动LOF、ETF和封基》和《20天净值增长率和溢价率轮动债券和境外基金》 46 | 1. 点击“点击更新”按钮,即可更新这2个策略(https://xueqiu.com/P/ZH3040077 和 https://xueqiu.com/P/ZH3044957 )的最新的排名数据。 47 | 2. 从券商PC端软件导出我的持仓放在左侧列表,即可看到目前持仓排名了。 -------------------------------------------------------------------------------- /UniversalRotation.py: -------------------------------------------------------------------------------- 1 | import os, random, time, datetime, schedule, webbrowser 2 | import xlwings 3 | import pandas 4 | import requests 5 | import pysnowball 6 | import browser_cookie3 7 | from chinese_calendar import is_workday 8 | 9 | from requests.packages.urllib3.exceptions import InsecureRequestWarning 10 | requests.packages.urllib3.disable_warnings(InsecureRequestWarning) 11 | 12 | 13 | @xlwings.func 14 | def get_fund_net_asset_value_history(fund_code: str, total_pages: int = 38, page_sizes: int = 20) -> pandas.DataFrame: 15 | ''' 16 | 根据基金代码、获取的总页数和每页数据条数来抓取基金净值信息 17 | 18 | Parameters 19 | ---------- 20 | fund_code : 6位基金代码 21 | total_pages : 总页数,用于确定需要抓取的页码范围 22 | page_sizes: 每页获取的数据条数 23 | 24 | Return 25 | ------ 26 | DataFrame : 包含基金历史净值数据 27 | ''' 28 | # 请求头 29 | EastmoneyFundHeaders = { 30 | 'Accept': '*/*', 31 | 'Accept-Encoding': 'gzip, deflate', 32 | 'Accept-Language': 'zh-CN,zh;q=0.9', 33 | # 'Connection': 'keep-alive', 34 | 'Host': 'api.fund.eastmoney.com', 35 | 'Referer': 'http://fundf10.eastmoney.com/', 36 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36', 37 | } 38 | 39 | url = 'https://api.fund.eastmoney.com/f10/lsjz' 40 | all_rows = [] 41 | columns = ['日期', '单位净值', '累计净值', '涨跌幅'] 42 | 43 | # 设置重连次数 44 | requests.adapters.DEFAULT_RETRIES = 10 45 | session1 = requests.session() 46 | session1.keep_alive = False 47 | 48 | for page in range(1, total_pages + 1): 49 | Eastmoneyparams = { 50 | 'fundCode': fund_code, 51 | 'pageIndex': str(page), 52 | 'pageSize': f'{page_sizes}', 53 | 'startDate': '', 54 | 'endDate': '', 55 | } 56 | 57 | response1 = session1.get(url, headers=EastmoneyFundHeaders, params=Eastmoneyparams, verify=False, stream=False, 58 | timeout=10) 59 | json_response = response1.json() 60 | response1.close() 61 | del(response1) 62 | 63 | if json_response is None: 64 | return pandas.DataFrame(all_rows, columns=columns) 65 | datas = json_response.get('Data').get('LSJZList') 66 | 67 | for stock in datas: 68 | all_rows.append({ 69 | '日期': stock['FSRQ'], 70 | '单位净值': stock['DWJZ'], 71 | '累计净值': stock['LJJZ'], 72 | '涨跌幅': stock['JZZZL'] 73 | }) 74 | 75 | df = pandas.DataFrame(all_rows) 76 | df['日期'] = pandas.to_datetime(df['日期'], errors='coerce') 77 | df['单位净值'] = pandas.to_numeric(df['单位净值'], errors='coerce') 78 | df['累计净值'] = pandas.to_numeric(df['累计净值'], errors='coerce') 79 | df['涨跌幅'] = pandas.to_numeric(df['涨跌幅'], errors='coerce') 80 | 81 | return df 82 | 83 | 84 | @xlwings.func 85 | # 获取基金的市价、溢价率、成交量 86 | def get_fund_premium_rate_and_amount(fund_code: str): 87 | detail = pandas.DataFrame(pysnowball.quote_detail(fund_code)) 88 | row1=detail.loc["quote"].iloc[0] 89 | premium_rate1 = row1["premium_rate"] 90 | current_price = row1["current"] 91 | amount1 = row1["amount"] 92 | amount1 = amount1/10000 if amount1 else 0 93 | return current_price, premium_rate1, amount1 94 | 95 | @xlwings.func 96 | # 获取xq_a_token 97 | def get_xq_a_token(): 98 | str_xq_a_token = ';' 99 | while True: 100 | cj = browser_cookie3.load() 101 | for item in cj: 102 | if item.name == "xq_a_token" : 103 | print('get token, %s = %s' % (item.name, item.value)) 104 | str_xq_a_token = 'xq_a_token=' + item.value + ';' 105 | return str_xq_a_token 106 | if str_xq_a_token == ";" : 107 | print('get token, retrying ......') 108 | webbrowser.open("https://xueqiu.com/") 109 | time.sleep(1) 110 | 111 | @xlwings.func 112 | # 根据排名计算排名分(精确到小数点后2位) 113 | def get_rank_value(length: int, rank: int): 114 | return round((length-rank)/length*100, 2) 115 | 116 | 117 | @xlwings.func 118 | def rotate_fund_by_premium_rate_and_20net_asset_value(source_sheets: str, dest_range: str, delay: int): 119 | ''' 120 | 根据20天净值增长率和溢价率来量化轮动基金 121 | Parameters 122 | ---------- 123 | source_sheets : Excel里sheet名称 124 | dest_range : 写回Excel的区域 125 | delay : 延时 126 | ''' 127 | xlwings.Book("UniversalRotation.xlsm").set_mock_caller() 128 | wb = xlwings.Book.caller() # wb = xlwings.Book(r'UniversalRotation.xlsm') 129 | pandas.options.display.max_columns = None 130 | pandas.options.display.max_rows = None 131 | pysnowball.set_token(get_xq_a_token()) 132 | 133 | sheet_fund = wb.sheets[source_sheets] 134 | # 数据表范围请参考Excel里的"最新数据"区域 135 | source_range = 'H2:Y' + str(sheet_fund.used_range.last_cell.row) 136 | print('数据表范围:' + source_range) 137 | data_fund = pandas.DataFrame(sheet_fund.range(source_range).value, 138 | columns=['基金代码','基金名称','投资种类','溢价率','5天净值增长率','10天净值增长率', 139 | '20天净值增长率','60天净值增长率','120天净值增长率','250天净值增长率', 140 | '500天净值增长率','750天净值增长率','总排名分','20天净值增长率排名分', 141 | '溢价率排名分','成交额(万元)','Mark','持有周期']) 142 | refresh_time = str(time.strftime("%Y-%m-%d__%H.%M.%S", time.localtime())) 143 | sheet_fund.range('AA6').value = '更新时间:' + refresh_time 144 | log_file = open('log-' + source_sheets + refresh_time + '.txt', 'a+') 145 | 146 | for i,fund_code in enumerate(data_fund['基金代码']): 147 | # 延时 148 | if (i % 10 == 0) : 149 | time.sleep(random.randrange(delay)) 150 | 151 | netAssetValueRaw = get_fund_net_asset_value_history(fund_code[2:8]) 152 | # 最新净值以及5个交易日前、10个交易日前、20个交易日前、60个交易日前、120个交易日前、250个交易日前、500个交易日前、750个交易日前的累计净值 153 | netAssetDate1 = str(netAssetValueRaw.loc[0]['日期'])[0:10] 154 | netAssetLJValue1 = netAssetValueRaw.loc[0]['累计净值'] 155 | netAssetDate20 = str(netAssetValueRaw.loc[len(netAssetValueRaw)-1]['日期'])[0:10] 156 | netAssetLJValue5 = netAssetLJValue10 = netAssetLJValue20 = netAssetLJValue60 = netAssetLJValue120 \ 157 | = netAssetLJValue250 = netAssetLJValue500 \ 158 | = netAssetLJValue750 = netAssetValueRaw.loc[len(netAssetValueRaw)-1]['累计净值'] 159 | if (len(netAssetValueRaw) > 20) : 160 | netAssetLJValue5 = netAssetValueRaw.loc[5]['累计净值'] 161 | netAssetLJValue10 = netAssetValueRaw.loc[10]['累计净值'] 162 | netAssetDate20 = str(netAssetValueRaw.loc[20]['日期'])[0:10] 163 | netAssetLJValue20 = netAssetValueRaw.loc[20]['累计净值'] 164 | if (len(netAssetValueRaw) > 60) : 165 | netAssetLJValue60 = netAssetValueRaw.loc[60]['累计净值'] 166 | if (len(netAssetValueRaw) > 120) : 167 | netAssetLJValue120 = netAssetValueRaw.loc[120]['累计净值'] 168 | if (len(netAssetValueRaw) > 250) : 169 | netAssetLJValue250 = netAssetValueRaw.loc[250]['累计净值'] 170 | if (len(netAssetValueRaw) > 500) : 171 | netAssetLJValue500 = netAssetValueRaw.loc[500]['累计净值'] 172 | if (len(netAssetValueRaw) > 750) : 173 | netAssetLJValue750 = netAssetValueRaw.loc[750]['累计净值'] 174 | # 更新5天、10天、20天、60天、120天、250天、500天、750天的累计净值增长率 175 | netAssetLJValue5Rate = round((netAssetLJValue1 - netAssetLJValue5) / netAssetLJValue5, 4) 176 | data_fund.loc[i, '5天净值增长率'] = netAssetLJValue5Rate 177 | netAssetLJValue10Rate = round((netAssetLJValue1 - netAssetLJValue10) / netAssetLJValue10, 4) 178 | data_fund.loc[i, '10天净值增长率'] = netAssetLJValue10Rate 179 | netAssetLJValue20Rate = round((netAssetLJValue1 - netAssetLJValue20) / netAssetLJValue20, 4) 180 | data_fund.loc[i, '20天净值增长率'] = netAssetLJValue20Rate 181 | data_fund.loc[i, '60天净值增长率'] = round((netAssetLJValue1 - netAssetLJValue60) / netAssetLJValue60, 4) 182 | data_fund.loc[i, '120天净值增长率'] = round((netAssetLJValue1 - netAssetLJValue120) / netAssetLJValue120, 4) 183 | data_fund.loc[i, '250天净值增长率'] = round((netAssetLJValue1 - netAssetLJValue250) / netAssetLJValue250, 4) 184 | data_fund.loc[i, '500天净值增长率'] = round((netAssetLJValue1 - netAssetLJValue500) / netAssetLJValue500, 4) 185 | data_fund.loc[i, '750天净值增长率'] = round((netAssetLJValue1 - netAssetLJValue750) / netAssetLJValue750, 4) 186 | 187 | # 更新基金的市价、溢价率、成交量 188 | current_price,fundPremiumRateValue,fundAmount = get_fund_premium_rate_and_amount(fund_code) 189 | netAssetDWValue1 = netAssetValueRaw.loc[0]['单位净值'] 190 | if (current_price) : 191 | fundPremiumRateValue = round((current_price - netAssetDWValue1) / netAssetDWValue1, 4) 192 | data_fund.loc[i, '溢价率'] = fundPremiumRateValue 193 | data_fund.loc[i, '成交额(万元)'] = fundAmount 194 | 195 | log_str = 'No.' + format(str(i), "<6") + fund_code + ':'+ format(data_fund.loc[i, '基金名称'], "<15") \ 196 | + netAssetDate1 + ':净值:' + format(str(netAssetLJValue1), "<10") \ 197 | + netAssetDate20 + ':净值:' + format(str(netAssetLJValue20), "<10") \ 198 | + '二十个交易日净值增长率:' + format(str(netAssetLJValue20Rate), "<10") \ 199 | + '溢价率:'+ format(str(fundPremiumRateValue), "<10") 200 | print(log_str) 201 | print(log_str, file=log_file) 202 | 203 | # 更新'溢价率排名分' 204 | data_fund = data_fund.sort_values(by='溢价率', ascending=True) 205 | data_fund.reset_index(drop=True, inplace=True) 206 | for i,fundPremiumRateValueItem in enumerate(data_fund['溢价率']): 207 | data_fund.loc[i, '溢价率排名分'] = get_rank_value(len(data_fund), i) 208 | 209 | # 更新'20天净值增长率排名分' 210 | data_fund = data_fund.sort_values(by='20天净值增长率', ascending=False) 211 | data_fund.reset_index(drop=True, inplace=True) 212 | for i,netAssetLJValue20RateItem in enumerate(data_fund['20天净值增长率']): 213 | data_fund.loc[i, '20天净值增长率排名分'] = get_rank_value(len(data_fund), i) 214 | 215 | # 计算'总排名分' 216 | for i,sumValue in enumerate(data_fund['总排名分']): 217 | data_fund.loc[i, '总排名分'] = data_fund.loc[i, '溢价率排名分'] + data_fund.loc[i, '20天净值增长率排名分'] 218 | data_fund = data_fund.sort_values(by='总排名分', ascending=False) 219 | data_fund.reset_index(drop=True, inplace=True) 220 | # 根据排名更新'总排名分' 221 | for i,fundPremiumRateValue in enumerate(data_fund['总排名分']): 222 | data_fund.loc[i, '总排名分'] = get_rank_value(len(data_fund), i) 223 | # 将数据起始下标从1开始计数 224 | data_fund.index += 1 225 | print(data_fund) 226 | print(data_fund, file=log_file) 227 | 228 | log_file.close() 229 | 230 | # 更新数据到原Excel 231 | sheet_fund.range(dest_range).value = data_fund 232 | wb.save() 233 | 234 | @xlwings.func 235 | # 轮动20天净值增长和溢价率选LOF、ETF和封基 236 | def rotate_LOF_ETF(): 237 | print("------------------------20天净值增长率和溢价率轮动LOF、ETF和封基-------------------------------------------------") 238 | rotate_fund_by_premium_rate_and_20net_asset_value('20天净值增长率和溢价率轮动LOF、ETF和封基','G1', 10) 239 | 240 | @xlwings.func 241 | # 轮动20天净增和溢价率选债券和境外基金 242 | def rotate_abroad_fund(): 243 | print("-----------------------20天净值增长率和溢价率轮动债券和境外基金---------------------------------------------------") 244 | rotate_fund_by_premium_rate_and_20net_asset_value('20天净值增长率和溢价率轮动债券和境外基金', 'G1', 10) 245 | 246 | source_range_convertible_bond = 'B8:T' 247 | 248 | # 获取可转债实时数据列表上方的多因子策略的阈值上限和权重 249 | def get_convertible_bond_factor(factor:str): 250 | factor = factor.split(',', -1) 251 | return float(factor[0]), float(factor[1]) 252 | 253 | @xlwings.func 254 | # 更新可转债实时数据:价格、涨跌幅、转股价、转股价值、溢价率、双低值、到期时间、剩余年限、剩余规模、成交金额、换手率、税前收益、振幅等 255 | def refresh_convertible_bond(): 256 | print("更新可转债实时数据:价格、涨跌幅、转股价、转股价值、溢价率、双低值、到期时间、剩余年限、剩余规模、成交金额、换手率、税前收益、振幅等") 257 | xlwings.Book("UniversalRotation.xlsm").set_mock_caller() 258 | wb = xlwings.Book.caller() 259 | pandas.options.display.max_columns = None 260 | pandas.options.display.max_rows = None 261 | pysnowball.set_token(get_xq_a_token()) 262 | 263 | source_sheets = '可转债实时数据' 264 | sheet_fund = wb.sheets[source_sheets] 265 | source_range = source_range_convertible_bond + str(sheet_fund.used_range.last_cell.row) 266 | print('数据表范围:' + source_range) 267 | data_fund = pandas.DataFrame(sheet_fund.range(source_range).value, 268 | columns=['转债代码','转债名称','当前价','涨跌幅','转股价','转股价值','溢价率','双低值', 269 | '到期时间','剩余年限','剩余规模','成交金额','换手率','税前收益','最高价','最低价', 270 | '振幅','多因子1排名','多因子2排名']) 271 | refresh_time = str(time.strftime("%Y-%m-%d__%H.%M.%S", time.localtime())) 272 | sheet_fund.range('W12').value = '更新时间:' + refresh_time 273 | log_file = open('log-' + source_sheets + refresh_time + '.txt', 'a+') 274 | 275 | for i,fund_code in enumerate(data_fund['转债代码']): 276 | if str(fund_code).startswith('11') or str(fund_code).startswith('13'): 277 | fund_code_str = ('SH' + str(fund_code))[0:8] 278 | elif str(fund_code).startswith('12'): 279 | fund_code_str = ('SZ' + str(fund_code))[0:8] 280 | detail = pandas.DataFrame(pysnowball.quote_detail(fund_code_str)) 281 | row1 = detail.loc["quote"].iloc[0] 282 | data_fund.loc[i, '当前价'] = row1["current"] 283 | data_fund.loc[i, '涨跌幅'] = row1["percent"] / 100 if row1["percent"] != None else '停牌' 284 | data_fund.loc[i, '转股价'] = row1["conversion_price"] 285 | data_fund.loc[i, '转股价值'] = row1["conversion_value"] 286 | data_fund.loc[i, '溢价率'] = row1["premium_rate"] / 100 if row1["premium_rate"] != None else '停牌' 287 | data_fund.loc[i, '双低值'] = row1["current"] + row1["premium_rate"] 288 | data_fund.loc[i, '到期时间'] = str(time.strftime("%Y-%m-%d", time.localtime(row1["maturity_date"]/1000))) 289 | data_fund.loc[i, '剩余年限'] = row1["remain_year"] 290 | data_fund.loc[i, '剩余规模'] = row1["outstanding_amt"] / 100000000 if row1["outstanding_amt"] != None else 1 291 | data_fund.loc[i, '成交金额'] = row1["amount"] / 10000 if row1["amount"] != None else 0 292 | data_fund.loc[i, '换手率'] = (data_fund.loc[i, '成交金额'] / 10000 / row1["current"]) / (data_fund.loc[i, '剩余规模'] / 100) 293 | data_fund.loc[i, '税前收益'] = row1["benefit_before_tax"] / 100 if row1["benefit_before_tax"] != None else '停牌' 294 | data_fund.loc[i, '最高价'] = row1["high"] 295 | data_fund.loc[i, '最低价'] = row1["low"] 296 | if row1["high"] and row1["low"]: 297 | data_fund.loc[i, '振幅'] = (row1["high"] - row1["low"]) / row1["low"] 298 | else: 299 | data_fund.loc[i, '振幅'] = '停牌' 300 | log_str = 'No.' + format(str(i), "<6") + format(str(fund_code_str), "<10") \ 301 | + format(data_fund.loc[i, '转债名称'], "<15") \ 302 | + '当前价:' + format(str(row1["current"]), "<10") \ 303 | + '溢价率:' + format(str(row1["premium_rate"]), "<10") \ 304 | + '涨跌幅:'+ format(str(row1["percent"]), "<10") 305 | print(log_str) 306 | print(log_str, file=log_file) 307 | 308 | data_fund = data_fund.sort_values(by='溢价率') 309 | data_fund.reset_index(drop=True, inplace=True) 310 | data_fund.index += 1 311 | print(data_fund) 312 | print(data_fund, file=log_file) 313 | 314 | log_file.close() 315 | # 更新原Excel 316 | sheet_fund.range('A7').value = data_fund 317 | wb.save() 318 | 319 | @xlwings.func 320 | # 更新低溢价可转债数据 321 | def refresh_premium_rate_convertible_bond(): 322 | print("--------------------------------------更新低溢价可转债数据----------------------------------------------------") 323 | xlwings.Book("UniversalRotation.xlsm").set_mock_caller() 324 | wb = xlwings.Book.caller() 325 | pandas.options.display.max_columns = None 326 | pandas.options.display.max_rows = None 327 | 328 | sheet_src = wb.sheets['可转债实时数据'] 329 | source_range = source_range_convertible_bond + str(sheet_src.used_range.last_cell.row) 330 | data_fund_source = pandas.DataFrame(sheet_src.range(source_range).value, 331 | columns=['转债代码','转债名称','当前价','涨跌幅','转股价','转股价值','溢价率','双低值', 332 | '到期时间','剩余年限','剩余规模','成交金额','换手率','税前收益','最高价','最低价', 333 | '振幅','多因子1排名','多因子2排名']) 334 | data_fund_destination = data_fund_source[['转债代码','转债名称','当前价','溢价率','剩余规模']] 335 | data_fund_destination = data_fund_destination[(data_fund_destination['当前价'] < sheet_src.range('D2').value) & 336 | (data_fund_destination['溢价率'] < sheet_src.range('H2').value) & 337 | (data_fund_destination['剩余规模'] < sheet_src.range('L2').value)] 338 | data_fund_destination = data_fund_destination.sort_values(by='溢价率') 339 | data_fund_destination.reset_index(drop=True, inplace=True) 340 | data_fund_destination.index += 1 341 | print(data_fund_destination) 342 | 343 | # 更新低溢价可转债轮动sheet 344 | sheet_dest = wb.sheets['低溢价可转债轮动'] 345 | sheet_dest.range('H2').value = data_fund_destination[:50] 346 | wb.save() 347 | 348 | @xlwings.func 349 | # 更新双低可转债数据 350 | def refresh_price_and_premium_rate_convertible_bond(): 351 | print("----------------------------------------更新双低可转债数据----------------------------------------------------") 352 | xlwings.Book("UniversalRotation.xlsm").set_mock_caller() 353 | wb = xlwings.Book.caller() 354 | pandas.options.display.max_columns = None 355 | pandas.options.display.max_rows = None 356 | 357 | sheet_src = wb.sheets['可转债实时数据'] 358 | source_range = source_range_convertible_bond + str(sheet_src.used_range.last_cell.row) 359 | data_fund_source = pandas.DataFrame(sheet_src.range(source_range).value, 360 | columns=['转债代码','转债名称','当前价','涨跌幅','转股价','转股价值','溢价率','双低值', 361 | '到期时间','剩余年限','剩余规模','成交金额','换手率','税前收益','最高价','最低价', 362 | '振幅','多因子1排名','多因子2排名']) 363 | data_fund_destination = data_fund_source[['转债代码','转债名称','当前价','溢价率','双低值','剩余规模']] 364 | data_fund_destination = data_fund_destination[(data_fund_destination['当前价'] < sheet_src.range('D3').value) & 365 | (data_fund_destination['溢价率'] < sheet_src.range('H3').value) & 366 | (data_fund_destination['剩余规模'] < sheet_src.range('L3').value)] 367 | data_fund_destination = data_fund_destination.sort_values(by='双低值') 368 | data_fund_destination.reset_index(drop=True, inplace=True) 369 | data_fund_destination.index +=1 370 | print(data_fund_destination) 371 | 372 | # 更新双低可转债轮动sheet 373 | sheet_dest = wb.sheets['双低可转债轮动'] 374 | sheet_dest.range('H2').value = data_fund_destination[:50] 375 | wb.save() 376 | 377 | @xlwings.func 378 | # 更新多因子1可转债数据 379 | def refresh_multifactor1_convertible_bond(): 380 | print("--------------------------------------更新多因子1可转债数据----------------------------------------------------") 381 | xlwings.Book("UniversalRotation.xlsm").set_mock_caller() 382 | wb = xlwings.Book.caller() 383 | pandas.options.display.max_columns = None 384 | pandas.options.display.max_rows = None 385 | 386 | sheet_src = wb.sheets['可转债实时数据'] 387 | source_range = source_range_convertible_bond + str(sheet_src.used_range.last_cell.row) 388 | data_fund_source = pandas.DataFrame(sheet_src.range(source_range).value, 389 | columns=['转债代码','转债名称','当前价','涨跌幅','转股价','转股价值','溢价率','双低值', 390 | '到期时间','剩余年限','剩余规模','成交金额','换手率','税前收益','最高价','最低价', 391 | '振幅','多因子1排名','多因子2排名']) 392 | data_fund_destination = data_fund_source[['转债代码','转债名称','当前价','溢价率','剩余规模']] 393 | threshold_current_price, weight_current_price = get_convertible_bond_factor(sheet_src.range('D5').value) 394 | threshold_premium_rate, weight_premium_rate = get_convertible_bond_factor(sheet_src.range('H5').value) 395 | threshold_outstanding_amt, weight_outstanding_amt = get_convertible_bond_factor(sheet_src.range('L5').value) 396 | 397 | # for i, fund_code in enumerate(data_fund_source['转债代码']): 398 | # data_fund_source.loc[i, '多因子1排名'] = data_fund_source.loc[i, '当前价'] * weight_current_price 399 | # + data_fund_source.loc[i, '溢价率'] * weight_premium_rate 400 | # + data_fund_source.loc[i, '剩余规模'] * weight_outstanding_amt 401 | 402 | data_fund_destination = data_fund_destination[(data_fund_destination['当前价'] < threshold_current_price) & 403 | (data_fund_destination['溢价率'] < threshold_premium_rate) & 404 | (data_fund_destination['剩余规模'] < threshold_outstanding_amt)] 405 | data_fund_destination = data_fund_destination.sort_values(by='溢价率') 406 | data_fund_destination.reset_index(drop=True, inplace=True) 407 | data_fund_destination.index += 1 408 | print(data_fund_destination) 409 | 410 | # 更新低溢价可转债轮动sheet 411 | sheet_dest = wb.sheets['低溢价可转债轮动'] 412 | sheet_dest.range('Q2').value = data_fund_destination[:50] 413 | wb.save() 414 | 415 | @xlwings.func 416 | # 更新多因子2可转债数据 417 | def refresh_multifactor2_convertible_bond(): 418 | print("----------------------------------------更新多因子2可转债数据----------------------------------------------------") 419 | xlwings.Book("UniversalRotation.xlsm").set_mock_caller() 420 | wb = xlwings.Book.caller() 421 | pandas.options.display.max_columns = None 422 | pandas.options.display.max_rows = None 423 | 424 | sheet_src = wb.sheets['可转债实时数据'] 425 | source_range = source_range_convertible_bond + str(sheet_src.used_range.last_cell.row) 426 | data_fund_source = pandas.DataFrame(sheet_src.range(source_range).value, 427 | columns=['转债代码','转债名称','当前价','涨跌幅','转股价','转股价值','溢价率','双低值', 428 | '到期时间','剩余年限','剩余规模','成交金额','换手率','税前收益','最高价','最低价', 429 | '振幅','多因子1排名','多因子2排名']) 430 | data_fund_destination = data_fund_source[['转债代码','转债名称','当前价','溢价率','剩余规模']] 431 | threshold_current_price, weight_current_price = get_convertible_bond_factor(sheet_src.range('D6').value) 432 | threshold_premium_rate, weight_premium_rate = get_convertible_bond_factor(sheet_src.range('H6').value) 433 | threshold_outstanding_amt, weight_outstanding_amt = get_convertible_bond_factor(sheet_src.range('L6').value) 434 | data_fund_destination = data_fund_destination[(data_fund_destination['当前价'] < threshold_current_price) & 435 | (data_fund_destination['溢价率'] < threshold_premium_rate) & 436 | (data_fund_destination['剩余规模'] < threshold_outstanding_amt)] 437 | data_fund_destination = data_fund_destination.sort_values(by='溢价率') 438 | data_fund_destination.reset_index(drop=True, inplace=True) 439 | data_fund_destination.index += 1 440 | print(data_fund_destination) 441 | 442 | # 更新双低可转债轮动sheet 443 | sheet_dest = wb.sheets['双低可转债轮动'] 444 | sheet_dest.range('R2').value = data_fund_destination[:50] 445 | wb.save() 446 | 447 | @xlwings.func 448 | # 更新股票实时数据:价格、涨跌幅等 449 | def refresh_stock(): 450 | print("更新股票实时数据:价格、涨跌幅等") 451 | xlwings.Book("UniversalRotation.xlsm").set_mock_caller() 452 | wb = xlwings.Book.caller() 453 | pandas.options.display.max_columns = None 454 | pandas.options.display.max_rows = None 455 | pysnowball.set_token(get_xq_a_token()) 456 | 457 | source_sheets = '股票实时数据' 458 | sheet_fund = wb.sheets[source_sheets] 459 | source_range = 'B2:E' + str(sheet_fund.used_range.last_cell.row) 460 | print('数据表范围:' + source_range) 461 | data_fund = pandas.DataFrame(sheet_fund.range(source_range).value, 462 | columns=['股票代码','股票名称','当前价','涨跌幅']) 463 | refresh_time = str(time.strftime("%Y-%m-%d__%H.%M.%S", time.localtime())) 464 | sheet_fund.range('W3').value = '更新时间:' + refresh_time 465 | log_file = open('log-' + source_sheets + refresh_time + '.txt', 'a+') 466 | 467 | for i,fund_code in enumerate(data_fund['股票代码']): 468 | detail = pandas.DataFrame(pysnowball.quote_detail(fund_code)) 469 | row1 = detail.loc["quote"].iloc[0] 470 | print(str(row1)) 471 | # data_fund.loc[i, '当前价'] = row1["current"] 472 | # data_fund.loc[i, '涨跌幅'] = row1["percent"] / 100 if row1["percent"] != None else '停牌' 473 | # data_fund.loc[i, '转股价'] = row1["conversion_price"] 474 | # data_fund.loc[i, '转股价值'] = row1["conversion_value"] 475 | # data_fund.loc[i, '溢价率'] = row1["premium_rate"] / 100 if row1["premium_rate"] != None else '停牌' 476 | # data_fund.loc[i, '双低值'] = row1["current"] + row1["premium_rate"] 477 | # data_fund.loc[i, '到期时间'] = str(time.strftime("%Y-%m-%d", time.localtime(row1["maturity_date"]/1000))) 478 | # data_fund.loc[i, '剩余年限'] = row1["remain_year"] 479 | # data_fund.loc[i, '剩余规模'] = row1["outstanding_amt"] / 100000000 if row1["outstanding_amt"] != None else 1 480 | # data_fund.loc[i, '成交金额'] = row1["amount"] / 10000 if row1["amount"] != None else 0 481 | # data_fund.loc[i, '换手率'] = (data_fund.loc[i, '成交金额'] / 10000 / row1["current"]) / (data_fund.loc[i, '剩余规模'] / 100) 482 | # data_fund.loc[i, '税前收益'] = row1["benefit_before_tax"] / 100 if row1["benefit_before_tax"] != None else '停牌' 483 | # data_fund.loc[i, '最高价'] = row1["high"] 484 | # data_fund.loc[i, '最低价'] = row1["low"] 485 | # if row1["high"] and row1["low"]: 486 | # data_fund.loc[i, '振幅'] = (row1["high"] - row1["low"]) / row1["low"] 487 | # else: 488 | # data_fund.loc[i, '振幅'] = '停牌' 489 | # log_str = 'No.' + format(str(i), "<6") + format(str(fund_code_str), "<10") \ 490 | # + format(data_fund.loc[i, '转债名称'], "<15") \ 491 | # + '当前价:' + format(str(row1["current"]), "<10") \ 492 | # + '溢价率:' + format(str(row1["premium_rate"]), "<10") \ 493 | # + '涨跌幅:'+ format(str(row1["percent"]), "<10") 494 | # print(log_str) 495 | # print(log_str, file=log_file) 496 | # 497 | # data_fund = data_fund.sort_values(by='溢价率') 498 | # data_fund.reset_index(drop=True, inplace=True) 499 | # data_fund.index += 1 500 | # print(data_fund) 501 | # print(data_fund, file=log_file) 502 | 503 | log_file.close() 504 | # 更新原Excel 505 | sheet_fund.range('A1').value = data_fund 506 | wb.save() 507 | 508 | def main_function(): 509 | # date = datetime.datetime.now().date() 510 | # if not is_workday(date): 511 | # return 512 | webbrowser.open("https://xueqiu.com/") 513 | 514 | #删除旧log 515 | for eachfile in os.listdir('./'): 516 | filename = os.path.join('./', eachfile) 517 | if os.path.isfile(filename) and filename.startswith("./log") : 518 | os.remove(filename) 519 | 520 | rotate_LOF_ETF() 521 | 522 | webbrowser.open("https://xueqiu.com/") 523 | 524 | refresh_convertible_bond() 525 | refresh_premium_rate_convertible_bond() 526 | refresh_price_and_premium_rate_convertible_bond() 527 | refresh_multifactor1_convertible_bond() 528 | refresh_multifactor2_convertible_bond() 529 | 530 | rotate_abroad_fund() 531 | 532 | # 更新股票实时数据 533 | refresh_stock() 534 | 535 | def main(): 536 | main_function() 537 | schedule.every().day.at("07:00").do(main_function) # 部署7:00执行更新数据任务 538 | while True: 539 | schedule.run_pending() 540 | 541 | if __name__ == "__main__": 542 | main() -------------------------------------------------------------------------------- /UniversalRotation.xlsm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wbbyfd/UniversalRotation/e02a9663c0b0675dc98e91a0b7a98fc31bb2ff24/UniversalRotation.xlsm -------------------------------------------------------------------------------- /res/LSJZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wbbyfd/UniversalRotation/e02a9663c0b0675dc98e91a0b7a98fc31bb2ff24/res/LSJZ.png -------------------------------------------------------------------------------- /res/author.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wbbyfd/UniversalRotation/e02a9663c0b0675dc98e91a0b7a98fc31bb2ff24/res/author.png -------------------------------------------------------------------------------- /res/download.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wbbyfd/UniversalRotation/e02a9663c0b0675dc98e91a0b7a98fc31bb2ff24/res/download.png -------------------------------------------------------------------------------- /res/premium_rate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wbbyfd/UniversalRotation/e02a9663c0b0675dc98e91a0b7a98fc31bb2ff24/res/premium_rate.png --------------------------------------------------------------------------------