-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathmain.py
More file actions
1747 lines (1428 loc) · 75.2 KB
/
main.py
File metadata and controls
1747 lines (1428 loc) · 75.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import os
import json
import time
import hmac
import hashlib
import requests
import numpy as np
from datetime import datetime
from typing import Dict, List, Optional
from dotenv import load_dotenv
import threading
import logging
# 加载环境变量
load_dotenv()
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('trading_bot.log'),
logging.StreamHandler()
]
)
class BinanceFuturesAPI:
"""Binance合约API客户端"""
def __init__(self, api_key: str = None, api_secret: str = None, testnet: bool = False):
"""
初始化Binance合约API客户端
Args:
api_key: Binance API密钥
api_secret: Binance API密钥
testnet: 是否使用测试网
"""
self.api_key = api_key or os.getenv('BINANCE_API_KEY')
self.api_secret = api_secret or os.getenv('BINANCE_API_SECRET')
if not self.api_key or not self.api_secret:
raise ValueError("请提供Binance API密钥或在.env文件中设置BINANCE_API_KEY和BINANCE_API_SECRET")
# 设置API基础URL
if testnet:
self.base_url = "https://testnet.binancefuture.com"
else:
self.base_url = "https://fapi.binance.com"
self.session = requests.Session()
self.session.headers.update({
'X-MBX-APIKEY': self.api_key
})
def _generate_signature(self, params: Dict) -> str:
"""生成API签名"""
query_string = '&'.join([f"{k}={v}" for k, v in params.items()])
return hmac.new(
self.api_secret.encode('utf-8'),
query_string.encode('utf-8'),
hashlib.sha256
).hexdigest()
def _make_request(self, method: str, endpoint: str, params: Dict = None, signed: bool = False) -> Dict:
"""发送API请求"""
if params is None:
params = {}
if signed:
params['timestamp'] = int(time.time() * 1000)
params['signature'] = self._generate_signature(params)
url = f"{self.base_url}{endpoint}"
try:
if method.upper() == 'GET':
response = self.session.get(url, params=params)
elif method.upper() == 'POST':
# Binance API需要使用data参数而不是json参数
response = self.session.post(url, data=params)
else:
raise ValueError(f"不支持的HTTP方法: {method}")
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"API请求失败: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"响应内容: {e.response.text}")
raise
def get_account_info(self) -> Dict:
"""获取账户信息"""
return self._make_request('GET', '/fapi/v2/account', signed=True)
def get_balance(self) -> List[Dict]:
"""获取账户余额"""
return self._make_request('GET', '/fapi/v2/balance', signed=True)
def get_positions(self) -> List[Dict]:
"""获取持仓信息"""
return self._make_request('GET', '/fapi/v2/positionRisk', signed=True)
def get_open_orders(self, symbol: str = None) -> List[Dict]:
"""获取当前挂单"""
params = {}
if symbol:
params['symbol'] = symbol
return self._make_request('GET', '/fapi/v1/openOrders', params, signed=True)
def get_order_history(self, symbol: str = None, limit: int = 100) -> List[Dict]:
"""获取订单历史"""
params = {'limit': limit}
if symbol:
params['symbol'] = symbol
return self._make_request('GET', '/fapi/v1/allOrders', params, signed=True)
def get_trade_history(self, symbol: str = None, limit: int = 100) -> List[Dict]:
"""获取交易历史"""
params = {'limit': limit}
if symbol:
params['symbol'] = symbol
return self._make_request('GET', '/fapi/v1/userTrades', params, signed=True)
def get_funding_rates(self) -> List[Dict]:
"""获取所有交易对的资金费率"""
return self._make_request('GET', '/fapi/v1/premiumIndex', signed=False)
def get_klines(self, symbol: str, interval: str = '1d', limit: int = 100) -> List[List]:
"""获取K线数据"""
params = {
'symbol': symbol,
'interval': interval,
'limit': limit
}
return self._make_request('GET', '/fapi/v1/klines', params, signed=False)
def get_symbol_funding_rate(self, symbol: str) -> Dict:
"""获取指定交易对的资金费率"""
params = {'symbol': symbol}
return self._make_request('GET', '/fapi/v1/premiumIndex', params, signed=False)
def get_top_long_short_position_ratio(self, symbol: str, period: str = '5m', limit: int = 10) -> List[Dict]:
"""获取大户持仓量多空比数据"""
# 直接使用完整URL,不通过_make_request
url = f"{self.base_url}/futures/data/topLongShortPositionRatio"
params = {
'symbol': symbol,
'period': period,
'limit': limit
}
try:
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"获取大户持仓多空比失败: {e}")
return []
def get_top_long_short_account_ratio(self, symbol: str, period: str = '5m', limit: int = 10) -> List[Dict]:
"""获取多空账户人数比数据"""
# 直接使用完整URL,不通过_make_request
url = f"{self.base_url}/futures/data/topLongShortAccountRatio"
params = {
'symbol': symbol,
'period': period,
'limit': limit
}
try:
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
return response.json()
except Exception as e:
print(f"获取账户多空比失败: {e}")
return []
def set_leverage(self, symbol: str, leverage: int) -> Dict:
"""设置杠杆倍数"""
params = {
'symbol': symbol,
'leverage': str(leverage) # Binance API需要字符串格式
}
try:
return self._make_request('POST', '/fapi/v1/leverage', params, signed=True)
except Exception as e:
error_msg = str(e)
if "-4028" in error_msg:
logging.info(f"{symbol} 杠杆已经是目标倍数")
return {"msg": "杠杆已经是目标倍数"}
logging.error(f"设置杠杆失败: {e}")
raise
def change_margin_type(self, symbol: str, margin_type: str = 'CROSSED') -> Dict:
"""更改保证金模式"""
params = {
'symbol': symbol,
'marginType': margin_type
}
try:
return self._make_request('POST', '/fapi/v1/marginType', params, signed=True)
except Exception as e:
error_msg = str(e)
if "No need to change margin type" in error_msg or "-4046" in error_msg:
logging.info(f"{symbol} 保证金类型已经是目标类型")
return {"msg": "保证金类型已经是目标类型"}
elif "-4168" in error_msg:
logging.info(f"{symbol} 在多资产模式下自动使用全仓模式")
return {"msg": "多资产模式下自动使用全仓模式"}
logging.error(f"更改保证金模式失败: {e}")
raise
def get_symbol_info(self, symbol: str) -> Dict:
"""获取交易对信息"""
try:
exchange_info = self._make_request('GET', '/fapi/v1/exchangeInfo', signed=False)
for s in exchange_info.get('symbols', []):
if s.get('symbol') == symbol:
return s
return None
except Exception as e:
logging.error(f"获取交易对信息失败: {e}")
return None
def get_position_mode(self) -> str:
"""获取持仓模式"""
try:
result = self._make_request('GET', '/fapi/v1/positionSide/dual', signed=True)
return "hedge" if result.get('dualSidePosition') else "oneway"
except Exception as e:
logging.warning(f"获取持仓模式失败,默认使用单向模式: {e}")
return "oneway"
def place_order(self, symbol: str, side: str, order_type: str, quantity: float = None,
quoteOrderQty: float = None, price: float = None, time_in_force: str = 'GTC') -> Dict:
"""下单"""
params = {
'symbol': symbol,
'side': side, # BUY or SELL
'type': order_type # MARKET, LIMIT, etc.
}
# 检查持仓模式,如果是对冲模式需要指定positionSide
position_mode = self.get_position_mode()
if position_mode == "hedge":
# 对冲模式下需要指定持仓方向
if side == 'BUY':
params['positionSide'] = 'LONG'
else: # SELL
params['positionSide'] = 'SHORT'
logging.info(f"对冲模式下设置 positionSide: {params['positionSide']}")
# 如果使用按金额下单但没有quantity,需要计算quantity
if quoteOrderQty and not quantity and order_type == 'MARKET':
try:
# 获取当前价格
ticker = self._make_request('GET', '/fapi/v1/ticker/price', {'symbol': symbol}, signed=False)
current_price = float(ticker.get('price', 0))
# 获取交易对信息以确定精度
symbol_info = self.get_symbol_info(symbol)
if symbol_info:
# 获取数量精度
for f in symbol_info.get('filters', []):
if f.get('filterType') == 'LOT_SIZE':
step_size = float(f.get('stepSize', '0.001'))
break
else:
step_size = 0.001
# 计算数量
calculated_quantity = quoteOrderQty / current_price
# 获取最小数量要求
min_qty = 0
for f in symbol_info.get('filters', []):
if f.get('filterType') == 'MIN_NOTIONAL':
min_notional = float(f.get('notional', '0'))
min_qty = max(min_qty, min_notional / current_price)
elif f.get('filterType') == 'LOT_SIZE':
min_qty = max(min_qty, float(f.get('minQty', '0')))
# 确保数量满足最小要求
if calculated_quantity < min_qty:
calculated_quantity = min_qty
logging.info(f"调整数量到最小值: {calculated_quantity}")
# 向下舍入到步长精度
import math
calculated_quantity = math.floor(calculated_quantity / step_size) * step_size
if calculated_quantity > 0:
# 确定小数位数
decimal_places = max(0, -int(math.log10(step_size)))
params['quantity'] = f"{calculated_quantity:.{decimal_places}f}"
logging.info(f"计算得出 {symbol} 数量: {params['quantity']}")
else:
logging.warning(f"计算的数量太小: {calculated_quantity},回退使用 quoteOrderQty")
# 回退使用 quoteOrderQty,让交易所自己计算
params['quoteOrderQty'] = str(quoteOrderQty)
except Exception as e:
logging.error(f"计算订单数量失败: {e}")
# 如果计算失败,回退使用quoteOrderQty
params['quoteOrderQty'] = str(quoteOrderQty)
else:
# Binance API需要字符串格式的数值
if quantity:
params['quantity'] = str(quantity)
if quoteOrderQty:
params['quoteOrderQty'] = str(quoteOrderQty)
if price:
params['price'] = str(price)
if order_type == 'LIMIT':
params['timeInForce'] = time_in_force
try:
return self._make_request('POST', '/fapi/v1/order', params, signed=True)
except Exception as e:
logging.error(f"下单失败: {e}")
raise
def get_position_info(self, symbol: str) -> Dict:
"""获取指定交易对的持仓信息"""
positions = self.get_positions()
for position in positions:
if position.get('symbol') == symbol:
return position
return None
def close_position(self, symbol: str, position_side: str = None) -> Dict:
"""平仓指定交易对的持仓"""
try:
# 获取当前持仓信息
position = self.get_position_info(symbol)
if not position:
raise ValueError(f"未找到 {symbol} 的持仓信息")
position_amt = float(position.get('positionAmt', 0))
if abs(position_amt) == 0:
raise ValueError(f"{symbol} 当前没有持仓")
# 获取持仓模式
position_mode = self.get_position_mode()
# 确定平仓参数
if position_amt > 0: # 多头持仓,需要卖出平仓
side = 'SELL'
close_position_side = 'LONG' if position_mode == 'hedge' else None
logging.info(f"📈 {symbol} 多头持仓平仓,执行卖出操作")
else: # 空头持仓,需要买入平仓
side = 'BUY'
close_position_side = 'SHORT' if position_mode == 'hedge' else None
logging.info(f"📉 {symbol} 空头持仓平仓,执行买入操作(这是平仓,不是开新仓)")
# 如果指定了position_side,使用指定的
if position_side:
close_position_side = position_side
# 构建平仓订单参数
params = {
'symbol': symbol,
'side': side,
'type': 'MARKET',
'quantity': str(abs(position_amt)) # 使用绝对值
}
# 对冲模式下需要指定positionSide
if position_mode == 'hedge' and close_position_side:
params['positionSide'] = close_position_side
logging.info(f"对冲模式下平仓设置 positionSide: {close_position_side}")
logging.info(f"准备平仓 {symbol}: {side} {abs(position_amt)}")
# 执行平仓订单
result = self._make_request('POST', '/fapi/v1/order', params, signed=True)
logging.info(f"平仓成功: {result}")
return result
except Exception as e:
logging.error(f"平仓 {symbol} 失败: {e}")
raise
def debug_funding_rates(self, limit: int = 5) -> None:
"""调试函数:查看资金费率数据结构"""
try:
funding_rates = self.get_funding_rates()
print(f"📊 获取到 {len(funding_rates) if funding_rates else 0} 个交易对的数据")
if funding_rates and len(funding_rates) > 0:
print("\n🔍 前几个交易对的数据结构:")
for i, item in enumerate(funding_rates[:limit]):
print(f"\n--- 交易对 {i+1} ---")
print(f"完整数据: {item}")
if isinstance(item, dict):
print(f"symbol: {item.get('symbol', 'N/A')}")
print(f"lastFundingRate: {item.get('lastFundingRate', 'N/A')}")
print(f"markPrice: {item.get('markPrice', 'N/A')}")
print(f"nextFundingTime: {item.get('nextFundingTime', 'N/A')}")
else:
print("❌ 未获取到任何数据或数据为空")
except Exception as e:
print(f"❌ 调试资金费率数据失败: {e}")
import traceback
print(f"错误详情: {traceback.format_exc()}")
def get_24hr_ticker(self) -> List[Dict]:
"""获取24小时价格变动统计"""
return self._make_request('GET', '/fapi/v1/ticker/24hr', signed=False)
class AutoTradingBot:
"""自动化交易机器人"""
def __init__(self, api_key: str = None, api_secret: str = None, testnet: bool = False):
"""初始化自动化交易机器人"""
self.api = BinanceFuturesAPI(api_key, api_secret, testnet)
self.running = False
# 交易参数(按README.md要求设置)
self.trade_amount = 100 # 每次交易金额(USDT)
self.leverage = 20 # 杠杆倍数
self.funding_rate_min = -0.02 # 资金费率下限(-2%)
self.funding_rate_max = -0.001 # 资金费率上限(-0.1%)
self.min_volume_usdt = 60000000 # 最小24小时交易量(6000万USDT)
self.monitor_interval = 300 # 监控间隔(5分钟)
self.position_check_interval = 300 # 持仓检查间隔(5分钟)
self.position_monitor_thread = None # 持仓监控线程
# 做多候选列表
self.long_candidates = set()
# 黑名单设置
self.load_blacklist()
logging.info("自动化交易机器人初始化完成 - 基于1小时资金费率结算+无持仓+负资金费率的做多策略")
# 显示当前黑名单状态
if self.blacklist:
logging.info(f"🚫 当前黑名单: {list(self.blacklist)}")
else:
logging.info("📝 当前未设置黑名单")
def setup_margin_modes(self, symbols: List[str] = None) -> Dict[str, bool]:
"""
批量设置交易对为全仓模式(仅在没有持仓时执行)
Args:
symbols: 要设置的交易对列表,如果为None则设置常用交易对
Returns:
Dict[str, bool]: 每个交易对的设置结果
"""
if symbols is None:
# 默认设置一些主要的交易对
symbols = [
'BTCUSDT', 'ETHUSDT', 'BNBUSDT', 'ADAUSDT', 'SOLUSDT', 'XRPUSDT',
'DOGEUSDT', 'DOTUSDT', 'MATICUSDT', 'AVAXUSDT', 'LINKUSDT', 'LTCUSDT'
]
results = {}
logging.info(f"开始为 {len(symbols)} 个交易对设置全仓模式...")
for symbol in symbols:
try:
# 先检查是否有持仓
position = self.api.get_position_info(symbol)
if position and abs(float(position.get('positionAmt', 0))) > 0:
results[symbol] = False
logging.warning(f"⚠️ {symbol} 有持仓,跳过保证金模式设置")
continue
# 尝试设置全仓模式
result = self.api.change_margin_type(symbol, 'CROSSED')
results[symbol] = True
logging.info(f"✅ {symbol} 已设置为全仓模式")
except Exception as e:
error_msg = str(e)
if "-4046" in error_msg or "No need to change margin type" in error_msg:
results[symbol] = True
logging.info(f"✅ {symbol} 已是全仓模式")
elif "-4168" in error_msg:
results[symbol] = True
logging.info(f"✅ {symbol} 在多资产模式下自动使用全仓模式")
else:
results[symbol] = False
logging.error(f"❌ {symbol} 设置全仓模式失败: {e}")
success_count = sum(results.values())
total_count = len(symbols)
logging.info(f"保证金模式设置完成: {success_count}/{total_count} 个交易对成功")
return results
def load_blacklist(self):
"""从.env文件加载黑名单币种"""
try:
blacklist_str = os.getenv('BLACKLIST_SYMBOLS', '')
if blacklist_str:
# 支持逗号分隔的币种列表
blacklist_symbols = [symbol.strip().upper() for symbol in blacklist_str.split(',') if symbol.strip()]
# 确保都以USDT结尾
self.blacklist = set()
for symbol in blacklist_symbols:
if not symbol.endswith('USDT'):
symbol = symbol + 'USDT'
self.blacklist.add(symbol)
if self.blacklist:
logging.info(f"📋 加载黑名单币种: {list(self.blacklist)}")
else:
logging.info("📋 未设置黑名单币种")
else:
self.blacklist = set()
logging.info("📋 未设置黑名单币种")
except Exception as e:
logging.error(f"加载黑名单配置失败: {e}")
self.blacklist = set()
def debug_blacklist(self):
"""调试黑名单设置"""
blacklist_env = os.getenv('BLACKLIST_SYMBOLS', '')
logging.info(f"🔍 环境变量 BLACKLIST_SYMBOLS: '{blacklist_env}'")
logging.info(f"🔍 解析后的黑名单: {list(self.blacklist)}")
# 测试特定币种是否在黑名单中
test_symbols = ['LEVERUSDT', 'SKATEUSDT', 'BTCUSDT']
for symbol in test_symbols:
in_blacklist = symbol in self.blacklist
logging.info(f"🔍 {symbol} 是否在黑名单: {in_blacklist}")
return self.blacklist
def check_hourly_funding(self, symbol: str) -> bool:
"""
检查合约是否有1小时资金费率结算
参考binance-py-monitor的逻辑,检查历史资金费率间隔是否存在1小时(0.8-1.2小时容错范围)
"""
try:
import requests
# 获取历史资金费率数据(最近4次,确保有足够数据分析间隔)
url = f"https://fapi.binance.com/fapi/v1/fundingRate?symbol={symbol}&limit=4"
response = requests.get(url, timeout=5)
response.raise_for_status()
funding_history_data = response.json()
if funding_history_data and len(funding_history_data) >= 2:
# 分析相邻交割之间的时间间隔
for i in range(len(funding_history_data) - 1):
current_funding_time = funding_history_data[i]["fundingTime"]
next_funding_time = funding_history_data[i + 1]["fundingTime"]
# 计算时间间隔(小时)
interval_hours = (next_funding_time - current_funding_time) / (1000 * 3600)
# 检查是否存在大约1小时的交割间隔(容错范围:0.8-1.2小时)
if 0.8 <= interval_hours <= 1.2:
logging.debug(f"{symbol} 发现1小时资金费率结算,间隔: {interval_hours:.1f}h")
return True
logging.debug(f"{symbol} 未发现1小时资金费率结算")
return False
else:
logging.debug(f"{symbol} 历史资金费率数据不足")
return False
except Exception as e:
logging.debug(f"检查 {symbol} 1小时资金费率结算失败: {e}")
return False
def get_long_candidates(self) -> List[str]:
"""
获取做多候选合约列表
筛选条件:
1. 资金费率结算时间为1小时
2. 当前没有持仓
3. 资金费率小于-0.1%
"""
candidates = []
try:
# 获取所有合约的资金费率
logging.info("开始筛选做多候选合约...")
funding_rates = self.api.get_funding_rates()
if not funding_rates:
logging.warning("未能获取资金费率数据")
return candidates
# 筛选符合条件的合约
for data in funding_rates:
try:
symbol = data.get('symbol', '')
if not symbol.endswith('USDT'):
continue
# 检查黑名单
if symbol in self.blacklist:
logging.debug(f"❌ {symbol} 在黑名单中,跳过做多")
continue
# 筛选条件1:资金费率结算时间为1小时
if not self.check_hourly_funding(symbol):
logging.debug(f"{symbol} 资金费率结算时间不是1小时,跳过做多")
continue
# 筛选条件2:当前没有持仓
if self.has_position(symbol):
logging.debug(f"{symbol} 已有持仓,跳过做多")
continue
# 筛选条件3:资金费率小于-0.1%
funding_rate = float(data.get('lastFundingRate', 0))
if funding_rate >= -0.001: # -0.1%
logging.debug(f"{symbol} 资金费率 {funding_rate:.6f} >= -0.1%,跳过做多")
continue
candidates.append(symbol)
logging.info(f"✅ 做多候选: {symbol}, 1小时结算: ✓, 无持仓: ✓, 资金费率: {funding_rate:.6f}")
except Exception as e:
logging.error(f"处理合约 {symbol} 时出错: {e}")
continue
logging.info(f"筛选完成,共找到 {len(candidates)} 个做多候选合约")
return candidates
except Exception as e:
logging.error(f"获取做多候选合约失败: {e}")
return candidates
def get_usdt_balance(self) -> float:
"""获取USDT余额"""
try:
balance_data = self.api.get_balance()
for balance in balance_data:
if balance.get('asset') == 'USDT':
return float(balance.get('availableBalance', 0))
return 0.0
except Exception as e:
logging.error(f"获取USDT余额失败: {e}")
return 0.0
def has_position(self, symbol: str) -> bool:
"""检查是否已有持仓(增强版,更严格的检查)"""
try:
# 直接获取所有持仓,而不是依赖get_position_info
positions = self.api.get_positions()
if not positions:
logging.debug(f"未获取到任何持仓信息")
return False
for position in positions:
if position.get('symbol') == symbol:
position_amt = float(position.get('positionAmt', 0))
if abs(position_amt) > 0:
# 记录持仓详情
side = "做空" if position_amt < 0 else "做多"
logging.info(f"🔍 {symbol} 已有持仓: {side} {abs(position_amt)}")
# 额外检查:确保持仓量超过最小交易单位
if abs(position_amt) > 0.0001: # 增加最小持仓量检查
return True
else:
logging.warning(f"⚠️ {symbol} 持仓量过小: {position_amt}, 可能是精度问题")
return False
logging.debug(f"🔍 {symbol} 无持仓")
return False
except Exception as e:
logging.error(f"检查 {symbol} 持仓失败: {e}")
# 出错时保守处理,返回True以避免重复下单
return True
def execute_buy_order(self, symbol: str) -> bool:
"""执行买入订单(做多)"""
try:
# 黑名单安全检查:再次确认不在黑名单中
if symbol in self.blacklist:
logging.warning(f"🚫 {symbol} 在黑名单中,禁止做多操作")
return False
# 安全检查:再次确认是否已有持仓
if self.has_position(symbol):
logging.warning(f"🚫 {symbol} 已有持仓,取消做多操作")
return False
# 检查余额
balance = self.get_usdt_balance()
if balance < self.trade_amount:
logging.warning(f"USDT余额不足: {balance} < {self.trade_amount}")
return False
# 设置杠杆倍数
try:
self.api.set_leverage(symbol, self.leverage)
logging.info(f"✅ {symbol} 杠杆设置为 {self.leverage}x")
except Exception as e:
logging.warning(f"设置杠杆可能失败: {e}")
# 注意:保证金模式应在机器人启动前手动设置为全仓模式
# 如果有持仓,币安不允许程序化更改保证金模式
# 请确保所有交易对都已设置为全仓(CROSSED)模式
# 下市价买单(做多)
order_result = self.api.place_order(
symbol=symbol,
side='BUY',
order_type='MARKET',
quoteOrderQty=self.trade_amount
)
logging.info(f"📈 成功做多 {symbol}: {order_result}")
# 等待一小段时间让订单生效
import time
time.sleep(2)
# 验证持仓是否已经建立
if self.has_position(symbol):
logging.info(f"✅ {symbol} 做多订单已确认,持仓已建立")
else:
logging.warning(f"⚠️ {symbol} 做多订单已执行,但未检测到持仓(可能需要更多时间同步)")
return True
except Exception as e:
logging.error(f"执行 {symbol} 做多订单失败: {e}")
return False
def check_position_conditions(self, symbol: str) -> tuple[bool, str]:
"""
检查持仓合约是否符合交易条件
返回: (是否符合条件, 不符合的原因)
"""
try:
# 获取当前资金费率
funding_data = self.api.get_symbol_funding_rate(symbol)
if not funding_data:
return False, "无法获取资金费率数据"
funding_rate = float(funding_data.get('lastFundingRate', 0))
# 唯一条件:资金费率必须小于等于-0.1%
if funding_rate > -0.001: # -0.1%
return False, f"资金费率不符合({funding_rate:.6f} > -0.1%)"
return True, "符合条件"
except Exception as e:
return False, f"检查条件时出错: {e}"
def check_and_close_positions(self):
"""检查并平仓(新策略:当持仓合约不符合条件时自动平仓)"""
try:
logging.info("开始检查需要平仓的持仓...")
# 获取所有持仓
positions = self.api.get_positions()
if not positions:
logging.debug("未获取到持仓信息")
return
# 过滤有效持仓
active_positions = []
for position in positions:
position_amt = float(position.get('positionAmt', 0))
if abs(position_amt) > 0:
active_positions.append(position)
if not active_positions:
logging.debug("当前没有持仓")
return
logging.info(f"当前有 {len(active_positions)} 个持仓")
# 检查每个持仓是否符合条件
for position in active_positions:
try:
symbol = position.get('symbol', '')
position_amt = float(position.get('positionAmt', 0))
unrealized_pnl = float(position.get('unRealizedProfit', 0))
logging.info(f"{symbol} 持仓量: {position_amt}, 未实现盈亏: {unrealized_pnl:.4f} USDT")
# 📝 只检查多头仓位,跳过空头仓位
if position_amt < 0:
logging.info(f"🔴 {symbol} 为空头仓位,跳过检查")
continue
# 检查持仓合约是否符合交易条件
is_valid, reason = self.check_position_conditions(symbol)
if not is_valid:
logging.warning(f"🚨 {symbol} 不符合持仓条件: {reason},准备平仓")
# 检查是否在黑名单中
if symbol in self.blacklist:
logging.warning(f"🚨 {symbol} 在黑名单中且不符合条件,强制平仓")
try:
result = self.api.close_position(symbol)
logging.info(f"✅ 成功平仓 {symbol}: {result}")
# 记录平仓信息
logging.info(f"📊 平仓详情 - 交易对: {symbol}, 持仓量: {position_amt}, 未实现盈亏: {unrealized_pnl:.4f} USDT, 平仓原因: {reason}")
except Exception as e:
logging.error(f"❌ 平仓 {symbol} 失败: {e}")
else:
logging.debug(f"{symbol} 符合持仓条件,保持持仓")
except Exception as e:
logging.error(f"处理持仓 {symbol} 时出错: {e}")
continue
except Exception as e:
logging.error(f"检查和平仓过程失败: {e}")
def scan_and_trade(self):
"""扫描并执行做多交易(基于1小时资金费率结算+无持仓+负资金费率策略)"""
try:
logging.info("开始基于1小时资金费率结算+无持仓+负资金费率策略扫描做多交易机会...")
# 获取做多候选合约
current_candidates = self.get_long_candidates()
if not current_candidates:
logging.info("本轮扫描未发现符合条件的做多候选合约")
self.long_candidates = set()
return
# 更新候选列表
self.long_candidates = set(current_candidates)
logging.info(f"当前做多候选列表: {list(self.long_candidates)}")
# 检查每个候选合约的持仓情况
for symbol in current_candidates:
try:
# 如果已有持仓,跳过
if self.has_position(symbol):
logging.info(f"⚠️ {symbol} 已有持仓,跳过做多操作")
continue
# 执行做多
if self.execute_buy_order(symbol):
logging.info(f"✅ 成功做多 {symbol}")
# 为了避免过度交易,每次只执行一个交易
return
else:
logging.warning(f"❌ {symbol} 做多失败")
except Exception as e:
logging.error(f"处理做多合约 {symbol} 时出错: {e}")
continue
logging.info("本轮扫描完成,未执行新的做多交易")
except Exception as e:
logging.error(f"扫描和交易过程失败: {e}")
def position_monitor_loop(self):
"""持仓监控循环 - 独立线程运行"""
logging.info(f"🔍 持仓监控线程启动,检查间隔: {self.position_check_interval}秒")
while self.running:
try:
# 每5分钟检查一次持仓情况
self.check_and_close_positions()
if self.running: # 检查是否仍在运行
time.sleep(self.position_check_interval)
except Exception as e:
logging.error(f"持仓监控出错: {e}")
time.sleep(60) # 出错后等待1分钟再继续
logging.info("持仓监控线程已停止")
def start_monitoring(self):
"""开始监控"""
self.running = True
logging.info(f"🤖 自动化交易机器人开始运行,监控间隔: {self.monitor_interval}秒")
# 启动独立的持仓监控线程
self.position_monitor_thread = threading.Thread(target=self.position_monitor_loop, daemon=True)
self.position_monitor_thread.start()
logging.info("✅ 持仓监控线程已启动")
while self.running:
try:
self.scan_and_trade()
if self.running: # 检查是否仍在运行
logging.info(f"⏰ 等待 {self.monitor_interval} 秒后进行下次扫描...")
time.sleep(self.monitor_interval)
except KeyboardInterrupt:
logging.info("接收到停止信号")
break
except Exception as e:
logging.error(f"监控过程出错: {e}")
time.sleep(60) # 出错后等待1分钟再继续
logging.info("自动化交易机器人已停止")
def stop_monitoring(self):
"""停止监控"""
self.running = False
logging.info("正在停止自动化交易机器人...")
# 等待持仓监控线程结束
if self.position_monitor_thread and self.position_monitor_thread.is_alive():
logging.info("等待持仓监控线程结束...")
self.position_monitor_thread.join(timeout=5)
if self.position_monitor_thread.is_alive():
logging.warning("持仓监控线程未能在5秒内结束")
else:
logging.info("持仓监控线程已结束")
logging.info("自动化交易机器人已完全停止")
def calculate_ema(prices: List[float], period: int) -> List[float]:
"""
计算指数移动平均线 (EMA)
Args:
prices: 价格数据列表
period: EMA周期
Returns:
EMA值列表,长度为 len(prices) - period + 1
"""
if len(prices) < period:
return []
ema_values = []
multiplier = 2 / (period + 1)
# 第一个EMA值使用简单移动平均
sma = sum(prices[:period]) / period
ema_values.append(sma)
# 计算后续EMA值
for i in range(period, len(prices)):
ema = (prices[i] * multiplier) + (ema_values[-1] * (1 - multiplier))
ema_values.append(ema)
return ema_values
def calculate_macd(prices: List[float], fast_period: int = 12, slow_period: int = 26, signal_period: int = 9) -> Dict:
"""
计算MACD指标
MACD指标包含:
- MACD线:快线EMA(12) - 慢线EMA(26)
- 信号线(慢线):MACD线的EMA(9)
- 柱状图:MACD线 - 信号线
Args:
prices: 收盘价数据列表
fast_period: 快线EMA周期,默认12
slow_period: 慢线EMA周期,默认26
signal_period: 信号线EMA周期,默认9
Returns:
包含MACD线、信号线、柱状图的字典
"""
# 检查数据充足性:需要 slow_period + signal_period - 1 个数据点
required_length = slow_period + signal_period - 1
if len(prices) < required_length:
logging.debug(f"MACD计算数据不足:需要{required_length}个数据点,实际{len(prices)}个")
return {'macd_line': None, 'signal_line': None, 'histogram': None}
try:
# 计算快线和慢线EMA
ema_fast = calculate_ema(prices, fast_period)
ema_slow = calculate_ema(prices, slow_period)
if not ema_fast or not ema_slow:
logging.warning("EMA计算失败")
return {'macd_line': None, 'signal_line': None, 'histogram': None}
# 对齐数据长度:以慢线EMA为准(因为它开始得最晚)
alignment_length = len(ema_slow)
ema_fast_aligned = ema_fast[-alignment_length:]
ema_slow_aligned = ema_slow
# 计算MACD线 (快线EMA - 慢线EMA)
macd_line = [fast - slow for fast, slow in zip(ema_fast_aligned, ema_slow_aligned)]
# 计算信号线 (MACD线的EMA)
signal_line = calculate_ema(macd_line, signal_period)
if not signal_line: