import logging import math import time import config from data_fetcher import _auth_post logger = logging.getLogger(__name__) def execute_trade(signal: dict, current_prices: dict, mode: str | None = None) -> dict | None: """Execute a trade signal. Returns trade result dict or None on failure.""" mode = mode or ("paper" if config.PAPER_TRADING else "live") symbol = signal["symbol"] action = signal["action"] amount_usdt = signal.get("amount_usdt", 0) price = current_prices.get(symbol, 0) if price <= 0: logger.error("No price for %s, cannot execute trade", symbol) return None # Calculate amount in base currency if action == "SELL" and signal.get("sell_amount"): # Truncate to 8 decimals (Bitfinex wallet precision) to avoid "not enough balance" amount = -math.floor(abs(signal["sell_amount"]) * 1e8) / 1e8 else: amount = amount_usdt / price if action == "SELL": amount = -abs(amount) # negative = sell on Bitfinex trade_result = { "symbol": symbol, "action": action, "amount": abs(amount), "amount_usdt": amount_usdt, "price": price, "timestamp": time.time(), "mode": mode, "reason": signal.get("reason", ""), "confidence": signal.get("confidence", 0), } if mode == "paper": trade_result["status"] = "filled" trade_result["order_id"] = f"paper_{int(time.time() * 1000)}" logger.info( "PAPER %s %s: %.6f @ %.6g (%.2f USDT)", action, symbol, abs(amount), price, amount_usdt, ) else: try: order = _submit_order(symbol, amount) trade_result["status"] = "submitted" trade_result["order_id"] = order.get("id", "unknown") logger.info( "LIVE %s %s: %.6f @ %.6g (%.2f USDT) — order %s", action, symbol, abs(amount), price, amount_usdt, trade_result["order_id"], ) except Exception as e: logger.error("Order submission failed for %s: %s", symbol, e) trade_result["status"] = "failed" trade_result["error"] = str(e) return trade_result def place_stop_loss_order(symbol: str, amount: float, entry_price: float, stop_price: float | None = None, mode: str | None = None) -> dict | None: """Place an EXCHANGE STOP order as stop-loss after a BUY. Args: symbol: Trading pair (e.g. tBTCUST) amount: Position size in base currency (positive, will be negated for sell) entry_price: The buy entry price stop_price: Explicit stop price (e.g. from ATR calc). Falls back to fixed % if None. mode: "paper" or "live" Returns: dict with stop order info, or None on failure. """ mode = mode or ("paper" if config.PAPER_TRADING else "live") # Skip if below exchange minimum order size min_amt = config.MIN_ORDER_AMOUNT.get(symbol, 0) if min_amt > 0 and abs(amount) < min_amt: logger.info("Skip stop-loss for %s: amount %.6f < min %s", symbol, abs(amount), min_amt) return None if stop_price is None: stop_price = round(entry_price * (1 - config.STOP_LOSS_PCT), 8) # Truncate to 8 decimals (Bitfinex wallet precision) to avoid "not enough balance" amount = math.floor(abs(amount) * 1e8) / 1e8 sell_amount = -amount # negative = sell if mode == "paper": order_id = f"paper_sl_{int(time.time() * 1000)}" sl_pct = (1 - stop_price / entry_price) * 100 if entry_price else 0 logger.info( "PAPER STOP-LOSS placed: %s sell %.6f @ %.6g (entry %.6g, SL %.1f%%)", symbol, abs(amount), stop_price, entry_price, sl_pct, ) return {"order_id": order_id, "stop_price": stop_price, "symbol": symbol, "mode": "paper"} # Live: submit EXCHANGE STOP order to Bitfinex try: body = { "type": "EXCHANGE STOP", "symbol": symbol, "amount": str(sell_amount), "price": str(stop_price), } result = _auth_post("/v2/auth/w/order/submit", body) order_id = "unknown" if isinstance(result, list) and len(result) > 4: order_data = result[4] if isinstance(order_data, list) and len(order_data) > 0: order = order_data[0] if isinstance(order_data[0], list) else order_data order_id = order[0] logger.info( "LIVE STOP-LOSS placed: %s sell %.6f @ %.6g — order %s", symbol, abs(amount), stop_price, order_id, ) return {"order_id": order_id, "stop_price": stop_price, "symbol": symbol, "mode": "live"} except Exception as e: logger.error("Failed to place stop-loss for %s: %s", symbol, e) return None def cancel_order(order_id, mode: str | None = None) -> bool: """Cancel an existing order by ID.""" mode = mode or ("paper" if config.PAPER_TRADING else "live") if mode == "paper": logger.info("PAPER cancel order: %s", order_id) return True try: _auth_post("/v2/auth/w/order/cancel", {"id": int(order_id)}) logger.info("LIVE cancel order: %s", order_id) return True except Exception as e: if "not found" in str(e).lower(): logger.info("Cancel order %s: already gone", order_id) return True logger.error("Failed to cancel order %s: %s", order_id, e) return False def _submit_order(symbol: str, amount: float) -> dict: """Submit an EXCHANGE MARKET order to Bitfinex.""" body = { "type": "EXCHANGE MARKET", "symbol": symbol, "amount": str(amount), } result = _auth_post("/v2/auth/w/order/submit", body) if isinstance(result, list) and len(result) > 4: order_data = result[4] if isinstance(order_data, list) and len(order_data) > 0: order = order_data[0] if isinstance(order_data[0], list) else order_data return {"id": order[0], "raw": order} return {"id": "unknown", "raw": result} def get_wallet_balance() -> float: """Get USDT balance from exchange wallet.""" if config.PAPER_TRADING and not config.BFX_API_KEY: return 0 try: wallets = _auth_post("/v2/auth/r/wallets") for w in wallets: if w[0] == "exchange" and w[1] in ("UST", "USDT"): return float(w[2]) except Exception as e: logger.error("Failed to get wallet balance: %s", e) return 0