openfund-maker 2.2.9__py3-none-any.whl → 2.3.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.
@@ -0,0 +1,404 @@
1
+ # -*- coding: utf-8 -*-
2
+ import traceback
3
+ from typing import override
4
+
5
+ from maker.StrategyMaker import StrategyMaker
6
+
7
+ class BestTopDownStrategyMaker(StrategyMaker):
8
+ def __init__(self, config, platform_config, common_config, logger=None, exchangeKey='okx'):
9
+ super().__init__(config, platform_config, common_config, logger, exchangeKey)
10
+ self.htf_last_struct = {} # 缓存HTF的最后一个结构
11
+ self.logger = logger
12
+
13
+ @override
14
+ def reset_all_cache(self, symbol):
15
+ """
16
+ 重置所有缓存
17
+ """
18
+ super().reset_all_cache(symbol)
19
+ self.htf_last_struct.pop(symbol, None)
20
+ self.clear_cache_historical_klines_df(symbol)
21
+
22
+ @override
23
+ def process_pair(self,symbol,pair_config):
24
+ self.logger.info("-" * 60)
25
+ """_summary_
26
+ HTF (Daily & 4H)
27
+ 1.1. Price's Current Trend 市场趋势
28
+ 1.2. Who's In Control 供需控制
29
+ 1.3. Key Support & Resistance Levels 关键位置
30
+ ATF (1H & 30 Min & 15 Min)
31
+ 2.1. Market Condition
32
+ 2.2. PD Arrays
33
+ 2.3. Liquidity Areas
34
+ ETF (5 Min & 1 Min)
35
+ 1. Reversal Signs
36
+ 2. PD Arrays
37
+ 3. Place Order
38
+
39
+ """
40
+ try:
41
+ # 是否有持仓,有持仓不进行下单
42
+ if self.fetch_position(symbol=symbol) :
43
+ self.reset_all_cache(symbol)
44
+ self.logger.info(f"{symbol} : 有持仓合约,不进行下单。")
45
+ return
46
+ precision = self.get_precision_length(symbol)
47
+
48
+ top_down_strategy = pair_config.get('top_down_strategy',{})
49
+
50
+ """
51
+ 获取策略配置
52
+ """
53
+ htf = str(top_down_strategy.get('HTF','4h'))
54
+ atf = str(top_down_strategy.get('ATF','15m'))
55
+ etf = str(top_down_strategy.get('ETF', '1m'))
56
+
57
+ self.logger.info(f"{symbol} : TopDownSMC策略 {htf}|{atf}|{etf} \n")
58
+ market_price = self.get_market_price(symbol=symbol)
59
+
60
+ """
61
+ step 1 : Higher Time Frame Analysis
62
+ """
63
+ step = "1"
64
+ # 初始化HTF趋势相关变量
65
+ htf_side, htf_struct, htf_trend = None, None, None
66
+ # HTF 缓存,减小流量损耗
67
+ htf_df = self.get_historical_klines_df_by_cache(symbol=symbol, tf=htf)
68
+ htf_struct =self.build_struct(symbol=symbol, data=htf_df)
69
+
70
+ htf_latest_struct = self.get_latest_struct(symbol=symbol, data=htf_struct)
71
+ htf_trend = htf_latest_struct[self.STRUCT_DIRECTION_COL]
72
+ htf_side = self.BUY_SIDE if htf_trend == self.BULLISH_TREND else self.SELL_SIDE
73
+ # 1.1. Price's Current Trend 市场趋势(HTF)
74
+ step = "1.1"
75
+ self.logger.info(f"{symbol} : {step}. HTF {htf} Price's Current Trend is {htf_trend}。")
76
+ # 1.2. Who's In Control 供需控制,Bullish 或者 Bearish | Choch 或者 BOS
77
+ step = "1.2"
78
+ self.logger.info(f"{symbol} : {step}. HTF {htf} struct is {htf_latest_struct[self.STRUCT_COL]}。")
79
+
80
+ # 1.3. HTF Key Support & Resistance Levels 支撑或阻力关键位置(HTF 看上下的供需区位置)
81
+ step = "1.3"
82
+ htf_OBs_df = self.find_OBs(symbol=symbol,struct=htf_struct)
83
+
84
+ if htf_OBs_df is None or len(htf_OBs_df) == 0:
85
+ self.logger.debug(f"{symbol} : {step}. HTF {htf} 未找到OB。")
86
+ return
87
+ else:
88
+ # self.logger.debug(f"{symbol} : {step}. HTF {htf} 找到OB。")
89
+
90
+ htf_support_OB = self.get_latest_OB(symbol=symbol,data=htf_OBs_df,trend=self.BULLISH_TREND)
91
+ if htf_support_OB :
92
+ htf_support_price = htf_support_OB.get(self.OB_MID_COL)
93
+ else:
94
+ htf_support_price = htf_struct.at[htf_struct.index[-1], self.STRUCT_LOW_COL]
95
+
96
+ htf_resistance_OB = self.get_latest_OB(symbol=symbol,data=htf_OBs_df,trend=self.BEARISH_TREND)
97
+ if htf_resistance_OB :
98
+ htf_resistance_price = htf_resistance_OB.get(self.OB_MID_COL)
99
+ else:
100
+ htf_resistance_price = htf_struct.at[htf_struct.index[-1], self.STRUCT_HIGH_COL]
101
+ self.logger.info(f"{symbol} : {step}. HTF {htf}, Key Support={htf_support_price:.{precision}f} & Key Resistance={htf_resistance_price:.{precision}f} ")
102
+ #1.4. 检查关键支撑位和阻力位之间是否有利润空间。
103
+ step = "1.4"
104
+ # 计算支撑位和阻力位之间的利润空间百分比
105
+ htf_profit_percent = abs((htf_resistance_price - htf_support_price) / htf_support_price * 100)
106
+ min_profit_percent = pair_config.get('min_profit_percent', 4) # 默认最小利润空间为0.5%
107
+
108
+ if htf_profit_percent < min_profit_percent:
109
+ self.logger.info(f"{symbol} : {step}. HTF {htf} 支撑位={htf_support_price:.{precision}f} 与阻力位={htf_resistance_price:.{precision}f} 之间利润空间{htf_profit_percent:.2f}% < {min_profit_percent}%,等待...")
110
+ return
111
+ else:
112
+ self.logger.info(f"{symbol} : {step}. HTF {htf} 支撑位={htf_support_price:.{precision}f} 与阻力位={htf_resistance_price:.{precision}f} 之间利润空间{htf_profit_percent:.2f}% >= {min_profit_percent}%")
113
+
114
+ # 1.5. 检查当前价格是否在关键支撑位和阻力位,支撑位可以做多,阻力位可以做空。
115
+ step = "1.5"
116
+ htf_support_OB_top = None
117
+ if htf_support_OB :
118
+ htf_support_OB_top = htf_support_OB.get(self.OB_HIGH_COL)
119
+ htf_resistance_OB_bottom = None
120
+ if htf_resistance_OB :
121
+ htf_resistance_OB_bottom = htf_resistance_OB.get(self.OB_LOW_COL)
122
+
123
+ # 检查支撑位做多条件
124
+ if htf_support_OB_top is not None:
125
+ if market_price <= htf_support_OB_top:
126
+ # 价格进入支撑OB,可以开始做多
127
+ if htf_side != self.BUY_SIDE:
128
+ htf_side = self.BUY_SIDE
129
+ self.logger.info(f"{symbol} : {step}. HTF {htf} 当前价格{market_price:.{precision}f} <= HTF_OB_TOP{htf_support_OB_top:.{precision}f}, 开始做多{htf_side}。")
130
+ else:
131
+ self.logger.info(f"{symbol} : {step}. HTF {htf} 当前价格{market_price:.{precision}f} > HTF_OB_TOP{htf_support_OB_top:.{precision}f}, 无需做多{htf_side}。")
132
+ else:
133
+ self.logger.info(f"{symbol} : {step}. HTF {htf} 未找到HTF_OB_TOP。")
134
+
135
+ # 检查阻力位做空条件
136
+ if htf_resistance_OB_bottom is not None:
137
+ if market_price >= htf_resistance_OB_bottom:
138
+ # 价格进入阻力OB,可以开始做空
139
+ if htf_side != self.SELL_SIDE:
140
+ htf_side = self.SELL_SIDE
141
+ self.logger.info(f"{symbol} : {step}. HTF {htf} 当前价格{market_price:.{precision}f} >= HTF_OB_BOTTOM{htf_resistance_OB_bottom:.{precision}f}, 开始做空{htf_side}。")
142
+ else:
143
+ self.logger.info(f"{symbol} : {step}. HTF {htf} 当前价格{market_price:.{precision}f} < HTF_OB_BOTTOM{htf_resistance_OB_bottom:.{precision}f}, 无需做空{htf_side}。")
144
+ else:
145
+ self.logger.info(f"{symbol} : {step}. HTF {htf} 未找到HTF_OB_BOTTOM。")
146
+
147
+ """
148
+ step 2 : Analysis Time Frames
149
+ """
150
+ # 2. ATF Step
151
+ # 2.1 Market Condition 市场状况(ATF 看上下的供需区位置)
152
+
153
+ atf_side, atf_struct, atf_trend = None, None, None
154
+ atf_df = self.get_historical_klines_df(symbol=symbol, tf=atf)
155
+ atf_struct =self.build_struct(symbol=symbol, data=atf_df)
156
+ # 获取最新的市场结构,如果为空则返回None
157
+ atf_latest_struct = self.get_latest_struct(symbol=symbol, data=atf_struct)
158
+ if atf_latest_struct is None:
159
+ self.logger.info(f"{symbol} : {step}. ATF {atf} 未形成结构,等待... ")
160
+ return
161
+ atf_trend = atf_latest_struct[self.STRUCT_DIRECTION_COL]
162
+ atf_side = self.BUY_SIDE if atf_trend == self.BULLISH_TREND else self.SELL_SIDE
163
+ # 2.1. Price's Current Trend 市场趋势(HTF )
164
+ step = "2.1"
165
+ self.logger.info(f"{symbol} : {step}. ATF {atf} Price's Current Trend is {atf_trend}。")
166
+ # 2.2. Who's In Control 供需控制,Bullish 或者 Bearish | Choch 或者 BOS
167
+ step = "2.2"
168
+ self.logger.info(f"{symbol} : {step}. ATF {atf} struct is {atf_latest_struct[self.STRUCT_COL]}。")
169
+ # 2.3. 检查关键支撑位和阻力位之间是否有利润空间。
170
+ step = "2.3"
171
+ atf_OBs_df = self.find_OBs(symbol=symbol,struct=atf_struct)
172
+ atf_support_OB = self.get_latest_OB(symbol=symbol,data=atf_OBs_df,trend=self.BULLISH_TREND)
173
+ if atf_support_OB :
174
+ atf_support_price = atf_support_OB.get(self.OB_MID_COL)
175
+ else:
176
+ atf_support_price = atf_struct.at[atf_struct.index[-1], self.STRUCT_LOW_COL]
177
+
178
+ atf_resistance_OB = self.get_latest_OB(symbol=symbol,data=atf_OBs_df,trend=self.BEARISH_TREND)
179
+ if atf_resistance_OB :
180
+ atf_resistance_price = atf_resistance_OB.get(self.OB_MID_COL)
181
+ else:
182
+ atf_resistance_price = atf_struct.at[atf_struct.index[-1], self.STRUCT_HIGH_COL]
183
+
184
+ self.logger.info(f"{symbol} : {step}.1 ATF {atf}, Key Support={atf_support_price:.{precision}f} "
185
+ f"& Key Resistance={atf_resistance_price:.{precision}f} ")
186
+ # 计算支撑位和阻力位之间的利润空间百分比
187
+ atf_profit_percent = abs((atf_resistance_price - atf_support_price) / atf_support_price * 100)
188
+
189
+ if atf_profit_percent < min_profit_percent:
190
+ self.logger.info(f"{symbol} : {step}.2 ATF {atf} 支撑位={atf_support_price:.{precision}f} 与阻力位={atf_resistance_price:.{precision}f} "
191
+ f"之间利润空间{atf_profit_percent:.2f}% < {min_profit_percent}%,等待...")
192
+ return
193
+ else:
194
+ self.logger.info(f"{symbol} : {step}.2 ATF {atf} 支撑位={atf_support_price:.{precision}f} 与阻力位={atf_resistance_price:.{precision}f} "
195
+ f"之间利润空间{atf_profit_percent:.2f}% >= {min_profit_percent}%,允许下单...")
196
+
197
+
198
+
199
+ # 2.4. ATF 方向要和 HTF方向一致
200
+ step = "2.4"
201
+
202
+ if htf_side != atf_side:
203
+ self.logger.info(f"{symbol} : {step}. ATF {atf} is {atf_side} 与 HTF {htf} is {htf_side} 不一致,等待...")
204
+ return
205
+ else:
206
+ self.logger.info(f"{symbol} : {step}. ATF {atf} is {atf_side} 与 HTF {htf} is {htf_side} 一致。")
207
+
208
+ #2.5. 反转结构CHOCH, check Liquidity Areas ,检查当前结构是否是流动性摄取。
209
+ step = "2.5"
210
+ # if "CHOCH" in atf_struct[self.STRUCT_COL] or "BOS" in atf_struct[self.STRUCT_COL]:
211
+ # 2.5.1. Equal Lows & Equal Highs
212
+ end_idx = atf_latest_struct[self.STRUCT_HIGH_INDEX_COL] if atf_side == self.BUY_SIDE else atf_latest_struct[self.STRUCT_LOW_INDEX_COL]
213
+ last_EQ = self.find_EQH_EQL(symbol=symbol, data=atf_df, trend=atf_trend, end_idx=end_idx, pair_config=pair_config)
214
+ if last_EQ and last_EQ[self.HAS_EQ_KEY]:
215
+ price_eq = last_EQ[self.EQUAL_HIGH_COL] if atf_side == self.BUY_SIDE else last_EQ[self.EQUAL_LOW_COL]
216
+ self.logger.info(f"{symbol} : {step}.1 ATF {atf} {atf_side} find EQ {price_eq}")
217
+ # 检查是否Liquidity Sweeps
218
+ if (atf_side == self.BUY_SIDE and atf_latest_struct[self.STRUCT_HIGH_COL] > price_eq) \
219
+ or (atf_side == self.SELL_SIDE and atf_latest_struct[self.STRUCT_LOW_COL] < price_eq):
220
+
221
+ atf_side = self.SELL_SIDE if atf_side == self.BUY_SIDE else self.BUY_SIDE
222
+ self.logger.info(f"{symbol} : {step}.1 ATF {atf} Liquidity Sweeps , Reverse the ATF {atf} {atf_side} side。")
223
+ else:
224
+ self.logger.info(f"{symbol} : {step}.1 ATF {atf} is not found Liquidity Sweeps .")
225
+ else:
226
+ self.logger.info(f"{symbol} : {step}.1 ATF {atf} is not found EQ .")
227
+
228
+ # FIXME 2.5.2. Dynamic Trendlines and Channels
229
+ # atf_pre_struct = atf_struct[atf_struct[self.STRUCT_DIRECTION_COL].notna()].iloc[-2] # 看前一个结构是否为动态趋势
230
+ # atf_start_index = min(atf_pre_struct[self.STRUCT_LOW_INDEX_COL] ,atf_pre_struct[self.STRUCT_HIGH_INDEX_COL])
231
+ # atf_end_index = max(atf_latest_struct[self.STRUCT_LOW_INDEX_COL] ,atf_latest_struct[self.STRUCT_HIGH_INDEX_COL])
232
+
233
+ # is_dynamic_trendlines = self.identify_dynamic_trendlines(symbol=symbol, data=atf_struct, trend=atf_trend, start_idx=atf_start_index, end_idx=atf_end_index)
234
+ # if is_dynamic_trendlines :
235
+ # self.logger.info(f"{symbol} : {step}.2 ATF {atf} {atf_trend} find Dynamic Trendlines .")
236
+ # else:
237
+ # self.logger.info(f"{symbol} : {step}.2 ATF {atf} {atf_trend} not find Dynamic Trendlines .")
238
+
239
+
240
+ # 2.6. 在HTF供需区范围,找ATF的PDArray,FVG和OB,供需区,计算监测下单区域范围。
241
+ step = "2.6"
242
+ atf_pdArrays_df = self.find_PDArrays(symbol=symbol,struct=atf_struct,side=atf_side)
243
+
244
+ # 不同的结构,不同位置,如果是Choch则等待价格进入PDArray,如果是BOS则等待价格进入折价区
245
+ # 划分 折价(discount)区和溢价(premium)区
246
+ atf_struct_high = atf_latest_struct[self.STRUCT_HIGH_COL]
247
+ atf_struct_low = atf_latest_struct[self.STRUCT_LOW_COL]
248
+ atf_struct_mid = atf_latest_struct[self.STRUCT_MID_COL]
249
+
250
+ if "CHOCH" in atf_struct[self.STRUCT_COL]:
251
+ # 找PDArray,Bullish 则PDArray的mid要小于 atf_struct_mid,Bearish 则PDArray的mid要大于 atf_struct_mid
252
+ # atf_discount_mid = (atf_struct_mid + atf_struct_high) / 2 if atf_trend == self.BEARISH_TREND else (atf_struct_mid + atf_struct_low) / 2
253
+ mask = atf_pdArrays_df[self.PD_MID_COL] >= atf_struct_mid if atf_side == self.BUY_SIDE else atf_pdArrays_df[self.PD_MID_COL] <= atf_struct_mid
254
+ atf_pdArrays_df = atf_pdArrays_df[mask]
255
+ if len(atf_pdArrays_df) == 0:
256
+ self.logger.info(f"{symbol} : {step}.1. ATF {atf} 未找到PDArray,不下单")
257
+ return
258
+ else:
259
+ # 找到最新的PDArray
260
+ atf_vaild_pdArray = atf_pdArrays_df.iloc[-1]
261
+ self.logger.info(f"{symbol} : {step}.1. ATF {atf} 找到PDArray\n"
262
+ f"{atf_vaild_pdArray[[self.TIMESTAMP_COL,self.PD_TYPE_COL,self.PD_HIGH_COL,self.PD_LOW_COL,self.PD_MID_COL]]}。")
263
+
264
+
265
+ #SMS
266
+ elif "SMS" in atf_struct[self.STRUCT_COL]:
267
+ mask = atf_pdArrays_df[self.PD_MID_COL] >= atf_struct_mid if atf_side == self.BUY_SIDE else atf_pdArrays_df[self.PD_MID_COL] <= atf_struct_mid
268
+ atf_pdArrays_df = atf_pdArrays_df[mask]
269
+ if len(atf_pdArrays_df) == 0:
270
+ self.logger.info(f"{symbol} : {step}.1. ATF {atf} 在{atf_struct_mid:.{precision}f}未找到PDArray,不下单")
271
+ return
272
+ else:
273
+ # 找到最新的PDArray
274
+ atf_vaild_pdArray = atf_pdArrays_df.iloc[-1]
275
+ self.logger.info(f"{symbol} : {step}.1. ATF {atf} 找到PDArray\n"
276
+ f"{atf_vaild_pdArray[[self.TIMESTAMP_COL,self.PD_TYPE_COL,self.PD_HIGH_COL,self.PD_LOW_COL,self.PD_MID_COL]]}。")
277
+
278
+
279
+ #BMS
280
+ else:
281
+ atf_premium_mid = (atf_struct_mid + atf_struct_low) / 2 if atf_side == self.BUY_SIDE else (atf_struct_mid + atf_struct_high) / 2
282
+ mask = atf_pdArrays_df[self.PD_HIGH_COL] >= atf_premium_mid if atf_side == self.BUY_SIDE else atf_pdArrays_df[self.PD_LOW_COL] <= atf_premium_mid
283
+ atf_pdArrays_df = atf_pdArrays_df[mask]
284
+ if len(atf_pdArrays_df) == 0:
285
+ self.logger.info(f"{symbol} : {step}.1. ATF {atf} ,在{atf_premium_mid:.{precision}f}未找到PDArray,不下单")
286
+ return
287
+ else:
288
+ # 找到最新的PDArray
289
+ atf_vaild_pdArray = atf_pdArrays_df.iloc[-1]
290
+ self.logger.info(f"{symbol} : {step}.1. ATF {atf} 找到PDArray\n"
291
+ f"{atf_vaild_pdArray[[self.TIMESTAMP_COL,self.PD_TYPE_COL,self.PD_HIGH_COL,self.PD_LOW_COL,self.PD_MID_COL]]}")
292
+
293
+
294
+
295
+ step = "2.7"
296
+
297
+ # 2.7. 等待价格进入 PDArray
298
+
299
+ if not (market_price <= atf_vaild_pdArray[self.PD_HIGH_COL] and market_price >= atf_vaild_pdArray[self.PD_LOW_COL]):
300
+ self.logger.info(f"{symbol} : {step}. ATF {atf} market_price={market_price:.{precision}f} 未达到PDArray范围。"
301
+ f"PD_HIGH={atf_vaild_pdArray[self.PD_LOW_COL]:.{precision}f} "
302
+ f"PD_LOW={atf_vaild_pdArray[self.PD_HIGH_COL]:.{precision}f} ")
303
+
304
+ return
305
+ else:
306
+ self.logger.info(f"{symbol} : {step}. ATF {atf} market_price={market_price:.{precision}f} 已到达PDArray范围。"
307
+ f"PD_HIGH={atf_vaild_pdArray[self.PD_LOW_COL]:.{precision}f} "
308
+ f"PD_LOW={atf_vaild_pdArray[self.PD_HIGH_COL]:.{precision}f} ")
309
+
310
+
311
+ # 3. ETF Step
312
+ step = "3.1"
313
+ etf_side, etf_struct, etf_trend = None, None, None
314
+ etf_df = self.get_historical_klines_df(symbol=symbol, tf=etf)
315
+ etf_struct =self.build_struct(symbol=symbol, data=etf_df)
316
+ etf_latest_struct = self.get_latest_struct(symbol=symbol, data=etf_struct)
317
+
318
+ # 初始化ETF趋势相关变量
319
+ if etf_latest_struct is None:
320
+ self.logger.info(f"{symbol} : {step}. ETF {etf} 未形成结构,等待... ")
321
+ return
322
+ etf_trend = etf_latest_struct[self.STRUCT_DIRECTION_COL]
323
+ etf_side = self.BUY_SIDE if etf_trend == self.BULLISH_TREND else self.SELL_SIDE
324
+
325
+ # 3.1. Price's Current Trend 市场趋势(ETF )
326
+ step = "3.1"
327
+ self.logger.info(f"{symbol} : {step}. ETF {etf} Price's Current Trend is {etf_trend}。")
328
+ # 3.2. Who's In Control 供需控制,Bullish 或者 Bearish | Choch 或者 BOS
329
+ step = "3.2"
330
+ self.logger.info(f"{symbol} : {step}. ETF {etf} struct is {etf_latest_struct[self.STRUCT_COL]}。")
331
+
332
+
333
+ # 3.3 Reversal Signs 反转信号
334
+ step = "3.3"
335
+
336
+ if atf_side != etf_side:
337
+
338
+ self.logger.info(f"{symbol} : {step}. ETF {etf} 市场结构{etf_latest_struct[self.STRUCT_COL]}未反转,等待...")
339
+ return
340
+ else:
341
+ self.logger.info(f"{symbol} : {step}. ETF {etf} 市场结构{etf_latest_struct[self.STRUCT_COL]}已反转。")
342
+
343
+ # TODO "CHOCH"|"BOS" 的PDArray 入场位置不一样
344
+
345
+ # 3.4 找 PD Arrays 价格区间(ETF 看上下的供需区位置)
346
+ step = "3.4"
347
+ etf_pdArrays_df = self.find_PDArrays(symbol=symbol,struct=etf_struct,side=etf_side)
348
+ # 划分 折价(discount)区和溢价(premium)区
349
+ etf_struct_high = etf_latest_struct[self.STRUCT_HIGH_COL]
350
+ etf_struct_low = etf_latest_struct[self.STRUCT_LOW_COL]
351
+ etf_struct_mid = etf_latest_struct[self.STRUCT_MID_COL]
352
+ mask = etf_pdArrays_df[self.PD_MID_COL] >= etf_struct_mid if etf_side == self.SELL_SIDE else etf_pdArrays_df[self.PD_MID_COL] <= etf_struct_mid
353
+ etf_pdArrays_df = etf_pdArrays_df[mask]
354
+ if len(etf_pdArrays_df) == 0:
355
+ self.logger.info(f"{symbol} : {step}.1. ETF {etf} 未找到PDArray,不下单")
356
+ return
357
+ else:
358
+ # 找到最新的PDArray
359
+ etf_vaild_pdArray = etf_pdArrays_df.iloc[-1]
360
+ self.logger.info(f"{symbol} : {step}.1. ETF {etf} 找到PDArray.\n"
361
+ f"{etf_vaild_pdArray[[self.TIMESTAMP_COL,self.PD_TYPE_COL,self.PD_HIGH_COL,self.PD_LOW_COL,self.PD_MID_COL]]}。")
362
+
363
+
364
+ if not (market_price <= etf_vaild_pdArray[self.PD_HIGH_COL] and market_price >= etf_vaild_pdArray[self.PD_LOW_COL]):
365
+ self.logger.info(f"{symbol} : {step}.2. ETF {etf} market_price={market_price:.{precision}f} 未达到PDArray范围。"
366
+ f"PD_HIGH={etf_vaild_pdArray[self.PD_HIGH_COL]:.{precision}f} "
367
+ f"PD_LOW={etf_vaild_pdArray[self.PD_LOW_COL]:.{precision}f}")
368
+
369
+ return
370
+ else:
371
+ self.logger.info(f"{symbol} : {step}.2. ETF {etf} market_price={market_price:.{precision}f} 已到达PDArray范围。"
372
+ f"PD_HIGH={etf_vaild_pdArray[self.PD_HIGH_COL]:.{precision}f} "
373
+ f"PD_LOW={etf_vaild_pdArray[self.PD_LOW_COL]:.{precision}f}")
374
+
375
+ # 3.5 Place Order 下单
376
+ step = "3.5"
377
+ # order_price = self.toDecimal(etf_vaild_pdArray[self.PD_HIGH_COL] if etf_trend == self.BULLISH_TREND else etf_vaild_pdArray[self.PD_LOW_COL] )
378
+ order_price = self.toDecimal(etf_vaild_pdArray[self.PD_MID_COL])
379
+
380
+ latest_order_price = self.toDecimal(self.place_order_prices.get(symbol,0))
381
+ if order_price == latest_order_price:
382
+ self.logger.info(f"{symbol} : {step}. ETF {etf}, 下单价格 {order_price:.{precision}} 未变化,不进行下单。")
383
+ return
384
+
385
+ self.cancel_all_orders(symbol=symbol)
386
+ self.place_order(symbol=symbol, price=order_price, side=etf_side, pair_config=pair_config)
387
+ self.place_order_prices[symbol] = order_price # 记录下单价格,过滤重复下单
388
+ self.logger.info(f"{symbol} : {step}. ETF {etf}, {etf_side} 价格={order_price:.{precision}}")
389
+
390
+
391
+ except KeyboardInterrupt:
392
+ self.logger.info("程序收到中断信号,开始退出...")
393
+ except Exception as e:
394
+ error_message = f"程序异常退出: {str(e)}"
395
+ # 记录错误信息和堆栈跟踪
396
+ self.logger.error(f"{error_message}\n{traceback.format_exc()}")
397
+ traceback.print_exc()
398
+ self.send_feishu_notification(symbol, error_message)
399
+ finally:
400
+ self.logger.info("=" * 60 + "\n")
401
+
402
+
403
+
404
+
maker/StrategyMaker.py ADDED
@@ -0,0 +1,487 @@
1
+ # -*- coding: utf-8 -*-
2
+ from functools import lru_cache
3
+ import pandas as pd
4
+ from datetime import datetime, timedelta
5
+ from decimal import Decimal
6
+ from abc import abstractmethod
7
+ from concurrent.futures import ThreadPoolExecutor, as_completed
8
+ from core.utils.OPTools import OPTools
9
+ from core.Exchange import Exchange
10
+ # 导入SMC相关模块
11
+ from core.smc import (
12
+ SMCBase,
13
+ SMCPDArray,
14
+ SMCStruct,
15
+ SMCOrderBlock,
16
+ SMCFVG,
17
+ SMCLiquidity
18
+ )
19
+
20
+ class StrategyMaker():
21
+ BUY_SIDE = 'buy'
22
+ SELL_SIDE = 'sell'
23
+ BULLISH_TREND = 'Bullish'
24
+ BEARISH_TREND = 'Bearish'
25
+
26
+ HIGH_COL = SMCBase.SMCBase.HIGH_COL
27
+ LOW_COL = SMCBase.SMCBase.LOW_COL
28
+ CLOSE_COL = SMCBase.SMCBase.CLOSE_COL
29
+ OPEN_COL = SMCBase.SMCBase.OPEN_COL
30
+ TIMESTAMP_COL = SMCBase.SMCBase.TIMESTAMP_COL
31
+ VOLUME_COL = SMCBase.SMCBase.VOLUME_COL
32
+
33
+ STRUCT_COL = SMCStruct.SMCStruct.STRUCT_COL
34
+ STRUCT_HIGH_COL = SMCStruct.SMCStruct.STRUCT_HIGH_COL
35
+ STRUCT_LOW_COL = SMCStruct.SMCStruct.STRUCT_LOW_COL
36
+ STRUCT_MID_COL = SMCStruct.SMCStruct.STRUCT_MID_COL
37
+ STRUCT_HIGH_INDEX_COL = SMCStruct.SMCStruct.STRUCT_HIGH_INDEX_COL
38
+ STRUCT_LOW_INDEX_COL = SMCStruct.SMCStruct.STRUCT_LOW_INDEX_COL
39
+ STRUCT_DIRECTION_COL = SMCStruct.SMCStruct.STRUCT_DIRECTION_COL
40
+ HIGH_START_COL = SMCStruct.SMCStruct.HIGH_START_COL
41
+ LOW_START_COL = SMCStruct.SMCStruct.LOW_START_COL
42
+
43
+ OB_HIGH_COL = SMCOrderBlock.SMCOrderBlock.OB_HIGH_COL
44
+ OB_LOW_COL = SMCOrderBlock.SMCOrderBlock.OB_LOW_COL
45
+ OB_MID_COL = SMCOrderBlock.SMCOrderBlock.OB_MID_COL
46
+ OB_VOLUME_COL = SMCOrderBlock.SMCOrderBlock.OB_VOLUME_COL
47
+ OB_DIRECTION_COL = SMCOrderBlock.SMCOrderBlock.OB_DIRECTION_COL
48
+ OB_ATR = SMCOrderBlock.SMCOrderBlock.OB_ATR
49
+ OB_IS_COMBINED = SMCOrderBlock.SMCOrderBlock.OB_IS_COMBINED
50
+ OB_WAS_CROSSED = SMCOrderBlock.SMCOrderBlock.OB_WAS_CROSSED
51
+
52
+ PD_HIGH_COL = SMCPDArray.SMCPDArray.PD_HIGH_COL
53
+ PD_LOW_COL = SMCPDArray.SMCPDArray.PD_LOW_COL
54
+ PD_MID_COL = SMCPDArray.SMCPDArray.PD_MID_COL
55
+ PD_TYPE_COL = SMCPDArray.SMCPDArray.PD_TYPE_COL
56
+
57
+ LIQU_HIGH_COL = SMCLiquidity.SMCLiquidity.LIQU_HIGH_COL
58
+ LIQU_LOW_COL = SMCLiquidity.SMCLiquidity.LIQU_LOW_COL
59
+ EQUAL_HIGH_COL = SMCLiquidity.SMCLiquidity.EQUAL_HIGH_COL
60
+ EQUAL_LOW_COL = SMCLiquidity.SMCLiquidity.EQUAL_LOW_COL
61
+ EQH_INDEX_KEY = SMCLiquidity.SMCLiquidity.EQUAL_HIGH_INDEX_KEY
62
+ EQL_INDEX_KEY = SMCLiquidity.SMCLiquidity.EQUAL_LOW_INDEX_KEY
63
+ HAS_EQ_KEY = SMCLiquidity.SMCLiquidity.HAS_EQ_KEY
64
+
65
+ def __init__(self, config, platform_config, common_config, feishu_webhook=None, logger=None ,exchangeKey='okx'):
66
+ """_summary_
67
+ 初始化
68
+ Args:
69
+ config (_type_): _description_
70
+ platform_config (_type_): _description_
71
+ common_config (_type_): _description_
72
+ feishu_webhook (_type_, optional): _description_. Defaults to None.
73
+ logger (_type_, optional): _description_. Defaults to None.
74
+ """
75
+ self.logger = logger
76
+ self.g_config = config
77
+
78
+ self.common_config = common_config
79
+ self.feishu_webhook = self.common_config.get('feishu_webhook',"")
80
+
81
+ self.strategy_config = self.g_config.get('strategy', {})
82
+ self.trading_pairs_config = self.g_config.get('tradingPairs', {})
83
+
84
+ self.leverage_value = self.strategy_config.get('leverage', 20)
85
+ self.is_demo_trading = self.common_config.get('is_demo_trading', 1) # live trading: 0, demo trading: 1
86
+ proxies = {
87
+ "http": self.common_config.get('proxy', "http://localhost:7890"),
88
+ "https": self.common_config.get('proxy', "http://localhost:7890")
89
+ }
90
+ try:
91
+ self.exchange = Exchange({
92
+ 'apiKey': platform_config["apiKey"],
93
+ 'secret': platform_config["secret"],
94
+ 'password': platform_config["password"],
95
+ 'timeout': 3000,
96
+ 'rateLimit': 50,
97
+ 'options': {'defaultType': 'future'},
98
+ 'proxies': proxies
99
+ }, exchangeKey)
100
+ except Exception as e:
101
+ self.logger.error(f"连接交易所失败: {e}")
102
+ raise Exception(f"连接交易所失败: {e}")
103
+
104
+ self.smcPDArray = SMCPDArray.SMCPDArray()
105
+ self.smcStruct = SMCStruct.SMCStruct()
106
+ self.smcOB = SMCOrderBlock.SMCOrderBlock()
107
+ self.smcFVG = SMCFVG.SMCFVG()
108
+ self.smcLiqu = SMCLiquidity.SMCLiquidity()
109
+
110
+ self.interval_map = {
111
+ '1d': 24 * 60 * 60 , # 1天
112
+ '4h': 4 * 60 * 60 , # 4小时
113
+ '1h': 60 * 60 , # 1小时
114
+ '30m': 30 * 60 , # 30分钟
115
+ '15m': 15 * 60 , # 15分钟
116
+ '5m': 5 * 60 , # 5分钟
117
+ }
118
+
119
+ self.place_order_prices = {} # 记录每个symbol的挂单价格
120
+ self.cache_time = {} # 记录缓存时间的字典
121
+
122
+
123
+ def toDecimal(self, price):
124
+ """_summary_
125
+ 将价格转换为Decimal类型
126
+ Args:
127
+ price (_type_): _description_
128
+ Returns:
129
+ _type_: _description_
130
+ """
131
+ return OPTools.toDecimal(price)
132
+
133
+ def get_pair_config(self,symbol):
134
+ # 获取交易对特定配置,如果没有则使用全局策略配置
135
+ pair_config = self.trading_pairs_config.get(symbol, {})
136
+
137
+ # 使用字典推导式合并配置,trading_pairs_config优先级高于strategy_config
138
+ pair_config = {
139
+ **self.strategy_config, # 基础配置
140
+ **pair_config # 交易对特定配置会覆盖基础配置
141
+ }
142
+ return pair_config
143
+
144
+
145
+ def send_feishu_notification(self, symbol, message):
146
+ if self.feishu_webhook:
147
+ try:
148
+ OPTools.send_feishu_notification(self.feishu_webhook,message)
149
+ except Exception as e:
150
+ self.logger.warning(f"{symbol} 发送飞书消息失败: {e}")
151
+
152
+ def get_precision_length(self, symbol):
153
+ """_summary_
154
+ 获取价格的精度长度
155
+ Args:
156
+ price (_type_): _description_
157
+ Returns:
158
+ _type_: _description_
159
+ """
160
+ tick_size = self.exchange.get_tick_size(symbol)
161
+ return self.smcStruct.get_precision_length(tick_size)
162
+
163
+ def get_market_price(self, symbol):
164
+ """_summary_
165
+ 获取最新成交价
166
+ Args:
167
+ symbol (_type_): _description_
168
+ Returns:
169
+ _type_: _description_
170
+ """
171
+ return self.exchange.get_market_price(symbol)
172
+
173
+ def place_order(self, symbol, price:Decimal, side, pair_config, leverage:int=0, order_type='limit'):
174
+ """_summary_
175
+ 下单
176
+ Args:
177
+ symbol (_type_): _description_
178
+ price (_type_): _description_
179
+ amount_usdt (_type_): _description_
180
+ side (_type_): _description_
181
+ order_type (_type_): _description_
182
+ """
183
+ # 获取做多和做空的下单金额配置
184
+ long_amount_usdt = pair_config.get('long_amount_usdt', 5)
185
+ short_amount_usdt = pair_config.get('short_amount_usdt', 5)
186
+
187
+ # 设置杠杆倍数
188
+ leverage = leverage or self.leverage_value
189
+
190
+ # 根据交易方向设置下单金额
191
+ order_amount_usdt = short_amount_usdt if side == self.SELL_SIDE else long_amount_usdt
192
+
193
+ # 记录下单日志
194
+ direction = self.BULLISH_TREND if side == self.BUY_SIDE else self.BEARISH_TREND
195
+ self.logger.info(f"{symbol} : 触发{direction}下单条件. 下单价格: {price}")
196
+
197
+ # 执行下单
198
+ try :
199
+ self.exchange.place_order(
200
+ symbol=symbol,
201
+ price=price,
202
+ amount_usdt=order_amount_usdt,
203
+ side=side,
204
+ leverage=leverage,
205
+ order_type=order_type
206
+ )
207
+ except Exception as e:
208
+ error_message = f"{symbol} 下单失败: {e}"
209
+ self.logger.warning(error_message)
210
+ self.send_feishu_notification(symbol, error_message)
211
+
212
+ def cancel_all_orders(self, symbol):
213
+ """_summary_
214
+ 取消所有挂单
215
+ Args:
216
+ symbol (_type_): _description_
217
+ """
218
+ try:
219
+ self.exchange.cancel_all_orders(symbol=symbol)
220
+ except Exception as e:
221
+ error_message = f"{symbol} 取消所有挂单失败: {e}"
222
+ self.logger.warning(error_message)
223
+ self.send_feishu_notification(symbol, error_message)
224
+
225
+ def get_historical_klines(self, symbol, tf='15m'):
226
+ """_summary_
227
+ 获取历史K线数据
228
+ Args:
229
+ symbol (_type_): _description_
230
+ bar (_type_, optional): _description_. Defaults to '15m'.
231
+ Returns:
232
+ _type_: _description_
233
+ """
234
+ return self.exchange.get_historical_klines(symbol=symbol, bar=tf)
235
+
236
+ @lru_cache(maxsize=32) # 缓存最近32个不同的请求
237
+ def _get_cache_historical_klines_df(self, symbol, tf):
238
+ """被缓存的获取K线数据的方法"""
239
+ return self.get_historical_klines_df(symbol, tf)
240
+ def clear_cache_historical_klines_df(self, symbol=None):
241
+ """
242
+ 清除指定交易对和时间周期的缓存
243
+
244
+ 参数:
245
+ symbol (str, optional): 交易对符号,如为None则清除所有缓存
246
+ tf (str, optional): 时间周期,如为None则清除所有缓存
247
+ """
248
+ if symbol is None:
249
+ # 清除所有缓存
250
+ self._get_cache_historical_klines_df.cache_clear()
251
+ self.cache_time.clear()
252
+ # print("已清除所有K线数据缓存")
253
+ else:
254
+ # 删除所有包含cache_key的缓存
255
+ keys_to_delete = [k for k in self.cache_time.keys() if symbol in k]
256
+ if keys_to_delete:
257
+ for k in keys_to_delete:
258
+ del self.cache_time[k]
259
+ # 由于lru_cache无法单独清除特定键,这里只能清除所有缓存
260
+ self._get_cache_historical_klines_df.cache_clear()
261
+
262
+
263
+ def get_historical_klines_df_by_cache(self, symbol, tf='15m'):
264
+ """_summary_
265
+ 获取历史K线数据
266
+ Args:
267
+ symbol (_type_): _description_
268
+ bar (_type_, optional): _description_. Defaults to '15m'.
269
+ Returns:
270
+ _type_: _description_
271
+ """
272
+ # cache_key = (symbol, tf)
273
+ cache_valid_second = self.interval_map.get(tf, 4 * 60 * 60) # 默认缓存时间为60分钟
274
+ cache_key = (symbol, tf)
275
+
276
+ # 检查缓存是否存在且未过期
277
+ current_time = datetime.now()
278
+ if cache_key in self.cache_time:
279
+ # 计算缓存时间与当前时间的差值(秒)
280
+ cache_age = (current_time - self.cache_time[cache_key]).total_seconds()
281
+ if cache_age <= cache_valid_second:
282
+ # 缓存有效,直接返回
283
+ # print(f"使用缓存数据: {symbol} {tf} (缓存时间: {cache_age:.2f} 分钟前)")
284
+ return self._get_cache_historical_klines_df(symbol, tf)
285
+ else:
286
+ # 缓存过期,清除缓存
287
+ self.logger.debug(f"{symbol} : 缓存已过期: {symbol} {tf} (缓存时间: {cache_age:.2f} 秒前)")
288
+ self._get_cache_historical_klines_df.cache_clear()
289
+
290
+ # 获取新数据并更新缓存时间
291
+ self.logger.debug(f"{symbol} : 重新获取新数据: {symbol} {tf}")
292
+ self.cache_time[cache_key] = current_time
293
+ return self._get_cache_historical_klines_df(symbol, tf)
294
+
295
+
296
+ def get_historical_klines_df(self, symbol, tf='15m'):
297
+ """_summary_
298
+ 获取历史K线数据
299
+ Args:
300
+ symbol (_type_): _description_
301
+ bar (_type_, optional): _description_. Defaults to '15m'.
302
+ Returns:
303
+ _type_: _description_
304
+ """
305
+ return self.exchange.get_historical_klines_df(symbol=symbol, bar=tf)
306
+ def format_klines(self, klines) -> pd.DataFrame:
307
+
308
+ """_summary_
309
+ 格式化K线数据
310
+ Args:
311
+ klines (_type_): _description_
312
+ Returns:
313
+ _type_: _description_
314
+ """
315
+
316
+ return self.exchange.format_klines(klines)
317
+
318
+ def find_PDArrays(self, symbol, struct, side=None, start_index=-1, pair_config=None) -> pd.DataFrame:
319
+ """_summary_
320
+ 寻找PDArray
321
+ Args:
322
+ symbol (_type_): _description_
323
+ data (_type_): _description_
324
+ side (_type_): _description_
325
+ start_index (_type_): _description_
326
+ is_valid (bool, optional): _description_. Defaults to True.
327
+ pair_config (_type_): _description_
328
+ Returns:
329
+ _type_: _description_
330
+ """
331
+ return self.smcPDArray.find_PDArrays(struct=struct, side=side, start_index=start_index)
332
+
333
+ def find_OBs(self, symbol, struct, side=None, start_index=-1, is_valid=True, pair_config=None) -> pd.DataFrame:
334
+ """_summary_
335
+ 识别OB
336
+ Args:
337
+ symbol (_type_): _description_
338
+ data (_type_): _description_
339
+ side (_type_): _description_
340
+ start_index (_type_): _description_
341
+ is_valid (bool, optional): _description_. Defaults to True.
342
+ pair_config (_type_): _description_
343
+ Returns:
344
+ _type_: _description_
345
+ """
346
+
347
+
348
+ return self.smcOB.find_OBs(struct=struct, side=side, start_index=start_index, is_valid=is_valid)
349
+
350
+ def get_latest_OB(self, symbol, data, trend, start_index=-1) -> dict:
351
+ """_summary_
352
+ 获取最新的Order Block
353
+ Args:
354
+ symbol (_type_): _description_
355
+ data (_type_): _description_
356
+ trend (_type_): _description_
357
+ start_index (_type_): _description_
358
+ Returns:
359
+ _type_: _description_
360
+ """
361
+
362
+ return self.smcOB.get_latest_OB(data=data, trend=trend, start_index=start_index)
363
+
364
+
365
+ def find_FVGs(self, symbol, data, side, check_balanced=True, start_index=-1, pair_config=None) -> pd.DataFrame:
366
+ """_summary_
367
+ 寻找公允价值缺口
368
+ Args:
369
+ symbol (_type_): _description_
370
+ data (_type_): _description_
371
+ side (_type_): _description_
372
+ check_balanced (bool, optional): _description_. Defaults to True.
373
+ start_index (_type_): _description_
374
+ pair_config (_type_): _description_
375
+ Returns:
376
+ _type_: _description_
377
+ """
378
+
379
+
380
+ return self.smcFVG.find_FVGs(data, side, check_balanced, start_index)
381
+
382
+ def find_EQH_EQL(self, symbol, data, trend, end_idx=-1, atr_offset=0.1, pair_config=None) -> dict:
383
+ """_summary_
384
+ 寻找等值高点和等值低点
385
+ Args:
386
+ symbol (_type_): _description_
387
+ data (_type_): _description_
388
+ trend (_type_): _description_
389
+ end_idx (int, optional): _description_. Defaults to -1.
390
+ atr_offset (float, optional): _description_. Defaults to 0.1.
391
+ Returns:
392
+ _type_: _description_
393
+ """
394
+ return self.smcLiqu.find_EQH_EQL(data, trend, end_idx=end_idx, atr_offset=atr_offset)
395
+
396
+ def identify_dynamic_trendlines(self, symbol, data, trend, start_idx=-1, end_idx=-1, ratio=0.8) -> bool:
397
+ """_summary_
398
+ 识别动态趋势线
399
+ Args:
400
+ symbol (_type_): _description_
401
+ data (_type_): _description_
402
+ trend (_type_): _description_
403
+ start_idx (int, optional): _description_. Defaults to -1.
404
+ end_idx (int, optional): _description_. Defaults to -1.
405
+ ratio (float, optional): _description_. Defaults to 0.5.
406
+ Returns:
407
+ _type_: _description_
408
+ """
409
+ return self.smcLiqu.identify_dynamic_trendlines(data, trend, start_idx, end_idx, ratio)
410
+
411
+ def build_struct(self, symbol, data) -> pd.DataFrame:
412
+
413
+ """_summary_
414
+ 构建SMC结构,参考 Tradingview OP@SMC Structures and FVG
415
+ Args:
416
+ symbol (_type_): _description_
417
+ data (_type_): _description_
418
+ Returns:
419
+ _type_: _description_
420
+ """
421
+
422
+
423
+ return self.smcStruct.build_struct(data)
424
+
425
+ def get_latest_struct(self, symbol, data) -> dict:
426
+ """_summary_
427
+ 获取最后一个SMC结构
428
+ Args:
429
+ symbol (_type_): _description_
430
+ data (_type_): _description_
431
+ Returns:
432
+ _type_: _description_
433
+ """
434
+ return self.smcStruct.get_latest_struct(data)
435
+
436
+ def reset_all_cache(self, symbol):
437
+ """_summary_
438
+ 重置所有缓存数据
439
+ """
440
+ if symbol in self.place_order_prices:
441
+ self.place_order_prices.pop(symbol)
442
+ self.clear_cache_historical_klines_df(symbol)
443
+
444
+ def fetch_position(self, symbol) -> bool:
445
+ """
446
+ 检查指定交易对是否有持仓,失败时最多重试3次
447
+
448
+ Args:
449
+ symbol: 交易对ID
450
+
451
+ Returns:
452
+ bool: 是否有持仓
453
+ """
454
+ try:
455
+ position = self.exchange.fetch_position(symbol=symbol)
456
+ return position['contracts'] > 0
457
+ except Exception as e:
458
+ error_message = f"{symbol} 检查持仓失败: {e}"
459
+ self.logger.error(error_message)
460
+ self.send_feishu_notification(symbol,error_message)
461
+ return True
462
+
463
+ @abstractmethod
464
+ def process_pair(self, symbol, pair_config):
465
+ """
466
+ 处理单个交易对的策略逻辑
467
+
468
+ Args:
469
+ symbol: 交易对名称
470
+ pair_config: 交易对配置信息
471
+
472
+ Raises:
473
+ NotImplementedError: 子类必须实现此方法
474
+ """
475
+ raise NotImplementedError("必须在子类中实现process_pair方法")
476
+
477
+ def monitor_klines(self):
478
+ symbols = list(self.trading_pairs_config.keys()) # 获取所有币对的ID
479
+ batch_size = 10 # 每批处理的数量
480
+ # while True:
481
+
482
+ for i in range(0, len(symbols), batch_size):
483
+ batch = symbols[i:i + batch_size]
484
+ with ThreadPoolExecutor(max_workers=batch_size) as executor:
485
+ futures = [executor.submit(self.process_pair, symbol,self.get_pair_config(symbol)) for symbol in batch]
486
+ for future in as_completed(futures):
487
+ future.result() # Raise any exceptions caught during execution
@@ -396,7 +396,6 @@ class ThreeLineStrategyMaker:
396
396
  prices: 价格列表。
