From 0aa2901f4c318d03124445c97176d3f9724c644f Mon Sep 17 00:00:00 2001 From: XiangJianxiong <1152203250@qq.com> Date: Tue, 8 Oct 2024 14:06:24 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=AC=E6=AC=A1=E7=9A=84=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 23 + .idea/.gitignore | 8 + .idea/.name | 1 + .idea/dataSources.xml | 12 + .idea/inspectionProfiles/Project_Default.xml | 146 +++++++ .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/other.xml | 6 + .idea/sqldialects.xml | 6 + .idea/wance_data.iml | 15 + banner.txt | 19 + migrations/models/3_20240909094115_None.py | 239 ++++++++++ models.py | Bin 0 -> 40996 bytes pyproject.toml | 4 + src/__init__.py | 4 + src/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 246 bytes src/__pycache__/constants.cpython-311.pyc | Bin 0 -> 2255 bytes src/__pycache__/exceptions.cpython-311.pyc | Bin 0 -> 6672 bytes src/__pycache__/main.cpython-311.pyc | Bin 0 -> 2123 bytes src/__pycache__/responses.cpython-311.pyc | Bin 0 -> 2955 bytes src/__pycache__/tortoises.cpython-311.pyc | Bin 0 -> 3469 bytes .../tortoises_orm_config.cpython-311.pyc | Bin 0 -> 1658 bytes .../bollinger_bands.cpython-311.pyc | Bin 0 -> 12258 bytes .../dual_moving_average.cpython-311.pyc | Bin 0 -> 12001 bytes .../reverse_dual_ma_strategy.cpython-311.pyc | Bin 0 -> 11982 bytes .../__pycache__/router.cpython-311.pyc | Bin 0 -> 1211 bytes .../__pycache__/rsi_strategy.cpython-311.pyc | Bin 0 -> 14095 bytes .../__pycache__/service.cpython-311.pyc | Bin 0 -> 2476 bytes .../__pycache__/until.cpython-311.pyc | Bin 0 -> 4845 bytes src/backtest/backtest.py | 106 +++++ src/backtest/bollinger.py | 79 ++++ src/backtest/bollinger_bands.py | 274 ++++++++++++ src/backtest/dual_moving_average.py | 267 ++++++++++++ src/backtest/living_backtesting.py | 92 ++++ src/backtest/macd_strategy.py | 271 ++++++++++++ src/backtest/reverse_dual_ma_strategy.py | 264 +++++++++++ src/backtest/router.py | 23 + src/backtest/rsi_strategy.py | 296 +++++++++++++ src/backtest/service.py | 74 ++++ src/backtest/until.py | 99 +++++ src/constants.py | 43 ++ readme.md => src/data_processing/__init__.py | 0 .../history_data_processing.py | 412 ++++++++++++++++++ src/data_processing/response_factor.py | 15 + src/exceptions.py | 92 ++++ src/main.py | 52 +++ src/models/__init__.py | 292 +++++++++++++ .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 12289 bytes .../back_observed_data.cpython-311.pyc | Bin 0 -> 1268 bytes .../back_observed_data_detail.cpython-311.pyc | Bin 0 -> 1142 bytes .../__pycache__/back_position.cpython-311.pyc | Bin 0 -> 1174 bytes .../back_result_indicator.cpython-311.pyc | Bin 0 -> 1127 bytes .../back_trand_info.cpython-311.pyc | Bin 0 -> 1104 bytes .../__pycache__/backtest.cpython-311.pyc | Bin 0 -> 2142 bytes src/models/__pycache__/order.cpython-311.pyc | Bin 0 -> 4680 bytes .../__pycache__/position.cpython-311.pyc | Bin 0 -> 2693 bytes .../security_account.cpython-311.pyc | Bin 0 -> 2013 bytes .../__pycache__/snowball.cpython-311.pyc | Bin 0 -> 1275 bytes src/models/__pycache__/stock.cpython-311.pyc | Bin 0 -> 1569 bytes .../stock_bt_history.cpython-311.pyc | Bin 0 -> 1712 bytes .../stock_data_processing.cpython-311.pyc | Bin 0 -> 1793 bytes .../__pycache__/stock_details.cpython-311.pyc | Bin 0 -> 1792 bytes .../__pycache__/stock_history.cpython-311.pyc | Bin 0 -> 1456 bytes .../__pycache__/strategy.cpython-311.pyc | Bin 0 -> 1988 bytes .../__pycache__/test_table.cpython-311.pyc | Bin 0 -> 2585 bytes .../tran_observer_data.cpython-311.pyc | Bin 0 -> 1210 bytes .../__pycache__/tran_orders.cpython-311.pyc | Bin 0 -> 1132 bytes .../__pycache__/tran_position.cpython-311.pyc | Bin 0 -> 1174 bytes .../__pycache__/tran_return.cpython-311.pyc | Bin 0 -> 1164 bytes .../tran_trade_info.cpython-311.pyc | Bin 0 -> 1165 bytes .../__pycache__/trand_info.cpython-311.pyc | Bin 0 -> 1205 bytes .../__pycache__/transaction.cpython-311.pyc | Bin 0 -> 2655 bytes .../wance_data_stock.cpython-311.pyc | Bin 0 -> 4433 bytes ...ance_data_storage_backtest.cpython-311.pyc | Bin 0 -> 8099 bytes src/models/back_observed_data.py | 13 + src/models/back_observed_data_detail.py | 12 + src/models/back_position.py | 14 + src/models/back_result_indicator.py | 14 + src/models/back_trand_info.py | 13 + src/models/backtest.py | 25 ++ src/models/backtest_parameters.py | 23 + src/models/observed_data.py | 123 ++++++ src/models/order.py | 68 +++ src/models/page_Info.py | 65 +++ src/models/position.py | 36 ++ src/models/security_account.py | 31 ++ src/models/snowball.py | 21 + src/models/stock.py | 23 + src/models/stock_bt_history.py | 17 + src/models/stock_data_processing.py | 19 + src/models/stock_details.py | 25 ++ src/models/stock_history.py | 13 + src/models/strategy.py | 19 + src/models/test_table.py | 35 ++ src/models/tran_observer_data.py | 13 + src/models/tran_orders.py | 14 + src/models/tran_position.py | 13 + src/models/tran_return.py | 13 + src/models/tran_trade_info.py | 12 + src/models/trand_info.py | 30 ++ src/models/transaction.py | 30 ++ src/models/transaction_strategy.py | 19 + src/models/user.py | 27 ++ src/models/wance_data_stock.py | 43 ++ src/models/wance_data_storage_backtest.py | 67 +++ .../backtest_request.cpython-311.pyc | Bin 0 -> 3881 bytes .../codelistrequest.cpython-311.pyc | Bin 0 -> 549 bytes .../factor_request.cpython-311.pyc | Bin 0 -> 3494 bytes .../__pycache__/request_data.cpython-311.pyc | Bin 0 -> 2640 bytes src/pydantic/backtest_request.py | 29 ++ src/pydantic/codelistrequest.py | 5 + src/pydantic/factor_request.py | 140 ++++++ src/pydantic/request_data.py | 21 + src/responses.py | 68 +++ src/settings/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 168 bytes .../__pycache__/config.cpython-311.pyc | Bin 0 -> 2084 bytes src/settings/config.py | 52 +++ src/tests/__init__.py | 0 src/tests/stock_query.py | 37 ++ src/tests/xtquan_data_test.py | 78 ++++ src/tortoises.py | 185 ++++++++ src/tortoises_orm_config.py | 30 ++ src/utils/__init__.py | 37 ++ .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 2193 bytes src/utils/__pycache__/helpers.cpython-311.pyc | Bin 0 -> 2231 bytes ...tory_data_processing_utils.cpython-311.pyc | Bin 0 -> 11896 bytes src/utils/__pycache__/models.cpython-311.pyc | Bin 0 -> 2529 bytes .../__pycache__/paginations.cpython-311.pyc | Bin 0 -> 3022 bytes src/utils/__pycache__/redis.cpython-311.pyc | Bin 0 -> 2475 bytes src/utils/helpers.py | 55 +++ src/utils/history_data_processing_utils.py | 249 +++++++++++ src/utils/history_stock.py | 40 ++ src/utils/jsonformatter.py | 15 + src/utils/models.py | 37 ++ src/utils/paginations.py | 74 ++++ src/utils/redis.py | 31 ++ src/utils/remove_duplicates_databases.py | 40 ++ src/utils/split_stock_utils.py | 41 ++ src/xtdata/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 166 bytes src/xtdata/__pycache__/router.cpython-311.pyc | Bin 0 -> 8984 bytes .../__pycache__/service.cpython-311.pyc | Bin 0 -> 13079 bytes src/xtdata/router.py | 202 +++++++++ src/xtdata/service.py | 363 +++++++++++++++ 146 files changed, 5974 insertions(+) create mode 100644 .env create mode 100644 .idea/.gitignore create mode 100644 .idea/.name create mode 100644 .idea/dataSources.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/other.xml create mode 100644 .idea/sqldialects.xml create mode 100644 .idea/wance_data.iml create mode 100644 banner.txt create mode 100644 migrations/models/3_20240909094115_None.py create mode 100644 models.py create mode 100644 pyproject.toml create mode 100644 src/__init__.py create mode 100644 src/__pycache__/__init__.cpython-311.pyc create mode 100644 src/__pycache__/constants.cpython-311.pyc create mode 100644 src/__pycache__/exceptions.cpython-311.pyc create mode 100644 src/__pycache__/main.cpython-311.pyc create mode 100644 src/__pycache__/responses.cpython-311.pyc create mode 100644 src/__pycache__/tortoises.cpython-311.pyc create mode 100644 src/__pycache__/tortoises_orm_config.cpython-311.pyc create mode 100644 src/backtest/__pycache__/bollinger_bands.cpython-311.pyc create mode 100644 src/backtest/__pycache__/dual_moving_average.cpython-311.pyc create mode 100644 src/backtest/__pycache__/reverse_dual_ma_strategy.cpython-311.pyc create mode 100644 src/backtest/__pycache__/router.cpython-311.pyc create mode 100644 src/backtest/__pycache__/rsi_strategy.cpython-311.pyc create mode 100644 src/backtest/__pycache__/service.cpython-311.pyc create mode 100644 src/backtest/__pycache__/until.cpython-311.pyc create mode 100644 src/backtest/backtest.py create mode 100644 src/backtest/bollinger.py create mode 100644 src/backtest/bollinger_bands.py create mode 100644 src/backtest/dual_moving_average.py create mode 100644 src/backtest/living_backtesting.py create mode 100644 src/backtest/macd_strategy.py create mode 100644 src/backtest/reverse_dual_ma_strategy.py create mode 100644 src/backtest/router.py create mode 100644 src/backtest/rsi_strategy.py create mode 100644 src/backtest/service.py create mode 100644 src/backtest/until.py create mode 100644 src/constants.py rename readme.md => src/data_processing/__init__.py (100%) create mode 100644 src/data_processing/history_data_processing.py create mode 100644 src/data_processing/response_factor.py create mode 100644 src/exceptions.py create mode 100644 src/main.py create mode 100644 src/models/__init__.py create mode 100644 src/models/__pycache__/__init__.cpython-311.pyc create mode 100644 src/models/__pycache__/back_observed_data.cpython-311.pyc create mode 100644 src/models/__pycache__/back_observed_data_detail.cpython-311.pyc create mode 100644 src/models/__pycache__/back_position.cpython-311.pyc create mode 100644 src/models/__pycache__/back_result_indicator.cpython-311.pyc create mode 100644 src/models/__pycache__/back_trand_info.cpython-311.pyc create mode 100644 src/models/__pycache__/backtest.cpython-311.pyc create mode 100644 src/models/__pycache__/order.cpython-311.pyc create mode 100644 src/models/__pycache__/position.cpython-311.pyc create mode 100644 src/models/__pycache__/security_account.cpython-311.pyc create mode 100644 src/models/__pycache__/snowball.cpython-311.pyc create mode 100644 src/models/__pycache__/stock.cpython-311.pyc create mode 100644 src/models/__pycache__/stock_bt_history.cpython-311.pyc create mode 100644 src/models/__pycache__/stock_data_processing.cpython-311.pyc create mode 100644 src/models/__pycache__/stock_details.cpython-311.pyc create mode 100644 src/models/__pycache__/stock_history.cpython-311.pyc create mode 100644 src/models/__pycache__/strategy.cpython-311.pyc create mode 100644 src/models/__pycache__/test_table.cpython-311.pyc create mode 100644 src/models/__pycache__/tran_observer_data.cpython-311.pyc create mode 100644 src/models/__pycache__/tran_orders.cpython-311.pyc create mode 100644 src/models/__pycache__/tran_position.cpython-311.pyc create mode 100644 src/models/__pycache__/tran_return.cpython-311.pyc create mode 100644 src/models/__pycache__/tran_trade_info.cpython-311.pyc create mode 100644 src/models/__pycache__/trand_info.cpython-311.pyc create mode 100644 src/models/__pycache__/transaction.cpython-311.pyc create mode 100644 src/models/__pycache__/wance_data_stock.cpython-311.pyc create mode 100644 src/models/__pycache__/wance_data_storage_backtest.cpython-311.pyc create mode 100644 src/models/back_observed_data.py create mode 100644 src/models/back_observed_data_detail.py create mode 100644 src/models/back_position.py create mode 100644 src/models/back_result_indicator.py create mode 100644 src/models/back_trand_info.py create mode 100644 src/models/backtest.py create mode 100644 src/models/backtest_parameters.py create mode 100644 src/models/observed_data.py create mode 100644 src/models/order.py create mode 100644 src/models/page_Info.py create mode 100644 src/models/position.py create mode 100644 src/models/security_account.py create mode 100644 src/models/snowball.py create mode 100644 src/models/stock.py create mode 100644 src/models/stock_bt_history.py create mode 100644 src/models/stock_data_processing.py create mode 100644 src/models/stock_details.py create mode 100644 src/models/stock_history.py create mode 100644 src/models/strategy.py create mode 100644 src/models/test_table.py create mode 100644 src/models/tran_observer_data.py create mode 100644 src/models/tran_orders.py create mode 100644 src/models/tran_position.py create mode 100644 src/models/tran_return.py create mode 100644 src/models/tran_trade_info.py create mode 100644 src/models/trand_info.py create mode 100644 src/models/transaction.py create mode 100644 src/models/transaction_strategy.py create mode 100644 src/models/user.py create mode 100644 src/models/wance_data_stock.py create mode 100644 src/models/wance_data_storage_backtest.py create mode 100644 src/pydantic/__pycache__/backtest_request.cpython-311.pyc create mode 100644 src/pydantic/__pycache__/codelistrequest.cpython-311.pyc create mode 100644 src/pydantic/__pycache__/factor_request.cpython-311.pyc create mode 100644 src/pydantic/__pycache__/request_data.cpython-311.pyc create mode 100644 src/pydantic/backtest_request.py create mode 100644 src/pydantic/codelistrequest.py create mode 100644 src/pydantic/factor_request.py create mode 100644 src/pydantic/request_data.py create mode 100644 src/responses.py create mode 100644 src/settings/__init__.py create mode 100644 src/settings/__pycache__/__init__.cpython-311.pyc create mode 100644 src/settings/__pycache__/config.cpython-311.pyc create mode 100644 src/settings/config.py create mode 100644 src/tests/__init__.py create mode 100644 src/tests/stock_query.py create mode 100644 src/tests/xtquan_data_test.py create mode 100644 src/tortoises.py create mode 100644 src/tortoises_orm_config.py create mode 100644 src/utils/__init__.py create mode 100644 src/utils/__pycache__/__init__.cpython-311.pyc create mode 100644 src/utils/__pycache__/helpers.cpython-311.pyc create mode 100644 src/utils/__pycache__/history_data_processing_utils.cpython-311.pyc create mode 100644 src/utils/__pycache__/models.cpython-311.pyc create mode 100644 src/utils/__pycache__/paginations.cpython-311.pyc create mode 100644 src/utils/__pycache__/redis.cpython-311.pyc create mode 100644 src/utils/helpers.py create mode 100644 src/utils/history_data_processing_utils.py create mode 100644 src/utils/history_stock.py create mode 100644 src/utils/jsonformatter.py create mode 100644 src/utils/models.py create mode 100644 src/utils/paginations.py create mode 100644 src/utils/redis.py create mode 100644 src/utils/remove_duplicates_databases.py create mode 100644 src/utils/split_stock_utils.py create mode 100644 src/xtdata/__init__.py create mode 100644 src/xtdata/__pycache__/__init__.cpython-311.pyc create mode 100644 src/xtdata/__pycache__/router.cpython-311.pyc create mode 100644 src/xtdata/__pycache__/service.cpython-311.pyc create mode 100644 src/xtdata/router.py create mode 100644 src/xtdata/service.py diff --git a/.env b/.env new file mode 100644 index 0000000..42bf6bc --- /dev/null +++ b/.env @@ -0,0 +1,23 @@ +;使用本地docker数据库就将这段代码解开,如何将IP换成本机IP或者localhost +; DATABASE_URL="mysql://root:123456@10.1.5.219:3306/ftrade" +; DATABASE_CREATE_URL ="mysql://root:123456@10.1.5.219:3306/ftrade" + +DATABASE_URL="mysql://wangche:fN7sXX8saiQKXWbG@cqxqg.tech:3308/wangche" +DATABASE_CREATE_URL ="mysql://wangche:fN7sXX8saiQKXWbG@cqxqg.tech:3308/wangche" + +REDIS_URL="redis://:redis_tQNjCH@cqxqg.tech:6380" + +SITE_DOMAIN=127.0.0.1 +SECURE_COOKIES=false + +ENVIRONMENT=LOCAL + +CORS_HEADERS=["*"] +CORS_ORIGINS=["http://localhost:3000"] + +# postgres variables, must be the same as in DATABASE_URL +POSTGRES_USER=app +POSTGRES_PASSWORD=app +POSTGRES_HOST=app_db +POSTGRES_PORT=5432 +POSTGRES_DB=app \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..7bfcf52 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +wance_data \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..1e41358 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + mysql.8 + true + com.mysql.cj.jdbc.Driver + jdbc:mysql://lawyer5.cn:3308/wangche + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..dd1720e --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,146 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..39831c4 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..4c5c71c --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/other.xml b/.idea/other.xml new file mode 100644 index 0000000..2e75c2e --- /dev/null +++ b/.idea/other.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..6743fb4 --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/wance_data.iml b/.idea/wance_data.iml new file mode 100644 index 0000000..d33acca --- /dev/null +++ b/.idea/wance_data.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/banner.txt b/banner.txt new file mode 100644 index 0000000..d37d152 --- /dev/null +++ b/banner.txt @@ -0,0 +1,19 @@ + # ## # # # ## ## # # # # # # # # # # # # + ## ## # ## ## ## # ########## # ## # # #### ## ## ## ## # ############## ### ## ############# + ## ############# ## ## ## # ## # ############## # #### # ## ## ## # ############### ## ## ## ## ## ## ## + ## ## # ## ## ## # ## ############### ## # ## ## ## ## ## ########## ## ## ## ## ## ## ## # ## + # ### ########### ############### # # ## # ## ## # ## #### ###### ## ## # ## ## ####### ## ## ######### ## + ## ## # ## # ## ## ## ## ########### ## ## # ## ###### # ## ## ## # ########### ## # ## ## ## ### ## + ## ## ## ############### ### ## ## # ## ## ## ## ########### # ## # ## ## ######## ## ## ## ### ## ## ## # ### # # ## + ## ## ## # # #### ## ## ## ########## ## ## ## #### ## ## ## # # ## # ## ## ## # ####### ## ### ### ######## ## + ## ## ### ########## ##### ###### ## ## ## ## ## ## ## ##### ## ## # ### ## ## ######## ## ## ### # # ## # ## ## ## + ## ## ### ## ## # ## # ## ## # ########## ## ## # ### # # ##### ### # ## ## ## ## ## ## # # ## ## ## ## ## + ## ## ## ######### # ## ## ## ## # ## # ## ## # ## # ## #### #### #### ## ## ## ## ## ## ## ## ## ## ## ## + # ## # ## ## ## ## ## # ## # ## ## # # # ## ### ## # ## ## ######## ##### ## ### ## ## ####### ## + ## ######### ## ## ## ## ########### # ## ## ## ## ## ## #### ## ## ## # ## ### ## ### # # ## + ## ## ## ## ## ## # ## # # ## # ## ## ## ## ### ## ## ## #### ### ########### ## + #### ## #### ## ###### # ## # # # ### # ## ## ### ## ### ## ### ## #### ### ##### ## #### + # # # # # # ############### # # # # # # # # # # ## # # # # # +========================================================================================================================================================================================================================== +*********************************************************************************************聪明帅气可爱睿智的老板保佑我永不报错************************************************************************************************ +========================================================================================================================================================================================================================== diff --git a/migrations/models/3_20240909094115_None.py b/migrations/models/3_20240909094115_None.py new file mode 100644 index 0000000..98b6f97 --- /dev/null +++ b/migrations/models/3_20240909094115_None.py @@ -0,0 +1,239 @@ +from tortoise import BaseDBAsyncClient + + +async def upgrade(db: BaseDBAsyncClient) -> str: + return """ + CREATE TABLE IF NOT EXISTS `users` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键', + `nickname` VARCHAR(30) COMMENT '用户昵称', + `avatar_url` VARCHAR(255) COMMENT '头像', + `member_type` INT COMMENT '会员类型', + `beta_account_type` VARCHAR(30) COMMENT '内测账号类型', + `pre_cost_time` INT COMMENT '预支付时间(单位年)', + `qr_code` VARCHAR(255) COMMENT '专属客服二维码', + `dedicated_id` INT COMMENT '专属客服id', + `invited_user_id` INT COMMENT '邀请人id', + `created_user_id` INT COMMENT '创建人id', + `is_deleted` BOOL NOT NULL COMMENT '是否删除' DEFAULT 0, + `login_at` DATETIME(6) COMMENT '最后一次登录时间', + `created_at` DATETIME(6) NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` DATETIME(6) NOT NULL COMMENT '修改时间' DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `deleted_at` DATETIME(6) COMMENT '删除时间', + KEY `idx_users_is_dele_9cdc79` (`is_deleted`) +) CHARACTER SET utf8mb4 COMMENT='用户'; +CREATE TABLE IF NOT EXISTS `stock` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键', + `stock_code` VARCHAR(30) NOT NULL COMMENT '股票代码', + `stock_name` VARCHAR(30) COMMENT '股票名称', + `type` VARCHAR(2) COMMENT '类型', + `stock_pinyin` VARCHAR(30) NOT NULL COMMENT '股票拼音' +) CHARACTER SET utf8mb4 COMMENT='股票相关信息'; +CREATE TABLE IF NOT EXISTS `security_account` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键', + `securities_name` VARCHAR(30) NOT NULL COMMENT '证券公司名字昵称', + `fund_account` BIGINT COMMENT '资金账户', + `account_alias` VARCHAR(30) NOT NULL COMMENT '账户别名', + `money` DOUBLE COMMENT '账户金额', + `available_money` DOUBLE COMMENT '可用金额', + `available_proportion` DOUBLE COMMENT '可用资金占比', + `freeze` DOUBLE COMMENT '冻结金额' +) CHARACTER SET utf8mb4 COMMENT='证券账户'; +CREATE TABLE IF NOT EXISTS `backtest` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键id', + `key` VARCHAR(30) NOT NULL COMMENT '回测key' +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `Entrust` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键id', + `fund_account` INT COMMENT '资金账户', + `securities_alias` VARCHAR(30) COMMENT '账户别名', + `account_type` SMALLINT COMMENT '账户类型', + `stock_code` VARCHAR(30) NOT NULL COMMENT '证券代码', + `stock_name` VARCHAR(30) NOT NULL COMMENT '证券名称', + `limit_price` DOUBLE NOT NULL COMMENT '委托价', + `entrust_number` INT NOT NULL COMMENT '委托数量', + `deal_price` DOUBLE COMMENT '成交价格', + `deal_number` INT COMMENT '成家数量', + `order_type` VARCHAR(30) NOT NULL COMMENT '操作', + `entrust_date` DATE COMMENT '委托日期', + `entrust_money` DOUBLE NOT NULL COMMENT '委托金额', + `is_repair` BOOL NOT NULL COMMENT '是否补单' DEFAULT 0, + `entrust_time` TIME(6), + KEY `idx_Entrust_is_repa_5a195d` (`is_repair`) +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `orders` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID', + `stock_code` VARCHAR(30) NOT NULL COMMENT '股票代码', + `stock_name` VARCHAR(30) NOT NULL COMMENT '股票名称', + `limit_price` DOUBLE NOT NULL COMMENT '限价', + `order_quantity` INT NOT NULL COMMENT '委托数量', + `order_amount` DOUBLE NOT NULL COMMENT '委托金额', + `order_type` VARCHAR(20) NOT NULL COMMENT '订单类型', + `position` VARCHAR(30) NOT NULL COMMENT '仓位', + `user_id` INT NOT NULL COMMENT '用户Id', + `entrust_date` DATETIME(6) COMMENT '委托日期', + `entrust_time` TIME(6) +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `snowball` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键', + `snowball_token` VARCHAR(10000) COMMENT '雪球用户的token' +) CHARACTER SET utf8mb4 COMMENT='雪球相关信息'; +CREATE TABLE IF NOT EXISTS `Strategy` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键', + `strategy_name` VARCHAR(255) NOT NULL COMMENT '策略名称', + `strategy_hash` VARCHAR(255) NOT NULL COMMENT '策略版本号', + `strategy_type` VARCHAR(255) COMMENT '策略类型', + `user_id` INT COMMENT '所属用户', + `backtest_count` INT COMMENT '回测次数', + `backtest_keys` JSON COMMENT '回测key列表', + `is_deleted` BOOL NOT NULL COMMENT '是否删除' DEFAULT 0, + `created_at` DATETIME(6) NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` DATETIME(6) NOT NULL COMMENT '修改时间' DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `deleted_at` DATETIME(6) COMMENT '删除时间' +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `Backtest` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `key` VARCHAR(20) NOT NULL UNIQUE COMMENT '回测key', + `user_id` INT COMMENT '回测用户', + `backtest_at` DATETIME(6) NOT NULL COMMENT '回测时间' DEFAULT CURRENT_TIMESTAMP(6), + `backtest_code` LONGTEXT NOT NULL COMMENT '回测代码', + `is_running` BOOL NOT NULL COMMENT '回测状态' DEFAULT 1, + `updated_at` DATETIME(6) NOT NULL COMMENT '修改时间' DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `deleted_at` DATETIME(6) COMMENT '删除时间', + `is_active` BOOL NOT NULL COMMENT '是否可用' DEFAULT 1, + `strategy_id` INT NOT NULL, + CONSTRAINT `fk_Backtest_Strategy_7eabb20d` FOREIGN KEY (`strategy_id`) REFERENCES `Strategy` (`id`) ON DELETE CASCADE, + KEY `idx_Backtest_key_eb92b1` (`key`), + KEY `idx_Backtest_strateg_83ac3d` (`strategy_id`) +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `stock_details` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键', + `stock_code` VARCHAR(30) NOT NULL COMMENT '股票代码', + `stock_name` VARCHAR(30) COMMENT '股票名称', + `type` VARCHAR(2) COMMENT '类型', + `stock_pinyin` VARCHAR(30) NOT NULL COMMENT '股票拼音', + `latest_price` DOUBLE COMMENT '最新价', + `rise_fall` DOUBLE COMMENT '跌涨幅' +) CHARACTER SET utf8mb4 COMMENT='股票相关信息'; +CREATE TABLE IF NOT EXISTS `Transaction` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键', + `key` VARCHAR(20) NOT NULL COMMENT 'key,数据标识', + `cash` DOUBLE COMMENT '资金', + `transaction_name` VARCHAR(255) NOT NULL COMMENT '交易名称', + `transaction_type` VARCHAR(255) COMMENT '交易类型', + `user_id` INT COMMENT '用户id', + `is_running` BOOL NOT NULL COMMENT '运行状态' DEFAULT 1, + `is_deleted` BOOL NOT NULL COMMENT '是否删除' DEFAULT 0, + `process_id` INT COMMENT '进程号', + `bar` VARCHAR(10) COMMENT '频率K线', + `created_at` DATETIME(6) NOT NULL COMMENT '创建时间' DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` DATETIME(6) NOT NULL COMMENT '修改时间' DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `stopped_at` DATETIME(6) COMMENT '停止时间', + `deleted_at` DATETIME(6) COMMENT '删除时间', + `strategy_id` INT NOT NULL, + CONSTRAINT `fk_Transact_Strategy_849d0577` FOREIGN KEY (`strategy_id`) REFERENCES `Strategy` (`id`) ON DELETE CASCADE, + KEY `idx_Transaction_strateg_b8f0b2` (`strategy_id`) +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `position` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT '主键', + `key` VARCHAR(30) NOT NULL COMMENT '键', + `date` DATE NOT NULL COMMENT '日期', + `name` VARCHAR(30) NOT NULL COMMENT '名称', + `size` INT NOT NULL COMMENT '数量', + `price` DOUBLE NOT NULL COMMENT '价格', + `adjbase` DOUBLE NOT NULL COMMENT '复权基数', + `profit` DOUBLE NOT NULL COMMENT '利润' +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `trand_info` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'id', + `key` VARCHAR(255) COMMENT '唯一索引', + `tran_info_data` LONGBLOB +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `tran_observer_data` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `key` VARCHAR(255) NOT NULL, + `tran_observer_data` LONGBLOB COMMENT '存储大量数据' +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `tranorders` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `key` VARCHAR(255), + `order_return` VARCHAR(255) +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `tran_return` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `key` VARCHAR(255), + `tran_return_data` LONGBLOB +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `tran_trade_info` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `key` VARCHAR(25), + `tran_trade_info` LONGBLOB +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `back_observed_data` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'id表', + `key` VARCHAR(255) COMMENT 'key', + `observed_data` LONGBLOB COMMENT '格式化后的json数据' +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `back_observed_data_detail` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `key` VARCHAR(255) NOT NULL, + `back_observed_data` LONGBLOB +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `back_position` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `key` VARCHAR(25), + `back_position_data` LONGBLOB +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `back_result_indicator` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `key` VARCHAR(25), + `indicator` LONGBLOB +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `back_trand_info` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `key` VARCHAR(25), + `trade_info` LONGBLOB +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `tran_position` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `key` VARCHAR(25), + `tran_position_data` LONGBLOB +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `stock_bt_history` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `end_bt_time` VARCHAR(8) COMMENT '回测最终时间', + `bt_stock_code` VARCHAR(8) COMMENT '回测股票代码', + `bt_stock_name` VARCHAR(10) COMMENT '回测股票名称', + `bt_benchmark_code` VARCHAR(8) COMMENT '股票基准代码', + `bt_stock_period` VARCHAR(10) COMMENT '回测类型', + `bt_strategy_name` VARCHAR(10) COMMENT '回测策略名称', + `bt_stock_data` LONGBLOB COMMENT '回测股票数据' +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `stock_history` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `stock_code` INT NOT NULL COMMENT '股票代码', + `stock_name` VARCHAR(10), + `start_time_to_market` VARCHAR(10) COMMENT '股票上市时间', + `end_bt_time` VARCHAR(10) COMMENT '最终回测时间', + `symbol_data` LONGBLOB COMMENT '股票数据' +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `stock_data_processing` ( + `bt_benchmark_code` VARCHAR(6) COMMENT '基准代码', + `bt_stock_period` VARCHAR(10) COMMENT '数据类型', + `bt_strategy_name` VARCHAR(10) COMMENT '回测策略名', + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `processing_data` LONGBLOB COMMENT '清洗后的数据', + `prosessing_date` VARCHAR(10) COMMENT '当前回测时间', + `stock_name` VARCHAR(10), + `stocke_code` VARCHAR(10) +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `aerich` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `version` VARCHAR(255) NOT NULL, + `app` VARCHAR(100) NOT NULL, + `content` JSON NOT NULL +) CHARACTER SET utf8mb4;""" + + +async def downgrade(db: BaseDBAsyncClient) -> str: + return """ + """ diff --git a/models.py b/models.py new file mode 100644 index 0000000000000000000000000000000000000000..4b13f384dd40c6941788c135a4328c8936bf2108 GIT binary patch literal 40996 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 literal 0 HcmV?d00001 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..62a63ce --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,4 @@ +[tool.aerich] +tortoise_orm = "src.tortoises.TORTOISE_ORM" +location = "./migrations" +src_folder = "./." diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..8ab04ac --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,4 @@ +from dotenv import load_dotenv + +# load_dotenv(dotenv_path=Path("../.env")) +load_dotenv() \ No newline at end of file diff --git a/src/__pycache__/__init__.cpython-311.pyc b/src/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7d636cc8bb20a209dbacb3b73bc7586d15ab7bfb GIT binary patch literal 246 zcmZ3^%ge<81YHGt(xQR%V-N=hn4pZ$YCy(xh7^Vr#vF!R#wbQc1}277#$`a!)iAXT zj0`DE!3>(rFF_)jjJLRR@)J|yQ}Rnv^UC}*nQpN`m_^J$>6HwhL8^WkIa|eq7N-^! z$GD{C$D(l9=M6}I*Oe7lwIhW{DUC2J^3KTFYPGBE ztU@GG*n{aghk|p+A<&Q#a_OP;?+`sL1BF0Oy(u`SociAC!?Nto?)>)6dv9ib^JeDl zZ)0O|0;P2S*GgU?G)yBjN>v)8 z@n_*uSSFi}W;4&T{erU}5Su3g`G@94J0UC{7L^`@BO%q`wG_-)%X^kxo* zcM>ohlIAFQ#Gjq_a`+rVljsQjGGIFinginY_3$ka2LqgvoEUtZC_)ke7bd3mPRu$E zdi3HdfJgpL%(s>KpiArQ@``Sojhdxh*39xQGgDkM=%d!s@`_vDHJOG{0o{;WPg7;h z*2*bf5CEraKu~&q`hBKXTg#_-_NMLmRdB%R5w|)hQ(+++kA&QKAY{EN zY2@$|fCIuv3APr_nFOayhBFo_g{e}C&~PbABXCZlV(Jc~2H#@MbbrTMF<#@Vx=q4YX%;zGaOWgZ9&j_s#Wo!U8xwlQ8rKO zkxQvU)q>z?nuu#!&87{AvnG<72B*oyaA2!&tDvRx8yRgYb5F#I`K?SY5GU8u=}e)Z z9cdB;XlL_X^L+m1&1^cmzO9Y=jT{tb7Gf2^Gzk220B`X4(-*yMfAXT2_2;g%l^Orc zr{0#oaHFkU=)T%yLC|JD*zi_UIM_p@R(rQ>K2d(IRxZEI;w;o4(?&d!DHtvA}j&Cg8ajoS>f1op?fP;gKL!S zHuhRGLu4=YwI72gKM&xMHxtuOm1pti8!xthU;5*V_S{-~Vy&&N1ra|+9LWU4&w(&X zyc$qFN)<@V`3zu~RUGEW++|pMt+OM%dirJTG64`Ud*c4`_^*k@wz?S55fO*E4aYi0 z3n>M(Fe;<8hT~b0k76iy!-un-V3`P_R5$G=qpi6Snv``7Dd@@10`w!8d`@53?YV31 ziEC~3T0l3KN;()TXC48=J+gBS&M(xuW7nDEHXR()!J8qJUE4MtJRzgk2)E8b_y8*Z z8NeGnesbE&_~UclW*4R|c;CGVk9A}z{I%2}h;KtAGQR&qPZfa%ic4r*8nP{Q`ljno zi)g%l1Ve5^+U^ZSV!2dIC=7mx4sJ{Lr-A<;2mcFVX5$+Tesyp|Al>A?&*boe1bIP( zAe9G88(H&v^{Z~yy5U@fAtb2$Pk>HHk|dwZwvWGWrlvhym2@6B_aQQn0-Z_{i+=pF zAHV9yzx3lDbrO+ta)$u4$xP==SX%3llkgUG?~V+UlM+PWBs^Js;=V=wyHkuop#B3i CavN3v literal 0 HcmV?d00001 diff --git a/src/__pycache__/exceptions.cpython-311.pyc b/src/__pycache__/exceptions.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..36d44e7c9d769296fa0c58ca6df021d8b0160a7a GIT binary patch literal 6672 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 literal 0 HcmV?d00001 diff --git a/src/__pycache__/main.cpython-311.pyc b/src/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..251626c83fa91dcab03c7b0acb7f5245b7e9b4c2 GIT binary patch literal 2123 zcmZ`3U2oe|^g4EuHeX$xv{|!4HkibMphW{77;MwDhO`??q9&j%s$`km+ti&u!nLz? zeJW6iy}$|*ViH130+pdvXpcPZC%982tou|6ZLqhLjh8)fY}cXDwCi)uJ>NdQ=Uo4i zOvVtbS^b~IqhW;pbcJ9I?JVT3sqHT<1xa3F`&kj6K7&eid%poJS@j~CR4 z7Hvd5KCBLD!;N8&kEk&%-bn26N#Ii+A5}-R1C0Y7KctRoVnfu@jWmZsi17HJf{6Gt z_Z8R3fILhxn;b%S0R828MW5A@%&oD&Xa7zTco@f8+*W*(hdhA(;?Q#=QaJIPi1hM! z5Wk!aI64V+qUD2uOw!8}`}lsuR&o#D3biR;%%KvjQzfAhhOpB{dD_u-enL13g!$)d8t1WN}0;pva%=4FeR7MOT! z>MTz5D+)Dqjp$ar5Mg|&SZ6}DQmZq*`eB`gt7qzGXIW&X_(8E=WYJ2sK36H%3SpL% zRn@o(H7hMeH<`F!l;GRQ3LEuxO-eANS7g-;qBLSH8MrSl5g8L|KINvH{$cyn?a$|c zk5fR-BhJ0691bbahXP+_QH_|U+#;(Zvjl2+?eJbRj{J(KJ2sC2(Z9!m%$!)bsK|Qj z9FP@gLu=u|-5Za7_~zk{UoUvaC%NahVAAG-CM)_>`x+CdVOaDq1iubwia`E`XASKn z#_a4dJ6^Db0(Iw)4M~z4B}uCZ$S~#dC!k-e(Kn{NM>(HIP%smg4AWvl8`=#`CaNK0 zug=0tEM~$1(QA^4FSBUpiqbTw&Z3JjTyk5X?oDPBz*7}EPo}i~lud!7N!=M`Dfiv| zK{F}*^bkwB%3!`s>Rui;>gxQ-n({}DMm=9&C;2*-cHM$;*>hwgfgmD-SG+!(gFYbBF7SGHS=WC3I>*Dp) z?U^oXM3NX!JG$2fubC8Dg`N(*ry01Tl2h~)i0%*B+y&a>IgWGCs2zZVQfuDhphNZ` zIOr{V5O&bp_8>TD+8zXF&;k4%^o~6UJ1A!l!n0BSi+V8>O|7-;$Rxl<+~3#u9W-X6 zv5n?7%5}X&e#V<`Zlin`-NbDp0(UXWgC@y5hNtKe74KK+(QBvafI~Rx%G;Z$aE8VCo|d29P5SoaAX$&dE%0u zM`DN*{Fgd{=!m(l_==OwceBTx@th;R?`wKV#HDvplnXs^NzWq(Q7HUH_OtBu^rw^S zlf4kfz1l-Sc6k&^1&SC{b$#_dKVkC|jxe%TT0g&$x^?Mp{oYYqC~XU+u22HThaQiF J95{L-@gM5~MLz%l literal 0 HcmV?d00001 diff --git a/src/__pycache__/responses.cpython-311.pyc b/src/__pycache__/responses.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..497bb680f95d56e8d12a0c86638e622d5edf3de7 GIT binary patch literal 2955 zcmb_dUu+ab7@ytS`+se(w7n~BZHgdz5H7_Kjf7}`;vtkP(jvHNvRQApd|v~NcWFFyHwv$uC#3lWU7v%mdj z=9}4Xe&09W{8UrpBT)4B{}|vrg#3vOyFi`ZymUeU{BSIU?6i++yqM0F_c%dqZMc&#D^coHLuXN)Lb z)vt1DV7)y=E(@RuZ%MO;Y5G9Z|1X-gN|REn)(r4U-I!CXW35_f1)&x65-(9gO1)aI ztW(L*I(vPP8_F__4)7IADiW;g9dIcPuu>zFd8Kho5StXPmqfzLxIjGOFx@d#Q)x0` zdV1B9SyfA@rn_%sSp7hz5zch!hHPYYkb*;cMw35EsS@a96ctAU2YP$rM^t?{qv^CgRsoYOL%8VUnmsoremB_dF>Ci}M$#Cu|C$Ye(!%WdzixlMc<%b*=d;Uqp~LiBc1vif>F&)Y5~{A7 z?lho0sG5Qz8*)72Gd+@|$!S%ROrIpBGfEcrCz%0BI+>MIr4hFzDVc;M(OO*Dk<<*c zPLgCz%K$e?*g6aa*^q8fit&1dtN@b7I{>jLR-P-&&6`FGw|kOw8dWW>27%!hF{PvzDlUAa#5f=sT^9v1!}#jRV2&*K*H0POTkj0!{qZ?}eBK|=3vsr!|6vd4!qOg|^m+$$gXxSql&eh- z*``V7<*KC$rWg{9xmE_`?X3SV(0 z_?89tCit7_PGmF#92XHN-fYx`o&NoOeTSvjJ3FPGgA|R5@YVsqDLPtgJ3m-vP`9!j z-2$VXsMZCLBadAq(0s44WxjE9AsD_F?6}vuVZL=+A>3YcI$rh_3BW_*sP{iYD7syN zz(WG?2%$(^&VUsmuvod2sin~MJk%)wrzLTIDrQsAsedc+M~);rCE&12WMv%$A4Rzy|nbpnT1O?mVW(_;_0NX0#vcy z#8}_axm`LM-+i=if6tNk#C=_i|5d5RY%0^9)nxc_h9*y|3XQ^&cxmY|06cJE#(Za? ze&faC`C!zh|II>p>kH8j{x2sUkTGGzBO%jej0`8WL5gv1w^K4fkz^~;vWtnGVN53!k0-@kYRKS} z`RaOKfE#jr@0sqg?i^Qi2^@s^TA29oqI-tB1ZM-r9#s@-1ulg8t6}1;slki#^bqQY zP`{+m#&bPfks#iUTHmK&BhMXTT8Fs1U{0k~CHu%L(L(HC(cwj#0j3h{Q)u5botYW9 o_VLY)H}#vZ%}4jnw|6nE!r}gkHq*7B*#nbDiv&K;1a|KK0iofmrvLx| literal 0 HcmV?d00001 diff --git a/src/__pycache__/tortoises.cpython-311.pyc b/src/__pycache__/tortoises.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07b77316a4631048721b23def1b286a21235bc64 GIT binary patch literal 3469 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 Wy)mU}k~B?X&SxByrUuHA9sGaXsR||l literal 0 HcmV?d00001 diff --git a/src/backtest/__pycache__/router.cpython-311.pyc b/src/backtest/__pycache__/router.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5dc9f4e1166e6186cb3059d80512058386e28a63 GIT binary patch literal 1211 zcmZ`&%TE(Q9G=;2w=W8csGtG4HWGYbDJp1u;e$X35~EF%X5E?Am2J1qZh82KM2L@Q zV)Ww4=m}%=Knw@}3v1HDW=NqG^-D?|{%2Fl<#&PZ_y3tKYej3@4KxomhdN?gH6LKH8-j1RSSP~1m- zZ~b+%fe_CUhD;chM!Zx>Nkm!n{Ix=W_%5S}e_fCcjPPRmHg0i58uV0m}iW8HN&(h_t<)dMnXK8(y2j`hHlwBXxXMVRpCmG zGTkJ?C*%{^+)uNl!n~T9&)Pgp^l6>A-nks*fs}3-Nut^+_hrCe-J=&%zVEJn{ehXz)B|^ zCTfX^BI0dup5EGddaDKQipx7iC^tvctgUb6B&)T}M6H32m0w3rQ`jqO4w)GUCBx8&n)KmCZUq!LP*6yHaVQXIvv_9^5viI5Ed8MQv$^U52 jgFS_&&ab$uh`UNs7`MLNUqbNOgbf)<`)bimx4YRtz;HT% literal 0 HcmV?d00001 diff --git a/src/backtest/__pycache__/rsi_strategy.cpython-311.pyc b/src/backtest/__pycache__/rsi_strategy.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8e81093e6e069614e56af94598060f0e18328f44 GIT binary patch literal 14095 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 VfNARi)57v81X@Z{1x1tX_rKQ}lLG(% literal 0 HcmV?d00001 diff --git a/src/backtest/__pycache__/service.cpython-311.pyc b/src/backtest/__pycache__/service.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ce8746d943a54ae4a05412d83a537cf2c2cb41f7 GIT binary patch literal 2476 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 aV{^$%*(wC`)s(FvealAB_1{hDUi}ZM{m=XW literal 0 HcmV?d00001 diff --git a/src/backtest/backtest.py b/src/backtest/backtest.py new file mode 100644 index 0000000..bf5ef49 --- /dev/null +++ b/src/backtest/backtest.py @@ -0,0 +1,106 @@ +import bt +import pandas as pd +import numpy as np +from xtquant import xtdata +import matplotlib.pyplot as plt + +# 数据的列名 +columns = ['open', 'high', 'low', 'close', 'volume', 'amount', 'settelmentPrice', + 'openInterest', 'preClose', 'suspendFlag'] + + +# 获取本地数据并进行处理 +def get_local_data(field_list: list, stock_list: list, period: str, start_time: str, end_time: str, + count: int, dividend_type: str, fill_data: bool, data_dir: str): + result = xtdata.get_local_data(field_list=field_list, stock_list=stock_list, period=period, start_time=start_time, + end_time=end_time, count=count, dividend_type=dividend_type, fill_data=fill_data, + data_dir=data_dir) + return data_processing(result) + + +# 数据处理函数 +def data_processing(result_local): + # 初始化一个空的列表,用于存储每个股票的数据框 + df_list = [] + + # 遍历字典中的 DataFrame + for stock_code, df in result_local.items(): + # 确保 df 是一个 DataFrame + if isinstance(df, pd.DataFrame): + # 将时间戳转换为日期时间格式,只保留年-月-日 + df['time'] = pd.to_datetime(df['time'], unit='ms').dt.date + # 将 'time' 列设置为索引,保留为日期格式 + df.set_index('time', inplace=True) + # 指定列名 + df.columns = columns + # 添加一列 'stock_code' 用于标识不同的股票 + df['stock_code'] = stock_code + # 将 DataFrame 添加到列表中 + df_list.append(df[['close']]) # 只保留 'close' 列 + else: + print(f"数据格式错误: {stock_code} 不包含 DataFrame") + + # 使用 pd.concat() 将所有 DataFrame 合并为一个大的 DataFrame + combined_df = pd.concat(df_list, axis=1) + + # 确保返回的 DataFrame 索引是日期格式 + combined_df.index = pd.to_datetime(combined_df.index) + + # 打印最终的 DataFrame + print(combined_df) + + return combined_df + + +# 定义策略 +def moving_average_strategy(data, short_window=20, long_window=50): + data['Short_MA'] = data['close'].rolling(window=short_window, min_periods=1).mean() + data['Long_MA'] = data['close'].rolling(window=long_window, min_periods=1).mean() + + data['Signal'] = 0 + data.loc[data.index[short_window:], 'Signal'] = np.where( + data['Short_MA'][short_window:] > data['Long_MA'][short_window:], 1, 0) + data['Position'] = data['Signal'].diff() + + return data + + +# 定义策略函数 +def bt_strategy(data): + # 计算策略信号 + # data = moving_average_strategy(data) + + # 定义策略 + dual_ma_strategy = bt.Strategy('Dual MA Strategy', [bt.algos.RunOnce(), + bt.algos.SelectAll(), + bt.algos.WeighEqually(), + bt.algos.Rebalance()]) + return dual_ma_strategy + + +# 运行回测 +def run_backtest(): + # 生成数据 + data = get_local_data(field_list=[], stock_list=["300391.SZ"], period='1d', start_time='', end_time='', count=-1, + dividend_type='none', fill_data=True, data_dir="") + + # 创建策略 + strategy = bt_strategy(data) + + # 创建回测 + portfolio = bt.Backtest(strategy, data) + result = bt.run(portfolio) + + return result + + +# 执行回测并显示结果 +if __name__ == "__main__": + result = run_backtest() + result.plot() + b = xtdata.get_sector_list() + print(b) + xtdata.download_sector_data() + a = result.stats + print(a) + plt.show() diff --git a/src/backtest/bollinger.py b/src/backtest/bollinger.py new file mode 100644 index 0000000..f6e020f --- /dev/null +++ b/src/backtest/bollinger.py @@ -0,0 +1,79 @@ +import bt + +# 模拟从前端接收到的因子列表 +factors = [ + {'name': 'SMA', 'period': 50}, + {'name': 'EMA', 'period': 200}, + {'name': 'RSI', 'period': 14}, + {'name': 'BollingerBands', 'period': 20} +] + + +def create_indicator(name, **kwargs): + """ + 根据名称和参数动态创建bt的技术指标。 + """ + if name == 'SMA': + return bt.indicators.SMA(kwargs['period']) + elif name == 'EMA': + return bt.indicators.EMA(kwargs['period']) + elif name == 'RSI': + return bt.indicators.RSI(kwargs['period']) + elif name == 'BollingerBands': + return bt.indicators.BollingerBands(kwargs['period']) + + else: + raise ValueError(f"未知的指标名称: {name}") + + +def create_dynamic_strategy(name, factors): + """ + 根据传递的因子列表动态构建策略。 + """ + algos = [bt.algos.RunMonthly(), # 每月运行一次 + bt.algos.SelectAll()] # 选择所有资产 + + # 动态生成指标选择逻辑 + for factor in factors: + indicator = create_indicator(factor['name'], **factor) + if factor['name'] == 'RSI': + # RSI 特定的买入/卖出逻辑 + buy_signal = bt.algos.SelectWhere(indicator < 30) # 超卖区域买入 + sell_signal = bt.algos.SelectWhere(indicator > 70) # 超买区域卖出 + algos.append(buy_signal) + algos.append(bt.algos.WeighTarget(1.0)) + algos.append(bt.algos.Rebalance()) + algos.append(sell_signal) + algos.append(bt.algos.WeighTarget(0.0)) + algos.append(bt.algos.Rebalance()) + elif factor['name'] == 'BollingerBands': + # 布林带特定的逻辑(买入接近下轨,卖出接近上轨) + buy_signal = bt.algos.SelectWhere(bt.data < indicator['lower']) + sell_signal = bt.algos.SelectWhere(bt.data > indicator['upper']) + algos.append(buy_signal) + algos.append(bt.algos.WeighTarget(1.0)) + algos.append(bt.algos.Rebalance()) + algos.append(sell_signal) + algos.append(bt.algos.WeighTarget(0.0)) + algos.append(bt.algos.Rebalance()) + # 可以为其他指标添加更多逻辑 + + return bt.Strategy(name, algos) + + +# 创建动态策略 +dynamic_strategy = create_dynamic_strategy("Dynamic_Strategy", factors) + +# 获取数据 +data = bt.get('spy,agg', start='2020-01-01', end='2023-01-01') + +# 创建回测 +backtest = bt.Backtest(dynamic_strategy, data) + +# 运行回测 +result = bt.run(backtest) + +# 展示结果 +result.display() +result.plot() + diff --git a/src/backtest/bollinger_bands.py b/src/backtest/bollinger_bands.py new file mode 100644 index 0000000..576e26d --- /dev/null +++ b/src/backtest/bollinger_bands.py @@ -0,0 +1,274 @@ +import asyncio +import json +import time +from datetime import datetime + +import bt +import numpy as np +import pandas as pd +from xtquant import xtdata +import matplotlib.pyplot as plt + +from src.backtest.until import get_local_data, convert_pandas_to_json_serializable +from src.models import wance_data_storage_backtest, wance_data_stock +from src.tortoises_orm_config import init_tortoise + + +# 布林带策略函数 +async def create_bollinger_bands_strategy(data, stock_code: str, bollingerMA: int = 50, std_dev: int = 200): + # 生成布林带策略信号 + signal = await bollinger_bands_strategy(data, bollingerMA, std_dev) + + # 使用bt框架构建策略 + strategy = bt.Strategy(f'{stock_code} 布林带策略', + [bt.algos.RunDaily(), + bt.algos.SelectAll(), # 选择所有股票 + bt.algos.WeighTarget(signal), # 根据信号调整权重 + bt.algos.Rebalance()]) # 调仓 + return strategy, signal + + +async def bollinger_bands_strategy(df, window=20, num_std_dev=2): + """ + 基于布林带策略生成买卖信号。 + + 参数: + df: pd.DataFrame, 股票的价格数据,行索引为日期,列为股票代码。 + window: int, 计算布林带中轨线的窗口期。 + num_std_dev: float, 标准差的倍数,用于计算上下轨。 + + 返回: + signal: pd.DataFrame, 每只股票的买卖信号,1 表示买入,0 表示卖出。 + """ + # 计算中轨线(移动平均) + middle_band = df.rolling(window=window, min_periods=1).mean() + + # 计算滚动标准差 + rolling_std = df.rolling(window=window, min_periods=1).std() + + # 计算上轨线和下轨线 + upper_band = middle_band + (rolling_std * num_std_dev) + lower_band = middle_band - (rolling_std * num_std_dev) + + # 初始化信号 DataFrame + signal = pd.DataFrame(index=df.index, columns=df.columns) + + # 生成买入信号:当价格突破下轨时 + for column in df.columns: + signal[column] = np.where(df[column] < lower_band[column], 1, np.nan) # 买入信号 + + # 生成卖出信号:当价格突破上轨时 + for column in df.columns: + signal[column] = np.where(df[column] > upper_band[column], 0, signal[column]) # 卖出信号 + + # 前向填充信号,持仓不变 + signal = signal.ffill() + + # 将剩余的 NaN 替换为 0 + signal = signal.fillna(0) + + return signal + + +async def storage_backtest_data(source_column_name, result, signal, stock_code, stock_data_series, bollingerMA, + std_dev): + await init_tortoise() + + # 要存储的字段列表 + fields_to_store = [ + 'stock_code', 'strategy_name', 'stock_close_price', 'daily_price', + 'price', 'returns', 'data_start_time', 'data_end_time', + 'backtest_end_time', 'position', 'backtest_name', 'rf', 'total_return', 'cagr', + 'max_drawdown', 'calmar', 'mtd', 'three_month', + 'six_month', 'ytd', 'one_year', 'three_year', + 'five_year', 'ten_year', 'incep', 'daily_sharpe', + 'daily_sortino', 'daily_mean', 'daily_vol', + 'daily_skew', 'daily_kurt', 'best_day', 'worst_day', + 'monthly_sharpe', 'monthly_sortino', 'monthly_mean', + 'monthly_vol', 'monthly_skew', 'monthly_kurt', + 'best_month', 'worst_month', 'yearly_sharpe', + 'yearly_sortino', 'yearly_mean', 'yearly_vol', + 'yearly_skew', 'yearly_kurt', 'best_year', 'worst_year', + 'avg_drawdown', 'avg_drawdown_days', 'avg_up_month', + 'avg_down_month', 'win_year_perc', 'twelve_month_win_perc' + ] + + # 准备要存储的数据 + data_to_store = { + 'stock_code': stock_code, + 'strategy_name': "布林带策略", + 'stock_close_price': json.dumps(stock_data_series.fillna(0).rename_axis('time').reset_index().assign( + time=stock_data_series.index.strftime('%Y%m%d')).set_index('time').to_dict(orient='index')), + 'daily_price': convert_pandas_to_json_serializable(result[source_column_name].daily_prices), + 'price': convert_pandas_to_json_serializable(result[source_column_name].prices), + 'returns': convert_pandas_to_json_serializable(result[source_column_name].returns.fillna(0)), + 'data_start_time': pd.to_datetime(result.stats.loc["start"].iloc[0]).strftime('%Y%m%d'), + 'data_end_time': pd.to_datetime(result.stats.loc["end"].iloc[0]).strftime('%Y%m%d'), + 'backtest_end_time': int(datetime.now().strftime('%Y%m%d')), + 'position': convert_pandas_to_json_serializable(signal), + 'backtest_name': f'{stock_code} 布林带策略 MA{bollingerMA}-{std_dev}倍标准差', + 'indicator_type': 'Bollinger', + 'indicator_information': json.dumps({'bollingerMA': bollingerMA, 'std_dev': std_dev}) + } + + # 使用循环填充其他字段 + for field in fields_to_store[12:]: # 从第10个字段开始 + value = result.stats.loc[field].iloc[0] + data_to_store[field] = 0.0 if (isinstance(value, float) and np.isnan(value)) else value + + # 检查是否存在该 backtest_name + existing_record = await wance_data_storage_backtest.WanceDataStorageBacktest.filter( + backtest_name=data_to_store['backtest_name'] + ).first() + + if existing_record: + # 如果存在,更新记录 + await wance_data_storage_backtest.WanceDataStorageBacktest.filter( + id=existing_record.id + ).update(**data_to_store) + else: + # 如果不存在,创建新的记录 + await wance_data_storage_backtest.WanceDataStorageBacktest.create(**data_to_store) + + return data_to_store + + +async def run_bollinger_backtest(field_list: list, + stock_list: list, + period: str = '1d', + start_time: str = '', + end_time: str = '', + count: int = 100, + dividend_type: str = 'none', + fill_data: bool = True, + data_dir: str = '', + bollingerMA: int = 50, + std_dev: int = 200): + try: + # 初始化一个列表用于存储每只股票的回测结果字典 + results_list = [] + + # 遍历每只股票的数据(每列代表一个股票的收盘价) + data = await get_local_data(field_list, stock_list, period, start_time, end_time, count, dividend_type, + fill_data, + data_dir) + + for stock_code in stock_list: + + data_column_name = f'close_{stock_code}' + source_column_name = f'{stock_code} 布林带策略' + backtest_name = f'{stock_code} 布林带策略 MA{bollingerMA}-{std_dev}倍标准差' + now_time = int(datetime.now().strftime('%Y%m%d')) + db_result = await wance_data_storage_backtest.WanceDataStorageBacktest.filter( + backtest_name=backtest_name) + if db_result: + if db_result[0].backtest_end_time == now_time: + results_list.append({source_column_name: db_result[0]}) + + # elif data_column_name in data.columns: + if data_column_name in data.columns: + stock_data_series = data[[data_column_name]] # 提取该股票的收盘价 DataFrame + stock_data_series.columns = ['close'] # 重命名列为 'close' + + # 创建布林带策略 + strategy, signal = await create_bollinger_bands_strategy(stock_data_series, stock_code, + bollingerMA=bollingerMA, + std_dev=std_dev) + # 创建回测 + backtest = bt.Backtest(strategy=strategy, data=stock_data_series, initial_capital=100000) + # 运行回测 + result = bt.run(backtest) + # 存储回测结果 + data_to_store = await storage_backtest_data(source_column_name, result, signal, stock_code, + stock_data_series, + bollingerMA, std_dev) + # # 绘制回测结果图表 + # result.plot() + # # 绘制个别股票数据图表 + # plt.figure(figsize=(12, 6)) + # plt.plot(stock_data_series.index, stock_data_series['close'], label='Stock Price') + # plt.title(f'Stock Price for {stock_code}') + # plt.xlabel('Date') + # plt.ylabel('Price') + # plt.legend() + # plt.grid(True) + # plt.show() + # 将结果存储为字典并添加到列表中 + results_list.append({source_column_name: data_to_store}) + + else: + print(f"数据中缺少列: {data_column_name}") + + return results_list # 返回结果列表 + + except Exception as e: + print(f"Error occurred: {e}") + + +async def start_bollinger_backtest_service(field_list: list, + stock_list: list, + period: str = '1d', + start_time: str = '', + end_time: str = '', + count: int = -1, + dividend_type: str = 'none', + fill_data: bool = True, + data_dir: str = '', + bollingerMA: int = 50, + std_dev: int = 200): + for stock_code in stock_list: + backtest_name = f'{stock_code} 布林带策略 MA{bollingerMA}-{std_dev}倍标准差' + db_result = await wance_data_storage_backtest.WanceDataStorageBacktest.filter( + backtest_name=backtest_name) + now_time = int(datetime.now().strftime('%Y%m%d')) + + if db_result and db_result[0].backtest_end_time == now_time: + return db_result + else: + # 执行回测 + result = await run_bollinger_backtest( + field_list=field_list, + stock_list=stock_list, + period=period, + start_time=start_time, + end_time=end_time, + count=count, + dividend_type=dividend_type, + fill_data=fill_data, + data_dir=data_dir, + bollingerMA=bollingerMA, + std_dev=std_dev, + ) + return result + + +async def init_backtest_db(): + bollinger_list = [{"bollingerMA": 20, "std_dev": 2}, {"bollingerMA": 30, "std_dev": 2}, + {"bollingerMA": 70, "std_dev": 2}, {"bollingerMA": 5, "std_dev": 1}, + {"bollingerMA": 20, "std_dev": 3}, {"bollingerMA": 50, "std_dev": 2.5}] + await init_tortoise() + wance_db = await wance_data_stock.WanceDataStock.all() + bollinger_list_lenght = len(bollinger_list) + + for stock_code in wance_db: + for i in range(bollinger_list_lenght): + bollingerMA = bollinger_list[i]['bollingerMA'] + std_dev = bollinger_list[i]['std_dev'] + source_column_name = f'{stock_code} 布林带策略 MA{bollingerMA}-{std_dev}倍标准差' + result = await run_bollinger_backtest(field_list=['close', 'time'], + stock_list=[stock_code.stock_code], + bollingerMA=bollingerMA, + std_dev=std_dev) + + print(f"回测成功 {source_column_name}") + + +if __name__ == '__main__': + # 测试类的回测 + asyncio.run(run_bollinger_backtest(field_list=['close', 'time'], + stock_list=['601222.SH', '601677.SH'], + bollingerMA=20, + std_dev=2)) + + # # 初始化数据库表 + # asyncio.run(init_backtest_db()) diff --git a/src/backtest/dual_moving_average.py b/src/backtest/dual_moving_average.py new file mode 100644 index 0000000..d5c274f --- /dev/null +++ b/src/backtest/dual_moving_average.py @@ -0,0 +1,267 @@ +import asyncio +import json +import time +from datetime import datetime + +import bt +import numpy as np +import pandas as pd +from xtquant import xtdata +import matplotlib.pyplot as plt + +from src.backtest.until import get_local_data, convert_pandas_to_json_serializable +from src.models import wance_data_storage_backtest, wance_data_stock +from src.tortoises_orm_config import init_tortoise + + +# 双均线策略函数 +async def create_dual_ma_strategy(data, stock_code: str, short_window: int = 50, long_window: int = 200): + # 生成双均线策略信号 + signal = await dual_ma_strategy(data, short_window, long_window) + + # 使用bt框架构建策略 + strategy = bt.Strategy(f'{stock_code} 双均线策略', + [bt.algos.RunDaily(), + bt.algos.SelectAll(), # 选择所有股票 + bt.algos.WeighTarget(signal), # 根据信号调整权重 + bt.algos.Rebalance()]) # 调仓 + return strategy, signal + + +async def dual_ma_strategy(df, short_window=20, long_window=50): + """ + 基于双均线策略生成买卖信号。 + + 参数: + df: pd.DataFrame, 股票的价格数据,行索引为日期,列为股票代码。 + short_window: int, 短期均线窗口期。 + long_window: int, 长期均线窗口期。 + + 返回: + signal: pd.DataFrame, 每只股票的买卖信号,1 表示买入,0 表示卖出。 + """ + # 计算短期均线和长期均线 + short_ma = df.rolling(window=short_window, min_periods=1).mean() + long_ma = df.rolling(window=long_window, min_periods=1).mean() + + # 生成买入信号: 当短期均线从下方穿过长期均线 + buy_signal = np.where(short_ma > long_ma, 1, np.nan) + + # 生成卖出信号: 当短期均线从上方穿过长期均线 + sell_signal = np.where(short_ma < long_ma, 0, np.nan) + + # 合并买卖信号 + signal = pd.DataFrame(buy_signal, index=df.index, columns=df.columns) + signal = np.where(short_ma < long_ma, 0, signal) + + # 前向填充信号,持仓不变 + signal = pd.DataFrame(signal, index=df.index, columns=df.columns).ffill() + + # 将剩余的 NaN 替换为 0 + signal = signal.fillna(0) + + return signal + + +async def storage_backtest_data(source_column_name, result, signal, stock_code, stock_data_series, short_window, + long_window): + await init_tortoise() + + # 要存储的字段列表 + fields_to_store = [ + 'stock_code', 'strategy_name', 'stock_close_price', 'daily_price', + 'price', 'returns', 'data_start_time', 'data_end_time', + 'backtest_end_time', 'position', 'backtest_name', 'rf', 'total_return', 'cagr', + 'max_drawdown', 'calmar', 'mtd', 'three_month', + 'six_month', 'ytd', 'one_year', 'three_year', + 'five_year', 'ten_year', 'incep', 'daily_sharpe', + 'daily_sortino', 'daily_mean', 'daily_vol', + 'daily_skew', 'daily_kurt', 'best_day', 'worst_day', + 'monthly_sharpe', 'monthly_sortino', 'monthly_mean', + 'monthly_vol', 'monthly_skew', 'monthly_kurt', + 'best_month', 'worst_month', 'yearly_sharpe', + 'yearly_sortino', 'yearly_mean', 'yearly_vol', + 'yearly_skew', 'yearly_kurt', 'best_year', 'worst_year', + 'avg_drawdown', 'avg_drawdown_days', 'avg_up_month', + 'avg_down_month', 'win_year_perc', 'twelve_month_win_perc' + ] + + # 准备要存储的数据 + data_to_store = { + 'stock_code': stock_code, + 'strategy_name': "双均线策略", + 'stock_close_price': json.dumps(stock_data_series.fillna(0).rename_axis('time').reset_index().assign( + time=stock_data_series.index.strftime('%Y%m%d')).set_index('time').to_dict(orient='index')), + 'daily_price': convert_pandas_to_json_serializable(result[source_column_name].daily_prices), + 'price': convert_pandas_to_json_serializable(result[source_column_name].prices), + 'returns': convert_pandas_to_json_serializable(result[source_column_name].returns.fillna(0)), + 'data_start_time': pd.to_datetime(result.stats.loc["start"].iloc[0]).strftime('%Y%m%d'), + 'data_end_time': pd.to_datetime(result.stats.loc["end"].iloc[0]).strftime('%Y%m%d'), + 'backtest_end_time': int(datetime.now().strftime('%Y%m%d')), + 'position': convert_pandas_to_json_serializable(signal), + 'backtest_name': f'{stock_code} 双均线策略 MA{short_window}-{long_window}日', + 'indicator_type': 'SMA', + 'indicator_information': json.dumps({'short_window': short_window, 'long_window': long_window}) + } + + # 使用循环填充其他字段 + for field in fields_to_store[12:]: # 从第10个字段开始 + value = result.stats.loc[field].iloc[0] + data_to_store[field] = 0.0 if (isinstance(value, float) and np.isnan(value)) else value + + # 检查是否存在该 backtest_name + existing_record = await wance_data_storage_backtest.WanceDataStorageBacktest.filter( + backtest_name=data_to_store['backtest_name'] + ).first() + + if existing_record: + # 如果存在,更新记录 + await wance_data_storage_backtest.WanceDataStorageBacktest.filter( + id=existing_record.id + ).update(**data_to_store) + else: + # 如果不存在,创建新的记录 + await wance_data_storage_backtest.WanceDataStorageBacktest.create(**data_to_store) + + return data_to_store + + +async def run_sma_backtest(field_list: list, + stock_list: list, + period: str = '1d', + start_time: str = '', + end_time: str = '', + count: int = 100, + dividend_type: str = 'none', + fill_data: bool = True, + data_dir: str = '', + short_window: int = 50, + long_window: int = 200): + try: + # 初始化一个列表用于存储每只股票的回测结果字典 + results_list = [] + + # 遍历每只股票的数据(每列代表一个股票的收盘价) + data = await get_local_data(field_list, stock_list, period, start_time, end_time, count, dividend_type, + fill_data, + data_dir) + + for stock_code in stock_list: + + data_column_name = f'close_{stock_code}' + source_column_name = f'{stock_code} 双均线策略' + backtest_name = f'{stock_code} 双均线策略 MA{short_window}-{long_window}日' + now_data = int(datetime.now().strftime('%Y%m%d')) + db_result_data = await wance_data_storage_backtest.WanceDataStorageBacktest.filter( + backtest_name=backtest_name) + + if db_result_data: + if db_result_data[0].backtest_end_time == now_data: + results_list.append({source_column_name: db_result_data[0]}) + + if data_column_name in data.columns: + stock_data_series = data[[data_column_name]] # 提取该股票的收盘价 DataFrame + stock_data_series.columns = ['close'] # 重命名列为 'close' + + # 创建双均线策略 + strategy, signal = await create_dual_ma_strategy(stock_data_series, stock_code, + short_window=short_window, + long_window=long_window) + # 创建回测 + backtest = bt.Backtest(strategy=strategy, data=stock_data_series, initial_capital=100000) + # 运行回测 + result = bt.run(backtest) + # 存储回测结果 + data_to_store = await storage_backtest_data(source_column_name, result, signal, stock_code, + stock_data_series, + short_window, long_window) + # # 绘制回测结果图表 + # result.plot() + # # 绘制个别股票数据图表 + # plt.figure(figsize=(12, 6)) + # plt.plot(stock_data_series.index, stock_data_series['close'], label='Stock Price') + # plt.title(f'Stock Price for {stock_code}') + # plt.xlabel('Date') + # plt.ylabel('Price') + # plt.legend() + # plt.grid(True) + # plt.show() + # 将结果存储为字典并添加到列表中 + results_list.append({source_column_name: data_to_store}) + + else: + print(f"数据中缺少列: {data_column_name}") + + return results_list # 返回结果列表 + + except Exception as e: + print(f"Error occurred: {e}") + + +async def start_sma_backtest_service(field_list: list, + stock_list: list, + period: str = '1d', + start_time: str = '', + end_time: str = '', + count: int = -1, + dividend_type: str = 'none', + fill_data: bool = True, + data_dir: str = '', + short_window: int = 50, + long_window: int = 200): + for stock_code in stock_list: + backtest_name = f'{stock_code} 双均线策略 MA{short_window}-{long_window}日' + db_result = await wance_data_storage_backtest.WanceDataStorageBacktest.filter( + backtest_name=backtest_name) + now_time = int(datetime.now().strftime('%Y%m%d')) + + if db_result and db_result[0].backtest_end_time == now_time: + return db_result + else: + # 执行回测 + result = await run_sma_backtest( + field_list=field_list, + stock_list=stock_list, + period=period, + start_time=start_time, + end_time=end_time, + count=count, + dividend_type=dividend_type, + fill_data=fill_data, + data_dir=data_dir, + short_window=short_window, + long_window=long_window, + ) + return result + + +async def init_backtest_db(): + sma_list = [{"short_window": 5, "long_window": 10}, {"short_window": 10, "long_window": 30}, + {"short_window": 30, "long_window": 60}, {"short_window": 30, "long_window": 90}, + {"short_window": 70, "long_window": 140}, {"short_window": 120, "long_window": 250}] + await init_tortoise() + wance_db = await wance_data_stock.WanceDataStock.all() + sma_list_lenght = len(sma_list) + + for stock_code in wance_db: + for i in range(sma_list_lenght): + short_window = sma_list[i]['short_window'] + long_window = sma_list[i]['long_window'] + source_column_name = f'{stock_code} 双均线策略 MA{short_window}-{long_window}日' + result = await run_sma_backtest(field_list=['close', 'time'], + stock_list=[stock_code.stock_code], + short_window=short_window, + long_window=long_window) + + print(f"回测成功 {source_column_name}") + + +if __name__ == '__main__': + # 测试类的回测 + # asyncio.run(run_sma_backtest(field_list=['close', 'time'], + # stock_list=['601222.SH', '601677.SH'], + # short_window=10, + # long_window=30)) + + # 初始化数据库表 + asyncio.run(init_backtest_db()) diff --git a/src/backtest/living_backtesting.py b/src/backtest/living_backtesting.py new file mode 100644 index 0000000..6da418c --- /dev/null +++ b/src/backtest/living_backtesting.py @@ -0,0 +1,92 @@ +import bt +import pandas as pd +import numpy as np +from xtquant import xtdata +import matplotlib.pyplot as plt + +# 数据的列名 +columns = ['open', 'high', 'low', 'close', 'volume', 'amount', 'settelmentPrice', + 'openInterest', 'preClose', 'suspendFlag'] + + +def get_local_data(field_list: list, stock_list: list, period: str, start_time: str, end_time: str, + count: int, dividend_type: str, fill_data: bool, data_dir: str): + result = xtdata.get_local_data(field_list=field_list, stock_list=stock_list, period=period, start_time=start_time, + end_time=end_time, count=count, dividend_type=dividend_type, fill_data=fill_data, + data_dir=data_dir) + return data_processing(result) + + +# 数据处理函数 +def data_processing(result_local): + # 初始化一个空的列表,用于存储每个股票的数据框 + df_list = [] + + # 遍历字典中的 DataFrame + for stock_code, df in result_local.items(): + # 确保 df 是一个 DataFrame + if isinstance(df, pd.DataFrame): + # 将时间戳转换为日期时间格式,只保留年-月-日 + df['time'] = pd.to_datetime(df['time'], unit='ms').dt.date + # 将 'time' 列设置为索引,保留为日期格式 + df.set_index('time', inplace=True) + # 指定列名 + df.columns = columns + # 添加一列 'stock_code' 用于标识不同的股票 + df['stock_code'] = stock_code + # 将 DataFrame 添加到列表中 + df_list.append(df[['close']]) # 只保留 'close' 列 + else: + print(f"数据格式错误: {stock_code} 不包含 DataFrame") + + # 使用 pd.concat() 将所有 DataFrame 合并为一个大的 DataFrame + combined_df = pd.concat(df_list, axis=1) + + # 确保返回的 DataFrame 索引是日期格式 + combined_df.index = pd.to_datetime(combined_df.index) + + # 打印最终的 DataFrame + print(combined_df) + + return combined_df + + +def bt_strategy(data): + + # 计算策略信号 + # data = moving_average_strategy(data) + + # 定义策略 + dual_ma_strategy = bt.Strategy('Dual MA Strategy', [bt.algos.RunOnce(), + bt.algos.SelectAll(), + bt.algos.WeighEqually(), + bt.algos.Rebalance()]) + return dual_ma_strategy + + +# 运行回测 +def run_backtest(): + # 生成数据 + data = get_local_data(field_list=[], stock_list=["300391.SZ"], period='1d', start_time='', end_time='', count=-1, + dividend_type='none', fill_data=True, data_dir="") + + # 创建策略 + strategy = bt_strategy(data) + + # 创建回测 + portfolio = bt.Backtest(strategy, data) + result = bt.run(portfolio) + + return result + + +# 执行回测并显示结果 +if __name__ == "__main__": + result = run_backtest() + result.plot() + b = xtdata.get_sector_list() + print(b) + xtdata.download_sector_data() + a = result.stats + print(a) + plt.show() diff --git a/src/backtest/macd_strategy.py b/src/backtest/macd_strategy.py new file mode 100644 index 0000000..7d23359 --- /dev/null +++ b/src/backtest/macd_strategy.py @@ -0,0 +1,271 @@ +import asyncio +import json +import time +from datetime import datetime + +import bt +import numpy as np +import pandas as pd +from xtquant import xtdata +import matplotlib.pyplot as plt + +from src.backtest.until import get_local_data, convert_pandas_to_json_serializable +from src.models import wance_data_storage_backtest, wance_data_stock +from src.tortoises_orm_config import init_tortoise + + +# MACD策略函数 +async def create_dual_ma_strategy(data, stock_code: str, short_window: int = 50, long_window: int = 200): + # 生成MACD策略信号 + signal = await macd_strategy(data, short_window, long_window) + + # 使用bt框架构建策略 + strategy = bt.Strategy(f'{stock_code} MACD策略', + [bt.algos.RunDaily(), + bt.algos.SelectAll(), # 选择所有股票 + bt.algos.WeighTarget(signal), # 根据信号调整权重 + bt.algos.Rebalance()]) # 调仓 + return strategy, signal + + +# 定义 MACD 策略的函数 +def macd_strategy(data, short_window=12, long_window=26, signal_window=9): + """ + MACD 策略,当 MACD 线穿过信号线时买入,反之卖出。 + + 参数: + data: pd.DataFrame, 股票的价格数据,行索引为日期,列为股票代码。 + short_window: int, 短期 EMA 的窗口期。 + long_window: int, 长期 EMA 的窗口期。 + signal_window: int, 信号线 EMA 的窗口期。 + + 返回: + signal: pd.DataFrame, 每只股票的买卖信号,1 表示买入,-1 表示卖出。 + """ + # 计算短期和长期的 EMA + short_ema = data.ewm(span=short_window, adjust=False).mean() + long_ema = data.ewm(span=long_window, adjust=False).mean() + + # 计算 MACD 线 + macd_line = short_ema - long_ema + + # 计算信号线 + signal_line = macd_line.ewm(span=signal_window, adjust=False).mean() + + # 生成买入和卖出信号 + signal = pd.DataFrame(index=data.index, columns=data.columns) + for column in data.columns: + signal[column] = 0 # 初始化信号为 0 + # 买入信号:MACD 线从下方穿过信号线 + signal[column] = (macd_line[column] > signal_line[column]) & (macd_line[column].shift(1) <= signal_line[column].shift(1)).astype(int) + # 卖出信号:MACD 线从上方穿过信号线 + signal[column] = (macd_line[column] < signal_line[column]) & (macd_line[column].shift(1) >= signal_line[column].shift(1)).astype(int) * -1 + signal[column] + + # 前向填充信号,保持持仓不变 + signal = signal.ffill().fillna(0) + + return signal + + +async def storage_backtest_data(source_column_name, result, signal, stock_code, stock_data_series, short_window, + long_window): + await init_tortoise() + + # 要存储的字段列表 + fields_to_store = [ + 'stock_code', 'strategy_name', 'stock_close_price', 'daily_price', + 'price', 'returns', 'data_start_time', 'data_end_time', + 'backtest_end_time', 'position', 'backtest_name', 'rf', 'total_return', 'cagr', + 'max_drawdown', 'calmar', 'mtd', 'three_month', + 'six_month', 'ytd', 'one_year', 'three_year', + 'five_year', 'ten_year', 'incep', 'daily_sharpe', + 'daily_sortino', 'daily_mean', 'daily_vol', + 'daily_skew', 'daily_kurt', 'best_day', 'worst_day', + 'monthly_sharpe', 'monthly_sortino', 'monthly_mean', + 'monthly_vol', 'monthly_skew', 'monthly_kurt', + 'best_month', 'worst_month', 'yearly_sharpe', + 'yearly_sortino', 'yearly_mean', 'yearly_vol', + 'yearly_skew', 'yearly_kurt', 'best_year', 'worst_year', + 'avg_drawdown', 'avg_drawdown_days', 'avg_up_month', + 'avg_down_month', 'win_year_perc', 'twelve_month_win_perc' + ] + + # 准备要存储的数据 + data_to_store = { + 'stock_code': stock_code, + 'strategy_name': "MACD策略", + 'stock_close_price': json.dumps(stock_data_series.fillna(0).rename_axis('time').reset_index().assign( + time=stock_data_series.index.strftime('%Y%m%d')).set_index('time').to_dict(orient='index')), + 'daily_price': convert_pandas_to_json_serializable(result[source_column_name].daily_prices), + 'price': convert_pandas_to_json_serializable(result[source_column_name].prices), + 'returns': convert_pandas_to_json_serializable(result[source_column_name].returns.fillna(0)), + 'data_start_time': pd.to_datetime(result.stats.loc["start"].iloc[0]).strftime('%Y%m%d'), + 'data_end_time': pd.to_datetime(result.stats.loc["end"].iloc[0]).strftime('%Y%m%d'), + 'backtest_end_time': int(datetime.now().strftime('%Y%m%d')), + 'position': convert_pandas_to_json_serializable(signal), + 'backtest_name': f'{stock_code} MACD策略 MA{short_window}-{long_window}日', + 'indicator_type': 'MACD', + 'indicator_information': json.dumps({'short_window': short_window, 'long_window': long_window}) + } + + # 使用循环填充其他字段 + for field in fields_to_store[12:]: # 从第10个字段开始 + value = result.stats.loc[field].iloc[0] + data_to_store[field] = 0.0 if (isinstance(value, float) and np.isnan(value)) else value + + # 检查是否存在该 backtest_name + existing_record = await wance_data_storage_backtest.WanceDataStorageBacktest.filter( + backtest_name=data_to_store['backtest_name'] + ).first() + + if existing_record: + # 如果存在,更新记录 + await wance_data_storage_backtest.WanceDataStorageBacktest.filter( + id=existing_record.id + ).update(**data_to_store) + else: + # 如果不存在,创建新的记录 + await wance_data_storage_backtest.WanceDataStorageBacktest.create(**data_to_store) + + return data_to_store + + +async def run_macd_backtest(field_list: list, + stock_list: list, + period: str = '1d', + start_time: str = '', + end_time: str = '', + count: int = 100, + dividend_type: str = 'none', + fill_data: bool = True, + data_dir: str = '', + short_window: int = 50, + long_window: int = 200): + try: + # 初始化一个列表用于存储每只股票的回测结果字典 + results_list = [] + + # 遍历每只股票的数据(每列代表一个股票的收盘价) + data = await get_local_data(field_list, stock_list, period, start_time, end_time, count, dividend_type, + fill_data, + data_dir) + + for stock_code in stock_list: + + data_column_name = f'close_{stock_code}' + source_column_name = f'{stock_code} MACD策略' + backtest_name = f'{stock_code} MACD策略 MA{short_window}-{long_window}日' + now_data = int(datetime.now().strftime('%Y%m%d')) + db_result_data = await wance_data_storage_backtest.WanceDataStorageBacktest.filter( + backtest_name=backtest_name) + + if db_result_data: + if db_result_data[0].backtest_end_time == now_data: + results_list.append({source_column_name: db_result_data[0]}) + + elif data_column_name in data.columns: + stock_data_series = data[[data_column_name]] # 提取该股票的收盘价 DataFrame + stock_data_series.columns = ['close'] # 重命名列为 'close' + + # 创建MACD策略 + strategy, signal = await create_dual_ma_strategy(stock_data_series, stock_code, + short_window=short_window, + long_window=long_window) + # 创建回测 + backtest = bt.Backtest(strategy=strategy, data=stock_data_series, initial_capital=100000) + # 运行回测 + result = bt.run(backtest) + # 存储回测结果 + data_to_store = await storage_backtest_data(source_column_name, result, signal, stock_code, + stock_data_series, + short_window, long_window) + # # 绘制回测结果图表 + # result.plot() + # # 绘制个别股票数据图表 + # plt.figure(figsize=(12, 6)) + # plt.plot(stock_data_series.index, stock_data_series['close'], label='Stock Price') + # plt.title(f'Stock Price for {stock_code}') + # plt.xlabel('Date') + # plt.ylabel('Price') + # plt.legend() + # plt.grid(True) + # plt.show() + # 将结果存储为字典并添加到列表中 + results_list.append({source_column_name: data_to_store}) + + else: + print(f"数据中缺少列: {data_column_name}") + + return results_list # 返回结果列表 + + except Exception as e: + print(f"Error occurred: {e}") + + +async def start_macd_backtest_service(field_list: list, + stock_list: list, + period: str = '1d', + start_time: str = '', + end_time: str = '', + count: int = -1, + dividend_type: str = 'none', + fill_data: bool = True, + data_dir: str = '', + short_window: int = 50, + long_window: int = 200): + for stock_code in stock_list: + backtest_name = f'{stock_code} MACD策略 MA{short_window}-{long_window}日' + db_result = await wance_data_storage_backtest.WanceDataStorageBacktest.filter( + backtest_name=backtest_name) + now_time = int(datetime.now().strftime('%Y%m%d')) + + if db_result and db_result[0].backtest_end_time == now_time: + return db_result + else: + # 执行回测 + result = await run_macd_backtest( + field_list=field_list, + stock_list=stock_list, + period=period, + start_time=start_time, + end_time=end_time, + count=count, + dividend_type=dividend_type, + fill_data=fill_data, + data_dir=data_dir, + short_window=short_window, + long_window=long_window, + ) + return result + + +async def init_backtest_db(): + MACD_list = [{"short_window": 5, "long_window": 10}, {"short_window": 10, "long_window": 30}, + {"short_window": 30, "long_window": 60}, {"short_window": 30, "long_window": 90}, + {"short_window": 70, "long_window": 140}, {"short_window": 120, "long_window": 250}] + await init_tortoise() + wance_db = await wance_data_stock.WanceDataStock.all() + MACD_list_lenght = len(MACD_list) + + for stock_code in wance_db: + for i in range(MACD_list_lenght): + short_window = MACD_list[i]['short_window'] + long_window = MACD_list[i]['long_window'] + source_column_name = f'{stock_code} MACD策略 MA{short_window}-{long_window}日' + result = await run_macd_backtest(field_list=['close', 'time'], + stock_list=[stock_code.stock_code], + short_window=short_window, + long_window=long_window) + + print(f"回测成功 {source_column_name}") + + +if __name__ == '__main__': + # 测试类的回测 + # asyncio.run(run_macd_backtest(field_list=['close', 'time'], + # stock_list=['601222.SH', '601677.SH'], + # short_window=10, + # long_window=30)) + + # 初始化数据库表 + asyncio.run(init_backtest_db()) diff --git a/src/backtest/reverse_dual_ma_strategy.py b/src/backtest/reverse_dual_ma_strategy.py new file mode 100644 index 0000000..2417c53 --- /dev/null +++ b/src/backtest/reverse_dual_ma_strategy.py @@ -0,0 +1,264 @@ +import asyncio +import json +import time +from datetime import datetime + +import bt +import numpy as np +import pandas as pd +from xtquant import xtdata +import matplotlib.pyplot as plt + +from src.backtest.until import get_local_data, convert_pandas_to_json_serializable +from src.models import wance_data_storage_backtest, wance_data_stock +from src.tortoises_orm_config import init_tortoise + + +# 反双均线策略函数 +async def create_dual_ma_strategy(data, stock_code: str, short_window: int = 50, long_window: int = 200): + # 生成反双均线策略信号 + signal = await reverse_dual_ma_strategy(data, short_window, long_window) + + # 使用bt框架构建策略 + strategy = bt.Strategy(f'{stock_code} 反双均线策略', + [bt.algos.RunDaily(), + bt.algos.SelectAll(), # 选择所有股票 + bt.algos.WeighTarget(signal), # 根据信号调整权重 + bt.algos.Rebalance()]) # 调仓 + return strategy, signal + + + +# 定义反反双均线策略的函数 +def reverse_dual_ma_strategy(data, short_window=50, long_window=200): + """ + 反反双均线策略,当短期均线跌破长期均线时买入,穿过长期均线时卖出。 + + 参数: + data: pd.DataFrame, 股票的价格数据,行索引为日期,列为股票代码。 + short_window: int, 短期均线的窗口期。 + long_window: int, 长期均线的窗口期。 + + 返回: + signal: pd.DataFrame, 每只股票的买卖信号,1 表示买入,0 表示卖出。 + """ + # 计算短期均线和长期均线 + short_ma = data.rolling(window=short_window).mean() + long_ma = data.rolling(window=long_window).mean() + + # 初始化信号 DataFrame + signal = pd.DataFrame(index=data.index, columns=data.columns) + + # 生成买入信号:短期均线从上往下穿过长期均线 + for column in data.columns: + signal[column] = (short_ma[column] < long_ma[column]).astype(int) # 跌破时买入,信号为1 + signal[column] = (short_ma[column] > long_ma[column]).astype(int) * -1 + signal[column] # 穿过时卖出,信号为0 + + # 前向填充信号,保持持仓不变 + signal = signal.ffill() + + return signal + + +async def storage_backtest_data(source_column_name, result, signal, stock_code, stock_data_series, short_window, + long_window): + await init_tortoise() + + # 要存储的字段列表 + fields_to_store = [ + 'stock_code', 'strategy_name', 'stock_close_price', 'daily_price', + 'price', 'returns', 'data_start_time', 'data_end_time', + 'backtest_end_time', 'position', 'backtest_name', 'rf', 'total_return', 'cagr', + 'max_drawdown', 'calmar', 'mtd', 'three_month', + 'six_month', 'ytd', 'one_year', 'three_year', + 'five_year', 'ten_year', 'incep', 'daily_sharpe', + 'daily_sortino', 'daily_mean', 'daily_vol', + 'daily_skew', 'daily_kurt', 'best_day', 'worst_day', + 'monthly_sharpe', 'monthly_sortino', 'monthly_mean', + 'monthly_vol', 'monthly_skew', 'monthly_kurt', + 'best_month', 'worst_month', 'yearly_sharpe', + 'yearly_sortino', 'yearly_mean', 'yearly_vol', + 'yearly_skew', 'yearly_kurt', 'best_year', 'worst_year', + 'avg_drawdown', 'avg_drawdown_days', 'avg_up_month', + 'avg_down_month', 'win_year_perc', 'twelve_month_win_perc' + ] + + # 准备要存储的数据 + data_to_store = { + 'stock_code': stock_code, + 'strategy_name': "反双均线策略", + 'stock_close_price': json.dumps(stock_data_series.fillna(0).rename_axis('time').reset_index().assign( + time=stock_data_series.index.strftime('%Y%m%d')).set_index('time').to_dict(orient='index')), + 'daily_price': convert_pandas_to_json_serializable(result[source_column_name].daily_prices), + 'price': convert_pandas_to_json_serializable(result[source_column_name].prices), + 'returns': convert_pandas_to_json_serializable(result[source_column_name].returns.fillna(0)), + 'data_start_time': pd.to_datetime(result.stats.loc["start"].iloc[0]).strftime('%Y%m%d'), + 'data_end_time': pd.to_datetime(result.stats.loc["end"].iloc[0]).strftime('%Y%m%d'), + 'backtest_end_time': int(datetime.now().strftime('%Y%m%d')), + 'position': convert_pandas_to_json_serializable(signal), + 'backtest_name': f'{stock_code} 反双均线策略 MA{short_window}-{long_window}日', + 'indicator_type': 'reverse_SMA', + 'indicator_information': json.dumps({'short_window': short_window, 'long_window': long_window}) + } + + # 使用循环填充其他字段 + for field in fields_to_store[12:]: # 从第10个字段开始 + value = result.stats.loc[field].iloc[0] + data_to_store[field] = 0.0 if (isinstance(value, float) and np.isnan(value)) else value + + # 检查是否存在该 backtest_name + existing_record = await wance_data_storage_backtest.WanceDataStorageBacktest.filter( + backtest_name=data_to_store['backtest_name'] + ).first() + + if existing_record: + # 如果存在,更新记录 + await wance_data_storage_backtest.WanceDataStorageBacktest.filter( + id=existing_record.id + ).update(**data_to_store) + else: + # 如果不存在,创建新的记录 + await wance_data_storage_backtest.WanceDataStorageBacktest.create(**data_to_store) + + return data_to_store + + +async def run_reverse_reverse_SMA_backtest(field_list: list, + stock_list: list, + period: str = '1d', + start_time: str = '', + end_time: str = '', + count: int = 100, + dividend_type: str = 'none', + fill_data: bool = True, + data_dir: str = '', + short_window: int = 50, + long_window: int = 200): + try: + # 初始化一个列表用于存储每只股票的回测结果字典 + results_list = [] + + # 遍历每只股票的数据(每列代表一个股票的收盘价) + data = await get_local_data(field_list, stock_list, period, start_time, end_time, count, dividend_type, + fill_data, + data_dir) + + for stock_code in stock_list: + + data_column_name = f'close_{stock_code}' + source_column_name = f'{stock_code} 反双均线策略' + backtest_name = f'{stock_code} 反双均线策略 MA{short_window}-{long_window}日' + now_data = int(datetime.now().strftime('%Y%m%d')) + db_result_data = await wance_data_storage_backtest.WanceDataStorageBacktest.filter( + backtest_name=backtest_name) + + if db_result_data: + if db_result_data[0].backtest_end_time == now_data: + results_list.append({source_column_name: db_result_data[0]}) + + elif data_column_name in data.columns: + stock_data_series = data[[data_column_name]] # 提取该股票的收盘价 DataFrame + stock_data_series.columns = ['close'] # 重命名列为 'close' + + # 创建反双均线策略 + strategy, signal = await create_dual_ma_strategy(stock_data_series, stock_code, + short_window=short_window, + long_window=long_window) + # 创建回测 + backtest = bt.Backtest(strategy=strategy, data=stock_data_series, initial_capital=100000) + # 运行回测 + result = bt.run(backtest) + # 存储回测结果 + data_to_store = await storage_backtest_data(source_column_name, result, signal, stock_code, + stock_data_series, + short_window, long_window) + # # 绘制回测结果图表 + # result.plot() + # # 绘制个别股票数据图表 + # plt.figure(figsize=(12, 6)) + # plt.plot(stock_data_series.index, stock_data_series['close'], label='Stock Price') + # plt.title(f'Stock Price for {stock_code}') + # plt.xlabel('Date') + # plt.ylabel('Price') + # plt.legend() + # plt.grid(True) + # plt.show() + # 将结果存储为字典并添加到列表中 + results_list.append({source_column_name: data_to_store}) + + else: + print(f"数据中缺少列: {data_column_name}") + + return results_list # 返回结果列表 + + except Exception as e: + print(f"Error occurred: {e}") + + +async def start_reverse_SMA_backtest_service(field_list: list, + stock_list: list, + period: str = '1d', + start_time: str = '', + end_time: str = '', + count: int = -1, + dividend_type: str = 'none', + fill_data: bool = True, + data_dir: str = '', + short_window: int = 50, + long_window: int = 200): + for stock_code in stock_list: + backtest_name = f'{stock_code} 反双均线策略 MA{short_window}-{long_window}日' + db_result = await wance_data_storage_backtest.WanceDataStorageBacktest.filter( + backtest_name=backtest_name) + now_time = int(datetime.now().strftime('%Y%m%d')) + + if db_result and db_result[0].backtest_end_time == now_time: + return db_result + else: + # 执行回测 + result = await run_reverse_reverse_SMA_backtest( + field_list=field_list, + stock_list=stock_list, + period=period, + start_time=start_time, + end_time=end_time, + count=count, + dividend_type=dividend_type, + fill_data=fill_data, + data_dir=data_dir, + short_window=short_window, + long_window=long_window, + ) + return result + + +async def init_backtest_db(): + reverse_SMA_list = [{"short_window": 5, "long_window": 10}, {"short_window": 10, "long_window": 30}, + {"short_window": 30, "long_window": 60}, {"short_window": 30, "long_window": 90}, + {"short_window": 70, "long_window": 140}, {"short_window": 120, "long_window": 250}] + await init_tortoise() + wance_db = await wance_data_stock.WanceDataStock.all() + reverse_SMA_list_lenght = len(reverse_SMA_list) + + for stock_code in wance_db: + for i in range(reverse_SMA_list_lenght): + short_window = reverse_SMA_list[i]['short_window'] + long_window = reverse_SMA_list[i]['long_window'] + source_column_name = f'{stock_code} 反双均线策略 MA{short_window}-{long_window}日' + result = await run_reverse_reverse_SMA_backtest(field_list=['close', 'time'], + stock_list=[stock_code.stock_code], + short_window=short_window, + long_window=long_window) + + print(f"回测成功 {source_column_name}") + + +if __name__ == '__main__': + # 测试类的回测 + # asyncio.run(run_reverse_SMA_backtest(field_list=['close', 'time'], + # stock_list=['601222.SH', '601677.SH'], + # short_window=10, + # long_window=30)) + + # 初始化数据库表 + asyncio.run(init_backtest_db()) diff --git a/src/backtest/router.py b/src/backtest/router.py new file mode 100644 index 0000000..4eed6ea --- /dev/null +++ b/src/backtest/router.py @@ -0,0 +1,23 @@ +from fastapi import APIRouter, HTTPException # 从 FastAPI 中导入 APIRouter,用于创建 API 路由器 + +from src.backtest.service import start_backtest_service +from src.pydantic.backtest_request import BackRequest + +router = APIRouter() # 创建一个 FastAPI 路由器实例 + + +@router.get("/start_backtest") +async def start_backtest(request: BackRequest): + result = await start_backtest_service(field_list=['close', 'time'], + stock_list=request.stock_list, + period=request.period, + start_time=request.start_time, + end_time=request.end_time, + count=request.count, + dividend_type=request.dividend_type, + fill_data=request.fill_data, + ma_type=request.ma_type, + short_window=request.short_window, + long_window=request.long_window + ) + return result diff --git a/src/backtest/rsi_strategy.py b/src/backtest/rsi_strategy.py new file mode 100644 index 0000000..9832db2 --- /dev/null +++ b/src/backtest/rsi_strategy.py @@ -0,0 +1,296 @@ +import asyncio +import json +import time +from datetime import datetime + +import bt +import numpy as np +import pandas as pd +from xtquant import xtdata +import matplotlib.pyplot as plt + +from src.backtest.until import get_local_data, convert_pandas_to_json_serializable +from src.models import wance_data_storage_backtest, wance_data_stock +from src.tortoises_orm_config import init_tortoise + + +# RSI策略函数 +async def create_dual_ma_strategy(data, stock_code: str, short_window: int = 50, long_window: int = 200, + overbought: int = 70, oversold: int = 30): + # 生成RSI策略信号 + signal = await rsi_strategy(data, short_window, long_window, overbought, oversold) + + # 使用bt框架构建策略 + strategy = bt.Strategy(f'{stock_code} RSI策略', + [bt.algos.RunDaily(), + bt.algos.SelectAll(), # 选择所有股票 + bt.algos.WeighTarget(signal), # 根据信号调整权重 + bt.algos.Rebalance()]) # 调仓 + return strategy, signal + + +async def rsi_strategy(df, short_window=14, long_window=28, overbought=70, oversold=30): + """ + 基于RSI的策略生成买卖信号。 + + 参数: + df: pd.DataFrame, 股票的价格数据,行索引为日期,列为股票代码。 + short_window: int, 短期RSI的窗口期。 + long_window: int, 长期RSI的窗口期。 + overbought: int, 超买水平。 + oversold: int, 超卖水平。 + + 返回: + signal: pd.DataFrame, 每只股票的买卖信号,1 表示买入,0 表示卖出。 + """ + delta = df.diff().fillna(0) + + gain = (delta.where(delta > 0, 0).rolling(window=short_window).mean()).fillna(0) + loss = (-delta.where(delta < 0, 0).rolling(window=short_window).mean()).fillna(0) + + short_rsi = (100 - (100 / (1 + (gain / loss)))).fillna(0) + + long_gain = (delta.where(delta > 0, 0).rolling(window=long_window).mean()).fillna(0) + long_loss = (-delta.where(delta < 0, 0).rolling(window=long_window).mean()).fillna(0) + + long_rsi = (100 - (100 / (1 + (long_gain / long_loss)))).fillna(0) + + signal = pd.DataFrame(index=df.index, columns=df.columns) + + for column in df.columns: + signal[column] = np.where((short_rsi[column] < 30) & (long_rsi[column] < 30) & (short_rsi[column] != 0) & (long_rsi[column] != 0), 1, 0) + signal[column] = np.where((short_rsi[column] > 70) & (long_rsi[column] > 70) & (short_rsi[column] != 0) & (long_rsi[column] != 0), 0, signal[column]) + + return signal.ffill().fillna(0) + + +async def storage_backtest_data(source_column_name, result, signal, stock_code, stock_data_series, short_window: int, + long_window: int, overbought: int = 70, + oversold: int = 30): + await init_tortoise() + + # 要存储的字段列表 + fields_to_store = [ + 'stock_code', 'strategy_name', 'stock_close_price', 'daily_price', + 'price', 'returns', 'data_start_time', 'data_end_time', + 'backtest_end_time', 'position', 'backtest_name', 'rf', 'total_return', 'cagr', + 'max_drawdown', 'calmar', 'mtd', 'three_month', + 'six_month', 'ytd', 'one_year', 'three_year', + 'five_year', 'ten_year', 'incep', 'daily_sharpe', + 'daily_sortino', 'daily_mean', 'daily_vol', + 'daily_skew', 'daily_kurt', 'best_day', 'worst_day', + 'monthly_sharpe', 'monthly_sortino', 'monthly_mean', + 'monthly_vol', 'monthly_skew', 'monthly_kurt', + 'best_month', 'worst_month', 'yearly_sharpe', + 'yearly_sortino', 'yearly_mean', 'yearly_vol', + 'yearly_skew', 'yearly_kurt', 'best_year', 'worst_year', + 'avg_drawdown', 'avg_drawdown_days', 'avg_up_month', + 'avg_down_month', 'win_year_perc', 'twelve_month_win_perc' + ] + + # 准备要存储的数据 + data_to_store = { + 'stock_code': stock_code, + 'strategy_name': "RSI策略", + 'stock_close_price': json.dumps(stock_data_series.fillna(0).rename_axis('time').reset_index().assign( + time=stock_data_series.index.strftime('%Y%m%d')).set_index('time').to_dict(orient='index')), + 'daily_price': convert_pandas_to_json_serializable(result[source_column_name].daily_prices), + 'price': convert_pandas_to_json_serializable(result[source_column_name].prices), + 'returns': convert_pandas_to_json_serializable(result[source_column_name].returns.fillna(0)), + 'data_start_time': pd.to_datetime(result.stats.loc["start"].iloc[0]).strftime('%Y%m%d'), + 'data_end_time': pd.to_datetime(result.stats.loc["end"].iloc[0]).strftime('%Y%m%d'), + 'backtest_end_time': int(datetime.now().strftime('%Y%m%d')), + 'position': convert_pandas_to_json_serializable(signal), + 'backtest_name': f'{stock_code} RSI策略 RSI{short_window}-RSI{long_window}-overbought{overbought}-oversold{oversold}', + 'indicator_type': 'RSI', + 'indicator_information': json.dumps( + {'short_window': short_window, 'long_window': long_window, 'overbought': overbought, 'oversold': oversold}) + } + + # 使用循环填充其他字段 + for field in fields_to_store[12:]: # 从第12个字段开始 + value = result.stats.loc[field].iloc[0] + + if isinstance(value, float): + if np.isnan(value): + data_to_store[field] = 0.0 # NaN 处理为 0 + elif np.isinf(value): # 判断是否为无穷大或无穷小 + if value > 0: + data_to_store[field] = 99999.9999 # 正无穷处理 + else: + data_to_store[field] = -99999.9999 # 负无穷处理 + else: + data_to_store[field] = value # 正常的浮点值 + else: + data_to_store[field] = value # 非浮点类型保持不变 + + # 检查是否存在该 backtest_name + existing_record = await wance_data_storage_backtest.WanceDataStorageBacktest.filter( + backtest_name=data_to_store['backtest_name'] + ).first() + + if existing_record: + # 如果存在,更新记录 + await wance_data_storage_backtest.WanceDataStorageBacktest.filter( + id=existing_record.id + ).update(**data_to_store) + else: + # 如果不存在,创建新的记录 + await wance_data_storage_backtest.WanceDataStorageBacktest.create(**data_to_store) + + return data_to_store + + +async def run_rsi_backtest(field_list: list, + stock_list: list, + period: str = '1d', + start_time: str = '', + end_time: str = '', + count: int = 100, + dividend_type: str = 'none', + fill_data: bool = True, + data_dir: str = '', + short_window: int = 50, + long_window: int = 200, + overbought: int = 70, + oversold: int = 30 + ): + try: + # 初始化一个列表用于存储每只股票的回测结果字典 + results_list = [] + + # 遍历每只股票的数据(每列代表一个股票的收盘价) + data = await get_local_data(field_list, stock_list, period, start_time, end_time, count, dividend_type, + fill_data, + data_dir) + + for stock_code in stock_list: + + data_column_name = f'close_{stock_code}' + source_column_name = f'{stock_code} RSI策略' + backtest_name = f'{stock_code} RSI策略 RSI{short_window}-RSI{long_window}' + now_data = int(datetime.now().strftime('%Y%m%d')) + db_result_data = await wance_data_storage_backtest.WanceDataStorageBacktest.filter( + backtest_name=backtest_name) + + if db_result_data: + if db_result_data[0].backtest_end_time == now_data: + results_list.append({source_column_name: db_result_data[0]}) + + # elif data_column_name in data.columns: + if data_column_name in data.columns: + stock_data_series = data[[data_column_name]] # 提取该股票的收盘价 DataFrame + stock_data_series.columns = ['close'] # 重命名列为 'close' + + # 创建RSI策略 + strategy, signal = await create_dual_ma_strategy(stock_data_series, stock_code, + short_window=short_window, long_window=long_window) + # 创建回测 + backtest = bt.Backtest(strategy=strategy, data=stock_data_series, initial_capital=100000) + # 运行回测 + result = bt.run(backtest) + # 存储回测结果 + data_to_store = await storage_backtest_data(source_column_name, result, signal, stock_code, + stock_data_series, short_window, long_window, overbought, + oversold) + # # 绘制回测结果图表 + # result.plot() + # # 绘制个别股票数据图表 + # plt.figure(figsize=(12, 6)) + # plt.plot(stock_data_series.index, stock_data_series['close'], label='Stock Price') + # plt.title(f'Stock Price for {stock_code}') + # plt.xlabel('Date') + # plt.ylabel('Price') + # plt.legend() + # plt.grid(True) + # plt.show() + # 将结果存储为字典并添加到列表中 + results_list.append({source_column_name: data_to_store}) + + else: + print(f"数据中缺少列: {data_column_name}") + + return results_list # 返回结果列表 + + except Exception as e: + print(f"Error occurred: {e}") + + +async def start_rsi_backtest_service(field_list: list, + stock_list: list, + period: str = '1d', + start_time: str = '', + end_time: str = '', + count: int = -1, + dividend_type: str = 'none', + fill_data: bool = True, + data_dir: str = '', + short_window: int = 50, + long_window: int = 200, + overbought: int = 70, + oversold: int = 30 + ): + for stock_code in stock_list: + backtest_name = f'{stock_code} RSI策略 RSI{short_window}-RSI{long_window}' + db_result = await wance_data_storage_backtest.WanceDataStorageBacktest.filter( + backtest_name=backtest_name) + now_time = int(datetime.now().strftime('%Y%m%d')) + + if db_result and db_result[0].backtest_end_time == now_time: + return db_result + else: + # 执行回测 + result = await run_rsi_backtest( + field_list=field_list, + stock_list=stock_list, + period=period, + start_time=start_time, + end_time=end_time, + count=count, + dividend_type=dividend_type, + fill_data=fill_data, + data_dir=data_dir, + short_window=short_window, + long_window=long_window, + overbought=overbought, + oversold=oversold + ) + return result + + +async def init_backtest_db(): + sma_list = [{"short_window": 3, "long_window": 6}, {"short_window": 6, "long_window": 12}, + {"short_window": 12, "long_window": 24}, {"short_window": 14, "long_window": 18}, + {"short_window": 15, "long_window": 10}] + await init_tortoise() + wance_db = await wance_data_stock.WanceDataStock.all() + sma_list_lenght = len(sma_list) + + for stock_code in wance_db: + for i in range(sma_list_lenght): + short_window = sma_list[i]['short_window'] + long_window = sma_list[i]['long_window'] + source_column_name = f'{stock_code} RSI策略 RSI{short_window}-RSI{long_window}' + result = await start_rsi_backtest_service(field_list=['close', 'time'], + stock_list=[stock_code.stock_code], + short_window=short_window, + long_window=long_window, + overbought=70, + oversold=30) + + print(f"回测成功 {source_column_name}") + + +if __name__ == '__main__': + # 测试类的回测 + asyncio.run(run_rsi_backtest(field_list=['close', 'time'], + stock_list=['601222.SH', '601677.SH'], + count=-1, + short_window=10, + long_window=30, + overbought=70, + oversold=30 + )) + + # # 初始化数据库表 + # asyncio.run(init_backtest_db()) diff --git a/src/backtest/service.py b/src/backtest/service.py new file mode 100644 index 0000000..e07e9b8 --- /dev/null +++ b/src/backtest/service.py @@ -0,0 +1,74 @@ +from src.backtest.bollinger_bands import run_bollinger_backtest, start_bollinger_backtest_service +from src.backtest.dual_moving_average import run_sma_backtest, start_sma_backtest_service +from src.backtest.reverse_dual_ma_strategy import start_reverse_SMA_backtest_service +from src.backtest.rsi_strategy import start_rsi_backtest_service +from src.backtest.until import data_check + + +async def start_backtest_service(field_list: list, + stock_list: list, + period: str = '1d', + start_time: str = '', + end_time: str = '', + count: int = -1, + dividend_type: str = 'none', + fill_data: bool = True, + data_dir: str = '', + ma_type: str = 'SMA', + short_window: int = 50, + long_window: int = 200, + bollingerMA: int = 200, + std_dev: int = 200, + overbought: int = 70, + oversold: int = 30, + signal_window: int = 9): + # 数据检查 + await data_check(field_list=field_list, + stock_list=stock_list, + period=period, + start_time=start_time, + end_time=end_time, + count=count, + dividend_type=dividend_type, + fill_data=fill_data, + data_dir=data_dir) + + # 策略映射 + strategies = { + 'SMA': start_sma_backtest_service, + 'Bollinger': start_bollinger_backtest_service, + 'RSI': start_rsi_backtest_service, + 'RESMA': start_reverse_SMA_backtest_service, + 'MACD': start_rsi_backtest_service + } + + # 通用参数 + base_params = { + 'field_list': field_list, + 'stock_list': stock_list, + 'period': period, + 'start_time': start_time, + 'end_time': end_time, + 'count': count, + 'dividend_type': dividend_type, + 'fill_data': fill_data, + 'data_dir': data_dir, + } + + # 特定策略参数 + strategy_params = { + 'SMA': {'short_window': short_window, 'long_window': long_window}, + 'Bollinger': {'bollingerMA': bollingerMA, 'std_dev': std_dev}, + 'RSI': {'short_window': short_window, 'long_window': long_window, 'overbought': overbought, + 'oversold': oversold}, + 'RESMA': {'short_window': short_window, 'long_window': long_window}, + 'MACD': {'short_window': short_window, 'long_window': signal_window} + } + + # 选择策略并执行 + strategy_func = strategies.get(ma_type) + if strategy_func: + result = await strategy_func(**base_params, **strategy_params[ma_type]) + return result + else: + return None diff --git a/src/backtest/until.py b/src/backtest/until.py new file mode 100644 index 0000000..253919c --- /dev/null +++ b/src/backtest/until.py @@ -0,0 +1,99 @@ +import json +from datetime import datetime + +import numpy as np +from xtquant import xtdata +import pandas as pd + +# 数据的列名 +columns = ['open', 'high', 'low', 'close', 'volume', 'amount', 'settelmentPrice', + 'openInterest', 'preClose', 'suspendFlag'] + + +# 获取本地数据并进行处理 +async def get_local_data(field_list: list, stock_list: list, period: str, start_time: str, end_time: str, + count: int, dividend_type: str, fill_data: bool, data_dir: str): + result = xtdata.get_local_data(field_list=field_list, stock_list=stock_list, period=period, start_time=start_time, + end_time=end_time, count=count, dividend_type=dividend_type, fill_data=fill_data, + data_dir=data_dir) + return await data_processing(result) + + +async def data_processing(result_local): + # 初始化一个空的列表,用于存储每个股票的数据框 + df_list = [] + + # 遍历字典中的 DataFrame + for stock_code, df in result_local.items(): + # 确保 df 是一个 DataFrame + if isinstance(df, pd.DataFrame): + # 将时间戳转换为日期时间格式,只保留年-月-日 + df['time'] = pd.to_datetime(df['time'], unit='ms').dt.date + # 将 'time' 列设置为索引,保留为日期格式 + df.set_index('time', inplace=True) + # 将 'close' 列重命名为 'close_股票代码' + df.rename(columns={'close': f'close_{stock_code}'}, inplace=True) + # 将 DataFrame 添加到列表中 + df_list.append(df[[f'close_{stock_code}']]) # 只保留 'close_股票代码' 列 + else: + print(f"数据格式错误: {stock_code} 不包含 DataFrame") + + # 使用 pd.concat() 将所有 DataFrame 合并为一个大的 DataFrame + combined_df = pd.concat(df_list, axis=1) + + # 确保返回的 DataFrame 索引是日期格式 + combined_df.index = pd.to_datetime(combined_df.index) + + return combined_df + + +def convert_pandas_to_json_serializable(data: pd.Series) -> str: + """ + 将 Pandas Series 或 DataFrame 中的 Timestamp 索引转换为字符串,并返回 JSON 可序列化的结果。 + + 参数: + data: pd.Series 或 pd.DataFrame, 带有时间戳索引的 pandas 数据 + + 返回: + JSON 字符串,键为日期字符串,值为原数据的值。 + """ + # 判断数据类型 + if isinstance(data, (pd.Series, pd.DataFrame)): + # 如果索引是时间戳类型,则转换为 YYYYMMDD 格式 + if isinstance(data.index, pd.DatetimeIndex): + data.index = data.index.strftime('%Y%m%d') + + # 处理 NaN 和 None 的情况,替换为 0 或其他合适的默认值 + data = data.replace([np.nan, None], 0) + + # 将索引重置为普通列,然后转换为字典 + json_serializable_data = data.rename_axis('date').reset_index().to_dict(orient='records') + + # 将字典转换为 JSON 格式字符串 + json_string = json.dumps(json_serializable_data) + return json_string + else: + raise ValueError("输入必须为 Pandas Series 或 DataFrame") + + +async def data_check(field_list: list, + stock_list: list, + period: str = '1d', + start_time: str = '', + end_time: str = '', + count: int = -1, + dividend_type: str = 'none', + fill_data: bool = True, + data_dir: str = '', ): + result_data = xtdata.get_local_data(field_list=[], stock_list=stock_list, period=period, start_time=start_time, + end_time=end_time, count=count, dividend_type=dividend_type, fill_data=fill_data, + data_dir=data_dir) + time_now = int(datetime.now().strftime('%Y%m%d')) + for i in stock_list: + close = int(result_data.get(i).index[-1]) + if close != 0 and close < time_now: + xtdata.download_history_data(stock_code=i, + period='1d', + start_time='', + end_time='', + incrementally=True) diff --git a/src/constants.py b/src/constants.py new file mode 100644 index 0000000..e1eab81 --- /dev/null +++ b/src/constants.py @@ -0,0 +1,43 @@ +from enum import Enum + +THISPOLICYNAME = '' +def setThisPolicyName(name): + THISPOLICYNAME = name + +def getThisPolicyName(): + return THISPOLICYNAME + +DB_NAMING_CONVENTION = { + "ix": "%(column_0_label)s_idx", + "uq": "%(table_name)s_%(column_0_name)s_key", + "ck": "%(table_name)s_%(constraint_name)s_check", + "fk": "%(table_name)s_%(column_0_name)s_fkey", + "pk": "%(table_name)s_pkey", +} + + +class RedisKeyConstants: + SMS_CODE_KEY = "sms:code:" + TOKEN_KEY = "Youcailogin:" + ACCESS_TOKEN_KEY = "applet_access_token_youcai" + SESSION_KEY = "session_id_youcai:" + OFFICIAL_ACCESS_TOKEN_KEY = "official_access_token_youcai" + + +class Environment(str, Enum): + LOCAL = "LOCAL" + STAGING = "STAGING" + TESTING = "TESTING" + PRODUCTION = "PRODUCTION" + + @property + def is_debug(self): + return self in (self.LOCAL, self.STAGING, self.TESTING) + + @property + def is_testing(self): + return self == self.TESTING + + @property + def is_deployed(self) -> bool: + return self in (self.STAGING, self.PRODUCTION) diff --git a/readme.md b/src/data_processing/__init__.py similarity index 100% rename from readme.md rename to src/data_processing/__init__.py diff --git a/src/data_processing/history_data_processing.py b/src/data_processing/history_data_processing.py new file mode 100644 index 0000000..63c1531 --- /dev/null +++ b/src/data_processing/history_data_processing.py @@ -0,0 +1,412 @@ +import asyncio +from datetime import datetime + +import akshare as ak +import pandas as pd +from xtquant import xtdata + +from src.models import wance_data_stock +from src.tortoises_orm_config import init_tortoise +from src.utils.history_data_processing_utils import translation_dict, get_best_match, safe_get_value, on_progress +from src.utils.split_stock_utils import split_stock_code, join_stock_code, percent_to_float +from src.xtdata.service import download_history_data_service, get_full_tick_keys_service, download_history_data2_service + +# period - 周期,用于表示要获取的周期和具体数据类型 +period_list = ["1d", "1h", "30m", "15m", "5m", "1m", "tick", "1w", "1mon", "1q", "1hy", "1y"] +# 数据的列名 +columns = ['open', 'high', 'low', 'close', 'volume', 'amount', 'settelmentPrice', + 'openInterest', 'preClose', 'suspendFlag'] + + +def processing_data(field_list: list, stock_list: list, period: str, start_time: str, end_time: str, count: int, + dividend_type: str, fill_data: bool): + """ + + :param field_list: [] + :param stock_list: ["186511.SH","173312.SH","231720.SH","173709.SH","019523.SH"] + :param period: "1d" + :param start_time: "20240506" + :param end_time: "" + :param count: -1 + :param dividend_type: "none" + :param fill_data: False + :return: + """ + try: + # 获取本地数据 + result_local = xtdata.get_local_data(field_list=field_list, + stock_list=stock_list, + period=period, + start_time=start_time, + end_time=end_time, + count=count, + dividend_type=dividend_type, + fill_data=fill_data, + data_dir="" + ) + + # 初始化一个空的列表,用于存储每个股票的数据框 + df_list = [] + # 遍历字典中的 DataFrame + for stock_code, df in result_local.items(): + # 确保 df 是一个 DataFrame + if isinstance(df, pd.DataFrame): + # 将时间戳转换为日期时间格式,并格式化为字符串 'YYYYMMDD' + df['time'] = pd.to_datetime(df['time'], unit='ms').dt.strftime('%Y%m%d') + # 将 'time' 列设置为索引 + df.set_index('time', inplace=True) + # 指定列名 + df.columns = columns + # 添加一列 'stock_code' 用于标识不同的股票 + df['stock_code'] = stock_code + # 将 DataFrame 添加到列表中 + df_list.append(df) + else: + print(f"数据格式错误: {stock_code} 不包含 DataFrame") + + # 使用 pd.concat() 将所有 DataFrame 合并为一个大的 DataFrame + combined_df = pd.concat(df_list) + + # 打印合并后的 DataFrame + print(combined_df) + + print(f"开始获取股票数据{result_local}") + except Exception as e: + print(f"处理数据发生错误: {str(e)}") + + +def history_data_processing(): + """ + 本地路径 D:\\e海方舟-量化交易版\\userdata_mini\\datadir + :return: + """ + + try: + result = xtdata.get_full_tick(code_list=['SH', 'SZ']) + result = list(result.keys()) + # for key in result: + # xtdata.download_history_data(stock_code=key, period="1d", start_time="", end_time="", incrementally="") + datas = xtdata.get_local_data(field_list=[], stock_list=result, period='1d', start_time='', end_time='', + count=-1, + dividend_type='none', fill_data=True) + # datas = xtdata.download_history_data2(stock_list=result, period="1d", start_time="", end_time="", + # callback=on_progress) + print(datas, "这里是返回的数据") + except Exception as e: + print(f"处理数据发生错误: {str(e)}") + + +async def init_indicator(): + await init_tortoise() + # 从数据库中获取股票列表 + stock_list = await wance_data_stock.WanceDataStock.filter(stock_type__contains=["stock"]).all() + stock_zh_a_spot_em_df = ak.stock_zh_a_spot_em() + + # 遍历股票列表拿到股票实体 + for stock in stock_list: + try: + # 使用 akshare 获取股票指标数据 + stock_code = stock.stock_code # 假设 stock_code 是股票代码的字段 + stock_code_suffix = split_stock_code(stock_code) # 提取股票代码部分 + stock_code_xq = join_stock_code(stock_code_suffix) + stock_code_front = stock_code_suffix[0] + # 筛选匹配的行 + match = stock_zh_a_spot_em_df.loc[stock_zh_a_spot_em_df['代码'] == stock_code_front, '涨跌幅'] + + # 从akshare中获取数据 + stock_a_indicator_lg_df = ak.stock_a_indicator_lg(symbol=stock_code_front) # 乐咕乐股-A 股个股指标: 市盈率, 市净率, 股息率 + stock_financial_abstract_ths_df = ak.stock_financial_abstract_ths(symbol=stock_code_front, + indicator="按报告期") # 同花顺-财务指标-主要指标 + stock_financial_abstract_df = ak.stock_financial_abstract(symbol=stock_code_front) # 新浪财经-财务报表-关键指标 + stock_individual_spot_xq_df = ak.stock_individual_spot_xq(symbol=stock_code_xq) # 雪球-行情中心-个股 + stock_zh_valuation_baidu_df = ak.stock_zh_valuation_baidu(symbol=stock_code_front, indicator="市现率", + period="近一年") # 百度股市通-A 股-财务报表-估值数据 + stock_fhps_detail_em_df = ak.stock_fhps_detail_em(symbol=stock_code_front) # 东方财富网-数据中心-分红送配-分红送配详情 + + # 查询数据库中是否已有该股票的数据 + existing_record = await wance_data_stock.WanceDataStock.filter(stock_code=stock_code).first() + + if existing_record is None: + print(f"未找到股票记录: {stock_code}") + continue + + # 处理并插入数据到数据库 + last_indicator_row = stock_a_indicator_lg_df.iloc[-1] + last_abstract_row = stock_financial_abstract_ths_df.iloc[0] + last_financial_row = stock_financial_abstract_df.iloc[:, 2] + last_spot_xq_row = stock_individual_spot_xq_df.iloc[:, 1] + last_baidu_df_row = stock_zh_valuation_baidu_df.iloc[-1] + last_detail_em_row = stock_fhps_detail_em_df.iloc[-1] + + # 更新字段的对应数据 + # 每股指标模块 + existing_record.financial_dividend = safe_get_value(last_detail_em_row.get('现金分红-现金分红比例')) + existing_record.financial_ex_gratia = safe_get_value(last_spot_xq_row[8]) + existing_record.financial_cash_flow = safe_get_value(last_financial_row[10]) + existing_record.financial_asset_value = safe_get_value(last_financial_row[9]) + existing_record.financial_reserve_per = safe_get_value(last_abstract_row.get('每股资本公积金')) + existing_record.financial_undistributed_profit = safe_get_value(last_abstract_row.get('每股未分配利润')) + + # 盈利能力模块 + existing_record.profit_asset_value = safe_get_value(last_financial_row.iloc[33]) + existing_record.profit_sale_ratio = safe_get_value(last_financial_row.iloc[43]) + existing_record.profit_gross_rate = safe_get_value(last_financial_row.iloc[44]) + existing_record.profit_business_increase = safe_get_value(last_financial_row.iloc[53]) + existing_record.profit_dividend_rate = safe_get_value(last_spot_xq_row.iloc[26]) + + # 成长能力 + existing_record.growth_Income_rate = percent_to_float( + safe_get_value(last_abstract_row.get('营业总收入同比增长率', 0.0))) + existing_record.growth_growth_rate = safe_get_value(last_financial_row.iloc[46]) + existing_record.growth_nonnet_profit = percent_to_float( + safe_get_value(last_abstract_row.get('扣非净利润同比增长率', 0.0))) + existing_record.growth_attributable_rate = safe_get_value(last_financial_row.iloc[54]) + + # 估值指标 + existing_record.valuation_PEGTTM_ratio = safe_get_value(last_indicator_row.get('pe_ttm')) + existing_record.valuation_PEG_percentile = safe_get_value(last_indicator_row.get('pe')) + existing_record.valuation_PB_TTM = safe_get_value(last_indicator_row.get('ps')) + existing_record.valuation_PB_percentile = safe_get_value(last_indicator_row.get('pb')) + existing_record.valuation_PTS_TTM = safe_get_value(last_indicator_row.get('dv_ratio')) + existing_record.valuation_PTS_percentile = safe_get_value(last_indicator_row.get('ps_ttm')) + existing_record.valuation_market_TTM = safe_get_value(last_baidu_df_row[-1]) + existing_record.valuation_market_percentile = safe_get_value(1 / last_baidu_df_row[-1] * 100) if \ + last_baidu_df_row[-1] != 0 else 0 + + # 行情指标 + existing_record.market_indicator = safe_get_value(match.values[0]) if not match.empty else 0 + + # 保存更改 + await existing_record.save() + + print(f"更新股票指标成功!: {stock_code}") + + except Exception as e: + print(f"处理股票 {stock_code} 时发生错误: {e}") + continue # 继续处理下一个股票 + + +async def init_stock_pool(incremental: bool = False): + """ + 初始化股票池参数,包括股票名、股票代码、股票上市时间、股票板块等信息。 + @param incremental: 是否执行增量下载 + @type incremental: bool + """ + await init_tortoise() + + # 获取所有现有股票代码 + existing_stocks = set() + if incremental: + existing_stocks = {stock.stock_code for stock in await wance_data_stock.WanceDataStock.all()} + + # 初始化股票池 + tick_result = xtdata.get_full_tick(['SH', 'SZ']) + stocks_to_create = [] # 使用一个列表批量创建 + for key in tick_result.keys(): + if incremental and key in existing_stocks: + print(f"股票代码 {key} 已经存在,跳过...\n") + continue + + detail_result = xtdata.get_instrument_detail(key, False) + InstrumentName_result = detail_result.get("InstrumentName") + start_time = detail_result.get("OpenDate") or detail_result.get("CreateDate") + end_time = datetime.now().strftime('%Y%m%d') + time_expire = detail_result.get("ExpireDate") + type_dict = xtdata.get_instrument_type(key) + type_list = [] + if type_dict: + for i in type_dict.keys(): + type_list.append(i) + + # 只在 type_list 包含 "stock" 时继续执行 + if "stock" in type_list: + # 检查股票是否已存在 + existing_stock = await wance_data_stock.WanceDataStock.filter(stock_code=key).first() + if not existing_stock: + stocks_to_create.append( + wance_data_stock.WanceDataStock( + stock_code=key, + stock_name=InstrumentName_result, + stock_sector=[], # 初始化为空列表,后续会加入板块 + stock_type=type_list, + time_start=start_time, + time_end=end_time, + time_expire=time_expire, + market_sector=key.rsplit('.', 1)[-1] + ) + ) + print(f"加载成功 股票名称 {InstrumentName_result} 股票代码 {key} 股票类型 {type_list} \n") + else: + print(f"股票代码 {key} 已经存在,跳过...\n") + else: + print(f"跳过非股票类型:{key} 类型:{type_list} \n") + + # 如果有新的股票,批量创建所有新股票记录 + if stocks_to_create: + bulk_db_result = await wance_data_stock.WanceDataStock.bulk_create(stocks_to_create) + print(bulk_db_result, "股票池创建完成 \n") + else: + print("没有新的股票需要创建 \n") + + # 获取并更新sector模块 + sector_list = xtdata.get_sector_list() + + for sector in sector_list: + # 使用模糊匹配找到最佳的中文板块匹配 + best_match = get_best_match(sector, translation_dict) + if best_match: + translated_sector = translation_dict[best_match] # 获取对应的英文名称 + else: + print(f"没有找到合适的板块匹配:{sector} \n") + continue # 如果没有找到匹配,跳过该板块 + + # 获取板块对应的股票列表 + sector_stock = xtdata.get_stock_list_in_sector(sector) + # 获取所有相关股票 + stocks_to_update = await wance_data_stock.WanceDataStock.filter(stock_code__in=sector_stock) + + # 遍历并更新每个股票的sector,避免重复添加相同的英文板块 + for stock in stocks_to_update: + if translated_sector not in stock.stock_sector: # 检查是否已经存在该英文板块 + stock.stock_sector.append(translated_sector) + await stock.save() # 保存更新后的数据 + else: + print(f"{stock.stock_code} 已经包含板块 {translated_sector}, 跳过重复添加 \n") + + print(f"更新板块完成 {sector}: {translated_sector} \n") + + print(f"所有股票已经加载完成 \n") + + + """ + # 初始化股票池参数 股票名、股票代码、股票上市时间、股票板块、股票最后回测时间、股票退市时间、股票所属市场 + @param incremental:是否执行增量下载 + @type incremental:bool + @return: + @rtype: + """ + + +""" +async def init_stock_pool(incremental: bool = False): + # 这行代码存在一个问题就是,存入的板块信息,是中文的,在查询的时候应为是中文的数据所以没有办法被选中 + + await init_tortoise() + + # 获取所有现有股票代码 + existing_stocks = set() + if incremental: + existing_stocks = {stock.stock_code for stock in await wance_data_stock.WanceDataStock.all()} + + # 初始化股票池 + tick_result = xtdata.get_full_tick(['SH', 'SZ']) + stocks_to_create = [] # 使用一个列表批量创建 + for key in tick_result.keys(): + # 如果是增量更新,且该股票已经存在,则跳过 + if incremental and key in existing_stocks: + continue + + detail_result = xtdata.get_instrument_detail(key, False) + InstrumentName_result = detail_result.get("InstrumentName") + start_time = detail_result.get("OpenDate") or detail_result.get("CreateDate") + end_time = datetime.now().strftime('%Y%m%d') + time_expire = detail_result.get("ExporeDate") + type_dict = xtdata.get_instrument_type(key) + type_list = [] + if type_dict: + for i in type_dict.keys(): + type_list.append(i) + + stocks_to_create.append( + wance_data_stock.WanceDataStock( + stock_code=key, + stock_name=InstrumentName_result, + stock_sector=[], + stock_type=type_list, + time_start=start_time, + time_end=end_time, + time_expire=time_expire, + market_sector=key.rsplit('.', 1)[-1] + ) + ) + print(f"加载成功 股票名称 {InstrumentName_result} 股票代码 {key} 股票类型 {type_list} \n") + + # 如果有新的股票,批量创建所有新股票记录 + if stocks_to_create: + bulk_db_result = await wance_data_stock.WanceDataStock.bulk_create(stocks_to_create) + print(bulk_db_result, "股票池创建完成 \n") + else: + print("没有新的股票需要创建 \n") + + # 获取并更新sector模块 + sector_list = xtdata.get_sector_list() + if incremental: + # 获取已经更新过的sector + updated_sectors = set() + # 获取所有已经存在的股票及其相关板块 + existing_stock_sectors = await wance_data_stock.WanceDataStock.all().values('stock_code', 'stock_sector') + for stock_info in existing_stock_sectors: + for sector in stock_info['stock_sector']: + updated_sectors.add(sector) + + # 过滤出没有更新过的sector + sector_list = [sector for sector in sector_list if sector not in updated_sectors] + + for sector in sector_list: + sector_stock = xtdata.get_stock_list_in_sector(sector) + # 获取所有相关股票 + stocks_to_update = await wance_data_stock.WanceDataStock.filter(stock_code__in=sector_stock) + + # 遍历并更新每个股票的sector,避免重复添加 + for stock in stocks_to_update: + if sector not in stock.stock_sector: + stock.stock_sector.append(sector) + await stock.save() # 保存更新后的数据 + print(f"更新板块完成 {stock.stock_code}: {stock.stock_sector} \n") + + print(f"所有股票已经加载完成 \n") +""" + +""" +# 每次都会操作数据库 +async def init_stock_pool(): + await init_tortoise() + + # 初始化股票池 + tick_result = xtdata.get_full_tick(['SH', 'SZ']) + tick_keys = list(tick_result.keys()) + for key in tick_keys: + detail_result = xtdata.get_instrument_detail(key, False) + InstrumentName_result = detail_result.get("InstrumentName") + type_result = xtdata.get_instrument_type(key) + # 创建新的股票记录 + await wance_data_stock.WanceDataStock.create( + stock_code=key, + stock_name=InstrumentName_result, + stock_sector="", + stock_type=type_result + ) + + # 获取并更新sector模块 + sector_list = xtdata.get_sector_list() + for sector in sector_list: + sector_stock = xtdata.get_stock_list_in_sector(sector) + for stock in sector_stock: + # 更新已有股票的sector + await wance_data_stock.WanceDataStock.filter(stock_code=stock).update(stock_sector=sector) +""" + +if __name__ == '__main__': + # processing_data(field_list=[], + # stock_list=["000062.SZ","600611.SH"], + # period="1d", + # start_time="20240506", + # end_time="", + # count=-1, + # dividend_type="none", + # fill_data=False, + # ) + # history_data_processing() + asyncio.run(init_stock_pool(False)) + asyncio.run(init_indicator()) +# xtdata.run() diff --git a/src/data_processing/response_factor.py b/src/data_processing/response_factor.py new file mode 100644 index 0000000..6f6f000 --- /dev/null +++ b/src/data_processing/response_factor.py @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/src/exceptions.py b/src/exceptions.py new file mode 100644 index 0000000..0e45d3f --- /dev/null +++ b/src/exceptions.py @@ -0,0 +1,92 @@ +from typing import Any + +from fastapi import FastAPI, HTTPException, Request, status +from fastapi.encoders import jsonable_encoder +from fastapi.exceptions import RequestValidationError +from fastapi.responses import JSONResponse +from starlette.exceptions import HTTPException as StarletteHTTPException + +from src.utils.helpers import first + + +def register_exception_handler(app: FastAPI): + @app.exception_handler(RequestValidationError) + async def validation_exception_handler(request: Request, exc: RequestValidationError): + """请求参数验证错误处理""" + error = first(exc.errors()) + field = error.get('loc')[1] or '' + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content=jsonable_encoder({ + "status_code": status.HTTP_422_UNPROCESSABLE_ENTITY, + "message": "{} param {}".format(field, error.get('msg')), + "detail": exc.errors()}), + ) + + @app.exception_handler(StarletteHTTPException) + async def http_exception_handler(request: Request, exc: StarletteHTTPException): + """Http 异常处理""" + return JSONResponse( + status_code=exc.status_code, + content=jsonable_encoder({ + "status_code": exc.status_code, + "message": exc.detail}), + ) + + @app.exception_handler(Exception) + async def exception_callback(request: Request, exc: Exception): + + """其他异常处理,遇到其他问题再自定义异常""" + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content=jsonable_encoder({ + "status_code": status.HTTP_500_INTERNAL_SERVER_ERROR, + "message": "Internal Server Error", + # "detail": ''.join(exc.args) + }), + ) + + +class CommonHttpException(HTTPException): + def __init__(self, detail, status_code, **kwargs: dict[str, Any]) -> None: + super().__init__(status_code=status_code, detail=detail, **kwargs) + + +class DetailedHTTPException(HTTPException): + STATUS_CODE = status.HTTP_500_INTERNAL_SERVER_ERROR + DETAIL = "Server error" + + def __init__(self, **kwargs: dict[str, Any]) -> None: + super().__init__(status_code=self.STATUS_CODE, detail=self.DETAIL, **kwargs) + + +class PermissionDenied(DetailedHTTPException): + STATUS_CODE = status.HTTP_403_FORBIDDEN + DETAIL = "Permission denied" + + +class NotFound(DetailedHTTPException): + STATUS_CODE = status.HTTP_404_NOT_FOUND + + +class BadRequest(DetailedHTTPException): + STATUS_CODE = status.HTTP_400_BAD_REQUEST + DETAIL = "Bad Request" + + +class UnprocessableEntity(DetailedHTTPException): + STATUS_CODE = status.HTTP_422_UNPROCESSABLE_ENTITY + DETAIL = "Unprocessable entity" + + +class NotAuthenticated(DetailedHTTPException): + STATUS_CODE = status.HTTP_401_UNAUTHORIZED + DETAIL = "User not authenticated" + + +class WxResponseError(DetailedHTTPException): + STATUS_CODE = status.HTTP_400_BAD_REQUEST + DETAIL = "请求微信异常" + + def __init__(self) -> None: + super().__init__(headers={"WWW-Authenticate": "Bearer"}) diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..9f9cac5 --- /dev/null +++ b/src/main.py @@ -0,0 +1,52 @@ +import sentry_sdk +import uvicorn +from fastapi import FastAPI +from starlette.middleware.cors import CORSMiddleware + +from src.exceptions import register_exception_handler +from src.tortoises import register_tortoise_orm +from src.xtdata.router import router as xtdata_router +from src.backtest.router import router as backtest_router + +from xtquant import xtdata +from src.settings.config import app_configs, settings + + + + +app = FastAPI(**app_configs) + +register_tortoise_orm(app) + +register_exception_handler(app) + +app.include_router(xtdata_router, prefix="/getwancedata", tags=["盘口数据"]) +app.include_router(backtest_router, prefix="/backtest", tags=["盘口数据"]) + + +if settings.ENVIRONMENT.is_deployed: + sentry_sdk.init( + dsn=settings.SENTRY_DSN, + environment=settings.ENVIRONMENT, + ) + +app.add_middleware( + CORSMiddleware, + allow_origins=settings.CORS_ORIGINS, + allow_origin_regex=settings.CORS_ORIGINS_REGEX, + allow_credentials=True, + allow_methods=("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"), + allow_headers=settings.CORS_HEADERS, +) + + + + +@app.get("/") +async def root(): + return {"message": "Hello, FastAPI!"} + + + +if __name__ == "__main__": + uvicorn.run('src.main:app', host="0.0.0.0", port=8011, reload=True) diff --git a/src/models/__init__.py b/src/models/__init__.py new file mode 100644 index 0000000..508eccf --- /dev/null +++ b/src/models/__init__.py @@ -0,0 +1,292 @@ +from datetime import datetime +from decimal import Decimal +from enum import Enum, IntEnum +from typing import Optional + +from pydantic import BaseModel + +from src.settings.config import settings + + +def with_table_name(table_name: str): + return f"{settings.DATABASE_PREFIX}{table_name}" + + +class Decimals: + MAX_DIGITS = 15 + DECIMAL_PLACES = 3 + DECIMAL_PLACES_MORE = 6 + + +class SecurityPriceCode(Enum): + TRANSFER_FEE = "fee_1" # 过户费 + + STAMP_TAX = "fee_2" # 印花税 + + TOTAL_COMMISSION = "csf_fee" # 总证券佣金 + + +class UserSecurityExtraReminded(BaseModel): + """ + reminded_target_buy_price: 最近提醒目标买入价 + reminded_target_buy_time: 目标买入价最近提醒时间 + reminded_buy_price: 最近提醒买入价 + reminded_buy_fluctuation: 最近提醒买入波动 + reminded_buy_time: 买入最近提醒时间 + reminded_sell_price: 最近提醒目标卖出价 + reminded_sell_fluctuation: 最近提醒卖出波动 + reminded_sell_time: 卖出最近提醒时间 + buy_up_first_interval: 买入上涨初次提醒波动间隔 + buy_up_continue_interval: 买入继续上涨提醒波动间隔 + buy_down_first_interval: 买入下跌初次提醒波动间隔 + buy_down_continue_interval: 买入继续下跌提醒波动间隔 + sell_down_first_interval: 卖出下跌初次提醒波动间隔 + sell_down_continue_interval: 卖出继续下跌提醒波动间隔 + browse_msg_alarm_flag: 浏览器消息提醒通知设置 + wx_msg_alarm_flag: 微信公众号消息提醒通知设置 + """ + reminded_target_buy_price: int = 0 + reminded_target_buy_time: Optional[datetime] = None + reminded_buy_price: Decimal = 0.000 + reminded_buy_fluctuation: int = 0 + reminded_buy_time: Optional[datetime] = None + reminded_sell_price: Decimal = 0.000 + reminded_sell_fluctuation: int = 0 + reminded_sell_time: Optional[datetime] = None + buy_up_first_interval: int = 1 + buy_up_continue_interval: int = 1 + buy_down_first_interval: int = -1 + buy_down_continue_interval: int = -1 + sell_down_first_interval: int = -1 + sell_down_continue_interval: int = -1 + browse_msg_alarm_flag: bool = True + wx_msg_alarm_flag: bool = True + + class Config: + json_encoders = {Decimal: str} + + +class Status(IntEnum): + """ + 状态 + 0-正常 + 1-删除 + 2-禁用 + 3-退市 + """ + # 正常 + NORMAL = 0 + STOP = 1 + DISABLED = 2 + DELISTED = 3 + + +class StockType(str, Enum): + """ + 股票类型 + """ + SH = "sh" + SZ = "sz" + + +class UserSecurityAccountType(IntEnum): + """ + 用户中国证券账户账户类型 + """ + + NORMAL = 0 # 普通账户 + + MARGIN = 1 # 融资账户 + + +class ExchangeType(str, Enum): + """ + 交易类型 + """ + SH = "SH" + SZ = "SZ" + NQ = "NQ" + BJ = "BJ" + + +class SellReferType(IntEnum): + """ + 参考卖出类型 + highest-最高卖出价 + latest-最近卖出价 + """ + HIGHEST = 0 # 最高卖出价 + LATEST = 1 # 最近卖出价 + + +class InvestmentSource(IntEnum): + """ + 投资资金来源 + """ + CASH = 0 # 现金 + FINANCING = 1 # 场内融资 + LOANED = 2 # 场外借贷 + + +class SecuritySource(IntEnum): + """ + 证券获得方式 + """ + BUY = 0 # 正常买入 + BONUS_SHARES = 1 # 送股 + CONVERT_SHARES = 2 # 转增股 + + +class StatusSell(IntEnum): + """ + 售出状态 + """ + + UNSOLD = 0 # 未售出 + PARTIAL_SOLD = 1 # 部分售出 + ALL_SOLD = 2 # 全部售出 + + +class BuyAndSellType(IntEnum): + """ + 卖出卖出记录类型 + """ + COMMISSION = 0 # 委托单 + CONDITION = 1 # 条件单 + CONTRACT = 2 # 成交单 + + +class SecurityType(IntEnum): + """ + 证券类型 + """ + + security_stock_a = 0 # A 股股票 + + security_stock_b = 1 # B 股股票 + + security_etf = 2 # 场内基金-ETF + + security_lof = 3 # 场内基金-LOF + + +class PushConfigModel(BaseModel): + """ + 驼峰转下线 + """ + device_id: int = None + device_name: str = None + eable_flag: str = None + update_at: Optional[datetime] = None + + +class CashFlowType(IntEnum): + INTO = 0 # 转入 + OUT = 1 # 转出 + + +class AdminStatus(str, Enum): + """ + 管理员账户状态 + """ + + DELETE = "delete" # 删除 + NORMAL = "normal" # 正常 + FORBIDDEN = "forbidden" # 禁用 + UNAUTHORIZED = "unauthorized" # 未验证 + + +class CustomerSecurityAccountSecurityAdminStatus(str, Enum): + """ + 客户的证券账户中的券商状态 + """ + JOIN_GROUP = "join_group" # 新入群 + ACCOUNT_READY_NOT_DEPOSIT = "account_ready_not_deposit" # 已开户但未入金 + ACCOUNT_SUBSTANDARD = "account_substandard" # 非有效户(20个交易日里平均市值1万) + ACCOUNT_UNSETTLED = "account_unsettled" # 待结算 + ACCOUNT_SETTLED = "account_settled" # 已结算 + + +class CustomerSecurityAccountVipStatus(str, Enum): + """ + 客户的证券账户中的vip状态 + """ + REWARDED = "rewarded" # 已奖励 + UNREWARD = "unreward" # 未奖励 + + +class UserBenefitStatus(IntEnum): + """ + 用户福利领取状态 + """ + UNRECEIVED = 0 # 未领取 + RECEIVED = 1 # 已领取 + + +class SecurityAdminTransferStatus(IntEnum): + """ + 券商转账的状态 + """ + PAYED = 1 # 已支付 + CONFIRMED = 2 # 已确认 + DISAGREED = 3 # 有异议 + + +class YuanMaTalkQuestionPublicStatus(IntEnum): + """ + 问题是否公开 + """ + PUBLIC = 1 # 公开 + PRIVATE = 0 # 私密 + + +class YuanMaTalkQuestionStatus(str, Enum): + """ + 问题状态 + """ + NEW = "new" # 新问题 + REPLIED = "replied" # 已回复 + WAITING = "waiting" # 待回复 + CLOSED = "closed" # 关闭 + DELETED = "deleted" # 删除 + FORBIDDEN = "forbidden" # 禁止 + + +class LastReplySourceType(str, Enum): + """ + 问题状态 + """ + ADMIN = "admin" # 管理后台 + USER = "user" # 用户端 + + +class AccountType(IntEnum): + """ + 账户类型 + """ + LIANGRONG = 1 # 两融账户 + ORDINARY = 0 # 普通用户 + + +class supervisionFee(IntEnum): + """ + 深市是否申请监管费 + """ + NOT_CHARGE = 1 # 不收 + CHARGE = 0 # 收 + + +class transferFee(IntEnum): + """ + 账户类型 + """ + NOT_CHARGE = 1 # 不收 + CHARGE = 0 # 收 + + +class IsType(IntEnum): + """ + 是否默认 + """ + YES = 1 # 是 + NO = 0 # 否 diff --git a/src/models/__pycache__/__init__.cpython-311.pyc b/src/models/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c0f0082b4c28670ceba057c2b05393c2267f34c6 GIT binary patch literal 12289 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 literal 0 HcmV?d00001 diff --git a/src/models/__pycache__/back_position.cpython-311.pyc b/src/models/__pycache__/back_position.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..83fae245932f5012daa404b3b274562f909a907e GIT binary patch literal 1174 zcmZuw&1(}u6rb6TWV3B-T5B`~DOxFpLZ}Bz5%G`~#X^fv53&S?ZDyOU-E8XYhBgPG z2M;|6Rxj;E?5QgC(7!|y$RUhJQRpot^&$wq*-f9gdt*1kT zdL~OEpcn&+@iX7*R0Dzu-k&#%t4}JP?b{XiDbSGCR*y1}5?JeHbAvdRTk==9;#M6e zi&;OoA_RLq>)ZlQlpw`t%2B8Uo65fjPj8x=w|tW)m!J-^)-7Zqt8$G5oh5|pgp@0^ z3auu*pO6<-(-A!62gAryBZH zQ=i(LX=NsM)6LAp;cz`O-N;NgGt+e?-PW*rr;X0Tv9O(7;0P-dMJ;4CuK5+_S8UH> z;);Yl(4pnIu;t;|UH)&#dENhq2{s|ODBs`O{qP<@ c_2_J&+x6&d$21I7&%^fJV_`kHz!i}F3tKxP4gdfE literal 0 HcmV?d00001 diff --git a/src/models/__pycache__/back_result_indicator.cpython-311.pyc b/src/models/__pycache__/back_result_indicator.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66b76badc38b4a62fd33ef0a22235dc8e7c93d37 GIT binary patch literal 1127 zcmZuw&rj4q6rSm?Z3~1Ngdhhopd`J(4bd15Ks1oRK{=SsCX=#LmP)sa({6yo!yY_v zFcFOjT#TL&g9rZ-vzzQ;GgqStw zDn{t1Fw&;B2d!Bc93YA)_E8aMaZ$=jSlE&;7nQ7nk%XQgD!)claq*Fi&JK6S&gA1wyd%R5ntt%4|NYxDFLL|Q( zrBf%xS6C@jg0tm|;Vx%SF1wM#2iJfVVKFV@rMy;bmFpzVF(KR}q*$V5NIK!YguE;} zzQ{fyOk6ux;|uwUT410*O8^TGbI&~|C@cZ&c%`6_`}O_p>BslKKfTLsJ3-zh)QOy2 z$nrS|?D}D@D~rjkU9Je0tPg_rLJw~PR4$)}HM-wly>q{2&D5=#JyksHCb+O6fEBxk z^$Wuia6^Eo1FkOy(PL3kT=FP4<~JOcnDVB(%X{X$z+t<|jB%NfI3gPmLE-}j#VC`4 zsGjf@z<=C~;k}_ZqlXJsW4vaJ*NySYT*DmMo2Z*3pVy9~sySUVr|af)WxipJ?Q3;w z>{zKzd7;Zh!6eE}$oOQE@%ZWz z0iB{Q^he2M&$3b2DE^aB)L&D^7&p*ZwcFc36V=YQsb?_IzYd4@&cy6_m!(+1Kg^F6 A761SM literal 0 HcmV?d00001 diff --git a/src/models/__pycache__/back_trand_info.cpython-311.pyc b/src/models/__pycache__/back_trand_info.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..621c2576d48ba76f1c8ec02c5b83bdd9234ca1cf GIT binary patch literal 1104 zcmZuw&rcIU6rR~1?e0R;R*@(NG1iduLJ$)U#&F<=1`@c)!EBmLmd*ex+bzy60TK^A zc;H}S^@10pC&b{vzr?gj56#?+(a5c&c;V!m-ImzG?9R9I=Dm6IeQ#zzr&1Pz_UFg< zwRw!tPib_U-Wjx~VQ_#bqS!+vY~zw*D_Gi!r`7OnQUG&RQn!ydKFUK z$48?I?5dt$>)CaDiG_|xuL2ViN-D$!F3Wjc7$mAFA;Kc0RHoZdVj>0!d9m$yvetxf zdB{SK%Krtm!9aiK0OlSRp1F>{z6h}Emi_g@uWxToKD_(=@oizp@r#U5Cv*w{FBTv% z^MXR({^MJFLX*sR3epWi4{ifgE}mv)w3@EnxnIvrHZqg@x_tN$gz#a2<@^9oOCuv8 zD?tMg#)2O{mRCn8E*0kNhQni1-DG=WV8-iyMmBde zRLf4(vlET%L}js+!1{w0x(ugs?(7OjOiXfG&L@NsmU&oq1IFe3h^1Rl?D4qR(W%7( zdPSWZkK%Kl=A*Ds{t$t@ZY>pK+(dV4SG`R%R_lFRMiK-4%TS#^m9uA8xFP}n0D{8` A=>Px# literal 0 HcmV?d00001 diff --git a/src/models/__pycache__/backtest.cpython-311.pyc b/src/models/__pycache__/backtest.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5ea8bc4ee41ca0edefd55cc465bbc1b4be9ef92e GIT binary patch literal 2142 zcmZuyO>7fK6rT0ZdcBE%)-fSKfu@AC7FDE5sVY^p0^)`eaD+-CRZhfe@l3*&^*XF~ zOVUb}k$SLNB+?2As{TcyN>gE#94JlILpkm)|Dawk#NB@6q8KP6}E!7Kl~Q6`G{kw5tQdJ$_K__aT5FpHsTG;)dGuPaSuLk z%k+u2>$oBW8?+UM}0jJ_xV#qIxv46I5%RaFUAqf!6Ve7EpcM4 zxnZ%^u^520;PXa4?%E`L9^!P?Vl6gF*GAg5iPsV>drY|&UE3BMPS&KBDCxG9QnjuY zn>}qd>9&l!+t%9Cw$@%365Rlay)7&B;2r?PJ}R723Rg^0Hpigkftf3xB#I0gA(&h3NCNd!^x`r-VXs1CmzzX)bC>TNvViFowbxdZd;QOto;cP5c)YWf) zX#V!)6*A|BQf6h1=+ow9%3Hd6)gne5Y-k52<$|Hku(d5#XwPfy)>5Kz;iScrh@DqyDU>tmZ2yX^sr1jW%;V5XsnrK*czSYm`R4lfUpIgJYVw+*7l@1%Q<*Fqg-H(B{a=mj#G*=Ou$%mGGTVuZCl70&49 zFuRjzK*cmMTr!9{t)C@xo-i_W^1{%`?0W|5Q<~aQf56}e`5Wwdp$S=5biHJ{qB5LC zbm+2Tcp-v-I2y6a#E@ZySQb!gszu@{JN(_ribbk9jt z%d%nVx~fl8(VBq+gFXO%(Q5&B9t9hvV5%Pzcl*X6wqy4+US=~^Ntdwn%Q?6+ch0eb zoM=zA`^Mor*?P3M5$(03y_HkW;K=f@JvicY^*MWv{gdjsHQ>Zk8y)zp;V%Fz*u3!z_+E4pjSXasR@l<@Czf-=EgSiH0~~ixZV0Cw{Oxb)&Q} zxO}l5&o<&&JD#nac2fQG{dTH>-bs7yB2d9B8D>cZ1h5|T za3=!72Rwv(tT%NTR=8~9LG86mHLZb2ldx? czYcn>zWd$ayExctJE>YvnDprjRPJW~2Y-xB7XSbN literal 0 HcmV?d00001 diff --git a/src/models/__pycache__/order.cpython-311.pyc b/src/models/__pycache__/order.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d6eb0c3f27be9e977b908d1e918270402e188526 GIT binary patch literal 4680 zcmbVPU2GKB6`uX|?s&a_v-SevM_{nCK({JsX&q5&JI29|air9Z(8_c)>?X#u0Gh$A)0sxkz)PNb&Yk`7&X_dK z?9AD@=bn4+x#!;deRpp3^so$EfB4P6&wK@M|G`4@@-$u^>hN-f5gE~uVX}^t!-4P4 zj5F&>xw7t*o0eS}Pu83A(sy^JE9*=7vi_9c!8n=c8PW4PBYGvrbr-{Y29J5ASSKTS zCHA@lwD2^v-Lyp)wD8$l1ZazZr9}^I;fEHitwoTw2wGZ%#O_}+iNG3mnQ$1M6M0d} zB%Fr#j4Wlug5k}gq5xHWbC*ORr^soZs`xaK1SLFv(a=A4qwuf_qrAdMOv)iLDJMkcI^V2=?268+>$($i0grj5Jk-Y9EQ@Z@ zQ}tSGx@acN( zL7NTE*~VBxVz?Tyj53Mz<&x(nOw9fR`{MS>M{iP3%eUWK{^s&HjrZRC;%fQp2@%3@=Zcw(&HI&Ws~>y} zz4n2-juqS7qYw8&P3MXg9hMxrMJHs+^({*?l{_ISg5k|$)(X=%J%*2`yMyNqmd8C* zgzdr`0iOR|QOMxF;d#;py%-+4m2NT*p};(T7R1=IvnORCcXkrQMM(15*_EZY*FJoA z_2YMDUlnp`i5CS$m@Sa>thrxjX}8I_ONJZ6Altx6FFb`EAWE%YJ-n+DTJUM%gdR?m zJ@_Mgz=GgtrxU#d%?ZvZ*@eOl!thPxlyO7{!zqh~|G9GlF^gu`r2+}BV{D3RUB_m*#g)D-H!I~unK*aFP^val6jCfGmU#<9?G_Tw8OKNYyN3rT>c4H@!pDfhj# z>+0CzdDTCp`G<7>P-(Ot*gZ| zuDO0x_b1T9{hENj(HC3ow)AI0`bHZBb!&en zs0Uk71*;(oNSJWl9$VOE5vC(+`*4U6fOO6hvkB>KKMpZw%Td&pqh8y%0QiAo5Ju?* z`RTLy>9s@@x5eB?E#fUN0r0b>y44c%7HYM%W#y=Dvt(x*rJ<*QhPGSs|CG1`z;&n5 zby9aw7Unxm0Mq#R7W{%^}Wl>-+VwYuY_y4yZZS`<@)_kt^oxnTt=5D zoe_!|#qh{EQMyRIku6}{u^C~()9e4?u|Ol$l*!0pXg^K1gS6u3Uex^(#82Salwtf@ zc)uRrZ)ce8n=uU8hYklo7=bgzoX892agq$8DqN7#MFM0X6?h>d3k4$pgmf%d%u*sC zxGao7qY01fL7ql`2^9NL96*a;lP{p*X^9xDC<&PcFTr^R-pvYZb+=R|e0FK88E3fGB!KFjBu{$|6eoBj<(&ML8cdX70Ai@tU{Js{ub5!M08kf?! z)RS4IMQlsbotaccFjf`uNaXm5)-L;sh22oFH+@#J;Le#N+m2o|`Zz-w9Mzq+7 z9vdm0xEt=Tc=d4q;xRhLa7qiO^l+*)aku5fLT?9<-mC)Y?Euo7wZfz~oU4u<)r8)M zb5QdG=&k*A@Puy}_*$gjeik6~iGBb=*61D;(iaq|pvbwiGvw#Eln?|>xTBc#*N;}e zeDi{Ii2_$g>^}qz!W;TC&$hnPjR?L27N#Jiqws7B3#=ACpob6GVZs01uwVj#3x~GZ zwwlB=punSZK?d^2RZaTp; z2t_%1W;J?V#4n7Oq{3W22S;OEr$(rO+$}9eWN;g1uvOCuN#FX8m|rGG$;;3L5Q!=D zgIsqx9F97(TkX8-OuxF>U1yG{o%dbl8FjOJ-5YekY11ys#~z^eQM+mn6B$&)19i9m zfy)_jK4L(Wde+&P19q%kl%)r#eblZ>GF(E9K3#Wrg98p7aL!U_o%J|i8ttO8?*VEb cwX4ER{6}iv!MZyH4&vw_?o=7hyy?{c7upk{c>n+a literal 0 HcmV?d00001 diff --git a/src/models/__pycache__/position.cpython-311.pyc b/src/models/__pycache__/position.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..75caafd266b19bb23c035bd58c2b3c635bf5cef7 GIT binary patch literal 2693 zcmb7GU2GIp6uvXFvpYZgS6V^a!d6Nu%TLn=Ch90>hRlwx65g_V8~t|1x8R1C$b5EZ9GG(~he#>5>V zhb1#HHtq~LDMX_aNOoL6GOJKC3_^48*d^qWoflAulU;zk%=MulzeJjXw8@@K%1X?1 z4n>uitOhC5Hx|`K!kRP~Q^E-;u0R#nr(sD`hR;P-9a=A#wpCV+P-L143Q;l&(KJ$+ zGsQNTx=hbAGc-&G9=n8C%f?yMWrxhpJ1cBlmJL_3am{lxFwPu2b_wxSUid06Ld9%^ zDlejC;VycW#VQL=g@wDy!dqeCsj~1{!SO5RHIrnnRq=#KFlBA z-(C9j8;B1*de#(WMUCL-C~02W2CXR)TDx6{OXJ~~k{H%TFrk#yd-2*=i=W>NQfoB} zS1$c|D`z)e%`Lru*&gNYcZ<_Mq{!+QzPz+}la$`0sI z9&wqfh>k<1VO`8)PjOwXClyUHxzW{S_6Av#3tJ2ehfN_Ijwj_5P%dn`!{N75QjBmf z9L8ibn5;#|S+oKQ`uhcd?n48oqEce`6@YR0EW-l}xyws8K3=@}(ZHCLh$vxM(xd?u zM+U&L5>p3?1GkQzGaY0u*b9FA@Tgk>rb<`AAGqS17V`exhJSa4B^Q1QEbtb9-e3(T zeQ*tdO#}$nP44A{)8F=S;pEwvVklU|x#pAgePSYV^Ef3O%u; zwAz^+#nFgjx+M9`prk6Ma}+0sqM9`{t|OyaGuccevNtRsd{O@bShoiL>CEPff$QDb z5uHDn=MNhE!Ks%DLVc#i5bCD~bBZo>=7ml}=$txM5F0WbhS-qhbg?}zwi{ymRClp* z8t2A!@n~K=YKTWkb$zDe;)~Nz*OnLC46zOB+*>m}hI?yvr|#aDckeUY`=*W;yn)P+ z;SFSab?@Q4_psqTJawYr*_P=yJlnF*>z@62&wj(Rf2wEML9u(5Q8hdubq_aaus;G> ze}o4p77+wO(UqS8t|1<2rDTCn8l|Z;oo3RGG@Ev&U1@Hfnt|SPB@YvPnx9_Rqv#f) z84%}6DuvQ~A8JM#3H)l=f$**gL30UXtOd4|!2^6l{%xFtkjKw}drZ9I1_Fe(n4|+7An+`K?F5#z}mr zDacx+0rE+2D<*$@Jfc{M#U$KIXeOg-I4J(Fv9J#YG$}UJYRjEE_*ZU?%|idl;3l z6vsi(RC2yIHCZvodx#AILQnM!00}q?0q6X`a*e-mjo07EbIk_VtaHr;-(KCf_eoJb zaJw&8GuuDkuX_)`o##3Xu0!WK{v)dO+t3$6*Cr+#=<+c4JS>R4LBZs-B-WBqRly{r zcJ3D7lxVe23H!LIU8`t?-C+ItV3K9+oJ)voudqBLKju2^L+l7{2NS{}^%H<)hN7qf v+OBVO7f_2{eizVAz5HHwiWFqXYRIG>klKf9O^q%JwnYi*oJ6e#D}(+6xp9C5 literal 0 HcmV?d00001 diff --git a/src/models/__pycache__/security_account.cpython-311.pyc b/src/models/__pycache__/security_account.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..48bb59871c982fb44c170a96e7513d4c75f6ea25 GIT binary patch literal 2013 zcma)7&1)M+6rWwKWJ#7R+p?7pH6H{!K_!^9aS|w@&{S4Qag@@SVy{h>jdpBrx>~8T zt0eBBsF1cgG{k8P4u#OP2gfCDqeDXl{s($o!5+k%46Xcevx$4jsc&{=JC<_jX!dE| zn>TOY{NBet4TpmW#=~B!RdZT0{IKe-i|LM=P+a#VdiOV3Vcb z8!WZASYlubH$$&k3bj}Yx9EghbUIpeI$CrhEjp1FolYn4oe=o0ru%d$-4Ng?ja^DcdS4Ex?ZZR|FN)I`qPn(6nwD#Pi1*y?srHFKL0swSFn*GYTkrc zZybf)RN){{_h|i_rTYC!efD1cxB1maFV>!x6CN7O%6B9U>r>`574(9pkq$UeV5qIX zxV!P)1NVgqo^*lewN1y5dg&S1bG01-9N75fu_LJ8f4(-i*bsEXVOQW1gH`h3pS z`*wM4`Jt<~FWYv?;$Q41(`eu-$!=k!p=$h+ zQ!J81gA&X60#uJgLz47GLDpCqNfKcRO}$QK{fb@!gZ>=?l6-GsRF(CqkAd6)sHZ0C z<@+0te^~wL!NhG@&tOTBO?ko~nF*(ujEQCk2J$mhV7`$k*kfTfSh$M2zgp+9*@$)g z-D>AqyYp5ucwHj0Hm|DEL678QoZ%6wV2P@aD=x{YU zY)6NS!(gble>P?J^p*Ka?_X)FXS~`oZug8AudIZQlu~x+$dkeHbt{ythLUzDSsbdx zPO(>-V#$Exu$J3eL!2W#P@rO)i}(I?5W zZiR=c;UPObR7`COoOf^&wUR9++kV4_3lhU8od{DuG-ETT8kn%UaHAZA3>$DW%t=+k z8UkS*mM}C4>!S3~&cT69PB)2q19H#p^n`ut9PH_Vrn>nQ8~L0LFWVojSN@A`rMyoD z;4J$@89xKr^l%(kLno{^%@s6i{lB?_E?WENrmvd=)LO|b-eS`2y{)%V{2lA?nVJyY R^6+tf8v(J<0e3oB{s${TKxzO0 literal 0 HcmV?d00001 diff --git a/src/models/__pycache__/snowball.cpython-311.pyc b/src/models/__pycache__/snowball.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..540cc205fd55d07827352e41de32f9374494361d GIT binary patch literal 1275 zcmZuw&ui2`6rTLtY_xXO?n)0*6|1aDuLV&LS`-EMpdu0$#xyf_W3$PcWNp1wtWZI) zN$2O=(S5ufyaiB8UhMDhq~C77bD0w&+M@*^r~TM-?&O&t>C1=c=fhb0`E{SL*h zS*Ea*tK1=`8`>5wb7yS%)C@h}YK4qm>*?PH^C_ap5C}3vA{Y{+A>T~)fd-ML<(ddd z!HAooMn0*;CJK0|)2S_lw1}(*WYb%|PZJ$TZDZPH*Bk0L9jDp_vGjd$Tb_KsG=6Vs za%SQF=O5o*FW&ta?Ods<;PhkW>+-}0sDxMI$SM`eO_s%L;}7;t6-pCCjt777q8i- zJ8%JD80ZZY7d|~&e(_}S<>TUz=~@&MGc=2VZxthzgJSaIp2{62^IZHL5T_kRuoqxt z{h7;jjkmwjXER3|nWJM0fBYIN{t)?g{S`%?lk={9$d)W1??`^4`?ww8d zHqyN#r{}WW<5wHm?oZj7zIuVwvvf8~8(BJ1Xv%_etcm`G6~4CipN%*vo<<>`W*Xd& z(6a;T^CJ)w)uFaMacyIfIuoZJ%X35D?uSap7tQA7%Q6rTOFyC| zZ{9cG%!6z;ji7w_`xjUL6cPHHDZQfXcXly!?jnq^;2~cy1Ya~nf$5?r`LZGNzT_!> z!btc@BPk#ey@{~=0m6zSEK3MIgccVgB_c<5Qp*BZq3v5$oR~(1WQVB*kt!F<*zsuM zs_S?-EC_U{GLNkwa&6ORBc@FpD=HIk9lYU2b7o{+@fXTc zlWaCYy^9=V2pAb677U5)=vr?avcl4;ybNeOgccVg!F`fF9VtO2THpHc(dMuBw{QK(&6leQ@Yr16=zR7AASza_Qw=+zP25W4mV?goFj~!ki05x^ zeA`~S1&(Rox@me&FdNMg?Xb_*;z#WVYXym9!A|9%>UpsXi`4#UW9ysS1(6JZ(m!+i z_RpPrj}9GOk373Cf&anL0YnlO`iLZB5mJ{Pr+MmQ7oEtWO2`;dEGRT(a*miLO`D8K zRUnN?Gp6}&)$$m7Op~x3QiT)hd$bD*`u8k|iL<4*T`QQq0OBT~GF#eO`?&M#G{T?=BdnfL2|H&4lR5RMD0m!3MHf@`^*M{gYMMtkZMZFTzt3++ zmbsh{sy=rkjIC6T;Ihz6Jfq?U^KQVSlOYzBZ{}InQ+tBV3V#7{kVaP&l~flokwEF; zg`qnmpHHmJ)z$HaI^I;rYp=ERlZ!)5{p8A}^>^y}R70O?>Ql7~2j(^W$3R`5Y3MUe zeFg@!k%co&ZDdJZlYbknYm*IavZ+nhCR+JZi)WhoQ!8Zsy?Vab$QPUWVr{A`3reAj zj*1;-+dF0;PK{+%ETm})-jk^8h7MtTi{Tya*~aX05E1tZ9B2Fv5%yjHvcw*XkDK#` zQN*=a_)OHNoR2e9U0C?BWJ#q4{PoQ8GSI~d*p(Uk;2XrUMD$9*kyu73FY3%yc5 p?&~Hr0iZZ47S8W5>)yf838bH{=U#5f>W(Dp;vND~%d*PR`43MxpjrR` literal 0 HcmV?d00001 diff --git a/src/models/__pycache__/stock_bt_history.cpython-311.pyc b/src/models/__pycache__/stock_bt_history.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4044df9e07a3d5de68f22e61dd7e6fc650b71983 GIT binary patch literal 1712 zcmZux&2Jk;6yII%I{t`b$KBdKQIte(eaWHi386@Y&{8S~sU;FJ#+$Wg5-(Y=!+K4d zn?Za;DisE)q!Q4AP}88M$Ve1LJ4A!k)AX>tMQUO3vH z9)&9`rGS|fiKnz9dO3JxYXWC@}W z9LqGBx@r`28;i!9O35hdTkpV$aGM()4+JQOK{UU+(|UBH^?jv%xZT?MqqFmS!bN-q z`MRWHZQaNdUMs6A@grQy>B^>|6tzCTe?IxCeQ)pY!=Kwfe%zszHezd5Z?^B(>}dzw zy!&VKi_aaG@??)e`?tg9og01b?T6pCZ{6#2cbGasTDNMgo4+PF-46kD8fBoYy486+ z2bRIILhMi}@rZ*&xt^aGOw@c58!`z&(sCP8){yc}{+1I0@kn+dOA_%(QlW^-P}vfh zl%x;KvPw%_l5|?wM6j#=7v>lg=HE9!EWMI`SCO^#w?JHnRIaC+wXZrqeABx7b^4mD z<*Co)OO_&A33XEa4PHyszhF-BJBmLv;U!Qotkt1BXx!*!8J& z*JK*3S*+#q1zF#aY(%i$oDM@<+ zQ97FF3d*}Az(1jH@7~yU_r< zCS@~*>M>-+kjbBHMB>#oD-y5G9GrigF(a9JBx6M~mA4zw)76ix=;_*7@LA59QCyE= zD~e5CY>076A-=a{E?hCimAbfMi7S=lk%tw|A2FliDV=*Z#^7X#CTBU}CLXx!Mo}qY zon8g!h<#91^G;RS<>_Q{FuTt^uQ~<2pvS?H{%}k5W*xa$mTfRI=D4@P#LdC?h!3zp Re^gXgp3>Q8V@&o2{sZ=1=2ZXy literal 0 HcmV?d00001 diff --git a/src/models/__pycache__/stock_data_processing.cpython-311.pyc b/src/models/__pycache__/stock_data_processing.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1f6b4f529be6a04f03a9dfaf3a632146fd385334 GIT binary patch literal 1793 zcma)6O>7%Q6y9C`$4ULuX6+pJXym+ZEg(zD7c^ujMU95eStWIGx?0xM`_cLDLEa+ETNAP( z7P68gXr8yddq6B=341oYhqCh>LiqsY??U-|paNZ}07nJ)XOuRAYmm_m_}r2WaRlE1 z=7G6F8+~gMVBm90Hr!*baMxV@-KdTJLy-pl2j&RhxdGU@BZo#ua0K=(N{@2V(~70M zK}xD-nz}K+?T2KiEB#>G*M4h%_`%<|u5W$&e#%4pipm94(2RL&fqIQfp+Lh}GgYFO zExlxHv!(N4o#jSn>8GuqZ?&&psYFjwZVJV5QsmTC~#vtwAzi5B7zTz^xk|k(OA>>ij3)C@Ftz=s6+Kap*pEHijGlI)r!V{1Ly`a?kATUfqU0!R9AjpX z8FrXx=%odPyv$B7jpZ$r*9>)`sE|dZ!ttdMaJku(HKLa=jdBN3EN#B(p5oxEhUJcz zbWFo%bweRl*AaQZAzve94%5{fb7-{B4bHEFFt2NTVxBTduq^osjXd89t)$RmxR&8KhA+VX5uo^|Be+Uu>vsrtK4 z;#A|Q&9t3Cb|T+QZx~UO%Cfv zoe8^TfqtTnxuWChC5%~@=Y!ne3!ZUBKS5FeVOOB}9+(}ED2gp%$nN#FgfsTR_fO$T YyVtwp?-yb1-DdTPJIs5xM;BlHf1J1S>;M1& literal 0 HcmV?d00001 diff --git a/src/models/__pycache__/stock_details.cpython-311.pyc b/src/models/__pycache__/stock_details.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dae2e16d4f575a4ae59aba08c0152eac54384659 GIT binary patch literal 1792 zcmZuxO=ufO6rNqJ{v~_YGLB7u949_hr8UW+F_h3lYNt-@=nw~7gy^!_S=*beR_g4E z#D^B8G+;TO1_su+r$0G>Zmw$dS`<@S>f2gw8{LR4*hQVEg5oR=$WztNRO|uNuS9s9*ypRWC!SR98qt$BBM_;hvn@NTub zp;~;~Y5@ySl^!SNLOQ0HlByYn=V8alQD{#-xWD;*dGo8v#?6Q8zu&Ll_}MjIDg?k| zy|T9T=}%A~HhyotItO1 zqs<4aNgs)W-Qo0trg<)Or_Ep1Hom=;WC?|$eP#XDZ(H{s7Q!%9|GHTJY;}F@v8z3K ze7pXryz%SJ7MASsbY~bA9IymX^kJrrvm9mt9p6*oS#(A&07_2Wpd1vjhgd4e;E&3TD4?_a6*RQ*FNlM)HdxjlY9J~CC^E3vcYE;jvGTkX9<7E)?eJ*v zOidUn_1VHu`C{cAOPHz(Q?@WwJbz$b#ea2K!c0||v4t6!hz>5EvZI4b;Z^RBm#pYy zH9BcWCyQgX#PQN8J8`^BD(_o~R5g*Z6RF}2#P@}8F51JNSZl-i%@2J6&u*IjmSCO zB`Upa?h&7%Q6rNe{y8cbFt2R`!AT)_8S(T#pP!5Rr5GtUpoI}e28N+1b*(6JL*I{<8 zHaEB85b+}n0;LjvMTlUO2>Ae2q+YmjT}ykg<_d9|TdK5|o_Mnxx3QDio#&nR|GoFd zzh$#21nd1*`~_P==wET@E#>gzpa381h$4zDWaARHrILijS+ZibT#_-8(A$W{K0#DA z@J0-wP1w;YDO98C1pS(wj~c}#6yi!JqxOUm-q&hU=#MCm&9=$ zdTJ{%05ExiR`QruD%48%d{azQTj_y#nXrr*C}Va&lBGjX#9^Mf=~XMs@0hMvWvlN( z^`w>MS_Ygxy8H9)ug(7+{<3@j4zSbIa4XDQ@yx2TCprw3PucoyVj0e&x5QkszU|IEjjNRtV-ED0pk>waI~eYm#s-H+YBK8y5@#O;3D>VEM@UV35E z*Tvp2987|@{w`8H&=;9XfHd|@J&JUyXm~o$0_w^#S&Gol<`teG;m8oeQ-s)6T7wxP zJVVIGHQf@!L3YTzS3xo~b*F*MQuHDlL|GT3_z{@OAg6ugbS{ zr(zJQdwSVrl`=RsEVtYjeQITu%OWc_1kOaM+?RpWpFf?EvBvQI;r7VIVC3SOA|7@I zG}uc(X7fXAL>yE>L>rixW}dj}cyEb|!6lROR1~oS;d%1SC7ng0L$-j&PrHzTM?4dt zdR1cUY}xR5+HfeDkJvmtV>&upjcn8Isy$z|LTq6s?6e3TCN?U*#Dxfn=$ZQukRy$OPfoOx6G3vKKGRWOZH#_1*8I>{3vIO!sD=8qj(V;!8mQ-*H(JzJr`zgu zpiTojcj4Y^LGD6}{Vn@97W|yq&Y3~Z^yRZ1ZM<t6%SE zlR`4toZO!EwPIT<23oOxeLs$sOZ(_Jc`SHOP7p*G1-#jOoG0M^cvaIin7|noKLx#> zih3OmO;n5i`aDa#5sg`aO@N{J!*MG>_G1|14jT7QdOPU6-~aAQDGcc!CymRG1^3Ab Hq7cCUI&y%b literal 0 HcmV?d00001 diff --git a/src/models/__pycache__/strategy.cpython-311.pyc b/src/models/__pycache__/strategy.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..abe28aeead3e6936cda4de3c81cf92084b031efc GIT binary patch literal 1988 zcmZuxO>7!R6y9CH25hj`UL5C;TCGSWl*lJbmZ~IDQ{<@rp{Tf3g;7QuW^AYzaCa9u zzH#V*D5*j#r%sjBb)?pz3QdTVHncu8#~yd3J)pfdK)E?`dda1o+2zlyXPL*m_r7^Q z``+7!9*>*C>;Bu%R;TO~^*3SK%h5UKfM@pl85setA(GH4+ zY-y}7BG!T#FL8aCF$>0LiD=v?{{$}oslMABoW#XHtve&K$jfMLYdK3w^0IQ!+271YSkFHXFd`9km9=}j^t^}A1Mh5cH7ule1NZ7+imt@2qx?y;4>S3CHlzVlmi z=NZmnn9s@?kj`uZ9>Ol_<)4r1+rM{E&NixxF#VW_W8=}DvLipv;rMt!dLNI>BfQNO zwL=D%!uH0~F2(c`l6i3ut3v^Y?gYm40ie49NM&Fak0a2B0r(=z3uG<<5Xm&^4r5e* zrjCf+e=lMZd@uG{l25PAV{#oAaV=IW-)?^YP5pCr7&eRqDJHeL zVR~arw;N%w*v=T<(iu#0ou@iFRT$ZGtE1<%(Q|nRdBjr~A)dx06!3}UjyO&TM+o^Q z=*|z*@*MfR=u8sot`8EtXtJKRUn5n$%O-d!(ZkAH8%Q6_N=O9QfcF|7Uzd?2gLo#J zmUY*B;`^4 zt#b>Hr6BOo~H78&|cgh)2&LnZw$yk}K7k9L=_>V(a?-C#y{jDm9S(SD9KMz0v#*Hs^$98?f&T&K{U;Rw literal 0 HcmV?d00001 diff --git a/src/models/__pycache__/test_table.cpython-311.pyc b/src/models/__pycache__/test_table.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b6094220b652a0e4810784e20e69a1b5d2f91db6 GIT binary patch literal 2585 zcmZuzUu@Gx7gt4h~GL8V1g8tS<@xAjPz;Mn|m z%Ql8G0V8Zd6vkMGP{~9DMJgk-prJkS=tzEu{DibPB~L4Yz3{T#*-1-d&hh2G?|%2Y z-<`kv>^t@K4h)v-e_fmU(1KxqBcoc(mB#BYVdE+WFklE{5ktTb!2`Gf*?8C(F$GL& z+ZZ-SECGuF!?FDsFrCGKnKsNDG3+k9wGy!67;U1h^9JaJcg1T1mLr(oT1KWHm(2&G zfDX%+DTWS%xZfaqV`l)x@=S<~pdK>B(G(x$V2}G0!_SaBH5sN!mWt4!3Z?qDCVvD+ z@@Xt!09XJAhJX>t_^R3umjR4f(>z4og|}7$78PTy+Q1A^maMgALADx{4WjHilwCKU zBU@K9pQDBehch~}^)>U=>6lzPCT9&3(&W}@s@E|!=$KqO457ogb(lsSra{FJYkS!Q z6?wAFHEU~BQB7+o9C&q19v!A7>#NzxW*w$AyQv1_)nPX4=4w#`c?%5Wtu>>4pbZ9W zyKEn&~m`j)yoV#xqg2+y;9E zC#u*kajqWrs$iiJ;m&6lQ@MAmbmL-i>B7qL;>z6Df~%&70YV+(`zG=7{-fg3 zQpHx1KP$^is@<=uF3sLoQu9i3u6*q_T(1?-Q5I5=!caVlGk2 z{c`23ZwKzk{o)tvRTb&zhkeVf&IG|R(-pJUoaFqJqWAQFmR@W%EG1cFF%%ky&OD6 zu_2lS6i)@?TqvmK$as*4p{ZrYp4b`LgrsmD$g~ySaa1MNu0lh5$~|YzH*`x4-AOaL zxXlp3A=t3LmGdH_1y$&g$X1dB(GV1CLYD1wmhVTYTE-b5+drG3IL)-NOz1dTmh7NT zLUkM|aACPFLPsWPj^xk8XqlLVvPdcviV7@Li(K_FjwVA zrvVA4aWm{m2DQ=Vst)y~@Q%Z|FB%Qg6su`*FmV#TI%qgVuJ3~^h`b^*9GzxZlHz4Y zl>@bf7~E0FhuW2qu&r(hw+U^9x+aiu$Z0mcwisNS6DuyZx=4efNFSO70 zWoAVCuDpGhWZ#w8S8%r{PfG6g%*P9T&sfntmUoXy?yAe|~ zxO-4+8p<~fNlinE124Q?si|}{)0;acdWZ7fA;~+GI8=DQm*_<5J2PXs&ga7-F`g&J zC1M<^cXy!mb|5ulqWfsxeN=KEMa%6%1G*M^pG}CwXr36Ah*1cyxHJ5-Hjy}-Ck{)* z;dk74#>^;OFJ3VGkGclBZoJ4AO^!Bqds Qh&SMGFjys==u&s&f1zIR`v3p{ literal 0 HcmV?d00001 diff --git a/src/models/__pycache__/tran_observer_data.cpython-311.pyc b/src/models/__pycache__/tran_observer_data.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1eb549f9c841d226a0013678d178c06c4d56f30d GIT binary patch literal 1210 zcmZuw&rcIk5Pti!+pUBOQYi;fF~kPalXx)30}zQM-~kfNrpadMTgu9Ii*L6=FNPQs zIcPu=0vDqP0}{~imzZtRL$g;?lEzC(@xn#t?Y3wOZ{M5F%$uEl-^}c8M~94H{o4C6 zzsVrG?e-zC|d|4%$O+4C`^`BScclH$z^$k_jS$`vZ5j~h(!|!^RE#WbY_P` zXdh0nln@qYP$E>NW`gyEsT|f#Pns&UR1=Z2!OkK6KtPU<|#vz)cXA0>? z690#981)TMwvdh#1|x-Kkdo*J5!nFO%{$TO+R;d=GshoevLQ_Gr_*8M+8*(9BU#JXvx z&O=Bp6+E8eBb|^j0>{1vV)g7PNBcKA-RpOX(UDSgWKEzCi9rbI1u>n7kjvDd1t#58 z(9!UuN0#%DPMpUY*b9#>Xe2PXMZMrfMh#0N1>ZxW^d*{$^ibiazDhMZAHJZYWWNOQ zAF6QgTI_BA_P84!D24}0;eplBvfQ^eSd#lbr(JotC=Zw9;nlHnd}upXiVq!LamSvx zaithnN^!;IV>O-;?$*$GaZG(D7hD9;D5~j%;C6R z+E9G@jS)&GAlFLxRJtGZ{{guHA@qZ?^B`&*!!TuZ)4gby(V*M9%jmY-x@)4y0M+wi OBY#Z2Cl_4#Wd8z_ZZ=&2 literal 0 HcmV?d00001 diff --git a/src/models/__pycache__/tran_orders.cpython-311.pyc b/src/models/__pycache__/tran_orders.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f7d5c7a321f91ce02c5aa2c2229234db5ed1036e GIT binary patch literal 1132 zcmaJ<&rcIU6rR~1Wp`8LP(j5Q5^D(SfukhGm@25zAeg|xY?@5g&d^qNw>Y~EaO=SX z2NTf?T#Q~s4IVgp@Q-Md9-6rtO}Ld5FJAP`Zi^I&GdthDH*e<6``&vW1_vz!V`Bd6 z(h5fCn^by3?+P6X!WN>4Vh{P)!M@@sSn7(W`kJG~vg+x6%1L3QpnHgF&k@xb-cb?S zg_R6vAhxBzHn5|B1y@Y6NNzx4cj4s9 zIyb;c3{nUm0o0CW%RIj^@?vazrfy~%X0~Z&Yqwk0=*D={8huyT)9Th#!Vv1C;AH7>z>XPF!vXf;uA&Gr~=7Xh32!!6&6sS z34=LX=Y!Jy83{uYWOoZa?kGMiH{$2dN$Ell(93Hn@7+-Cpf8jvx9iC!{GxP^b C_Yn60 literal 0 HcmV?d00001 diff --git a/src/models/__pycache__/tran_position.cpython-311.pyc b/src/models/__pycache__/tran_position.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ecc315885edddef3ffc548e4d5038d5d4d2fbacc GIT binary patch literal 1174 zcmZuw%}*0S6rb6TcDsNS#A-ym0c(Zi^I{o%!wi+Bfg_X7)=@kB(sc+4->? zVuXGQr9IS^(3}Bb2T??^i#%*%PckJebjg)H#Z)3$c2zH7CNPrFGeniQh^jW;mk~OE z6%R8>6Z6PO9tqWuxH?;;w#yUCj_uOGz`S?e30FvHExI=GEzbs%HXkx8vR+8wY zw3OyH2s_9|CZ@=gBxK94+CC`vyR;9a4q(N@RHKMQTcA-|AEYf+^m;z%2hoF>pes=q~^bX>Ii=3n_uM?pYhewf#c4!WF;dx&~(5 z;OZdQ?pgC5csdDEe4!kLO0cPX+IN1_+^ij1ya$@+leJbMbB4+_5_Ogkt`p)FX$e|Q zcsC)hOO`8oP6!hR&(-J{zM&>4=tk-6d{4E<@jZCL?m&q6S%MNkL&g}_(cQ{bvyMh8 bowJVaS2|}yOJJaS8MYrD3+u@hu88bEgaIN` literal 0 HcmV?d00001 diff --git a/src/models/__pycache__/tran_return.cpython-311.pyc b/src/models/__pycache__/tran_return.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..99ac91072243594ff2d68c3c27659c196744d6d7 GIT binary patch literal 1164 zcmZuw%}*0S6rb6zZnq#+ff^D+6hqugy+LA(F+enuz(F{eO_RyW45hN&;_O!F!Gwbc z4klJTpa&x-)Zl@?#I#8d&0LKp+)9cUFZyP;6$;GG{PunAoA-M&`?p;scjz6VJqyA%qKIM-6|sYhilbnuE1p`^94(SnPcIsdfsuk9AgaAYRA+csMQ9IJ zJe&kI7Lc7dkgBZ+eXc~ACyeC+^JrjWF|b|;S4ikS@fh*lA_J4T5OOzU`N~o!N%Yb> zO1lceHe$%Z6gi55nEJHqgL1zqyFh9WRy-U%iZHr5RUt;(RUigdCngcam|Yzr^ix@q z5k&(iCQf~8Q4z=63FJ!S(aA02_sppU?f=e|_-r?XOR7a_g?Y#0Ygm zHy7}w93*C5kUOz=cC8{bSrNYgsZy|ls{ot-pJsZrInKC0vpq4_KHF{qVlLH$o{;k(^#}d913dtRhlV1>fbB*pfa`OjKx;m=DTa(LTdc zk_R4vydA*{fD;uauWSv!8ht-gOODl(V~yn4=2X)f*&1(HBRfm`TFsiQTayiIa&x+w zv8&G;8T(6E%S_ZW6OGJ7O&e@!SijXmXW>xVj?Qt!mC2&!ZBv+GiHD^^z_`2`aR)4D zcQ$T!d~TPI4cP!V%fE(8HZiwezJJU+@g2C!uR)0XMS?OwOT`#B(e>JSvx&xQy|amK X)_P~lFfdR(3)P`RX+1i}6_Nc13hf?} literal 0 HcmV?d00001 diff --git a/src/models/__pycache__/tran_trade_info.cpython-311.pyc b/src/models/__pycache__/tran_trade_info.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c92430b9f2e84737d324718f053a37c6cdeb04bc GIT binary patch literal 1165 zcmZuw&rcIU6rR~%-7a7&YE=$G#1MLEA_fk|cmUKu0v_OCHcd7QGc7CIEzWKOO+57A zfrE)v59q<@2{Cx^FEMS>Lo-*S3Ad8sg_Cb~TeXGRdEdS_^Y+dA-kbeAG-Mzc@85pU zF^td;VRVPuQQC{3>>+{(c2NPFxFDGl7PjQd1;tcCU3S%iW@;Eo=n*2y3q(|k_hp0* zL84*Ch_-^#u_IxoC9W)V1DY?dcY=g3 zrDL?KpzI-vOiYj|Nr=i%yL*uCS7{$e9fCx|R6`G~YZDoKlzj<&Ksr9L&`0mu;Gy)*w0%jPs#YW_uunE_k`(D)5lDHjl1DOrUKP>>bOcUOw31idS;oG-iXa z)x)q`wDv97>19Z8Up@+v;8gi^xaZW&5)Evgfbw}30HKxxxh$tuu3KSsEsGnLRVb1Y zl-uHimi4S;yQ1!v#YFSD8n)sKYJ-7(&jZZg%|3B#FTV(|;}pGo_UE@(M<3q&`uIA# zZF}p~B6eVBeYT#3z|{4#edlMk%3Kkdu?rAx0K~rmQ2F~blcUu{ZR~bEIn_u`?W*Eo zSHOi`1z1iGu%s|93K$lk0C0W53m%GV#3hID_}qrgB2(U^Wj-+Lcs47CD=Z;C60z82 z0b%Hr&_v5a5ck7>4)9-A@oT%Om!t3IYw_`Ve7q4Kugo@$k===gG4g5cAgCELbz`Ps z%v9!@smW@tk(xXpwbXPyHQh)}S01+%tlnv%vv4fdPR`kg5)(Nsr**CeMHUntpE7YV zBF+X>ITKYmYS*QI3mG5kE`A#($VAk7>7MHMqSoJMW8fnG5x)e`k}<|jbhCEeY@&%; b?`)#UTJLOW8V0Ioq59xhte%{+6_Wh}N9`Q3 literal 0 HcmV?d00001 diff --git a/src/models/__pycache__/trand_info.cpython-311.pyc b/src/models/__pycache__/trand_info.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7337dfd21c1f424de16636088f890162f1990240 GIT binary patch literal 1205 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 hOOXm9vu}|aMvdQREyEUY-}uP=`g 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 literal 0 HcmV?d00001 diff --git a/src/models/back_observed_data.py b/src/models/back_observed_data.py new file mode 100644 index 0000000..ce6662e --- /dev/null +++ b/src/models/back_observed_data.py @@ -0,0 +1,13 @@ +from tortoise import Model, fields +from src.models import with_table_name +from src.models.strategy import Strategy + + +class BackObservedData(Model): + id = fields.IntField(pk=True, description='id表', ) + key = fields.CharField(max_length=255, null=True, description='key', ) + observed_data = fields.BinaryField(null=True, description='格式化后的json数据', ) + + + class Meta: + table = with_table_name("back_observed_data") \ No newline at end of file diff --git a/src/models/back_observed_data_detail.py b/src/models/back_observed_data_detail.py new file mode 100644 index 0000000..3b23197 --- /dev/null +++ b/src/models/back_observed_data_detail.py @@ -0,0 +1,12 @@ +from tortoise import Model, fields +from src.models import with_table_name + + +class BackObservedDataDetail(Model): + id = fields.IntField(pk=True, ) + key = fields.CharField(max_length=255, ) + back_observed_data = fields.BinaryField(null=True, ) + + + class Meta: + table = with_table_name("back_observed_data_detail") diff --git a/src/models/back_position.py b/src/models/back_position.py new file mode 100644 index 0000000..4646eaa --- /dev/null +++ b/src/models/back_position.py @@ -0,0 +1,14 @@ +from tortoise import Model, fields +from src.models import with_table_name +from src.models.strategy import Strategy + + +class BackPosition(Model): + id = fields.IntField(pk=True, ) + key = fields.CharField(max_length=25, null=True, ) + back_position_data = fields.BinaryField(null=True, ) + + + + class Meta: + table = with_table_name("back_position") \ No newline at end of file diff --git a/src/models/back_result_indicator.py b/src/models/back_result_indicator.py new file mode 100644 index 0000000..ce3b07b --- /dev/null +++ b/src/models/back_result_indicator.py @@ -0,0 +1,14 @@ +from tortoise import Model, fields +from src.models import with_table_name + + + +class BackResultIndicator(Model): + id = fields.IntField(pk=True, ) + key = fields.CharField(max_length=25, null=True, ) + indicator = fields.BinaryField(null=True, ) + + + class Meta: + table = with_table_name("back_result_indicator") + diff --git a/src/models/back_trand_info.py b/src/models/back_trand_info.py new file mode 100644 index 0000000..f80f8f9 --- /dev/null +++ b/src/models/back_trand_info.py @@ -0,0 +1,13 @@ +from tortoise import Model, fields +from src.models import with_table_name + + + +class BackTrandInfo(Model): + id = fields.IntField(pk=True, ) + key = fields.CharField(max_length=25, null=True, ) + trade_info = fields.BinaryField(null=True, ) + + + class Meta: + table = with_table_name("back_trand_info") \ No newline at end of file diff --git a/src/models/backtest.py b/src/models/backtest.py new file mode 100644 index 0000000..6ec1224 --- /dev/null +++ b/src/models/backtest.py @@ -0,0 +1,25 @@ +from tortoise import Model, fields +from src.models import with_table_name +from src.models.strategy import Strategy + + +class Backtest(Model): + id = fields.IntField(pk=True) + strategy: fields.ForeignKeyRelation[Strategy] = fields.ForeignKeyField( + model_name="models.Strategy", + related_name="integration_strategy", + # db_constraint=False, + on_delete=fields.CASCADE, + index=True, + ) + key = fields.CharField(max_length=20, unique=True, index=True, description="回测key") + user_id = fields.IntField(null=True, description="回测用户") + backtest_at = fields.DatetimeField(auto_now_add=True, description="回测时间") + backtest_code = fields.TextField(description="回测代码") + is_running = fields.BooleanField(default=True, description="回测状态") + updated_at = fields.DatetimeField(auto_now=True, description="修改时间") + deleted_at = fields.DatetimeField(null=True, description="删除时间") + is_active = fields.BooleanField(default=True, description="是否可用") + + class Meta: + table = with_table_name("Backtest") diff --git a/src/models/backtest_parameters.py b/src/models/backtest_parameters.py new file mode 100644 index 0000000..0c1ee0b --- /dev/null +++ b/src/models/backtest_parameters.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel, Field +from typing import Dict + + +class BenchmarkCodes(BaseModel): + """ + 模型类用于表示基准名称和对应代码的映射。 + """ + codes: Dict[str, str] = Field( + default_factory=lambda: { + "沪深300": "000300", # 使用字符串来表示代码 + "中证500": "000905" + }, + title="Benchmark Codes", + description="A mapping of benchmark names to their respective codes." + ) + + # 添加一个属性或方法来根据基准名称返回代码 + async def get_code(self, benchmark_name: str) -> str: + if benchmark_name in self.codes: + return self.codes[benchmark_name] + else: + raise ValueError(f"Unknown benchmark name: {benchmark_name}") diff --git a/src/models/observed_data.py b/src/models/observed_data.py new file mode 100644 index 0000000..3ba07ae --- /dev/null +++ b/src/models/observed_data.py @@ -0,0 +1,123 @@ +from objectbox.model import Entity, Id, Property, PropertyType + + +@Entity(id=1, uid=1) +class ObservedData: + id = Id(id=1, uid=1001) + key = Property(str, id=2, uid=1002) + date = Property(str, id=3, uid=1003) + time_return = Property(float, type=PropertyType.float, id=4, uid=1004) + cash = Property(float, type=PropertyType.float, id=5, uid=1005) + benchmark_return = Property(float, type=PropertyType.float, id=6, uid=1006) + is_running = Property(bool, id=7, uid=1007) + benchmark_ratio = Property(float, type=PropertyType.float, id=8, uid=1008) + max_draw_down = Property(float, type=PropertyType.float, id=9, uid=1009) + strategy_ratio = Property(float, type=PropertyType.float, id=10, uid=1010) + + + +@Entity(id=2, uid=2) +class BackResultIndicator: + id = Id(id=5, uid=2001) + key = Property(str, id=6, uid=2002) + indicator = Property(str, id=7, uid=2003) + + +@Entity(id=3, uid=3) +class Position: + id = Id(id=1, uid=3001) + key = Property(str, id=2, uid=3002) + date = Property(str, id=3, uid=3003) + name = Property(str, id=4, uid=3004) + size = Property(int, id=5, uid=3005) + price = Property(float, id=6, uid=3006) + adjbase = Property(float, id=7, uid=3007) + profit_loss = Property(float, id=8, uid=3008) + + +@Entity(id=4, uid=4) +class TradeInfo: + id = Id(id=1, uid=4001) + key = Property(str, id=2, uid=4002) + stock_code = Property(str, id=3, uid=4003) + ordtype = Property(str, id=4, uid=4004) + executed_size = Property(int, id=5, uid=4005) + executed_price = Property(float, id=6, uid=4006) + value = Property(float, id=7, uid=4007) + size = Property(int, id=8, uid=4008) + price = Property(float, id=9, uid=4009) + status = Property(str, id=10, uid=4010) + pnl = Property(float, id=11, uid=4011) + date = Property(str, id=13, uid=4013) + commission = Property(float, id=14, uid=4014) + + +@Entity(id=5, uid=5) +class TranObserverData: + id = Id(id=1, uid=5001) + key = Property(str, id=2, uid=5002) + date = Property(str, id=3, uid=5003) + time_return = Property(float, type=PropertyType.float, id=4, uid=5004) + cumulative_return = Property(float, type=PropertyType.float, id=5, uid=5005) + trades = Property(float, type=PropertyType.float, id=6, uid=5006) + total_revenue = Property(float, type=PropertyType.float, id=7, uid=5007) + current_price = Property(float, type=PropertyType.float, id=8, uid=5008) + open = Property(float, type=PropertyType.float, id=9, uid=5009) + close = Property(float, type=PropertyType.float, id=10, uid=50010) + high = Property(float, type=PropertyType.float, id=11, uid=50011) + low = Property(float, type=PropertyType.float, id=12, uid=50012) + volume = Property(float, type=PropertyType.float, id=13, uid=50013) + cash = Property(float, type=PropertyType.float, id=14, uid=50014) + annualized_return = Property(float, type=PropertyType.float, id=15, uid=50015) + + +@Entity(id=6, uid=6) +class TranReturn: + id = Id(id=1, uid=6001) + key = Property(str, id=2, uid=6002) + date = Property(str, id=3, uid=6003) + time_return = Property(float, type=PropertyType.float, id=4, uid=6004) + benchmark_return = Property(float, type=PropertyType.float, id=5, uid=6005) + cumulative_return = Property(float, type=PropertyType.float, id=6, uid=6006) + max_draw_down = Property(float, type=PropertyType.float, id=7, uid=6007) + + +@Entity(id=7, uid=7) +class TranOrders: + id = Id(id=1, uid=7001) + key = Property(str, id=2, uid=7002) + order_return = Property(str, id=3, uid=7003) + + +@Entity(id=8, uid=8) +class TranTradeInfo: + id = Id(id=1, uid=8001) + key = Property(str, id=2, uid=8002) + stock_code = Property(str, id=3, uid=8003) + ordtype = Property(str, id=4, uid=8004) + executed_size = Property(int, id=5, uid=8005) + executed_price = Property(float, id=6, uid=8006) + value = Property(float, id=7, uid=8007) + size = Property(int, id=8, uid=8008) + price = Property(float, id=9, uid=8009) + status = Property(str, id=10, uid=8010) + pnl = Property(float, id=11, uid=8011) + date = Property(str, id=13, uid=8013) + commission = Property(float, id=14, uid=8014) + exec_type = Property(str, id=15, uid=8015) + trail_stop = Property(float, id=16, uid=8016) + trail_price_limit = Property(float, id=17, uid=8017) + + +@Entity(id=9, uid=9) +class TranData: + id = Id(id=1, uid=9001) + key = Property(str, id=2, uid=9002) + alpha = Property(float,type=PropertyType.float, id=3,uid=9003) + beta = Property(float, type=PropertyType.float, id=4,uid=9004) + sharpe_ratio = Property(float, id=5,uid=9005) + start_date = Property(str,id=6, uid=9006) + close_date = Property(str, id=7, uid=9007) + use_date = Property(str, id=8, uid=9008) + filename = Property(str, id=11, uid=1011) + day_alpha_array = Property(str,id=12,uid=1012) \ No newline at end of file diff --git a/src/models/order.py b/src/models/order.py new file mode 100644 index 0000000..a428d5b --- /dev/null +++ b/src/models/order.py @@ -0,0 +1,68 @@ +from tortoise.models import Model +from tortoise import fields, models +from tortoise.contrib.pydantic import pydantic_model_creator + +from src.models import with_table_name, AccountType + + +class Order(Model): + id = fields.IntField(pk=True, description="订单ID") + stock_code = fields.CharField(max_length=30, description="股票代码") + stock_name = fields.CharField(max_length=30, description="股票名称") + limit_price = fields.FloatField(description="限价") + order_quantity = fields.IntField(description="委托数量") + order_amount = fields.FloatField(description="委托金额") + order_type = fields.CharField(max_length=20, description="订单类型") # 例如:买入、卖出 + position = fields.CharField(max_length=30, description="仓位") + user_id = fields.IntField(null=False, description="用户Id") + entrust_date = fields.DatetimeField(null=True, description="委托日期") + entrust_time = fields.TimeField(null=True) + + class Meta: + table = with_table_name("orders") + + +OrderResponse = pydantic_model_creator( + Order +) + + +class Entrust(Model): + id = fields.IntField(pk=True, description="主键id") + fund_account = fields.IntField(null=True, description="资金账户") + securities_alias = fields.CharField(max_length=30, null=True, description="账户别名") + account_type = fields.IntEnumField(AccountType, null=True, description="账户类型") + stock_code = fields.CharField(max_length=30, description="证券代码") + stock_name = fields.CharField(max_length=30, null=False, description="证券名称") + limit_price = fields.FloatField(description="委托价") + entrust_number = fields.IntField(description="委托数量") + deal_price = fields.FloatField(null=True, max_digits=10, description="成交价格") + deal_number = fields.IntField(null=True, description="成家数量") + order_type = fields.CharField(max_length=30, description="操作") # 买入卖出 + entrust_date = fields.DateField(null=True, description="委托日期") + entrust_money = fields.FloatField(description="委托金额") + # Python + is_repair = fields.BooleanField(default=False, index=True, description="是否补单") + + entrust_time = fields.TimeField(null=True) # 委托时间 + + class Meta: + table = with_table_name("Entrust") + + +EntrustResponse = pydantic_model_creator( + Entrust +) + + +class Backtesting(Model): + id = fields.IntField(pk=True, description="主键id") + key = fields.CharField(max_length=30, description="回测key") + + class Meta: + table = with_table_name("backtest") + + +BacktestingResponse = pydantic_model_creator( + Backtesting +) diff --git a/src/models/page_Info.py b/src/models/page_Info.py new file mode 100644 index 0000000..35e7293 --- /dev/null +++ b/src/models/page_Info.py @@ -0,0 +1,65 @@ +from datetime import datetime + +from pydantic import BaseModel, Field +from typing import Optional, List + +from src.pydantics.transaction import TransactionsPydantic, TransactionPydantic + + +class PageInfo(BaseModel): + page: Optional[int] + pageSize: Optional[int] + searchname: Optional[str] + + + +# class PageResponse(BaseModel): +# current_page: int +# page_size: int +# total_pages: int +# total_items: int +# items: List[dict] # 使用 List[dict] 类型 +# +# # 单个事务的 Pydantic 模型 +# class TransactionPagePydantic(BaseModel): +# id: int +# key: str +# cash: float +# transaction_name: str +# transaction_type: Optional[str] # 可以为 None +# user_id: Optional[int] = None +# is_running: bool +# is_deleted: bool +# process_id: Optional[int] = None +# bar: Optional[str] = None +# created_at: datetime +# updated_at: datetime +# stopped_at: Optional[str] = None +# deleted_at: Optional[str] = None +# strategy_id: int +# +# class Config: +# orm_mode = True +# from_attributes = True +# +# # 分页的 Pydantic 模型 +# class PageData(BaseModel): +# results: List[TransactionPydantic] +# total: int +# page: int +# pages: int +# size: int +# # next: Optional[str] = None +# # previous: Optional[str] = None +# # total_pages: int +# +# # 包装响应的 Pydantic 模型 +# class EntityResponse(BaseModel): +# status_code: int +# message: str +# data: PageData + + + + + diff --git a/src/models/position.py b/src/models/position.py new file mode 100644 index 0000000..16991fe --- /dev/null +++ b/src/models/position.py @@ -0,0 +1,36 @@ +from tortoise import Model, fields +from src.models import with_table_name +from src.models.strategy import Strategy + + +class Position(Model): + id = fields.IntField(pk=True, description="主键") + key = fields.CharField(max_length=30, description="键") + date = fields.DateField(description="日期") + name = fields.CharField(max_length=30, description="名称") + size = fields.IntField(description="数量") + price = fields.FloatField(description="价格") + adjbase = fields.FloatField(description="复权基数") + profit = fields.FloatField(description="利润") + + class Meta: + table = with_table_name("position") + + +def create(id,pk,key, date, name, size, price, adjbase, profit): + try: + position = Position(id=id,pk=pk,key=key, date=date, name=name, size=size, price=price, adjbase=adjbase, profit=profit) + position.save() + return position + except Exception as e: + print(f"Position create error: {str(e)}") + return None + + +def bulk_create(param): + try: + Position.bulk_create(param) + return "Position bulk create success" + except Exception as e: + print(f"Position bulk create error: {str(e)}") + return None \ No newline at end of file diff --git a/src/models/security_account.py b/src/models/security_account.py new file mode 100644 index 0000000..1ff3711 --- /dev/null +++ b/src/models/security_account.py @@ -0,0 +1,31 @@ +from tortoise import Model, fields + +from src.models import with_table_name, AccountType, supervisionFee, transferFee, IsType + +from tortoise.contrib.pydantic import pydantic_model_creator + + +class SecurityAccount(Model): + """ + 证券账户 + """ + id = fields.IntField(pk=True, description="主键") + securities_name = fields.CharField(max_length=30, null=False, description="证券公司名字昵称") + + fund_account = fields.BigIntField(null=True, description="资金账户") + + + account_alias = fields.CharField(max_length=30, null=False, description="账户别名") + + money = fields.FloatField(null=True, description="账户金额") + available_money = fields.FloatField(null=True, description="可用金额") + available_proportion = fields.FloatField(null=True, description="可用资金占比") + freeze = fields.FloatField(null=True, description="冻结金额") + + class Meta: + table = with_table_name("security_account") + + +SecurityAccountResponse = pydantic_model_creator( + SecurityAccount +) diff --git a/src/models/snowball.py b/src/models/snowball.py new file mode 100644 index 0000000..767819e --- /dev/null +++ b/src/models/snowball.py @@ -0,0 +1,21 @@ +from tortoise import Model, fields + +from src.models import with_table_name + +from tortoise.contrib.pydantic import pydantic_model_creator + + +class Snowball(Model): + """ + 雪球相关信息 + """ + id = fields.IntField(pk=True, description="主键") + snowball_token = fields.CharField(max_length=10000,null=True, description="雪球用户的token") + + class Meta: + table = with_table_name("snowball") + + +SnowballResponse = pydantic_model_creator( + Snowball +) diff --git a/src/models/stock.py b/src/models/stock.py new file mode 100644 index 0000000..74fdeea --- /dev/null +++ b/src/models/stock.py @@ -0,0 +1,23 @@ +from tortoise import Model, fields +from tortoise.contrib.pydantic import pydantic_model_creator + +from src.models import with_table_name, StockType + + +class Stock(Model): + """ + 股票相关信息 + """ + id = fields.IntField(pk=True, description="主键") + stock_code = fields.CharField(max_length=30, description="股票代码") + stock_name = fields.CharField(max_length=30, null=True, description="股票名称") + type = fields.CharEnumField(StockType, null=True, description="类型") + stock_pinyin = fields.CharField(max_length=30, description="股票拼音") + + class Meta: + table = with_table_name("stock") + + +StockResponse = pydantic_model_creator( + Stock +) diff --git a/src/models/stock_bt_history.py b/src/models/stock_bt_history.py new file mode 100644 index 0000000..011670e --- /dev/null +++ b/src/models/stock_bt_history.py @@ -0,0 +1,17 @@ +from tortoise import Model, fields +from src.models import with_table_name + +class StockBtHistory(Model): + id = fields.IntField(pk=True, ) + end_bt_time = fields.CharField(max_length=8, null=True,description='回测最终时间', ) + bt_stock_code = fields.CharField(max_length=8, null=True,description='回测股票代码', ) + bt_stock_name = fields.CharField(max_length=10, null=True, description='回测股票名称', ) + bt_benchmark_code = fields.CharField(max_length=8, null=True,description='股票基准代码', ) + bt_stock_period = fields.CharField(max_length=10, null=True, description='回测类型', ) + bt_strategy_name = fields.CharField(max_length=10, null=True, description='回测策略名称', ) + bt_stock_data = fields.BinaryField(null=True, description='回测股票数据', ) + + + + class Meta: + table = with_table_name("stock_bt_history") \ No newline at end of file diff --git a/src/models/stock_data_processing.py b/src/models/stock_data_processing.py new file mode 100644 index 0000000..bcf16f7 --- /dev/null +++ b/src/models/stock_data_processing.py @@ -0,0 +1,19 @@ +from tortoise import Model, fields +from tortoise.contrib.pydantic import pydantic_model_creator + +from src.models import with_table_name, StockType + + +class StockDataProcessing(Model): + bt_benchmark_code = fields.CharField(max_length=6, null=True, description='基准代码', ) + bt_stock_period = fields.CharField(max_length=10, null=True, description='数据类型', ) + bt_strategy_name = fields.CharField(max_length=10, null=True, description='回测策略名', ) + id = fields.IntField(pk=True, ) + processing_data = fields.BinaryField(null=True, description='清洗后的数据', ) + prosessing_date = fields.CharField(max_length=10, null=True, description='当前回测时间', ) + stock_name = fields.CharField(max_length=10, null=True, ) + stocke_code = fields.CharField(max_length=10, null=True, ) + + + class Meta: + table = with_table_name("stock_data_processing") diff --git a/src/models/stock_details.py b/src/models/stock_details.py new file mode 100644 index 0000000..2f53022 --- /dev/null +++ b/src/models/stock_details.py @@ -0,0 +1,25 @@ +from tortoise import Model, fields +from tortoise.contrib.pydantic import pydantic_model_creator + +from src.models import with_table_name, StockType + + +class StockDetails(Model): + """ + 股票相关信息 + """ + id = fields.IntField(pk=True, description="主键") + stock_code = fields.CharField(max_length=30, description="股票代码") + stock_name = fields.CharField(max_length=30, null=True, description="股票名称") + type = fields.CharEnumField(StockType, null=True, description="类型") + stock_pinyin = fields.CharField(max_length=30, description="股票拼音") + latest_price = fields.FloatField(null=True, description="最新价") + rise_fall = fields.FloatField(null=True, description="跌涨幅") + + class Meta: + table = with_table_name("stock_details") + + +StockDetailsResponse = pydantic_model_creator( + StockDetails +) diff --git a/src/models/stock_history.py b/src/models/stock_history.py new file mode 100644 index 0000000..50ba323 --- /dev/null +++ b/src/models/stock_history.py @@ -0,0 +1,13 @@ +from tortoise import Model, fields +from src.models import with_table_name +class StockHistory(Model): + id = fields.IntField(pk=True, ) + stock_code = fields.IntField(description='股票代码', ) + stock_name = fields.CharField(max_length=10, null=True, ) + start_time_to_market = fields.CharField(max_length=10, null=True, description='股票上市时间', ) + end_bt_time = fields.CharField(max_length=10, null=True, description='最终回测时间', ) + symbol_data = fields.BinaryField(null=True, description='股票数据', ) + + + class Meta: + table = with_table_name("stock_history") \ No newline at end of file diff --git a/src/models/strategy.py b/src/models/strategy.py new file mode 100644 index 0000000..fc01fdd --- /dev/null +++ b/src/models/strategy.py @@ -0,0 +1,19 @@ +from tortoise import Model, fields +from src.models import with_table_name + + +class Strategy(Model): + id = fields.IntField(pk=True, description='主键') + strategy_name = fields.CharField(max_length=255, description='策略名称') + strategy_hash = fields.CharField(max_length=255, description='策略版本号') + strategy_type = fields.CharField(max_length=255, null=True, description='策略类型') + user_id = fields.IntField(null=True, description='所属用户') + backtest_count = fields.IntField(null=True, description='回测次数') + backtest_keys = fields.JSONField(null=True, description='回测key列表') + is_deleted = fields.BooleanField(max_length=255, default=False, description='是否删除') + created_at = fields.DatetimeField(auto_now_add=True, description="创建时间") + updated_at = fields.DatetimeField(auto_now=True, description="修改时间") + deleted_at = fields.DatetimeField(null=True, description="删除时间") + + class Meta: + table = with_table_name("Strategy") diff --git a/src/models/test_table.py b/src/models/test_table.py new file mode 100644 index 0000000..50caed8 --- /dev/null +++ b/src/models/test_table.py @@ -0,0 +1,35 @@ +from tortoise import Model, fields +from tortoise.contrib.pydantic import pydantic_model_creator + +from src.models import with_table_name + + +class TestTable(Model): + """ + 用户 + """ + id = fields.IntField(pk=True, description="主键") + nickname = fields.CharField(max_length=30, null=True, description="用户昵称") + avatar_url = fields.CharField(max_length=255, null=True, description="头像") + member_type = fields.IntField(null=True, description="会员类型") + beta_account_type = fields.CharField(null=True, max_length=30, description="内测账号类型") + pre_cost_time = fields.IntField(null=True, description="预支付时间(单位年)") + + qr_code = fields.CharField(max_length=255, null=True, description="专属客服二维码") + dedicated_id = fields.IntField(null=True, description="专属客服id") + + invited_user_id = fields.IntField(null=True, description="邀请人id") + created_user_id = fields.IntField(null=True, description="创建人id") + is_deleted = fields.BooleanField(default=False, index=True, description="是否删除") + login_at = fields.DatetimeField(null=True, description="最后一次登录时间") + created_at = fields.DatetimeField(auto_now_add=True, description="创建时间") + updated_at = fields.DatetimeField(auto_now=True, description="修改时间") + deleted_at = fields.DatetimeField(null=True, description="删除时间") + + class Meta: + table = with_table_name("users") + + +UserResponse = pydantic_model_creator( + TestTable +) diff --git a/src/models/tran_observer_data.py b/src/models/tran_observer_data.py new file mode 100644 index 0000000..01d145e --- /dev/null +++ b/src/models/tran_observer_data.py @@ -0,0 +1,13 @@ +from tortoise import Model, fields +from src.models import with_table_name +from src.models.strategy import Strategy + + +class TranObserverData(Model): + id = fields.IntField(pk=True, ) + key = fields.CharField(max_length=255, ) + tran_observer_data = fields.BinaryField(null=True, description='存储大量数据', ) + + class Meta: + table = with_table_name("tran_observer_data") + diff --git a/src/models/tran_orders.py b/src/models/tran_orders.py new file mode 100644 index 0000000..868ea50 --- /dev/null +++ b/src/models/tran_orders.py @@ -0,0 +1,14 @@ +from tortoise import Model, fields +from src.models import with_table_name +from src.models.strategy import Strategy + + + + +class TranOrders(Model): + id = fields.IntField(pk=True, ) + key = fields.CharField(max_length=255, null=True, ) + order_return = fields.CharField(max_length=255, null=True, ) + +class meta: + table = with_table_name('tradorders') \ No newline at end of file diff --git a/src/models/tran_position.py b/src/models/tran_position.py new file mode 100644 index 0000000..8e1f6ee --- /dev/null +++ b/src/models/tran_position.py @@ -0,0 +1,13 @@ +from tortoise import Model, fields +from src.models import with_table_name +from src.models.strategy import Strategy + + + +class TranPosition(Model): + id = fields.IntField(pk=True, ) + key = fields.CharField(max_length=25, null=True, ) + tran_position_data = fields.BinaryField(null=True, ) + + class Meta: + table = with_table_name("tran_position") diff --git a/src/models/tran_return.py b/src/models/tran_return.py new file mode 100644 index 0000000..f89f4a3 --- /dev/null +++ b/src/models/tran_return.py @@ -0,0 +1,13 @@ +from tortoise import Model, fields +from src.models import with_table_name +from src.models.strategy import Strategy + + +class TranReturn(Model): + id = fields.IntField(pk=True, ) + key = fields.CharField(max_length=255, null=True, ) + tran_return_data = fields.BinaryField(null=True, ) + + class Meta: + table = with_table_name('tran_return') + diff --git a/src/models/tran_trade_info.py b/src/models/tran_trade_info.py new file mode 100644 index 0000000..dc3e742 --- /dev/null +++ b/src/models/tran_trade_info.py @@ -0,0 +1,12 @@ +from tortoise import Model, fields +from src.models import with_table_name +from src.models.strategy import Strategy + + +class TranTradeInfo(Model): + id = fields.IntField(pk=True, ) + key = fields.CharField(max_length=25, null=True, ) + tran_trade_info = fields.BinaryField(null=True, ) + + class Meta: + table = with_table_name("tran_trade_info") diff --git a/src/models/trand_info.py b/src/models/trand_info.py new file mode 100644 index 0000000..dd836c6 --- /dev/null +++ b/src/models/trand_info.py @@ -0,0 +1,30 @@ +from tortoise import Model, fields +from src.models import with_table_name +from src.models.strategy import Strategy + +class TrandInfo(Model): + id = fields.IntField(pk=True, description='id', ) + key = fields.CharField(max_length=255, null=True, description='唯一索引', ) + tran_info_data = fields.BinaryField(null=True, ) + + + class Meta: + table = with_table_name("trand_info") + + + + + + + + + + + + + + + + + + diff --git a/src/models/transaction.py b/src/models/transaction.py new file mode 100644 index 0000000..4cf6516 --- /dev/null +++ b/src/models/transaction.py @@ -0,0 +1,30 @@ +from tortoise import Model, fields +from src.models import with_table_name +from src.models.strategy import Strategy + + +class Transaction(Model): + id = fields.IntField(pk=True, description='主键') + key = fields.CharField(max_length=20, description='key,数据标识') + cash = fields.FloatField(description='资金', null=True) + transaction_name = fields.CharField(max_length=255, description='交易名称') + transaction_type = fields.CharField(max_length=255, null=True, description='交易类型') + user_id = fields.IntField(null=True, description='用户id') + is_running = fields.BooleanField(max_length=255, default=True, description='运行状态') + is_deleted = fields.BooleanField(max_length=255, default=False, description='是否删除') + process_id = fields.IntField(null=True, description='进程号') + bar = fields.CharField(max_length=10, description='频率K线', null=True) + created_at = fields.DatetimeField(auto_now_add=True, description="创建时间") + updated_at = fields.DatetimeField(auto_now=True, description="修改时间") + stopped_at = fields.DatetimeField(null=True, description="停止时间") + deleted_at = fields.DatetimeField(null=True, description="删除时间") + strategy: fields.ForeignKeyRelation[Strategy] = fields.ForeignKeyField( + model_name="models.Strategy", + related_name="transaction_strategy", + # db_constraint=False, + on_delete=fields.CASCADE, + index=True, + ) + + class Meta: + table = with_table_name("Transaction") diff --git a/src/models/transaction_strategy.py b/src/models/transaction_strategy.py new file mode 100644 index 0000000..f472707 --- /dev/null +++ b/src/models/transaction_strategy.py @@ -0,0 +1,19 @@ +from tortoise import Model, fields +from src.models import with_table_name + + +class Strategy(Model): + id = fields.IntField(pk=True, description='主键') + strategy_name = fields.CharField(max_length=255, description='策略名称') + strategy_hash = fields.CharField(max_length=255, description='策略版本号') + strategy_type = fields.CharField(max_length=255, null=True, description='策略类型') + user_id = fields.IntField(null=True, description='所属用户') + backtest_count = fields.IntField(null=True, description='回测次数') + backtest_keys = fields.JSONField(null=True, description='回测key列表') + is_deleted = fields.BooleanField(max_length=255, default=False, description='是否删除') + created_at = fields.DatetimeField(auto_now_add=True, description="创建时间") + updated_at = fields.DatetimeField(auto_now=True, description="修改时间") + deleted_at = fields.DatetimeField(null=True, description="删除时间") + + class Meta: + table = with_table_name("strategy") diff --git a/src/models/user.py b/src/models/user.py new file mode 100644 index 0000000..97f2195 --- /dev/null +++ b/src/models/user.py @@ -0,0 +1,27 @@ +from tortoise import Model, fields +from src.models import with_table_name + + + + +class Users(Model): + avatar_url = fields.CharField(max_length=255, null=True, ) + beta_account_type = fields.CharField(max_length=30, null=True, ) + created_at = fields.DatetimeField(auto_now_add=True, ) + created_user_id = fields.IntField() + dedicated_id = fields.IntField() + deleted_at = fields.DatetimeField(null=True, ) + id = fields.IntField(pk=True, ) + invitation_code = fields.CharField(max_length=10, null=True, ) + invited_user_id = fields.IntField() + is_deleted = fields.IntField(index=True, ) + login_at = fields.DatetimeField(null=True, ) + member_type = fields.IntField() + nickname = fields.CharField(max_length=30, null=True, ) + pre_cost_time = fields.IntField() + qr_code = fields.CharField(max_length=255, null=True, ) + status = fields.BooleanField(null=True, default=False, ) + updated_at = fields.DatetimeField(auto_now_add=True, ) + + class Meta: + table = with_table_name("users") \ No newline at end of file diff --git a/src/models/wance_data_stock.py b/src/models/wance_data_stock.py new file mode 100644 index 0000000..17d6e63 --- /dev/null +++ b/src/models/wance_data_stock.py @@ -0,0 +1,43 @@ +from tortoise import Model, fields +from src.models import with_table_name + + +class WanceDataStock(Model): + id = fields.IntField(pk=True, ) + stock_name = fields.CharField(max_length=50, null=True, description='股票名称', ) + stock_code = fields.CharField(max_length=50, null=True, description='股票代码', ) + stock_sector = fields.JSONField(null=True, description='股票板块', ) + stock_type = fields.JSONField(null=True, description='股票类型', ) + time_start = fields.CharField(max_length=10, null=True, description='上市时间', ) + time_expire = fields.CharField(max_length=10, null=True, description='退市时间', ) + time_end = fields.CharField(max_length=10, null=True, description='上一次回测结束时间', ) + market_sector = fields.CharField(max_length=20, null=True, description='所属市场', ) + financial_dividend = fields.FloatField(null=True, default=0, description='分红率', ) + financial_ex_gratia = fields.FloatField(null=True, default=0, description='扣非后每股收益', ) + financial_cash_flow = fields.FloatField(null=True, default=0, description='每股现金流', ) + financial_asset_value = fields.FloatField(null=True, default=0, description='每股净资产', ) + financial_reserve_per = fields.FloatField(null=True, default=0, description='每股资本公积金', ) + financial_undistributed_profit = fields.FloatField(null=True, default=0, description='每股未分配利润', ) + profit_asset_value = fields.FloatField(null=True, default=0, description='净资产收益率', ) + profit_sale_ratio = fields.FloatField(null=True, default=0, description='盈利销售净利率', ) + profit_gross_rate = fields.FloatField(null=True, default=0, description='销售毛利率', ) + profit_business_increase = fields.FloatField(null=True, default=0, description='营业收入增长率', ) + profit_dividend_rate = fields.FloatField(null=True, default=0, description='股息率', ) + growth_Income_rate = fields.FloatField(null=True, default=0, description='营业总收入同比增长率', ) + growth_growth_rate = fields.FloatField(null=True, default=0, description='营业利润同比增长率', ) + growth_nonnet_profit = fields.FloatField(null=True, default=0, description='扣非净利润增长率', ) + growth_attributable_rate = fields.FloatField(null=True, default=0, description='归母净利润同比增长率', ) + valuation_PEGTTM_ratio = fields.FloatField(null=True, default=0, description='市盈率TTM', ) + valuation_PEG_percentile = fields.FloatField(null=True, default=0, description='市盈率百分位', ) + valuation_PB_TTM = fields.FloatField(null=True, default=0, description='市净率TTM', ) + valuation_PB_percentile = fields.FloatField(null=True, default=0, description='市净率百分比', ) + valuation_PTS_TTM = fields.FloatField(null=True, default=0, description='市销率TTM', ) + valuation_PTS_percentile = fields.FloatField(null=True, default=0, description='市销率百分位', ) + valuation_market_TTM = fields.FloatField(null=True, default=0, description='市现率TTM', ) + valuation_market_percentile = fields.FloatField(null=True, default=0, description='市现率百分比', ) + market_indicator = fields.FloatField(null=True, default=0, description='行情指标', ) + + + + class Meta: + table = with_table_name("wance_data_stock") \ No newline at end of file diff --git a/src/models/wance_data_storage_backtest.py b/src/models/wance_data_storage_backtest.py new file mode 100644 index 0000000..02914cc --- /dev/null +++ b/src/models/wance_data_storage_backtest.py @@ -0,0 +1,67 @@ +from tortoise import Model, fields +from src.models import with_table_name + +class WanceDataStorageBacktest(Model): + avg_down_month = fields.FloatField(null=True, default=0, description='回测期间每个月下跌时的平均损失', ) + avg_drawdown = fields.FloatField(null=True, default=0, description='平均回撤,表示每次回撤的平均幅度', ) + avg_drawdown_days = fields.FloatField(null=True, default=0, description='平均回撤持续的天数', ) + avg_up_month = fields.FloatField(null=True, default=0, description='回测期间每个月上涨时的平均收益', ) + backtest_end_time = fields.IntField(description='回测结束时间', ) + backtest_name = fields.CharField(max_length=100, null=True, ) + best_day = fields.FloatField(null=True, default=0, description='回测期间单日的最大回报', ) + best_month = fields.FloatField(null=True, default=0, description='回测期间的最佳月份回报', ) + best_year = fields.FloatField(null=True, default=0, description='回测期间的最佳年度回报(若为空,表示回测不足一年)', ) + cagr = fields.FloatField(null=True, default=0, description='年化复合增长率,表示投资在整个回测期间的年均增长率', ) + calmar = fields.FloatField(null=True, default=0, description='Calmar 比率,表示年化收益率与最大回撤的比率,用于衡量风险调整后的收益。Calmar 比率越高,意味着风险调整后的表现越好', ) + daily_kurt = fields.FloatField(null=True, default=0, description='每日回报的峰度,表示回报分布的尖锐程度', ) + daily_mean = fields.FloatField(null=True, default=0, description='每日的平均回报率', ) + daily_price = fields.JSONField(null=True, description='每日的价格', ) + daily_sharpe = fields.FloatField(null=True, default=0, description='日频率的夏普比率,表示每单位风险所获得的超额收益', ) + daily_skew = fields.FloatField(null=True, default=0, description='每日回报的偏度,表示回报分布的对称性', ) + daily_sortino = fields.FloatField(null=True, default=0, description='日频率的 Sortino 比率,衡量投资组合的下行风险,考虑的是负波动率', ) + daily_vol = fields.FloatField(null=True, default=0, description='每日回报率的标准差(波动率),表示收益的波动性', ) + data_end_time = fields.CharField(max_length=10, null=True, description='回测数据结束时间', ) + data_start_time = fields.CharField(max_length=10, null=True, description='回测数据开始时间', ) + five_year = fields.FloatField(null=True, default=0, description='过去五年的回报', ) + id = fields.IntField(pk=True, ) + incep = fields.FloatField(null=True, default=0, description='自策略开始运行以来的年化回报率(如果策略没有完整的一年,则等于 cagr)', ) + indicator_information = fields.JSONField(null=True, description='指标信息', ) + indicator_type = fields.CharField(max_length=20, null=True, description='指标类型', ) + max_drawdown = fields.FloatField(null=True, default=0, description='最大回撤,表示投资组合在回测期间从最高点到最低点的最大亏损百分比', ) + monthly_kurt = fields.FloatField(null=True, default=0, description='月度回报的峰度', ) + monthly_mean = fields.FloatField(null=True, default=0, description='月度平均回报率', ) + monthly_sharpe = fields.FloatField(null=True, default=0, description='月频率的夏普比率', ) + monthly_skew = fields.FloatField(null=True, default=0, description='月度回报的偏度', ) + monthly_sortino = fields.FloatField(null=True, default=0, description='月频率的 Sortino 比率', ) + monthly_vol = fields.FloatField(null=True, default=0, description='月度回报率的标准差(波动率)', ) + mtd = fields.FloatField(null=True, default=0, description='月内截至目前的回报', ) + one_year = fields.FloatField(null=True, default=0, description='的回报', ) + position = fields.JSONField(null=True, description='持仓记录', ) + price = fields.JSONField(null=True, description='每日的余额', ) + returns = fields.JSONField(null=True, description='每日的回报率', ) + rf = fields.FloatField(null=True, default=0, description='无风险收益率,通常用来计算夏普比率等指标,通常为0表示忽略无风险收益', ) + six_month = fields.FloatField(null=True, default=0, description='过去六个月的回报(如果为空,表示无相关数据', ) + stock_close_price = fields.JSONField(null=True, description='股票收盘价', ) + stock_code = fields.CharField(max_length=20, null=True, ) + strategy_name = fields.CharField(max_length=50, null=True, description='回测的策略名称', ) + ten_year = fields.IntField(description='过去十年的回报', ) + three_month = fields.FloatField(null=True, default=0, description='过去三个月的回报', ) + three_year = fields.FloatField(null=True, default=0, description='过去三年的回报', ) + total_return = fields.FloatField(null=True, default=0, description='总回报率', ) + twelve_month_win_perc = fields.FloatField(null=True, default=0, description='过去12个月的胜率,表示在过去12个月中有多少月的回报为正', ) + win_year_perc = fields.FloatField(null=True, default=0, description='策略年度胜率,表示回测期间策略表现优于无风险收益率的年份百分比', ) + worst_day = fields.FloatField(null=True, default=0, description='回测期间单日的最差回报', ) + worst_month = fields.FloatField(null=True, default=0, description='回测期间的最差月份回报', ) + worst_year = fields.FloatField(null=True, default=0, description='回测期间的最差年度回报(若为空,表示回测不足一年)', ) + yearly_kurt = fields.FloatField(null=True, default=0, description='年度的峰度', ) + yearly_mean = fields.FloatField(null=True, default=0, description='年度的平均回报', ) + yearly_sharpe = fields.FloatField(null=True, default=0, description='年度的夏普比率', ) + yearly_skew = fields.FloatField(null=True, default=0, description='年度的偏度', ) + yearly_sortino = fields.FloatField(null=True, default=0, description='年度的Sortino 比率', ) + yearly_vol = fields.FloatField(null=True, default=0, description='年度的波动率', ) + ytd = fields.FloatField(null=True, default=0, description='年初至今的回报', ) + + + + class Meta: + table = with_table_name("wance_data_storage_backtest") \ No newline at end of file diff --git a/src/pydantic/__pycache__/backtest_request.cpython-311.pyc b/src/pydantic/__pycache__/backtest_request.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8b44f5889d95e44d104d4a18f8c50571e93dbff3 GIT binary patch literal 3881 zcmbW4TW}NC8Gu*v%9d>m*s>(s7-KH3>rjZnh9m?sQ(_Fk0W&Dfq^_omtg|+n=wenY zQ@a+UawS%|Bu%9OG=6uxW<}(?D|U~!~BhO7B5}q=EYvP2{DL48Ykn@bZA`K4y}gt zwaz>j+rheY9Xbu8W%e^D?;L|zo*m0$n1}FHua11AvoH<$Y0_=b%Iy0NuPE!=dqjuF zZF9nt!o4;xf5?M)r>tvr@J?jUsM0@drt{)MFd1TarbC064(%~;$>;Gb(mc|RX~7|U z)vH6t=ko=Xi(Q7-qrBbF%kXRtUzo$|7I~D93Lfc~*)QS?z-D2VDMH0LOfh9vWSJ7Q zGKX2gmn@5O)e?Sn4!<&oFU|60$dJRY;#WhQPSp}- zO%78|nd&9X+8oA6nVKxK4%Ox`CK}KBEVBV^%wf!w*_37K(9d$13hJvq%lsTQ=g z&TCm_Gx|jiQ$^QsOP1M+Ue96HP^K}0hl-W%fv$2HJkI*L< zBi?Hl9JJi!qApV3oD+@!-%jwcaOO_xi|Bv#p<<~|&#BJjBCqJNe?-mn7dAFFHZ?YG zwH#PtF){mjD)RgP5cM*P5O_f*aDkkOiyrK7D#_Yy$oCpN%*^zYX!5h$U|JbFQ(M0c z)z`xMr0&fmLtmt3o@D~e@8Jc92g!_Y4_V2%fO7v*GW1ykOG`^V45Vkn3*+}-Aob-8ENJS{SYqlc5{gX_Inm+bskPMX)#T{y|JRzHcOz;m z{ZF`j&DnV+NAE!JOQWdWvMn<=b-$Ar1If=WW@f0fdwgzDX5Ah)@7q8oIdma4b2s&L zR=IU4Yefmn(RmW8UM@k7UI(H9^!4x`_%;&f;U`LPOu6$=Ie%U?omD299|%5PF6?qR zogA`>HecCGmAkjngRqL17Ut*W5{KI^@Gjmh+MLclU&TubmOl7Ixp`}0;;b?_4<2N_ z9hgo?cADiayR3Zj+sWjO>4l-&Tfm}n`9@;qUNU?&b#ch*^YQ}m%efqGhn3tRhoA}dn`DprVRIMqw$l`f8mgW+zDj-#Z7V zVe00T@?<*wFq}FwO&p9oSvWhN9=)5G`bJGTxyaMY3!NTc_X#mwv&4roeB*!ek(j^o z3LhiG%G|W9CqBF$CrZOh93z)_9o_I^mD!KO4eMn+NAry1NKH zc@DQIm`RtEqRe)BJWheUk_lBrtRbSBh_ytlBVs)f-SFaE6o7jX~`fzo>RNWt|?hhOcvO&wWLd^CamTFJOYEOf{K5V#Fg4xs1B=&T?W@DI- z?!~)*hie9;nt@o&K;Xd8iJ|0)CyxrCm~KSFE%vuy3ddvt7_6 zwkuv)6E;SA@!K4(JT6rpk5wKI916ntacB~IT%AV?-uY{6vPvdv%w!F`HzZ=#3Qc0I z@v3#Yy)j48M64 zn=O*r5;I%KUSQS&O=2x^Lq%{$#E6@{*dR&%RkQ`(4{pS48#IY+i&Og+GHz~@%xy7q8~8UY8$bG-VRp7^L00V7eoy5OR1y}R z>h_Z{kT3>P4a$JG4`72|GWcT#zZ%p}=DZhFcI+dH=2oHz5{!Z*iA2m%haid3RxMr0 z$Oaf_dY32}TgU)NPy$I7l_<54F_1*5MXPTANqF=I4YBF~82|}NAW13^`~X#=W7&K4 zxv*ogoDMe>$ocRS1E)4UO6UP0>lbTLApc&{Ge@n!X40j~) literal 0 HcmV?d00001 diff --git a/src/pydantic/__pycache__/codelistrequest.cpython-311.pyc b/src/pydantic/__pycache__/codelistrequest.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bc8a46fcf89dfbbd5689daeb185c928f99c56dae GIT binary patch literal 549 zcmZutF;Buk6n@th0z^a;7Z@BIOj!5zfu???fo=4F zo7}2?SMnSqO;pT4PcyEx(dgWl>XHvfT&lR}VS)yj>D2Rafzr_Jb4ue9rTvJF0?oH5 z9gf^!vt#Mv*py1d6-wPOjFhYVD3p{w!%8<^x7tTePjVrh6CMNz@Pdo%uQ zARWlIq+2|Um$}UdzAqR7;lTGkhWQCT)gv8jWd#2=lL)*fjs&vNmZ)lW@WG)b$`Y5c zeeX*rHo7J9TEc6$Zg48TCkr@Z`#%tIM&Ri@m9GHpyb%z|4l`+wz@)v8A5lAwbiN?IMnPf44D1Sn~DkQPch93)6d$U#~ux#S>ilyo{sJ0)E} zx|?dyK}py#CPYaOkVw;*OO*6F#&lBB=OA5_^gBp5B~b?nQ!?NnJ(LUrY0fP|$&iEe zQZfu=q;XW!+WKhJ=v$-uY1EkGEKy3v9b|x#2?rUZWD-d89XdqGl!FXY5_6CdO5zSO zO3Ac?j8QV-bi2?v>^WDdw>9KrrQZ@tEK3!cC!8kxK>GDaikFN}=S$b}0d zr)lIOjdbMtzj@D8N5ZZ(YqxD{GXM1B99B;5+r4|D#7Vr!WmryCvBo~TRMYIHUdRH1D_PcDDpy^L zDBEo+sXxoZJE3MvTo&6MRX@##MrkL95T%K*LRI4wc+FI7Qu_9)b$lo)*d8L+4T>*I zEL|5=oX3(TW*}P!eDvs^-Cy@U+#naI7T(naNNBXdvw6EQwy%jmELk=zvT?+t?Zy$^ z4I_Y%>Am}!?agSmZy&C;F9XuDi?t(+r9{dOvMebN%i3)$o0Wwe6q2<&SoT?t%T$Td z4(~+;-k^-_hdY(i>`s>DBuUmP6;WAM=^zmyBA1AC66qq+O(aaDhe(7-FNp0EB~9rg z-Tq1!ZdWBrx@1F828j$2874A9WR%Dl5%Oa}87DG9WRl1fkriv z;+6SJexF8L_aWcXK)^{lNL&rr)Ki^^lQe=vl}Lwfo7o@_j9{u>3Z(O zyoq}C)B7w zRVIiSd|{x}HD#hXXcn4ton>?~f9Lc-S6^h|B0#gyWO*W<|45G-Q)eS4f_dW>ikFvH zO*B@FzKXwu3lH|Hg;vY1D_vYR(U9?^hzqQVz+Se{vU{0btG95)M1#hi;^P9`4Palf z&=vQ)!L@qvMCBSsbl$kCe^Tn6CSc7I&6bxmp_UPSDwAumK?nP<8x_^-U zOdu`Z?niec(^x{i}{1dSV`QzU5g3Wk4#~{Ri~=j)MRI literal 0 HcmV?d00001 diff --git a/src/pydantic/__pycache__/request_data.cpython-311.pyc b/src/pydantic/__pycache__/request_data.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8b512bc744047dbaf8f06f3a64bb7cc987186363 GIT binary patch literal 2640 zcmbW3|8o;X6u>XZrcGN~XlYVfVhLa=)d7nj!yxzr7DN!zu`r`C<27VAa6`V7%U!j# z=|lu614fIBN@x9XpDcY9r3N0y7WS7cn(tF4$?9!OSmWY7NFYgL$rq zu^UWdfmwiEMU2CUXJLU^gclbvbq4c%foZ}^ikNwZuV%*JrBHq?YDJq*^1V&=$s{Vt zE8)5J?Vx`Bwl*+0c_H=d_|fdi)Wq25`q4w$_0yA|-_eep(oS5N7(1hXIjVnlJqJUv z&c-C*lYOGYlexbd&$Y`7hxiMc?=Soy*4s&_ZADqCrZ2|got4+ zd+&rkbpHQZvym`1Y)$f}a_MYuSqY-g56-KOEQ39?tL3>W|1_Nk!pE zn8=IRCI>#vj(wHAc~3icq+mshj~n}BRI^%%g>C^G0rYf{9r9x4Jb6P)jA$3GY6lPI zOcxfjl{-Y4sAT~m800b3r(E-7$yev54nY==Oy0k*RtjOiNJ1nm`GUb7dEOHWQlGe? zojEr-`iXYvK6p@VeqeU_{CkZmZ(Hjn`t3XVsUIc>&MyOt+R;-JV;A-0iR|G4uN);J z^UH^Xu;67qEQq-v@@1dY_NONA+|YhJpxO+Q0kfF@TUYcuH{ra1&)kEd2ink}miR1t zX;iI<5_UZJurEY5&4@RDvJR%EK1t=ylv*xC{gF^tkVs^EOTB92jYT}Kmh*flg5@BA z{w$u~C;NiT9M6kzS#c_2r%bGXuE;JM)dC}RHqZOQ;fUm8w@VaI7OGgq)EuT*enobr zh;x~;gHkO*SQ6{lkmXfHyCRXG$nFoZfvNdSIhlG6)Mf55Xzq{KK|yZc5q#m@TR?pX z{&st{(PL9XU+CwLdq41n{ls{u-l*vJcJ*LiSQ7kRG4~h9m|5P{Big`@q1c|H4^12w zpYh2!7bZ83K2O_vsIymb_NJY^@hu6I=r~zMQ7?#sdJ9Y+b;cBDEbWXvVPX`;6co!i z<|oMTR=QrMj&8-#opy8sW1*-UL_yt|nz}^OFrqKNPiuIkhELb<@l6A-Q^bQPh|jp1 zhOMbx^tBju^((Iaw5uQ1+mdrnR#MatqM-gv_FSDTVMAl_Qt16@TbDZ@uZb_h5K4!OMnOHYm`tV& jw~#(de&(9!v*b}pm1&3R5%+AJX!*^U`TZZ}jiCMpuoAgH literal 0 HcmV?d00001 diff --git a/src/pydantic/backtest_request.py b/src/pydantic/backtest_request.py new file mode 100644 index 0000000..905e025 --- /dev/null +++ b/src/pydantic/backtest_request.py @@ -0,0 +1,29 @@ +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class BackRequest(BaseModel): + field_list: List[str] = Field(default_factory=list, description="字段列表,用于指定获取哪些数据字段") + stock_list: List[str] = Field(default_factory=list, description="股票列表,用于指定获取哪些股票的数据") + stock_code: str = Field(default="000300.SH", description="股票代码,用于指定获取哪些股票的数据") + period: str = Field(default='1d', description="数据周期,如 '1d' 表示日线数据") + start_time: Optional[str] = Field(default='', description="开始时间,格式为 'YYYY-MM-DD',默认为空字符串") + end_time: Optional[str] = Field(default='', description="结束时间,格式为 'YYYY-MM-DD',默认为空字符串") + count: int = Field(default=-1, description="数据条数,默认为 -1 表示获取所有数据") + dividend_type: str = Field(default='none', description="分红类型,默认值为 'none'") + fill_data: bool = Field(default=True, description="是否填充数据,默认为 True") + incrementally: bool = Field(default=True, description="是否增量下载") + callback: bool = Field(default=True, description="是否开启回调函数") + data_dir: str = Field(default="D:\\e海方舟-量化交易版\\userdata_mini\\datadir", description="数据存储路径") + sector_name:str = Field(default="沪深指数", description="板块名称") + iscomplete: bool = Field(default=False, description="是否获取全部字段") + ma_type: str = Field(default='SMA', description="移动平均线类型,如 'SMA' 表示简单移动平均线") + short_window: int = Field(default=50, description="短周期线长度") + long_window: int = Field(default=200, description="长周期线长度") + bollingerMA: int = Field(default=50, description="布林带中的移动平均线周期,决定计算均值的时间窗口长度") + std_dev: int = Field(default=200, description="布林带中用于计算上下轨的标准差倍数,影响带宽大小") + overbought: int = Field(default=70, description="超买区间的RSI阈值,表示价格处于相对高点,可能面临回调") + oversold: int = Field(default=30, description="超卖区间的RSI阈值,表示价格处于相对低点,可能面临反弹") + signal_window: int = Field(default=9, description="超卖区间的RSI阈值,表示价格处于相对低点,可能面临反弹") + diff --git a/src/pydantic/codelistrequest.py b/src/pydantic/codelistrequest.py new file mode 100644 index 0000000..2b0de4e --- /dev/null +++ b/src/pydantic/codelistrequest.py @@ -0,0 +1,5 @@ +from pydantic import BaseModel + + +class CodeListRequest(BaseModel): + code_list : list[str] \ No newline at end of file diff --git a/src/pydantic/factor_request.py b/src/pydantic/factor_request.py new file mode 100644 index 0000000..09af0ab --- /dev/null +++ b/src/pydantic/factor_request.py @@ -0,0 +1,140 @@ +from typing import List, Optional, Any, Dict + +from pydantic import BaseModel, Field + + +# 定义查询条件模型,覆盖所有字段 +class StockQuery(BaseModel): + financial_asset_value: Optional[float] = None + financial_cash_flow: Optional[float] = None + financial_dividend: Optional[float] = None + financial_ex_gratia: Optional[float] = None + financial_reserve_per: Optional[float] = None + financial_undistributed_profit: Optional[float] = None + growth_attributable_rate: Optional[float] = None + growth_growth_rate: Optional[float] = None + growth_Income_rate: Optional[float] = None + growth_nonnet_profit: Optional[float] = None + id: Optional[int] = None + market_indicator: Optional[float] = None + market_sector: Optional[str] = None + profit_asset_value: Optional[float] = None + profit_business_increase: Optional[float] = None + profit_dividend_rate: Optional[float] = None + profit_gross_rate: Optional[float] = None + profit_sale_ratio: Optional[float] = None + stock_code: Optional[str] = None + stock_name: Optional[str] = None + stock_sector: Optional[List[str]] = None + stock_type: Optional[List[str]] = None + time_start: Optional[str] = None + time_end: Optional[str] = None + time_expire: Optional[str] = None + valuation_market_percentile: Optional[float] = None + valuation_market_TTM: Optional[float] = None + valuation_PB_percentile: Optional[float] = None + valuation_PB_TTM: Optional[float] = None + valuation_PEG_percentile: Optional[float] = None + valuation_PEGTTM_ratio: Optional[float] = None + valuation_PTS_percentile: Optional[float] = None + valuation_PTS_TTM: Optional[float] = None + gt: Optional[Dict[str, float]] = None # 大于 + lt: Optional[Dict[str, float]] = None # 小于 + gte: Optional[Dict[str, float]] = None # 大于等于 + lte: Optional[Dict[str, float]] = None # 小于等于 + between: Optional[Dict[str, Dict[str, float]]] = None # 在某两个值之间 + +""" +# 这里是因子pydantic的定义 + +class FactorRequest(BaseModel): + financial_asset_value: Optional[Dict[str, Any]] = Field(None, description='每股净资产') + financial_cash_flow: Optional[Dict[str, Any]] = Field(None, description='每股现金流') + financial_dividend: Optional[Dict[str, Any]] = Field(None, description='分红率') + financial_ex_gratia: Optional[Dict[str, Any]] = Field(None, description='扣非后每股收益') + financial_reserve_per: Optional[Dict[str, Any]] = Field(None, description='每股资本公积金') + financial_undistributed_profit: Optional[Dict[str, Any]] = Field(None, description='每股未分配利润') + growth_attributable_rate: Optional[Dict[str, Any]] = Field(None, description='归母净利润同比增长率') + growth_growth_rate: Optional[Dict[str, Any]] = Field(None, description='营业利润同比增长率') + growth_Income_rate: Optional[Dict[str, Any]] = Field(None, description='营业总收入同比增长率') + growth_nonnet_profit: Optional[Dict[str, Any]] = Field(None, description='扣非净利润增长率') + market_indicator: Optional[Dict[str, Any]] = Field(None, description='行情指标') + market_sector: Optional[str] = Field(None, description='所属市场') + profit_asset_value: Optional[Dict[str, Any]] = Field(None, description='净资产收益率') + profit_business_increase: Optional[Dict[str, Any]] = Field(None, description='营业收入增长率') + profit_dividend_rate: Optional[Dict[str, Any]] = Field(None, description='股息率') + profit_gross_rate: Optional[Dict[str, Any]] = Field(None, description='销售毛利率') + profit_sale_ratio: Optional[Dict[str, Any]] = Field(None, description='盈利销售净利率') + stock_code: Optional[str] = Field(None, description='股票代码') + stock_name: Optional[str] = Field(None, description='股票名称') + stock_sector: Optional[List[str]] = Field(None, description='股票板块') + stock_type: Optional[dict] = Field(None, description='股票类型') + time_end: Optional[str] = Field(None, description='上一次回测结束时间') + time_expire: Optional[str] = Field(None, description='退市时间') + time_start: Optional[Dict[str, Any]] = Field(None, description='上市时间') + valuation_market_percentile: Optional[Dict[str, Any]] = Field(None, description='市现率百分比') + valuation_market_TTM: Optional[Dict[str, Any]] = Field(None, description='市现率TTM') + valuation_PB_percentile: Optional[Dict[str, Any]] = Field(None, description='市净率百分位') + valuation_PB_TTM: Optional[Dict[str, Any]] = Field(None, description='市净率TTM') + valuation_PEG_percentile: Optional[Dict[str, Any]] = Field(None, description='市盈率百分位') + valuation_PEGTTM_ratio: Optional[Dict[str, Any]] = Field(None, description='市盈率TTM') + valuation_PTS_percentile: Optional[Dict[str, Any]] = Field(None, description='市销率百分位') + valuation_PTS_TTM: Optional[Dict[str, Any]] = Field(None, description='市销率TTM') +""" + +""" +使用示例: + factor_request = FactorRequest( + { + "financial_asset_value": 10.7169, + "financial_cash_flow": -0.42982, + "gt": { + "profit_gross_rate": 4.0, + "valuation_PB_TTM": 0.8 + }, + "lt": { + "market_indicator": -0.6 + }, + "gte": { + "valuation_market_percentile":5.38793 + }, + "lte": { + "valuation_PEGTTM_ratio": 15.2773 + }, + "between": { + "financial_dividend": { + "min": 1.0, + "max": 2.0 + }, + "growth_Income_rate": { + "min": -0.6, + "max": 1.0 + } + }, + "stock_code": "600051.SH", + "stock_name": "宁波联合", + "stock_sector": ["Shanghai A-shares", "Shenzhen A-shares"], + "stock_type": ["stock"], + "time_start": "0", + "time_end": "20240911", + "valuation_PEG_percentile": 15.2222, + "valuation_PTS_TTM": 2.3077 + } + ) + + result = await WanceDataStock.dynamic_query(**factor_request.dict(exclude_none=True)) + print(result) + + +比较大小字段说明: + 'gt' 对应 __gt(大于) + 'lt' 对应 __lt(小于) + 'gte' 对应 __gte(大于等于) + 'lte' 对应 __lte(小于等于) + 'between' 对应在某个值之间的条件,使用 __gt 和 __lt 组合实现 + +时间字段说明: + time_start={'recent_years': 2} 近2年 + time_start={'recent_months': 2} 近2月 + +""" diff --git a/src/pydantic/request_data.py b/src/pydantic/request_data.py new file mode 100644 index 0000000..381048c --- /dev/null +++ b/src/pydantic/request_data.py @@ -0,0 +1,21 @@ +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class DataRequest(BaseModel): + field_list: List[str] = Field(default_factory=list, description="字段列表,用于指定获取哪些数据字段") + stock_list: List[str] = Field(default_factory=list, description="股票列表,用于指定获取哪些股票的数据") + stock_code: str = Field(default="000300.SH", description="股票代码,用于指定获取哪些股票的数据") + period: str = Field(default='1d', description="数据周期,如 '1d' 表示日线数据") + start_time: Optional[str] = Field(default='', description="开始时间,格式为 'YYYY-MM-DD',默认为空字符串") + end_time: Optional[str] = Field(default='', description="结束时间,格式为 'YYYY-MM-DD',默认为空字符串") + count: int = Field(default=-1, description="数据条数,默认为 -1 表示获取所有数据") + dividend_type: str = Field(default='none', description="分红类型,默认值为 'none'") + fill_data: bool = Field(default=True, description="是否填充数据,默认为 True") + incrementally: bool = Field(default=True, description="是否增量下载") + callback: bool = Field(default=True, description="是否开启回调函数") + data_dir: str = Field(default="D:\\e海方舟-量化交易版\\userdata_mini\\datadir", description="数据存储路径") + sector_name:str = Field(default="沪深指数", description="板块名称") + iscomplete: bool = Field(default=False, description="是否获取全部字段") + diff --git a/src/responses.py b/src/responses.py new file mode 100644 index 0000000..b607ca6 --- /dev/null +++ b/src/responses.py @@ -0,0 +1,68 @@ +import typing +from typing import Generic, Sequence, TypeVar + +from fastapi import status +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse +from pydantic import BaseModel + +T = TypeVar("T") + + +class EntityResponse(BaseModel, Generic[T]): + """实体数据""" + status_code: int = 200 + message: str = "Success" + data: T + + +class ListResponse(BaseModel, Generic[T]): + """列表数据""" + status_code: int = 200 + message: str = "Success" + data: Sequence[T] + + +# 包装响应的 Pydantic 模型 +# class EntityPageResponse(BaseModel, Generic[T]): +# status_code: int +# message: str +# data: PageData + + +def response_entity_response(data, status_code=200, message="Success") -> EntityResponse: + """普通实体类""" + return EntityResponse(data=data, status_code=status_code, message=message) + + +# def response_page_response(data, status_code=200, message="Success") -> EntityPageResponse: +# """普通分页类""" +# return EntityPageResponse(data=data, status_code=status_code, message=message) + + +def response_list_response(data, status_code=200, message="Success") -> ListResponse: + """普通列表数据""" + return ListResponse(data=data, status_code=status_code, message=message) + + +def response_success(message: str = 'Success', + headers: typing.Optional[typing.Dict[str, str]] = None) -> JSONResponse: + """成功返回""" + return JSONResponse( + status_code=status.HTTP_200_OK, + headers=headers, + content=jsonable_encoder({ + "status_code": status.HTTP_200_OK, + "message": message, + })) + + +def response_unauthorized() -> JSONResponse: + """未登录""" + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + headers={"WWW-Authenticate": "Bearer"}, + content=jsonable_encoder({ + "status_code": status.HTTP_401_UNAUTHORIZED, + "message": '用户认证失败', + })) diff --git a/src/settings/__init__.py b/src/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/settings/__pycache__/__init__.cpython-311.pyc b/src/settings/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c285bd87ec4539dbd51149bdfbfaf00b9e566805 GIT binary patch literal 168 zcmZ3^%ge<81hWp!Oasx6K?DpiLK&agfQ;!3DGb33nv8xc8H$*I{LdiCU)Ii6F`>n& zMa3~LsW~}7qBy%GzaXZfq_8wGuOz-CwYVgvxF|WMIJKlCGcUb3CO$qhFS8^*Uaz3? l7l%!5eoARhs$CH)&@7N`#r#0x12ZEd;|B&9QN#=s0|5AHDgFQe literal 0 HcmV?d00001 diff --git a/src/settings/__pycache__/config.cpython-311.pyc b/src/settings/__pycache__/config.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b205db1a4ce48895fd1b80c442cfa05eda1ff364 GIT binary patch literal 2084 zcmZ`(&2Jh<6rY9R8;k+Rj*Y1?QJj*faa}1<8by_eAqi;=F5t9Xp^hj!PH~rAy1R}F z$^lh5xKa*Os+3g9IaQS&dgx!#(;`_a8mSUePd$~$J>}FlLm*Dl4v#nQ{pPpx@!p&H zF%poK4)cwSX&{O!2o9uHS>U#pt(+PxRzv7|(y}$%eyo&F!Qx4J) zouSgMV{tNoXjt)I0Z$2(Q9Ae+$I}vXX0&$N!Mr7^G|a52v2asYs7mhE)P_>CbOSiS zt2L9Bsbw{^J0|#noOZWi=vtF%R((1@7?YhI1tordieyPavIF8E$xe!?OL6cW2+KLO zu&~@P>Js~N%O1h-2;95pn=B#wsTWvY!ScxgiWOhS1M|7YC!;}%kDb@01YR&h$CyDe z>+8&rV9v-hV&$;#X63N(BEpNxvor!L#kdD~h$?eGpLhc2de_DO4nr)Q%~Ud1GvyrFF6FM}KViWsuv5ClVpDIkl*?3dgoD0PE}JiNv*T;F zYpvG#y53}gavsLB#jQ-f09h&A&XeDDetazR9|ItlQ579le2mQG1O?k#V;EoAy zhAqhSq$$#?k^e@zhak$1LMKv&qBPpchMClKD``?Ijg49Ojs+SfQD{rm+f*?k9P!_- zsrxi%7^2FtN!4AGFQ0t={^sTCj!6x(nx(1=%KXyOTh*_vul8%2MJ#GsRnw?fVR~^X zs&$dv^Q|^pd~tIqF~v@ZrfWpi>ov8L9GAfP|7E-di<|F&>Y`yFd>Duy2F{F-Epy8dcwwB z`uN0PaoNUP`gnN|Ua&EjK3*6uq`DvWVm3~J^l@sqa`tfL0zh#aUjXUj3&X@}_h!%8 zQ@-=tcon3NSBKI0p80*+#z2eraeQzZ?DA`EH;lk2*ttA^T_3_dm zu?Cy#nKp)gt&i7+%d2pJ%F9(;b6xW2jO|MGU8zG?YUIGKxe=PEXHMik^(H)y7mrXU z^l?qA)8t26m1Jf+-|>mNsOWVw?PM;iVX2h)3|+TKt7d({ynKZ08!8LHBSFSxX%^D6 z%x$$>@O~To-Z5{htw13+>f=oFOtHuWgTftGwc+M?2nrou8_l%f3sV$?$RG1gc_Q=c zoQ#!5-7?m|G|q5!j;m?%pL?&2Yw-i)A`I}mX8s0h3Pt=LWO!kq56hAYUtv bZs4HOJJmCLZ$J190EwfL "Config": + if self.ENVIRONMENT.is_deployed and not self.SENTRY_DSN: + raise ValueError("Sentry is not set") + + return self + + +settings = Config() + +# fastapi/applications.py +app_configs: dict[str, Any] = { + "title": "Wance QMT API", + "root_path": settings.APP_ROUTER_PREFIX, + "docs_url": "/api/docs", +} + +# app_configs['debug'] = True +# if settings.ENVIRONMENT.is_deployed: +# app_configs["root_path"] = f"/v{settings.APP_VERSION}" +# +# if not settings.ENVIRONMENT.is_debug: +# app_configs["openapi_url"] = None # hide docs diff --git a/src/tests/__init__.py b/src/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/tests/stock_query.py b/src/tests/stock_query.py new file mode 100644 index 0000000..49a6b1c --- /dev/null +++ b/src/tests/stock_query.py @@ -0,0 +1,37 @@ +import asyncio +import json + +from src.models import wance_data_stock +from src.tortoises_orm_config import init_tortoise + + +async def init_test(): + await init_tortoise() + + await wance_data_stock.WanceDataStock.create( + stock_code='600051.SH', + stock_name='宁波联合', + stock_sector=['stockA'], # 确保这是一个列表格式 + stock_type=['stock'], + time_start='2010-01-01', + time_end='20240911', + time_expire='2024-09-11', + market_sector='SH' + ) + filters = { + 'stock_sector__contains': ["上证A股"] # 列表形式用于 JSON 数组匹配 + } + # filters = { + # 'stock_sector__contains': ['stockA'] # 列表形式用于 JSON 数组匹配 + # } + # stocks = await wance_data_stock.WanceDataStock.filter(stock_sector__contains=["上证A股"]).all() + # stocks = await wance_data_stock.WanceDataStock.filter(stock_sector__contains=["上证A股", "沪深A股", "TGN共同富裕示范区", "TGN旅游概念", "TGN煤炭概念", "TGN物业管理", "TGN跨境电商", "THY1综合", "THY2综合", "THY3综合", "DY1浙江省", "DY2浙江省宁波市", "上证指数", "上证收益", "上证流通", "中型综指", "中证全指", "中证民企", "中证流通", "国证A指", "新综指", "民企成长", "浙企综指", "浙江民企", "综合指数", "A股指数", "GN共同富裕示范区", "GN婚庆", "GN小盘", "GN微盘股", "GN房地产", "GN批发业", "GN旅游", "GN浙江", "GN煤炭", "GN物业管理", "GN环杭州湾大湾区", "GN电力改革", "GN破净股", "GN舟山自贸区", "GN跨境电商", "GN进口博览会", "SW1综合", "SW1综合加权", "SW2综合", "SW2综合加权", "SW3综合", "SW3综合加权", "A股非科创等权", "上海A股等权", "上海主板等权", "上海全市场等权", "上证指数等权", "上证收益等权", "上证流通等权", "中型综指等权", "中证全指等权", "国证A指等权", "新综指等权", "沪深全市场等权", "综合指数等权", "A股指数等权", "CSRC1批发和零售业", "CSRC2批发业"]).all() + # stocks = await wance_data_stock.WanceDataStock.filter(stock_sector__contains=["stock"]).all() + stocks = await wance_data_stock.WanceDataStock.filter(**filters).all() + + + print(stocks) + + +if __name__ == '__main__': + asyncio.run(init_test()) diff --git a/src/tests/xtquan_data_test.py b/src/tests/xtquan_data_test.py new file mode 100644 index 0000000..bd0c0fa --- /dev/null +++ b/src/tests/xtquan_data_test.py @@ -0,0 +1,78 @@ +# 用前须知 + +## xtdata提供和MiniQmt的交互接口,本质是和MiniQmt建立连接,由MiniQmt处理行情数据请求,再把结果回传返回到python层。使用的行情服务器以及能获取到的行情数据和MiniQmt是一致的,要检查数据或者切换连接时直接操作MiniQmt即可。 + +## 对于数据获取接口,使用时需要先确保MiniQmt已有所需要的数据,如果不足可以通过补充数据接口补充,再调用数据获取接口获取。 + +## 对于订阅接口,直接设置数据回调,数据到来时会由回调返回。订阅接收到的数据一般会保存下来,同种数据不需要再单独补充。 + +# 代码讲解 + +# 从本地python导入xtquant库,如果出现报错则说明安装失败 +from xtquant import xtdata +import time + +# 设定一个标的列表 +code_list = ["000001.SZ"] +# 设定获取数据的周期 +period = "1d" + +# 下载标的行情数据 +if 1: + ## 为了方便用户进行数据管理,xtquant的大部分历史数据都是以压缩形式存储在本地的 + ## 比如行情数据,需要通过download_history_data下载,财务数据需要通过 + ## 所以在取历史数据之前,我们需要调用数据下载接口,将数据下载到本地 + for i in code_list: + xtdata.download_history_data(i, period=period, incrementally=True) # 增量下载行情数据(开高低收,等等)到本地 + + xtdata.download_financial_data(code_list) # 下载财务数据到本地 + xtdata.download_sector_data() # 下载板块数据到本地 + # 更多数据的下载方式可以通过数据字典查询 + +# 读取本地历史行情数据 +history_data = xtdata.get_market_data_ex([], code_list, period=period, count=-1) +print(history_data) +print("=" * 20) + +# 如果需要盘中的实时行情,需要向服务器进行订阅后才能获取 +# 订阅后,get_market_data函数于get_market_data_ex函数将会自动拼接本地历史行情与服务器实时行情 + +# 向服务器订阅数据 +for i in code_list: + xtdata.subscribe_quote(i, period=period, count=-1) # 设置count = -1来取到当天所有实时行情 + +# 等待订阅完成 +time.sleep(1) + +# 获取订阅后的行情 +kline_data = xtdata.get_market_data_ex([], code_list, period=period) +print(kline_data) + +# 获取订阅后的行情,并以固定间隔进行刷新,预期会循环打印10次 +for i in range(10): + # 这边做演示,就用for来循环了,实际使用中可以用while True + kline_data = xtdata.get_market_data_ex([], code_list, period=period) + print(kline_data) + time.sleep(3) # 三秒后再次获取行情 + + +# 如果不想用固定间隔触发,可以以用订阅后的回调来执行 +# 这种模式下当订阅的callback回调函数将会异步的执行,每当订阅的标的tick发生变化更新,callback回调函数就会被调用一次 +# 本地已有的数据不会触发callback + +# 定义的回测函数 +## 回调函数中,data是本次触发回调的数据,只有一条 +def f(data): + # print(data) + + code_list = list(data.keys()) # 获取到本次触发的标的代码 + + kline_in_callabck = xtdata.get_market_data_ex([], code_list, period=period) # 在回调中获取klines数据 + print(kline_in_callabck) + + +for i in code_list: + xtdata.subscribe_quote(i, period=period, count=-1, callback=f) # 订阅时设定回调函数 + +# 使用回调时,必须要同时使用xtdata.run()来阻塞程序,否则程序运行到最后一行就直接结束退出了。 +xtdata.run() \ No newline at end of file diff --git a/src/tortoises.py b/src/tortoises.py new file mode 100644 index 0000000..a9db0fe --- /dev/null +++ b/src/tortoises.py @@ -0,0 +1,185 @@ +import logging +from fastapi import FastAPI +from tortoise.contrib.fastapi import register_tortoise +from src.settings.config import settings + +DATABASE_URL = str(settings.DATABASE_URL) +DATABASE_CREATE_URL = str(settings.DATABASE_CREATE_URL) + + +# 定义不同日志级别的颜色 +class ColoredFormatter(logging.Formatter): + COLORS = { + "DEBUG": "\033[94m", # 蓝色 + "INFO": "\033[92m", # 绿色 + "WARNING": "\033[93m", # 黄色 + "ERROR": "\033[91m", # 红色 + "CRITICAL": "\033[41m", # 红色背景 + } + RESET = "\033[0m" # 重置颜色 + + def format(self, record): + color = self.COLORS.get(record.levelname, self.RESET) + log_message = super().format(record) + return f"{color}{log_message}{self.RESET}" + + +# 配置日志 +logger_db_client = logging.getLogger("tortoise.db_client") +logger_db_client.setLevel(logging.DEBUG) + +# 创建一个控制台处理程序 +ch = logging.StreamHandler() +ch.setLevel(logging.DEBUG) + +# 创建并设置格式化器 +formatter = ColoredFormatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +ch.setFormatter(formatter) + +# 将处理程序添加到日志记录器中 +logger_db_client.addHandler(ch) + +# 定义模型 +models = [ + "src.models.test_table", + "src.models.stock", + "src.models.security_account", + "src.models.order", + "src.models.snowball", + "src.models.backtest", + "src.models.strategy", + "src.models.stock_details", + "src.models.transaction", + "src.models.position", + "src.models.trand_info", + "src.models.tran_observer_data", + "src.models.tran_orders", + "src.models.tran_return", + "src.models.tran_trade_info", + "src.models.back_observed_data", + "src.models.back_observed_data_detail", + "src.models.back_position", + "src.models.back_result_indicator", + "src.models.back_trand_info", + "src.models.tran_position", + "src.models.stock_bt_history", + "src.models.stock_history", + "src.models.stock_data_processing", + "src.models.wance_data_stock", + "src.models.wance_data_storage_backtest" + +] + +aerich_models = models +aerich_models.append("aerich.models") + +# Tortoise ORM 配置 +TORTOISE_ORM = { + "connections": {"default": DATABASE_CREATE_URL, + }, + "apps": { + "models": { + "models": aerich_models, + "default_connection": "default", + }, + }, +} + +config = { + 'connections': { + 'default': DATABASE_URL + }, + 'apps': { + 'models': { + 'models': models, + 'default_connection': 'default', + } + }, + 'use_tz': False, + 'timezone': 'Asia/Shanghai' +} + + +def register_tortoise_orm(app: FastAPI): + # 注册 Tortoise ORM 配置 + register_tortoise( + app, + config=config, + generate_schemas=False, + add_exception_handlers=True, + ) + + +""" +********** +以下内容对原有无改色配置文件进行注释,上述内容为修改后日志为蓝色 +********** +""" + +# import logging +# +# from fastapi import FastAPI +# from tortoise.contrib.fastapi import register_tortoise +# +# from src.settings.config import settings +# +# DATABASE_URL = str(settings.DATABASE_URL) +# DATABASE_CREATE_URL = str(settings.DATABASE_CREATE_URL) +# +# # will print debug sql +# logging.basicConfig() +# logger_db_client = logging.getLogger("tortoise.db_client") +# logger_db_client.setLevel(logging.DEBUG) +# +# models = [ +# "src.models.test_table", +# "src.models.stock", +# "src.models.security_account", +# "src.models.order", +# "src.models.snowball", +# "src.models.backtest", +# "src.models.strategy", +# "src.models.stock_details", +# "src.models.transaction", +# ] +# +# aerich_models = models +# aerich_models.append("aerich.models") +# +# TORTOISE_ORM = { +# +# "connections": {"default": DATABASE_CREATE_URL}, +# "apps": { +# "models": { +# "models": aerich_models, +# "default_connection": "default", +# }, +# }, +# } +# +# config = { +# 'connections': { +# # Using a DB_URL string +# 'default': DATABASE_URL +# }, +# 'apps': { +# 'models': { +# 'models': models, +# 'default_connection': 'default', +# } +# }, +# 'use_tz': False, +# 'timezone': 'Asia/Shanghai' +# } +# +# +# def register_tortoise_orm(app: FastAPI): +# register_tortoise( +# app, +# config=config, +# # db_url=DATABASE_URL, +# # db_url="mysql://adams:adams@127.0.0.1:3306/adams", +# # modules={"models": models}, +# generate_schemas=False, +# add_exception_handlers=True, +# ) diff --git a/src/tortoises_orm_config.py b/src/tortoises_orm_config.py new file mode 100644 index 0000000..b09b70c --- /dev/null +++ b/src/tortoises_orm_config.py @@ -0,0 +1,30 @@ +from tortoise import Tortoise +from tortoise.exceptions import OperationalError +from src.tortoises import TORTOISE_ORM +import asyncio + + +async def init_tortoise(): + """ + 初始化 Tortoise-ORM 数据库连接 + """ + try: + await Tortoise.init(config=TORTOISE_ORM) + # print("Tortoise-ORM initialized successfully.") + except OperationalError as e: + print(f"OperationalError during initialization: {e}") + except Exception as e: + print(f"Error during initialization: {e}") + + +async def close_tortoise(): + """ + 关闭 Tortoise-ORM 数据库连接 + """ + try: + await Tortoise.close_connections() + # print("Tortoise-ORM connections closed successfully.") + except Exception as e: + print(f"Error during closing connections: {e}") + + diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..679558b --- /dev/null +++ b/src/utils/__init__.py @@ -0,0 +1,37 @@ +import sqlite3 +from collections import deque + +import redis.asyncio as aioredis +from fastapi import FastAPI + +from src.settings.config import settings +from src.utils import redis + + +import akshare + +from pypinyin import lazy_pinyin + +from src.models import StockType +from src.models.stock import Stock, StockResponse +from src.utils.paginations import PaginationPydantic, pagination, Params + + + +def register_redis(app: FastAPI): + @app.on_event("startup") + async def startup_redis(): + pool = aioredis.ConnectionPool.from_url( + str(settings.REDIS_URL), max_connections=10, decode_responses=True + ) + redis.redis_client = aioredis.Redis(connection_pool=pool) + + + + + @app.on_event("shutdown") + async def shutdown_redis(): + await redis.redis_client.connection_pool.disconnect() + await redis.redis_client.shutdown() + + diff --git a/src/utils/__pycache__/__init__.cpython-311.pyc b/src/utils/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b87d3d9bbefc03760dc776c3477fd3ead251e79 GIT binary patch literal 2193 zcmaJ?O>7%Q6rSDn+UviyNl3y^Tnudkrj=U+Cz@6Y1VjNvky8*_8CI*^jl1doxU*|o zmlg%6fD7VJC{PuV0}4Xvp-04#6GuC;6l2v}?yYkz2%O3~`{Xr+a#)r)A6=2qofeg+@3LSZyb9ja4x-!9uC?ewpJL<%g z7~>ImQW$JLmgcxa6~lBvo#Z#*vpz9%q^Y-R`Sgtu-uAI z*Rg3>lu6p!g}Pd`+`8q$G`DweASmgg?k z4b2TKog}J#8Hp}xSaWqnLH`tlR-!%mMRYAPygQJ4YY3_R)581h zP(Yu8JZ2p2ysCSyYw9#-KFJ!U?ir?v!<Lh z_8hf_Z6f#q#`JC^UV3T%2)HOD7r9poTQiqRktnE4d9#zHSW{<N!Zq{9rt zP2j)_2eDbP{J_NBd2!bEbj|ip6o=F%;Z(l_8H<{P=WW^S&PoBK>`NslyG-vz4( z9@^971(g)Uh9}?7VuMIzikD&CtC%lQ)gpVWKD5yqyd;dS) z&klnSw@?sS67e+b!xKPA0^%GZ1wRggpApW}#eH;9?p-|0$IQrlgaPX6Q3L+lK*EH` z<1IPgl8KAf;iXVdGT-u0ilZfh9pef_4Hw=PiJi_FrOt!-LviR@nY&ENi)C~HNWoa zmPcYCgDJL2Y#D04R<$rqI~j+kQ|y)gBIIt_s)R3=uCPbdeX23-jDK3gCXuT3?mq&i z)r=&lo&yEL_AxCoBs&nm(}F(?90xI`^@dFJjTQ%g2|i9zg%l-eN@y_g0ZQ_eQ1tvO z&B7Tq+dP4vgDI^L{@*}40>^P}B(Jhx8|50q{U*vchI<=5+8FLP(Nm4#zE`IcMO>zP i$X=6sC2jOf<6gauUTKW(9ZBX6e;ex{7)BBnlm7tUmLC8B literal 0 HcmV?d00001 diff --git a/src/utils/__pycache__/helpers.cpython-311.pyc b/src/utils/__pycache__/helpers.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2d832907831c4b5925c5a8045855844c920703fc GIT binary patch literal 2231 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 literal 0 HcmV?d00001 diff --git a/src/utils/__pycache__/models.cpython-311.pyc b/src/utils/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3fa21bdfa8c9664856ed2453673e1e9187a4d528 GIT binary patch literal 2529 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 B-s=DW literal 0 HcmV?d00001 diff --git a/src/utils/__pycache__/redis.cpython-311.pyc b/src/utils/__pycache__/redis.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dd1277a38cb4264c1458b08d84621f987c5f5cc7 GIT binary patch literal 2475 zcmcH)?{5@EbY^#NyZ5_SN)fT7&}+*TEh;9W0zqgnLM&=U6G;Dpz2*`wo0v2)8u@mlm^6O!&0M#=Yq9dh**9J#+cE9DP+a8$Z64(vVpYB zX~}HLDrp61F&!i_3F(1@u!4X-K)QSx=}knLg@4UKueEeoSM=Z=tcOTg50i)<(Z%z! z9=$B5qk4?Q^av42Y`brc&P#gyvXqY3bYLs#&A>P7;vh;To-n%<_JWR)B|0%34H)HA z-Z65PW)1)oK31@uT=t_JXuSv}x?!ss(;$|US^WnPzJE&}12c^XN@E?Rh0_SUkuJjS zkbsU^WOcm2c!*mj5G4sRt2MvDUaINA&wv;Fl>ND1b2LxgFt|b`K%Qt= z4rSroM@xXwU+)0we|P8$L$iiY02+fS!$ZG6yz%6xTYvm~bLfI*Wr(VSYeP283>6&1 zw1@ac_2tK@0<$?{f3QyinyC9K!PwN6t2I9w`>(#|N^JlosjbqLnmBgl zrK!%D;LUyI;BHq+0hBlfg0}jrABUrg!O=KWvurKH!tz43(_)^{HZb&LN}K|oISkJA_n1qz^*TT#$;M>t|Sef~}p{-kab;hPE3q?P18`hfzZf;f5 z=nW(@eCt*f>iyNxAnHVKpvgL^EnN`C#IxuEl!LRd%PjffVhcRQ>JBZXP!^r1)z-CtJHF++;Yf`JO>tG zdGnfi)w*uE$}1b88gB6MDRcSRfBU_8R4<@iUS8hZ%5H%TzWaXP(JpbmD|B>U)BKwl zFh9xF1OyXaIe7ve2j+zn5H+S>{S_TV_whhVsNExRm^k+_|EtZCb>%{YK&+k{SJ`do z0`bex?NpX0k4#HZvhR2Fg6A zTNvV%oh9$z%E8)$c9^~nEQ{LS2DmC>j4NoTy8$X_hua8?sKaf9MYP9lgvIUc6Ok(; zE@}rTq4ugQ;@&FSh^E3%ne^G3DuPbnQ@Dy`4CTT{#Sc0CD9HzR@XVU3j%vZUXQmU1 g0`4j9xwrrBYn)t<6pVXk2AQn+a literal 0 HcmV?d00001 diff --git a/src/utils/helpers.py b/src/utils/helpers.py new file mode 100644 index 0000000..d42497c --- /dev/null +++ b/src/utils/helpers.py @@ -0,0 +1,55 @@ +import numpy as np + + +# 英文连接的多个参数转成list +def comma_string_to_array(value): + if not value: + return [] + + return np.array(value.split(',')) + + +def first(iterable, default=None, condition=lambda x: True): + """ + Returns the first item in the `iterable` that + satisfies the `condition`. + + If the condition is not given, returns the first item of + the iterable. + + If the `default` argument is given and the iterable is empty, + or if it has no items matching the condition, the `default` argument + is returned if it matches the condition. + + The `default` argument being None is the same as it not being given. + + Raises `StopIteration` if no item satisfying the condition is found + and default is not given or doesn't satisfy the condition. + + >>> first( (1,2,3), condition=lambda x: x % 2 == 0) + 2 + >>> first(range(3, 100)) + 3 + >>> first( () ) + Traceback (most recent call last): + ... + StopIteration + >>> first([], default=1) + 1 + >>> first([], default=1, condition=lambda x: x % 2 == 0) + Traceback (most recent call last): + ... + StopIteration + >>> first([1,3,5], default=1, condition=lambda x: x % 2 == 0) + Traceback (most recent call last): + ... + StopIteration + """ + + try: + return next(x for x in iterable if condition(x)) + except StopIteration: + if default is not None and condition(default): + return default + else: + raise diff --git a/src/utils/history_data_processing_utils.py b/src/utils/history_data_processing_utils.py new file mode 100644 index 0000000..c155b23 --- /dev/null +++ b/src/utils/history_data_processing_utils.py @@ -0,0 +1,249 @@ +import difflib +import re + +import pandas as pd + + +def arrays_to_dict(Chinese: list, English: list) -> dict: + # 使用 zip 将两个数组配对,然后转换为字典 + return dict(zip(Chinese, English)) + + +# 中文数组 +Chinese = [ + "上证A股", "深证A股", "沪深300", "中证500", "中证1000", "国证2000", "农林牧渔基础", "化工钢铁", "有色金属", + "电子", "汽车", "家用电器", "食品饮料", "纺织服饰", "轻工制造", "医药生物", "公用事业", "交通运输", + "房地产", "商贸零售", "社会服务", "银行", "非银金融", "综合", "建筑材料", "建筑装饰", "电力设备", + "机械设备", "国防军工", "计算机", "传媒", "通信", "煤炭", "石油石化", "环保", "美容护理", "中字头股票", + "中特估100", "人形机器人", "低空经济", "数字货币", "工业互联网", "Web3.0", "网络直播", "跨境电商", + "消费电子概念", "网络游戏", "国产软件", "抖音概念", "手机游戏", "元宇宙", "区块链", "虚拟现实", "量子科技", + "智能穿戴", "机器视觉", "ChatGPT概念", "脑机接口", "人工智能", "机器人概念", "数字孪生", "算力租赁", "碳中和", + "风电", "光热发电", "光伏概念", "中俄贸易概念", "露营经济", "柔性屏", "宠物经济", "免税店", "预制菜", + "在线旅游", "减肥药", "体育产业", "无人机", "华为海思概念股", "先进封装(Chiplet)", "第三代半导体", "中芯国际概念", + "光刻机", "氟化工概念", "光刻胶", "汽车芯片", "传感器", "英伟达概念", "边缘计算", "存储芯片", "6G概念", + "石墨烯", "芯片概念", "特斯拉", "充电桩", "抽水蓄能", "固态电池", "一体化压铸", "动力电池回收", "新能源汽车", + "钠离子电池", "储能", "钙钛矿电池", "汽车电子", "稀土永磁", "华为汽车", "毫米波雷达", "锂电池", "阿尔茨海默概念", + "创新药", "仿制药一致性评价", "重组蛋白", "毛发医疗", "生物医药", "医美概念辅助生殖", "生物疫苗", "细胞免疫治疗", + "基因测序", "集成电路概念", "黄金概念", "新疆振兴", "一带一路", "养老概念", "新材料概念", "职业教育", "工业4.0", + "碳纤维", "军工", "航运概念", "独角兽概念", "国家大基金持股", "海南自贸区", "专精特新", "智能制造", "超超临界发电" +] + +# 英文数组(仅为示例,可以根据具体需求替换成正确的英文) +English = [ + "Shanghai A-shares", "Shenzhen A-shares", "CSI 300", "CSI 500", "CSI 1000", "CSI 2000", "Agriculture and Forestry", + "Chemical and Steel", "Non-ferrous Metals", "Electronics", "Automobile", "Household Appliances", + "Food and Beverage", + "Textiles and Apparel", "Light Industry Manufacturing", "Pharmaceuticals", "Public Utilities", "Transportation", + "Real Estate", "Retail", "Social Services", "Banking", "Non-bank Finance", "Comprehensive", "Building Materials", + "Building Decoration", "Electric Power Equipment", "Machinery", "Defense", "Computer", "Media", "Telecom", "Coal", + "Petroleum", "Environmental Protection", "Beauty Care", "State-owned Enterprises", "Special Valuation 100", + "Humanoid Robot", + "Low-altitude Economy", "Digital Currency", "Industrial Internet", "Web3.0", "Live Streaming", + "Cross-border E-commerce", + "Consumer Electronics", "Online Games", "Domestic Software", "Douyin Concept", "Mobile Games", "Metaverse", + "Blockchain", + "Virtual Reality", "Quantum Technology", "Wearable Devices", "Machine Vision", "ChatGPT Concept", + "Brain-computer Interface", + "Artificial Intelligence", "Robotics", "Digital Twin", "Computing Power Leasing", "Carbon Neutrality", "Wind Power", + "Solar Thermal", "Photovoltaic", "China-Russia Trade", "Camping Economy", "Flexible Screen", "Pet Economy", + "Duty-free", + "Prepared Foods", "Online Travel", "Weight Loss Drugs", "Sports Industry", "Drone", "Huawei HiSilicon", "Chiplet", + "Third-generation Semiconductors", "SMIC Concept", "Lithography Machine", "Fluorine Chemicals", "Photoresist", + "Automotive Chips", "Sensors", "NVIDIA Concept", "Edge Computing", "Memory Chips", "6G Concept", "Graphene", + "Chip Concept", + "Tesla", "Charging Pile", "Pumped Storage", "Solid-state Battery", "Integrated Die-casting", "Battery Recycling", + "New Energy Vehicles", "Sodium-ion Battery", "Energy Storage", "Perovskite Battery", "Automotive Electronics", + "Rare Earth", + "Huawei Vehicles", "Millimeter Wave Radar", "Lithium Battery", "Alzheimer's Concept", "Innovative Drugs", + "Generic Drugs", + "Recombinant Protein", "Hair Medical", "Biomedical", "Aesthetic Medicine", "Reproductive Assistance", "Biovaccine", + "Cell Immunotherapy", "Gene Sequencing", "IC Concept", "Gold Concept", "Xinjiang Development", "Belt and Road", + "Elderly Care Concept", "New Materials", "Vocational Education", "Industry 4.0", "Carbon Fiber", "Military", + "Shipping", + "Unicorn Concept", "National Fund Holdings", "Hainan Free Trade Zone", "Specialized and New", "Smart Manufacturing", + "Ultra-supercritical Power" +] + +# 调用方法 +result_dict = arrays_to_dict(Chinese, English) + + +translation_dict = { + "上证A股": "Shanghai A-shares", + "深证A股": "Shenzhen A-shares", + "沪深300": "CSI 300", + "中证500": "CSI 500", + "中证1000": "CSI 1000", + "国证2000": "CSI 2000", + "农林牧渔基础": "Agriculture and Forestry", + "化工钢铁": "Chemical and Steel", + "有色金属": "Non-ferrous Metals", + "电子": "Electronics", + "汽车": "Automobile", + "家用电器": "Household Appliances", + "食品饮料": "Food and Beverage", + "纺织服饰": "Textiles and Apparel", + "轻工制造": "Light Industry Manufacturing", + "医药生物": "Pharmaceuticals", + "公用事业": "Public Utilities", + "交通运输": "Transportation", + "房地产": "Real Estate", + "商贸零售": "Retail", + "社会服务": "Social Services", + "银行": "Banking", + "非银金融": "Non-bank Finance", + "综合": "Comprehensive", + "建筑材料": "Building Materials", + "建筑装饰": "Building Decoration", + "电力设备": "Electric Power Equipment", + "机械设备": "Machinery", + "国防军工": "Defense", + "计算机": "Computer", + "传媒": "Media", + "通信": "Telecom", + "煤炭": "Coal", + "石油石化": "Petroleum", + "环保": "Environmental Protection", + "美容护理": "Beauty Care", + "中字头股票": "State-owned Enterprises", + "中特估100": "Special Valuation 100", + "人形机器人": "Humanoid Robot", + "低空经济": "Low-altitude Economy", + "数字货币": "Digital Currency", + "工业互联网": "Industrial Internet", + "Web3.0": "Web3.0", + "网络直播": "Live Streaming", + "跨境电商": "Cross-border E-commerce", + "消费电子概念": "Consumer Electronics", + "网络游戏": "Online Games", + "国产软件": "Domestic Software", + "抖音概念": "Douyin Concept", + "手机游戏": "Mobile Games", + "元宇宙": "Metaverse", + "区块链": "Blockchain", + "虚拟现实": "Virtual Reality", + "量子科技": "Quantum Technology", + "智能穿戴": "Wearable Devices", + "机器视觉": "Machine Vision", + "ChatGPT概念": "ChatGPT Concept", + "脑机接口": "Brain-computer Interface", + "人工智能": "Artificial Intelligence", + "机器人概念": "Robotics", + "数字孪生": "Digital Twin", + "算力租赁": "Computing Power Leasing", + "碳中和": "Carbon Neutrality", + "风电": "Wind Power", + "光热发电": "Solar Thermal", + "光伏概念": "Photovoltaic", + "中俄贸易概念": "China-Russia Trade", + "露营经济": "Camping Economy", + "柔性屏": "Flexible Screen", + "宠物经济": "Pet Economy", + "免税店": "Duty-free", + "预制菜": "Prepared Foods", + "在线旅游": "Online Travel", + "减肥药": "Weight Loss Drugs", + "体育产业": "Sports Industry", + "无人机": "Drone", + "华为海思概念股": "Huawei HiSilicon", + "先进封装(Chiplet)": "Chiplet", + "第三代半导体": "Third-generation Semiconductors", + "中芯国际概念": "SMIC Concept", + "光刻机": "Lithography Machine", + "氟化工概念": "Fluorine Chemicals", + "光刻胶": "Photoresist", + "汽车芯片": "Automotive Chips", + "传感器": "Sensors", + "英伟达概念": "NVIDIA Concept", + "边缘计算": "Edge Computing", + "存储芯片": "Memory Chips", + "6G概念": "6G Concept", + "石墨烯": "Graphene", + "芯片概念": "Chip Concept", + "特斯拉": "Tesla", + "充电桩": "Charging Pile", + "抽水蓄能": "Pumped Storage", + "固态电池": "Solid-state Battery", + "一体化压铸": "Integrated Die-casting", + "动力电池回收": "Battery Recycling", + "新能源汽车": "New Energy Vehicles", + "钠离子电池": "Sodium-ion Battery", + "储能": "Energy Storage", + "钙钛矿电池": "Perovskite Battery", + "汽车电子": "Automotive Electronics", + "稀土永磁": "Rare Earth", + "华为汽车": "Huawei Vehicles", + "毫米波雷达": "Millimeter Wave Radar", + "锂电池": "Lithium Battery", + "阿尔茨海默概念": "Alzheimer's Concept", + "创新药": "Innovative Drugs", + "仿制药一致性评价": "Generic Drugs", + "重组蛋白": "Recombinant Protein", + "毛发医疗": "Hair Medical", + "生物医药": "Biomedical", + "医美概念辅助生殖": "Aesthetic Medicine", + "生物疫苗": "Biovaccine", + "细胞免疫治疗": "Cell Immunotherapy", + "基因测序": "Gene Sequencing", + "集成电路概念": "IC Concept", + "黄金概念": "Gold Concept", + "新疆振兴": "Xinjiang Development", + "一带一路": "Belt and Road", + "养老概念": "Elderly Care Concept", + "新材料概念": "New Materials", + "职业教育": "Vocational Education", + "工业4.0": "Industry 4.0", + "碳纤维": "Carbon Fiber", + "军工": "Military", + "航运概念": "Shipping", + "独角兽概念": "Unicorn Concept", + "国家大基金持股": "National Fund Holdings", + "海南自贸区": "Hainan Free Trade Zone", + "专精特新": "Specialized and New", + "智能制造": "Smart Manufacturing", + "超超临界发电": "Ultra-supercritical Power" +} + + +def chinese_to_english(chinese_term): + return translation_dict.get(chinese_term, "Translation not found") + + +def english_to_chinese(english_term): + # Invert the dictionary for reverse lookup + inverted_dict = {v: k for k, v in translation_dict.items()} + return inverted_dict.get(english_term, "Translation not found") + + +def fuzzy_search(pattern, string_list): + """ + 使用正则表达式在字符串列表中进行模糊搜索。 + + :param pattern: 要搜索的模式(字符串) + :param string_list: 字符串列表 + :return: 匹配的字符串列表 + """ + regex = re.compile(pattern) + return [s for s in string_list if regex.search(s)] + + +def get_best_match(sector, translation_dict): + """ + 模糊匹配获取最佳匹配的中文板块。 + @param sector: 需要匹配的中文板块名 + @param translation_dict: 中文板块到英文的映射字典 + @return: 最佳匹配的中文板块名(如果找到),否则返回None + """ + matches = difflib.get_close_matches(sector, translation_dict.keys(), n=1, cutoff=0.6) + return matches[0] if matches else None + + +def on_progress(data): + print("这里是回执数据", data) + + +def safe_get_value(value): + """检查值是否为 None、NaN 或 False,如果是,则返回 0,否则返回值本身""" + if value is None or pd.isna(value) or value is False: + return 0 + return value diff --git a/src/utils/history_stock.py b/src/utils/history_stock.py new file mode 100644 index 0000000..fc9b596 --- /dev/null +++ b/src/utils/history_stock.py @@ -0,0 +1,40 @@ +from datetime import datetime + +import akshare +from pypinyin import lazy_pinyin + +from src.models import StockType +from src.models.snowball import Snowball +from src.models.stock_details import StockDetails +from src.tortoises_orm_config import init_tortoise, close_tortoise + + +async def job(): + await init_tortoise() + print(f"执行定时任务: 当前时间是 {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + + await StockDetails.all().delete() + stock_name_df = akshare.stock_sh_a_spot_em() + stock_list = stock_name_df[['代码', '名称', '最新价', '涨跌幅']].values.tolist() + + for stock in stock_list: + # 将文字转化为拼音列表 + pinyin_list = lazy_pinyin(stock[1]) + + # 取每个拼音的首字母并连接成字符串 + initial_pinyin = ''.join([pinyin[0] for pinyin in pinyin_list]) + await StockDetails.create(stock_code=stock[0], stock_name=stock[1], type=StockType.SH, + stock_pinyin=initial_pinyin, latest_price=stock[2], rise_fall=stock[3]) + stock_name_df = akshare.stock_sz_a_spot_em() + stock_list = stock_name_df[['代码', '名称', '最新价', '涨跌幅']].values.tolist() + + for stock in stock_list: + # 将文字转化为拼音列表 + pinyin_list = lazy_pinyin(stock[1]) + + # 取每个拼音的首字母并连接成字符串 + initial_pinyin = ''.join([pinyin[0] for pinyin in pinyin_list]) + await StockDetails.create(stock_code=stock[0], stock_name=stock[1], type=StockType.SZ, + stock_pinyin=initial_pinyin, latest_price=stock[2], rise_fall=stock[3]) + + await close_tortoise() \ No newline at end of file diff --git a/src/utils/jsonformatter.py b/src/utils/jsonformatter.py new file mode 100644 index 0000000..32ae936 --- /dev/null +++ b/src/utils/jsonformatter.py @@ -0,0 +1,15 @@ +import json +import logging + + +class JsonFormatter(logging.Formatter): + """日志文件转为json格式""" + + def format(self, record): + obj = { + 'timestamp': self.formatTime(record, self.datefmt), + 'name': record.name, + 'level': record.levelname, + 'message': record.getMessage() + } + return json.dumps(obj) diff --git a/src/utils/models.py b/src/utils/models.py new file mode 100644 index 0000000..587efbc --- /dev/null +++ b/src/utils/models.py @@ -0,0 +1,37 @@ +from datetime import datetime +from typing import Any +from zoneinfo import ZoneInfo + +from fastapi.encoders import jsonable_encoder +from pydantic import BaseModel, ConfigDict, model_validator + + +def convert_datetime_to_gmt(dt: datetime) -> str: + if not dt.tzinfo: + dt = dt.replace(tzinfo=ZoneInfo("UTC")) + + return dt.strftime("%Y-%m-%dT%H:%M:%S%z") + + +class CustomModel(BaseModel): + model_config = ConfigDict( + json_encoders={datetime: convert_datetime_to_gmt}, + populate_by_name=True, + ) + + @model_validator(mode="before") + @classmethod + def set_null_microseconds(cls, data: dict[str, Any]) -> dict[str, Any]: + datetime_fields = { + k: v.replace(microsecond=0) + for k, v in data.items() + if isinstance(k, datetime) + } + + return {**data, **datetime_fields} + + def serializable_dict(self, **kwargs): + """Return a dict which contains only serializable fields.""" + default_dict = self.model_dump() + + return jsonable_encoder(default_dict) diff --git a/src/utils/paginations.py b/src/utils/paginations.py new file mode 100644 index 0000000..21cf264 --- /dev/null +++ b/src/utils/paginations.py @@ -0,0 +1,74 @@ +from typing import Generic, Optional, Sequence, TypeVar + +import math +from fastapi import Query +from pydantic import BaseModel +from tortoise.queryset import QuerySet + +T = TypeVar("T") + + +class PaginationPydantic(BaseModel, Generic[T]): + """分页模型""" + status_code: int = 200 + message: str = "Success" + total: int + page: int + size: int + total_pages: int + data: Sequence[T] + + +class Params(BaseModel): + """传参""" + # 设置默认值为1,不能够小于1 + page: int = Query(1, ge=1, description="Page number") + + # 设置默认值为10,最大为100 + size: int = Query(10, gt=0, le=200, description="Page size") + + # 默认值None表示选传 + order_by: Optional[str] = Query(None, max_length=32, description="Sort key") + + +async def pagination(pydantic_model, query_set: QuerySet, params: Params, callback=None): + """分页响应""" + """ + pydantic_model: Pydantic model + query_set: QuerySet + params: Params + callback: if you want to do something for query_set,it will be useful. + """ + page: int = params.page + size: int = params.size + order_by: str = params.order_by + total = await query_set.count() + + # 通过总数和每页数量计算出总页数 + total_pages = math.ceil(total / size) + + if page > total_pages and total: # 排除查询集为空时报错,即total=0时 + raise ValueError("页数输入有误") + + # 排序后分页 + if order_by: + order_by = order_by.split(',') + query_set = query_set.order_by(*order_by) + + # 分页 + query_set = query_set.offset((page - 1) * size) # 页数 * 页面大小=偏移量 + query_set = query_set.limit(size) + + if callback: + """对查询集操作""" + query_set = await callback(query_set) + + # query_set = await query_set + data = await pydantic_model.from_queryset(query_set) + return PaginationPydantic(**{ + "total": total, + "page": page, + "size": size, + "total_pages": total_pages, + "data": data, + }) diff --git a/src/utils/redis.py b/src/utils/redis.py new file mode 100644 index 0000000..0e7f139 --- /dev/null +++ b/src/utils/redis.py @@ -0,0 +1,31 @@ +from datetime import timedelta +from typing import Optional + +import redis.asyncio as aioredis + +from src.utils.models import CustomModel + +redis_client: aioredis = None # type: ignore + + +class RedisData(CustomModel): + key: bytes | str + value: bytes | str + ttl: Optional[int | timedelta] = None + + +async def set_redis_key(redis_data: RedisData, *, is_transaction: bool = False) -> None: + async with redis_client.pipeline(transaction=is_transaction) as pipe: + await pipe.set(redis_data.key, redis_data.value) + if redis_data.ttl: + await pipe.expire(redis_data.key, redis_data.ttl) + + await pipe.execute() + + +async def get_by_key(key: str) -> Optional[str]: + return await redis_client.get(key) + + +async def delete_by_key(key: str) -> None: + return await redis_client.delete(key) diff --git a/src/utils/remove_duplicates_databases.py b/src/utils/remove_duplicates_databases.py new file mode 100644 index 0000000..972fb04 --- /dev/null +++ b/src/utils/remove_duplicates_databases.py @@ -0,0 +1,40 @@ +from tortoise.models import Model +from tortoise import fields +from tortoise.functions import Count + +# 调用异步方法 +import asyncio + +from src.models.wance_data_stock import WanceDataStock +from src.tortoises_orm_config import init_tortoise + + +async def remove_duplicates(): + await init_tortoise() + + # 获取重复的股票代码和时间 + duplicates = await WanceDataStock.all().group_by('stock_code', 'time_end').annotate(count=Count('id')).filter( + count__gt=1) + + if not duplicates: + print("No duplicates found.") + return + + print(f"Found {len(duplicates)} duplicates.") + + for record in duplicates: + # 查询相同 stock_code 和 time_end 的所有记录,并按 id 逆序排序 + duplicate_records = await WanceDataStock.filter(stock_code=record.stock_code, + time_end=record.time_end).order_by('-id') + + # 输出当前处理的记录 + print( + f"Processing {len(duplicate_records)} records for stock_code={record.stock_code} and time_end={record.time_end}.") + + if duplicate_records: + # 删除最新的一条记录,保留其余的 + await WanceDataStock.filter(id=duplicate_records[0].id).delete() + print(f"Deleted record with id={duplicate_records[0].id}") + + +asyncio.run(remove_duplicates()) diff --git a/src/utils/split_stock_utils.py b/src/utils/split_stock_utils.py new file mode 100644 index 0000000..945aa1f --- /dev/null +++ b/src/utils/split_stock_utils.py @@ -0,0 +1,41 @@ +""" + +通用工具区域 + +""" + + +def split_stock_code(stock_code): + """ + 将股票代码按点(".")切割成两部分。 + + Args: + stock_code (str): 原始股票代码(如 "000300.SH")。 + + Returns: + tuple: 返回切割后的两部分(如 ('000300', 'SH'))。 + """ + return stock_code.split('.') + + +def join_stock_code(parts): + """ + 将切割后的股票代码部分按指定格式拼接。 + + Args: + parts (tuple): 切割后的股票代码部分(如 ('000300', 'SH'))。 + + Returns: + str: 返回拼接后的字符串(如 "SH000300")。 + """ + part1, part2 = parts # 解构切割后的结果 + return f"{part2}{part1}" + + +def percent_to_float(percent_string): + if isinstance(percent_string, int): + return float(percent_string) + elif isinstance(percent_string, str): + return float(percent_string.strip('%')) + else: + raise ValueError(f"Unsupported type: {type(percent_string)}") \ No newline at end of file diff --git a/src/xtdata/__init__.py b/src/xtdata/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/xtdata/__pycache__/__init__.cpython-311.pyc b/src/xtdata/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..809829846af04b5bf02870abc977fe60570913e0 GIT binary patch literal 166 zcmZ3^%ge<81jTQ!r-A6lAOZ#$p^VRLK*n^26oz01O-8?!3`I;p{%4TnFH2{un9$6T3ixST$BtHNJ%V7jERrW%*!l^kJl@x{Ka9D ho1apelWJGQ3N#61S1~`3_`uA_$oPQ)Miemv#Q?IKDN_Id literal 0 HcmV?d00001 diff --git a/src/xtdata/__pycache__/router.cpython-311.pyc b/src/xtdata/__pycache__/router.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..006c2d65144a03695474af942927ebf8e0850d92 GIT binary patch literal 8984 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 literal 0 HcmV?d00001 diff --git a/src/xtdata/router.py b/src/xtdata/router.py new file mode 100644 index 0000000..af619b7 --- /dev/null +++ b/src/xtdata/router.py @@ -0,0 +1,202 @@ +import json # 导入 json 库 + +from fastapi import APIRouter, HTTPException # 从 FastAPI 中导入 APIRouter,用于创建 API 路由器 + +import src.xtdata.service as service # 导入服务模块,该模块包含各种服务的实现 +from src.pydantic.codelistrequest import CodeListRequest # 导入 Pydantic 模型,用于验证请求参数 +from src.pydantic.factor_request import StockQuery +from src.pydantic.request_data import DataRequest # 导入 Pydantic 模型,用于验证请求参数 +from src.responses import response_entity_response, response_list_response # 导入自定义响应格式化函数 + +router = APIRouter() # 创建一个 FastAPI 路由器实例 + + +# 获取完整的逐笔成交键 +@router.get("/get_full_tick_keys") +async def get_full_tick_keys(request: CodeListRequest): + """ + 获取完整的逐笔成交键。 + """ + result = await service.get_full_tick_keys_service(request.code_list) # 调用服务获取逐笔成交键 + return result # 返回获取的结果 + + +# 获取完整的逐笔成交数据 +@router.get("/get_full_tick") +async def get_full_tick(request: CodeListRequest): + """ + 获取完整的逐笔成交数据。 + """ + result = await service.get_full_tick_service(request.code_list) # 调用服务获取逐笔成交数据 + return result # 返回获取的结果 + + +# 获取市场数据 +@router.get("/get_market_data") +async def get_market_data(request: DataRequest): + """ + 获取市场数据。 + """ + # 调用服务获取市场数据 + market_data = await service.get_market_data_service(field_list=request.field_list, + stock_list=request.stock_list, + period=request.period, + start_time=request.start_time, + end_time=request.end_time, + count=request.count, + dividend_type=request.dividend_type, + fill_data=request.fill_data) + return market_data # 返回市场数据 + + +# 获取完整的 K 线数据 +@router.get("/get_full_kline") +async def get_full_kline(request: DataRequest): + """ + 获取完整的 K 线数据。 + """ + # 调用服务获取完整的 K 线数据 + result = await service.get_full_kline_service(field_list=request.field_list, + stock_list=request.stock_list, + period=request.period, + start_time=request.start_time, + end_time=request.end_time, + count=request.count, + dividend_type=request.dividend_type, + fill_data=request.fill_data) + return result # 返回 K 线数据 + + +# 获取合约详情 +@router.get("/get_instrument_detail") +async def get_instrument_detail(request: DataRequest): + """ + 获取合约的详细信息。 + """ + # 调用服务获取合约详情数据 + result = await service.get_instrument_detail_service(stock_code=request.stock_code, + iscomplete=request.iscomplete) + return result # 返回合约详情 + + +@router.get("/get_stock_factor") +async def get_stock_factor(query_params: StockQuery): + stocks = await service.get_stock_factor_service(query_params) + return stocks + + +# 获取本地数据 +@router.get("/get_local_data") +async def get_local_data(request: DataRequest): + """ + 获取本地存储的股票数据。 + """ + # 调用服务获取本地数据 + result = await service.get_local_data_service(field_list=request.field_list, stock_list=request.stock_list, + period=request.period, start_time=request.start_time, + end_time=request.end_time, count=request.count, + dividend_type=request.dividend_type, fill_data=request.fill_data, + data_dir=request.data_dir) + return result + + +# 获取板块列表 +@router.post("/get_sector_list") +async def get_sector_list(): + """ + 获取所有板块的列表。 + """ + # 调用服务获取板块列表 + result = await service.get_sector_list_service() + return result # 返回板块列表 + + +# 获取某板块中的股票列表 +@router.get("/get_stock_list_in_sector") +async def get_stock_list_in_sector(request: DataRequest): + """ + 获取指定板块中的股票列表。 + """ + # 调用服务获取板块中的股票列表 + result = await service.get_stock_list_in_sector_service(sector_name=request.sector_name) + return result # 返回股票列表 + + +# 下载板块数据 +@router.post("/download_sector_data") +async def download_sector_data(): + """ + 下载板块的详细数据。 + """ + # 调用服务下载板块数据 + result = await service.download_sector_data_service() + return result # 返回下载结果 + + +# 订阅股票报价 +@router.post("/subscribe_quote") +async def subscribe_quote(request: DataRequest): + """ + 订阅单只股票的报价。 + """ + # 调用服务订阅股票报价 + result = await service.subscribe_quote_service(stock_code=request.stock_code, + period=request.period, + start_time=request.start_time, + end_time=request.end_time, + count=request.count, + callback=request.callback) + return response_list_response(data=result, status_code=200, message="Success") # 返回响应 + + +# 订阅所有股票报价 +@router.post("/subscribe_whole_quote") +async def subscribe_whole_quote(request: DataRequest): + """ + 订阅所有股票的报价。 + """ + # 调用服务订阅所有股票的报价 + result = await service.subscribe_whole_quote_service(code_list=request.code_list, + callback=request.callback) + return response_list_response(data=result, status_code=200, message="Success") # 返回响应 + + +# 下载历史数据 +@router.post("/download_history_data") +async def download_history_data(request: DataRequest): + """ + 下载指定股票的历史数据。 + """ + # 调用服务下载指定股票的历史数据 + await service.download_history_data_service(stock_code=request.stock_code, + period=request.period, + start_time=request.start_time, + end_time=request.end_time, + incrementally=request.incrementally) + return response_list_response(data=[], status_code=200, message="Success") # 返回响应 + + +# 批量下载历史数据 +@router.post("/batch_download_history_data") +async def download_history_data(request: DataRequest): + """ + 批量下载多个股票的历史数据。 + """ + # 调用服务批量下载历史数据 + await service.download_history_data2_service(stock_list=request.stock_list, + period=request.period, + start_time=request.start_time, + end_time=request.end_time, + callback=request.callback) + return response_list_response(data=[], status_code=200, message="Success") # 返回响应 + + +# 批量下载历史数据 +@router.post("/update_stock_data") +async def update_stock_data(): + """ + 批量下载多个股票的历史数据。 + """ + # 调用服务批量下载历史数据 + await service.update_stock_data_service() + return response_list_response(data=[], status_code=200, message="Success") # 返回响应 diff --git a/src/xtdata/service.py b/src/xtdata/service.py new file mode 100644 index 0000000..76708be --- /dev/null +++ b/src/xtdata/service.py @@ -0,0 +1,363 @@ +import asyncio +import json +from datetime import datetime, timedelta + +import numpy as np # 导入 numpy 库 +from tortoise.expressions import Q +from xtquant import xtdata # 导入 xtquant 库的 xtdata 模块 + +from src.backtest.until import convert_pandas_to_json_serializable +from src.models.wance_data_stock import WanceDataStock +from src.pydantic.factor_request import StockQuery +from src.utils.history_data_processing_utils import translation_dict + + +# 获取股票池中的所有股票的股票代码 +async def get_full_tick_keys_service(code_list: list): + """ + 获取所有股票的逐笔成交键。若未提供股票列表,则默认使用 ['SH', 'SZ']。 + """ + if len(code_list) == 0: # 如果股票代码列表为空 + code_list = ['SH', 'SZ'] # 默认使用上证和深证股票池 + result = xtdata.get_full_tick(code_list=['SH', 'SZ']) # 调用 xtdata 库的函数获取逐笔成交数据 + return list(result.keys()) # 返回所有股票代码的列表 + + +# 获取股票池中的所有股票的逐笔成交数据 +async def get_full_tick_service(code_list: list): + """ + 获取所有股票的逐笔成交数据。若未提供股票列表,则默认使用 ['SH', 'SZ']。 + """ + if len(code_list) == 0: # 如果股票代码列表为空 + code_list = ['SH', 'SZ'] # 默认使用上证和深证股票池 + result = xtdata.get_full_tick(code_list=code_list) # 获取逐笔成交数据 + return result # 返回逐笔成交数据 + + +# 获取板块信息 +async def get_sector_list_service(): + """ + 获取所有板块的列表。 + """ + result = xtdata.get_sector_list() # 调用 xtdata 库的函数获取板块信息 + return result # 返回板块列表 + + +# 通过板块名称获取股票列表 +async def get_stock_list_in_sector_service(sector_name): + """ + 获取指定板块中的所有股票名称。 + """ + result = xtdata.get_stock_list_in_sector(sector_name=sector_name) # 根据板块名称获取股票列表 + return result # 返回股票列表 + + +# 下载板块信息 +async def download_sector_data_service(): + """ + 下载板块的详细数据。 + """ + result = xtdata.download_sector_data() # 调用 xtdata 库的函数下载板块数据 + return result # 返回下载结果 + + +# 获取股票的详细信息 +async def get_instrument_detail_service(stock_code: str, iscomplete: bool): + """ + 获取股票的详细信息。 + """ + result = xtdata.get_instrument_detail(stock_code=stock_code, iscomplete=iscomplete) # 获取股票的详细信息 + return result # 返回股票的详细信息 + + +# 获取市场行情数据 +async def get_market_data_service(field_list: list, stock_list: list, period: str, start_time: str, end_time: str, + count: int, dividend_type: str, fill_data: bool): + """ + 获取指定条件下的市场行情数据。 + """ + result = xtdata.get_market_data(field_list=field_list, stock_list=stock_list, period=period, start_time=start_time, + end_time=end_time, count=count, dividend_type=dividend_type, + fill_data=fill_data) # 获取市场数据 + return result # 返回市场数据 + + +# 获取最新的 K 线数据 +async def get_full_kline_service(field_list: list, stock_list: list, period: str, start_time: str, end_time: str, + count: int, + dividend_type: str, fill_data: bool): + """ + 获取指定条件下的完整 K 线数据。 + """ + result = xtdata.get_full_kline(field_list=field_list, stock_list=stock_list, period=period, start_time=start_time, + end_time=end_time, count=count, dividend_type=dividend_type, + fill_data=fill_data) # 获取完整的 K 线数据 + return result # 返回 K 线数据 + + +# 下载历史数据 +async def download_history_data_service(stock_code: str, period: str, start_time: str, end_time: str, + incrementally: bool): + """ + 下载指定股票的历史数据。 + """ + xtdata.download_history_data(stock_code=stock_code, period=period, start_time=start_time, end_time=end_time, + incrementally=incrementally) # 下载股票历史数据 + + +# 获取本地数据 +async def get_local_data_service(field_list: list, stock_list: list, period: str, start_time: str, end_time: str, + count: int, dividend_type: str, fill_data: bool, data_dir: str): + """ + @param field_list: + @type field_list: + @param stock_list: + @type stock_list: + @param period: + @type period: + @param start_time: + @type start_time: + @param end_time: + @type end_time: + @param count: + @type count: + @param dividend_type: + @type dividend_type: + @param fill_data: + @type fill_data: + @param data_dir: + @type data_dir: + @return: + @rtype: + """ + """ + 获取本地存储的股票数据。 + """ + return_list = [] + result = xtdata.get_local_data(field_list=field_list, stock_list=stock_list, period=period, start_time=start_time, + end_time=end_time, count=count, dividend_type=dividend_type, fill_data=fill_data, + data_dir=data_dir) # 获取本地数据 + for i in stock_list: + return_list.append({ + i : convert_pandas_to_json_serializable(result.get(i)) + }) + return return_list + + +# 订阅全市场行情推送 +async def subscribe_whole_quote_service(code_list, callback=None): + """ + 订阅全市场行情推送。 + """ + if callback: + result = xtdata.subscribe_whole_quote(code_list, callback=on_data) # 有回调函数时调用 + else: + result = xtdata.subscribe_whole_quote(code_list, callback=None) # 无回调函数时调用 + return result # 返回订阅结果 + + +# 订阅单只股票行情推送 +async def subscribe_quote_service(stock_code: str, period: str, start_time: str, end_time: str, count: int, + callback: bool): + """ + 订阅单只股票的行情推送。 + """ + if callback: + result = xtdata.subscribe_quote(stock_code=stock_code, period=period, start_time=start_time, end_time=end_time, + count=count, callback=on_data) # 有回调函数时调用 + else: + result = xtdata.subscribe_quote(stock_code=stock_code, period=period, start_time=start_time, end_time=end_time, + count=count, callback=None) # 无回调函数时调用 + return result # 返回订阅结果 + + +def on_data(datas): + """ + 行情数据推送的回调函数,处理并打印接收到的行情数据。 + """ + for stock_code in datas: + print(stock_code, datas[stock_code]) # 打印接收到的行情数据 + + +# 批量增量下载或批量下载 +async def download_history_data2_service(stock_list: list, period: str, start_time: str, end_time: str, callback: bool): + """ + 批量下载股票历史数据,支持进度回调。 + """ + if callback: + xtdata.download_history_data2(stock_list=stock_list, period=period, start_time=start_time, end_time=end_time, + callback=on_progress) # 有回调函数时调用 + else: + xtdata.download_history_data2(stock_list=stock_list, period=period, start_time=start_time, end_time=end_time, + callback=None) # 无回调函数时调用 + + +def on_progress(data): + """ + 数据下载进度的回调函数,实时显示进度。 + """ + print(data) # 打印下载进度 + + +# 单只股票下载示例 +def download_data(): + """ + 测试函数:下载单只股票的历史数据并获取相关信息。 + """ + xtdata.download_history_data("000300.SH", "1d", start_time='', end_time='', incrementally=None) # 下载历史数据 + a = xtdata.get_full_tick(code_list=['SH', 'SZ']) # 获取逐笔成交数据 + print(list(a.keys())) # 打印获取的股票代码 + + +async def update_stock_data_service(): + """ + # 用于本地xtdata数据更新 + @return: + @rtype: + """ + stock_list = await get_full_tick_keys_service(['SH', 'SZ']) + for stock in stock_list: + await download_history_data_service(stock_code=stock, + period='1d', + start_time='', + end_time='', + incrementally=True) + + +async def get_stock_factor_service(query_params: StockQuery): + """ + 动态查询方法,根据传入的字段和值进行条件构造 + :param kwargs: 查询条件,键为字段名,值为查询值 + :return: 符合条件的记录列表 + """ + """ + 使用示例: + { + "financial_asset_value": 10.7169, + "financial_cash_flow": -0.42982, + "gt": { + "profit_gross_rate": 4.0, + "valuation_PB_TTM": 0.8 + }, + "lt": { + "market_indicator": -0.6 + }, + "gte": { + "valuation_market_percentile":5.38793 + }, + "lte": { + "valuation_PEGTTM_ratio": 15.2773 + }, + "between": { + "financial_dividend": { + "min": 1.0, + "max": 2.0 + }, + "growth_Income_rate": { + "min": -0.6, + "max": 1.0 + } + }, + "stock_code": "600051.SH", + "stock_name": "宁波联合", + "stock_sector": ["上证A股", "沪深A股"], + "stock_type": ["stock"], + "time_start": "0", + "time_end": "20240911", + "valuation_PEG_percentile": 15.2222, + "valuation_PTS_TTM": 2.3077 + } + + + financial_asset_value 和 financial_cash_flow 字段为普通等于条件。 + gt 字段用于指定字段大于某个值的条件,例如 profit_gross_rate 大于 15.0。 + lt 字段用于指定字段小于某个值的条件,例如 market_indicator 小于 30.0。 + gte 字段用于指定字段大于等于某个值的条件,例如 valuation_market_percentile 大于等于 0.5。 + lte 字段用于指定字段小于等于某个值的条件,例如 valuation_PEGTTM_ratio 小于等于 2.0。 + between 字段用于指定字段在两个值之间的范围,例如 financial_dividend 在 1.0 和 5.0 之间,growth_Income_rate 在 10.0 和 25.0 之间。 + stock_code 和 stock_name 字段为普通等于条件。 + stock_sector 和 stock_type 字段为列表类型的条件,可能会进行包含匹配。 + time_start 和 time_end 字段为普通等于条件。 + valuation_PEG_percentile 和 valuation_PTS_TTM 字段为普通等于条件。 + + 比较大小字段说明: + 'gt' 对应 __gt(大于) + 'lt' 对应 __lt(小于) + 'gte' 对应 __gte(大于等于) + 'lte' 对应 __lte(小于等于) + 'between' 对应在某个值之间的条件,使用 __gt 和 __lt 组合实现 + + 时间字段说明: + time_start={'recent_years': 2} 近2年 + time_start={'recent_months': 2} 近2月 + """ + + filters = {} + + # 处理大于、小于等操作 + if query_params.gt: + for field, value in query_params.gt.items(): + filters[f"{field}__gt"] = value + if query_params.lt: + for field, value in query_params.lt.items(): + filters[f"{field}__lt"] = value + if query_params.gte: + for field, value in query_params.gte.items(): + filters[f"{field}__gte"] = value + if query_params.lte: + for field, value in query_params.lte.items(): + filters[f"{field}__lte"] = value + + # 处理范围查询 + if query_params.between: + for field, range_values in query_params.between.items(): + if 'min' in range_values and 'max' in range_values: + filters[f"{field}__gte"] = range_values['min'] + filters[f"{field}__lte"] = range_values['max'] + + # 处理普通字段 + for field, value in query_params.model_dump(exclude_unset=True).items(): + if field not in ['gt', 'lt', 'gte', 'lte', 'between']: + if isinstance(value, float): + filters[f"{field}__gte"] = value + elif isinstance(value, str): + filters[f"{field}__icontains"] = value + elif isinstance(value, list): + if field == "stock_sector": + # 转换 `stock_sector` 中的中文板块名到英文 + translated_sectors = [translation_dict.get(sector, sector) for sector in value] + filters[f"{field}__contains"] = translated_sectors + else: + filters[f"{field}__contains"] = value + + try: + stocks = await WanceDataStock.filter(**filters).all() + return stocks + except Exception as e: + print(f"Error occurred when querying stocks: {e}") + return [] + + +# 将 numpy 类型转换为原生 Python 类型 +async def convert_numpy_to_native(obj): + """ + 递归地将 numpy 类型转换为原生 Python 类型。 + """ + if isinstance(obj, np.generic): # 如果是 numpy 类型 + return obj.item() # 转换为 Python 原生类型 + elif isinstance(obj, dict): # 如果是字典类型 + return {k: convert_numpy_to_native(v) for k, v in obj.items()} # 递归转换字典中的值 + elif isinstance(obj, list): # 如果是列表类型 + return [convert_numpy_to_native(i) for i in obj] # 递归转换列表中的值 + elif isinstance(obj, tuple): # 如果是元组类型 + return tuple(convert_numpy_to_native(i) for i in obj) # 递归转换元组中的值 + else: + return obj # 如果是其他类型,直接返回 + + +if __name__ == '__main__': + # result = asyncio.run(get_instrument_detail_service("000300.SH",False)) + # print(result) + # resulta = asyncio.run(get_sector_list_service()) + # print(resulta) + pass