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.
- cyqnt_trd/__init__.py +26 -0
- cyqnt_trd/backtesting/README.md +264 -0
- cyqnt_trd/backtesting/__init__.py +12 -0
- cyqnt_trd/backtesting/factor_test.py +332 -0
- cyqnt_trd/backtesting/framework.py +311 -0
- cyqnt_trd/backtesting/strategy_backtest.py +545 -0
- cyqnt_trd/diagnose_api.py +28 -0
- cyqnt_trd/get_data/__init__.py +15 -0
- cyqnt_trd/get_data/get_futures_data.py +472 -0
- cyqnt_trd/get_data/get_trending_data.py +771 -0
- cyqnt_trd/online_trading/__init__.py +13 -0
- cyqnt_trd/online_trading/realtime_price_tracker.py +1001 -0
- cyqnt_trd/test.py +119 -0
- cyqnt_trd/test_script/README.md +411 -0
- cyqnt_trd/test_script/get_network_info.py +192 -0
- cyqnt_trd/test_script/get_symbols_by_volume.py +227 -0
- cyqnt_trd/test_script/realtime_price_tracker.py +839 -0
- cyqnt_trd/test_script/test_alpha.py +261 -0
- cyqnt_trd/test_script/test_kline_data.py +479 -0
- cyqnt_trd/test_script/test_order.py +1360 -0
- cyqnt_trd/trading_signal/README.md +276 -0
- cyqnt_trd/trading_signal/__init__.py +17 -0
- cyqnt_trd/trading_signal/example_test_alpha.py +430 -0
- cyqnt_trd/trading_signal/example_usage.py +431 -0
- cyqnt_trd/trading_signal/factor/__init__.py +18 -0
- cyqnt_trd/trading_signal/factor/ma_factor.py +75 -0
- cyqnt_trd/trading_signal/factor/rsi_factor.py +56 -0
- cyqnt_trd/trading_signal/selected_alpha/__init__.py +158 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha1.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha10.py +90 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha100.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha101.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha11.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha12.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha13.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha14.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha15.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha16.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha17.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha18.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha19.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha2.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha20.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha21.py +89 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha22.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha23.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha24.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha25.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha26.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha27.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha28.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha29.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha3.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha30.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha31.py +90 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha32.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha33.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha34.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha35.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha36.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha37.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha38.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha39.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha4.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha40.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha41.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha42.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha43.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha44.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha45.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha46.py +89 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha47.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha48.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha49.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha5.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha50.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha51.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha52.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha53.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha54.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha55.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha56.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha57.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha58.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha59.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha6.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha60.py +89 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha61.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha62.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha63.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha64.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha65.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha66.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha67.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha68.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha69.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha7.py +88 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha70.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha71.py +92 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha72.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha73.py +91 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha74.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha75.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha76.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha77.py +92 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha78.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha79.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha8.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha80.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha81.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha82.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha83.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha84.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha85.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha86.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha87.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha88.py +92 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha89.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha9.py +90 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha90.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha91.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha92.py +92 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha93.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha94.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha95.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha96.py +92 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha97.py +74 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha98.py +87 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha99.py +86 -0
- cyqnt_trd/trading_signal/selected_alpha/alpha_utils.py +342 -0
- cyqnt_trd/trading_signal/selected_alpha/create_all_alphas.py +279 -0
- cyqnt_trd/trading_signal/selected_alpha/generate_alphas.py +133 -0
- cyqnt_trd/trading_signal/selected_alpha/test_alpha.py +261 -0
- cyqnt_trd/trading_signal/signal/__init__.py +20 -0
- cyqnt_trd/trading_signal/signal/factor_based_signal.py +387 -0
- cyqnt_trd/trading_signal/signal/ma_signal.py +163 -0
- cyqnt_trd/utils/__init__.py +3 -0
- cyqnt_trd/utils/set_user.py +33 -0
- cyqnt_trd-0.1.2.dist-info/METADATA +148 -0
- cyqnt_trd-0.1.2.dist-info/RECORD +147 -0
- cyqnt_trd-0.1.2.dist-info/WHEEL +5 -0
- cyqnt_trd-0.1.2.dist-info/licenses/LICENSE +21 -0
- cyqnt_trd-0.1.2.dist-info/top_level.txt +2 -0
- test/real_time_trade.py +746 -0
- test/test_example_usage.py +381 -0
- test/test_get_data.py +310 -0
- 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
|
+
|