Skip to content

Commit 6d22396

Browse files
authored
Merge branch 'pyth-network:main' into feature/to_json
2 parents 89dd41a + 1e43edb commit 6d22396

File tree

8 files changed

+102
-34
lines changed

8 files changed

+102
-34
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Install the library:
1616

1717
You can then read the current Pyth price using the following:
1818

19-
```
19+
```python
2020
from pythclient.pythclient import PythClient
2121
from pythclient.pythaccounts import PythPriceAccount
2222
from pythclient.utils import get_key
@@ -34,6 +34,7 @@ async with PythClient(
3434
for _, pr in prices.items():
3535
print(
3636
pr.price_type,
37+
pr.aggregate_price_status,
3738
pr.aggregate_price,
3839
"p/m",
3940
pr.aggregate_price_confidence_interval,
@@ -44,11 +45,11 @@ This code snippet lists the products on pyth and the price for each product. Sam
4445

4546
```
4647
{'symbol': 'Crypto.ETH/USD', 'asset_type': 'Crypto', 'quote_currency': 'USD', 'description': 'ETH/USD', 'generic_symbol': 'ETHUSD', 'base': 'ETH'}
47-
PythPriceType.PRICE 4390.286 p/m 2.4331
48+
PythPriceType.PRICE PythPriceStatus.TRADING 4390.286 p/m 2.4331
4849
{'symbol': 'Crypto.SOL/USD', 'asset_type': 'Crypto', 'quote_currency': 'USD', 'description': 'SOL/USD', 'generic_symbol': 'SOLUSD', 'base': 'SOL'}
49-
PythPriceType.PRICE 192.27550000000002 p/m 0.0485
50+
PythPriceType.PRICE PythPriceStatus.TRADING 192.27550000000002 p/m 0.0485
5051
{'symbol': 'Crypto.SRM/USD', 'asset_type': 'Crypto', 'quote_currency': 'USD', 'description': 'SRM/USD', 'generic_symbol': 'SRMUSD', 'base': 'SRM'}
51-
PythPriceType.PRICE 4.23125 p/m 0.0019500000000000001
52+
PythPriceType.PRICE PythPriceStatus.UNKNOWN None p/m None
5253
...
5354
```
5455

examples/dump.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ async def main():
5454
pr.key,
5555
pr.product_account_key,
5656
pr.price_type,
57+
pr.aggregate_price_status,
5758
pr.aggregate_price,
5859
"p/m",
5960
pr.aggregate_price_confidence_interval,
@@ -86,11 +87,12 @@ async def main():
8687
print(
8788
pr.product.symbol,
8889
pr.price_type,
90+
pr.aggregate_price_status,
8991
pr.aggregate_price,
9092
"p/m",
9193
pr.aggregate_price_confidence_interval,
9294
)
93-
break
95+
break
9496

9597
print("Unsubscribing...")
9698
if use_program:

examples/read_one_price_feed.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import asyncio
44

5-
from pythclient.pythaccounts import PythPriceAccount
5+
from pythclient.pythaccounts import PythPriceAccount, PythPriceStatus
66
from pythclient.solana import SolanaClient, SolanaPublicKey, SOLANA_DEVNET_HTTP_ENDPOINT, SOLANA_DEVNET_WS_ENDPOINT
77

88
async def get_price():
@@ -12,7 +12,14 @@ async def get_price():
1212
price: PythPriceAccount = PythPriceAccount(account_key, solana_client)
1313

1414
await price.update()
15-
# Sample output: "DOGE/USD is 0.141455 ± 7.4e-05"
16-
print("DOGE/USD is", price.aggregate_price, "±", price.aggregate_price_confidence_interval)
15+
16+
price_status = price.aggregate_price_status
17+
if price_status == PythPriceStatus.TRADING:
18+
# Sample output: "DOGE/USD is 0.141455 ± 7.4e-05"
19+
print("DOGE/USD is", price.aggregate_price, "±", price.aggregate_price_confidence_interval)
20+
else:
21+
print("Price is not valid now. Status is", price_status)
22+
23+
await solana_client.close()
1724

1825
asyncio.run(get_price())

pythclient/pythaccounts.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
_SUPPORTED_VERSIONS = set((_VERSION_1, _VERSION_2))
1818
_ACCOUNT_HEADER_BYTES = 16 # magic + version + type + size, u32 * 4
1919
_NULL_KEY_BYTES = b'\x00' * SolanaPublicKey.LENGTH
20+
MAX_SLOT_DIFFERENCE = 25
2021

2122

2223
class PythAccountType(Enum):
@@ -377,7 +378,7 @@ class PythPriceInfo:
377378
price (int): the price
378379
confidence_interval (int): the price confidence interval
379380
price_status (PythPriceStatus): the price status
380-
slot (int): the slot time this price information was published
381+
pub_slot (int): the slot time this price information was published
381382
exponent (int): the power-of-10 order of the price
382383
"""
383384

@@ -386,7 +387,7 @@ class PythPriceInfo:
386387
raw_price: int
387388
raw_confidence_interval: int
388389
price_status: PythPriceStatus
389-
slot: int
390+
pub_slot: int
390391
exponent: int
391392

392393
price: float = field(init=False)
@@ -410,9 +411,9 @@ def deserialise(buffer: bytes, offset: int = 0, *, exponent: int) -> PythPriceIn
410411
slot (u64)
411412
"""
412413
# _ is corporate_action
413-
price, confidence_interval, price_status, _, slot = struct.unpack_from(
414+
price, confidence_interval, price_status, _, pub_slot = struct.unpack_from(
414415
"<qQIIQ", buffer, offset)
415-
return PythPriceInfo(price, confidence_interval, PythPriceStatus(price_status), slot, exponent)
416+
return PythPriceInfo(price, confidence_interval, PythPriceStatus(price_status), pub_slot, exponent)
416417

417418
def __str__(self) -> str:
418419
return f"PythPriceInfo status {self.price_status} price {self.price}"
@@ -507,7 +508,7 @@ class PythPriceAccount(PythAccount):
507508
aggregate_price_info (PythPriceInfo): the aggregate price information
508509
price_components (List[PythPriceComponent]): the price components that the
509510
aggregate price is composed of
510-
slot (int): the slot time when this account was last updated
511+
slot (int): the slot time when this account was last fetched
511512
product (Optional[PythProductAccount]): the product this price is for, if loaded
512513
"""
513514

@@ -528,13 +529,41 @@ def __init__(self, key: SolanaPublicKey, solana: SolanaClient, *, product: Optio
528529

529530
@property
530531
def aggregate_price(self) -> Optional[float]:
531-
"""the aggregate price"""
532-
return self.aggregate_price_info and self.aggregate_price_info.price
532+
"""
533+
The aggregate price. Returns None if price is not currently available.
534+
If you need the price value regardless of availability use `aggregate_price_info.price`
535+
"""
536+
if self.aggregate_price_status == PythPriceStatus.TRADING:
537+
return self.aggregate_price_info.price
538+
else:
539+
return None
533540

534541
@property
535542
def aggregate_price_confidence_interval(self) -> Optional[float]:
536-
"""the aggregate price confidence interval"""
537-
return self.aggregate_price_info and self.aggregate_price_info.confidence_interval
543+
"""
544+
The aggregate price confidence interval. Returns None if price is not currently available.
545+
If you need the confidence value regardless of availability use `aggregate_price_info.confidence_interval`
546+
"""
547+
if self.aggregate_price_status == PythPriceStatus.TRADING:
548+
return self.aggregate_price_info.confidence_interval
549+
else:
550+
return None
551+
552+
@property
553+
def aggregate_price_status(self) -> Optional[PythPriceStatus]:
554+
"""The aggregate price status."""
555+
return self.get_aggregate_price_status_with_slot(self.slot)
556+
557+
def get_aggregate_price_status_with_slot(self, slot: int) -> Optional[PythPriceStatus]:
558+
"""
559+
Gets the aggregate price status given a solana slot.
560+
You might consider using this function with the latest solana slot to make sure the price has not gone stale.
561+
"""
562+
if self.aggregate_price_info.price_status == PythPriceStatus.TRADING and \
563+
slot - self.aggregate_price_info.pub_slot > MAX_SLOT_DIFFERENCE:
564+
return PythPriceStatus.UNKNOWN
565+
566+
return self.aggregate_price_info.price_status
538567

539568
def update_from(self, buffer: bytes, *, version: int, offset: int = 0) -> None:
540569
"""

pythclient/solana.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -286,10 +286,10 @@ async def get_health(self) -> Union[Literal['ok'], Dict[str, Any]]:
286286
async def get_cluster_nodes(self) -> List[Dict[str, Any]]:
287287
return await self.http_send("getClusterNodes")
288288

289-
async def get_commitment_slot(
289+
async def get_slot(
290290
self,
291-
commitment: str
292-
) -> Dict[str, Any]:
291+
commitment: str = SolanaCommitment.CONFIRMED,
292+
) -> Union[int, Dict[str, Any]]:
293293
return await self.http_send(
294294
"getSlot",
295295
[{"commitment": commitment}]

tests/test_price_account.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from dataclasses import asdict
44

55
from pythclient.pythaccounts import (
6+
MAX_SLOT_DIFFERENCE,
67
PythPriceAccount,
78
PythPriceType,
89
PythPriceStatus,
@@ -55,15 +56,13 @@ def price_account_bytes():
5556
b'AAABAAAAAAAAANipUgYAAAAAn4M0eBAAAABh7UgCAAAAAAEAAAAAAAAA2alSBgAAAAA='
5657
))
5758

58-
5959
@pytest.fixture
6060
def price_account(solana_client: SolanaClient) -> PythPriceAccount:
6161
return PythPriceAccount(
6262
key=SolanaPublicKey("5ALDzwcRJfSyGdGyhP3kP628aqBNHZzLuVww7o9kdspe"),
6363
solana=solana_client,
6464
)
6565

66-
6766
def test_price_account_update_from(price_account_bytes: bytes, price_account: PythPriceAccount):
6867
price_account.update_from(buffer=price_account_bytes, version=2, offset=0)
6968

@@ -81,7 +80,7 @@ def test_price_account_update_from(price_account_bytes: bytes, price_account: Py
8180
"raw_price": 70712500000,
8281
"raw_confidence_interval": 36630500,
8382
"price_status": PythPriceStatus.TRADING,
84-
"slot": 106080731,
83+
"pub_slot": 106080731,
8584
"exponent": -8,
8685
"price": 707.125,
8786
"confidence_interval": 0.366305,
@@ -95,7 +94,7 @@ def test_price_account_update_from(price_account_bytes: bytes, price_account: Py
9594
"raw_price": 70709500000,
9695
"raw_confidence_interval": 21500000,
9796
"price_status": PythPriceStatus.TRADING,
98-
"slot": 106080728,
97+
"pub_slot": 106080728,
9998
"exponent": -8,
10099
"price": 707.095,
101100
"confidence_interval": 0.215,
@@ -104,7 +103,7 @@ def test_price_account_update_from(price_account_bytes: bytes, price_account: Py
104103
"raw_price": 70709500000,
105104
"raw_confidence_interval": 21500000,
106105
"price_status": PythPriceStatus.TRADING,
107-
"slot": 106080729,
106+
"pub_slot": 106080729,
108107
"exponent": -8,
109108
"price": 707.095,
110109
"confidence_interval": 0.215,
@@ -141,11 +140,41 @@ def test_price_account_agregate_conf_interval(
141140
price_account_bytes: bytes, price_account: PythPriceAccount,
142141
):
143142
price_account.update_from(buffer=price_account_bytes, version=2, offset=0)
143+
price_account.slot = price_account.aggregate_price_info.pub_slot
144144
assert price_account.aggregate_price_confidence_interval == 0.366305
145145

146146

147147
def test_price_account_agregate_price(
148148
price_account_bytes: bytes, price_account: PythPriceAccount,
149149
):
150150
price_account.update_from(buffer=price_account_bytes, version=2, offset=0)
151+
price_account.slot = price_account.aggregate_price_info.pub_slot
151152
assert price_account.aggregate_price == 707.125
153+
154+
def test_price_account_unknown_status(
155+
price_account_bytes: bytes, price_account: PythPriceAccount,
156+
):
157+
price_account.update_from(buffer=price_account_bytes, version=2, offset=0)
158+
price_account.slot = price_account.aggregate_price_info.pub_slot
159+
price_account.aggregate_price_info.price_status = PythPriceStatus.UNKNOWN
160+
161+
assert price_account.aggregate_price is None
162+
assert price_account.aggregate_price_confidence_interval is None
163+
164+
def test_price_account_get_aggregate_price_status_still_trading(
165+
price_account_bytes: bytes, price_account: PythPriceAccount
166+
):
167+
price_account.update_from(buffer=price_account_bytes, version=2, offset=0)
168+
price_account.slot = price_account.aggregate_price_info.pub_slot + MAX_SLOT_DIFFERENCE
169+
170+
price_status = price_account.aggregate_price_status
171+
assert price_status == PythPriceStatus.TRADING
172+
173+
def test_price_account_get_aggregate_price_status_got_stale(
174+
price_account_bytes: bytes, price_account: PythPriceAccount
175+
):
176+
price_account.update_from(buffer=price_account_bytes, version=2, offset=0)
177+
price_account.slot = price_account.aggregate_price_info.pub_slot + MAX_SLOT_DIFFERENCE + 1
178+
179+
price_status = price_account.aggregate_price_status
180+
assert price_status == PythPriceStatus.UNKNOWN

tests/test_price_component.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ def price_component() -> PythPriceComponent:
2121
'raw_price': 62931500000,
2222
'raw_confidence_interval': 16500000,
2323
'price_status': PythPriceStatus.TRADING,
24-
'slot': 105886163,
24+
'pub_slot': 105886163,
2525
'exponent': exponent,
2626
})
2727
latest_price = PythPriceInfo(**{
2828
'raw_price': 62931500000,
2929
'raw_confidence_interval': 16500000,
3030
'price_status': PythPriceStatus.TRADING,
31-
'slot': 105886164,
31+
'pub_slot': 105886164,
3232
'exponent': exponent,
3333
})
3434
return PythPriceComponent(

tests/test_price_info.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def price_info_trading():
1010
raw_price=59609162000,
1111
raw_confidence_interval=43078500,
1212
price_status=PythPriceStatus.TRADING,
13-
slot=105367617,
13+
pub_slot=105367617,
1414
exponent=-8,
1515
)
1616

@@ -21,7 +21,7 @@ def price_info_trading_bytes():
2121

2222

2323
@pytest.mark.parametrize(
24-
"raw_price,raw_confidence_interval,price_status,slot,exponent,price,confidence_interval",
24+
"raw_price,raw_confidence_interval,price_status,pub_slot,exponent,price,confidence_interval",
2525
[
2626
(
2727
1234567890,
@@ -42,7 +42,7 @@ def test_price_info(
4242
raw_price,
4343
raw_confidence_interval,
4444
price_status,
45-
slot,
45+
pub_slot,
4646
exponent,
4747
price,
4848
confidence_interval,
@@ -51,7 +51,7 @@ def test_price_info(
5151
raw_price=raw_price,
5252
raw_confidence_interval=raw_confidence_interval,
5353
price_status=price_status,
54-
slot=slot,
54+
pub_slot=pub_slot,
5555
exponent=exponent,
5656
)
5757
for key, actual_value in asdict(actual).items():
@@ -62,7 +62,7 @@ def test_price_info_iter(
6262
raw_price,
6363
raw_confidence_interval,
6464
price_status,
65-
slot,
65+
pub_slot,
6666
exponent,
6767
price,
6868
confidence_interval,
@@ -72,15 +72,15 @@ def test_price_info_iter(
7272
raw_price=raw_price,
7373
raw_confidence_interval=raw_confidence_interval,
7474
price_status=price_status,
75-
slot=slot,
75+
pub_slot=pub_slot,
7676
exponent=exponent,
7777
)
7878
)
7979
expected = {
8080
"raw_price": raw_price,
8181
"raw_confidence_interval": raw_confidence_interval,
8282
"price_status": price_status,
83-
"slot": slot,
83+
"pub_slot": pub_slot,
8484
"exponent": exponent,
8585
"price": price,
8686
"confidence_interval": confidence_interval,

0 commit comments

Comments
 (0)