openfund-core 1.0.1__py3-none-any.whl → 1.0.7__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.
core/Exchange.py CHANGED
@@ -10,6 +10,15 @@ from ccxt.base.exchange import ConstructorArgs
10
10
 
11
11
 
12
12
  class Exchange:
13
+ BUY_SIDE = 'buy'
14
+ SELL_SIDE = 'sell'
15
+ LONG_KEY = 'long'
16
+ SHORT_KEY = 'short'
17
+ SIDE_KEY = 'side'
18
+ SYMBOL_KEY = 'symbol'
19
+ ENTRY_PRICE_KEY = 'entryPrice'
20
+ MARK_PRICE_KEY = 'markPrice'
21
+ CONTRACTS_KEY = 'contracts'
13
22
  def __init__(self, config:ConstructorArgs, exchangeKey:str = "okx",) :
14
23
  # 配置交易所
15
24
  self.exchange = getattr(ccxt, exchangeKey)(config)
@@ -22,19 +31,8 @@ class Exchange:
22
31
  self.exchange.load_markets()
23
32
 
24
33
  return self.exchange.market(symbol)
25
-
26
- def get_tick_size(self,symbol) -> Decimal:
27
-
28
- market = self.getMarket(symbol)
29
- if market and 'precision' in market and 'price' in market['precision']:
30
- return OPTools.toDecimal(market['precision']['price'])
31
- else:
32
- raise ValueError(f"{symbol}: 无法从市场数据中获取价格精度")
33
-
34
- def amount_to_precision(self,symbol, contract_size):
35
- return self.exchange.amount_to_precision(symbol, contract_size)
36
-
37
- def get_position_mode(self):
34
+
35
+ def get_position_mode(self) -> str:
38
36
 
39
37
  try:
40
38
  # 假设获取账户持仓模式的 API
@@ -52,6 +50,19 @@ class Exchange:
52
50
  error_message = f"Error fetching position mode: {e}"
53
51
  self.logger.error(error_message)
54
52
  raise Exception(error_message)
53
+
54
+ def get_tick_size(self,symbol) -> Decimal:
55
+
56
+ market = self.getMarket(symbol)
57
+ if market and 'precision' in market and 'price' in market['precision']:
58
+ return OPTools.toDecimal(market['precision']['price'])
59
+ else:
60
+ raise ValueError(f"{symbol}: 无法从市场数据中获取价格精度")
61
+
62
+ def amount_to_precision(self,symbol, contract_size):
63
+ return self.exchange.amount_to_precision(symbol, contract_size)
64
+
65
+
55
66
 
56
67
  def set_leverage(self,symbol, leverage, mgnMode='isolated',posSide=None):
57
68
  try:
@@ -96,10 +107,66 @@ class Exchange:
96
107
  elif direction == 'cost_to_contract':
97
108
  contract_size = amount / price / market_contractSize
98
109
  else:
99
- raise Exception(f"{symbol}:{direction} 是无效的转换方向,请输入 'amount_to_contract' 或 'cost_to_contract'。")
110
+ raise Exception(f"{symbol} : {direction} 是无效的转换方向,请输入 'amount_to_contract' 或 'cost_to_contract'。")
100
111
 
101
112
  return self.amount_to_precision(symbol, contract_size)
102
-
113
+
114
+ def close_position(self, symbol, position, params={}) -> dict:
115
+
116
+ amount = abs(float(position['contracts']))
117
+
118
+ if amount <= 0:
119
+ self.logger.warning(f"{symbol}: position contracts must be greater than 0")
120
+ return
121
+
122
+ max_retries = 3
123
+ retry_count = 0
124
+ while retry_count < max_retries:
125
+
126
+ try:
127
+ side = position[self.SIDE_KEY]
128
+ self.logger.debug(f"{symbol}: Preparing to close position, side= {side}, amount={amount}")
129
+ position_mode = self.get_position_mode() # 获取持仓模式
130
+ if position_mode == 'long_short_mode':
131
+ # 在双向持仓模式下,指定平仓方向
132
+ # pos_side = 'long' if side == 'long' else 'short'
133
+ pos_side = side
134
+ else:
135
+ # 在单向模式下,不指定方向
136
+ pos_side = 'net'
137
+ orderSide = 'buy' if side == 'long' else 'sell'
138
+
139
+ td_mode = position['marginMode']
140
+ params = {
141
+ 'mgnMode': td_mode,
142
+ 'posSide': pos_side,
143
+ # 当市价全平时,平仓单是否需要自动撤销,默认为false. false:不自动撤单 true:自动撤单
144
+ 'autoCxl': 'true',
145
+ **params
146
+
147
+ }
148
+
149
+ # 发送平仓请求并获取返回值
150
+ order = self.exchange.close_position(
151
+ symbol=symbol,
152
+ side=orderSide,
153
+ params=params
154
+ )
155
+
156
+ self.logger.info(f"{symbol} Close position response : {order}")
157
+ return order
158
+
159
+ except Exception as e:
160
+
161
+ retry_count += 1
162
+ if retry_count == max_retries:
163
+ error_message = f"{symbol} Error closing position : {str(e)}"
164
+ self.logger.error(error_message)
165
+ raise Exception(error_message)
166
+ else:
167
+ self.logger.warning(f"{symbol} 平仓失败,正在进行第{retry_count}次重试: {str(e)}")
168
+ time.sleep(0.1) # 重试前等待0.1秒
169
+
103
170
 
104
171
  def cancel_all_orders(self, symbol):
105
172
  max_retries = 3
@@ -133,8 +200,171 @@ class Exchange:
133
200
  self.logger.warning(f"{symbol} 取消挂单失败,正在进行第{retry_count}次重试: {str(e)}")
134
201
  time.sleep(0.1) # 重试前等待0.1秒
135
202
 
