openfund-maker 2.2.6__py3-none-any.whl → 2.2.8__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.
@@ -1,14 +1,14 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import traceback
3
3
  from typing import override
4
- import pandas as pd
5
4
 
6
- from maker.SMCStrategyMaker import SMCStrategyMaker
7
5
 
6
+ from maker.SMCStrategyMaker import SMCStrategyMaker
8
7
 
9
8
  class BestFVGStrategyMaker(SMCStrategyMaker):
10
- def __init__(self, config, platform_config, feishu_webhook=None,logger=None):
11
- super().__init__(config, platform_config, feishu_webhook, logger)
9
+ def __init__(self, config, platform_config, common_config, feishu_webhook=None,logger=None):
10
+ super().__init__(config, platform_config, common_config, feishu_webhook, logger)
11
+
12
12
 
13
13
  self.htf_last_CHoCH = {} #记录HTF的CHoCH struct
14
14
 
@@ -97,7 +97,7 @@ class BestFVGStrategyMaker(SMCStrategyMaker):
97
97
 
98
98
  htf_Klines = self.get_historical_klines(symbol=symbol, bar=htf)
99
99
  htf_df = self.format_klines(htf_Klines)
100
-
100
+ precision = self.get_precision_length(symbol)
101
101
 
102
102
  # 初始化HTF趋势相关变量
103
103
  htf_side, valid_htf_struct = None, None
@@ -153,7 +153,7 @@ class BestFVGStrategyMaker(SMCStrategyMaker):
153
153
  }
154
154
 
155
155
  self.logger.info(f"{symbol} : {htf} 趋势={htf_struct_label} 匹配 {htf_entry_struct} struct")
156
- self.logger.debug(f"{symbol} : {htf}\npivot_high={htf_pivot_high} pivot_low={htf_pivot_low} mid_line={htf_mid_line}\n溢价区={premium_box}\n折价区={discount_box}")
156
+ self.logger.debug(f"{symbol} : {htf}\npivot_high={htf_pivot_high:.{precision}} pivot_low={htf_pivot_low:.{precision}} mid_line={htf_mid_line:.{precision}}\n溢价区={premium_box}\n折价区={discount_box}")
157
157
 
158
158
  # 3. find HTF FVG
159
159
 
@@ -171,7 +171,7 @@ class BestFVGStrategyMaker(SMCStrategyMaker):
171
171
  self.logger.debug(f"{symbol} : 价格[未进入] HTF_FVG区域,不进行下单")
172
172
  return
173
173
  else:
174
- self.logger.debug(f"{symbol} : 价格[进入] HTF_FVG区域,开始下单。fvgbox={htf_fvg_boxes[-1]}")
174
+ self.logger.debug(f"{symbol} : 价格[进入] HTF_FVG区域,开始下单。\n fvgbox={htf_fvg_boxes[-1]}")
175
175
 
176
176
  # 4. LTF 判断struct趋势是否有CHoCH
177
177
 
@@ -199,7 +199,7 @@ class BestFVGStrategyMaker(SMCStrategyMaker):
199
199
  }
200
200
 
201
201
  self.logger.info(f"{symbol} : {ltf} 趋势={ltf_struct_label} struct={ltf_struct}")
202
- self.logger.debug(f"{symbol} : {ltf} \npivot_high={ltf_last_pivot_high} pivot_low={ltf_last_pivot_low} mid_line={ltf_last_mid_line}\n溢价区={ltf_premium_box}\n折价区={ltf_discount_box}")
202
+ self.logger.debug(f"{symbol} : {ltf} \npivot_high={ltf_last_pivot_high} pivot_low={ltf_last_pivot_low:.{precision}} mid_line={ltf_last_mid_line:.{precision}}\n溢价区={ltf_premium_box}\n折价区={ltf_discount_box}")
203
203
 
204
204
 
205
205
  # 5. LTF 寻找FVG,下单
@@ -227,11 +227,11 @@ class BestFVGStrategyMaker(SMCStrategyMaker):
227
227
 
228
228
 
229
229
  # 4. LTF 寻找FVG,下单
230
- order_price = ltf_fvg_boxes[-1]["top"] if ltf_struct_side == "buy" else ltf_fvg_boxes[-1]["bot"]
230
+ order_price = self.toDecimal(ltf_fvg_boxes[-1]["top"] if ltf_struct_side == "buy" else ltf_fvg_boxes[-1]["bot"] )
231
231
 
232
- latest_order_price = self.place_order_prices.get(symbol,0.0)
232
+ latest_order_price = self.toDecimal(self.place_order_prices.get(symbol,0))
233
233
  if order_price == latest_order_price:
234
- self.logger.debug(f"{symbol} : 下单价格 {order_price} 未变化,不进行下单。")
234
+ self.logger.debug(f"{symbol} : 下单价格 {order_price:.{precision}} 未变化,不进行下单。")
235
235
  return
236
236
 
237
237
 
@@ -239,7 +239,7 @@ class BestFVGStrategyMaker(SMCStrategyMaker):
239
239
  self.cancel_all_orders(symbol=symbol)
240
240
  self.place_order(symbol=symbol, price=order_price, side=ltf_struct_side, pair_config=pair_config)
241
241
  self.place_order_prices[symbol] = order_price # 记录下单价格,过滤重复下单
242
- self.logger.debug(f"{symbol} : {ltf_struct_side}, 下单价格 {order_price}")
242
+ self.logger.debug(f"{symbol} : {ltf_struct_side}, 下单价格 {order_price:.{precision}}")
243
243
 
244
244
 
245
245
  except KeyboardInterrupt:
@@ -8,8 +8,8 @@ from maker.ThreeLineStrategyMaker import ThreeLineStrategyMaker
8
8
 
9
9
 
10
10
  class MACDStrategyMaker(ThreeLineStrategyMaker):
11
- def __init__(self, config, platform_config, feishu_webhook=None,logger=None):
12
- super().__init__(config, platform_config, feishu_webhook, logger)
11
+ def __init__(self, config, platform_config, common_config, feishu_webhook=None,logger=None):
12
+ super().__init__(config, platform_config, common_config, feishu_webhook, logger)
13
13
 
14
14
  def get_macd_cross_direction(self, symbol, kLines, strategy=None) -> dict:
15
15
  # 计算最近三个交叉点
maker/SMCStrategyMaker.py CHANGED
@@ -1,16 +1,18 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import traceback
3
3
  import pandas as pd
4
+ from decimal import Decimal
4
5
 
5
- from maker.ThreeLineStrategyMaker import ThreeLineStrategyMaker
6
6
 
7
+ from maker.ThreeLineStrategyMaker import ThreeLineStrategyMaker
7
8
 
8
9
  class SMCStrategyMaker(ThreeLineStrategyMaker):
9
- def __init__(self, config, platform_config, feishu_webhook=None,logger=None):
10
- super().__init__(config, platform_config, feishu_webhook, logger)
10
+ def __init__(self, config, platform_config, common_config, feishu_webhook=None,logger=None):
11
+ super().__init__(config, platform_config, common_config, feishu_webhook, logger)
12
+
11
13
  self.place_order_prices = {} # 记录每个symbol的挂单价格
12
14
 
13
- def place_order(self,symbol, price, side,pair_config):
15
+ def place_order(self,symbol, price:Decimal, side,pair_config):
14
16
  """_summary_
15
17
  下单
16
18
  Args:
@@ -81,8 +83,8 @@ class SMCStrategyMaker(ThreeLineStrategyMaker):
81
83
  OB_boxes = [
82
84
  {
83
85
  '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
+ 'top': self.toDecimal(df.loc[idx, 'low']), # OB低点为当前K线的最低点
87
+ 'bot': self.toDecimal(df.loc[idx - 1 if idx > df.index[0] else idx, 'body_high']) # OB高点为前一根K线的实体高点
86
88
  }
87
89
  for idx in df.index
88
90
  # 判断条件:是第一根K线(极值点)或当前下降趋势大于前一个,且前一根K线实体高点小于阈值
@@ -94,8 +96,8 @@ class SMCStrategyMaker(ThreeLineStrategyMaker):
94
96
  OB_boxes = [
95
97
  {
96
98
  '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
+ 'top': self.toDecimal(df.loc[idx, 'high']), # OB高点为当前K线的最高点
100
+ 'bot': self.toDecimal(df.loc[idx - 1 if idx > df.index[0] else idx, 'body_low']) # OB低点为前一根K线的实体低点
99
101
  }
100
102
  for idx in df.index
101
103
  # 判断条件:是第一根K线(极值点)或当前上升趋势小于前一个,且前一根K线实体低点大于阈值
@@ -106,7 +108,7 @@ class SMCStrategyMaker(ThreeLineStrategyMaker):
106
108
  return OB_boxes
107
109
 
108
110
 
109
- def find_fvg_boxes(self, data, side, threshold, check_balanced=True, pivot_index=0, symbol=None, pair_config=None) -> list:
111
+ def find_fvg_boxes(self, data, side, threshold:Decimal, check_balanced=True, pivot_index=0, symbol=None, pair_config=None) -> list:
110
112
  """_summary_
