1
0
mirror of https://github.com/microsoft/qlib.git synced 2026-07-19 18:27:38 +08:00

Compare commits

..

9 Commits

Author SHA1 Message Date
monkeyjack123
d5379c520f docs: replace broken RD-Agent demo links in README (#2150)
Co-authored-by: monkeyjack123 <monkeyjack123@users.noreply.github.com>
Co-authored-by: Linlang Lv (iSoftStone Information) <v-llv@microsoft.com>
2026-04-22 15:08:01 +08:00
Srujan Rana
7ccf3f7658 fix: incorrect index implementation in FileCalendarStorage (#2195) 2026-04-21 13:53:18 +08:00
Linlang
2c21b8089a fix: use baostock to fetch trading calendar instead of Eastmoney API (#2193)
* fix: use baostock to fetch trading calendar instead of Eastmoney API

* fix: lint error

* fix: lint error

* ci: enable concurrency to cancel in-progress runs for same workflow and ref

---------

Co-authored-by: Linlang Lv (iSoftStone Information) <v-llv@microsoft.com>
2026-04-17 16:21:54 +08:00
Linlang
b87a2c294d fix: value error caused by incorrect date format in daily data (#2015)
Co-authored-by: Linlang Lv (iSoftStone Information) <v-llv@microsoft.com>
2026-04-15 17:07:00 +08:00
Linlang
3097dcc995 fix(security): use RestrictedUnpickler in load_instance (#2153)
* fix(security): enforce RestrictedUnpickler for load_instance to prevent unsafe pickle deserialization

* fix: lint error
2026-03-10 20:45:38 +08:00
Linlang
2fb9380b34 fix(backtest): avoid calendar overflow when end_time is missing (#2127)
* fix(backtest): avoid calendar overflow when end_time is missing

* fix: pkg_source not found error when build docs
2026-02-12 21:07:15 +08:00
Linlang
8fd6d5ca7e fix: the bug that the US STMBOLS URL is faild (#1975)
* fix the bug that the US STMBOLS URL is faild

* recover code

* fix package dependence error

* fix package dependence error

* fix package dependence error

* fix package dependence error

* fix package dependence error

* format with black

* disable pylint error
2026-02-04 17:37:47 +08:00
feedseawave
69bb755f37 refactor: implement deterministic budget allocation in SoftTopkStrategy (#2077)
* refactor: implement deterministic budget allocation in SoftTopkStrategy

* style: fix formatting issues using black

* fix: remove unused imports and pass pylint

* refactor: simplify SoftTopkStrategy impact limit

* style: relocate test files per maintainer request
2026-02-03 16:52:59 +08:00
Linlang
39634b2158 fix(security): address reported unsafe pickle.load usages (#2099) 2026-01-28 22:19:43 +08:00
67 changed files with 285 additions and 241 deletions

View File

@@ -1,5 +1,9 @@
name: Test qlib from pip name: Test qlib from pip
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
on: on:
push: push:
branches: [ main ] branches: [ main ]

View File

@@ -1,5 +1,9 @@
name: Test qlib from source name: Test qlib from source
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
on: on:
push: push:
branches: [ main ] branches: [ main ]
@@ -76,8 +80,11 @@ jobs:
run: | run: |
make mypy make mypy
# Due to issues that cannot be automatically fixed when running `nbqa black . -l 120 --check --diff` on Jupyter notebooks,
# we reverted to a version of `black` earlier than 26.1.0 before performing the checks.
- name: Check Qlib ipynb with nbqa - name: Check Qlib ipynb with nbqa
run: | run: |
python -m pip install "black<26.1"
make nbqa make nbqa
- name: Test data downloads - name: Test data downloads

View File

@@ -1,5 +1,9 @@
name: Test qlib from source slow name: Test qlib from source slow
concurrency:
cancel-in-progress: true
group: ${{ github.workflow }}-${{ github.ref }}
on: on:
push: push:
branches: [ main ] branches: [ main ]

View File

@@ -17,14 +17,13 @@ We are excited to announce the release of **RD-Agent**📢, a powerful tool that
RD-Agent is now available on [GitHub](https://github.com/microsoft/RD-Agent), and we welcome your star🌟! RD-Agent is now available on [GitHub](https://github.com/microsoft/RD-Agent), and we welcome your star🌟!
To learn more, please visit our [Demo page](https://rdagent.azurewebsites.net/). Here, you will find demo videos in both English and Chinese to help you better understand the scenario and usage of RD-Agent. To learn more, please visit the [RD-Agent repository](https://github.com/microsoft/RD-Agent). We have prepared several public demo videos for you:
We have prepared several demo videos for you:
| Scenario | Demo video (English) | Demo video (中文) | | Scenario | Demo video (English) | Demo video (中文) |
| -- | ------ | ------ | | -- | ------ | ------ |
| Quant Factor Mining | [Link](https://rdagent.azurewebsites.net/factor_loop?lang=en) | [Link](https://rdagent.azurewebsites.net/factor_loop?lang=zh) | | Quant Factor Mining | [YouTube](https://www.youtube.com/watch?v=X4DK2QZKaKY&t=6s) | [YouTube](https://www.youtube.com/watch?v=X4DK2QZKaKY&t=6s) |
| Quant Factor Mining from reports | [Link](https://rdagent.azurewebsites.net/report_factor?lang=en) | [Link](https://rdagent.azurewebsites.net/report_factor?lang=zh) | | Quant Factor Mining from reports | [YouTube](https://www.youtube.com/watch?v=ECLTXVcSx-c) | [YouTube](https://www.youtube.com/watch?v=ECLTXVcSx-c) |
| Quant Model Optimization | [Link](https://rdagent.azurewebsites.net/model_loop?lang=en) | [Link](https://rdagent.azurewebsites.net/model_loop?lang=zh) | | Quant Model Optimization | [YouTube](https://www.youtube.com/watch?v=dm0dWL49Bc0&t=104s) | [YouTube](https://www.youtube.com/watch?v=dm0dWL49Bc0&t=104s) |
- 📃**Paper**: [R&D-Agent-Quant: A Multi-Agent Framework for Data-Centric Factors and Model Joint Optimization](https://arxiv.org/abs/2505.15155) - 📃**Paper**: [R&D-Agent-Quant: A Multi-Agent Framework for Data-Centric Factors and Model Joint Optimization](https://arxiv.org/abs/2505.15155)
- 👾**Code**: https://github.com/microsoft/RD-Agent/ - 👾**Code**: https://github.com/microsoft/RD-Agent/

View File

@@ -21,8 +21,7 @@
import os import os
import sys import sys
import pkg_resources from importlib.metadata import version as ver
# -- General configuration ------------------------------------------------ # -- General configuration ------------------------------------------------
@@ -63,9 +62,9 @@ author = "Microsoft"
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = pkg_resources.get_distribution("pyqlib").version version = ver("pyqlib")
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
release = pkg_resources.get_distribution("pyqlib").version release = version
# The language for content autogenerated by Sphinx. Refer to documentation # The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages. # for a list of supported languages.

View File

@@ -19,7 +19,6 @@ from qlib.model.base import ModelFT
from qlib.data.dataset import DatasetH from qlib.data.dataset import DatasetH
from qlib.data.dataset.handler import DataHandlerLP from qlib.data.dataset.handler import DataHandlerLP
# To register new datasets, please add them here. # To register new datasets, please add them here.
ALLOW_DATASET = ["Alpha158", "Alpha360"] ALLOW_DATASET = ["Alpha158", "Alpha360"]
# To register new datasets, please add their configurations here. # To register new datasets, please add their configurations here.

View File

@@ -8,7 +8,6 @@ import pandas as pd
from qlib.data.dataset import DatasetH from qlib.data.dataset import DatasetH
device = "cuda" if torch.cuda.is_available() else "cpu" device = "cuda" if torch.cuda.is_available() else "cpu"

View File

@@ -1,9 +1,10 @@
import pickle
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import seaborn as sns import seaborn as sns
from qlib.utils.pickle_utils import restricted_pickle_load
sns.set(color_codes=True) sns.set(color_codes=True)
plt.rcParams["font.sans-serif"] = "SimHei" plt.rcParams["font.sans-serif"] = "SimHei"
plt.rcParams["axes.unicode_minus"] = False plt.rcParams["axes.unicode_minus"] = False
@@ -18,7 +19,7 @@ from tqdm.auto import tqdm
# + # +
with open("./internal_data_s20.pkl", "rb") as f: with open("./internal_data_s20.pkl", "rb") as f:
data = pickle.load(f) data = restricted_pickle_load(f)
data.data_ic_df.columns.names = ["start_date", "end_date"] data.data_ic_df.columns.names = ["start_date", "end_date"]
@@ -52,7 +53,7 @@ pd.DataFrame(meta_m.tn.twm.linear.weight.detach().numpy()).T[0].rolling(5).mean(
# + # +
with open("./tasks_s20.pkl", "rb") as f: with open("./tasks_s20.pkl", "rb") as f:
tasks = pickle.load(f) tasks = restricted_pickle_load(f)
task_df = {} task_df = {}
for t in tasks: for t in tasks:

View File

@@ -4,11 +4,11 @@
import fire import fire
import qlib import qlib
import pickle
from qlib.constant import REG_CN from qlib.constant import REG_CN
from qlib.config import HIGH_FREQ_CONFIG from qlib.config import HIGH_FREQ_CONFIG
from qlib.utils import init_instance_by_config from qlib.utils import init_instance_by_config
from qlib.utils.pickle_utils import restricted_pickle_load
from qlib.data.dataset.handler import DataHandlerLP from qlib.data.dataset.handler import DataHandlerLP
from qlib.data.ops import Operators from qlib.data.ops import Operators
from qlib.data.data import Cal from qlib.data.data import Cal
@@ -125,10 +125,10 @@ class HighfreqWorkflow:
del dataset, dataset_backtest del dataset, dataset_backtest
##=============reload dataset============= ##=============reload dataset=============
with open("dataset.pkl", "rb") as file_dataset: with open("dataset.pkl", "rb") as file_dataset:
dataset = pickle.load(file_dataset) dataset = restricted_pickle_load(file_dataset)
with open("dataset_backtest.pkl", "rb") as file_dataset_backtest: with open("dataset_backtest.pkl", "rb") as file_dataset_backtest:
dataset_backtest = pickle.load(file_dataset_backtest) dataset_backtest = restricted_pickle_load(file_dataset_backtest)
self._prepare_calender_cache() self._prepare_calender_cache()
##=============reinit dataset============= ##=============reinit dataset=============

View File

@@ -9,7 +9,6 @@ from qlib.utils import init_instance_by_config
from qlib.tests.data import GetData from qlib.tests.data import GetData
from qlib.tests.config import CSI300_GBDT_TASK from qlib.tests.config import CSI300_GBDT_TASK
if __name__ == "__main__": if __name__ == "__main__":
# use default data # use default data
provider_uri = "~/.qlib/qlib_data/cn_data" # target_dir provider_uri = "~/.qlib/qlib_data/cn_data" # target_dir

View File

@@ -95,7 +95,6 @@ pos 0.000000
[1706497:MainThread](2021-12-07 14:08:30,627) INFO - qlib.timer - [log.py:113] - Time cost: 0.014s | waiting `async_log` Done [1706497:MainThread](2021-12-07 14:08:30,627) INFO - qlib.timer - [log.py:113] - Time cost: 0.014s | waiting `async_log` Done
""" """
from copy import deepcopy from copy import deepcopy
import qlib import qlib
import fire import fire

View File

@@ -7,6 +7,7 @@ There are two parts including first_train and update_online_pred.
Firstly, we will finish the training and set the trained models to the `online` models. Firstly, we will finish the training and set the trained models to the `online` models.
Next, we will finish updating online predictions. Next, we will finish updating online predictions.
""" """
import copy import copy
import fire import fire
import qlib import qlib

View File

@@ -6,6 +6,7 @@ NOTE:
- !!!!!!!!!!!!!!!TODO!!!!!!!!!!!!!!!!!!!: - !!!!!!!!!!!!!!!TODO!!!!!!!!!!!!!!!!!!!:
- Its structure is not well designed and very ugly, your contribution is welcome to make importing dataset easier - Its structure is not well designed and very ugly, your contribution is welcome to make importing dataset easier
""" """
from datetime import date, datetime as dt from datetime import date, datetime as dt
import os import os
from pathlib import Path from pathlib import Path

View File

@@ -1,13 +1,15 @@
import pickle
import os import os
import pandas as pd import pandas as pd
from tqdm import tqdm from tqdm import tqdm
from qlib.utils.pickle_utils import restricted_pickle_load
for tag in ["test", "valid"]: for tag in ["test", "valid"]:
files = os.listdir(os.path.join("data/orders/", tag)) files = os.listdir(os.path.join("data/orders/", tag))
dfs = [] dfs = []
for f in tqdm(files): for f in tqdm(files):
df = pickle.load(open(os.path.join("data/orders/", tag, f), "rb")) with open(os.path.join("data/orders/", tag, f), "rb") as fr:
df = restricted_pickle_load(fr)
df = df.drop(["$close0"], axis=1) df = df.drop(["$close0"], axis=1)
dfs.append(df) dfs.append(df)

View File

@@ -3,12 +3,12 @@
import qlib import qlib
import fire import fire
import pickle
from datetime import datetime from datetime import datetime
from qlib.constant import REG_CN from qlib.constant import REG_CN
from qlib.data.dataset.handler import DataHandlerLP from qlib.data.dataset.handler import DataHandlerLP
from qlib.utils import init_instance_by_config from qlib.utils import init_instance_by_config
from qlib.utils.pickle_utils import restricted_pickle_load
from qlib.tests.data import GetData from qlib.tests.data import GetData
@@ -42,7 +42,7 @@ class RollingDataWorkflow:
def _load_pre_handler(self, path): def _load_pre_handler(self, path):
with open(path, "rb") as file_dataset: with open(path, "rb") as file_dataset:
pre_handler = pickle.load(file_dataset) pre_handler = restricted_pickle_load(file_dataset)
return pre_handler return pre_handler
def rolling_process(self): def rolling_process(self):

View File

@@ -7,6 +7,7 @@ Qlib provides two kinds of interfaces.
The interface of (1) is `qrun XXX.yaml`. The interface of (2) is script like this, which nearly does the same thing as `qrun XXX.yaml` The interface of (1) is `qrun XXX.yaml`. The interface of (2) is script like this, which nearly does the same thing as `qrun XXX.yaml`
""" """
import qlib import qlib
from qlib.constant import REG_CN from qlib.constant import REG_CN
from qlib.utils import init_instance_by_config, flatten_dict from qlib.utils import init_instance_by_config, flatten_dict
@@ -15,7 +16,6 @@ from qlib.workflow.record_temp import SignalRecord, PortAnaRecord, SigAnaRecord
from qlib.tests.data import GetData from qlib.tests.data import GetData
from qlib.tests.config import CSI300_BENCH, CSI300_GBDT_TASK from qlib.tests.config import CSI300_BENCH, CSI300_GBDT_TASK
if __name__ == "__main__": if __name__ == "__main__":
# use default data # use default data
provider_uri = "~/.qlib/qlib_data/cn_data" # target_dir provider_uri = "~/.qlib/qlib_data/cn_data" # target_dir

View File

@@ -69,10 +69,9 @@ rl = [
"torch", "torch",
"numpy<2.0.0", "numpy<2.0.0",
] ]
# We exclude black version 26.1.0 due to known issues with nbqa when formatting Jupyter notebooks,
# which can cause false-positive --check results and inconsistent notebook formatting.
lint = [ lint = [
"black!=26.1.0", "black",
"pylint", "pylint",
"mypy<1.5.0", "mypy<1.5.0",
"flake8", "flake8",

View File

@@ -18,7 +18,6 @@ from tqdm.auto import tqdm
from ..utils.time import Freq from ..utils.time import Freq
PORT_METRIC = Dict[str, Tuple[pd.DataFrame, dict]] PORT_METRIC = Dict[str, Tuple[pd.DataFrame, dict]]
INDICATOR_METRIC = Dict[str, Tuple[pd.DataFrame, Indicator]] INDICATOR_METRIC = Dict[str, Tuple[pd.DataFrame, Indicator]]

View File

@@ -4,6 +4,5 @@
import fire import fire
from qlib.tests.data import GetData from qlib.tests.data import GetData
if __name__ == "__main__": if __name__ == "__main__":
fire.Fire(GetData) fire.Fire(GetData)

View File

@@ -10,6 +10,7 @@ Two modes are supported
- server - server
""" """
from __future__ import annotations from __future__ import annotations
import os import os

View File

@@ -11,7 +11,6 @@ from qlib.utils import init_instance_by_config
from qlib.data.dataset import DatasetH from qlib.data.dataset import DatasetH
device = "cuda" if torch.cuda.is_available() else "cpu" device = "cuda" if torch.cuda.is_available() else "cpu"

View File

@@ -20,7 +20,6 @@ from ..data import D
from ..config import C from ..config import C
from ..data.dataset.utils import get_level_index from ..data.dataset.utils import get_level_index
logger = get_module_logger("Evaluate") logger = get_module_logger("Evaluate")

View File

@@ -3,5 +3,4 @@
from .data_selection import MetaTaskDS, MetaDatasetDS, MetaModelDS from .data_selection import MetaTaskDS, MetaDatasetDS, MetaModelDS
__all__ = ["MetaTaskDS", "MetaDatasetDS", "MetaModelDS"] __all__ = ["MetaTaskDS", "MetaDatasetDS", "MetaModelDS"]

View File

@@ -4,5 +4,4 @@
from .dataset import MetaDatasetDS, MetaTaskDS from .dataset import MetaDatasetDS, MetaTaskDS
from .model import MetaModelDS from .model import MetaModelDS
__all__ = ["MetaDatasetDS", "MetaTaskDS", "MetaModelDS"] __all__ = ["MetaDatasetDS", "MetaTaskDS", "MetaModelDS"]

View File

@@ -317,7 +317,7 @@ class TabnetModel(Model):
feature = x_train_values.float().to(self.device) feature = x_train_values.float().to(self.device)
label = y_train_values.float().to(self.device) label = y_train_values.float().to(self.device)
priors = 1 - S_mask priors = 1 - S_mask
(vec, sparse_loss) = self.tabnet_model(feature, priors) vec, sparse_loss = self.tabnet_model(feature, priors)
f = self.tabnet_decoder(vec) f = self.tabnet_decoder(vec)
loss = self.pretrain_loss_fn(label, f, S_mask) loss = self.pretrain_loss_fn(label, f, S_mask)
@@ -348,7 +348,7 @@ class TabnetModel(Model):
S_mask = S_mask.to(self.device) S_mask = S_mask.to(self.device)
priors = 1 - S_mask priors = 1 - S_mask
with torch.no_grad(): with torch.no_grad():
(vec, sparse_loss) = self.tabnet_model(feature, priors) vec, sparse_loss = self.tabnet_model(feature, priors)
f = self.tabnet_decoder(vec) f = self.tabnet_decoder(vec)
loss = self.pretrain_loss_fn(label, f, S_mask) loss = self.pretrain_loss_fn(label, f, S_mask)

View File

@@ -12,6 +12,7 @@ from ...data import D
from ...config import C from ...config import C
from ...log import get_module_logger from ...log import get_module_logger
from ...utils import get_next_trading_date from ...utils import get_next_trading_date
from ...utils.pickle_utils import restricted_pickle_load
from ...backtest.exchange import Exchange from ...backtest.exchange import Exchange
log = get_module_logger("utils") log = get_module_logger("utils")
@@ -30,7 +31,7 @@ def load_instance(file_path):
if not file_path.exists(): if not file_path.exists():
raise ValueError("Cannot find file {}".format(file_path)) raise ValueError("Cannot find file {}".format(file_path))
with file_path.open("rb") as fr: with file_path.open("rb") as fr:
instance = pickle.load(fr) instance = restricted_pickle_load(fr)
return instance return instance

View File

@@ -3,5 +3,4 @@
from .analysis_model_performance import model_performance_graph from .analysis_model_performance import model_performance_graph
__all__ = ["model_performance_graph"] __all__ = ["model_performance_graph"]

View File

@@ -7,5 +7,4 @@ from .report import report_graph
from .rank_label import rank_label_graph from .rank_label import rank_label_graph
from .risk_analysis import risk_analysis_graph from .risk_analysis import risk_analysis_graph
__all__ = ["cumulative_return_graph", "score_ic_graph", "report_graph", "rank_label_graph", "risk_analysis_graph"] __all__ = ["cumulative_return_graph", "score_ic_graph", "report_graph", "rank_label_graph", "risk_analysis_graph"]

View File

@@ -12,6 +12,7 @@ Here is an example.
fa.plot_all(wspace=0.3, sub_figsize=(12, 3), col_n=5) fa.plot_all(wspace=0.3, sub_figsize=(12, 3), col_n=5)
""" """
import pandas as pd import pandas as pd
import numpy as np import numpy as np
from qlib.contrib.report.data.base import FeaAnalyser from qlib.contrib.report.data.base import FeaAnalyser

View File

@@ -7,6 +7,7 @@ Assumptions
- The analyse each feature individually - The analyse each feature individually
""" """
import pandas as pd import pandas as pd
from qlib.log import TimeInspector from qlib.log import TimeInspector
from qlib.contrib.report.utils import sub_fig_generator from qlib.contrib.report.utils import sub_fig_generator

View File

@@ -16,7 +16,6 @@ from .rule_strategy import (
from .cost_control import SoftTopkStrategy from .cost_control import SoftTopkStrategy
__all__ = [ __all__ = [
"TopkDropoutStrategy", "TopkDropoutStrategy",
"WeightStrategyBase", "WeightStrategyBase",

View File

@@ -1,101 +1,117 @@
# Copyright (c) Microsoft Corporation. # Copyright (c) Microsoft Corporation.
# Licensed under the MIT License. # Licensed under the MIT License.
"""
This strategy is not well maintained
"""
from .order_generator import OrderGenWInteract from .order_generator import OrderGenWInteract
from .signal_strategy import WeightStrategyBase from .signal_strategy import WeightStrategyBase
import copy
class SoftTopkStrategy(WeightStrategyBase): class SoftTopkStrategy(WeightStrategyBase):
def __init__( def __init__(
self, self,
model, model=None,
dataset, dataset=None,
topk, topk=None,
order_generator_cls_or_obj=OrderGenWInteract, order_generator_cls_or_obj=OrderGenWInteract,
max_sold_weight=1.0, max_sold_weight=1.0,
trade_impact_limit=None,
risk_degree=0.95, risk_degree=0.95,
buy_method="first_fill", buy_method="first_fill",
trade_exchange=None,
level_infra=None,
common_infra=None,
**kwargs, **kwargs,
): ):
""" """
Refactored SoftTopkStrategy with a budget-constrained rebalancing engine.
Parameters Parameters
---------- ----------
topk : int topk : int
top-N stocks to buy The number of top-N stocks to be held in the portfolio.
trade_impact_limit : float
Maximum weight change for each stock in one trade. If None, fallback to max_sold_weight.
max_sold_weight : float
Backward-compatible alias for trade_impact_limit. Use 1.0 to effectively disable the limit.
risk_degree : float risk_degree : float
position percentage of total value buy_method: The target percentage of total value to be invested.
rank_fill: assign the weight stocks that rank high first(1/topk max)
average_fill: assign the weight to the stocks rank high averagely.
""" """
super(SoftTopkStrategy, self).__init__( super(SoftTopkStrategy, self).__init__(
model, dataset, order_generator_cls_or_obj, trade_exchange, level_infra, common_infra, **kwargs model=model, dataset=dataset, order_generator_cls_or_obj=order_generator_cls_or_obj, **kwargs
) )
self.topk = topk self.topk = topk
self.max_sold_weight = max_sold_weight self.trade_impact_limit = trade_impact_limit if trade_impact_limit is not None else max_sold_weight
self.risk_degree = risk_degree self.risk_degree = risk_degree
self.buy_method = buy_method self.buy_method = buy_method
def get_risk_degree(self, trade_step=None): def get_risk_degree(self, trade_step=None):
"""get_risk_degree
Return the proportion of your total value you will used in investment.
Dynamically risk_degree will result in Market timing
"""
# It will use 95% amount of your total value by default
return self.risk_degree return self.risk_degree
def generate_target_weight_position(self, score, current, trade_start_time, trade_end_time): def generate_target_weight_position(self, score, current, trade_start_time, trade_end_time, **kwargs):
""" """
Parameters Generates target position using Proportional Budget Allocation.
---------- Ensures deterministic sells and synchronized buys under impact limits.
score:
pred score for this trade date, pd.Series, index is stock_id, contain 'score' column
current:
current position, use Position() class
trade_date:
trade date
generate target position from score for this date and the current position
The cache is not considered in the position
""" """
# TODO:
# If the current stock list is more than topk(eg. The weights are modified
# by risk control), the weight will not be handled correctly.
buy_signal_stocks = set(score.sort_values(ascending=False).iloc[: self.topk].index)
cur_stock_weight = current.get_stock_weight_dict(only_stock=True)
if len(cur_stock_weight) == 0: if self.topk is None or self.topk <= 0:
final_stock_weight = {code: 1 / self.topk for code in buy_signal_stocks} return {}
else:
final_stock_weight = copy.deepcopy(cur_stock_weight) def apply_impact_limit(weight):
sold_stock_weight = 0.0 return weight if self.trade_impact_limit is None else min(weight, self.trade_impact_limit)
for stock_id in final_stock_weight:
if stock_id not in buy_signal_stocks: ideal_per_stock = self.risk_degree / self.topk
sw = min(self.max_sold_weight, final_stock_weight[stock_id]) ideal_list = score.sort_values(ascending=False).iloc[: self.topk].index.tolist()
sold_stock_weight += sw
final_stock_weight[stock_id] -= sw cur_weights = current.get_stock_weight_dict(only_stock=True)
if self.buy_method == "first_fill": initial_total_weight = sum(cur_weights.values())
for stock_id in buy_signal_stocks:
add_weight = min( # --- Case A: Cold Start ---
max(1 / self.topk - final_stock_weight.get(stock_id, 0), 0.0), if not cur_weights:
sold_stock_weight, fill = apply_impact_limit(ideal_per_stock)
) return {code: fill for code in ideal_list}
final_stock_weight[stock_id] = final_stock_weight.get(stock_id, 0.0) + add_weight
sold_stock_weight -= add_weight # --- Case B: Rebalancing ---
elif self.buy_method == "average_fill": all_tickers = set(cur_weights.keys()) | set(ideal_list)
for stock_id in buy_signal_stocks: next_weights = {t: cur_weights.get(t, 0.0) for t in all_tickers}
final_stock_weight[stock_id] = final_stock_weight.get(stock_id, 0.0) + sold_stock_weight / len(
buy_signal_stocks # Phase 1: Deterministic Sell Phase
) released_cash = 0.0
else: for t in list(next_weights.keys()):
raise ValueError("Buy method not found") cur = next_weights[t]
return final_stock_weight if cur <= 1e-8:
continue
if t not in ideal_list:
sell = apply_impact_limit(cur)
next_weights[t] -= sell
released_cash += sell
elif cur > ideal_per_stock + 1e-8:
excess = cur - ideal_per_stock
sell = apply_impact_limit(excess)
next_weights[t] -= sell
released_cash += sell
# Phase 2: Budget Calculation
# Budget = Cash from sells + Available space from target risk degree
total_budget = released_cash + (self.risk_degree - initial_total_weight)
# Phase 3: Proportional Buy Allocation
if total_budget > 1e-8:
shortfalls = {
t: (ideal_per_stock - next_weights.get(t, 0.0))
for t in ideal_list
if next_weights.get(t, 0.0) < ideal_per_stock - 1e-8
}
if shortfalls:
total_shortfall = sum(shortfalls.values())
# Normalize total_budget to not exceed total_shortfall
available_to_spend = min(total_budget, total_shortfall)
for t, shortfall in shortfalls.items():
# Every stock gets its fair share based on its distance to target
share_of_budget = (shortfall / total_shortfall) * available_to_spend
# Capped by impact limit
max_buy_cap = apply_impact_limit(shortfall)
next_weights[t] += min(share_of_budget, max_buy_cap)
return {k: v for k, v in next_weights.items() if v > 1e-8}

View File

@@ -5,5 +5,4 @@ from .base import BaseOptimizer
from .optimizer import PortfolioOptimizer from .optimizer import PortfolioOptimizer
from .enhanced_indexing import EnhancedIndexingOptimizer from .enhanced_indexing import EnhancedIndexingOptimizer
__all__ = ["BaseOptimizer", "PortfolioOptimizer", "EnhancedIndexingOptimizer"] __all__ = ["BaseOptimizer", "PortfolioOptimizer", "EnhancedIndexingOptimizer"]

View File

@@ -9,7 +9,6 @@ from typing import Union, Optional, Dict, Any, List
from qlib.log import get_module_logger from qlib.log import get_module_logger
from .base import BaseOptimizer from .base import BaseOptimizer
logger = get_module_logger("EnhancedIndexingOptimizer") logger = get_module_logger("EnhancedIndexingOptimizer")

View File

@@ -4,6 +4,7 @@
""" """
This order generator is for strategies based on WeightStrategyBase This order generator is for strategies based on WeightStrategyBase
""" """
from ...backtest.position import Position from ...backtest.position import Position
from ...backtest.exchange import Exchange from ...backtest.exchange import Exchange

View File

@@ -5,6 +5,7 @@ This module is not a necessary part of Qlib.
They are just some tools for convenience They are just some tools for convenience
It is should not imported into the core part of qlib It is should not imported into the core part of qlib
""" """
import torch import torch
import numpy as np import numpy as np
import pandas as pd import pandas as pd

View File

@@ -13,7 +13,6 @@ import yaml
from .config import TunerConfigManager from .config import TunerConfigManager
args_parser = argparse.ArgumentParser(prog="tuner") args_parser = argparse.ArgumentParser(prog="tuner")
args_parser.add_argument( args_parser.add_argument(
"-c", "-c",

View File

@@ -6,7 +6,6 @@
from hyperopt import hp from hyperopt import hp
TopkAmountStrategySpace = { TopkAmountStrategySpace = {
"topk": hp.choice("topk", [30, 35, 40]), "topk": hp.choice("topk", [30, 35, 40]),
"buffer_margin": hp.choice("buffer_margin", [200, 250, 300]), "buffer_margin": hp.choice("buffer_margin", [200, 250, 300]),

View File

@@ -3,5 +3,4 @@
from .record_temp import MultiSegRecord from .record_temp import MultiSegRecord
from .record_temp import SignalMseRecord from .record_temp import SignalMseRecord
__all__ = ["MultiSegRecord", "SignalMseRecord"] __all__ = ["MultiSegRecord", "SignalMseRecord"]

View File

@@ -36,7 +36,6 @@ from .cache import (
MemoryCalendarCache, MemoryCalendarCache,
) )
__all__ = [ __all__ = [
"D", "D",
"CalendarProvider", "CalendarProvider",

View File

@@ -19,7 +19,6 @@ from .loader import DataLoader
from . import processor as processor_module from . import processor as processor_module
from . import loader as data_loader_module from . import loader as data_loader_module
DATA_KEY_TYPE = Literal["raw", "infer", "learn"] DATA_KEY_TYPE = Literal["raw", "infer", "learn"]

View File

@@ -13,6 +13,7 @@ The calculation of both <period_time, feature> and <observe_time, feature> data
2) concatenate all th collasped data, we will get data with format <observe_time, feature>. 2) concatenate all th collasped data, we will get data with format <observe_time, feature>.
Qlib will use the operator `P` to perform the collapse. Qlib will use the operator `P` to perform the collapse.
""" """
import numpy as np import numpy as np
import pandas as pd import pandas as pd
from qlib.data.ops import ElemOperator from qlib.data.ops import ElemOperator

View File

@@ -3,5 +3,4 @@
from .storage import CalendarStorage, InstrumentStorage, FeatureStorage, CalVT, InstVT, InstKT from .storage import CalendarStorage, InstrumentStorage, FeatureStorage, CalVT, InstVT, InstKT
__all__ = ["CalendarStorage", "InstrumentStorage", "FeatureStorage", "CalVT", "InstVT", "InstKT"] __all__ = ["CalendarStorage", "InstrumentStorage", "FeatureStorage", "CalVT", "InstVT", "InstKT"]

View File

@@ -156,7 +156,7 @@ class FileCalendarStorage(FileStorageMixin, CalendarStorage):
def index(self, value: CalVT) -> int: def index(self, value: CalVT) -> int:
self.check() self.check()
calendar = self._read_calendar() calendar = self._read_calendar()
return int(np.argwhere(calendar == value)[0]) return calendar.index(value)
def insert(self, index: int, value: CalVT): def insert(self, index: int, value: CalVT):
calendar = self._read_calendar() calendar = self._read_calendar()

View File

@@ -5,5 +5,4 @@ import warnings
from .base import Model from .base import Model
__all__ = ["Model", "warnings"] __all__ = ["Model", "warnings"]

View File

@@ -4,5 +4,4 @@
from .task import MetaTask from .task import MetaTask
from .dataset import MetaTaskDataset from .dataset import MetaTaskDataset
__all__ = ["MetaTask", "MetaTaskDataset"] __all__ = ["MetaTask", "MetaTaskDataset"]

View File

@@ -6,7 +6,6 @@ from .poet import POETCovEstimator
from .shrink import ShrinkCovEstimator from .shrink import ShrinkCovEstimator
from .structured import StructuredCovEstimator from .structured import StructuredCovEstimator
__all__ = [ __all__ = [
"RiskModel", "RiskModel",
"POETCovEstimator", "POETCovEstimator",

View File

@@ -9,7 +9,6 @@ import tempfile
from importlib import import_module from importlib import import_module
from ruamel.yaml import YAML from ruamel.yaml import YAML
DELETE_KEY = "_delete_" DELETE_KEY = "_delete_"

View File

@@ -3,6 +3,7 @@
""" """
This module covers some utility functions that operate on data or basic object This module covers some utility functions that operate on data or basic object
""" """
from copy import deepcopy from copy import deepcopy
from typing import List, Union from typing import List, Union

View File

@@ -3,6 +3,7 @@
""" """
Time related utils are compiled in this script Time related utils are compiled in this script
""" """
import bisect import bisect
from datetime import datetime, time, date, timedelta from datetime import datetime, time, date, timedelta
from typing import List, Optional, Tuple, Union from typing import List, Optional, Tuple, Union
@@ -14,7 +15,6 @@ import pandas as pd
from qlib.config import C from qlib.config import C
from qlib.constant import REG_CN, REG_TW, REG_US from qlib.constant import REG_CN, REG_TW, REG_US
CN_TIME = [ CN_TIME = [
datetime.strptime("9:30", "%H:%M"), datetime.strptime("9:30", "%H:%M"),
datetime.strptime("11:30", "%H:%M"), datetime.strptime("11:30", "%H:%M"),

View File

@@ -16,7 +16,6 @@ from .recorder import Recorder
from ..log import get_module_logger from ..log import get_module_logger
from ..utils.exceptions import ExpAlreadyExistError from ..utils.exceptions import ExpAlreadyExistError
logger = get_module_logger("workflow") logger = get_module_logger("workflow")

View File

@@ -22,7 +22,6 @@ from ..utils.data import deepcopy_basic_type
from ..utils.exceptions import QlibException from ..utils.exceptions import QlibException
from ..contrib.eva.alpha import calc_ic, calc_long_short_return, calc_long_short_prec from ..contrib.eva.alpha import calc_ic, calc_long_short_return, calc_long_short_prec
logger = get_module_logger("workflow", logging.INFO) logger = get_module_logger("workflow", logging.INFO)
@@ -476,7 +475,13 @@ class PortAnaRecord(ACRecordTemp):
if self.backtest_config["start_time"] is None: if self.backtest_config["start_time"] is None:
self.backtest_config["start_time"] = dt_values.min() self.backtest_config["start_time"] = dt_values.min()
if self.backtest_config["end_time"] is None: if self.backtest_config["end_time"] is None:
self.backtest_config["end_time"] = get_date_by_shift(dt_values.max(), 1) self.backtest_config["end_time"] = get_date_by_shift(dt_values.max(), -1)
warnings.warn(
"No explicit backtest end_time provided. "
"Qlib requires one extra calendar step to determine the right boundary of a bar. "
"Therefore the end_time is shifted backward by one trading day from "
f"{dt_values.max()} -> {self.backtest_config['end_time']}."
)
artifact_objects = {} artifact_objects = {}
# custom strategy and get backtest # custom strategy and get backtest

View File

@@ -3,6 +3,7 @@
""" """
TaskGenerator module can generate many tasks based on TaskGen and some task templates. TaskGenerator module can generate many tasks based on TaskGen and some task templates.
""" """
import abc import abc
import copy import copy
import pandas as pd import pandas as pd

View File

@@ -12,6 +12,7 @@ A task in TaskManager consists of 3 parts
- tasks status: the status of the task - tasks status: the status of the task
- tasks result: A user can get the task with the task description and task result. - tasks result: A user can get the task with the task description and task result.
""" """
import concurrent import concurrent
import pickle import pickle
import time import time

View File

@@ -280,11 +280,20 @@ class Normalize:
self._symbol_field_name = symbol_field_name self._symbol_field_name = symbol_field_name
self._end_date = kwargs.get("end_date", None) self._end_date = kwargs.get("end_date", None)
self._max_workers = max_workers self._max_workers = max_workers
self.interval = kwargs.get("interval", "1d")
self._normalize_obj = normalize_class( self._normalize_obj = normalize_class(
date_field_name=date_field_name, symbol_field_name=symbol_field_name, **kwargs date_field_name=date_field_name, symbol_field_name=symbol_field_name, **kwargs
) )
def format_data(self, df: pd.DataFrame):
if self.interval == "1d":
try:
pd.to_datetime(df.iloc[-1]["date"], format="%Y-%m-%d", errors="raise")
except Exception:
df = df.iloc[:-1]
return df
def _executor(self, file_path: Path): def _executor(self, file_path: Path):
file_path = Path(file_path) file_path = Path(file_path)
@@ -300,14 +309,18 @@ class Normalize:
keep_default_na=False, keep_default_na=False,
na_values={col: symbol_na if col == self._symbol_field_name else default_na for col in columns}, na_values={col: symbol_na if col == self._symbol_field_name else default_na for col in columns},
) )
df = self.format_data(df=df)
# NOTE: It has been reported that there may be some problems here, and the specific issues will be dealt with when they are identified. if not df.empty:
df = self._normalize_obj.normalize(df) # NOTE: It has been reported that there may be some problems here, and the specific issues will be dealt with when they are identified.
if df is not None and not df.empty: df = self._normalize_obj.normalize(df)
if self._end_date is not None: if df is not None and not df.empty:
_mask = pd.to_datetime(df[self._date_field_name]) <= pd.Timestamp(self._end_date) if self._end_date is not None:
df = df[_mask] _mask = pd.to_datetime(df[self._date_field_name]) <= pd.Timestamp(self._end_date)
df.to_csv(self._target_dir.joinpath(file_path.name), index=False) df = df[_mask]
df.to_csv(self._target_dir.joinpath(file_path.name), index=False)
else:
logger.warning(f"{file_path.stem} source data is empty and will not undergo normalization processing.")
def normalize(self): def normalize(self):
logger.info("normalize data......") logger.info("normalize data......")

View File

@@ -22,7 +22,6 @@ from data_collector.index import IndexBase
from data_collector.utils import get_calendar_list, get_trading_date_by_shift, deco_retry from data_collector.utils import get_calendar_list, get_trading_date_by_shift, deco_retry
from data_collector.utils import get_instruments from data_collector.utils import get_instruments
NEW_COMPANIES_URL = ( NEW_COMPANIES_URL = (
"https://oss-ch.csindex.com.cn/static/html/csindex/public/uploads/file/autofile/cons/{index_code}cons.xls" "https://oss-ch.csindex.com.cn/static/html/csindex/public/uploads/file/autofile/cons/{index_code}cons.xls"
) )

View File

@@ -19,7 +19,6 @@ from time import mktime
from datetime import datetime as dt from datetime import datetime as dt
import time import time
_CG_CRYPTO_SYMBOLS = None _CG_CRYPTO_SYMBOLS = None

View File

@@ -16,7 +16,6 @@ from tqdm import tqdm
from loguru import logger from loguru import logger
from fake_useragent import UserAgent from fake_useragent import UserAgent
CUR_DIR = Path(__file__).resolve().parent CUR_DIR = Path(__file__).resolve().parent
sys.path.append(str(CUR_DIR.parent.parent)) sys.path.append(str(CUR_DIR.parent.parent))
@@ -24,7 +23,6 @@ from data_collector.index import IndexBase
from data_collector.utils import deco_retry, get_calendar_list, get_trading_date_by_shift from data_collector.utils import deco_retry, get_calendar_list, get_trading_date_by_shift
from data_collector.utils import get_instruments from data_collector.utils import get_instruments
WIKI_URL = "https://en.wikipedia.org/wiki" WIKI_URL = "https://en.wikipedia.org/wiki"
WIKI_INDEX_NAME_MAP = { WIKI_INDEX_NAME_MAP = {

View File

@@ -3,12 +3,10 @@
import re import re
import copy import copy
import datetime
import importlib import importlib
import time import time
import bisect import bisect
import pickle import pickle
import random
import requests import requests
import functools import functools
from pathlib import Path from pathlib import Path
@@ -22,10 +20,13 @@ from tqdm import tqdm
from functools import partial from functools import partial
from concurrent.futures import ProcessPoolExecutor from concurrent.futures import ProcessPoolExecutor
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import baostock as bs
from qlib.utils.pickle_utils import restricted_pickle_load
HS_SYMBOLS_URL = "http://app.finance.ifeng.com/hq/list.php?type=stock_a&class={s_type}" HS_SYMBOLS_URL = "http://app.finance.ifeng.com/hq/list.php?type=stock_a&class={s_type}"
CALENDAR_URL_BASE = "http://push2his.eastmoney.com/api/qt/stock/kline/get?secid={market}.{bench_code}&fields1=f1%2Cf2%2Cf3%2Cf4%2Cf5&fields2=f51%2Cf52%2Cf53%2Cf54%2Cf55%2Cf56%2Cf57%2Cf58&klt=101&fqt=0" CALENDAR_URL_BASE = "http://push2his.eastmoney.com/api/qt/stock/kline/get?secid={market}.{bench_code}&fields1=f1%2Cf2%2Cf3%2Cf4%2Cf5&fields2=f51%2Cf52%2Cf53%2Cf54%2Cf55%2Cf56%2Cf57%2Cf58&klt=101&fqt=0&beg=19900101&end=20991231"
SZSE_CALENDAR_URL = "http://www.szse.cn/api/report/exchange/onepersistenthour/monthList?month={month}&random={random}" SZSE_CALENDAR_URL = "http://www.szse.cn/api/report/exchange/onepersistenthour/monthList?month={month}&random={random}"
CALENDAR_BENCH_URL_MAP = { CALENDAR_BENCH_URL_MAP = {
@@ -40,24 +41,6 @@ CALENDAR_BENCH_URL_MAP = {
"BR_ALL": "^BVSP", "BR_ALL": "^BVSP",
} }
CHROME_UA_POOL = [
# Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/121.0.6167.85 Safari/537.36",
# macOS
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/121.0.0.0 Safari/537.36",
# Linux
"Mozilla/5.0 (X11; Linux x86_64) " # pylint: disable=W1404
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36",
]
_BENCH_CALENDAR_LIST = None _BENCH_CALENDAR_LIST = None
_ALL_CALENDAR_LIST = None _ALL_CALENDAR_LIST = None
_HS_SYMBOLS = None _HS_SYMBOLS = None
@@ -71,16 +54,6 @@ _CALENDAR_MAP = {}
MINIMUM_SYMBOLS_NUM = 3900 MINIMUM_SYMBOLS_NUM = 3900
def build_headers():
return {
"User-Agent": random.choice(CHROME_UA_POOL),
"Accept": "application/json,text/plain,*/*",
"Accept-Language": "zh-CN,zh;q=0.9",
"Referer": "https://quote.eastmoney.com/",
"Connection": "keep-alive",
}
def get_calendar_list(bench_code="CSI300") -> List[pd.Timestamp]: def get_calendar_list(bench_code="CSI300") -> List[pd.Timestamp]:
"""get SH/SZ history calendar list """get SH/SZ history calendar list
@@ -96,38 +69,16 @@ def get_calendar_list(bench_code="CSI300") -> List[pd.Timestamp]:
logger.info(f"get calendar list: {bench_code}......") logger.info(f"get calendar list: {bench_code}......")
def _get_calendar(url, max_retry=3): def _get_calendar(end_date):
session = requests.Session() bs.login()
session.headers.update(build_headers()) rs = bs.query_trade_dates(start_date="2005-01-01", end_date=end_date)
current_datetime = datetime.datetime.now() data_list = []
cur_year = current_datetime.year while (rs.error_code == "0") & rs.next():
res_list = [] data_list.append(rs.get_row_data())
failed_years = [] bs.logout()
for year in range(2000, cur_year + 1): df = pd.DataFrame(data_list, columns=rs.fields)
start = f"{year}0101" trade_days = df[df["is_trading_day"] == "1"]["calendar_date"]
end = f"{year}1231" return sorted(map(pd.Timestamp, trade_days.to_list()))
formatted_url = url + f"&beg={start}&end={end}".format(start=start, end=end)
for attempt in range(max_retry):
try:
resp = session.get(formatted_url, timeout=10)
resp.raise_for_status()
data = resp.json().get("data")
if not data or "klines" not in data:
raise ValueError("missing klines")
res_list.extend(pd.Timestamp(x.split(",")[0]) for x in data["klines"])
break
except Exception as e:
time.sleep(random.uniform(0.8, 1.5))
else:
failed_years.append(year)
if failed_years:
logger.warning(f"Calendar incomplete, failed years: {failed_years}")
return sorted(set(res_list))
calendar = _CALENDAR_MAP.get(bench_code, None) calendar = _CALENDAR_MAP.get(bench_code, None)
if calendar is None: if calendar is None:
@@ -147,7 +98,8 @@ def get_calendar_list(bench_code="CSI300") -> List[pd.Timestamp]:
filtered_dates = dates[(dates >= "2000-01-04") & (dates <= pd.Timestamp.today().normalize())] filtered_dates = dates[(dates >= "2000-01-04") & (dates <= pd.Timestamp.today().normalize())]
calendar = filtered_dates.tolist() calendar = filtered_dates.tolist()
else: else:
calendar = _get_calendar(CALENDAR_BENCH_URL_MAP[bench_code]) end_date = time.strftime("%Y-%m-%d", time.localtime())
calendar = _get_calendar(end_date=end_date)
_CALENDAR_MAP[bench_code] = calendar _CALENDAR_MAP[bench_code] = calendar
logger.info(f"end of get calendar list: {bench_code}.") logger.info(f"end of get calendar list: {bench_code}.")
return calendar return calendar
@@ -305,11 +257,7 @@ def get_hs_stock_symbols() -> list:
# Add suffix after the stock code to conform to yahooquery standard, otherwise the data will not be fetched. # Add suffix after the stock code to conform to yahooquery standard, otherwise the data will not be fetched.
_symbols = [ _symbols = [
( _symbol + ".ss" if _symbol.startswith("6") else _symbol + ".sz" if _symbol.startswith(("0", "3")) else None
_symbol + ".ss"
if _symbol.startswith("6")
else _symbol + ".sz" if _symbol.startswith(("0", "3")) else None
)
for _symbol in _symbols for _symbol in _symbols
] ]
_symbols = [_symbol for _symbol in _symbols if _symbol is not None] _symbols = [_symbol for _symbol in _symbols if _symbol is not None]
@@ -328,7 +276,7 @@ def get_hs_stock_symbols() -> list:
symbol_cache_path.parent.mkdir(parents=True, exist_ok=True) symbol_cache_path.parent.mkdir(parents=True, exist_ok=True)
if symbol_cache_path.exists(): if symbol_cache_path.exists():
with symbol_cache_path.open("rb") as fp: with symbol_cache_path.open("rb") as fp:
cache_symbols = pickle.load(fp) cache_symbols = restricted_pickle_load(fp)
symbols |= cache_symbols symbols |= cache_symbols
with symbol_cache_path.open("wb") as fp: with symbol_cache_path.open("wb") as fp:
pickle.dump(symbols, fp) pickle.dump(symbols, fp)
@@ -345,20 +293,14 @@ def get_us_stock_symbols(qlib_data_path: [str, Path] = None) -> list:
------- -------
stock symbols stock symbols
""" """
import akshare as ak # pylint: disable=C0415
global _US_SYMBOLS # pylint: disable=W0603 global _US_SYMBOLS # pylint: disable=W0603
@deco_retry @deco_retry
def _get_eastmoney(): def _get_eastmoney():
url = "http://4.push2.eastmoney.com/api/qt/clist/get?pn=1&pz=10000&fs=m:105,m:106,m:107&fields=f12" df = ak.get_us_stock_name()
resp = requests.get(url, timeout=None) _symbols = df["symbol"].to_list()
if resp.status_code != 200:
raise ValueError("request error")
try:
_symbols = [_v["f12"].replace("_", "-P") for _v in resp.json()["data"]["diff"].values()]
except Exception as e:
logger.warning(f"request error: {e}")
raise
if len(_symbols) < 8000: if len(_symbols) < 8000:
raise ValueError("request error") raise ValueError("request error")
@@ -420,14 +362,7 @@ def get_us_stock_symbols(qlib_data_path: [str, Path] = None) -> list:
s_ = s_.strip("*") s_ = s_.strip("*")
return s_ return s_
_US_SYMBOLS = sorted( _US_SYMBOLS = sorted(set(map(_format, filter(lambda x: len(x) < 8 and not x.endswith("WS"), _all_symbols))))
set(
map(
_format,
filter(lambda x: len(x) < 8 and not x.endswith("WS"), _all_symbols),
)
)
)
return _US_SYMBOLS return _US_SYMBOLS
@@ -541,10 +476,7 @@ def get_en_fund_symbols(qlib_data_path: [str, Path] = None) -> list:
raise ValueError("request error") raise ValueError("request error")
try: try:
_symbols = [] _symbols = []
for sub_data in re.findall( for sub_data in re.findall(r"[\[](.*?)[\]]", resp.content.decode().split("= [")[-1].replace("];", "")):
r"[\[](.*?)[\]]",
resp.content.decode().split("= [")[-1].replace("];", ""),
):
data = sub_data.replace('"', "").replace("'", "") data = sub_data.replace('"', "").replace("'", "")
# TODO: do we need other information, like fund_name from ['000001', 'HXCZHH', '华夏成长混合', '混合型', 'HUAXIACHENGZHANGHUNHE'] # TODO: do we need other information, like fund_name from ['000001', 'HXCZHH', '华夏成长混合', '混合型', 'HUAXIACHENGZHANGHUNHE']
_symbols.append(data.split(",")[0]) _symbols.append(data.split(",")[0])
@@ -725,11 +657,7 @@ def get_instruments(
""" """
_cur_module = importlib.import_module("data_collector.{}.collector".format(market_index)) _cur_module = importlib.import_module("data_collector.{}.collector".format(market_index))
obj = getattr(_cur_module, f"{index_name.upper()}Index")( obj = getattr(_cur_module, f"{index_name.upper()}Index")(
qlib_dir=qlib_dir, qlib_dir=qlib_dir, index_name=index_name, freq=freq, request_retry=request_retry, retry_sleep=retry_sleep
index_name=index_name,
freq=freq,
request_retry=request_retry,
retry_sleep=retry_sleep,
) )
getattr(obj, method)() getattr(obj, method)()
@@ -737,10 +665,7 @@ def get_instruments(
def _get_all_1d_data(_date_field_name: str, _symbol_field_name: str, _1d_data_all: pd.DataFrame): def _get_all_1d_data(_date_field_name: str, _symbol_field_name: str, _1d_data_all: pd.DataFrame):
df = copy.deepcopy(_1d_data_all) df = copy.deepcopy(_1d_data_all)
df.reset_index(inplace=True) df.reset_index(inplace=True)
df.rename( df.rename(columns={"datetime": _date_field_name, "instrument": _symbol_field_name}, inplace=True)
columns={"datetime": _date_field_name, "instrument": _symbol_field_name},
inplace=True,
)
df.columns = list(map(lambda x: x[1:] if x.startswith("$") else x, df.columns)) df.columns = list(map(lambda x: x[1:] if x.startswith("$") else x, df.columns))
return df return df

View File

@@ -4,6 +4,5 @@
import fire import fire
from qlib.tests.data import GetData from qlib.tests.data import GetData
if __name__ == "__main__": if __name__ == "__main__":
fire.Fire(GetData) fire.Fire(GetData)

View File

@@ -3,7 +3,6 @@ import os
import numpy import numpy
from setuptools import Extension, setup from setuptools import Extension, setup
NUMPY_INCLUDE = numpy.get_include() NUMPY_INCLUDE = numpy.get_include()

View File

@@ -0,0 +1,56 @@
import pandas as pd
import pytest
from qlib.contrib.strategy.cost_control import SoftTopkStrategy
class MockPosition:
def __init__(self, weights):
self.weights = weights
def get_stock_weight_dict(self, only_stock=True):
return self.weights
def test_soft_topk_logic():
# Initial: A=0.8, B=0.2 (Total=1.0). Target Risk=0.95.
# Scores: A and B are low, C and D are topk.
scores = pd.Series({"C": 0.9, "D": 0.8, "A": 0.1, "B": 0.1})
current_pos = MockPosition({"A": 0.8, "B": 0.2})
topk = 2
risk_degree = 0.95
impact_limit = 0.1 # Max change per step
def create_test_strategy(impact_limit_value):
strat = SoftTopkStrategy.__new__(SoftTopkStrategy)
strat.topk = topk
strat.risk_degree = risk_degree
strat.trade_impact_limit = impact_limit_value
return strat
# 1. With impact limit: Expect deterministic sell and limited buy
strat_i = create_test_strategy(impact_limit)
res_i = strat_i.generate_target_weight_position(scores, current_pos, None, None)
# A should be exactly 0.8 - 0.1 = 0.7
assert abs(res_i["A"] - 0.7) < 1e-8
# B should be exactly 0.2 - 0.1 = 0.1
assert abs(res_i["B"] - 0.1) < 1e-8
# Total sells = 0.2 released. New budget = 0.2 + (0.95 - 1.0) = 0.15.
# C and D share 0.15 -> 0.075 each.
assert abs(res_i["C"] - 0.075) < 1e-8
assert abs(res_i["D"] - 0.075) < 1e-8
# 2. Without impact limit: Expect full liquidation and full target fill
strat_c = create_test_strategy(1.0)
res_c = strat_c.generate_target_weight_position(scores, current_pos, None, None)
# A, B not in topk -> Liquidated
assert "A" not in res_c and "B" not in res_c
# C, D should reach ideal_per_stock (0.95/2 = 0.475)
assert abs(res_c["C"] - 0.475) < 1e-8
assert abs(res_c["D"] - 0.475) < 1e-8
if __name__ == "__main__":
pytest.main([__file__])

View File

@@ -0,0 +1,38 @@
import pandas as pd
import pytest
from qlib.contrib.strategy.cost_control import SoftTopkStrategy
class MockPosition:
def __init__(self, weights):
self.weights = weights
def get_stock_weight_dict(self, only_stock=True):
return self.weights
def create_test_strategy(topk, risk_degree, impact_limit):
strat = SoftTopkStrategy.__new__(SoftTopkStrategy)
strat.topk = topk
strat.risk_degree = risk_degree
strat.trade_impact_limit = impact_limit
return strat
@pytest.mark.parametrize(
("impact_limit", "expected_fill"),
[
(0.1, 0.1),
(1.0, 0.475),
],
)
def test_soft_topk_cold_start_impact_limit(impact_limit, expected_fill):
scores = pd.Series({"C": 0.9, "D": 0.8, "A": 0.1, "B": 0.1})
current_pos = MockPosition({})
strat = create_test_strategy(topk=2, risk_degree=0.95, impact_limit=impact_limit)
res = strat.generate_target_weight_position(scores, current_pos, None, None)
assert abs(res["C"] - expected_fill) < 1e-8
assert abs(res["D"] - expected_fill) < 1e-8

View File

@@ -17,7 +17,6 @@ from qlib.rl.utils.finite_env import (
generate_nan_observation, generate_nan_observation,
) )
_test_space = gym.spaces.Dict( _test_space = gym.spaces.Dict(
{ {
"sensors": gym.spaces.Dict( "sensors": gym.spaces.Dict(

View File

@@ -13,7 +13,6 @@ from qlib.workflow import R
from qlib.tests import TestAutoData from qlib.tests import TestAutoData
from qlib.tests.config import GBDT_MODEL, get_dataset_config, CSI300_MARKET from qlib.tests.config import GBDT_MODEL, get_dataset_config, CSI300_MARKET
CSI300_GBDT_TASK = { CSI300_GBDT_TASK = {
"model": GBDT_MODEL, "model": GBDT_MODEL,
"dataset": get_dataset_config( "dataset": get_dataset_config(

View File

@@ -16,7 +16,6 @@ sys.path.append(str(Path(__file__).resolve().parent.parent.joinpath("scripts")))
from get_data import GetData from get_data import GetData
from dump_bin import DumpDataAll, DumpDataFix from dump_bin import DumpDataAll, DumpDataFix
DATA_DIR = Path(__file__).parent.joinpath("test_dump_data") DATA_DIR = Path(__file__).parent.joinpath("test_dump_data")
SOURCE_DIR = DATA_DIR.joinpath("source") SOURCE_DIR = DATA_DIR.joinpath("source")
SOURCE_DIR.mkdir(exist_ok=True, parents=True) SOURCE_DIR.mkdir(exist_ok=True, parents=True)

View File

@@ -19,7 +19,6 @@ from dump_pit import DumpPitData
sys.path.append(str(Path(__file__).resolve().parent.parent.joinpath("scripts/data_collector/pit"))) sys.path.append(str(Path(__file__).resolve().parent.parent.joinpath("scripts/data_collector/pit")))
from collector import Run from collector import Run
pd.set_option("display.width", 1000) pd.set_option("display.width", 1000)
pd.set_option("display.max_columns", None) pd.set_option("display.max_columns", None)