mango-explorer/WalletBalancer.ipynb

678 lines
27 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"cells": [
{
"cell_type": "markdown",
"id": "important-firmware",
"metadata": {},
"source": [
"# ⚠ Warning\n",
"\n",
"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.\n",
"\n",
"[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gl/OpinionatedGeek%2Fmango-explorer/HEAD?filepath=Trade.ipynb) _🏃 To run this notebook press the ⏩ icon in the toolbar above._\n",
"\n",
"[🥭 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)"
]
},
{
"cell_type": "markdown",
"id": "limited-ordering",
"metadata": {},
"source": [
"# 🥭 WalletBalancer\n",
"\n",
"This notebook deals with balancing a wallet after processing liquidations, so that it has appropriate funds for the next liquidation.\n",
"\n",
"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?\n",
"\n",
"We 'balance' our wallet tokens, buying or selling or swapping them as required."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "immediate-berlin",
"metadata": {
"jupyter": {
"source_hidden": true
}
},
"outputs": [],
"source": [
"import abc\n",
"import logging\n",
"import typing\n",
"\n",
"from decimal import Decimal\n",
"from solana.publickey import PublicKey\n",
"\n",
"from BaseModel import BasketToken, Group, Token, TokenValue\n",
"from Context import Context\n",
"from TradeExecutor import TradeExecutor\n",
"from Wallet import Wallet\n"
]
},
{
"cell_type": "markdown",
"id": "atomic-studio",
"metadata": {},
"source": [
"# Target Balances\n",
"\n",
"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.\n",
"\n",
"There are two possible approaches to specifying the target value:\n",
"* A 'fixed' value, like 10 ETH\n",
"* A 'percentage' value, like 20% ETH\n",
"\n",
"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.\n",
"\n",
"The way this all hangs together is:\n",
"* A parser parses string values (probably from a command-line) into `TargetBalance` objects.\n",
"* There are two types of `TargetBalance` objects - `FixedTargetBalance` and `PercentageTargetBalance`.\n",
"* To get the actual `TokenValue` for balancing, the `TargetBalance` must be 'resolved' by calling `resolve()` with the appropriate token price and wallet value."
]
},
{
"cell_type": "markdown",
"id": "muslim-whale",
"metadata": {},
"source": [
"## TargetBalance class\n",
"\n",
"This is the abstract base class for our target balances, to allow them to be treated polymorphically."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fallen-plumbing",
"metadata": {},
"outputs": [],
"source": [
"class TargetBalance(metaclass=abc.ABCMeta):\n",
" def __init__(self, token: Token):\n",
" self.token = token\n",
"\n",
" @abc.abstractmethod\n",
" def resolve(self, current_price: Decimal, total_value: Decimal) -> TokenValue:\n",
" raise NotImplementedError(\"TargetBalance.resolve() is not implemented on the base type.\")\n",
"\n",
" def __repr__(self) -> str:\n",
" return f\"{self}\"\n",
"\n"
]
},
{
"cell_type": "markdown",
"id": "acting-december",
"metadata": {},
"source": [
"## FixedTargetBalance class\n",
"\n",
"This is the simple case, where the `FixedTargetBalance` object contains enough information on its own to build the resolved `TokenValue` object."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fuzzy-laptop",
"metadata": {},
"outputs": [],
"source": [
"class FixedTargetBalance(TargetBalance):\n",
" def __init__(self, token: Token, value: Decimal):\n",
" super().__init__(token)\n",
" self.value = value\n",
"\n",
" def resolve(self, current_price: Decimal, total_value: Decimal) -> TokenValue:\n",
" return TokenValue(self.token, self.value)\n",
"\n",
" def __str__(self) -> str:\n",
" return f\"\"\"« FixedTargetBalance [{self.value} {self.token.name}] »\"\"\"\n"
]
},
{
"cell_type": "markdown",
"id": "ready-costume",
"metadata": {},
"source": [
"## PercentageTargetBalance\n",
"\n",
"This is the more complex case, where the target is a percentage of the total wallet balance.\n",
"\n",
"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:\n",
"> _wallet fraction_ is _percentage_ of _wallet value_\n",
"\n",
"> _target balance_ is _wallet fraction_ divided by _token price_"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "premium-basin",
"metadata": {},
"outputs": [],
"source": [
"class PercentageTargetBalance(TargetBalance):\n",
" def __init__(self, token: Token, target_percentage: Decimal):\n",
" super().__init__(token)\n",
" self.target_fraction = target_percentage / 100\n",
"\n",
" def resolve(self, current_price: Decimal, total_value: Decimal) -> TokenValue:\n",
" target_value = total_value * self.target_fraction\n",
" target_size = target_value / current_price\n",
" return TokenValue(self.token, target_size)\n",
"\n",
" def __str__(self) -> str:\n",
" return f\"\"\"« PercentageTargetBalance [{self.target_fraction * 100}% {self.token.name}] »\"\"\"\n"
]
},
{
"cell_type": "markdown",
"id": "behavioral-convertible",
"metadata": {},
"source": [
"## TargetBalanceParser class\n",
"\n",
"The `TargetBalanceParser` takes a string like \"BTC:0.2\" or \"ETH:20%\" and returns the appropriate TargetBalance object.\n",
"\n",
"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."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "democratic-crowd",
"metadata": {},
"outputs": [],
"source": [
"class TargetBalanceParser:\n",
" def __init__(self, tokens: typing.List[Token]):\n",
" self.tokens = tokens\n",
"\n",
" def parse(self, to_parse: str) -> TargetBalance:\n",
" try:\n",
" token_name, value = to_parse.split(\":\")\n",
" except Exception as exception:\n",
" raise Exception(f\"Could not parse target balance '{to_parse}'\") from exception\n",
"\n",
" token = Token.find_by_name(self.tokens, token_name)\n",
"\n",
" # The value we have may be an int (like 27), a fraction (like 0.1) or a percentage\n",
" # (like 25%). In all cases we want the number as a number, but we also want to know if\n",
" # we have a percent or not\n",
" values = value.split(\"%\")\n",
" numeric_value_string = values[0]\n",
" try:\n",
" numeric_value = Decimal(numeric_value_string)\n",
" except Exception as exception:\n",
" 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\n",
"\n",
" if len(values) > 2:\n",
" 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.\")\n",
"\n",
" if len(values) == 1:\n",
" return FixedTargetBalance(token, numeric_value)\n",
" else:\n",
" return PercentageTargetBalance(token, numeric_value)\n",
"\n"
]
},
{
"cell_type": "markdown",
"id": "supreme-kentucky",
"metadata": {},
"source": [
"# sort_changes_for_trades function\n",
"\n",
"It's important to process SELLs first, so we have enough funds in the quote balance for the BUYs.\n",
"\n",
"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.)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "small-period",
"metadata": {},
"outputs": [],
"source": [
"def sort_changes_for_trades(changes: typing.List[TokenValue]) -> typing.List[TokenValue]:\n",
" return sorted(changes, key=lambda change: change.value)\n"
]
},
{
"cell_type": "markdown",
"id": "significant-bearing",
"metadata": {},
"source": [
"# calculate_required_balance_changes function\n",
"\n",
"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."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "regulation-essay",
"metadata": {},
"outputs": [],
"source": [
"def calculate_required_balance_changes(current_balances: typing.List[TokenValue], desired_balances: typing.List[TokenValue]) -> typing.List[TokenValue]:\n",
" changes: typing.List[TokenValue] = []\n",
" for desired in desired_balances:\n",
" current = TokenValue.find_by_token(current_balances, desired.token)\n",
" change = TokenValue(desired.token, desired.value - current.value)\n",
" changes += [change]\n",
"\n",
" return changes\n"
]
},
{
"cell_type": "markdown",
"id": "taken-salvation",
"metadata": {},
"source": [
"# FilterSmallChanges class\n",
"\n",
"Allows us to filter out changes that aren't worth the effort.\n",
"\n",
"For instance, if our desired balance requires changing less than 1% of our total balance, it may not be worth bothering with right not.\n",
"\n",
"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."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "clean-horizon",
"metadata": {},
"outputs": [],
"source": [
"class FilterSmallChanges:\n",
" def __init__(self, action_threshold: Decimal, balances: typing.List[TokenValue], prices: typing.List[TokenValue]):\n",
" self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n",
" self.prices: typing.Dict[str, TokenValue] = {}\n",
" total = Decimal(0)\n",
" for balance in balances:\n",
" price = TokenValue.find_by_token(prices, balance.token)\n",
" self.prices[f\"{price.token.mint}\"] = price\n",
" total += price.value * balance.value\n",
" self.total_balance = total\n",
" self.action_threshold_value = total * action_threshold\n",
" self.logger.info(f\"Wallet total balance of {total} gives action threshold value of {self.action_threshold_value}\")\n",
"\n",
" def allow(self, token_value: TokenValue) -> bool:\n",
" price = self.prices[f\"{token_value.token.mint}\"]\n",
" value = price.value * token_value.value\n",
" absolute_value = value.copy_abs()\n",
" result = absolute_value > self.action_threshold_value\n",
"\n",
" self.logger.info(f\"Value of {token_value.token.name} trade is {absolute_value}, threshold value is {self.action_threshold_value}. Is this worth doing? {result}.\")\n",
" return result\n"
]
},
{
"cell_type": "markdown",
"id": "animated-switzerland",
"metadata": {},
"source": [
"# WalletBalancers\n",
"\n",
"We want two types of this class:\n",
"* A 'null' implementation that adheres to the interface but doesn't do anything, and\n",
"* A 'live' implementation that actually does the balancing.\n",
"\n",
"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.\n",
"\n",
"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."
]
},
{
"cell_type": "markdown",
"id": "essential-heating",
"metadata": {},
"source": [
"## WalletBalancer class\n",
"\n",
"This is the abstract class which defines the interface."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "behind-product",
"metadata": {},
"outputs": [],
"source": [
"class WalletBalancer(metaclass=abc.ABCMeta):\n",
" @abc.abstractmethod\n",
" def balance(self, prices: typing.List[TokenValue]):\n",
" raise NotImplementedError(\"WalletBalancer.balance() is not implemented on the base type.\")\n"
]
},
{
"cell_type": "markdown",
"id": "mighty-minimum",
"metadata": {},
"source": [
"## NullWalletBalancer class\n",
"\n",
"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."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "serious-ghost",
"metadata": {},
"outputs": [],
"source": [
"class NullWalletBalancer(WalletBalancer):\n",
" def balance(self, prices: typing.List[TokenValue]):\n",
" pass\n"
]
},
{
"cell_type": "markdown",
"id": "metric-veteran",
"metadata": {},
"source": [
"## LiveWalletBalancer class\n",
"\n",
"This is the high-level class that does much of the work."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "shared-craft",
"metadata": {},
"outputs": [],
"source": [
"class LiveWalletBalancer(WalletBalancer):\n",
" def __init__(self, context: Context, wallet: Wallet, trade_executor: TradeExecutor, action_threshold: Decimal, tokens: typing.List[Token], target_balances: typing.List[TargetBalance]):\n",
" self.logger: logging.Logger = logging.getLogger(self.__class__.__name__)\n",
" self.context: Context = context\n",
" self.wallet: Wallet = wallet\n",
" self.trade_executor: TradeExecutor = trade_executor\n",
" self.action_threshold: Decimal = action_threshold\n",
" self.tokens: typing.List[Token] = tokens\n",
" self.target_balances: typing.List[TargetBalance] = target_balances\n",
"\n",
" def balance(self, prices: typing.List[TokenValue]):\n",
" padding = \"\\n \"\n",
"\n",
" def balances_report(balances) -> str:\n",
" return padding.join(list([f\"{bal}\" for bal in balances]))\n",
"\n",
" current_balances = self._fetch_balances()\n",
" total_value = Decimal(0)\n",
" for bal in current_balances:\n",
" price = TokenValue.find_by_token(prices, bal.token)\n",
" value = bal.value * price.value\n",
" total_value += value\n",
" self.logger.info(f\"Starting balances: {padding}{balances_report(current_balances)} - total: {total_value}\")\n",
" resolved_targets: typing.List[TokenValue] = []\n",
" for target in self.target_balances:\n",
" price = TokenValue.find_by_token(prices, target.token)\n",
" resolved_targets += [target.resolve(price.value, total_value)]\n",
"\n",
" balance_changes = calculate_required_balance_changes(current_balances, resolved_targets)\n",
" self.logger.info(f\"Full balance changes: {padding}{balances_report(balance_changes)}\")\n",
"\n",
" dont_bother = FilterSmallChanges(self.action_threshold, current_balances, prices)\n",
" filtered_changes = list(filter(dont_bother.allow, balance_changes))\n",
" self.logger.info(f\"Filtered balance changes: {padding}{balances_report(filtered_changes)}\")\n",
" if len(filtered_changes) == 0:\n",
" self.logger.info(\"No balance changes to make.\")\n",
" return\n",
"\n",
" sorted_changes = sort_changes_for_trades(filtered_changes)\n",
" self._make_changes(sorted_changes)\n",
" updated_balances = self._fetch_balances()\n",
" self.logger.info(f\"Finishing balances: {padding}{balances_report(updated_balances)}\")\n",
"\n",
" def _make_changes(self, balance_changes: typing.List[TokenValue]):\n",
" self.logger.info(f\"Balance changes to make: {balance_changes}\")\n",
" for change in balance_changes:\n",
" if change.value < 0:\n",
" self.trade_executor.sell(change.token.name, change.value.copy_abs())\n",
" else:\n",
" self.trade_executor.buy(change.token.name, change.value.copy_abs())\n",
"\n",
" def _fetch_balances(self) -> typing.List[TokenValue]:\n",
" balances: typing.List[TokenValue] = []\n",
" for token in self.tokens:\n",
" balance = TokenValue.fetch_total_value(self.context, self.wallet.address, token)\n",
" balances += [balance]\n",
"\n",
" return balances\n",
"\n"
]
},
{
"cell_type": "markdown",
"id": "living-fitness",
"metadata": {},
"source": [
"# ✅ Testing"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "superb-proportion",
"metadata": {},
"outputs": [],
"source": [
"def _notebook_tests():\n",
" log_level = logging.getLogger().level\n",
" try:\n",
" logging.getLogger().setLevel(logging.CRITICAL)\n",
" eth = Token(\"ETH\", PublicKey(\"2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk\"), Decimal(6))\n",
" btc = Token(\"BTC\", PublicKey(\"9n4nbM75f5Ui33ZbPYXn59EwSgE8CGsHtAeTH5YFeJ9E\"), Decimal(6))\n",
" usdt = Token(\"USDT\", PublicKey(\"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB\"), Decimal(6))\n",
" current_prices = [\n",
" TokenValue(eth, Decimal(\"4000\")),\n",
" TokenValue(btc, Decimal(\"60000\")),\n",
" TokenValue(usdt, Decimal(\"1\")),\n",
" ]\n",
" current_balances = [\n",
" TokenValue(eth, Decimal(\"0.5\")),\n",
" TokenValue(btc, Decimal(\"0.2\")),\n",
" TokenValue(usdt, Decimal(\"10000\")),\n",
" ]\n",
" desired_balances = [\n",
" TokenValue(eth, Decimal(\"1\")),\n",
" TokenValue(btc, Decimal(\"0.1\"))\n",
" ]\n",
"\n",
" changes = calculate_required_balance_changes(current_balances, desired_balances)\n",
"\n",
" assert(changes[0].token.name == \"ETH\")\n",
" assert(changes[0].value == Decimal(\"0.5\"))\n",
" assert(changes[1].token.name == \"BTC\")\n",
" assert(changes[1].value == Decimal(\"-0.1\"))\n",
"\n",
" parsed_balance_change = FixedTargetBalance(eth, Decimal(\"0.1\"))\n",
" assert(parsed_balance_change.token == eth)\n",
" assert(parsed_balance_change.value == Decimal(\"0.1\"))\n",
"\n",
" percentage_parsed_balance_change = PercentageTargetBalance(eth, Decimal(33))\n",
" assert(percentage_parsed_balance_change.token == eth)\n",
"\n",
" current_eth_price = Decimal(2000) # It's $2,000 per ETH\n",
" current_account_value = Decimal(10000) # We hold $10,000 in total across all assets in our account.\n",
" resolved_parsed_balance_change = percentage_parsed_balance_change.resolve(current_eth_price, current_account_value)\n",
" assert(resolved_parsed_balance_change.token == eth)\n",
" # 33% of $10,000 is $3,300\n",
" # $3,300 spent on ETH gives us 1.65 ETH\n",
" assert(resolved_parsed_balance_change.value == Decimal(\"1.65\"))\n",
"\n",
" parser = TargetBalanceParser([eth, btc, usdt])\n",
" parsed_percent = parser.parse(\"eth:10%\")\n",
" assert(parsed_percent.token == eth)\n",
" assert(parsed_percent.target_fraction == Decimal(\"0.1\"))\n",
"\n",
" parsed_fixed = parser.parse(\"eth:70\")\n",
" assert(parsed_fixed.token == eth)\n",
" assert(parsed_fixed.value == Decimal(70))\n",
"\n",
" action_threshold = Decimal(\"0.01\") # Don't bother if it's less than 1% of the total value (24,000)\n",
" dont_bother = FilterSmallChanges(action_threshold, current_balances, current_prices)\n",
"\n",
" # 0.05 ETH is worth $200 at our test prices, which is less than our $240 threshold\n",
" assert(not dont_bother.allow(TokenValue(eth, Decimal(\"0.05\"))))\n",
"\n",
" # 0.05 BTC is worth $3,000 at our test prices, which is much more than our $240 threshold\n",
" assert(dont_bother.allow(TokenValue(btc, Decimal(\"0.05\"))))\n",
"\n",
" eth_buy = TokenValue(eth, Decimal(\"5\"))\n",
" btc_sell = TokenValue(btc, Decimal(\"-1\"))\n",
" sorted_changes = sort_changes_for_trades([\n",
" eth_buy,\n",
" btc_sell\n",
" ])\n",
"\n",
" assert(sorted_changes[0] == btc_sell)\n",
" assert(sorted_changes[1] == eth_buy)\n",
" finally:\n",
" logging.getLogger().setLevel(log_level)\n",
"\n",
"\n",
"_notebook_tests()\n",
"del _notebook_tests"
]
},
{
"cell_type": "markdown",
"id": "built-evidence",
"metadata": {},
"source": [
"# 🏃 Running\n",
"\n",
"If running interactively, try to buy then sell 0.1 ETH.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "welcome-secretariat",
"metadata": {},
"outputs": [],
"source": [
"if __name__ == \"__main__\":\n",
" logging.getLogger().setLevel(logging.INFO)\n",
"\n",
" from Context import default_context\n",
"\n",
" group = Group.load(default_context)\n",
" eth = BasketToken.find_by_name(group.basket_tokens, \"eth\").token\n",
" btc = BasketToken.find_by_name(group.basket_tokens, \"btc\").token\n",
" usdt = BasketToken.find_by_name(group.basket_tokens, \"usdt\").token\n",
"\n",
" parser = TargetBalanceParser([eth, btc])\n",
" eth_target = parser.parse(\"ETH:20%\")\n",
" btc_target = parser.parse(\"btc:0.05\")\n",
" prices = [Decimal(\"60000\"), Decimal(\"4000\"), Decimal(\"1\")] # Ordered as per Group index ordering\n",
" desired_balances = []\n",
" for target in [eth_target, btc_target]:\n",
" token_index = group.price_index_of_token(target.token)\n",
" price = prices[token_index]\n",
" resolved = target.resolve(price, Decimal(10000))\n",
" desired_balances += [resolved]\n",
"\n",
" assert(desired_balances[0].token.name == \"ETH\")\n",
" assert(desired_balances[0].value == Decimal(\"0.5\"))\n",
" assert(desired_balances[1].token.name == \"BTC\")\n",
" assert(desired_balances[1].value == Decimal(\"0.05\"))\n",
"\n",
" current_balances = [\n",
" TokenValue(eth, Decimal(\"0.6\")), # Worth $2,400 at the test prices\n",
" TokenValue(btc, Decimal(\"0.01\")), # Worth $6,00 at the test prices\n",
" TokenValue(usdt, Decimal(\"7000\")), # Remainder of $10,000 minus above token values\n",
" ]\n",
"\n",
" changes = calculate_required_balance_changes(current_balances, desired_balances)\n",
" for change in changes:\n",
" order_type = \"BUY\" if change.value > 0 else \"SELL\"\n",
" print(f\"{change.token.name} {order_type} {change.value}\")\n",
"\n",
" # To get from our current balances of 0.6 ETH and 0.01 BTC to our desired balances of\n",
" # 0.5 ETH and 0.05 BTC, we need to sell 0.1 ETH and buy 0.04 BTC. But we want to do the sell\n",
" # first, to make sure we have the proper liquidity when it comes to buying.\n",
" sorted_changes = sort_changes_for_trades(changes)\n",
" assert(sorted_changes[0].token.name == \"ETH\")\n",
" assert(sorted_changes[0].value == Decimal(\"-0.1\"))\n",
" assert(sorted_changes[1].token.name == \"BTC\")\n",
" assert(sorted_changes[1].value == Decimal(\"0.04\"))\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.6"
},
"toc": {
"base_numbering": 1,
"nav_menu": {},
"number_sections": true,
"sideBar": true,
"skip_h1_title": false,
"title_cell": "Table of Contents",
"title_sidebar": "Contents",
"toc_cell": false,
"toc_position": {},
"toc_section_display": true,
"toc_window_display": true
},
"varInspector": {
"cols": {
"lenName": 16,
"lenType": 16,
"lenVar": 40
},
"kernels_config": {
"python": {
"delete_cmd_postfix": "",
"delete_cmd_prefix": "del ",
"library": "var_list.py",
"varRefreshCmd": "print(var_dic_list())"
},
"r": {
"delete_cmd_postfix": ") ",
"delete_cmd_prefix": "rm(",
"library": "var_list.r",
"varRefreshCmd": "cat(var_dic_list()) "
}
},
"types_to_exclude": [
"module",
"function",
"builtin_function_or_method",
"instance",
"_Feature"
],
"window_display": false
}
},
"nbformat": 4,
"nbformat_minor": 5
}