[
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\ndata\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\n\nnotebooks\nnohup.out\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test_akshare / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# dotenv\n.env\n\n# virtualenv\n.venv\nvenv/\nENV/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n\n.idea\n*.iml\n.DS_Store\n*.zip\n*.log\n*.pyc\ndoc\n/bin\npkg\n*.tmp"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"{}\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright {yyyy} {name of copyright owner}\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "\n\n### pythonstock V3.0 项目简介，2025.02.28更新\n\n**特别说明：股市有风险投资需谨慎，本项目只能用于Python代码学习，股票分析，投资失败亏钱不负责，不算BUG。**\n\n**github/gitee是项目地址**\n\ngithub地址：\nhttps://github.com/pythonstock/stock\n\ngitee地址：\nhttps://gitee.com/pythonstock/stock\n\n**视频地址：**\nhttps://space.bilibili.com/52280367/lists/1923758?type=season\n\n**相关博客资料：**\nhttps://blog.csdn.net/freewebsys/category_9285317.html\n\n数据分析清洗使用pandas，numpy。\nhttp://pandas.pydata.org/\n\n数据存储到磁盘上，使用Mysql数据库。存储股票数据。\nhttps://pypi.python.org/pypi/mysqlclient\n\nweb框架使用tornado\nhttp://www.tornadoweb.org/en/stable/\n\ntornado web系统\nhttp://docs.pythontab.com/tornado/introduction-to-tornado/\n\n```\n\nPythonStock V3.0 是基于Python的pandas，akshare，bokeh，tornado，stockstats，ta-lib等框架开发的全栈股票系统。\n项目创建于2017年7月17日，每月不定期更新。\n1）可以直接使用docker直接本地部署运行，整个项目在docker hub上压缩后200MB，本地占用500MB磁盘空间。\n2）使用Docker解决了Python库安装问题，使用Mariadb（MySQL）存储数据。借助akshare抓取数据。\n3）使用cron做定时任务，每天进行数据抓取计算，每天18点开始进行数据计算，计算当日数据，使用300天数据进行计算，大约需要15分钟计算完毕。\n4）股票数据接口防止被封，按天进行数据缓存，储存最近3天数据，每天定时清除，同时使用read_pickle to_pickle 的gzip压缩模式存储。\n5）使用tornado开发web系统，支持每日股票数据-东财，龙虎榜-个股上榜-新浪，数据中心-大宗交易行情等。\n6）数据展示系统，是通用数据展示系统，配置字典模板之后，页面自动加载数据，并完成数据展示，后续自己开发的指标数据可以加入进去。\n7）增加曲线数据分析，在查看股票中，可以直接跳转到东方财富页面查看相关信息，点击指标之后使用Bokeh将多达 17 个指标的数据绘图，进行图表展示。\n8）2.0 最大的更新在于替换tushare库（因部分库不能使用），使用akshare进行数据抓取。\n9）3.0 主要做的是项目整合，前端使用vue开发了，后端使用API,使用docker-compose开发部署。\n\n基础库版本\n\n1，pandas使用【 2.2.3 】版本， \n2，numpy使用【 2.2.1 】版本， \n3，sqlalchemy使用【 2.0.36 】版本， \n4，akshare使用【 1.15.59 】版本， \n5，bokeh使用【 3.6.2 】版本， \n6，stockstats使用【 0.3.2 】版本， \n\n```\n\n\n版本3.0 说明\n\n\n ![image](https://gitee.com/pythonstock/stock/raw/master/frontend/public/stock-001.png)\n ![image](https://gitee.com/pythonstock/stock/raw/master/frontend/public/stock-002.png)\n ![image](https://gitee.com/pythonstock/stock/raw/master/frontend/public/stock-003.png)\n\n\n然后根据3个指标进行股票数据计算：\n\n```\n\nKDJ:\n1，超买区：K值在80以上，D值在70以上，J值大于90时为超买。一般情况下，股价有可能下跌。投资者应谨慎行事，局外人不应再追涨，局内人应适时卖出。\n2，超卖区：K值在20以下，D值在30以下为超卖区。一般情况下，股价有可能上涨，反弹的可能性增大。局内人不应轻易抛出股票，局外人可寻机入场。\n\nRSI:\n1．当六日指标上升到达80时，表示股市已有超买现象，如果一旦继续上升，超过90以上时，则表示已到严重超买的警戒区，股价已形成头部，极可能在短期内反转回转。\n2．当六日强弱指标下降至20时，表示股市有超卖现象，如果一旦继续下降至10以下时则表示已到严重超卖区域，股价极可能有止跌回升的机会。\n\n购买条件结果表：guess_indicators_lite_buy_daily\n购买条件结果表：guess_indicators_lite_sell_daily\n\n```\n\n每日股票指标数据计算17个指标如下（数据表 guess_indicators_daily）： \n\n\n| 计算指标           | 说明 |\n|---------- |------------------------------------------|\n| 1，交易量delta指标分析     | The Volume Delta (Vol ∆)  |\n| 2，计算n天差     |  可以计算，向前n天，和向后n天的差。  |\n| 3，n天涨跌百分百计算     |  可以看到，-n天数据和今天数据的百分比。  |\n| 4, CR指标     | http://wiki.mbalib.com/wiki/CR%E6%8C%87%E6%A0%87 价格动量指标 CR跌穿a、b、c、d四条线，再由低点向上爬升160时，为短线获利的一个良机，应适当卖出股票。 CR跌至40以下时，是建仓良机。而CR高于300~400时，应注意适当减仓。  |\n| 5，最大值，最小值     |  计算区间最大值 volume max of three days ago, yesterday and two days later stock[\"volume_-3,2,-1_max\"] volume min between 3 days ago and tomorrow stock[\"volume_-3~1_min\"] 实际使用的时候使用 -2~2 可计算出5天的最大，最小值。 |\n| 6, KDJ指标     | http://wiki.mbalib.com/wiki/%E9%9A%8F%E6%9C%BA%E6%8C%87%E6%A0%87    随机指标(KDJ)一般是根据统计学的原理，通过一个特定的周期（常为9日、9周等）内出现过的最高价、 最低价及最后一个计算周期的收盘价及这三者之间的比例关系，来计算最后一个计算周期的未成熟随机值RSV， 然后根据平滑移动平均线的方法来计算K值、D值与J值，并绘成曲线图来研判股票走势。 （3）在使用中，常有J线的指标，即3乘以K值减2乘以D值（3K－2D＝J），其目的是求出K值与D值的最大乖离程度， 以领先KD值找出底部和头部。J大于100时为超买，小于10时为超卖。 |\n| 7，SMA指标     | http://wiki.mbalib.com/wiki/Sma 简单移动平均线（Simple Moving Average，SMA） 可以动态输入参数，获得几天的移动平均。 |\n| 8, MACD指标     | http://wiki.mbalib.com/wiki/MACD   平滑异同移动平均线(Moving Average Convergence Divergence，简称MACD指标)，也称移动平均聚散指标 MACD 则可发挥其应有的功能，但当市场呈牛皮盘整格局，股价不上不下时，MACD买卖讯号较不明显。 当用MACD作分析时，亦可运用其他的技术分析指标如短期 K，D图形作为辅助工具，而且也可对买卖讯号作双重的确认。 |\n| 9, BOLL指标     | http://wiki.mbalib.com/wiki/BOLL   布林线指标(Bollinger Bands) |\n| 10, RSI指标     | http://wiki.mbalib.com/wiki/RSI    相对强弱指标（Relative Strength Index，简称RSI），也称相对强弱指数、相对力度指数 2）强弱指标保持高于50表示为强势市场，反之低于50表示为弱势市场。 （3）强弱指标多在70与30之间波动。当六日指标上升到达80时，表示股市已有超买现象，如果一旦继续上升，超过90以上时，则表示已到严重超买的警戒区，股价已形成头部，极可能在短期内反转回转。 |\n| 11, W%R指标     | http://wiki.mbalib.com/wiki/%E5%A8%81%E5%BB%89%E6%8C%87%E6%A0%87 威廉指数（Williams%Rate）该指数是利用摆动点来度量市场的超买超卖现象。 |\n| 14, TR、ATR指标     | http://wiki.mbalib.com/wiki/%E5%9D%87%E5%B9%85%E6%8C%87%E6%A0%87   均幅指标（Average True Ranger,ATR）均幅指标（ATR）是取一定时间周期内的股价波动幅度的移动平均值，主要用于研判买卖时机。 |\n| 14, DMA指标     | http://wiki.mbalib.com/wiki/DMA   DMA指标（Different of Moving Average）又叫平行线差指标，是目前股市分析技术指标中的一种中短期指标，它常用于大盘指数和个股的研判。 DMA, difference of 10 and 50 moving average stock[‘dma’] |\n| 15, DMI，+DI，-DI，DX，ADX，ADXR指标    | http://wiki.mbalib.com/wiki/DMI    动向指数Directional Movement Index,DMI）   http://wiki.mbalib.com/wiki/ADX   平均趋向指标（Average Directional Indicator，简称ADX）   http://wiki.mbalib.com/wiki/%E5%B9%B3%E5%9D%87%E6%96%B9%E5%90%91%E6%8C%87%E6%95%B0%E8%AF%84%E4%BC%B0   平均方向指数评估（ADXR）实际是今日ADX与前面某一日的ADX的平均值。ADXR在高位与ADX同步下滑，可以增加对ADX已经调头的尽早确认。 ADXR是ADX的附属产品，只能发出一种辅助和肯定的讯号，并非入市的指标，而只需同时配合动向指标(DMI)的趋势才可作出买卖策略。 在应用时，应以ADX为主，ADXR为辅。 |\n| 16, TRIX，MATRIX指标     | http://wiki.mbalib.com/wiki/TRIX   TRIX指标又叫三重指数平滑移动平均指标（Triple Exponentially Smoothed Average） |\n| 17, VR，MAVR指标     | http://wiki.mbalib.com/wiki/%E6%88%90%E4%BA%A4%E9%87%8F%E6%AF%94%E7%8E%87   成交量比率（Volumn Ratio，VR）（简称VR），是一项通过分析股价上升日成交额（或成交量，下同）与股价下降日成交额比值， 从而掌握市场买卖气势的中期技术指标。 |\n\n\n\n\n### 项目部署放到docker-compose\n\n\n```bash\n\n# 下载 docker\ncurl -fsSL https://get.docker.com -o get-docker.sh\nsh get-docker.sh \n\n# \ncurl -L \"https://www.ghproxy.cn/https://github.com/docker/compose/releases/download/v2.23.1/docker-compose-$(uname -s)-$(uname -m)\" -o /usr/local/bin/docker-compose\n\n```\n  \n\n```bash\n\n# 生产环境，编译前端部署：\ndocker-compose up -d\n\n# 开发环境，node dev 方式部署：\ndocker-compose -f dev-docker-compose.yml up -d\n```\n\n进入镜像：\n\n```bash\ndocker exec -it stock bash \nsh /data/stock/jobs/cron.daily/run_daily\n```\n\n说明，启动容器后，会调用。run_init.sh 进行数据初始化，同时第一次执行后台执行当日数据。\n以后每日18点（只有18点左右才有今日的数据）进行股票数据抓取并计算。\n\n\n### 本地访问端口\n\n> http://localhost:8080 股票系统前端地址 \n> http://localhost:9090 股票系统后端地址\n\n\n\n### 架构设计\n全系使用python实现。因为都是python的类库，互相之间调用方便。\n从数据抓取，数据处理，到数据展示数据运算都是python实现。\n\n最终的数据都到前端展示出来。主要分为4个文件夹。\n\n> jobs 抓取数据并存储实现类。\n> \n> libs 通用工具类。\n> \n> web 前端展示框架。\n> \n> supervisor 进程管理工具。\n\n\n### 应用部署\n\n需要mysql数据库启动。项目放到/data/stock 目录。\n```\nCREATE DATABASE IF NOT EXISTS `stock_data` CHARACTER SET utf8 COLLATE utf8_general_ci;\n```\n\n使用 :\n\nhttp://docs.sqlalchemy.org/en/latest/core/reflection.html\n\n## 更新日志\n\n### 18 修改bug，前端使用编译nginx方式部署，修改数据库字段，解决定时任务BUG 2025-02-28\n\n存储数据格式为 double 方便进行排序，decimal 类型转换出问题。\n拆分生产部署，切换成nginx，前端进行编译构建。提高前端加载速度。\nnginx的html影射到./data/html目录，前端编译完成需要拷贝文件到html，等待完成即可。\n解决定时任务问题，需要设置权限，才可以。\n\n\n### 17 v3.0发布，前端分离，项目和部署整合到一起 2025-01-10\n\n修改接口展示空。修改数据库脚本。\n解决预测数据买和卖的脚本。\n进行缩减、计算相关添加操作。\n修改启动脚本、接口路径及配置。\n设置分页数据。增加联合主键判断。\n解决分页问题并执行查询语句。\n增加日期查询方法。\n进行数据搜索相关添加操作。\n添加配置。修改路由地址。\n解决端口映射问题及修改端口测试。\n拆分前后端，用 dockerfile 构建镜像并解决前端编译问题。\n修改开发者模式，解决本地开发启动问题。\n增加地址。增加前端开发者模式启动。\n增加每天数据跑批。进行猜工作相关添加操作。\n增加日志跑数据。升级增加展示。\n架构升级并使用 vue 的 ui 开发。\n增加 install docker 说明。\n添加 vue api。\n\n### 16 更新发布 2.1 版本进行镜像升级 2023-06-03\n\n使用新方式打包镜像，镜像大小从本地的 852MB 缩小到 597MB。\n为了支持更多 AKShare 特性，请尽快升级 Python 到 3.8 以上版本\n\n1，numpy从【 1.21.5 】升级到了【 1.21.6 】版本\n2，akshare从【 1.3.50 】升级到了【 1.10.5 】版本\n3，bokeh从【 2.4.2 】升级到了【 2.4.3 】版本\n\n根据 https://www.akshare.xyz/changelog.html \n修改方法：\n\n1.7.99 替换 stock_sina_lhb_ggtj 成：stock_lhb_ggtj_sina\n\n\n### 15 发布一个 2.0 的版本 - 2021-10-11\n\n构建基础版本 pythonstock/pythonstock:base-2021-09 在这个镜像的基础上使用 akshare 1.1.9\n折腾几个月，终于把2.0 弄好了，为啥弄2.0 因为之前发现 tushare的数据不能抓取了。需要注册成 pro 版本，但是pro 还有积分限制。\n诸多不便吧，于是换成了 akshare 库了，大改了，需要找到相关的新库。然后在些代码。\n删除掉了 ta-lib 安装了之后从来没有用到，jupyter 也是没有用。占空间影响下载心情。将镜像进一步减小。\n\n\n### 14 bokeh 升级到 2.4.0 版本\n\n目录\n/usr/local/lib/python3.7/site-packages\n使用脚本进行升级。\n\n### 13 升级ak到v1.0.80 做好每日东方财经数据\n\nhttps://www.akshare.xyz/zh_CN/latest/data/stock/stock.html#id1\n限量: 单次返回所有 A 股上市公司的实时行情数据\n\n600开头的股票是上证A股，属于大盘股，其中6006开头的股票是最早上市的股票，\n6016开头的股票为大盘蓝筹股；900开头的股票是上证B股；\n000开头的股票是深证A股，001、002开头的股票也都属于深证A股，\n其中002开头的股票是深证A股中小企业股票；200开头的股票是深证B股；\n300开头的股票是创业板股票；400开头的股票是三板市场股票。\n\n过滤包括：600，6006，601，000，001，002，且不包括ST的股票数据。\n\n增加数据库utf8 参数 --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci \n\n\n### 12 升级基础镜像到3.7 python，保障 akshare 0.6.10 以上版本支持\n\n发现 akshare 要求升级python 3.7 以上版本才可以，需要升级基础镜像。\n然后 akshare 就可以升级到 0.9.65 的最新版本了。\n新版本就可以按照日期进行查询，解决 TypeError: stock_zh_a_daily() got an unexpected keyword argument 'start_date' 这个问题了。\n\n\n### 11 使用 akshare 做相关股票数据抓取\n\n\t\n中国的股市开盘时间为：每周一至周五的上午9:30——11：30，\n下午13:00——15:00。中国股市收盘时间为：每周一至周五的下午3点。\n\n实时行情数据\n接口: stock_zh_a_spot\n目标地址: http://vip.stock.finance.sina.com.cn/mkt/#hs_a\n描述: A 股数据是从新浪财经获取的数据, 重复运行本函数会被新浪暂时封 IP, 建议增加时间间隔\n限量: 单次返回所有 A 股上市公司的实时行情数据\n\n历史行情数据\n日频率\n接口: stock_zh_a_daily\n目标地址: https://finance.sina.com.cn/realstock/company/sh600006/nc.shtml(示例)\n描述: A 股数据是从新浪财经获取的数据, 历史数据按日频率更新; 注意其中的 sh689009 为 CDR, 请 通过 stock_zh_a_cdr_daily 接口获取\n限量: 单次返回指定 A 股上市公司指定日期间的历史行情日频率数据\n\n### 10 增加东方财经弹窗窗口、增加指标计算弹窗窗口\n\n发现了一个东方财富的页面，是给pc端用的。\n可以做个弹出框放到系统中。不进行调整了，长宽高可以做的小点。使用iframe引入界面。否则有跨域和样式问题。\n\n修改指标页面，改成窗口弹窗，做页面适配，方便查看。\n\n### 9，增加日历\n\n```\n古老的jquery 代码：\n\t\t$( \".date-picker\" ).datepicker({\n\t\t\tlanguage: 'zh-CN', //设置语言\n            format:\"yyyymmdd\",\n            showOtherMonths: true,\n            selectOtherMonths: false,\n            autoclose: true,\n\t\t\ttodayHighlight: true\n        });\n针对日期类型的搜索条件增加日历\n\n```\n\nhttps://www.bootcss.com/p/bootstrap-datetimepicker/\n不是使用jQuery的时间。\n\n### 8，发现MariaDb 版本不兼容问题，最后切换成mysql,使用 mysql:5.7 镜像\n\n相关数据执行只支持到10.5.4，版本可以使用，但是10.5.8 就有问题了。\n限制死了版本。看来软件也不能瞎升级，都用最新的有问题。可以解决数据问题。\n使用 mysql:5.7 镜像，更通用些，不折腾mariaDb了。\n\n\n### 7，解决 Bokeh JS兼容问题。\n\n> 升级 bokeh 到 2.1.1 版本\n>\n> https://pypi.org/project/bokeh/#files\n> \n> 升级JS，因为 lib 包升级导致问题。\n\n\n\n### 6，升级 bokeh 到 2.1.1 版本\n\n```\n\nhttps://pypi.org/project/bokeh/#files\n\n```\n\n### 5，解决日志打印问题\n\n```\n\n配置 main.py \ntornado.options.parse_command_line()\n\n然后启动配置参数：\n/usr/local/bin/python3 /data/stock/web/main.py -log_file_prefix=/data/logs/web.log\n\n```\n\n### 4，解决跑数据问题\n\n```\n# 通过数据库链接 engine。\ndef conn():\n    try:\n        db = MySQLdb.connect(MYSQL_HOST, MYSQL_USER, MYSQL_PWD, MYSQL_DB, charset=\"utf8\")\n        # db.autocommit = True\n    except Exception as e:\n        print(\"conn error :\", e)\n    db.autocommit(on=True)\n    return db.cursor()\n```\n\n之前升级过代码，造成 db.cursor() 问题。\n\n### 3，增加多字段排序\n\n> 1，点击是单个字段进行排序。\n>\n> 2，按照【shift】，点击多个，即可完成多字段排序。\n> \n> 3，服务端分页排序。\n>\n> 4，按照多个字段进行筛选查询。\n\n### 2，使用pandas处理重复数据\n\nhttps://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.drop_duplicates.html\n\n```python\n    data = get_data(year, quarter)\n    # 处理重复数据，保存最新一条数据。\n    data.drop_duplicates(subset=\"code\", keep=\"last\")\n```\n\n### 1，web使用datatable显示报表\n\n通用数据配置，在 libs/stock_web_dic.py 配置数据之后，可以实现动态加载菜单，根据数据库表的行列显示数据。\n\n不用一个表一个表进行开发，通用数据展示。\n\n\n"
  },
  {
    "path": "backend/docs/1-todo-2020-12-06.md",
    "content": "## 切换到了mysql 数据库 5.7 的版本\n\n```log\n$ docker logs mysqldb\n2020-12-06 23:01:40+08:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 5.7.32-1debian10 started.\n2020-12-06 23:01:41+08:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql'\n2020-12-06 23:01:41+08:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 5.7.32-1debian10 started.\n2020-12-06 23:01:41+08:00 [Note] [Entrypoint]: Initializing database files\n2020-12-06T15:01:41.637316Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).\n2020-12-06T15:01:43.872609Z 0 [Warning] InnoDB: New log files created, LSN=45790\n2020-12-06T15:01:44.535591Z 0 [Warning] InnoDB: Creating foreign key constraint system tables.\n2020-12-06T15:01:44.961598Z 0 [Warning] No existing UUID has been found, so we assume that this is the first time that this server has been started. Generating a new UUID: f4ccb5f1-37d3-11eb-8fd1-0242ac180002.\n2020-12-06T15:01:45.054324Z 0 [Warning] Gtid table is not ready to be used. Table 'mysql.gtid_executed' cannot be opened.\n2020-12-06T15:01:45.604908Z 0 [Warning] CA certificate ca.pem is self signed.\n2020-12-06T15:01:45.765331Z 1 [Warning] root@localhost is created with an empty password ! Please consider switching off the --initialize-insecure option.\n2020-12-06 23:02:35+08:00 [Note] [Entrypoint]: Database files initialized\n2020-12-06 23:02:35+08:00 [Note] [Entrypoint]: Starting temporary server\n2020-12-06 23:02:35+08:00 [Note] [Entrypoint]: Waiting for server startup\n2020-12-06T15:02:35.990607Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).\n2020-12-06T15:02:35.992115Z 0 [Note] mysqld (mysqld 5.7.32) starting as process 81 ...\n2020-12-06T15:02:35.995048Z 0 [Note] InnoDB: PUNCH HOLE support available\n2020-12-06T15:02:35.995066Z 0 [Note] InnoDB: Mutexes and rw_locks use GCC atomic builtins\n2020-12-06T15:02:35.995070Z 0 [Note] InnoDB: Uses event mutexes\n2020-12-06T15:02:35.995075Z 0 [Note] InnoDB: GCC builtin __atomic_thread_fence() is used for memory barrier\n2020-12-06T15:02:35.995078Z 0 [Note] InnoDB: Compressed tables use zlib 1.2.11\n2020-12-06T15:02:35.995082Z 0 [Note] InnoDB: Using Linux native AIO\n2020-12-06T15:02:35.995320Z 0 [Note] InnoDB: Number of pools: 1\n2020-12-06T15:02:35.995429Z 0 [Note] InnoDB: Using CPU crc32 instructions\n2020-12-06T15:02:35.996774Z 0 [Note] InnoDB: Initializing buffer pool, total size = 128M, instances = 1, chunk size = 128M\n2020-12-06T15:02:36.007354Z 0 [Note] InnoDB: Completed initialization of buffer pool\n2020-12-06T15:02:36.009348Z 0 [Note] InnoDB: If the mysqld execution user is authorized, page cleaner thread priority can be changed. See the man page of setpriority().\n2020-12-06T15:02:36.021712Z 0 [Note] InnoDB: Highest supported file format is Barracuda.\n2020-12-06T15:02:36.153467Z 0 [Note] InnoDB: Creating shared tablespace for temporary tables\n2020-12-06T15:02:36.153634Z 0 [Note] InnoDB: Setting file './ibtmp1' size to 12 MB. Physically writing the file full; Please wait ...\n2020-12-06T15:02:37.782713Z 0 [Note] InnoDB: File './ibtmp1' size is now 12 MB.\n2020-12-06T15:02:37.784862Z 0 [Note] InnoDB: 96 redo rollback segment(s) found. 96 redo rollback segment(s) are active.\n2020-12-06T15:02:37.784894Z 0 [Note] InnoDB: 32 non-redo rollback segment(s) are active.\n2020-12-06T15:02:37.785844Z 0 [Note] InnoDB: 5.7.32 started; log sequence number 2748463\n2020-12-06T15:02:37.786241Z 0 [Note] InnoDB: Loading buffer pool(s) from /var/lib/mysql/ib_buffer_pool\n2020-12-06T15:02:37.786817Z 0 [Note] Plugin 'FEDERATED' is disabled.\n2020-12-06T15:02:37.790246Z 0 [Note] InnoDB: Buffer pool(s) load completed at 201206 23:02:37\n2020-12-06T15:02:37.803947Z 0 [Note] Found ca.pem, server-cert.pem and server-key.pem in data directory. Trying to enable SSL support using them.\n2020-12-06T15:02:37.803988Z 0 [Note] Skipping generation of SSL certificates as certificate files are present in data directory.\n2020-12-06T15:02:37.806098Z 0 [Warning] CA certificate ca.pem is self signed.\n2020-12-06T15:02:37.806183Z 0 [Note] Skipping generation of RSA key pair as key files are present in data directory.\n2020-12-06T15:02:37.950541Z 0 [Warning] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.\n2020-12-06T15:02:37.973492Z 0 [Note] Event Scheduler: Loaded 0 events\n2020-12-06T15:02:37.974048Z 0 [Note] mysqld: ready for connections.\nVersion: '5.7.32'  socket: '/var/run/mysqld/mysqld.sock'  port: 0  MySQL Community Server (GPL)\n2020-12-06 23:02:38+08:00 [Note] [Entrypoint]: Temporary server started.\nWarning: Unable to load '/usr/share/zoneinfo/iso3166.tab' as time zone. Skipping it.\nWarning: Unable to load '/usr/share/zoneinfo/leap-seconds.list' as time zone. Skipping it.\nWarning: Unable to load '/usr/share/zoneinfo/zone.tab' as time zone. Skipping it.\nWarning: Unable to load '/usr/share/zoneinfo/zone1970.tab' as time zone. Skipping it.\n2020-12-06 23:02:54+08:00 [Note] [Entrypoint]: Creating database stock_data\n\n2020-12-06 23:02:54+08:00 [Note] [Entrypoint]: Stopping temporary server\n2020-12-06T15:02:54.825959Z 0 [Note] Giving 0 client threads a chance to die gracefully\n2020-12-06T15:02:54.825987Z 0 [Note] Shutting down slave threads\n2020-12-06T15:02:54.825992Z 0 [Note] Forcefully disconnecting 0 remaining clients\n2020-12-06T15:02:54.825998Z 0 [Note] Event Scheduler: Purging the queue. 0 events\n2020-12-06T15:02:54.826139Z 0 [Note] Binlog end\n2020-12-06T15:02:54.826658Z 0 [Note] Shutting down plugin 'ngram'\n2020-12-06T15:02:54.826669Z 0 [Note] Shutting down plugin 'partition'\n2020-12-06T15:02:54.826673Z 0 [Note] Shutting down plugin 'BLACKHOLE'\n2020-12-06T15:02:54.826677Z 0 [Note] Shutting down plugin 'ARCHIVE'\n2020-12-06T15:02:54.826682Z 0 [Note] Shutting down plugin 'PERFORMANCE_SCHEMA'\n2020-12-06T15:02:54.826714Z 0 [Note] Shutting down plugin 'MRG_MYISAM'\n2020-12-06T15:02:54.826723Z 0 [Note] Shutting down plugin 'MyISAM'\n2020-12-06T15:02:54.826732Z 0 [Note] Shutting down plugin 'INNODB_SYS_VIRTUAL'\n2020-12-06T15:02:54.826737Z 0 [Note] Shutting down plugin 'INNODB_SYS_DATAFILES'\n2020-12-06T15:02:54.826741Z 0 [Note] Shutting down plugin 'INNODB_SYS_TABLESPACES'\n2020-12-06T15:02:54.826746Z 0 [Note] Shutting down plugin 'INNODB_SYS_FOREIGN_COLS'\n2020-12-06T15:02:54.826749Z 0 [Note] Shutting down plugin 'INNODB_SYS_FOREIGN'\n2020-12-06T15:02:54.826786Z 0 [Note] Shutting down plugin 'INNODB_SYS_FIELDS'\n2020-12-06T15:02:54.826791Z 0 [Note] Shutting down plugin 'INNODB_SYS_COLUMNS'\n2020-12-06T15:02:54.826795Z 0 [Note] Shutting down plugin 'INNODB_SYS_INDEXES'\n2020-12-06T15:02:54.826800Z 0 [Note] Shutting down plugin 'INNODB_SYS_TABLESTATS'\n2020-12-06T15:02:54.826804Z 0 [Note] Shutting down plugin 'INNODB_SYS_TABLES'\n2020-12-06T15:02:54.826808Z 0 [Note] Shutting down plugin 'INNODB_FT_INDEX_TABLE'\n2020-12-06T15:02:54.826811Z 0 [Note] Shutting down plugin 'INNODB_FT_INDEX_CACHE'\n2020-12-06T15:02:54.826813Z 0 [Note] Shutting down plugin 'INNODB_FT_CONFIG'\n2020-12-06T15:02:54.826816Z 0 [Note] Shutting down plugin 'INNODB_FT_BEING_DELETED'\n2020-12-06T15:02:54.826819Z 0 [Note] Shutting down plugin 'INNODB_FT_DELETED'\n2020-12-06T15:02:54.826822Z 0 [Note] Shutting down plugin 'INNODB_FT_DEFAULT_STOPWORD'\n2020-12-06T15:02:54.826825Z 0 [Note] Shutting down plugin 'INNODB_METRICS'\n2020-12-06T15:02:54.826828Z 0 [Note] Shutting down plugin 'INNODB_TEMP_TABLE_INFO'\n2020-12-06T15:02:54.826831Z 0 [Note] Shutting down plugin 'INNODB_BUFFER_POOL_STATS'\n2020-12-06T15:02:54.826834Z 0 [Note] Shutting down plugin 'INNODB_BUFFER_PAGE_LRU'\n2020-12-06T15:02:54.826836Z 0 [Note] Shutting down plugin 'INNODB_BUFFER_PAGE'\n2020-12-06T15:02:54.826839Z 0 [Note] Shutting down plugin 'INNODB_CMP_PER_INDEX_RESET'\n2020-12-06T15:02:54.826842Z 0 [Note] Shutting down plugin 'INNODB_CMP_PER_INDEX'\n2020-12-06T15:02:54.826845Z 0 [Note] Shutting down plugin 'INNODB_CMPMEM_RESET'\n2020-12-06T15:02:54.826848Z 0 [Note] Shutting down plugin 'INNODB_CMPMEM'\n2020-12-06T15:02:54.826851Z 0 [Note] Shutting down plugin 'INNODB_CMP_RESET'\n2020-12-06T15:02:54.826854Z 0 [Note] Shutting down plugin 'INNODB_CMP'\n2020-12-06T15:02:54.826857Z 0 [Note] Shutting down plugin 'INNODB_LOCK_WAITS'\n2020-12-06T15:02:54.826859Z 0 [Note] Shutting down plugin 'INNODB_LOCKS'\n2020-12-06T15:02:54.826862Z 0 [Note] Shutting down plugin 'INNODB_TRX'\n2020-12-06T15:02:54.826910Z 0 [Note] Shutting down plugin 'InnoDB'\n2020-12-06T15:02:54.826953Z 0 [Note] InnoDB: FTS optimize thread exiting.\n2020-12-06T15:02:54.827046Z 0 [Note] InnoDB: Starting shutdown...\n2020-12-06T15:02:54.927339Z 0 [Note] InnoDB: Dumping buffer pool(s) to /var/lib/mysql/ib_buffer_pool\n2020-12-06T15:02:54.993267Z 0 [Note] InnoDB: Buffer pool(s) dump completed at 201206 23:02:54\n2020-12-06T15:02:57.668186Z 0 [Note] InnoDB: Shutdown completed; log sequence number 12619636\n2020-12-06T15:02:57.673193Z 0 [Note] InnoDB: Removed temporary tablespace data file: \"ibtmp1\"\n2020-12-06T15:02:57.673260Z 0 [Note] Shutting down plugin 'MEMORY'\n2020-12-06T15:02:57.673278Z 0 [Note] Shutting down plugin 'CSV'\n2020-12-06T15:02:57.673291Z 0 [Note] Shutting down plugin 'sha256_password'\n2020-12-06T15:02:57.673302Z 0 [Note] Shutting down plugin 'mysql_native_password'\n2020-12-06T15:02:57.673866Z 0 [Note] Shutting down plugin 'binlog'\n2020-12-06T15:02:57.677294Z 0 [Note] mysqld: Shutdown complete\n\n2020-12-06 23:02:57+08:00 [Note] [Entrypoint]: Temporary server stopped\n\n2020-12-06 23:02:57+08:00 [Note] [Entrypoint]: MySQL init process done. Ready for start up.\n\n2020-12-06T15:02:58.038797Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).\n2020-12-06T15:02:58.040198Z 0 [Note] mysqld (mysqld 5.7.32) starting as process 1 ...\n2020-12-06T15:02:58.043137Z 0 [Note] InnoDB: PUNCH HOLE support available\n2020-12-06T15:02:58.043152Z 0 [Note] InnoDB: Mutexes and rw_locks use GCC atomic builtins\n2020-12-06T15:02:58.043155Z 0 [Note] InnoDB: Uses event mutexes\n2020-12-06T15:02:58.043158Z 0 [Note] InnoDB: GCC builtin __atomic_thread_fence() is used for memory barrier\n2020-12-06T15:02:58.043161Z 0 [Note] InnoDB: Compressed tables use zlib 1.2.11\n2020-12-06T15:02:58.043163Z 0 [Note] InnoDB: Using Linux native AIO\n2020-12-06T15:02:58.043392Z 0 [Note] InnoDB: Number of pools: 1\n2020-12-06T15:02:58.043486Z 0 [Note] InnoDB: Using CPU crc32 instructions\n2020-12-06T15:02:58.044796Z 0 [Note] InnoDB: Initializing buffer pool, total size = 128M, instances = 1, chunk size = 128M\n2020-12-06T15:02:58.055082Z 0 [Note] InnoDB: Completed initialization of buffer pool\n2020-12-06T15:02:58.057154Z 0 [Note] InnoDB: If the mysqld execution user is authorized, page cleaner thread priority can be changed. See the man page of setpriority().\n2020-12-06T15:02:58.068627Z 0 [Note] InnoDB: Highest supported file format is Barracuda.\n2020-12-06T15:02:58.191412Z 0 [Note] InnoDB: Creating shared tablespace for temporary tables\n2020-12-06T15:02:58.191760Z 0 [Note] InnoDB: Setting file './ibtmp1' size to 12 MB. Physically writing the file full; Please wait ...\n2020-12-06T15:02:58.636078Z 0 [Note] InnoDB: File './ibtmp1' size is now 12 MB.\n2020-12-06T15:02:58.638452Z 0 [Note] InnoDB: 96 redo rollback segment(s) found. 96 redo rollback segment(s) are active.\n2020-12-06T15:02:58.638497Z 0 [Note] InnoDB: 32 non-redo rollback segment(s) are active.\n2020-12-06T15:02:58.639548Z 0 [Note] InnoDB: Waiting for purge to start\n2020-12-06T15:02:58.689910Z 0 [Note] InnoDB: 5.7.32 started; log sequence number 12619636\n2020-12-06T15:02:58.690442Z 0 [Note] InnoDB: Loading buffer pool(s) from /var/lib/mysql/ib_buffer_pool\n2020-12-06T15:02:58.691219Z 0 [Note] Plugin 'FEDERATED' is disabled.\n2020-12-06T15:02:58.702492Z 0 [Note] InnoDB: Buffer pool(s) load completed at 201206 23:02:58\n2020-12-06T15:02:58.715442Z 0 [Note] Found ca.pem, server-cert.pem and server-key.pem in data directory. Trying to enable SSL support using them.\n2020-12-06T15:02:58.715904Z 0 [Note] Skipping generation of SSL certificates as certificate files are present in data directory.\n2020-12-06T15:02:58.717982Z 0 [Warning] CA certificate ca.pem is self signed.\n2020-12-06T15:02:58.718051Z 0 [Note] Skipping generation of RSA key pair as key files are present in data directory.\n2020-12-06T15:02:58.719045Z 0 [Note] Server hostname (bind-address): '*'; port: 3306\n2020-12-06T15:02:58.720034Z 0 [Note] IPv6 is available.\n2020-12-06T15:02:58.720077Z 0 [Note]   - '::' resolves to '::';\n2020-12-06T15:02:58.720116Z 0 [Note] Server socket created on IP: '::'.\n2020-12-06T15:02:59.182050Z 0 [Warning] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.\n2020-12-06T15:02:59.217680Z 0 [Note] Event Scheduler: Loaded 0 events\n2020-12-06T15:02:59.218060Z 0 [Note] mysqld: ready for connections.\nVersion: '5.7.32'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)\n\n```\n\n要是第一次启动，没有数据，启动会比较慢，需要1分多钟，为了保险，定2分钟后执行初始化任务。\n\n\n## 2，初始化数据接口被停用\n\n```\n本接口即将停止更新，请尽快使用Pro版接口：https://tushare.pro/document/2\nerror : HTTP Error 404: Not Found\n\n```"
  },
  {
    "path": "backend/docs/git-push-tag.md",
    "content": "\n## 创建 tag 并发布到 github 上\n\n\ngit tag -a v2.0 -m \"v2.0\"\n\ngit push origin --tags\n"
  },
  {
    "path": "backend/jobs/18h_daily_job.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\n\nimport libs.common as common\nimport sys\nimport time\nimport pandas as pd\nimport numpy as np\nfrom sqlalchemy.types import NVARCHAR\nfrom sqlalchemy import inspect\nimport datetime\nimport akshare as ak\nimport traceback\nimport MySQLdb\n\n# 600开头的股票是上证A股，属于大盘股\n# 600开头的股票是上证A股，属于大盘股，其中6006开头的股票是最早上市的股票，\n# 6016开头的股票为大盘蓝筹股；900开头的股票是上证B股；\n# 000开头的股票是深证A股，001、002开头的股票也都属于深证A股，\n# 其中002开头的股票是深证A股中小企业股票；\n# 200开头的股票是深证B股；\n# 300开头的股票是创业板股票；400开头的股票是三板市场股票。\ndef stock_a(code):\n    # print(code)\n    # print(type(code))\n    # 上证A股  # 深证A股\n    if code.startswith('600') or code.startswith('6006') or code.startswith('601') or code.startswith('000') or code.startswith('001') or code.startswith('002'):\n        return True\n    else:\n        return False\n# 过滤掉 st 股票。\ndef stock_a_filter_st(name):\n    # print(code)\n    # print(type(code))\n    # 上证A股  # 深证A股\n    if name.find(\"ST\") == -1:\n        return True\n    else:\n        return False\n\n# 过滤价格，如果没有基本上是退市了。\ndef stock_a_filter_price(latest_price):\n    # float 在 pandas 里面判断 空。\n    if np.isnan(latest_price):\n        return False\n    else:\n        return True\n\n####### 3.pdf 方法。宏观经济数据\n# 接口全部有错误。只专注股票数据。\ndef stat_all(tmp_datetime):\n\n    datetime_str = (tmp_datetime).strftime(\"%Y-%m-%d\")\n    datetime_int = (tmp_datetime).strftime(\"%Y%m%d\")\n    print(\"datetime_str:\", datetime_str)\n    print(\"datetime_int:\", datetime_int)\n\n    # 股票列表\n    try:\n        data = ak.stock_zh_a_spot_em()\n        # print(data.index)\n        # 解决ESP 小数问题。\n        # data[\"esp\"] = data[\"esp\"].round(2)  # 数据保留2位小数\n        data.columns = ['index', 'code', 'name', 'last_price', 'change_percent', 'change_amount',\n        'volume', 'turnover', 'amplitude', 'high', 'low', 'open', 'closed', 'volume_ratio',\n        'turnover_rate', 'pe_ratio','pb_ratio', 'market_cap','circulating_market_cap','rise_speed',\n        'change_5min', 'change_ercent_60day','ytd_change_percent']\n\n        data = data.loc[data[\"code\"].apply(stock_a)].loc[data[\"name\"].apply(stock_a_filter_st)].loc[\n            data[\"last_price\"].apply(stock_a_filter_price)]\n        print(data)\n        data['date'] = datetime_int  # 修改时间成为int类型。\n\n        # 删除老数据。\n        del_sql = \" DELETE FROM `stock_zh_a_spot_em` where `date` = '%s' \" % datetime_int\n        common.insert(del_sql)\n\n        data.set_index('code', inplace=True)\n        data.drop('index', axis=1, inplace=True)\n        print(data)\n        # 删除index，然后和原始数据合并。\n        common.insert_db(data, \"stock_zh_a_spot_em\", True, \"`date`,`code`\")\n    except Exception as e:\n        print(\"error :\", e)\n        traceback.print_exc()\n\n\n\n    # 龙虎榜-个股上榜统计\n    # 接口: stock_lhb_ggtj_sina\n    #\n    # 目标地址: http://vip.stock.finance.sina.com.cn/q/go.php/vLHBData/kind/ggtj/index.phtml\n    #\n    # 描述: 获取新浪财经-龙虎榜-个股上榜统计\n    #\n\n    try:\n        stock_lhb_ggtj_sina = ak.stock_lhb_ggtj_sina(symbol=\"5\")\n        print(stock_lhb_ggtj_sina)\n\n        stock_lhb_ggtj_sina.columns = ['code', 'name', 'ranking_times', 'sum_buy', 'sum_sell', 'net_amount', 'buy_seat',\n                                       'sell_seat']\n\n        stock_lhb_ggtj_sina = stock_lhb_ggtj_sina.loc[stock_lhb_ggtj_sina[\"code\"].apply(stock_a)].loc[\n            stock_lhb_ggtj_sina[\"name\"].apply(stock_a_filter_st)]\n\n        stock_lhb_ggtj_sina.set_index('code', inplace=True)\n        # data_sina_lhb.drop('index', axis=1, inplace=True)\n        # 删除老数据。\n        stock_lhb_ggtj_sina['date'] = datetime_int  # 修改时间成为int类型。\n\n        # 删除老数据。\n        del_sql = \" DELETE FROM `stock_lhb_ggtj_sina` where `date` = '%s' \" % datetime_int\n        common.insert(del_sql)\n\n        common.insert_db(stock_lhb_ggtj_sina, \"stock_lhb_ggtj_sina\", True, \"`date`,`code`\")\n\n    except Exception as e:\n        print(\"error :\", e)\n        traceback.print_exc()\n        \n\n\n    # 每日统计\n    # 接口: stock_dzjy_mrtj\n    #\n    # 目标地址: http://data.eastmoney.com/dzjy/dzjy_mrtj.aspx\n    #\n    # 描述: 获取东方财富网-数据中心-大宗交易-每日统计\n    # https://akshare.akfamily.xyz/data/stock/stock.html#id318\n    # import akshare as ak\n    # stock_dzjy_mrtj_df = ak.stock_dzjy_mrtj(start_date='20220105', end_date='20220105')\n    # print(stock_dzjy_mrtj_df)\n\n    try:\n\n        print(\"################ tmp_datetime : \" + datetime_int)\n        # 格式要 int类型日期\n        stock_dzjy_mrtj = ak.stock_dzjy_mrtj(start_date=datetime_int, end_date=datetime_int)\n        print(stock_dzjy_mrtj)\n\n        stock_dzjy_mrtj.columns = ['index', 'trade_date', 'code', 'name', 'quote_change', 'close_price', 'average_price',\n                                   'overflow_rate', 'trade_number', 'sum_volume', 'sum_turnover',\n                                   'turnover_market_rate']\n\n        stock_dzjy_mrtj.set_index('code', inplace=True)\n        # data_sina_lhb.drop('index', axis=1, inplace=True)\n        # 删除老数据。\n        stock_dzjy_mrtj['date'] = datetime_int  # 修改时间成为int类型。\n        stock_dzjy_mrtj.drop('trade_date', axis=1, inplace=True)\n        stock_dzjy_mrtj.drop('index', axis=1, inplace=True)\n\n        # 数据保留2位小数\n        try:\n            stock_dzjy_mrtj = stock_dzjy_mrtj.loc[stock_dzjy_mrtj[\"code\"].apply(stock_a)].loc[\n                stock_dzjy_mrtj[\"name\"].apply(stock_a_filter_st)]\n\n            stock_dzjy_mrtj[\"average_price\"] = stock_dzjy_mrtj[\"average_price\"].round(2)\n            stock_dzjy_mrtj[\"overflow_rate\"] = stock_dzjy_mrtj[\"overflow_rate\"].round(4)\n            stock_dzjy_mrtj[\"turnover_market_rate\"] = stock_dzjy_mrtj[\"turnover_market_rate\"].round(6)\n        except Exception as e:\n            print(\"round error :\", e)\n            traceback.print_exc()\n\n        # 删除老数据。\n        del_sql = \" DELETE FROM `stock_dzjy_mrtj` where `date` = '%s' \" % datetime_int\n        common.insert(del_sql)\n\n        print(stock_dzjy_mrtj)\n\n        common.insert_db(stock_dzjy_mrtj, \"stock_dzjy_mrtj\", True, \"`date`,`code`\")\n\n    except Exception as e:\n        print(\"error :\", e)\n        traceback.print_exc()\n\n# main函数入口\nif __name__ == '__main__':\n    # 执行数据初始化。\n    # 使用方法传递。\n    tmp_datetime = common.run_with_args(stat_all)\n"
  },
  {
    "path": "backend/jobs/README.txt",
    "content": "1,计算每日买全部推荐买。\n2，计算每日全部推荐卖数据。\n3，设置个人账号，设置购买和卖的数据。进行关联查询。\n4，最重要的沪深300，中正500数据。进行大盘股分析。"
  },
  {
    "path": "backend/jobs/aps_job.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\nfrom pytz import utc\nfrom apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore\nfrom apscheduler.schedulers.blocking import BlockingScheduler\n\nfrom apscheduler.executors.pool import ProcessPoolExecutor\nimport libs.common as common\n\n# doc : http://apscheduler.readthedocs.io/en/latest/modules/jobstores/sqlalchemy.html\njobstores = {\n    'default': SQLAlchemyJobStore(url=common.MYSQL_CONN_URL, tablename='apscheduler_jobs')\n}\nexecutors = {\n    'default': {'type': 'threadpool', 'max_workers': 20},\n    'processpool': ProcessPoolExecutor(max_workers=5)\n}\njob_defaults = {\n    'coalesce': False,\n    'max_instances': 3\n}\nscheduler = BlockingScheduler(jobstores=jobstores, executors=executors, job_defaults=job_defaults, timezone=utc)\nscheduler.start()\nprint(\"start ...\")\n"
  },
  {
    "path": "backend/jobs/basic_job.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\nimport libs.common as common\nimport MySQLdb\n\n# 创建新数据库。\ndef create_new_database():\n    with MySQLdb.connect(common.MYSQL_HOST, common.MYSQL_USER, common.MYSQL_PWD, \"mysql\", charset=\"utf8\") as db:\n        try:\n            create_sql = \" CREATE DATABASE IF NOT EXISTS %s CHARACTER SET utf8 COLLATE utf8_general_ci \" % common.MYSQL_DB\n            print(create_sql)\n            db.autocommit(on=True)\n            db.cursor().execute(create_sql)\n        except Exception as e:\n            print(\"error CREATE DATABASE :\", e)\n\n\n# main函数入口\nif __name__ == '__main__':\n\n    # 检查，如果执行 select 1 失败，说明数据库不存在，然后创建一个新的数据库。\n    try:\n        with MySQLdb.connect(common.MYSQL_HOST, common.MYSQL_USER, common.MYSQL_PWD, common.MYSQL_DB,\n                             charset=\"utf8\") as db:\n            db.autocommit(on=True)\n            db.cursor().execute(\" select 1 \")\n            print(\"########### db exists ###########\")\n    except Exception as e:\n        print(\"check  MYSQL_DB error and create new one :\", e)\n        # 检查数据库失败，\n        create_new_database()\n    # 执行数据初始化。\n"
  },
  {
    "path": "backend/jobs/cron.daily/run_daily",
    "content": "#!/bin/sh\n\nmkdir -p /data/logs\nDATETIME=`date +%Y-%m-%d:%H:%M:%S`\n\nDATE=`date +%Y-%m-%d`\n\nexport PYTHONIOENCODING=utf-8\nexport LANG=zh_CN.UTF-8\nexport PYTHONPATH=/data/stock\nexport LC_CTYPE=zh_CN.UTF-8\n\n\necho \"###################\"$DATETIME\"###################\" >> /data/logs/daily.${DATE}.log\n#增加获得今日全部数据和大盘数据\n/usr/local/bin/python3 /data/stock/jobs/18h_daily_job.py >> /data/logs/daily.${DATE}.log\n\n\necho \"###################\"$DATETIME\"###################\" >> /data/logs/daily.${DATE}.log\n#使用股票指标预测。\n/usr/local/bin/python3 /data/stock/jobs/guess_indicators_daily_job.py >> /data/logs/daily.${DATE}.log\n/usr/local/bin/python3 /data/stock/jobs/guess_indicators_daily_buy_job.py >> /data/logs/daily.${DATE}.log\n\n#清除前3天数据。\nDATE_20=`date -d '-20 days' +%Y-%m-%d`\nMONTH_20=`date -d '-20 days' +%Y-%m`\necho \"rm -f /data/cache/hist_data_cache/${MONTH_20}/${DATETIME_20}\"\nrm -f /data/cache/hist_data_cache/${MONTH_20}/${DATETIME_20}"
  },
  {
    "path": "backend/jobs/cron.hourly/run_hourly",
    "content": "#!/bin/sh\n\nmkdir -p /data/logs\nDATE=`date +%Y-%m-%d:%H:%M:%S`\necho $DATE >> /data/logs/hourly.log\n\n"
  },
  {
    "path": "backend/jobs/cron.minutely/run_1minute",
    "content": "#!/bin/bash\n\nmkdir -p /data/logs\nDATE=`date +%Y-%m-%d:%H:%M:%S`\necho $DATE >> /data/logs/1min.log\n"
  },
  {
    "path": "backend/jobs/cron.monthly/run_monthly",
    "content": "#!/bin/sh\n\nmkdir -p /data/logs\nDATE=`date +%Y-%m-%d:%H:%M:%S`\necho $DATE >> /data/logs/monthly.log\n\n"
  },
  {
    "path": "backend/jobs/crontab",
    "content": "SHELL=/bin/sh \nPATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin \n*/1     *       *       *       *       /bin/run-parts /etc/cron.minutely \n10       *       *       *       *       /bin/run-parts /etc/cron.hourly \n30       16       *       *       *       /bin/run-parts /etc/cron.daily \n30       17       1,10,20       *       *       /bin/run-parts /etc/cron.monthly \n"
  },
  {
    "path": "backend/jobs/daily_job.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\n\nimport libs.common as common\nimport sys\nimport os\nimport time\nimport pandas as pd\nimport tushare as ts\nfrom sqlalchemy.types import NVARCHAR\nfrom sqlalchemy import inspect\nimport datetime\nimport shutil\n\n\n####### 使用 5.pdf，先做 基本面数据 的数据，然后在做交易数据。\n#\ndef stat_all(tmp_datetime):\n    datetime_str = (tmp_datetime).strftime(\"%Y-%m-%d\")\n    datetime_int = (tmp_datetime).strftime(\"%Y%m%d\")\n\n    cache_dir = common.bash_stock_tmp % (datetime_str[0:7], datetime_str)\n    if os.path.exists(cache_dir):\n        shutil.rmtree(cache_dir)\n        print(\"remove cache dir force :\", cache_dir)\n\n    print(\"datetime_str:\", datetime_str)\n    print(\"datetime_int:\", datetime_int)\n    data = ts.top_list(datetime_str)\n    # 处理重复数据，保存最新一条数据。最后一步处理，否则concat有问题。\n    #\n    if not data is None and len(data) > 0:\n        # 插入数据库。\n        # del data[\"reason\"]\n        data[\"date\"] = datetime_int  # 修改时间成为int类型。\n        data = data.drop_duplicates(subset=\"code\", keep=\"last\")\n        data.head(n=1)\n        common.insert_db(data, \"ts_top_list\", False, \"`date`,`code`\")\n    else:\n        print(\"no data .\")\n\n    print(datetime_str)\n\n\n# main函数入口\nif __name__ == '__main__':\n    # 使用方法传递。\n    tmp_datetime = common.run_with_args(stat_all)\n"
  },
  {
    "path": "backend/jobs/guess_indicators_daily_buy_job.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\n\nimport libs.common as common\nimport pandas as pd\nimport numpy as np\nimport math\nimport datetime\nimport stockstats\nfrom sqlalchemy import text\n\n\n### 对每日指标数据，进行筛选。将符合条件的。二次筛选出来。\n### 只是做简单筛选\ndef stat_all_lite_buy(tmp_datetime):\n    datetime_str = (tmp_datetime).strftime(\"%Y-%m-%d\")\n    datetime_int = (tmp_datetime).strftime(\"%Y%m%d\")\n    print(\"datetime_str:\", datetime_str)\n    print(\"datetime_int:\", datetime_int)\n\n    # 查询参数\n    params = {\"datetime\": datetime_int}\n\n    sql_kdjk = text(\" SELECT avg(`kdjk`) as avg_kdjk FROM  guess_indicators_daily  \")\n    data_kdjk  = pd.read_sql(sql=sql_kdjk, con=common.engine(), params=params)\n    kdjk = data_kdjk[\"avg_kdjk\"][0]\n\n    sql_kdjd = text(\" SELECT avg(`kdjd`) as avg_kdjd FROM  guess_indicators_daily  \")\n    data_kdjd  = pd.read_sql(sql=sql_kdjd, con=common.engine(), params=params)\n    kdjd = data_kdjd[\"avg_kdjd\"][0]\n\n    sql_kdjj = text(\" SELECT avg(`kdjj`) as avg_kdjj FROM  guess_indicators_daily  \")\n    data_kdjj  = pd.read_sql(sql=sql_kdjj, con=common.engine(), params=params)\n    kdjj = data_kdjj[\"avg_kdjj\"][0]\n\n    # K值在80以上，D值在70以上，J值大于90时为超买。\n    # J大于100时为超买，小于10时为超卖。\n    # 当六日指标上升到达80时，表示股市已有超买现象\n    # 当CCI＞﹢100 时，表明股价已经进入非常态区间——超买区间，股价的异动现象应多加关注。\n    params_1 = {\"datetime\": datetime_int, \"kdjk\": kdjk, \"kdjd\": kdjd, \"kdjj\": kdjj}\n    sql_1 = text(\"\"\"\n            SELECT `date`,`code`,`name`,`last_price`,`change_percent`,`change_amount`,`volume`,`turnover`,\n                            `amplitude`,`high`,`low`,`open`,`closed`,`volume_ratio`,`turnover_rate`,\n                            `pe_ratio`,`pb_ratio`,`market_cap`,`circulating_market_cap`,`rise_speed`,\n                            `change_5min`,`change_ercent_60day`,`ytd_change_percent`,\n         `boll`, `boll_lb`, `boll_ub`, `kdjd`, `kdjj`, `kdjk`, `macd`, `macdh`,\n         `macds`, `pdi`,`trix`, `trix_9_sma`, `vr`, `vr_6_sma`, `wr_10`, `wr_6`     \n        FROM stock_data.guess_indicators_daily WHERE `date` = :datetime\n                        and kdjk >= :kdjk and kdjd >= :kdjd and kdjj >= :kdjj  \n    \"\"\")  # and kdjj > 100 and rsi_6 > 80  and cci > 100 # 调整参数，提前获得股票增长。\n\n    try:\n        # 删除老数据。\n        del_sql = \" DELETE FROM `stock_data`.`guess_indicators_lite_buy_daily` WHERE `date`= '%s' \" % datetime_int\n        common.insert(del_sql)\n    except Exception as e:\n        print(\"error :\", e)\n    \n    print(f\"sql_1 : {sql_1}\")\n    data = pd.read_sql(sql=sql_1, con=common.engine(), params=params_1)\n    data = data.drop_duplicates(subset=\"code\", keep=\"last\")\n    print(\"######## stat_all_lite_buy len data ########:\", len(data))\n\n    try:\n        common.insert_db(data, \"guess_indicators_lite_buy_daily\", False, \"`date`,`code`\")\n    except Exception as e:\n        print(\"error :\", e)\n\n\n\n# main函数入口\nif __name__ == '__main__':\n    # 使用方法传递。\n    # 二次筛选数据。直接计算买卖股票数据。\n    tmp_datetime = common.run_with_args(stat_all_lite_buy)\n\n"
  },
  {
    "path": "backend/jobs/guess_indicators_daily_job.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\n\nimport libs.common as common\nimport pandas as pd\nimport numpy as np\nimport math\nimport datetime\nimport stockstats\n\n# 批处理数据。\ndef stat_all_batch(tmp_datetime):\n    datetime_str = (tmp_datetime).strftime(\"%Y-%m-%d\")\n    datetime_int = (tmp_datetime).strftime(\"%Y%m%d\")\n    print(\"datetime_str:\", datetime_str)\n    print(\"datetime_int:\", datetime_int)\n\n    try:\n        # 删除老数据。\n        del_sql = \" DELETE FROM `guess_indicators_daily` WHERE `date`= %s \" % datetime_int\n        common.insert(del_sql)\n    except Exception as e:\n        print(\"error :\", e)\n\n    sql_count = \"\"\"\n    SELECT count(1) FROM  stock_zh_a_spot_em WHERE `date` = %s and `open` > 0\n    \"\"\"\n    # 修改逻辑，增加中小板块计算。 中小板：002，创业板：300 。已经是经过筛选的数据了。\n    count = common.select_count(sql_count, params=[datetime_int])\n    print(\"count :\", count)\n    batch_size = 100\n    end = int(math.ceil(float(count) / batch_size) * batch_size)\n    print(end)\n    for i in range(0, end, batch_size):\n        print(\"loop :\", i)\n        # 查询今日满足股票数据。剔除数据：创业板股票数据，中小板股票数据，所有st股票\n        # #`code` not like '002%' and `code` not like '300%'  and `name` not like '%st%'\n        sql_1 = \"\"\" \n                    SELECT `date`,`code`,`name`,`last_price`,`change_percent`,`change_amount`,`volume`,`turnover`,\n                            `amplitude`,`high`,`low`,`open`,`closed`,`volume_ratio`,`turnover_rate`,\n                            `pe_ratio`,`pb_ratio`,`market_cap`,`circulating_market_cap`,`rise_speed`,\n                            `change_5min`,`change_ercent_60day`,`ytd_change_percent`\n                    FROM stock_zh_a_spot_em WHERE `date` = %s and `open` > 0  limit %s , %s\n                    \"\"\"\n        sql_2 = sql_1 % (datetime_int, i, batch_size)\n        print(sql_2)\n        # data = pd.read_sql(sql=sql_1, con=common.engine(), params=[datetime_int, '002%', '300%', '%st%', i, batch_size])\n        data = pd.read_sql(sql=sql_2, con=common.engine())\n        data = data.drop_duplicates(subset=\"code\", keep=\"last\")\n        print(\"########data[last_price]########:\", len(data))\n        stat_index_all(data, i)\n\n\n# 分批执行。\ndef stat_index_all(data, idx):\n    # print(data[\"last_price\"])\n    # 1), n天涨跌百分百计算\n    # open price change (in percent) between today and the day before yesterday ‘r’ stands for rate.\n    # stock[‘close_-2_r’]\n    # 可以看到，-n天数据和今天数据的百分比。\n\n\n    # 2), CR指标\n    # http://wiki.mbalib.com/wiki/CR%E6%8C%87%E6%A0%87 价格动量指标\n    # CR跌穿a、b、c、d四条线，再由低点向上爬升160时，为短线获利的一个良机，应适当卖出股票。\n    # CR跌至40以下时，是建仓良机。而CR高于300~400时，应注意适当减仓。\n\n    # 3), KDJ指标\n    # http://wiki.mbalib.com/wiki/%E9%9A%8F%E6%9C%BA%E6%8C%87%E6%A0%87\n    # 随机指标(KDJ)一般是根据统计学的原理，通过一个特定的周期（常为9日、9周等）内出现过的最高价、\n    # 最低价及最后一个计算周期的收盘价及这三者之间的比例关系，来计算最后一个计算周期的未成熟随机值RSV，\n    # 然后根据平滑移动平均线的方法来计算K值、D值与J值，并绘成曲线图来研判股票走势。\n    # （3）在使用中，常有J线的指标，即3乘以K值减2乘以D值（3K－2D＝J），其目的是求出K值与D值的最大乖离程度，\n    # 以领先KD值找出底部和头部。J大于100时为超买，小于10时为超卖。\n\n    # 4), MACD指标\n    # http://wiki.mbalib.com/wiki/MACD\n    # 平滑异同移动平均线(Moving Average Convergence Divergence，简称MACD指标)，也称移动平均聚散指标\n    # MACD 则可发挥其应有的功能，但当市场呈牛皮盘整格局，股价不上不下时，MACD买卖讯号较不明显。\n    # 当用MACD作分析时，亦可运用其他的技术分析指标如短期 K，D图形作为辅助工具，而且也可对买卖讯号作双重的确认。\n\n\n    # 5), BOLL指标\n    # http://wiki.mbalib.com/wiki/BOLL\n    # 布林线指标(Bollinger Bands)\n\n    # 6), RSI指标\n    # http://wiki.mbalib.com/wiki/RSI\n    # 相对强弱指标（Relative Strength Index，简称RSI），也称相对强弱指数、相对力度指数\n    # （2）强弱指标保持高于50表示为强势市场，反之低于50表示为弱势市场。\n    # （3）强弱指标多在70与30之间波动。当六日指标上升到达80时，表示股市已有超买现象，\n    # 如果一旦继续上升，超过90以上时，则表示已到严重超买的警戒区，股价已形成头部，极可能在短期内反转回转。\n\n\n    # 7), W%R指标\n    # http://wiki.mbalib.com/wiki/%E5%A8%81%E5%BB%89%E6%8C%87%E6%A0%87\n    # 威廉指数（Williams%Rate）该指数是利用摆动点来度量市场的超买超卖现象。\n\n    # 8), CCI指标\n    # http://wiki.mbalib.com/wiki/%E9%A1%BA%E5%8A%BF%E6%8C%87%E6%A0%87\n    # 顺势指标又叫CCI指标，其英文全称为“Commodity Channel Index”，\n    # 是由美国股市分析家唐纳德·蓝伯特（Donald Lambert）所创造的，是一种重点研判股价偏离度的股市分析工具。\n    # 1、当CCI指标从下向上突破﹢100线而进入非常态区间时，表明股价脱离常态而进入异常波动阶段，\n    # 中短线应及时买入，如果有比较大的成交量配合，买入信号则更为可靠。\n    # 　　2、当CCI指标从上向下突破﹣100线而进入另一个非常态区间时，表明股价的盘整阶段已经结束，\n    # 将进入一个比较长的寻底过程，投资者应以持币观望为主。\n    # CCI, default to 14 days\n\n    # 9), TR、ATR指标\n    # http://wiki.mbalib.com/wiki/%E5%9D%87%E5%B9%85%E6%8C%87%E6%A0%87\n    # 均幅指标（Average True Ranger,ATR）\n    # 均幅指标（ATR）是取一定时间周期内的股价波动幅度的移动平均值，主要用于研判买卖时机。\n\n    # 10), DMA指标\n    # http://wiki.mbalib.com/wiki/DMA\n    # 　DMA指标（Different of Moving Average）又叫平行线差指标，是目前股市分析技术指标中的一种中短期指标，它常用于大盘指数和个股的研判。\n    # DMA, difference of 10 and 50 moving average\n    # stock[‘dma’]\n\n    # 11), DMI，+DI，-DI，DX，ADX，ADXR指标\n    # http://wiki.mbalib.com/wiki/DMI\n    # 动向指数Directional Movement Index,DMI）\n    # http://wiki.mbalib.com/wiki/ADX\n    # 平均趋向指标（Average Directional Indicator，简称ADX）\n    # http://wiki.mbalib.com/wiki/%E5%B9%B3%E5%9D%87%E6%96%B9%E5%90%91%E6%8C%87%E6%95%B0%E8%AF%84%E4%BC%B0\n    # 平均方向指数评估（ADXR）实际是今日ADX与前面某一日的ADX的平均值。ADXR在高位与ADX同步下滑，可以增加对ADX已经调头的尽早确认。\n    # ADXR是ADX的附属产品，只能发出一种辅助和肯定的讯号，并非入市的指标，而只需同时配合动向指标(DMI)的趋势才可作出买卖策略。\n    # 在应用时，应以ADX为主，ADXR为辅。\n\n    # 12), TRIX，MATRIX指标\n    # http://wiki.mbalib.com/wiki/TRIX\n    # TRIX指标又叫三重指数平滑移动平均指标（Triple Exponentially Smoothed Average）\n\n    # 13), VR，MAVR指标\n    # http://wiki.mbalib.com/wiki/%E6%88%90%E4%BA%A4%E9%87%8F%E6%AF%94%E7%8E%87\n    # 成交量比率（Volumn Ratio，VR）（简称VR），是一项通过分析股价上升日成交额（或成交量，下同）与股价下降日成交额比值，\n    # 从而掌握市场买卖气势的中期技术指标。\n    \n    #stock_column = ['adx', 'adxr', 'boll', 'boll_lb', 'boll_ub', 'cci', 'cci_20', 'close_-1_r',\n    #                'close_-2_r', 'code', 'cr', 'cr-ma1', 'cr-ma2', 'cr-ma3', 'date', 'dma', 'dx',\n    #                'kdjd', 'kdjj', 'kdjk', 'macd', 'macdh', 'macds', 'pdi',\n    #                'rsi_12', 'rsi_6', 'trix', 'trix_9_sma', 'vr', 'vr_6_sma', 'wr_10', 'wr_6']\n    \n    stock_column = ['date','code', 'boll', 'boll_lb', 'boll_ub', \n                    'kdjd', 'kdjj', 'kdjk', 'macd', 'macdh', 'macds', 'pdi',\n                    'trix', 'trix_9_sma', 'vr', 'vr_6_sma', 'wr_10', 'wr_6']\n    # code     cr cr-ma1 cr-ma2 cr-ma3      date\n\n    data_new = concat_guess_data(stock_column, data)\n\n    data_new = data_new.round(2)  # 数据保留2位小数\n\n    # print(data_new.head())\n    print(\"########insert db guess_indicators_daily idx :########:\", idx)\n    try:\n        common.insert_db(data_new, \"guess_indicators_daily\", False, \"`date`,`code`\")\n    except Exception as e:\n        print(\"error :\", e)\n\n\n# 链接guess 数据。\ndef concat_guess_data(stock_column, data):\n    # 使用 trade 填充数据\n    print(\"stock_column:\", stock_column)\n    tmp_dic = {}\n    # 循环增加临时数据。如果要是date，和code，\n    for col in stock_column:\n        if col == 'date':\n            tmp_dic[col] = data[\"date\"]\n        elif col == 'code':\n            tmp_dic[col] = data[\"code\"]\n        else:\n            tmp_dic[col] = data[\"last_price\"]\n    # print(\"##########tmp_dic: \", tmp_dic)\n    print(\"########################## BEGIN ##########################\")\n    stock_guess = pd.DataFrame(tmp_dic, index=data.index.values)\n    print(stock_guess.columns.values)\n    # print(stock_guess.head())\n    stock_guess = stock_guess.apply(apply_guess, stock_column=stock_column, axis=1)  # , axis=1)\n    print(stock_guess.head())\n    # stock_guess.astype('float32', copy=False)\n    stock_guess.drop('date', axis=1, inplace=True)  # 删除日期字段，然后和原始数据合并。\n    # print(stock_guess[\"5d\"])\n    data_new = pd.merge(data, stock_guess, on=['code'], how='left')\n    print(\"#############\")\n    return data_new\n\n\n# 带参数透传。\ndef apply_guess(tmp, stock_column):\n    # print(\"apply_guess columns args:\", stock_column)\n    # print(\"apply_guess data :\", type(tmp))\n    date = tmp[\"date\"]\n    code = tmp[\"code\"]\n    date_end = datetime.datetime.strptime(date, \"%Y%m%d\")\n    date_start = (date_end + datetime.timedelta(days=-100)).strftime(\"%Y-%m-%d\")\n    date_end = date_end.strftime(\"%Y-%m-%d\")\n    # print(code, date_start, date_end)\n    # open, high, close, low, volume, price_change, p_change, ma5, ma10, ma20, v_ma5, v_ma10, v_ma20, turnover\n    # 使用缓存方法。加快计算速度。\n    stock = common.get_hist_data_cache(code, date_start, date_end)\n    # 设置返回数组。\n    stock_data_list = []\n    stock_name_list = []\n    print(f\"stock_column : {stock_column}\")\n    # 增加空判断，如果是空返回 0 数据。\n    if stock is None:\n        for col in stock_column:\n            if col == 'date':\n                stock_data_list.append(date)\n                stock_name_list.append('date')\n            elif col == 'code':\n                stock_data_list.append(code)\n                stock_name_list.append('code')\n            else:\n                stock_data_list.append(0)\n                stock_name_list.append(col)\n        return pd.Series(stock_data_list, index=stock_name_list)\n\n    # print(stock.head())\n    # open  high  close   low     volume\n    # stock = pd.DataFrame({\"close\": stock[\"close\"]}, index=stock.index.values)\n    # stock = stock.sort_index(0)  # 将数据按照日期排序下。\n\n    stock[\"date\"] = stock.index.values  # 增加日期列。\n    print(f\"stock: {stock}\")\n    # stock = stock.sort_index(0)  # 将数据按照日期排序下。\n    # print(stock) [186 rows x 14 columns]\n    # 初始化统计类\n    # stockStat = stockstats.StockDataFrame.retype(pd.read_csv('002032.csv'))\n    stockStat = stockstats.StockDataFrame.retype(stock)\n    print(f\"stockStat : {stockStat}\")\n\n    print(\"########################## print result ##########################\")\n    for col in stock_column:\n        if col == 'date':\n            stock_data_list.append(date)\n            stock_name_list.append('date')\n        elif col == 'code':\n            stock_data_list.append(code)\n            stock_name_list.append('code')\n        else:\n            # 将数据的最后一个返回。\n            print(col)\n            print(stockStat[col])\n            print(stockStat[col].values[1])\n            #print(stockStat[col].head(1))\n            \n            tmp_val = stockStat[col].values[1]\n            if np.isinf(tmp_val):  # 解决值中存在INF问题。\n                tmp_val = 0\n            if np.isnan(tmp_val):  # 解决值中存在NaN问题。\n                tmp_val = 0\n            # print(\"col name : \", col, tmp_val)\n            stock_data_list.append(tmp_val)\n            stock_name_list.append(col)\n    # print(stock_data_list)\n    return pd.Series(stock_data_list, index=stock_name_list)\n\n\n# print(stock[\"mov_vol\"].tail())\n# print(stock[\"return\"].tail())\n# print(\"stock[10d].tail(1)\", stock[\"10d\"].tail(1).values[0])\n# 10d    20d  5-10d  5-20d     5d    60d    code      date  mov_vol  return\n# tmp = list([stock[\"10d\"].tail(1).values[0], stock[\"20d\"].tail(1).values[0], stock[\"5-10d\"].tail(1).values[0],\n#             stock[\"5-20d\"].tail(1).values[0], stock[\"5d\"].tail(1).values[0], stock[\"60d\"].tail(1).values[0],\n#             code, date, stock[\"mov_vol\"].tail(1).values[0], stock[\"return\"].tail(1).values[0]])\n# # print(tmp)\n# return tmp\n\n\n# main函数入口\nif __name__ == '__main__':\n    # 使用方法传递。\n    tmp_datetime = common.run_with_args(stat_all_batch)\n    # 二次筛选数据。直接计算买卖股票数据。\n\n"
  },
  {
    "path": "backend/jobs/guess_indicators_daily_sell_job.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\n\nimport libs.common as common\nimport pandas as pd\nimport numpy as np\nimport math\nimport datetime\nimport stockstats\nfrom sqlalchemy import text\n\n# 设置卖出数据。\ndef stat_all_lite_sell(tmp_datetime):\n    datetime_str = (tmp_datetime).strftime(\"%Y-%m-%d\")\n    datetime_int = (tmp_datetime).strftime(\"%Y%m%d\")\n    print(\"datetime_str:\", datetime_str)\n    print(\"datetime_int:\", datetime_int)\n\n    # 超卖区：K值在20以下，D值在30以下为超卖区。一般情况下，股价有可能上涨，反弹的可能性增大。局内人不应轻易抛出股票，局外人可寻机入场。\n    # J大于100时为超买，小于10时为超卖。\n    # 当六日强弱指标下降至20时，表示股市有超卖现象\n    # 当CCI＜﹣100时，表明股价已经进入另一个非常态区间——超卖区间，投资者可以逢低吸纳股票。\n    sql_1 = text(\"\"\"\n            SELECT `date`,`code`,`name`,`last_price`,`change_percent`,`change_amount`,`volume`,`turnover`,\n                            `amplitude`,`high`,`low`,`open`,`closed`,`volume_ratio`,`turnover_rate`,\n                            `pe_ratio`,`pb_ratio`,`market_cap`,`circulating_market_cap`,`rise_speed`,\n                            `change_5min`,`change_ercent_60day`,`ytd_change_percent`,\n         `boll`, `boll_lb`, `boll_ub`, `kdjd`, `kdjj`, `kdjk`, `macd`, `macdh`,\n         `macds`, `pdi`,`trix`, `trix_9_sma`, `vr`, `vr_6_sma`, `wr_10`, `wr_6` \n                        FROM stock_data.guess_indicators_daily WHERE `date` = :datetime\n                        and kdjk <= 20 and kdjd <= 30 and kdjj <= 10  \n    \"\"\")\n\n    try:\n        # 删除老数据。\n        del_sql = \" DELETE FROM `stock_data`.`guess_indicators_lite_sell_daily` WHERE `date`= '%s' \" % datetime_int\n        common.insert(del_sql)\n    except Exception as e:\n        print(\"error :\", e)\n\n    # 查询参数\n    params = {\"datetime\": datetime_int}\n    print(sql_1)\n    data = pd.read_sql(sql=sql_1, con=common.engine(), params=params)\n    data = data.drop_duplicates(subset=\"code\", keep=\"last\")\n    print(\"######## stat_all_lite_sell len data ########:\", len(data))\n\n    try:\n        common.insert_db(data, \"guess_indicators_lite_sell_daily\", False, \"`date`,`code`\")\n    except Exception as e:\n        print(\"error :\", e)\n\n\n\n# main函数入口\nif __name__ == '__main__':\n    # 使用方法传递。\n    # 二次筛选数据。直接计算买卖股票数据。\n    tmp_datetime = common.run_with_args(stat_all_lite_sell)\n\n"
  },
  {
    "path": "backend/jobs/guess_rsrs_daily_job.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\n\nimport libs.common as common\nimport pandas as pd\nimport numpy as np\nimport math\nimport datetime\nimport stockstats\n\n\n### 对每日指标数据，进行筛选。将符合条件的。二次筛选出来。\n### 只是做简单筛选\ndef stat_all_lite_buy(tmp_datetime):\n    datetime_str = (tmp_datetime).strftime(\"%Y-%m-%d\")\n    datetime_int = (tmp_datetime).strftime(\"%Y%m%d\")\n    print(\"datetime_str:\", datetime_str)\n    print(\"datetime_int:\", datetime_int)\n\n    # K值在80以上，D值在70以上，J值大于90时为超买。\n    # J大于100时为超买，小于10时为超卖。\n    # 当六日指标上升到达80时，表示股市已有超买现象\n    # 当CCI＞﹢100 时，表明股价已经进入非常态区间——超买区间，股价的异动现象应多加关注。\n    sql_1 = \"\"\"\n            SELECT `date`, `code`, `name`, `changepercent`, `trade`, `open`, `high`, `low`, \n                            `settlement`, `volume`, `turnoverratio`, `amount`, `per`, `pb`, `mktcap`,\n                             `nmc` ,`kdjj`,`rsi_6`,`cci`\n                        FROM stock_data.guess_indicators_daily WHERE `date` = %s \n                        and kdjk >= 80 and kdjd >= 70 and kdjj >= 100  and rsi_6 >= 80  and cci >= 100\n    \"\"\"  # and kdjj > 100 and rsi_6 > 80  and cci > 100 # 调整参数，提前获得股票增长。\n\n    try:\n        # 删除老数据。\n        del_sql = \" DELETE FROM `stock_data`.`guess_indicators_lite_buy_daily` WHERE `date`= '%s' \" % datetime_int\n        common.insert(del_sql)\n    except Exception as e:\n        print(\"error :\", e)\n\n    data = pd.read_sql(sql=sql_1, con=common.engine(), params=[datetime_int])\n    data = data.drop_duplicates(subset=\"code\", keep=\"last\")\n    print(\"######## len data ########:\", len(data))\n\n    try:\n        common.insert_db(data, \"guess_indicators_lite_buy_daily\", False, \"`date`,`code`\")\n    except Exception as e:\n        print(\"error :\", e)\n\n# 设置卖出数据。\ndef stat_all_lite_sell(tmp_datetime):\n    datetime_str = (tmp_datetime).strftime(\"%Y-%m-%d\")\n    datetime_int = (tmp_datetime).strftime(\"%Y%m%d\")\n    print(\"datetime_str:\", datetime_str)\n    print(\"datetime_int:\", datetime_int)\n\n    # 超卖区：K值在20以下，D值在30以下为超卖区。一般情况下，股价有可能上涨，反弹的可能性增大。局内人不应轻易抛出股票，局外人可寻机入场。\n    # J大于100时为超买，小于10时为超卖。\n    # 当六日强弱指标下降至20时，表示股市有超卖现象\n    # 当CCI＜﹣100时，表明股价已经进入另一个非常态区间——超卖区间，投资者可以逢低吸纳股票。\n    sql_1 = \"\"\"\n            SELECT `date`, `code`, `name`, `changepercent`, `trade`, `open`, `high`, `low`, \n                            `settlement`, `volume`, `turnoverratio`, `amount`, `per`, `pb`, `mktcap`,\n                             `nmc` ,`kdjj`,`rsi_6`,`cci`\n                        FROM stock_data.guess_indicators_daily WHERE `date` = %s \n                        and kdjk <= 20 and kdjd <= 30 and kdjj <= 10  and rsi_6 <= 20  and cci <= -100\n    \"\"\"\n\n    try:\n        # 删除老数据。\n        del_sql = \" DELETE FROM `stock_data`.`guess_indicators_lite_sell_daily` WHERE `date`= '%s' \" % datetime_int\n        common.insert(del_sql)\n    except Exception as e:\n        print(\"error :\", e)\n\n    data = pd.read_sql(sql=sql_1, con=common.engine(), params=[datetime_int])\n    data = data.drop_duplicates(subset=\"code\", keep=\"last\")\n    print(\"######## len data ########:\", len(data))\n\n    try:\n        common.insert_db(data, \"guess_indicators_lite_sell_daily\", False, \"`date`,`code`\")\n    except Exception as e:\n        print(\"error :\", e)\n\n# 批处理数据。\ndef stat_all_batch(tmp_datetime):\n    datetime_str = (tmp_datetime).strftime(\"%Y-%m-%d\")\n    datetime_int = (tmp_datetime).strftime(\"%Y%m%d\")\n    print(\"datetime_str:\", datetime_str)\n    print(\"datetime_int:\", datetime_int)\n\n    try:\n        # 删除老数据。\n        del_sql = \" DELETE FROM `stock_data`.`guess_indicators_daily` WHERE `date`= %s \" % datetime_int\n        common.insert(del_sql)\n    except Exception as e:\n        print(\"error :\", e)\n\n    sql_count = \"\"\"\n    SELECT count(1) FROM stock_data.ts_today_all WHERE `date` = %s and `trade` > 0 and `open` > 0 and trade <= 20 \n                 and `code` not like %s and `name` not like %s\n    \"\"\"\n    # 修改逻辑，增加中小板块计算。 中小板：002，创业板：300 。and `code` not like %s and `code` not like %s and `name` not like %s\n    # count = common.select_count(sql_count, params=[datetime_int, '002%', '300%', '%st%'])\n    count = common.select_count(sql_count, params=[datetime_int, '300%', '%st%'])\n    print(\"count :\", count)\n    batch_size = 100\n    end = int(math.ceil(float(count) / batch_size) * batch_size)\n    print(end)\n    for i in range(0, end, batch_size):\n        print(\"loop :\", i)\n        # 查询今日满足股票数据。剔除数据：创业板股票数据，中小板股票数据，所有st股票\n        # #`code` not like '002%' and `code` not like '300%'  and `name` not like '%st%'\n        sql_1 = \"\"\" \n                    SELECT `date`, `code`, `name`, `changepercent`, `trade`, `open`, `high`, `low`, \n                        `settlement`, `volume`, `turnoverratio`, `amount`, `per`, `pb`, `mktcap`, `nmc` \n                    FROM stock_data.ts_today_all WHERE `date` = %s and `trade` > 0 and `open` > 0 and trade <= 20 \n                        and `code` not like %s and `name` not like %s limit %s , %s\n                    \"\"\"\n        print(sql_1)\n        # data = pd.read_sql(sql=sql_1, con=common.engine(), params=[datetime_int, '002%', '300%', '%st%', i, batch_size])\n        data = pd.read_sql(sql=sql_1, con=common.engine(), params=[datetime_int, '300%', '%st%', i, batch_size])\n        data = data.drop_duplicates(subset=\"code\", keep=\"last\")\n        print(\"########data[trade]########:\", len(data))\n        stat_index_all(data, i)\n\n\n# 分批执行。\ndef stat_index_all(data, idx):\n    # print(data[\"trade\"])\n    # 1), n天涨跌百分百计算\n    # open price change (in percent) between today and the day before yesterday ‘r’ stands for rate.\n    # stock[‘close_-2_r’]\n    # 可以看到，-n天数据和今天数据的百分比。\n\n\n    # 2), CR指标\n    # http://wiki.mbalib.com/wiki/CR%E6%8C%87%E6%A0%87 价格动量指标\n    # CR跌穿a、b、c、d四条线，再由低点向上爬升160时，为短线获利的一个良机，应适当卖出股票。\n    # CR跌至40以下时，是建仓良机。而CR高于300~400时，应注意适当减仓。\n\n    # 3), KDJ指标\n    # http://wiki.mbalib.com/wiki/%E9%9A%8F%E6%9C%BA%E6%8C%87%E6%A0%87\n    # 随机指标(KDJ)一般是根据统计学的原理，通过一个特定的周期（常为9日、9周等）内出现过的最高价、\n    # 最低价及最后一个计算周期的收盘价及这三者之间的比例关系，来计算最后一个计算周期的未成熟随机值RSV，\n    # 然后根据平滑移动平均线的方法来计算K值、D值与J值，并绘成曲线图来研判股票走势。\n    # （3）在使用中，常有J线的指标，即3乘以K值减2乘以D值（3K－2D＝J），其目的是求出K值与D值的最大乖离程度，\n    # 以领先KD值找出底部和头部。J大于100时为超买，小于10时为超卖。\n\n    # 4), MACD指标\n    # http://wiki.mbalib.com/wiki/MACD\n    # 平滑异同移动平均线(Moving Average Convergence Divergence，简称MACD指标)，也称移动平均聚散指标\n    # MACD 则可发挥其应有的功能，但当市场呈牛皮盘整格局，股价不上不下时，MACD买卖讯号较不明显。\n    # 当用MACD作分析时，亦可运用其他的技术分析指标如短期 K，D图形作为辅助工具，而且也可对买卖讯号作双重的确认。\n\n\n    # 5), BOLL指标\n    # http://wiki.mbalib.com/wiki/BOLL\n    # 布林线指标(Bollinger Bands)\n\n    # 6), RSI指标\n    # http://wiki.mbalib.com/wiki/RSI\n    # 相对强弱指标（Relative Strength Index，简称RSI），也称相对强弱指数、相对力度指数\n    # （2）强弱指标保持高于50表示为强势市场，反之低于50表示为弱势市场。\n    # （3）强弱指标多在70与30之间波动。当六日指标上升到达80时，表示股市已有超买现象，\n    # 如果一旦继续上升，超过90以上时，则表示已到严重超买的警戒区，股价已形成头部，极可能在短期内反转回转。\n\n\n    # 7), W%R指标\n    # http://wiki.mbalib.com/wiki/%E5%A8%81%E5%BB%89%E6%8C%87%E6%A0%87\n    # 威廉指数（Williams%Rate）该指数是利用摆动点来度量市场的超买超卖现象。\n\n    # 8), CCI指标\n    # http://wiki.mbalib.com/wiki/%E9%A1%BA%E5%8A%BF%E6%8C%87%E6%A0%87\n    # 顺势指标又叫CCI指标，其英文全称为“Commodity Channel Index”，\n    # 是由美国股市分析家唐纳德·蓝伯特（Donald Lambert）所创造的，是一种重点研判股价偏离度的股市分析工具。\n    # 1、当CCI指标从下向上突破﹢100线而进入非常态区间时，表明股价脱离常态而进入异常波动阶段，\n    # 中短线应及时买入，如果有比较大的成交量配合，买入信号则更为可靠。\n    # 　　2、当CCI指标从上向下突破﹣100线而进入另一个非常态区间时，表明股价的盘整阶段已经结束，\n    # 将进入一个比较长的寻底过程，投资者应以持币观望为主。\n    # CCI, default to 14 days\n\n    # 9), TR、ATR指标\n    # http://wiki.mbalib.com/wiki/%E5%9D%87%E5%B9%85%E6%8C%87%E6%A0%87\n    # 均幅指标（Average True Ranger,ATR）\n    # 均幅指标（ATR）是取一定时间周期内的股价波动幅度的移动平均值，主要用于研判买卖时机。\n\n    # 10), DMA指标\n    # http://wiki.mbalib.com/wiki/DMA\n    # 　DMA指标（Different of Moving Average）又叫平行线差指标，是目前股市分析技术指标中的一种中短期指标，它常用于大盘指数和个股的研判。\n    # DMA, difference of 10 and 50 moving average\n    # stock[‘dma’]\n\n    # 11), DMI，+DI，-DI，DX，ADX，ADXR指标\n    # http://wiki.mbalib.com/wiki/DMI\n    # 动向指数Directional Movement Index,DMI）\n    # http://wiki.mbalib.com/wiki/ADX\n    # 平均趋向指标（Average Directional Indicator，简称ADX）\n    # http://wiki.mbalib.com/wiki/%E5%B9%B3%E5%9D%87%E6%96%B9%E5%90%91%E6%8C%87%E6%95%B0%E8%AF%84%E4%BC%B0\n    # 平均方向指数评估（ADXR）实际是今日ADX与前面某一日的ADX的平均值。ADXR在高位与ADX同步下滑，可以增加对ADX已经调头的尽早确认。\n    # ADXR是ADX的附属产品，只能发出一种辅助和肯定的讯号，并非入市的指标，而只需同时配合动向指标(DMI)的趋势才可作出买卖策略。\n    # 在应用时，应以ADX为主，ADXR为辅。\n\n    # 12), TRIX，MATRIX指标\n    # http://wiki.mbalib.com/wiki/TRIX\n    # TRIX指标又叫三重指数平滑移动平均指标（Triple Exponentially Smoothed Average）\n\n    # 13), VR，MAVR指标\n    # http://wiki.mbalib.com/wiki/%E6%88%90%E4%BA%A4%E9%87%8F%E6%AF%94%E7%8E%87\n    # 成交量比率（Volumn Ratio，VR）（简称VR），是一项通过分析股价上升日成交额（或成交量，下同）与股价下降日成交额比值，\n    # 从而掌握市场买卖气势的中期技术指标。\n\n    stock_column = ['adx', 'adxr', 'boll', 'boll_lb', 'boll_ub', 'cci', 'cci_20', 'close_-1_r',\n                    'close_-2_r', 'code', 'cr', 'cr-ma1', 'cr-ma2', 'cr-ma3', 'date', 'dma', 'dx',\n                    'kdjd', 'kdjj', 'kdjk', 'macd', 'macdh', 'macds', 'mdi', 'pdi',\n                    'rsi_12', 'rsi_6', 'trix', 'trix_9_sma', 'vr', 'vr_6_sma', 'wr_10', 'wr_6']\n    # code     cr cr-ma1 cr-ma2 cr-ma3      date\n\n    data_new = concat_guess_data(stock_column, data)\n\n    data_new = data_new.round(2)  # 数据保留2位小数\n\n    # print(data_new.head())\n    print(\"########insert db guess_indicators_daily idx :########:\", idx)\n    try:\n        common.insert_db(data_new, \"guess_indicators_daily\", False, \"`date`,`code`\")\n    except Exception as e:\n        print(\"error :\", e)\n\n\n# 链接guess 数据。\ndef concat_guess_data(stock_column, data):\n    # 使用 trade 填充数据\n    print(\"stock_column:\", stock_column)\n    tmp_dic = {}\n    # 循环增加临时数据。如果要是date，和code，\n    for col in stock_column:\n        if col == 'date':\n            tmp_dic[col] = data[\"date\"]\n        elif col == 'code':\n            tmp_dic[col] = data[\"code\"]\n        else:\n            tmp_dic[col] = data[\"trade\"]\n    # print(\"##########tmp_dic: \", tmp_dic)\n    print(\"########################## BEGIN ##########################\")\n    stock_guess = pd.DataFrame(tmp_dic, index=data.index.values)\n    print(stock_guess.columns.values)\n    # print(stock_guess.head())\n    stock_guess = stock_guess.apply(apply_guess, stock_column=stock_column, axis=1)  # , axis=1)\n    print(stock_guess.head())\n    # stock_guess.astype('float32', copy=False)\n    stock_guess.drop('date', axis=1, inplace=True)  # 删除日期字段，然后和原始数据合并。\n    # print(stock_guess[\"5d\"])\n    data_new = pd.merge(data, stock_guess, on=['code'], how='left')\n    print(\"#############\")\n    return data_new\n\n\n# 带参数透传。\ndef apply_guess(tmp, stock_column):\n    # print(\"apply_guess columns args:\", stock_column)\n    # print(\"apply_guess data :\", type(tmp))\n    date = tmp[\"date\"]\n    code = tmp[\"code\"]\n    date_end = datetime.datetime.strptime(date, \"%Y%m%d\")\n    date_start = (date_end + datetime.timedelta(days=-100)).strftime(\"%Y-%m-%d\")\n    date_end = date_end.strftime(\"%Y-%m-%d\")\n    # print(code, date_start, date_end)\n    # open, high, close, low, volume, price_change, p_change, ma5, ma10, ma20, v_ma5, v_ma10, v_ma20, turnover\n    # 使用缓存方法。加快计算速度。\n    stock = common.get_hist_data_cache(code, date_start, date_end)\n    # 设置返回数组。\n    stock_data_list = []\n    stock_name_list = []\n    # 增加空判断，如果是空返回 0 数据。\n    if stock is None:\n        for col in stock_column:\n            if col == 'date':\n                stock_data_list.append(date)\n                stock_name_list.append('date')\n            elif col == 'code':\n                stock_data_list.append(code)\n                stock_name_list.append('code')\n            else:\n                stock_data_list.append(0)\n                stock_name_list.append(col)\n        return pd.Series(stock_data_list, index=stock_name_list)\n\n    # print(stock.head())\n    # open  high  close   low     volume\n    # stock = pd.DataFrame({\"close\": stock[\"close\"]}, index=stock.index.values)\n    stock = stock.sort_index(0)  # 将数据按照日期排序下。\n\n    stock[\"date\"] = stock.index.values  # 增加日期列。\n    stock = stock.sort_index(0)  # 将数据按照日期排序下。\n    # print(stock) [186 rows x 14 columns]\n    # 初始化统计类\n    # stockStat = stockstats.StockDataFrame.retype(pd.read_csv('002032.csv'))\n    stockStat = stockstats.StockDataFrame.retype(stock)\n\n    print(\"########################## print result ##########################\")\n    for col in stock_column:\n        if col == 'date':\n            stock_data_list.append(date)\n            stock_name_list.append('date')\n        elif col == 'code':\n            stock_data_list.append(code)\n            stock_name_list.append('code')\n        else:\n            # 将数据的最后一个返回。\n            tmp_val = stockStat[col].tail(1).values[0]\n            if np.isinf(tmp_val):  # 解决值中存在INF问题。\n                tmp_val = 0\n            if np.isnan(tmp_val):  # 解决值中存在NaN问题。\n                tmp_val = 0\n            # print(\"col name : \", col, tmp_val)\n            stock_data_list.append(tmp_val)\n            stock_name_list.append(col)\n    # print(stock_data_list)\n    return pd.Series(stock_data_list, index=stock_name_list)\n\n\n# print(stock[\"mov_vol\"].tail())\n# print(stock[\"return\"].tail())\n# print(\"stock[10d].tail(1)\", stock[\"10d\"].tail(1).values[0])\n# 10d    20d  5-10d  5-20d     5d    60d    code      date  mov_vol  return\n# tmp = list([stock[\"10d\"].tail(1).values[0], stock[\"20d\"].tail(1).values[0], stock[\"5-10d\"].tail(1).values[0],\n#             stock[\"5-20d\"].tail(1).values[0], stock[\"5d\"].tail(1).values[0], stock[\"60d\"].tail(1).values[0],\n#             code, date, stock[\"mov_vol\"].tail(1).values[0], stock[\"return\"].tail(1).values[0]])\n# # print(tmp)\n# return tmp\n\n\n# main函数入口\nif __name__ == '__main__':\n    # 使用方法传递。\n    tmp_datetime = common.run_with_args(stat_all_batch)\n    # 二次筛选数据。直接计算买卖股票数据。\n    tmp_datetime = common.run_with_args(stat_all_lite_buy)\n    tmp_datetime = common.run_with_args(stat_all_lite_sell)\n\n\n####################### 老方法，弃用了。#######################\ndef stat_index_all_no_use(tmp_datetime):\n    datetime_str = (tmp_datetime).strftime(\"%Y-%m-%d\")\n    datetime_int = (tmp_datetime).strftime(\"%Y%m%d\")\n    print(\"datetime_str:\", datetime_str)\n    print(\"datetime_int:\", datetime_int)\n\n    # 查询今日满足股票数据。剔除数据：创业板股票数据，中小板股票数据，所有st股票\n    # #`code` not like '002%' and `code` not like '300%'  and `name` not like '%st%'\n    sql_1 = \"\"\" \n            SELECT `date`, `code`, `name`, `changepercent`, `trade`, `open`, `high`, `low`, \n                `settlement`, `volume`, `turnoverratio`, `amount`, `per`, `pb`, `mktcap`, `nmc` \n            FROM stock_data.ts_today_all WHERE `date` = %s and `trade` > 0 and `open` > 0 and trade <= 20 \n                and `code` not like %s and `code` not like %s and `name` not like %s\n            \"\"\"\n    print(sql_1)\n    data = pd.read_sql(sql=sql_1, con=common.engine(), params=[datetime_int, '002%', '300%', '%st%'])\n    data = data.drop_duplicates(subset=\"code\", keep=\"last\")\n    print(\"########data[trade]########:\", len(data))\n    # print(data[\"trade\"])\n\n    # 1), n天涨跌百分百计算\n    # open price change (in percent) between today and the day before yesterday ‘r’ stands for rate.\n    # stock[‘close_-2_r’]\n    # 可以看到，-n天数据和今天数据的百分比。\n    stock_column = ['close_-1_r', 'close_-2_r', 'code', 'date']  # close_-1_r  close_-2_r    code      date\n    data_new = concat_guess_data(stock_column, data)\n\n    # 2), CR指标\n    # http://wiki.mbalib.com/wiki/CR%E6%8C%87%E6%A0%87 价格动量指标\n    # CR跌穿a、b、c、d四条线，再由低点向上爬升160时，为短线获利的一个良机，应适当卖出股票。\n    # CR跌至40以下时，是建仓良机。而CR高于300~400时，应注意适当减仓。\n    stock_column = ['code', 'cr', 'cr-ma1', 'cr-ma2', 'cr-ma3', 'date']  # code     cr cr-ma1 cr-ma2 cr-ma3      date\n    data_new = concat_guess_data(stock_column, data_new)\n\n    # 3), KDJ指标\n    # http://wiki.mbalib.com/wiki/%E9%9A%8F%E6%9C%BA%E6%8C%87%E6%A0%87\n    # 随机指标(KDJ)一般是根据统计学的原理，通过一个特定的周期（常为9日、9周等）内出现过的最高价、\n    # 最低价及最后一个计算周期的收盘价及这三者之间的比例关系，来计算最后一个计算周期的未成熟随机值RSV，\n    # 然后根据平滑移动平均线的方法来计算K值、D值与J值，并绘成曲线图来研判股票走势。\n    # （3）在使用中，常有J线的指标，即3乘以K值减2乘以D值（3K－2D＝J），其目的是求出K值与D值的最大乖离程度，\n    # 以领先KD值找出底部和头部。J大于100时为超买，小于10时为超卖。\n    stock_column = ['code', 'date', 'kdjd', 'kdjj', 'kdjk']  # code      date   kdjd   kdjj   kdjk\n    data_new = concat_guess_data(stock_column, data_new)\n\n    # 4), MACD指标\n    # http://wiki.mbalib.com/wiki/MACD\n    # 平滑异同移动平均线(Moving Average Convergence Divergence，简称MACD指标)，也称移动平均聚散指标\n    # MACD 则可发挥其应有的功能，但当市场呈牛皮盘整格局，股价不上不下时，MACD买卖讯号较不明显。\n    # 当用MACD作分析时，亦可运用其他的技术分析指标如短期 K，D图形作为辅助工具，而且也可对买卖讯号作双重的确认。\n    stock_column = ['code', 'date', 'macd', 'macdh', 'macds']  # code      date   macd  macdh  macds\n    data_new = concat_guess_data(stock_column, data_new)\n\n    # 5), BOLL指标\n    # http://wiki.mbalib.com/wiki/BOLL\n    # 布林线指标(Bollinger Bands)\n    stock_column = ['boll', 'boll_lb', 'boll_ub', 'code', 'date']  # boll boll_lb boll_ub    code      date\n    data_new = concat_guess_data(stock_column, data_new)\n\n    # 6), RSI指标\n    # http://wiki.mbalib.com/wiki/RSI\n    # 相对强弱指标（Relative Strength Index，简称RSI），也称相对强弱指数、相对力度指数\n    # （2）强弱指标保持高于50表示为强势市场，反之低于50表示为弱势市场。\n    # （3）强弱指标多在70与30之间波动。当六日指标上升到达80时，表示股市已有超买现象，\n    # 如果一旦继续上升，超过90以上时，则表示已到严重超买的警戒区，股价已形成头部，极可能在短期内反转回转。\n    stock_column = ['code', 'date', 'rsi_12', 'rsi_6']  # code      date rsi_12  rsi_6\n    data_new = concat_guess_data(stock_column, data_new)\n\n    # 7), W%R指标\n    # http://wiki.mbalib.com/wiki/%E5%A8%81%E5%BB%89%E6%8C%87%E6%A0%87\n    # 威廉指数（Williams%Rate）该指数是利用摆动点来度量市场的超买超卖现象。\n    stock_column = ['code', 'date', 'wr_10', 'wr_6']  # code      date  wr_10   wr_6\n    data_new = concat_guess_data(stock_column, data_new)\n\n    # 8), CCI指标\n    # http://wiki.mbalib.com/wiki/%E9%A1%BA%E5%8A%BF%E6%8C%87%E6%A0%87\n    # 顺势指标又叫CCI指标，其英文全称为“Commodity Channel Index”，\n    # 是由美国股市分析家唐纳德·蓝伯特（Donald Lambert）所创造的，是一种重点研判股价偏离度的股市分析工具。\n    # 1、当CCI指标从下向上突破﹢100线而进入非常态区间时，表明股价脱离常态而进入异常波动阶段，\n    # 中短线应及时买入，如果有比较大的成交量配合，买入信号则更为可靠。\n    # 　　2、当CCI指标从上向下突破﹣100线而进入另一个非常态区间时，表明股价的盘整阶段已经结束，\n    # 将进入一个比较长的寻底过程，投资者应以持币观望为主。\n    # CCI, default to 14 days\n    stock_column = ['cci', 'cci_20', 'code', 'date']  # cci cci_20 code date\n    data_new = concat_guess_data(stock_column, data_new)\n\n    # 9), TR、ATR指标\n    # http://wiki.mbalib.com/wiki/%E5%9D%87%E5%B9%85%E6%8C%87%E6%A0%87\n    # 均幅指标（Average True Ranger,ATR）\n    # 均幅指标（ATR）是取一定时间周期内的股价波动幅度的移动平均值，主要用于研判买卖时机。\n    stock_column = ['cci', 'cci_20', 'code', 'date']  # cci cci_20 code date\n    data_new = concat_guess_data(stock_column, data_new)\n\n    # 10), DMA指标\n    # http://wiki.mbalib.com/wiki/DMA\n    # 　DMA指标（Different of Moving Average）又叫平行线差指标，是目前股市分析技术指标中的一种中短期指标，它常用于大盘指数和个股的研判。\n    # DMA, difference of 10 and 50 moving average\n    # stock[‘dma’]\n    stock_column = ['code', 'date', 'dma']  # code    date       dma\n    data_new = concat_guess_data(stock_column, data_new)\n\n    # 11), DMI，+DI，-DI，DX，ADX，ADXR指标\n    # http://wiki.mbalib.com/wiki/DMI\n    # 动向指数Directional Movement Index,DMI）\n    # http://wiki.mbalib.com/wiki/ADX\n    # 平均趋向指标（Average Directional Indicator，简称ADX）\n    # http://wiki.mbalib.com/wiki/%E5%B9%B3%E5%9D%87%E6%96%B9%E5%90%91%E6%8C%87%E6%95%B0%E8%AF%84%E4%BC%B0\n    # 平均方向指数评估（ADXR）实际是今日ADX与前面某一日的ADX的平均值。ADXR在高位与ADX同步下滑，可以增加对ADX已经调头的尽早确认。\n    # ADXR是ADX的附属产品，只能发出一种辅助和肯定的讯号，并非入市的指标，而只需同时配合动向指标(DMI)的趋势才可作出买卖策略。\n    # 在应用时，应以ADX为主，ADXR为辅。\n    stock_column = ['adx', 'adxr', 'code', 'date', 'dx', 'mdi',\n                    'pdi']  # adx   adxr    code      date     dx    mdi    pdi\n    data_new = concat_guess_data(stock_column, data_new)\n\n    # 12), TRIX，MATRIX指标\n    # http://wiki.mbalib.com/wiki/TRIX\n    # TRIX指标又叫三重指数平滑移动平均指标（Triple Exponentially Smoothed Average）\n    stock_column = ['code', 'date', 'trix', 'trix_9_sma']  # code      date    trix trix_9_sma\n    data_new = concat_guess_data(stock_column, data_new)\n\n    # 13), VR，MAVR指标\n    # http://wiki.mbalib.com/wiki/%E6%88%90%E4%BA%A4%E9%87%8F%E6%AF%94%E7%8E%87\n    # 成交量比率（Volumn Ratio，VR）（简称VR），是一项通过分析股价上升日成交额（或成交量，下同）与股价下降日成交额比值，\n    # 从而掌握市场买卖气势的中期技术指标。\n    stock_column = ['code', 'date', 'vr', 'vr_6_sma']  # code      date          vr    vr_6_sma\n    data_new = concat_guess_data(stock_column, data_new)\n\n    data_new = data_new.round(2)  # 数据保留2位小数\n\n    # 删除老数据。\n    del_sql = \" DELETE FROM `stock_data`.`guess_indicators_daily` WHERE `date`= %s \" % datetime_int\n    common.insert(del_sql)\n\n    # print(data_new.head())\n    # data_new[\"down_rate\"] = (data_new[\"trade\"] - data_new[\"wave_mean\"]) / data_new[\"wave_base\"]\n    common.insert_db(data_new, \"guess_indicators_daily\", False, \"`date`,`code`\")\n\n    # 进行左连接.\n    # tmp = pd.merge(tmp, tmp2, on=['company_id'], how='left')\n"
  },
  {
    "path": "backend/jobs/quarter_job.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\n\nimport libs.common as common\nimport sys\nimport time\nimport pandas as pd\nimport tushare as ts\nfrom sqlalchemy.types import NVARCHAR\nfrom sqlalchemy import inspect\nimport datetime\n\n\n# 增加一个新quarter列，用来存储季度信息。\ndef concat_quarter(year, quarter, data_array):\n    print(len(data_array))\n    quarter_str = str(year) + str(\"%02d\" % quarter)  # 格式化季度数据。2位。\n    # 增加到列。\n    quarter_col = pd.DataFrame([quarter_str for _ in range(len(data_array))], columns=[\"quarter\"])\n    return pd.concat([quarter_col, data_array], axis=1)\n\n\n#############################基本面数据 http://tushare.org/fundamental.html\ndef stat_all(tmp_datetime):\n    # 返回 31 天前的数据，做上个季度数据统计。\n    tmp_datetime_1month = tmp_datetime + datetime.timedelta(days=-31)\n    year = int((tmp_datetime_1month).strftime(\"%Y\"))\n    quarter = int(pd.Timestamp(tmp_datetime_1month).quarter)  # 获得上个季度的数据。\n    print(\"############ year %d, quarter %d\", year, quarter)\n    # 业绩报告（主表）\n    data = ts.get_report_data(year, quarter)\n    # 增加季度字段。\n    data = concat_quarter(year, quarter, data)\n    # 处理重复数据，保存最新一条数据。最后一步处理，否则concat有问题。\n    data = data.drop_duplicates(subset=\"code\", keep=\"last\")\n    # 插入数据库。\n    common.insert_db(data, \"ts_report_data\", False, \"`quarter`,`code`\")\n\n    # 盈利能力\n    data = ts.get_profit_data(year, quarter)\n    # 增加季度字段。\n    data = concat_quarter(year, quarter, data)\n    # 处理重复数据，保存最新一条数据。\n    data = data.drop_duplicates(subset=\"code\", keep=\"last\")\n    # 插入数据库。\n    common.insert_db(data, \"ts_profit_data\", False, \"`quarter`,`code`\")\n\n    # 营运能力\n    data = ts.get_operation_data(year, quarter)\n    # 增加季度字段。\n    data = concat_quarter(year, quarter, data)\n    # 处理重复数据，保存最新一条数据。最后一步处理，否则concat有问题。\n    data = data.drop_duplicates(subset=\"code\", keep=\"last\")\n    # 插入数据库。\n    common.insert_db(data, \"ts_operation_data\", False, \"`quarter`,`code`\")\n\n    # 成长能力\n    data = ts.get_growth_data(year, quarter)\n    # 增加季度字段。\n    data = concat_quarter(year, quarter, data)\n    # 处理重复数据，保存最新一条数据。最后一步处理，否则concat有问题。\n    data = data.drop_duplicates(subset=\"code\", keep=\"last\")\n    # 插入数据库。\n    common.insert_db(data, \"ts_growth_data\", False, \"`quarter`,`code`\")\n\n    # 偿债能力\n    data = ts.get_debtpaying_data(year, quarter)\n    # 增加季度字段。\n    data = concat_quarter(year, quarter, data)\n    # 处理重复数据，保存最新一条数据。最后一步处理，否则concat有问题。\n    data = data.drop_duplicates(subset=\"code\", keep=\"last\")\n    # 插入数据库。\n    common.insert_db(data, \"ts_debtpaying_data\", False, \"`quarter`,`code`\")\n\n    # 现金流量\n    data = ts.get_cashflow_data(year, quarter)\n    # 增加季度字段。\n    data = concat_quarter(year, quarter, data)\n    # 处理重复数据，保存最新一条数据。最后一步处理，否则concat有问题。\n    data = data.drop_duplicates(subset=\"code\", keep=\"last\")\n    # 插入数据库。\n    common.insert_db(data, \"ts_cashflow_data\", False, \"`quarter`,`code`\")\n\n\n# main函数入口\nif __name__ == '__main__':\n    # 使用方法传递。\n    tmp_datetime = common.run_with_args(stat_all)\n"
  },
  {
    "path": "backend/jobs/restart_mnist_serving.sh",
    "content": "#!/bin/sh\n\nps -ef | grep 'tensorflow_model_server' | grep -v grep | awk '{print$2}' | xargs kill -9\necho \"\" > /data/logs/mnist_serving.log\nnohup tensorflow_model_server --model_name=mnist --model_base_path=/data/mnist_model >> /data/logs/mnist_serving.log &"
  },
  {
    "path": "backend/jobs/restart_web.sh",
    "content": "#!/bin/sh\n\nps -ef | grep python3 | grep '/data/stock/web/main.py' | awk '{print$2}' | xargs kill -9\necho \"restart web ... \" > /data/logs/tornado.log\n"
  },
  {
    "path": "backend/jobs/run_cron.sh",
    "content": "#!/bin/sh\n\nexport PYTHONIOENCODING=utf-8\nexport LANG=zh_CN.UTF-8\nexport PYTHONPATH=/data/stock\nexport LC_CTYPE=zh_CN.UTF-8\n\nmkdir -p /data/logs/tensorflow\n\n\n\nDATE=`date +%Y-%m-%d:%H:%M:%S`\n\necho $DATE >> /data/logs/run_cron.log\n\n# 解决定时任务不启动问题，因为权限导致\nchmod 755 /etc/cron.minutely/* && chmod 755 /etc/cron.hourly/*\nchmod 755 /etc/cron.daily/* && chmod 755 /etc/cron.monthly/*\n\n# 配置文件每次都设置权限\nchmod 600 /var/spool/cron/crontabs/root\nchown root:root /var/spool/cron/crontabs/root\n\n#启动cron服务。在前台\n/usr/sbin/cron -f"
  },
  {
    "path": "backend/jobs/run_init.sh",
    "content": "#!/bin/sh\n\nexport PYTHONIOENCODING=utf-8\nexport LANG=zh_CN.UTF-8\nexport PYTHONPATH=/data/stock\nexport LC_CTYPE=zh_CN.UTF-8\n\nmkdir -p /data/logs/tensorflow\n\n\n\nDATE=`date +%Y-%m-%d:%H:%M:%S`\n\necho $DATE >> /data/logs/run_init.log\n\necho \"wait 120 second , mysqldb is starting .\" >> /data/logs/run_init.log\nsleep 120\n\n/usr/local/bin/python3 /data/stock/jobs/basic_job.py  >> /data/logs/run_init.log\n\n# https://stackoverflow.com/questions/27771781/how-can-i-access-docker-set-environment-variables-from-a-cron-job\n# 解决环境变量输出问题。\nprintenv | grep -v \"no_proxy\" >> /etc/environment\n\n# 第一次后台执行日数据。\nnohup bash /data/stock/jobs/cron.daily/run_daily &\n\n#防止 supervisor 重复执行\nsleep 999999d"
  },
  {
    "path": "backend/jobs/run_jupyter.sh",
    "content": "#!/bin/sh\n\nmkdir -p /data/notebooks\n\n/usr/local/bin/jupyter notebook --NotebookApp.notebook_dir='/data/notebooks'  --ip=0.0.0.0 \\\n    --allow-root >> /data/logs/jupyter-notebook.log\n"
  },
  {
    "path": "backend/jobs/run_web.sh",
    "content": "#!/bin/bash\n\nexport PYTHONIOENCODING=utf-8\nexport LANG=zh_CN.UTF-8\nexport PYTHONPATH=/data/stock\nexport LC_CTYPE=zh_CN.UTF-8\n\necho \"\" > /data/logs/web.log\n/usr/local/bin/python3 /data/stock/web/main.py -log_file_prefix=/data/logs/web.log"
  },
  {
    "path": "backend/jobs/start_mariadb.sh",
    "content": "#!/bin/sh\n\nDATE=`date +%Y-%m-%d:%H:%M:%S`\necho $DATE\n\nif [ ! -d \"/data/mariadb\" ]; then\n    mkdir -p /data/mariadb\n    /usr/bin/mysql_install_db\nfi\n\n\n/usr/bin/mysqld_safe >> /data/logs/start_mariadb.log"
  },
  {
    "path": "backend/jobs/test_akshare/test_stock_zh_a_daily.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\nimport akshare as ak\nimport libs.common as common\n\nprint(ak.__version__)\n\n# 历史行情数据\n# 日频率\n# 接口: stock_zh_a_daily\n# 目标地址: https://finance.sina.com.cn/realstock/company/sh600006/nc.shtml(示例)\n# 描述: A 股数据是从新浪财经获取的数据, 历史数据按日频率更新; 注意其中的 sh689009 为 CDR, 请 通过 stock_zh_a_cdr_daily 接口获取\n# 限量: 单次返回指定 A 股上市公司指定日期间的历史行情日频率数据\n# adjust=\"\"; 默认为空: 返回不复权的数据; qfq: 返回前复权后的数据; hfq: 返回后复权后的数据;\n\nstock_zh_a_daily_qfq_df = ak.stock_zh_a_daily(symbol=\"sz000002\", adjust=\"\")\nprint(stock_zh_a_daily_qfq_df)\n\nstock_zh_a_daily_qfq_df = ak.stock_zh_a_daily(symbol=\"sz000002\", start_date=\"20200101\", end_date=\"20210101\", adjust=\"\")\nprint(stock_zh_a_daily_qfq_df)\n\n# 插入到 MySQL 数据库中\ncommon.insert_db(stock_zh_a_daily_qfq_df, \"stock_zh_a_daily\", True, \"`symbol`\")\n\n\n\n"
  },
  {
    "path": "backend/jobs/test_akshare/test_stock_zh_a_spot.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\nimport akshare as ak\nimport libs.common as common\n\nprint(ak.__version__)\n\n# 实时行情数据\n# 接口: stock_zh_a_spot\n# 目标地址: http://vip.stock.finance.sina.com.cn/mkt/#hs_a\n# 描述: A 股数据是从新浪财经获取的数据, 重复运行本函数会被新浪暂时封 IP, 建议增加时间间隔\n# 限量: 单次返回所有 A 股上市公司的实时行情数据\n\nstock_zh_a_spot_df = ak.stock_zh_a_spot()\nprint(stock_zh_a_spot_df)\n\n# 插入到 MySQL 数据库中\ncommon.insert_db(stock_zh_a_spot_df, \"stock_zh_a_spot\", True, \"`symbol`\")\n"
  },
  {
    "path": "backend/jobs/test_akshare/test_stock_zh_index_spot.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\nimport akshare as ak\nimport libs.common as common\n\nprint(ak.__version__)\n\n#stock_sse_summary_df = ak.stock_sse_summary()\n#print(stock_sse_summary_df)\n\n# 接口: stock_zh_index_spot\n# 目标地址: http://vip.stock.finance.sina.com.cn/mkt/#hs_s\n# 描述: 中国股票指数数据, 注意该股票指数指新浪提供的国内股票指数\n# 限量: 单次返回所有指数的实时行情数据\nstock_zh_index_spot_df = ak.stock_zh_index_spot()\nprint(stock_zh_index_spot_df)\n\n# 插入到 MySQL 数据库中\ncommon.insert_db(stock_zh_index_spot_df, \"stock_zh_index_spot_df\", True, \"`symbol`\")\n"
  },
  {
    "path": "backend/libs/common.py",
    "content": "#!/usr/local/bin/python\n# -*- coding: utf-8 -*-\n\n# apk add py-mysqldb or\n\nimport platform\nimport datetime\nimport time\nimport sys\nimport os\nimport MySQLdb\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.types import NVARCHAR\nfrom sqlalchemy import inspect\nimport pandas as pd\nimport traceback\nimport akshare as ak\n\n# 使用环境变量获得数据库。兼容开发模式可docker模式。\nMYSQL_HOST = os.environ.get('MYSQL_HOST') if (os.environ.get('MYSQL_HOST') != None) else \"mysqldb\"\nMYSQL_USER = os.environ.get('MYSQL_USER') if (os.environ.get('MYSQL_USER') != None) else \"root\"\nMYSQL_PWD = os.environ.get('MYSQL_PWD') if (os.environ.get('MYSQL_PWD') != None) else \"mysqldb\"\nMYSQL_DB = os.environ.get('MYSQL_DB') if (os.environ.get('MYSQL_DB') != None) else \"stock_data\"\nMYSQL_PORT = os.environ.get('MYSQL_PORT') if (os.environ.get('MYSQL_PORT') != None) else \"3306\"\n\nprint(\"MYSQL_HOST :\", MYSQL_HOST, \",MYSQL_USER :\", MYSQL_USER, \",MYSQL_DB :\", MYSQL_DB)\nMYSQL_CONN_URL = \"mysql+mysqldb://\" + MYSQL_USER + \":\" + MYSQL_PWD + \"@\" + MYSQL_HOST + \":\" + MYSQL_PORT + \"/\" + MYSQL_DB + \"?charset=utf8mb4\"\nprint(\"MYSQL_CONN_URL :\", MYSQL_CONN_URL)\n\n__version__ = \"2.0.0\"\n# 每次发布时候更新。\n\n# https://docs.sqlalchemy.org/en/20/errors.html#error-e3q8\n# \ndef engine():\n    engine = create_engine(MYSQL_CONN_URL, pool_size=10, max_overflow=20)\n        #encoding='utf8', convert_unicode=True)\n    return engine\n\ndef engine_to_db(to_db):\n    MYSQL_CONN_URL_NEW = \"mysql+mysqldb://\" + MYSQL_USER + \":\" + MYSQL_PWD + \"@\" + MYSQL_HOST + \":\" + MYSQL_PORT + \"/\" + to_db + \"?charset=utf8mb4\"\n    engine = create_engine(MYSQL_CONN_URL_NEW, pool_size=10, max_overflow=20)\n        #encoding='utf8', convert_unicode=True)\n    return engine\n\n# 通过数据库链接 engine。\ndef conn():\n    try:\n        db = MySQLdb.connect(MYSQL_HOST, MYSQL_USER, MYSQL_PWD, MYSQL_DB, charset=\"utf8\")\n        # db.autocommit = True\n    except Exception as e:\n        print(\"conn error :\", e)\n    db.autocommit(on=True)\n    return db.cursor()\n\n\n# 定义通用方法函数，插入数据库表，并创建数据库主键，保证重跑数据的时候索引唯一。\ndef insert_db(data, table_name, write_index, primary_keys):\n    # 插入默认的数据库。\n    insert_other_db(MYSQL_DB, data, table_name, write_index, primary_keys)\n\n\n# 增加一个插入到其他数据库的方法。\ndef insert_other_db(to_db, data, table_name, write_index, primary_keys):\n    # 定义engine\n    engine_mysql = engine_to_db(to_db)\n    # 使用 http://docs.sqlalchemy.org/en/latest/core/reflection.html\n    # 使用检查检查数据库表是否有主键。\n    insp = inspect(engine_mysql)\n    col_name_list = data.columns.tolist()\n    # 如果有索引，把索引增加到varchar上面。\n    if write_index:\n        # 插入到第一个位置：\n        col_name_list.insert(0, data.index.name)\n    print(col_name_list)\n    data.to_sql(name=table_name, con=engine_mysql, schema=to_db, if_exists='append',\n                dtype={col_name: NVARCHAR(length=255) for col_name in col_name_list}, index=write_index)\n\n    # print(insp.get_pk_constraint(table_name))\n    # print()\n    # print(type(insp))\n    # 判断是否存在主键\n    if insp.get_pk_constraint(table_name)['constrained_columns'] == []:\n        with engine_mysql.connect() as con:\n            # 执行数据库插入数据。\n            try:\n                con.execute('ALTER TABLE `%s` ADD PRIMARY KEY (%s);' % (table_name, primary_keys))\n            except  Exception as e:\n                print(\"################## ADD PRIMARY KEY ERROR :\", e)\n\n\n\n\n# 插入数据。\ndef insert(sql, params=()):\n    with conn() as db:\n        print(\"insert sql:\" + sql)\n        try:\n            db.execute(sql, params)\n        except  Exception as e:\n            print(\"error :\", e)\n\n\n# 查询数据\ndef select(sql, params=()):\n    with conn() as db:\n        print(\"select sql:\" + sql)\n        try:\n            db.execute(sql, params)\n        except  Exception as e:\n            print(\"error :\", e)\n        result = db.fetchall()\n        return result\n\n\n# 计算数量\ndef select_count(sql, params=()):\n    with conn() as db:\n        print(\"select sql:\" + sql)\n        try:\n            db.execute(sql, params)\n        except  Exception as e:\n            print(\"error :\", e)\n        result = db.fetchall()\n        # 只有一个数组中的第一个数据\n        if len(result) == 1:\n            return int(result[0][0])\n        else:\n            return 0\n\n\n# 通用函数。获得日期参数。\ndef run_with_args(run_fun):\n    tmp_datetime_show = datetime.datetime.now()  # 修改成默认是当日执行 + datetime.timedelta()\n    tmp_hour_int = int(tmp_datetime_show.strftime(\"%H\"))\n    if tmp_hour_int < 12 :\n        # 判断如果是每天 中午 12 点之前运行，跑昨天的数据。\n        tmp_datetime_show = (tmp_datetime_show + datetime.timedelta(days=-1))\n    tmp_datetime_str = tmp_datetime_show.strftime(\"%Y-%m-%d %H:%M:%S.%f\")\n    print(\"\\n######################### hour_int %d \" % tmp_hour_int)\n    str_db = \"MYSQL_HOST :\" + MYSQL_HOST + \", MYSQL_USER :\" + MYSQL_USER + \", MYSQL_DB :\" + MYSQL_DB\n    print(\"\\n######################### \" + str_db + \"  ######################### \")\n    print(\"\\n######################### begin run %s %s  #########################\" % (run_fun, tmp_datetime_str))\n    start = time.time()\n    # 要支持数据重跑机制，将日期传入。循环次数\n    if len(sys.argv) == 3:\n        # python xxx.py 2017-07-01 10\n        tmp_year, tmp_month, tmp_day = sys.argv[1].split(\"-\")\n        loop = int(sys.argv[2])\n        tmp_datetime = datetime.datetime(int(tmp_year), int(tmp_month), int(tmp_day))\n        for i in range(0, loop):\n            # 循环插入多次数据，重复跑历史数据使用。\n            # time.sleep(5)\n            tmp_datetime_new = tmp_datetime + datetime.timedelta(days=i)\n            try:\n                run_fun(tmp_datetime_new)\n            except Exception as e:\n                print(\"error :\", e)\n                traceback.print_exc()\n    elif len(sys.argv) == 2:\n        # python xxx.py 2017-07-01\n        tmp_year, tmp_month, tmp_day = sys.argv[1].split(\"-\")\n        tmp_datetime = datetime.datetime(int(tmp_year), int(tmp_month), int(tmp_day))\n        try:\n            run_fun(tmp_datetime)\n        except Exception as e:\n            print(\"error :\", e)\n            traceback.print_exc()\n    else:\n        # tmp_datetime = datetime.datetime.now() + datetime.timedelta(days=-1)\n        try:\n            run_fun(tmp_datetime_show)  # 使用当前时间\n        except Exception as e:\n            print(\"error :\", e)\n            traceback.print_exc()\n    print(\"######################### finish %s , use time: %s #########################\" % (\n        tmp_datetime_str, time.time() - start))\n\n\n# 设置基础目录，每次加载使用。\nbash_stock_tmp = \"/data/cache/hist_data_cache/%s/%s/\"\nif not os.path.exists(bash_stock_tmp):\n    os.makedirs(bash_stock_tmp)  # 创建多个文件夹结构。\n    print(\"######################### init tmp dir #########################\")\n\n\n# 增加读取股票缓存方法。加快处理速度。\ndef get_hist_data_cache(code, date_start, date_end):\n    cache_dir = bash_stock_tmp % (date_end[0:7], date_end)\n    # 如果没有文件夹创建一个。月文件夹和日文件夹。方便删除。\n    # print(\"cache_dir:\", cache_dir)\n    if not os.path.exists(cache_dir):\n        os.makedirs(cache_dir)\n    cache_file = cache_dir + \"%s^%s.gzip.pickle\" % (date_end, code)\n    # 如果缓存存在就直接返回缓存数据。压缩方式。\n    if os.path.isfile(cache_file):\n        print(\"######### read from cache #########\", cache_file)\n        return pd.read_pickle(cache_file, compression=\"gzip\")\n    else:\n        # https://akshare.akfamily.xyz/data/index/index.html#id4\n        # 获取历史行情，em\n        #stock = ak.stock_zh_a_hist(symbol= code, start_date=date_start, \n        #    end_date=date_end, adjust=\"\")\n        code = gp_type_szsh(code)+ code\n        print(\"######### get data, write cache #########\", code, date_start, date_end)\n\n        stock = ak.stock_zh_index_daily_em(symbol= code, \n            start_date=date_start.replace(\"-\", \"\"), end_date=date_end.replace(\"-\", \"\"))\n        print(stock)\n        if stock is None or stock.empty:\n            return None\n        \n        stock.columns = ['date', 'open', 'close', 'high', 'low', 'volume', 'amount']\n        # 数据返回的是带 0 列是索引，第一列是 date 日期\n        #  date   open  close   high    low   volume        amount\n        #  0   2024-09-20   9.81   9.90   9.90   9.78   797297  7.851212e+08\n\n        stock.set_index('date', inplace=True)\n        #stock = stock.sort_index(0)  # 将数据按照日期排序下。\n        print(stock)\n        stock.to_pickle(cache_file, compression=\"gzip\")\n        return stock\n\n\n# 沪市股票包含上证主板和科创板和B股：沪市主板股票代码是60开头、科创板股票代码是688开头、B股代码900开头。\n# 深市股票包含主板、中小板、创业板和B股：深市主板股票代码是000开头、中小板股票代码002开头、创业板300开头、B股代码200开头\n# print(gp_type_szsh('002340'))\n# \ndef gp_type_szsh(gp):\n    if gp.find('60',0,3)==0:\n        gp_type='sh'\n    elif gp.find('688',0,4)==0:\n        gp_type='sh'\n    elif gp.find('900',0,4)==0:\n        gp_type='sh'\n    elif gp.find('00',0,3)==0:\n        gp_type='sz'\n    elif gp.find('300',0,4)==0:\n        gp_type='sz'\n    elif gp.find('200',0,4)==0:\n        gp_type='sz'\n    return gp_type\n"
  },
  {
    "path": "backend/libs/stock_web_dic.py",
    "content": "#!/usr/local/bin/python\n# -*- coding: utf-8 -*-\n\nclass StockWebData:\n    def __init__(self, mode, type, name, table_name, columns, column_names, primary_key, order_by):\n        self.mode = mode  # 模式，query，editor 查询和编辑模式\n        self.type = type\n        self.name = name\n        self.table_name = table_name\n        self.columns = columns\n        self.column_names = column_names\n        self.primary_key = primary_key\n        self.order_by = order_by\n        if mode == \"query\":\n            self.url = \"/stock/data?table_name=\" + self.table_name\n        elif mode == \"editor\":\n            self.url = \"/data/editor?table_name=\" + self.table_name\n\n\nSTOCK_WEB_DATA_LIST = []\n\n\n# https://www.akshare.xyz/zh_CN/latest/data/stock/stock.html#id1\n# 限量: 单次返回所有 A 股上市公司的实时行情数据\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"1，股票基本数据\",\n        name=\"每日股票数据-东财\",\n        table_name=\"stock_zh_a_spot_em\",\n        columns= ['date', 'code', 'name', 'last_price', 'change_percent', 'change_amount',\n        'volume', 'turnover', 'amplitude', 'high', 'low', 'open', 'closed', 'volume_ratio',\n        'turnover_rate', 'pe_ratio','pb_ratio', 'market_cap','circulating_market_cap','rise_speed',\n        'change_5min', 'change_ercent_60day','ytd_change_percent'] ,\n        column_names=['日期','代码','名称','最新价','涨跌幅','涨跌额','成交量','成交额',\n                      '振幅','最高','最低','今开','昨收','量比','换手率','动态市盈率',\n                      '市净率', '总市值', '流通市值', '涨速', '5分钟涨跌', '60日涨跌幅', '年初至今涨跌幅'],\n        primary_key=[],\n        order_by=\" code asc \"\n    )\n)\n\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"1，股票基本数据\",\n        name=\"龙虎榜-个股上榜-新浪\",\n        table_name=\"stock_lhb_ggtj_sina\",\n        columns= ['date','code','name','ranking_times','sum_buy','sum_sell','net_amount','buy_seat','sell_seat'],\n        column_names=['日期','代码', '名称', '上榜次数', '累积购买额', '累积卖出额', '净额', '买入席位数', '卖出席位数'],\n        primary_key=[],\n        order_by=\" code asc \"\n    )\n)\n\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"1，股票基本数据\",\n        name=\"数据中心-大宗交易\",\n        table_name=\"stock_dzjy_mrtj\",\n        columns= ['date', 'code', 'name', 'quote_change', 'close_price', 'average_price',\n                                   'overflow_rate', 'trade_number', 'sum_volume', 'sum_turnover',\n                                   'turnover_market_rate'],\n        column_names=['日期', '代码', '名称', '涨跌幅', '收盘价', '成交均价',\n                      '折溢率', '成交笔数', '成交总量', '成交总额',\n                      '成交总额/流通市值'],\n        primary_key=[],\n        order_by=\" code asc \"\n    )\n)\n\n\n\n# 每日股票指标lite猜想买入。\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"2，每日数据猜想\",\n        name=\"股票指标lite猜想买入\",\n        table_name=\"guess_indicators_lite_buy_daily\",\n        # columns=['date', 'code', 'name', 'latest_price', 'quote_change', 'ups_downs', 'volume', 'turnover',\n        #          'amplitude', 'high', 'low', 'open', 'closed', 'volume_ratio', 'turnover_rate', 'pe_dynamic', 'pb',\n        #          'kdjj', 'rsi_6', 'cci'],\n        # column_names=['日期', '代码', '名称', '最新价', '涨跌幅', '涨跌额', '成交量', '成交额',\n        #               '振幅', '最高', '最低', '今开', '昨收', '量比', '换手率', '动态市盈率', '市净率',\n        #               'kdjj', 'rsi_6', 'cci'],\n        columns= ['date', 'code', 'name', 'last_price', 'change_percent', 'change_amount',\n        'volume', 'turnover', 'amplitude', 'high', 'low', 'open', 'closed', 'volume_ratio',\n        'turnover_rate', 'pe_ratio','pb_ratio', 'market_cap','circulating_market_cap','rise_speed',\n        'change_5min', 'change_ercent_60day','ytd_change_percent',\n        'boll', 'boll_lb', 'boll_ub', 'kdjd', 'kdjj', 'kdjk', 'macd', 'macdh',\n         'macds', 'pdi','trix', 'trix_9_sma', 'vr', 'vr_6_sma', 'wr_10', 'wr_6'] ,\n        # 中文说明前面和 1 数据一致。\n        column_names=['日期','代码','名称','最新价','涨跌幅','涨跌额','成交量','成交额',\n                      '振幅','最高','最低','今开','昨收','量比','换手率','动态市盈率',\n                      '市净率', '总市值', '流通市值', '涨速', '5分钟涨跌', '60日涨跌幅', \n                      '年初至今涨跌幅',\n                      'boll', 'boll_lb', 'boll_ub', \n                    'kdjd', 'kdjj', 'kdjk', 'macd', 'macdh', 'macds', 'pdi',\n                    'trix', 'trix_9_sma', 'vr', 'vr_6_sma', 'wr_10', 'wr_6'],\n        primary_key=[],\n        order_by=\" buy_date desc  \"\n    )\n)\n\n# 每日股票指标lite猜想卖出。\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"2，每日数据猜想\",\n        name=\"股票指标lite猜想卖出\",\n        table_name=\"guess_indicators_lite_sell_daily\",\n        # columns=['date', 'code', 'name', 'latest_price', 'quote_change', 'ups_downs', 'volume', 'turnover',\n        #          'amplitude', 'high', 'low', 'open', 'closed', 'volume_ratio', 'turnover_rate', 'pe_dynamic', 'pb',\n        #          'kdjj', 'rsi_6', 'cci'],\n        # column_names=['日期', '代码', '名称', '最新价', '涨跌幅', '涨跌额', '成交量', '成交额',\n        #               '振幅', '最高', '最低', '今开', '昨收', '量比', '换手率', '动态市盈率', '市净率',\n        #               'kdjj', 'rsi_6', 'cci'],\n        columns= ['date', 'code', 'name', 'last_price', 'change_percent', 'change_amount',\n        'volume', 'turnover', 'amplitude', 'high', 'low', 'open', 'closed', 'volume_ratio',\n        'turnover_rate', 'pe_ratio','pb_ratio', 'market_cap','circulating_market_cap','rise_speed',\n        'change_5min', 'change_ercent_60day','ytd_change_percent',\n        'boll', 'boll_lb', 'boll_ub', 'kdjd', 'kdjj', 'kdjk', 'macd', 'macdh',\n         'macds', 'pdi','trix', 'trix_9_sma', 'vr', 'vr_6_sma', 'wr_10', 'wr_6'] ,\n        # 中文说明前面和 1 数据一致。\n        column_names=['日期','代码','名称','最新价','涨跌幅','涨跌额','成交量','成交额',\n                      '振幅','最高','最低','今开','昨收','量比','换手率','动态市盈率',\n                      '市净率', '总市值', '流通市值', '涨速', '5分钟涨跌', '60日涨跌幅', \n                      '年初至今涨跌幅',\n                      'boll', 'boll_lb', 'boll_ub', \n                    'kdjd', 'kdjj', 'kdjk', 'macd', 'macdh', 'macds', 'pdi',\n                    'trix', 'trix_9_sma', 'vr', 'vr_6_sma', 'wr_10', 'wr_6'],\n        primary_key=[],\n        order_by=\" buy_date desc  \"\n    )\n)\n\n# 每日股票指标lite猜想。\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"2，每日数据猜想\",\n        name=\"股票指标猜想原始数据\",\n        table_name=\"guess_indicators_daily\",\n\n        columns= ['date', 'code', 'name', 'last_price', 'change_percent', 'change_amount',\n        'volume', 'turnover', 'amplitude', 'high', 'low', 'open', 'closed', 'volume_ratio',\n        'turnover_rate', 'pe_ratio','pb_ratio', 'market_cap','circulating_market_cap','rise_speed',\n        'change_5min', 'change_ercent_60day','ytd_change_percent',\n        'boll', 'boll_lb', 'boll_ub', 'kdjd', 'kdjj', 'kdjk', 'macd', 'macdh',\n         'macds', 'pdi','trix', 'trix_9_sma', 'vr', 'vr_6_sma', 'wr_10', 'wr_6'] ,\n        # 中文说明前面和 1 数据一致。\n        column_names=['日期','代码','名称','最新价','涨跌幅','涨跌额','成交量','成交额',\n                      '振幅','最高','最低','今开','昨收','量比','换手率','动态市盈率',\n                      '市净率', '总市值', '流通市值', '涨速', '5分钟涨跌', '60日涨跌幅', \n                      '年初至今涨跌幅',\n                      'boll', 'boll_lb', 'boll_ub', \n                    'kdjd', 'kdjj', 'kdjk', 'macd', 'macdh', 'macds', 'pdi',\n                    'trix', 'trix_9_sma', 'vr', 'vr_6_sma', 'wr_10', 'wr_6'],\n\n        # columns=['date','code','name','latest_price','quote_change','ups_downs',\n        #          'adx', 'adxr', 'boll', 'boll_lb', 'boll_ub', 'cci', 'cci_20', 'close_-1_r',\n        #             'close_-2_r', 'code', 'cr', 'cr-ma1', 'cr-ma2', 'cr-ma3', 'date', 'dma', 'dx',\n        #             'kdjd', 'kdjj', 'kdjk', 'macd', 'macdh', 'macds', 'mdi', 'pdi',\n        #             'rsi_12', 'rsi_6', 'trix', 'trix_9_sma', 'vr', 'vr_6_sma', 'wr_10', 'wr_6'],\n        # column_names=['日期','代码','名称','最新价','涨跌幅','涨跌额',\n        #               'adx', 'adxr', 'boll', 'boll_lb', 'boll_ub', 'cci', 'cci_20', 'close_-1_r',\n        #             'close_-2_r', 'code', 'cr', 'cr-ma1', 'cr-ma2', 'cr-ma3', 'date', 'dma', 'dx',\n        #             'kdjd', 'kdjj', 'kdjk', 'macd', 'macdh', 'macds', 'mdi', 'pdi',\n        #             'rsi_12', 'rsi_6', 'trix', 'trix_9_sma', 'vr', 'vr_6_sma', 'wr_10', 'wr_6'],\n        \n        primary_key=[],\n        order_by=' date desc  '\n    )\n)\n\n# \"code\", \"name: pchange\", \"amount\", \"buy\", \"bratio\", \"sell\", \"sratio\", \"reason\", \"date\"\n# 代码 名称 当日涨跌幅 龙虎榜成交额(万) 买入额(万) 买入占总成交比例 卖出额(万) 卖出占总成交比例 上榜原因 日期\n\n\nSTOCK_WEB_DATA_MAP = {}\nWEB_EASTMONEY_URL = \"http://quote.eastmoney.com/%s.html\"\n# 再拼接成Map使用。\nfor tmp in STOCK_WEB_DATA_LIST:\n    # try:\n    #     # 增加columns 字段中的【查看股票】\n    #     tmp_idx = tmp.columns.index(\"code\")\n    #     tmp.column_names.insert(tmp_idx + 1, \"查看股票\")\n    # except  Exception as e:\n    #     print(\"error :\", e)\n\n    STOCK_WEB_DATA_MAP[tmp.table_name] = tmp\n\n    if len(tmp.columns) != len(tmp.column_names):\n        print(u\"error:\", tmp.table_name, \",columns:\", len(tmp.columns), \",column_names:\", len(tmp.column_names))\n"
  },
  {
    "path": "backend/libs/stock_web_dic.py.bk",
    "content": "#!/usr/local/bin/python\n# -*- coding: utf-8 -*-\n\nclass StockWebData:\n    def __init__(self, mode, type, name, table_name, columns, column_names, primary_key, order_by):\n        self.mode = mode  # 模式，query，editor 查询和编辑模式\n        self.type = type\n        self.name = name\n        self.table_name = table_name\n        self.columns = columns\n        self.column_names = column_names\n        self.primary_key = primary_key\n        self.order_by = order_by\n        if mode == \"query\":\n            self.url = \"/stock/data?table_name=\" + self.table_name\n        elif mode == \"editor\":\n            self.url = \"/data/editor?table_name=\" + self.table_name\n\n\nSTOCK_WEB_DATA_LIST = []\n\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"宏观经济数据\",\n        name=\"存款利率\",\n        table_name=\"ts_deposit_rate\",\n        columns=[\"date\", \"deposit_type\", \"rate\"],\n        column_names=[\"日期\", \"存款类型\", \"存款利率\"],\n        primary_key=[],\n        order_by=\" date desc \"\n    )\n)\n\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"宏观经济数据\",\n        name=\"贷款利率\",\n        table_name=\"ts_loan_rate\",\n        columns=[\"date\", \"loan_type\", \"rate\"],\n        column_names=[\"日期\", \"贷款类型\", \"存款利率\"],\n        primary_key=[],\n        order_by=\" date desc \"\n    )\n)\n\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"宏观经济数据\",\n        name=\"存款准备金率\",\n        table_name=\"ts_rrr\",\n        columns=[\"date\", \"before\", \"now\", \"changed\"],\n        column_names=[\"变动日期\", \"调整前存款准备金率(%)\", \"调整后存款准备金率(%)\", \"调整幅度(%)\"],\n        primary_key=[],\n        order_by=\" date desc \"\n    )\n)\n\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"宏观经济数据\",\n        name=\"货币供应量\",\n        table_name=\"ts_money_supply\",\n        columns=[\"month\", \"m2\", \"m2_yoy\", \"m1\", \"m1_yoy\", \"m0\", \"m0_yoy\", \"cd\", \"cd_yoy\", \"qm\", \"qm_yoy\", \"ftd\",\n                 \"ftd_yoy\", \"sd\", \"sd_yoy\", \"rests\", \"rests_yoy\"],\n        column_names=[\"统计时间\", \"货币和准货币(广义货币M2)(亿元)\", \"货币和准货币(广义货币M2)同比增长(%)\",\n                      \"货币(狭义货币M1)(亿元)\", \"货币(狭义货币M1)同比增长(%)\",\n                      \"流通中现金(M0)(亿元)\", \"流通中现金(M0)同比增长(%)\",\n                      \"活期存款(亿元)\", \"活期存款同比增长(%)\",\n                      \"准货币(亿元)\", \"准货币同比增长(%)\",\n                      \"定期存款(亿元)\", \"定期存款同比增长(%)\",\n                      \"储蓄存款(亿元)\", \"储蓄存款同比增长(%)\",\n                      \"其他存款(亿元)\", \"其他存款同比增长(%)\"\n                      ],\n        primary_key=[],\n        order_by=\" month desc \"\n    )\n)\n\n# http://tushare.org/fundamental.html\n# 参考官网网站的文档，是最全的。\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"基本面数据\",\n        name=\"股票列表\",\n        table_name=\"ts_stock_basics\",\n        columns=[\"code\", \"name\", \"industry\", \"area\", \"pe\", \"outstanding\", \"totals\", \"totalAssets\", \"liquidAssets\",\n                 \"fixedAssets\", \"reserved\", \"reservedPerShare\", \"esp\", \"bvps\", \"pb\", \"timeToMarket\",\n                 \"undp\", \"perundp\", \"rev\", \"profit\", \"gpr\", \"npr\", \"holders\"],\n        column_names=[\"代码\", \"名称\", \"所属行业\", \"地区\", \"市盈率\", \"流通股本(亿)\", \"总股本(亿)\", \"总资产(万)\", \"流动资产\",\n                      \"固定资产\", \"公积金\", \"每股公积金\", \"每股收益\", \"每股净资\", \"市净率\", \"上市日期\", \"未分利润\",\n                      \"每股未分配\", \"收入同比(%)\", \"利润同比(%)\", \"毛利率(%)\", \"净利润率(%)\", \"股东人数\"\n                      ],\n        primary_key=[],\n        order_by=\" code asc \"\n    )\n)\n\n# 业绩报告（主表）\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"基本面数据\",\n        name=\"业绩报告（主表）\",\n        table_name=\"ts_report_data\",\n        columns=[\"quarter\", \"code\", \"name\", \"eps\", \"eps_yoy\", \"bvps\", \"roe\", \"epcf\", \"net_profits\",\n                 \"profits_yoy\", \"distrib\", \"report_date\"],\n        column_names=[\"季度\", \"代码\", \"名称\", \"每股收益\", \"每股收益同比(%)\", \"每股净资产\", \"净资产收益率(%)\", \"每股现金流量(元)\", \",净利润(万元)\",\n                      \"净利润同比(%)\", \"分配方案\", \"发布日期\"\n                      ],\n        primary_key=[],\n        order_by=\" quarter desc \"\n    )\n)\n\n# 盈利能力\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"基本面数据\",\n        name=\"盈利能力\",\n        table_name=\"ts_profit_data\",\n        columns=[\"quarter\", \"code\", \"name\", \"roe\", \"net_profit_ratio\", \"gross_profit_rate\",\n                 \"net_profits\", \"eps\", \"business_income\", \"bips\"],\n        column_names=[\"季度\", \"代码\", \"名称\", \"净资产收益率(%)\", \"净利率(%)\", \"毛利率(%)\", \"净利润(万元)\",\n                      \"每股收益\", \"营业收入(百万元)\", \"每股主营业务收入(元)\"],\n        primary_key=[],\n        order_by=\" quarter desc \"\n    )\n)\n\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"基本面数据\",\n        name=\"营运能力\",\n        table_name=\"ts_operation_data\",\n        columns=[\"quarter\", \"code\", \"name\", \"arturnover\", \"arturndays\",\n                 \"inventory_turnover\", \"inventory_days\", \"currentasset_turnover\", \"currentasset_days\"],\n        column_names=[\"季度\", \"代码\", \"名称\", \"应收账款周转率(次)\", \"应收账款周转天数(天)\", \"存货周转率(次)\", \"存货周转天数(天)\",\n                      \"流动资产周转率(次)\", \"流动资产周转天数(天)\"\n                      ],\n        primary_key=[],\n        order_by=\" quarter desc \"\n    )\n)\n\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"基本面数据\",\n        name=\"成长能力\",\n        table_name=\"ts_growth_data\",\n        columns=[\"quarter\", \"code\", \"name\", \"mbrg\", \"nprg\", \"nav\", \"targ\", \"epsg\", \"seg\"],\n        column_names=[\"季度\", \"代码\", \"名称\", \"主营业务收入增长率(%)\", \"净利润增长率(%)\", \"净资产增长率\", \"总资产增长率\",\n                      \"每股收益增长率\", \"股东权益增长率\"],\n        primary_key=[],\n        order_by=\" quarter desc  \"\n    )\n)\n\n# \"code\", \"name: pchange\", \"amount\", \"buy\", \"bratio\", \"sell\", \"sratio\", \"reason\", \"date\"\n# 代码 名称 当日涨跌幅 龙虎榜成交额(万) 买入额(万) 买入占总成交比例 卖出额(万) 卖出占总成交比例 上榜原因 日期\n\n\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"每日数据\",\n        name=\"龙虎榜\",\n        table_name=\"ts_top_list\",\n        columns=[\"date\", \"code\", \"name\", \"pchange\", \"amount\", \"buy\", \"bratio\", \"sell\", \"sratio\", \"reason\"],\n        column_names=[\"日期\", \"代码\", \"名称\", \"当日涨跌幅\", \"龙虎榜成交额(万)\", \"买入额(万)\", \"买入占总成交比例\", \"卖出额(万)\",\n                      \"卖出占总成交比例\", \"上榜原因\"],\n        primary_key=[],\n        order_by=\" date desc  \"\n    )\n)\n# 实时行情\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"每日数据\",\n        name=\"每日股票数据\",\n        table_name=\"ts_today_all\",\n        columns=[\"date\", \"code\", \"name\", \"changepercent\", \"trade\", \"open\", \"high\", \"low\", \"settlement\", \"volume\",\n                 \"turnoverratio\", \"amount\", \"per\", \"pb\", \"mktcap\", \"nmc\"],\n        column_names=[\"日期\", \"代码\", \"名称\", \"涨跌幅\", \"现价\", \"开盘价\", \"最高价\", \"最低价\", \"昨日收盘价\", \"成交量\",\n                      \"换手率\", \"成交金额\", \"市盈率\", \"市净率\", \"总市值\", \"流通市值\"],\n        primary_key=[],\n        order_by=\" date desc  \"\n    )\n)\n# 大盘指数行情列表\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"每日数据\",\n        name=\"每日大盘指数行情\",\n        table_name=\"ts_index_all\",\n        columns=[\"date\", \"code\", \"name\", \"change\", \"open\", \"preclose\", \"close\", \"high\", \"low\", \"volume\", \"amount\"],\n        column_names=[\"日期\", \"代码\", \"名称\", \"涨跌幅\", \"开盘点位\", \"昨日收盘点位\", \"收盘点位\", \"最高点位\", \"最低点位\", \"成交量(手)\", \"成交金额（亿元）\"],\n        primary_key=[],\n        order_by=\" date desc  \"\n    )\n)\n\n# 每日波峰波谷猜想\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"每日数据猜想\",\n        name=\"每日波峰波谷猜想\",\n        table_name=\"guess_period_daily\",\n        columns=[\"date\", \"code\", \"name\", \"wave_base\", \"wave_crest\", \"wave_mean\", \"up_rate\",\n                 \"changepercent\", \"trade\", \"open\", \"high\", \"low\", \"settlement\", \"volume\",\n                 \"turnoverratio\", \"amount\", \"per\", \"pb\", \"mktcap\", \"nmc\"],\n        column_names=[\"日期\", \"代码\", \"名称\", \"5波峰平均\", \"5波谷平均\", \"价格平均\", \"上涨率猜想%\",\n                      \"涨跌幅\", \"现价\", \"开盘价\", \"最高价\", \"最低价\", \"昨日收盘价\", \"成交量\",\n                      \"换手率\", \"成交金额\", \"市盈率\", \"市净率\", \"总市值\", \"流通市值\"],\n        primary_key=[],\n        order_by=\" date desc  \"\n    )\n)\n\n# 每日收益率猜想。\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"每日数据猜想\",\n        name=\"每日收益率猜想\",\n        table_name=\"guess_return_daily\",\n        columns=[\"date\", \"code\", \"name\",\n                 \"5d\", \"10d\", \"20d\", \"60d\", \"5-10d\", \"5-20d\", \"mov_vol\", \"return\",\n                 \"changepercent\", \"trade\", \"open\", \"high\", \"low\", \"settlement\", \"volume\",\n                 \"turnoverratio\", \"amount\", \"per\", \"pb\", \"mktcap\", \"nmc\"],\n        column_names=[\"日期\", \"代码\", \"名称\",\n                      \"5周线\", \"10半月线\", \"20月线\", \"60季度线\", \"5-10日差%\", \"5-20日差%\", \"收益\", \"收益率移动标准差\",\n                      \"涨跌幅\", \"现价\", \"开盘价\", \"最高价\", \"最低价\", \"昨日收盘价\", \"成交量\",\n                      \"换手率\", \"成交金额\", \"市盈率\", \"市净率\", \"总市值\", \"流通市值\"],\n        primary_key=[],\n        order_by=\" date desc  \"\n    )\n)\n\n# 每日股票指标lite猜想。\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"每日数据猜想\",\n        name=\"每日股票指标lite猜想\",\n        table_name=\"guess_indicators_lite_daily\",\n        columns=[\"date\", \"code\", \"name\", \"changepercent\", \"trade\", \"open\", \"high\", \"low\", \"settlement\", \"volume\",\n                 \"turnoverratio\", \"amount\", \"per\", \"pb\", \"mktcap\", \"nmc\",\n                 \"kdjj\", \"rsi_6\", \"cci\"],\n        column_names=[\"日期\", \"代码\", \"名称\",\n                      \"涨跌幅\", \"现价\", \"开盘价\", \"最高价\", \"最低价\", \"昨日收盘价\", \"成交量\",\n                      \"换手率\", \"成交金额\", \"市盈率\", \"市净率\", \"总市值\", \"流通市值\",\n                      \"kdjj\", \"rsi_6\", \"cci\"],\n        primary_key=[],\n        order_by=\" date desc  \"\n    )\n)\n\n# 每日股票指标lite猜想买入。\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"每日数据猜想\",\n        name=\"每日股票指标lite猜想买入\",\n        table_name=\"guess_indicators_lite_buy_daily\",\n        columns=[\"buy_date\", \"code\", \"name\", \"changepercent\", \"trade\", \"turnoverratio\", \"pb\",\n                 \"kdjj\", \"rsi_6\", \"cci\", \"wave_base\", \"wave_crest\", \"wave_mean\", \"up_rate\", \"buy\", \"sell\",\n                 \"today_trade\", \"income\"],\n        column_names=[\"购买日期\", \"代码\", \"名称\", \"涨跌幅\", \"现价\", \"换手率%\", \"市净率%\",\n                      \"买入kdjj\", \"买入rsi_6\", \"买入cci\", \"波谷\", \"波峰\", \"波平均\", \"上涨率%\", \"买入\", \"卖出\", \"今日价格\", \"收益\"],\n        primary_key=[],\n        order_by=\" buy_date desc  \"\n    )\n)\n\n# 每日股票指标lite猜想卖出。\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"每日数据猜想\",\n        name=\"每日股票指标lite猜想卖出\",\n        table_name=\"guess_indicators_lite_sell_daily\",\n        columns=[\"date\", \"buy_date\", \"code\", \"name\", \"changepercent\", \"trade\", \"turnoverratio\", \"pb\",\n                 \"kdjj\", \"rsi_6\", \"cci\", \"wave_base\", \"wave_crest\", \"wave_mean\", \"up_rate\", \"buy\", \"sell\",\n                 \"today_trade\", \"income\", \"sell_cci\", \"sell_kdjj\", \"sell_rsi_6\"],\n        column_names=[\"日期\", \"购买日期\", \"代码\", \"名称\", \"涨跌幅\", \"现价\", \"换手率%\", \"市净率%\",\n                      \"买入kdjj\", \"买入rsi_6\", \"买入cci\", \"波谷\", \"波峰\", \"波平均\", \"上涨率%\", \"买入\", \"卖出\", \"今日价格\", \"收益\",\n                      \"卖出kdjj\", \"卖出rsi_6\", \"卖出cci\", ],\n        primary_key=[],\n        order_by=\" buy_date desc  \"\n    )\n)\n\n# 每日股票指标猜想。\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"每日数据猜想\",\n        name=\"每日股票指标All猜想\",\n        table_name=\"guess_indicators_daily\",\n        columns=[\"date\", \"code\", \"name\", \"changepercent\", \"trade\", \"open\", \"high\", \"low\", \"settlement\", \"volume\",\n                 \"turnoverratio\", \"amount\", \"per\", \"pb\", \"mktcap\", \"nmc\",\n                 'adx', 'adxr', 'boll', 'boll_lb', 'boll_ub', 'cci', 'cci_20', 'close_-1_r',\n                 'close_-2_r', 'code', 'cr', 'cr-ma1', 'cr-ma2', 'cr-ma3', 'date', 'dma', 'dx',\n                 'kdjd', 'kdjj', 'kdjk', 'macd', 'macdh', 'macds', 'mdi', 'pdi',\n                 'rsi_12', 'rsi_6', 'trix', 'trix_9_sma', 'vr', 'vr_6_sma', 'wr_10', 'wr_6'],\n        column_names=[\"日期\", \"代码\", \"名称\",\n                      \"涨跌幅\", \"现价\", \"开盘价\", \"最高价\", \"最低价\", \"昨日收盘价\", \"成交量\",\n                      \"换手率\", \"成交金额\", \"市盈率\", \"市净率\", \"总市值\", \"流通市值\",\n                      'adx', 'adxr', 'boll', 'boll_lb', 'boll_ub', 'cci', 'cci_20', 'close_-1_r',\n                      'close_-2_r', 'code', 'cr', 'cr-ma1', 'cr-ma2', 'cr-ma3', 'date', 'dma', 'dx',\n                      'kdjd', 'kdjj', 'kdjk', 'macd', 'macdh', 'macds', 'mdi', 'pdi',\n                      'rsi_12', 'rsi_6', 'trix', 'trix_9_sma', 'vr', 'vr_6_sma', 'wr_10', 'wr_6'],\n        primary_key=[],\n        order_by=\" date desc  \"\n    )\n)\n\nSTOCK_WEB_DATA_LIST.append(\n    StockWebData(\n        mode=\"query\",\n        type=\"每日数据Keras猜想\",\n        name=\"每日股票数据Keras猜想\",\n        table_name=\"guess_sklearn_ma_daily\",\n        columns=[\"date\", \"code\", \"name\", \"changepercent\", \"trade\", \"open\", \"high\", \"low\", \"settlement\", \"volume\",\n                 \"turnoverratio\", \"next_close\", \"sklearn_score\", \"up_rate\"],\n        column_names=[\"日期\", \"代码\", \"名称\", \"涨跌幅\", \"现价\", \"开盘价\", \"最高价\", \"最低价\", \"昨日收盘价\", \"成交量\",\n                      \"换手率\", \"预测收盘价\", \"sk概率\", \"预测上涨率\"],\n        primary_key=[],\n        order_by=\" date desc  \"\n    )\n)\n\nSTOCK_WEB_DATA_MAP = {}\nWEB_EASTMONEY_URL = \"http://quote.eastmoney.com/%s.html\"\n# 再拼接成Map使用。\nfor tmp in STOCK_WEB_DATA_LIST:\n    try:\n        # 增加columns 字段中的【东方财富】\n        tmp_idx = tmp.columns.index(\"code\")\n        tmp.column_names.insert(tmp_idx + 1, \"东方财富\")\n    except  Exception as e:\n        print(\"error :\", e)\n\n    STOCK_WEB_DATA_MAP[tmp.table_name] = tmp\n\n    if len(tmp.columns) != len(tmp.column_names):\n        print(u\"error:\", tmp.table_name, \",columns:\", len(tmp.columns), \",column_names:\", len(tmp.column_names))\n"
  },
  {
    "path": "backend/old_jobs/README.md",
    "content": "## 说明\n\n\n之前测试使用的脚本。执行了一段时间，只是用来进行练习使用的。\n"
  },
  {
    "path": "backend/old_jobs/guess_indicators_lite_buy_daily_job.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\n\nimport libs.common as common\nimport pandas as pd\nimport numpy as np\nimport math\nimport datetime\nimport heapq\n\n\n### 对每日指标数据，进行筛选。将符合条件的。二次筛选出来。\ndef stat_all_lite(tmp_datetime):\n    # 要操作的数据库表名称。\n    table_name = \"guess_indicators_lite_buy_daily\"\n    datetime_str = (tmp_datetime).strftime(\"%Y-%m-%d\")\n    datetime_int = (tmp_datetime).strftime(\"%Y%m%d\")\n    print(\"datetime_str:\", datetime_str)\n    print(\"datetime_int:\", datetime_int)\n\n    # try:\n    #     # 删除老数据。guess_indicators_lite_buy_daily 是一张单表，没有日期字段。\n    #     del_sql = \" DELETE FROM `stock_data`.`%s` WHERE `date`= '%s' \" % (table_name, datetime_int)\n    #     print(\"del_sql:\", del_sql)\n    #     common.insert(del_sql)\n    #     print(\"del_sql\")\n    # except Exception as e:\n    #     print(\"error :\", e)\n\n    sql_1 = \"\"\"\n                SELECT `date`, `code`, `name`, `changepercent`, `trade`,`turnoverratio`, `pb` ,`kdjj`,`rsi_6`,`cci`\n                            FROM stock_data.guess_indicators_lite_daily WHERE `date` = %s \n                            and `changepercent` > 2 and `pb` > 0 \n        \"\"\"\n    # and `changepercent` > 2 and `pb` > 0 and `turnoverratio` > 5 去除掉换手率参数。\n    data = pd.read_sql(sql=sql_1, con=common.engine(), params=[datetime_int])\n    data = data.drop_duplicates(subset=\"code\", keep=\"last\")\n    print(\"######## len data ########:\", len(data))\n    # del data[\"name\"]\n    # print(data)\n    data[\"trade_float32\"] = data[\"trade\"].astype('float32', copy=True)\n    # 输入 date 用作历史数据查询。\n    stock_merge = pd.DataFrame({\n        \"date\": data[\"date\"], \"code\": data[\"code\"], \"wave_mean\": data[\"trade\"],\n        \"wave_crest\": data[\"trade\"], \"wave_base\": data[\"trade\"]}, index=data.index.values)\n    print(stock_merge.head(1))\n\n    stock_merge = stock_merge.apply(apply_merge, axis=1)  # , axis=1)\n    del stock_merge[\"date\"]  # 合并前删除 date 字段。\n    # 合并数据\n    data_new = pd.merge(data, stock_merge, on=['code'], how='left')\n\n    # 使用 trade_float32 参加计算。\n    data_new = data_new[data_new[\"trade_float32\"] > data_new[\"wave_base\"]]  # 交易价格大于波谷价格。\n    data_new = data_new[data_new[\"trade_float32\"] < data_new[\"wave_crest\"]]  # 小于波峰价格\n\n    # wave_base  wave_crest  wave_mean\n    data_new[\"wave_base\"] = data_new[\"wave_base\"].round(2)  # 数据保留2位小数\n    data_new[\"wave_crest\"] = data_new[\"wave_crest\"].round(2)  # 数据保留2位小数\n    data_new[\"wave_mean\"] = data_new[\"wave_mean\"].round(2)  # 数据保留2位小数\n\n    data_new[\"up_rate\"] = (data_new[\"wave_mean\"].sub(data_new[\"trade_float32\"])).div(data_new[\"wave_crest\"]).mul(100)\n    data_new[\"up_rate\"] = data_new[\"up_rate\"].round(2)  # 数据保留2位小数\n\n    data_new[\"buy\"] = 1\n    data_new[\"sell\"] = 0\n    data_new[\"today_trade\"] = data_new[\"trade\"]\n    data_new[\"income\"] = 0\n    # 重命名 date\n    data_new.columns.values[0] = \"buy_date\"\n    del data_new[\"trade_float32\"]\n\n    try:\n        common.insert_db(data_new, table_name, False, \"`code`\")\n        print(\"insert_db\")\n    except Exception as e:\n        print(\"error :\", e)\n    # 重命名\n    del data_new[\"name\"]\n    print(data_new)\n\n\ndef apply_merge(tmp):\n    date = tmp[\"date\"]\n    code = tmp[\"code\"]\n    date_end = datetime.datetime.strptime(date, \"%Y%m%d\")\n    date_start = (date_end + datetime.timedelta(days=-300)).strftime(\"%Y-%m-%d\")\n    date_end = date_end.strftime(\"%Y-%m-%d\")\n    print(code, date_start, date_end)\n\n    # open, high, close, low, volume, price_change, p_change, ma5, ma10, ma20, v_ma5, v_ma10, v_ma20, turnover\n    # 使用缓存方法。加快计算速度。\n    stock = common.get_hist_data_cache(code, date_start, date_end)\n    # 增加空判断，如果是空返回 0 数据。\n    if stock is None:\n        return list([code, date, 0, 0, 0])\n\n    stock = pd.DataFrame({\"close\": stock[\"close\"]}, index=stock.index.values)\n    stock = stock.sort_index(0)  # 将数据按照日期排序下。\n\n    # print(stock.head(10))\n    arr = pd.Series(stock[\"close\"].values)\n    # print(df_arr)\n    wave_mean = arr.mean()\n    max_point = 3  # 获得最高的几个采样点。\n    # 计算股票的波峰值。\n    wave_crest = heapq.nlargest(max_point, enumerate(arr), key=lambda x: x[1])\n    wave_crest_mean = pd.DataFrame(wave_crest).mean()\n\n    # 输出元祖第一个元素是index，第二元素是比较的数值 计算数据的波谷值\n    wave_base = heapq.nsmallest(max_point, enumerate(arr), key=lambda x: x[1])\n    wave_base_mean = pd.DataFrame(wave_base).mean()\n    # 输出数据\n    print(\"##############\", len(stock))\n    if len(stock) > 180:\n        #     code      date wave_base wave_crest wave_mean 顺序必须一致。返回的是行数据，然后填充。\n        return list([code, date, wave_base_mean[1], wave_crest_mean[1], wave_mean])\n    else:\n        return list([code, date, 0, 0, 0])\n\n\n# main函数入口\nif __name__ == '__main__':\n    # 二次筛选数据。\n    tmp_datetime = common.run_with_args(stat_all_lite)\n"
  },
  {
    "path": "backend/old_jobs/guess_indicators_lite_sell_daily_job.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\n\nimport libs.common as common\nimport pandas as pd\nimport numpy as np\nimport math\nimport datetime\nimport heapq\nimport stockstats\n\n\n# code      date today_trade\ndef apply_merge(tmp):\n    date = tmp[\"date\"]\n    code = tmp[\"code\"]\n    date_end = datetime.datetime.strptime(date, \"%Y%m%d\")\n    date_start = (date_end + datetime.timedelta(days=-300)).strftime(\"%Y-%m-%d\")\n    date_end = date_end.strftime(\"%Y-%m-%d\")\n    print(code, date_start, date_end)\n\n    # open, high, close, low, volume, price_change, p_change, ma5, ma10, ma20, v_ma5, v_ma10, v_ma20, turnover\n    # 使用缓存方法。加快计算速度。\n    stock = common.get_hist_data_cache(code, date_start, date_end)\n    # 增加空判断，如果是空返回 0 数据。\n    if stock is None:\n        return list([code, date, 0.0])\n    print(\"########\")\n    # print(stock.tail(1))\n    close = stock.tail(1)[\"close\"].values[0]\n    print(\"close:  \", close)\n    print(\"########\")\n    return list([code, date, close])\n\n\n#    buy    code      date  sell  sell_cci  sell_kdjj  sell_rsi_6\ndef apply_merge_sell(tmp):\n    date = tmp[\"date\"]\n    code = tmp[\"code\"]\n    date_end = datetime.datetime.strptime(date, \"%Y%m%d\")\n    date_start = (date_end + datetime.timedelta(days=-300)).strftime(\"%Y-%m-%d\")\n    date_end = date_end.strftime(\"%Y-%m-%d\")\n    print(code, date_start, date_end)\n\n    # open, high, close, low, volume, price_change, p_change, ma5, ma10, ma20, v_ma5, v_ma10, v_ma20, turnover\n    # 使用缓存方法。加快计算速度。\n    stock = common.get_hist_data_cache(code, date_start, date_end)\n    # 增加空判断，如果是空返回 0 数据。\n    if stock is None:\n        return list([1, code, date, 0, 0, 0, 0])\n    print(\"########\")\n    # J大于100时为超买，小于10时为超卖。\n    # 强弱指标保持高于50表示为强势市场，反之低于50表示为弱势市场。\n    # 1、当CCI指标从下向上突破﹢100线而进入非常态区间时，表明股价脱离常态而进入异常波动阶段，\n    # 2、当CCI指标从上向下突破﹣100线而进入另一个非常态区间时，表明股价的盘整阶段已经结束，\n    stockStat = stockstats.StockDataFrame.retype(stock)\n    kdjj = int(stockStat[\"kdjj\"].tail(1).values[0])\n    rsi_6 = int(stockStat[\"rsi_6\"].tail(1).values[0])\n    cci = int(stockStat[\"cci\"].tail(1).values[0])\n    print(\"kdjj:\", kdjj, \"rsi_6:\", rsi_6, \"cci:\", cci)\n    # and kdjj > 80 and rsi_6 > 55  and cci > 100 判断卖出时刻。也就是买入时刻的反面。发现有波动就卖了。\n    # if kdjj <= 10 and rsi_6 <= 50 and cci <= 100: old\n    if kdjj <= 80 or rsi_6 <= 55 or cci <= 100:\n        return list([0, code, date, 1, cci, kdjj, rsi_6])\n    else:\n        return list([1, code, date, 0, cci, kdjj, rsi_6])\n\n\n# 增加 收益计算。\ndef stat_index_calculate(tmp_datetime):\n    # 要操作的数据库表名称。\n    table_name = \"guess_indicators_lite_sell_daily\"\n    datetime_str = (tmp_datetime).strftime(\"%Y-%m-%d\")\n    datetime_int = (tmp_datetime).strftime(\"%Y%m%d\")\n    print(\"datetime_str:\", datetime_str)\n    print(\"datetime_int:\", datetime_int)\n\n    sql_1 = \"\"\" \n                SELECT `buy_date`, `code`, `name`, `changepercent`, `trade`, `turnoverratio`, `pb`, `kdjj`, `rsi_6`, \n                `cci`, `wave_base`, `wave_crest`, `wave_mean`, `up_rate`\n                FROM guess_indicators_lite_buy_daily where `buy_date` <= \"\"\" + datetime_int\n    print(sql_1)\n    data = pd.read_sql(sql=sql_1, con=common.engine(), params=[])\n    data = data.drop_duplicates(subset=\"code\", keep=\"last\")\n    print(data[\"trade\"])\n    data[\"trade_float32\"] = data[\"trade\"].astype('float32', copy=False)\n    print(len(data))\n    data[\"date\"] = datetime_int\n\n    stock_merge = pd.DataFrame({\n        \"date\": data[\"date\"], \"code\": data[\"code\"], \"today_trade\": data[\"trade\"]}, index=data.index.values)\n    print(stock_merge.head(1))\n\n    stock_merge = stock_merge.apply(apply_merge, axis=1)  # , axis=1)\n\n    del stock_merge[\"date\"]  # 合并前删除 date 字段。\n    # 合并数据\n    data_new = pd.merge(data, stock_merge, on=['code'], how='left')\n    data_new[\"income\"] = (data_new[\"today_trade\"] - data_new[\"trade_float32\"]) * 100\n    data_new[\"income\"] = data_new[\"income\"].round(4)  # 保留4位小数。\n\n    # 增加售出列。看看是否需要卖出。\n    stock_sell_merge = pd.DataFrame({\n        \"date\": data[\"date\"], \"code\": data[\"code\"], \"sell\": 0, \"buy\": 0, \"sell_kdjj\": 0, \"sell_rsi_6\": 0,\n        \"sell_cci\": 0},\n        index=data.index.values)\n    print(stock_sell_merge.head(1))\n\n    merge_sell_data = stock_sell_merge.apply(apply_merge_sell, axis=1)  # , axis=1)\n    # 重命名\n    del merge_sell_data[\"date\"]  # 合并前删除 date 字段。\n    # 合并数据\n    data_new = pd.merge(data_new, merge_sell_data, on=['code'], how='left')\n\n    # 删除老数据。\n    try:\n        del_sql = \" DELETE FROM `stock_data`.`\" + table_name + \"` WHERE `date`= '%s' \" % datetime_int\n        common.insert(del_sql)\n        print(\"insert_db\")\n    except Exception as e:\n        print(\"error :\", e)\n    del data_new[\"trade_float32\"]\n    try:\n        common.insert_db(data_new, table_name, False, \"`date`,`code`\")\n        print(\"insert_db\")\n    except Exception as e:\n        print(\"error :\", e)\n    # 重命名\n    del data_new[\"name\"]\n    print(data_new)\n\n\n# main函数入口\nif __name__ == '__main__':\n    # 计算买卖。\n    tmp_datetime = common.run_with_args(stat_index_calculate)\n"
  },
  {
    "path": "backend/old_jobs/guess_period_daily_job.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\n\nimport libs.common as common\nimport sys\nimport time\nimport pandas as pd\nimport tushare as ts\nfrom sqlalchemy.types import NVARCHAR\nfrom sqlalchemy import inspect\nimport datetime\nimport heapq\n\n\"\"\"\nSELECT `date`, `code`, `name`, `changepercent`, `trade`, `open`, `high`, `low`, \n                `settlement`, `volume`, `turnoverratio`, `amount`, `per`, `pb`, `mktcap`, `nmc` \n    FROM stock_data.ts_today_all where `date` = 20171106 and trade > 0 and trade <= 20\nand `code` not like '002%' and `code` not like '300%'  and `name` not like '%st%'\n\n\"\"\"\n\n\ndef stat_index_all(tmp_datetime):\n    datetime_str = (tmp_datetime).strftime(\"%Y-%m-%d\")\n    datetime_int = (tmp_datetime).strftime(\"%Y%m%d\")\n    print(\"datetime_str:\", datetime_str)\n    print(\"datetime_int:\", datetime_int)\n\n    # 查询今日满足股票数据。剔除数据：创业板股票数据，中小板股票数据，所有st股票\n    # #`code` not like '002%' and `code` not like '300%'  and `name` not like '%st%'\n    sql_1 = \"\"\" \n            SELECT `date`, `code`, `name`, `changepercent`, `trade`, `open`, `high`, `low`, \n                `settlement`, `volume`, `turnoverratio`, `amount`, `per`, `pb`, `mktcap`, `nmc` \n            FROM stock_data.ts_today_all WHERE `date` = %s and `trade` > 0 and `open` > 0 and trade <= 20 \n                and `code` not like %s and `code` not like %s and `name` not like %s\n            \"\"\"\n    print(sql_1)\n    data = pd.read_sql(sql=sql_1, con=common.engine(), params=[datetime_int, '002%', '300%', '%st%'])\n    print(type(data))\n    data = data.drop_duplicates(subset=\"code\", keep=\"last\")\n    print(data[\"trade\"])\n    data[\"trade_float32\"] = data[\"trade\"].astype('float32', copy=False)\n    print(len(data))\n    print(\"########data[trade]########:\")\n    print(data[\"trade\"])\n\n    # 使用 trade 填充数据\n    stock_guess = pd.DataFrame({\n        \"date\": data[\"date\"], \"code\": data[\"code\"], \"wave_mean\": data[\"trade\"],\n        \"wave_crest\": data[\"trade\"], \"wave_base\": data[\"trade\"]}, index=data.index.values)\n    print(stock_guess.head())\n    stock_guess = stock_guess.apply(apply_guess, axis=1)  # , axis=1)\n    print(stock_guess.head())\n    # stock_guess.astype('float32', copy=False)\n    stock_guess.drop('date', axis=1, inplace=True)  # 删除日期字段，然后和原始数据合并。\n    stock_guess = stock_guess.round(2)  # 数据保留2位小数\n    print(stock_guess[\"wave_base\"])\n\n    data_new = pd.merge(data, stock_guess, on=['code'], how='left')\n    print(\"#############\")\n\n    # 使用pandas 函数 ： https://pandas.pydata.org/pandas-docs/stable/api.html#id4\n    data_new[\"up_rate\"] = (data_new[\"trade_float32\"].sub(data_new[\"wave_mean\"])).div(data_new[\"wave_crest\"]).mul(100)\n    data_new[\"up_rate\"] = data_new[\"up_rate\"].round(2)  # 数据保留2位小数\n    data_new.drop('trade_float32', axis=1, inplace=True)  # 删除计算字段。\n\n    # 删除老数据。\n    del_sql = \" DELETE FROM `stock_data`.`guess_period_daily` WHERE `date`= '%s' \" % datetime_int\n    common.insert(del_sql)\n    # print(data_new.head())\n    # data_new[\"down_rate\"] = (data_new[\"trade\"] - data_new[\"wave_mean\"]) / data_new[\"wave_base\"]\n    common.insert_db(data_new, \"guess_period_daily\", False, \"`date`,`code`\")\n\n    # 进行左连接.\n    # tmp = pd.merge(tmp, tmp2, on=['company_id'], how='left')\n\n\ndef apply_guess(tmp):\n    date = tmp[\"date\"]\n    code = tmp[\"code\"]\n    date_end = datetime.datetime.strptime(date, \"%Y%m%d\")\n    date_start = (date_end + datetime.timedelta(days=-300)).strftime(\"%Y-%m-%d\")\n    date_end = date_end.strftime(\"%Y-%m-%d\")\n    print(code, date_start, date_end)\n\n    # open, high, close, low, volume, price_change, p_change, ma5, ma10, ma20, v_ma5, v_ma10, v_ma20, turnover\n    # 使用缓存方法。加快计算速度。\n    stock = common.get_hist_data_cache(code, date_start, date_end)\n    # 增加空判断，如果是空返回 0 数据。\n    if stock is None:\n        return pd.Series([date, code, 0.0, 0.0, 0.0],\n                     index=['date', 'code', 'wave_mean', 'wave_crest', 'wave_base'])\n\n    stock = pd.DataFrame({\"close\": stock[\"close\"]}, index=stock.index.values)\n    stock = stock.sort_index(0)  # 将数据按照日期排序下。\n\n    # print(stock.head(10))\n    arr = pd.Series(stock[\"close\"].values)\n    # print(df_arr)\n    wave_mean = arr.mean()\n    # 计算股票的波峰值。\n    wave_crest = heapq.nlargest(5, enumerate(arr), key=lambda x: x[1])\n    wave_crest_mean = pd.DataFrame(wave_crest).mean()\n\n    # 输出元祖第一个元素是index，第二元素是比较的数值 计算数据的波谷值\n    wave_base = heapq.nsmallest(5, enumerate(arr), key=lambda x: x[1])\n    wave_base_mean = pd.DataFrame(wave_base).mean()\n    # 输出数据\n    # print(\"##############\")\n    #     code      date wave_base wave_crest wave_mean 顺序必须一致。返回的是行数据，然后填充。\n    return pd.Series([date, code, wave_base_mean[1], wave_crest_mean[1], wave_mean],\n                     index=['date','code','wave_mean','wave_crest','wave_base'])\n\n\n# main函数入口\nif __name__ == '__main__':\n    # 使用方法传递。\n    tmp_datetime = common.run_with_args(stat_index_all)\n"
  },
  {
    "path": "backend/old_jobs/guess_return_daily_job.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\n\nimport libs.common as common\nimport sys\nimport time\nimport pandas as pd\nimport numpy as np\nimport math\nimport tushare as ts\nfrom sqlalchemy.types import NVARCHAR\nfrom sqlalchemy import inspect\nimport datetime\nimport heapq\n\n\"\"\"\nSELECT `date`, `code`, `name`, `changepercent`, `trade`, `open`, `high`, `low`, \n                `settlement`, `volume`, `turnoverratio`, `amount`, `per`, `pb`, `mktcap`, `nmc` \n    FROM stock_data.ts_today_all where `date` = 20171106 and trade > 0 and trade <= 20\nand `code` not like '002%' and `code` not like '300%'  and `name` not like '%st%'\n\n\"\"\"\n\n\ndef stat_index_all(tmp_datetime):\n    datetime_str = (tmp_datetime).strftime(\"%Y-%m-%d\")\n    datetime_int = (tmp_datetime).strftime(\"%Y%m%d\")\n    print(\"datetime_str:\", datetime_str)\n    print(\"datetime_int:\", datetime_int)\n\n    # 查询今日满足股票数据。剔除数据：创业板股票数据，中小板股票数据，所有st股票\n    # #`code` not like '002%' and `code` not like '300%'  and `name` not like '%st%'\n    sql_1 = \"\"\" \n            SELECT `date`, `code`, `name`, `changepercent`, `trade`, `open`, `high`, `low`, \n                `settlement`, `volume`, `turnoverratio`, `amount`, `per`, `pb`, `mktcap`, `nmc` \n            FROM stock_data.ts_today_all WHERE `date` = %s and `trade` > 0 and `open` > 0 and trade <= 20 \n                and `code` not like %s and `code` not like %s and `name` not like %s\n            \"\"\"\n    print(sql_1)\n    data = pd.read_sql(sql=sql_1, con=common.engine(), params=[datetime_int, '002%', '300%', '%st%'])\n    data = data.drop_duplicates(subset=\"code\", keep=\"last\")\n    print(\"########data[trade]########:\")\n    # print(data[\"trade\"])\n\n    # 使用 trade 填充数据\n    stock_guess = pd.DataFrame({\n        \"date\": data[\"date\"], \"code\": data[\"code\"], \"5d\": data[\"trade\"],\n        \"10d\": data[\"trade\"], \"20d\": data[\"trade\"], \"60d\": data[\"trade\"], \"5-10d\": data[\"trade\"],\n        \"5-20d\": data[\"trade\"], \"return\": data[\"trade\"], \"mov_vol\": data[\"trade\"]\n    }, index=data.index.values)\n\n    stock_guess = stock_guess.apply(apply_guess, axis=1)  # , axis=1)\n    # print(stock_guess.head())\n    # stock_guess.astype('float32', copy=False)\n    stock_guess.drop('date', axis=1, inplace=True)  # 删除日期字段，然后和原始数据合并。\n\n    # print(stock_guess[\"5d\"])\n\n    data_new = pd.merge(data, stock_guess, on=['code'], how='left')\n    print(\"#############\")\n\n    # 使用pandas 函数 ： https://pandas.pydata.org/pandas-docs/stable/api.html#id4\n    data_new[\"return\"] = data_new[\"return\"].mul(100)  # 扩大100 倍方便观察\n    data_new[\"mov_vol\"] = data_new[\"mov_vol\"].mul(100)\n\n    data_new = data_new.round(2)  # 数据保留2位小数\n\n    # 删除老数据。\n    del_sql = \" DELETE FROM `stock_data`.`guess_return_daily` WHERE `date`= '%s' \" % datetime_int\n    common.insert(del_sql)\n\n    # data_new[\"down_rate\"] = (data_new[\"trade\"] - data_new[\"wave_mean\"]) / data_new[\"wave_base\"]\n    common.insert_db(data_new, \"guess_return_daily\", False, \"`date`,`code`\")\n\n    # 进行左连接.\n    # tmp = pd.merge(tmp, tmp2, on=['company_id'], how='left')\n\n\ndef apply_guess(tmp):\n    date = tmp[\"date\"]\n    code = tmp[\"code\"]\n    date_end = datetime.datetime.strptime(date, \"%Y%m%d\")\n    date_start = (date_end + datetime.timedelta(days=-300)).strftime(\"%Y-%m-%d\")\n    date_end = date_end.strftime(\"%Y-%m-%d\")\n    print(code, date_start, date_end)\n    # open, high, close, low, volume, price_change, p_change, ma5, ma10, ma20, v_ma5, v_ma10, v_ma20, turnover\n    # 使用缓存方法。加快计算速度。\n    stock = common.get_hist_data_cache(code, date_start, date_end)\n    # 增加空判断，如果是空返回 0 数据。\n    if stock is None:\n        return pd.Series([0.0, 0.0, 0.0, 0.0, 0.0, 0.0, code, date, 0.0, 0.0],\n                         index=['10d', '20d', '5-10d', '5-20d', '5d', '60d', 'code', 'date', 'mov_vol', 'return'])\n\n    stock = pd.DataFrame({\"close\": stock[\"close\"]}, index=stock.index.values)\n    stock = stock.sort_index(0)  # 将数据按照日期排序下。\n    # print(stock.head(10))\n    # 5周期、10周期、20周期和60周期\n    # 周线、半月线、月线和季度线\n    stock[\"5d\"] = stock[\"close\"].rolling(window=5).mean()  # 周线\n    stock[\"10d\"] = stock[\"close\"].rolling(window=10).mean()  # 半月线\n    stock[\"20d\"] = stock[\"close\"].rolling(window=20).mean()  # 月线\n    stock[\"60d\"] = stock[\"close\"].rolling(window=60).mean()  # 季度线\n    # 计算日期差。\n    stock[\"5-10d\"] = (stock[\"5d\"] - stock[\"10d\"]) * 100 / stock[\"10d\"]  # 周-半月线差\n    stock[\"5-20d\"] = (stock[\"5d\"] - stock[\"20d\"]) * 100 / stock[\"20d\"]  # 周-月线差\n    # 计算股票的收益价格\n    stock[\"return\"] = np.log(stock[\"close\"] / stock[\"close\"].shift(1))\n\n    # print(stock[\"return\"])\n    # 计算股票的【收益率的移动历史标准差】\n    mov_day = int(len(stock) / 20)\n    # print(\"mov_day:\", mov_day, len(stock))\n    stock[\"mov_vol\"] = stock[\"return\"].rolling(window=mov_day).std() * math.sqrt(mov_day)\n    # print(stock[\"mov_vol\"].tail())\n    # print(stock[\"return\"].tail())\n    # print(\"stock[10d].tail(1)\", stock[\"10d\"].tail(1).values[0])\n    # 10d    20d  5-10d  5-20d     5d    60d    code      date  mov_vol  return\n    tmp = pd.Series([stock[\"10d\"].tail(1).values[0], stock[\"20d\"].tail(1).values[0], stock[\"5-10d\"].tail(1).values[0],\n                stock[\"5-20d\"].tail(1).values[0], stock[\"5d\"].tail(1).values[0], stock[\"60d\"].tail(1).values[0],\n                code, date, stock[\"mov_vol\"].tail(1).values[0], stock[\"return\"].tail(1).values[0]],\n                    index=['10d', '20d', '5-10d', '5-20d', '5d', '60d', 'code', 'date', 'mov_vol', 'return'])\n    # print(tmp)\n    return tmp\n\n\n# main函数入口\nif __name__ == '__main__':\n    # 使用方法传递。\n    tmp_datetime = common.run_with_args(stat_index_all)\n"
  },
  {
    "path": "backend/old_jobs/guess_sklearn_ma_daily_job.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\n\nimport libs.common as common\nimport pandas as pd\nimport numpy as np\nimport math\nimport datetime\nimport sklearn as skl\nfrom sklearn import datasets, linear_model\n# https://github.com/udacity/machine-learning/issues/202\n# sklearn.cross_validation 这个包不推荐使用了。\nfrom sklearn.model_selection import train_test_split, cross_val_score\nfrom sklearn.neighbors import KNeighborsClassifier\n\n# 要操作的数据库表名称。\ntable_name = \"guess_sklearn_ma_daily\"\n\n\n# 批处理数据。\ndef stat_all_batch(tmp_datetime):\n    datetime_str = (tmp_datetime).strftime(\"%Y-%m-%d\")\n    datetime_int = (tmp_datetime).strftime(\"%Y%m%d\")\n    print(\"datetime_str:\", datetime_str)\n    print(\"datetime_int:\", datetime_int)\n\n    try:\n        # 删除老数据。\n        del_sql = \" DELETE FROM `stock_data`.`%s` WHERE `date`= %s \" % (table_name, datetime_int)\n        print(\"del_sql:\", del_sql)\n        common.insert(del_sql)\n    except Exception as e:\n        print(\"error :\", e)\n\n    sql_count = \"\"\"\n            SELECT count(1) FROM stock_data.ts_today_all WHERE `date` = %s and `trade` > 0 and `open` > 0 and trade <= 20 \n                 and `code` not like %s and `name` not like %s \n    \"\"\"\n    # 修改逻辑，增加中小板块计算。 中小板：002，创业板：300 。and `code` not like %s and `code` not like %s and `name` not like %s\n    # count = common.select_count(sql_count, params=[datetime_int, '002%', '300%', '%st%'])\n    count = common.select_count(sql_count, params=[datetime_int, '300%', '%st%'])\n    print(\"count :\", count)\n    batch_size = 100\n    end = int(math.ceil(float(count) / batch_size) * batch_size)\n    print(end)\n    # for i in range(0, end, batch_size):\n    for i in range(0, end, batch_size):\n        print(\"loop :\", i)\n        # 查询今日满足股票数据。剔除数据：创业板股票数据，中小板股票数据，所有st股票\n        # #`code` not like '002%' and `code` not like '300%'  and `name` not like '%st%'\n        sql_1 = \"\"\" \n                    SELECT `date`, `code`, `name`, `changepercent`, `trade`, `open`, `high`, `low`, \n                        `settlement`, `volume`, `turnoverratio`, `amount`, `per`, `pb`, `mktcap`, `nmc` \n                    FROM stock_data.ts_today_all WHERE `date` = %s and `trade` > 0 and `open` > 0 and trade <= 20 \n                        and `code` not like %s and `name` not like %s limit %s , %s\n                    \"\"\"\n        print(sql_1)\n        # data = pd.read_sql(sql=sql_1, con=common.engine(), params=[datetime_int, '002%', '300%', '%st%', i, batch_size])\n        data = pd.read_sql(sql=sql_1, con=common.engine(), params=[datetime_int, '300%', '%st%', i, batch_size])\n        data = data.drop_duplicates(subset=\"code\", keep=\"last\")\n        print(\"########data[trade]########:\", len(data))\n\n        # 使用 trade 填充数据\n        stock_sklearn = pd.DataFrame({\n            \"date\": data[\"date\"], \"code\": data[\"code\"], \"next_close\": data[\"trade\"],\n            \"sklearn_score\": data[\"trade\"]}, index=data.index.values)\n        print(stock_sklearn.head())\n        stock_sklearn_apply = stock_sklearn.apply(apply_sklearn, axis=1)  # , axis=1)\n        # 重命名\n        del stock_sklearn_apply[\"date\"]  # 合并前删除 date 字段。\n        # 合并数据\n        data_new = pd.merge(data, stock_sklearn_apply, on=['code'], how='left')\n        # for index, row in data.iterrows():\n        #     next_stock, score = stat_index_all(row, i)\n        #     print(next_stock, score)\n        data_new[\"next_close\"] = data_new[\"next_close\"].round(2)  # 数据保留4位小数\n        data_new[\"sklearn_score\"] = data_new[\"sklearn_score\"].round(2)  # 数据保留2位小数\n\n        data_new[\"trade_float32\"] = data[\"trade\"].astype('float32', copy=False)\n        data_new[\"up_rate\"] = (data_new[\"next_close\"] - data_new[\"trade_float32\"]) * 100 / data_new[\"trade_float32\"]\n        data_new[\"up_rate\"] = data_new[\"up_rate\"].round(2)  # 数据保留2位小数\n        del data_new[\"trade_float32\"]\n\n        try:\n            common.insert_db(data_new, table_name, False, \"`date`,`code`\")\n            print(\"insert_db\")\n        except Exception as e:\n            print(\"error :\", e)\n        # 重命名\n        del data_new[\"name\"]\n        print(data_new)\n\n\n# code date next_close sklearn_score\ndef apply_sklearn(data):\n    # 要操作的数据库表名称。\n    print(\"########stat_index_all########:\", len(data))\n    date = data[\"date\"]\n    code = data[\"code\"]\n    print(date, code)\n    date_end = datetime.datetime.strptime(date, \"%Y%m%d\")\n    date_start = (date_end + datetime.timedelta(days=-300)).strftime(\"%Y-%m-%d\")\n    date_end = date_end.strftime(\"%Y-%m-%d\")\n    print(code, date_start, date_end)\n\n    # open high close low volume price_change p_change ma5 ma10 ma20 v_ma5 v_ma10 v_ma20 turnover\n    stock_X = common.get_hist_data_cache(code, date_start, date_end)\n    # 增加空判断，如果是空返回 0 数据。\n    if stock_X is None:\n        return list([code, date, 0.0, 0.0])\n\n    stock_X = stock_X.sort_index(0)  # 将数据按照日期排序下。\n    stock_y = pd.Series(stock_X[\"close\"].values)  # 标签\n\n    stock_X_next = stock_X.iloc[len(stock_X) - 1]\n    print(\"########################### stock_X_next date:\", stock_X_next)\n    # 使用今天的交易价格，13 个指标预测明天的价格。偏移股票数据，今天的数据，目标是明天的价格。\n    stock_X = stock_X.drop(stock_X.index[len(stock_X) - 1])  # 删除最后一条数据\n    stock_y = stock_y.drop(stock_y.index[0])  # 删除第一条数据\n    # print(\"########################### stock_X date:\", stock_X)\n\n    # 删除掉close 也就是收盘价格。\n    del stock_X[\"close\"]\n    del stock_X_next[\"close\"]\n\n    model = linear_model.LinearRegression()\n    # model = KNeighborsClassifier()\n\n    model.fit(stock_X.values, stock_y)\n    # print(\"############## test_akshare & target #############\")\n    # print(\"############## coef_ & intercept_ #############\")\n    # print(model.coef_)  # 系数\n    # print(model.intercept_)  # 截断\n    next_close = model.predict([stock_X_next.values])\n    if len(next_close) == 1:\n        next_close = next_close[0]\n    sklearn_score = model.score(stock_X.values, stock_y)\n    print(\"score:\", sklearn_score)  # 评分\n    return list([code, date, next_close, sklearn_score * 100])\n\n\n# main函数入口\nif __name__ == '__main__':\n    # 使用方法传递。\n    tmp_datetime = common.run_with_args(stat_all_batch)\n"
  },
  {
    "path": "backend/supervisor/example_supervisord_conf",
    "content": "; Sample supervisor config file.\n;\n; For more information on the config file, please see:\n; http://supervisord.org/configuration.html\n;\n; Notes:\n;  - Shell expansion (\"~\" or \"$HOME\") is not supported.  Environment\n;    variables can be expanded using this syntax: \"%(ENV_HOME)s\".\n;  - Quotes around values are not supported, except in the case of\n;    the environment= options as shown below.\n;  - Comments must have a leading space: \"a=b ;comment\" not \"a=b;comment\".\n;  - Command will be truncated if it looks like a config file comment, e.g.\n;    \"command=bash -c 'foo ; bar'\" will truncate to \"command=bash -c 'foo \".\n\n[unix_http_server]\nfile=/tmp/supervisor.sock   ; the path to the socket file\n;chmod=0700                 ; socket file mode (default 0700)\n;chown=nobody:nogroup       ; socket file uid:gid owner\n;username=user              ; default is no username (open server)\n;password=123               ; default is no password (open server)\n\n;[inet_http_server]         ; inet (TCP) server disabled by default\n;port=127.0.0.1:9001        ; ip_address:port specifier, *:port for all iface\n;username=user              ; default is no username (open server)\n;password=123               ; default is no password (open server)\n\n[supervisord]\nlogfile=/tmp/supervisord.log ; main log file; default $CWD/supervisord.log\nlogfile_maxbytes=50MB        ; max main logfile bytes b4 rotation; default 50MB\nlogfile_backups=10           ; # of main logfile backups; 0 means none, default 10\nloglevel=info                ; log level; default info; others: debug,warn,trace\npidfile=/tmp/supervisord.pid ; supervisord pidfile; default supervisord.pid\nnodaemon=false               ; start in foreground if true; default false\nminfds=1024                  ; min. avail startup file descriptors; default 1024\nminprocs=200                 ; min. avail process descriptors;default 200\n;umask=022                   ; process file creation umask; default 022\n;user=chrism                 ; default is current user, required if root\n;identifier=supervisor       ; supervisord identifier, default is 'supervisor'\n;directory=/tmp              ; default is not to cd during start\n;nocleanup=true              ; don't clean up tempfiles at start; default false\n;childlogdir=/tmp            ; 'AUTO' child log dir, default $TEMP\n;environment=KEY=\"value\"     ; key value pairs to add to environment\n;strip_ansi=false            ; strip ansi escape codes in logs; def. false\n\n; The rpcinterface:supervisor section must remain in the config file for\n; RPC (supervisorctl/web interface) to work.  Additional interfaces may be\n; added by defining them in separate [rpcinterface:x] sections.\n\n[rpcinterface:supervisor]\nsupervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface\n\n; The supervisorctl section configures how supervisorctl will connect to\n; supervisord.  configure it match the settings in either the unix_http_server\n; or inet_http_server section.\n\n[supervisorctl]\nserverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket\n;serverurl=http://127.0.0.1:9001 ; use an http:// url to specify an inet socket\n;username=chris              ; should be same as in [*_http_server] if set\n;password=123                ; should be same as in [*_http_server] if set\n;prompt=mysupervisor         ; cmd line prompt (default \"supervisor\")\n;history_file=~/.sc_history  ; use readline history if available\n\n; The sample program section below shows all possible program subsection values.\n; Create one or more 'real' program: sections to be able to control them under\n; supervisor.\n\n;[program:theprogramname]\n;command=/bin/cat              ; the program (relative uses PATH, can take args)\n;process_name=%(program_name)s ; process_name expr (default %(program_name)s)\n;numprocs=1                    ; number of processes copies to start (def 1)\n;directory=/tmp                ; directory to cwd to before exec (def no cwd)\n;umask=022                     ; umask for process (default None)\n;priority=999                  ; the relative start priority (default 999)\n;autostart=true                ; start at supervisord start (default: true)\n;startsecs=1                   ; # of secs prog must stay up to be running (def. 1)\n;startretries=3                ; max # of serial start failures when starting (default 3)\n;autorestart=unexpected        ; when to restart if exited after running (def: unexpected)\n;exitcodes=0,2                 ; 'expected' exit codes used with autorestart (default 0,2)\n;stopsignal=QUIT               ; signal used to kill process (default TERM)\n;stopwaitsecs=10               ; max num secs to wait b4 SIGKILL (default 10)\n;stopasgroup=false             ; send stop signal to the UNIX process group (default false)\n;killasgroup=false             ; SIGKILL the UNIX process group (def false)\n;user=chrism                   ; setuid to this UNIX account to run the program\n;redirect_stderr=true          ; redirect proc stderr to stdout (default false)\n;stdout_logfile=/a/path        ; stdout log path, NONE for none; default AUTO\n;stdout_logfile_maxbytes=1MB   ; max # logfile bytes b4 rotation (default 50MB)\n;stdout_logfile_backups=10     ; # of stdout logfile backups (0 means none, default 10)\n;stdout_capture_maxbytes=1MB   ; number of bytes in 'capturemode' (default 0)\n;stdout_events_enabled=false   ; emit events on stdout writes (default false)\n;stderr_logfile=/a/path        ; stderr log path, NONE for none; default AUTO\n;stderr_logfile_maxbytes=1MB   ; max # logfile bytes b4 rotation (default 50MB)\n;stderr_logfile_backups=10     ; # of stderr logfile backups (0 means none, default 10)\n;stderr_capture_maxbytes=1MB   ; number of bytes in 'capturemode' (default 0)\n;stderr_events_enabled=false   ; emit events on stderr writes (default false)\n;environment=A=\"1\",B=\"2\"       ; process environment additions (def no adds)\n;serverurl=AUTO                ; override serverurl computation (childutils)\n\n; The sample eventlistener section below shows all possible eventlistener\n; subsection values.  Create one or more 'real' eventlistener: sections to be\n; able to handle event notifications sent by supervisord.\n\n;[eventlistener:theeventlistenername]\n;command=/bin/eventlistener    ; the program (relative uses PATH, can take args)\n;process_name=%(program_name)s ; process_name expr (default %(program_name)s)\n;numprocs=1                    ; number of processes copies to start (def 1)\n;events=EVENT                  ; event notif. types to subscribe to (req'd)\n;buffer_size=10                ; event buffer queue size (default 10)\n;directory=/tmp                ; directory to cwd to before exec (def no cwd)\n;umask=022                     ; umask for process (default None)\n;priority=-1                   ; the relative start priority (default -1)\n;autostart=true                ; start at supervisord start (default: true)\n;startsecs=1                   ; # of secs prog must stay up to be running (def. 1)\n;startretries=3                ; max # of serial start failures when starting (default 3)\n;autorestart=unexpected        ; autorestart if exited after running (def: unexpected)\n;exitcodes=0,2                 ; 'expected' exit codes used with autorestart (default 0,2)\n;stopsignal=QUIT               ; signal used to kill process (default TERM)\n;stopwaitsecs=10               ; max num secs to wait b4 SIGKILL (default 10)\n;stopasgroup=false             ; send stop signal to the UNIX process group (default false)\n;killasgroup=false             ; SIGKILL the UNIX process group (def false)\n;user=chrism                   ; setuid to this UNIX account to run the program\n;redirect_stderr=false         ; redirect_stderr=true is not allowed for eventlisteners\n;stdout_logfile=/a/path        ; stdout log path, NONE for none; default AUTO\n;stdout_logfile_maxbytes=1MB   ; max # logfile bytes b4 rotation (default 50MB)\n;stdout_logfile_backups=10     ; # of stdout logfile backups (0 means none, default 10)\n;stdout_events_enabled=false   ; emit events on stdout writes (default false)\n;stderr_logfile=/a/path        ; stderr log path, NONE for none; default AUTO\n;stderr_logfile_maxbytes=1MB   ; max # logfile bytes b4 rotation (default 50MB)\n;stderr_logfile_backups=10     ; # of stderr logfile backups (0 means none, default 10)\n;stderr_events_enabled=false   ; emit events on stderr writes (default false)\n;environment=A=\"1\",B=\"2\"       ; process environment additions\n;serverurl=AUTO                ; override serverurl computation (childutils)\n\n; The sample group section below shows all possible group values.  Create one\n; or more 'real' group: sections to create \"heterogeneous\" process groups.\n\n;[group:thegroupname]\n;programs=progname1,progname2  ; each refers to 'x' in [program:x] definitions\n;priority=999                  ; the relative start priority (default 999)\n\n; The [include] section can just contain the \"files\" setting.  This\n; setting can list multiple files (separated by whitespace or\n; newlines).  It can also contain wildcards.  The filenames are\n; interpreted as relative to this file.  Included files *cannot*\n; include files themselves.\n\n;[include]\n;files = relative/directory/*.ini"
  },
  {
    "path": "backend/supervisor/supervisord.conf",
    "content": "[unix_http_server]\nfile=/tmp/supervisor.sock   ; the path to the socket file\n\n[inet_http_server]         ; inet (TCP) server disabled by default\nport=*:9001        ; ip_address:port specifier, *:port for all iface\n;username=user              ; default is no username (open server)\n;password=123               ; default is no password (open server)\n\n[supervisord]\nlogfile=/tmp/supervisord.log ; main log file; default $CWD/supervisord.log\nlogfile_maxbytes=50MB        ; max main logfile bytes b4 rotation; default 50MB\nlogfile_backups=10           ; # of main logfile backups; 0 means none, default 10\nloglevel=info                ; log level; default info; others: debug,warn,trace\npidfile=/tmp/supervisord.pid ; supervisord pidfile; default supervisord.pid\nnodaemon=false               ; start in foreground if true; default false\nminfds=1024                  ; min. avail startup file descriptors; default 1024\nminprocs=200                 ; min. avail process descriptors;default 200\n\n[rpcinterface:supervisor]\nsupervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface\n\n[supervisorctl]\nserverurl=unix:///tmp/supervisor.sock ; use a unix:// URL  for a unix socket\n\n[program:init]\ncommand=/data/stock/jobs/run_init.sh\nautostart=true\nautorestart=true\nstartsecs=20\npriority=1\nstopasgroup=true\nkillasgroup=true\n\n[program:cron]\ncommand=/data/stock/jobs/run_cron.sh\nautostart=true\nautorestart=true\nstartsecs=20\npriority=1\nstopasgroup=true\nkillasgroup=true\n\n\n[program:stock-web]\ncommand=/data/stock/jobs/run_web.sh\nautostart=true\nautorestart=true\nstartsecs=20\npriority=1\nstopasgroup=true\nkillasgroup=true\n"
  },
  {
    "path": "backend/web/README.md",
    "content": ""
  },
  {
    "path": "backend/web/base.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\nimport tornado.web\nimport libs.stock_web_dic as stock_web_dic\nimport libs.common as common\nimport logging\n\n#基础handler，主要负责检查mysql的数据库链接。\nclass BaseHandler(tornado.web.RequestHandler):\n    def set_default_headers(self):\n        headers = self.request.headers\n        # logging.info('head的类型：',type(headers))\n        origin =  headers.get('origin',None)\n        logging.info(\"######################## BaseHandler ########################\")\n        logging.info(origin)\n        \n        if origin != None and origin.find(\"localhost\") > 0:\n            self.set_header(\"Access-Control-Allow-Credentials\", \"true\")\n            self.set_header(\"Access-Control-Allow-Origin\",origin)\n            self.set_header(\"Access-Control-Allow-Methods\", \"POST, GET, PUT, DELETE, OPTIONS\")\n            self.set_header(\"Access-Control-Allow-Headers\", \"x-token, authorization, Authorization, Content-Type, Access-Control-Allow-Origin, Access-Control-Allow-Headers, X-Requested-By, Access-Control-Allow-Methods\")\n            self.set_header(\"Access-Control-Expose-Headers\", \"Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma\")\n    # 同时定义一个option方法\n    def options(self):\n        self.set_status(204)\n        self.finish()\n\n    @property\n    def db(self):\n        try:\n            # check every time。\n            self.application.db.query(\"SELECT 1 \")\n        except Exception as e:\n            print(e)\n            self.application.db.reconnect()\n        return self.application.db\n\nclass LeftMenu:\n    def __init__(self, url):\n        self.leftMenuList = stock_web_dic.STOCK_WEB_DATA_LIST\n        self.current_url = url\n\n# 获得左菜单。\ndef GetLeftMenu(url):\n    return LeftMenu(url)\n"
  },
  {
    "path": "backend/web/chartHandler.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\n\nfrom tornado import gen\nimport libs.stock_web_dic as stock_web_dic\nimport web.base as webBase\nimport libs.common as common\nimport logging\nimport tornado.web\nimport matplotlib\nmatplotlib.use('Agg')\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport io\n\ndef GenImage(freq):\n    t = np.linspace(0, 10, 500)\n    y = np.sin(t * freq * 2 * 3.141)\n    fig1 = plt.figure()\n    plt.plot(t, y)\n    plt.xlabel('Time [s]')\n    memdata = io.BytesIO()\n    plt.grid(True)\n    plt.savefig(memdata, format='png')\n    image = memdata.getvalue()\n    return image\n\n\nclass ImageHandler(tornado.web.RequestHandler):\n    @gen.coroutine\n    def get(self):\n        image = GenImage(0.5)\n        self.set_header('Content-type', 'image/png')\n        self.set_header('Content-length', len(image))\n        self.write(image)\n\n# 获得页面数据。\nclass GetChartHtmlHandler(webBase.BaseHandler):\n    @gen.coroutine\n    def get(self):\n        name = self.get_argument(\"table_name\", default=None, strip=False)\n        #stockWeb = stock_web_dic.STOCK_WEB_DATA_MAP[name]\n        # self.uri_ = (\"self.request.url:\", self.request.uri)\n        # print self.uri_\n        logging.info(\"chart...\")\n        self.render(\"stock_chart.html\", entries=\"\",\n                    pythonStockVersion=common.__version__,\n                    leftMenu=webBase.GetLeftMenu(self.request.uri))\n\n"
  },
  {
    "path": "backend/web/dataEditorHandler.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\n\nfrom tornado import gen\n# import sys\n# import os\n# sys.path.append(os.path.abspath('/data/stock/libs'))\nimport libs.stock_web_dic as stock_web_dic\nimport web.base as webBase\nimport libs.common as common\nimport logging\nimport re\n\n# 获得页面数据。\nclass GetEditorHtmlHandler(webBase.BaseHandler):\n    @gen.coroutine\n    def get(self):\n        name = self.get_argument(\"table_name\", default=None, strip=False)\n        stockWeb = stock_web_dic.STOCK_WEB_DATA_MAP[name]\n        # self.uri_ = (\"self.request.url:\", self.request.uri)\n        # print self.uri_\n        self.render(\"data_editor.html\", stockWeb=stockWeb,\n                    pythonStockVersion=common.__version__,\n                    leftMenu=webBase.GetLeftMenu(self.request.uri))\n\n\n# 拼接sql，将value的key 和 value 放到一起。\ndef genSql(primary_key, param_map, join_string):\n    tmp_sql = \"\"\n    idx = 0\n    for tmp_key in primary_key:\n        tmp_val = param_map[tmp_key]\n        if idx == 0:\n            tmp_sql = \" `%s` = '%s' \" % (tmp_key, tmp_val)\n        else:\n            tmp_sql += join_string + (\" `%s` = '%s' \" % (tmp_key, tmp_val))\n        idx += 1\n    return tmp_sql\n\n\n# 获得页面数据。\nclass SaveEditorHandler(webBase.BaseHandler):\n    @gen.coroutine\n    def post(self):\n        action = self.get_argument(\"action\", default=None, strip=False)\n        logging.info(action)\n        table_name = self.get_argument(\"table_name\", default=None, strip=False)\n        stockWeb = stock_web_dic.STOCK_WEB_DATA_MAP[table_name]\n        # 临时map数组。\n        param_map = {}\n        # 支持多排序。使用shift+鼠标左键。\n        for item, val in self.request.arguments.items():\n            # 正则查找  data[1112][code] 里面的code字段\n            item_key = re.search(r\"\\]\\[(.*?)\\]\", item)\n            if item_key:\n                tmp_1 = item_key.group()\n                if tmp_1:\n                    tmp_1 = tmp_1.replace(\"][\", \"\").replace(\"]\", \"\")\n                    param_map[tmp_1] = val[0].decode(\"utf-8\")\n        #logging.info(param_map)\n        if action == \"create\":\n            logging.info(\"###########################create\")\n            # 拼接where 和 update 语句。\n            tmp_columns = \"`, `\".join(stockWeb.columns)\n            tmp_values = []\n            for tmp_key in stockWeb.columns:\n                tmp_values.append(param_map[tmp_key])\n            # 更新sql。\n            tmp_values2 = \"', '\".join(tmp_values)\n            insert_sql = \" INSERT INTO %s (`%s`) VALUES('%s'); \" % (stockWeb.table_name, tmp_columns, tmp_values2)\n            logging.info(insert_sql)\n            try:\n                self.db.execute(insert_sql)\n            except Exception as e:\n                err = {\"error\": str(e)}\n                logging.info(err)\n                self.write(err)\n                return\n\n        elif action == \"edit\":\n            logging.info(\"###########################edit\")\n            # 拼接where 和 update 语句。\n            tmp_update = genSql(stockWeb.columns, param_map, \",\")\n            tmp_where = genSql(stockWeb.primary_key, param_map, \"and\")\n            # 更新sql。\n            update_sql = \" UPDATE %s SET %s WHERE %s \" % (stockWeb.table_name, tmp_update, tmp_where)\n            logging.info(update_sql)\n            try:\n                self.db.execute(update_sql)\n            except Exception as e:\n                err = {\"error\": str(e)}\n                logging.info(err)\n                self.write(err)\n                return\n        elif action == \"remove\":\n            logging.info(\"###########################remove\")\n            # 拼接where 语句。\n            tmp_where = genSql(stockWeb.primary_key, param_map, \"and\")\n            # 更新sql。\n            delete_sql = \" DELETE FROM %s WHERE %s \" % (stockWeb.table_name, tmp_where)\n            logging.info(delete_sql)\n            try:\n                self.db.execute(delete_sql)\n            except Exception as e:\n                err = {\"error\": str(e)}\n                logging.info(err)\n                self.write(err)\n                return\n        self.write(\"{\\\"data\\\":[{}]}\")\n"
  },
  {
    "path": "backend/web/dataIndicatorsHandler.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\nfrom tornado import gen\nimport web.base as webBase\nimport logging\n\n# 首映 bokeh 画图。\nfrom bokeh.plotting import figure\nfrom bokeh.embed import components\nimport datetime\nimport libs.common as common\nimport stockstats\nimport numpy as np\nimport pandas as pd\nfrom bokeh.layouts import gridplot\nfrom bokeh.palettes import Category20\nfrom math import radians\nfrom bokeh.models import DatetimeTickFormatter\n\n\n# 获得页面数据。\nclass GetDataIndicatorsHandler(webBase.BaseHandler):\n    @gen.coroutine\n    def get(self):\n        code = self.get_argument(\"code\", default=None, strip=False)\n        logging.info(code)\n        # self.uri_ = (\"self.request.url:\", self.request.uri)\n        # print self.uri_\n        comp_list = []\n\n        try:\n            date_now = datetime.datetime.now()\n            date_end = date_now.strftime(\"%Y-%m-%d\")\n            date_start = (date_now + datetime.timedelta(days=-100)).strftime(\"%Y-%m-%d\")\n            print(code, date_start, date_end)\n\n            # open, high, close, low, volume, price_change, p_change, ma5, ma10, ma20, v_ma5, v_ma10, v_ma20, turnover\n            # 使用缓存方法。加快计算速度。\n            stock = common.get_hist_data_cache(code, date_start, date_end)\n            logging.info(stock.head(1))\n\n            # print(stock) [186 rows x 14 columns]\n            # 初始化统计类\n            # stockStat = stockstats.StockDataFrame.retype(pd.read_csv(\"002032.csv\"))\n            stockStat = stockstats.StockDataFrame.retype(stock)\n            batch_add(comp_list, stockStat)\n\n\n        except Exception as e:\n            logging.info(\"error :\", e)\n        logging.info(\"#################### GetStockHtmlHandlerEnd ####################\")\n\n        self.render(\"stock_indicators.html\", comp_list=comp_list,\n                    pythonStockVersion=common.__version__,\n                    leftMenu=webBase.GetLeftMenu(self.request.uri))\n\n# 全部指标数据汇总\nindicators_all_dic = [\n    {\n        \"title\": \"1，交易量delta指标分析\",\n        \"desc\": \"The Volume Delta (Vol ∆) \",\n        \"dic\": [\"volume\", \"volume_delta\"]\n    }, {\n        \"title\": \"2，计算n天差\",\n        \"desc\": \"可以计算，向前n天，和向后n天的差。\",\n        \"dic\": [\"close\", \"close_1_d\", \"close_2_d\", \"close_-1_d\", \"close_-2_d\"]\n    }, {\n        \"title\": \"3，n天涨跌百分百计算\",\n        \"desc\": \"可以看到，-n天数据和今天数据的百分比。\",\n        \"dic\": [\"close\", \"close_-1_r\", \"close_-2_r\"]\n    }, {\n        \"title\": \"4，CR指标\",\n        \"desc\": \"\"\"\n            http://wiki.mbalib.com/wiki/CR%E6%8C%87%E6%A0%87 价格动量指标\n            4. CR跌穿a、b、c、d四条线，再由低点向上爬升160时，为短线获利的一个良机，应适当卖出股票。\n            5. CR跌至40以下时，是建仓良机。而CR高于300~400时，应注意适当减仓。\n        \"\"\",\n        \"dic\": [\"close\",\"cr\",\"cr-ma1\",\"cr-ma2\",\"cr-ma3\"]\n    }, {\n        \"title\": \"5，最大值，最小值\",\n        \"desc\": \"\"\"\n            计算区间最大值\n            volume max of three days ago, yesterday and two days later\n            stock[\"volume_-3,2,-1_max\"]\n            volume min between 3 days ago and tomorrow\n            stock[\"volume_-3~1_min\"]\n            实际使用的时候使用 -2~2 可计算出5天的最大，最小值。\n        \"\"\",\n        \"dic\": [\"volume\",\"volume_-2~2_max\",\"volume_-2~2_min\"]\n    }, {\n        \"title\": \"6，KDJ指标\",\n        \"desc\": \"\"\"\n            http://wiki.mbalib.com/wiki/%E9%9A%8F%E6%9C%BA%E6%8C%87%E6%A0%87\n            随机指标(KDJ)一般是根据统计学的原理，通过一个特定的周期（常为9日、9周等）内出现过的最高价、最低价及最后一个计算周期的收盘价及这三者之间的比例关系，来计算最后一个计算周期的未成熟随机值RSV，然后根据平滑移动平均线的方法来计算K值、D值与J值，并绘成曲线图来研判股票走势。\n            （3）在使用中，常有J线的指标，即3乘以K值减2乘以D值（3K－2D＝J），其目的是求出K值与D值的最大乖离程度，以领先KD值找出底部和头部。J大于100时为超买，小于10时为超卖。\n        \"\"\",\n        \"dic\": [\"close\",\"kdjk\",\"kdjd\",\"kdjj\"]\n    }, {\n        \"title\": \"7，SMA指标\",\n        \"desc\": \"\"\"\n            http://wiki.mbalib.com/wiki/Sma\n            简单移动平均线（Simple Moving Average，SMA）\n            可以动态输入参数，获得几天的移动平均。\n        \"\"\",\n        \"dic\": [\"close\",\"close_5_sma\",\"close_10_sma\"]\n    }, {\n        \"title\": \"8，MACD指标\",\n        \"desc\": \"\"\"\n            http://wiki.mbalib.com/wiki/MACD\n            平滑异同移动平均线(Moving Average Convergence Divergence，简称MACD指标)，也称移动平均聚散指标\n            MACD\n            stock[\"macd\"]\n            MACD signal line\n            stock[\"macds\"]\n            MACD histogram\n            stock[\"macdh\"]\n            MACD技术分析，运用DIF线与MACD线之相交型态及直线棒高低点与背离现象，作为买卖讯号，尤其当市场股价走势呈一较为明确波段趋势时，\n            MACD 则可发挥其应有的功能，但当市场呈牛皮盘整格局，股价不上不下时，MACD买卖讯号较不明显。\n            当用MACD作分析时，亦可运用其他的技术分析指标如短期 K，D图形作为辅助工具，而且也可对买卖讯号作双重的确认。\n        \"\"\",\n        \"dic\": [\"close\",\"macd\",\"macds\",\"macdh\"]\n    }, {\n        \"title\": \"9，BOLL指标\",\n        \"desc\": \"\"\"\n        http://wiki.mbalib.com/wiki/BOLL\n            布林线指标(Bollinger Bands)\n            bolling, including upper band and lower band\n            stock[\"boll\"]\n            stock[\"boll_ub\"]\n            stock[\"boll_lb\"]\n            1、当布林线开口向上后，只要股价K线始终运行在布林线的中轨上方的时候，说明股价一直处在一个中长期上升轨道之中，这是BOLL指标发出的持股待涨信号，如果TRIX指标也是发出持股信号时，这种信号更加准确。此时，投资者应坚决持股待涨。\n            2、当布林线开口向下后，只要股价K线始终运行在布林线的中轨下方的时候，说明股价一直处在一个中长期下降轨道之中，这是BOLL指标发出的持币观望信号，如果TRIX指标也是发出持币信号时，这种信号更加准确。此时，投资者应坚决持币观望。\n        \"\"\",\n        \"dic\": [\"close\",\"boll\",\"boll_ub\",\"boll_lb\"]\n    }, {\n        \"title\": \"10，RSI指标\",\n        \"desc\": \"\"\"\n            http://wiki.mbalib.com/wiki/RSI\n            相对强弱指标（Relative Strength Index，简称RSI），也称相对强弱指数、相对力度指数\n            6 days RSI\n            stock[\"rsi_6\"]\n            12 days RSI\n            stock[\"rsi_12\"]\n            （2）强弱指标保持高于50表示为强势市场，反之低于50表示为弱势市场。\n            （3）强弱指标多在70与30之间波动。当六日指标上升到达80时，表示股市已有超买现象，如果一旦继续上升，超过90以上时，则表示已到严重超买的警戒区，股价已形成头部，极可能在短期内反转回转。\n            （4）当六日强弱指标下降至20时，表示股市有超卖现象，如果一旦继续下降至10以下时则表示已到严重超卖区域，股价极可能有止跌回升的机会。\n        \"\"\",\n        \"dic\": [\"close\",\"rsi_6\",\"rsi_12\"]\n    }, {\n        \"title\": \"11，WR指标\",\n        \"desc\": \"\"\"\n            http://wiki.mbalib.com/wiki/%E5%A8%81%E5%BB%89%E6%8C%87%E6%A0%87\n            威廉指数（Williams%Rate）该指数是利用摆动点来度量市场的超买超卖现象。\n            10 days WR\n            stock[\"wr_10\"]\n            6 days WR\n            stock[\"wr_6\"]\n        \"\"\",\n        \"dic\": [\"close\",\"wr_10\",\"wr_6\"]\n    }, {\n        \"title\": \"12，CCI指标\",\n        \"desc\": \"\"\"\n            http://wiki.mbalib.com/wiki/%E9%A1%BA%E5%8A%BF%E6%8C%87%E6%A0%87\n            顺势指标又叫CCI指标，其英文全称为“Commodity Channel Index”，\n            是由美国股市分析家唐纳德·蓝伯特（Donald Lambert）所创造的，是一种重点研判股价偏离度的股市分析工具。\n             CCI, default to 14 days\n            stock[\"cci\"]\n             20 days CCI\n            stock[\"cci_20\"]\n            1、当CCI指标从下向上突破﹢100线而进入非常态区间时，表明股价脱离常态而进入异常波动阶段，\n              中短线应及时买入，如果有比较大的成交量配合，买入信号则更为可靠。\n            2、当CCI指标从上向下突破﹣100线而进入另一个非常态区间时，表明股价的盘整阶段已经结束，\n              将进入一个比较长的寻底过程，投资者应以持币观望为主。\n        \"\"\",\n        \"dic\": [\"close\",\"cci\",\"cci_20\"]\n    }, {\n        \"title\": \"13，TR、ATR指标\",\n        \"desc\": \"\"\"\n            http://wiki.mbalib.com/wiki/%E5%9D%87%E5%B9%85%E6%8C%87%E6%A0%87\n            均幅指标（Average True Ranger,ATR）\n            均幅指标（ATR）是取一定时间周期内的股价波动幅度的移动平均值，主要用于研判买卖时机。\n            TR (true range)\n            stock[\"tr\"]\n             ATR (Average True Range)\n            stock[\"atr\"]\n            均幅指标无论是从下向上穿越移动平均线，还是从上向下穿越移动平均线时，都是一种研判信号。\n        \"\"\",\n        \"dic\": [\"close\",\"tr\",\"atr\"]\n    }, {\n        \"title\": \"14，DMA指标\",\n        \"desc\": \"\"\"\n            http://wiki.mbalib.com/wiki/DMA\n            DMA指标（Different of Moving Average）又叫平行线差指标，是目前股市分析技术指标中的一种中短期指标，它常用于大盘指数和个股的研判。\n            DMA, difference of 10 and 50 moving average\n            stock[\"dma\"]\n        \"\"\",\n        \"dic\": [\"close\",\"dma\"]\n    }, {\n        \"title\": \"15，DMI，+DI，-DI，DX，ADX，ADXR指标\",\n        \"desc\": \"\"\"\n            http://wiki.mbalib.com/wiki/DMI\n            动向指数Directional Movement Index,DMI）\n            http://wiki.mbalib.com/wiki/ADX\n            平均趋向指标（Average Directional Indicator，简称ADX）\n            http://wiki.mbalib.com/wiki/%E5%B9%B3%E5%9D%87%E6%96%B9%E5%90%91%E6%8C%87%E6%95%B0%E8%AF%84%E4%BC%B0\n            平均方向指数评估（ADXR）实际是今日ADX与前面某一日的ADX的平均值。ADXR在高位与ADX同步下滑，可以增加对ADX已经调头的尽早确认。\n            ADXR是ADX的附属产品，只能发出一种辅助和肯定的讯号，并非入市的指标，而只需同时配合动向指标(DMI)的趋势才可作出买卖策略。\n            在应用时，应以ADX为主，ADXR为辅。\n        \"\"\",\n        \"dic\": [\"close\",\"pdi\",\"mdi\",\"dx\",\"adx\",\"adxr\"]\n    }, {\n        \"title\": \"16，TRIX，MATRIX指标\",\n        \"desc\": \"\"\"\n            http://wiki.mbalib.com/wiki/TRIX\n            TRIX指标又叫三重指数平滑移动平均指标（Triple Exponentially Smoothed Average）\n        \"\"\",\n        \"dic\": [\"close\",\"trix\",\"trix_9_sma\"]\n    }, {\n        \"title\": \"17，VR，MAVR指标\",\n        \"desc\": \"\"\"\n            http://wiki.mbalib.com/wiki/%E6%88%90%E4%BA%A4%E9%87%8F%E6%AF%94%E7%8E%87\n            成交量比率（Volumn Ratio，VR）（简称VR），是一项通过分析股价上升日成交额（或成交量，下同）与股价下降日成交额比值，\n            从而掌握市场买卖气势的中期技术指标。\n             VR, default to 26 days\n            stock[\"vr\"]\n             MAVR is the simple moving average of VR\n            stock[\"vr_6_sma\"]\n        \"\"\",\n        \"dic\": [\"close\",\"vr\",\"vr_6_sma\"]\n    }\n]\n# 配置数据\nindicators_dic = [\n     {\n        \"title\": \"6，KDJ指标\",\n        \"desc\": \"\"\"\n            http://wiki.mbalib.com/wiki/%E9%9A%8F%E6%9C%BA%E6%8C%87%E6%A0%87\n            随机指标(KDJ)一般是根据统计学的原理，通过一个特定的周期（常为9日、9周等）内出现过的最高价、最低价及最后一个计算周期的收盘价及这三者之间的比例关系，来计算最后一个计算周期的未成熟随机值RSV，然后根据平滑移动平均线的方法来计算K值、D值与J值，并绘成曲线图来研判股票走势。\n            （3）在使用中，常有J线的指标，即3乘以K值减2乘以D值（3K－2D＝J），其目的是求出K值与D值的最大乖离程度，以领先KD值找出底部和头部。J大于100时为超买，小于10时为超卖。\n        \"\"\",\n        \"dic\": [\"close\",\"kdjk\",\"kdjd\",\"kdjj\"]\n    }, {\n        \"title\": \"7，SMA指标\",\n        \"desc\": \"\"\"\n            http://wiki.mbalib.com/wiki/Sma\n            简单移动平均线（Simple Moving Average，SMA）\n            可以动态输入参数，获得几天的移动平均。\n        \"\"\",\n        \"dic\": [\"close\",\"close_5_sma\",\"close_10_sma\"]\n    }, {\n        \"title\": \"8，MACD指标\",\n        \"desc\": \"\"\"\n            http://wiki.mbalib.com/wiki/MACD\n            平滑异同移动平均线(Moving Average Convergence Divergence，简称MACD指标)，也称移动平均聚散指标\n            MACD\n            stock[\"macd\"]\n            MACD signal line\n            stock[\"macds\"]\n            MACD histogram\n            stock[\"macdh\"]\n            MACD技术分析，运用DIF线与MACD线之相交型态及直线棒高低点与背离现象，作为买卖讯号，尤其当市场股价走势呈一较为明确波段趋势时，\n            MACD 则可发挥其应有的功能，但当市场呈牛皮盘整格局，股价不上不下时，MACD买卖讯号较不明显。\n            当用MACD作分析时，亦可运用其他的技术分析指标如短期 K，D图形作为辅助工具，而且也可对买卖讯号作双重的确认。\n        \"\"\",\n        \"dic\": [\"close\",\"macd\",\"macds\",\"macdh\"]\n    }, {\n        \"title\": \"9，BOLL指标\",\n        \"desc\": \"\"\"\n        http://wiki.mbalib.com/wiki/BOLL\n            布林线指标(Bollinger Bands)\n            bolling, including upper band and lower band\n            stock[\"boll\"]\n            stock[\"boll_ub\"]\n            stock[\"boll_lb\"]\n            1、当布林线开口向上后，只要股价K线始终运行在布林线的中轨上方的时候，说明股价一直处在一个中长期上升轨道之中，这是BOLL指标发出的持股待涨信号，如果TRIX指标也是发出持股信号时，这种信号更加准确。此时，投资者应坚决持股待涨。\n            2、当布林线开口向下后，只要股价K线始终运行在布林线的中轨下方的时候，说明股价一直处在一个中长期下降轨道之中，这是BOLL指标发出的持币观望信号，如果TRIX指标也是发出持币信号时，这种信号更加准确。此时，投资者应坚决持币观望。\n        \"\"\",\n        \"dic\": [\"close\",\"boll\",\"boll_ub\",\"boll_lb\"]\n    }, {\n        \"title\": \"10，RSI指标\",\n        \"desc\": \"\"\"\n            http://wiki.mbalib.com/wiki/RSI\n            相对强弱指标（Relative Strength Index，简称RSI），也称相对强弱指数、相对力度指数\n            6 days RSI\n            stock[\"rsi_6\"]\n            12 days RSI\n            stock[\"rsi_12\"]\n            （2）强弱指标保持高于50表示为强势市场，反之低于50表示为弱势市场。\n            （3）强弱指标多在70与30之间波动。当六日指标上升到达80时，表示股市已有超买现象，如果一旦继续上升，超过90以上时，则表示已到严重超买的警戒区，股价已形成头部，极可能在短期内反转回转。\n            （4）当六日强弱指标下降至20时，表示股市有超卖现象，如果一旦继续下降至10以下时则表示已到严重超卖区域，股价极可能有止跌回升的机会。\n        \"\"\",\n        \"dic\": [\"close\",\"rsi_6\",\"rsi_12\"]\n    },{\n        \"title\": \"12，CCI指标\",\n        \"desc\": \"\"\"\n            http://wiki.mbalib.com/wiki/%E9%A1%BA%E5%8A%BF%E6%8C%87%E6%A0%87\n            顺势指标又叫CCI指标，其英文全称为“Commodity Channel Index”，\n            是由美国股市分析家唐纳德·蓝伯特（Donald Lambert）所创造的，是一种重点研判股价偏离度的股市分析工具。\n             CCI, default to 14 days\n            stock[\"cci\"]\n             20 days CCI\n            stock[\"cci_20\"]\n            1、当CCI指标从下向上突破﹢100线而进入非常态区间时，表明股价脱离常态而进入异常波动阶段，\n              中短线应及时买入，如果有比较大的成交量配合，买入信号则更为可靠。\n            2、当CCI指标从上向下突破﹣100线而进入另一个非常态区间时，表明股价的盘整阶段已经结束，\n              将进入一个比较长的寻底过程，投资者应以持币观望为主。\n        \"\"\",\n        \"dic\": [\"close\",\"cci\",\"cci_20\"]\n    }\n]\n\n\n# 批量添加数据。\ndef batch_add(comp_list, stockStat):\n    for conf in indicators_dic:\n        logging.info(conf)\n\n        comp_list.append(add_plot(stockStat, conf))\n\n\n# 增加画图方法\ndef add_plot(stockStat, conf):\n    p_list = []\n    logging.info(\"############################\", type(conf[\"dic\"]))\n    # 循环 多个line 信息。\n    for key, val in enumerate(conf[\"dic\"]):\n        logging.info(key)\n        logging.info(val)\n\n        p1 = figure(width=1000, height=150, x_axis_type=\"datetime\")\n        # add renderers\n        stockStat[\"date\"] = pd.to_datetime(stockStat.index.values)\n        # [\"volume\",\"volume_delta\"]\n        # 设置20个颜色循环，显示0 2 4 6 号序列。\n        p1.line(stockStat[\"date\"], stockStat[val], color=Category20[20][key * 2])\n\n        # Set date format for x axis 格式化。\n        p1.xaxis.formatter = DatetimeTickFormatter(\n            hours=[\"%Y-%m-%d\"], days=[\"%Y-%m-%d\"],\n            months=[\"%Y-%m-%d\"], years=[\"%Y-%m-%d\"])\n        # p1.xaxis.major_label_orientation = radians(30) #可以旋转一个角度。\n\n        p_list.append([p1])\n\n    gp = gridplot(p_list)\n    script, div = components(gp)\n    return {\n        \"script\": script,\n        \"div\": div,\n        \"title\": conf[\"title\"],\n        \"desc\": conf[\"desc\"]\n    }\n"
  },
  {
    "path": "backend/web/dataTableHandler.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\nimport json\nfrom tornado import gen\nimport libs.common as common\nimport libs.stock_web_dic as stock_web_dic\nimport web.base as webBase\nimport logging\nimport datetime\n\n# info 蓝色 云财经\n# success 绿色\n#  danger 红色 东方财富\n#  warning 黄色\nWEB_EASTMONEY_URL = u\"\"\"\n    <a class='btn btn-danger btn-xs tooltip-danger' data-rel=\"tooltip\" data-placement=\"right\" data-original-title=\"东方财富，股票详细地址，新窗口跳转。\"\n    href='http://quote.eastmoney.com/%s.html' target='_blank'>东财</a>\n    \n    <a class='btn btn-success btn-xs tooltip-success' data-rel=\"tooltip\" data-placement=\"right\" data-original-title=\"本地MACD，KDJ等指标，本地弹窗窗口，数据加载中，请稍候。\"\n    onclick=\"showIndicatorsWindow('%s');\">指标</a>\n    \n    <a class='btn btn-warning btn-xs tooltip-warning' data-rel=\"tooltip\" data-placement=\"right\" data-original-title=\"东方财富，研报地址，本地弹窗窗口。\"\n    onclick=\"showDFCFWindow('%s');\">东研</a>\n    \n    \n    \"\"\"\n# 和在dic中的字符串一致。字符串前面都不特别声明是u\"\"\neastmoney_name = \"查看股票\"\n\n\n# 获得页面数据，进入页面中。\nclass GetStockHtmlHandler(webBase.BaseHandler):\n    @gen.coroutine\n    def get(self):\n        name = self.get_argument(\"table_name\", default=None, strip=False)\n        tableInfo = stock_web_dic.STOCK_WEB_DATA_MAP[name]\n        # self.uri_ = (\"self.request.url:\", self.request.uri)\n        # print self.uri_\n        date_now = datetime.datetime.now()\n        date_now_str = date_now.strftime(\"%Y%m%d\")\n        # 每天的 16 点前显示昨天数据。\n        if date_now.hour < 16:\n            date_now_str = (date_now + datetime.timedelta(days=-1)).strftime(\"%Y%m%d\")\n\n        # try:\n        #     # 增加columns 字段中的【查看股票 东方财富】\n        #     logging.info(eastmoney_name in tableInfo.column_names)\n        #     if eastmoney_name in tableInfo.column_names:\n        #         tmp_idx = tableInfo.column_names.index(eastmoney_name)\n        #         logging.info(tmp_idx)\n        #         try:\n        #             # 防止重复插入数据。可能会报错。\n        #             tableInfo.columns.remove(\"eastmoney_url\")\n        #         except Exception as e:\n        #             print(\"error :\", e)\n        #         tableInfo.columns.insert(tmp_idx, \"eastmoney_url\")\n        # except Exception as e:\n        #     print(\"error :\", e)\n        logging.info(\"####################GetStockHtmlHandlerEnd\")\n        self.render(\"tableInfo.html\", tableInfo=tableInfo, date_now=date_now_str,\n                    pythonStockVersion=common.__version__,\n                    leftMenu=webBase.GetLeftMenu(self.request.uri))\n\n\n# 获得股票数据内容。\nclass GetStockDataHandler(webBase.BaseHandler):\n    def get(self):\n\n        self.set_header('Content-Type', 'application/json;charset=UTF-8')\n\n        logging.info(\"######################## GetStockDataHandler ########################\")\n        # 获得分页参数。\n        page_param = self.get_argument(\"page\", default=0, strip=False)\n        limit_param = self.get_argument(\"limit\", default=10, strip=False)\n\n        name_param = self.get_argument(\"name\", default=\"stock_zh_ah_name\", strip=False)\n        type_param = self.get_argument(\"type\", default=None, strip=False)\n        date_param = self.get_argument(\"date\", default=None, strip=False)\n        code_param = self.get_argument(\"code\", default=None, strip=False)\n\n        logging.info(f\"page param: {page_param}, {limit_param}, {type_param}, {date_param}, {code_param}\")\n\n\n        if name_param == \":tableName\":\n            obj = {\n            \"code\": 20000,\n            \"message\": \"success\",\n            \"draw\": 0,\n            \"data\": []\n            }\n            # logging.info(\"####################\")\n            # logging.info(obj)\n            self.write(json.dumps(obj))\n            return\n\n\n        tableInfo = stock_web_dic.STOCK_WEB_DATA_MAP[name_param]\n\n        \n        order_by_column = []\n        order_by_dir = []\n        # 支持多排序。使用shift+鼠标左键。\n        for item, val in self.request.arguments.items():\n            # logging.info(\"item: %s, val: %s\" % (item, val) )\n            if str(item).startswith(\"order[\"):\n                print(\"order:\", item, \",val:\", val[0])\n            if str(item).startswith(\"order[\") and str(item).endswith(\"[column]\"):\n                order_by_column.append(int(val[0]))\n            if str(item).startswith(\"order[\") and str(item).endswith(\"[dir]\"):\n                order_by_dir.append(val[0].decode(\"utf-8\"))  # bytes转换字符串\n\n        search_by_column = []\n        search_by_data = []\n\n        # 返回search字段。\n        for item, val in self.request.arguments.items():\n            # logging.info(\"item: %s, val: %s\" % (item, val))\n            if str(item).startswith(\"columns[\") and str(item).endswith(\"[search][value]\"):\n                logging.info(\"item: %s, val: %s\" % (item, val))\n                str_idx = item.replace(\"columns[\", \"\").replace(\"][search][value]\", \"\")\n                int_idx = int(str_idx)\n                # 找到字符串\n                str_val = val[0].decode(\"utf-8\")\n                if str_val != \"\":  # 字符串。\n                    search_by_column.append(tableInfo.columns[int_idx])\n                    search_by_data.append(val[0].decode(\"utf-8\"))  # bytes转换字符串\n\n        # 打印日志。\n        search_sql = \"\"\n        search_idx = 0\n\n        logging.info(\"################# search_by_column #################\")\n\n        logging.info(search_by_column)\n        logging.info(search_by_data)\n        for item in search_by_column:\n            val = search_by_data[search_idx]\n            logging.info(\"idx: %s, column: %s, value: %s \" % (search_idx, item, val))\n            # 查询sql\n            if search_idx == 0:\n                search_sql = \" WHERE `%s` = '%s' \" % (item, val)\n            else:\n                search_sql = search_sql + \" AND `%s` = '%s' \" % (item, val)\n            search_idx = search_idx + 1\n\n        if date_param:\n            if \"WHERE\" not in search_sql:\n                search_sql += f\" WHERE `date` = '{date_param}' \"\n            else:\n                search_sql += f\" AND `date` = '{date_param}' \"\n        \n        if code_param:\n            if \"WHERE\" not in search_sql:\n                search_sql += f\" WHERE `code` = '{code_param}' \"\n            else:\n                search_sql += f\" AND `code` = '{code_param}' \"\n        \n        # print(\"tableInfo :\", stock_web)\n        order_by_sql = \"\"\n        # 增加排序。\n        if len(order_by_column) != 0 and len(order_by_dir) != 0:\n            order_by_sql = \"  ORDER BY \"\n            idx = 0\n            for key in order_by_column:\n                # 找到排序字段和dir。\n                col_tmp = tableInfo.columns[key]\n                dir_tmp = order_by_dir[idx]\n                if idx != 0:\n                    order_by_sql += \" , %s %s\" % (col_tmp, dir_tmp)\n                else:\n                    order_by_sql += \"  %s %s\" % (col_tmp, dir_tmp)\n                idx += 1\n        # 查询数据库。\n        limit_sql = \"\"\n        if int(limit_param) > 0:\n            start = ( int(page_param) - 1 ) * int(limit_param)\n            limit_sql = f\" LIMIT {start} , {limit_param} \"\n        sql = \" SELECT * FROM `%s` %s %s %s \" % (\n            tableInfo.table_name, search_sql, order_by_sql, limit_sql)\n        count_sql = \" SELECT count(1) as num FROM `%s` %s \" % (tableInfo.table_name, search_sql)\n\n        logging.info(\"select sql : \" + sql)\n        logging.info(\"count sql : \" + count_sql)\n        stock_web_list = self.db.query(sql)\n\n        stock_web_size = self.db.query(count_sql)\n        logging.info(\"tableInfoList size : %s \" % stock_web_size)\n\n        # 动态表格展示：\n        table_columns = []\n        try:\n            tmp_len = len(tableInfo.columns)\n            logging.info(\"ableInfo.columns tmp_len : %s \" % tmp_len)\n            # 循环数据，转换成对象，放入到数组中，方便前端 vue table 循环使用。\n            for tmp_idx in range(0, tmp_len):\n                logging.info(tmp_idx)\n\n                column = tableInfo.columns[tmp_idx]\n                column_name = tableInfo.column_names[tmp_idx]\n\n                tpm_column_obj = {\n                    \"column\": column,\n                    \"columnName\" : column_name\n                }\n                table_columns.append(tpm_column_obj)\n               \n        except Exception as e:\n            print(\"error :\", e)\n\n        obj = {\n            \"code\": 20000,\n            \"message\": \"success\",\n            \"draw\": 0,\n            \"tableName\" : tableInfo.name,\n            \"tableColumns\":  table_columns,\n            \"total\": stock_web_size[0][\"num\"],\n            \"recordsTotal\": stock_web_size[0][\"num\"],\n            \"recordsFiltered\": stock_web_size[0][\"num\"],\n            \"data\": stock_web_list\n        }\n        # logging.info(\"####################\")\n        # logging.info(obj)\n        self.write(json.dumps(obj))\n"
  },
  {
    "path": "backend/web/demo-chart.py",
    "content": "\"\"\"\nThis example demonstrates how to embed matplotlib WebAgg interactive\nplotting in your own web application and framework.  It is not\nnecessary to do all this if you merely want to display a plot in a\nbrowser or use matplotlib's built-in Tornado-based server \"on the\nside\".\n\nThe framework being used must support web sockets.\n\"\"\"\n\nimport io\n\ntry:\n    import tornado\nexcept ImportError:\n    raise RuntimeError(\"This example requires tornado.\")\nimport tornado.web\nimport tornado.httpserver\nimport tornado.ioloop\nimport tornado.websocket\n\nfrom matplotlib.backends.backend_webagg_core import (\n    FigureManagerWebAgg, new_figure_manager_given_figure)\nfrom matplotlib.figure import Figure\n\nimport numpy as np\n\nimport json\n\n\ndef create_figure():\n    \"\"\"\n    Creates a simple example figure.\n    \"\"\"\n    fig = Figure()\n    a = fig.add_subplot(111)\n    t = np.arange(0.0, 3.0, 0.01)\n    s = np.sin(2 * np.pi * t)\n    a.plot(t, s)\n    return fig\n\n\n# The following is the content of the web page.  You would normally\n# generate this using some sort of template facility in your web\n# framework, but here we just use Python string formatting.\nhtml_content = \"\"\"\n<html>\n  <head>\n    <!-- TODO: There should be a way to include all of the required javascript\n               and CSS so matplotlib can add to the set in the future if it\n               needs to. -->\n    <link rel=\"stylesheet\" href=\"_static/css/page.css\" type=\"text/css\">\n    <link rel=\"stylesheet\" href=\"_static/css/boilerplate.css\" type=\"text/css\" />\n    <link rel=\"stylesheet\" href=\"_static/css/fbm.css\" type=\"text/css\" />\n    <link rel=\"stylesheet\" href=\"_static/jquery/css/themes/base/jquery-ui.min.css\" >\n    <script src=\"_static/jquery/js/jquery-1.11.3.min.js\"></script>\n    <script src=\"_static/jquery/js/jquery-ui.min.js\"></script>\n    <script src=\"mpl.js\"></script>\n\n    <script>\n      /* This is a callback that is called when the user saves\n         (downloads) a file.  Its purpose is really to map from a\n         figure and file format to a url in the application. */\n      function ondownload(figure, format) {\n        window.open('download.' + format, '_blank');\n      };\n\n      $(document).ready(\n        function() {\n          /* It is up to the application to provide a websocket that the figure\n             will use to communicate to the server.  This websocket object can\n             also be a \"fake\" websocket that underneath multiplexes messages\n             from multiple figures, if necessary. */\n          var websocket_type = mpl.get_websocket_type();\n          var websocket = new websocket_type(\"%(ws_uri)sws\");\n\n          // mpl.figure creates a new figure on the webpage.\n          var fig = new mpl.figure(\n              // A unique numeric identifier for the figure\n              %(fig_id)s,\n              // A websocket object (or something that behaves like one)\n              websocket,\n              // A function called when a file type is selected for download\n              ondownload,\n              // The HTML element in which to place the figure\n              $('div#figure'));\n        }\n      );\n    </script>\n\n    <title>matplotlib</title>\n  </head>\n\n  <body>\n    <div id=\"figure\">\n    </div>\n  </body>\n</html>\n\"\"\"\n\n\nclass MyApplication(tornado.web.Application):\n    class MainPage(tornado.web.RequestHandler):\n        \"\"\"\n        Serves the main HTML page.\n        \"\"\"\n\n        def get(self):\n            manager = self.application.manager\n            ws_uri = \"ws://{req.host}/\".format(req=self.request)\n            content = html_content % {\n                \"ws_uri\": ws_uri, \"fig_id\": manager.num}\n            self.write(content)\n\n    class MplJs(tornado.web.RequestHandler):\n        \"\"\"\n        Serves the generated matplotlib javascript file.  The content\n        is dynamically generated based on which toolbar functions the\n        user has defined.  Call `FigureManagerWebAgg` to get its\n        content.\n        \"\"\"\n\n        def get(self):\n            self.set_header('Content-Type', 'application/javascript')\n            js_content = FigureManagerWebAgg.get_javascript()\n\n            self.write(js_content)\n\n    class Download(tornado.web.RequestHandler):\n        \"\"\"\n        Handles downloading of the figure in various file formats.\n        \"\"\"\n\n        def get(self, fmt):\n            manager = self.application.manager\n\n            mimetypes = {\n                'ps': 'application/postscript',\n                'eps': 'application/postscript',\n                'pdf': 'application/pdf',\n                'svg': 'image/svg+xml',\n                'png': 'image/png',\n                'jpeg': 'image/jpeg',\n                'tif': 'image/tiff',\n                'emf': 'application/emf'\n            }\n\n            self.set_header('Content-Type', mimetypes.get(fmt, 'binary'))\n\n            buff = io.BytesIO()\n            manager.canvas.print_figure(buff, format=fmt)\n            self.write(buff.getvalue())\n\n    class WebSocket(tornado.websocket.WebSocketHandler):\n        \"\"\"\n        A websocket for interactive communication between the plot in\n        the browser and the server.\n\n        In addition to the methods required by tornado, it is required to\n        have two callback methods:\n\n            - ``send_json(json_content)`` is called by matplotlib when\n              it needs to send json to the browser.  `json_content` is\n              a JSON tree (Python dictionary), and it is the responsibility\n              of this implementation to encode it as a string to send over\n              the socket.\n\n            - ``send_binary(blob)`` is called to send binary image data\n              to the browser.\n        \"\"\"\n        supports_binary = True\n\n        def open(self):\n            # Register the websocket with the FigureManager.\n            manager = self.application.manager\n            manager.add_web_socket(self)\n            if hasattr(self, 'set_nodelay'):\n                self.set_nodelay(True)\n\n        def on_close(self):\n            # When the socket is closed, deregister the websocket with\n            # the FigureManager.\n            manager = self.application.manager\n            manager.remove_web_socket(self)\n\n        def on_message(self, message):\n            # The 'supports_binary' message is relevant to the\n            # websocket itself.  The other messages get passed along\n            # to matplotlib as-is.\n\n            # Every message has a \"type\" and a \"figure_id\".\n            message = json.loads(message)\n            if message['type'] == 'supports_binary':\n                self.supports_binary = message['value']\n            else:\n                manager = self.application.manager\n                manager.handle_json(message)\n\n        def send_json(self, content):\n            self.write_message(json.dumps(content))\n\n        def send_binary(self, blob):\n            if self.supports_binary:\n                self.write_message(blob, binary=True)\n            else:\n                data_uri = \"data:image/png;base64,{0}\".format(\n                    blob.encode('base64').replace('\\n', ''))\n                self.write_message(data_uri)\n\n    def __init__(self, figure):\n        self.figure = figure\n        self.manager = new_figure_manager_given_figure(\n            id(figure), figure)\n\n        super(MyApplication, self).__init__([\n            # Static files for the CSS and JS\n            (r'/_static/(.*)',\n             tornado.web.StaticFileHandler,\n             {'path': FigureManagerWebAgg.get_static_file_path()}),\n\n            # The page that contains all of the pieces\n            ('/', self.MainPage),\n\n            ('/mpl.js', self.MplJs),\n\n            # Sends images and events to the browser, and receives\n            # events from the browser\n            ('/ws', self.WebSocket),\n\n            # Handles the downloading (i.e., saving) of static images\n            (r'/download.([a-z0-9.]+)', self.Download),\n        ], debug=True)\n\n\nif __name__ == \"__main__\":\n    figure = create_figure()\n    application = MyApplication(figure)\n    http_server = tornado.httpserver.HTTPServer(application)\n    http_server.listen(9999)\n\n    print(\"http://127.0.0.1:9090/\")\n    print(\"Press Ctrl+C to quit\")\n\n    tornado.ioloop.IOLoop.instance().start()\n"
  },
  {
    "path": "backend/web/main.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\n\nimport os.path\nimport torndb\nimport tornado.escape\nfrom tornado import gen\nimport tornado.httpserver\nimport tornado.ioloop\nimport tornado.options\nimport libs.common as common\nimport libs.stock_web_dic as stock_web_dic\nimport web.dataTableHandler as dataTableHandler\nimport web.dataEditorHandler as dataEditorHandler\nimport web.dataIndicatorsHandler as dataIndicatorsHandler\nimport web.base as webBase\nimport pandas as pd\nimport numpy as np\nimport akshare as ak\nimport bokeh as bh\nimport sqlalchemy\nimport json\n\nclass Application(tornado.web.Application):\n    def __init__(self):\n        handlers = [\n            # 设置路由\n            (r\"/\", HomeHandler),\n            (r\"/stock/\", HomeHandler),\n            (r\"/api/v1/package_verison\", PackageVersionHandler),# 包版本\n            (r\"/api/v1/menu_list\", MenuListHandler), # 菜单接口\n            (r\"/test_akshare\", TestHandler),# 测试页面，做写js 测试。\n            (r\"/test2\", Test2Handler),# 测试页面，做写js 测试。\n            # 使用datatable 展示报表数据模块。\n            (r\"/api/v1/api_data\", dataTableHandler.GetStockDataHandler),\n            (r\"/stock/data\", dataTableHandler.GetStockHtmlHandler),\n            # 数据修改dataEditor。\n            (r\"/data/editor\", dataEditorHandler.GetEditorHtmlHandler),\n            (r\"/data/editor/save\", dataEditorHandler.SaveEditorHandler),\n            # 获得股票指标数据。\n            (r\"/api/v1/data/indicators\", dataIndicatorsHandler.GetDataIndicatorsHandler),\n        ]\n        settings = dict(  # 配置\n            template_path=os.path.join(os.path.dirname(__file__), \"templates\"),\n            static_path=os.path.join(os.path.dirname(__file__), \"static\"),\n            xsrf_cookies=False,  # True,\n            # cookie加密\n            cookie_secret=\"027bb1b670eddf0392cdda8709268a17b58b7\",\n            debug=True,\n            default_encoding=\"utf-8\",\n        )\n        super(Application, self).__init__(handlers, **settings)\n        # Have one global connection to the blog DB across all handlers\n        self.db = torndb.Connection(\n            charset=\"utf8\", max_idle_time=3600, connect_timeout=1000,\n            host=common.MYSQL_HOST, database=common.MYSQL_DB,\n            user=common.MYSQL_USER, password=common.MYSQL_PWD)\n\n\n# 获得包版本 handler。\nclass PackageVersionHandler(webBase.BaseHandler):\n    @gen.coroutine\n    def get(self):\n        pandasVersion = pd.__version__\n        numpyVersion = np.__version__\n        sqlalchemyVersion = sqlalchemy.__version__\n        akshareVersion = ak.__version__\n        bokehVersion = bh.__version__\n        # 返回包的版本信息。\n        obj = {\n            \"code\": 20000,\n            \"message\": \"success\",\n            \"pandasVersion\" : pandasVersion,\n            \"numpyVersion\" : numpyVersion,\n            \"sqlalchemyVersion\" : sqlalchemyVersion,\n            \"akshareVersion\" : akshareVersion,\n            \"bokehVersion\" : bokehVersion,\n             \"stockstatsVersion\": \"0.3.2\"\n        }\n        # logging.info(\"####################\")\n        # logging.info(obj)\n        self.write(json.dumps(obj))\n\n\n# 获得菜单列表数据 handler。\nclass MenuListHandler(webBase.BaseHandler):\n    @gen.coroutine\n    def get(self):\n        \n        leftMenuList = stock_web_dic.STOCK_WEB_DATA_LIST\n        out_data = []\n        menu_name = ''\n        menu_children = []\n        index = 0\n        for table_info in leftMenuList:\n            print(table_info.name)\n            index = index + 1\n            # 使用 children 作为二级菜单。\n            tmp_menu = {\n                    \"name\": table_info.name,\n                    \"path\": \"/stock/table/\" + table_info.table_name\n            }\n            menu_children.append(tmp_menu)\n\n            # 使用 type作为 一级目录\n            if menu_name != table_info.type or index == len(leftMenuList):\n                # 进行数据循环\n                if menu_name != '' :\n                    if index != len(leftMenuList):\n                        menu_children.pop() # 删除当前的节点信息。\n                    tmp_children = list(menu_children)\n                    tmp_menu2 = {\n                        \"name\": menu_name,\n                        \"path\": \"#\",\n                        \"children\": tmp_children\n                    }\n                    # 下一个数据清空和放置。\n                    menu_children = []\n                    menu_children.append(tmp_menu)\n\n                    out_data.append(tmp_menu2)\n                menu_name = table_info.type\n        \n        obj = {\n            \"code\": 20000,\n            \"message\": \"success\",\n            \"data\": out_data\n        }\n        print(out_data)\n        # self.write(json.dumps(o\n        self.write(json.dumps(obj))\n\n\n# 首页handler。\nclass HomeHandler(webBase.BaseHandler):\n    @gen.coroutine\n    def get(self):\n        print(\"################## index.html ##################\")\n        pandasVersion = pd.__version__\n        numpyVersion = np.__version__\n        akshareVersion = ak.__version__\n        bokehVersion = bh.__version__\n        #stockstatsVersion = ss.__version__ # 没有这个函数，但是好久不更新了\n        # https://github.com/jealous/stockstats\n        self.render(\"index.html\", pandasVersion=pandasVersion, numpyVersion=numpyVersion,\n                    akshareVersion=akshareVersion, bokehVersion=bokehVersion,\n                    stockstatsVersion=\"0.3.2\",\n                    pythonStockVersion = common.__version__,\n                    leftMenu=webBase.GetLeftMenu(self.request.uri))\nclass TestHandler(webBase.BaseHandler):\n    @gen.coroutine\n    def get(self):\n        self.render(\"test_akshare.html\", entries=\"hello\",\n                    pythonStockVersion=common.__version__,\n                    leftMenu=webBase.GetLeftMenu(self.request.uri))\nclass Test2Handler(webBase.BaseHandler):\n    @gen.coroutine\n    def get(self):\n        self.render(\"test2.html\", entries=\"hello\",\n                    pythonStockVersion=common.__version__,\n                    leftMenu=webBase.GetLeftMenu(self.request.uri))\n\ndef main():\n    tornado.options.parse_command_line()\n    http_server = tornado.httpserver.HTTPServer(Application())\n    port = 9090\n    http_server.listen(port)\n    # tornado.options.options.logging = \"debug\"\n    tornado.options.parse_command_line()\n\n    tornado.ioloop.IOLoop.current().start()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "backend/web/minstServingHandler.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\nimport os.path\nimport json\nimport subprocess\nimport torndb\nimport tornado.escape\nfrom tornado import gen\nimport tornado.httpserver\nimport tornado.ioloop\nimport tornado.options\nimport tornado.web\nimport web.base as webBase\nimport logging\nimport numpy as np\nfrom PIL import Image\nfrom PIL import ImageOps\nimport base64\nimport io #python2 import StringIO\n\nwork_dir = \"/data/stock/tf/minst_serving/input_data\"\nout_dir = \"/static/img/minst_serving/%s.bmp\"\n\n\n# 获得页面数据。\nclass GetMinstServingHtmlHandler(webBase.BaseHandler):\n    @gen.coroutine\n    def get(self):\n        # print self.uri_\n        arr = np.arange(30)\n        image_array = []\n        for idx in arr:\n            out_file = out_dir % (\"%05d\" % idx)\n            print(out_file)\n            image_array.append(out_file)\n        self.render(\"minst_serving.html\", image_array=image_array)\n\n\n# 获得股票数据内容。\nclass GetPredictionDataHandler(webBase.BaseHandler):\n    def get(self):\n        # 获得分页参数。\n        img_url = self.get_argument(\"img_url\", default=0, strip=False)\n        print(img_url)\n        img_obj = Image.open(\"/data/stock/web\" + img_url)\n        print(\"img_obj\", img_obj)\n        server = \"0.0.0.0:8500\"\n        prediction = do_inference(server, img_obj)\n        print('######### prediction : ', prediction)\n        self.write(json.dumps(prediction))\n\n\n# 获得股票数据内容。\nclass GetPrediction2DataHandler(webBase.BaseHandler):\n    def post(self):\n        # 获得分页参数。\n        imgStr = self.get_argument(\"txt\", default=\"\", strip=False)\n        # imgStr.replace(\" \", \"+\")\n        imgStr = base64.b64decode(imgStr)\n        print(\"imgStr:\", type(imgStr))\n        image = Image.open(io.StringIO(imgStr))\n        image.thumbnail((28, 28), Image.ANTIALIAS)\n        image = image.convert('L')\n        image = ImageOps.invert(image)\n        image.save(work_dir + \"/web-tmp.bmp\", format=\"BMP\") #保存看看，是否\n        #print(image)\n        # img_url = self.get_argument(\"img_url\", default=0, strip=False)\n        # print(img_url)\n        server = \"0.0.0.0:8500\"\n        prediction = do_inference(server, image)\n        print('######### prediction : ', prediction)\n        self.write(json.dumps(prediction))\n\n\n\n# 调用 grpc 代码，将图片转换成数组，让后放到 grpc 调用。\ndef do_inference(hostport, img_obj):\n\n    print(\"############\", hostport)\n\n"
  },
  {
    "path": "backend/web/static/css/fonts.googleapis.com.css",
    "content": "@font-face {\n  font-family: 'Open Sans';\n  font-style: normal;\n  font-weight: 300;\n  src: local('Open Sans Light'), local('OpenSans-Light'), url(/static/font-awesome/opensans/v13/DXI1ORHCpsQm3Vp6mXoaTXhCUOGz7vYGh680lGh-uXM.woff) format('woff');\n}\n@font-face {\n  font-family: 'Open Sans';\n  font-style: normal;\n  font-weight: 400;\n  src: local('Open Sans'), local('OpenSans'), url(/static/font-awesome/opensans/v13/cJZKeOuBrn4kERxqtaUH3T8E0i7KZn-EPnyo3HZu7kw.woff) format('woff');\n}\n"
  },
  {
    "path": "backend/web/static/js/bootbox.js",
    "content": "/**\n * bootbox.js [v4.4.0]\n *\n * http://bootboxjs.com/license.txt\n */\n!function(a,b){\"use strict\";\"function\"==typeof define&&define.amd?define([\"jquery\"],b):\"object\"==typeof exports?module.exports=b(require(\"jquery\")):a.bootbox=b(a.jQuery)}(this,function a(b,c){\"use strict\";function d(a){var b=q[o.locale];return b?b[a]:q.en[a]}function e(a,c,d){a.stopPropagation(),a.preventDefault();var e=b.isFunction(d)&&d.call(c,a)===!1;e||c.modal(\"hide\")}function f(a){var b,c=0;for(b in a)c++;return c}function g(a,c){var d=0;b.each(a,function(a,b){c(a,b,d++)})}function h(a){var c,d;if(\"object\"!=typeof a)throw new Error(\"Please supply an object of options\");if(!a.message)throw new Error(\"Please specify a message\");return a=b.extend({},o,a),a.buttons||(a.buttons={}),c=a.buttons,d=f(c),g(c,function(a,e,f){if(b.isFunction(e)&&(e=c[a]={callback:e}),\"object\"!==b.type(e))throw new Error(\"button with key \"+a+\" must be an object\");e.label||(e.label=a),e.className||(2>=d&&f===d-1?e.className=\"btn-primary\":e.className=\"btn-default\")}),a}function i(a,b){var c=a.length,d={};if(1>c||c>2)throw new Error(\"Invalid argument length\");return 2===c||\"string\"==typeof a[0]?(d[b[0]]=a[0],d[b[1]]=a[1]):d=a[0],d}function j(a,c,d){return b.extend(!0,{},a,i(c,d))}function k(a,b,c,d){var e={className:\"bootbox-\"+a,buttons:l.apply(null,b)};return m(j(e,d,c),b)}function l(){for(var a={},b=0,c=arguments.length;c>b;b++){var e=arguments[b],f=e.toLowerCase(),g=e.toUpperCase();a[f]={label:d(g)}}return a}function m(a,b){var d={};return g(b,function(a,b){d[b]=!0}),g(a.buttons,function(a){if(d[a]===c)throw new Error(\"button key \"+a+\" is not allowed (options are \"+b.join(\"\\n\")+\")\")}),a}var n={dialog:\"<div class='bootbox modal' tabindex='-1' role='dialog'><div class='modal-dialog'><div class='modal-content'><div class='modal-body'><div class='bootbox-body'></div></div></div></div></div>\",header:\"<div class='modal-header'><h4 class='modal-title'></h4></div>\",footer:\"<div class='modal-footer'></div>\",closeButton:\"<button type='button' class='bootbox-close-button close' data-dismiss='modal' aria-hidden='true'>&times;</button>\",form:\"<form class='bootbox-form'></form>\",inputs:{text:\"<input class='bootbox-input bootbox-input-text form-control' autocomplete=off type=text />\",textarea:\"<textarea class='bootbox-input bootbox-input-textarea form-control'></textarea>\",email:\"<input class='bootbox-input bootbox-input-email form-control' autocomplete='off' type='email' />\",select:\"<select class='bootbox-input bootbox-input-select form-control'></select>\",checkbox:\"<div class='checkbox'><label><input class='bootbox-input bootbox-input-checkbox' type='checkbox' /></label></div>\",date:\"<input class='bootbox-input bootbox-input-date form-control' autocomplete=off type='date' />\",time:\"<input class='bootbox-input bootbox-input-time form-control' autocomplete=off type='time' />\",number:\"<input class='bootbox-input bootbox-input-number form-control' autocomplete=off type='number' />\",password:\"<input class='bootbox-input bootbox-input-password form-control' autocomplete='off' type='password' />\"}},o={locale:\"en\",backdrop:\"static\",animate:!0,className:null,closeButton:!0,show:!0,container:\"body\"},p={};p.alert=function(){var a;if(a=k(\"alert\",[\"ok\"],[\"message\",\"callback\"],arguments),a.callback&&!b.isFunction(a.callback))throw new Error(\"alert requires callback property to be a function when provided\");return a.buttons.ok.callback=a.onEscape=function(){return b.isFunction(a.callback)?a.callback.call(this):!0},p.dialog(a)},p.confirm=function(){var a;if(a=k(\"confirm\",[\"cancel\",\"confirm\"],[\"message\",\"callback\"],arguments),a.buttons.cancel.callback=a.onEscape=function(){return a.callback.call(this,!1)},a.buttons.confirm.callback=function(){return a.callback.call(this,!0)},!b.isFunction(a.callback))throw new Error(\"confirm requires a callback\");return p.dialog(a)},p.prompt=function(){var a,d,e,f,h,i,k;if(f=b(n.form),d={className:\"bootbox-prompt\",buttons:l(\"cancel\",\"confirm\"),value:\"\",inputType:\"text\"},a=m(j(d,arguments,[\"title\",\"callback\"]),[\"cancel\",\"confirm\"]),i=a.show===c?!0:a.show,a.message=f,a.buttons.cancel.callback=a.onEscape=function(){return a.callback.call(this,null)},a.buttons.confirm.callback=function(){var c;switch(a.inputType){case\"text\":case\"textarea\":case\"email\":case\"select\":case\"date\":case\"time\":case\"number\":case\"password\":c=h.val();break;case\"checkbox\":var d=h.find(\"input:checked\");c=[],g(d,function(a,d){c.push(b(d).val())})}return a.callback.call(this,c)},a.show=!1,!a.title)throw new Error(\"prompt requires a title\");if(!b.isFunction(a.callback))throw new Error(\"prompt requires a callback\");if(!n.inputs[a.inputType])throw new Error(\"invalid prompt type\");switch(h=b(n.inputs[a.inputType]),a.inputType){case\"text\":case\"textarea\":case\"email\":case\"date\":case\"time\":case\"number\":case\"password\":h.val(a.value);break;case\"select\":var o={};if(k=a.inputOptions||[],!b.isArray(k))throw new Error(\"Please pass an array of input options\");if(!k.length)throw new Error(\"prompt with select requires options\");g(k,function(a,d){var e=h;if(d.value===c||d.text===c)throw new Error(\"given options in wrong format\");d.group&&(o[d.group]||(o[d.group]=b(\"<optgroup/>\").attr(\"label\",d.group)),e=o[d.group]),e.append(\"<option value='\"+d.value+\"'>\"+d.text+\"</option>\")}),g(o,function(a,b){h.append(b)}),h.val(a.value);break;case\"checkbox\":var q=b.isArray(a.value)?a.value:[a.value];if(k=a.inputOptions||[],!k.length)throw new Error(\"prompt with checkbox requires options\");if(!k[0].value||!k[0].text)throw new Error(\"given options in wrong format\");h=b(\"<div/>\"),g(k,function(c,d){var e=b(n.inputs[a.inputType]);e.find(\"input\").attr(\"value\",d.value),e.find(\"label\").append(d.text),g(q,function(a,b){b===d.value&&e.find(\"input\").prop(\"checked\",!0)}),h.append(e)})}return a.placeholder&&h.attr(\"placeholder\",a.placeholder),a.pattern&&h.attr(\"pattern\",a.pattern),a.maxlength&&h.attr(\"maxlength\",a.maxlength),f.append(h),f.on(\"submit\",function(a){a.preventDefault(),a.stopPropagation(),e.find(\".btn-primary\").click()}),e=p.dialog(a),e.off(\"shown.bs.modal\"),e.on(\"shown.bs.modal\",function(){h.focus()}),i===!0&&e.modal(\"show\"),e},p.dialog=function(a){a=h(a);var d=b(n.dialog),f=d.find(\".modal-dialog\"),i=d.find(\".modal-body\"),j=a.buttons,k=\"\",l={onEscape:a.onEscape};if(b.fn.modal===c)throw new Error(\"$.fn.modal is not defined; please double check you have included the Bootstrap JavaScript library. See http://getbootstrap.com/javascript/ for more details.\");if(g(j,function(a,b){k+=\"<button data-bb-handler='\"+a+\"' type='button' class='btn \"+b.className+\"'>\"+b.label+\"</button>\",l[a]=b.callback}),i.find(\".bootbox-body\").html(a.message),a.animate===!0&&d.addClass(\"fade\"),a.className&&d.addClass(a.className),\"large\"===a.size?f.addClass(\"modal-lg\"):\"small\"===a.size&&f.addClass(\"modal-sm\"),a.title&&i.before(n.header),a.closeButton){var m=b(n.closeButton);a.title?d.find(\".modal-header\").prepend(m):m.css(\"margin-top\",\"-10px\").prependTo(i)}return a.title&&d.find(\".modal-title\").html(a.title),k.length&&(i.after(n.footer),d.find(\".modal-footer\").html(k)),d.on(\"hidden.bs.modal\",function(a){a.target===this&&d.remove()}),d.on(\"shown.bs.modal\",function(){d.find(\".btn-primary:first\").focus()}),\"static\"!==a.backdrop&&d.on(\"click.dismiss.bs.modal\",function(a){d.children(\".modal-backdrop\").length&&(a.currentTarget=d.children(\".modal-backdrop\").get(0)),a.target===a.currentTarget&&d.trigger(\"escape.close.bb\")}),d.on(\"escape.close.bb\",function(a){l.onEscape&&e(a,d,l.onEscape)}),d.on(\"click\",\".modal-footer button\",function(a){var c=b(this).data(\"bb-handler\");e(a,d,l[c])}),d.on(\"click\",\".bootbox-close-button\",function(a){e(a,d,l.onEscape)}),d.on(\"keyup\",function(a){27===a.which&&d.trigger(\"escape.close.bb\")}),b(a.container).append(d),d.modal({backdrop:a.backdrop?\"static\":!1,keyboard:!1,show:!1}),a.show&&d.modal(\"show\"),d},p.setDefaults=function(){var a={};2===arguments.length?a[arguments[0]]=arguments[1]:a=arguments[0],b.extend(o,a)},p.hideAll=function(){return b(\".bootbox\").modal(\"hide\"),p};var q={bg_BG:{OK:\"Ок\",CANCEL:\"Отказ\",CONFIRM:\"Потвърждавам\"},br:{OK:\"OK\",CANCEL:\"Cancelar\",CONFIRM:\"Sim\"},cs:{OK:\"OK\",CANCEL:\"Zrušit\",CONFIRM:\"Potvrdit\"},da:{OK:\"OK\",CANCEL:\"Annuller\",CONFIRM:\"Accepter\"},de:{OK:\"OK\",CANCEL:\"Abbrechen\",CONFIRM:\"Akzeptieren\"},el:{OK:\"Εντάξει\",CANCEL:\"Ακύρωση\",CONFIRM:\"Επιβεβαίωση\"},en:{OK:\"OK\",CANCEL:\"Cancel\",CONFIRM:\"OK\"},es:{OK:\"OK\",CANCEL:\"Cancelar\",CONFIRM:\"Aceptar\"},et:{OK:\"OK\",CANCEL:\"Katkesta\",CONFIRM:\"OK\"},fa:{OK:\"قبول\",CANCEL:\"لغو\",CONFIRM:\"تایید\"},fi:{OK:\"OK\",CANCEL:\"Peruuta\",CONFIRM:\"OK\"},fr:{OK:\"OK\",CANCEL:\"Annuler\",CONFIRM:\"D'accord\"},he:{OK:\"אישור\",CANCEL:\"ביטול\",CONFIRM:\"אישור\"},hu:{OK:\"OK\",CANCEL:\"Mégsem\",CONFIRM:\"Megerősít\"},hr:{OK:\"OK\",CANCEL:\"Odustani\",CONFIRM:\"Potvrdi\"},id:{OK:\"OK\",CANCEL:\"Batal\",CONFIRM:\"OK\"},it:{OK:\"OK\",CANCEL:\"Annulla\",CONFIRM:\"Conferma\"},ja:{OK:\"OK\",CANCEL:\"キャンセル\",CONFIRM:\"確認\"},lt:{OK:\"Gerai\",CANCEL:\"Atšaukti\",CONFIRM:\"Patvirtinti\"},lv:{OK:\"Labi\",CANCEL:\"Atcelt\",CONFIRM:\"Apstiprināt\"},nl:{OK:\"OK\",CANCEL:\"Annuleren\",CONFIRM:\"Accepteren\"},no:{OK:\"OK\",CANCEL:\"Avbryt\",CONFIRM:\"OK\"},pl:{OK:\"OK\",CANCEL:\"Anuluj\",CONFIRM:\"Potwierdź\"},pt:{OK:\"OK\",CANCEL:\"Cancelar\",CONFIRM:\"Confirmar\"},ru:{OK:\"OK\",CANCEL:\"Отмена\",CONFIRM:\"Применить\"},sq:{OK:\"OK\",CANCEL:\"Anulo\",CONFIRM:\"Prano\"},sv:{OK:\"OK\",CANCEL:\"Avbryt\",CONFIRM:\"OK\"},th:{OK:\"ตกลง\",CANCEL:\"ยกเลิก\",CONFIRM:\"ยืนยัน\"},tr:{OK:\"Tamam\",CANCEL:\"İptal\",CONFIRM:\"Onayla\"},zh_CN:{OK:\"OK\",CANCEL:\"取消\",CONFIRM:\"确认\"},zh_TW:{OK:\"OK\",CANCEL:\"取消\",CONFIRM:\"確認\"}};return p.addLocale=function(a,c){return b.each([\"OK\",\"CANCEL\",\"CONFIRM\"],function(a,b){if(!c[b])throw new Error(\"Please supply a translation for '\"+b+\"'\")}),q[a]={OK:c.OK,CANCEL:c.CANCEL,CONFIRM:c.CONFIRM},p},p.removeLocale=function(a){return delete q[a],p},p.setLocale=function(a){return p.setDefaults(\"locale\",a)},p.init=function(c){return a(c||b)},p});"
  },
  {
    "path": "backend/web/static/js/bootstrap-datepicker.zh-CN.js",
    "content": "/**\n * Simplified Chinese translation for bootstrap-datepicker\n * Yuan Cheung <advanimal@gmail.com>\n */\n;(function($){\n\t$.fn.datepicker.dates['zh-CN'] = {\n\t\tdays: [\"星期日\", \"星期一\", \"星期二\", \"星期三\", \"星期四\", \"星期五\", \"星期六\"],\n\t\tdaysShort: [\"周日\", \"周一\", \"周二\", \"周三\", \"周四\", \"周五\", \"周六\"],\n\t\tdaysMin:  [\"日\", \"一\", \"二\", \"三\", \"四\", \"五\", \"六\"],\n\t\tmonths: [\"一月\", \"二月\", \"三月\", \"四月\", \"五月\", \"六月\", \"七月\", \"八月\", \"九月\", \"十月\", \"十一月\", \"十二月\"],\n\t\tmonthsShort: [\"1月\", \"2月\", \"3月\", \"4月\", \"5月\", \"6月\", \"7月\", \"8月\", \"9月\", \"10月\", \"11月\", \"12月\"],\n\t\ttoday: \"今日\",\n\t\tclear: \"清除\",\n\t\tformat: \"yyyy年mm月dd日\",\n\t\ttitleFormat: \"yyyy年mm月\",\n\t\tweekStart: 1\n\t};\n}(jQuery));"
  },
  {
    "path": "backend/web/static/js/datatables.Chinese.json",
    "content": "{\n    \"sProcessing\":   \"处理中...\",\n    \"sLengthMenu\":   \"显示 _MENU_ 项结果\",\n    \"sZeroRecords\":  \"没有匹配结果\",\n    \"sInfo\":         \"显示第 _START_ 至 _END_ 项结果，共 _TOTAL_ 项\",\n    \"sInfoEmpty\":    \"显示第 0 至 0 项结果，共 0 项\",\n    \"sInfoFiltered\": \"(由 _MAX_ 项结果过滤)\",\n    \"sInfoPostFix\":  \"\",\n    \"sSearch\":       \"搜索:\",\n    \"sUrl\":          \"\",\n    \"sEmptyTable\":     \"表中数据为空\",\n    \"sLoadingRecords\": \"载入中...\",\n    \"sInfoThousands\":  \",\",\n    \"oPaginate\": {\n        \"sFirst\":    \"首页\",\n        \"sPrevious\": \"上页\",\n        \"sNext\":     \"下页\",\n        \"sLast\":     \"末页\"\n    },\n    \"oAria\": {\n        \"sSortAscending\":  \": 以升序排列此列\",\n        \"sSortDescending\": \": 以降序排列此列\"\n    }\n}"
  },
  {
    "path": "backend/web/static/js/draw.js",
    "content": "\nvar drawing = false;\n\nvar context;\n\nvar offset_left = 0;\nvar offset_top = 0;\n\n\nfunction start_canvas ()\n{\n    var scribbler = document.getElementById (\"the_stage\");\n    context = scribbler.getContext (\"2d\");\n    scribbler.onmousedown = function (event) {mousedown(event)};\n    scribbler.onmousemove = function (event) {mousemove(event)};\n    scribbler.onmouseup   = function (event) {mouseup(event)};\n    for (var o = scribbler; o ; o = o.offsetParent) {\n    offset_left += (o.offsetLeft - o.scrollLeft);\n    offset_top  += (o.offsetTop - o.scrollTop);\n    }\n    draw();\n}\n\nfunction getPosition(evt)\n{\n    evt = (evt) ?  evt : ((event) ? event : null);\n    var left = 0;\n    var top = 0;\n    var scribbler = document.getElementById(\"the_stage\");\n\n    if (evt.pageX) {\n    left = evt.pageX;\n    top  = evt.pageY;\n    } else if (document.documentElement.scrollLeft) {\n    left = evt.clientX + document.documentElement.scrollLeft;\n    top  = evt.clientY + document.documentElement.scrollTop;\n    } else  {\n    left = evt.clientX + document.body.scrollLeft;\n    top  = evt.clientY + document.body.scrollTop;\n    }\n    left -= offset_left;\n    top -= offset_top;\n\n    return {x : left, y : top}; \n}\n\nfunction\nmousedown(event)\n{\n    drawing = true;\n    var location = getPosition(event);\n    context.lineWidth = 20.0;\n    context.strokeStyle=\"#000000\";\n    context.beginPath();\n    context.moveTo(location.x,location.y);\n}\n\n\nfunction\nmousemove(event)\n{\n    if (!drawing) \n        return;\n    var location = getPosition(event);\n    context.lineTo(location.x,location.y);\n    context.stroke();\n}\n\n\n\nfunction\nmouseup(event)\n{\n    if (!drawing) \n        return;\n    mousemove(event);\n    drawing = false;\n}\n\nfunction draw()\n{\n\n    context.fillStyle = '#ffffff';\n    context.fillRect(0, 0, 400, 400);\n\n}\n\nfunction clearCanvas()\n{\n    context.clearRect (0, 0, 400, 400);\n    draw();\n    document.getElementById(\"rec_result\").innerHTML = \"\";\n}\n\nonload = start_canvas;\n\n"
  },
  {
    "path": "backend/web/static/js/grid.locale-en.js",
    "content": "!function(a){\"use strict\";\"function\"==typeof define&&define.amd?define([\"jquery\",\"../grid.base\"],a):a(jQuery)}(function(a){a.jgrid=a.jgrid||{},a.jgrid.hasOwnProperty(\"regional\")||(a.jgrid.regional=[]),a.jgrid.regional.en={defaults:{recordtext:\"View {0} - {1} of {2}\",emptyrecords:\"No records to view\",loadtext:\"Loading...\",savetext:\"Saving...\",pgtext:\"Page {0} of {1}\",pgfirst:\"First Page\",pglast:\"Last Page\",pgnext:\"Next Page\",pgprev:\"Previous Page\",pgrecs:\"Records per Page\",showhide:\"Toggle Expand Collapse Grid\",pagerCaption:\"Grid::Page Settings\",pageText:\"Page:\",recordPage:\"Records per Page\",nomorerecs:\"No more records...\",scrollPullup:\"Pull up to load more...\",scrollPulldown:\"Pull down to refresh...\",scrollRefresh:\"Release to refresh...\"},search:{caption:\"Search...\",Find:\"Find\",Reset:\"Reset\",odata:[{oper:\"eq\",text:\"equal\"},{oper:\"ne\",text:\"not equal\"},{oper:\"lt\",text:\"less\"},{oper:\"le\",text:\"less or equal\"},{oper:\"gt\",text:\"greater\"},{oper:\"ge\",text:\"greater or equal\"},{oper:\"bw\",text:\"begins with\"},{oper:\"bn\",text:\"does not begin with\"},{oper:\"in\",text:\"is in\"},{oper:\"ni\",text:\"is not in\"},{oper:\"ew\",text:\"ends with\"},{oper:\"en\",text:\"does not end with\"},{oper:\"cn\",text:\"contains\"},{oper:\"nc\",text:\"does not contain\"},{oper:\"nu\",text:\"is null\"},{oper:\"nn\",text:\"is not null\"}],groupOps:[{op:\"AND\",text:\"all\"},{op:\"OR\",text:\"any\"}],operandTitle:\"Click to select search operation.\",resetTitle:\"Reset Search Value\"},edit:{addCaption:\"Add Record\",editCaption:\"Edit Record\",bSubmit:\"Submit\",bCancel:\"Cancel\",bClose:\"Close\",saveData:\"Data has been changed! Save changes?\",bYes:\"Yes\",bNo:\"No\",bExit:\"Cancel\",msg:{required:\"Field is required\",number:\"Please, enter valid number\",minValue:\"value must be greater than or equal to \",maxValue:\"value must be less than or equal to\",email:\"is not a valid e-mail\",integer:\"Please, enter valid integer value\",date:\"Please, enter valid date value\",url:\"is not a valid URL. Prefix required ('http://' or 'https://')\",nodefined:\" is not defined!\",novalue:\" return value is required!\",customarray:\"Custom function should return array!\",customfcheck:\"Custom function should be present in case of custom checking!\"}},view:{caption:\"View Record\",bClose:\"Close\"},del:{caption:\"Delete\",msg:\"Delete selected record(s)?\",bSubmit:\"Delete\",bCancel:\"Cancel\"},nav:{edittext:\"\",edittitle:\"Edit selected row\",addtext:\"\",addtitle:\"Add new row\",deltext:\"\",deltitle:\"Delete selected row\",searchtext:\"\",searchtitle:\"Find records\",refreshtext:\"\",refreshtitle:\"Reload Grid\",alertcap:\"Warning\",alerttext:\"Please, select row\",viewtext:\"\",viewtitle:\"View selected row\",savetext:\"\",savetitle:\"Save row\",canceltext:\"\",canceltitle:\"Cancel row editing\",selectcaption:\"Actions...\"},col:{caption:\"Select columns\",bSubmit:\"Ok\",bCancel:\"Cancel\"},errors:{errcap:\"Error\",nourl:\"No url is set\",norecords:\"No records to process\",model:\"Length of colNames <> colModel!\"},formatter:{integer:{thousandsSeparator:\",\",defaultValue:\"0\"},number:{decimalSeparator:\".\",thousandsSeparator:\",\",decimalPlaces:2,defaultValue:\"0.00\"},currency:{decimalSeparator:\".\",thousandsSeparator:\",\",decimalPlaces:2,prefix:\"\",suffix:\"\",defaultValue:\"0.00\"},date:{dayNames:[\"Sun\",\"Mon\",\"Tue\",\"Wed\",\"Thr\",\"Fri\",\"Sat\",\"Sunday\",\"Monday\",\"Tuesday\",\"Wednesday\",\"Thursday\",\"Friday\",\"Saturday\"],monthNames:[\"Jan\",\"Feb\",\"Mar\",\"Apr\",\"May\",\"Jun\",\"Jul\",\"Aug\",\"Sep\",\"Oct\",\"Nov\",\"Dec\",\"January\",\"February\",\"March\",\"April\",\"May\",\"June\",\"July\",\"August\",\"September\",\"October\",\"November\",\"December\"],AmPm:[\"am\",\"pm\",\"AM\",\"PM\"],S:function(a){return 11>a||a>13?[\"st\",\"nd\",\"rd\",\"th\"][Math.min((a-1)%10,3)]:\"th\"},srcformat:\"Y-m-d\",newformat:\"n/j/Y\",parseRe:/[#%\\\\\\/:_;.,\\t\\s-]/,masks:{ISO8601Long:\"Y-m-d H:i:s\",ISO8601Short:\"Y-m-d\",ShortDate:\"n/j/Y\",LongDate:\"l, F d, Y\",FullDateTime:\"l, F d, Y g:i:s A\",MonthDay:\"F d\",ShortTime:\"g:i A\",LongTime:\"g:i:s A\",SortableDateTime:\"Y-m-d\\\\TH:i:s\",UniversalSortableDateTime:\"Y-m-d H:i:sO\",YearMonth:\"F, Y\"},reformatAfterEdit:!1,userLocalTime:!1},baseLinkUrl:\"\",showAction:\"\",target:\"\",checkbox:{disabled:!0},idName:\"id\"}}});"
  },
  {
    "path": "backend/web/static/update_bokeh.sh",
    "content": "\n\nrm -f ./js/bokeh.min.js\nrm -f ./js/bokeh-api.min.js\nrm -f ./js/bokeh-gl.min.js\nrm -f ./js/bokeh-tables.min.js\nrm -f ./js/bokeh-widgets.min.js\n\ncp /usr/local/lib/python3.7/site-packages/bokeh/server/static/js/bokeh.min.js ./js/\ncp /usr/local/lib/python3.7/site-packages/bokeh/server/static/js/bokeh-api.min.js ./js/\ncp /usr/local/lib/python3.7/site-packages/bokeh/server/static/js/bokeh-gl.min.js ./js/\ncp /usr/local/lib/python3.7/site-packages/bokeh/server/static/js/bokeh-tables.min.js ./js/\ncp /usr/local/lib/python3.7/site-packages/bokeh/server/static/js/bokeh-widgets.min.js ./js/"
  },
  {
    "path": "backend/web/templates/bokeh_embed.html",
    "content": "<!doctype html>\n\n<html lang=\"en\">\n<head>\n  <meta charset=\"utf-8\">\n  <title>Embedding a Bokeh Server With {{ framework }}</title>\n</head>\n\n<body>\n  <div>\n    This Bokeh app below served by a Bokeh server that has been embedded\n    in another web app framework. For more information see the section\n    <a  target=\"_blank\"\n        href=\"https://bokeh.pydata.org/en/latest/docs/user_guide/server.html#embedding-bokeh-server-as-a-library\">\n        Embedding Bokeh Server as a Library</a>\n    in the User's Guide.\n  </div>\n  {{ script|safe }}\n</body>\n</html>"
  },
  {
    "path": "backend/web/templates/common/footer.html",
    "content": "{% block footer %}\n{% end %}"
  },
  {
    "path": "backend/web/templates/common/header.html",
    "content": "{% block header %}\n<div id=\"navbar\" class=\"navbar navbar-default ace-save-state\">\n    <div class=\"navbar-container ace-save-state\" id=\"navbar-container\">\n        <div class=\"navbar-header pull-left\">\n            <a href=\"/stock/\" class=\"navbar-brand\">\n                <small><i class=\"fa fa-leaf\"></i>开源Python全栈股票系统，数据抓取、统计分析、报表展示 版本：{{ pythonStockVersion }}</small>\n            </a>\n        </div>\n\n    </div><!-- /.navbar-container -->\n</div>\n{% end %}"
  },
  {
    "path": "backend/web/templates/common/left_menu.html",
    "content": "{% block left_menu %}\n<div id=\"sidebar\" class=\"sidebar responsive ace-save-state\">\n    <script type=\"text/javascript\">\n        try{ace.settings.loadState('sidebar')}catch(e){}\n    </script>\n\n    <ul class=\"nav nav-list\">\n        <li class=\"\">\n            <a href=\"/\">\n                <i class=\"menu-icon fa fa-tachometer\"></i>\n                <span class=\"menu-text\"> Dashboard </span>\n            </a>\n            <b class=\"arrow\"></b>\n        </li>\n\n        <li class=\"active open\">\n            <!--<a href=\"#\" class=\"dropdown-toggle\">-->\n                <!--<i class=\"menu-icon fa fa-desktop\"></i>-->\n                <!--<span class=\"menu-text\">-->\n                    <!--股票原始数据-->\n                <!--</span>-->\n                <!--<b class=\"arrow fa fa-angle-down\"></b>-->\n            <!--</a>-->\n            <!--<b class=\"arrow\"></b>-->\n            <li class=\"submenu\">\n                    <li class=\"open\" style=\"display:none;\">\n                    {% set loopType = \"\" %}\n                    {% for leftMenuTmp in leftMenu.leftMenuList %}\n\n                        {% if leftMenuTmp.type != loopType %}\n                        </li>\n                        <li class=\"open\">\n                            {% set loopType = leftMenuTmp.type %}\n                            <a href=\"#\" class=\"dropdown-toggle\">\n                                <i class=\"menu-icon fa fa-pencil-square-o\"></i>\n                                {{ leftMenuTmp.type }}\n                                <b class=\"arrow fa fa-angle-down\"></b>\n                            </a>\n                            <b class=\"arrow\"></b>\n\n                        {% end %}\n\n                        <ul class=\"submenu\">\n                            <li>\n                                <a href=\"{{ leftMenuTmp.url }}\">\n                                    <i class=\"menu-icon fa fa-caret-right\"></i>\n                                    {{ leftMenuTmp.name }}\n                                </a>\n                                <b class=\"arrow\"></b>\n                            </li>\n                        </ul>\n\n                    {% end %}\n                        </li>\n            </ul>\n\n        </li>\n    </ul><!-- /.nav-list -->\n\n    <div class=\"sidebar-toggle sidebar-collapse\" id=\"sidebar-collapse\">\n        <i id=\"sidebar-toggle-icon\" class=\"ace-icon fa fa-angle-double-left ace-save-state\" data-icon1=\"ace-icon fa fa-angle-double-left\" data-icon2=\"ace-icon fa fa-angle-double-right\"></i>\n    </div>\n</div>\n{% end %}"
  },
  {
    "path": "backend/web/templates/common/meta.html",
    "content": "{% block meta %}\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0\" />\n<!-- bootstrap & fontawesome -->\n<link rel=\"stylesheet\" href=\"/static/css/bootstrap.min.css\" />\n<link rel=\"stylesheet\" href=\"/static/font-awesome/4.5.0/css/font-awesome.min.css\" />\n<!-- ace styles -->\n<link rel=\"stylesheet\" href=\"/static/css/ace.min.css\" class=\"ace-main-stylesheet\" id=\"main-ace-style\" />\n<!-- basic scripts -->\n<script src=\"/static/js/jquery-2.1.4.min.js\"></script>\n<script src=\"/static/js/ace-extra.min.js\"></script>\n<script src=\"/static/js/bootstrap.min.js\"></script>\n\n<!-- page specific plugin scripts -->\n<script src=\"/static/js/jquery.dataTables.min.js\"></script>\n<script src=\"/static/js/jquery.dataTables.bootstrap.min.js\"></script>\n<script src=\"/static/js/dataTables.buttons.min.js\"></script>\n<script src=\"/static/js/dataTables.select.min.js\"></script>\n<!-- ace scripts -->\n<script src=\"/static/js/moment.min.js\"></script>\n<script src=\"/static/js/bootstrap-datepicker.min.js\"></script>\n<script src=\"/static/js/bootstrap-datetimepicker.min.js\"></script>\n<script src=\"/static/js/bootstrap-datepicker.zh-CN.js\"></script>\n\n<link rel=\"stylesheet\" href=\"/static/css/bootstrap-datepicker3.min.css\" />\n<link rel=\"stylesheet\" href=\"/static/css/bootstrap-datetimepicker.min.css\" />\n\n<script src=\"/static/js/ace-elements.min.js\"></script>\n<script src=\"/static/js/ace.min.js\"></script>\n<script src=\"/static/js/jquery-ui.custom.min.js\"></script>\n<script src=\"/static/js/bootbox.js\"></script>\n\n<script src=\"/static/js/buttons.colVis.min.js\"></script>\n<script src=\"/static/js/buttons.print.min.js\"></script>\n<script src=\"/static/js/buttons.html5.min.js\"></script>\n\n\n<script type='text/javascript'>\njQuery(function($) {\n    //初始化窗口。\n    $('.modal.aside').ace_aside();\n    $('#aside-inside-modal').addClass('aside').ace_aside({container: \"\"});\n    $(document).one('ajaxloadstart.page', function(e) {\n        $('.modal.aside').remove();\n        $(window).off('.aside')\n    });\n})\n$.extend({\n\tgetUrlVars: function(){\n\t\tvar vars = [], hash;\n\t\tvar hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');\n\t\tfor(var i = 0; i < hashes.length; i++)\n\t\t{\n\t\t\thash = hashes[i].split('=');\n\t\t\tvars.push(hash[0]);\n\t\t\tvars[hash[0]] = hash[1];\n\t\t}\n\t\treturn vars;\n\t},\n\tgetUrlVar: function(name){\n\t\treturn $.getUrlVars()[name];\n\t}\n});\njQuery(function($) {\n    $('[data-rel=tooltip]').tooltip();\n    $('[data-rel=popover]').popover({html:true});\n});\n</script>\n<style>\n\t#dynamic-table_filter {display: none;}\n</style>\n{% end %}"
  },
  {
    "path": "backend/web/templates/data_editor.html",
    "content": "{% extends \"layout/default.html\" %}\n\n\n{% block main_content %}\n\n\t<h3 class=\"header smaller lighter blue\">{{ stockWeb.name }}</h3>\n\t<div class=\"table-header\">{{ stockWeb.name }}</div>\n\n\t<div class=\"row\">&nbsp;</div>\n\t<div class=\"row\">\n\t\t{% for index,element in enumerate(stockWeb.columns) %}\n\t\t<div class=\"col-md-4\" id=\"filter_col{{ index+1 }}\" data-column=\"{{ index }}\">\n\t\t\t{{ stockWeb.column_names[index] }} &nbsp;\n\t\t\t<!-- Column - {{ stockWeb.column_names[index] }} - {{ element }} -->\n\t\t\t<input type=\"text\" class=\"column_filter input-group-sm form-control\" id=\"col{{ index }}_filter\">\n\t\t</div>\n\t\t{% end %}\n\t</div>\n\n\t<table cellpadding=\"3\" cellspacing=\"0\" border=\"0\" style=\"width: 67%; margin: 0 auto 2em auto;\">\n\t\t<tbody>\n\n\t\t</tbody>\n\t</table>\n\t<!-- div.table-responsive -->\n\t<!-- div.dataTables_borderWrap -->\n\t<div>\n\t\t<div class=\"pull-right tableTools-container\"></div>\n\t\t<table id=\"dynamic-table\" class=\"table table-striped table-bordered table-hover\">\n\t\t\t<thead>\n\t\t\t\t<tr>\n\t\t\t\t\t<th></th>\n\t\t\t\t\t{% for column_name in stockWeb.column_names %}\n\t\t\t\t\t<th>{{ column_name }}</th>\n\t\t\t\t    {% end %}\n\t\t\t\t</tr>\n\t\t\t</thead>\n\t\t</table>\n\t</div>\n\n<!-- inline scripts related to this page -->\n<script type=\"text/javascript\">\n\n\tjQuery(function($) {\n\t\teditor = new $.fn.dataTable.Editor( {\n\t\ti18n: {\n\t\t\tcreate: {\n\t\t\t\tbutton: \"新建\",\n\t\t\t\ttitle:  \"创建新数据\",\n\t\t\t\tsubmit: \"保存\"\n\t\t\t},\n\t\t\tedit: {\n\t\t\t\tbutton: \"修改\",\n\t\t\t\ttitle:  \"修改数据\",\n\t\t\t\tsubmit: \"保存\"\n\t\t\t},\n\t\t\tremove: {\n\t\t\t\tbutton: \"删除\",\n\t\t\t\ttitle:  \"删除数据\",\n\t\t\t\tsubmit: \"删除\",\n\t\t\t\tconfirm: {\n\t\t\t\t\t_: \"确定删除 %d 条数据？\",\n\t\t\t\t\t1: \"确定删除 1 条数据？\"\n\t\t\t\t}\n\t\t\t},\n\t\t\terror: {\n\t\t\t\tsystem: \"发生了一系统错误，（更多信息）\"\n\t\t\t},\n\t\t\tdatetime: {\n\t\t\t\tprevious: '前一月',\n\t\t\t\tnext:     '下一月',\n\t\t\t\tmonths:   [ '1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月' ],\n\t\t\t\tweekdays: [ '星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六' ]\n\t\t\t}\n\t\t},\n        ajax: \"/data/editor/save?table_name={{ stockWeb.table_name }}\",\n        table: \"#dynamic-table\",\n\t\t\tfields: [\n\t\t\t\t{% for index,element in enumerate(stockWeb.columns) %}\n\t\t\t\t{\n\t\t\t\t\tlabel: \"{{ stockWeb.column_names[index] }}:\",\n\t\t\t\t\tname: \"{{ element }}\"\n\t\t\t\t\t{% if element == \"date\" %} ,type: \"datetime\" {% end %}\n\t\t\t\t},{% end %}\n\t\t\t]\n\t\t} );\n\n\t\tvar nameParam = $.getUrlVar('table_name');\n\t\t//console.log(nameParam);\n\t\tvar myTable = $('#dynamic-table').DataTable( {\n\t\t\t\"dom\": \"Bfrtip\",\n\t\t\t\"bFilter\": true,\n\t\t\t\"ordering\": true,\n\t\t\t\"processing\": true,\n\t\t\t\"serverSide\": true,\n\t\t\t\"lengthMenu\": [[20, 30, 50, 100,1000, -1], [20, 30, 50, 100,1000, \"All\"]],\n\t\t\t\"language\": {\n                \"url\": \"/static/js/datatables.Chinese.json\"\n            },\n\t\t\t\"ajax\": \"/api/v1/api_data?type=editor&name=\"+nameParam,\n\t\t\t\"columns\": [\n\t\t\t\t{\n\t\t\t\t\t\"data\": \"\",\n\t\t\t\t\t\"defaultContent\": \"\",\n\t\t\t\t\t\"className\": 'select-checkbox',\n\t\t\t\t\t\"orderable\": true\n\t\t\t\t},\n\t\t\t\t{% for column in stockWeb.columns %}\n\t\t\t\t\t{ \"data\": \"{{ column }}\" },\n\t\t\t\t{% end %}\n        \t],\n        \tselect: {\n\t\t\t\tstyle:    'os',\n\t\t\t\tselector: 'td:first-child'\n\t\t\t},\n        \tbuttons: [\n\t\t\t\t{ extend: \"create\", editor: editor, text: '新增' },\n\t\t\t\t{ extend: \"edit\",   editor: editor, text: '编辑' },\n\t\t\t\t{ extend: \"remove\", editor: editor, text: '删除' }\n\t\t\t]\n\t\t} );\n\n\t\t$('input.column_filter').on( 'keyup click', function () {\n\t\t\tvar i =  $(this).parents('div').attr('data-column') ;\n\t\t\t//console.log(i,\"val:\",$(this).val());\n\t\t\tmyTable.column(i).search(\n\t\t\t\t$(this).val()\n\t\t\t).draw();\n\t\t});\n\n\t\t$.fn.dataTable.Buttons.defaults.dom.container.className = 'dt-buttons btn-overlap btn-group btn-overlap';\n\n\t\tnew $.fn.dataTable.Buttons( myTable, {\n\t\t\tbuttons: [\n\t\t\t  {\n\t\t\t\t\"extend\": \"copy\",\n\t\t\t\t\"text\": \"<i class='fa fa-copy bigger-110 pink'></i> <span class='hidden'>Copy to clipboard</span>\",\n\t\t\t\t\"className\": \"btn btn-white btn-primary btn-bold\"\n\t\t\t  },\n\t\t\t  {\n\t\t\t\t\"extend\": \"csv\",\n\t\t\t\t\"text\": \"<i class='fa fa-database bigger-110 orange'></i> <span class='hidden'>Export to CSV</span>\",\n\t\t\t\t\"className\": \"btn btn-white btn-primary btn-bold\"\n\t\t\t  },\n\t\t\t  {\n\t\t\t\t\"extend\": \"print\",\n\t\t\t\t\"text\": \"<i class='fa fa-print bigger-110 grey'></i> <span class='hidden'>Print</span>\",\n\t\t\t\t\"className\": \"btn btn-white btn-primary btn-bold\",\n\t\t\t\tautoPrint: false,\n\t\t\t\tmessage: 'This print was produced using the Print button for DataTables'\n\t\t\t  }\n\t\t\t]\n\t\t} );\n\t\tmyTable.buttons().container().appendTo( $('.tableTools-container') );\n\t})\n</script>\n{% end %}"
  },
  {
    "path": "backend/web/templates/index.html",
    "content": "{% extends \"layout/default.html\" %}\n\n\n{% block main_content %}\n\n\t<h3 class=\"header smaller lighter blue\">开源Python全栈股票系统，数据抓取、统计分析、报表展示。</h3>\n\n\t<div class=\"clearfix\">\n\t\t<div class=\"pull-left tableTools-container\">\n\t\t\t<h3>基础库版本</h3>\n\t\t\t<p>1，pandas使用【 {{ pandasVersion }} 】版本， <a href=\"https://www.pypandas.cn/docs/\" target=\"_blank\">中文文档</a> </p>\n\t\t\t<p>2，numpy使用【 {{ numpyVersion }} 】版本， <a href=\"https://www.numpy.org.cn/user/\" target=\"_blank\">中文文档</a></p>\n\t\t\t<p>3，akshare使用【 {{ akshareVersion }} 】版本， <a href=\"https://www.akshare.xyz/\" target=\"_blank\">中文文档</a></p>\n\t\t\t<p>4，bokeh使用【 {{ bokehVersion }} 】版本， <a href=\"http://docs.bokeh.org/en/latest/\" target=\"_blank\">官方文档</a></p>\n\t\t\t<p>5，stockstats使用【 {{ stockstatsVersion }} 】版本， <a href=\"https://github.com/jealous/stockstats/\" target=\"_blank\">官方文档</a></p>\n\t\t</div>\n\t</div>\n\n\t<div class=\"clearfix\">\n\t\t<div class=\"pull-left tableTools-container\">\n\t\t\t<h3>相关资料信息</h3>\n\t\t\t<p>1，github项目地址。<a href=\"https://github.com/pythonstock/stock\" target=\"_blank\">pythonstock</a></p>\n\t\t\t<p>2，博客地址。<a href=\"https://blog.csdn.net/freewebsys/category_9285317.html\" target=\"_blank\">博客地址</a></p>\n\t\t</div>\n\t</div>\n\n\t<div class=\"clearfix\">\n\t\t<div class=\"pull-left tableTools-container\">\n\t\t\t<h3>2021年9月20日更新，发布2.0版本</h3>\n\t\t\t<p>1，修复bokeh的版本升级问题，可以显示趋势数据了。</p>\n\t\t\t<p>2，使用 stock_zh_ah_name 做每日数据。</p>\n\t\t\t<p>3，AkShare 升级到 1.1.9 版本。</p>\n\t\t</div>\n\t</div>\n\n\t<div class=\"clearfix\">\n\t\t<div class=\"pull-left tableTools-container\">\n\t\t\t<h3>2021年8月31日更新job服务</h3>\n\t\t\t<p>1，过滤包括：600，6006，601，000，001，002，且不包括ST的股票数据。。</p>\n\t\t\t<p>2，使用 stock_zh_ah_name 做每日数据。</p>\n\t\t\t<p>3，AkShare 升级到 1.0.80 版本。</p>\n\n\t\t</div>\n\t</div>\n\n\t<div class=\"clearfix\">\n\t\t<div class=\"pull-left tableTools-container\">\n\t\t\t<h3>2021年6月3日使用 AkShare 做数据抓取服务</h3>\n\t\t\t<p>1，使用 stock_zh_a_spot 做实时行情数据。</p>\n\t\t\t<p>2，使用 stock_zh_a_daily 做历史数据统计。</p>\n\t\t\t<p>3，升级基础镜像使用python3.7，AkShare 的 0.9.65 版本。</p>\n\n\t\t</div>\n\t</div>\n\n{% end %}"
  },
  {
    "path": "backend/web/templates/layout/default.html",
    "content": "{% extends \"../common/meta.html\" %}\n{% extends \"../common/header.html\" %}\n{% extends \"../common/footer.html\" %}\n{% extends \"../common/left_menu.html\" %}\n\n{% extends \"main.html\" %}"
  },
  {
    "path": "backend/web/templates/layout/indicators-main.html",
    "content": "<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\" />\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>股票系统</title>\n\t\t{% block meta %}{% end %}\n\t</head>\n\n\t<body class=\"no-skin\">\n\t\t\t<div class=\"main-content\">\n\t\t\t\t<div class=\"main-content-inner\">\n\t\t\t\t\t<div class=\"page-content\">\n\t\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t\t<div class=\"col-xs-12\">\n\t\t\t{% block main_content %}{% end %}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div><!-- /.row -->\n\t\t\t\t\t</div><!-- /.page-content -->\n\t\t\t\t</div>\n\t\t\t</div><!-- /.main-content -->\n\t</body>\n</html>\n"
  },
  {
    "path": "backend/web/templates/layout/indicators.html",
    "content": "{% extends \"../common/meta.html\" %}\n\n{% extends \"indicators-main.html\" %}"
  },
  {
    "path": "backend/web/templates/layout/main.html",
    "content": "<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\" />\n\t\t<meta charset=\"utf-8\" />\n\t\t<title>股票系统 {{ pythonStockVersion }}</title>\n\t\t{% block meta %}{% end %}\n\t</head>\n\n\t<body class=\"no-skin\">\n\n\t\t{% block header %}{% end %}\n\n\t\t<div class=\"main-container ace-save-state\" id=\"main-container\">\n\t\t\t<script type=\"text/javascript\">\n\t\t\t\ttry{ace.settings.loadState('main-container')}catch(e){}\n\t\t\t</script>\n\n\t\t\t{% block left_menu %}{% end %}\n\n\t\t\t<div class=\"main-content\">\n\t\t\t\t<div class=\"main-content-inner\">\n\t\t\t\t\t<div class=\"page-content\">\n\t\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t\t<div class=\"col-xs-12\">\n\t\t\t{% block main_content %}{% end %}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div><!-- /.row -->\n\t\t\t\t\t</div><!-- /.page-content -->\n\t\t\t\t</div>\n\t\t\t</div><!-- /.main-content -->\n\n\t\t\t<a href=\"#\" id=\"btn-scroll-up\" class=\"btn-scroll-up btn btn-sm btn-inverse\">\n\t\t\t\t<i class=\"ace-icon fa fa-angle-double-up icon-only bigger-110\"></i>\n\t\t\t</a>\n\t\t</div><!-- /.main-container -->\n\t</body>\n</html>\n"
  },
  {
    "path": "backend/web/templates/layout/single_default.html",
    "content": "{% extends \"../common/meta.html\" %}\n{% extends \"../common/header.html\" %}\n{% extends \"../common/footer.html\" %}\n\n{% extends \"single_main.html\" %}"
  },
  {
    "path": "backend/web/templates/layout/single_main.html",
    "content": "<!DOCTYPE html>\n<html>\n\t<head>\n\t\t<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\" />\n\t\t<meta charset=\"utf-8\" />\n\t\t{% block meta %}{% end %}\n\t</head>\n\n\t<body class=\"no-skin\">\n\n\t\t<div class=\"main-container ace-save-state\" id=\"main-container\">\n\t\t\t<div class=\"main-content\">\n\t\t\t\t<div class=\"main-content-inner\">\n\t\t\t\t\t<div class=\"page-content\">\n\t\t\t\t\t\t<div class=\"row\">\n\t\t\t\t\t\t\t<div class=\"col-xs-12\">\n\t\t\t{% block main_content %}{% end %}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div><!-- /.row -->\n\t\t\t\t\t</div><!-- /.page-content -->\n\t\t\t\t</div>\n\t\t\t</div><!-- /.main-content -->\n\n\t\t\t<a href=\"#\" id=\"btn-scroll-up\" class=\"btn-scroll-up btn btn-sm btn-inverse\">\n\t\t\t\t<i class=\"ace-icon fa fa-angle-double-up icon-only bigger-110\"></i>\n\t\t\t</a>\n\t\t</div><!-- /.main-container -->\n\t</body>\n</html>\n"
  },
  {
    "path": "backend/web/templates/minst_serving.html",
    "content": "{% extends \"layout/single_default.html\" %}\n\n\n{% block main_content %}\n\n\n\t<h3 class=\"header smaller lighter blue\">手写图片识别演示</h3>\n\n\t\t<div class=\"row clearfix\">\n                <div class=\"col-md-6 column\">\n\t\t\t\t\t<canvas id=\"the_stage\" width=\"280\" height=\"280\">your browser don't support canvas!</canvas>\n\t\t\t\t\t<div>\n\t\t\t\t\t\t<button type=\"button\" class=\"btn btn-default butt\" onclick=\"clearCanvas()\"><strong>删除</strong></button>\n\t\t\t\t\t\t<button type=\"button\" class=\"btn btn-default butt\" id=\"recognize\" onclick=\"processImg()\"><strong>识别</strong></button>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"col-md-6 column\">\n                    <h3>result:</h3>\n                    <h2 id=\"rec_result\"></h2>\n                </div>\n\t\t</div>\n\n\t<h3 class=\"header smaller lighter blue\">测试图片识别演示</h3>\n\t<div class=\"row clearfix\" style=\"width:400px;\">\n\t\t{% for image in image_array %}\n\t\t\t\t  <div class=\"col-md-2\" style=\"margin-top: 15px;\">\n\t\t\t\t\t  <img src=\"{{ image }}\" alt=\"\" width=\"50\" class=\"minst_img\" height=\"50\" style=\"1px solid #e1e4e5;cursor: pointer;\">\n\t\t\t\t  </div>\n\t\t{% end %}\n\t</div>\n\t<div class=\"col-md-6 column\">\n\t\t\t<h3>结果:</h3>\n\t\t\t<h2 id=\"prediction_text\">\"-1\"</h2>\n    </div>\n<!-- inline scripts related to this page -->\n<script src=\"/static/js/mnist-draw.js\"></script>\n<style>\n\t#the_stage {\n    border: 1px solid #999;\n    border-radius: 4px;\n    box-shadow: -10px 10px 5px #888888;\n}\n</style>\n<script type=\"text/javascript\">\njQuery(function($) {\n\t$(\"#recognize\").click(\n\t\t//提交数据\n\t\tfunction () {\n\t\t\t$(\"#rec_result\").html(\"connecting...\");\n\t\t\tvar scribbler = document.getElementById (\"the_stage\");\n\t\t\tvar imageData =  scribbler.toDataURL('image/png');\n\t\t\tvar dataTemp = imageData.substr(22);\n\n\t\t\tvar sendPackage = {\"id\": \"1\", \"txt\": dataTemp};\n\t\t\t$.post(\"/minst_serving/prediction2\", sendPackage, function(msg){\n\t\t\t\t$(\"#rec_result\").html(msg);\n\t\t\t});\n\t\t}\n\t);\n\n\t$(\".minst_img\").click(\n\t\t//提交数据\n\t\tfunction () {\n\t\t\timg_url = $(this).attr(\"src\")\n\t\t\tconsole.log(\"here\",);\n\t\t\t//\n\t\t\t$.ajax({\n\t\t\t   type: \"GET\",\n\t\t\t   url: \"/minst_serving/prediction\",\n\t\t\t   data: \"img_url=\"+img_url,\n\t\t\t   success: function(msg){\n\t\t\t\t console.log( \"prediction : \" + msg );\n\t\t\t\t $(\"#prediction_text\").html(msg);\n\t\t\t   }\n\t\t\t});\n\t\t}\n\t);\n});\n</script>\n{% end %}"
  },
  {
    "path": "backend/web/templates/stock_chart.html",
    "content": "{% extends \"layout/default.html\" %}\n\n\n{% block main_content %}\n\n\t<h3 class=\"header smaller lighter blue\">欢迎使用股票系统。</h3>\n\n\t<div class=\"clearfix\">\n\t\t<div class=\"pull-right tableTools-container\"></div>\n\t\t<img src=\"/stock/chart/image1\" />\n\t</div>\n{% end %}"
  },
  {
    "path": "backend/web/templates/stock_indicators.html",
    "content": "{% extends \"layout/indicators.html\" %}\n\n\n{% block main_content %}\n\n<!-- 增加 bokeh 样式。-->\n<link rel=\"stylesheet\" href=\"/static/css/bokeh.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"/static/css/bokeh-widgets.min.css\" type=\"text/css\"/>\n<link rel=\"stylesheet\" href=\"/static/css/bokeh-tables.min.css\" type=\"text/css\"/>\n\n<script type=\"text/javascript\" src=\"/static/js/bokeh.min.js\"></script>\n<script type=\"text/javascript\" src=\"/static/js/bokeh-widgets.min.js\"></script>\n<script type=\"text/javascript\" src=\"/static/js/bokeh-tables.min.js\"></script>\n<script type=\"text/javascript\" src=\"/static/js/bokeh-api.min.js\"></script>\n<script type=\"text/javascript\" src=\"/static/js/bokeh-gl.min.js\"></script>\n\n\n\n<script type=\"text/javascript\">\n    Bokeh.set_log_level(\"info\");\n</script>\n\n{% for index,element in enumerate(comp_list) %}\n<h4 class=\"header smaller lighter blue\">{{ element[\"title\"] }}</h4>\n<div class=\"row\">{{ element[\"desc\"] }}</div>\n<div class=\"row\" id=\"_col_{{ index+1 }}\" data-column=\"{{ index }}\">\n    {% raw element[\"div\"] %}\n    {% raw element[\"script\"] %}\n</div>\n{% end %}\n\n{% end %}"
  },
  {
    "path": "backend/web/templates/stock_web.html",
    "content": "{% extends \"layout/default.html\" %}\n\n\n{% block main_content %}\n\n\t<h3 class=\"header smaller lighter blue\">{{ stockWeb.name }}</h3>\n\t<div class=\"table-header\">{{ stockWeb.name }}</div>\n\n\t<div class=\"row\">&nbsp;</div>\n\t<div class=\"row\">\n\t\t{% for index,element in enumerate(stockWeb.columns) %}\n\t\t{% if index < 15 %}\n\t\t\t{% if element != 'eastmoney_url' %}\n\t\t\t<div class=\"col-md-4\" id=\"filter_col{{ index+1 }}\" data-column=\"{{ index }}\">\n\t\t\t\t{{ stockWeb.column_names[index] }} &nbsp;\n\t\t\t\t<!-- Column - {{ stockWeb.column_names[index] }} - {{ element }} -->\n\n\t\t\t\t<input type=\"text\" class=\"column_filter input-group-sm form-control {% if element == 'date' %}date-picker{% end %}\"\n\t\t\t\t\t   {% if element == 'date' %} value=\"{{ date_now }}\" {% end %}\n\t\t\t\t\t   id=\"col{{ index }}_filter\">\n\t\t\t</div>\n\t\t\t{% end %}\n\t\t{% end %}{% end %}\n\t</div>\n\n\t<table cellpadding=\"3\" cellspacing=\"0\" border=\"0\" style=\"width: 67%; margin: 0 auto 2em auto;\">\n\t\t<tbody>\n\n\t\t</tbody>\n\t</table>\n\t<!-- div.table-responsive -->\n\t<!-- div.dataTables_borderWrap -->\n\t<div>\n\t\t<div class=\"pull-right tableTools-container\"></div>\n\t\t<table id=\"dynamic-table\" class=\"table table-striped table-bordered table-hover\">\n\t\t\t<thead>\n\t\t\t\t<tr>{% for column_name in stockWeb.column_names %}\n\t\t\t\t\t<th>{{ column_name }}</th>\n\t\t\t\t{% end %}\n\t\t\t\t</tr>\n\t\t\t</thead>\n\t\t</table>\n\t</div>\n\n\t<!-- 定义一个窗口的Div 东方财富股票数据页面 -->\n\t<div id=\"dfcf-window-modal\" class=\"modal fade\">\n\t\t<div class=\"modal-dialog\" style=\"width:1095px;\">\n\t\t\t<div class=\"modal-content\">\n\t\t\t\t<div class=\"modal-header\">\n\t\t\t\t\t<button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n\t\t\t\t\t<h3 class=\"smaller lighter blue no-margin\">东方财富分析</h3>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"modal-body\" id=\"dfcf-iframe-body\" >\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<!-- 定义一个窗口的Div 指标分析数据页面 -->\n\t<div id=\"indicators-window-modal\" class=\"modal fade\">\n\t\t<div class=\"modal-dialog\" style=\"width:1050px;\">\n\t\t\t<div class=\"modal-content\">\n\t\t\t\t<div class=\"modal-header\">\n\t\t\t\t\t<button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n\t\t\t\t\t<h3 class=\"smaller lighter blue no-margin\">股票数据详细指标分析</h3>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"modal-body\" id=\"indicators-window-body\" >\n\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n<!-- inline scripts related to this page -->\n<script type=\"text/javascript\">\n\n\t//每次动态加载 指标分析窗口\n\tfunction showIndicatorsWindow(code) {\n\t\tvar baseUrl = '/api/v1/data/indicators?code='+code; // 没有跨域问题，直接加载\n\t\t$('#indicators-window-body').html( \"<h5>数据加载中...请稍候.</h5>\" );\n\t\t$('#indicators-window-body').context = $(\"#indicators-window-body\").load(baseUrl);\n\t\t$('#indicators-window-modal').modal('show');\n\t}\n\n\t//每次动态加载 东方财富窗口\n\tfunction showDFCFWindow(code) {\n\n\t\tvar iframe = document.createElement('iframe')\n\t\t// var baseUrl = 'https://emweb.eastmoney.com/PC_HSF10/ShareholderResearch/Index?type=soft&code='+code;\n\t\tvar baseUrl = 'https://emweb.eastmoney.com/PC_HSF10/OperationsRequired/Index?type=soft&code='+code;\n\t\tiframe.src = baseUrl;\n\t\tiframe.width = '1060px';\n\t\tiframe.height = '700px';\n\t\tiframe.frameborder = '0';\n\t\t$('#dfcf-iframe-body').empty(); // 先清空数据\n\t\tdocument.getElementById(\"dfcf-iframe-body\").appendChild(iframe)\n\t\t$('#dfcf-window-modal').modal('show');\n\t}\n\n\tjQuery(function($) {\n\n\n\t\t$( \".date-picker\" ).datepicker({\n\t\t\tlanguage: 'zh-CN', //设置语言\n            format:\"yyyymmdd\",\n            showOtherMonths: true,\n            selectOtherMonths: false,\n            autoclose: true,\n\t\t\ttodayHighlight: true,\n\t\t\tonSelect: function(selected,evnt) {\n\t\t\t\t console.log(selected);\n\t\t\t}\n        }).on('changeDate', function(ev){\n\t\t\t$('input.column_filter').click();//触发搜索\n\t\t});\n\n\n\t\tvar nameParam = $.getUrlVar('table_name');\n\t\t//console.log(nameParam);\n\t\tvar myTable = $('#dynamic-table').DataTable( {\n\t\t\t\"select\": true,\n\t\t\t\"bFilter\": true,\n\t\t\t\"ordering\": true,\n\t\t\t\"processing\": true,\n\t\t\t\"serverSide\": true,\n\t\t\t\"lengthMenu\": [[20, 30, 50, 100,1000, -1], [20, 30, 50, 100,1000, \"All\"]],\n\t\t\t\"language\": {\n                \"url\": \"/static/js/datatables.Chinese.json\"\n            },\n\t\t\t\"ajax\": \"/api/v1/api_data?name=\"+nameParam,\n\t\t\t\"columns\": [\n\t\t\t\t{% for column in stockWeb.columns %}\n\t\t\t\t\t{ \"data\": \"{{ column }}\" },\n\t\t\t\t{% end %}\n        \t]\n\t\t} );\n\n\t\t$('input.column_filter').on( 'keyup click', function () {\n\t\t\tvar i =  $(this).parents('div').attr('data-column') ;\n\t\t\t//console.log(i,\"val:\",$(this).val());\n\t\t\tmyTable.column(i).search(\n\t\t\t\t$(this).val()\n\t\t\t).draw();\n\t\t});\n\n\t\t$('input.column_filter').click();//第一次点击执行，读取时间信息。\n\n\t\t$.fn.dataTable.Buttons.defaults.dom.container.className = 'dt-buttons btn-overlap btn-group btn-overlap';\n\n\t\tnew $.fn.dataTable.Buttons( myTable, {\n\t\t\tbuttons: [\n\t\t\t  {\n\t\t\t\t\"extend\": \"copy\",\n\t\t\t\t\"text\": \"<i class='fa fa-copy bigger-110 pink'></i> <span class='hidden'>Copy to clipboard</span>\",\n\t\t\t\t\"className\": \"btn btn-white btn-primary btn-bold\"\n\t\t\t  },\n\t\t\t  {\n\t\t\t\t\"extend\": \"csv\",\n\t\t\t\t\"text\": \"<i class='fa fa-database bigger-110 orange'></i> <span class='hidden'>Export to CSV</span>\",\n\t\t\t\t\"className\": \"btn btn-white btn-primary btn-bold\"\n\t\t\t  },\n\t\t\t  {\n\t\t\t\t\"extend\": \"print\",\n\t\t\t\t\"text\": \"<i class='fa fa-print bigger-110 grey'></i> <span class='hidden'>Print</span>\",\n\t\t\t\t\"className\": \"btn btn-white btn-primary btn-bold\",\n\t\t\t\tautoPrint: false,\n\t\t\t\tmessage: 'This print was produced using the Print button for DataTables'\n\t\t\t  }\n\t\t\t]\n\t\t} );\n\t\tmyTable.buttons().container().appendTo( $('.tableTools-container') );\n\n\t})\n</script>\n{% end %}"
  },
  {
    "path": "backend/web/templates/test.html",
    "content": "{% extends \"layout/default.html\" %}\n\n\n{% block main_content %}\n\n\t<h3 class=\"header smaller lighter blue\">欢迎使用股票系统。</h3>\n\n\t<div class=\"clearfix\">\n\t\t<div class=\"pull-right tableTools-container\"></div>\n\t</div>\n\n\t<div class=\"col-sm-1\">\n\t\t\t<button onclick=\"showDFCFWindow('SZ000001');\" role=\"button\" class=\"btn btn-sm btn-primary\" data-toggle=\"modal\">\n\t\t\t\t<i class=\"ace-icon fa fa-plus\"></i>\n\t\t\t\t测试弹窗东方财富窗口\n\t\t\t</button>\n\t</div><!-- /.col -->\n\n\t<!--<iframe src=\"https://emweb.eastmoney.com/PC_HSF10/ShareholderResearch/Index?type=soft&code=SZ000001\" width=\"700px\" height=\"700px\"></iframe>-->\n\n\t<!-- 定义一个窗口的Div 东方财富股票数据页面 -->\n\t<div id=\"dfcf-window-modal\" class=\"modal fade\">\n\t\t<div class=\"modal-dialog\" style=\"width:1010px;\">\n\t\t\t<div class=\"modal-content\">\n\t\t\t\t<div class=\"modal-header\">\n\t\t\t\t\t<button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n\t\t\t\t\t<h3 class=\"smaller lighter blue no-margin\">东方财富分析</h3>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"modal-body\" id=\"dfcf-iframe-body\" >\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n    <script type=\"text/javascript\">\n        //每次动态加载 东方财富窗口\n        function showDFCFWindow(code) {\n\n        \tvar iframe = document.createElement('iframe')\n        \tvar baseUrl = 'https://emweb.eastmoney.com/PC_HSF10/ShareholderResearch/Index?type=soft&code='+code;\n        \t// var baseUrl = 'https://emweb.eastmoney.com/PC_HSF10/OperationsRequired/Index?type=soft&code=';\n\t\t\tiframe.src = baseUrl;\n\t\t\tiframe.width = '980px';\n\t\t\tiframe.height = '700px';\n\t\t\tiframe.frameborder = '0';\n\t\t\t$('#iframe-body').empty(); // 先清空数据\n\t\t\tdocument.getElementById(\"dfcf-iframe-body\").appendChild(iframe)\n            $('#dfcf-window-modal').modal('show');\n        }\n\t</script>\n\n{% end %}"
  },
  {
    "path": "backend/web/templates/test2.html",
    "content": "{% extends \"layout/default.html\" %}\n\n\n{% block main_content %}\n\n\t<h3 class=\"header smaller lighter blue\">欢迎使用股票系统。</h3>\n\n\t<div class=\"clearfix\">\n\t\t<div class=\"pull-right tableTools-container\"></div>\n\t</div>\n\n\t<div class=\"col-sm-1\">\n\t\t\t<button onclick=\"showIndicatorsWindow('000001');\" role=\"button\" class=\"btn btn-sm btn-primary\" data-toggle=\"modal\">\n\t\t\t\t<i class=\"ace-icon fa fa-plus\"></i>\n\t\t\t\t测试弹窗指标分析窗口\n\t\t\t</button>\n\t</div><!-- /.col -->\n\n\t<!--<iframe src=\"https://emweb.eastmoney.com/PC_HSF10/ShareholderResearch/Index?type=soft&code=SZ000001\" width=\"700px\" height=\"700px\"></iframe>-->\n\n\t<!-- 定义一个窗口的Div 指标分析数据页面 -->\n\t<div id=\"indicators-window-modal\" class=\"modal fade\">\n\t\t<div class=\"modal-dialog\" style=\"width:1050px;\">\n\t\t\t<div class=\"modal-content\">\n\t\t\t\t<div class=\"modal-header\">\n\t\t\t\t\t<button type=\"button\" class=\"close\" data-dismiss=\"modal\" aria-hidden=\"true\">&times;</button>\n\t\t\t\t\t<h3 class=\"smaller lighter blue no-margin\">股票数据详细指标分析</h3>\n\t\t\t\t</div>\n\t\t\t\t<div class=\"modal-body\" id=\"indicators-window-body\" >\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n    <script type=\"text/javascript\">\n        //每次动态加载 指标分析窗口\n        function showIndicatorsWindow(code) {\n        \tvar baseUrl = '/data/indicators?code='+code; // 没有跨域问题，直接加载\n\t\t\t$('#indicators-window-body').context = $(\"#indicators-window-body\").load(baseUrl);\n            $('#indicators-window-modal').modal('show');\n        }\n\t</script>\n\n{% end %}"
  },
  {
    "path": "backend/web/test_thread.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\nimport time\nfrom tornado.httpserver import HTTPServer\nfrom tornado.ioloop import IOLoop\nfrom tornado.web import Application, asynchronous, RequestHandler\nfrom multiprocessing.pool import ThreadPool\nfrom multiprocessing.pool import ApplyResult\nfrom tornado import gen\n\n# https://gist.github.com/methane/2185380 参考\n\n\n\nhtml_content = \"\"\"\n<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n      <script src=\"http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js\"></script>\n</head>\n<body>\n    <h1>任务测试</h1></br>\n    <button id=\"job\">开始</button>\n</body>\n</html>\n<script type=\"text/javascript\">\n    function job_check(timer,tid) {\n        $.ajax({\n           type: \"GET\",\n           url: \"job_check?tid=\"+tid,\n           success: function(msg){\n                console.log(msg);\n                if(msg != \"\"){\n                    alert( \"任务结果: \" + msg );\n                    clearInterval(timer);//结束轮询\n                }\n            }\n        });\n    }\n\tjQuery(function($) {\n\t    \n\t    $(\"#job\").click( function () {\n\t        $.ajax({\n               type: \"GET\",\n               url: \"add_job\",\n               success: function(tid){\n                    alert( \"开始任务: \" + tid );\n                    timer = setInterval(function(){\n                        console.log(\"run.\");\n                        job_check(timer,tid);\n                    },1000);\n               }\n            });\n\t    });\n\t})\n</script>\n\"\"\"\n\n\nclass MainPage(RequestHandler):\n    def get(self):\n        self.write(html_content)\n\n\n_workers = ThreadPool(10)\n_result = {}\n\n\n# 后台任务。\ndef blocking_task(n, tid):\n    time.sleep(n)\n    print(tid)\n    _result[tid] = {\"finish\"}\n\n\nclass AddJobHandler(RequestHandler):\n    @gen.coroutine\n    def get(self):\n        tid = str(int(time.time() * 10000))\n        _workers.apply_async(blocking_task, (10, tid))  # 传递参数 10 秒。\n        self.write(tid)\n        self.finish()  # 先finish 掉，然后在后台执行。\n\n\nclass JobCheckHandler(RequestHandler):\n    def get(self):\n        tid = self.get_argument(\"tid\")\n        if tid in _result.keys():\n            out = _result[tid]  # 结果\n            del _result[tid]  # 删除tid的数据。\n            self.write(str(out))\n        else:\n            self.write(\"\")\n\n\n# main 启动。\nif __name__ == \"__main__\":\n    HTTPServer(Application([\n        (\"/\", MainPage),\n        (\"/add_job\", AddJobHandler),\n        (\"/job_check\", JobCheckHandler)\n    ], debug=True)).listen(9090)\n    print(\"start web .\")\n    IOLoop.instance().start()\n"
  },
  {
    "path": "backend/web/test_thread_v2.py",
    "content": "#!/usr/local/bin/python3\n# -*- coding: utf-8 -*-\nimport time\nfrom tornado.httpserver import HTTPServer\nfrom tornado.ioloop import IOLoop\nfrom tornado.web import Application, asynchronous, RequestHandler\nfrom tornado import gen\nfrom tornado.concurrent import run_on_executor\nfrom concurrent.futures import ThreadPoolExecutor\n\n# `pip install futures` for python2\n\n# https://gist.github.com/methane/2185380 参考\n\nhtml_content = \"\"\"\n<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n      <script src=\"http://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js\"></script>\n</head>\n<body>\n    <h1>任务测试</h1></br>\n    <button id=\"job\">开始</button>\n</body>\n</html>\n<script type=\"text/javascript\">\n    function job_check(timer,tid) {\n        $.ajax({\n           type: \"GET\",\n           url: \"job_check?tid=\"+tid,\n           success: function(msg){\n                console.log(msg);\n                if(msg != \"\"){\n                    alert( \"任务结果: \" + msg );\n                    clearInterval(timer);//结束轮询\n                }\n            }\n        });\n    }\n\tjQuery(function($) {\n\t    \n\t    $(\"#job\").click( function () {\n\t        $.ajax({\n               type: \"GET\",\n               url: \"add_job\",\n               success: function(tid){\n                    alert( \"开始任务: \" + tid );\n                    timer = setInterval(function(){\n                        console.log(\"run.\");\n                        job_check(timer,tid);\n                    },1000);\n               }\n            });\n\t    });\n\t})\n</script>\n\"\"\"\n\n\nclass MainPage(RequestHandler):\n    def get(self):\n        self.write(html_content)\n\n\nMAX_WORKERS = 4\n_result = {}\n\n\nclass AddJobHandler(RequestHandler):\n    # 必须定义一个executor的属性，然后run_on_executor 注解才管用。\n    executor = ThreadPoolExecutor(max_workers=MAX_WORKERS)\n\n    @run_on_executor  # 标记成后台程序执行。\n    def background_task(self, tid):\n        time.sleep(10)  # 传递参数 10 秒。\n        _result[tid] = {\"finish\"}\n\n    @gen.coroutine\n    def get(self):\n        tid = str(int(time.time() * 10000))\n        self.background_task(tid)\n        self.write(tid)\n\n\nclass JobCheckHandler(RequestHandler):\n    def get(self):\n        tid = self.get_argument(\"tid\")\n        if tid in _result.keys():\n            out = _result[tid]  # 结果\n            del _result[tid]  # 删除tid的数据。\n            self.write(str(out))\n        else:\n            self.write(\"\")\n\n\n# main 启动。\nif __name__ == \"__main__\":\n    HTTPServer(Application([\n        (\"/\", MainPage),\n        (\"/add_job\", AddJobHandler),\n        (\"/job_check\", JobCheckHandler)\n    ], debug=True)).listen(9999)\n    print(\"start web .\")\n    IOLoop.instance().start()\n"
  },
  {
    "path": "backend/web/tornado_bokeh_embed.py",
    "content": "from jinja2 import Environment, FileSystemLoader\n\nfrom tornado.web import RequestHandler\n\nfrom bokeh.application import Application\nfrom bokeh.application.handlers import FunctionHandler\nfrom bokeh.embed import server_document\nfrom bokeh.layouts import column\nfrom bokeh.models import ColumnDataSource, Slider\nfrom bokeh.plotting import figure\nfrom bokeh.server.server import Server\nfrom bokeh.themes import Theme\n\nfrom bokeh.sampledata.sea_surface_temperature import sea_surface_temperature\n\nenv = Environment(loader=FileSystemLoader('templates'))\nserver_url = \"http://localhost:9090/\"\n\n\nclass IndexHandler(RequestHandler):\n    def get(self):\n        print(\"index ...\")\n        template = env.get_template('bokeh_embed.html')\n        script = server_document(server_url + 'bkapp')\n        print(script)\n        self.write(template.render(script=script, template=\"Tornado\"))\n        # self.write(html_content)\n\n\ndef modify_doc(doc):\n    df = sea_surface_temperature.copy()\n    source = ColumnDataSource(data=df)\n\n    plot = figure(x_axis_type='datetime', y_range=(0, 25), y_axis_label='Temperature (Celsius)',\n                  title=\"Sea Surface Temperature at 43.18, -70.43\")\n    plot.line('time', 'temperature', source=source)\n\n    def callback(attr, old, new):\n        if new == 0:\n            data = df\n        else:\n            data = df.rolling('{0}D'.format(new)).mean()\n        source.data = ColumnDataSource(data=data).data\n\n    slider = Slider(start=0, end=30, value=0, step=1, title=\"Smoothing by N Days\")\n    slider.on_change('value', callback)\n\n    doc.add_root(column(slider, plot))\n\n    # doc.theme = Theme(filename=\"theme.yaml\")\n\n\nbokeh_app = Application(FunctionHandler(modify_doc))\n\n# Setting num_procs here means we can't touch the IOLoop before now, we must\n# let Server handle that. If you need to explicitly handle IOLoops then you\n# will need to use the lower level BaseServer class.\nserver = Server(\n    {'/bkapp': bokeh_app}, num_procs=1, port=9999,\n    extra_patterns=[('/', IndexHandler)]\n)\nserver.start()\n\nif __name__ == '__main__':\n    from bokeh.util.browser import view\n\n    print('Opening Tornado app with embedded Bokeh application on ' + server_url)\n\n    server.io_loop.add_callback(view, server_url)\n    server.io_loop.start()\n"
  },
  {
    "path": "docker-compose/.gitignore",
    "content": "# C extensions\n*.so\n\ndata\n.idea\n*.iml\n.DS_Store\n*.zip\n*.log\n*.pyc\ndoc\n/bin\npkg\n*.tmp"
  },
  {
    "path": "docker-compose/LICENSE",
    "content": "                                 Apache License\r\n                           Version 2.0, January 2004\r\n                        http://www.apache.org/licenses/\r\n\r\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\r\n\r\n   1. Definitions.\r\n\r\n      \"License\" shall mean the terms and conditions for use, reproduction,\r\n      and distribution as defined by Sections 1 through 9 of this document.\r\n\r\n      \"Licensor\" shall mean the copyright owner or entity authorized by\r\n      the copyright owner that is granting the License.\r\n\r\n      \"Legal Entity\" shall mean the union of the acting entity and all\r\n      other entities that control, are controlled by, or are under common\r\n      control with that entity. For the purposes of this definition,\r\n      \"control\" means (i) the power, direct or indirect, to cause the\r\n      direction or management of such entity, whether by contract or\r\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\r\n      outstanding shares, or (iii) beneficial ownership of such entity.\r\n\r\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\r\n      exercising permissions granted by this License.\r\n\r\n      \"Source\" form shall mean the preferred form for making modifications,\r\n      including but not limited to software source code, documentation\r\n      source, and configuration files.\r\n\r\n      \"Object\" form shall mean any form resulting from mechanical\r\n      transformation or translation of a Source form, including but\r\n      not limited to compiled object code, generated documentation,\r\n      and conversions to other media types.\r\n\r\n      \"Work\" shall mean the work of authorship, whether in Source or\r\n      Object form, made available under the License, as indicated by a\r\n      copyright notice that is included in or attached to the work\r\n      (an example is provided in the Appendix below).\r\n\r\n      \"Derivative Works\" shall mean any work, whether in Source or Object\r\n      form, that is based on (or derived from) the Work and for which the\r\n      editorial revisions, annotations, elaborations, or other modifications\r\n      represent, as a whole, an original work of authorship. For the purposes\r\n      of this License, Derivative Works shall not include works that remain\r\n      separable from, or merely link (or bind by name) to the interfaces of,\r\n      the Work and Derivative Works thereof.\r\n\r\n      \"Contribution\" shall mean any work of authorship, including\r\n      the original version of the Work and any modifications or additions\r\n      to that Work or Derivative Works thereof, that is intentionally\r\n      submitted to Licensor for inclusion in the Work by the copyright owner\r\n      or by an individual or Legal Entity authorized to submit on behalf of\r\n      the copyright owner. For the purposes of this definition, \"submitted\"\r\n      means any form of electronic, verbal, or written communication sent\r\n      to the Licensor or its representatives, including but not limited to\r\n      communication on electronic mailing lists, source code control systems,\r\n      and issue tracking systems that are managed by, or on behalf of, the\r\n      Licensor for the purpose of discussing and improving the Work, but\r\n      excluding communication that is conspicuously marked or otherwise\r\n      designated in writing by the copyright owner as \"Not a Contribution.\"\r\n\r\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\r\n      on behalf of whom a Contribution has been received by Licensor and\r\n      subsequently incorporated within the Work.\r\n\r\n   2. Grant of Copyright License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      copyright license to reproduce, prepare Derivative Works of,\r\n      publicly display, publicly perform, sublicense, and distribute the\r\n      Work and such Derivative Works in Source or Object form.\r\n\r\n   3. Grant of Patent License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      (except as stated in this section) patent license to make, have made,\r\n      use, offer to sell, sell, import, and otherwise transfer the Work,\r\n      where such license applies only to those patent claims licensable\r\n      by such Contributor that are necessarily infringed by their\r\n      Contribution(s) alone or by combination of their Contribution(s)\r\n      with the Work to which such Contribution(s) was submitted. If You\r\n      institute patent litigation against any entity (including a\r\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\r\n      or a Contribution incorporated within the Work constitutes direct\r\n      or contributory patent infringement, then any patent licenses\r\n      granted to You under this License for that Work shall terminate\r\n      as of the date such litigation is filed.\r\n\r\n   4. Redistribution. You may reproduce and distribute copies of the\r\n      Work or Derivative Works thereof in any medium, with or without\r\n      modifications, and in Source or Object form, provided that You\r\n      meet the following conditions:\r\n\r\n      (a) You must give any other recipients of the Work or\r\n          Derivative Works a copy of this License; and\r\n\r\n      (b) You must cause any modified files to carry prominent notices\r\n          stating that You changed the files; and\r\n\r\n      (c) You must retain, in the Source form of any Derivative Works\r\n          that You distribute, all copyright, patent, trademark, and\r\n          attribution notices from the Source form of the Work,\r\n          excluding those notices that do not pertain to any part of\r\n          the Derivative Works; and\r\n\r\n      (d) If the Work includes a \"NOTICE\" text file as part of its\r\n          distribution, then any Derivative Works that You distribute must\r\n          include a readable copy of the attribution notices contained\r\n          within such NOTICE file, excluding those notices that do not\r\n          pertain to any part of the Derivative Works, in at least one\r\n          of the following places: within a NOTICE text file distributed\r\n          as part of the Derivative Works; within the Source form or\r\n          documentation, if provided along with the Derivative Works; or,\r\n          within a display generated by the Derivative Works, if and\r\n          wherever such third-party notices normally appear. The contents\r\n          of the NOTICE file are for informational purposes only and\r\n          do not modify the License. You may add Your own attribution\r\n          notices within Derivative Works that You distribute, alongside\r\n          or as an addendum to the NOTICE text from the Work, provided\r\n          that such additional attribution notices cannot be construed\r\n          as modifying the License.\r\n\r\n      You may add Your own copyright statement to Your modifications and\r\n      may provide additional or different license terms and conditions\r\n      for use, reproduction, or distribution of Your modifications, or\r\n      for any such Derivative Works as a whole, provided Your use,\r\n      reproduction, and distribution of the Work otherwise complies with\r\n      the conditions stated in this License.\r\n\r\n   5. Submission of Contributions. Unless You explicitly state otherwise,\r\n      any Contribution intentionally submitted for inclusion in the Work\r\n      by You to the Licensor shall be under the terms and conditions of\r\n      this License, without any additional terms or conditions.\r\n      Notwithstanding the above, nothing herein shall supersede or modify\r\n      the terms of any separate license agreement you may have executed\r\n      with Licensor regarding such Contributions.\r\n\r\n   6. Trademarks. This License does not grant permission to use the trade\r\n      names, trademarks, service marks, or product names of the Licensor,\r\n      except as required for reasonable and customary use in describing the\r\n      origin of the Work and reproducing the content of the NOTICE file.\r\n\r\n   7. Disclaimer of Warranty. Unless required by applicable law or\r\n      agreed to in writing, Licensor provides the Work (and each\r\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\r\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\r\n      implied, including, without limitation, any warranties or conditions\r\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\r\n      PARTICULAR PURPOSE. You are solely responsible for determining the\r\n      appropriateness of using or redistributing the Work and assume any\r\n      risks associated with Your exercise of permissions under this License.\r\n\r\n   8. Limitation of Liability. In no event and under no legal theory,\r\n      whether in tort (including negligence), contract, or otherwise,\r\n      unless required by applicable law (such as deliberate and grossly\r\n      negligent acts) or agreed to in writing, shall any Contributor be\r\n      liable to You for damages, including any direct, indirect, special,\r\n      incidental, or consequential damages of any character arising as a\r\n      result of this License or out of the use or inability to use the\r\n      Work (including but not limited to damages for loss of goodwill,\r\n      work stoppage, computer failure or malfunction, or any and all\r\n      other commercial damages or losses), even if such Contributor\r\n      has been advised of the possibility of such damages.\r\n\r\n   9. Accepting Warranty or Additional Liability. While redistributing\r\n      the Work or Derivative Works thereof, You may choose to offer,\r\n      and charge a fee for, acceptance of support, warranty, indemnity,\r\n      or other liability obligations and/or rights consistent with this\r\n      License. However, in accepting such obligations, You may act only\r\n      on Your own behalf and on Your sole responsibility, not on behalf\r\n      of any other Contributor, and only if You agree to indemnify,\r\n      defend, and hold each Contributor harmless for any liability\r\n      incurred by, or claims asserted against, such Contributor by reason\r\n      of your accepting any such warranty or additional liability.\r\n\r\n   END OF TERMS AND CONDITIONS\r\n\r\n   APPENDIX: How to apply the Apache License to your work.\r\n\r\n      To apply the Apache License to your work, attach the following\r\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\r\n      replaced with your own identifying information. (Don't include\r\n      the brackets!)  The text should be enclosed in the appropriate\r\n      comment syntax for the file format. We also recommend that a\r\n      file or class name and description of purpose be included on the\r\n      same \"printed page\" as the copyright notice for easier\r\n      identification within third-party archives.\r\n\r\n   Copyright [yyyy] [name of copyright owner]\r\n\r\n   Licensed under the Apache License, Version 2.0 (the \"License\");\r\n   you may not use this file except in compliance with the License.\r\n   You may obtain a copy of the License at\r\n\r\n       http://www.apache.org/licenses/LICENSE-2.0\r\n\r\n   Unless required by applicable law or agreed to in writing, software\r\n   distributed under the License is distributed on an \"AS IS\" BASIS,\r\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n   See the License for the specific language governing permissions and\r\n   limitations under the License.\r\n"
  },
  {
    "path": "docker-compose/README.md",
    "content": "## 镜像仓库选择\n\nhttps://github.com/DaoCloud/public-image-mirror\n\n```bash\n\n# python使用镜像\ndocker.m.daocloud.io/library/python:3.11-slim-bullseye\n\n# mysql使用：\ndocker.m.daocloud.io/library/mysql:8\n\n```\n\n\n## 本地部署方法\n\n\n```\ngit clone git@gitee.com:pythonstock/docker-compose.git\n\ncd docker-compose\n\ndocker-compose up -d\n\n```\n\n## 访问地址\n\nhttp://localhost:9090/\n\n\n\n## 查看日至，进入项目代码\n\n```\n# 查看启动日志：\ndocker logs -f stock\n\n# 进入stock容器\ndocker exec -it stock bash\n```\n\n## 开发模式，映射stock 代码方法\n\n直接使用 dev yml 即可，会映射stock到/data/stock 然后在外部修改代码容器中运行即可。\n\n```bash\ndocker-compose  -f dev-docker-compose.yml  up -d\n```\n\n```bash\ndocker-compose  -f docker-compose-v2.0.yml up -d\n```\n\n## 老镜像还保存一个版本\n\n```\npythonstock/pythonstock:v2021\n```"
  },
  {
    "path": "docker-compose/build_stock.sh",
    "content": "#!/bin/sh\n\ncd ../stock\n\nNOW_MONTH=$(date \"+%Y-%m\")\n\nDOCKER_TAG=pythonstock/pythonstock:latest\nDOCKER_TAG_MONTH=pythonstock/pythonstock:stock-${NOW_MONTH}\n\necho \" docker build -f Dockerfile -t ${DOCKER_TAG} .\"\ndocker build -f Dockerfile -t ${DOCKER_TAG} .\necho \" docker build tag xxx ${DOCKER_TAG_MONTH} \"\necho \"#################################################################\"\necho \" docker push ${DOCKER_TAG} \"\n\n\n"
  },
  {
    "path": "docker-compose/dev-docker-compose-restart.sh",
    "content": "#!/bin/sh\n\n\ngit pull\n\nsleep 1\ndocker-compose -f dev-docker-compose.yml down\n\nsleep 1\ndocker-compose -f dev-docker-compose.yml up -d\n\necho \"restart dev-docker-compose\"\n\n"
  },
  {
    "path": "docker-compose/dev-docker-compose.yml",
    "content": "networks:\n  stock-dev-network:\n    driver: bridge\n\nversion: \"3\"\nservices:\n    frontend:\n        image: pythonstock/frontend-dev:latest\n        build:\n            context: .\n            dockerfile: docker/DevFrontendDockerfile\n        container_name: frontend\n        ports:\n            - \"8080:8080\"\n        volumes:\n        # 设置开发目录，方便开发调试\n            - \"../frontend:/usr/src/app\"\n        environment:\n            LANG: zh_CN.UTF-8\n            LC_CTYPE: zh_CN.UTF-8\n        restart: always\n        networks:\n            stock-dev-network: {}\n        # 入口写死，手动启动应用。\n        entrypoint: /usr/src/app/docker-entrypoint.sh\n        depends_on:\n            - backend\n    backend:\n        image: pythonstock/backend-dev:latest\n        build:\n            context: .\n            dockerfile: docker/DevBackendDockerfile\n        container_name: backend\n        ports:\n            - \"8888:8888\"\n            - \"9090:9090\"\n        volumes:\n        # 设置开发目录，方便开发调试\n            - \"../backend/jobs/crontab:/var/spool/cron/crontabs/root\"\n            - \"../backend/jobs/cron.minutely:/etc/cron.minutely\"\n            - \"../backend/jobs/cron.hourly:/etc/cron.hourly\"\n            - \"../backend/jobs/cron.daily:/etc/cron.daily\"\n            - \"../backend/jobs/cron.monthly:/etc/cron.monthly\"\n            - \"../backend:/data/stock\"\n            - \"../backend/supervisor:/data/supervisor\"\n            - \"./data/notebooks:/data/notebooks\"\n            - \"./data/logs:/data/logs\"\n        environment:\n            MYSQL_HOST: mysql-stock\n            MYSQL_USER: root\n            MYSQL_PWD: mysql-stock\n            MYSQL_DB: stock_data\n            MYSQL_PORT: 3306\n            LANG: zh_CN.UTF-8\n            LC_CTYPE: zh_CN.UTF-8\n            PYTHONIOENCODING: utf-8\n        restart: always\n        networks:\n            stock-dev-network: {}\n        # 入口写死，手动启动应用。\n        #entrypoint: sleep 999999d\n        depends_on:\n            - mysql-stock\n    mysql-stock:\n        # image: hub.atomgit.com/library/mysql:5.7\n        # https://hub.atomgit.com/repos/amd64/mysql\n        image: docker.m.daocloud.io/library/mysql:8\n        container_name: mysql-stock\n        # 执行命令：https://juejin.cn/s/mysql%20healthcheck%20docker-compose\n        healthcheck:\n            test: [\"CMD\", \"mysqladmin\" ,\"ping\", \"-uroot\", \"-pmysql-stock\"]\n            interval: 10s\n            timeout: 5s\n            retries: 5\n        ports:\n            - \"3306:3306\"\n        networks:\n            stock-dev-network: {}\n        volumes:\n            - \"./mysql/my.cnf:/etc/mysql/my.cnf\"\n            - \"./data/mysql-stock/data:/var/lib/mysql\"\n            - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql\n        environment:\n            MYSQL_ROOT_PASSWORD: mysql-stock\n            MYSQL_DATABASE: stock_data\n            TZ: Asia/Shanghai\n        command: [\n            '--character-set-server=utf8mb4',\n            '--collation-server=utf8mb4_general_ci',\n            '--max_connections=3000'\n        ]\n        restart: always\n"
  },
  {
    "path": "docker-compose/docker/DevBackendDockerfile",
    "content": "\n\n# https://hub.docker.com/_/python?tab=tags&page=1&name=3.11-slim-bullseye\n# 用这个做为基础镜像，防止每次都进行构建。\n\n#FROM docker.io/python:3.11-slim-bullseye\nFROM docker.m.daocloud.io/library/python:3.11-slim-bullseye\n\n# https://opsx.alibaba.com/mirror\n# 使用阿里云镜像地址。修改debian apt 更新地址，pip 地址，设置时区。\n# 设置debian的镜像源\nRUN echo \"deb http://mirrors.aliyun.com/debian/ bullseye main non-free contrib\" > /etc/apt/sources.list && \\\necho \"deb-src http://mirrors.aliyun.com/debian/ bullseye main non-free contrib\" >> /etc/apt/sources.list && \\\necho \"deb http://mirrors.aliyun.com/debian-security/ bullseye-security main\" >> /etc/apt/sources.list && \\\necho \"deb-src http://mirrors.aliyun.com/debian-security/ bullseye-security main\" >> /etc/apt/sources.list && \\\necho \"deb http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib\" >> /etc/apt/sources.list && \\\necho \"deb-src http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib\" >> /etc/apt/sources.list && \\\necho \"deb http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib\" >> /etc/apt/sources.list && \\\necho \"deb-src http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib\" >> /etc/apt/sources.list && \\\necho  \"[global]\\n\\\ntrusted-host=mirrors.aliyun.com\\n\\\nindex-url=http://mirrors.aliyun.com/pypi/simple\" > /etc/pip.conf && \\\nln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \\\n    echo \"Asia/Shanghai\" > /etc/timezone\n\n#增加语言utf-8\nENV LANG=zh_CN.UTF-8\nENV LC_CTYPE=zh_CN.UTF-8\nENV LC_ALL=C\nENV PYTHONPATH=/data/stock\n\n# 增加 TensorFlow 的支持，使用最新的2.0 编写代码。目前还是使用 1.x 吧，还没有学明白。\n# RUN pip3 install tensorflow==2.0.0-rc1 keras\n# RUN pip3 install tensorflow keras sklearn\n\n# 设置 vim 的语言配置\nRUN mkdir -p /etc/vim/ && \\\n    echo \"set fileencodings=utf-8,ucs-bom,gb18030,gbk,gb2312,cp936\" >> /etc/vim/vimrc && \\\n    echo \"set termencoding=utf-8\" >> /etc/vim/vimrc && \\\n    echo \"set encoding=utf-8\" >> /etc/vim/vimrc\n\n# 安装 mysqlclient akshare (pandas ,numpy) tornado bokeh\n# 安装 nodejs 库\n# apt-get autoremove -y 删除没有用的依赖lib。减少镜像大小。1MB 也要节省。\n# apt-get --purge remove 软件包名称 , 删除已安装包（不保留配置文件)。\nRUN apt-get update && apt-get install -y git pkg-config net-tools procps gcc make python3-dev default-libmysqlclient-dev libxml2-dev cron  && \\\n    pip3 install mysqlclient sqlalchemy && \\\n    pip3 install supervisor && \\\n    pip3 install numpy pandas  && \\\n    pip3 install akshare --upgrade && \\\n    apt-get install -y nodejs && \\\n    pip3 install tornado torndb && \\\n    pip3 install bokeh stockstats scikit-learn && \\\n    apt-get --purge remove -y gcc make python3-dev default-libmysqlclient-dev libxml2-dev && \\\n    rm -rf /root/.cache/* && apt-get clean && apt-get autoremove -y  && \\\n    cd /usr/local/lib/python3.11/site-packages && find ./ -name *.pyc | xargs rm -f  \n\n# /usr/local/lib/python3.11/site-packages/pandas/\n# 1.解决 pandas 数据插入问题。直接修改数据库驱动 sqlalchemy\n# 修改：statement.replace(\"INSERT INTO\",\"INSERT IGNORE INTO\")\n# /usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/mysqldb.py\n# 增加了一个 IGNORE 参数。\n# 2.解决torndb在python3下面的问题：\n# http://blog.csdn.net/littlethunder/article/details/8917378\n# 3. 解决 type 问题，使用sed 进行替换。\n#  File \"/usr/local/lib/python3.11/site-packages/torndb.py\", line 260, in <module>\n#    CONVERSIONS[field_type] = [(FLAG.BINARY, str)] + CONVERSIONS[field_type]\n#  TypeError: can only concatenate list (not \"type\") to list\n\nRUN echo `date +%Y-%m-%d:%H:%M:%S` >> /etc/docker.build && \\\n    sed -i -e 's/executemany(statement/executemany(statement.replace\\(\"INSERT INTO\",\"INSERT IGNORE INTO\")/g' \\\n        /usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/mysqldb.py && \\\n    rm -f /etc/cron.daily/apt-compat /etc/cron.daily/dpkg /etc/cron.daily/passwd && \\\n    sed -i -e 's/itertools\\.izip/zip/g' \\\n    /usr/local/lib/python3.11/site-packages/torndb.py  && \\\n    sed -i -e 's/\\+ CONVERSIONS\\[field_type\\]/\\+ \\[CONVERSIONS\\[field_type\\],bytes\\]/g' \\\n    /usr/local/lib/python3.11/site-packages/torndb.py\n\n\n#add cron sesrvice.\n#每分钟，每小时1分钟，每天1点1分，每月1号执行\nRUN mkdir -p /etc/cron.minutely && mkdir -p /etc/cron.hourly && mkdir -p /etc/cron.monthly && mkdir -p /var/spool/cron/crontabs && \\\n    echo \"SHELL=/bin/sh \\n\\\nPATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin \\n\\\n# min   hour    day     month   weekday command \\n\\\n*/1     *       *       *       *       /bin/run-parts /etc/cron.minutely \\n\\\n10       *       *       *       *       /bin/run-parts /etc/cron.hourly \\n\\\n30       16       *       *       *       /bin/run-parts /etc/cron.daily \\n\\\n30       17       1,10,20       *       *       /bin/run-parts /etc/cron.monthly \\n\" > /var/spool/cron/crontabs/root && \\\n    chmod 600 /var/spool/cron/crontabs/root\n\n\n#增加服务端口\nEXPOSE 8888 9090\n\nENTRYPOINT [\"supervisord\",\"-n\",\"-c\",\"/data/supervisor/supervisord.conf\"]\n\n"
  },
  {
    "path": "docker-compose/docker/DevFrontendDockerfile",
    "content": "#使用 node:bullseye-slim 做基础镜像减少大小。\n\n# FROM docker.m.daocloud.io/library/node:bullseye-slim\n# fixbug 最新node 版本编译不过去，\n#  ERROR  Error: Cannot find module './passes/web-incoming'\n\nFROM docker.m.daocloud.io/library/node:23.5.0-bullseye-slim\n\n# https://opsx.alibaba.com/mirror\n# 使用阿里云镜像地址。修改debian apt 更新地址，pip 地址，设置时区。\n# 设置debian的镜像源\nRUN echo \"deb http://mirrors.aliyun.com/debian/ bullseye main non-free contrib\" > /etc/apt/sources.list && \\\necho \"deb-src http://mirrors.aliyun.com/debian/ bullseye main non-free contrib\" >> /etc/apt/sources.list && \\\necho \"deb http://mirrors.aliyun.com/debian-security/ bullseye-security main\" >> /etc/apt/sources.list && \\\necho \"deb-src http://mirrors.aliyun.com/debian-security/ bullseye-security main\" >> /etc/apt/sources.list && \\\necho \"deb http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib\" >> /etc/apt/sources.list && \\\necho \"deb-src http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib\" >> /etc/apt/sources.list && \\\necho \"deb http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib\" >> /etc/apt/sources.list && \\\necho \"deb-src http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib\" >> /etc/apt/sources.list && \\\necho  \"[global]\\n\\\ntrusted-host=mirrors.aliyun.com\\n\\\nindex-url=http://mirrors.aliyun.com/pypi/simple\" > /etc/pip.conf && \\\nln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \\\n    echo \"Asia/Shanghai\" > /etc/timezone\n\n#增加语言utf-8\nENV LANG=zh_CN.UTF-8\nENV LC_CTYPE=zh_CN.UTF-8\nENV LC_ALL=C\n\n# 增加 TensorFlow 的支持，使用最新的2.0 编写代码。目前还是使用 1.x 吧，还没有学明白。\n# RUN pip3 install tensorflow==2.0.0-rc1 keras\n# RUN pip3 install tensorflow keras sklearn\n\n# 设置 vim 的语言配置\nRUN mkdir -p /etc/vim/ && \\\n    echo \"set fileencodings=utf-8,ucs-bom,gb18030,gbk,gb2312,cp936\" >> /etc/vim/vimrc && \\\n    echo \"set termencoding=utf-8\" >> /etc/vim/vimrc && \\\n    echo \"set encoding=utf-8\" >> /etc/vim/vimrc\n\n# 安装 mysqlclient akshare (pandas ,numpy) tornado bokeh\n# 安装 nodejs 库\n# apt-get autoremove -y 删除没有用的依赖lib。减少镜像大小。1MB 也要节省。\n# apt-get --purge remove 软件包名称 , 删除已安装包（不保留配置文件)。\nRUN apt-get update && apt-get install -y python3 make g++ && apt-get clean\n"
  },
  {
    "path": "docker-compose/docker/Dockerfile",
    "content": "#使用 python:3.8-slim 做基础镜像减少大小。其中tensorflow再用另外的镜像跑数据。\n\n# 之前使用的是python:3.6-slim\n# 可以更新 3.8-slim-bullseye slim-bullseye\n\n# https://hub.docker.com/_/python?tab=tags&page=1&name=3.8-slim-bullseye\n# 用这个做为基础镜像，防止每次都进行构建。\n\n#FROM docker.io/python:3.8-slim-bullseye\n#FROM hub.atomgit.com/amd64/python:3.8-slim-bullseye\n# https://hub.atomgit.com/repos/amd64/python\n#FROM hub.atomgit.com/amd64/python:3.11-slim-bullseye\nFROM docker.m.daocloud.io/library/python:3.11-slim-bullseye\n\n# https://opsx.alibaba.com/mirror\n# 使用阿里云镜像地址。修改debian apt 更新地址，pip 地址，设置时区。\n# 设置debian的镜像源\nRUN echo \"deb http://mirrors.aliyun.com/debian/ bullseye main non-free contrib\" > /etc/apt/sources.list && \\\necho \"deb-src http://mirrors.aliyun.com/debian/ bullseye main non-free contrib\" >> /etc/apt/sources.list && \\\necho \"deb http://mirrors.aliyun.com/debian-security/ bullseye-security main\" >> /etc/apt/sources.list && \\\necho \"deb-src http://mirrors.aliyun.com/debian-security/ bullseye-security main\" >> /etc/apt/sources.list && \\\necho \"deb http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib\" >> /etc/apt/sources.list && \\\necho \"deb-src http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib\" >> /etc/apt/sources.list && \\\necho \"deb http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib\" >> /etc/apt/sources.list && \\\necho \"deb-src http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib\" >> /etc/apt/sources.list && \\\necho  \"[global]\\n\\\ntrusted-host=mirrors.aliyun.com\\n\\\nindex-url=http://mirrors.aliyun.com/pypi/simple\" > /etc/pip.conf && \\\nln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \\\n    echo \"Asia/Shanghai\" > /etc/timezone\n\n#增加语言utf-8\nENV LANG=zh_CN.UTF-8\nENV LC_CTYPE=zh_CN.UTF-8\nENV LC_ALL=C\nENV PYTHONPATH=/data/stock\n\n# 增加 TensorFlow 的支持，使用最新的2.0 编写代码。目前还是使用 1.x 吧，还没有学明白。\n# RUN pip3 install tensorflow==2.0.0-rc1 keras\n# RUN pip3 install tensorflow keras sklearn\n\n# 设置 vim 的语言配置\nRUN mkdir -p /etc/vim/ && \\\n    echo \"set fileencodings=utf-8,ucs-bom,gb18030,gbk,gb2312,cp936\" >> /etc/vim/vimrc && \\\n    echo \"set termencoding=utf-8\" >> /etc/vim/vimrc && \\\n    echo \"set encoding=utf-8\" >> /etc/vim/vimrc\n\n# 安装 mysqlclient akshare (pandas ,numpy) tornado bokeh\n# 安装 nodejs 库\n# apt-get autoremove -y 删除没有用的依赖lib。减少镜像大小。1MB 也要节省。\n# apt-get --purge remove 软件包名称 , 删除已安装包（不保留配置文件)。\nRUN apt-get update && apt-get install -y git pkg-config net-tools procps gcc make python3-dev default-libmysqlclient-dev libxml2-dev cron  && \\\n    pip3 install mysqlclient sqlalchemy && \\\n    pip3 install supervisor && \\\n    pip3 install numpy pandas  && \\\n    pip3 install akshare --upgrade && \\\n    apt-get install -y nodejs && \\\n    pip3 install tornado torndb && \\\n    pip3 install bokeh stockstats scikit-learn && \\\n    apt-get --purge remove -y gcc make python3-dev default-libmysqlclient-dev libxml2-dev && \\\n    rm -rf /root/.cache/* && apt-get clean && apt-get autoremove -y  && \\\n    cd /usr/local/lib/python3.11/site-packages && find ./ -name *.pyc | xargs rm -f  \n\n# /usr/local/lib/python3.11/site-packages/pandas/\n# 1.解决 pandas 数据插入问题。直接修改数据库驱动 sqlalchemy\n# 修改：statement.replace(\"INSERT INTO\",\"INSERT IGNORE INTO\")\n# /usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/mysqldb.py\n# 增加了一个 IGNORE 参数。\n# 2.解决torndb在python3下面的问题：\n# http://blog.csdn.net/littlethunder/article/details/8917378\n# 3. 解决 type 问题，使用sed 进行替换。\n#  File \"/usr/local/lib/python3.11/site-packages/torndb.py\", line 260, in <module>\n#    CONVERSIONS[field_type] = [(FLAG.BINARY, str)] + CONVERSIONS[field_type]\n#  TypeError: can only concatenate list (not \"type\") to list\n\nRUN echo `date +%Y-%m-%d:%H:%M:%S` >> /etc/docker.build && \\\n    sed -i -e 's/executemany(statement/executemany(statement.replace\\(\"INSERT INTO\",\"INSERT IGNORE INTO\")/g' \\\n        /usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/mysqldb.py && \\\n    rm -f /etc/cron.daily/apt-compat /etc/cron.daily/dpkg /etc/cron.daily/passwd && \\\n    sed -i -e 's/itertools\\.izip/zip/g' \\\n    /usr/local/lib/python3.11/site-packages/torndb.py  && \\\n    sed -i -e 's/\\+ CONVERSIONS\\[field_type\\]/\\+ \\[CONVERSIONS\\[field_type\\],bytes\\]/g' \\\n    /usr/local/lib/python3.11/site-packages/torndb.py\n\n\n#add cron sesrvice.\n#每分钟，每小时1分钟，每天1点1分，每月1号执行\nRUN mkdir -p /etc/cron.minutely && mkdir -p /etc/cron.hourly && mkdir -p /etc/cron.monthly && mkdir -p /var/spool/cron/crontabs && \\\n    echo \"SHELL=/bin/sh \\n\\\nPATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin \\n\\\n# min   hour    day     month   weekday command \\n\\\n*/1     *       *       *       *       /bin/run-parts /etc/cron.minutely \\n\\\n10       *       *       *       *       /bin/run-parts /etc/cron.hourly \\n\\\n30       16       *       *       *       /bin/run-parts /etc/cron.daily \\n\\\n30       17       1,10,20       *       *       /bin/run-parts /etc/cron.monthly \\n\" > /var/spool/cron/crontabs/root && \\\n    chmod 600 /var/spool/cron/crontabs/root\n\n\n#增加服务端口就两个 6006 8500 9001\nEXPOSE 8888 9999\n\nENTRYPOINT [\"supervisord\",\"-n\",\"-c\",\"/data/supervisor/supervisord.conf\"]\n\n"
  },
  {
    "path": "docker-compose/docker/ProdBackendDockerfile",
    "content": "#使用 python:3.8-slim 做基础镜像减少大小。其中tensorflow再用另外的镜像跑数据。\n\n# 之前使用的是python:3.6-slim\n# 可以更新 3.8-slim-bullseye slim-bullseye\n\n# https://hub.docker.com/_/python?tab=tags&page=1&name=3.8-slim-bullseye\n# 用这个做为基础镜像，防止每次都进行构建。\n\n#FROM docker.io/python:3.8-slim-bullseye\n#FROM hub.atomgit.com/amd64/python:3.8-slim-bullseye\n# https://hub.atomgit.com/repos/amd64/python\n#FROM hub.atomgit.com/amd64/python:3.11-slim-bullseye\nFROM docker.m.daocloud.io/library/python:3.11-slim-bullseye\n\n# https://opsx.alibaba.com/mirror\n# 使用阿里云镜像地址。修改debian apt 更新地址，pip 地址，设置时区。\n# 设置debian的镜像源\nRUN echo \"deb http://mirrors.aliyun.com/debian/ bullseye main non-free contrib\" > /etc/apt/sources.list && \\\necho \"deb-src http://mirrors.aliyun.com/debian/ bullseye main non-free contrib\" >> /etc/apt/sources.list && \\\necho \"deb http://mirrors.aliyun.com/debian-security/ bullseye-security main\" >> /etc/apt/sources.list && \\\necho \"deb-src http://mirrors.aliyun.com/debian-security/ bullseye-security main\" >> /etc/apt/sources.list && \\\necho \"deb http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib\" >> /etc/apt/sources.list && \\\necho \"deb-src http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib\" >> /etc/apt/sources.list && \\\necho \"deb http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib\" >> /etc/apt/sources.list && \\\necho \"deb-src http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib\" >> /etc/apt/sources.list && \\\necho  \"[global]\\n\\\ntrusted-host=mirrors.aliyun.com\\n\\\nindex-url=http://mirrors.aliyun.com/pypi/simple\" > /etc/pip.conf && \\\nln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \\\n    echo \"Asia/Shanghai\" > /etc/timezone\n\n#增加语言utf-8\nENV LANG=zh_CN.UTF-8\nENV LC_CTYPE=zh_CN.UTF-8\nENV LC_ALL=C\nENV PYTHONPATH=/data/stock\n\n# 增加 TensorFlow 的支持，使用最新的2.0 编写代码。目前还是使用 1.x 吧，还没有学明白。\n# RUN pip3 install tensorflow==2.0.0-rc1 keras\n# RUN pip3 install tensorflow keras sklearn\n\n# 设置 vim 的语言配置\nRUN mkdir -p /etc/vim/ && \\\n    echo \"set fileencodings=utf-8,ucs-bom,gb18030,gbk,gb2312,cp936\" >> /etc/vim/vimrc && \\\n    echo \"set termencoding=utf-8\" >> /etc/vim/vimrc && \\\n    echo \"set encoding=utf-8\" >> /etc/vim/vimrc\n\n# 安装 mysqlclient akshare (pandas ,numpy) tornado bokeh\n# 安装 nodejs 库\n# apt-get autoremove -y 删除没有用的依赖lib。减少镜像大小。1MB 也要节省。\n# apt-get --purge remove 软件包名称 , 删除已安装包（不保留配置文件)。\nRUN apt-get update && apt-get install -y git pkg-config net-tools procps gcc make python3-dev default-libmysqlclient-dev libxml2-dev cron  && \\\n    pip3 install mysqlclient sqlalchemy && \\\n    pip3 install supervisor && \\\n    pip3 install numpy pandas  && \\\n    pip3 install akshare --upgrade && \\\n    apt-get install -y nodejs && \\\n    pip3 install tornado torndb && \\\n    pip3 install bokeh stockstats scikit-learn && \\\n    apt-get --purge remove -y gcc make python3-dev default-libmysqlclient-dev libxml2-dev && \\\n    rm -rf /root/.cache/* && apt-get clean && apt-get autoremove -y  && \\\n    cd /usr/local/lib/python3.11/site-packages && find ./ -name *.pyc | xargs rm -f  \n\n# /usr/local/lib/python3.11/site-packages/pandas/\n# 1.解决 pandas 数据插入问题。直接修改数据库驱动 sqlalchemy\n# 修改：statement.replace(\"INSERT INTO\",\"INSERT IGNORE INTO\")\n# /usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/mysqldb.py\n# 增加了一个 IGNORE 参数。\n# 2.解决torndb在python3下面的问题：\n# http://blog.csdn.net/littlethunder/article/details/8917378\n# 3. 解决 type 问题，使用sed 进行替换。\n#  File \"/usr/local/lib/python3.11/site-packages/torndb.py\", line 260, in <module>\n#    CONVERSIONS[field_type] = [(FLAG.BINARY, str)] + CONVERSIONS[field_type]\n#  TypeError: can only concatenate list (not \"type\") to list\n\nRUN echo `date +%Y-%m-%d:%H:%M:%S` >> /etc/docker.build && \\\n    sed -i -e 's/executemany(statement/executemany(statement.replace\\(\"INSERT INTO\",\"INSERT IGNORE INTO\")/g' \\\n        /usr/local/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/mysqldb.py && \\\n    rm -f /etc/cron.daily/apt-compat /etc/cron.daily/dpkg /etc/cron.daily/passwd && \\\n    sed -i -e 's/itertools\\.izip/zip/g' \\\n    /usr/local/lib/python3.11/site-packages/torndb.py  && \\\n    sed -i -e 's/\\+ CONVERSIONS\\[field_type\\]/\\+ \\[CONVERSIONS\\[field_type\\],bytes\\]/g' \\\n    /usr/local/lib/python3.11/site-packages/torndb.py\n\n\n#add cron sesrvice.\n#每分钟，每小时1分钟，每天1点1分，每月1号执行\nRUN mkdir -p /etc/cron.minutely && mkdir -p /etc/cron.hourly && mkdir -p /etc/cron.monthly && mkdir -p /var/spool/cron/crontabs && \\\n    echo \"SHELL=/bin/sh \\n\\\nPATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin \\n\\\n# min   hour    day     month   weekday command \\n\\\n*/1     *       *       *       *       /bin/run-parts /etc/cron.minutely \\n\\\n10       *       *       *       *       /bin/run-parts /etc/cron.hourly \\n\\\n30       16       *       *       *       /bin/run-parts /etc/cron.daily \\n\\\n30       17       1,10,20       *       *       /bin/run-parts /etc/cron.monthly \\n\" > /var/spool/cron/crontabs/root && \\\n    chmod 600 /var/spool/cron/crontabs/root\n\n\n#增加服务端口就两个 6006 8500 9001\nEXPOSE 8888 9999\n\nENTRYPOINT [\"supervisord\",\"-n\",\"-c\",\"/data/supervisor/supervisord.conf\"]\n\n"
  },
  {
    "path": "docker-compose/docker/ProdFrontendDockerfile",
    "content": "#使用 node:bullseye-slim 做基础镜像减少大小。\n\nFROM docker.m.daocloud.io/library/node:bullseye-slim\n\n# https://opsx.alibaba.com/mirror\n# 使用阿里云镜像地址。修改debian apt 更新地址，pip 地址，设置时区。\n# 设置debian的镜像源\nRUN echo \"deb http://mirrors.aliyun.com/debian/ bullseye main non-free contrib\" > /etc/apt/sources.list && \\\necho \"deb-src http://mirrors.aliyun.com/debian/ bullseye main non-free contrib\" >> /etc/apt/sources.list && \\\necho \"deb http://mirrors.aliyun.com/debian-security/ bullseye-security main\" >> /etc/apt/sources.list && \\\necho \"deb-src http://mirrors.aliyun.com/debian-security/ bullseye-security main\" >> /etc/apt/sources.list && \\\necho \"deb http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib\" >> /etc/apt/sources.list && \\\necho \"deb-src http://mirrors.aliyun.com/debian/ bullseye-updates main non-free contrib\" >> /etc/apt/sources.list && \\\necho \"deb http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib\" >> /etc/apt/sources.list && \\\necho \"deb-src http://mirrors.aliyun.com/debian/ bullseye-backports main non-free contrib\" >> /etc/apt/sources.list && \\\necho  \"[global]\\n\\\ntrusted-host=mirrors.aliyun.com\\n\\\nindex-url=http://mirrors.aliyun.com/pypi/simple\" > /etc/pip.conf && \\\nln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \\\n    echo \"Asia/Shanghai\" > /etc/timezone\n\n#增加语言utf-8\nENV LANG=zh_CN.UTF-8\nENV LC_CTYPE=zh_CN.UTF-8\nENV LC_ALL=C\n\n# 增加 TensorFlow 的支持，使用最新的2.0 编写代码。目前还是使用 1.x 吧，还没有学明白。\n# RUN pip3 install tensorflow==2.0.0-rc1 keras\n# RUN pip3 install tensorflow keras sklearn\n\n# 设置 vim 的语言配置\nRUN mkdir -p /etc/vim/ && \\\n    echo \"set fileencodings=utf-8,ucs-bom,gb18030,gbk,gb2312,cp936\" >> /etc/vim/vimrc && \\\n    echo \"set termencoding=utf-8\" >> /etc/vim/vimrc && \\\n    echo \"set encoding=utf-8\" >> /etc/vim/vimrc\n\n# 安装 mysqlclient akshare (pandas ,numpy) tornado bokeh\n# 安装 nodejs 库\n# apt-get autoremove -y 删除没有用的依赖lib。减少镜像大小。1MB 也要节省。\n# apt-get --purge remove 软件包名称 , 删除已安装包（不保留配置文件)。\nRUN apt-get update && apt-get install -y python3 make g++ && apt-get clean\n"
  },
  {
    "path": "docker-compose/docker/README.md",
    "content": "\n# python 基础镜像\n\n基础镜像升级到 2020年7月的版本\n\n保证运行的最少基础环境，基础环境使用python3.6的版本。安装了超级多的lib库。非常的好用。\n\nmysqlclient\nsqlalchemy\nrequests\nnumpy\ntushare\ntornado torndb\nbokeh\nstockstats\nta-lib\njupyter\nsklearn\n\n# 2021年6月版本，使用 akshare 替换掉 tushare 库\n\nakshare 地址：\n\nhttps://www.akshare.xyz/zh_CN/latest/introduction.html\n\nAKShare 是基于 Python 的财经数据接口库, 目的是实现对股票、期货、期权、基金、外汇、债券、指数、\n加密货币等金融产品的基本面数据、实时和历史行情数据、衍生数据从数据采集、数据清洗到数据落地的一套工具, \n主要用于学术研究目的。\n\n\n# 2021年 9 月版本，镜像裁剪，supervisor 使用python3\n\nsupervisor 使用 python3 后好像减少了不少大小。\n同时删除掉一直没有用的 ta-lib 和 jupyter 。升级python基础镜像。\n\n"
  },
  {
    "path": "docker-compose/docker/build.sh",
    "content": "#!/bin/sh\n\n\nNOW_MONTH=$(date \"+%Y-%m\")\n\nDOCKER_TAG=pythonstock/pythonstock:base-${NOW_MONTH}\n\necho \" docker build -f Dockerfile -t ${DOCKER_TAG} .\"\ndocker build -f Dockerfile -t ${DOCKER_TAG} .\necho \"#################################################################\"\necho \" docker push ${DOCKER_TAG} \"\n\n\n"
  },
  {
    "path": "docker-compose/docker-compose.yml",
    "content": "networks:\n  stock-dev-network:\n    driver: bridge\n\nversion: \"3\"\nservices:\n\n#### 使用node 镜像进行编译，构建成 js 文件给nginx 使用。\n    frontend-build:\n        image: pythonstock/frontend-dev:latest\n        build:\n            context: .\n            dockerfile: docker/DevFrontendDockerfile\n        container_name: frontend-build\n        volumes:\n        # 设置开发目录，方便开发调试，生产环境拷贝到 /data/html\n            - \"../frontend:/usr/src/app\"\n            - \"./data/html:/data/html\"\n        environment:\n            LANG: zh_CN.UTF-8\n            LC_CTYPE: zh_CN.UTF-8\n        networks:\n            stock-dev-network: {}\n        # 入口写死，手动启动应用。每次构建时候使用，构建完成结束\n        entrypoint: /usr/src/app/docker-build.sh\n    nginx:\n        image: docker.m.daocloud.io/library/nginx:stable-alpine\n        container_name: nginx\n        ports:\n            - \"8080:8080\"\n        volumes:\n        # 设置开发目录，方便开发调试\n            - \"./data/html:/usr/share/nginx/html\"\n            - \"./nginx/nginx.conf:/etc/nginx/conf.d/default.conf\"\n        environment:\n            LANG: zh_CN.UTF-8\n            LC_CTYPE: zh_CN.UTF-8\n        restart: always\n        networks:\n            stock-dev-network: {}\n        depends_on:\n            - backend\n    backend:\n        image: pythonstock/backend-dev:latest\n        build:\n            context: .\n            dockerfile: docker/DevBackendDockerfile\n        container_name: backend\n        ports:\n            - \"8888:8888\"\n            - \"9090:9090\"\n        volumes:\n        # 设置开发目录，方便开发调试\n            - \"../backend/jobs/crontab:/var/spool/cron/crontabs/root\"\n            - \"../backend/jobs/cron.minutely:/etc/cron.minutely\"\n            - \"../backend/jobs/cron.hourly:/etc/cron.hourly\"\n            - \"../backend/jobs/cron.daily:/etc/cron.daily\"\n            - \"../backend/jobs/cron.monthly:/etc/cron.monthly\"\n            - \"../backend:/data/stock\"\n            - \"../backend/supervisor:/data/supervisor\"\n            - \"./data/notebooks:/data/notebooks\"\n            - \"./data/logs:/data/logs\"\n        environment:\n            MYSQL_HOST: mysql-stock\n            MYSQL_USER: root\n            MYSQL_PWD: mysql-stock\n            MYSQL_DB: stock_data\n            MYSQL_PORT: 3306\n            LANG: zh_CN.UTF-8\n            LC_CTYPE: zh_CN.UTF-8\n            PYTHONIOENCODING: utf-8\n        restart: always\n        networks:\n            stock-dev-network: {}\n        # 入口写死，手动启动应用。\n        #entrypoint: sleep 999999d\n        depends_on:\n            - mysql-stock\n    mysql-stock:\n        # image: hub.atomgit.com/library/mysql:5.7\n        # https://hub.atomgit.com/repos/amd64/mysql\n        image: docker.m.daocloud.io/library/mysql:8\n        container_name: mysql-stock\n        # 执行命令：https://juejin.cn/s/mysql%20healthcheck%20docker-compose\n        healthcheck:\n            test: [\"CMD\", \"mysqladmin\" ,\"ping\", \"-uroot\", \"-pmysql-stock\"]\n            interval: 10s\n            timeout: 5s\n            retries: 5\n        ports:\n            - \"3306:3306\"\n        networks:\n            stock-dev-network: {}\n        volumes:\n            - \"./mysql/my.cnf:/etc/mysql/my.cnf\"\n            - \"./data/mysql-stock/data:/var/lib/mysql\"\n            - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql\n        environment:\n            MYSQL_ROOT_PASSWORD: mysql-stock\n            MYSQL_DATABASE: stock_data\n            TZ: Asia/Shanghai\n        command: [\n            '--character-set-server=utf8mb4',\n            '--collation-server=utf8mb4_general_ci',\n            '--max_connections=3000'\n        ]\n        restart: always\n"
  },
  {
    "path": "docker-compose/mysql/init.sql",
    "content": "SET character_set_client = utf8;\n-- 切库：\nuse stock_data;\n-- 建表\n-- 表里面都是使用 code 和 date 两个字段做联合主键的。\n\n\nCREATE TABLE IF NOT EXISTS `stock_zh_a_spot_em` (\n  `date` varchar(255) NOT NULL,\n  `code` varchar(255) NOT NULL,\n  `name` varchar(255) DEFAULT NULL,\n  `last_price` double(20,2) DEFAULT NULL,\n  `change_percent` double(20,2) DEFAULT NULL,\n  `change_amount` double(20,2) DEFAULT NULL,\n  `volume` double(20,2) DEFAULT NULL,\n  `turnover` double(20,2) DEFAULT NULL,\n  `amplitude` double(20,2) DEFAULT NULL,\n  `high` double(20,2) DEFAULT NULL,\n  `low` double(20,2) DEFAULT NULL,\n  `open` double(20,2) DEFAULT NULL,\n  `closed` double(20,2) DEFAULT NULL,\n  `volume_ratio` double(20,2) DEFAULT NULL,\n  `turnover_rate` double(20,2) DEFAULT NULL,\n  `pe_ratio` double(20,2) DEFAULT NULL,\n  `pb_ratio` double(20,2) DEFAULT NULL,\n  `market_cap` double(20,2) DEFAULT NULL,\n  `circulating_market_cap` double(20,2) DEFAULT NULL,\n  `rise_speed` double(20,2) DEFAULT NULL,\n  `change_5min` double(20,2) DEFAULT NULL,\n  `change_ercent_60day` double(20,2) DEFAULT NULL,\n  `ytd_change_percent` double(20,2) DEFAULT NULL,\n  PRIMARY KEY (`code`,`date`),\n   KEY `idx_code` (`code`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;\n\nCREATE TABLE IF NOT EXISTS `stock_lhb_ggtj_sina` (\n  `date` varchar(255) NOT NULL,\n  `code` varchar(255) NOT NULL,\n  `name` varchar(255) DEFAULT NULL,\n  `ranking_times` double(20,2) DEFAULT NULL,\n  `sum_buy` double(20,2) DEFAULT NULL,\n  `sum_sell` double(20,2) DEFAULT NULL,\n  `net_amount` double(20,2) DEFAULT NULL,\n  `buy_seat` double(20,2) DEFAULT NULL,\n  `sell_seat` double(20,2) DEFAULT NULL,\n  PRIMARY KEY (`code`,`date`),\n   KEY `idx_code` (`code`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;\n\n\nCREATE TABLE `stock_dzjy_mrtj` (\n  `date` varchar(255) NOT NULL,\n  `code` varchar(255) NOT NULL,\n  `name` varchar(255) DEFAULT NULL,\n  `quote_change` double(20,2) DEFAULT NULL,\n  `close_price` double(20,2) DEFAULT NULL,\n  `average_price` double(20,2) DEFAULT NULL,\n  `overflow_rate` double(20,2) DEFAULT NULL,\n  `trade_number` double(20,2) DEFAULT NULL,\n  `sum_volume` double(20,2) DEFAULT NULL,\n  `sum_turnover` double(20,2) DEFAULT NULL,\n  `turnover_market_rate` double(20,2) DEFAULT NULL,\n  PRIMARY KEY (`code`,`date`),\n   KEY `idx_code` (`code`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;\n\n\nCREATE TABLE `guess_indicators_daily` (\n  `date` varchar(255) NOT NULL,\n  `code` varchar(255) NOT NULL,\n  `name` varchar(255) DEFAULT NULL,\n  `last_price` double(20,2) DEFAULT NULL,\n  `change_percent` double(20,2) DEFAULT NULL,\n  `change_amount` double(20,2) DEFAULT NULL,\n  `volume` double(20,2) DEFAULT NULL,\n  `turnover` double(20,2) DEFAULT NULL,\n  `amplitude` double(20,2) DEFAULT NULL,\n  `high` double(20,2) DEFAULT NULL,\n  `low` double(20,2) DEFAULT NULL,\n  `open` double(20,2) DEFAULT NULL,\n  `closed` double(20,2) DEFAULT NULL,\n  `volume_ratio` double(20,2) DEFAULT NULL,\n  `turnover_rate` double(20,2) DEFAULT NULL,\n  `pe_ratio` double(20,2) DEFAULT NULL,\n  `pb_ratio` double(20,2) DEFAULT NULL,\n  `market_cap` double(20,2) DEFAULT NULL,\n  `circulating_market_cap` double(20,2) DEFAULT NULL,\n  `rise_speed` double(20,2) DEFAULT NULL,\n  `change_5min` double(20,2) DEFAULT NULL,\n  `change_ercent_60day` double(20,2) DEFAULT NULL,\n  `ytd_change_percent` double(20,2) DEFAULT NULL,\n  `boll` double(20,2) DEFAULT NULL,\n  `boll_lb` double(20,2) DEFAULT NULL,\n  `boll_ub` double(20,2) DEFAULT NULL,\n  `kdjd` double(20,2) DEFAULT NULL,\n  `kdjj` double(20,2) DEFAULT NULL,\n  `kdjk` double(20,2) DEFAULT NULL,\n  `macd` double(20,2) DEFAULT NULL,\n  `macdh` double(20,2) DEFAULT NULL,\n  `macds` double(20,2) DEFAULT NULL,\n  `pdi` double(20,2) DEFAULT NULL,\n  `trix` double(20,2) DEFAULT NULL,\n  `trix_9_sma` double(20,2) DEFAULT NULL,\n  `vr` double(20,2) DEFAULT NULL,\n  `vr_6_sma` double(20,2) DEFAULT NULL,\n  `wr_10` double(20,2) DEFAULT NULL,\n  `wr_6` double(20,2) DEFAULT NULL,\n  PRIMARY KEY (`code`,`date`),\n  KEY `idx_code` (`code`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;\n\n\nCREATE TABLE `guess_indicators_lite_buy_daily` (\n  `date` varchar(255) NOT NULL,\n  `code` varchar(255) NOT NULL,\n  `name` varchar(255) DEFAULT NULL,\n  `last_price` double(20,2) DEFAULT NULL,\n  `change_percent` double(20,2) DEFAULT NULL,\n  `change_amount` double(20,2) DEFAULT NULL,\n  `volume` double(20,2) DEFAULT NULL,\n  `turnover` double(20,2) DEFAULT NULL,\n  `amplitude` double(20,2) DEFAULT NULL,\n  `high` double(20,2) DEFAULT NULL,\n  `low` double(20,2) DEFAULT NULL,\n  `open` double(20,2) DEFAULT NULL,\n  `closed` double(20,2) DEFAULT NULL,\n  `volume_ratio` double(20,2) DEFAULT NULL,\n  `turnover_rate` double(20,2) DEFAULT NULL,\n  `pe_ratio` double(20,2) DEFAULT NULL,\n  `pb_ratio` double(20,2) DEFAULT NULL,\n  `market_cap` double(20,2) DEFAULT NULL,\n  `circulating_market_cap` double(20,2) DEFAULT NULL,\n  `rise_speed` double(20,2) DEFAULT NULL,\n  `change_5min` double(20,2) DEFAULT NULL,\n  `change_ercent_60day` double(20,2) DEFAULT NULL,\n  `ytd_change_percent` double(20,2) DEFAULT NULL,\n  `boll` double(20,2) DEFAULT NULL,\n  `boll_lb` double(20,2) DEFAULT NULL,\n  `boll_ub` double(20,2) DEFAULT NULL,\n  `kdjd` double(20,2) DEFAULT NULL,\n  `kdjj` double(20,2) DEFAULT NULL,\n  `kdjk` double(20,2) DEFAULT NULL,\n  `macd` double(20,2) DEFAULT NULL,\n  `macdh` double(20,2) DEFAULT NULL,\n  `macds` double(20,2) DEFAULT NULL,\n  `pdi` double(20,2) DEFAULT NULL,\n  `trix` double(20,2) DEFAULT NULL,\n  `trix_9_sma` double(20,2) DEFAULT NULL,\n  `vr` double(20,2) DEFAULT NULL,\n  `vr_6_sma` double(20,2) DEFAULT NULL,\n  `wr_10` double(20,2) DEFAULT NULL,\n  `wr_6` double(20,2) DEFAULT NULL,\n  PRIMARY KEY (`code`,`date`),\n  KEY `idx_code` (`code`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;\n\n\nCREATE TABLE `guess_indicators_lite_sell_daily` (\n  `date` varchar(255) NOT NULL,\n  `code` varchar(255) NOT NULL,\n  `name` varchar(255) DEFAULT NULL,\n  `last_price` double(20,2) DEFAULT NULL,\n  `change_percent` double(20,2) DEFAULT NULL,\n  `change_amount` double(20,2) DEFAULT NULL,\n  `volume` double(20,2) DEFAULT NULL,\n  `turnover` double(20,2) DEFAULT NULL,\n  `amplitude` double(20,2) DEFAULT NULL,\n  `high` double(20,2) DEFAULT NULL,\n  `low` double(20,2) DEFAULT NULL,\n  `open` double(20,2) DEFAULT NULL,\n  `closed` double(20,2) DEFAULT NULL,\n  `volume_ratio` double(20,2) DEFAULT NULL,\n  `turnover_rate` double(20,2) DEFAULT NULL,\n  `pe_ratio` double(20,2) DEFAULT NULL,\n  `pb_ratio` double(20,2) DEFAULT NULL,\n  `market_cap` double(20,2) DEFAULT NULL,\n  `circulating_market_cap` double(20,2) DEFAULT NULL,\n  `rise_speed` double(20,2) DEFAULT NULL,\n  `change_5min` double(20,2) DEFAULT NULL,\n  `change_ercent_60day` double(20,2) DEFAULT NULL,\n  `ytd_change_percent` double(20,2) DEFAULT NULL,\n  `boll` double(20,2) DEFAULT NULL,\n  `boll_lb` double(20,2) DEFAULT NULL,\n  `boll_ub` double(20,2) DEFAULT NULL,\n  `kdjd` double(20,2) DEFAULT NULL,\n  `kdjj` double(20,2) DEFAULT NULL,\n  `kdjk` double(20,2) DEFAULT NULL,\n  `macd` double(20,2) DEFAULT NULL,\n  `macdh` double(20,2) DEFAULT NULL,\n  `macds` double(20,2) DEFAULT NULL,\n  `pdi` double(20,2) DEFAULT NULL,\n  `trix` double(20,2) DEFAULT NULL,\n  `trix_9_sma` double(20,2) DEFAULT NULL,\n  `vr` double(20,2) DEFAULT NULL,\n  `vr_6_sma` double(20,2) DEFAULT NULL,\n  `wr_10` double(20,2) DEFAULT NULL,\n  `wr_6` double(20,2) DEFAULT NULL,\n  PRIMARY KEY (`code`,`date`),\n  KEY `idx_code` (`code`)\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;\n\n"
  },
  {
    "path": "docker-compose/mysql/my.cnf",
    "content": "# https://blog.csdn.net/aichogn/article/details/117788275\n# mysql 推荐配置\n[client]\nsocket=/var/lib/mysql/mysql.sock\nport=3306\n[mysqld]\n \nbasedir=/var/lib/mysql/\ndatadir=/var/lib/mysql/data\nsocket=/var/lib/mysql/mysql.sock\ndefault-storage-engine=INNODB\ncharacter_set_server=utf8mb4\ncollation_server=utf8mb4_general_ci \n \nport=3306\n# Disabling symbolic-links is recommended to prevent assorted security risks\nsymbolic-links=0\n\nserver_id=1\n\n ## 最大连接数,MySQL服务器允许的最大连接数16384,连接数越多消耗内存越多\nmax_connections = 1000\n## 日志过期时间,包括二进制日志(过期自动删除)\n# expire_logs_days = 15\n## Enable Per Table Data for InnoDB to shrink ibdata1(innoDB表优化)\ninnodb_file_per_table = 1\n#默认128M,用于存储页面缓存数据外，另外正常情况下还有大约8%的开销，主要用在每个缓存页帧的描述、adaptive hash等数据结构,适当的增加这个参数的大小，可以有效的减少 InnoDB 类型的表的磁盘 I/O \ninnodb_buffer_pool_size = 2048M\ninnodb_log_file_size = 512M\n#默认是8MB,InnoDB在写事务日志的时候，为了提高性能，也是先将信息写入Innofb Log Buffer中，当满足innodb_flush_log_trx_commit参数所设置的相应条件(或者日志缓冲区写满)之后，才会将日志写到文件 (或者同步到磁盘)中\ninnodb_log_buffer_size = 8M\ninnodb_flush_log_at_trx_commit = 2\n#表大小写不敏感\nlower_case_table_names=1\n#跳过密码  安装完后屏蔽该选项\n#skip-grant-tables\n#关闭 binlog\nskip-log-bin"
  },
  {
    "path": "docker-compose/nginx/nginx.conf",
    "content": "server {\n    listen       8080;\n    server_name  localhost;\n    #access_log  /var/log/nginx/host.access.log  main;\n\n    location / {\n        root   /usr/share/nginx/html;\n        index  index.html index.htm;\n    }\n\n    location /api/v1 {\n        proxy_pass http://backend:9090;\n    }\n\n\n    error_page  404              /404.html;\n    error_page   500 502 503 504  /50x.html;\n    location = /50x.html {\n        root   /usr/share/nginx/html;\n    }\n\n}\n"
  },
  {
    "path": "docker-compose/nginx.conf",
    "content": "# 设置nginx启动\n# systemctl enable nginx\nserver {\n    listen       8080;\n    server_name  www.pythonstock.com;\n    root         /usr/share/nginx/html;\n\n    error_page 404 /404.html;\n    error_page 500 502 503 504 /50x.html;\n\n    # 开启gzip\n    gzip on;\n    # 启用gzip压缩的最小文件；小于设置值的文件将不会被压缩\n    gzip_min_length 1k;\n    # gzip 压缩级别 1-10\n    gzip_comp_level 2;\n    # 进行压缩的文件类型。\n    gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;\n    # 是否在http header中添加Vary: Accept-Encoding，建议开启\n    gzip_vary on;\n\n    access_log /var/log/nginx/stock.access.log main;\n    error_log /var/log/nginx/stock.error.log warn;\n\n    # https://medium.com/aviabird/413-414-request-url-entity-too-large-error-nginx-b6dcece6f5dd\n    # 解决GET 参数过长问题。\n    client_max_body_size 10M;\n    large_client_header_buffers 4 20k;\n\n\n    location ^~ /static/ {\n        access_log off;\n        # 需要把源代码,再下载一遍。\n        root /data/pythonstock/stock/web;\n        expires 30d; # 设置30天超时\n        # 参考 https://www.cnblogs.com/kevingrace/p/10459429.html\n        # add_header    Cache-Control  max-age=360000;\n    }\n\n    location / {\n        proxy_next_upstream     http_500 http_502 http_503 http_504 error timeout invalid_header;\n        proxy_set_header        Host  $host;\n        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_pass              http://127.0.0.1:9090;\n        expires                 0;\n    }\n}"
  },
  {
    "path": "frontend/.eslintignore",
    "content": "build/*.js\nsrc/assets\npublic\ndist\neslint-disable "
  },
  {
    "path": "frontend/.gitignore",
    "content": ".DS_Store\nnode_modules/\ndist/\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npackage-lock.json\ntests/**/coverage/\n\n# Editor directories and files\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln"
  },
  {
    "path": "frontend/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2017-present PanJiaChen\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "frontend/README.md",
    "content": "### 说明，项目迁移到了Gitee 啦，最后一次修改，2023-06-02 执行存档\n\n项目迁移到这里了：此项目后续更新访问这里：\n\nhttps://gitee.com/pythonstock/stock-ui\n\ngithub项目后续就Archives存档了，不再更新了！\n\ncsdn的pythonstock专栏地址，相关资料都在这里有说明：\n\nhttps://blog.csdn.net/freewebsys/category_9285317.html\n\n\n## 1，股票系统前端项目\n\nelementUI\n\nhttps://element.eleme.cn/#/zh-CN\n\n使用vue-element-admin的模板进行项目开发：\n\nhttps://panjiachen.github.io/vue-element-admin-site/zh/guide/\n\n在线预览地址：\n\nhttps://panjiachen.github.io/vue-element-admin/#/dashboard\n\n模板源自：\nhttps://gitee.com/panjiachen/vue-admin-template\n\n【相关python stock资料分类】：\nhttp://blog.csdn.net/freewebsys/article/category/7076584\n\n\n## 2，动态展示表格\n\nhttp://localhost:9528/#/example/table\n\n\n在.eslintrc.js 文件中配置 eslint-disable 指令来关闭ESlint语法检测。\n\n\n"
  },
  {
    "path": "frontend/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app\n    '@vue/cli-plugin-babel/preset'\n  ],\n  'env': {\n    'development': {\n      // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().\n      // This plugin can significantly increase the speed of hot updates, when you have a large number of pages.\n      // https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html\n      'plugins': ['dynamic-import-node']\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/docker-build.sh",
    "content": "#!/bin/sh\n\nsleep 1\n# 只依赖启动。\ncd /usr/src/app\n\n#!/bin/bash\n\n# 定义要检查的文件夹路径\nmodules_path=\"/usr/src/app/node_modules\"\n\n# 使用[ ]检查文件夹是否存在\nif [ -d \"$modules_path\" ]; then\n    echo \"文件夹 $modules_path 存在\"\nelse\n    echo \"文件夹 $modules_path 不存在，执行 install 安装\"\n    npm install --registry=https://registry.npmmirror.com\nfi\n\nnpm run build\n# 编译完成之后拷贝 html 资源到 影射目录，等待即可。每次编译前都清空内容。\nrm -rf /data/html/*\ncp -r ./dist/* /data/html/\necho \"######### build finish and cp all html  #########\""
  },
  {
    "path": "frontend/docker-entrypoint.sh",
    "content": "#!/bin/sh\n\nsleep 1\n# 只依赖启动。\ncd /usr/src/app\n\n#!/bin/bash\n\n# 定义要检查的文件夹路径\nmodules_path=\"/usr/src/app/node_modules\"\n\n# 使用[ ]检查文件夹是否存在\nif [ -d \"$modules_path\" ]; then\n    echo \"文件夹 $modules_path 存在\"\nelse\n    echo \"文件夹 $modules_path 不存在，执行 install 安装\"\n    npm install --registry=https://registry.npmmirror.com\nfi\n\nnpm run dev\nsleep 999999d\n"
  },
  {
    "path": "frontend/jest.config.js",
    "content": "module.exports = {\n  moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],\n  transform: {\n    '^.+\\\\.vue$': 'vue-jest',\n    '.+\\\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':\n      'jest-transform-stub',\n    '^.+\\\\.jsx?$': 'babel-jest'\n  },\n  moduleNameMapper: {\n    '^@/(.*)$': '<rootDir>/src/$1'\n  },\n  snapshotSerializers: ['jest-serializer-vue'],\n  testMatch: [\n    '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'\n  ],\n  collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'],\n  coverageDirectory: '<rootDir>/tests/unit/coverage',\n  // 'collectCoverage': true,\n  'coverageReporters': [\n    'lcov',\n    'text-summary'\n  ],\n  testURL: 'http://localhost/'\n}\n"
  },
  {
    "path": "frontend/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \"./\",\n    \"paths\": {\n        \"@/*\": [\"src/*\"]\n    }\n  },\n  \"exclude\": [\"node_modules\", \"dist\"]\n}\n"
  },
  {
    "path": "frontend/mock/index.js",
    "content": "const Mock = require('mockjs')\nconst { param2Obj } = require('./utils')\n\nconst user = require('./user')\nconst table = require('./table')\n\nconst mocks = [\n  ...user,\n  ...table\n]\n\n// for front mock\n// please use it cautiously, it will redefine XMLHttpRequest,\n// which will cause many of your third-party libraries to be invalidated(like progress event).\nfunction mockXHR() {\n  // mock patch\n  // https://github.com/nuysoft/Mock/issues/300\n  Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send\n  Mock.XHR.prototype.send = function() {\n    if (this.custom.xhr) {\n      this.custom.xhr.withCredentials = this.withCredentials || false\n\n      if (this.responseType) {\n        this.custom.xhr.responseType = this.responseType\n      }\n    }\n    this.proxy_send(...arguments)\n  }\n\n  function XHR2ExpressReqWrap(respond) {\n    return function(options) {\n      let result = null\n      if (respond instanceof Function) {\n        const { body, type, url } = options\n        // https://expressjs.com/en/4x/api.html#req\n        result = respond({\n          method: type,\n          body: JSON.parse(body),\n          query: param2Obj(url)\n        })\n      } else {\n        result = respond\n      }\n      return Mock.mock(result)\n    }\n  }\n\n  for (const i of mocks) {\n    Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response))\n  }\n}\n\nmodule.exports = {\n  mocks,\n  mockXHR\n}\n\n"
  },
  {
    "path": "frontend/mock/mock-server.js",
    "content": "const chokidar = require('chokidar')\nconst bodyParser = require('body-parser')\nconst chalk = require('chalk')\nconst path = require('path')\nconst Mock = require('mockjs')\n\nconst mockDir = path.join(process.cwd(), 'mock')\n\nfunction registerRoutes(app) {\n  let mockLastIndex\n  const { mocks } = require('./index.js')\n  const mocksForServer = mocks.map(route => {\n    return responseFake(route.url, route.type, route.response)\n  })\n  for (const mock of mocksForServer) {\n    app[mock.type](mock.url, mock.response)\n    mockLastIndex = app._router.stack.length\n  }\n  const mockRoutesLength = Object.keys(mocksForServer).length\n  return {\n    mockRoutesLength: mockRoutesLength,\n    mockStartIndex: mockLastIndex - mockRoutesLength\n  }\n}\n\nfunction unregisterRoutes() {\n  Object.keys(require.cache).forEach(i => {\n    if (i.includes(mockDir)) {\n      delete require.cache[require.resolve(i)]\n    }\n  })\n}\n\n// for mock server\nconst responseFake = (url, type, respond) => {\n  return {\n    url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`),\n    type: type || 'get',\n    response(req, res) {\n      console.log('request invoke:' + req.path)\n      res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond))\n    }\n  }\n}\n\nmodule.exports = app => {\n  // parse app.body\n  // https://expressjs.com/en/4x/api.html#req.body\n  app.use(bodyParser.json())\n  app.use(bodyParser.urlencoded({\n    extended: true\n  }))\n\n  const mockRoutes = registerRoutes(app)\n  var mockRoutesLength = mockRoutes.mockRoutesLength\n  var mockStartIndex = mockRoutes.mockStartIndex\n\n  // watch files, hot reload mock server\n  chokidar.watch(mockDir, {\n    ignored: /mock-server/,\n    ignoreInitial: true\n  }).on('all', (event, path) => {\n    if (event === 'change' || event === 'add') {\n      try {\n        // remove mock routes stack\n        app._router.stack.splice(mockStartIndex, mockRoutesLength)\n\n        // clear routes cache\n        unregisterRoutes()\n\n        const mockRoutes = registerRoutes(app)\n        mockRoutesLength = mockRoutes.mockRoutesLength\n        mockStartIndex = mockRoutes.mockStartIndex\n\n        console.log(chalk.magentaBright(`\\n > Mock Server hot reload success! changed  ${path}`))\n      } catch (error) {\n        console.log(chalk.redBright(error))\n      }\n    }\n  })\n}\n"
  },
  {
    "path": "frontend/mock/table.js",
    "content": "const Mock = require('mockjs')\n\nconst data = Mock.mock({\n  'items|30': [{\n    id: '@id',\n    title: '@sentence(10, 20)',\n    'status|1': ['published', 'draft', 'deleted'],\n    author: 'name',\n    display_time: '@datetime',\n    pageviews: '@integer(300, 5000)'\n  }]\n})\n\nmodule.exports = [\n  {\n    url: '/vue-admin-template/table/list',\n    type: 'get',\n    response: config => {\n      const items = data.items\n      return {\n        code: 20000,\n        data: {\n          total: items.length,\n          items: items\n        }\n      }\n    }\n  }\n]\n"
  },
  {
    "path": "frontend/mock/user.js",
    "content": "\nconst tokens = {\n  admin: {\n    token: 'admin-token'\n  },\n  editor: {\n    token: 'editor-token'\n  }\n}\n\nconst users = {\n  'admin-token': {\n    roles: ['admin'],\n    introduction: 'I am a super administrator',\n    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',\n    name: 'Super Admin'\n  },\n  'editor-token': {\n    roles: ['editor'],\n    introduction: 'I am an editor',\n    avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif',\n    name: 'Normal Editor'\n  }\n}\n\nmodule.exports = [\n  // user login\n  {\n    url: '/vue-admin-template/user/login',\n    type: 'post',\n    response: config => {\n      const { username } = config.body\n      const token = tokens[username]\n\n      // mock error\n      if (!token) {\n        return {\n          code: 60204,\n          message: 'Account and password are incorrect.'\n        }\n      }\n\n      return {\n        code: 20000,\n        data: token\n      }\n    }\n  },\n\n  // get user info\n  {\n    url: '/vue-admin-template/user/info\\.*',\n    type: 'get',\n    response: config => {\n      const { token } = config.query\n      const info = users[token]\n\n      // mock error\n      if (!info) {\n        return {\n          code: 50008,\n          message: 'Login failed, unable to get user details.'\n        }\n      }\n\n      return {\n        code: 20000,\n        data: info\n      }\n    }\n  },\n\n  // user logout\n  {\n    url: '/vue-admin-template/user/logout',\n    type: 'post',\n    response: _ => {\n      return {\n        code: 20000,\n        data: 'success'\n      }\n    }\n  }\n]\n"
  },
  {
    "path": "frontend/mock/utils.js",
    "content": "/**\n * @param {string} url\n * @returns {Object}\n */\nfunction param2Obj(url) {\n  const search = decodeURIComponent(url.split('?')[1]).replace(/\\+/g, ' ')\n  if (!search) {\n    return {}\n  }\n  const obj = {}\n  const searchArr = search.split('&')\n  searchArr.forEach(v => {\n    const index = v.indexOf('=')\n    if (index !== -1) {\n      const name = v.substring(0, index)\n      const val = v.substring(index + 1, v.length)\n      obj[name] = val\n    }\n  })\n  return obj\n}\n\nmodule.exports = {\n  param2Obj\n}\n"
  },
  {
    "path": "frontend/package.json",
    "content": "{\n  \"name\": \"vue-admin-template\",\n  \"version\": \"4.4.0\",\n  \"description\": \"A vue admin template with Element UI & axios & iconfont & permission control & lint\",\n  \"author\": \"Pan <panfree23@gmail.com>\",\n  \"scripts\": {\n    \"dev\": \"./node_modules/.bin/vue-cli-service serve \",\n    \"build\": \"./node_modules/.bin/vue-cli-service build\",\n    \"build:stage\": \"./node_modules/.bin/vue-cli-service build --mode staging\",\n    \"preview\": \"node build/index.js --preview\",\n    \"svgo\": \"./node_modules/.bin/svgo -f src/icons/svg --config=src/icons/svgo.yml\",\n    \"lint\": \"./node_modules/.bin/eslint --ext .js,.vue src\",\n    \"test:unit\": \"./node_modules/.bin/jest --clearCache && vue-cli-service test:unit\",\n    \"test:ci\": \"npm run lint && npm run test:unit\"\n  },\n  \"dependencies\": {\n    \"axios\": \"0.28.0\",\n    \"core-js\": \"^3.26.1\",\n    \"element-ui\": \"2.15.14\",\n    \"file-saver\": \"^2.0.5\",\n    \"js-cookie\": \"2.2.0\",\n    \"normalize.css\": \"7.0.0\",\n    \"nprogress\": \"0.2.0\",\n    \"path-to-regexp\": \"2.4.0\",\n    \"vue\": \"2.6.10\",\n    \"vue-router\": \"3.0.6\",\n    \"vuex\": \"3.1.0\",\n    \"xlsx\": \"^0.18.5\"\n  },\n  \"devDependencies\": {\n    \"@vue/cli-plugin-babel\": \"4.4.4\",\n    \"@vue/cli-plugin-eslint\": \"4.4.4\",\n    \"@vue/cli-plugin-unit-jest\": \"4.4.4\",\n    \"@vue/cli-service\": \"4.4.4\",\n    \"@vue/test-utils\": \"1.0.0-beta.29\",\n    \"autoprefixer\": \"9.5.1\",\n    \"babel-eslint\": \"10.1.0\",\n    \"babel-jest\": \"23.6.0\",\n    \"babel-plugin-dynamic-import-node\": \"2.3.3\",\n    \"chalk\": \"2.4.2\",\n    \"connect\": \"3.6.6\",\n    \"eslint\": \"6.7.2\",\n    \"eslint-plugin-vue\": \"6.2.2\",\n    \"html-webpack-plugin\": \"3.2.0\",\n    \"mockjs\": \"1.0.1-beta3\",\n    \"runjs\": \"4.3.2\",\n    \"sass\": \"1.26.8\",\n    \"sass-loader\": \"8.0.2\",\n    \"script-ext-html-webpack-plugin\": \"2.1.3\",\n    \"serve-static\": \"1.16.0\",\n    \"svg-sprite-loader\": \"4.1.3\",\n    \"svgo\": \"1.2.2\",\n    \"vue-template-compiler\": \"2.6.10\"\n  },\n  \"browserslist\": [\n    \"> 1%\",\n    \"last 2 versions\"\n  ],\n  \"engines\": {\n    \"node\": \">=8.9\",\n    \"npm\": \">= 3.0.0\"\n  },\n  \"license\": \"MIT\"\n}\n"
  },
  {
    "path": "frontend/postcss.config.js",
    "content": "// https://github.com/michael-ciniawsky/postcss-load-config\n\nmodule.exports = {\n  'plugins': {\n    // to edit target browsers: use \"browserslist\" field in package.json\n    'autoprefixer': {}\n  }\n}\n"
  },
  {
    "path": "frontend/public/40x.html",
    "content": "\n<html>\n<head><title>404 Not Found</title></head>\n<body>\n<center><h1>404 Not Found</h1></center>\n<hr><center>nginx/1.26.3</center>\n</body>\n</html>"
  },
  {
    "path": "frontend/public/50x.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n<title>Error</title>\n<style>\nhtml { color-scheme: light dark; }\nbody { width: 35em; margin: 0 auto;\nfont-family: Tahoma, Verdana, Arial, sans-serif; }\n</style>\n</head>\n<body>\n<h1>An error occurred.</h1>\n<p>Sorry, the page you are looking for is currently unavailable.<br/>\nPlease try again later.</p>\n<p>If you are the system administrator of this resource then you should check\nthe error log for details.</p>\n<p><em>Faithfully yours, nginx.</em></p>\n</body>\n</html>\n"
  },
  {
    "path": "frontend/public/index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\">\n    <link rel=\"icon\" href=\"<%= BASE_URL %>favicon.ico\">\n    <title><%= webpackConfig.name %></title>\n  </head>\n  <body>\n    <noscript>\n      <strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>\n    </noscript>\n    <div id=\"app\"></div>\n    <!-- built files will be auto injected -->\n  </body>\n</html>\n"
  },
  {
    "path": "frontend/src/App.vue",
    "content": "<template>\n  <div id=\"app\">\n    <router-view />\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'App'\n}\n</script>\n"
  },
  {
    "path": "frontend/src/api/article.js",
    "content": "import request from '@/utils/request'\n\nexport function fetchList(query) {\n  return request({\n    url: '/api/v1/api_data',\n    method: 'get',\n    params: query\n  })\n}\n\nexport function fetchArticle(id) {\n  return request({\n    url: '/vue-element-admin/article/detail',\n    method: 'get',\n    params: { id }\n  })\n}\n\nexport function fetchPv(pv) {\n  return request({\n    url: '/vue-element-admin/article/pv',\n    method: 'get',\n    params: { pv }\n  })\n}\n\nexport function createArticle(data) {\n  return request({\n    url: '/vue-element-admin/article/create',\n    method: 'post',\n    data\n  })\n}\n\nexport function updateArticle(data) {\n  return request({\n    url: '/vue-element-admin/article/update',\n    method: 'post',\n    data\n  })\n}\n"
  },
  {
    "path": "frontend/src/api/menu.js",
    "content": "import request from '@/utils/request'\n\n// 同步获得菜单相关数据。\n\nexport  function fetchMenuList(query) {\n  return request({\n    url: '/api/v1/menu_list',\n    method: 'get',\n    params: query\n  })\n}\n\n"
  },
  {
    "path": "frontend/src/api/package.js",
    "content": "import request from '@/utils/request'\n\nexport function fetchPackageVersion(query) {\n  return request({\n    url: '/api/v1/package_verison',\n    method: 'get',\n    params: query\n  })\n}\n\n"
  },
  {
    "path": "frontend/src/api/table.js",
    "content": "import request from '@/utils/request'\n\nexport function getList(params) {\n  return request({\n    url: '/vue-admin-template/table/list',\n    method: 'get',\n    params\n  })\n}\n"
  },
  {
    "path": "frontend/src/api/user.js",
    "content": "import request from '@/utils/request'\n\nexport function login(data) {\n  return request({\n    url: '/vue-admin-template/user/login',\n    method: 'post',\n    data\n  })\n}\n\nexport function getInfo(token) {\n  return request({\n    url: '/vue-admin-template/user/info',\n    method: 'get',\n    params: { token }\n  })\n}\n\nexport function logout() {\n  return request({\n    url: '/vue-admin-template/user/logout',\n    method: 'post'\n  })\n}\n"
  },
  {
    "path": "frontend/src/components/Breadcrumb/index.vue",
    "content": "<template>\n  <el-breadcrumb class=\"app-breadcrumb\" separator=\"/\">\n    <transition-group name=\"breadcrumb\">\n      <el-breadcrumb-item v-for=\"(item,index) in levelList\" :key=\"item.path\">\n        <span v-if=\"item.redirect==='noRedirect'||index==levelList.length-1\" class=\"no-redirect\">{{ item.meta.title }}</span>\n        <a v-else @click.prevent=\"handleLink(item)\">{{ item.meta.title }}</a>\n      </el-breadcrumb-item>\n    </transition-group>\n  </el-breadcrumb>\n</template>\n\n<script>\nimport pathToRegexp from 'path-to-regexp'\n\nexport default {\n  data() {\n    return {\n      levelList: null\n    }\n  },\n  watch: {\n    $route() {\n      this.getBreadcrumb()\n    }\n  },\n  created() {\n    this.getBreadcrumb()\n  },\n  methods: {\n    getBreadcrumb() {\n      // only show routes with meta.title\n      let matched = this.$route.matched.filter(item => item.meta && item.meta.title)\n      const first = matched[0]\n\n      if (!this.isDashboard(first)) {\n        matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)\n      }\n\n      this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)\n    },\n    isDashboard(route) {\n      const name = route && route.name\n      if (!name) {\n        return false\n      }\n      return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()\n    },\n    pathCompile(path) {\n      // To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561\n      const { params } = this.$route\n      var toPath = pathToRegexp.compile(path)\n      return toPath(params)\n    },\n    handleLink(item) {\n      const { redirect, path } = item\n      if (redirect) {\n        this.$router.push(redirect)\n        return\n      }\n      this.$router.push(this.pathCompile(path))\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.app-breadcrumb.el-breadcrumb {\n  display: inline-block;\n  font-size: 14px;\n  line-height: 50px;\n  margin-left: 8px;\n\n  .no-redirect {\n    color: #97a8be;\n    cursor: text;\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Hamburger/index.vue",
    "content": "<template>\n  <div style=\"padding: 0 15px;\" @click=\"toggleClick\">\n    <svg\n      :class=\"{'is-active':isActive}\"\n      class=\"hamburger\"\n      viewBox=\"0 0 1024 1024\"\n      xmlns=\"http://www.w3.org/2000/svg\"\n      width=\"64\"\n      height=\"64\"\n    >\n      <path d=\"M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z\" />\n    </svg>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'Hamburger',\n  props: {\n    isActive: {\n      type: Boolean,\n      default: false\n    }\n  },\n  methods: {\n    toggleClick() {\n      this.$emit('toggleClick')\n    }\n  }\n}\n</script>\n\n<style scoped>\n.hamburger {\n  display: inline-block;\n  vertical-align: middle;\n  width: 20px;\n  height: 20px;\n}\n\n.hamburger.is-active {\n  transform: rotate(180deg);\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/Pagination/index.vue",
    "content": "<template>\n  <div :class=\"{'hidden':hidden}\" class=\"pagination-container\">\n    <el-pagination\n      :background=\"background\"\n      :current-page.sync=\"currentPage\"\n      :page-size.sync=\"pageSize\"\n      :layout=\"layout\"\n      :page-sizes=\"pageSizes\"\n      :total=\"total\"\n      v-bind=\"$attrs\"\n      @size-change=\"handleSizeChange\"\n      @current-change=\"handleCurrentChange\"\n    />\n  </div>\n</template>\n\n<script>\nimport { scrollTo } from '@/utils/scroll-to'\n\nexport default {\n  name: 'Pagination',\n  props: {\n    total: {\n      required: true,\n      type: Number\n    },\n    page: {\n      type: Number,\n      default: 1\n    },\n    limit: {\n      type: Number,\n      default: 20\n    },\n    pageSizes: {\n      type: Array,\n      default() {\n        return [10, 20, 30, 50]\n      }\n    },\n    layout: {\n      type: String,\n      default: 'total, sizes, prev, pager, next, jumper'\n    },\n    background: {\n      type: Boolean,\n      default: true\n    },\n    autoScroll: {\n      type: Boolean,\n      default: true\n    },\n    hidden: {\n      type: Boolean,\n      default: false\n    }\n  },\n  computed: {\n    currentPage: {\n      get() {\n        return this.page\n      },\n      set(val) {\n        this.$emit('update:page', val)\n      }\n    },\n    pageSize: {\n      get() {\n        return this.limit\n      },\n      set(val) {\n        this.$emit('update:limit', val)\n      }\n    }\n  },\n  methods: {\n    handleSizeChange(val) {\n      this.$emit('pagination', { page: this.currentPage, limit: val })\n      if (this.autoScroll) {\n        scrollTo(0, 800)\n      }\n    },\n    handleCurrentChange(val) {\n      this.$emit('pagination', { page: val, limit: this.pageSize })\n      if (this.autoScroll) {\n        scrollTo(0, 800)\n      }\n    }\n  }\n}\n</script>\n\n<style scoped>\n.pagination-container {\n  background: #fff;\n  padding: 32px 16px;\n}\n.pagination-container.hidden {\n  display: none;\n}\n</style>\n"
  },
  {
    "path": "frontend/src/components/SvgIcon/index.vue",
    "content": "<template>\n  <div v-if=\"isExternal\" :style=\"styleExternalIcon\" class=\"svg-external-icon svg-icon\" v-on=\"$listeners\" />\n  <svg v-else :class=\"svgClass\" aria-hidden=\"true\" v-on=\"$listeners\">\n    <use :xlink:href=\"iconName\" />\n  </svg>\n</template>\n\n<script>\n// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage\nimport { isExternal } from '@/utils/validate'\n\nexport default {\n  name: 'SvgIcon',\n  props: {\n    iconClass: {\n      type: String,\n      required: true\n    },\n    className: {\n      type: String,\n      default: ''\n    }\n  },\n  computed: {\n    isExternal() {\n      return isExternal(this.iconClass)\n    },\n    iconName() {\n      return `#icon-${this.iconClass}`\n    },\n    svgClass() {\n      if (this.className) {\n        return 'svg-icon ' + this.className\n      } else {\n        return 'svg-icon'\n      }\n    },\n    styleExternalIcon() {\n      return {\n        mask: `url(${this.iconClass}) no-repeat 50% 50%`,\n        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`\n      }\n    }\n  }\n}\n</script>\n\n<style scoped>\n.svg-icon {\n  width: 1em;\n  height: 1em;\n  vertical-align: -0.15em;\n  fill: currentColor;\n  overflow: hidden;\n}\n\n.svg-external-icon {\n  background-color: currentColor;\n  mask-size: cover!important;\n  display: inline-block;\n}\n</style>\n"
  },
  {
    "path": "frontend/src/directive/el-table/adaptive.js",
    "content": "import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event'\n\n/**\n * How to use\n * <el-table height=\"100px\" v-el-height-adaptive-table=\"{bottomOffset: 30}\">...</el-table>\n * el-table height is must be set\n * bottomOffset: 30(default)   // The height of the table from the bottom of the page.\n */\n\nconst doResize = (el, binding, vnode) => {\n  const { componentInstance: $table } = vnode\n\n  const { value } = binding\n\n  if (!$table.height) {\n    throw new Error(`el-$table must set the height. Such as height='100px'`)\n  }\n  const bottomOffset = (value && value.bottomOffset) || 30\n\n  if (!$table) return\n\n  const height = window.innerHeight - el.getBoundingClientRect().top - bottomOffset\n  $table.layout.setHeight(height)\n  $table.doLayout()\n}\n\nexport default {\n  bind(el, binding, vnode) {\n    el.resizeListener = () => {\n      doResize(el, binding, vnode)\n    }\n    // parameter 1 is must be \"Element\" type\n    addResizeListener(window.document.body, el.resizeListener)\n  },\n  inserted(el, binding, vnode) {\n    doResize(el, binding, vnode)\n  },\n  unbind(el) {\n    removeResizeListener(window.document.body, el.resizeListener)\n  }\n}\n"
  },
  {
    "path": "frontend/src/directive/el-table/index.js",
    "content": "import adaptive from './adaptive'\n\nconst install = function(Vue) {\n  Vue.directive('el-height-adaptive-table', adaptive)\n}\n\nif (window.Vue) {\n  window['el-height-adaptive-table'] = adaptive\n  Vue.use(install); // eslint-disable-line\n}\n\nadaptive.install = install\nexport default adaptive\n"
  },
  {
    "path": "frontend/src/directive/waves/index.js",
    "content": "import waves from './waves'\n\nconst install = function(Vue) {\n  Vue.directive('waves', waves)\n}\n\nif (window.Vue) {\n  window.waves = waves\n  Vue.use(install); // eslint-disable-line\n}\n\nwaves.install = install\nexport default waves\n"
  },
  {
    "path": "frontend/src/directive/waves/waves.css",
    "content": ".waves-ripple {\n    position: absolute;\n    border-radius: 100%;\n    background-color: rgba(0, 0, 0, 0.15);\n    background-clip: padding-box;\n    pointer-events: none;\n    -webkit-user-select: none;\n    -moz-user-select: none;\n    -ms-user-select: none;\n    user-select: none;\n    -webkit-transform: scale(0);\n    -ms-transform: scale(0);\n    transform: scale(0);\n    opacity: 1;\n}\n\n.waves-ripple.z-active {\n    opacity: 0;\n    -webkit-transform: scale(2);\n    -ms-transform: scale(2);\n    transform: scale(2);\n    -webkit-transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;\n    transition: opacity 1.2s ease-out, -webkit-transform 0.6s ease-out;\n    transition: opacity 1.2s ease-out, transform 0.6s ease-out;\n    transition: opacity 1.2s ease-out, transform 0.6s ease-out, -webkit-transform 0.6s ease-out;\n}"
  },
  {
    "path": "frontend/src/directive/waves/waves.js",
    "content": "import './waves.css'\n\nconst context = '@@wavesContext'\n\nfunction handleClick(el, binding) {\n  function handle(e) {\n    const customOpts = Object.assign({}, binding.value)\n    const opts = Object.assign({\n      ele: el, // 波纹作用元素\n      type: 'hit', // hit 点击位置扩散 center中心点扩展\n      color: 'rgba(0, 0, 0, 0.15)' // 波纹颜色\n    },\n    customOpts\n    )\n    const target = opts.ele\n    if (target) {\n      target.style.position = 'relative'\n      target.style.overflow = 'hidden'\n      const rect = target.getBoundingClientRect()\n      let ripple = target.querySelector('.waves-ripple')\n      if (!ripple) {\n        ripple = document.createElement('span')\n        ripple.className = 'waves-ripple'\n        ripple.style.height = ripple.style.width = Math.max(rect.width, rect.height) + 'px'\n        target.appendChild(ripple)\n      } else {\n        ripple.className = 'waves-ripple'\n      }\n      switch (opts.type) {\n        case 'center':\n          ripple.style.top = rect.height / 2 - ripple.offsetHeight / 2 + 'px'\n          ripple.style.left = rect.width / 2 - ripple.offsetWidth / 2 + 'px'\n          break\n        default:\n          ripple.style.top =\n            (e.pageY - rect.top - ripple.offsetHeight / 2 - document.documentElement.scrollTop ||\n              document.body.scrollTop) + 'px'\n          ripple.style.left =\n            (e.pageX - rect.left - ripple.offsetWidth / 2 - document.documentElement.scrollLeft ||\n              document.body.scrollLeft) + 'px'\n      }\n      ripple.style.backgroundColor = opts.color\n      ripple.className = 'waves-ripple z-active'\n      return false\n    }\n  }\n\n  if (!el[context]) {\n    el[context] = {\n      removeHandle: handle\n    }\n  } else {\n    el[context].removeHandle = handle\n  }\n\n  return handle\n}\n\nexport default {\n  bind(el, binding) {\n    el.addEventListener('click', handleClick(el, binding), false)\n  },\n  update(el, binding) {\n    el.removeEventListener('click', el[context].removeHandle, false)\n    el.addEventListener('click', handleClick(el, binding), false)\n  },\n  unbind(el) {\n    el.removeEventListener('click', el[context].removeHandle, false)\n    el[context] = null\n    delete el[context]\n  }\n}\n"
  },
  {
    "path": "frontend/src/icons/index.js",
    "content": "import Vue from 'vue'\nimport SvgIcon from '@/components/SvgIcon'// svg component\n\n// register globally\nVue.component('svg-icon', SvgIcon)\n\nconst req = require.context('./svg', false, /\\.svg$/)\nconst requireAll = requireContext => requireContext.keys().map(requireContext)\nrequireAll(req)\n"
  },
  {
    "path": "frontend/src/icons/svgo.yml",
    "content": "# replace default config\n\n# multipass: true\n# full: true\n\nplugins:\n\n  # - name\n  #\n  # or:\n  # - name: false\n  # - name: true\n  #\n  # or:\n  # - name:\n  #     param1: 1\n  #     param2: 2\n\n- removeAttrs:\n    attrs:\n      - 'fill'\n      - 'fill-rule'\n"
  },
  {
    "path": "frontend/src/layout/components/AppMain.vue",
    "content": "<template>\n  <section class=\"app-main\">\n    <transition name=\"fade-transform\" mode=\"out-in\">\n      <router-view :key=\"key\" />\n    </transition>\n  </section>\n</template>\n\n<script>\nexport default {\n  name: 'AppMain',\n  computed: {\n    key() {\n      return this.$route.path\n    }\n  }\n}\n</script>\n\n<style scoped>\n.app-main {\n  /*50 = navbar  */\n  min-height: calc(100vh - 50px);\n  width: 100%;\n  position: relative;\n  overflow: hidden;\n}\n.fixed-header+.app-main {\n  padding-top: 50px;\n}\n</style>\n\n<style lang=\"scss\">\n// fix css style bug in open el-dialog\n.el-popup-parent--hidden {\n  .fixed-header {\n    padding-right: 15px;\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/layout/components/Navbar.vue",
    "content": "<template>\n  <div class=\"navbar\">\n    <hamburger :is-active=\"sidebar.opened\" class=\"hamburger-container\" @toggleClick=\"toggleSideBar\" />\n\n    <breadcrumb class=\"breadcrumb-container\" />\n\n    <div class=\"right-menu\">\n      <el-dropdown class=\"avatar-container\" trigger=\"click\">\n        <div class=\"avatar-wrapper\">\n          <img src=\"/freewebsys-logo.jpg\" class=\"user-avatar\">\n          <i class=\"el-icon-caret-bottom\" />\n        </div>\n        <el-dropdown-menu slot=\"dropdown\" class=\"user-dropdown\">\n          <router-link to=\"/\">\n            <el-dropdown-item>\n              首页\n            </el-dropdown-item>\n          </router-link>\n          <a target=\"_blank\" href=\"https://gitee.com/pythonstock/stock\">\n            <el-dropdown-item>Gitee项目地址</el-dropdown-item>\n          </a>\n          <a target=\"_blank\" href=\"https://blog.csdn.net/freewebsys/category_9285317.html\">\n            <el-dropdown-item>博客地址</el-dropdown-item>\n          </a>\n          <el-dropdown-item divided @click.native=\"logout\">\n            <span style=\"display:block;\">Log Out</span>\n          </el-dropdown-item>\n        </el-dropdown-menu>\n      </el-dropdown>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { mapGetters } from 'vuex'\nimport Breadcrumb from '@/components/Breadcrumb'\nimport Hamburger from '@/components/Hamburger'\n\nexport default {\n  components: {\n    Breadcrumb,\n    Hamburger\n  },\n  computed: {\n    ...mapGetters([\n      'sidebar',\n      'avatar'\n    ])\n  },\n  methods: {\n    toggleSideBar() {\n      this.$store.dispatch('app/toggleSideBar')\n    },\n    async logout() {\n      await this.$store.dispatch('user/logout')\n      this.$router.push(`/login?redirect=${this.$route.fullPath}`)\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.navbar {\n  height: 50px;\n  overflow: hidden;\n  position: relative;\n  background: #fff;\n  box-shadow: 0 1px 4px rgba(0,21,41,.08);\n\n  .hamburger-container {\n    line-height: 46px;\n    height: 100%;\n    float: left;\n    cursor: pointer;\n    transition: background .3s;\n    -webkit-tap-highlight-color:transparent;\n\n    &:hover {\n      background: rgba(0, 0, 0, .025)\n    }\n  }\n\n  .breadcrumb-container {\n    float: left;\n  }\n\n  .right-menu {\n    float: right;\n    height: 100%;\n    line-height: 50px;\n\n    &:focus {\n      outline: none;\n    }\n\n    .right-menu-item {\n      display: inline-block;\n      padding: 0 8px;\n      height: 100%;\n      font-size: 18px;\n      color: #5a5e66;\n      vertical-align: text-bottom;\n\n      &.hover-effect {\n        cursor: pointer;\n        transition: background .3s;\n\n        &:hover {\n          background: rgba(0, 0, 0, .025)\n        }\n      }\n    }\n\n    .avatar-container {\n      margin-right: 30px;\n\n      .avatar-wrapper {\n        margin-top: 5px;\n        position: relative;\n\n        .user-avatar {\n          cursor: pointer;\n          width: 40px;\n          height: 40px;\n          border-radius: 10px;\n        }\n\n        .el-icon-caret-bottom {\n          cursor: pointer;\n          position: absolute;\n          right: -20px;\n          top: 25px;\n          font-size: 12px;\n        }\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/layout/components/Sidebar/FixiOSBug.js",
    "content": "export default {\n  computed: {\n    device() {\n      return this.$store.state.app.device\n    }\n  },\n  mounted() {\n    // In order to fix the click on menu on the ios device will trigger the mouseleave bug\n    // https://github.com/PanJiaChen/vue-element-admin/issues/1135\n    this.fixBugIniOS()\n  },\n  methods: {\n    fixBugIniOS() {\n      const $subMenu = this.$refs.subMenu\n      if ($subMenu) {\n        const handleMouseleave = $subMenu.handleMouseleave\n        $subMenu.handleMouseleave = (e) => {\n          if (this.device === 'mobile') {\n            return\n          }\n          handleMouseleave(e)\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/layout/components/Sidebar/Item.vue",
    "content": "<script>\nexport default {\n  name: 'MenuItem',\n  functional: true,\n  props: {\n    icon: {\n      type: String,\n      default: ''\n    },\n    title: {\n      type: String,\n      default: ''\n    }\n  },\n  render(h, context) {\n    const { icon, title } = context.props\n    const vnodes = []\n\n    if (icon) {\n      if (icon.includes('el-icon')) {\n        vnodes.push(<i class={[icon, 'sub-el-icon']} />)\n      } else {\n        vnodes.push(<svg-icon icon-class={icon}/>)\n      }\n    }\n\n    if (title) {\n      vnodes.push(<span slot='title'>{(title)}</span>)\n    }\n    return vnodes\n  }\n}\n</script>\n\n<style scoped>\n.sub-el-icon {\n  color: currentColor;\n  width: 1em;\n  height: 1em;\n}\n</style>\n"
  },
  {
    "path": "frontend/src/layout/components/Sidebar/Link.vue",
    "content": "<template>\n  <component :is=\"type\" v-bind=\"linkProps(to)\">\n    <slot />\n  </component>\n</template>\n\n<script>\nimport { isExternal } from '@/utils/validate'\n\nexport default {\n  props: {\n    to: {\n      type: String,\n      required: true\n    }\n  },\n  computed: {\n    isExternal() {\n      return isExternal(this.to)\n    },\n    type() {\n      if (this.isExternal) {\n        return 'a'\n      }\n      return 'router-link'\n    }\n  },\n  methods: {\n    linkProps(to) {\n      if (this.isExternal) {\n        return {\n          href: to,\n          target: '_blank',\n          rel: 'noopener'\n        }\n      }\n      return {\n        to: to\n      }\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "frontend/src/layout/components/Sidebar/Logo.vue",
    "content": "<template>\n  <div class=\"sidebar-logo-container\" :class=\"{'collapse':collapse}\">\n    <transition name=\"sidebarLogoFade\">\n      <router-link v-if=\"collapse\" key=\"collapse\" class=\"sidebar-logo-link\" to=\"/\">\n        <img v-if=\"logo\" :src=\"logo\" class=\"sidebar-logo\">\n        <h1 v-else class=\"sidebar-title\">{{ title }} </h1>\n      </router-link>\n      <router-link v-else key=\"expand\" class=\"sidebar-logo-link\" to=\"/\">\n        <img v-if=\"logo\" :src=\"logo\" class=\"sidebar-logo\">\n        <h1 class=\"sidebar-title\">{{ title }} </h1>\n      </router-link>\n    </transition>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'SidebarLogo',\n  props: {\n    collapse: {\n      type: Boolean,\n      required: true\n    }\n  },\n  data() {\n    return {\n      title: 'Vue Admin Template',\n      logo: 'https://wpimg.wallstcn.com/69a1c46c-eb1c-4b46-8bd4-e9e686ef5251.png'\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.sidebarLogoFade-enter-active {\n  transition: opacity 1.5s;\n}\n\n.sidebarLogoFade-enter,\n.sidebarLogoFade-leave-to {\n  opacity: 0;\n}\n\n.sidebar-logo-container {\n  position: relative;\n  width: 100%;\n  height: 50px;\n  line-height: 50px;\n  background: #2b2f3a;\n  text-align: center;\n  overflow: hidden;\n\n  & .sidebar-logo-link {\n    height: 100%;\n    width: 100%;\n\n    & .sidebar-logo {\n      width: 32px;\n      height: 32px;\n      vertical-align: middle;\n      margin-right: 12px;\n    }\n\n    & .sidebar-title {\n      display: inline-block;\n      margin: 0;\n      color: #fff;\n      font-weight: 600;\n      line-height: 50px;\n      font-size: 14px;\n      font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;\n      vertical-align: middle;\n    }\n  }\n\n  &.collapse {\n    .sidebar-logo {\n      margin-right: 0px;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/layout/components/Sidebar/SidebarItem.vue",
    "content": "<template>\n  <div v-if=\"!item.hidden\">\n    <template v-if=\"hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow\">\n      <app-link v-if=\"onlyOneChild.meta\" :to=\"resolvePath(onlyOneChild.path)\">\n        <el-menu-item :index=\"resolvePath(onlyOneChild.path)\" :class=\"{'submenu-title-noDropdown':!isNest}\">\n          <item :icon=\"onlyOneChild.meta.icon||(item.meta&&item.meta.icon)\" :title=\"onlyOneChild.meta.title\" />\n        </el-menu-item>\n      </app-link>\n    </template>\n\n    <el-submenu v-else ref=\"subMenu\" :index=\"resolvePath(item.path)\" popper-append-to-body>\n      <template slot=\"title\">\n        <item v-if=\"item.meta\" :icon=\"item.meta && item.meta.icon\" :title=\"item.meta.title\" />\n      </template>\n      <sidebar-item\n        v-for=\"child in item.children\"\n        :key=\"child.path\"\n        :is-nest=\"true\"\n        :item=\"child\"\n        :base-path=\"resolvePath(child.path)\"\n        class=\"nest-menu\"\n      />\n    </el-submenu>\n  </div>\n</template>\n\n<script>\nimport path from 'path'\nimport { isExternal } from '@/utils/validate'\nimport Item from './Item'\nimport AppLink from './Link'\nimport FixiOSBug from './FixiOSBug'\n\nexport default {\n  name: 'SidebarItem',\n  components: { Item, AppLink },\n  mixins: [FixiOSBug],\n  props: {\n    // route object\n    item: {\n      type: Object,\n      required: true\n    },\n    isNest: {\n      type: Boolean,\n      default: false\n    },\n    basePath: {\n      type: String,\n      default: ''\n    }\n  },\n  data() {\n    // To fix https://github.com/PanJiaChen/vue-admin-template/issues/237\n    // TODO: refactor with render function\n    this.onlyOneChild = null\n    return {}\n  },\n  methods: {\n    hasOneShowingChild(children = [], parent) {\n      const showingChildren = children.filter(item => {\n        if (item.hidden) {\n          return false\n        } else {\n          // Temp set(will be used if only has one showing child)\n          this.onlyOneChild = item\n          return true\n        }\n      })\n\n      // When there is only one child router, the child router is displayed by default\n      if (showingChildren.length === 1) {\n        return true\n      }\n\n      // Show parent if there are no child router to display\n      if (showingChildren.length === 0) {\n        this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }\n        return true\n      }\n\n      return false\n    },\n    resolvePath(routePath) {\n      if (isExternal(routePath)) {\n        return routePath\n      }\n      if (isExternal(this.basePath)) {\n        return this.basePath\n      }\n      return path.resolve(this.basePath, routePath)\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "frontend/src/layout/components/Sidebar/index.vue",
    "content": "<template>\n  <div :class=\"{'has-logo':showLogo}\">\n    <logo v-if=\"showLogo\" :collapse=\"isCollapse\" />\n    <el-scrollbar wrap-class=\"scrollbar-wrapper\">\n      <el-menu\n        :default-active=\"activeMenu\"\n        :collapse=\"isCollapse\"\n        :background-color=\"variables.menuBg\"\n        :text-color=\"variables.menuText\"\n        :unique-opened=\"false\"\n        :active-text-color=\"variables.menuActiveText\"\n        :collapse-transition=\"false\"\n        mode=\"vertical\"\n      >\n        <sidebar-item v-for=\"route in routes\" :key=\"route.path\" :item=\"route\" :base-path=\"route.path\" />\n      </el-menu>\n    </el-scrollbar>\n  </div>\n</template>\n\n<script>\nimport { mapGetters } from 'vuex'\nimport Logo from './Logo'\nimport SidebarItem from './SidebarItem'\nimport variables from '@/styles/variables.scss'\n\nexport default {\n  components: { SidebarItem, Logo },\n  computed: {\n    ...mapGetters([\n      'sidebar'\n    ]),\n    routes() {\n      return this.$router.options.routes\n    },\n    activeMenu() {\n      const route = this.$route\n      const { meta, path } = route\n      // if set path, the sidebar will highlight the path you set\n      if (meta.activeMenu) {\n        return meta.activeMenu\n      }\n      return path\n    },\n    showLogo() {\n      return this.$store.state.settings.sidebarLogo\n    },\n    variables() {\n      return variables\n    },\n    isCollapse() {\n      return !this.sidebar.opened\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "frontend/src/layout/components/index.js",
    "content": "export { default as Navbar } from './Navbar'\nexport { default as Sidebar } from './Sidebar'\nexport { default as AppMain } from './AppMain'\n"
  },
  {
    "path": "frontend/src/layout/index.vue",
    "content": "<template>\n  <div :class=\"classObj\" class=\"app-wrapper\">\n    <div v-if=\"device==='mobile'&&sidebar.opened\" class=\"drawer-bg\" @click=\"handleClickOutside\" />\n    <sidebar class=\"sidebar-container\" />\n    <div class=\"main-container\">\n      <div :class=\"{'fixed-header':fixedHeader}\">\n        <navbar />\n      </div>\n      <app-main />\n    </div>\n  </div>\n</template>\n\n<script>\nimport { Navbar, Sidebar, AppMain } from './components'\nimport ResizeMixin from './mixin/ResizeHandler'\n\nexport default {\n  name: 'Layout',\n  components: {\n    Navbar,\n    Sidebar,\n    AppMain\n  },\n  mixins: [ResizeMixin],\n  computed: {\n    sidebar() {\n      return this.$store.state.app.sidebar\n    },\n    device() {\n      return this.$store.state.app.device\n    },\n    fixedHeader() {\n      return this.$store.state.settings.fixedHeader\n    },\n    classObj() {\n      return {\n        hideSidebar: !this.sidebar.opened,\n        openSidebar: this.sidebar.opened,\n        withoutAnimation: this.sidebar.withoutAnimation,\n        mobile: this.device === 'mobile'\n      }\n    }\n  },\n  methods: {\n    handleClickOutside() {\n      this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n  @import \"~@/styles/mixin.scss\";\n  @import \"~@/styles/variables.scss\";\n\n  .app-wrapper {\n    @include clearfix;\n    position: relative;\n    height: 100%;\n    width: 100%;\n    &.mobile.openSidebar{\n      position: fixed;\n      top: 0;\n    }\n  }\n  .drawer-bg {\n    background: #000;\n    opacity: 0.3;\n    width: 100%;\n    top: 0;\n    height: 100%;\n    position: absolute;\n    z-index: 999;\n  }\n\n  .fixed-header {\n    position: fixed;\n    top: 0;\n    right: 0;\n    z-index: 9;\n    width: calc(100% - #{$sideBarWidth});\n    transition: width 0.28s;\n  }\n\n  .hideSidebar .fixed-header {\n    width: calc(100% - 54px)\n  }\n\n  .mobile .fixed-header {\n    width: 100%;\n  }\n</style>\n"
  },
  {
    "path": "frontend/src/layout/mixin/ResizeHandler.js",
    "content": "import store from '@/store'\n\nconst { body } = document\nconst WIDTH = 992 // refer to Bootstrap's responsive design\n\nexport default {\n  watch: {\n    $route(route) {\n      if (this.device === 'mobile' && this.sidebar.opened) {\n        store.dispatch('app/closeSideBar', { withoutAnimation: false })\n      }\n    }\n  },\n  beforeMount() {\n    window.addEventListener('resize', this.$_resizeHandler)\n  },\n  beforeDestroy() {\n    window.removeEventListener('resize', this.$_resizeHandler)\n  },\n  mounted() {\n    const isMobile = this.$_isMobile()\n    if (isMobile) {\n      store.dispatch('app/toggleDevice', 'mobile')\n      store.dispatch('app/closeSideBar', { withoutAnimation: true })\n    }\n  },\n  methods: {\n    // use $_ for mixins properties\n    // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential\n    $_isMobile() {\n      const rect = body.getBoundingClientRect()\n      return rect.width - 1 < WIDTH\n    },\n    $_resizeHandler() {\n      if (!document.hidden) {\n        const isMobile = this.$_isMobile()\n        store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')\n\n        if (isMobile) {\n          store.dispatch('app/closeSideBar', { withoutAnimation: true })\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/main.js",
    "content": "import Vue from 'vue'\n\nimport 'normalize.css/normalize.css' // A modern alternative to CSS resets\n\nimport ElementUI from 'element-ui'\nimport 'element-ui/lib/theme-chalk/index.css'\nimport locale from 'element-ui/lib/locale/lang/en' // lang i18n\n\nimport '@/styles/index.scss' // global css\n\nimport App from './App'\nimport store from './store'\nimport router from './router'\n\nimport '@/icons' // icon\n// import '@/permission' // permission control\n// 不进行登录校验。\n\n/**\n * If you don't want to use mock-server\n * you want to use MockJs for mock api\n * you can execute: mockXHR()\n *\n * Currently MockJs will be used in the production environment,\n * please remove it before going online ! ! !\n */\nif (process.env.NODE_ENV === 'production') {\n  const { mockXHR } = require('../mock')\n  mockXHR()\n}\n\n// set ElementUI lang to EN\nVue.use(ElementUI, { locale })\n// 如果想要中文版 element-ui，按如下方式声明\n// Vue.use(ElementUI)\n\nVue.config.productionTip = false\n\nnew Vue({\n  el: '#app',\n  router,\n  store,\n  render: h => h(App)\n})\n"
  },
  {
    "path": "frontend/src/permission.js",
    "content": "import router from './router'\nimport store from './store'\nimport { Message } from 'element-ui'\nimport NProgress from 'nprogress' // progress bar\nimport 'nprogress/nprogress.css' // progress bar style\nimport { getToken } from '@/utils/auth' // get token from cookie\nimport getPageTitle from '@/utils/get-page-title'\n\nNProgress.configure({ showSpinner: false }) // NProgress Configuration\n\nconst whiteList = ['/login'] // no redirect whitelist\n\nrouter.beforeEach(async(to, from, next) => {\n  // start progress bar\n  NProgress.start()\n\n  // set page title\n  document.title = getPageTitle(to.meta.title)\n\n  // determine whether the user has logged in\n  const hasToken = getToken()\n\n  if (hasToken) {\n    if (to.path === '/login') {\n      // if is logged in, redirect to the home page\n      next({ path: '/' })\n      NProgress.done()\n    } else {\n      const hasGetUserInfo = store.getters.name\n      if (hasGetUserInfo) {\n        next()\n      } else {\n        try {\n          // get user info\n          await store.dispatch('user/getInfo')\n\n          next()\n        } catch (error) {\n          // remove token and go to login page to re-login\n          await store.dispatch('user/resetToken')\n          Message.error(error || 'Has Error')\n          next(`/login?redirect=${to.path}`)\n          NProgress.done()\n        }\n      }\n    }\n  } else {\n    /* has no token*/\n\n    if (whiteList.indexOf(to.path) !== -1) {\n      // in the free login whitelist, go directly\n      next()\n    } else {\n      // other pages that do not have permission to access are redirected to the login page.\n      next(`/login?redirect=${to.path}`)\n      NProgress.done()\n    }\n  }\n})\n\nrouter.afterEach(() => {\n  // finish progress bar\n  NProgress.done()\n})\n"
  },
  {
    "path": "frontend/src/router/index.js",
    "content": "import Vue from 'vue'\nimport Router from 'vue-router'\n\n//Vue全局使用Router\nVue.use(Router)\n\n/* Layout */\nimport Layout from '@/layout'\nimport { fetchMenuList } from '@/api/menu'\n\n\n/**\n * Note: sub-menu only appear when route children.length >= 1\n * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html\n *\n * hidden: true                   if set true, item will not show in the sidebar(default is false)\n * alwaysShow: true               if set true, will always show the root menu\n *                                if not set alwaysShow, when item has more than one children route,\n *                                it will becomes nested mode, otherwise not show the root menu\n * redirect: noRedirect           if set noRedirect will no redirect in the breadcrumb\n * name:'router-name'             the name is used by <keep-alive> (must set!!!)\n * meta : {\n    roles: ['admin','editor']    control the page roles (you can set multiple roles)\n    title: 'title'               the name show in sidebar and breadcrumb (recommend set)\n    icon: 'svg-name'/'el-icon-x' the icon show in the sidebar\n    breadcrumb: false            if set false, the item will hidden in breadcrumb(default is true)\n    activeMenu: '/example/list'  if set path, the sidebar will highlight the path you set\n  }\n */\n\n/**\n * constantRoutes\n * a base page that does not have permission requirements\n * all roles can be accessed\n */\nlet constantRoutes = [\n  {\n    path: '/login',\n    component: () => import('@/views/login/index'),\n    hidden: true\n  },\n\n  {\n    path: '/404',\n    component: () => import('@/views/404'),\n    hidden: true\n  },\n\n  {\n    path: '/',\n    component: Layout,\n    redirect: '/dashboard',\n    children: [{\n      path: 'dashboard',\n      name: 'Dashboard',\n      component: () => import('@/views/dashboard/index'),\n      meta: { title: 'Dashboard', icon: 'dashboard' }\n    }]\n  },\n\n  {\n    path: '/stock',\n    component: Layout,\n    redirect: '#',\n    name: 'stock-data-table',\n    meta: { title: 'Example', icon: 'el-icon-s-help' },\n    children: [\n      {\n        path: 'table/:tableName',\n        name: 'Table',\n        component: () => import('@/views/table/index'),\n        meta: { title: 'Table', icon: 'table' }\n      }\n    ]\n  },\n \n \n]\n\nfetchMenuList().then(response => {\n\n  let menu_data = response.data\n  for (const menu of menu_data) {\n    console.info(menu)\n    var childrenList = []\n    for (const childrenMenu of menu.children) {\n      var tmpChildren =   {\n        path: childrenMenu.path,\n        name: childrenMenu.name,\n        component: () => import('@/views/table/index'),\n        meta: { title: childrenMenu.name , icon: 'table' }\n      }\n      childrenList.push(tmpChildren)\n    }\n    var tmp_menu =   {\n      path: '/stock'+menu.name,\n      alwaysShow: true, \n      component: Layout,\n      name: menu.name,\n      redirect: '/#'+menu.name ,\n      meta: { title: menu.name , icon: 'el-icon-s-help' },\n      children: childrenList\n    }\n\n    constantRoutes.push(tmp_menu)\n    constantRoutes.push( { path: '*', redirect: '/404', hidden: true })\n\n  }\n})\n\n//   // 404 page must be placed at the end !!!\n// constantRoutes.push( { path: '*', redirect: '/404', hidden: true })\n\nconst createRouter = () => new Router({\n  // mode: 'history', // require service support\n  scrollBehavior: () => ({ y: 0 }),\n  routes: constantRoutes\n})\n\nconst router = createRouter()\n\n// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465\nexport function resetRouter() {\n  const newRouter = createRouter()\n  router.matcher = newRouter.matcher // reset router\n}\n\nexport default router\n"
  },
  {
    "path": "frontend/src/settings.js",
    "content": "module.exports = {\n\n  title: 'Vue Admin Template',\n\n  /**\n   * @type {boolean} true | false\n   * @description Whether fix the header\n   */\n  fixedHeader: false,\n\n  /**\n   * @type {boolean} true | false\n   * @description Whether show the logo in sidebar\n   */\n  sidebarLogo: false\n}\n"
  },
  {
    "path": "frontend/src/store/getters.js",
    "content": "const getters = {\n  sidebar: state => state.app.sidebar,\n  device: state => state.app.device,\n  token: state => state.user.token,\n  avatar: state => state.user.avatar,\n  name: state => state.user.name\n}\nexport default getters\n"
  },
  {
    "path": "frontend/src/store/index.js",
    "content": "import Vue from 'vue'\nimport Vuex from 'vuex'\nimport getters from './getters'\nimport app from './modules/app'\nimport settings from './modules/settings'\nimport user from './modules/user'\n\nVue.use(Vuex)\n\nconst store = new Vuex.Store({\n  modules: {\n    app,\n    settings,\n    user\n  },\n  getters\n})\n\nexport default store\n"
  },
  {
    "path": "frontend/src/store/modules/app.js",
    "content": "import Cookies from 'js-cookie'\n\nconst state = {\n  sidebar: {\n    opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,\n    withoutAnimation: false\n  },\n  device: 'desktop'\n}\n\nconst mutations = {\n  TOGGLE_SIDEBAR: state => {\n    state.sidebar.opened = !state.sidebar.opened\n    state.sidebar.withoutAnimation = false\n    if (state.sidebar.opened) {\n      Cookies.set('sidebarStatus', 1)\n    } else {\n      Cookies.set('sidebarStatus', 0)\n    }\n  },\n  CLOSE_SIDEBAR: (state, withoutAnimation) => {\n    Cookies.set('sidebarStatus', 0)\n    state.sidebar.opened = false\n    state.sidebar.withoutAnimation = withoutAnimation\n  },\n  TOGGLE_DEVICE: (state, device) => {\n    state.device = device\n  }\n}\n\nconst actions = {\n  toggleSideBar({ commit }) {\n    commit('TOGGLE_SIDEBAR')\n  },\n  closeSideBar({ commit }, { withoutAnimation }) {\n    commit('CLOSE_SIDEBAR', withoutAnimation)\n  },\n  toggleDevice({ commit }, device) {\n    commit('TOGGLE_DEVICE', device)\n  }\n}\n\nexport default {\n  namespaced: true,\n  state,\n  mutations,\n  actions\n}\n"
  },
  {
    "path": "frontend/src/store/modules/settings.js",
    "content": "import defaultSettings from '@/settings'\n\nconst { showSettings, fixedHeader, sidebarLogo } = defaultSettings\n\nconst state = {\n  showSettings: showSettings,\n  fixedHeader: fixedHeader,\n  sidebarLogo: sidebarLogo\n}\n\nconst mutations = {\n  CHANGE_SETTING: (state, { key, value }) => {\n    // eslint-disable-next-line no-prototype-builtins\n    if (state.hasOwnProperty(key)) {\n      state[key] = value\n    }\n  }\n}\n\nconst actions = {\n  changeSetting({ commit }, data) {\n    commit('CHANGE_SETTING', data)\n  }\n}\n\nexport default {\n  namespaced: true,\n  state,\n  mutations,\n  actions\n}\n\n"
  },
  {
    "path": "frontend/src/store/modules/user.js",
    "content": "import { login, logout, getInfo } from '@/api/user'\nimport { getToken, setToken, removeToken } from '@/utils/auth'\nimport { resetRouter } from '@/router'\n\nconst getDefaultState = () => {\n  return {\n    token: getToken(),\n    name: '',\n    avatar: ''\n  }\n}\n\nconst state = getDefaultState()\n\nconst mutations = {\n  RESET_STATE: (state) => {\n    Object.assign(state, getDefaultState())\n  },\n  SET_TOKEN: (state, token) => {\n    state.token = token\n  },\n  SET_NAME: (state, name) => {\n    state.name = name\n  },\n  SET_AVATAR: (state, avatar) => {\n    state.avatar = avatar\n  }\n}\n\nconst actions = {\n  // user login\n  login({ commit }, userInfo) {\n    const { username, password } = userInfo\n    return new Promise((resolve, reject) => {\n      login({ username: username.trim(), password: password }).then(response => {\n        const { data } = response\n        commit('SET_TOKEN', data.token)\n        setToken(data.token)\n        resolve()\n      }).catch(error => {\n        reject(error)\n      })\n    })\n  },\n\n  // get user info\n  getInfo({ commit, state }) {\n    return new Promise((resolve, reject) => {\n      getInfo(state.token).then(response => {\n        const { data } = response\n\n        if (!data) {\n          return reject('Verification failed, please Login again.')\n        }\n\n        const { name, avatar } = data\n\n        commit('SET_NAME', name)\n        commit('SET_AVATAR', avatar)\n        resolve(data)\n      }).catch(error => {\n        reject(error)\n      })\n    })\n  },\n\n  // user logout\n  logout({ commit, state }) {\n    return new Promise((resolve, reject) => {\n      logout(state.token).then(() => {\n        removeToken() // must remove  token  first\n        resetRouter()\n        commit('RESET_STATE')\n        resolve()\n      }).catch(error => {\n        reject(error)\n      })\n    })\n  },\n\n  // remove token\n  resetToken({ commit }) {\n    return new Promise(resolve => {\n      removeToken() // must remove  token  first\n      commit('RESET_STATE')\n      resolve()\n    })\n  }\n}\n\nexport default {\n  namespaced: true,\n  state,\n  mutations,\n  actions\n}\n\n"
  },
  {
    "path": "frontend/src/styles/element-ui.scss",
    "content": "// cover some element-ui styles\n\n.el-breadcrumb__inner,\n.el-breadcrumb__inner a {\n  font-weight: 400 !important;\n}\n\n.el-upload {\n  input[type=\"file\"] {\n    display: none !important;\n  }\n}\n\n.el-upload__input {\n  display: none;\n}\n\n\n// to fixed https://github.com/ElemeFE/element/issues/2461\n.el-dialog {\n  transform: none;\n  left: 0;\n  position: relative;\n  margin: 0 auto;\n}\n\n// refine element ui upload\n.upload-container {\n  .el-upload {\n    width: 100%;\n\n    .el-upload-dragger {\n      width: 100%;\n      height: 200px;\n    }\n  }\n}\n\n// dropdown\n.el-dropdown-menu {\n  a {\n    display: block\n  }\n}\n\n// to fix el-date-picker css style\n.el-range-separator {\n  box-sizing: content-box;\n}\n"
  },
  {
    "path": "frontend/src/styles/index.scss",
    "content": "@import './variables.scss';\n@import './mixin.scss';\n@import './transition.scss';\n@import './element-ui.scss';\n@import './sidebar.scss';\n\nbody {\n  height: 100%;\n  -moz-osx-font-smoothing: grayscale;\n  -webkit-font-smoothing: antialiased;\n  text-rendering: optimizeLegibility;\n  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;\n}\n\nlabel {\n  font-weight: 700;\n}\n\nhtml {\n  height: 100%;\n  box-sizing: border-box;\n}\n\n#app {\n  height: 100%;\n}\n\n*,\n*:before,\n*:after {\n  box-sizing: inherit;\n}\n\na:focus,\na:active {\n  outline: none;\n}\n\na,\na:focus,\na:hover {\n  cursor: pointer;\n  color: inherit;\n  text-decoration: none;\n}\n\ndiv:focus {\n  outline: none;\n}\n\n.clearfix {\n  &:after {\n    visibility: hidden;\n    display: block;\n    font-size: 0;\n    content: \" \";\n    clear: both;\n    height: 0;\n  }\n}\n\n// main-container global css\n.app-container {\n  padding: 20px;\n}\n"
  },
  {
    "path": "frontend/src/styles/mixin.scss",
    "content": "@mixin clearfix {\n  &:after {\n    content: \"\";\n    display: table;\n    clear: both;\n  }\n}\n\n@mixin scrollBar {\n  &::-webkit-scrollbar-track-piece {\n    background: #d3dce6;\n  }\n\n  &::-webkit-scrollbar {\n    width: 6px;\n  }\n\n  &::-webkit-scrollbar-thumb {\n    background: #99a9bf;\n    border-radius: 20px;\n  }\n}\n\n@mixin relative {\n  position: relative;\n  width: 100%;\n  height: 100%;\n}\n"
  },
  {
    "path": "frontend/src/styles/sidebar.scss",
    "content": "#app {\n\n  .main-container {\n    min-height: 100%;\n    transition: margin-left .28s;\n    margin-left: $sideBarWidth;\n    position: relative;\n  }\n\n  .sidebar-container {\n    transition: width 0.28s;\n    width: $sideBarWidth !important;\n    background-color: $menuBg;\n    height: 100%;\n    position: fixed;\n    font-size: 0px;\n    top: 0;\n    bottom: 0;\n    left: 0;\n    z-index: 1001;\n    overflow: hidden;\n\n    // reset element-ui css\n    .horizontal-collapse-transition {\n      transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;\n    }\n\n    .scrollbar-wrapper {\n      overflow-x: hidden !important;\n    }\n\n    .el-scrollbar__bar.is-vertical {\n      right: 0px;\n    }\n\n    .el-scrollbar {\n      height: 100%;\n    }\n\n    &.has-logo {\n      .el-scrollbar {\n        height: calc(100% - 50px);\n      }\n    }\n\n    .is-horizontal {\n      display: none;\n    }\n\n    a {\n      display: inline-block;\n      width: 100%;\n      overflow: hidden;\n    }\n\n    .svg-icon {\n      margin-right: 16px;\n    }\n\n    .sub-el-icon {\n      margin-right: 12px;\n      margin-left: -2px;\n    }\n\n    .el-menu {\n      border: none;\n      height: 100%;\n      width: 100% !important;\n    }\n\n    // menu hover\n    .submenu-title-noDropdown,\n    .el-submenu__title {\n      &:hover {\n        background-color: $menuHover !important;\n      }\n    }\n\n    .is-active>.el-submenu__title {\n      color: $subMenuActiveText !important;\n    }\n\n    & .nest-menu .el-submenu>.el-submenu__title,\n    & .el-submenu .el-menu-item {\n      min-width: $sideBarWidth !important;\n      background-color: $subMenuBg !important;\n\n      &:hover {\n        background-color: $subMenuHover !important;\n      }\n    }\n  }\n\n  .hideSidebar {\n    .sidebar-container {\n      width: 54px !important;\n    }\n\n    .main-container {\n      margin-left: 54px;\n    }\n\n    .submenu-title-noDropdown {\n      padding: 0 !important;\n      position: relative;\n\n      .el-tooltip {\n        padding: 0 !important;\n\n        .svg-icon {\n          margin-left: 20px;\n        }\n\n        .sub-el-icon {\n          margin-left: 19px;\n        }\n      }\n    }\n\n    .el-submenu {\n      overflow: hidden;\n\n      &>.el-submenu__title {\n        padding: 0 !important;\n\n        .svg-icon {\n          margin-left: 20px;\n        }\n\n        .sub-el-icon {\n          margin-left: 19px;\n        }\n\n        .el-submenu__icon-arrow {\n          display: none;\n        }\n      }\n    }\n\n    .el-menu--collapse {\n      .el-submenu {\n        &>.el-submenu__title {\n          &>span {\n            height: 0;\n            width: 0;\n            overflow: hidden;\n            visibility: hidden;\n            display: inline-block;\n          }\n        }\n      }\n    }\n  }\n\n  .el-menu--collapse .el-menu .el-submenu {\n    min-width: $sideBarWidth !important;\n  }\n\n  // mobile responsive\n  .mobile {\n    .main-container {\n      margin-left: 0px;\n    }\n\n    .sidebar-container {\n      transition: transform .28s;\n      width: $sideBarWidth !important;\n    }\n\n    &.hideSidebar {\n      .sidebar-container {\n        pointer-events: none;\n        transition-duration: 0.3s;\n        transform: translate3d(-$sideBarWidth, 0, 0);\n      }\n    }\n  }\n\n  .withoutAnimation {\n\n    .main-container,\n    .sidebar-container {\n      transition: none;\n    }\n  }\n}\n\n// when menu collapsed\n.el-menu--vertical {\n  &>.el-menu {\n    .svg-icon {\n      margin-right: 16px;\n    }\n    .sub-el-icon {\n      margin-right: 12px;\n      margin-left: -2px;\n    }\n  }\n\n  .nest-menu .el-submenu>.el-submenu__title,\n  .el-menu-item {\n    &:hover {\n      // you can use $subMenuHover\n      background-color: $menuHover !important;\n    }\n  }\n\n  // the scroll bar appears when the subMenu is too long\n  >.el-menu--popup {\n    max-height: 100vh;\n    overflow-y: auto;\n\n    &::-webkit-scrollbar-track-piece {\n      background: #d3dce6;\n    }\n\n    &::-webkit-scrollbar {\n      width: 6px;\n    }\n\n    &::-webkit-scrollbar-thumb {\n      background: #99a9bf;\n      border-radius: 20px;\n    }\n  }\n}\n"
  },
  {
    "path": "frontend/src/styles/transition.scss",
    "content": "// global transition css\n\n/* fade */\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.28s;\n}\n\n.fade-enter,\n.fade-leave-active {\n  opacity: 0;\n}\n\n/* fade-transform */\n.fade-transform-leave-active,\n.fade-transform-enter-active {\n  transition: all .5s;\n}\n\n.fade-transform-enter {\n  opacity: 0;\n  transform: translateX(-30px);\n}\n\n.fade-transform-leave-to {\n  opacity: 0;\n  transform: translateX(30px);\n}\n\n/* breadcrumb transition */\n.breadcrumb-enter-active,\n.breadcrumb-leave-active {\n  transition: all .5s;\n}\n\n.breadcrumb-enter,\n.breadcrumb-leave-active {\n  opacity: 0;\n  transform: translateX(20px);\n}\n\n.breadcrumb-move {\n  transition: all .5s;\n}\n\n.breadcrumb-leave-active {\n  position: absolute;\n}\n"
  },
  {
    "path": "frontend/src/styles/variables.scss",
    "content": "// sidebar\n$menuText:#bfcbd9;\n$menuActiveText:#409EFF;\n$subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951\n\n$menuBg:#304156;\n$menuHover:#263445;\n\n$subMenuBg:#1f2d3d;\n$subMenuHover:#001528;\n\n$sideBarWidth: 210px;\n\n// the :export directive is the magic sauce for webpack\n// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass\n:export {\n  menuText: $menuText;\n  menuActiveText: $menuActiveText;\n  subMenuActiveText: $subMenuActiveText;\n  menuBg: $menuBg;\n  menuHover: $menuHover;\n  subMenuBg: $subMenuBg;\n  subMenuHover: $subMenuHover;\n  sideBarWidth: $sideBarWidth;\n}\n"
  },
  {
    "path": "frontend/src/utils/auth.js",
    "content": "import Cookies from 'js-cookie'\n\nconst TokenKey = 'vue_admin_template_token'\n\nexport function getToken() {\n  return Cookies.get(TokenKey)\n}\n\nexport function setToken(token) {\n  return Cookies.set(TokenKey, token)\n}\n\nexport function removeToken() {\n  return Cookies.remove(TokenKey)\n}\n"
  },
  {
    "path": "frontend/src/utils/get-page-title.js",
    "content": "import defaultSettings from '@/settings'\n\nconst title = defaultSettings.title || 'Vue Admin Template'\n\nexport default function getPageTitle(pageTitle) {\n  if (pageTitle) {\n    return `${pageTitle} - ${title}`\n  }\n  return `${title}`\n}\n"
  },
  {
    "path": "frontend/src/utils/index.js",
    "content": "/**\n * Created by PanJiaChen on 16/11/18.\n */\n\n/**\n * Parse the time to string\n * @param {(Object|string|number)} time\n * @param {string} cFormat\n * @returns {string | null}\n */\nexport function parseTime(time, cFormat) {\n  if (arguments.length === 0 || !time) {\n    return null\n  }\n  const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'\n  let date\n  if (typeof time === 'object') {\n    date = time\n  } else {\n    if ((typeof time === 'string')) {\n      if ((/^[0-9]+$/.test(time))) {\n        // support \"1548221490638\"\n        time = parseInt(time)\n      } else {\n        // support safari\n        // https://stackoverflow.com/questions/4310953/invalid-date-in-safari\n        time = time.replace(new RegExp(/-/gm), '/')\n      }\n    }\n\n    if ((typeof time === 'number') && (time.toString().length === 10)) {\n      time = time * 1000\n    }\n    date = new Date(time)\n  }\n  const formatObj = {\n    y: date.getFullYear(),\n    m: date.getMonth() + 1,\n    d: date.getDate(),\n    h: date.getHours(),\n    i: date.getMinutes(),\n    s: date.getSeconds(),\n    a: date.getDay()\n  }\n  const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {\n    const value = formatObj[key]\n    // Note: getDay() returns 0 on Sunday\n    if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }\n    return value.toString().padStart(2, '0')\n  })\n  return time_str\n}\n\n/**\n * @param {number} time\n * @param {string} option\n * @returns {string}\n */\nexport function formatTime(time, option) {\n  if (('' + time).length === 10) {\n    time = parseInt(time) * 1000\n  } else {\n    time = +time\n  }\n  const d = new Date(time)\n  const now = Date.now()\n\n  const diff = (now - d) / 1000\n\n  if (diff < 30) {\n    return '刚刚'\n  } else if (diff < 3600) {\n    // less 1 hour\n    return Math.ceil(diff / 60) + '分钟前'\n  } else if (diff < 3600 * 24) {\n    return Math.ceil(diff / 3600) + '小时前'\n  } else if (diff < 3600 * 24 * 2) {\n    return '1天前'\n  }\n  if (option) {\n    return parseTime(time, option)\n  } else {\n    return (\n      d.getMonth() +\n      1 +\n      '月' +\n      d.getDate() +\n      '日' +\n      d.getHours() +\n      '时' +\n      d.getMinutes() +\n      '分'\n    )\n  }\n}\n\n/**\n * @param {string} url\n * @returns {Object}\n */\nexport function param2Obj(url) {\n  const search = decodeURIComponent(url.split('?')[1]).replace(/\\+/g, ' ')\n  if (!search) {\n    return {}\n  }\n  const obj = {}\n  const searchArr = search.split('&')\n  searchArr.forEach(v => {\n    const index = v.indexOf('=')\n    if (index !== -1) {\n      const name = v.substring(0, index)\n      const val = v.substring(index + 1, v.length)\n      obj[name] = val\n    }\n  })\n  return obj\n}\n"
  },
  {
    "path": "frontend/src/utils/request.js",
    "content": "import axios from 'axios'\nimport { MessageBox, Message } from 'element-ui'\nimport store from '@/store'\nimport { getToken } from '@/utils/auth'\n\n// create an axios instance\nconst service = axios.create({\n  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url\n   withCredentials: true, // send cookies when cross-domain requests\n  timeout: 5000 // request timeout\n})\n\n// request interceptor\nservice.interceptors.request.use(\n  config => {\n    // do something before request is sent\n\n    if (store.getters.token) {\n      // let each request carry token\n      // ['X-Token'] is a custom headers key\n      // please modify it according to the actual situation\n      config.headers['X-Token'] = getToken()\n    }\n    return config\n  },\n  error => {\n    // do something with request error\n    console.log(error) // for debug\n    return Promise.reject(error)\n  }\n)\n\n// response interceptor\nservice.interceptors.response.use(\n  /**\n   * If you want to get http information such as headers or status\n   * Please return  response => response\n  */\n\n  /**\n   * Determine the request status by custom code\n   * Here is just an example\n   * You can also judge the status by HTTP Status Code\n   */\n  response => {\n    const res = response.data\n    console.log('res.code: ' + res.code) // for debug\n\n    // if the custom code is not 20000, it is judged as an error.\n    if (res.code !== 20000) {\n      Message({\n        message: res.message || 'Error',\n        type: 'error',\n        duration: 5 * 1000\n      })\n\n      // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;\n      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {\n        // to re-login\n        MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {\n          confirmButtonText: 'Re-Login',\n          cancelButtonText: 'Cancel',\n          type: 'warning'\n        }).then(() => {\n          store.dispatch('user/resetToken').then(() => {\n            location.reload()\n          })\n        })\n      }\n      return Promise.reject(new Error(res.message || 'Error'))\n    } else {\n      return res\n    }\n  },\n  error => {\n    console.log('err' + error) // for debug\n    Message({\n      message: error.message,\n      type: 'error',\n      duration: 5 * 1000\n    })\n    return Promise.reject(error)\n  }\n)\n\nexport default service\n"
  },
  {
    "path": "frontend/src/utils/scroll-to.js",
    "content": "Math.easeInOutQuad = function(t, b, c, d) {\n  t /= d / 2\n  if (t < 1) {\n    return c / 2 * t * t + b\n  }\n  t--\n  return -c / 2 * (t * (t - 2) - 1) + b\n}\n\n// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts\nvar requestAnimFrame = (function() {\n  return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }\n})()\n\n/**\n * Because it's so fucking difficult to detect the scrolling element, just move them all\n * @param {number} amount\n */\nfunction move(amount) {\n  document.documentElement.scrollTop = amount\n  document.body.parentNode.scrollTop = amount\n  document.body.scrollTop = amount\n}\n\nfunction position() {\n  return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop\n}\n\n/**\n * @param {number} to\n * @param {number} duration\n * @param {Function} callback\n */\nexport function scrollTo(to, duration, callback) {\n  const start = position()\n  const change = to - start\n  const increment = 20\n  let currentTime = 0\n  duration = (typeof (duration) === 'undefined') ? 500 : duration\n  var animateScroll = function() {\n    // increment the time\n    currentTime += increment\n    // find the value with the quadratic in-out easing function\n    var val = Math.easeInOutQuad(currentTime, start, change, duration)\n    // move the document.body\n    move(val)\n    // do the animation unless its over\n    if (currentTime < duration) {\n      requestAnimFrame(animateScroll)\n    } else {\n      if (callback && typeof (callback) === 'function') {\n        // the animation is done so lets callback\n        callback()\n      }\n    }\n  }\n  animateScroll()\n}\n"
  },
  {
    "path": "frontend/src/utils/validate.js",
    "content": "/**\n * Created by PanJiaChen on 16/11/18.\n */\n\n/**\n * @param {string} path\n * @returns {Boolean}\n */\nexport function isExternal(path) {\n  return /^(https?:|mailto:|tel:)/.test(path)\n}\n\n/**\n * @param {string} str\n * @returns {Boolean}\n */\nexport function validUsername(str) {\n  const valid_map = ['admin', 'editor']\n  return valid_map.indexOf(str.trim()) >= 0\n}\n"
  },
  {
    "path": "frontend/src/vendor/Export2Excel.js",
    "content": "/* eslint-disable */\nimport { saveAs } from 'file-saver'\nimport XLSX from 'xlsx'\n\nfunction generateArray(table) {\n  var out = [];\n  var rows = table.querySelectorAll('tr');\n  var ranges = [];\n  for (var R = 0; R < rows.length; ++R) {\n    var outRow = [];\n    var row = rows[R];\n    var columns = row.querySelectorAll('td');\n    for (var C = 0; C < columns.length; ++C) {\n      var cell = columns[C];\n      var colspan = cell.getAttribute('colspan');\n      var rowspan = cell.getAttribute('rowspan');\n      var cellValue = cell.innerText;\n      if (cellValue !== \"\" && cellValue == +cellValue) cellValue = +cellValue;\n\n      //Skip ranges\n      ranges.forEach(function (range) {\n        if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) {\n          for (var i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null);\n        }\n      });\n\n      //Handle Row Span\n      if (rowspan || colspan) {\n        rowspan = rowspan || 1;\n        colspan = colspan || 1;\n        ranges.push({\n          s: {\n            r: R,\n            c: outRow.length\n          },\n          e: {\n            r: R + rowspan - 1,\n            c: outRow.length + colspan - 1\n          }\n        });\n      };\n\n      //Handle Value\n      outRow.push(cellValue !== \"\" ? cellValue : null);\n\n      //Handle Colspan\n      if (colspan)\n        for (var k = 0; k < colspan - 1; ++k) outRow.push(null);\n    }\n    out.push(outRow);\n  }\n  return [out, ranges];\n};\n\nfunction datenum(v, date1904) {\n  if (date1904) v += 1462;\n  var epoch = Date.parse(v);\n  return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000);\n}\n\nfunction sheet_from_array_of_arrays(data, opts) {\n  var ws = {};\n  var range = {\n    s: {\n      c: 10000000,\n      r: 10000000\n    },\n    e: {\n      c: 0,\n      r: 0\n    }\n  };\n  for (var R = 0; R != data.length; ++R) {\n    for (var C = 0; C != data[R].length; ++C) {\n      if (range.s.r > R) range.s.r = R;\n      if (range.s.c > C) range.s.c = C;\n      if (range.e.r < R) range.e.r = R;\n      if (range.e.c < C) range.e.c = C;\n      var cell = {\n        v: data[R][C]\n      };\n      if (cell.v == null) continue;\n      var cell_ref = XLSX.utils.encode_cell({\n        c: C,\n        r: R\n      });\n\n      if (typeof cell.v === 'number') cell.t = 'n';\n      else if (typeof cell.v === 'boolean') cell.t = 'b';\n      else if (cell.v instanceof Date) {\n        cell.t = 'n';\n        cell.z = XLSX.SSF._table[14];\n        cell.v = datenum(cell.v);\n      } else cell.t = 's';\n\n      ws[cell_ref] = cell;\n    }\n  }\n  if (range.s.c < 10000000) ws['!ref'] = XLSX.utils.encode_range(range);\n  return ws;\n}\n\nfunction Workbook() {\n  if (!(this instanceof Workbook)) return new Workbook();\n  this.SheetNames = [];\n  this.Sheets = {};\n}\n\nfunction s2ab(s) {\n  var buf = new ArrayBuffer(s.length);\n  var view = new Uint8Array(buf);\n  for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;\n  return buf;\n}\n\nexport function export_table_to_excel(id) {\n  var theTable = document.getElementById(id);\n  var oo = generateArray(theTable);\n  var ranges = oo[1];\n\n  /* original data */\n  var data = oo[0];\n  var ws_name = \"SheetJS\";\n\n  var wb = new Workbook(),\n    ws = sheet_from_array_of_arrays(data);\n\n  /* add ranges to worksheet */\n  // ws['!cols'] = ['apple', 'banan'];\n  ws['!merges'] = ranges;\n\n  /* add worksheet to workbook */\n  wb.SheetNames.push(ws_name);\n  wb.Sheets[ws_name] = ws;\n\n  var wbout = XLSX.write(wb, {\n    bookType: 'xlsx',\n    bookSST: false,\n    type: 'binary'\n  });\n\n  saveAs(new Blob([s2ab(wbout)], {\n    type: \"application/octet-stream\"\n  }), \"test.xlsx\")\n}\n\nexport function export_json_to_excel({\n  multiHeader = [],\n  header,\n  data,\n  filename,\n  merges = [],\n  autoWidth = true,\n  bookType = 'xlsx'\n} = {}) {\n  /* original data */\n  filename = filename || 'excel-list'\n  data = [...data]\n  data.unshift(header);\n\n  for (let i = multiHeader.length - 1; i > -1; i--) {\n    data.unshift(multiHeader[i])\n  }\n\n  var ws_name = \"SheetJS\";\n  var wb = new Workbook(),\n    ws = sheet_from_array_of_arrays(data);\n\n  if (merges.length > 0) {\n    if (!ws['!merges']) ws['!merges'] = [];\n    merges.forEach(item => {\n      ws['!merges'].push(XLSX.utils.decode_range(item))\n    })\n  }\n\n  if (autoWidth) {\n    /*设置worksheet每列的最大宽度*/\n    const colWidth = data.map(row => row.map(val => {\n      /*先判断是否为null/undefined*/\n      if (val == null) {\n        return {\n          'wch': 10\n        };\n      }\n      /*再判断是否为中文*/\n      else if (val.toString().charCodeAt(0) > 255) {\n        return {\n          'wch': val.toString().length * 2\n        };\n      } else {\n        return {\n          'wch': val.toString().length\n        };\n      }\n    }))\n    /*以第一行为初始值*/\n    let result = colWidth[0];\n    for (let i = 1; i < colWidth.length; i++) {\n      for (let j = 0; j < colWidth[i].length; j++) {\n        if (result[j]['wch'] < colWidth[i][j]['wch']) {\n          result[j]['wch'] = colWidth[i][j]['wch'];\n        }\n      }\n    }\n    ws['!cols'] = result;\n  }\n\n  /* add worksheet to workbook */\n  wb.SheetNames.push(ws_name);\n  wb.Sheets[ws_name] = ws;\n\n  var wbout = XLSX.write(wb, {\n    bookType: bookType,\n    bookSST: false,\n    type: 'binary'\n  });\n  saveAs(new Blob([s2ab(wbout)], {\n    type: \"application/octet-stream\"\n  }), `${filename}.${bookType}`);\n}\n"
  },
  {
    "path": "frontend/src/views/404.vue",
    "content": "<template>\n  <div class=\"wscn-http404-container\">\n    <div class=\"wscn-http404\">\n      <div class=\"pic-404\">\n        <img class=\"pic-404__parent\" src=\"@/assets/404_images/404.png\" alt=\"404\">\n        <img class=\"pic-404__child left\" src=\"@/assets/404_images/404_cloud.png\" alt=\"404\">\n        <img class=\"pic-404__child mid\" src=\"@/assets/404_images/404_cloud.png\" alt=\"404\">\n        <img class=\"pic-404__child right\" src=\"@/assets/404_images/404_cloud.png\" alt=\"404\">\n      </div>\n      <div class=\"bullshit\">\n        <div class=\"bullshit__oops\">OOPS!</div>\n        <div class=\"bullshit__info\">All rights reserved\n          <a style=\"color:#20a0ff\" href=\"https://wallstreetcn.com\" target=\"_blank\">wallstreetcn</a>\n        </div>\n        <div class=\"bullshit__headline\">{{ message }}</div>\n        <div class=\"bullshit__info\">Please check that the URL you entered is correct, or click the button below to return to the homepage.</div>\n        <a href=\"\" class=\"bullshit__return-home\">Back to home</a>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\n\nexport default {\n  name: 'Page404',\n  computed: {\n    message() {\n      return 'The webmaster said that you can not enter this page...'\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.wscn-http404-container{\n  transform: translate(-50%,-50%);\n  position: absolute;\n  top: 40%;\n  left: 50%;\n}\n.wscn-http404 {\n  position: relative;\n  width: 1200px;\n  padding: 0 50px;\n  overflow: hidden;\n  .pic-404 {\n    position: relative;\n    float: left;\n    width: 600px;\n    overflow: hidden;\n    &__parent {\n      width: 100%;\n    }\n    &__child {\n      position: absolute;\n      &.left {\n        width: 80px;\n        top: 17px;\n        left: 220px;\n        opacity: 0;\n        animation-name: cloudLeft;\n        animation-duration: 2s;\n        animation-timing-function: linear;\n        animation-fill-mode: forwards;\n        animation-delay: 1s;\n      }\n      &.mid {\n        width: 46px;\n        top: 10px;\n        left: 420px;\n        opacity: 0;\n        animation-name: cloudMid;\n        animation-duration: 2s;\n        animation-timing-function: linear;\n        animation-fill-mode: forwards;\n        animation-delay: 1.2s;\n      }\n      &.right {\n        width: 62px;\n        top: 100px;\n        left: 500px;\n        opacity: 0;\n        animation-name: cloudRight;\n        animation-duration: 2s;\n        animation-timing-function: linear;\n        animation-fill-mode: forwards;\n        animation-delay: 1s;\n      }\n      @keyframes cloudLeft {\n        0% {\n          top: 17px;\n          left: 220px;\n          opacity: 0;\n        }\n        20% {\n          top: 33px;\n          left: 188px;\n          opacity: 1;\n        }\n        80% {\n          top: 81px;\n          left: 92px;\n          opacity: 1;\n        }\n        100% {\n          top: 97px;\n          left: 60px;\n          opacity: 0;\n        }\n      }\n      @keyframes cloudMid {\n        0% {\n          top: 10px;\n          left: 420px;\n          opacity: 0;\n        }\n        20% {\n          top: 40px;\n          left: 360px;\n          opacity: 1;\n        }\n        70% {\n          top: 130px;\n          left: 180px;\n          opacity: 1;\n        }\n        100% {\n          top: 160px;\n          left: 120px;\n          opacity: 0;\n        }\n      }\n      @keyframes cloudRight {\n        0% {\n          top: 100px;\n          left: 500px;\n          opacity: 0;\n        }\n        20% {\n          top: 120px;\n          left: 460px;\n          opacity: 1;\n        }\n        80% {\n          top: 180px;\n          left: 340px;\n          opacity: 1;\n        }\n        100% {\n          top: 200px;\n          left: 300px;\n          opacity: 0;\n        }\n      }\n    }\n  }\n  .bullshit {\n    position: relative;\n    float: left;\n    width: 300px;\n    padding: 30px 0;\n    overflow: hidden;\n    &__oops {\n      font-size: 32px;\n      font-weight: bold;\n      line-height: 40px;\n      color: #1482f0;\n      opacity: 0;\n      margin-bottom: 20px;\n      animation-name: slideUp;\n      animation-duration: 0.5s;\n      animation-fill-mode: forwards;\n    }\n    &__headline {\n      font-size: 20px;\n      line-height: 24px;\n      color: #222;\n      font-weight: bold;\n      opacity: 0;\n      margin-bottom: 10px;\n      animation-name: slideUp;\n      animation-duration: 0.5s;\n      animation-delay: 0.1s;\n      animation-fill-mode: forwards;\n    }\n    &__info {\n      font-size: 13px;\n      line-height: 21px;\n      color: grey;\n      opacity: 0;\n      margin-bottom: 30px;\n      animation-name: slideUp;\n      animation-duration: 0.5s;\n      animation-delay: 0.2s;\n      animation-fill-mode: forwards;\n    }\n    &__return-home {\n      display: block;\n      float: left;\n      width: 110px;\n      height: 36px;\n      background: #1482f0;\n      border-radius: 100px;\n      text-align: center;\n      color: #ffffff;\n      opacity: 0;\n      font-size: 14px;\n      line-height: 36px;\n      cursor: pointer;\n      animation-name: slideUp;\n      animation-duration: 0.5s;\n      animation-delay: 0.3s;\n      animation-fill-mode: forwards;\n    }\n    @keyframes slideUp {\n      0% {\n        transform: translateY(60px);\n        opacity: 0;\n      }\n      100% {\n        transform: translateY(0);\n        opacity: 1;\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/views/dashboard/index.vue",
    "content": "<template>\n  <div class=\"dashboard-container\">\n\n<div class=\"clearfix\">\n\t\t<div class=\"pull-left tableTools-container\">\n\t\t\t<h3>基础库版本</h3>\n\t\t\t<p>1，pandas使用【 {{ pandasVersion }} 】版本， <el-link type=\"primary\" href=\"https://www.runoob.com/pandas/pandas-tutorial.html\" target=\"_blank\">中文文档</el-link> </p>\n\t\t\t<p>2，numpy使用【 {{ numpyVersion }} 】版本， <el-link type=\"primary\" href=\"https://www.runoob.com/numpy/numpy-tutorial.html\" target=\"_blank\">中文文档</el-link></p>\n\t\t\t<p>3，sqlalchemy使用【 {{ sqlalchemyVersion }} 】版本， <el-link type=\"primary\" href=\"https://docs.sqlalchemy.org/en/20/\" target=\"_blank\">英文文档</el-link></p>\n\t\t\t<p>4，akshare使用【 {{ akshareVersion }} 】版本， <el-link type=\"primary\" href=\"https://akshare.akfamily.xyz/data/index/index.html\" target=\"_blank\">中文文档</el-link></p>\n\t\t\t<p>5，bokeh使用【 {{ bokehVersion }} 】版本， <el-link type=\"primary\" href=\"http://docs.bokeh.org/en/latest/\" target=\"_blank\">官方文档</el-link></p>\n\t\t\t<p>6，stockstats使用【 {{ stockstatsVersion }} 】版本， <el-link type=\"primary\" href=\"https://github.com/jealous/stockstats/\" target=\"_blank\">官方文档</el-link></p>\n\t\t</div>\n\t</div>\n\n\t<div class=\"clearfix\">\n\t\t<div class=\"pull-left tableTools-container\">\n\t\t\t<h3>相关资料信息</h3>\n\t\t\t<p>1，github项目地址。<el-link type=\"primary\" href=\"https://gitee.com/pythonstock/stock\" target=\"_blank\">pythonstock</el-link></p>\n\t\t\t<p>2，博客地址。<el-link type=\"primary\" href=\"https://blog.csdn.net/freewebsys/category_9285317.html\" target=\"_blank\">博客地址</el-link></p>\n\t\t</div>\n\t</div>\n\n\t<div class=\"clearfix\">\n\t\t<div class=\"pull-left tableTools-container\">\n\t\t\t<h3>2021年9月20日更新，发布2.0版本</h3>\n\t\t\t<p>1，修复bokeh的版本升级问题，可以显示趋势数据了。</p>\n\t\t\t<p>2，使用 stock_zh_ah_name 做每日数据。</p>\n\t\t\t<p>3，AkShare 升级到 1.1.9 版本。</p>\n\t\t</div>\n\t</div>\n\n\t<div class=\"clearfix\">\n\t\t<div class=\"pull-left tableTools-container\">\n\t\t\t<h3>2021年8月31日更新job服务</h3>\n\t\t\t<p>1，过滤包括：600，6006，601，000，001，002，且不包括ST的股票数据。。</p>\n\t\t\t<p>2，使用 stock_zh_ah_name 做每日数据。</p>\n\t\t\t<p>3，AkShare 升级到 1.0.80 版本。</p>\n\n\t\t</div>\n\t</div>\n\n\t<div class=\"clearfix\">\n\t\t<div class=\"pull-left tableTools-container\">\n\t\t\t<h3>2021年6月3日使用 AkShare 做数据抓取服务</h3>\n\t\t\t<p>1，使用 stock_zh_a_spot 做实时行情数据。</p>\n\t\t\t<p>2，使用 stock_zh_a_daily 做历史数据统计。</p>\n\t\t\t<p>3，升级基础镜像使用python3.7，AkShare 的 0.9.65 版本。</p>\n\t\t</div>\n\t</div>\n\n\n\n  </div>\n</template>\n\n<script>\nimport { mapGetters } from 'vuex'\nimport { fetchPackageVersion } from '@/api/package'\n\nexport default {\n  name: '首页',\n  data() {\n    return {\n\t\t\tpandasVersion: '',\n\t\t\tnumpyVersion: '',\n\t\t\tsqlalchemyVersion: '',\n\t\t\takshareVersion: '',\n\t\t\tbokehVersion: '',\n\t\t\tstockstatsVersion: '',\n\t}\n  },\n  created() {\n    this.getPackageVersion()\n  },\n  methods: {\n    getPackageVersion() {\n\tconsole.info(\"$router.params : \", this.$router.params)\n      fetchPackageVersion().then(response => {\n\n\t\tthis.pandasVersion = response.pandasVersion\n\t\tthis.numpyVersion = response.numpyVersion\n\t\tthis.sqlalchemyVersion = response.sqlalchemyVersion\n\t\tthis.akshareVersion = response.akshareVersion\n\t\tthis.bokehVersion = response.bokehVersion\n\t\tthis.stockstatsVersion = response.stockstatsVersion\n\n      })\n\t}\n  }\n}\n</script>\n\n<style lang=\"scss\" scoped>\n.dashboard {\n  &-container {\n    margin: 30px;\n  }\n  &-text {\n    font-size: 30px;\n    line-height: 46px;\n  }\n}\n</style>"
  },
  {
    "path": "frontend/src/views/form/index.vue",
    "content": "<template>\n  <div class=\"app-container\">\n    <el-form ref=\"form\" :model=\"form\" label-width=\"120px\">\n      <el-form-item label=\"Activity name\">\n        <el-input v-model=\"form.name\" />\n      </el-form-item>\n      <el-form-item label=\"Activity zone\">\n        <el-select v-model=\"form.region\" placeholder=\"please select your zone\">\n          <el-option label=\"Zone one\" value=\"shanghai\" />\n          <el-option label=\"Zone two\" value=\"beijing\" />\n        </el-select>\n      </el-form-item>\n      <el-form-item label=\"Activity time\">\n        <el-col :span=\"11\">\n          <el-date-picker v-model=\"form.date1\" type=\"date\" placeholder=\"Pick a date\" style=\"width: 100%;\" />\n        </el-col>\n        <el-col :span=\"2\" class=\"line\">-</el-col>\n        <el-col :span=\"11\">\n          <el-time-picker v-model=\"form.date2\" type=\"fixed-time\" placeholder=\"Pick a time\" style=\"width: 100%;\" />\n        </el-col>\n      </el-form-item>\n      <el-form-item label=\"Instant delivery\">\n        <el-switch v-model=\"form.delivery\" />\n      </el-form-item>\n      <el-form-item label=\"Activity type\">\n        <el-checkbox-group v-model=\"form.type\">\n          <el-checkbox label=\"Online activities\" name=\"type\" />\n          <el-checkbox label=\"Promotion activities\" name=\"type\" />\n          <el-checkbox label=\"Offline activities\" name=\"type\" />\n          <el-checkbox label=\"Simple brand exposure\" name=\"type\" />\n        </el-checkbox-group>\n      </el-form-item>\n      <el-form-item label=\"Resources\">\n        <el-radio-group v-model=\"form.resource\">\n          <el-radio label=\"Sponsor\" />\n          <el-radio label=\"Venue\" />\n        </el-radio-group>\n      </el-form-item>\n      <el-form-item label=\"Activity form\">\n        <el-input v-model=\"form.desc\" type=\"textarea\" />\n      </el-form-item>\n      <el-form-item>\n        <el-button type=\"primary\" @click=\"onSubmit\">Create</el-button>\n        <el-button @click=\"onCancel\">Cancel</el-button>\n      </el-form-item>\n    </el-form>\n  </div>\n</template>\n\n<script>\nexport default {\n  data() {\n    return {\n      form: {\n        name: '',\n        region: '',\n        date1: '',\n        date2: '',\n        delivery: false,\n        type: [],\n        resource: '',\n        desc: ''\n      }\n    }\n  },\n  methods: {\n    onSubmit() {\n      this.$message('submit!')\n    },\n    onCancel() {\n      this.$message({\n        message: 'cancel!',\n        type: 'warning'\n      })\n    }\n  }\n}\n</script>\n\n<style scoped>\n.line{\n  text-align: center;\n}\n</style>\n\n"
  },
  {
    "path": "frontend/src/views/login/index.vue",
    "content": "<template>\n  <div class=\"login-container\">\n    <el-form ref=\"loginForm\" :model=\"loginForm\" :rules=\"loginRules\" class=\"login-form\" auto-complete=\"on\" label-position=\"left\">\n\n      <div class=\"title-container\">\n        <h3 class=\"title\">Login Form</h3>\n      </div>\n\n      <el-form-item prop=\"username\">\n        <span class=\"svg-container\">\n          <svg-icon icon-class=\"user\" />\n        </span>\n        <el-input\n          ref=\"username\"\n          v-model=\"loginForm.username\"\n          placeholder=\"Username\"\n          name=\"username\"\n          type=\"text\"\n          tabindex=\"1\"\n          auto-complete=\"on\"\n        />\n      </el-form-item>\n\n      <el-form-item prop=\"password\">\n        <span class=\"svg-container\">\n          <svg-icon icon-class=\"password\" />\n        </span>\n        <el-input\n          :key=\"passwordType\"\n          ref=\"password\"\n          v-model=\"loginForm.password\"\n          :type=\"passwordType\"\n          placeholder=\"Password\"\n          name=\"password\"\n          tabindex=\"2\"\n          auto-complete=\"on\"\n          @keyup.enter.native=\"handleLogin\"\n        />\n        <span class=\"show-pwd\" @click=\"showPwd\">\n          <svg-icon :icon-class=\"passwordType === 'password' ? 'eye' : 'eye-open'\" />\n        </span>\n      </el-form-item>\n\n      <el-button :loading=\"loading\" type=\"primary\" style=\"width:100%;margin-bottom:30px;\" @click.native.prevent=\"handleLogin\">Login</el-button>\n\n      <div class=\"tips\">\n        <span style=\"margin-right:20px;\">username: admin</span>\n        <span> password: any</span>\n      </div>\n\n    </el-form>\n  </div>\n</template>\n\n<script>\nimport { validUsername } from '@/utils/validate'\n\nexport default {\n  name: 'Login',\n  data() {\n    const validateUsername = (rule, value, callback) => {\n      if (!validUsername(value)) {\n        callback(new Error('Please enter the correct user name'))\n      } else {\n        callback()\n      }\n    }\n    const validatePassword = (rule, value, callback) => {\n      if (value.length < 6) {\n        callback(new Error('The password can not be less than 6 digits'))\n      } else {\n        callback()\n      }\n    }\n    return {\n      loginForm: {\n        username: 'admin',\n        password: '111111'\n      },\n      loginRules: {\n        username: [{ required: true, trigger: 'blur', validator: validateUsername }],\n        password: [{ required: true, trigger: 'blur', validator: validatePassword }]\n      },\n      loading: false,\n      passwordType: 'password',\n      redirect: undefined\n    }\n  },\n  watch: {\n    $route: {\n      handler: function(route) {\n        this.redirect = route.query && route.query.redirect\n      },\n      immediate: true\n    }\n  },\n  methods: {\n    showPwd() {\n      if (this.passwordType === 'password') {\n        this.passwordType = ''\n      } else {\n        this.passwordType = 'password'\n      }\n      this.$nextTick(() => {\n        this.$refs.password.focus()\n      })\n    },\n    handleLogin() {\n      this.$refs.loginForm.validate(valid => {\n        if (valid) {\n          this.loading = true\n          this.$store.dispatch('user/login', this.loginForm).then(() => {\n            this.$router.push({ path: this.redirect || '/' })\n            this.loading = false\n          }).catch(() => {\n            this.loading = false\n          })\n        } else {\n          console.log('error submit!!')\n          return false\n        }\n      })\n    }\n  }\n}\n</script>\n\n<style lang=\"scss\">\n/* 修复input 背景不协调 和光标变色 */\n/* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */\n\n$bg:#283443;\n$light_gray:#fff;\n$cursor: #fff;\n\n@supports (-webkit-mask: none) and (not (cater-color: $cursor)) {\n  .login-container .el-input input {\n    color: $cursor;\n  }\n}\n\n/* reset element-ui css */\n.login-container {\n  .el-input {\n    display: inline-block;\n    height: 47px;\n    width: 85%;\n\n    input {\n      background: transparent;\n      border: 0px;\n      -webkit-appearance: none;\n      border-radius: 0px;\n      padding: 12px 5px 12px 15px;\n      color: $light_gray;\n      height: 47px;\n      caret-color: $cursor;\n\n      &:-webkit-autofill {\n        box-shadow: 0 0 0px 1000px $bg inset !important;\n        -webkit-text-fill-color: $cursor !important;\n      }\n    }\n  }\n\n  .el-form-item {\n    border: 1px solid rgba(255, 255, 255, 0.1);\n    background: rgba(0, 0, 0, 0.1);\n    border-radius: 5px;\n    color: #454545;\n  }\n}\n</style>\n\n<style lang=\"scss\" scoped>\n$bg:#2d3a4b;\n$dark_gray:#889aa4;\n$light_gray:#eee;\n\n.login-container {\n  min-height: 100%;\n  width: 100%;\n  background-color: $bg;\n  overflow: hidden;\n\n  .login-form {\n    position: relative;\n    width: 520px;\n    max-width: 100%;\n    padding: 160px 35px 0;\n    margin: 0 auto;\n    overflow: hidden;\n  }\n\n  .tips {\n    font-size: 14px;\n    color: #fff;\n    margin-bottom: 10px;\n\n    span {\n      &:first-of-type {\n        margin-right: 16px;\n      }\n    }\n  }\n\n  .svg-container {\n    padding: 6px 5px 6px 15px;\n    color: $dark_gray;\n    vertical-align: middle;\n    width: 30px;\n    display: inline-block;\n  }\n\n  .title-container {\n    position: relative;\n\n    .title {\n      font-size: 26px;\n      color: $light_gray;\n      margin: 0px auto 40px auto;\n      text-align: center;\n      font-weight: bold;\n    }\n  }\n\n  .show-pwd {\n    position: absolute;\n    right: 10px;\n    top: 7px;\n    font-size: 16px;\n    color: $dark_gray;\n    cursor: pointer;\n    user-select: none;\n  }\n}\n</style>\n"
  },
  {
    "path": "frontend/src/views/nested/menu1/index.vue",
    "content": "<template>\n  <div style=\"padding:30px;\">\n    <el-alert :closable=\"false\" title=\"menu 1\">\n      <router-view />\n    </el-alert>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/nested/menu1/menu1-1/index.vue",
    "content": "<template>\n  <div style=\"padding:30px;\">\n    <el-alert :closable=\"false\" title=\"menu 1-1\" type=\"success\">\n      <router-view />\n    </el-alert>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/nested/menu1/menu1-2/index.vue",
    "content": "<template>\n  <div style=\"padding:30px;\">\n    <el-alert :closable=\"false\" title=\"menu 1-2\" type=\"success\">\n      <router-view />\n    </el-alert>\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/nested/menu1/menu1-2/menu1-2-1/index.vue",
    "content": "<template functional>\n  <div style=\"padding:30px;\">\n    <el-alert :closable=\"false\" title=\"menu 1-2-1\" type=\"warning\" />\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/nested/menu1/menu1-2/menu1-2-2/index.vue",
    "content": "<template functional>\n  <div style=\"padding:30px;\">\n    <el-alert :closable=\"false\" title=\"menu 1-2-2\" type=\"warning\" />\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/nested/menu1/menu1-3/index.vue",
    "content": "<template functional>\n  <div style=\"padding:30px;\">\n    <el-alert :closable=\"false\" title=\"menu 1-3\" type=\"success\" />\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/nested/menu2/index.vue",
    "content": "<template>\n  <div style=\"padding:30px;\">\n    <el-alert :closable=\"false\" title=\"menu 2\" />\n  </div>\n</template>\n"
  },
  {
    "path": "frontend/src/views/table/complex-table.vue",
    "content": "<template>\n  <div class=\"app-container\">\n    <div class=\"filter-container\">\n      <el-input v-model=\"listQuery.title\" placeholder=\"Title\" style=\"width: 200px;\" class=\"filter-item\" @keyup.enter.native=\"handleFilter\" />\n      <el-select v-model=\"listQuery.importance\" placeholder=\"Imp\" clearable style=\"width: 90px\" class=\"filter-item\">\n        <el-option v-for=\"item in importanceOptions\" :key=\"item\" :label=\"item\" :value=\"item\" />\n      </el-select>\n      <el-select v-model=\"listQuery.type\" placeholder=\"Type\" clearable class=\"filter-item\" style=\"width: 130px\">\n        <el-option v-for=\"item in calendarTypeOptions\" :key=\"item.key\" :label=\"item.display_name+'('+item.key+')'\" :value=\"item.key\" />\n      </el-select>\n      <el-select v-model=\"listQuery.sort\" style=\"width: 140px\" class=\"filter-item\" @change=\"handleFilter\">\n        <el-option v-for=\"item in sortOptions\" :key=\"item.key\" :label=\"item.label\" :value=\"item.key\" />\n      </el-select>\n      <el-button v-waves class=\"filter-item\" type=\"primary\" icon=\"el-icon-search\" @click=\"handleFilter\">\n        Search\n      </el-button>\n      <el-button class=\"filter-item\" style=\"margin-left: 10px;\" type=\"primary\" icon=\"el-icon-edit\" @click=\"handleCreate\">\n        Add\n      </el-button>\n      <el-button v-waves :loading=\"downloadLoading\" class=\"filter-item\" type=\"primary\" icon=\"el-icon-download\" @click=\"handleDownload\">\n        Export\n      </el-button>\n      <el-checkbox v-model=\"showReviewer\" class=\"filter-item\" style=\"margin-left:15px;\" @change=\"tableKey=tableKey+1\">\n        reviewer\n      </el-checkbox>\n    </div>\n\n    <el-table\n      :key=\"tableKey\"\n      v-loading=\"listLoading\"\n      :data=\"list\"\n      border\n      fit\n      highlight-current-row\n      style=\"width: 100%;\"\n      @sort-change=\"sortChange\"\n    >\n      <el-table-column label=\"ID\" prop=\"id\" sortable=\"custom\" align=\"center\" width=\"80\" :class-name=\"getSortClass('id')\">\n        <template slot-scope=\"{row}\">\n          <span>{{ row.id }}</span>\n        </template>\n      </el-table-column>\n      <el-table-column label=\"Date\" width=\"150px\" align=\"center\">\n        <template slot-scope=\"{row}\">\n          <span>{{ row.timestamp | parseTime('{y}-{m}-{d} {h}:{i}') }}</span>\n        </template>\n      </el-table-column>\n      <el-table-column label=\"Title\" min-width=\"150px\">\n        <template slot-scope=\"{row}\">\n          <span class=\"link-type\" @click=\"handleUpdate(row)\">{{ row.title }}</span>\n          <el-tag>{{ row.type | typeFilter }}</el-tag>\n        </template>\n      </el-table-column>\n      <el-table-column label=\"Author\" width=\"110px\" align=\"center\">\n        <template slot-scope=\"{row}\">\n          <span>{{ row.author }}</span>\n        </template>\n      </el-table-column>\n      <el-table-column v-if=\"showReviewer\" label=\"Reviewer\" width=\"110px\" align=\"center\">\n        <template slot-scope=\"{row}\">\n          <span style=\"color:red;\">{{ row.reviewer }}</span>\n        </template>\n      </el-table-column>\n      <el-table-column label=\"Imp\" width=\"80px\">\n        <template slot-scope=\"{row}\">\n          <svg-icon v-for=\"n in + row.importance\" :key=\"n\" icon-class=\"star\" class=\"meta-item__icon\" />\n        </template>\n      </el-table-column>\n      <el-table-column label=\"Readings\" align=\"center\" width=\"95\">\n        <template slot-scope=\"{row}\">\n          <span v-if=\"row.pageviews\" class=\"link-type\" @click=\"handleFetchPv(row.pageviews)\">{{ row.pageviews }}</span>\n          <span v-else>0</span>\n        </template>\n      </el-table-column>\n      <el-table-column label=\"Status\" class-name=\"status-col\" width=\"100\">\n        <template slot-scope=\"{row}\">\n          <el-tag :type=\"row.status | statusFilter\">\n            {{ row.status }}\n          </el-tag>\n        </template>\n      </el-table-column>\n      <el-table-column label=\"Actions\" align=\"center\" width=\"230\" class-name=\"small-padding fixed-width\">\n        <template slot-scope=\"{row,$index}\">\n          <el-button type=\"primary\" size=\"mini\" @click=\"handleUpdate(row)\">\n            Edit\n          </el-button>\n          <el-button v-if=\"row.status!='published'\" size=\"mini\" type=\"success\" @click=\"handleModifyStatus(row,'published')\">\n            Publish\n          </el-button>\n          <el-button v-if=\"row.status!='draft'\" size=\"mini\" @click=\"handleModifyStatus(row,'draft')\">\n            Draft\n          </el-button>\n          <el-button v-if=\"row.status!='deleted'\" size=\"mini\" type=\"danger\" @click=\"handleDelete(row,$index)\">\n            Delete\n          </el-button>\n        </template>\n      </el-table-column>\n    </el-table>\n\n    <pagination v-show=\"total>0\" :total=\"total\" :page.sync=\"listQuery.page\" :limit.sync=\"listQuery.limit\" @pagination=\"getList\" />\n\n    <el-dialog :title=\"textMap[dialogStatus]\" :visible.sync=\"dialogFormVisible\">\n      <el-form ref=\"dataForm\" :rules=\"rules\" :model=\"temp\" label-position=\"left\" label-width=\"70px\" style=\"width: 400px; margin-left:50px;\">\n        <el-form-item label=\"Type\" prop=\"type\">\n          <el-select v-model=\"temp.type\" class=\"filter-item\" placeholder=\"Please select\">\n            <el-option v-for=\"item in calendarTypeOptions\" :key=\"item.key\" :label=\"item.display_name\" :value=\"item.key\" />\n          </el-select>\n        </el-form-item>\n        <el-form-item label=\"Date\" prop=\"timestamp\">\n          <el-date-picker v-model=\"temp.timestamp\" type=\"datetime\" placeholder=\"Please pick a date\" />\n        </el-form-item>\n        <el-form-item label=\"Title\" prop=\"title\">\n          <el-input v-model=\"temp.title\" />\n        </el-form-item>\n        <el-form-item label=\"Status\">\n          <el-select v-model=\"temp.status\" class=\"filter-item\" placeholder=\"Please select\">\n            <el-option v-for=\"item in statusOptions\" :key=\"item\" :label=\"item\" :value=\"item\" />\n          </el-select>\n        </el-form-item>\n        <el-form-item label=\"Imp\">\n          <el-rate v-model=\"temp.importance\" :colors=\"['#99A9BF', '#F7BA2A', '#FF9900']\" :max=\"3\" style=\"margin-top:8px;\" />\n        </el-form-item>\n        <el-form-item label=\"Remark\">\n          <el-input v-model=\"temp.remark\" :autosize=\"{ minRows: 2, maxRows: 4}\" type=\"textarea\" placeholder=\"Please input\" />\n        </el-form-item>\n      </el-form>\n      <div slot=\"footer\" class=\"dialog-footer\">\n        <el-button @click=\"dialogFormVisible = false\">\n          Cancel\n        </el-button>\n        <el-button type=\"primary\" @click=\"dialogStatus==='create'?createData():updateData()\">\n          Confirm\n        </el-button>\n      </div>\n    </el-dialog>\n\n    <el-dialog :visible.sync=\"dialogPvVisible\" title=\"Reading statistics\">\n      <el-table :data=\"pvData\" border fit highlight-current-row style=\"width: 100%\">\n        <el-table-column prop=\"key\" label=\"Channel\" />\n        <el-table-column prop=\"pv\" label=\"Pv\" />\n      </el-table>\n      <span slot=\"footer\" class=\"dialog-footer\">\n        <el-button type=\"primary\" @click=\"dialogPvVisible = false\">Confirm</el-button>\n      </span>\n    </el-dialog>\n  </div>\n</template>\n\n<script>\nimport { fetchList, fetchPv, createArticle, updateArticle } from '@/api/article'\nimport waves from '@/directive/waves' // waves directive\nimport { parseTime } from '@/utils'\nimport Pagination from '@/components/Pagination' // secondary package based on el-pagination\n\nconst calendarTypeOptions = [\n  { key: 'CN', display_name: 'China' },\n  { key: 'US', display_name: 'USA' },\n  { key: 'JP', display_name: 'Japan' },\n  { key: 'EU', display_name: 'Eurozone' }\n]\n\n// arr to obj, such as { CN : \"China\", US : \"USA\" }\nconst calendarTypeKeyValue = calendarTypeOptions.reduce((acc, cur) => {\n  acc[cur.key] = cur.display_name\n  return acc\n}, {})\n\nexport default {\n  name: 'ComplexTable',\n  components: { Pagination },\n  directives: { waves },\n  filters: {\n    statusFilter(status) {\n      const statusMap = {\n        published: 'success',\n        draft: 'info',\n        deleted: 'danger'\n      }\n      return statusMap[status]\n    },\n    typeFilter(type) {\n      return calendarTypeKeyValue[type]\n    }\n  },\n  data() {\n    return {\n      tableKey: 0,\n      list: null,\n      total: 0,\n      listLoading: true,\n      listQuery: {\n        page: 1,\n        limit: 20,\n        importance: undefined,\n        title: undefined,\n        type: undefined,\n        sort: '+id'\n      },\n      importanceOptions: [1, 2, 3],\n      calendarTypeOptions,\n      sortOptions: [{ label: 'ID Ascending', key: '+id' }, { label: 'ID Descending', key: '-id' }],\n      statusOptions: ['published', 'draft', 'deleted'],\n      showReviewer: false,\n      temp: {\n        id: undefined,\n        importance: 1,\n        remark: '',\n        timestamp: new Date(),\n        title: '',\n        type: '',\n        status: 'published'\n      },\n      dialogFormVisible: false,\n      dialogStatus: '',\n      textMap: {\n        update: 'Edit',\n        create: 'Create'\n      },\n      dialogPvVisible: false,\n      pvData: [],\n      rules: {\n        type: [{ required: true, message: 'type is required', trigger: 'change' }],\n        timestamp: [{ type: 'date', required: true, message: 'timestamp is required', trigger: 'change' }],\n        title: [{ required: true, message: 'title is required', trigger: 'blur' }]\n      },\n      downloadLoading: false\n    }\n  },\n  created() {\n    this.getList()\n  },\n  methods: {\n    getList() {\n      this.listLoading = true\n      fetchList(this.listQuery).then(response => {\n        this.list = response.data.items\n        this.total = response.data.total\n\n        // Just to simulate the time of the request\n        setTimeout(() => {\n          this.listLoading = false\n        }, 1.5 * 1000)\n      })\n    },\n    handleFilter() {\n      this.listQuery.page = 1\n      this.getList()\n    },\n    handleModifyStatus(row, status) {\n      this.$message({\n        message: '操作Success',\n        type: 'success'\n      })\n      row.status = status\n    },\n    sortChange(data) {\n      const { prop, order } = data\n      if (prop === 'id') {\n        this.sortByID(order)\n      }\n    },\n    sortByID(order) {\n      if (order === 'ascending') {\n        this.listQuery.sort = '+id'\n      } else {\n        this.listQuery.sort = '-id'\n      }\n      this.handleFilter()\n    },\n    resetTemp() {\n      this.temp = {\n        id: undefined,\n        importance: 1,\n        remark: '',\n        timestamp: new Date(),\n        title: '',\n        status: 'published',\n        type: ''\n      }\n    },\n    handleCreate() {\n      this.resetTemp()\n      this.dialogStatus = 'create'\n      this.dialogFormVisible = true\n      this.$nextTick(() => {\n        this.$refs['dataForm'].clearValidate()\n      })\n    },\n    createData() {\n      this.$refs['dataForm'].validate((valid) => {\n        if (valid) {\n          this.temp.id = parseInt(Math.random() * 100) + 1024 // mock a id\n          this.temp.author = 'vue-element-admin'\n          createArticle(this.temp).then(() => {\n            this.list.unshift(this.temp)\n            this.dialogFormVisible = false\n            this.$notify({\n              title: 'Success',\n              message: 'Created Successfully',\n              type: 'success',\n              duration: 2000\n            })\n          })\n        }\n      })\n    },\n    handleUpdate(row) {\n      this.temp = Object.assign({}, row) // copy obj\n      this.temp.timestamp = new Date(this.temp.timestamp)\n      this.dialogStatus = 'update'\n      this.dialogFormVisible = true\n      this.$nextTick(() => {\n        this.$refs['dataForm'].clearValidate()\n      })\n    },\n    updateData() {\n      this.$refs['dataForm'].validate((valid) => {\n        if (valid) {\n          const tempData = Object.assign({}, this.temp)\n          tempData.timestamp = +new Date(tempData.timestamp) // change Thu Nov 30 2017 16:41:05 GMT+0800 (CST) to 1512031311464\n          updateArticle(tempData).then(() => {\n            const index = this.list.findIndex(v => v.id === this.temp.id)\n            this.list.splice(index, 1, this.temp)\n            this.dialogFormVisible = false\n            this.$notify({\n              title: 'Success',\n              message: 'Update Successfully',\n              type: 'success',\n              duration: 2000\n            })\n          })\n        }\n      })\n    },\n    handleDelete(row, index) {\n      this.$notify({\n        title: 'Success',\n        message: 'Delete Successfully',\n        type: 'success',\n        duration: 2000\n      })\n      this.list.splice(index, 1)\n    },\n    handleFetchPv(pv) {\n      fetchPv(pv).then(response => {\n        this.pvData = response.data.pvData\n        this.dialogPvVisible = true\n      })\n    },\n    handleDownload() {\n      this.downloadLoading = true\n      import('@/vendor/Export2Excel').then(excel => {\n        const tHeader = ['timestamp', 'title', 'type', 'importance', 'status']\n        const filterVal = ['timestamp', 'title', 'type', 'importance', 'status']\n        const data = this.formatJson(filterVal)\n        excel.export_json_to_excel({\n          header: tHeader,\n          data,\n          filename: 'table-list'\n        })\n        this.downloadLoading = false\n      })\n    },\n    formatJson(filterVal) {\n      return this.list.map(v => filterVal.map(j => {\n        if (j === 'timestamp') {\n          return parseTime(v[j])\n        } else {\n          return v[j]\n        }\n      }))\n    },\n    getSortClass: function(key) {\n      const sort = this.listQuery.sort\n      return sort === `+${key}` ? 'ascending' : 'descending'\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "frontend/src/views/table/index.vue",
    "content": "<template>\n  <div class=\"app-container\">\n\n    <div class=\"filter-container\">\n      <el-date-picker\n      v-model=\"queryDate\" type=\"date\"  format=\"yyyyMMdd\"\n                value-format=\"yyyyMMdd\"\n      placeholder=\"选择日期\">\n\n    </el-date-picker>\n      <el-input v-model=\"queryCode\" placeholder=\"code\" \n      style=\"width: 200px;\" class=\"filter-item\" @keyup.enter.native=\"handleFilter\" />\n     \n      <el-button v-waves class=\"filter-item\" type=\"primary\" icon=\"el-icon-search\" @click=\"handleFilter\">\n        搜索\n      </el-button>\n \n    </div>\n\n    <el-table :key=\"tableKey\" v-loading=\"listLoading\" :data=\"list\"\n      stripe border fit fixed highlight-current-row style=\"width: 100%;\" \n      @sort-change=\"sortChange\"\n    >\n\n      <!-- \n        columns=['date','code','name','latest_price','quote_change','ups_downs','volume','turnover',\n                 'amplitude','high','low','open','closed','quantity_ratio','turnover_rate','pe_dynamic','pb'],\n        column_names=['日期','代码','名称','最新价','涨跌幅','涨跌额','成交量','成交额',\n                      '振幅','最高','最低','今开','昨收','量比','换手率','动态市盈率','市净率'],\n        -->\n\n        <el-table-column sortable v-for=\"column in tableColumns\" :key=\"column.column\" \n          :label=\"column.columnName\" :prop=\"column.column\" align=\"center\" width=\"120\"/>\n\n\n      <el-table-column  fixed=\"right\" label=\"操作\" align=\"center\" width=\"230\" class-name=\"small-padding fixed-width\">\n        <template slot-scope=\"{row,$index}\">\n          <el-button type=\"primary\" size=\"mini\" @click=\"handleView(row,$index)\">\n            查看\n          </el-button>\n          <!-- <el-button v-if=\"row.code!='published'\" size=\"mini\" type=\"success\" @click=\"handleModifyStatus(row,'published')\">\n            Publish\n          </el-button> -->\n\n        </template>\n      </el-table-column>\n\n    </el-table>\n\n    <!--\n    https://element.eleme.cn/#/zh-CN/component/pagination#slot\n    文档设置分页数据：\n    -->\n    <pagination v-show=\"total>0\" :total=\"total\" \n      :page-sizes=\"[10, 20, 50, 100, 200, 300]\"\n      :page.sync=\"listQuery.page\" :limit.sync=\"listQuery.limit\" \n      @pagination=\"getList\"/>\n\n     \n  </div>\n</template>\n\n<script>\nimport { fetchList, fetchPv, createArticle, updateArticle } from '@/api/article'\nimport waves from '@/directive/waves' // waves directive\nimport { parseTime } from '@/utils'\nimport Pagination from '@/components/Pagination' // secondary package based on el-pagination\n\nconst calendarTypeOptions = [\n  { key: 'CN', display_name: 'China' },\n  { key: 'US', display_name: 'USA' },\n  { key: 'JP', display_name: 'Japan' },\n  { key: 'EU', display_name: 'Eurozone' }\n]\n\n// arr to obj, such as { CN : \"China\", US : \"USA\" }\nconst calendarTypeKeyValue = calendarTypeOptions.reduce((acc, cur) => {\n  acc[cur.key] = cur.display_name\n  return acc\n}, {})\n\nexport default {\n  name: 'ComplexTable',\n  components: { Pagination },\n  directives: { waves },\n  filters: {\n    statusFilter(status) {\n      const statusMap = {\n        published: 'success',\n        draft: 'info',\n        deleted: 'danger'\n      }\n      return statusMap[status]\n    },\n    typeFilter(type) {\n      return calendarTypeKeyValue[type]\n    }\n  },\n  data() {\n    return {\n      tableKey: 0,\n      list: [],\n      tableColumns: [],\n      total: 0,\n      listLoading: true,\n      listQuery: {\n        page: 1,\n        limit: 10,\n        importance: undefined,\n        date: undefined,\n        code: undefined,\n        name: '',\n        sort: '+id'\n      },\n      calendarTypeOptions,\n\n      dialogFormVisible: false,\n \n      queryDate: '',\n      queryCode: ''\n\n    }\n  },\n  created() {\n    this.getList()\n  },\n  methods: {\n    getList() {\n\n      console.info(\"$router.path:\", this.$route.path)\n      let table_name = this.$route.path.replace(\"/stock/table/\",\"\")\n      console.info(table_name == ':tableName')\n      if(table_name == ':tableName'){\n        this.list=[]\n        this.total=0\n        this.listLoading = false\n        return\n      }\n      console.info(\"$table_name:\", table_name)\n      this.listQuery.name =  table_name\n\n      this.listQuery.date =  this.queryDate//this.$formatDate(this.queryDate, 'yyyyMMdd')\n      this.listQuery.code =  this.queryCode\n      this.listLoading = true\n      fetchList(this.listQuery).then(response => {\n        this.list = response.data\n        this.tableColumns = response.tableColumns\n        this.total = response.total\n        // Just to simulate the time of the request\n        setTimeout(() => {\n          this.listLoading = false\n        }, 1.5 * 1000)\n      })\n    },\n    handleFilter() {\n      this.listQuery.page = 1\n      this.getList()\n    },\n    handleModifyStatus(row, status) {\n      this.$message({\n        message: '操作Success',\n        type: 'success'\n      })\n      row.status = status\n    },\n    sortChange(data) {\n      const { prop, order } = data\n      if (prop === 'id') {\n        this.sortByID(order)\n      }\n    },\n    sortByID(order) {\n      if (order === 'ascending') {\n        this.listQuery.sort = '+id'\n      } else {\n        this.listQuery.sort = '-id'\n      }\n      this.handleFilter()\n    },\n    handleView(row, index) {\n      this.$notify({\n        title: 'Success',\n        message: 'Delete Successfully'+row['code'],\n        type: 'success',\n        duration: 2000\n      })\n      //http://quote.eastmoney.com/%s.html\n      const url = 'http://quote.eastmoney.com/'+row['code']+'.html'\n      window.open(url, '_blank')\n      this.list.splice(index, 1)\n    },\n\n    getSortClass: function(key) {\n      const sort = this.listQuery.sort\n      return sort === `+${key}` ? 'ascending' : 'descending'\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "frontend/src/views/tree/index.vue",
    "content": "<template>\n  <div class=\"app-container\">\n    <el-input v-model=\"filterText\" placeholder=\"Filter keyword\" style=\"margin-bottom:30px;\" />\n\n    <el-tree\n      ref=\"tree2\"\n      :data=\"data2\"\n      :props=\"defaultProps\"\n      :filter-node-method=\"filterNode\"\n      class=\"filter-tree\"\n      default-expand-all\n    />\n\n  </div>\n</template>\n\n<script>\nexport default {\n\n  data() {\n    return {\n      filterText: '',\n      data2: [{\n        id: 1,\n        label: 'Level one 1',\n        children: [{\n          id: 4,\n          label: 'Level two 1-1',\n          children: [{\n            id: 9,\n            label: 'Level three 1-1-1'\n          }, {\n            id: 10,\n            label: 'Level three 1-1-2'\n          }]\n        }]\n      }, {\n        id: 2,\n        label: 'Level one 2',\n        children: [{\n          id: 5,\n          label: 'Level two 2-1'\n        }, {\n          id: 6,\n          label: 'Level two 2-2'\n        }]\n      }, {\n        id: 3,\n        label: 'Level one 3',\n        children: [{\n          id: 7,\n          label: 'Level two 3-1'\n        }, {\n          id: 8,\n          label: 'Level two 3-2'\n        }]\n      }],\n      defaultProps: {\n        children: 'children',\n        label: 'label'\n      }\n    }\n  },\n  watch: {\n    filterText(val) {\n      this.$refs.tree2.filter(val)\n    }\n  },\n\n  methods: {\n    filterNode(value, data) {\n      if (!value) return true\n      return data.label.indexOf(value) !== -1\n    }\n  }\n}\n</script>\n\n"
  },
  {
    "path": "frontend/tests/unit/.eslintrc.js",
    "content": "module.exports = {\n  env: {\n    jest: true\n  }\n}\n"
  },
  {
    "path": "frontend/tests/unit/components/Breadcrumb.spec.js",
    "content": "import { mount, createLocalVue } from '@vue/test-utils'\nimport VueRouter from 'vue-router'\nimport ElementUI from 'element-ui'\nimport Breadcrumb from '@/components/Breadcrumb/index.vue'\n\nconst localVue = createLocalVue()\nlocalVue.use(VueRouter)\nlocalVue.use(ElementUI)\n\nconst routes = [\n  {\n    path: '/',\n    name: 'home',\n    children: [{\n      path: 'dashboard',\n      name: 'dashboard'\n    }]\n  },\n  {\n    path: '/menu',\n    name: 'menu',\n    children: [{\n      path: 'menu1',\n      name: 'menu1',\n      meta: { title: 'menu1' },\n      children: [{\n        path: 'menu1-1',\n        name: 'menu1-1',\n        meta: { title: 'menu1-1' }\n      },\n      {\n        path: 'menu1-2',\n        name: 'menu1-2',\n        redirect: 'noredirect',\n        meta: { title: 'menu1-2' },\n        children: [{\n          path: 'menu1-2-1',\n          name: 'menu1-2-1',\n          meta: { title: 'menu1-2-1' }\n        },\n        {\n          path: 'menu1-2-2',\n          name: 'menu1-2-2'\n        }]\n      }]\n    }]\n  }]\n\nconst router = new VueRouter({\n  routes\n})\n\ndescribe('Breadcrumb.vue', () => {\n  const wrapper = mount(Breadcrumb, {\n    localVue,\n    router\n  })\n  it('dashboard', () => {\n    router.push('/dashboard')\n    const len = wrapper.findAll('.el-breadcrumb__inner').length\n    expect(len).toBe(1)\n  })\n  it('normal route', () => {\n    router.push('/menu/menu1')\n    const len = wrapper.findAll('.el-breadcrumb__inner').length\n    expect(len).toBe(2)\n  })\n  it('nested route', () => {\n    router.push('/menu/menu1/menu1-2/menu1-2-1')\n    const len = wrapper.findAll('.el-breadcrumb__inner').length\n    expect(len).toBe(4)\n  })\n  it('no meta.title', () => {\n    router.push('/menu/menu1/menu1-2/menu1-2-2')\n    const len = wrapper.findAll('.el-breadcrumb__inner').length\n    expect(len).toBe(3)\n  })\n  // it('click link', () => {\n  //   router.push('/menu/menu1/menu1-2/menu1-2-2')\n  //   const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')\n  //   const second = breadcrumbArray.at(1)\n  //   console.log(breadcrumbArray)\n  //   const href = second.find('a').attributes().href\n  //   expect(href).toBe('#/menu/menu1')\n  // })\n  // it('noRedirect', () => {\n  //   router.push('/menu/menu1/menu1-2/menu1-2-1')\n  //   const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')\n  //   const redirectBreadcrumb = breadcrumbArray.at(2)\n  //   expect(redirectBreadcrumb.contains('a')).toBe(false)\n  // })\n  it('last breadcrumb', () => {\n    router.push('/menu/menu1/menu1-2/menu1-2-1')\n    const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')\n    const redirectBreadcrumb = breadcrumbArray.at(3)\n    expect(redirectBreadcrumb.contains('a')).toBe(false)\n  })\n})\n"
  },
  {
    "path": "frontend/tests/unit/components/Hamburger.spec.js",
    "content": "import { shallowMount } from '@vue/test-utils'\nimport Hamburger from '@/components/Hamburger/index.vue'\ndescribe('Hamburger.vue', () => {\n  it('toggle click', () => {\n    const wrapper = shallowMount(Hamburger)\n    const mockFn = jest.fn()\n    wrapper.vm.$on('toggleClick', mockFn)\n    wrapper.find('.hamburger').trigger('click')\n    expect(mockFn).toBeCalled()\n  })\n  it('prop isActive', () => {\n    const wrapper = shallowMount(Hamburger)\n    wrapper.setProps({ isActive: true })\n    expect(wrapper.contains('.is-active')).toBe(true)\n    wrapper.setProps({ isActive: false })\n    expect(wrapper.contains('.is-active')).toBe(false)\n  })\n})\n"
  },
  {
    "path": "frontend/tests/unit/components/SvgIcon.spec.js",
    "content": "import { shallowMount } from '@vue/test-utils'\nimport SvgIcon from '@/components/SvgIcon/index.vue'\ndescribe('SvgIcon.vue', () => {\n  it('iconClass', () => {\n    const wrapper = shallowMount(SvgIcon, {\n      propsData: {\n        iconClass: 'test'\n      }\n    })\n    expect(wrapper.find('use').attributes().href).toBe('#icon-test')\n  })\n  it('className', () => {\n    const wrapper = shallowMount(SvgIcon, {\n      propsData: {\n        iconClass: 'test'\n      }\n    })\n    expect(wrapper.classes().length).toBe(1)\n    wrapper.setProps({ className: 'test' })\n    expect(wrapper.classes().includes('test')).toBe(true)\n  })\n})\n"
  },
  {
    "path": "frontend/tests/unit/utils/formatTime.spec.js",
    "content": "import { formatTime } from '@/utils/index.js'\n\ndescribe('Utils:formatTime', () => {\n  const d = new Date('2018-07-13 17:54:01') // \"2018-07-13 17:54:01\"\n  const retrofit = 5 * 1000\n\n  it('ten digits timestamp', () => {\n    expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分')\n  })\n  it('test now', () => {\n    expect(formatTime(+new Date() - 1)).toBe('刚刚')\n  })\n  it('less two minute', () => {\n    expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前')\n  })\n  it('less two hour', () => {\n    expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前')\n  })\n  it('less one day', () => {\n    expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前')\n  })\n  it('more than one day', () => {\n    expect(formatTime(d)).toBe('7月13日17时54分')\n  })\n  it('format', () => {\n    expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')\n    expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')\n    expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')\n  })\n})\n"
  },
  {
    "path": "frontend/tests/unit/utils/param2Obj.spec.js",
    "content": "import { param2Obj } from '@/utils/index.js'\ndescribe('Utils:param2Obj', () => {\n  const url = 'https://github.com/PanJiaChen/vue-element-admin?name=bill&age=29&sex=1&field=dGVzdA==&key=%E6%B5%8B%E8%AF%95'\n\n  it('param2Obj test', () => {\n    expect(param2Obj(url)).toEqual({\n      name: 'bill',\n      age: '29',\n      sex: '1',\n      field: window.btoa('test'),\n      key: '测试'\n    })\n  })\n})\n"
  },
  {
    "path": "frontend/tests/unit/utils/parseTime.spec.js",
    "content": "import { parseTime } from '@/utils/index.js'\n\ndescribe('Utils:parseTime', () => {\n  const d = new Date('2018-07-13 17:54:01') // \"2018-07-13 17:54:01\"\n  it('timestamp', () => {\n    expect(parseTime(d)).toBe('2018-07-13 17:54:01')\n  })\n  it('timestamp string', () => {\n    expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01')\n  })\n  it('ten digits timestamp', () => {\n    expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01')\n  })\n  it('new Date', () => {\n    expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01')\n  })\n  it('format', () => {\n    expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')\n    expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')\n    expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')\n  })\n  it('get the day of the week', () => {\n    expect(parseTime(d, '{a}')).toBe('五') // 星期五\n  })\n  it('get the day of the week', () => {\n    expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日\n  })\n  it('empty argument', () => {\n    expect(parseTime()).toBeNull()\n  })\n\n  it('null', () => {\n    expect(parseTime(null)).toBeNull()\n  })\n})\n"
  },
  {
    "path": "frontend/tests/unit/utils/validate.spec.js",
    "content": "import { validUsername, isExternal } from '@/utils/validate.js'\n\ndescribe('Utils:validate', () => {\n  it('validUsername', () => {\n    expect(validUsername('admin')).toBe(true)\n    expect(validUsername('editor')).toBe(true)\n    expect(validUsername('xxxx')).toBe(false)\n  })\n  it('isExternal', () => {\n    expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true)\n    expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true)\n    expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false)\n    expect(isExternal('/dashboard')).toBe(false)\n    expect(isExternal('./dashboard')).toBe(false)\n    expect(isExternal('dashboard')).toBe(false)\n  })\n})\n"
  },
  {
    "path": "frontend/vue.config.js",
    "content": "'use strict'\nconst path = require('path')\nconst defaultSettings = require('./src/settings.js')\n\nfunction resolve(dir) {\n  return path.join(__dirname, dir)\n}\n\nconst name = defaultSettings.title || 'vue Admin Template' // page title\n\n// If your port is set to 80,\n// use administrator privileges to execute the command line.\n// For example, Mac: sudo npm run\n// You can change the port by the following methods:\n// port = 9528 npm run dev OR npm run dev --port = 9528\nconst port = process.env.port || process.env.npm_config_port || 9528 // dev port\n\n// All configuration item explanations can be find in https://cli.vuejs.org/config/\nmodule.exports = {\n  /**\n   * You will need to set publicPath if you plan to deploy your site under a sub path,\n   * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,\n   * then publicPath should be set to \"/bar/\".\n   * In most cases please use '/' !!!\n   * Detail: https://cli.vuejs.org/config/#publicpath\n   */\n  publicPath: '/',\n  outputDir: 'dist',\n  assetsDir: 'static',\n  lintOnSave: process.env.NODE_ENV === 'development',\n  productionSourceMap: false,\n  devServer: {\n    host: '0.0.0.0',\n    port: 8080,\n    proxy: {\n      '/api/v1': {\n        target: 'http://backend:9090/api/v1',\n        changeOrigin: true,\n        pathRewrite: {\n          '^/api/v1': ''\n        }\n      }\n    }\n  },\n  configureWebpack: {\n    // provide the app's title in webpack's name field, so that\n    // it can be accessed in index.html to inject the correct title.\n    name: name,\n    resolve: {\n      alias: {\n        '@': resolve('src')\n      }\n    }\n  },\n  chainWebpack(config) {\n    // it can improve the speed of the first screen, it is recommended to turn on preload\n    config.plugin('preload').tap(() => [\n      {\n        rel: 'preload',\n        // to ignore runtime.js\n        // https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171\n        fileBlacklist: [/\\.map$/, /hot-update\\.js$/, /runtime\\..*\\.js$/],\n        include: 'initial'\n      }\n    ])\n\n    // when there are many pages, it will cause too many meaningless requests\n    config.plugins.delete('prefetch')\n\n    // set svg-sprite-loader\n    config.module\n      .rule('svg')\n      .exclude.add(resolve('src/icons'))\n      .end()\n    config.module\n      .rule('icons')\n      .test(/\\.svg$/)\n      .include.add(resolve('src/icons'))\n      .end()\n      .use('svg-sprite-loader')\n      .loader('svg-sprite-loader')\n      .options({\n        symbolId: 'icon-[name]'\n      })\n      .end()\n\n    config\n      .when(process.env.NODE_ENV !== 'development',\n        config => {\n          config\n            .plugin('ScriptExtHtmlWebpackPlugin')\n            .after('html')\n            .use('script-ext-html-webpack-plugin', [{\n            // `runtime` must same as runtimeChunk name. default is `runtime`\n              inline: /runtime\\..*\\.js$/\n            }])\n            .end()\n          config\n            .optimization.splitChunks({\n              chunks: 'all',\n              cacheGroups: {\n                libs: {\n                  name: 'chunk-libs',\n                  test: /[\\\\/]node_modules[\\\\/]/,\n                  priority: 10,\n                  chunks: 'initial' // only package third parties that are initially dependent\n                },\n                elementUI: {\n                  name: 'chunk-elementUI', // split elementUI into a single package\n                  priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app\n                  test: /[\\\\/]node_modules[\\\\/]_?element-ui(.*)/ // in order to adapt to cnpm\n                },\n                commons: {\n                  name: 'chunk-commons',\n                  test: resolve('src/components'), // can customize your rules\n                  minChunks: 3, //  minimum common number\n                  priority: 5,\n                  reuseExistingChunk: true\n                }\n              }\n            })\n          // https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk\n          config.optimization.runtimeChunk('single')\n        }\n      )\n  }\n}\n"
  }
]