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,545 @@
1
+ """
2
+ 策略回测模块
3
+
4
+ 根据买卖信号进行回测,计算收益率和收益曲线
5
+ """
6
+
7
+ import pandas as pd
8
+ import numpy as np
9
+ import matplotlib.pyplot as plt
10
+ from typing import Callable, Optional, Dict, List, Tuple
11
+ from datetime import datetime
12
+ import json
13
+ import os
14
+
15
+
16
+ class StrategyBacktester:
17
+ """
18
+ 策略回测器
19
+
20
+ 根据买卖信号进行回测,计算收益率和收益曲线
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ data: pd.DataFrame,
26
+ initial_capital: float = 10000.0,
27
+ commission_rate: float = 0.001
28
+ ):
29
+ """
30
+ 初始化策略回测器
31
+
32
+ Args:
33
+ data: 包含K线数据的DataFrame,必须包含以下列:
34
+ - datetime 或 open_time_str: 时间
35
+ - close_price: 收盘价
36
+ initial_capital: 初始资金
37
+ commission_rate: 手续费率(默认0.1%)
38
+ """
39
+ self.data = data.copy()
40
+
41
+ # 确保有datetime列
42
+ if 'datetime' not in self.data.columns:
43
+ if 'open_time_str' in self.data.columns:
44
+ self.data['datetime'] = pd.to_datetime(self.data['open_time_str'])
45
+ elif 'open_time' in self.data.columns:
46
+ self.data['datetime'] = pd.to_datetime(self.data['open_time'], unit='ms')
47
+ else:
48
+ raise ValueError("数据必须包含 'datetime', 'open_time_str' 或 'open_time' 列")
49
+
50
+ # 确保有close_price列
51
+ if 'close_price' not in self.data.columns:
52
+ raise ValueError("数据必须包含 'close_price' 列")
53
+
54
+ # 确保有high_price和low_price列(用于止盈止损)
55
+ if 'high_price' not in self.data.columns:
56
+ raise ValueError("数据必须包含 'high_price' 列(用于止盈止损)")
57
+ if 'low_price' not in self.data.columns:
58
+ raise ValueError("数据必须包含 'low_price' 列(用于止盈止损)")
59
+
60
+ # 按时间排序
61
+ self.data = self.data.sort_values('datetime').reset_index(drop=True)
62
+
63
+ self.initial_capital = initial_capital
64
+ self.commission_rate = commission_rate
65
+
66
+ def backtest(
67
+ self,
68
+ signal_func: Callable,
69
+ min_periods: int = 0,
70
+ position_size: float = 1.0,
71
+ take_profit: Optional[float] = None,
72
+ stop_loss: Optional[float] = None,
73
+ check_periods: int = 1
74
+ ) -> Dict:
75
+ """
76
+ 执行回测
77
+
78
+ Args:
79
+ signal_func: 信号生成函数,必须接受以下参数:
80
+ (data_slice, position, entry_price, entry_index, take_profit, stop_loss, check_periods) -> str
81
+ - data_slice: 数据切片(包含历史数据和当前数据点)
82
+ - position: 当前持仓数量(如果没有持仓则为0)
83
+ - entry_price: 入场价格(如果没有持仓则为0)
84
+ - entry_index: 入场索引(如果没有持仓则为-1)
85
+ - take_profit: 止盈比例(例如:0.1 表示 10%)
86
+ - stop_loss: 止损比例(例如:0.1 表示 10%)
87
+ - check_periods: 检查未来多少个周期(只能为1,因为实际使用时无法看到未来数据)
88
+ - 返回: 'buy'(买入), 'sell'(卖出), 'hold'(持有)或 None
89
+ 策略函数可以根据持仓信息自行决定是否止盈止损
90
+ min_periods: 最小需要的周期数(用于计算信号时)
91
+ position_size: 每次交易的仓位大小(相对于可用资金的比例,0-1之间)
92
+ take_profit: 止盈比例(传递给策略函数,由策略决定是否使用)
93
+ stop_loss: 止损比例(传递给策略函数,由策略决定是否使用)
94
+ check_periods: 检查未来多少个周期(只能为1,默认1,即只检查当前周期)
95
+ 注意:回测时只能为1,因为实际交易中无法看到今天之后的数据
96
+
97
+ Returns:
98
+ 包含回测结果的字典:
99
+ - initial_capital: 初始资金
100
+ - final_capital: 最终资金
101
+ - total_return: 总收益率
102
+ - total_trades: 总交易次数
103
+ - win_trades: 盈利交易次数
104
+ - loss_trades: 亏损交易次数
105
+ - win_rate: 胜率
106
+ - max_drawdown: 最大回撤
107
+ - sharpe_ratio: 夏普比率
108
+ - equity_curve: 资金曲线(DataFrame)
109
+ - trades: 交易记录列表
110
+ """
111
+ import concurrent.futures
112
+
113
+ # 初始化
114
+ capital = self.initial_capital
115
+ position = 0 # 持仓数量
116
+ entry_price = 0 # 入场价格
117
+ entry_index = -1 # 入场索引
118
+
119
+ equity_curve = []
120
+ trades = []
121
+
122
+ # 预先准备多线程需要的参数
123
+ max_period = 50
124
+ idx_range = range(min_periods, len(self.data))
125
+
126
+ # 1. 并行调用信号函数
127
+ def signal_func_runner(args):
128
+ i, position, entry_price, entry_index = args
129
+ try:
130
+ start_idx = max(0, i - max_period)
131
+ data_slice = self.data.iloc[start_idx:i+1].copy()
132
+ signal = signal_func(
133
+ data_slice, position, entry_price, entry_index,
134
+ take_profit, stop_loss, check_periods
135
+ )
136
+ current_price = self.data.iloc[i]['close_price']
137
+ current_time = self.data.iloc[i]['datetime']
138
+ return (i, signal, current_price, current_time)
139
+ except Exception as e:
140
+ return (i, None, None, None)
141
+
142
+ # 为所有时间点提前构建并行参数列表(注意,position/entry_price/entry_index变化只能串行,这里仅signal并行!)
143
+ # 所以只能并行 signal_func 的输入,每步的仓位信息和买卖流水必须主进程顺序推进。
144
+ # 做法:先并行计算所有点的信号,然后主进程串行执行持仓等逻辑。
145
+
146
+ # 获取每个时间点需要的切片和并行参数(仓位参数全部为当前最新,后续再用)
147
+ # 方案:只并行计算信号,不动持仓逻辑。
148
+ with concurrent.futures.ThreadPoolExecutor() as executor:
149
+ # 注意:这里只能给signal_func实际需要的参数,其余(如position, entry_price等)在回测流程串行推演
150
+ # 由于并行只加速技术信号(只依赖slice数据,不依赖仓位),所以先串行取决于之前的position。
151
+ # 所有信号都需要position等,回测需要逐步推进状态,不能直接全部并发。
152
+ #
153
+ # 解决办法:只并行信号生成(即信号与持仓变化无关的情形,例如策略只考虑slice,不用position等参数)。如果策略要用这些参数仍必须主线程推演,信号函数并行意义有限。
154
+ #
155
+ # 这里我们假设信号函数主要耗时来自数据slice运算(如大量因子),用过去slice计算信号,不严重依赖实时持仓等。
156
+ # 所以我们在持仓、entry_price、entry_index等参数为"推演至当前时刻"的主线程状态下,同步推进主循环,并行仅用于信号slice耗时运算。
157
+
158
+ # 改进方案:主循环推演仓位,但每次 slice 调 signal_func 并行
159
+ signals_future = [None] * len(idx_range)
160
+
161
+ def signal_worker(i, position, entry_price, entry_index):
162
+ try:
163
+ start_idx = max(0, i - max_period)
164
+ data_slice = self.data.iloc[start_idx:i+1].copy()
165
+ signal = signal_func(
166
+ data_slice, position, entry_price, entry_index,
167
+ take_profit, stop_loss, check_periods
168
+ )
169
+ current_price = self.data.iloc[i]['close_price']
170
+ current_time = self.data.iloc[i]['datetime']
171
+ return (i, signal, current_price, current_time)
172
+ except Exception as e:
173
+ return (i, None, None, None)
174
+
175
+ # 逐步推进账户状态,但每步并发信号计算
176
+ with concurrent.futures.ThreadPoolExecutor() as pool:
177
+ # 先占位,后面主循环会推进position等参数
178
+ results_for_period = [None] * len(idx_range)
179
+ # 记录期望并发任务
180
+ futures = {}
181
+ for local_idx, i in enumerate(idx_range):
182
+ # 提交并占位future
183
+ futures[local_idx] = None # 先预占
184
+ # 下面主流程顺序推进持仓等状态,同时在每步将signal_func并发出去
185
+ for local_idx, i in enumerate(idx_range):
186
+ # 提交signal_func任务(给实时position等参数)
187
+ futures[local_idx] = pool.submit(signal_worker, i, position, entry_price, entry_index)
188
+
189
+ # 取出回调结果(非阻塞等待,可以微调batch方式用于进一步加速)
190
+ # 由于策略必须顺序推演(因为signal_func依赖当前仓位等),无法完全乱序
191
+ i, signal, current_price, current_time = futures[local_idx].result()
192
+
193
+ # 处理信号
194
+ if signal == 'buy' and position == 0:
195
+ # 买入
196
+ trade_amount = capital * position_size
197
+ position = trade_amount / current_price
198
+ commission = trade_amount * self.commission_rate
199
+ capital = capital - trade_amount - commission
200
+ entry_price = current_price
201
+ entry_index = i
202
+
203
+ trades.append({
204
+ 'type': 'buy',
205
+ 'datetime': current_time,
206
+ 'index': i,
207
+ 'price': float(current_price),
208
+ 'position': float(position),
209
+ 'capital': float(capital)
210
+ })
211
+ elif signal == 'sell' and position > 0:
212
+ trade_amount = position * current_price
213
+ commission = trade_amount * self.commission_rate
214
+ capital = capital + trade_amount - commission
215
+
216
+ pnl = (current_price - entry_price) / entry_price if entry_price > 0 else 0
217
+ pnl_amount = trade_amount - (position * entry_price) - commission if entry_price > 0 else 0
218
+
219
+ reason = 'signal'
220
+ if entry_price > 0:
221
+ price_change = (current_price - entry_price) / entry_price
222
+ if take_profit is not None and price_change >= take_profit:
223
+ reason = 'take_profit'
224
+ elif stop_loss is not None and price_change <= -stop_loss:
225
+ reason = 'stop_loss'
226
+
227
+ trades.append({
228
+ 'type': 'sell',
229
+ 'reason': reason,
230
+ 'datetime': current_time,
231
+ 'index': i,
232
+ 'price': float(current_price),
233
+ 'entry_price': float(entry_price),
234
+ 'pnl': float(pnl),
235
+ 'pnl_amount': float(pnl_amount),
236
+ 'capital': float(capital)
237
+ })
238
+
239
+ position = 0
240
+ entry_price = 0
241
+ entry_index = -1
242
+
243
+ # 计算当前总资产(现金 + 持仓市值)
244
+ current_equity = capital + (position * current_price if position > 0 else 0)
245
+ equity_curve.append({
246
+ 'datetime': current_time,
247
+ 'index': i,
248
+ 'equity': float(current_equity),
249
+ 'capital': float(capital),
250
+ 'position': float(position),
251
+ 'price': float(current_price)
252
+ })
253
+
254
+ # 如果最后还有持仓,按最后价格平仓
255
+ if position > 0:
256
+ last_price = self.data.iloc[-1]['close_price']
257
+ last_time = self.data.iloc[-1]['datetime']
258
+ trade_amount = position * last_price
259
+ commission = trade_amount * self.commission_rate
260
+ capital = capital + trade_amount - commission
261
+
262
+ pnl = (last_price - entry_price) / entry_price
263
+ pnl_amount = trade_amount - (position * entry_price) - commission
264
+
265
+ trades.append({
266
+ 'type': 'sell',
267
+ 'reason': 'close_position',
268
+ 'datetime': last_time,
269
+ 'index': len(self.data) - 1,
270
+ 'price': float(last_price),
271
+ 'entry_price': float(entry_price),
272
+ 'pnl': float(pnl),
273
+ 'pnl_amount': float(pnl_amount),
274
+ 'capital': float(capital)
275
+ })
276
+
277
+ # 更新最后一条资金曲线
278
+ if equity_curve:
279
+ equity_curve[-1]['equity'] = float(capital)
280
+ equity_curve[-1]['capital'] = float(capital)
281
+ equity_curve[-1]['position'] = 0.0
282
+
283
+ # 构建资金曲线DataFrame
284
+ equity_df = pd.DataFrame(equity_curve)
285
+
286
+ # 计算统计指标
287
+ final_capital = capital
288
+ total_return = (final_capital - self.initial_capital) / self.initial_capital
289
+
290
+ # 计算交易统计
291
+ sell_trades = [t for t in trades if t['type'] == 'sell']
292
+ total_trades = len(sell_trades)
293
+ win_trades = len([t for t in sell_trades if t.get('pnl', 0) > 0])
294
+ loss_trades = len([t for t in sell_trades if t.get('pnl', 0) < 0])
295
+ win_rate = win_trades / total_trades if total_trades > 0 else 0.0
296
+
297
+ # 计算最大回撤
298
+ equity_values = equity_df['equity'].values
299
+ peak = np.maximum.accumulate(equity_values)
300
+ drawdown = (equity_values - peak) / peak
301
+ max_drawdown = abs(np.min(drawdown)) if len(drawdown) > 0 else 0.0
302
+
303
+ # 计算夏普比率(简化版,假设无风险利率为0)
304
+ returns = equity_df['equity'].pct_change().dropna()
305
+ if len(returns) > 0 and returns.std() > 0:
306
+ sharpe_ratio = (returns.mean() / returns.std()) * np.sqrt(252) # 年化
307
+ else:
308
+ sharpe_ratio = 0.0
309
+
310
+ results = {
311
+ 'initial_capital': self.initial_capital,
312
+ 'final_capital': final_capital,
313
+ 'total_return': total_return,
314
+ 'total_trades': total_trades,
315
+ 'win_trades': win_trades,
316
+ 'loss_trades': loss_trades,
317
+ 'win_rate': win_rate,
318
+ 'max_drawdown': max_drawdown,
319
+ 'sharpe_ratio': sharpe_ratio,
320
+ 'equity_curve': equity_df,
321
+ 'trades': trades
322
+ }
323
+
324
+ return results
325
+
326
+ def plot_results(
327
+ self,
328
+ results: Dict,
329
+ figsize: Tuple[int, int] = (14, 10),
330
+ save_image: Optional[str] = None,
331
+ save_json: Optional[str] = None,
332
+ strategy_name: Optional[str] = None,
333
+ data_name: Optional[str] = None,
334
+ save_dir: Optional[str] = None,
335
+ source_start_str: Optional[str] = None,
336
+ source_end_str: Optional[str] = None
337
+ ):
338
+ """
339
+ 绘制回测结果
340
+
341
+ Args:
342
+ results: backtest返回的结果字典
343
+ figsize: 图形大小
344
+ save_image: 图片保存路径(如果提供,将保存图片,优先级高于自动生成)
345
+ save_json: JSON结果保存路径(如果提供,将保存JSON,优先级高于自动生成)
346
+ strategy_name: 策略名称(用于自动生成文件名)
347
+ data_name: 数据名称(用于自动生成文件名)
348
+ save_dir: 保存目录(如果提供strategy_name和data_name,将在此目录下保存文件)
349
+ source_start_str: 源数据的开始日期字符串(格式:YYYYMMDD),如果提供则优先使用
350
+ source_end_str: 源数据的结束日期字符串(格式:YYYYMMDD),如果提供则优先使用
351
+ """
352
+ equity_df = results['equity_curve']
353
+
354
+ # 如果提供了策略名称和数据名称,自动生成文件名
355
+ if strategy_name and data_name:
356
+ # 清理名称,移除特殊字符,用于文件名
357
+ clean_strategy_name = strategy_name.replace(' ', '_').replace('/', '_').replace('\\', '_')
358
+ clean_data_name = data_name.replace(' ', '_').replace('/', '_').replace('\\', '_')
359
+
360
+ # 优先使用源数据的时间范围,如果没有则从equity_curve中提取
361
+ start_str = source_start_str or ""
362
+ end_str = source_end_str or ""
363
+
364
+ if not start_str or not end_str:
365
+ # 从equity_curve中提取测试时间段
366
+ if not equity_df.empty and 'datetime' in equity_df.columns:
367
+ try:
368
+ start_time = equity_df['datetime'].min()
369
+ end_time = equity_df['datetime'].max()
370
+
371
+ # 转换为datetime对象(如果还不是)
372
+ if not isinstance(start_time, (pd.Timestamp, datetime)):
373
+ start_time = pd.to_datetime(start_time)
374
+ end_time = pd.to_datetime(end_time)
375
+
376
+ # 格式化时间为 YYYYMMDD
377
+ start_str = start_time.strftime('%Y%m%d')
378
+ end_str = end_time.strftime('%Y%m%d')
379
+ except Exception:
380
+ # 如果时间提取失败,使用空字符串
381
+ pass
382
+
383
+ # 确定保存目录结构
384
+ if save_dir:
385
+ # 根据数据名称和开始结束日期创建子文件夹
386
+ if start_str and end_str:
387
+ # 子文件夹格式:{数据名称}_{开始日期}_{结束日期}
388
+ subfolder_name = f"{clean_data_name}_{start_str}_{end_str}"
389
+ final_save_dir = os.path.join(save_dir, subfolder_name)
390
+ else:
391
+ # 如果没有时间段,只使用数据名称
392
+ final_save_dir = os.path.join(save_dir, clean_data_name)
393
+
394
+ # 确保目录存在
395
+ os.makedirs(final_save_dir, exist_ok=True)
396
+
397
+ # 文件名只包含策略名称
398
+ base_filename = f"{clean_strategy_name}_backtest"
399
+ base_path = os.path.join(final_save_dir, base_filename)
400
+ else:
401
+ # 如果没有提供保存目录,使用原来的逻辑(文件名包含所有信息)
402
+ time_period_str = f"_{start_str}_{end_str}" if (start_str and end_str) else ""
403
+ base_filename = f"{clean_strategy_name}_{clean_data_name}{time_period_str}_backtest"
404
+ base_path = base_filename
405
+
406
+ # 如果未明确指定路径,使用自动生成的路径
407
+ if save_image is None:
408
+ save_image = f"{base_path}.png"
409
+ if save_json is None:
410
+ save_json = f"{base_path}.json"
411
+ elif save_dir:
412
+ # 如果提供了save_dir但没有strategy_name或data_name,使用默认文件名
413
+ # 确保目录存在
414
+ os.makedirs(save_dir, exist_ok=True)
415
+
416
+ # 使用默认文件名
417
+ default_name = "backtest_result"
418
+ if strategy_name:
419
+ default_name = strategy_name.replace(' ', '_').replace('/', '_').replace('\\', '_')
420
+ elif data_name:
421
+ default_name = data_name.replace(' ', '_').replace('/', '_').replace('\\', '_')
422
+
423
+ base_path = os.path.join(save_dir, default_name)
424
+
425
+ if save_image is None:
426
+ save_image = f"{base_path}.png"
427
+ if save_json is None:
428
+ save_json = f"{base_path}.json"
429
+
430
+ # Set default font (no need for Chinese fonts)
431
+ plt.rcParams['axes.unicode_minus'] = False
432
+
433
+ fig, axes = plt.subplots(3, 1, figsize=figsize, gridspec_kw={'height_ratios': [2, 1, 1]})
434
+
435
+ # 1. Equity Curve
436
+ ax1 = axes[0]
437
+ ax1.plot(equity_df['datetime'], equity_df['equity'], label='Equity Curve', linewidth=2, color='blue')
438
+ ax1.axhline(y=results['initial_capital'], color='gray', linestyle='--', label='Initial Capital')
439
+ ax1.set_title('Backtest Equity Curve', fontsize=14, fontweight='bold')
440
+ ax1.set_ylabel('Equity (USDT)', fontsize=12)
441
+ ax1.legend()
442
+ ax1.grid(True, alpha=0.3)
443
+
444
+ # 2. Price Curve and Trading Signals
445
+ ax2 = axes[1]
446
+ ax2.plot(equity_df['datetime'], equity_df['price'], label='Price', linewidth=1.5, color='black', alpha=0.7)
447
+
448
+ # Mark buy/sell points
449
+ buy_trades = [t for t in results['trades'] if t['type'] == 'buy']
450
+ sell_trades = [t for t in results['trades'] if t['type'] == 'sell']
451
+
452
+ if buy_trades:
453
+ buy_times = [t['datetime'] for t in buy_trades]
454
+ buy_prices = [t['price'] for t in buy_trades]
455
+ ax2.scatter(buy_times, buy_prices, color='green', marker='^', s=100, label='Buy', zorder=5)
456
+
457
+ if sell_trades:
458
+ sell_times = [t['datetime'] for t in sell_trades]
459
+ sell_prices = [t['price'] for t in sell_trades]
460
+ ax2.scatter(sell_times, sell_prices, color='red', marker='v', s=100, label='Sell', zorder=5)
461
+
462
+ ax2.set_title('Price Curve and Trading Signals', fontsize=14, fontweight='bold')
463
+ ax2.set_ylabel('Price (USDT)', fontsize=12)
464
+ ax2.legend()
465
+ ax2.grid(True, alpha=0.3)
466
+
467
+ # 3. Drawdown Curve
468
+ ax3 = axes[2]
469
+ equity_values = equity_df['equity'].values
470
+ peak = np.maximum.accumulate(equity_values)
471
+ drawdown = (equity_values - peak) / peak * 100
472
+ ax3.fill_between(equity_df['datetime'], drawdown, 0, color='red', alpha=0.3, label='Drawdown')
473
+ ax3.plot(equity_df['datetime'], drawdown, color='red', linewidth=1)
474
+ ax3.set_title('Drawdown Curve', fontsize=14, fontweight='bold')
475
+ ax3.set_ylabel('Drawdown (%)', fontsize=12)
476
+ ax3.set_xlabel('Time', fontsize=12)
477
+ ax3.legend()
478
+ ax3.grid(True, alpha=0.3)
479
+
480
+ plt.tight_layout()
481
+
482
+ # 保存图片
483
+ if save_image:
484
+ plt.savefig(save_image, dpi=300, bbox_inches='tight')
485
+ print(f"图片已保存到: {save_image}")
486
+
487
+ plt.show()
488
+
489
+ # 保存JSON结果
490
+ if save_json:
491
+ self.save_results(results, save_json)
492
+
493
+ # 打印统计信息
494
+ # self.print_results(results)
495
+
496
+ def print_results(self, results: Dict):
497
+ """
498
+ 打印回测结果
499
+
500
+ Args:
501
+ results: backtest返回的结果字典
502
+ """
503
+ print(f"\n{'='*60}")
504
+ print("回测结果统计")
505
+ print(f"{'='*60}")
506
+ print(f"初始资金: {results['initial_capital']:.2f} USDT")
507
+ print(f"最终资金: {results['final_capital']:.2f} USDT")
508
+ print(f"总收益率: {results['total_return']:.2%}")
509
+ print(f"\n交易统计:")
510
+ print(f" 总交易次数: {results['total_trades']}")
511
+ print(f" 盈利交易: {results['win_trades']}")
512
+ print(f" 亏损交易: {results['loss_trades']}")
513
+ print(f" 胜率: {results['win_rate']:.2%}")
514
+ print(f"\n风险指标:")
515
+ print(f" 最大回撤: {results['max_drawdown']:.2%}")
516
+ print(f" 夏普比率: {results['sharpe_ratio']:.2f}")
517
+ print(f"{'='*60}\n")
518
+
519
+ def save_results(self, results: Dict, filepath: str):
520
+ """
521
+ 保存回测结果到JSON文件
522
+
523
+ Args:
524
+ results: backtest返回的结果字典
525
+ filepath: 保存路径
526
+ """
527
+ output = results.copy()
528
+
529
+ # 转换DataFrame为字典
530
+ output['equity_curve'] = output['equity_curve'].to_dict('records')
531
+
532
+ # 转换datetime为字符串
533
+ for record in output['equity_curve']:
534
+ if isinstance(record['datetime'], (pd.Timestamp, datetime)):
535
+ record['datetime'] = record['datetime'].isoformat()
536
+
537
+ for trade in output['trades']:
538
+ if isinstance(trade['datetime'], (pd.Timestamp, datetime)):
539
+ trade['datetime'] = trade['datetime'].isoformat()
540
+
541
+ with open(filepath, 'w', encoding='utf-8') as f:
542
+ json.dump(output, f, indent=2, ensure_ascii=False)
543
+
544
+ print(f"结果已保存到: {filepath}")
545
+
@@ -0,0 +1,28 @@
1
+ import os
2
+ import logging
3
+ from binance_common.configuration import ConfigurationRestAPI
4
+ from binance_common.constants import ALGO_REST_API_PROD_URL
5
+ from binance_sdk_algo.algo import Algo
6
+
7
+ logging.basicConfig(level=logging.INFO)
8
+
9
+ # API credentials - can be set via environment variables or use defaults below
10
+ API_KEY = os.getenv("API_KEY", "yNCZdF58V32y7oL2EATCIUKlmn8wkQ8ywoQukGIR7w4nkXBLldUFgld68I2xN0fj")
11
+ API_SECRET = os.getenv("API_SECRET", "xktvKv6fcTxcgGeLrAmC3MMpX5qcDntzvBByVTPTyHEsNThg7rHoRW48qQhUpP0k")
12
+
13
+ # Create configuration for the REST API
14
+ configuration_rest_api = ConfigurationRestAPI(
15
+ api_key=API_KEY,
16
+ api_secret=API_SECRET,
17
+ base_path=os.getenv("BASE_PATH", ALGO_REST_API_PROD_URL),
18
+ )
19
+
20
+ client = Algo(config_rest_api=configuration_rest_api)
21
+
22
+ try:
23
+ response = client.rest_api.query_historical_algo_orders_spot_algo()
24
+
25
+ data = response.data()
26
+ logging.info(f"query_historical_algo_orders_spot_algo() response: {data}")
27
+ except Exception as e:
28
+ logging.error(f"query_historical_algo_orders_spot_algo() error: {e}")
@@ -0,0 +1,15 @@
1
+ """
2
+ 数据获取模块
3
+
4
+ 提供从 Binance 获取期货和现货K线数据的功能
5
+ """
6
+
7
+ from .get_futures_data import get_and_save_futures_klines
8
+ from .get_trending_data import get_and_save_klines, get_and_save_klines_direct
9
+
10
+ __all__ = [
11
+ 'get_and_save_futures_klines',
12
+ 'get_and_save_klines',
13
+ 'get_and_save_klines_direct',
14
+ ]
15
+