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:
Gemini CLI
2026-03-08 22:07:11 +01:00
parent 2840d9b0b3
commit cd66a976de

View File

@ -5,6 +5,7 @@ import hmac
import hashlib import hashlib
import json import json
import logging import logging
from logging.handlers import RotatingFileHandler
import asyncio import asyncio
import pandas as pd import pandas as pd
import numpy as np import numpy as np
@ -30,14 +31,28 @@ load_dotenv()
log_level = os.getenv("LOG_LEVEL", "INFO") log_level = os.getenv("LOG_LEVEL", "INFO")
# Setup Logging # Setup Logging
logging.basicConfig( log_dir = "logs"
level=getattr(logging, log_level), os.makedirs(log_dir, exist_ok=True)
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', error_log_path = os.path.join(log_dir, "ping_pong_errors.log")
handlers=[
logging.StreamHandler() # Create logger
]
)
logger = logging.getLogger("PingPongBot") 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: class DatabaseManager:
"""Minimal Database Manager for the bot""" """Minimal Database Manager for the bot"""
@ -165,7 +180,7 @@ class PingPongStrategy:
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.8.2" self.version = "1.8.6"
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)
@ -361,13 +376,21 @@ class PingPongBot:
buyLeverage=str(self.leverage), buyLeverage=str(self.leverage),
sellLeverage=str(self.leverage) sellLeverage=str(self.leverage)
) )
# If pybit returns normally, check the retCode
if res['retCode'] == 0: if res['retCode'] == 0:
logger.info(f"Leverage successfully set to {self.leverage}x") logger.info(f"Leverage successfully set to {self.leverage}x")
elif res['retCode'] == 110043: # Leverage not modified elif res['retCode'] == 110043: # Leverage not modified
logger.info(f"Leverage is already {self.leverage}x") logger.info(f"Leverage is already {self.leverage}x")
else: else:
logger.warning(f"Bybit Leverage Warning: {res['retMsg']} (Code: {res['retCode']})") logger.warning(f"Bybit Leverage Warning: {res['retMsg']} (Code: {res['retCode']})")
except Exception as e: 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}") logger.error(f"Failed to set leverage on Bybit: {e}")
async def close_all_positions(self): async def close_all_positions(self):
@ -510,13 +533,21 @@ class PingPongBot:
max_retries = 5 max_retries = 5
for attempt in range(1, max_retries + 1): for attempt in range(1, max_retries + 1):
try: 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) ticker = await asyncio.to_thread(self.session.get_tickers, category=self.category, symbol=self.symbol)
if ticker['retCode'] == 0: if ticker['retCode'] == 0 and ticker['result']['list']:
self.market_price = float(ticker['result']['list'][0]['lastPrice']) 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)) 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, res = await asyncio.to_thread(self.session.place_order,
category=self.category, symbol=self.symbol, side=side, orderType="Limit", category=self.category, symbol=self.symbol, side=side, orderType="Limit",
@ -525,6 +556,17 @@ class PingPongBot:
) )
if res['retCode'] != 0: 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']}") logger.warning(f"Maker rejected (Try {attempt}): {res['retMsg']}")
await asyncio.sleep(2) await asyncio.sleep(2)
continue continue
@ -534,16 +576,38 @@ class PingPongBot:
# Monitor for fill (Wait 10 seconds) # Monitor for fill (Wait 10 seconds)
for _ in range(10): for _ in range(10):
await asyncio.sleep(1) 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) category=self.category, symbol=self.symbol, orderId=order_id)
if open_orders['retCode'] == 0 and not open_orders['result']['list']: if history['retCode'] == 0 and history['result']['list']:
# Order filled (or manually cancelled) status = history['result']['list'][0]['orderStatus']
if status == "Filled":
await self._process_filled_order(order_id, trade, qty_str, attempts=attempt) await self._process_filled_order(order_id, trade, qty_str, attempts=attempt)
return return
elif status in ["Cancelled", "Rejected", "Deactivated"]:
break # Go to retry
# Timeout: Cancel and retry # Timeout: Cancel and retry
await asyncio.to_thread(self.session.cancel_order, category=self.category, symbol=self.symbol, orderId=order_id) try:
logger.info(f"Maker {trade} timed out, cancelling and retrying ({attempt}/{max_retries})") 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: except Exception as e:
logger.error(f"Maker Chase Error (Try {attempt}): {e}") logger.error(f"Maker Chase Error (Try {attempt}): {e}")
@ -557,7 +621,8 @@ class PingPongBot:
self.last_signal = f"{trade} {qty_str}" self.last_signal = f"{trade} {qty_str}"
self.status_msg = f"Order Success: {trade} ({self.exec_type})" 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) await asyncio.sleep(1.5)
try: try:
exec_info = await asyncio.to_thread(self.session.get_executions, exec_info = await asyncio.to_thread(self.session.get_executions,
@ -581,10 +646,12 @@ class PingPongBot:
self.total_fees += usd_fee self.total_fees += usd_fee
self.total_realized_pnl += usd_pnl 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") await self.log_transaction(trade, qty_str, exec_price, pnl=usd_pnl, fee=usd_fee, attempts=attempts, status="Filled")
else: return
await self.log_transaction(trade, qty_str, self.market_price, attempts=attempts, status=f"Filled ({self.exec_type})")
except Exception as e: 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): def render_dashboard(self):
self.console.print("\n" + "="*60) self.console.print("\n" + "="*60)