openfund-maker 1.3.17__py3-none-any.whl → 2.0.1__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,555 @@
1
+ # -*- coding: utf-8 -*-
2
+ import traceback
3
+ import pandas as pd
4
+ import talib as ta
5
+
6
+ from maker.ThreeLineStrategyMaker import ThreeLineStrategyMaker
7
+
8
+
9
+ class SMCStrategyMaker(ThreeLineStrategyMaker):
10
+ def __init__(self, config, platform_config, feishu_webhook=None,logger=None):
11
+ super().__init__(config, platform_config, feishu_webhook, logger)
12
+ self.place_order_prices = {} # 记录每个symbol的挂单价格
13
+
14
+ def place_order(self,symbol, price, side,pair_config):
15
+ """_summary_
16
+ 下单
17
+ Args:
18
+ symbol (_type_): _description_
19
+ price (_type_): _description_
20
+ amount_usdt (_type_): _description_
21
+ side (_type_): _description_
22
+ order_type (_type_): _description_
23
+ """
24
+ long_amount_usdt = pair_config.get('long_amount_usdt', 5)
25
+ short_amount_usdt = pair_config.get('short_amount_usdt', 5)
26
+ order_amount_usdt = 5
27
+ # order_type='optimal_limit_ioc'
28
+
29
+ if side == 'sell' :
30
+ self.logger.debug(f"{symbol} : 触发做空下单条件。")
31
+ order_amount_usdt = short_amount_usdt
32
+ elif side == 'buy' :
33
+ self.logger.debug(f"{symbol} : 触发做多下单条件。")
34
+ order_amount_usdt = long_amount_usdt
35
+ super().place_order(symbol=symbol, price=price, amount_usdt=order_amount_usdt, side=side)
36
+
37
+ def format_klines(self,klines) -> pd.DataFrame:
38
+
39
+ """_summary_
40
+ 格式化K线数据
41
+ Args:
42
+ klines (_type_): _description_
43
+ """
44
+ klines_df = pd.DataFrame(klines, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
45
+ # 转换时间戳为日期时间
46
+ klines_df['timestamp'] = pd.to_datetime(klines_df['timestamp'], unit='ms')
47
+ klines_df['timestamp'] = klines_df['timestamp'].dt.tz_localize('UTC').dt.tz_convert('Asia/Shanghai')
48
+
49
+ return klines_df
50
+
51
+ def find_OB_boxes(self, data, side, threshold, pivot_index, symbol=None, pair_config=None) -> list:
52
+ """_summary_
53
+ 识别OB
54
+ Args:
55
+ data (_type_): _description_
56
+ symbol (_type_): _description_
57
+
58
+ """
59
+ df = data.copy().iloc[pivot_index:]
60
+ # 首先计算实体的高点和低点,即开盘价和收盘价中的较大值和较小值
61
+ df['body_high'] = df[['open', 'close']].max(axis=1)
62
+ df['body_low'] = df[['open', 'close']].min(axis=1)
63
+
64
+ # 初始化OB的高点和低点列为空
65
+ df['OB_high'] = None
66
+ df['OB_low'] = None
67
+
68
+ # 使用布尔索引一次性更新OB_high
69
+ # df.loc[df['iUp'] == df.index, 'OB_high'] = df.loc[df['iUp'] == df.index, 'high']
70
+ # df.loc[df['iUp'] == df.index, 'OB_low'] = df['body_low'].shift(1).fillna(df['body_low'])
71
+
72
+ # df.loc[df['iDn'] == df.index, 'OB_low'] = df.loc[df['iDn'] == df.index, 'low']
73
+ # df.loc[df['iDn'] == df.index, 'OB_high'] = df['body_high'].shift(1).fillna(df['body_high'])
74
+
75
+ # print(df[['timestamp', 'pattern','high','low','Dn','iDn','Up','iUp','body_high','body_low']])
76
+
77
+ OB_boxes = []
78
+ # 根据交易方向构建OB盒子,OB区规则孤立高点+实体低点 孤立低点+实体高点
79
+ if side == 'buy':
80
+ # 买入方向的OB盒子构建
81
+ OB_boxes = [
82
+ {
83
+ 'index': idx,
84
+ 'top': float(df.loc[idx, 'low']), # OB低点为当前K线的最低点
85
+ 'bot': float(df.loc[idx - 1 if idx > df.index[0] else idx, 'body_high']) # OB高点为前一根K线的实体高点
86
+ }
87
+ for idx in df.index
88
+ # 判断条件:是第一根K线(极值点)或当前下降趋势大于前一个,且前一根K线实体高点小于阈值
89
+ if (idx == df.index[0] or (df.loc[idx, 'Dn'] > df.loc[idx - 1, 'Dn']))
90
+ and df.loc[idx - 1 if idx > df.index[0] else idx, 'body_high'] <= threshold
91
+ ]
92
+ else:
93
+ # 卖出方向的OB盒子构建
94
+ OB_boxes = [
95
+ {
96
+ 'index': idx,
97
+ 'top': float(df.loc[idx, 'high']), # OB高点为当前K线的最高点
98
+ 'bot': float(df.loc[idx - 1 if idx > df.index[0] else idx, 'body_low']) # OB低点为前一根K线的实体低点
99
+ }
100
+ for idx in df.index
101
+ # 判断条件:是第一根K线(极值点)或当前上升趋势小于前一个,且前一根K线实体低点大于阈值
102
+ if (idx == df.index[0] or (df.loc[idx, 'Up'] < df.loc[idx - 1, 'Up']))
103
+ and df.loc[idx - 1 if idx > df.index[0] else idx, 'body_low'] >= threshold
104
+ ]
105
+
106
+ return OB_boxes
107
+
108
+
109
+ def find_fvg_boxes(self, data, side, threshold, pivot_index=0, symbol=None, pair_config=None) -> list:
110
+ """_summary_
111
+ 寻找公允价值缺口
112
+ Args:
113
+ df (_type_): _description_
114
+ side (_type_): _description_
115
+ threshold (_type_): _description_
116
+ """
117
+
118
+ df = data.copy().iloc[pivot_index:]
119
+
120
+ fvg_boxes = []
121
+ if side == 'buy' :
122
+
123
+ # 处理看涨公允价值缺口
124
+ df.loc[:, 'is_bullish_fvg'] = df['high'].shift(3) < df['low'].shift(1)
125
+ bullish_df = df[df['is_bullish_fvg']].copy()
126
+ valid_indices = bullish_df.index[
127
+ (bullish_df.index - 1).isin(df.index) &
128
+ (bullish_df.index - 2).isin(df.index) &
129
+ (bullish_df.index - 3).isin(df.index)
130
+ ]
131
+
132
+ fvg_boxes = [
133
+ {
134
+ 'index': idx - 2, # FVG的索引
135
+ 'top': float(df.loc[idx - 1, 'low']), # FVG高点为右1K线的最低点
136
+ 'bot': float(df.loc[idx - 3, 'high']) # FVG低点为左1K线的最高点
137
+ }
138
+ # [df.loc[idx - 1, 'low'], df.loc[idx - 3, 'high'], idx - 2]
139
+ for idx in valid_indices
140
+ if df.loc[idx - 3, 'high'] <= threshold and
141
+ all((df.loc[idx:, 'low'] >= df.loc[idx - 3, 'high'])) # 检查FVG是否被平衡过
142
+ ]
143
+
144
+
145
+ else :
146
+ # 处理看跌公允价值缺口
147
+ df.loc[:, 'is_bearish_fvg'] = df['low'].shift(3) > df['high'].shift(1)
148
+
149
+ bearish_df = df[df['is_bearish_fvg']].copy()
150
+ valid_indices = bearish_df.index[
151
+ (bearish_df.index - 1).isin(df.index) &
152
+ (bearish_df.index - 2).isin(df.index) &
153
+ (bearish_df.index - 3).isin(df.index)
154
+ ]
155
+
156
+
157
+ fvg_boxes = [
158
+ {
159
+ 'index': idx - 2, # FVG的索引
160
+ 'top': float(df.loc[idx - 1, 'high']), # FVG高点为右1K线的最高点
161
+ 'bot': float(df.loc[idx - 3, 'low']) # FVG低点为左1K线的最低点
162
+ }
163
+
164
+ for idx in valid_indices
165
+ if df.loc[idx - 3, 'low'] >= threshold and
166
+ all((df.loc[idx:, 'high'] >= df.loc[idx - 3, 'low'])) # 检查FVG是否被平衡过
167
+ ]
168
+
169
+
170
+ return fvg_boxes
171
+
172
+
173
+ def detect_struct(self, data, prd=10, s1=True, resp=7) -> dict:
174
+ """_summary_
175
+ 识别智能资金结构
176
+
177
+ :param data: 包含 'high' 和 'low' 列的 DataFrame
178
+ :param prd: 结构周期
179
+ :param s1: 结构响应布尔值
180
+ :param resp: 响应周期
181
+ :return: 包含结构识别结果的 DataFrame
182
+ """
183
+
184
+
185
+ # data = data.copy()
186
+ data['Up'] = None
187
+ data['Dn'] = None
188
+ data['iUp'] = None
189
+ data['iDn'] = None
190
+ data['pos'] = 0
191
+ data['pattern'] = None
192
+
193
+ # 初始化 Up 和 Dn 的第一个值
194
+ data.at[0, 'Up'] = data.at[0, 'high']
195
+ data.at[0, 'Dn'] = data.at[0, 'low']
196
+
197
+
198
+ for index in range(1, len(data)):
199
+
200
+ data.at[index, 'Up'] = max(data.at[index - 1, 'Up'], data.at[index, 'high'])
201
+ data.at[index, 'Dn'] = min(data.at[index - 1, 'Dn'], data.at[index, 'low'])
202
+ data.at[index, 'pos'] = data.at[index - 1, 'pos']
203
+ data.at[index, 'iUp'] = data.at[max(0,index - 1), 'iUp'] if data.at[max(0,index - 1), 'iUp'] is not None else index
204
+ data.at[index, 'iDn'] = data.at[max(0,index - 1), 'iDn'] if data.at[max(0,index - 1), 'iDn'] is not None else index
205
+
206
+ # 寻找枢轴高点和低点
207
+ pvtHi = self.is_pivot_high(data, index, prd)
208
+ pvtLo = self.is_pivot_low(data, index, prd)
209
+
210
+ if pvtHi:
211
+ data.at[index, 'Up'] = data.at[index, 'high']
212
+ data.at[index, 'iUp'] = index
213
+ if pvtLo:
214
+ data.at[index, 'Dn'] = data.at[index, 'low']
215
+ data.at[index, 'iDn'] = index
216
+ # 寻找Bullish结构
217
+ if data.at[index, 'Up'] > data.at[index - 1, 'Up']:
218
+
219
+ if data.at[index - 1, 'pos'] <= 0:
220
+ # data.at[index, 'pattern'] = 'CHoCH (Bullish)'
221
+ data.at[index, 'pattern'] = 'Bullish_CHoCH'
222
+ data.at[index, 'pos'] = 1
223
+ elif data.at[index - 1, 'pos'] == 1 \
224
+ and data.at[index - 1, 'Up'] == data.at[max(0,index - (resp if s1 else prd)), 'Up']:
225
+ data.at[index, 'pattern'] = 'Bullish_SMS'
226
+ data.at[index, 'pos'] = 2
227
+
228
+ elif data.at[index - 1, 'pos'] > 1 \
229
+ and data.at[index - 1, 'Up'] == data.at[max(0,index - (resp if s1 else prd)), 'Up']:
230
+ data.at[index, 'pattern'] = 'Bullish_BMS'
231
+ data.at[index, 'pos'] = data.at[index - 1, 'pos'] + 1
232
+
233
+ elif data.at[index, 'Up'] < data.at[index - 1, 'Up']:
234
+ data.at[index, 'iUp'] = data.at[index - 1, 'iUp']
235
+
236
+ # # 寻找Bearish结构
237
+ if data.at[index, 'Dn'] < data.at[index - 1, 'Dn']:
238
+
239
+ if data.at[index - 1, 'pos'] >= 0:
240
+
241
+ data.at[index, 'pattern'] = 'Bearish_CHoCH'
242
+ data.at[index, 'pos'] = -1
243
+ elif data.at[index - 1, 'pos'] == -1 \
244
+ and data.at[index - 1, 'Dn'] == data.at[max(0,index - (resp if s1 else prd)), 'Dn']:
245
+ data.at[index, 'pattern'] = 'Bearish_SMS'
246
+ data.at[index, 'pos'] = -2
247
+ elif data.at[index - 1, 'pos'] < -1 \
248
+ and data.at[index - 1, 'Dn'] == data.at[max(0,index - (resp if s1 else prd)), 'Dn']:
249
+ data.at[index, 'pattern'] = 'Bearish_BMS'
250
+ data.at[index, 'pos'] = data.at[index - 1, 'pos'] - 1
251
+
252
+ elif data.at[index, 'Dn'] > data.at[index - 1, 'Dn']:
253
+ data.at[index, 'iDn'] = data.at[index - 1, 'iDn']
254
+
255
+ # 获取最后一个结构和位置
256
+ last_struct = {
257
+ "struct": None,
258
+ "index": -1,
259
+ "pivot_high": None,
260
+ "pivot_high_index": -1,
261
+ "pivot_low": None,
262
+ "pivot_low_index": -1,
263
+ "side": None
264
+
265
+ }
266
+
267
+
268
+ for i in range(len(data)-1, -1, -1):
269
+ if data.at[i, 'pattern'] is not None:
270
+ last_struct["struct"] = data.at[i, 'pattern']
271
+ last_struct["index"] = i
272
+
273
+ break
274
+
275
+ if last_struct['struct'] is not None :
276
+ # 找到最后一个结构的枢轴高点和低点,如果当前是孤立点,则取前一个孤立点
277
+ # 判断交易方向
278
+ if 'Bearish' in last_struct["struct"]:
279
+ last_struct["side"] = 'sell'
280
+ else :
281
+ last_struct["side"] = 'buy'
282
+
283
+ last_struct["pivot_high_index"] = int(data["iUp"].iloc[-1])
284
+ last_struct["pivot_low_index"] = int(data["iDn"].iloc[-1])
285
+
286
+ last_struct["pivot_high"] = float(data.loc[last_struct["pivot_high_index"], 'high'])
287
+ last_struct["pivot_low"] = float(data.loc[last_struct["pivot_low_index"], 'low'])
288
+
289
+
290
+
291
+ # last_struct["pivot_high_index"] = int(data.loc[data.index > last_struct["index"], 'high'].idxmax())
292
+ # last_struct["pivot_high"] = float(data.loc[data.index > last_struct["index"], 'high'].max())
293
+ # last_struct["pivot_low_index"] = int(data.loc[data.index > last_struct["index"], 'low'].idxmin())
294
+ # last_struct["pivot_low"] = float(data.loc[data.index > last_struct["index"], 'low'].min())
295
+
296
+ return last_struct
297
+
298
+
299
+
300
+
301
+ def is_pivot_high(self,data, index, period, check_bounds=False):
302
+ """
303
+ 判断当前索引处是否为枢轴高点
304
+ :param data: 包含 'high' 列的 DataFrame
305
+ :param index: 当前索引
306
+ :param period: 前后比较的周期数
307
+ :return: 是否为枢轴高点
308
+ """
309
+ if check_bounds and (index < period or index >= len(data) - period):
310
+ return False
311
+ current_high = data.at[index, 'high']
312
+ prev_highs = data['high'].iloc[max(0,index - period):index]
313
+ next_highs = data['high'].iloc[index :min(len(data),index + period + 1)]
314
+ return all(current_high >= prev_highs) and all(current_high >= next_highs)
315
+
316
+
317
+ def is_pivot_low(self,data, index, period, check_bounds=False):
318
+ """
319
+ 判断当前索引处是否为枢轴低点
320
+ :param data: 包含 'low' 列的 DataFrame
321
+ :param index: 当前索引
322
+ :param period: 前后比较的周期数
323
+ :return: 是否为枢轴低点
324
+ """
325
+ if check_bounds and (index < period or index >= len(data) - period):
326
+ return False
327
+ current_low = data.at[index, 'low']
328
+ prev_lows = data['low'].iloc[max(0,index - period):index]
329
+ next_lows = data['low'].iloc[index :min(len(data),index + period + 1)]
330
+ return all(current_low <= prev_lows) and all(current_low <= next_lows)
331
+
332
+ def round_price(self,symbol, price):
333
+ tick_size = self.get_tick_size(symbol)
334
+ return super().round_price_to_tick(price, tick_size)
335
+
336
+ def calculate_ce(self,symbol,pivot_high , pivot_low) -> float:
337
+ ce = (pivot_high + pivot_low) / 2
338
+ return float(self.round_price(symbol, ce))
339
+
340
+
341
+ def process_pair(self,symbol,pair_config):
342
+ self.logger.info("=" * 60)
343
+ """_summary_
344
+ 1. HTF 判断struct趋势(SMS和BMS)
345
+ 2. HTF 获取最新的两个极值点,设置折价区和溢价区
346
+ 3. CTF 在折价区获取FVG和OB的位置
347
+ 4. CTF 下单
348
+ 5.
349
+ """
350
+ try:
351
+ # 检查是否有持仓,有持仓不进行下单
352
+ if self.check_position(symbol=symbol) :
353
+ self.logger.info(f"{symbol} : 有持仓合约,不进行下单。")
354
+ if symbol in self.place_order_prices:
355
+ self.place_order_prices[symbol] = {}
356
+ return
357
+
358
+
359
+ smc_strategy = pair_config.get('smc_strategy',{})
360
+
361
+ # 获取历史K线,HTF和CTF
362
+ htf = str(smc_strategy.get('HTF','15m'))
363
+ htf_Klines = self.get_historical_klines(symbol=symbol, bar=htf)
364
+ htf_df = self.format_klines(htf_Klines)
365
+
366
+ ctf = str(pair_config.get('CHF', '5m'))
367
+ ctf_kLines = self.get_historical_klines(symbol=symbol, bar=ctf)
368
+ ctf_df = self.format_klines(ctf_kLines)
369
+
370
+ enable_FVG = smc_strategy.get('enable_FVG',True) # 是否启用FVG
371
+ enable_OB = smc_strategy.get('enable_OB',True) # 是否启用OB
372
+ self.logger.debug(f"{symbol} : SMC策略 {ctf}\{htf} enable_FVG={enable_FVG} enable_OB={enable_OB} ...")
373
+
374
+ side = 'none'
375
+ # 1. HTF 判断struct趋势(CHoCH\SMS\BMS) ,HTF struct 看趋势,CTF 看FVG和OB的位置
376
+ swing_points_length = smc_strategy.get('swing_points_length',10)
377
+ htf_last_struct = self.detect_struct(htf_df,prd=swing_points_length)
378
+ htf_last_struct_label = htf_last_struct["struct"]
379
+
380
+
381
+ if htf_last_struct_label is None:
382
+ self.logger.debug(f"{symbol} : {htf} 未形成 struct,不下单。{htf_last_struct}。")
383
+ return
384
+
385
+ # ctf_last_struct = self.detect_struct(ctf_df)
386
+ # ctf_last_struct_label = ctf_last_struct["struct"]
387
+
388
+ # if ctf_last_struct_label is None:
389
+ # self.logger.debug(f"{symbol} :{ctf} 未形成 struct,不下单。{ctf_last_struct}。")
390
+ # return
391
+
392
+ side = htf_last_struct["side"]
393
+ # self.logger.debug(f"{symbol} : {htf} 趋势={htf_last_struct_label}-{side}: \n{htf_last_struct}")
394
+
395
+
396
+ # 2. HTF 获取最新的两个极值点,设置折价(discount)区和溢价(premium)区
397
+ pivot_high = htf_last_struct["pivot_high"]
398
+ pivot_low = htf_last_struct["pivot_low"]
399
+ mid_line = self.calculate_ce(symbol,pivot_high,pivot_low)
400
+
401
+ # 计算溢价和折价区
402
+ premium_box = {
403
+ 'top': pivot_high,
404
+ 'bot': mid_line,
405
+ 'ce': self.calculate_ce(symbol,pivot_high,mid_line)
406
+ }
407
+ discount_box = {
408
+ 'top': mid_line,
409
+ 'bot': pivot_low,
410
+ 'ce': self.calculate_ce(symbol,mid_line,pivot_low)
411
+ }
412
+
413
+ self.logger.debug(f"{symbol} : {htf} 趋势={htf_last_struct_label}: \npivot_high={pivot_high} pivot_low={pivot_low} mid_line={mid_line}\n溢价区={premium_box}\n折价区={discount_box}")
414
+
415
+ # 3. 根据HTF结构来分析下单位置和止盈位置
416
+ threshold = 0.0
417
+ order_side = side
418
+ # 获取当前市场价格
419
+ market_price = ctf_df['close'].iloc[-1]
420
+
421
+ if 'CHoCH' in htf_last_struct_label:
422
+ """
423
+ ChoCh 结构。
424
+ Bearish趋势 如果价格,
425
+ 1.在溢价区上半区,可以考虑顺当前趋势,做空。
426
+ 2.在折价区下半区,则考虑回收流动性,做多。
427
+ 3.溢价区下半区和折价区上半区,不做单。
428
+
429
+ Bullish趋势 如果价格,
430
+ 1.在折价区下半区,可以考虑顺当前趋势,做多。
431
+ 2.在溢价区上半区,则考虑回收流动性的,做空。
432
+ 3.溢价区下半区和折价区上半区,不做单。
433
+
434
+ """
435
+ # 溢价区上半区做空
436
+ if market_price >= premium_box['ce'] and side == 'sell':
437
+ threshold = premium_box['ce']
438
+ # 折价区下半区做多
439
+ elif market_price <= discount_box['ce'] and side == 'buy':
440
+ threshold = discount_box['ce']
441
+ # 折价区下半区回收流动性做空 # TODO 要考虑是否有孤立点
442
+ # elif market_price <= discount_box['ce'] and side == 'sell':
443
+ # threshold = discount_box['ce']
444
+ # order_side = 'buy'
445
+ # # 溢价区上半区回收流动性做多
446
+ # elif market_price >= premium_box['ce'] and side == 'buy':
447
+ # threshold = premium_box['ce']
448
+ # order_side = 'sell'
449
+
450
+
451
+ elif 'SMS' in htf_last_struct_label or 'BMS' in htf_last_struct_label:
452
+ """
453
+ SMS/BMS 结构。
454
+ Bullish趋势 如果价格,
455
+ 1.在折价区可以下单,不区分上下半区
456
+
457
+ Bearish趋势 如果价格,
458
+ 1.在溢价区可以下单,不区分上下半区
459
+
460
+ """
461
+ # Bearish趋势 如果价格在溢价区可以下单
462
+ # if market_price >= mid_line and side == 'sell':
463
+ # threshold = mid_line
464
+ # # Bullish趋势 如果价格在折价区可以下单
465
+ # elif market_price <= mid_line and side == 'buy':
466
+ # threshold = mid_line
467
+ threshold = mid_line
468
+
469
+
470
+
471
+ if threshold == 0.0:
472
+ self.logger.debug(f"{symbol} : 价格{market_price}不在目标区域,不下单。")
473
+ # 取消所有未成交订单
474
+ self.cancel_all_orders(symbol=symbol)
475
+ return
476
+
477
+
478
+ # 4. 在CTF折价区获取FVG的位置
479
+ order_price = 0.0
480
+
481
+ if enable_FVG and order_price == 0.0:
482
+
483
+ all_tf = ['1m', '3m', '5m', '15m', '30m', '1H', '2H', '4H']
484
+ # 获取当前时间周期之前的所有时间周期
485
+ ctf_index = all_tf.index(ctf)
486
+ ltf_tfs = all_tf[:ctf_index + 1]
487
+
488
+ # 遍历所有LTF时间周期,获取FVG
489
+ for tf in ltf_tfs[::-1]:
490
+ tf_Klines = self.get_historical_klines(symbol=symbol, bar=tf)
491
+ tf_df = self.format_klines(tf_Klines)
492
+
493
+ fvg_boxes = self.find_fvg_boxes(tf_df,side=order_side,threshold=threshold)
494
+ if len(fvg_boxes) > 0:
495
+ self.logger.debug(f"{symbol} : 方向={order_side}, {tf} FVG={fvg_boxes}")
496
+ break
497
+ else:
498
+ self.logger.debug(f"{symbol} : 方向={order_side}, {tf} 未找到 FVG")
499
+
500
+
501
+
502
+ if len(fvg_boxes) != 0 and order_price == 0.0:
503
+ last_fvg_box = fvg_boxes[-1]
504
+ ce_price = self.calculate_ce(symbol,last_fvg_box['top'],last_fvg_box['bot'])
505
+ self.logger.info(f"{symbol} : 方向={order_side}, FVG_ce={ce_price} FVG={last_fvg_box} ")
506
+ order_price = ce_price
507
+
508
+ # 4. 找OB位置,OB规则孤立高点+实体低点 孤立低点+实体高点
509
+
510
+ if enable_OB and order_price == 0.0: # OB 优先级低于 FVG, order_price有价格时,不再计算OB
511
+
512
+ ctf_last_struct = self.detect_struct(ctf_df,prd=swing_points_length)
513
+ # 找到最近的一个极值点的位置
514
+ if order_side == 'buy':
515
+ pivot_index = ctf_last_struct["pivot_low_index"]
516
+ else:
517
+ pivot_index = ctf_last_struct["pivot_high_index"]
518
+ # TODO 不同级别的pivot_index 需要优化计算
519
+ OB_boxes = self.find_OB_boxes(ctf_df,side=side,threshold=threshold,pivot_index=pivot_index)
520
+
521
+ if len(OB_boxes) != 0 :
522
+ last_OB_box = OB_boxes[-1]
523
+ ce_price = self.calculate_ce(symbol,last_OB_box['top'],last_OB_box['bot'])
524
+ self.logger.info(f"{symbol} : 方向={order_side}, OB_ce={ce_price} , OB={last_OB_box} ")
525
+ order_price = ce_price
526
+
527
+ if order_price == 0.0:
528
+ self.logger.warning(f"!!!{symbol} : 未找到 FVG和OB")
529
+ self.cancel_all_orders(symbol=symbol)
530
+ return
531
+
532
+ latest_order_price = self.place_order_prices.get(symbol,0.0)
533
+ if order_price == latest_order_price:
534
+ self.logger.debug(f"{symbol} : 下单价格 {order_price} 未变化,不进行下单。")
535
+ return
536
+
537
+
538
+ # 下单
539
+ self.cancel_all_orders(symbol=symbol)
540
+ self.place_order(symbol=symbol, price=order_price, side=order_side, pair_config=pair_config)
541
+ self.place_order_prices[symbol] = order_price # 记录下单价格,过滤重复下单
542
+ self.logger.debug(f"{symbol} : {side}, 下单价格 {order_price}")
543
+
544
+
545
+ except KeyboardInterrupt:
546
+ self.logger.info("程序收到中断信号,开始退出...")
547
+ except Exception as e:
548
+ error_message = f"程序异常退出: {str(e)}"
549
+ self.logger.error(error_message,exc_info=True)
550
+ traceback.print_exc()
551
+ self.send_feishu_notification(error_message)
552
+ finally:
553
+ self.logger.info("-" * 60)
554
+
555
+
@@ -42,7 +42,9 @@ class ThreeLineStrategyMaker:
42
42
  return self.exchange.market(symbol)
43
43
 
44
44
  def get_tick_size(self,symbol):
45
- return float(self.getMarket(symbol)['precision']['price'])
45
+ market = self.getMarket(symbol)
46
+ marketPrecision = self.exchange.safe_dict(market, 'precision')
47
+ return self.exchange.safe_float(marketPrecision, 'price')
46
48
 
47
49
  def convert_contract(self, symbol, amount, price:float, direction='cost_to_contract'):
48
50
  """
@@ -123,7 +125,7 @@ class ThreeLineStrategyMaker:
123
125
  else:
124
126
  raise ValueError("Unexpected response structure or missing 'last' key")
125
127
 
126
- def round_price_to_tick(self,price, tick_size):
128
+ def round_price_to_tick(self, price, tick_size):
127
129
  # 计算 tick_size 的小数位数
128
130
  tick_decimals = len(f"{tick_size:.10f}".rstrip('0').split('.')[1]) if '.' in f"{tick_size:.10f}" else 0
129
131
 
@@ -424,14 +426,15 @@ class ThreeLineStrategyMaker:
424
426
  def place_order(self,symbol, price, amount_usdt, side,order_type='limit'):
425
427
 
426
428
 
427
- markets = self.exchange.load_markets()
428
- if symbol not in markets:
429
- self.logger.error(f"{symbol}: Instrument {symbol} not found in markets")
430
- return
431
- market = markets[symbol]
432
- # 获取价格精度
433
- price_precision = market['precision']['price']
434
- adjusted_price = self.round_price_to_tick(price, price_precision)
429
+ # markets = self.exchange.load_markets()
430
+ # if symbol not in markets:
431
+ # self.logger.error(f"{symbol}: Instrument {symbol} not found in markets")
432
+ # return
433
+ # market = markets[symbol]
434
+ # # 获取价格精度
435
+ # price_precision = market['precision']['price']
436
+ tick_size = self.get_tick_size(symbol)
437
+ adjusted_price = self.round_price_to_tick(price, tick_size)
435
438
 
436
439
  if amount_usdt > 0:
437
440
  if side == 'buy':
@@ -501,15 +504,15 @@ class ThreeLineStrategyMaker:
501
504
 
502
505
  self.logger.info(f"{symbol}: {order_ids} 挂单取消成功.")
503
506
  else:
504
- self.logger.info(f"{symbol}: 没有未完成订单.")
507
+ self.logger.info(f"{symbol}: 无挂单.")
505
508
  return True
506
509
 
507
510
  except Exception as e:
508
511
  retry_count += 1
509
512
  if retry_count == max_retries:
510
- self.logger.warning(f"{symbol} 取消订单失败(重试{retry_count}次): {str(e)}")
513
+ self.logger.warning(f"{symbol} 取消挂单失败(重试{retry_count}次): {str(e)}")
511
514
  return False
512
- self.logger.warning(f"{symbol} 取消订单失败,正在进行第{retry_count}次重试: {str(e)}")
515
+ self.logger.warning(f"{symbol} 取消挂单失败,正在进行第{retry_count}次重试: {str(e)}")
513
516
  time.sleep(0.1) # 重试前等待0.1秒
514
517
 
515
518
  def process_pair(self,symbol,pair_config):
@@ -666,13 +669,13 @@ class ThreeLineStrategyMaker:
666
669
  def monitor_klines(self):
667
670
  symbols = list(self.trading_pairs_config.keys()) # 获取所有币对的ID
668
671
  batch_size = 5 # 每批处理的数量
669
- while True:
670
-
671
- for i in range(0, len(symbols), batch_size):
672
- batch = symbols[i:i + batch_size]
673
- with ThreadPoolExecutor(max_workers=batch_size) as executor:
674
- futures = [executor.submit(self.process_pair, symbol,self.trading_pairs_config[symbol]) for symbol in batch]
675
- for future in as_completed(futures):
676
- future.result() # Raise any exceptions caught during execution
672
+ # while True:
673
+
674
+ for i in range(0, len(symbols), batch_size):
675
+ batch = symbols[i:i + batch_size]
676
+ with ThreadPoolExecutor(max_workers=batch_size) as executor:
677
+ futures = [executor.submit(self.process_pair, symbol,self.trading_pairs_config[symbol]) for symbol in batch]
678
+ for future in as_completed(futures):
679
+ future.result() # Raise any exceptions caught during execution
677
680
 
678
681
  # time.sleep(self.monitor_interval)
maker/main.py CHANGED
@@ -8,6 +8,7 @@ from datetime import datetime
8
8
  from maker.WickReversalStrategyMaker import WickReversalStrategyMaker
9
9
  from maker.ThreeLineStrategyMaker import ThreeLineStrategyMaker
10
10
  from maker.MACDStrategyMaker import MACDStrategyMaker
11
+ from maker.SMCStrategyMaker import SMCStrategyMaker
11
12
 
12
13
  def build_logger(log_config) -> logging.Logger:
13
14
  # 配置日志
@@ -60,7 +61,7 @@ def main():
60
61
  package_name = __package__ or "maker"
61
62
  logger.info(f" ++ {package_name}:{version} is doing...")
62
63
  logger.info("开始执行交易任务...")
63
- bot = MACDStrategyMaker(config_data, platform_config, feishu_webhook=feishu_webhook_url, logger=logger)
64
+ bot = SMCStrategyMaker(config_data, platform_config, feishu_webhook=feishu_webhook_url, logger=logger)
64
65
 
65
66
  # 获取计划配置
66
67
  schedule_config = config_data.get('schedule', {})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: openfund-maker
3
- Version: 1.3.17
3
+ Version: 2.0.1
4
4
  Summary: Openfund-maker.
5
5
  Requires-Python: >=3.9,<4.0
6
6
  Classifier: Programming Language :: Python :: 3
@@ -1,14 +1,15 @@
1
1
  maker/MACDStrategyMaker.py,sha256=iS5HO04piKHFJxUI2e5QmicxzGeK-V1aphJSr2n_4Ac,12651
2
- maker/ThreeLineStrategyMaker.py,sha256=5uUTRMOCe0-0d2Ll7Bzhe-0WKpnfddhDEaj3XTGDgyA,30270
2
+ maker/SMCStrategyMaker.py,sha256=PWy3eQsX8XFb3gJoNamZFKZf1O09zEQClm7R-YjY06I,25099
3
+ maker/ThreeLineStrategyMaker.py,sha256=ArjnHlGECiD3cCFXxO0Ex5scR2agwoxZY-4mKukyKc4,30402
3
4
  maker/WickReversalStrategyMaker.py,sha256=7DqPDVJot4EM0_lSAcFAHrR9rNvkIds9KLMoDOiAHEc,17486
4
5
  maker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
6
  maker/config.py,sha256=YPxghO5i0vgRg9Cja8kGj9O7pgSbbtzOgf3RexqXXwY,1188
6
- maker/main.py,sha256=3VMvFJW_ugmB3QpUrzjBfkma9erDsVw3Tox6RdGjhRk,4150
7
+ maker/main.py,sha256=WhrfrZecbV9ihDL14J1-oTuLJq8Zt1mg9lN4pvnlaN0,4201
7
8
  maker/main_m.py,sha256=0PzDTnuBrxfpy5WDfsIHKAzZ_7pkuvuqqeWik0vpWio,15522
8
9
  maker/okxapi.py,sha256=_9G0U_o0ZC8NxaT6PqpiLgxBm9gPobC9PsFHZE1c5w0,553
9
10
  maker/zhen.py.bak,sha256=HNkrQbJts8G9umE9chEFsc0cLQApcM9KOVNMYPpkBXM,10918
10
11
  maker/zhen_2.py,sha256=4IaHVtTCMSlrLGSTZrWpW2q-f7HZsUNRkW_-5QgWv24,10509
11
- openfund_maker-1.3.17.dist-info/METADATA,sha256=3iQhkAgwJ2BWzaTVCNmCNT1cmKTM19jc6m2auOZuDGg,1954
12
- openfund_maker-1.3.17.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
13
- openfund_maker-1.3.17.dist-info/entry_points.txt,sha256=gKMytICEKcMRFQDFkHZLnIpID7UQFoTIM_xcpiiV6Ns,50
14
- openfund_maker-1.3.17.dist-info/RECORD,,
12
+ openfund_maker-2.0.1.dist-info/METADATA,sha256=0rnk8ay_ii8XOu-UbbqF_U3raNfusEnzjWYdvqCF-4Y,1953
13
+ openfund_maker-2.0.1.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
14
+ openfund_maker-2.0.1.dist-info/entry_points.txt,sha256=gKMytICEKcMRFQDFkHZLnIpID7UQFoTIM_xcpiiV6Ns,50
15
+ openfund_maker-2.0.1.dist-info/RECORD,,