sweep command now takes a target balance.
This commit is contained in:
parent
0297fda92c
commit
b4406a13d9
|
@ -20,7 +20,7 @@ parser = argparse.ArgumentParser(
|
|||
description="Balance the value of tokens in a Mango Markets group to specific values or percentages.")
|
||||
mango.ContextBuilder.add_command_line_parameters(parser)
|
||||
mango.Wallet.add_command_line_parameters(parser)
|
||||
parser.add_argument("--target", type=str, action="append", required=True,
|
||||
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')")
|
||||
parser.add_argument("--action-threshold", type=Decimal, default=Decimal("0.01"),
|
||||
help="fraction of total wallet value a trade must be above to be carried out")
|
||||
|
@ -45,8 +45,7 @@ try:
|
|||
group = mango.Group.load(context)
|
||||
tokens = [token_info.token for token_info in group.tokens if token_info is not None]
|
||||
|
||||
balance_parser = mango.TargetBalanceParser(tokens)
|
||||
targets = list(map(balance_parser.parse, args.target))
|
||||
targets = args.target
|
||||
logging.info(f"Targets: {targets}")
|
||||
|
||||
# TODO - fetch prices when available for V3.
|
||||
|
|
|
@ -26,7 +26,7 @@ parser.add_argument("--throttle-reload-to-seconds", type=Decimal, default=Decima
|
|||
help="minimum number of seconds between each full margin account reload loop (including time taken processing accounts)")
|
||||
parser.add_argument("--throttle-ripe-update-to-seconds", type=Decimal, default=Decimal(5),
|
||||
help="minimum number of seconds between each ripe update loop (including time taken processing accounts)")
|
||||
parser.add_argument("--target", type=str, action="append",
|
||||
parser.add_argument("--target", type=mango.parse_target_balance, action="append",
|
||||
help="token symbol plus target value or percentage, separated by a colon (e.g. 'ETH:2.5')")
|
||||
parser.add_argument("--action-threshold", type=Decimal, default=Decimal("0.01"),
|
||||
help="fraction of total wallet value a trade must be above to be carried out")
|
||||
|
@ -127,8 +127,7 @@ try:
|
|||
if args.dry_run or (args.target is None) or (len(args.target) == 0):
|
||||
wallet_balancer: mango.WalletBalancer = mango.NullWalletBalancer()
|
||||
else:
|
||||
balance_parser = mango.TargetBalanceParser(tokens)
|
||||
targets = list(map(balance_parser.parse, args.target))
|
||||
targets = args.target
|
||||
trade_executor = mango.ImmediateTradeExecutor(context, wallet, None, adjustment_factor)
|
||||
wallet_balancer = mango.LiveWalletBalancer(
|
||||
context, wallet, group, trade_executor, action_threshold, tokens, targets)
|
||||
|
|
59
bin/sweep
59
bin/sweep
|
@ -14,12 +14,22 @@ sys.path.insert(0, os.path.abspath(
|
|||
os.path.join(os.path.dirname(__file__), "..")))
|
||||
import mango # nopep8
|
||||
|
||||
|
||||
def resolve_quantity(token: mango.Token, current_quantity: Decimal, target_quantity: mango.TargetBalance, price: Decimal):
|
||||
current_value: Decimal = current_quantity * price
|
||||
resolved_value_to_keep: mango.TokenValue = target_quantity.resolve(token, price, current_value)
|
||||
return resolved_value_to_keep
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description="Sells all base tokens for quote on a market.")
|
||||
mango.ContextBuilder.add_command_line_parameters(parser)
|
||||
mango.Wallet.add_command_line_parameters(parser)
|
||||
parser.add_argument("--market", type=str, required=True, help="market symbol to sweep (e.g. ETH/USDC)")
|
||||
parser.add_argument("--target-quantity", type=mango.parse_target_balance, required=True,
|
||||
help="target quantity to maintain - token symbol plus target value or percentage, separated by a colon (e.g. 'ETH:2.5')")
|
||||
parser.add_argument("--max-slippage", type=Decimal, default=Decimal("0.05"),
|
||||
help="maximum slippage allowed for the IOC order price")
|
||||
parser.add_argument("--action-threshold", type=Decimal, default=Decimal("0.01"),
|
||||
help="fraction of total wallet value a trade must be above to be carried out")
|
||||
parser.add_argument("--account-address", type=PublicKey,
|
||||
help="address of the specific account to use, if more than one available")
|
||||
parser.add_argument("--dry-run", action="store_true", default=False,
|
||||
|
@ -34,7 +44,10 @@ wallet = mango.Wallet.from_command_line_parameters_or_raise(args)
|
|||
group = mango.Group.load(context, context.group_address)
|
||||
account = mango.Account.load_for_owner_by_address(context, wallet.address, group, args.account_address)
|
||||
|
||||
market_symbol = args.market.upper()
|
||||
target_quantity: mango.TargetBalance = args.target_quantity
|
||||
base = mango.TokenInfo.find_by_symbol(group.base_tokens, target_quantity.symbol)
|
||||
|
||||
market_symbol = f"{base.token.symbol}/{group.shared_quote_token.token.symbol}"
|
||||
market: typing.Optional[mango.Market] = context.market_lookup.find_by_symbol(market_symbol)
|
||||
if market is None:
|
||||
raise Exception(f"Could not find market {market_symbol}")
|
||||
|
@ -44,17 +57,39 @@ if not isinstance(loaded_market, mango.SpotMarket):
|
|||
raise Exception(f"Market {market_symbol} is not a spot market")
|
||||
|
||||
basket_token: mango.AccountBasketBaseToken = account.find_basket_token(market.base)
|
||||
quantity: Decimal = basket_token.net_value.value
|
||||
if quantity <= 0:
|
||||
print(f"No {basket_token.net_value.token.symbol} to sweep.")
|
||||
logging.info(f"Value in basket:{basket_token}")
|
||||
|
||||
market_operations: mango.MarketOperations = mango.create_market_operations(context, wallet, account, market, False)
|
||||
orders: typing.Sequence[mango.Order] = market_operations.load_orders()
|
||||
top_bid: Decimal = max([order.price for order in orders if order.side == mango.Side.BUY])
|
||||
top_ask: Decimal = min([order.price for order in orders if order.side == mango.Side.SELL])
|
||||
logging.info(f"Spread is {top_bid:,.8f} / {top_ask:,.8f}")
|
||||
|
||||
resolved_value_to_keep = resolve_quantity(base.token, basket_token.net_value.value, target_quantity, top_bid)
|
||||
quantity: Decimal = basket_token.net_value.value - resolved_value_to_keep.value
|
||||
if quantity < 0:
|
||||
quantity = 0 - quantity
|
||||
side: mango.Side = mango.Side.BUY
|
||||
slippage_factor: Decimal = Decimal(1) + args.max_slippage
|
||||
price: Decimal = top_ask * slippage_factor
|
||||
else:
|
||||
market_operations: mango.MarketOperations = mango.create_market_operations(context, wallet, account, market, False)
|
||||
orders: typing.Sequence[mango.Order] = market_operations.load_orders()
|
||||
top_bid: Decimal = max([order.price for order in orders if order.side == mango.Side.BUY])
|
||||
decrease_factor: Decimal = Decimal(1) - args.max_slippage
|
||||
price: Decimal = top_bid * decrease_factor
|
||||
order: mango.Order = mango.Order.from_basic_info(
|
||||
mango.Side.SELL, price, basket_token.net_value.value, mango.OrderType.IOC)
|
||||
side = mango.Side.SELL
|
||||
slippage_factor = Decimal(1) - args.max_slippage
|
||||
price = top_bid * slippage_factor
|
||||
|
||||
logging.info(f"Resolved value to order {quantity:,.8f}")
|
||||
logging.info(f"Worst acceptable price: {price:,.8f}")
|
||||
|
||||
token_price: mango.TokenValue = mango.TokenValue(basket_token.net_value.token, price)
|
||||
quantity_change: mango.TokenValue = mango.TokenValue(basket_token.net_value.token, quantity)
|
||||
|
||||
worthwhile_checker = mango.FilterSmallChanges(args.action_threshold, [basket_token.net_value], [token_price])
|
||||
worth_doing: bool = worthwhile_checker.allow(quantity_change)
|
||||
|
||||
if not worth_doing:
|
||||
print(f"Balance change {quantity_change} is below threshold for change: {args.action_threshold}")
|
||||
else:
|
||||
order: mango.Order = mango.Order.from_basic_info(side, price, quantity, mango.OrderType.IOC)
|
||||
if args.dry_run:
|
||||
print("Dry run: not completing order", order)
|
||||
else:
|
||||
|
|
|
@ -77,7 +77,7 @@ from .tradeexecutor import TradeExecutor, NullTradeExecutor, ImmediateTradeExecu
|
|||
from .transactionscout import TransactionScout, fetch_all_recent_transaction_signatures, mango_instruction_from_response
|
||||
from .version import Version
|
||||
from .wallet import Wallet
|
||||
from .walletbalancer import TargetBalance, FixedTargetBalance, PercentageTargetBalance, TargetBalanceParser, sort_changes_for_trades, calculate_required_balance_changes, FilterSmallChanges, WalletBalancer, NullWalletBalancer, LiveWalletBalancer
|
||||
from .walletbalancer import TargetBalance, FixedTargetBalance, PercentageTargetBalance, parse_target_balance, sort_changes_for_trades, calculate_required_balance_changes, FilterSmallChanges, WalletBalancer, NullWalletBalancer, LiveWalletBalancer
|
||||
from .watcher import Watcher, ManualUpdateWatcher, LamdaUpdateWatcher
|
||||
from .watchers import build_group_watcher, build_account_watcher, build_cache_watcher, build_spot_open_orders_watcher, build_serum_open_orders_watcher, build_perp_open_orders_watcher, build_price_watcher, build_serum_inventory_watcher, build_perp_orderbook_side_watcher, build_serum_orderbook_side_watcher
|
||||
from .websocketsubscription import WebSocketSubscription, WebSocketProgramSubscription, WebSocketAccountSubscription, WebSocketLogSubscription, WebSocketSubscriptionManager, IndividualWebSocketSubscriptionManager, SharedWebSocketSubscriptionManager
|
||||
|
|
|
@ -61,9 +61,8 @@ class SpotMarketOperations(MarketOperations):
|
|||
def place_order(self, order: Order) -> Order:
|
||||
client_id: int = self.context.random_client_id()
|
||||
signers: CombinableInstructions = CombinableInstructions.from_wallet(self.wallet)
|
||||
order_with_client_id: Order = Order(id=0, client_id=client_id, side=order.side, price=order.price,
|
||||
quantity=order.quantity, owner=self.open_orders_address or SYSTEM_PROGRAM_ADDRESS,
|
||||
order_type=order.order_type)
|
||||
order_with_client_id: Order = order.with_client_id(client_id).with_owner(
|
||||
self.open_orders_address or SYSTEM_PROGRAM_ADDRESS)
|
||||
self.logger.info(f"Placing {self.spot_market.symbol} order {order}.")
|
||||
place: CombinableInstructions = self.market_instruction_builder.build_place_order_instructions(
|
||||
order_with_client_id)
|
||||
|
|
|
@ -67,14 +67,12 @@ from .wallet import Wallet
|
|||
#
|
||||
# This is the abstract base class for our target balances, to allow them to be treated polymorphically.
|
||||
#
|
||||
|
||||
|
||||
class TargetBalance(metaclass=abc.ABCMeta):
|
||||
def __init__(self, token: Token):
|
||||
self.token = token
|
||||
def __init__(self, symbol: str):
|
||||
self.symbol = symbol.upper()
|
||||
|
||||
@abc.abstractmethod
|
||||
def resolve(self, current_price: Decimal, total_value: Decimal) -> TokenValue:
|
||||
def resolve(self, token: Token, current_price: Decimal, total_value: Decimal) -> TokenValue:
|
||||
raise NotImplementedError("TargetBalance.resolve() is not implemented on the base type.")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
@ -85,17 +83,16 @@ class TargetBalance(metaclass=abc.ABCMeta):
|
|||
#
|
||||
# This is the simple case, where the `FixedTargetBalance` object contains enough information on its own to build the resolved `TokenValue` object.
|
||||
#
|
||||
|
||||
class FixedTargetBalance(TargetBalance):
|
||||
def __init__(self, token: Token, value: Decimal):
|
||||
super().__init__(token)
|
||||
def __init__(self, symbol: str, value: Decimal):
|
||||
super().__init__(symbol)
|
||||
self.value = value
|
||||
|
||||
def resolve(self, current_price: Decimal, total_value: Decimal) -> TokenValue:
|
||||
return TokenValue(self.token, self.value)
|
||||
def resolve(self, token: Token, current_price: Decimal, total_value: Decimal) -> TokenValue:
|
||||
return TokenValue(token, self.value)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"""« FixedTargetBalance [{self.value} {self.token.name}] »"""
|
||||
return f"""« 𝙵𝚒𝚡𝚎𝚍𝚃𝚊𝚛𝚐𝚎𝚝𝙱𝚊𝚕𝚊𝚗𝚌𝚎 [{self.value} {self.symbol}] »"""
|
||||
|
||||
|
||||
# # 🥭 PercentageTargetBalance
|
||||
|
@ -110,56 +107,51 @@ class FixedTargetBalance(TargetBalance):
|
|||
# >
|
||||
# > _target balance_ is _wallet fraction_ divided by _token price_
|
||||
#
|
||||
|
||||
class PercentageTargetBalance(TargetBalance):
|
||||
def __init__(self, token: Token, target_percentage: Decimal):
|
||||
super().__init__(token)
|
||||
def __init__(self, symbol: str, target_percentage: Decimal):
|
||||
super().__init__(symbol)
|
||||
self.target_fraction = target_percentage / 100
|
||||
|
||||
def resolve(self, current_price: Decimal, total_value: Decimal) -> TokenValue:
|
||||
def resolve(self, token: Token, current_price: Decimal, total_value: Decimal) -> TokenValue:
|
||||
target_value = total_value * self.target_fraction
|
||||
target_size = target_value / current_price
|
||||
return TokenValue(self.token, target_size)
|
||||
return TokenValue(token, target_size)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"""« PercentageTargetBalance [{self.target_fraction * 100}% {self.token.name}] »"""
|
||||
return f"""« 𝙿𝚎𝚛𝚌𝚎𝚗𝚝𝚊𝚐𝚎𝚃𝚊𝚛𝚐𝚎𝚝𝙱𝚊𝚕𝚊𝚗𝚌𝚎 [{self.target_fraction * 100}% {self.symbol}] »"""
|
||||
|
||||
|
||||
# # 🥭 TargetBalanceParser class
|
||||
# # 🥭 parse_target_balance function
|
||||
#
|
||||
# The `TargetBalanceParser` takes a string like "BTC:0.2" or "ETH:20%" and returns the appropriate TargetBalance object.
|
||||
#
|
||||
# This has a lot of manual error handling because it's likely the error messages will be seen by people and so we want to be as clear as we can what specifically is wrong.
|
||||
# `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')")
|
||||
#
|
||||
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
|
||||
|
||||
class TargetBalanceParser:
|
||||
def __init__(self, tokens: typing.Sequence[Token]):
|
||||
self.tokens = tokens
|
||||
# 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
|
||||
|
||||
def parse(self, to_parse: str) -> TargetBalance:
|
||||
try:
|
||||
token_name, value = to_parse.split(":")
|
||||
except Exception as exception:
|
||||
raise Exception(f"Could not parse target balance '{to_parse}'") from exception
|
||||
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.")
|
||||
|
||||
token = Token.find_by_symbol(self.tokens, token_name)
|
||||
|
||||
# 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
|
||||
|
||||
if len(values) > 1:
|
||||
raise ValueError(
|
||||
f"Error parsing '{value}'. Percentage targets could lead to over-rebalancing (due to token value changes rather than liquidations) and so are no longer supported.")
|
||||
|
||||
return FixedTargetBalance(token, numeric_value)
|
||||
if len(values) == 1:
|
||||
return FixedTargetBalance(symbol, numeric_value)
|
||||
else:
|
||||
return PercentageTargetBalance(symbol, numeric_value)
|
||||
|
||||
|
||||
# # 🥭 sort_changes_for_trades function
|
||||
|
@ -172,7 +164,6 @@ class TargetBalanceParser:
|
|||
# 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.)
|
||||
#
|
||||
|
||||
def sort_changes_for_trades(changes: typing.Sequence[TokenValue]) -> typing.Sequence[TokenValue]:
|
||||
return sorted(changes, key=lambda change: change.value)
|
||||
|
||||
|
@ -181,8 +172,6 @@ def sort_changes_for_trades(changes: typing.Sequence[TokenValue]) -> typing.Sequ
|
|||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
def calculate_required_balance_changes(current_balances: typing.Sequence[TokenValue], desired_balances: typing.Sequence[TokenValue]) -> typing.Sequence[TokenValue]:
|
||||
changes: typing.List[TokenValue] = []
|
||||
for desired in desired_balances:
|
||||
|
@ -205,8 +194,6 @@ def calculate_required_balance_changes(current_balances: typing.Sequence[TokenVa
|
|||
# of 10 in another token. Normalising values to our wallet balance makes these changes
|
||||
# easier to reason about.
|
||||
#
|
||||
|
||||
|
||||
class FilterSmallChanges:
|
||||
def __init__(self, action_threshold: Decimal, balances: typing.Sequence[TokenValue], prices: typing.Sequence[TokenValue]):
|
||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
||||
|
@ -252,7 +239,6 @@ class FilterSmallChanges:
|
|||
#
|
||||
# This is the abstract class which defines the interface.
|
||||
#
|
||||
|
||||
class WalletBalancer(metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
def balance(self, prices: typing.Sequence[TokenValue]):
|
||||
|
@ -264,8 +250,6 @@ class WalletBalancer(metaclass=abc.ABCMeta):
|
|||
# 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):
|
||||
def balance(self, prices: typing.Sequence[TokenValue]):
|
||||
pass
|
||||
|
@ -275,7 +259,6 @@ class NullWalletBalancer(WalletBalancer):
|
|||
#
|
||||
# This is the high-level class that does much of the work.
|
||||
#
|
||||
|
||||
class LiveWalletBalancer(WalletBalancer):
|
||||
def __init__(self, context: Context, wallet: Wallet, group: Group, trade_executor: TradeExecutor, action_threshold: Decimal, tokens: typing.Sequence[Token], target_balances: typing.Sequence[TargetBalance]):
|
||||
self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)
|
||||
|
@ -302,8 +285,8 @@ class LiveWalletBalancer(WalletBalancer):
|
|||
self.logger.info(f"Starting balances: {padding}{balances_report(current_balances)} - total: {total_value}")
|
||||
resolved_targets: typing.List[TokenValue] = []
|
||||
for target in self.target_balances:
|
||||
price = TokenValue.find_by_token(prices, target.token)
|
||||
resolved_targets += [target.resolve(price.value, total_value)]
|
||||
price = TokenValue.find_by_symbol(prices, target.symbol)
|
||||
resolved_targets += [target.resolve(price.token, price.value, total_value)]
|
||||
|
||||
balance_changes = calculate_required_balance_changes(current_balances, resolved_targets)
|
||||
self.logger.info(f"Full balance changes: {padding}{balances_report(balance_changes)}")
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import pytest
|
||||
|
||||
from .context import mango
|
||||
from .fakes import fake_token
|
||||
|
||||
|
@ -27,30 +25,21 @@ def test_target_balance_constructor():
|
|||
def test_fixed_target_balance_constructor():
|
||||
token = fake_token()
|
||||
value = Decimal(23)
|
||||
actual = mango.FixedTargetBalance(token, value)
|
||||
actual = mango.FixedTargetBalance(token.symbol, value)
|
||||
assert actual is not None
|
||||
assert actual.token == token
|
||||
assert actual.symbol == token.symbol
|
||||
assert actual.value == value
|
||||
|
||||
|
||||
def test_percentage_target_balance_constructor():
|
||||
token = fake_token()
|
||||
value = Decimal(5)
|
||||
actual = mango.PercentageTargetBalance(token, value)
|
||||
actual = mango.PercentageTargetBalance(token.symbol, value)
|
||||
assert actual is not None
|
||||
assert actual.token == token
|
||||
assert actual.symbol == token.symbol
|
||||
assert actual.target_fraction == Decimal("0.05") # Calculated as a fraction instead of a percentage.
|
||||
|
||||
|
||||
def test_target_balance_parser_constructor():
|
||||
token1 = fake_token()
|
||||
token2 = fake_token()
|
||||
tokens = [token1, token2]
|
||||
actual = mango.TargetBalanceParser(tokens)
|
||||
assert actual is not None
|
||||
assert actual.tokens == tokens
|
||||
|
||||
|
||||
def test_calculate_required_balance_changes():
|
||||
current_balances = [
|
||||
mango.TokenValue(ETH_TOKEN, Decimal("0.5")),
|
||||
|
@ -72,27 +61,32 @@ def test_calculate_required_balance_changes():
|
|||
|
||||
def test_percentage_target_balance():
|
||||
token = fake_token()
|
||||
percentage_parsed_balance_change = mango.PercentageTargetBalance(token, Decimal(33))
|
||||
assert(percentage_parsed_balance_change.token == token)
|
||||
percentage_parsed_balance_change = mango.PercentageTargetBalance(token.symbol, Decimal(33))
|
||||
assert(percentage_parsed_balance_change.symbol == token.symbol)
|
||||
|
||||
current_token_price = Decimal(2000) # It's $2,000 per TOKEN
|
||||
current_account_value = Decimal(10000) # We hold $10,000 in total across all assets in our account.
|
||||
resolved_parsed_balance_change = percentage_parsed_balance_change.resolve(
|
||||
current_token_price, current_account_value)
|
||||
resolved_parsed_balance_change = percentage_parsed_balance_change.resolve(token,
|
||||
current_token_price,
|
||||
current_account_value)
|
||||
assert(resolved_parsed_balance_change.token == token)
|
||||
# 33% of $10,000 is $3,300
|
||||
# $3,300 spent on TOKEN gives us 1.65 TOKEN
|
||||
assert(resolved_parsed_balance_change.value == Decimal("1.65"))
|
||||
|
||||
|
||||
def test_target_balance_parser():
|
||||
parser = mango.TargetBalanceParser([ETH_TOKEN, BTC_TOKEN, USDT_TOKEN])
|
||||
with pytest.raises(ValueError):
|
||||
parser.parse("eth:10%")
|
||||
def test_target_balance_parser_fixedvalue():
|
||||
parsed = mango.parse_target_balance("eth:70")
|
||||
assert isinstance(parsed, mango.FixedTargetBalance)
|
||||
assert parsed.symbol == "ETH"
|
||||
assert parsed.value == Decimal(70)
|
||||
|
||||
parsed_fixed = parser.parse("eth:70")
|
||||
assert(parsed_fixed.token == ETH_TOKEN)
|
||||
assert(parsed_fixed.value == Decimal(70))
|
||||
|
||||
def test_target_balance_parser_percentagevalue():
|
||||
parsed = mango.parse_target_balance("btc:10%")
|
||||
assert isinstance(parsed, mango.PercentageTargetBalance)
|
||||
assert parsed.symbol == "BTC"
|
||||
assert parsed.target_fraction == Decimal("0.1")
|
||||
|
||||
|
||||
def test_filter_small_changes_constructor():
|
||||
|
|
Loading…
Reference in New Issue