884 lines
32 KiB
Python
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"
|
|
)
|