From efcd4131278289bc1198754375901017828256ce Mon Sep 17 00:00:00 2001 From: 8baller Date: Mon, 18 Mar 2024 09:01:05 +0000 Subject: [PATCH 1/2] feat loads of changes --- lyra/async_client.py | 154 ++++ lyra/autonomy/packages/__init__.py | 0 .../eightballer/protocols/markets/__init__.py | 29 + .../protocols/markets/custom_types.py | 212 ++++++ .../protocols/markets/dialogues.py | 153 ++++ .../protocols/markets/markets.proto | 94 +++ .../protocols/markets/markets_pb2.py | 201 +++++ .../eightballer/protocols/markets/message.py | 335 +++++++++ .../protocols/markets/protocol.yaml | 21 + .../protocols/markets/serialization.py | 164 ++++ .../markets/tests/test_markets_dialogues.py | 49 ++ .../markets/tests/test_markets_messages.py | 227 ++++++ lyra/autonomy/packages/packages.json | 6 + lyra/base_client.py | 706 ++++++++++++++++++ lyra/constants.py | 1 + lyra/http_client.py | 27 + lyra/lyra.py | 680 +---------------- lyra/utils.py | 4 + lyra/ws_client.py | 32 + tests/conftest.py | 33 + tests/test_main.py | 23 +- tests/test_rfq.py | 57 ++ 22 files changed, 2517 insertions(+), 691 deletions(-) create mode 100644 lyra/async_client.py create mode 100644 lyra/autonomy/packages/__init__.py create mode 100644 lyra/autonomy/packages/eightballer/protocols/markets/__init__.py create mode 100644 lyra/autonomy/packages/eightballer/protocols/markets/custom_types.py create mode 100644 lyra/autonomy/packages/eightballer/protocols/markets/dialogues.py create mode 100644 lyra/autonomy/packages/eightballer/protocols/markets/markets.proto create mode 100644 lyra/autonomy/packages/eightballer/protocols/markets/markets_pb2.py create mode 100644 lyra/autonomy/packages/eightballer/protocols/markets/message.py create mode 100644 lyra/autonomy/packages/eightballer/protocols/markets/protocol.yaml create mode 100644 lyra/autonomy/packages/eightballer/protocols/markets/serialization.py create mode 100644 lyra/autonomy/packages/eightballer/protocols/markets/tests/test_markets_dialogues.py create mode 100644 lyra/autonomy/packages/eightballer/protocols/markets/tests/test_markets_messages.py create mode 100644 lyra/autonomy/packages/packages.json create mode 100644 lyra/base_client.py create mode 100644 lyra/http_client.py create mode 100644 lyra/ws_client.py create mode 100644 tests/conftest.py create mode 100644 tests/test_rfq.py diff --git a/lyra/async_client.py b/lyra/async_client.py new file mode 100644 index 0000000..ce5dfe2 --- /dev/null +++ b/lyra/async_client.py @@ -0,0 +1,154 @@ +""" +Async client for Lyra +""" + +import asyncio +import json +import sys +import time +import traceback + +from lyra.constants import PUBLIC_HEADERS +from lyra.enums import InstrumentType, UnderlyingCurrency +from lyra.ws_client import WsClient as BaseClient + +from multiprocessing import Process +# we need a thread safe way to collect the events +from multiprocessing import Lock +from threading import Thread + + +class AsyncClient(BaseClient): + """ + We use the async client to make async requests to the lyra API + We us the ws client to make async requests to the lyra ws API + """ + + current_subscriptions = {} + + listener = None + subscribing = False + + + + async def fetch_ticker(self, instrument_name: str): + """ + Fetch the ticker for a symbol + """ + id = str(int(time.time())) + payload = {"instrument_name": instrument_name} + self.ws.send(json.dumps({"method": "public/get_ticker", "params": payload, "id": id})) + + # we now wait for the response + while True: + response = self.ws.recv() + response = json.loads(response) + if response["id"] == id: + close = float(response["result"]["best_bid_price"]) + float(response["result"]["best_ask_price"]) / 2 + response["result"]["close"] = close + return response["result"] + + + + + async def subscribe(self, instrument_name: str, group: str = "1", depth: str = "100"): + """ + Subscribe to the order book for a symbol + """ + + self.subscribing = True + if instrument_name not in self.current_subscriptions: + channel = f"orderbook.{instrument_name}.{group}.{depth}" + msg = json.dumps({ + "method": "subscribe", + "params": { + "channels": [channel] + } + + }) + print(f"Subscribing with {msg}") + self.ws.send(msg) + await self.collect_events(instrument_name=instrument_name) + print(f"Subscribed to {instrument_name}") + return + + while instrument_name not in self.current_subscriptions: + await asyncio.sleep(1) + return self.current_subscriptions[instrument_name] + + + + async def collect_events(self, subscription: str = None, instrument_name: str = None): + """Use a thread to check the subscriptions""" + try: + response = self.ws.recv() + response = json.loads(response) + if "error" in response: + print(response) + raise Exception(response["error"]) + if "result" in response: + result = response["result"] + if "status" in result: + print(f"Succesfully subscribed to {result['status']}") + for channel, value in result['status'].items(): + print(f"Channel {channel} has value {value}") + if "error" in value: + raise Exception(value["error"]) + self.subscribing = False + return + + + channel = response["params"]["channel"] + + bids = response['params']['data']['bids'] + asks = response['params']['data']['asks'] + + bids = list(map(lambda x: (float(x[0]), float(x[1])), bids)) + asks = list(map(lambda x: (float(x[0]), float(x[1])), asks)) + + if instrument_name in self.current_subscriptions: + old_params = self.current_subscriptions[instrument_name] + _asks, _bids = old_params["asks"], old_params["bids"] + if not asks: + asks = _asks + if not bids: + bids = _bids + self.current_subscriptions[instrument_name] = {"asks": asks, "bids": bids} + return self.current_subscriptions[instrument_name] + except Exception as e: + print(f"Error: {e}") + print(traceback.print_exc()) + sys.exit(1) + + async def watch_order_book(self, instrument_name: str, group: str = "1", depth: str = "100"): + """ + Watch the order book for a symbol + orderbook.{instrument_name}.{group}.{depth} + """ + + if not self.subscribing: + await self.subscribe(instrument_name, group, depth) + + + if not self.listener: + print(f"Started listener for {instrument_name}") + self.listener = True + + await self.collect_events(instrument_name=instrument_name) + while instrument_name not in self.current_subscriptions: + await asyncio.sleep(1) + print(f"Waiting for {instrument_name} to be in current subscriptions") + + return self.current_subscriptions[instrument_name] + + + async def fetch_instruments(self, expired=False, instrument_type: InstrumentType = InstrumentType.PERP, currency: UnderlyingCurrency = UnderlyingCurrency.BTC): + return super().fetch_instruments(expired, instrument_type, currency) + + async def close(self): + """ + Close the connection + """ + self.ws.close() + # if self.listener: + # self.listener.join() \ No newline at end of file diff --git a/lyra/autonomy/packages/__init__.py b/lyra/autonomy/packages/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lyra/autonomy/packages/eightballer/protocols/markets/__init__.py b/lyra/autonomy/packages/eightballer/protocols/markets/__init__.py new file mode 100644 index 0000000..33f36fd --- /dev/null +++ b/lyra/autonomy/packages/eightballer/protocols/markets/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023 eightballer +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +""" +This module contains the support resources for the markets protocol. + +It was created with protocol buffer compiler version `libprotoc 3.19.4` and aea protocol generator version `1.0.0`. +""" + +from packages.eightballer.protocols.markets.message import MarketsMessage +from packages.eightballer.protocols.markets.serialization import MarketsSerializer + +MarketsMessage.serializer = MarketsSerializer diff --git a/lyra/autonomy/packages/eightballer/protocols/markets/custom_types.py b/lyra/autonomy/packages/eightballer/protocols/markets/custom_types.py new file mode 100644 index 0000000..a6d7527 --- /dev/null +++ b/lyra/autonomy/packages/eightballer/protocols/markets/custom_types.py @@ -0,0 +1,212 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023 eightballer +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains class representations corresponding to every custom type in the protocol specification.""" + + +from dataclasses import dataclass +from enum import Enum +from typing import Any, Dict, List, Optional + + +class ErrorCode(Enum): + """This class represents an instance of ErrorCode.""" + + UNSUPPORTED_PROTOCOL = 0 + DECODING_ERROR = 1 + INVALID_MESSAGE = 2 + UNSUPPORTED_SKILL = 3 + INVALID_DIALOGUE = 4 + + @staticmethod + def encode(error_code_protobuf_object: Any, error_code_object: "ErrorCode") -> None: + """ + Encode an instance of this class into the protocol buffer object. + The protocol buffer object in the error_code_protobuf_object argument is matched with the instance of this class in the 'error_code_object' argument. + :param error_code_protobuf_object: the protocol buffer object whose type corresponds with this class. + :param error_code_object: an instance of this class to be encoded in the protocol buffer object. + """ + error_code_protobuf_object.error_code = error_code_object.value + + @classmethod + def decode(cls, error_code_protobuf_object: Any) -> "ErrorCode": + """ + Decode a protocol buffer object that corresponds with this class into an instance of this class. + A new instance of this class is created that matches the protocol buffer object in the 'error_code_protobuf_object' argument. + :param error_code_protobuf_object: the protocol buffer object whose type corresponds with this class. + :return: A new instance of this class that matches the protocol buffer object in the 'error_code_protobuf_object' argument. + """ + enum_value_from_pb2 = error_code_protobuf_object.error_code + return ErrorCode(enum_value_from_pb2) + + +@dataclass +class Market: + """ + This class represents an instance of Market. + """ + + id: str + lowercaseId: Optional[str] = None + symbol: Optional[str] = None + base: Optional[str] = None + quote: Optional[str] = None + settle: Optional[str] = None + baseId: Optional[str] = None + quoteId: Optional[str] = None + settleId: Optional[str] = None + type: Optional[str] = None + spot: Optional[bool] = None + margin: Optional[bool] = None + swap: Optional[bool] = None + future: Optional[bool] = None + option: Optional[bool] = None + active: Optional[bool] = None + contract: Optional[bool] = None + linear: Optional[bool] = None + inverse: Optional[bool] = None + taker: Optional[float] = None + maker: Optional[float] = None + contractSize: Optional[float] = None + expiry: Optional[float] = None + expiryDatetime: Optional[str] = None + strike: Optional[float] = None + optionType: Optional[str] = None + precision: Optional[float] = None + limits: Optional[str] = None + info: Optional[Dict[str, Any]] = None + exchange_id: Optional[str] = None + created: Optional[str] = None + + @staticmethod + def encode(market_protobuf_object, market_object: "Market") -> None: + """ + Encode an instance of this class into the protocol buffer object. + + The protocol buffer object in the market_protobuf_object argument is matched with the instance of this class in the 'market_object' argument. + + :param market_protobuf_object: the protocol buffer object whose type corresponds with this class. + :param market_object: an instance of this class to be encoded in the protocol buffer object. + """ + for ( + attribute + ) in Market.__dataclass_fields__.keys(): # pylint: disable=no-member + if hasattr(market_object, attribute): + value = getattr(market_object, attribute) + setattr(market_protobuf_object.Market, attribute, value) + else: + setattr(market_protobuf_object.Market, attribute, None) + + @classmethod + def decode(cls, market_protobuf_object) -> "Market": + """ + Decode a protocol buffer object that corresponds with this class into an instance of this class. + + A new instance of this class is created that matches the protocol buffer object in the 'market_protobuf_object' argument. + + :param market_protobuf_object: the protocol buffer object whose type corresponds with this class. + :return: A new instance of this class that matches the protocol buffer object in the 'market_protobuf_object' argument. + """ + attribute_dict = dict() + for ( + attribute + ) in Market.__dataclass_fields__.keys(): # pylint: disable=no-member + if hasattr(market_protobuf_object.Market, attribute): + if getattr(market_protobuf_object.Market, attribute) is not None: + attribute_dict[attribute] = getattr( + market_protobuf_object.Market, attribute + ) + return cls(**attribute_dict) + + def __eq__(self, other): + if isinstance(other, Market): + set_of_self_attributue = set( + i + for i in Market.__dataclass_fields__.keys() # pylint: disable=no-member + if getattr(self, i) is not None + ) + set_of_other_attributue = set( + i + for i in Market.__dataclass_fields__.keys() # pylint: disable=no-member + if getattr(other, i) is not None + ) + return set_of_self_attributue == set_of_other_attributue + return False + + def to_json(self): + """TO a pretty dictionary string.""" + result = {} + for ( + attribute + ) in Market.__dataclass_fields__.keys(): # pylint: disable=no-member + if hasattr(self, attribute): + value = getattr(self, attribute) + if value is not None: + result[attribute] = value + return result + + +@dataclass +class Markets: + """This class represents an instance of Markets.""" + + markets: List[Market] + + @staticmethod + def encode(markets_protobuf_object, markets_object: "Markets") -> None: + """ + Encode an instance of this class into the protocol buffer object. + + The protocol buffer object in the markets_protobuf_object argument is matched with the instance of this class in the 'markets_object' argument. + + :param markets_protobuf_object: the protocol buffer object whose type corresponds with this class. + :param markets_object: an instance of this class to be encoded in the protocol buffer object. + """ + if markets_protobuf_object is None: + raise ValueError( + "The protocol buffer object 'markets_protobuf_object' is not initialized." + ) + markets_protobuf_object.Markets.markets = markets_object.markets + + @classmethod + def decode(cls, markets_protobuf_object) -> "Markets": + """ + Decode a protocol buffer object that corresponds with this class into an instance of this class. + + A new instance of this class is created that matches the protocol buffer object in the 'markets_protobuf_object' argument. + + :param markets_protobuf_object: the protocol buffer object whose type corresponds with this class. + :return: A new instance of this class that matches the protocol buffer object in the 'markets_protobuf_object' argument. + """ + return cls(markets_protobuf_object.Markets.markets) + + def __eq__(self, other): + if isinstance(other, Markets): + set_of_self_attributue = set( + i + for i in Markets.__dataclass_fields__.keys() # pylint: disable=no-member + if getattr(self, i) is not None + ) + set_of_other_attributue = set( + i + for i in Markets.__dataclass_fields__.keys() # pylint: disable=no-member + if getattr(other, i) is not None + ) + return set_of_self_attributue == set_of_other_attributue + return False diff --git a/lyra/autonomy/packages/eightballer/protocols/markets/dialogues.py b/lyra/autonomy/packages/eightballer/protocols/markets/dialogues.py new file mode 100644 index 0000000..327f779 --- /dev/null +++ b/lyra/autonomy/packages/eightballer/protocols/markets/dialogues.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023 eightballer +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +""" +This module contains the classes required for markets dialogue management. + +- MarketsDialogue: The dialogue class maintains state of a dialogue and manages it. +- MarketsDialogues: The dialogues class keeps track of all dialogues. +""" + +from abc import ABC +from typing import Callable, Dict, FrozenSet, Optional, Type, cast + +from aea.common import Address +from aea.protocols.base import Message +from aea.protocols.dialogue.base import Dialogue, DialogueLabel, Dialogues +from aea.skills.base import Model + +from packages.eightballer.protocols.markets.message import MarketsMessage + + +class MarketsDialogue(Dialogue): + """The markets dialogue class maintains state of a dialogue and manages it.""" + + INITIAL_PERFORMATIVES: FrozenSet[Message.Performative] = frozenset( + {MarketsMessage.Performative.GET_MARKET, + MarketsMessage.Performative.GET_ALL_MARKETS} + ) + TERMINAL_PERFORMATIVES: FrozenSet[Message.Performative] = frozenset( + { + MarketsMessage.Performative.ALL_MARKETS, + MarketsMessage.Performative.MARKET, + MarketsMessage.Performative.ERROR, + } + ) + VALID_REPLIES: Dict[Message.Performative, FrozenSet[Message.Performative]] = { + MarketsMessage.Performative.ALL_MARKETS: frozenset(), + MarketsMessage.Performative.ERROR: frozenset(), + MarketsMessage.Performative.GET_ALL_MARKETS: frozenset( + {MarketsMessage.Performative.ALL_MARKETS, MarketsMessage.Performative.ERROR} + ), + MarketsMessage.Performative.GET_MARKET: frozenset( + {MarketsMessage.Performative.MARKET, MarketsMessage.Performative.ERROR} + ), + MarketsMessage.Performative.MARKET: frozenset(), + } + + class Role(Dialogue.Role): + """This class defines the agent's role in a markets dialogue.""" + + AGENT = "agent" + + class EndState(Dialogue.EndState): + """This class defines the end states of a markets dialogue.""" + + MARKET = 0 + ALL_MARKETS = 1 + ERROR = 2 + + def __init__( + self, + dialogue_label: DialogueLabel, + self_address: Address, + role: Dialogue.Role, + message_class: Type[MarketsMessage] = MarketsMessage, + ) -> None: + """ + Initialize a dialogue. + + :param dialogue_label: the identifier of the dialogue + :param self_address: the address of the entity for whom this dialogue is maintained + :param role: the role of the agent this dialogue is maintained for + :param message_class: the message class used + """ + Dialogue.__init__( + self, + dialogue_label=dialogue_label, + message_class=message_class, + self_address=self_address, + role=role, + ) + + +class BaseMarketsDialogues(Dialogues, ABC): + """This class keeps track of all markets dialogues.""" + + END_STATES = frozenset( + { + MarketsDialogue.EndState.MARKET, + MarketsDialogue.EndState.ALL_MARKETS, + MarketsDialogue.EndState.ERROR, + } + ) + + _keep_terminal_state_dialogues = False + + def __init__( + self, + self_address: Address, + role_from_first_message: Optional[ + Callable[[Message, Address], Dialogue.Role] + ] = None, + dialogue_class: Type[MarketsDialogue] = MarketsDialogue, + ) -> None: + """ + Initialize dialogues. + + :param self_address: the address of the entity for whom dialogues are maintained + :param dialogue_class: the dialogue class used + :param role_from_first_message: the callable determining role from first message + """ + del role_from_first_message + + def _role_from_first_message( + message: Message, sender: Address + ) -> Dialogue.Role: # pylint: + """Infer the role of the agent from an incoming/outgoing first message.""" + del sender, message + return MarketsDialogue.Role.AGENT + + Dialogues.__init__( + self, + self_address=self_address, + end_states=cast(FrozenSet[Dialogue.EndState], self.END_STATES), + message_class=MarketsMessage, + dialogue_class=dialogue_class, + role_from_first_message=_role_from_first_message, + ) + + +class MarketsDialogues(BaseMarketsDialogues, Model): + """Dialogue class for Markets.""" + + def __init__(self, **kwargs): + """Initialize the Dialogue.""" + Model.__init__(self, keep_terminal_state_dialogues=False, **kwargs) + BaseMarketsDialogues.__init__(self, self_address=str(self.context.skill_id)) diff --git a/lyra/autonomy/packages/eightballer/protocols/markets/markets.proto b/lyra/autonomy/packages/eightballer/protocols/markets/markets.proto new file mode 100644 index 0000000..9f9aa86 --- /dev/null +++ b/lyra/autonomy/packages/eightballer/protocols/markets/markets.proto @@ -0,0 +1,94 @@ +syntax = "proto3"; + +package aea.eightballer.markets.v0_1_0; + +message MarketsMessage{ + + // Custom Types + message ErrorCode{ + enum ErrorCodeEnum { + UNSUPPORTED_PROTOCOL = 0; + DECODING_ERROR = 1; + INVALID_MESSAGE = 2; + UNSUPPORTED_SKILL = 3; + INVALID_DIALOGUE = 4; + } + ErrorCodeEnum error_code = 1; + } + + message Market{ + message Market { + string id = 1; + string lowercaseId = 2; + string symbol = 3; + string base = 4; + string quote = 5; + string settle = 6; + string baseId = 7; + string quoteId = 8; + string settleId = 9; + string type = 10; + bool spot = 11; + bool margin = 12; + bool swap = 13; + bool future = 14; + bool option = 15; + bool active = 16; + bool contract = 17; + bool linear = 18; + bool inverse = 19; + float taker = 20; + float maker = 21; + float contractSize = 22; + float expiry = 23; + string expiryDatetime = 24; + float strike = 25; + string optionType = 26; + float precision = 27; + string limits = 28; + string info = 29; + } + } + + message Markets{ + message Markets { + repeated Market markets = 1; + } + } + + + // Performatives and contents + message Get_All_Markets_Performative{ + string exchange_id = 1; + string currency = 2; + bool currency_is_set = 3; + } + + message Get_Market_Performative{ + string id = 1; + string exchange_id = 2; + } + + message All_Markets_Performative{ + Markets markets = 1; + } + + message Market_Performative{ + Market market = 1; + } + + message Error_Performative{ + ErrorCode error_code = 1; + string error_msg = 2; + map error_data = 3; + } + + + oneof performative{ + All_Markets_Performative all_markets = 5; + Error_Performative error = 6; + Get_All_Markets_Performative get_all_markets = 7; + Get_Market_Performative get_market = 8; + Market_Performative market = 9; + } +} diff --git a/lyra/autonomy/packages/eightballer/protocols/markets/markets_pb2.py b/lyra/autonomy/packages/eightballer/protocols/markets/markets_pb2.py new file mode 100644 index 0000000..e1759c1 --- /dev/null +++ b/lyra/autonomy/packages/eightballer/protocols/markets/markets_pb2.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database + +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( + b'\n\rmarkets.proto\x12\x1e\x61\x65\x61.eightballer.markets.v0_1_0"\x9d\x0f\n\x0eMarketsMessage\x12^\n\x0b\x61ll_markets\x18\x05 \x01(\x0b\x32G.aea.eightballer.markets.v0_1_0.MarketsMessage.All_Markets_PerformativeH\x00\x12R\n\x05\x65rror\x18\x06 \x01(\x0b\x32\x41.aea.eightballer.markets.v0_1_0.MarketsMessage.Error_PerformativeH\x00\x12\x66\n\x0fget_all_markets\x18\x07 \x01(\x0b\x32K.aea.eightballer.markets.v0_1_0.MarketsMessage.Get_All_Markets_PerformativeH\x00\x12\\\n\nget_market\x18\x08 \x01(\x0b\x32\x46.aea.eightballer.markets.v0_1_0.MarketsMessage.Get_Market_PerformativeH\x00\x12T\n\x06market\x18\t \x01(\x0b\x32\x42.aea.eightballer.markets.v0_1_0.MarketsMessage.Market_PerformativeH\x00\x1a\xe8\x01\n\tErrorCode\x12Z\n\nerror_code\x18\x01 \x01(\x0e\x32\x46.aea.eightballer.markets.v0_1_0.MarketsMessage.ErrorCode.ErrorCodeEnum"\x7f\n\rErrorCodeEnum\x12\x18\n\x14UNSUPPORTED_PROTOCOL\x10\x00\x12\x12\n\x0e\x44\x45\x43ODING_ERROR\x10\x01\x12\x13\n\x0fINVALID_MESSAGE\x10\x02\x12\x15\n\x11UNSUPPORTED_SKILL\x10\x03\x12\x14\n\x10INVALID_DIALOGUE\x10\x04\x1a\xf2\x03\n\x06Market\x1a\xe7\x03\n\x06Market\x12\n\n\x02id\x18\x01 \x01(\t\x12\x13\n\x0blowercaseId\x18\x02 \x01(\t\x12\x0e\n\x06symbol\x18\x03 \x01(\t\x12\x0c\n\x04\x62\x61se\x18\x04 \x01(\t\x12\r\n\x05quote\x18\x05 \x01(\t\x12\x0e\n\x06settle\x18\x06 \x01(\t\x12\x0e\n\x06\x62\x61seId\x18\x07 \x01(\t\x12\x0f\n\x07quoteId\x18\x08 \x01(\t\x12\x10\n\x08settleId\x18\t \x01(\t\x12\x0c\n\x04type\x18\n \x01(\t\x12\x0c\n\x04spot\x18\x0b \x01(\x08\x12\x0e\n\x06margin\x18\x0c \x01(\x08\x12\x0c\n\x04swap\x18\r \x01(\x08\x12\x0e\n\x06\x66uture\x18\x0e \x01(\x08\x12\x0e\n\x06option\x18\x0f \x01(\x08\x12\x0e\n\x06\x61\x63tive\x18\x10 \x01(\x08\x12\x10\n\x08\x63ontract\x18\x11 \x01(\x08\x12\x0e\n\x06linear\x18\x12 \x01(\x08\x12\x0f\n\x07inverse\x18\x13 \x01(\x08\x12\r\n\x05taker\x18\x14 \x01(\x02\x12\r\n\x05maker\x18\x15 \x01(\x02\x12\x14\n\x0c\x63ontractSize\x18\x16 \x01(\x02\x12\x0e\n\x06\x65xpiry\x18\x17 \x01(\x02\x12\x16\n\x0e\x65xpiryDatetime\x18\x18 \x01(\t\x12\x0e\n\x06strike\x18\x19 \x01(\x02\x12\x12\n\noptionType\x18\x1a \x01(\t\x12\x11\n\tprecision\x18\x1b \x01(\x02\x12\x0e\n\x06limits\x18\x1c \x01(\t\x12\x0c\n\x04info\x18\x1d \x01(\t\x1a\\\n\x07Markets\x1aQ\n\x07Markets\x12\x46\n\x07markets\x18\x01 \x03(\x0b\x32\x35.aea.eightballer.markets.v0_1_0.MarketsMessage.Market\x1a^\n\x1cGet_All_Markets_Performative\x12\x13\n\x0b\x65xchange_id\x18\x01 \x01(\t\x12\x10\n\x08\x63urrency\x18\x02 \x01(\t\x12\x17\n\x0f\x63urrency_is_set\x18\x03 \x01(\x08\x1a:\n\x17Get_Market_Performative\x12\n\n\x02id\x18\x01 \x01(\t\x12\x13\n\x0b\x65xchange_id\x18\x02 \x01(\t\x1a\x63\n\x18\x41ll_Markets_Performative\x12G\n\x07markets\x18\x01 \x01(\x0b\x32\x36.aea.eightballer.markets.v0_1_0.MarketsMessage.Markets\x1a\\\n\x13Market_Performative\x12\x45\n\x06market\x18\x01 \x01(\x0b\x32\x35.aea.eightballer.markets.v0_1_0.MarketsMessage.Market\x1a\x8d\x02\n\x12\x45rror_Performative\x12L\n\nerror_code\x18\x01 \x01(\x0b\x32\x38.aea.eightballer.markets.v0_1_0.MarketsMessage.ErrorCode\x12\x11\n\terror_msg\x18\x02 \x01(\t\x12\x64\n\nerror_data\x18\x03 \x03(\x0b\x32P.aea.eightballer.markets.v0_1_0.MarketsMessage.Error_Performative.ErrorDataEntry\x1a\x30\n\x0e\x45rrorDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01\x42\x0e\n\x0cperformativeb\x06proto3' +) + + +_MARKETSMESSAGE = DESCRIPTOR.message_types_by_name["MarketsMessage"] +_MARKETSMESSAGE_ERRORCODE = _MARKETSMESSAGE.nested_types_by_name["ErrorCode"] +_MARKETSMESSAGE_MARKET = _MARKETSMESSAGE.nested_types_by_name["Market"] +_MARKETSMESSAGE_MARKET_MARKET = _MARKETSMESSAGE_MARKET.nested_types_by_name["Market"] +_MARKETSMESSAGE_MARKETS = _MARKETSMESSAGE.nested_types_by_name["Markets"] +_MARKETSMESSAGE_MARKETS_MARKETS = _MARKETSMESSAGE_MARKETS.nested_types_by_name[ + "Markets" +] +_MARKETSMESSAGE_GET_ALL_MARKETS_PERFORMATIVE = _MARKETSMESSAGE.nested_types_by_name[ + "Get_All_Markets_Performative" +] +_MARKETSMESSAGE_GET_MARKET_PERFORMATIVE = _MARKETSMESSAGE.nested_types_by_name[ + "Get_Market_Performative" +] +_MARKETSMESSAGE_ALL_MARKETS_PERFORMATIVE = _MARKETSMESSAGE.nested_types_by_name[ + "All_Markets_Performative" +] +_MARKETSMESSAGE_MARKET_PERFORMATIVE = _MARKETSMESSAGE.nested_types_by_name[ + "Market_Performative" +] +_MARKETSMESSAGE_ERROR_PERFORMATIVE = _MARKETSMESSAGE.nested_types_by_name[ + "Error_Performative" +] +_MARKETSMESSAGE_ERROR_PERFORMATIVE_ERRORDATAENTRY = ( + _MARKETSMESSAGE_ERROR_PERFORMATIVE.nested_types_by_name["ErrorDataEntry"] +) +_MARKETSMESSAGE_ERRORCODE_ERRORCODEENUM = _MARKETSMESSAGE_ERRORCODE.enum_types_by_name[ + "ErrorCodeEnum" +] +MarketsMessage = _reflection.GeneratedProtocolMessageType( + "MarketsMessage", + (_message.Message,), + { + "ErrorCode": _reflection.GeneratedProtocolMessageType( + "ErrorCode", + (_message.Message,), + { + "DESCRIPTOR": _MARKETSMESSAGE_ERRORCODE, + "__module__": "markets_pb2" + # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.ErrorCode) + }, + ), + "Market": _reflection.GeneratedProtocolMessageType( + "Market", + (_message.Message,), + { + "Market": _reflection.GeneratedProtocolMessageType( + "Market", + (_message.Message,), + { + "DESCRIPTOR": _MARKETSMESSAGE_MARKET_MARKET, + "__module__": "markets_pb2" + # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.Market.Market) + }, + ), + "DESCRIPTOR": _MARKETSMESSAGE_MARKET, + "__module__": "markets_pb2" + # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.Market) + }, + ), + "Markets": _reflection.GeneratedProtocolMessageType( + "Markets", + (_message.Message,), + { + "Markets": _reflection.GeneratedProtocolMessageType( + "Markets", + (_message.Message,), + { + "DESCRIPTOR": _MARKETSMESSAGE_MARKETS_MARKETS, + "__module__": "markets_pb2" + # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.Markets.Markets) + }, + ), + "DESCRIPTOR": _MARKETSMESSAGE_MARKETS, + "__module__": "markets_pb2" + # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.Markets) + }, + ), + "Get_All_Markets_Performative": _reflection.GeneratedProtocolMessageType( + "Get_All_Markets_Performative", + (_message.Message,), + { + "DESCRIPTOR": _MARKETSMESSAGE_GET_ALL_MARKETS_PERFORMATIVE, + "__module__": "markets_pb2" + # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.Get_All_Markets_Performative) + }, + ), + "Get_Market_Performative": _reflection.GeneratedProtocolMessageType( + "Get_Market_Performative", + (_message.Message,), + { + "DESCRIPTOR": _MARKETSMESSAGE_GET_MARKET_PERFORMATIVE, + "__module__": "markets_pb2" + # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.Get_Market_Performative) + }, + ), + "All_Markets_Performative": _reflection.GeneratedProtocolMessageType( + "All_Markets_Performative", + (_message.Message,), + { + "DESCRIPTOR": _MARKETSMESSAGE_ALL_MARKETS_PERFORMATIVE, + "__module__": "markets_pb2" + # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.All_Markets_Performative) + }, + ), + "Market_Performative": _reflection.GeneratedProtocolMessageType( + "Market_Performative", + (_message.Message,), + { + "DESCRIPTOR": _MARKETSMESSAGE_MARKET_PERFORMATIVE, + "__module__": "markets_pb2" + # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.Market_Performative) + }, + ), + "Error_Performative": _reflection.GeneratedProtocolMessageType( + "Error_Performative", + (_message.Message,), + { + "ErrorDataEntry": _reflection.GeneratedProtocolMessageType( + "ErrorDataEntry", + (_message.Message,), + { + "DESCRIPTOR": _MARKETSMESSAGE_ERROR_PERFORMATIVE_ERRORDATAENTRY, + "__module__": "markets_pb2" + # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.Error_Performative.ErrorDataEntry) + }, + ), + "DESCRIPTOR": _MARKETSMESSAGE_ERROR_PERFORMATIVE, + "__module__": "markets_pb2" + # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.Error_Performative) + }, + ), + "DESCRIPTOR": _MARKETSMESSAGE, + "__module__": "markets_pb2" + # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage) + }, +) +_sym_db.RegisterMessage(MarketsMessage) +_sym_db.RegisterMessage(MarketsMessage.ErrorCode) +_sym_db.RegisterMessage(MarketsMessage.Market) +_sym_db.RegisterMessage(MarketsMessage.Market.Market) +_sym_db.RegisterMessage(MarketsMessage.Markets) +_sym_db.RegisterMessage(MarketsMessage.Markets.Markets) +_sym_db.RegisterMessage(MarketsMessage.Get_All_Markets_Performative) +_sym_db.RegisterMessage(MarketsMessage.Get_Market_Performative) +_sym_db.RegisterMessage(MarketsMessage.All_Markets_Performative) +_sym_db.RegisterMessage(MarketsMessage.Market_Performative) +_sym_db.RegisterMessage(MarketsMessage.Error_Performative) +_sym_db.RegisterMessage(MarketsMessage.Error_Performative.ErrorDataEntry) + +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _MARKETSMESSAGE_ERROR_PERFORMATIVE_ERRORDATAENTRY._options = None + _MARKETSMESSAGE_ERROR_PERFORMATIVE_ERRORDATAENTRY._serialized_options = b"8\001" + _MARKETSMESSAGE._serialized_start = 50 + _MARKETSMESSAGE._serialized_end = 1999 + _MARKETSMESSAGE_ERRORCODE._serialized_start = 533 + _MARKETSMESSAGE_ERRORCODE._serialized_end = 765 + _MARKETSMESSAGE_ERRORCODE_ERRORCODEENUM._serialized_start = 638 + _MARKETSMESSAGE_ERRORCODE_ERRORCODEENUM._serialized_end = 765 + _MARKETSMESSAGE_MARKET._serialized_start = 768 + _MARKETSMESSAGE_MARKET._serialized_end = 1266 + _MARKETSMESSAGE_MARKET_MARKET._serialized_start = 779 + _MARKETSMESSAGE_MARKET_MARKET._serialized_end = 1266 + _MARKETSMESSAGE_MARKETS._serialized_start = 1268 + _MARKETSMESSAGE_MARKETS._serialized_end = 1360 + _MARKETSMESSAGE_MARKETS_MARKETS._serialized_start = 1279 + _MARKETSMESSAGE_MARKETS_MARKETS._serialized_end = 1360 + _MARKETSMESSAGE_GET_ALL_MARKETS_PERFORMATIVE._serialized_start = 1362 + _MARKETSMESSAGE_GET_ALL_MARKETS_PERFORMATIVE._serialized_end = 1456 + _MARKETSMESSAGE_GET_MARKET_PERFORMATIVE._serialized_start = 1458 + _MARKETSMESSAGE_GET_MARKET_PERFORMATIVE._serialized_end = 1516 + _MARKETSMESSAGE_ALL_MARKETS_PERFORMATIVE._serialized_start = 1518 + _MARKETSMESSAGE_ALL_MARKETS_PERFORMATIVE._serialized_end = 1617 + _MARKETSMESSAGE_MARKET_PERFORMATIVE._serialized_start = 1619 + _MARKETSMESSAGE_MARKET_PERFORMATIVE._serialized_end = 1711 + _MARKETSMESSAGE_ERROR_PERFORMATIVE._serialized_start = 1714 + _MARKETSMESSAGE_ERROR_PERFORMATIVE._serialized_end = 1983 + _MARKETSMESSAGE_ERROR_PERFORMATIVE_ERRORDATAENTRY._serialized_start = 1935 + _MARKETSMESSAGE_ERROR_PERFORMATIVE_ERRORDATAENTRY._serialized_end = 1983 +# @@protoc_insertion_point(module_scope) diff --git a/lyra/autonomy/packages/eightballer/protocols/markets/message.py b/lyra/autonomy/packages/eightballer/protocols/markets/message.py new file mode 100644 index 0000000..d041412 --- /dev/null +++ b/lyra/autonomy/packages/eightballer/protocols/markets/message.py @@ -0,0 +1,335 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023 eightballer +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""This module contains markets's message definition.""" + +# pylint: disable=too-many-statements,too-many-locals,no-member,too-few-public-methods,too-many-branches,not-an-iterable,unidiomatic-typecheck,unsubscriptable-object +import logging +from typing import Any, Dict, Optional, Set, Tuple, cast + +from aea.configurations.base import PublicId +from aea.exceptions import AEAEnforceError, enforce +from aea.protocols.base import Message + +from packages.eightballer.protocols.markets.custom_types import ( + ErrorCode as CustomErrorCode, +) +from packages.eightballer.protocols.markets.custom_types import Market as CustomMarket +from packages.eightballer.protocols.markets.custom_types import Markets as CustomMarkets + +_default_logger = logging.getLogger( + "aea.packages.eightballer.protocols.markets.message" +) + +DEFAULT_BODY_SIZE = 4 + + +class MarketsMessage(Message): + """A protocol for passing ohlcv data between compoents.""" + + protocol_id = PublicId.from_str("eightballer/markets:0.1.0") + protocol_specification_id = PublicId.from_str("eightballer/markets:0.1.0") + + ErrorCode = CustomErrorCode + + Market = CustomMarket + + Markets = CustomMarkets + + class Performative(Message.Performative): + """Performatives for the markets protocol.""" + + ALL_MARKETS = "all_markets" + ERROR = "error" + GET_ALL_MARKETS = "get_all_markets" + GET_MARKET = "get_market" + MARKET = "market" + + def __str__(self) -> str: + """Get the string representation.""" + return str(self.value) + + _performatives = {"all_markets", "error", "get_all_markets", "get_market", "market"} + __slots__: Tuple[str, ...] = tuple() + + class _SlotsCls: + __slots__ = ( + "currency", + "dialogue_reference", + "error_code", + "error_data", + "error_msg", + "exchange_id", + "id", + "market", + "markets", + "message_id", + "performative", + "target", + ) + + def __init__( + self, + performative: Performative, + dialogue_reference: Tuple[str, str] = ("", ""), + message_id: int = 1, + target: int = 0, + **kwargs: Any, + ): + """ + Initialise an instance of MarketsMessage. + + :param message_id: the message id. + :param dialogue_reference: the dialogue reference. + :param target: the message target. + :param performative: the message performative. + :param **kwargs: extra options. + """ + super().__init__( + dialogue_reference=dialogue_reference, + message_id=message_id, + target=target, + performative=MarketsMessage.Performative(performative), + **kwargs, + ) + + @property + def valid_performatives(self) -> Set[str]: + """Get valid performatives.""" + return self._performatives + + @property + def dialogue_reference(self) -> Tuple[str, str]: + """Get the dialogue_reference of the message.""" + enforce(self.is_set("dialogue_reference"), "dialogue_reference is not set.") + return cast(Tuple[str, str], self.get("dialogue_reference")) + + @property + def message_id(self) -> int: + """Get the message_id of the message.""" + enforce(self.is_set("message_id"), "message_id is not set.") + return cast(int, self.get("message_id")) + + @property + def performative(self) -> Performative: # type: ignore # noqa: F821 + """Get the performative of the message.""" + enforce(self.is_set("performative"), "performative is not set.") + return cast(MarketsMessage.Performative, self.get("performative")) + + @property + def target(self) -> int: + """Get the target of the message.""" + enforce(self.is_set("target"), "target is not set.") + return cast(int, self.get("target")) + + @property + def currency(self) -> Optional[str]: + """Get the 'currency' content from the message.""" + return cast(Optional[str], self.get("currency")) + + @property + def error_code(self) -> CustomErrorCode: + """Get the 'error_code' content from the message.""" + enforce(self.is_set("error_code"), "'error_code' content is not set.") + return cast(CustomErrorCode, self.get("error_code")) + + @property + def error_data(self) -> Dict[str, bytes]: + """Get the 'error_data' content from the message.""" + enforce(self.is_set("error_data"), "'error_data' content is not set.") + return cast(Dict[str, bytes], self.get("error_data")) + + @property + def error_msg(self) -> str: + """Get the 'error_msg' content from the message.""" + enforce(self.is_set("error_msg"), "'error_msg' content is not set.") + return cast(str, self.get("error_msg")) + + @property + def exchange_id(self) -> str: + """Get the 'exchange_id' content from the message.""" + enforce(self.is_set("exchange_id"), "'exchange_id' content is not set.") + return cast(str, self.get("exchange_id")) + + @property + def id(self) -> str: + """Get the 'id' content from the message.""" + enforce(self.is_set("id"), "'id' content is not set.") + return cast(str, self.get("id")) + + @property + def market(self) -> CustomMarket: + """Get the 'market' content from the message.""" + enforce(self.is_set("market"), "'market' content is not set.") + return cast(CustomMarket, self.get("market")) + + @property + def markets(self) -> CustomMarkets: + """Get the 'markets' content from the message.""" + enforce(self.is_set("markets"), "'markets' content is not set.") + return cast(CustomMarkets, self.get("markets")) + + def _is_consistent(self) -> bool: + """Check that the message follows the markets protocol.""" + try: + enforce( + isinstance(self.dialogue_reference, tuple), + "Invalid type for 'dialogue_reference'. Expected 'tuple'. Found '{}'.".format( + type(self.dialogue_reference) + ), + ) + enforce( + isinstance(self.dialogue_reference[0], str), + "Invalid type for 'dialogue_reference[0]'. Expected 'str'. Found '{}'.".format( + type(self.dialogue_reference[0]) + ), + ) + enforce( + isinstance(self.dialogue_reference[1], str), + "Invalid type for 'dialogue_reference[1]'. Expected 'str'. Found '{}'.".format( + type(self.dialogue_reference[1]) + ), + ) + enforce( + type(self.message_id) is int, + "Invalid type for 'message_id'. Expected 'int'. Found '{}'.".format( + type(self.message_id) + ), + ) + enforce( + type(self.target) is int, + "Invalid type for 'target'. Expected 'int'. Found '{}'.".format( + type(self.target) + ), + ) + + # Light Protocol Rule 2 + # Check correct performative + enforce( + isinstance(self.performative, MarketsMessage.Performative), + "Invalid 'performative'. Expected either of '{}'. Found '{}'.".format( + self.valid_performatives, self.performative + ), + ) + + # Check correct contents + actual_nb_of_contents = len(self._body) - DEFAULT_BODY_SIZE + expected_nb_of_contents = 0 + if self.performative == MarketsMessage.Performative.GET_ALL_MARKETS: + expected_nb_of_contents = 1 + enforce( + isinstance(self.exchange_id, str), + "Invalid type for content 'exchange_id'. Expected 'str'. Found '{}'.".format( + type(self.exchange_id) + ), + ) + if self.is_set("currency"): + expected_nb_of_contents += 1 + currency = cast(str, self.currency) + enforce( + isinstance(currency, str), + "Invalid type for content 'currency'. Expected 'str'. Found '{}'.".format( + type(currency) + ), + ) + elif self.performative == MarketsMessage.Performative.GET_MARKET: + expected_nb_of_contents = 2 + enforce( + isinstance(self.id, str), + "Invalid type for content 'id'. Expected 'str'. Found '{}'.".format( + type(self.id) + ), + ) + enforce( + isinstance(self.exchange_id, str), + "Invalid type for content 'exchange_id'. Expected 'str'. Found '{}'.".format( + type(self.exchange_id) + ), + ) + elif self.performative == MarketsMessage.Performative.ALL_MARKETS: + expected_nb_of_contents = 1 + enforce( + isinstance(self.markets, CustomMarkets), + "Invalid type for content 'markets'. Expected 'Markets'. Found '{}'.".format( + type(self.markets) + ), + ) + elif self.performative == MarketsMessage.Performative.MARKET: + expected_nb_of_contents = 1 + enforce( + isinstance(self.market, CustomMarket), + "Invalid type for content 'market'. Expected 'Market'. Found '{}'.".format( + type(self.market) + ), + ) + elif self.performative == MarketsMessage.Performative.ERROR: + expected_nb_of_contents = 3 + enforce( + isinstance(self.error_code, CustomErrorCode), + "Invalid type for content 'error_code'. Expected 'ErrorCode'. Found '{}'.".format( + type(self.error_code) + ), + ) + enforce( + isinstance(self.error_msg, str), + "Invalid type for content 'error_msg'. Expected 'str'. Found '{}'.".format( + type(self.error_msg) + ), + ) + enforce( + isinstance(self.error_data, dict), + "Invalid type for content 'error_data'. Expected 'dict'. Found '{}'.".format( + type(self.error_data) + ), + ) + for key_of_error_data, value_of_error_data in self.error_data.items(): + enforce( + isinstance(key_of_error_data, str), + "Invalid type for dictionary keys in content 'error_data'. Expected 'str'. Found '{}'.".format( + type(key_of_error_data) + ), + ) + enforce( + isinstance(value_of_error_data, bytes), + "Invalid type for dictionary values in content 'error_data'. Expected 'bytes'. Found '{}'.".format( + type(value_of_error_data) + ), + ) + + # Check correct content count + enforce( + expected_nb_of_contents == actual_nb_of_contents, + "Incorrect number of contents. Expected {}. Found {}".format( + expected_nb_of_contents, actual_nb_of_contents + ), + ) + + # Light Protocol Rule 3 + if self.message_id == 1: + enforce( + self.target == 0, + "Invalid 'target'. Expected 0 (because 'message_id' is 1). Found {}.".format( + self.target + ), + ) + except (AEAEnforceError, ValueError, KeyError) as e: + _default_logger.error(str(e)) + return False + + return True diff --git a/lyra/autonomy/packages/eightballer/protocols/markets/protocol.yaml b/lyra/autonomy/packages/eightballer/protocols/markets/protocol.yaml new file mode 100644 index 0000000..bdaec9d --- /dev/null +++ b/lyra/autonomy/packages/eightballer/protocols/markets/protocol.yaml @@ -0,0 +1,21 @@ +name: markets +author: eightballer +version: 0.1.0 +protocol_specification_id: eightballer/markets:0.1.0 +type: protocol +description: A protocol for passing ohlcv data between compoents. +license: Apache-2.0 +aea_version: '>=1.0.0, <2.0.0' +fingerprint: + __init__.py: bafybeicfxz7qdkubd7bv5vnj7lg7eoepevbtpowzjfi4hqa5yzrbywbo7y + custom_types.py: bafybeihefst4r55a3cc552m2v6xahgonb2dychdb2pljclwyyv4s3my24a + dialogues.py: bafybeigqgitua32dms6n4izw3a22un4c4mxocbu54564ud3ykxiigtyny4 + markets.proto: bafybeibihxjeibzycgd5e4rqifzq7wkaeqlzf2cmg75ppwl47murmygp6e + markets_pb2.py: bafybeiaebcrubvd25akmoxndo4h7qy2an7dmyg6cd4pya73au2yia2vsme + message.py: bafybeib2u33yyizdlqbj4stkrf2xsc2qnpsk5qkpeyekub5xuif6iplady + serialization.py: bafybeicvxnw7hwuedxkou6ijnqtv6td4k2kdnrlg2vrfoxgfgogj7xniym + tests/test_markets_dialogues.py: bafybeihikml67lyih4g7nhemayldz6ahi3tll4usen754r7jcxvtzw2vj4 + tests/test_markets_messages.py: bafybeigysvwtkc2k6teatleketos4agq6ze3tg7rzhlpygh64fumhxarni +fingerprint_ignore_patterns: [] +dependencies: + protobuf: {} diff --git a/lyra/autonomy/packages/eightballer/protocols/markets/serialization.py b/lyra/autonomy/packages/eightballer/protocols/markets/serialization.py new file mode 100644 index 0000000..af24a42 --- /dev/null +++ b/lyra/autonomy/packages/eightballer/protocols/markets/serialization.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023 eightballer +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Serialization module for markets protocol.""" + +# pylint: disable=too-many-statements,too-many-locals,no-member,too-few-public-methods,redefined-builtin +from typing import cast + +from aea.mail.base_pb2 import DialogueMessage +from aea.mail.base_pb2 import Message as ProtobufMessage +from aea.protocols.base import Message, Serializer + +from packages.eightballer.protocols.markets import markets_pb2 +from packages.eightballer.protocols.markets.custom_types import ( + ErrorCode, + Market, + Markets, +) +from packages.eightballer.protocols.markets.message import MarketsMessage + + +class MarketsSerializer(Serializer): + """Serialization for the 'markets' protocol.""" + + @staticmethod + def encode(msg: Message) -> bytes: + """ + Encode a 'Markets' message into bytes. + + :param msg: the message object. + :return: the bytes. + """ + msg = cast(MarketsMessage, msg) + message_pb = ProtobufMessage() + dialogue_message_pb = DialogueMessage() + markets_msg = markets_pb2.MarketsMessage() + + dialogue_message_pb.message_id = msg.message_id + dialogue_reference = msg.dialogue_reference + dialogue_message_pb.dialogue_starter_reference = dialogue_reference[0] + dialogue_message_pb.dialogue_responder_reference = dialogue_reference[1] + dialogue_message_pb.target = msg.target + + performative_id = msg.performative + if performative_id == MarketsMessage.Performative.GET_ALL_MARKETS: + performative = markets_pb2.MarketsMessage.Get_All_Markets_Performative() # type: ignore + exchange_id = msg.exchange_id + performative.exchange_id = exchange_id + if msg.is_set("currency"): + performative.currency_is_set = True + currency = msg.currency + performative.currency = currency + markets_msg.get_all_markets.CopyFrom(performative) + elif performative_id == MarketsMessage.Performative.GET_MARKET: + performative = markets_pb2.MarketsMessage.Get_Market_Performative() # type: ignore + id = msg.id + performative.id = id + exchange_id = msg.exchange_id + performative.exchange_id = exchange_id + markets_msg.get_market.CopyFrom(performative) + elif performative_id == MarketsMessage.Performative.ALL_MARKETS: + performative = markets_pb2.MarketsMessage.All_Markets_Performative() # type: ignore + markets = msg.markets + Markets.encode(performative.markets, markets) + markets_msg.all_markets.CopyFrom(performative) + elif performative_id == MarketsMessage.Performative.MARKET: + performative = markets_pb2.MarketsMessage.Market_Performative() # type: ignore + market = msg.market + Market.encode(performative.market, market) + markets_msg.market.CopyFrom(performative) + elif performative_id == MarketsMessage.Performative.ERROR: + performative = markets_pb2.MarketsMessage.Error_Performative() # type: ignore + error_code = msg.error_code + ErrorCode.encode(performative.error_code, error_code) + error_msg = msg.error_msg + performative.error_msg = error_msg + error_data = msg.error_data + performative.error_data.update(error_data) + markets_msg.error.CopyFrom(performative) + else: + raise ValueError("Performative not valid: {}".format(performative_id)) + + dialogue_message_pb.content = markets_msg.SerializeToString() + + message_pb.dialogue_message.CopyFrom(dialogue_message_pb) + message_bytes = message_pb.SerializeToString() + return message_bytes + + @staticmethod + def decode(obj: bytes) -> Message: + """ + Decode bytes into a 'Markets' message. + + :param obj: the bytes object. + :return: the 'Markets' message. + """ + message_pb = ProtobufMessage() + markets_pb = markets_pb2.MarketsMessage() + message_pb.ParseFromString(obj) + message_id = message_pb.dialogue_message.message_id + dialogue_reference = ( + message_pb.dialogue_message.dialogue_starter_reference, + message_pb.dialogue_message.dialogue_responder_reference, + ) + target = message_pb.dialogue_message.target + + markets_pb.ParseFromString(message_pb.dialogue_message.content) + performative = markets_pb.WhichOneof("performative") + performative_id = MarketsMessage.Performative(str(performative)) + performative_content = dict() # type: Dict[str, Any] + if performative_id == MarketsMessage.Performative.GET_ALL_MARKETS: + exchange_id = markets_pb.get_all_markets.exchange_id + performative_content["exchange_id"] = exchange_id + if markets_pb.get_all_markets.currency_is_set: + currency = markets_pb.get_all_markets.currency + performative_content["currency"] = currency + elif performative_id == MarketsMessage.Performative.GET_MARKET: + id = markets_pb.get_market.id + performative_content["id"] = id + exchange_id = markets_pb.get_market.exchange_id + performative_content["exchange_id"] = exchange_id + elif performative_id == MarketsMessage.Performative.ALL_MARKETS: + pb2_markets = markets_pb.all_markets.markets + markets = Markets.decode(pb2_markets) + performative_content["markets"] = markets + elif performative_id == MarketsMessage.Performative.MARKET: + pb2_market = markets_pb.market.market + market = Market.decode(pb2_market) + performative_content["market"] = market + elif performative_id == MarketsMessage.Performative.ERROR: + pb2_error_code = markets_pb.error.error_code + error_code = ErrorCode.decode(pb2_error_code) + performative_content["error_code"] = error_code + error_msg = markets_pb.error.error_msg + performative_content["error_msg"] = error_msg + error_data = markets_pb.error.error_data + error_data_dict = dict(error_data) + performative_content["error_data"] = error_data_dict + else: + raise ValueError("Performative not valid: {}.".format(performative_id)) + + return MarketsMessage( + message_id=message_id, + dialogue_reference=dialogue_reference, + target=target, + performative=performative, + **performative_content + ) diff --git a/lyra/autonomy/packages/eightballer/protocols/markets/tests/test_markets_dialogues.py b/lyra/autonomy/packages/eightballer/protocols/markets/tests/test_markets_dialogues.py new file mode 100644 index 0000000..7c148ad --- /dev/null +++ b/lyra/autonomy/packages/eightballer/protocols/markets/tests/test_markets_dialogues.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023 eightballer +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test dialogues module for markets protocol.""" + +# pylint: disable=too-many-statements,too-many-locals,no-member,too-few-public-methods,redefined-builtin +from aea.test_tools.test_protocol import BaseProtocolDialoguesTestCase + +from packages.eightballer.protocols.markets.dialogues import ( + MarketsDialogue, + MarketsDialogues, +) +from packages.eightballer.protocols.markets.message import MarketsMessage + + +class TestDialoguesMarkets(BaseProtocolDialoguesTestCase): + """Test for the 'markets' protocol dialogues.""" + + MESSAGE_CLASS = MarketsMessage + + DIALOGUE_CLASS = MarketsDialogue + + DIALOGUES_CLASS = MarketsDialogues + + ROLE_FOR_THE_FIRST_MESSAGE = MarketsDialogue.Role.AGENT # CHECK + + def make_message_content(self) -> dict: + """Make a dict with message contruction content for dialogues.create.""" + return dict( + performative=MarketsMessage.Performative.GET_ALL_MARKETS, + exchange_id="some str", + currency="some str", + ) diff --git a/lyra/autonomy/packages/eightballer/protocols/markets/tests/test_markets_messages.py b/lyra/autonomy/packages/eightballer/protocols/markets/tests/test_markets_messages.py new file mode 100644 index 0000000..6f19c83 --- /dev/null +++ b/lyra/autonomy/packages/eightballer/protocols/markets/tests/test_markets_messages.py @@ -0,0 +1,227 @@ +# -*- coding: utf-8 -*- +# ------------------------------------------------------------------------------ +# +# Copyright 2023 eightballer +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ------------------------------------------------------------------------------ + +"""Test messages module for markets protocol.""" + +# pylint: disable=too-many-statements,too-many-locals,no-member,too-few-public-methods,redefined-builtin +from typing import List + +from aea.test_tools.test_protocol import BaseProtocolMessagesTestCase + +from packages.eightballer.protocols.markets.custom_types import ( + ErrorCode, + Market, + Markets, +) +from packages.eightballer.protocols.markets.message import MarketsMessage + +TEST_MARKET_CASE = { + "id": "ETHBTC", + "lowercaseId": "ethbtc", + "symbol": "ETH/BTC", + "base": "ETH", + "quote": "BTC", + "settle": None, + "baseId": "ETH", + "quoteId": "BTC", + "settleId": None, + "type": "spot", + "spot": True, + "margin": True, + "swap": False, + "future": False, + "option": False, + "active": True, + "contract": False, + "linear": None, + "inverse": None, + "taker": 0.001, + "maker": 0.001, + "contractSize": None, + "expiry": None, + "expiryDatetime": None, + "strike": None, + "optionType": None, + "precision": {"amount": 4, "price": 5, "base": 8, "quote": 8}, + "limits": { + "leverage": {"min": None, "max": None}, + "amount": {"min": 0.0001, "max": 100000.0}, + "price": {"min": 1e-05, "max": 922327.0}, + "cost": {"min": 0.0001, "max": 9000000.0}, + "market": {"min": 0.0, "max": 3832.38128875}, + }, + "info": { + "symbol": "ETHBTC", + "status": "TRADING", + "baseAsset": "ETH", + "baseAssetPrecision": "8", + "quoteAsset": "BTC", + "quotePrecision": "8", + "quoteAssetPrecision": "8", + "baseCommissionPrecision": "8", + "quoteCommissionPrecision": "8", + "orderTypes": [ + "LIMIT", + "LIMIT_MAKER", + "MARKET", + "STOP_LOSS_LIMIT", + "TAKE_PROFIT_LIMIT", + ], + "icebergAllowed": True, + "ocoAllowed": True, + "quoteOrderQtyMarketAllowed": True, + "allowTrailingStop": True, + "cancelReplaceAllowed": True, + "isSpotTradingAllowed": True, + "isMarginTradingAllowed": True, + "filters": [ + { + "filterType": "PRICE_FILTER", + "minPrice": "0.00001000", + "maxPrice": "922327.00000000", + "tickSize": "0.00001000", + }, + { + "filterType": "LOT_SIZE", + "minQty": "0.00010000", + "maxQty": "100000.00000000", + "stepSize": "0.00010000", + }, + {"filterType": "ICEBERG_PARTS", "limit": "10"}, + { + "filterType": "MARKET_LOT_SIZE", + "minQty": "0.00000000", + "maxQty": "3832.38128875", + "stepSize": "0.00000000", + }, + { + "filterType": "TRAILING_DELTA", + "minTrailingAboveDelta": "10", + "maxTrailingAboveDelta": "2000", + "minTrailingBelowDelta": "10", + "maxTrailingBelowDelta": "2000", + }, + { + "filterType": "PERCENT_PRICE_BY_SIDE", + "bidMultiplierUp": "5", + "bidMultiplierDown": "0.2", + "askMultiplierUp": "5", + "askMultiplierDown": "0.2", + "avgPriceMins": "5", + }, + { + "filterType": "NOTIONAL", + "minNotional": "0.00010000", + "applyMinToMarket": True, + "maxNotional": "9000000.00000000", + "applyMaxToMarket": False, + "avgPriceMins": "5", + }, + {"filterType": "MAX_NUM_ORDERS", "maxNumOrders": "200"}, + {"filterType": "MAX_NUM_ALGO_ORDERS", "maxNumAlgoOrders": "5"}, + ], + "permissions": [ + "SPOT", + "MARGIN", + "TRD_GRP_004", + "TRD_GRP_005", + "TRD_GRP_006", + "TRD_GRP_008", + "TRD_GRP_009", + "TRD_GRP_010", + "TRD_GRP_011", + "TRD_GRP_012", + "TRD_GRP_013", + ], + "defaultSelfTradePreventionMode": "NONE", + "allowedSelfTradePreventionModes": [ + "NONE", + "EXPIRE_TAKER", + "EXPIRE_MAKER", + "EXPIRE_BOTH", + ], + }, +} + + +class TestMessageMarkets(BaseProtocolMessagesTestCase): + """Test for the 'markets' protocol message.""" + + MESSAGE_CLASS = MarketsMessage + + def build_messages(self) -> List[MarketsMessage]: # type: ignore[override] + """Build the messages to be used for testing.""" + return [ + MarketsMessage( + performative=MarketsMessage.Performative.GET_ALL_MARKETS, + exchange_id="some str", + currency="some str", + ), + MarketsMessage( + performative=MarketsMessage.Performative.GET_MARKET, + id="some str", + exchange_id="some str", + ), + MarketsMessage( + performative=MarketsMessage.Performative.ALL_MARKETS, + markets=Markets([Market(**TEST_MARKET_CASE)]), # check it please! + ), + MarketsMessage( + performative=MarketsMessage.Performative.MARKET, + market=Market(**TEST_MARKET_CASE), # check it please! + ), + MarketsMessage( + performative=MarketsMessage.Performative.ERROR, + error_code=ErrorCode.INVALID_MESSAGE, # check it please! + error_msg="some str", + error_data={"some str": b"some_bytes"}, + ), + MarketsMessage( + performative=MarketsMessage.Performative.END, + ), + ] + + def build_inconsistent(self) -> List[MarketsMessage]: # type: ignore[override] + """Build inconsistent messages to be used for testing.""" + return [ + MarketsMessage( + performative=MarketsMessage.Performative.GET_ALL_MARKETS, + # skip content: exchange_id + currency="some str", + ), + MarketsMessage( + performative=MarketsMessage.Performative.GET_MARKET, + # skip content: id + exchange_id="some str", + ), + MarketsMessage( + performative=MarketsMessage.Performative.ALL_MARKETS, + # skip content: markets + ), + MarketsMessage( + performative=MarketsMessage.Performative.MARKET, + # skip content: market + ), + MarketsMessage( + performative=MarketsMessage.Performative.ERROR, + # skip content: error_code + error_msg="some str", + error_data={"some str": b"some_bytes"}, + ), + ] diff --git a/lyra/autonomy/packages/packages.json b/lyra/autonomy/packages/packages.json new file mode 100644 index 0000000..73e094c --- /dev/null +++ b/lyra/autonomy/packages/packages.json @@ -0,0 +1,6 @@ +{ + "dev": {}, + "third_party": { + "protocol/eightballer/markets/0.1.0": "bafybeihzukefu4kffjgwbd5p6svfseryui2ugoki6vrdnemdzyrfhpyiea" + } +} \ No newline at end of file diff --git a/lyra/base_client.py b/lyra/base_client.py new file mode 100644 index 0000000..9b62b96 --- /dev/null +++ b/lyra/base_client.py @@ -0,0 +1,706 @@ +""" +Base Client for the lyra dex. +""" +import json +import random +import time +from datetime import datetime + +import eth_abi +import requests +from eth_account.messages import encode_defunct +from rich import print +from web3 import Web3 +from websocket import create_connection + +from lyra.constants import CONTRACTS, PUBLIC_HEADERS, TEST_PRIVATE_KEY +from lyra.enums import ( + ActionType, + CollateralAsset, + InstrumentType, + OrderSide, + OrderStatus, + OrderType, + SubaccountType, + TimeInForce, + UnderlyingCurrency, + Environment +) +from lyra.utils import get_logger +import sys +import os +from pathlib import Path + +# we get the install location +# install_location = os.path.dirname(os.path.realpath(__file__)) +# sys.path.append(str(Path(install_location) / "autonomy")) +# breakpoint() +# from packages.eightballer.protocols.markets.custom_types import Market + +def to_market(api_result): + """Convert to a market object. + raw_resulot = {'instrument_type': 'perp', 'instrument_name': 'BTC-PERP', 'scheduled_activation': 1699035945, 'scheduled_deactivation': 9223372036854775807, 'is_active': True, 'tick_size': '0.1', 'minimum_amount': '0.01', 'maximum_amount': '10000', 'amount_step': '0.001', 'mark_price_fee_rate_cap': '0', 'maker_fee_rate': '0.0005', 'taker_fee_rate': '0.001', 'base_fee': '1.5', 'base_currency': 'BTC', 'quote_currency': 'USD', 'option_details': None, 'perp_details': {'index': 'BTC-USD', 'max_rate_per_hour': '0.1', 'min_rate_per_hour': '-0.1', 'static_interest_rate': '0', 'aggregate_funding': '244.249950785486024857', 'funding_rate': '-0.0000125'}, 'base_asset_address': '0xAFB6Bb95cd70D5367e2C39e9dbEb422B9815339D', 'base_asset_sub_id': '0'} + + """ + + market = Market( + id=api_result['instrument_name'], + lowercaseId=api_result['instrument_name'].lower(), + symbol=api_result['instrument_name'], + base=api_result['base_currency'], + quote=api_result['quote_currency'], + settle=api_result['quote_currency'], + baseId=api_result['base_currency'], + quoteId=api_result['quote_currency'], + settleId=api_result['quote_currency'], + type=api_result['instrument_type'], + future=api_result['instrument_type'] == InstrumentType.PERP, + option=api_result['instrument_type'] == InstrumentType.OPTION, + active=api_result['is_active'], + taker=api_result['taker_fee_rate'], + maker=api_result['maker_fee_rate'], + ) + return market + + + +class BaseClient: + """Client for the lyra dex.""" + + def __init__(self, private_key: str = TEST_PRIVATE_KEY, env: Environment = Environment.TEST, logger=None, verbose=False, subaccount_id=None, wallet=None): + """ + Initialize the LyraClient class. + """ + self.verbose = verbose + self.env = env + self.contracts = CONTRACTS[env] + self.logger = logger or get_logger() + self.web3_client = Web3() + self.signer = self.web3_client.eth.account.from_key(private_key) + self.wallet = self.signer.address if not wallet else wallet + print(f"Signing address: {self.signer.address}") + if wallet: + print(f"Using wallet: {wallet}") + if not subaccount_id: + self.subaccount_id = self.fetch_subaccounts()['subaccount_ids'][0] + else: + self.subaccount_id = subaccount_id + print(f"Using subaccount id: {self.subaccount_id}") + self.ws = self.connect_ws() + self.login_client() + + def sign_authentication_header(self): + timestamp = str(int(time.time() * 1000)) + msg = encode_defunct( + text=timestamp, + ) + signature = self.web3_client.eth.account.sign_message( + msg, private_key=self.signer._private_key + ).signature.hex() # pylint: disable=protected-access + return { + 'wallet': self.wallet, + 'timestamp': str(timestamp), + 'signature': signature, + } + + def connect_ws(self): + ws = create_connection(self.contracts['WS_ADDRESS']) + return ws + + def create_account(self, wallet): + """Call the create account endpoint.""" + payload = {"wallet": wallet} + url = f"{self.contracts['BASE_URL']}/public/create_account" + result = requests.post( + headers=PUBLIC_HEADERS, + url=url, + json=payload, + ) + result_code = json.loads(result.content) + + if "error" in result_code: + raise Exception(result_code["error"]) + return True + + def fetch_instruments( + self, + expired=False, + instrument_type: InstrumentType = InstrumentType.PERP, + currency: UnderlyingCurrency = UnderlyingCurrency.BTC, + ): + """ + Return the tickers. + First fetch all instrucments + Then get the ticket for all instruments. + """ + url = f"{self.contracts['BASE_URL']}/public/get_instruments" + payload = { + "expired": expired, + "instrument_type": instrument_type.value, + "currency": currency.name, + } + response = requests.post(url, json=payload, headers=PUBLIC_HEADERS) + results = response.json()["result"] + + return [ + to_market(market) + for market in results + ] + + def fetch_subaccounts(self): + """ + Returns the subaccounts for a given wallet + """ + url = f"{self.contracts['BASE_URL']}/private/get_subaccounts" + payload = {"wallet": self.wallet} + headers = self._create_signature_headers() + response = requests.post(url, json=payload, headers=headers) + results = json.loads(response.content)["result"] + return results + + def fetch_subaccount(self, subaccount_id): + """ + Returns information for a given subaccount + """ + url = f"{self.contracts['BASE_URL']}/private/get_subaccount" + payload = {"subaccount_id": subaccount_id} + headers = self._create_signature_headers() + response = requests.post(url, json=payload, headers=headers) + results = response.json()["result"] + return results + + def create_order( + self, + price, + amount, + instrument_name: str, + reduce_only=False, + side: OrderSide = OrderSide.BUY, + order_type: OrderType = OrderType.LIMIT, + time_in_force: TimeInForce = TimeInForce.GTC, + ): + """ + Create the order. + """ + if side.name.upper() not in OrderSide.__members__: + raise Exception(f"Invalid side {side}") + order = self._define_order( + instrument_name=instrument_name, + price=price, + amount=amount, + side=side, + ) + _currency = UnderlyingCurrency[instrument_name.split("-")[0]] + if instrument_name.split("-")[1] == "PERP": + instruments = self.fetch_instruments(instrument_type=InstrumentType.PERP, currency=_currency) + instruments = {i['instrument_name']: i for i in instruments} + base_asset_sub_id = instruments[instrument_name]['base_asset_sub_id'] + instrument_type = InstrumentType.PERP + else: + instruments = self.fetch_instruments(instrument_type=InstrumentType.OPTION, currency=_currency) + instruments = {i['instrument_name']: i for i in instruments} + base_asset_sub_id = instruments[instrument_name]['base_asset_sub_id'] + instrument_type = InstrumentType.OPTION + + signed_order = self._sign_order(order, base_asset_sub_id, instrument_type, _currency) + response = self.submit_order(signed_order) + return response + + def _define_order( + self, + instrument_name: str, + price: float, + amount: float, + side: OrderSide, + time_in_force: TimeInForce = TimeInForce.GTC, + ): + """ + Define the order, in preparation for encoding and signing + """ + ts = int(datetime.now().timestamp() * 1000) + return { + 'instrument_name': instrument_name, + 'subaccount_id': self.subaccount_id, + 'direction': side.name.lower(), + 'limit_price': price, + 'amount': amount, + 'signature_expiry_sec': int(ts) + 3000, + 'max_fee': '200.01', + 'nonce': int(f"{int(ts)}{random.randint(100, 999)}"), + 'signer': self.signer.address, + 'order_type': 'limit', + 'mmp': False, + 'time_in_force': time_in_force.value, + 'signature': 'filled_in_below', + } + + def submit_order(self, order): + id = str(int(time.time())) + self.ws.send(json.dumps({'method': 'private/order', 'params': order, 'id': id})) + while True: + message = json.loads(self.ws.recv()) + if message['id'] == id: + try: + return message['result']['order'] + except KeyError as error: + print(message) + raise Exception(f"Unable to submit order {message}") from error + + def _encode_trade_data(self, order, base_asset_sub_id, instrument_type, currency): + encoded_data = eth_abi.encode( + ['address', 'uint256', 'int256', 'int256', 'uint256', 'uint256', 'bool'], + [ + self.contracts[f'{currency.name}_{instrument_type.name}_ADDRESS'], + int(base_asset_sub_id), + self.web3_client.to_wei(order['limit_price'], 'ether'), + self.web3_client.to_wei(order['amount'], 'ether'), + self.web3_client.to_wei(order['max_fee'], 'ether'), + order['subaccount_id'], + order['direction'] == 'buy', + ], + ) + + return self.web3_client.keccak(encoded_data) + + def _sign_order(self, order, base_asset_sub_id, instrument_type, currency): + trade_module_data = self._encode_trade_data(order, base_asset_sub_id, instrument_type, currency) + encoded_action_hash = eth_abi.encode( + ['bytes32', 'uint256', 'uint256', 'address', 'bytes32', 'uint256', 'address', 'address'], + [ + bytes.fromhex(self.contracts['ACTION_TYPEHASH'][2:]), + order['subaccount_id'], + order['nonce'], + self.contracts['TRADE_MODULE_ADDRESS'], + trade_module_data, + order['signature_expiry_sec'], + self.wallet, + order['signer'], + ], + ) + + action_hash = self.web3_client.keccak(encoded_action_hash) + encoded_typed_data_hash = "".join(['0x1901', self.contracts['DOMAIN_SEPARATOR'][2:], action_hash.hex()[2:]]) + typed_data_hash = self.web3_client.keccak(hexstr=encoded_typed_data_hash) + order['signature'] = self.signer.signHash(typed_data_hash).signature.hex() + return order + + def login_client( + self, + ): + login_request = { + 'method': 'public/login', + 'params': self.sign_authentication_header(), + 'id': str(int(time.time())), + } + self.ws.send(json.dumps(login_request)) + # we need to wait for the response + while True: + message = json.loads(self.ws.recv()) + if message['id'] == login_request['id']: + if "result" not in message: + raise Exception(f"Unable to login {message}") + break + + def fetch_ticker(self, instrument_name): + """ + Fetch the ticker for a given instrument name. + """ + url = f"{self.contracts['BASE_URL']}/public/get_ticker" + payload = {"instrument_name": instrument_name} + response = requests.post(url, json=payload, headers=PUBLIC_HEADERS) + results = json.loads(response.content)["result"] + return results + + def fetch_orders( + self, + instrument_name: str = None, + label: str = None, + page: int = 1, + page_size: int = 100, + status: OrderStatus = None, + ): + """ + Fetch the orders for a given instrument name. + """ + url = f"{self.contracts['BASE_URL']}/private/get_orders" + payload = {"instrument_name": instrument_name, "subaccount_id": self.subaccount_id} + for key, value in {"label": label, "page": page, "page_size": page_size, "status": status}.items(): + if value: + payload[key] = value + headers = self._create_signature_headers() + response = requests.post(url, json=payload, headers=headers) + results = response.json()["result"]['orders'] + return results + + def cancel(self, order_id, instrument_name): + """ + Cancel an order + """ + + id = str(int(time.time())) + payload = {"order_id": order_id, "subaccount_id": self.subaccount_id, "instrument_name": instrument_name} + self.ws.send(json.dumps({'method': 'private/cancel', 'params': payload, 'id': id})) + while True: + message = json.loads(self.ws.recv()) + if message['id'] == id: + return message['result'] + + def cancel_all(self): + """ + Cancel all orders + """ + id = str(int(time.time())) + payload = {"subaccount_id": self.subaccount_id} + self.ws.send(json.dumps({'method': 'private/cancel_all', 'params': payload, 'id': id})) + while True: + message = json.loads(self.ws.recv()) + if message['id'] == id: + return message['result'] + + def get_positions(self): + """ + Get positions + """ + url = f"{self.contracts['BASE_URL']}/private/get_positions" + payload = {"subaccount_id": self.subaccount_id} + headers = self._create_signature_headers() + response = requests.post(url, json=payload, headers=headers) + results = response.json()["result"]['positions'] + return results + + def get_collaterals(self): + """ + Get collaterals + """ + url = f"{self.contracts['BASE_URL']}/private/get_collaterals" + payload = {"subaccount_id": self.subaccount_id} + headers = self._create_signature_headers() + response = requests.post(url, json=payload, headers=headers) + results = response.json()["result"]['collaterals'] + return results.pop() + + def fetch_tickers( + self, + instrument_type: InstrumentType = InstrumentType.OPTION, + currency: UnderlyingCurrency = UnderlyingCurrency.BTC, + ): + """ + Fetch tickers using the ws connection + """ + instruments = self.fetch_instruments(instrument_type=instrument_type, currency=currency) + instrument_names = [i['instrument_name'] for i in instruments] + id_base = str(int(time.time())) + ids_to_instrument_names = { + f'{id_base}_{enumerate}': instrument_name for enumerate, instrument_name in enumerate(instrument_names) + } + for id, instrument_name in ids_to_instrument_names.items(): + payload = {"instrument_name": instrument_name} + self.ws.send(json.dumps({'method': 'public/get_ticker', 'params': payload, 'id': id})) + time.sleep(0.05) # otherwise we get rate limited... + results = {} + while ids_to_instrument_names: + message = json.loads(self.ws.recv()) + if message['id'] in ids_to_instrument_names: + results[message['result']['instrument_name']] = message['result'] + del ids_to_instrument_names[message['id']] + return results + + def create_subaccount( + self, + amount=0, + subaccount_type: SubaccountType = SubaccountType.STANDARD, + collateral_asset: CollateralAsset = CollateralAsset.USDC, + underlying_currency: UnderlyingCurrency = UnderlyingCurrency.ETH, + ): + """ + Create a subaccount. + """ + url = f"{self.contracts['BASE_URL']}/private/create_subaccount" + _, nonce, expiration = self.get_nonce_and_signature_expiry() + if subaccount_type is SubaccountType.STANDARD: + contract_key = f"{subaccount_type.name}_RISK_MANAGER_ADDRESS" + elif subaccount_type is SubaccountType.PORTFOLIO: + if not collateral_asset: + raise Exception("Underlying currency must be provided for portfolio subaccounts") + contract_key = f"{underlying_currency.name}_{subaccount_type.name}_RISK_MANAGER_ADDRESS" + else: + raise Exception(f"Invalid subaccount type {subaccount_type}") + payload = { + "amount": f"{amount}", + "asset_name": collateral_asset.name, + "margin_type": "SM" if subaccount_type is SubaccountType.STANDARD else "PM", + 'nonce': nonce, + "signature": "string", + "signature_expiry_sec": expiration, + "signer": self.signer.address, + "wallet": self.wallet, + } + if subaccount_type is SubaccountType.PORTFOLIO: + payload['currency'] = underlying_currency.name + encoded_deposit_data = self._encode_deposit_data( + amount=amount, + contract_key=contract_key, + ) + action_hash = self._generate_action_hash( + subaccount_id=0, # as we are depositing to a new subaccount. + nonce=nonce, + expiration=expiration, + encoded_deposit_data=encoded_deposit_data, + ) + + typed_data_hash = self._generate_typed_data_hash( + action_hash=action_hash, + ) + + signature = self.signer.signHash(typed_data_hash).signature.hex() + payload['signature'] = signature + print(f"Payload: {payload}") + + headers = self._create_signature_headers() + response = requests.post(url, json=payload, headers=headers) + + if "error" in response.json(): + raise Exception(response.json()["error"]) + print(response.text) + if "result" not in response.json(): + raise Exception(f"Unable to create subaccount {response.json()}") + return response.json()["result"] + + def _encode_deposit_data(self, amount: int, contract_key: str): + """Encode the deposit data""" + + encoded_data = eth_abi.encode( + ['uint256', 'address', 'address'], + [ + int(amount * 1e6), + self.contracts["CASH_ASSET"], + self.contracts[contract_key], + ], + ) + print(f"Encoded data: {encoded_data}") + return self.web3_client.keccak(encoded_data) + + def get_nonce_and_signature_expiry(self): + """ + Returns the nonce and signature expiry + """ + ts = int(datetime.now().timestamp() * 1000) + nonce = int(f"{int(ts)}{random.randint(100, 999)}") + expiration = int(ts) + 6000 + return ts, nonce, expiration + + def _generate_typed_data_hash( + self, + action_hash: bytes, + ): + """Generate the typed data hash.""" + + encoded_typed_data_hash = "".join(['0x1901', self.contracts['DOMAIN_SEPARATOR'][2:], action_hash.hex()[2:]]) + typed_data_hash = self.web3_client.keccak(hexstr=encoded_typed_data_hash) + return typed_data_hash + + def transfer_collateral(self, amount: int, to: str, asset: CollateralAsset): + """ + Transfer collateral + """ + + ts = int(datetime.now().timestamp() * 1000) + nonce = int(f"{int(ts)}{random.randint(100, 499)}") + nonce_2 = int(f"{int(ts)}{random.randint(500, 999)}") + expiration = int(datetime.now().timestamp() + 10000) + + url = f"{self.contracts['BASE_URL']}/private/transfer_erc20" + _, nonce, expiration = self.get_nonce_and_signature_expiry() + transfer = { + "address": self.contracts["CASH_ASSET"], + "amount": int(amount), + "sub_id": 0, + } + print(f"Transfering to {to} amount {amount} asset {asset.name}") + + encoded_data = self.encode_transfer( + amount=amount, + to=to, + ) + + action_hash_1 = self._generate_action_hash( + subaccount_id=self.subaccount_id, + nonce=nonce, + expiration=expiration, + encoded_deposit_data=encoded_data, + action_type=ActionType.TRANSFER, + ) + + from_signed_action_hash = self._generate_signed_action( + action_hash=action_hash_1, + nonce=nonce, + expiration=expiration, + ) + + print(f"from_signed_action_hash: {from_signed_action_hash}") + print(f"From action hash: {action_hash_1.hex()}") + + action_hash_2 = self._generate_action_hash( + subaccount_id=to, + nonce=nonce_2, + expiration=expiration, + encoded_deposit_data=self.web3_client.keccak(bytes.fromhex('')), + action_type=ActionType.TRANSFER, + ) + to_signed_action_hash = self._generate_signed_action( + action_hash=action_hash_2, + nonce=nonce_2, + expiration=expiration, + ) + + print(f"To action hash: {action_hash_2.hex()}") + print(f"To signed action hash: {to_signed_action_hash}") + payload = { + "subaccount_id": self.subaccount_id, + "recipient_subaccount_id": to, + "sender_details": { + "nonce": nonce, + "signature": "string", + "signature_expiry_sec": expiration, + "signer": self.signer.address, + }, + "recipient_details": { + "nonce": nonce_2, + "signature": "string", + "signature_expiry_sec": expiration, + "signer": self.signer.address, + }, + "transfer": transfer, + } + payload['sender_details']['signature'] = from_signed_action_hash['signature'] + payload['recipient_details']['signature'] = to_signed_action_hash['signature'] + + print(payload) + headers = self._create_signature_headers() + response = requests.post(url, json=payload, headers=headers) + + print(response.json()) + + if "error" in response.json(): + raise Exception(response.json()["error"]) + if "result" not in response.json(): + raise Exception(f"Unable to transfer collateral {response.json()}") + return response.json()["result"] + + def encode_transfer(self, amount: int, to: str, asset_sub_id=0, signature_expiry=300): + """ + Encode the transfer + const encoder = ethers.AbiCoder.defaultAbiCoder(); + const TransferDataABI = ['(uint256,address,(address,uint256,int256)[])']; + const signature_expiry = getUTCEpochSec() + 300; + + const fromTransfers = [ + [ + assetAddress, + assetSubId, + ethers.parseUnits(amount, 18), // Amount in wei + ], + ]; + + const fromTransferData = [ + toAccount.subaccountId, + "0x0000000000000000000000000000000000000000", // manager (if new account)` + fromTransfers, + ]; + + const fromEncodedData = encoder.encode(TransferDataABI, [fromTransferData]); + """ + transfer_data_abi = ["(uint256,address,(address,uint256,int256)[])"] + + from_transfers = [ + [ + self.contracts["CASH_ASSET"], + asset_sub_id, + self.web3_client.to_wei(amount, 'ether'), + ] + ] + + from_transfer_data = [ + int(to), + "0x0000000000000000000000000000000000000000", + from_transfers, + ] + + from_encoded_data = eth_abi.encode(transfer_data_abi, [from_transfer_data]) + print(f"From transfers: {from_transfers}") + print(f"From transfer data: {from_transfer_data}") + print(f"From encoded data: {from_encoded_data.hex()}") + + # need to add the signature expiry + return self.web3_client.keccak(from_encoded_data) + + def _generate_action_hash( + self, + subaccount_id: int, + nonce: int, + expiration: int, + encoded_deposit_data: bytes, + action_type: ActionType = ActionType.DEPOSIT, + ): + """Handle the deposit to a new subaccount.""" + encoded_action_hash = eth_abi.encode( + ['bytes32', 'uint256', 'uint256', 'address', 'bytes32', 'uint256', 'address', 'address'], + [ + bytes.fromhex(self.contracts['ACTION_TYPEHASH'][2:]), + subaccount_id, + nonce, + self.contracts[f'{action_type.name}_MODULE_ADDRESS'], + encoded_deposit_data, + expiration, + self.wallet, + self.signer.address, + ], + ) + return self.web3_client.keccak(encoded_action_hash) + + def _generate_signed_action(self, action_hash: bytes, nonce: int, expiration: int): + """Generate the signed action.""" + encoded_typed_data_hash = "".join(['0x1901', self.contracts['DOMAIN_SEPARATOR'][2:], action_hash.hex()[2:]]) + typed_data_hash = self.web3_client.keccak(hexstr=encoded_typed_data_hash) + signature = self.signer.signHash(typed_data_hash).signature.hex() + return { + "nonce": nonce, + "signature": signature, + "signature_expiry_sec": expiration, + "signer": self.signer.address, + } + + def get_mmp_config(self, subaccount_id: int, currency: UnderlyingCurrency = None): + """Get the mmp config.""" + url = f"{self.contracts['BASE_URL']}/private/get_mmp_config" + payload = {"subaccount_id": self.subaccount_id} + if currency: + payload['currency'] = currency.name + headers = self._create_signature_headers() + response = requests.post(url, json=payload, headers=headers) + results = response.json()["result"] + return results + + def set_mmp_config( + self, + subaccount_id, + currency: UnderlyingCurrency, + mmp_frozen_time: int, + mmp_interval: int, + mmp_amount_limit: str, + mmp_delta_limit: str, + ): + """Set the mmp config.""" + url = f"{self.contracts['BASE_URL']}/private/set_mmp_config" + payload = { + "subaccount_id": subaccount_id, + "currency": currency.name, + "mmp_frozen_time": mmp_frozen_time, + "mmp_interval": mmp_interval, + "mmp_amount_limit": mmp_amount_limit, + "mmp_delta_limit": mmp_delta_limit, + } + headers = self._create_signature_headers() + response = requests.post(url, json=payload, headers=headers) + results = response.json()["result"] + return results diff --git a/lyra/constants.py b/lyra/constants.py index da75106..2431b6b 100644 --- a/lyra/constants.py +++ b/lyra/constants.py @@ -5,6 +5,7 @@ PUBLIC_HEADERS = {"accept": "application/json", "content-type": "application/json"} +TEST_PRIVATE_KEY = "0xc14f53ee466dd3fc5fa356897ab276acbef4f020486ec253a23b0d1c3f89d4f4" CONTRACTS = { Environment.TEST: { diff --git a/lyra/http_client.py b/lyra/http_client.py new file mode 100644 index 0000000..8af9d97 --- /dev/null +++ b/lyra/http_client.py @@ -0,0 +1,27 @@ +""" +Base class for HTTP client. +""" + +import time + +from eth_account.messages import encode_defunct +from lyra.base_client import BaseClient +from web3 import Web3 + + +class HttpClient(BaseClient): + + def _create_signature_headers(self): + """ + Create the signature headers + """ + timestamp = str(int(time.time() * 1000)) + msg = encode_defunct( + text=timestamp, + ) + signature = self.signer.sign_message(msg) + return { + "X-LyraWallet": self.wallet, + "X-LyraTimestamp": timestamp, + "X-LyraSignature": Web3.to_hex(signature.signature), + } diff --git a/lyra/lyra.py b/lyra/lyra.py index 2e45b1a..1e66f0e 100644 --- a/lyra/lyra.py +++ b/lyra/lyra.py @@ -1,35 +1,12 @@ """ Lyra is a Python library for trading on lyra v2 """ -import json -import random -import time -from datetime import datetime -import eth_abi - -# OPTION_NAME = 'ETH-PERP' -# OPTION_SUB_ID = '0' import pandas as pd -import requests -from eth_account.messages import encode_defunct -from rich import print from web3 import Web3 -from websocket import create_connection -from lyra.constants import CONTRACTS, PUBLIC_HEADERS -from lyra.enums import ( - ActionType, - CollateralAsset, - InstrumentType, - OrderSide, - OrderStatus, - OrderType, - SubaccountType, - TimeInForce, - UnderlyingCurrency, -) -from lyra.utils import get_logger +from lyra.base_client import BaseClient +from lyra.http_client import HttpClient # we set to show 4 decimal places pd.options.display.float_format = '{:,.4f}'.format @@ -39,656 +16,21 @@ def to_32byte_hex(val): return Web3.to_hex(Web3.to_bytes(val).rjust(32, b"\0")) -class LyraClient: +class LyraClient(BaseClient): """Client for the lyra dex.""" + http_client: HttpClient - def _create_signature_headers( - self, - ): - """ - Create the signature headers - """ - timestamp = str(int(time.time() * 1000)) - msg = encode_defunct( - text=timestamp, - ) - signature = self.signer.sign_message(msg) - return { - "X-LyraWallet": self.wallet, - "X-LyraTimestamp": timestamp, - "X-LyraSignature": Web3.to_hex(signature.signature), - } - - def __init__(self, private_key, env, logger=None, verbose=False, subaccount_id=None, wallet=None): - """ - Initialize the LyraClient class. - """ - self.verbose = verbose - self.env = env - self.contracts = CONTRACTS[env] - self.logger = logger or get_logger() - self.web3_client = Web3() - self.signer = self.web3_client.eth.account.from_key(private_key) - self.wallet = self.signer.address if not wallet else wallet - print(f"Signing address: {self.signer.address}") - if wallet: - print(f"Using wallet: {wallet}") - if not subaccount_id: - self.subaccount_id = self.fetch_subaccounts()['subaccount_ids'][0] - else: - self.subaccount_id = subaccount_id - print(f"Using subaccount id: {self.subaccount_id}") - self.ws = self.connect_ws() - self.login_client() - - def sign_authentication_header(self): - timestamp = str(int(time.time() * 1000)) - msg = encode_defunct( - text=timestamp, - ) - signature = self.web3_client.eth.account.sign_message( - msg, private_key=self.signer._private_key - ).signature.hex() # pylint: disable=protected-access - return { - 'wallet': self.wallet, - 'timestamp': str(timestamp), - 'signature': signature, - } - - def connect_ws(self): - ws = create_connection(self.contracts['WS_ADDRESS']) - return ws - - def create_account(self, wallet): - """Call the create account endpoint.""" - payload = {"wallet": wallet} - url = f"{self.contracts['BASE_URL']}/public/create_account" - result = requests.post( - headers=PUBLIC_HEADERS, - url=url, - json=payload, - ) - result_code = json.loads(result.content) - - if "error" in result_code: - raise Exception(result_code["error"]) - return True + def _create_signature_headers(self): + """Generate the signature headers.""" + return self.http_client._create_signature_headers() - def fetch_instruments( - self, - expired=False, - instrument_type: InstrumentType = InstrumentType.PERP, - currency: UnderlyingCurrency = UnderlyingCurrency.BTC, - ): - """ - Return the tickers. - First fetch all instrucments - Then get the ticket for all instruments. - """ - url = f"{self.contracts['BASE_URL']}/public/get_instruments" - payload = { - "expired": expired, - "instrument_type": instrument_type.value, - "currency": currency.name, - } - response = requests.post(url, json=payload, headers=PUBLIC_HEADERS) - results = response.json()["result"] - return results - def fetch_subaccounts(self): - """ - Returns the subaccounts for a given wallet - """ - url = f"{self.contracts['BASE_URL']}/private/get_subaccounts" - payload = {"wallet": self.wallet} - headers = self._create_signature_headers() - response = requests.post(url, json=payload, headers=headers) - results = json.loads(response.content)["result"] - return results - - def fetch_subaccount(self, subaccount_id): - """ - Returns information for a given subaccount - """ - url = f"{self.contracts['BASE_URL']}/private/get_subaccount" - payload = {"subaccount_id": subaccount_id} - headers = self._create_signature_headers() - response = requests.post(url, json=payload, headers=headers) - results = response.json()["result"] - return results - - def create_order( - self, - price, - amount, - instrument_name: str, - reduce_only=False, - side: OrderSide = OrderSide.BUY, - order_type: OrderType = OrderType.LIMIT, - time_in_force: TimeInForce = TimeInForce.GTC, - ): - """ - Create the order. - """ - if side.name.upper() not in OrderSide.__members__: - raise Exception(f"Invalid side {side}") - order = self._define_order( - instrument_name=instrument_name, - price=price, - amount=amount, - side=side, + def __init__(self, *args, **kwargs): + self.http_client = HttpClient( + *args, **kwargs, ) - _currency = UnderlyingCurrency[instrument_name.split("-")[0]] - if instrument_name.split("-")[1] == "PERP": - instruments = self.fetch_instruments(instrument_type=InstrumentType.PERP, currency=_currency) - instruments = {i['instrument_name']: i for i in instruments} - base_asset_sub_id = instruments[instrument_name]['base_asset_sub_id'] - instrument_type = InstrumentType.PERP - else: - instruments = self.fetch_instruments(instrument_type=InstrumentType.OPTION, currency=_currency) - instruments = {i['instrument_name']: i for i in instruments} - base_asset_sub_id = instruments[instrument_name]['base_asset_sub_id'] - instrument_type = InstrumentType.OPTION - - signed_order = self._sign_order(order, base_asset_sub_id, instrument_type, _currency) - response = self.submit_order(signed_order) - return response - - def _define_order( - self, - instrument_name: str, - price: float, - amount: float, - side: OrderSide, - time_in_force: TimeInForce = TimeInForce.GTC, - ): - """ - Define the order, in preparation for encoding and signing - """ - ts = int(datetime.now().timestamp() * 1000) - return { - 'instrument_name': instrument_name, - 'subaccount_id': self.subaccount_id, - 'direction': side.name.lower(), - 'limit_price': price, - 'amount': amount, - 'signature_expiry_sec': int(ts) + 3000, - 'max_fee': '200.01', - 'nonce': int(f"{int(ts)}{random.randint(100, 999)}"), - 'signer': self.signer.address, - 'order_type': 'limit', - 'mmp': False, - 'time_in_force': time_in_force.value, - 'signature': 'filled_in_below', - } - - def submit_order(self, order): - id = str(int(time.time())) - self.ws.send(json.dumps({'method': 'private/order', 'params': order, 'id': id})) - while True: - message = json.loads(self.ws.recv()) - if message['id'] == id: - try: - return message['result']['order'] - except KeyError as error: - print(message) - raise Exception(f"Unable to submit order {message}") from error - - def _encode_trade_data(self, order, base_asset_sub_id, instrument_type, currency): - encoded_data = eth_abi.encode( - ['address', 'uint256', 'int256', 'int256', 'uint256', 'uint256', 'bool'], - [ - self.contracts[f'{currency.name}_{instrument_type.name}_ADDRESS'], - int(base_asset_sub_id), - self.web3_client.to_wei(order['limit_price'], 'ether'), - self.web3_client.to_wei(order['amount'], 'ether'), - self.web3_client.to_wei(order['max_fee'], 'ether'), - order['subaccount_id'], - order['direction'] == 'buy', - ], - ) - - return self.web3_client.keccak(encoded_data) - - def _sign_order(self, order, base_asset_sub_id, instrument_type, currency): - trade_module_data = self._encode_trade_data(order, base_asset_sub_id, instrument_type, currency) - encoded_action_hash = eth_abi.encode( - ['bytes32', 'uint256', 'uint256', 'address', 'bytes32', 'uint256', 'address', 'address'], - [ - bytes.fromhex(self.contracts['ACTION_TYPEHASH'][2:]), - order['subaccount_id'], - order['nonce'], - self.contracts['TRADE_MODULE_ADDRESS'], - trade_module_data, - order['signature_expiry_sec'], - self.wallet, - order['signer'], - ], - ) - - action_hash = self.web3_client.keccak(encoded_action_hash) - encoded_typed_data_hash = "".join(['0x1901', self.contracts['DOMAIN_SEPARATOR'][2:], action_hash.hex()[2:]]) - typed_data_hash = self.web3_client.keccak(hexstr=encoded_typed_data_hash) - order['signature'] = self.signer.signHash(typed_data_hash).signature.hex() - return order - - def login_client( - self, - ): - login_request = { - 'method': 'public/login', - 'params': self.sign_authentication_header(), - 'id': str(int(time.time())), - } - self.ws.send(json.dumps(login_request)) - # we need to wait for the response - while True: - message = json.loads(self.ws.recv()) - if message['id'] == login_request['id']: - if "result" not in message: - raise Exception(f"Unable to login {message}") - break - - def fetch_ticker(self, instrument_name): - """ - Fetch the ticker for a given instrument name. - """ - url = f"{self.contracts['BASE_URL']}/public/get_ticker" - payload = {"instrument_name": instrument_name} - response = requests.post(url, json=payload, headers=PUBLIC_HEADERS) - results = json.loads(response.content)["result"] - return results - - def fetch_orders( - self, - instrument_name: str = None, - label: str = None, - page: int = 1, - page_size: int = 100, - status: OrderStatus = None, - ): - """ - Fetch the orders for a given instrument name. - """ - url = f"{self.contracts['BASE_URL']}/private/get_orders" - payload = {"instrument_name": instrument_name, "subaccount_id": self.subaccount_id} - for key, value in {"label": label, "page": page, "page_size": page_size, "status": status}.items(): - if value: - payload[key] = value - headers = self._create_signature_headers() - response = requests.post(url, json=payload, headers=headers) - results = response.json()["result"]['orders'] - return results - - def cancel(self, order_id, instrument_name): - """ - Cancel an order - """ - - id = str(int(time.time())) - payload = {"order_id": order_id, "subaccount_id": self.subaccount_id, "instrument_name": instrument_name} - self.ws.send(json.dumps({'method': 'private/cancel', 'params': payload, 'id': id})) - while True: - message = json.loads(self.ws.recv()) - if message['id'] == id: - return message['result'] - - def cancel_all(self): - """ - Cancel all orders - """ - id = str(int(time.time())) - payload = {"subaccount_id": self.subaccount_id} - self.ws.send(json.dumps({'method': 'private/cancel_all', 'params': payload, 'id': id})) - while True: - message = json.loads(self.ws.recv()) - if message['id'] == id: - return message['result'] - - def get_positions(self): - """ - Get positions - """ - url = f"{self.contracts['BASE_URL']}/private/get_positions" - payload = {"subaccount_id": self.subaccount_id} - headers = self._create_signature_headers() - response = requests.post(url, json=payload, headers=headers) - results = response.json()["result"]['positions'] - return results - - def get_collaterals(self): - """ - Get collaterals - """ - url = f"{self.contracts['BASE_URL']}/private/get_collaterals" - payload = {"subaccount_id": self.subaccount_id} - headers = self._create_signature_headers() - response = requests.post(url, json=payload, headers=headers) - results = response.json()["result"]['collaterals'] - return results.pop() - - def fetch_tickers( - self, - instrument_type: InstrumentType = InstrumentType.OPTION, - currency: UnderlyingCurrency = UnderlyingCurrency.BTC, - ): - """ - Fetch tickers using the ws connection - """ - instruments = self.fetch_instruments(instrument_type=instrument_type, currency=currency) - instrument_names = [i['instrument_name'] for i in instruments] - id_base = str(int(time.time())) - ids_to_instrument_names = { - f'{id_base}_{enumerate}': instrument_name for enumerate, instrument_name in enumerate(instrument_names) - } - for id, instrument_name in ids_to_instrument_names.items(): - payload = {"instrument_name": instrument_name} - self.ws.send(json.dumps({'method': 'public/get_ticker', 'params': payload, 'id': id})) - time.sleep(0.05) # otherwise we get rate limited... - results = {} - while ids_to_instrument_names: - message = json.loads(self.ws.recv()) - if message['id'] in ids_to_instrument_names: - results[message['result']['instrument_name']] = message['result'] - del ids_to_instrument_names[message['id']] - return results - - def create_subaccount( - self, - amount=0, - subaccount_type: SubaccountType = SubaccountType.STANDARD, - collateral_asset: CollateralAsset = CollateralAsset.USDC, - underlying_currency: UnderlyingCurrency = UnderlyingCurrency.ETH, - ): - """ - Create a subaccount. - """ - url = f"{self.contracts['BASE_URL']}/private/create_subaccount" - _, nonce, expiration = self.get_nonce_and_signature_expiry() - if subaccount_type is SubaccountType.STANDARD: - contract_key = f"{subaccount_type.name}_RISK_MANAGER_ADDRESS" - elif subaccount_type is SubaccountType.PORTFOLIO: - if not collateral_asset: - raise Exception("Underlying currency must be provided for portfolio subaccounts") - contract_key = f"{underlying_currency.name}_{subaccount_type.name}_RISK_MANAGER_ADDRESS" - else: - raise Exception(f"Invalid subaccount type {subaccount_type}") - payload = { - "amount": f"{amount}", - "asset_name": collateral_asset.name, - "margin_type": "SM" if subaccount_type is SubaccountType.STANDARD else "PM", - 'nonce': nonce, - "signature": "string", - "signature_expiry_sec": expiration, - "signer": self.signer.address, - "wallet": self.wallet, - } - if subaccount_type is SubaccountType.PORTFOLIO: - payload['currency'] = underlying_currency.name - encoded_deposit_data = self._encode_deposit_data( - amount=amount, - contract_key=contract_key, - ) - action_hash = self._generate_action_hash( - subaccount_id=0, # as we are depositing to a new subaccount. - nonce=nonce, - expiration=expiration, - encoded_deposit_data=encoded_deposit_data, - ) - - typed_data_hash = self._generate_typed_data_hash( - action_hash=action_hash, - ) - - signature = self.signer.signHash(typed_data_hash).signature.hex() - payload['signature'] = signature - print(f"Payload: {payload}") + super().__init__(*args, **kwargs) - headers = self._create_signature_headers() - response = requests.post(url, json=payload, headers=headers) - - if "error" in response.json(): - raise Exception(response.json()["error"]) - print(response.text) - if "result" not in response.json(): - raise Exception(f"Unable to create subaccount {response.json()}") - return response.json()["result"] - - def _encode_deposit_data(self, amount: int, contract_key: str): - """Encode the deposit data""" - - encoded_data = eth_abi.encode( - ['uint256', 'address', 'address'], - [ - int(amount * 1e6), - self.contracts["CASH_ASSET"], - self.contracts[contract_key], - ], - ) - print(f"Encoded data: {encoded_data}") - return self.web3_client.keccak(encoded_data) - - def get_nonce_and_signature_expiry(self): - """ - Returns the nonce and signature expiry - """ - ts = int(datetime.now().timestamp() * 1000) - nonce = int(f"{int(ts)}{random.randint(100, 999)}") - expiration = int(ts) + 6000 - return ts, nonce, expiration - - def _generate_typed_data_hash( - self, - action_hash: bytes, - ): - """Generate the typed data hash.""" - - encoded_typed_data_hash = "".join(['0x1901', self.contracts['DOMAIN_SEPARATOR'][2:], action_hash.hex()[2:]]) - typed_data_hash = self.web3_client.keccak(hexstr=encoded_typed_data_hash) - return typed_data_hash - - def transfer_collateral(self, amount: int, to: str, asset: CollateralAsset): - """ - Transfer collateral - """ - - ts = int(datetime.now().timestamp() * 1000) - nonce = int(f"{int(ts)}{random.randint(100, 499)}") - nonce_2 = int(f"{int(ts)}{random.randint(500, 999)}") - expiration = int(datetime.now().timestamp() + 10000) - - url = f"{self.contracts['BASE_URL']}/private/transfer_erc20" - _, nonce, expiration = self.get_nonce_and_signature_expiry() - transfer = { - "address": self.contracts["CASH_ASSET"], - "amount": int(amount), - "sub_id": 0, - } - print(f"Transfering to {to} amount {amount} asset {asset.name}") - - encoded_data = self.encode_transfer( - amount=amount, - to=to, - ) - - action_hash_1 = self._generate_action_hash( - subaccount_id=self.subaccount_id, - nonce=nonce, - expiration=expiration, - encoded_deposit_data=encoded_data, - action_type=ActionType.TRANSFER, - ) - - from_signed_action_hash = self._generate_signed_action( - action_hash=action_hash_1, - nonce=nonce, - expiration=expiration, - ) - - print(f"from_signed_action_hash: {from_signed_action_hash}") - print(f"From action hash: {action_hash_1.hex()}") - - action_hash_2 = self._generate_action_hash( - subaccount_id=to, - nonce=nonce_2, - expiration=expiration, - encoded_deposit_data=self.web3_client.keccak(bytes.fromhex('')), - action_type=ActionType.TRANSFER, - ) - to_signed_action_hash = self._generate_signed_action( - action_hash=action_hash_2, - nonce=nonce_2, - expiration=expiration, - ) - - print(f"To action hash: {action_hash_2.hex()}") - print(f"To signed action hash: {to_signed_action_hash}") - payload = { - "subaccount_id": self.subaccount_id, - "recipient_subaccount_id": to, - "sender_details": { - "nonce": nonce, - "signature": "string", - "signature_expiry_sec": expiration, - "signer": self.signer.address, - }, - "recipient_details": { - "nonce": nonce_2, - "signature": "string", - "signature_expiry_sec": expiration, - "signer": self.signer.address, - }, - "transfer": transfer, - } - payload['sender_details']['signature'] = from_signed_action_hash['signature'] - payload['recipient_details']['signature'] = to_signed_action_hash['signature'] - - print(payload) - headers = self._create_signature_headers() - response = requests.post(url, json=payload, headers=headers) - - print(response.json()) - - if "error" in response.json(): - raise Exception(response.json()["error"]) - if "result" not in response.json(): - raise Exception(f"Unable to transfer collateral {response.json()}") - return response.json()["result"] - - def encode_transfer(self, amount: int, to: str, asset_sub_id=0, signature_expiry=300): - """ - Encode the transfer - const encoder = ethers.AbiCoder.defaultAbiCoder(); - const TransferDataABI = ['(uint256,address,(address,uint256,int256)[])']; - const signature_expiry = getUTCEpochSec() + 300; - - const fromTransfers = [ - [ - assetAddress, - assetSubId, - ethers.parseUnits(amount, 18), // Amount in wei - ], - ]; - - const fromTransferData = [ - toAccount.subaccountId, - "0x0000000000000000000000000000000000000000", // manager (if new account)` - fromTransfers, - ]; - - const fromEncodedData = encoder.encode(TransferDataABI, [fromTransferData]); - """ - transfer_data_abi = ["(uint256,address,(address,uint256,int256)[])"] - - from_transfers = [ - [ - self.contracts["CASH_ASSET"], - asset_sub_id, - self.web3_client.to_wei(amount, 'ether'), - ] - ] - - from_transfer_data = [ - int(to), - "0x0000000000000000000000000000000000000000", - from_transfers, - ] - - from_encoded_data = eth_abi.encode(transfer_data_abi, [from_transfer_data]) - print(f"From transfers: {from_transfers}") - print(f"From transfer data: {from_transfer_data}") - print(f"From encoded data: {from_encoded_data.hex()}") - - # need to add the signature expiry - return self.web3_client.keccak(from_encoded_data) - - def _generate_action_hash( - self, - subaccount_id: int, - nonce: int, - expiration: int, - encoded_deposit_data: bytes, - action_type: ActionType = ActionType.DEPOSIT, - ): - """Handle the deposit to a new subaccount.""" - encoded_action_hash = eth_abi.encode( - ['bytes32', 'uint256', 'uint256', 'address', 'bytes32', 'uint256', 'address', 'address'], - [ - bytes.fromhex(self.contracts['ACTION_TYPEHASH'][2:]), - subaccount_id, - nonce, - self.contracts[f'{action_type.name}_MODULE_ADDRESS'], - encoded_deposit_data, - expiration, - self.wallet, - self.signer.address, - ], - ) - return self.web3_client.keccak(encoded_action_hash) - def _generate_signed_action(self, action_hash: bytes, nonce: int, expiration: int): - """Generate the signed action.""" - encoded_typed_data_hash = "".join(['0x1901', self.contracts['DOMAIN_SEPARATOR'][2:], action_hash.hex()[2:]]) - typed_data_hash = self.web3_client.keccak(hexstr=encoded_typed_data_hash) - signature = self.signer.signHash(typed_data_hash).signature.hex() - return { - "nonce": nonce, - "signature": signature, - "signature_expiry_sec": expiration, - "signer": self.signer.address, - } - def get_mmp_config(self, subaccount_id: int, currency: UnderlyingCurrency = None): - """Get the mmp config.""" - url = f"{self.contracts['BASE_URL']}/private/get_mmp_config" - payload = {"subaccount_id": self.subaccount_id} - if currency: - payload['currency'] = currency.name - headers = self._create_signature_headers() - response = requests.post(url, json=payload, headers=headers) - results = response.json()["result"] - return results - def set_mmp_config( - self, - subaccount_id, - currency: UnderlyingCurrency, - mmp_frozen_time: int, - mmp_interval: int, - mmp_amount_limit: str, - mmp_delta_limit: str, - ): - """Set the mmp config.""" - url = f"{self.contracts['BASE_URL']}/private/set_mmp_config" - payload = { - "subaccount_id": subaccount_id, - "currency": currency.name, - "mmp_frozen_time": mmp_frozen_time, - "mmp_interval": mmp_interval, - "mmp_amount_limit": mmp_amount_limit, - "mmp_delta_limit": mmp_delta_limit, - } - headers = self._create_signature_headers() - response = requests.post(url, json=payload, headers=headers) - results = response.json()["result"] - return results diff --git a/lyra/utils.py b/lyra/utils.py index e054762..b85e299 100644 --- a/lyra/utils.py +++ b/lyra/utils.py @@ -3,9 +3,13 @@ """ import logging import sys +import os from rich.logging import RichHandler +# install_location = os.path.dirname(os.path.realpath(__file__)) +# sys.path.append(install_location) + def get_logger(): """Get the logger.""" diff --git a/lyra/ws_client.py b/lyra/ws_client.py new file mode 100644 index 0000000..43ff727 --- /dev/null +++ b/lyra/ws_client.py @@ -0,0 +1,32 @@ +""" +Class to handle base websocket client +""" + +import json +import time + +from eth_account.messages import encode_defunct +import requests +from lyra.base_client import BaseClient +from web3 import Web3 + +from lyra.constants import PUBLIC_HEADERS + + +class WsClient(BaseClient): + + def _create_signature_headers(self): + """ + Create the signature headers + """ + timestamp = str(int(time.time() * 1000)) + msg = encode_defunct( + text=timestamp, + ) + signature = self.signer.sign_message(msg) + return { + "X-LyraWallet": self.wallet, + "X-LyraTimestamp": timestamp, + "X-LyraSignature": Web3.to_hex(signature.signature), + } + diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..162cb7f --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,33 @@ +""" +Conftest for lyra tests +""" + +from unittest.mock import MagicMock + +import pytest + +from lyra.enums import ( + Environment, +) +from lyra.lyra import LyraClient +from lyra.utils import get_logger + + +TEST_WALLET = "0x3A5c777edf22107d7FdFB3B02B0Cdfe8b75f3453" +TEST_PRIVATE_KEY = "0xc14f53ee466dd3fc5fa356897ab276acbef4f020486ec253a23b0d1c3f89d4f4" + + +def freeze_time(lyra_client): + ts = 1705439697008 + nonce = 17054396970088651 + expiration = 1705439703008 + lyra_client.get_nonce_and_signature_expiry = MagicMock(return_value=(ts, nonce, expiration)) + return lyra_client + + +@pytest.fixture +def lyra_client(): + lyra_client = LyraClient(TEST_PRIVATE_KEY, env=Environment.TEST, logger=get_logger()) + lyra_client.subaccount_id = 5 + yield lyra_client + lyra_client.cancel_all() diff --git a/tests/test_main.py b/tests/test_main.py index b9c25e4..3f144bc 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -5,7 +5,6 @@ import time from datetime import datetime from itertools import product -from unittest.mock import MagicMock import pytest import requests @@ -14,34 +13,14 @@ from lyra.enums import ( ActionType, CollateralAsset, - Environment, InstrumentType, OrderSide, OrderType, SubaccountType, UnderlyingCurrency, ) -from lyra.lyra import LyraClient -from lyra.utils import get_logger +from tests.conftest import TEST_WALLET -TEST_WALLET = "0x3A5c777edf22107d7FdFB3B02B0Cdfe8b75f3453" -TEST_PRIVATE_KEY = "0xc14f53ee466dd3fc5fa356897ab276acbef4f020486ec253a23b0d1c3f89d4f4" - - -def freeze_time(lyra_client): - ts = 1705439697008 - nonce = 17054396970088651 - expiration = 1705439703008 - lyra_client.get_nonce_and_signature_expiry = MagicMock(return_value=(ts, nonce, expiration)) - return lyra_client - - -@pytest.fixture -def lyra_client(): - lyra_client = LyraClient(TEST_PRIVATE_KEY, env=Environment.TEST, logger=get_logger()) - lyra_client.subaccount_id = 5 - yield lyra_client - lyra_client.cancel_all() def test_lyra_client(lyra_client): diff --git a/tests/test_rfq.py b/tests/test_rfq.py new file mode 100644 index 0000000..286f138 --- /dev/null +++ b/tests/test_rfq.py @@ -0,0 +1,57 @@ +""" +Implement tests for the RFQ class. +""" + + +from lyra.enums import OrderSide + + +LEG_1_NAME = 'ETH-20240329-2400-C' +LEG_2_NAME = 'ETH-20240329-2600-C' + +LEGS_TO_SUB_ID: any = { + 'ETH-20240329-2400-C': '39614082287924319838483674368', + 'ETH-20240329-2600-C': '39614082373823665758483674368' +} + + + +from dataclasses import asdict, dataclass + +@dataclass +class Leg: + instrument_name: str + amount: str + direction: str + + +@dataclass +class Rfq: + subaccount_id: str + leg_1: Leg + leg_2: Leg + + def to_dict(self): + return { + "legs": [ + asdict(self.leg_1), + asdict(self.leg_2) + ], + "subaccount_id": self.subaccount_id + } + + +def test_lyra_client_create_rfq(lyra_client, instrument_type, currency): + """ + Test the LyraClient class. + """ + + subaccount_id = lyra_client.subaccount_id + leg_1 = Leg(instrument_name=LEG_1_NAME, amount='1', direction=OrderSide.BUY) + leg_2 = Leg(instrument_name=LEG_2_NAME, amount='1', direction=OrderSide.SELL) + rfq = Rfq(leg_1=leg_1, leg_2=leg_2, subaccount_id=subaccount_id) + + + assert lyra_client.create_rfq(rfq.to_dict()) + + From 86141b0874e1c742ce63b1e9ba223543663bc100 Mon Sep 17 00:00:00 2001 From: 8baller Date: Tue, 19 Mar 2024 10:40:12 +0000 Subject: [PATCH 2/2] feat: added in async along with basic rfq steps --- lyra/async_client.py | 73 ++-- lyra/autonomy/packages/__init__.py | 0 .../eightballer/protocols/markets/__init__.py | 29 -- .../protocols/markets/custom_types.py | 212 ----------- .../protocols/markets/dialogues.py | 153 -------- .../protocols/markets/markets.proto | 94 ----- .../protocols/markets/markets_pb2.py | 201 ----------- .../eightballer/protocols/markets/message.py | 335 ------------------ .../protocols/markets/protocol.yaml | 21 -- .../protocols/markets/serialization.py | 164 --------- .../markets/tests/test_markets_dialogues.py | 49 --- .../markets/tests/test_markets_messages.py | 227 ------------ lyra/autonomy/packages/packages.json | 6 - lyra/base_client.py | 172 ++++++--- lyra/cli.py | 1 + lyra/enums.py | 6 + lyra/http_client.py | 4 +- lyra/lyra.py | 9 +- lyra/utils.py | 4 - lyra/ws_client.py | 7 +- tests/conftest.py | 13 +- tests/test_main.py | 3 +- tests/test_rfq.py | 61 +++- 23 files changed, 242 insertions(+), 1602 deletions(-) delete mode 100644 lyra/autonomy/packages/__init__.py delete mode 100644 lyra/autonomy/packages/eightballer/protocols/markets/__init__.py delete mode 100644 lyra/autonomy/packages/eightballer/protocols/markets/custom_types.py delete mode 100644 lyra/autonomy/packages/eightballer/protocols/markets/dialogues.py delete mode 100644 lyra/autonomy/packages/eightballer/protocols/markets/markets.proto delete mode 100644 lyra/autonomy/packages/eightballer/protocols/markets/markets_pb2.py delete mode 100644 lyra/autonomy/packages/eightballer/protocols/markets/message.py delete mode 100644 lyra/autonomy/packages/eightballer/protocols/markets/protocol.yaml delete mode 100644 lyra/autonomy/packages/eightballer/protocols/markets/serialization.py delete mode 100644 lyra/autonomy/packages/eightballer/protocols/markets/tests/test_markets_dialogues.py delete mode 100644 lyra/autonomy/packages/eightballer/protocols/markets/tests/test_markets_messages.py delete mode 100644 lyra/autonomy/packages/packages.json diff --git a/lyra/async_client.py b/lyra/async_client.py index ce5dfe2..3811161 100644 --- a/lyra/async_client.py +++ b/lyra/async_client.py @@ -8,15 +8,9 @@ import time import traceback -from lyra.constants import PUBLIC_HEADERS from lyra.enums import InstrumentType, UnderlyingCurrency from lyra.ws_client import WsClient as BaseClient -from multiprocessing import Process -# we need a thread safe way to collect the events -from multiprocessing import Lock -from threading import Thread - class AsyncClient(BaseClient): """ @@ -29,8 +23,6 @@ class AsyncClient(BaseClient): listener = None subscribing = False - - async def fetch_ticker(self, instrument_name: str): """ Fetch the ticker for a symbol @@ -44,12 +36,9 @@ async def fetch_ticker(self, instrument_name: str): response = self.ws.recv() response = json.loads(response) if response["id"] == id: - close = float(response["result"]["best_bid_price"]) + float(response["result"]["best_ask_price"]) / 2 + close = float(response["result"]["best_bid_price"]) + float(response["result"]["best_ask_price"]) / 2 response["result"]["close"] = close return response["result"] - - - async def subscribe(self, instrument_name: str, group: str = "1", depth: str = "100"): """ @@ -59,24 +48,16 @@ async def subscribe(self, instrument_name: str, group: str = "1", depth: str = " self.subscribing = True if instrument_name not in self.current_subscriptions: channel = f"orderbook.{instrument_name}.{group}.{depth}" - msg = json.dumps({ - "method": "subscribe", - "params": { - "channels": [channel] - } - - }) + msg = json.dumps({"method": "subscribe", "params": {"channels": [channel]}}) print(f"Subscribing with {msg}") self.ws.send(msg) await self.collect_events(instrument_name=instrument_name) print(f"Subscribed to {instrument_name}") return - + while instrument_name not in self.current_subscriptions: await asyncio.sleep(1) return self.current_subscriptions[instrument_name] - - async def collect_events(self, subscription: str = None, instrument_name: str = None): """Use a thread to check the subscriptions""" @@ -97,7 +78,6 @@ async def collect_events(self, subscription: str = None, instrument_name: str = self.subscribing = False return - channel = response["params"]["channel"] bids = response['params']['data']['bids'] @@ -125,11 +105,10 @@ async def watch_order_book(self, instrument_name: str, group: str = "1", depth: Watch the order book for a symbol orderbook.{instrument_name}.{group}.{depth} """ - + if not self.subscribing: await self.subscribe(instrument_name, group, depth) - if not self.listener: print(f"Started listener for {instrument_name}") self.listener = True @@ -141,8 +120,12 @@ async def watch_order_book(self, instrument_name: str, group: str = "1", depth: return self.current_subscriptions[instrument_name] - - async def fetch_instruments(self, expired=False, instrument_type: InstrumentType = InstrumentType.PERP, currency: UnderlyingCurrency = UnderlyingCurrency.BTC): + async def fetch_instruments( + self, + expired=False, + instrument_type: InstrumentType = InstrumentType.PERP, + currency: UnderlyingCurrency = UnderlyingCurrency.BTC, + ): return super().fetch_instruments(expired, instrument_type, currency) async def close(self): @@ -151,4 +134,38 @@ async def close(self): """ self.ws.close() # if self.listener: - # self.listener.join() \ No newline at end of file + # self.listener.join() + + async def fetch_tickers( + self, + instrument_type: InstrumentType = InstrumentType.OPTION, + currency: UnderlyingCurrency = UnderlyingCurrency.BTC, + ): + instruments = await self.fetch_instruments(instrument_type=instrument_type, currency=currency) + instrument_names = [i['instrument_name'] for i in instruments] + id_base = str(int(time.time())) + ids_to_instrument_names = { + f'{id_base}_{enumerate}': instrument_name for enumerate, instrument_name in enumerate(instrument_names) + } + for id, instrument_name in ids_to_instrument_names.items(): + payload = {"instrument_name": instrument_name} + self.ws.send(json.dumps({'method': 'public/get_ticker', 'params': payload, 'id': id})) + await asyncio.sleep(0.05) # otherwise we get rate limited... + results = {} + while ids_to_instrument_names: + message = json.loads(self.ws.recv()) + if message['id'] in ids_to_instrument_names: + results[message['result']['instrument_name']] = message['result'] + del ids_to_instrument_names[message['id']] + return results + + async def get_collaterals(self): + return super().get_collaterals() + + async def get_positions(self, currency: UnderlyingCurrency = UnderlyingCurrency.BTC): + return super().get_positions() + + async def get_open_orders(self, status, currency: UnderlyingCurrency = UnderlyingCurrency.BTC): + return super().fetch_orders( + status=status, + ) diff --git a/lyra/autonomy/packages/__init__.py b/lyra/autonomy/packages/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/lyra/autonomy/packages/eightballer/protocols/markets/__init__.py b/lyra/autonomy/packages/eightballer/protocols/markets/__init__.py deleted file mode 100644 index 33f36fd..0000000 --- a/lyra/autonomy/packages/eightballer/protocols/markets/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2023 eightballer -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -""" -This module contains the support resources for the markets protocol. - -It was created with protocol buffer compiler version `libprotoc 3.19.4` and aea protocol generator version `1.0.0`. -""" - -from packages.eightballer.protocols.markets.message import MarketsMessage -from packages.eightballer.protocols.markets.serialization import MarketsSerializer - -MarketsMessage.serializer = MarketsSerializer diff --git a/lyra/autonomy/packages/eightballer/protocols/markets/custom_types.py b/lyra/autonomy/packages/eightballer/protocols/markets/custom_types.py deleted file mode 100644 index a6d7527..0000000 --- a/lyra/autonomy/packages/eightballer/protocols/markets/custom_types.py +++ /dev/null @@ -1,212 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2023 eightballer -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains class representations corresponding to every custom type in the protocol specification.""" - - -from dataclasses import dataclass -from enum import Enum -from typing import Any, Dict, List, Optional - - -class ErrorCode(Enum): - """This class represents an instance of ErrorCode.""" - - UNSUPPORTED_PROTOCOL = 0 - DECODING_ERROR = 1 - INVALID_MESSAGE = 2 - UNSUPPORTED_SKILL = 3 - INVALID_DIALOGUE = 4 - - @staticmethod - def encode(error_code_protobuf_object: Any, error_code_object: "ErrorCode") -> None: - """ - Encode an instance of this class into the protocol buffer object. - The protocol buffer object in the error_code_protobuf_object argument is matched with the instance of this class in the 'error_code_object' argument. - :param error_code_protobuf_object: the protocol buffer object whose type corresponds with this class. - :param error_code_object: an instance of this class to be encoded in the protocol buffer object. - """ - error_code_protobuf_object.error_code = error_code_object.value - - @classmethod - def decode(cls, error_code_protobuf_object: Any) -> "ErrorCode": - """ - Decode a protocol buffer object that corresponds with this class into an instance of this class. - A new instance of this class is created that matches the protocol buffer object in the 'error_code_protobuf_object' argument. - :param error_code_protobuf_object: the protocol buffer object whose type corresponds with this class. - :return: A new instance of this class that matches the protocol buffer object in the 'error_code_protobuf_object' argument. - """ - enum_value_from_pb2 = error_code_protobuf_object.error_code - return ErrorCode(enum_value_from_pb2) - - -@dataclass -class Market: - """ - This class represents an instance of Market. - """ - - id: str - lowercaseId: Optional[str] = None - symbol: Optional[str] = None - base: Optional[str] = None - quote: Optional[str] = None - settle: Optional[str] = None - baseId: Optional[str] = None - quoteId: Optional[str] = None - settleId: Optional[str] = None - type: Optional[str] = None - spot: Optional[bool] = None - margin: Optional[bool] = None - swap: Optional[bool] = None - future: Optional[bool] = None - option: Optional[bool] = None - active: Optional[bool] = None - contract: Optional[bool] = None - linear: Optional[bool] = None - inverse: Optional[bool] = None - taker: Optional[float] = None - maker: Optional[float] = None - contractSize: Optional[float] = None - expiry: Optional[float] = None - expiryDatetime: Optional[str] = None - strike: Optional[float] = None - optionType: Optional[str] = None - precision: Optional[float] = None - limits: Optional[str] = None - info: Optional[Dict[str, Any]] = None - exchange_id: Optional[str] = None - created: Optional[str] = None - - @staticmethod - def encode(market_protobuf_object, market_object: "Market") -> None: - """ - Encode an instance of this class into the protocol buffer object. - - The protocol buffer object in the market_protobuf_object argument is matched with the instance of this class in the 'market_object' argument. - - :param market_protobuf_object: the protocol buffer object whose type corresponds with this class. - :param market_object: an instance of this class to be encoded in the protocol buffer object. - """ - for ( - attribute - ) in Market.__dataclass_fields__.keys(): # pylint: disable=no-member - if hasattr(market_object, attribute): - value = getattr(market_object, attribute) - setattr(market_protobuf_object.Market, attribute, value) - else: - setattr(market_protobuf_object.Market, attribute, None) - - @classmethod - def decode(cls, market_protobuf_object) -> "Market": - """ - Decode a protocol buffer object that corresponds with this class into an instance of this class. - - A new instance of this class is created that matches the protocol buffer object in the 'market_protobuf_object' argument. - - :param market_protobuf_object: the protocol buffer object whose type corresponds with this class. - :return: A new instance of this class that matches the protocol buffer object in the 'market_protobuf_object' argument. - """ - attribute_dict = dict() - for ( - attribute - ) in Market.__dataclass_fields__.keys(): # pylint: disable=no-member - if hasattr(market_protobuf_object.Market, attribute): - if getattr(market_protobuf_object.Market, attribute) is not None: - attribute_dict[attribute] = getattr( - market_protobuf_object.Market, attribute - ) - return cls(**attribute_dict) - - def __eq__(self, other): - if isinstance(other, Market): - set_of_self_attributue = set( - i - for i in Market.__dataclass_fields__.keys() # pylint: disable=no-member - if getattr(self, i) is not None - ) - set_of_other_attributue = set( - i - for i in Market.__dataclass_fields__.keys() # pylint: disable=no-member - if getattr(other, i) is not None - ) - return set_of_self_attributue == set_of_other_attributue - return False - - def to_json(self): - """TO a pretty dictionary string.""" - result = {} - for ( - attribute - ) in Market.__dataclass_fields__.keys(): # pylint: disable=no-member - if hasattr(self, attribute): - value = getattr(self, attribute) - if value is not None: - result[attribute] = value - return result - - -@dataclass -class Markets: - """This class represents an instance of Markets.""" - - markets: List[Market] - - @staticmethod - def encode(markets_protobuf_object, markets_object: "Markets") -> None: - """ - Encode an instance of this class into the protocol buffer object. - - The protocol buffer object in the markets_protobuf_object argument is matched with the instance of this class in the 'markets_object' argument. - - :param markets_protobuf_object: the protocol buffer object whose type corresponds with this class. - :param markets_object: an instance of this class to be encoded in the protocol buffer object. - """ - if markets_protobuf_object is None: - raise ValueError( - "The protocol buffer object 'markets_protobuf_object' is not initialized." - ) - markets_protobuf_object.Markets.markets = markets_object.markets - - @classmethod - def decode(cls, markets_protobuf_object) -> "Markets": - """ - Decode a protocol buffer object that corresponds with this class into an instance of this class. - - A new instance of this class is created that matches the protocol buffer object in the 'markets_protobuf_object' argument. - - :param markets_protobuf_object: the protocol buffer object whose type corresponds with this class. - :return: A new instance of this class that matches the protocol buffer object in the 'markets_protobuf_object' argument. - """ - return cls(markets_protobuf_object.Markets.markets) - - def __eq__(self, other): - if isinstance(other, Markets): - set_of_self_attributue = set( - i - for i in Markets.__dataclass_fields__.keys() # pylint: disable=no-member - if getattr(self, i) is not None - ) - set_of_other_attributue = set( - i - for i in Markets.__dataclass_fields__.keys() # pylint: disable=no-member - if getattr(other, i) is not None - ) - return set_of_self_attributue == set_of_other_attributue - return False diff --git a/lyra/autonomy/packages/eightballer/protocols/markets/dialogues.py b/lyra/autonomy/packages/eightballer/protocols/markets/dialogues.py deleted file mode 100644 index 327f779..0000000 --- a/lyra/autonomy/packages/eightballer/protocols/markets/dialogues.py +++ /dev/null @@ -1,153 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2023 eightballer -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -""" -This module contains the classes required for markets dialogue management. - -- MarketsDialogue: The dialogue class maintains state of a dialogue and manages it. -- MarketsDialogues: The dialogues class keeps track of all dialogues. -""" - -from abc import ABC -from typing import Callable, Dict, FrozenSet, Optional, Type, cast - -from aea.common import Address -from aea.protocols.base import Message -from aea.protocols.dialogue.base import Dialogue, DialogueLabel, Dialogues -from aea.skills.base import Model - -from packages.eightballer.protocols.markets.message import MarketsMessage - - -class MarketsDialogue(Dialogue): - """The markets dialogue class maintains state of a dialogue and manages it.""" - - INITIAL_PERFORMATIVES: FrozenSet[Message.Performative] = frozenset( - {MarketsMessage.Performative.GET_MARKET, - MarketsMessage.Performative.GET_ALL_MARKETS} - ) - TERMINAL_PERFORMATIVES: FrozenSet[Message.Performative] = frozenset( - { - MarketsMessage.Performative.ALL_MARKETS, - MarketsMessage.Performative.MARKET, - MarketsMessage.Performative.ERROR, - } - ) - VALID_REPLIES: Dict[Message.Performative, FrozenSet[Message.Performative]] = { - MarketsMessage.Performative.ALL_MARKETS: frozenset(), - MarketsMessage.Performative.ERROR: frozenset(), - MarketsMessage.Performative.GET_ALL_MARKETS: frozenset( - {MarketsMessage.Performative.ALL_MARKETS, MarketsMessage.Performative.ERROR} - ), - MarketsMessage.Performative.GET_MARKET: frozenset( - {MarketsMessage.Performative.MARKET, MarketsMessage.Performative.ERROR} - ), - MarketsMessage.Performative.MARKET: frozenset(), - } - - class Role(Dialogue.Role): - """This class defines the agent's role in a markets dialogue.""" - - AGENT = "agent" - - class EndState(Dialogue.EndState): - """This class defines the end states of a markets dialogue.""" - - MARKET = 0 - ALL_MARKETS = 1 - ERROR = 2 - - def __init__( - self, - dialogue_label: DialogueLabel, - self_address: Address, - role: Dialogue.Role, - message_class: Type[MarketsMessage] = MarketsMessage, - ) -> None: - """ - Initialize a dialogue. - - :param dialogue_label: the identifier of the dialogue - :param self_address: the address of the entity for whom this dialogue is maintained - :param role: the role of the agent this dialogue is maintained for - :param message_class: the message class used - """ - Dialogue.__init__( - self, - dialogue_label=dialogue_label, - message_class=message_class, - self_address=self_address, - role=role, - ) - - -class BaseMarketsDialogues(Dialogues, ABC): - """This class keeps track of all markets dialogues.""" - - END_STATES = frozenset( - { - MarketsDialogue.EndState.MARKET, - MarketsDialogue.EndState.ALL_MARKETS, - MarketsDialogue.EndState.ERROR, - } - ) - - _keep_terminal_state_dialogues = False - - def __init__( - self, - self_address: Address, - role_from_first_message: Optional[ - Callable[[Message, Address], Dialogue.Role] - ] = None, - dialogue_class: Type[MarketsDialogue] = MarketsDialogue, - ) -> None: - """ - Initialize dialogues. - - :param self_address: the address of the entity for whom dialogues are maintained - :param dialogue_class: the dialogue class used - :param role_from_first_message: the callable determining role from first message - """ - del role_from_first_message - - def _role_from_first_message( - message: Message, sender: Address - ) -> Dialogue.Role: # pylint: - """Infer the role of the agent from an incoming/outgoing first message.""" - del sender, message - return MarketsDialogue.Role.AGENT - - Dialogues.__init__( - self, - self_address=self_address, - end_states=cast(FrozenSet[Dialogue.EndState], self.END_STATES), - message_class=MarketsMessage, - dialogue_class=dialogue_class, - role_from_first_message=_role_from_first_message, - ) - - -class MarketsDialogues(BaseMarketsDialogues, Model): - """Dialogue class for Markets.""" - - def __init__(self, **kwargs): - """Initialize the Dialogue.""" - Model.__init__(self, keep_terminal_state_dialogues=False, **kwargs) - BaseMarketsDialogues.__init__(self, self_address=str(self.context.skill_id)) diff --git a/lyra/autonomy/packages/eightballer/protocols/markets/markets.proto b/lyra/autonomy/packages/eightballer/protocols/markets/markets.proto deleted file mode 100644 index 9f9aa86..0000000 --- a/lyra/autonomy/packages/eightballer/protocols/markets/markets.proto +++ /dev/null @@ -1,94 +0,0 @@ -syntax = "proto3"; - -package aea.eightballer.markets.v0_1_0; - -message MarketsMessage{ - - // Custom Types - message ErrorCode{ - enum ErrorCodeEnum { - UNSUPPORTED_PROTOCOL = 0; - DECODING_ERROR = 1; - INVALID_MESSAGE = 2; - UNSUPPORTED_SKILL = 3; - INVALID_DIALOGUE = 4; - } - ErrorCodeEnum error_code = 1; - } - - message Market{ - message Market { - string id = 1; - string lowercaseId = 2; - string symbol = 3; - string base = 4; - string quote = 5; - string settle = 6; - string baseId = 7; - string quoteId = 8; - string settleId = 9; - string type = 10; - bool spot = 11; - bool margin = 12; - bool swap = 13; - bool future = 14; - bool option = 15; - bool active = 16; - bool contract = 17; - bool linear = 18; - bool inverse = 19; - float taker = 20; - float maker = 21; - float contractSize = 22; - float expiry = 23; - string expiryDatetime = 24; - float strike = 25; - string optionType = 26; - float precision = 27; - string limits = 28; - string info = 29; - } - } - - message Markets{ - message Markets { - repeated Market markets = 1; - } - } - - - // Performatives and contents - message Get_All_Markets_Performative{ - string exchange_id = 1; - string currency = 2; - bool currency_is_set = 3; - } - - message Get_Market_Performative{ - string id = 1; - string exchange_id = 2; - } - - message All_Markets_Performative{ - Markets markets = 1; - } - - message Market_Performative{ - Market market = 1; - } - - message Error_Performative{ - ErrorCode error_code = 1; - string error_msg = 2; - map error_data = 3; - } - - - oneof performative{ - All_Markets_Performative all_markets = 5; - Error_Performative error = 6; - Get_All_Markets_Performative get_all_markets = 7; - Get_Market_Performative get_market = 8; - Market_Performative market = 9; - } -} diff --git a/lyra/autonomy/packages/eightballer/protocols/markets/markets_pb2.py b/lyra/autonomy/packages/eightballer/protocols/markets/markets_pb2.py deleted file mode 100644 index e1759c1..0000000 --- a/lyra/autonomy/packages/eightballer/protocols/markets/markets_pb2.py +++ /dev/null @@ -1,201 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -"""Generated protocol buffer code.""" -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import message as _message -from google.protobuf import reflection as _reflection -from google.protobuf import symbol_database as _symbol_database - -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile( - b'\n\rmarkets.proto\x12\x1e\x61\x65\x61.eightballer.markets.v0_1_0"\x9d\x0f\n\x0eMarketsMessage\x12^\n\x0b\x61ll_markets\x18\x05 \x01(\x0b\x32G.aea.eightballer.markets.v0_1_0.MarketsMessage.All_Markets_PerformativeH\x00\x12R\n\x05\x65rror\x18\x06 \x01(\x0b\x32\x41.aea.eightballer.markets.v0_1_0.MarketsMessage.Error_PerformativeH\x00\x12\x66\n\x0fget_all_markets\x18\x07 \x01(\x0b\x32K.aea.eightballer.markets.v0_1_0.MarketsMessage.Get_All_Markets_PerformativeH\x00\x12\\\n\nget_market\x18\x08 \x01(\x0b\x32\x46.aea.eightballer.markets.v0_1_0.MarketsMessage.Get_Market_PerformativeH\x00\x12T\n\x06market\x18\t \x01(\x0b\x32\x42.aea.eightballer.markets.v0_1_0.MarketsMessage.Market_PerformativeH\x00\x1a\xe8\x01\n\tErrorCode\x12Z\n\nerror_code\x18\x01 \x01(\x0e\x32\x46.aea.eightballer.markets.v0_1_0.MarketsMessage.ErrorCode.ErrorCodeEnum"\x7f\n\rErrorCodeEnum\x12\x18\n\x14UNSUPPORTED_PROTOCOL\x10\x00\x12\x12\n\x0e\x44\x45\x43ODING_ERROR\x10\x01\x12\x13\n\x0fINVALID_MESSAGE\x10\x02\x12\x15\n\x11UNSUPPORTED_SKILL\x10\x03\x12\x14\n\x10INVALID_DIALOGUE\x10\x04\x1a\xf2\x03\n\x06Market\x1a\xe7\x03\n\x06Market\x12\n\n\x02id\x18\x01 \x01(\t\x12\x13\n\x0blowercaseId\x18\x02 \x01(\t\x12\x0e\n\x06symbol\x18\x03 \x01(\t\x12\x0c\n\x04\x62\x61se\x18\x04 \x01(\t\x12\r\n\x05quote\x18\x05 \x01(\t\x12\x0e\n\x06settle\x18\x06 \x01(\t\x12\x0e\n\x06\x62\x61seId\x18\x07 \x01(\t\x12\x0f\n\x07quoteId\x18\x08 \x01(\t\x12\x10\n\x08settleId\x18\t \x01(\t\x12\x0c\n\x04type\x18\n \x01(\t\x12\x0c\n\x04spot\x18\x0b \x01(\x08\x12\x0e\n\x06margin\x18\x0c \x01(\x08\x12\x0c\n\x04swap\x18\r \x01(\x08\x12\x0e\n\x06\x66uture\x18\x0e \x01(\x08\x12\x0e\n\x06option\x18\x0f \x01(\x08\x12\x0e\n\x06\x61\x63tive\x18\x10 \x01(\x08\x12\x10\n\x08\x63ontract\x18\x11 \x01(\x08\x12\x0e\n\x06linear\x18\x12 \x01(\x08\x12\x0f\n\x07inverse\x18\x13 \x01(\x08\x12\r\n\x05taker\x18\x14 \x01(\x02\x12\r\n\x05maker\x18\x15 \x01(\x02\x12\x14\n\x0c\x63ontractSize\x18\x16 \x01(\x02\x12\x0e\n\x06\x65xpiry\x18\x17 \x01(\x02\x12\x16\n\x0e\x65xpiryDatetime\x18\x18 \x01(\t\x12\x0e\n\x06strike\x18\x19 \x01(\x02\x12\x12\n\noptionType\x18\x1a \x01(\t\x12\x11\n\tprecision\x18\x1b \x01(\x02\x12\x0e\n\x06limits\x18\x1c \x01(\t\x12\x0c\n\x04info\x18\x1d \x01(\t\x1a\\\n\x07Markets\x1aQ\n\x07Markets\x12\x46\n\x07markets\x18\x01 \x03(\x0b\x32\x35.aea.eightballer.markets.v0_1_0.MarketsMessage.Market\x1a^\n\x1cGet_All_Markets_Performative\x12\x13\n\x0b\x65xchange_id\x18\x01 \x01(\t\x12\x10\n\x08\x63urrency\x18\x02 \x01(\t\x12\x17\n\x0f\x63urrency_is_set\x18\x03 \x01(\x08\x1a:\n\x17Get_Market_Performative\x12\n\n\x02id\x18\x01 \x01(\t\x12\x13\n\x0b\x65xchange_id\x18\x02 \x01(\t\x1a\x63\n\x18\x41ll_Markets_Performative\x12G\n\x07markets\x18\x01 \x01(\x0b\x32\x36.aea.eightballer.markets.v0_1_0.MarketsMessage.Markets\x1a\\\n\x13Market_Performative\x12\x45\n\x06market\x18\x01 \x01(\x0b\x32\x35.aea.eightballer.markets.v0_1_0.MarketsMessage.Market\x1a\x8d\x02\n\x12\x45rror_Performative\x12L\n\nerror_code\x18\x01 \x01(\x0b\x32\x38.aea.eightballer.markets.v0_1_0.MarketsMessage.ErrorCode\x12\x11\n\terror_msg\x18\x02 \x01(\t\x12\x64\n\nerror_data\x18\x03 \x03(\x0b\x32P.aea.eightballer.markets.v0_1_0.MarketsMessage.Error_Performative.ErrorDataEntry\x1a\x30\n\x0e\x45rrorDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x0c:\x02\x38\x01\x42\x0e\n\x0cperformativeb\x06proto3' -) - - -_MARKETSMESSAGE = DESCRIPTOR.message_types_by_name["MarketsMessage"] -_MARKETSMESSAGE_ERRORCODE = _MARKETSMESSAGE.nested_types_by_name["ErrorCode"] -_MARKETSMESSAGE_MARKET = _MARKETSMESSAGE.nested_types_by_name["Market"] -_MARKETSMESSAGE_MARKET_MARKET = _MARKETSMESSAGE_MARKET.nested_types_by_name["Market"] -_MARKETSMESSAGE_MARKETS = _MARKETSMESSAGE.nested_types_by_name["Markets"] -_MARKETSMESSAGE_MARKETS_MARKETS = _MARKETSMESSAGE_MARKETS.nested_types_by_name[ - "Markets" -] -_MARKETSMESSAGE_GET_ALL_MARKETS_PERFORMATIVE = _MARKETSMESSAGE.nested_types_by_name[ - "Get_All_Markets_Performative" -] -_MARKETSMESSAGE_GET_MARKET_PERFORMATIVE = _MARKETSMESSAGE.nested_types_by_name[ - "Get_Market_Performative" -] -_MARKETSMESSAGE_ALL_MARKETS_PERFORMATIVE = _MARKETSMESSAGE.nested_types_by_name[ - "All_Markets_Performative" -] -_MARKETSMESSAGE_MARKET_PERFORMATIVE = _MARKETSMESSAGE.nested_types_by_name[ - "Market_Performative" -] -_MARKETSMESSAGE_ERROR_PERFORMATIVE = _MARKETSMESSAGE.nested_types_by_name[ - "Error_Performative" -] -_MARKETSMESSAGE_ERROR_PERFORMATIVE_ERRORDATAENTRY = ( - _MARKETSMESSAGE_ERROR_PERFORMATIVE.nested_types_by_name["ErrorDataEntry"] -) -_MARKETSMESSAGE_ERRORCODE_ERRORCODEENUM = _MARKETSMESSAGE_ERRORCODE.enum_types_by_name[ - "ErrorCodeEnum" -] -MarketsMessage = _reflection.GeneratedProtocolMessageType( - "MarketsMessage", - (_message.Message,), - { - "ErrorCode": _reflection.GeneratedProtocolMessageType( - "ErrorCode", - (_message.Message,), - { - "DESCRIPTOR": _MARKETSMESSAGE_ERRORCODE, - "__module__": "markets_pb2" - # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.ErrorCode) - }, - ), - "Market": _reflection.GeneratedProtocolMessageType( - "Market", - (_message.Message,), - { - "Market": _reflection.GeneratedProtocolMessageType( - "Market", - (_message.Message,), - { - "DESCRIPTOR": _MARKETSMESSAGE_MARKET_MARKET, - "__module__": "markets_pb2" - # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.Market.Market) - }, - ), - "DESCRIPTOR": _MARKETSMESSAGE_MARKET, - "__module__": "markets_pb2" - # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.Market) - }, - ), - "Markets": _reflection.GeneratedProtocolMessageType( - "Markets", - (_message.Message,), - { - "Markets": _reflection.GeneratedProtocolMessageType( - "Markets", - (_message.Message,), - { - "DESCRIPTOR": _MARKETSMESSAGE_MARKETS_MARKETS, - "__module__": "markets_pb2" - # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.Markets.Markets) - }, - ), - "DESCRIPTOR": _MARKETSMESSAGE_MARKETS, - "__module__": "markets_pb2" - # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.Markets) - }, - ), - "Get_All_Markets_Performative": _reflection.GeneratedProtocolMessageType( - "Get_All_Markets_Performative", - (_message.Message,), - { - "DESCRIPTOR": _MARKETSMESSAGE_GET_ALL_MARKETS_PERFORMATIVE, - "__module__": "markets_pb2" - # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.Get_All_Markets_Performative) - }, - ), - "Get_Market_Performative": _reflection.GeneratedProtocolMessageType( - "Get_Market_Performative", - (_message.Message,), - { - "DESCRIPTOR": _MARKETSMESSAGE_GET_MARKET_PERFORMATIVE, - "__module__": "markets_pb2" - # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.Get_Market_Performative) - }, - ), - "All_Markets_Performative": _reflection.GeneratedProtocolMessageType( - "All_Markets_Performative", - (_message.Message,), - { - "DESCRIPTOR": _MARKETSMESSAGE_ALL_MARKETS_PERFORMATIVE, - "__module__": "markets_pb2" - # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.All_Markets_Performative) - }, - ), - "Market_Performative": _reflection.GeneratedProtocolMessageType( - "Market_Performative", - (_message.Message,), - { - "DESCRIPTOR": _MARKETSMESSAGE_MARKET_PERFORMATIVE, - "__module__": "markets_pb2" - # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.Market_Performative) - }, - ), - "Error_Performative": _reflection.GeneratedProtocolMessageType( - "Error_Performative", - (_message.Message,), - { - "ErrorDataEntry": _reflection.GeneratedProtocolMessageType( - "ErrorDataEntry", - (_message.Message,), - { - "DESCRIPTOR": _MARKETSMESSAGE_ERROR_PERFORMATIVE_ERRORDATAENTRY, - "__module__": "markets_pb2" - # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.Error_Performative.ErrorDataEntry) - }, - ), - "DESCRIPTOR": _MARKETSMESSAGE_ERROR_PERFORMATIVE, - "__module__": "markets_pb2" - # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage.Error_Performative) - }, - ), - "DESCRIPTOR": _MARKETSMESSAGE, - "__module__": "markets_pb2" - # @@protoc_insertion_point(class_scope:aea.eightballer.markets.v0_1_0.MarketsMessage) - }, -) -_sym_db.RegisterMessage(MarketsMessage) -_sym_db.RegisterMessage(MarketsMessage.ErrorCode) -_sym_db.RegisterMessage(MarketsMessage.Market) -_sym_db.RegisterMessage(MarketsMessage.Market.Market) -_sym_db.RegisterMessage(MarketsMessage.Markets) -_sym_db.RegisterMessage(MarketsMessage.Markets.Markets) -_sym_db.RegisterMessage(MarketsMessage.Get_All_Markets_Performative) -_sym_db.RegisterMessage(MarketsMessage.Get_Market_Performative) -_sym_db.RegisterMessage(MarketsMessage.All_Markets_Performative) -_sym_db.RegisterMessage(MarketsMessage.Market_Performative) -_sym_db.RegisterMessage(MarketsMessage.Error_Performative) -_sym_db.RegisterMessage(MarketsMessage.Error_Performative.ErrorDataEntry) - -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - _MARKETSMESSAGE_ERROR_PERFORMATIVE_ERRORDATAENTRY._options = None - _MARKETSMESSAGE_ERROR_PERFORMATIVE_ERRORDATAENTRY._serialized_options = b"8\001" - _MARKETSMESSAGE._serialized_start = 50 - _MARKETSMESSAGE._serialized_end = 1999 - _MARKETSMESSAGE_ERRORCODE._serialized_start = 533 - _MARKETSMESSAGE_ERRORCODE._serialized_end = 765 - _MARKETSMESSAGE_ERRORCODE_ERRORCODEENUM._serialized_start = 638 - _MARKETSMESSAGE_ERRORCODE_ERRORCODEENUM._serialized_end = 765 - _MARKETSMESSAGE_MARKET._serialized_start = 768 - _MARKETSMESSAGE_MARKET._serialized_end = 1266 - _MARKETSMESSAGE_MARKET_MARKET._serialized_start = 779 - _MARKETSMESSAGE_MARKET_MARKET._serialized_end = 1266 - _MARKETSMESSAGE_MARKETS._serialized_start = 1268 - _MARKETSMESSAGE_MARKETS._serialized_end = 1360 - _MARKETSMESSAGE_MARKETS_MARKETS._serialized_start = 1279 - _MARKETSMESSAGE_MARKETS_MARKETS._serialized_end = 1360 - _MARKETSMESSAGE_GET_ALL_MARKETS_PERFORMATIVE._serialized_start = 1362 - _MARKETSMESSAGE_GET_ALL_MARKETS_PERFORMATIVE._serialized_end = 1456 - _MARKETSMESSAGE_GET_MARKET_PERFORMATIVE._serialized_start = 1458 - _MARKETSMESSAGE_GET_MARKET_PERFORMATIVE._serialized_end = 1516 - _MARKETSMESSAGE_ALL_MARKETS_PERFORMATIVE._serialized_start = 1518 - _MARKETSMESSAGE_ALL_MARKETS_PERFORMATIVE._serialized_end = 1617 - _MARKETSMESSAGE_MARKET_PERFORMATIVE._serialized_start = 1619 - _MARKETSMESSAGE_MARKET_PERFORMATIVE._serialized_end = 1711 - _MARKETSMESSAGE_ERROR_PERFORMATIVE._serialized_start = 1714 - _MARKETSMESSAGE_ERROR_PERFORMATIVE._serialized_end = 1983 - _MARKETSMESSAGE_ERROR_PERFORMATIVE_ERRORDATAENTRY._serialized_start = 1935 - _MARKETSMESSAGE_ERROR_PERFORMATIVE_ERRORDATAENTRY._serialized_end = 1983 -# @@protoc_insertion_point(module_scope) diff --git a/lyra/autonomy/packages/eightballer/protocols/markets/message.py b/lyra/autonomy/packages/eightballer/protocols/markets/message.py deleted file mode 100644 index d041412..0000000 --- a/lyra/autonomy/packages/eightballer/protocols/markets/message.py +++ /dev/null @@ -1,335 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2023 eightballer -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""This module contains markets's message definition.""" - -# pylint: disable=too-many-statements,too-many-locals,no-member,too-few-public-methods,too-many-branches,not-an-iterable,unidiomatic-typecheck,unsubscriptable-object -import logging -from typing import Any, Dict, Optional, Set, Tuple, cast - -from aea.configurations.base import PublicId -from aea.exceptions import AEAEnforceError, enforce -from aea.protocols.base import Message - -from packages.eightballer.protocols.markets.custom_types import ( - ErrorCode as CustomErrorCode, -) -from packages.eightballer.protocols.markets.custom_types import Market as CustomMarket -from packages.eightballer.protocols.markets.custom_types import Markets as CustomMarkets - -_default_logger = logging.getLogger( - "aea.packages.eightballer.protocols.markets.message" -) - -DEFAULT_BODY_SIZE = 4 - - -class MarketsMessage(Message): - """A protocol for passing ohlcv data between compoents.""" - - protocol_id = PublicId.from_str("eightballer/markets:0.1.0") - protocol_specification_id = PublicId.from_str("eightballer/markets:0.1.0") - - ErrorCode = CustomErrorCode - - Market = CustomMarket - - Markets = CustomMarkets - - class Performative(Message.Performative): - """Performatives for the markets protocol.""" - - ALL_MARKETS = "all_markets" - ERROR = "error" - GET_ALL_MARKETS = "get_all_markets" - GET_MARKET = "get_market" - MARKET = "market" - - def __str__(self) -> str: - """Get the string representation.""" - return str(self.value) - - _performatives = {"all_markets", "error", "get_all_markets", "get_market", "market"} - __slots__: Tuple[str, ...] = tuple() - - class _SlotsCls: - __slots__ = ( - "currency", - "dialogue_reference", - "error_code", - "error_data", - "error_msg", - "exchange_id", - "id", - "market", - "markets", - "message_id", - "performative", - "target", - ) - - def __init__( - self, - performative: Performative, - dialogue_reference: Tuple[str, str] = ("", ""), - message_id: int = 1, - target: int = 0, - **kwargs: Any, - ): - """ - Initialise an instance of MarketsMessage. - - :param message_id: the message id. - :param dialogue_reference: the dialogue reference. - :param target: the message target. - :param performative: the message performative. - :param **kwargs: extra options. - """ - super().__init__( - dialogue_reference=dialogue_reference, - message_id=message_id, - target=target, - performative=MarketsMessage.Performative(performative), - **kwargs, - ) - - @property - def valid_performatives(self) -> Set[str]: - """Get valid performatives.""" - return self._performatives - - @property - def dialogue_reference(self) -> Tuple[str, str]: - """Get the dialogue_reference of the message.""" - enforce(self.is_set("dialogue_reference"), "dialogue_reference is not set.") - return cast(Tuple[str, str], self.get("dialogue_reference")) - - @property - def message_id(self) -> int: - """Get the message_id of the message.""" - enforce(self.is_set("message_id"), "message_id is not set.") - return cast(int, self.get("message_id")) - - @property - def performative(self) -> Performative: # type: ignore # noqa: F821 - """Get the performative of the message.""" - enforce(self.is_set("performative"), "performative is not set.") - return cast(MarketsMessage.Performative, self.get("performative")) - - @property - def target(self) -> int: - """Get the target of the message.""" - enforce(self.is_set("target"), "target is not set.") - return cast(int, self.get("target")) - - @property - def currency(self) -> Optional[str]: - """Get the 'currency' content from the message.""" - return cast(Optional[str], self.get("currency")) - - @property - def error_code(self) -> CustomErrorCode: - """Get the 'error_code' content from the message.""" - enforce(self.is_set("error_code"), "'error_code' content is not set.") - return cast(CustomErrorCode, self.get("error_code")) - - @property - def error_data(self) -> Dict[str, bytes]: - """Get the 'error_data' content from the message.""" - enforce(self.is_set("error_data"), "'error_data' content is not set.") - return cast(Dict[str, bytes], self.get("error_data")) - - @property - def error_msg(self) -> str: - """Get the 'error_msg' content from the message.""" - enforce(self.is_set("error_msg"), "'error_msg' content is not set.") - return cast(str, self.get("error_msg")) - - @property - def exchange_id(self) -> str: - """Get the 'exchange_id' content from the message.""" - enforce(self.is_set("exchange_id"), "'exchange_id' content is not set.") - return cast(str, self.get("exchange_id")) - - @property - def id(self) -> str: - """Get the 'id' content from the message.""" - enforce(self.is_set("id"), "'id' content is not set.") - return cast(str, self.get("id")) - - @property - def market(self) -> CustomMarket: - """Get the 'market' content from the message.""" - enforce(self.is_set("market"), "'market' content is not set.") - return cast(CustomMarket, self.get("market")) - - @property - def markets(self) -> CustomMarkets: - """Get the 'markets' content from the message.""" - enforce(self.is_set("markets"), "'markets' content is not set.") - return cast(CustomMarkets, self.get("markets")) - - def _is_consistent(self) -> bool: - """Check that the message follows the markets protocol.""" - try: - enforce( - isinstance(self.dialogue_reference, tuple), - "Invalid type for 'dialogue_reference'. Expected 'tuple'. Found '{}'.".format( - type(self.dialogue_reference) - ), - ) - enforce( - isinstance(self.dialogue_reference[0], str), - "Invalid type for 'dialogue_reference[0]'. Expected 'str'. Found '{}'.".format( - type(self.dialogue_reference[0]) - ), - ) - enforce( - isinstance(self.dialogue_reference[1], str), - "Invalid type for 'dialogue_reference[1]'. Expected 'str'. Found '{}'.".format( - type(self.dialogue_reference[1]) - ), - ) - enforce( - type(self.message_id) is int, - "Invalid type for 'message_id'. Expected 'int'. Found '{}'.".format( - type(self.message_id) - ), - ) - enforce( - type(self.target) is int, - "Invalid type for 'target'. Expected 'int'. Found '{}'.".format( - type(self.target) - ), - ) - - # Light Protocol Rule 2 - # Check correct performative - enforce( - isinstance(self.performative, MarketsMessage.Performative), - "Invalid 'performative'. Expected either of '{}'. Found '{}'.".format( - self.valid_performatives, self.performative - ), - ) - - # Check correct contents - actual_nb_of_contents = len(self._body) - DEFAULT_BODY_SIZE - expected_nb_of_contents = 0 - if self.performative == MarketsMessage.Performative.GET_ALL_MARKETS: - expected_nb_of_contents = 1 - enforce( - isinstance(self.exchange_id, str), - "Invalid type for content 'exchange_id'. Expected 'str'. Found '{}'.".format( - type(self.exchange_id) - ), - ) - if self.is_set("currency"): - expected_nb_of_contents += 1 - currency = cast(str, self.currency) - enforce( - isinstance(currency, str), - "Invalid type for content 'currency'. Expected 'str'. Found '{}'.".format( - type(currency) - ), - ) - elif self.performative == MarketsMessage.Performative.GET_MARKET: - expected_nb_of_contents = 2 - enforce( - isinstance(self.id, str), - "Invalid type for content 'id'. Expected 'str'. Found '{}'.".format( - type(self.id) - ), - ) - enforce( - isinstance(self.exchange_id, str), - "Invalid type for content 'exchange_id'. Expected 'str'. Found '{}'.".format( - type(self.exchange_id) - ), - ) - elif self.performative == MarketsMessage.Performative.ALL_MARKETS: - expected_nb_of_contents = 1 - enforce( - isinstance(self.markets, CustomMarkets), - "Invalid type for content 'markets'. Expected 'Markets'. Found '{}'.".format( - type(self.markets) - ), - ) - elif self.performative == MarketsMessage.Performative.MARKET: - expected_nb_of_contents = 1 - enforce( - isinstance(self.market, CustomMarket), - "Invalid type for content 'market'. Expected 'Market'. Found '{}'.".format( - type(self.market) - ), - ) - elif self.performative == MarketsMessage.Performative.ERROR: - expected_nb_of_contents = 3 - enforce( - isinstance(self.error_code, CustomErrorCode), - "Invalid type for content 'error_code'. Expected 'ErrorCode'. Found '{}'.".format( - type(self.error_code) - ), - ) - enforce( - isinstance(self.error_msg, str), - "Invalid type for content 'error_msg'. Expected 'str'. Found '{}'.".format( - type(self.error_msg) - ), - ) - enforce( - isinstance(self.error_data, dict), - "Invalid type for content 'error_data'. Expected 'dict'. Found '{}'.".format( - type(self.error_data) - ), - ) - for key_of_error_data, value_of_error_data in self.error_data.items(): - enforce( - isinstance(key_of_error_data, str), - "Invalid type for dictionary keys in content 'error_data'. Expected 'str'. Found '{}'.".format( - type(key_of_error_data) - ), - ) - enforce( - isinstance(value_of_error_data, bytes), - "Invalid type for dictionary values in content 'error_data'. Expected 'bytes'. Found '{}'.".format( - type(value_of_error_data) - ), - ) - - # Check correct content count - enforce( - expected_nb_of_contents == actual_nb_of_contents, - "Incorrect number of contents. Expected {}. Found {}".format( - expected_nb_of_contents, actual_nb_of_contents - ), - ) - - # Light Protocol Rule 3 - if self.message_id == 1: - enforce( - self.target == 0, - "Invalid 'target'. Expected 0 (because 'message_id' is 1). Found {}.".format( - self.target - ), - ) - except (AEAEnforceError, ValueError, KeyError) as e: - _default_logger.error(str(e)) - return False - - return True diff --git a/lyra/autonomy/packages/eightballer/protocols/markets/protocol.yaml b/lyra/autonomy/packages/eightballer/protocols/markets/protocol.yaml deleted file mode 100644 index bdaec9d..0000000 --- a/lyra/autonomy/packages/eightballer/protocols/markets/protocol.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: markets -author: eightballer -version: 0.1.0 -protocol_specification_id: eightballer/markets:0.1.0 -type: protocol -description: A protocol for passing ohlcv data between compoents. -license: Apache-2.0 -aea_version: '>=1.0.0, <2.0.0' -fingerprint: - __init__.py: bafybeicfxz7qdkubd7bv5vnj7lg7eoepevbtpowzjfi4hqa5yzrbywbo7y - custom_types.py: bafybeihefst4r55a3cc552m2v6xahgonb2dychdb2pljclwyyv4s3my24a - dialogues.py: bafybeigqgitua32dms6n4izw3a22un4c4mxocbu54564ud3ykxiigtyny4 - markets.proto: bafybeibihxjeibzycgd5e4rqifzq7wkaeqlzf2cmg75ppwl47murmygp6e - markets_pb2.py: bafybeiaebcrubvd25akmoxndo4h7qy2an7dmyg6cd4pya73au2yia2vsme - message.py: bafybeib2u33yyizdlqbj4stkrf2xsc2qnpsk5qkpeyekub5xuif6iplady - serialization.py: bafybeicvxnw7hwuedxkou6ijnqtv6td4k2kdnrlg2vrfoxgfgogj7xniym - tests/test_markets_dialogues.py: bafybeihikml67lyih4g7nhemayldz6ahi3tll4usen754r7jcxvtzw2vj4 - tests/test_markets_messages.py: bafybeigysvwtkc2k6teatleketos4agq6ze3tg7rzhlpygh64fumhxarni -fingerprint_ignore_patterns: [] -dependencies: - protobuf: {} diff --git a/lyra/autonomy/packages/eightballer/protocols/markets/serialization.py b/lyra/autonomy/packages/eightballer/protocols/markets/serialization.py deleted file mode 100644 index af24a42..0000000 --- a/lyra/autonomy/packages/eightballer/protocols/markets/serialization.py +++ /dev/null @@ -1,164 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2023 eightballer -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Serialization module for markets protocol.""" - -# pylint: disable=too-many-statements,too-many-locals,no-member,too-few-public-methods,redefined-builtin -from typing import cast - -from aea.mail.base_pb2 import DialogueMessage -from aea.mail.base_pb2 import Message as ProtobufMessage -from aea.protocols.base import Message, Serializer - -from packages.eightballer.protocols.markets import markets_pb2 -from packages.eightballer.protocols.markets.custom_types import ( - ErrorCode, - Market, - Markets, -) -from packages.eightballer.protocols.markets.message import MarketsMessage - - -class MarketsSerializer(Serializer): - """Serialization for the 'markets' protocol.""" - - @staticmethod - def encode(msg: Message) -> bytes: - """ - Encode a 'Markets' message into bytes. - - :param msg: the message object. - :return: the bytes. - """ - msg = cast(MarketsMessage, msg) - message_pb = ProtobufMessage() - dialogue_message_pb = DialogueMessage() - markets_msg = markets_pb2.MarketsMessage() - - dialogue_message_pb.message_id = msg.message_id - dialogue_reference = msg.dialogue_reference - dialogue_message_pb.dialogue_starter_reference = dialogue_reference[0] - dialogue_message_pb.dialogue_responder_reference = dialogue_reference[1] - dialogue_message_pb.target = msg.target - - performative_id = msg.performative - if performative_id == MarketsMessage.Performative.GET_ALL_MARKETS: - performative = markets_pb2.MarketsMessage.Get_All_Markets_Performative() # type: ignore - exchange_id = msg.exchange_id - performative.exchange_id = exchange_id - if msg.is_set("currency"): - performative.currency_is_set = True - currency = msg.currency - performative.currency = currency - markets_msg.get_all_markets.CopyFrom(performative) - elif performative_id == MarketsMessage.Performative.GET_MARKET: - performative = markets_pb2.MarketsMessage.Get_Market_Performative() # type: ignore - id = msg.id - performative.id = id - exchange_id = msg.exchange_id - performative.exchange_id = exchange_id - markets_msg.get_market.CopyFrom(performative) - elif performative_id == MarketsMessage.Performative.ALL_MARKETS: - performative = markets_pb2.MarketsMessage.All_Markets_Performative() # type: ignore - markets = msg.markets - Markets.encode(performative.markets, markets) - markets_msg.all_markets.CopyFrom(performative) - elif performative_id == MarketsMessage.Performative.MARKET: - performative = markets_pb2.MarketsMessage.Market_Performative() # type: ignore - market = msg.market - Market.encode(performative.market, market) - markets_msg.market.CopyFrom(performative) - elif performative_id == MarketsMessage.Performative.ERROR: - performative = markets_pb2.MarketsMessage.Error_Performative() # type: ignore - error_code = msg.error_code - ErrorCode.encode(performative.error_code, error_code) - error_msg = msg.error_msg - performative.error_msg = error_msg - error_data = msg.error_data - performative.error_data.update(error_data) - markets_msg.error.CopyFrom(performative) - else: - raise ValueError("Performative not valid: {}".format(performative_id)) - - dialogue_message_pb.content = markets_msg.SerializeToString() - - message_pb.dialogue_message.CopyFrom(dialogue_message_pb) - message_bytes = message_pb.SerializeToString() - return message_bytes - - @staticmethod - def decode(obj: bytes) -> Message: - """ - Decode bytes into a 'Markets' message. - - :param obj: the bytes object. - :return: the 'Markets' message. - """ - message_pb = ProtobufMessage() - markets_pb = markets_pb2.MarketsMessage() - message_pb.ParseFromString(obj) - message_id = message_pb.dialogue_message.message_id - dialogue_reference = ( - message_pb.dialogue_message.dialogue_starter_reference, - message_pb.dialogue_message.dialogue_responder_reference, - ) - target = message_pb.dialogue_message.target - - markets_pb.ParseFromString(message_pb.dialogue_message.content) - performative = markets_pb.WhichOneof("performative") - performative_id = MarketsMessage.Performative(str(performative)) - performative_content = dict() # type: Dict[str, Any] - if performative_id == MarketsMessage.Performative.GET_ALL_MARKETS: - exchange_id = markets_pb.get_all_markets.exchange_id - performative_content["exchange_id"] = exchange_id - if markets_pb.get_all_markets.currency_is_set: - currency = markets_pb.get_all_markets.currency - performative_content["currency"] = currency - elif performative_id == MarketsMessage.Performative.GET_MARKET: - id = markets_pb.get_market.id - performative_content["id"] = id - exchange_id = markets_pb.get_market.exchange_id - performative_content["exchange_id"] = exchange_id - elif performative_id == MarketsMessage.Performative.ALL_MARKETS: - pb2_markets = markets_pb.all_markets.markets - markets = Markets.decode(pb2_markets) - performative_content["markets"] = markets - elif performative_id == MarketsMessage.Performative.MARKET: - pb2_market = markets_pb.market.market - market = Market.decode(pb2_market) - performative_content["market"] = market - elif performative_id == MarketsMessage.Performative.ERROR: - pb2_error_code = markets_pb.error.error_code - error_code = ErrorCode.decode(pb2_error_code) - performative_content["error_code"] = error_code - error_msg = markets_pb.error.error_msg - performative_content["error_msg"] = error_msg - error_data = markets_pb.error.error_data - error_data_dict = dict(error_data) - performative_content["error_data"] = error_data_dict - else: - raise ValueError("Performative not valid: {}.".format(performative_id)) - - return MarketsMessage( - message_id=message_id, - dialogue_reference=dialogue_reference, - target=target, - performative=performative, - **performative_content - ) diff --git a/lyra/autonomy/packages/eightballer/protocols/markets/tests/test_markets_dialogues.py b/lyra/autonomy/packages/eightballer/protocols/markets/tests/test_markets_dialogues.py deleted file mode 100644 index 7c148ad..0000000 --- a/lyra/autonomy/packages/eightballer/protocols/markets/tests/test_markets_dialogues.py +++ /dev/null @@ -1,49 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2023 eightballer -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test dialogues module for markets protocol.""" - -# pylint: disable=too-many-statements,too-many-locals,no-member,too-few-public-methods,redefined-builtin -from aea.test_tools.test_protocol import BaseProtocolDialoguesTestCase - -from packages.eightballer.protocols.markets.dialogues import ( - MarketsDialogue, - MarketsDialogues, -) -from packages.eightballer.protocols.markets.message import MarketsMessage - - -class TestDialoguesMarkets(BaseProtocolDialoguesTestCase): - """Test for the 'markets' protocol dialogues.""" - - MESSAGE_CLASS = MarketsMessage - - DIALOGUE_CLASS = MarketsDialogue - - DIALOGUES_CLASS = MarketsDialogues - - ROLE_FOR_THE_FIRST_MESSAGE = MarketsDialogue.Role.AGENT # CHECK - - def make_message_content(self) -> dict: - """Make a dict with message contruction content for dialogues.create.""" - return dict( - performative=MarketsMessage.Performative.GET_ALL_MARKETS, - exchange_id="some str", - currency="some str", - ) diff --git a/lyra/autonomy/packages/eightballer/protocols/markets/tests/test_markets_messages.py b/lyra/autonomy/packages/eightballer/protocols/markets/tests/test_markets_messages.py deleted file mode 100644 index 6f19c83..0000000 --- a/lyra/autonomy/packages/eightballer/protocols/markets/tests/test_markets_messages.py +++ /dev/null @@ -1,227 +0,0 @@ -# -*- coding: utf-8 -*- -# ------------------------------------------------------------------------------ -# -# Copyright 2023 eightballer -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ------------------------------------------------------------------------------ - -"""Test messages module for markets protocol.""" - -# pylint: disable=too-many-statements,too-many-locals,no-member,too-few-public-methods,redefined-builtin -from typing import List - -from aea.test_tools.test_protocol import BaseProtocolMessagesTestCase - -from packages.eightballer.protocols.markets.custom_types import ( - ErrorCode, - Market, - Markets, -) -from packages.eightballer.protocols.markets.message import MarketsMessage - -TEST_MARKET_CASE = { - "id": "ETHBTC", - "lowercaseId": "ethbtc", - "symbol": "ETH/BTC", - "base": "ETH", - "quote": "BTC", - "settle": None, - "baseId": "ETH", - "quoteId": "BTC", - "settleId": None, - "type": "spot", - "spot": True, - "margin": True, - "swap": False, - "future": False, - "option": False, - "active": True, - "contract": False, - "linear": None, - "inverse": None, - "taker": 0.001, - "maker": 0.001, - "contractSize": None, - "expiry": None, - "expiryDatetime": None, - "strike": None, - "optionType": None, - "precision": {"amount": 4, "price": 5, "base": 8, "quote": 8}, - "limits": { - "leverage": {"min": None, "max": None}, - "amount": {"min": 0.0001, "max": 100000.0}, - "price": {"min": 1e-05, "max": 922327.0}, - "cost": {"min": 0.0001, "max": 9000000.0}, - "market": {"min": 0.0, "max": 3832.38128875}, - }, - "info": { - "symbol": "ETHBTC", - "status": "TRADING", - "baseAsset": "ETH", - "baseAssetPrecision": "8", - "quoteAsset": "BTC", - "quotePrecision": "8", - "quoteAssetPrecision": "8", - "baseCommissionPrecision": "8", - "quoteCommissionPrecision": "8", - "orderTypes": [ - "LIMIT", - "LIMIT_MAKER", - "MARKET", - "STOP_LOSS_LIMIT", - "TAKE_PROFIT_LIMIT", - ], - "icebergAllowed": True, - "ocoAllowed": True, - "quoteOrderQtyMarketAllowed": True, - "allowTrailingStop": True, - "cancelReplaceAllowed": True, - "isSpotTradingAllowed": True, - "isMarginTradingAllowed": True, - "filters": [ - { - "filterType": "PRICE_FILTER", - "minPrice": "0.00001000", - "maxPrice": "922327.00000000", - "tickSize": "0.00001000", - }, - { - "filterType": "LOT_SIZE", - "minQty": "0.00010000", - "maxQty": "100000.00000000", - "stepSize": "0.00010000", - }, - {"filterType": "ICEBERG_PARTS", "limit": "10"}, - { - "filterType": "MARKET_LOT_SIZE", - "minQty": "0.00000000", - "maxQty": "3832.38128875", - "stepSize": "0.00000000", - }, - { - "filterType": "TRAILING_DELTA", - "minTrailingAboveDelta": "10", - "maxTrailingAboveDelta": "2000", - "minTrailingBelowDelta": "10", - "maxTrailingBelowDelta": "2000", - }, - { - "filterType": "PERCENT_PRICE_BY_SIDE", - "bidMultiplierUp": "5", - "bidMultiplierDown": "0.2", - "askMultiplierUp": "5", - "askMultiplierDown": "0.2", - "avgPriceMins": "5", - }, - { - "filterType": "NOTIONAL", - "minNotional": "0.00010000", - "applyMinToMarket": True, - "maxNotional": "9000000.00000000", - "applyMaxToMarket": False, - "avgPriceMins": "5", - }, - {"filterType": "MAX_NUM_ORDERS", "maxNumOrders": "200"}, - {"filterType": "MAX_NUM_ALGO_ORDERS", "maxNumAlgoOrders": "5"}, - ], - "permissions": [ - "SPOT", - "MARGIN", - "TRD_GRP_004", - "TRD_GRP_005", - "TRD_GRP_006", - "TRD_GRP_008", - "TRD_GRP_009", - "TRD_GRP_010", - "TRD_GRP_011", - "TRD_GRP_012", - "TRD_GRP_013", - ], - "defaultSelfTradePreventionMode": "NONE", - "allowedSelfTradePreventionModes": [ - "NONE", - "EXPIRE_TAKER", - "EXPIRE_MAKER", - "EXPIRE_BOTH", - ], - }, -} - - -class TestMessageMarkets(BaseProtocolMessagesTestCase): - """Test for the 'markets' protocol message.""" - - MESSAGE_CLASS = MarketsMessage - - def build_messages(self) -> List[MarketsMessage]: # type: ignore[override] - """Build the messages to be used for testing.""" - return [ - MarketsMessage( - performative=MarketsMessage.Performative.GET_ALL_MARKETS, - exchange_id="some str", - currency="some str", - ), - MarketsMessage( - performative=MarketsMessage.Performative.GET_MARKET, - id="some str", - exchange_id="some str", - ), - MarketsMessage( - performative=MarketsMessage.Performative.ALL_MARKETS, - markets=Markets([Market(**TEST_MARKET_CASE)]), # check it please! - ), - MarketsMessage( - performative=MarketsMessage.Performative.MARKET, - market=Market(**TEST_MARKET_CASE), # check it please! - ), - MarketsMessage( - performative=MarketsMessage.Performative.ERROR, - error_code=ErrorCode.INVALID_MESSAGE, # check it please! - error_msg="some str", - error_data={"some str": b"some_bytes"}, - ), - MarketsMessage( - performative=MarketsMessage.Performative.END, - ), - ] - - def build_inconsistent(self) -> List[MarketsMessage]: # type: ignore[override] - """Build inconsistent messages to be used for testing.""" - return [ - MarketsMessage( - performative=MarketsMessage.Performative.GET_ALL_MARKETS, - # skip content: exchange_id - currency="some str", - ), - MarketsMessage( - performative=MarketsMessage.Performative.GET_MARKET, - # skip content: id - exchange_id="some str", - ), - MarketsMessage( - performative=MarketsMessage.Performative.ALL_MARKETS, - # skip content: markets - ), - MarketsMessage( - performative=MarketsMessage.Performative.MARKET, - # skip content: market - ), - MarketsMessage( - performative=MarketsMessage.Performative.ERROR, - # skip content: error_code - error_msg="some str", - error_data={"some str": b"some_bytes"}, - ), - ] diff --git a/lyra/autonomy/packages/packages.json b/lyra/autonomy/packages/packages.json deleted file mode 100644 index 73e094c..0000000 --- a/lyra/autonomy/packages/packages.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "dev": {}, - "third_party": { - "protocol/eightballer/markets/0.1.0": "bafybeihzukefu4kffjgwbd5p6svfseryui2ugoki6vrdnemdzyrfhpyiea" - } -} \ No newline at end of file diff --git a/lyra/base_client.py b/lyra/base_client.py index 9b62b96..b0eacaa 100644 --- a/lyra/base_client.py +++ b/lyra/base_client.py @@ -17,57 +17,31 @@ from lyra.enums import ( ActionType, CollateralAsset, + Environment, InstrumentType, OrderSide, OrderStatus, OrderType, + RfqStatus, SubaccountType, TimeInForce, UnderlyingCurrency, - Environment ) from lyra.utils import get_logger -import sys -import os -from pathlib import Path - -# we get the install location -# install_location = os.path.dirname(os.path.realpath(__file__)) -# sys.path.append(str(Path(install_location) / "autonomy")) -# breakpoint() -# from packages.eightballer.protocols.markets.custom_types import Market - -def to_market(api_result): - """Convert to a market object. - raw_resulot = {'instrument_type': 'perp', 'instrument_name': 'BTC-PERP', 'scheduled_activation': 1699035945, 'scheduled_deactivation': 9223372036854775807, 'is_active': True, 'tick_size': '0.1', 'minimum_amount': '0.01', 'maximum_amount': '10000', 'amount_step': '0.001', 'mark_price_fee_rate_cap': '0', 'maker_fee_rate': '0.0005', 'taker_fee_rate': '0.001', 'base_fee': '1.5', 'base_currency': 'BTC', 'quote_currency': 'USD', 'option_details': None, 'perp_details': {'index': 'BTC-USD', 'max_rate_per_hour': '0.1', 'min_rate_per_hour': '-0.1', 'static_interest_rate': '0', 'aggregate_funding': '244.249950785486024857', 'funding_rate': '-0.0000125'}, 'base_asset_address': '0xAFB6Bb95cd70D5367e2C39e9dbEb422B9815339D', 'base_asset_sub_id': '0'} - - """ - - market = Market( - id=api_result['instrument_name'], - lowercaseId=api_result['instrument_name'].lower(), - symbol=api_result['instrument_name'], - base=api_result['base_currency'], - quote=api_result['quote_currency'], - settle=api_result['quote_currency'], - baseId=api_result['base_currency'], - quoteId=api_result['quote_currency'], - settleId=api_result['quote_currency'], - type=api_result['instrument_type'], - future=api_result['instrument_type'] == InstrumentType.PERP, - option=api_result['instrument_type'] == InstrumentType.OPTION, - active=api_result['is_active'], - taker=api_result['taker_fee_rate'], - maker=api_result['maker_fee_rate'], - ) - return market - class BaseClient: """Client for the lyra dex.""" - def __init__(self, private_key: str = TEST_PRIVATE_KEY, env: Environment = Environment.TEST, logger=None, verbose=False, subaccount_id=None, wallet=None): + def __init__( + self, + private_key: str = TEST_PRIVATE_KEY, + env: Environment = Environment.TEST, + logger=None, + verbose=False, + subaccount_id=None, + wallet=None, + ): """ Initialize the LyraClient class. """ @@ -141,11 +115,7 @@ def fetch_instruments( } response = requests.post(url, json=payload, headers=PUBLIC_HEADERS) results = response.json()["result"] - - return [ - to_market(market) - for market in results - ] + return results def fetch_subaccounts(self): """ @@ -284,6 +254,49 @@ def _sign_order(self, order, base_asset_sub_id, instrument_type, currency): order['signature'] = self.signer.signHash(typed_data_hash).signature.hex() return order + def _sign_quote(self, quote): + """ + Sign the quote + """ + rfq_module_data = self._encode_quote_data(quote) + return self._sign_quote_data(quote, rfq_module_data) + + def _encode_quote_data(self, quote, underlying_currency: UnderlyingCurrency = UnderlyingCurrency.ETH): + """ + Convert the quote to encoded data. + """ + instruments = self.fetch_instruments(instrument_type=InstrumentType.OPTION, currency=underlying_currency) + ledgs_to_subids = {i['instrument_name']: i['base_asset_sub_id'] for i in instruments} + dir_sign = 1 if quote['direction'] == 'buy' else -1 + quote['price'] = '10' + + def encode_leg(leg): + print(quote) + sub_id = ledgs_to_subids[leg['instrument_name']] + leg_sign = 1 if leg['direction'] == 'buy' else -1 + signed_amount = self.web3_client.to_wei(leg['amount'], 'ether') * leg_sign * dir_sign + return [ + self.contracts[f"{underlying_currency.name}_OPTION_ADDRESS"], + sub_id, + self.web3_client.to_wei(quote['price'], 'ether'), + signed_amount, + ] + + encoded_legs = [encode_leg(leg) for leg in quote['legs']] + rfq_data = [self.web3_client.to_wei(quote['max_fee'], 'ether'), encoded_legs] + + encoded_data = eth_abi.encode( + # ['uint256(address,uint256,uint256,int256)[]'], + [ + 'uint256', + 'address', + 'uint256', + 'int256', + ], + [rfq_data], + ) + return self.web3_client.keccak(encoded_data) + def login_client( self, ): @@ -704,3 +717,78 @@ def set_mmp_config( response = requests.post(url, json=payload, headers=headers) results = response.json()["result"] return results + + def send_rfq(self, rfq): + """Send an RFQ.""" + url = f"{self.contracts['BASE_URL']}/private/send_rfq" + headers = self._create_signature_headers() + response = requests.post(url, json=rfq, headers=headers) + results = response.json()["result"] + return results + + def poll_rfqs(self): + """ + Poll RFQs. + type RfqResponse = { + subaccount_id: number, + creation_timestamp: number, + last_update_timestamp: number, + status: string, + cancel_reason: string, + rfq_id: string, + valid_until: number, + legs: Array + } + """ + url = f"{self.contracts['BASE_URL']}/private/poll_rfqs" + headers = self._create_signature_headers() + params = { + "subaccount_id": self.subaccount_id, + "status": RfqStatus.OPEN.value, + } + response = requests.post(url, headers=headers, params=params) + results = response.json()["result"] + return results + + def send_quote(self, quote): + """Send a quote.""" + url = f"{self.contracts['BASE_URL']}/private/send_quote" + headers = self._create_signature_headers() + response = requests.post(url, json=quote, headers=headers) + results = response.json()["result"] + return results + + # pricedLegs[0].price = direction == 'buy' ? '160' : '180'; + # pricedLegs[1].price = direction == 'buy' ? '70' : '50'; + # return { + # subaccount_id: subaccount_id_maker, + # rfq_id: rfq_response.rfq_id, + # legs: pricedLegs, + # direction: direction, + # max_fee: '10', + # nonce: Number(`${Date.now()}${Math.round(Math.random() * 999)}`), + # signer: wallet.address, + # signature_expiry_sec: Math.floor(Date.now() / 1000 + 350), + # signature: "filled_in_below" + # }; + # } + + def create_quote_object( + self, + rfq_id, + legs, + direction, + ): + """Create a quote object.""" + _, nonce, expiration = self.get_nonce_and_signature_expiry() + return { + "subaccount_id": self.subaccount_id, + "rfq_id": rfq_id, + "legs": legs, + "direction": direction, + "max_fee": '10.0', + "nonce": nonce, + "signer": self.signer.address, + "signature_expiry_sec": expiration, + "signature": "filled_in_below", + } diff --git a/lyra/cli.py b/lyra/cli.py index cefc5b0..c5ec129 100644 --- a/lyra/cli.py +++ b/lyra/cli.py @@ -422,6 +422,7 @@ def fetch_orders(ctx, instrument_name, label, page, page_size, status, regex): if regex: orders = [o for o in orders if regex in o["instrument_name"]] df = pd.DataFrame.from_records(orders) + print(orders[0]) instrument_names = df["instrument_name"].unique() print(f"Found {len(instrument_names)} instruments") print(instrument_names) diff --git a/lyra/enums.py b/lyra/enums.py index 41bcf50..173a55e 100644 --- a/lyra/enums.py +++ b/lyra/enums.py @@ -80,3 +80,9 @@ class ActionType(Enum): DEPOSIT = "deposit" TRANSFER = "transfer" + + +class RfqStatus(Enum): + """RFQ statuses.""" + + OPEN = "open" diff --git a/lyra/http_client.py b/lyra/http_client.py index 8af9d97..fbce9c1 100644 --- a/lyra/http_client.py +++ b/lyra/http_client.py @@ -5,12 +5,12 @@ import time from eth_account.messages import encode_defunct -from lyra.base_client import BaseClient from web3 import Web3 +from lyra.base_client import BaseClient + class HttpClient(BaseClient): - def _create_signature_headers(self): """ Create the signature headers diff --git a/lyra/lyra.py b/lyra/lyra.py index 1e66f0e..80f2b71 100644 --- a/lyra/lyra.py +++ b/lyra/lyra.py @@ -18,19 +18,16 @@ def to_32byte_hex(val): class LyraClient(BaseClient): """Client for the lyra dex.""" + http_client: HttpClient def _create_signature_headers(self): """Generate the signature headers.""" return self.http_client._create_signature_headers() - def __init__(self, *args, **kwargs): self.http_client = HttpClient( - *args, **kwargs, + *args, + **kwargs, ) super().__init__(*args, **kwargs) - - - - diff --git a/lyra/utils.py b/lyra/utils.py index b85e299..e054762 100644 --- a/lyra/utils.py +++ b/lyra/utils.py @@ -3,13 +3,9 @@ """ import logging import sys -import os from rich.logging import RichHandler -# install_location = os.path.dirname(os.path.realpath(__file__)) -# sys.path.append(install_location) - def get_logger(): """Get the logger.""" diff --git a/lyra/ws_client.py b/lyra/ws_client.py index 43ff727..3b2a2bb 100644 --- a/lyra/ws_client.py +++ b/lyra/ws_client.py @@ -2,19 +2,15 @@ Class to handle base websocket client """ -import json import time from eth_account.messages import encode_defunct -import requests -from lyra.base_client import BaseClient from web3 import Web3 -from lyra.constants import PUBLIC_HEADERS +from lyra.base_client import BaseClient class WsClient(BaseClient): - def _create_signature_headers(self): """ Create the signature headers @@ -29,4 +25,3 @@ def _create_signature_headers(self): "X-LyraTimestamp": timestamp, "X-LyraSignature": Web3.to_hex(signature.signature), } - diff --git a/tests/conftest.py b/tests/conftest.py index 162cb7f..77152d2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,13 +6,10 @@ import pytest -from lyra.enums import ( - Environment, -) +from lyra.enums import Environment from lyra.lyra import LyraClient from lyra.utils import get_logger - TEST_WALLET = "0x3A5c777edf22107d7FdFB3B02B0Cdfe8b75f3453" TEST_PRIVATE_KEY = "0xc14f53ee466dd3fc5fa356897ab276acbef4f020486ec253a23b0d1c3f89d4f4" @@ -31,3 +28,11 @@ def lyra_client(): lyra_client.subaccount_id = 5 yield lyra_client lyra_client.cancel_all() + + +@pytest.fixture +def lyra_client_2(): + lyra_client = LyraClient(TEST_PRIVATE_KEY, env=Environment.TEST, logger=get_logger()) + lyra_client.subaccount_id = lyra_client.fetch_subaccounts()[-1]['id'] + yield lyra_client + lyra_client.cancel_all() diff --git a/tests/test_main.py b/tests/test_main.py index 3f144bc..e79c5d2 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -19,8 +19,7 @@ SubaccountType, UnderlyingCurrency, ) -from tests.conftest import TEST_WALLET - +from tests.conftest import TEST_WALLET, freeze_time def test_lyra_client(lyra_client): diff --git a/tests/test_rfq.py b/tests/test_rfq.py index 286f138..8337aed 100644 --- a/tests/test_rfq.py +++ b/tests/test_rfq.py @@ -3,21 +3,19 @@ """ -from lyra.enums import OrderSide +from dataclasses import asdict, dataclass +from lyra.enums import OrderSide LEG_1_NAME = 'ETH-20240329-2400-C' LEG_2_NAME = 'ETH-20240329-2600-C' LEGS_TO_SUB_ID: any = { - 'ETH-20240329-2400-C': '39614082287924319838483674368', - 'ETH-20240329-2600-C': '39614082373823665758483674368' + 'ETH-20240329-2400-C': '39614082287924319838483674368', + 'ETH-20240329-2600-C': '39614082373823665758483674368', } - -from dataclasses import asdict, dataclass - @dataclass class Leg: instrument_name: str @@ -32,26 +30,55 @@ class Rfq: leg_2: Leg def to_dict(self): - return { - "legs": [ - asdict(self.leg_1), - asdict(self.leg_2) - ], - "subaccount_id": self.subaccount_id - } + return {"legs": [asdict(self.leg_1), asdict(self.leg_2)], "subaccount_id": self.subaccount_id} -def test_lyra_client_create_rfq(lyra_client, instrument_type, currency): +def test_lyra_client_create_rfq( + lyra_client, +): """ Test the LyraClient class. """ subaccount_id = lyra_client.subaccount_id - leg_1 = Leg(instrument_name=LEG_1_NAME, amount='1', direction=OrderSide.BUY) - leg_2 = Leg(instrument_name=LEG_2_NAME, amount='1', direction=OrderSide.SELL) + leg_1 = Leg(instrument_name=LEG_1_NAME, amount='1', direction=OrderSide.BUY.value) + leg_2 = Leg(instrument_name=LEG_2_NAME, amount='1', direction=OrderSide.SELL.value) rfq = Rfq(leg_1=leg_1, leg_2=leg_2, subaccount_id=subaccount_id) + assert lyra_client.send_rfq(rfq.to_dict()) + + +def test_lyra_client_create_quote( + lyra_client, +): + """ + Test the LyraClient class. + """ + subaccount_id = lyra_client.subaccount_id + leg_1 = Leg(instrument_name=LEG_1_NAME, amount='1', direction=OrderSide.BUY.value) + leg_2 = Leg(instrument_name=LEG_2_NAME, amount='1', direction=OrderSide.SELL.value) + rfq = Rfq(leg_1=leg_1, leg_2=leg_2, subaccount_id=subaccount_id) + res = lyra_client.send_rfq(rfq.to_dict()) - assert lyra_client.create_rfq(rfq.to_dict()) + # we now create the quote + quote = lyra_client.create_quote_object( + rfq_id=res['rfq_id'], + legs=[asdict(leg_1), asdict(leg_2)], + direction='sell', + ) + # we now sign it + assert lyra_client._sign_quote(quote) + breakpoint() +def test_poll_rfqs(lyra_client): + """ + Test the LyraClient class. + """ + subaccount_id = lyra_client.subaccount_id + leg_1 = Leg(instrument_name=LEG_1_NAME, amount='1', direction=OrderSide.BUY.value) + leg_2 = Leg(instrument_name=LEG_2_NAME, amount='1', direction=OrderSide.SELL.value) + rfq = Rfq(leg_1=leg_1, leg_2=leg_2, subaccount_id=subaccount_id) + assert lyra_client.send_rfq(rfq.to_dict()) + quotes = lyra_client.poll_rfqs() + assert quotes