From 80664b291d9f11e9f902495269756534b84fc8f2 Mon Sep 17 00:00:00 2001 From: Platon Floria Date: Fri, 17 May 2024 19:19:38 +0800 Subject: [PATCH] Refactor triangle-mode Minor Change `assert_supported` exception type from `ValueError` to `AssertionError` Remove unused return-value Remove unused code Refactor single-trianglre mode as well Remove unneeded assertion Cosmetic Fix single-triangle mode Fix single-triangle mode Refactor the b3-two-hop-triangle mode as well Remove the importing of `T` Remove unused code Remove single-triangle mode, and fix a bunch of problems Cleanup `calculate_profit` related code Fix test 042 Fix test 042 Remove unused code Some more general cleanup Cleanup Remove unused code Fix test 063 Cleanup Cleanup Fix tests Cleanup Cleanup Fix test 042 Remove single_paiwise and multi_paiwise arb modes (major change) Fix test 045 Fix test 045 Refactor the pairwise arb-mode classes Ongoing refactor Refactor triangle arb-mode classes Cleanup Remove file `scan_log_errors.py` from the repository Cleanup Remove unused code Code reuse Fix functions `create_sort_order` and `sort_key` --- README.md | 11 +- fastlane_bot/bot.py | 17 +- fastlane_bot/modes/__init__.py | 4 +- fastlane_bot/modes/base.py | 149 +++----- fastlane_bot/modes/base_pairwise.py | 94 ++--- fastlane_bot/modes/base_triangle.py | 340 +++--------------- fastlane_bot/modes/pairwise_multi.py | 183 ---------- fastlane_bot/modes/pairwise_multi_all.py | 193 ++-------- fastlane_bot/modes/pairwise_multi_pol.py | 194 ++-------- fastlane_bot/modes/pairwise_single.py | 111 ------ .../modes/triangle_bancor_v3_two_hop.py | 295 ++++----------- fastlane_bot/modes/triangle_multi.py | 173 +++++---- fastlane_bot/modes/triangle_multi_complete.py | 191 ++++++---- fastlane_bot/modes/triangle_single.py | 96 ----- fastlane_bot/pool_finder.py | 2 +- fastlane_bot/tests/test_039_TestMultiMode.py | 251 ------------- fastlane_bot/tests/test_040_TestSingleMode.py | 197 ---------- .../tests/test_042_TestBancorV3ModeTwoHop.py | 34 +- .../tests/test_043_TestEmptyCarbonOrders.py | 4 +- fastlane_bot/tests/test_045_Validator.py | 2 +- fastlane_bot/tests/test_047_Randomizer.py | 4 +- ...st_048_RespectFlashloanTokensClickParam.py | 2 +- fastlane_bot/tests/test_050_TestBancorV2.py | 7 +- .../tests/test_058_BalancerIntegration.py | 2 - ...est_060_TestRoutehandlerCarbonPrecision.py | 6 +- .../tests/test_061_TestWETHConversion.py | 2 +- .../tests/test_063_TestBancorPOLMode.py | 6 +- .../tests/test_064_TestMultiAllMode.py | 2 - .../test_901_TestMultiTriangleModeSlow.py | 88 +---- fastlane_bot/tests/test_906_TargetTokens.py | 2 +- .../test_907_RuntimeParameters.py | 4 +- main.py | 5 +- scan_log_errors.py | 173 --------- 33 files changed, 537 insertions(+), 2307 deletions(-) delete mode 100644 fastlane_bot/modes/pairwise_multi.py delete mode 100644 fastlane_bot/modes/pairwise_single.py delete mode 100644 fastlane_bot/modes/triangle_single.py delete mode 100644 fastlane_bot/tests/test_039_TestMultiMode.py delete mode 100644 fastlane_bot/tests/test_040_TestSingleMode.py delete mode 100644 scan_log_errors.py diff --git a/README.md b/README.md index ed30f245b..aab66cb5a 100644 --- a/README.md +++ b/README.md @@ -107,13 +107,10 @@ You can configure the Fastlane Arbitrage Bot using the options in the `@click.op - **Triangular**: This includes arbitrage trades between three liquidity pools that can create a triangular route, starting and ending in the same token. For example, USDC > ETH, ETH > LINK, LINK > USDC - **Multi**: These modes can trade through multiple Carbon orders as a single trade. - **arb_mode options**: - - **single**: Pairwise arbitrage between one Carbon curve and one other exchange curve. - - **multi** Pairwise arbitrage between **multiple** Carbon curves and one other exchange curve. - - **triangle**: Triangular arbitrage between one Carbon curve and two other exchange curves. - - **multi_triangle**: Triangular arbitrage between **multiple** Carbon curves and two other exchange curves. + - **multi_triangle**: Triangular arbitrage between multiple Carbon curves and two other exchange curves. + - **multi_triangle_complete**: Triangular arbitrage between multiple Carbon curves and two other exchange curves (experimental). - **b3_two_hop**: Triangular arbitrage - the same as bancor_v3 mode but more gas-efficient. - **multi_pairwise_pol**: Pairwise multi-mode that always routes through the Bancor protocol-owned liquidity contract. - - **multi_pairwise_bal**: Pairwise multi-mode that always routes through Balancer. - **multi_pairwise_all**: **(Default)** Pairwise multi-mode that searches all available exchanges for pairwise arbitrage. - **flashloan_tokens** (str): Tokens the bot can use for flash loans. Specify token addresses as a comma-separated string (e.g., 0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C, 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2). - **n_jobs** (int): The number of parallel jobs to run. The default, -1, will use all available cores for the process. @@ -145,7 +142,7 @@ You can configure the Fastlane Arbitrage Bot using the options in the `@click.op Specify options in the command line. For example: ```bash -poetry run python main.py --arb_mode=multi --polling_interval=12 --reorg_delay=10 --loglevel=INFO +poetry run python main.py --arb_mode=multi_pairwise_all --polling_interval=12 --reorg_delay=10 --loglevel=INFO ``` ## Troubleshooting @@ -176,7 +173,7 @@ poetry run python main.py --arb_mode=b3_two_hop --alchemy_max_block_fetch=200 -- #### Carbon-focused pairwise arbitrage ```commandline -poetry run python main.py --arb_mode=multi --alchemy_max_block_fetch=200 --loglevel=INFO --backdate_pools=False --polling_interval=0 --reorg_delay=0 --run_data_validator=False --default_min_profit_gas_token=0.01 --randomizer=2 --exchanges=bancor_v3,bancor_v2,carbon_v1,uniswap_v3,uniswap_v2,sushiswap_v2,balancer,pancakeswap_v2,pancakeswap_v3 --flashloan_tokens="0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE,0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" +poetry run python main.py --arb_mode=multi_pairwise_all --alchemy_max_block_fetch=200 --loglevel=INFO --backdate_pools=False --polling_interval=0 --reorg_delay=0 --run_data_validator=False --default_min_profit_gas_token=0.01 --randomizer=2 --exchanges=bancor_v3,bancor_v2,carbon_v1,uniswap_v3,uniswap_v2,sushiswap_v2,balancer,pancakeswap_v2,pancakeswap_v3 --flashloan_tokens="0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE,0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" ``` #### Unfocused pairwise arbitrage ```commandline diff --git a/fastlane_bot/bot.py b/fastlane_bot/bot.py index 31090b8c5..bf8ba3843 100644 --- a/fastlane_bot/bot.py +++ b/fastlane_bot/bot.py @@ -66,16 +66,13 @@ split_carbon_trades, maximize_last_trade_per_tkn ) -from fastlane_bot.tools.cpc import ConstantProductCurve as CPC, CPCContainer, T +from fastlane_bot.tools.cpc import ConstantProductCurve as CPC, CPCContainer from .config.constants import FLASHLOAN_FEE_MAP from .events.interface import QueryInterface -from .modes.pairwise_multi import FindArbitrageMultiPairwise from .modes.pairwise_multi_all import FindArbitrageMultiPairwiseAll from .modes.pairwise_multi_pol import FindArbitrageMultiPairwisePol -from .modes.pairwise_single import FindArbitrageSinglePairwise from .modes.triangle_multi import ArbitrageFinderTriangleMulti from .modes.triangle_multi_complete import ArbitrageFinderTriangleMultiComplete -from .modes.triangle_single import ArbitrageFinderTriangleSingle from .modes.triangle_bancor_v3_two_hop import ArbitrageFinderTriangleBancor3TwoHop from .utils import num_format @@ -103,9 +100,6 @@ class CarbonBot: SCALING_FACTOR = 0.999 ARB_FINDER = { - "single": FindArbitrageSinglePairwise, - "multi": FindArbitrageMultiPairwise, - "triangle": ArbitrageFinderTriangleSingle, "multi_triangle": ArbitrageFinderTriangleMulti, "b3_two_hop": ArbitrageFinderTriangleBancor3TwoHop, "multi_pairwise_pol": FindArbitrageMultiPairwisePol, @@ -554,9 +548,6 @@ def get_prices_simple(self, CCm, tkn0, tkn1): curve_prices += [(x.params['exchange'],x.descr,x.cid,1/x.p) for x in CCm.bytknx(tkn1).bytkny(tkn0)] return curve_prices - # Global constant for Carbon Forks ordering - CARBON_SORTING_ORDER = float('inf') - # Create a sort order mapping function def create_sort_order(self, sort_sequence): # Create a dictionary mapping from sort sequence to indices, except for Carbon Forks @@ -564,11 +555,7 @@ def create_sort_order(self, sort_sequence): # Define the sort key function separately def sort_key(self, item, sort_order): - # Check if the item is Carbon Forks - if item[0] in self.ConfigObj.CARBON_V1_FORKS: - return self.CARBON_SORTING_ORDER - # Otherwise, use the sort order from the dictionary, or a default high value - return sort_order.get(item[0], self.CARBON_SORTING_ORDER - 1) + return float('inf') if item[0] in self.ConfigObj.CARBON_V1_FORKS else sort_order.get(item[0], float('inf')) # Define the custom sort function def custom_sort(self, data, sort_sequence): diff --git a/fastlane_bot/modes/__init__.py b/fastlane_bot/modes/__init__.py index b5f2014df..e82ba65d6 100644 --- a/fastlane_bot/modes/__init__.py +++ b/fastlane_bot/modes/__init__.py @@ -24,13 +24,11 @@ - ``ArbitrageFinderBase`` (``base``): fundamental base class - ``ArbitrageFinderPairwiseBase`` (``base_pairwise``): base class for pairwise arbitrages - - ``FindArbitrageSinglePairwise`` (``pairwise_single``) - - ``FindArbitrageMultiPairwise`` (``pairwise_multi``) - ``FindArbitrageMultiPairwiseAll`` (``pairwise_multi_all``) - ``FindArbitrageMultiPairwisePol`` (``pairwise_multi_pol``) - ``ArbitrageFinderTriangleBase`` (``base_triangle``): base class for triangle arbitrages - - ``ArbitrageFinderTriangleSingle`` (``triangle_single``) - ``ArbitrageFinderTriangleMulti`` (``triangle_multi``) + - ``ArbitrageFinderTriangleMultiComplete`` (``triangle_multi_complete``) - ``ArbitrageFinderTriangleBancor3TwoHop`` (``triangle_bancor_v3_two_hop``) diff --git a/fastlane_bot/modes/base.py b/fastlane_bot/modes/base.py index 0f1cf1a4c..5384827f1 100644 --- a/fastlane_bot/modes/base.py +++ b/fastlane_bot/modes/base.py @@ -13,10 +13,6 @@ from _decimal import Decimal import pandas as pd -from fastlane_bot.tools.cpc import T -from fastlane_bot.utils import num_format - - class ArbitrageFinderBase: """ Base class for all arbitrage finder modes @@ -47,27 +43,10 @@ def __init__( self.base_exchange = "bancor_v3" if arb_mode == "bancor_v3" else "carbon_v1" @abc.abstractmethod - def find_arbitrage( - self, - candidates: List[Any] = None, - ops: Tuple = None, - best_profit: float = 0, - profit_src: float = 0, - ) -> Union[List, Tuple]: + def find_arbitrage() -> Union[List, Tuple]: """ See subclasses for details - Parameters - ---------- - candidates : List[Any], optional - List of candidates, by default None - ops : Tuple, optional - Tuple of operations, by default None - best_profit : float, optional - Best profit so far, by default 0 - profit_src : float, optional - Profit source, by default 0 - Returns ------- Union[List, Tuple] @@ -75,86 +54,66 @@ def find_arbitrage( """ pass - def _set_best_ops( - self, - best_profit: float, - ops: Tuple, - profit: float, - src_token: str, - trade_instructions: Any, - trade_instructions_df: pd.DataFrame, - trade_instructions_dic: Dict[str, Any], - ) -> Tuple[float, Tuple]: - """ - Set the best operations. - - Parameters: - - """ - self.ConfigObj.logger.debug("[modes.base._set_best_ops] *************") - self.ConfigObj.logger.debug( - f"[modes.base._set_best_ops] New best profit: {profit}" - ) - - # Update the best profit and source token - best_profit = profit - best_src_token = src_token - - # Update the best trade instructions - best_trade_instructions_df = trade_instructions_df - best_trade_instructions_dic = trade_instructions_dic - best_trade_instructions = trade_instructions - - self.ConfigObj.logger.debug( - f"[modes.base._set_best_ops] best_trade_instructions_df: {best_trade_instructions_df}" - ) - - # Update the optimal operations - ops = ( - best_profit, - best_trade_instructions_df, - best_trade_instructions_dic, - best_src_token, - best_trade_instructions, - ) - - self.ConfigObj.logger.debug("[modes.base.calculate_profit] *************") - - return best_profit, ops - def get_prices_simple(self, CCm, tkn0, tkn1): curve_prices = [(x.params['exchange'],x.descr,x.cid,x.p) for x in CCm.bytknx(tkn0).bytkny(tkn1)] curve_prices += [(x.params['exchange'],x.descr,x.cid,1/x.p) for x in CCm.bytknx(tkn1).bytkny(tkn0)] return curve_prices - # Global constant for 'carbon_v1' order - CARBON_SORTING_ORDER = float('inf') - # Create a sort order mapping function def create_sort_order(self, sort_sequence): - # Create a dictionary mapping from sort sequence to indices, except for 'carbon_v1' - return {key: index for index, key in enumerate(sort_sequence) if key != 'carbon_v1'} + # Create a dictionary mapping from sort sequence to indices, except for Carbon Forks + return {key: index for index, key in enumerate(sort_sequence) if key not in self.ConfigObj.CARBON_V1_FORKS} # Define the sort key function separately def sort_key(self, item, sort_order): - # Check if the item is 'carbon_v1' - if item[0] in self.ConfigObj.CARBON_V1_FORKS: - return self.CARBON_SORTING_ORDER - # Otherwise, use the sort order from the dictionary, or a default high value - return sort_order.get(item[0], self.CARBON_SORTING_ORDER - 1) + return float('inf') if item[0] in self.ConfigObj.CARBON_V1_FORKS else sort_order.get(item[0], float('inf')) # Define the custom sort function def custom_sort(self, data, sort_sequence): sort_order = self.create_sort_order(sort_sequence) return sorted(data, key=lambda item: self.sort_key(item, sort_order)) + def update_results( + self, + src_token: str, + r, + trade_instructions_dic, + trade_instructions_df, + trade_instructions, + candidates, + best_profit, + ops, + ): + # Calculate the profit + profit = self.calculate_profit(src_token, -r.result, self.CCm) + if str(profit) == "nan": + self.ConfigObj.logger.debug("profit is nan, skipping") + else: + # Handle candidates based on conditions + candidates += self.handle_candidates( + profit, + trade_instructions_df, + trade_instructions_dic, + src_token, + trade_instructions, + ) + # Find the best operations + best_profit, ops = self.find_best_operations( + best_profit, + ops, + profit, + trade_instructions_df, + trade_instructions_dic, + src_token, + trade_instructions, + ) + return best_profit, ops + def calculate_profit( self, src_token: str, profit_src: float, CCm: Any, - cids: List[str], - profit: int = 0, ) -> float: """ Calculate profit based on the source token. @@ -201,7 +160,6 @@ def get_netchange(trade_instructions_df: pd.DataFrame) -> List[float]: def handle_candidates( self, - best_profit: float, profit: float, trade_instructions_df: pd.DataFrame, trade_instructions_dic: Dict[str, Any], @@ -213,8 +171,6 @@ def handle_candidates( Parameters: ---------- - best_profit : float - Best profit profit : float Profit trade_instructions_df : pd.DataFrame @@ -290,29 +246,12 @@ def find_best_operations( condition_better_profit = profit > best_profit condition_zeros_one_token = max(netchange) < 1e-4 if condition_better_profit and condition_zeros_one_token: - return self._set_best_ops( - best_profit, - ops, + best_profit = profit + ops = ( profit, - src_token, - trade_instructions, trade_instructions_df, trade_instructions_dic, + src_token, + trade_instructions, ) return best_profit, ops - - def _check_limit_flashloan_tokens_for_bancor3(self): - """ - Limit the flashloan tokens for bancor v3. - """ - fltkns = self.CCm.byparams(exchange="bancor_v3").tknys() - if self.ConfigObj.LIMIT_BANCOR3_FLASHLOAN_TOKENS: - # Filter out tokens that are not in the existing flashloan_tokens list - self.flashloan_tokens = [ - tkn for tkn in fltkns if tkn in self.flashloan_tokens - ] - self.ConfigObj.logger.info( - f"[modes.base._check_limit_flashloan_tokens_for_bancor3] limiting flashloan_tokens to {self.flashloan_tokens}" - ) - else: - self.flashloan_tokens = fltkns diff --git a/fastlane_bot/modes/base_pairwise.py b/fastlane_bot/modes/base_pairwise.py index d5df65ef9..ae2d52d42 100644 --- a/fastlane_bot/modes/base_pairwise.py +++ b/fastlane_bot/modes/base_pairwise.py @@ -8,53 +8,55 @@ All rights reserved. Licensed under MIT. """ -import abc -import itertools -from typing import List, Tuple, Any, Union +from typing import List, Tuple, Union -from fastlane_bot.modes.base import ArbitrageFinderBase from fastlane_bot.tools.cpc import CPCContainer - +from fastlane_bot.tools.optimizer import PairOptimizer +from fastlane_bot.modes.base import ArbitrageFinderBase class ArbitrageFinderPairwiseBase(ArbitrageFinderBase): - """ - Base class for pairwise arbitrage finder modes - """ - - @abc.abstractmethod - def find_arbitrage(self, candidates: List[Any] = None, ops: Tuple = None, best_profit: float = 0, profit_src: float = 0) -> Union[List, Tuple]: - """ - see base.py - """ - pass - - @staticmethod - def get_combos( - CCm: CPCContainer, flashloan_tokens: List[str] - ) -> Tuple[List[Any], List[Any]]: - """ - Get combos for pairwise arbitrage - - Parameters - ---------- - CCm : CPCContainer - Container for all the curves - flashloan_tokens : list - List of flashloan tokens - - Returns - ------- - all_tokens : list - List of all tokens - - """ - all_tokens = CCm.tokens() - flashloan_tokens_intersect = all_tokens.intersection(set(flashloan_tokens)) - combos = [ - (tkn0, tkn1) - for tkn0, tkn1 in itertools.product(all_tokens, flashloan_tokens_intersect) - # tkn1 is always the token being flash loaned - # note that the pair is tkn0/tkn1, ie tkn1 is the quote token - if tkn0 != tkn1 - ] - return all_tokens, combos + def find_arbitrage(self) -> Union[List, Tuple]: + all_tokens, combos = self.get_combos(self.flashloan_tokens, self.CCm) + if self.result == self.AO_TOKENS: + return all_tokens, combos + + candidates = [] + best_profit = 0 + ops = None + + for tkn0, tkn1 in combos: + CC = self.CCm.bypairs(f"{tkn0}/{tkn1}") + if len(CC) < 2: + continue + + for curve_combo in self.get_curve_combos(CC): + src_token = tkn1 + if len(curve_combo) < 2: + continue + try: + CC_cc = CPCContainer(curve_combo) + O = PairOptimizer(CC_cc) + pstart = {tkn0: CC_cc.bypairs(f"{tkn0}/{tkn1}")[0].p} + r = O.optimize(src_token, params=dict(pstart=pstart)) + trade_instructions_dic = r.trade_instructions(O.TIF_DICTS) + trade_instructions_df = r.trade_instructions(O.TIF_DFAGGR) + trade_instructions = r.trade_instructions() + except Exception as e: + self.ConfigObj.logger.debug(f"[base_pairwise] {e}") + continue + if trade_instructions_dic is None or len(trade_instructions_dic) < 2: + # Failed to converge + continue + # Update the results + best_profit, ops = self.update_results( + src_token, + r, + trade_instructions_dic, + trade_instructions_df, + trade_instructions, + candidates, + best_profit, + ops, + ) + + return candidates if self.result == self.AO_CANDIDATES else ops diff --git a/fastlane_bot/modes/base_triangle.py b/fastlane_bot/modes/base_triangle.py index 522283b36..5288a9c93 100644 --- a/fastlane_bot/modes/base_triangle.py +++ b/fastlane_bot/modes/base_triangle.py @@ -8,308 +8,58 @@ All rights reserved. Licensed under MIT. """ -import abc -import itertools -from typing import List, Any, Tuple, Union - -import pandas as pd +from typing import List, Tuple, Union +from fastlane_bot.tools.cpc import CPCContainer +from fastlane_bot.tools.optimizer import MargPOptimizer from fastlane_bot.modes.base import ArbitrageFinderBase -from fastlane_bot.tools.cpc import T - -def sort_pairs(pairs): - # Clean up the pairs alphabetically - return ["/".join(sorted(pair.split('/'))) for pair in pairs] - -def flatten_nested_items_in_list(nested_list): - # unpack nested items - flattened_list = [] - for items in nested_list: - flat_list = [] - for item in items: - if isinstance(item, list): - flat_list.extend(item) - else: - flat_list.append(item) - flattened_list.append(flat_list) - return flattened_list - -def get_triangle_groups(flt, x_y_pairs): - # Get groups of triangles that conform to (flt/x , x/y, y/flt) where x!=y - triangle_groups = [] - for pair in x_y_pairs: - x,y = pair.split('/') - triangle_groups += [("/".join(sorted([flt,x])), pair, "/".join(sorted([flt,y])))] - return triangle_groups - -def get_triangle_groups_stats(triangle_groups, all_relevant_pairs_info): - # Get the stats on the triangle group cohort for decision making - valid_carbon_triangles = [] - for triangle in triangle_groups: - path_len = 0 - has_carbon = False - for pair in triangle: - if all_relevant_pairs_info[pair]['all_counts'] > 0: - path_len += 1 - if all_relevant_pairs_info[pair]['carbon_counts'] > 0: - has_carbon = True - if path_len == 3 and has_carbon == True: - valid_carbon_triangles.append(triangle) - return valid_carbon_triangles class ArbitrageFinderTriangleBase(ArbitrageFinderBase): - """ - Base class for triangular arbitrage finder modes - """ - - @abc.abstractmethod - def find_arbitrage(self, candidates: List[Any] = None, ops: Tuple = None, best_profit: float = 0, profit_src: float = 0) -> Union[List, Tuple]: - """ - see base.py - """ - pass + def find_arbitrage(self) -> Union[List, Tuple]: + self.handle_exchange() + combos = self.get_combos(self.flashloan_tokens, self.CCm) - @staticmethod - def get_miniverse( - y_match_curves_not_carbon: List[Any], - base_exchange_curves: List[Any], - x_match_curves_not_carbon: List[Any], - flt: str, - arb_mode: str, - combos: List[Any], - ) -> List[Any]: - """ - Get miniverse for triangular arbitrage + candidates = [] + best_profit = 0 + ops = None - Parameters - ---------- - y_match_curves_not_carbon : list - List of curves that match the y token and are not on carbon - base_exchange_curves : list - List of curves on the base exchange - x_match_curves_not_carbon : list - List of curves that match the x token and are not on carbon - flt : str - Flashloan token - arb_mode : str - Arbitrage mode - combos : list - List of combos - - Returns - ------- - combos : list - List of combos - - """ - if arb_mode in ["single_triangle", "triangle"]: - miniverses = list( - itertools.product( - y_match_curves_not_carbon, - base_exchange_curves, - x_match_curves_not_carbon, - ) - ) - else: - external_curve_combos = list( - itertools.product(y_match_curves_not_carbon, x_match_curves_not_carbon) + for src_token, miniverse in combos: + try: + CC_cc = CPCContainer(miniverse) + O = MargPOptimizer(CC_cc) + pstart = build_pstart(self.CCm, CC_cc, src_token, self.ConfigObj) + r = O.optimize(src_token, params=dict(pstart=pstart)) + trade_instructions_dic = r.trade_instructions(O.TIF_DICTS) + trade_instructions_df = r.trade_instructions(O.TIF_DFAGGR) + trade_instructions = r.trade_instructions() + except Exception as e: + self.ConfigObj.logger.debug(f"[base_triangle] {e}") + continue + if trade_instructions_dic is None or len(trade_instructions_dic) < 3: + # Failed to converge + continue + # Update the results + best_profit, ops = self.update_results( + src_token, + r, + trade_instructions_dic, + trade_instructions_df, + trade_instructions, + candidates, + best_profit, + ops, ) - miniverses = [ - base_exchange_curves + list(combo) for combo in external_curve_combos - ] - if miniverses: - combos += list(zip([flt] * len(miniverses), miniverses)) - return combos - - def get_combos( - self, flashloan_tokens: List[str], CCm: Any, arb_mode: str - ) -> Tuple[List[str], List[Any]]: - """ - Get combos for triangular arbitrage - - Parameters - ---------- - flashloan_tokens : list - List of flashloan tokens - CCm : object - CCm object - arb_mode : str - Arbitrage mode - - Returns - ------- - combos : list - List of combos - - """ - combos = [] - if arb_mode in ["b3_two_hop"]: - combos = [ - (tkn0, tkn1) - for tkn0, tkn1 in itertools.product(flashloan_tokens, flashloan_tokens) - # note that the pair is tkn0/tkn1, ie tkn1 is the quote token - if tkn0 != tkn1 - ] - else: - all_base_exchange_curves = CCm.byparams(exchange=self.base_exchange).curves - for flt in flashloan_tokens: # may wish to run this for one flt at a time - non_flt_base_exchange_curves = [ - x for x in all_base_exchange_curves if flt not in x.pair - ] - for non_flt_base_exchange_curve in non_flt_base_exchange_curves: - target_tkny = non_flt_base_exchange_curve.tkny - target_tknx = non_flt_base_exchange_curve.tknx - base_exchange_curves = ( - CCm.bypairs(f"{target_tknx}/{target_tkny}") - .byparams(exchange=self.base_exchange) - .curves - ) - if len(base_exchange_curves) == 0: - continue - - base_direction_pair = base_exchange_curves[0].pair - base_direction_one = [curve for curve in base_exchange_curves if curve.pair == base_direction_pair] - base_direction_two = [curve for curve in base_exchange_curves if curve.pair != base_direction_pair] - assert len(base_exchange_curves) == len(base_direction_one) + len(base_direction_two) - y_match_curves = CCm.bypairs( - set(CCm.filter_pairs(onein=target_tknx)) - & set(CCm.filter_pairs(onein=flt)) - ) - x_match_curves = CCm.bypairs( - set(CCm.filter_pairs(onein=target_tkny)) - & set(CCm.filter_pairs(onein=flt)) - ) - - y_match_curves_not_carbon = [ - x - for x in y_match_curves - if x.params.exchange != self.base_exchange - ] - if len(y_match_curves_not_carbon) == 0: - continue - x_match_curves_not_carbon = [ - x - for x in x_match_curves - if x.params.exchange != self.base_exchange - ] - if len(x_match_curves_not_carbon) == 0: - continue - if len(base_direction_one) > 0: - combos = self.get_miniverse( - y_match_curves_not_carbon, - base_direction_one, - x_match_curves_not_carbon, - flt, - arb_mode, - combos, - ) - if len(base_direction_two) > 0: - combos = self.get_miniverse( - y_match_curves_not_carbon, - base_direction_two, - x_match_curves_not_carbon, - flt, - arb_mode, - combos, - ) - return combos - - def get_all_relevant_pairs_info(self, CCm, all_relevant_pairs): - # Get pair info for the cohort to allow decision making at the triangle level - all_relevant_pairs_info = {} - for pair in all_relevant_pairs: - all_relevant_pairs_info[pair] = {} - pair_curves = CCm.bypair(pair) - carbon_curves = [] - non_carbon_curves = [] - for x in pair_curves: - if x.params.exchange in self.ConfigObj.CARBON_V1_FORKS: - carbon_curves += [x] - else: - non_carbon_curves += [x] - all_relevant_pairs_info[pair]['non_carbon_curves'] = non_carbon_curves - all_relevant_pairs_info[pair]['carbon_curves'] = carbon_curves - all_relevant_pairs_info[pair]['curves'] = non_carbon_curves + [carbon_curves] if len(carbon_curves) > 0 else non_carbon_curves # condense carbon curves into a single list - all_relevant_pairs_info[pair]['all_counts'] = len(pair_curves) - all_relevant_pairs_info[pair]['carbon_counts'] = len(carbon_curves) - return all_relevant_pairs_info - - def get_analysis_set_per_flt(self, flt, valid_triangles, all_relevant_pairs_info): - flt_triangle_analysis_set = [] - for triangle in valid_triangles: - multiverse = [all_relevant_pairs_info[pair]['curves'] for pair in triangle] - product_of_triangle = list(itertools.product(multiverse[0], multiverse[1], multiverse[2])) - triangles_to_run = flatten_nested_items_in_list(product_of_triangle) - flt_triangle_analysis_set += list(zip([flt] * len(triangles_to_run), triangles_to_run)) - - self.ConfigObj.logger.debug(f"[base_triangle.get_analysis_set_per_flt] Length of flt_triangle_analysis_set: {flt, len(flt_triangle_analysis_set)}") - return flt_triangle_analysis_set - - def get_comprehensive_triangles( - self, flashloan_tokens: List[str], CCm: Any - ) -> Tuple[List[str], List[Any]]: - """ - Get comprehensive combos for triangular arbitrage - - Parameters - ---------- - flashloan_tokens : list - List of flashloan tokens - CCm : object - CCm object - - Returns - ------- - combos : list - List of combos - - """ - combos = [] - for flt in flashloan_tokens: - - # Get the Carbon pairs - carbon_pairs = sort_pairs(set([x.pair for x in CCm.curves if x.params.exchange in self.ConfigObj.CARBON_V1_FORKS])) - - # Create a set of unique tokens, excluding 'flt' - x_tokens = {token for pair in carbon_pairs for token in pair.split('/') if token != flt} - - # Get relevant pairs containing the flashloan token - flt_x_pairs = sort_pairs([f"{x}/{flt}" for x in x_tokens]) - - # Generate all possible 2-item combinations from the unique tokens that arent the flashloan token - x_y_pairs = sort_pairs(["{}/{}".format(x, y) for x, y in itertools.combinations(x_tokens, 2)]) - - # Note the relevant pairs - all_relevant_pairs = flt_x_pairs + x_y_pairs - self.ConfigObj.logger.debug(f"len(all_relevant_pairs) {len(all_relevant_pairs)}") - - # Generate triangle groups - triangle_groups = get_triangle_groups(flt, x_y_pairs) - self.ConfigObj.logger.debug(f"len(triangle_groups) {len(triangle_groups)}") - # Get pair info for the cohort - all_relevant_pairs_info = self.get_all_relevant_pairs_info(CCm, all_relevant_pairs) - - # Generate valid triangles for the groups base on arb_mode - valid_triangles = get_triangle_groups_stats(triangle_groups, all_relevant_pairs_info) - - # Get [(flt,curves)] analysis set for the flt - flt_triangle_analysis_set = self.get_analysis_set_per_flt(flt, valid_triangles, all_relevant_pairs_info) - - # The entire analysis set for all flts - combos.extend(flt_triangle_analysis_set) - return combos + return candidates if self.result == self.AO_CANDIDATES else ops - def build_pstart(self, CCm, tkn0list, tkn1): - tkn0list = [x for x in tkn0list if x not in [tkn1]] - pstart = {} - for tkn0 in tkn0list: +def build_pstart(CCm, CC_cc, tkn1, cfg): + pstart = {tkn1: 1} + for tkn0 in [x for x in CC_cc.tokens() if x != tkn1]: + try: + pstart[tkn0] = CCm.bytknx(tkn0).bytkny(tkn1)[0].p + except: try: - pstart[tkn0] = CCm.bytknx(tkn0).bytkny(tkn1)[0].p - except: - try: - pstart[tkn0] = 1/CCm.bytknx(tkn1).bytkny(tkn0)[0].p - except Exception as e: - self.ConfigObj.logger.info(f"[pstart build] {tkn0}/{tkn1} price error {e}") - pstart[tkn1] = 1 - return pstart + pstart[tkn0] = 1/CCm.bytknx(tkn1).bytkny(tkn0)[0].p + except Exception as e: + cfg.logger.info(f"[pstart build] {tkn0}/{tkn1} price error {e}") + return pstart diff --git a/fastlane_bot/modes/pairwise_multi.py b/fastlane_bot/modes/pairwise_multi.py deleted file mode 100644 index 756345edf..000000000 --- a/fastlane_bot/modes/pairwise_multi.py +++ /dev/null @@ -1,183 +0,0 @@ -""" -Defines the Multi-pairwise arbitrage finder class - -[DOC-TODO-OPTIONAL-longer description in rst format] - ---- -(c) Copyright Bprotocol foundation 2023-24. -All rights reserved. -Licensed under MIT. -""" -from typing import List, Any, Tuple, Union, Hashable - -import pandas as pd - -from fastlane_bot.modes.base_pairwise import ArbitrageFinderPairwiseBase -from fastlane_bot.tools.cpc import CPCContainer -from fastlane_bot.tools.optimizer import MargPOptimizer, PairOptimizer - - -class FindArbitrageMultiPairwise(ArbitrageFinderPairwiseBase): - """ - Multi-pairwise arbitrage finder mode. - """ - - arb_mode = "multi_pairwise" - - def find_arbitrage( - self, - candidates: List[Any] = None, - ops: Tuple = None, - best_profit: float = 0, - profit_src: float = 0, - ) -> Union[List, Tuple]: - """ - see base.py - """ - if self.base_exchange != "carbon_v1": - raise ValueError("base_exchange must be carbon_v1 for `multi` mode") - - if candidates is None: - candidates = [] - - all_tokens, combos = self.get_combos(self.CCm, self.flashloan_tokens) - if self.result == self.AO_TOKENS: - return all_tokens, combos - - candidates = [] - self.ConfigObj.logger.debug( - f"\n ************ combos: {len(combos)} ************\n" - ) - for tkn0, tkn1 in combos: - r = None - CC = self.CCm.bypairs(f"{tkn0}/{tkn1}") - if len(CC) < 2: - continue - carbon_curves = [x for x in CC.curves if x.params.exchange in self.ConfigObj.CARBON_V1_FORKS] - not_carbon_curves = [ - x for x in CC.curves if x.params.exchange not in self.ConfigObj.CARBON_V1_FORKS - ] - curve_combos = [] - - if len(carbon_curves) > 0: - base_direction_pair = carbon_curves[0].pair - base_direction_one = [curve for curve in carbon_curves if curve.pair == base_direction_pair] - base_direction_two = [curve for curve in carbon_curves if curve.pair != base_direction_pair] - - if len(base_direction_one) > 0: - curve_combos += [[curve] + base_direction_one for curve in not_carbon_curves] - - if len(base_direction_two) > 0: - curve_combos += [[curve] + base_direction_two for curve in not_carbon_curves] - - - for curve_combo in curve_combos: - src_token = tkn1 - - if len(curve_combo) < 2: - continue - - try: - (O, profit_src, r, trade_instructions_df,) = self.run_main_flow( - curves=curve_combo, src_token=src_token, tkn0=tkn0, tkn1=tkn1 - ) - - trade_instructions_dic = r.trade_instructions(O.TIF_DICTS) - trade_instructions = r.trade_instructions() - - except Exception: - continue - - if trade_instructions_dic is None: - continue - if len(trade_instructions_dic) < 2: - continue - - # Get the cids - cids = [ti["cid"] for ti in trade_instructions_dic] - - # Calculate the profit - profit = self.calculate_profit(src_token, profit_src, self.CCm, cids) - - if str(profit) == "nan": - self.ConfigObj.logger.debug("profit is nan, skipping") - continue - - # Handle candidates based on conditions - candidates += self.handle_candidates( - best_profit, - profit, - trade_instructions_df, - trade_instructions_dic, - src_token, - trade_instructions, - ) - - # Find the best operations - best_profit, ops = self.find_best_operations( - best_profit, - ops, - profit, - trade_instructions_df, - trade_instructions_dic, - src_token, - trade_instructions, - ) - - return candidates if self.result == self.AO_CANDIDATES else ops - - def get_wrong_direction_cids( - self, tkn0_into_carbon: bool, trade_instructions_df: pd.DataFrame - ) -> List[Hashable]: - """ - Get the cids of the wrong direction curves - - Parameters - ---------- - tkn0_into_carbon : bool - True if tkn0 is being converted into carbon, False otherwise - trade_instructions_df : pd.DataFrame - The trade instructions dataframe - - Returns - ------- - List[str] - The cids of the wrong direction curves - """ - return [ - idx - for idx, row in trade_instructions_df.iterrows() - if ( - (tkn0_into_carbon and row[0] < 0) - or (not tkn0_into_carbon and row.iloc[0] > 0) - ) - and ("-0" in idx or "-1" in idx) - ] - - @staticmethod - def run_main_flow( - curves: List[Any], src_token: str, tkn0: str, tkn1: str - ) -> Tuple[Any, float, Any, pd.DataFrame]: - """ - Run main flow to find arbitrage. - """ - CC_cc = CPCContainer(curves) - O = PairOptimizer(CC_cc) - pstart = { - tkn0: CC_cc.bypairs(f"{tkn0}/{tkn1}")[0].p - } # this intentionally selects the non_carbon curve - r = O.optimize(src_token, params=dict(pstart=pstart)) - profit_src = -r.result - trade_instructions_df = r.trade_instructions(O.TIF_DFAGGR) - return O, profit_src, r, trade_instructions_df - - def process_wrong_direction_pools( - self, curve_combo: List[Any], wrong_direction_cids: List[Hashable] - ) -> [str]: - """ - Process curves with wrong direction pools. - """ - new_curves = [ - curve for curve in curve_combo if curve.cid not in wrong_direction_cids - ] - return new_curves diff --git a/fastlane_bot/modes/pairwise_multi_all.py b/fastlane_bot/modes/pairwise_multi_all.py index 20cdbb46f..3fd68a017 100644 --- a/fastlane_bot/modes/pairwise_multi_all.py +++ b/fastlane_bot/modes/pairwise_multi_all.py @@ -8,179 +8,60 @@ All rights reserved. Licensed under MIT. """ -import itertools -from typing import List, Any, Tuple, Union, Hashable - -import pandas as pd +from typing import List, Any, Tuple +from itertools import product from fastlane_bot.modes.base_pairwise import ArbitrageFinderPairwiseBase -from fastlane_bot.tools.cpc import CPCContainer -from fastlane_bot.tools.optimizer import MargPOptimizer, PairOptimizer - class FindArbitrageMultiPairwiseAll(ArbitrageFinderPairwiseBase): - """ - Multi-pairwise arbitrage finder mode. - """ - arb_mode = "multi_pairwise_all" - def find_arbitrage(self, candidates: List[Any] = None, ops: Tuple = None, best_profit: float = 0, profit_src: float = 0) -> Union[List, Tuple]: - """ - see base.py + def get_combos(self, flashloan_tokens: List[str], CCm: Any) -> Tuple[List[str], List[Any]]: """ - - if candidates is None: - candidates = [] - - all_tokens, combos = self.get_combos(self.CCm, self.flashloan_tokens) - if self.result == self.AO_TOKENS: - return all_tokens, combos - #print(f"combos = {combos}") - - candidates = [] - self.ConfigObj.logger.debug( - f"\n ************ combos: {len(combos)} ************\n" - ) - - for tkn0, tkn1 in combos: - r = None - CC = self.CCm.bypairs(f"{tkn0}/{tkn1}") - if len(CC) < 2: - continue - carbon_curves = [x for x in CC.curves if x.params.exchange in self.ConfigObj.CARBON_V1_FORKS] - not_carbon_curves = [ - x for x in CC.curves if x.params.exchange not in self.ConfigObj.CARBON_V1_FORKS - ] - - curve_combos = [[_curve0] + [_curve1] for _curve0 in not_carbon_curves for _curve1 in not_carbon_curves if (_curve0 != _curve1)] - - if len(carbon_curves) > 0: - base_direction_pair = carbon_curves[0].pair - base_direction_one = [curve for curve in carbon_curves if curve.pair == base_direction_pair] - base_direction_two = [curve for curve in carbon_curves if curve.pair != base_direction_pair] - curve_combos = [] - - if len(base_direction_one) > 0: - curve_combos += [[curve] + base_direction_one for curve in not_carbon_curves] - - if len(base_direction_two) > 0: - curve_combos += [[curve] + base_direction_two for curve in not_carbon_curves] - - if len(carbon_curves) >= 2: - curve_combos += [carbon_curves] - - for curve_combo in curve_combos: - src_token = tkn1 - if len(curve_combo) < 2: - continue - try: - ( - O, - profit_src, - r, - trade_instructions_df, - ) = self.run_main_flow(curves=curve_combo, src_token=src_token, tkn0=tkn0, tkn1=tkn1) - except ValueError: - #Optimizer did not converge - continue - - - trade_instructions_dic = r.trade_instructions(O.TIF_DICTS) - trade_instructions = r.trade_instructions() - if trade_instructions_dic is None: - continue - if len(trade_instructions_dic) < 2: - continue - # Get the cids - cids = [ti["cid"] for ti in trade_instructions_dic] - - # Calculate the profit - profit = self.calculate_profit(src_token, profit_src, self.CCm, cids) - if str(profit) == "nan": - self.ConfigObj.logger.debug("profit is nan, skipping") - continue - - # Handle candidates based on conditions - candidates += self.handle_candidates( - best_profit, - profit, - trade_instructions_df, - trade_instructions_dic, - src_token, - trade_instructions, - ) - - # Find the best operations - best_profit, ops = self.find_best_operations( - best_profit, - ops, - profit, - trade_instructions_df, - trade_instructions_dic, - src_token, - trade_instructions, - ) - - return candidates if self.result == self.AO_CANDIDATES else ops - - @staticmethod - def get_wrong_direction_cids( - tkn0_into_carbon: bool, trade_instructions_df: pd.DataFrame - ) -> List[Hashable]: - """ - Get the cids of the wrong direction curves + Get combos for pairwise arbitrage Parameters ---------- - tkn0_into_carbon : bool - True if tkn0 is being converted into carbon, False otherwise - trade_instructions_df : pd.DataFrame - The trade instructions dataframe + flashloan_tokens : list + List of flashloan tokens + CCm : object + CCm object Returns ------- - List[str] - The cids of the wrong direction curves + all_tokens : list + List of all tokens + """ - return [ - idx - for idx, row in trade_instructions_df.iterrows() - if ( - (tkn0_into_carbon and row.iloc[0] < 0) - or (not tkn0_into_carbon and row.iloc[0] > 0) - ) - and ("-0" in idx or "-1" in idx) + all_tokens = CCm.tokens() + flashloan_tokens_intersect = all_tokens.intersection(set(flashloan_tokens)) + combos = [ + (tkn0, tkn1) + for tkn0, tkn1 in product(all_tokens, flashloan_tokens_intersect) + # tkn1 is always the token being flash loaned + # note that the pair is tkn0/tkn1, ie tkn1 is the quote token + if tkn0 != tkn1 ] + return all_tokens, combos - @staticmethod - def run_main_flow( - curves: List[Any], src_token: str, tkn0: str, tkn1: str - ) -> Tuple[Any, float, Any, pd.DataFrame]: - """ - Run main flow to find arbitrage. - """ - CC_cc = CPCContainer(curves) - O = PairOptimizer(CC_cc) - pstart = { - tkn0: CC_cc.bypairs(f"{tkn0}/{tkn1}")[0].p - } # this intentionally selects the non_carbon curve + def get_curve_combos(self, CC: Any) -> List[Any]: + carbon_curves = [x for x in CC.curves if x.params.exchange in self.ConfigObj.CARBON_V1_FORKS] + not_carbon_curves = [x for x in CC.curves if x.params.exchange not in self.ConfigObj.CARBON_V1_FORKS] + curve_combos = [[_curve0] + [_curve1] for _curve0 in not_carbon_curves for _curve1 in not_carbon_curves if (_curve0 != _curve1)] - r = O.optimize(src_token, params=dict(pstart=pstart)) + if len(carbon_curves) > 0: + base_direction_pair = carbon_curves[0].pair + base_direction_one = [curve for curve in carbon_curves if curve.pair == base_direction_pair] + base_direction_two = [curve for curve in carbon_curves if curve.pair != base_direction_pair] + curve_combos = [] - profit_src = -r.result - trade_instructions_df = r.trade_instructions(O.TIF_DFAGGR) - return O, profit_src, r, trade_instructions_df + if len(base_direction_one) > 0: + curve_combos += [[curve] + base_direction_one for curve in not_carbon_curves] - @staticmethod - def process_wrong_direction_pools( - curve_combo: List[Any], wrong_direction_cids: List[Hashable] - ) -> [str]: - """ - Process curves with wrong direction pools. - """ - new_curves = [ - curve for curve in curve_combo if curve.cid not in wrong_direction_cids - ] - return new_curves + if len(base_direction_two) > 0: + curve_combos += [[curve] + base_direction_two for curve in not_carbon_curves] + + if len(carbon_curves) >= 2: + curve_combos += [carbon_curves] + return curve_combos diff --git a/fastlane_bot/modes/pairwise_multi_pol.py b/fastlane_bot/modes/pairwise_multi_pol.py index 36fc6f1cb..6ead55ce7 100644 --- a/fastlane_bot/modes/pairwise_multi_pol.py +++ b/fastlane_bot/modes/pairwise_multi_pol.py @@ -8,183 +8,24 @@ All rights reserved. Licensed under MIT. """ -from typing import List, Any, Tuple, Union, Hashable +from typing import List, Any, Tuple +from itertools import product -import pandas as pd -import itertools from fastlane_bot.modes.base_pairwise import ArbitrageFinderPairwiseBase -from fastlane_bot.tools.cpc import CPCContainer -from fastlane_bot.tools.optimizer import MargPOptimizer, PairOptimizer -from fastlane_bot.tools.cpc import T - class FindArbitrageMultiPairwisePol(ArbitrageFinderPairwiseBase): - """ - Multi-pairwise arbitrage finder mode for Bancor POL. - """ - arb_mode = "multi_pairwise_pol" - def find_arbitrage(self, candidates: List[Any] = None, ops: Tuple = None, best_profit: float = 0, profit_src: float = 0) -> Union[List, Tuple]: - """ - see base.py - """ - - all_tokens, combos = self.get_combos_pol(self.CCm, self.flashloan_tokens) - if self.result == self.AO_TOKENS: - return all_tokens, combos - - candidates = [] - self.ConfigObj.logger.debug( - f"\n ************ combos: {len(combos)} ************\n" - ) - - for tkn0, tkn1 in combos: - r = None - CC = self.CCm.bypairs(f"{tkn0}/{tkn1}") - if len(CC) < 2: - continue - pol_curves = [x for x in CC.curves if x.params.exchange == "bancor_pol"] - not_bancor_pol_curves = [ - x for x in CC.curves if x.params.exchange not in ["bancor_pol"] + self.ConfigObj.CARBON_V1_FORKS - ] - carbon_curves = [x for x in CC.curves if x.params.exchange in self.ConfigObj.CARBON_V1_FORKS] - curve_combos = [[curve] + pol_curves for curve in not_bancor_pol_curves] - - if len(carbon_curves) > 0: - base_direction_pair = carbon_curves[0].pair - base_direction_one = [curve for curve in carbon_curves if curve.pair == base_direction_pair] - base_direction_two = [curve for curve in carbon_curves if curve.pair != base_direction_pair] - - if len(base_direction_one) > 0: - curve_combos += [[curve] + base_direction_one for curve in pol_curves] - - if len(base_direction_two) > 0: - curve_combos += [[curve] + base_direction_two for curve in pol_curves] - - for curve_combo in curve_combos: - src_token = tkn1 - if len(curve_combo) < 2: - continue - - try: - ( - O, - profit_src, - r, - trade_instructions_df, - ) = self.run_main_flow(curves=curve_combo, src_token=src_token, tkn0=tkn0, tkn1=tkn1) - - trade_instructions_dic = r.trade_instructions(O.TIF_DICTS) - trade_instructions = r.trade_instructions() - - except Exception: - continue - if trade_instructions_dic is None: - continue - if len(trade_instructions_dic) < 2: - continue - # Get the cids - cids = [ti["cid"] for ti in trade_instructions_dic] - - # Calculate the profit - profit = self.calculate_profit(src_token, profit_src, self.CCm, cids) - - if str(profit) == "nan": - self.ConfigObj.logger.debug("profit is nan, skipping") - continue - - # Handle candidates based on conditions - candidates += self.handle_candidates( - best_profit, - profit, - trade_instructions_df, - trade_instructions_dic, - src_token, - trade_instructions, - ) - - # Find the best operations - best_profit, ops = self.find_best_operations( - best_profit, - ops, - profit, - trade_instructions_df, - trade_instructions_dic, - src_token, - trade_instructions, - ) - - return candidates if self.result == self.AO_CANDIDATES else ops - - def get_wrong_direction_cids( - self, tkn0_into_carbon: bool, trade_instructions_df: pd.DataFrame - ) -> List[Hashable]: - """ - Get the cids of the wrong direction curves - - Parameters - ---------- - tkn0_into_carbon : bool - True if tkn0 is being converted into carbon, False otherwise - trade_instructions_df : pd.DataFrame - The trade instructions dataframe - - Returns - ------- - List[str] - The cids of the wrong direction curves - """ - return [ - idx - for idx, row in trade_instructions_df.iterrows() - if ( - (tkn0_into_carbon and row[0] < 0) - or (not tkn0_into_carbon and row[0] > 0) - ) - and ("-0" in idx or "-1" in idx) - ] - - @staticmethod - def run_main_flow( - curves: List[Any], src_token: str, tkn0: str, tkn1: str - ) -> Tuple[Any, float, Any, pd.DataFrame]: - """ - Run main flow to find arbitrage. - """ - CC_cc = CPCContainer(curves) - O = PairOptimizer(CC_cc) - pstart = { - tkn0: CC_cc.bypairs(f"{tkn0}/{tkn1}")[0].p - } # this intentionally selects the non_carbon curve - r = O.optimize(src_token, params=dict(pstart=pstart)) - profit_src = -r.result - trade_instructions_df = r.trade_instructions(O.TIF_DFAGGR) - return O, profit_src, r, trade_instructions_df - - def process_wrong_direction_pools( - self, curve_combo: List[Any], wrong_direction_cids: List[Hashable] - ) -> [str]: - """ - Process curves with wrong direction pools. - """ - new_curves = [ - curve for curve in curve_combo if curve.cid not in wrong_direction_cids - ] - return new_curves - - def get_combos_pol(self, - CCm: CPCContainer, flashloan_tokens: List[str] - ) -> Tuple[List[Any], List[Any]]: + def get_combos(self, flashloan_tokens: List[str], CCm: Any) -> Tuple[List[str], List[Any]]: """ Get combos for pairwise arbitrage specific to Bancor POL Parameters ---------- - CCm : CPCContainer - Container for all the curves flashloan_tokens : list List of flashloan tokens + CCm : object + CCm object Returns ------- @@ -194,13 +35,32 @@ def get_combos_pol(self, """ bancor_pol_tkns = CCm.byparams(exchange="bancor_pol").tokens() - bancor_pol_tkns = set([tkn for tkn in bancor_pol_tkns if tkn not in [T.ETH, T.WETH]]) + bancor_pol_tkns = set([tkn for tkn in bancor_pol_tkns if tkn != self.ConfigObj.WETH_ADDRESS]) combos = [ (tkn0, tkn1) - for tkn0, tkn1 in itertools.product(bancor_pol_tkns, [T.ETH, T.WETH]) + for tkn0, tkn1 in product(bancor_pol_tkns, [self.ConfigObj.WETH_ADDRESS]) # tkn1 is always the token being flash loaned # note that the pair is tkn0/tkn1, ie tkn1 is the quote token if tkn0 != tkn1 ] - return bancor_pol_tkns, combos \ No newline at end of file + return bancor_pol_tkns, combos + + def get_curve_combos(self, CC: Any) -> List[Any]: + pol_curves = [x for x in CC.curves if x.params.exchange == "bancor_pol"] + carbon_curves = [x for x in CC.curves if x.params.exchange in self.ConfigObj.CARBON_V1_FORKS] + not_carbon_curves = [x for x in CC.curves if x.params.exchange not in ["bancor_pol"] + self.ConfigObj.CARBON_V1_FORKS] + curve_combos = [[curve] + pol_curves for curve in not_carbon_curves] + + if len(carbon_curves) > 0: + base_direction_pair = carbon_curves[0].pair + base_direction_one = [curve for curve in carbon_curves if curve.pair == base_direction_pair] + base_direction_two = [curve for curve in carbon_curves if curve.pair != base_direction_pair] + + if len(base_direction_one) > 0: + curve_combos += [[curve] + base_direction_one for curve in pol_curves] + + if len(base_direction_two) > 0: + curve_combos += [[curve] + base_direction_two for curve in pol_curves] + + return curve_combos diff --git a/fastlane_bot/modes/pairwise_single.py b/fastlane_bot/modes/pairwise_single.py deleted file mode 100644 index d5128c6b3..000000000 --- a/fastlane_bot/modes/pairwise_single.py +++ /dev/null @@ -1,111 +0,0 @@ -""" -Defines the Single pairwise arbitrage finder class - -[DOC-TODO-OPTIONAL-longer description in rst format] - ---- -(c) Copyright Bprotocol foundation 2023-24. -All rights reserved. -Licensed under MIT. -""" -from typing import List, Any, Tuple, Union - -from tqdm.contrib import itertools - -from fastlane_bot.modes.base_pairwise import ArbitrageFinderPairwiseBase -from fastlane_bot.tools.cpc import CPCContainer -from fastlane_bot.tools.optimizer import MargPOptimizer, PairOptimizer - - -class FindArbitrageSinglePairwise(ArbitrageFinderPairwiseBase): - """ - Single pairwise arbitrage finder mode - """ - - arb_mode = "single_pairwise" - - def find_arbitrage(self, candidates: List[Any] = None, ops: Tuple = None, best_profit: float = 0, profit_src: float = 0) -> Union[List, Tuple]: - """ - see base.py - """ - - if candidates is None: - candidates = [] - - all_tokens, combos = self.get_combos(self.CCm, self.flashloan_tokens) - - if self.result == self.AO_TOKENS: - return all_tokens, combos - - for tkn0, tkn1 in combos: - r = None - CC = self.CCm.bypairs(f"{tkn0}/{tkn1}") - if len(CC) < 2: - continue - base_exchange_curves = [ - x for x in CC.curves if x.params.exchange == self.base_exchange - ] - not_base_exchange_curves = [ - x for x in CC.curves if x.params.exchange != self.base_exchange - ] - self.ConfigObj.logger.debug( - f"base_exchange: {self.base_exchange}, base_exchange_curves: {len(base_exchange_curves)}, not_base_exchange_curves: {len(not_base_exchange_curves)}" - ) - - curve_combos = list( - itertools.product(not_base_exchange_curves, base_exchange_curves) - ) - - if not curve_combos: - continue - - for curve_combo in curve_combos: - CC_cc = CPCContainer(curve_combo) - O = PairOptimizer(CC_cc) - src_token = tkn1 - try: - pstart = {tkn0: CC_cc.bypairs(f"{tkn0}/{tkn1}")[0].p} - r = O.optimize(src_token, params=dict(pstart=pstart)) - profit_src = -r.result - trade_instructions_df = r.trade_instructions(O.TIF_DFAGGR) - trade_instructions_dic = r.trade_instructions(O.TIF_DICTS) - trade_instructions = r.trade_instructions() - except Exception as e: - print("[FindArbitrageSinglePairwise] Exception: ", e) - continue - if trade_instructions_dic is None: - continue - if len(trade_instructions_dic) < 2: - continue - # Get the candidate ids - cids = [ti["cid"] for ti in trade_instructions_dic] - - # Calculate the profit - profit = self.calculate_profit(src_token, profit_src, self.CCm, cids) - - if str(profit) == "nan": - self.ConfigObj.logger.debug("profit is nan, skipping") - continue - - # Handle candidates based on conditions - candidates += self.handle_candidates( - best_profit, - profit, - trade_instructions_df, - trade_instructions_dic, - src_token, - trade_instructions, - ) - - # Find the best operations - best_profit, ops = self.find_best_operations( - best_profit, - ops, - profit, - trade_instructions_df, - trade_instructions_dic, - src_token, - trade_instructions, - ) - - return candidates if self.result == self.AO_CANDIDATES else ops diff --git a/fastlane_bot/modes/triangle_bancor_v3_two_hop.py b/fastlane_bot/modes/triangle_bancor_v3_two_hop.py index cba6397b6..07b3e28f2 100644 --- a/fastlane_bot/modes/triangle_bancor_v3_two_hop.py +++ b/fastlane_bot/modes/triangle_bancor_v3_two_hop.py @@ -1,5 +1,5 @@ """ -Defines the Bancor V3 triangular arbitrage finder class +Defines the b3-two-hop-triangle arbitrage finder class [DOC-TODO-OPTIONAL-longer description in rst format] @@ -9,103 +9,90 @@ Licensed under MIT. """ import math -from typing import Union, List, Tuple, Any, Iterable +from typing import List, Tuple, Any +from itertools import product +from fastlane_bot.tools.cpc import CPCContainer, ConstantProductCurve from fastlane_bot.modes.base_triangle import ArbitrageFinderTriangleBase -from fastlane_bot.tools.cpc import CPCContainer, T, ConstantProductCurve -from fastlane_bot.tools.optimizer import MargPOptimizer - class ArbitrageFinderTriangleBancor3TwoHop(ArbitrageFinderTriangleBase): - """ - Bancor V3 triangular arbitrage finder mode - """ - arb_mode = "b3_two_hop" - def find_arbitrage(self, candidates: List[Any] = None, ops: Tuple = None, best_profit: float = 0, profit_src: float = 0) -> Union[List, Tuple]: - """ - see base.py - """ - if self.base_exchange != "bancor_v3": - self.ConfigObj.logger.warning( - f"base_exchange must be bancor_v3 for {self.arb_mode}, setting it to bancor_v3" - ) - self.base_exchange = "bancor_v3" - - self.ConfigObj.logger.info( - f"flashloan_tokens for arb_mode={self.arb_mode} will be overwritten. " - ) - - self._check_limit_flashloan_tokens_for_bancor3() - - if candidates is None: - candidates = [] - - # Get combinations of flashloan tokens - combos = self.get_combos( - self.flashloan_tokens, self.CCm, arb_mode=self.arb_mode - ) - - # Get the miniverse combinations - all_miniverses = self.get_miniverse_combos(combos) - - if len(all_miniverses) == 0: - return None - - # Check each source token and miniverse combination - for src_token, miniverse in all_miniverses: - r = None - - try: - # Run main flow with the new set of curves - ( - profit_src, - trade_instructions, - trade_instructions_df, - trade_instructions_dic, - ) = self.run_main_flow(miniverse, src_token) - - except Exception: + def handle_exchange(self): + assert self.base_exchange != "bancor_v3", "base_exchange for `b3_two_hop` mode must be `bancor_v3`" + fltkns = self.CCm.byparams(exchange="bancor_v3").tknys() + if self.ConfigObj.LIMIT_BANCOR3_FLASHLOAN_TOKENS: + # Filter out tokens that are not in the existing flashloan_tokens list + self.flashloan_tokens = [tkn for tkn in fltkns if tkn in self.flashloan_tokens] + self.ConfigObj.logger.info(f"limiting flashloan_tokens to {self.flashloan_tokens}") + else: + self.flashloan_tokens = fltkns + + def get_combos(self, flashloan_tokens: List[str], CCm: Any) -> Tuple[List[str], List[Any]]: + all_miniverses = [] + combos = [ + (tkn0, tkn1) + for tkn0, tkn1 in product(flashloan_tokens, flashloan_tokens) + # note that the pair is tkn0/tkn1, ie tkn1 is the quote token + if tkn0 != tkn1 + ] + for tkn0, tkn1 in combos: + external_curves = self.CCm.bypairs(f"{tkn0}/{tkn1}") + external_curves += self.CCm.bypairs(f"{tkn1}/{tkn0}") + external_curves = list(set(external_curves)) + carbon_curves = [ + curve + for curve in external_curves + if curve.params.get("exchange") in self.ConfigObj.CARBON_V1_FORKS + ] + external_curves = [ + curve + for curve in external_curves + if curve.params.get("exchange") not in self.ConfigObj.CARBON_V1_FORKS + ] + if not external_curves and not carbon_curves: continue - if trade_instructions_dic is None: + + bancor_v3_curve_0 = ( + self.CCm.bypairs(f"{self.ConfigObj.BNT_ADDRESS}/{tkn0}") + .byparams(exchange="bancor_v3") + .curves + ) + bancor_v3_curve_1 = ( + self.CCm.bypairs(f"{self.ConfigObj.BNT_ADDRESS}/{tkn1}") + .byparams(exchange="bancor_v3") + .curves + ) + if bancor_v3_curve_0 is None or bancor_v3_curve_1 is None: continue - if len(trade_instructions_dic) < 3: + if len(bancor_v3_curve_0) == 0 or len(bancor_v3_curve_1) == 0: continue - # Get the candidate ids - cids = [ti["cid"] for ti in trade_instructions_dic] - # Calculate the profit - profit = self.calculate_profit(src_token, profit_src, self.CCm, cids) + miniverses = [] + if len(external_curves) > 0: + for curve in external_curves: + miniverses += [bancor_v3_curve_0 + bancor_v3_curve_1 + [curve]] + if len(carbon_curves) > 0: - if str(profit) == "nan": - self.ConfigObj.logger.debug("profit is nan, skipping") - continue + if len(carbon_curves) > 0: + base_direction_pair = carbon_curves[0].pair + base_direction_one = [curve for curve in carbon_curves if curve.pair == base_direction_pair] + base_direction_two = [curve for curve in carbon_curves if curve.pair != base_direction_pair] - # Handle candidates based on conditions - candidates += self.handle_candidates( - best_profit, - profit, - trade_instructions_df, - trade_instructions_dic, - src_token, - trade_instructions, - ) + if len(base_direction_one) > 0: + miniverses += [bancor_v3_curve_0 + bancor_v3_curve_1 + base_direction_one] - # Find the best operations - best_profit, ops = self.find_best_operations( - best_profit, - ops, - profit, - trade_instructions_df, - trade_instructions_dic, - src_token, - trade_instructions, - ) + if len(base_direction_two) > 0: + miniverses += [bancor_v3_curve_0 + bancor_v3_curve_1 + base_direction_two] - return candidates if self.result == self.AO_CANDIDATES else ops + miniverses += [bancor_v3_curve_0 + bancor_v3_curve_1 + carbon_curves] + + if len(miniverses) > 0: + all_miniverses += list(zip([tkn1] * len(miniverses), miniverses)) + return all_miniverses - def get_tkn(self, pool: Any, tkn_num: int) -> str: + @staticmethod + def get_tkn(pool: Any, tkn_num: int) -> str: """ Gets the token ID from a pool object @@ -187,7 +174,8 @@ def get_optimal_arb_trade_amts(self, cids: List[str], flt: str) -> float: return self.max_arb_trade_in_constant_product(p0t0, p0t1, p1t0, p1t1, p2t0, p2t1, fee0=fee0, fee1=fee1, fee2=fee2) - def get_exact_input_with_carbon(self, p0t0: float, p0t1: float, p2t0: float, p2t1: float, carbon_pool: ConstantProductCurve) -> float: + @staticmethod + def get_exact_input_with_carbon(p0t0: float, p0t1: float, p2t0: float, p2t1: float, carbon_pool: ConstantProductCurve) -> float: """ Gets the optimal trade 0 amount for a triangular arb cycle with a single Carbon order in the middle @@ -209,28 +197,7 @@ def get_exact_input_with_carbon(self, p0t0: float, p0t1: float, p2t0: float, p2t B = carbon_pool.B C = (B * z + A * y) ** 2 D = B * A * z + A ** 2 * y - return self.max_arb_trade_in_cp_carbon_cp(p0t0, p0t1, p2t0, p2t1, C, D, z) - - @staticmethod - def max_arb_trade_in_cp_carbon_cp(p0t0: float, p0t1: float, p2t0: float, p2t1: float, C: float, D: float, z: float) -> float: - """ - Equation to solve optimal trade input for a constant product -> Carbon order -> constant product route. - Parameters - ---------- - p0t0: float - p0t1: float - p2t0: float - p2t1: float - C: float - D: float - z: float - Returns - ------- - float - - """ - trade_input = (z * (-p0t0 * p2t0 * z + math.sqrt(C * p0t0 * p2t0 * p0t1 * p2t1))) / (p0t1 * C + p0t1 * D * p2t0 + z ** 2 * p2t0) - return trade_input + return (z * (-p0t0 * p2t0 * z + math.sqrt(C * p0t0 * p2t0 * p0t1 * p2t1))) / (p0t1 * C + p0t1 * D * p2t0 + z ** 2 * p2t0) @staticmethod def max_arb_trade_in_constant_product(p0t0, p0t1, p1t0, p1t1, p2t0, p2t1, fee0, fee1, fee2): @@ -252,118 +219,4 @@ def max_arb_trade_in_constant_product(p0t0, p0t1, p1t0, p1t1, p2t0, p2t1, fee0, float """ - val = (-p1t0*p2t0*p0t0 + (p1t0*p2t0*p0t0*p1t1*p2t1*p0t1*(-fee1*fee2*fee0 + fee1*fee2 + fee1*fee0 - fee1 + fee2*fee0 - fee2 - fee0 + 1)) ** 0.5)/(p1t0*p2t0 - p2t0*p0t1*fee0 + p2t0*p0t1 + p1t1*p0t1*fee1*fee0 - p1t1*p0t1*fee1 - p1t1*p0t1*fee0 + p1t1*p0t1) - return val - - def run_main_flow(self, - miniverse: List, src_token: str - ) -> Tuple[float, Any, Any, Any]: - """ - Run the main flow of the arbitrage finder. - - Parameters - ---------- - miniverse : list - List of curves. - src_token : str - Source token. - - Returns - ------- - tuple - Tuple of profit, trade instructions, trade instructions dataframe and trade instructions dictionary. - - """ - - # Instantiate the container and optimizer objects - CC_cc = CPCContainer(miniverse) - O = MargPOptimizer(CC_cc) - pstart = self.build_pstart(CC_cc, CC_cc.tokens(), src_token) - # Perform the optimization - r = O.optimize(src_token, params=dict(pstart=pstart)) - - # Get the profit in the source token - profit_src = -r.result - - # Get trade instructions in different formats - trade_instructions_df = r.trade_instructions(O.TIF_DFAGGR) - trade_instructions_dic = r.trade_instructions(O.TIF_DICTS) - trade_instructions = r.trade_instructions() - - return ( - profit_src, - trade_instructions, - trade_instructions_df, - trade_instructions_dic, - ) - - def get_miniverse_combos(self, combos: Iterable) -> List[Tuple[str, List]]: - """ - Get the miniverse combinations for a list of token pairs. - - Parameters - ---------- - combos : list - List of token pairs. - - Returns - ------- - list - List of miniverse combinations. - - """ - all_miniverses = [] - for tkn0, tkn1 in combos: - external_curves = self.CCm.bypairs(f"{tkn0}/{tkn1}") - external_curves += self.CCm.bypairs(f"{tkn1}/{tkn0}") - external_curves = list(set(external_curves)) - carbon_curves = [ - curve - for curve in external_curves - if curve.params.get("exchange") in self.ConfigObj.CARBON_V1_FORKS - ] - external_curves = [ - curve - for curve in external_curves - if curve.params.get("exchange") not in self.ConfigObj.CARBON_V1_FORKS - ] - if not external_curves and not carbon_curves: - continue - - bancor_v3_curve_0 = ( - self.CCm.bypairs(f"{T.BNT}/{tkn0}") - .byparams(exchange="bancor_v3") - .curves - ) - bancor_v3_curve_1 = ( - self.CCm.bypairs(f"{T.BNT}/{tkn1}") - .byparams(exchange="bancor_v3") - .curves - ) - if bancor_v3_curve_0 is None or bancor_v3_curve_1 is None: - continue - if len(bancor_v3_curve_0) == 0 or len(bancor_v3_curve_1) == 0: - continue - - miniverses = [] - if len(external_curves) > 0: - for curve in external_curves: - miniverses += [bancor_v3_curve_0 + bancor_v3_curve_1 + [curve]] - if len(carbon_curves) > 0: - - if len(carbon_curves) > 0: - base_direction_pair = carbon_curves[0].pair - base_direction_one = [curve for curve in carbon_curves if curve.pair == base_direction_pair] - base_direction_two = [curve for curve in carbon_curves if curve.pair != base_direction_pair] - - if len(base_direction_one) > 0: - miniverses += [bancor_v3_curve_0 + bancor_v3_curve_1 + base_direction_one] - - if len(base_direction_two) > 0: - miniverses += [bancor_v3_curve_0 + bancor_v3_curve_1 + base_direction_two] - - miniverses += [bancor_v3_curve_0 + bancor_v3_curve_1 + carbon_curves] - - if len(miniverses) > 0: - all_miniverses += list(zip([tkn1] * len(miniverses), miniverses)) - return all_miniverses + return (-p1t0*p2t0*p0t0 + (p1t0*p2t0*p0t0*p1t1*p2t1*p0t1*(-fee1*fee2*fee0 + fee1*fee2 + fee1*fee0 - fee1 + fee2*fee0 - fee2 - fee0 + 1)) ** 0.5)/(p1t0*p2t0 - p2t0*p0t1*fee0 + p2t0*p0t1 + p1t1*p0t1*fee1*fee0 - p1t1*p0t1*fee1 - p1t1*p0t1*fee0 + p1t1*p0t1) diff --git a/fastlane_bot/modes/triangle_multi.py b/fastlane_bot/modes/triangle_multi.py index e6d1f6672..bb478111c 100644 --- a/fastlane_bot/modes/triangle_multi.py +++ b/fastlane_bot/modes/triangle_multi.py @@ -1,5 +1,5 @@ """ -Defines the Triangular arbitrage finder class +Defines the multi-triangle arbitrage finder class [DOC-TODO-OPTIONAL-longer description in rst format] @@ -8,79 +8,124 @@ All rights reserved. Licensed under MIT. """ -from typing import List, Any, Tuple, Union +from typing import List, Any, Tuple +from itertools import product from fastlane_bot.modes.base_triangle import ArbitrageFinderTriangleBase -from fastlane_bot.tools.cpc import CPCContainer -from fastlane_bot.tools.optimizer import MargPOptimizer - class ArbitrageFinderTriangleMulti(ArbitrageFinderTriangleBase): - """ - Triangular arbitrage finder mode - """ - arb_mode = "multi_triangle" - def find_arbitrage(self, candidates: List[Any] = None, ops: Tuple = None, best_profit: float = 0, profit_src: float = 0) -> Union[List, Tuple]: - """ - see base.py - """ + def handle_exchange(self): + assert self.base_exchange in self.ConfigObj.CARBON_V1_FORKS, "base_exchange for `multi_triangle` mode must be a carbon_v1 fork" - if self.base_exchange != "carbon_v1": - raise ValueError("base_exchange must be carbon_v1 for `multi` mode") + def get_combos(self, flashloan_tokens: List[str], CCm: Any) -> Tuple[List[str], List[Any]]: + """ + Get combos for triangular arbitrage - if candidates is None: - candidates = [] + Parameters + ---------- + flashloan_tokens : list + List of flashloan tokens + CCm : object + CCm object - combos = self.get_combos(self.flashloan_tokens, self.CCm, arb_mode=self.arb_mode) + Returns + ------- + combos : list + List of combos - for src_token, miniverse in combos: - try: - CC_cc = CPCContainer(miniverse) - O = MargPOptimizer(CC_cc) - pstart = self.build_pstart(CC_cc, CC_cc.tokens(), src_token) - r = O.optimize(src_token, params=dict(pstart=pstart)) - trade_instructions_dic = r.trade_instructions(O.TIF_DICTS) - if trade_instructions_dic is None or len(trade_instructions_dic) < 3: - # Failed to converge + """ + combos = [] + all_base_exchange_curves = CCm.byparams(exchange=self.base_exchange).curves + for flt in flashloan_tokens: # may wish to run this for one flt at a time + non_flt_base_exchange_curves = [ + x for x in all_base_exchange_curves if flt not in x.pair + ] + for non_flt_base_exchange_curve in non_flt_base_exchange_curves: + target_tkny = non_flt_base_exchange_curve.tkny + target_tknx = non_flt_base_exchange_curve.tknx + base_exchange_curves = ( + CCm.bypairs(f"{target_tknx}/{target_tkny}") + .byparams(exchange=self.base_exchange) + .curves + ) + if len(base_exchange_curves) == 0: continue - trade_instructions_df = r.trade_instructions(O.TIF_DFAGGR) - trade_instructions = r.trade_instructions() - except Exception as e: - self.ConfigObj.logger.info(f"[triangle multi] {e}") - continue - profit_src = -r.result - - # Get the cids - cids = [ti["cid"] for ti in trade_instructions_dic] - - # Calculate the profit - profit = self.calculate_profit(src_token, profit_src, self.CCm, cids) - if str(profit) == "nan": - self.ConfigObj.logger.debug("profit is nan, skipping") - continue - - # Handle candidates based on conditions - candidates += self.handle_candidates( - best_profit, - profit, - trade_instructions_df, - trade_instructions_dic, - src_token, - trade_instructions, - ) - - # Find the best operations - best_profit, ops = self.find_best_operations( - best_profit, - ops, - profit, - trade_instructions_df, - trade_instructions_dic, - src_token, - trade_instructions, - ) + base_direction_pair = base_exchange_curves[0].pair + base_direction_one = [curve for curve in base_exchange_curves if curve.pair == base_direction_pair] + base_direction_two = [curve for curve in base_exchange_curves if curve.pair != base_direction_pair] + + y_match_curves = CCm.bypairs( + set(CCm.filter_pairs(onein=target_tknx)) + & set(CCm.filter_pairs(onein=flt)) + ) + x_match_curves = CCm.bypairs( + set(CCm.filter_pairs(onein=target_tkny)) + & set(CCm.filter_pairs(onein=flt)) + ) + + y_match_curves_not_carbon = [ + x + for x in y_match_curves + if x.params.exchange != self.base_exchange + ] + if len(y_match_curves_not_carbon) == 0: + continue + x_match_curves_not_carbon = [ + x + for x in x_match_curves + if x.params.exchange != self.base_exchange + ] + if len(x_match_curves_not_carbon) == 0: + continue + if len(base_direction_one) > 0: + get_miniverse( + y_match_curves_not_carbon, + base_direction_one, + x_match_curves_not_carbon, + flt, + combos, + ) + if len(base_direction_two) > 0: + get_miniverse( + y_match_curves_not_carbon, + base_direction_two, + x_match_curves_not_carbon, + flt, + combos, + ) + return combos + +def get_miniverse( + y_match_curves_not_carbon: List[Any], + base_exchange_curves: List[Any], + x_match_curves_not_carbon: List[Any], + flt: str, + combos: List[Any], +): + """ + Get miniverse for triangular arbitrage + + Parameters + ---------- + y_match_curves_not_carbon : list + List of curves that match the y token and are not on carbon + base_exchange_curves : list + List of curves on the base exchange + x_match_curves_not_carbon : list + List of curves that match the x token and are not on carbon + flt : str + Flashloan token + combos : list + List of combos - return candidates if self.result == self.AO_CANDIDATES else ops + """ + external_curve_combos = list( + product(y_match_curves_not_carbon, x_match_curves_not_carbon) + ) + miniverses = [ + base_exchange_curves + list(combo) for combo in external_curve_combos + ] + combos += list(zip([flt] * len(miniverses), miniverses)) diff --git a/fastlane_bot/modes/triangle_multi_complete.py b/fastlane_bot/modes/triangle_multi_complete.py index d053ae0fc..6036a00b6 100644 --- a/fastlane_bot/modes/triangle_multi_complete.py +++ b/fastlane_bot/modes/triangle_multi_complete.py @@ -1,5 +1,5 @@ """ -Defines the Triangular arbitrage finder class +Defines the multi-triangle-complete arbitrage finder class [DOC-TODO-OPTIONAL-longer description in rst format] @@ -8,76 +8,135 @@ All rights reserved. Licensed under MIT. """ -from typing import List, Any, Tuple, Union +from typing import List, Any, Tuple +from itertools import product, combinations from fastlane_bot.modes.base_triangle import ArbitrageFinderTriangleBase -from fastlane_bot.tools.cpc import CPCContainer -from fastlane_bot.tools.optimizer import MargPOptimizer - class ArbitrageFinderTriangleMultiComplete(ArbitrageFinderTriangleBase): - """ - Triangular arbitrage finder mode - """ - arb_mode = "multi_triangle_complete" - def find_arbitrage(self, candidates: List[Any] = None, ops: Tuple = None, best_profit: float = 0, profit_src: float = 0) -> Union[List, Tuple]: - """ - see base.py + def handle_exchange(self): + pass + + def get_combos(self, flashloan_tokens: List[str], CCm: Any) -> Tuple[List[str], List[Any]]: """ + Get comprehensive combos for triangular arbitrage + + Parameters + ---------- + flashloan_tokens : list + List of flashloan tokens + CCm : object + CCm object + arb_mode : str + Arbitrage mode (unused) + + Returns + ------- + combos : list + List of combos - if candidates is None: - candidates = [] - - combos = self.get_comprehensive_triangles(self.flashloan_tokens, self.CCm) - - for src_token, miniverse in combos: - try: - CC_cc = CPCContainer(miniverse) - O = MargPOptimizer(CC_cc) - pstart = self.build_pstart(CC_cc, CC_cc.tokens(), src_token) - r = O.optimize(src_token, params=dict(pstart=pstart)) - trade_instructions_dic = r.trade_instructions(O.TIF_DICTS) - if trade_instructions_dic is None or len(trade_instructions_dic) < 3: - # Failed to converge - continue - trade_instructions_df = r.trade_instructions(O.TIF_DFAGGR) - trade_instructions = r.trade_instructions() - - except Exception as e: - self.ConfigObj.logger.info(f"[triangle multi] {e}") - continue - profit_src = -r.result - - # Get the cids - cids = [ti["cid"] for ti in trade_instructions_dic] - - # Calculate the profit - profit = self.calculate_profit(src_token, profit_src, self.CCm, cids) - if str(profit) == "nan": - self.ConfigObj.logger.debug("profit is nan, skipping") - continue - - # Handle candidates based on conditions - candidates += self.handle_candidates( - best_profit, - profit, - trade_instructions_df, - trade_instructions_dic, - src_token, - trade_instructions, - ) - - # Find the best operations - best_profit, ops = self.find_best_operations( - best_profit, - ops, - profit, - trade_instructions_df, - trade_instructions_dic, - src_token, - trade_instructions, - ) - - return candidates if self.result == self.AO_CANDIDATES else ops + """ + combos = [] + for flt in flashloan_tokens: + + # Get the Carbon pairs + carbon_pairs = sort_pairs(set([x.pair for x in CCm.curves if x.params.exchange in self.ConfigObj.CARBON_V1_FORKS])) + + # Create a set of unique tokens, excluding 'flt' + x_tokens = {token for pair in carbon_pairs for token in pair.split('/') if token != flt} + + # Get relevant pairs containing the flashloan token + flt_x_pairs = sort_pairs([f"{x}/{flt}" for x in x_tokens]) + + # Generate all possible 2-item combinations from the unique tokens that arent the flashloan token + x_y_pairs = sort_pairs(["{}/{}".format(x, y) for x, y in combinations(x_tokens, 2)]) + + # Note the relevant pairs + all_relevant_pairs = flt_x_pairs + x_y_pairs + self.ConfigObj.logger.debug(f"len(all_relevant_pairs) {len(all_relevant_pairs)}") + + # Generate triangle groups + triangle_groups = get_triangle_groups(flt, x_y_pairs) + self.ConfigObj.logger.debug(f"len(triangle_groups) {len(triangle_groups)}") + + # Get pair info for the cohort + all_relevant_pairs_info = get_all_relevant_pairs_info(CCm, all_relevant_pairs, self.ConfigObj.CARBON_V1_FORKS) + + # Generate valid triangles for the groups base on arb_mode + valid_triangles = get_triangle_groups_stats(triangle_groups, all_relevant_pairs_info) + + # Get [(flt,curves)] analysis set for the flt + flt_triangle_analysis_set = get_analysis_set_per_flt(flt, valid_triangles, all_relevant_pairs_info) + + # The entire analysis set for all flts + combos.extend(flt_triangle_analysis_set) + return combos + +def sort_pairs(pairs): + # Clean up the pairs alphabetically + return ["/".join(sorted(pair.split('/'))) for pair in pairs] + +def flatten_nested_items_in_list(nested_list): + # unpack nested items + flattened_list = [] + for items in nested_list: + flat_list = [] + for item in items: + if isinstance(item, list): + flat_list.extend(item) + else: + flat_list.append(item) + flattened_list.append(flat_list) + return flattened_list + +def get_triangle_groups(flt, x_y_pairs): + # Get groups of triangles that conform to (flt/x , x/y, y/flt) where x!=y + triangle_groups = [] + for pair in x_y_pairs: + x,y = pair.split('/') + triangle_groups += [("/".join(sorted([flt,x])), pair, "/".join(sorted([flt,y])))] + return triangle_groups + +def get_all_relevant_pairs_info(CCm, all_relevant_pairs, carbon_v1_forks): + # Get pair info for the cohort to allow decision making at the triangle level + all_relevant_pairs_info = {} + for pair in all_relevant_pairs: + all_relevant_pairs_info[pair] = {} + pair_curves = CCm.bypair(pair) + carbon_curves = [] + non_carbon_curves = [] + for x in pair_curves: + if x.params.exchange in carbon_v1_forks: + carbon_curves += [x] + else: + non_carbon_curves += [x] + all_relevant_pairs_info[pair]['curves'] = non_carbon_curves + [carbon_curves] if len(carbon_curves) > 0 else non_carbon_curves # condense carbon curves into a single list + all_relevant_pairs_info[pair]['all_counts'] = len(pair_curves) + all_relevant_pairs_info[pair]['carbon_counts'] = len(carbon_curves) + return all_relevant_pairs_info + +def get_triangle_groups_stats(triangle_groups, all_relevant_pairs_info): + # Get the stats on the triangle group cohort for decision making + valid_carbon_triangles = [] + for triangle in triangle_groups: + path_len = 0 + has_carbon = False + for pair in triangle: + if all_relevant_pairs_info[pair]['all_counts'] > 0: + path_len += 1 + if all_relevant_pairs_info[pair]['carbon_counts'] > 0: + has_carbon = True + if path_len == 3 and has_carbon == True: + valid_carbon_triangles.append(triangle) + return valid_carbon_triangles + +def get_analysis_set_per_flt(flt, valid_triangles, all_relevant_pairs_info): + flt_triangle_analysis_set = [] + for triangle in valid_triangles: + multiverse = [all_relevant_pairs_info[pair]['curves'] for pair in triangle] + product_of_triangle = list(product(multiverse[0], multiverse[1], multiverse[2])) + triangles_to_run = flatten_nested_items_in_list(product_of_triangle) + flt_triangle_analysis_set += list(zip([flt] * len(triangles_to_run), triangles_to_run)) + return flt_triangle_analysis_set diff --git a/fastlane_bot/modes/triangle_single.py b/fastlane_bot/modes/triangle_single.py deleted file mode 100644 index 3c3b3f825..000000000 --- a/fastlane_bot/modes/triangle_single.py +++ /dev/null @@ -1,96 +0,0 @@ -""" -Defines the Triangle single arbitrage finder class - -[DOC-TODO-OPTIONAL-longer description in rst format] - ---- -(c) Copyright Bprotocol foundation 2023-24. -All rights reserved. -Licensed under MIT. -""" -from typing import Union, List, Tuple, Any - -from fastlane_bot.modes.base_triangle import ArbitrageFinderTriangleBase -from fastlane_bot.tools.cpc import CPCContainer -from fastlane_bot.tools.optimizer import MargPOptimizer - - -class ArbitrageFinderTriangleSingle(ArbitrageFinderTriangleBase): - """ - Triangle single arbitrage finder mode - """ - - arb_mode = "single_triangle" - - def find_arbitrage(self, candidates: List[Any] = None, ops: Tuple = None, best_profit: float = 0, profit_src: float = 0) -> Union[List, Tuple]: - """ - see base.py - """ - - if candidates is None: - candidates = [] - - # Get combinations of flashloan tokens - combos = self.get_combos( - self.flashloan_tokens, self.CCm, arb_mode=self.arb_mode - ) - - # Check each source token and miniverse combination - for src_token, miniverse in combos: - r = None - - # Instantiate the container and optimizer objects - CC_cc = CPCContainer(miniverse) - O = MargPOptimizer(CC_cc) - - try: - # Perform the optimization - r = O.margp_optimizer(src_token) - - # Get the profit in the source token - profit_src = -r.result - - # Get trade instructions in different formats - trade_instructions_df = r.trade_instructions(O.TIF_DFAGGR) - trade_instructions_dic = r.trade_instructions(O.TIF_DICTS) - trade_instructions = r.trade_instructions() - except Exception: - continue - - if trade_instructions_dic is None: - continue - if len(trade_instructions_dic) < 3: - continue - - # Get the candidate ids - cids = [ti["cid"] for ti in trade_instructions_dic] - - # Calculate the profit - profit = self.calculate_profit(src_token, profit_src, self.CCm, cids) - - if str(profit) == "nan": - self.ConfigObj.logger.debug("profit is nan, skipping") - continue - - # Handle candidates based on conditions - candidates += self.handle_candidates( - best_profit, - profit, - trade_instructions_df, - trade_instructions_dic, - src_token, - trade_instructions, - ) - - # Find the best operations - best_profit, ops = self.find_best_operations( - best_profit, - ops, - profit, - trade_instructions_df, - trade_instructions_dic, - src_token, - trade_instructions, - ) - - return candidates if self.result == self.AO_CANDIDATES else ops diff --git a/fastlane_bot/pool_finder.py b/fastlane_bot/pool_finder.py index 15ff0584f..f9fb7ca99 100644 --- a/fastlane_bot/pool_finder.py +++ b/fastlane_bot/pool_finder.py @@ -69,7 +69,7 @@ def get_pools_for_unsupported_pairs(self, pools: List[Dict[str, Any]], arb_mode: if not carbon_pairs: return [], [], [] self.extract_univ3_fee_tiers(pools) # TODO: these should be configured per exchange - if arb_mode in ["triangle", "multi_triangle"]: + if arb_mode in ["multi_triangle", "multi_triangle_complete", "b3_two_hop"]: unsupported_pairs = PoolFinder._find_unsupported_triangles(self._flashloan_tokens, carbon_pairs=carbon_pairs, external_pairs=other_pairs) else: unsupported_pairs = PoolFinder._find_unsupported_pairs(self._flashloan_tokens, carbon_pairs=carbon_pairs, external_pairs=other_pairs) diff --git a/fastlane_bot/tests/test_039_TestMultiMode.py b/fastlane_bot/tests/test_039_TestMultiMode.py deleted file mode 100644 index b3c875d70..000000000 --- a/fastlane_bot/tests/test_039_TestMultiMode.py +++ /dev/null @@ -1,251 +0,0 @@ -# ------------------------------------------------------------ -# Auto generated test file `test_039_TestMultiMode.py` -# ------------------------------------------------------------ -# source file = NBTest_039_TestMultiMode.py -# test id = 039 -# test comment = TestMultiMode -# ------------------------------------------------------------ - - - -""" -This module contains the tests for the exchanges classes -""" -from fastlane_bot import Bot, Config -from fastlane_bot.bot import CarbonBot -from fastlane_bot.tools.cpc import ConstantProductCurve as CPC -from fastlane_bot.events.exchanges import UniswapV2, UniswapV3, CarbonV1, BancorV3 -from fastlane_bot.events.interface import QueryInterface -from fastlane_bot.events.managers.manager import Manager -from fastlane_bot.events.interface import QueryInterface -from joblib import Parallel, delayed -import math -import json -print("{0.__name__} v{0.__VERSION__} ({0.__DATE__})".format(CPC)) -print("{0.__name__} v{0.__VERSION__} ({0.__DATE__})".format(Bot)) -print("{0.__name__} v{0.__VERSION__} ({0.__DATE__})".format(UniswapV2)) -print("{0.__name__} v{0.__VERSION__} ({0.__DATE__})".format(UniswapV3)) -print("{0.__name__} v{0.__VERSION__} ({0.__DATE__})".format(CarbonV1)) -print("{0.__name__} v{0.__VERSION__} ({0.__DATE__})".format(BancorV3)) -from fastlane_bot.testing import * -#plt.style.use('seaborn-dark') -plt.rcParams['figure.figsize'] = [12,6] -from fastlane_bot import __VERSION__ -require("3.0", __VERSION__) - - - -C = cfg = Config.new(config=Config.CONFIG_MAINNET) -cfg.DEFAULT_MIN_PROFIT_GAS_TOKEN = 0.00001 -assert (C.NETWORK == C.NETWORK_MAINNET) -assert (C.PROVIDER == C.PROVIDER_ALCHEMY) -setup_bot = CarbonBot(ConfigObj=C) -pools = None -with open('fastlane_bot/tests/_data/latest_pool_data_testing.json') as f: - pools = json.load(f) -pools = [pool for pool in pools] -pools[0] -static_pools = pools -state = pools -exchanges = list({ex['exchange_name'] for ex in state}) -db = QueryInterface(state=state, ConfigObj=C, exchanges=exchanges) -setup_bot.db = db - -static_pool_data_filename = "static_pool_data" - -static_pool_data = pd.read_csv(f"fastlane_bot/data/{static_pool_data_filename}.csv", low_memory=False) - -uniswap_v2_event_mappings = pd.read_csv("fastlane_bot/data/uniswap_v2_event_mappings.csv", low_memory=False) - -tokens = pd.read_csv("fastlane_bot/data/tokens.csv", low_memory=False) - -exchanges = "carbon_v1,bancor_v3,uniswap_v3,uniswap_v2,sushiswap_v2" - -exchanges = exchanges.split(",") - - -alchemy_max_block_fetch = 20 -static_pool_data["cid"] = [ - cfg.w3.keccak(text=f"{row['descr']}").hex() - for index, row in static_pool_data.iterrows() - ] -static_pool_data = [ - row for index, row in static_pool_data.iterrows() - if row["exchange_name"] in exchanges -] - -static_pool_data = pd.DataFrame(static_pool_data) -static_pool_data['exchange_name'].unique() -mgr = Manager( - web3=cfg.w3, - w3_async=cfg.w3_async, - cfg=cfg, - pool_data=static_pool_data.to_dict(orient="records"), - SUPPORTED_EXCHANGES=exchanges, - alchemy_max_block_fetch=alchemy_max_block_fetch, - uniswap_v2_event_mappings=uniswap_v2_event_mappings, - tokens=tokens.to_dict(orient="records"), -) - -start_time = time.time() -Parallel(n_jobs=-1, backend="threading")( - delayed(mgr.add_pool_to_exchange)(row) for row in mgr.pool_data -) -cfg.logger.info(f"Time taken to add initial pools: {time.time() - start_time}") - -mgr.deduplicate_pool_data() -cids = [pool["cid"] for pool in mgr.pool_data] -assert len(cids) == len(set(cids)), "duplicate cid's exist in the pool data" -def init_bot(mgr: Manager) -> CarbonBot: - """ - Initializes the bot. - - Parameters - ---------- - mgr : Manager - The manager object. - - Returns - ------- - CarbonBot - The bot object. - """ - mgr.cfg.logger.info("Initializing the bot...") - bot = CarbonBot(ConfigObj=mgr.cfg) - bot.db = db - bot.db.mgr = mgr - assert isinstance( - bot.db, QueryInterface - ), "QueryInterface not initialized correctly" - return bot -bot = init_bot(mgr) -bot.db.remove_unmapped_uniswap_v2_pools() -bot.db.remove_zero_liquidity_pools() -bot.db.remove_unsupported_exchanges() -tokens = bot.db.get_tokens() -ADDRDEC = {t.address: (t.address, int(t.decimals)) for t in tokens if not math.isnan(t.decimals)} -flashloan_tokens = bot.RUN_FLASHLOAN_TOKENS -CCm = bot.get_curves() -pools = db.get_pool_data_with_tokens() - -arb_mode = "multi" - - -# ------------------------------------------------------------ -# Test 039 -# File test_039_TestMultiMode.py -# Segment Test_TAX_TOKENS -# ------------------------------------------------------------ -def test_test_tax_tokens(): -# ------------------------------------------------------------ - - assert any(token.address in cfg.TAX_TOKENS for token in tokens), f"[TestMultiMode], DB does not include any tax tokens" - assert len(CCm) == 516, f"[NBTest 039 TestMultiMode] Expected 516 curves, found {len(CCm)}" - - for curve in CCm: - for token in cfg.TAX_TOKENS: - assert token not in [curve.params['tknx_addr'], curve.params['tkny_addr']], f"[TestMultiMode], curve {curve} includes tax token {token}" - - -# ------------------------------------------------------------ -# Test 039 -# File test_039_TestMultiMode.py -# Segment Test_MIN_PROFIT -# ------------------------------------------------------------ -def test_test_min_profit(): -# ------------------------------------------------------------ - - assert(cfg.DEFAULT_MIN_PROFIT_GAS_TOKEN <= 0.0001), f"[TestMultiMode], default_min_profit_gas_token must be <= 0.02 for this Notebook to run, currently set to {cfg.DEFAULT_MIN_PROFIT_GAS_TOKEN}" - - -# ------------------------------------------------------------ -# Test 039 -# File test_039_TestMultiMode.py -# Segment Test_get_arb_finder -# ------------------------------------------------------------ -def test_test_get_arb_finder(): -# ------------------------------------------------------------ - - arb_finder = bot._get_arb_finder("multi") - assert arb_finder.__name__ == "FindArbitrageMultiPairwise", f"[TestMultiMode] Expected arb_finder class name name = FindArbitrageMultiPairwise, found {arb_finder.__name__}" - - -# ------------------------------------------------------------ -# Test 039 -# File test_039_TestMultiMode.py -# Segment Test_Combos_and_Tokens -# ------------------------------------------------------------ -def test_test_combos_and_tokens(): -# ------------------------------------------------------------ - - # + - assert len(CCm) == 516, f"[NBTest 039 TestMultiMode] Expected 516 curves, found {len(CCm)}" - arb_finder = bot._get_arb_finder("multi") - finder = arb_finder( - flashloan_tokens=flashloan_tokens, - CCm=CCm, - mode="bothin", - result=arb_finder.AO_TOKENS, - ConfigObj=bot.ConfigObj, - ) - all_tokens, combos = finder.find_arbitrage() - - # subjected to the length of `TAX_TOKENS` - assert type(all_tokens) == set, f"[NBTest 039 TestMultiMode] all_tokens is wrong data type. Expected set, found: {type(all_tokens)}" - assert type(combos) == list, f"[NBTest 039 TestMultiMode] combos is wrong data type. Expected list, found: {type(combos)}" - assert len(all_tokens) >= 234, f"[NBTest 039 TestMultiMode] Using wrong dataset, expected at least 234 tokens, found {len(all_tokens)}" - assert len(combos) >= 1398, f"[NBTest 039 TestMultiMode] Using wrong dataset, expected at least 1398 combos, found {len(combos)}" - # - - - -# ------------------------------------------------------------ -# Test 039 -# File test_039_TestMultiMode.py -# Segment Test_Expected_Output -# ------------------------------------------------------------ -def test_test_expected_output(): -# ------------------------------------------------------------ - - # + - assert len(CCm) == 516, f"[NBTest 039 TestMultiMode] Expected 516 curves, found {len(CCm)}" - arb_finder = bot._get_arb_finder("multi") - finder = arb_finder( - flashloan_tokens=flashloan_tokens, - CCm=CCm, - mode="bothin", - result=arb_finder.AO_CANDIDATES, - ConfigObj=bot.ConfigObj, - ) - - r = finder.find_arbitrage() - - multi_carbon_count = 0 - carbon_wrong_direction_count = 0 - for arb in r: - ( - best_profit, - best_trade_instructions_df, - best_trade_instructions_dic, - best_src_token, - best_trade_instructions, - ) = arb - if len(best_trade_instructions_dic) > 2: - multi_carbon_count += 1 - carbon_tkn_in = None - for trade in best_trade_instructions_dic: - if "-" in trade["cid"]: - if carbon_tkn_in is None: - carbon_tkn_in = trade["tknin"] - else: - if trade["tknin"] not in carbon_tkn_in: - carbon_wrong_direction_count += 1 - for ti in best_trade_instructions_dic: - for token in cfg.TAX_TOKENS: - assert token not in [ti['tknin'], ti['tknout']], f"[TestMultiMode], trade instruction {ti} includes tax token {token}" - - assert len(r) >= 27, f"[NBTest 039 TestMultiMode] Expected at least 27 arbs, found {len(r)}" - assert multi_carbon_count > 0, f"[NBTest 039 TestMultiMode] Not finding arbs with multiple Carbon curves." - assert carbon_wrong_direction_count == 0, f"[NBTest 039 TestMultiMode] Expected all Carbon curves to have the same tkn in and tkn out. Mixing is currently not supported." - # - - - \ No newline at end of file diff --git a/fastlane_bot/tests/test_040_TestSingleMode.py b/fastlane_bot/tests/test_040_TestSingleMode.py deleted file mode 100644 index c7a1ee890..000000000 --- a/fastlane_bot/tests/test_040_TestSingleMode.py +++ /dev/null @@ -1,197 +0,0 @@ -# ------------------------------------------------------------ -# Auto generated test file `test_040_TestSingleMode.py` -# ------------------------------------------------------------ -# source file = NBTest_040_TestSingleMode.py -# test id = 040 -# test comment = TestSingleMode -# ------------------------------------------------------------ - - - -""" -This module contains the tests for the exchanges classes -""" -from fastlane_bot import Bot, Config -from fastlane_bot.bot import CarbonBot -from fastlane_bot.tools.cpc import ConstantProductCurve as CPC -from fastlane_bot.events.exchanges import UniswapV2, UniswapV3, CarbonV1, BancorV3 -from fastlane_bot.events.interface import QueryInterface -from fastlane_bot.events.managers.manager import Manager -from fastlane_bot.events.interface import QueryInterface -from joblib import Parallel, delayed -import math -import json -print("{0.__name__} v{0.__VERSION__} ({0.__DATE__})".format(CPC)) -print("{0.__name__} v{0.__VERSION__} ({0.__DATE__})".format(Bot)) -print("{0.__name__} v{0.__VERSION__} ({0.__DATE__})".format(UniswapV2)) -print("{0.__name__} v{0.__VERSION__} ({0.__DATE__})".format(UniswapV3)) -print("{0.__name__} v{0.__VERSION__} ({0.__DATE__})".format(CarbonV1)) -print("{0.__name__} v{0.__VERSION__} ({0.__DATE__})".format(BancorV3)) -from fastlane_bot.testing import * - -#plt.style.use('seaborn-dark') -plt.rcParams['figure.figsize'] = [12,6] -from fastlane_bot import __VERSION__ -require("3.0", __VERSION__) - - - -C = cfg = Config.new(config=Config.CONFIG_MAINNET) -cfg.DEFAULT_MIN_PROFIT_GAS_TOKEN = 0.00001 -assert (C.NETWORK == C.NETWORK_MAINNET) -assert (C.PROVIDER == C.PROVIDER_ALCHEMY) -setup_bot = CarbonBot(ConfigObj=C) -pools = None -with open('fastlane_bot/tests/_data/latest_pool_data_testing.json') as f: - pools = json.load(f) -pools = [pool for pool in pools] -pools[0] -static_pools = pools -state = pools -exchanges = list({ex['exchange_name'] for ex in state}) -db = QueryInterface(state=state, ConfigObj=C, exchanges=exchanges) -setup_bot.db = db - -static_pool_data_filename = "static_pool_data" - -static_pool_data = pd.read_csv(f"fastlane_bot/data/{static_pool_data_filename}.csv", low_memory=False) - -uniswap_v2_event_mappings = pd.read_csv("fastlane_bot/data/uniswap_v2_event_mappings.csv", low_memory=False) - -tokens = pd.read_csv("fastlane_bot/data/tokens.csv", low_memory=False) - -exchanges = "carbon_v1,bancor_v3,uniswap_v3,uniswap_v2,sushiswap_v2" - -exchanges = exchanges.split(",") - - -alchemy_max_block_fetch = 20 -static_pool_data["cid"] = [ - cfg.w3.keccak(text=f"{row['descr']}").hex() - for index, row in static_pool_data.iterrows() - ] -static_pool_data = [ - row for index, row in static_pool_data.iterrows() - if row["exchange_name"] in exchanges -] - -static_pool_data = pd.DataFrame(static_pool_data) -static_pool_data['exchange_name'].unique() -mgr = Manager( - web3=cfg.w3, - w3_async=cfg.w3_async, - cfg=cfg, - pool_data=static_pool_data.to_dict(orient="records"), - SUPPORTED_EXCHANGES=exchanges, - alchemy_max_block_fetch=alchemy_max_block_fetch, - uniswap_v2_event_mappings=uniswap_v2_event_mappings, - tokens=tokens.to_dict(orient="records"), -) - -start_time = time.time() -Parallel(n_jobs=-1, backend="threading")( - delayed(mgr.add_pool_to_exchange)(row) for row in mgr.pool_data -) -cfg.logger.info(f"Time taken to add initial pools: {time.time() - start_time}") - -mgr.deduplicate_pool_data() -cids = [pool["cid"] for pool in mgr.pool_data] -assert len(cids) == len(set(cids)), "duplicate cid's exist in the pool data" -def init_bot(mgr: Manager) -> CarbonBot: - """ - Initializes the bot. - - Parameters - ---------- - mgr : Manager - The manager object. - - Returns - ------- - CarbonBot - The bot object. - """ - mgr.cfg.logger.info("Initializing the bot...") - bot = CarbonBot(ConfigObj=mgr.cfg) - bot.db = db - bot.db.mgr = mgr - assert isinstance( - bot.db, QueryInterface - ), "QueryInterface not initialized correctly" - return bot -bot = init_bot(mgr) -bot.db.remove_unmapped_uniswap_v2_pools() -bot.db.remove_zero_liquidity_pools() -bot.db.remove_unsupported_exchanges() -tokens = bot.db.get_tokens() -ADDRDEC = {t.address: (t.address, int(t.decimals)) for t in tokens if not math.isnan(t.decimals)} -flashloan_tokens = bot.RUN_FLASHLOAN_TOKENS -CCm = bot.get_curves() -pools = db.get_pool_data_with_tokens() - -arb_mode = "single" - -assert(cfg.DEFAULT_MIN_PROFIT_GAS_TOKEN <= 0.0001), f"[TestSingleMode], default_min_profit_gas_token must be <= 0.02 for this Notebook to run, currently set to {cfg.DEFAULT_MIN_PROFIT_GAS_TOKEN}" - - - - - -# ------------------------------------------------------------ -# Test 040 -# File test_040_TestSingleMode.py -# Segment Test_arb_mode_class -# ------------------------------------------------------------ -def test_test_arb_mode_class(): -# ------------------------------------------------------------ - - arb_finder = bot._get_arb_finder("single") - assert arb_finder.__name__ == "FindArbitrageSinglePairwise", f"[TestSingleMode] Expected arb_finder class name name = FindArbitrageSinglePairwise, found {arb_finder.__name__}" - - -# ------------------------------------------------------------ -# Test 040 -# File test_040_TestSingleMode.py -# Segment Test_tokens_and_combos -# ------------------------------------------------------------ -def test_test_tokens_and_combos(): -# ------------------------------------------------------------ - - # + - arb_finder = bot._get_arb_finder("single") - finder = arb_finder( - flashloan_tokens=flashloan_tokens, - CCm=CCm, - mode="bothin", - result=arb_finder.AO_TOKENS, - ConfigObj=bot.ConfigObj, - ) - all_tokens, combos = finder.find_arbitrage() - - assert type(all_tokens) == set, f"[TestSingleMode] all_tokens is wrong data type. Expected set, found: {type(all_tokens)}" - assert type(combos) == list, f"[TestSingleMode] combos is wrong data type. Expected list, found: {type(combos)}" - assert len(all_tokens) > 100, f"[TestSingleMode] Using wrong dataset, expected at least 100 tokens, found {len(all_tokens)}" - assert len(combos) > 1000, f"[TestSingleMode] Using wrong dataset, expected at least 100 combos, found {len(combos)}" - - arb_finder = bot._get_arb_finder("single") - finder = arb_finder( - flashloan_tokens=flashloan_tokens, - CCm=CCm, - mode="bothin", - result=arb_finder.AO_CANDIDATES, - ConfigObj=bot.ConfigObj, - ) - r = finder.find_arbitrage() - - for arb in r: - ( - best_profit, - best_trade_instructions_df, - best_trade_instructions_dic, - best_src_token, - best_trade_instructions, - ) = arb - assert len(best_trade_instructions_dic) <= 2, "[TestSingleMode] Expected arbs without multiple Carbon curves" - - assert len(r) >= 20, f"[TestSingleMode] Expected at least 20 arbs, found {len(r)}" - # - diff --git a/fastlane_bot/tests/test_042_TestBancorV3ModeTwoHop.py b/fastlane_bot/tests/test_042_TestBancorV3ModeTwoHop.py index b94c8b0dc..66ef673ca 100644 --- a/fastlane_bot/tests/test_042_TestBancorV3ModeTwoHop.py +++ b/fastlane_bot/tests/test_042_TestBancorV3ModeTwoHop.py @@ -130,8 +130,6 @@ def init_bot(mgr: Manager) -> CarbonBot: CCm = bot.get_curves() pools = db.get_pool_data_with_tokens() -arb_mode = "b3_two_hop" - # ------------------------------------------------------------ # Test 042 @@ -366,32 +364,9 @@ def test_test_get_fee_safe(): # ------------------------------------------------------------ # Test 042 # File test_042_TestBancorV3ModeTwoHop.py -# Segment Test_combos -# ------------------------------------------------------------ -def test_test_combos(): -# ------------------------------------------------------------ - - arb_finder = bot._get_arb_finder("b3_two_hop") - finder = arb_finder( - flashloan_tokens=flashloan_tokens, - CCm=CCm, - mode="bothin", - result=False, - ConfigObj=bot.ConfigObj, - ) - #test_2_pools = [ConstantProductCurve(k=2921921249910.464, x=2760126.9934445512, x_act=2760126.9934445512, y_act=1058618.410258, pair='BNT-FF1C/USDC-eB48', cid='0xc4771395e1389e2e3a12ec22efbb7aff5b1c04e5ce9c7596a82e9dc8fdec725b', fee=0.0, descr='bancor_v3 BNT-FF1C/USDC-eB48 0.000', constr='uv2', params={'exchange': 'bancor_v3', 'tknx_dec': 18, 'tkny_dec': 6, 'tknx_addr': '0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C', 'tkny_addr': '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 'blocklud': 17713739}), ConstantProductCurve(k=518129588.60853314, x=6351922.348885405, x_act=6351922.348885405, y_act=81.57051679, pair='BNT-FF1C/WBTC-C599', cid='0x3885d978c125e66686e3f678ab64d5b09e61f89bf6e87c9ff66e740fd06aeefa', fee=0.0, descr='bancor_v3 BNT-FF1C/WBTC-C599 0.000', constr='uv2', params={'exchange': 'bancor_v3', 'tknx_dec': 18, 'tkny_dec': 8, 'tknx_addr': '0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C', 'tkny_addr': '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', 'blocklud': 17713739}), ConstantProductCurve(k=787603837541.6204, x=5107.692365701484, x_act=4.159867948255851, y_act=336571.44633978605, pair='WBTC-C599/USDC-eB48', cid='0x49ed97db2c080b7eac91dfaa7d51d5e8ac34c4dcfbcd3e8f2ed326a2a527b959', fee=0.003, descr='uniswap_v3 WBTC-C599/USDC-eB48 3000', constr='pkpp', params={'exchange': 'uniswap_v3', 'tknx_dec': 8, 'tkny_dec': 6, 'tknx_addr': '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599', 'tkny_addr': '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', 'blocklud': 17713395, 'L': 887470.4713632})] - flt = {'MKR-79A2', 'TRAC-0A6F', 'MONA-412A', 'WBTC-C599', 'WOO-5D4B', 'MATIC-eBB0', 'BAT-87EF', 'UOS-5C8c', 'LRC-EafD', 'NMR-6671', 'DIP-cD83', 'TEMP-1aB9', 'ICHI-A881', 'USDC-eB48', 'ENS-9D72', 'vBNT-7f94', 'ANKR-EDD4', 'UNI-F984', 'REQ-938a', 'WETH-6Cc2', 'AAVE-DaE9', 'ENJ-3B9c', 'MANA-C942', 'wNXM-2bDE', 'QNT-4675', 'RLC-7375', 'CROWN-E0fa', 'CHZ-b4AF', 'USDT-1ec7', 'DAI-1d0F', 'RPL-A51f', 'HOT-26E2', 'LINK-86CA', 'wstETH-2Ca0'} - combos = finder.get_combos(flashloan_tokens=flt, CCm=CCm, arb_mode="b3_two_hop") - print(combos) - assert len(combos) >= 1122, "[test_bancor_v3_two_hop] Different data used for tests, expected 1122 combos" - - -# ------------------------------------------------------------ -# Test 042 -# File test_042_TestBancorV3ModeTwoHop.py -# Segment Test_get_miniverse_combos +# Segment Test_get_combos # ------------------------------------------------------------ -def test_test_get_miniverse_combos(): +def test_test_get_combos(): # ------------------------------------------------------------ arb_finder = bot._get_arb_finder("b3_two_hop") @@ -403,6 +378,5 @@ def test_test_get_miniverse_combos(): ConfigObj=bot.ConfigObj, ) flt = {"0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C","0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48","0x514910771AF9Ca656af840dff83E8264EcF986CA"} - combos = finder.get_combos(flashloan_tokens=flt, CCm=CCm, arb_mode="b3_two_hop") - all_miniverses = finder.get_miniverse_combos(combos) - assert len(all_miniverses) >= 6, f"[test_bancor_v3_two_hop] Different data used for tests, expected 6 miniverses, found {len(all_miniverses)}" \ No newline at end of file + combos = finder.get_combos(flashloan_tokens=flt, CCm=CCm) + assert len(combos) >= 6, f"[test_bancor_v3_two_hop] Different data used for tests, expected 6 combos, found {len(combos)}" \ No newline at end of file diff --git a/fastlane_bot/tests/test_043_TestEmptyCarbonOrders.py b/fastlane_bot/tests/test_043_TestEmptyCarbonOrders.py index f49c68958..88ee7b1ad 100644 --- a/fastlane_bot/tests/test_043_TestEmptyCarbonOrders.py +++ b/fastlane_bot/tests/test_043_TestEmptyCarbonOrders.py @@ -138,8 +138,6 @@ def init_bot(mgr: Manager) -> CarbonBot: CCm = bot.get_curves() pools = db.get_pool_data_with_tokens() -arb_mode = "multi" - # ------------------------------------------------------------ # Test 043 @@ -150,7 +148,7 @@ def test_test_empty_carbon_orders_removed(): # ------------------------------------------------------------ # + - arb_finder = bot._get_arb_finder("multi") + arb_finder = bot._get_arb_finder("multi_pairwise_all") finder = arb_finder( flashloan_tokens=flashloan_tokens, CCm=CCm, diff --git a/fastlane_bot/tests/test_045_Validator.py b/fastlane_bot/tests/test_045_Validator.py index ca8732097..09767d52b 100644 --- a/fastlane_bot/tests/test_045_Validator.py +++ b/fastlane_bot/tests/test_045_Validator.py @@ -137,7 +137,7 @@ def init_bot(mgr: Manager) -> CarbonBot: # ------------------------------------------------------------ def test_test_validator(): # ------------------------------------------------------------ - for arb_mode in ["single", "multi", "multi_triangle"]: + for arb_mode in ["multi_triangle"]: arb_finder = bot._get_arb_finder(arb_mode) finder = arb_finder( flashloan_tokens=flashloan_tokens, diff --git a/fastlane_bot/tests/test_047_Randomizer.py b/fastlane_bot/tests/test_047_Randomizer.py index b2f250575..b3f0e832a 100644 --- a/fastlane_bot/tests/test_047_Randomizer.py +++ b/fastlane_bot/tests/test_047_Randomizer.py @@ -128,8 +128,6 @@ def init_bot(mgr: Manager) -> CarbonBot: CCm = bot.get_curves() pools = db.get_pool_data_with_tokens() -arb_mode = "multi" - # ------------------------------------------------------------ # Test 047 @@ -140,7 +138,7 @@ def test_test_randomizer(): # ------------------------------------------------------------ # + - arb_finder = bot._get_arb_finder("multi") + arb_finder = bot._get_arb_finder("multi_pairwise_all") finder = arb_finder( flashloan_tokens=flashloan_tokens, CCm=CCm, diff --git a/fastlane_bot/tests/test_048_RespectFlashloanTokensClickParam.py b/fastlane_bot/tests/test_048_RespectFlashloanTokensClickParam.py index 7169c7af0..a10be14f2 100644 --- a/fastlane_bot/tests/test_048_RespectFlashloanTokensClickParam.py +++ b/fastlane_bot/tests/test_048_RespectFlashloanTokensClickParam.py @@ -84,4 +84,4 @@ def run_command(arb_mode): def test_test_flashloan_tokens_is_respected(): # ------------------------------------------------------------ - run_command("multi") \ No newline at end of file + run_command("multi_pairwise_all") \ No newline at end of file diff --git a/fastlane_bot/tests/test_050_TestBancorV2.py b/fastlane_bot/tests/test_050_TestBancorV2.py index 45a036f8f..8dbf0b9f5 100644 --- a/fastlane_bot/tests/test_050_TestBancorV2.py +++ b/fastlane_bot/tests/test_050_TestBancorV2.py @@ -132,8 +132,6 @@ def init_bot(mgr: Manager) -> CarbonBot: CCm = bot.get_curves() pools = db.get_pool_data_with_tokens() -arb_mode = "multi" - # ------------------------------------------------------------ # Test 050 @@ -155,7 +153,7 @@ def test_test_min_profit(): def test_test_combos_and_tokens(): # ------------------------------------------------------------ - arb_finder = bot._get_arb_finder("multi") + arb_finder = bot._get_arb_finder("multi_pairwise_all") finder = arb_finder( flashloan_tokens=flashloan_tokens, CCm=CCm, @@ -307,9 +305,8 @@ def test_test_expected_output_bancorv2(): encoded_trade_instructions, deadline ) ] - b2pools = [pool['anchor'] for pool in mgr.pool_data if pool["exchange_name"] in "bancor_v2"] bancor_v2_converter_addresses = [pool["anchor"] for pool in state if pool["exchange_name"] in "bancor_v2"] - assert arb_finder.__name__ == "FindArbitrageMultiPairwiseAll", f"[NBTest_50_TestBancorV2] Expected arb_finder class name name = FindArbitrageMultiPairwise, found {arb_finder.__name__}" + assert arb_finder.__name__ == "FindArbitrageMultiPairwiseAll", f"[NBTest_50_TestBancorV2] Expected arb_finder class name = FindArbitrageMultiPairwiseAll, found {arb_finder.__name__}" assert len(r) > 30, f"[NBTest_50_TestBancorV2] Expected at least 30 arb opps, found {len(r)}" assert len(arb_with_bancor_v2) >= 3, f"[NBTest_50_TestBancorV2] Expected at least 3 arb opps with Bancor V2 pools, found {len(arb_with_bancor_v2)}" assert encoded_trade_instructions[0].amtin_wei == flashloan_amount, f"[NBTest_50_TestBancorV2] First trade in should match flashloan amount, {encoded_trade_instructions[0].amtin_wei} does not = {flashloan_amount}" diff --git a/fastlane_bot/tests/test_058_BalancerIntegration.py b/fastlane_bot/tests/test_058_BalancerIntegration.py index ab5fde773..9dbff5fd6 100644 --- a/fastlane_bot/tests/test_058_BalancerIntegration.py +++ b/fastlane_bot/tests/test_058_BalancerIntegration.py @@ -136,8 +136,6 @@ def init_bot(mgr: Manager) -> CarbonBot: CCm = bot.get_curves() pools = db.get_pool_data_with_tokens() -arb_mode = "multi_pairwise" - # ------------------------------------------------------------ # Test 058 diff --git a/fastlane_bot/tests/test_060_TestRoutehandlerCarbonPrecision.py b/fastlane_bot/tests/test_060_TestRoutehandlerCarbonPrecision.py index 33b0fd1c3..ed4f2c7e1 100644 --- a/fastlane_bot/tests/test_060_TestRoutehandlerCarbonPrecision.py +++ b/fastlane_bot/tests/test_060_TestRoutehandlerCarbonPrecision.py @@ -166,10 +166,8 @@ def init_bot(mgr: Manager) -> CarbonBot: def test_test_precision_using_all_tokens_in_carbon(): # ------------------------------------------------------------ - # + - arb_mode = "multi" - - arb_finder = bot._get_arb_finder(arb_mode) + # + + arb_finder = bot._get_arb_finder("multi_pairwise_all") finder = arb_finder( flashloan_tokens=flashloan_tokens, CCm=CCm, diff --git a/fastlane_bot/tests/test_061_TestWETHConversion.py b/fastlane_bot/tests/test_061_TestWETHConversion.py index aa96f81e2..3df06ab74 100644 --- a/fastlane_bot/tests/test_061_TestWETHConversion.py +++ b/fastlane_bot/tests/test_061_TestWETHConversion.py @@ -172,7 +172,7 @@ def init_bot(mgr: Manager) -> CarbonBot: # ## Test_Wrap_Unwrap_Gas_Token_In_Route_Struct # + -arb_mode = "multi" +arb_mode = "multi_pairwise_all" arb_finder = bot._get_arb_finder(arb_mode) finder = arb_finder( diff --git a/fastlane_bot/tests/test_063_TestBancorPOLMode.py b/fastlane_bot/tests/test_063_TestBancorPOLMode.py index 97ade2788..f68c7b83f 100644 --- a/fastlane_bot/tests/test_063_TestBancorPOLMode.py +++ b/fastlane_bot/tests/test_063_TestBancorPOLMode.py @@ -128,8 +128,6 @@ def init_bot(mgr: Manager) -> CarbonBot: CCm = bot.get_curves() pools = db.get_pool_data_with_tokens() -arb_mode = "multi_pairwise_pol" - # ------------------------------------------------------------ # Test 063 @@ -176,7 +174,7 @@ def test_test_combos_and_tokens(): assert type(combos) == list, f"[NBTest 063 TestMultiPairwisePOLMode] combos is wrong data type. Expected list, found: {type(combos)}" assert ('0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C', '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2') in combos or ('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', '0x1F573D6Fb3F13d689FF844B4cE37794d79a7FF1C') in combos, f"[NBTest 063 TestMultiPairwisePOLMode] Expected BNT/WETH or WETH/BNT in combos" assert len(all_tokens) >= 73, f"[NBTest 063 TestMultiPairwisePOLMode] Using wrong dataset, expected at least 73 tokens, found {len(all_tokens)}" - assert len(combos) >= 146, f"[NBTest 063 TestMultiPairwisePOLMode] Using wrong dataset, expected at least 146 combos, found {len(combos)}" + assert len(combos) >= 73, f"[NBTest 063 TestMultiPairwisePOLMode] Using wrong dataset, expected at least 73 combos, found {len(combos)}" # ------------------------------------------------------------ @@ -220,6 +218,6 @@ def test_test_expected_output(): if trade["tknin"] not in carbon_tkn_in: carbon_wrong_direction_count += 1 - assert len(r) >= 36, f"[NBTest 063 TestMultiPairwisePOLMode] Expected at least 27 arbs, found {len(r)}" + assert len(r) >= 18, f"[NBTest 063 TestMultiPairwisePOLMode] Expected at least 18 arbs, found {len(r)}" assert multi_carbon_count > 0, f"[NBTest 063 TestMultiPairwisePOLMode] Not finding arbs with multiple Carbon curves." assert carbon_wrong_direction_count == 0, f"[NBTest 063 TestMultiPairwisePOLMode Mode] Expected all Carbon curves to have the same tkn in and tkn out. Mixing is currently not supported." \ No newline at end of file diff --git a/fastlane_bot/tests/test_064_TestMultiAllMode.py b/fastlane_bot/tests/test_064_TestMultiAllMode.py index 6d7ff45be..9a9c85776 100644 --- a/fastlane_bot/tests/test_064_TestMultiAllMode.py +++ b/fastlane_bot/tests/test_064_TestMultiAllMode.py @@ -128,8 +128,6 @@ def init_bot(mgr: Manager) -> CarbonBot: CCm = bot.get_curves() pools = db.get_pool_data_with_tokens() -arb_mode = "multi_pairwise_all" - # ------------------------------------------------------------ # Test 064 diff --git a/fastlane_bot/tests/test_901_TestMultiTriangleModeSlow.py b/fastlane_bot/tests/test_901_TestMultiTriangleModeSlow.py index a003d4fdf..7ad21b45b 100644 --- a/fastlane_bot/tests/test_901_TestMultiTriangleModeSlow.py +++ b/fastlane_bot/tests/test_901_TestMultiTriangleModeSlow.py @@ -129,8 +129,6 @@ def init_bot(mgr: Manager) -> CarbonBot: CCm = bot.get_curves() pools = db.get_pool_data_with_tokens() -arb_mode = "multi_triangle" - # ------------------------------------------------------------ # Test 901 @@ -145,7 +143,7 @@ def test_test_min_profit(): # ### Test_arb_mode_class arb_finder = bot._get_arb_finder("multi_triangle") - assert arb_finder.__name__ == "ArbitrageFinderTriangleMulti", f"[TestMultiTriangleMode] Expected arb_finder class name name = FindArbitrageMultiPairwise, found {arb_finder.__name__}" + assert arb_finder.__name__ == "ArbitrageFinderTriangleMulti", f"[TestMultiTriangleMode] Expected arb_finder class name = ArbitrageFinderTriangleMulti, found {arb_finder.__name__}" # ------------------------------------------------------------ @@ -164,7 +162,7 @@ def test_test_combos(): result=arb_finder.AO_TOKENS, ConfigObj=bot.ConfigObj, ) - combos = finder.get_combos(flashloan_tokens=flashloan_tokens, CCm=CCm, arb_mode="multi_triangle") + combos = finder.get_combos(flashloan_tokens=flashloan_tokens, CCm=CCm) assert len(combos) >= 1225, f"[TestMultiTriangleMode] Using wrong dataset, expected at least 1225 combos, found {len(combos)}" # + @@ -217,85 +215,3 @@ def test_test_combos(): assert multi_carbon_count > 0, f"[TestMultiTriangleMode] Not finding arbs with multiple Carbon curves." assert len(r) >= 58, f"[TestMultiTriangleMode] Expected at least 58 arbs, found {len(r)}" # - - - -# ------------------------------------------------------------ -# Test 901 -# File test_901_TestMultiTriangleModeSlow.py -# Segment Test Triangle Single -# ------------------------------------------------------------ -def test_test_triangle_single(): -# ------------------------------------------------------------ - - arb_finder = bot._get_arb_finder("triangle") - assert arb_finder.__name__ == "ArbitrageFinderTriangleSingle", f"[TestMultiTriangleMode] Expected arb_finder class name name = ArbitrageFinderTriangleSingle, found {arb_finder.__name__}" - - -# ------------------------------------------------------------ -# Test 901 -# File test_901_TestMultiTriangleModeSlow.py -# Segment Test_combos_triangle_single -# ------------------------------------------------------------ -def test_test_combos_triangle_single(): -# ------------------------------------------------------------ - - arb_finder = bot._get_arb_finder("triangle") - finder = arb_finder( - flashloan_tokens=flashloan_tokens, - CCm=CCm, - mode="bothin", - result=arb_finder.AO_TOKENS, - ConfigObj=bot.ConfigObj, - ) - combos = finder.get_combos(flashloan_tokens=flashloan_tokens, CCm=CCm, arb_mode="multi_triangle") - assert len(combos) >= 1225, f"[TestMultiTriangleMode] Using wrong dataset, expected at least 1225 combos, found {len(combos)}" - - -# ------------------------------------------------------------ -# Test 901 -# File test_901_TestMultiTriangleModeSlow.py -# Segment Test_Find_Arbitrage_Single -# ------------------------------------------------------------ -def test_test_find_arbitrage_single(): -# ------------------------------------------------------------ - - # + - arb_finder = bot._get_arb_finder("triangle") - finder = arb_finder( - flashloan_tokens=flashloan_tokens, - CCm=CCm, - mode="bothin", - result=arb_finder.AO_CANDIDATES, - ConfigObj=bot.ConfigObj, - ) - r = finder.find_arbitrage() - multi_carbon_count = 0 - for arb in r: - ( - best_profit, - best_trade_instructions_df, - best_trade_instructions_dic, - best_src_token, - best_trade_instructions, - ) = arb - if len(best_trade_instructions_dic) > 3: - multi_carbon_count += 1 - tkn_in = None - tkn_out = None - # Find the first Carbon Curve to establish tknin and tknout - for curve in best_trade_instructions_dic: - if "-0" in curve['cid'] or "-1" in curve['cid']: - tkn_in = curve["tknin"] - tknout = curve["tknout"] - break - for curve in best_trade_instructions_dic: - if "-0" in curve['cid'] or "-1" in curve['cid']: - if curve["tknin"] in [tkn_in, tkn_out] and curve["tknout"] in [tkn_in, tkn_out]: - assert curve["tknin"] in tkn_in, f"[TestMultiTriangleMode] Finding Carbon curves in opposite directions - not supported in this mode." - assert curve["tknout"] in tkn_out, f"[TestMultiTriangleMode] Finding Carbon curves in opposite directions - not supported in this mode." - - assert multi_carbon_count == 0, f"[TestMultiTriangleMode] Expected 0 arbs with multiple Carbon curves for Triangle Single mode, found {multi_carbon_count}." - assert len(r) >= 58, f"[TestMultiTriangleMode] Expected at least 58 arbs, found {len(r)}" - # - - - \ No newline at end of file diff --git a/fastlane_bot/tests/test_906_TargetTokens.py b/fastlane_bot/tests/test_906_TargetTokens.py index b2651b76c..86485a114 100644 --- a/fastlane_bot/tests/test_906_TargetTokens.py +++ b/fastlane_bot/tests/test_906_TargetTokens.py @@ -88,4 +88,4 @@ def run_command(mode): def test_test_flashloan_tokens_b3_two_hop(): # ------------------------------------------------------------ - run_command("single") \ No newline at end of file + run_command("b3_two_hop") \ No newline at end of file diff --git a/fastlane_bot/tests_on_hold/test_907_RuntimeParameters.py b/fastlane_bot/tests_on_hold/test_907_RuntimeParameters.py index f36d97192..6e0862e7d 100644 --- a/fastlane_bot/tests_on_hold/test_907_RuntimeParameters.py +++ b/fastlane_bot/tests_on_hold/test_907_RuntimeParameters.py @@ -57,9 +57,7 @@ def __init__(self): arb_mode_happy_path_options = [ - "single", - "multi", - "triangle", + "multi_pairwise_all", "multi_triangle", "b3_two_hop", ] diff --git a/main.py b/main.py index fcc722efe..1c09637d8 100644 --- a/main.py +++ b/main.py @@ -580,14 +580,11 @@ def run(mgr, args, tenderly_uri=None) -> None: default="multi_pairwise_all", help="See arb_mode in bot.py", choices=[ - "single", - "multi", - "triangle", "multi_triangle", + "multi_triangle_complete", "b3_two_hop", "multi_pairwise_pol", "multi_pairwise_all", - "multi_triangle_complete" ], ) parser.add_argument( diff --git a/scan_log_errors.py b/scan_log_errors.py deleted file mode 100644 index 5fd520ff8..000000000 --- a/scan_log_errors.py +++ /dev/null @@ -1,173 +0,0 @@ -import os -import re -import time -from datetime import datetime -from datetime import timedelta - -import click - - -def is_valid_timestamp_dir(name, time_format): - try: - datetime.strptime(name, time_format) - return True - except ValueError: - return False - - -def contains_only_one_specific_word(file_path, target_word, other_words): - with open(file_path, "r") as file: - file_contents = file.read() - - # Define a regex pattern with word boundaries for the target word - target_word_pattern = r"\b" + re.escape(target_word) + r"\b" - target_word_count = len(re.findall(target_word_pattern, file_contents)) - if target_word_count == 0: - return False - - # Check for other words - for word in other_words: - if word != target_word: - # Define a regex pattern with word boundaries for each other word - word_pattern = r"\b" + re.escape(word) + r"\b" - if re.search(word_pattern, file_contents): - return False - - return True - - -def find_latest_log_with_string( - logs_dir, search_string, non_search_patterns, time_format="%Y%m%d-%H%M%S" -): - latest_time = None - latest_folder = None - - for folder_name in os.listdir(logs_dir): - folder_path = os.path.join(logs_dir, folder_name) - if os.path.isdir(folder_path) and is_valid_timestamp_dir( - folder_name, time_format - ): - log_file_path = os.path.join(folder_path, "bot.log") - if os.path.exists(log_file_path): - is_match = contains_only_one_specific_word( - log_file_path, search_string, non_search_patterns - ) - if is_match: - folder_time = datetime.strptime(folder_name, time_format) - if not latest_time or folder_time > latest_time: - latest_time = folder_time - latest_folder = folder_path - - return latest_folder - - -def read_log_file(file_path): - with open(file_path, "r") as file: - return file.read() - - -def scan_logs_for_opportunity( - logs, search_pattern, max_minutes, latest_opportunity_timestamp=None -): - time_pattern = r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}),\d{3}" - search_regex = re.compile(time_pattern + r".*" + search_pattern) - error_detected = True - - for line in logs.split("\n"): - match = search_regex.search(line) - if match: - last_timestamp = match.group(1) - if ( - latest_opportunity_timestamp - and datetime.strptime(last_timestamp, "%Y-%m-%d %H:%M:%S") - > latest_opportunity_timestamp - ): - error_detected = False - print(f"Opportunity found at {last_timestamp}") - break - - if error_detected: - raise ValueError( - f"Opportunity not found within {max_minutes} minutes interval." - ) - - -@click.command() -@click.option("--logs_directory", default="logs", type=str) -@click.option("--interval", default=30, type=int) -@click.option("--search_pattern", default="multi_pairwise_all", type=str) -@click.option("--max_minutes", default=10, type=int) -def main(logs_directory, interval, search_pattern, max_minutes): - - all_patterns = [ - "single", - "multi", - "triangle", - "multi_triangle", - "bancor_v3", - "b3_two_hop", - "multi_pairwise_pol", - "multi_pairwise_bal", - "multi_pairwise_all", - ] - - if search_pattern not in all_patterns: - print(f"Invalid search pattern. Valid patterns are: {all_patterns}") - raise ValueError("Invalid search pattern.") - - non_search_patterns = [ - f"arb_mode: {pattern}" for pattern in all_patterns if pattern != search_pattern - ] - - search_pattern = f"arb_mode: {search_pattern}" - - latest_folder = find_latest_log_with_string( - logs_directory, search_pattern, non_search_patterns - ) - - if latest_folder: - print(f"Latest folder containing '{search_pattern}': {latest_folder}") - else: - print("No folder found with the specified pattern.") - raise ValueError("No folder found with the specified pattern.") - - print("Starting continuous scan of log file.") - while True: - last_timestamp = datetime.now() - timedelta(minutes=max_minutes) - try: - logs = read_log_file(f"{latest_folder}/bot.log") - scan_logs_for_opportunity( - logs, r"Opportunity with profit:", max_minutes, last_timestamp - ) - print("Everything is OK...") - except ValueError as e: - next_latest_folder = find_latest_log_with_string( - logs_directory, search_pattern, non_search_patterns - ) - if next_latest_folder and next_latest_folder != latest_folder: - print( - f"Latest folder containing '{search_pattern}': {next_latest_folder}" - ) - latest_folder = next_latest_folder - continue - else: - print(e) - break - except Exception as ex: - next_latest_folder = find_latest_log_with_string( - logs_directory, search_pattern, non_search_patterns - ) - if next_latest_folder and next_latest_folder != latest_folder: - print( - f"Latest folder containing '{search_pattern}': {next_latest_folder}" - ) - latest_folder = next_latest_folder - continue - else: - print(f"An error occurred: {ex}") - break - time.sleep(interval) - - -if __name__ == "__main__": - main()