openfund-core 1.0.1__tar.gz → 1.0.5__tar.gz

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,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: openfund-core
3
- Version: 1.0.1
3
+ Version: 1.0.5
4
4
  Summary: Openfund-core.
5
5
  Requires-Python: >=3.9,<4.0
6
6
  Classifier: Programming Language :: Python :: 3
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "openfund-core"
3
- version = "1.0.1"
3
+ version = "1.0.5"
4
4
  description = "Openfund-core."
5
5
  authors = []
6
6
  readme = "README.md"
@@ -0,0 +1,533 @@
1
+ import logging
2
+ import time
3
+ import ccxt
4
+ import pandas as pd
5
+
6
+
7
+ from decimal import Decimal
8
+ from core.utils.OPTools import OPTools
9
+ from ccxt.base.exchange import ConstructorArgs
10
+
11
+
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'
22
+ def __init__(self, config:ConstructorArgs, exchangeKey:str = "okx",) :
23
+ # 配置交易所
24
+ self.exchange = getattr(ccxt, exchangeKey)(config)
25
+ self.logger = logging.getLogger(__name__)
26
+
27
+
28
+
29
+ def getMarket(self, symbol:str):
30
+ # 配置交易对
31
+ self.exchange.load_markets()
32
+
33
+ return self.exchange.market(symbol)
34
+
35
+ def get_tick_size(self,symbol) -> Decimal:
36
+
37
+ market = self.getMarket(symbol)
38
+ if market and 'precision' in market and 'price' in market['precision']:
39
+ return OPTools.toDecimal(market['precision']['price'])
40
+ else:
41
+ raise ValueError(f"{symbol}: 无法从市场数据中获取价格精度")
42
+
43
+ def amount_to_precision(self,symbol, contract_size):
44
+ return self.exchange.amount_to_precision(symbol, contract_size)
45
+
46
+ def get_position_mode(self):
47
+
48
+ try:
49
+ # 假设获取账户持仓模式的 API
50
+ response = self.exchange.private_get_account_config()
51
+ data = response.get('data', [])
52
+ if data and isinstance(data, list):
53
+ # 取列表的第一个元素(假设它是一个字典),然后获取 'posMode'
54
+ position_mode = data[0].get('posMode', 'single') # 默认值为单向
55
+
56
+ return position_mode
57
+ else:
58
+
59
+ return 'single' # 返回默认值
60
+ except Exception as e:
61
+ error_message = f"Error fetching position mode: {e}"
62
+ self.logger.error(error_message)
63
+ raise Exception(error_message)
64
+
65
+ def set_leverage(self,symbol, leverage, mgnMode='isolated',posSide=None):
66
+ try:
67
+ # 设置杠杆
68
+ params = {
69
+ # 'instId': instId,
70
+ 'leverage': leverage,
71
+ 'marginMode': mgnMode
72
+ }
73
+ if posSide:
74
+ params['side'] = posSide
75
+
76
+ self.exchange.set_leverage(leverage, symbol=symbol, params=params)
77
+ self.logger.info(f"{symbol} Successfully set leverage to {leverage}x")
78
+ except Exception as e:
79
+ error_message = f"{symbol} Error setting leverage: {e}"
80
+ self.logger.error(error_message)
81
+ raise Exception(error_message)
82
+ # 获取价格精度
83
+ def get_precision_length(self,symbol) -> int:
84
+ tick_size = self.get_tick_size(symbol)
85
+ return len(f"{tick_size:.15f}".rstrip('0').split('.')[1]) if '.' in f"{tick_size:.15f}" else 0
86
+
87
+ def format_price(self, symbol, price:Decimal) -> str:
88
+ precision = self.get_precision_length(symbol)
89
+ return f"{price:.{precision}f}"
90
+
91
+ def convert_contract(self, symbol, amount, price:Decimal, direction='cost_to_contract'):
92
+ """
93
+ 进行合约与币的转换
94
+ :param symbol: 交易对符号,如 'BTC/USDT:USDT'
95
+ :param amount: 输入的数量,可以是合约数量或币的数量
96
+ :param direction: 转换方向,'amount_to_contract' 表示从数量转换为合约,'cost_to_contract' 表示从金额转换为合约
97
+ :return: 转换后的数量
98
+ """
99
+
100
+ # 获取合约规模
101
+ market_contractSize = OPTools.toDecimal(self.getMarket(symbol)['contractSize'])
102
+ amount = OPTools.toDecimal(amount)
103
+ if direction == 'amount_to_contract':
104
+ contract_size = amount / market_contractSize
105
+ elif direction == 'cost_to_contract':
106
+ contract_size = amount / price / market_contractSize
107
+ else:
108
+ raise Exception(f"{symbol}:{direction} 是无效的转换方向,请输入 'amount_to_contract' 或 'cost_to_contract'。")
109
+
110
+ return self.amount_to_precision(symbol, contract_size)
111
+
112
+
113
+ def cancel_all_orders(self, symbol):
114
+ max_retries = 3
115
+ retry_count = 0
116
+
117
+ while retry_count < max_retries:
118
+ try:
119
+ # 获取所有未完成订单
120
+ params = {
121
+ # 'instId': instId
122
+ }
123
+ open_orders = self.exchange.fetch_open_orders(symbol=symbol, params=params)
124
+
125
+ # 批量取消所有订单
126
+ if open_orders:
127
+ order_ids = [order['id'] for order in open_orders]
128
+ self.exchange.cancel_orders(order_ids, symbol, params=params)
129
+
130
+ self.logger.debug(f"{symbol}: {order_ids} 挂单取消成功.")
131
+ else:
132
+ self.logger.debug(f"{symbol}: 无挂单.")
133
+ return True
134
+
135
+ except Exception as e:
136
+ retry_count += 1
137
+ if retry_count == max_retries:
138
+ error_message = f"{symbol} 取消挂单失败(重试{retry_count}次): {str(e)}"
139
+ self.logger.error(error_message)
140
+ raise Exception(error_message)
141
+ else:
142
+ self.logger.warning(f"{symbol} 取消挂单失败,正在进行第{retry_count}次重试: {str(e)}")
143
+ time.sleep(0.1) # 重试前等待0.1秒
144
+
145
+ def cancel_all_algo_orders(self, symbol, attachType=None) -> bool:
146
+ """_summary_
147
+
148
+ Args:
149
+ symbol (_type_): _description_
150
+ attachType (_type_, optional): "TP"|"SL". Defaults to None.
151
+ """
152
+
153
+ params = {
154
+ "ordType": "conditional",
155
+ }
156
+ try:
157
+ orders = self.fetch_open_orders(symbol=symbol,params=params)
158
+ except Exception as e:
159
+ error_message = f"!!{symbol} : Error fetching open orders: {e}"
160
+ self.logger.error(error_message)
161
+ raise Exception(error_message)
162
+
163
+
164
+ if len(orders) == 0:
165
+ self.logger.debug(f"{symbol} 未设置策略订单列表。")
166
+ return True
167
+
168
+ algo_ids = []
169
+ if attachType and attachType == 'SL':
170
+ algo_ids = [order['id'] for order in orders if order['stopLossPrice'] and order['stopLossPrice'] > 0.0 ]
171
+ elif attachType and attachType == 'TP':
172
+ algo_ids = [order['id'] for order in orders if order['takeProfitPrice'] and order['takeProfitPrice'] > 0.0]
173
+ else :
174
+ algo_ids = [order['id'] for order in orders ]
175
+
176
+ if len(algo_ids) == 0 :
177
+ self.logger.debug(f"{symbol} 未设置策略订单列表。")
178
+ return True
179
+
180
+ max_retries = 3
181
+ retry_count = 0
182
+
183
+ while retry_count < max_retries:
184
+ try:
185
+ params = {
186
+ "algoId": algo_ids,
187
+ "trigger": 'trigger'
188
+ }
189
+ rs = self.exchange.cancel_orders(ids=algo_ids, symbol=symbol, params=params)
190
+
191
+ return len(rs) > 0
192
+
193
+ except Exception as e:
194
+ retry_count += 1
195
+ if retry_count == max_retries:
196
+ error_message = f"!!{symbol} : Error cancelling order {algo_ids}: {e}"
197
+ self.logger.error(error_message)
198
+ raise Exception(error_message)
199
+
200
+ self.logger.warning(f"{symbol} : Error cancelling order {algo_ids}: {str(e)}")
201
+ time.sleep(0.1) # 重试前等待0.1秒
202
+ def place_algo_orders(self, symbol, position, price: Decimal, order_type, sl_or_tp='SL', params={}) -> bool:
203
+ """
204
+ 下单
205
+ Args:
206
+ symbol: 交易对
207
+ position: 仓位
208
+ price: 下单价格
209
+ order_type: 订单类型
210
+ """
211
+ # 计算下单数量
212
+ amount = abs(position[self.CONTRACTS_KEY])
213
+
214
+ if amount <= 0:
215
+ self.logger.warning(f"{symbol}: amount is 0 for {symbol}")
216
+ return
217
+
218
+ # 止损单逻辑
219
+ adjusted_price = self.format_price(symbol, price)
220
+
221
+ # 默认市价止损,委托价格为-1时,执行市价止损。
222
+ sl_params = {
223
+ **params,
224
+ 'slTriggerPx':adjusted_price ,
225
+ 'slOrdPx':'-1', # 委托价格为-1时,执行市价止损
226
+ # 'slOrdPx' : adjusted_price,
227
+ 'slTriggerPxType':'last',
228
+ 'tdMode':position['marginMode'],
229
+ 'sz': str(amount),
230
+ 'cxlOnClosePos': True,
231
+ 'reduceOnly':True,
232
+ }
233
+
234
+ tp_params = {
235
+ **params,
236
+ 'tpTriggerPx':adjusted_price,
237
+ 'tpOrdPx' : adjusted_price,
238
+ 'tpOrdKind': 'condition',
239
+ 'tpTriggerPxType':'last',
240
+ 'tdMode':position['marginMode'],
241
+ 'sz': str(amount),
242
+ 'cxlOnClosePos': True,
243
+ 'reduceOnly':True
244
+ }
245
+
246
+ order_params = sl_params if sl_or_tp == 'SL' else tp_params
247
+ # order_params.update(params)
248
+
249
+ if order_type == 'limit' and sl_or_tp =='SL':
250
+ order_params['slOrdPx'] = adjusted_price
251
+
252
+ orderSide = self.BUY_SIDE if position[self.SIDE_KEY] == self.SHORT_KEY else self.SELL_SIDE # 和持仓反向相反下单
253
+
254
+ order = {
255
+ 'symbol': symbol,
256
+ 'side': orderSide,
257
+ 'type': order_type,
258
+ 'amount': amount,
259
+ 'price': adjusted_price,
260
+ 'params': order_params
261
+ }
262
+
263
+ max_retries = 3
264
+ retry_count = 0
265
+ self.logger.debug(f"{symbol} : Pre Algo Order placed: {order} ")
266
+ while retry_count < max_retries:
267
+ try:
268
+
269
+ self.exchange.create_order(
270
+ **order
271
+ # symbol=symbol,
272
+ # type=order_type,
273
+ # price=adjusted_price,
274
+ # side=orderSide,
275
+ # amount=amount,
276
+ # params=order_params
277
+ )
278
+
279
+ break
280
+
281
+ except ccxt.NetworkError as e:
282
+ # 处理网络相关错误
283
+ retry_count += 1
284
+ self.logger.warning(f"{symbol} : 设置止盈止损时发生网络错误,正在进行第{retry_count}次重试: {str(e)}")
285
+ time.sleep(0.1) # 重试前等待1秒
286
+ continue
287
+ except ccxt.ExchangeError as e:
288
+ # 处理交易所API相关错误
289
+ retry_count += 1
290
+ self.logger.warning(f"{symbol} : 设置止盈止损单时发生交易所错误,正在进行第{retry_count}次重试: {str(e)}")
291
+ time.sleep(0.1)
292
+ continue
293
+ except Exception as e:
294
+ # 处理其他未预期的错误
295
+ retry_count += 1
296
+ self.logger.warning(f"{symbol} : 设置止盈止损单时发生未知错误,正在进行第{retry_count}次重试: {str(e)}")
297
+ time.sleep(0.1)
298
+ continue
299
+
300
+ if retry_count >= max_retries:
301
+ # 重试次数用完仍未成功设置止损单
302
+ error_message = f"!! {symbol}: 设置止盈止损单时重试次数用完仍未成功设置成功。 "
303
+ self.logger.error(error_message)
304
+ raise Exception(error_message)
305
+ self.logger.debug(f"{symbol} : --------- ++ Order placed done. --------")
306
+ return True
307
+
308
+
309
+ def place_order(self, symbol, price: Decimal, amount_usdt, side, leverage=20, order_type='limit', params={}) -> bool:
310
+ """
311
+ 下单
312
+ Args:
313
+ symbol: 交易对
314
+ price: 下单价格
315
+ amount_usdt: 下单金额
316
+ side: 下单方向
317
+ order_type: 订单类型
318
+ """
319
+ # 格式化价格
320
+ adjusted_price = self.format_price(symbol, price)
321
+
322
+ if amount_usdt <= 0:
323
+ self.logger.warning(f"{symbol}: amount_usdt must be greater than 0")
324
+ return
325
+
326
+ pos_side = self.LONG_KEY if side == self.BUY_SIDE else self.SHORT_KEY
327
+
328
+ # 设置杠杆
329
+ self.set_leverage(symbol=symbol, leverage=leverage, mgnMode='isolated',posSide=pos_side)
330
+ # 20250220 SWAP类型计算合约数量
331
+ contract_size = self.convert_contract(symbol=symbol, price = OPTools.toDecimal(adjusted_price) ,amount=amount_usdt)
332
+
333
+ order_params = {
334
+ **params,
335
+ "tdMode": 'isolated',
336
+ "side": side,
337
+ "ordType": order_type,
338
+ "sz": contract_size,
339
+ "px": adjusted_price
340
+ }
341
+
342
+ # # 模拟盘(demo_trading)需要 posSide
343
+ # if self.is_demo_trading == 1 :
344
+ # params["posSide"] = pos_side
345
+
346
+ order = {
347
+ 'symbol': symbol,
348
+ 'side': side,
349
+ 'type': order_type,
350
+ 'amount': contract_size,
351
+ 'price': adjusted_price,
352
+ 'params': order_params
353
+ }
354
+
355
+ max_retries = 3
356
+ retry_count = 0
357
+
358
+ while retry_count < max_retries:
359
+ try:
360
+ # 使用ccxt创建订单
361
+ self.logger.debug(f"{symbol} : Pre Order placed: {order} ")
362
+ order_result = self.exchange.create_order(
363
+ **order
364
+ # symbol=symbol,
365
+ # type='limit',
366
+ # side=side,
367
+ # amount=amount_usdt,
368
+ # price=float(adjusted_price),
369
+ # params=params
370
+ )
371
+ except ccxt.NetworkError as e:
372
+ # 处理网络相关错误
373
+ retry_count += 1
374
+ self.logger.warning(f"{symbol} : 设置下单时发生网络错误,正在进行第{retry_count}次重试: {str(e)}")
375
+ time.sleep(0.1) # 重试前等待1秒
376
+ continue
377
+ except ccxt.ExchangeError as e:
378
+ # 处理交易所API相关错误
379
+ retry_count += 1
380
+ self.logger.warning(f"{symbol} : 设置下单时发生交易所错误,正在进行第{retry_count}次重试: {str(e)}")
381
+ time.sleep(0.1)
382
+ continue
383
+ except Exception as e:
384
+ # 处理其他未预期的错误
385
+ retry_count += 1
386
+ self.logger.warning(f"{symbol} : 设置下单时发生未知错误,正在进行第{retry_count}次重试: {str(e)}")
387
+ time.sleep(0.1)
388
+ continue
389
+ if retry_count >= max_retries:
390
+ # 重试次数用完仍未成功设置止损单
391
+ error_message = f"!! {symbol}: 设置止盈止损单时重试次数用完仍未成功设置成功。 "
392
+ self.logger.error(error_message)
393
+ raise Exception(error_message)
394
+ self.logger.debug(f"{symbol} : --------- ++ Order placed done. --------")
395
+ return True
396
+
397
+ def fetch_position(self, symbol):
398
+ """_summary_
399
+
400
+ Args:
401
+ symbol (_type_): _description_
402
+
403
+ Returns:
404
+ _type_: _description_
405
+ """
406
+
407
+ max_retries = 3
408
+ retry_count = 0
409
+
410
+ while retry_count < max_retries:
411
+ try:
412
+ position = self.exchange.fetch_position(symbol=symbol)
413
+ if position :
414
+ # self.logger.debug(f"{symbol} 有持仓合约数: {position['contracts']}")
415
+ return position
416
+ return None
417
+ except Exception as e:
418
+ retry_count += 1
419
+ if retry_count == max_retries:
420
+ error_message = f"!!{symbol} 获取持仓失败(重试{retry_count}次): {str(e)}"
421
+ self.logger.error(error_message)
422
+ raise Exception(error_message)
423
+
424
+ self.logger.warning(f"{symbol} 检查持仓失败,正在进行第{retry_count}次重试: {str(e)}")
425
+ time.sleep(0.1) # 重试前等待0.1秒
426
+
427
+ def fetch_positions(self):
428
+ """_summary_
429
+ Returns:
430
+ _type_: _description_
431
+ """
432
+ max_retries = 3
433
+ retry_count = 0
434
+
435
+ while retry_count < max_retries:
436
+ try:
437
+ positions = self.exchange.fetch_positions()
438
+ return positions
439
+ except Exception as e:
440
+ retry_count += 1
441
+ if retry_count == max_retries:
442
+ error_message = f"!! 获取持仓列表失败(重试{retry_count}次): {str(e)}"
443
+ self.logger.error(error_message)
444
+ raise Exception(error_message)
445
+
446
+ self.logger.warning(f"获取持仓列表失败,正在进行第{retry_count}次重试: {str(e)}")
447
+ time.sleep(0.1) # 重试前等待0.1秒
448
+
449
+ def fetch_open_orders(self,symbol,params={}):
450
+ max_retries = 3
451
+ retry_count = 0
452
+
453
+ while retry_count < max_retries:
454
+ try:
455
+ orders = self.exchange.fetch_open_orders(symbol=symbol,params=params)
456
+ return orders
457
+
458
+ except Exception as e:
459
+ retry_count += 1
460
+ if retry_count == max_retries:
461
+ error_message = f"{symbol} : fetching open orders(retry {retry_count} times): {str(e)}"
462
+ self.logger.error(error_message)
463
+ raise Exception(error_message)
464
+
465
+ self.logger.warning(f"{symbol} : Error fetching open orders: {str(e)}")
466
+ time.sleep(0.1) # 重试前等待0.1秒
467
+ def get_market_price(self, symbol) -> Decimal:
468
+ """
469
+ 获取最新价格
470
+ Args:
471
+ symbol: 交易对
472
+ """
473
+ max_retries = 3
474
+ retry_count = 0
475
+
476
+ while retry_count < max_retries:
477
+ try:
478
+ ticker = self.exchange.fetch_ticker(symbol)
479
+ if ticker and 'last' in ticker:
480
+ return OPTools.toDecimal(ticker['last'])
481
+ else:
482
+ raise Exception(f"{symbol} : Unexpected response structure or missing 'last' price")
483
+ except Exception as e:
484
+ retry_count += 1
485
+ if retry_count == max_retries:
486
+ error_message = f"{symbol} 获取最新价格失败(重试{retry_count}次): {str(e)}"
487
+ self.logger.error(error_message)
488
+ raise Exception(error_message)
489
+
490
+ def get_historical_klines(self, symbol, bar='15m', limit=300, after:str=None, params={}):
491
+ """
492
+ 获取历史K线数据
493
+ Args:
494
+ symbol: 交易对
495
+ bar: K线周期
496
+ limit: 数据条数
497
+ after: 之后时间,格式为 "2025-05-21 23:00:00+08:00"
498
+ """
499
+
500
+ params = {
501
+ **params,
502
+ # 'instId': instId,
503
+ }
504
+ since = None
505
+ if after:
506
+ since = self.exchange.parse8601(after)
507
+ limit = None
508
+ if since:
509
+ params['paginate'] = True
510
+
511
+ klines = self.exchange.fetch_ohlcv(symbol, timeframe=bar,since=since, limit=limit, params=params)
512
+ # if 'data' in response and len(response['data']) > 0:
513
+ if klines :
514
+ # return response['data']
515
+ return klines
516
+ else:
517
+ raise Exception(f"{symbol} : Unexpected response structure or missing candlestick data")
518
+
519
+ def get_historical_klines_df(self, symbol, bar='15m', limit=300, after:str=None, params={}) -> pd.DataFrame:
520
+ klines = self.get_historical_klines(symbol, bar=bar, limit=limit, after=after, params=params)
521
+ return self.format_klines(klines)
522
+
523
+ def format_klines(self, klines) -> pd.DataFrame:
524
+ """_summary_
525
+ 格式化K线数据
526
+ Args:
527
+ klines (_type_): _description_
528
+ """
529
+ klines_df = pd.DataFrame(klines, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
530
+ # 转换时间戳为日期时间
531
+ klines_df['timestamp'] = pd.to_datetime(klines_df['timestamp'], unit='ms').dt.tz_localize('UTC').dt.tz_convert('Asia/Shanghai')
532
+
533
+ return klines_df
@@ -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
@@ -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]]}")
@@ -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,19 @@ 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
+ data = self.build_struct(df=df)
275
+ else:
276
+ data = df.copy()
270
277
 