397
397
  Returns:
398
398
  diff: 计算最高价列的最大值与最小值的差值
399
-
400
399
  """
401
400
  # limit = int(pair_config.get('ema_range_limit', 1))
402
401
  # period = int(pair_config.get('ema_range_period', 3))
maker/main.py CHANGED
@@ -1,98 +1,103 @@
1
1
  import logging
2
2
  import logging.config
3
3
  import yaml
4
+ import importlib
5
+ import importlib.metadata
4
6
  from apscheduler.triggers.interval import IntervalTrigger
5
7
  from apscheduler.schedulers.blocking import BlockingScheduler
6
8
  from datetime import datetime
7
9
  from pyfiglet import Figlet
10
+ from typing import Dict, Any
8
11
 
9
- from maker.ThreeLineStrategyMaker import ThreeLineStrategyMaker
10
- from maker.SMCStrategyMaker import SMCStrategyMaker
11
- from maker.BestFVGStrategyMaker import BestFVGStrategyMaker
12
-
13
-
14
- def read_config_file(file_path):
12
+ def read_config_file(file_path: str) -> Dict[str, Any]:
13
+ """读取并解析YAML配置文件"""
15
14
  try:
16
- # 打开 YAML 文件
17
15
  with open(file_path, 'r', encoding='utf-8') as file:
18
- # 使用 yaml.safe_load 方法解析 YAML 文件内容
19
- data = yaml.safe_load(file)
20
- return data
16
+ return yaml.safe_load(file)
21
17
  except FileNotFoundError:
22
- raise Exception(f"文件 {file_path} 未找到。")
18
+ raise FileNotFoundError(f"文件 {file_path} 未找到。")
23
19
  except yaml.YAMLError as e:
24
- raise Exception(f"解析 {file_path} 文件时出错: {e}")
25
-
26
- def run_bot(bot, logger):
27
- try:
20
+ raise yaml.YAMLError(f"解析 {file_path} 文件时出错: {e}")
28
21
 
22
+ def run_bot(bot: Any, logger: logging.Logger) -> None:
23
+ """执行机器人监控任务"""
24
+ try:
29
25
  bot.monitor_klines()
30
26
  except Exception as e:
31
27
  logger.error(f"执行任务时发生错误: {str(e)}", exc_info=True)
32
28
 
33
- def main():
29
+ def calculate_next_run_time(current_time: datetime, interval: int) -> datetime:
30
+ """计算下一次运行时间"""
31
+ next_run = current_time.replace(second=58, microsecond=0)
32
+ current_minute = next_run.minute
33
+ next_interval = ((current_minute // interval) + 1) * interval - 1
34
+
35
+ if next_interval >= 60:
36
+ next_interval %= 60
37
+ next_run = next_run.replace(hour=next_run.hour + 1)
38
+
39
+ return next_run.replace(minute=next_interval)
34
40
 
35
- import importlib.metadata
36
- package_name = __package__ or "openfund-maker"
41
+ def setup_scheduler(bot: Any, logger: logging.Logger, interval: int) -> None:
42
+ """设置并启动调度器"""
43
+ scheduler = BlockingScheduler()
44
+ next_run = calculate_next_run_time(datetime.now(), interval)
45
+
46
+ scheduler.add_job(
47
+ run_bot,
48
+ IntervalTrigger(minutes=interval),
49
+ args=[bot, logger],
50
+ next_run_time=next_run
51
+ )
52
+
53
+ try:
54
+ logger.info(f"启动定时任务调度器,从 {next_run} 开始每{interval}分钟执行一次...")
55
+ scheduler.start()
56
+ except (KeyboardInterrupt, SystemExit):
57
+ logger.info("程序收到中断信号,正在退出...")
58
+ scheduler.shutdown()
59
+ def create_strategy_instance(maker_name: str, configs: Dict[str, Any], logger: logging.Logger, exchangeKey:str):
60
+ """创建策略实例"""
61
+ module = importlib.import_module(f"maker.{maker_name}")
62
+ strategy_class = getattr(module, maker_name)
63
+ return strategy_class(
64
+ configs,
65
+ configs['platform'][exchangeKey],
66
+ configs['common'],
67
+ logger=logger,
68
+ exchangeKey=exchangeKey
69
+ )
70
+ def main():
71
+ # 获取包信息
37
72
  version = importlib.metadata.version("openfund-maker")
73
+ package_name = __package__ or "openfund-maker"
38
74
 
75
+ # 读取配置
39
76
  maker_config_path = 'maker_config.yaml'
40
77
  config_data = read_config_file(maker_config_path)
41
-
78
+
79
+ # 设置日志
42
80
  logging.config.dictConfig(config_data["Logger"])
43
81
  logger = logging.getLogger("openfund-maker")
44
82
 
45
- f = Figlet(font="standard") # 字体可选(如 "block", "bubble")
46
- logger.info(f"\n{f.renderText("OpenFund Maker")}")
47
-
48
- common_config = config_data['common']
49
- feishu_webhook_url = common_config['feishu_webhook']
50
- maker = common_config.get('actived_maker', 'MACDStrategyMaker')
51
- logger.info(f" ++ {package_name}.{maker}:{version} is doing...")
83
+ # 显示启动标题
84
+ f = Figlet(font="standard")
85
+ logger.info(f"\n{f.renderText('OpenFund Maker')}")
52
86
 
53
- platform_config = config_data['platform']['okx']
54
- # 根据配置动态创建策略实例
55
- strategy_class = globals()[maker]
56
- bot = strategy_class(config_data, platform_config, common_config, feishu_webhook=feishu_webhook_url, logger=logger)
57
-
58
-
59
- # 获取计划配置
87
+ # 获取配置信息
88
+ common_config = config_data['common']
89
+ maker_name = common_config.get('actived_maker', 'StrategyMaker')
90
+ logger.info(f" ++ {package_name}.{maker_name}:{version} is doing...")
91
+ exchangeKey = common_config.get("exchange_key", "okx")
92
+ # 创建并运行策略实例
93
+ bot = create_strategy_instance(maker_name, config_data, logger, exchangeKey)
94
+
95
+ # 处理调度
60
96
  schedule_config = common_config.get('schedule', {})
61
97
  if schedule_config.get('enabled', False):
62
- scheduler = BlockingScheduler()
63
-
64
- # 设置每5分钟执行一次的任务,从整点开始
65
- monitor_interval = int(schedule_config.get('monitor_interval', 4))
66
-
67
- # 计算下一个整点分钟
68
- now = datetime.now()
69
- # 将当前时间的秒和微秒设置为0
70
- next_run = now.replace(second=58, microsecond=0)
71
- # 计算下一个周期的开始时间
72
- current_minute = next_run.minute
73
- # 向上取整到下一个周期时间点, 然后再减去2Units,比如秒就是58秒执行。
74
- next_interval = ((current_minute // monitor_interval) + 1) * monitor_interval -1
75
- # 如果下一个周期时间点超过60分钟,需要调整为下一个小时的对应分钟数
76
- if next_interval >= 60:
77
- next_interval = next_interval % 60
78
- next_run = next_run.replace(hour=next_run.hour + 1)
79
- next_run = next_run.replace(minute=next_interval)
80
-
81
- scheduler.add_job(
82
- run_bot,
83
- IntervalTrigger(minutes=monitor_interval),
84
- args=[bot, logger],
85
- next_run_time=next_run # 从下一个周期整点开始
86
- )
87
-
88
- try:
89
- logger.info(f"启动定时任务调度器,从 {next_run} 开始每{monitor_interval}分钟执行一次...")
90
- scheduler.start()
91
- except (KeyboardInterrupt, SystemExit):
92
- logger.info("程序收到中断信号,正在退出...")
93
- scheduler.shutdown()
98
+ monitor_interval = int(schedule_config.get('monitor_interval', 4))
99
+ setup_scheduler(bot, logger, monitor_interval)
94
100
  else:
95
- # 如果未启用计划,直接运行
96
101
  run_bot(bot, logger)
97
102
 
98
103
  if __name__ == "__main__":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: openfund-maker
3
- Version: 2.2.9
3
+ Version: 2.3.2
4
4
  Summary: Openfund-maker.
5
5
  Requires-Python: >=3.9,<4.0
6
6
  Classifier: Programming Language :: Python :: 3
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.12
11
11
  Classifier: Programming Language :: Python :: 3.13
12
12
  Requires-Dist: apscheduler (>=3.11.0,<4.0.0)
13
13
  Requires-Dist: ccxt (>=4.4.26,<5.0.0)
14
+ Requires-Dist: openfund-core (>=1.0.0,<2.0.0)
14
15
  Requires-Dist: pandas (>=2.2.3,<3.0.0)
15
16
  Requires-Dist: pyfiglet (>=1.0.2,<2.0.0)
16
17
  Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
@@ -1,15 +1,17 @@
1
1
  maker/BestFVGStrategyMaker.py,sha256=a9UfClrfzkgX6jXL2FODzANtawrmGeZ_PVeO1-tweDc,12532
2
+ maker/BestTopDownStrategyMaker.py,sha256=-cyoxGn6ZKFJ4neFKnHHJ84ZvODMR-a_agitHz6VNXI,23735
2
3
  maker/MACDStrategyMaker.py,sha256=WX8wqpF9h5W4WclN1NjZ_Bur7KFi_aMTvacfLyHzEcI,12681
3
4
  maker/SMCStrategyMaker.py,sha256=hkDqymWnuyYDo1gTYY_uyO4H4yOwCw8NBTM9RcfLRPc,28780
4
- maker/ThreeLineStrategyMaker.py,sha256=jCdyTvI9oTMixCM0tQF9D_-6ig0-bYstKuRFECE4deE,32099
5
+ maker/StrategyMaker.py,sha256=iJa-9MxuUwPDOZot2YJmT-sdYHnPZbgPsdCaKvw3Sis,18821
6
+ maker/ThreeLineStrategyMaker.py,sha256=K4NZB1rH8IZMVrCFEnzwXctZQbyI9ZdyTMrYObpl-vM,32095
5
7
  maker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
8
  maker/history_code/WickReversalStrategyMaker.py,sha256=7DqPDVJot4EM0_lSAcFAHrR9rNvkIds9KLMoDOiAHEc,17486
7
9
  maker/history_code/config.py,sha256=YPxghO5i0vgRg9Cja8kGj9O7pgSbbtzOgf3RexqXXwY,1188
8
10
  maker/history_code/okxapi.py,sha256=_9G0U_o0ZC8NxaT6PqpiLgxBm9gPobC9PsFHZE1c5w0,553
9
11
  maker/history_code/zhen.py.bak,sha256=HNkrQbJts8G9umE9chEFsc0cLQApcM9KOVNMYPpkBXM,10918
10
12
  maker/history_code/zhen_2.py,sha256=4IaHVtTCMSlrLGSTZrWpW2q-f7HZsUNRkW_-5QgWv24,10509
11
- maker/main.py,sha256=kumgkgyYj1tVKrHTQmSx0prFs9K4i5KpvcOnqimV2eA,3805
12
- openfund_maker-2.2.9.dist-info/METADATA,sha256=e62T_JqAp7JrCPPm-sDYnOy0YjWWGy0wMULeQs-X2cI,1955
13
- openfund_maker-2.2.9.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
14
- openfund_maker-2.2.9.dist-info/entry_points.txt,sha256=gKMytICEKcMRFQDFkHZLnIpID7UQFoTIM_xcpiiV6Ns,50
15
- openfund_maker-2.2.9.dist-info/RECORD,,
13
+ maker/main.py,sha256=PRCP2qCUiUFPQyi1YbvnmW9KqeCZcc0zGjy9OBvMWbM,3723
14
+ openfund_maker-2.3.2.dist-info/METADATA,sha256=V93SlcoCwKl4tYklrw4SMY_t2LqVZyFB9ZsF2OhOSD4,2001
15
+ openfund_maker-2.3.2.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
16
+ openfund_maker-2.3.2.dist-info/entry_points.txt,sha256=gKMytICEKcMRFQDFkHZLnIpID7UQFoTIM_xcpiiV6Ns,50
17
+ openfund_maker-2.3.2.dist-info/RECORD,,