bifitnex-trading/trader.py
kroutony 972d66ab1b Initial commit: LLM-driven crypto trading bot
Includes: Bitfinex API integration, technical indicators,
LLM signal generation, risk management, Slack notifications.

Recent fixes:
- SELL orders use position value instead of total balance
- SELL signals always close full position
- Failed orders added to rejected list for Slack reporting
- Position/exposure limits auto-cap to remaining room
- BUY order minimum raised to 10% of portfolio

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 03:25:18 +00:00

157 lines
5.5 KiB
Python

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