-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmarket_maker.py
More file actions
1162 lines (982 loc) · 46.7 KB
/
Copy pathmarket_maker.py
File metadata and controls
1162 lines (982 loc) · 46.7 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 argparse
import asyncio
import os
import signal
import time
from datetime import datetime
from zoneinfo import ZoneInfo
# 在导入其他模块之前:先解析参数和加载 .env
from dotenv import load_dotenv
parser = argparse.ArgumentParser(description='StandX 做市机器人')
parser.add_argument('--config', type=str, default='.env',
help='配置文件路径 (默认: .env)')
parser.add_argument('--log-prefix', type=str, default='',
help='日志文件前缀 (默认: 空)')
args = parser.parse_args()
# 加载环境变量(必须在其他模块导入之前)
load_dotenv(args.config)
from adapter.standx_adapter import StandXAdapter
from standx_auth import StandXAuth
import standx_api as api
from notifier import Notifier
from logger import get_logger, configure_logging
class MarketMaker:
"""双向限价单做市器"""
def __init__(
self,
auth: StandXAuth,
symbol: str,
qty: str,
target_bps: float = 7.5,
min_bps: float = 7.0,
max_bps: float = 10,
notifier: Notifier = None,
exchange_adapter: StandXAdapter = None,
account_name: str = None,
):
"""
初始化做市器
Args:
auth: 认证后的StandXAuth实例
symbol: 交易对
qty: 订单数量(字符串格式)
target_bps: 目标挂单偏离(basis points,默认7.5,用于初始下单)
min_bps: 最小允许偏离(默认7.0,低于此值重新挂单)
max_bps: 最大允许偏离(默认10,超过此值重新挂单)
balance_threshold_1: 余额阈值1-手续费容忍阈值(默认100 USDT,低于此进入降级模式1)
balance_threshold_2: 余额阈值2-止损阈值(默认50 USDT,低于此进入降级模式2)
force_degraded_on_us_open: 美股开盘时间是否强制降级模式2(默认False)
notifier: 通知器实例(可选,默认从环境变量创建)
"""
self.auth = auth
self.symbol = symbol
self.qty = qty
self.exchange_adapter = exchange_adapter
self.account_name = account_name or "default"
# 通知器
self.notifier = notifier or Notifier.from_env()
# 挂单参数(静态)
self.target_bps = target_bps
self.min_bps = min_bps
self.max_bps = max_bps
self.leverage = 40 # 杠杆倍数
self.margin_mode = "isolated" # 单仓模式
# 优雅关闭相关
self._shutdown_requested = False
self._shutdown_event = asyncio.Event()
# 风险评估平滑与迟滞
self._risk_ema = None # 风险分数EMA(指数移动平均)
self._risk_ema_alpha = float(os.getenv("RISK_EMA_ALPHA", "0.3")) # EMA平滑系数
self._current_risk_level = "medium" # 当前风险等级(low/medium/high)
# 持仓管理参数(分层止盈止损)
self._position_quick_tp_bps = float(os.getenv("POSITION_QUICK_TP_BPS", "1.5")) # 一级止盈点数
self._position_hold_seconds = float(os.getenv("POSITION_HOLD_SECONDS", "3")) # 持仓等待时间
self._position_force_exit_bps = float(os.getenv("POSITION_FORCE_EXIT_BPS", "5")) # 二级强制止盈点数
self._position_stop_loss_bps = float(os.getenv("POSITION_STOP_LOSS_BPS", "4")) # 止损点数
self._max_position_hold_time = float(os.getenv("MAX_POSITION_HOLD_TIME", "15")) # 最大持仓时间(秒)
# 持仓跟踪状态
self._tracked_position = None # 当前跟踪的持仓对象
self._tp_order_id = None # 止盈单ID
self._sl_order_id = None # 止损单ID
self._position_entry_time = None # 持仓入场时间
# 余额汇报参数
self._balance_report_interval = float(os.getenv("BALANCE_REPORT_INTERVAL", "1800")) # 默认每半小时汇报一次(秒)
# 余额退出阈值(平仓后如果余额低于此值则优雅退出)
self._balance_exit_threshold = float(os.getenv("BALANCE_EXIT_THRESHOLD", "10")) # 默认10 USDT
# 获取 logger 实例
self.logger = get_logger(__name__)
def _setup_signal_handlers(self):
"""设置信号处理器以支持优雅关闭"""
def handle_signal(signum, frame):
self.logger.info("收到信号 %s,准备优雅关闭...", signum)
self._shutdown_requested = True
self._shutdown_event.set()
signal.signal(signal.SIGTERM, handle_signal)
signal.signal(signal.SIGINT, handle_signal)
def _get_price_precision(self) -> int:
"""
根据交易对获取价格精度
Returns:
int: 小数位数 (XAU-USD: 1, 其他: 2)
"""
if "XAU" in self.symbol:
return 1 # XAU-USD 精度 0.1
else:
return 2 # 其他(如 BTC-USD)精度 0.01
def calculate_order_prices(self, market_price: float) -> tuple:
"""
计算双向订单价格
Args:
market_price: 当前市场价格
Returns:
(buy_price, sell_price) 买单价格和卖单价格
"""
buy_price = market_price * (1 - self.target_bps / 10000)
sell_price = market_price * (1 + self.target_bps / 10000)
# 根据交易对精度进行四舍五入
precision = self._get_price_precision()
buy_price = round(buy_price, precision)
sell_price = round(sell_price, precision)
return (buy_price, sell_price)
def calculate_market_risk(self) -> tuple[float, str]:
"""
计算市场风险等级(基于盘口压力)
改进:深度指标改为相对指标,避免低价资产(XAG)被放大
Returns:
(risk_score, description) 风险分数 0-100 和描述
"""
# 资产类别风险系数配置(考虑不同市场的波动特性)
asset_risk_multipliers = {
"XAU": 0.5, # 黄金:低波动贵金属
"XAG": 0.5, # 白银:低波动贵金属
"BTC": 1.0, # 比特币:高波动加密货币
"ETH": 1.0, # 以太坊:高波动加密货币
}
# 根据交易对识别资产类别
asset_multiplier = 1.0 # 默认系数
for asset_code, multiplier in asset_risk_multipliers.items():
if asset_code in self.symbol:
asset_multiplier = multiplier
break
depth_data = self.exchange_adapter.get_depth_book_data()
if not depth_data:
return 50.0, "数据不足"
bids = depth_data.get("bids", [])
asks = depth_data.get("asks", [])
if len(bids) < 5 or len(asks) < 5:
return 50.0, "深度不足"
mid_price = self.exchange_adapter.get_depth_mid_price()
if not mid_price:
return 50.0, "价格缺失"
# 1. 计算买卖盘口价差(相对值)
best_bid = float(bids[0][0])
best_ask = float(asks[0][0])
spread_bps = (best_ask - best_bid) / mid_price * 10000
# 2. 计算前5档买卖量比
bid_volume = sum(float(b[1]) for b in bids[:5])
ask_volume = sum(float(a[1]) for a in asks[:5])
volume_ratio = min(bid_volume, ask_volume) / max(bid_volume, ask_volume) if max(bid_volume, ask_volume) > 0 else 0.5
# 3. 改进:计算盘口深度作为"稀疏度"指标(归一化为bps相对指标)
# 将价格跨度归一化为bps,避免不同价格区间资产(XAU $2800 vs BTC $50000)评估不公平
if len(bids) >= 10 and len(asks) >= 10:
bid_price_range = float(bids[0][0]) - float(bids[9][0])
ask_price_range = float(asks[9][0]) - float(asks[0][0])
# 将10档跨度归一化为相对于中间价的bps
bid_range_bps = (bid_price_range / mid_price) * 10000 if mid_price > 0 else 0
ask_range_bps = (ask_price_range / mid_price) * 10000 if mid_price > 0 else 0
total_range_bps = (bid_range_bps + ask_range_bps) / 2
# 稀疏度 = 价差bps / 跨度bps 的比例
# 这个比例反映了"价差相对于盘口深度的大小",与绝对价格无关
if total_range_bps > 0:
sparsity_ratio = spread_bps / total_range_bps
# 归一化到 0-50 范围,正常市场稀疏度比例约为 0.01-0.5
# 使用对数缩放避免极端值,sparsity_ratio=0.1 -> depth_sparsity≈10
depth_sparsity = min(sparsity_ratio * 100, 50)
else:
depth_sparsity = 25 # 保护性默认值
else:
depth_sparsity = 25 # 深度不足时的默认值
# 综合评分(0-100,越高越危险)
# 价差大 -> 风险高(权重:1.0,绝对价差bps)
# 买卖不平衡 -> 风险高(权重:25,盘口压力)
# 盘口稀疏(价差相对于深度跨度大) -> 风险高(权重:0.5,相对深度)
#
# 权重调整逻辑:
# - spread_bps:绝对价差是直接成本,保持权重1.0
# - volume_ratio:买卖不平衡是主要风险,保持权重25
# - depth_sparsity:归一化后降低权重到0.5,避免XAU等中价资产被过度惩罚
risk_score = (
spread_bps * 1.0 + # 价差权重(绝对成本)
(1 - volume_ratio) * 25 + # 不平衡度权重(主要风险)
depth_sparsity * 0.5 # 深度稀疏度权重(归一化后降低)
)
risk_score = max(0, min(100, risk_score))
# 应用资产类别系数
risk_score *= asset_multiplier
risk_score = max(0, min(100, risk_score))
# EMA平滑风险分数,减少短期波动
if self._risk_ema is None:
self._risk_ema = risk_score
else:
self._risk_ema = self._risk_ema_alpha * risk_score + (1 - self._risk_ema_alpha) * self._risk_ema
smoothed_score = self._risk_ema
self.logger.debug(
"风险计算详情: spread_bps=%.2f, volume_ratio=%.2f, depth_sparsity=%.2f, multiplier=%.2f, raw_score=%.1f, ema_score=%.1f",
spread_bps, volume_ratio, depth_sparsity, asset_multiplier, risk_score, smoothed_score
)
desc = f"价差:{spread_bps:.1f}bps 量比:{volume_ratio:.2f} 稀疏度:{depth_sparsity:.1f} 系数:{asset_multiplier:.1f}"
return smoothed_score, desc
def get_adaptive_bps(self) -> tuple[float, float, str]:
"""
根据市场风险动态调整挂单偏离(带迟滞阈值)
Returns:
(target_bps, min_bps, reason) 目标偏离、最小偏离、决策原因
"""
# 计算市场风险(已EMA平滑)
risk_score, risk_desc = self.calculate_market_risk()
# 使用迟滞阈值防止频繁切换
# 当前状态决定切换阈值(上升阈值 > 下降阈值)
new_level = self._current_risk_level
if self._current_risk_level == "low":
# 低风险状态:需要 ≥25 才升到中风险
if risk_score >= 25:
new_level = "medium"
elif self._current_risk_level == "medium":
# 中风险状态:<20 降到低风险,≥55 升到高风险
if risk_score < 20:
new_level = "low"
elif risk_score >= 55:
new_level = "high"
else: # high
# 高风险状态:需要 <45 才降到中风险
if risk_score < 45:
new_level = "medium"
# 更新当前等级
self._current_risk_level = new_level
# 根据风险等级决定挂单策略
if new_level == "low":
target_bps = 9.0
min_bps = 8.0
max_bps = 10.0
reason = f"低风险({risk_score:.0f})"
elif new_level == "medium":
target_bps = 25.0
min_bps = 20.0
max_bps = 30.0
reason = f"中风险({risk_score:.0f})"
else: # high
target_bps = 80.0
min_bps = 60.0
max_bps = 100.0
reason = f"高风险({risk_score:.0f})"
return target_bps, min_bps, max_bps, f"{reason} - {risk_desc}"
def check_order_count(self) -> tuple[bool, str]:
"""
检查订单数量是否正确
Returns:
(need_replace, reason) 是否需要重挂和原因
"""
if (
self.exchange_adapter.get_buy_order_count() != 1
or self.exchange_adapter.get_sell_order_count() != 1
):
self.logger.info(
"订单数量异常,买单: %d, 卖单: %d",
self.exchange_adapter.get_buy_order_count(),
self.exchange_adapter.get_sell_order_count(),
)
reason = "订单数量异常(非各1单)"
return True, reason
return False, ""
def check_price_deviation(self) -> tuple[bool, str]:
"""
检查订单偏离度是否超过阈值
Returns:
(need_replace, reason) 是否需要重挂和原因
"""
if not (
self.exchange_adapter.get_buy_orders()
and self.exchange_adapter.get_sell_orders()
and not self.exchange_adapter.is_price_updated_and_processed()
):
return False, ""
buy_price = float(self.exchange_adapter.get_buy_orders()[0]["price"])
buy_bps = abs(
(self.exchange_adapter.get_depth_mid_price() - buy_price)
/ self.exchange_adapter.get_depth_mid_price()
* 10000
)
sell_price = float(self.exchange_adapter.get_sell_orders()[0]["price"])
sell_bps = abs(
(sell_price - self.exchange_adapter.get_depth_mid_price())
/ self.exchange_adapter.get_depth_mid_price()
* 10000
)
self.logger.info(
"买单: %.2f (偏离: %.1f bps), 卖单: %.2f (偏离: %.1f bps)",
buy_price,
buy_bps,
sell_price,
sell_bps,
)
if (
buy_bps < self.min_bps
or buy_bps > self.max_bps
or sell_bps < self.min_bps
or sell_bps > self.max_bps
):
reason = f"订单偏离范围异常(买单: {buy_bps:.1f} bps, 卖单: {sell_bps:.1f} bps)"
return True, reason
self.exchange_adapter.mark_price_processed()
return False, ""
async def place_orders(self, market_price: float = None):
"""下双向限价单
Args:
market_price: 市场价格,如果为None则等待最新价格更新
"""
# 如果未提供价格,则等待最新价格更新
if market_price is None:
if await self.exchange_adapter.wait_for_new_price(timeout=2.0):
# 成功等待到新价格
market_price = self.exchange_adapter.get_depth_mid_price()
else:
# 超时则取消下单
self.logger.warning("获取市场价格超时,取消下单")
return
buy_price, sell_price = self.calculate_order_prices(market_price)
precision = self._get_price_precision()
price_format = f"{{:.{precision}f}}"
self.logger.info("下双向限价单 (市价: %.2f)", market_price)
# 下买单
try:
await self.exchange_adapter.new_order(
symbol=self.symbol,
side="buy",
order_type="limit",
qty=self.qty,
price=price_format.format(buy_price),
time_in_force="alo",
reduce_only=False,
margin_mode=self.margin_mode,
leverage=self.leverage,
)
self.logger.info(
"买单: %s @ %s",
self.qty,
price_format.format(buy_price),
)
except Exception as e:
self.logger.exception("买单失败: %s", e)
# 下卖单
try:
await self.exchange_adapter.new_order(
symbol=self.symbol,
side="sell",
order_type="limit",
qty=self.qty,
price=price_format.format(sell_price),
time_in_force="alo",
reduce_only=False,
margin_mode=self.margin_mode,
leverage=self.leverage,
)
self.logger.info(
"卖单: %s @ %s",
self.qty,
price_format.format(sell_price),
)
except Exception as e:
self.logger.exception("卖单失败: %s", e)
async def run(self, check_interval: float = 0.025):
"""
运行做市策略(事件驱动架构)
Args:
check_interval: 保留参数以兼容旧配置,实际使用事件驱动机制
"""
# 设置信号处理器
self._setup_signal_handlers()
beijing_tz = ZoneInfo("Asia/Shanghai")
beijing_time = datetime.now(beijing_tz).strftime("%Y-%m-%d %H:%M:%S")
self.logger.info("双向限价单做市策略启动(事件驱动模式) - %s", beijing_time)
self.logger.info("交易对: %s", self.symbol)
self.logger.info("订单数量: %s", self.qty)
# 启动通知
await self.notifier.send(
f"*做市策略启动*\n"
f"账户: `{self.account_name}`\n"
f"时间: {beijing_time}\n"
f"交易对: `{self.symbol}`\n"
f"数量: {self.qty}\n"
f"模式: 事件驱动\n"
)
# 等待 mid_price 数据就绪(只执行一次)
while self.exchange_adapter.get_depth_mid_price() is None:
self.logger.info("等待行情数据(mid_price)...")
await asyncio.sleep(0.2)
# 创建独立的监控任务
try:
price_check_task = asyncio.create_task(self._price_monitor_loop())
position_check_task = asyncio.create_task(self._position_monitor_loop())
balance_report_task = asyncio.create_task(self._balance_report_loop())
shutdown_task = asyncio.create_task(self._shutdown_event.wait())
# 任意任务结束或收到关闭信号时退出
done, _pending = await asyncio.wait(
[price_check_task, position_check_task, balance_report_task, shutdown_task],
return_when=asyncio.FIRST_COMPLETED,
)
if shutdown_task in done:
self.logger.info("检测到关闭信号,准备停止任务...")
else:
self.logger.warning("监控任务提前结束,触发关闭...")
self._shutdown_requested = True
self._shutdown_event.set()
for task in [price_check_task, position_check_task, balance_report_task]:
if not task.done():
task.cancel()
await asyncio.gather(
price_check_task,
position_check_task,
balance_report_task,
return_exceptions=True,
)
if not shutdown_task.done():
shutdown_task.cancel()
except KeyboardInterrupt:
self.logger.info("收到中断信号,停止策略...")
await self.notifier.send(
f"*策略停止*\n" f"账户: `{self.account_name}`\n" f"交易对: `{self.symbol}`\n" f"原因: 收到中断信号"
)
except Exception as e:
self.logger.exception("策略运行出现严重错误: %s", e)
self.logger.info("正在清理订单并退出...")
await self.notifier.send(
f"*致命异常*\n" f"账户: `{self.account_name}`\n" f"交易对: `{self.symbol}`\n" f"错误: {e}"
)
def _create_position_obj(self, qty: float, side: str, entry_price: float) -> dict:
"""
创建持仓跟踪对象
Args:
qty: 持仓数量
side: 持仓方向 (buy/sell)
entry_price: 入场价格
Returns:
持仓对象字典
"""
return {
"qty": qty,
"side": side,
"entry_price": entry_price,
"entry_time": time.time(),
"tp_placed": False, # 止盈单是否已挂
"sl_placed": False, # 止损单是否已挂
"stage": "entry", # 持仓阶段: entry->hold->tp_timeout->force_exit
}
async def _place_tp_order(self, position: dict) -> bool:
"""
挂一级止盈单(小利润快速退出)
Args:
position: 持仓对象
Returns:
是否成功
"""
if position["tp_placed"]:
return True
try:
qty = str(abs(position["qty"]))
# 根据持仓方向确定止盈方向(对方向)
tp_side = "sell" if position["side"] == "buy" else "buy"
# 计算止盈价格
tp_price = position["entry_price"] * (
1 + self._position_quick_tp_bps / 10000
if position["side"] == "buy"
else 1 - self._position_quick_tp_bps / 10000
)
precision = self._get_price_precision()
tp_price = round(tp_price, precision)
price_format = f"{{:.{precision}f}}"
await self.exchange_adapter.new_order(
symbol=self.symbol,
side=tp_side,
order_type="limit",
qty=qty,
price=price_format.format(tp_price),
time_in_force="gtc",
reduce_only=True,
margin_mode=self.margin_mode,
leverage=self.leverage,
)
position["tp_placed"] = True
self.logger.info(
"✅ 一级止盈单已挂: 数量=%s, 价格=%s (利润: %.1f bps)",
qty, price_format.format(tp_price), self._position_quick_tp_bps
)
return True
except Exception as e:
self.logger.exception("止盈单挂单失败: %s", e)
return False
async def _place_sl_order(self, position: dict) -> bool:
"""
挂止损单(防止亏损扩大)
Args:
position: 持仓对象
Returns:
是否成功
"""
if position["sl_placed"]:
return True
try:
qty = str(abs(position["qty"]))
# 根据持仓方向确定止损方向(对方向)
sl_side = "sell" if position["side"] == "buy" else "buy"
# 计算止损价格
sl_price = position["entry_price"] * (
1 - self._position_stop_loss_bps / 10000
if position["side"] == "buy"
else 1 + self._position_stop_loss_bps / 10000
)
precision = self._get_price_precision()
sl_price = round(sl_price, precision)
price_format = f"{{:.{precision}f}}"
await self.exchange_adapter.new_order(
symbol=self.symbol,
side=sl_side,
order_type="limit",
qty=qty,
price=price_format.format(sl_price),
time_in_force="gtc",
reduce_only=True,
margin_mode=self.margin_mode,
leverage=self.leverage,
)
position["sl_placed"] = True
self.logger.info(
"🛡️ 止损单已挂: 数量=%s, 价格=%s (止损: %.1f bps)",
qty, price_format.format(sl_price), self._position_stop_loss_bps
)
return True
except Exception as e:
self.logger.exception("止损单挂单失败: %s", e)
return False
async def _cancel_tp_sl_orders(self, position: dict):
"""
取消止盈/止损单
Args:
position: 持仓对象
"""
try:
await self.exchange_adapter.cancel_all_orders(symbol=self.symbol)
position["tp_placed"] = False
position["sl_placed"] = False
self.logger.info("止盈/止损单已取消")
except Exception as e:
self.logger.exception("取消止盈/止损单失败: %s", e)
async def _check_balance_and_exit(self):
"""
检查账户余额,如果低于阈值则触发优雅退出
"""
try:
# 等待5秒让平仓订单完全结算
await asyncio.sleep(5.0)
# 查询余额
balance = await api.query_balance(self.auth)
total_balance = float(balance.get("balance", "0"))
equity = float(balance.get("equity", "0"))
self.logger.info(
"平仓后余额检查: 总余额=%.2f, 权益=%.2f, 退出阈值=%.2f",
total_balance, equity, self._balance_exit_threshold
)
# 检查是否低于阈值
if total_balance < self._balance_exit_threshold:
self.logger.warning(
"⚠️ 余额 %.2f 低于退出阈值 %.2f,触发优雅退出",
total_balance, self._balance_exit_threshold
)
# 发送通知
if self.notifier:
beijing_tz = ZoneInfo("Asia/Shanghai")
beijing_time = datetime.now(beijing_tz).strftime("%Y-%m-%d %H:%M:%S")
await self.notifier.send(
f"⚠️ *余额不足,程序退出*\n"
f"账户: `{self.account_name}`\n"
f"时间: {beijing_time}\n"
f"交易对: `{self.symbol}`\n"
f"总余额: ${total_balance:.2f}\n"
f"权益: ${equity:.2f}\n"
f"退出阈值: ${self._balance_exit_threshold:.2f}\n"
f"原因: 平仓后余额低于设定阈值"
)
# 触发优雅关闭
self._shutdown_requested = True
self._shutdown_event.set()
else:
self.logger.info("✅ 余额充足,继续运行")
except Exception as e:
self.logger.exception("余额检查失败: %s", e)
# 余额查询失败不应该导致程序退出,只记录错误
async def _market_close_position(self, position: dict) -> bool:
"""
市价平仓
Args:
position: 持仓对象
Returns:
是否成功
"""
try:
qty = str(abs(position["qty"]))
close_side = "sell" if position["side"] == "buy" else "buy"
await self.exchange_adapter.new_order(
symbol=self.symbol,
side=close_side,
order_type="market",
qty=qty,
time_in_force="ioc",
reduce_only=True,
)
self.logger.info("🔴 市价平仓已执行: 数量=%s", qty)
# 平仓后检查余额
await self._check_balance_and_exit()
return True
except Exception as e:
self.logger.exception("市价平仓失败: %s", e)
return False
async def _price_monitor_loop(self):
"""
价格监控循环 - 仅在价格变化时触发检查
使用事件驱动机制 + 自适应挂单策略
"""
self.logger.info("价格监控任务启动(自适应挂单模式)")
while not self._shutdown_requested:
try:
# 等待新价格更新(阻塞直到有新价格或超时)
price_updated = await self.exchange_adapter.wait_for_new_price(timeout=30.0)
if not price_updated:
# 30秒无新价格更新,继续等待
self.logger.debug("30秒内无价格更新,继续等待...")
continue
# 动态调整挂单参数(基于市场风险)
new_target_bps, new_min_bps, new_max_bps, reason = self.get_adaptive_bps()
self.logger.info("当前风险等级: %s, 目标偏离: %.1f bps", self._current_risk_level, new_target_bps)
# 检测参数是否发生显著变化(超过20%)
params_changed = (
abs(new_target_bps - self.target_bps) / self.target_bps > 0.2 if self.target_bps > 0 else False
)
if params_changed:
self.logger.info(
"📊 挂单参数调整: %.1f→%.1f bps (范围: %.1f-%.1f), 原因: %s",
self.target_bps, new_target_bps, new_min_bps, new_max_bps, reason
)
self.target_bps = new_target_bps
self.min_bps = new_min_bps
self.max_bps = new_max_bps
# 参数变化时强制重挂单
await self._replace_orders(f"策略调整: {reason}")
continue
else:
# 参数未变化,更新内部值(用于下次比较)
self.target_bps = new_target_bps
self.min_bps = new_min_bps
self.max_bps = new_max_bps
# 正常偏离检查
need_replace, check_reason = self.check_order_count()
if not need_replace:
need_replace, check_reason = self.check_price_deviation()
if need_replace:
await self._replace_orders(check_reason)
except asyncio.TimeoutError:
# wait_for_new_price 超时,继续循环
continue
except Exception as e:
self.logger.exception("价格监控循环异常: %s", e)
await asyncio.sleep(1.0) # 出错后等待1秒再继续
self.logger.info("价格监控任务结束")
async def _position_monitor_loop(self):
"""
持仓监控循环 - 分层止盈止损机制
策略流程:
1. 检测新持仓 -> 挂一级止盈单 + 止损单
2. 等待持仓hold_seconds秒 -> 持续监控
3. 如果止盈单未成交但已等待hold_seconds秒 -> 改为二级市价止盈
4. 最长持仓时间超过max_hold_time秒 -> 强制市价平仓
5. 有订单成交 -> 自动清理持仓状态
"""
self.logger.info("持仓监控任务启动(分层止盈止损模式)")
while not self._shutdown_requested:
try:
# 1. 检查是否有新持仓(来自 exchange_adapter)
current_position = await self.exchange_adapter.get_position(symbol=self.symbol)
current_qty = float(current_position.get("qty", 0)) if current_position else 0
# 2. 如果当前没有跟踪的持仓
if self._tracked_position is None:
# 2.1 有新的实际持仓
if current_qty != 0:
side = "buy" if current_qty > 0 else "sell"
entry_price = float(current_position.get("entry_price", 0))
self._tracked_position = self._create_position_obj(
qty=current_qty,
side=side,
entry_price=entry_price
)
self.logger.info(
"🔴 检测到新持仓: 方向=%s, 数量=%.4f, 入场价=%.2f",
side, abs(current_qty), entry_price
)
# 2.2 挂止盈 + 止损单
await self._place_tp_order(self._tracked_position)
await self._place_sl_order(self._tracked_position)
# 2.3 发送通知
if self.notifier:
await self.notifier.send(
f"⚠️ *新增持仓(分层止盈止损)*\n"
f"账户: `{self.account_name}`\n"
f"交易对: `{self.symbol}`\n"
f"方向: {side}\n"
f"数量: {abs(current_qty):.4f}\n"
f"入场价: {entry_price:.2f}\n"
f"一级止盈: {self._position_quick_tp_bps:.1f}bps @ {entry_price * (1 + self._position_quick_tp_bps / 10000 if side == 'buy' else 1 - self._position_quick_tp_bps / 10000):.2f}\n"
f"止损: {self._position_stop_loss_bps:.1f}bps"
)
# 2.4 正常循环间隔
await asyncio.sleep(0.5)
continue
# 3. 有跟踪的持仓,检查状态变化
if current_qty == 0:
# 3.1 持仓已清(被止盈或止损成交)
self.logger.info("✅ 持仓已清(成交或平仓完成)")
if self.notifier:
await self.notifier.send(
f"✅ *持仓已清*\n"
f"账户: `{self.account_name}`\n"
f"交易对: `{self.symbol}`\n"
f"原始方向: {self._tracked_position['side']}\n"
f"原始数量: {abs(self._tracked_position['qty']):.4f}\n"
f"入场价: {self._tracked_position['entry_price']:.2f}"
)
self._tracked_position = None
await asyncio.sleep(0.5)
continue
# 4. 持仓状态管理(分阶段处理)
elapsed = time.time() - self._tracked_position["entry_time"]
# 4.1 持仓超时保护(超过最大持仓时间 -> 强制市价平仓)
if elapsed > self._max_position_hold_time:
self.logger.warning(
"⏰ 持仓已超过最大时间 %.1f 秒,执行强制市价平仓",
self._max_position_hold_time
)
await self._cancel_tp_sl_orders(self._tracked_position)
await self._market_close_position(self._tracked_position)
if self.notifier:
await self.notifier.send(
f"🔴 *持仓超时强制平仓*\n"
f"账户: `{self.account_name}`\n"
f"交易对: `{self.symbol}`\n"
f"持仓时间: {elapsed:.1f}秒"
)
self._tracked_position = None
await asyncio.sleep(0.5)
continue
# 4.2 进行中阶段:等待hold_seconds秒 -> 尝试二级止盈
if self._tracked_position["stage"] == "entry":
if elapsed > self._position_hold_seconds:
# 从entry阶段进入hold阶段
self._tracked_position["stage"] = "hold"
self.logger.info(
"⏱️ 持仓已等待 %.1f 秒,从entry阶段进入hold阶段",
elapsed
)
# 取消止盈/止损单,改为市价平仓(二级强制止盈)
await self._cancel_tp_sl_orders(self._tracked_position)
# 尝试市价平仓
success = await self._market_close_position(self._tracked_position)
if success:
self.logger.info("二级市价止盈已执行")
if self.notifier:
await self.notifier.send(
f"💰 *二级市价止盈已执行*\n"
f"账户: `{self.account_name}`\n"
f"交易对: `{self.symbol}`\n"
f"持仓时间: {elapsed:.1f}秒\n"
f"目标止盈点数: {self._position_force_exit_bps:.1f}bps"
)
self._tracked_position = None
else:
# 市价平仓失败,继续等待或回到hold继续监控
self.logger.warning("二级市价止盈失败,继续等待")
await asyncio.sleep(0.5)
except Exception as e:
self.logger.exception("持仓监控循环异常: %s", e)
await asyncio.sleep(1.0) # 出错后等待1秒再继续
self.logger.info("持仓监控任务结束")
async def _balance_report_loop(self):
"""
定期汇报账户余额(后台任务)
防止程序挂了或出现异常时无法感知
"""
self.logger.info("余额汇报任务启动,间隔: %.0f秒", self._balance_report_interval)
while not self._shutdown_requested:
try:
try:
await asyncio.wait_for(
self._shutdown_event.wait(),
timeout=self._balance_report_interval,
)
break
except asyncio.TimeoutError:
pass
if self._shutdown_requested:
break
# 查询余额
balance = await api.query_balance(self.auth)
# 格式化余额信息
total_balance = float(balance.get("balance", "0"))
equity = float(balance.get("equity", "0"))
upnl = float(balance.get("upnl", "0"))
cross_available = float(balance.get("cross_available", "0"))
isolated_balance = float(balance.get("isolated_balance", "0"))
locked = float(balance.get("locked", "0"))
# 发送Telegram汇报
beijing_tz = ZoneInfo("Asia/Shanghai")
beijing_time = datetime.now(beijing_tz).strftime("%Y-%m-%d %H:%M:%S")
message = (
f"💰 *账户余额汇报*\n"
f"账户: `{self.account_name}`\n"
f"时间: {beijing_time}\n"
f"交易对: `{self.symbol}`\n"
f"\n"
f"*余额概览*\n"
f"总余额: ${total_balance:.2f}\n"
f"权益: ${equity:.2f}\n"
f"未实现收益: ${upnl:.2f}\n"
f"\n"
f"*仓位详情*\n"
f"单仓余额: ${isolated_balance:.2f}\n"
f"可用: ${cross_available:.2f}\n"
f"锁定: ${locked:.2f}"
)
await self.notifier.send(message)
self.logger.info("✅ 余额汇报已发送: 总余额=%.2f, 权益=%.2f", total_balance, equity)
except asyncio.CancelledError: