TradingAgents/tradingagents/dataflows/bybit.py

884 lines
32 KiB
Python

import hashlib
import hmac
import json
import time
from typing import Dict, Optional, List
from urllib.parse import urlencode
import requests
from tradingagents.config import settings
import json
from datetime import datetime, timedelta, timezone
import pandas as pd
from stockstats import StockDataFrame
def bybit_v5_request(method: str, path: str, params: Optional[Dict] = None, body: Optional[Dict] = None) -> Dict:
"""Generic signed HTTP request helper for Bybit V5 API."""
base_url = settings.BYBIT_BASE_URL.rstrip("/")
api_key = settings.BYBIT_API_KEY
api_secret = settings.BYBIT_API_SECRET
if not api_key or not api_secret:
raise ValueError("Missing BYBIT_API_KEY or BYBIT_API_SECRET")
timestamp = str(int(time.time() * 1000))
recv_window = "5000"
# Build query string for GET or body for POST
if method.upper() == "GET" and params:
query_string = urlencode(sorted(params.items()))
url = f"{base_url}{path}?{query_string}"
payload = query_string
else:
url = f"{base_url}{path}"
payload = json.dumps(body, separators=(',', ':')) if body else ""
# Create signature
sign_payload = f"{timestamp}{api_key}{recv_window}{payload}"
signature = hmac.new(
api_secret.encode('utf-8'),
sign_payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Headers
headers = {
"X-BAPI-API-KEY": api_key,
"X-BAPI-TIMESTAMP": timestamp,
"X-BAPI-RECV-WINDOW": recv_window,
"X-BAPI-SIGN": signature,
"Content-Type": "application/json"
}
# Make request
if method.upper() == "GET":
response = requests.get(url, headers=headers)
else:
response = requests.post(url, headers=headers, data=payload)
response.raise_for_status()
data = response.json()
if data.get("retCode") != 0:
raise ValueError(f"Bybit API error: {data.get('retMsg')}")
return data
def get_account_balance(symbol: str) -> dict:
"""
To determine total equity, available free margin for new trades, and locked capital.
Args:
symbol: Format "BASE/QUOTE" (e.g., "BTC/USDT")
quote_coin: The currency used for buying (e.g., "USDT")
"""
if "/" not in symbol:
return f"Error: Symbol '{symbol}' is not in the correct format. Please use 'BASE/QUOTE' format, e.g., 'BTC/USDT'."
base_coin, quote_coin = symbol.split("/")
# 1. Fetch all assets from Bybit (omitting 'coin' gets everything)
data = bybit_v5_request("GET", "/v5/account/wallet-balance", {
"accountType": "UNIFIED"
})
# 2. Parse the raw response
try:
raw_list = data["result"]["list"][0]["coin"]
# Convert list to a dictionary for easy lookup: {'BTC': {...}, 'USDT': {...}}
result = {
item["coin"]: {k: float(v) if v else 0.0 for k, v in item.items() if k != "coin"}
for item in raw_list
}
except (IndexError, KeyError, TypeError):
result = {"error": "Could not retrieve wallet balance"}
total_equity = sum(asset.get("usdValue", 0.0) for asset in result.values())
report = f"# Account Balance Report for {base_coin}/{quote_coin}\n"
report += f"** Total Equity: ${total_equity} **\n"
report += f"## {quote_coin} (Quote) Details:\n"
report += json.dumps(result.get(quote_coin, {}), indent=2) + "\n"
report += f"## {base_coin} (Base) Details:\n"
report += json.dumps(result.get(base_coin, {}), indent=2) + "\n"
return report
def get_symbol(base_coin: str, quote_coin: str) -> str:
"""
Safely retrieves the correct Bybit symbol (e.g., "BTCUSDT") for a given base/quote pair.
Args:
base_coin: The asset (e.g., "BTC")
quote_coin: The currency (e.g., "USDT")
category: "linear", "spot", or "inverse"
Returns:
The valid symbol string (e.g., "BTCUSDT") or None if not found.
"""
# 1. Query the API specifically for this Base Coin
# This filters the search on the server side, which is much faster.
params = {
"category": "spot",
"baseCoin": base_coin.upper(),
"limit": 20 # We only expect a few matches (e.g., BTCUSDT, BTC-PERP)
}
data = bybit_v5_request("GET", "/v5/market/instruments-info", params)
result = data.get("result", {})
instruments = result.get("list", [])
# 2. Find the exact match for the Quote Coin
# This handles cases where BTC might pair with USDT, USDC, or DAI
for item in instruments:
if item.get("quoteCoin") == quote_coin.upper() and item.get("baseCoin") == base_coin.upper():
return item.get("symbol")
# 3. Fallback/Error handling
return None
def get_open_orders(symbol: str) -> str:
"""
Fetches active orders and returns a text report analyzing capital lock-up and order age.
"""
if "/" not in symbol:
return f"Error: Symbol '{symbol}' is not in the correct format. Please use 'BASE/QUOTE' format, e.g., 'BTC/USDT'."
base_coin, quote_coin = symbol.split("/")
symbol = get_symbol(base_coin, quote_coin)
if not symbol:
return f"Error: No valid spot symbol found for {base_coin}/{quote_coin}"
# 1. Fetch Open Orders
data = bybit_v5_request("GET", "/v5/order/realtime", {
"category": "spot",
"symbol": symbol.upper(),
"openOnly": 0 # 0=Active orders (Pending)
})
result = data.get("result", {})
orders = result.get("list", [])
for i in range(len(orders)):
# try to change to float if can
for k, v in orders[i].items():
if k in ["orderLinkId", "orderId"]:
continue
try:
orders[i][k] = float(v)
except:
pass
# change createdTime and updatedTime to yyyy-mm-dd hh:mm:ss format (utc)
orders[i]["createdTime"] = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(orders[i]["createdTime"]/1000))
orders[i]["updatedTime"] = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(orders[i]["updatedTime"]/1000))
report = f"# Open Orders for {symbol.upper()}\n"
report += json.dumps(orders, indent=2)
return report
def get_market_data(symbol:str, start_date: str, end_date: str) -> str:
"""
Fetches historical Daily (1D) OHLCV data for a specific date range.
Args:
symbol: Format "BASE/QUOTE" (e.g., "BTC/USDT").
start_date: Format "YYYY-MM-DD" (Inclusive).
end_date: Format "YYYY-MM-DD" (Inclusive).
Returns:
A formatted CSV-style string report.
"""
if "/" not in symbol:
return f"Error: Symbol '{symbol}' is not in the correct format. Please use 'BASE/QUOTE' format, e.g., 'BTC/USDT'."
# 1. Convert Date Strings to Timestamps (ms)
try:
# Start of the start_date (00:00:00)
dt_start = datetime.strptime(start_date, "%Y-%m-%d")
ts_start = int(dt_start.timestamp() * 1000)
# End of the end_date (23:59:59) - Bybit 'end' parameter is inclusive if valid candle exists
dt_end = datetime.strptime(end_date, "%Y-%m-%d") + timedelta(days=1)
ts_end = int(dt_end.timestamp() * 1000)
except ValueError:
return "Error: Invalid date format. Please use YYYY-MM-DD."
# 2. Request Data from Bybit
# We use interval "D" for Daily. Limit 1000 covers ~2.7 years of daily data in one call.
base_coin, quote_coin = symbol.split("/")
symbol2 = get_symbol(base_coin, quote_coin)
if not symbol2:
return f"# Error: No valid spot symbol found for {base_coin}/{quote_coin}."
params = {
"category": "spot",
"symbol": symbol2.upper(),
"interval": "D",
"start": ts_start,
"end": ts_end,
"limit": 1000
}
data = bybit_v5_request("GET", "/v5/market/kline", params)
result = data.get("result", {})
raw_candles = result.get("list", []) # Returns [timestamp, open, high, low, close, volume, turnover]
if not raw_candles:
return f"# No market data found for {symbol.upper()} from {start_date} to {end_date}."
# 3. Format Data
# Bybit returns Newest -> Oldest. We reverse it to get chronological order (Oldest -> Newest).
raw_candles.reverse()
csv_lines = []
for candle in raw_candles:
# Parse Timestamp
ts_ms = int(candle[0])
date_str = datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc).strftime('%Y-%m-%d')
# Parse Values
open_p = candle[1]
high_p = candle[2]
low_p = candle[3]
close_p = candle[4]
volume = float(candle[5]) # Volume in Base Currency (e.g. BTC)
# Format Line: Date,Open,High,Low,Close,Volume
# Note: We omit Dividends/Stock Splits as requested
line = f"{date_str},{open_p},{high_p},{low_p},{close_p},{int(volume)}"
csv_lines.append(line)
# 4. Construct Final Report Header
current_time = datetime.now(tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')
header = [
f"# Crypto data for {symbol.upper()} from {start_date} to {end_date}",
f"# Total records: {len(csv_lines)}",
f"# Data retrieved on: {current_time}",
"",
"Date,Open,High,Low,Close,Volume"
]
return "\n".join(header + csv_lines)
def get_crypto_indicator_window(
symbol: str,
indicator: str,
curr_date: str,
look_back_days: int
) -> str:
"""
Calculates technical indicators for a crypto pair using Bybit data.
Fix: Uses index lookup for dates since stockstats sets 'date' as the index.
"""
if "/" not in symbol:
return f"Error: Symbol '{symbol}' is not in the correct format. Please use 'BASE/QUOTE' format, e.g., 'BTC/USDT'."
base_coin, quote_coin = symbol.split("/")
symbol2 = get_symbol(base_coin, quote_coin)
# 1. Define Supported Indicators (Same as before)
best_ind_params = {
"close_50_sma": (
"50 SMA: A medium-term trend indicator. "
"Usage: Identify trend direction and serve as dynamic support/resistance. "
"Tips: It lags price; combine with faster indicators for timely signals."
),
"close_200_sma": (
"200 SMA: A long-term trend benchmark. "
"Usage: Confirm overall market trend and identify golden/death cross setups. "
"Tips: It reacts slowly; best for strategic trend confirmation rather than frequent trading entries."
),
"close_10_ema": (
"10 EMA: A responsive short-term average. "
"Usage: Capture quick shifts in momentum and potential entry points. "
"Tips: Prone to noise in choppy markets; use alongside longer averages for filtering false signals."
),
# MACD Related
"macd": (
"MACD: Computes momentum via differences of EMAs. "
"Usage: Look for crossovers and divergence as signals of trend changes. "
"Tips: Confirm with other indicators in low-volatility or sideways markets."
),
"macds": (
"MACD Signal: An EMA smoothing of the MACD line. "
"Usage: Use crossovers with the MACD line to trigger trades. "
"Tips: Should be part of a broader strategy to avoid false positives."
),
"macdh": (
"MACD Histogram: Shows the gap between the MACD line and its signal. "
"Usage: Visualize momentum strength and spot divergence early. "
"Tips: Can be volatile; complement with additional filters in fast-moving markets."
),
# Momentum Indicators
"rsi": (
"RSI: Measures momentum to flag overbought/oversold conditions. "
"Usage: Apply 70/30 thresholds and watch for divergence to signal reversals. "
"Tips: In strong trends, RSI may remain extreme; always cross-check with trend analysis."
),
# Volatility Indicators
"boll": (
"Bollinger Middle: A 20 SMA serving as the basis for Bollinger Bands. "
"Usage: Acts as a dynamic benchmark for price movement. "
"Tips: Combine with the upper and lower bands to effectively spot breakouts or reversals."
),
"boll_ub": (
"Bollinger Upper Band: Typically 2 standard deviations above the middle line. "
"Usage: Signals potential overbought conditions and breakout zones. "
"Tips: Confirm signals with other tools; prices may ride the band in strong trends."
),
"boll_lb": (
"Bollinger Lower Band: Typically 2 standard deviations below the middle line. "
"Usage: Indicates potential oversold conditions. "
"Tips: Use additional analysis to avoid false reversal signals."
),
"atr": (
"ATR: Averages true range to measure volatility. "
"Usage: Set stop-loss levels and adjust position sizes based on current market volatility. "
"Tips: It's a reactive measure, so use it as part of a broader risk management strategy."
),
# Volume-Based Indicators
"vwma": (
"VWMA: A moving average weighted by volume. "
"Usage: Confirm trends by integrating price action with volume data. "
"Tips: Watch for skewed results from volume spikes; use in combination with other volume analyses."
),
"mfi": (
"MFI: The Money Flow Index is a momentum indicator that uses both price and volume to measure buying and selling pressure. "
"Usage: Identify overbought (>80) or oversold (<20) conditions and confirm the strength of trends or reversals. "
"Tips: Use alongside RSI or MACD to confirm signals; divergence between price and MFI can indicate potential reversals."
),
}
# 2. Calculate Date Range
target_date_dt = datetime.strptime(curr_date, "%Y-%m-%d")
start_window_dt = target_date_dt - timedelta(days=look_back_days)
# Buffer: Fetch 250 extra days before start_window for lagging indicators (like 200 SMA)
buffer_days = 250
fetch_start_dt = start_window_dt - timedelta(days=buffer_days)
# Convert to timestamps for Bybit (ms)
ts_start = int(fetch_start_dt.timestamp() * 1000)
ts_end = int((target_date_dt + timedelta(days=1)).timestamp() * 1000)
# 3. Fetch Data from Bybit
params = {
"category": "linear",
"symbol": symbol2.upper(),
"interval": "D",
"start": ts_start,
"end": ts_end,
"limit": 1000
}
# Ensure 'bybit_v5_request' is defined in your scope
data = bybit_v5_request("GET", "/v5/market/kline", params)
raw_list = data.get("result", {}).get("list", [])
if not raw_list:
return f"Error: No data found for {symbol2}."
# 4. Prepare DataFrame
parsed_data = []
for row in raw_list:
parsed_data.append({
# Standardize date format for the Index
"date": datetime.fromtimestamp(int(row[0])/1000).strftime('%Y-%m-%d'),
"open": float(row[1]),
"high": float(row[2]),
"low": float(row[3]),
"close": float(row[4]),
"volume": float(row[5])
})
df = pd.DataFrame(parsed_data)
# Sort Ascending (Oldest -> Newest) is CRITICAL for stockstats
df = df.sort_values("date").reset_index(drop=True)
# 5. Calculate Indicator
# retype(df) sets 'date' column as the Index!
stock = StockDataFrame.retype(df)
try:
# Trigger calculation
_ = stock[indicator]
except KeyError:
return f"Error: Could not calculate {indicator}. Check if supported."
# 6. Build Report
report_lines = []
current_check_date = target_date_dt
while current_check_date >= start_window_dt:
date_str = current_check_date.strftime('%Y-%m-%d')
# --- FIX: Use .loc to find the row by Index (Date) ---
try:
# Locate the row using the date string index
val = stock.loc[date_str][indicator]
# Format value
if isinstance(val, (float, int)):
val_str = f"{val:.4f}"
else:
val_str = str(val)
report_lines.append(f"{date_str}: {val_str}")
except KeyError:
# Date not found in index
report_lines.append(f"{date_str}: N/A (No Data)")
except Exception as e:
report_lines.append(f"{date_str}: Error ({str(e)})")
current_check_date -= timedelta(days=1)
# 7. Final Output
description = best_ind_params.get(indicator, "No description available.")
result_str = (
f"## {indicator} values for {symbol2} from {start_window_dt.strftime('%Y-%m-%d')} to {curr_date}:\n\n"
+ "\n".join(report_lines)
+ "\n\n"
+ description
)
return result_str
def get_crypto_indicators_bulk(
symbol: str,
indicators: List[str],
curr_date: str,
look_back_days: int
) -> str:
"""
Calculates multiple technical indicators for a crypto pair in one go.
Args:
symbol: e.g., "BTC/USDT"
indicators: List of keys, e.g. ["rsi", "close_200_sma", "macd"]
curr_date: "YYYY-MM-DD"
look_back_days: Days of history to show in the report.
"""
if "/" not in symbol:
return f"Error: Symbol '{symbol}' is not in the correct format. Please use 'BASE/QUOTE' format, e.g., 'BTC/USDT'."
base_coin, quote_coin = symbol.split("/")
symbol2 = get_symbol(base_coin, quote_coin)
# 1. Define Descriptions
best_ind_params = {
"close_50_sma": (
"50 SMA: A medium-term trend indicator. "
"Usage: Identify trend direction and serve as dynamic support/resistance. "
"Tips: It lags price; combine with faster indicators for timely signals."
),
"close_200_sma": (
"200 SMA: A long-term trend benchmark. "
"Usage: Confirm overall market trend and identify golden/death cross setups. "
"Tips: It reacts slowly; best for strategic trend confirmation rather than frequent trading entries."
),
"close_10_ema": (
"10 EMA: A responsive short-term average. "
"Usage: Capture quick shifts in momentum and potential entry points. "
"Tips: Prone to noise in choppy markets; use alongside longer averages for filtering false signals."
),
# MACD Related
"macd": (
"MACD: Computes momentum via differences of EMAs. "
"Usage: Look for crossovers and divergence as signals of trend changes. "
"Tips: Confirm with other indicators in low-volatility or sideways markets."
),
"macds": (
"MACD Signal: An EMA smoothing of the MACD line. "
"Usage: Use crossovers with the MACD line to trigger trades. "
"Tips: Should be part of a broader strategy to avoid false positives."
),
"macdh": (
"MACD Histogram: Shows the gap between the MACD line and its signal. "
"Usage: Visualize momentum strength and spot divergence early. "
"Tips: Can be volatile; complement with additional filters in fast-moving markets."
),
# Momentum Indicators
"rsi": (
"RSI: Measures momentum to flag overbought/oversold conditions. "
"Usage: Apply 70/30 thresholds and watch for divergence to signal reversals. "
"Tips: In strong trends, RSI may remain extreme; always cross-check with trend analysis."
),
# Volatility Indicators
"boll": (
"Bollinger Middle: A 20 SMA serving as the basis for Bollinger Bands. "
"Usage: Acts as a dynamic benchmark for price movement. "
"Tips: Combine with the upper and lower bands to effectively spot breakouts or reversals."
),
"boll_ub": (
"Bollinger Upper Band: Typically 2 standard deviations above the middle line. "
"Usage: Signals potential overbought conditions and breakout zones. "
"Tips: Confirm signals with other tools; prices may ride the band in strong trends."
),
"boll_lb": (
"Bollinger Lower Band: Typically 2 standard deviations below the middle line. "
"Usage: Indicates potential oversold conditions. "
"Tips: Use additional analysis to avoid false reversal signals."
),
"atr": (
"ATR: Averages true range to measure volatility. "
"Usage: Set stop-loss levels and adjust position sizes based on current market volatility. "
"Tips: It's a reactive measure, so use it as part of a broader risk management strategy."
),
# Volume-Based Indicators
"vwma": (
"VWMA: A moving average weighted by volume. "
"Usage: Confirm trends by integrating price action with volume data. "
"Tips: Watch for skewed results from volume spikes; use in combination with other volume analyses."
),
"mfi": (
"MFI: The Money Flow Index is a momentum indicator that uses both price and volume to measure buying and selling pressure. "
"Usage: Identify overbought (>80) or oversold (<20) conditions and confirm the strength of trends or reversals. "
"Tips: Use alongside RSI or MACD to confirm signals; divergence between price and MFI can indicate potential reversals."
),
}
# 2. Fetch Data (ONLY ONCE)
# ---------------------------------------------------------
target_date_dt = datetime.strptime(curr_date, "%Y-%m-%d")
start_window_dt = target_date_dt - timedelta(days=look_back_days)
# Buffer for lagging indicators
buffer_days = 250
fetch_start_dt = start_window_dt - timedelta(days=buffer_days)
ts_start = int(fetch_start_dt.timestamp() * 1000)
ts_end = int((target_date_dt + timedelta(days=1)).timestamp() * 1000)
params = {
"category": "linear",
"symbol": symbol2.upper(),
"interval": "D",
"start": ts_start,
"end": ts_end,
"limit": 1000
}
data = bybit_v5_request("GET", "/v5/market/kline", params)
raw_list = data.get("result", {}).get("list", [])
if not raw_list:
return f"Error: No data found for {symbol2}."
parsed_data = []
for row in raw_list:
parsed_data.append({
"date": datetime.fromtimestamp(int(row[0])/1000).strftime('%Y-%m-%d'),
"open": float(row[1]),
"high": float(row[2]),
"low": float(row[3]),
"close": float(row[4]),
"volume": float(row[5])
})
df = pd.DataFrame(parsed_data)
df = df.sort_values("date").reset_index(drop=True)
stock = StockDataFrame.retype(df)
# ---------------------------------------------------------
# 3. Process Each Indicator
final_report = [f"# Technical Indicators Report for {symbol2}\n" + '-' * 40]
for ind in indicators:
# Pre-calculation check
try:
# Trigger calculation of the specific column
_ = stock[ind]
except (KeyError, ValueError):
final_report.append(f"## Error: Indicator '{ind}' is not supported or failed to calculate.\n")
continue
# Generate Block Report
report_lines = []
current_check_date = target_date_dt
while current_check_date >= start_window_dt:
date_str = current_check_date.strftime('%Y-%m-%d')
try:
# Use .loc because 'date' is the index
val = stock.loc[date_str][ind]
if isinstance(val, (float, int)):
val_str = f"{val:.4f}"
else:
val_str = str(val)
report_lines.append(f"{date_str}: {val_str}")
except KeyError:
report_lines.append(f"{date_str}: N/A")
current_check_date -= timedelta(days=1)
description = best_ind_params.get(ind, "No description available.")
block = (
f"## {ind} values for {symbol2} from {start_window_dt.strftime('%Y-%m-%d')} to {curr_date}:\n\n"
+ "\n".join(report_lines)
+ "\n\n"
+ f"Description: {description}\n"
+ "-" * 40 # Separator line
)
final_report.append(block)
return "\n\n".join(final_report)
def get_order_status(order_id: str, category: str = "spot") -> Dict:
"""
Get order status by order ID.
Args:
order_id: Order ID to query
category: Trading category ("spot", "linear", "inverse")
Returns:
Dict containing order information
"""
params = {
"category": category.lower(),
"orderId": order_id
}
data = bybit_v5_request("GET", "/v5/order/realtime", params=params)
result = data.get("result", {})
orders = result.get("list", [])
return orders[0] if orders else {}
def cancel_order(order_id: str, symbol: str, category: str = "spot") -> Dict:
"""
Cancel an existing order.
Args:
order_id: Order ID to cancel
symbol: Trading pair symbol
category: Trading category ("spot", "linear", "inverse")
Returns:
Dict containing cancellation result
"""
body = {
"category": category.lower(),
"symbol": symbol.upper(),
"orderId": order_id
}
data = bybit_v5_request("POST", "/v5/order/cancel", body=body)
return data.get("result", {})
def get_order_history(
symbol: Optional[str] = None,
category: str = "spot",
limit: int = 20
) -> Dict:
"""
Get order history.
Args:
symbol: Optional trading pair to filter by
category: Trading category ("spot", "linear", "inverse")
limit: Number of records to return (max 50)
Returns:
Dict containing order history
"""
params = {
"category": category.lower(),
"limit": min(limit, 50)
}
if symbol:
params["symbol"] = symbol.upper()
data = bybit_v5_request("GET", "/v5/order/history", params=params)
return data.get("result", {})
def get_account_info(account_type: str = "UNIFIED") -> Dict:
"""
Get account information.
Args:
account_type: Account type ("UNIFIED")
Returns:
Dict containing account information
"""
params = {
"accountType": account_type
}
data = bybit_v5_request("GET", "/v5/account/info", params=params)
return data.get("result", {})
def place_order(
symbol: str,
side: str,
order_type: str,
qty: float,
price: Optional[float] = None,
stop_loss: Optional[float] = None,
take_profit: Optional[float] = None,
sl_limit_price: Optional[float] = None,
tp_limit_price: Optional[float] = None,
sl_order_type: str = "Market",
tp_order_type: str = "Market",
time_in_force: str = "GTC",
account_type: str = "UNIFIED",
category: str = "spot",
order_link_id: Optional[str] = None,
reduce_only: bool = False,
close_on_trigger: bool = False
) -> Dict:
"""
Place an order on Bybit with comprehensive support for spot trading.
Args:
symbol: Trading pair symbol (e.g., "BTCUSDT")
side: Order side ("Buy" or "Sell")
order_type: Order type ("Market", "Limit")
qty: Order quantity
price: Order price (required for Limit orders)
stop_loss: Stop loss trigger price
take_profit: Take profit trigger price
sl_limit_price: Stop loss limit price (for limit SL orders)
tp_limit_price: Take profit limit price (for limit TP orders)
sl_order_type: Stop loss order type ("Market" or "Limit")
tp_order_type: Take profit order type ("Market" or "Limit")
time_in_force: Time in force ("GTC", "IOC", "FOK", "PostOnly")
account_type: Account type ("UNIFIED")
category: Trading category ("spot", "linear", "inverse")
order_link_id: Custom order ID
reduce_only: Reduce only flag
close_on_trigger: Close on trigger flag
Returns:
Dict containing order result
Raises:
ValueError: If required parameters are missing or invalid
"""
# Validate required parameters
if not symbol or not side or not order_type:
raise ValueError("symbol, side, and order_type are required")
if qty <= 0:
raise ValueError("qty must be greater than 0")
# Validate order type and price requirement
if order_type.upper() == "LIMIT" and price is None:
raise ValueError("price is required for Limit orders")
# Validate side
if side.upper() not in ["BUY", "SELL"]:
raise ValueError("side must be 'Buy' or 'Sell'")
# Validate order type
valid_order_types = ["MARKET", "LIMIT"]
if order_type.upper() not in valid_order_types:
raise ValueError(f"order_type must be one of {valid_order_types}")
# Build order body
body = {
"category": category.lower(),
"symbol": symbol.upper(),
"side": side.capitalize(),
"orderType": order_type.capitalize(),
"qty": str(qty),
"timeInForce": time_in_force,
}
# Add price for limit orders
if price is not None:
body["price"] = str(price)
# Add stop loss with proper formatting
if stop_loss is not None:
body["stopLoss"] = str(stop_loss)
body["slOrderType"] = sl_order_type.capitalize()
# Add limit price for stop loss if specified
if sl_order_type.upper() == "LIMIT" and sl_limit_price is not None:
body["slLimitPrice"] = str(sl_limit_price)
# Add take profit with proper formatting
if take_profit is not None:
body["takeProfit"] = str(take_profit)
body["tpOrderType"] = tp_order_type.capitalize()
# Add limit price for take profit if specified
if tp_order_type.upper() == "LIMIT" and tp_limit_price is not None:
body["tpLimitPrice"] = str(tp_limit_price)
# Add optional parameters
if order_link_id:
body["orderLinkId"] = order_link_id
if reduce_only:
body["reduceOnly"] = True
if close_on_trigger:
body["closeOnTrigger"] = True
try:
data = bybit_v5_request("POST", "/v5/order/create", body=body)
return data.get("result", {})
except Exception as e:
raise ValueError(f"Failed to place order: {str(e)}")
def place_spot_order_with_sl_tp(
symbol: str,
side: str,
qty: float,
price: Optional[float] = None,
stop_loss_price: Optional[float] = None,
take_profit_price: Optional[float] = None,
sl_limit_price: Optional[float] = None,
tp_limit_price: Optional[float] = None,
sl_order_type: str = "Market",
tp_order_type: str = "Market",
order_type: str = "Limit",
time_in_force: str = "PostOnly"
) -> Dict:
"""
Convenience function to place a spot order with stop loss and take profit.
Args:
symbol: Trading pair symbol (e.g., "BTCUSDT")
side: Order side ("Buy" or "Sell")
qty: Order quantity
price: Limit price (None for market orders)
stop_loss_price: Stop loss trigger price
take_profit_price: Take profit trigger price
sl_limit_price: Stop loss limit price (for limit SL orders)
tp_limit_price: Take profit limit price (for limit TP orders)
sl_order_type: Stop loss order type ("Market" or "Limit")
tp_order_type: Take profit order type ("Market" or "Limit")
order_type: "Limit" or "Market"
time_in_force: Time in force ("GTC", "IOC", "FOK", "PostOnly")
Returns:
Dict containing order result
"""
return place_order(
symbol=symbol,
side=side,
order_type=order_type,
qty=qty,
price=price,
stop_loss=stop_loss_price,
take_profit=take_profit_price,
sl_limit_price=sl_limit_price,
tp_limit_price=tp_limit_price,
sl_order_type=sl_order_type,
tp_order_type=tp_order_type,
time_in_force=time_in_force,
category="spot"
)