111
113
  寻找公允价值缺口
112
114
  Args:
@@ -142,8 +144,8 @@ class SMCStrategyMaker(ThreeLineStrategyMaker):
142
144
  fvg_boxes = [
143
145
  {
144
146
  'index': idx - 2, # FVG的索引
145
- 'top': min(float(df.loc[idx - 1, 'low']),threshold), # FVG高点为右1K线的最低点
146
- 'bot': float(df.loc[idx - 3, 'high']) # FVG低点为左1K线的最高点
147
+ 'top': min(self.toDecimal(df.loc[idx - 1, 'low']),threshold), # FVG高点为右1K线的最低点
148
+ 'bot': self.toDecimal(df.loc[idx - 3, 'high']) # FVG低点为左1K线的最高点
147
149
  }
148
150
  # [df.loc[idx - 1, 'low'], df.loc[idx - 3, 'high'], idx - 2]
149
151
  for idx in valid_indices
@@ -167,8 +169,8 @@ class SMCStrategyMaker(ThreeLineStrategyMaker):
167
169
  fvg_boxes = [
168
170
  {
169
171
  'index': idx - 2, # FVG的索引
170
- 'top': float(df.loc[idx - 3, 'low']), # FVG高点为右1K线的最高点
171
- 'bot': max(float(df.loc[idx - 1, 'high']),threshold) # FVG低点为左1K线的最低点
172
+ 'top': self.toDecimal(df.loc[idx - 3, 'low']), # FVG高点为右1K线的最高点
173
+ 'bot': max(self.toDecimal(df.loc[idx - 1, 'high']),threshold) # FVG低点为左1K线的最低点
172
174
  }
173
175
 
174
176
  for idx in valid_indices
@@ -179,153 +181,6 @@ class SMCStrategyMaker(ThreeLineStrategyMaker):
179
181
 
180
182
  return fvg_boxes
181
183
 
182
-
183
- # def detect_struct(self, data, prd=10, struct_key=None, check_valid_range=True, check_bounds=True, global_extremum=False, s1=True, resp=7) -> dict:
184
- # """_summary_
185
- # 识别SMC结构,参考 Tradingview Smart Money Concepts Probability (Expo)@Openfund
186
-
187
- # Args:
188
- # data (df): df格式的K线数据
189
- # prd (int): 计算Swing Points的bar数量
190
- # struct_key (str): 结构类型,如 'CHoCH'|'SMS'|'BMS'
191
- # check_valid_range (bool): 结构类型在 pivot_high_index 和 pivot_low_index 之间为有效范围内,默认为False
192
- # check_bounds (bool): 计算Swing Points是否检查边界,默认为True
193
- # global_extremum (bool): 是否使用全局极值点,默认为False
194
- # s1 (bool): 结构响应布尔值
195
- # resp (int): 响应周期
196
- # Returns:
197
- # dict: 包含结构识别结果的字典,包含以下字段:
198
- # "struct": 结构类型,如 'Bullish_CHoCH'|'Bullish_SMS'|'Bullish_BMS'|'Bearish_CHoCH'|'Bearish_SMS'|'Bearish_BMS'
199
- # "index": 结构出现的位置索引
200
- # "pivot_high": 枢轴高点价格
201
- # "pivot_high_index": 枢轴高点索引
202
- # "pivot_low": 枢轴低点价格
203
- # "pivot_low_index": 枢轴低点索引
204
- # "side": 交易方向,'buy'或'sell'
205
-
206
-
207
- # """
208
-
209
- # # data = data.copy()
210
- # data['Up'] = None
211
- # data['Dn'] = None
212
- # data['iUp'] = None
213
- # data['iDn'] = None
214
- # data['pos'] = 0
215
- # data['pattern'] = None
216
-
217
- # # 初始化 Up 和 Dn 的第一个值
218
- # data.at[0, 'Up'] = data.at[0, 'high']
219
- # data.at[0, 'Dn'] = data.at[0, 'low']
220
-
221
-
222
- # for index in range(1, len(data)):
223
-
224
- # data.at[index, 'Up'] = max(data.at[index - 1, 'Up'], data.at[index, 'high'])
225
- # data.at[index, 'Dn'] = min(data.at[index - 1, 'Dn'], data.at[index, 'low'])
226
- # data.at[index, 'pos'] = data.at[index - 1, 'pos']
227
- # data.at[index, 'iUp'] = data.at[max(0,index - 1), 'iUp'] if data.at[max(0,index - 1), 'iUp'] is not None else index
228
- # data.at[index, 'iDn'] = data.at[max(0,index - 1), 'iDn'] if data.at[max(0,index - 1), 'iDn'] is not None else index
229
-
230
- # # 寻找枢轴高点和低点
231
- # pvtHi = self.is_pivot_high(data, index, prd, check_bounds)
232
- # pvtLo = self.is_pivot_low(data, index, prd, check_bounds)
233
-
234
- # if pvtHi:
235
- # data.at[index, 'Up'] = data.at[index, 'high']
236
- # data.at[index, 'iUp'] = index
237
- # if pvtLo:
238
- # data.at[index, 'Dn'] = data.at[index, 'low']
239
- # data.at[index, 'iDn'] = index
240
- # # 寻找Bullish结构
241
- # if data.at[index, 'Up'] > data.at[index - 1, 'Up']:
242
-
243
- # data.at[index, 'iUp'] = index
244
- # if data.at[index - 1, 'pos'] <= 0:
245
- # # data.at[index, 'pattern'] = 'CHoCH (Bullish)'
246
- # data.at[index, 'pattern'] = 'Bullish_CHoCH'
247
- # data.at[index, 'pos'] = 1
248
- # elif data.at[index - 1, 'pos'] == 1 \
249
- # and data.at[index - 1, 'Up'] == data.at[max(0,index - (resp if s1 else prd)), 'Up']:
250
- # data.at[index, 'pattern'] = 'Bullish_SMS'
251
- # data.at[index, 'pos'] = 2
252
-
253
- # elif data.at[index - 1, 'pos'] > 1 \
254
- # and data.at[index - 1, 'Up'] == data.at[max(0,index - (resp if s1 else prd)), 'Up']:
255
- # data.at[index, 'pattern'] = 'Bullish_BMS'
256
- # data.at[index, 'pos'] = data.at[index - 1, 'pos'] + 1
257
-
258
- # elif global_extremum and data.at[index, 'Up'] < data.at[index - 1, 'Up']:
259
- # data.at[index, 'iUp'] = data.at[index - 1, 'iUp']
260
-
261
- # # # 寻找Bearish结构
262
- # if data.at[index, 'Dn'] < data.at[index - 1, 'Dn']:
263
- # data.at[index, 'iDn'] = index
264
- # if data.at[index - 1, 'pos'] >= 0:
265
-
266
- # data.at[index, 'pattern'] = 'Bearish_CHoCH'
267
- # data.at[index, 'pos'] = -1
268
- # elif data.at[index - 1, 'pos'] == -1 \
269
- # and data.at[index - 1, 'Dn'] == data.at[max(0,index - (resp if s1 else prd)), 'Dn']:
270
- # data.at[index, 'pattern'] = 'Bearish_SMS'
271
- # data.at[index, 'pos'] = -2
272
- # elif data.at[index - 1, 'pos'] < -1 \
273
- # and data.at[index - 1, 'Dn'] == data.at[max(0,index - (resp if s1 else prd)), 'Dn']:
274
- # data.at[index, 'pattern'] = 'Bearish_BMS'
275
- # data.at[index, 'pos'] = data.at[index - 1, 'pos'] - 1
276
-
277
- # elif global_extremum and data.at[index, 'Dn'] > data.at[index - 1, 'Dn']:
278
- # data.at[index, 'iDn'] = data.at[index - 1, 'iDn']
279
-
280
- # # 获取最后一个结构和位置
281
- # last_struct = {
282
- # "struct": None,
283
- # "index": -1,
284
- # "pivot_high": None,
285
- # "pivot_high_index": -1,
286
- # "pivot_low": None,
287
- # "pivot_low_index": -1,
288
- # "side": None
289
-
290
- # }
291
-
292
-
293
- # for i in range(len(data)-1, -1, -1):
294
- # if check_valid_range:
295
- # # 检查是否在pivot_high_index和pivot_low_index之间的有效范围内
296
- # if data.at[i, 'iUp'] != -1 and data.at[i, 'iDn'] != -1:
297
- # pivot_high_index = data.at[i, 'iUp']
298
- # pivot_low_index = data.at[i, 'iDn']
299
- # if i < min(pivot_high_index, pivot_low_index) or i > max(pivot_high_index, pivot_low_index):
300
- # continue
301
-
302
- # if data.at[i, 'pattern'] is not None:
303
- # if struct_key is not None and struct_key not in data.at[i, 'pattern']:
304
- # continue
305
- # last_struct["struct"] = data.at[i, 'pattern']
306
- # last_struct["index"] = i
307
-
308
- # break
309
-
310
- # if last_struct['struct'] is not None :
311
- # # 找到最后一个结构的枢轴高点和低点,如果当前是孤立点,则取前一个孤立点
312
- # # 判断交易方向
313
- # if 'Bearish' in last_struct["struct"]:
314
- # last_struct["side"] = 'sell'
315
- # else :
316
- # last_struct["side"] = 'buy'
317
-
318
- # last_struct["pivot_high_index"] = int(data["iUp"].iloc[-1])
319
- # last_struct["pivot_low_index"] = int(data["iDn"].iloc[-1])
320
-
321
- # last_struct["pivot_high"] = float(data.loc[last_struct["pivot_high_index"], 'high'])
322
- # last_struct["pivot_low"] = float(data.loc[last_struct["pivot_low_index"], 'low'])
323
- # else:
324
- # last_struct['struct'] = "None"
325
- # last_struct["index"] = -1
326
-
327
-
328
- # return last_struct
329
184
  def build_struct(self, df, prd=20, check_bounds=True, global_extremum=False) :
330
185
 
331
186
  """_summary_
@@ -345,9 +200,16 @@ class SMCStrategyMaker(ThreeLineStrategyMaker):
345
200
 
346
201
 
347
202
  for index in range(1, len(data)):
203
+ prev_up = self.toDecimal(data.at[index - 1, 'Up'])
204
+ curr_high = self.toDecimal(data.at[index, 'high'])
205
+ prev_dn = self.toDecimal(data.at[index - 1, 'Dn'])
206
+ curr_low = self.toDecimal(data.at[index, 'low'])
207
+
208
+ data.at[index, 'Up'] = max(prev_up, curr_high)
209
+ data.at[index, 'Dn'] = min(prev_dn, curr_low)
348
210
 
349
- data.at[index, 'Up'] = max(data.at[index - 1, 'Up'], data.at[index, 'high'])
350
- data.at[index, 'Dn'] = min(data.at[index - 1, 'Dn'], data.at[index, 'low'])
211
+ # data.at[index, 'Up'] = max(data.at[index - 1, 'Up'], data.at[index, 'high'])
212
+ # data.at[index, 'Dn'] = min(data.at[index - 1, 'Dn'], data.at[index, 'low'])
351
213
  data.at[index, 'pos'] = data.at[index - 1, 'pos']
352
214
  data.at[index, 'iUp'] = data.at[max(0,index - 1), 'iUp'] if data.at[max(0,index - 1), 'iUp'] is not None else index
353
215
  data.at[index, 'iDn'] = data.at[max(0,index - 1), 'iDn'] if data.at[max(0,index - 1), 'iDn'] is not None else index
@@ -357,10 +219,10 @@ class SMCStrategyMaker(ThreeLineStrategyMaker):
357
219
  pvtLo = self.is_pivot_low(data, index, prd, check_bounds)
358
220
 
359
221
  if pvtHi:
360
- data.at[index, 'Up'] = data.at[index, 'high']
222
+ data.at[index, 'Up'] = self.toDecimal(data.at[index, 'high'])
361
223
  data.at[index, 'iUp'] = index
362
224
  if pvtLo:
363
- data.at[index, 'Dn'] = data.at[index, 'low']
225
+ data.at[index, 'Dn'] = self.toDecimal(data.at[index, 'low'])
364
226
  data.at[index, 'iDn'] = index
365
227
  # 寻找Bullish结构
366
228
  if data.at[index, 'Up'] > data.at[index - 1, 'Up']:
@@ -444,8 +306,8 @@ class SMCStrategyMaker(ThreeLineStrategyMaker):
444
306
  pivot_high_index = last_struct["pivot_high_index"] = int(data["iUp"].iloc[-1])
445
307
  pivot_low_index = last_struct["pivot_low_index"] = int(data["iDn"].iloc[-1])
446
308
 
447
- last_struct["pivot_high"] = float(data.loc[last_struct["pivot_high_index"], 'high'])
448
- last_struct["pivot_low"] = float(data.loc[last_struct["pivot_low_index"], 'low'])
309
+ last_struct["pivot_high"] = self.toDecimal(data.loc[last_struct["pivot_high_index"], 'high'])
310
+ last_struct["pivot_low"] = self.toDecimal(data.loc[last_struct["pivot_low_index"], 'low'])
449
311
 
450
312
  for i in range(len(data)-1, -1, -1):
451
313
  if check_valid_range:
@@ -510,17 +372,12 @@ class SMCStrategyMaker(ThreeLineStrategyMaker):
510
372
  next_lows = data['low'].iloc[index+1 :min(len(data),index + period)+1]
511
373
  return all(current_low <= prev_lows) and all(current_low < next_lows)
512
374
 
513
- def round_price(self,symbol, price):
514
- tick_size = self.get_tick_size(symbol)
515
- return super().round_price_to_tick(price, tick_size)
375
+ # def round_price(self,symbol, price: Decimal) -> Decimal:
376
+ # return super().round_price_to_tick(symbol, price)
516
377
 
517
- def calculate_ce(self,symbol,pivot_high , pivot_low) -> float:
378
+ def calculate_ce(self, symbol, pivot_high:Decimal , pivot_low:Decimal) -> Decimal:
518
379
  ce = (pivot_high + pivot_low) / 2
519
- return float(self.round_price(symbol, ce))
520
-
521
- def calculate_pe(self,symbol,pivot_high, pivot_low) -> float:
522
- pe = (pivot_high + pivot_low) / 2
523
- return float(self.round_price(symbol, pe))
380
+ return self.round_price_to_tick(symbol, ce)
524
381
 
525
382
  def reset_all_cache(self, symbol):
526
383
  """_summary_
@@ -568,7 +425,7 @@ class SMCStrategyMaker(ThreeLineStrategyMaker):
568
425
  swing_points_length = smc_strategy.get('swing_points_length',10)
569
426
  htf_last_struct = self.detect_struct(htf_df,prd=swing_points_length)
570
427
  htf_last_struct_label = htf_last_struct["struct"]
571
-
428
+ precision = self.get_precision_length(symbol)
572
429
 
573
430
  if htf_last_struct_label is None:
574
431
  self.logger.debug(f"{symbol} : {htf} 未形成 struct,不下单。{htf_last_struct}。")
@@ -586,8 +443,8 @@ class SMCStrategyMaker(ThreeLineStrategyMaker):
586
443
 
587
444
 
588
445
  # 2. HTF 获取最新的两个极值点,设置折价(discount)区和溢价(premium)区
589
- pivot_high = htf_last_struct["pivot_high"]
590
- pivot_low = htf_last_struct["pivot_low"]
446
+ pivot_high = self.toDecimal(htf_last_struct["pivot_high"])
447
+ pivot_low = self.toDecimal(htf_last_struct["pivot_low"])
591
448
  mid_line = self.calculate_ce(symbol,pivot_high,pivot_low)
592
449
 
593
450
  # 计算溢价和折价区
@@ -602,13 +459,16 @@ class SMCStrategyMaker(ThreeLineStrategyMaker):
602
459
  'ce': self.calculate_ce(symbol,mid_line,pivot_low)
603
460
  }
604
461
 
605
- 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}")
462
+ self.logger.debug(f"{symbol} : {htf} 趋势={htf_last_struct_label}: \n" \
463
+ f"pivot_high={pivot_high:.{precision}} pivot_low={pivot_low:.{precision}} mid_line={mid_line:.{precision}}\n" \
464
+ f"溢价区={premium_box}\n"
465
+ f"折价区={discount_box}")
606
466
 
