diff --git a/packages/packages.json b/packages/packages.json index 20a41cbf4..cb67b674d 100644 --- a/packages/packages.json +++ b/packages/packages.json @@ -15,15 +15,15 @@ "contract/valory/mech_activity/0.1.0": "bafybeibmqmle5fnal3gxlpdmcos2kogzra4q3pr3o5nh7shplxuilji3t4", "contract/valory/staking_token/0.1.0": "bafybeiep4r6qyilbfgzdvx6t7zvpgaioxqktmxm7puwtnbpb2ftlib43gy", "contract/valory/relayer/0.1.0": "bafybeicawmds6czx7db2lcktvexwrp245jpekgulndtos5s5zdid3ilvq4", - "skill/valory/market_manager_abci/0.1.0": "bafybeicztk62pslofv6ui3aw3giw2tnvlfwfmatqbyvvzv4ampneu6isqa", - "skill/valory/decision_maker_abci/0.1.0": "bafybeig3sqaeqedobqdg7gynrxnbq2kgzh4gp5pe5gxo5kw4hczfjmj6e4", - "skill/valory/trader_abci/0.1.0": "bafybeigm2oqol7yvbspdapdrq3hxugybwmaazom773ncsyz6mlgps7y3pi", - "skill/valory/tx_settlement_multiplexer_abci/0.1.0": "bafybeibx63ico4nlp6etvtzgvlcrl3jdy6rx7zodwmxhvvb4phizd732l4", - "skill/valory/staking_abci/0.1.0": "bafybeictd5pxhscuhqntvctb7l5lfjausxt2m22rg5mkaiuj4cwwcxpvne", - "skill/valory/check_stop_trading_abci/0.1.0": "bafybeib75qrimmvensqmskdp5kzki5ijjwolqk2ojekeommakaf64mzn54", - "agent/valory/trader/0.1.0": "bafybeifwqory3yuyhi6sxkoy3ihyzdbus444pkehyoxiibjqo5mjcawbhe", - "service/valory/trader/0.1.0": "bafybeiezli7klgpvdlvdkh7wxmulyosp7f3xzmkmdtzrgtcloqdng5qcea", - "service/valory/trader_pearl/0.1.0": "bafybeic52jtgilmipang6wcr3ogbfyskwzb7iaat3lur5pe7fjkpkqc7da" + "skill/valory/market_manager_abci/0.1.0": "bafybeiaru2d32wpmcgqs64eepxud4idgubc3vmsbdwbia7gygipql2mmqi", + "skill/valory/decision_maker_abci/0.1.0": "bafybeiddnmcquiuznts67ridhpnaqw2y3rrt4nfau5kjm74zhk5lhjxi2q", + "skill/valory/trader_abci/0.1.0": "bafybeiemjz3ca7la7jkeqdr7hxo7fa77xe2fkfadzb53gdkji7abpl2eiu", + "skill/valory/tx_settlement_multiplexer_abci/0.1.0": "bafybeietwknem7iiood6pwkfup322ywwjmdrmdapllrcms6jpeev5w2qfe", + "skill/valory/staking_abci/0.1.0": "bafybeicupccurmrg7qesivonlyt3nryarsmk5qf5yh6auno64wn45bybvq", + "skill/valory/check_stop_trading_abci/0.1.0": "bafybeieduekpd4zbvjztyxyooppqnmjvup6jfp74uo6hhupvtvzzscdzkq", + "agent/valory/trader/0.1.0": "bafybeihq257kwjybsvrpzhjx4wbretrsurodpckjqdhv3idlnbu4mqvfnq", + "service/valory/trader/0.1.0": "bafybeihr6m4lec5r6kuf2zikusgyqilqwhwlpyehufyndjuziylr73dlqe", + "service/valory/trader_pearl/0.1.0": "bafybeibfqhsrtekx6nvwpz2nbbjyobljcahe6wz6jikyebt7opzxlal45u" }, "third_party": { "protocol/open_aea/signing/1.0.0": "bafybeihv62fim3wl2bayavfcg3u5e5cxu3b7brtu4cn5xoxd6lqwachasi", diff --git a/packages/valory/agents/trader/aea-config.yaml b/packages/valory/agents/trader/aea-config.yaml index 9b19512a1..5471b02ee 100644 --- a/packages/valory/agents/trader/aea-config.yaml +++ b/packages/valory/agents/trader/aea-config.yaml @@ -45,12 +45,12 @@ skills: - valory/reset_pause_abci:0.1.0:bafybeigrdlxed3xlsnxtjhnsbl3cojruihxcqx4jxhgivkd5i2fkjncgba - valory/termination_abci:0.1.0:bafybeib5l7jhew5ic6iq24dd23nidcoimzqkrk556gqywhoziatj33zvwm - valory/transaction_settlement_abci:0.1.0:bafybeic7q7recyka272udwcupblwbkc3jkodgp74fvcdxb7urametg5dae -- valory/tx_settlement_multiplexer_abci:0.1.0:bafybeibx63ico4nlp6etvtzgvlcrl3jdy6rx7zodwmxhvvb4phizd732l4 -- valory/market_manager_abci:0.1.0:bafybeicztk62pslofv6ui3aw3giw2tnvlfwfmatqbyvvzv4ampneu6isqa -- valory/decision_maker_abci:0.1.0:bafybeig3sqaeqedobqdg7gynrxnbq2kgzh4gp5pe5gxo5kw4hczfjmj6e4 -- valory/trader_abci:0.1.0:bafybeigm2oqol7yvbspdapdrq3hxugybwmaazom773ncsyz6mlgps7y3pi -- valory/staking_abci:0.1.0:bafybeictd5pxhscuhqntvctb7l5lfjausxt2m22rg5mkaiuj4cwwcxpvne -- valory/check_stop_trading_abci:0.1.0:bafybeib75qrimmvensqmskdp5kzki5ijjwolqk2ojekeommakaf64mzn54 +- valory/tx_settlement_multiplexer_abci:0.1.0:bafybeietwknem7iiood6pwkfup322ywwjmdrmdapllrcms6jpeev5w2qfe +- valory/market_manager_abci:0.1.0:bafybeiaru2d32wpmcgqs64eepxud4idgubc3vmsbdwbia7gygipql2mmqi +- valory/decision_maker_abci:0.1.0:bafybeiddnmcquiuznts67ridhpnaqw2y3rrt4nfau5kjm74zhk5lhjxi2q +- valory/trader_abci:0.1.0:bafybeiemjz3ca7la7jkeqdr7hxo7fa77xe2fkfadzb53gdkji7abpl2eiu +- valory/staking_abci:0.1.0:bafybeicupccurmrg7qesivonlyt3nryarsmk5qf5yh6auno64wn45bybvq +- valory/check_stop_trading_abci:0.1.0:bafybeieduekpd4zbvjztyxyooppqnmjvup6jfp74uo6hhupvtvzzscdzkq - valory/mech_interact_abci:0.1.0:bafybeid6m3i5ofq7vuogqapdnoshhq7mswmudhvfcr2craw25fdwtoe3lm customs: - valory/mike_strat:0.1.0:bafybeihjiol7f4ch4piwfikurdtfwzsh6qydkbsztpbwbwb2yrqdqf726m diff --git a/packages/valory/services/trader/service.yaml b/packages/valory/services/trader/service.yaml index a1ea6c517..3f33c0a3c 100644 --- a/packages/valory/services/trader/service.yaml +++ b/packages/valory/services/trader/service.yaml @@ -7,7 +7,7 @@ license: Apache-2.0 fingerprint: README.md: bafybeigtuothskwyvrhfosps2bu6suauycolj67dpuxqvnicdrdu7yhtvq fingerprint_ignore_patterns: [] -agent: valory/trader:0.1.0:bafybeifwqory3yuyhi6sxkoy3ihyzdbus444pkehyoxiibjqo5mjcawbhe +agent: valory/trader:0.1.0:bafybeihq257kwjybsvrpzhjx4wbretrsurodpckjqdhv3idlnbu4mqvfnq number_of_agents: 4 deployment: agent: diff --git a/packages/valory/services/trader_pearl/service.yaml b/packages/valory/services/trader_pearl/service.yaml index 90e1c3d6f..88e6cefea 100644 --- a/packages/valory/services/trader_pearl/service.yaml +++ b/packages/valory/services/trader_pearl/service.yaml @@ -8,7 +8,7 @@ license: Apache-2.0 fingerprint: README.md: bafybeibg7bdqpioh4lmvknw3ygnllfku32oca4eq5pqtvdrdsgw6buko7e fingerprint_ignore_patterns: [] -agent: valory/trader:0.1.0:bafybeifwqory3yuyhi6sxkoy3ihyzdbus444pkehyoxiibjqo5mjcawbhe +agent: valory/trader:0.1.0:bafybeihq257kwjybsvrpzhjx4wbretrsurodpckjqdhv3idlnbu4mqvfnq number_of_agents: 1 deployment: agent: diff --git a/packages/valory/skills/check_stop_trading_abci/behaviours.py b/packages/valory/skills/check_stop_trading_abci/behaviours.py index cbc0191ec..f1eac99e8 100644 --- a/packages/valory/skills/check_stop_trading_abci/behaviours.py +++ b/packages/valory/skills/check_stop_trading_abci/behaviours.py @@ -140,27 +140,29 @@ def is_staking_kpi_met(self) -> Generator[None, None, bool]: return True return False - def _compute_stop_trading(self) -> Generator: + def _compute_stop_trading(self) -> Generator[None, None, bool]: # This is a "hacky" way of getting required data initialized on # the Trader: On first period, the FSM needs to initialize some # data on the trading branch so that it is available in the # cross-period persistent keys. if self.is_first_period: self.context.logger.debug(f"{self.is_first_period=}") + yield # ensures this is a generator return False self.context.logger.debug(f"{self.params.disable_trading=}") if self.params.disable_trading: + yield # ensures this is a generator return True - stop_trading_conditions = [] self.context.logger.debug(f"{self.params.stop_trading_if_staking_kpi_met=}") if self.params.stop_trading_if_staking_kpi_met: staking_kpi_met = yield from self.is_staking_kpi_met() self.context.logger.debug(f"{staking_kpi_met=}") - stop_trading_conditions.append(staking_kpi_met) + return staking_kpi_met - return any(stop_trading_conditions) + yield + return False def async_act(self) -> Generator: """Do the action.""" diff --git a/packages/valory/skills/check_stop_trading_abci/skill.yaml b/packages/valory/skills/check_stop_trading_abci/skill.yaml index 45c3db5eb..d37ff0def 100644 --- a/packages/valory/skills/check_stop_trading_abci/skill.yaml +++ b/packages/valory/skills/check_stop_trading_abci/skill.yaml @@ -8,7 +8,7 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: README.md: bafybeif2pq7fg5upl6vmfgfzpiwsh4nbk4zaeyz6upyucqi5tasrxgq4ee __init__.py: bafybeifc23rlw2hzhplp3wfceixnmwq5ztnixhh7jp4dd5av3crwp3x22a - behaviours.py: bafybeifdfqkczchnbfzzb4q2gwe3qqrn5ci2tk4mq65bunivjkw4scuzse + behaviours.py: bafybeie5mkpxsd6z3vjsoacvswin6zz4q4um5gqp6jhwtn65fepx2kma3m dialogues.py: bafybeictrrnwcijiejczy23dfvbx5kujgef3dulzqhs3etl2juvz5spm2e fsm_specification.yaml: bafybeihhau35a5xclncjpxh5lg7qiw34xs4d5qlez7dnjpkf45d3gc57ai handlers.py: bafybeiard64fwxib3rtyp67ymhf222uongcyqhfhdyttpsyqkmyh5ajipu @@ -27,7 +27,7 @@ contracts: protocols: [] skills: - valory/abstract_round_abci:0.1.0:bafybeib733xfbndtpvkf44mtk7oyodnficgloo6xhn7xmqxxeos33es65u -- valory/staking_abci:0.1.0:bafybeictd5pxhscuhqntvctb7l5lfjausxt2m22rg5mkaiuj4cwwcxpvne +- valory/staking_abci:0.1.0:bafybeicupccurmrg7qesivonlyt3nryarsmk5qf5yh6auno64wn45bybvq behaviours: main: args: {} diff --git a/packages/valory/skills/decision_maker_abci/behaviours/base.py b/packages/valory/skills/decision_maker_abci/behaviours/base.py index cc91d2a9a..805518b7b 100644 --- a/packages/valory/skills/decision_maker_abci/behaviours/base.py +++ b/packages/valory/skills/decision_maker_abci/behaviours/base.py @@ -351,6 +351,22 @@ def check_balance(self) -> WaitableConditionType: self._report_balance() return True + def update_bet_transaction_information(self) -> None: + """Get whether the bet's invested amount should be updated.""" + sampled_bet = self.sampled_bet + # Update the bet's invested amount, the new bet amount is added to previously invested amount + sampled_bet.invested_amount += self.synchronized_data.bet_amount + # Update bet transaction timestamp + sampled_bet.processed_timestamp = self.synced_timestamp + # update no of bets made + sampled_bet.n_bets += 1 + # Update Queue number for priority logic + sampled_bet.queue_status = sampled_bet.queue_status.next_status() + + # the bets are stored here, but we do not update the hash in the synced db in the redeeming round + # this will need to change if this sovereign agent is ever converted to a multi-agent service + self.store_bets() + def send_message( self, msg: Message, dialogue: Dialogue, callback: Callable ) -> None: diff --git a/packages/valory/skills/decision_maker_abci/behaviours/blacklisting.py b/packages/valory/skills/decision_maker_abci/behaviours/blacklisting.py index ae50fcab7..4aba14762 100644 --- a/packages/valory/skills/decision_maker_abci/behaviours/blacklisting.py +++ b/packages/valory/skills/decision_maker_abci/behaviours/blacklisting.py @@ -45,9 +45,10 @@ def _blacklist(self) -> None: """Blacklist the sampled bet.""" sampled_bet_index = self.synchronized_data.sampled_bet_index sampled_bet = self.bets[sampled_bet_index] + # the question is blacklisted, i.e., we did not place a bet on it, - # therefore, we decrease the number of bets which was increased on sampling - sampled_bet.n_bets -= 1 + # therefore, we bump to the queue status to next status + sampled_bet.queue_status = sampled_bet.queue_status.next_status() def setup(self) -> None: """Setup the behaviour""" diff --git a/packages/valory/skills/decision_maker_abci/behaviours/decision_receive.py b/packages/valory/skills/decision_maker_abci/behaviours/decision_receive.py index 58eae2f91..5ad86dec8 100644 --- a/packages/valory/skills/decision_maker_abci/behaviours/decision_receive.py +++ b/packages/valory/skills/decision_maker_abci/behaviours/decision_receive.py @@ -508,25 +508,18 @@ def _update_selected_bet( ) -> None: """Update the selected bet.""" # update the bet's timestamp of processing and its number of bets for the given id - if self.benchmarking_mode.enabled: - active_sampled_bet = self.get_active_sampled_bet() - active_sampled_bet.processed_timestamp = ( - self.shared_state.get_simulated_now_timestamp( - self.bets, self.params.safe_voting_range - ) - ) - self.context.logger.info(f"Updating bet id: {active_sampled_bet.id}") - self.context.logger.info( - f"with the timestamp:{datetime.fromtimestamp(active_sampled_bet.processed_timestamp)}" + active_sampled_bet = self.get_active_sampled_bet() + active_sampled_bet.processed_timestamp = ( + self.shared_state.get_simulated_now_timestamp( + self.bets, self.params.safe_voting_range ) - if prediction_response is not None: - active_sampled_bet.n_bets += 1 - - else: - # update the bet's timestamp of processing and its number of bets for the given - sampled_bet = self.sampled_bet - sampled_bet.n_bets += 1 - sampled_bet.processed_timestamp = self.synced_timestamp + ) + self.context.logger.info(f"Updating bet id: {active_sampled_bet.id}") + self.context.logger.info( + f"with the timestamp:{datetime.fromtimestamp(active_sampled_bet.processed_timestamp)}" + ) + if prediction_response is not None: + active_sampled_bet.n_bets += 1 self.store_bets() @@ -558,6 +551,7 @@ def async_act(self) -> Generator: prediction_response, bet_amount, ) + # always remove the processed trade from the benchmarking input file # now there is one reader pointer per market if self.benchmarking_mode.enabled: @@ -568,7 +562,8 @@ def async_act(self) -> Generator: if rows_queue: rows_queue.pop(0) - self._update_selected_bet(prediction_response) + self._update_selected_bet(prediction_response) + payload = DecisionReceivePayload( self.context.agent_address, bets_hash, diff --git a/packages/valory/skills/decision_maker_abci/behaviours/reedem.py b/packages/valory/skills/decision_maker_abci/behaviours/reedem.py index a00c5b60e..e6019516b 100644 --- a/packages/valory/skills/decision_maker_abci/behaviours/reedem.py +++ b/packages/valory/skills/decision_maker_abci/behaviours/reedem.py @@ -52,6 +52,9 @@ FPMM, Trade, ) +from packages.valory.skills.decision_maker_abci.states.bet_placement import ( + BetPlacementRound, +) from packages.valory.skills.decision_maker_abci.states.redeem import RedeemRound from packages.valory.skills.market_manager_abci.graph_tooling.requests import ( FetchStatus, @@ -970,6 +973,17 @@ def async_act(self) -> Generator: if self.benchmarking_mode.enabled: payload = self._benchmarking_act() else: + # Checking if the last round that submitted the transaction was the bet placement round + # If so, we need to update the bet transaction information, because the transaction was successful + # tx settlement multiplexer assures transitions from Post transaction to Redeem round + # only if the transaction was successful + if ( + self.synchronized_data.did_transact + and self.synchronized_data.tx_submitter + == BetPlacementRound.auto_round_id() + ): + self.update_bet_transaction_information() + payload = yield from self._normal_act() if payload is None: return diff --git a/packages/valory/skills/decision_maker_abci/behaviours/sampling.py b/packages/valory/skills/decision_maker_abci/behaviours/sampling.py index 12d71ecef..944149a49 100644 --- a/packages/valory/skills/decision_maker_abci/behaviours/sampling.py +++ b/packages/valory/skills/decision_maker_abci/behaviours/sampling.py @@ -19,16 +19,16 @@ """This module contains the behaviour for sampling a bet.""" -import random +from collections import defaultdict from datetime import datetime -from typing import Any, Generator, List, Optional +from typing import Any, Dict, Generator, List, Optional, Tuple from packages.valory.skills.decision_maker_abci.behaviours.base import ( DecisionMakerBaseBehaviour, ) from packages.valory.skills.decision_maker_abci.payloads import SamplingPayload from packages.valory.skills.decision_maker_abci.states.sampling import SamplingRound -from packages.valory.skills.market_manager_abci.bets import Bet +from packages.valory.skills.market_manager_abci.bets import Bet, QueueStatus WEEKDAYS = 7 @@ -49,15 +49,6 @@ def __init__(self, **kwargs: Any) -> None: def setup(self) -> None: """Setup the behaviour.""" self.read_bets() - has_bet_in_the_past = any(bet.n_bets > 0 for bet in self.bets) - if has_bet_in_the_past: - if self.benchmarking_mode.enabled: - random.seed(self.benchmarking_mode.randomness) - else: - random.seed(self.synchronized_data.most_voted_randomness) - self.should_rebet = random.random() <= self.params.rebet_chance # nosec - rebetting_status = "enabled" if self.should_rebet else "disabled" - self.context.logger.info(f"Rebetting {rebetting_status}.") def processable_bet(self, bet: Bet, now: int) -> bool: """Whether we can process the given bet.""" @@ -74,32 +65,78 @@ def processable_bet(self, bet: Bet, now: int) -> bool: within_ranges = within_opening_range and within_safe_range - # rebetting is allowed only if we have already placed at least one bet in this market. - # conversely, if we should not rebet, no bets should have been placed in this market. - if self.should_rebet ^ bool(bet.n_bets): - return False + # check if bet queue number is processable + processable_statuses = { + QueueStatus.TO_PROCESS, + QueueStatus.PROCESSED, + QueueStatus.REPROCESSED, + } + bet_queue_processable = bet.queue_status in processable_statuses - # if we should not rebet, we have all the information we need - if not self.should_rebet: - return within_ranges + return within_ranges and bet_queue_processable - # create a filter based on whether we can rebet or not - lifetime = bet.openingTimestamp - now - t_rebetting = (lifetime // UNIX_WEEK) + UNIX_DAY - can_rebet = now >= bet.processed_timestamp + t_rebetting - return within_ranges and can_rebet + @staticmethod + def _sort_by_priority_logic(bets: List[Bet]) -> List[Bet]: + """ + Sort bets based on the priority logic. - def _sampled_bet_idx(self, bets: List[Bet]) -> int: + :param bets: the bets to sort. + :return: the sorted list of bets. """ - Sample a bet and return its id. + return sorted( + bets, + key=lambda bet: ( + bet.invested_amount, + -bet.processed_timestamp, # Increasing order of processed_timestamp + bet.scaledLiquidityMeasure, + bet.openingTimestamp, + ), + reverse=True, + ) - The sampling logic is relatively simple at the moment. - It simply selects the unprocessed bet with the largest liquidity. + @staticmethod + def _get_bets_queue_wise(bets: List[Bet]) -> Tuple[List[Bet], List[Bet], List[Bet]]: + """Return a dictionary of bets with queue status as key.""" + + bets_by_status: Dict[QueueStatus, List[Bet]] = defaultdict(list) + + for bet in bets: + bets_by_status[bet.queue_status].append(bet) + + return ( + bets_by_status[QueueStatus.TO_PROCESS], + bets_by_status[QueueStatus.PROCESSED], + bets_by_status[QueueStatus.REPROCESSED], + ) + + def _sampled_bet_idx(self, bets: List[Bet]) -> int: + """ + Sample a bet and return its index. + + The sampling logic follows the specified priority logic: + 1. Filter out all the bets that have a processed_timestamp != 0 to get a list of new bets. + 2. If the list of new bets is not empty: + 2.1 Order the list in decreasing order of liquidity (highest liquidity first). + 2.2 For bets with the same liquidity, order them in decreasing order of market closing time (openingTimestamp). + 3. If the list of new bets is empty: + 3.1 Order the bets in decreasing order of invested_amount. + 3.2 For bets with the same invested_amount, order them in increasing order of processed_timestamp (least recently processed first). + 3.3 For bets with the same invested_amount and processed_timestamp, order them in decreasing order of liquidity. + 3.4 For bets with the same invested_amount, processed_timestamp, and liquidity, order them in decreasing order of market closing time (openingTimestamp). :param bets: the bets' values to compare for the sampling. - :return: the id of the sampled bet, out of all the available bets, not only the given ones. + :return: the index of the sampled bet, out of all the available bets, not only the given ones. """ - return self.bets.index(max(bets)) + + to_process_bets, processed_bets, reprocessed_bets = self._get_bets_queue_wise( + bets + ) + # pick the first queue status that has bets in it + bets_to_sort: List[Bet] = to_process_bets or processed_bets or reprocessed_bets + + sorted_bets = self._sort_by_priority_logic(bets_to_sort) + + return self.bets.index(sorted_bets[0]) def _sample(self) -> Optional[int]: """Sample a bet, mark it as processed, and return its index.""" @@ -114,16 +151,24 @@ def _sample(self) -> Optional[int]: self.context.logger.info(f"Simulating date: {datetime.fromtimestamp(now)}") else: now = self.synced_timestamp + + # filter out only the bets that are processable and have a queue_status that allows them to be sampled available_bets = list( - filter(lambda bet: self.processable_bet(bet, now=now), self.bets) + filter( + lambda bet: self.processable_bet(bet, now=now), + self.bets, + ) ) if len(available_bets) == 0: msg = "There were no unprocessed bets available to sample from!" self.context.logger.warning(msg) return None + # sample a bet using the priority logic idx = self._sampled_bet_idx(available_bets) sampled_bet = self.bets[idx] + + # fetch the liquidity of the sampled bet and cache it liquidity = sampled_bet.scaledLiquidityMeasure if liquidity == 0: msg = "There were no unprocessed bets with non-zero liquidity!" @@ -135,28 +180,38 @@ def _sample(self) -> Optional[int]: self.context.logger.info(msg) return idx + def _benchmarking_inc_day(self) -> Tuple[bool, bool]: + """Increase the simulated day in benchmarking mode.""" + self.context.logger.info( + "No more markets to bet in the simulated day. Increasing simulated day." + ) + self.shared_state.increase_one_day_simulation() + benchmarking_finished = self.shared_state.check_benchmarking_finished() + if benchmarking_finished: + self.context.logger.info("No more days to simulate in benchmarking mode.") + + day_increased = True + + return benchmarking_finished, day_increased + def async_act(self) -> Generator: """Do the action.""" with self.context.benchmark_tool.measure(self.behaviour_id).local(): idx = self._sample() benchmarking_finished = None day_increased = None + + # day increase simulation and benchmarking finished check if idx is None and self.benchmarking_mode.enabled: - self.context.logger.info( - "No more markets to bet in the simulated day. Increasing simulated day." - ) - self.shared_state.increase_one_day_simulation() - benchmarking_finished = self.shared_state.check_benchmarking_finished() - if benchmarking_finished: - self.context.logger.info( - "No more days to simulate in benchmarking mode." - ) - day_increased = True + benchmarking_finished, day_increased = self._benchmarking_inc_day() + self.store_bets() + if idx is None: bets_hash = None else: bets_hash = self.hash_stored_bets() + payload = SamplingPayload( self.context.agent_address, bets_hash, diff --git a/packages/valory/skills/decision_maker_abci/skill.yaml b/packages/valory/skills/decision_maker_abci/skill.yaml index fccbe682c..cf113fe63 100644 --- a/packages/valory/skills/decision_maker_abci/skill.yaml +++ b/packages/valory/skills/decision_maker_abci/skill.yaml @@ -12,19 +12,19 @@ fingerprint: README.md: bafybeia367zzdwndvlhw27rvnwodytjo3ms7gbc3q7mhrrjqjgfasnk47i __init__.py: bafybeih563ujnigeci2ldzh7hakbau6a222vsed7leg3b7lq32vcn3nm4a behaviours/__init__.py: bafybeih6ddz2ocvm6x6ytvlbcz6oi4snb5ee5xh5h65nq4w2qf7fd7zfky - behaviours/base.py: bafybeifjgxzhwzxiky3okgtv4ojumm7fj7bom6qe3ysdvs3cpu32w446g4 + behaviours/base.py: bafybeieqvxpzjnwjgq4ffhjojle2q7onif44nefrlza6pxriewisqcm324 behaviours/bet_placement.py: bafybeia4listbfzsk4n4wkc4ycaftxgywjnl3mmpcqhuo3nwwia4n3oufu - behaviours/blacklisting.py: bafybeifitqx2omj5qdwokizhqjkxvybtsyxo22dxkucbtxaocafzgbseku + behaviours/blacklisting.py: bafybeicah7vcruhbakrhpsxdmw3ftq72o5e3plfhhqsnvhk3vk6iud6og4 behaviours/check_benchmarking.py: bafybeiao2lyj7apezkqrpgsyzb3dwvrdgsrgtprf6iuhsmlsufvxfl5bci behaviours/claim_subscription.py: bafybeigbqkhc6mb73rbwaks32tfiqx6u2xza43uiy6rvbtrnqd6m4fru3e - behaviours/decision_receive.py: bafybeiaph3ft4j3br4k7bddymzv5ffcexmlup2l4prk5rvhqlilxtq57oa + behaviours/decision_receive.py: bafybeifrsnxref7yyl4hq4nmh4xa4rden4wygv5y5ugvqb5ji7zgj7mv5m behaviours/decision_request.py: bafybeia22omb7tvocyfe3z2ucn5au5mcas7dg37ha42u7znefzrewjpk7y behaviours/handle_failed_tx.py: bafybeidxpc6u575ymct5tdwutvzov6zqfdoio5irgldn3fw7q3lg36mmxm behaviours/order_subscription.py: bafybeib3maqohhx35wzryy4otdcjp5thkr4sbp27ksvwidy3pwm444itra behaviours/randomness.py: bafybeiaoj3awyyg2onhpsdsn3dyczs23gr4smuzqcbw3e5ocljwxswjkce - behaviours/reedem.py: bafybeiaxwp4lx62owcaqfp6xcqh6567f5yvwnl4rage2f5hmq4nltkzjjy + behaviours/reedem.py: bafybeidjmhh6c6shbg25d7exmc4nnp4heqbqselwuxj7rp2ss665lrytxe behaviours/round_behaviour.py: bafybeih63hpia2bwwzu563hxs5yd3t5ycvxvkfnhvxbzghbyy3mw3xjl3i - behaviours/sampling.py: bafybeihlpkinxgewpyazax2qlwzlo5iwpxcce6g5juybn6qinstzku27fi + behaviours/sampling.py: bafybeicvtxjv5rxlsdrmbtetqwzzau6is47guystvw245grd6s2qs5pxea behaviours/storage_manager.py: bafybeiez6daaj2bufxdcsghtmqybyrzdh74z26cc4ajsqsiy5krgjo2tla behaviours/tool_selection.py: bafybeienlxcgjs3ogyofli3d7q3p5rst3mcxxcnwqf7qolqjeefjtixeke dialogues.py: bafybeigpwuzku3we7axmxeamg7vn656maww6emuztau5pg3ebsoquyfdqm @@ -38,7 +38,7 @@ fingerprint: redeem_info.py: bafybeifiiix4gihfo4avraxt34sfw35v6dqq45do2drrssei2shbps63mm rounds.py: bafybeiazjcsukgefair52aw37hhvxzlopnzqqmi4ntqrinakljlcm4kt4a states/__init__.py: bafybeid23llnyp6j257dluxmrnztugo5llsrog7kua53hllyktz4dqhqoy - states/base.py: bafybeifkip6bw3oacpnyhko7fi3i72nv2fc33ld6bkr2myaay4qa2ybcie + states/base.py: bafybeiatr6cqa3juxkhm6p4h7dhol7tfmxh2fo6nr2gtc6wuwyrtu3k3xm states/bet_placement.py: bafybeih5eopyxubczys5u5t3bdxbxpc7mmfdyqrpqsbm2uha5jc2phza4i states/blacklisting.py: bafybeiapelgjhbjjn4uq4z5gspyirqzwzgccg5anktrp5kxdwamfnfw5mi states/check_benchmarking.py: bafybeiabv6pq7q45jd3nkor5afmlycqgec5ctuwcfbdukkjjm4imesv4ni @@ -101,10 +101,10 @@ protocols: - valory/http:1.0.0:bafybeifugzl63kfdmwrxwphrnrhj7bn6iruxieme3a4ntzejf6kmtuwmae skills: - valory/abstract_round_abci:0.1.0:bafybeib733xfbndtpvkf44mtk7oyodnficgloo6xhn7xmqxxeos33es65u -- valory/market_manager_abci:0.1.0:bafybeicztk62pslofv6ui3aw3giw2tnvlfwfmatqbyvvzv4ampneu6isqa +- valory/market_manager_abci:0.1.0:bafybeiaru2d32wpmcgqs64eepxud4idgubc3vmsbdwbia7gygipql2mmqi - valory/transaction_settlement_abci:0.1.0:bafybeic7q7recyka272udwcupblwbkc3jkodgp74fvcdxb7urametg5dae - valory/mech_interact_abci:0.1.0:bafybeid6m3i5ofq7vuogqapdnoshhq7mswmudhvfcr2craw25fdwtoe3lm -- valory/staking_abci:0.1.0:bafybeictd5pxhscuhqntvctb7l5lfjausxt2m22rg5mkaiuj4cwwcxpvne +- valory/staking_abci:0.1.0:bafybeicupccurmrg7qesivonlyt3nryarsmk5qf5yh6auno64wn45bybvq behaviours: main: args: {} diff --git a/packages/valory/skills/decision_maker_abci/states/base.py b/packages/valory/skills/decision_maker_abci/states/base.py index 33bdc4638..76dd9a67e 100644 --- a/packages/valory/skills/decision_maker_abci/states/base.py +++ b/packages/valory/skills/decision_maker_abci/states/base.py @@ -178,6 +178,11 @@ def is_profitable(self) -> bool: """Get whether the current vote is profitable or not.""" return bool(self.db.get_strict("is_profitable")) + @property + def did_transact(self) -> bool: + """Get whether the service performed any transactions in the current period.""" + return bool(self.db.get("tx_submitter", None)) + @property def tx_submitter(self) -> str: """Get the round that submitted a tx to transaction_settlement_abci.""" diff --git a/packages/valory/skills/market_manager_abci/behaviours.py b/packages/valory/skills/market_manager_abci/behaviours.py index 04c26cba7..999523130 100644 --- a/packages/valory/skills/market_manager_abci/behaviours.py +++ b/packages/valory/skills/market_manager_abci/behaviours.py @@ -47,6 +47,7 @@ BETS_FILENAME = "bets.json" +MULTI_BETS_FILENAME = "multi_bets.json" READ_MODE = "r" WRITE_MODE = "w" @@ -58,6 +59,7 @@ def __init__(self, **kwargs: Any) -> None: """Initialize `BetsManagerBehaviour`.""" super().__init__(**kwargs) self.bets: List[Bet] = [] + self.multi_bets_filepath: str = self.params.store_path / MULTI_BETS_FILENAME self.bets_filepath: str = self.params.store_path / BETS_FILENAME def store_bets(self) -> None: @@ -68,44 +70,48 @@ def store_bets(self) -> None: return try: - with open(self.bets_filepath, WRITE_MODE) as bets_file: + with open(self.multi_bets_filepath, WRITE_MODE) as bets_file: try: bets_file.write(serialized) return except (IOError, OSError): - err = f"Error writing to file {self.bets_filepath!r}!" + err = f"Error writing to file {self.multi_bets_filepath!r}!" except (FileNotFoundError, PermissionError, OSError): - err = f"Error opening file {self.bets_filepath!r} in write mode!" + err = f"Error opening file {self.multi_bets_filepath!r} in write mode!" self.context.logger.error(err) def read_bets(self) -> None: """Read the bets from the agent's data dir as JSON.""" self.bets = [] + _read_path = self.multi_bets_filepath - if not os.path.isfile(self.bets_filepath): + if not os.path.isfile(_read_path): self.context.logger.warning( - f"No stored bets file was detected in {self.bets_filepath}. Assuming bets are empty." + f"No stored bets file was detected in {_read_path}. Assuming trader is being run for the first time in multi-bets mode." + ) + _read_path = self.bets_filepath + elif not os.path.isfile(_read_path): + self.context.logger.warning( + f"No stored bets file was detected in {_read_path}. Assuming bets are empty" ) return try: - with open(self.bets_filepath, READ_MODE) as bets_file: + with open(_read_path, READ_MODE) as bets_file: try: self.bets = json.load(bets_file, cls=BetsDecoder) return except (JSONDecodeError, TypeError): - err = ( - f"Error decoding file {self.bets_filepath!r} to a list of bets!" - ) + err = f"Error decoding file {_read_path!r} to a list of bets!" except (FileNotFoundError, PermissionError, OSError): - err = f"Error opening file {self.bets_filepath!r} in read mode!" + err = f"Error opening file {_read_path!r} in read mode!" self.context.logger.error(err) def hash_stored_bets(self) -> str: """Get the hash of the stored bets' file.""" - return IPFSHashOnly.hash_file(self.bets_filepath) + return IPFSHashOnly.hash_file(self.multi_bets_filepath) class UpdateBetsBehaviour(BetsManagerBehaviour, QueryingBehaviour): @@ -117,6 +123,33 @@ def __init__(self, **kwargs: Any) -> None: """Initialize `UpdateBetsBehaviour`.""" super().__init__(**kwargs) + def _requeue_all_bets(self) -> None: + """Requeue all bets.""" + for bet in self.bets: + bet.queue_status = bet.queue_status.move_to_fresh() + + def _blacklist_expired_bets(self) -> None: + """Blacklist bets that are older than the opening margin.""" + for bet in self.bets: + if self.synced_time >= bet.openingTimestamp - self.params.opening_margin: + bet.blacklist_forever() + + def setup(self) -> None: + """Set up the behaviour.""" + + # Read the bets from the agent's data dir as JSON, if they exist + self.read_bets() + + # fetch checkpoint status and if reached requeue all bets + if self.synchronized_data.is_checkpoint_reached: + self._requeue_all_bets() + + # blacklist bets that are older than the opening margin + # if trader ran after a long time + # helps in resetting the queue number to 0 + if self.bets: + self._blacklist_expired_bets() + def get_bet_idx(self, bet_id: str) -> Optional[int]: """Get the index of the bet with the given id, if it exists, otherwise `None`.""" return next((i for i, bet in enumerate(self.bets) if bet.id == bet_id), None) @@ -138,6 +171,8 @@ def _update_bets( self, ) -> Generator: """Fetch the questions from all the prediction markets and update the local copy of the bets.""" + + # Fetching bets from the prediction markets while True: can_proceed = self._prepare_fetching() if not can_proceed: @@ -150,20 +185,38 @@ def _update_bets( # this won't wipe the bets as the `store_bets` of the `BetsManagerBehaviour` takes this into consideration self.bets = [] - for bet in self.bets: - if self.synced_time >= bet.openingTimestamp - self.params.opening_margin: - bet.blacklist_forever() - # truncate the bets, otherwise logs get too big bets_str = str(self.bets)[:MAX_LOG_SIZE] self.context.logger.info(f"Updated bets: {bets_str}") + def _bet_freshness_check_and_update(self) -> None: + """Check the freshness of the bets.""" + all_bets_fresh = all( + bet.queue_status.is_fresh() + for bet in self.bets + if not bet.queue_status.is_expired() + ) + + if all_bets_fresh: + for bet in self.bets: + bet.queue_status = bet.queue_status.move_to_process() + + return + def async_act(self) -> Generator: """Do the action.""" with self.context.benchmark_tool.measure(self.behaviour_id).local(): - self.read_bets() + # Update the bets list with new bets or update existing ones yield from self._update_bets() + + # if trader is run after a long time, there is a possibility that + # all bets are fresh and this should be updated to DAY_0_FRESH + if self.bets: + self._bet_freshness_check_and_update() + + # Store the bets to the agent's data dir as JSON self.store_bets() + bets_hash = self.hash_stored_bets() if self.bets else None payload = UpdateBetsPayload(self.context.agent_address, bets_hash) diff --git a/packages/valory/skills/market_manager_abci/bets.py b/packages/valory/skills/market_manager_abci/bets.py index 92a8170dc..6380aae48 100644 --- a/packages/valory/skills/market_manager_abci/bets.py +++ b/packages/valory/skills/market_manager_abci/bets.py @@ -24,6 +24,7 @@ import dataclasses import json import sys +from enum import Enum from typing import Any, Dict, List, Optional, Union @@ -34,6 +35,47 @@ BINARY_N_SLOTS = 2 +class QueueStatus(Enum): + """The status of a bet in the queue.""" + + # Common statuses + EXPIRED = -1 # Bets that have expired, i.e., the market is not live anymore + FRESH = 0 # Fresh bets that have just been added + TO_PROCESS = 1 # Bets that are ready to be processed + PROCESSED = 2 # Bets that have been processed + REPROCESSED = 3 # Bets that have been reprocessed + + def is_fresh(self) -> bool: + """Check if the bet is fresh.""" + return self == QueueStatus.FRESH + + def is_expired(self) -> bool: + """Check if the bet is expired.""" + return self == QueueStatus.EXPIRED + + def move_to_process(self) -> "QueueStatus": + """Move the bet to the process status.""" + if self == QueueStatus.FRESH: + return QueueStatus.TO_PROCESS + return self + + def move_to_fresh(self) -> "QueueStatus": + """Move the bet to the fresh status.""" + if self != QueueStatus.EXPIRED: + return QueueStatus.FRESH + return self + + def next_status(self) -> "QueueStatus": + """Get the next status in the queue.""" + if self == QueueStatus.TO_PROCESS: + return QueueStatus.PROCESSED + elif self == QueueStatus.PROCESSED: + return QueueStatus.REPROCESSED + elif self != QueueStatus.REPROCESSED: + return QueueStatus.FRESH + return self + + @dataclasses.dataclass(init=False) class PredictionResponse: """A response of a prediction.""" @@ -95,10 +137,12 @@ class Bet: prediction_response: PredictionResponse = dataclasses.field( default_factory=get_default_prediction_response ) + invested_amount: float = 0.0 position_liquidity: int = 0 potential_net_profit: int = 0 processed_timestamp: int = 0 n_bets: int = 0 + queue_status: QueueStatus = QueueStatus.FRESH def __post_init__(self) -> None: """Post initialization to adjust the values.""" @@ -114,6 +158,7 @@ def blacklist_forever(self) -> None: """Blacklist a bet forever. Should only be used in cases where it is impossible to bet.""" self.outcomes = None self.processed_timestamp = sys.maxsize + self.queue_status = QueueStatus.EXPIRED def _validate(self) -> None: """Validate the values of the instance.""" @@ -205,6 +250,7 @@ def update_market_info(self, bet: "Bet") -> None: ): # do not update the bet if it has been blacklisted forever return + self.outcomeTokenAmounts = bet.outcomeTokenAmounts.copy() self.outcomeTokenMarginalPrices = bet.outcomeTokenMarginalPrices.copy() self.scaledLiquidityMeasure = bet.scaledLiquidityMeasure @@ -237,8 +283,10 @@ class BetsEncoder(json.JSONEncoder): def default(self, o: Any) -> Any: """The default encoder.""" - if dataclasses.is_dataclass(o): + if dataclasses.is_dataclass(o) and not isinstance(o, type): return dataclasses.asdict(o) + if isinstance(o, QueueStatus): + return o.value return super().default(o) @@ -261,7 +309,16 @@ def hook(data: Dict[str, Any]) -> Union[Bet, PredictionResponse, Dict[str, Bet]] # if this is a `Bet` bet_annotations = sorted(Bet.__annotations__.keys()) if bet_annotations == data_attributes: + data["queue_status"] = QueueStatus(data["queue_status"]) return Bet(**data) + else: + # fetch missing attributes from the data + missing_attributes = set(bet_annotations) - set(data_attributes) + new_attributes = {"queue_status", "invested_amount"} + if missing_attributes == new_attributes: + data["queue_status"] = QueueStatus(0) + data["invested_amount"] = 0 + return Bet(**data) return data diff --git a/packages/valory/skills/market_manager_abci/rounds.py b/packages/valory/skills/market_manager_abci/rounds.py index ba8a16e44..1ac5dc8be 100644 --- a/packages/valory/skills/market_manager_abci/rounds.py +++ b/packages/valory/skills/market_manager_abci/rounds.py @@ -68,6 +68,11 @@ def participant_to_bets_hash(self) -> DeserializedCollection: """Get the participants to bets' hash.""" return self._get_deserialized("participant_to_bets_hash") + @property + def is_checkpoint_reached(self) -> bool: + """Check if the checkpoint is reached.""" + return bool(self.db.get("is_checkpoint_reached", False)) + class MarketManagerAbstractRound(AbstractRound[Event], ABC): """Abstract round for the MarketManager skill.""" diff --git a/packages/valory/skills/market_manager_abci/skill.yaml b/packages/valory/skills/market_manager_abci/skill.yaml index 44e6f05ad..0bff3c686 100644 --- a/packages/valory/skills/market_manager_abci/skill.yaml +++ b/packages/valory/skills/market_manager_abci/skill.yaml @@ -8,8 +8,8 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: README.md: bafybeie6miwn67uin3bphukmf7qgiifh4xtm42i5v3nuyqxzxtehxsqvcq __init__.py: bafybeigrtedqzlq5mtql2ssjsdriw76ml3666m4e2c3fay6vmyzofl6v6e - behaviours.py: bafybeifzt6vykvvgr5rtzmzdbo7bfsxi765xsljdfrktrq35ogf6cdhmna - bets.py: bafybeif25efeykh4lcw36dxzb35bcoaqtays6o2pmtqhgxske3mf2lkku4 + behaviours.py: bafybeicfoszavcyrzahe6qaydlaf27mpbwui7a6wgdbstydbzxmdisxhju + bets.py: bafybeibx3x5nasuj6loneeat2lb7fr7kgsmnz7on7f4gxfbpzerzdvawou dialogues.py: bafybeiebofyykseqp3fmif36cqmmyf3k7d2zbocpl6t6wnlpv4szghrxbm fsm_specification.yaml: bafybeic5cvwfbiu5pywyp3h5s2elvu7jqdrcwayay7o3v3ow47vu2jw53q graph_tooling/__init__.py: bafybeigzo7nhbzafyq3fuhrlewksjvmzttiuk4vonrggtjtph4rw4ncpk4 @@ -24,7 +24,7 @@ fingerprint: handlers.py: bafybeihot2i2yvfkz2gcowvt66wdu6tkjbmv7hsmc4jzt4reqeaiuphbtu models.py: bafybeibjttnga54y4auz6f33ecfrngyw53b2xzpompm72drjsr4xoytmiy payloads.py: bafybeicfymvvtdpkcgmkvthfzmb7dqakepkzslqrz6rcs7nxkz7qq3mrzy - rounds.py: bafybeiflb2k6ritv5tlexlfxyg2okadtviijprqnc7sa7zxdlhr7nnqxfy + rounds.py: bafybeibqqq3vjotaasc67olhlqthka6e6refodguntkmpksgdbqlzme73a tests/__init__.py: bafybeigaewntxawezvygss345kytjijo56bfwddjtfm6egzxfajsgojam4 tests/test_dialogues.py: bafybeiet646su5nsjmvruahuwg6un4uvwzyj2lnn2jvkye6cxooz22f3ja tests/test_handlers.py: bafybeiaz3idwevvlplcyieaqo5oeikuthlte6e2gi4ajw452ylvimwgiki diff --git a/packages/valory/skills/staking_abci/behaviours.py b/packages/valory/skills/staking_abci/behaviours.py index 011705c6a..74df6e501 100644 --- a/packages/valory/skills/staking_abci/behaviours.py +++ b/packages/valory/skills/staking_abci/behaviours.py @@ -21,6 +21,7 @@ from abc import ABC from datetime import datetime, timedelta +from pathlib import Path from typing import Any, Callable, Generator, Optional, Set, Tuple, Type, Union, cast from aea.configurations.data_types import PublicId @@ -64,6 +65,9 @@ NULL_ADDRESS = "0x0000000000000000000000000000000000000000" +CHECKPOINT_FILENAME = "checkpoint.txt" +READ_MODE = "r" +WRITE_MODE = "w" class StakingInteractBaseBehaviour(BaseBehaviour, ABC): @@ -73,6 +77,7 @@ def __init__(self, **kwargs: Any) -> None: """Initialize the behaviour.""" super().__init__(**kwargs) self._service_staking_state: StakingState = StakingState.UNSTAKED + self._checkpoint_ts = 0 @property def params(self) -> StakingParams: @@ -364,6 +369,7 @@ def __init__(self, **kwargs: Any) -> None: self._next_checkpoint: int = 0 self._checkpoint_data: bytes = b"" self._safe_tx_hash: str = "" + self._checkpoint_filepath: Path = self.params.store_path / CHECKPOINT_FILENAME @property def params(self) -> StakingParams: @@ -375,6 +381,17 @@ def synchronized_data(self) -> SynchronizedData: """Return the synchronized data.""" return SynchronizedData(super().synchronized_data.db) + @property + def is_first_period(self) -> bool: + """Return whether it is the first period of the service.""" + return self.synchronized_data.period_count == 0 + + @property + def new_checkpoint_detected(self) -> bool: + """Whether a new checkpoint has been detected.""" + previous_checkpoint = self.synchronized_data.previous_checkpoint + return bool(previous_checkpoint) and previous_checkpoint != self.ts_checkpoint + @property def checkpoint_data(self) -> bytes: """Get the checkpoint data.""" @@ -401,6 +418,38 @@ def safe_tx_hash(self, safe_hash: str) -> None: ) self._safe_tx_hash = safe_hash[2:] + def read_stored_timestamp(self) -> Optional[int]: + """Read the timestamp from the agent's data dir.""" + try: + with open(self._checkpoint_filepath, READ_MODE) as checkpoint_file: + try: + return int(checkpoint_file.readline()) + except (ValueError, TypeError, StopIteration): + err = f"Stored checkpoint timestamp could not be parsed from {self._checkpoint_filepath!r}!" + except (FileNotFoundError, PermissionError, OSError): + err = f"Error opening file {self._checkpoint_filepath!r} in {READ_MODE!r} mode!" + + self.context.logger.error(err) + return None + + def store_timestamp(self) -> int: + """Store the timestamp to the agent's data dir.""" + if self.ts_checkpoint == 0: + self.context.logger.warning("No checkpoint timestamp to store!") + return 0 + + try: + with open(self._checkpoint_filepath, WRITE_MODE) as checkpoint_file: + try: + return checkpoint_file.write(str(self.ts_checkpoint)) + except (IOError, OSError): + err = f"Error writing to file {self._checkpoint_filepath!r}!" + except (FileNotFoundError, PermissionError, OSError): + err = f"Error opening file {self._checkpoint_filepath!r} in {WRITE_MODE!r} mode!" + + self.context.logger.error(err) + return 0 + def _build_checkpoint_tx(self) -> WaitableConditionType: """Get the request tx data encoded.""" result = yield from self._staking_contract_interact( @@ -436,6 +485,33 @@ def _prepare_safe_tx(self) -> Generator[None, None, str]: self.checkpoint_data, ) + def check_new_epoch(self) -> Generator[None, None, bool]: + """Check if a new epoch has been reached.""" + yield from self.wait_for_condition_with_sleep(self._get_ts_checkpoint) + stored_timestamp_invalidated = False + + # if it is the first period of the service, + # 1. check if the stored timestamp is the same as the current checkpoint + # 2. if not, update the corresponding flag + # That way, we protect from the case in which the user stops their service + # before the next checkpoint is reached and restarts it afterward. + if self.is_first_period: + stored_timestamp = self.read_stored_timestamp() + stored_timestamp_invalidated = stored_timestamp != self.ts_checkpoint + + is_checkpoint_reached = ( + stored_timestamp_invalidated or self.new_checkpoint_detected + ) + if is_checkpoint_reached: + self.context.logger.info("An epoch change has been detected.") + status = self.store_timestamp() + if status: + self.context.logger.info( + "Successfully updated the stored checkpoint timestamp." + ) + + return is_checkpoint_reached + def async_act(self) -> Generator: """Do the action.""" with self.context.benchmark_tool.measure(self.behaviour_id).local(): @@ -451,11 +527,14 @@ def async_act(self) -> Generator: self.context.logger.critical("Service has been evicted!") tx_submitter = self.matching_round.auto_round_id() + is_checkpoint_reached = yield from self.check_new_epoch() payload = CallCheckpointPayload( self.context.agent_address, tx_submitter, checkpoint_tx_hex, self.service_staking_state.value, + self.ts_checkpoint, + is_checkpoint_reached, ) with self.context.benchmark_tool.measure(self.behaviour_id).consensus(): diff --git a/packages/valory/skills/staking_abci/models.py b/packages/valory/skills/staking_abci/models.py index 460f4647b..b9ee386a2 100644 --- a/packages/valory/skills/staking_abci/models.py +++ b/packages/valory/skills/staking_abci/models.py @@ -20,6 +20,8 @@ """Models for the Staking ABCI application.""" +import os +from pathlib import Path from typing import Any from packages.valory.skills.abstract_round_abci.models import BaseParams @@ -37,6 +39,25 @@ BenchmarkTool = BaseBenchmarkTool +def get_store_path(kwargs: dict) -> Path: + """Get the path of the store.""" + path = kwargs.get("store_path", "") + if not path: + msg = "The path to the store must be provided as a keyword argument." + raise ValueError(msg) + + # check if the path exists, and we can write to it + if ( + not os.path.isdir(path) + or not os.access(path, os.W_OK) + or not os.access(path, os.R_OK) + ): + msg = f"The store path {path!r} is not a directory or is not writable." + raise ValueError(msg) + + return Path(path) + + class StakingParams(BaseParams): """Staking parameters.""" @@ -51,6 +72,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.mech_activity_checker_contract: str = self._ensure( "mech_activity_checker_contract", kwargs, str ) + self.store_path = get_store_path(kwargs) super().__init__(*args, **kwargs) diff --git a/packages/valory/skills/staking_abci/payloads.py b/packages/valory/skills/staking_abci/payloads.py index ade453f4e..4b1f47e2c 100644 --- a/packages/valory/skills/staking_abci/payloads.py +++ b/packages/valory/skills/staking_abci/payloads.py @@ -38,3 +38,5 @@ class CallCheckpointPayload(MultisigTxPayload): """A transaction payload for the checkpoint call.""" service_staking_state: int + ts_checkpoint: int + is_checkpoint_reached: bool diff --git a/packages/valory/skills/staking_abci/rounds.py b/packages/valory/skills/staking_abci/rounds.py index 9485cb9c5..c6de99b2a 100644 --- a/packages/valory/skills/staking_abci/rounds.py +++ b/packages/valory/skills/staking_abci/rounds.py @@ -86,6 +86,19 @@ def participant_to_checkpoint(self) -> DeserializedCollection: """Get the participants to the checkpoint round.""" return self._get_deserialized("participant_to_checkpoint") + @property + def previous_checkpoint(self) -> Optional[int]: + """Get the previous checkpoint.""" + previous_checkpoint = self.db.get("previous_checkpoint", None) + if previous_checkpoint is None: + return None + return int(previous_checkpoint) + + @property + def is_checkpoint_reached(self) -> bool: + """Check if the checkpoint is reached.""" + return bool(self.db.get("is_checkpoint_reached", False)) + class CallCheckpointRound(CollectSameUntilThresholdRound): """A round for the checkpoint call preparation.""" @@ -97,6 +110,8 @@ class CallCheckpointRound(CollectSameUntilThresholdRound): get_name(SynchronizedData.tx_submitter), get_name(SynchronizedData.most_voted_tx_hash), get_name(SynchronizedData.service_staking_state), + get_name(SynchronizedData.previous_checkpoint), + get_name(SynchronizedData.is_checkpoint_reached), ) collection_key = get_name(SynchronizedData.participant_to_checkpoint) synchronized_data_class = SynchronizedData @@ -179,7 +194,11 @@ class StakingAbciApp(AbciApp[Event]): # pylint: disable=too-few-public-methods ServiceEvictedRound: {}, } cross_period_persisted_keys = frozenset( - {get_name(SynchronizedData.service_staking_state)} + { + get_name(SynchronizedData.service_staking_state), + get_name(SynchronizedData.previous_checkpoint), + get_name(SynchronizedData.is_checkpoint_reached), + } ) final_states: Set[AppState] = { CheckpointCallPreparedRound, @@ -195,6 +214,8 @@ class StakingAbciApp(AbciApp[Event]): # pylint: disable=too-few-public-methods get_name(SynchronizedData.tx_submitter), get_name(SynchronizedData.most_voted_tx_hash), get_name(SynchronizedData.service_staking_state), + get_name(SynchronizedData.previous_checkpoint), + get_name(SynchronizedData.is_checkpoint_reached), }, FinishedStakingRound: { get_name(SynchronizedData.service_staking_state), diff --git a/packages/valory/skills/staking_abci/skill.yaml b/packages/valory/skills/staking_abci/skill.yaml index 43e2dcc22..9d866d5c2 100644 --- a/packages/valory/skills/staking_abci/skill.yaml +++ b/packages/valory/skills/staking_abci/skill.yaml @@ -8,18 +8,18 @@ aea_version: '>=1.0.0, <2.0.0' fingerprint: README.md: bafybeifrpl36fddmgvniwvghqtxdzc44ry6l2zvqy37vu3y2xvwyd23ugy __init__.py: bafybeiageyes36ujnvvodqd5vlnihgz44rupysrk2ebbhskjkueetj6dai - behaviours.py: bafybeib5rcg26usohcbf2suahk5jticruzzdl2hdakchwsf6bptksi32oy + behaviours.py: bafybeiels3us2s5sf2lrloekjky46o2owlo3awsocuxt67h76ap2k5qaqy dialogues.py: bafybeiebofyykseqp3fmif36cqmmyf3k7d2zbocpl6t6wnlpv4szghrxbm fsm_specification.yaml: bafybeicuoejmaks3ndwhbflp64kkfdkrdyn74a2fplarg4l3gxlonfmeoq handlers.py: bafybeichsi2y5zvzffupj2vhgagocwvnm7cbzr6jmavp656mfrzsdvkfnu - models.py: bafybeidc6aghkskpy5ze62xpjbinwgsyvtzvyrlsfckrygcnj4cts54zpa - payloads.py: bafybeibnub5ehb2mvpcoan3x23pp5oz4azpofwrtcl32abswcfl4cmjlwq - rounds.py: bafybeic7kre4hriounn6at63fjzttw45zoivxatg23cmojok4ah6fca7ca + models.py: bafybeibhzoi25wqpgxwn7ibgz3juax2aeqep7qdl6q7u3abdnb6jahc264 + payloads.py: bafybeib7hxsibtabb5vt6gjtco2x2s2yodhs24ojqecgrcexqsiv3pvmuy + rounds.py: bafybeiezsn42e2qqdpwgtkvrlgjzfwtg7fldfqglf5ofc36g7uyaobvuhq tests/__init__.py: bafybeid7m6ynosqeb4mvsss2hqg75aly5o2d47r7yfg2xtgwzkkilv2d2m tests/test_dialogues.py: bafybeidwjk52mufwvkj4cr3xgqycbdzxc6gvosmqyuqdjarnrgwth6wcai tests/test_handers.py: bafybeibnxlwznx3tsdpjpzh62bnp6lq7zdpolyjxfvxeumzz52ljxfzpme - tests/test_payloads.py: bafybeiaq2dxpbein6qhipalibi57x6niiydxi6kvbpeqripzlngcgpb3qq - tests/test_rounds.py: bafybeih27bkijv6vcqfdbrfxgbtajtqbquekknc77omkxsfgnboiduj7sm + tests/test_payloads.py: bafybeidtkbudsmrx77xs2oooji6cpj7kx7nembf77u32if2sb2kvkavvhq + tests/test_rounds.py: bafybeidge7sp3udobhvjpbdj5kirkukd6w5qycqkcuc6sy3qqiedco2q4i fingerprint_ignore_patterns: [] connections: [] contracts: @@ -137,6 +137,7 @@ models: mech_activity_checker_contract: '0x0000000000000000000000000000000000000000' staking_contract_address: '0x2Ef503950Be67a98746F484DA0bBAdA339DF3326' staking_interaction_sleep_time: 5 + store_path: data class_name: StakingParams requests: args: {} diff --git a/packages/valory/skills/staking_abci/tests/test_payloads.py b/packages/valory/skills/staking_abci/tests/test_payloads.py index 5c00765ee..f9a7b316b 100644 --- a/packages/valory/skills/staking_abci/tests/test_payloads.py +++ b/packages/valory/skills/staking_abci/tests/test_payloads.py @@ -41,6 +41,8 @@ "service_staking_state": 1, "tx_submitter": "dummy tx submitter", "tx_hash": "dummy tx hash", + "ts_checkpoint": 1, + "is_checkpoint_reached": True, }, ), ], diff --git a/packages/valory/skills/staking_abci/tests/test_rounds.py b/packages/valory/skills/staking_abci/tests/test_rounds.py index 9703a46df..2a0564afd 100644 --- a/packages/valory/skills/staking_abci/tests/test_rounds.py +++ b/packages/valory/skills/staking_abci/tests/test_rounds.py @@ -64,6 +64,8 @@ def abci_app() -> StakingAbciApp: "service_staking_state": StakingState.UNSTAKED.value, "tx_submitter": "dummy_submitter", "tx_hash": "dummy_tx_hash", + "ts_checkpoint": 0, + "is_checkpoint_reached": True, } @@ -148,12 +150,16 @@ class TestCallCheckpointRound(BaseStakingRoundTestClass): "service_staking_state": StakingState.STAKED.value, "tx_submitter": "dummy_submitter", "tx_hash": "dummy_tx_hash", + "ts_checkpoint": 0, + "is_checkpoint_reached": True, } ), final_data={ "service_staking_state": StakingState.STAKED.value, "tx_submitter": "dummy_submitter", "tx_hash": "dummy_tx_hash", + "ts_checkpoint": 0, + "is_checkpoint_reached": True, }, event=Event.DONE, most_voted_payload=DUMMY_SERVICE_STATE["tx_submitter"], @@ -184,6 +190,8 @@ class TestCallCheckpointRound(BaseStakingRoundTestClass): "service_staking_state": StakingState.EVICTED.value, "tx_submitter": "dummy_submitter", "tx_hash": "dummy_tx_hash", + "ts_checkpoint": 0, + "is_checkpoint_reached": True, } ), final_data={}, @@ -202,6 +210,8 @@ class TestCallCheckpointRound(BaseStakingRoundTestClass): "service_staking_state": StakingState.STAKED.value, "tx_submitter": "dummy_submitter", "tx_hash": None, + "ts_checkpoint": 0, + "is_checkpoint_reached": True, } ), final_data={}, @@ -279,6 +289,8 @@ def test_staking_abci_app_initialization(abci_app: StakingAbciApp) -> None: get_name(SynchronizedData.tx_submitter), get_name(SynchronizedData.most_voted_tx_hash), get_name(SynchronizedData.service_staking_state), + get_name(SynchronizedData.previous_checkpoint), + get_name(SynchronizedData.is_checkpoint_reached), }, FinishedStakingRound: { get_name(SynchronizedData.service_staking_state), diff --git a/packages/valory/skills/trader_abci/skill.yaml b/packages/valory/skills/trader_abci/skill.yaml index 2a005c7d4..96451c908 100644 --- a/packages/valory/skills/trader_abci/skill.yaml +++ b/packages/valory/skills/trader_abci/skill.yaml @@ -26,11 +26,11 @@ skills: - valory/reset_pause_abci:0.1.0:bafybeigrdlxed3xlsnxtjhnsbl3cojruihxcqx4jxhgivkd5i2fkjncgba - valory/transaction_settlement_abci:0.1.0:bafybeic7q7recyka272udwcupblwbkc3jkodgp74fvcdxb7urametg5dae - valory/termination_abci:0.1.0:bafybeib5l7jhew5ic6iq24dd23nidcoimzqkrk556gqywhoziatj33zvwm -- valory/market_manager_abci:0.1.0:bafybeicztk62pslofv6ui3aw3giw2tnvlfwfmatqbyvvzv4ampneu6isqa -- valory/decision_maker_abci:0.1.0:bafybeig3sqaeqedobqdg7gynrxnbq2kgzh4gp5pe5gxo5kw4hczfjmj6e4 -- valory/tx_settlement_multiplexer_abci:0.1.0:bafybeibx63ico4nlp6etvtzgvlcrl3jdy6rx7zodwmxhvvb4phizd732l4 -- valory/staking_abci:0.1.0:bafybeictd5pxhscuhqntvctb7l5lfjausxt2m22rg5mkaiuj4cwwcxpvne -- valory/check_stop_trading_abci:0.1.0:bafybeib75qrimmvensqmskdp5kzki5ijjwolqk2ojekeommakaf64mzn54 +- valory/market_manager_abci:0.1.0:bafybeiaru2d32wpmcgqs64eepxud4idgubc3vmsbdwbia7gygipql2mmqi +- valory/decision_maker_abci:0.1.0:bafybeiddnmcquiuznts67ridhpnaqw2y3rrt4nfau5kjm74zhk5lhjxi2q +- valory/tx_settlement_multiplexer_abci:0.1.0:bafybeietwknem7iiood6pwkfup322ywwjmdrmdapllrcms6jpeev5w2qfe +- valory/staking_abci:0.1.0:bafybeicupccurmrg7qesivonlyt3nryarsmk5qf5yh6auno64wn45bybvq +- valory/check_stop_trading_abci:0.1.0:bafybeieduekpd4zbvjztyxyooppqnmjvup6jfp74uo6hhupvtvzzscdzkq - valory/mech_interact_abci:0.1.0:bafybeid6m3i5ofq7vuogqapdnoshhq7mswmudhvfcr2craw25fdwtoe3lm behaviours: main: diff --git a/packages/valory/skills/tx_settlement_multiplexer_abci/skill.yaml b/packages/valory/skills/tx_settlement_multiplexer_abci/skill.yaml index 3f2dfba78..367792b9d 100644 --- a/packages/valory/skills/tx_settlement_multiplexer_abci/skill.yaml +++ b/packages/valory/skills/tx_settlement_multiplexer_abci/skill.yaml @@ -23,8 +23,8 @@ protocols: - valory/ledger_api:1.0.0:bafybeihdk6psr4guxmbcrc26jr2cbgzpd5aljkqvpwo64bvaz7tdti2oni skills: - valory/abstract_round_abci:0.1.0:bafybeib733xfbndtpvkf44mtk7oyodnficgloo6xhn7xmqxxeos33es65u -- valory/decision_maker_abci:0.1.0:bafybeig3sqaeqedobqdg7gynrxnbq2kgzh4gp5pe5gxo5kw4hczfjmj6e4 -- valory/staking_abci:0.1.0:bafybeictd5pxhscuhqntvctb7l5lfjausxt2m22rg5mkaiuj4cwwcxpvne +- valory/decision_maker_abci:0.1.0:bafybeiddnmcquiuznts67ridhpnaqw2y3rrt4nfau5kjm74zhk5lhjxi2q +- valory/staking_abci:0.1.0:bafybeicupccurmrg7qesivonlyt3nryarsmk5qf5yh6auno64wn45bybvq - valory/mech_interact_abci:0.1.0:bafybeid6m3i5ofq7vuogqapdnoshhq7mswmudhvfcr2craw25fdwtoe3lm behaviours: main: diff --git a/tox.ini b/tox.ini index aba6913e7..e2f88bd15 100644 --- a/tox.ini +++ b/tox.ini @@ -640,4 +640,6 @@ pathable: ==0.4.3 pyinstaller: ==6.8.0 pyinstaller-hooks-contrib: >=2024.6 ; sub-dep of aiohttp, has PSF-2.0 License https://github.com/aio-libs/aiohappyeyeballs/blob/main/LICENSE -aiohappyeyeballs: >=2.3.4,<3.0.0 \ No newline at end of file +aiohappyeyeballs: >=2.3.4,<3.0.0 +; licence is MIT, but the tool does not detect it +attrs: ==24.3.0