import logging 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 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, 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 mode: "paper" or "live" Returns: dict with stop order info, or None on failure. """ mode = mode or ("paper" if config.PAPER_TRADING else "live") stop_price = round(entry_price * (1 - config.STOP_LOSS_PCT), 8) sell_amount = -abs(amount) # negative = sell if mode == "paper": order_id = f"paper_sl_{int(time.time() * 1000)}" logger.info( "PAPER STOP-LOSS placed: %s sell %.6f @ %.6g (entry %.6g, SL %.1f%%)", symbol, abs(amount), stop_price, entry_price, config.STOP_LOSS_PCT * 100, ) 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: 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