cyqnt-trd 0.1.2__py3-none-any.whl
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.
- cyqnt_trd/__init__.py +26 -0
- cyqnt_trd/backtesting/README.md +264 -0
- cyqnt_trd/backtesting/__init__.py +12 -0
- cyqnt_trd/backtesting/factor_test.py +332 -0
- cyqnt_trd/backtesting/framework.py +311 -0
- cyqnt_trd/backtesting/strategy_backtest.py +545 -0
- cyqnt_trd/diagnose_api.py +28 -0
- cyqnt_trd/get_data/__init__.py +15 -0
- cyqnt_trd/get_data/get_futures_data.py +472 -0
- cyqnt_trd/get_data/get_trending_data.py +771 -0
- cyqnt_trd/online_trading/__init__.py +13 -0
- cyqnt_trd/online_trading/realtime_price_tracker.py +1001 -0
- cyqnt_trd/test.py +119 -0
- cyqnt_trd/test_script/README.md +411 -0
- cyqnt_trd/test_script/get_network_info.py +192 -0
- cyqnt_trd/test_script/get_symbols_by_volume.py +227 -0
- cyqnt_trd/test_script/realtime_price_tracker.py +839 -0
- cyqnt_trd/test_script/test_alpha.py +261 -0
- cyqnt_trd/test_script/test_kline_data.py +479 -0
- cyqnt_trd/test_script/test_order.py +1360 -0
- cyqnt_trd/trading_signal/README.md +276 -0
- cyqnt_trd/trading_signal/__init__.py +17 -0
- cyqnt_trd/trading_signal/example_test_alpha.py +430 -0
- cyqnt_trd/trading_signal/example_usage.py +431 -0
- cyqnt_trd/trading_signal/factor/__init__.py +18 -0
- cyqnt_trd/trading_signal/factor/ma_factor.py +75 -0
- cyqnt_trd/trading_signal/factor/rsi_factor.py +56 -0
- cyqnt_trd/trading_signal/selected_alpha/__init__.py +158 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha1.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha10.py +90 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha100.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha101.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha11.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha12.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha13.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha14.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha15.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha16.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha17.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha18.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha19.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha2.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha20.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha21.py +89 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha22.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha23.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha24.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha25.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha26.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha27.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha28.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha29.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha3.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha30.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha31.py +90 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha32.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha33.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha34.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha35.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha36.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha37.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha38.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha39.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha4.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha40.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha41.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha42.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha43.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha44.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha45.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha46.py +89 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha47.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha48.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha49.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha5.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha50.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha51.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha52.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha53.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha54.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha55.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha56.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha57.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha58.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha59.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha6.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha60.py +89 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha61.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha62.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha63.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha64.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha65.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha66.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha67.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha68.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha69.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha7.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha70.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha71.py +92 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha72.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha73.py +91 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha74.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha75.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha76.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha77.py +92 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha78.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha79.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha8.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha80.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha81.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha82.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha83.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha84.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha85.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha86.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha87.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha88.py +92 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha89.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha9.py +90 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha90.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha91.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha92.py +92 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha93.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha94.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha95.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha96.py +92 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha97.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha98.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha99.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha_utils.py +342 -0
- cyqnt_trd/trading_signal/selected_alpha/create_all_alphas.py +279 -0
- cyqnt_trd/trading_signal/selected_alpha/generate_alphas.py +133 -0
- cyqnt_trd/trading_signal/selected_alpha/test_alpha.py +261 -0
- cyqnt_trd/trading_signal/signal/__init__.py +20 -0
- cyqnt_trd/trading_signal/signal/factor_based_signal.py +387 -0
- cyqnt_trd/trading_signal/signal/ma_signal.py +163 -0
- cyqnt_trd/utils/__init__.py +3 -0
- cyqnt_trd/utils/set_user.py +33 -0
- cyqnt_trd-0.1.2.dist-info/METADATA +148 -0
- cyqnt_trd-0.1.2.dist-info/RECORD +147 -0
- cyqnt_trd-0.1.2.dist-info/WHEEL +5 -0
- cyqnt_trd-0.1.2.dist-info/licenses/LICENSE +21 -0
- cyqnt_trd-0.1.2.dist-info/top_level.txt +2 -0
- test/real_time_trade.py +746 -0
- test/test_example_usage.py +381 -0
- test/test_get_data.py +310 -0
- test/test_realtime_price_tracker.py +546 -0
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
import csv
|
|
4
|
+
import json
|
|
5
|
+
import requests
|
|
6
|
+
import time
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Optional, Union
|
|
9
|
+
|
|
10
|
+
from binance_sdk_spot.spot import Spot, ConfigurationRestAPI, SPOT_REST_API_PROD_URL
|
|
11
|
+
from binance_sdk_spot.rest_api.models import UiKlinesIntervalEnum
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Configure logging
|
|
15
|
+
logging.basicConfig(
|
|
16
|
+
level=logging.INFO,
|
|
17
|
+
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _get_interval_duration_ms(interval: str) -> int:
|
|
22
|
+
"""
|
|
23
|
+
获取时间间隔对应的毫秒数
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
interval: 时间间隔字符串,例如 '1m', '1h', '1d'
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
对应的毫秒数
|
|
30
|
+
"""
|
|
31
|
+
interval_durations = {
|
|
32
|
+
"1s": 1 * 1000,
|
|
33
|
+
"1m": 60 * 1000,
|
|
34
|
+
"3m": 3 * 60 * 1000,
|
|
35
|
+
"5m": 5 * 60 * 1000,
|
|
36
|
+
"15m": 15 * 60 * 1000,
|
|
37
|
+
"30m": 30 * 60 * 1000,
|
|
38
|
+
"1h": 60 * 60 * 1000,
|
|
39
|
+
"2h": 2 * 60 * 60 * 1000,
|
|
40
|
+
"4h": 4 * 60 * 60 * 1000,
|
|
41
|
+
"6h": 6 * 60 * 60 * 1000,
|
|
42
|
+
"8h": 8 * 60 * 60 * 1000,
|
|
43
|
+
"12h": 12 * 60 * 60 * 1000,
|
|
44
|
+
"1d": 24 * 60 * 60 * 1000,
|
|
45
|
+
"3d": 3 * 24 * 60 * 60 * 1000,
|
|
46
|
+
"1w": 7 * 24 * 60 * 60 * 1000,
|
|
47
|
+
"1M": 30 * 24 * 60 * 60 * 1000, # 近似值,实际月份天数不同
|
|
48
|
+
}
|
|
49
|
+
return interval_durations.get(interval, 60 * 1000)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _convert_to_timestamp_ms(time_input: Union[datetime, str, int, None]) -> Optional[int]:
|
|
53
|
+
"""
|
|
54
|
+
将各种时间格式转换为毫秒时间戳
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
time_input: 时间输入,可以是:
|
|
58
|
+
- datetime 对象
|
|
59
|
+
- 字符串格式的时间,例如 '2023-01-01 00:00:00' 或 '2023-01-01'
|
|
60
|
+
- 整数时间戳(秒或毫秒,自动判断)
|
|
61
|
+
- None
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
毫秒时间戳,如果输入为 None 则返回 None
|
|
65
|
+
"""
|
|
66
|
+
if time_input is None:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
if isinstance(time_input, datetime):
|
|
70
|
+
return int(time_input.timestamp() * 1000)
|
|
71
|
+
|
|
72
|
+
if isinstance(time_input, str):
|
|
73
|
+
# 尝试解析字符串格式的时间
|
|
74
|
+
try:
|
|
75
|
+
# 尝试多种时间格式
|
|
76
|
+
for fmt in ['%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M', '%Y-%m-%d', '%Y/%m/%d %H:%M:%S', '%Y/%m/%d']:
|
|
77
|
+
try:
|
|
78
|
+
dt = datetime.strptime(time_input, fmt)
|
|
79
|
+
return int(dt.timestamp() * 1000)
|
|
80
|
+
except ValueError:
|
|
81
|
+
continue
|
|
82
|
+
raise ValueError(f"无法解析时间字符串: {time_input}")
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logging.error(f"时间字符串解析失败: {e}")
|
|
85
|
+
raise
|
|
86
|
+
|
|
87
|
+
if isinstance(time_input, int):
|
|
88
|
+
# 判断是秒还是毫秒时间戳(通常毫秒时间戳 > 10^10)
|
|
89
|
+
if time_input > 1e10:
|
|
90
|
+
return time_input # 已经是毫秒时间戳
|
|
91
|
+
else:
|
|
92
|
+
return time_input * 1000 # 秒时间戳,转换为毫秒
|
|
93
|
+
|
|
94
|
+
raise TypeError(f"不支持的时间类型: {type(time_input)}")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_and_save_klines(
|
|
98
|
+
symbol: str,
|
|
99
|
+
interval: str = "1m",
|
|
100
|
+
limit: int = 30,
|
|
101
|
+
start_time: Optional[Union[datetime, str, int]] = None,
|
|
102
|
+
end_time: Optional[Union[datetime, str, int]] = None,
|
|
103
|
+
output_dir: str = "data",
|
|
104
|
+
save_csv: bool = True,
|
|
105
|
+
save_json: bool = True
|
|
106
|
+
) -> Optional[list]:
|
|
107
|
+
"""
|
|
108
|
+
查询并保存 Binance 行情数据
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
symbol: 交易对符号,例如 'BTCUSDT', 'ETHUSDT'
|
|
112
|
+
interval: 时间间隔,例如 '1d' (1天), '1h' (1小时), '1m' (1分钟)
|
|
113
|
+
可选值: 1s, 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M
|
|
114
|
+
limit: 返回的数据条数,默认30,最大1000
|
|
115
|
+
start_time: 开始时间,可以是:
|
|
116
|
+
- datetime 对象
|
|
117
|
+
- 字符串格式,例如 '2023-01-01 00:00:00' 或 '2023-01-01'
|
|
118
|
+
- 整数时间戳(秒或毫秒,自动判断)
|
|
119
|
+
- None(不指定开始时间)
|
|
120
|
+
end_time: 结束时间,格式同 start_time
|
|
121
|
+
- None(不指定结束时间)
|
|
122
|
+
output_dir: 保存数据的目录,默认 'data'
|
|
123
|
+
save_csv: 是否保存为 CSV 格式,默认 True
|
|
124
|
+
save_json: 是否保存为 JSON 格式,默认 True
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
返回查询到的K线数据列表,如果出错返回 None
|
|
128
|
+
|
|
129
|
+
Note:
|
|
130
|
+
- 如果 start_time 和 end_time 都不指定,返回最近的 limit 条数据
|
|
131
|
+
- 如果只指定 start_time,返回从 start_time 开始的 limit 条数据
|
|
132
|
+
- 如果只指定 end_time,返回 end_time 之前的 limit 条数据
|
|
133
|
+
- 如果同时指定 start_time 和 end_time,会自动进行分页请求(每次最多1000条),
|
|
134
|
+
获取整个时间范围内的所有数据,不受 limit 参数限制
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
# 创建配置(uiKlines 是公开API,不需要认证)
|
|
138
|
+
configuration_rest_api = ConfigurationRestAPI(
|
|
139
|
+
api_key=os.getenv("API_KEY", ""),
|
|
140
|
+
api_secret=os.getenv("API_SECRET", ""),
|
|
141
|
+
base_path=os.getenv("BASE_PATH", SPOT_REST_API_PROD_URL),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# 初始化 Spot 客户端
|
|
145
|
+
client = Spot(config_rest_api=configuration_rest_api)
|
|
146
|
+
|
|
147
|
+
# 将字符串间隔转换为枚举值
|
|
148
|
+
interval_map = {
|
|
149
|
+
"1s": UiKlinesIntervalEnum.INTERVAL_1s,
|
|
150
|
+
"1m": UiKlinesIntervalEnum.INTERVAL_1m,
|
|
151
|
+
"3m": UiKlinesIntervalEnum.INTERVAL_3m,
|
|
152
|
+
"5m": UiKlinesIntervalEnum.INTERVAL_5m,
|
|
153
|
+
"15m": UiKlinesIntervalEnum.INTERVAL_15m,
|
|
154
|
+
"30m": UiKlinesIntervalEnum.INTERVAL_30m,
|
|
155
|
+
"1h": UiKlinesIntervalEnum.INTERVAL_1h,
|
|
156
|
+
"2h": UiKlinesIntervalEnum.INTERVAL_2h,
|
|
157
|
+
"4h": UiKlinesIntervalEnum.INTERVAL_4h,
|
|
158
|
+
"6h": UiKlinesIntervalEnum.INTERVAL_6h,
|
|
159
|
+
"8h": UiKlinesIntervalEnum.INTERVAL_8h,
|
|
160
|
+
"12h": UiKlinesIntervalEnum.INTERVAL_12h,
|
|
161
|
+
"1d": UiKlinesIntervalEnum.INTERVAL_1d,
|
|
162
|
+
"3d": UiKlinesIntervalEnum.INTERVAL_3d,
|
|
163
|
+
"1w": UiKlinesIntervalEnum.INTERVAL_1w,
|
|
164
|
+
"1M": UiKlinesIntervalEnum.INTERVAL_1M,
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if interval not in interval_map:
|
|
168
|
+
logging.error(f"不支持的间隔: {interval}")
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
interval_enum = interval_map[interval]
|
|
172
|
+
|
|
173
|
+
# 转换时间参数为毫秒时间戳
|
|
174
|
+
start_time_ms = _convert_to_timestamp_ms(start_time) if start_time is not None else None
|
|
175
|
+
end_time_ms = _convert_to_timestamp_ms(end_time) if end_time is not None else None
|
|
176
|
+
|
|
177
|
+
# 构建查询日志信息
|
|
178
|
+
time_info = []
|
|
179
|
+
if start_time_ms:
|
|
180
|
+
start_str = datetime.fromtimestamp(start_time_ms / 1000).strftime('%Y-%m-%d %H:%M:%S')
|
|
181
|
+
time_info.append(f"开始时间: {start_str}")
|
|
182
|
+
if end_time_ms:
|
|
183
|
+
end_str = datetime.fromtimestamp(end_time_ms / 1000).strftime('%Y-%m-%d %H:%M:%S')
|
|
184
|
+
time_info.append(f"结束时间: {end_str}")
|
|
185
|
+
time_info_str = ", ".join(time_info) if time_info else "最近数据"
|
|
186
|
+
|
|
187
|
+
# 如果同时指定了 start_time 和 end_time,进行分页请求
|
|
188
|
+
all_klines_data = []
|
|
189
|
+
if start_time_ms is not None and end_time_ms is not None:
|
|
190
|
+
logging.info(f"检测到时间范围,将自动分页获取数据: {symbol}, 间隔: {interval}, {time_info_str}")
|
|
191
|
+
|
|
192
|
+
current_start_time = start_time_ms
|
|
193
|
+
request_count = 0
|
|
194
|
+
max_requests = 1000 # 防止无限循环
|
|
195
|
+
|
|
196
|
+
while current_start_time < end_time_ms and request_count < max_requests:
|
|
197
|
+
request_count += 1
|
|
198
|
+
# 每次请求最多 1000 条
|
|
199
|
+
current_limit = 1000
|
|
200
|
+
|
|
201
|
+
logging.info(f"第 {request_count} 次请求: 从 {datetime.fromtimestamp(current_start_time / 1000).strftime('%Y-%m-%d %H:%M:%S')} 开始")
|
|
202
|
+
|
|
203
|
+
# 重试机制
|
|
204
|
+
max_retries = 3
|
|
205
|
+
retry_count = 0
|
|
206
|
+
batch_data = None
|
|
207
|
+
|
|
208
|
+
while retry_count < max_retries:
|
|
209
|
+
try:
|
|
210
|
+
response = client.rest_api.ui_klines(
|
|
211
|
+
symbol=symbol,
|
|
212
|
+
interval=interval_enum,
|
|
213
|
+
start_time=current_start_time,
|
|
214
|
+
end_time=end_time_ms,
|
|
215
|
+
limit=current_limit
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
batch_data = response.data()
|
|
219
|
+
break # 成功获取数据,退出重试循环
|
|
220
|
+
|
|
221
|
+
except Exception as e:
|
|
222
|
+
retry_count += 1
|
|
223
|
+
if retry_count < max_retries:
|
|
224
|
+
wait_time = retry_count * 2 # 递增等待时间:2秒、4秒
|
|
225
|
+
logging.warning(f"第 {request_count} 次请求失败(重试 {retry_count}/{max_retries}): {e},等待 {wait_time} 秒后重试...")
|
|
226
|
+
time.sleep(wait_time)
|
|
227
|
+
else:
|
|
228
|
+
logging.error(f"第 {request_count} 次请求失败,已重试 {max_retries} 次: {e}")
|
|
229
|
+
break
|
|
230
|
+
|
|
231
|
+
if batch_data is None:
|
|
232
|
+
logging.error(f"第 {request_count} 次请求最终失败,停止分页请求")
|
|
233
|
+
break
|
|
234
|
+
|
|
235
|
+
if not batch_data:
|
|
236
|
+
logging.info("本次请求未获取到数据,可能已到达结束时间")
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
# 过滤掉超过 end_time 的数据
|
|
240
|
+
filtered_data = []
|
|
241
|
+
for kline in batch_data:
|
|
242
|
+
close_time = int(kline[6]) if isinstance(kline[6], str) else kline[6]
|
|
243
|
+
if close_time <= end_time_ms:
|
|
244
|
+
filtered_data.append(kline)
|
|
245
|
+
else:
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
if not filtered_data:
|
|
249
|
+
logging.info("过滤后无有效数据,已到达结束时间")
|
|
250
|
+
break
|
|
251
|
+
|
|
252
|
+
all_klines_data.extend(filtered_data)
|
|
253
|
+
logging.info(f"本次获取 {len(filtered_data)} 条数据,累计 {len(all_klines_data)} 条")
|
|
254
|
+
|
|
255
|
+
# 获取最后一条数据的 close_time,作为下一次请求的 start_time
|
|
256
|
+
last_kline = filtered_data[-1]
|
|
257
|
+
last_close_time = int(last_kline[6]) if isinstance(last_kline[6], str) else last_kline[6]
|
|
258
|
+
|
|
259
|
+
# 如果返回的数据少于 limit,说明已经到达结束时间或没有更多数据
|
|
260
|
+
if len(filtered_data) < current_limit:
|
|
261
|
+
logging.info("返回数据少于限制数量,已获取完所有数据")
|
|
262
|
+
break
|
|
263
|
+
|
|
264
|
+
# 如果最后一条数据的 close_time 已经达到或超过 end_time,停止请求
|
|
265
|
+
if last_close_time >= end_time_ms:
|
|
266
|
+
logging.info("已到达结束时间")
|
|
267
|
+
break
|
|
268
|
+
|
|
269
|
+
# 下一次请求从最后一条数据的 close_time + 1 开始(避免重复)
|
|
270
|
+
current_start_time = last_close_time + 1
|
|
271
|
+
|
|
272
|
+
# 添加短暂延迟,避免请求过快
|
|
273
|
+
time.sleep(0.5)
|
|
274
|
+
|
|
275
|
+
if request_count >= max_requests:
|
|
276
|
+
logging.warning(f"达到最大请求次数限制 ({max_requests}),停止请求")
|
|
277
|
+
|
|
278
|
+
klines_data = all_klines_data
|
|
279
|
+
|
|
280
|
+
if not klines_data:
|
|
281
|
+
logging.warning("未获取到任何数据")
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
logging.info(f"分页请求完成,共请求 {request_count} 次,总计获取 {len(klines_data)} 条数据")
|
|
285
|
+
else:
|
|
286
|
+
# 单次请求模式(原有逻辑)
|
|
287
|
+
logging.info(f"正在查询 {symbol} 的行情数据,间隔: {interval}, 数量: {limit}, {time_info_str}")
|
|
288
|
+
response = client.rest_api.ui_klines(
|
|
289
|
+
symbol=symbol,
|
|
290
|
+
interval=interval_enum,
|
|
291
|
+
start_time=start_time_ms,
|
|
292
|
+
end_time=end_time_ms,
|
|
293
|
+
limit=limit
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
# 获取数据
|
|
297
|
+
klines_data = response.data()
|
|
298
|
+
|
|
299
|
+
if not klines_data:
|
|
300
|
+
logging.warning("未获取到数据")
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
logging.info(f"成功获取 {len(klines_data)} 条数据")
|
|
304
|
+
|
|
305
|
+
# 创建输出目录
|
|
306
|
+
if not os.path.exists(output_dir):
|
|
307
|
+
os.makedirs(output_dir)
|
|
308
|
+
logging.info(f"创建目录: {output_dir}")
|
|
309
|
+
|
|
310
|
+
# 生成文件名(包含时间戳和时间范围)
|
|
311
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
312
|
+
time_range_str = ""
|
|
313
|
+
if start_time_ms or end_time_ms:
|
|
314
|
+
if start_time_ms:
|
|
315
|
+
start_str = datetime.fromtimestamp(start_time_ms / 1000).strftime("%Y%m%d_%H%M%S")
|
|
316
|
+
time_range_str += f"_{start_str}"
|
|
317
|
+
if end_time_ms:
|
|
318
|
+
end_str = datetime.fromtimestamp(end_time_ms / 1000).strftime("%Y%m%d_%H%M%S")
|
|
319
|
+
time_range_str += f"_{end_str}"
|
|
320
|
+
# 如果进行了分页请求,在文件名中显示实际数据条数而不是 limit
|
|
321
|
+
data_count = len(klines_data)
|
|
322
|
+
base_filename = f"{symbol}_{interval}_{data_count}{time_range_str}_{timestamp}"
|
|
323
|
+
|
|
324
|
+
# 保存为 CSV
|
|
325
|
+
if save_csv:
|
|
326
|
+
csv_filename = os.path.join(output_dir, f"{base_filename}.csv")
|
|
327
|
+
with open(csv_filename, 'w', newline='', encoding='utf-8') as csvfile:
|
|
328
|
+
writer = csv.writer(csvfile)
|
|
329
|
+
|
|
330
|
+
# 写入表头
|
|
331
|
+
writer.writerow([
|
|
332
|
+
'开盘时间', '开盘价', '最高价', '最低价', '收盘价',
|
|
333
|
+
'成交量', '收盘时间', '成交额', '成交笔数',
|
|
334
|
+
'主动买入成交量', '主动买入成交额', '忽略'
|
|
335
|
+
])
|
|
336
|
+
|
|
337
|
+
# 写入数据
|
|
338
|
+
for kline in klines_data:
|
|
339
|
+
# 处理时间戳(可能是int或str)
|
|
340
|
+
open_time = int(kline[0]) if isinstance(kline[0], str) else kline[0]
|
|
341
|
+
close_time = int(kline[6]) if isinstance(kline[6], str) else kline[6]
|
|
342
|
+
|
|
343
|
+
# 转换时间戳为可读格式
|
|
344
|
+
row = list(kline)
|
|
345
|
+
row[0] = datetime.fromtimestamp(open_time / 1000).strftime('%Y-%m-%d %H:%M:%S')
|
|
346
|
+
row[6] = datetime.fromtimestamp(close_time / 1000).strftime('%Y-%m-%d %H:%M:%S')
|
|
347
|
+
writer.writerow(row)
|
|
348
|
+
|
|
349
|
+
logging.info(f"数据已保存为 CSV: {csv_filename}")
|
|
350
|
+
|
|
351
|
+
# 保存为 JSON
|
|
352
|
+
if save_json:
|
|
353
|
+
json_filename = os.path.join(output_dir, f"{base_filename}.json")
|
|
354
|
+
|
|
355
|
+
# 格式化数据以便阅读
|
|
356
|
+
formatted_data = []
|
|
357
|
+
for kline in klines_data:
|
|
358
|
+
# 处理时间戳(可能是int或str)
|
|
359
|
+
open_time = int(kline[0]) if isinstance(kline[0], str) else kline[0]
|
|
360
|
+
close_time = int(kline[6]) if isinstance(kline[6], str) else kline[6]
|
|
361
|
+
|
|
362
|
+
formatted_data.append({
|
|
363
|
+
'open_time': open_time,
|
|
364
|
+
'open_time_str': datetime.fromtimestamp(open_time / 1000).strftime('%Y-%m-%d %H:%M:%S'),
|
|
365
|
+
'open_price': float(kline[1]),
|
|
366
|
+
'high_price': float(kline[2]),
|
|
367
|
+
'low_price': float(kline[3]),
|
|
368
|
+
'close_price': float(kline[4]),
|
|
369
|
+
'volume': float(kline[5]),
|
|
370
|
+
'close_time': close_time,
|
|
371
|
+
'close_time_str': datetime.fromtimestamp(close_time / 1000).strftime('%Y-%m-%d %H:%M:%S'),
|
|
372
|
+
'quote_volume': float(kline[7]),
|
|
373
|
+
'trades': int(kline[8]),
|
|
374
|
+
'taker_buy_base_volume': float(kline[9]),
|
|
375
|
+
'taker_buy_quote_volume': float(kline[10]),
|
|
376
|
+
'ignore': kline[11]
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
# 构建元数据
|
|
380
|
+
metadata = {
|
|
381
|
+
'symbol': symbol,
|
|
382
|
+
'interval': interval,
|
|
383
|
+
'request_limit': limit, # 原始请求的 limit
|
|
384
|
+
'data_count': len(formatted_data),
|
|
385
|
+
'timestamp': timestamp,
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
# 添加时间范围信息
|
|
389
|
+
if start_time_ms:
|
|
390
|
+
metadata['start_time'] = start_time_ms
|
|
391
|
+
metadata['start_time_str'] = datetime.fromtimestamp(start_time_ms / 1000).strftime('%Y-%m-%d %H:%M:%S')
|
|
392
|
+
if end_time_ms:
|
|
393
|
+
metadata['end_time'] = end_time_ms
|
|
394
|
+
metadata['end_time_str'] = datetime.fromtimestamp(end_time_ms / 1000).strftime('%Y-%m-%d %H:%M:%S')
|
|
395
|
+
|
|
396
|
+
# 如果进行了分页请求,添加相关信息
|
|
397
|
+
if start_time_ms is not None and end_time_ms is not None:
|
|
398
|
+
metadata['pagination_used'] = True
|
|
399
|
+
metadata['note'] = '数据通过分页请求获取,每次请求最多1000条'
|
|
400
|
+
|
|
401
|
+
metadata['data'] = formatted_data
|
|
402
|
+
|
|
403
|
+
with open(json_filename, 'w', encoding='utf-8') as jsonfile:
|
|
404
|
+
json.dump(metadata, jsonfile, indent=2, ensure_ascii=False)
|
|
405
|
+
|
|
406
|
+
logging.info(f"数据已保存为 JSON: {json_filename}")
|
|
407
|
+
|
|
408
|
+
return klines_data
|
|
409
|
+
|
|
410
|
+
except Exception as e:
|
|
411
|
+
logging.error(f"查询或保存数据时出错: {e}")
|
|
412
|
+
import traceback
|
|
413
|
+
logging.error(traceback.format_exc())
|
|
414
|
+
return None
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def get_and_save_klines_direct(
|
|
418
|
+
symbol: str,
|
|
419
|
+
interval: str = "1d",
|
|
420
|
+
limit: int = 30,
|
|
421
|
+
start_time: Optional[Union[datetime, str, int]] = None,
|
|
422
|
+
end_time: Optional[Union[datetime, str, int]] = None,
|
|
423
|
+
output_dir: str = "data",
|
|
424
|
+
save_csv: bool = False,
|
|
425
|
+
save_json: bool = True,
|
|
426
|
+
base_url: str = "https://www.binance.com/api/v3/uiKlines"
|
|
427
|
+
) -> Optional[list]:
|
|
428
|
+
"""
|
|
429
|
+
直接使用HTTP请求查询并保存 Binance 行情数据(避免数据量限制)
|
|
430
|
+
|
|
431
|
+
使用直接的HTTP请求调用 uiKlines 接口,可以避免SDK的数据量限制
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
symbol: 交易对符号,例如 'BTCUSDT', 'ETHUSDT'
|
|
435
|
+
interval: 时间间隔,例如 '1d' (1天), '1h' (1小时), '1m' (1分钟)
|
|
436
|
+
可选值: 1s, 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M
|
|
437
|
+
limit: 返回的数据条数,默认30,最大1000
|
|
438
|
+
start_time: 开始时间,可以是:
|
|
439
|
+
- datetime 对象
|
|
440
|
+
- 字符串格式,例如 '2023-01-01 00:00:00' 或 '2023-01-01'
|
|
441
|
+
- 整数时间戳(秒或毫秒,自动判断)
|
|
442
|
+
- None(不指定开始时间)
|
|
443
|
+
end_time: 结束时间,格式同 start_time
|
|
444
|
+
- None(不指定结束时间)
|
|
445
|
+
output_dir: 保存数据的目录,默认 'data'
|
|
446
|
+
save_csv: 是否保存为 CSV 格式,默认 True
|
|
447
|
+
save_json: 是否保存为 JSON 格式,默认 True
|
|
448
|
+
base_url: API基础URL,默认 'https://www.binance.com/api/v3/uiKlines'
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
返回查询到的K线数据列表,如果出错返回 None
|
|
452
|
+
|
|
453
|
+
Note:
|
|
454
|
+
- 如果 start_time 和 end_time 都不指定,返回最近的 limit 条数据
|
|
455
|
+
- 如果只指定 start_time,返回从 start_time 开始的 limit 条数据
|
|
456
|
+
- 如果只指定 end_time,返回 end_time 之前的 limit 条数据
|
|
457
|
+
- 如果同时指定 start_time 和 end_time,会自动进行分页请求(每次最多1000条),
|
|
458
|
+
获取整个时间范围内的所有数据,不受 limit 参数限制
|
|
459
|
+
"""
|
|
460
|
+
try:
|
|
461
|
+
# 转换时间参数为毫秒时间戳
|
|
462
|
+
start_time_ms = _convert_to_timestamp_ms(start_time) if start_time is not None else None
|
|
463
|
+
end_time_ms = _convert_to_timestamp_ms(end_time) if end_time is not None else None
|
|
464
|
+
|
|
465
|
+
# 构建查询日志信息
|
|
466
|
+
time_info = []
|
|
467
|
+
if start_time_ms:
|
|
468
|
+
start_str = datetime.fromtimestamp(start_time_ms / 1000).strftime('%Y-%m-%d %H:%M:%S')
|
|
469
|
+
time_info.append(f"开始时间: {start_str}")
|
|
470
|
+
if end_time_ms:
|
|
471
|
+
end_str = datetime.fromtimestamp(end_time_ms / 1000).strftime('%Y-%m-%d %H:%M:%S')
|
|
472
|
+
time_info.append(f"结束时间: {end_str}")
|
|
473
|
+
time_info_str = ", ".join(time_info) if time_info else "最近数据"
|
|
474
|
+
|
|
475
|
+
# 如果同时指定了 start_time 和 end_time,进行分页请求
|
|
476
|
+
all_klines_data = []
|
|
477
|
+
if start_time_ms is not None and end_time_ms is not None:
|
|
478
|
+
logging.info(f"检测到时间范围,将自动分页获取数据: {symbol}, 间隔: {interval}, {time_info_str}")
|
|
479
|
+
|
|
480
|
+
current_start_time = start_time_ms
|
|
481
|
+
request_count = 0
|
|
482
|
+
max_requests = 1000 # 防止无限循环
|
|
483
|
+
|
|
484
|
+
while current_start_time < end_time_ms and request_count < max_requests:
|
|
485
|
+
request_count += 1
|
|
486
|
+
# 每次请求最多 1000 条
|
|
487
|
+
current_limit = 1000
|
|
488
|
+
|
|
489
|
+
logging.info(f"第 {request_count} 次请求: 从 {datetime.fromtimestamp(current_start_time / 1000).strftime('%Y-%m-%d %H:%M:%S')} 开始")
|
|
490
|
+
|
|
491
|
+
# 重试机制
|
|
492
|
+
max_retries = 3
|
|
493
|
+
retry_count = 0
|
|
494
|
+
batch_data = None
|
|
495
|
+
|
|
496
|
+
while retry_count < max_retries:
|
|
497
|
+
try:
|
|
498
|
+
# 构建请求参数
|
|
499
|
+
params = {
|
|
500
|
+
'symbol': symbol,
|
|
501
|
+
'interval': interval,
|
|
502
|
+
'startTime': current_start_time,
|
|
503
|
+
'endTime': end_time_ms,
|
|
504
|
+
'limit': current_limit
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
# 增加超时时间到60秒,并设置连接和读取超时
|
|
508
|
+
response = requests.get(base_url, params=params, timeout=(30, 60))
|
|
509
|
+
response.raise_for_status()
|
|
510
|
+
|
|
511
|
+
batch_data = response.json()
|
|
512
|
+
break # 成功获取数据,退出重试循环
|
|
513
|
+
|
|
514
|
+
except requests.exceptions.Timeout as e:
|
|
515
|
+
retry_count += 1
|
|
516
|
+
if retry_count < max_retries:
|
|
517
|
+
wait_time = retry_count * 2 # 递增等待时间:2秒、4秒
|
|
518
|
+
logging.warning(f"第 {request_count} 次请求超时(重试 {retry_count}/{max_retries}): {e},等待 {wait_time} 秒后重试...")
|
|
519
|
+
time.sleep(wait_time)
|
|
520
|
+
else:
|
|
521
|
+
logging.error(f"第 {request_count} 次请求超时,已重试 {max_retries} 次: {e}")
|
|
522
|
+
break
|
|
523
|
+
except requests.exceptions.RequestException as e:
|
|
524
|
+
retry_count += 1
|
|
525
|
+
if retry_count < max_retries:
|
|
526
|
+
wait_time = retry_count * 2 # 递增等待时间:2秒、4秒
|
|
527
|
+
logging.warning(f"第 {request_count} 次请求失败(重试 {retry_count}/{max_retries}): {e},等待 {wait_time} 秒后重试...")
|
|
528
|
+
time.sleep(wait_time)
|
|
529
|
+
else:
|
|
530
|
+
logging.error(f"第 {request_count} 次请求失败,已重试 {max_retries} 次: {e}")
|
|
531
|
+
break
|
|
532
|
+
except Exception as e:
|
|
533
|
+
retry_count += 1
|
|
534
|
+
if retry_count < max_retries:
|
|
535
|
+
wait_time = retry_count * 2 # 递增等待时间:2秒、4秒
|
|
536
|
+
logging.warning(f"第 {request_count} 次请求出错(重试 {retry_count}/{max_retries}): {e},等待 {wait_time} 秒后重试...")
|
|
537
|
+
time.sleep(wait_time)
|
|
538
|
+
else:
|
|
539
|
+
logging.error(f"第 {request_count} 次请求出错,已重试 {max_retries} 次: {e}")
|
|
540
|
+
break
|
|
541
|
+
|
|
542
|
+
if batch_data is None:
|
|
543
|
+
logging.error(f"第 {request_count} 次请求最终失败,停止分页请求")
|
|
544
|
+
break
|
|
545
|
+
|
|
546
|
+
if not batch_data:
|
|
547
|
+
logging.info("本次请求未获取到数据,可能已到达结束时间")
|
|
548
|
+
break
|
|
549
|
+
|
|
550
|
+
# 过滤掉超过 end_time 的数据
|
|
551
|
+
filtered_data = []
|
|
552
|
+
for kline in batch_data:
|
|
553
|
+
close_time = int(kline[6]) if isinstance(kline[6], str) else kline[6]
|
|
554
|
+
if close_time <= end_time_ms:
|
|
555
|
+
filtered_data.append(kline)
|
|
556
|
+
else:
|
|
557
|
+
break
|
|
558
|
+
|
|
559
|
+
if not filtered_data:
|
|
560
|
+
logging.info("过滤后无有效数据,已到达结束时间")
|
|
561
|
+
break
|
|
562
|
+
|
|
563
|
+
all_klines_data.extend(filtered_data)
|
|
564
|
+
logging.info(f"本次获取 {len(filtered_data)} 条数据,累计 {len(all_klines_data)} 条")
|
|
565
|
+
|
|
566
|
+
# 获取最后一条数据的 close_time,作为下一次请求的 start_time
|
|
567
|
+
last_kline = filtered_data[-1]
|
|
568
|
+
last_close_time = int(last_kline[6]) if isinstance(last_kline[6], str) else last_kline[6]
|
|
569
|
+
|
|
570
|
+
# 如果返回的数据少于 limit,说明已经到达结束时间或没有更多数据
|
|
571
|
+
if len(filtered_data) < current_limit:
|
|
572
|
+
logging.info("返回数据少于限制数量,已获取完所有数据")
|
|
573
|
+
break
|
|
574
|
+
|
|
575
|
+
# 如果最后一条数据的 close_time 已经达到或超过 end_time,停止请求
|
|
576
|
+
if last_close_time >= end_time_ms:
|
|
577
|
+
logging.info("已到达结束时间")
|
|
578
|
+
break
|
|
579
|
+
|
|
580
|
+
# 下一次请求从最后一条数据的 close_time + 1 开始(避免重复)
|
|
581
|
+
current_start_time = last_close_time + 1
|
|
582
|
+
|
|
583
|
+
# 添加短暂延迟,避免请求过快
|
|
584
|
+
time.sleep(0.5)
|
|
585
|
+
|
|
586
|
+
if request_count >= max_requests:
|
|
587
|
+
logging.warning(f"达到最大请求次数限制 ({max_requests}),停止请求")
|
|
588
|
+
|
|
589
|
+
klines_data = all_klines_data
|
|
590
|
+
|
|
591
|
+
if not klines_data:
|
|
592
|
+
logging.warning("未获取到任何数据")
|
|
593
|
+
return None
|
|
594
|
+
|
|
595
|
+
logging.info(f"分页请求完成,共请求 {request_count} 次,总计获取 {len(klines_data)} 条数据")
|
|
596
|
+
else:
|
|
597
|
+
# 单次请求模式(原有逻辑)
|
|
598
|
+
# 构建请求参数
|
|
599
|
+
params = {
|
|
600
|
+
'symbol': symbol,
|
|
601
|
+
'interval': interval,
|
|
602
|
+
'limit': limit
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if start_time_ms is not None:
|
|
606
|
+
params['startTime'] = start_time_ms
|
|
607
|
+
if end_time_ms is not None:
|
|
608
|
+
params['endTime'] = end_time_ms
|
|
609
|
+
|
|
610
|
+
# 查询数据
|
|
611
|
+
logging.info(f"正在通过HTTP请求查询 {symbol} 的行情数据,间隔: {interval}, 数量: {limit}, {time_info_str}")
|
|
612
|
+
# 增加超时时间:连接超时30秒,读取超时60秒
|
|
613
|
+
response = requests.get(base_url, params=params, timeout=(30, 60))
|
|
614
|
+
|
|
615
|
+
# 检查响应状态
|
|
616
|
+
response.raise_for_status()
|
|
617
|
+
|
|
618
|
+
# 解析JSON数据
|
|
619
|
+
klines_data = response.json()
|
|
620
|
+
|
|
621
|
+
if not klines_data:
|
|
622
|
+
logging.warning("未获取到数据")
|
|
623
|
+
return None
|
|
624
|
+
|
|
625
|
+
logging.info(f"成功获取 {len(klines_data)} 条数据")
|
|
626
|
+
|
|
627
|
+
# 创建输出目录
|
|
628
|
+
if not os.path.exists(output_dir):
|
|
629
|
+
os.makedirs(output_dir)
|
|
630
|
+
logging.info(f"创建目录: {output_dir}")
|
|
631
|
+
|
|
632
|
+
# 生成文件名(包含时间戳和时间范围)
|
|
633
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
634
|
+
time_range_str = ""
|
|
635
|
+
if start_time_ms or end_time_ms:
|
|
636
|
+
if start_time_ms:
|
|
637
|
+
start_str = datetime.fromtimestamp(start_time_ms / 1000).strftime("%Y%m%d_%H%M%S")
|
|
638
|
+
time_range_str += f"_{start_str}"
|
|
639
|
+
if end_time_ms:
|
|
640
|
+
end_str = datetime.fromtimestamp(end_time_ms / 1000).strftime("%Y%m%d_%H%M%S")
|
|
641
|
+
time_range_str += f"_{end_str}"
|
|
642
|
+
# 如果进行了分页请求,在文件名中显示实际数据条数而不是 limit
|
|
643
|
+
data_count = len(klines_data)
|
|
644
|
+
base_filename = f"{symbol}_{interval}_{data_count}{time_range_str}_{timestamp}"
|
|
645
|
+
|
|
646
|
+
# 保存为 CSV
|
|
647
|
+
if save_csv:
|
|
648
|
+
csv_filename = os.path.join(output_dir, f"{base_filename}.csv")
|
|
649
|
+
with open(csv_filename, 'w', newline='', encoding='utf-8') as csvfile:
|
|
650
|
+
writer = csv.writer(csvfile)
|
|
651
|
+
|
|
652
|
+
# 写入表头
|
|
653
|
+
writer.writerow([
|
|
654
|
+
'开盘时间', '开盘价', '最高价', '最低价', '收盘价',
|
|
655
|
+
'成交量', '收盘时间', '成交额', '成交笔数',
|
|
656
|
+
'主动买入成交量', '主动买入成交额', '忽略'
|
|
657
|
+
])
|
|
658
|
+
|
|
659
|
+
# 写入数据
|
|
660
|
+
for kline in klines_data:
|
|
661
|
+
# 处理时间戳(可能是int或str)
|
|
662
|
+
open_time = int(kline[0]) if isinstance(kline[0], str) else kline[0]
|
|
663
|
+
close_time = int(kline[6]) if isinstance(kline[6], str) else kline[6]
|
|
664
|
+
|
|
665
|
+
# 转换时间戳为可读格式
|
|
666
|
+
row = list(kline)
|
|
667
|
+
row[0] = datetime.fromtimestamp(open_time / 1000).strftime('%Y-%m-%d %H:%M:%S')
|
|
668
|
+
row[6] = datetime.fromtimestamp(close_time / 1000).strftime('%Y-%m-%d %H:%M:%S')
|
|
669
|
+
writer.writerow(row)
|
|
670
|
+
|
|
671
|
+
logging.info(f"数据已保存为 CSV: {csv_filename}")
|
|
672
|
+
|
|
673
|
+
# 保存为 JSON
|
|
674
|
+
if save_json:
|
|
675
|
+
json_filename = os.path.join(output_dir, f"{base_filename}.json")
|
|
676
|
+
|
|
677
|
+
# 格式化数据以便阅读
|
|
678
|
+
formatted_data = []
|
|
679
|
+
for kline in klines_data:
|
|
680
|
+
# 处理时间戳(可能是int或str)
|
|
681
|
+
open_time = int(kline[0]) if isinstance(kline[0], str) else kline[0]
|
|
682
|
+
close_time = int(kline[6]) if isinstance(kline[6], str) else kline[6]
|
|
683
|
+
|
|
684
|
+
formatted_data.append({
|
|
685
|
+
'open_time': open_time,
|
|
686
|
+
'open_time_str': datetime.fromtimestamp(open_time / 1000).strftime('%Y-%m-%d %H:%M:%S'),
|
|
687
|
+
'open_price': float(kline[1]),
|
|
688
|
+
'high_price': float(kline[2]),
|
|
689
|
+
'low_price': float(kline[3]),
|
|
690
|
+
'close_price': float(kline[4]),
|
|
691
|
+
'volume': float(kline[5]),
|
|
692
|
+
'close_time': close_time,
|
|
693
|
+
'close_time_str': datetime.fromtimestamp(close_time / 1000).strftime('%Y-%m-%d %H:%M:%S'),
|
|
694
|
+
'quote_volume': float(kline[7]),
|
|
695
|
+
'trades': int(kline[8]),
|
|
696
|
+
'taker_buy_base_volume': float(kline[9]),
|
|
697
|
+
'taker_buy_quote_volume': float(kline[10]),
|
|
698
|
+
'ignore': str(kline[11]) if kline[11] is not None else "0"
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
# 构建元数据
|
|
702
|
+
metadata = {
|
|
703
|
+
'symbol': symbol,
|
|
704
|
+
'interval': interval,
|
|
705
|
+
'request_limit': limit, # 原始请求的 limit
|
|
706
|
+
'data_count': len(formatted_data),
|
|
707
|
+
'timestamp': timestamp,
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
# 添加时间范围信息
|
|
711
|
+
if start_time_ms:
|
|
712
|
+
metadata['start_time'] = start_time_ms
|
|
713
|
+
metadata['start_time_str'] = datetime.fromtimestamp(start_time_ms / 1000).strftime('%Y-%m-%d %H:%M:%S')
|
|
714
|
+
if end_time_ms:
|
|
715
|
+
metadata['end_time'] = end_time_ms
|
|
716
|
+
metadata['end_time_str'] = datetime.fromtimestamp(end_time_ms / 1000).strftime('%Y-%m-%d %H:%M:%S')
|
|
717
|
+
|
|
718
|
+
# 如果进行了分页请求,添加相关信息
|
|
719
|
+
if start_time_ms is not None and end_time_ms is not None:
|
|
720
|
+
metadata['pagination_used'] = True
|
|
721
|
+
metadata['note'] = '数据通过分页请求获取,每次请求最多1000条'
|
|
722
|
+
|
|
723
|
+
metadata['data'] = formatted_data
|
|
724
|
+
|
|
725
|
+
with open(json_filename, 'w', encoding='utf-8') as jsonfile:
|
|
726
|
+
json.dump(metadata, jsonfile, indent=2, ensure_ascii=False)
|
|
727
|
+
|
|
728
|
+
logging.info(f"数据已保存为 JSON: {json_filename}")
|
|
729
|
+
|
|
730
|
+
return klines_data
|
|
731
|
+
|
|
732
|
+
except requests.exceptions.RequestException as e:
|
|
733
|
+
logging.error(f"HTTP请求出错: {e}")
|
|
734
|
+
return None
|
|
735
|
+
except Exception as e:
|
|
736
|
+
logging.error(f"查询或保存数据时出错: {e}")
|
|
737
|
+
import traceback
|
|
738
|
+
logging.error(traceback.format_exc())
|
|
739
|
+
return None
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
if __name__ == "__main__":
|
|
743
|
+
# 示例用法 - 使用SDK方法
|
|
744
|
+
# 查询 BTCUSDT 最近30天的日线数据
|
|
745
|
+
# symbol = "BTCUSDT"
|
|
746
|
+
# get_and_save_klines(
|
|
747
|
+
# symbol=symbol,
|
|
748
|
+
# interval="1s",
|
|
749
|
+
# limit=10,
|
|
750
|
+
# output_dir=f"/Users/user/Desktop/repo/cyqnt_trd/tmp/data/{symbol}"
|
|
751
|
+
# )
|
|
752
|
+
|
|
753
|
+
# 示例用法 - 使用直接HTTP请求方法(避免数据量限制)
|
|
754
|
+
symbol_list = ['BTCUSDT', 'BNBUSDT', 'DOGEUSDT', 'ETHUSDT', 'SOLUSDT', 'XRPUSDT', 'ZECUSDT']
|
|
755
|
+
for symbol in symbol_list:
|
|
756
|
+
get_and_save_klines_direct(
|
|
757
|
+
symbol=symbol,
|
|
758
|
+
interval="3m",
|
|
759
|
+
start_time='2025-12-15',
|
|
760
|
+
end_time='2025-12-21',
|
|
761
|
+
output_dir=f"/Users/user/Desktop/repo/data_all/tmp/data/{symbol}_current"
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
# 查询 ETHUSDT 最近100天的日线数据
|
|
765
|
+
# get_and_save_klines_direct(
|
|
766
|
+
# symbol="ETHUSDT",
|
|
767
|
+
# interval="1d",
|
|
768
|
+
# limit=100,
|
|
769
|
+
# output_dir="data"
|
|
770
|
+
# )
|
|
771
|
+
|