Ensured all public classes are exposed and available. Removed unused classes.
This commit is contained in:
parent
db398e5f8b
commit
8bbe48abaf
|
@ -96,30 +96,3 @@ colons are replaced with %3A).
|
|||
* `FROM-ADDRESS` is the address the email appears to come from. This must be validated with ailjet](https://mailjet.com).
|
||||
* `TO-ADDRESS` is the destination address - the email account to which the email is being sent.
|
||||
Mailjet provides a client library, but really we don't need or want more dependencies.
|
||||
|
||||
|
||||
# 📃 CSV Files
|
||||
|
||||
The `CsvFileNotificationTarget` writes liquidation events to a CSV file.
|
||||
|
||||
The `CsvFileNotificationTarget` is reserved for `LiquidationEvent`s - nothing is written if the item is not a `LiquidationEvent`.
|
||||
|
||||
The format for configuring the CSV file notification target is:
|
||||
1. The word 'csvfile'
|
||||
2. A colon ':'
|
||||
3. The full or relative pathname to the desired CSV file (bearing in mind this filename may be in the context of the docker container, not the native filesystem).
|
||||
|
||||
So:
|
||||
csvfile:<CSV-FILENAME>
|
||||
|
||||
For example:
|
||||
```
|
||||
csvfile:/path/to/filename.csv
|
||||
```
|
||||
|
||||
The following headers should be automatically added to new CSV files:
|
||||
```
|
||||
"Timestamp","Liquidator Name","Group","Succeeded","Signature","Wallet","Margin Account","Token Changes"
|
||||
```
|
||||
|
||||
Token changes are listed as pairs of value plus symbol, so each token change adds two columns to the output. Token changes may arrive in different orders, so ordering of token changes is not guaranteed to be consistent from transaction to transaction.
|
|
@ -9,38 +9,37 @@
|
|||
#
|
||||
from .account import Account as Account
|
||||
from .account import AccountSlot as AccountSlot
|
||||
from .account import ReferrerMemory as ReferrerMemory
|
||||
from .account import Valuation as Valuation
|
||||
from .accountflags import AccountFlags as AccountFlags
|
||||
from .accountinfo import AccountInfo as AccountInfo
|
||||
from .accountinfoconverter import (
|
||||
build_account_info_converter as build_account_info_converter,
|
||||
)
|
||||
from .accountliquidator import AccountLiquidator as AccountLiquidator
|
||||
from .accountliquidator import NullAccountLiquidator as NullAccountLiquidator
|
||||
from .accountscout import AccountScout as AccountScout
|
||||
from .accountscout import ScoutReport as ScoutReport
|
||||
from .addressableaccount import AddressableAccount as AddressableAccount
|
||||
from .arguments import parse_args as parse_args
|
||||
from .arguments import setup_logging as setup_logging
|
||||
from .balancesheet import BalanceSheet as BalanceSheet
|
||||
from .cache import Cache as Cache
|
||||
from .cache import MarketCache as MarketCache
|
||||
from .cache import PerpMarketCache as PerpMarketCache
|
||||
from .cache import PriceCache as PriceCache
|
||||
from .cache import RootBankCache as RootBankCache
|
||||
from .client import AbstractSlotHolder as AbstractSlotHolder
|
||||
from .client import BetterClient as BetterClient
|
||||
from .client import ClusterUrlData as ClusterUrlData
|
||||
from .client import BlockhashNotFoundException as BlockhashNotFoundException
|
||||
from .client import CheckingSlotHolder as CheckingSlotHolder
|
||||
from .client import ClientException as ClientException
|
||||
from .client import ClusterUrlData as ClusterUrlData
|
||||
from .client import CompoundException as CompoundException
|
||||
from .client import CompoundRPCCaller as CompoundRPCCaller
|
||||
from .client import FailedToFetchBlockhashException as FailedToFetchBlockhashException
|
||||
from .client import NodeIsBehindException as NodeIsBehindException
|
||||
from .client import NullSlotHolder as NullSlotHolder
|
||||
from .client import NullTransactionMonitor as NullTransactionMonitor
|
||||
from .client import RateLimitException as RateLimitException
|
||||
from .client import RPCCaller as RPCCaller
|
||||
from .client import AbstractSlotHolder as AbstractSlotHolder
|
||||
from .client import CheckingSlotHolder as CheckingSlotHolder
|
||||
from .client import NullSlotHolder as NullSlotHolder
|
||||
from .client import StaleSlotException as StaleSlotException
|
||||
from .client import (
|
||||
TooManyRequestsRateLimitException as TooManyRequestsRateLimitException,
|
||||
|
@ -52,9 +51,12 @@ from .client import (
|
|||
TransactionAlreadyProcessedException as TransactionAlreadyProcessedException,
|
||||
)
|
||||
from .client import TransactionException as TransactionException
|
||||
from .client import TransactionMonitor as TransactionMonitor
|
||||
from .combinableinstructions import CombinableInstructions as CombinableInstructions
|
||||
from .constants import MangoConstants as MangoConstants
|
||||
from .constants import PackageVersion as PackageVersion
|
||||
from .constants import DATA_PATH as DATA_PATH
|
||||
from .constants import I64_MAX as I64_MAX
|
||||
from .constants import SOL_DECIMAL_DIVISOR as SOL_DECIMAL_DIVISOR
|
||||
from .constants import SOL_DECIMALS as SOL_DECIMALS
|
||||
from .constants import SOL_MINT_ADDRESS as SOL_MINT_ADDRESS
|
||||
|
@ -76,11 +78,20 @@ from .group import GroupSlot as GroupSlot
|
|||
from .group import GroupSlotPerpMarket as GroupSlotPerpMarket
|
||||
from .group import GroupSlotSpotMarket as GroupSlotSpotMarket
|
||||
from .healthcheck import HealthCheck as HealthCheck
|
||||
from .idgenerator import IdGenerator as IdGenerator
|
||||
from .idgenerator import MonotonicIdGenerator as MonotonicIdGenerator
|
||||
from .idgenerator import RandomIdGenerator as RandomIdGenerator
|
||||
from .idl import IdlParser as IdlParser
|
||||
from .idl import IdlType as IdlType
|
||||
from .idl import lazy_load_cached_idl_parser as lazy_load_cached_idl_parser
|
||||
from .idsjsonmarketlookup import IdsJsonMarketLookup as IdsJsonMarketLookup
|
||||
from .inventory import Inventory as Inventory
|
||||
from .inventory import InventoryAccountWatcher as InventoryAccountWatcher
|
||||
from .idsjsonmarketlookup import IdsJsonMarketType as IdsJsonMarketType
|
||||
from .instructionreporter import (
|
||||
CompoundInstructionReporter as CompoundInstructionReporter,
|
||||
)
|
||||
from .instructionreporter import InstructionReporter as InstructionReporter
|
||||
from .instructionreporter import MangoInstructionReporter as MangoInstructionReporter
|
||||
from .instructionreporter import SerumInstructionReporter as SerumInstructionReporter
|
||||
from .instructions import (
|
||||
build_mango_cache_perp_markets_instructions as build_mango_cache_perp_markets_instructions,
|
||||
)
|
||||
|
@ -180,35 +191,24 @@ from .instructions import (
|
|||
from .instructions import (
|
||||
build_spot_settle_instructions as build_spot_settle_instructions,
|
||||
)
|
||||
from .instructionreporter import InstructionReporter as InstructionReporter
|
||||
from .instructionreporter import SerumInstructionReporter as SerumInstructionReporter
|
||||
from .instructionreporter import MangoInstructionReporter as MangoInstructionReporter
|
||||
from .instructionreporter import (
|
||||
CompoundInstructionReporter as CompoundInstructionReporter,
|
||||
)
|
||||
from .instructiontype import InstructionType as InstructionType
|
||||
from .instrumentlookup import InstrumentLookup as InstrumentLookup
|
||||
from .instrumentlookup import NullInstrumentLookup as NullInstrumentLookup
|
||||
from .instrumentlookup import CompoundInstrumentLookup as CompoundInstrumentLookup
|
||||
from .instrumentlookup import IdsJsonTokenLookup as IdsJsonTokenLookup
|
||||
from .instrumentlookup import InstrumentLookup as InstrumentLookup
|
||||
from .instrumentlookup import NonSPLInstrumentLookup as NonSPLInstrumentLookup
|
||||
from .instrumentlookup import NullInstrumentLookup as NullInstrumentLookup
|
||||
from .instrumentlookup import SPLTokenLookup as SPLTokenLookup
|
||||
from .instrumentvalue import InstrumentValue as InstrumentValue
|
||||
from .liquidatablereport import LiquidatableState as LiquidatableState
|
||||
from .liquidatablereport import LiquidatableReport as LiquidatableReport
|
||||
from .liquidationevent import LiquidationEvent as LiquidationEvent
|
||||
from .liquidationprocessor import LiquidationProcessor as LiquidationProcessor
|
||||
from .liquidationprocessor import LiquidationProcessorState as LiquidationProcessorState
|
||||
from .inventory import Inventory as Inventory
|
||||
from .inventory import InventoryAccountWatcher as InventoryAccountWatcher
|
||||
from .loadedmarket import Event as Event
|
||||
from .loadedmarket import FillEvent as FillEvent
|
||||
from .loadedmarket import LoadedMarket as LoadedMarket
|
||||
from .logmessages import expand_log_messages as expand_log_messages
|
||||
from .lotsizeconverter import LotSizeConverter as LotSizeConverter
|
||||
from .mangoinstruction import MangoInstruction as MangoInstruction
|
||||
from .lotsizeconverter import NullLotSizeConverter as NullLotSizeConverter
|
||||
from .markets import InventorySource as InventorySource
|
||||
from .markets import MarketType as MarketType
|
||||
from .markets import Market as Market
|
||||
from .lotsizeconverter import RaisingLotSizeConverter as RaisingLotSizeConverter
|
||||
from .mangoinstruction import MangoInstruction as MangoInstruction
|
||||
from .marketlookup import CompoundMarketLookup as CompoundMarketLookup
|
||||
from .marketlookup import MarketLookup as MarketLookup
|
||||
from .marketlookup import NullMarketLookup as NullMarketLookup
|
||||
|
@ -218,13 +218,15 @@ from .marketoperations import (
|
|||
NullMarketInstructionBuilder as NullMarketInstructionBuilder,
|
||||
)
|
||||
from .marketoperations import NullMarketOperations as NullMarketOperations
|
||||
from .markets import InventorySource as InventorySource
|
||||
from .markets import MarketType as MarketType
|
||||
from .markets import Market as Market
|
||||
from .metadata import Metadata as Metadata
|
||||
from .modelstate import EventQueue as EventQueue
|
||||
from .modelstate import NullEventQueue as NullEventQueue
|
||||
from .modelstate import ModelState as ModelState
|
||||
from .notification import CompoundNotificationTarget as CompoundNotificationTarget
|
||||
from .notification import ConsoleNotificationTarget as ConsoleNotificationTarget
|
||||
from .notification import CsvFileNotificationTarget as CsvFileNotificationTarget
|
||||
from .notification import DiscordNotificationTarget as DiscordNotificationTarget
|
||||
from .notification import FilteringNotificationTarget as FilteringNotificationTarget
|
||||
from .notification import MailjetNotificationTarget as MailjetNotificationTarget
|
||||
|
@ -236,6 +238,7 @@ from .observables import CaptureFirstItem as CaptureFirstItem
|
|||
from .observables import CollectingObserverSubscriber as CollectingObserverSubscriber
|
||||
from .observables import Disposable as Disposable
|
||||
from .observables import DisposeWrapper as DisposeWrapper
|
||||
from .observables import DisposingSubject as DisposingSubject
|
||||
from .observables import EventSource as EventSource
|
||||
from .observables import FunctionObserver as FunctionObserver
|
||||
from .observables import LatestItemObserverSubscriber as LatestItemObserverSubscriber
|
||||
|
@ -262,18 +265,19 @@ from .orders import Order as Order
|
|||
from .orders import OrderType as OrderType
|
||||
from .orders import OrderBook as OrderBook
|
||||
from .orders import Side as Side
|
||||
from .ownedinstrumentvalue import OwnedInstrumentValue as OwnedInstrumentValue
|
||||
from .oraclefactory import create_oracle_provider as create_oracle_provider
|
||||
from .output import output as output
|
||||
from .output import output_formatter as output_formatter
|
||||
from .output import OutputFormat as OutputFormat
|
||||
from .output import OutputFormatter as OutputFormatter
|
||||
from .output import to_json as to_json
|
||||
from .ownedinstrumentvalue import OwnedInstrumentValue as OwnedInstrumentValue
|
||||
from .perpaccount import PerpAccount as PerpAccount
|
||||
from .perpeventqueue import PerpEvent as PerpEvent
|
||||
from .perpeventqueue import PerpEventQueue as PerpEventQueue
|
||||
from .perpeventqueue import PerpFillEvent as PerpFillEvent
|
||||
from .perpeventqueue import PerpOutEvent as PerpOutEvent
|
||||
from .perpeventqueue import PerpLiquidateEvent as PerpLiquidateEvent
|
||||
from .perpeventqueue import PerpUnknownEvent as PerpUnknownEvent
|
||||
from .perpeventqueue import (
|
||||
UnseenAccountFillEventTracker as UnseenAccountFillEventTracker,
|
||||
|
@ -281,11 +285,13 @@ from .perpeventqueue import (
|
|||
from .perpeventqueue import (
|
||||
UnseenPerpEventChangesTracker as UnseenPerpEventChangesTracker,
|
||||
)
|
||||
from .perpmarket import FundingRate as FundingRate
|
||||
from .perpmarket import PerpMarket as PerpMarket
|
||||
from .perpmarket import PerpMarketInstructionBuilder as PerpMarketInstructionBuilder
|
||||
from .perpmarket import PerpMarketOperations as PerpMarketOperations
|
||||
from .perpmarket import PerpMarketStub as PerpMarketStub
|
||||
from .perpmarket import PerpOrderBookSide as PerpOrderBookSide
|
||||
from .perpmarketdetails import LiquidityMiningInfo as LiquidityMiningInfo
|
||||
from .perpmarketdetails import PerpMarketDetails as PerpMarketDetails
|
||||
from .perpopenorders import PerpOpenOrders as PerpOpenOrders
|
||||
from .placedorder import PlacedOrder as PlacedOrder
|
||||
|
@ -300,6 +306,8 @@ from .publickey import encode_public_key_for_sorting as encode_public_key_for_so
|
|||
from .reconnectingwebsocket import ReconnectingWebsocket as ReconnectingWebsocket
|
||||
from .retrier import RetryWithPauses as RetryWithPauses
|
||||
from .retrier import retry_context as retry_context
|
||||
from .serumeventqueue import SerumEvent as SerumEvent
|
||||
from .serumeventqueue import SerumEventFlags as SerumEventFlags
|
||||
from .serumeventqueue import SerumEventQueue as SerumEventQueue
|
||||
from .serumeventqueue import (
|
||||
UnseenSerumEventChangesTracker as UnseenSerumEventChangesTracker,
|
||||
|
@ -315,10 +323,6 @@ from .spotmarket import SpotMarketOperations as SpotMarketOperations
|
|||
from .spotmarket import SpotMarketStub as SpotMarketStub
|
||||
from .text import indent_collection_as_str as indent_collection_as_str
|
||||
from .text import indent_item_by as indent_item_by
|
||||
from .tokens import Instrument as Instrument
|
||||
from .tokens import RoundDirection as RoundDirection
|
||||
from .tokens import SolToken as SolToken
|
||||
from .tokens import Token as Token
|
||||
from .tokenaccount import TokenAccount as TokenAccount
|
||||
from .tokenbank import BankBalances as BankBalances
|
||||
from .tokenbank import InterestRates as InterestRates
|
||||
|
@ -328,16 +332,20 @@ from .tokenbank import TokenBank as TokenBank
|
|||
from .tokenoperations import (
|
||||
build_create_associated_instructions_and_account as build_create_associated_instructions_and_account,
|
||||
)
|
||||
from .tokens import Instrument as Instrument
|
||||
from .tokens import RoundDirection as RoundDirection
|
||||
from .tokens import SolToken as SolToken
|
||||
from .tokens import Token as Token
|
||||
from .tradehistory import TradeHistory as TradeHistory
|
||||
from .transactionmonitoring import (
|
||||
SignatureSubscription as SignatureSubscription,
|
||||
)
|
||||
from .transactionmonitoring import (
|
||||
DequeTransactionStatusCollector as DequeTransactionStatusCollector,
|
||||
)
|
||||
from .transactionmonitoring import (
|
||||
NullTransactionStatusCollector as NullTransactionStatusCollector,
|
||||
)
|
||||
from .transactionmonitoring import (
|
||||
SignatureSubscription as SignatureSubscription,
|
||||
)
|
||||
from .transactionmonitoring import (
|
||||
TransactionOutcome as TransactionOutcome,
|
||||
)
|
||||
|
@ -388,9 +396,17 @@ from .watchers import build_orderbook_watcher as build_orderbook_watcher
|
|||
from .watchers import build_serum_event_queue_watcher as build_serum_event_queue_watcher
|
||||
from .watchers import build_spot_event_queue_watcher as build_spot_event_queue_watcher
|
||||
from .watchers import build_perp_event_queue_watcher as build_perp_event_queue_watcher
|
||||
from .websocketsubscription import ActiveWebSocket as ActiveWebSocket
|
||||
|
||||
from .websocketsubscription import (
|
||||
AddressWebSocketSubscription as AddressWebSocketSubscription,
|
||||
)
|
||||
from .websocketsubscription import (
|
||||
IndividualWebSocketSubscriptionManager as IndividualWebSocketSubscriptionManager,
|
||||
)
|
||||
from .websocketsubscription import (
|
||||
LogEvent as LogEvent,
|
||||
)
|
||||
from .websocketsubscription import (
|
||||
SharedWebSocketSubscriptionManager as SharedWebSocketSubscriptionManager,
|
||||
)
|
||||
|
@ -402,6 +418,9 @@ from .websocketsubscription import (
|
|||
WebSocketProgramSubscription as WebSocketProgramSubscription,
|
||||
)
|
||||
from .websocketsubscription import WebSocketSubscription as WebSocketSubscription
|
||||
from .websocketsubscription import (
|
||||
WebSocketSignatureSubscription as WebSocketSignatureSubscription,
|
||||
)
|
||||
from .websocketsubscription import (
|
||||
WebSocketSubscriptionManager as WebSocketSubscriptionManager,
|
||||
)
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
# # ⚠ Warning
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# [🥭 Mango Markets](https://mango.markets/) support is available at:
|
||||
# [Docs](https://docs.mango.markets/)
|
||||
# [Discord](https://discord.gg/67jySBhxrg)
|
||||
# [Twitter](https://twitter.com/mangomarkets)
|
||||
# [Github](https://github.com/blockworks-foundation)
|
||||
# [Email](mailto:hello@blockworks.foundation)
|
||||
|
||||
|
||||
import abc
|
||||
import logging
|
||||
import typing
|
||||
|
||||
from solana.transaction import TransactionInstruction
|
||||
|
||||
from .liquidatablereport import LiquidatableReport
|
||||
|
||||
|
||||
# # 🥭 AccountLiquidator
|
||||
#
|
||||
# An `AccountLiquidator` liquidates an `Account`, if possible.
|
||||
#
|
||||
# The follows the common pattern of having an abstract base class that defines the interface
|
||||
# external code should use, along with a 'null' implementation and at least one full
|
||||
# implementation.
|
||||
#
|
||||
# The idea is that preparing code can choose whether to use the null implementation (in the
|
||||
# case of a 'dry run' for instance) or the full implementation, but the code that defines
|
||||
# the algorithm - which actually calls the `AccountLiquidator` - doesn't have to care about
|
||||
# this choice.
|
||||
#
|
||||
|
||||
# # 💧 AccountLiquidator class
|
||||
#
|
||||
# This abstract base class defines the interface to account liquidators, which in this case
|
||||
# is just the `liquidate()` method.
|
||||
#
|
||||
|
||||
|
||||
class AccountLiquidator(metaclass=abc.ABCMeta):
|
||||
def __init__(self) -> None:
|
||||
self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
@abc.abstractmethod
|
||||
def prepare_instructions(
|
||||
self, liquidatable_report: LiquidatableReport
|
||||
) -> typing.Sequence[TransactionInstruction]:
|
||||
raise NotImplementedError(
|
||||
"AccountLiquidator.prepare_instructions() is not implemented on the base type."
|
||||
)
|
||||
|
||||
@abc.abstractmethod
|
||||
def liquidate(
|
||||
self, liquidatable_report: LiquidatableReport
|
||||
) -> typing.Optional[typing.Sequence[str]]:
|
||||
raise NotImplementedError(
|
||||
"AccountLiquidator.liquidate() is not implemented on the base type."
|
||||
)
|
||||
|
||||
|
||||
# # NullAccountLiquidator class
|
||||
#
|
||||
# A 'null', 'no-op', 'dry run' implementation of the `AccountLiquidator` class.
|
||||
#
|
||||
|
||||
|
||||
class NullAccountLiquidator(AccountLiquidator):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
def prepare_instructions(
|
||||
self, liquidatable_report: LiquidatableReport
|
||||
) -> typing.Sequence[TransactionInstruction]:
|
||||
return []
|
||||
|
||||
def liquidate(
|
||||
self, liquidatable_report: LiquidatableReport
|
||||
) -> typing.Optional[typing.Sequence[str]]:
|
||||
self._logger.info(
|
||||
f"Skipping liquidation of account [{liquidatable_report.account.address}]"
|
||||
)
|
||||
return None
|
|
@ -1,80 +0,0 @@
|
|||
# # ⚠ Warning
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# [🥭 Mango Markets](https://mango.markets/) support is available at:
|
||||
# [Docs](https://docs.mango.markets/)
|
||||
# [Discord](https://discord.gg/67jySBhxrg)
|
||||
# [Twitter](https://twitter.com/mangomarkets)
|
||||
# [Github](https://github.com/blockworks-foundation)
|
||||
# [Email](mailto:hello@blockworks.foundation)
|
||||
|
||||
|
||||
import logging
|
||||
import typing
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from .output import output
|
||||
from .tokens import Token
|
||||
|
||||
|
||||
# # 🥭 BalanceSheet class
|
||||
#
|
||||
class BalanceSheet:
|
||||
def __init__(
|
||||
self,
|
||||
token: Token,
|
||||
liabilities: Decimal,
|
||||
settled_assets: Decimal,
|
||||
unsettled_assets: Decimal,
|
||||
) -> None:
|
||||
self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
||||
self.token: Token = token
|
||||
self.liabilities: Decimal = liabilities
|
||||
self.settled_assets: Decimal = settled_assets
|
||||
self.unsettled_assets: Decimal = unsettled_assets
|
||||
|
||||
@property
|
||||
def assets(self) -> Decimal:
|
||||
return self.settled_assets + self.unsettled_assets
|
||||
|
||||
@property
|
||||
def value(self) -> Decimal:
|
||||
return self.assets - self.liabilities
|
||||
|
||||
@property
|
||||
def collateral_ratio(self) -> Decimal:
|
||||
if self.liabilities == Decimal(0):
|
||||
return Decimal(0)
|
||||
return self.assets / self.liabilities
|
||||
|
||||
@staticmethod
|
||||
def report(
|
||||
values: typing.Sequence["BalanceSheet"],
|
||||
reporter: typing.Callable[[str], None] = output,
|
||||
) -> None:
|
||||
for value in values:
|
||||
reporter(str(value))
|
||||
|
||||
def __str__(self) -> str:
|
||||
name = "«Unspecified»"
|
||||
if self.token is not None:
|
||||
name = self.token.name
|
||||
|
||||
return f"""« BalanceSheet [{name}]:
|
||||
Assets : {self.assets:>18,.8f}
|
||||
Settled Assets : {self.settled_assets:>18,.8f}
|
||||
Unsettled Assets : {self.unsettled_assets:>18,.8f}
|
||||
Liabilities : {self.liabilities:>18,.8f}
|
||||
Value : {self.value:>18,.8f}
|
||||
Collateral Ratio : {self.collateral_ratio:>18,.2%}
|
||||
»
|
||||
"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self}"
|
|
@ -1,72 +0,0 @@
|
|||
# # ⚠ Warning
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# [🥭 Mango Markets](https://mango.markets/) support is available at:
|
||||
# [Docs](https://docs.mango.markets/)
|
||||
# [Discord](https://discord.gg/67jySBhxrg)
|
||||
# [Twitter](https://twitter.com/mangomarkets)
|
||||
# [Github](https://github.com/blockworks-foundation)
|
||||
# [Email](mailto:hello@blockworks.foundation)
|
||||
|
||||
|
||||
import enum
|
||||
import logging
|
||||
import typing
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from .account import Account
|
||||
from .group import Group
|
||||
from .instrumentvalue import InstrumentValue
|
||||
|
||||
|
||||
# # 🥭 LiquidatableState flag enum
|
||||
#
|
||||
# A margin account may have a combination of these flag values.
|
||||
#
|
||||
|
||||
|
||||
class LiquidatableState(enum.Flag):
|
||||
UNSET = 0
|
||||
RIPE = enum.auto()
|
||||
BEING_LIQUIDATED = enum.auto()
|
||||
LIQUIDATABLE = enum.auto()
|
||||
ABOVE_WATER = enum.auto()
|
||||
WORTHWHILE = enum.auto()
|
||||
|
||||
|
||||
# # 🥭 LiquidatableReport class
|
||||
#
|
||||
|
||||
|
||||
class LiquidatableReport:
|
||||
def __init__(
|
||||
self,
|
||||
group: Group,
|
||||
prices: typing.Sequence[InstrumentValue],
|
||||
account: Account,
|
||||
state: LiquidatableState,
|
||||
worthwhile_threshold: Decimal,
|
||||
) -> None:
|
||||
self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
||||
self.group: Group = group
|
||||
self.prices: typing.Sequence[InstrumentValue] = prices
|
||||
self.account: Account = account
|
||||
self.state: LiquidatableState = state
|
||||
self.worthwhile_threshold: Decimal = worthwhile_threshold
|
||||
|
||||
@staticmethod
|
||||
def build(
|
||||
group: Group,
|
||||
prices: typing.Sequence[InstrumentValue],
|
||||
account: Account,
|
||||
worthwhile_threshold: Decimal,
|
||||
) -> "LiquidatableReport":
|
||||
return LiquidatableReport(
|
||||
group, prices, account, LiquidatableState.UNSET, worthwhile_threshold
|
||||
)
|
|
@ -1,68 +0,0 @@
|
|||
# # ⚠ Warning
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# [🥭 Mango Markets](https://mango.markets/) support is available at:
|
||||
# [Docs](https://docs.mango.markets/)
|
||||
# [Discord](https://discord.gg/67jySBhxrg)
|
||||
# [Twitter](https://twitter.com/mangomarkets)
|
||||
# [Github](https://github.com/blockworks-foundation)
|
||||
# [Email](mailto:hello@blockworks.foundation)
|
||||
|
||||
import typing
|
||||
|
||||
from datetime import datetime
|
||||
from solana.publickey import PublicKey
|
||||
|
||||
from .instrumentvalue import InstrumentValue
|
||||
|
||||
|
||||
# # 🥭 LiquidationEvent class
|
||||
#
|
||||
class LiquidationEvent:
|
||||
def __init__(
|
||||
self,
|
||||
timestamp: datetime,
|
||||
liquidator_name: str,
|
||||
group_name: str,
|
||||
succeeded: bool,
|
||||
signatures: typing.Sequence[str],
|
||||
wallet_address: PublicKey,
|
||||
account_address: PublicKey,
|
||||
balances_before: typing.Sequence[InstrumentValue],
|
||||
balances_after: typing.Sequence[InstrumentValue],
|
||||
) -> None:
|
||||
self.timestamp: datetime = timestamp
|
||||
self.liquidator_name: str = liquidator_name
|
||||
self.group_name: str = group_name
|
||||
self.succeeded: bool = succeeded
|
||||
self.signatures: typing.Sequence[str] = signatures
|
||||
self.wallet_address: PublicKey = wallet_address
|
||||
self.account_address: PublicKey = account_address
|
||||
self.balances_before: typing.Sequence[InstrumentValue] = balances_before
|
||||
self.balances_after: typing.Sequence[InstrumentValue] = balances_after
|
||||
self.changes: typing.Sequence[InstrumentValue] = InstrumentValue.changes(
|
||||
balances_before, balances_after
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
result = "✅" if self.succeeded else "❌"
|
||||
changes_text = "\n ".join(
|
||||
[f"{change.value:>15,.8f} {change.token.symbol}" for change in self.changes]
|
||||
)
|
||||
return f"""« 🥭 Liqudation Event {result} at {self.timestamp}
|
||||
💧 Liquidator: {self.liquidator_name}
|
||||
🏫 Group: {self.group_name}
|
||||
📇 Signatures: {self.signatures}
|
||||
👛 Wallet: {self.wallet_address}
|
||||
💳 Margin Account: {self.account_address}
|
||||
💸 Changes:
|
||||
{changes_text}
|
||||
»"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self}"
|
|
@ -1,229 +0,0 @@
|
|||
# # ⚠ Warning
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
# LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#
|
||||
# [🥭 Mango Markets](https://mango.markets/) support is available at:
|
||||
# [Docs](https://docs.mango.markets/)
|
||||
# [Discord](https://discord.gg/67jySBhxrg)
|
||||
# [Twitter](https://twitter.com/mangomarkets)
|
||||
# [Github](https://github.com/blockworks-foundation)
|
||||
# [Email](mailto:hello@blockworks.foundation)
|
||||
|
||||
import enum
|
||||
import logging
|
||||
import time
|
||||
import typing
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from .account import Account
|
||||
from .accountliquidator import AccountLiquidator
|
||||
from .context import Context
|
||||
from .datetimes import local_now
|
||||
from .group import Group
|
||||
from .instrumentvalue import InstrumentValue
|
||||
from .liquidatablereport import LiquidatableReport, LiquidatableState
|
||||
from .liquidationevent import LiquidationEvent
|
||||
from .observables import EventSource
|
||||
from .walletbalancer import WalletBalancer
|
||||
|
||||
# # 🥭 Liquidation Processor
|
||||
#
|
||||
# This file contains a liquidator processor that looks after the mechanics of liquidating an
|
||||
# account.
|
||||
#
|
||||
|
||||
|
||||
# # 💧 LiquidationProcessorState enum
|
||||
#
|
||||
# An enum that describes the current state of the `LiquidationProcessor`.
|
||||
#
|
||||
class LiquidationProcessorState(enum.Enum):
|
||||
STARTING = enum.auto()
|
||||
HEALTHY = enum.auto()
|
||||
UNHEALTHY = enum.auto()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
# # 💧 LiquidationProcessor class
|
||||
#
|
||||
# An `AccountLiquidator` liquidates a `Account`. A `LiquidationProcessor` processes a
|
||||
# list of `Account`s, determines if they're liquidatable, and calls an
|
||||
# `AccountLiquidator` to do the work.
|
||||
#
|
||||
class LiquidationProcessor:
|
||||
_AGE_ERROR_THRESHOLD = timedelta(minutes=5)
|
||||
_AGE_WARNING_THRESHOLD = timedelta(minutes=2)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Context,
|
||||
name: str,
|
||||
account_liquidator: AccountLiquidator,
|
||||
wallet_balancer: WalletBalancer,
|
||||
worthwhile_threshold: Decimal = Decimal("0.01"),
|
||||
) -> None:
|
||||
self._logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
||||
self.context: Context = context
|
||||
self.name: str = name
|
||||
self.account_liquidator: AccountLiquidator = account_liquidator
|
||||
self.wallet_balancer: WalletBalancer = wallet_balancer
|
||||
self.worthwhile_threshold: Decimal = worthwhile_threshold
|
||||
self.liquidations: EventSource[LiquidationEvent] = EventSource[
|
||||
LiquidationEvent
|
||||
]()
|
||||
self.ripe_accounts: typing.Optional[typing.Sequence[Account]] = None
|
||||
self.ripe_accounts_updated_at: datetime = local_now()
|
||||
self.prices_updated_at: datetime = local_now()
|
||||
self.state: LiquidationProcessorState = LiquidationProcessorState.STARTING
|
||||
self.state_change: EventSource[LiquidationProcessor] = EventSource[
|
||||
LiquidationProcessor
|
||||
]()
|
||||
|
||||
def update_accounts(self, ripe_accounts: typing.Sequence[Account]) -> None:
|
||||
self._logger.info(
|
||||
f"Received {len(ripe_accounts)} ripe 🥭 margin accounts to process - prices last updated {self.prices_updated_at:%Y-%m-%d %H:%M:%S}"
|
||||
)
|
||||
self._check_update_recency("prices", self.prices_updated_at)
|
||||
self.ripe_accounts = ripe_accounts
|
||||
self.ripe_accounts_updated_at = local_now()
|
||||
# If this is the first time through, mark ourselves as Healthy.
|
||||
if self.state == LiquidationProcessorState.STARTING:
|
||||
self.state = LiquidationProcessorState.HEALTHY
|
||||
|
||||
def update_prices(
|
||||
self, group: Group, prices: typing.Sequence[InstrumentValue]
|
||||
) -> None:
|
||||
started_at = time.time()
|
||||
|
||||
if self.state == LiquidationProcessorState.STARTING:
|
||||
self._logger.info("Still starting - skipping price update.")
|
||||
return
|
||||
|
||||
if self.ripe_accounts is None:
|
||||
self._logger.info("Ripe accounts is None - skipping price update.")
|
||||
return
|
||||
|
||||
self._logger.info(
|
||||
f"Ripe accounts last updated {self.ripe_accounts_updated_at:%Y-%m-%d %H:%M:%S}"
|
||||
)
|
||||
self._check_update_recency("ripe account", self.ripe_accounts_updated_at)
|
||||
|
||||
report: typing.List[str] = []
|
||||
updated: typing.List[LiquidatableReport] = []
|
||||
for account in self.ripe_accounts:
|
||||
updated += [
|
||||
LiquidatableReport.build(
|
||||
group, prices, account, self.worthwhile_threshold
|
||||
)
|
||||
]
|
||||
|
||||
liquidatable = list(
|
||||
filter(
|
||||
lambda report: report.state & LiquidatableState.LIQUIDATABLE, updated
|
||||
)
|
||||
)
|
||||
report += [
|
||||
f"Of those {len(updated)} ripe accounts, {len(liquidatable)} are liquidatable."
|
||||
]
|
||||
|
||||
above_water = list(
|
||||
filter(
|
||||
lambda report: report.state & LiquidatableState.ABOVE_WATER,
|
||||
liquidatable,
|
||||
)
|
||||
)
|
||||
report += [
|
||||
f"Of those {len(liquidatable)} liquidatable margin accounts, {len(above_water)} have assets greater than their liabilities."
|
||||
]
|
||||
|
||||
worthwhile = list(
|
||||
filter(
|
||||
lambda report: report.state & LiquidatableState.WORTHWHILE, above_water
|
||||
)
|
||||
)
|
||||
report += [
|
||||
f"Of those {len(above_water)} above water margin accounts, {len(worthwhile)} are worthwhile margin accounts with more than ${self.worthwhile_threshold} net assets."
|
||||
]
|
||||
|
||||
report_text = "\n ".join(report)
|
||||
self._logger.info(
|
||||
f"""Running on {len(self.ripe_accounts)} ripe accounts:
|
||||
{report_text}"""
|
||||
)
|
||||
|
||||
self._liquidate_all(group, prices, worthwhile)
|
||||
|
||||
self.prices_updated_at = local_now()
|
||||
time_taken = time.time() - started_at
|
||||
self._logger.info(
|
||||
f"Check of all ripe 🥭 accounts complete. Time taken: {time_taken:.2f} seconds."
|
||||
)
|
||||
|
||||
def _liquidate_all(
|
||||
self,
|
||||
group: Group,
|
||||
prices: typing.Sequence[InstrumentValue],
|
||||
to_liquidate: typing.Sequence[LiquidatableReport],
|
||||
) -> None:
|
||||
to_process = list(to_liquidate)
|
||||
while len(to_process) > 0:
|
||||
# TODO - sort this when LiquidationReport has the proper details for V3.
|
||||
# highest_first = sorted(to_process,
|
||||
# key=lambda report: report.balance_sheet.assets - report.balance_sheet.liabilities, reverse=True)
|
||||
highest_first = to_process
|
||||
highest = highest_first[0]
|
||||
try:
|
||||
self.account_liquidator.liquidate(highest)
|
||||
self.wallet_balancer.balance(self.context, prices)
|
||||
|
||||
updated_account = Account.load(
|
||||
self.context, highest.account.address, group
|
||||
)
|
||||
updated_report = LiquidatableReport.build(
|
||||
group, prices, updated_account, highest.worthwhile_threshold
|
||||
)
|
||||
if not (updated_report.state & LiquidatableState.WORTHWHILE):
|
||||
self._logger.info(
|
||||
f"Margin account {updated_account.address} has been drained and is no longer worthwhile."
|
||||
)
|
||||
else:
|
||||
self._logger.info(
|
||||
f"Margin account {updated_account.address} is still worthwhile - putting it back on list."
|
||||
)
|
||||
to_process += [updated_report]
|
||||
except Exception as exception:
|
||||
self._logger.error(
|
||||
f"[{self.name}] Failed to liquidate account '{highest.account.address}' - {exception}."
|
||||
)
|
||||
finally:
|
||||
# highest should always be in to_process, but we're outside the try-except block
|
||||
# so let's be a little paranoid about it.
|
||||
self._logger.info(
|
||||
f"Liquidatable accounts to process was: {len(to_process)}"
|
||||
)
|
||||
if highest in to_process:
|
||||
to_process.remove(highest)
|
||||
self._logger.info(
|
||||
f"Liquidatable accounts to process is now: {len(to_process)}"
|
||||
)
|
||||
|
||||
def _check_update_recency(self, name: str, last_updated_at: datetime) -> None:
|
||||
how_long_ago_was_last_update = local_now() - last_updated_at
|
||||
if how_long_ago_was_last_update > LiquidationProcessor._AGE_ERROR_THRESHOLD:
|
||||
self.state = LiquidationProcessorState.UNHEALTHY
|
||||
self.state_change.on_next(self)
|
||||
self._logger.error(
|
||||
f"[{self.name}] Liquidator - last {name} update was {how_long_ago_was_last_update} ago - more than error threshold {LiquidationProcessor._AGE_ERROR_THRESHOLD}"
|
||||
)
|
||||
elif how_long_ago_was_last_update > LiquidationProcessor._AGE_WARNING_THRESHOLD:
|
||||
self._logger.warning(
|
||||
f"[{self.name}] Liquidator - last {name} update was {how_long_ago_was_last_update} ago - more than warning threshold {LiquidationProcessor._AGE_WARNING_THRESHOLD}"
|
||||
)
|
|
@ -15,15 +15,12 @@
|
|||
|
||||
|
||||
import abc
|
||||
import csv
|
||||
import logging
|
||||
import os.path
|
||||
import requests
|
||||
import typing
|
||||
|
||||
from urllib.parse import unquote
|
||||
|
||||
from .liquidationevent import LiquidationEvent
|
||||
from .output import output
|
||||
|
||||
|
||||
|
@ -214,53 +211,6 @@ class MailjetNotificationTarget(NotificationTarget):
|
|||
return f"« MailjetNotificationTarget To: '{self.to_name}' '{self.to_address}' with subject '{self.subject}' »"
|
||||
|
||||
|
||||
# # 🥭 CsvFileNotificationTarget class
|
||||
#
|
||||
# Outputs a liquidation event to CSV. Nothing is written if the item is not a
|
||||
# `LiquidationEvent`.
|
||||
#
|
||||
# Headers for the CSV file should be:
|
||||
# ```
|
||||
# "Timestamp","Liquidator Name","Group","Succeeded","Signature","Wallet","Margin Account","Token Changes"
|
||||
# ```
|
||||
# Token changes are listed as pairs of value plus symbol, so each token change adds two
|
||||
# columns to the output. Token changes may arrive in different orders, so ordering of token
|
||||
# changes is not guaranteed to be consistent from transaction to transaction.
|
||||
#
|
||||
class CsvFileNotificationTarget(NotificationTarget):
|
||||
def __init__(self, filename: str) -> None:
|
||||
super().__init__()
|
||||
self.filename = filename
|
||||
|
||||
def send_notification(self, item: typing.Any) -> None:
|
||||
if isinstance(item, LiquidationEvent):
|
||||
event: LiquidationEvent = item
|
||||
if not os.path.isfile(self.filename) or os.path.getsize(self.filename) == 0:
|
||||
with open(self.filename, "w") as empty_file:
|
||||
empty_file.write(
|
||||
'"Timestamp","Liquidator Name","Group","Succeeded","Signature","Wallet","Margin Account","Token Changes"\n'
|
||||
)
|
||||
|
||||
with open(self.filename, "a") as csvfile:
|
||||
result = "Succeeded" if event.succeeded else "Failed"
|
||||
row_data = [
|
||||
event.timestamp,
|
||||
event.liquidator_name,
|
||||
event.group_name,
|
||||
result,
|
||||
" ".join(event.signatures),
|
||||
event.wallet_address,
|
||||
event.account_address,
|
||||
]
|
||||
for change in event.changes:
|
||||
row_data += [f"{change.value:.8f}", change.token.name]
|
||||
file_writer = csv.writer(csvfile, quoting=csv.QUOTE_MINIMAL)
|
||||
file_writer.writerow(row_data)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"« CsvFileNotificationTarget File: {self.filename} »"
|
||||
|
||||
|
||||
# # 🥭 FilteringNotificationTarget class
|
||||
#
|
||||
# This class takes a `NotificationTarget` and a filter function, and only calls the
|
||||
|
@ -351,8 +301,6 @@ def parse_notification_target(target: str) -> NotificationTarget:
|
|||
return DiscordNotificationTarget(destination)
|
||||
elif protocol == "mailjet":
|
||||
return MailjetNotificationTarget(destination)
|
||||
elif protocol == "csvfile":
|
||||
return CsvFileNotificationTarget(destination)
|
||||
elif protocol == "console":
|
||||
return ConsoleNotificationTarget(destination)
|
||||
else:
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
from .context import mango
|
||||
|
||||
|
||||
def test_account_liquidator_constructor() -> None:
|
||||
succeeded = False
|
||||
try:
|
||||
mango.AccountLiquidator() # type: ignore[abstract]
|
||||
except TypeError:
|
||||
# Can't instantiate the abstract base class.
|
||||
succeeded = True
|
||||
assert succeeded
|
||||
|
||||
|
||||
def test_null_account_liquidator_constructor() -> None:
|
||||
actual = mango.NullAccountLiquidator()
|
||||
assert actual is not None
|
|
@ -1,9 +0,0 @@
|
|||
from .context import mango
|
||||
from .fakes import fake_token
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
def test_constructor() -> None:
|
||||
actual = mango.BalanceSheet(fake_token(), Decimal(0), Decimal(0), Decimal(0))
|
||||
assert actual is not None
|
|
@ -1,45 +0,0 @@
|
|||
from .context import mango
|
||||
from .fakes import fake_public_key, fake_token
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
import datetime
|
||||
|
||||
|
||||
def test_liquidation_event() -> None:
|
||||
balances_before = [
|
||||
mango.InstrumentValue(fake_token("ETH"), Decimal(1)),
|
||||
mango.InstrumentValue(fake_token("BTC"), Decimal("0.1")),
|
||||
mango.InstrumentValue(fake_token("USDT"), Decimal(1000)),
|
||||
]
|
||||
balances_after = [
|
||||
mango.InstrumentValue(fake_token("ETH"), Decimal(1)),
|
||||
mango.InstrumentValue(fake_token("BTC"), Decimal("0.05")),
|
||||
mango.InstrumentValue(fake_token("USDT"), Decimal(2000)),
|
||||
]
|
||||
timestamp = datetime.datetime(2021, 5, 17, 12, 20, 56)
|
||||
event = mango.LiquidationEvent(
|
||||
timestamp,
|
||||
"Liquidator",
|
||||
"Group",
|
||||
True,
|
||||
["signature"],
|
||||
fake_public_key(),
|
||||
fake_public_key(),
|
||||
balances_before,
|
||||
balances_after,
|
||||
)
|
||||
assert (
|
||||
str(event)
|
||||
== """« 🥭 Liqudation Event ✅ at 2021-05-17 12:20:56
|
||||
💧 Liquidator: Liquidator
|
||||
🏫 Group: Group
|
||||
📇 Signatures: ['signature']
|
||||
👛 Wallet: 11111111111111111111111111111112
|
||||
💳 Margin Account: 11111111111111111111111111111112
|
||||
💸 Changes:
|
||||
0.00000000 ETH
|
||||
-0.05000000 BTC
|
||||
1,000.00000000 USDT
|
||||
»"""
|
||||
)
|
|
@ -15,7 +15,7 @@ class MockNotificationTarget(mango.NotificationTarget):
|
|||
def test_notification_target_constructor() -> None:
|
||||
succeeded = False
|
||||
try:
|
||||
mango.AccountLiquidator() # type: ignore[abstract]
|
||||
mango.NotificationTarget() # type: ignore[abstract]
|
||||
except TypeError:
|
||||
# Can't instantiate the abstract base class.
|
||||
succeeded = True
|
||||
|
@ -50,13 +50,6 @@ def test_mailjet_notification_target_constructor() -> None:
|
|||
assert actual.to_address == "to@address"
|
||||
|
||||
|
||||
def test_csvfile_notification_target_constructor() -> None:
|
||||
filename = "test-filename"
|
||||
actual = mango.CsvFileNotificationTarget(filename)
|
||||
assert actual is not None
|
||||
assert actual.filename == filename
|
||||
|
||||
|
||||
def test_filtering_notification_target_constructor() -> None:
|
||||
mock = MockNotificationTarget()
|
||||
|
||||
|
@ -93,6 +86,3 @@ def test_parse_notification_target() -> None:
|
|||
"mailjet:user:secret:subject:from%20name:from@address:to%20name%20with%20colon%3A:to@address"
|
||||
)
|
||||
assert mailjet_target is not None
|
||||
|
||||
csvfile_target = mango.parse_notification_target("csvfile:filename.csv")
|
||||
assert csvfile_target is not None
|
||||
|
|
Loading…
Reference in New Issue