prism-quant 1.0.3__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 (49) hide show
  1. prism_quant/__init__.py +38 -0
  2. prism_quant/adapters/__init__.py +3 -0
  3. prism_quant/adapters/data/__init__.py +4 -0
  4. prism_quant/adapters/data/miniqmt_gateway.py +360 -0
  5. prism_quant/adapters/data/yfinance_gateway.py +177 -0
  6. prism_quant/adapters/trade/__init__.py +5 -0
  7. prism_quant/adapters/trade/miniqmt_client.py +237 -0
  8. prism_quant/adapters/trade/miniqmt_gateway.py +121 -0
  9. prism_quant/adapters/trade/paper_gateway.py +31 -0
  10. prism_quant/backtest/__init__.py +32 -0
  11. prism_quant/backtest/engine.py +182 -0
  12. prism_quant/backtest/metrics.py +74 -0
  13. prism_quant/backtest/performance.py +394 -0
  14. prism_quant/charts/__init__.py +14 -0
  15. prism_quant/charts/performance.py +320 -0
  16. prism_quant/charts/price.py +64 -0
  17. prism_quant/charts/signals.py +181 -0
  18. prism_quant/core/__init__.py +14 -0
  19. prism_quant/core/base.py +20 -0
  20. prism_quant/core/config.py +80 -0
  21. prism_quant/core/logger.py +44 -0
  22. prism_quant/data/__init__.py +14 -0
  23. prism_quant/data/cleaner.py +39 -0
  24. prism_quant/data/fetcher.py +99 -0
  25. prism_quant/data/miniqmt_xtdata.py +101 -0
  26. prism_quant/data/source_map.py +25 -0
  27. prism_quant/data/storage.py +247 -0
  28. prism_quant/indicators/__init__.py +14 -0
  29. prism_quant/indicators/fundamental.py +45 -0
  30. prism_quant/indicators/talib_indicators.py +67 -0
  31. prism_quant/indicators/technical.py +251 -0
  32. prism_quant/live/__init__.py +28 -0
  33. prism_quant/live/engine.py +567 -0
  34. prism_quant/live/event_engine.py +19 -0
  35. prism_quant/live/gateway_registry.py +27 -0
  36. prism_quant/live/gateways.py +75 -0
  37. prism_quant/live/models.py +24 -0
  38. prism_quant/strategy/__init__.py +12 -0
  39. prism_quant/strategy/base.py +31 -0
  40. prism_quant/strategy/signals.py +190 -0
  41. prism_quant/trader/__init__.py +14 -0
  42. prism_quant/trader/engine.py +197 -0
  43. prism_quant/trader/order_manager.py +106 -0
  44. prism_quant/trader/position_manager.py +88 -0
  45. prism_quant-1.0.3.dist-info/METADATA +217 -0
  46. prism_quant-1.0.3.dist-info/RECORD +49 -0
  47. prism_quant-1.0.3.dist-info/WHEEL +5 -0
  48. prism_quant-1.0.3.dist-info/licenses/LICENSE +21 -0
  49. prism_quant-1.0.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,38 @@