203
+ def cancel_all_algo_orders(self, symbol, attachType=None) -> bool:
204
+ """_summary_
205
+
206
+ Args:
207
+ symbol (_type_): _description_
208
+ attachType (_type_, optional): "TP"|"SL". Defaults to None.
209
+ """
210
+
211
+ params = {
212
+ "ordType": "conditional",
213
+ }
214
+ try:
215
+ orders = self.fetch_open_orders(symbol=symbol,params=params)
216
+ except Exception as e:
217
+ error_message = f"!!{symbol} : Error fetching open orders: {e}"
218
+ self.logger.error(error_message)
219
+ raise Exception(error_message)
220
+
221
+
222
+ if len(orders) == 0:
223
+ self.logger.debug(f"{symbol} 未设置策略订单列表。")
224
+ return True
225
+
226
+ algo_ids = []
227
+ if attachType and attachType == 'SL':
228
+ algo_ids = [order['id'] for order in orders if order['stopLossPrice'] and order['stopLossPrice'] > 0.0 ]
229
+ elif attachType and attachType == 'TP':
230
+ algo_ids = [order['id'] for order in orders if order['takeProfitPrice'] and order['takeProfitPrice'] > 0.0]
231
+ else :
232
+ algo_ids = [order['id'] for order in orders ]
233
+
234
+ if len(algo_ids) == 0 :
235
+ self.logger.debug(f"{symbol} 未设置策略订单列表。")
236
+ return True
237
+
238
+ max_retries = 3
239
+ retry_count = 0
240
+
241
+ while retry_count < max_retries:
242
+ try:
243
+ params = {
244
+ "algoId": algo_ids,
245
+ "trigger": 'trigger'
246
+ }
247
+ rs = self.exchange.cancel_orders(ids=algo_ids, symbol=symbol, params=params)
248
+
249
+ return len(rs) > 0
250
+
251
+ except Exception as e:
252
+ retry_count += 1
253
+ if retry_count == max_retries:
254
+ error_message = f"!!{symbol} : Error cancelling order {algo_ids}: {e}"
255
+ self.logger.error(error_message)
256
+ raise Exception(error_message)
257
+
258
+ self.logger.warning(f"{symbol} : Error cancelling order {algo_ids}: {str(e)}")
259
+ time.sleep(0.1) # 重试前等待0.1秒
260
+ def place_algo_orders(self, symbol, position, price: Decimal, order_type, sl_or_tp='SL', params={}) -> bool:
261
+ """
262
+ 下单
263
+ Args:
264
+ symbol: 交易对
265
+ position: 仓位
266
+ price: 下单价格
267
+ order_type: 订单类型
268
+ """
269
+ # 计算下单数量
270
+ amount = abs(position[self.CONTRACTS_KEY])
271
+
272
+ if amount <= 0:
273
+ self.logger.warning(f"{symbol}: amount is 0 for {symbol}")
274
+ return
275
+
276
+ # 止损单逻辑
277
+ adjusted_price = self.format_price(symbol, price)
278
+
279
+ # 默认市价止损,委托价格为-1时,执行市价止损。
280
+ sl_params = {
281
+ **params,
282
+ 'slTriggerPx':adjusted_price ,
283
+ 'slOrdPx':'-1', # 委托价格为-1时,执行市价止损
284
+ # 'slOrdPx' : adjusted_price,
285
+ 'slTriggerPxType':'last',
286
+ 'tdMode':position['marginMode'],
287
+ 'sz': str(amount),
288
+ 'cxlOnClosePos': True,
289
+ 'reduceOnly':True,
290
+ }
291
+
292
+ tp_params = {
293
+ **params,
294
+ 'tpTriggerPx':adjusted_price,
295
+ 'tpOrdPx' : adjusted_price,
296
+ 'tpOrdKind': 'condition',
297
+ 'tpTriggerPxType':'last',
298
+ 'tdMode':position['marginMode'],
299
+ 'sz': str(amount),
300
+ 'cxlOnClosePos': True,
301
+ 'reduceOnly':True
302
+ }
303
+
304
+ order_params = sl_params if sl_or_tp == 'SL' else tp_params
305
+ # order_params.update(params)
306
+
307
+ if order_type == 'limit' and sl_or_tp =='SL':
308
+ order_params['slOrdPx'] = adjusted_price
309
+
310
+ orderSide = self.BUY_SIDE if position[self.SIDE_KEY] == self.SHORT_KEY else self.SELL_SIDE # 和持仓反向相反下单
311
+
312
+ order = {
313
+ 'symbol': symbol,
314
+ 'side': orderSide,
315
+ 'type': order_type,
316
+ 'amount': amount,
317
+ 'price': adjusted_price,
318
+ 'params': order_params
319
+ }
320
+
321
+ max_retries = 3
322
+ retry_count = 0
323
+ self.logger.info(f"{symbol} : Pre Algo Order placed: {order} ")
324
+ while retry_count < max_retries:
325
+ try:
326
+
327
+ self.exchange.create_order(
328
+ **order
329
+ # symbol=symbol,
330
+ # type=order_type,
331
+ # price=adjusted_price,
332
+ # side=orderSide,
333
+ # amount=amount,
334
+ # params=order_params
335
+ )
336
+
337
+ break
338
+
339
+ except ccxt.NetworkError as e:
340
+ # 处理网络相关错误
341
+ retry_count += 1
342
+ self.logger.warning(f"{symbol} : 设置止盈止损时发生网络错误,正在进行第{retry_count}次重试: {str(e)}")
343
+ time.sleep(0.1) # 重试前等待1秒
344
+ continue
345
+ except ccxt.ExchangeError as e:
346
+ # 处理交易所API相关错误
347
+ retry_count += 1
348
+ self.logger.warning(f"{symbol} : 设置止盈止损单时发生交易所错误,正在进行第{retry_count}次重试: {str(e)}")
349
+ time.sleep(0.1)
350
+ continue
351
+ except Exception as e:
352
+ # 处理其他未预期的错误
353
+ retry_count += 1
354
+ self.logger.warning(f"{symbol} : 设置止盈止损单时发生未知错误,正在进行第{retry_count}次重试: {str(e)}")
355
+ time.sleep(0.1)
356
+ continue
357
+
358
+ if retry_count >= max_retries:
359
+ # 重试次数用完仍未成功设置止损单
360
+ error_message = f"!! {symbol}: 设置止盈止损单时重试次数用完仍未成功设置成功。 "
361
+ self.logger.error(error_message)
362
+ raise Exception(error_message)
363
+ self.logger.debug(f"{symbol} : --------- ++ Order placed done. --------")
364
+ return True
365
+
136
366
 