271
- # 获取prd范围内的数据
272
- start_idx = max(0, len(data) - 1 - prd)
273
278
  # 筛选有效结构且在prd范围内的数据
274
279
  last_struct = None
275
- mask = data[self.STRUCT_COL].notna() & (data.index >= start_idx) if prd > 0 else data[self.STRUCT_COL].notna()
280
+ mask = data[self.STRUCT_COL].notna()
276
281
  valid_structs = data[ mask ]
277
282
  if not valid_structs.empty:
278
283
  # 获取最近的结构
@@ -281,6 +286,7 @@ class SMCStruct(SMCBase):
281
286
  self.STRUCT_COL: last_struct[self.STRUCT_COL],
282
287
  self.STRUCT_HIGH_COL: last_struct[self.STRUCT_HIGH_COL],
283
288
  self.STRUCT_LOW_COL: last_struct[self.STRUCT_LOW_COL],
289
+ self.STRUCT_MID_COL: (last_struct[self.STRUCT_HIGH_COL] + last_struct[self.STRUCT_LOW_COL]) / 2,
284
290
  self.STRUCT_HIGH_INDEX_COL: last_struct[self.STRUCT_HIGH_INDEX_COL],
285
291
  self.STRUCT_LOW_INDEX_COL: last_struct[self.STRUCT_LOW_INDEX_COL],
