-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #760 from tellor-io/improve_sources
USDM, wUSDM, sDAI feeds and new CurveFi price source
- Loading branch information
Showing
14 changed files
with
335 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
from telliot_feeds.datafeed import DataFeed | ||
from telliot_feeds.queries.price.spot_price import SpotPrice | ||
from telliot_feeds.sources.price.spot.coingecko import CoinGeckoSpotPriceSource | ||
from telliot_feeds.sources.price.spot.coinpaprika import CoinpaprikaSpotPriceSource | ||
from telliot_feeds.sources.price.spot.curvefiprice import CurveFiUSDPriceSource | ||
from telliot_feeds.sources.price_aggregator import PriceAggregator | ||
|
||
sdai_usd_median_feed = DataFeed( | ||
query=SpotPrice(asset="SDAI", currency="USD"), | ||
source=PriceAggregator( | ||
asset="sdai", | ||
currency="usd", | ||
algorithm="median", | ||
sources=[ | ||
CoinGeckoSpotPriceSource(asset="sdai", currency="usd"), | ||
CoinpaprikaSpotPriceSource(asset="sdai-savings-dai", currency="usd"), | ||
CurveFiUSDPriceSource(asset="sdai", currency="usd"), | ||
], | ||
), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
from telliot_feeds.datafeed import DataFeed | ||
from telliot_feeds.queries.price.spot_price import SpotPrice | ||
from telliot_feeds.sources.price.spot.coingecko import CoinGeckoSpotPriceSource | ||
from telliot_feeds.sources.price.spot.coinpaprika import CoinpaprikaSpotPriceSource | ||
from telliot_feeds.sources.price.spot.curvefiprice import CurveFiUSDPriceSource | ||
from telliot_feeds.sources.price.spot.uniswapV3Pool import UniswapV3PoolPriceSource | ||
from telliot_feeds.sources.price_aggregator import PriceAggregator | ||
|
||
|
||
usdm_usd_median_feed = DataFeed( | ||
query=SpotPrice(asset="USDM", currency="USD"), | ||
source=PriceAggregator( | ||
asset="usdm", | ||
currency="usd", | ||
algorithm="median", | ||
sources=[ | ||
CoinGeckoSpotPriceSource(asset="usdm", currency="usd"), | ||
CurveFiUSDPriceSource(asset="usdm", currency="usd"), | ||
CoinpaprikaSpotPriceSource(asset="usdm-mountain-protocol-usd", currency="usd"), | ||
UniswapV3PoolPriceSource(asset="usdm", currency="usd"), | ||
], | ||
), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from telliot_feeds.datafeed import DataFeed | ||
from telliot_feeds.queries.price.spot_price import SpotPrice | ||
from telliot_feeds.sources.wusdm_source import wUSDMSpotPriceSource | ||
|
||
|
||
wusdm_usd_feed = DataFeed( | ||
query=SpotPrice(asset="WUSDM", currency="USD"), source=wUSDMSpotPriceSource(asset="wusdm", currency="usd") | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -80,6 +80,9 @@ | |
"USDY/USD", | ||
"WMNT/USD", | ||
"PRIMEETH/ETH", | ||
"WUSDM/USD", | ||
"SDAI/USD", | ||
"USDM/USD", | ||
] | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
from dataclasses import dataclass | ||
from dataclasses import field | ||
from typing import Any | ||
|
||
from telliot_feeds.dtypes.datapoint import datetime_now_utc | ||
from telliot_feeds.dtypes.datapoint import OptionalDataPoint | ||
from telliot_feeds.pricing.price_service import WebPriceService | ||
from telliot_feeds.pricing.price_source import PriceSource | ||
from telliot_feeds.utils.log import get_logger | ||
|
||
|
||
logger = get_logger(__name__) | ||
|
||
|
||
ethereum_contract_map = { | ||
"usdm": "0x59D9356E565Ab3A36dD77763Fc0d87fEaf85508C", | ||
"sdai": "0x83F20F44975D03b1b09e64809B757c47f942BEeA", | ||
"eth": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", | ||
"steth": "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", | ||
"btc": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", | ||
} | ||
|
||
|
||
class CurveFiUSDPriceService(WebPriceService): | ||
"""CurveFinance Curve-Prices Service""" | ||
|
||
def __init__(self, **kwargs: Any) -> None: | ||
kwargs["name"] = "Curve-Prices Service" | ||
kwargs["url"] = "https://prices.curve.fi/v1/usd_price" | ||
super().__init__(**kwargs) | ||
|
||
async def get_price(self, asset: str, currency: str) -> OptionalDataPoint[float]: | ||
"""This implementation gets the price from the Curve finance API.""" | ||
asset = asset.lower() | ||
currency = currency.lower() | ||
if asset not in ethereum_contract_map: | ||
logger.error(f"Asset not mapped for curve price Source: {asset}") | ||
return None, None | ||
asset_address = ethereum_contract_map[asset] | ||
|
||
if currency != "usd": | ||
logger.error("Service for usd pairs only") | ||
return None, None | ||
|
||
request_url = f"/ethereum/{asset_address}" | ||
|
||
d = self.get_url(request_url) | ||
|
||
if "error" in d: | ||
logger.error(d) | ||
return None, None | ||
|
||
response = d.get("response") | ||
if response is None: | ||
logger.error("Error parsing Curve Finance API response, 'response' key not found") | ||
return None, None | ||
|
||
data = response.get("data") | ||
if data is None: | ||
logger.error("Error parsing Curve Finance API response 'data' key not found") | ||
return None, None | ||
|
||
asset_price = data.get("usd_price") | ||
if asset_price is None: | ||
logger.error("Failed to parse response data from Curve Finance API 'usd_price' key not found") | ||
return None, None | ||
|
||
isAddress = data.get("address", "").lower() == asset_address.lower() | ||
|
||
if not isAddress: | ||
logger.error("Asset '0x' address in API response doesn't match contract address stored") | ||
return None, None | ||
return asset_price, datetime_now_utc() | ||
|
||
|
||
@dataclass | ||
class CurveFiUSDPriceSource(PriceSource): | ||
asset: str = "" | ||
currency: str = "" | ||
service: CurveFiUSDPriceService = field(default_factory=CurveFiUSDPriceService, init=False) | ||
|
||
|
||
if __name__ == "__main__": | ||
import asyncio | ||
|
||
async def main() -> None: | ||
source = CurveFiUSDPriceSource(asset="usdm", currency="usd") | ||
v, _ = await source.fetch_new_datapoint() | ||
print(v) | ||
|
||
asyncio.run(main()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
from dataclasses import dataclass | ||
from dataclasses import field | ||
from typing import Any | ||
from typing import Optional | ||
|
||
from telliot_core.apps.telliot_config import TelliotConfig | ||
|
||
from telliot_feeds.dtypes.datapoint import OptionalDataPoint | ||
from telliot_feeds.pricing.price_service import WebPriceService | ||
from telliot_feeds.pricing.price_source import PriceSource | ||
from telliot_feeds.sources.price.spot.coingecko import CoinGeckoSpotPriceSource | ||
from telliot_feeds.sources.price.spot.coinpaprika import CoinpaprikaSpotPriceSource | ||
from telliot_feeds.sources.price.spot.curvefiprice import CurveFiUSDPriceSource | ||
from telliot_feeds.sources.price.spot.uniswapV3Pool import UniswapV3PoolPriceSource | ||
from telliot_feeds.sources.price_aggregator import PriceAggregator | ||
from telliot_feeds.utils.log import get_logger | ||
|
||
logger = get_logger(__name__) | ||
|
||
|
||
class wUSDMSpotPriceService(WebPriceService): | ||
"""Custom wUSDM Price Service""" | ||
|
||
def __init__(self, **kwargs: Any) -> None: | ||
kwargs["name"] = "Custom wUSDM Price Service" | ||
kwargs["url"] = "" | ||
super().__init__(**kwargs) | ||
self.cfg = TelliotConfig() | ||
|
||
def get_wusdm_usd_ratio(self) -> Optional[float]: | ||
# get endpoint | ||
endpoint = self.cfg.endpoints.find(chain_id=1) | ||
if not endpoint: | ||
logger.error("Endpoint not found for mainnet to get wusdm_eth_ratio") | ||
return None | ||
ep = endpoint[0] | ||
if not ep.connect(): | ||
logger.error("Unable to connect endpoint for mainnet to get wusdm_eth_ratio") | ||
return None | ||
w3 = ep.web3 | ||
# get ratio | ||
wusdm_eth_ratio_bytes = w3.eth.call( | ||
{ | ||
"to": "0x57F5E098CaD7A3D1Eed53991D4d66C45C9AF7812", | ||
"data": "0x07a2d13a0000000000000000000000000000000000000000000000000de0b6b3a7640000", | ||
} | ||
) | ||
wusdm_usd_ratio_decoded = w3.toInt(wusdm_eth_ratio_bytes) | ||
wusdm_usd_ratio = w3.fromWei(wusdm_usd_ratio_decoded, "ether") | ||
print(f"Ratio from wUSDM contract: {wusdm_usd_ratio}") | ||
return float(wusdm_usd_ratio) | ||
|
||
async def get_price(self, asset: str, currency: str) -> OptionalDataPoint[float]: | ||
"""This implementation gets the price of USDM from multiple sources and | ||
calculates the price of wUSDM using the ratio of wUSDM to USDMfrom wUSDM contract. | ||
""" | ||
asset = asset.lower() | ||
currency = currency.lower() | ||
|
||
wusdm_ratio = self.get_wusdm_usd_ratio() | ||
if wusdm_ratio is None: | ||
logger.error("Unable to get wUSDM_USDM_ratio") | ||
return None, None | ||
|
||
source = PriceAggregator( | ||
algorithm="median", | ||
sources=[ | ||
CoinGeckoSpotPriceSource(asset="usdm", currency="usd"), | ||
CurveFiUSDPriceSource(asset="usdm", currency="usd"), | ||
CoinpaprikaSpotPriceSource(asset="usdm-mountain-protocol-usd", currency=currency), | ||
UniswapV3PoolPriceSource(asset="usdm", currency=currency), | ||
], | ||
) | ||
|
||
wusdm_price, timestamp = await source.fetch_new_datapoint() | ||
if wusdm_price is None: | ||
logger.error("Unable to get wusdm price") | ||
return None, None | ||
return wusdm_price * wusdm_ratio, timestamp | ||
|
||
|
||
@dataclass | ||
class wUSDMSpotPriceSource(PriceSource): | ||
asset: str = "" | ||
currency: str = "" | ||
service: wUSDMSpotPriceService = field(default_factory=wUSDMSpotPriceService, init=False) | ||
|
||
|
||
if __name__ == "__main__": | ||
import asyncio | ||
|
||
async def main() -> None: | ||
source = wUSDMSpotPriceSource(asset="wusdm", currency="usd") | ||
v, _ = await source.fetch_new_datapoint() | ||
print(v) | ||
|
||
asyncio.run(main()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import statistics | ||
|
||
import pytest | ||
|
||
from telliot_feeds.feeds.sdai_usd_feed import sdai_usd_median_feed | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_sdai_usd_median_feed(caplog): | ||
"""Retrieve median SDAI/USD price.""" | ||
v, _ = await sdai_usd_median_feed.source.fetch_new_datapoint() | ||
|
||
assert v is not None | ||
assert v > 0 | ||
assert "sources used in aggregate: 3" in caplog.text.lower() | ||
print(f"sDAI/USD Price: {v}") | ||
|
||
# Get list of data sources from sources dict | ||
source_prices = [source.latest[0] for source in sdai_usd_median_feed.source.sources if source.latest[0]] | ||
|
||
# Make sure error is less than decimal tolerance | ||
assert (v - statistics.median(source_prices)) < 10**-6 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import statistics | ||
|
||
import pytest | ||
|
||
from telliot_feeds.feeds.usdm_usd_feed import usdm_usd_median_feed | ||
|
||
|
||
@pytest.mark.asyncio | ||
async def test_usdm_usd_median_feed(caplog): | ||
"""Retrieve median USDM/USD price.""" | ||
v, _ = await usdm_usd_median_feed.source.fetch_new_datapoint() | ||
|
||
assert v is not None | ||
assert v > 0 | ||
assert "sources used in aggregate: 4" in caplog.text.lower() | ||
print(f"USDM/ETH Price: {v}") | ||
# Get list of data sources from sources dict | ||
source_prices = usdm_usd_median_feed.source.latest[0] | ||
|
||
# Make sure error is less than decimal tolerance | ||
assert (v - statistics.median([source_prices])) < 10**-6 |
Oops, something went wrong.