607
467
  # 3. 根据HTF结构来分析下单位置和止盈位置
608
- threshold = 0.0
468
+ threshold = self.toDecimal(0.0)
609
469
  order_side = side
610
470
  # 获取当前市场价格
611
- market_price = ctf_df['close'].iloc[-1]
471
+ market_price = self.toDecimal(ctf_df['close'].iloc[-1])
612
472
 
613
473
  if 'CHoCH' in htf_last_struct_label:
614
474
  """
@@ -661,14 +521,14 @@ class SMCStrategyMaker(ThreeLineStrategyMaker):
661
521
 
662
522
 
663
523
  if threshold == 0.0:
664
- self.logger.debug(f"{symbol} : 价格{market_price}不在目标区域,不下单。")
524
+ self.logger.debug(f"{symbol} : 价格{market_price:.{precision}}不在目标区域,不下单。")
665
525
  # 取消所有未成交订单
666
526
  self.cancel_all_orders(symbol=symbol)
667
527
  return
668
528
 
669
529
 
670
530
  # 4. 在CTF折价区获取FVG的位置
671
- order_price = 0.0
531
+ order_price = self.toDecimal(0.0)
672
532
 
673
533
  if enable_FVG and order_price == 0.0:
674
534
 
@@ -694,7 +554,7 @@ class SMCStrategyMaker(ThreeLineStrategyMaker):
694
554
  if len(fvg_boxes) != 0 and order_price == 0.0:
695
555
  last_fvg_box = fvg_boxes[-1]
696
556
  ce_price = self.calculate_ce(symbol,last_fvg_box['top'],last_fvg_box['bot'])
697
- self.logger.info(f"{symbol} : 方向={order_side}, FVG_ce={ce_price} FVG={last_fvg_box} ")
557
+ self.logger.info(f"{symbol} : 方向={order_side}, FVG_ce={ce_price:.{precision}} FVG={last_fvg_box} ")
698
558
  order_price = ce_price
699
559
 
700
560
  # 4. 找OB位置,OB规则孤立高点+实体低点 孤立低点+实体高点
@@ -713,7 +573,7 @@ class SMCStrategyMaker(ThreeLineStrategyMaker):
713
573
  if len(OB_boxes) != 0 :
714
574
  last_OB_box = OB_boxes[-1]
715
575
  ce_price = self.calculate_ce(symbol,last_OB_box['top'],last_OB_box['bot'])
716
- self.logger.info(f"{symbol} : 方向={order_side}, OB_ce={ce_price} , OB={last_OB_box} ")
576
+ self.logger.info(f"{symbol} : 方向={order_side}, OB_ce={ce_price:.{precision}} , OB={last_OB_box} ")
717
577
  order_price = ce_price
718
578
 
719
579
  if order_price == 0.0:
@@ -723,7 +583,7 @@ class SMCStrategyMaker(ThreeLineStrategyMaker):
723
583
 
724
584
  latest_order_price = self.place_order_prices.get(symbol,0.0)
725
585
  if order_price == latest_order_price:
726
- self.logger.debug(f"{symbol} : 下单价格 {order_price} 未变化,不进行下单。")
586
+ self.logger.debug(f"{symbol} : 下单价格 {order_price:.{precision}} 未变化,不进行下单。")
727
587
  return
728
588
 
729
589
 
@@ -731,7 +591,7 @@ class SMCStrategyMaker(ThreeLineStrategyMaker):
731
591
  self.cancel_all_orders(symbol=symbol)
732
592
  self.place_order(symbol=symbol, price=order_price, side=order_side, pair_config=pair_config)
733
593
  self.place_order_prices[symbol] = order_price # 记录下单价格,过滤重复下单
734
- self.logger.debug(f"{symbol} : {side}, 下单价格 {order_price}")
594
+ self.logger.debug(f"{symbol} : {side}, 下单价格 {order_price:.{precision}}")
735
595
 
736
596
 
737
597
  except KeyboardInterrupt:
@@ -4,25 +4,31 @@ import ccxt
4
4
  import traceback
5
5
  import requests
6
6
  import pandas as pd
7
+ from decimal import Decimal
7
8
 
8
9
  from concurrent.futures import ThreadPoolExecutor, as_completed
9
10
 
10
11
 
12
+
11
13
  class ThreeLineStrategyMaker:
12
- def __init__(self, config, platform_config, feishu_webhook=None,logger=None):
14
+ def __init__(self, config, platform_config, common_config, feishu_webhook=None,logger=None):
13
15
 
14
16
  self.g_config = config
15
17
  self.feishu_webhook = feishu_webhook
16
- self.monitor_interval = self.g_config.get("monitor_interval", 4) # 默认值为60秒 # 监控循环时间是分仓监控的3倍
18
+ self.common_config = common_config
19
+ self.strategy_config = self.g_config.get('strategy', {})
20
+
17
21
  self.trading_pairs_config = self.g_config.get('tradingPairs', {})
22
+
23
+ self.leverage_value = self.strategy_config.get('leverage', 20)
24
+ self.is_demo_trading = self.common_config.get('is_demo_trading', 1) # live trading: 0, demo trading: 1
25
+
18
26
  self.highest_total_profit = 0 # 记录最高总盈利
19
- self.leverage_value = self.g_config.get('leverage', 2)
20
- self.is_demo_trading = self.g_config.get('is_demo_trading', 1) # live trading: 0, demo trading: 1
21
27
  # self.instrument_info_dict = {}
22
28
  self.cross_directions = {} # 持仓期间,存储每个交易对的交叉方向
23
29
  proxies = {
24
- "http": self.g_config.get('proxy', "http://localhost:7890"),
25
- "https": self.g_config.get('proxy', "http://localhost:7890")
30
+ "http": self.common_config.get('proxy', "http://localhost:7890"),
31
+ "https": self.common_config.get('proxy', "http://localhost:7890")
26
32
  }
27
33
 
28
34
  # 配置交易所
@@ -35,22 +41,39 @@ class ThreeLineStrategyMaker:
35
41
  'options': {'defaultType': 'future'},
36
42
  'proxies': proxies
37
43
  })
38
-
39
-
40
44
 
41
45
  self.logger = logger
42
46
  self.position_mode = self.get_position_mode() # 获取持仓模式
43
47
 
48
+ def format_price(self, symbol, price:Decimal) -> str:
49
+ precision = self.get_precision_length(symbol)
50
+ return f"{price:.{precision}f}"
51
+
52
+ @staticmethod
53
+ def toDecimal(value):
54
+ return Decimal(str(value))
55
+
56
+
44
57
  def getMarket(self,symbol):
45
58
  self.exchange.load_markets()
46
59
  return self.exchange.market(symbol)
47
60
 
48
- def get_tick_size(self,symbol):
49
- market = self.getMarket(symbol)
50
- marketPrecision = self.exchange.safe_dict(market, 'precision')
51
- return self.exchange.safe_float(marketPrecision, 'price')
61
+ def get_tick_size(self,symbol) -> Decimal:
62
+
63
+ try:
64
+ market = self.getMarket(symbol)
65
+ if market and 'precision' in market and 'price' in market['precision']:
66
+
67
+ return self.toDecimal(market['precision']['price'])
68
+ else:
69
+ # self.logger.error(f"{symbol}: 无法从市场数据中获取价格精度")
70
+ # return self.toDecimal("0.00001") # 返回默认精度
71
+ raise ValueError(f"{symbol}: 无法从市场数据中获取价格精度")
72
+ except Exception as e:
73
+ self.logger.error(f"{symbol}: 获取市场价格精度时发生错误: {str(e)}")
74
+ self.send_feishu_notification(f"{symbol}: 获取市场价格精度时发生错误: {str(e)}")
52
75
 
53
- def convert_contract(self, symbol, amount, price:float, direction='cost_to_contract'):
76
+ def convert_contract(self, symbol, amount, price:Decimal, direction='cost_to_contract'):
54
77
  """
