diff --git a/pyproject.toml b/pyproject.toml index adc4381..b2979db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ ignore_missing_imports = true [tool.poetry] name = "pyth-observer" -version = "0.2.7" +version = "0.2.8" description = "Alerts and stuff" authors = [] readme = "README.md" diff --git a/pyth_observer/__init__.py b/pyth_observer/__init__.py index c7edfb6..8fa1d98 100644 --- a/pyth_observer/__init__.py +++ b/pyth_observer/__init__.py @@ -144,6 +144,7 @@ async def run(self): PublisherState( publisher_name=publisher_name, symbol=product.attrs["symbol"], + asset_type=product.attrs["asset_type"], public_key=component.publisher_key, confidence_interval=component.latest_price_info.confidence_interval, confidence_interval_aggregate=price_account.aggregate_price_info.confidence_interval, diff --git a/pyth_observer/check/publisher.py b/pyth_observer/check/publisher.py index b012a18..5636ae5 100644 --- a/pyth_observer/check/publisher.py +++ b/pyth_observer/check/publisher.py @@ -1,7 +1,10 @@ import time from dataclasses import dataclass +from datetime import datetime from typing import Dict, Protocol, runtime_checkable +from zoneinfo import ZoneInfo +from pythclient.calendar import is_market_open from pythclient.pythaccounts import PythPriceStatus from pythclient.solana import SolanaPublicKey @@ -14,6 +17,7 @@ class PublisherState: publisher_name: str symbol: str + asset_type: str public_key: SolanaPublicKey status: PythPriceStatus aggregate_status: PythPriceStatus @@ -139,6 +143,14 @@ def state(self) -> PublisherState: return self.__state def run(self) -> bool: + market_open = is_market_open( + self.__state.asset_type.lower(), + datetime.now(ZoneInfo("America/New_York")), + ) + + if not market_open: + return True + distance = self.__state.latest_block_slot - self.__state.slot # Pass if publisher slot is not too far from aggregate slot @@ -225,11 +237,26 @@ def __init__(self, state: PublisherState, config: PublisherCheckConfig): self.__stall_time_limit: int = int( config["stall_time_limit"] ) # Time in seconds + self.__max_slot_distance: int = int(config["max_slot_distance"]) def state(self) -> PublisherState: return self.__state def run(self) -> bool: + market_open = is_market_open( + self.__state.asset_type.lower(), + datetime.now(ZoneInfo("America/New_York")), + ) + + if not market_open: + return True + + distance = self.__state.latest_block_slot - self.__state.slot + + # Pass when publisher is offline because PublisherOfflineCheck will be triggered + if distance >= self.__max_slot_distance: + return True + publisher_key = (self.__state.publisher_name, self.__state.symbol) current_time = time.time() previous_price, last_change_time = PUBLISHER_CACHE.get( diff --git a/sample.config.yaml b/sample.config.yaml index d7abd91..f68650d 100644 --- a/sample.config.yaml +++ b/sample.config.yaml @@ -47,6 +47,7 @@ checks: PublisherStalledCheck: enable: true stall_time_limit: 60 + max_slot_distance: 25 # Per-symbol config Crypto.MNGO/USD: PriceFeedOfflineCheck: diff --git a/tests/test_checks_publisher.py b/tests/test_checks_publisher.py index 486c029..b38cfc5 100644 --- a/tests/test_checks_publisher.py +++ b/tests/test_checks_publisher.py @@ -23,6 +23,7 @@ def make_state( return PublisherState( publisher_name="publisher", symbol="Crypto.BTC/USD", + asset_type="Crypto", public_key=SolanaPublicKey("2hgu6Umyokvo8FfSDdMa9nDKhcdv9Q4VvGNhRCeSWeD3"), status=PythPriceStatus.TRADING, aggregate_status=PythPriceStatus.TRADING, @@ -62,8 +63,14 @@ def simulate_time_pass(seconds): current_time += seconds return current_time - def setup_check(state, stall_time_limit): - check = PublisherStalledCheck(state, {"stall_time_limit": stall_time_limit}) + def setup_check(state, stall_time_limit, max_slot_distance): + check = PublisherStalledCheck( + state, + { + "stall_time_limit": stall_time_limit, + "max_slot_distance": max_slot_distance, + }, + ) PUBLISHER_CACHE[(state.publisher_name, state.symbol)] = ( state.price, current_time, @@ -76,17 +83,17 @@ def run_check(check, seconds, expected): PUBLISHER_CACHE.clear() state_a = make_state(1, 100.0, 2.0, 1, 100.0, 1.0) - check_a = setup_check(state_a, 5) + check_a = setup_check(state_a, 5, 25) run_check(check_a, 5, True) # Should pass as it hits the limit exactly PUBLISHER_CACHE.clear() state_b = make_state(1, 100.0, 2.0, 1, 100.0, 1.0) - check_b = setup_check(state_b, 5) + check_b = setup_check(state_b, 5, 25) run_check(check_b, 6, False) # Should fail as it exceeds the limit PUBLISHER_CACHE.clear() state_c = make_state(1, 100.0, 2.0, 1, 100.0, 1.0) - check_c = setup_check(state_c, 5) + check_c = setup_check(state_c, 5, 25) run_check(check_c, 2, True) # Initial check should pass state_c.price = 105.0 # Change the price run_check(check_c, 3, True) # Should pass as price changes @@ -95,3 +102,11 @@ def run_check(check, seconds, expected): run_check( check_c, 8, False ) # Should fail as price stalls for too long after last change + + # Adding a check for when the publisher is offline + PUBLISHER_CACHE.clear() + state_d = make_state(1, 100.0, 2.0, 1, 100.0, 1.0) + state_d.latest_block_slot = 25 + state_d.slot = 0 + check_d = setup_check(state_d, 5, 25) + run_check(check_d, 10, True) # Should pass as the publisher is offline