fix: resolve invalid symbol error and refine DB queries (v1.5.5)
This commit is contained in:
@ -86,11 +86,10 @@ class DatabaseManager:
|
|||||||
|
|
||||||
class PingPongBot:
|
class PingPongBot:
|
||||||
def __init__(self, config_path="config/ping_pong_config.yaml"):
|
def __init__(self, config_path="config/ping_pong_config.yaml"):
|
||||||
self.version = "1.5.4"
|
self.version = "1.5.5"
|
||||||
with open(config_path, 'r') as f:
|
with open(config_path, 'r') as f:
|
||||||
self.config = yaml.safe_load(f)
|
self.config = yaml.safe_load(f)
|
||||||
|
|
||||||
# Explicitly load from ENV to ensure they are available
|
|
||||||
self.api_key = os.getenv("BYBIT_API_KEY") or os.getenv("API_KEY")
|
self.api_key = os.getenv("BYBIT_API_KEY") or os.getenv("API_KEY")
|
||||||
self.api_secret = os.getenv("BYBIT_API_SECRET") or os.getenv("API_SECRET")
|
self.api_secret = os.getenv("BYBIT_API_SECRET") or os.getenv("API_SECRET")
|
||||||
|
|
||||||
@ -104,7 +103,6 @@ class PingPongBot:
|
|||||||
timeout=10
|
timeout=10
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize DB with explicit credentials
|
|
||||||
self.db = DatabaseManager(
|
self.db = DatabaseManager(
|
||||||
host=os.getenv('DB_HOST', '20.20.20.20'),
|
host=os.getenv('DB_HOST', '20.20.20.20'),
|
||||||
port=os.getenv('DB_PORT', 5433),
|
port=os.getenv('DB_PORT', 5433),
|
||||||
@ -113,22 +111,17 @@ class PingPongBot:
|
|||||||
password=os.getenv('DB_PASSWORD', '')
|
password=os.getenv('DB_PASSWORD', '')
|
||||||
)
|
)
|
||||||
|
|
||||||
# Base settings - Improved extraction
|
|
||||||
raw_symbol = self.config['symbol'].upper()
|
raw_symbol = self.config['symbol'].upper()
|
||||||
# Remove common suffixes to get base coin (e.g., BTCUSD -> BTC, BTCUSDT -> BTC)
|
|
||||||
self.base_coin = raw_symbol.replace("USDT", "").replace("USDC", "").replace("USD", "")
|
self.base_coin = raw_symbol.replace("USDT", "").replace("USDC", "").replace("USD", "")
|
||||||
|
|
||||||
self.db_symbol = self.base_coin
|
self.db_symbol = self.base_coin
|
||||||
self.interval = str(self.config['interval'])
|
self.interval = str(self.config['interval'])
|
||||||
# Map interval to DB format: '30' -> '30m'
|
|
||||||
self.db_interval = self.interval + "m" if self.interval.isdigit() else self.interval
|
self.db_interval = self.interval + "m" if self.interval.isdigit() else self.interval
|
||||||
|
|
||||||
logger.info(f"Bot v{self.version} Initialized | DB Symbol: {self.db_symbol} | DB Interval: {self.db_interval}")
|
|
||||||
|
|
||||||
# Dynamic Strategy State
|
# Dynamic Strategy State
|
||||||
self.direction = None # 'long' or 'short'
|
self.direction = None
|
||||||
self.category = None # 'linear' or 'inverse'
|
self.category = None
|
||||||
self.symbol = None # 'BTCUSDC' or 'BTCUSD'
|
self.symbol = None
|
||||||
|
self.settle_coin = None
|
||||||
|
|
||||||
# Tracking for SMA(44, 1D)
|
# Tracking for SMA(44, 1D)
|
||||||
self.ma_44_val = 0.0
|
self.ma_44_val = 0.0
|
||||||
@ -150,7 +143,6 @@ class PingPongBot:
|
|||||||
self.start_time = datetime.now()
|
self.start_time = datetime.now()
|
||||||
self.console = Console()
|
self.console = Console()
|
||||||
|
|
||||||
# Fixed Parameters from Config
|
|
||||||
self.partial_exit_pct = float(self.config.get('partial_exit_pct', 0.15))
|
self.partial_exit_pct = float(self.config.get('partial_exit_pct', 0.15))
|
||||||
self.min_val_usd = float(self.config.get('min_position_value_usd', 15.0))
|
self.min_val_usd = float(self.config.get('min_position_value_usd', 15.0))
|
||||||
self.pos_size_margin = float(self.config.get('pos_size_margin', 20.0))
|
self.pos_size_margin = float(self.config.get('pos_size_margin', 20.0))
|
||||||
@ -162,14 +154,12 @@ class PingPongBot:
|
|||||||
return series.ewm(alpha=alpha, adjust=False).mean()
|
return series.ewm(alpha=alpha, adjust=False).mean()
|
||||||
|
|
||||||
def calculate_indicators(self, df):
|
def calculate_indicators(self, df):
|
||||||
# RSI
|
|
||||||
rsi_cfg = self.config['rsi']
|
rsi_cfg = self.config['rsi']
|
||||||
delta = df['close'].diff()
|
delta = df['close'].diff()
|
||||||
gain = delta.where(delta > 0, 0)
|
gain = delta.where(delta > 0, 0)
|
||||||
loss = -delta.where(delta < 0, 0)
|
loss = -delta.where(delta < 0, 0)
|
||||||
df['rsi'] = 100 - (100 / (1 + (self.rma(gain, rsi_cfg['period']) / self.rma(loss, rsi_cfg['period']))))
|
df['rsi'] = 100 - (100 / (1 + (self.rma(gain, rsi_cfg['period']) / self.rma(loss, rsi_cfg['period']))))
|
||||||
|
|
||||||
# Hurst
|
|
||||||
hurst_cfg = self.config['hurst']
|
hurst_cfg = self.config['hurst']
|
||||||
mcl = hurst_cfg['period'] / 2
|
mcl = hurst_cfg['period'] / 2
|
||||||
mcl_2 = int(round(mcl / 2))
|
mcl_2 = int(round(mcl / 2))
|
||||||
@ -189,14 +179,12 @@ class PingPongBot:
|
|||||||
return df
|
return df
|
||||||
|
|
||||||
async def update_direction(self):
|
async def update_direction(self):
|
||||||
"""Logic Point I: 1D MA44 check and Point II: Asset/Perp selection"""
|
|
||||||
try:
|
try:
|
||||||
logger.info(f"Checking direction based on SMA(44, 1D) for {self.db_symbol}...")
|
logger.info(f"Checking direction based on SMA(44, 1D) for {self.db_symbol}...")
|
||||||
candles_1d = await self.db.get_candles(self.db_symbol, "1d", limit=100)
|
candles_1d = await self.db.get_candles(self.db_symbol, "1d", limit=100)
|
||||||
|
|
||||||
if not candles_1d or len(candles_1d) < 44:
|
if not candles_1d or len(candles_1d) < 44:
|
||||||
got = len(candles_1d) if candles_1d else 0
|
got = len(candles_1d) if candles_1d else 0
|
||||||
logger.warning(f"Not enough 1D data for MA44. Got {got} candles for {self.db_symbol} / 1d.")
|
|
||||||
self.status_msg = f"Error: Need 44 1D candles (Got {got})"
|
self.status_msg = f"Error: Need 44 1D candles (Got {got})"
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -204,11 +192,8 @@ class PingPongBot:
|
|||||||
df_1d['close'] = df_1d['close'].astype(float)
|
df_1d['close'] = df_1d['close'].astype(float)
|
||||||
self.ma_44_val = df_1d['close'].rolling(window=44).mean().iloc[-1]
|
self.ma_44_val = df_1d['close'].rolling(window=44).mean().iloc[-1]
|
||||||
|
|
||||||
# Use BTCUSDT for price check if not initialized, otherwise use current symbol
|
# Use BTCUSDT (Linear) for reliable initial price check
|
||||||
ticker_symbol = self.symbol if self.symbol else f"{self.base_coin}USDT"
|
ticker = await asyncio.to_thread(self.session.get_tickers, category="linear", symbol=f"{self.base_coin}USDT")
|
||||||
ticker_cat = self.category if self.category else "linear"
|
|
||||||
|
|
||||||
ticker = await asyncio.to_thread(self.session.get_tickers, category=ticker_cat, symbol=ticker_symbol)
|
|
||||||
current_price = float(ticker['result']['list'][0]['lastPrice'])
|
current_price = float(ticker['result']['list'][0]['lastPrice'])
|
||||||
self.market_price = current_price
|
self.market_price = current_price
|
||||||
|
|
||||||
@ -227,9 +212,12 @@ class PingPongBot:
|
|||||||
if self.direction == "long":
|
if self.direction == "long":
|
||||||
self.category = "inverse"
|
self.category = "inverse"
|
||||||
self.symbol = f"{self.base_coin}USD"
|
self.symbol = f"{self.base_coin}USD"
|
||||||
|
self.settle_coin = self.base_coin
|
||||||
else:
|
else:
|
||||||
self.category = "linear"
|
self.category = "linear"
|
||||||
self.symbol = f"{self.base_coin}USDC"
|
# BTCPERP is the Bybit symbol for USDC Linear Perpetual (BTCUSDC display name)
|
||||||
|
self.symbol = "BTCPERP" if self.base_coin == "BTC" else f"{self.base_coin}USDC"
|
||||||
|
self.settle_coin = "USDC"
|
||||||
|
|
||||||
logger.info(f"Bot configured for {self.direction.upper()} | Symbol: {self.symbol} | Category: {self.category}")
|
logger.info(f"Bot configured for {self.direction.upper()} | Symbol: {self.symbol} | Category: {self.category}")
|
||||||
self.last_candle_time = None
|
self.last_candle_time = None
|
||||||
@ -242,7 +230,6 @@ class PingPongBot:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
async def close_all_positions(self):
|
async def close_all_positions(self):
|
||||||
"""Closes any active position in the current category/symbol"""
|
|
||||||
try:
|
try:
|
||||||
if not self.category or not self.symbol: return
|
if not self.category or not self.symbol: return
|
||||||
pos = await asyncio.to_thread(self.session.get_positions, category=self.category, symbol=self.symbol)
|
pos = await asyncio.to_thread(self.session.get_positions, category=self.category, symbol=self.symbol)
|
||||||
@ -255,17 +242,14 @@ class PingPongBot:
|
|||||||
logger.error(f"Error closing positions: {e}")
|
logger.error(f"Error closing positions: {e}")
|
||||||
|
|
||||||
async def swap_assets(self, target_direction):
|
async def swap_assets(self, target_direction):
|
||||||
"""Point II: Exchange BTC/USDC on Spot market"""
|
|
||||||
try:
|
try:
|
||||||
logger.info(f"Swapping assets for {target_direction.upper()} mode...")
|
logger.info(f"Swapping assets for {target_direction.upper()} mode...")
|
||||||
spot_symbol = f"{self.base_coin}USDC"
|
spot_symbol = f"{self.base_coin}USDC"
|
||||||
|
|
||||||
# Get Spot Balances
|
|
||||||
balance = await asyncio.to_thread(self.session.get_wallet_balance, category="spot", coin=f"{self.base_coin},USDC")
|
balance = await asyncio.to_thread(self.session.get_wallet_balance, category="spot", coin=f"{self.base_coin},USDC")
|
||||||
coins = {c['coin']: float(c['walletBalance']) for c in balance['result']['list'][0]['coin']}
|
coins = {c['coin']: float(c['walletBalance']) for c in balance['result']['list'][0]['coin']}
|
||||||
|
|
||||||
if target_direction == "short":
|
if target_direction == "short":
|
||||||
# Need USDC: Sell all BTC
|
|
||||||
btc_bal = coins.get(self.base_coin, 0)
|
btc_bal = coins.get(self.base_coin, 0)
|
||||||
if btc_bal > 0.0001:
|
if btc_bal > 0.0001:
|
||||||
logger.info(f"Spot: Selling {btc_bal} {self.base_coin} for USDC")
|
logger.info(f"Spot: Selling {btc_bal} {self.base_coin} for USDC")
|
||||||
@ -273,7 +257,6 @@ class PingPongBot:
|
|||||||
category="spot", symbol=spot_symbol, side="Sell", orderType="Market", qty=str(btc_bal)
|
category="spot", symbol=spot_symbol, side="Sell", orderType="Market", qty=str(btc_bal)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Need BTC: Buy BTC with all USDC
|
|
||||||
usdc_bal = coins.get("USDC", 0)
|
usdc_bal = coins.get("USDC", 0)
|
||||||
if usdc_bal > 1.0:
|
if usdc_bal > 1.0:
|
||||||
logger.info(f"Spot: Buying {self.base_coin} with {usdc_bal} USDC")
|
logger.info(f"Spot: Buying {self.base_coin} with {usdc_bal} USDC")
|
||||||
@ -282,27 +265,23 @@ class PingPongBot:
|
|||||||
qty=str(usdc_bal), marketUnit="quote"
|
qty=str(usdc_bal), marketUnit="quote"
|
||||||
)
|
)
|
||||||
|
|
||||||
await asyncio.sleep(2) # Wait for spot settlement
|
await asyncio.sleep(2)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Asset Swap Error: {e}")
|
logger.error(f"Asset Swap Error: {e}")
|
||||||
|
|
||||||
async def update_exchange_data(self):
|
async def update_exchange_data(self):
|
||||||
"""Fetch Price, Balance, Position every 15s"""
|
|
||||||
if not self.category or not self.symbol: return
|
if not self.category or not self.symbol: return
|
||||||
try:
|
try:
|
||||||
ticker = await asyncio.to_thread(self.session.get_tickers, category=self.category, symbol=self.symbol)
|
ticker = await asyncio.to_thread(self.session.get_tickers, category=self.category, symbol=self.symbol)
|
||||||
if ticker['retCode'] == 0:
|
if ticker['retCode'] == 0:
|
||||||
self.market_price = float(ticker['result']['list'][0]['lastPrice'])
|
self.market_price = float(ticker['result']['list'][0]['lastPrice'])
|
||||||
|
|
||||||
# settleCoin is only for USDC linear perpetuals
|
pos = await asyncio.to_thread(self.session.get_positions, category=self.category, symbol=self.symbol, settleCoin=self.settle_coin if self.category == "linear" else None)
|
||||||
settle_coin = "USDC" if (self.category == "linear" and "USDC" in self.symbol) else None
|
|
||||||
pos = await asyncio.to_thread(self.session.get_positions, category=self.category, symbol=self.symbol, settleCoin=settle_coin)
|
|
||||||
if pos['retCode'] == 0:
|
if pos['retCode'] == 0:
|
||||||
active = [p for p in pos['result']['list'] if float(p.get('size', 0)) > 0]
|
active = [p for p in pos['result']['list'] if float(p.get('size', 0)) > 0]
|
||||||
self.position = active[0] if active else None
|
self.position = active[0] if active else None
|
||||||
|
|
||||||
# Use appropriate coin for balance based on category
|
target_coin = self.settle_coin
|
||||||
target_coin = "USDC" if self.category == "linear" else self.base_coin
|
|
||||||
wallet = await asyncio.to_thread(self.session.get_wallet_balance, category=self.category, accountType="UNIFIED", coin=target_coin)
|
wallet = await asyncio.to_thread(self.session.get_wallet_balance, category=self.category, accountType="UNIFIED", coin=target_coin)
|
||||||
if wallet['retCode'] == 0:
|
if wallet['retCode'] == 0:
|
||||||
res_list = wallet['result']['list']
|
res_list = wallet['result']['list']
|
||||||
@ -416,17 +395,14 @@ class PingPongBot:
|
|||||||
try:
|
try:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
# 1. Periodically check direction (every 2m)
|
|
||||||
if now - self.last_ma_check_time >= 120:
|
if now - self.last_ma_check_time >= 120:
|
||||||
await self.update_direction()
|
await self.update_direction()
|
||||||
self.last_ma_check_time = now
|
self.last_ma_check_time = now
|
||||||
|
|
||||||
# 2. Exchange Sync (15s)
|
|
||||||
if now - last_exchange_update >= 15:
|
if now - last_exchange_update >= 15:
|
||||||
await self.update_exchange_data()
|
await self.update_exchange_data()
|
||||||
last_exchange_update = now
|
last_exchange_update = now
|
||||||
|
|
||||||
# 3. DB Sync (5s)
|
|
||||||
candles = await self.db.get_candles(self.db_symbol, self.db_interval, limit=100)
|
candles = await self.db.get_candles(self.db_symbol, self.db_interval, limit=100)
|
||||||
if candles:
|
if candles:
|
||||||
latest = candles[0]
|
latest = candles[0]
|
||||||
|
|||||||
Reference in New Issue
Block a user