137
- def place_order(self, symbol, price: Decimal, amount_usdt, side, leverage=20, order_type='limit'):
367
+ def place_order(self, symbol, price: Decimal, amount_usdt, side, leverage=20, order_type='limit', params={}) -> bool:
138
368
  """
139
369
  下单
140
370
  Args:
@@ -147,41 +377,47 @@ class Exchange:
147
377
  # 格式化价格
148
378
  adjusted_price = self.format_price(symbol, price)
149
379
 
150
- if amount_usdt > 0:
151
- if side == 'buy':
152
- pos_side = 'long'
153
- else:
154
- pos_side = 'short'
155
- # 设置杠杆
156
- self.set_leverage(symbol=symbol, leverage=leverage, mgnMode='isolated',posSide=pos_side)
157
- # 20250220 SWAP类型计算合约数量
158
- contract_size = self.convert_contract(symbol=symbol, price = OPTools.toDecimal(adjusted_price) ,amount=amount_usdt)
380
+ if amount_usdt <= 0:
381
+ self.logger.warning(f"{symbol}: amount_usdt must be greater than 0")
382
+ return
159
383
 
160
- params = {
161
-
162
- "tdMode": 'isolated',
163
- "side": side,
164
- "ordType": order_type,
165
- "sz": contract_size,
166
- "px": adjusted_price
167
- }
384
+ pos_side = self.LONG_KEY if side == self.BUY_SIDE else self.SHORT_KEY
385
+
386
+ # 设置杠杆
387
+ self.set_leverage(symbol=symbol, leverage=leverage, mgnMode='isolated',posSide=pos_side)
388
+ # 20250220 SWAP类型计算合约数量
389
+ contract_size = self.convert_contract(symbol=symbol, price = OPTools.toDecimal(adjusted_price) ,amount=amount_usdt)
390
+
391
+ order_params = {
392
+ **params,
393
+ "tdMode": 'isolated',
394
+ "side": side,
395
+ "ordType": order_type,
396
+ "sz": contract_size,
397
+ "px": adjusted_price
398
+ }
399
+
400
+ # # 模拟盘(demo_trading)需要 posSide
401
+ # if self.is_demo_trading == 1 :
402
+ # params["posSide"] = pos_side
168
403
 
169
- # # 模拟盘(demo_trading)需要 posSide
170
- # if self.is_demo_trading == 1 :
171
- # params["posSide"] = pos_side
172
-
173
- # self.logger.debug(f"---- Order placed params: {params}")
404
+ order = {
405
+ 'symbol': symbol,
406
+ 'side': side,
407
+ 'type': order_type,
408
+ 'amount': contract_size,
409
+ 'price': adjusted_price,
410
+ 'params': order_params
411
+ }
412
+
413
+ max_retries = 3
414
+ retry_count = 0
415
+
416
+ self.logger.info(f"{symbol} : Pre Order placed: {order} ")
417
+
418
+ while retry_count < max_retries:
174
419
  try:
175
- order = {
176
- 'symbol': symbol,
177
- 'side': side,
178
- 'type': 'limit',
179
- 'amount': contract_size,
180
- 'price': adjusted_price,
181
- 'params': params
182
- }
183
420
  # 使用ccxt创建订单
184
- self.logger.debug(f"Pre Order placed: {order} ")
185
421
  order_result = self.exchange.create_order(
186
422
  **order
187
423
  # symbol=symbol,
@@ -191,13 +427,31 @@ class Exchange:
191
427
  # price=float(adjusted_price),
192
428
  # params=params
193
429
  )
194
- # self.logger.debug(f"{symbol} ++ Order placed rs : {order_result}")
430
+ except ccxt.NetworkError as e:
431
+ # 处理网络相关错误
432
+ retry_count += 1
433
+ self.logger.warning(f"{symbol} : 下单时发生网络错误,正在进行第{retry_count}次重试: {str(e)}")
434
+ time.sleep(0.1) # 重试前等待1秒
435
+ continue
436
+ except ccxt.ExchangeError as e:
437
+ # 处理交易所API相关错误
438
+ retry_count += 1
439
+ self.logger.warning(f"{symbol} : 下单时发生交易所错误,正在进行第{retry_count}次重试: {str(e)}")
440
+ time.sleep(0.1)
441
+ continue
195
442
  except Exception as e:
196
- error_message = f"{symbol} Failed to place order: {e}"
197
- self.logger.error(error_message)
198
- raise Exception(error_message)
199
-
200
- self.logger.debug(f"--------- ++ {symbol} Order placed done! --------")
443
+ # 处理其他未预期的错误
444
+ retry_count += 1
445
+ self.logger.warning(f"{symbol} : 下单时发生未知错误,正在进行第{retry_count}次重试: {str(e)}")
446
+ time.sleep(0.1)
447
+ continue
448
+ if retry_count >= max_retries:
449
+ # 重试次数用完仍未成功设置止损单
450
+ error_message = f"!! {symbol}: 下单时重试次数用完仍未成功设置成功。 "
451
+ self.logger.error(error_message)
452
+ raise Exception(error_message)
453
+ self.logger.debug(f"{symbol} : --------- ++ Order placed done. --------")
454
+ return True
201
455
 
202
456
  def fetch_position(self, symbol):
203
457
  """_summary_
@@ -215,8 +469,8 @@ class Exchange:
215
469
  while retry_count < max_retries:
216
470
  try:
217
471
  position = self.exchange.fetch_position(symbol=symbol)
218
- if position and position['contracts'] > 0:
219
- self.logger.debug(f"{symbol} 有持仓合约数: {position['contracts']}")
472
+ if position :
473
+ # self.logger.debug(f"{symbol} 有持仓合约数: {position['contracts']}")
220
474
  return position
221
475
  return None
222
476
  except Exception as e:
@@ -228,7 +482,69 @@ class Exchange:
228
482
 
229
483
  self.logger.warning(f"{symbol} 检查持仓失败,正在进行第{retry_count}次重试: {str(e)}")
