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,839 @@
1
+ """
2
+ 实时价格跟踪脚本
3
+
4
+ 通过 WebSocket 实时跟踪当前价格数据,并向前追溯 n 个周期,
5
+ 为实时计算 signal 和计算 strategy 做准备。
6
+
7
+ 使用方法:
8
+ # 方式1: 作为模块运行(推荐)
9
+ cd /Users/user/Desktop/repo/cyqnt_trd
10
+ python -m cyqnt_trd.test_script.realtime_price_tracker
11
+
12
+ # 方式2: 直接运行脚本
13
+ cd /Users/user/Desktop/repo/cyqnt_trd
14
+ python cyqnt_trd/test_script/realtime_price_tracker.py
15
+ """
16
+
17
+ import os
18
+ import sys
19
+ import asyncio
20
+ import logging
21
+ import ssl
22
+ import pandas as pd
23
+ from datetime import datetime
24
+ from typing import Optional, Callable, Dict, Any
25
+ from collections import deque
26
+
27
+ # 添加项目根目录到路径
28
+ current_dir = os.path.dirname(os.path.abspath(__file__))
29
+ project_root = os.path.dirname(os.path.dirname(current_dir))
30
+ if project_root not in sys.path:
31
+ sys.path.insert(0, project_root)
32
+
33
+ # 导入 Binance SDK
34
+ try:
35
+ from binance_sdk_derivatives_trading_usds_futures.derivatives_trading_usds_futures import (
36
+ DerivativesTradingUsdsFutures,
37
+ ConfigurationRestAPI,
38
+ ConfigurationWebSocketStreams,
39
+ DERIVATIVES_TRADING_USDS_FUTURES_REST_API_PROD_URL,
40
+ DERIVATIVES_TRADING_USDS_FUTURES_WS_STREAMS_PROD_URL,
41
+ )
42
+ from binance_sdk_derivatives_trading_usds_futures.rest_api.models import (
43
+ KlineCandlestickDataIntervalEnum,
44
+ )
45
+ except ImportError as e:
46
+ print(f"导入错误: {e}")
47
+ print("\n提示:请确保已安装 binance-connector-python")
48
+ sys.exit(1)
49
+
50
+ # Configure logging
51
+ logging.basicConfig(
52
+ level=logging.INFO,
53
+ format='%(asctime)s - %(levelname)s - %(message)s'
54
+ )
55
+
56
+
57
+ class RealtimePriceTracker:
58
+ """
59
+ 实时价格跟踪器
60
+
61
+ 通过 WebSocket 实时接收价格数据,并维护一个包含历史 n 个周期的 DataFrame,
62
+ 为实时计算 signal 和 strategy 做准备。
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ symbol: str,
68
+ interval: str = "1m",
69
+ lookback_periods: int = 100,
70
+ market_type: str = "futures",
71
+ ssl_verify: bool = True
72
+ ):
73
+ """
74
+ 初始化实时价格跟踪器
75
+
76
+ Args:
77
+ symbol: 交易对符号,例如 'BTCUSDT', 'ETHUSDT'
78
+ interval: 时间间隔,例如 '1m', '5m', '1h', '1d'
79
+ 可选值: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M
80
+ lookback_periods: 向前追溯的周期数(默认100)
81
+ market_type: 市场类型,'futures' 或 'spot'(默认 'futures')
82
+ ssl_verify: 是否验证 SSL 证书(默认 True)。如果遇到 SSL 证书验证错误,可以设置为 False(仅用于开发/测试)
83
+ """
84
+ self.symbol = symbol.upper()
85
+ self.interval = interval
86
+ self.lookback_periods = lookback_periods
87
+ self.market_type = market_type
88
+ self.ssl_verify = ssl_verify
89
+
90
+ # 数据存储
91
+ self.data_df: Optional[pd.DataFrame] = None
92
+ self.latest_kline: Optional[Dict[str, Any]] = None
93
+
94
+ # WebSocket 连接
95
+ self.connection = None
96
+ self.stream = None
97
+ self.is_running = False
98
+
99
+ # 回调函数
100
+ self.on_new_kline_callbacks: list = []
101
+ self.on_data_updated_callbacks: list = []
102
+
103
+ # 初始化 REST API 客户端(用于获取历史数据)
104
+ if market_type == "futures":
105
+ self.rest_config = ConfigurationRestAPI(
106
+ api_key=os.getenv("API_KEY", "KB6hxLqPAvkV8DBJq6xY1tnyXR7bLxPbCQMX6zjUMwQbrujdfKlShgJ9uGQqPsrn"),
107
+ api_secret=os.getenv("API_SECRET", "Gv7l5ht1nyfl3Npw4q4zaT4FWPGCAOiSw8EldeSTXdQUQrsxLlE22Yi5ttoj9eaD"),
108
+ base_path=os.getenv(
109
+ "BASE_PATH", DERIVATIVES_TRADING_USDS_FUTURES_REST_API_PROD_URL
110
+ ),
111
+ )
112
+ self.rest_client = DerivativesTradingUsdsFutures(config_rest_api=self.rest_config)
113
+ else:
114
+ raise NotImplementedError("目前只支持期货市场")
115
+
116
+ # 初始化 WebSocket 客户端
117
+ # 配置 SSL 上下文
118
+ ssl_context = None
119
+ if not ssl_verify:
120
+ # 创建不验证证书的 SSL 上下文(仅用于开发/测试)
121
+ ssl_context = ssl.create_default_context()
122
+ ssl_context.check_hostname = False
123
+ ssl_context.verify_mode = ssl.CERT_NONE
124
+ logging.warning("SSL 证书验证已禁用(仅用于开发/测试环境)")
125
+
126
+ self.ws_config = ConfigurationWebSocketStreams(
127
+ stream_url=os.getenv(
128
+ "STREAM_URL", DERIVATIVES_TRADING_USDS_FUTURES_WS_STREAMS_PROD_URL
129
+ ),
130
+ https_agent=ssl_context
131
+ )
132
+ self.ws_client = DerivativesTradingUsdsFutures(config_ws_streams=self.ws_config)
133
+
134
+ # 间隔映射
135
+ self.interval_map = {
136
+ "1m": KlineCandlestickDataIntervalEnum.INTERVAL_1m,
137
+ "3m": KlineCandlestickDataIntervalEnum.INTERVAL_3m,
138
+ "5m": KlineCandlestickDataIntervalEnum.INTERVAL_5m,
139
+ "15m": KlineCandlestickDataIntervalEnum.INTERVAL_15m,
140
+ "30m": KlineCandlestickDataIntervalEnum.INTERVAL_30m,
141
+ "1h": KlineCandlestickDataIntervalEnum.INTERVAL_1h,
142
+ "2h": KlineCandlestickDataIntervalEnum.INTERVAL_2h,
143
+ "4h": KlineCandlestickDataIntervalEnum.INTERVAL_4h,
144
+ "6h": KlineCandlestickDataIntervalEnum.INTERVAL_6h,
145
+ "8h": KlineCandlestickDataIntervalEnum.INTERVAL_8h,
146
+ "12h": KlineCandlestickDataIntervalEnum.INTERVAL_12h,
147
+ "1d": KlineCandlestickDataIntervalEnum.INTERVAL_1d,
148
+ "3d": KlineCandlestickDataIntervalEnum.INTERVAL_3d,
149
+ "1w": KlineCandlestickDataIntervalEnum.INTERVAL_1w,
150
+ "1M": KlineCandlestickDataIntervalEnum.INTERVAL_1M,
151
+ }
152
+
153
+ if interval not in self.interval_map:
154
+ raise ValueError(f"不支持的间隔: {interval}")
155
+
156
+ def _kline_to_dict(self, kline_data: list) -> Dict[str, Any]:
157
+ """
158
+ 将 K 线数据转换为字典格式
159
+
160
+ Args:
161
+ kline_data: K 线数据列表,格式为 [open_time, open, high, low, close, volume, ...]
162
+
163
+ Returns:
164
+ 字典格式的 K 线数据
165
+ """
166
+ open_time = int(kline_data[0]) if isinstance(kline_data[0], str) else kline_data[0]
167
+ close_time = int(kline_data[6]) if isinstance(kline_data[6], str) else kline_data[6]
168
+
169
+ return {
170
+ 'open_time': open_time,
171
+ 'open_time_str': datetime.fromtimestamp(open_time / 1000).strftime('%Y-%m-%d %H:%M:%S'),
172
+ 'open_price': float(kline_data[1]),
173
+ 'high_price': float(kline_data[2]),
174
+ 'low_price': float(kline_data[3]),
175
+ 'close_price': float(kline_data[4]),
176
+ 'volume': float(kline_data[5]),
177
+ 'close_time': close_time,
178
+ 'close_time_str': datetime.fromtimestamp(close_time / 1000).strftime('%Y-%m-%d %H:%M:%S'),
179
+ 'quote_volume': float(kline_data[7]),
180
+ 'trades': int(kline_data[8]),
181
+ 'taker_buy_base_volume': float(kline_data[9]),
182
+ 'taker_buy_quote_volume': float(kline_data[10]),
183
+ 'ignore': kline_data[11] if len(kline_data) > 11 else "0"
184
+ }
185
+
186
+ def _dict_to_dataframe_row(self, kline_dict: Dict[str, Any]) -> pd.Series:
187
+ """
188
+ 将 K 线字典转换为 DataFrame 行
189
+
190
+ Args:
191
+ kline_dict: K 线字典
192
+
193
+ Returns:
194
+ pandas Series
195
+ """
196
+ return pd.Series({
197
+ 'datetime': pd.to_datetime(kline_dict['open_time_str']),
198
+ 'open_time': kline_dict['open_time'],
199
+ 'open_time_str': kline_dict['open_time_str'],
200
+ 'open_price': kline_dict['open_price'],
201
+ 'high_price': kline_dict['high_price'],
202
+ 'low_price': kline_dict['low_price'],
203
+ 'close_price': kline_dict['close_price'],
204
+ 'volume': kline_dict['volume'],
205
+ 'close_time': kline_dict['close_time'],
206
+ 'close_time_str': kline_dict['close_time_str'],
207
+ 'quote_volume': kline_dict['quote_volume'],
208
+ 'trades': kline_dict['trades'],
209
+ 'taker_buy_base_volume': kline_dict['taker_buy_base_volume'],
210
+ 'taker_buy_quote_volume': kline_dict['taker_buy_quote_volume'],
211
+ 'ignore': kline_dict['ignore']
212
+ })
213
+
214
+ async def load_historical_data(self) -> bool:
215
+ """
216
+ 加载历史数据
217
+
218
+ Returns:
219
+ 是否成功加载
220
+ """
221
+ try:
222
+ logging.info(f"正在加载 {self.symbol} 的历史数据,间隔: {self.interval}, 周期数: {self.lookback_periods}")
223
+
224
+ interval_enum = self.interval_map[self.interval]
225
+
226
+ # 查询历史 K 线数据
227
+ response = self.rest_client.rest_api.kline_candlestick_data(
228
+ symbol=self.symbol,
229
+ interval=interval_enum,
230
+ limit=self.lookback_periods
231
+ )
232
+
233
+ klines_data = response.data()
234
+
235
+ if not klines_data:
236
+ logging.warning("未获取到历史数据")
237
+ return False
238
+
239
+ # 转换为 DataFrame
240
+ data_list = []
241
+ for kline in klines_data:
242
+ kline_dict = self._kline_to_dict(kline)
243
+ data_list.append(self._dict_to_dataframe_row(kline_dict))
244
+
245
+ self.data_df = pd.DataFrame(data_list)
246
+ self.data_df = self.data_df.sort_values('datetime').reset_index(drop=True)
247
+
248
+ logging.info(f"成功加载 {len(self.data_df)} 条历史数据")
249
+ logging.info(f"数据时间范围: {self.data_df.iloc[0]['open_time_str']} 至 {self.data_df.iloc[-1]['open_time_str']}")
250
+
251
+ return True
252
+
253
+ except Exception as e:
254
+ logging.error(f"加载历史数据时出错: {e}")
255
+ import traceback
256
+ logging.error(traceback.format_exc())
257
+ return False
258
+
259
+ def _handle_kline_message(self, data: Any):
260
+ """
261
+ 处理 WebSocket 接收到的 K 线消息
262
+
263
+ Args:
264
+ data: WebSocket 消息数据(可能是字典或 Pydantic 模型对象)
265
+ """
266
+ try:
267
+ # 如果 data 是 Pydantic 模型对象,转换为字典
268
+ if hasattr(data, 'model_dump'):
269
+ data = data.model_dump(by_alias=True)
270
+ elif hasattr(data, 'dict'):
271
+ data = data.dict(by_alias=True)
272
+ elif not isinstance(data, dict):
273
+ # 尝试转换为字典
274
+ data = dict(data) if hasattr(data, '__dict__') else data
275
+
276
+ # WebSocket 消息格式: {"e": "kline", "k": {...}}
277
+ if 'k' in data:
278
+ kline_info = data['k']
279
+
280
+ # 如果 kline_info 是 Pydantic 模型对象,转换为字典
281
+ if hasattr(kline_info, 'model_dump'):
282
+ kline_info = kline_info.model_dump(by_alias=True)
283
+ elif hasattr(kline_info, 'dict'):
284
+ kline_info = kline_info.dict(by_alias=True)
285
+ elif not isinstance(kline_info, dict):
286
+ kline_info = dict(kline_info) if hasattr(kline_info, '__dict__') else kline_info
287
+
288
+ # 检查是否是新的 K 线(is_closed = True)
289
+ is_closed = kline_info.get('x', False) # x 表示 K 线是否已关闭
290
+
291
+ if is_closed:
292
+ # 构建 K 线数据列表(与 REST API 格式一致)
293
+ # 注意:字段值可能是字符串或数字,需要统一处理
294
+ kline_data = [
295
+ int(kline_info.get('t', 0)), # open_time
296
+ str(kline_info.get('o', '0')), # open
297
+ str(kline_info.get('h', '0')), # high
298
+ str(kline_info.get('l', '0')), # low
299
+ str(kline_info.get('c', '0')), # close
300
+ str(kline_info.get('v', '0')), # volume
301
+ int(kline_info.get('T', 0)), # close_time
302
+ str(kline_info.get('q', '0')), # quote_volume
303
+ int(kline_info.get('n', 0)), # trades
304
+ str(kline_info.get('V', '0')), # taker_buy_base_volume
305
+ str(kline_info.get('Q', '0')), # taker_buy_quote_volume
306
+ "0" # ignore
307
+ ]
308
+
309
+ kline_dict = self._kline_to_dict(kline_data)
310
+ self.latest_kline = kline_dict
311
+
312
+ # 更新 DataFrame
313
+ new_row = self._dict_to_dataframe_row(kline_dict)
314
+
315
+ if self.data_df is not None:
316
+ # 检查是否已存在该时间点的数据
317
+ existing_idx = self.data_df[
318
+ self.data_df['open_time'] == kline_dict['open_time']
319
+ ].index
320
+
321
+ if len(existing_idx) > 0:
322
+ # 更新现有数据
323
+ self.data_df.loc[existing_idx[0]] = new_row
324
+ logging.debug(f"更新 K 线数据: {kline_dict['open_time_str']}")
325
+ else:
326
+ # 添加新数据
327
+ self.data_df = pd.concat([self.data_df, new_row.to_frame().T], ignore_index=True)
328
+ self.data_df = self.data_df.sort_values('datetime').reset_index(drop=True)
329
+
330
+ # 如果数据超过 lookback_periods,删除最旧的数据
331
+ if len(self.data_df) > self.lookback_periods:
332
+ self.data_df = self.data_df.tail(self.lookback_periods).reset_index(drop=True)
333
+
334
+ logging.info(f"新增 K 线数据: {kline_dict['open_time_str']}, 价格: {kline_dict['close_price']}")
335
+ else:
336
+ # 如果 DataFrame 为空,创建新的
337
+ self.data_df = new_row.to_frame().T
338
+
339
+ # 调用回调函数
340
+ for callback in self.on_new_kline_callbacks:
341
+ try:
342
+ callback(kline_dict, self.get_data())
343
+ except Exception as e:
344
+ logging.error(f"回调函数执行出错: {e}")
345
+
346
+ for callback in self.on_data_updated_callbacks:
347
+ try:
348
+ callback(self.get_data())
349
+ except Exception as e:
350
+ logging.error(f"数据更新回调函数执行出错: {e}")
351
+ else:
352
+ # 未关闭的 K 线,更新最新价格(不添加到 DataFrame)
353
+ open_time = int(kline_info.get('t', 0))
354
+ close_time = int(kline_info.get('T', 0))
355
+ self.latest_kline = {
356
+ 'open_time': open_time,
357
+ 'open_time_str': datetime.fromtimestamp(open_time / 1000).strftime('%Y-%m-%d %H:%M:%S'),
358
+ 'open_price': float(kline_info.get('o', '0')),
359
+ 'high_price': float(kline_info.get('h', '0')),
360
+ 'low_price': float(kline_info.get('l', '0')),
361
+ 'close_price': float(kline_info.get('c', '0')),
362
+ 'volume': float(kline_info.get('v', '0')),
363
+ 'close_time': close_time,
364
+ 'close_time_str': datetime.fromtimestamp(close_time / 1000).strftime('%Y-%m-%d %H:%M:%S'),
365
+ 'quote_volume': float(kline_info.get('q', '0')),
366
+ 'trades': int(kline_info.get('n', 0)),
367
+ 'taker_buy_base_volume': float(kline_info.get('V', '0')),
368
+ 'taker_buy_quote_volume': float(kline_info.get('Q', '0')),
369
+ 'ignore': "0"
370
+ }
371
+ logging.debug(f"更新当前 K 线(未关闭): {self.latest_kline['close_price']}")
372
+
373
+ except Exception as e:
374
+ logging.error(f"处理 K 线消息时出错: {e}")
375
+ import traceback
376
+ logging.error(traceback.format_exc())
377
+
378
+ def get_data(self) -> Optional[pd.DataFrame]:
379
+ """
380
+ 获取当前的数据 DataFrame
381
+
382
+ Returns:
383
+ 包含历史数据的 DataFrame,如果数据未加载则返回 None
384
+ """
385
+ return self.data_df.copy() if self.data_df is not None else None
386
+
387
+ def get_latest_price(self) -> Optional[float]:
388
+ """
389
+ 获取最新价格
390
+
391
+ Returns:
392
+ 最新价格,如果无数据则返回 None
393
+ """
394
+ if self.latest_kline:
395
+ return self.latest_kline['close_price']
396
+ elif self.data_df is not None and len(self.data_df) > 0:
397
+ return self.data_df.iloc[-1]['close_price']
398
+ return None
399
+
400
+ def register_on_new_kline(self, callback: Callable[[Dict[str, Any], pd.DataFrame], None]):
401
+ """
402
+ 注册新 K 线回调函数
403
+
404
+ Args:
405
+ callback: 回调函数,接收 (kline_dict, data_df) 作为参数
406
+ """
407
+ self.on_new_kline_callbacks.append(callback)
408
+
409
+ def register_on_data_updated(self, callback: Callable[[pd.DataFrame], None]):
410
+ """
411
+ 注册数据更新回调函数
412
+
413
+ Args:
414
+ callback: 回调函数,接收 data_df 作为参数
415
+ """
416
+ self.on_data_updated_callbacks.append(callback)
417
+
418
+ async def start(self):
419
+ """
420
+ 启动实时跟踪
421
+ """
422
+ if self.is_running:
423
+ logging.warning("实时跟踪已在运行中")
424
+ return
425
+
426
+ # 先加载历史数据
427
+ if not await self.load_historical_data():
428
+ logging.error("加载历史数据失败,无法启动实时跟踪")
429
+ return
430
+
431
+ try:
432
+ # 创建 WebSocket 连接
433
+ self.connection = await self.ws_client.websocket_streams.create_connection()
434
+
435
+ # 检查连接是否成功
436
+ if self.connection is None:
437
+ error_msg = "WebSocket 连接失败:连接对象为 None。可能是 SSL 证书验证问题。"
438
+ if self.ssl_verify:
439
+ error_msg += " 提示:如果遇到 SSL 证书验证错误,可以在初始化时设置 ssl_verify=False(仅用于开发/测试)"
440
+ logging.error(error_msg)
441
+ self.is_running = False
442
+ return
443
+
444
+ # 订阅 K 线流
445
+ self.stream = await self.connection.kline_candlestick_streams(
446
+ symbol=self.symbol.lower(),
447
+ interval=self.interval,
448
+ )
449
+
450
+ # 检查流是否成功创建
451
+ if self.stream is None:
452
+ logging.error("K 线流订阅失败:流对象为 None")
453
+ self.is_running = False
454
+ return
455
+
456
+ # 设置消息处理
457
+ self.stream.on("message", self._handle_kline_message)
458
+
459
+ self.is_running = True
460
+ logging.info(f"实时跟踪已启动: {self.symbol} {self.interval}")
461
+
462
+ except Exception as e:
463
+ error_msg = f"启动实时跟踪时出错: {e}"
464
+ if "SSL" in str(e) or "certificate" in str(e).lower():
465
+ if self.ssl_verify:
466
+ error_msg += "\n提示:如果遇到 SSL 证书验证错误,可以在初始化时设置 ssl_verify=False(仅用于开发/测试)"
467
+ logging.error(error_msg)
468
+ import traceback
469
+ logging.error(traceback.format_exc())
470
+ self.is_running = False
471
+
472
+ async def stop(self):
473
+ """
474
+ 停止实时跟踪
475
+ """
476
+ if not self.is_running:
477
+ return
478
+
479
+ try:
480
+ if self.stream:
481
+ await self.stream.unsubscribe()
482
+
483
+ if self.connection:
484
+ await self.connection.close_connection(close_session=True)
485
+
486
+ self.is_running = False
487
+ logging.info("实时跟踪已停止")
488
+
489
+ except Exception as e:
490
+ logging.error(f"停止实时跟踪时出错: {e}")
491
+
492
+ async def run_forever(self):
493
+ """
494
+ 运行实时跟踪(持续运行直到中断)
495
+ """
496
+ await self.start()
497
+
498
+ try:
499
+ # 保持运行
500
+ while self.is_running:
501
+ await asyncio.sleep(60)
502
+ except KeyboardInterrupt:
503
+ logging.info("收到中断信号,正在停止...")
504
+ finally:
505
+ await self.stop()
506
+
507
+
508
+ async def example_usage():
509
+ """
510
+ 示例用法:实时追踪价格并计算信号
511
+ """
512
+ import sys
513
+ import os
514
+
515
+ # 导入信号函数(优先尝试直接导入,适用于已安装的package)
516
+ try:
517
+ from cyqnt_trd.trading_signal.signal.ma_signal import ma_signal, ma_cross_signal
518
+ from cyqnt_trd.trading_signal.signal.factor_based_signal import factor_based_signal
519
+ from cyqnt_trd.trading_signal.factor.ma_factor import ma_factor
520
+ from cyqnt_trd.trading_signal.factor.rsi_factor import rsi_factor
521
+ from cyqnt_trd.trading_signal.selected_alpha.alpha1 import alpha1_factor
522
+ except ImportError:
523
+ # 如果直接导入失败,尝试添加项目路径(用于开发模式)
524
+ current_dir = os.path.dirname(os.path.abspath(__file__))
525
+ project_root = os.path.dirname(os.path.dirname(current_dir))
526
+ if project_root not in sys.path:
527
+ sys.path.insert(0, project_root)
528
+
529
+ # 再次尝试导入
530
+ try:
531
+ from cyqnt_trd.trading_signal.signal.ma_signal import ma_signal, ma_cross_signal
532
+ from cyqnt_trd.trading_signal.signal.factor_based_signal import factor_based_signal
533
+ from cyqnt_trd.trading_signal.factor.ma_factor import ma_factor
534
+ from cyqnt_trd.trading_signal.factor.rsi_factor import rsi_factor
535
+ from cyqnt_trd.trading_signal.selected_alpha.alpha1 import alpha1_factor
536
+ except ImportError as e:
537
+ logging.error(f"导入信号模块失败: {e}")
538
+ logging.error("请确保已安装 package 或项目路径正确")
539
+ logging.error("安装方式: pip install -e .")
540
+ return
541
+
542
+ # 创建跟踪器
543
+ # 如果遇到 SSL 证书验证错误,可以设置 ssl_verify=False(仅用于开发/测试)
544
+ tracker = RealtimePriceTracker(
545
+ symbol="BTCUSDT",
546
+ interval="1m",
547
+ lookback_periods=100,
548
+ ssl_verify=False # 如果遇到 SSL 证书验证错误,设置为 False
549
+ )
550
+
551
+ # 交易状态跟踪
552
+ position = 0.0 # 当前持仓
553
+ entry_price = 0.0 # 入场价格
554
+ entry_index = -1 # 入场索引
555
+
556
+ # 交易记录和统计
557
+ from datetime import datetime
558
+ initial_capital = 10000.0 # 初始资金
559
+ current_capital = initial_capital # 当前资金(不包括持仓)
560
+ completed_trades = [] # 已完成的交易记录
561
+ total_trades = 0 # 总交易次数
562
+ win_trades = 0 # 盈利交易次数
563
+ loss_trades = 0 # 亏损交易次数
564
+ total_profit = 0.0 # 累计盈亏(金额)
565
+ start_time = datetime.now() # 程序开始时间
566
+
567
+ # 信号计算函数
568
+ def calculate_and_display_signals(kline_dict, data_df):
569
+ """计算并显示所有信号"""
570
+ nonlocal position, entry_price, entry_index
571
+ nonlocal initial_capital, current_capital, completed_trades
572
+ nonlocal total_trades, win_trades, loss_trades, total_profit, start_time
573
+
574
+ if len(data_df) < 10: # 确保有足够的数据
575
+ print(f"\n[{kline_dict['open_time_str']}] 数据不足,等待更多数据...")
576
+ return
577
+
578
+ current_price = kline_dict['close_price']
579
+ current_time = kline_dict['open_time_str']
580
+
581
+ # 使用足够的数据切片(最后30行,确保有足够的历史数据)
582
+ data_slice = data_df.iloc[-30:].copy() if len(data_df) >= 30 else data_df.copy()
583
+
584
+ # 计算当前总资产(包括持仓价值)
585
+ if position > 0:
586
+ position_value = position * current_price
587
+ total_assets = current_capital + position_value
588
+ floating_profit_pct = (current_price - entry_price) / entry_price * 100
589
+ else:
590
+ total_assets = current_capital
591
+ floating_profit_pct = 0.0
592
+
593
+ # 计算整体收益率
594
+ total_return_pct = (total_assets - initial_capital) / initial_capital * 100
595
+
596
+ # 计算运行时间
597
+ runtime = datetime.now() - start_time
598
+ runtime_str = f"{runtime.days}天 {runtime.seconds // 3600}小时 {(runtime.seconds % 3600) // 60}分钟"
599
+
600
+ # 计算胜率
601
+ win_rate = (win_trades / total_trades * 100) if total_trades > 0 else 0.0
602
+
603
+ print(f"\n{'='*80}")
604
+ print(f"📊 新 K 线数据")
605
+ print(f"{'='*80}")
606
+ print(f"时间: {current_time}")
607
+ print(f"价格: {current_price:.2f}")
608
+ if position > 0:
609
+ print(f"持仓: {position:.4f} | 入场价: {entry_price:.2f} | 浮动盈亏: {floating_profit_pct:+.2f}%")
610
+ else:
611
+ print(f"持仓: 无")
612
+ print(f"{'='*80}")
613
+ print(f"💰 账户统计:")
614
+ print(f" 初始资金: {initial_capital:.2f}")
615
+ print(f" 当前资金: {current_capital:.2f}")
616
+ if position > 0:
617
+ print(f" 持仓价值: {position_value:.2f}")
618
+ print(f" 总资产: {total_assets:.2f}")
619
+ print(f" 累计盈亏: {total_profit:+.2f} ({total_return_pct:+.2f}%)")
620
+ print(f" 运行时间: {runtime_str}")
621
+ print(f" 总交易次数: {total_trades} | 盈利: {win_trades} | 亏损: {loss_trades} | 胜率: {win_rate:.2f}%")
622
+ print(f"{'='*80}")
623
+ print(f"📈 交易信号:")
624
+
625
+ # 1. MA 信号(MA5)
626
+ try:
627
+ if len(data_slice) >= 6:
628
+ ma5_signal = ma_signal(
629
+ data_slice=data_slice,
630
+ position=position,
631
+ entry_price=entry_price,
632
+ entry_index=entry_index,
633
+ take_profit=0.1, # 止盈10%
634
+ stop_loss=0.05, # 止损5%
635
+ period=5
636
+ )
637
+ signal_emoji = "🟢" if ma5_signal == 'buy' else "🔴" if ma5_signal == 'sell' else "⚪"
638
+ print(f" {signal_emoji} MA5信号: {ma5_signal.upper()}")
639
+ except Exception as e:
640
+ print(f"MA5信号计算失败: {e}")
641
+
642
+ # 2. MA交叉信号(MA5/MA20)
643
+ try:
644
+ if len(data_slice) >= 22:
645
+ ma_cross_sig = ma_cross_signal(
646
+ data_slice=data_slice,
647
+ position=position,
648
+ entry_price=entry_price,
649
+ entry_index=entry_index,
650
+ take_profit=0.1,
651
+ stop_loss=0.05,
652
+ check_periods=1,
653
+ short_period=5,
654
+ long_period=20
655
+ )
656
+ signal_emoji = "🟢" if ma_cross_sig == 'buy' else "🔴" if ma_cross_sig == 'sell' else "⚪"
657
+ print(f" {signal_emoji} MA交叉信号(5/20): {ma_cross_sig.upper()}")
658
+ except Exception as e:
659
+ print(f"MA交叉信号计算失败: {e}")
660
+
661
+ # 3. 基于因子的信号(MA因子)
662
+ try:
663
+ if len(data_slice) >= 6:
664
+ ma_factor_signal = factor_based_signal(
665
+ data_slice=data_slice,
666
+ position=position,
667
+ entry_price=entry_price,
668
+ entry_index=entry_index,
669
+ take_profit=0.1,
670
+ stop_loss=0.05,
671
+ check_periods=1,
672
+ factor_func=lambda d: ma_factor(d, period=5),
673
+ factor_period=5
674
+ )
675
+ signal_emoji = "🟢" if ma_factor_signal == 'buy' else "🔴" if ma_factor_signal == 'sell' else "⚪"
676
+ print(f" {signal_emoji} 基于MA因子的信号: {ma_factor_signal.upper()}")
677
+ except Exception as e:
678
+ print(f"基于MA因子的信号计算失败: {e}")
679
+
680
+ # 4. 基于因子的信号(RSI因子)
681
+ try:
682
+ if len(data_slice) >= 16:
683
+ rsi_factor_signal = factor_based_signal(
684
+ data_slice=data_slice,
685
+ position=position,
686
+ entry_price=entry_price,
687
+ entry_index=entry_index,
688
+ take_profit=0.1,
689
+ stop_loss=0.05,
690
+ check_periods=1,
691
+ factor_func=lambda d: rsi_factor(d, period=14),
692
+ factor_period=14
693
+ )
694
+ signal_emoji = "🟢" if rsi_factor_signal == 'buy' else "🔴" if rsi_factor_signal == 'sell' else "⚪"
695
+ print(f" {signal_emoji} 基于RSI因子的信号: {rsi_factor_signal.upper()}")
696
+ except Exception as e:
697
+ print(f"基于RSI因子的信号计算失败: {e}")
698
+
699
+ # 5. Alpha#1 因子信号
700
+ try:
701
+ if len(data_slice) >= 26: # alpha1需要至少25行数据
702
+ alpha1_signal = factor_based_signal(
703
+ data_slice=data_slice,
704
+ position=position,
705
+ entry_price=entry_price,
706
+ entry_index=entry_index,
707
+ take_profit=0.1,
708
+ stop_loss=0.05,
709
+ check_periods=1,
710
+ factor_func=lambda d: alpha1_factor(d, lookback_days=5, stddev_period=20, power=2.0),
711
+ factor_period=25
712
+ )
713
+ signal_emoji = "🟢" if alpha1_signal == 'buy' else "🔴" if alpha1_signal == 'sell' else "⚪"
714
+ print(f" {signal_emoji} 基于Alpha#1因子的信号: {alpha1_signal.upper()}")
715
+ except Exception as e:
716
+ print(f"基于Alpha#1因子的信号计算失败: {e}")
717
+
718
+ # 显示因子值(用于参考)
719
+ print(f"\n📊 因子值:")
720
+ try:
721
+ if len(data_slice) >= 6:
722
+ ma5_factor_val = ma_factor(data_slice, period=5)
723
+ print(f" MA5因子: {ma5_factor_val:.4f}")
724
+ except:
725
+ pass
726
+
727
+ try:
728
+ if len(data_slice) >= 16:
729
+ rsi_factor_val = rsi_factor(data_slice, period=14)
730
+ print(f" RSI因子: {rsi_factor_val:.4f}")
731
+ except:
732
+ pass
733
+
734
+ try:
735
+ if len(data_slice) >= 26:
736
+ alpha1_factor_val = alpha1_factor(data_slice, lookback_days=5, stddev_period=20, power=2.0)
737
+ print(f" Alpha#1因子: {alpha1_factor_val:.4f}")
738
+ except:
739
+ pass
740
+
741
+ print(f"{'='*80}\n")
742
+
743
+ # 更新持仓状态(示例:使用MA5信号进行交易)
744
+ # 注意:这里只是示例,实际交易需要更完善的逻辑
745
+ if len(data_slice) >= 6:
746
+ try:
747
+ ma5_signal = ma_signal(
748
+ data_slice=data_slice,
749
+ position=position,
750
+ entry_price=entry_price,
751
+ entry_index=entry_index,
752
+ take_profit=0.1,
753
+ stop_loss=0.05,
754
+ period=5
755
+ )
756
+
757
+ if ma5_signal == 'buy' and position == 0:
758
+ # 买入
759
+ # 计算可买入的数量(使用当前资金的90%)
760
+ buy_amount = current_capital * 0.9
761
+ position = buy_amount / current_price
762
+ entry_price = current_price
763
+ entry_index = len(data_df) - 1
764
+
765
+ # 更新资金(扣除买入金额)
766
+ current_capital -= buy_amount
767
+
768
+ print(f"\n{'='*80}")
769
+ print(f"✅ 执行买入")
770
+ print(f" 价格: {entry_price:.2f}")
771
+ print(f" 数量: {position:.4f}")
772
+ print(f" 金额: {buy_amount:.2f}")
773
+ print(f" 剩余资金: {current_capital:.2f}")
774
+ print(f"{'='*80}")
775
+
776
+ elif ma5_signal == 'sell' and position > 0:
777
+ # 卖出
778
+ sell_amount = position * current_price
779
+ profit_amount = sell_amount - (position * entry_price)
780
+ profit_pct = (current_price - entry_price) / entry_price * 100
781
+
782
+ # 更新资金(增加卖出金额)
783
+ current_capital += sell_amount
784
+
785
+ # 记录交易
786
+ trade_record = {
787
+ 'entry_time': data_df.iloc[entry_index]['open_time_str'] if entry_index >= 0 else 'N/A',
788
+ 'exit_time': current_time,
789
+ 'entry_price': entry_price,
790
+ 'exit_price': current_price,
791
+ 'quantity': position,
792
+ 'profit_amount': profit_amount,
793
+ 'profit_pct': profit_pct
794
+ }
795
+ completed_trades.append(trade_record)
796
+
797
+ # 更新统计
798
+ total_trades += 1
799
+ total_profit += profit_amount
800
+ if profit_amount > 0:
801
+ win_trades += 1
802
+ else:
803
+ loss_trades += 1
804
+
805
+ print(f"\n{'='*80}")
806
+ print(f"✅ 执行卖出")
807
+ print(f" 价格: {current_price:.2f}")
808
+ print(f" 入场价: {entry_price:.2f}")
809
+ print(f" 数量: {position:.4f}")
810
+ print(f" 盈亏金额: {profit_amount:+.2f}")
811
+ print(f" 盈亏比例: {profit_pct:+.2f}%")
812
+ print(f" 当前资金: {current_capital:.2f}")
813
+ print(f"{'='*80}")
814
+
815
+ # 重置持仓
816
+ position = 0.0
817
+ entry_price = 0.0
818
+ entry_index = -1
819
+ except Exception as e:
820
+ logging.debug(f"更新持仓状态时出错: {e}")
821
+
822
+ # 注册回调函数
823
+ tracker.register_on_new_kline(calculate_and_display_signals)
824
+
825
+ print("="*80)
826
+ print("实时价格跟踪和信号计算已启动")
827
+ print(f"交易对: {tracker.symbol}")
828
+ print(f"时间间隔: {tracker.interval}")
829
+ print(f"历史数据周期数: {tracker.lookback_periods}")
830
+ print("="*80)
831
+ print("\n等待新 K 线数据...\n")
832
+
833
+ # 运行跟踪器
834
+ await tracker.run_forever()
835
+
836
+
837
+ if __name__ == "__main__":
838
+ asyncio.run(example_usage())
839
+