55
78
  进行合约与币的转换
56
79
  :param symbol: 交易对符号,如 'BTC/USDT:USDT'
@@ -60,11 +83,12 @@ class ThreeLineStrategyMaker:
60
83
  """
61
84
 
62
85
  # 获取合约规模
63
- market_contractSize = self.getMarket(symbol)['contractSize']
86
+ market_contractSize = self.toDecimal(self.getMarket(symbol)['contractSize'])
87
+ amount = self.toDecimal(amount)
64
88
  if direction == 'amount_to_contract':
65
- contract_size = float(amount) / float(market_contractSize)
89
+ contract_size = amount / market_contractSize
66
90
  elif direction == 'cost_to_contract':
67
- contract_size = float(amount) / float(price) / float(market_contractSize)
91
+ contract_size = amount / price / market_contractSize
68
92
  else:
69
93
  raise Exception(f"{symbol}:{direction} 是无效的转换方向,请输入 'amount_to_contract' 或 'cost_to_contract'。")
70
94
 
@@ -74,7 +98,7 @@ class ThreeLineStrategyMaker:
74
98
  # 获取价格精度
75
99
  def get_precision_length(self,symbol) -> int:
76
100
  tick_size = self.get_tick_size(symbol)
77
- return len(f"{tick_size:.10f}".rstrip('0').split('.')[1]) if '.' in f"{tick_size:.10f}" else 0
101
+ return len(f"{tick_size:.15f}".rstrip('0').split('.')[1]) if '.' in f"{tick_size:.15f}" else 0
78
102
 
79
103
  def get_position_mode(self):
80
104
  try:
@@ -129,13 +153,13 @@ class ThreeLineStrategyMaker:
129
153
  else:
130
154
  raise ValueError("Unexpected response structure or missing 'last' key")
131
155
 
132
- def round_price_to_tick(self, price, tick_size):
156
+ def round_price_to_tick(self, symbol, price: Decimal) -> Decimal:
157
+ tick_size = self.get_tick_size(symbol)
133
158
  # 计算 tick_size 的小数位数
134
- tick_decimals = len(f"{tick_size:.10f}".rstrip('0').split('.')[1]) if '.' in f"{tick_size:.10f}" else 0
135
-
159
+ tick_decimals = self.get_precision_length(symbol)
136
160
  # 调整价格为 tick_size 的整数倍
137
161
  adjusted_price = round(price / tick_size) * tick_size
138
- return f"{adjusted_price:.{tick_decimals}f}"
162
+ return self.toDecimal(f"{adjusted_price:.{tick_decimals}f}")
139
163
 
140
164
  def get_historical_klines(self,symbol, bar='1m', limit=241):
141
165
  # response = market_api.get_candlesticks(instId, bar=bar, limit=limit)
@@ -214,7 +238,7 @@ class ThreeLineStrategyMaker:
214
238
 
215
239
  return diff
216
240
 
217
- def calculate_place_order_price(self, symbol,side,base_price, amplitude_limit, offset=1) -> float:
241
+ def calculate_place_order_price(self, symbol,side,base_price:Decimal, amplitude_limit: float, offset=1) -> Decimal:
218
242
  """