230
484
  time.sleep(0.1) # 重试前等待0.1秒
231
-
485
+
486
+ def fetch_positions(self):
487
+ """_summary_
488
+ Returns:
489
+ _type_: _description_
490
+ """
491
+ max_retries = 3
492
+ retry_count = 0
493
+
494
+ while retry_count < max_retries:
495
+ try:
496
+ positions = self.exchange.fetch_positions()
497
+ return positions
498
+ except Exception as e:
499
+ retry_count += 1
500
+ if retry_count == max_retries:
501
+ error_message = f"!! 获取持仓列表失败(重试{retry_count}次): {str(e)}"
502
+ self.logger.error(error_message)
503
+ raise Exception(error_message)
504
+
505
+ self.logger.warning(f"获取持仓列表失败,正在进行第{retry_count}次重试: {str(e)}")
506
+ time.sleep(0.1) # 重试前等待0.1秒
507
+
508
+ def fetch_open_orders(self,symbol,params={}):
509
+ max_retries = 3
510
+ retry_count = 0
511
+
512
+ while retry_count < max_retries:
513
+ try:
514
+ orders = self.exchange.fetch_open_orders(symbol=symbol,params=params)
515
+ return orders
516
+
517
+ except Exception as e:
518
+ retry_count += 1
519
+ if retry_count == max_retries:
520
+ error_message = f"{symbol} : fetching open orders(retry {retry_count} times): {str(e)}"
521
+ self.logger.error(error_message)
522
+ raise Exception(error_message)
523
+
524
+ self.logger.warning(f"{symbol} : Error fetching open orders: {str(e)}")
525
+ time.sleep(0.1) # 重试前等待0.1秒
526
+ def get_market_price(self, symbol) -> Decimal:
527
+ """
528
+ 获取最新价格
529
+ Args:
530
+ symbol: 交易对
531
+ """
532
+ max_retries = 3
533
+ retry_count = 0
534
+
535
+ while retry_count < max_retries:
536
+ try:
537
+ ticker = self.exchange.fetch_ticker(symbol)
538
+ if ticker and 'last' in ticker:
539
+ return OPTools.toDecimal(ticker['last'])
540
+ else:
541
+ raise Exception(f"{symbol} : Unexpected response structure or missing 'last' price")
542
+ except Exception as e:
543
+ retry_count += 1
544
+ if retry_count == max_retries:
545
+ error_message = f"{symbol} 获取最新价格失败(重试{retry_count}次): {str(e)}"
546
+ self.logger.error(error_message)
547
+ raise Exception(error_message)
232
548
 
233
549
  def get_historical_klines(self, symbol, bar='15m', limit=300, after:str=None, params={}):
