Skip to content

Commit fbe1fd3

Browse files
committed
PriceUpdate object with timestamp
1 parent 57bd3b8 commit fbe1fd3

File tree

5 files changed

+63
-47
lines changed

5 files changed

+63
-47
lines changed

apps/hip-3-pusher/src/hermes_listener.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import websockets
66

77
from config import Config
8-
from price_state import PriceState
8+
from price_state import PriceState, PriceUpdate
99

1010

1111
class HermesListener:
@@ -72,10 +72,10 @@ def parse_hermes_message(self, data):
7272
expo = price_object["expo"]
7373
publish_time = price_object["publish_time"]
7474
logger.debug("Hermes update: {} {} {} {}", id, price, expo, publish_time)
75+
now = time.time()
7576
if id == self.base_feed_id:
76-
self.price_state.hermes_base_price = price
77+
self.price_state.hermes_base_price = PriceUpdate(price, now)
7778
if id == self.quote_feed_id:
78-
self.price_state.hermes_quote_price = price
79-
self.price_state.latest_hermes_timestamp = time.time()
79+
self.price_state.hermes_quote_price = PriceUpdate(price, now)
8080
except Exception as e:
8181
logger.error("parse_hermes_message error: {}", e)

apps/hip-3-pusher/src/hyperliquid_listener.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import time
66

77
from config import Config
8-
from price_state import PriceState
8+
from price_state import PriceState, PriceUpdate
99

1010
# This will be in config, but note here.
1111
# Other RPC providers exist but so far we've seen their support is incomplete.
@@ -51,21 +51,27 @@ async def subscribe_single_inner(self, url):
5151
async for message in ws:
5252
try:
5353
data = json.loads(message)
54-
if "ctx" not in message:
55-
# TODO: Should check subscription status response
56-
logger.debug("HL update without ctx: {}", message)
57-
continue
58-
self.parse_hyperliquid_ws_message(data)
54+
channel = data.get("channel", None)
55+
if not channel:
56+
logger.error("No channel in message: {}", data)
57+
elif channel == "subscriptionResponse":
58+
logger.debug("Received subscription response: {}", data)
59+
elif channel == "error":
60+
logger.error("Received Hyperliquid error response: {}", data)
61+
elif channel == "activeAssetCtx":
62+
self.parse_hyperliquid_ws_message(data)
63+
else:
64+
logger.error("Received unknown channel: {}", channel)
5965
except json.JSONDecodeError as e:
60-
logger.error("Failed to decode JSON message: {}", e)
66+
logger.error("Failed to decode JSON message: {} error: {}", message, e)
6167

6268
def parse_hyperliquid_ws_message(self, message):
6369
try:
6470
ctx = message["data"]["ctx"]
65-
self.price_state.hl_oracle_price = ctx["oraclePx"]
66-
self.price_state.hl_mark_price = ctx["markPx"]
71+
now = time.time()
72+
self.price_state.hl_oracle_price = PriceUpdate(ctx["oraclePx"], now)
73+
self.price_state.hl_mark_price = PriceUpdate(ctx["markPx"], now)
6774
logger.debug("on_activeAssetCtx: oraclePx: {} marketPx: {}", self.price_state.hl_oracle_price,
6875
self.price_state.hl_mark_price)
69-
self.price_state.latest_hl_timestamp = time.time()
7076
except Exception as e:
7177
logger.error("parse_hyperliquid_ws_message error: message: {} e: {}", e)

apps/hip-3-pusher/src/lazer_listener.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import websockets
66

77
from config import Config
8-
from price_state import PriceState
8+
from price_state import PriceState, PriceUpdate
99

1010

1111
class LazerListener:
@@ -75,15 +75,15 @@ def parse_lazer_message(self, data):
7575
return
7676
price_feeds = data["parsed"]["priceFeeds"]
7777
logger.debug("price_feeds: {}", price_feeds)
78+
now = time.time()
7879
for feed_update in price_feeds:
7980
feed_id = feed_update.get("priceFeedId", None)
8081
price = feed_update.get("price", None)
8182
if feed_id is None or price is None:
8283
continue
8384
if feed_id == self.base_feed_id:
84-
self.price_state.lazer_base_price = price
85+
self.price_state.lazer_base_price = PriceUpdate(price, now)
8586
if feed_id == self.quote_feed_id:
86-
self.price_state.lazer_quote_price = price
87-
self.price_state.latest_lazer_timestamp = time.time()
87+
self.price_state.lazer_quote_price = PriceUpdate(price, now)
8888
except Exception as e:
8989
logger.error("parse_lazer_message error: {}", e)

apps/hip-3-pusher/src/price_state.py

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,70 +6,78 @@
66
DEFAULT_STALE_PRICE_THRESHOLD_SECONDS = 5
77

88

9+
class PriceUpdate:
10+
def __init__(self, price, timestamp):
11+
self.price = price
12+
self.timestamp = timestamp
13+
14+
def __str__(self):
15+
return f"PriceUpdate(price={self.price}, timestamp={self.timestamp})"
16+
17+
def time_diff(self, now):
18+
return now - self.timestamp
19+
20+
921
class PriceState:
1022
"""
1123
Maintain latest prices seen across listeners and publisher.
1224
"""
1325
def __init__(self, config: Config):
1426
self.stale_price_threshold_seconds = config.stale_price_threshold_seconds
15-
now = time.time()
1627

