bid, ask, last traded price

This commit is contained in:
2025-11-04 13:34:49 +01:00
parent 5f9109c3a9
commit 596fcde0bf
2 changed files with 214 additions and 115 deletions

View File

@ -3,6 +3,7 @@ import json
import time
import os
import traceback
import sys
from hyperliquid.info import Info
from hyperliquid.utils import constants
@ -28,38 +29,114 @@ def log_error(error_message: str, include_traceback: bool = True):
except Exception:
print(f"CRITICAL: Failed to write to error log file: {error_message}", file=sys.stderr)
def on_message(message, shared_prices_dict):
"""
Callback function to process incoming 'allMids' messages and update the
shared memory dictionary directly.
Callback function to process incoming WebSocket messages for 'bbo' and 'trades'
and update the shared memory dictionary.
"""
try:
if message.get("channel") == "allMids":
new_prices = message.get("data", {}).get("mids", {})
shared_prices_dict.update(new_prices)
logging.debug(f"Received WebSocket message: {message}")
channel = message.get("channel")
# --- Parser 1: Handle Best Bid/Offer messages ---
if channel == "bbo":
data = message.get("data")
if not data:
logging.warning("BBO message received with no data.")
return
coin = data.get("coin")
if not coin:
logging.warning("BBO data received with no coin identifier.")
return
bid_ask_data = data.get("bbo")
if not bid_ask_data or not isinstance(bid_ask_data, list) or len(bid_ask_data) < 2:
logging.warning(f"[{coin}] Received BBO message with invalid 'bbo' array: {bid_ask_data}")
return
try:
bid_price_str = bid_ask_data[0].get('px')
ask_price_str = bid_ask_data[1].get('px')
if not bid_price_str or not ask_price_str:
logging.warning(f"[{coin}] BBO data missing 'px' field.")
return
bid_price = float(bid_price_str)
ask_price = float(ask_price_str)
# Update the shared dictionary for Bid and Ask
shared_prices_dict[f"{coin}_bid"] = bid_price
shared_prices_dict[f"{coin}_ask"] = ask_price
logging.info(f"Updated {coin} (BBO): Bid={bid_price:.4f}, Ask={ask_price:.4f}")
except (ValueError, TypeError, IndexError) as e:
logging.error(f"[{coin}] Error parsing BBO data: {e}. Data: {bid_ask_data}")
# --- Parser 2: Handle Live Trade messages ---
elif channel == "trades":
trade_list = message.get("data")
if not trade_list or not isinstance(trade_list, list) or len(trade_list) == 0:
logging.warning(f"Received 'trades' message with invalid data: {trade_list}")
return
# Process all trades in the batch
for trade in trade_list:
try:
coin = trade.get("coin")
price_str = trade.get("px")
if not coin or not price_str:
logging.warning(f"Trade data missing 'coin' or 'px': {trade}")
continue
price = float(price_str)
# Update the shared dictionary for the "Live Price" column
shared_prices_dict[coin] = price
logging.info(f"Updated {coin} (Live Price) to last trade: {price:.4f}")
except (ValueError, TypeError) as e:
logging.error(f"Error parsing trade data: {e}. Data: {trade}")
except Exception as e:
log_error(f"Error in WebSocket on_message: {e}")
def start_live_feed(shared_prices_dict, log_level='off'):
def start_live_feed(shared_prices_dict, coins_to_watch: list, log_level='off'):
"""
Main function for the WebSocket process. It takes a shared dictionary
and continuously feeds it with live market data.
Includes a watchdog to auto-reconnect on failure.
Main function for the WebSocket process.
Subscribes to BOTH 'bbo' and 'trades' for all watched coins.
"""
setup_logging(log_level, 'LiveMarketFeed')
setup_logging(log_level, 'LiveMarketFeed_Combined')
info = None
callback = lambda msg: on_message(msg, shared_prices_dict)
def connect_and_subscribe():
"""Establishes a new WebSocket connection and subscribes to allMids."""
"""Establishes a new WebSocket connection and subscribes to both streams."""
try:
logging.info("Connecting to Hyperliquid WebSocket...")
# Ensure skip_ws=False to create the ws_manager
new_info = Info(constants.MAINNET_API_URL, skip_ws=False)
subscription = {"type": "allMids"}
new_info.subscribe(subscription, callback)
logging.info("WebSocket connected and subscribed to 'allMids'.")
# --- MODIFIED: Subscribe to 'bbo' AND 'trades' for each coin ---
for coin in coins_to_watch:
# Subscribe to Best Bid/Offer
bbo_sub = {"type": "bbo", "coin": coin}
new_info.subscribe(bbo_sub, callback)
logging.info(f"Subscribed to 'bbo' for {coin}.")
# Subscribe to Live Trades
trades_sub = {"type": "trades", "coin": coin}
new_info.subscribe(trades_sub, callback)
logging.info(f"Subscribed to 'trades' for {coin}.")
logging.info("WebSocket connected and all subscriptions sent.")
return new_info
except Exception as e:
log_error(f"Failed to connect to WebSocket: {e}")
@ -67,24 +144,28 @@ def start_live_feed(shared_prices_dict, log_level='off'):
info = connect_and_subscribe()
logging.info("Starting live price feed process. Press Ctrl+C in main app to stop.")
if info is None:
logging.critical("Initial WebSocket connection failed. Exiting process.")
log_error("Initial WebSocket connection failed. Exiting process.", include_traceback=False)
time.sleep(10) # Wait before letting the process manager restart it
return
logging.info("Starting Combined (BBO + Trades) live price feed process.")
try:
while True:
# --- Watchdog Logic ---
time.sleep(15) # Check the connection every 15 seconds
# --- FIX: Changed 'is_running()' to the correct method 'is_alive()' ---
if info is None or not info.ws_manager.is_alive():
error_msg = "WebSocket connection lost or not running. Attempting to reconnect..."
if not info.ws_manager.is_alive():
error_msg = "WebSocket connection lost. Attempting to reconnect..."
logging.warning(error_msg)
log_error(error_msg, include_traceback=False) # Log it to the file
if info and info.ws_manager: # Check if ws_manager exists before stopping
try:
info.ws_manager.stop() # Clean up old manager
except Exception as e:
log_error(f"Error stopping old ws_manager: {e}")
try:
info.ws_manager.stop() # Clean up old manager
except Exception as e:
log_error(f"Error stopping old ws_manager: {e}")
info = connect_and_subscribe()
@ -102,5 +183,5 @@ def start_live_feed(shared_prices_dict, log_level='off'):
finally:
if info and info.ws_manager:
info.ws_manager.stop()
logging.info("Listener stopped.")
logging.info("Combined Listener stopped.")