-
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 #726 from tellor-io/conditional-report
Telliot Conditional Reporting (tellor is stale / max price change)
- Loading branch information
Showing
6 changed files
with
394 additions
and
3 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
from typing import Optional | ||
from typing import TypeVar | ||
|
||
import click | ||
from click.core import Context | ||
from telliot_core.cli.utils import async_run | ||
|
||
from telliot_feeds.cli.utils import common_options | ||
from telliot_feeds.cli.utils import common_reporter_options | ||
from telliot_feeds.cli.utils import get_accounts_from_name | ||
from telliot_feeds.cli.utils import reporter_cli_core | ||
from telliot_feeds.feeds import CATALOG_FEEDS | ||
from telliot_feeds.reporters.customized.conditional_reporter import ConditionalReporter | ||
from telliot_feeds.utils.cfg import check_endpoint | ||
from telliot_feeds.utils.cfg import setup_config | ||
from telliot_feeds.utils.log import get_logger | ||
|
||
logger = get_logger(__name__) | ||
T = TypeVar("T") | ||
|
||
|
||
@click.group() | ||
def conditional_reporter() -> None: | ||
"""Report data on-chain.""" | ||
pass | ||
|
||
|
||
@conditional_reporter.command() | ||
@common_options | ||
@common_reporter_options | ||
@click.option( | ||
"-pc", | ||
"--percent-change", | ||
help="Price change percentage for triggering a report. Default=0.01 (1%)", | ||
type=float, | ||
default=0.01, | ||
) | ||
@click.option( | ||
"-st", | ||
"--stale-timeout", | ||
help="Triggers a report when the oracle value is stale. Default=85500 (23.75 hours)", | ||
type=int, | ||
default=85500, | ||
) | ||
@click.pass_context | ||
@async_run | ||
async def conditional( | ||
ctx: Context, | ||
tx_type: int, | ||
gas_limit: int, | ||
max_fee_per_gas: Optional[float], | ||
priority_fee_per_gas: Optional[float], | ||
base_fee_per_gas: Optional[float], | ||
legacy_gas_price: Optional[int], | ||
expected_profit: str, | ||
submit_once: bool, | ||
wait_period: int, | ||
password: str, | ||
min_native_token_balance: float, | ||
stake: float, | ||
account_str: str, | ||
check_rewards: bool, | ||
gas_multiplier: int, | ||
max_priority_fee_range: int, | ||
percent_change: float, | ||
stale_timeout: int, | ||
query_tag: str, | ||
unsafe: bool, | ||
) -> None: | ||
"""Report values to Tellor oracle if certain conditions are met.""" | ||
click.echo("Starting Conditional Reporter...") | ||
ctx.obj["ACCOUNT_NAME"] = account_str | ||
ctx.obj["SIGNATURE_ACCOUNT_NAME"] = None | ||
if query_tag is None: | ||
raise click.UsageError("--query-tag (-qt) is required. Use --help for a list of feeds with API support.") | ||
datafeed = CATALOG_FEEDS.get(query_tag) | ||
if datafeed is None: | ||
raise click.UsageError(f"Invalid query tag: {query_tag}, enter a valid query tag with API support, use --help") | ||
|
||
accounts = get_accounts_from_name(account_str) | ||
if not accounts: | ||
return | ||
chain_id = accounts[0].chains[0] | ||
ctx.obj["CHAIN_ID"] = chain_id # used in reporter_cli_core | ||
# Initialize telliot core app using CLI context | ||
async with reporter_cli_core(ctx) as core: | ||
|
||
core._config, account = setup_config(core.config, account_name=account_str, unsafe=unsafe) | ||
|
||
endpoint = check_endpoint(core._config) | ||
|
||
if not endpoint or not account: | ||
click.echo("Accounts and/or endpoint unset.") | ||
click.echo(f"Account: {account}") | ||
click.echo(f"Endpoint: {core._config.get_endpoint()}") | ||
return | ||
|
||
# Make sure current account is unlocked | ||
if not account.is_unlocked: | ||
account.unlock(password) | ||
|
||
click.echo("Reporter settings:") | ||
click.echo(f"Max tolerated price change: {percent_change * 100}%") | ||
click.echo(f"Value considered stale after: {stale_timeout} seconds") | ||
click.echo(f"Transaction type: {tx_type}") | ||
click.echo(f"Transaction type: {tx_type}") | ||
click.echo(f"Gas Limit: {gas_limit}") | ||
click.echo(f"Legacy gas price (gwei): {legacy_gas_price}") | ||
click.echo(f"Max fee (gwei): {max_fee_per_gas}") | ||
click.echo(f"Priority fee (gwei): {priority_fee_per_gas}") | ||
click.echo(f"Desired stake amount: {stake}") | ||
click.echo(f"Minimum native token balance (e.g. ETH if on Ethereum mainnet): {min_native_token_balance}") | ||
click.echo("\n") | ||
|
||
_ = input("Press [ENTER] to confirm settings.") | ||
|
||
contracts = core.get_tellor360_contracts() | ||
|
||
common_reporter_kwargs = { | ||
"endpoint": core.endpoint, | ||
"account": account, | ||
"datafeed": datafeed, | ||
"gas_limit": gas_limit, | ||
"max_fee_per_gas": max_fee_per_gas, | ||
"priority_fee_per_gas": priority_fee_per_gas, | ||
"base_fee_per_gas": base_fee_per_gas, | ||
"legacy_gas_price": legacy_gas_price, | ||
"chain_id": core.config.main.chain_id, | ||
"wait_period": wait_period, | ||
"oracle": contracts.oracle, | ||
"autopay": contracts.autopay, | ||
"token": contracts.token, | ||
"expected_profit": expected_profit, | ||
"stake": stake, | ||
"transaction_type": tx_type, | ||
"min_native_token_balance": int(min_native_token_balance * 10**18), | ||
"check_rewards": check_rewards, | ||
"gas_multiplier": gas_multiplier, | ||
"max_priority_fee_range": max_priority_fee_range, | ||
} | ||
|
||
reporter = ConditionalReporter( | ||
stale_timeout=stale_timeout, | ||
max_price_change=percent_change, | ||
**common_reporter_kwargs, | ||
) | ||
|
||
if submit_once: | ||
reporter.wait_period = 0 | ||
await reporter.report(report_count=1) | ||
else: | ||
await reporter.report() |
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
145 changes: 145 additions & 0 deletions
145
src/telliot_feeds/reporters/customized/conditional_reporter.py
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,145 @@ | ||
import asyncio | ||
from dataclasses import dataclass | ||
from typing import Any | ||
from typing import Optional | ||
from typing import TypeVar | ||
|
||
from web3 import Web3 | ||
|
||
from telliot_feeds.feeds import DataFeed | ||
from telliot_feeds.reporters.tellor_360 import Tellor360Reporter | ||
from telliot_feeds.utils.log import get_logger | ||
from telliot_feeds.utils.reporter_utils import current_time | ||
|
||
logger = get_logger(__name__) | ||
T = TypeVar("T") | ||
|
||
|
||
@dataclass | ||
class GetDataBefore: | ||
retrieved: bool | ||
value: bytes | ||
timestampRetrieved: int | ||
|
||
|
||
@dataclass | ||
class ConditionalReporter(Tellor360Reporter): | ||
"""Backup Reporter that inherits from Tellor360Reporter and | ||
implements conditions when intended as backup to chainlink""" | ||
|
||
def __init__( | ||
self, | ||
stale_timeout: int, | ||
max_price_change: float, | ||
datafeed: Optional[DataFeed[Any]] = None, | ||
*args: Any, | ||
**kwargs: Any, | ||
) -> None: | ||
super().__init__(*args, **kwargs) | ||
self.stale_timeout = stale_timeout | ||
self.max_price_change = max_price_change | ||
self.datafeed = datafeed | ||
|
||
async def get_tellor_latest_data(self) -> Optional[GetDataBefore]: | ||
"""Get latest data from tellor oracle (getDataBefore with current time) | ||
Returns: | ||
- Optional[GetDataBefore]: latest data from tellor oracle | ||
""" | ||
if self.datafeed is None: | ||
logger.debug(f"no datafeed set: {self.datafeed}") | ||
return None | ||
data, status = await self.oracle.read("getDataBefore", self.datafeed.query.query_id, current_time()) | ||
if not status.ok: | ||
logger.warning(f"error getting tellor data: {status.e}") | ||
return None | ||
return GetDataBefore(*data) | ||
|
||
async def get_telliot_feed_data(self, datafeed: DataFeed[Any]) -> Optional[float]: | ||
"""Fetch spot price data from API sources and calculate a value | ||
Returns: | ||
- Optional[GetDataBefore]: latest data from tellor oracle | ||
""" | ||
v, _ = await datafeed.source.fetch_new_datapoint() | ||
logger.info(f"telliot feeds value: {v}") | ||
return v | ||
|
||
def tellor_price_change_above_max( | ||
self, tellor_latest_data: GetDataBefore, telliot_feed_data: Optional[float] | ||
) -> bool: | ||
"""Check if spot price change since last report is above max price deviation | ||
params: | ||
- tellor_latest_data: latest data from tellor oracle | ||
- telliot_feed_data: latest data from API sources | ||
Returns: | ||
- bool: True if price change is above max price deviation, False otherwise | ||
""" | ||
oracle_price = (Web3.toInt(tellor_latest_data.value)) / 10**18 | ||
feed_price = telliot_feed_data if telliot_feed_data else None | ||
|
||
if feed_price is None: | ||
logger.warning("No feed data available") | ||
return False | ||
|
||
min_price = min(oracle_price, feed_price) | ||
max_price = max(oracle_price, feed_price) | ||
logger.info(f"Latest Tellor price = {oracle_price}") | ||
percent_change = (max_price - min_price) / max_price | ||
logger.info(f"feed price change = {percent_change}") | ||
if percent_change > self.max_price_change: | ||
logger.info("Feed price change above max") | ||
return True | ||
else: | ||
return False | ||
|
||
async def conditions_met(self) -> bool: | ||
"""Trigger methods to check conditions if reporting spot is necessary | ||
Returns: | ||
- bool: True if conditions are met, False otherwise | ||
""" | ||
logger.info("checking conditions and reporting if necessary") | ||
if self.datafeed is None: | ||
logger.info(f"no datafeed was setß: {self.datafeed}. Please provide a spot-price query type (see --help)") | ||
return False | ||
tellor_latest_data = await self.get_tellor_latest_data() | ||
telliot_feed_data = await self.get_telliot_feed_data(datafeed=self.datafeed) | ||
time = current_time() | ||
time_passed_since_tellor_report = time - tellor_latest_data.timestampRetrieved if tellor_latest_data else time | ||
if tellor_latest_data is None: | ||
logger.debug("tellor data returned None") | ||
return True | ||
elif not tellor_latest_data.retrieved: | ||
logger.debug(f"No oracle submissions in tellor for query: {self.datafeed.query.descriptor}") | ||
return True | ||
elif time_passed_since_tellor_report > self.stale_timeout: | ||
logger.debug(f"tellor data is stale, time elapsed since last report: {time_passed_since_tellor_report}") | ||
return True | ||
elif self.tellor_price_change_above_max(tellor_latest_data, telliot_feed_data): | ||
logger.debug("tellor price change above max") | ||
return True | ||
else: | ||
logger.debug(f"tellor {self.datafeed.query.descriptor} data is recent enough") | ||
return False | ||
|
||
async def report(self, report_count: Optional[int] = None) -> None: | ||
"""Submit values to Tellor oracles on an interval.""" | ||
|
||
while report_count is None or report_count > 0: | ||
online = await self.is_online() | ||
if online: | ||
if self.has_native_token(): | ||
if await self.conditions_met(): | ||
_, _ = await self.report_once() | ||
else: | ||
logger.info("feeds are recent enough, no need to report") | ||
|
||
else: | ||
logger.warning("Unable to connect to the internet!") | ||
|
||
logger.info(f"Sleeping for {self.wait_period} seconds") | ||
await asyncio.sleep(self.wait_period) | ||
|
||
if report_count is not None: | ||
report_count -= 1 |
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
Oops, something went wrong.