234
550
  """
core/smc/SMCLiquidity.py CHANGED
@@ -1,7 +1,209 @@
1
1
  import logging
2
+ from re import S
3
+ import pandas as pd
4
+ from core.smc.SMCStruct import SMCStruct
5
+ from pandas.core.strings.accessor import F
6
+ from pandas.io.parquet import catch_warnings
7
+
8
+ class SMCLiquidity(SMCStruct):
9
+ EQUAL_HIGH_COL = "equal_high"
10
+ EQUAL_LOW_COL = "equal_low"
11
+ LIQU_HIGH_COL = "liqu_high"
12
+ LIQU_LOW_COL = "liqu_low"
13
+ EQUAL_HIGH_INDEX_KEY = "equal_high_index"
14
+ EQUAL_LOW_INDEX_KEY = "equal_low_index"
15
+ HAS_EQ_KEY = "has_EQ"
16
+ LIQU_HIGH_DIFF_COL = "liqu_high_diff"
17
+ LIQU_LOW_DIFF_COL = "liqu_low_diff"
18
+
2
19
 
3
- class SMCLiquidity:
4
20
  def __init__(self):
21
+ super().__init__()
5
22
  self.logger = logging.getLogger(__name__)
6
-
7
-
23
+
24
+
25
+ def _identify_liquidity_pivots(self, data, pivot_length=1):
26
+ """
27
+ 识别流动性的高点和低点
28
+ """
29
+
30
+ df = data.copy()
31
+
32
+ # 识别高点
33
+ df[self.LIQU_HIGH_COL] = 0
34
+ for i in range(pivot_length, len(df) - pivot_length):
35
+ if df[self.HIGH_COL].iloc[i] == max(df[self.HIGH_COL].iloc[i-pivot_length:i+pivot_length+1]):
36
+ df.loc[df.index[i], self.LIQU_HIGH_COL] = df[self.HIGH_COL].iloc[i]
37
+
38
+ # 识别低点
39
+ df[self.LIQU_LOW_COL] = 0
40
+ for i in range(pivot_length, len(df) - pivot_length):
41
+
42
+ if df[self.LOW_COL].iloc[i] == min(df[self.LOW_COL].iloc[i-pivot_length:i+pivot_length+1]):
43
+ df.loc[df.index[i], self.LIQU_LOW_COL] = df[self.LOW_COL].iloc[i]
44
+
45
+
46
+
47
+ return df
48
+
49
+ def find_EQH_EQL(self, data, trend, end_idx=-1, atr_offset=0.1) -> dict:
50
+ """_summary_
51
+ 识别等高等低流动性
52
+ Args:
53
+ data (_type_): _description_
54
+ trend (_type_): _description_
55
+ end_idx (int, optional): _description_. Defaults to -1.
56
+ atr_offset (float, optional): _description_. Defaults to 0.1.
57
+
58
+ Returns:
59
+ dict: _description_
60
+ """
61
+
62
+ df = data.copy() if end_idx == -1 else data.copy().iloc[:end_idx+1]
63
+
64
+ check_columns = [self.LIQU_HIGH_COL, self.LIQU_LOW_COL]
65
+
66
+ try:
67
+ self.check_columns(df, check_columns)
68
+ except ValueError as e:
69
+ self.logger.warning(f"DataFrame must contain columns {check_columns} : {str(e)}")
70
+ df = self._identify_liquidity_pivots(df)
71
+
72
+ df = df[(df[self.LIQU_HIGH_COL] > 0) | (df[self.LIQU_LOW_COL] > 0)]
73
+ # 初始化结果列
74
+ df[self.EQUAL_HIGH_COL] = 0
75
+ df[self.EQUAL_LOW_COL] = 0
76
+ df[self.ATR_COL] = self.calculate_atr(df)
77
+ # 跟踪前一个高点和低点
78
+ previous_high = None
79
+ previous_high_index = None
80
+ previous_high_pos = -1
81
+ previous_low = None
82
+ previous_low_index = None
83
+ previous_low_pos = -1
84
+ for i in range(len(df)-1, -1, -1):
85
+
86
+ offset = self.toDecimal(df[self.ATR_COL].iloc[i] * atr_offset)
87
+
88
+ if trend == self.BULLISH_TREND:
89
+ current_high = df[self.LIQU_HIGH_COL].iloc[i]
90
+ if current_high == 0:
91
+ continue
92
+
93
+ if previous_high is None:
94
+ previous_high = current_high
95
+ previous_high_index = df.index[i]
96
+ previous_high_pos = i
97
+ continue
98
+
99
+ max_val = max(current_high, previous_high)
100
+ min_val = min(current_high, previous_high)
101
+
102
+
103
+ if abs(max_val - min_val) <= offset: # EQH|EQL
104
+
105
+ df.loc[df.index[i], self.EQUAL_HIGH_COL] = previous_high_index
106
+ df.loc[df.index[previous_high_pos], self.EQUAL_HIGH_COL] = previous_high_index
107
+
108
+ else:
109
+ # 倒序遍历,等高线被高点破坏,则更新等高点位置
110
+ if current_high > previous_high:
111
+ previous_high = current_high
112
+ previous_high_index = df.index[i]
113
+ previous_high_pos = i
114
+
115
+
116
+
117
+ else:
118
+ current_low = df[self.LIQU_LOW_COL].iloc[i]
119
+ if current_low == 0:
120
+ continue
121
+
122
+ # current_low = df[self.EQUAL_LOW_COL].iloc[i]
123
+ if previous_low is None:
124
+ previous_low = current_low
125
+ previous_low_index = df.index[i]
126
+ previous_low_pos = i
127
+ continue
128
+
129
+ max_val = max(current_low, previous_low)
130
+ min_val = min(current_low, previous_low)
131
+
132
+
133
+
134
+
135
+ if abs(max_val - min_val) <= offset: # EQH|EQL
136
+
137
+ df.loc[df.index[i], self.EQUAL_LOW_COL] = previous_low_index
138
+ df.loc[df.index[previous_low_pos], self.EQUAL_LOW_COL] = previous_low_index
139
+
140
+ else:
141
+ # 倒序遍历,等高线被高点破坏,则更新等高点位置
142
+ if current_low < previous_low:
143
+ previous_low = current_low
144
+ previous_low_index = df.index[i]
145
+ previous_low_pos = i
146
+
147
+ # 筛选有效结构且在prd范围内的数据
148
+ last_EQ = {
149
+
150
+ }
151
+ if trend == self.BULLISH_TREND :
152
+ mask = df[self.EQUAL_HIGH_COL] > 0
153
+ valid_EQH_df = df[ mask ]
154
+ if not valid_EQH_df.empty:
155
+ last_EQ[self.HAS_EQ_KEY] = True
156
+ last_EQ[self.EQUAL_HIGH_COL] = valid_EQH_df.iloc[-1][self.LIQU_HIGH_COL]
157
+ last_EQ[self.EQUAL_HIGH_INDEX_KEY] = valid_EQH_df.iloc[-1][self.EQUAL_HIGH_COL]
158
+ else:
159
+ mask = df[self.EQUAL_LOW_COL] > 0
160
+ valid_EQL_df = df[ mask ]
161
+ if not valid_EQL_df.empty:
162
+ last_EQ[self.HAS_EQ_KEY] = True
163
+ last_EQ[self.EQUAL_LOW_COL] = valid_EQL_df.iloc[-1][self.LIQU_LOW_COL]
164
+ last_EQ[self.EQUAL_LOW_INDEX_KEY] = valid_EQL_df.iloc[-1][self.EQUAL_LOW_COL]
165
+
166
+ return last_EQ
167
+
168
+ def identify_dynamic_trendlines(self, data, trend, start_idx=-1, end_idx=-1, ratio=0.8) -> tuple:
169
+ """
170
+ 识别动态趋势线或隧道
171
+ Args:
172
+ data (pd.DataFrame): _description_
173
+
174
+ Returns:
175
+ pd.DataFrame: _description_
176
+ """
177
+
178
+ df = data.copy() if start_idx == -1 or end_idx == -1 else data.copy().iloc[start_idx-1:end_idx+2] #考虑poivt值,前后各增加一个
179
+
180
+ check_columns = [self.LIQU_HIGH_COL]
181
+
182
+ try:
183
+ self.check_columns(df, check_columns)
184
+ except ValueError as e:
185
+ self.logger.warning(f"DataFrame must contain columns {check_columns} : {str(e)}")
186
+ df = self._identify_liquidity_pivots(df)
187
+ diff_ratio = 0.0
188
+ if trend == self.BEARISH_TREND:
189
+ # 判断Bearish趋势是高点不断升高,
190
+ liqu_bear_df = df[df[self.LIQU_HIGH_COL] > 0]
191
+ liqu_bear_df[self.LIQU_HIGH_DIFF_COL] = liqu_bear_df[self.LIQU_HIGH_COL].diff()
192
+ # self.logger.info(f"dynamic_trendlines:\n {liqu_bear_df[[self.TIMESTAMP_COL,self.LIQU_HIGH_COL,self.LIQU_HIGH_DIFF_COL]]}")
193
+ diff_ratio = self.toDecimal(liqu_bear_df[self.LIQU_HIGH_DIFF_COL].dropna().lt(0).mean(),2)
194
+ if diff_ratio >= ratio:
195
+ return diff_ratio,True
196
+ else:
197
+ # Bullish趋势是低点不断降低
198
+ liqu_bullish_df = df[df[self.LIQU_LOW_COL] > 0]
199
+ liqu_bullish_df[self.LIQU_LOW_DIFF_COL] = liqu_bullish_df[self.LIQU_LOW_COL].diff()
200
+ # self.logger.info(f"dynamic_trendlines:\n {liqu_bullish_df[[self.TIMESTAMP_COL,self.LIQU_LOW_COL,self.LIQU_LOW_DIFF_COL]]}")
201
+ diff_ratio = self.toDecimal(liqu_bullish_df[self.LIQU_LOW_DIFF_COL].dropna().gt(0).mean(),2)
202
+ if diff_ratio >= ratio:
203
+ return diff_ratio,True
204
+
205
+ return diff_ratio,False
206
+
207
+
208
+
209
+
core/smc/SMCOrderBlock.py CHANGED
@@ -54,7 +54,7 @@ class SMCOrderBlock(SMCStruct):
54
54
  ob_df = ob_df[ob_df[self.OB_DIRECTION_COL] == direction]
55
55
 
56
56
  # 检查OB是否被平衡过
57
-
57
+ ob_df = ob_df.copy()
58
58
  ob_df.loc[:, self.OB_WAS_CROSSED] = ob_df.apply(
59
59
  lambda row: any(
60
60
  df.loc[row.name + 1 :, self.LOW_COL] <= row[self.OB_LOW_COL]
@@ -64,25 +64,7 @@ class SMCOrderBlock(SMCStruct):
64
64
  axis=1,
65
65
  )
66
66
 
67
- ob_df = ob_df[~ob_df[self.OB_WAS_CROSSED]]
68
-
69
- # # 过滤出有效的OB
70
- # if is_valid is not None and not ob_df.empty:
71
- # valid_mask = []
72
- # for _, row in ob_df.iterrows():
73
- # start_idx = row[self.OB_START_INDEX_COL] + 1
74
- # if start_idx >= len(df):
75
- # valid = False
76
- # else:
77
- # future_data = df.loc[start_idx:]
78
- # if row[self.OB_DIRECTION_COL] == "Bullish":
79
- # valid = row[self.OB_LOW_COL] < future_data[self.LOW_COL].min()
80
- # else: # Bearish
81
- # valid = row[self.OB_HIGH_COL] > future_data[self.HIGH_COL].max()
82
-
83
- # valid_mask.append(valid if is_valid else not valid)
84
-
85
- # ob_df = ob_df[pd.Series(valid_mask, index=ob_df.index)]
67
+ ob_df = ob_df[~ob_df[self.OB_WAS_CROSSED]]
86
68
 
87
69
  if if_combine:
88
70
  # 合并OB
@@ -91,7 +73,7 @@ class SMCOrderBlock(SMCStruct):
91
73
  return ob_df
92
74
 
93
75
  def build_struct_for_ob(
94
- self, df, window=20, is_struct_body_break=True, atr_multiplier=0.6
76
+ self, df, is_struct_body_break=True, atr_multiplier=0.6
95
77
  ):
96
78
  """
