-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathmexc_client.py
More file actions
1631 lines (1404 loc) · 70.1 KB
/
Copy pathmexc_client.py
File metadata and controls
1631 lines (1404 loc) · 70.1 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 hmac
import time
import json
import asyncio
import aiohttp
import hashlib
import urllib.parse
from typing import Dict, Any, Optional, List
from loguru import logger
from config import MexcCredentials
class MexcClient:
"""High-performance async MEXC API client with rate limiting and error handling"""
BASE_URL = "https://api.mexc.com"
def __init__(self, credentials: MexcCredentials, rate_limit_rps: float = 10.0):
self.api_key = credentials.api_key
self.secret_key = credentials.secret_key
self.rate_limit_rps = rate_limit_rps
self.session: Optional[aiohttp.ClientSession] = None
self._last_request_time = 0
self._request_count = 0
self._rate_limit_lock = asyncio.Lock()
async def __aenter__(self):
"""Async context manager entry"""
self.session = aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=30),
connector=aiohttp.TCPConnector(limit=100, ttl_dns_cache=300)
)
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
"""Async context manager exit"""
if self.session:
await self.session.close()
def _generate_signature(self, query_string: str) -> str:
"""Generate HMAC SHA256 signature for API requests"""
return hmac.new(
self.secret_key.encode('utf-8'),
query_string.encode('utf-8'),
hashlib.sha256
).hexdigest()
async def _rate_limit(self):
"""Implement rate limiting to stay within API limits"""
async with self._rate_limit_lock:
now = time.time()
time_since_last = now - self._last_request_time
min_interval = 1.0 / self.rate_limit_rps
if time_since_last < min_interval:
sleep_time = min_interval - time_since_last
await asyncio.sleep(sleep_time)
self._last_request_time = time.time()
self._request_count += 1
async def _make_request(
self,
method: str,
endpoint: str,
params: Optional[Dict[str, Any]] = None,
signed: bool = True
) -> Dict[str, Any]:
"""Make authenticated API request with rate limiting"""
await self._rate_limit()
if params is None:
params = {}
# Add timestamp for signed requests
if signed:
params['timestamp'] = int(time.time() * 1000)
params['recvWindow'] = 60000 # 60 second receive window
# Create query string
query_string = '&'.join([f"{k}={v}" for k, v in sorted(params.items())])
headers = {
'X-MEXC-APIKEY': self.api_key,
'Content-Type': 'application/json'
}
# Add signature for signed requests
if signed:
signature = self._generate_signature(query_string)
query_string += f"&signature={signature}"
url = f"{self.BASE_URL}{endpoint}"
if query_string:
url += f"?{query_string}"
try:
logger.debug(f"Making {method} request to {endpoint}")
async with self.session.request(method, url, headers=headers) as response:
response_text = await response.text()
if response.status == 200:
return json.loads(response_text)
else:
logger.error(f"API Error {response.status}: {response_text}")
# Provide more helpful error messages
if response.status == 400:
try:
error_data = json.loads(response_text)
if error_data.get('code') == 10007:
logger.error("Symbol not supported. Use get_exchange_info() to see available symbols.")
except:
pass
raise Exception(f"MEXC API Error {response.status}: {response_text}")
except Exception as e:
logger.error(f"Request failed: {str(e)}")
raise
async def get_exchange_info(self, symbol: Optional[str] = None) -> Dict[str, Any]:
"""Get exchange information and available symbols"""
params = {}
if symbol:
params['symbol'] = symbol
try:
# Try the standard endpoint first
result = await self._make_request('GET', '/api/v3/exchangeInfo', params, signed=False)
logger.info(f"Exchange info returned {len(result.get('symbols', []))} symbols")
return result
except Exception as e:
logger.warning(f"Standard exchangeInfo failed: {e}")
# Try alternative endpoints
try:
# Try with symbols parameter as array
if symbol:
params['symbols'] = f'["{symbol}"]'
del params['symbol']
result = await self._make_request('GET', '/api/v3/exchangeInfo', params, signed=False)
logger.info(f"Alternative exchange info returned {len(result.get('symbols', []))} symbols")
return result
except Exception as e2:
logger.error(f"Alternative exchangeInfo also failed: {e2}")
raise e
async def get_all_symbols(self) -> List[str]:
"""Get list of all available trading symbols that allow spot trading"""
try:
# Try multiple approaches to get symbols
exchange_info = await self.get_exchange_info()
symbols = []
for symbol_info in exchange_info.get('symbols', []):
symbol_name = symbol_info.get('symbol', '')
status = symbol_info.get('status', '')
is_spot_trading_allowed = symbol_info.get('isSpotTradingAllowed', False)
# Check for various status indicators that mean trading is allowed
status_ok = status in ['TRADING', 'ENABLED', 'ACTIVE', 1, '1']
# Only include symbols that allow spot trading
if status_ok and is_spot_trading_allowed:
symbols.append(symbol_name)
if not symbols:
logger.warning("No symbols found with standard filters, trying alternative approach")
# If no symbols with standard status, try all symbols but still check spot trading
for symbol_info in exchange_info.get('symbols', []):
symbol_name = symbol_info.get('symbol', '')
is_spot_trading_allowed = symbol_info.get('isSpotTradingAllowed', False)
if symbol_name and is_spot_trading_allowed:
symbols.append(symbol_name)
logger.info(f"Found {len(symbols)} tradeable symbols (with spot trading enabled)")
return sorted(symbols)
except Exception as e:
logger.error(f"Failed to get symbols: {str(e)}")
return []
async def search_symbols(self, search_term: str) -> List[str]:
"""Search for symbols containing the search term"""
all_symbols = await self.get_all_symbols()
search_term_upper = search_term.upper()
matching_symbols = [symbol for symbol in all_symbols if search_term_upper in symbol.upper()]
logger.info(f"Found {len(matching_symbols)} symbols matching '{search_term}'")
return matching_symbols
async def validate_symbol(self, symbol: str) -> bool:
"""Check if a symbol is valid and tradable"""
try:
exchange_info = await self.get_exchange_info(symbol)
symbols = exchange_info.get('symbols', [])
for symbol_info in symbols:
if symbol_info.get('symbol') == symbol:
status = symbol_info.get('status', '')
is_spot_trading_allowed = symbol_info.get('isSpotTradingAllowed', False)
# Check both status and spot trading permission
status_ok = status in ['TRADING', 'ENABLED', 'ACTIVE', 1, '1']
logger.info(f"Symbol {symbol} validation: status={status}, spotTradingAllowed={is_spot_trading_allowed}")
return status_ok and is_spot_trading_allowed
return False
except Exception as e:
logger.error(f"Failed to validate symbol {symbol}: {str(e)}")
return False
async def get_server_time(self) -> Dict[str, Any]:
"""Get server time - useful for testing connectivity"""
return await self._make_request('GET', '/api/v3/time', {}, signed=False)
async def test_connectivity(self) -> bool:
"""Test API connectivity"""
try:
result = await self._make_request('GET', '/api/v3/ping', {}, signed=False)
logger.info("API connectivity test successful")
return True
except Exception as e:
logger.error(f"API connectivity test failed: {e}")
return False
async def test_api_permissions(self) -> Dict[str, Any]:
"""Test API key permissions and account status"""
permissions = {
"connectivity": False,
"account_access": False,
"trading_enabled": False,
"account_type": "unknown",
"trading_status": "unknown",
"error_details": []
}
try:
# Test basic connectivity
ping_result = await self._make_request('GET', '/api/v3/ping', {}, signed=False)
permissions["connectivity"] = True
logger.info("✅ API connectivity successful")
except Exception as e:
permissions["error_details"].append(f"Connectivity failed: {e}")
logger.error(f"❌ API connectivity failed: {e}")
return permissions
try:
# Test account access
account_info = await self._make_request('GET', '/api/v3/account')
permissions["account_access"] = True
permissions["account_type"] = account_info.get("accountType", "unknown")
logger.info("✅ Account access successful")
logger.info(f"Account type: {permissions['account_type']}")
# Check if account can trade
can_trade = account_info.get("canTrade", False)
can_withdraw = account_info.get("canWithdraw", False)
can_deposit = account_info.get("canDeposit", False)
permissions["trading_enabled"] = can_trade
permissions["trading_status"] = f"Trade: {can_trade}, Withdraw: {can_withdraw}, Deposit: {can_deposit}"
if can_trade:
logger.info("✅ Trading permissions enabled")
else:
logger.error("❌ Trading permissions disabled")
permissions["error_details"].append("Account trading is disabled")
# Check balances
balances = account_info.get("balances", [])
usdt_balance = None
for balance in balances:
if balance.get("asset") == "USDT":
usdt_balance = float(balance.get("free", 0))
break
if usdt_balance is not None:
logger.info(f"USDT balance: {usdt_balance}")
if usdt_balance < 1: # Minimum for testing
permissions["error_details"].append(f"Low USDT balance: {usdt_balance}")
except Exception as e:
permissions["error_details"].append(f"Account access failed: {e}")
logger.error(f"❌ Account access failed: {e}")
return permissions
async def get_account_info(self) -> Dict[str, Any]:
"""Get account information"""
return await self._make_request('GET', '/api/v3/account')
async def get_symbol_info(self, symbol: str) -> Dict[str, Any]:
"""Get symbol information"""
params = {'symbol': symbol}
return await self._make_request('GET', '/api/v3/exchangeInfo', params, signed=False)
async def get_ticker_price(self, symbol: str) -> Dict[str, Any]:
"""Get current ticker price"""
params = {'symbol': symbol}
return await self._make_request('GET', '/api/v3/ticker/price', params, signed=False)
async def place_limit_order(
self,
symbol: str,
side: str,
quantity: float,
price: float,
time_in_force: str = 'GTC'
) -> Dict[str, Any]:
"""Place a limit order (BUY or SELL)"""
# Store symbol for quantity formatting
self._current_symbol = symbol
params = {
'symbol': symbol,
'side': side,
'type': 'LIMIT',
'timeInForce': time_in_force,
'quantity': quantity,
'price': price
}
# Debug logging for troubleshooting
logger.info(f"Placing {side} order with parameters:")
logger.info(f" Symbol: {symbol}")
logger.info(f" Side: {side}")
logger.info(f" Type: LIMIT")
logger.info(f" Quantity: {quantity}")
logger.info(f" Price: {price}")
logger.info(f" TimeInForce: {time_in_force}")
try:
result = await self._make_request('POST', '/api/v3/order', params)
logger.info(f"Order placed successfully: {result}")
return result
except Exception as e:
# Enhanced error handling for common issues
error_str = str(e)
if "10007" in error_str and "symbol not support api" in error_str:
logger.error("Error 10007: Symbol not supported for API trading")
logger.error("This could be due to:")
logger.error(" • API key lacks trading permissions")
logger.error(" • Symbol is restricted for your account region")
logger.error(" • Account verification level insufficient")
logger.error(" • Using spot trading symbols on futures API or vice versa")
# Try to get more detailed symbol info
try:
logger.info("Checking detailed symbol information...")
symbol_details = await self.get_exchange_info(symbol)
if symbol_details.get('symbols'):
symbol_info = symbol_details['symbols'][0]
logger.info(f"Symbol details: {symbol_info}")
# Check permissions and filters
permissions = symbol_info.get('permissions', [])
status = symbol_info.get('status', '')
logger.info(f"Symbol permissions: {permissions}")
logger.info(f"Symbol status: {status}")
if 'SPOT' not in permissions:
logger.error("Symbol does not have SPOT trading permission")
if status != 'TRADING':
logger.error(f"Symbol status is '{status}', not 'TRADING'")
except Exception as detail_error:
logger.error(f"Could not get detailed symbol info: {detail_error}")
raise
async def place_limit_order_with_stop_loss(
self,
symbol: str,
side: str,
quantity: float,
price: float,
stop_price: float,
time_in_force: str = 'GTC'
) -> Dict[str, Any]:
"""Place a limit order with integrated stop-loss using MEXC's correct API format"""
# MEXC API approach 1: LIMIT with stopPrice + workingType (confirmed working?)
params = {
'symbol': symbol,
'side': side,
'type': 'LIMIT',
'timeInForce': time_in_force,
'quantity': quantity,
'price': price,
'stopPrice': stop_price,
'workingType': 'MARK_PRICE',
'priceProtect': 'true'
}
logger.info(f"Placing LIMIT order with integrated stop-loss (MEXC format):")
logger.info(f" Symbol: {symbol}")
logger.info(f" Side: {side}")
logger.info(f" Type: LIMIT with stopPrice")
logger.info(f" Quantity: {quantity}")
logger.info(f" Limit Price: {price}")
logger.info(f" Stop Price: {stop_price}")
logger.info(f" Working Type: MARK_PRICE")
logger.info(f" Price Protect: true")
logger.info(f" TimeInForce: {time_in_force}")
try:
result = await self._make_request('POST', '/api/v3/order', params)
logger.info(f"LIMIT order with integrated stop-loss placed successfully: {result}")
return result
except Exception as e:
error_str = str(e)
logger.warning(f"Primary LIMIT+stopPrice method failed: {error_str}")
# MEXC API approach 2: Try with OCO (One-Cancels-Other) order
try:
logger.info("Trying OCO (One-Cancels-Other) order approach...")
# For OCO orders, we need to place a limit order with a stop-loss order
oco_params = {
'symbol': symbol,
'side': side,
'quantity': quantity,
'price': price, # Limit order price
'stopPrice': stop_price, # Stop-loss trigger price
'stopLimitPrice': stop_price * 0.995 if side == 'SELL' else stop_price * 1.005, # Stop-loss execution price
'stopLimitTimeInForce': 'GTC',
'type': 'OCO'
}
logger.info(f"Placing OCO order with parameters: {oco_params}")
result = await self._make_request('POST', '/api/v3/order/oco', oco_params)
logger.info(f"OCO order placed successfully: {result}")
return result
except Exception as oco_e:
logger.warning(f"OCO order also failed: {oco_e}")
# MEXC API approach 3: Try standard order with additional TP/SL parameters
try:
logger.info("Trying standard order with TP/SL parameters...")
standard_params = {
'symbol': symbol,
'side': side,
'type': 'LIMIT',
'timeInForce': time_in_force,
'quantity': quantity,
'price': price,
'stopPrice': stop_price, # Stop-loss trigger
'workingType': 'MARK_PRICE', # Use mark price for stop-loss
'priceProtect': 'true' # Enable price protection
}
logger.info(f"Placing standard order with TP/SL: {standard_params}")
result = await self._make_request('POST', '/api/v3/order', standard_params)
logger.info(f"Standard order with TP/SL placed successfully: {result}")
return result
except Exception as standard_e:
logger.error(f"All TP/SL integration methods failed. Falling back to regular limit order.")
logger.error(f"Standard method error: {standard_e}")
# Final fallback: place regular limit order
fallback_params = {
'symbol': symbol,
'side': side,
'type': 'LIMIT',
'timeInForce': time_in_force,
'quantity': quantity,
'price': price
}
try:
result = await self._make_request('POST', '/api/v3/order', fallback_params)
logger.info(f"Fallback limit order placed (stop-loss will be software-based): {result}")
# Add flags to indicate this order needs software stop-loss monitoring
result['needs_software_stop_loss'] = True
result['stop_price'] = stop_price
return result
except Exception as fallback_e:
logger.error(f"Even fallback limit order failed: {fallback_e}")
raise fallback_e
async def place_stop_loss_order(
self,
symbol: str,
side: str,
quantity: float,
stop_price: float,
limit_price: Optional[float] = None
) -> Dict[str, Any]:
"""Place a standalone stop loss order (fallback method)"""
# Try STOP_LOSS_LIMIT first
if limit_price:
params = {
'symbol': symbol,
'side': side,
'type': 'STOP_LOSS_LIMIT',
'quantity': quantity,
'stopPrice': stop_price,
'price': limit_price,
'timeInForce': 'GTC'
}
order_type_name = 'STOP_LOSS_LIMIT'
else:
# Use STOP_LOSS (market order triggered at stop price)
params = {
'symbol': symbol,
'side': side,
'type': 'STOP_LOSS',
'quantity': quantity,
'stopPrice': stop_price,
'timeInForce': 'GTC'
}
order_type_name = 'STOP_LOSS'
logger.info(f"Placing {order_type_name} order with parameters:")
logger.info(f" Symbol: {symbol}")
logger.info(f" Side: {side}")
logger.info(f" Type: {order_type_name}")
logger.info(f" Quantity: {quantity}")
logger.info(f" Stop Price: {stop_price}")
if limit_price:
logger.info(f" Limit Price: {limit_price}")
try:
result = await self._make_request('POST', '/api/v3/order', params)
logger.info(f"{order_type_name} order placed successfully: {result}")
return result
except Exception as e:
error_str = str(e)
logger.error(f"{order_type_name} order failed: {error_str}")
raise e
async def place_market_order(
self,
symbol: str,
side: str,
quantity: float
) -> Dict[str, Any]:
"""Place a market order (immediate execution at current market price)"""
# Store symbol for quantity formatting
self._current_symbol = symbol
params = {
'symbol': symbol,
'side': side,
'type': 'MARKET',
'quantity': quantity
}
logger.info(f"Placing MARKET order: {side} {quantity} {symbol}")
try:
result = await self._make_request('POST', '/api/v3/order', params)
logger.info(f"Market order placed successfully: {result}")
return result
except Exception as e:
logger.error(f"Market order failed: {str(e)}")
raise
async def cancel_order(self, symbol: str, order_id: int) -> Dict[str, Any]:
"""Cancel an existing order"""
params = {
'symbol': symbol,
'orderId': order_id
}
return await self._make_request('DELETE', '/api/v3/order', params)
async def get_order_status(self, symbol: str, order_id: int) -> Dict[str, Any]:
"""Get order status"""
params = {
'symbol': symbol,
'orderId': order_id
}
return await self._make_request('GET', '/api/v3/order', params)
async def get_open_orders(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]:
"""Get all open orders"""
params = {}
if symbol:
params['symbol'] = symbol
return await self._make_request('GET', '/api/v3/openOrders', params)
async def get_order_history(self, symbol: str, limit: int = 500) -> List[Dict[str, Any]]:
"""Get order history"""
params = {
'symbol': symbol,
'limit': limit
}
return await self._make_request('GET', '/api/v3/allOrders', params)
async def get_tradeable_usdt_pairs(self, limit: int = 20) -> List[Dict[str, Any]]:
"""Get USDT pairs that allow spot trading"""
try:
exchange_info = await self.get_exchange_info()
tradeable_pairs = []
for symbol_info in exchange_info.get('symbols', []):
symbol_name = symbol_info.get('symbol', '')
quote_asset = symbol_info.get('quoteAsset', '')
is_spot_trading_allowed = symbol_info.get('isSpotTradingAllowed', False)
status = symbol_info.get('status', '')
# Look for USDT pairs that allow spot trading
if (quote_asset == 'USDT' and
is_spot_trading_allowed and
status in ['TRADING', 'ENABLED', 'ACTIVE', 1, '1']):
tradeable_pairs.append({
'symbol': symbol_name,
'baseAsset': symbol_info.get('baseAsset', ''),
'status': status,
'orderTypes': symbol_info.get('orderTypes', []),
'minQuantity': symbol_info.get('baseSizePrecision', ''),
'maxQuoteAmount': symbol_info.get('maxQuoteAmount', '')
})
# Sort by symbol name and limit results
tradeable_pairs.sort(key=lambda x: x['symbol'])
logger.info(f"Found {len(tradeable_pairs)} tradeable USDT pairs")
return tradeable_pairs[:limit]
except Exception as e:
logger.error(f"Failed to get tradeable USDT pairs: {e}")
return []
async def check_symbol_tpsl_support(self, symbol: str) -> Dict[str, Any]:
"""Check what TP/SL features are supported for a symbol"""
try:
exchange_info = await self.get_exchange_info(symbol)
if not exchange_info.get('symbols'):
return {"error": "Symbol not found"}
symbol_info = exchange_info['symbols'][0]
# Extract relevant TP/SL information
tpsl_info = {
"symbol": symbol,
"status": symbol_info.get('status'),
"orderTypes": symbol_info.get('orderTypes', []),
"spotTradingAllowed": symbol_info.get('isSpotTradingAllowed', False),
"filters": []
}
# Check for relevant filters
for filter_info in symbol_info.get('filters', []):
filter_type = filter_info.get('filterType', '')
if filter_type in ['PRICE_FILTER', 'LOT_SIZE', 'MIN_NOTIONAL', 'PERCENT_PRICE']:
tpsl_info["filters"].append(filter_info)
logger.info(f"TP/SL Support Analysis for {symbol}:")
logger.info(f" Order Types: {tpsl_info['orderTypes']}")
logger.info(f" Spot Trading: {tpsl_info['spotTradingAllowed']}")
logger.info(f" Status: {tpsl_info['status']}")
# Check for specific TP/SL related order types
tpsl_order_types = []
for order_type in tpsl_info['orderTypes']:
if any(keyword in order_type for keyword in ['STOP', 'OCO', 'LIMIT']):
tpsl_order_types.append(order_type)
tpsl_info["tpsl_order_types"] = tpsl_order_types
logger.info(f" TP/SL Related Order Types: {tpsl_order_types}")
return tpsl_info
except Exception as e:
logger.error(f"Failed to check TP/SL support for {symbol}: {e}")
return {"error": str(e)}
async def test_tpsl_order_types(self, symbol: str, side: str, quantity: float, price: float, stop_price: float) -> Dict[str, Any]:
"""Test different TP/SL order type combinations to find what works with MEXC"""
results = {
"symbol": symbol,
"tested_methods": [],
"successful_method": None,
"error_details": []
}
# List of different TP/SL order type variations to test
test_methods = [
# Method 1: TP_SL_LIMIT order type
{
"name": "TP_SL_LIMIT",
"params": {
'symbol': symbol,
'side': side,
'type': 'TP_SL_LIMIT',
'quantity': quantity,
'price': price,
'stopPrice': stop_price,
'timeInForce': 'GTC'
}
},
# Method 2: STOP_LOSS_LIMIT order type
{
"name": "STOP_LOSS_LIMIT",
"params": {
'symbol': symbol,
'side': side,
'type': 'STOP_LOSS_LIMIT',
'quantity': quantity,
'price': price,
'stopPrice': stop_price,
'timeInForce': 'GTC'
}
},
# Method 3: LIMIT with stopLimitPrice
{
"name": "LIMIT_with_stopLimitPrice",
"params": {
'symbol': symbol,
'side': side,
'type': 'LIMIT',
'quantity': quantity,
'price': price,
'stopLimitPrice': stop_price,
'timeInForce': 'GTC'
}
},
# Method 4: TAKE_PROFIT_LIMIT
{
"name": "TAKE_PROFIT_LIMIT",
"params": {
'symbol': symbol,
'side': side,
'type': 'TAKE_PROFIT_LIMIT',
'quantity': quantity,
'price': price,
'stopPrice': stop_price,
'timeInForce': 'GTC'
}
},
# Method 5: LIMIT with additional SL parameters
{
"name": "LIMIT_with_SL_params",
"params": {
'symbol': symbol,
'side': side,
'type': 'LIMIT',
'quantity': quantity,
'price': price,
'stopLossPrice': stop_price,
'stopLossType': 'LIMIT',
'timeInForce': 'GTC'
}
},
# Method 6: CONDITIONAL_LIMIT
{
"name": "CONDITIONAL_LIMIT",
"params": {
'symbol': symbol,
'side': side,
'type': 'CONDITIONAL_LIMIT',
'quantity': quantity,
'price': price,
'triggerPrice': stop_price,
'timeInForce': 'GTC'
}
}
]
logger.info(f"Testing {len(test_methods)} different TP/SL order type methods for {symbol}")
for method in test_methods:
method_name = method["name"]
params = method["params"]
try:
logger.info(f"Testing method: {method_name}")
logger.info(f" Parameters: {params}")
# DRY RUN - just test if the API accepts the parameters
# Add a flag to indicate this is a test
test_result = await self._test_order_parameters(params)
results["tested_methods"].append({
"method": method_name,
"params": params,
"status": "success" if test_result else "failed",
"result": test_result
})
if test_result and not results["successful_method"]:
results["successful_method"] = method_name
logger.info(f"✅ SUCCESS: {method_name} method appears to work!")
except Exception as e:
error_msg = str(e)
logger.warning(f"❌ Method {method_name} failed: {error_msg}")
results["tested_methods"].append({
"method": method_name,
"params": params,
"status": "error",
"error": error_msg
})
results["error_details"].append(f"{method_name}: {error_msg}")
return results
async def _test_order_parameters(self, params: Dict[str, Any]) -> bool:
"""Test order parameters without actually placing the order"""
try:
# Add test flag to avoid actually placing the order
test_params = params.copy()
test_params['test'] = 'true'
# Try the test endpoint first
try:
result = await self._make_request('POST', '/api/v3/order/test', test_params)
logger.info("Order test endpoint succeeded")
return True
except Exception as test_e:
logger.debug(f"Test endpoint failed: {test_e}")
# If test endpoint doesn't exist, try to validate by checking the error message
# We'll make a real request but cancel it immediately if it succeeds
try:
# Remove test flag and try real endpoint
real_params = params.copy()
if 'test' in real_params:
del real_params['test']
# This might actually place an order, so we should be careful
# For now, let's just return False to be safe
logger.debug("Cannot safely test without test endpoint")
return False
except Exception as real_e:
error_msg = str(real_e).lower()
# Check if the error indicates the order type is invalid
if any(indicator in error_msg for indicator in [
'invalid type', 'unsupported type', 'unknown type',
'invalid order type', 'order type not supported'
]):
return False
# If it's a different error (like insufficient balance), the order type might be valid
if any(indicator in error_msg for indicator in [
'insufficient', 'balance', 'minimum', 'precision'
]):
logger.info("Order type appears valid (got balance/precision error)")
return True
return False
except Exception as e:
logger.debug(f"Parameter test failed: {e}")
return False
async def place_bracket_order(
self,
symbol: str,
side: str,
quantity: float,
price: float,
stop_loss_percentage: float,
take_profit_percentage: float,
time_in_force: str = 'GTC'
) -> Dict[str, Any]:
"""Place a bracket order with both stop-loss and take-profit"""
# Calculate stop-loss and take-profit prices
if side == 'BUY':
stop_loss_price = price * (1 - stop_loss_percentage / 100)
take_profit_price = price * (1 + take_profit_percentage / 100)
else: # SELL
stop_loss_price = price * (1 + stop_loss_percentage / 100)
take_profit_price = price * (1 - take_profit_percentage / 100)
logger.info(f"Placing bracket order ({side}):")
logger.info(f" Symbol: {symbol}")
logger.info(f" Quantity: {quantity}")
logger.info(f" Entry Price: {price}")
logger.info(f" Stop Loss: {stop_loss_price} ({stop_loss_percentage}%)")
logger.info(f" Take Profit: {take_profit_price} ({take_profit_percentage}%)")
# Method 1: Try MEXC's bracket order (supported?)
bracket_params = {
'symbol': symbol,
'side': side,
'type': 'LIMIT',
'timeInForce': time_in_force,
'quantity': quantity,
'price': price,
'stopPrice': stop_loss_price,
'takeProfitPrice': take_profit_price,
'workingType': 'MARK_PRICE',
'priceProtect': 'true'
}
try:
logger.info("Attempting bracket order with both TP and SL...")
result = await self._make_request('POST', '/api/v3/order', bracket_params)
logger.info(f"Bracket order placed successfully: {result}")
return {
'main_order': result,
'stop_loss_price': stop_loss_price,
'take_profit_price': take_profit_price,
'bracket_type': 'integrated'
}
except Exception as e:
logger.warning(f"Integrated bracket order failed: {e}")
# Method 2: Try OCO order
try:
logger.info("Trying OCO bracket order...")
oco_params = {
'symbol': symbol,
'side': side,
'quantity': quantity,
'price': price,
'stopPrice': stop_loss_price,
'stopLimitPrice': stop_loss_price,
'stopLimitTimeInForce': 'GTC',
'takeProfitPrice': take_profit_price,
'takeProfitLimitPrice': take_profit_price,
'takeProfitTimeInForce': 'GTC',
'type': 'OCO'
}
result = await self._make_request('POST', '/api/v3/order/oco', oco_params)
logger.info(f"OCO bracket order placed successfully: {result}")
return {
'main_order': result,
'stop_loss_price': stop_loss_price,
'take_profit_price': take_profit_price,
'bracket_type': 'oco'
}
except Exception as oco_e:
logger.warning(f"OCO bracket order failed: {oco_e}")
# Method 3: Place main order and set up software-based TP/SL monitoring
logger.info("Falling back to software-based bracket monitoring...")
# Place the main order first
main_params = {
'symbol': symbol,
'side': side,
'type': 'LIMIT',
'timeInForce': time_in_force,
'quantity': quantity,
'price': price
}
try:
main_result = await self._make_request('POST', '/api/v3/order', main_params)
logger.info(f"Main order placed, will monitor with software TP/SL: {main_result}")
# Add monitoring flags
main_result.update({
'needs_software_bracket': True,
'stop_loss_price': stop_loss_price,
'take_profit_price': take_profit_price,
'stop_loss_percentage': stop_loss_percentage,
'take_profit_percentage': take_profit_percentage,
'bracket_type': 'software'
})
return {
'main_order': main_result,
'stop_loss_price': stop_loss_price,
'take_profit_price': take_profit_price,
'bracket_type': 'software'
}
except Exception as main_e:
logger.error(f"Even main order failed: {main_e}")
raise main_e
async def place_stop_loss_market_order(
self,
symbol: str,
side: str,
quantity: float,
stop_price: float,
time_in_force: str = 'GTC'
) -> Dict[str, Any]:
"""Place a stop loss market order using MEXC's STOP_LOSS order type"""
# For MEXC, STOP_LOSS orders need both stopPrice and price parameters
# The price should be slightly worse than stopPrice to ensure execution
if side == 'SELL':
# For sell stop loss, price should be slightly below stop price
limit_price = stop_price * 0.999 # 0.1% below stop price
else:
# For buy stop loss, price should be slightly above stop price
limit_price = stop_price * 1.001 # 0.1% above stop price