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.
Files changed (147) hide show
  1. cyqnt_trd/__init__.py +26 -0
  2. cyqnt_trd/backtesting/README.md +264 -0
  3. cyqnt_trd/backtesting/__init__.py +12 -0
  4. cyqnt_trd/backtesting/factor_test.py +332 -0
  5. cyqnt_trd/backtesting/framework.py +311 -0
  6. cyqnt_trd/backtesting/strategy_backtest.py +545 -0
  7. cyqnt_trd/diagnose_api.py +28 -0
  8. cyqnt_trd/get_data/__init__.py +15 -0
  9. cyqnt_trd/get_data/get_futures_data.py +472 -0
  10. cyqnt_trd/get_data/get_trending_data.py +771 -0
  11. cyqnt_trd/online_trading/__init__.py +13 -0
  12. cyqnt_trd/online_trading/realtime_price_tracker.py +1001 -0
  13. cyqnt_trd/test.py +119 -0
  14. cyqnt_trd/test_script/README.md +411 -0
  15. cyqnt_trd/test_script/get_network_info.py +192 -0
  16. cyqnt_trd/test_script/get_symbols_by_volume.py +227 -0
  17. cyqnt_trd/test_script/realtime_price_tracker.py +839 -0
  18. cyqnt_trd/test_script/test_alpha.py +261 -0
  19. cyqnt_trd/test_script/test_kline_data.py +479 -0
  20. cyqnt_trd/test_script/test_order.py +1360 -0
  21. cyqnt_trd/trading_signal/README.md +276 -0
  22. cyqnt_trd/trading_signal/__init__.py +17 -0
  23. cyqnt_trd/trading_signal/example_test_alpha.py +430 -0
  24. cyqnt_trd/trading_signal/example_usage.py +431 -0
  25. cyqnt_trd/trading_signal/factor/__init__.py +18 -0
  26. cyqnt_trd/trading_signal/factor/ma_factor.py +75 -0
  27. cyqnt_trd/trading_signal/factor/rsi_factor.py +56 -0
  28. cyqnt_trd/trading_signal/selected_alpha/__init__.py +158 -0
  29. cyqnt_trd/trading_signal/selected_alpha/alpha1.py +87 -0
  30. cyqnt_trd/trading_signal/selected_alpha/alpha10.py +90 -0
  31. cyqnt_trd/trading_signal/selected_alpha/alpha100.py +74 -0
  32. cyqnt_trd/trading_signal/selected_alpha/alpha101.py +86 -0
  33. cyqnt_trd/trading_signal/selected_alpha/alpha11.py +86 -0
  34. cyqnt_trd/trading_signal/selected_alpha/alpha12.py +86 -0
  35. cyqnt_trd/trading_signal/selected_alpha/alpha13.py +86 -0
  36. cyqnt_trd/trading_signal/selected_alpha/alpha14.py +87 -0
  37. cyqnt_trd/trading_signal/selected_alpha/alpha15.py +87 -0
  38. cyqnt_trd/trading_signal/selected_alpha/alpha16.py +86 -0
  39. cyqnt_trd/trading_signal/selected_alpha/alpha17.py +88 -0
  40. cyqnt_trd/trading_signal/selected_alpha/alpha18.py +88 -0
  41. cyqnt_trd/trading_signal/selected_alpha/alpha19.py +87 -0
  42. cyqnt_trd/trading_signal/selected_alpha/alpha2.py +86 -0
  43. cyqnt_trd/trading_signal/selected_alpha/alpha20.py +88 -0
  44. cyqnt_trd/trading_signal/selected_alpha/alpha21.py +89 -0
  45. cyqnt_trd/trading_signal/selected_alpha/alpha22.py +87 -0
  46. cyqnt_trd/trading_signal/selected_alpha/alpha23.py +88 -0
  47. cyqnt_trd/trading_signal/selected_alpha/alpha24.py +88 -0
  48. cyqnt_trd/trading_signal/selected_alpha/alpha25.py +86 -0
  49. cyqnt_trd/trading_signal/selected_alpha/alpha26.py +87 -0
  50. cyqnt_trd/trading_signal/selected_alpha/alpha27.py +88 -0
  51. cyqnt_trd/trading_signal/selected_alpha/alpha28.py +88 -0
  52. cyqnt_trd/trading_signal/selected_alpha/alpha29.py +87 -0
  53. cyqnt_trd/trading_signal/selected_alpha/alpha3.py +86 -0
  54. cyqnt_trd/trading_signal/selected_alpha/alpha30.py +87 -0
  55. cyqnt_trd/trading_signal/selected_alpha/alpha31.py +90 -0
  56. cyqnt_trd/trading_signal/selected_alpha/alpha32.py +86 -0
  57. cyqnt_trd/trading_signal/selected_alpha/alpha33.py +86 -0
  58. cyqnt_trd/trading_signal/selected_alpha/alpha34.py +87 -0
  59. cyqnt_trd/trading_signal/selected_alpha/alpha35.py +88 -0
  60. cyqnt_trd/trading_signal/selected_alpha/alpha36.py +86 -0
  61. cyqnt_trd/trading_signal/selected_alpha/alpha37.py +86 -0
  62. cyqnt_trd/trading_signal/selected_alpha/alpha38.py +87 -0
  63. cyqnt_trd/trading_signal/selected_alpha/alpha39.py +87 -0
  64. cyqnt_trd/trading_signal/selected_alpha/alpha4.py +86 -0
  65. cyqnt_trd/trading_signal/selected_alpha/alpha40.py +86 -0
  66. cyqnt_trd/trading_signal/selected_alpha/alpha41.py +86 -0
  67. cyqnt_trd/trading_signal/selected_alpha/alpha42.py +86 -0
  68. cyqnt_trd/trading_signal/selected_alpha/alpha43.py +86 -0
  69. cyqnt_trd/trading_signal/selected_alpha/alpha44.py +87 -0
  70. cyqnt_trd/trading_signal/selected_alpha/alpha45.py +88 -0
  71. cyqnt_trd/trading_signal/selected_alpha/alpha46.py +89 -0
  72. cyqnt_trd/trading_signal/selected_alpha/alpha47.py +86 -0
  73. cyqnt_trd/trading_signal/selected_alpha/alpha48.py +74 -0
  74. cyqnt_trd/trading_signal/selected_alpha/alpha49.py +88 -0
  75. cyqnt_trd/trading_signal/selected_alpha/alpha5.py +86 -0
  76. cyqnt_trd/trading_signal/selected_alpha/alpha50.py +86 -0
  77. cyqnt_trd/trading_signal/selected_alpha/alpha51.py +88 -0
  78. cyqnt_trd/trading_signal/selected_alpha/alpha52.py +87 -0
  79. cyqnt_trd/trading_signal/selected_alpha/alpha53.py +86 -0
  80. cyqnt_trd/trading_signal/selected_alpha/alpha54.py +86 -0
  81. cyqnt_trd/trading_signal/selected_alpha/alpha55.py +88 -0
  82. cyqnt_trd/trading_signal/selected_alpha/alpha56.py +86 -0
  83. cyqnt_trd/trading_signal/selected_alpha/alpha57.py +86 -0
  84. cyqnt_trd/trading_signal/selected_alpha/alpha58.py +74 -0
  85. cyqnt_trd/trading_signal/selected_alpha/alpha59.py +74 -0
  86. cyqnt_trd/trading_signal/selected_alpha/alpha6.py +86 -0
  87. cyqnt_trd/trading_signal/selected_alpha/alpha60.py +89 -0
  88. cyqnt_trd/trading_signal/selected_alpha/alpha61.py +88 -0
  89. cyqnt_trd/trading_signal/selected_alpha/alpha62.py +86 -0
  90. cyqnt_trd/trading_signal/selected_alpha/alpha63.py +74 -0
  91. cyqnt_trd/trading_signal/selected_alpha/alpha64.py +86 -0
  92. cyqnt_trd/trading_signal/selected_alpha/alpha65.py +86 -0
  93. cyqnt_trd/trading_signal/selected_alpha/alpha66.py +86 -0
  94. cyqnt_trd/trading_signal/selected_alpha/alpha67.py +74 -0
  95. cyqnt_trd/trading_signal/selected_alpha/alpha68.py +86 -0
  96. cyqnt_trd/trading_signal/selected_alpha/alpha69.py +74 -0
  97. cyqnt_trd/trading_signal/selected_alpha/alpha7.py +88 -0
  98. cyqnt_trd/trading_signal/selected_alpha/alpha70.py +74 -0
  99. cyqnt_trd/trading_signal/selected_alpha/alpha71.py +92 -0
  100. cyqnt_trd/trading_signal/selected_alpha/alpha72.py +86 -0
  101. cyqnt_trd/trading_signal/selected_alpha/alpha73.py +91 -0
  102. cyqnt_trd/trading_signal/selected_alpha/alpha74.py +86 -0
  103. cyqnt_trd/trading_signal/selected_alpha/alpha75.py +86 -0
  104. cyqnt_trd/trading_signal/selected_alpha/alpha76.py +74 -0
  105. cyqnt_trd/trading_signal/selected_alpha/alpha77.py +92 -0
  106. cyqnt_trd/trading_signal/selected_alpha/alpha78.py +86 -0
  107. cyqnt_trd/trading_signal/selected_alpha/alpha79.py +74 -0
  108. cyqnt_trd/trading_signal/selected_alpha/alpha8.py +87 -0
  109. cyqnt_trd/trading_signal/selected_alpha/alpha80.py +74 -0
  110. cyqnt_trd/trading_signal/selected_alpha/alpha81.py +86 -0
  111. cyqnt_trd/trading_signal/selected_alpha/alpha82.py +74 -0
  112. cyqnt_trd/trading_signal/selected_alpha/alpha83.py +86 -0
  113. cyqnt_trd/trading_signal/selected_alpha/alpha84.py +86 -0
  114. cyqnt_trd/trading_signal/selected_alpha/alpha85.py +86 -0
  115. cyqnt_trd/trading_signal/selected_alpha/alpha86.py +86 -0
  116. cyqnt_trd/trading_signal/selected_alpha/alpha87.py +74 -0
  117. cyqnt_trd/trading_signal/selected_alpha/alpha88.py +92 -0
  118. cyqnt_trd/trading_signal/selected_alpha/alpha89.py +74 -0
  119. cyqnt_trd/trading_signal/selected_alpha/alpha9.py +90 -0
  120. cyqnt_trd/trading_signal/selected_alpha/alpha90.py +74 -0
  121. cyqnt_trd/trading_signal/selected_alpha/alpha91.py +74 -0
  122. cyqnt_trd/trading_signal/selected_alpha/alpha92.py +92 -0
  123. cyqnt_trd/trading_signal/selected_alpha/alpha93.py +74 -0
  124. cyqnt_trd/trading_signal/selected_alpha/alpha94.py +86 -0
  125. cyqnt_trd/trading_signal/selected_alpha/alpha95.py +86 -0
  126. cyqnt_trd/trading_signal/selected_alpha/alpha96.py +92 -0
  127. cyqnt_trd/trading_signal/selected_alpha/alpha97.py +74 -0
  128. cyqnt_trd/trading_signal/selected_alpha/alpha98.py +87 -0
  129. cyqnt_trd/trading_signal/selected_alpha/alpha99.py +86 -0
  130. cyqnt_trd/trading_signal/selected_alpha/alpha_utils.py +342 -0
  131. cyqnt_trd/trading_signal/selected_alpha/create_all_alphas.py +279 -0
  132. cyqnt_trd/trading_signal/selected_alpha/generate_alphas.py +133 -0
  133. cyqnt_trd/trading_signal/selected_alpha/test_alpha.py +261 -0
  134. cyqnt_trd/trading_signal/signal/__init__.py +20 -0
  135. cyqnt_trd/trading_signal/signal/factor_based_signal.py +387 -0
  136. cyqnt_trd/trading_signal/signal/ma_signal.py +163 -0
  137. cyqnt_trd/utils/__init__.py +3 -0
  138. cyqnt_trd/utils/set_user.py +33 -0
  139. cyqnt_trd-0.1.2.dist-info/METADATA +148 -0
  140. cyqnt_trd-0.1.2.dist-info/RECORD +147 -0
  141. cyqnt_trd-0.1.2.dist-info/WHEEL +5 -0
  142. cyqnt_trd-0.1.2.dist-info/licenses/LICENSE +21 -0
  143. cyqnt_trd-0.1.2.dist-info/top_level.txt +2 -0
  144. test/real_time_trade.py +746 -0
  145. test/test_example_usage.py +381 -0
  146. test/test_get_data.py +310 -0
  147. 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
+