hubble-futures 0.2.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hubble_futures/__init__.py +151 -0
- hubble_futures/aster.py +601 -0
- hubble_futures/base.py +430 -0
- hubble_futures/config.py +34 -0
- hubble_futures/function_log.py +303 -0
- hubble_futures/version.py +8 -0
- hubble_futures/weex.py +1246 -0
- hubble_futures-0.2.13.dist-info/METADATA +217 -0
- hubble_futures-0.2.13.dist-info/RECORD +11 -0
- hubble_futures-0.2.13.dist-info/WHEEL +4 -0
- hubble_futures-0.2.13.dist-info/licenses/LICENSE +21 -0
hubble_futures/aster.py
ADDED
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Aster Futures API Client
|
|
3
|
+
|
|
4
|
+
Implementation for Aster DEX futures trading.
|
|
5
|
+
Uses /fapi/v1/ and /fapi/v2/ endpoints.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import hmac
|
|
10
|
+
from decimal import ROUND_DOWN, ROUND_HALF_UP, Decimal
|
|
11
|
+
|
|
12
|
+
from loguru import logger
|
|
13
|
+
|
|
14
|
+
from .base import BaseFuturesClient
|
|
15
|
+
from .config import ExchangeConfig
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AsterFuturesClient(BaseFuturesClient):
|
|
19
|
+
"""Aster Futures REST API Client"""
|
|
20
|
+
|
|
21
|
+
DEFAULT_BASE_URL = "https://fapi.asterdex.com"
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
api_key: str,
|
|
26
|
+
api_secret: str,
|
|
27
|
+
base_url: str | None = None,
|
|
28
|
+
max_retries: int = 5,
|
|
29
|
+
retry_delay: float = 1.0,
|
|
30
|
+
timeout: float = 5.0,
|
|
31
|
+
proxy_url: str | None = None
|
|
32
|
+
):
|
|
33
|
+
super().__init__(
|
|
34
|
+
api_key=api_key,
|
|
35
|
+
api_secret=api_secret,
|
|
36
|
+
base_url=base_url or self.DEFAULT_BASE_URL,
|
|
37
|
+
max_retries=max_retries,
|
|
38
|
+
retry_delay=retry_delay,
|
|
39
|
+
timeout=timeout,
|
|
40
|
+
proxy_url=proxy_url
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def _setup_session_headers(self) -> None:
|
|
44
|
+
"""Setup Aster-specific headers."""
|
|
45
|
+
self.session.headers.update({
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
"X-MBX-APIKEY": self.api_key
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_config(cls, config: ExchangeConfig) -> "AsterFuturesClient":
|
|
52
|
+
"""Create client from ExchangeConfig."""
|
|
53
|
+
return cls(
|
|
54
|
+
api_key=config.api_key,
|
|
55
|
+
api_secret=config.api_secret,
|
|
56
|
+
base_url=config.base_url or cls.DEFAULT_BASE_URL,
|
|
57
|
+
proxy_url=config.proxy_url
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def _generate_signature(self, params: dict) -> str: # type: ignore[type-arg]
|
|
61
|
+
"""
|
|
62
|
+
Generate request signature.
|
|
63
|
+
|
|
64
|
+
Note: Aster DEX does NOT require sorted parameters.
|
|
65
|
+
Uses insertion order (tested and confirmed).
|
|
66
|
+
"""
|
|
67
|
+
query_string = "&".join([f"{k}={v}" for k, v in params.items()])
|
|
68
|
+
signature = hmac.new(
|
|
69
|
+
self.api_secret.encode('utf-8'),
|
|
70
|
+
query_string.encode('utf-8'),
|
|
71
|
+
hashlib.sha256
|
|
72
|
+
).hexdigest()
|
|
73
|
+
return signature
|
|
74
|
+
|
|
75
|
+
# ==================== Market Data ====================
|
|
76
|
+
|
|
77
|
+
def get_klines(self, symbol: str, interval: str = "1h", limit: int = 200) -> list[dict]: # type: ignore[type-arg]
|
|
78
|
+
"""Fetch candlestick (kline) data."""
|
|
79
|
+
params = {
|
|
80
|
+
"symbol": symbol,
|
|
81
|
+
"interval": interval,
|
|
82
|
+
"limit": limit
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
data = self._request("GET", "/fapi/v1/klines", params=params)
|
|
86
|
+
|
|
87
|
+
klines = []
|
|
88
|
+
for k in data:
|
|
89
|
+
klines.append({
|
|
90
|
+
"open_time": k[0],
|
|
91
|
+
"open": float(k[1]),
|
|
92
|
+
"high": float(k[2]),
|
|
93
|
+
"low": float(k[3]),
|
|
94
|
+
"close": float(k[4]),
|
|
95
|
+
"volume": float(k[5]),
|
|
96
|
+
"close_time": k[6],
|
|
97
|
+
"quote_volume": float(k[7]),
|
|
98
|
+
"trades": int(k[8]),
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
return klines
|
|
102
|
+
|
|
103
|
+
def get_mark_price(self, symbol: str) -> dict: # type: ignore[type-arg]
|
|
104
|
+
"""Fetch mark price information."""
|
|
105
|
+
params = {"symbol": symbol}
|
|
106
|
+
data = self._request("GET", "/fapi/v1/premiumIndex", params=params)
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
"symbol": data["symbol"],
|
|
110
|
+
"mark_price": float(data["markPrice"]),
|
|
111
|
+
"index_price": float(data["indexPrice"]),
|
|
112
|
+
"funding_rate": float(data["lastFundingRate"]),
|
|
113
|
+
"next_funding_time": data["nextFundingTime"],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
def get_funding_rate_history(self, symbol: str, limit: int = 100) -> list[dict]: # type: ignore[type-arg]
|
|
117
|
+
"""Fetch historical funding rates."""
|
|
118
|
+
params = {
|
|
119
|
+
"symbol": symbol,
|
|
120
|
+
"limit": limit
|
|
121
|
+
}
|
|
122
|
+
return self._request("GET", "/fapi/v1/fundingRate", params=params)
|
|
123
|
+
|
|
124
|
+
def get_open_interest(self, symbol: str) -> dict: # type: ignore[type-arg]
|
|
125
|
+
"""Fetch open interest statistics."""
|
|
126
|
+
params = {"symbol": symbol}
|
|
127
|
+
data = self._request("GET", "/fapi/v1/openInterest", params=params)
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
"symbol": data["symbol"],
|
|
131
|
+
"open_interest": float(data["openInterest"]),
|
|
132
|
+
"timestamp": data["time"]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
def get_ticker_24hr(self, symbol: str) -> dict: # type: ignore[type-arg]
|
|
136
|
+
"""Fetch 24-hour price change statistics."""
|
|
137
|
+
params = {"symbol": symbol}
|
|
138
|
+
return self._request("GET", "/fapi/v1/ticker/24hr", params=params)
|
|
139
|
+
|
|
140
|
+
def get_depth(self, symbol: str, limit: int = 20) -> dict: # type: ignore[type-arg]
|
|
141
|
+
"""Fetch orderbook depth."""
|
|
142
|
+
params = {"symbol": symbol, "limit": limit}
|
|
143
|
+
return self._request("GET", "/fapi/v1/depth", params=params)
|
|
144
|
+
|
|
145
|
+
# ==================== Exchange Metadata ====================
|
|
146
|
+
|
|
147
|
+
def get_exchange_info(self) -> dict: # type: ignore[type-arg]
|
|
148
|
+
"""Fetch exchange information."""
|
|
149
|
+
return self._request("GET", "/fapi/v1/exchangeInfo")
|
|
150
|
+
|
|
151
|
+
def get_symbol_filters(self, symbol: str, force_refresh: bool = False) -> dict: # type: ignore[type-arg]
|
|
152
|
+
"""Fetch symbol filters (precision, min notional, etc.)."""
|
|
153
|
+
if symbol in self._symbol_filters and not force_refresh:
|
|
154
|
+
return self._symbol_filters[symbol]
|
|
155
|
+
|
|
156
|
+
exchange_info = self.get_exchange_info()
|
|
157
|
+
|
|
158
|
+
for s in exchange_info['symbols']:
|
|
159
|
+
if s['symbol'] == symbol:
|
|
160
|
+
filters: dict = {} # type: ignore[type-arg]
|
|
161
|
+
|
|
162
|
+
# Contract specifications
|
|
163
|
+
filters['contract_type'] = s.get('contractType', '')
|
|
164
|
+
filters['contract_size'] = float(s.get('contractSize', 1.0))
|
|
165
|
+
filters['contract_status'] = s.get('contractStatus', '')
|
|
166
|
+
filters['underlying_type'] = s.get('underlyingType', '')
|
|
167
|
+
|
|
168
|
+
# Precision settings
|
|
169
|
+
filters['price_precision'] = int(s.get('pricePrecision', 0))
|
|
170
|
+
filters['quantity_precision'] = int(s.get('quantityPrecision', 0))
|
|
171
|
+
filters['base_asset_precision'] = int(s.get('baseAssetPrecision', 0))
|
|
172
|
+
filters['quote_precision'] = int(s.get('quotePrecision', 0))
|
|
173
|
+
|
|
174
|
+
# Extract filter rules
|
|
175
|
+
for f in s['filters']:
|
|
176
|
+
filter_type = f['filterType']
|
|
177
|
+
|
|
178
|
+
if filter_type == 'PRICE_FILTER':
|
|
179
|
+
filters['tick_size'] = float(f['tickSize'])
|
|
180
|
+
filters['min_price'] = float(f['minPrice'])
|
|
181
|
+
filters['max_price'] = float(f['maxPrice'])
|
|
182
|
+
elif filter_type == 'LOT_SIZE':
|
|
183
|
+
filters['step_size'] = float(f['stepSize'])
|
|
184
|
+
filters['min_qty'] = float(f['minQty'])
|
|
185
|
+
filters['max_qty'] = float(f['maxQty'])
|
|
186
|
+
elif filter_type == 'NOTIONAL':
|
|
187
|
+
min_notional_val = (
|
|
188
|
+
f.get('minNotional') or
|
|
189
|
+
f.get('minNotionalValue') or
|
|
190
|
+
f.get('notional') or
|
|
191
|
+
f.get('notionalValue')
|
|
192
|
+
)
|
|
193
|
+
if min_notional_val:
|
|
194
|
+
filters['min_notional'] = float(min_notional_val)
|
|
195
|
+
else:
|
|
196
|
+
logger.warning(f"NOTIONAL filter found for {symbol} but no minNotional field")
|
|
197
|
+
|
|
198
|
+
max_notional_val = f.get('maxNotional') or f.get('maxNotionalValue')
|
|
199
|
+
if max_notional_val:
|
|
200
|
+
filters['max_notional'] = float(max_notional_val)
|
|
201
|
+
elif filter_type == 'MIN_NOTIONAL':
|
|
202
|
+
if 'min_notional' not in filters:
|
|
203
|
+
filters['min_notional'] = float(f.get('notional', f.get('notionalValue', 0)))
|
|
204
|
+
elif filter_type == 'MAX_NUM_ORDERS':
|
|
205
|
+
filters['max_num_orders'] = int(f.get('maxNumOrders', 0))
|
|
206
|
+
elif filter_type == 'MAX_NUM_ALGO_ORDERS':
|
|
207
|
+
filters['max_num_algo_orders'] = int(f.get('maxNumAlgoOrders', 0))
|
|
208
|
+
elif filter_type == 'PERCENT_PRICE':
|
|
209
|
+
filters['multiplier_up'] = float(f.get('multiplierUp', 0))
|
|
210
|
+
filters['multiplier_down'] = float(f.get('multiplierDown', 0))
|
|
211
|
+
filters['multiplier_decimal'] = float(f.get('multiplierDecimal', 0))
|
|
212
|
+
|
|
213
|
+
self._symbol_filters[symbol] = filters
|
|
214
|
+
return filters
|
|
215
|
+
|
|
216
|
+
raise ValueError(f"Symbol {symbol} not found")
|
|
217
|
+
|
|
218
|
+
def get_leverage_bracket(self, symbol: str | None = None, force_refresh: bool = False) -> dict: # type: ignore[type-arg]
|
|
219
|
+
"""Fetch leverage bracket information."""
|
|
220
|
+
cache_key = symbol or 'ALL'
|
|
221
|
+
|
|
222
|
+
if cache_key in self._leverage_brackets and not force_refresh:
|
|
223
|
+
return self._leverage_brackets[cache_key]
|
|
224
|
+
|
|
225
|
+
params: dict = {} # type: ignore[type-arg]
|
|
226
|
+
if symbol:
|
|
227
|
+
params['symbol'] = symbol
|
|
228
|
+
|
|
229
|
+
try:
|
|
230
|
+
data = self._request("GET", "/fapi/v1/leverageBracket", params=params)
|
|
231
|
+
self._leverage_brackets[cache_key] = data
|
|
232
|
+
return data
|
|
233
|
+
except Exception as e:
|
|
234
|
+
logger.debug(f"Leverage bracket endpoint not available: {e}")
|
|
235
|
+
return {}
|
|
236
|
+
|
|
237
|
+
# ==================== Account ====================
|
|
238
|
+
|
|
239
|
+
def get_account(self) -> dict: # type: ignore[type-arg]
|
|
240
|
+
"""Fetch account information."""
|
|
241
|
+
data = self._request("GET", "/fapi/v2/account", signed=True)
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
"total_wallet_balance": float(data["totalWalletBalance"]),
|
|
245
|
+
"total_unrealized_profit": float(data["totalUnrealizedProfit"]),
|
|
246
|
+
"total_margin_balance": float(data["totalMarginBalance"]),
|
|
247
|
+
"total_position_initial_margin": float(data["totalPositionInitialMargin"]),
|
|
248
|
+
"total_open_order_initial_margin": float(data["totalOpenOrderInitialMargin"]),
|
|
249
|
+
"available_balance": float(data["availableBalance"]),
|
|
250
|
+
"max_withdraw_amount": float(data["maxWithdrawAmount"]),
|
|
251
|
+
"assets": data.get("assets", []),
|
|
252
|
+
"positions": data.get("positions", [])
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
def get_positions(self, symbol: str | None = None) -> list[dict]: # type: ignore[type-arg]
|
|
256
|
+
"""Fetch current positions."""
|
|
257
|
+
params: dict = {} # type: ignore[type-arg]
|
|
258
|
+
if symbol:
|
|
259
|
+
params['symbol'] = symbol
|
|
260
|
+
|
|
261
|
+
data = self._request("GET", "/fapi/v2/positionRisk", signed=True, params=params)
|
|
262
|
+
|
|
263
|
+
positions = []
|
|
264
|
+
for p in data:
|
|
265
|
+
if float(p['positionAmt']) != 0:
|
|
266
|
+
positions.append({
|
|
267
|
+
"symbol": p["symbol"],
|
|
268
|
+
"position_amt": float(p["positionAmt"]),
|
|
269
|
+
"entry_price": float(p["entryPrice"]),
|
|
270
|
+
"mark_price": float(p["markPrice"]),
|
|
271
|
+
"unrealized_profit": float(p["unRealizedProfit"]),
|
|
272
|
+
"liquidation_price": float(p["liquidationPrice"]),
|
|
273
|
+
"leverage": int(p["leverage"]),
|
|
274
|
+
"margin_type": p["marginType"],
|
|
275
|
+
"isolated_margin": float(p.get("isolatedMargin", 0)),
|
|
276
|
+
"position_side": p.get("positionSide", "BOTH")
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
return positions
|
|
280
|
+
|
|
281
|
+
def get_balance(self) -> dict: # type: ignore[type-arg]
|
|
282
|
+
"""Fetch account balance summary."""
|
|
283
|
+
account = self.get_account()
|
|
284
|
+
return {
|
|
285
|
+
"available_balance": account["available_balance"],
|
|
286
|
+
"total_margin_balance": account["total_margin_balance"],
|
|
287
|
+
"total_unrealized_profit": account["total_unrealized_profit"]
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# ==================== Trading ====================
|
|
291
|
+
|
|
292
|
+
def set_leverage(self, symbol: str, leverage: int) -> dict: # type: ignore[type-arg]
|
|
293
|
+
"""Configure leverage for a symbol."""
|
|
294
|
+
params = {
|
|
295
|
+
"symbol": symbol,
|
|
296
|
+
"leverage": leverage
|
|
297
|
+
}
|
|
298
|
+
return self._request("POST", "/fapi/v1/leverage", signed=True, params=params)
|
|
299
|
+
|
|
300
|
+
def set_margin_type(self, symbol: str, margin_type: str = "ISOLATED") -> dict: # type: ignore[type-arg]
|
|
301
|
+
"""Configure margin mode."""
|
|
302
|
+
params = {
|
|
303
|
+
"symbol": symbol,
|
|
304
|
+
"marginType": margin_type
|
|
305
|
+
}
|
|
306
|
+
return self._request("POST", "/fapi/v1/marginType", signed=True, params=params)
|
|
307
|
+
|
|
308
|
+
def place_order(
|
|
309
|
+
self,
|
|
310
|
+
symbol: str,
|
|
311
|
+
side: str,
|
|
312
|
+
order_type: str = "LIMIT",
|
|
313
|
+
quantity: float | None = None,
|
|
314
|
+
price: float | None = None,
|
|
315
|
+
stop_price: float | None = None,
|
|
316
|
+
reduce_only: bool = False,
|
|
317
|
+
time_in_force: str = "GTC",
|
|
318
|
+
client_order_id: str | None = None,
|
|
319
|
+
**kwargs: dict # type: ignore[type-arg]
|
|
320
|
+
) -> dict: # type: ignore[type-arg]
|
|
321
|
+
"""Place an order."""
|
|
322
|
+
params: dict = { # type: ignore[type-arg]
|
|
323
|
+
"symbol": symbol,
|
|
324
|
+
"side": side,
|
|
325
|
+
"type": order_type,
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
filters: dict | None = None # type: ignore[type-arg]
|
|
329
|
+
|
|
330
|
+
if quantity is not None:
|
|
331
|
+
filters = filters or self.get_symbol_filters(symbol)
|
|
332
|
+
params["quantity"] = self._format_decimal(
|
|
333
|
+
quantity,
|
|
334
|
+
step=filters.get("step_size"),
|
|
335
|
+
precision=filters.get("quantity_precision"),
|
|
336
|
+
rounding=ROUND_DOWN
|
|
337
|
+
)
|
|
338
|
+
if price is not None:
|
|
339
|
+
filters = filters or self.get_symbol_filters(symbol)
|
|
340
|
+
params["price"] = self._format_decimal(
|
|
341
|
+
price,
|
|
342
|
+
step=filters.get("tick_size"),
|
|
343
|
+
precision=filters.get("price_precision"),
|
|
344
|
+
rounding=ROUND_HALF_UP
|
|
345
|
+
)
|
|
346
|
+
if stop_price is not None:
|
|
347
|
+
filters = filters or self.get_symbol_filters(symbol)
|
|
348
|
+
params["stopPrice"] = self._format_decimal(
|
|
349
|
+
stop_price,
|
|
350
|
+
step=filters.get("tick_size"),
|
|
351
|
+
precision=filters.get("price_precision"),
|
|
352
|
+
rounding=ROUND_HALF_UP
|
|
353
|
+
)
|
|
354
|
+
if reduce_only:
|
|
355
|
+
params["reduceOnly"] = "true"
|
|
356
|
+
if time_in_force and order_type == "LIMIT":
|
|
357
|
+
params["timeInForce"] = time_in_force
|
|
358
|
+
if client_order_id:
|
|
359
|
+
params["newClientOrderId"] = client_order_id
|
|
360
|
+
|
|
361
|
+
params.update(kwargs)
|
|
362
|
+
|
|
363
|
+
return self._request("POST", "/fapi/v1/order", signed=True, params=params)
|
|
364
|
+
|
|
365
|
+
def cancel_order(
|
|
366
|
+
self,
|
|
367
|
+
symbol: str,
|
|
368
|
+
order_id: int | None = None,
|
|
369
|
+
client_order_id: str | None = None
|
|
370
|
+
) -> dict: # type: ignore[type-arg]
|
|
371
|
+
"""Cancel a specific order."""
|
|
372
|
+
params = {"symbol": symbol}
|
|
373
|
+
|
|
374
|
+
if order_id:
|
|
375
|
+
params["orderId"] = order_id
|
|
376
|
+
elif client_order_id:
|
|
377
|
+
params["origClientOrderId"] = client_order_id
|
|
378
|
+
else:
|
|
379
|
+
raise ValueError("Must provide either order_id or client_order_id")
|
|
380
|
+
|
|
381
|
+
return self._request("DELETE", "/fapi/v1/order", signed=True, params=params)
|
|
382
|
+
|
|
383
|
+
def get_order(
|
|
384
|
+
self,
|
|
385
|
+
symbol: str,
|
|
386
|
+
order_id: int | None = None,
|
|
387
|
+
client_order_id: str | None = None
|
|
388
|
+
) -> dict: # type: ignore[type-arg]
|
|
389
|
+
"""Query an order."""
|
|
390
|
+
params = {"symbol": symbol}
|
|
391
|
+
|
|
392
|
+
if order_id:
|
|
393
|
+
params["orderId"] = order_id
|
|
394
|
+
elif client_order_id:
|
|
395
|
+
params["origClientOrderId"] = client_order_id
|
|
396
|
+
else:
|
|
397
|
+
raise ValueError("Must provide either order_id or client_order_id")
|
|
398
|
+
|
|
399
|
+
return self._request("GET", "/fapi/v1/order", signed=True, params=params)
|
|
400
|
+
|
|
401
|
+
def get_open_orders(self, symbol: str | None = None) -> list[dict]: # type: ignore[type-arg]
|
|
402
|
+
"""Fetch open orders."""
|
|
403
|
+
params: dict = {} # type: ignore[type-arg]
|
|
404
|
+
if symbol:
|
|
405
|
+
params["symbol"] = symbol
|
|
406
|
+
|
|
407
|
+
return self._request("GET", "/fapi/v1/openOrders", signed=True, params=params)
|
|
408
|
+
|
|
409
|
+
def cancel_all_orders(self, symbol: str) -> dict: # type: ignore[type-arg]
|
|
410
|
+
"""Cancel all open orders for the symbol."""
|
|
411
|
+
params = {"symbol": symbol}
|
|
412
|
+
return self._request("DELETE", "/fapi/v1/allOpenOrders", signed=True, params=params)
|
|
413
|
+
|
|
414
|
+
# ==================== Advanced Trading ====================
|
|
415
|
+
|
|
416
|
+
def place_sl_tp_orders(
|
|
417
|
+
self,
|
|
418
|
+
symbol: str,
|
|
419
|
+
side: str,
|
|
420
|
+
quantity: float,
|
|
421
|
+
stop_loss_price: float | None = None,
|
|
422
|
+
take_profit_price: float | None = None,
|
|
423
|
+
trigger_type: str = "MARK_PRICE"
|
|
424
|
+
) -> dict: # type: ignore[type-arg]
|
|
425
|
+
"""Submit stop-loss and take-profit orders."""
|
|
426
|
+
filters = self.get_symbol_filters(symbol)
|
|
427
|
+
tick_size = filters.get("tick_size")
|
|
428
|
+
tick_decimal = Decimal(str(tick_size)) if tick_size else None
|
|
429
|
+
|
|
430
|
+
def _align_price(price: float | None) -> float | None:
|
|
431
|
+
if price is None or tick_decimal is None or tick_decimal <= 0:
|
|
432
|
+
return price
|
|
433
|
+
return float(Decimal(str(price)).quantize(tick_decimal, rounding=ROUND_HALF_UP))
|
|
434
|
+
|
|
435
|
+
stop_loss_price = _align_price(stop_loss_price)
|
|
436
|
+
take_profit_price = _align_price(take_profit_price)
|
|
437
|
+
|
|
438
|
+
result: dict = {"stop_loss": None, "take_profit": None} # type: ignore[type-arg]
|
|
439
|
+
|
|
440
|
+
if stop_loss_price:
|
|
441
|
+
sl_order = self.place_order(
|
|
442
|
+
symbol=symbol,
|
|
443
|
+
side=side,
|
|
444
|
+
order_type="STOP_MARKET",
|
|
445
|
+
quantity=quantity,
|
|
446
|
+
stop_price=stop_loss_price,
|
|
447
|
+
reduce_only=True,
|
|
448
|
+
workingType=trigger_type
|
|
449
|
+
)
|
|
450
|
+
result["stop_loss"] = sl_order
|
|
451
|
+
|
|
452
|
+
if take_profit_price:
|
|
453
|
+
tp_order = self.place_order(
|
|
454
|
+
symbol=symbol,
|
|
455
|
+
side=side,
|
|
456
|
+
order_type="TAKE_PROFIT_MARKET",
|
|
457
|
+
quantity=quantity,
|
|
458
|
+
stop_price=take_profit_price,
|
|
459
|
+
reduce_only=True,
|
|
460
|
+
workingType=trigger_type
|
|
461
|
+
)
|
|
462
|
+
result["take_profit"] = tp_order
|
|
463
|
+
|
|
464
|
+
return result
|
|
465
|
+
|
|
466
|
+
def close_position(self, symbol: str, percent: float = 100.0) -> dict: # type: ignore[type-arg]
|
|
467
|
+
"""Close an existing position by percentage."""
|
|
468
|
+
positions = self.get_positions(symbol)
|
|
469
|
+
|
|
470
|
+
if not positions:
|
|
471
|
+
return {"message": "No position to close"}
|
|
472
|
+
|
|
473
|
+
position = positions[0]
|
|
474
|
+
position_amt = position["position_amt"]
|
|
475
|
+
|
|
476
|
+
close_qty = abs(position_amt) * (percent / 100.0)
|
|
477
|
+
side = "SELL" if position_amt > 0 else "BUY"
|
|
478
|
+
|
|
479
|
+
return self.place_order(
|
|
480
|
+
symbol=symbol,
|
|
481
|
+
side=side,
|
|
482
|
+
order_type="MARKET",
|
|
483
|
+
quantity=close_qty,
|
|
484
|
+
reduce_only=True
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
# ==================== Helpers ====================
|
|
488
|
+
|
|
489
|
+
def validate_order_params(self, symbol: str, price: float, quantity: float) -> dict: # type: ignore[type-arg]
|
|
490
|
+
"""Validate order parameters against exchange filters."""
|
|
491
|
+
filters = self.get_symbol_filters(symbol)
|
|
492
|
+
|
|
493
|
+
tick_size = filters['tick_size']
|
|
494
|
+
adjusted_price = round(price / tick_size) * tick_size
|
|
495
|
+
|
|
496
|
+
step_size = filters['step_size']
|
|
497
|
+
adjusted_quantity = round(quantity / step_size) * step_size
|
|
498
|
+
|
|
499
|
+
notional = adjusted_price * adjusted_quantity
|
|
500
|
+
min_notional = filters.get('min_notional', 0)
|
|
501
|
+
|
|
502
|
+
validation = {
|
|
503
|
+
"valid": True,
|
|
504
|
+
"adjusted_price": adjusted_price,
|
|
505
|
+
"adjusted_quantity": adjusted_quantity,
|
|
506
|
+
"notional": notional,
|
|
507
|
+
"errors": []
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if adjusted_price < filters['min_price']:
|
|
511
|
+
validation["valid"] = False
|
|
512
|
+
validation["errors"].append(f"Price {adjusted_price} below minimum {filters['min_price']}")
|
|
513
|
+
|
|
514
|
+
if adjusted_quantity < filters['min_qty']:
|
|
515
|
+
validation["valid"] = False
|
|
516
|
+
validation["errors"].append(f"Quantity {adjusted_quantity} below minimum {filters['min_qty']}")
|
|
517
|
+
|
|
518
|
+
if notional < min_notional:
|
|
519
|
+
validation["valid"] = False
|
|
520
|
+
validation["errors"].append(f"Notional {notional} below minimum {min_notional}")
|
|
521
|
+
|
|
522
|
+
return validation
|
|
523
|
+
|
|
524
|
+
def calculate_liquidation_price(
|
|
525
|
+
self,
|
|
526
|
+
entry_price: float,
|
|
527
|
+
leverage: int,
|
|
528
|
+
side: str,
|
|
529
|
+
maintenance_margin_rate: float = 0.005
|
|
530
|
+
) -> float:
|
|
531
|
+
"""Calculate approximate liquidation price."""
|
|
532
|
+
if side == "LONG":
|
|
533
|
+
liq_price = entry_price * (1 - (1 / leverage) + maintenance_margin_rate)
|
|
534
|
+
else:
|
|
535
|
+
liq_price = entry_price * (1 + (1 / leverage) - maintenance_margin_rate)
|
|
536
|
+
|
|
537
|
+
return liq_price
|
|
538
|
+
|
|
539
|
+
# ==================== Order History ====================
|
|
540
|
+
|
|
541
|
+
def get_all_orders(
|
|
542
|
+
self,
|
|
543
|
+
symbol: str,
|
|
544
|
+
start_time: int | None = None,
|
|
545
|
+
end_time: int | None = None,
|
|
546
|
+
limit: int = 500,
|
|
547
|
+
) -> list[dict]: # type: ignore[type-arg]
|
|
548
|
+
"""
|
|
549
|
+
Fetch historical order records.
|
|
550
|
+
|
|
551
|
+
Aster API: GET /fapi/v1/allOrders
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
symbol: Trading pair, e.g., "BTCUSDT"
|
|
555
|
+
start_time: Start timestamp (milliseconds)
|
|
556
|
+
end_time: End timestamp (milliseconds)
|
|
557
|
+
limit: Max records to return, default 500
|
|
558
|
+
|
|
559
|
+
Returns:
|
|
560
|
+
List of orders
|
|
561
|
+
"""
|
|
562
|
+
params: dict = {"symbol": symbol} # type: ignore[type-arg]
|
|
563
|
+
if start_time:
|
|
564
|
+
params["startTime"] = start_time
|
|
565
|
+
if end_time:
|
|
566
|
+
params["endTime"] = end_time
|
|
567
|
+
if limit:
|
|
568
|
+
params["limit"] = min(limit, 1000)
|
|
569
|
+
|
|
570
|
+
return self._request("GET", "/fapi/v1/allOrders", signed=True, params=params)
|
|
571
|
+
|
|
572
|
+
def get_user_trades(
|
|
573
|
+
self,
|
|
574
|
+
symbol: str,
|
|
575
|
+
start_time: int | None = None,
|
|
576
|
+
end_time: int | None = None,
|
|
577
|
+
limit: int = 500,
|
|
578
|
+
) -> list[dict]: # type: ignore[type-arg]
|
|
579
|
+
"""
|
|
580
|
+
Fetch trade fill records.
|
|
581
|
+
|
|
582
|
+
Aster API: GET /fapi/v1/userTrades
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
symbol: Trading pair, e.g., "BTCUSDT"
|
|
586
|
+
start_time: Start timestamp (milliseconds)
|
|
587
|
+
end_time: End timestamp (milliseconds)
|
|
588
|
+
limit: Max records to return, default 500
|
|
589
|
+
|
|
590
|
+
Returns:
|
|
591
|
+
List of trades (includes id, price, qty, commission, realizedPnl)
|
|
592
|
+
"""
|
|
593
|
+
params: dict = {"symbol": symbol} # type: ignore[type-arg]
|
|
594
|
+
if start_time:
|
|
595
|
+
params["startTime"] = start_time
|
|
596
|
+
if end_time:
|
|
597
|
+
params["endTime"] = end_time
|
|
598
|
+
if limit:
|
|
599
|
+
params["limit"] = min(limit, 1000)
|
|
600
|
+
|
|
601
|
+
return self._request("GET", "/fapi/v1/userTrades", signed=True, params=params)
|