akquant 0.1.0__cp39-abi3-win_amd64.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.

Potentially problematic release.


This version of akquant might be problematic. Click here for more details.

akquant/strategy.py ADDED
@@ -0,0 +1,516 @@
1
+ from typing import Optional, Any, Dict
2
+ from collections import deque, defaultdict
3
+ import numpy as np
4
+ from .akquant import StrategyContext, TimeInForce, OrderStatus, Bar, Tick
5
+ from .sizer import Sizer, FixedSize
6
+
7
+
8
+ class Strategy:
9
+ """
10
+ 策略基类 (Base Strategy Class)
11
+ 采用类似 NautilusTrader 的事件驱动设计
12
+ """
13
+
14
+ def __new__(cls, *args, **kwargs):
15
+ instance = super().__new__(cls)
16
+ instance.ctx: Optional[StrategyContext] = None
17
+ instance.sizer: Sizer = FixedSize(100)
18
+ instance.current_bar: Optional[Bar] = None
19
+ instance.current_tick: Optional[Tick] = None
20
+
21
+ # 历史数据存储
22
+ instance._history_depth = 0
23
+ instance._bars_history = defaultdict(
24
+ lambda: deque(maxlen=max(1, instance._history_depth))
25
+ )
26
+ return instance
27
+
28
+ def __init__(self):
29
+ pass
30
+
31
+ def set_history_depth(self, depth: int):
32
+ """
33
+ 设置历史数据回溯长度
34
+
35
+ :param depth: 保留的 Bar 数量 (0 表示不保留)
36
+ """
37
+ self._history_depth = depth
38
+ if depth > 0:
39
+ # 如果已有数据,需要调整 maxlen (通过重新创建 deque)
40
+ # 注意: 这会清空现有历史,通常只在初始化时调用
41
+ self._bars_history = defaultdict(lambda: deque(maxlen=depth))
42
+ else:
43
+ self._bars_history.clear()
44
+
45
+ def get_history(
46
+ self, count: int, symbol: Optional[str] = None, field: str = "close"
47
+ ) -> np.ndarray:
48
+ """
49
+ 获取历史数据 (类似 Zipline data.history)
50
+
51
+ :param count: 获取的数据长度 (必须 <= history_depth)
52
+ :param symbol: 标的代码 (默认当前 Bar 的 symbol)
53
+ :param field: 字段名 (open, high, low, close, volume)
54
+ :return: Numpy 数组
55
+ """
56
+ if self._history_depth == 0:
57
+ raise RuntimeError(
58
+ "History tracking is not enabled. Call set_history_depth() first."
59
+ )
60
+
61
+ symbol = self._resolve_symbol(symbol)
62
+ history = self._bars_history[symbol]
63
+
64
+ if len(history) < count:
65
+ # 数据不足时返回 NaN 填充的数组
66
+ return np.full(count, np.nan)
67
+
68
+ # 获取最近的 count 个 Bar
69
+ bars = list(history)[-count:]
70
+
71
+ # 提取字段
72
+ return np.array([getattr(b, field) for b in bars])
73
+
74
+ def set_sizer(self, sizer: Sizer):
75
+ """设置仓位管理器"""
76
+ self.sizer = sizer
77
+
78
+ def _on_bar_event(self, bar: Bar, ctx: StrategyContext):
79
+ """引擎调用的 Bar 回调 (Internal)"""
80
+ self.ctx = ctx
81
+ self.current_bar = bar
82
+
83
+ # 自动维护历史数据
84
+ if self._history_depth > 0:
85
+ self._bars_history[bar.symbol].append(bar)
86
+
87
+ self.on_bar(bar)
88
+
89
+ def _on_tick_event(self, tick: Tick, ctx: StrategyContext):
90
+ """引擎调用的 Tick 回调 (Internal)"""
91
+ self.ctx = ctx
92
+ self.current_tick = tick
93
+ self.on_tick(tick)
94
+
95
+ def _on_timer_event(self, payload: str, ctx: StrategyContext):
96
+ """引擎调用的 Timer 回调 (Internal)"""
97
+ self.ctx = ctx
98
+ self.on_timer(payload)
99
+
100
+ def on_bar(self, bar: Bar):
101
+ """
102
+ 策略逻辑入口 (Bar 数据)
103
+ 用户应重写此方法
104
+ """
105
+ pass
106
+
107
+ def on_tick(self, tick: Tick):
108
+ """
109
+ 策略逻辑入口 (Tick 数据)
110
+ 用户应重写此方法
111
+ """
112
+ pass
113
+
114
+ def on_timer(self, payload: str):
115
+ """
116
+ 策略逻辑入口 (Timer 事件)
117
+
118
+ Args:
119
+ payload: 定时器携带的数据
120
+ """
121
+ pass
122
+
123
+ def _resolve_symbol(self, symbol: Optional[str] = None):
124
+ if symbol is None:
125
+ if self.current_bar:
126
+ symbol = self.current_bar.symbol
127
+ elif self.current_tick:
128
+ symbol = self.current_tick.symbol
129
+ else:
130
+ raise ValueError("Symbol must be provided")
131
+ return symbol
132
+
133
+ def get_open_orders(self, symbol: Optional[str] = None):
134
+ """
135
+ 获取当前未完成的订单
136
+
137
+ Args:
138
+ symbol: 标的代码 (如果为 None,返回所有标的订单)
139
+
140
+ Returns:
141
+ List[Order]: 订单列表
142
+ """
143
+ if self.ctx is None:
144
+ return []
145
+
146
+ orders = [
147
+ o
148
+ for o in self.ctx.active_orders
149
+ if o.status in (OrderStatus.New, OrderStatus.Submitted)
150
+ ]
151
+ if symbol:
152
+ return [o for o in orders if o.symbol == symbol]
153
+ return orders
154
+
155
+ def cancel_order(self, order_or_id: Any):
156
+ """
157
+ 取消订单
158
+
159
+ Args:
160
+ order_or_id: 订单对象或订单 ID
161
+ """
162
+ if self.ctx is None:
163
+ raise RuntimeError("Context not ready")
164
+
165
+ order_id = order_or_id
166
+ if hasattr(order_or_id, "id"):
167
+ order_id = order_or_id.id
168
+
169
+ self.ctx.cancel_order(order_id)
170
+
171
+ def cancel_all_orders(self, symbol: Optional[str] = None):
172
+ """
173
+ 取消所有未完成订单
174
+
175
+ Args:
176
+ symbol: 标的代码 (如果为 None,取消所有标的订单)
177
+ """
178
+ for order in self.get_open_orders(symbol):
179
+ self.cancel_order(order)
180
+
181
+ def buy_all(self, symbol: Optional[str] = None):
182
+ """
183
+ 全仓买入 (Buy All)
184
+ 使用当前所有可用资金买入
185
+
186
+ Args:
187
+ symbol: 标的代码 (如果不填,默认使用当前 Bar/Tick 的 symbol)
188
+ """
189
+ if self.ctx is None:
190
+ raise RuntimeError("Context not ready")
191
+
192
+ symbol = self._resolve_symbol(symbol)
193
+
194
+ # 获取参考价格
195
+ price = 0.0
196
+ if self.current_bar and self.current_bar.symbol == symbol:
197
+ price = self.current_bar.close
198
+ elif self.current_tick and self.current_tick.symbol == symbol:
199
+ price = self.current_tick.price
200
+
201
+ if price <= 0:
202
+ # 无法获取价格,无法计算数量
203
+ # 这里可以选择记录日志或抛出警告,暂时直接返回
204
+ return
205
+
206
+ cash = self.ctx.cash
207
+ # 计算最大可买数量 (向下取整)
208
+ # 注意:这里未扣除预估手续费,如果资金刚好卡在边界,可能会因为手续费导致拒单
209
+ # 建议引擎层或用户预留 buffer,或者在这里 * 0.99
210
+ quantity = int(cash / price)
211
+
212
+ if quantity > 0:
213
+ self.buy(symbol=symbol, quantity=quantity)
214
+
215
+ def close_position(self, symbol: Optional[str] = None):
216
+ """
217
+ 平仓 (Close Position)
218
+ 卖出/买入以抵消当前持仓
219
+
220
+ Args:
221
+ symbol: 标的代码 (如果不填,默认使用当前 Bar/Tick 的 symbol)
222
+ """
223
+ symbol = self._resolve_symbol(symbol)
224
+ position = self.get_position(symbol)
225
+
226
+ if position > 0:
227
+ self.sell(symbol=symbol, quantity=position)
228
+ elif position < 0:
229
+ self.buy(symbol=symbol, quantity=abs(position))
230
+
231
+ def buy(
232
+ self,
233
+ symbol: Optional[str] = None,
234
+ quantity: Optional[float] = None,
235
+ price: Optional[float] = None,
236
+ time_in_force: Optional[TimeInForce] = None,
237
+ trigger_price: Optional[float] = None,
238
+ ):
239
+ """
240
+ 买入下单
241
+
242
+ Args:
243
+ symbol: 标的代码 (如果不填,默认使用当前 Bar/Tick 的 symbol)
244
+ quantity: 数量 (如果不填,使用 Sizer 计算)
245
+ price: 限价 (None 为市价)
246
+ time_in_force: 订单有效期
247
+ trigger_price: 触发价 (止损/止盈)
248
+ """
249
+ if self.ctx is None:
250
+ raise RuntimeError("Context not ready")
251
+
252
+ # 1. Determine Symbol
253
+ symbol = self._resolve_symbol(symbol)
254
+
255
+ # 2. Determine Reference Price for Sizing
256
+ ref_price = price
257
+ if ref_price is None:
258
+ if self.current_bar:
259
+ ref_price = self.current_bar.close
260
+ elif self.current_tick:
261
+ ref_price = self.current_tick.price
262
+ else:
263
+ ref_price = 0.0
264
+
265
+ # 3. Determine Quantity via Sizer
266
+ if quantity is None:
267
+ quantity = self.sizer.get_size(ref_price, self.ctx.cash, self.ctx, symbol)
268
+
269
+ # 4. Execute Buy
270
+ if quantity > 0:
271
+ self.ctx.buy(symbol, quantity, price, time_in_force, trigger_price)
272
+
273
+ def sell(
274
+ self,
275
+ symbol: Optional[str] = None,
276
+ quantity: Optional[float] = None,
277
+ price: Optional[float] = None,
278
+ time_in_force: Optional[TimeInForce] = None,
279
+ trigger_price: Optional[float] = None,
280
+ ):
281
+ """
282
+ 卖出下单
283
+
284
+ Args:
285
+ symbol: 标的代码 (如果不填,默认使用当前 Bar/Tick 的 symbol)
286
+ quantity: 数量 (如果不填,默认卖出当前标的所有持仓)
287
+ price: 限价 (None 为市价)
288
+ time_in_force: 订单有效期
289
+ trigger_price: 触发价 (止损/止盈)
290
+ """
291
+ if self.ctx is None:
292
+ raise RuntimeError("Context not ready")
293
+
294
+ # 1. Determine Symbol
295
+ if symbol is None:
296
+ if self.current_bar:
297
+ symbol = self.current_bar.symbol
298
+ elif self.current_tick:
299
+ symbol = self.current_tick.symbol
300
+ else:
301
+ raise ValueError("Symbol must be provided")
302
+
303
+ # 2. Determine Quantity (Default to Close Position if None)
304
+ if quantity is None:
305
+ # Default to closing the entire position for this symbol
306
+ pos = self.ctx.get_position(symbol)
307
+ if pos > 0:
308
+ quantity = pos
309
+ else:
310
+ # If no position, maybe use Sizer?
311
+ # For now, if no position and no quantity, we can't sell.
312
+ return
313
+
314
+ # 3. Execute Sell
315
+ if quantity > 0:
316
+ self.ctx.sell(symbol, quantity, price, time_in_force, trigger_price)
317
+
318
+ def short(
319
+ self,
320
+ symbol: Optional[str] = None,
321
+ quantity: Optional[float] = None,
322
+ price: Optional[float] = None,
323
+ time_in_force: Optional[TimeInForce] = None,
324
+ trigger_price: Optional[float] = None,
325
+ ):
326
+ """
327
+ 卖出开空 (Short Sell)
328
+
329
+ Args:
330
+ symbol: 标的代码 (如果不填,默认使用当前 Bar/Tick 的 symbol)
331
+ quantity: 数量 (如果不填,使用 Sizer 计算)
332
+ price: 限价 (None 为市价)
333
+ time_in_force: 订单有效期
334
+ trigger_price: 触发价 (止损/止盈)
335
+ """
336
+ if self.ctx is None:
337
+ raise RuntimeError("Context not ready")
338
+
339
+ # 1. Determine Symbol
340
+ symbol = self._resolve_symbol(symbol)
341
+
342
+ # 2. Determine Reference Price for Sizing
343
+ ref_price = price
344
+ if ref_price is None:
345
+ if self.current_bar:
346
+ ref_price = self.current_bar.close
347
+ elif self.current_tick:
348
+ ref_price = self.current_tick.price
349
+ else:
350
+ ref_price = 0.0
351
+
352
+ # 3. Determine Quantity via Sizer
353
+ if quantity is None:
354
+ quantity = self.sizer.get_size(ref_price, self.ctx.cash, self.ctx, symbol)
355
+
356
+ # 4. Execute Sell (Short)
357
+ if quantity > 0:
358
+ self.ctx.sell(symbol, quantity, price, time_in_force, trigger_price)
359
+
360
+ def cover(
361
+ self,
362
+ symbol: Optional[str] = None,
363
+ quantity: Optional[float] = None,
364
+ price: Optional[float] = None,
365
+ time_in_force: Optional[TimeInForce] = None,
366
+ trigger_price: Optional[float] = None,
367
+ ):
368
+ """
369
+ 买入平空 (Buy to Cover)
370
+
371
+ Args:
372
+ symbol: 标的代码 (如果不填,默认使用当前 Bar/Tick 的 symbol)
373
+ quantity: 数量 (如果不填,默认平掉当前标的所有空头持仓)
374
+ price: 限价 (None 为市价)
375
+ time_in_force: 订单有效期
376
+ trigger_price: 触发价 (止损/止盈)
377
+ """
378
+ if self.ctx is None:
379
+ raise RuntimeError("Context not ready")
380
+
381
+ # 1. Determine Symbol
382
+ symbol = self._resolve_symbol(symbol)
383
+
384
+ # 2. Determine Quantity (Default to Close Short Position if None)
385
+ if quantity is None:
386
+ pos = self.ctx.get_position(symbol)
387
+ if pos < 0:
388
+ quantity = abs(pos)
389
+ else:
390
+ # No short position to cover
391
+ return
392
+
393
+ # 3. Execute Buy (Cover)
394
+ if quantity > 0:
395
+ self.ctx.buy(symbol, quantity, price, time_in_force, trigger_price)
396
+
397
+ def stop_buy(
398
+ self,
399
+ symbol: Optional[str] = None,
400
+ trigger_price: float = 0.0,
401
+ quantity: Optional[float] = None,
402
+ price: Optional[float] = None,
403
+ time_in_force: Optional[TimeInForce] = None,
404
+ ):
405
+ """
406
+ 发送止损买入单 (Stop Buy Order)
407
+
408
+ 当市价上涨突破 trigger_price 时触发买入。
409
+ - 如果 price 为 None,触发后转为市价单 (Stop Market)。
410
+ - 如果 price 不为 None,触发后转为限价单 (Stop Limit)。
411
+ """
412
+ self.buy(symbol, quantity, price, time_in_force, trigger_price=trigger_price)
413
+
414
+ def stop_sell(
415
+ self,
416
+ symbol: Optional[str] = None,
417
+ trigger_price: float = 0.0,
418
+ quantity: Optional[float] = None,
419
+ price: Optional[float] = None,
420
+ time_in_force: Optional[TimeInForce] = None,
421
+ ):
422
+ """
423
+ 发送止损卖出单 (Stop Sell Order)
424
+
425
+ 当市价下跌跌破 trigger_price 时触发卖出。
426
+ - 如果 price 为 None,触发后转为市价单 (Stop Market)。
427
+ - 如果 price 不为 None,触发后转为限价单 (Stop Limit)。
428
+ """
429
+ self.sell(symbol, quantity, price, time_in_force, trigger_price=trigger_price)
430
+
431
+ def schedule(self, timestamp: int, payload: str):
432
+ """
433
+ 注册定时事件
434
+
435
+ Args:
436
+ timestamp: 触发时间戳 (Unix 纳秒)
437
+ payload: 事件携带的数据
438
+ """
439
+ if self.ctx is None:
440
+ raise RuntimeError("Context not ready")
441
+ self.ctx.schedule(timestamp, payload)
442
+
443
+ def get_position(self, symbol: Optional[str] = None) -> float:
444
+ """获取持仓"""
445
+ if symbol is None:
446
+ if self.current_bar:
447
+ symbol = self.current_bar.symbol
448
+ elif self.current_tick:
449
+ symbol = self.current_tick.symbol
450
+ else:
451
+ return 0.0
452
+ return self.ctx.get_position(symbol)
453
+
454
+ def get_cash(self) -> float:
455
+ """获取现金"""
456
+ return self.ctx.cash
457
+
458
+
459
+ class VectorizedStrategy(Strategy):
460
+ """
461
+ 向量化策略基类 (Vectorized Strategy Base Class)
462
+
463
+ 支持预计算指标的高速回测模式。
464
+ 用户应在回测前使用 Pandas/Numpy 计算好所有指标,
465
+ 然后通过本类提供的高速游标访问机制在 on_bar 中读取。
466
+ """
467
+
468
+ def __init__(self, precalculated_data: Dict[str, Dict[str, np.ndarray]]):
469
+ """
470
+ :param precalculated_data: 预计算数据字典
471
+ Structure: {symbol: {indicator_name: numpy_array}}
472
+ """
473
+ super().__init__()
474
+ self.precalc = precalculated_data
475
+ # 游标管理: {symbol: index}
476
+ self.cursors = defaultdict(int)
477
+
478
+ # 默认禁用 Python 侧历史数据缓存以提升性能
479
+ self.set_history_depth(0)
480
+
481
+ def _on_bar_event(self, bar: Bar, ctx: StrategyContext):
482
+ """Internal handler wrapping the user on_bar"""
483
+ # 1. Call standard setup (ctx, current_bar, history)
484
+ # Note: We copy logic from Strategy._on_bar_event to avoid double calling on_bar
485
+ # if we just called super()._on_bar_event(bar, ctx).
486
+ # Actually Strategy._on_bar_event calls self.on_bar(bar).
487
+
488
+ self.ctx = ctx
489
+ self.current_bar = bar
490
+
491
+ # Skip history maintenance if depth is 0 (default for VectorizedStrategy)
492
+ if self._history_depth > 0:
493
+ self._bars_history[bar.symbol].append(bar)
494
+
495
+ # 2. Call User Strategy
496
+ self.on_bar(bar)
497
+
498
+ # 3. Increment Cursor
499
+ self.cursors[bar.symbol] += 1
500
+
501
+ def get_value(self, indicator_name: str, symbol: Optional[str] = None) -> float:
502
+ """
503
+ 获取当前 Bar 对应的预计算指标值 (O(1) Access)
504
+
505
+ :param indicator_name: 指标名称 (key in precalculated_data)
506
+ :param symbol: 标的代码 (默认当前 Bar symbol)
507
+ :return: 指标值 (float) 或 NaN
508
+ """
509
+ sym = symbol or self.current_bar.symbol
510
+ idx = self.cursors[sym]
511
+
512
+ try:
513
+ return self.precalc[sym][indicator_name][idx]
514
+ except (KeyError, IndexError):
515
+ # 越界或键不存在
516
+ return np.nan
akquant/utils.py ADDED
@@ -0,0 +1,167 @@
1
+ import pandas as pd
2
+ from typing import List, Optional
3
+ from .akquant import Bar, from_arrays
4
+
5
+
6
+ def load_akshare_bar(df: pd.DataFrame, symbol: Optional[str] = None) -> List[Bar]:
7
+ r"""
8
+ 将 AKShare 返回的 DataFrame 转换为 akquant.Bar 列表
9
+
10
+ :param df: AKShare 历史行情数据
11
+ :type df: pandas.DataFrame
12
+ :param symbol: 标的代码;未提供时尝试使用 DataFrame 的“股票代码”列
13
+ :type symbol: str, optional
14
+ :return: 转换后的 Bar 对象列表
15
+ :rtype: List[Bar]
16
+ """
17
+ if df.empty:
18
+ return []
19
+
20
+ # Check for required columns
21
+ required_map = {
22
+ "日期": "timestamp",
23
+ "开盘": "open",
24
+ "最高": "high",
25
+ "最低": "low",
26
+ "收盘": "close",
27
+ "成交量": "volume",
28
+ }
29
+
30
+ # Validate columns
31
+ missing = [col for col in required_map.keys() if col not in df.columns]
32
+ if missing:
33
+ raise ValueError(f"DataFrame 缺少必要列: {missing}")
34
+
35
+ # Vectorized Preprocessing
36
+
37
+ # 1. Handle Timestamp
38
+ # Convert to datetime with error coercion (invalid dates becomes NaT)
39
+ dt_series = pd.to_datetime(df["日期"], errors="coerce")
40
+ # Fill NaT with 0 (Epoch 0) or handle appropriately
41
+ dt_series = dt_series.fillna(pd.Timestamp(0))
42
+ if dt_series.dt.tz is None:
43
+ dt_series = dt_series.dt.tz_localize("Asia/Shanghai")
44
+ dt_series = dt_series.dt.tz_convert("UTC")
45
+ timestamps = dt_series.astype("int64").tolist()
46
+
47
+ # 2. Extract numeric columns
48
+ # Use astype(float) to ensure correct type, fillna(0.0) for safety
49
+ opens = df["开盘"].fillna(0.0).astype(float).tolist()
50
+ highs = df["最高"].fillna(0.0).astype(float).tolist()
51
+ lows = df["最低"].fillna(0.0).astype(float).tolist()
52
+ closes = df["收盘"].fillna(0.0).astype(float).tolist()
53
+ volumes = df["成交量"].fillna(0.0).astype(float).tolist()
54
+
55
+ # 3. Handle Symbol
56
+ symbols_list = None
57
+ symbol_val = None
58
+
59
+ if symbol:
60
+ symbol_val = symbol
61
+ elif "股票代码" in df.columns:
62
+ # Convert to string
63
+ symbols_list = df["股票代码"].astype(str).tolist()
64
+ else:
65
+ symbol_val = "UNKNOWN"
66
+
67
+ # Call Rust extension
68
+ return from_arrays(
69
+ timestamps, opens, highs, lows, closes, volumes, symbol_val, symbols_list
70
+ )
71
+
72
+
73
+ def df_to_arrays(df: pd.DataFrame, symbol: Optional[str] = None):
74
+ r"""
75
+ 将 DataFrame 转换为用于 DataFeed.add_arrays 的数组元组
76
+
77
+ :return: (timestamps, opens, highs, lows, closes, volumes, symbol_val, symbols_list)
78
+ """
79
+ if df.empty:
80
+ return ([], [], [], [], [], [], symbol, None)
81
+
82
+ # Check for required columns
83
+ required_map = {
84
+ "日期": "timestamp",
85
+ "开盘": "open",
86
+ "最高": "high",
87
+ "最低": "low",
88
+ "收盘": "close",
89
+ "成交量": "volume",
90
+ }
91
+
92
+ # ... logic reused ...
93
+ # 为了避免重复代码,我们应该重构 load_akshare_bar
94
+ # 但为了最小化修改风险,我先把逻辑复制过来,或者调用 load_akshare_bar 内部逻辑
95
+
96
+ # 既然 load_akshare_bar 已经有了逻辑,我们把它提取出来
97
+
98
+ # 1. Handle Timestamp
99
+ dt_series = pd.to_datetime(df["日期"], errors="coerce")
100
+ dt_series = dt_series.fillna(pd.Timestamp(0))
101
+ if dt_series.dt.tz is None:
102
+ dt_series = dt_series.dt.tz_localize("Asia/Shanghai")
103
+ dt_series = dt_series.dt.tz_convert("UTC")
104
+ timestamps = dt_series.astype("int64").values
105
+
106
+ # 2. Extract numeric columns
107
+ opens = df["开盘"].fillna(0.0).astype(float).values
108
+ highs = df["最高"].fillna(0.0).astype(float).values
109
+ lows = df["最低"].fillna(0.0).astype(float).values
110
+ closes = df["收盘"].fillna(0.0).astype(float).values
111
+ volumes = df["成交量"].fillna(0.0).astype(float).values
112
+
113
+ # 3. Handle Symbol
114
+ symbols_list = None
115
+ symbol_val = None
116
+
117
+ if symbol:
118
+ symbol_val = symbol
119
+ elif "股票代码" in df.columns:
120
+ symbols_list = df["股票代码"].astype(str).tolist() # Strings still need list or object array
121
+ else:
122
+ symbol_val = "UNKNOWN"
123
+
124
+ return (timestamps, opens, highs, lows, closes, volumes, symbol_val, symbols_list)
125
+
126
+
127
+
128
+ def prepare_dataframe(
129
+ df: pd.DataFrame, date_col: Optional[str] = None, tz: str = "Asia/Shanghai"
130
+ ) -> pd.DataFrame:
131
+ r"""
132
+ 自动预处理 DataFrame,处理时区并生成标准时间戳列
133
+
134
+ :param df: 输入 DataFrame
135
+ :param date_col: 日期列名 (若为 None 则自动探测)
136
+ :param tz: 默认时区 (若数据为 Naive 时间,则假定为此时区)
137
+ :return: 处理后的 DataFrame (包含 'timestamp' 列)
138
+ """
139
+ df = df.copy()
140
+
141
+ # 1. Auto-detect date column
142
+ if date_col is None:
143
+ candidates = ["date", "datetime", "time", "timestamp", "日期", "时间"]
144
+ for c in candidates:
145
+ if c in df.columns:
146
+ date_col = c
147
+ break
148
+
149
+ if date_col and date_col in df.columns:
150
+ # 2. Convert to datetime
151
+ dt = pd.to_datetime(df[date_col], errors="coerce")
152
+
153
+ # 3. Handle Timezone
154
+ if dt.dt.tz is None:
155
+ dt = dt.dt.tz_localize(tz)
156
+
157
+ # 4. Convert to UTC
158
+ dt = dt.dt.tz_convert("UTC")
159
+
160
+ # 5. Assign back
161
+ df[date_col] = dt
162
+ df["timestamp"] = dt.astype("int64")
163
+ else:
164
+ # Warn or ignore? For now silent, user might be processing non-time data?
165
+ pass
166
+
167
+ return df