219
243
  计算开仓价格
220
244
  Args:
@@ -227,15 +251,16 @@ class ThreeLineStrategyMaker:
227
251
  place_order_price: 开仓价格
228
252
  """
229
253
  tick_size = self.get_tick_size(symbol)
254
+ precision= self.get_precision_length(symbol)
230
255
  place_order_price = None
231
256
  # 计算止盈价格,用市场价格(取持仓期间历史最高)减去开仓价格的利润,再乘以不同阶段的止盈百分比。
232
257
 
233
258
  if side == 'buy':
234
- place_order_price = base_price * (1- amplitude_limit/100) - offset * tick_size
259
+ place_order_price = base_price * (1- self.toDecimal(amplitude_limit)/100) - offset * tick_size
235
260
  else:
236
- place_order_price = base_price * (1 + amplitude_limit/100) + offset * tick_size
237
- self.logger.debug(f"++++ {symbol} 下单价格: {place_order_price:.9f} 方向 {side} 基准价格{base_price} 振幅限制 {amplitude_limit} ")
238
- return float(self.round_price_to_tick(place_order_price,tick_size))
261
+ place_order_price = base_price * (1 + self.toDecimal(amplitude_limit)/100) + offset * tick_size
262
+ self.logger.debug(f"++++ {symbol} 下单价格: {place_order_price:.{precision}f} 方向 {side} 基准价格{base_price:.{precision}} 振幅限制 {amplitude_limit} ")
263
+ return self.round_price_to_tick(place_order_price,tick_size)
239
264
 
240
265
  # 定义根据均线斜率判断 K 线方向的函数: 0 空 1 多 -1 平
241
266
  def judge_k_line_direction(self, symbol, ema: pd.Series, klines, period=3) -> int:
@@ -427,18 +452,18 @@ class ThreeLineStrategyMaker:
427
452
  self.logger.warning(f"{symbol} 检查持仓失败,正在进行第{retry_count}次重试: {str(e)}")
428
453
  time.sleep(0.1) # 重试前等待0.1秒
429
454
 
430
- def place_order(self,symbol, price, amount_usdt, side,order_type='limit'):
431
-
432
-
433
- # markets = self.exchange.load_markets()
434
- # if symbol not in markets:
435
- # self.logger.error(f"{symbol}: Instrument {symbol} not found in markets")
436
- # return
437
- # market = markets[symbol]
438
- # # 获取价格精度
439
- # price_precision = market['precision']['price']
440
- tick_size = self.get_tick_size(symbol)
441
- adjusted_price = self.round_price_to_tick(price, tick_size)
455
+ def place_order(self, symbol, price: Decimal, amount_usdt, side, order_type='limit'):
456
+ """
457
+ 下单
458
+ Args:
459
+ symbol: 交易对
460
+ price: 下单价格
461
+ amount_usdt: 下单金额
462
+ side: 下单方向
463
+ order_type: 订单类型
464
+ """
465
+ # 格式化价格
466
+ adjusted_price = self.format_price(symbol, price)
442
467
 
443
468
  if amount_usdt > 0:
444
469
  if side == 'buy':
@@ -448,7 +473,7 @@ class ThreeLineStrategyMaker:
448
473
  # 设置杠杆
449
474
  self.set_leverage(symbol=symbol, leverage=self.leverage_value, mgnMode='isolated',posSide=pos_side)
450
475
  # 20250220 SWAP类型计算合约数量
451
- contract_size = self.convert_contract(symbol=symbol, price = adjusted_price ,amount=amount_usdt)
476
+ contract_size = self.convert_contract(symbol=symbol, price = self.toDecimal(adjusted_price) ,amount=amount_usdt)
452
477
 
453
478
  params = {
454
479
 
@@ -456,7 +481,7 @@ class ThreeLineStrategyMaker:
456
481
  "side": side,
457
482
  "ordType": order_type,
458
483
  "sz": contract_size,
459
- "px": str(adjusted_price)
484
+ "px": adjusted_price
460
485
  }
461
486
 
462
487
  # 模拟盘(demo_trading)需要 posSide
@@ -470,7 +495,7 @@ class ThreeLineStrategyMaker:
470
495
  'side': side,
471
496
  'type': 'limit',
472
497
  'amount': contract_size,
473
- 'price': float(adjusted_price),
498
+ 'price': adjusted_price,
474
499
  'params': params
475
500
  }
476
501
  # 使用ccxt创建订单
@@ -486,7 +511,9 @@ class ThreeLineStrategyMaker:
486
511
  )
487
512
  # self.logger.debug(f"{symbol} ++ Order placed rs : {order_result}")
488
513
  except Exception as e:
489
- self.logger.error(f"{symbol} Failed to place order: {e}")
514
+ error_message = f"{symbol} Failed to place order: {e}"
515
+ self.logger.error(error_message)
516
+ self.send_feishu_notification(error_message)
490
517
  self.logger.info(f"--------- ++ {symbol} Order placed done! --------")
491
518
 
492
519
  def cancel_all_orders(self, symbol):
@@ -514,7 +541,9 @@ class ThreeLineStrategyMaker:
514
541
  except Exception as e:
515
542
  retry_count += 1
516
543
  if retry_count == max_retries:
517
- self.logger.warning(f"{symbol} 取消挂单失败(重试{retry_count}次): {str(e)}")
544
+ error_message = f"{symbol} 取消挂单失败(重试{retry_count}次): {str(e)}"
545
+ self.logger.error(error_message)
546
+ self.send_feishu_notification(error_message)
518
547
  return False
519
548
  self.logger.warning(f"{symbol} 取消挂单失败,正在进行第{retry_count}次重试: {str(e)}")
520
549
  time.sleep(0.1) # 重试前等待0.1秒
@@ -616,8 +645,8 @@ class ThreeLineStrategyMaker:
616
645
  df_3 = pd.DataFrame(klines[-4:-1], columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
617
646
  low_prices = df_3['low']
618
647
  high_prices = df_3['high']
619
- max_high = high_prices.max()
620
- min_low = low_prices.min()
648
+ max_high = self.toDecimal(high_prices.max())
649
+ min_low = self.toDecimal(low_prices.min())
621
650
 
622
651
  # 计算当前 振幅是否超过amplitude_limit
623
652
  long_amount_usdt = pair_config.get('long_amount_usdt', 5)
@@ -669,7 +698,19 @@ class ThreeLineStrategyMaker:
669
698
  self.send_feishu_notification(error_message)
670
699
 
671
700
  self.logger.info("-" * 60)
672
-
701
+
702
+ def get_pair_config(self,symbol):
703
+ # 获取交易对特定配置,如果没有则使用全局策略配置
704
+ pair_config = self.trading_pairs_config.get(symbol, {})
705
+
706
+ # 使用字典推导式合并配置,trading_pairs_config优先级高于strategy_config
707
+ pair_config = {
708
+ **self.strategy_config, # 基础配置
709
+ **pair_config # 交易对特定配置会覆盖基础配置
710
+ }
711
+ return pair_config
712
+
713
+
673
714
  def monitor_klines(self):
674
715
  symbols = list(self.trading_pairs_config.keys()) # 获取所有币对的ID
675
716
  batch_size = 5 # 每批处理的数量
@@ -678,8 +719,6 @@ class ThreeLineStrategyMaker:
678
719
  for i in range(0, len(symbols), batch_size):
679
720
  batch = symbols[i:i + batch_size]
680
721
  with ThreadPoolExecutor(max_workers=batch_size) as executor:
681
- futures = [executor.submit(self.process_pair, symbol,self.trading_pairs_config[symbol]) for symbol in batch]
722
+ futures = [executor.submit(self.process_pair, symbol,self.get_pair_config(symbol)) for symbol in batch]
682
723
  for future in as_completed(futures):
683
724
  future.result() # Raise any exceptions caught during execution
684
-
685
- # time.sleep(self.monitor_interval)
maker/main.py CHANGED
@@ -1,35 +1,15 @@
1
1
  import logging
2
+ import logging.config
2
3
  import yaml
3
- from logging.handlers import TimedRotatingFileHandler
4
4
  from apscheduler.triggers.interval import IntervalTrigger
5
5
  from apscheduler.schedulers.blocking import BlockingScheduler
6
6
  from datetime import datetime
7
7
  from pyfiglet import Figlet
8
8
 
9
- from maker.WickReversalStrategyMaker import WickReversalStrategyMaker
10
9
  from maker.ThreeLineStrategyMaker import ThreeLineStrategyMaker
11
- # from maker.MACDStrategyMaker import MACDStrategyMaker
12
10
  from maker.SMCStrategyMaker import SMCStrategyMaker
13
11
  from maker.BestFVGStrategyMaker import BestFVGStrategyMaker
14
12
 
15
- def build_logger(log_config) -> logging.Logger:
16
- # 配置日志
17
-
18
- log_file = log_config["file"]
19
- logger = logging.getLogger(__name__)
20
- logger.setLevel(log_config["level"])
21
-
22
- file_handler = TimedRotatingFileHandler(log_file, when='midnight', interval=1, backupCount=7, encoding='utf-8')
23
- file_handler.suffix = "%Y-%m-%d"
24
- formatter = logging.Formatter('%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s')
25
- file_handler.setFormatter(formatter)
26
- logger.addHandler(file_handler)
27
-
28
- console_handler = logging.StreamHandler()
29
- console_handler.setFormatter(formatter)
30
- logger.addHandler(console_handler)
31
-
32
- return logger
33
13
 
34
14
  def read_config_file(file_path):
35
15
  try:
@@ -51,36 +31,39 @@ def run_bot(bot, logger):
51
31
  logger.error(f"执行任务时发生错误: {str(e)}", exc_info=True)
52
32
 
53
33
  def main():
34
+
54
35
  import importlib.metadata
36
+ package_name = __package__ or "openfund-maker"
55
37
  version = importlib.metadata.version("openfund-maker")
56
38
 
57
39
  maker_config_path = 'maker_config.yaml'
58
40
  config_data = read_config_file(maker_config_path)
59
41
 
60
- platform_config = config_data['okx']
61
- feishu_webhook_url = config_data['feishu_webhook']
62
- logger = build_logger(config_data["Logger"])
63
- package_name = __package__ or "maker"
64
-
65
-
66
- maker = config_data.get('actived_maker', 'MACDStrategyMaker')
42
+ logging.config.dictConfig(config_data["Logger"])
43
+ logger = logging.getLogger("openfund-maker")
67
44
 
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...")
68
52
 
53
+ platform_config = config_data['platform']['okx']
69
54
  # 根据配置动态创建策略实例
70
55
  strategy_class = globals()[maker]
71
- bot = strategy_class(config_data, platform_config, feishu_webhook=feishu_webhook_url, logger=logger)
56
+ bot = strategy_class(config_data, platform_config, common_config, feishu_webhook=feishu_webhook_url, logger=logger)
72
57
 
73
- logger.info(f" ++ {package_name}.{maker}:{version} is doing...")
74
58
 
75
59
  # 获取计划配置
76
- schedule_config = config_data.get('schedule', {})
60
+ schedule_config = common_config.get('schedule', {})
77
61
  if schedule_config.get('enabled', False):
78
62
  scheduler = BlockingScheduler()
79
63
 
80
64
  # 设置每5分钟执行一次的任务,从整点开始
81
65
  monitor_interval = int(schedule_config.get('monitor_interval', 4))
82
- f = Figlet(font="standard") # 字体可选(如 "block", "bubble")
83
- logger.info(f"\n{f.renderText("OpenFund Maker")}")
66
+
84
67
  # 计算下一个整点分钟
85
68
  now = datetime.now()
86
69
  # 将当前时间的秒和微秒设置为0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: openfund-maker
3
- Version: 2.2.6
3
+ Version: 2.2.8
4
4
  Summary: Openfund-maker.
5
5
  Requires-Python: >=3.9,<4.0
6
6
  Classifier: Programming Language :: Python :: 3
@@ -0,0 +1,15 @@
1
+ maker/BestFVGStrategyMaker.py,sha256=xIiDIvMw8RwHayUuz5Won3stSG9BxdYIQgeXDok_SqM,12336
2
+ maker/MACDStrategyMaker.py,sha256=WX8wqpF9h5W4WclN1NjZ_Bur7KFi_aMTvacfLyHzEcI,12681
3
+ maker/SMCStrategyMaker.py,sha256=hkDqymWnuyYDo1gTYY_uyO4H4yOwCw8NBTM9RcfLRPc,28780
4
+ maker/ThreeLineStrategyMaker.py,sha256=jCdyTvI9oTMixCM0tQF9D_-6ig0-bYstKuRFECE4deE,32099
5
+ maker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ maker/history_code/WickReversalStrategyMaker.py,sha256=7DqPDVJot4EM0_lSAcFAHrR9rNvkIds9KLMoDOiAHEc,17486
7
+ maker/history_code/config.py,sha256=YPxghO5i0vgRg9Cja8kGj9O7pgSbbtzOgf3RexqXXwY,1188
8
+ maker/history_code/okxapi.py,sha256=_9G0U_o0ZC8NxaT6PqpiLgxBm9gPobC9PsFHZE1c5w0,553
9
+ maker/history_code/zhen.py.bak,sha256=HNkrQbJts8G9umE9chEFsc0cLQApcM9KOVNMYPpkBXM,10918
10
+ maker/history_code/zhen_2.py,sha256=4IaHVtTCMSlrLGSTZrWpW2q-f7HZsUNRkW_-5QgWv24,10509
11
+ maker/main.py,sha256=kumgkgyYj1tVKrHTQmSx0prFs9K4i5KpvcOnqimV2eA,3805
12
+ openfund_maker-2.2.8.dist-info/METADATA,sha256=sXu087Rbc_JVoJ3aKcWrt2RCJQ7rhB5u0zgblu2Rcf4,1955
13
+ openfund_maker-2.2.8.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
14
+ openfund_maker-2.2.8.dist-info/entry_points.txt,sha256=gKMytICEKcMRFQDFkHZLnIpID7UQFoTIM_xcpiiV6Ns,50
15
+ openfund_maker-2.2.8.dist-info/RECORD,,
maker/main_m.py DELETED
@@ -1,378 +0,0 @@
1
- import time
2
- import json
3
- import logging
4
- import requests
5
- import pandas as pd
6
-
7
- import okx.PublicData as PublicData
8
- import okx.Trade as Trade
9
- import okx.MarketData as MarketData
10
- import okx.Account as Account
11
-
12
- from concurrent.futures import ThreadPoolExecutor, as_completed
13
- from logging.handlers import TimedRotatingFileHandler
14
-
15
- # import app.okx_buou.Trade_api as TradeAPI
16
- # import app.okx_buou.Public_api as PublicAPI
17
- # import app.okx_buou.Market_api as MarketAPI
18
- # import app.okx_buou.Account_api as AccountAPI
19
-
20
- import os
21
- openfund_config_path = os.getenv('wick_reversal_config_path','config_okx.json')
22
- # 读取配置文件
23
- with open(openfund_config_path, 'r') as f:
24
- config = json.load(f)
25
-
26
- # 提取配置
27
- okx_config = config['okx']
28
- trading_pairs_config = config.get('tradingPairs', {})
29
- monitor_interval = config.get('monitor_interval', 60) # 默认60秒
30
- feishu_webhook = config.get('feishu_webhook', '')
31
- leverage_value = config.get('leverage', 10)
32
- api_key = okx_config["apiKey"]
33
- secret_key = okx_config["secret"]
34
- passphrase = okx_config["password"]
35
- flag = "0" # live trading: 0, demo trading: 1
36
-
37
- public_api = PublicData.PublicAPI(api_key, secret_key, passphrase, False, flag)
38
- trade_api = Trade.TradeAPI(api_key, secret_key, passphrase, False, flag)
39
- market_api = MarketData.MarketAPI(api_key, secret_key, passphrase, False, flag)
40
- account_api = Account.AccountAPI(api_key, secret_key, passphrase, False, flag)
41
- # trade_api = TradeAPI.TradeAPI(okx_config["apiKey"], okx_config["secret"], okx_config["password"], False, flag)
42
- # market_api = MarketAPI.MarketAPI(okx_config["apiKey"], okx_config["secret"], okx_config["password"], False, flag)
43
- # public_api = PublicAPI.PublicAPI(okx_config["apiKey"], okx_config["secret"], okx_config["password"], False, flag)
44
- # account_api = AccountAPI.AccountAPI(okx_config["apiKey"], okx_config["secret"], okx_config["password"], False, flag)
45
-
46
- log_file = "log/okx.log"
47
- logger = logging.getLogger(__name__)
48
- logger.setLevel(logging.DEBUG)
49
-
50
- file_handler = TimedRotatingFileHandler(log_file, when='midnight', interval=1, backupCount=7, encoding='utf-8')
51
- file_handler.suffix = "%Y-%m-%d"
52
- formatter = logging.Formatter('%(asctime)s - %(name)s -%(lineno)d - %(levelname)s - %(message)s')
53
- file_handler.setFormatter(formatter)
54
- logger.addHandler(file_handler)
55
-
56
- console_handler = logging.StreamHandler()
57
- console_handler.setFormatter(formatter)
58
- logger.addHandler(console_handler)
59
-
60
- instrument_info_dict = {}
61
-
62
- def fetch_and_store_all_instruments(instType='SWAP'):
63
- try:
64
- logger.info(f"Fetching all instruments for type: {instType}")
65
-
66
- response = public_api.get_instruments(instType=instType)
67
- # response = public_api.get_instruments(instType=instType)
68
- # logger.debug(f"data: {response['data']}")
69
- if 'data' in response and len(response['data']) > 0:
70
- instrument_info_dict.clear()
71
- for instrument in response['data']:
72
- instId = instrument['instId']
73
- instrument_info_dict[instId] = instrument
74
- # logger.debug(f"Stored instrument: {instId}")
75
- else:
76
- raise ValueError("Unexpected response structure or no instrument data available")
77
- except Exception as e:
78
- logger.error(f"Error fetching instruments: {e}")
79
- raise
80
-
81
- def send_feishu_notification(message):
82
- if feishu_webhook:
83
- headers = {'Content-Type': 'application/json'}
84
- data = {"msg_type": "text", "content": {"text": message}}
85
- response = requests.post(feishu_webhook, headers=headers, json=data)
86
- if response.status_code == 200:
87
- logger.debug("飞书通知发送成功")
88
- else:
89
- logger.error(f"飞书通知发送失败: {response.text}")
90
-
91
- def get_close_price(instId):
92
- '''
93
- bar =
94
- 时间粒度,默认值1m
95
- 如 [1m/3m/5m/15m/30m/1H/2H/4H]
96
- 香港时间开盘价k线:[6H/12H/1D/2D/3D/1W/1M/3M]
97
- UTC时间开盘价k线:[/6Hutc/12Hutc/1Dutc/2Dutc/3Dutc/1Wutc/1Mutc/3Mutc]
98
- '''
99
- response = market_api.get_candlesticks(instId=instId,bar='1m')
100
- if 'data' in response and len(response['data']) > 0:
101
- close_price = response['data'][0][4]
102
- return float(close_price)
103
- else:
104
- raise ValueError("Unexpected response structure or missing 'c' value")
105
-
106
-
107
- def get_mark_price(instId):
108
- response = market_api.get_ticker(instId)
109
- if 'data' in response and len(response['data']) > 0:
110
- last_price = response['data'][0]['last']
111
- return float(last_price)
112
- else:
113
- raise ValueError("Unexpected response structure or missing 'last' key")
114
-
115
- def round_price_to_tick(price, tick_size):
116
- # 计算 tick_size 的小数位数
117
- tick_decimals = len(f"{tick_size:.10f}".rstrip('0').split('.')[1]) if '.' in f"{tick_size:.10f}" else 0
118
-
119
- # 调整价格为 tick_size 的整数倍
120
- adjusted_price = round(price / tick_size) * tick_size
121
- return f"{adjusted_price:.{tick_decimals}f}"
122
-
123
- def get_historical_klines(instId, bar='1m', limit=241):
124
- response = market_api.get_candlesticks(instId, bar=bar, limit=limit)
125
- if 'data' in response and len(response['data']) > 0:
126
- return response['data']
127
- else:
128
- raise ValueError("Unexpected response structure or missing candlestick data")
129
-
130
- def calculate_atr(klines, period=60):
131
- trs = []
132
- for i in range(1, len(klines)):
133
- high = float(klines[i][2])
134
- low = float(klines[i][3])
135
- prev_close = float(klines[i-1][4])
136
- tr = max(high - low, abs(high - prev_close), abs(low - prev_close))
137
- trs.append(tr)
138
- atr = sum(trs[-period:]) / period
139
- return atr
140
-
141
- def calculate_ema_pandas(data, period):
142
- """
143
- 使用 pandas 计算 EMA
144
- :param 收盘价列表
145
- :param period: EMA 周期
146
- :return: EMA 值
147
- """
148
- df = pd.Series(data)
149
- ema = df.ewm(span=period, adjust=False).mean()
150
- return ema.iloc[-1] # 返回最后一个 EMA 值
151
-
152
-
153
- def calculate_average_amplitude(klines, period=60):
154
- amplitudes = []
155
- for i in range(len(klines) - period, len(klines)):
156
- high = float(klines[i][2])
157
- low = float(klines[i][3])
158
- close = float(klines[i][4])
159
- amplitude = ((high - low) / close) * 100
160
- amplitudes.append(amplitude)
161
- average_amplitude = sum(amplitudes) / len(amplitudes)
162
- return average_amplitude
163
-
164
- def cancel_all_orders(instId):
165
- open_orders = trade_api.get_order_list(instId=instId, state='live')
166
- order_ids = [order['ordId'] for order in open_orders['data']]
167
- for ord_id in order_ids:
168
- trade_api.cancel_order(instId=instId, ordId=ord_id)
169
- logger.info(f"{instId}挂单取消成功.")
170
-
171
- def set_leverage(instId, leverage, mgnMode='isolated',posSide=None):
172
- try:
173
- body = {
174
- "instId": instId,
175
- "lever": str(leverage),
176
- "mgnMode": mgnMode
177
- }
178
- # 模拟盘需要控制 posSide
179
- if flag =='1' and mgnMode == 'isolated' and posSide:
180
- body["posSide"] = posSide
181
- logger.debug(f"Leverage set parameter is:{body}")
182
- response = account_api.set_leverage(**body)
183
- if response['code'] == '0':
184
- logger.debug(f"Leverage set to {leverage}x for {instId} with mgnMode: {mgnMode}")
185
- else:
186
- logger.error(f"Failed to set leverage: {response['msg']}")
187
- except Exception as e:
188
- logger.error(f"Error setting leverage: {e}")
189
-
190
- def place_order(instId, price, amount_usdt, side):
191
- if instId not in instrument_info_dict:
192
- logger.error(f"Instrument {instId} not found in instrument info dictionary")
193
- return
194
- tick_size = float(instrument_info_dict[instId]['tickSz'])
195
- adjusted_price = round_price_to_tick(price, tick_size)
196
- # response = public_api.convert_contract_coin(type='1', instId=instId, sz=str(amount_usdt), px=str(adjusted_price), unit='usdt', opType='open')
197
-
198
- # https://www.okx.com/docs-v5/zh/#public-data-rest-api-unit-convert
199
- '''
200
- type 转换类型
201
- 1:币转张
202
- 2:张转币
203
- 默认为1
204
- '''
205
-
206
- response = public_api.get_convert_contract_coin(type='1', instId=instId, sz=str(amount_usdt), px=str(adjusted_price), unit='usdt')
207
- if response['code'] == '0':
208
- sz = response['data'][0]['sz']
209
- if float(sz) > 0:
210
-
211
- if side == 'buy':
212
- pos_side = 'long'
213
- else:
214
- pos_side = 'short'
215
-
216
- set_leverage(instId=instId, leverage=leverage_value, mgnMode='isolated',posSide=pos_side)
217
-
218
-
219
- params = {
220
- "instId": instId,
221
- "tdMode": 'isolated',
222
- "side": side,
223
- "ordType": 'limit',
224
- "sz": sz,
225
- "px": str(adjusted_price)
226
- }
227
- # 模拟盘需要控制 posSide
228
- if flag == 1 :
229
- params["posSide"] = pos_side
230
-
231
- logger.info(f"Order placed params: {params}")
232
- order_result = trade_api.place_order(
233
- **params
234
- # instId=instId,
235
- # #tdMode='isolated',
236
- # tdMode='cross',# 保证金模式:isolated:逐仓 ;cross:全仓
237
- # side=side, # 订单方向 buy:买, sell:卖
238
- # posSide=pos_side, #持仓方向 在开平仓模式下必填,且仅可选择 long 或 short。 仅适用交割、永续。
239
- # ordType='limit',
240
- # sz=sz,
241
- # px=str(adjusted_price)
242
- )
243
- logger.debug(f"Order placed: {order_result}")
244
- else:
245
- logger.info(f"{instId}计算出的合约张数太小,无法下单。")
246
- else:
247
- logger.info(f"{instId}转换失败: {response['msg']}")
248
- send_feishu_notification(f"{instId}转换失败: {response['msg']}")
249
- logger.info(f"------------------ {instId} Order placed done! ------------------")
250
- def check_position(instId, instType='SWAP') -> bool:
251
- """
252
- 检查指定交易对是否有持仓
253
-
254
- Args:
255
- instType: 交易对类型 SPOT、SWAP、FUTURES
256
- instId: 交易对ID
257
-
258
- Returns:
259
- bool: 是否有持仓
260
- """
261
- try:
262
-
263
- positions = account_api.get_positions(instType=instType)
264
- if positions and 'data' in positions and len(positions['data']) > 0:
265
- logger.debug(f"{instId} 有持仓,{positions['data']}")
266
- return True
267
- return False
268
- except Exception as e:
269
- logger.error(f"检查持仓失败 {instId}: {str(e)}")
270
- return False
271
-
272
-
273
- # 处理交易对
274
- def process_pair(instId, pair_config):
275
- if check_position(instId,instType='SWAP'):
276
- logger.info(f"{instId} 有持仓,不下单!")
277
- return
278
- try:
279
- use_market_price = pair_config.get('use_market_price', 1)
280
- if use_market_price == 1 :
281
- mark_price = get_mark_price(instId)
282
- else :
283
- mark_price = get_close_price(instId) # 替换成上周期的收盘价格
284
- klines = get_historical_klines(instId)
285
-
286
- # 提取收盘价数据用于计算 EMA
287
- close_prices = [float(kline[4]) for kline in klines[::-1]] # K线中的收盘价,顺序要新的在最后
288
-
289
- # 计算 EMA
290
- ema_value = pair_config.get('ema', 240)
291
- # 如果ema值为0 不区分方向,两头都挂单
292
- if ema_value == 0:
293
- is_bullish_trend = True
294
- is_bearish_trend = True
295
- else:
296
- ema60 = calculate_ema_pandas(close_prices, period=ema_value)
297
- logger.info(f"{instId} EMA60: {ema60:.6f}, 当前价格: {mark_price:.6f}")
298
- # 判断趋势:多头趋势或空头趋势
299
- is_bullish_trend = close_prices[-1] > ema60 # 收盘价在 EMA60 之上
300
- is_bearish_trend = close_prices[-1] < ema60 # 收盘价在 EMA60 之下
301
-
302
- # 计算 ATR
303
- atr = calculate_atr(klines)
304
- # 当前价格/ATR比值
305
- price_atr_ratio = (mark_price / atr) / 100
306
- logger.info(f"{instId} ATR: {atr:.3f}, 当前价格/ATR比值: {price_atr_ratio:.3f}")
307
- # 平均振幅
308
- average_amplitude = calculate_average_amplitude(klines)
309
- logger.info(f"{instId} 平均振幅: {average_amplitude:.2f}%")
310
-
311
- value_multiplier = pair_config.get('value_multiplier', 2)
312
- '''
313
- 接针的挂单距离,默认计算逻辑是atr/close 跟 振幅ma的区间求最小值 *系数,如果周期小这样其实大部分时候都是采用的振幅,
314
- 其实可以多试试其他方案,比如改成atr/close 跟 振幅ma的平均值,这样的话atr权重实际会更大,大部分行情还是atr反应更直接。
315
- '''
316
- # selected_value = (average_amplitude + price_atr_ratio)/2 * value_multiplier
317
-
318
- selected_value = min(average_amplitude, price_atr_ratio) * value_multiplier
319
- amplitude_limit = float(pair_config.get('amplitude_limit', 0.8))
320
- selected_value = max(selected_value, amplitude_limit)
321
- logger.info(f"{instId} selected_value: {selected_value} ")
322
-
323
-
324
- long_price_factor = 1 - selected_value / 100
325
- short_price_factor = 1 + selected_value / 100
326
-
327
- long_amount_usdt = pair_config.get('long_amount_usdt', 5)
328
- short_amount_usdt = pair_config.get('short_amount_usdt', 5)
329
-
330
- target_price_long = mark_price * long_price_factor
331
- target_price_short = mark_price * short_price_factor
332
-
333
- logger.info(f"{instId} mark_price: {mark_price} Long target price: {target_price_long:.6f}, Short target price: {target_price_short:.6f}")
334
-
335
- cancel_all_orders(instId)
336
-
337
- # 判断趋势后决定是否挂单
338
- if is_bullish_trend:
339
- logger.info(f"{instId} 当前为多头趋势,允许挂多单")
340
- # send_feishu_notification(f"{instId} place_order:+buy+,目标价格:{target_price_long},交易USDT:{long_amount_usdt} ")
341
- place_order(instId, target_price_long, long_amount_usdt, 'buy')
342
- else:
343
- logger.info(f"{instId} 当前非多头趋势,跳过多单挂单")
344
-
345
- if is_bearish_trend:
346
- logger.info(f"{instId} 当前为空头趋势,允许挂空单")
347
- # send_feishu_notification(f"{instId} place_order:-sell-,目标价格:{target_price_short},交易USDT:{short_amount_usdt} ")
348
- place_order(instId, target_price_short, short_amount_usdt, 'sell')
349
- else:
350
- logger.info(f"{instId} 当前非空头趋势,跳过空单挂单")
351
-
352
- except Exception as e:
353
- error_message = f'Error processing {instId}: {e}'
354
- logger.error(error_message)
355
- send_feishu_notification(error_message)
356
-
357
- def main():
358
- import importlib.metadata
359
-
360
- version = importlib.metadata.version("openfund-wick-reversal")
361
- logger.info(f" ++ openfund-wick-reversal:{version} is doing...")
362
- logger.info(f" ++ api_key : {api_key}")
363
- fetch_and_store_all_instruments()
364
- inst_ids = list(trading_pairs_config.keys()) # 获取所有币对的ID
365
- batch_size = 5 # 每批处理的数量
366
-
367
- while True:
368
- for i in range(0, len(inst_ids), batch_size):
369
- batch = inst_ids[i:i + batch_size]
370
- with ThreadPoolExecutor(max_workers=batch_size) as executor:
371
- futures = [executor.submit(process_pair, instId, trading_pairs_config[instId]) for instId in batch]
372
- for future in as_completed(futures):
373
- future.result() # Raise any exceptions caught during execution
374
-
375
- time.sleep(monitor_interval)
376
-
377
- if __name__ == '__main__':
378
- main()
@@ -1,16 +0,0 @@
1
- maker/BestFVGStrategyMaker.py,sha256=YZiHrUlQPVnlFwsfuwbgG81mrNLZDrq6nIHAjh2KQRk,12154
2
- maker/MACDStrategyMaker.py,sha256=iS5HO04piKHFJxUI2e5QmicxzGeK-V1aphJSr2n_4Ac,12651
3
- maker/SMCStrategyMaker.py,sha256=grmQKqUj03qVhHyo24qUk8eFmPeyTqq1taYebxzSDXw,35351
4
- maker/ThreeLineStrategyMaker.py,sha256=JhPfebBLEL7LIQj1YcH0j1mco1Nuocs8ceR6bjdI8qc,30517
5
- maker/WickReversalStrategyMaker.py,sha256=7DqPDVJot4EM0_lSAcFAHrR9rNvkIds9KLMoDOiAHEc,17486
6
- maker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
- maker/config.py,sha256=YPxghO5i0vgRg9Cja8kGj9O7pgSbbtzOgf3RexqXXwY,1188
8
- maker/main.py,sha256=9b7YRh0rOZlCXmTvAXE3oGhmE4b19NCc0557_jY8bV8,4564
9
- maker/main_m.py,sha256=0PzDTnuBrxfpy5WDfsIHKAzZ_7pkuvuqqeWik0vpWio,15522
10
- maker/okxapi.py,sha256=_9G0U_o0ZC8NxaT6PqpiLgxBm9gPobC9PsFHZE1c5w0,553
11
- maker/zhen.py.bak,sha256=HNkrQbJts8G9umE9chEFsc0cLQApcM9KOVNMYPpkBXM,10918
12
- maker/zhen_2.py,sha256=4IaHVtTCMSlrLGSTZrWpW2q-f7HZsUNRkW_-5QgWv24,10509
13
- openfund_maker-2.2.6.dist-info/METADATA,sha256=qW2yuUoP7qTjOj3cinBb7SzKzzpbRjLMPD54GgeznOg,1955
14
- openfund_maker-2.2.6.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
15
- openfund_maker-2.2.6.dist-info/entry_points.txt,sha256=gKMytICEKcMRFQDFkHZLnIpID7UQFoTIM_xcpiiV6Ns,50
16
- openfund_maker-2.2.6.dist-info/RECORD,,
File without changes
File without changes
File without changes
File without changes