feat: implement Mid-Price logic and separate error logging (v1.8.6)
- Implement Mid-Price calculation (Bid+Ask)/2 for Maker orders to improve fill rates. - Add separate 'logs/ping_pong_errors.log' for WARNING and ERROR messages. - Add RotatingFileHandler for error logs (5MB cap, 2 backups). - Refine 'leverage not modified' (110043) handling from ERROR to INFO. - Improve order verification with explicit status checks and race condition handling. - Verify script syntax and update version to 1.8.6.
This commit is contained in:
@ -5,6 +5,7 @@ import hmac
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import asyncio
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
@ -30,14 +31,28 @@ load_dotenv()
|
||||
log_level = os.getenv("LOG_LEVEL", "INFO")
|
||||
|
||||
# Setup Logging
|
||||
logging.basicConfig(
|
||||
level=getattr(logging, log_level),
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
log_dir = "logs"
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
error_log_path = os.path.join(log_dir, "ping_pong_errors.log")
|
||||
|
||||
# Create logger
|
||||
logger = logging.getLogger("PingPongBot")
|
||||
logger.setLevel(logging.DEBUG) # Catch everything, handlers will filter
|
||||
|
||||
# Formatter
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
|
||||
# Console Handler (Normal logs)
|
||||
ch = logging.StreamHandler()
|
||||
ch.setLevel(getattr(logging, log_level))
|
||||
ch.setFormatter(formatter)
|
||||
logger.addHandler(ch)
|
||||
|
||||
# Error File Handler (Warnings and Errors only)
|
||||
fh = RotatingFileHandler(error_log_path, maxBytes=5*1024*1024, backupCount=2)
|
||||
fh.setLevel(logging.WARNING)
|
||||
fh.setFormatter(formatter)
|
||||
logger.addHandler(fh)
|
||||
|
||||
class DatabaseManager:
|
||||
"""Minimal Database Manager for the bot"""
|
||||
@ -165,7 +180,7 @@ class PingPongStrategy:
|
||||
|
||||
class PingPongBot:
|
||||
def __init__(self, config_path="config/ping_pong_config.yaml"):
|
||||
self.version = "1.8.2"
|
||||
self.version = "1.8.6"
|
||||
with open(config_path, 'r') as f:
|
||||
self.config = yaml.safe_load(f)
|
||||
|
||||
@ -361,13 +376,21 @@ class PingPongBot:
|
||||
buyLeverage=str(self.leverage),
|
||||
sellLeverage=str(self.leverage)
|
||||
)
|
||||
|
||||
# If pybit returns normally, check the retCode
|
||||
if res['retCode'] == 0:
|
||||
logger.info(f"Leverage successfully set to {self.leverage}x")
|
||||
elif res['retCode'] == 110043: # Leverage not modified
|
||||
logger.info(f"Leverage is already {self.leverage}x")
|
||||
else:
|
||||
logger.warning(f"Bybit Leverage Warning: {res['retMsg']} (Code: {res['retCode']})")
|
||||
|
||||
except Exception as e:
|
||||
# Check if exception contains "leverage not modified" or code 110043
|
||||
err_str = str(e)
|
||||
if "110043" in err_str or "leverage not modified" in err_str.lower():
|
||||
logger.info(f"Leverage is already correctly set ({self.leverage}x)")
|
||||
else:
|
||||
logger.error(f"Failed to set leverage on Bybit: {e}")
|
||||
|
||||
async def close_all_positions(self):
|
||||
@ -510,13 +533,21 @@ class PingPongBot:
|
||||
max_retries = 5
|
||||
for attempt in range(1, max_retries + 1):
|
||||
try:
|
||||
# Fresh market price for limit order
|
||||
# Fresh Bid/Ask for Mid-Price Limit order
|
||||
ticker = await asyncio.to_thread(self.session.get_tickers, category=self.category, symbol=self.symbol)
|
||||
if ticker['retCode'] == 0:
|
||||
self.market_price = float(ticker['result']['list'][0]['lastPrice'])
|
||||
if ticker['retCode'] == 0 and ticker['result']['list']:
|
||||
t = ticker['result']['list'][0]
|
||||
bid = float(t.get('bid1Price', 0))
|
||||
ask = float(t.get('ask1Price', 0))
|
||||
last = float(t.get('lastPrice', 0))
|
||||
|
||||
if bid > 0 and ask > 0:
|
||||
self.market_price = (bid + ask) / 2
|
||||
else:
|
||||
self.market_price = last
|
||||
|
||||
price_str = str(round(self.market_price, 1))
|
||||
self.status_msg = f"Chase {trade}: {attempt}/{max_retries} @ {price_str}"
|
||||
self.status_msg = f"Chase {trade}: {attempt}/{max_retries} @ {price_str} (Mid)"
|
||||
|
||||
res = await asyncio.to_thread(self.session.place_order,
|
||||
category=self.category, symbol=self.symbol, side=side, orderType="Limit",
|
||||
@ -525,6 +556,17 @@ class PingPongBot:
|
||||
)
|
||||
|
||||
if res['retCode'] != 0:
|
||||
# Specific check for race condition: order filled while trying to place/cancel
|
||||
if res['retCode'] in [110001, 170213, 170210]:
|
||||
# Check if actually filled
|
||||
history = await asyncio.to_thread(self.session.get_order_history,
|
||||
category=self.category, symbol=self.symbol, limit=1)
|
||||
if history['retCode'] == 0 and history['result']['list']:
|
||||
latest = history['result']['list'][0]
|
||||
if latest['orderStatus'] == "Filled" and float(latest['cumExecQty']) > 0:
|
||||
await self._process_filled_order(latest['orderId'], trade, qty_str, attempts=attempt)
|
||||
return
|
||||
|
||||
logger.warning(f"Maker rejected (Try {attempt}): {res['retMsg']}")
|
||||
await asyncio.sleep(2)
|
||||
continue
|
||||
@ -534,16 +576,38 @@ class PingPongBot:
|
||||
# Monitor for fill (Wait 10 seconds)
|
||||
for _ in range(10):
|
||||
await asyncio.sleep(1)
|
||||
open_orders = await asyncio.to_thread(self.session.get_open_orders,
|
||||
# Check order history for definitive status
|
||||
history = await asyncio.to_thread(self.session.get_order_history,
|
||||
category=self.category, symbol=self.symbol, orderId=order_id)
|
||||
if open_orders['retCode'] == 0 and not open_orders['result']['list']:
|
||||
# Order filled (or manually cancelled)
|
||||
if history['retCode'] == 0 and history['result']['list']:
|
||||
status = history['result']['list'][0]['orderStatus']
|
||||
if status == "Filled":
|
||||
await self._process_filled_order(order_id, trade, qty_str, attempts=attempt)
|
||||
return
|
||||
elif status in ["Cancelled", "Rejected", "Deactivated"]:
|
||||
break # Go to retry
|
||||
|
||||
# Timeout: Cancel and retry
|
||||
await asyncio.to_thread(self.session.cancel_order, category=self.category, symbol=self.symbol, orderId=order_id)
|
||||
logger.info(f"Maker {trade} timed out, cancelling and retrying ({attempt}/{max_retries})")
|
||||
try:
|
||||
cancel_res = await asyncio.to_thread(self.session.cancel_order, category=self.category, symbol=self.symbol, orderId=order_id)
|
||||
# Even if successful, double check if it filled in the last millisecond
|
||||
if cancel_res['retCode'] in [0, 110001, 170213]:
|
||||
history = await asyncio.to_thread(self.session.get_order_history,
|
||||
category=self.category, symbol=self.symbol, orderId=order_id)
|
||||
if history['retCode'] == 0 and history['result']['list'] and history['result']['list'][0]['orderStatus'] == "Filled":
|
||||
await self._process_filled_order(order_id, trade, qty_str, attempts=attempt)
|
||||
return
|
||||
except Exception as ce:
|
||||
# Handle exception for 110001
|
||||
if "110001" in str(ce) or "170213" in str(ce):
|
||||
history = await asyncio.to_thread(self.session.get_order_history,
|
||||
category=self.category, symbol=self.symbol, orderId=order_id)
|
||||
if history['retCode'] == 0 and history['result']['list'] and history['result']['list'][0]['orderStatus'] == "Filled":
|
||||
await self._process_filled_order(order_id, trade, qty_str, attempts=attempt)
|
||||
return
|
||||
logger.warning(f"Cancel error during chase: {ce}")
|
||||
|
||||
logger.info(f"Maker {trade} timed out, retrying ({attempt}/{max_retries})")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Maker Chase Error (Try {attempt}): {e}")
|
||||
@ -557,7 +621,8 @@ class PingPongBot:
|
||||
self.last_signal = f"{trade} {qty_str}"
|
||||
self.status_msg = f"Order Success: {trade} ({self.exec_type})"
|
||||
|
||||
# Wait for Bybit indexing
|
||||
# Wait for Bybit indexing (multiple attempts if needed)
|
||||
for _ in range(3):
|
||||
await asyncio.sleep(1.5)
|
||||
try:
|
||||
exec_info = await asyncio.to_thread(self.session.get_executions,
|
||||
@ -581,10 +646,12 @@ class PingPongBot:
|
||||
self.total_fees += usd_fee
|
||||
self.total_realized_pnl += usd_pnl
|
||||
await self.log_transaction(trade, qty_str, exec_price, pnl=usd_pnl, fee=usd_fee, attempts=attempts, status="Filled")
|
||||
else:
|
||||
await self.log_transaction(trade, qty_str, self.market_price, attempts=attempts, status=f"Filled ({self.exec_type})")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing filled order {order_id}: {e}")
|
||||
logger.error(f"Execution fetch error: {e}")
|
||||
|
||||
# Fallback if execution list is still empty after retries
|
||||
await self.log_transaction(trade, qty_str, self.market_price, attempts=attempts, status=f"Filled ({self.exec_type})")
|
||||
|
||||
def render_dashboard(self):
|
||||
self.console.print("\n" + "="*60)
|
||||
|
||||
Reference in New Issue
Block a user