97
79
  构建结构并检测Order Block
@@ -107,7 +89,7 @@ class SMCOrderBlock(SMCStruct):
107
89
  处理后的数据框,包含结构和Order Block相关列
108
90
  """
109
91
  # 首先构建基础结构
110
- df = self.build_struct(df, window, is_struct_body_break)
92
+ df = self.build_struct(df, is_struct_body_break)
111
93
 
112
94
  check_columns = [self.HIGH_COL, self.LOW_COL, self.CLOSE_COL]
113
95
  self.check_columns(df, check_columns)
@@ -256,22 +238,30 @@ class SMCOrderBlock(SMCStruct):
256
238
  # df.at[i, self.OB_START_TS_COL] = df.loc[index, self.TIMESTAMP_COL]
257
239
  df.at[index, self.OB_ATR] = atr
258
240
 
259
- def get_last_ob(self, df, prd=-1):
241
+ def get_lastest_OB(self, data, trend, start_index=-1):
260
242
  """
261
243
  获取最新的Order Block
262
244
 
263
245
  Args:
264
- df: 包含OB信息的数据框
265
- prd: 回溯周期,-1表示全部
246
+ df: 数据框
247
+ trend: 趋势,"Bullish" 或 "Bearish"
266
248
 
267
249
  Returns:
268
250
  最新的Order Block信息或None
269
251
  """
270
252
  # 获取prd范围内的数据
271
- start_idx = max(0, len(df) - 1 - prd) if prd > 0 else 0
253
+ df = (
254
+ data.copy()
255
+ if start_index == -1
256
+ else data.copy().iloc[start_index :]
257
+ )
258
+
259
+ # 检查数据中是否包含必要的列
260
+ check_columns = [self.OB_DIRECTION_COL]
261
+ self.check_columns(df, check_columns)
272
262
 
273
263
  # 筛选有效OB且在prd范围内的数据
274
- mask = df[self.OB_DIRECTION_COL].notna() & (df.index >= start_idx)
264
+ mask = df[self.OB_DIRECTION_COL] == trend
275
265
  valid_obs = df[mask]
276
266
 
277
267
  if not valid_obs.empty:
@@ -283,6 +273,8 @@ class SMCOrderBlock(SMCStruct):
283
273
  self.OB_MID_COL: last_ob[self.OB_MID_COL],
284
274
  self.OB_VOLUME_COL: last_ob[self.OB_VOLUME_COL],
285
275
  self.OB_DIRECTION_COL: last_ob[self.OB_DIRECTION_COL],
276
+ self.OB_ATR: last_ob[self.OB_ATR],
277
+ self.OB_WAS_CROSSED: last_ob[self.OB_WAS_CROSSED],
286
278
  }
287
279
 
288
280
  return None
core/smc/SMCPDArray.py CHANGED
@@ -36,8 +36,6 @@ class SMCPDArray(SMCFVG,SMCOrderBlock):
36
36
  if start_index == -1
37
37
  else struct.copy().iloc[max(0, start_index - 1) :]
38
38
  )
39
- # check_columns = [self.HIGH_COL, self.LOW_COL, self.CLOSE_COL]
40
- # self.check_columns(df, check_columns)
41
39
 
42
40
  df_FVGs = self.find_FVGs(df, side)
43
41
  # self.logger.info(f"fvgs:\n{df_FVGs[['timestamp', self.FVG_SIDE, self.FVG_TOP, self.FVG_BOT, self.FVG_WAS_BALANCED]]}")
core/smc/SMCStruct.py CHANGED
@@ -4,9 +4,12 @@ from core.utils.OPTools import OPTools
4
4
  from core.smc.SMCBase import SMCBase
5
5
 
6
6
  class SMCStruct(SMCBase):
7
+ BULLISH_TREND = 'Bullish'
8
+ BEARISH_TREND = 'Bearish'
7
9
  STRUCT_COL = "struct"
8
10
  STRUCT_HIGH_COL = "struct_high"
9
11
  STRUCT_LOW_COL = "struct_low"
12
+ STRUCT_MID_COL = "struct_mid"
10
13
  STRUCT_HIGH_INDEX_COL = "struct_high_index"
11
14
  STRUCT_LOW_INDEX_COL = "struct_low_index"
12
15
  STRUCT_DIRECTION_COL = "struct_direction"
@@ -16,10 +19,9 @@ class SMCStruct(SMCBase):
16
19
  def __init__(self):
17
20
  super().__init__()
18
21
  self.logger = logging.getLogger(__name__)
19
-
20
-
22
+
21
23
 
22
- def build_struct(self, data, window=10, is_struct_body_break=True):
24
+ def build_struct(self, data, is_struct_body_break=True):
23
25
  """处理价格结构,识别高低点突破和结构方向
