Ensured all public classes are exposed and available. Removed unused classes.

This commit is contained in:
Geoff Taylor 2022-03-22 19:18:26 +00:00
parent db398e5f8b
commit 8bbe48abaf
12 changed files with 55 additions and 733 deletions

View File

@ -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.

View File

@ -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,
)

View File

@ -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

View File

@ -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}"

View File

@ -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
)

View File

@ -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}"

View File

@ -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}"
)

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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
»"""
)

View File

@ -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