quant1x 0.7.2__tar.gz → 0.7.4__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.4}/PKG-INFO +1 -1
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/cache/f10.py +13 -1
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/cache/market.py +3 -3
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/cache/sector.py +2 -2
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/cache/security.py +1 -1
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/config/__init__.py +8 -1
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/exchange/__init__.py +18 -5
- quant1x-0.7.4/quant1x/exchange/calendar.py +336 -0
- quant1x-0.7.4/quant1x/exchange/code.py +193 -0
- quant1x-0.7.4/quant1x/exchange/markets.py +116 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/exchange/security.py +1 -1
- quant1x-0.7.4/quant1x/exchange/session.py +348 -0
- quant1x-0.7.4/quant1x/exchange/timestamp.py +216 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x.egg-info/PKG-INFO +1 -1
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x.egg-info/SOURCES.txt +3 -1
- {quant1x-0.7.2 → quant1x-0.7.4}/tests/test_cache.py +2 -2
- 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/timestamp.py +0 -170
- {quant1x-0.7.2 → quant1x-0.7.4}/LICENSE +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/README.md +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/__init__.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/cache/__init__.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/config/config.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/device.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/devp.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/encoding/__init__.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/encoding/yaml.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/formula/__init__.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/formula/formula.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/formula/formula_plus.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/indicators/__init__.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/indicators/cdtd.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/indicators/f89k.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/indicators/hjk.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/indicators/ma1x.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/indicators/p2fb.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/level1/__init__.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/level1/client.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/level1/config.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/level1/protocol.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/level1/security_list.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/log/__init__.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/log/logger.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/net/__init__.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/net/connection_pool.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/net/endpoint.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/net/operation_handler.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/network.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/numerics.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/std/__init__.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/std/pattern.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/system.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/util/__init__.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/util/chart.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/util/font.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x/version.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x.egg-info/dependency_links.txt +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x.egg-info/not-zip-safe +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x.egg-info/requires.txt +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/quant1x.egg-info/top_level.txt +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/setup.cfg +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/setup.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/tests/test_connection_pool.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/tests/test_handshake.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/tests/test_protocol_fixtures.py +0 -0
- {quant1x-0.7.2 → quant1x-0.7.4}/tests/test_protocol_real_network.py +0 -0
|
@@ -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:
|
|
@@ -13,7 +13,14 @@ from .config import (
|
|
|
13
13
|
quant1x_config
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
+
# 导出常用配置路径
|
|
17
|
+
data_path = quant1x_config.data_path
|
|
18
|
+
meta_path = quant1x_config.meta_path
|
|
19
|
+
kline_path = quant1x_config.kline_path
|
|
20
|
+
|
|
16
21
|
__all__ = [
|
|
17
|
-
"quant1x_config",
|
|
18
22
|
"get_quant1x_config_filename",
|
|
23
|
+
"data_path",
|
|
24
|
+
"meta_path",
|
|
25
|
+
"kline_path",
|
|
19
26
|
]
|
|
@@ -8,15 +8,28 @@
|
|
|
8
8
|
@Date : 2025/9/15 16:28
|
|
9
9
|
@Desc : 配置信息
|
|
10
10
|
"""
|
|
11
|
-
from .
|
|
12
|
-
|
|
13
|
-
last_trade_date,
|
|
11
|
+
from .calendar import (
|
|
12
|
+
calendar,
|
|
14
13
|
fix_trade_date,
|
|
14
|
+
get_today,
|
|
15
|
+
is_session_pre,
|
|
16
|
+
is_session_post,
|
|
17
|
+
front_trade_date,
|
|
18
|
+
last_trade_date,
|
|
19
|
+
next_trade_date
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
from .code import (
|
|
23
|
+
correct_security_code,
|
|
15
24
|
)
|
|
16
25
|
|
|
17
26
|
__all__ = [
|
|
18
27
|
"correct_security_code",
|
|
19
|
-
"last_trade_date",
|
|
20
28
|
"fix_trade_date",
|
|
21
|
-
|
|
29
|
+
"front_trade_date",
|
|
30
|
+
"last_trade_date",
|
|
31
|
+
"next_trade_date",
|
|
32
|
+
"get_today",
|
|
33
|
+
"is_session_pre",
|
|
34
|
+
"is_session_post",
|
|
22
35
|
]
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from .. import config
|
|
6
|
+
from . import session
|
|
7
|
+
from .timestamp import Timestamp
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
import bisect
|
|
12
|
+
|
|
13
|
+
exchange_start_time = '09:15:00'
|
|
14
|
+
exchange_end_time = '15:00:00'
|
|
15
|
+
# time_range = "09:15:00~11:30:00,13:00:00~15:00:00"
|
|
16
|
+
trade_session = session.TimeRange(f'{exchange_start_time}~{exchange_end_time}')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@lru_cache(maxsize=None)
|
|
20
|
+
def __calendar() -> pd.Series:
|
|
21
|
+
"""
|
|
22
|
+
交易日历
|
|
23
|
+
"""
|
|
24
|
+
fn = os.path.join(config.meta_path, "calendar")
|
|
25
|
+
df = pd.read_csv(fn)
|
|
26
|
+
return df['date']
|
|
27
|
+
|
|
28
|
+
@lru_cache(maxsize=None)
|
|
29
|
+
def __calendar_timestamps() -> List[Timestamp]:
|
|
30
|
+
"""
|
|
31
|
+
交易日历 (Timestamp对象列表)
|
|
32
|
+
"""
|
|
33
|
+
dates = __calendar()
|
|
34
|
+
# 转换为 Timestamp 对象,并设置为当天的盘前时间 (09:00:00)
|
|
35
|
+
return [Timestamp.parse(d).get_pre_market_time() for d in dates]
|
|
36
|
+
|
|
37
|
+
def calendar() -> pd.Series:
|
|
38
|
+
"""
|
|
39
|
+
获取全部的交易日期
|
|
40
|
+
Returns:
|
|
41
|
+
pd.Series
|
|
42
|
+
"""
|
|
43
|
+
return __calendar()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def fix_trade_date(date_str: str, fmt: str = "%Y-%m-%d") -> str:
|
|
47
|
+
"""强制将日期字符串转换为指定格式
|
|
48
|
+
|
|
49
|
+
参数:
|
|
50
|
+
date_str: 输入日期字符串
|
|
51
|
+
fmt: 目标格式(默认%Y-%m-%d)
|
|
52
|
+
|
|
53
|
+
返回:
|
|
54
|
+
统一格式的日期字符串
|
|
55
|
+
|
|
56
|
+
示例:
|
|
57
|
+
>>> fix_trade_date("2023/12/25")
|
|
58
|
+
"2023-12-25"
|
|
59
|
+
"""
|
|
60
|
+
from datetime import datetime
|
|
61
|
+
return datetime.strptime(date_str, "%Y-%m-%d").strftime(fmt) if date_str else date_str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_today() -> str:
|
|
65
|
+
"""
|
|
66
|
+
获取当前日期
|
|
67
|
+
"""
|
|
68
|
+
date = time.strftime(session.FORMAT_ONLY_DATE)
|
|
69
|
+
return date
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_session_pre() -> bool:
|
|
73
|
+
"""
|
|
74
|
+
是否盘前
|
|
75
|
+
"""
|
|
76
|
+
now = time.strftime(session.FORMAT_ONLY_TIME)
|
|
77
|
+
return now < exchange_start_time
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def is_session_reg() -> bool:
|
|
81
|
+
"""
|
|
82
|
+
是否盘中
|
|
83
|
+
"""
|
|
84
|
+
now = time.strftime(session.FORMAT_ONLY_TIME)
|
|
85
|
+
return trade_session.is_trading(now)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def is_session_post() -> bool:
|
|
89
|
+
"""
|
|
90
|
+
是否盘后
|
|
91
|
+
"""
|
|
92
|
+
now = time.strftime(session.FORMAT_ONLY_TIME)
|
|
93
|
+
return now > exchange_end_time
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def last_trade_date(base_date: str = None) -> str:
|
|
97
|
+
"""
|
|
98
|
+
获取基准日期之前最近的一个交易日(若未指定基准日期则使用当天)
|
|
99
|
+
|
|
100
|
+
参数:
|
|
101
|
+
base_date: 基准日期字符串(格式:YYYY-MM-DD),可选
|
|
102
|
+
|
|
103
|
+
返回:
|
|
104
|
+
最近交易日的日期字符串(格式:YYYY-MM-DD)
|
|
105
|
+
"""
|
|
106
|
+
calendar_series = __calendar() # 获取交易日历
|
|
107
|
+
ref_date = base_date if base_date is not None else get_today() # 确定基准日期
|
|
108
|
+
session_pre = trade_session.is_session_pre() if base_date is None else False # 仅对当天检查盘前
|
|
109
|
+
|
|
110
|
+
# 查找基准日期的位置
|
|
111
|
+
idx = calendar_series.searchsorted(ref_date)
|
|
112
|
+
if isinstance(idx, np.ndarray): # 处理可能的数组返回
|
|
113
|
+
idx = idx[0]
|
|
114
|
+
|
|
115
|
+
# 获取候选日期
|
|
116
|
+
date = calendar_series.iloc[idx]
|
|
117
|
+
|
|
118
|
+
# 逻辑判断
|
|
119
|
+
if (str(date) > ref_date) or (str(date) == ref_date and session_pre):
|
|
120
|
+
date = calendar_series.iloc[idx - 1]
|
|
121
|
+
|
|
122
|
+
# 确保返回字符串
|
|
123
|
+
return str(date) if not isinstance(date, str) else date
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def front_trade_date(n: int = 1, base_date: str = None) -> str:
|
|
127
|
+
"""获取基准日期前N个交易日
|
|
128
|
+
|
|
129
|
+
参数:
|
|
130
|
+
n: 向前追溯的交易日的数量(默认1)
|
|
131
|
+
base_date: 基准日期(可选),格式为YYYY-MM-DD
|
|
132
|
+
"""
|
|
133
|
+
dates = __calendar()
|
|
134
|
+
ref_date = base_date if base_date is not None else last_trade_date()
|
|
135
|
+
|
|
136
|
+
# 处理searchsorted返回值(兼容所有类型)
|
|
137
|
+
idx = dates.searchsorted(ref_date)
|
|
138
|
+
idx = idx[0] if hasattr(idx, '__iter__') else idx
|
|
139
|
+
|
|
140
|
+
return str(dates.iat[max(0, int(idx) - n)])
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def next_trade_date(base_date: str = None) -> str:
|
|
144
|
+
"""获取基准日期后的下一个交易日
|
|
145
|
+
|
|
146
|
+
参数:
|
|
147
|
+
base_date: 基准日期(可选),格式为YYYY-MM-DD
|
|
148
|
+
"""
|
|
149
|
+
dates = __calendar()
|
|
150
|
+
ref_date = base_date if base_date is not None else last_trade_date()
|
|
151
|
+
|
|
152
|
+
# 使用searchsorted高效查找
|
|
153
|
+
idx = dates.searchsorted(ref_date, side='right')
|
|
154
|
+
idx = idx[0] if hasattr(idx, '__iter__') else idx
|
|
155
|
+
|
|
156
|
+
return str(dates.iloc[min(int(idx), len(dates) - 1)])
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def last_trading_day(date: Optional[Timestamp] = None, debug_timestamp: Optional[Timestamp] = None) -> Timestamp:
|
|
160
|
+
"""
|
|
161
|
+
获取最近一个交易日 (Timestamp版本)
|
|
162
|
+
"""
|
|
163
|
+
trade_dates = __calendar_timestamps()
|
|
164
|
+
if not trade_dates:
|
|
165
|
+
return Timestamp.zero()
|
|
166
|
+
|
|
167
|
+
if date is None:
|
|
168
|
+
# 默认使用今天
|
|
169
|
+
date = Timestamp.now().get_pre_market_time()
|
|
170
|
+
|
|
171
|
+
# 查找 date 在 trade_dates 中的位置 (upper_bound)
|
|
172
|
+
# bisect_right 相当于 C++ 的 upper_bound
|
|
173
|
+
idx = bisect.bisect_right(trade_dates, date)
|
|
174
|
+
|
|
175
|
+
if idx > 0:
|
|
176
|
+
idx -= 1
|
|
177
|
+
|
|
178
|
+
# 判断是否盘前
|
|
179
|
+
last_ts = trade_dates[idx]
|
|
180
|
+
current_ts = debug_timestamp if debug_timestamp is not None else Timestamp.now()
|
|
181
|
+
|
|
182
|
+
if current_ts < last_ts and idx > 0:
|
|
183
|
+
idx -= 1
|
|
184
|
+
|
|
185
|
+
return trade_dates[idx]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def prev_trading_day(date: Optional[Timestamp] = None, debug_timestamp: Optional[Timestamp] = None) -> Timestamp:
|
|
189
|
+
"""
|
|
190
|
+
获取上一个交易日 (Timestamp版本)
|
|
191
|
+
"""
|
|
192
|
+
trade_dates = __calendar_timestamps()
|
|
193
|
+
if not trade_dates:
|
|
194
|
+
return Timestamp.zero()
|
|
195
|
+
|
|
196
|
+
if date is None:
|
|
197
|
+
date = Timestamp.now().get_pre_market_time()
|
|
198
|
+
|
|
199
|
+
# 查找 date 在 trade_dates 中的位置 (lower_bound)
|
|
200
|
+
# bisect_left 相当于 C++ 的 lower_bound
|
|
201
|
+
idx = bisect.bisect_left(trade_dates, date)
|
|
202
|
+
|
|
203
|
+
if idx > 0:
|
|
204
|
+
idx -= 1
|
|
205
|
+
|
|
206
|
+
# 判断是否盘前
|
|
207
|
+
last_ts = trade_dates[idx]
|
|
208
|
+
current_ts = debug_timestamp if debug_timestamp is not None else Timestamp.now()
|
|
209
|
+
|
|
210
|
+
if current_ts < last_ts and idx > 0:
|
|
211
|
+
idx -= 1
|
|
212
|
+
|
|
213
|
+
return trade_dates[idx]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def next_trading_day_ts(date: Optional[Timestamp] = None, debug_timestamp: Optional[Timestamp] = None) -> Timestamp:
|
|
217
|
+
"""
|
|
218
|
+
获取下一个交易日 (Timestamp版本)
|
|
219
|
+
"""
|
|
220
|
+
trade_dates = __calendar_timestamps()
|
|
221
|
+
if not trade_dates:
|
|
222
|
+
return Timestamp.zero()
|
|
223
|
+
|
|
224
|
+
if date is None:
|
|
225
|
+
date = Timestamp.now().get_pre_market_time()
|
|
226
|
+
|
|
227
|
+
current_time = debug_timestamp if debug_timestamp is not None else Timestamp.now()
|
|
228
|
+
|
|
229
|
+
# 找到第一个大于等于 date 的交易日 (lower_bound)
|
|
230
|
+
idx = bisect.bisect_left(trade_dates, date)
|
|
231
|
+
|
|
232
|
+
if idx >= len(trade_dates):
|
|
233
|
+
if trade_dates:
|
|
234
|
+
return trade_dates[-1]
|
|
235
|
+
return Timestamp.zero()
|
|
236
|
+
|
|
237
|
+
candidate_day = trade_dates[idx]
|
|
238
|
+
|
|
239
|
+
# 如果当前时间已经过了候选交易日的盘前时间,则取下一个
|
|
240
|
+
if current_time >= candidate_day and idx < len(trade_dates):
|
|
241
|
+
idx += 1
|
|
242
|
+
if idx >= len(trade_dates):
|
|
243
|
+
return trade_dates[-1]
|
|
244
|
+
return trade_dates[idx]
|
|
245
|
+
|
|
246
|
+
return candidate_day
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def date_range(begin: Timestamp, end: Optional[Timestamp] = None, skip_today: bool = False) -> List[Timestamp]:
|
|
250
|
+
"""
|
|
251
|
+
获取日期范围 (Timestamp版本)
|
|
252
|
+
"""
|
|
253
|
+
if end is None:
|
|
254
|
+
end = Timestamp.now()
|
|
255
|
+
|
|
256
|
+
if begin > end:
|
|
257
|
+
return []
|
|
258
|
+
|
|
259
|
+
trade_dates = __calendar_timestamps()
|
|
260
|
+
if not trade_dates:
|
|
261
|
+
return []
|
|
262
|
+
|
|
263
|
+
# 查找范围边界
|
|
264
|
+
lower = bisect.bisect_left(trade_dates, begin)
|
|
265
|
+
upper = bisect.bisect_right(trade_dates, end)
|
|
266
|
+
|
|
267
|
+
# 处理 skip_today 逻辑
|
|
268
|
+
if skip_today and upper > 0:
|
|
269
|
+
today = Timestamp.now().get_pre_market_time()
|
|
270
|
+
last_in_range = trade_dates[upper - 1]
|
|
271
|
+
if last_in_range > today or last_in_range > end:
|
|
272
|
+
upper -= 1
|
|
273
|
+
else:
|
|
274
|
+
# 调整 upper 到最后一个 <= end 的日期
|
|
275
|
+
# bisect_right 返回的是第一个 > end 的位置,所以 upper-1 就是 <= end 的位置
|
|
276
|
+
# 这里不需要额外调整,切片 [lower:upper] 刚好包含 lower 到 upper-1
|
|
277
|
+
pass
|
|
278
|
+
|
|
279
|
+
if lower >= upper:
|
|
280
|
+
return []
|
|
281
|
+
|
|
282
|
+
return trade_dates[lower:upper]
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def get_date_range(begin: str, end: str, skip_today: bool = False) -> List[str]:
|
|
286
|
+
"""
|
|
287
|
+
获取日期范围 (字符串版本)
|
|
288
|
+
"""
|
|
289
|
+
if begin > end:
|
|
290
|
+
return []
|
|
291
|
+
|
|
292
|
+
trade_dates = __calendar().tolist() # Convert Series to list for bisect
|
|
293
|
+
if not trade_dates:
|
|
294
|
+
return []
|
|
295
|
+
|
|
296
|
+
# 查找起始索引
|
|
297
|
+
it_start = bisect.bisect_left(trade_dates, begin)
|
|
298
|
+
|
|
299
|
+
# 查找结束索引
|
|
300
|
+
it_end = bisect.bisect_left(trade_dates, end)
|
|
301
|
+
|
|
302
|
+
# bisect_left 返回的是第一个 >= end 的位置
|
|
303
|
+
# 如果 trade_dates[it_end] == end, 我们需要包含它,所以切片应该是 it_end + 1
|
|
304
|
+
# 如果 trade_dates[it_end] > end, 我们不需要包含它,切片应该是 it_end
|
|
305
|
+
|
|
306
|
+
# 为了匹配 C++ lower_bound 逻辑:
|
|
307
|
+
# C++: lower_bound(begin), lower_bound(end)
|
|
308
|
+
# range: [itStart, itEnd) if *itEnd >= end? No, C++ logic is complex.
|
|
309
|
+
# Let's simplify: find all dates d such that begin <= d <= end
|
|
310
|
+
|
|
311
|
+
# Re-implement using simple filtering or bisect_right for end
|
|
312
|
+
|
|
313
|
+
it_start = bisect.bisect_left(trade_dates, begin)
|
|
314
|
+
it_end = bisect.bisect_right(trade_dates, end) # first element > end
|
|
315
|
+
|
|
316
|
+
if skip_today:
|
|
317
|
+
if it_end > 0:
|
|
318
|
+
today_str = get_today()
|
|
319
|
+
last_day = trade_dates[it_end - 1]
|
|
320
|
+
if last_day > today_str or last_day > end:
|
|
321
|
+
it_end -= 1
|
|
322
|
+
|
|
323
|
+
if it_start >= it_end:
|
|
324
|
+
return []
|
|
325
|
+
|
|
326
|
+
return trade_dates[it_start:it_end]
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
if __name__ == '__main__':
|
|
330
|
+
a1 = last_trade_date()
|
|
331
|
+
print(type(a1))
|
|
332
|
+
print(a1)
|
|
333
|
+
d1 = last_trade_date()
|
|
334
|
+
print(d1)
|
|
335
|
+
print(front_trade_date())
|
|
336
|
+
print(next_trade_date())
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Tuple, List
|
|
3
|
+
|
|
4
|
+
# Constants
|
|
5
|
+
MARKET_SHANGHAI = "sh"
|
|
6
|
+
MARKET_SHENZHEN = "sz"
|
|
7
|
+
MARKET_BEIJING = "bj"
|
|
8
|
+
MARKET_HONGKONG = "hk"
|
|
9
|
+
MARKET_USA = "us"
|
|
10
|
+
|
|
11
|
+
MARKET_FLAGS = ["sh", "sz", "SH", "SZ", "bj", "BJ", "hk", "HK", "us", "US"]
|
|
12
|
+
|
|
13
|
+
SHANGHAI_MAIN_BOARD_PREFIXES = ["50", "51", "60", "68", "90", "110", "113", "132", "204"]
|
|
14
|
+
SHANGHAI_SPECIAL_PREFIXES = ["5", "6", "9", "7"]
|
|
15
|
+
SECTOR_PREFIXES = ["880", "881"]
|
|
16
|
+
SHENZHEN_MAIN_BOARD_PREFIXES = ["00", "12", "13", "18", "15", "16", "18", "20", "30", "39", "115", "1318"]
|
|
17
|
+
BEIJING_MAIN_BOARD_PREFIXES = ["40", "43", "83", "87", "88", "420", "820", "899", "920"]
|
|
18
|
+
|
|
19
|
+
class MarketType(Enum):
|
|
20
|
+
ShenZhen = 0
|
|
21
|
+
ShangHai = 1
|
|
22
|
+
BeiJing = 2
|
|
23
|
+
HongKong = 21
|
|
24
|
+
USA = 22
|
|
25
|
+
|
|
26
|
+
class TargetKind(Enum):
|
|
27
|
+
STOCK = 0
|
|
28
|
+
INDEX = 1
|
|
29
|
+
BLOCK = 2
|
|
30
|
+
ETF = 3
|
|
31
|
+
|
|
32
|
+
def starts_with(s: str, prefixes: List[str]) -> bool:
|
|
33
|
+
return any(s.startswith(prefix) for prefix in prefixes)
|
|
34
|
+
|
|
35
|
+
def ends_with(s: str, suffixes: List[str]) -> bool:
|
|
36
|
+
return any(s.endswith(suffix) for suffix in suffixes)
|
|
37
|
+
|
|
38
|
+
def get_security_code(market: MarketType, symbol: str) -> str:
|
|
39
|
+
if market == MarketType.USA:
|
|
40
|
+
return MARKET_USA + symbol
|
|
41
|
+
elif market == MarketType.HongKong:
|
|
42
|
+
return MARKET_HONGKONG + symbol[:5]
|
|
43
|
+
elif market == MarketType.BeiJing:
|
|
44
|
+
return MARKET_BEIJING + symbol[:6]
|
|
45
|
+
elif market == MarketType.ShenZhen:
|
|
46
|
+
return MARKET_SHENZHEN + symbol[:6]
|
|
47
|
+
else:
|
|
48
|
+
return MARKET_SHANGHAI + symbol[:6]
|
|
49
|
+
|
|
50
|
+
def get_market(symbol: str) -> str:
|
|
51
|
+
code = symbol.strip()
|
|
52
|
+
market = MARKET_SHANGHAI
|
|
53
|
+
|
|
54
|
+
if starts_with(code, MARKET_FLAGS):
|
|
55
|
+
market = code[:2].lower()
|
|
56
|
+
elif ends_with(code, MARKET_FLAGS):
|
|
57
|
+
market = code[-2:].lower()
|
|
58
|
+
elif starts_with(code, SHANGHAI_MAIN_BOARD_PREFIXES):
|
|
59
|
+
market = MARKET_SHANGHAI
|
|
60
|
+
elif starts_with(code, SHENZHEN_MAIN_BOARD_PREFIXES):
|
|
61
|
+
market = MARKET_SHENZHEN
|
|
62
|
+
elif starts_with(code, SHANGHAI_SPECIAL_PREFIXES):
|
|
63
|
+
market = MARKET_SHANGHAI
|
|
64
|
+
elif starts_with(code, SECTOR_PREFIXES):
|
|
65
|
+
market = MARKET_SHANGHAI
|
|
66
|
+
elif starts_with(code, BEIJING_MAIN_BOARD_PREFIXES):
|
|
67
|
+
market = MARKET_BEIJING
|
|
68
|
+
|
|
69
|
+
return market
|
|
70
|
+
|
|
71
|
+
def get_market_id(symbol: str) -> MarketType:
|
|
72
|
+
market = get_market(symbol)
|
|
73
|
+
if market == MARKET_SHANGHAI:
|
|
74
|
+
return MarketType.ShangHai
|
|
75
|
+
if market == MARKET_SHENZHEN:
|
|
76
|
+
return MarketType.ShenZhen
|
|
77
|
+
if market == MARKET_BEIJING:
|
|
78
|
+
return MarketType.BeiJing
|
|
79
|
+
return MarketType.ShangHai
|
|
80
|
+
|
|
81
|
+
def get_market_flag(market_id: MarketType) -> str:
|
|
82
|
+
if market_id == MarketType.ShenZhen:
|
|
83
|
+
return MARKET_SHENZHEN
|
|
84
|
+
elif market_id == MarketType.BeiJing:
|
|
85
|
+
return MARKET_BEIJING
|
|
86
|
+
elif market_id == MarketType.HongKong:
|
|
87
|
+
return MARKET_HONGKONG
|
|
88
|
+
elif market_id == MarketType.USA:
|
|
89
|
+
return MARKET_USA
|
|
90
|
+
else:
|
|
91
|
+
return MARKET_SHANGHAI
|
|
92
|
+
|
|
93
|
+
def detect_market(symbol: str) -> Tuple[MarketType, str, str]:
|
|
94
|
+
pure_code = symbol.strip()
|
|
95
|
+
market_code = MARKET_SHANGHAI
|
|
96
|
+
|
|
97
|
+
if starts_with(pure_code, MARKET_FLAGS):
|
|
98
|
+
market_code = pure_code[:2].lower()
|
|
99
|
+
if len(pure_code) > 2 and pure_code[2] == '.':
|
|
100
|
+
pure_code = pure_code[3:]
|
|
101
|
+
else:
|
|
102
|
+
pure_code = pure_code[2:]
|
|
103
|
+
elif ends_with(pure_code, MARKET_FLAGS):
|
|
104
|
+
market_code = pure_code[-2:].lower()
|
|
105
|
+
pure_code = pure_code[:-3]
|
|
106
|
+
elif starts_with(pure_code, SHANGHAI_MAIN_BOARD_PREFIXES):
|
|
107
|
+
market_code = MARKET_SHANGHAI
|
|
108
|
+
elif starts_with(pure_code, SHENZHEN_MAIN_BOARD_PREFIXES):
|
|
109
|
+
market_code = MARKET_SHENZHEN
|
|
110
|
+
elif starts_with(pure_code, SHANGHAI_SPECIAL_PREFIXES):
|
|
111
|
+
market_code = MARKET_SHANGHAI
|
|
112
|
+
elif starts_with(pure_code, SECTOR_PREFIXES):
|
|
113
|
+
market_code = MARKET_SHANGHAI
|
|
114
|
+
elif starts_with(pure_code, BEIJING_MAIN_BOARD_PREFIXES):
|
|
115
|
+
market_code = MARKET_BEIJING
|
|
116
|
+
|
|
117
|
+
market_id = MarketType.ShangHai
|
|
118
|
+
if market_code == MARKET_SHANGHAI:
|
|
119
|
+
market_id = MarketType.ShangHai
|
|
120
|
+
elif market_code == MARKET_SHENZHEN:
|
|
121
|
+
market_id = MarketType.ShenZhen
|
|
122
|
+
elif market_code == MARKET_BEIJING:
|
|
123
|
+
market_id = MarketType.BeiJing
|
|
124
|
+
elif market_code == MARKET_HONGKONG:
|
|
125
|
+
market_id = MarketType.HongKong
|
|
126
|
+
|
|
127
|
+
return market_id, market_code, pure_code
|
|
128
|
+
|
|
129
|
+
def assert_index_by_market_and_code(market_id: MarketType, symbol: str) -> bool:
|
|
130
|
+
if market_id == MarketType.ShangHai and starts_with(symbol, ["000", "880", "881"]):
|
|
131
|
+
return True
|
|
132
|
+
if market_id == MarketType.ShenZhen and starts_with(symbol, ["399"]):
|
|
133
|
+
return True
|
|
134
|
+
if market_id == MarketType.BeiJing and starts_with(symbol, ["899"]):
|
|
135
|
+
return True
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
def assert_index_by_security_code(security_code: str) -> bool:
|
|
139
|
+
market_id, _, code = detect_market(security_code)
|
|
140
|
+
return assert_index_by_market_and_code(market_id, code)
|
|
141
|
+
|
|
142
|
+
def assert_block_by_security_code(security_code: str) -> Tuple[bool, str]:
|
|
143
|
+
market_id, flag, code = detect_market(security_code)
|
|
144
|
+
if market_id != MarketType.ShangHai or not starts_with(code, SECTOR_PREFIXES):
|
|
145
|
+
return False, security_code
|
|
146
|
+
return True, flag + code
|
|
147
|
+
|
|
148
|
+
def assert_etf_by_market_and_code(market_id: MarketType, symbol: str) -> bool:
|
|
149
|
+
return market_id == MarketType.ShangHai and starts_with(symbol, ["510"])
|
|
150
|
+
|
|
151
|
+
def assert_stock_by_market_and_code(market_id: MarketType, symbol: str) -> bool:
|
|
152
|
+
if market_id == MarketType.ShangHai and starts_with(symbol, ["60", "68", "510"]):
|
|
153
|
+
return True
|
|
154
|
+
if market_id == MarketType.ShenZhen and starts_with(symbol, ["00", "30"]):
|
|
155
|
+
return True
|
|
156
|
+
if market_id == MarketType.BeiJing and starts_with(symbol, ["40", "43", "83", "87", "88", "420", "820", "920"]):
|
|
157
|
+
return True
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
def assert_stock_by_security_code(security_code: str) -> bool:
|
|
161
|
+
market_id, _, code = detect_market(security_code)
|
|
162
|
+
return assert_stock_by_market_and_code(market_id, code)
|
|
163
|
+
|
|
164
|
+
def correct_security_code(symbol: str) -> str:
|
|
165
|
+
if not symbol:
|
|
166
|
+
return ""
|
|
167
|
+
_, m_flag, m_symbol = detect_market(symbol)
|
|
168
|
+
return m_flag + m_symbol
|
|
169
|
+
|
|
170
|
+
def assert_code(security_code: str) -> TargetKind:
|
|
171
|
+
market_id, _, code = detect_market(security_code)
|
|
172
|
+
if market_id == MarketType.ShangHai:
|
|
173
|
+
if starts_with(code, SECTOR_PREFIXES):
|
|
174
|
+
return TargetKind.BLOCK
|
|
175
|
+
if starts_with(code, ["000"]):
|
|
176
|
+
return TargetKind.INDEX
|
|
177
|
+
if starts_with(code, ["5"]):
|
|
178
|
+
return TargetKind.ETF
|
|
179
|
+
if market_id == MarketType.ShenZhen:
|
|
180
|
+
if starts_with(code, ["399"]):
|
|
181
|
+
return TargetKind.INDEX
|
|
182
|
+
if starts_with(code, ["159"]):
|
|
183
|
+
return TargetKind.ETF
|
|
184
|
+
if market_id == MarketType.BeiJing and starts_with(code, ["899"]):
|
|
185
|
+
return TargetKind.INDEX
|
|
186
|
+
return TargetKind.STOCK
|
|
187
|
+
|
|
188
|
+
def check_index_and_stock(security_code: str) -> bool:
|
|
189
|
+
if assert_index_by_security_code(security_code):
|
|
190
|
+
return True
|
|
191
|
+
if assert_stock_by_security_code(security_code):
|
|
192
|
+
return True
|
|
193
|
+
return False
|