24
26
 
25
27
  Args:
@@ -30,7 +32,8 @@ class SMCStruct(SMCBase):
30
32
  Returns:
31
33
  处理后的数据框,包含结构相关列
32
34
  """
33
- df = data.copy()
35
+ # 复制数据并去掉最后一条记录,因为最后一条记录不是完成状态的K线
36
+ df = data.copy().iloc[:-1]
34
37
  check_columns = [self.HIGH_COL, self.LOW_COL, self.CLOSE_COL]
35
38
  self.check_columns(df, check_columns)
36
39
 
@@ -46,7 +49,7 @@ class SMCStruct(SMCBase):
46
49
  self.STRUCT_LOW_COL: self.toDecimal('0.0'), # 结构低点价格初始化为0
47
50
  self.STRUCT_HIGH_INDEX_COL: 0, # 结构高点索引初始化为0
48
51
  self.STRUCT_LOW_INDEX_COL: 0, # 结构低点索引初始化为0
49
- self.STRUCT_DIRECTION_COL: 0 # 结构方向初始化为0
52
+ self.STRUCT_DIRECTION_COL: None # 结构方向初始化为0
50
53
  }
51
54
 
52
55
  # 为每个结构列赋默认值
@@ -103,7 +106,7 @@ class SMCStruct(SMCBase):
103
106
  if is_low_broken:
104
107
  # 处理低点突破
105
108
  structure = self._handle_structure_break(
106
- df, i, window, structure,
109
+ df, i, structure,
107
110
  break_type=self.LOW_COL,
108
111
  struct_type='BOS' if structure['direction'] == 1 else 'CHOCH'
109
112
  )
@@ -111,7 +114,7 @@ class SMCStruct(SMCBase):
111
114
  elif is_high_broken:
112
115
  # 处理高点突破
113
116
  structure = self._handle_structure_break(
114
- df, i, window, structure,
117
+ df, i, structure,
115
118
  break_type=self.HIGH_COL,
116
119
  struct_type='BOS' if structure['direction'] == 2 else 'CHOCH'
117
120
  )
@@ -128,7 +131,7 @@ class SMCStruct(SMCBase):
128
131
 
129
132
  return df
130
133
 
131
- def _get_structure_extreme_bar(self, df, bar_index, struct_index, lookback=10, mode='high'):
134
+ def _get_structure_extreme_bar(self, df, bar_index, struct_index, mode='high'):
132
135
  """
133
136
  获取结构最高点或最低点
134
137
  :param df: DataFrame数据
@@ -199,7 +202,7 @@ class SMCStruct(SMCBase):
199
202
 
200
203
  return basic_break or direction_break
201
204
 
202
- def _handle_structure_break(self, df, i, window, structure, break_type, struct_type):
205
+ def _handle_structure_break(self, df, i, structure, break_type, struct_type):
203
206
  """处理结构突破"""
204
207
  is_high_break = break_type == self.HIGH_COL
205
208
 
@@ -207,7 +210,7 @@ class SMCStruct(SMCBase):
207
210
 
208
211
  # 获取新的极值点
209
212
  extreme_idx = self._get_structure_extreme_bar(
210
- df, i, struct_start, window,
213
+ df, i, struct_start,
211
214
  mode=self.LOW_COL if is_high_break else self.HIGH_COL
212
215
  )
213
216
 
@@ -231,8 +234,9 @@ class SMCStruct(SMCBase):
231
234
  })
232
235
 
233
236
  # 更新DataFrame结构信息
234
- df.at[i, 'struct_direction'] = new_structure['direction']
235
- df.at[i, 'struct'] = f"{'Bullish' if is_high_break else 'Bearish'}_{struct_type}"
237
+ # df.at[i, self.STRUCT_DIRECTION_COL] = new_structure['direction']
238
+ df.at[i, self.STRUCT_DIRECTION_COL] = self.BULLISH_TREND if is_high_break else self.BEARISH_TREND
239
+ df.at[i, self.STRUCT_COL] = f"{self.BULLISH_TREND if is_high_break else self.BEARISH_TREND}_{struct_type}"
236
240
 
237
241
  return new_structure
238
242
 
@@ -261,18 +265,22 @@ class SMCStruct(SMCBase):
261
265
  df.at[i, self.STRUCT_HIGH_INDEX_COL] = structure[self.HIGH_START_COL]
262
266
  df.at[i, self.STRUCT_LOW_INDEX_COL] = structure[self.LOW_START_COL]
263
267
 
264
- def get_last_struct(self, df, prd=-1):
268
+ def get_last_struct(self, df):
265
269
  """
266
270
  获取最新的结构
267
271
  """
