|
5 | 5 | from math import isfinite |
6 | 6 |
|
7 | 7 | from app.services.live_trading.binance import BinanceFuturesClient |
| 8 | +from app.services.live_trading.binance_spot import BinanceSpotClient |
8 | 9 | from app.services.live_trading.bitget import BitgetMixClient |
9 | 10 | from app.services.live_trading.bitget_classic import BitgetClassicMixClient |
10 | 11 | from app.services.live_trading.bybit import BybitClient |
@@ -121,11 +122,7 @@ def binance_position_history(client, symbol, limit=50): |
121 | 122 | return {"supported": False, "positions": []} |
122 | 123 | native_symbol = to_binance_futures_symbol(symbol) |
123 | 124 | try: |
124 | | - rows = client._signed_request( |
125 | | - "GET", |
126 | | - "/fapi/v1/userTrades", |
127 | | - params={"symbol": native_symbol, "limit": 1000}, |
128 | | - ) |
| 125 | + rows = client.get_user_trades(symbol=symbol, limit=1000) |
129 | 126 | except Exception as exc: |
130 | 127 | logger.info("Binance native trade history unavailable: %s", exc) |
131 | 128 | return {"supported": False, "positions": []} |
@@ -221,6 +218,99 @@ def close(side, quantity, price, fee, gross_pnl, timestamp, row_id): |
221 | 218 | return {"supported": True, "positions": result[: max(1, int(limit or 50))]} |
222 | 219 |
|
223 | 220 |
|
| 221 | +def binance_trade_history(client, symbol, market_type="swap", limit=50): |
| 222 | + """Return exchange-owned Binance fills for the selected instrument.""" |
| 223 | + market = str(market_type or "swap").strip().lower() |
| 224 | + if isinstance(client, BinanceFuturesClient) and market == "swap": |
| 225 | + reader = client.get_user_trades |
| 226 | + elif isinstance(client, BinanceSpotClient) and market == "spot": |
| 227 | + reader = client.get_my_trades |
| 228 | + else: |
| 229 | + return {"supported": False, "trades": []} |
| 230 | + |
| 231 | + native_symbol = to_binance_futures_symbol(symbol) |
| 232 | + try: |
| 233 | + rows = reader(symbol=symbol, limit=max(1, min(int(limit or 50), 1000))) |
| 234 | + except Exception as exc: |
| 235 | + logger.info("Binance native trade history unavailable: %s", exc) |
| 236 | + return {"supported": False, "trades": []} |
| 237 | + |
| 238 | + net_quantity = 0.0 |
| 239 | + normalized = [] |
| 240 | + ordered = sorted( |
| 241 | + (row for row in (rows or []) if isinstance(row, dict)), |
| 242 | + key=lambda row: (_number(row.get("time")), _number(row.get("id") or row.get("tradeId"))), |
| 243 | + ) |
| 244 | + for row in ordered: |
| 245 | + if str(row.get("symbol") or "").upper() != native_symbol: |
| 246 | + continue |
| 247 | + quantity = abs(_number(row.get("qty") or row.get("quantity"))) |
| 248 | + price = _number(row.get("price")) |
| 249 | + if quantity <= 0 or price <= 0: |
| 250 | + continue |
| 251 | + side = str(row.get("side") or ("BUY" if row.get("isBuyer") else "SELL")).strip().lower() |
| 252 | + if side not in {"buy", "sell"}: |
| 253 | + continue |
| 254 | + position_side = str(row.get("positionSide") or "BOTH").strip().upper() |
| 255 | + realized_value = row.get("realizedPnl") |
| 256 | + realized_pnl = None if realized_value in (None, "") else _number(realized_value) |
| 257 | + is_close = False |
| 258 | + close_side = "" |
| 259 | + if market == "swap": |
| 260 | + if position_side == "LONG": |
| 261 | + is_close = side == "sell" |
| 262 | + close_side = "long" if is_close else "" |
| 263 | + elif position_side == "SHORT": |
| 264 | + is_close = side == "buy" |
| 265 | + close_side = "short" if is_close else "" |
| 266 | + else: |
| 267 | + delta = quantity if side == "buy" else -quantity |
| 268 | + if net_quantity > 1e-15 and delta < 0: |
| 269 | + is_close = True |
| 270 | + close_side = "long" |
| 271 | + elif net_quantity < -1e-15 and delta > 0: |
| 272 | + is_close = True |
| 273 | + close_side = "short" |
| 274 | + net_quantity += delta |
| 275 | + if abs(net_quantity) <= 1e-15: |
| 276 | + net_quantity = 0.0 |
| 277 | + if realized_pnl is not None and abs(realized_pnl) > 1e-15: |
| 278 | + is_close = True |
| 279 | + close_side = close_side or ("short" if side == "buy" else "long") |
| 280 | + |
| 281 | + commission_ccy = str(row.get("commissionAsset") or "").upper() |
| 282 | + commission = abs(_number(row.get("commission"))) |
| 283 | + quote_ccy = str(symbol or "").split("/", 1)[-1].split(":", 1)[0].upper() |
| 284 | + trade_id = str(row.get("id") or row.get("tradeId") or f"{row.get('orderId')}:{row.get('time')}") |
| 285 | + normalized.append({ |
| 286 | + "id": f"exchange-{trade_id}", |
| 287 | + "symbol": symbol, |
| 288 | + "side": side, |
| 289 | + "order_type": "fill", |
| 290 | + "amount": quantity * price, |
| 291 | + "price": 0.0, |
| 292 | + "leverage": 0, |
| 293 | + "market_type": market, |
| 294 | + "status": "filled", |
| 295 | + "exchange_order_id": str(row.get("orderId") or ""), |
| 296 | + "client_order_id": str(row.get("clientOrderId") or ""), |
| 297 | + "filled_amount": quantity, |
| 298 | + "avg_fill_price": price, |
| 299 | + "commission": commission, |
| 300 | + "commission_ccy": commission_ccy, |
| 301 | + "commission_quote": commission if not commission_ccy or commission_ccy == quote_ccy else None, |
| 302 | + "fee_status": "confirmed" if commission > 0 else "actual_zero", |
| 303 | + "realized_pnl": realized_pnl if is_close else None, |
| 304 | + "source": "exchange", |
| 305 | + "is_close": is_close, |
| 306 | + "close_side": close_side, |
| 307 | + "created_at": utc_time(row.get("time")), |
| 308 | + }) |
| 309 | + |
| 310 | + normalized.sort(key=lambda row: str(row.get("created_at") or ""), reverse=True) |
| 311 | + return {"supported": True, "trades": normalized[: max(1, int(limit or 50))]} |
| 312 | + |
| 313 | + |
224 | 314 | def bybit_position_history(client, symbol, limit=50): |
225 | 315 | """Return exchange-owned closed position cycles for Bybit linear contracts.""" |
226 | 316 | if not isinstance(client, BybitClient) or str(getattr(client, "category", "linear")) != "linear": |
|
0 commit comments