qka 1.2.3.dev1__tar.gz → 1.2.4.dev3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/PKG-INFO +1 -1
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/core/data.py +80 -19
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/.github/workflows/docs.yml +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/.github/workflows/release.yml +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/.gitignore +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/.vscode/settings.json +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/CHANGELOG.md +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/LICENSE +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/README.md +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/docs/api/brokers.md +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/docs/api/core.md +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/docs/api/utils.md +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/docs/getting-started/concepts.md +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/docs/getting-started/first-strategy.md +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/docs/getting-started/installation.md +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/docs/index.md +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/docs/user-guide/backtest.md +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/docs/user-guide/data.md +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/docs/user-guide/trading.md +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/mkdocs.yml +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/pyproject.toml +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/__init__.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/brokers/__init__.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/brokers/client.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/brokers/server.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/brokers/trade.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/cli.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/core/__init__.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/core/backtest.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/core/broker.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/core/report.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/core/strategy.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/mcp/__init__.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/mcp/api.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/mcp/server.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/server/__init__.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/server/handlers/__init__.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/server/handlers/class_inspector_handler.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/server/handlers/code_executor_handler.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/server/ws_client.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/server/zmq_server.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/utils/__init__.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/utils/anis.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/utils/logger.py +0 -0
- {qka-1.2.3.dev1 → qka-1.2.4.dev3}/qka/utils/util.py +0 -0
|
@@ -11,6 +11,7 @@ import pandas as pd
|
|
|
11
11
|
import pyarrow as pa
|
|
12
12
|
import pyarrow.parquet as pq
|
|
13
13
|
import akshare as ak
|
|
14
|
+
import baostock as bs
|
|
14
15
|
import dask.dataframe as dd
|
|
15
16
|
from typing import List, Dict, Optional, Callable
|
|
16
17
|
from qka.utils.logger import logger
|
|
@@ -26,7 +27,7 @@ class Data():
|
|
|
26
27
|
period (str): 数据周期,如 '1d'、'1m' 等
|
|
27
28
|
adjust (str): 复权方式,如 'qfq'、'hfq'、'bfq'
|
|
28
29
|
factor (Callable): 因子计算函数
|
|
29
|
-
source (str): 数据源,如 'akshare'、'qmt'
|
|
30
|
+
source (str): 数据源,如 'baostock'(默认)、'akshare'、'qmt'
|
|
30
31
|
pool_size (int): 并发下载线程数
|
|
31
32
|
datadir (Path): 数据缓存目录
|
|
32
33
|
target_dir (Path): 目标存储目录
|
|
@@ -38,7 +39,7 @@ class Data():
|
|
|
38
39
|
period: str = '1d',
|
|
39
40
|
adjust: str = 'qfq',
|
|
40
41
|
factor: Callable[[pd.DataFrame], pd.DataFrame] = lambda df: df,
|
|
41
|
-
source: str = '
|
|
42
|
+
source: str = 'baostock',
|
|
42
43
|
pool_size: int = 10,
|
|
43
44
|
datadir: Optional[Path] = None
|
|
44
45
|
):
|
|
@@ -50,7 +51,7 @@ class Data():
|
|
|
50
51
|
period: 数据周期,如 '1d'(日线)、'1m'(分钟)
|
|
51
52
|
adjust: 复权方式,'qfq'(前复权)、'hfq'(后复权)、'bfq'(不复权)
|
|
52
53
|
factor: 因子计算函数,接收 DataFrame 返回 DataFrame,用于扩展自定义因子
|
|
53
|
-
source: 数据来源,'akshare'
|
|
54
|
+
source: 数据来源,'baostock'(默认)、'akshare'、'qmt'
|
|
54
55
|
pool_size: 并发下载线程数
|
|
55
56
|
datadir: 缓存根目录,默认为当前工作目录下的 datadir/
|
|
56
57
|
"""
|
|
@@ -90,6 +91,8 @@ class Data():
|
|
|
90
91
|
|
|
91
92
|
if self.source == 'akshare':
|
|
92
93
|
df = self._get_from_akshare(symbol)
|
|
94
|
+
elif self.source == 'baostock':
|
|
95
|
+
df = self._get_from_baostock(symbol)
|
|
93
96
|
else:
|
|
94
97
|
df = pd.DataFrame()
|
|
95
98
|
|
|
@@ -120,28 +123,46 @@ class Data():
|
|
|
120
123
|
|
|
121
124
|
# 准备缓存目录
|
|
122
125
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
126
|
+
# baostock 需要先登录,且其 C/S 架构不支持多线程并发
|
|
127
|
+
bs_logged_in = False
|
|
128
|
+
if self.source == 'baostock':
|
|
129
|
+
lg = bs.login()
|
|
130
|
+
if lg.error_code != '0':
|
|
131
|
+
raise RuntimeError(f"baostock 登录失败: {lg.error_msg}")
|
|
132
|
+
bs_logged_in = True
|
|
133
|
+
|
|
134
|
+
errors = []
|
|
135
|
+
try:
|
|
136
|
+
if self.source == 'baostock':
|
|
137
|
+
# baostock 串行下载(C/S 架构不支持并发)
|
|
138
|
+
for symbol in tqdm(self.symbols, desc="下载数据"):
|
|
135
139
|
try:
|
|
136
|
-
|
|
140
|
+
self._download(symbol)
|
|
137
141
|
except Exception as e:
|
|
138
142
|
errors.append(f"{symbol}: {e}")
|
|
139
143
|
logger.warning(f"下载 {symbol} 失败: {e}")
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
144
|
+
else:
|
|
145
|
+
# 其他数据源(akshare 等)并发下载
|
|
146
|
+
with ThreadPoolExecutor(max_workers=self.pool_size) as executor:
|
|
147
|
+
futures = {
|
|
148
|
+
executor.submit(self._download, symbol): symbol
|
|
149
|
+
for symbol in self.symbols
|
|
150
|
+
}
|
|
151
|
+
with tqdm(total=len(self.symbols), desc="下载数据") as pbar:
|
|
152
|
+
for future in as_completed(futures):
|
|
153
|
+
symbol = futures[future]
|
|
154
|
+
try:
|
|
155
|
+
future.result()
|
|
156
|
+
except Exception as e:
|
|
157
|
+
errors.append(f"{symbol}: {e}")
|
|
158
|
+
logger.warning(f"下载 {symbol} 失败: {e}")
|
|
159
|
+
pbar.update(1)
|
|
160
|
+
pbar.set_postfix_str(f"当前: {symbol}")
|
|
143
161
|
if errors:
|
|
144
162
|
logger.warning(f"共 {len(errors)} 只股票下载失败: {errors[:3]}...")
|
|
163
|
+
finally:
|
|
164
|
+
if bs_logged_in:
|
|
165
|
+
bs.logout()
|
|
145
166
|
|
|
146
167
|
dfs = []
|
|
147
168
|
for symbol in self.symbols:
|
|
@@ -209,3 +230,43 @@ class Data():
|
|
|
209
230
|
df = df.set_index('date')
|
|
210
231
|
# 设置索引
|
|
211
232
|
return df
|
|
233
|
+
|
|
234
|
+
def _get_from_baostock(self, symbol: str) -> pd.DataFrame:
|
|
235
|
+
"""
|
|
236
|
+
从 baostock 获取单个股票的数据。
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
symbol (str): 股票代码,支持带后缀如 000001.SZ 或 600000.SH
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
pd.DataFrame: 股票数据,以 date 为索引,包含 open, high, low, close, volume, amount 列
|
|
243
|
+
"""
|
|
244
|
+
# baostock 代码格式:sz.000001 / sh.600000
|
|
245
|
+
bs_code = symbol.replace('.SZ', '.sz').replace('.SH', '.sh').replace('.BJ', '.bj')
|
|
246
|
+
|
|
247
|
+
# adjustflag: 1=不复权, 2=前复权, 3=后复权
|
|
248
|
+
adjust_map = {'bfq': '1', 'qfq': '2', 'hfq': '3'}
|
|
249
|
+
adjustflag = adjust_map.get(self.adjust, '2')
|
|
250
|
+
|
|
251
|
+
rs = bs.query_history_k_data_plus(
|
|
252
|
+
bs_code,
|
|
253
|
+
"date,open,high,low,close,volume,amount",
|
|
254
|
+
start_date='1990-01-01',
|
|
255
|
+
end_date='2050-12-31',
|
|
256
|
+
frequency='d',
|
|
257
|
+
adjustflag=adjustflag,
|
|
258
|
+
)
|
|
259
|
+
df = rs.get_data()
|
|
260
|
+
|
|
261
|
+
if len(df) == 0:
|
|
262
|
+
return df
|
|
263
|
+
|
|
264
|
+
# baostock 返回的数值列是字符串,转数值类型
|
|
265
|
+
numeric_cols = ["open", "high", "low", "close", "volume", "amount"]
|
|
266
|
+
for col in numeric_cols:
|
|
267
|
+
if col in df.columns:
|
|
268
|
+
df[col] = pd.to_numeric(df[col], errors="coerce")
|
|
269
|
+
|
|
270
|
+
df["date"] = pd.to_datetime(df["date"])
|
|
271
|
+
df = df.set_index("date")
|
|
272
|
+
return df
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|