17-
self.hl_oracle_price = None
18-
self.hl_mark_price = None
19-
self.latest_hl_timestamp = now
28+
self.hl_oracle_price: PriceUpdate | None = None
29+
self.hl_mark_price: PriceUpdate | None = None
2030

21-
self.lazer_base_price = None
31+
self.lazer_base_price: PriceUpdate | None = None
2232
self.lazer_base_exponent = config.lazer.base_feed_exponent
23-
self.lazer_quote_price = None
33+
self.lazer_quote_price: PriceUpdate | None = None
2434
self.lazer_quote_exponent = config.lazer.quote_feed_exponent
25-
self.latest_lazer_timestamp = now
2635

27-
self.hermes_base_price = None
36+
self.hermes_base_price: PriceUpdate | None = None
2837
self.hermes_base_exponent = config.hermes.base_feed_exponent
29-
self.hermes_quote_price = None
38+
self.hermes_quote_price: PriceUpdate | None = None
3039
self.hermes_quote_exponent = config.hermes.quote_feed_exponent
31-
self.latest_hermes_timestamp = now
3240

3341
def get_current_oracle_price(self):
3442
now = time.time()
3543
if self.hl_oracle_price:
36-
time_diff = now - self.latest_hl_timestamp
44+
time_diff = self.hl_oracle_price.time_diff(now)
3745
if time_diff < self.stale_price_threshold_seconds:
38-
return self.hl_oracle_price
46+
return self.hl_oracle_price.price
3947
else:
4048
logger.error("Hyperliquid oracle price stale by {} seconds", time_diff)
4149
else:
4250
logger.error("Hyperliquid oracle price not received yet")
4351

44-
# fall back to Hermes
45-
if self.hermes_base_price and self.hermes_quote_price:
46-
time_diff = now - self.latest_hermes_timestamp
47-
if time_diff < self.stale_price_threshold_seconds:
48-
return self.get_hermes_price()
49-
else:
50-
logger.error("Hermes price stale by {} seconds", time_diff)
51-
else:
52-
logger.error("Hermes base/quote prices not received yet")
53-
5452
# fall back to Lazer
5553
if self.lazer_base_price and self.lazer_quote_price:
56-
time_diff = now - self.latest_lazer_timestamp
57-
if time_diff < self.stale_price_threshold_seconds:
54+
max_time_diff = max(self.lazer_base_price.time_diff(now), self.lazer_quote_price.time_diff(now))
55+
if max_time_diff < self.stale_price_threshold_seconds:
5856
return self.get_lazer_price()
5957
else:
60-
logger.error("Lazer price stale by {} seconds", time_diff)
58+
logger.error("Lazer price stale by {} seconds", max_time_diff)
6159
else:
6260
logger.error("Lazer base/quote prices not received yet")
6361

62+
# fall back to Hermes
63+
if self.hermes_base_price and self.hermes_quote_price:
64+
max_time_diff = max(self.hermes_base_price.time_diff(now), self.hermes_quote_price.time_diff(now))
65+
if max_time_diff < self.stale_price_threshold_seconds:
66+
return self.get_hermes_price()
67+
else:
68+
logger.error("Hermes price stale by {} seconds", max_time_diff)
69+
else:
70+
logger.error("Hermes base/quote prices not received yet")
71+
6472
logger.error("All prices missing or stale!")
6573
return None
6674

6775
def get_hermes_price(self):
68-
base_price = float(self.hermes_base_price) / (10.0 ** -self.hermes_base_exponent)
69-
quote_price = float(self.hermes_quote_price) / (10.0 ** -self.hermes_quote_exponent)
76+
base_price = float(self.hermes_base_price.price) / (10.0 ** -self.hermes_base_exponent)
77+
quote_price = float(self.hermes_quote_price.price) / (10.0 ** -self.hermes_quote_exponent)
7078
return str(round(base_price / quote_price, 2))
7179

7280
def get_lazer_price(self):
73-
base_price = float(self.lazer_base_price) / (10.0 ** -self.lazer_base_exponent)
74-
quote_price = float(self.lazer_quote_price) / (10.0 ** -self.lazer_quote_exponent)
81+
base_price = float(self.lazer_base_price.price) / (10.0 ** -self.lazer_base_exponent)
82+
quote_price = float(self.lazer_quote_price.price) / (10.0 ** -self.lazer_quote_exponent)
7583
return str(round(base_price / quote_price, 2))

apps/hip-3-pusher/src/publisher.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ def publish(self):
6868
# mark_pxs.append({self.market_symbol: self.price_state.hl_mark_price})
6969

7070
external_perp_pxs = {}
71+
# TODO: "Each update can change oraclePx and markPx by at most 1%."
72+
# TODO: "The markPx cannot be updated such that open interest would be 10x the open interest cap."
7173

7274
if self.enable_publish:
7375
if self.enable_kms:

0 commit comments

Comments
 (0)