quant1x 0.7.2__tar.gz → 0.7.5__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.
- {quant1x-0.7.2 → quant1x-0.7.5}/PKG-INFO +1 -1
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/cache/__init__.py +2 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/cache/f10.py +13 -1
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/cache/market.py +3 -3
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/cache/sector.py +2 -2
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/cache/security.py +1 -1
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/config/__init__.py +10 -1
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/config/config.py +80 -0
- quant1x-0.7.5/quant1x/datasets/__init__.py +22 -0
- quant1x-0.7.5/quant1x/datasets/kline.py +286 -0
- quant1x-0.7.5/quant1x/datasets/trans.py +462 -0
- quant1x-0.7.5/quant1x/datasets/xdxr.py +94 -0
- quant1x-0.7.5/quant1x/exchange/__init__.py +62 -0
- quant1x-0.7.5/quant1x/exchange/calendar.py +336 -0
- quant1x-0.7.5/quant1x/exchange/code.py +363 -0
- quant1x-0.7.5/quant1x/exchange/margin_trading.py +177 -0
- quant1x-0.7.5/quant1x/exchange/markets.py +102 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/exchange/security.py +1 -1
- quant1x-0.7.5/quant1x/exchange/session.py +348 -0
- quant1x-0.7.5/quant1x/exchange/timestamp.py +216 -0
- quant1x-0.7.5/quant1x/factors/__init__.py +21 -0
- quant1x-0.7.5/quant1x/factors/base.py +169 -0
- quant1x-0.7.5/quant1x/factors/f10.py +301 -0
- quant1x-0.7.5/quant1x/factors/financial_report.py +340 -0
- quant1x-0.7.5/quant1x/factors/notice.py +455 -0
- quant1x-0.7.5/quant1x/factors/safety_score.py +129 -0
- quant1x-0.7.5/quant1x/factors/share_holder.py +141 -0
- quant1x-0.7.5/quant1x/level1/block_info.py +68 -0
- quant1x-0.7.5/quant1x/level1/block_meta.py +69 -0
- quant1x-0.7.5/quant1x/level1/client.py +185 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/level1/config.py +7 -16
- quant1x-0.7.5/quant1x/level1/finance_info.py +164 -0
- quant1x-0.7.5/quant1x/level1/heartbeat.py +47 -0
- quant1x-0.7.5/quant1x/level1/hello1.py +45 -0
- quant1x-0.7.5/quant1x/level1/hello2.py +45 -0
- quant1x-0.7.5/quant1x/level1/helpers.py +147 -0
- quant1x-0.7.5/quant1x/level1/minute_time.py +91 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/level1/protocol.py +6 -10
- quant1x-0.7.5/quant1x/level1/security_bars.py +172 -0
- quant1x-0.7.5/quant1x/level1/security_count.py +45 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/level1/security_list.py +29 -14
- quant1x-0.7.5/quant1x/level1/security_quote.py +391 -0
- quant1x-0.7.5/quant1x/level1/transaction.py +220 -0
- quant1x-0.7.5/quant1x/level1/xdxr_info.py +227 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/numerics.py +22 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/std/__init__.py +3 -0
- quant1x-0.7.5/quant1x/std/time.py +40 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x.egg-info/PKG-INFO +1 -1
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x.egg-info/SOURCES.txt +39 -2
- {quant1x-0.7.2 → quant1x-0.7.5}/tests/test_cache.py +2 -2
- {quant1x-0.7.2 → quant1x-0.7.5}/tests/test_handshake.py +7 -3
- quant1x-0.7.5/tests/test_kline.py +194 -0
- quant1x-0.7.5/tests/test_margin_trading.py +22 -0
- quant1x-0.7.5/tests/test_notice.py +53 -0
- quant1x-0.7.5/tests/test_protocol_kline.py +220 -0
- quant1x-0.7.5/tests/test_real_kline.py +53 -0
- quant1x-0.7.5/tests/test_safety_score.py +29 -0
- quant1x-0.7.5/tests/test_share_holder.py +61 -0
- quant1x-0.7.5/tests/test_trans.py +137 -0
- quant1x-0.7.5/tests/test_xdxr.py +68 -0
- quant1x-0.7.2/quant1x/exchange/__init__.py +0 -22
- quant1x-0.7.2/quant1x/exchange/code.py +0 -76
- quant1x-0.7.2/quant1x/exchange/markets.py +0 -340
- quant1x-0.7.2/quant1x/level1/client.py +0 -200
- quant1x-0.7.2/quant1x/timestamp.py +0 -170
- {quant1x-0.7.2 → quant1x-0.7.5}/LICENSE +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/README.md +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/__init__.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/device.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/devp.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/encoding/__init__.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/encoding/yaml.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/formula/__init__.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/formula/formula.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/formula/formula_plus.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/indicators/__init__.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/indicators/cdtd.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/indicators/f89k.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/indicators/hjk.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/indicators/ma1x.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/indicators/p2fb.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/level1/__init__.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/log/__init__.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/log/logger.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/net/__init__.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/net/connection_pool.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/net/endpoint.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/net/operation_handler.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/network.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/std/pattern.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/system.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/util/__init__.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/util/chart.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/util/font.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x/version.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x.egg-info/dependency_links.txt +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x.egg-info/not-zip-safe +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x.egg-info/requires.txt +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/quant1x.egg-info/top_level.txt +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/setup.cfg +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/setup.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/tests/test_connection_pool.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/tests/test_protocol_fixtures.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.5}/tests/test_protocol_real_network.py +0 -0
|
@@ -5,3 +5,5 @@ from .security import securities, stock_name
|
|
|
5
5
|
from .market import klines, get_period_name, convert_klines_trading, get_minutes_data, get_tick_transaction, date_format
|
|
6
6
|
from .sector import block_list, sector_filename, get_sector_list, get_sector_constituents
|
|
7
7
|
from .f10 import cache_f10, get_f10
|
|
8
|
+
|
|
9
|
+
trains_begin_date = "2024-10-01"
|
|
@@ -8,12 +8,24 @@ from .. import exchange, config
|
|
|
8
8
|
|
|
9
9
|
@lru_cache(maxsize=None)
|
|
10
10
|
def cache_f10(date: str = None) -> DataFrame:
|
|
11
|
+
"""
|
|
12
|
+
从闪存数据中读取F10因子数据
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
date (str, optional): 交易日期,格式为'YYYYMMDD'。如果未提供,则使用交易所最后一个交易日
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
DataFrame: 包含F10因子数据的Pandas DataFrame
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
FileNotFoundError: 如果指定日期的数据文件不存在
|
|
22
|
+
"""
|
|
11
23
|
factor_name = 'f10'
|
|
12
24
|
trade_date = date or exchange.last_trade_date()
|
|
13
25
|
file_extension = exchange.fix_trade_date(trade_date)
|
|
14
26
|
filename = f"{factor_name}.{file_extension}"
|
|
15
27
|
year = trade_date[:4]
|
|
16
|
-
base_path = os.path.join(config.
|
|
28
|
+
base_path = os.path.join(config.data_path, 'flash')
|
|
17
29
|
return pd.read_csv(os.path.join(base_path, year, filename))
|
|
18
30
|
|
|
19
31
|
def get_f10(code: str, date: str = None) -> Dict[str, Any]:
|
|
@@ -14,7 +14,7 @@ def klines(code: str, freq:str = 'D') -> pd.DataFrame | None:
|
|
|
14
14
|
corrected_symbol = exchange.correct_security_code(code)
|
|
15
15
|
suffix_length = 3 # 修正拼写并明确表示后缀长度
|
|
16
16
|
freq_path = 'day' if freq == 'D' else freq
|
|
17
|
-
symbol_directory = os.path.join(config.
|
|
17
|
+
symbol_directory = os.path.join(config.data_path, freq_path, corrected_symbol[:-suffix_length]) # 更清晰表达目录用途
|
|
18
18
|
file_extension = '.csv'
|
|
19
19
|
filename = f"{corrected_symbol}{file_extension}" # 使用f-string格式化
|
|
20
20
|
full_path = os.path.join(symbol_directory, filename)
|
|
@@ -106,7 +106,7 @@ def get_minutes_data(code: str, date: str) -> DataFrame | None:
|
|
|
106
106
|
# 获取年份
|
|
107
107
|
cache_date = date_format(cache_date, layout='%Y%m%d')
|
|
108
108
|
year = cache_date[:4]
|
|
109
|
-
base_path = os.path.join(config.
|
|
109
|
+
base_path = os.path.join(config.data_path, 'minutes')
|
|
110
110
|
full_path = os.path.join(base_path, year, cache_date, filename)
|
|
111
111
|
|
|
112
112
|
if os.path.isfile(full_path):
|
|
@@ -126,7 +126,7 @@ def get_tick_transaction(code: str, date: str) -> DataFrame | None:
|
|
|
126
126
|
# 获取年份
|
|
127
127
|
cache_date = date_format(cache_date, layout='%Y%m%d')
|
|
128
128
|
year = cache_date[:4]
|
|
129
|
-
base_path = os.path.join(config.
|
|
129
|
+
base_path = os.path.join(config.data_path, 'trans')
|
|
130
130
|
full_path = os.path.join(base_path, year, cache_date, filename)
|
|
131
131
|
|
|
132
132
|
if os.path.isfile(full_path):
|
|
@@ -15,10 +15,10 @@ def sector_filename(date: str = '') -> str:
|
|
|
15
15
|
cache_date = date.strip()
|
|
16
16
|
if len(cache_date) == 0:
|
|
17
17
|
cache_date = exchange.last_trade_date()
|
|
18
|
-
filename = os.path.join(config.
|
|
18
|
+
filename = os.path.join(config.meta_path, f'{name}.{cache_date}')
|
|
19
19
|
if not os.path.isfile(filename):
|
|
20
20
|
# fallback to blocks.csv
|
|
21
|
-
filename = os.path.join(config.
|
|
21
|
+
filename = os.path.join(config.meta_path, 'blocks.csv')
|
|
22
22
|
return filename
|
|
23
23
|
|
|
24
24
|
@lru_cache(maxsize=None)
|
|
@@ -9,7 +9,7 @@ def securities() -> pd.DataFrame:
|
|
|
9
9
|
"""
|
|
10
10
|
证券列表
|
|
11
11
|
"""
|
|
12
|
-
full_path = os.path.join(config.
|
|
12
|
+
full_path = os.path.join(config.meta_path, 'securities.csv')
|
|
13
13
|
if not os.path.isfile(full_path):
|
|
14
14
|
return pd.DataFrame(columns=['code', 'name'])
|
|
15
15
|
# with open(full_path, 'rb') as f:
|
|
@@ -10,10 +10,19 @@
|
|
|
10
10
|
"""
|
|
11
11
|
from .config import (
|
|
12
12
|
get_quant1x_config_filename,
|
|
13
|
+
get_historical_trade_filename,
|
|
13
14
|
quant1x_config
|
|
14
15
|
)
|
|
15
16
|
|
|
17
|
+
# 导出常用配置路径
|
|
18
|
+
data_path = quant1x_config.data_path
|
|
19
|
+
meta_path = quant1x_config.meta_path
|
|
20
|
+
kline_path = quant1x_config.kline_path
|
|
21
|
+
|
|
16
22
|
__all__ = [
|
|
17
|
-
"quant1x_config",
|
|
18
23
|
"get_quant1x_config_filename",
|
|
24
|
+
"get_historical_trade_filename",
|
|
25
|
+
"data_path",
|
|
26
|
+
"meta_path",
|
|
27
|
+
"kline_path",
|
|
19
28
|
]
|
|
@@ -117,3 +117,83 @@ class Quant1XConfig:
|
|
|
117
117
|
|
|
118
118
|
# 创建配置单例
|
|
119
119
|
quant1x_config = Quant1XConfig()
|
|
120
|
+
|
|
121
|
+
def get_historical_trade_filename(code: str, date: str) -> str:
|
|
122
|
+
"""
|
|
123
|
+
获取历史成交记录文件路径
|
|
124
|
+
目录结构: ${trans}/${YYYY}/${YYYYMMDD}/${SecurityCode}.csv
|
|
125
|
+
"""
|
|
126
|
+
date_str = date.replace('-', '').replace('/', '')
|
|
127
|
+
year = date_str[:4]
|
|
128
|
+
base_path = os.path.join(quant1x_config.data_path, 'trans')
|
|
129
|
+
return os.path.join(base_path, year, date_str, f"{code}.csv")
|
|
130
|
+
|
|
131
|
+
def get_xdxr_path() -> str:
|
|
132
|
+
"""获取除权除息文件路径"""
|
|
133
|
+
return os.path.join(quant1x_config.data_path, 'xdxr')
|
|
134
|
+
|
|
135
|
+
def get_xdxr_filename(code: str) -> str:
|
|
136
|
+
"""
|
|
137
|
+
获取除权除息文件路径
|
|
138
|
+
目录结构: ${xdxr}/${subpath}/${SecurityCode}.csv
|
|
139
|
+
subpath: code[:-3]
|
|
140
|
+
"""
|
|
141
|
+
if len(code) <= 3:
|
|
142
|
+
sub = ""
|
|
143
|
+
else:
|
|
144
|
+
sub = code[:-3]
|
|
145
|
+
|
|
146
|
+
base_path = get_xdxr_path()
|
|
147
|
+
return os.path.join(base_path, sub, f"{code}.csv")
|
|
148
|
+
|
|
149
|
+
def get_day_path() -> str:
|
|
150
|
+
"""获取日K线文件路径"""
|
|
151
|
+
return quant1x_config.kline_path
|
|
152
|
+
|
|
153
|
+
def get_kline_filename(code: str, forward: bool = True) -> str:
|
|
154
|
+
"""
|
|
155
|
+
获取日K线文件路径
|
|
156
|
+
目录结构: ${day}/${subpath}/${SecurityCode}.${ext}
|
|
157
|
+
subpath: code[:-3]
|
|
158
|
+
ext: csv if forward else raw
|
|
159
|
+
"""
|
|
160
|
+
if len(code) <= 3:
|
|
161
|
+
sub = ""
|
|
162
|
+
else:
|
|
163
|
+
sub = code[:-3]
|
|
164
|
+
|
|
165
|
+
base_path = get_day_path()
|
|
166
|
+
ext = "csv" if forward else "raw"
|
|
167
|
+
return os.path.join(base_path, sub, f"{code}.{ext}")
|
|
168
|
+
|
|
169
|
+
from quant1x import std
|
|
170
|
+
|
|
171
|
+
def get_holding_path() -> str:
|
|
172
|
+
return os.path.join(quant1x_config.data_path, "holding")
|
|
173
|
+
|
|
174
|
+
def cache_id_path(code: str) -> str:
|
|
175
|
+
"""
|
|
176
|
+
Generate cache ID path.
|
|
177
|
+
SH600000 -> SH600/SH600000
|
|
178
|
+
"""
|
|
179
|
+
if len(code) <= 3:
|
|
180
|
+
return code
|
|
181
|
+
|
|
182
|
+
prefix = code[:-3]
|
|
183
|
+
return os.path.join(prefix, code)
|
|
184
|
+
|
|
185
|
+
def top10_holders_filename(code: str, date_str: str) -> str:
|
|
186
|
+
quarter, _, _ = std.get_quarter_by_date(date_str)
|
|
187
|
+
id_path = cache_id_path(code)
|
|
188
|
+
return os.path.join(get_holding_path(), quarter, f"{id_path}.csv")
|
|
189
|
+
|
|
190
|
+
def quarterly_cache_path(date: str) -> str:
|
|
191
|
+
quarter, _, _ = std.get_quarter_by_date(date)
|
|
192
|
+
return os.path.join(quant1x_config.data_path, "infoq", quarter)
|
|
193
|
+
|
|
194
|
+
def quarterly_filename(date: str, keyword: str) -> str:
|
|
195
|
+
return os.path.join(quarterly_cache_path(date), f"{keyword}.csv")
|
|
196
|
+
|
|
197
|
+
def reports_filename(date: str) -> str:
|
|
198
|
+
return quarterly_filename(date, "reports")
|
|
199
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
from .trans import (
|
|
3
|
+
TickTransaction,
|
|
4
|
+
TurnoverDataSummary,
|
|
5
|
+
checkout_transaction_data,
|
|
6
|
+
count_inflow,
|
|
7
|
+
DataTrans
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from .xdxr import (
|
|
11
|
+
load_xdxr,
|
|
12
|
+
save_xdxr,
|
|
13
|
+
DataXdxr
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from .kline import (
|
|
17
|
+
KLine,
|
|
18
|
+
load_kline,
|
|
19
|
+
save_kline,
|
|
20
|
+
fetch_kline,
|
|
21
|
+
DataKLine
|
|
22
|
+
)
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# -*- coding: UTF-8 -*-
|
|
2
|
+
import os
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import List, Optional, Any
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
from quant1x.exchange import Timestamp
|
|
9
|
+
from quant1x.level1 import protocol, client
|
|
10
|
+
from quant1x.level1.security_bars import SecurityBarsRequest, SecurityBarsResponse, KLineType, SecurityBar, SECURITY_BARS_MAX
|
|
11
|
+
from quant1x.factors import base as factors
|
|
12
|
+
from quant1x.level1.xdxr_info import XdxrInfo
|
|
13
|
+
from quant1x.datasets import xdxr
|
|
14
|
+
from quant1x.config import config
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
MAX_KLINE_LOOKBACK_DAYS = 1
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class KLine:
|
|
22
|
+
Date: str = ""
|
|
23
|
+
Open: float = 0.0
|
|
24
|
+
Close: float = 0.0
|
|
25
|
+
High: float = 0.0
|
|
26
|
+
Low: float = 0.0
|
|
27
|
+
Volume: float = 0.0
|
|
28
|
+
Amount: float = 0.0
|
|
29
|
+
Up: int = 0
|
|
30
|
+
Down: int = 0
|
|
31
|
+
Datetime: str = ""
|
|
32
|
+
AdjustmentCount: int = 0
|
|
33
|
+
|
|
34
|
+
def adjust(self, adj: factors.CumulativeAdjustment):
|
|
35
|
+
self.Open = self.Open * adj.m + adj.a
|
|
36
|
+
self.Close = self.Close * adj.m + adj.a
|
|
37
|
+
self.High = self.High * adj.m + adj.a
|
|
38
|
+
self.Low = self.Low * adj.m + adj.a
|
|
39
|
+
|
|
40
|
+
# 成交量复权
|
|
41
|
+
if self.Volume != 0:
|
|
42
|
+
# 1. 计算均价
|
|
43
|
+
ap = self.Amount / self.Volume
|
|
44
|
+
# 2. 均价复权
|
|
45
|
+
ap_adjusted = ap * adj.m + adj.a
|
|
46
|
+
# 3. 成交量复权
|
|
47
|
+
self.Volume *= (1 + adj.share_adjustment_ratio)
|
|
48
|
+
# 4. 以新成交量*均价计算成交额
|
|
49
|
+
self.Amount = self.Volume * ap_adjusted
|
|
50
|
+
|
|
51
|
+
# 5. 更新除权除息次数
|
|
52
|
+
self.AdjustmentCount = adj.no
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def headers() -> List[str]:
|
|
56
|
+
return ["Date", "Open", "Close", "High", "Low", "Volume", "Amount", "Up", "Down", "Datetime", "AdjustmentCount"]
|
|
57
|
+
|
|
58
|
+
def save_kline(filename: str, values: List[KLine]):
|
|
59
|
+
if not values:
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
dirname = os.path.dirname(filename)
|
|
63
|
+
if dirname:
|
|
64
|
+
os.makedirs(dirname, exist_ok=True)
|
|
65
|
+
|
|
66
|
+
data = [
|
|
67
|
+
{
|
|
68
|
+
"Date": v.Date,
|
|
69
|
+
"Open": v.Open,
|
|
70
|
+
"Close": v.Close,
|
|
71
|
+
"High": v.High,
|
|
72
|
+
"Low": v.Low,
|
|
73
|
+
"Volume": v.Volume,
|
|
74
|
+
"Amount": v.Amount,
|
|
75
|
+
"Up": v.Up,
|
|
76
|
+
"Down": v.Down,
|
|
77
|
+
"Datetime": v.Datetime,
|
|
78
|
+
"AdjustmentCount": v.AdjustmentCount
|
|
79
|
+
}
|
|
80
|
+
for v in values
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
df = pd.DataFrame(data, columns=KLine.headers())
|
|
84
|
+
df.to_csv(filename, index=False)
|
|
85
|
+
|
|
86
|
+
def read_kline_from_csv(filename: str) -> List[KLine]:
|
|
87
|
+
klines = []
|
|
88
|
+
if not os.path.exists(filename):
|
|
89
|
+
return klines
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
df = pd.read_csv(filename)
|
|
93
|
+
# Ensure columns exist
|
|
94
|
+
required_cols = KLine.headers()
|
|
95
|
+
if not all(col in df.columns for col in required_cols):
|
|
96
|
+
return klines
|
|
97
|
+
|
|
98
|
+
for _, row in df.iterrows():
|
|
99
|
+
kline = KLine(
|
|
100
|
+
Date=str(row['Date']),
|
|
101
|
+
Open=float(row['Open']),
|
|
102
|
+
Close=float(row['Close']),
|
|
103
|
+
High=float(row['High']),
|
|
104
|
+
Low=float(row['Low']),
|
|
105
|
+
Volume=float(row['Volume']),
|
|
106
|
+
Amount=float(row['Amount']),
|
|
107
|
+
Up=int(row['Up']),
|
|
108
|
+
Down=int(row['Down']),
|
|
109
|
+
Datetime=str(row['Datetime']),
|
|
110
|
+
AdjustmentCount=int(row['AdjustmentCount'])
|
|
111
|
+
)
|
|
112
|
+
klines.append(kline)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.error(f"Failed to read kline csv {filename}: {e}")
|
|
115
|
+
|
|
116
|
+
return klines
|
|
117
|
+
|
|
118
|
+
def load_kline(code: str) -> List[KLine]:
|
|
119
|
+
filename = config.get_kline_filename(code)
|
|
120
|
+
logger.debug(f"[dataset::KLine] kline file: {filename}")
|
|
121
|
+
return read_kline_from_csv(filename)
|
|
122
|
+
|
|
123
|
+
def fetch_kline(code: str, start: int, count: int, kline_type: KLineType = KLineType.DAILY) -> List[SecurityBar]:
|
|
124
|
+
try:
|
|
125
|
+
with client.client() as conn:
|
|
126
|
+
req = SecurityBarsRequest(code, kline_type.value, start, count)
|
|
127
|
+
# Determine is_index from request logic or pass it explicitly if needed
|
|
128
|
+
# SecurityBarsRequest constructor determines is_index
|
|
129
|
+
|
|
130
|
+
resp = SecurityBarsResponse(req.is_index, kline_type.value)
|
|
131
|
+
protocol.process(conn.socket, req, resp)
|
|
132
|
+
return resp.list
|
|
133
|
+
except Exception as e:
|
|
134
|
+
logger.error(f"[dataset::KLine] fetch_kline error: {e}")
|
|
135
|
+
return []
|
|
136
|
+
|
|
137
|
+
def apply_forward_adjustment_for_event(klines: List[KLine],
|
|
138
|
+
current_start_date: Timestamp,
|
|
139
|
+
dividends: List[XdxrInfo]):
|
|
140
|
+
if not klines:
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
# 最后一根K线的日期
|
|
144
|
+
last_day = klines[-1].Date
|
|
145
|
+
# 转成时间戳且对齐时间
|
|
146
|
+
ts_last_day = Timestamp.parse(last_day).get_pre_market_time()
|
|
147
|
+
# 计算最后一根K线的下一个交易日的日期
|
|
148
|
+
# TODO: Implement next_trading_day properly. For now, use +1 day approximation or just rely on date comparison
|
|
149
|
+
last_day_next = ts_last_day.offset(hour=24).only_date() # Approximation
|
|
150
|
+
start_date_str = current_start_date.only_date()
|
|
151
|
+
|
|
152
|
+
# Filter dividends
|
|
153
|
+
xdxr_infos = [
|
|
154
|
+
x for x in dividends
|
|
155
|
+
if x.Date <= last_day_next and x.Category == 1
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
# Sort by date? C++ uses std::views::filter, order depends on input.
|
|
159
|
+
# Assuming dividends are sorted by date.
|
|
160
|
+
|
|
161
|
+
times = len(xdxr_infos)
|
|
162
|
+
|
|
163
|
+
for info in xdxr_infos:
|
|
164
|
+
if info.Date <= start_date_str:
|
|
165
|
+
# IPO check logic in C++ is commented out or specific?
|
|
166
|
+
# "除权除息数据在日线第一条数据之前... continue" is commented out in C++.
|
|
167
|
+
pass
|
|
168
|
+
else:
|
|
169
|
+
m, a = info.adjust_factor()
|
|
170
|
+
share_ratio = info.compute_share_adjustment_ratio()
|
|
171
|
+
|
|
172
|
+
for kline in klines:
|
|
173
|
+
if kline.Date >= info.Date:
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
if kline.Date < info.Date:
|
|
177
|
+
kline.Open = kline.Open * m + a
|
|
178
|
+
kline.Close = kline.Close * m + a
|
|
179
|
+
kline.High = kline.High * m + a
|
|
180
|
+
kline.Low = kline.Low * m + a
|
|
181
|
+
|
|
182
|
+
if kline.Volume != 0:
|
|
183
|
+
ap = kline.Amount / kline.Volume
|
|
184
|
+
ap_adjusted = ap * m + a
|
|
185
|
+
kline.Volume *= (1 + share_ratio)
|
|
186
|
+
kline.Amount = kline.Volume * ap_adjusted
|
|
187
|
+
|
|
188
|
+
kline.AdjustmentCount += 1
|
|
189
|
+
|
|
190
|
+
times -= 1
|
|
191
|
+
|
|
192
|
+
class DataKLine:
|
|
193
|
+
def update(self, code: str, date: Timestamp):
|
|
194
|
+
# 1. Determine start date from local cache
|
|
195
|
+
current_start_date = Timestamp.parse("1990-12-19") # market_first_date
|
|
196
|
+
cache_filename = config.get_kline_filename(code)
|
|
197
|
+
cache_klines = read_kline_from_csv(cache_filename)
|
|
198
|
+
|
|
199
|
+
klines_length = len(cache_klines)
|
|
200
|
+
klines_offset_days = MAX_KLINE_LOOKBACK_DAYS
|
|
201
|
+
adjust_times = 0
|
|
202
|
+
|
|
203
|
+
if klines_length > 0:
|
|
204
|
+
if klines_offset_days > klines_length:
|
|
205
|
+
klines_offset_days = klines_length
|
|
206
|
+
|
|
207
|
+
kline = cache_klines[klines_length - klines_offset_days]
|
|
208
|
+
current_start_date = Timestamp.parse(kline.Date)
|
|
209
|
+
adjust_times = kline.AdjustmentCount
|
|
210
|
+
|
|
211
|
+
# 2. Determine end date
|
|
212
|
+
current_end_date = Timestamp.now().get_pre_market_time()
|
|
213
|
+
logger.debug(f"[dataset::KLine] [{code}]: from {current_start_date.only_date()} to {current_end_date.only_date()}")
|
|
214
|
+
|
|
215
|
+
step = SECURITY_BARS_MAX
|
|
216
|
+
start = 0
|
|
217
|
+
hs: List[List[SecurityBar]] = []
|
|
218
|
+
element_count = 0
|
|
219
|
+
|
|
220
|
+
while True:
|
|
221
|
+
count = step
|
|
222
|
+
reply = fetch_kline(code, start, count)
|
|
223
|
+
if not reply:
|
|
224
|
+
break
|
|
225
|
+
|
|
226
|
+
element_count += len(reply)
|
|
227
|
+
hs.append(reply)
|
|
228
|
+
|
|
229
|
+
last_bar = reply[-1]
|
|
230
|
+
last_bar_date = Timestamp.parse(f"{last_bar.Year}-{last_bar.Month:02d}-{last_bar.Day:02d}").get_pre_market_time()
|
|
231
|
+
|
|
232
|
+
if last_bar_date < current_start_date:
|
|
233
|
+
break
|
|
234
|
+
|
|
235
|
+
if len(reply) < count:
|
|
236
|
+
break
|
|
237
|
+
|
|
238
|
+
start += count
|
|
239
|
+
|
|
240
|
+
hs.reverse()
|
|
241
|
+
|
|
242
|
+
incremental_klines: List[KLine] = []
|
|
243
|
+
|
|
244
|
+
for vec in hs:
|
|
245
|
+
for row in vec:
|
|
246
|
+
date_time = Timestamp.parse(f"{row.Year}-{row.Month:02d}-{row.Day:02d}").get_pre_market_time()
|
|
247
|
+
|
|
248
|
+
if date_time < current_start_date or date_time > current_end_date:
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
kx = KLine(
|
|
252
|
+
Date=date_time.only_date(),
|
|
253
|
+
Open=row.Open,
|
|
254
|
+
Close=row.Close,
|
|
255
|
+
High=row.High,
|
|
256
|
+
Low=row.Low,
|
|
257
|
+
Volume=row.Vol * 100, # Convert to shares
|
|
258
|
+
Amount=row.Amount,
|
|
259
|
+
Up=row.UpCount,
|
|
260
|
+
Down=row.DownCount,
|
|
261
|
+
Datetime=row.DateTime,
|
|
262
|
+
AdjustmentCount=0
|
|
263
|
+
)
|
|
264
|
+
incremental_klines.append(kx)
|
|
265
|
+
|
|
266
|
+
# 6. Adjustment logic
|
|
267
|
+
is_fresh_fetch_require_adjustment = (adjust_times == 1)
|
|
268
|
+
dividends = xdxr.load_xdxr(code)
|
|
269
|
+
|
|
270
|
+
if is_fresh_fetch_require_adjustment:
|
|
271
|
+
apply_forward_adjustment_for_event(incremental_klines, current_start_date, dividends)
|
|
272
|
+
|
|
273
|
+
# 7. Merge
|
|
274
|
+
klines = []
|
|
275
|
+
if klines_length > klines_offset_days:
|
|
276
|
+
klines.extend(cache_klines[:klines_length - klines_offset_days])
|
|
277
|
+
|
|
278
|
+
klines.extend(incremental_klines)
|
|
279
|
+
|
|
280
|
+
# 8. Forward adjust
|
|
281
|
+
if not is_fresh_fetch_require_adjustment:
|
|
282
|
+
apply_forward_adjustment_for_event(klines, current_start_date, dividends)
|
|
283
|
+
|
|
284
|
+
# 9. Save
|
|
285
|
+
save_kline(cache_filename, klines)
|
|
286
|
+
|