-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Telliot Conditional Reporting (tellor is stale / max price change) #726
Changes from all commits
bfbe8f1
f691b11
8697f47
49a3ec8
e5c63e7
3a130fa
258d984
47400e9
658be3a
e7f2c7f
e501e3a
0e787e7
0bc557e
b08ff24
397d062
7c772b3
d4163d4
ba6cf78
67b9b6d
679e7d8
d9822c6
92d49ea
3f6ee7d
f2aed06
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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}") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. duplicate |
||
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() |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should current data, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Was my first try, but |
||
""" | ||
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
default 50% comment