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)
import abc
import logging
import typing
from decimal import Decimal
2021-09-07 10:40:34 -07:00
from . account import Account
2021-06-07 07:10:18 -07:00
from . context import Context
2021-10-04 10:27:07 -07:00
from . group import Group
2021-11-08 03:39:09 -08:00
from . token import Instrument , Token
from . instrumentvalue import InstrumentValue
2021-06-07 07:10:18 -07:00
from . tradeexecutor import TradeExecutor
from . wallet import Wallet
# # 🥭 WalletBalancer
#
# This notebook deals with balancing a wallet after processing liquidations, so that it has
# appropriate funds for the next liquidation.
#
# We want to be able to maintain liquidity in our wallet. For instance if there are a lot of
# ETH shorts being liquidated, we'll need to supply ETH, but what do we do when we run out
# of ETH and there are still liquidations to perform?
#
# We 'balance' our wallet tokens, buying or selling or swapping them as required.
#
# # 🥭 Target Balances
#
# To be able to maintain the right balance of tokens, we need to know what the right
# balance is. Different people have different opinions, and we don't all have the same
# value in our liquidator accounts, so we need a way to allow whoever is running the
# liquidator to specify what the target token balances should be.
#
# There are two possible approaches to specifying the target value:
# * A 'fixed' value, like 10 ETH
# * A 'percentage' value, like 20% ETH
#
# Percentage is trickier, because to come up with the actual target we need to take into
# account the wallet value and the current price of the target token.
#
# The way this all hangs together is:
# * A parser parses string values (probably from a command-line) into `TargetBalance`
# objects.
# * There are two types of `TargetBalance` objects - `FixedTargetBalance` and
# `PercentageTargetBalance`.
2021-11-08 03:39:09 -08:00
# * To get the actual `InstrumentValue` for balancing, the `TargetBalance` must be 'resolved'
2021-06-07 07:10:18 -07:00
# by calling `resolve()` with the appropriate token price and wallet value.
#
# # 🥭 TargetBalance class
#
# This is the abstract base class for our target balances, to allow them to be treated polymorphically.
#
class TargetBalance ( metaclass = abc . ABCMeta ) :
2021-11-09 05:23:36 -08:00
def __init__ ( self , symbol : str ) - > None :
2021-09-06 13:38:12 -07:00
self . symbol = symbol . upper ( )
2021-06-07 07:10:18 -07:00
@abc.abstractmethod
2021-11-08 03:39:09 -08:00
def resolve ( self , instrument : Instrument , current_price : Decimal , total_value : Decimal ) - > InstrumentValue :
2021-06-07 07:10:18 -07:00
raise NotImplementedError ( " TargetBalance.resolve() is not implemented on the base type. " )
def __repr__ ( self ) - > str :
return f " { self } "
# # 🥭 FixedTargetBalance class
#
2021-11-08 03:39:09 -08:00
# This is the simple case, where the `FixedTargetBalance` object contains enough information on its own to build the resolved `InstrumentValue` object.
2021-06-07 07:10:18 -07:00
#
class FixedTargetBalance ( TargetBalance ) :
2021-11-09 05:23:36 -08:00
def __init__ ( self , symbol : str , value : Decimal ) - > None :
2021-09-06 13:38:12 -07:00
super ( ) . __init__ ( symbol )
2021-06-07 07:10:18 -07:00
self . value = value
2021-11-08 03:39:09 -08:00
def resolve ( self , instrument : Instrument , current_price : Decimal , total_value : Decimal ) - > InstrumentValue :
return InstrumentValue ( instrument , self . value )
2021-06-07 07:10:18 -07:00
def __str__ ( self ) - > str :
2021-09-06 13:38:12 -07:00
return f """ « 𝙵 𝚒 𝚡 𝚎 𝚍 𝚃 𝚊 𝚛 𝚐 𝚎 𝚝 𝙱 𝚊 𝚕 𝚊 𝚗 𝚌 𝚎 [ { self . value } { self . symbol } ] » """
2021-06-07 07:10:18 -07:00
# # 🥭 PercentageTargetBalance
#
# This is the more complex case, where the target is a percentage of the total wallet
# balance.
#
# So, to actually calculate the right target, we need to know the total wallet balance and
# the current price. Once we have those the calculation is just:
# >
# > _wallet fraction_ is _percentage_ of _wallet value_
# >
# > _target balance_ is _wallet fraction_ divided by _token price_
#
class PercentageTargetBalance ( TargetBalance ) :
2021-11-09 05:23:36 -08:00
def __init__ ( self , symbol : str , target_percentage : Decimal ) - > None :
2021-09-06 13:38:12 -07:00
super ( ) . __init__ ( symbol )
2021-06-07 07:10:18 -07:00
self . target_fraction = target_percentage / 100
2021-11-08 03:39:09 -08:00
def resolve ( self , instrument : Instrument , current_price : Decimal , total_value : Decimal ) - > InstrumentValue :
2021-06-07 07:10:18 -07:00
target_value = total_value * self . target_fraction
target_size = target_value / current_price
2021-11-08 03:39:09 -08:00
return InstrumentValue ( instrument , target_size )
2021-06-07 07:10:18 -07:00
def __str__ ( self ) - > str :
2021-09-06 13:38:12 -07:00
return f """ « 𝙿 𝚎 𝚛 𝚌 𝚎 𝚗 𝚝 𝚊 𝚐 𝚎 𝚃 𝚊 𝚛 𝚐 𝚎 𝚝 𝙱 𝚊 𝚕 𝚊 𝚗 𝚌 𝚎 [ { self . target_fraction * 100 } % { self . symbol } ] » """
2021-06-07 07:10:18 -07:00
2021-09-06 13:38:12 -07:00
# # 🥭 parse_target_balance function
2021-06-07 07:10:18 -07:00
#
2021-09-06 13:38:12 -07:00
# `argparse` handler for `TargetBalance` parsing. Can be used like:
# parser.add_argument("--target", type=mango.parse_target_balance, action="append", required=True,
# help="token symbol plus target value or percentage, separated by a colon (e.g. 'ETH:2.5')")
2021-06-07 07:10:18 -07:00
#
2021-09-06 13:38:12 -07:00
def parse_target_balance ( to_parse : str ) - > TargetBalance :
try :
symbol , value = to_parse . split ( " : " )
except Exception as exception :
raise Exception ( f " Could not parse target balance ' { to_parse } ' " ) from exception
2021-06-07 07:10:18 -07:00
2021-09-06 13:38:12 -07:00
# The value we have may be an int (like 27), a fraction (like 0.1) or a percentage
# (like 25%). In all cases we want the number as a number, but we also want to know if
# we have a percent or not
values = value . split ( " % " )
numeric_value_string = values [ 0 ]
try :
numeric_value = Decimal ( numeric_value_string )
except Exception as exception :
raise Exception (
f " Could not parse ' { numeric_value_string } ' as a decimal number. It should be formatted as a decimal number, e.g. ' 2.345 ' , with no surrounding spaces. " ) from exception
2021-06-07 07:10:18 -07:00
2021-09-06 13:38:12 -07:00
if len ( values ) > 2 :
raise Exception (
f " Could not parse ' { value } ' as a decimal percentage. It should be formatted as a decimal number followed by a percentage sign, e.g. ' 30% ' , with no surrounding spaces. " )
2021-06-07 07:10:18 -07:00
2021-09-06 13:38:12 -07:00
if len ( values ) == 1 :
return FixedTargetBalance ( symbol , numeric_value )
else :
return PercentageTargetBalance ( symbol , numeric_value )
2021-06-07 07:10:18 -07:00
2021-09-07 10:40:34 -07:00
# # 🥭 parse_fixed_target_balance function
#
# `argparse` handler for `TargetBalance` parsing. Can only be used for `FixedTargetBalance`s - will raise an
# error if a `PercentageTargetBalance` is attempted. This is useful for circumstances where percentage
# balance targets aren't allowed.
#
# Can be used like:
# parser.add_argument("--target", type=mango.parse_fixed_target_balance, action="append", required=True,
# help="token symbol plus target value or percentage, separated by a colon (e.g. 'ETH:2.5')")
#
def parse_fixed_target_balance ( to_parse : str ) - > TargetBalance :
try :
symbol , value = to_parse . split ( " : " )
except Exception as exception :
raise Exception ( f " Could not parse target balance ' { to_parse } ' " ) from exception
# The value we have may be an int (like 27)or a fraction (like 0.1). In all cases we want the number
# as a number, but we also want to know if we have a percent or not so we can raise an exception.
values = value . split ( " % " )
if len ( values ) > 1 :
raise Exception (
f " Could not parse ' { value } ' as a decimal target. (Percentage targets are not allowed in this context.) " )
numeric_value_string = values [ 0 ]
try :
numeric_value = Decimal ( numeric_value_string )
except Exception as exception :
raise Exception (
f " Could not parse ' { numeric_value_string } ' as a decimal number. It should be formatted as a decimal number, e.g. ' 2.345 ' , with no surrounding spaces. " ) from exception
return FixedTargetBalance ( symbol , numeric_value )
2021-06-07 07:10:18 -07:00
# # 🥭 sort_changes_for_trades function
#
# It's important to process SELLs first, so we have enough funds in the quote balance for the
# BUYs.
#
# It looks like this function takes size into account, but it doesn't really - 2 ETH is
# smaller than 1 BTC (for now?) but the value 2 will be treated as bigger than 1. We don't
# really care that much as long as we have SELLs before BUYs. (We could, later, take price
# into account for this sorting but we don't need to now so we don't.)
#
2021-11-08 03:39:09 -08:00
def sort_changes_for_trades ( changes : typing . Sequence [ InstrumentValue ] ) - > typing . Sequence [ InstrumentValue ] :
2021-06-07 07:10:18 -07:00
return sorted ( changes , key = lambda change : change . value )
# # 🥭 calculate_required_balance_changes function
#
# Takes a list of current balances, and a list of desired balances, and returns the list of changes required to get us to the desired balances.
#
2021-11-08 03:39:09 -08:00
def calculate_required_balance_changes ( current_balances : typing . Sequence [ InstrumentValue ] , desired_balances : typing . Sequence [ InstrumentValue ] ) - > typing . Sequence [ InstrumentValue ] :
changes : typing . List [ InstrumentValue ] = [ ]
2021-06-07 07:10:18 -07:00
for desired in desired_balances :
2021-11-08 03:39:09 -08:00
current = InstrumentValue . find_by_token ( current_balances , desired . token )
change = InstrumentValue ( desired . token , desired . value - current . value )
2021-06-07 07:10:18 -07:00
changes + = [ change ]
return changes
# # 🥭 FilterSmallChanges class
#
# Allows us to filter out changes that aren't worth the effort.
#
# For instance, if our desired balance requires changing less than 1% of our total balance,
# it may not be worth bothering with right not.
#
# Calculations are based on the total wallet balance, rather than the magnitude of the
# change per-token, because a change of 0.01 of one token may be worth more than a change
# of 10 in another token. Normalising values to our wallet balance makes these changes
# easier to reason about.
#
class FilterSmallChanges :
2021-11-09 05:23:36 -08:00
def __init__ ( self , action_threshold : Decimal , balances : typing . Sequence [ InstrumentValue ] ,
prices : typing . Sequence [ InstrumentValue ] ) - > None :
2021-06-07 07:10:18 -07:00
self . logger : logging . Logger = logging . getLogger ( self . __class__ . __name__ )
2021-11-08 03:39:09 -08:00
self . prices : typing . Dict [ str , InstrumentValue ] = { }
2021-06-07 07:10:18 -07:00
total = Decimal ( 0 )
for balance in balances :
2021-11-08 03:39:09 -08:00
price = InstrumentValue . find_by_token ( prices , balance . token )
self . prices [ price . token . symbol ] = price
2021-06-07 07:10:18 -07:00
total + = price . value * balance . value
self . total_balance = total
self . action_threshold_value = total * action_threshold
self . logger . info (
2021-09-07 10:40:34 -07:00
f " Wallet total balance of { total : ,.8f } gives action threshold: { self . action_threshold_value : ,.8f } " )
2021-06-07 07:10:18 -07:00
2021-11-08 03:39:09 -08:00
def allow ( self , token_value : InstrumentValue ) - > bool :
price = self . prices [ token_value . token . symbol ]
2021-06-07 07:10:18 -07:00
value = price . value * token_value . value
absolute_value = value . copy_abs ( )
result = absolute_value > self . action_threshold_value
self . logger . info (
2021-09-07 10:40:34 -07:00
f " Worth doing? { result } . { token_value . token . name } trade is worth: { absolute_value : ,.8f } , threshold is: { self . action_threshold_value : ,.8f } . " )
2021-06-07 07:10:18 -07:00
return result
# # 🥭 WalletBalancers
#
# We want two types of this class:
2021-09-07 10:40:34 -07:00
# * 'null' implementation that adheres to the interface but doesn't do anything, and
# * 'live' implementations that actually do the balancing.
2021-06-07 07:10:18 -07:00
#
# This allows us to have code that implements logic including wallet balancing, without
# having to worry about whether the user wants to re-balance or not - we can just plug
# in the 'null' variant and the logic all still works.
#
# To have this work we define an abstract base class `WalletBalancer` which defines the
# interface, then a `NullWalletBalancer` which adheres to this interface but doesn't
# perform any action, and finally the real `LiveWalletBalancer` which can perform the
# balancing action.
#
# # 🥭 WalletBalancer class
#
# This is the abstract class which defines the interface.
#
class WalletBalancer ( metaclass = abc . ABCMeta ) :
2021-11-09 05:23:36 -08:00
def __init__ ( self ) - > None :
2021-09-07 10:40:34 -07:00
self . logger : logging . Logger = logging . getLogger ( self . __class__ . __name__ )
2021-06-07 07:10:18 -07:00
@abc.abstractmethod
2021-11-09 05:23:36 -08:00
def balance ( self , context : Context , prices : typing . Sequence [ InstrumentValue ] ) - > None :
2021-06-07 07:10:18 -07:00
raise NotImplementedError ( " WalletBalancer.balance() is not implemented on the base type. " )
# # 🥭 NullWalletBalancer class
#
# This is the 'empty', 'no-op', 'dry run' wallet balancer which doesn't do anything but
# which can be plugged into algorithms that may want balancing logic.
#
class NullWalletBalancer ( WalletBalancer ) :
2021-11-09 05:23:36 -08:00
def __init__ ( self ) - > None :
2021-09-07 10:40:34 -07:00
super ( ) . __init__ ( )
2021-11-09 05:23:36 -08:00
def balance ( self , context : Context , prices : typing . Sequence [ InstrumentValue ] ) - > None :
2021-06-07 07:10:18 -07:00
pass
# # 🥭 LiveWalletBalancer class
#
# This is the high-level class that does much of the work.
#
class LiveWalletBalancer ( WalletBalancer ) :
2021-11-09 05:23:36 -08:00
def __init__ ( self , wallet : Wallet , quote_token : Token , trade_executor : TradeExecutor ,
targets : typing . Sequence [ TargetBalance ] , action_threshold : Decimal ) - > None :
2021-09-07 10:40:34 -07:00
super ( ) . __init__ ( )
2021-06-07 07:10:18 -07:00
self . wallet : Wallet = wallet
2021-09-07 10:40:34 -07:00
self . quote_token : Token = quote_token
2021-06-07 07:10:18 -07:00
self . trade_executor : TradeExecutor = trade_executor
2021-09-07 10:40:34 -07:00
self . targets : typing . Sequence [ TargetBalance ] = targets
2021-06-07 07:10:18 -07:00
self . action_threshold : Decimal = action_threshold
2021-11-09 05:23:36 -08:00
def balance ( self , context : Context , prices : typing . Sequence [ InstrumentValue ] ) - > None :
2021-06-07 07:10:18 -07:00
padding = " \n "
2021-11-09 05:23:36 -08:00
def balances_report ( balances : typing . Sequence [ InstrumentValue ] ) - > str :
2021-06-07 07:10:18 -07:00
return padding . join ( list ( [ f " { bal } " for bal in balances ] ) )
2021-09-07 10:40:34 -07:00
tokens : typing . List [ Token ] = [ ]
for target_balance in self . targets :
2021-11-08 03:39:09 -08:00
token = context . instrument_lookup . find_by_symbol ( target_balance . symbol )
2021-09-07 10:40:34 -07:00
if token is None :
raise Exception ( f " Could not find details of token { target_balance . symbol } . " )
2021-11-08 03:39:09 -08:00
tokens + = [ Token . ensure ( token ) ]
2021-09-07 10:40:34 -07:00
tokens + = [ self . quote_token ]
balances = self . _fetch_balances ( context , tokens )
2021-06-07 07:10:18 -07:00
total_value = Decimal ( 0 )
2021-09-07 10:40:34 -07:00
for bal in balances :
2021-11-08 03:39:09 -08:00
price = InstrumentValue . find_by_token ( prices , bal . token )
2021-06-07 07:10:18 -07:00
value = bal . value * price . value
total_value + = value
2021-09-07 10:40:34 -07:00
self . logger . info ( f " Starting balances: { padding } { balances_report ( balances ) } " )
2021-11-08 03:39:09 -08:00
total_token_value : InstrumentValue = InstrumentValue ( self . quote_token , total_value )
2021-09-07 10:40:34 -07:00
self . logger . info ( f " Total: { total_token_value } " )
2021-11-08 03:39:09 -08:00
resolved_targets : typing . List [ InstrumentValue ] = [ ]
2021-09-07 10:40:34 -07:00
for target in self . targets :
2021-11-08 03:39:09 -08:00
price = InstrumentValue . find_by_symbol ( prices , target . symbol )
2021-09-06 13:38:12 -07:00
resolved_targets + = [ target . resolve ( price . token , price . value , total_value ) ]
2021-06-07 07:10:18 -07:00
2021-09-07 10:40:34 -07:00
balance_changes = calculate_required_balance_changes ( balances , resolved_targets )
self . logger . info ( f " Desired balance changes: { padding } { balances_report ( balance_changes ) } " )
2021-06-07 07:10:18 -07:00
2021-09-07 10:40:34 -07:00
dont_bother = FilterSmallChanges ( self . action_threshold , balances , prices )
2021-06-07 07:10:18 -07:00
filtered_changes = list ( filter ( dont_bother . allow , balance_changes ) )
self . logger . info ( f " Filtered balance changes: { padding } { balances_report ( filtered_changes ) } " )
if len ( filtered_changes ) == 0 :
self . logger . info ( " No balance changes to make. " )
return
sorted_changes = sort_changes_for_trades ( filtered_changes )
self . _make_changes ( sorted_changes )
2021-09-07 10:40:34 -07:00
updated_balances = self . _fetch_balances ( context , tokens )
2021-06-07 07:10:18 -07:00
self . logger . info ( f " Finishing balances: { padding } { balances_report ( updated_balances ) } " )
2021-11-09 05:23:36 -08:00
def _make_changes ( self , balance_changes : typing . Sequence [ InstrumentValue ] ) - > None :
2021-09-07 10:40:34 -07:00
quote = self . quote_token . symbol
2021-06-07 07:10:18 -07:00
for change in balance_changes :
2021-09-07 10:40:34 -07:00
market_symbol = f " serum: { change . token . symbol } / { quote } "
2021-06-07 07:10:18 -07:00
if change . value < 0 :
2021-06-08 06:38:28 -07:00
self . trade_executor . sell ( market_symbol , change . value . copy_abs ( ) )
2021-06-07 07:10:18 -07:00
else :
2021-06-08 06:38:28 -07:00
self . trade_executor . buy ( market_symbol , change . value . copy_abs ( ) )
2021-06-07 07:10:18 -07:00
2021-11-08 03:39:09 -08:00
def _fetch_balances ( self , context : Context , tokens : typing . Sequence [ Token ] ) - > typing . Sequence [ InstrumentValue ] :
balances : typing . List [ InstrumentValue ] = [ ]
2021-09-07 10:40:34 -07:00
for token in tokens :
2021-11-08 03:39:09 -08:00
balance = InstrumentValue . fetch_total_value ( context , self . wallet . address , token )
2021-06-07 07:10:18 -07:00
balances + = [ balance ]
return balances
2021-09-07 10:40:34 -07:00
# # 🥭 LiveAccountBalancer class
#
# This is the high-level class that does much of the work.
#
class LiveAccountBalancer ( WalletBalancer ) :
2021-11-09 05:23:36 -08:00
def __init__ ( self , account : Account , group : Group , trade_executor : TradeExecutor ,
targets : typing . Sequence [ TargetBalance ] , action_threshold : Decimal ) - > None :
2021-09-07 10:40:34 -07:00
super ( ) . __init__ ( )
self . account : Account = account
2021-10-04 10:27:07 -07:00
self . group : Group = group
2021-09-07 10:40:34 -07:00
self . trade_executor : TradeExecutor = trade_executor
self . targets : typing . Sequence [ TargetBalance ] = targets
self . action_threshold : Decimal = action_threshold
2021-11-09 05:23:36 -08:00
def balance ( self , context : Context , prices : typing . Sequence [ InstrumentValue ] ) - > None :
2021-09-07 10:40:34 -07:00
padding = " \n "
2021-11-09 05:23:36 -08:00
def balances_report ( balances : typing . Sequence [ InstrumentValue ] ) - > str :
2021-09-07 10:40:34 -07:00
return padding . join ( list ( [ f " { bal } " for bal in balances ] ) )
2021-11-10 09:46:54 -08:00
balances = [ basket_token . net_value for basket_token in self . account . base_slots ]
2021-09-07 10:40:34 -07:00
total_value = Decimal ( 0 )
for bal in balances :
2021-11-08 03:39:09 -08:00
price = InstrumentValue . find_by_token ( prices , bal . token )
2021-09-07 10:40:34 -07:00
value = bal . value * price . value
total_value + = value
self . logger . info ( f " Starting balances: { padding } { balances_report ( balances ) } " )
2021-11-08 03:39:09 -08:00
quote_token : Token = self . account . shared_quote_token
total_token_value : InstrumentValue = InstrumentValue ( quote_token , total_value )
2021-09-07 10:40:34 -07:00
self . logger . info ( f " Total: { total_token_value } " )
2021-11-08 03:39:09 -08:00
resolved_targets : typing . List [ InstrumentValue ] = [ ]
2021-09-07 10:40:34 -07:00
for target in self . targets :
2021-11-08 03:39:09 -08:00
price = InstrumentValue . find_by_symbol ( prices , target . symbol )
2021-09-07 10:40:34 -07:00
resolved_targets + = [ target . resolve ( price . token , price . value , total_value ) ]
balance_changes = calculate_required_balance_changes ( balances , resolved_targets )
self . logger . info ( f " Desired balance changes: { padding } { balances_report ( balance_changes ) } " )
dont_bother = FilterSmallChanges ( self . action_threshold , balances , prices )
filtered_changes = list ( filter ( dont_bother . allow , balance_changes ) )
self . logger . info ( f " Worthwhile balance changes: { padding } { balances_report ( filtered_changes ) } " )
if len ( filtered_changes ) == 0 :
self . logger . info ( " No balance changes to make. " )
return
sorted_changes = sort_changes_for_trades ( filtered_changes )
self . _make_changes ( sorted_changes )
2021-10-04 10:27:07 -07:00
updated_account : Account = Account . load ( context , self . account . address , self . group )
2021-11-10 09:46:54 -08:00
updated_balances = [ basket_token . net_value for basket_token in updated_account . base_slots ]
2021-09-07 10:40:34 -07:00
self . logger . info ( f " Finishing balances: { padding } { balances_report ( updated_balances ) } " )
2021-11-09 05:23:36 -08:00
def _make_changes ( self , balance_changes : typing . Sequence [ InstrumentValue ] ) - > None :
2021-11-08 03:39:09 -08:00
quote = self . account . shared_quote_token . symbol
2021-09-07 10:40:34 -07:00
for change in balance_changes :
market_symbol = f " { change . token . symbol } / { quote } "
if change . value < 0 :
self . trade_executor . sell ( market_symbol , change . value . copy_abs ( ) )
else :
self . trade_executor . buy ( market_symbol , change . value . copy_abs ( ) )