286
292
  self.STRUCT_DIRECTION_COL: last_struct[self.STRUCT_DIRECTION_COL]
@@ -1,276 +0,0 @@
1
- import logging
2
- import time
3
- import ccxt
4
- import pandas as pd
5
-
6
-
7
- from decimal import Decimal
8
- from core.utils.OPTools import OPTools
9
- from ccxt.base.exchange import ConstructorArgs
10
-
11
-
12
- class Exchange:
13
- def __init__(self, config:ConstructorArgs, exchangeKey:str = "okx",) :
14
- # 配置交易所
15
- self.exchange = getattr(ccxt, exchangeKey)(config)
16
- self.logger = logging.getLogger(__name__)
17
-
18
-
19
-
20
- def getMarket(self, symbol:str):
21
- # 配置交易对
22
- self.exchange.load_markets()
23
-
24
- 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):
38
-
39
- try:
40
- # 假设获取账户持仓模式的 API
41
- response = self.exchange.private_get_account_config()
42
- data = response.get('data', [])
43
- if data and isinstance(data, list):
44
- # 取列表的第一个元素(假设它是一个字典),然后获取 'posMode'
45
- position_mode = data[0].get('posMode', 'single') # 默认值为单向
46
-
47
- return position_mode
48
- else:
49
-
50
- return 'single' # 返回默认值
51
- except Exception as e:
52
- error_message = f"Error fetching position mode: {e}"
53
- self.logger.error(error_message)
54
- raise Exception(error_message)
55
-
56
- def set_leverage(self,symbol, leverage, mgnMode='isolated',posSide=None):
57
- try:
58
- # 设置杠杆
59
- params = {
60
- # 'instId': instId,
61
- 'leverage': leverage,
62
- 'marginMode': mgnMode
63
- }
64
- if posSide:
65
- params['side'] = posSide
66
-
67
- self.exchange.set_leverage(leverage, symbol=symbol, params=params)
68
- self.logger.info(f"{symbol} Successfully set leverage to {leverage}x")
69
- except Exception as e:
70
- error_message = f"{symbol} Error setting leverage: {e}"
71
- self.logger.error(error_message)
72
- raise Exception(error_message)
73
- # 获取价格精度
74
- def get_precision_length(self,symbol) -> int:
75
- tick_size = self.get_tick_size(symbol)
76
- return len(f"{tick_size:.15f}".rstrip('0').split('.')[1]) if '.' in f"{tick_size:.15f}" else 0
77
-
78
- def format_price(self, symbol, price:Decimal) -> str:
79
- precision = self.get_precision_length(symbol)
80
- return f"{price:.{precision}f}"
81
-
82
- def convert_contract(self, symbol, amount, price:Decimal, direction='cost_to_contract'):
83
- """
84
- 进行合约与币的转换
85
- :param symbol: 交易对符号,如 'BTC/USDT:USDT'
86
- :param amount: 输入的数量,可以是合约数量或币的数量
87
- :param direction: 转换方向,'amount_to_contract' 表示从数量转换为合约,'cost_to_contract' 表示从金额转换为合约
88
- :return: 转换后的数量
89
- """
90
-
91
- # 获取合约规模
92
- market_contractSize = OPTools.toDecimal(self.getMarket(symbol)['contractSize'])
93
- amount = OPTools.toDecimal(amount)
94
- if direction == 'amount_to_contract':
95
- contract_size = amount / market_contractSize
96
- elif direction == 'cost_to_contract':
97
- contract_size = amount / price / market_contractSize
98
- else:
99
- raise Exception(f"{symbol}:{direction} 是无效的转换方向,请输入 'amount_to_contract' 或 'cost_to_contract'。")
100
-
101
- return self.amount_to_precision(symbol, contract_size)
102
-
103
-
104
- def cancel_all_orders(self, symbol):
105
- max_retries = 3
106
- retry_count = 0
107
-
108
- while retry_count < max_retries:
109
- try:
110
- # 获取所有未完成订单
111
- params = {
112
- # 'instId': instId
113
- }
114
- open_orders = self.exchange.fetch_open_orders(symbol=symbol, params=params)
115
-
116
- # 批量取消所有订单
117
- if open_orders:
118
- order_ids = [order['id'] for order in open_orders]
119
- self.exchange.cancel_orders(order_ids, symbol, params=params)
120
-
121
- self.logger.debug(f"{symbol}: {order_ids} 挂单取消成功.")
122
- else:
123
- self.logger.debug(f"{symbol}: 无挂单.")
124
- return True
125
-
126
- except Exception as e:
127
- retry_count += 1
128
- if retry_count == max_retries:
129
- error_message = f"{symbol} 取消挂单失败(重试{retry_count}次): {str(e)}"
130
- self.logger.error(error_message)
131
- raise Exception(error_message)
132
- else:
133
- self.logger.warning(f"{symbol} 取消挂单失败,正在进行第{retry_count}次重试: {str(e)}")
134
- time.sleep(0.1) # 重试前等待0.1秒
135
-
136
-
137
- def place_order(self, symbol, price: Decimal, amount_usdt, side, leverage=20, order_type='limit'):
138
- """
139
- 下单
140
- Args:
141
- symbol: 交易对
142
- price: 下单价格
143
- amount_usdt: 下单金额
144
- side: 下单方向
145
- order_type: 订单类型
146
- """
147
- # 格式化价格
148
- adjusted_price = self.format_price(symbol, price)
149
-
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)
159
-
160
- params = {
161
-
162
- "tdMode": 'isolated',
163
- "side": side,
164
- "ordType": order_type,
165
- "sz": contract_size,
166
- "px": adjusted_price
167
- }
168
-
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}")
174
- 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
- # 使用ccxt创建订单
184
- self.logger.debug(f"Pre Order placed: {order} ")
185
- order_result = self.exchange.create_order(
186
- **order
187
- # symbol=symbol,
188
- # type='limit',
189
- # side=side,
190
- # amount=amount_usdt,
191
- # price=float(adjusted_price),
192
- # params=params
193
- )
194
- # self.logger.debug(f"{symbol} ++ Order placed rs : {order_result}")
195
- 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! --------")
201
-
202
- def fetch_position(self, symbol):
203
- """_summary_
204
-
205
- Args:
206
- symbol (_type_): _description_
207
-
208
- Returns:
209
- _type_: _description_
210
- """
211
-
212
- max_retries = 3
213
- retry_count = 0
214
-
215
- while retry_count < max_retries:
216
- try:
217
- position = self.exchange.fetch_position(symbol=symbol)
218
- if position and position['contracts'] > 0:
219
- self.logger.debug(f"{symbol} 有持仓合约数: {position['contracts']}")
220
- return position
221
- return None
222
- except Exception as e:
223
- retry_count += 1
224
- if retry_count == max_retries:
225
- error_message = f"!!{symbol} 获取持仓失败(重试{retry_count}次): {str(e)}"
226
- self.logger.error(error_message)
227
- raise Exception(error_message)
228
-
229
- self.logger.warning(f"{symbol} 检查持仓失败,正在进行第{retry_count}次重试: {str(e)}")
230
- time.sleep(0.1) # 重试前等待0.1秒
231
-
232
-
233
- def get_historical_klines(self, symbol, bar='15m', limit=300, after:str=None, params={}):
234
- """
235
- 获取历史K线数据
236
- Args:
237
- symbol: 交易对
238
- bar: K线周期
239
- limit: 数据条数
240
- after: 之后时间,格式为 "2025-05-21 23:00:00+08:00"
241
- """
242
-
243
- params = {
244
- **params,
245
- # 'instId': instId,
246
- }
247
- since = None
248
- if after:
249
- since = self.exchange.parse8601(after)
250
- limit = None
251
- if since:
252
- params['paginate'] = True
253
-
254
- klines = self.exchange.fetch_ohlcv(symbol, timeframe=bar,since=since, limit=limit, params=params)
255
- # if 'data' in response and len(response['data']) > 0:
256
- if klines :
257
- # return response['data']
258
- return klines
259
- else:
260
- raise Exception(f"{symbol} : Unexpected response structure or missing candlestick data")
261
-
262
- def get_historical_klines_df(self, symbol, bar='15m', limit=300, after:str=None, params={}) -> pd.DataFrame:
263
- klines = self.get_historical_klines(symbol, bar=bar, limit=limit, after=after, params=params)
264
- return self.format_klines(klines)
265
-
266
- def format_klines(self, klines) -> pd.DataFrame:
267
- """_summary_
268
- 格式化K线数据
269
- Args:
270
- klines (_type_): _description_
271
- """
272
- klines_df = pd.DataFrame(klines, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
273
- # 转换时间戳为日期时间
274
- klines_df['timestamp'] = pd.to_datetime(klines_df['timestamp'], unit='ms').dt.tz_localize('UTC').dt.tz_convert('Asia/Shanghai')
275
-
276
- return klines_df
File without changes