cyqnt-trd 0.1.2__py3-none-any.whl → 0.1.7__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/CHANGELOG_0.1.7.md +111 -0
- cyqnt_trd/__init__.py +1 -1
- cyqnt_trd/backtesting/factor_test.py +3 -2
- cyqnt_trd/get_data/__init__.py +16 -1
- cyqnt_trd/get_data/get_futures_data.py +3 -3
- cyqnt_trd/get_data/get_kline_with_factor.py +808 -0
- cyqnt_trd/get_data/get_web3_trending_data.py +389 -0
- cyqnt_trd/online_trading/__init__.py +1 -0
- cyqnt_trd/online_trading/realtime_price_tracker.py +142 -2
- cyqnt_trd/trading_signal/example_usage.py +123 -7
- cyqnt_trd/trading_signal/factor/__init__.py +23 -0
- cyqnt_trd/trading_signal/factor/adx_factor.py +116 -0
- cyqnt_trd/trading_signal/factor/ao_factor.py +66 -0
- cyqnt_trd/trading_signal/factor/bbp_factor.py +68 -0
- cyqnt_trd/trading_signal/factor/cci_factor.py +65 -0
- cyqnt_trd/trading_signal/factor/ema_factor.py +102 -0
- cyqnt_trd/trading_signal/factor/macd_factor.py +97 -0
- cyqnt_trd/trading_signal/factor/momentum_factor.py +44 -0
- cyqnt_trd/trading_signal/factor/stochastic_factor.py +76 -0
- cyqnt_trd/trading_signal/factor/stochastic_tsi_factor.py +129 -0
- cyqnt_trd/trading_signal/factor/uo_factor.py +92 -0
- cyqnt_trd/trading_signal/factor/williams_r_factor.py +60 -0
- cyqnt_trd/trading_signal/factor_details.json +107 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha1.py +4 -2
- cyqnt_trd/trading_signal/selected_alpha/alpha15.py +4 -2
- cyqnt_trd/trading_signal/selected_alpha/generate_alphas.py +1 -0
- {cyqnt_trd-0.1.2.dist-info → cyqnt_trd-0.1.7.dist-info}/METADATA +16 -12
- {cyqnt_trd-0.1.2.dist-info → cyqnt_trd-0.1.7.dist-info}/RECORD +34 -23
- {cyqnt_trd-0.1.2.dist-info → cyqnt_trd-0.1.7.dist-info}/WHEEL +1 -1
- test/real_time_trade.py +467 -10
- test/test_now_factor.py +1082 -0
- test/track_k_line_continue.py +372 -0
- cyqnt_trd/test_script/get_symbols_by_volume.py +0 -227
- cyqnt_trd/test_script/test_alpha.py +0 -261
- cyqnt_trd/test_script/test_kline_data.py +0 -479
- test/test_example_usage.py +0 -381
- test/test_get_data.py +0 -310
- test/test_realtime_price_tracker.py +0 -546
- {cyqnt_trd-0.1.2.dist-info → cyqnt_trd-0.1.7.dist-info}/licenses/LICENSE +0 -0
- {cyqnt_trd-0.1.2.dist-info → cyqnt_trd-0.1.7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,808 @@
|
|
|
1
|
+
"""
|
|
2
|
+
K 线 + 因子获取(单点 / 以某点为 end 向前取 n 点)
|
|
3
|
+
|
|
4
|
+
参考:model-training/trading_report_signal_ranking_v1/get_klines_data_with_factor_dire.py
|
|
5
|
+
公用方法 get_klines_with_factors(klines_data):输入原始 klines_data,输出带因子的 klines 列表。
|
|
6
|
+
对外接口:
|
|
7
|
+
- get_kline_with_factor_at_time:输出当前时间点的单条带因子数据
|
|
8
|
+
- get_kline_with_factor_n_points:输出当前所有时间点的带因子数据
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import pandas as pd
|
|
19
|
+
|
|
20
|
+
logging.basicConfig(
|
|
21
|
+
level=logging.INFO,
|
|
22
|
+
format="%(asctime)s - %(levelname)s - %(message)s",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# 时间转换(与 get_futures_data 一致)
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
def _convert_to_timestamp_ms(time_input: Union[datetime, str, int, None]) -> Optional[int]:
|
|
30
|
+
if time_input is None:
|
|
31
|
+
return None
|
|
32
|
+
if isinstance(time_input, datetime):
|
|
33
|
+
return int(time_input.timestamp() * 1000)
|
|
34
|
+
if isinstance(time_input, str):
|
|
35
|
+
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"]:
|
|
36
|
+
try:
|
|
37
|
+
dt = datetime.strptime(time_input, fmt)
|
|
38
|
+
return int(dt.timestamp() * 1000)
|
|
39
|
+
except ValueError:
|
|
40
|
+
continue
|
|
41
|
+
raise ValueError(f"无法解析时间字符串: {time_input}")
|
|
42
|
+
if isinstance(time_input, int):
|
|
43
|
+
return time_input if time_input > 1e10 else time_input * 1000
|
|
44
|
+
raise TypeError(f"不支持的时间类型: {type(time_input)}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _get_interval_duration_ms(interval: str) -> int:
|
|
48
|
+
mapping = {
|
|
49
|
+
"1m": 60 * 1000,
|
|
50
|
+
"3m": 3 * 60 * 1000,
|
|
51
|
+
"5m": 5 * 60 * 1000,
|
|
52
|
+
"15m": 15 * 60 * 1000,
|
|
53
|
+
"30m": 30 * 60 * 1000,
|
|
54
|
+
"1h": 60 * 60 * 1000,
|
|
55
|
+
"2h": 2 * 60 * 60 * 1000,
|
|
56
|
+
"4h": 4 * 60 * 60 * 1000,
|
|
57
|
+
"6h": 6 * 60 * 60 * 1000,
|
|
58
|
+
"8h": 8 * 60 * 60 * 1000,
|
|
59
|
+
"12h": 12 * 60 * 60 * 1000,
|
|
60
|
+
"1d": 24 * 60 * 60 * 1000,
|
|
61
|
+
"3d": 3 * 24 * 60 * 60 * 1000,
|
|
62
|
+
"1w": 7 * 24 * 60 * 60 * 1000,
|
|
63
|
+
"1M": 30 * 24 * 60 * 60 * 1000,
|
|
64
|
+
}
|
|
65
|
+
return mapping.get(interval, 60 * 1000)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# 因子计算(与 get_klines_data_with_factor_dire 一致)
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
def calculate_normalized_alpha_factor(
|
|
73
|
+
data_slice: pd.DataFrame,
|
|
74
|
+
factor_func: Callable,
|
|
75
|
+
factor_name: str,
|
|
76
|
+
min_required: int = 30,
|
|
77
|
+
lookback_periods: int = 30,
|
|
78
|
+
factor_cache: Optional[Dict[tuple, float]] = None,
|
|
79
|
+
current_point_idx: Optional[int] = None,
|
|
80
|
+
**factor_kwargs: Any,
|
|
81
|
+
) -> Optional[Dict[str, Any]]:
|
|
82
|
+
try:
|
|
83
|
+
if len(data_slice) < min_required + 2:
|
|
84
|
+
return None
|
|
85
|
+
available_periods = len(data_slice) - min_required - 1
|
|
86
|
+
if available_periods < 2:
|
|
87
|
+
return None
|
|
88
|
+
actual_lookback = min(lookback_periods, max(2, available_periods))
|
|
89
|
+
factor_values: List[float] = []
|
|
90
|
+
cache_key_base = factor_name if current_point_idx is not None else None
|
|
91
|
+
|
|
92
|
+
for i in range(actual_lookback + 1):
|
|
93
|
+
end_idx = len(data_slice) - i
|
|
94
|
+
point_idx = current_point_idx - i if current_point_idx is not None else None
|
|
95
|
+
if factor_cache is not None and point_idx is not None and cache_key_base:
|
|
96
|
+
cache_key = (point_idx, cache_key_base)
|
|
97
|
+
if cache_key in factor_cache:
|
|
98
|
+
factor_values.append(factor_cache[cache_key])
|
|
99
|
+
continue
|
|
100
|
+
start_idx = max(0, end_idx - min_required - 1)
|
|
101
|
+
if end_idx <= start_idx:
|
|
102
|
+
factor_values.append(0.0)
|
|
103
|
+
continue
|
|
104
|
+
period_slice = data_slice.iloc[start_idx:end_idx]
|
|
105
|
+
try:
|
|
106
|
+
factor_value = factor_func(period_slice, **factor_kwargs)
|
|
107
|
+
if factor_value is not None:
|
|
108
|
+
value = float(factor_value)
|
|
109
|
+
factor_values.append(value)
|
|
110
|
+
if factor_cache is not None and point_idx is not None and cache_key_base:
|
|
111
|
+
factor_cache[(point_idx, cache_key_base)] = value
|
|
112
|
+
else:
|
|
113
|
+
factor_values.append(0.0)
|
|
114
|
+
except Exception:
|
|
115
|
+
factor_values.append(0.0)
|
|
116
|
+
|
|
117
|
+
if len(factor_values) < 2:
|
|
118
|
+
return None
|
|
119
|
+
factor_array = np.array(factor_values)
|
|
120
|
+
factor_mean = factor_array.mean()
|
|
121
|
+
factor_std = factor_array.std()
|
|
122
|
+
if factor_std == 0 or np.isnan(factor_std):
|
|
123
|
+
normalized_factors = np.zeros_like(factor_array)
|
|
124
|
+
else:
|
|
125
|
+
normalized_factors = (factor_array - factor_mean) / factor_std
|
|
126
|
+
current_normalized = float(normalized_factors[0])
|
|
127
|
+
prev_normalized = float(normalized_factors[1]) if len(normalized_factors) > 1 else 0.0
|
|
128
|
+
signal = "看多" if current_normalized > 0 else ("看空" if current_normalized < 0 else "中性")
|
|
129
|
+
return {
|
|
130
|
+
"value": current_normalized,
|
|
131
|
+
"signal": signal,
|
|
132
|
+
"raw_value": float(factor_values[0]) if factor_values else 0.0,
|
|
133
|
+
"prev_normalized": prev_normalized,
|
|
134
|
+
}
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logging.warning("Error calculating normalized %s factor: %s", factor_name, e)
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _get_factor_configs() -> List[tuple]:
|
|
141
|
+
from cyqnt_trd.trading_signal.factor.ma_factor import ma_factor
|
|
142
|
+
from cyqnt_trd.trading_signal.factor.rsi_factor import rsi_factor
|
|
143
|
+
from cyqnt_trd.trading_signal.selected_alpha import (
|
|
144
|
+
alpha1_factor,
|
|
145
|
+
alpha3_factor,
|
|
146
|
+
alpha7_factor,
|
|
147
|
+
alpha9_factor,
|
|
148
|
+
alpha11_factor,
|
|
149
|
+
alpha15_factor,
|
|
150
|
+
alpha17_factor,
|
|
151
|
+
alpha21_factor,
|
|
152
|
+
alpha23_factor,
|
|
153
|
+
alpha25_factor,
|
|
154
|
+
alpha29_factor,
|
|
155
|
+
alpha33_factor,
|
|
156
|
+
alpha34_factor,
|
|
157
|
+
)
|
|
158
|
+
return [
|
|
159
|
+
("ma_factor_5", ma_factor, 6, {"period": 5}, False),
|
|
160
|
+
("rsi_factor_14", rsi_factor, 16, {"period": 14}, False),
|
|
161
|
+
("alpha1", alpha1_factor, 30, {"lookback_days": 5, "stddev_period": 20, "power": 2.0}, True),
|
|
162
|
+
("alpha3", alpha3_factor, 30, {}, True),
|
|
163
|
+
("alpha7", alpha7_factor, 30, {}, True),
|
|
164
|
+
("alpha9", alpha9_factor, 30, {}, True),
|
|
165
|
+
("alpha11", alpha11_factor, 30, {}, True),
|
|
166
|
+
("alpha15", alpha15_factor, 30, {}, True),
|
|
167
|
+
("alpha17", alpha17_factor, 30, {}, True),
|
|
168
|
+
("alpha21", alpha21_factor, 30, {}, True),
|
|
169
|
+
("alpha23", alpha23_factor, 30, {}, True),
|
|
170
|
+
("alpha25", alpha25_factor, 30, {}, True),
|
|
171
|
+
("alpha29", alpha29_factor, 30, {}, True),
|
|
172
|
+
("alpha33", alpha33_factor, 30, {}, True),
|
|
173
|
+
("alpha34", alpha34_factor, 30, {}, True),
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
HISTORY_WINDOW = 100
|
|
178
|
+
# 接口单次请求 K 线数量上限,超过则分多次请求
|
|
179
|
+
MAX_KLINES_PER_REQUEST = 1500
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _compute_point_factors(
|
|
183
|
+
data_df: pd.DataFrame,
|
|
184
|
+
idx: int,
|
|
185
|
+
factor_configs: List[tuple],
|
|
186
|
+
factor_cache: Dict[tuple, float],
|
|
187
|
+
lookback_periods: int = 30,
|
|
188
|
+
) -> Optional[Dict[str, Any]]:
|
|
189
|
+
time_point_data = data_df.iloc[: idx + 1]
|
|
190
|
+
kline_point = data_df.iloc[idx]
|
|
191
|
+
open_time = int(kline_point["open_time"])
|
|
192
|
+
close_time = int(kline_point["close_time"])
|
|
193
|
+
open_time_str = kline_point.get(
|
|
194
|
+
"open_time_str", pd.to_datetime(open_time, unit="ms").strftime("%Y-%m-%d %H:%M:%S")
|
|
195
|
+
)
|
|
196
|
+
close_time_str = kline_point.get(
|
|
197
|
+
"close_time_str", pd.to_datetime(close_time, unit="ms").strftime("%Y-%m-%d %H:%M:%S")
|
|
198
|
+
)
|
|
199
|
+
point_factors: Dict[str, Any] = {}
|
|
200
|
+
|
|
201
|
+
for factor_name, factor_func, min_req, kwargs, is_alpha in factor_configs:
|
|
202
|
+
if len(time_point_data) < min_req:
|
|
203
|
+
continue
|
|
204
|
+
try:
|
|
205
|
+
if is_alpha:
|
|
206
|
+
result = calculate_normalized_alpha_factor(
|
|
207
|
+
time_point_data,
|
|
208
|
+
factor_func,
|
|
209
|
+
factor_name,
|
|
210
|
+
min_required=min_req,
|
|
211
|
+
lookback_periods=lookback_periods,
|
|
212
|
+
factor_cache=factor_cache,
|
|
213
|
+
current_point_idx=idx,
|
|
214
|
+
**kwargs,
|
|
215
|
+
)
|
|
216
|
+
if result:
|
|
217
|
+
point_factors[factor_name] = {
|
|
218
|
+
"value": result.get("value", 0.0),
|
|
219
|
+
"raw_value": result.get("raw_value", 0.0),
|
|
220
|
+
"signal": result.get("signal", "中性"),
|
|
221
|
+
"prev_normalized": result.get("prev_normalized", 0.0),
|
|
222
|
+
}
|
|
223
|
+
else:
|
|
224
|
+
cache_key = (idx, factor_name)
|
|
225
|
+
if cache_key in factor_cache:
|
|
226
|
+
factor_value = factor_cache[cache_key]
|
|
227
|
+
else:
|
|
228
|
+
factor_value = factor_func(time_point_data, **kwargs)
|
|
229
|
+
if factor_value is not None:
|
|
230
|
+
factor_cache[cache_key] = float(factor_value)
|
|
231
|
+
if factor_value is not None:
|
|
232
|
+
signal = "看多" if factor_value > 0 else ("看空" if factor_value < 0 else "中性")
|
|
233
|
+
point_factors[factor_name] = {
|
|
234
|
+
"value": float(factor_value),
|
|
235
|
+
"raw_value": float(factor_value),
|
|
236
|
+
"signal": signal,
|
|
237
|
+
}
|
|
238
|
+
except Exception as e:
|
|
239
|
+
logging.debug("Failed %s at idx %s: %s", factor_name, idx, e)
|
|
240
|
+
|
|
241
|
+
# 仅在有足够历史时输出,不进行任何 0 填充
|
|
242
|
+
if idx < HISTORY_WINDOW:
|
|
243
|
+
return None
|
|
244
|
+
start_history_idx = idx - HISTORY_WINDOW
|
|
245
|
+
end_history_idx = idx
|
|
246
|
+
hist = data_df.iloc[start_history_idx:end_history_idx]
|
|
247
|
+
open_price_list = hist["open_price"].astype(float).tolist()
|
|
248
|
+
high_price_list = hist["high_price"].astype(float).tolist()
|
|
249
|
+
low_price_list = hist["low_price"].astype(float).tolist()
|
|
250
|
+
close_price_list = hist["close_price"].astype(float).tolist()
|
|
251
|
+
volume_list = hist["volume"].astype(float).tolist()
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
"time_index": idx,
|
|
255
|
+
"open_time": open_time,
|
|
256
|
+
"open_time_str": open_time_str,
|
|
257
|
+
"close_time": close_time,
|
|
258
|
+
"close_time_str": close_time_str,
|
|
259
|
+
"open_price": float(kline_point["open_price"]),
|
|
260
|
+
"high_price": float(kline_point["high_price"]),
|
|
261
|
+
"low_price": float(kline_point["low_price"]),
|
|
262
|
+
"close_price": float(kline_point["close_price"]),
|
|
263
|
+
"volume": float(kline_point["volume"]),
|
|
264
|
+
"factors": point_factors,
|
|
265
|
+
"open_price_list": open_price_list,
|
|
266
|
+
"high_price_list": high_price_list,
|
|
267
|
+
"low_price_list": low_price_list,
|
|
268
|
+
"close_price_list": close_price_list,
|
|
269
|
+
"volume_list": volume_list,
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _klines_to_df(klines_data: List[Any]) -> pd.DataFrame:
|
|
274
|
+
formatted: List[Dict[str, Any]] = []
|
|
275
|
+
for kline in klines_data:
|
|
276
|
+
if isinstance(kline, list):
|
|
277
|
+
open_time = int(kline[0]) if isinstance(kline[0], str) else kline[0]
|
|
278
|
+
close_time = int(kline[6]) if isinstance(kline[6], str) else kline[6]
|
|
279
|
+
formatted.append({
|
|
280
|
+
"datetime": pd.to_datetime(open_time, unit="ms"),
|
|
281
|
+
"open_time": open_time,
|
|
282
|
+
"open_time_str": pd.to_datetime(open_time, unit="ms").strftime("%Y-%m-%d %H:%M:%S"),
|
|
283
|
+
"open_price": float(kline[1]),
|
|
284
|
+
"high_price": float(kline[2]),
|
|
285
|
+
"low_price": float(kline[3]),
|
|
286
|
+
"close_price": float(kline[4]),
|
|
287
|
+
"volume": float(kline[5]),
|
|
288
|
+
"close_time": close_time,
|
|
289
|
+
"close_time_str": pd.to_datetime(close_time, unit="ms").strftime("%Y-%m-%d %H:%M:%S"),
|
|
290
|
+
})
|
|
291
|
+
elif isinstance(kline, dict):
|
|
292
|
+
row = dict(kline)
|
|
293
|
+
if "datetime" not in row and "open_time" in row:
|
|
294
|
+
row["datetime"] = pd.to_datetime(row["open_time"], unit="ms")
|
|
295
|
+
formatted.append(row)
|
|
296
|
+
df = pd.DataFrame(formatted)
|
|
297
|
+
return df.sort_values("datetime").reset_index(drop=True)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _get_fetch_fn(type: str):
|
|
301
|
+
"""按 type 返回拉取 K 线的函数(futures / spot)。"""
|
|
302
|
+
if type == "futures":
|
|
303
|
+
from cyqnt_trd.get_data.get_futures_data import get_and_save_futures_klines
|
|
304
|
+
return get_and_save_futures_klines
|
|
305
|
+
from cyqnt_trd.get_data.get_trending_data import get_and_save_klines_direct
|
|
306
|
+
return get_and_save_klines_direct
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _fetch_klines_df(
|
|
310
|
+
token: str,
|
|
311
|
+
type: str,
|
|
312
|
+
interval: str,
|
|
313
|
+
start_time: Optional[Union[datetime, str, int]] = None,
|
|
314
|
+
end_time: Optional[Union[datetime, str, int]] = None,
|
|
315
|
+
limit: Optional[int] = None,
|
|
316
|
+
) -> Optional[pd.DataFrame]:
|
|
317
|
+
get_fn = _get_fetch_fn(type)
|
|
318
|
+
kwargs: Dict[str, Any] = {"symbol": token, "interval": interval, "save_csv": False, "save_json": False}
|
|
319
|
+
if limit is not None:
|
|
320
|
+
kwargs["limit"] = limit
|
|
321
|
+
if start_time is not None:
|
|
322
|
+
kwargs["start_time"] = start_time
|
|
323
|
+
if end_time is not None:
|
|
324
|
+
kwargs["end_time"] = end_time
|
|
325
|
+
raw = get_fn(**kwargs)
|
|
326
|
+
if not raw:
|
|
327
|
+
return None
|
|
328
|
+
return _klines_to_df(raw)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _fetch_klines_by_range_paginated(
|
|
332
|
+
token: str,
|
|
333
|
+
type: str,
|
|
334
|
+
interval: str,
|
|
335
|
+
start_ms: int,
|
|
336
|
+
end_ms: int,
|
|
337
|
+
max_per_request: int = MAX_KLINES_PER_REQUEST,
|
|
338
|
+
) -> Optional[pd.DataFrame]:
|
|
339
|
+
"""
|
|
340
|
+
按时间范围拉取 K 线;若条数超过 max_per_request,则按时间分段多次请求后合并。
|
|
341
|
+
返回合并去重并按 open_time 排序的 DataFrame。
|
|
342
|
+
"""
|
|
343
|
+
interval_ms = _get_interval_duration_ms(interval)
|
|
344
|
+
num_bars_approx = max(0, (end_ms - start_ms) // interval_ms)
|
|
345
|
+
if num_bars_approx == 0:
|
|
346
|
+
return _fetch_klines_df(token, type, interval, start_time=start_ms, end_time=end_ms)
|
|
347
|
+
|
|
348
|
+
get_fn = _get_fetch_fn(type)
|
|
349
|
+
if num_bars_approx <= max_per_request:
|
|
350
|
+
# 小范围时仅用 end_time + limit 单次请求,避免底层 start+end 触发的多轮分页导致卡住
|
|
351
|
+
request_limit = min(num_bars_approx + 20, max_per_request)
|
|
352
|
+
raw = get_fn(
|
|
353
|
+
symbol=token,
|
|
354
|
+
interval=interval,
|
|
355
|
+
end_time=end_ms,
|
|
356
|
+
limit=request_limit,
|
|
357
|
+
save_csv=False,
|
|
358
|
+
save_json=False,
|
|
359
|
+
)
|
|
360
|
+
if not raw:
|
|
361
|
+
return None
|
|
362
|
+
df = _klines_to_df(raw)
|
|
363
|
+
# 只保留 [start_ms, end_ms] 内的 K 线
|
|
364
|
+
if "open_time" in df.columns:
|
|
365
|
+
df = df[df["open_time"] >= start_ms].sort_values("open_time").reset_index(drop=True)
|
|
366
|
+
return df
|
|
367
|
+
|
|
368
|
+
all_raw: List[Any] = []
|
|
369
|
+
current_start = start_ms
|
|
370
|
+
request_count = 0
|
|
371
|
+
max_chunk_requests = 1000 # 防止异常时无限循环
|
|
372
|
+
while current_start < end_ms and request_count < max_chunk_requests:
|
|
373
|
+
request_count += 1
|
|
374
|
+
chunk_end_ms = min(current_start + max_per_request * interval_ms, end_ms)
|
|
375
|
+
batch = get_fn(
|
|
376
|
+
symbol=token,
|
|
377
|
+
interval=interval,
|
|
378
|
+
start_time=current_start,
|
|
379
|
+
end_time=chunk_end_ms,
|
|
380
|
+
save_csv=False,
|
|
381
|
+
save_json=False,
|
|
382
|
+
)
|
|
383
|
+
if not batch:
|
|
384
|
+
break
|
|
385
|
+
all_raw.extend(batch)
|
|
386
|
+
logging.info(
|
|
387
|
+
"K线分页第 %s 次请求: %s ~ %s, 本批 %s 条, 累计 %s 条",
|
|
388
|
+
request_count,
|
|
389
|
+
pd.to_datetime(current_start, unit="ms").strftime("%Y-%m-%d %H:%M"),
|
|
390
|
+
pd.to_datetime(chunk_end_ms, unit="ms").strftime("%Y-%m-%d %H:%M"),
|
|
391
|
+
len(batch),
|
|
392
|
+
len(all_raw),
|
|
393
|
+
)
|
|
394
|
+
if len(batch) < max_per_request:
|
|
395
|
+
break
|
|
396
|
+
current_start = chunk_end_ms
|
|
397
|
+
if not all_raw:
|
|
398
|
+
return None
|
|
399
|
+
df = _klines_to_df(all_raw)
|
|
400
|
+
# 分页边界可能重复,按 open_time 去重
|
|
401
|
+
if "open_time" in df.columns:
|
|
402
|
+
df = df.drop_duplicates(subset=["open_time"], keep="first").sort_values("open_time").reset_index(drop=True)
|
|
403
|
+
return df
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def _compute_kline_data_with_factors(
|
|
407
|
+
data_df: pd.DataFrame,
|
|
408
|
+
factor_configs: List[tuple],
|
|
409
|
+
lookback_periods: int = 30,
|
|
410
|
+
) -> List[Dict[str, Any]]:
|
|
411
|
+
min_required = max(c[2] for c in factor_configs)
|
|
412
|
+
# 仅对“前面至少有 HISTORY_WINDOW 根真实 K 线”的点算因子,不填充 0
|
|
413
|
+
start_idx = max(min_required + 2, HISTORY_WINDOW)
|
|
414
|
+
factor_cache: Dict[tuple, float] = {}
|
|
415
|
+
out: List[Dict[str, Any]] = []
|
|
416
|
+
for idx in range(start_idx, len(data_df)):
|
|
417
|
+
try:
|
|
418
|
+
point = _compute_point_factors(
|
|
419
|
+
data_df, idx, factor_configs, factor_cache, lookback_periods
|
|
420
|
+
)
|
|
421
|
+
if point is not None:
|
|
422
|
+
out.append(point)
|
|
423
|
+
except Exception as e:
|
|
424
|
+
logging.debug("Skip point idx %s (no padding): %s", idx, e)
|
|
425
|
+
return out
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
# ---------------------------------------------------------------------------
|
|
429
|
+
# 公用方法:输入 klines_data,输出带因子的 klines 列表
|
|
430
|
+
# ---------------------------------------------------------------------------
|
|
431
|
+
|
|
432
|
+
def get_klines_with_factors(
|
|
433
|
+
klines_data: List[Any],
|
|
434
|
+
factor_configs: Optional[List[tuple]] = None,
|
|
435
|
+
lookback_periods: int = 30,
|
|
436
|
+
) -> List[Dict[str, Any]]:
|
|
437
|
+
"""
|
|
438
|
+
公用方法:输入原始 klines_data(list of kline dict/list),输出带因子的 klines 列表。
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
klines_data: 原始 K 线数据,与 get_and_save_klines_direct / get_and_save_futures_klines 返回格式一致
|
|
442
|
+
factor_configs: 因子配置,默认 None 使用内置配置
|
|
443
|
+
lookback_periods: 因子回看周期
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
带因子的 K 线列表,每项为 dict(含 open_time, factors, open_price_list 等)
|
|
447
|
+
"""
|
|
448
|
+
data_df = _klines_to_df(klines_data)
|
|
449
|
+
if data_df.empty:
|
|
450
|
+
return []
|
|
451
|
+
configs = factor_configs or _get_factor_configs()
|
|
452
|
+
return _compute_kline_data_with_factors(data_df, configs, lookback_periods)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def get_klines_with_factors_at_time(
|
|
456
|
+
klines_data: List[Any],
|
|
457
|
+
factor_configs: Optional[List[tuple]] = None,
|
|
458
|
+
lookback_periods: int = 30,
|
|
459
|
+
) -> List[Dict[str, Any]]:
|
|
460
|
+
"""
|
|
461
|
+
输入 klines_data,输出带因子的 klines 数据点,仅包含最新一根(最后一个时间点)。
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
klines_data: 原始 K 线数据
|
|
465
|
+
factor_configs: 因子配置,默认 None 使用内置配置
|
|
466
|
+
lookback_periods: 因子回看周期
|
|
467
|
+
|
|
468
|
+
Returns:
|
|
469
|
+
长度为 0 或 1 的列表;有足够历史时为 [最新一根带因子的 K 线 dict],否则为 []。
|
|
470
|
+
"""
|
|
471
|
+
all_points = get_klines_with_factors(klines_data, factor_configs, lookback_periods)
|
|
472
|
+
if not all_points:
|
|
473
|
+
return []
|
|
474
|
+
return [all_points[-1]]
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def get_klines_with_factors_n_points(
|
|
478
|
+
klines_data: List[Any],
|
|
479
|
+
factor_configs: Optional[List[tuple]] = None,
|
|
480
|
+
lookback_periods: int = 30,
|
|
481
|
+
) -> List[Dict[str, Any]]:
|
|
482
|
+
"""
|
|
483
|
+
输入 klines_data,输出带因子的 klines 数据,包含所有可计算因子的时间点及对应数据点。
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
klines_data: 原始 K 线数据
|
|
487
|
+
factor_configs: 因子配置,默认 None 使用内置配置
|
|
488
|
+
lookback_periods: 因子回看周期
|
|
489
|
+
|
|
490
|
+
Returns:
|
|
491
|
+
带因子的 K 线列表,每项为 dict(含 open_time, factors, open_price_list 等),与 klines_data 中可算因子的点一一对应。
|
|
492
|
+
"""
|
|
493
|
+
return get_klines_with_factors(klines_data, factor_configs, lookback_periods)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
# ---------------------------------------------------------------------------
|
|
497
|
+
# 对外接口
|
|
498
|
+
# ---------------------------------------------------------------------------
|
|
499
|
+
|
|
500
|
+
def _success_at_time(
|
|
501
|
+
token: str,
|
|
502
|
+
type: str,
|
|
503
|
+
interval: str,
|
|
504
|
+
at_ms: int,
|
|
505
|
+
kline_data: List[Dict[str, Any]],
|
|
506
|
+
data_rows: Optional[int] = None,
|
|
507
|
+
reason: Optional[str] = None,
|
|
508
|
+
) -> Dict[str, Any]:
|
|
509
|
+
metadata = {"token": token, "type": type, "interval": interval, "at_time": at_ms}
|
|
510
|
+
if data_rows is not None:
|
|
511
|
+
metadata["data_rows"] = data_rows
|
|
512
|
+
if reason is not None:
|
|
513
|
+
metadata["reason"] = reason
|
|
514
|
+
elif data_rows is not None:
|
|
515
|
+
metadata["at_time_str"] = pd.to_datetime(at_ms, unit="ms").strftime("%Y-%m-%d %H:%M:%S")
|
|
516
|
+
return {"status": "success", "kline_data": kline_data, "metadata": metadata}
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def get_kline_with_factor_at_time(
|
|
520
|
+
token: str,
|
|
521
|
+
type: str = "futures",
|
|
522
|
+
interval: str = "30m",
|
|
523
|
+
at_time: Union[datetime, str, int] = None,
|
|
524
|
+
limit: int = 200,
|
|
525
|
+
) -> Dict[str, Any]:
|
|
526
|
+
"""
|
|
527
|
+
针对某个时间点:先拉取 klines_data,再经 get_klines_with_factors 计算因子,
|
|
528
|
+
返回该时间点的单根 K 线及其因子。
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
token: 交易对,如 BTCUSDT
|
|
532
|
+
type: futures | spot
|
|
533
|
+
interval: K 线周期,如 1m, 30m, 1h
|
|
534
|
+
at_time: 目标时间点(datetime/字符串/毫秒或秒时间戳)
|
|
535
|
+
limit: 拉取 K 线数量,默认 200
|
|
536
|
+
|
|
537
|
+
Returns:
|
|
538
|
+
{"status": "success"|"error", "kline_data": [单点], "metadata": {...}}
|
|
539
|
+
"""
|
|
540
|
+
at_ms = _convert_to_timestamp_ms(at_time) if at_time is not None else None
|
|
541
|
+
if at_ms is None:
|
|
542
|
+
return {"status": "error", "error": "at_time is required", "kline_data": [], "metadata": {}}
|
|
543
|
+
|
|
544
|
+
get_fn = _get_fetch_fn(type)
|
|
545
|
+
klines_data = get_fn(
|
|
546
|
+
symbol=token,
|
|
547
|
+
interval=interval,
|
|
548
|
+
limit=min(limit, MAX_KLINES_PER_REQUEST),
|
|
549
|
+
end_time=at_ms + _get_interval_duration_ms(interval),
|
|
550
|
+
save_csv=False,
|
|
551
|
+
save_json=False,
|
|
552
|
+
)
|
|
553
|
+
if not klines_data:
|
|
554
|
+
return {
|
|
555
|
+
"status": "error",
|
|
556
|
+
"error": f"No kline data for token {token}",
|
|
557
|
+
"kline_data": [],
|
|
558
|
+
"metadata": {"token": token, "type": type, "interval": interval, "at_time": at_ms},
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
data_df = _klines_to_df(klines_data).sort_values("datetime").reset_index(drop=True)
|
|
562
|
+
mask = data_df["open_time"] <= at_ms
|
|
563
|
+
if not mask.any():
|
|
564
|
+
return _success_at_time(token, type, interval, at_ms, [], data_rows=len(data_df))
|
|
565
|
+
row_idx = int(data_df.index[mask][-1])
|
|
566
|
+
min_idx = max(max(c[2] for c in _get_factor_configs()) + 2, HISTORY_WINDOW)
|
|
567
|
+
if row_idx < min_idx:
|
|
568
|
+
return _success_at_time(
|
|
569
|
+
token, type, interval, at_ms, [],
|
|
570
|
+
reason="Insufficient history (need at least %s bars before point)" % HISTORY_WINDOW,
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
kline_data_with_factors = get_klines_with_factors(klines_data)
|
|
574
|
+
target_open_time = int(data_df.iloc[row_idx]["open_time"])
|
|
575
|
+
point_data = next((p for p in kline_data_with_factors if int(p["open_time"]) == target_open_time), None)
|
|
576
|
+
return _success_at_time(token, type, interval, at_ms, [point_data] if point_data else [], data_rows=len(data_df))
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def get_kline_with_factor_n_points(
|
|
580
|
+
token: str,
|
|
581
|
+
type: str = "futures",
|
|
582
|
+
interval: str = "30m",
|
|
583
|
+
n: int = 500,
|
|
584
|
+
end_time: Optional[Union[datetime, str, int]] = None,
|
|
585
|
+
) -> Dict[str, Any]:
|
|
586
|
+
"""
|
|
587
|
+
以某时间为 end_time 向前取 n 个点:先拉取 klines_data,再经 get_klines_with_factors 计算因子,
|
|
588
|
+
返回当前所有时间点的带因子数据。
|
|
589
|
+
|
|
590
|
+
Args:
|
|
591
|
+
token: 交易对
|
|
592
|
+
type: futures | spot
|
|
593
|
+
interval: K 线周期
|
|
594
|
+
n: 向前取的 K 线根数
|
|
595
|
+
end_time: 结束时间,默认 None 表示当前时间
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
{"status": "success"|"error", "kline_data": [...], "metadata": {...}}
|
|
599
|
+
"""
|
|
600
|
+
end_ms = int(datetime.now().timestamp() * 1000) if end_time is None else _convert_to_timestamp_ms(end_time)
|
|
601
|
+
if end_ms is None:
|
|
602
|
+
return {"status": "error", "error": "Invalid end_time", "kline_data": [], "metadata": {}}
|
|
603
|
+
|
|
604
|
+
interval_ms = _get_interval_duration_ms(interval)
|
|
605
|
+
start_ms = end_ms - (n + HISTORY_WINDOW) * interval_ms
|
|
606
|
+
data_df = _fetch_klines_by_range_paginated(
|
|
607
|
+
token=token, type=type, interval=interval,
|
|
608
|
+
start_ms=start_ms, end_ms=end_ms + interval_ms,
|
|
609
|
+
)
|
|
610
|
+
if data_df is None or data_df.empty:
|
|
611
|
+
return {
|
|
612
|
+
"status": "error",
|
|
613
|
+
"error": f"No kline data for token {token}",
|
|
614
|
+
"kline_data": [],
|
|
615
|
+
"metadata": {"token": token, "type": type, "interval": interval, "n": n},
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
klines_data = data_df.to_dict("records")
|
|
619
|
+
kline_data_with_factors = get_klines_with_factors(klines_data)
|
|
620
|
+
kline_data_with_factors = [p for p in kline_data_with_factors if int(p["open_time"]) <= end_ms][-n:]
|
|
621
|
+
return {
|
|
622
|
+
"status": "success",
|
|
623
|
+
"kline_data": kline_data_with_factors,
|
|
624
|
+
"metadata": {
|
|
625
|
+
"token": token, "type": type, "interval": interval, "n": n,
|
|
626
|
+
"end_time": end_ms,
|
|
627
|
+
"end_time_str": pd.to_datetime(end_ms, unit="ms").strftime("%Y-%m-%d %H:%M:%S"),
|
|
628
|
+
"start_time": start_ms,
|
|
629
|
+
"start_time_str": pd.to_datetime(start_ms, unit="ms").strftime("%Y-%m-%d %H:%M:%S"),
|
|
630
|
+
"total_kline_points": len(kline_data_with_factors),
|
|
631
|
+
"total_data_points": len(data_df),
|
|
632
|
+
},
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
# ---------------------------------------------------------------------------
|
|
637
|
+
# 测试脚本
|
|
638
|
+
# ---------------------------------------------------------------------------
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
def _value_equals(va: Any, vb: Any, rtol: float, atol: float) -> bool:
|
|
642
|
+
"""递归比较两值是否在数值容差内一致(支持 dict/list/数值/其它)。"""
|
|
643
|
+
if isinstance(va, dict) and isinstance(vb, dict):
|
|
644
|
+
if set(va.keys()) != set(vb.keys()):
|
|
645
|
+
return False
|
|
646
|
+
return all(_value_equals(va[k], vb[k], rtol, atol) for k in va)
|
|
647
|
+
if isinstance(va, (list, tuple)) and isinstance(vb, (list, tuple)):
|
|
648
|
+
if len(va) != len(vb):
|
|
649
|
+
return False
|
|
650
|
+
return all(_value_equals(x, y, rtol, atol) for x, y in zip(va, vb))
|
|
651
|
+
if isinstance(va, (int, float)) and isinstance(vb, (int, float)):
|
|
652
|
+
try:
|
|
653
|
+
return bool(np.isclose(float(va), float(vb), rtol=rtol, atol=atol, equal_nan=True))
|
|
654
|
+
except (TypeError, ValueError):
|
|
655
|
+
return va == vb
|
|
656
|
+
return va == vb
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def _point_dict_equals(a: Dict[str, Any], b: Dict[str, Any], rtol: float = 1e-5, atol: float = 1e-8) -> bool:
|
|
660
|
+
"""比较两条 K 线+因子记录是否一致(数值字段用容差比较,含嵌套 factors)。排除 time_index(两处 data_df 不同)。"""
|
|
661
|
+
keys_a, keys_b = set(a.keys()), set(b.keys())
|
|
662
|
+
if keys_a != keys_b:
|
|
663
|
+
return False
|
|
664
|
+
skip_keys = {"time_index"} # 两处 data_df 行号不同,不参与比较
|
|
665
|
+
if int(a.get("open_time", 0)) != int(b.get("open_time", 0)):
|
|
666
|
+
return False
|
|
667
|
+
for k in a:
|
|
668
|
+
if k in skip_keys:
|
|
669
|
+
continue
|
|
670
|
+
if not _value_equals(a[k], b[k], rtol, atol):
|
|
671
|
+
return False
|
|
672
|
+
return True
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def _test_at_time_equals_n_points_last():
|
|
676
|
+
"""
|
|
677
|
+
准则:同一份 klines_data 下,get_klines_with_factors_at_time 返回的最新一根
|
|
678
|
+
应与 get_klines_with_factors_n_points 返回的最后一根完全相同。
|
|
679
|
+
"""
|
|
680
|
+
print("=" * 60)
|
|
681
|
+
print("测试: get_klines_with_factors_at_time 末条 == get_klines_with_factors_n_points 末条")
|
|
682
|
+
print("=" * 60)
|
|
683
|
+
token, type_, interval = "BTCUSDT", "futures", "1h"
|
|
684
|
+
at_time_str = "2026-01-30 8:00:00"
|
|
685
|
+
at_ms = _convert_to_timestamp_ms(at_time_str)
|
|
686
|
+
|
|
687
|
+
get_fn = _get_fetch_fn(type_)
|
|
688
|
+
klines_data = get_fn(
|
|
689
|
+
symbol=token,
|
|
690
|
+
interval=interval,
|
|
691
|
+
limit=min(200, MAX_KLINES_PER_REQUEST),
|
|
692
|
+
end_time=at_ms + _get_interval_duration_ms(interval),
|
|
693
|
+
save_csv=False,
|
|
694
|
+
save_json=False,
|
|
695
|
+
)
|
|
696
|
+
if not klines_data:
|
|
697
|
+
print("FAIL: 无法获取 klines_data")
|
|
698
|
+
return
|
|
699
|
+
|
|
700
|
+
at_list = get_klines_with_factors_at_time(klines_data)
|
|
701
|
+
n_list = get_klines_with_factors_n_points(klines_data)
|
|
702
|
+
if not at_list:
|
|
703
|
+
print("FAIL: get_klines_with_factors_at_time 返回空(可能历史不足)")
|
|
704
|
+
return
|
|
705
|
+
if not n_list:
|
|
706
|
+
print("FAIL: get_klines_with_factors_n_points 返回空")
|
|
707
|
+
return
|
|
708
|
+
|
|
709
|
+
at_point = at_list[0]
|
|
710
|
+
n_last = n_list[-1]
|
|
711
|
+
if int(at_point["open_time"]) != int(n_last["open_time"]):
|
|
712
|
+
print(
|
|
713
|
+
"FAIL: open_time 不一致 at_time=%s n_points_last=%s"
|
|
714
|
+
% (at_point["open_time"], n_last["open_time"])
|
|
715
|
+
)
|
|
716
|
+
return
|
|
717
|
+
if not _point_dict_equals(at_point, n_last):
|
|
718
|
+
print("FAIL: 两条记录内容不一致(数值容差内)")
|
|
719
|
+
print("at_time 首条 open_time:", at_point.get("open_time"))
|
|
720
|
+
print("n_points 末条 open_time:", n_last.get("open_time"))
|
|
721
|
+
return
|
|
722
|
+
print("PASS: get_klines_with_factors_at_time 单条 与 get_klines_with_factors_n_points 末条 完全相同")
|
|
723
|
+
print()
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
def _test_get_kline_with_factor_at_time():
|
|
727
|
+
"""测试 get_klines_with_factors_at_time:先获取 klines_data,再调用得到仅最新一根带因子数据."""
|
|
728
|
+
print("=" * 60)
|
|
729
|
+
print("测试: get_klines_with_factors_at_time(输入 klines_data,输出仅最新一根带因子)")
|
|
730
|
+
print("=" * 60)
|
|
731
|
+
token, type_, interval = "BTCUSDT", "futures", "1h"
|
|
732
|
+
at_time = "2026-01-30 8:00:00"
|
|
733
|
+
at_ms = _convert_to_timestamp_ms(at_time)
|
|
734
|
+
|
|
735
|
+
get_fn = _get_fetch_fn(type_)
|
|
736
|
+
klines_data = get_fn(
|
|
737
|
+
symbol=token,
|
|
738
|
+
interval=interval,
|
|
739
|
+
limit=min(200, MAX_KLINES_PER_REQUEST),
|
|
740
|
+
end_time=at_ms + _get_interval_duration_ms(interval),
|
|
741
|
+
save_csv=False,
|
|
742
|
+
save_json=False,
|
|
743
|
+
)
|
|
744
|
+
if not klines_data:
|
|
745
|
+
print("error: No kline data")
|
|
746
|
+
return
|
|
747
|
+
|
|
748
|
+
kline_data = get_klines_with_factors_at_time(klines_data)
|
|
749
|
+
print("at_time:", at_time)
|
|
750
|
+
print("status: success")
|
|
751
|
+
print("kline_data 条数:", len(kline_data), "(预期 0 或 1)")
|
|
752
|
+
if kline_data:
|
|
753
|
+
print("首条 keys:", list(kline_data[0].keys())[:10], "...")
|
|
754
|
+
print("metadata:", {"token": token, "type": type_, "interval": interval, "at_time": at_ms, "data_rows": len(klines_data)})
|
|
755
|
+
print()
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def _test_get_kline_with_factor_n_points():
|
|
759
|
+
"""测试 get_klines_with_factors_n_points:先获取 klines_data,再调用得到所有带因子数据点."""
|
|
760
|
+
import os
|
|
761
|
+
|
|
762
|
+
print("=" * 60)
|
|
763
|
+
print("测试: get_klines_with_factors_n_points(输入 klines_data,输出所有带因子数据点)")
|
|
764
|
+
print("=" * 60)
|
|
765
|
+
token, type_, interval = "BTCUSDT", "futures", "1h"
|
|
766
|
+
n, end_time = 3000, "2026-01-30 8:00:00"
|
|
767
|
+
end_ms = _convert_to_timestamp_ms(end_time)
|
|
768
|
+
interval_ms = _get_interval_duration_ms(interval)
|
|
769
|
+
start_ms = end_ms - (n + HISTORY_WINDOW) * interval_ms
|
|
770
|
+
|
|
771
|
+
data_df = _fetch_klines_by_range_paginated(
|
|
772
|
+
token=token, type=type_, interval=interval,
|
|
773
|
+
start_ms=start_ms, end_ms=end_ms + interval_ms,
|
|
774
|
+
)
|
|
775
|
+
if data_df is None or data_df.empty:
|
|
776
|
+
print("error: No kline data")
|
|
777
|
+
return
|
|
778
|
+
|
|
779
|
+
klines_data = data_df.to_dict("records")
|
|
780
|
+
kline_data_with_factors = get_klines_with_factors_n_points(klines_data)
|
|
781
|
+
# 可选:只保留 open_time <= end_ms 的最后 n 条,便于与 n_points 语义对比
|
|
782
|
+
displayed = [p for p in kline_data_with_factors if int(p["open_time"]) <= end_ms][-n:]
|
|
783
|
+
|
|
784
|
+
print("n:", n, "end_time:", end_time)
|
|
785
|
+
print("status: success")
|
|
786
|
+
print("kline_data_with_factors 总条数:", len(kline_data_with_factors))
|
|
787
|
+
print("displayed (open_time<=end_ms 最后 n 条):", len(displayed))
|
|
788
|
+
if displayed:
|
|
789
|
+
first, last = displayed[0], displayed[-1]
|
|
790
|
+
print("首条 open_time:", first.get("open_time"), "末条 open_time:", last.get("open_time"))
|
|
791
|
+
print("metadata:", {"token": token, "type": type_, "interval": interval, "n": n, "end_time": end_ms})
|
|
792
|
+
|
|
793
|
+
save_dir = "/Users/user/Desktop/repo/crypto_trading/cyqnt_trd/tmp"
|
|
794
|
+
os.makedirs(save_dir, exist_ok=True)
|
|
795
|
+
save_path = os.path.join(save_dir, "get_kline_with_factor_n_points_result.parquet")
|
|
796
|
+
try:
|
|
797
|
+
pd.DataFrame(displayed).to_parquet(save_path, engine="pyarrow", index=False)
|
|
798
|
+
print(f"结果已保存: {save_path}")
|
|
799
|
+
except Exception as e:
|
|
800
|
+
print(f"保存 parquet 时出错: {e}")
|
|
801
|
+
print()
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
if __name__ == "__main__":
|
|
805
|
+
_test_get_kline_with_factor_at_time()
|
|
806
|
+
_test_get_kline_with_factor_n_points()
|
|
807
|
+
_test_at_time_equals_n_points_last()
|
|
808
|
+
print("全部测试脚本执行完毕.")
|