2021-06-07 07:10:18 -07:00
# # ⚠ 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)
2021-06-21 03:06:26 -07:00
import enum
2021-06-07 07:10:18 -07:00
import logging
import time
import typing
2021-06-07 12:35:26 -07:00
from datetime import datetime , timedelta
2021-06-07 07:10:18 -07:00
from decimal import Decimal
2021-06-25 07:50:37 -07:00
from . account import Account
2021-06-07 07:10:18 -07:00
from . accountliquidator import AccountLiquidator
from . context import Context
from . group import Group
2021-11-08 03:39:09 -08:00
from . instrumentvalue import InstrumentValue
2021-06-15 09:30:25 -07:00
from . liquidatablereport import LiquidatableReport , LiquidatableState
2021-06-07 07:10:18 -07:00
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.
#
2021-06-21 03:06:26 -07:00
# # 💧 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 ( )
2021-08-01 10:03:46 -07:00
def __str__ ( self ) - > str :
2021-06-21 03:06:26 -07:00
return self . name
2021-06-07 07:10:18 -07:00
# # 💧 LiquidationProcessor class
#
2021-06-25 07:50:37 -07:00
# An `AccountLiquidator` liquidates a `Account`. A `LiquidationProcessor` processes a
# list of `Account`s, determines if they're liquidatable, and calls an
2021-06-07 07:10:18 -07:00
# `AccountLiquidator` to do the work.
#
class LiquidationProcessor :
2021-08-16 14:32:28 -07:00
_AGE_ERROR_THRESHOLD = timedelta ( minutes = 5 )
_AGE_WARNING_THRESHOLD = timedelta ( minutes = 2 )
2021-06-07 12:35:26 -07:00
2021-11-09 05:23:36 -08:00
def __init__ ( self , context : Context , name : str , account_liquidator : AccountLiquidator , wallet_balancer : WalletBalancer , worthwhile_threshold : Decimal = Decimal ( " 0.01 " ) ) - > None :
2021-12-13 03:15:24 -08:00
self . _logger : logging . Logger = logging . getLogger ( self . __class__ . __name__ )
2021-06-07 07:10:18 -07:00
self . context : Context = context
2021-06-21 03:06:26 -07:00
self . name : str = name
2021-06-07 07:10:18 -07:00
self . account_liquidator : AccountLiquidator = account_liquidator
self . wallet_balancer : WalletBalancer = wallet_balancer
self . worthwhile_threshold : Decimal = worthwhile_threshold
self . liquidations : EventSource [ LiquidationEvent ] = EventSource [ LiquidationEvent ] ( )
2021-06-25 07:50:37 -07:00
self . ripe_accounts : typing . Optional [ typing . Sequence [ Account ] ] = None
2021-06-07 12:35:26 -07:00
self . ripe_accounts_updated_at : datetime = datetime . now ( )
self . prices_updated_at : datetime = datetime . now ( )
2021-06-21 03:06:26 -07:00
self . state : LiquidationProcessorState = LiquidationProcessorState . STARTING
self . state_change : EventSource [ LiquidationProcessor ] = EventSource [ LiquidationProcessor ] ( )
2021-06-07 07:10:18 -07:00
2021-11-09 05:23:36 -08:00
def update_accounts ( self , ripe_accounts : typing . Sequence [ Account ] ) - > None :
2021-12-13 03:15:24 -08:00
self . _logger . info (
2021-07-12 10:26:35 -07:00
f " Received { len ( ripe_accounts ) } ripe 🥭 margin accounts to process - prices last updated { self . prices_updated_at : %Y-%m-%d %H:%M:%S } " )
2021-06-07 12:35:26 -07:00
self . _check_update_recency ( " prices " , self . prices_updated_at )
2021-07-12 10:26:35 -07:00
self . ripe_accounts = ripe_accounts
2021-06-07 12:35:26 -07:00
self . ripe_accounts_updated_at = datetime . now ( )
2021-06-21 03:06:26 -07:00
# If this is the first time through, mark ourselves as Healthy.
if self . state == LiquidationProcessorState . STARTING :
self . state = LiquidationProcessorState . HEALTHY
2021-06-07 07:10:18 -07:00
2021-11-09 05:23:36 -08:00
def update_prices ( self , group : Group , prices : typing . Sequence [ InstrumentValue ] ) - > None :
2021-06-07 07:10:18 -07:00
started_at = time . time ( )
2021-06-21 03:06:26 -07:00
if self . state == LiquidationProcessorState . STARTING :
2021-12-13 03:15:24 -08:00
self . _logger . info ( " Still starting - skipping price update. " )
2021-06-21 03:06:26 -07:00
return
2021-06-07 07:10:18 -07:00
if self . ripe_accounts is None :
2021-12-13 03:15:24 -08:00
self . _logger . info ( " Ripe accounts is None - skipping price update. " )
2021-06-07 07:10:18 -07:00
return
2021-12-13 03:15:24 -08:00
self . _logger . info (
2021-06-16 12:54:11 -07:00
f " Ripe accounts last updated { self . ripe_accounts_updated_at : %Y-%m-%d %H:%M:%S } " )
2021-06-07 12:35:26 -07:00
self . _check_update_recency ( " ripe account " , self . ripe_accounts_updated_at )
2021-06-16 12:54:11 -07:00
report : typing . List [ str ] = [ ]
2021-06-15 09:30:25 -07:00
updated : typing . List [ LiquidatableReport ] = [ ]
2021-07-12 10:26:35 -07:00
for account in self . ripe_accounts :
updated + = [ LiquidatableReport . build ( group , prices , account , self . worthwhile_threshold ) ]
2021-06-07 07:10:18 -07:00
2021-06-15 09:30:25 -07:00
liquidatable = list ( filter ( lambda report : report . state & LiquidatableState . LIQUIDATABLE , updated ) )
2021-06-16 12:54:11 -07:00
report + = [ f " Of those { len ( updated ) } ripe accounts, { len ( liquidatable ) } are liquidatable. " ]
2021-06-07 07:10:18 -07:00
2021-06-15 09:30:25 -07:00
above_water = list ( filter ( lambda report : report . state & LiquidatableState . ABOVE_WATER , liquidatable ) )
2021-06-16 12:54:11 -07:00
report + = [ f " Of those { len ( liquidatable ) } liquidatable margin accounts, { len ( above_water ) } have assets greater than their liabilities. " ]
2021-06-07 07:10:18 -07:00
2021-06-15 09:30:25 -07:00
worthwhile = list ( filter ( lambda report : report . state & LiquidatableState . WORTHWHILE , above_water ) )
2021-06-16 12:54:11 -07:00
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 )
2021-12-13 03:15:24 -08:00
self . _logger . info ( f """ Running on { len ( self . ripe_accounts ) } ripe accounts:
2021-06-16 12:54:11 -07:00
{ report_text } """ )
2021-06-07 07:10:18 -07:00
self . _liquidate_all ( group , prices , worthwhile )
2021-06-07 12:35:26 -07:00
self . prices_updated_at = datetime . now ( )
2021-06-07 07:10:18 -07:00
time_taken = time . time ( ) - started_at
2021-12-13 03:15:24 -08:00
self . _logger . info ( f " Check of all ripe 🥭 accounts complete. Time taken: { time_taken : .2f } seconds. " )
2021-06-07 07:10:18 -07:00
2021-11-09 05:23:36 -08:00
def _liquidate_all ( self , group : Group , prices : typing . Sequence [ InstrumentValue ] , to_liquidate : typing . Sequence [ LiquidatableReport ] ) - > None :
2021-06-25 07:50:37 -07:00
to_process = list ( to_liquidate )
2021-06-07 07:10:18 -07:00
while len ( to_process ) > 0 :
2021-06-25 07:50:37 -07:00
# 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
2021-06-07 07:10:18 -07:00
highest = highest_first [ 0 ]
try :
2021-06-15 09:30:25 -07:00
self . account_liquidator . liquidate ( highest )
2021-09-07 10:40:34 -07:00
self . wallet_balancer . balance ( self . context , prices )
2021-06-07 07:10:18 -07:00
2021-07-12 10:26:35 -07:00
updated_account = Account . load ( self . context , highest . account . address , group )
2021-06-15 09:30:25 -07:00
updated_report = LiquidatableReport . build (
2021-07-12 10:26:35 -07:00
group , prices , updated_account , highest . worthwhile_threshold )
2021-06-15 09:30:25 -07:00
if not ( updated_report . state & LiquidatableState . WORTHWHILE ) :
2021-12-13 03:15:24 -08:00
self . _logger . info (
2021-07-12 10:26:35 -07:00
f " Margin account { updated_account . address } has been drained and is no longer worthwhile. " )
2021-06-07 07:10:18 -07:00
else :
2021-12-13 03:15:24 -08:00
self . _logger . info (
2021-07-12 10:26:35 -07:00
f " Margin account { updated_account . address } is still worthwhile - putting it back on list. " )
2021-06-15 09:30:25 -07:00
to_process + = [ updated_report ]
2021-06-07 07:10:18 -07:00
except Exception as exception :
2021-12-13 03:15:24 -08:00
self . _logger . error (
2021-08-09 02:27:47 -07:00
f " [ { self . name } ] Failed to liquidate account ' { highest . account . address } ' - { exception } . " )
2021-06-07 07:10:18 -07:00
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.
2021-12-13 03:15:24 -08:00
self . _logger . info ( f " Liquidatable accounts to process was: { len ( to_process ) } " )
2021-06-07 07:10:18 -07:00
if highest in to_process :
to_process . remove ( highest )
2021-12-13 03:15:24 -08:00
self . _logger . info ( f " Liquidatable accounts to process is now: { len ( to_process ) } " )
2021-06-07 12:35:26 -07:00
def _check_update_recency ( self , name : str , last_updated_at : datetime ) - > None :
how_long_ago_was_last_update = datetime . now ( ) - last_updated_at
if how_long_ago_was_last_update > LiquidationProcessor . _AGE_ERROR_THRESHOLD :
2021-06-21 03:06:26 -07:00
self . state = LiquidationProcessorState . UNHEALTHY
self . state_change . on_next ( self )
2021-12-13 03:15:24 -08:00
self . _logger . error (
2021-08-09 02:27:47 -07:00
f " [ { self . name } ] Liquidator - last { name } update was { how_long_ago_was_last_update } ago - more than error threshold { LiquidationProcessor . _AGE_ERROR_THRESHOLD } " )
2021-06-07 12:35:26 -07:00
elif how_long_ago_was_last_update > LiquidationProcessor . _AGE_WARNING_THRESHOLD :
2021-12-13 03:15:24 -08:00
self . _logger . warning (
2021-08-09 02:27:47 -07:00
f " [ { self . name } ] Liquidator - last { name } update was { how_long_ago_was_last_update } ago - more than warning threshold { LiquidationProcessor . _AGE_WARNING_THRESHOLD } " )