From 35ae37867f03da7ce0ffcb449e51eb78c2dd3b51 Mon Sep 17 00:00:00 2001 From: Charmve Date: Sat, 16 Sep 2023 17:28:21 +0800 Subject: [PATCH] Update utils and strategies --- .github/workflows/greetings.yml | 15 + .vscode/launch.json | 58 ++-- .vscode/settings.json | 71 ++++- README.md | 14 +- WORKSPACE | 27 ++ docs/README.md | 15 +- gui/imgs/UFund.png | Bin 13107 -> 12467 bytes gui/panels/panel_backtest.py | 6 +- pytrader/data/data_utils.py | 35 ++- .../easyquant/strategy/strategyTemplate.py | 2 +- pytrader/strategies/lgb_strategy.py | 163 ++++++++++- setup.py => qbot/setup.py | 0 qbot/strategies/adx_strategy.py | 64 +++++ qbot/strategies/get_stack_data.py | 84 ++++++ qbot/strategies/klines_bt.py | 106 +++++++ qbot/strategies/util.py | 96 +++++++ utils/common/AShareDailyData.py | 246 ++++++++++++++++ utils/common/BaseService.py | 271 ++++++++++++++++++ utils/common/TuShare.py | 235 +++++++++++++++ utils/common/__init__.py | 0 utils/common/utils.py | 36 +++ utils/configure/ util.py | 237 +++++++++++++++ utils/configure/__init__.py | 0 utils/configure/config.json | 99 +++++++ utils/configure/sample_config.json | 99 +++++++ utils/configure/settings.py | 104 +++++++ utils/thsauto | 1 + 27 files changed, 2036 insertions(+), 48 deletions(-) create mode 100644 WORKSPACE rename setup.py => qbot/setup.py (100%) create mode 100644 qbot/strategies/adx_strategy.py create mode 100644 qbot/strategies/get_stack_data.py create mode 100644 qbot/strategies/klines_bt.py create mode 100644 qbot/strategies/util.py create mode 100644 utils/common/AShareDailyData.py create mode 100644 utils/common/BaseService.py create mode 100644 utils/common/TuShare.py create mode 100644 utils/common/__init__.py create mode 100644 utils/common/utils.py create mode 100644 utils/configure/ util.py create mode 100644 utils/configure/__init__.py create mode 100644 utils/configure/config.json create mode 100644 utils/configure/sample_config.json create mode 100644 utils/configure/settings.py create mode 160000 utils/thsauto diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml index 67a2827..bbb18ea 100644 --- a/.github/workflows/greetings.yml +++ b/.github/workflows/greetings.yml @@ -25,3 +25,18 @@ jobs: with: repo-token: ${{ secrets.GITHUB_TOKEN }} favorite-image: 'https://your.favorite/image1.png,https://your.favorite/image2.png' + + post-lgtm-image: + runs-on: ubuntu-latest + if: (!contains(github.actor, '[bot]')) # Exclude bot comment + steps: + - uses: ddradar/choose-random-action@v2 + id: act + with: + contents: | + https://example.com/your-lgtm-image-1.jpg + https://example.com/your-lgtm-image-2.jpg + https://example.com/your-lgtm-image-3.jpg + - uses: ddradar/lgtm-action@v2.0.2 + with: + image-url: ${{ steps.act.outputs.selected }} diff --git a/.vscode/launch.json b/.vscode/launch.json index d683a63..ded41c6 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,22 +1,38 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Launch fund", - "args": [ - "${workspaceFolder}/pyfunds/fund-strategy/src/utils/fund-stragegy/index.ts" - ], - "runtimeArgs": [ - "-r", - "ts-node/register", - "-r", "tsconfig-paths/register" - ], - "env": { "TS_NODE_PROJECT": "${workspaceFolder}/tsconfig.json" } - }, - ] - } \ No newline at end of file + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Qbot", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "justMyCode": true + }, + { + "name": "Python: Qbot", + "type": "python", + "request": "launch", + "cwd": "${workspaceRoot}", + }, + { + "type": "node", + "request": "launch", + "name": "Launch fund", + "args": [ + "${workspaceFolder}/pyfunds/fund-strategy/src/utils/fund-stragegy/index.ts" + ], + "runtimeArgs": [ + "-r", + "ts-node/register", + "-r", "tsconfig-paths/register" + ], + "env": { + "TS_NODE_PROJECT": "${workspaceFolder}/tsconfig.json" + } + }, + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 8b13789..3859038 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1 +1,70 @@ - +// Place your settings in this file to overwrite default and user settings. +{ + "files.exclude": { + "out": true, // set this to true to hide the "out" folder with the compiled JS files + "dist": true, + "**/*.pyc": true, + ".nyc_output": true, + "obj": true, + "bin": true, + "**/__pycache__": true, + "**/node_modules": true, + ".vscode-test": false, + ".vscode test": false, + "**/.mypy_cache/**": true + }, + "search.exclude": { + "out": true, // set this to false to include "out" folder in search results + "dist": true, + "**/node_modules": true, + "coverage": true, + "languageServer*/**": true, + ".vscode-test": true, + ".vscode test": true + }, + "[python]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.organizeImports": true + } + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[JSON]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "[YAML]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true + }, + "typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version + "python.linting.enabled": false, + "python.pythonPath": "/usr/local/anaconda3/bin/python", + "python.formatting.provider": "black", + "python.sortImports.args": ["--profile", "black"], + "typescript.preferences.quoteStyle": "single", + "javascript.preferences.quoteStyle": "single", + "typescriptHero.imports.stringQuoteStyle": "'", + "prettier.printWidth": 120, + "prettier.singleQuote": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "python.languageServer": "Default", + "python.linting.pylintEnabled": false, + "python.linting.flake8Enabled": true, + "cucumberautocomplete.skipDocStringsFormat": true, + "python.linting.flake8Args": [ + // Match what black does. + "--max-line-length=88" + ], + "typescript.preferences.importModuleSpecifier": "relative", + "debug.javascript.usePreview": false +} \ No newline at end of file diff --git a/README.md b/README.md index b071021..eb46e74 100644 --- a/README.md +++ b/README.md @@ -64,9 +64,9 @@ ## Quick Start -Qbot是一个免费的投研平台,提供从数据获取、交易策略开发、策略回测、模拟交易到最终实盘交易的全闭环流程。在实盘接入前,有股票、基金评测和策略回测,在模拟环境下做交易验证,近乎实盘的时延、滑点仿真。故,本平台提供GUI前端/客户端(部分功能也支持网页),后端做数据处理、交易调度,实现事件驱动的交易流程。对于策略研究部分,尤其强调机器学习、强化学习的AI策略,结合多因子模型提高收益比。 +Qbot是一个免费的量化投研平台,提供从数据获取、交易策略开发、策略回测、模拟交易到最终实盘交易的全闭环流程。在实盘接入前,有股票、基金评测和策略回测,在模拟环境下做交易验证,近乎实盘的时延、滑点仿真。故,本平台提供GUI前端/客户端(部分功能也支持网页),后端做数据处理、交易调度,实现事件驱动的交易流程。对于策略研究部分,尤其强调机器学习、强化学习的AI策略,结合多因子模型提高收益比。 -但本项目可能需要一一点python基础知识,有一点点交易经验,会更容易体会作者的初衷,解决当下产品空缺和广大散户朋友的交易痛点,现在直接免费开源出来! +但本项目可能需要一点点python基础知识,有一点点交易经验,会更容易体会作者的初衷,解决当下产品空缺和广大散户朋友的交易痛点,现在直接免费开源出来! ```bash cd ~ # $HOME as workspace @@ -194,15 +194,13 @@ pip install -r requirements.txt python main.py ``` -主要包含四个窗口,如果启动界面有问题可以参考这里的启动方式。 +主要包含四个窗口,如果启动界面未显示或有问题可以参考下图中对应的启动方式。👉 点击[这里](gui/mainframe.py)查看源码,下文也有文字介绍。 ![image](https://github.com/UFund-Me/Qbot/assets/29084184/9f1dcc07-ca76-4600-a02c-76104fb28c51) -👉 点击[这里](gui/mainframe.py)查看源码 - #### 后端/服务端 -1. 选基、选股助手(对应客户端第二个菜单:AI选股/选基) +1. 选基、选股助手(对应前端/客户端第二个菜单:AI选股/选基) 运行命令 @@ -212,11 +210,12 @@ go build ./investool webserver ``` -2. 基金策略在线分析(对应客户端第四个菜单:基金投资策略分析) +2. 基金策略在线分析(对应于前端/客户端第四个菜单:基金投资策略分析) 需要 node 开发环境: `npm`、`node`,点击[查看](pyfunds/fund-strategies/README.md)详细操作文档
版本信息(作为参考) + ``` ▶ go version go version go1.20.4 darwin/amd64 @@ -227,6 +226,7 @@ v19.7.0 ▶ npm --version 9.5.0 ``` +
使用docker运行项目,在项目路径下运行以下命令构建项目的docker镜像 diff --git a/WORKSPACE b/WORKSPACE new file mode 100644 index 0000000..e25243b --- /dev/null +++ b/WORKSPACE @@ -0,0 +1,27 @@ +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +http_archive( + name = "rules_python", + sha256 = "c03246c11efd49266e8e41e12931090b613e12a59e6f55ba2efd29a7cb8b4258", + strip_prefix = "rules_python-0.11.0", + url = "https://github.com/bazelbuild/rules_python/archive/refs/tags/0.11.0.tar.gz", +) + +load("@rules_python//python:pip.bzl", "pip_install") +load("@rules_python//python:repositories.bzl", "python_register_toolchains") + +# Use a hermetic Python interpreter so that builds are reproducible +# irrespective of the Python version available on the host machine. +python_register_toolchains( + name = "python3_10", + python_version = "3.10", +) + +load("@python3_10//:defs.bzl", "interpreter") + +# Translate requirements.txt into a @third_party external repository. +pip_install( + name = "third_party", + python_interpreter_target = interpreter, + requirements = "//third_party:requirements.txt", +) diff --git a/docs/README.md b/docs/README.md index d121b74..2d16e76 100644 --- a/docs/README.md +++ b/docs/README.md @@ -68,9 +68,9 @@ ## Quick Start -Qbot是一个免费的投研平台,提供从数据获取、交易策略开发、策略回测、模拟交易到最终实盘交易的全闭环流程。在实盘接入前,有股票、基金评测和策略回测,在模拟环境下做交易验证,近乎实盘的时延、滑点仿真。故,本平台提供GUI前端/客户端(部分功能也支持网页),后端做数据处理、交易调度,实现事件驱动的交易流程。对于策略研究部分,尤其强调机器学习、强化学习的AI策略,结合多因子模型提高收益比。 +Qbot是一个免费的量化投研平台,提供从数据获取、交易策略开发、策略回测、模拟交易到最终实盘交易的全闭环流程。在实盘接入前,有股票、基金评测和策略回测,在模拟环境下做交易验证,近乎实盘的时延、滑点仿真。故,本平台提供GUI前端/客户端(部分功能也支持网页),后端做数据处理、交易调度,实现事件驱动的交易流程。对于策略研究部分,尤其强调机器学习、强化学习的AI策略,结合多因子模型提高收益比。 -但本项目可能需要一一点python基础知识,有一点点交易经验,会更容易体会作者的初衷,解决当下产品空缺和广大散户朋友的交易痛点,现在直接免费开源出来! +但本项目可能需要一点点python基础知识,有一点点交易经验,会更容易体会作者的初衷,解决当下产品空缺和广大散户朋友的交易痛点,现在直接免费开源出来! ```bash cd ~ # $HOME as workspace @@ -197,16 +197,13 @@ pip install -r requirements.txt # if run on Mac, please use 'pythonw main.py' python main.py ``` - -主要包含四个窗口,如果启动界面有问题可以参考这里的启动方式。 +主要包含四个窗口,如果启动界面未显示或有问题可以参考下图中对应的启动方式。👉 点击[这里](https://github.com/UFund-Me/Qbot/blob/main/gui/mainframe.py#L122-L141)查看源码,下文也有文字介绍。 ![image](https://github.com/UFund-Me/Qbot/assets/29084184/9f1dcc07-ca76-4600-a02c-76104fb28c51) -👉 点击[这里](https://github.com/UFund-Me/Qbot/blob/main/gui/mainframe.py#L122-L141)查看源码 - #### 后端/服务端 -1. 选基、选股助手(对应客户端第二个菜单:AI选股/选基) +1. 选基、选股助手(对应前端/客户端第二个菜单:AI选股/选基) 运行命令 @@ -216,11 +213,12 @@ go build ./investool webserver ``` -2. 基金策略在线分析(对应客户端第四个菜单:基金投资策略分析) +2. 基金策略在线分析(对应于前端/客户端第四个菜单:基金投资策略分析) 需要 node 开发环境: `npm`、`node`,点击[查看](https://github.com/UFund-Me/Qbot/blob/main/pyfunds/fund-strategies/README.md)详细操作文档。
版本信息(作为参考) + ``` ▶ go version go version go1.20.4 darwin/amd64 @@ -231,6 +229,7 @@ v19.7.0 ▶ npm --version 9.5.0 ``` +
使用docker运行项目,在项目路径下运行以下命令构建项目的docker镜像 diff --git a/gui/imgs/UFund.png b/gui/imgs/UFund.png index 54c00d40374ffaf84731d8c9946b76a3483cade1..89819e578085ab1073d837618a237585cf3c43d8 100644 GIT binary patch literal 12467 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 l|C_!4|991ogYx`JX=HG35owU_@ABUF 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 X$Q*p@P2C2(=z<_8r3|f*FbV!Ylw7!6 diff --git a/gui/panels/panel_backtest.py b/gui/panels/panel_backtest.py index 848756e..264aa9a 100644 --- a/gui/panels/panel_backtest.py +++ b/gui/panels/panel_backtest.py @@ -13,9 +13,9 @@ class PanelBacktest(wx.Panel): def __init__(self, parent): super(PanelBacktest, self).__init__(parent) - # # 回测按钮 - # self.btn_bkt = wx.Button(self, label="回测") - # self.Bind(wx.EVT_BUTTON, OnBkt, self.btn_bkt) + # 回测按钮 + self.btn_bkt = wx.Button(self, label="回测") + self.Bind(wx.EVT_BUTTON, OnBkt, self.btn_bkt) # 进度条 diff --git a/pytrader/data/data_utils.py b/pytrader/data/data_utils.py index 8159d1d..b20eb7c 100755 --- a/pytrader/data/data_utils.py +++ b/pytrader/data/data_utils.py @@ -1,3 +1,17 @@ +''' +Author: Charmve yidazhang1@gmail.com +Date: 2022-12-04 12:03:50 +LastEditors: Charmve yidazhang1@gmail.com +LastEditTime: 2023-07-02 20:11:13 +FilePath: /Qbot/pytrader/data/data_utils.py +Version: 1.0.1 +Blogs: charmve.blog.csdn.net +GitHub: https://github.com/Charmve +Description: + +Copyright (c) 2023 by Charmve, All Rights Reserved. +Licensed under the MIT License. +''' import os import pandas as pd @@ -18,21 +32,32 @@ def load_data(codes, start_time="20100101", end_time="20211231"): def load_from_file(code): path = os.path.dirname(__file__) filename = "{}/{}.csv".format(os.path.dirname(path) + "/data/indexes", code) - # print("stack data:" + filename) + print("stack data:" + filename) if os.path.exists(filename): df = pd.read_csv(filename, index_col=[0]) - # print(df.head()) + print(df.head()) df.rename( columns={"trade_date": "date", "ts_code": "code", "vol": "volume"}, inplace=True, ) df["date"] = df["date"].apply(lambda x: str(x)) - df = df[["code", "open", "high", "low", "close", "date", "volume"]] - # df = df[['open', 'high', 'low', 'close', 'date', 'volume']] - df.index = df["date"] + # df = df[["code", "open", "high", "low", "close", "date", "volume"]] + df = df[['open', 'high', 'low', 'close', 'date', 'volume']] + df.index = df["code"] df.sort_index(ascending=True, inplace=True) df["rate"] = df["close"].pct_change() else: print("load_from_file error") return None return df + +class DataUtil: + def __init__(self, df_results, benchmarks=['000300.SH']): + self.df_results = df_results + self.benchmarks = benchmarks + + def to_backtrader_dataframe(self.df): + df.index = pd.to_datetime(df.index) + df['openinterest'] = 0 + df = df[['open', 'high', 'low', 'close', 'volume', 'openinterest']] + return df \ No newline at end of file diff --git a/pytrader/easyquant/strategy/strategyTemplate.py b/pytrader/easyquant/strategy/strategyTemplate.py index 4fe45ba..d203284 100644 --- a/pytrader/easyquant/strategy/strategyTemplate.py +++ b/pytrader/easyquant/strategy/strategyTemplate.py @@ -7,7 +7,7 @@ from pandas import DataFrame from ..context import Context from ..event_engine import Event -from easytrader.webtrader import WebTrader +from ..easytrader.webtrader import WebTrader class StrategyTemplate: diff --git a/pytrader/strategies/lgb_strategy.py b/pytrader/strategies/lgb_strategy.py index 14e7e45..893cc40 100755 --- a/pytrader/strategies/lgb_strategy.py +++ b/pytrader/strategies/lgb_strategy.py @@ -9,9 +9,168 @@ import matplotlib.pyplot as plt import pandas as pd import talib as ta from base import Strategy -from data.data_utils import load_data, load_from_file +# from data.data_utils import load_data, load_from_file from IPython.display import display -from model.lgb import LGBModel +# from model.lgb import LGBModel + +import os + +import pandas as pd + + +def load_data(codes, start_time="20100101", end_time="20211231"): + dfs = [] + for code in codes: + df = load_from_file(code) + # df.dropna(inplace=True) + dfs.append(df) + df_all = pd.concat(dfs, axis=0) + df_all.sort_index(inplace=True) + df_all = df_all.loc[start_time:end_time] + return df_all + + +def load_from_file(code): + path = os.path.dirname(__file__) + filename = "{}/{}.csv".format(os.path.dirname(path) + "/data/indexes", code) + # print("stack data:" + filename) + if os.path.exists(filename): + df = pd.read_csv(filename, index_col=[0]) + # print(df.head()) + df.rename( + columns={"trade_date": "date", "ts_code": "code", "vol": "volume"}, + inplace=True, + ) + df["date"] = df["date"].apply(lambda x: str(x)) + ## code,date,close,open,high,low,volume,amount + # df = df[["code", "open", "high", "low", "close", "date", "volume"]] + df = df[['open', 'high', 'low', 'close', 'date', 'volume']] + df.index = df["date"] + df.sort_index(ascending=True, inplace=True) + df["rate"] = df["close"].pct_change() + else: + print("load_from_file error") + return None + return df + + +import numpy as np +import pandas as pd +import lightgbm as lgb +from sklearn.metrics import r2_score, accuracy_score + +class LGBModel: + def __init__(self, regression = True): + self.regression = regression + def fit(self, dataset): + X_train, X_valid, y_train, y_valid = dataset.split() + + dtrain = lgb.Dataset(X_train, label=y_train) + dvalid = lgb.Dataset(X_valid, label=y_valid) + + #params = {"objective": 'mse', "verbosity": -1} + # 参数 + params_regression = { + 'learning_rate': 0.1, + 'metrics':{'auc','mse'}, + 'lambda_l1': 0.1, + 'lambda_l2': 0.2, + 'max_depth': 4, + 'objective': 'mse'#'mse', # 目标函数 + } + + params = {'num_leaves': 90, + 'min_data_in_leaf': 30, + 'objective': 'multiclass', + 'num_class': 10, + 'max_depth': -1, + 'learning_rate': 0.03, + "min_sum_hessian_in_leaf": 6, + "boosting": "gbdt", + "feature_fraction": 0.9, + "bagging_freq": 1, + "bagging_fraction": 0.8, + "bagging_seed": 11, + "lambda_l1": 0.1, + "verbosity": -1, + "nthread": 15, + 'metric': {'multi_logloss'}, + "random_state": 2022, + #'device': 'gpu' + } + + if self.regression: + params = params_regression + self.model = lgb.train( + params, + dtrain, + num_boost_round=1000, + valid_sets=[dtrain, dvalid], + valid_names=["train", "valid"], + early_stopping_rounds=50, + verbose_eval=True, + # evals_result=evals_result, + #**kwargs + ) + y_pred = self.model.predict(X_valid) + if not self.regression: + y_pred = np.argmax(y_pred, axis=1) + print('accuracy:',accuracy_score(y_pred, y_valid)) + + y_pred_train = np.argmax(self.model.predict(X_train), axis=1) + print('accuracy_train:',accuracy_score(y_pred_train, y_train)) + else: + print('R2系数:', r2_score(y_valid, y_pred)) + print('训练集——R2系数:', r2_score(y_train, self.model.predict(X_train))) + + def predict(self, dataset): + if self.model is None: + raise ValueError("model is not fitted yet!") + x_test,_ = dataset.get_data(date_range=['20160101', '20211231']) + pred = self.model.predict(x_test) + print(pred) + if not self.regression: + return pd.Series(np.argmax(pred, axis=1), index=x_test.index) + else: + return pd.Series(pred, index=x_test.index) + + +# if __name__ == '__main__': +# from bak.data.dataset import Dataset +# from engine.data.datahandler import DataHandler + +# fields = ['Return($close,5)', 'Return($close,20)', 'Ref($close,126)/$close -1','$close','$open','$high','$low','$volume','$amount'] +# names = ['return_5', 'return_20', 'return_126','close','open','high','low','volume','amount'] + +# #fields += ['Ref($close,-5)/$close -1'] +# #names += ['return_-5'] + +# #ds = Dataset(codes=, fields=fields, feature_names=names, +# # label_expr='QCut(Ref($close,-20)/$close -1,10)') +# #print(ds.df) +# codes = ['512690.SH', '512170.SH', '512660.SH','159928.SZ','512010.SH'] +# codes = ['159915.SZ','510300.SH','512690.SH', '512170.SH', '512660.SH','159928.SZ','512010.SH'] +# codes = ['159928.SZ','510050.SH','512010.SH','513100.SH','518880.SH','511220.SH','511010.SH','161716.SZ'] +# codes = [ +# '000300.SH', +# '000905.SH', +# '399006.SZ', #创业板 +# '000852.SH', #中证1000 +# '399324.SZ', #深证红利 +# #'000922.SH', #中证红利 +# '399997.SZ', #中证白酒 +# '399396.SZ', #食品饮料 + +# '000013.SH',#上证企债 +# '000016.SH' #上证50 +# ] +# ds = Dataset(codes=codes, handler=DataHandler()) +# print(ds.df) + +# m = LGBModel() +# m.fit(ds) +# pred = m.predict(ds) +# print(pred) # Step 1: load dataset and generate features diff --git a/setup.py b/qbot/setup.py similarity index 100% rename from setup.py rename to qbot/setup.py diff --git a/qbot/strategies/adx_strategy.py b/qbot/strategies/adx_strategy.py new file mode 100644 index 0000000..bd3d7b7 --- /dev/null +++ b/qbot/strategies/adx_strategy.py @@ -0,0 +1,64 @@ +''' +Author: Charmve yidazhang1@gmail.com +Date: 2023-03-18 18:06:06 +LastEditors: Charmve yidazhang1@gmail.com +LastEditTime: 2023-03-18 18:14:39 +FilePath: /Qbot/qbot/strategies/adx_strategy.py +Version: 1.0.1 +Blogs: charmve.blog.csdn.net +GitHub: https://github.com/Charmve +Description: 获取股票数据并进行量化回测——基于ADX和MACD趋势策略 + +https://blog.csdn.net/ndhtou222/article/details/121219649 + +Copyright (c) 2023 by Charmve, All Rights Reserved. +Licensed under the MIT License. +''' +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +#正常显示画图时出现的中文和负号 +from pylab import mpl +mpl.rcParams['font.sans-serif']=['SimHei'] +mpl.rcParams['axes.unicode_minus']=False +#不显示警告信息 +import warnings +warnings.filterwarnings('ignore') + +from get_stack_data import get_from_tushare +import pyfolio as pf +import talib as ta + +def adx_strategy(df,ma1=13,ma2=55,ma3=89,adx=25): + #计算MACD和ADX指标 + df['EMA1'] = ta.EMA(df.close,ma1) + df['EMA2'] = ta.EMA(df.close,ma2) + df['EMA3'] = ta.EMA(df.close,ma3) + df['MACD'],df['MACDSignal'],df['MACDHist'] = ta.MACD(df.close,12,26,9) + df['ADX'] = ta.ADX(df.high,df.low,df.close,14) + #设计买卖信号:21日均线大于42日均线且42日均线大于63日均线;ADX大于前值小于25;MACD大于前值 + df['Buy_Sig'] =(df['EMA1']>df['EMA2'])&(df['EMA2']>df['EMA3'])&(df['ADX']<=adx)\ + &(df['ADX']>df['ADX'].shift(1))&(df['MACDHist']>df['MACDHist'].shift(1)) + df.loc[df.Buy_Sig,'Buy_Trade'] = 1 + df.loc[df.Buy_Trade.shift(1)==1,'Buy_Trade'] = " " + #避免最后三天内出现交易 + df.Buy_Trade.iloc[-3:] = " " + df.loc[df.Buy_Trade==1,'Buy_Price'] = df.close + df.Buy_Price = df.Buy_Price.ffill() + df['Buy_Daily_Return']= (df.close - df.Buy_Price)/df.Buy_Price + df.loc[df.Buy_Trade.shift(3)==1,'Sell_Trade'] = -1 + df.loc[df.Sell_Trade==-1,'Buy_Total_Return'] = df.Buy_Daily_Return + df.loc[(df.Sell_Trade==-1)&(df.Buy_Daily_Return==0),'Buy_Total_Return'] = \ + (df.Buy_Price - df.Buy_Price.shift(1))/df.Buy_Price.shift(1) + df.loc[(df.Sell_Trade==-1)&(df.Buy_Trade.shift(1)==1),'Buy_Total_Return'] = \ + (df.close-df.Buy_Price.shift(2))/df.Buy_Price.shift(2) + #返回策略的日收益率 + return df.Buy_Total_Return.fillna(0) + +df=get_from_tushare('300002') +df.close.plot(figsize=(12,6)) +plt.title('神州泰岳股价走势\n2010-2021',size=15) + +pf.create_simple_tear_sheet((df.close.pct_change()).fillna(0).tz_localize('UTC')) + +pf.create_simple_tear_sheet(adx_strategy(df).tz_localize('UTC')) \ No newline at end of file diff --git a/qbot/strategies/get_stack_data.py b/qbot/strategies/get_stack_data.py new file mode 100644 index 0000000..11d605c --- /dev/null +++ b/qbot/strategies/get_stack_data.py @@ -0,0 +1,84 @@ +''' +Author: Charmve yidazhang1@gmail.com +Date: 2023-03-18 18:09:25 +LastEditors: Charmve yidazhang1@gmail.com +LastEditTime: 2023-03-18 18:09:26 +FilePath: /Qbot/qbot/strategies/get_stack_data.py +Version: 1.0.1 +Blogs: charmve.blog.csdn.net +GitHub: https://github.com/Charmve +Description: + +Copyright (c) 2023 by Charmve, All Rights Reserved. +Licensed under the MIT License. +''' + +#使用tushare旧版获取数据 +import tushare as ts +def get_from_tushare(code,adj='hfq',start='2010-01-01',end='2021-11-05'): + df=ts.get_k_data(code,autype=adj,start=start,end=end) + df.index=pd.to_datetime(df.date) + #原数据已默认按日期进行了排序 + return df + +#使用tushare pro获取数据 +import tushare as ts +token='输入你自己的token' +pro=ts.pro_api(token) +ts.set_token(token) +def get_from_tushare_pro(code,adj='hfq',start='2010-01-01',end='2021-11-05'): + #code:输入数字字符串,如‘300002’ + #start和end输入'年-月-日'需转为'年月日'格式 + if code.startswith('6'): + code=code+'.SH' + else: + code=code+'.SZ' + start=''.join(start.split('-')) + end=''.join(end.split('-')) + df=ts.pro_bar(ts_code=code,adj=adj,start_date=start,end_date=end) + #原数据是倒序的,所以将时间设置为索引,根据索引重新排序 + df.index=pd.to_datetime(df.trade_date) + df=df.sort_index() + return df + +#使用akshare获取数据,其数据源来自新浪,与tushare旧版本相似 +import akshare as ak +def get_from_akshare(code,adj='hfq',start='2010-01-01',end='2021-11-05'): + if code.startswith('6'): + code='sh'+code + else: + code='sz'+code + start=''.join(start.split('-')) + end=''.join(end.split('-')) + df = ak.stock_zh_a_daily(symbol=code, start_date=start, end_date=end, adjust=adj) + return df + +#使用baostock获取数据 +import baostock as bs +def get_from_baostock(code,adj='hfq',start='2010-01-01',end='2021-11-05'): + if code.startswith('6'): + code='sh.'+code + else: + code='sz.'+code + #转换复权为数字 + if adj=='hfq': + adj='1' + elif adj=='qfq': + adj='2' + else: + adj='3' + #必须登陆和登出系统 + bs.login() #登陆系统 + rs = bs.query_history_k_data_plus(code, + fields="date,code,open,high,low,close,volume", + start_date=start, end_date=end, + frequency="d", adjustflag=adj) + #adjustflag:复权类型,默认不复权:3;1:后复权;2:前复权 + data_list = [] + while (rs.error_code == '0') & rs.next(): + data_list.append(rs.get_row_data()) + #将数据转为dataframe格式 + df = pd.DataFrame(data_list, columns=rs.fields) + df.index=pd.to_datetime(df.date) + bs.logout() #登出系统 + return df \ No newline at end of file diff --git a/qbot/strategies/klines_bt.py b/qbot/strategies/klines_bt.py new file mode 100644 index 0000000..9f418e2 --- /dev/null +++ b/qbot/strategies/klines_bt.py @@ -0,0 +1,106 @@ +''' +Author: Charmve yidazhang1@gmail.com +Date: 2023-03-12 18:44:54 +LastEditors: Charmve yidazhang1@gmail.com +LastEditTime: 2023-05-17 09:35:01 +FilePath: /Qbot/qbot/strategies/klines_bt.py +Version: 1.0.1 +Blogs: charmve.blog.csdn.net +GitHub: https://github.com/Charmve +Description: + +Copyright (c) 2023 by Charmve, All Rights Reserved. +Licensed under the MIT License. +''' +from datetime import datetime +import backtrader +from loguru import logger +import matplotlib.pyplot as plt +import pandas as pd +import efinance + + +def get_k_data(stock_code, begin: datetime, end: datetime) -> pd.DataFrame: + """ + 根据efinance工具包获取股票数据 + :param stock_code:股票代码 + :param begin: 开始日期 + :param end: 结束日期 + :return: + """ + # stock_code = '600519' # 股票代码,茅台 + k_dataframe: pd.DataFrame = efinance.stock.get_quote_history( + stock_code, beg=begin.strftime("%Y%m%d"), end=end.strftime("%Y%m%d")) + k_dataframe = k_dataframe.iloc[:, :9] + k_dataframe.columns = ['name', 'code', 'date', 'open', 'close', 'high', 'low', 'volume', 'turnover'] + k_dataframe.index = pd.to_datetime(k_dataframe.date) + k_dataframe.drop(['name', 'code', 'date'], axis=1, inplace=True) + return k_dataframe + + +class KlinesStrategy(backtrader.Strategy): # 策略 + def __init__(self): + # 初始化交易指令、买卖价格和手续费 + self.close_price = self.datas[0].close # 这里加一个数据引用,方便后续操作 + self.sma = backtrader.indicators.SimpleMovingAverage(self.datas[0], period=5) # 借用这个策略,计算5日的均线 + + def notify_order(self, order): # 固定写法,查看订单情况 + # 查看订单情况 + if order.status in [order.Submitted, order.Accepted]: # 接受订单交易,正常情况 + return + if order.status in [order.Completed]: + if order.isbuy(): + logger.debug('已买入, 购入金额 %.2f' % order.executed.price) + elif order.issell(): + logger.debug('已卖出, 卖出金额 %.2f' % order.executed.price) + elif order.status in [order.Canceled, order.Margin, order.Rejected]: + logger.debug('订单取消、保证金不足、金额不足拒绝交易') + + def next(self): # 固定的函数,框架执行过程中会不断循环next(),过一个K线,执行一次next() + # 此时调用 self.datas[0]即可查看当天的数据 + # 执行买入条件判断:当天收盘价格突破5日均线 + if self.close_price[0] > self.sma[0]: + # 执行买入 + logger.debug("buy 500 in {}, 预期购入金额 {}, 剩余可用资金 {}", self.datetime.date(), self.data.close[0], + self.broker.getcash()) + self.buy(size=500, price=self.data.close[0]) + # 执行卖出条件已有持仓,且收盘价格跌破5日均线 + if self.position: + if self.close_price[0] < self.sma[0]: + # 执行卖出 + logger.debug("sell in {}, 预期卖出金额 {}, 剩余可用资金 {}", self.datetime.date(), self.data.close[0], + self.broker.getcash()) + self.sell(size=500, price=self.data.close[0]) + + +if __name__ == '__main__': + # 获取数据 + start_time = datetime(2015, 1, 1) + end_time = datetime(2021, 1, 1) + dataframe = get_k_data('600519', begin=start_time, end=end_time) + # =============== 为系统注入数据 ================= + # 加载数据 + data = backtrader.feeds.PandasData(dataname=dataframe, fromdate=start_time, todate=end_time) + # 初始化cerebro回测系统 + cerebral_system = backtrader.Cerebro() # Cerebro引擎在后台创建了broker(经纪人)实例,系统默认每个broker的初始资金量为10000 + # 将数据传入回测系统 + cerebral_system.adddata(data) # 导入数据,在策略中使用 self.datas 来获取数据源 + # 将交易策略加载到回测系统中 + cerebral_system.addstrategy(KlinesStrategy) + # =============== 系统设置 ================== + # 设置启动资金为 100000 + start_cash = 1000000 + cerebral_system.broker.setcash(start_cash) + # 设置手续费 万2.5 + cerebral_system.broker.setcommission(commission=0.00025) + logger.debug('初始资金: {} 回测期间:from {} to {}'.format(start_cash, start_time, end_time)) + # 运行回测系统 + cerebral_system.run() + # 获取回测结束后的总资金 + portvalue = cerebral_system.broker.getvalue() + pnl = portvalue - start_cash + # 打印结果 + logger.debug('净收益: {}', pnl) + logger.debug("总资金: {}", portvalue) + cerebral_system.plot(style='candlestick') + plt.show() diff --git a/qbot/strategies/util.py b/qbot/strategies/util.py new file mode 100644 index 0000000..eddd87c --- /dev/null +++ b/qbot/strategies/util.py @@ -0,0 +1,96 @@ +''' +Author: Charmve yidazhang1@gmail.com +Date: 2023-03-14 01:49:02 +LastEditors: Charmve yidazhang1@gmail.com +LastEditTime: 2023-03-14 01:50:11 +FilePath: /Qbot/qbot/strategies/util.py +Version: 1.0.1 +Blogs: charmve.blog.csdn.net +GitHub: https://github.com/Charmve +Description: + +Copyright (c) 2023 by Charmve, All Rights Reserved. +Licensed under the MIT License. +''' + +import baostock as bs +import pandas as pd +import talib as ta +import matplotlib.pyplot as plt + + +def computeMACD(code, startdate, enddate): + login_result = bs.login(user_id='anonymous', password='123456') + print(login_result) + # 获取股票日 K 线数据 + rs = bs.query_history_k_data(code, + "date,code,close,tradeStatus", + start_date=startdate, + end_date=enddate, + frequency="d", adjustflag="3") + # 打印结果集 + result_list = [] + while (rs.error_code == '0') & rs.next(): + # 获取一条记录,将记录合并在一起 + result_list.append(rs.get_row_data()) + df = pd.DataFrame(result_list, columns=rs.fields) + # 剔除停盘数据 + df2 = df[df['tradeStatus'] == '1'] + # 获取 dif,dea,hist,它们的数据类似是 tuple,且跟 df2 的 date 日期一一对应 + # 记住了 dif,dea,hist 前 33 个为 Nan,所以推荐用于计算的数据量一般为你所求日期之间数据量的 3 倍 + # 这里计算的 hist 就是 dif-dea,而很多证券商计算的 MACD=hist*2=(difdea)*2 + dif, dea, hist = ta.MACD(df2['close'].astype(float).values, fastperiod=12, slowperiod=26, signalperiod=9) + df3 = pd.DataFrame({'dif': dif[33:], 'dea': dea[33:], 'hist':hist[33:]},index=df2['date'][33:], columns=['dif', 'dea','hist']) + df3.plot(title='MACD') + plt.show() + # 寻找 MACD 金叉和死叉 + datenumber = int(df3.shape[0]) + for i in range(datenumber - 1): + if ((df3.iloc[i, 0] <= df3.iloc[i, 1]) & (df3.iloc[i + 1, 0] >= df3.iloc[i + 1, 1])): + print("MACD 金叉的日期:" + df3.index[i + 1]) + if ((df3.iloc[i, 0] >= df3.iloc[i, 1]) & (df3.iloc[i + 1, 0] <=df3.iloc[i + 1, 1])): + print("MACD 死叉的日期:" + df3.index[i + 1]) + bs.logout() + return (dif, dea, hist) + +def calculateEMA(period, closeArray, emaArray=[]): + length = len(closeArray) + nanCounter = np.count_nonzero(np.isnan(closeArray)) + if not emaArray: + emaArray.extend(np.tile([np.nan], (nanCounter + period - 1))) + firstema = np.mean(closeArray[nanCounter:nanCounter + period - 1]) + emaArray.append(firstema) + for i in range(nanCounter + period, length): + ema = (2 * closeArray[i] + (period - 1) * emaArray[-1]) / (period + 1) + emaArray.append(ema) + return np.array(emaArray) + + +def calculateMACD(closeArray, shortPeriod=12, longPeriod=26, signalPeriod=9): + ema12 = calculateEMA(shortPeriod, closeArray, []) + ema26 = calculateEMA(longPeriod, closeArray, []) + diff = ema12 - ema26 + + dea = calculateEMA(signalPeriod, diff, []) + macd = (diff - dea)*2 + + fast_values = diff # 快线 + slow_values = dea # 慢线 + diff_values = macd # macd + # return fast_values, slow_values, diff_values # 返回所有的快慢线和macd值 + return fast_values[-1], slow_values[-1], diff_values[-1] # 返回最新的快慢线和macd值 + # return round(fast_values[-1],5), round(slow_values[-1],5), round(diff_values[-1],5) + +def getMACD(): + data = RequestUtil.sendRequest_GET(UrlConstant.Get_K_Line) + closeArray = [float(i[4]) for i in data] + closeArray.reverse() + return calculateMACD(closeArray) + + +if __name__ == '__main__': + code = 'sh.600000' + startdate = '2022-03-01' + enddate = '2023-03-18' + (dif, dea, hist) = computeMACD(code, startdate, enddate) + print((dif, dea, hist)) \ No newline at end of file diff --git a/utils/common/AShareDailyData.py b/utils/common/AShareDailyData.py new file mode 100644 index 0000000..909d641 --- /dev/null +++ b/utils/common/AShareDailyData.py @@ -0,0 +1,246 @@ +''' +Author: Charmve yidazhang1@gmail.com +Date: 2023-03-12 20:11:54 +LastEditors: Charmve yidazhang1@gmail.com +LastEditTime: 2023-03-12 20:12:24 +FilePath: /Qbot/utils/common/AShareDailyData.py +Version: 1.0.1 +Blogs: charmve.blog.csdn.net +GitHub: https://github.com/Charmve +Description: + +Copyright (c) 2023 by Charmve, All Rights Reserved. +Licensed under the MIT License. +''' + +import multiprocessing +import os +import sys +import traceback +from datetime import datetime, timedelta, time +from time import sleep +from typing import List + +from tqdm import tqdm +from vnpy.trader.constant import Interval, Exchange +from vnpy.trader.database import database_manager, BarOverview +from vnpy.trader.object import HistoryRequest, BarData + +from utils import log + +sys.path.append(os.getcwd()) + +from TuShare import tushare_client, to_split_ts_codes, TS_DATE_FORMATE + + +class AShareDailyDataManager: + + def __init__(self): + """""" + self.tushare_client = tushare_client + self.symbols = None + self.trade_cal = None + self.bar_overviews: List[BarOverview] = None + self.init() + + def init(self): + """""" + self.tushare_client.init() + self.symbols = self.tushare_client.symbols + self.trade_cal = self.tushare_client.trade_cal + self.bar_overviews = database_manager.get_bar_overview() + + def download_all(self): + """ + 使用tushare下载A股股票全市场日线数据 + :return: + """ + log.info("开始下载A股股票全市场日线数据") + if self.symbols is not None: + with tqdm(total=len(self.symbols)) as pbar: + for tscode, list_date in zip(self.symbols['ts_code'], self.symbols['list_date']): + symbol, exchange = to_split_ts_codes(tscode) + + pbar.set_description_str("下载A股日线数据股票代码:" + tscode) + start_date = datetime.strptime(list_date, TS_DATE_FORMATE) + req = HistoryRequest(symbol=symbol, + exchange=exchange, + start=start_date, + end=datetime.now(), + interval=Interval.DAILY) + bardata = self.tushare_client.query_history(req=req) + + if bardata: + try: + database_manager.save_bar_data(bardata) + except Exception as ex: + log.error(tscode + "数据存入数据库异常") + log.error(ex) + traceback.print_exc() + + pbar.update(1) + log.info(pbar.desc) + + log.info("A股股票全市场日线数据下载完毕") + + def get_newest_bar_data(self, symbol: str, exchange: Exchange, interval: Interval) -> BarData or None: + """""" + for overview in self.bar_overviews: + if exchange == overview.exchange and interval == overview.interval and symbol == overview.symbol: + bars = database_manager.load_bar_data(symbol=symbol, exchange=exchange, interval=interval, + start=overview.end, end=overview.end) + return bars[0] if bars is not None else None + return None + + def update_newest(self): + """ + 使用tushare更新本地数据库中的最新数据,默认本地数据库中原最新的数据之前的数据都是完备的 + :return: + """ + log.info("开始更新最新的A股股票全市场日线数据") + if self.symbols is not None: + with tqdm(total=len(self.symbols)) as pbar: + for tscode, list_date in zip(self.symbols['ts_code'], self.symbols['list_date']): + symbol, exchange = to_split_ts_codes(tscode) + + newest_local_bar = self.get_newest_bar_data(symbol=symbol, + exchange=exchange, + interval=Interval.DAILY) + if newest_local_bar is not None: + pbar.set_description_str("正在处理股票代码:" + tscode + "本地最新数据:" + + newest_local_bar.datetime.strftime(TS_DATE_FORMATE)) + start_date = newest_local_bar.datetime + timedelta(days=1) + else: + pbar.set_description_str("正在处理股票代码:" + tscode + "无本地数据") + start_date = datetime.strptime(list_date, TS_DATE_FORMATE) + req = HistoryRequest(symbol=symbol, + exchange=exchange, + start=start_date, + end=datetime.now(), + interval=Interval.DAILY) + bardata = self.tushare_client.query_history(req=req) + if bardata: + try: + database_manager.save_bar_data(bardata) + except Exception as ex: + log.error(tscode + "数据存入数据库异常") + log.error(ex) + traceback.print_exc() + + pbar.update(1) + log.info(pbar.desc) + + log.info("A股股票全市场日线数据更新完毕") + + def check_update_all(self): + """ + 这个方法太慢了,不建议调用。 + 这个方法用于本地数据库已经建立,但可能有部分数据缺失时使用 + 使用tushare检查更新所有的A股股票全市场日线数据 + 检查哪一个交易日的数据是缺失的,补全它 + 检查上市后是否每个交易日都有数据,若存在某一交易日无数据,尝试从tushare查询该日数据,若仍无,则说明当天停盘 + :return: + """ + log.info("开始检查更新所有的A股股票全市场日线数据") + + if self.symbols is not None: + with tqdm(total=len(self.symbols)) as pbar: + for tscode, list_date in zip(self.symbols['ts_code'], self.symbols['list_date']): + pbar.set_description_str("正在检查A股日线数据,股票代码:" + tscode) + + symbol, exchange = to_split_ts_codes(tscode) + + local_bar = database_manager.load_bar_data(symbol=symbol, + exchange=exchange, + interval=Interval.DAILY, + start=datetime.strptime(list_date, TS_DATE_FORMATE), + end=datetime.now()) + local_bar_dates = [bar.datetime.strftime(TS_DATE_FORMATE) for bar in local_bar] + + index = (self.trade_cal[exchange.value][(self.trade_cal[exchange.value].cal_date == list_date)]) + trade_cal = self.trade_cal[exchange.value].iloc[index.index[0]:] + for trade_date in trade_cal['cal_date']: + if trade_date not in local_bar_dates: + req = HistoryRequest(symbol=symbol, + exchange=exchange, + start=datetime.strptime(trade_date, TS_DATE_FORMATE), + end=datetime.strptime(trade_date, TS_DATE_FORMATE), + interval=Interval.DAILY) + bardata = self.tushare_client.query_history(req=req) + if bardata: + log.info(tscode + "本地数据库缺失:" + trade_date) + try: + database_manager.save_bar_data(bardata) + except Exception as ex: + log.error(tscode + "数据存入数据库异常") + log.error(ex) + traceback.print_exc() + pbar.update(1) + log.info(pbar.desc) + + log.info("A股股票全市场日线数据检查更新完毕") + + +a_share_daily_data_manager = AShareDailyDataManager() + + +def auto_update(start_time: time = time(18, 0)): + """ + 每日盘后自动更新最新日线数据到本地数据库 + """ + log.info("启动A股股票全市场日线数据定时更新") + run_parent(start_time=start_time) + + +def run_parent(start_time: time = time(18, 0)): + """ + 运行父进程,定时启动子进程下载任务 + :return: + """ + log.info("启动A股股票全市场日线数据定时更新父进程") + + # 每天晚上18:30从tushare更新当时K线数据 + UPDATE_TIME = start_time + + child_process = None + + while True: + current_time = datetime.now().time() + + if current_time.hour == UPDATE_TIME.hour and current_time.minute == UPDATE_TIME.minute and child_process is None: + log.info("启动日线数据更新子进程") + child_process = multiprocessing.Process(target=run_child) + child_process.start() + log.info("日线数据更新子进程启动成功") + + if (not (current_time.hour == UPDATE_TIME.hour and current_time.minute == UPDATE_TIME.minute)) \ + and child_process is not None: + child_process.join() + child_process = None + log.info("数据更新子进程关闭成功") + log.info("进入A股股票全市场日线数据定时更新父进程") + + sleep(10) + + +def run_child(): + """ + 子线程下载数据 + :return: + """ + log.info("启动A股股票全市场日线数据定时更新子进程") + + try: + a_share_daily_data_manager.update_newest() + except Exception: + log.info("子进程异常") + traceback.print_exc() + + +if __name__ == '__main__': + log.info("自动更新A股股票全市场日线数据") + + # a_share_daily_data_manager.download_all() + # a_share_daily_data_manager.update_newest() + # a_share_daily_data_manager.check_update_all() + auto_update(start_time=time(21, 47)) diff --git a/utils/common/BaseService.py b/utils/common/BaseService.py new file mode 100644 index 0000000..1054a47 --- /dev/null +++ b/utils/common/BaseService.py @@ -0,0 +1,271 @@ +''' +Author: Charmve yidazhang1@gmail.com +Date: 2023-03-10 00:45:44 +LastEditors: Charmve yidazhang1@gmail.com +LastEditTime: 2023-03-12 20:10:22 +FilePath: /Qbot/utils/common/BaseService.py +Version: 1.0.1 +Blogs: charmve.blog.csdn.net +GitHub: https://github.com/Charmve +Description: + +Copyright (c) 2023 by Charmve, All Rights Reserved. +Licensed under the MIT License. +''' +# -*-coding=utf-8-*- + +import datetime +import json +import os +import re +import time + +import parsel +import requests +from configure.util import send_message_via_wechat +from loguru import logger + + +class BaseService(object): + def __init__(self, logfile="default.log"): + self.logger = logger + self.logger.add(logfile) + self.init_const_data() + self.params = None + self.cookies = None + + def init_const_data(self): + """ + 常见的数据初始化 + """ + self.today = datetime.datetime.now().strftime("%Y-%m-%d") + + def check_path(self, path): + if not os.path.exists(path): + try: + os.makedirs(path) + except Exception as e: + self.logger.error(e) + + def get_url_filename(self, url): + return url.split("/")[-1] + + def save_iamge(self, content, path): + with open(path, "wb") as fp: + fp.write(content) + + def get(self, url, _json=False, binary=False, retry=5): + + start = 0 + while start < retry: + + try: + r = requests.get( + url=url, + params=self.params, + headers=self.headers, + cookies=self.cookies, + ) + + except Exception as e: + self.logger.error("base class error ".format(e)) + start += 1 + continue + + else: + if _json: + result = r.json() + elif binary: + result = r.content + else: + r.encoding = "utf8" + result = r.text + return result + + return None + + def post(self, url, post_data, _json=False, binary=False, retry=5): + + start = 0 + while start < retry: + + try: + r = requests.post(url=url, headers=self.headers, data=post_data) + + except Exception as e: + print(e) + start += 1 + continue + + else: + if _json: + result = r.json() + elif binary: + result = r.content + else: + result = r.text + return result + + return None + + @property + def headers(self): + raise NotImplementedError + + def parse(self, content): + """ + 页面解析 + """ + response = parsel.Selector(text=content) + + return response + + def process(self, data, history=False): + """ + 数据存储 + """ + pass + + def time_str(self, x): + return x.strftime("%Y-%m-%d") + + def trading_time(self): + """ + 判定时候交易时间 0 为交易时间, 1和-1为非交易时间 + :return: + """ + TRADING = 0 + MORNING_STOP = -1 + AFTERNOON_STOP = 1 + NOON_STOP = -1 + current = datetime.datetime.now() + year, month, day = current.year, current.month, current.day + start = datetime.datetime(year, month, day, 9, 23, 0) + noon_start = datetime.datetime(year, month, day, 12, 58, 0) + + morning_end = datetime.datetime(year, month, day, 11, 31, 0) + end = datetime.datetime(year, month, day, 15, 2, 5) + + if current > start and current < morning_end: + return TRADING + + elif current > noon_start and current < end: + return TRADING + + elif current > end: + return AFTERNOON_STOP + + elif current < start: + return MORNING_STOP + + else: + return NOON_STOP + + def notify(self, title): + send_message_via_wechat(title) + + def weekday(self, day=datetime.datetime.now().strftime("%Y-%m-%d")): + """判断星期几""" + + if re.search(f"\d{4}-\d{2}-\d{2}", day): + fmt = "%Y-%m-%d" + elif re.search("\d{8}", day): + fmt = "%Y%m%d" + else: + raise ValueError("请输入正确的日期格式") + + current_date = datetime.datetime.strptime(day, fmt) + year_2000th = datetime.datetime(year=2000, month=1, day=2) + day_diff = current_date - year_2000th + return day_diff.days % 7 + + def is_weekday(self, day=datetime.datetime.now().strftime("%Y-%m-%d")): + if self.weekday(day) in [0, 6]: + return False + else: + return True + + def execute(self, cmd, data, conn, logger=None): + + cursor = conn.cursor() + + if not isinstance(data, tuple): + data = (data,) + try: + cursor.execute(cmd, data) + except Exception as e: + conn.rollback() + logger.error("执行数据库错误 {},{}".format(e, cmd)) + ret = None + else: + ret = cursor.fetchall() + conn.commit() + + return ret + + def jsonp2json(self, str_): + return json.loads(str_[str_.find("{") : str_.rfind("}") + 1]) + + def set_proxy_param(self, proxy): + self.proxy_ip = proxy + + def get_proxy(self, retry=10): + + if not hasattr(self, "proxy_ip"): + raise AttributeError("Please set proxy ip before use it") + + proxyurl = f"http://{self.proxy_ip}/dynamicIp/common/getDynamicIp.do" + count = 0 + for i in range(retry): + try: + r = requests.get(proxyurl, timeout=10) + except Exception as e: + print(e) + count += 1 + print("代理获取失败,重试" + str(count)) + time.sleep(1) + + else: + js = r.json() + proxyServer = "://{0}:{1}".format(js.get("ip"), js.get("port")) + + proxies_random = { + "http": "http" + proxyServer, + "https": "https" + proxyServer, + } + return proxies_random + + return None + + def convert_timestamp(self, t): + return datetime.datetime.fromtimestamp(int(t / 1000)).strftime("%Y-%m-%d") + + +class HistorySet(object): + def __init__(self, expire=1800): + self.data = {} + self.expire = expire + + def add(self, value): + now = datetime.datetime.now() + expire = now + datetime.timedelta(seconds=self.expire) + try: + hash(value) + except: # noqa E722 + raise ValueError("value not hashble") + else: + self.data.update({value: expire}) + + def is_expire(self, value): + # 没有过期 返回 False + if value not in self.data or self.data[value] < datetime.datetime.now(): + return True + else: + return False + + +if __name__ == "__main__": + base = BaseService() + base.is_weekday() + # base.set_proxy_param() + print(base.get_proxy()) diff --git a/utils/common/TuShare.py b/utils/common/TuShare.py new file mode 100644 index 0000000..15a4ed2 --- /dev/null +++ b/utils/common/TuShare.py @@ -0,0 +1,235 @@ +import requests +import tushare as ts +from tushare.pro import client +from pytz import timezone +from typing import List, Optional, Dict +import pandas as pd +from datetime import datetime, timedelta +import time +import traceback + +from vnpy.trader.object import HistoryRequest, BarData +from vnpy.trader.constant import Exchange, Interval + +from utils import log + +CHINA_TZ = timezone("Asia/Shanghai") + +tushare_token: str = "" + +MAX_QUERY_SIZE: int = 5000 +TS_DATE_FORMATE: str = '%Y%m%d' +MAX_QUERY_TIMES: int = 500 + +EXCHANGE_TS2VT: Dict[str, Exchange] = { + 'SH': Exchange.SSE, + 'SZ': Exchange.SZSE +} + +EXCHANGE_VT2TS: Dict[Exchange, str] = {v: k for k, v in EXCHANGE_TS2VT.items()} + + +def to_ts_symbol(symbol: str, exchange: Exchange): + """ + 转换合约代码为tushare查询代码 + """ + if exchange == Exchange.SSE: + tcode = f'{symbol}' + '.' + f'{EXCHANGE_VT2TS[exchange]}' + elif exchange == Exchange.SZSE: + tcode = f'{symbol}' + '.' + f'{EXCHANGE_VT2TS[exchange]}' + else: + print("目前只研究深圳证券交易所和上海证券交易所A股股票!") + raise TypeError("目前只研究深圳证券交易所和上海证券交易所A股股票!") + return tcode + + +def to_split_ts_codes(tscode: str): + symbol, exchange_ts = tscode.split('.') + exchange = EXCHANGE_TS2VT[exchange_ts] + return symbol, exchange + + +class TuShareClient: + """ + 从TuShare中查询历史数据的Client + tushare日线数据说明:交易日每天15点~16点之间更新数据,daily接口是未复权行情,停牌期间不提供数据。 + tushare调取说明:基础积分每分钟内最多调取500次,每次5000条数据 + """ + + def __init__(self): + """""" + + self.pro: client.DataApi = None + + self.inited: bool = False + + # 获得所有股票代码 + self.symbols: pd.DataFrame = None + + # 获得交易日历 + self.trade_cal: Dict[str, pd.DataFrame] = None + + def init(self, token: str = "") -> bool: + """""" + if self.inited: + return True + + if token: + ts.set_token(tushare_token) + else: + ts.set_token(tushare_token) + + try: + self.pro = ts.pro_api() + self.stock_list() + self.trade_day_list() + except (BaseException, "tushare连接失败"): + return False + + self.inited = True + return True + + def query_history(self, req: HistoryRequest) -> Optional[List[BarData]]: + """ + 从tushare里查询历史数据 + :param req:查询请求 + :return: Optional[List[BarData]] + """ + if self.symbols is None: + return None + + symbol = req.symbol + exchange = req.exchange + interval = req.interval + start = req.start.strftime(TS_DATE_FORMATE) + end = req.end.strftime(TS_DATE_FORMATE) + + if interval is not Interval.DAILY: + return None + if exchange not in [Exchange.SSE, Exchange.SZSE]: + return None + + tscode = to_ts_symbol(symbol, exchange) + + # 修改查询数据逻辑,在每次5000条数据的限制下,很可能一次无法读取完 + cnt = 0 + df: pd.DataFrame = None + while datetime.strptime(start, TS_DATE_FORMATE) <= datetime.strptime(end, TS_DATE_FORMATE): + # 保证每次查询最多5000天数据 + start_date = datetime.strptime(start, TS_DATE_FORMATE) + simulate_end_date = min(datetime.strptime(end, TS_DATE_FORMATE), + start_date + timedelta(days=MAX_QUERY_SIZE)) + simulate_end = simulate_end_date.strftime(TS_DATE_FORMATE) + + # 保证每次调用时间在60/500=0.12秒内,以保证每分钟调用次数少于500次 + # begin_time = time.time() + tushare_df = None + while True: + try: + tushare_df = self.pro.query('daily', ts_code=tscode, start_date=start, end_date=simulate_end) + except (requests.exceptions.SSLError, requests.exceptions.ConnectionError) as e: + log.error(e) + # traceback.print_exc() + # ('Connection aborted.', ConnectionResetError(10054, '远程主机强迫关闭了一个现有的连接。', None, 10054, None)) + if '10054' in str(e): + sleep_time = 60.0 + log.info("请求过于频繁,sleep:" + str(sleep_time) + "s") + time.sleep(sleep_time) + log.info("继续发送请求:" + tscode) + continue # 继续发请求 + else: + raise Exception(e) # 其他异常,抛出来 + break + if tushare_df is not None: + if df is None: + df = tushare_df + else: + df = pd.concat([df, tushare_df], ignore_index=True) + # end_time = time.time() + # delta = round(end_time - begin_time, 3) + # if delta < 60 / MAX_QUERY_TIMES: + sleep_time = 0.50 + log.info("sleep:" + str(sleep_time) + "s") + time.sleep(sleep_time) + + cnt += 1 + start = (simulate_end_date + timedelta(days=1)).strftime(TS_DATE_FORMATE) + + data: List[BarData] = [] + + if df is not None: + for ix, row in df.iterrows(): + date = datetime.strptime(row.trade_date, '%Y%m%d') + date = CHINA_TZ.localize(date) + + if pd.isnull(row['open']): + log.info(symbol + '.' + EXCHANGE_VT2TS[exchange] + row['trade_date'] + "open_price为None") + elif pd.isnull(row['high']): + log.info(symbol + '.' + EXCHANGE_VT2TS[exchange] + row['trade_date'] + "high_price为None") + elif pd.isnull(row['low']): + log.info(symbol + '.' + EXCHANGE_VT2TS[exchange] + row['trade_date'] + "low_price为None") + elif pd.isnull(row['close']): + log.info(symbol + '.' + EXCHANGE_VT2TS[exchange] + row['trade_date'] + "close_price为None") + elif pd.isnull(row['amount']): + log.info(symbol + '.' + EXCHANGE_VT2TS[exchange] + row['trade_date'] + "volume为None") + + row.fillna(0) + bar = BarData( + symbol=symbol, + exchange=exchange, + interval=interval, + datetime=date, + open_price=row['open'], + high_price=row['high'], + low_price=row['low'], + close_price=row['close'], + volume=row['amount'], + gateway_name='tushare' + ) + + data.append(bar) + return data + + def stock_list(self): + """ + 调用tushare stock_basic 接口 + 获得上海证券交易所和深圳证券交易所所有股票代码 + 获取基础信息数据,包括股票代码、名称、上市日期、退市日期等 + :return: + """ + if self.symbols is None: + symbols_sse = self.pro.query('stock_basic', exchange=Exchange.SSE.value, fields='ts_code,symbol,name,' + 'fullname,enname,market,' + 'list_status,list_date,' + 'delist_date,is_hs') + symbols_szse = self.pro.query('stock_basic', exchange=Exchange.SZSE.value, fields='ts_code,symbol,name,' + 'fullname,enname,market,' + 'list_status,list_date,' + 'delist_date,is_hs') + self.symbols = pd.concat([symbols_sse, symbols_szse], axis=0, ignore_index=True) + + def trade_day_list(self): + """ + 查询交易日历 + :return: + """ + if self.trade_cal is None: + self.trade_cal = dict() + self.trade_cal[Exchange.SSE.value] = self.pro.query('trade_cal', exchange=Exchange.SSE.value, is_open='1') + self.trade_cal[Exchange.SZSE.value] = self.pro.query('trade_cal', exchange=Exchange.SZSE.value, is_open='1') + + +tushare_client = TuShareClient() + +if __name__ == "__main__": + print("测试TuShare数据接口") + # tushare_client = TuShareClient() + tushare_client.init() + # print(tushare_client.symbols) + # print(tushare_client.trade_cal) + + req = HistoryRequest(symbol='600600', exchange=Exchange.SSE, + start=datetime(year=1999, month=11, day=10), end=datetime.now(), interval=Interval.DAILY) + + ts_data = tushare_client.query_history(req) + print(len(ts_data)) diff --git a/utils/common/__init__.py b/utils/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/common/utils.py b/utils/common/utils.py new file mode 100644 index 0000000..3e0203f --- /dev/null +++ b/utils/common/utils.py @@ -0,0 +1,36 @@ +import logging + + +class logger: + def __init__(self, path, clevel=logging.INFO, Flevel=logging.INFO): + self.logger = logging.getLogger(path) + self.logger.setLevel(logging.DEBUG) + fmt = logging.Formatter('[%(asctime)s] [%(levelname)s] %(message)s', '%Y-%m-%d %H:%M:%S') + # 设置CMD日志 + sh = logging.StreamHandler() + sh.setFormatter(fmt) + sh.setLevel(clevel) + # 设置文件日志 + fh = logging.FileHandler(path, encoding='utf-8') + fh.setFormatter(fmt) + fh.setLevel(Flevel) + self.logger.addHandler(sh) + self.logger.addHandler(fh) + + def debug(self, message): + self.logger.debug(message) + + def info(self, message): + self.logger.info(message) + + def war(self, message): + self.logger.warn(message) + + def error(self, message): + self.logger.error(message) + + def cri(self, message): + self.logger.critical(message) + + +log = logger("log.txt") diff --git a/utils/configure/ util.py b/utils/configure/ util.py new file mode 100644 index 0000000..7cd12a0 --- /dev/null +++ b/utils/configure/ util.py @@ -0,0 +1,237 @@ +# -*- coding: utf-8 -*- + +import datetime +import random +import smtplib +import time +import warnings +from email.mime.text import MIMEText +from email.header import Header +from email.utils import parseaddr, formataddr +import json +import pandas as pd +import re +import requests +from .settings import config, get_config_data,DBSelector + + +def notify(title='', desp=''): + warnings.warn("该接口需要收费了,请使用企业微信") + url = f"https://sc.ftqq.com/{config['WECHAT_ID']}.send?text={title}&desp={desp}" + try: + res = requests.get(url, timeout=5) + except Exception as e: + print(e) + return False + + else: + try: + js = res.json() + result = True if js['data']['errno'] == 0 else False + if result: + print('发送成功') + return True + else: + print('发送失败') + return False + + except Exception as e: + print(e) + print(res.text) + + +def read_web_headers_cookies(website, headers=False, cookies=False): + config = get_config_data('web_headers.json') + return_headers = None + return_cookies = None + + if headers: + return_headers = config[website]['headers'] + + if cookies: + return_headers = config[website]['cookies'] + + return return_headers, return_cookies + + +def send_message_via_wechat(_message): # 默认发送给自己 + _config = config['enterprise_wechat'] + userid = _config['userid'] + agentid = _config['agentid'] + corpid = _config['corpid'] + corpsecret = _config['corpsecret'] + + response = requests.get(f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={corpid}&corpsecret={corpsecret}") + data = json.loads(response.text) + access_token = data['access_token'] + + json_dict = { + "touser": userid, + "msgtype": "text", + "agentid": agentid, + "text": { + "content": _message + }, + "safe": 0, + "enable_id_trans": 0, + "enable_duplicate_check": 0, + "duplicate_check_interval": 1800 + } + json_str = json.dumps(json_dict) + response_send = requests.post(f"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}", + data=json_str) + return json.loads(response_send.text)['errmsg'] == 'ok' + + +def rsa_encrypt(): + import rsa + (pubkey, privkey) = rsa.newkeys(1024) + print('pubkey >>>> {}'.format(pubkey)) + print('privkey >>>> {}'.format(privkey)) + + with open('pub.pem', 'w') as f: + f.write(pubkey.save_pkcs1().decode()) + + with open('datasender.pem', 'w') as f: + f.write(privkey.save_pkcs1().decode()) + + message = '' + print("message encode {}".format(message.encode())) + crypto = rsa.encrypt(message.encode(), pubkey) # 加密数据为bytes + + print('密文:\n{}'.format(crypto)) + + with open('encrypt.bin', 'wb') as f: + f.write(crypto) + # 解密 + e_message = rsa.decrypt(crypto, privkey) # 解密数据也是为bytes + print("解密后\n{}".format(e_message.decode())) + + +def rsa_decrypt(): + import rsa + with open('encrypt.bin', 'rb') as f: + content = f.read() + + file = 'priva.pem' + with open(file, 'r') as f: + privkey = rsa.PrivateKey.load_pkcs1(f.read().encode()) + + e_message = rsa.decrypt(content, privkey) # 解密数据也是为bytes + print("解密后\n{}".format(e_message.decode())) + + +def market_status(): + ''' + 收盘 + ''' + now = datetime.datetime.now() + end = datetime.datetime(now.year, now.month, now.day, 15, 2, 5) + return True if now < end else False + + +def _format_addr(s): + name, addr = parseaddr(s) + return formataddr((Header(name, 'utf-8').encode(), addr)) + + +def send_from_aliyun(title, content, TO_MAIL_=config['mail']['qq']['user'], types='plain'): + username = config['aliyun']['EMAIL_USER_ALI'] # 阿里云 + password = config['aliyun']['LOGIN_EMAIL_ALYI_PASSWORD'] # 阿里云 + stmp = smtplib.SMTP() + + msg = MIMEText(content, types, 'utf-8') + subject = title + msg['Subject'] = Header(subject, 'utf-8') + msg['From'] = _format_addr('{} <{}>'.format('数据推送', username)) + msg['To'] = TO_MAIL_ + + try: + stmp.connect('smtp.qiye.aliyun.com', 25) + stmp.login(username, password) + stmp.sendmail(username, TO_MAIL_, msg.as_string()) + except Exception as e: + time.sleep(10 + random.randint(1, 5)) + stmp = smtplib.SMTP() + stmp.connect('smtp.qiye.aliyun.com', 25) + stmp.login(username, password) + stmp.sendmail(username, TO_MAIL_, msg.as_string()) + else: + print('发送完毕') + + +def send_sms(content): + ''' + 一个海外的短信接口 + ''' + from twilio.rest import Client + + client = Client(config.twilio_account_sid, config.twilio_auth_token) + try: + message = client.messages.create( + body=content, + from_=config.FROM_MOBILE, + to=config.TO_MOBILE + ) + except Exception as e: + print(e) + + + +def jsonp2json(str_): + return json.loads(str_[str_.find('{'):str_.rfind('}') + 1]) + +def js2json(str_): + import demjson + return demjson.decode(str_[str_.find('{'):str_.rfind('}') + 1]) + +def bond_filter(code): + m = re.search('^(11|12)', code) + return True if m else False + + +def get_holding_list(filename=None): + ''' + 获取持仓列表 + ''' + df = pd.read_csv(filename, encoding='gbk') + df['证券代码'] = df['证券代码'].astype(str) + df['kzz'] = df['证券代码'].map(bond_filter) + df = df[df['kzz'] == True] + return df['证券代码'].tolist() + +def mongo_convert_df(doc,condition=None,project=None): + import pandas as pd + result =[] + for item in doc.find(condition,project): + result.append(item) + return pd.DataFrame(result) + +def get_jsl_code(table): + # from settings import DBSelector + engine = DBSelector().get_engine('db_stock','kh') + df = pd.read_sql(table,engine) + return df + +def fmt_date(x,src='%Y%m%d',trgt='%Y-%m-%d'): + return datetime.datetime.strptime(x, src).strftime(trgt) + + +def calendar(start_date,end_date): + from .settings import get_tushare_pro + + src='%Y-%m-%d' + trgt='%Y%m%d' + start_date = fmt_date(start_date,src,trgt) + end_date = fmt_date(end_date,src,trgt) + + pro = get_tushare_pro() + df = pro.trade_cal(exchange='SSE', start_date=start_date, end_date=end_date, is_open='1') + + cal = df['trade_date'].tolist() + cal = list(map(fmt_date, cal)) + + return cal + +if __name__ == '__main__': + print(get_jsl_code('tb_bond_jisilu')) diff --git a/utils/configure/__init__.py b/utils/configure/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/configure/config.json b/utils/configure/config.json new file mode 100644 index 0000000..2601a3e --- /dev/null +++ b/utils/configure/config.json @@ -0,0 +1,99 @@ +{ + "mysql": { + "local": { + "host": "", + "port": 3306, + "user": "", + "password": "" + }, + "qq": { + "host": "", + "port": 2222, + "user": "", + "password": "" + }, + "ubuntu": { + "host": "1", + "port": 3306, + "user": "", + "password": "" + }, + "ptrade": { + "host": "", + "port": 3306, + "user": "", + "password": "" + }, + "tencent-1c": { + "host": "", + "port": 3306, + "user": "", + "password": "" + } + }, + "redis": { + "qq": { + "host": "127.0.0.1", + "port": 6379, + "password": "" + } + }, + "mail": { + "qq": { + "user": "" + } + }, + "data_path": "", + "mongo": { + "qq": { + "host": "", + "port": 11111, + "user": "", + "password": "" + }, + "local": { + "host": "127.0.0.1", + "port": 17017, + "user": null, + "password": null + } + }, + "WECHAT_ID": "", + "jsl_cookies": { + "auto_reload": "", + "kbzw_r_uname": "", + "kbz_newcookie": "1", + "kbzw__Session": "", + "Hm_lvt_164fe01b1433a19b507595a43bf58262": "", + "Hm_lpvt_164fe01b1433a19b507595a43bf58262": "" + }, + "twillio": { + "twilio_account_sid": "", + "twilio_auth_token": "" + }, + "jsl_monitor": { + "EXPIRE_TIME": 1800, + "MONITOR_PERCENT": 8, + "ACCESS_INTERVAL": 20, + "JSL_USER": "", + "JSL_PASSWORD": "" + }, + + "aliyun": { + "EMAIL_USER_XT": "", + "EMAIL_USER_ALI": "", + "LOGIN_EMAIL_ALYI_PASSWORD": "" + }, + "ts_token": "", + "holding_file": "v", + "xc_server": "", + "xc_token_pro": "", + + "enterprise_wechat": { + "userid": "", + "agentid": "", + "corpid": "", + "corpsecret": "" + } + } + \ No newline at end of file diff --git a/utils/configure/sample_config.json b/utils/configure/sample_config.json new file mode 100644 index 0000000..2601a3e --- /dev/null +++ b/utils/configure/sample_config.json @@ -0,0 +1,99 @@ +{ + "mysql": { + "local": { + "host": "", + "port": 3306, + "user": "", + "password": "" + }, + "qq": { + "host": "", + "port": 2222, + "user": "", + "password": "" + }, + "ubuntu": { + "host": "1", + "port": 3306, + "user": "", + "password": "" + }, + "ptrade": { + "host": "", + "port": 3306, + "user": "", + "password": "" + }, + "tencent-1c": { + "host": "", + "port": 3306, + "user": "", + "password": "" + } + }, + "redis": { + "qq": { + "host": "127.0.0.1", + "port": 6379, + "password": "" + } + }, + "mail": { + "qq": { + "user": "" + } + }, + "data_path": "", + "mongo": { + "qq": { + "host": "", + "port": 11111, + "user": "", + "password": "" + }, + "local": { + "host": "127.0.0.1", + "port": 17017, + "user": null, + "password": null + } + }, + "WECHAT_ID": "", + "jsl_cookies": { + "auto_reload": "", + "kbzw_r_uname": "", + "kbz_newcookie": "1", + "kbzw__Session": "", + "Hm_lvt_164fe01b1433a19b507595a43bf58262": "", + "Hm_lpvt_164fe01b1433a19b507595a43bf58262": "" + }, + "twillio": { + "twilio_account_sid": "", + "twilio_auth_token": "" + }, + "jsl_monitor": { + "EXPIRE_TIME": 1800, + "MONITOR_PERCENT": 8, + "ACCESS_INTERVAL": 20, + "JSL_USER": "", + "JSL_PASSWORD": "" + }, + + "aliyun": { + "EMAIL_USER_XT": "", + "EMAIL_USER_ALI": "", + "LOGIN_EMAIL_ALYI_PASSWORD": "" + }, + "ts_token": "", + "holding_file": "v", + "xc_server": "", + "xc_token_pro": "", + + "enterprise_wechat": { + "userid": "", + "agentid": "", + "corpid": "", + "corpsecret": "" + } + } + \ No newline at end of file diff --git a/utils/configure/settings.py b/utils/configure/settings.py new file mode 100644 index 0000000..620cddb --- /dev/null +++ b/utils/configure/settings.py @@ -0,0 +1,104 @@ +''' +Author: Charmve yidazhang1@gmail.com +Date: 2023-03-02 00:12:18 +LastEditors: Charmve yidazhang1@gmail.com +LastEditTime: 2023-03-09 23:29:18 +FilePath: /Qbot/utils/configure/settings.py +Version: 1.0.1 +Blogs: charmve.blog.csdn.net +Description: + +Copyright (c) 2023 by Charmve, All Rights Reserved. +''' +# -*-coding=utf-8-*- + +import os +import json + + +def get_config_data(config_file='config.json'): + json_file = os.path.join(os.path.dirname(__file__), config_file) + with open(json_file, 'r', encoding='utf8') as f: + _config = json.load(f) + return _config + + +config = get_config_data() + + +def config_dict(*args): + result = config + for arg in args: + try: + result = result[arg] + except: + print('找不到对应的key') + return None + + return result + + +class DBSelector(object): + ''' + 数据库选择类 + ''' + + def __init__(self): + self.json_data = config + + def config(self, db_type='mysql', local='qq'): + db_dict = self.json_data[db_type][local] + user = db_dict['user'] + password = db_dict['password'] + host = db_dict['host'] + port = db_dict['port'] + return (user, password, host, port) + + def get_engine(self, db, type_='qq'): + from sqlalchemy import create_engine + user, password, host, port = self.config(db_type='mysql', local=type_) + try: + engine = create_engine( + 'mysql+pymysql://{}:{}@{}:{}/{}?charset=utf8'.format(user, password, host, port, db)) + except Exception as e: + print(e) + return None + return engine + + def get_mysql_conn(self, db, type_='qq',use_dict=False): + import pymysql + user, password, host, port = self.config(db_type='mysql', local=type_) + try: + if use_dict: + conn = pymysql.connect(host=host, port=port, user=user, password=password, db=db, charset='utf8mb4',read_timeout=10,cursorclass= pymysql.cursors.DictCursor) + else: + conn = pymysql.connect(host=host, port=port, user=user, password=password, db=db, charset='utf8mb4',read_timeout=10) + except Exception as e: + print(e) + return None + else: + return conn + + def mongo(self, location_type='qq', async_type=False): + user, password, host, port = self.config('mongo', location_type) + connect_uri = f'mongodb://{user}:{password}@{host}:{port}' + if async_type: + from motor.motor_asyncio import AsyncIOMotorClient + client = AsyncIOMotorClient(connect_uri) + else: + import pymongo + client = pymongo.MongoClient(connect_uri) + return client + + +def get_tushare_pro(): + import xcsc_tushare as xc + xc_token_pro = config.get('xc_token_pro') + xc_server = config.get('xc_server') + xc.set_token(xc_token_pro) + pro = xc.pro_api(env='prd', server=xc_server) + return pro + + +if __name__ == '__main__': + pass \ No newline at end of file diff --git a/utils/thsauto b/utils/thsauto new file mode 160000 index 0000000..b7cd763 --- /dev/null +++ b/utils/thsauto @@ -0,0 +1 @@ +Subproject commit b7cd76359ae23f627190ec03fda3f4a58802f097