268
- data = self.build_struct(df=df, window=10)
269
- # 筛选出有效的结构
272
+ check_columns = [self.STRUCT_COL]
273
+ # if not self.check_columns(df, check_columns):
274
+ try :
275
+ self.check_columns(df, check_columns)
276
+ except ValueError as e:
277
+ df = self.build_struct(df)
278
+
279
+ data = df.copy()
270
280
 
271
- # 获取prd范围内的数据
272
- start_idx = max(0, len(data) - 1 - prd)
273
281
  # 筛选有效结构且在prd范围内的数据
274
282
  last_struct = None
275
- mask = data[self.STRUCT_COL].notna() & (data.index >= start_idx) if prd > 0 else data[self.STRUCT_COL].notna()
283
+ mask = data[self.STRUCT_COL].notna()
276
284
  valid_structs = data[ mask ]
277
285
  if not valid_structs.empty:
278
286
  # 获取最近的结构
@@ -281,6 +289,7 @@ class SMCStruct(SMCBase):
281
289
  self.STRUCT_COL: last_struct[self.STRUCT_COL],
282
290
  self.STRUCT_HIGH_COL: last_struct[self.STRUCT_HIGH_COL],
283
291
  self.STRUCT_LOW_COL: last_struct[self.STRUCT_LOW_COL],
292
+ self.STRUCT_MID_COL: (last_struct[self.STRUCT_HIGH_COL] + last_struct[self.STRUCT_LOW_COL]) / 2,
284
293
  self.STRUCT_HIGH_INDEX_COL: last_struct[self.STRUCT_HIGH_INDEX_COL],
285
294
  self.STRUCT_LOW_INDEX_COL: last_struct[self.STRUCT_LOW_INDEX_COL],
286
295
  self.STRUCT_DIRECTION_COL: last_struct[self.STRUCT_DIRECTION_COL]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: openfund-core
3
- Version: 1.0.1
3
+ Version: 1.0.7
4
4
  Summary: Openfund-core.
5
5
  Requires-Python: >=3.9,<4.0
6
6
  Classifier: Programming Language :: Python :: 3
@@ -0,0 +1,15 @@
1
+ core/Exchange.py,sha256=MDhV71jmWyM1IGaUI-iSfZqrUG9RNOpSWqm0jNV_GXU,23173
2
+ core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ core/main.py,sha256=E-VZzem7-0_J6EmOo9blLPokc5MRcgjqCbqAvbkPnWI,630
4
+ core/smc/SMCBase.py,sha256=epRC5bWDymx7ZMIhn_bVJRjvBHItt6BCnYASO2fhSDg,4302
5
+ core/smc/SMCFVG.py,sha256=QtqlW1oooYVA7CG5ld5X0Q5twX1XCELO118IlMUhX6M,2974
6
+ core/smc/SMCLiquidity.py,sha256=EkfyBVXmAiRihof6w3YJvRHlbwUtqag8S6aVrV_XWvc,8100
7
+ core/smc/SMCOrderBlock.py,sha256=Il5JKmVER2vT6AKZLo0mD4wRqV_Op9IBK3jB1SfgTqY,9894
8
+ core/smc/SMCPDArray.py,sha256=Vn_nTBLaIhrBhxe_hX3Iycn0gY0tmYd_qaqNalztfmA,2841
9
+ core/smc/SMCStruct.py,sha256=BIp5CIipLtOq-Z7aXhHK8KwjdPZzg2_7TVANirO-HlY,12337
10
+ core/smc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ core/utils/OPTools.py,sha256=tJ1Jq_Caab6OWaX12xn4_g9ryf98Rm5I1zsJEEU8NIQ,1002
12
+ openfund_core-1.0.7.dist-info/METADATA,sha256=tLNUyBlVeEkAKu1QyHltTgtvMmGUH_OUuOgjTNmGq3I,1953
13
+ openfund_core-1.0.7.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
14
+ openfund_core-1.0.7.dist-info/entry_points.txt,sha256=g8GUw3cyKFtcG5VWs8geU5VBLqiWr59GElqERuH8zD0,48
15
+ openfund_core-1.0.7.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.0.1
2
+ Generator: poetry-core 2.1.2
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,15 +0,0 @@
1
- core/Exchange.py,sha256=_nAQy0frzaY4LHWwmDvJPmjX2BNsBz-fAPi1cPTpPRc,10799
2
- core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- core/main.py,sha256=E-VZzem7-0_J6EmOo9blLPokc5MRcgjqCbqAvbkPnWI,630
4
- core/smc/SMCBase.py,sha256=epRC5bWDymx7ZMIhn_bVJRjvBHItt6BCnYASO2fhSDg,4302
5
- core/smc/SMCFVG.py,sha256=QtqlW1oooYVA7CG5ld5X0Q5twX1XCELO118IlMUhX6M,2974
6
- core/smc/SMCLiquidity.py,sha256=lZt2IQk3TWaT-nA7he57dUxPdLEWW61jRZWLAzOTat0,119
7
- core/smc/SMCOrderBlock.py,sha256=hdY9X_9xrlbUGQGyXjzf-Un-XWgf3QihSWVgzefyP18,10378
8
- core/smc/SMCPDArray.py,sha256=yf-ZVabQhe1xcYz2gsETHpTJCSsMiUyPVQ6DdAiUd4U,2961
9
- core/smc/SMCStruct.py,sha256=m_LsVlwGXZjDLovSO1Swvvx5JLCsJ5VYwvP-CAM3KzY,11926
10
- core/smc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
- core/utils/OPTools.py,sha256=tJ1Jq_Caab6OWaX12xn4_g9ryf98Rm5I1zsJEEU8NIQ,1002
12
- openfund_core-1.0.1.dist-info/METADATA,sha256=9PvQQtpzuz4LG5hQFaXJ83vpOaz01DTz37NRH9Fkf0E,1953
13
- openfund_core-1.0.1.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
14
- openfund_core-1.0.1.dist-info/entry_points.txt,sha256=g8GUw3cyKFtcG5VWs8geU5VBLqiWr59GElqERuH8zD0,48
15
- openfund_core-1.0.1.dist-info/RECORD,,