forked from EBOLABOY/GridBNB-USDT
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtrader.py
More file actions
2041 lines (1711 loc) · 91.9 KB
/
Copy pathtrader.py
File metadata and controls
2041 lines (1711 loc) · 91.9 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
from config import TradingConfig, FLIP_THRESHOLD, settings
from exchange_client import ExchangeClient
from order_tracker import OrderTracker, OrderThrottler
from risk_manager import AdvancedRiskManager, RiskState
import logging
import asyncio
import numpy as np
from datetime import datetime
import time
import math
from helpers import send_pushplus_message, format_trade_message
import json
import os
from monitor import TradingMonitor
from position_controller_s1 import PositionControllerS1
class GridTrader:
def __init__(self, exchange, config, symbol: str):
"""初始化网格交易器"""
self.exchange = exchange
self.config = config
self.symbol = symbol # 使用传入的symbol参数
# 解析并存储基础和计价货币
try:
self.base_asset, self.quote_asset = self.symbol.split('/')
except ValueError:
raise ValueError(f"交易对格式不正确: {self.symbol}。应为 'BASE/QUOTE' 格式。")
# 从结构化配置中获取交易对特定的初始值
symbol_params = settings.INITIAL_PARAMS_JSON.get(self.symbol, {})
# 优先使用交易对特定配置,否则使用全局默认值
self.base_price = symbol_params.get('initial_base_price', 0.0) # 默认为0,让initialize逻辑处理
self.grid_size = symbol_params.get('initial_grid', settings.INITIAL_GRID)
self.initialized = False
self.highest = None
self.lowest = None
self.current_price = None
self.active_orders = {'buy': None, 'sell': None}
self.order_tracker = OrderTracker()
self.risk_manager = AdvancedRiskManager(self)
self.total_assets = 0
self.last_trade_time = None
self.last_trade_price = None
self.price_history = []
self.last_grid_adjust_time = time.time()
self.start_time = time.time()
# EWMA波动率状态变量
self.ewma_volatility = None # EWMA波动率
self.last_price = None # 上一次价格,用于计算收益率
self.ewma_initialized = False # EWMA是否已初始化
# 日志也带上交易对标识
self.logger = logging.getLogger(f"{self.__class__.__name__}[{self.symbol}]")
self.symbol_info = None
self.amount_precision = None # 数量精度
self.price_precision = None # 价格精度
self.monitored_orders = []
self.pending_orders = {}
self.order_timestamps = {}
self.throttler = OrderThrottler(limit=10, interval=60)
self.last_price_check = 0 # 新增价格检查时间戳
self.ORDER_TIMEOUT = 10 # 订单超时时间(秒)
self.MIN_TRADE_INTERVAL = 30 # 两次交易之间的最小间隔(秒)
self.grid_params = {
'base_size': 2.0, # 基础网格大小
'min_size': 1.0, # 最小网格
'max_size': 4.0, # 最大网格
'adjust_step': 0.2 # 调整步长
}
self.volatility_window = 24 # 波动率计算周期(小时)
self.monitor = TradingMonitor(self) # 初始化monitor
self.balance_check_interval = 60 # 每60秒检查一次余额
self.last_balance_check = 0
self.funding_balance_cache = {
'timestamp': 0,
'data': {}
}
self.funding_cache_ttl = 60 # 理财余额缓存60秒
self.position_controller_s1 = PositionControllerS1(self)
# 独立的监测状态变量,避免买入和卖出监测相互干扰
self.is_monitoring_buy = False # 是否在监测买入机会
self.is_monitoring_sell = False # 是否在监测卖出机会
# 【新增】波动率平滑化相关变量
self.volatility_history = [] # 用于存储最近的波动率值
self.volatility_smoothing_window = 3 # 平滑窗口大小,取最近3次的平均值
# 状态持久化相关 - 状态文件名与交易对挂钩
state_filename = f"trader_state_{self.symbol.replace('/', '_')}.json"
self.state_file_path = os.path.join(os.path.dirname(__file__), 'data', state_filename)
def _save_state(self):
"""【重构后】以原子方式安全地保存当前核心策略状态到文件"""
state = {
'base_price': self.base_price,
'grid_size': self.grid_size,
'highest': self.highest,
'lowest': self.lowest,
'last_grid_adjust_time': self.last_grid_adjust_time,
'last_trade_time': self.last_trade_time,
'last_trade_price': self.last_trade_price,
'timestamp': time.time(),
# EWMA波动率状态
'ewma_volatility': self.ewma_volatility,
'last_price': self.last_price,
'ewma_initialized': self.ewma_initialized,
# 独立监测状态
'is_monitoring_buy': self.is_monitoring_buy,
'is_monitoring_sell': self.is_monitoring_sell,
# 波动率平滑相关
'volatility_history': self.volatility_history
}
temp_file_path = self.state_file_path + ".tmp"
try:
# 确保目录存在
os.makedirs(os.path.dirname(self.state_file_path), exist_ok=True)
# 1. 写入临时文件
with open(temp_file_path, 'w', encoding='utf-8') as f:
json.dump(state, f, indent=2, ensure_ascii=False)
# 2. 原子性地重命名临时文件为正式文件
os.rename(temp_file_path, self.state_file_path)
self.logger.info(f"核心状态已安全保存。基准价: {self.base_price:.2f}, 网格: {self.grid_size:.2f}%")
except Exception as e:
self.logger.error(f"保存核心状态失败: {e}")
finally:
# 3. 确保临时文件在任何情况下都被删除
if os.path.exists(temp_file_path):
try:
os.remove(temp_file_path)
except OSError as e:
self.logger.error(f"删除临时状态文件失败: {e}")
def _load_state(self):
"""从文件加载核心策略状态"""
if not os.path.exists(self.state_file_path):
self.logger.info("未找到状态文件,将使用默认配置启动。")
return
try:
with open(self.state_file_path, 'r', encoding='utf-8') as f:
state = json.load(f)
# 加载并验证状态值
saved_base_price = state.get('base_price')
if saved_base_price and saved_base_price > 0:
self.base_price = float(saved_base_price)
saved_grid_size = state.get('grid_size')
if saved_grid_size and saved_grid_size > 0:
self.grid_size = float(saved_grid_size)
self.highest = state.get('highest') # 可以是 None
self.lowest = state.get('lowest') # 可以是 None
saved_last_grid_adjust_time = state.get('last_grid_adjust_time')
if saved_last_grid_adjust_time:
self.last_grid_adjust_time = float(saved_last_grid_adjust_time)
saved_last_trade_time = state.get('last_trade_time')
if saved_last_trade_time:
self.last_trade_time = float(saved_last_trade_time)
saved_last_trade_price = state.get('last_trade_price')
if saved_last_trade_price:
self.last_trade_price = float(saved_last_trade_price)
# 加载EWMA波动率状态
saved_ewma_volatility = state.get('ewma_volatility')
if saved_ewma_volatility is not None:
self.ewma_volatility = float(saved_ewma_volatility)
saved_last_price = state.get('last_price')
if saved_last_price is not None:
self.last_price = float(saved_last_price)
saved_ewma_initialized = state.get('ewma_initialized')
if saved_ewma_initialized is not None:
self.ewma_initialized = bool(saved_ewma_initialized)
# 加载独立监测状态
saved_is_monitoring_buy = state.get('is_monitoring_buy')
if saved_is_monitoring_buy is not None:
self.is_monitoring_buy = bool(saved_is_monitoring_buy)
saved_is_monitoring_sell = state.get('is_monitoring_sell')
if saved_is_monitoring_sell is not None:
self.is_monitoring_sell = bool(saved_is_monitoring_sell)
# 加载波动率历史记录
saved_volatility_history = state.get('volatility_history')
if saved_volatility_history is not None and isinstance(saved_volatility_history, list):
self.volatility_history = saved_volatility_history
self.logger.info(
f"成功从文件加载状态。基准价: {self.base_price:.2f}, 网格: {self.grid_size:.2f}%, "
f"EWMA已初始化: {self.ewma_initialized}, 监测状态: 买入={self.is_monitoring_buy}, 卖出={self.is_monitoring_sell}, "
f"波动率历史记录数: {len(self.volatility_history)}"
)
except Exception as e:
self.logger.error(f"加载核心状态失败,将使用默认值: {e}")
async def initialize(self):
if self.initialized:
return
# 首先加载保存的状态
self._load_state()
self.logger.info("正在加载市场数据...")
try:
# 确保市场数据加载成功
retry_count = 0
while not self.exchange.markets_loaded and retry_count < 3:
try:
await self.exchange.load_markets()
await asyncio.sleep(1)
except Exception as e:
self.logger.warning(f"加载市场数据失败: {str(e)}")
retry_count += 1
if retry_count >= 3:
raise
await asyncio.sleep(2)
# 检查现货账户资金并划转
await self._check_and_transfer_initial_funds()
self.symbol_info = self.exchange.exchange.market(self.symbol)
# 从市场信息中获取精度
if self.symbol_info and 'precision' in self.symbol_info:
self.amount_precision = self.symbol_info['precision'].get('amount')
self.price_precision = self.symbol_info['precision'].get('price')
self.logger.info(f"交易对精度: 数量 {self.amount_precision}, 价格 {self.price_precision}")
else:
self.logger.warning("无法获取交易对精度信息,将使用默认值")
# 使用动态默认精度,而不是硬编码BNB/USDT精度
self.amount_precision = 6 # 通用默认精度
self.price_precision = 2 # 通用默认精度
# 设置基准价:优先使用加载的状态,然后是交易对特定配置,最后是实时价格
if self.base_price is None or self.base_price == 0:
# self.base_price 在 __init__ 中已经从 INITIAL_PARAMS_JSON 加载
# 如果它仍然是0,说明配置中没指定,此时才获取实时价格
self.logger.info(f"交易对 {self.symbol} 未在INITIAL_PARAMS_JSON中指定初始基准价")
self.base_price = await self._get_latest_price()
self.logger.info(f"使用实时价格作为基准价: {self.base_price}")
else:
self.logger.info(f"使用配置的基准价: {self.base_price}")
if self.base_price is None:
raise ValueError("无法获取当前价格")
self.logger.info(f"初始化完成 | 交易对: {self.symbol} | 基准价: {self.base_price}")
# 发送启动通知
threshold = FLIP_THRESHOLD(self.grid_size) # 计算实际阈值
send_pushplus_message(
f"网格交易启动成功\n"
f"交易对: {self.symbol}\n"
f"基准价: {self.base_price} {self.quote_asset}\n"
f"网格大小: {self.grid_size}%\n"
f"触发阈值: {threshold * 100}% (网格大小的1/5)"
)
# 添加市场价对比
market_price = await self._get_latest_price()
price_diff = (market_price - self.base_price) / self.base_price * 100
self.logger.info(
f"市场当前价: {market_price:.4f} | "
f"价差: {price_diff:+.2f}%"
)
# 启动时合并最近成交,不覆盖本地历史
await self._sync_recent_trades(limit=50)
self.initialized = True
except Exception as e:
self.initialized = False
self.logger.error(f"初始化失败: {str(e)}")
# 发送错误通知
send_pushplus_message(
f"网格交易启动失败\n"
f"错误信息: {str(e)}",
"错误通知"
)
raise
async def _get_latest_price(self):
try:
ticker = await self.exchange.fetch_ticker(self.symbol)
if ticker and 'last' in ticker:
return ticker['last']
self.logger.error("获取价格失败: 返回数据格式不正确")
return self.base_price
except Exception as e:
self.logger.error(f"获取最新价格失败: {str(e)}")
return self.base_price
def _get_upper_band(self):
return self.base_price * (1 + self.grid_size / 100)
def _get_lower_band(self):
return self.base_price * (1 - self.grid_size / 100)
def _reset_extremes(self):
"""
清空上一轮监测记录的最高价 / 最低价,防止残留值
引发虚假“反弹/回撤”判定
"""
if self.highest is not None or self.lowest is not None:
self.logger.debug(
f"复位 high/low 变量 | highest={self.highest} lowest={self.lowest}"
)
self.highest = None
self.lowest = None
async def _sync_recent_trades(self, limit: int = 50):
"""
启动同步:
1) 把交易所最近 N 条 fill 聚合为整单;
2) cost < MIN_TRADE_AMOUNT 的跳过;
3) 用聚合结果覆盖本地同 id 旧记录,然后保存。
"""
try:
latest_fills = await self.exchange.fetch_my_trades(self.symbol, limit=limit)
if not latest_fills:
self.logger.info("启动同步:未获取到任何成交记录")
return
# ---------- 聚合 ----------
aggregated: dict[str, dict] = {}
for tr in latest_fills:
oid = tr.get('order') or tr.get('orderId')
if not oid: # 无 orderId 的利息 / 返佣跳过
continue
price = float(tr.get('price', 0))
amount = float(tr.get('amount', 0))
cost = float(tr.get('cost') or price * amount)
entry = aggregated.setdefault(
oid,
{'timestamp': tr['timestamp'] / 1000,
'side': tr['side'],
'amount': 0.0,
'cost': 0.0}
)
entry['amount'] += amount
entry['cost'] += cost
entry['timestamp'] = min(entry['timestamp'], tr['timestamp'] / 1000)
# ---------- 本地字典 ----------
local = {t['order_id']: t for t in self.order_tracker.trade_history}
# ---------- 覆盖写入 ----------
for oid, info in aggregated.items():
avg_price = info['cost'] / info['amount']
local[oid] = { # 直接覆盖或新增
'timestamp': info['timestamp'],
'side': info['side'],
'price': avg_price,
'amount': info['amount'],
'order_id': oid,
'profit': 0
}
# ---------- 保存 ----------
merged = sorted(local.values(), key=lambda x: x['timestamp'])
self.order_tracker.trade_history = merged
self.order_tracker.save_trade_history()
self.logger.info(f"启动同步:本地历史共 {len(merged)} 条记录")
except Exception as e:
self.logger.error(f"同步最近成交失败: {e}")
async def _check_buy_signal(self):
current_price = self.current_price
initial_lower_band = self._get_lower_band()
if current_price <= initial_lower_band:
# --- START OF CORRECTION ---
self.is_monitoring_buy = True
old_lowest = self.lowest if self.lowest is not None else float('inf')
# 正确的逻辑:self.lowest 只能减小,不能增加
self.lowest = current_price if self.lowest is None else min(self.lowest, current_price)
# 只有在最低价确实被刷新(降低)时,才打印日志
if self.lowest < old_lowest:
threshold = FLIP_THRESHOLD(self.grid_size)
self.logger.info(
f"买入监测 | "
f"当前价: {current_price:.2f} | "
f"触发价: {initial_lower_band:.5f} | "
f"最低价: {self.lowest:.2f} (已更新) | "
f"反弹阈值: {threshold * 100:.2f}%"
)
# --- END OF CORRECTION ---
# 触发买入的逻辑保持不变
threshold = FLIP_THRESHOLD(self.grid_size)
if self.lowest and current_price >= self.lowest * (1 + threshold):
self.is_monitoring_buy = False # 准备交易,退出监测
self.logger.info(
f"触发买入信号 | 当前价: {current_price:.2f} | 已反弹: {(current_price / self.lowest - 1) * 100:.2f}%")
# 只返回价格条件是否满足,余额检查在execute_order中进行
return True
else:
# 只有当价格回升,并且我们之前正处于"买入监测"状态时,才重置
if self.is_monitoring_buy:
self.logger.info(f"价格已回升至 {current_price:.2f},高于下轨 {initial_lower_band:.2f}。重置买入监测状态。")
self.is_monitoring_buy = False
self._reset_extremes()
return False
async def _check_sell_signal(self):
current_price = self.current_price
initial_upper_band = self._get_upper_band()
if current_price >= initial_upper_band:
# --- START OF CORRECTION ---
# 无论如何,先进入监测状态
self.is_monitoring_sell = True
# 使用一个临时变量来记录旧的最高价,方便对比
old_highest = self.highest if self.highest is not None else 0.0
# 正确的逻辑:self.highest 只能增加,不能减少
self.highest = current_price if self.highest is None else max(self.highest, current_price)
# 只有在最高价确实被刷新(提高)时,才打印日志
if self.highest > old_highest:
threshold = FLIP_THRESHOLD(self.grid_size)
dynamic_trigger_price = self.highest * (1 - threshold)
self.logger.info(
f"卖出监测 | "
f"当前价: {current_price:.2f} | "
f"触发价(动态): {dynamic_trigger_price:.5f} | "
f"最高价: {self.highest:.2f} (已更新)"
)
# --- END OF CORRECTION ---
# 触发卖出的逻辑保持不变
threshold = FLIP_THRESHOLD(self.grid_size)
if self.highest and current_price <= self.highest * (1 - threshold):
self.is_monitoring_sell = False # 准备交易,退出监测
self.logger.info(
f"触发卖出信号 | 当前价: {current_price:.2f} | 目标价: {self.highest * (1 - threshold):.5f} | 已下跌: {(1 - current_price / self.highest) * 100:.2f}%")
# 只返回价格条件是否满足,余额检查在execute_order中进行
return True
else:
# 只有当价格回落,并且我们之前正处于"卖出监测"状态时,才意味着本次机会结束,可以重置了
if self.is_monitoring_sell:
self.logger.info(f"价格已回落至 {current_price:.2f},低于上轨 {initial_upper_band:.2f}。重置卖出监测状态。")
self.is_monitoring_sell = False
self._reset_extremes()
return False
async def _calculate_order_amount(self, order_type):
"""计算目标订单金额 (总资产的10%)\n"""
try:
current_time = time.time()
# 使用缓存避免频繁计算和日志输出
cache_key = f'order_amount_target' # 使用不同的缓存键
if hasattr(self, cache_key) and \
current_time - getattr(self, f'{cache_key}_time') < 60: # 1分钟缓存
return getattr(self, cache_key)
total_assets = await self._get_pair_specific_assets_value()
# 目标金额严格等于总资产的10%
amount = total_assets * 0.1
# 只在金额变化超过1%时记录日志
# 使用 max(..., 0.01) 避免除以零错误
if not hasattr(self, f'{cache_key}_last') or \
abs(amount - getattr(self, f'{cache_key}_last', 0)) / max(getattr(self, f'{cache_key}_last', 0.01),
0.01) > 0.01:
self.logger.info(
f"目标订单金额计算 | "
f"交易对相关资产: {total_assets:.2f} {self.quote_asset} | "
f"计算金额 (10%): {amount:.2f} {self.quote_asset}"
)
setattr(self, f'{cache_key}_last', amount)
# 更新缓存
setattr(self, cache_key, amount)
setattr(self, f'{cache_key}_time', current_time)
return amount
except Exception as e:
self.logger.error(f"计算目标订单金额失败: {str(e)}")
# 返回一个合理的默认值或上次缓存值,避免返回0导致后续计算错误
return getattr(self, cache_key, 0) # 如果缓存存在则返回缓存,否则返回0
async def get_available_balance(self, currency):
balance = await self.exchange.fetch_balance({'type': 'spot'})
return balance.get('free', {}).get(currency, 0) * settings.SAFETY_MARGIN
async def _calculate_dynamic_interval_seconds(self):
"""根据波动率动态计算网格调整的时间间隔(秒)"""
try:
volatility = await self._calculate_volatility()
if volatility is None: # Handle case where volatility calculation failed
raise ValueError("波动率计算失败") # Volatility calculation failed
interval_rules = TradingConfig.DYNAMIC_INTERVAL_PARAMS['volatility_to_interval_hours']
default_interval_hours = TradingConfig.DYNAMIC_INTERVAL_PARAMS['default_interval_hours']
matched_interval_hours = default_interval_hours # Start with default
for rule in interval_rules:
vol_range = rule['range']
# Check if volatility falls within the defined range [min, max)
if vol_range[0] <= volatility < vol_range[1]:
matched_interval_hours = rule['interval_hours']
self.logger.debug(
f"动态间隔匹配: 波动率 {volatility:.4f} 在范围 {vol_range}, 间隔 {matched_interval_hours} 小时") # Dynamic interval match
break # Stop after first match
interval_seconds = matched_interval_hours * 3600
# Add a minimum interval safety check
min_interval_seconds = 5 * 60 # Example: minimum 5 minutes
final_interval_seconds = max(interval_seconds, min_interval_seconds)
self.logger.debug(
f"计算出的动态调整间隔: {final_interval_seconds:.0f} 秒 ({final_interval_seconds / 3600:.2f} 小时)") # Calculated dynamic adjustment interval
return final_interval_seconds
except Exception as e:
self.logger.error(
f"计算动态调整间隔失败: {e}, 使用默认间隔。") # Failed to calculate dynamic interval, using default.
# Fallback to default interval from config
default_interval_hours = TradingConfig.DYNAMIC_INTERVAL_PARAMS.get('default_interval_hours', 1.0)
return default_interval_hours * 3600
async def main_loop(self):
consecutive_errors = 0
max_consecutive_errors = 5
while True:
try:
# ------------------------------------------------------------------
# 阶段一:初始化与状态更新
# ------------------------------------------------------------------
if not self.initialized:
await self.initialize()
# 获取最新的价格,这是后续所有决策的基础
current_price = await self._get_latest_price()
if not current_price:
await asyncio.sleep(5)
continue
self.current_price = current_price
# ========== 新增:获取本轮循环的统一账户快照 ==========
spot_balance = await self.exchange.fetch_balance()
funding_balance = await self.exchange.fetch_funding_balance()
# ========== 新增结束 ==========
# --- 核心理念:维护任务与交易任务分离 ---
# ------------------------------------------------------------------
# 阶段二:周期性维护模块 (始终运行,保证机器人认知更新)
# ------------------------------------------------------------------
# 1. 更新S1策略的每日高低点
await self.position_controller_s1.update_daily_s1_levels()
# 2. 检查是否需要调整网格大小 (包含波动率计算)
# 这个任务现在独立运行,不再被交易状态阻塞
dynamic_interval_seconds = await self._calculate_dynamic_interval_seconds()
if time.time() - self.last_grid_adjust_time > dynamic_interval_seconds:
self.logger.info(
f"维护时间到达,准备更新波动率并调整网格 (间隔: {dynamic_interval_seconds / 3600:.2f} 小时).")
# adjust_grid_size 内部会调用 _calculate_volatility
await self.adjust_grid_size()
self.last_grid_adjust_time = time.time() # 更新时间戳
# ------------------------------------------------------------------
# 阶段三:交易决策模块 (根据风控和市场信号执行)
# ------------------------------------------------------------------
# 1. 【核心】首先获取唯一的风控许可
risk_state = await self.risk_manager.check_position_limits(spot_balance, funding_balance)
# 2. 定义标志位,确保一轮循环只做一次主网格交易
trade_executed_this_loop = False
# 3. 卖出逻辑:只有在风控允许的情况下,才去检查信号
if risk_state != RiskState.ALLOW_BUY_ONLY:
sell_signal = await self._check_signal_with_retry(
lambda: self._check_sell_signal(), "卖出检测")
if sell_signal:
if await self.execute_order('sell'):
trade_executed_this_loop = True
# 4. 买入逻辑:如果没卖出,且风控允许,才去检查买入信号
if not trade_executed_this_loop and risk_state != RiskState.ALLOW_SELL_ONLY:
buy_signal = await self._check_signal_with_retry(
lambda: self._check_buy_signal(), "买入检测")
if buy_signal:
if await self.execute_order('buy'):
trade_executed_this_loop = True
# 5. S1辅助策略:它也是一种交易,但独立于主网格
# 只有在本轮没有发生主网格交易时才考虑执行S1,避免冲突
if not trade_executed_this_loop:
await self.position_controller_s1.check_and_execute(risk_state)
# --- 逻辑执行完毕 ---
# 循环成功,重置错误计数器
consecutive_errors = 0
await asyncio.sleep(5) # 主循环的固定休眠时间
except Exception as e:
consecutive_errors += 1
self.logger.error(f"主循环发生错误 (第{consecutive_errors}次连续失败): {e}", exc_info=True)
if consecutive_errors >= max_consecutive_errors:
fatal_msg = (
f"交易对[{self.symbol}]连续失败 {max_consecutive_errors} 次,任务已自动停止!\n"
f"最后一次错误: {str(e)}"
)
self.logger.critical(fatal_msg)
try:
from helpers import send_pushplus_message
send_pushplus_message(fatal_msg, f"!!!系统致命错误 - {self.symbol}!!!")
except Exception as notify_error:
self.logger.error(f"发送紧急通知失败: {notify_error}")
break # 退出循环,结束此交易对的任务
await asyncio.sleep(30) # 发生错误后等待30秒重试
async def _check_signal_with_retry(self, check_func, check_name, max_retries=3, retry_delay=2):
"""带重试机制的信号检测函数
Args:
check_func: 要执行的检测函数 (_check_buy_signal 或 _check_sell_signal)
check_name: 检测名称,用于日志
max_retries: 最大重试次数
retry_delay: 重试间隔(秒)
Returns:
bool: 检测结果
"""
retries = 0
while retries <= max_retries:
try:
return await check_func()
except Exception as e:
retries += 1
if retries <= max_retries:
self.logger.warning(f"{check_name}出错,{retry_delay}秒后进行第{retries}次重试: {str(e)}")
await asyncio.sleep(retry_delay)
else:
self.logger.error(f"{check_name}失败,达到最大重试次数({max_retries}次): {str(e)}")
return False
return False
async def _ensure_trading_funds(self):
"""确保现货账户有足够的交易资金"""
try:
balance = await self.exchange.fetch_balance()
current_price = self.current_price
# 计算所需资金
required_quote = settings.MIN_TRADE_AMOUNT * 2 # 保持两倍最小交易额
required_base = required_quote / current_price
# 获取现货余额
spot_quote = float(balance['free'].get(self.quote_asset, 0))
spot_base = float(balance['free'].get(self.base_asset, 0))
# 一次性检查和赎回所需资金
transfers = []
if spot_quote < required_quote:
transfers.append({
'asset': self.quote_asset,
'amount': required_quote - spot_quote
})
if spot_base < required_base:
transfers.append({
'asset': self.base_asset,
'amount': required_base - spot_base
})
# 如果需要赎回,一次性执行所有赎回操作
if transfers:
self.logger.info("开始资金赎回操作...")
for transfer in transfers:
self.logger.info(f"从理财赎回 {transfer['amount']:.8f} {transfer['asset']}")
await self.exchange.transfer_to_spot(transfer['asset'], transfer['amount'])
self.logger.info("资金赎回完成")
# 等待资金到账
await asyncio.sleep(2)
except Exception as e:
self.logger.error(f"资金检查和划转失败: {str(e)}")
async def emergency_stop(self):
try:
open_orders = await self.exchange.fetch_open_orders(self.symbol)
for order in open_orders:
await self.exchange.cancel_order(order['id'])
send_pushplus_message("程序紧急停止", "系统通知")
self.logger.critical("所有交易已停止,进入复盘程序")
except Exception as e:
self.logger.error(f"紧急停止失败: {str(e)}")
send_pushplus_message(f"程序异常停止: {str(e)}", "错误通知")
finally:
await self.exchange.close()
exit()
async def _handle_filled_order(
self,
order_dict: dict,
side: str,
retry_count: int,
max_retries: int
):
"""
对已成交订单进行统一后续处理:更新基准价、复位 high/low、
记录交易、推送通知、资金转移。
"""
order_price = float(order_dict['price'])
order_amount = float(order_dict['filled'])
order_id = order_dict['id']
# 1) 更新基准价并复位最高/最低
self.base_price = order_price
self._reset_extremes()
# 2) 清除活跃订单
self.active_orders[side] = None
# 3) 记录交易
trade_info = {
'timestamp': time.time(),
'side': side,
'price': order_price,
'amount': order_amount,
'order_id': order_id
}
self.order_tracker.add_trade(trade_info)
# 4) 更新时间戳 / 总资产
self.last_trade_time = time.time()
self.last_trade_price = order_price
await self._update_total_assets()
self.logger.info(f"基准价已更新: {self.base_price}")
# 保存状态
self._save_state()
# 5) 推送通知
msg = format_trade_message(
side='buy' if side == 'buy' else 'sell',
symbol=self.symbol,
price=order_price,
amount=order_amount,
total=order_price * order_amount,
grid_size=self.grid_size,
base_asset=self.base_asset,
quote_asset=self.quote_asset,
retry_count=(retry_count + 1, max_retries)
)
send_pushplus_message(msg, "交易成功通知")
# 6) 将多余资金转入理财 (如果功能开启)
if settings.ENABLE_SAVINGS_FUNCTION:
await self._transfer_excess_funds()
else:
self.logger.info("理财功能已禁用,跳过资金转移。")
return order_dict
async def execute_order(self, side):
"""执行订单,带重试机制"""
max_retries = 10 # 最大重试次数
retry_count = 0
check_interval = 3 # 下单后等待检查时间(秒)
while retry_count < max_retries:
try:
# 获取最新订单簿数据
order_book = await self.exchange.fetch_order_book(self.symbol, limit=5)
if not order_book or not order_book.get('asks') or not order_book.get('bids'):
self.logger.error("获取订单簿数据失败或数据不完整")
retry_count += 1
await asyncio.sleep(3)
continue
# 使用买1/卖1价格
if side == 'buy':
order_price = order_book['asks'][0][0] # 卖1价买入
else:
order_price = order_book['bids'][0][0] # 买1价卖出
# 计算交易数量
amount_quote = await self._calculate_order_amount(side)
amount = self._adjust_amount_precision(amount_quote / order_price)
# 调整价格精度
order_price = self._adjust_price_precision(order_price)
# 检查余额是否足够 - 需要获取最新的余额信息
spot_balance = await self.exchange.fetch_balance({'type': 'spot'})
funding_balance = await self.exchange.fetch_funding_balance()
if not await self._ensure_balance_for_trade(side, spot_balance, funding_balance):
self.logger.warning(f"{side}余额不足,第 {retry_count + 1} 次尝试中止")
return False
# 为了日志记录,将字符串类型的 amount 临时转为浮点数
log_display_amount = float(amount)
self.logger.info(
f"尝试第 {retry_count + 1}/{max_retries} 次 {side} 单 | "
f"价格: {order_price} | "
f"金额: {amount_quote:.2f} {self.quote_asset} | "
f"数量: {log_display_amount:.8f} {self.base_asset}"
)
# 创建订单
order = await self.exchange.create_order(
self.symbol,
'limit',
side,
amount,
order_price
)
# 更新活跃订单状态
order_id = order['id']
self.active_orders[side] = order_id
self.order_tracker.add_order(order)
# 等待指定时间后检查订单状态
self.logger.info(f"订单已提交,等待 {check_interval} 秒后检查状态")
await asyncio.sleep(check_interval)
# 检查订单状态
updated_order = await self.exchange.fetch_order(order_id, self.symbol)
# 订单已成交
if updated_order['status'] == 'closed':
self.logger.info(f"订单已成交 | ID: {order_id}")
return await self._handle_filled_order(
updated_order, side, retry_count, max_retries
)
# 如果订单未成交,取消订单并重试
self.logger.warning(f"订单未成交,尝试取消 | ID: {order_id} | 状态: {updated_order['status']}")
try:
await self.exchange.cancel_order(order_id, self.symbol)
self.logger.info(f"订单已取消,准备重试 | ID: {order_id}")
except Exception as e:
# 如果取消订单时出错,检查是否已成交
self.logger.warning(f"取消订单时出错: {str(e)},再次检查订单状态")
try:
check_order = await self.exchange.fetch_order(order_id, self.symbol)
if check_order['status'] == 'closed':
self.logger.info(f"订单已经成交 | ID: {order_id}")
return await self._handle_filled_order(
check_order, side, retry_count, max_retries
)
except Exception as check_e:
self.logger.error(f"检查订单状态失败: {str(check_e)}")
# 清除活跃订单状态
self.active_orders[side] = None
# 增加重试计数
retry_count += 1
# 如果还有重试次数,等待一秒后继续
if retry_count < max_retries:
self.logger.info(f"等待1秒后进行第 {retry_count + 1} 次尝试")
await asyncio.sleep(1)
except Exception as e:
self.logger.error(f"执行{side}单失败: {str(e)}")
# 尝试清理可能存在的订单
if 'order_id' in locals() and self.active_orders.get(side) == order_id:
try:
await self.exchange.cancel_order(order_id, self.symbol)
self.logger.info(f"已取消错误订单 | ID: {order_id}")
except Exception as cancel_e:
self.logger.error(f"取消错误订单失败: {str(cancel_e)}")
finally:
self.active_orders[side] = None
# 增加重试计数
retry_count += 1
# 如果是关键错误,停止重试
if "资金不足" in str(e) or "Insufficient" in str(e):
self.logger.error("资金不足,停止重试")
# 发送错误通知
error_message = f"""❌ 交易失败
━━━━━━━━━━━━━━━━━━━━
🔍 类型: {side} 失败
📊 交易对: {self.symbol}
⚠️ 错误: 资金不足
"""
send_pushplus_message(error_message, "交易错误通知")
return False
# 如果还有重试次数,稍等后继续
if retry_count < max_retries:
self.logger.info(f"等待2秒后进行第 {retry_count + 1} 次尝试")
await asyncio.sleep(2)
# 达到最大重试次数后仍未成功
if retry_count >= max_retries:
self.logger.error(f"{side}单执行失败,达到最大重试次数: {max_retries}")
error_message = f"""❌ 交易失败
━━━━━━━━━━━━━━━━━━━━
🔍 类型: {side} 失败
📊 交易对: {self.symbol}
⚠️ 错误: 达到最大重试次数 {max_retries} 次
"""
send_pushplus_message(error_message, "交易错误通知")
return False
async def _wait_for_balance(self, side, amount, price):
"""等待直到有足够的余额可用"""
max_attempts = 10
for i in range(max_attempts):
balance = await self.exchange.fetch_balance()
if side == 'buy':
required = amount * price
available = float(balance['free'].get(self.quote_asset, 0))
if available >= required:
return True
else:
available = float(balance['free'].get(self.base_asset, 0))
if available >= amount:
return True
self.logger.info(f"等待资金到账 ({i + 1}/{max_attempts})...")
await asyncio.sleep(1)
raise Exception("等待资金到账超时")
async def _adjust_grid_after_trade(self):
"""根据市场波动动态调整网格大小"""
trade_count = self.order_tracker.trade_count
if trade_count % TradingConfig.GRID_PARAMS.get('adjust_interval', 5) == 0:
volatility = await self._calculate_volatility()
# 根据波动率调整
high_threshold = TradingConfig.GRID_PARAMS.get('volatility_threshold', {}).get('high', 0.3)
if volatility > high_threshold:
new_size = min(
self.grid_size * 1.1, # 扩大10%
TradingConfig.GRID_PARAMS['max']
)
action = "扩大"
else:
new_size = max(
self.grid_size * 0.9, # 缩小10%
TradingConfig.GRID_PARAMS['min']
)
action = "缩小"
# 建议改进:添加趋势判断
price_trend = self._get_price_trend() # 获取价格趋势(1小时)
if price_trend > 0: # 上涨趋势
new_size *= 1.05 # 额外增加5%
elif price_trend < 0: # 下跌趋势
new_size *= 0.95 # 额外减少5%
self.grid_size = new_size
self.logger.info(
f"动态调整网格 | 操作: {action} | "
f"波动率: {volatility:.2%} | "
f"新尺寸: {self.grid_size:.2f}%"
)