1
+ """
2
+ PrismQuant - A comprehensive Python quantitative finance library.
3
+
4
+ This library provides tools for strategy development, backtesting,
5
+ paper trading, and live trading.
6
+ """
7
+
8
+ import os
9
+ from pathlib import Path
10
+
11
+ # Read version from VERSION file
12
+ _version_file = Path(__file__).parent.parent / "VERSION"
13
+ if _version_file.exists():
14
+ __version__ = _version_file.read_text().strip()
15
+ else:
16
+ __version__ = "0.9.1"
17
+
18
+ __author__ = "PrismQuant"
19
+
20
+ # Import core modules
21
+ from . import core
22
+ from . import data
23
+ from . import strategy
24
+ from . import backtest
25
+ from . import indicators
26
+ from . import trader
27
+ from . import live
28
+
29
+ __all__ = [
30
+ "core",
31
+ "data",
32
+ "strategy",
33
+ "backtest",
34
+ "indicators",
35
+ "trader",
36
+ "live"
37
+ ]
38
+
@@ -0,0 +1,3 @@
1
+ """
2
+ Adapter implementations for data and trade gateways.
3
+ """
@@ -0,0 +1,4 @@
1
+ from .miniqmt_gateway import MiniQmtDataGateway
2
+ from .yfinance_gateway import YFinanceDataGateway
3
+
4
+ __all__ = ["YFinanceDataGateway", "MiniQmtDataGateway"]
@@ -0,0 +1,360 @@
1
+ """
2
+ miniQMT 行情(xtdata),类 MiniQmtDataGateway。
3
+
4
+ 对外
5
+ connect 加载 xtdata,需 miniQMT 已开
6
+ subscribe 追加标的并 1m 暖机回放
7
+ start 开 daemon:poll 轮询或 push 订分笔
8
+ stop 停线程,push 会退订
9
+ get_today_ohlc 当日开高低(从快照解析)
10
+ get_depths 买卖盘口(从快照解析)
11
+
12
+ 私有
13
+ _warm_up 近一日 1m 合成暖机 tick
14
+ _unsubscribe_push push 停时退订
15
+ _run_poll 按间隔拉全快照轮询
16
+ _run_push 订分笔并阻塞 run
17
+ _on_push_datas 分笔回调里组 TickData
18
+ _get_full_tick 封装 get_full_tick
19
+ _bid_ask_from_dict 快照 dict 取买一卖一
20
+ _ts_from_millis_or_now 行情时间转 datetime
21
+ """
22
+ import threading
23
+ import time
24
+ from datetime import datetime, timedelta
25
+ from typing import Any, Dict, List, Optional, Tuple
26
+
27
+ from ...data.miniqmt_xtdata import fetch_miniqmt_bars, import_xtdata as _import_xtdata
28
+ from ...live.gateways import DataGateway
29
+ from ...live.models import TickData
30
+
31
+
32
+ class MiniQmtDataGateway(DataGateway):
33
+ """poll 定时拉全快照,push 订分笔推送;xt 标的代码,Tick 含最新价及可选买卖盘。"""
34
+
35
+ def __init__(
36
+ self,
37
+ interval: float = 3.0,
38
+ dividend_type: str = "none",
39
+ mode: str = "poll",
40
+ **kwargs: Any,
41
+ ) -> None:
42
+ """轮询间隔秒、K 线除权类型、模式 poll 或 push。"""
43
+ super().__init__(**kwargs)
44
+ self.interval = interval
45
+ self.dividend_type = dividend_type
46
+ self.mode = (mode or "poll").strip().lower()
47
+ if self.mode not in ("poll", "push"):
48
+ raise ValueError('mode must be "poll" or "push"')
49
+ self._symbols: List[str] = []
50
+ self._running = False
51
+ self._thread: Optional[threading.Thread] = None
52
+ self._quote_seqs: List[int] = []
53
+ self.logger.info(f"Initialized MiniQmtDataGateway mode={self.mode} interval={self.interval}s")
54
+
55
+ def connect(self) -> bool:
56
+ """加载 xtdata,本机需已启动 miniQMT。"""
57
+ try:
58
+ _import_xtdata()
59
+ self.logger.info("xtquant xtdata loaded (ensure miniQMT is running)")
60
+ return True
61
+ except Exception as e:
62
+ self.logger.error(f"miniQMT connect failed: {e}")
63
+ return False
64
+
65
+ def subscribe(self, symbols: List[str]) -> bool:
66
+ """追加订阅;新标的用近一日 1m K 线逐根暖机回调。"""
67
+ new_symbols = [s for s in symbols if s not in self._symbols]
68
+ for symbol in new_symbols:
69
+ self._symbols.append(symbol)
70
+ self._warm_up(symbol)
71
+ return True
72
+
73
+ def start(self) -> None:
74
+ """起后台线程:poll 轮询快照,push 订分笔并跑 xtdata.run。"""
75
+ if self._running:
76
+ return
77
+ self._running = True
78
+ if self.mode == "poll":
79
+ self.logger.info("Starting miniQMT poll loop")
80
+ self._thread = threading.Thread(target=self._run_poll, daemon=True)
81
+ else:
82
+ self.logger.info("Starting miniQMT subscribe_quote + xtdata.run()")
83
+ self._thread = threading.Thread(target=self._run_push, daemon=True)
84
+ self._thread.start()
85
+
86
+ def stop(self) -> None:
87
+ """停线程;push 会退订并调 stop(若有);join 等线程结束。"""
88
+ self._running = False
89
+ if self.mode == "push":
90
+ self._unsubscribe_push()
91
+ if self._thread:
92
+ self._thread.join(timeout=5.0)
93
+ self._thread = None
94
+ self.logger.info(f"Stopped MiniQmtDataGateway ({self.mode})")
95
+
96
+ def get_today_ohlc(self, symbol: str) -> Optional[Dict[str, float]]:
97
+ """从快照取当日开、高、低三个 float;缺或错返回 None。"""
98
+ tick, err = self._get_full_tick(symbol)
99
+ if err or not tick:
100
+ self.logger.warning(f"get_today_ohlc: {err}")
101
+ return None
102
+ try:
103
+ o = tick.get("open")
104
+ h = tick.get("high") or tick.get("highPrice")
105
+ l_ = tick.get("low") or tick.get("lowPrice")
106
+ if o is None or h is None or l_ is None:
107
+ return None
108
+ return {"open": float(o), "high": float(h), "low": float(l_)}
109
+ except Exception as e:
110
+ self.logger.error(f"get_today_ohlc parse error: {e}")
111
+ return None
112
+
113
+ def get_depths(self, symbol: str, levels: int = 5) -> Dict[str, List[Dict[str, float]]]:
114
+ """返回买卖盘口深度(价格+委托量)。"""
115
+ tick, err = self._get_full_tick(symbol)
116
+ if err or not tick:
117
+ self.logger.debug(f"get_depths {symbol}: {err}")
118
+ return {"bids": [], "asks": []}
119
+ lv = max(1, min(int(levels), 10))
120
+ bids: List[Dict[str, float]] = []
121
+ asks: List[Dict[str, float]] = []
122
+
123
+ for i in range(1, lv + 1):
124
+ bp = self._level_value(tick, "bid", "price", i)
125
+ bv = self._level_value(tick, "bid", "volume", i)
126
+ ap = self._level_value(tick, "ask", "price", i)
127
+ av = self._level_value(tick, "ask", "volume", i)
128
+ if bp is not None:
129
+ bids.append({"level": float(i), "price": bp, "volume": float(bv or 0.0)})
130
+ if ap is not None:
131
+ asks.append({"level": float(i), "price": ap, "volume": float(av or 0.0)})
132
+ return {"bids": bids, "asks": asks}
133
+
134
+ # ---------- 私有 ----------
135
+
136
+ def _warm_up(self, symbol: str) -> None:
137
+ """近一日 1m 收盘合成暖机 tick,来源标记 miniqmt_warmup。"""
138
+ self.logger.debug(f"Warming up {symbol} with miniQMT 1m history...")
139
+ try:
140
+ end = datetime.now()
141
+ start = end - timedelta(days=1)
142
+ data = fetch_miniqmt_bars(
143
+ symbol,
144
+ start.strftime("%Y-%m-%d"),
145
+ None,
146
+ interval="1m",
147
+ dividend_type=self.dividend_type,
148
+ )
149
+ if data.empty:
150
+ self.logger.warning(f"No warm-up data for {symbol}")
151
+ return
152
+ pushed = 0
153
+ for timestamp, row in data.iterrows():
154
+ ts = timestamp.to_pydatetime() if hasattr(timestamp, "to_pydatetime") else timestamp
155
+ if getattr(ts, "tzinfo", None) is not None:
156
+ ts = ts.replace(tzinfo=None)
157
+ price = float(row["Close"])
158
+ volume = int(row["Volume"])
159
+ tick = TickData(
160
+ symbol=symbol,
161
+ price=price,
162
+ timestamp=ts,
163
+ volume=volume,
164
+ source="miniqmt_warmup",
165
+ )
166
+ if self._tick_handler:
167
+ self._tick_handler(tick)
168
+ pushed += 1
169
+ self.logger.info(f"Subscribed & warmed up {symbol} ({pushed} bars)")
170
+ except Exception as e:
171
+ self.logger.warning(f"Warm-up failed for {symbol}: {e}")
172
+
173
+ def _unsubscribe_push(self) -> None:
174
+ """push 停时逐个退订 quote,再调 xtdata.stop(有则调)。"""
175
+ if not self._quote_seqs:
176
+ return
177
+ try:
178
+ xd = _import_xtdata()
179
+ for seq in self._quote_seqs:
180
+ try:
181
+ xd.unsubscribe_quote(seq)
182
+ except Exception as e:
183
+ self.logger.debug(f"unsubscribe_quote {seq}: {e}")
184
+ self._quote_seqs.clear()
185
+ stop_fn = getattr(xd, "stop", None)
186
+ if callable(stop_fn):
187
+ stop_fn()
188
+ except Exception as e:
189
+ self.logger.warning(f"push cleanup: {e}")
190
+
191
+ def _run_poll(self) -> None:
192
+ """对每个标的拉全快照,组 TickData,调 tick 回调。"""
193
+ while self._running:
194
+ for symbol in self._symbols:
195
+ tick, err = self._get_full_tick(symbol)
196
+ if err or not tick:
197
+ self.logger.debug(f"tick skip {symbol}: {err}")
198
+ continue
199
+ try:
200
+ last = tick.get("lastPrice") or tick.get("last") or tick.get("price")
201
+ vol = tick.get("volume") or tick.get("lastVolume") or 0
202
+ if last is None:
203
+ continue
204
+ ts = self._ts_from_millis_or_now(tick.get("time"))
205
+ bid, ask = self._bid_ask_from_dict(tick)
206
+ t = TickData(
207
+ symbol=symbol,
208
+ price=float(last),
209
+ timestamp=ts,
210
+ volume=int(vol) if vol is not None else None,
211
+ source="miniqmt",
212
+ bid=bid,
213
+ ask=ask,
214
+ )
215
+ if self._tick_handler:
216
+ self._tick_handler(t)
217
+ except Exception as e:
218
+ self.logger.error(f"Error polling {symbol}: {e}")
219
+ time.sleep(self.interval)
220
+
221
+ def _run_push(self) -> None:
222
+ """等标的有列表后逐只 subscribe_quote,最后阻塞 xd.run。"""
223
+ # 启动可能早于订阅,先空转等到标的非空。
224
+ while self._running and not self._symbols:
225
+ time.sleep(0.1)
226
+ if not self._running:
227
+ return
228
+ xd = _import_xtdata()
229
+ self._quote_seqs = []
230
+ for symbol in list(self._symbols):
231
+ if not self._running:
232
+ break
233
+ seq = xd.subscribe_quote(
234
+ symbol,
235
+ period="tick",
236
+ start_time="",
237
+ end_time="",
238
+ count=0,
239
+ callback=self._on_push_datas,
240
+ )
241
+ if seq < 0:
242
+ self.logger.error(f"subscribe_quote failed {symbol}: {seq}")
243
+ continue
244
+ self._quote_seqs.append(seq)
245
+ if not self._running or not self._quote_seqs:
246
+ return
247
+ try:
248
+ xd.run()
249
+ except Exception as e:
250
+ if self._running:
251
+ self.logger.error(f"xtdata.run: {e}")
252
+
253
+ def _on_push_datas(self, datas: dict) -> None:
254
+ """分笔推送回调:行转 TickData 再交 tick 回调。"""
255
+ if not self._running:
256
+ return
257
+ for code, rows in (datas or {}).items():
258
+ for row in rows or []:
259
+ if not isinstance(row, dict):
260
+ continue
261
+ last = row.get("lastPrice") or row.get("last_price") or row.get("price")
262
+ if last is None:
263
+ continue
264
+ vol = row.get("volume") or row.get("lastVolume")
265
+ ts = self._ts_from_millis_or_now(row.get("time"))
266
+ bid, ask = self._bid_ask_from_dict(row)
267
+ t = TickData(
268
+ symbol=code,
269
+ price=float(last),
270
+ timestamp=ts,
271
+ volume=int(vol) if vol is not None else None,
272
+ source="miniqmt_push",
273
+ bid=bid,
274
+ ask=ask,
275
+ )
276
+ if self._tick_handler:
277
+ self._tick_handler(t)
278
+
279
+ def _get_full_tick(self, symbol: str) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
280
+ """调 get_full_tick;成功返回快照和 None,失败返回 None 和错误说明。"""
281
+ try:
282
+ xtdata = _import_xtdata()
283
+ data = xtdata.get_full_tick([symbol])
284
+ if not data or symbol not in data:
285
+ return None, f"{symbol} 无快照"
286
+ return data[symbol], None
287
+ except Exception as e:
288
+ return None, str(e)
289
+
290
+ @staticmethod
291
+ def _bid_ask_from_dict(d: Dict[str, Any]) -> Tuple[Optional[float], Optional[float]]:
292
+ """从行情 dict 取买一卖一;若买价大于卖价则对调一次。"""
293
+ def _to_f(v: Any) -> Optional[float]:
294
+ if v is None:
295
+ return None
296
+ if isinstance(v, (list, tuple)) and len(v) > 0:
297
+ v = v[0]
298
+ try:
299
+ return float(v)
300
+ except (TypeError, ValueError):
301
+ return None
302
+
303
+ bid = d.get("bid1") or d.get("bidPrice") or d.get("bid") or d.get("bidPx")
304
+ ask = d.get("ask1") or d.get("askPrice") or d.get("ask") or d.get("askPx")
305
+ b, a = _to_f(bid), _to_f(ask)
306
+ if b is not None and a is not None and b > a:
307
+ return a, b
308
+ return b, a
309
+
310
+ @classmethod
311
+ def _level_value(cls, d: Dict[str, Any], side: str, kind: str, idx: int) -> Optional[float]:
312
+ """取指定档位字段,兼容数组字段和逐档字段。"""
313
+ if side == "bid":
314
+ arr_keys = ["bidPrice", "bid", "bidPx"] if kind == "price" else ["bidVol", "bidVolume", "bidQty"]
315
+ scalar_keys = (
316
+ [f"bid{idx}", f"bidPrice{idx}", f"bidPx{idx}"]
317
+ if kind == "price"
318
+ else [f"bidVol{idx}", f"bidVolume{idx}", f"bidQty{idx}"]
319
+ )
320
+ else:
321
+ arr_keys = ["askPrice", "ask", "askPx"] if kind == "price" else ["askVol", "askVolume", "askQty"]
322
+ scalar_keys = (
323
+ [f"ask{idx}", f"askPrice{idx}", f"askPx{idx}"]
324
+ if kind == "price"
325
+ else [f"askVol{idx}", f"askVolume{idx}", f"askQty{idx}"]
326
+ )
327
+
328
+ for key in arr_keys:
329
+ arr = d.get(key)
330
+ if isinstance(arr, (list, tuple)) and len(arr) >= idx:
331
+ return cls._to_float(arr[idx - 1])
332
+ for key in scalar_keys:
333
+ if key in d:
334
+ return cls._to_float(d.get(key))
335
+ return None
336
+
337
+ @staticmethod
338
+ def _to_float(v: Any) -> Optional[float]:
339
+ """将任意值转为 float,失败返回 None。"""
340
+ if v is None:
341
+ return None
342
+ if isinstance(v, (list, tuple)) and len(v) > 0:
343
+ v = v[0]
344
+ try:
345
+ return float(v)
346
+ except (TypeError, ValueError):
347
+ return None
348
+
349
+ @staticmethod
350
+ def _ts_from_millis_or_now(raw: Any) -> datetime:
351
+ """时间戳毫秒太长则按毫秒除;坏了或没有就用本机当前时间。"""
352
+ if raw is None:
353
+ return datetime.now().replace(tzinfo=None)
354
+ try:
355
+ n = int(raw)
356
+ if n > 10**12:
357
+ n = n // 1000
358
+ return datetime.fromtimestamp(n)
359
+ except (TypeError, ValueError, OSError):
360
+ return datetime.now().replace(tzinfo=None)
@@ -0,0 +1,177 @@
1
+ """
2
+ yfinance 行情,类 YFinanceDataGateway。
3
+
4
+ 对外
5
+ connect 探测网络与 yfinance 可用性
6
+ subscribe 追加标的并 1m 暖机回放
7
+ start 开 daemon 线程轮询 fast_info
8
+ stop 停线程
9
+ get_today_ohlc 当日开高低(fast_info)
10
+ get_depths 合成盘口(价由 last_price 铺档,量随机且尺度参考 last_volume)
11
+
12
+ 私有
13
+ _warm_up 近一日 1m K 线逐根暖机 tick
14
+ _run 主循环:按 interval 拉 last_price / last_volume
15
+ """
16
+ import random
17
+ import threading
18
+ import time
19
+ from datetime import datetime
20
+ from typing import Any, Dict, List, Optional
21
+
22
+ import yfinance as yf
23
+
24
+ from ...live.gateways import DataGateway
25
+ from ...live.models import TickData
26
+
27
+
28
+ class YFinanceDataGateway(DataGateway):
29
+ """轮询 yfinance fast_info;时间戳为 naive UTC;无真实 Level2。"""
30
+
31
+ def __init__(self, interval: float = 60.0, **kwargs: Any) -> None:
32
+ """轮询间隔(秒)。"""
33
+ super().__init__(**kwargs)
34
+ self.interval = interval
35
+ self._symbols: List[str] = []
36
+ self._running = False
37
+ self._thread: Optional[threading.Thread] = None
38
+ self.logger.info(f"Initialized YFinanceDataGateway with interval: {self.interval}s")
39
+
40
+ def connect(self) -> bool:
41
+ """验证网络与 yfinance 可访问。"""
42
+ try:
43
+ yf.Ticker("AAPL").fast_info
44
+ self.logger.info("Connected to yfinance")
45
+ return True
46
+ except Exception as e:
47
+ self.logger.error(f"Failed to connect: {e}")
48
+ return False
49
+
50
+ def subscribe(self, symbols: List[str]) -> bool:
51
+ """追加订阅;新标的拉近一日 1m 数据暖机并逐根回调。"""
52
+ new_symbols = [s for s in symbols if s not in self._symbols]
53
+ for symbol in new_symbols:
54
+ self._symbols.append(symbol)
55
+ self._warm_up(symbol)
56
+ return True
57
+
58
+ def start(self) -> None:
59
+ """启动轮询线程。"""
60
+ if self._running:
61
+ return
62
+ self.logger.info("Starting yfinance polling")
63
+ self._running = True
64
+ self._thread = threading.Thread(target=self._run, daemon=True)
65
+ self._thread.start()
66
+
67
+ def stop(self) -> None:
68
+ """停止轮询线程。"""
69
+ self._running = False
70
+ if self._thread:
71
+ self._thread.join(timeout=2)
72
+ self.logger.info("Stopped yfinance polling")
73
+
74
+ def get_today_ohlc(self, symbol: str) -> Optional[Dict[str, float]]:
75
+ """从 fast_info 取当日开、高、低;缺字段返回 None。"""
76
+ try:
77
+ ticker = yf.Ticker(symbol)
78
+ info = ticker.fast_info
79
+ open_price = info.open
80
+ high_price = info.day_high
81
+ low_price = info.day_low
82
+ if open_price is None or high_price is None or low_price is None:
83
+ self.logger.warning(
84
+ f"Incomplete OHLC data for {symbol}: open={open_price}, high={high_price}, low={low_price}"
85
+ )
86
+ return None
87
+ return {"open": open_price, "high": high_price, "low": low_price}
88
+ except Exception as e:
89
+ self.logger.error(f"Failed to get today's OHLC for {symbol}: {e}")
90
+ return None
91
+
92
+ def get_depths(self, symbol: str, levels: int = 5) -> Dict[str, List[Dict[str, float]]]:
93
+ """
94
+ 合成盘口(非真实 L2):价由 fast_info.last_price 按相对价差铺档;
95
+ 各档成交量在 [~5%·last_volume, last_volume] 内独立随机(无 last_volume 时用默认尺度)。
96
+ """
97
+ levels = max(1, min(int(levels), 10))
98
+ try:
99
+ info = yf.Ticker(symbol).fast_info
100
+ last = info.last_price
101
+ if last is None:
102
+ return {"bids": [], "asks": []}
103
+ last_f = float(last)
104
+ if last_f <= 0:
105
+ return {"bids": [], "asks": []}
106
+ vol_raw = info.last_volume
107
+ base = int(vol_raw) if vol_raw is not None and int(vol_raw) > 0 else 1000
108
+ except Exception as e:
109
+ self.logger.debug(f"get_depths {symbol}: {e}")
110
+ return {"bids": [], "asks": []}
111
+
112
+ lo = max(1, base // 20)
113
+ hi = max(lo, base)
114
+ step = max(last_f * 1e-4, 0.01)
115
+ bids: List[Dict[str, float]] = []
116
+ asks: List[Dict[str, float]] = []
117
+ for i in range(levels):
118
+ lv = float(i + 1)
119
+ vb = float(random.randint(lo, hi))
120
+ va = float(random.randint(lo, hi))
121
+ bids.append({"level": lv, "price": round(last_f - lv * step, 6), "volume": vb})
122
+ asks.append({"level": lv, "price": round(last_f + lv * step, 6), "volume": va})
123
+ return {"bids": bids, "asks": asks}
124
+
125
+ # ---------- 私有 ----------
126
+
127
+ def _warm_up(self, symbol: str) -> None:
128
+ """下载近一日 1m K 线,逐根合成暖机 tick(source=yf_warmup)。"""
129
+ self.logger.debug(f"Warming up {symbol} with intraday history...")
130
+ try:
131
+ data = yf.download(symbol, period="1d", interval="1m", progress=False)
132
+ if data.empty:
133
+ self.logger.warning(f"No warm-up data for {symbol}")
134
+ return
135
+ pushed_count = 0
136
+ for timestamp, row in data.iterrows():
137
+ local_ts = timestamp.to_pydatetime().replace(tzinfo=None)
138
+ price = float(row["Close"])
139
+ volume = int(row["Volume"])
140
+ tick = TickData(
141
+ symbol=symbol,
142
+ price=price,
143
+ timestamp=local_ts,
144
+ volume=volume,
145
+ source="yf_warmup",
146
+ )
147
+ if self._tick_handler:
148
+ self._tick_handler(tick)
149
+ pushed_count += 1
150
+ self.logger.info(f"Subscribed & Warmed up {symbol} ({pushed_count} bars)")
151
+ except Exception as e:
152
+ self.logger.warning(f"Warm-up failed for {symbol}: {e}")
153
+
154
+ def _run(self) -> None:
155
+ """轮询各标的 fast_info,组 TickData 回调。"""
156
+ while self._running:
157
+ for symbol in self._symbols:
158
+ try:
159
+ ticker = yf.Ticker(symbol)
160
+ info = ticker.fast_info
161
+ price = info.last_price
162
+ volume = info.last_volume
163
+ if price is None or volume is None:
164
+ continue
165
+ tick = TickData(
166
+ symbol=symbol,
167
+ price=float(price),
168
+ timestamp=datetime.utcnow(),
169
+ volume=int(volume),
170
+ source="yfinance",
171
+ )
172
+ if self._tick_handler:
173
+ self._tick_handler(tick)
174
+ except Exception as e:
175
+ self.logger.error(f"Error fetching data for {symbol}: {str(e)}")
176
+ continue
177
+ time.sleep(self.interval)
@@ -0,0 +1,5 @@
1
+ from .miniqmt_client import MiniQmtXtTraderClient
2
+ from .miniqmt_gateway import MiniQmtTradeGateway
3
+ from .paper_gateway import PaperTradeGateway
4
+
5
+ __all__ = ["MiniQmtTradeGateway", "MiniQmtXtTraderClient", "PaperTradeGateway"]