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,14 +376,22 @@ 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:
logger.error(f"Failed to set leverage on Bybit: {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): async def close_all_positions(self):
"""Closes any active position in the current category/symbol""" """Closes any active position in the current category/symbol"""
@ -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
category=self.category, symbol=self.symbol, orderId=order_id) history = await asyncio.to_thread(self.session.get_order_history,
if open_orders['retCode'] == 0 and not open_orders['result']['list']: category=self.category, symbol=self.symbol, orderId=order_id)
# Order filled (or manually cancelled) if history['retCode'] == 0 and history['result']['list']:
await self._process_filled_order(order_id, trade, qty_str, attempts=attempt) status = history['result']['list'][0]['orderStatus']
return 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 # 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,34 +621,37 @@ 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)
await asyncio.sleep(1.5) for _ in range(3):
try: await asyncio.sleep(1.5)
exec_info = await asyncio.to_thread(self.session.get_executions, try:
category=self.category, exec_info = await asyncio.to_thread(self.session.get_executions,
symbol=self.symbol, category=self.category,
orderId=order_id) symbol=self.symbol,
orderId=order_id)
if exec_info['retCode'] == 0 and exec_info['result']['list']: if exec_info['retCode'] == 0 and exec_info['result']['list']:
fills = exec_info['result']['list'] fills = exec_info['result']['list']
exec_fee = sum(float(f.get('execFee', 0)) for f in fills) exec_fee = sum(float(f.get('execFee', 0)) for f in fills)
exec_pnl = sum(float(f.get('closedPnl', 0)) for f in fills) exec_pnl = sum(float(f.get('closedPnl', 0)) for f in fills)
exec_price = float(fills[0].get('execPrice', self.market_price)) exec_price = float(fills[0].get('execPrice', self.market_price))
if self.category == "inverse": if self.category == "inverse":
usd_fee = exec_fee * exec_price usd_fee = exec_fee * exec_price
usd_pnl = exec_pnl * exec_price usd_pnl = exec_pnl * exec_price
else: else:
usd_fee = exec_fee usd_fee = exec_fee
usd_pnl = exec_pnl usd_pnl = exec_pnl
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"Execution fetch error: {e}")
logger.error(f"Error processing filled order {order_id}: {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)