Repository navigation
Expand file tree
/
Copy pathtrading_engine.py
More file actions
1517 lines (1250 loc) · 79.6 KB
/
Copy pathtrading_engine.py
File metadata and controls
1517 lines (1250 loc) · 79.6 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 time
import pytz
import asyncio
from datetime import datetime, timedelta
from typing import Dict, Any, Optional, List, Tuple
from loguru import logger
from mexc_client import MexcClient
from config import BotConfig, TimeWindow
class TradingEngine:
"""High-performance trading engine with stop-loss, time windows, and order management"""
def __init__(self, config: BotConfig, client: MexcClient):
self.config = config
self.client = client
self.positions: Dict[str, Dict[str, Any]] = {}
self.daily_order_count = 0
self.last_reset_date = datetime.now().date()
self.stop_loss_orders: Dict[str, int] = {} # position_id -> stop_loss_order_id
async def is_trading_time(self) -> bool:
"""Check if current time is within any trading window"""
if not self.config.trading_windows:
return True # Trade 24/7 if no windows specified
now = datetime.now()
for window in self.config.trading_windows:
# Convert window timezone
tz = pytz.timezone(window.timezone)
local_now = now.astimezone(tz)
# Parse time strings
start_time = datetime.strptime(window.start_time, "%H:%M").time()
end_time = datetime.strptime(window.end_time, "%H:%M").time()
current_time = local_now.time()
# Handle overnight windows (e.g., 22:00 to 06:00)
if start_time <= end_time:
if start_time <= current_time <= end_time:
return True
else:
if current_time >= start_time or current_time <= end_time:
return True
return False
def _reset_daily_counters(self):
"""Reset daily counters if new day"""
today = datetime.now().date()
if today != self.last_reset_date:
self.daily_order_count = 0
self.last_reset_date = today
logger.info("Daily counters reset")
def _can_place_order(self) -> bool:
"""Check if we can place more orders today"""
self._reset_daily_counters()
return self.daily_order_count < self.config.trading_params.max_orders_per_day
def _calculate_order_quantity(self, price: float, usdt_amount: Optional[float] = None) -> float:
"""Calculate order quantity based on USDT amount or configured quantity"""
if self.config.trading_params.quantity_is_usdt:
# Calculate quantity from USDT amount
usdt_value = usdt_amount or self.config.trading_params.quantity
return usdt_value / price
else:
# Use direct quantity (base currency amount)
return usdt_amount or self.config.trading_params.quantity
def _get_available_balance(self, account_info: Dict[str, Any], asset: str) -> float:
"""Get available balance for a specific asset from account info"""
try:
balances = account_info.get('balances', [])
for balance in balances:
if balance.get('asset') == asset:
return float(balance.get('free', 0))
return 0.0
except Exception as e:
logger.error(f"Error getting available balance for {asset}: {str(e)}")
return 0.0
def _calculate_stop_loss_price(self, entry_price: float, side: str) -> float:
"""Calculate stop loss price based on entry price and percentage"""
stop_loss_percentage = self.config.trading_params.stop_loss_percentage / 100
if side == 'BUY':
# For long positions, stop loss is below entry price
return entry_price * (1 - stop_loss_percentage)
else:
# For short positions, stop loss is above entry price
return entry_price * (1 + stop_loss_percentage)
def _calculate_take_profit_price(self, entry_price: float, side: str) -> Optional[float]:
"""Calculate take profit price if configured"""
if not self.config.trading_params.take_profit_percentage:
return None
take_profit_percentage = self.config.trading_params.take_profit_percentage / 100
if side == 'BUY':
# For long positions, take profit is above entry price
return entry_price * (1 + take_profit_percentage)
else:
# For short positions, take profit is below entry price
return entry_price * (1 - take_profit_percentage)
async def get_current_price(self, symbol: str) -> float:
"""Get current market price for symbol"""
try:
ticker = await self.client.get_ticker_price(symbol)
return float(ticker['price'])
except Exception as e:
logger.error(f"Failed to get current price for {symbol}: {str(e)}")
raise
async def place_limit_buy_order(self, price: float, quantity: Optional[float] = None) -> Optional[Dict[str, Any]]:
"""Place a limit buy order with automatic stop loss using MEXC's integrated approach"""
if not await self.is_trading_time():
logger.warning("Outside trading hours, skipping buy order")
return None
if not self._can_place_order():
logger.warning("Daily order limit reached, skipping buy order")
return None
symbol = self.config.trading_params.symbol
# Calculate order quantity (handles both USDT and base currency modes)
order_quantity = self._calculate_order_quantity(price, quantity)
# Round to appropriate precision (make this configurable?)
order_quantity = round(order_quantity, 6)
try:
if self.config.dry_run:
usdt_value = order_quantity * price
logger.info(f"DRY RUN: Would place BUY order for {order_quantity} {symbol} at {price} (${usdt_value:.2f} USDT)")
return {"orderId": f"dry_run_{int(time.time())}", "side": "BUY", "price": price, "quantity": order_quantity}
# Calculate stop-loss price
stop_loss_price = self._calculate_stop_loss_price(price, 'BUY')
# Try to place limit order with integrated stop-loss first
try:
order_result = await self.client.place_limit_order_with_stop_loss(
symbol=symbol,
side='BUY',
quantity=order_quantity,
price=price,
stop_price=stop_loss_price
)
logger.info(f"Buy order with integrated stop-loss placed: {order_result}")
self.daily_order_count += 1
# Store position with integrated stop-loss info
self.positions[order_result['orderId']] = {
'entry_price': price,
'quantity': order_quantity,
'stop_loss_price': stop_loss_price,
'side': 'BUY',
'timestamp': time.time(),
'integrated_stop_loss': True # Flag indicating stop-loss is built into the order
}
return order_result
except Exception as e:
logger.warning(f"Integrated stop-loss order failed: {str(e)}")
logger.info("Falling back to regular limit order with software stop-loss...")
# Fallback to regular limit order
order_result = await self.client.place_limit_order(
symbol=symbol,
side='BUY',
quantity=order_quantity,
price=price
)
logger.info(f"Buy order placed: {order_result}")
self.daily_order_count += 1
# Setup software-based stop-loss monitoring
await self._setup_stop_loss(order_result['orderId'], price, order_quantity, stop_loss_price)
return order_result
except Exception as e:
logger.error(f"Failed to place buy order: {str(e)}")
raise
async def place_limit_sell_order(self, price: float, quantity: Optional[float] = None) -> Optional[Dict[str, Any]]:
"""Place a limit sell order with automatic stop loss using MEXC's integrated approach"""
if not await self.is_trading_time():
logger.warning("Outside trading hours, skipping sell order")
return None
if not self._can_place_order():
logger.warning("Daily order limit reached, skipping sell order")
return None
symbol = self.config.trading_params.symbol
# Calculate order quantity (handles both USDT and base currency modes)
order_quantity = self._calculate_order_quantity(price, quantity)
# Round to appropriate precision (make this configurable?)
order_quantity = round(order_quantity, 6)
try:
if self.config.dry_run:
usdt_value = order_quantity * price
logger.info(f"DRY RUN: Would place SELL order for {order_quantity} {symbol} at {price} (${usdt_value:.2f} USDT)")
return {"orderId": f"dry_run_{int(time.time())}", "side": "SELL", "price": price, "quantity": order_quantity}
# Calculate stop-loss price
stop_loss_price = self._calculate_stop_loss_price(price, 'SELL')
# Try to place limit order with integrated stop-loss first
try:
order_result = await self.client.place_limit_order_with_stop_loss(
symbol=symbol,
side='SELL',
quantity=order_quantity,
price=price,
stop_price=stop_loss_price
)
logger.info(f"Sell order with integrated stop-loss placed: {order_result}")
self.daily_order_count += 1
# Store position with integrated stop-loss info
self.positions[order_result['orderId']] = {
'entry_price': price,
'quantity': order_quantity,
'stop_loss_price': stop_loss_price,
'side': 'SELL',
'timestamp': time.time(),
'integrated_stop_loss': True # Flag indicating stop-loss is built into the order
}
return order_result
except Exception as e:
logger.warning(f"Integrated stop-loss order failed: {str(e)}")
logger.info("Falling back to regular limit order with software stop-loss...")
# Fallback to regular limit order
order_result = await self.client.place_limit_order(
symbol=symbol,
side='SELL',
quantity=order_quantity,
price=price
)
logger.info(f"Sell order placed: {order_result}")
self.daily_order_count += 1
# Setup software-based stop-loss monitoring
await self._setup_stop_loss(order_result['orderId'], price, order_quantity, stop_loss_price)
return order_result
except Exception as e:
logger.error(f"Failed to place sell order: {str(e)}")
raise
async def _setup_stop_loss(self, position_id: str, entry_price: float, quantity: float, stop_loss_price: float):
"""Set up stop loss order on MEXC exchange"""
try:
symbol = self.config.trading_params.symbol
if self.config.dry_run:
logger.info(f"DRY RUN: Would place stop loss order at {stop_loss_price} for position {position_id}")
return
# Determine opposite side for stop-loss order (sell to close long position)
stop_loss_side = 'SELL' # Assuming we're protecting a long position (BUY order)
# Place actual stop-loss order on MEXC exchange
try:
stop_loss_result = await self.client.place_stop_loss_order(
symbol=symbol,
side=stop_loss_side,
quantity=quantity,
stop_price=stop_loss_price
)
# Store the stop-loss order ID for later management
self.stop_loss_orders[position_id] = stop_loss_result['orderId']
logger.info(f"Stop loss order placed on exchange: {stop_loss_result['orderId']} at price {stop_loss_price}")
# Also store position for monitoring (as backup)
self.positions[position_id] = {
'entry_price': entry_price,
'quantity': quantity,
'stop_loss_price': stop_loss_price,
'stop_loss_order_id': stop_loss_result['orderId'],
'side': 'BUY',
'timestamp': time.time()
}
except Exception as e:
logger.error(f"Failed to place stop-loss order on exchange: {str(e)}")
logger.info("Falling back to software-based stop-loss monitoring...")
# Fallback to software monitoring if exchange order fails
self.positions[position_id] = {
'entry_price': entry_price,
'quantity': quantity,
'stop_loss_price': stop_loss_price,
'side': 'BUY',
'timestamp': time.time(),
'fallback_monitoring': True # Flag to indicate software monitoring
}
logger.info(f"Software-based stop loss monitoring activated at {stop_loss_price} for position {position_id}")
except Exception as e:
logger.error(f"Failed to setup stop loss: {str(e)}")
# Don't let stop-loss errors prevent the main order from succeeding
async def monitor_positions(self):
"""Monitor all active positions for stop-loss triggers and bracket completion"""
logger.info("Starting position monitoring...")
while True:
try:
positions_to_check = list(self.positions.items())
for position_id, position_data in positions_to_check:
try:
# Handle different types of position monitoring
bracket_type = position_data.get('bracket_type', 'standard')
if bracket_type == 'sequential':
# Monitor sequential bracket orders (new functionality)
await self._monitor_sequential_bracket_position(position_id, position_data)
elif position_data.get('bracket_monitoring'):
# Monitor existing bracket orders
await self._monitor_bracket_position(position_id, position_data)
else:
# Monitor standard positions with software stop-loss
await self._check_position_status(position_id, position_data)
except Exception as e:
logger.error(f"Error checking position {position_id}: {str(e)}")
# Wait before next check (adjust?)
await asyncio.sleep(0.1) # Check every 0.1 seconds for faster stop loss execution
except Exception as e:
logger.error(f"Error in position monitoring loop: {str(e)}")
await asyncio.sleep(1) # Wait longer on error (?)
async def _check_position_status(self, position_id: str, position_data: Dict[str, Any]):
"""Check status of a specific position and handle stop-loss triggers"""
try:
symbol = self.config.trading_params.symbol
# Check if main order is filled
order_status = await self.client.get_order_status(symbol, int(position_id))
if order_status['status'] == 'FILLED':
# Check if this position has integrated stop-loss
if position_data.get('integrated_stop_loss', False):
# For integrated stop-loss orders, MEXC handles everything automatically
# We just need to monitor the position and log status
current_price = await self.get_current_price(symbol)
entry_price = position_data['entry_price']
stop_loss_price = position_data['stop_loss_price']
side = position_data['side']
# Calculate PnL
pnl_percent = ((current_price - entry_price) / entry_price) * 100
if side == 'SELL':
pnl_percent = -pnl_percent
# Check if we're close to stop-loss trigger
if side == 'BUY' and current_price <= stop_loss_price * 1.01: # Within 1% of stop-loss
logger.warning(f"Position {position_id} approaching stop-loss: current={current_price}, stop-loss={stop_loss_price}")
elif side == 'SELL' and current_price >= stop_loss_price * 0.99: # Within 1% of stop-loss
logger.warning(f"Position {position_id} approaching stop-loss: current={current_price}, stop-loss={stop_loss_price}")
else:
logger.info(f"Position {position_id} active (integrated SL): entry={entry_price}, current={current_price}, PnL={pnl_percent:.2f}%, SL={stop_loss_price}")
# Check if this position has an exchange-based stop-loss order (fallback method)
elif 'stop_loss_order_id' in position_data and not position_data.get('fallback_monitoring', False):
# Exchange-based stop-loss - just check if it's still active
stop_loss_order_id = position_data['stop_loss_order_id']
try:
sl_status = await self.client.get_order_status(symbol, int(stop_loss_order_id))
if sl_status['status'] == 'FILLED':
logger.info(f"Stop-loss order {stop_loss_order_id} executed for position {position_id}")
await self._cleanup_position(position_id)
elif sl_status['status'] == 'CANCELED':
logger.warning(f"Stop-loss order {stop_loss_order_id} was canceled for position {position_id}")
await self._cleanup_position(position_id)
else:
# Stop-loss order is still active, just log position status
current_price = await self.get_current_price(symbol)
entry_price = position_data['entry_price']
pnl_percent = ((current_price - entry_price) / entry_price) * 100
logger.info(f"Position {position_id} active: entry={entry_price}, current={current_price}, PnL={pnl_percent:.2f}%, SL Order={stop_loss_order_id}")
except Exception as e:
logger.error(f"Error checking stop-loss order {stop_loss_order_id}: {e}")
# If we can't check the stop-loss order, fall back to software monitoring
position_data['fallback_monitoring'] = True
logger.info(f"Switching to software monitoring for position {position_id}")
# Software-based monitoring (fallback or primary if exchange orders failed)
elif position_data.get('fallback_monitoring', False) or 'stop_loss_order_id' not in position_data:
current_price = await self.get_current_price(symbol)
stop_loss_price = position_data['stop_loss_price']
entry_price = position_data['entry_price']
side = position_data['side']
quantity = position_data['quantity']
should_trigger_stop_loss = False
if side == 'BUY':
# For long positions, trigger if price drops below stop-loss
if current_price <= stop_loss_price:
should_trigger_stop_loss = True
logger.warning(f"Software stop-loss triggered for BUY position {position_id}: current_price={current_price} <= stop_loss={stop_loss_price}")
else:
# For short positions, trigger if price rises above stop-loss
if current_price >= stop_loss_price:
should_trigger_stop_loss = True
logger.warning(f"Software stop-loss triggered for SELL position {position_id}: current_price={current_price} >= stop_loss={stop_loss_price}")
if should_trigger_stop_loss:
await self._execute_software_stop_loss(position_id, position_data, current_price)
else:
# Log position status periodically
pnl_percent = ((current_price - entry_price) / entry_price) * 100
if side == 'SELL':
pnl_percent = -pnl_percent
logger.info(f"Position {position_id} active (software monitoring): entry={entry_price}, current={current_price}, PnL={pnl_percent:.2f}%")
elif order_status['status'] == 'CANCELED':
# Clean up if main order was canceled
logger.info(f"Main order {position_id} was canceled, cleaning up position")
await self._cleanup_position(position_id)
except Exception as e:
logger.error(f"Error checking position {position_id}: {str(e)}")
async def _execute_software_stop_loss(self, position_id: str, position_data: Dict[str, Any], current_price: float):
"""Execute software-based stop-loss by placing a market order"""
try:
symbol = self.config.trading_params.symbol
quantity = position_data['quantity']
side = position_data['side']
entry_price = position_data['entry_price']
# Determine opposite side for stop-loss order
stop_loss_side = 'SELL' if side == 'BUY' else 'BUY'
if self.config.dry_run:
loss_amount = (entry_price - current_price) * quantity
logger.warning(f"DRY RUN: Would execute software stop-loss {stop_loss_side} order for {quantity} {symbol} at market price ~{current_price} (loss: ${loss_amount:.2f})")
await self._cleanup_position(position_id)
return
# Place market order to close position immediately
stop_loss_result = await self.client.place_market_order(
symbol=symbol,
side=stop_loss_side,
quantity=quantity
)
loss_amount = abs((entry_price - current_price) * quantity)
logger.warning(f"Software stop-loss executed: {stop_loss_result['orderId']} - Loss: ${loss_amount:.2f}")
# Clean up position
await self._cleanup_position(position_id)
except Exception as e:
logger.error(f"Failed to execute software stop-loss for position {position_id}: {str(e)}")
# Try to clean up anyway
await self._cleanup_position(position_id)
async def _cleanup_position(self, position_id: str):
"""Clean up a position and its associated stop loss"""
try:
# Cancel stop loss order if exists
if position_id in self.stop_loss_orders:
stop_loss_id = self.stop_loss_orders[position_id]
try:
await self.client.cancel_order(self.config.trading_params.symbol, stop_loss_id)
logger.info(f"Canceled stop loss order {stop_loss_id}")
except:
pass # Stop loss might already be executed
del self.stop_loss_orders[position_id]
# Remove from active positions
if position_id in self.positions:
del self.positions[position_id]
logger.info(f"Cleaned up position {position_id}")
except Exception as e:
logger.error(f"Error cleaning up position {position_id}: {str(e)}")
async def cancel_order(self, order_id: int) -> bool:
"""Cancel an order and clean up associated data"""
try:
symbol = self.config.trading_params.symbol
result = await self.client.cancel_order(symbol, order_id)
# Clean up position data
order_id_str = str(order_id)
await self._cleanup_position(order_id_str)
logger.info(f"Order {order_id} canceled successfully")
return True
except Exception as e:
logger.error(f"Failed to cancel order {order_id}: {str(e)}")
return False
async def get_account_summary(self) -> Dict[str, Any]:
"""Get trading account summary"""
try:
account_info = await self.client.get_account_info()
open_orders = await self.client.get_open_orders()
current_price = await self.get_current_price(self.config.trading_params.symbol)
return {
"account_balance": account_info.get('balances', []),
"open_orders_count": len(open_orders),
"active_positions_count": len(self.positions),
"daily_orders_used": self.daily_order_count,
"daily_orders_remaining": self.config.trading_params.max_orders_per_day - self.daily_order_count,
"current_price": current_price,
"trading_time_active": await self.is_trading_time(),
"quantity_mode": "USDT-based" if self.config.trading_params.quantity_is_usdt else "Base currency",
"configured_quantity": self.config.trading_params.quantity
}
except Exception as e:
logger.error(f"Failed to get account summary: {str(e)}")
return {"error": str(e)}
async def place_bracket_buy_order(
self,
price: float,
quantity: Optional[float] = None,
stop_loss_percentage: float = 5.0,
take_profit_percentage: float = 10.0
) -> Optional[Dict[str, Any]]:
"""Place a buy order with both stop-loss and take-profit (bracket order)"""
symbol = self.config.trading_params.symbol
# Calculate quantity if not provided
if quantity is None:
account_info = await self.client.get_account_info()
available_balance = self._get_available_balance(account_info, 'USDT')
# Use configured allocation percentage
usable_balance = available_balance * (self.config.trading_params.allocation_percentage / 100)
quantity = usable_balance / price
logger.info(f"Auto-calculated quantity: {quantity} (from ${usable_balance:.2f} USDT)")
if self.config.dry_run:
logger.info("DRY RUN: Would place bracket buy order")
logger.info(f" Symbol: {symbol}")
logger.info(f" Entry Price: ${price}")
logger.info(f" Quantity: {quantity}")
logger.info(f" Stop Loss: {stop_loss_percentage}% (${price * (1 - stop_loss_percentage/100):.4f})")
logger.info(f" Take Profit: {take_profit_percentage}% (${price * (1 + take_profit_percentage/100):.4f})")
return {
'dry_run': True,
'symbol': symbol,
'side': 'BUY',
'quantity': quantity,
'price': price,
'stop_loss_percentage': stop_loss_percentage,
'take_profit_percentage': take_profit_percentage
}
try:
# Place the bracket order
result = await self.client.place_bracket_order(
symbol=symbol,
side='BUY',
quantity=quantity,
price=price,
stop_loss_percentage=stop_loss_percentage,
take_profit_percentage=take_profit_percentage
)
logger.info(f"Bracket buy order placed successfully:")
logger.info(f" Order ID: {result['main_order'].get('orderId', 'Unknown')}")
logger.info(f" Bracket Type: {result['bracket_type']}")
logger.info(f" Stop Loss: ${result['stop_loss_price']:.4f}")
logger.info(f" Take Profit: ${result['take_profit_price']:.4f}")
# Store position for monitoring if needed
if result['bracket_type'] == 'software':
position_key = f"{symbol}_{result['main_order'].get('orderId')}"
self.positions[position_key] = {
'order_id': result['main_order'].get('orderId'),
'symbol': symbol,
'side': 'BUY',
'quantity': quantity,
'entry_price': price,
'stop_loss_price': result['stop_loss_price'],
'take_profit_price': result['take_profit_price'],
'stop_loss_percentage': stop_loss_percentage,
'take_profit_percentage': take_profit_percentage,
'bracket_monitoring': True,
'created_at': asyncio.get_event_loop().time()
}
logger.info(f"Added bracket position for software monitoring: {position_key}")
return result
except Exception as e:
logger.error(f"Failed to place bracket buy order: {str(e)}")
return None
async def _monitor_bracket_position(self, position_key: str, position: Dict[str, Any]):
"""Monitor a bracket position for both stop-loss and take-profit triggers"""
try:
symbol = position['symbol']
order_id = position['order_id']
entry_price = position['entry_price']
stop_loss_price = position['stop_loss_price']
take_profit_price = position['take_profit_price']
quantity = position['quantity']
# Check if main order is still open and filled
try:
order_status = await self.client.get_order_status(symbol, order_id)
status = order_status.get('status', '')
if status not in ['FILLED', 'PARTIALLY_FILLED']:
# Order not filled yet, keep monitoring
return
if status == 'FILLED':
logger.info(f"Bracket order {order_id} filled, starting TP/SL monitoring")
except Exception as e:
logger.warning(f"Could not check order status for {order_id}: {e}")
return
# Get current price
ticker = await self.client.get_ticker_price(symbol)
current_price = float(ticker.get('price', 0))
if current_price <= 0:
logger.warning(f"Invalid current price for {symbol}: {current_price}")
return
# Calculate current profit/loss percentage
price_change_pct = ((current_price - entry_price) / entry_price) * 100
logger.debug(f"Bracket monitoring {order_id}: Current ${current_price:.4f}, Entry ${entry_price:.4f}, Change: {price_change_pct:.2f}%")
# Check stop-loss condition
if current_price <= stop_loss_price:
logger.warning(f"STOP-LOSS TRIGGERED for {order_id}!")
logger.warning(f" Current Price: ${current_price:.4f}")
logger.warning(f" Stop Loss Price: ${stop_loss_price:.4f}")
logger.warning(f" Loss: {price_change_pct:.2f}%")
# Execute stop-loss sell
await self._execute_bracket_exit(position_key, position, 'STOP_LOSS', current_price)
return
# Check take-profit condition
if current_price >= take_profit_price:
logger.info(f"TAKE-PROFIT TRIGGERED for {order_id}!")
logger.info(f" Current Price: ${current_price:.4f}")
logger.info(f" Take Profit Price: ${take_profit_price:.4f}")
logger.info(f" Profit: {price_change_pct:.2f}%")
# Execute take-profit sell
await self._execute_bracket_exit(position_key, position, 'TAKE_PROFIT', current_price)
return
except Exception as e:
logger.error(f"Error monitoring bracket position {position_key}: {str(e)}")
async def _execute_bracket_exit(self, position_key: str, position: Dict[str, Any], exit_type: str, current_price: float):
"""Execute the exit order for a bracket position"""
try:
symbol = position['symbol']
quantity = position['quantity']
entry_price = position['entry_price']
logger.info(f"Executing {exit_type} for bracket position {position_key}")
if self.config.dry_run:
logger.info(f"DRY RUN: Would execute {exit_type} market sell")
logger.info(f" Symbol: {symbol}")
logger.info(f" Quantity: {quantity}")
logger.info(f" Current Price: ${current_price:.4f}")
# Calculate profit/loss
pnl = (current_price - entry_price) * quantity
pnl_pct = ((current_price - entry_price) / entry_price) * 100
logger.info(f" P&L: ${pnl:.4f} ({pnl_pct:.2f}%)")
# Remove from monitoring
if position_key in self.positions:
del self.positions[position_key]
return
# Execute market sell order
try:
# FIX: Cancel take profit order FIRST to unlock quantity
if position.get('take_profit_order_id'):
logger.warning(f" Cancelling take profit order {position['take_profit_order_id']} to unlock quantity for stop loss")
try:
cancel_result = await self.client.cancel_order(symbol, position['take_profit_order_id'])
logger.warning(f" Take profit order cancelled: {cancel_result}")
# Small delay to ensure cancellation is processed
await asyncio.sleep(0.5)
except Exception as cancel_e:
logger.error(f" Could not cancel take profit order: {cancel_e}")
logger.error(" QUANTITY STILL LOCKED - Stop loss may fail!")
sell_result = await self.client.place_market_order(symbol, 'SELL', quantity)
# Calculate actual profit/loss
fill_price = float(sell_result.get('fills', [{}])[0].get('price', current_price))
pnl = (fill_price - entry_price) * quantity
pnl_pct = ((fill_price - entry_price) / entry_price) * 100
logger.info(f"{exit_type} executed successfully:")
logger.info(f" Exit Order ID: {sell_result.get('orderId', 'Unknown')}")
logger.info(f" Fill Price: ${fill_price:.4f}")
logger.info(f" P&L: ${pnl:.4f} ({pnl_pct:.2f}%)")
# Remove position from monitoring
if position_key in self.positions:
del self.positions[position_key]
logger.info(f"Removed bracket position {position_key} from monitoring")
except Exception as e:
logger.error(f"Failed to execute {exit_type} market sell: {str(e)}")
# Keep monitoring in case we can retry
except Exception as e:
logger.error(f"Error executing bracket exit for {position_key}: {str(e)}")
async def place_sequential_bracket_buy_order(
self,
entry_price: float,
stop_loss_price: float,
take_profit_price: float,
quantity: Optional[float] = None
) -> Optional[Dict[str, Any]]:
"""
Place a sequential bracket buy order following the workflow:
1. Place LIMIT buy order at entry_price
2. Monitor until filled
3. Automatically place TAKE_PROFIT and STOP_LOSS orders
Args:
entry_price: Price for the initial LIMIT buy order (e.g., 1.1 USDT)
stop_loss_price: Price to sell if market goes down (e.g., 1.0 USDT)
take_profit_price: Price to sell for profit (e.g., 5.0 USDT)
quantity: Amount to buy (if None, uses configured amount)
"""
if not await self.is_trading_time():
logger.warning("Outside trading hours, skipping sequential bracket order")
return None
if not self._can_place_order():
logger.warning("Daily order limit reached, skipping sequential bracket order")
return None
symbol = self.config.trading_params.symbol
# Calculate order quantity if not provided
if quantity is None:
order_quantity = self._calculate_order_quantity(entry_price)
else:
order_quantity = quantity
# Round to appropriate precision
order_quantity = round(order_quantity, 6)
# Validate prices
if stop_loss_price >= entry_price:
raise ValueError(f"Stop loss price ({stop_loss_price}) must be below entry price ({entry_price}) for buy orders")
if take_profit_price <= entry_price:
raise ValueError(f"Take profit price ({take_profit_price}) must be above entry price ({entry_price}) for buy orders")
try:
if self.config.dry_run:
usdt_value = order_quantity * entry_price
logger.info(f"DRY RUN: Sequential bracket order for {symbol}")
logger.info(f" 1. BUY {order_quantity} @ ${entry_price} (${usdt_value:.2f} USDT)")
logger.info(f" 2. After fill, place STOP_LOSS @ ${stop_loss_price}")
logger.info(f" 3. After fill, place TAKE_PROFIT @ ${take_profit_price}")
# Simulate the order structure
fake_order_id = f"dry_run_seq_{int(time.time())}"
return {
"main_order": {"orderId": fake_order_id, "side": "BUY", "price": entry_price, "quantity": order_quantity},
"bracket_type": "sequential",
"symbol": symbol,
"entry_price": entry_price,
"stop_loss_price": stop_loss_price,
"take_profit_price": take_profit_price,
"quantity": order_quantity
}
# Place the sequential bracket order
result = await self.client.place_sequential_bracket_order(
symbol=symbol,
side='BUY',
quantity=order_quantity,
entry_price=entry_price,
stop_loss_price=stop_loss_price,
take_profit_price=take_profit_price
)
logger.info(f"Sequential bracket order initiated:")
logger.info(f" Main order ID: {result['main_order'].get('orderId', 'Unknown')}")
logger.info(f" Entry price: ${entry_price}")
logger.info(f" Stop loss: ${stop_loss_price}")
logger.info(f" Take profit: ${take_profit_price}")
logger.info(f" Quantity: {order_quantity}")
self.daily_order_count += 1
# Store position for sequential monitoring
position_key = f"seq_{symbol}_{result['main_order'].get('orderId')}"
self.positions[position_key] = {
'order_id': result['main_order'].get('orderId'),
'symbol': symbol,
'side': 'BUY',
'quantity': order_quantity,
'entry_price': entry_price,
'stop_loss_price': stop_loss_price,
'take_profit_price': take_profit_price,
'bracket_type': 'sequential',
'status': 'waiting_for_fill',
'protective_orders_placed': False,
'stop_loss_order_id': None,
'take_profit_order_id': None,
'created_at': time.time()
}
logger.info(f"Added sequential bracket position for monitoring: {position_key}")
return result
except Exception as e:
logger.error(f"Failed to place sequential bracket order: {str(e)}")
raise
async def _monitor_sequential_bracket_position(self, position_key: str, position: Dict[str, Any]):
"""Monitor a sequential bracket position through its lifecycle"""
try:
symbol = position['symbol']
order_id = position['order_id']
entry_price = position['entry_price']
stop_loss_price = position['stop_loss_price']
take_profit_price = position['take_profit_price']
quantity = position['quantity']
status = position['status']
# Step 1: Check if main order is filled
if status == 'waiting_for_fill':
try:
order_status = await self.client.get_order_status(symbol, order_id)
order_state = order_status.get('status', '')
if order_state == 'FILLED':
logger.info(f" Main order {order_id} FILLED! Placing protective orders...")
# Update position status
self.positions[position_key]['status'] = 'main_filled'
# Place protective orders (STOP_LOSS and TAKE_PROFIT)
if not self.config.dry_run:
try:
protective_result = await self.client.place_protective_orders_after_fill(
symbol=symbol,
original_side='BUY',
quantity=quantity,
stop_loss_price=stop_loss_price,
take_profit_price=take_profit_price
)
# Handle software-based protection
if protective_result.get('software_stop_loss'):
logger.info(" Software-based stop loss monitoring activated")
self.positions[position_key]['software_stop_loss'] = True
self.positions[position_key]['software_stop_loss_price'] = stop_loss_price
else:
logger.info(" MEXC native stop loss protection activated")
self.positions[position_key]['software_stop_loss'] = False
# Store the protective order IDs if they exist
if protective_result['stop_loss_order'] and isinstance(protective_result['stop_loss_order'], dict):
if 'orderId' in protective_result['stop_loss_order']:
self.positions[position_key]['stop_loss_order_id'] = protective_result['stop_loss_order'].get('orderId')
logger.info(f" MEXC native STOP_LOSS_LIMIT order: {protective_result['stop_loss_order'].get('orderId')}")
else:
logger.info(f" Software stop loss monitoring: @ {protective_result['stop_loss_order'].get('stop_price')}")
if protective_result['take_profit_order'] and isinstance(protective_result['take_profit_order'], dict):
if 'orderId' in protective_result['take_profit_order']:
self.positions[position_key]['take_profit_order_id'] = protective_result['take_profit_order'].get('orderId')
logger.info(f" MEXC native TAKE_PROFIT_LIMIT order: {protective_result['take_profit_order'].get('orderId')}")
else:
logger.info(f" Software take profit monitoring: @ {protective_result['take_profit_order'].get('target_price')}")
self.positions[position_key]['protective_orders_placed'] = True
self.positions[position_key]['status'] = 'protected'
if protective_result['errors']:
logger.warning(f"Some protection setup issues: {protective_result['errors']}")
except Exception as e:
logger.error(f"Failed to set up protection for {order_id}: {str(e)}")
# Keep monitoring, might retry later
else:
logger.info("DRY RUN: Would set up software-based stop loss and take profit monitoring")
self.positions[position_key]['protective_orders_placed'] = True
self.positions[position_key]['status'] = 'protected'
self.positions[position_key]['software_stop_loss'] = True
self.positions[position_key]['software_stop_loss_price'] = stop_loss_price
elif order_state in ['CANCELED', 'REJECTED', 'EXPIRED']:
logger.warning(f"Main order {order_id} was {order_state}, removing from monitoring")
del self.positions[position_key]
except Exception as e:
logger.warning(f"Could not check order status for {order_id}: {e}")
# Step 2: Monitor protective orders if they've been placed
elif status == 'protected':
stop_loss_order_id = position.get('stop_loss_order_id')
take_profit_order_id = position.get('take_profit_order_id')
software_stop_loss = position.get('software_stop_loss', False)
# Check current price for software monitoring
current_price = None
try:
ticker = await self.client.get_ticker_price(symbol)
current_price = float(ticker.get('price', 0))
except Exception as e:
logger.debug(f"Could not get current price: {e}")
return
position_closed = False
# Monitor exchange-based stop loss order
if stop_loss_order_id and not software_stop_loss:
try:
sl_status = await self.client.get_order_status(symbol, stop_loss_order_id)
if sl_status.get('status') == 'FILLED':
logger.warning(f" MEXC STOP_LOSS_LIMIT TRIGGERED for position {position_key}")
logger.warning(f" Native stop loss order {stop_loss_order_id} filled")
position_closed = True
# Cancel take profit order if it exists
if take_profit_order_id:
try:
await self.client.cancel_order(symbol, take_profit_order_id)
logger.info(f"Cancelled take profit order {take_profit_order_id}")
except Exception as e:
logger.warning(f"Could not cancel take profit order: {e}")
elif sl_status.get('status') in ['CANCELED', 'REJECTED', 'EXPIRED']:
logger.warning(f"⚠️ MEXC stop loss order {stop_loss_order_id} was {sl_status.get('status')}")
logger.warning("Switching to software monitoring for safety")
# Switch to software monitoring as fallback
position['software_stop_loss'] = True
position['software_stop_loss_price'] = stop_loss_price
except Exception as e:
logger.debug(f"Could not check stop loss order status: {e}")
# Software-based stop loss monitoring (fallback when native orders fail)
elif software_stop_loss and current_price:
software_stop_price = position.get('software_stop_loss_price', stop_loss_price)
# Check if stop loss should trigger (for BUY positions, trigger when price drops below stop)
if current_price <= software_stop_price:
logger.warning(f" SOFTWARE STOP LOSS TRIGGERED for position {position_key}")
logger.warning(f" Current price: {current_price}")
logger.warning(f" Stop loss price: {software_stop_price}")
logger.warning(" (Using software fallback - native MEXC order failed)")