diff --git a/fastlane_bot/testing.py b/fastlane_bot/testing.py index cf47d1c2f..1267ed662 100644 --- a/fastlane_bot/testing.py +++ b/fastlane_bot/testing.py @@ -67,7 +67,7 @@ class VersionRequirementNotMetError(RuntimeError): pass def _split_version_str(vstr): """splits version mumber string into tuple (int, int, int, ...)""" - m = _re.match("^([0-9\.]*)", vstr.strip()) + m = _re.match(r"^([0-9\.]*)", vstr.strip()) if m is None: raise ValueError("Invalid version number string", vstr) vlst = tuple(int(x) for x in m.group(0).split(".")) diff --git a/fastlane_bot/tests/nbtest/test_003_Serialization.py b/fastlane_bot/tests/nbtest/test_003_Serialization.py index 12076f10f..7a894fbf4 100644 --- a/fastlane_bot/tests/nbtest/test_003_Serialization.py +++ b/fastlane_bot/tests/nbtest/test_003_Serialization.py @@ -31,23 +31,30 @@ def notest_optimizer_pickling(): # ------------------------------------------------------------ - N=5 - curves = [ - CPC.from_xy(x=1, y=2000, pair="ETH/USDC"), - CPC.from_xy(x=1, y=2200, pair="ETH/USDC"), - CPC.from_xy(x=1, y=2400, pair="ETH/USDC"), - ] - # note: the below is a bit icky as the same curve objects are added multiple times - CC = CPCContainer(curves*N) - O = CPCArbOptimizer(CC) - O.CC.asdf() + pass - O.pickle("delme") - O.pickle("delme", addts=False) + # + + # N=5 + # curves = [ + # CPC.from_xy(x=1, y=2000, pair="ETH/USDC"), + # CPC.from_xy(x=1, y=2200, pair="ETH/USDC"), + # CPC.from_xy(x=1, y=2400, pair="ETH/USDC"), + # ] + + # CC = CPCContainer(curves*N) + # O = CPCArbOptimizer(CC) + # O.CC.asdf() - # !ls *.pickle + # + + # O.pickle("delme") + # O.pickle("delme", addts=False) - O.unpickle("delme") + # + + + + # + + # O.unpickle("delme") + # - # ------------------------------------------------------------ @@ -92,6 +99,7 @@ def test_creating_curves(): 'x': 100, 'x_act': 100, 'y_act': 100, + 'alpha': 0.5, 'pair': 'TKNB/TKNQ', 'cid': "1", 'fee': 0, @@ -278,7 +286,7 @@ def test_serializing_curves(): df = CC.asdf() assert len(df) == 3 - assert tuple(df.reset_index().columns) == ('cid', 'k', 'x', 'x_act', 'y_act', + assert tuple(df.reset_index().columns) == ('cid', 'k', 'x', 'x_act', 'y_act', 'alpha', 'pair', 'fee', 'descr', 'constr', 'params') assert tuple(df["k"]) == (2000, 8040, 1970) assert CPCContainer.from_df(df) == CC @@ -395,4 +403,6 @@ def notest_saving_curves(): + + \ No newline at end of file diff --git a/fastlane_bot/tests/nbtest/test_037_Exchanges.py b/fastlane_bot/tests/nbtest/test_037_Exchanges.py index 29fcdf253..944353a3a 100644 --- a/fastlane_bot/tests/nbtest/test_037_Exchanges.py +++ b/fastlane_bot/tests/nbtest/test_037_Exchanges.py @@ -39,6 +39,7 @@ mocked_contract.functions.token0.return_value.call.return_value = 'token0' mocked_contract.functions.token1.return_value.call.return_value = 'token1' mocked_contract.functions.fee.return_value.call.return_value = 3000 +mocked_contract.functions.tradingFeePPM.return_value.call.return_value = 2000 # ------------------------------------------------------------ @@ -107,7 +108,7 @@ def test_test_carbon_v1_exchange_update(): carbon_v1_exchange = CarbonV1() assert (carbon_v1_exchange.get_abi() == CARBON_CONTROLLER_ABI) - assert (carbon_v1_exchange.get_fee('', mocked_contract) == ('0.002', 0.002)) + assert (carbon_v1_exchange.get_fee('', mocked_contract) == ('2000', 0.002)) assert (carbon_v1_exchange.get_tkn0('', mocked_contract, setup_data['carbon_v1_event_update']) == setup_data['carbon_v1_event_update']['args']['token0']) @@ -121,7 +122,7 @@ def test_test_carbon_v1_exchange_create(): carbon_v1_exchange = CarbonV1() assert (carbon_v1_exchange.get_abi() == CARBON_CONTROLLER_ABI) - assert (carbon_v1_exchange.get_fee('', mocked_contract) == ('0.002', 0.002)) + assert (carbon_v1_exchange.get_fee('', mocked_contract) == ('2000', 0.002)) assert (carbon_v1_exchange.get_tkn0('', mocked_contract, setup_data['carbon_v1_event_create']) == setup_data['carbon_v1_event_create']['args']['token0']) diff --git a/fastlane_bot/tests/nbtest/test_038_TestBancorV3Mode.py b/fastlane_bot/tests/nbtest/test_038_TestBancorV3Mode.py index f1eb2a048..cc7f317d8 100644 --- a/fastlane_bot/tests/nbtest/test_038_TestBancorV3Mode.py +++ b/fastlane_bot/tests/nbtest/test_038_TestBancorV3Mode.py @@ -196,17 +196,7 @@ def init_bot(mgr: Manager) -> CarbonBot: assert pool.cid in pool_cids, f"[test_bancor_v3] Validation missing pool.cid {pool.cid} in {pool_cids}" optimal_arb = finder.get_optimal_arb_trade_amts(pool_cids, 'BNT-FF1C') assert type(optimal_arb) == float, f"[test_bancor_v3] Optimal arb calculation type is {type(optimal_arb)} not float" -assert iseq(optimal_arb, 5003.2368760578265), f"[test_bancor_v3] Optimal arb calculation type is {optimal_arb}, expected 5003.2368760578265" - - - - - - - - - - +assert iseq(optimal_arb, 4051.1611717583105), f"[test_bancor_v3] Optimal arb calculation type is {optimal_arb}, expected 4051.1611717583105" # ------------------------------------------------------------ # Test 038 @@ -282,7 +272,7 @@ def test_test_get_fee_safe(): ) ext_fee = finder.get_fee_safe(first_check_pools[1].fee) assert type(ext_fee) == float, f"[test_bancor_v3] Testing external pool, fee type is {type(ext_fee)} not float" - assert iseq(ext_fee, 0.003), f"[test_bancor_v3] Testing external pool, fee amt is {ext_fee} not 0.003" + assert iseq(ext_fee, 0.0005), f"[test_bancor_v3] Testing external pool, fee amt is {ext_fee} not 0.0005" # ------------------------------------------------------------ diff --git a/fastlane_bot/tests/nbtest/test_048_RespectFlashloanTokensClickParam.py b/fastlane_bot/tests/nbtest/test_048_RespectFlashloanTokensClickParam.py new file mode 100644 index 000000000..8d553c5c2 --- /dev/null +++ b/fastlane_bot/tests/nbtest/test_048_RespectFlashloanTokensClickParam.py @@ -0,0 +1,100 @@ +# ------------------------------------------------------------ +# Auto generated test file `test_048_RespectFlashloanTokensClickParam.py` +# ------------------------------------------------------------ +# source file = NBTest_048_RespectFlashloanTokensClickParam.py +# test id = 048 +# test comment = RespectFlashloanTokensClickParam +# ------------------------------------------------------------ + + + +""" +This module contains the tests which ensure that the flashloan tokens click parameters are respected. +""" +from fastlane_bot import Bot +from fastlane_bot.tools.cpc import ConstantProductCurve as CPC +from fastlane_bot.events.exchanges import UniswapV2, UniswapV3, SushiswapV2, CarbonV1, BancorV3 +import subprocess, os, sys +import pytest +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(SushiswapV2)) +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.rcParams['figure.figsize'] = [12,6] +from fastlane_bot import __VERSION__ +require("3.0", __VERSION__) + + + + +def find_main_py(): + # Start at the directory of the current script + cwd = os.path.abspath(os.path.join(os.getcwd())) + + print(f"Searching for main.py in {cwd}") + while True: + # Check if main.py exists in the current directory + if "main.py" in os.listdir(cwd): + return cwd # Found the directory containing main.py + else: + # If not, go up one directory + new_cwd = os.path.dirname(cwd) + + # If we're already at the root directory, stop searching + if new_cwd == cwd: + raise FileNotFoundError("Could not find main.py in any parent directory") + + cwd = new_cwd + + +def run_command(arb_mode, expected_log_line): + + # Find the correct path to main.py + main_script_path = find_main_py() + print(f"Found main.py in {main_script_path}") + main_script_path = main_script_path + "/main.py" + + # Run the command + cmd = [ + "python", + main_script_path, + f"--arb_mode={arb_mode}", + "--default_min_profit_bnt=60", + "--limit_bancor3_flashloan_tokens=False", + "--use_cached_events=True", + "--logging_path=fastlane_bot/data/", + "--timeout=1", + "--loglevel=DEBUG", + "--flashloan_tokens=BNT-FF1C,ETH-EEeE,ETH2X-FLI-USD", + ] + subprocess.Popen(cmd) + + # Wait for the expected log line to appear + found = False + result = subprocess.run(cmd, text=True, capture_output=True, check=True, timeout=7) + + # Check if the expected log line is in the output + if expected_log_line in result.stderr or expected_log_line in result.stdout: + found = True + + if not found: + pytest.fail("Expected log line was not found within 1 minute") # If we reach this point, the test has failed + + + + +# ------------------------------------------------------------ +# Test 048 +# File test_048_RespectFlashloanTokensClickParam.py +# Segment Test flashloan_tokens is Respected +# ------------------------------------------------------------ +def test_test_flashloan_tokens_is_respected(): +# ------------------------------------------------------------ + + expected_log_line = "Flashloan tokens are set as: ['BNT-FF1C', 'ETH-EEeE', 'ETH2X_FLI-USD']" + arb_mode = "multi" + run_command(arb_mode=arb_mode, expected_log_line=expected_log_line) \ No newline at end of file diff --git a/fastlane_bot/tests/nbtest/test_049_CPCBalancer.py b/fastlane_bot/tests/nbtest/test_049_CPCBalancer.py new file mode 100644 index 000000000..2e5d4b1b4 --- /dev/null +++ b/fastlane_bot/tests/nbtest/test_049_CPCBalancer.py @@ -0,0 +1,556 @@ +# ------------------------------------------------------------ +# Auto generated test file `test_049_CPCBalancer.py` +# ------------------------------------------------------------ +# source file = NBTest_049_CPCBalancer.py +# test id = 049 +# test comment = CPCBalancer +# ------------------------------------------------------------ + + + +from fastlane_bot.tools.cpc import ConstantProductCurve as CPC +#from flbtools.cpc import ConstantProductCurve as CPC +print("{0.__name__} v{0.__VERSION__} ({0.__DATE__})".format(CPC)) + +from fastlane_bot.testing import * +#from flbtesting import * +from math import sqrt + + + + +# ------------------------------------------------------------ +# Test 049 +# File test_049_CPCBalancer.py +# Segment Constant product constructor +# ------------------------------------------------------------ +def test_constant_product_constructor(): +# ------------------------------------------------------------ + + c0 = CPC.from_xy(100, 200) + assert c0.x == 100 + assert c0.y == 200 + assert c0.k == 20000 + assert c0.x == c0.x_act + assert c0.y == c0.y_act + assert c0.alpha == 0.5 + assert c0.eta == 1 + assert c0.constr == "xy" + assert c0.is_constant_product() == True + c0 + + assert c0.asdict() == { + 'k': 20000, + 'x': 100, + 'x_act': 100, + 'y_act': 200.0, + 'alpha': 0.5, + 'pair': 'TKNB/TKNQ', + 'cid': 'None', + 'fee': None, + 'descr': None, + 'constr': 'xy', + 'params': {} + } + + c1 = CPC.from_xyal(100, 200) + assert c1.constr == "xyal" + assert c1.is_constant_product() == True + assert c1==c0 + c1 + + assert c1.asdict() == { + 'k': 20000, + 'x': 100, + 'x_act': 100, + 'y_act': 200.0, + 'alpha': 0.5, + 'pair': 'TKNB/TKNQ', + 'cid': 'None', + 'fee': None, + 'descr': None, + 'constr': 'xyal', + 'params': {} + } + + c2 = CPC.from_xyal(100, 200, alpha=0.5) + assert c2.constr == "xyal" + assert c2.is_constant_product() == True + assert c2==c0 + assert c2.asdict() == c1.asdict() + c2 + + c3 = CPC.from_xyal(100, 200, eta=1) + assert c3.constr == "xyal" + assert c3.is_constant_product() == True + assert c3==c0 + assert c3.asdict() == c1.asdict() + c3 + + assert raises(CPC.from_xyal, 100, 200, + alpha=0.5, eta=1) == 'at most one of alpha and eta must be given [0.5, 1]' + + +# ------------------------------------------------------------ +# Test 049 +# File test_049_CPCBalancer.py +# Segment Weighted constructor +# ------------------------------------------------------------ +def test_weighted_constructor(): +# ------------------------------------------------------------ + + c0 = CPC.from_xy(100, 200) + assert c0.x == 100 + assert c0.y == 200 + assert c0.k == 20000 + assert c0.x == c0.x_act + assert c0.y == c0.y_act + assert c0.alpha == 0.5 + assert c0.eta == 1 + assert c0.constr == "xy" + assert c0.is_constant_product() == True + c0 + + c1 = CPC.from_xyal(100, 200) + assert c1.constr == "xyal" + assert c1.is_constant_product() == True + assert c1 == c0 + assert c1.asdict()["alpha"] == 0.5 + c1 + + c2 = CPC.from_xyal(100, 200, alpha=0.25) + assert c2.constr == "xyal" + assert c2.is_constant_product() == False + assert c2.alpha == 0.25 + assert c2.asdict()["alpha"] == 0.25 + assert iseq(c2.eta, 0.25/0.75) + assert c2 != c0 + assert c2 != c1 + c2 + + c3 = CPC.from_xyal(100, 200, alpha=0.8) + assert c3.constr == "xyal" + assert c3.is_constant_product() == False + assert iseq(c3.alpha, 0.8) + assert c3.asdict()["alpha"] == 0.8 + assert iseq(c3.eta, 0.8/0.2) + assert c3 != c0 + assert c3 != c1 + assert c3 != c2 + c2 + + c3b = CPC.fromdict(c3.asdict()) + assert c3b == c3 + c3b + + assert raises(CPC.from_xyal,100, 200, alpha=0) == 'alpha must be > 0 [0]' + assert raises(CPC.from_xyal,100, 200, alpha=-1) == 'alpha must be > 0 [-1]' + assert raises(CPC.from_xyal,100, 200, alpha=1) == 'alpha must be < 1 [1]' + assert raises(CPC.from_xyal,100, 200, alpha=2) == 'alpha must be < 1 [2]' + + raises(CPC.from_xyal,100, 200, alpha=0) + + assert not raises(CPC.from_xyal,100, 200, alpha=1-1e-10) + assert not raises(CPC.from_xyal,100, 200, alpha=0.01) + + raises(CPC.from_xyal,100, 200, alpha=0.001) + + +# ------------------------------------------------------------ +# Test 049 +# File test_049_CPCBalancer.py +# Segment High level testing of all functions +# ------------------------------------------------------------ +def test_high_level_testing_of_all_functions(): +# ------------------------------------------------------------ + # + # (including not YET implemented) + + c0 = CPC.from_xyal(100, 200) + assert c0.is_constant_product() == True + c0 + + c1 = CPC.from_xyal(100, 200, alpha=0.25) + assert c1.is_constant_product() == False + c1 + + # #### Not (yet) implemented functions + # + # Those function groups are not currently planned to be implemented at all + # + # - `execute` as there is no need to simulate those curves for the time being; that was a Carbon thing + # + # The functions we may implement at a later stage are + # + # - `description` should probably be updated, but it is tedious; `format` ditto + # - `x_max`, `x_min`, `p_max`, `p_min` and the other leverage functions once we consider it important and safe + + # execute + + assert not raises(c0.execute) + assert raises(c1.execute).startswith("only implemented for") + + # description and format + + assert not raises(c0.description) + assert raises(c1.description).startswith("only implemented for") + + assert not raises(c0.format) + assert raises(c1.format).startswith("only implemented for") + + # leverage related functions (primary) + + assert not raises(lambda: c0.p_max) + assert raises(lambda: c1.p_max).startswith("only implemented for") + + assert not raises(lambda: c0.p_min) + assert raises(lambda: c1.p_min).startswith("only implemented for") + + assert not raises(lambda: c0.x_min) + assert raises(lambda: c1.x_min).startswith("only implemented for") + + assert not raises(lambda: c0.x_max) + assert raises(lambda: c1.x_max).startswith("only implemented for") + + assert not raises(lambda: c0.y_min) + assert raises(lambda: c1.y_min).startswith("only implemented for") + + assert not raises(lambda: c0.y_max) + assert raises(lambda: c1.y_max).startswith("only implemented for") + + # leverage related functions (secondary, ie calling primary ones) + + assert not raises(c0.p_max_primary) + assert raises(c1.p_max_primary).startswith("only implemented for") + + assert not raises(c0.p_min_primary) + assert raises(c1.p_min_primary).startswith("only implemented for") + + assert not raises(lambda: c0.at_xmin) + assert raises(lambda: c1.at_xmin).startswith("only implemented for") + + assert not raises(lambda: c0.at_xmax) + assert raises(lambda: c1.at_xmax).startswith("only implemented for") + + assert not raises(lambda: c0.at_ymin) + assert raises(lambda: c1.at_ymin).startswith("only implemented for") + + assert not raises(lambda: c0.at_ymax) + assert raises(lambda: c1.at_ymax).startswith("only implemented for") + + assert not raises(lambda: c0.at_boundary) + assert raises(lambda: c1.at_boundary).startswith("only implemented for") + + # todo + + assert not raises(c0.xyfromp_f) + assert raises(c1.xyfromp_f).startswith("only implemented for") + + assert not raises(c0.dxdyfromp_f) + assert raises(c1.dxdyfromp_f).startswith("only implemented for") + + # #### Implemented functions + + assert not raises(lambda: c0.y) + assert not raises(lambda: c1.y) + + assert not raises(lambda: c0.p) + assert not raises(lambda: c1.p) + + assert not raises(lambda: c0.kbar) + assert not raises(lambda: c1.kbar) + + assert not raises(c0.tvl) + assert not raises(c1.tvl) + + assert not raises(c0.yfromx_f, 110) + assert not raises(c1.yfromx_f, 110, ignorebounds=True) + assert raises(c1.yfromx_f, 110, ignorebounds=False) + + assert not raises(c0.xfromy_f, 210) + assert not raises(c1.xfromy_f, 110, ignorebounds=True) + assert raises(c1.xfromy_f, 110, ignorebounds=False) + + assert not raises(c0.dyfromdx_f, 1) + assert not raises(c1.dyfromdx_f, 1, ignorebounds=True) + assert raises(c1.dyfromdx_f, 1, ignorebounds=False) + + assert not raises(c0.dxfromdy_f, 1) + assert not raises(c1.dxfromdy_f, 1, ignorebounds=True) + assert raises(c1.dxfromdy_f, 1, ignorebounds=False) + + +# ------------------------------------------------------------ +# Test 049 +# File test_049_CPCBalancer.py +# Segment Simple Tests +# ------------------------------------------------------------ +def test_simple_tests(): +# ------------------------------------------------------------ + + c0 = CPC.from_xyal(100, 200) + c1 = CPC.from_xyal(100, 200, eta=2) + c2 = CPC.from_xyal(100, 200, eta=0.5) + + assert iseq(c0.alpha, 1/2) + assert iseq(c1.alpha, 2/3) + assert iseq(c2.alpha, 1/3) + + # #### Current token balance $y$ + # + # $$ + # y = \left( \frac k x \right)^\eta + # $$ + + assert iseq(c0.y, 200) + assert iseq(c1.y, 200) + assert iseq(c2.y, 200) + + # #### Current price $p$ + # + # $$ + # p = \eta\, \frac y x + # $$ + + assert iseq(c0.p, 2 * c0.eta) + assert iseq(c1.p, 2 * c1.eta) + assert iseq(c2.p, 2 * c2.eta) + + # #### TVL + # + # $$ + # \mathrm{TVL} = x_a*p + y_a + # $$ + + assert c0.x == c0.x_act + assert c0.y == c0.y_act + assert c1.x == c1.x_act + assert c1.y == c1.y_act + assert c2.x == c2.x_act + assert c2.y == c2.y_act + + assert iseq(c0.tvl(), 100 * c0.p + 200) + assert iseq(c1.tvl(), 100 * c1.p + 200) + assert iseq(c2.tvl(), 100 * c2.p + 200) + + # #### Pool constant $k$ + # + # $$ + # k^\alpha = x^\alpha\, y^{1-\alpha} + # $$ + # + # $$ + # k = x\,y^\frac{1-\alpha}{\alpha} = x\,y^{\frac 1 \eta} + # $$ + # + + assert iseq(c0.k**(1/2), c0.x**(1/2) * c0.y**(1/2)) + assert iseq(c1.k**(2/3), c1.x**(2/3) * c1.y**(1/3)) + assert iseq(c2.k**(1/3), c1.x**(1/3) * c1.y**(2/3)) + + # #### Pool constant $\bar k$ + # + # $$ + # x^\alpha\, y^{1-\alpha} = \bar k = k^\alpha + # $$ + + assert iseq(c0.kbar, c0.x**(1/2) * c0.y**(1/2)) + assert iseq(c1.kbar, c1.x**(2/3) * c1.y**(1/3)) + assert iseq(c2.kbar, c1.x**(1/3) * c1.y**(2/3)) + + assert iseq(c0.kbar, c0.k**c0.alpha) + assert iseq(c1.kbar, c1.k**c1.alpha) + assert iseq(c2.kbar, c2.k**c2.alpha) + + # #### Token balance function $y(x)$ + # + # $$ + # y(x) = \left( \frac k x \right)^\eta + # $$ + + assert c0.eta == 1 + assert iseq(c0.yfromx_f(100, ignorebounds=True), 200) + assert iseq(c0.yfromx_f( 50, ignorebounds=True), 400) + assert iseq(c0.yfromx_f(200, ignorebounds=True), 100) + + assert iseq(c1.eta, 2) + assert iseq(c1.yfromx_f(100, ignorebounds=True), 200) + assert iseq(c1.yfromx_f( 50, ignorebounds=True), 200*2**2) + assert iseq(c1.yfromx_f(200, ignorebounds=True), 200/2**2) + + assert iseq(c2.eta, 1/2) + assert iseq(c2.yfromx_f(100, ignorebounds=True), 200) + assert iseq(c2.yfromx_f( 50, ignorebounds=True), 200*sqrt(2)) + assert iseq(c2.yfromx_f(200, ignorebounds=True), 200/sqrt(2)) + + # #### Token balance function $x(y)$ + # + # $$ + # x(y) + # = \frac{k}{ y^{\frac{1-\alpha}{\alpha}} } + # = \frac{k}{ y^{\frac{1}{\eta}} } + # $$ + + assert c0.eta == 1 + assert iseq(c0.xfromy_f(200, ignorebounds=True), 100) + assert iseq(c0.xfromy_f(100, ignorebounds=True), 200) + assert iseq(c0.xfromy_f(400, ignorebounds=True), 50) + + assert iseq(c1.eta, 2) + assert iseq(c1.xfromy_f(200, ignorebounds=True), 100) + assert iseq(c1.xfromy_f(100, ignorebounds=True), 100*2**0.5) + assert iseq(c1.xfromy_f(400, ignorebounds=True), 100/2**0.5) + + assert iseq(c2.eta, 1/2) + assert iseq(c2.xfromy_f(200, ignorebounds=True), 100) + assert iseq(c2.xfromy_f(100, ignorebounds=True), 100*2**2) + assert iseq(c2.xfromy_f(400, ignorebounds=True), 100/2**2) + + # #### Price response function $(x(p), y(p))$ + # + # $$ + # x(p) + # = + # \left(\frac \eta p\right)^{1-\alpha} k^\alpha + # $$ + # + # $$ + # y(p) = \left( \frac{kp}{\eta} \right)^\alpha + # $$ + + assert iseq(c0.xyfromp_f(c0.p, ignorebounds=True)[0], c0.x) + assert iseq(c1.xyfromp_f(c1.p, ignorebounds=True)[0], c1.x) + assert iseq(c2.xyfromp_f(c2.p, ignorebounds=True)[0], c2.x) + + assert iseq(c0.xyfromp_f(c0.p, ignorebounds=True)[1], c0.y) + assert iseq(c1.xyfromp_f(c1.p, ignorebounds=True)[1], c1.y) + assert iseq(c2.xyfromp_f(c2.p, ignorebounds=True)[1], c2.y) + + for ci in [c0, c1, c2]: + for p in [2, 1, 4]: + eta_over_p = ci.eta / p + x = eta_over_p ** (1-ci.alpha) * ci.kbar + y = 1/eta_over_p**ci.alpha * ci.kbar + xx, yy, pp = ci.xyfromp_f (p, ignorebounds=True) + dx, dy, _ = ci.dxdyfromp_f(p, ignorebounds=True) + assert iseq(x, xx) + assert iseq(y, yy) + assert iseq(p, pp) + assert iseq(dx, xx-ci.x) + assert iseq(dy, yy-ci.y) + + +# ------------------------------------------------------------ +# Test 049 +# File test_049_CPCBalancer.py +# Segment Consistency tests +# ------------------------------------------------------------ +def test_consistency_tests(): +# ------------------------------------------------------------ + + c0 = CPC.from_xyal(100, 200) + c1 = CPC.from_xyal(100, 200, eta=2) + c2 = CPC.from_xyal(100, 200, eta=0.5) + cc = [c0, c1, c2] + + assert iseq(c0.alpha, 1/2) + assert iseq(c1.alpha, 2/3) + assert iseq(c2.alpha, 1/3) + + # ### Assert inversions + # + # $$ + # y(x(y)) = y + # $$ + # + # and vice versa + + for xy in np.logspace(1, 3, 100): + for ci in cc: + #print(f"xy={xy}, eta={ci.eta}") + assert iseq(ci.yfromx_f(ci.xfromy_f(xy, ignorebounds=True), ignorebounds=True), xy) + assert iseq(ci.xfromy_f(ci.yfromx_f(xy, ignorebounds=True), ignorebounds=True), xy) + + # ### Assert that prices are correct + # + # $$ + # p \simeq -\frac{\Delta y}{\Delta x} + # $$ + + for alpha in np.linspace(0.01, 0.99, 100): + ci = CPC.from_xyal(100, 200, alpha=alpha) + dy = ci.yfromx_f(ci.x+0.1, ignorebounds=True)-ci.yfromx_f(ci.x-0.1, ignorebounds=True) + assert iseq(dy/0.2, -ci.p, eps=1e-2), f"error: {dy/0.2/ci.p+1}" + + # ### Check `dyfromdx_f` against `yfromx_f` + + for dxy in np.linspace(0.1, 99, 100): + for ci in cc: + assert iseq(ci.dyfromdx_f(dxy, ignorebounds=True), + (ci.yfromx_f(ci.x+dxy, ignorebounds=True)-ci.y)) + assert iseq(ci.dxfromdy_f(dxy, ignorebounds=True), + (ci.xfromy_f(ci.y+dxy, ignorebounds=True)-ci.x)) + + +# ------------------------------------------------------------ +# Test 049 +# File test_049_CPCBalancer.py +# Segment Charts [NOTEST] +# ------------------------------------------------------------ +def notest_charts(): +# ------------------------------------------------------------ + + plt.style.use('seaborn-v0_8-dark') + plt.rcParams['figure.figsize'] = [12,6] # only picked up at second run (?!?) + + c0 = CPC.from_xyal(100, 200) + c1 = CPC.from_xyal(100, 200, eta=2) + c2 = CPC.from_xyal(100, 200, eta=0.5) + cc = [c0, c1, c2] + xvals = np.linspace(50,200) + pvals = np.linspace(1,4) + + for ci in cc: + plt.plot(xvals, [ci.yfromx_f(x, ignorebounds=True) for x in xvals], label=f"eta={ci.eta:0.2f}") + plt.grid() + plt.legend() + plt.title("Indifference curve token balance y vs token balance x at different weights") + plt.xlabel("Token balance x [native units]") + plt.ylabel("Token balance y [native units]") + plt.show() + + for ci in cc: + plt.plot( + xvals, + [ + -(ci.yfromx_f(x+0.1, ignorebounds=True) - ci.yfromx_f(x-0.1, ignorebounds=True))/0.2 + for x in xvals + + ], + label=f"eta={ci.eta:0.2f}") + plt.grid() + plt.legend() + plt.title("Price vs token balance x at different weights") + plt.xlabel("Token balance x [native units]") + plt.ylabel("Price [dy/dx]") + plt.show() + + for ci in cc: + plt.plot( + pvals, + [ + ci.xyfromp_f(p, ignorebounds=True)[1] + for p in pvals + + ], + label=f"eta={ci.eta:0.2f}") + plt.grid() + plt.legend() + plt.title("Token balance y vs price at different weights") + plt.xlabel("Price [dy/dx]") + plt.ylabel("Token balance y [native units]") + plt.show() + + + + \ No newline at end of file diff --git a/fastlane_bot/tests/nbtest/test_049_CustomTradingFees.py b/fastlane_bot/tests/nbtest/test_049_CustomTradingFees.py new file mode 100644 index 000000000..6c7db1998 --- /dev/null +++ b/fastlane_bot/tests/nbtest/test_049_CustomTradingFees.py @@ -0,0 +1,150 @@ +# ------------------------------------------------------------ +# Auto generated test file `test_049_CustomTradingFees.py` +# ------------------------------------------------------------ +# source file = NBTest_049_CustomTradingFees.py +# test id = 049 +# test comment = CustomTradingFees +# ------------------------------------------------------------ + + + +from unittest.mock import Mock, patch, call + +import brownie +import pytest +from unittest.mock import MagicMock +from brownie import multicall as brownie_multicall + +from fastlane_bot import Bot, Config +from fastlane_bot.events.exchanges import UniswapV2, UniswapV3, SushiswapV2, CarbonV1, BancorV3 +from fastlane_bot.events.managers.manager import Manager +Base = None +from fastlane_bot.tools.cpc import ConstantProductCurve as CPC + +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(SushiswapV2)) +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__) + + +# + +import json + +with open("fastlane_bot/data/event_test_data.json", "r") as f: + event_data = json.load(f) + +with open("fastlane_bot/data/test_pool_data.json", "r") as f: + pool_data = json.load(f) + + +cfg = Config.new(config=Config.CONFIG_MAINNET) + +manager = Manager(cfg.w3, cfg, pool_data, 20, SUPPORTED_EXCHANGES=['bancor_v3', 'carbon_v1', 'uniswap_v2', 'uniswap_v3']) + + + +# ------------------------------------------------------------ +# Test 049 +# File test_049_CustomTradingFees.py +# Segment test_update_from_event_carbon_v1_pair_create +# ------------------------------------------------------------ +def test_test_update_from_event_carbon_v1_pair_create(): +# ------------------------------------------------------------ + + # + + event = event_data['carbon_v1_event_pair_created'] + assert (event['args']['token0'], event['args']['token1']) not in manager.fee_pairs + + manager.update_from_event(event) + + assert (event['args']['token0'], event['args']['token1']) in manager.fee_pairs + + # - + + +# ------------------------------------------------------------ +# Test 049 +# File test_049_CustomTradingFees.py +# Segment test_update_from_event_carbon_v1_trading_fee_updated +# ------------------------------------------------------------ +def test_test_update_from_event_carbon_v1_trading_fee_updated(): +# ------------------------------------------------------------ + # + + # + + event = event_data['carbon_v1_trading_fee_updated'] + prevFeePPM = event['args']['prevFeePPM'] + newFeePPM = event['args']['newFeePPM'] + + mocked_contract = Mock() + mocked_contract.functions.tradingFeePPM.return_value.call.return_value = prevFeePPM + assert int(manager.exchanges['carbon_v1'].get_fee('', mocked_contract)[0]) == prevFeePPM + + # find all pools with fee==prevFeePPM + prev_default_pools = [idx for idx, pool in enumerate(manager.pool_data) if pool['fee'] == prevFeePPM] + + manager.update_from_event(event) + + for idx in prev_default_pools: + assert manager.pool_data[idx]['fee'] == newFeePPM + + mocked_contract.functions.tradingFeePPM.return_value.call.return_value = newFeePPM + + assert int(manager.exchanges['carbon_v1'].get_fee('', mocked_contract)[0]) == newFeePPM + # - + + +# ------------------------------------------------------------ +# Test 049 +# File test_049_CustomTradingFees.py +# Segment test_update_from_event_carbon_v1_pair_trading_fee_updated +# ------------------------------------------------------------ +def test_test_update_from_event_carbon_v1_pair_trading_fee_updated(): +# ------------------------------------------------------------ + + # + + event = event_data['carbon_v1_pair_trading_fee_updated'] + prevFeePPM = event['args']['prevFeePPM'] + newFeePPM = event['args']['newFeePPM'] + token0 = event['args']['token0'] + token1 = event['args']['token1'] + + # set the fee for the pair to prevFeePPM + idxs = [idx for idx, pool in enumerate(manager.pool_data) if pool['tkn0_address'] == token0 and pool['tkn1_address'] == token1 and pool['exchange_name'] == 'carbon_v1'] + for idx in idxs: + manager.pool_data[idx]['fee'] = f"{prevFeePPM}" + manager.pool_data[idx]['fee_float'] = prevFeePPM / 1e6 + + # set all other pools with a different fee than prevFeePPM + others = [i for i, pool in enumerate(manager.pool_data) if i not in idxs and pool['exchange_name'] == 'carbon_v1'] + for i in others: + manager.pool_data[i]['fee'] = f"{prevFeePPM-1}" + manager.pool_data[i]['fee_float'] = (prevFeePPM-1) / 1e6 + + manager.update_from_event(event) + + # check that the fee for the pair is now newFeePPM + for idx in idxs: + assert manager.pool_data[idx]['fee'] == f"{newFeePPM}" + assert manager.pool_data[idx]['fee_float'] == newFeePPM / 1e6 + + # check that all other pools have not been changed + for i in others: + assert manager.pool_data[i]['fee'] == f"{prevFeePPM-1}" + assert manager.pool_data[i]['fee_float'] == (prevFeePPM-1) / 1e6 + + # - + + # + + # \ No newline at end of file diff --git a/fastlane_bot/tests/nbtest/test_900_OptimizerDetailedSlow.py b/fastlane_bot/tests/nbtest/test_900_OptimizerDetailedSlow.py index 7a4f1d147..10072420b 100644 --- a/fastlane_bot/tests/nbtest/test_900_OptimizerDetailedSlow.py +++ b/fastlane_bot/tests/nbtest/test_900_OptimizerDetailedSlow.py @@ -499,7 +499,7 @@ def test_general_and_specific_tests(): assert type(r) == ConvexOptimizer.NofeesOptimizerResult # assert round(r.result,-5) <= -1500000.0 # assert round(r.result,-5) >= -2500000.0 - assert r.time < 5 + # assert r.time < 8 assert r.method == "convex" assert set(r.token_table.keys()) == set(['USDT-1ec7', 'WETH-6Cc2', 'LINK-86CA', 'DAI-1d0F', 'HEX-eb39']) assert len(r.token_table[T.USDT].x)==0 diff --git a/fastlane_bot/tests/nbtest/test_902_ValidatorSlow.py b/fastlane_bot/tests/nbtest/test_902_ValidatorSlow.py deleted file mode 100644 index 0b5ac3fd1..000000000 --- a/fastlane_bot/tests/nbtest/test_902_ValidatorSlow.py +++ /dev/null @@ -1,283 +0,0 @@ -# ------------------------------------------------------------ -# Auto generated test file `test_902_ValidatorSlow.py` -# ------------------------------------------------------------ -# source file = NBTest_902_ValidatorSlow.py -# test id = 902 -# test comment = ValidatorSlow -# ------------------------------------------------------------ - - - -""" -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 -from fastlane_bot.tools.cpc import ConstantProductCurve as CPC -from fastlane_bot.events.exchanges import UniswapV2, UniswapV3, SushiswapV2, CarbonV1, BancorV3 -from fastlane_bot.events.interface import QueryInterface -from fastlane_bot.helpers.poolandtokens import PoolAndTokens -from fastlane_bot.helpers import TradeInstruction, TxReceiptHandler, TxRouteHandler, TxSubmitHandler, TxHelpers, TxHelper -from fastlane_bot.events.managers.manager import Manager -from fastlane_bot.events.interface import QueryInterface -from joblib import Parallel, delayed -import pytest -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(SushiswapV2)) -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 * -from fastlane_bot.modes import triangle_single_bancor3 -#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) -C.DEFAULT_MIN_PROFIT_BNT = 0.02 -C.DEFAULT_MIN_PROFIT = 0.02 -cfg.DEFAULT_MIN_PROFIT_BNT = 0.02 -cfg.DEFAULT_MIN_PROFIT = 0.02 -assert (C.NETWORK == C.NETWORK_MAINNET) -assert (C.PROVIDER == C.PROVIDER_ALCHEMY) -setup_bot = CarbonBot(ConfigObj=C) -pools = None -with open('fastlane_bot/data/tests/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, - 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.handle_token_key_cleanup() -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.key: (t.address, int(t.decimals)) for t in tokens if not math.isnan(t.decimals)} -flashloan_tokens = bot.setup_flashloan_tokens(None) -CCm = bot.setup_CCm(None) -pools = db.get_pool_data_with_tokens() - -arb_mode = "multi" - - -# ------------------------------------------------------------ -# Test 902 -# File test_902_ValidatorSlow.py -# Segment Test_MIN_PROFIT -# ------------------------------------------------------------ -def test_test_min_profit(): -# ------------------------------------------------------------ - - assert(cfg.DEFAULT_MIN_PROFIT_BNT <= 0.02), f"[TestMultiMode], DEFAULT_MIN_PROFIT_BNT must be <= 0.02 for this Notebook to run, currently set to {cfg.DEFAULT_MIN_PROFIT_BNT}" - assert(C.DEFAULT_MIN_PROFIT_BNT <= 0.02), f"[TestMultiMode], DEFAULT_MIN_PROFIT_BNT must be <= 0.02 for this Notebook to run, currently set to {cfg.DEFAULT_MIN_PROFIT_BNT}" - - -# ------------------------------------------------------------ -# Test 902 -# File test_902_ValidatorSlow.py -# Segment Test_validator_in_out -# ------------------------------------------------------------ -def test_test_validator_in_out(): -# ------------------------------------------------------------ - - 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 902 -# File test_902_ValidatorSlow.py -# Segment Test_validator_multi -# ------------------------------------------------------------ -def test_test_validator_multi(): -# ------------------------------------------------------------ - - # + - arb_finder = bot._get_arb_finder("multi") - finder = arb_finder( - flashloan_tokens=flashloan_tokens, - CCm=CCm, - mode="bothin", - result=bot.AO_CANDIDATES, - ConfigObj=bot.ConfigObj, - ) - r = finder.find_arbitrage() - - arb_opp = r[0] - - validated = bot.validate_optimizer_trades(arb_opp=arb_opp, arb_mode="multi", arb_finder=finder) - - - - assert arb_opp == validated - - # - - - -# ------------------------------------------------------------ -# Test 902 -# File test_902_ValidatorSlow.py -# Segment Test_validator_single -# ------------------------------------------------------------ -def test_test_validator_single(): -# ------------------------------------------------------------ - - # + - arb_mode="single" - arb_finder = bot._get_arb_finder(arb_mode) - finder = arb_finder( - flashloan_tokens=flashloan_tokens, - CCm=CCm, - mode="bothin", - result=bot.AO_CANDIDATES, - ConfigObj=bot.ConfigObj, - ) - r = finder.find_arbitrage() - - arb_opp = r[0] - - validated = bot.validate_optimizer_trades(arb_opp=arb_opp, arb_mode=arb_mode, arb_finder=finder) - - - assert arb_opp == validated - # - - - -# ------------------------------------------------------------ -# Test 902 -# File test_902_ValidatorSlow.py -# Segment Test_validator_bancor_v3 -# ------------------------------------------------------------ -def test_test_validator_bancor_v3(): -# ------------------------------------------------------------ - - # + - arb_mode="bancor_v3" - - arb_finder = bot._get_arb_finder(arb_mode) - finder = arb_finder( - flashloan_tokens=flashloan_tokens, - CCm=CCm, - mode="bothin", - result=bot.AO_CANDIDATES, - ConfigObj=bot.ConfigObj, - ) - r = finder.find_arbitrage() - - arb_opp = r[0] - - validated = bot.validate_optimizer_trades(arb_opp=arb_opp, arb_mode=arb_mode, arb_finder=finder) - - - - assert arb_opp != validated - # - - - -# ------------------------------------------------------------ -# Test 902 -# File test_902_ValidatorSlow.py -# Segment Test_validator_multi_triangle -# ------------------------------------------------------------ -def test_test_validator_multi_triangle(): -# ------------------------------------------------------------ - - # + - arb_mode="multi_triangle" - arb_finder = bot._get_arb_finder(arb_mode) - finder = arb_finder( - flashloan_tokens=flashloan_tokens, - CCm=CCm, - mode="bothin", - result=bot.AO_CANDIDATES, - ConfigObj=bot.ConfigObj, - ) - r = finder.find_arbitrage() - - arb_opp = r[0] - - validated = bot.validate_optimizer_trades(arb_opp=arb_opp, arb_mode=arb_mode, arb_finder=finder) - - - - assert arb_opp == validated \ No newline at end of file diff --git a/fastlane_bot/tests/nbtest/test_903_FlashloanTokens.py b/fastlane_bot/tests/nbtest/test_903_FlashloanTokens.py new file mode 100644 index 000000000..827e60b5d --- /dev/null +++ b/fastlane_bot/tests/nbtest/test_903_FlashloanTokens.py @@ -0,0 +1,100 @@ +# ------------------------------------------------------------ +# Auto generated test file `test_903_FlashloanTokens.py` +# ------------------------------------------------------------ +# source file = NBTest_903_FlashloanTokens.py +# test id = 903 +# test comment = FlashloanTokens +# ------------------------------------------------------------ + + + +""" +This module contains the tests which ensure the the flashloan_tokens parameter is respected when using the b3_two_hop and bancor_v3 arb modes. +""" +from fastlane_bot import Bot +from fastlane_bot.tools.cpc import ConstantProductCurve as CPC +from fastlane_bot.events.exchanges import UniswapV2, UniswapV3, SushiswapV2, CarbonV1, BancorV3 +import subprocess, os, sys +import pytest +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(SushiswapV2)) +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.rcParams['figure.figsize'] = [12,6] +from fastlane_bot import __VERSION__ +require("3.0", __VERSION__) + + + + +def find_main_py(): + # Start at the directory of the current script + cwd = os.path.abspath(os.path.join(os.getcwd())) + + with open("log.txt", "w") as f: + f.write(f"Searching for main.py in {cwd}") + + print(f"Searching for main.py in {cwd}") + while True: + # Check if main.py exists in the current directory + if "main.py" in os.listdir(cwd): + return cwd # Found the directory containing main.py + else: + # If not, go up one directory + new_cwd = os.path.dirname(cwd) + + # If we're already at the root directory, stop searching + if new_cwd == cwd: + raise FileNotFoundError("Could not find main.py in any parent directory") + + cwd = new_cwd + + +def run_command(mode): + + # Find the correct path to main.py + main_script_path = find_main_py() + print(f"Found main.py in {main_script_path}") + main_script_path = main_script_path + "/main.py" + + # Run the command + cmd = [ + "python", + main_script_path, + f"--arb_mode={mode}", + "--default_min_profit_bnt=60", + "--limit_bancor3_flashloan_tokens=True", + "--use_cached_events=True", + "--logging_path=fastlane_bot/data/", + "--timeout=45" + ] + subprocess.Popen(cmd) + + # Wait for the expected log line to appear + expected_log_line = "limiting flashloan_tokens to [" + found = False + result = subprocess.run(cmd, text=True, capture_output=True, check=True, timeout=120) + + # Check if the expected log line is in the output + if expected_log_line in result.stderr: + found = True + + if not found: + pytest.fail("Expected log line was not found within 1 minute") # If we reach this point, the test has failed + + + + +# ------------------------------------------------------------ +# Test 903 +# File test_903_FlashloanTokens.py +# Segment Test Flashloan Tokens b3_two_hop +# ------------------------------------------------------------ +def test_test_flashloan_tokens_b3_two_hop(): +# ------------------------------------------------------------ + + run_command("b3_two_hop") \ No newline at end of file diff --git a/fastlane_bot/tests/nbtest/test_904_Bancor3DataValidation.py b/fastlane_bot/tests/nbtest/test_904_Bancor3DataValidation.py new file mode 100644 index 000000000..4e6c2901b --- /dev/null +++ b/fastlane_bot/tests/nbtest/test_904_Bancor3DataValidation.py @@ -0,0 +1,98 @@ +# ------------------------------------------------------------ +# Auto generated test file `test_904_Bancor3DataValidation.py` +# ------------------------------------------------------------ +# source file = NBTest_904_Bancor3DataValidation.py +# test id = 904 +# test comment = Bancor3DataValidation +# ------------------------------------------------------------ + + + +""" +This module contains the tests which ensure that data validation checks always occur when running a bancor3-related arb_mode. +""" +from fastlane_bot import Bot +from fastlane_bot.tools.cpc import ConstantProductCurve as CPC +from fastlane_bot.events.exchanges import UniswapV2, UniswapV3, SushiswapV2, CarbonV1, BancorV3 +import subprocess, os, sys +import pytest +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(SushiswapV2)) +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.rcParams['figure.figsize'] = [12,6] +from fastlane_bot import __VERSION__ +require("3.0", __VERSION__) + + + + +def find_main_py(): + # Start at the directory of the current script + cwd = os.path.abspath(os.path.join(os.getcwd())) + + print(f"Searching for main.py in {cwd}") + while True: + # Check if main.py exists in the current directory + if "main.py" in os.listdir(cwd): + return cwd # Found the directory containing main.py + else: + # If not, go up one directory + new_cwd = os.path.dirname(cwd) + + # If we're already at the root directory, stop searching + if new_cwd == cwd: + raise FileNotFoundError("Could not find main.py in any parent directory") + + cwd = new_cwd + + +def run_command(arb_mode, expected_log_line): + + # Find the correct path to main.py + main_script_path = find_main_py() + print(f"Found main.py in {main_script_path}") + main_script_path = main_script_path + "/main.py" + + # Run the command + cmd = [ + "python", + main_script_path, + f"--arb_mode={arb_mode}", + "--default_min_profit_bnt=60", + "--limit_bancor3_flashloan_tokens=False", + "--use_cached_events=True", + "--logging_path=fastlane_bot/data/", + "--timeout=45" + ] + subprocess.Popen(cmd) + + # Wait for the expected log line to appear + found = False + result = subprocess.run(cmd, text=True, capture_output=True, check=True, timeout=120) + + # Check if the expected log line is in the output + if expected_log_line in result.stderr or expected_log_line in result.stdout: + found = True + + if not found: + pytest.fail("Expected log line was not found within 1 minute") # If we reach this point, the test has failed + + + + +# ------------------------------------------------------------ +# Test 904 +# File test_904_Bancor3DataValidation.py +# Segment Test Data Validation For b3_two_hop +# ------------------------------------------------------------ +def test_test_data_validation_for_b3_two_hop(): +# ------------------------------------------------------------ + + expected_log_line = "Transactions will be required to pass data validation for" + arb_mode = "b3_two_hop" + run_command(arb_mode=arb_mode, expected_log_line=expected_log_line) \ No newline at end of file diff --git a/fastlane_bot/tests/nbtest/test_905_RespectMinProfitClickParam.py b/fastlane_bot/tests/nbtest/test_905_RespectMinProfitClickParam.py new file mode 100644 index 000000000..f878b3974 --- /dev/null +++ b/fastlane_bot/tests/nbtest/test_905_RespectMinProfitClickParam.py @@ -0,0 +1,99 @@ +# ------------------------------------------------------------ +# Auto generated test file `test_905_RespectMinProfitClickParam.py` +# ------------------------------------------------------------ +# source file = NBTest_905_RespectMinProfitClickParam.py +# test id = 905 +# test comment = RespectMinProfitClickParam +# ------------------------------------------------------------ + + + +""" +This module contains the tests which ensure that the minimum profit BNT parameter is respected. +""" +from fastlane_bot import Bot +from fastlane_bot.tools.cpc import ConstantProductCurve as CPC +from fastlane_bot.events.exchanges import UniswapV2, UniswapV3, SushiswapV2, CarbonV1, BancorV3 +import subprocess, os, sys +import pytest +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(SushiswapV2)) +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.rcParams['figure.figsize'] = [12,6] +from fastlane_bot import __VERSION__ +require("3.0", __VERSION__) + + + + +def find_main_py(): + # Start at the directory of the current script + cwd = os.path.abspath(os.path.join(os.getcwd())) + + print(f"Searching for main.py in {cwd}") + while True: + # Check if main.py exists in the current directory + if "main.py" in os.listdir(cwd): + return cwd # Found the directory containing main.py + else: + # If not, go up one directory + new_cwd = os.path.dirname(cwd) + + # If we're already at the root directory, stop searching + if new_cwd == cwd: + raise FileNotFoundError("Could not find main.py in any parent directory") + + cwd = new_cwd + + +def run_command(arb_mode, expected_log_line): + + # Find the correct path to main.py + main_script_path = find_main_py() + print(f"Found main.py in {main_script_path}") + main_script_path = main_script_path + "/main.py" + + # Run the command + cmd = [ + "python", + main_script_path, + f"--arb_mode={arb_mode}", + "--default_min_profit_bnt=60", + "--limit_bancor3_flashloan_tokens=False", + "--use_cached_events=True", + "--logging_path=fastlane_bot/data/", + "--timeout=45", + "--loglevel=DEBUG", + ] + subprocess.Popen(cmd) + + # Wait for the expected log line to appear + found = False + result = subprocess.run(cmd, text=True, capture_output=True, check=True, timeout=120) + + # Check if the expected log line is in the output + if expected_log_line in result.stderr or expected_log_line in result.stdout: + found = True + + if not found: + pytest.fail("Expected log line was not found within 1 minute") # If we reach this point, the test has failed + + + + +# ------------------------------------------------------------ +# Test 905 +# File test_905_RespectMinProfitClickParam.py +# Segment Test Minimum Profit BNT Is Respected +# ------------------------------------------------------------ +def test_test_minimum_profit_bnt_is_respected(): +# ------------------------------------------------------------ + + expected_log_line = "Bot successfully updated min profit" + arb_mode = "multi" + run_command(arb_mode=arb_mode, expected_log_line=expected_log_line) \ No newline at end of file diff --git a/fastlane_bot/tests/nbtest/test_906_TargetTokens.py b/fastlane_bot/tests/nbtest/test_906_TargetTokens.py new file mode 100644 index 000000000..e7852d1a9 --- /dev/null +++ b/fastlane_bot/tests/nbtest/test_906_TargetTokens.py @@ -0,0 +1,101 @@ +# ------------------------------------------------------------ +# Auto generated test file `test_906_TargetTokens.py` +# ------------------------------------------------------------ +# source file = NBTest_906_TargetTokens.py +# test id = 906 +# test comment = TargetTokens +# ------------------------------------------------------------ + + + +""" +This module contains the tests which ensure the target_tokens parameter is respected. +""" +from fastlane_bot import Bot +from fastlane_bot.tools.cpc import ConstantProductCurve as CPC +from fastlane_bot.events.exchanges import UniswapV2, UniswapV3, SushiswapV2, CarbonV1, BancorV3 +import subprocess, os, sys +import pytest +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(SushiswapV2)) +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.rcParams['figure.figsize'] = [12,6] +from fastlane_bot import __VERSION__ +require("3.0", __VERSION__) + + + +from fastlane_bot.tools.cpc import T + + +def find_main_py(): + # Start at the directory of the current script + cwd = os.path.abspath(os.path.join(os.getcwd())) + + with open("log.txt", "w") as f: + f.write(f"Searching for main.py in {cwd}") + + print(f"Searching for main.py in {cwd}") + while True: + # Check if main.py exists in the current directory + if "main.py" in os.listdir(cwd): + return cwd # Found the directory containing main.py + else: + # If not, go up one directory + new_cwd = os.path.dirname(cwd) + + # If we're already at the root directory, stop searching + if new_cwd == cwd: + raise FileNotFoundError("Could not find main.py in any parent directory") + + cwd = new_cwd + + +def run_command(mode): + + # Find the correct path to main.py + main_script_path = find_main_py() + print(f"Found main.py in {main_script_path}") + main_script_path = main_script_path + "/main.py" + + # Run the command + cmd = [ + "python", + main_script_path, + f"--arb_mode={mode}", + "--use_cached_events=True", + "--logging_path=fastlane_bot/data/", + "--timeout=45", + f"--target_tokens={T.WETH},{T.DAI}" + ] + subprocess.Popen(cmd) + + # Wait for the expected log line to appear + expected_log_line = "Limiting pools by target_tokens. Removed " + found = False + result = subprocess.run(cmd, text=True, capture_output=True, check=True, timeout=120) + + # Check if the expected log line is in the output + if expected_log_line in result.stderr: + found = True + + if not found: + pytest.fail("Expected log line was not found within 1 minute") # If we reach this point, the test has failed + + + + +# ------------------------------------------------------------ +# Test 906 +# File test_906_TargetTokens.py +# Segment Test Flashloan Tokens b3_two_hop +# ------------------------------------------------------------ +def test_test_flashloan_tokens_b3_two_hop(): +# ------------------------------------------------------------ + + run_command("single") \ No newline at end of file diff --git a/fastlane_bot/tools/cpc.py b/fastlane_bot/tools/cpc.py index 116163537..e00cd5c3e 100644 --- a/fastlane_bot/tools/cpc.py +++ b/fastlane_bot/tools/cpc.py @@ -7,8 +7,8 @@ NOTE: this class is not part of the API of the Carbon protocol, and you must expect breaking changes even in minor version updates. Use at your own risk. """ -__VERSION__ = "2.14" -__DATE__ = "23/May/2023" +__VERSION__ = "3.0" +__DATE__ = "22/Aug/2023" from dataclasses import dataclass, field, asdict, InitVar from .simplepair import SimplePair as Pair @@ -351,10 +351,12 @@ class ConstantProductCurve: """ represents a, potentially levered, constant product curve - :k: pool constant k = xy [x=k/y, y=k/x] + :k: pool invariant k (see NOTE2 below) :x: (virtual) pool state x (virtual number of base tokens for sale) :x_act: actual pool state x (actual number of base tokens for sale) :y_act: actual pool state y (actual number of quote tokens for sale) + :alpha: weight factor alpha of token x (default = 0.5; see NOTE3 below) + :eta: portfolio weight factor eta (default = 1; see NOTE3 below) :pair: token pair in slash notation ("TKNB/TKNQ"); TKNB is on the x-axis, TKNQ on the y-axis :cid: unique id (optional) :fee: fee (optional); eg 0.01 for 1% @@ -362,7 +364,20 @@ class ConstantProductCurve: :constr: which (alternative) constructor was used (optional; user should not set) :params: additional parameters (optional) - NOTE: use the alternative constructors `from_xx` rather then the canonical one + NOTE1: always use the alternative constructors `from_xx` rather then the + canonical one; if you insist on using the canonical one then keep in mind + that the order of the parameters may change in future versions, so you + MUST use keyword arguments + + NOTE2: This class implements two distinct types of constant product curves: + (1) the standard constant product curve xy=k + (2) the weighted constant product curve x^al y^1-al = k^al + Note that the case alpha=0.5 is equivalent to the standard constant product curve + xy=k, including the value of k + + NOTE3: There are two different ways of specifying the weights of the tokens + (1) alpha: the weight of the x token (equal weight = 0.5), such that x^al y^1-al = k^al + (2) eta = alpha / (1-alpha): the relative weight (equal weight = 1; x overweight > 1) """ __VERSION__ = __VERSION__ @@ -372,6 +387,7 @@ class ConstantProductCurve: x: float x_act: float = None y_act: float = None + alpha: float = None pair: str = None cid: str = None fee: float = None @@ -380,6 +396,16 @@ class ConstantProductCurve: params: AttrDict = field(default=None, repr=True, compare=False, hash=False) def __post_init__(self): + + if self.alpha is None: + super().__setattr__("_is_constant_product", True) + super().__setattr__("alpha", 0.5) + else: + super().__setattr__("_is_constant_product", self.alpha == 0.5) + #print(f"[ConstantProductCurve] _is_constant_product = {self._is_constant_product}") + assert self.alpha > 0, f"alpha must be > 0 [{self.alpha}]" + assert self.alpha < 1, f"alpha must be < 1 [{self.alpha}]" + if self.constr is None: super().__setattr__("constr", "default") @@ -440,6 +466,15 @@ def cid0(self): "short cid [last 8 characters]" return self.cid[-8:] + @property + def eta(self): + "portfolio weight factor eta = alpha / (1-alpha)" + return self.alpha / (1 - self.alpha) + + def is_constant_product(self): + "True iff alpha == 0.5" + return self._is_constant_product + TOKENSCALE = ts.TokenScale1Data # default token scale object is the trivial scale (everything one) # change this to a different scale object be creating a derived class @@ -469,9 +504,11 @@ def asdict(self): return asdict(self) @classmethod - def from_dict(cls, d): + def fromdict(cls, d): "returns a curve from a dict representation" return cls(**d) + + from_dict = fromdict # DEPRECATED (use fromdict) def setcid(self, cid): """sets the curve id [can only be done once]""" @@ -562,6 +599,50 @@ def from_xy( params=params, ) + @classmethod + def from_xyal( + cls, + x, + y, + *, + alpha=None, + eta=None, + x_act=None, + y_act=None, + pair=None, + cid=None, + fee=None, + descr=None, + params=None, + ): + "constructor: from x,y,alpha/eta (and x_act, y_act)" + if not alpha is None and not eta is None: + raise ValueError(f"at most one of alpha and eta must be given [{alpha}, {eta}]") + if not eta is None: + alpha = eta / (eta + 1) + if alpha is None: + alpha = 0.5 + assert alpha > 0, f"alpha must be > 0 [{alpha}]" + eta_inv = (1-alpha) / alpha + k = x * (y**eta_inv) + #print(f"[from_xyal] eta_inv = {eta_inv}") + #print(f"[from_xyal] x={x}, y={y}, k = {k}") + return cls( + #k=(x**alpha * y**(1-alpha))**(1/alpha), + k=k, + x=x, + alpha=alpha, + x_act=x_act, + y_act=y_act, + pair=pair, + cid=cid, + fee=fee, + descr=descr, + constr="xyal", + params=params, + ) + + @classmethod def from_pk( cls, @@ -915,6 +996,8 @@ def execute(self, dx=None, dy=None, *, ignorebounds=False, verbose=False): *at least one of dx, dy must be None """ + assert self.is_constant_product(), "only implemented for constant product curves" + if not dx is None and not dy is None: raise ValueError(f"either dx or dy must be None dx={dx} dy={dy}") @@ -995,6 +1078,8 @@ def pairp(self): def description(self): "description of the pool" + assert self.is_constant_product(), "only implemented for constant product curves" + s = "" s += f"cid = {self.cid0} [{self.cid}]\n" s += f"primary = {Pair.n(self.pairo.primary)} [{self.pairo.primary}]\n" @@ -1010,15 +1095,21 @@ def description(self): @property def y(self): "(virtual) pool state x (virtual number of base tokens for sale)" + if self.k == 0: return 0 - return self.k / self.x - + if self.is_constant_product(): + return self.k / self.x + return (self.k / self.x)**(self.eta) + @property def p(self): "pool price (in dy/dx)" - return self.y / self.x - + if self.is_constant_product(): + return self.y / self.x + + return self.eta * self.y / self.x + def buysell(self, *, verbose=False, withprice=False): """ returns b (buy primary tknb), s (sells primary tknb) or bs (buys and sells) @@ -1081,6 +1172,8 @@ def itm(self, other, *, thresholdpc=None, aggr=True): :thresholdpc: in-the-money threshold in percent (default: ITM_THRESHOLD) :aggr: if True, and an iterable is passed, True iff one is in the money """ + assert self.is_constant_product(), "only implemented for constant product curves" + try: itm_t = tuple(self.itm(o) for o in other) if not aggr: @@ -1103,7 +1196,6 @@ def tvl(self, tkn=None, *, mult=1.0, incltkn=False, raiseonerror=True): :raiseonerror: if True, raises ValueError if tkn is not tknb or tknq :returns: tvl (in tkn) or (tvl, tkn, mult) if incltkn is True """ - if tkn is None: tkn = self.tknq if not tkn in {self.tknb, self.tknq}: @@ -1145,12 +1237,21 @@ def pp(self): @property def kbar(self): - "kbar = sqrt(k); kbar scales linearly with the pool size" - return sqrt(self.k) + """ + kbar is pool invariant the scales linearly with the pool size + + kbar = sqrt(k) for constant product + kbar = k^alpha for general curves + """ + if self.is_constant_product(): + return sqrt(self.k) + return self.k**self.alpha @property def x_min(self): "minimum (virtual) x value" + assert self.is_constant_product(), "only implemented for constant product curves" + return self.x - self.x_act @property @@ -1179,11 +1280,15 @@ def at_boundary(self): @property def y_min(self): "minimum (virtual) y value" + assert self.is_constant_product(), "only implemented for constant product curves" + return self.y - self.y_act @property def x_max(self): "maximum (virtual) x value" + assert self.is_constant_product(), "only implemented for constant product curves" + if self.y_min > 0: return self.k / self.y_min else: @@ -1192,6 +1297,8 @@ def x_max(self): @property def y_max(self): "maximum (virtual) y value" + assert self.is_constant_product(), "only implemented for constant product curves" + if self.x_min > 0: return self.k / self.x_min else: @@ -1200,6 +1307,8 @@ def y_max(self): @property def p_max(self): "maximum pool price (in dy/dx; None if unlimited) = y_max/x_min" + assert self.is_constant_product(), "only implemented for constant product curves" + if not self.x_min is None and self.x_min > 0: return self.y_max / self.x_min else: @@ -1214,6 +1323,8 @@ def p_max_primary(self, swap=True): @property def p_min(self): "minimum pool price (in dy/dx; None if unlimited) = y_min/x_max" + assert self.is_constant_product(), "only implemented for constant product curves" + if not self.x_max is None and self.x_max > 0: return self.y_min / self.x_max else: @@ -1227,6 +1338,8 @@ def p_min_primary(self, swap=True): def format(self, *, heading=False, formatid=None): """returns info about the curve as a formatted string""" + assert self.is_constant_product(), "only implemented for constant product curves" + if formatid is None: formatid = 0 assert formatid in [0], "only formatid in [0] is supported" @@ -1244,20 +1357,34 @@ def format(self, *, heading=False, formatid=None): return s def xyfromp_f(self, p=None, *, ignorebounds=False, withunits=False): - """ - returns x,y for a given marginal price p (stuck at the boundaries if ignorebounds=False) + r""" + returns x,y,p for a given marginal price p (stuck at the boundaries if ignorebounds=False) :p: marginal price (in dy/dx) :ignorebounds: if True, ignore x_act and y_act; if False, return the x,y values where x_act and y_act are at zero (i.e. the pool is empty in this direction) :withunits: if False, return x,y,p; if True, also return tknx, tkny, pair + + + $$ + x(p) = \left( \frac{\eta}{p} \right) ^ {1-\alpha} k^\alpha + y(p) = \left( \frac{p}{\eta} \right) ^ \alpha k^\alpha + $$ """ if p is None: p = self.p - sqrt_p = sqrt(p) - sqrt_k = self.kbar - x = sqrt_k / sqrt_p - y = sqrt_k * sqrt_p + + if self.is_constant_product(): + sqrt_p = sqrt(p) + sqrt_k = self.kbar + x = sqrt_k / sqrt_p + y = sqrt_k * sqrt_p + else: + eta = self.eta + alpha = self.alpha + x = (eta/p)**(1-alpha) * self.kbar + y = (p/eta)**alpha * self.kbar + if not ignorebounds: if not self.x_min is None: if x < self.x_min: @@ -1278,7 +1405,7 @@ def xyfromp_f(self, p=None, *, ignorebounds=False, withunits=False): return x, y, p def dxdyfromp_f(self, p=None, *, ignorebounds=False, withunits=False): - """like xyfromp_f, but returns dx,dy instead of x,y""" + """like xyfromp_f, but returns dx,dy,p instead of x,y,p""" x, y, p = self.xyfromp_f(p, ignorebounds=ignorebounds) dx = x - self.x dy = y - self.y @@ -1288,7 +1415,11 @@ def dxdyfromp_f(self, p=None, *, ignorebounds=False, withunits=False): def yfromx_f(self, x, *, ignorebounds=False): "y value for given x value (if in range; None otherwise)" - y = self.k / x + if self.is_constant_product(): + y = self.k / x + else: + y = (self.k / x) ** self.eta + if ignorebounds: return y if not self.inrange(y, self.y_min, self.y_max): @@ -1297,7 +1428,10 @@ def yfromx_f(self, x, *, ignorebounds=False): def xfromy_f(self, y, *, ignorebounds=False): "x value for given y value (if in range; None otherwise)" - x = self.k / y + if self.is_constant_product(): + x = self.k / y + else: + x = self.k / (y ** (1/self.eta)) if ignorebounds: return x if not self.inrange(x, self.x_min, self.x_max): diff --git a/fastlane_bot/tools/optimizer/margpoptimizer.py b/fastlane_bot/tools/optimizer/margpoptimizer.py index 6b384a1b2..68834d99c 100644 --- a/fastlane_bot/tools/optimizer/margpoptimizer.py +++ b/fastlane_bot/tools/optimizer/margpoptimizer.py @@ -214,7 +214,11 @@ def dtknfromp_f(p, *, islog10=True, asdct=False, quiet=False): sum_by_tkn = {t: 0 for t in alltokens_s} for pair, (tknb, tknq) in zip(pairs, pairs_t): - price = get(p, tokens_ix.get(tknb)) / get(p, tokens_ix.get(tknq)) + if get(p, tokens_ix.get(tknq)) > 0: + price = get(p, tokens_ix.get(tknb)) / get(p, tokens_ix.get(tknq)) + else: + #print(f"[dtknfromp_f] warning: price for {pair} is unknown, using 1 instead") + price = 1 curves = curves_by_pair[pair] c0 = curves[0] dxdy = tuple(dxdy_f(c.dxdyfromp_f(price)) for c in curves) diff --git a/resources/NBTest/NBTest_003_Serialization.ipynb b/resources/NBTest/NBTest_003_Serialization.ipynb index 62f857786..0788a5f6a 100644 --- a/resources/NBTest/NBTest_003_Serialization.ipynb +++ b/resources/NBTest/NBTest_003_Serialization.ipynb @@ -10,8 +10,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "ConstantProductCurve v2.14 (23/May/2023)\n", - "CPCArbOptimizer v4.0 (10/May/2023)\n", + "ConstantProductCurve v3.0 (22/Aug/2023)\n", + "CPCArbOptimizer v5.0 (26/Jul/2023)\n", "imported m, np, pd, plt, os, sys, decimal; defined iseq, raises, require\n", "Version = 3-b2.2 [requirements >= 2.0 is met]\n" ] @@ -50,324 +50,61 @@ { "cell_type": "code", "execution_count": 2, - "id": "8cb4f9bc-2f31-4eae-b77f-533aa188e49b", + "id": "4030cea3-3e03-4e0f-8d80-7a2bcca05fcf", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
kxx_acty_actpairfeedescrconstrparams
cid
None2000112000.0ETH/USDCNoneNonexy{}
None2200112200.0ETH/USDCNoneNonexy{}
None2400112400.0ETH/USDCNoneNonexy{}
None2000112000.0ETH/USDCNoneNonexy{}
None2200112200.0ETH/USDCNoneNonexy{}
None2400112400.0ETH/USDCNoneNonexy{}
None2000112000.0ETH/USDCNoneNonexy{}
None2200112200.0ETH/USDCNoneNonexy{}
None2400112400.0ETH/USDCNoneNonexy{}
None2000112000.0ETH/USDCNoneNonexy{}
None2200112200.0ETH/USDCNoneNonexy{}
None2400112400.0ETH/USDCNoneNonexy{}
None2000112000.0ETH/USDCNoneNonexy{}
None2200112200.0ETH/USDCNoneNonexy{}
None2400112400.0ETH/USDCNoneNonexy{}
\n", - "
" - ], - "text/plain": [ - " k x x_act y_act pair fee descr constr params\n", - "cid \n", - "None 2000 1 1 2000.0 ETH/USDC None None xy {}\n", - "None 2200 1 1 2200.0 ETH/USDC None None xy {}\n", - "None 2400 1 1 2400.0 ETH/USDC None None xy {}\n", - "None 2000 1 1 2000.0 ETH/USDC None None xy {}\n", - "None 2200 1 1 2200.0 ETH/USDC None None xy {}\n", - "None 2400 1 1 2400.0 ETH/USDC None None xy {}\n", - "None 2000 1 1 2000.0 ETH/USDC None None xy {}\n", - "None 2200 1 1 2200.0 ETH/USDC None None xy {}\n", - "None 2400 1 1 2400.0 ETH/USDC None None xy {}\n", - "None 2000 1 1 2000.0 ETH/USDC None None xy {}\n", - "None 2200 1 1 2200.0 ETH/USDC None None xy {}\n", - "None 2400 1 1 2400.0 ETH/USDC None None xy {}\n", - "None 2000 1 1 2000.0 ETH/USDC None None xy {}\n", - "None 2200 1 1 2200.0 ETH/USDC None None xy {}\n", - "None 2400 1 1 2400.0 ETH/USDC None None xy {}" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "N=5\n", - "curves = [\n", - " CPC.from_xy(x=1, y=2000, pair=\"ETH/USDC\"),\n", - " CPC.from_xy(x=1, y=2200, pair=\"ETH/USDC\"),\n", - " CPC.from_xy(x=1, y=2400, pair=\"ETH/USDC\"),\n", - "]\n", - "# note: the below is a bit icky as the same curve objects are added multiple times\n", - "CC = CPCContainer(curves*N)\n", - "O = CPCArbOptimizer(CC)\n", - "O.CC.asdf()" + "pass" ] }, { "cell_type": "code", "execution_count": 3, - "id": "a5ed0075-5ee5-4592-a192-e06d2b5af454", + "id": "8cb4f9bc-2f31-4eae-b77f-533aa188e49b", "metadata": {}, "outputs": [], "source": [ - "O.pickle(\"delme\")\n", - "O.pickle(\"delme\", addts=False)" + "# N=5\n", + "# curves = [\n", + "# CPC.from_xy(x=1, y=2000, pair=\"ETH/USDC\"),\n", + "# CPC.from_xy(x=1, y=2200, pair=\"ETH/USDC\"),\n", + "# CPC.from_xy(x=1, y=2400, pair=\"ETH/USDC\"),\n", + "# ]\n", + "# # note: the below is a bit icky as the same curve objects are added multiple times\n", + "# CC = CPCContainer(curves*N)\n", + "# O = CPCArbOptimizer(CC)\n", + "# O.CC.asdf()" ] }, { "cell_type": "code", "execution_count": 4, - "id": "1bf13d91-2bc0-4819-96b9-2712ef89b6f1", + "id": "a5ed0075-5ee5-4592-a192-e06d2b5af454", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "delme.169028648950.optimizer.pickle delme.optimizer.pickle\n" - ] - } - ], + "outputs": [], "source": [ - "!ls *.pickle" + "# O.pickle(\"delme\")\n", + "# O.pickle(\"delme\", addts=False)" ] }, { "cell_type": "code", "execution_count": 5, + "id": "1bf13d91-2bc0-4819-96b9-2712ef89b6f1", + "metadata": {}, + "outputs": [], + "source": [ + "# !ls *.pickle" + ] + }, + { + "cell_type": "code", + "execution_count": 6, "id": "ce05c578-5060-498e-b4eb-f55617d10cdd", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "O.unpickle(\"delme\")" + "# O.unpickle(\"delme\")" ] }, { @@ -397,17 +134,17 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 7, "id": "41a5cdfe-fb7b-4c8b-a270-1a52f0765e94", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "ConstantProductCurve(k=10000, x=100, x_act=100, y_act=100, pair='TKNB/TKNQ', cid='1', fee=0, descr='UniV2', constr='uv2', params={})" + "ConstantProductCurve(k=10000, x=100, x_act=100, y_act=100, alpha=0.5, pair='TKNB/TKNQ', cid='1', fee=0, descr='UniV2', constr='uv2', params={})" ] }, - "execution_count": 6, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -431,7 +168,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 8, "id": "ea3cdfbc-8edd-41f1-9703-0ae0d72fdb9a", "metadata": {}, "outputs": [ @@ -442,6 +179,7 @@ " 'x': 100,\n", " 'x_act': 100,\n", " 'y_act': 100,\n", + " 'alpha': 0.5,\n", " 'pair': 'TKNB/TKNQ',\n", " 'cid': '1',\n", " 'fee': 0,\n", @@ -450,7 +188,7 @@ " 'params': {}}" ] }, - "execution_count": 7, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -461,7 +199,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 9, "id": "595de023-5c66-40fc-928f-eca5fe6a50c9", "metadata": {}, "outputs": [], @@ -471,6 +209,7 @@ " 'x': 100,\n", " 'x_act': 100,\n", " 'y_act': 100,\n", + " 'alpha': 0.5,\n", " 'pair': 'TKNB/TKNQ',\n", " 'cid': \"1\",\n", " 'fee': 0,\n", @@ -482,7 +221,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 10, "id": "215b5105-08d9-4077-a51a-7658cafcffa9", "metadata": {}, "outputs": [], @@ -516,7 +255,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 11, "id": "0963034a-b36c-4cfb-84da-ccb3c88c4389", "metadata": {}, "outputs": [], @@ -534,7 +273,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 12, "id": "eb5dd380-dd90-4a3b-b88a-5a697bdbc3a0", "metadata": {}, "outputs": [], @@ -565,7 +304,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 13, "id": "624b80f1-c811-483b-ba24-b76c72fe3e0c", "metadata": {}, "outputs": [], @@ -580,7 +319,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "id": "34d52402-18d6-4485-8e5c-6cb4f8af2ab2", "metadata": {}, "outputs": [ @@ -604,7 +343,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 15, "id": "85175836-0fa9-4f64-a42f-b5b787e622f0", "metadata": {}, "outputs": [], @@ -619,7 +358,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 16, "id": "9753798a-b154-4865-a845-a1f5f1eb8e4b", "metadata": {}, "outputs": [ @@ -643,17 +382,17 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 17, "id": "5f683913-1799-4f3a-9473-a663d803448a", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "ConstantProductCurve(k=0.01, x=0.0015438708879488485, x_act=0, y_act=1, pair='ETH/USDC', cid='4', fee=0, descr='Carbon', constr='carb', params={'y': 1, 'yint': 1, 'A': 10, 'B': 54.772255750516614, 'pa': 4195.445115010333, 'pb': 3000.0000000000005})" + "ConstantProductCurve(k=0.01, x=0.0015438708879488485, x_act=0, y_act=1, alpha=0.5, pair='ETH/USDC', cid='4', fee=0, descr='Carbon', constr='carb', params={'y': 1, 'yint': 1, 'A': 10, 'B': 54.772255750516614, 'pa': 4195.445115010333, 'pb': 3000.0000000000005})" ] }, - "execution_count": 16, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -667,7 +406,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 18, "id": "cffdcaa4-f221-4bd7-bf2d-5418a33e3592", "metadata": {}, "outputs": [], @@ -691,7 +430,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 19, "id": "f66fc490-97e0-4c5e-958d-1e9014934d5c", "metadata": {}, "outputs": [], @@ -705,7 +444,7 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 20, "id": "465ff937-2382-4215-8e11-ec8096e1ea3d", "metadata": {}, "outputs": [], @@ -724,7 +463,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 21, "id": "c5c8d6c3-0d15-4c3d-8852-b2870a7b4caa", "metadata": {}, "outputs": [], @@ -740,7 +479,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 22, "id": "8296d087-d5a5-4b77-825a-dd53ed60d4bd", "metadata": {}, "outputs": [], @@ -758,7 +497,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 23, "id": "e72d0162-dd59-489c-8efb-dbb8327ff553", "metadata": {}, "outputs": [ @@ -826,7 +565,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 24, "id": "c2d5dc97-05e8-4eca-abc7-66eee6e7d706", "metadata": {}, "outputs": [], @@ -840,7 +579,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 25, "id": "9f467a32-370b-4634-bec8-3c28be84a0a0", "metadata": {}, "outputs": [], @@ -852,17 +591,17 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 26, "id": "d7563934-5381-476d-b9cb-99b909691049", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "CPCContainer(curves=[ConstantProductCurve(k=2000, x=1, x_act=1, y_act=2000, pair='ETH/USDC', cid='1', fee=0.001, descr='UniV2', constr='uv2', params={'meh': 1}), ConstantProductCurve(k=8040, x=2, x_act=2, y_act=4020, pair='ETH/USDC', cid='2', fee=0.001, descr='UniV2', constr='uv2', params={}), ConstantProductCurve(k=1970, x=1, x_act=1, y_act=1970, pair='ETH/USDC', cid='3', fee=0.001, descr='UniV2', constr='uv2', params={})])" + "CPCContainer(curves=[ConstantProductCurve(k=2000, x=1, x_act=1, y_act=2000, alpha=0.5, pair='ETH/USDC', cid='1', fee=0.001, descr='UniV2', constr='uv2', params={'meh': 1}), ConstantProductCurve(k=8040, x=2, x_act=2, y_act=4020, alpha=0.5, pair='ETH/USDC', cid='2', fee=0.001, descr='UniV2', constr='uv2', params={}), ConstantProductCurve(k=1970, x=1, x_act=1, y_act=1970, alpha=0.5, pair='ETH/USDC', cid='3', fee=0.001, descr='UniV2', constr='uv2', params={})])" ] }, - "execution_count": 25, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } @@ -880,112 +619,14 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "id": "131928b8-f927-4799-97c6-ec50631c7959", "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
kxx_acty_actpairfeedescrconstrparams
cid
12000112000ETH/USDC0.001UniV2uv2{'meh': 1}
28040224020ETH/USDC0.001UniV2uv2{}
31970111970ETH/USDC0.001UniV2uv2{}
\n", - "
" - ], - "text/plain": [ - " k x x_act y_act pair fee descr constr params\n", - "cid \n", - "1 2000 1 1 2000 ETH/USDC 0.001 UniV2 uv2 {'meh': 1}\n", - "2 8040 2 2 4020 ETH/USDC 0.001 UniV2 uv2 {}\n", - "3 1970 1 1 1970 ETH/USDC 0.001 UniV2 uv2 {}" - ] - }, - "execution_count": 26, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df = CC.asdf()\n", "assert len(df) == 3\n", - "assert tuple(df.reset_index().columns) == ('cid', 'k', 'x', 'x_act', 'y_act', \n", + "assert tuple(df.reset_index().columns) == ('cid', 'k', 'x', 'x_act', 'y_act', 'alpha',\n", " 'pair', 'fee', 'descr', 'constr', 'params')\n", "assert tuple(df[\"k\"]) == (2000, 8040, 1970)\n", "assert CPCContainer.from_df(df) == CC\n", @@ -1004,7 +645,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": null, "id": "6cd062ae-c465-4102-a57c-587874023de5", "metadata": {}, "outputs": [], @@ -1033,19 +674,10 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": null, "id": "8c046e70-ef8a-4de8-bd17-726afb617ea1", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "len 2145000\n", - "elapsed time: 0.53s\n" - ] - } - ], + "outputs": [], "source": [ "start_time = time.time()\n", "cc_json = json.dumps(CC.asdicts())\n", @@ -1068,106 +700,10 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": null, "id": "e892dc06-329d-477f-adcb-40a87eb7a009", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "elapsed time: 0.30s\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
cidkxx_acty_actpairfeedescrconstrparams
012000112000ETH/USDC0.001UniV2uv2{}
128040224020ETH/USDC0.001UniV2uv2{}
231970111970ETH/USDC0.001UniV2uv2{}
\n", - "
" - ], - "text/plain": [ - " cid k x x_act y_act pair fee descr constr params\n", - "0 1 2000 1 1 2000 ETH/USDC 0.001 UniV2 uv2 {}\n", - "1 2 8040 2 2 4020 ETH/USDC 0.001 UniV2 uv2 {}\n", - "2 3 1970 1 1 1970 ETH/USDC 0.001 UniV2 uv2 {}" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "start_time = time.time()\n", "df.to_csv(\".curves.csv\")\n", @@ -1189,18 +725,10 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": null, "id": "a2976017-2a84-4fba-885d-7680d9f61c3a", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "elapsed time: 0.38s\n" - ] - } - ], + "outputs": [], "source": [ "start_time = time.time()\n", "df.to_csv(\".curves.tsv\", sep=\"\\t\")\n", @@ -1221,20 +749,12 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": null, "id": "ed5aaa2c-2f5a-4863-87cf-a77240826a85", "metadata": { "lines_to_next_cell": 2 }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "elapsed time: 0.37s\n" - ] - } - ], + "outputs": [], "source": [ "start_time = time.time()\n", "df.to_csv(\".curves.csv.gz\", compression = \"gzip\")\n", @@ -1255,7 +775,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": null, "id": "f1507cc7-96ba-4342-bf1e-955b248bd8b4", "metadata": {}, "outputs": [], @@ -1280,115 +800,10 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": null, "id": "a1c75dfe-ce14-4840-9c62-39a8d5cfc3ad", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "elapsed time: 0.32s\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
kxx_acty_actpairfeedescrconstrparams
cid
12000112000ETH/USDC0.001UniV2uv2{}
28040224020ETH/USDC0.001UniV2uv2{}
31970111970ETH/USDC0.001UniV2uv2{}
\n", - "
" - ], - "text/plain": [ - " k x x_act y_act pair fee descr constr params\n", - "cid \n", - "1 2000 1 1 2000 ETH/USDC 0.001 UniV2 uv2 {}\n", - "2 8040 2 2 4020 ETH/USDC 0.001 UniV2 uv2 {}\n", - "3 1970 1 1 1970 ETH/USDC 0.001 UniV2 uv2 {}" - ] - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "start_time = time.time()\n", "df.to_pickle(\".curves.pkl\")\n", @@ -1419,23 +834,10 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": null, "id": "c43b9431-603d-49af-b5fd-1975e9f59e2f", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " 2145000 .curves.json\n", - "-rw-r--r-- 1 skl staff 660049 25 Jul 14:01 .curves.csv\n", - "-rw-r--r-- 1 skl staff 2725 25 Jul 14:01 .curves.csv.gz\n", - "-rw-r--r-- 1 skl staff 841163 25 Jul 14:01 .curves.pkl\n", - "-rw-r--r-- 1 skl staff 660049 25 Jul 14:01 .curves.tsv\n", - "-rw-r--r-- 1 skl staff 470552 1 May 12:43 .curves.xlsx\n" - ] - } - ], + "outputs": [], "source": [ "#print(f\"{len(df_xlsx)} curves\")\n", "print(f\" {len(cc_json)} .curves.json\", )\n", @@ -1465,6 +867,14 @@ "metadata": {}, "outputs": [], "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "076619c0-8c0d-4555-9e3e-62266225942b", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/resources/NBTest/NBTest_003_Serialization.py b/resources/NBTest/NBTest_003_Serialization.py index b9825d760..e71875926 100644 --- a/resources/NBTest/NBTest_003_Serialization.py +++ b/resources/NBTest/NBTest_003_Serialization.py @@ -32,23 +32,30 @@ # ## Optimizer pickling [NOTEST] -N=5 -curves = [ - CPC.from_xy(x=1, y=2000, pair="ETH/USDC"), - CPC.from_xy(x=1, y=2200, pair="ETH/USDC"), - CPC.from_xy(x=1, y=2400, pair="ETH/USDC"), -] -# note: the below is a bit icky as the same curve objects are added multiple times -CC = CPCContainer(curves*N) -O = CPCArbOptimizer(CC) -O.CC.asdf() +pass -O.pickle("delme") -O.pickle("delme", addts=False) +# + +# N=5 +# curves = [ +# CPC.from_xy(x=1, y=2000, pair="ETH/USDC"), +# CPC.from_xy(x=1, y=2200, pair="ETH/USDC"), +# CPC.from_xy(x=1, y=2400, pair="ETH/USDC"), +# ] +# # note: the below is a bit icky as the same curve objects are added multiple times +# CC = CPCContainer(curves*N) +# O = CPCArbOptimizer(CC) +# O.CC.asdf() -# !ls *.pickle +# + +# O.pickle("delme") +# O.pickle("delme", addts=False) -O.unpickle("delme") +# + +# # !ls *.pickle + +# + +# O.unpickle("delme") +# - # ## Creating curves # @@ -86,6 +93,7 @@ 'x': 100, 'x_act': 100, 'y_act': 100, + 'alpha': 0.5, 'pair': 'TKNB/TKNQ', 'cid': "1", 'fee': 0, @@ -258,7 +266,7 @@ df = CC.asdf() assert len(df) == 3 -assert tuple(df.reset_index().columns) == ('cid', 'k', 'x', 'x_act', 'y_act', +assert tuple(df.reset_index().columns) == ('cid', 'k', 'x', 'x_act', 'y_act', 'alpha', 'pair', 'fee', 'descr', 'constr', 'params') assert tuple(df["k"]) == (2000, 8040, 1970) assert CPCContainer.from_df(df) == CC @@ -369,3 +377,5 @@ + + diff --git a/resources/NBTest/NBTest_037_Exchanges.py b/resources/NBTest/NBTest_037_Exchanges.py index d2f888ec6..18b78ab97 100644 --- a/resources/NBTest/NBTest_037_Exchanges.py +++ b/resources/NBTest/NBTest_037_Exchanges.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.14.7 +# jupytext_version: 1.13.1 # kernelspec: # display_name: Python 3 # language: python diff --git a/resources/NBTest/NBTest_038_TestBancorV3Mode.py b/resources/NBTest/NBTest_038_TestBancorV3Mode.py index 3cba4fd91..580c86256 100644 --- a/resources/NBTest/NBTest_038_TestBancorV3Mode.py +++ b/resources/NBTest/NBTest_038_TestBancorV3Mode.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.14.7 +# jupytext_version: 1.13.1 # kernelspec: # display_name: Python 3 # language: python diff --git a/resources/NBTest/NBTest_048_RespectFlashloanTokensClickParam.py b/resources/NBTest/NBTest_048_RespectFlashloanTokensClickParam.py index 53329eff1..b06fc9230 100644 --- a/resources/NBTest/NBTest_048_RespectFlashloanTokensClickParam.py +++ b/resources/NBTest/NBTest_048_RespectFlashloanTokensClickParam.py @@ -5,7 +5,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.14.7 +# jupytext_version: 1.13.1 # kernelspec: # display_name: Python 3 (ipykernel) # language: python diff --git a/resources/NBTest/NBTest_049_CPCBalancer.ipynb b/resources/NBTest/NBTest_049_CPCBalancer.ipynb new file mode 100644 index 000000000..314618b02 --- /dev/null +++ b/resources/NBTest/NBTest_049_CPCBalancer.ipynb @@ -0,0 +1,1465 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 74, + "id": "a448e212", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ConstantProductCurve v3.0-beta3 (22/Aug/2023)\n" + ] + } + ], + "source": [ + "from fastlane_bot.tools.cpc import ConstantProductCurve as CPC\n", + "#from flbtools.cpc import ConstantProductCurve as CPC\n", + "print(\"{0.__name__} v{0.__VERSION__} ({0.__DATE__})\".format(CPC))\n", + "\n", + "from fastlane_bot.testing import *\n", + "#from flbtesting import *\n", + "from math import sqrt\n", + "# from fastlane_bot import __VERSION__\n", + "# require(\"3.0\", __VERSION__)" + ] + }, + { + "cell_type": "markdown", + "id": "d9917997", + "metadata": {}, + "source": [ + "# CPC for Balancer [NBTest049]" + ] + }, + { + "cell_type": "markdown", + "id": "521e4bc5-f003-4062-8978-18506ecff248", + "metadata": {}, + "source": [ + "## Constant product constructor" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "id": "c5cf94f7-77df-412c-9988-7085184bdd1f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "ConstantProductCurve(k=20000, x=100, x_act=100, y_act=200.0, alpha=0.5, pair='TKNB/TKNQ', cid='None', fee=None, descr=None, constr='xy', params={})" + ] + }, + "execution_count": 75, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c0 = CPC.from_xy(100, 200)\n", + "assert c0.x == 100\n", + "assert c0.y == 200\n", + "assert c0.k == 20000\n", + "assert c0.x == c0.x_act\n", + "assert c0.y == c0.y_act\n", + "assert c0.alpha == 0.5\n", + "assert c0.eta == 1\n", + "assert c0.constr == \"xy\"\n", + "assert c0.is_constant_product() == True\n", + "c0" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "id": "9eb7484e-a09a-4184-bd59-073a7b399b8a", + "metadata": {}, + "outputs": [], + "source": [ + "assert c0.asdict() == {\n", + " 'k': 20000,\n", + " 'x': 100,\n", + " 'x_act': 100,\n", + " 'y_act': 200.0,\n", + " 'alpha': 0.5,\n", + " 'pair': 'TKNB/TKNQ',\n", + " 'cid': 'None',\n", + " 'fee': None,\n", + " 'descr': None,\n", + " 'constr': 'xy',\n", + " 'params': {}\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "id": "2b12d88d-6d7d-4d39-b9bc-def990500df7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "ConstantProductCurve(k=20000.0, x=100, x_act=100, y_act=200.0, alpha=0.5, pair='TKNB/TKNQ', cid='None', fee=None, descr=None, constr='xyal', params={})" + ] + }, + "execution_count": 77, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c1 = CPC.from_xyal(100, 200)\n", + "assert c1.constr == \"xyal\"\n", + "assert c1.is_constant_product() == True\n", + "assert c1==c0\n", + "c1" + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "id": "fc98b1e0-0771-4cd6-877c-b70e74b548b8", + "metadata": {}, + "outputs": [], + "source": [ + "assert c1.asdict() == {\n", + " 'k': 20000,\n", + " 'x': 100,\n", + " 'x_act': 100,\n", + " 'y_act': 200.0,\n", + " 'alpha': 0.5,\n", + " 'pair': 'TKNB/TKNQ',\n", + " 'cid': 'None',\n", + " 'fee': None,\n", + " 'descr': None,\n", + " 'constr': 'xyal',\n", + " 'params': {}\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "id": "f6ededab-423a-489e-8d1b-295162fda59b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "ConstantProductCurve(k=20000.0, x=100, x_act=100, y_act=200.0, alpha=0.5, pair='TKNB/TKNQ', cid='None', fee=None, descr=None, constr='xyal', params={})" + ] + }, + "execution_count": 79, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c2 = CPC.from_xyal(100, 200, alpha=0.5)\n", + "assert c2.constr == \"xyal\"\n", + "assert c2.is_constant_product() == True\n", + "assert c2==c0\n", + "assert c2.asdict() == c1.asdict()\n", + "c2" + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "id": "203e5e7b-1cde-4154-a75c-35068f018a21", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "ConstantProductCurve(k=20000.0, x=100, x_act=100, y_act=200.0, alpha=0.5, pair='TKNB/TKNQ', cid='None', fee=None, descr=None, constr='xyal', params={})" + ] + }, + "execution_count": 80, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c3 = CPC.from_xyal(100, 200, eta=1)\n", + "assert c3.constr == \"xyal\"\n", + "assert c3.is_constant_product() == True\n", + "assert c3==c0\n", + "assert c3.asdict() == c1.asdict()\n", + "c3" + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "id": "ee693b31-3278-46d2-a578-ba6e0b2c6333", + "metadata": {}, + "outputs": [], + "source": [ + "assert raises(CPC.from_xyal, 100, 200, \n", + " alpha=0.5, eta=1) == 'at most one of alpha and eta must be given [0.5, 1]'" + ] + }, + { + "cell_type": "markdown", + "id": "9f8986b6-0d20-4a26-9dbe-19c20eb40034", + "metadata": {}, + "source": [ + "## Weighted constructor" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "id": "2331941a-3aeb-4fa3-887a-6ed45c37307b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "ConstantProductCurve(k=20000, x=100, x_act=100, y_act=200.0, alpha=0.5, pair='TKNB/TKNQ', cid='None', fee=None, descr=None, constr='xy', params={})" + ] + }, + "execution_count": 82, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c0 = CPC.from_xy(100, 200)\n", + "assert c0.x == 100\n", + "assert c0.y == 200\n", + "assert c0.k == 20000\n", + "assert c0.x == c0.x_act\n", + "assert c0.y == c0.y_act\n", + "assert c0.alpha == 0.5\n", + "assert c0.eta == 1\n", + "assert c0.constr == \"xy\"\n", + "assert c0.is_constant_product() == True\n", + "c0" + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "id": "db5f6d9a-87c3-4bba-9bbc-32993d2c09a7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "ConstantProductCurve(k=20000.0, x=100, x_act=100, y_act=200.0, alpha=0.5, pair='TKNB/TKNQ', cid='None', fee=None, descr=None, constr='xyal', params={})" + ] + }, + "execution_count": 83, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c1 = CPC.from_xyal(100, 200)\n", + "assert c1.constr == \"xyal\"\n", + "assert c1.is_constant_product() == True\n", + "assert c1 == c0\n", + "assert c1.asdict()[\"alpha\"] == 0.5\n", + "c1" + ] + }, + { + "cell_type": "code", + "execution_count": 84, + "id": "02dc9cc9-d30e-4daf-9f7d-11f9bd60ad04", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "ConstantProductCurve(k=800000000.0, x=100, x_act=100, y_act=199.99999999999994, alpha=0.25, pair='TKNB/TKNQ', cid='None', fee=None, descr=None, constr='xyal', params={})" + ] + }, + "execution_count": 84, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c2 = CPC.from_xyal(100, 200, alpha=0.25)\n", + "assert c2.constr == \"xyal\"\n", + "assert c2.is_constant_product() == False\n", + "assert c2.alpha == 0.25\n", + "assert c2.asdict()[\"alpha\"] == 0.25\n", + "assert iseq(c2.eta, 0.25/0.75)\n", + "assert c2 != c0\n", + "assert c2 != c1\n", + "c2" + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "id": "f48bec87-6674-4a5c-8a15-562c5caff154", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "ConstantProductCurve(k=800000000.0, x=100, x_act=100, y_act=199.99999999999994, alpha=0.25, pair='TKNB/TKNQ', cid='None', fee=None, descr=None, constr='xyal', params={})" + ] + }, + "execution_count": 85, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c3 = CPC.from_xyal(100, 200, alpha=0.8)\n", + "assert c3.constr == \"xyal\"\n", + "assert c3.is_constant_product() == False\n", + "assert iseq(c3.alpha, 0.8)\n", + "assert c3.asdict()[\"alpha\"] == 0.8\n", + "assert iseq(c3.eta, 0.8/0.2)\n", + "assert c3 != c0\n", + "assert c3 != c1\n", + "assert c3 != c2\n", + "c2" + ] + }, + { + "cell_type": "code", + "execution_count": 86, + "id": "4dc4a1f6-1d71-4f1d-b0a0-616f83fb58e8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "ConstantProductCurve(k=376.06030930863926, x=100, x_act=100, y_act=200.0, alpha=0.8, pair='TKNB/TKNQ', cid='None', fee=None, descr=None, constr='xyal', params={})" + ] + }, + "execution_count": 86, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c3b = CPC.fromdict(c3.asdict())\n", + "assert c3b == c3\n", + "c3b" + ] + }, + { + "cell_type": "code", + "execution_count": 87, + "id": "8caba1ff-65e7-496e-adcf-aea4cddcee4b", + "metadata": {}, + "outputs": [], + "source": [ + "assert raises(CPC.from_xyal,100, 200, alpha=0) == 'alpha must be > 0 [0]'\n", + "assert raises(CPC.from_xyal,100, 200, alpha=-1) == 'alpha must be > 0 [-1]'\n", + "assert raises(CPC.from_xyal,100, 200, alpha=1) == 'alpha must be < 1 [1]'\n", + "assert raises(CPC.from_xyal,100, 200, alpha=2) == 'alpha must be < 1 [2]'" + ] + }, + { + "cell_type": "code", + "execution_count": 88, + "id": "b54669fb-a128-41ae-a3ac-b9eff553f897", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'alpha must be > 0 [0]'" + ] + }, + "execution_count": 88, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "raises(CPC.from_xyal,100, 200, alpha=0)" + ] + }, + { + "cell_type": "code", + "execution_count": 89, + "id": "c4630e2a-b577-410b-8251-1c327866c9f1", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(CPC.from_xyal,100, 200, alpha=1-1e-10)\n", + "assert not raises(CPC.from_xyal,100, 200, alpha=0.01)" + ] + }, + { + "cell_type": "code", + "execution_count": 90, + "id": "c8c740c3-3ffb-4694-95dc-2932b7d39c6c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "\"(34, 'Result too large')\"" + ] + }, + "execution_count": 90, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "raises(CPC.from_xyal,100, 200, alpha=0.001)" + ] + }, + { + "cell_type": "markdown", + "id": "448e83dd-7f7d-4233-a129-729b31168667", + "metadata": {}, + "source": [ + "## High level testing of all functions\n", + "\n", + "(including not YET implemented)" + ] + }, + { + "cell_type": "code", + "execution_count": 91, + "id": "fd739374-c01b-432e-ab80-98e35fda3674", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "ConstantProductCurve(k=20000.0, x=100, x_act=100, y_act=200.0, alpha=0.5, pair='TKNB/TKNQ', cid='None', fee=None, descr=None, constr='xyal', params={})" + ] + }, + "execution_count": 91, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c0 = CPC.from_xyal(100, 200)\n", + "assert c0.is_constant_product() == True\n", + "c0" + ] + }, + { + "cell_type": "code", + "execution_count": 92, + "id": "927269a5-0c16-4336-8fcf-3e22392c8847", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "ConstantProductCurve(k=800000000.0, x=100, x_act=100, y_act=199.99999999999994, alpha=0.25, pair='TKNB/TKNQ', cid='None', fee=None, descr=None, constr='xyal', params={})" + ] + }, + "execution_count": 92, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "c1 = CPC.from_xyal(100, 200, alpha=0.25)\n", + "assert c1.is_constant_product() == False\n", + "c1" + ] + }, + { + "cell_type": "markdown", + "id": "dfa26d63-4a6b-4dbc-ac1a-f1f3ad1d2a0c", + "metadata": {}, + "source": [ + "#### Not (yet) implemented functions\n", + "\n", + "Those function groups are not currently planned to be implemented at all\n", + "\n", + "- `execute` as there is no need to simulate those curves for the time being; that was a Carbon thing\n", + "\n", + "The functions we may implement at a later stage are\n", + "\n", + "- `description` should probably be updated, but it is tedious; `format` ditto\n", + "- `x_max`, `x_min`, `p_max`, `p_min` and the other leverage functions once we consider it important and safe" + ] + }, + { + "cell_type": "markdown", + "id": "f02b6b1f-e11c-46c7-8ce4-a320b1200432", + "metadata": {}, + "source": [ + "execute" + ] + }, + { + "cell_type": "code", + "execution_count": 93, + "id": "30239c29-9c2d-4a35-b24e-593e27d23013", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(c0.execute)\n", + "assert raises(c1.execute).startswith(\"only implemented for\")" + ] + }, + { + "cell_type": "markdown", + "id": "ce3aa2c2-13e3-43a4-ae06-0815781f1dc1", + "metadata": {}, + "source": [ + "description and format" + ] + }, + { + "cell_type": "code", + "execution_count": 94, + "id": "31973a9d-f6fc-4ba6-8660-3ca6b3b31238", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(c0.description)\n", + "assert raises(c1.description).startswith(\"only implemented for\")" + ] + }, + { + "cell_type": "code", + "execution_count": 95, + "id": "6da717da-0e96-4af5-a011-74c54c5a8033", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(c0.format)\n", + "assert raises(c1.format).startswith(\"only implemented for\")" + ] + }, + { + "cell_type": "markdown", + "id": "85e0d16e-e9f6-4474-b334-19276c5c596a", + "metadata": {}, + "source": [ + "leverage related functions (primary)" + ] + }, + { + "cell_type": "code", + "execution_count": 96, + "id": "22c10615-7636-4ec9-ba02-01e9c58ad20d", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(lambda: c0.p_max)\n", + "assert raises(lambda: c1.p_max).startswith(\"only implemented for\")" + ] + }, + { + "cell_type": "code", + "execution_count": 97, + "id": "b63b25fa-8f4a-415d-b0c5-8d10be06f91b", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(lambda: c0.p_min)\n", + "assert raises(lambda: c1.p_min).startswith(\"only implemented for\")" + ] + }, + { + "cell_type": "code", + "execution_count": 98, + "id": "1e2a97a5-240b-49b4-abdc-f30b3b1e2788", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(lambda: c0.x_min)\n", + "assert raises(lambda: c1.x_min).startswith(\"only implemented for\")" + ] + }, + { + "cell_type": "code", + "execution_count": 99, + "id": "98262a43-8c8e-4eb2-af03-6c7f082a9a5e", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(lambda: c0.x_max)\n", + "assert raises(lambda: c1.x_max).startswith(\"only implemented for\")" + ] + }, + { + "cell_type": "code", + "execution_count": 100, + "id": "b3049924-bc62-44c3-b7fc-0c5513544b87", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(lambda: c0.y_min)\n", + "assert raises(lambda: c1.y_min).startswith(\"only implemented for\")" + ] + }, + { + "cell_type": "code", + "execution_count": 101, + "id": "9809fe75-81eb-4117-b06f-ea80e8e5d1af", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(lambda: c0.y_max)\n", + "assert raises(lambda: c1.y_max).startswith(\"only implemented for\")" + ] + }, + { + "cell_type": "markdown", + "id": "162d55a7-94b2-4470-809a-5cf59c6fc6de", + "metadata": {}, + "source": [ + "leverage related functions (secondary, ie calling primary ones)" + ] + }, + { + "cell_type": "code", + "execution_count": 102, + "id": "d53b7a88-b1b2-488f-b33e-cf85af69dad2", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(c0.p_max_primary)\n", + "assert raises(c1.p_max_primary).startswith(\"only implemented for\")" + ] + }, + { + "cell_type": "code", + "execution_count": 103, + "id": "47628578-dfab-4a28-a46f-b83b12af7745", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(c0.p_min_primary)\n", + "assert raises(c1.p_min_primary).startswith(\"only implemented for\")" + ] + }, + { + "cell_type": "code", + "execution_count": 104, + "id": "818af0e4", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(lambda: c0.at_xmin)\n", + "assert raises(lambda: c1.at_xmin).startswith(\"only implemented for\")" + ] + }, + { + "cell_type": "code", + "execution_count": 105, + "id": "bac20004", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(lambda: c0.at_xmax)\n", + "assert raises(lambda: c1.at_xmax).startswith(\"only implemented for\")" + ] + }, + { + "cell_type": "code", + "execution_count": 106, + "id": "490db431", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(lambda: c0.at_ymin)\n", + "assert raises(lambda: c1.at_ymin).startswith(\"only implemented for\")" + ] + }, + { + "cell_type": "code", + "execution_count": 107, + "id": "bc7fda17", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(lambda: c0.at_ymax)\n", + "assert raises(lambda: c1.at_ymax).startswith(\"only implemented for\")" + ] + }, + { + "cell_type": "code", + "execution_count": 108, + "id": "d1637e16-ae56-45f6-bb90-b10cd5e83194", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(lambda: c0.at_boundary)\n", + "assert raises(lambda: c1.at_boundary).startswith(\"only implemented for\")" + ] + }, + { + "cell_type": "markdown", + "id": "faeae9de-470f-4a8d-82a5-0edf1558ba09", + "metadata": {}, + "source": [ + "todo" + ] + }, + { + "cell_type": "code", + "execution_count": 109, + "id": "4a70d47e-3a89-452d-80f3-d69028433648", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(c0.xyfromp_f)\n", + "assert raises(c1.xyfromp_f).startswith(\"only implemented for\")" + ] + }, + { + "cell_type": "code", + "execution_count": 110, + "id": "a1bbada5-9cd2-4dd0-a226-fb7422614b53", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(c0.dxdyfromp_f)\n", + "assert raises(c1.dxdyfromp_f).startswith(\"only implemented for\")" + ] + }, + { + "cell_type": "markdown", + "id": "1d79b5fb-32b4-47f6-a852-f10e508d3fc4", + "metadata": {}, + "source": [ + "#### Implemented functions" + ] + }, + { + "cell_type": "code", + "execution_count": 111, + "id": "d90a02d0-21c9-47dc-894c-82ea93b01779", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(lambda: c0.y)\n", + "assert not raises(lambda: c1.y)" + ] + }, + { + "cell_type": "code", + "execution_count": 112, + "id": "052b8feb-038e-457a-a3f1-8ff25b030a0e", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(lambda: c0.p)\n", + "assert not raises(lambda: c1.p)" + ] + }, + { + "cell_type": "code", + "execution_count": 113, + "id": "8872ba9c-2186-4176-add5-4a36b66162a4", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(lambda: c0.kbar)\n", + "assert not raises(lambda: c1.kbar)" + ] + }, + { + "cell_type": "code", + "execution_count": 114, + "id": "795eac9c-fe49-447f-b3fe-c0268cdd20de", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(c0.tvl)\n", + "assert not raises(c1.tvl)" + ] + }, + { + "cell_type": "code", + "execution_count": 115, + "id": "13b1f308-3062-4872-a91e-e89bccf17cf8", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(c0.yfromx_f, 110)\n", + "assert not raises(c1.yfromx_f, 110, ignorebounds=True)\n", + "assert raises(c1.yfromx_f, 110, ignorebounds=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 116, + "id": "b90b56dd-bc10-4087-a3f4-5c0b8bcc681a", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(c0.xfromy_f, 210)\n", + "assert not raises(c1.xfromy_f, 110, ignorebounds=True)\n", + "assert raises(c1.xfromy_f, 110, ignorebounds=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 117, + "id": "9b797a1f-2d04-42ae-a03f-4b9675a65ed3", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(c0.dyfromdx_f, 1)\n", + "assert not raises(c1.dyfromdx_f, 1, ignorebounds=True)\n", + "assert raises(c1.dyfromdx_f, 1, ignorebounds=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 118, + "id": "ee7cd754-fe9c-4fb8-848d-46dc2850c0cd", + "metadata": {}, + "outputs": [], + "source": [ + "assert not raises(c0.dxfromdy_f, 1)\n", + "assert not raises(c1.dxfromdy_f, 1, ignorebounds=True)\n", + "assert raises(c1.dxfromdy_f, 1, ignorebounds=False)" + ] + }, + { + "cell_type": "markdown", + "id": "30cea356-d1ff-4f34-ad31-f4fbb89c69b3", + "metadata": {}, + "source": [ + "## Simple Tests" + ] + }, + { + "cell_type": "code", + "execution_count": 119, + "id": "6373dfe5-6d20-4c55-9f0b-80a8ac6e1e05", + "metadata": {}, + "outputs": [], + "source": [ + "c0 = CPC.from_xyal(100, 200)\n", + "c1 = CPC.from_xyal(100, 200, eta=2)\n", + "c2 = CPC.from_xyal(100, 200, eta=0.5)" + ] + }, + { + "cell_type": "code", + "execution_count": 120, + "id": "81cb94ae-1fd1-4d63-9f59-e79274737d4d", + "metadata": {}, + "outputs": [], + "source": [ + "assert iseq(c0.alpha, 1/2)\n", + "assert iseq(c1.alpha, 2/3)\n", + "assert iseq(c2.alpha, 1/3)" + ] + }, + { + "cell_type": "markdown", + "id": "88b1ae49-2c71-4468-bb83-743cadb96b93", + "metadata": {}, + "source": [ + "#### Current token balance $y$\n", + "\n", + "$$\n", + "y = \\left( \\frac k x \\right)^\\eta\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 121, + "id": "0eac45f7-ef6d-419d-8254-098858e7a529", + "metadata": {}, + "outputs": [], + "source": [ + "assert iseq(c0.y, 200)\n", + "assert iseq(c1.y, 200)\n", + "assert iseq(c2.y, 200)" + ] + }, + { + "cell_type": "markdown", + "id": "f08b3230-20b9-4b72-bd58-4f810a92991c", + "metadata": {}, + "source": [ + "#### Current price $p$\n", + "\n", + "$$\n", + "p = \\eta\\, \\frac y x\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 122, + "id": "cc39342b-1304-4e7d-8594-950df18ffb6e", + "metadata": {}, + "outputs": [], + "source": [ + "assert iseq(c0.p, 2 * c0.eta)\n", + "assert iseq(c1.p, 2 * c1.eta)\n", + "assert iseq(c2.p, 2 * c2.eta)" + ] + }, + { + "cell_type": "markdown", + "id": "bddf7d0d-bbc5-427b-8b01-e4c069ff4e47", + "metadata": {}, + "source": [ + "#### TVL\n", + "\n", + "$$\n", + "\\mathrm{TVL} = x_a*p + y_a\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 123, + "id": "80e3f97b-41b0-4263-b2e0-b02da963c60f", + "metadata": {}, + "outputs": [], + "source": [ + "assert c0.x == c0.x_act\n", + "assert c0.y == c0.y_act\n", + "assert c1.x == c1.x_act\n", + "assert c1.y == c1.y_act\n", + "assert c2.x == c2.x_act\n", + "assert c2.y == c2.y_act" + ] + }, + { + "cell_type": "code", + "execution_count": 124, + "id": "593fb93f-5d95-4796-9da6-abd79206ff3d", + "metadata": {}, + "outputs": [], + "source": [ + "assert iseq(c0.tvl(), 100 * c0.p + 200)\n", + "assert iseq(c1.tvl(), 100 * c1.p + 200)\n", + "assert iseq(c2.tvl(), 100 * c2.p + 200)" + ] + }, + { + "cell_type": "markdown", + "id": "98664ea1-c17d-4b42-854b-7b3db79c2b33", + "metadata": {}, + "source": [ + "#### Pool constant $k$\n", + "\n", + "$$\n", + "k^\\alpha = x^\\alpha\\, y^{1-\\alpha}\n", + "$$\n", + "\n", + "$$\n", + "k = x\\,y^\\frac{1-\\alpha}{\\alpha} = x\\,y^{\\frac 1 \\eta}\n", + "$$\n" + ] + }, + { + "cell_type": "code", + "execution_count": 125, + "id": "171b56b5-f270-4f51-8fd8-a185b3c0e91f", + "metadata": {}, + "outputs": [], + "source": [ + "assert iseq(c0.k**(1/2), c0.x**(1/2) * c0.y**(1/2))\n", + "assert iseq(c1.k**(2/3), c1.x**(2/3) * c1.y**(1/3))\n", + "assert iseq(c2.k**(1/3), c1.x**(1/3) * c1.y**(2/3))" + ] + }, + { + "cell_type": "markdown", + "id": "0b290a50-f765-4b62-8be6-19542281022b", + "metadata": {}, + "source": [ + "#### Pool constant $\\bar k$\n", + "\n", + "$$\n", + "x^\\alpha\\, y^{1-\\alpha} = \\bar k = k^\\alpha\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 126, + "id": "01d3e5e2-aba4-434d-a118-15bd63e854db", + "metadata": {}, + "outputs": [], + "source": [ + "assert iseq(c0.kbar, c0.x**(1/2) * c0.y**(1/2))\n", + "assert iseq(c1.kbar, c1.x**(2/3) * c1.y**(1/3))\n", + "assert iseq(c2.kbar, c1.x**(1/3) * c1.y**(2/3))" + ] + }, + { + "cell_type": "code", + "execution_count": 127, + "id": "0ae9f182-0f46-4fdd-9879-619cf3f6ecd2", + "metadata": {}, + "outputs": [], + "source": [ + "assert iseq(c0.kbar, c0.k**c0.alpha)\n", + "assert iseq(c1.kbar, c1.k**c1.alpha)\n", + "assert iseq(c2.kbar, c2.k**c2.alpha)" + ] + }, + { + "cell_type": "markdown", + "id": "d76dbd19-be45-4078-8896-35ba65d9d181", + "metadata": {}, + "source": [ + "#### Token balance function $y(x)$\n", + "\n", + "$$\n", + "y(x) = \\left( \\frac k x \\right)^\\eta\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 128, + "id": "9ff8935e-1898-40b9-802f-09a7b2cc608c", + "metadata": {}, + "outputs": [], + "source": [ + "assert c0.eta == 1\n", + "assert iseq(c0.yfromx_f(100, ignorebounds=True), 200)\n", + "assert iseq(c0.yfromx_f( 50, ignorebounds=True), 400)\n", + "assert iseq(c0.yfromx_f(200, ignorebounds=True), 100)" + ] + }, + { + "cell_type": "code", + "execution_count": 129, + "id": "081ec8c2-768c-45a5-a478-1df3a55590ab", + "metadata": {}, + "outputs": [], + "source": [ + "assert iseq(c1.eta, 2)\n", + "assert iseq(c1.yfromx_f(100, ignorebounds=True), 200)\n", + "assert iseq(c1.yfromx_f( 50, ignorebounds=True), 200*2**2)\n", + "assert iseq(c1.yfromx_f(200, ignorebounds=True), 200/2**2)" + ] + }, + { + "cell_type": "code", + "execution_count": 130, + "id": "5f556205-299f-4f5c-b717-076a64137784", + "metadata": {}, + "outputs": [], + "source": [ + "assert iseq(c2.eta, 1/2)\n", + "assert iseq(c2.yfromx_f(100, ignorebounds=True), 200)\n", + "assert iseq(c2.yfromx_f( 50, ignorebounds=True), 200*sqrt(2))\n", + "assert iseq(c2.yfromx_f(200, ignorebounds=True), 200/sqrt(2))" + ] + }, + { + "cell_type": "markdown", + "id": "0aabe321-cb4b-4c65-b84a-73493a95b740", + "metadata": {}, + "source": [ + "#### Token balance function $x(y)$\n", + "\n", + "$$\n", + "x(y) \n", + "= \\frac{k}{ y^{\\frac{1-\\alpha}{\\alpha}} }\n", + "= \\frac{k}{ y^{\\frac{1}{\\eta}} }\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 131, + "id": "bfbc589d-2124-4c51-9ac9-a7a3034ebd67", + "metadata": {}, + "outputs": [], + "source": [ + "assert c0.eta == 1\n", + "assert iseq(c0.xfromy_f(200, ignorebounds=True), 100)\n", + "assert iseq(c0.xfromy_f(100, ignorebounds=True), 200)\n", + "assert iseq(c0.xfromy_f(400, ignorebounds=True), 50)" + ] + }, + { + "cell_type": "code", + "execution_count": 132, + "id": "fb276d33-72b0-406d-9f3d-9ce16354098b", + "metadata": {}, + "outputs": [], + "source": [ + "assert iseq(c1.eta, 2)\n", + "assert iseq(c1.xfromy_f(200, ignorebounds=True), 100)\n", + "assert iseq(c1.xfromy_f(100, ignorebounds=True), 100*2**0.5)\n", + "assert iseq(c1.xfromy_f(400, ignorebounds=True), 100/2**0.5)" + ] + }, + { + "cell_type": "code", + "execution_count": 133, + "id": "d55dd588-8a49-43c5-bf23-22fa207876fd", + "metadata": {}, + "outputs": [], + "source": [ + "assert iseq(c2.eta, 1/2)\n", + "assert iseq(c2.xfromy_f(200, ignorebounds=True), 100)\n", + "assert iseq(c2.xfromy_f(100, ignorebounds=True), 100*2**2)\n", + "assert iseq(c2.xfromy_f(400, ignorebounds=True), 100/2**2)" + ] + }, + { + "cell_type": "markdown", + "id": "ea30dd5f-ae86-469c-8f9b-f7c43f9ed5a6", + "metadata": {}, + "source": [ + "#### Price response function $(x(p), y(p))$\n", + "\n", + "$$\n", + "x(p) \n", + "= \n", + "\\left(\\frac \\eta p\\right)^{1-\\alpha} k^\\alpha\n", + "$$\n", + "\n", + "$$\n", + "y(p) = \\left( \\frac{kp}{\\eta} \\right)^\\alpha\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 134, + "id": "eb60778c-edd0-42c5-8681-a259e02c1e91", + "metadata": {}, + "outputs": [], + "source": [ + "assert iseq(c0.xyfromp_f(c0.p, ignorebounds=True)[0], c0.x)\n", + "assert iseq(c1.xyfromp_f(c1.p, ignorebounds=True)[0], c1.x)\n", + "assert iseq(c2.xyfromp_f(c2.p, ignorebounds=True)[0], c2.x)" + ] + }, + { + "cell_type": "code", + "execution_count": 135, + "id": "c242f036-c366-4c25-ab1d-9b13ff283d60", + "metadata": {}, + "outputs": [], + "source": [ + "assert iseq(c0.xyfromp_f(c0.p, ignorebounds=True)[1], c0.y)\n", + "assert iseq(c1.xyfromp_f(c1.p, ignorebounds=True)[1], c1.y)\n", + "assert iseq(c2.xyfromp_f(c2.p, ignorebounds=True)[1], c2.y)" + ] + }, + { + "cell_type": "code", + "execution_count": 136, + "id": "1e39b3b6-ac20-4a7d-ab4a-f764cc949ee9", + "metadata": {}, + "outputs": [], + "source": [ + "for ci in [c0, c1, c2]:\n", + " for p in [2, 1, 4]:\n", + " eta_over_p = ci.eta / p\n", + " x = eta_over_p ** (1-ci.alpha) * ci.kbar\n", + " y = 1/eta_over_p**ci.alpha * ci.kbar\n", + " xx, yy, pp = ci.xyfromp_f (p, ignorebounds=True)\n", + " dx, dy, _ = ci.dxdyfromp_f(p, ignorebounds=True)\n", + " assert iseq(x, xx)\n", + " assert iseq(y, yy)\n", + " assert iseq(p, pp)\n", + " assert iseq(dx, xx-ci.x)\n", + " assert iseq(dy, yy-ci.y)" + ] + }, + { + "cell_type": "markdown", + "id": "2105f1e3-cb98-4e4d-9c58-c144bc49c33e", + "metadata": {}, + "source": [ + "## Consistency tests" + ] + }, + { + "cell_type": "code", + "execution_count": 137, + "id": "8fd1d683-195a-414b-bfa2-2155462efcc1", + "metadata": {}, + "outputs": [], + "source": [ + "c0 = CPC.from_xyal(100, 200)\n", + "c1 = CPC.from_xyal(100, 200, eta=2)\n", + "c2 = CPC.from_xyal(100, 200, eta=0.5)\n", + "cc = [c0, c1, c2]" + ] + }, + { + "cell_type": "code", + "execution_count": 138, + "id": "c2a77208-d1d6-4d99-8fab-d8b9f988dac9", + "metadata": {}, + "outputs": [], + "source": [ + "assert iseq(c0.alpha, 1/2)\n", + "assert iseq(c1.alpha, 2/3)\n", + "assert iseq(c2.alpha, 1/3)" + ] + }, + { + "cell_type": "markdown", + "id": "902c8cd4-d13b-4b40-96cd-86bfe8a089e7", + "metadata": {}, + "source": [ + "### Assert inversions\n", + "\n", + "$$\n", + "y(x(y)) = y\n", + "$$\n", + "\n", + "and vice versa" + ] + }, + { + "cell_type": "code", + "execution_count": 139, + "id": "30a07eda-449f-471e-a609-f979d894d09a", + "metadata": {}, + "outputs": [], + "source": [ + "for xy in np.logspace(1, 3, 100):\n", + " for ci in cc:\n", + " #print(f\"xy={xy}, eta={ci.eta}\")\n", + " assert iseq(ci.yfromx_f(ci.xfromy_f(xy, ignorebounds=True), ignorebounds=True), xy)\n", + " assert iseq(ci.xfromy_f(ci.yfromx_f(xy, ignorebounds=True), ignorebounds=True), xy)" + ] + }, + { + "cell_type": "markdown", + "id": "6c20dda7-f50f-4d35-86b1-80a303d90a59", + "metadata": {}, + "source": [ + "### Assert that prices are correct\n", + "\n", + "$$\n", + "p \\simeq -\\frac{\\Delta y}{\\Delta x}\n", + "$$" + ] + }, + { + "cell_type": "code", + "execution_count": 140, + "id": "8826b280-00f7-4771-9bc6-2d5d344b197e", + "metadata": {}, + "outputs": [], + "source": [ + "for alpha in np.linspace(0.01, 0.99, 100):\n", + " ci = CPC.from_xyal(100, 200, alpha=alpha)\n", + " dy = ci.yfromx_f(ci.x+0.1, ignorebounds=True)-ci.yfromx_f(ci.x-0.1, ignorebounds=True)\n", + " assert iseq(dy/0.2, -ci.p, eps=1e-2), f\"error: {dy/0.2/ci.p+1}\"" + ] + }, + { + "cell_type": "markdown", + "id": "b2944261-9291-496c-98f8-726b0ff26850", + "metadata": {}, + "source": [ + "### Check `dyfromdx_f` against `yfromx_f`" + ] + }, + { + "cell_type": "code", + "execution_count": 141, + "id": "853233ca-22bd-4d0e-a6d1-3dee13341b99", + "metadata": {}, + "outputs": [], + "source": [ + "for dxy in np.linspace(0.1, 99, 100):\n", + " for ci in cc:\n", + " assert iseq(ci.dyfromdx_f(dxy, ignorebounds=True),\n", + " (ci.yfromx_f(ci.x+dxy, ignorebounds=True)-ci.y))\n", + " assert iseq(ci.dxfromdy_f(dxy, ignorebounds=True),\n", + " (ci.xfromy_f(ci.y+dxy, ignorebounds=True)-ci.x))" + ] + }, + { + "cell_type": "markdown", + "id": "957ed182-572a-440c-bee2-1a1b33b4d06b", + "metadata": {}, + "source": [ + "## Charts [NOTEST]" + ] + }, + { + "cell_type": "code", + "execution_count": 142, + "id": "73dfd68b-56ba-4154-a1f8-79c8bddf4169", + "metadata": {}, + "outputs": [], + "source": [ + "plt.style.use('seaborn-v0_8-dark')\n", + "plt.rcParams['figure.figsize'] = [12,6] # only picked up at second run (?!?)" + ] + }, + { + "cell_type": "code", + "execution_count": 143, + "id": "bf52087a-ffb7-4dad-bffa-753983249b51", + "metadata": {}, + "outputs": [], + "source": [ + "c0 = CPC.from_xyal(100, 200)\n", + "c1 = CPC.from_xyal(100, 200, eta=2)\n", + "c2 = CPC.from_xyal(100, 200, eta=0.5)\n", + "cc = [c0, c1, c2]\n", + "xvals = np.linspace(50,200)\n", + "pvals = np.linspace(1,4)" + ] + }, + { + "cell_type": "code", + "execution_count": 144, + "id": "c3e74ed7-83cb-497d-82a8-4a8d38bf312d", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAs0AAAF8CAYAAAA0MYbMAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAAsTAAALEwEAmpwYAACO1UlEQVR4nOzdd3hUZdrH8e/0mmSSkNBbaFJFRbooogsWFFEERRB11VXRxfIuigKiYFmR1YVVZNXVRdeuiL0giHSUJkjvBEhIz/R23j8mGRJISOAkmSTcn+vKNTNnzpm5586c5Jcnz5yjURRFQQghhBBCCFEubawLEEIIIYQQoraT0CyEEEIIIUQFJDQLIYQQQghRAQnNQgghhBBCVEBCsxBCCCGEEBWQ0CyEEEIIIUQFJDSLeufSSy/l999/P61t3njjDR599FEAHn/8cVasWAHAq6++yiWXXMJjjz3Gp59+yiWXXMIdd9xR5TXXRwcPHuT++++vcL3Zs2fz1FNPVUsNY8aM4dtvvz2tbaqzntpiyZIlvPzyyxWudyb9q6wOHTqQk5NzWttUZz01obJ9z8nJoUOHDgAsWrSI6dOnA7B161Yuu+wyrrvuOn799VeuvvpqrrnmGtavX1+tdQM88cQTbN68ucof99prr6WgoOCU65zq+15ddQlRFgnNQpxgxowZ9O3bF4CPP/6YmTNn8uyzz7JgwQIefPBB3njjjRhXWDccPnyYvXv3xroMUYbff/+d/Pz8WJdx1jmTvg8aNIgnnngCiAToXr168dlnn3Ho0CEaNGjAwoULOe+886qj3FJWrFhBdZzW4fPPPyc+Pv6Mt6+uuoQoiz7WBQhRnbp27cpdd93F8uXLyczMZOzYsYwbN45AIMD06dNZsWIFycnJJCcnExcXB0RGNUaPHs23335LRkYGjz/+OIFAgNzcXA4dOkRubi4333wzM2fOZO3atYRCITp16sQTTzyB3W7n0ksvpVu3bmzfvp2HHnqIbt268dRTT3HkyBECgQBXXXUVf/nLXzh06BDjxo3j4osvZuPGjeTn5/Pggw9y5ZVXEgwGeeGFF1iyZAk6nY7zzjuPqVOnYjQaefXVV/n+++8Jh8M0bdqUqVOn0rBhw5Ne+2uvvcZnn32GXq+nZcuWPPfcc/zwww989913vPbaawB8+umn0duPPvooeXl5HDx4kH79+vHxxx/z3XffkZKSAsCNN97IfffdR58+fcp97cVCoRBPPPEEGRkZ3HHHHbzxxhv8+OOPzJkzh1AohN1u57HHHqNbt26lan7rrbf47LPPeP3110lJSSn3tY4ZM4bu3buzbt06jhw5wgUXXMDzzz+PVnvyOMAPP/zAvHnz8Hq9DB06lHvuuQeAuXPn8uOPP+Lz+fB4PEycOJHLL7+81LaLFy/mtddew+/3k5OTw7Bhw5gwYQKrV6/mH//4B82bN2fnzp34/X6mTJlC7969cblcTJ8+nXXr1qHT6bjssst48MEHCQQCFfZNURSGDBnC5MmT6d+/PxAZSWvXrh233nprdL1Zs2bhdDqZMmUKAEuXLmX27Nm89957PP3006xbtw6DwUCzZs149tlnsdls0W03btzI+++/TygUIi4ujgcffJB//etffPXVV+h0Olq3bs3kyZOj33eAYDDIww8/jF6v5/nnn8fj8TBjxgx27NhBIBCgT58+/O1vf0Ov15e7z5XlpZde4vfffyccDjNhwgQGDhyI2+3mySefZN++feTn52Oz2Zg5cyZpaWmlti3v+zd79mzS09M5duwY6enpJCUl8Y9//IOGDRuyd+9epkyZQk5ODlqtlnvuuYcrr7ySjIyMMvfRkrxeL9dffz0333wzo0eP5uOPP+btt9/mww8/xGKxRNcrr/7CwsKT+l7S999/zz/+8Q8sFgtdunSJLi/eR6+66iree+89QqEQS5cuRafTUVhYyJgxY5g/fz4//fQTr776KoFAALPZzMSJEznvvPOYPXs2GzZsIDMzkw4dOjBz5szT3q9efvllMjMzeeSRR/j73//OueeeC0T28379+vHBBx/QsmVL5s2bx3vvvcfixYsBuO222xg3bhznn39+ue+XDh06sHLlShISEvj73//OTz/9RFxcHN26dWP37t3Mnz8fiPzB8Prrr5OdnU2fPn2YPn36SXVlZGTw6quvotFo0Ol0/O1vf+PCCy8s870nxBlRhKhnBg4cqGzatElRFEVp3769Mn/+fEVRFOX3339XunTponi9XuWtt95Sxo4dq/h8PsXlcinXXXedMnHiREVRFOWWW25Rvvnmm5Meq+Ty2bNnK88995wSDocVRVGUF198UZk6dWp0mzlz5kTrGTNmjLJo0SJFURTF6/UqY8aMUb766ivl4MGDSvv27ZWffvpJURRF+fbbb5VLLrlEURRFefvtt5XRo0crHo9HCYVCyl//+lfls88+Uz777DNlwoQJSiAQUBRFUd5//33lz3/+80k9+PHHH5U//elPSl5enqIoivLMM88or7zyivLJJ58od911V3S9krcnTpyo3HrrrdH7/va3vymvv/66oiiKsmvXLuWSSy5RQqHQKV97SatWrVKuuuqq6PZ9+/ZVDhw4oCiKoqxYsULp16+fUlhYqPzzn/9Upk2bpsybN08ZOXKkkp+fryiKcsrXessttygPPPCAEgqFlMLCQqV///7KypUrT6rhlltuUe6++24lEAgohYWFypAhQ5QlS5Yohw4dUsaMGaN4PB5FURTlyy+/VK6++mpFUZRoPeFwWLnllluUvXv3KoqiKEePHlU6duyoZGdnK6tWrVI6duyo/PHHH4qiKMobb7yhjB49OtrrBx98UAkGg4rP51NGjx6trFq1qtJ9+89//qM88MADiqIoSmFhodK7d+9oT4odOHBA6dWrl+Lz+RRFUZS//vWvyocffqisXbtWGTJkSPQ5/v73vyu//fbbSc9R/BoVRVE+/vhjZeTIkYrL5Yred/vtt0f7t3DhQuXee++N9kRRFOXRRx9V/vvf/yqKoijBYFB55JFHlHnz5imKUv4+d6L27dsrr732mqIoirJ9+3alZ8+eSnZ2tvLNN98oTz/9dHS9yZMnK0899VS0nm+++abC79+gQYOUwsJCRVEU5e6771ZefvllRVEUZdiwYco777yjKIqiHD58OLpeefvoibZt26b07NlTWbJkidK3b19l9+7dJ61zqvpL9r2kY8eOKRdccIGyc+dORVEUZe7cuUr79u0VRSm9j5bcvuTyvXv3KldffbWSk5OjKIqi7NixQ+nXr5/icrmUf/7zn8rgwYOj+9GZ7lclfxaW9Oijj0a/37fccovSr18/Zc+ePUpBQUH0PVrR+yU7O1t57733lNGjRyter1fx+XzK7bffrtxyyy3Rx73nnnuUYDCouN1upV+/fsratWtPqmvQoEHK+vXrFUVRlF9++UWZPXv2SfUKoYaMNIt6b9CgQQB07twZv9+P2+1m5cqVXH311RiNRoxGI0OHDmX79u2VfswlS5ZQWFgYnfscCARITk6O3t+jRw8gMuq0du1a8vPzo3MZ3W4327Zto1u3bhgMBi6++GIAOnXqRF5eHhD5l+O1116L2WwGIiNyAH/961/5/fffuf766wEIh8N4PJ6T6lu5ciVDhgwhISEBgMceewyIjFqdygUXXBC9PmLECKZNm8Ydd9zBJ598wvDhw9FqtRW+9rKsWrWK3r1707x5cwD69OlDUlJSdC7i999/z7Fjx5g7d270X7WLFy8+5WsdOHAgWq0Wu91Oy5Yty/239w033IBer8dutzN48GBWrFjBxRdfzPPPP88XX3zB/v372bhxIy6Xq9R2Go2GuXPnsmTJEr788kt2796NoijRGpo0aULHjh2ByPfus88+AyLfu8ceewydTodOp+Odd94B4IUXXqhU34YPH86//vUvcnJy+Pbbb7nkkktO+vd18+bNOeecc/jpp5/o06cPK1euZMaMGYRCIXQ6HSNGjKB///4MHjz4pNH8Ey1dupThw4djtVoBGDt2LHPnzsXv9wPw/PPP43K5+OGHH9BoNEDk/f/777/z8ccfA5FR2JLK2udMJtNJz33TTTcB0L59e9q0acP69esZMmQIzZs3Z/78+ezfv581a9acNP2gadOmp/z+9ezZMzqC36lTJ/Lz88nLy2Pbtm2MGDECgMaNG/Pjjz+ech+98sorSz1vhw4dGD9+PHfffTfPPffcSaPfQKXqP9Fvv/1G+/btadu2LQAjR45k1qxZp9ympOJR/ZIj+hqNhgMHDgDQvXt39PrIr/uq2q+KXX755bz//vsMGzaMzMxMrr76alasWEFCQgIXXXQRRqOxwvcLwM8//8y1114bfZ+MHDkyOsoMcOWVV6LT6bBYLLRq1Yrs7OyTHuOqq65i/PjxXHzxxfTr148777yzMu0TotIkNIt6r/iHcPEvfKWM+W86ne60HjMcDjNp0qRo4HW5XPh8vuj9xQEkHA6jKArvv/9+9F+4OTk5mEwmcnNzMRgM0SkFxfUB0V9wxbKysgiHw4TDYf785z9z8803A+D3+8v8pabT6Uo9XkFBAQUFBWg0mlKvPxAIlNquuG6IBP9gMMimTZv48ssvef/99yv12stSVs8VRSEYDALQsmVLJk+ezLRp07jggguIj4+v8LUW/0EBnPS6TuxFyefU6/Vs2bKFe++9l3HjxtGvXz8uvPBCpk2bVmo7t9vNddddx2WXXUaPHj24/vrr+fHHH6PPU97z6/X6Ur0/cuQIZrO50n2Lj49nyJAhLFy4kC+++IKpU6eW+bpGjBjBggULyM7O5vLLL49Owfj8889Zt24dq1atYsKECaecHlHck5LC4XD0+wJwzTXXoCgKTzzxBHPnzo2u8/LLL9OmTRuA6HurWGX2OaDUdJri783//vc/PvzwQ0aPHs3QoUNxOBwcOnSo1HYVff/K+t4U71Ml69yzZw8pKSnl7qNl2blzJw0aNGDjxo0MGzbspPsrU/+JTnz/nrj/VyQcDtOnT5/oH9cQed+lpqbyww8/lNqvq2q/KtavXz+eeOIJfv75Z3r16kXfvn157733sFgs0T86Knq/lPWaT5xqVfL+8up68MEHueGGG1i2bBmffvop8+bN49NPPy1z2pYQZ0LeSeKsdNFFF7FgwQJ8Ph8+n4+vv/76tLbv378/7777Ln6/n3A4zOTJk8scGbLb7XTv3p3//Oc/QOSXxU033cSiRYtO+fh9+vThyy+/jD7+k08+yVdffUX//v35+OOPcTqdALz88sv87W9/O2n7vn378sMPP0TXmz17Nm+99RZJSUns3LkTn89HMBiMzj0sz4gRI3j66afp0KEDTZo0Oa3XrtPpoqG8d+/eLF++nIMHDwKRkfAjR45E50Z26NCBwYMH06dPn2j4qexrrciCBQtQFIX8/Hy++eYbBgwYwNq1a+nSpQu33XYbPXv2ZNGiRYRCoVLb7d+/H6fTyYQJE7j00ktZs2ZN9DWfSp8+ffjss88Ih8P4/X4eeOAB1q5dW+m+AYwePZr//ve/KIpS7kjx5ZdfzpYtW/jwww+58cYbgcgo4rhx4zjvvPO4//77GTZsGNu2bTtpW51OFw3G/fv359NPP8XtdgMwf/58LrzwQoxGIwDdunVjwoQJHDhwgA8//DC6zVtvvYWiKPj9fu65557oiPrpKB6d37JlC/v37+fcc89l2bJlXHfddYwYMYLWrVvz008/nfS9qcz370R2u53OnTuzYMECIBIqb7rpJrxeb6X30e+//57Vq1ezcOFCli9fzo8//njSOqeqv2TfS+rRowe7du2Kfq8q+o/QiYr3r927dwORUdtrrrmmzD/KznS/Kq92k8nEhRdeyJw5c+jXrx89e/Zkw4YN/Prrr1x00UXR56zo/XLxxRezcOFC/H4/wWAw+t6obF3BYJBLL70Ut9vNTTfdxNSpU9m9e3eZNQtxpmSkWZyVRo0axYEDB7j66qtxOBy0bNnytLa/9957ef7557nuuusIhUJ07Ngxesi6E82cOZOnn36aoUOH4vf7o4eJOtXo06hRo0hPT2f48OEoikLPnj0ZM2YMWq2WjIwMbrzxRjQaDY0bN+a55547afuLL76YXbt2Rf/93bZtW55++mnMZjMXXnghV1xxBSkpKfTq1euU01KGDRvGrFmzSoW7yr72du3aodPpuOGGG/joo4+YOnUq48ePJxQKYTabmTt3bvTDl8UmTZrE1Vdfzddff82IESMq9VorEhcXx/Dhw/F6vdxyyy306tWLNm3a8P3333PllVdiMBjo06cP+fn50SABkSB/ySWXcMUVVxAfH0+LFi1o27Yt+/fvjwbKsowfP54ZM2Zw7bXXEgqFuPLKK/nTn/7EgAEDKv2eOeecc0hISGDUqFHlPo/RaOTKK69kxYoV0WA9YMAAli5dytVXX43VaiUhIYGnn376pG379OnD/fffj8Fg4PHHH+fIkSOMGDGCcDhMy5YtmTlzZqn1TSYTzz33HLfffju9e/fm8ccfZ8aMGQwdOpRAIEDfvn3585//fMrvQ1kOHjzIsGHD0Gg0zJo1C4fDwe23386UKVP49NNP0el0dO7cmR07dpTa7uqrr67w+1eWF198kWnTpjF//nw0Gg0zZswgJSWl3H20pCNHjjB16lTmzp1LUlISzz33HPfddx9dunShUaNG0fVOVX/Jvk+ePDm6TVJSEjNnzuSRRx7BYDCc9ofX2rVrx1NPPcVDDz0UHVV/9dVXS40wFzvT/ar4A63Tp0+Pfki12OWXX873339P7969MZvN0fdv8Wh9Zd4vw4cPZ+/evQwbNgyr1UqzZs1KfcCyMnVNmjSJRx55JPrfnmeeeeaU+6oQp0ujVPS/FyGEEDXqwIED0WPTViY4CFHXLVu2jOzsbK699loApk+fjslk4v/+7/9iXJkQx8n0DCGEqEVefvllbrrpJiZOnCiBWZw12rVrx4IFC7jmmmu46qqryM3NPemwf0LEmow0CyGEEEIIUQEZaRZCCCGEEKICEpqFEEIIIYSogIRmIYQQQgghKlDrDzl37FhhrEuotex2E07nqU8qISomfVRPeqie9FA96aF60kP1pIfqxbKHKSlx5d4nI811mF5/emexE2WTPqonPVRPeqie9FA96aF60kP1amsPJTQLIYQQQghRAQnNQgghhBBCVEBCsxBCCCGEEBWQ0CyEEEIIIUQFJDQLIYQQQghRAQnNQgghhBBCVEBCsxBCCCGEEBWo9Sc3EUIIIYQQddeGDeuw2+No27bdGW3/88+LWbz4R558csZJ9y1c+Bmff/4pOp2OW2+9g379LiIvL49p0x7H5/PRoEEKkyZNxWw2q30ZMtIshBBCCCGqz1dfLSQr69gZbfvSSzN57bU5KEr4pPuys7P4+OP3efXVN5g1aw6vvTYHv9/PW2/9m8svH8Irr7xOu3Yd+PzzT9S+BEBGmoUQQgghRBUJBoO88MIzHDp0kHA4zIABA1m9eiU7dmyjVas0li//mZ9/XozH48HhcPDMMzNZtuxnPvnkw+hj6PU67rrrPjp16kLXrt0YMOCSMoPv1q1b6Nr1XIxGI0ajkaZNm7N79042bdrAmDG3AdC7d1/mzfsXI0eOVv3aqiU0BwIBHn30UdLT09FqtTz99NPo9XoeffRRNBoN7dq1Y+rUqWi1WubMmcOSJUvQ6/VMmjSJbt26VUdJQgghhBBnla+2ZLBw89EqfcxrujTiqs4Ny73/iy8WkJDg4LHHppCfn8d9991Fr159GDToT6SmppKfn89LL72CVqvloYfGs3XrFgYOvIyBAy+LPobDYSUvzw3AoEF/Yt26X8t8LpfLhc1mj962Wq04nU5cLhd2u73UsqpQLaH5559/JhgM8v7777N8+XJeeuklAoEAEyZMoFevXkyZMoVFixbRpEkT1qxZw0cffcSRI0e4//77+eSTqhlCF0IIIYQQNWv37l1s2rSeP/7YDEAoFCQ/Pw8ArVaLwWDgyScfx2KxkJmZSTAYZPHiH8sdaT4Vm82G2+2O3na73cTFxUWXm0zm6LKqUC2huXXr1oRCIcLhME6nE71ez4YNG+jZsycAAwYMYPny5bRu3Zr+/fuj0Who0qQJoVCInJwckpKSqqOs02Jb9iT+tCEEmvSOdSlCCCGEEKftqs4NTzkqXB1atmxFamoqY8fejs/n5e233yQr6xiKEmbXrp0sXbqEf//7bbxeL3fccQvAKUeaT6Vjx87Mm/cKPp+PQCDA/v17ad26DV27nsvKlcu58sqhrFq1gm7dulfJa6uW0Gy1WklPT+eKK64gNzeXuXPnsnbtWjQaDRD5y6CwsBCn04nD4YhuV7y8ZGi2203o9brqKPOU9OlLMedtJ9Tp0hp/7srS6bQ4HNZYl1HnSR/Vkx6qJz1UT3qonvRQvbO9h7feegtTp05hwoS/4HQ6GTXqJlq1as68ea/wwgsvEBdnZ/z4OwFo2LAhbnf+Sf06sYd2uxmDQR9d9vbbb9GiRQsGDryUsWPH8sADd6MoYR588EEaNkzk/vvH8/jjk/j664UkJjp4/vkXsFrVf080iqIoqh/lBM8++yxGo5GHH36YI0eOcOutt5Kfn8/q1asB+PHHH1mxYgWtWrXC5/Nx552R5g0bNow333yzVGg+dqywqsurFOuaWVjX/oOcW9cQtjeOSQ0VqexfYuLUpI/qSQ/Vkx6qJz1UT3qonvRQvVj2MCWl/Kkc1XLIufj4+Oj8kYSEBILBIJ06dYqG5qVLl9KjRw/OP/98li1bRjgc5vDhw4TD4VoxNQPA134YGhRMu76MdSlCCCGEECLGqmV6xrhx45g0aRI333wzgUCABx98kC5dujB58mRmzZpFWloagwcPRqfT0aNHD0aOHEk4HGbKlCnVUc4ZCTnSCKR0w7RzAZ7ud8a6HCGEEEIIEUPVMj2jKsVqegaAZcM87MufImf0UkKOtJjVUR75F1DVkD6qJz1UT3qonvRQPemhetJD9c6q6Rn1ha/tUBQ0mHYsiHUpQgghhBAihiQ0n0LY3phA096Ydn4OtXtAXgghhBBCVCMJzRXwtRuGPm83+qzNsS5FCCGEEELESLV8ELA+8bW5EvvSJzDtWEAwpWusyxFCCCGEqFM2bFiH3R5H27btTms7p9PJU09Nxu12EQgEuP/+B+nSpVupdRYu/IzPP/8UnU7HrbfeQb9+F5GXl8e0aY/j8/lo0CCFSZOmYjabVb8OGWmugGJOxN9iYNEUjXCsyxFCCCGEqFO++mohWVnHTnu7Dz54lx49LmTOnHk8/vhUZs16vtT92dlZfPzx+7z66hvMmjWH116bg9/v5623/s3llw/hlVdep127Dnz++SdV8jpkpLkSfO2vxbTvewyHVxNo2ifW5QghhBBC1ErBYJAXXniGQ4cOEg6HGTBgIKtXr2THjm20apXG8uU/8/PPi/F4PDgcDp55ZibLlv3MJ598GH0MvV7HXXfdx4033ozRaCh63BBGo6nUc23duoWuXc/FaDRiNBpp2rQ5u3fvZNOmDYwZcxsAvXv3Zd68fzFy5GjVr01CcyX4Wl2Oordi2rFAQrMQQggh6gTTto8xb32/Sh/T23EUvnNuKPf+L75YQEKCg8cem0J+fh733XcXvXr1YdCgP5Gamkp+fj4vvfQKWq2Whx4az9atWxg48DIGDrws+hgnHnIuOzuLp5+ezAMPPFzquVwuFzabPXrbarXidDpxuVzY7fZSy6qChObKMFjxpQ3GtPtLnAOeBp0x1hUJIYQQQtQ6u3fvYtOm9fzxR+QACqFQkPz8PAC0Wi0Gg4Enn3wci8VCZmYmwWCQxYt/LHOkuVOnLuzevYupUydx331/5bzzLij1XDabDbf7eLh2u93ExcVFl5tM5uiyqiChuZJ87YZh3vEZxgM/4299eazLEUIIIYQ4Jd85N5xyVLg6tGzZitTUVMaOvR2fz8vbb79JVtYxFCXMrl07Wbp0Cf/+99t4vV7uuOMWgHJHmvfu3cPkyROZNu1Z2rVrf9JzdezYmXnzXsHn8xEIBNi/fy+tW7eha9dzWblyOVdeOZRVq1bQrVv3KnltEporyd98AGFzIqadCyQ0CyGEEEKU4dprh/P889MZP/4uXC4n1103gtTUhsydO4epU2dgsVi4557bAUhObnDKDwgWf7Dv5ZdnAmC323nuuVm8//47NGvWnP79L+aGG0Zx3313Eg6HueuuezGZTNx66x1Mn/4kX3zxGQkJDqZOnVElr01Oo30a7Esew7z9Y7Ju2wBGW6zLkVN1VhHpo3rSQ/Wkh+pJD9WTHqonPVRPTqNdD/jaD0MT9GDa932sSxFCCCGEEDVIQvNpCDS+kJC9MaYdC2JdihBCCCGEqEESmk+HRouv3bUYD/6Mxpsb62qEEEIIIUQNkdB8mrztrkMTDmLa9VWsSxFCCCGEEDVEQvNpCjXoRDCxHaadn8W6FCGEEEIIUUMkNJ8ujSYyRePwarSFh2NdjRBCCCGEqAFynOYz4G13LbY1MzHtWojnvL/EuhwhhBBCiFprw4Z12O1xtG3b7rS28/m8PPXUZHJzc7FarTz++DQSExNLrfPoow+Rn5+HTqfHZDLz4ov/5NChg8yY8SQajYa0tDY89NBEtFr148Qy0nwGwo7WBFK7y1E0hBBCCCEq8NVXC095EpPyfPbZx6SlteWVV15nyJCrePvtN05a59Chg7zyyhvMmTOPF1/8JwCzZ8/izjvv4ZVXXkdRFH755WfVrwFkpPmM+doPw77sSXQ5Owklnd5fTkIIIYQQ9VEwGOSFF57h0KGDhMNhBgwYyOrVK9mxYxutWqWxfPnP/PzzYjweDw6Hg2eemcmyZT/zyScfRh9Dr9dx1133sWnTRm6+eSwAvXv34623SofmnJxsCgsLmTjxQQoLC7nllnH063cR27dv47zzLijari9r1qzm4osHqn5tEprPkK/tUGzLn8K0cwHuXv8X63KEEEIIIUr5/tA3fHPoyyp9zCuaXc2fml1R7v1ffLGAhAQHjz02hfz8PO677y569erDoEF/IjU1lfz8fF566RW0Wi0PPTSerVu3MHDgZQwceFn0MYrPCOhyubDb7QBYrVZcLmep5woEAowadQsjRoyisLCAe+65g06dOqMoChqNpmg720nbnSkJzWcobGtIoGlfTDs/x93zESj65gghhBBCnK12797Fpk3r+eOPzQCEQkHy8/MA0Gq1GAwGnnzycSwWC5mZmQSDQRYv/rHMkWabzYbb7QLA7XZHA3Sx5OQGDBt2PXq9nsTEJNq168CBA/tLzV92u10nbXemJDSr4Gs3jLjFj6DP3EiwYfdYlyOEEEIIEfWnZlecclS4OrRs2YrU1FTGjr0dn8/L22+/SVbWMRQlzK5dO1m6dAn//vfbeL1e7rjjFoByR5q7dj2XlSuX06lTF1atWs65555X6rnWrl3NJ598wMyZ/8TtdrN3725atmxNu3YdWLfuV84/vwerVq3g/PN7VMlrkw8CquBrcwWK1ohp54JYlyKEEEIIEXPXXjuc/fv3MX78XfzlL7fTqFFjOnXqwty5c9BqtVgsFu6553YefPBekpMbnPIDgtdddwN79+7hnnvuYOHCz7jttjsBeOWVl/njj8306dOP5s1bctdd43joofHcddd9OBwOxo+fwJtvzuPuu28jEAhwySWDquS1aRRFUarkkarJsWOFsS7hlOK/vgN9xnpybl0LWl2NPnfxX2JCHemjetJD9aSH6kkP1ZMeqic9VC+WPUxJiSv3PhlpVsnb/jp07kwMh1fFuhQhhBBCCFFNJDSr5G81iLDBhmmHnFZbCCGEEKK+ktCslt6CP20Ipt1fQ8gX62qEEEIIIUQ1kNBcBbzthqH1F2DcvzjWpQghhBBCiGogobkKBJr1J2xJxrTz81iXIoQQQgghqoGE5qqgM+BrezWmvd+j8ebGuhohhBBCCFHFquXkJp9++imffRb5YJzP52Pr1q3Mnz+fGTNmoNPp6N+/P+PHjyccDvPkk0+yfft2jEYj06dPp2XLltVRUrXzdL4Fy+9vY976AZ7z/hLrcoQQQgghRBWqltA8fPhwhg8fDsC0adO4/vrrmTp1KrNnz6Z58+bcdddd/PHHHxw6dAi/388HH3zAhg0beO6553j11Vero6RqF0ruiL9xLyyb5+PpfhdoZBBfCCGEEKK+qNZk9/vvv7Nr1y6uuuoq/H4/LVq0QKPR0L9/f1asWMFvv/3GRRddBED37t3ZvHlzdZZT7bxdb0VXsF8+ECiEEEIIUc9Ua2h+7bXXuO+++3A6ndjt9uhym81GYWHhSct1Oh3BYLA6S6pWvrQhhC0pmDf/N9alCCGEEEKIKlQt0zMACgoK2Lt3L71798bpdOJyuaL3uVwu4uPj8Xq9pZaHw2H0+tIl2e0m9PqaPT31mbOiXHArxmUv4uAYOKp3frZOp8XhsFbrc5wNpI/qSQ/Vkx6qJz1UT3qonvRQvdraw2oLzWvXrqVPnz4A2O12DAYDBw4coHnz5ixbtozx48dz9OhRFi9ezJVXXsmGDRto3779SY/jdNatE4Zo24wkafk/CKx4DVffJ6r1ueT89lVD+qie9FA96aF60kP1pIfqSQ/Vi2UPU1Liyr2v2kLz3r17adasWfT2tGnTeOSRRwiFQvTv359zzz2Xrl27snz5ckaNGoWiKDzzzDPVVU6NCdsb408bjPmP93H1fBj0lliXJIQQQgghVNIoiqLEuohTOXasMNYlnDbDoeU4Ph9JwaB/4DtnRLU9j/w1WzWkj+pJD9WTHqonPVRPeqie9FC92jrSLMdFqwaBpn0JJrbD8vtbsS5FCCGEEEJUAQnN1UGjwdNlLIbMjegzNsS6GiGEEEIIoZKE5mri63A9it6KRQ4/J4QQQghR50loriaKKR5vh+sx7fwcjTc31uUIIYQQQggVJDRXI0/XW9GEfJi3fhDrUoQQQgghhAoSmqtRKPkc/E16Ydk8H5RwrMsRQgghhBBnSEJzNfN2uRVdwX6M+xfHuhQhhBBCCHGGJDRXM1/aEELWVMzygUAhhBBCiDpLQnN10xnxdroZ4/6f0BYciHU1QgghhBDiDEhorgHezqNBo5XDzwkhhBBC1FESmmtA2N4Yf9pgzH+8D0FPrMsRQgghhBCnSUJzDfF0uRWtLw/Tri9jXYoQQgghhDhNEpprSKBpX4KJ7bH8/lasSxFCCCGEEKdJQnNN0WjwdB2LIXMj+owNsa5GCCGEEEKcBgnNNcjX4XrCBpt8IFAIIYQQoo6R0FyDFGMcvg7XY9r5ORpvbqzLEUIIIYQQlSShuYZ5utyKJuSLHElDCCGEEELUCRKaa1gouQP+Jr2xbJkPSjjW5QghhBBCiEqQ0BwD3i63ois4gHH/4liXIoQQQgghKkFCcwz40oYQsjbEvPntWJcihBBCCCEqQUJzLOgMeDvfjHH/YrT5+2NdjRBCCCGEqICE5hjxdh4NWj3WDfNiXYoQQgghhKiAhOYYCdsa4e04EvMf76F1Hol1OUIIIYQQ4hQkNMeQ+/z7gDCW9a/GuhQhhBBCCHEKEppjKBzfHG+H67FseReNKzPW5QghhBBCiHJIaI4x9wX3QziIdcNrsS5FCCGEEEKUQ0JzjIUTWuFrfx2Wzf9F486KdTlCCCGEEKIMEpprAfcF90PIh3WjHElDCCGEEKI2ktBcC4QS2+BrOxTz72+j8ebGuhwhhBBCCHECCc21hPuCB9AE3Fg2vh7rUoQQQgghxAkkNNcSoeQO+NpchWXTm2i8ebEuRwghhBBClCChuRZx93gArb8Qy6Y3Y12KEEIIIYQoQV9dD/zaa6/x008/EQgEuOmmm+jZsyePPvooGo2Gdu3aMXXqVLRaLXPmzGHJkiXo9XomTZpEt27dqqukWi/UoBO+1oOxbHoDT/c7UYxxsS5JCCGEEEJQTSPNq1evZv369bz33nvMnz+fo0eP8uyzzzJhwgT+97//oSgKixYtYsuWLaxZs4aPPvqIWbNmMW3atOoop05xXzgBrS8fy6a3Yl2KEEIIIYQoUi2hedmyZbRv35777ruPv/zlL1xyySVs2bKFnj17AjBgwABWrFjBb7/9Rv/+/dFoNDRp0oRQKEROTk51lFRnBFO64ms5CMvGeeB3xbocIYQQQghBNU3PyM3N5fDhw8ydO5dDhw5xzz33oCgKGo0GAJvNRmFhIU6nE4fDEd2ueHlSUlJ0md1uQq/XVUeZtZZm4ES0b/2JpN3vEe7zQLnr6XRaHA5rDVZWP0kf1ZMeqic9VE96qJ70UD3poXq1tYfVEpodDgdpaWkYjUbS0tIwmUwcPXo0er/L5SI+Ph673Y7L5Sq1PC6u9Dxep9NXHSXWbrZOJLS4GP3K2eS1HQ0GS5mrORxW8vLcNVxc/SN9VE96qJ70UD3poXrSQ/Wkh+rFsocpKeV/nqxapmdccMEF/PLLLyiKQkZGBh6Phz59+rB69WoAli5dSo8ePTj//PNZtmwZ4XCYw4cPEw6HS40yn81cPSag9WRj2fJOrEsRQgghhDjrVctI88CBA1m7di033HADiqIwZcoUmjVrxuTJk5k1axZpaWkMHjwYnU5Hjx49GDlyJOFwmClTplRHOXVSsPGF+Jv2w7L+VTxdbgF92aPNQgghhBCi+mkURVHKumPv3r0Vbty6desqL+hEx44VVvtz1FaG9JU4Foyg8KKn8Xa77aT75V9AVUP6qJ70UD3poXrSQ/Wkh+pJD9WrrdMzyh1pvvHGG+nYsSPlZGq2b9/OmjVr1FcnyhVo2gd/k15Y1/0Lb+ebQWeKdUlCCCGEEGelckPz4MGDmT59erkbPvHEE9VSkCjN3WMCjoU3Yd76Id4uY2JdjhBCCCHEWancDwIWB2a3283Ro0fJysriX//6F+np6aXuF9Ur0Kw/gUYXYP1tDoT8sS5HCCGEEOKsVOHRMx544AE2b97M3//+dwwGg3xYr6ZpNLh6TEDnTMe8/ZNYVyOEEEIIcVaqMDR7vV4GDRrE0aNHueuuuwiFQjVRlygh0OISAqnnYv1tNoQCsS5HCCGEEOKsU2FoDgQCvP3223Tu3Jldu3bh8Xhqoi5RkkaDu8cEdAUHMO1cEOtqhBBCCCHOOhWG5okTJ5KZmck999zDqlWrePzxx2uiLnECf6vLCDTogm3tPyB0Fp4lUQghhBAihioMzatWreJvf/sb8fHx3HLLLfzwww81UZc4kUaDq+/j6AoOYNn4eqyrEUIIIYQ4q5R7yLmPPvqIjz/+mN27d7N06VIAQqEQwWCQhx9+uMYKFMcFml+Er9WfsP76T7wdRoCjVaxLEkIIIYQ4K5Qbmq+99lr69OnDa6+9xl/+8hcAtFotycnJNVacOJmr3xMkvjcI2+q/w/BXYl2OEEIIIcRZodzpGdu3b6dZs2b86U9/Yu/evezdu5fdu3fLWQBjLORIw9PtdsxbP4Cjm2JdjhBCCCHEWaHckeaVK1fStWtXvv7665Pu69+/f7UWJU7N3eOvmLd/jO77x2Doh6DRxLokIYQQQoh6rdzQfNdddwHw7LPP1lgxonIUUzyuXv9H3JJHMe7+Cn/bq2NdkhBCCCFEvVbh0TNee+01evToQf/+/aNfIva8HW9CSe2CfcV0CMqxs4UQQgghqlO5I83FvvrqK3755RcsFktN1CMqS6sjdPkM9O9ei3XDv3H3eCDWFQkhhBBC1FsVjjQ3a9YMs9lcE7WI06S0ughf2hCsv81B6zoa63KEEEIIIeqtCkeaA4EAQ4cOpX379gBoNBpefPHFai9MVI6z72SS9g3Etup5Cgf9I9blCCGEEELUSxWG5jvvvLMm6hBnKJzQEk/3P2Nd9wqeLrcSbNg91iUJIYQQQtQ7FYbmw4cP10QdQgX3BQ9g3voR9mVPkjf8MzkEnRBCCCFEFatwTvPu3bvZvXs3u3bt4osvvuCXX36pibrEaVCMdly9J2I4+iumnZ/HuhwhhBBCiHqnwpHmhx9+OHpdURTuvvvuai1InBlvxxsxb34b28oZ+FoPBoMc7UQIIYQQoqpUONLs9/ujX4cPH+bQoUM1UZc4XRotrv5PonMewbphbqyrEUIIIYSoVyocaR4yZAgajQZFUTCbzdxxxx01UZc4A4EmvfC2uRrrulfwdhxJ2N4k1iUJIYQQQtQLFYbmn376qSbqEFXE1fdxTPt+wLbyWQovnx3rcoQQQggh6oUKp2eIuiUc3xx397sx7/gM/dHfYl2OEEIIIUS9IKG5HnKffx8ha0Psv0wFJRzrcoQQQggh6rxKheZ9+/bx888/c/ToURRFqe6ahFpGG64+j2HI3IBpx6exrkYIIYQQos6rcE7zO++8ww8//EB+fj7Dhg3jwIEDTJkypSZqEyr4Ogwn8Pt/sK18Fl/rK8Boi3VJQgghhBB1VoUjzV999RX/+c9/iIuLY9y4cWzcuLEm6hJqabQ4L3oKrSsT+6pnY12NEEIIIUSdVmFoVhQFjUaDpujUzEajsdqLElUj2OgCPOfegeX3tzCkr4x1OUIIIYQQdVaFofmqq65i9OjRHDhwgDvvvJPLLrusJuoSVcTVayKh+JbE/fQIBNyxLkcIIYQQok6qcE7zTTfdRN++fdmxYwetW7fmnHPOqdQDX3fdddjtdgCaNWvGyJEjmTFjBjqdjv79+zN+/HjC4TBPPvkk27dvx2g0Mn36dFq2bKnuFYnSDBYKB80i4bMbsK18FteAp2NdkRBCCCFEnVNhaB46dCgDBw5kxIgRtG7dulIP6vP5UBSF+fPnR5dde+21zJ49m+bNm3PXXXfxxx9/cOjQIfx+Px988AEbNmzgueee49VXXz3zVyPKFGjSC0+327BuehN/mysJNO0T65KEEEIIIeqUCqdnfP7553Tr1o3nnnuOcePGsXDhwgofdNu2bXg8Hm6//XbGjh3L2rVr8fv9tGjRAo1GQ//+/VmxYgW//fYbF110EQDdu3dn8+bN6l+RKJOr96MyTUMIIYQQ4gxVGJqNRiNDhgzhzjvvJD4+vlIjwWazmTvuuIM33niDadOm8dhjj2GxWKL322w2CgsLcTqd0SkcADqdjmAweIYvRZySwUrhoBfRFezHtuq5WFcjhBBCCFGnVDg9Y86cOXz77bd06tSJMWPGcOGFF1b4oK1bt6Zly5ZoNBpat25NXFwceXl50ftdLhfx8fF4vV5cLld0eTgcRq8vXZLdbkKv153GSzp76HRaHA5r5TdwXEro4F1Yf52H8dzhKC36Vl9xdchp91GcRHqonvRQPemhetJD9aSH6tXWHlYYmhMSEvjf//5HfHx8pR/0448/ZseOHTz55JNkZGTg8XiwWq0cOHCA5s2bs2zZMsaPH8/Ro0dZvHgxV155JRs2bKB9+/YnPZbT6Tu9V3QWcTis5OWd5lSL8x8hacd3aD6/j9xRP4LBUvE29dwZ9VGUIj1UT3qonvRQPemhetJD9WLZw5SUuHLvKzc0f/TRR4wYMYLMzExef/31Uvc99NBDp3zCG264gccee4ybbroJjUbDM888g1ar5ZFHHiEUCtG/f3/OPfdcunbtyvLlyxk1ahSKovDMM8+c5ksTp81gpfDSF3AsuBHb6udx9X8y1hUJIYQQQtR65YbmRo0aAZCWllZqefFJTk7FaDTy4osvnrT8ww8/LHVbq9Xy1FNPVapQUXUCTfvi6ToOy8Y38KddQaBJr1iXJIQQQghRq5X7QcDio1r8/vvvXHfdddGvFStW1Fhxovo4ez9GOL459p8ehoAn1uUIIYQQQtRq5Y40v/vuu7z66qvk5+fz/fffR5e3adOmRgoT1cxoo3DgCzg+H4lt9d9x9Z8a64qEEEIIIWqtckPz6NGjGT16NHPnzuUvf/lLTdYkakigWT88XW7FsvF1fGlXEGzSM9YlCSGEEELUShUePWPUqFF8+eWXBINBFEUhMzOTu+++uyZqEzXA2WcSxv0/EffTw+SO/F6OpiGEEEIIUYYKQ/P48eNJS0tjx44dmEymUicpEfWA0UbhpTOLpmm8gKv/lFhXJIQQQghR61R4RkBFUXjqqado3bo1//nPf0qdpETUD5FpGmOxbPw3+iO/xrocIYQQQohap8LQrNPp8Pl8eDweNBoNoVCoJuoSNczVZxLhuKbE/fQQBOVoGkIIIYQQJVUYmkePHs1bb71Fv379uPjii2nWrFlN1CVqmGK0UzhwJvq8PdhWPhvrcoQQQgghapUK5zQPHjw4ev2KK67AbrdXa0EidgLN++PudjvWTW8SaNILf5urYl2SEEIIIUStUGFoXr58OW+99RY+ny+67L///W+1FiVix9X3CQwZ64lb9DB5yR0JOdIq3kgIIYQQop6rMDQ/++yzTJo0KXpabVHP6YwUDJ5L4geDif/2bnKvXyiHoRNCCCHEWa/COc2NGzemb9++pKWlRb9E/RaOa0rB5bPRZW/DvvSJWJcjhBBCCBFzFY40JycnM2XKFDp16oRGowFg5MiR1V6YiK1Ay4G4ezyA7deXCTa+EG+nUbEuSQghhBAiZioMzcVHy8jKyqr2YkTt4r7wIQxHf8O+9HECqd0INegU65KEEEIIIWKi3NA8c+ZMHnnkEcaPH3/K+0U9ptVRcPkcEj8cTPy3d5E34msUU3ysqxJCCCGEqHHlhuZPP/2UI0eOlHmfoiisXr1aQvNZQLE2oGDwXByf3UDcTw9TMGQeFE3TEUIIIYQ4W5Qbml966aVTbjhqlMxxPVsEG1+Iq+/j2Jc/hWXj63i63xnrkoQQQgghalS5oblnz541WYeo5Tzn3onhyBpsK2cQaNidYOMLY12SEEIIIUSNqfCQc0IAoNFQeOmLhO1Nif/uHjRu+WCoEEIIIc4eFYbmY8eO1UQdog5QTAnkD5mH1ptL/A/3QzgU65KEEEIIIWpEhaH5gQce4L777mPx4sWEw+GaqEnUYqGUzjgHTMd46Besa/8R63KEEEIIIWpEhaH5vffe48EHH2TNmjWMGjWKf/zjHxw8eLAmahO1lLfjKLzn3Ij115cx7F8c63KEEEIIIapdpeY0N2zYkObNm2M2m9mxYwczZsxg5syZ1V2bqK00GgoHzCCU3IH4Hx9AW5ge64qEEEIIIapVhWcE/Otf/8rOnTu55ppreOGFF2jYsCEAw4cPr/biRC1msFAwZB6OD68k/ru/kDfsI9CbY12VEEIIIUS1qDA033jjjfTr1++k5e+99161FCTqjpAjjcJBs0j49i7iFj1I4Z/+BRo5IIsQQggh6p8KE05ZgRnAZDJVeTGi7vG3uRJnn8cx7/oC28pnYl2OEEIIIUS1qHCkWYiKeM77CzrnIazr5xKKa4a367hYlySEEEIIUaUq9b90p9PJtm3bcLvd1V2PqIs0Gpz9n8LX6nLsv0zBuPf7WFckhBBCCFGlKhxp/vbbb5k7dy6hUIghQ4ag0Wi49957a6I2UZdodRT86V84Fowg/vt7yRv2McGG3WNdlRBCCCFElahwpPmtt97iww8/xOFwcO+99/Ljjz/WRF2iLjJYyb/qLcLWVBK+Goc2f3+sKxJCCCGEqBIVhmadTofRaESj0aDRaLBYLDVRl6ijFGsK+VfPh3CQhC/HoPHmxrokIYQQQgjVKgzNF1xwAQ899BAZGRlMmTKFrl271kRdog4LJbYh/8o30RWmk/D17RD0xrokIYQQQghVKgzNDz30EMOGDWPEiBEMHDiQRx99tFIPnJ2dzcUXX8zu3bvZv38/N910EzfffDNTp04lHA4DMGfOHG644QZGjRrFpk2b1L0SUasEm/Sk4LKXMRxZS9yPE0AJx7okIYQQQogzVmFo/umnn9iwYQN//vOfeeedd1i2bFmFDxoIBJgyZQpmc+QMcc8++ywTJkzgf//7H4qisGjRIrZs2cKaNWv46KOPmDVrFtOmTVP/akSt4m97Nc6+kzHv/hLbihmxLkcIIYQQ4oxVGJpnz57NbbfdBsBLL73EnDlzKnzQ559/nlGjRpGamgrAli1b6NmzJwADBgxgxYoV/Pbbb/Tv3x+NRkOTJk0IhULk5OSoeS1V6oN16azcV3vqqas83e/C03Uc1g2vYd70n1iXI4QQQghxRio85JxerycuLg6AuLg4tNpT5+xPP/2UpKQkLrroIubNmweAoihoNBoAbDYbhYWFOJ1OHA5HdLvi5UlJSaUez243odfrTutFVYXl+3OZuXg3t/dtxcOXt8eor32nh9bptDgc1liXUbGrXyDsy8S+bCqWhq1ROlwZ64pKqTN9rMWkh+pJD9WTHqonPVRPeqhebe1hhaG5W7duPPzww3Tv3p1NmzbRqVOnU67/ySefoNFoWLlyJVu3bmXixImlRpBdLhfx8fHY7XZcLlep5cXhvCSn03c6r6fKvDC0Ey//vIc3V+xj5e4sZlzVkeaJtevIIQ6Hlby8OnLCmUtexpE3Av2CP5N37YcEG50f64qi6lQfaynpoXrSQ/Wkh+pJD9WTHqoXyx6mpJycRYtVOHw6efJkrrjiCjweD1dccQVPPPHEKdd/9913eeedd5g/fz4dO3bk+eefZ8CAAaxevRqApUuX0qNHD84//3yWLVtGOBzm8OHDhMPhk0aZY8mk1/K3QW35+zWdSM/3MuaddXy3NTPWZdVdBkvRMZwbkvD1bWjz9sa6IiGEEEKISqswNDudTvx+P6mpqRQUFLBgwYLTfpKJEycye/ZsRo4cSSAQYPDgwXTp0oUePXowcuRI7r//fqZMmXIm9Ve7ge0a8O6Y82nbwMYTX2/j6e+24wmEYl1WnaRYG5A/dD4oYRyfj5STnwghhBCiztAoiqKcaoWxY8eSmppK48aNIxtoNDz00EM1UhzAsWOFNfZcpxIMK/x7xT7+s/ogLZMsPHN1R9ql2GNaU139F5Au6w8cC25EMdjIu+4jwvEtYlpPXe1jbSI9VE96qJ70UD3poXrSQ/Vq6/SMCuc0K4rCzJkzq7Sgukiv1XBP/9b0aOFg8tfbGffueiZc0oYbzm0c/ZCjqJxQg07kXfsBjs9H4vhsRK0IzkIIIYQQp1Lh9IwOHTqwceNG/H5/9OtsdmGLRP439nwuaO7g74t2MfGLrRR4A7Euq84JpXQm/9r30QScOBbciLbgYKxLEkIIIYQoV4XTM6655hqcTufxDTQaFi1aVO2FFast0zNOFFYU3v31EP9ato8Um5HpV53DuU0TarSG+vAvIP2x30n4fBSKMY68YR8Rjm9e4zXUhz7GmvRQPemhetJD9aSH6kkP1aut0zMqDM2xVltDc7EtRwqY9NU2Mgq83NW3FWN7NkevrZnpGvVlxzwenOOLgnOzGn3++tLHWJIeqic9VE96qJ70UD3poXq1NTRXOD1j0aJF3HHHHYwdO5YxY8YwdOjQKi2uruvcOJ53x5zPoPYpvLp8H7f/bz07jzkr3lBEBVO6kn/Ne2j8BTgWjEBbcCjWJQkhhBBClFJhaH7ppZcYP348jRs35rrrrqN9+/Y1UVedYjfpmXF1R54b2pGMQh9j3lnPa8v3EQiFY11anRFM7Ub+Nf+LBOfPb0RbmB7rkoQQQgghoioMzampqZx33nkADB8+nMxMOcFHeQa1T+GDcT34U4cUXl91gDHvrGPL0do9vaQ2CaaeGwnO3rzIiLMEZyGEEELUEhWGZoPBwNq1awkGg/zyyy/k5ubWRF11lsNi4Kkrz2HWsM4UeoPc/r/1/PPnPXjlhCiVcjw450aOqlF4ONYlCSGEEEJUHJqnTZtGMBjknnvu4cMPP+See+6pibrqvIvaJPPBuB5c06UR8389xOj561h/KD/WZdUJwYbdi4JzTtGIswRnIYQQQsRWuUfP2Lt3b7kbtW7dutoKOlFtP3pGZazZn8uMH3ZyON/Ljd2bcN9FrbEadaoft75/Qld/dB0JX4xGMSdFToBib1Itz1Pf+1gTpIfqSQ/Vkx6qJz1UT3qoXm09eka5oXnMmDFlb6DR8N///rdqKquE+hCaATyBEK8s28cH69JpFG/i8cvb06tVoqrHPBt2TP3R30hYOBrFkkT+0HcIOdKq/DnOhj5WN+mhetJD9aSH6kkP1ZMeqlfnQnN5/H4/RqNRdVGVVV9Cc7GN6fk8/d0O9ud6uKZLQx4YkEaCxXBGj3W27Jj6jPUkfHkrAPlXvUWw0flV+vhnSx+rk/RQPemhetJD9aSH6kkP1autobnCOc3vv/8+gwcPZtCgQVx66aVynGaVzm2awLtjL+DWns35aksG17+5lk82HiYUrtXnmImpYMPzyLt+AYoxDsfnN2Lc92OsSxJCCCHEWabC0Pzuu+8yf/58BgwYwLPPPkubNm1qoq56zaTXMv6i1swfcz5tGth47sddjH1nHRvkg4LlCjnSyL1+AcGkDsR/fTvmLe/EuiQhhBBCnEUqdZzm1NRUXC4XvXr1orCwfk2XiKV2KXbm3tiNZ67uSL43yJ0fbOSJr7aSWeiLdWm1kmJNIe/aD/E3v5i4JY9iXf0C1O6zwAshhBCinqgwNMfFxfHjjz+i0Wh4//33ycvLq4Gyzh4ajYbLO6Tw0W09uKN3CxbvzOKG/6zlP6sP4A/KGQVPYrRRcOWbeDqOxPbry9h/egRCgVhXJYQQQoh6rsIPAjqdTg4ePEhSUhL/+c9/GDhwIL169aqp+urdBwErkp7v4aUle1iyK5tmDjMPXdKG/mlJaDSak9Y9qz9soChY187CtvYf+FtcQv7g18BoO6OHOqv7WEWkh+pJD9WTHqonPVRPeqhenf0goNFo5Ndff2XevHm0atWKHj16VGlxorSmCRZeuLYzc67vikGr5aEFW5jw2Wb258gOWIpGg7vnwxQO/DuGg7/gWHADGpec4l0IIYQQ1aPC0Dxx4kQyMjLo06cP+/fvZ9KkSTVR11mvV6tE/jf2fB68JI2N6QWMevs3/vnzHpy+YKxLq1W8nW6m4Mo30efuIvHTYejy9sS6JCGEEELUQxWG5qysLB555BEuu+wyJk6cSHp6ek3UJQC9TsvNFzTjk9sv5MpOqcz/9RDXv7mW99ely3znEvytBpE37CM0AReOT65Ff/S3WJckhBBCiHqm3NDs9/vx+/00a9aMTZs2AbBt2zZatWpVU7WJIsk2I5MHd+Ct0eeRlmzlxcW7ueE/a/l0fboc37lIsGF3cocvIGxKwLHgRox7vot1SUIIIYSoR8r9IOCll16KRqOh+G6j0Yjf78dkMvHNN9/UWIFn2wcBK6IoCmv25/GvZXvZmuGkdbKVe/u14uK2yWV+WPBso/Fkk/DlreiPbcLVZxKe7ndDBX2RD22oJz1UT3qonvRQPemhetJD9WrrBwFP+zTaNU1Cc9kURWH14UJmfred/bkeujSO477+renRwhHr0mIv4CZ+0YOYdn+Ft+1QCgfOPOWRNeQHnHrSQ/Wkh+pJD9WTHqonPVSvtobmCuc0i9pJo9EwpHMj3h/Xg8l/ak9moY97PtrE+I838cfRs/wPDYOVgsFzcfZ5HNPur0j85Br5gKAQQgghVJHQXMfptRqu6dqIT+/oyYOXpLE908Wt765n4sI/2Jd9Fv+lq9HgOf8e8of+D607E8dHV2Hc+32sqxJCCCFEHSWhuZ4w6SNH2vjsjgu5s08LVu3LZeTbv/L0d9s5lOeJdXkxE2jen9wR3xBKaE3C17dHTr0dDsW6LCGEEELUMfqKVpg7dy6vv/46ZrM5umzZsmXVWpQ4c3aTnrv6tmJE9ya8teYgH284zJdbMvjTOamM69mcNg3O7Kx5dVk4vhl5wz/F/vPj2H59GUPmRgoun41iTox1aUIIIYSoIyoMzV9//TW//PILFoulJuoRVSTRauTBS9owpkcz3vk1nU83HebbrZlc0jaZ23q1oFOj8ie610t6M85LZxJseB72XyaT+NFV5F/xOqEGnWJdmRBCCCHqgAqnZzRr1qzUKLOoWxrYTUy4JI2Fd/biz71b8NvBfG59dz33f/w76w7lxbq8mqXR4O1yC3nXfQwhH4mfXINp+yexrkoIIYQQdUCFI82BQIChQ4fSvn376HGAX3zxxWovTFQth8XA3f1aMbpHMz7ZeIT//XaIuz/YRPem8Yzr1YK+rRLPmuM8BxtdQO6N3xL/3V+I//GvhPK3wAWPgc4Q69KEEEIIUUtVeJzmNWvWnLSsZ8+e1VbQieQ4zeVTcxxDbyDEws1H+e/aQ2QU+uiQaue2Xs0Z2K4B2rMkPBMKYFs5A+vG1/E37kXB4FdRbKmxrqpOkuOSqic9VE96qJ70UD3poXq19TjNFYZmp9PJv//9bzIzMxk4cCAdOnSgZcuWp3zCUCjEE088wd69e9FoNEybNg2TycSjjz6KRqOhXbt2TJ06Fa1Wy5w5c1iyZAl6vZ5JkybRrVu3Uo8lobl8VfGmCoTCfLM1k7fXHORArodWSRbG9GjO4I6pmPRnx8FVEtO/Qffl/SgGO4WXvoi/1aBYl1TnyC8J9aSH6kkP1ZMeqic9VK+2huYKU9GkSZNo3rw5+/fvp0GDBjz++OMVPuHixYsBeP/995kwYQL/+Mc/ePbZZ5kwYQL/+9//UBSFRYsWsWXLFtasWcNHH33ErFmzmDZt2mm8LFEVDDot13RpxIfjejDjqnMw6LQ8/f0Ohs5bzdzl+8hy+mJdYrVTOl9P7g1fEramkPDVrdh/ngSBs/cwfUIIIYQ4WYWhOS8vjxtuuAG9Xs/5559POByu8EEvu+wynn76aQAOHz5MfHw8W7ZsiU7rGDBgACtWrOC3336jf//+aDQamjRpQigUIicnR+VLEmdCp9Xwp3NSeXfM+bwyoitdm8Tz5qoDDP33GqZ8vY2tGfV7xD+UfA65I77E3f1uLJv/S+KHQ9Bnbox1WUIIIYSoJSr1//fdu3cDcPToUXQ6XaUeWK/XM3HiRJ5++mmGDh2KoijRD5rZbDYKCwtxOp3Y7fboNsXLRexoNBoubJHIi8M688ntF3L9uY35eVc2Y99Zz5/f28CiHccIhk85o6fu0plw9ZtM3rUfoAm4cHxyLdZfZ8vJUIQQQghR8ZzmHTt2MHnyZHbv3k1aWhpPPvkknTpV/ti2x44d48Ybb8TpdLJ27VoAfvzxR1asWEGrVq3w+XzceeedAAwbNow333yTpKSk6PYejx+9vnJB/Wyj02kJhSoe+Ver0Bvg43Xp/HfVfg7lemiSYOaW3i258YJmJFjq/hEnyuyjJw/dNw+h3bqAcLNehK6dC45Tz+U/m9XUe7E+kx6qJz1UT3qonvRQvVj20GAoP3NWeMi5Jk2a8MEHH0Rvr1u3rsInXLBgARkZGdx9991YLBY0Gg1dunRh9erV9OrVi6VLl9K7d29atGjBCy+8wB133MHRo0cJh8OlAjOA8yyYU3umanKi/HWdUrnmnBR+2Z3Ne+vS+ft32/nnop1c3bkhI89vSqska43UUR3K7qMRBs7G1HQg9qVPoJt3Ec6Lp+Nrfz2cLUcXOQ3ywRf1pIfqSQ/Vkx6qJz1Ur7Z+ELDCkeZbb72VefPmodPpePnll1m2bBmfffbZKZ/Q7Xbz2GOPkZWVRTAY5M4776RNmzZMnjyZQCBAWloa06dPR6fTMXv2bJYuXUo4HOaxxx6jR48epR5Ljp5Rvli+qbZnOnl/XTrfbcskEFLo0TyB67o15pK2DTDWsaNuVNRHbcFB4n6cgPHIarxth+K8+Bk5BfcJ5JeEetJD9aSH6kkP1ZMeqldnQ/NPP/3Eu+++S0FBAf379+fee+/FYKi5f8lLaC5fbdgxs11+Fm4+yoLfj3I430uCWc/VnRsxrFujOjP6XKk+hkNY1r+Kbc1MwpYGFA56iUDz/jVTYB1QG96LdZ30UD3poXrSQ/Wkh+rV1tBc7pDg3r172bt3L61bt6Znz57Y7XauueYaDh06VC1Firop2Wbktl4t+OyOC5l9fRcuaO7g/fXpjPjPr9z9wUa+25qJL1gP5nZpdXguGE/eDV+gGO04Fo7CtmwaBOXQdEIIIcTZoNyR5jFjxpS9gUbDf//732otqqRYjTTvK9wLQEt7q1p7euna+tdslsvPl0Wjz+lFo89XdW7IdV0b0yq59o0+n3YfAx7sK6dj+f1tQvEtKbzkWQLNB1RfgXVAbX0v1iXSQ/Wkh+pJD9WTHqpXW0eaK5yeAZCbm8vBgwdp1qzZSR/Uq26xCs3jfr6JA679JJsacF7yBVzQ4ELOT+5BiqX2nGa5tu+YYUVh7f48Pvv9CEt2ZRMKK5zXLIHrujViYNsGmE/xCdWadKZ9NKSvwL7kUfR5e/C2H46z/1QUS3I1VFj71fb3Yl0gPVRPeqie9FA96aF6dTY0f/PNN7z00ku0adOGnTt3Mn78eK699toqL7I8sQrN2d4sVh1bwbqsX1mf/St5/jwAmttacH5yD85vcCHdk88jzhAfk/qgbu2Y2S4/X27J4LNNR0jP92Iz6risQwpXdWrIuU3j0cZwNF9VH4NerL/NxrruFRSDDWe/yfjOufGsO8JGXXov1lbSQ/Wkh+pJD9WTHqpXZ0PzyJEjefPNN7HZbDidTm699VY++eSTKi+yPLXhg4BhJczewj2sy1rLuuxf2ZizAW/IgxYt7RI6cH5yDy5ocCGdE7ti0plqrK66uGOGFYVfD+Tx9R8Z/LQzC08gTJN4E1d0asiVnRrSItFS4zVVRR91OTuIWzIRw5G1+Jv2xXnJc4QcaVVUYe1XF9+LtY30UD3poXrSQ/Wkh+rV1tBc4XGaNRoNNpsNALvdjslUc6GwttBqtLSJb0ub+LaMSLuJQDjAtrw/+C1rLeuzf+PDvf/jvT3zMWgNnJPQiW5J59ItqTudE7ti1dtiXX6totVo6NkykZ4tE5l4WYjFO7P4+o8M3lx1gDdWHaBr43iu6pzKZe1T6tSJU0JJ7cm77hPMf/wP24pnSHz/ctw9HsB93j2gM8a6PCGEEEKoVOFI8//93/+RnJxMjx49+PXXX8nLy+O5556rqfpqxUhzRdxBF5tyNrIhex2/525ke/42wkqoaCS6Pd2SutM18Vy6Jp1LgtFRZc9bn/6azSz08d22TL7cksGebDcGnYb+aclc1SmVvq2TMOiq79jPVd1HrSsD27InMe/6gmBiewoHPk+w8YVV9vi1UX16L8aK9FA96aF60kP1pIfq1daR5nJD84QJE3jppZcIBoN88MEH7N69mzZt2nDjjTfKcZor4Am6+SNvC5tyNrApZwN/5G0hEPYD0MreOhKik86lW9J5pJhTzvh56uOOqSgKOzJdfL01g2+3ZpLjDpBg1nN5hxQuPyeFc5skoNNW7Xzh6uqjcd8i7D9PQudMx9NpNK6+k1BMCVX+PLVBfXwv1jTpoXrSQ/Wkh+pJD9Wrc6F57NixNXpoufLUxdB8In/Iz478bZEQnbuBzbmbcAcjb4ZUc0M6J3ahk6MLnRK70ja+HQZt5f4oqe87ZjCssHpfLl/9kcHS3dn4gmEa2IwMat+Ayzuk0LVJ1XyAsFr76HdhW/Milk2vE7Y0wNX3CXzth4Gmbp01sSL1/b1YE6SH6kkP1ZMeqic9VK/OheaBAwcydOjQMjd66KGHqqaySqgPoflEoXCQ3YW72JSzkS25v/NH3maOeTMBMGqNdEjoSKeiIN05sQtJprIPY3Y27Zhuf4hle7L5cUcWK/bm4AuGSbUbuaxDCpe1T6FL47gzPp52TfRRn7kJ+5JHMRzbRKDheTj7P0mw0QXV+pw16Wx6L1YX6aF60kP1pIfqSQ/Vq3Oh+YorruCuu+4qc6PrrruuaiqrhPoYmstyzJPJlrzN/FEUoncW7CAQDgDQyNK4aCS6C50cnUmLa4tRZzxrd0yXP8gvu3P4cfsxVuzLIRBSaBRnigToDil0amg/rQBdY31Uwpi2fYxt1fPo3Bl4212Lq/djhOObVf9zV7Oz9b1YlaSH6kkP1ZMeqic9VK/OheYxY8Ywf/78aiuqss6W0Hwif8jPzoLt/JG7ORKm8zaT5T0GgF6jJy2uLd1Su9La0o72CefQyt4KnbbCg6HUO05fkKW7s/lh+zFW7cslGFZoEh8J0APbNaBTo7gKp3DU+M7pd2Fd/wrW9XMBcHe/C8/596EY7TVXQxWTXxLqSQ/Vkx6qJz1UT3qoXp0Lzc8//zwTJ06stqIq62wNzWXJ9GSwLe8PtuVvZUf+NnYUbMMZcAJg1plpG9+e9gnncE5CRzokdKSprRnaejZ39lQKvAF+3pXNjzuOsXp/HqGwQgObkYvbJnNx22R6NHeUeRSOWO2c2sLD2FY9i3nHZ4Ssqbh7/Q3vOSNAWzvOlHg65JeEetJD9aSH6kkP1ZMeqlfnQnNtIaG5fPEJZrak72B7/tZokN6Zvx1f2AeATW+nfUIH2sa3p118e9rGt6e5vQU6Td0LZaerwBtg2Z4clu7OZsXeHDyBMDajjr6tk7ikbTJ9WydhN0VG5mP9A05/dB325dMwHP2NQIPOuPpPJdC0b8zqOROx7mF9ID1UT3qonvRQPemhehKaz5CE5vKV9aYKhYPsc+5je/5WtudvY0f+VvYU7oke8s6kNZEW37YoSLejbXx70uLaYKzBMxnWNF8wzNoDuSzZlc0vu7PJcQfQazX0aOHgkrbJXH1eM0zhcGyLVBRMuxZiW/EMOmc6vtaDcfZ9grCjdWzrqiT5JaGe9FA96aF60kP1pIfqSWg+QxKay1fZN1UwHGS/cx+7Cnawq2Bn0eUOXEEXAFqNjpa2lrRNiIxGt41vR1pcmyo9EUttEQorbD5SwM+7slmyK4uDeV4AOjeKY0CbZPq1TqJ9qu2Mj8ShWtCDdcPrWNbNQRPy4+k6DvcF41EsZR9BpbaQXxLqSQ/Vkx6qJz1UT3qonoTmMyShuXxq3lSKonDEc5hd+TvYWRSidxXsJNuXFV0n2dSAtLg2tI5rQ1p8G9rEtaW5rSXGenJaaEVR2JvjZvWhAr7dfJQ/jkbeaw1sRvq2TqRf6yR6tkyMTuOoSRpXJrbVf8e87UPQmXGfewee7nehmBNrvJbKkF8S6kkP1ZMeqic9VE96qJ6E5jMkobl81fGmyvFls6dgN3sKd7GncDd7Cnez37k3evg7nUZHc1sL0uLaklYUplvHtSHV3DB2o7MqFfcx2+Vn5b4clu/JZdX+HJy+EDqthu5N4+nXOom+rZNIS7bW6OvU5e7CumYW5l0LCRvj8Jx7J57ud6IYy9+pY0F+SagnPVRPeqie9FA96aF6EprPkITm8tXUmyoUDnLQdZC9hcVheg97CneR4TkaXceqt9LS3ppW9ta0tLeiVVwareytSTGn1vowXVYfg2GF3w8XsHxvDiv25rDzWGQqS6M4E/3SIgH6whYOLIaa+VClLusPbGtexLT3O8ImB+7z/oKn621gtNXI81dEfkmoJz1UT3qonvRQPemhehKaz5CE5vLFesd0BpzsK9zDnsLd7HPuZb9zL/sK95Lrz4muUxfCdGX6mFHoY0VRgF69PxdPIIxBp+HcJvH0bJlIz5aJnJNqR6et3tekz9yEdc1MTPt/ImxJxn3+fXi6jAG9pVqftyKxfi/WB9JD9aSH6kkP1ZMeqieh+QxJaC5fbd0x8/357HPuYX/hPvY590avlwzTZp2FFraWtLC3oLm9ZdH1ljS1NqvxI3mcbh/9wTAb0vNZsTeXNQdyo6PQCWY9PVo4IiG6hYNmjuoLsvqjv2FbPRPjoV8IWRvivmA83s43Q4yOglJb34t1ifRQPemhetJD9aSH6kloPkMSmstX13bMfH9eJEQX7uWgaz8HnJGvTG9GdB0tWhpZG9PC1jISposCdXNbCxKMjmoZnVbbx2yXn7UH8lizP5fV+3PJdEYO79c0wUyvlon0aungguYOEiyGqio5ynB4FdbVL2A8vJqQvQnuHg/gPedGqOEPa9a192JtJD1UT3qonvRQPemhehKaz5CE5vLVlx3TE/RwyHWAA679HHRGLg8493PQdSB6fGkAuz6OZrbmNLM1o5mtBU1tzWhua0FTa3NshjOf21uVfVQUhf05HtYcyGX1/jx+O5iHyx9CA3RsFMeFLRxc0DyBc5skYDVW0XxoRcFwaBm21S9gyFhHyN4Yz7l34u10c42dmru+vBdjSXqonvRQPemhetJD9SQ0nyEJzeWr7ztmSAmR6cmIjEi79pPuOsihoq+So9MAicakokAd+Wpqa04za3MaW5tgqWC+b3X2MRgKs+VoIWv257F6fy6bjxYSCivoNNCpURznN6/CEK0oGA4swbr+FYzpKwmbEvB0GYun2+0o1pSqeUHlqO/vxZogPVRPeqie9FA96aF6EprPkITm8p3NO6Y35OWwK51D7oMcch3gkOsg6a5DHHIdINefW2rdZFMDmlibRr5sTWlqbUYTa1Oa2poRZ4iv0T56AiE2HS5g3cE8fjuYX20hWp+xHuv6VzHu/gZ0RrznjMDd/e5qO8Pg2fxerCrSQ/Wkh+pJD9WTHqonofkMSWgun+yYZXMGnKS7DpLuPsRhdzqH3emkuyLXS568BSDOEEeLuBakmhrTxNqUxtYmkS9LE1LMKei01XtiE08gxKb0An47FAnRW04I0ec1c9C9aTzdmsSf0ZxoXd4eLOtfw7ztI1CC+NKuxHP+vQRTu1Xp65D3onrSQ/Wkh+pJD9WTHqonofkMSWgun+yYp88b8nLkhCCd6T/C/oIDZHgyCCuh6Lo6jY6GlkY0tkSCdCNrJFg3sjSmsbUp8Yb4Kv9gYnkhGiAt2Ur3pgmc2zSe7k0TaBxvqvTza1yZWDe9gXnzfLT+AvxN++E+/14CzQdAFbwGeS+qJz1UT3qonvRQPemhehKaz5CE5vLJjlk1ivsYCgfJ9GZyxH2YI57DHHUf5rD7MEfchznqOUyeP6/Udla9lUaWxjS0NKahpRGNLI1oZGlMI2vkdrwhQXWo9gZCbDlayMb0Ajak57PpcAEufyTYp9iNnNskge5FIbptiq3C40Rr/IWYt7yLZeO/0bkyCDTojKf73fjaXq3qiBvyXlRPeqie9FA96aF60kP1JDSfIQnN5ZMds2pUto+eoJsj7iMc8aRHLt3pZHiOctRzlAzPEVxBV6n1zToLjSyNigJ1YxpaG9PI0ohUc0NSLQ1JMiWj1WhPq9ZQWGFPtosN6QVsTM9nQ3oBGYU+AGxGHV0ax9GtSTxdGsfTuVFc+VM6Qn5MOz7Duv5V9Lm7CFtS8HS+GW/nWwjbG59WTSDvxaogPVRPeqie9FA96aF6EprPkITm8smOWTWqqo/OQCFHPUc46j4SDdNHPZHrGZ4jFAZKv5f1Gj0p5lRSLZEQnWpuSENLQ1KLgnVDS0MsemuFz3u0wBsdid54uIDdWS6KZnTQMtFClybxdG0cR5fG8bRpYENfcjRaCWM4uBTL729h3LcINFp8aVfg7TaOQONelZ66Ie9F9aSH6kkP1ZMeqic9VE9C8xmS0Fw+2TGrRk310RlwcsybQYYng0xPBhmeo2R6j1/P8mWVmlMNkWNTp1pSSTEXfVlSSTU3LHXbrDOX2sblD7L1qJPfjxSw+Ughm48UkOMOAGDWa+nUKBKguzaOo0uTeBrYItMytPn7sWz+L+at76P15RNMPgdP13F42w8Hw6nDu7wX1ZMeqic9VE96qJ70UD0JzWdIQnP5ZMesGrWlj6FwkGxfdiREe4+SWRSuM72ZHCv6yj9hXjVAvCGeBuZUUs2ppFga0sDcgAamFFLMqTQwp5BsakC+W8eWI8eD9PZMJ8Gi4ehGcSY6NoqjU0M7HRvF0TlZT4MDX2H+/T8YsrYQNsbj7TgST5ex5R6yrrb0sC6THqonPVRPeqie9FC92hqaq/x4WoFAgEmTJpGeno7f7+eee+6hbdu2PProo2g0Gtq1a8fUqVPRarXMmTOHJUuWoNfrmTRpEt26Ve1hsISoS3RafXSqRhfK3hd8IR9Z3mMc82aS6c3gmCez6HomWd5M/sjbQkEg/6TtzDoLDcwppNhS6NAlhd7GBoT88eQ5rWTkmtl+LJ/FOw1A5NjQzR1pdGr4DwY13MdFeQtotOk/WDf+G3+LS/B0vQ1/i0tAW0VnNBRCCCHqgCoPzQsXLsThcPDCCy+Ql5fHsGHDOOecc5gwYQK9evViypQpLFq0iCZNmrBmzRo++ugjjhw5wv33388nn3xS1eUIUa+YdCaa2prR1Nas3HWKg3WW71jk0psVDdpZ3mNszF5Pti+LUPFUEC3QEOIbarDpEjDiIBSIY7XbxuJsG0qwHQm6Dtxg3MWNR1bS8sCtBCyN8J0zgmDnUYQTWtbMixdCCCFiqMpD85AhQxg8eDAAiqKg0+nYsmULPXv2BGDAgAEsX76c1q1b079/fzQaDU2aNCEUCpGTk0NSUlJVlyTEWaUywTqshMnz55HlzeSY9xg5vmyyvVlk+Y6R480my5eF1pxO2J+LgoIPeBd4FysoLYkPQdOMj0g5/D4GEtDFdcbW4AI6pLakmb0hSeZkkozJGFUcxk4IIYSoTao8NNtsNgCcTicPPPAAEyZM4Pnnn48er9Zms1FYWIjT6cThcJTarrCw8KTQbLeb0Ovl38Bl0em0OBwVH11BnNrZ2sck7KRRfrAGCIQD5HhzOObO5JjnGJmeY+zLPcKe3CNkFhxkh/cgPgrID64jnLEeMkpvb9HFkWJpQCNbCsnmZJItDUgyJ9HA3IBkS3JkmbkBDpMD3Vk+3eNsfR9WJemhetJD9aSH6tXWHlbLOYKPHDnCfffdx80338zQoUN54YUXove5XC7i4+Ox2+24XK5Sy+PiTp587XT6qqPEekE+bFA1pI+nZiKOZvo4msW1gTgg9YQVFIVwxiqOrnoDTfYv5GlCbNWnsoSW/KFNZrfexwFjJgbjHsLaAkL4T3oOLVoSjA4STUkkmZJINCWRaCy6NCWSaEyMLkswOtBX8+nNY0Heh+pJD9WTHqonPVTvrPkgYFZWFrfffjtTpkyhT58+AHTq1InVq1fTq1cvli5dSu/evWnRogUvvPACd9xxB0ePHiUcDsvUDCHqIo2GpHMGoW3UB42vgNa7FtJn6wfcnfELYY2ePYkXsUi5nC9zO7E9248/7EWjL0RvdNIwIUBygpc4qweD0YWiK6QwkMsB535y/bkEwicHbIB4Q8JJgdphTCz6cuAwJZFoTCTB6MCmt1X56c6FEEKcfar8kHPTp0/nm2++IS0tLbrs8ccfZ/r06QQCAdLS0pg+fTo6nY7Zs2ezdOlSwuEwjz32GD169Djp8eSQc+WTv2arhvRRvbJ6qMvejnnbh5i3f4zWk03Y5MDT5ioONLqCdZzDziwPO4+52HnMSabzeDhOshpo28BGWrKV5skaGsT7sds8eMMF5PpyyPPnkuvLIdeXe/y6P+ekMzIWM2gNOIoCdKIxEYepKFgXLUswOkrdjlXIlvehetJD9aSH6kkP1autI81ynOY6THbMqiF9VO+UPQwFMB5cimnnAkx7vkMTdBOyNcLX7lp87a8j2KAzed4gu7Nc7DjmYtcxJzuPudiT7cYXDEcfpkm8ibQGNtoUBeo2DWy0SrJi0kdORe4P+cn355Hrj4TpPH8ueb7i63nk+XKP3+fLxRcue+qXXqMvFaZLhup4Y0J0WYIxgQSjg3hDQpVMF5H3oXrSQ/Wkh+pJD9WrraG5/k0MFELULjoD/laD8LcaRGHAjWnfD5h2LMCy6U2sG14j6GiDtf0wkttdywXNj/+HKqwoHM73sjvLzZ5sF7uzXOzOcrNqX270xCxaDTR3WGjTwEbrZCtpyVZaJTWlW2JbzIZTf7DQE/SQ78+LhuoCf370emR5Hvn+XDLyj5Lnz8MVdJb7WDa9PRqiEwxFYdqYQIIhgXhjQonr8SQYHcQZ4jFoDVXTXyGEEDVCRprrMPlrtmpIH9U7kx5qvLmYdn+NaecCDOmr0KAQSD0XX7th+NoNJWxrVOZ2wVCYA3ke9mS5I0E6O3J5KM9DUZZGAzR1mGmVVBykrbQuurSbzmysIBAOUODPJ9+fT34gL3JZFLZL3o4uD+TjDXnLfTyr3kq8IYEEYwLxhgQa2JIwYyPemECcIZ4EQwJxxnjiDfGR4G2Ix6a3y/zsU5B9WT3poXrSQ/Vq60izhOY6THbMqiF9VE9tD7XOw5h2fhEJ0Md+R0FDsHEPfGlX4ksbQji+eYWP4Q+GOZDrYW+Om73ZLvZme9ib4+JArodA6PiPuVS7MRqiWyZZaZlooWWSlVS7scoDqTfkpcCfT0EgnwJ/AQWBSOiOBO38ovsKyPfn4QoVkus99Yi2VqMjzhBHvCGeuBJhOs4QCdf24vuMkWVxhjjiDPHYDXZ0mvp/SD/Zl9WTHqonPVRPQvMZktBcPtkxq4b0Ub2q7KEudzemXQsx7f4GffYfAARSuuJPuxJfmysIJbY9rccLhhXS8zzsy3GzJ9vNvhw3e4suPYHjc6atBh0tEi20TLKUCtMtEy0VTvWoCsU9DIWDFAYKI0E7UEiBP5/CQEE0YBcECkrczqcwUEhhoAB38NT9t+vjiDPGEacvCtPGeOL0cdgNccQZ4qKBu/h2XFEYt+qtdWZ0W/Zl9aSH6kkP1ZPQfIYkNJdPdsyqIX1Ur7p6qM3fh2n3N5j2fIMhYx0AwcR2+NpciT/tCoINOsMZBjpFUch0+tmf42Z/rid6eSDHzZECHyV/MDaKM9EyyUKLRCvNEy20cFhonmihSbwJvU5bBa9UfQ+D4SCFgYJoiC4sDtj+gqIQXhBdHlmnEGfR9egp1cug1eiw6+3YDfZouLbrjwftyPJIELfp7aVGt216e40eU1v2ZfWkh+pJD9WT0HyGJDSXT3bMqiF9VK8meqh1HsG459tIgD68Co0SJhTXHF/aFfjaXEmw0fmgqZoA6w2EOJjnYX+Oh/25bvbnREaqD+Z5cPqOB0ydVkOTeFM0TDd3WGiRaKZ5ooVGcWZ02soH+li9DxVFwRvyUBAowFkUpiOBujAasIuXO4POaNB2Bpw4g4UEwoFTPr5ZZ8FusBcF77hoALdFr8dFA3b0vhKXp3MqdtmX1ZMeqic9VE9C8xmS0Fw+2TGrhvRRvZruocaTg2nv9xj3fI3x4C9owgHClgb4Ww7E13IQgRYXoxjL/8F3phRFIc8T4ECuhwO5Hg7meThY4nrJ6R4GnYamCWaaOSyRr+h1M00SzBhOGKGui+9DRVHwhX3HQ3VRmC4MFuAKuHAGI7ddRQHbGXCWuD9yPUz4lM9h1BpLhejiS5vBFg3axddTE5LAb4jcr7fFZLS7rquL78PaRnqonoTmMyShuXyyY1YN6aN6seyhxleAcf9PGPf9iPHAYrS+fBStnkDjXvhbXYa/1SBCjrSKH0glRVHIcvkjAbpEkE7P93LohECt1UDDOFM0RDdLsNChaQKJBi1NEsxnfISPukZRFDwhdyRYB51lXjqDxaH7+KUzUIgr6MIVcJZ7vO2STFoTNkMkSFv1Nux6O1a9DZuh5G1rdJ3i9YrDt1Vvw6K3oK2i/2TUZvLzUD3poXoSms+QhObyyY5ZNaSP6tWaHoaDGI7+hnH/Ioz7FqHP2Q5AMKF1JEC3HESgSU84jX/5VwVFUch2B0jP83AoLxKijwdqL3me0lMcEsx6mjosNIk307RoZLpp0VejuKqbR10fBMPBaMDWWkIczc3CHXQVjXRHwrcr4IpcBl24gy6cAWdknWBkeUUfoixm1VuLwnQkSFv11qKQbY8ut+it2IrWK7m+pWhdq95Wq4/RXWv25TpMeqiehOYzJKG5fLJjVg3po3q1tYfagoPRAG1MX4Em5CNssBNoMSAyjaP5AML2xrEuE6cvSH5IYevBPA7ne0nP9xZdejhS4IuezAWOj1I3TYiE6SYJZhrHm2kSb6ZxgpkGNuNpzaWuT870fRhWwriD7qIg7cQVdOMqEazdQRfuoDt6vWTYPnGdyjBoDcdDtS5yadFbi0K2FavOWjS6XXLZyetZdFbMOnOVHt2ktu7LdYn0UL3aGprPjv8BCiHOSuH45ni7jsPbdRwE3BgPLYsE6P0/Ytr9NQDBxPb4m19EoPkA/E37gMFa43XaTXqaOaw0tZ48AhkKKxxz+kgvFaa9pOd5WbYnhxx36VFqvVZDo3hTiSBtklBdAa1GG/mwosEONDzjxwkrYTxBD+6QOxqii0O1pyh0e0qE7+g6ITf5/lwOu9PxBN24g248ocoFBi1aLHoLZp3leJiOBm8rFr0NS9F9Fp0Fi96KRW/BqotMObHqrdFtLTor8Yr5jF+/EPWdjDTXYfLXbNWQPqpX53qoKOiyt2I8+AvGg0sjR+MI+VC0BgKNe+BvfjGB5hcRTOlaZUfkqMiZ9tAbCHG00MeRAi9H8r0cLvBxJN/LkYLI9WyXv9T6eq2G1DgTjeJMNI430TDeTOM4E43iTTSKj0z/qInjUleHOvc+PIWSAdwTdOEKuosCtatombtEQHeXCNue6HV3yBVdrzJzv4uZdWYsOgtmvQWLrjhkRy4tOmvRckv0tqX4dtF1s84cCecl1jmbPoxZn96HsVJbR5olNNdhsmNWDemjenW+h0EvhiNrMR78GcPBXzBkbQEgbE7E3+wiAs0vwt98AOG4ptVWQnX1sKxQfbTAS0ahjyMFPo45fYRP+C2QaDHQKN5Ew7jIKHXDOFOpr+RaOlpd59+H1SikhPAEPUWh2lUikEdCtqcofIf1AfJchcfXC3lKbOcucdtd4eEGSzJoDUVhvESwLppeUjxSXnxZOrSXWKa3Hr9PZ8ast2DSmmrdyXfkfahebQ3NZ8+ffkIIUR69mUDzSDgG0LiPRUahD/2C4cBSzLsWAhBMaEWgaR8CTfsSaNK7VsyHrojZoKNVkpVWSWVPOwkWTf84WuDjaKE3clkQCdn7cz2s3p9b6sgfEDk+dYrNeFKYbhhnomFR2HZYDGhrWZg5m+k0uhJTUFLKXe90wkogHMAb8uAOuvGGvNFQ7Q168YRKB2xP0FNineL1PWT5svC6I8u9IS+ekIfwKU62cyINmmioNuvN0ZAdCd1mTNrI8sgyc4l1iy5LLtOdsExvPitOPy8qT0aa6zD5a7ZqSB/Vq9c9VBR0OTsi0zjSV2I4vAqtvwCIHJUjGqKb9iZsa3TGT1Nbe6goCoW+IBmFvlN+BUKlf5UYdJFgnRpnItVuIsVuIjUuErQjt400sJvQV+GIdW3tYV0S6x4qilIUxr3RcO0tGun2hjzRsF3ytrc4kBcvCxZf9x5fp2h5RccFP1HxCLnppFBdfNtU4r7I9UR7PIpfi6loO0t0e3OJx4pcGrXGs+JQhqerto40S2iuw2L9w62+kD6qd1b1MBxCn/1HJECnr8RwePXxEO1II9CkD4FmRSPRtsp/qKwu91BRFHI9ATIKI6PUmYU+Mp1+Mp2R68eckdu+YOnAotVAss0YCdR2Iw2KQnYDm5FUu4kG9sil3aSr1L/g63IPa4v63MNIIPfjDfmKwnSJsB2MBGtfyBcN2b6QLxq+fdEA7iu6z1u0nrdoGw+ekPe0RsmLmbSmEkG69HWzrmi0vMR90XW0JdYpta0Jo7b08roWziU0nyEJzeWrzz/capL0Ub2zuofFIfrQCgyHi0N05OdW0JFGoNGFBBv3INC4Z+QkK+WEv/reQ0VRyPcGIwG60E9GyUBdGAnYWS4/Bd7gSdua9droyHQkXEdGqlPsRpJtkbCdYjfRJDWuXvewJtT392F1C4aDmOyQkZNbZgD3Fy3zRYO7F1/YFw3mkTDuKxXK/SEf3nDx/T78YX/FhZTBqDVi1pkx6kxFgduMSWeMXGpNkeWlrkcui0O9qYzrRp2p9HVt5LbaD35KaD5DEprLJz/cqob0UT3pYQnhEPqsLRjSV2A4vAbD0bVovbmRu8xJBBpfSKBRDwKNLySY2hV0JkB6WMwbCJHl8nPM6eeY08exohHrrOLbRfedOGoNYDPqoiG6gc1IA/sJlzYTyTYDcSZ9rfvwWG0h70P1qruHISWEP+SPBOtwJEj7okE7EtB9Yd8Jy734wz68Id/x4F5qW1/p2+FIeD+TkXMArUaHSVs6eBcHbKPWeMrAbdSZaJnUlJ4JF8VkTrmE5npKfrhVDemjetLDU1AUdHm7MRxZi+HIWvRH1qLP3xu5S2cimHougcY9MLa9iLy4LijmxBgXXPsVz7POcvnJcvqjl4XBMIey3WS7IuE6y+nHW0a4Nuo0JNsio9TJ1uOj1ck2Q3R5A5uRJKsRo77u/Eu7Ksi+rF596mEwHCwRwr3REF4cyv0lwnnJUB69v2SAD5d9X8lloaKQbtaZefviD0gxl/+h1eoiobmeqk87ZixJH9WTHp4ejfsYhqO/YjjyK4Yja9Af+x1NODItIehII9jwPAINzyPY8DyCyR1r/LTfddWJ70NFUXD5Q9Fgne3yk+0uunQVLwuQ7fKT6yn78Gl2k45kq5Ekm5Fkq4GkopCdZDVElpVYXh8CtuzL6kkPz1woHMQf9uNw2PA5YxNP5ZBzQghRiyjWFPxpV+BPuyKyIOAh0b0N767lGDLWYzj4C+btn0TW1ZkIpnSJhuhAw/MIxzUvd260OE6j0WA36bGb9LRKPvWZHoOhMDnuQDRUZzn95LgD5BSHbHeAncdcZLtzcfrK/pe13aQjyRoJ1IlFl8XXk4suE4uWyRQRIU6m0+qxaPVY9BZ81L4/PCQ0CyFErBksKC374Uk4Dw+AoqAtTMeQsR59xnoMmeuxbJ6PZuPrAIQtycdDdOq5BFO6oViSYvoS6jq9Ths5PF6cqcJ1fcEwuSXCdE7RCHaOK0COO0Cux8++bDfrDvrJL+ODjRA5M2NxoE60GEi0Fn1Fr5debjVU7ggiQojqI6FZCCFqG42GcHwzfPHN8LUbGlkWCqDP2RYJ0UVh2rTvx+gmIXtTgqndCKZ0I5DaVYJ0NTLptZFTjsebK1w3GFbI80SCda47QI7neLjOcfvJ8wTIdQc4kOsm1xM46UQyJZ/TYYmEaofVcPx60e3i68WX8Ra9nFxGiComoVkIIeoCnYFgSleCKV3xdhkLgMaXj/7YZvSZm9Af+x39sU2Y9nwT3SQSpCMBOpDSlWBqNxRLcqxewVlJr9VEj+ZRGd5AiNyiIJ1bNGp9/HrkMs8T4ECuh3xPAJe/7KkiWg3Em4tDtJ6E4oBtMRRd10dvF3/ZjHL2OyFORUKzEELUUYopgUCzfgSa9YsuiwbpY79HvjI3YdrzbfT+kL0JwQadCTboVPTVmXB8C6hDJz6oz8wGHY0NOhpXYhQbIlNF8j2RQJ1XFKhzPZHL4lHsfG+Ag3kefj9SSJ4nQChc9gesdFoNSVYjcSYdCRYDCWZ90WVR8DYbSIheRq7Hmw1VelZHIWozCc1CCFGPlB2kC9BnbUafGRmN1mdtxbj/JzRFh3cKG2yEGnQimNzpeJhOOgcMlli9DFFJJn3l52LD8SOK5HkC5HsC5HmCkeveSMh2hxQy873keyJBe/ORQvK9gZNOk16S3aQj3lwUss3Hw3SCWU+85fjy+KIQHm/WE2fSo5OwLeoYCc1CCFHPKaZ4Ak37Emja9/jCoAd9zg70WX+gz9qCLmsrpu2fYNn8dmQbjZaQI41gg86Eks4hmHwOweQOhOOayah0HVbyiCLNHCf/UVTW4dIURcETCJPvjQTtfE8wGrKLr+d7gxR4I7fT8z0UeIMUeIOc6qBhcSY9cWZ9JFyb9cSZigN3JFQnmA3EFd0X+YoEbrNeKx+KFDEhoVkIIc5GegvB1HMJpp57fJkSRltwEH32H+iPbUGfvRXD0d8w7/w8ukrYYCOU1J5gUgdCyecQTIqEacXSQA6DV09pNBqsRh1WY+WnjQCEwgpOX7BUoC4O2IXeAAXeYNH1yP1HCnwUFN13ioFt9FpNNFjHm/VFwdpAvEkfDdnF99lNx2/HmfVyFBKhioRmIYQQERot4YSW+BNaHj+GNKDxF6LL2YE+exu6nO3os7dj2vcD2q3vR9cJm5MIJncgmHQOoaQOhJLaEUxsJ0fwOIvptJqiuc8GoPJTfYqnkBQUBer8ooBd4CsO2EEKfYHo9Vx3gH05Hgq9QZy+U49u6zRgLwrQcabjYbrkdbvp+G27SVfqfpOMcp/VJDQLIYQ4JcUYR7DRBQQbXVBqucadhT5ne1GY3oY+ezvmbR+iDbii64TNSQQT2xFKbBsN0qHEdoTtjWVkWpSp5BQSEk5v27ASGd0uKArQhUVBu7DUshAF3gBOXySYH3O6iwJ5AP+phriJ/CEQCdC6aI3F4br4dorDgj4cxm48fr/NpCu61MsHJ+swCc1CCCHOiGJtQMDaoNSHDlEUtM7D6HJ3os/ZGbnM3Ylp91do/8iLrhY22CJBOrEdwcS2hBLbEHK0IZTQEnSV+1CbECfSajRFc58NZ7S9LxjG6QtGvwqLQnahL4jTW3z7+H1OX4gslxtX0e3yjrNdksWgxWYsEbSLrttOul4cxHVF6+uxGXXYjDoZ8Y6RagvNGzduZObMmcyfP5/9+/fz6KOPotFoaNeuHVOnTkWr1TJnzhyWLFmCXq9n0qRJdOvWrbrKEUIIURM0GsJxTQnHNSXQ4pLjyxUFjScbfe4OdLm70OVEwrTh0C+Yt398fDWNlnBcc4KONEKOtEiYTkgjlJhG2Caj06J6mfRaTHojyZU8rvaJgmEFvdnAocxCXL4QTn9kpNvpj4Tv4jDu8oVw+SOh2+kPcrTQG7nuC+INVhy89VpNJEAXBemSgTpyPRK2rQYdtqLQXWr9ouvyocrTUy2h+d///jcLFy7EYonMYXr22WeZMGECvXr1YsqUKSxatIgmTZqwZs0aPvroI44cOcL999/PJ598Uh3lCCGEiDWN5vjIdMmjeBA5JJ4ub0/R1250eXvR5e3GeHgVmqAnup6itxSF6TaEHK0JJbQmlNCKUEIrOWmLqBX0Wg0OqxHKODJJZQVDYZz+UDRcO/1BXP7jIdvlC+L0Ry5dxev5Q2QW+nD6Q7iLlgXLOR53SVoN2Ix6rEWBO/J1/La1OGgXhW+r8XgAtxqLQrlRh9Wox2Ko/wG8WkJzixYtmD17Nn/7298A2LJlCz179gRgwIABLF++nNatW9O/f380Gg1NmjQhFAqRk5NDUpJ8aEQIIc4miimeYMPuBBt2P+EOBa3rSDRE6/L2oMvdjSFzI6bdX6JRjo/IhQ12NElpxMW1jAbpcPGlNVVGqEWdoddpcVgip01Xwx8M4yoO3L4QrkDxCHckgJcc7XYFImHb7Y+Mimc4fbh8QdyByLYVx2/QQPQoK1aDrkTwPh7CLSWWWwzHg7mlRPi2GrSYbbVzila1hObBgwdz6NCh6G1FUaJ/fdhsNgoLC3E6nTgcjug6xcslNAshhAAiUz3sTQjbm5SeNw0Q8qMrPBQJ1Pn70Obvw+w+iP7YZkx7vkETDkZXVfRWQgktCcW3IBTfklBCC8LF1+Oagr7yh1EToq4w6rUY9UYSreoeR1EUvMFwUfiOhHC3PxK+3YFgUdgOlVgeLHX7SIEXlz+Exx/CHQjhq8T0E5tRx8e39aCBvXaF5xr5IKBWe/xA+C6Xi/j4eOx2Oy6Xq9TyuLi4k7a1203o9bqaKLPO0em0OBwq9wYhfawC0kP1pIenywrJDqDL8UU6LUooTDAchPyDaHL2oMndC7l70ObsRZe/Hw4uLT3lAw3ENUJxtILEViiOlkXXW6IktAB7w7PqZC7yPlRPenhqgVAYT4npJu7iUW9fJGw7/UGsRj0tGydg0NWufa9GQnOnTp1YvXo1vXr1YunSpfTu3ZsWLVrwwgsvcMcdd3D06FHC4XCZo8xOp68mSqyTyjpzkzh90kf1pIfqSQ/VK9VDTUNIbgjJfUqvpCho3MfQFRxAV7C/6PIA2vwD6HYvRuc6Wnp1rZFQXFPC8c0JxTUjHNeMUFwzQvHNCcc1I2yrX6Fa3ofqSQ8rxwJYjFowGsFe+r5Y9jAl5eQB3GI1EponTpzI5MmTmTVrFmlpaQwePBidTkePHj0YOXIk4XCYKVOm1EQpQgghzmYaDYotlaAtlWDjHiffH/SiK0yPTPkoPISu8FDksuAgpqzv0XqySq2uaA2E7U0IRUN1U0L2ppFpJXFNCNmbyPQPIeoJjaIolZnfHTPHjhXGuoRaS/6arRrSR/Wkh+pJD9WrkR4GPOic6WgLDkbmVBceRFuYjq7gYCRcuzNP2iRsaRAZrbY3KbpsSsjeOHJoPnsTwpYGoK0d0xDlfaie9FC9s3qkWQghhKgXDJaik7K0JVDW/SEfWufRSLAuPFx0mY7OmY4udxfGAz+jCZYOA4pWT9jakLC9cSRM25tErtsaEbY3jnxZU0Erv7KFiCXZA4UQQoiqojMRTmhJOKFl2fcrChpf3vFA7TyCznkEresIWucR9Mc2o9v7PZpQ6c/zKBotYWsqYVsjwvZGhG2NIqHa1oiwrWH0UjHGyeH1hKgmEpqFEEKImqLRoJgTCZkTCaV0Lnud4mBdHKiLQnXxdV3uHgzpK9H68k/eVG8lZGsYCdbWhkWBumFR4E4tWpaKYrBLuBbiNEloFkIIIWqTksG6Qafy1wu40boy0LmOonVloC1xqXNlYMhYj9Z19KRRa4icXTESpBsSigbqVDQpzTHgIGxNIWxNQTEn1Zr51kLEmoRmIYQQoi4yWAk7WhN2tC5/HUVB48tH685E68pE684ouiz6cmWgz/4D7YElaANOABwlN9doUczJ0RAd+WoQCdzWFMKW4tspKCaHBGxRr0loFkIIIeorjQbF7CBkdhBKan/qdQNuHLoCnBkHi0L1sRJfWWjdmRhyd6H1ZJU9eh0N2MmELQ0iX9bIpRK9fvw+DJZqetFCVA8JzUIIIYQAgxUcDQhqG516PUVB4y+IBmmt+xgaTzZaT1ZkmSfyZchYH1leNIJ90sPoLUUhOpmwOQml+LolqSh8NyBsTiJsSUaxJKMYbDIPW8SUhGYhhBBCVJ5Gg2JKIGRKIJTYpuL1gx607qJQ7cmOhGxvNlpPzvFlnmy0OTvQerPRBL1lPoyiNRK2JKKYk4rCdFLR9cTj10suMyeC3iJBW1QZCc1CCCGEqD56C+H4ZoTjm1Vu/YC7RJjOKRrFzkbry0XjyUHrzUXrzUGf9Qdabw4abx4ayj5Pm6IzETY7UIpCtGJOJGxKjN6OLjMnopgdkUtTghwTW5RJ3hVCCCGEqD0MVsKGFoTjW1Ru/XAo8mFHb05RqM5B68lG481F68uLXHrzImE7ZycGby4aXx6acLD8hzTGoZgckcB9ikvFnEDYlIBiSiBsckRGtkW9JaFZCCGEEHWXVodiSSJkSYLESm6jKGj8hUWBOvf4pS+/KGjnlbrUZx2O3tYoofIfVmsAi4NEQ3xRkI4E6ki4LgrcpngUUzyKsXidyGXkxDTaqumJqBYSmoUQQghxdtFoIsHVFF/+2RvLoihoAs4SoTo/ciIaX15R4M7HpLgIFmSj9eVHppXk7YmMbPsKyp1GAqCgQTHGRQN1NEyb4iMj38b4EvfFoRgTUExxhKPL40BnrILmiPJIaBZCCCGEqAxNUbA1xhGmeZmrGBxWCvPcJ9+hhNH4nWh8+Wh8BWj9kctI2I5cavwFaH3FywvQ5e+LjIj7C9H6CyssT9GbIyHaGIditBcF7bii0F38FY9itBdNQYkvsdxO2BAXOYqKfHiyTBKahRBCCCGqm0YbHd0GKH+SRznCocgot68gEqJ9+ZFA7SuIhO0S1zV+J1p/ARpfYeQENv6ibQKuCp9G0WhRDPai0F0cviOBOrrMYIsuj6wbh2K0oRjshKPLbKAz16sALqFZCCGEEKK20+qKpmskAGcQuqFE8C5EEyiMhGp/YWSZvzAyEh4d2S6xzJuPvjC9KKwXogmWMZJeBkWrjwTs4hBeHLYN1miwVgxFIdtgLQrcNjSN24Ct45m8wmoloVkIIYQQ4mxwQvA+Y+EQmqC7KFS7ikK3s+jSFVkecEWCd6DEOgF3ZBTclYEm4CraxoUmHDi51LGrCcc1VVdnFZPQLIQQQgghKk+ri86FrhIhXzRQawJO4hIdhLWNq+axq5CEZiGEEEIIETs6E4rOhGIuOmagwwplfZgyxuSAgEIIIYQQQlRAQrMQQgghhBAVkNAshBBCCCFEBSQ0CyGEEEIIUQEJzUIIIYQQQlRAQrMQQgghhBAVkNAshBBCCCFEBSQ0CyGEEEIIUQEJzUIIIYQQQlRAQrMQQgghhBAVkNAshBBCCCFEBSQ0CyGEEEIIUQGNoihKrIsQQgghhBCiNpORZiGEEEIIISogoVkIIYQQQogKSGgWQgghhBCiAhKa65DXXnuNkSNHMnz4cD766CP279/PTTfdxM0338zUqVMJh8OxLrFWCwQCPPzww4waNYqbb76Z3bt3Sw9Pw8aNGxkzZgxAuX2bM2cON9xwA6NGjWLTpk2xLLfWKtnHrVu3cvPNNzNmzBjuuOMOsrKyAPjwww8ZPnw4N954I4sXL45lubVSyR4W++KLLxg5cmT0tvTw1Er2MDs7m3vuuYfRo0czatQoDhw4AEgPK3LivnzjjTdy00038dhjj0V/JkoPyxYIBPi///s/br75Zm644QYWLVpUN36vKKJOWLVqlXL33XcroVBIcTqdyj//+U/l7rvvVlatWqUoiqJMnjxZ+f7772NcZe32ww8/KA888ICiKIqybNkyZfz48dLDSpo3b55y9dVXKyNGjFAURSmzb5s3b1bGjBmjhMNhJT09XRk+fHgsS66VTuzj6NGjlT/++ENRFEV57733lGeeeUbJzMxUrr76asXn8ykFBQXR6yLixB4qiqJs2bJFGTt2bHSZ9PDUTuzhxIkTla+++kpRFEVZuXKlsnjxYulhBU7s4b333qssWbJEURRFeeihh5RFixZJD0/h448/VqZPn64oiqLk5uYqF198cZ34vSIjzXXEsmXLaN++Pffddx9/+ctfuOSSS9iyZQs9e/YEYMCAAaxYsSLGVdZurVu3JhQKEQ6HcTqd6PV66WEltWjRgtmzZ0dvl9W33377jf79+6PRaGjSpAmhUIicnJxYlVwrndjHWbNm0bFjRwBCoRAmk4lNmzZx3nnnYTQaiYuLo0WLFmzbti1WJdc6J/YwNzeXWbNmMWnSpOgy6eGpndjDdevWkZGRwbhx4/jiiy/o2bOn9LACJ/awY8eO5OXloSgKLpcLvV4vPTyFIUOG8Ne//hUARVHQ6XR14veKhOY6Ijc3l82bN/Pyyy8zbdo0HnnkERRFQaPRAGCz2SgsLIxxlbWb1WolPT2dK664gsmTJzNmzBjpYSUNHjwYvV4fvV1W35xOJ3a7PbqO9PNkJ/YxNTUViISWd955h3HjxuF0OomLi4uuY7PZcDqdNV5rbVWyh6FQiMcff5zHHnsMm80WXUd6eGonvg/T09OJj4/nrbfeonHjxvz73/+WHlbgxB62atWKGTNmcMUVV5CdnU2vXr2kh6dgs9mw2+04nU4eeOABJkyYUCd+r0horiMcDgf9+/fHaDSSlpaGyWQq9cZxuVzEx8fHsMLa76233qJ///589913fP755zz66KMEAoHo/dLDytNqj//oKO6b3W7H5XKVWl7yF4Yo29dff83UqVOZN28eSUlJ0sfTsGXLFvbv38+TTz7JQw89xK5du5gxY4b08DQ5HA4uvfRSAC699FI2b94sPTxNM2bM4N133+Xbb79l2LBhPPfcc9LDChw5coSxY8dy7bXXMnTo0Drxe0VC8/+3d/8xVdV/HMefVy8QdeVuMhVTt7oXLV27Y0AiKA7tTgFXLqc47roB6ZwOcDKli15ht7w4QZQphoYOh5fKK84WM2u23DKdIeCaVv5Yl6UCJukcP7Qu93Jvf7juQpErfu0L0fvx7/mc83mfNz/ua5997jn/ElFRUXz77bd4vV5u3rzJ77//TmxsLHV1dQCcPHmS6OjoQa5yaAsJCfH9sanVatxuN9OmTZMePoG++hYZGcmpU6fweDy0trbi8XgYPXr0IFc6tH322WdUV1djs9mYNGkSADqdjsbGRpxOJ52dnTgcDqZMmTLIlQ5NOp2Ozz//HJvNxvbt2wkPD8dsNksPBygqKopvvvkGgPr6esLDw6WHA6RWq30romPHjqWjo0N62I9bt27xzjvvkJuby+LFi4F/x+eK0v8QMRTMmTOH+vp6Fi9ejNfrpaCggIkTJ5Kfn8/27dvRaDTMnz9/sMsc0tLT09mwYQMGgwGXy0VOTg6vvPKK9PAJmEymh/o2cuRIoqOjWbp0KR6Ph4KCgsEuc0jr6emhsLCQ8ePHk52dDcCrr77K6tWrMRqNGAwGvF4vOTk5BAUFDXK1/y5jxoyRHg6AyWRi48aNHDx4EJVKxbZt21Cr1dLDAbBareTk5KBUKgkICGDTpk3ye9iPPXv20NHRQXl5OeXl5QCYzWasVuuQ/lyR12gLIYQQQgjhh2zPEEIIIYQQwg8JzUIIIYQQQvghoVkIIYQQQgg/JDQLIYQQQgjhh4RmIYQQQggh/JDQLIQYVrZs2YLRaCQxMZGEhASMRiOrV6/uc6zRaMThcDyVeefOnYvT6XyssU9z3v9FXV0dsbGxFBcXD/jc6upq4P7zVO12+9MurU8VFRWcP38ep9NJTU3NI8eZzWaio6OHRI+FEMOHPKdZCDGs5OXlAXDkyBGamppYt27dIFc0tM2YMYN33313wOft3r2bt956i9mzZ/8DVfVtxYoVADQ3N1NTU8OSJUv6HFdYWMi1a9f+b3UJIf4bJDQLIYY9l8vF+vXraW5upqenh4yMDJKTk33HT5w4wf79+/nggw+4ceMGVqsVuP964c2bN/PTTz+xd+9eAgICaG5uJjk5mVWrVj00T0FBAS0tLYSGhlJUVITb7cZsNtPZ2UlbWxsGgwGDweAb/+uvv2KxWHA6nfz222+sWbMGvV7P66+/zvTp07l8+TIKhYLy8nJUKhWbNm3i/PnzuFwusrOz0ev1bNu2jYaGBjweD+np6SQlJfmuf+PGDdLS0qiursbhcFBWVsaBAwdQKh/+119WVkZzczO3b9+mtbWV9evXEx8fz5dffslHH32E2+1GoVCwa9cu7HY77e3tWCwWdDodTU1NvregZWVl0d3dzRtvvEFtbS12u52jR4+iUChITk7m7bff7jXv3Llz+eKLLwgKCqKkpASNRsOECRP67HdeXh7JyckcP36cn3/+mV27dhEbG0tRURFKpZLg4GB27NjhezObEEI8TRKahRDDnt1uZ/To0ZSUlNDV1cWiRYuYMWMGAF999RX19fV8+OGHPPvssyxfvpzNmzcTHh5OTU0N+/btIy4ujtbWVmpra+nu7iY+Pr7P0JyamkpERATFxcUcOnSIqKgoFixYwLx587h586bv7WB/aWpqIiMjg5iYGM6dO0dZWRl6vZ67d++yYMEC8vPzWbt2LSdPniQwMJA7d+5w+PBh2tvb2b9/vy9UfvLJJzidTlJSUpg5cyYhISEAjB8/ntzcXPLy8rh16xYVFRV9Bua/BAYGsm/fPk6fPk1lZSXx8fH88ssvVFRUEBwcTEFBAadOnWLVqlVUV1djsVg4cuQIAAsXLsRgMJCZmcnXX3/NnDlzuHbtGseOHePjjz8GICMjg1mzZqHRaPz+zPrr98qVK7ly5QpZWVkUFRWRlJREWloaJ06coKOjQ0KzEOIfIaFZCDHsORwO4uLiAFCpVGi1Wq5fvw7AmTNn6Orq8oVJh8PBe++9B9xfoX7hhRcAmDJlCkqlEqVSyTPPPPPQHAEBAURERAAQGRnJ6dOnmT9/PlVVVRw/fhyVSoXb7e51zpgxY9i9ezeHDx9GoVD0Oj5t2jTgfvB1Op20tLT4rq9Wq1mzZg179+7lxx9/xGg0AuB2u2lpafGFZgC9Xk9paSlxcXGEhYX126epU6cCEBYWRnd3NwChoaGYTCaee+45mpqafDU8SK1WM3XqVBobG/n0008xmUxcvnyZ1tZW0tPTAWhvb+fq1auPDM1/f0Gtv37/ZeXKlezZs4e0tDTGjRuHTqfr9x6FEOJJyRcBhRDDnlarpaGhAYCuri6uXLnCxIkTgftbKmbNmsXOnTsBePHFFykqKsJms5Gbm0tCQgIACoWi3zlcLhcXL14EoKGhgcmTJ1NZWUlERAQlJSUkJib2CoUAO3bsYOHChWzdupWYmJhexx+cT6PRcOHCBQA6OztZtmwZGo2GmJgYbDYbVVVVJCUlMWnSpF7nVVZWMnPmTH744Qe+//77fu/hwTk7OzvZuXMnpaWlWK1WgoKCfDU+eC8AKSkpVFVV8ccff6DVatFoNISHh3PgwAFsNhuLFi3ipZde6nVOYGAgbW1teL1eLl269Mha/m7EiBF4PB4AamtrefPNN7HZbEyePJlDhw71e49CCPGkZKVZCDHspaSkkJ+fT2pqKk6nk6ysLEJDQ33HMzMzWbJkCQkJCVgsFkwmk28Pb2FhIW1tbX7nCAgIwGazcfXqVZ5//nnWrl1LY2MjVquVY8eOMWrUKEaOHOlbwQVITEykuLiYiooKwsLCuHPnziOv/9prr3HmzBlSU1Pp6ekhMzOT2bNnc/bsWQwGA/fu3UOv1/famnDhwgWOHj2K3W7n+vXrZGdnY7fbGTVq1GP1TaVSERkZydKlS1EqlYSEhPh6odVqWbdunW8FH2D69Onk5+f7tlK8/PLLxMbGkpqaSnd3NzqdjnHjxvWaY/ny5axYsYIJEyb0WiHvT2hoKC6Xi61btzJv3jw2btxIcHAwI0aM4P3333+sawghxEApvH0tFwghhBj26urqOHjwIKWlpYNdylNnNBqxWCxotdrBLkUIMUzI9gwhhPgP++67757oOc1Dmdls9m2VEUKIp0VWmoUQQgghhPBDVpqFEEIIIYTwQ0KzEEIIIYQQfkhoFkIIIYQQwg8JzUIIIYQQQvghoVkIIYQQQgg/JDQLIYQQQgjhx5+KDFx7Yzda8wAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "for ci in cc:\n", + " plt.plot(xvals, [ci.yfromx_f(x, ignorebounds=True) for x in xvals], label=f\"eta={ci.eta:0.2f}\")\n", + "plt.grid()\n", + "plt.legend()\n", + "plt.title(\"Indifference curve token balance y vs token balance x at different weights\")\n", + "plt.xlabel(\"Token balance x [native units]\")\n", + "plt.ylabel(\"Token balance y [native units]\")\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 145, + "id": "dcf6c394-8ad0-4ea1-a0fe-849374b16110", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAscAAAF8CAYAAAAjExYFAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8pXeV/AAAACXBIWXMAAAsTAAALEwEAmpwYAABwJklEQVR4nO3dd3gU5doG8HvK9k0loYUamhQRAVGqIIiAIIggEIwofGKBo4hyLIhg49gOFhQVPVYs2FDsBRUUARUEpAgSek9C2m62zO683x+7WRJISGA32STcv+vaa/rMk4cF7p3MzkhCCAEiIiIiIoIc7QKIiIiIiKoLhmMiIiIioiCGYyIiIiKiIIZjIiIiIqIghmMiIiIioiCGYyIiIiKiIIZjorPM/v370bZtWwwfPjz0uuKKK/Dhhx+Wuv6yZcvw8MMPV3GVpZs4cSKOHTt2ynXWrFmDoUOHVsrx58+fjwcffPC0tqnMeqpKRfoOAA8++CDmz58PALjhhhuwY8cOAMDs2bNxySWX4KmnnsILL7yAvn374p577qnUmgFg48aNuP/++yO+33fffRcLFy485Tqn+nOvrLqIKDLUaBdARFXPbDbj008/DU0fOXIEQ4cORYcOHXDOOeeUWLd///7o379/VZdYqpUrV0a7hLPSmfT95ZdfDo0vXrwYP/30E+rXr4/+/fvjySefRNeuXSNZYql27NiBI0eORHy/48aNC2v7yqqLiCKDZ46JCPXq1UPTpk2xe/dufPzxx0hLS8OVV16J9PR0fPzxx7jxxhsBAJmZmbjlllswaNAgDBkyBG+++SYAoKCgAHfffTdGjhyJYcOGYe7cufD5fCWOUVBQgM6dOyMzMzM07+qrr8by5cvxxx9/YNSoURg5ciRGjhyJb7755qQai840TpgwAYcOHcI///yD9PR0DBs2DFdccQU++eSTk7b5448/0K9fP6xbtw4A8MMPP2D06NEYMWIExo4diz///BNA4Izw3XffjUmTJmHQoEFIS0srM7xkZGRg/PjxGDp0KGbMmAGHwwEA+PHHHzF27FiMHDkSffv2xdNPP33Strt27cL111+PMWPGoF+/frj55pvh8XgAAOeeey7mz5+PsWPH4pJLLsHrr78e2u6ll17CoEGDMHToUEyZMgUFBQUAgA8++AAjR47EiBEjcN111yEjI+OkYy5ZsgT9+/eH0+lEYWEhBg8eXGqvyqr/xL4X53A4cNttt+Gyyy5Deno6du7cGVp2ySWX4K+//kJaWhqEELjhhhswYcIEHDlyBDNnzsSXX355yvdNhw4dQvv+66+/kJGRgYkTJ2LkyJEYPnx46Dcda9aswdixYzFjxgyMGDECQ4YMwerVq3Ho0CE8++yz+OOPP046S/3GG2/gzjvvBABomobOnTuH9rd27VqMGjUKwKnfL0W/Qdi4cWOo/ilTpuDKK6/EmjVrAACFhYW4/fbbMXz4cAwaNAh//PHHSXU5nU7ceuutGD58OK688krcd9990HX9pD8fIqpCgojOKvv27ROdOnUqMW/dunXiggsuEAcPHhQfffSRuOCCC0RBQYEQQoiPPvpITJ48WQghxJQpU8Rjjz0mhBAiPz9fXH755WL37t3i7rvvFm+++aYQQgifzyfuvPNOsXDhwpOO/e9//1u88sorQgghduzYIfr27Sv8fr+49tprxeeffy6EEGLr1q1izpw5pdbeunVrkZ2dLTRNE/379xfffPONEEKIw4cPi969e4t169aJ1atXi8svv1ysWrVKDBgwQGzdulUIIcSuXbvE0KFDxbFjx4QQQmzfvl307NlTOJ1O8eyzz4r+/fuHfuYbb7xRPPPMMycd/9lnnxV9+/YV2dnZQtd1cccdd4jHH39c6LourrnmGrFr165QPW3bthXZ2dmheoQQ4tFHHxWffPKJEEIIr9crhg4dKr7++uvQz/bWW28JIYT466+/RIcOHYTb7Rbff/+9GDhwoMjNzRVCCDF37lyxYMECsWbNGpGWliYKCwuFEEL8/PPPYvDgwaX2bfr06WL27NninnvuEffdd99Jy09Vf/G+n+iRRx4R//73v4Wu6yI7O1v06dNHPPvss0IIIfr16yc2btx40vbF55/qfdO6dWuxZMkSIYQQmqaJIUOGiE2bNgkhAu+9wYMHiz///FOsXr1atG3bVmzZskUIIcT//vc/MX78eCFEyfducfv37xfdu3cXuq6L1atXi549e4rp06cLIYR47LHHxMKFC8t9vzzwwANC0zTRp08f8dNPPwkhhFi1apVo06aNWL16daiu9evXCyGEeO2118S11157Ul1LliwREydODPVg5syZYvfu3aX+ORJR1eBlFURnIbfbjeHDhwMA/H4/EhIS8MQTT6BBgwYAgDZt2sBut5+03a+//ooZM2YAAGJiYvD5558DAH766Sf89ddfobNvbre71OOOHj0aDzzwACZNmoSPPvoII0eOhCzLGDx4MB588EH88MMP6NGjB6ZPn37K+nfv3g2Px4OBAwcCCJz5HjhwIH7++WdceOGFOHz4MG666SaMGzcudJnIypUrcfToUVx33XWh/UiShL179wIAunXrFvqZ27Vrh7y8vFKPfemllyIxMREAcNVVV+Hxxx+HJEl48cUX8dNPP+Hzzz9HRkYGhBBwuVwltp0xYwZWrlyJl19+Gbt378bRo0dRWFgYWl50+Ur79u3h9XpRWFiIVatWYdCgQYiLiwNw/Ezu448/jj179mDs2LGh7fPy8pCbm4v4+PgSx33ggQcwfPhwmM1mfPzxxyf9TBWt/0SrVq3CvffeC0mSkJiYiEsvvfSU65+ovPdN0aUXu3fvxt69e3HvvfeGlrndbmzZsgUtWrRAw4YN0bZtWwCBP7slS5ac8rgpKSmoX78+/vrrL/z888+YPHkyFi5cCCEEli1bhpdffhk///zzKd8vALB9+3YAwMUXXwwAuOiii9CqVavQ8saNG+O8884DAJxzzjn46KOPTqqlS5cueOqpp5Ceno4ePXpgwoQJaNq06akbR0SViuGY6Cx04jXHJ7JaraXOV1UVkiSFpvft24eEhATouo5nnnkGLVq0AADk5+eXWK9I165d4fP5sHHjRnz++ed47733AABjx45Fv379sHLlSvz888947rnnsHTpUsTExJRaR2m/dhZChH4lrygKFi5ciFtuuQWDBw9Gx44does6unfvXuJyh0OHDqFu3br47rvvYDabQ/MlSYIQotRjK4pS4piqqqKwsBBXXnklBgwYgK5du+Kqq67C999/f9I+pk+fDr/fj8GDB6Nv3744dOhQiXVMJlPo+EX7VxSlRC/z8/ORn58PXdcxfPjw0IcVXddx9OjRUIguLjs7Gx6PB16vF0ePHkXjxo1LLK9o/aUpvk7x3lREee+boveh3+9HbGxsifdsVlYWYmJisH79+gr/2RV36aWXYsWKFVi5ciVeeuklfP755/jyyy9hNpvRpEmTct8vRT/viccq3gODwVBuXY0bN8Z3332HNWvWYPXq1bj++utx3333YdCgQeX+DERUOXjNMRFVWPfu3UNnvwoKCjBhwgTs3r0bvXr1wuuvvw4hBLxeL26+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Hvr9fsycORP33HMPbDZbaB328NROfB8eOHAAsbGxeP3119GgQQO8/PLL7GEFnNjHZs2a4ZFHHsHgwYORnZ2NCy+8kH08BZvNBrvdDofDgVtvvRXTpk2r9v+vMBxXQ/Hx8ejVqxeMRiNSU1NhMplKvEGcTidiY2OjWGH19/rrr6NXr1745ptv8Omnn+Luu++Gpmmh5exhxcny8X8mivpmt9vhdDpLzC/+HwOd7Msvv8Ts2bOxcOFCJCYmsoenYfPmzdizZw/mzJmD6dOnY8eOHXjkkUfYw9MUHx+PSy65BABwySWXYNOmTezhGXjkkUfw9ttv4+uvv8aIESPw6KOPso/lOHToEK699loMHz4cw4YNq/b/rzAcV0NdunTBzz//DCEEjhw5ApfLhe7du2PNmjUAgBUrVqBr165RrrJ6i42NDf2liouLg8/nQ7t27djDM1Ba3zp37oxffvkFuq7j4MGD0HUdiYmJUa60+vr000+xaNEivPXWW2jcuDEAoGPHjli7di08Hg8KCgqQkZGB1q1bR7nS6qljx4744osv8NZbb2HevHlo2bIlZs6cyR6epi5dumD58uUAgN9//x0tW7ZkD89AXFxc6Axn3bp1kZ+fzz6eQlZWFiZOnIgZM2Zg1KhRAKr//ytq+atQVevXrx9+//13jBo1CkII3H///WjUqBFmzZqFefPmITU1FZdddlm0y6zWrrvuOtx7771IS0uDpmm4/fbb0aFDB/bwDNx1110n9U1RFHTt2hVjxoyBruu4//77o11mteX3+/HII4+gQYMG+Ne//gUAuOCCC3DrrbciPT0daWlpEELg9ttvh8lkinK1NUtycjJ7eBruuusu3HfffXjvvfdgt9vx3//+F3FxcezhaXr44Ydx++23Q1VVGAwGPPTQQ3wvnsKLL76I/Px8LFiwAAsWLAAAzJw5Ew8//HC1/X+Fj48mIiIiIgriZRVEREREREEMx0REREREQQzHRERERERBDMdEREREREEMx0REREREQQzHRFRjPfroo0hPT8egQYPQt29fpKen49Zbby113fT0dGRkZETkuJdccgk8Hk+F1o3kccOxZs0adO/eHY8//vhpb7to0SIAgfuRLl68ONKllWrhwoXYuHEjPB4PPvjggzLXmzlzJrp27VotekxEtQPvc0xENdbdd98NAPj444+xc+dO3HnnnVGuqHq76KKL8O9///u0t3vhhRdwzTXXoE+fPpVQVekmT54MANi/fz8++OADjB49utT1HnnkEezdu7fK6iKi2o/hmIhqFU3TcM8992D//v3w+/24/vrrMWTIkNDyH374Aa+99hqef/55HDp0CA8//DCAwKN1586diy1btuDll1+GwWDA/v37MWTIENx8880nHef+++/HgQMHUKdOHTz22GPw+XyYOXMmCgoKcPToUaSlpSEtLS20/uHDhzFnzhx4PB5kZmZi2rRpGDBgAIYNG4Zu3bph27ZtkCQJCxYsgN1ux0MPPYSNGzdC0zT861//woABA/Df//4Xf/zxB3Rdx3XXXYfBgweH9n/o0CFMmDABixYtQkZGBubPn48333wTqnryP/Pz58/H/v37kZ2djYMHD+Kee+5B79698fXXX+Ptt9+Gz+eDJEl47rnnsHjxYuTl5WHOnDno2LEjdu7cGXoq2NSpU+H1enHFFVdg6dKlWLx4MT7//HNIkoQhQ4bg2muvLXHcSy65BF999RVMJhOefPJJpKamIiUlpdR+33333RgyZAi+/fZb7NixA8899xy6d++Oxx57DKqqwmKx4Jlnngk9qYyIKFIYjomoVlm8eDESExPx5JNPwuFwYOTIkbjooosAAN999x1+//13vPTSS7Barfi///s/zJ07Fy1btsQHH3yAV155BT169MDBgwexdOlSeL1e9O7du9RwPG7cOHTq1AmPP/443n//fXTp0gWXX345Bg4ciCNHjoSellVk586duP7663HhhRdi3bp1mD9/PgYMGACn04nLL78cs2bNwh133IEVK1bAaDQiJycHH374IfLy8vDaa6+FwuO7774Lj8eDq6++Gj179kRsbCwAoEGDBpgxYwbuvvtuZGVlYeHChaUG4yJGoxGvvPIKVq5ciVdffRW9e/fG7t27sXDhQlgsFtx///345ZdfcPPNN2PRokWYM2cOPv74YwDA8OHDkZaWhilTpmDZsmXo168f9u7diy+//BLvvPMOAOD6669Hr169kJqaWu6f2an6fdNNN2H79u2YOnUqHnvsMQwePBgTJkzADz/8gPz8fIZjIoo4hmMiqlUyMjLQo0cPAIDdbkeLFi2wb98+AMCqVavgcDhCoTEjIwMPPPAAgMAZ52bNmgEAWrduDVVVoaoqzGbzSccwGAzo1KkTAKBz585YuXIlLrvsMrzxxhv49ttvYbfb4fP5SmyTnJyMF154AR9++CEkSSqxvF27dgACAdfj8eDAgQOh/cfFxWHatGl4+eWXsXnzZqSnpwMAfD4fDhw4EArHADBgwAA89dRT6NGjB+rXr3/KPrVt2xYAUL9+fXi9XgBAnTp1cNddd8Fms2Hnzp2hGk4UFxeHtm3bYu3atViyZAnuuusubNu2DQcPHsR1110HAMjLy8OePXvKDMfFH85aXr+L3HTTTXjxxRcxYcIE1KtXDx07djzlz0hEdCb4hTwiqlVatGiBP/74AwDgcDiwfft2NGrUCEDgUohevXrh2WefBQA0b94cjz32GN566y3MmDEDffv2BQBIknTKY2iahq1btwIA/vjjD7Rq1QqvvvoqOnXqhCeffBKDBg0qEf4A4JlnnsHw4cPxxBNP4MILLyyx/MTjpaam4q+//gIAFBQUYNKkSUhNTcWFF16It956C2+88QYGDx6Mxo0bl9ju1VdfRc+ePbFp0yasX7/+lD/DiccsKCjAs88+i6eeegoPP/wwTCZTqMYTfxYAuPrqq/HGG2/A7XajRYsWSE1NRcuWLfHmm2/irbfewsiRI9GmTZsS2xiNRhw9ehRCCPz9999l1lKcLMvQdR0AsHTpUlx55ZV466230KpVK7z//vun/BmJiM4EzxwTUa1y9dVXY9asWRg3bhw8Hg+mTp2KOnXqhJZPmTIFo0ePRt++fTFnzhzcddddoWtsH3nkERw9erTcYxgMBrz11lvYs2cPGjZsiDvuuANr167Fww8/jC+//BIxMTFQFCV0RhYABg0ahMcffxwLFy5E/fr1kZOTU+b++/fvj1WrVmHcuHHw+/2YMmUK+vTpg99++w1paWkoLCzEgAEDSlxS8Ndff+Hzzz/H4sWLsW/fPvzrX//C4sWLERMTU6G+2e12dO7cGWPGjIGqqoiNjQ31okWLFrjzzjtDZ+QBoFu3bpg1a1boEohzzjkH3bt3x7hx4+D1etGxY0fUq1evxDH+7//+D5MnT0ZKSkqJM96nUqdOHWiahieeeAIDBw7EfffdB4vFAlmW8eCDD1ZoH0REp0MSpZ0SICKiWmXNmjV477338NRTT0W7lIhLT0/HnDlz0KJFi2iXQkS1AC+rICI6S6xevfqM7nNcnc2cOTN0iQsRUSTwzDERERERURDPHBMRERERBTEcExEREREFMRwTEREREQUxHBMRERERBTEcExEREREFMRwTEREREQX9P7oOZC91Td4oAAAAAElFTkSuQmCC\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "for ci in cc:\n", + " plt.plot(\n", + " xvals, \n", + " [\n", + " -(ci.yfromx_f(x+0.1, ignorebounds=True) - ci.yfromx_f(x-0.1, ignorebounds=True))/0.2\n", + " for x in xvals\n", + " \n", + " ], \n", + " label=f\"eta={ci.eta:0.2f}\")\n", + "plt.grid()\n", + "plt.legend()\n", + "plt.title(\"Price vs token balance x at different weights\")\n", + "plt.xlabel(\"Token balance x [native units]\")\n", + "plt.ylabel(\"Price [dy/dx]\")\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 146, + "id": "3c8cbd59-5039-43bc-a52d-f05201e9e83b", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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zTVce+21vPTSS2d8jt/97ndcfvnl3Hnnnb6Ae+2117J//34APvvsMx599FEAduzYwaBBg9iwYQOXX345N998M0uWLAFg165dXHTRRTQ1NfHmm2/yve99ryO+JCIiAUszzSIinWjNmjXMnz8fk8mEzWbj4YcfJjQ0FIA5c+ZwwQUX8NZbbwHeoLp8+XJef/113G43v/3tb9m/fz8ffPAB//d//wfALbfcwsSJE8nKyjrpubZv38769et54403aGpqYsaMGYD31MC3336bH//4x7z55pvcfvvtVFRUEBsbi8lk4qc//Sm///3vyczM5JFHHgFgwIABXHXVVTzwwAMUFha2G9ZFRM41Cs0iIp3o+OUZ/y0zM/OEj/Pz8xk8eDAWiwWLxcIDDzzABx98QFFRETfffDMAtbW1HD58+JSh+dChQ+Tm5mI2mwkLCyMnJweACy+8kMsvv5wFCxZQWlrKwIEDeeONN3zHBldUVPhqGT58OEeOHAG8M9R//OMfufPOOwkLCzsrXw8Rka5CyzNERAKEyWQ64eOsrCx27dqFx+PB6XRyyy23kJWVRXZ2Ni+99BIvv/wyl19+OX379j3l42VnZ7Nt2zY8Hg9NTU0cOHAAgJCQEMaMGcPjjz/OxRdfDMCqVauYMGECAImJieTl5QHe2epjfv3rX7NgwQLeeustCgoKzvrnLyISyDTTLCISoPr378+kSZOYN28eHo+HefPm0a9fP8aNG8e8efNwOBwMHjyYxMTE095/8uTJXHnllSQkJBAbG+u77uqrr+a6667j0UcfxeFw4HQ6fctEfvazn/HjH/+YsLAwQkNDiYyMZMmSJRw6dIiHH36YoUOH8sMf/pBXXnmlU74OIiKBwGQYhuHvIkRE5Ow5//zz+fDDD8/YPWPbtm288sor/PrXv/5Gz/WHP/yBuLg45s2b940eR0Qk0Gl5hojIOejWW289qU/zMa+88gqLFi3izjvv/EbP8corr/D2229/o8cQEekqNNMsIiIiItIOzTSLiIiIiLRDoVlEREREpB0KzSIiIiIi7VBoFhERERFph0KziIiIiEg7FJpFRERERNrx/wGMsZKgrEms/wAAAABJRU5ErkJggg==\n", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "for ci in cc:\n", + " plt.plot(\n", + " pvals, \n", + " [\n", + " ci.xyfromp_f(p, ignorebounds=True)[1]\n", + " for p in pvals\n", + " \n", + " ], \n", + " label=f\"eta={ci.eta:0.2f}\")\n", + "plt.grid()\n", + "plt.legend()\n", + "plt.title(\"Token balance y vs price at different weights\")\n", + "plt.xlabel(\"Price [dy/dx]\")\n", + "plt.ylabel(\"Token balance y [native units]\")\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6595e456-ca4b-4c4a-81e1-1ceba2924ec9", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d6b880eb-6fe9-41d3-920d-05552fde7469", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "jupytext": { + "encoding": "# -*- coding: utf-8 -*-", + "formats": "ipynb,py:light" + }, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/resources/NBTest/NBTest_049_CPCBalancer.py b/resources/NBTest/NBTest_049_CPCBalancer.py new file mode 100644 index 000000000..8208529f0 --- /dev/null +++ b/resources/NBTest/NBTest_049_CPCBalancer.py @@ -0,0 +1,524 @@ +# -*- coding: utf-8 -*- +# --- +# jupyter: +# jupytext: +# formats: ipynb,py:light +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.13.1 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# + +from fastlane_bot.tools.cpc import ConstantProductCurve as CPC +#from flbtools.cpc import ConstantProductCurve as CPC +print("{0.__name__} v{0.__VERSION__} ({0.__DATE__})".format(CPC)) + +from fastlane_bot.testing import * +#from flbtesting import * +from math import sqrt +# from fastlane_bot import __VERSION__ +# require("3.0", __VERSION__) +# - + +# # CPC for Balancer [NBTest049] + +# ## Constant product constructor + +c0 = CPC.from_xy(100, 200) +assert c0.x == 100 +assert c0.y == 200 +assert c0.k == 20000 +assert c0.x == c0.x_act +assert c0.y == c0.y_act +assert c0.alpha == 0.5 +assert c0.eta == 1 +assert c0.constr == "xy" +assert c0.is_constant_product() == True +c0 + +assert c0.asdict() == { + 'k': 20000, + 'x': 100, + 'x_act': 100, + 'y_act': 200.0, + 'alpha': 0.5, + 'pair': 'TKNB/TKNQ', + 'cid': 'None', + 'fee': None, + 'descr': None, + 'constr': 'xy', + 'params': {} +} + +c1 = CPC.from_xyal(100, 200) +assert c1.constr == "xyal" +assert c1.is_constant_product() == True +assert c1==c0 +c1 + +assert c1.asdict() == { + 'k': 20000, + 'x': 100, + 'x_act': 100, + 'y_act': 200.0, + 'alpha': 0.5, + 'pair': 'TKNB/TKNQ', + 'cid': 'None', + 'fee': None, + 'descr': None, + 'constr': 'xyal', + 'params': {} +} + +c2 = CPC.from_xyal(100, 200, alpha=0.5) +assert c2.constr == "xyal" +assert c2.is_constant_product() == True +assert c2==c0 +assert c2.asdict() == c1.asdict() +c2 + +c3 = CPC.from_xyal(100, 200, eta=1) +assert c3.constr == "xyal" +assert c3.is_constant_product() == True +assert c3==c0 +assert c3.asdict() == c1.asdict() +c3 + +assert raises(CPC.from_xyal, 100, 200, + alpha=0.5, eta=1) == 'at most one of alpha and eta must be given [0.5, 1]' + +# ## Weighted constructor + +c0 = CPC.from_xy(100, 200) +assert c0.x == 100 +assert c0.y == 200 +assert c0.k == 20000 +assert c0.x == c0.x_act +assert c0.y == c0.y_act +assert c0.alpha == 0.5 +assert c0.eta == 1 +assert c0.constr == "xy" +assert c0.is_constant_product() == True +c0 + +c1 = CPC.from_xyal(100, 200) +assert c1.constr == "xyal" +assert c1.is_constant_product() == True +assert c1 == c0 +assert c1.asdict()["alpha"] == 0.5 +c1 + +c2 = CPC.from_xyal(100, 200, alpha=0.25) +assert c2.constr == "xyal" +assert c2.is_constant_product() == False +assert c2.alpha == 0.25 +assert c2.asdict()["alpha"] == 0.25 +assert iseq(c2.eta, 0.25/0.75) +assert c2 != c0 +assert c2 != c1 +c2 + +c3 = CPC.from_xyal(100, 200, alpha=0.8) +assert c3.constr == "xyal" +assert c3.is_constant_product() == False +assert iseq(c3.alpha, 0.8) +assert c3.asdict()["alpha"] == 0.8 +assert iseq(c3.eta, 0.8/0.2) +assert c3 != c0 +assert c3 != c1 +assert c3 != c2 +c2 + +c3b = CPC.fromdict(c3.asdict()) +assert c3b == c3 +c3b + +assert raises(CPC.from_xyal,100, 200, alpha=0) == 'alpha must be > 0 [0]' +assert raises(CPC.from_xyal,100, 200, alpha=-1) == 'alpha must be > 0 [-1]' +assert raises(CPC.from_xyal,100, 200, alpha=1) == 'alpha must be < 1 [1]' +assert raises(CPC.from_xyal,100, 200, alpha=2) == 'alpha must be < 1 [2]' + +raises(CPC.from_xyal,100, 200, alpha=0) + +assert not raises(CPC.from_xyal,100, 200, alpha=1-1e-10) +assert not raises(CPC.from_xyal,100, 200, alpha=0.01) + +raises(CPC.from_xyal,100, 200, alpha=0.001) + +# ## High level testing of all functions +# +# (including not YET implemented) + +c0 = CPC.from_xyal(100, 200) +assert c0.is_constant_product() == True +c0 + +c1 = CPC.from_xyal(100, 200, alpha=0.25) +assert c1.is_constant_product() == False +c1 + +# #### Not (yet) implemented functions +# +# Those function groups are not currently planned to be implemented at all +# +# - `execute` as there is no need to simulate those curves for the time being; that was a Carbon thing +# +# The functions we may implement at a later stage are +# +# - `description` should probably be updated, but it is tedious; `format` ditto +# - `x_max`, `x_min`, `p_max`, `p_min` and the other leverage functions once we consider it important and safe + +# execute + +assert not raises(c0.execute) +assert raises(c1.execute).startswith("only implemented for") + +# description and format + +assert not raises(c0.description) +assert raises(c1.description).startswith("only implemented for") + +assert not raises(c0.format) +assert raises(c1.format).startswith("only implemented for") + +# leverage related functions (primary) + +assert not raises(lambda: c0.p_max) +assert raises(lambda: c1.p_max).startswith("only implemented for") + +assert not raises(lambda: c0.p_min) +assert raises(lambda: c1.p_min).startswith("only implemented for") + +assert not raises(lambda: c0.x_min) +assert raises(lambda: c1.x_min).startswith("only implemented for") + +assert not raises(lambda: c0.x_max) +assert raises(lambda: c1.x_max).startswith("only implemented for") + +assert not raises(lambda: c0.y_min) +assert raises(lambda: c1.y_min).startswith("only implemented for") + +assert not raises(lambda: c0.y_max) +assert raises(lambda: c1.y_max).startswith("only implemented for") + +# leverage related functions (secondary, ie calling primary ones) + +assert not raises(c0.p_max_primary) +assert raises(c1.p_max_primary).startswith("only implemented for") + +assert not raises(c0.p_min_primary) +assert raises(c1.p_min_primary).startswith("only implemented for") + +assert not raises(lambda: c0.at_xmin) +assert raises(lambda: c1.at_xmin).startswith("only implemented for") + +assert not raises(lambda: c0.at_xmax) +assert raises(lambda: c1.at_xmax).startswith("only implemented for") + +assert not raises(lambda: c0.at_ymin) +assert raises(lambda: c1.at_ymin).startswith("only implemented for") + +assert not raises(lambda: c0.at_ymax) +assert raises(lambda: c1.at_ymax).startswith("only implemented for") + +assert not raises(lambda: c0.at_boundary) +assert raises(lambda: c1.at_boundary).startswith("only implemented for") + +# todo + +assert not raises(c0.xyfromp_f) +assert raises(c1.xyfromp_f).startswith("only implemented for") + +assert not raises(c0.dxdyfromp_f) +assert raises(c1.dxdyfromp_f).startswith("only implemented for") + +# #### Implemented functions + +assert not raises(lambda: c0.y) +assert not raises(lambda: c1.y) + +assert not raises(lambda: c0.p) +assert not raises(lambda: c1.p) + +assert not raises(lambda: c0.kbar) +assert not raises(lambda: c1.kbar) + +assert not raises(c0.tvl) +assert not raises(c1.tvl) + +assert not raises(c0.yfromx_f, 110) +assert not raises(c1.yfromx_f, 110, ignorebounds=True) +assert raises(c1.yfromx_f, 110, ignorebounds=False) + +assert not raises(c0.xfromy_f, 210) +assert not raises(c1.xfromy_f, 110, ignorebounds=True) +assert raises(c1.xfromy_f, 110, ignorebounds=False) + +assert not raises(c0.dyfromdx_f, 1) +assert not raises(c1.dyfromdx_f, 1, ignorebounds=True) +assert raises(c1.dyfromdx_f, 1, ignorebounds=False) + +assert not raises(c0.dxfromdy_f, 1) +assert not raises(c1.dxfromdy_f, 1, ignorebounds=True) +assert raises(c1.dxfromdy_f, 1, ignorebounds=False) + +# ## Simple Tests + +c0 = CPC.from_xyal(100, 200) +c1 = CPC.from_xyal(100, 200, eta=2) +c2 = CPC.from_xyal(100, 200, eta=0.5) + +assert iseq(c0.alpha, 1/2) +assert iseq(c1.alpha, 2/3) +assert iseq(c2.alpha, 1/3) + +# #### Current token balance $y$ +# +# $$ +# y = \left( \frac k x \right)^\eta +# $$ + +assert iseq(c0.y, 200) +assert iseq(c1.y, 200) +assert iseq(c2.y, 200) + +# #### Current price $p$ +# +# $$ +# p = \eta\, \frac y x +# $$ + +assert iseq(c0.p, 2 * c0.eta) +assert iseq(c1.p, 2 * c1.eta) +assert iseq(c2.p, 2 * c2.eta) + +# #### TVL +# +# $$ +# \mathrm{TVL} = x_a*p + y_a +# $$ + +assert c0.x == c0.x_act +assert c0.y == c0.y_act +assert c1.x == c1.x_act +assert c1.y == c1.y_act +assert c2.x == c2.x_act +assert c2.y == c2.y_act + +assert iseq(c0.tvl(), 100 * c0.p + 200) +assert iseq(c1.tvl(), 100 * c1.p + 200) +assert iseq(c2.tvl(), 100 * c2.p + 200) + +# #### Pool constant $k$ +# +# $$ +# k^\alpha = x^\alpha\, y^{1-\alpha} +# $$ +# +# $$ +# k = x\,y^\frac{1-\alpha}{\alpha} = x\,y^{\frac 1 \eta} +# $$ +# + +assert iseq(c0.k**(1/2), c0.x**(1/2) * c0.y**(1/2)) +assert iseq(c1.k**(2/3), c1.x**(2/3) * c1.y**(1/3)) +assert iseq(c2.k**(1/3), c1.x**(1/3) * c1.y**(2/3)) + +# #### Pool constant $\bar k$ +# +# $$ +# x^\alpha\, y^{1-\alpha} = \bar k = k^\alpha +# $$ + +assert iseq(c0.kbar, c0.x**(1/2) * c0.y**(1/2)) +assert iseq(c1.kbar, c1.x**(2/3) * c1.y**(1/3)) +assert iseq(c2.kbar, c1.x**(1/3) * c1.y**(2/3)) + +assert iseq(c0.kbar, c0.k**c0.alpha) +assert iseq(c1.kbar, c1.k**c1.alpha) +assert iseq(c2.kbar, c2.k**c2.alpha) + +# #### Token balance function $y(x)$ +# +# $$ +# y(x) = \left( \frac k x \right)^\eta +# $$ + +assert c0.eta == 1 +assert iseq(c0.yfromx_f(100, ignorebounds=True), 200) +assert iseq(c0.yfromx_f( 50, ignorebounds=True), 400) +assert iseq(c0.yfromx_f(200, ignorebounds=True), 100) + +assert iseq(c1.eta, 2) +assert iseq(c1.yfromx_f(100, ignorebounds=True), 200) +assert iseq(c1.yfromx_f( 50, ignorebounds=True), 200*2**2) +assert iseq(c1.yfromx_f(200, ignorebounds=True), 200/2**2) + +assert iseq(c2.eta, 1/2) +assert iseq(c2.yfromx_f(100, ignorebounds=True), 200) +assert iseq(c2.yfromx_f( 50, ignorebounds=True), 200*sqrt(2)) +assert iseq(c2.yfromx_f(200, ignorebounds=True), 200/sqrt(2)) + +# #### Token balance function $x(y)$ +# +# $$ +# x(y) +# = \frac{k}{ y^{\frac{1-\alpha}{\alpha}} } +# = \frac{k}{ y^{\frac{1}{\eta}} } +# $$ + +assert c0.eta == 1 +assert iseq(c0.xfromy_f(200, ignorebounds=True), 100) +assert iseq(c0.xfromy_f(100, ignorebounds=True), 200) +assert iseq(c0.xfromy_f(400, ignorebounds=True), 50) + +assert iseq(c1.eta, 2) +assert iseq(c1.xfromy_f(200, ignorebounds=True), 100) +assert iseq(c1.xfromy_f(100, ignorebounds=True), 100*2**0.5) +assert iseq(c1.xfromy_f(400, ignorebounds=True), 100/2**0.5) + +assert iseq(c2.eta, 1/2) +assert iseq(c2.xfromy_f(200, ignorebounds=True), 100) +assert iseq(c2.xfromy_f(100, ignorebounds=True), 100*2**2) +assert iseq(c2.xfromy_f(400, ignorebounds=True), 100/2**2) + +# #### Price response function $(x(p), y(p))$ +# +# $$ +# x(p) +# = +# \left(\frac \eta p\right)^{1-\alpha} k^\alpha +# $$ +# +# $$ +# y(p) = \left( \frac{kp}{\eta} \right)^\alpha +# $$ + +assert iseq(c0.xyfromp_f(c0.p, ignorebounds=True)[0], c0.x) +assert iseq(c1.xyfromp_f(c1.p, ignorebounds=True)[0], c1.x) +assert iseq(c2.xyfromp_f(c2.p, ignorebounds=True)[0], c2.x) + +assert iseq(c0.xyfromp_f(c0.p, ignorebounds=True)[1], c0.y) +assert iseq(c1.xyfromp_f(c1.p, ignorebounds=True)[1], c1.y) +assert iseq(c2.xyfromp_f(c2.p, ignorebounds=True)[1], c2.y) + +for ci in [c0, c1, c2]: + for p in [2, 1, 4]: + eta_over_p = ci.eta / p + x = eta_over_p ** (1-ci.alpha) * ci.kbar + y = 1/eta_over_p**ci.alpha * ci.kbar + xx, yy, pp = ci.xyfromp_f (p, ignorebounds=True) + dx, dy, _ = ci.dxdyfromp_f(p, ignorebounds=True) + assert iseq(x, xx) + assert iseq(y, yy) + assert iseq(p, pp) + assert iseq(dx, xx-ci.x) + assert iseq(dy, yy-ci.y) + +# ## Consistency tests + +c0 = CPC.from_xyal(100, 200) +c1 = CPC.from_xyal(100, 200, eta=2) +c2 = CPC.from_xyal(100, 200, eta=0.5) +cc = [c0, c1, c2] + +assert iseq(c0.alpha, 1/2) +assert iseq(c1.alpha, 2/3) +assert iseq(c2.alpha, 1/3) + +# ### Assert inversions +# +# $$ +# y(x(y)) = y +# $$ +# +# and vice versa + +for xy in np.logspace(1, 3, 100): + for ci in cc: + #print(f"xy={xy}, eta={ci.eta}") + assert iseq(ci.yfromx_f(ci.xfromy_f(xy, ignorebounds=True), ignorebounds=True), xy) + assert iseq(ci.xfromy_f(ci.yfromx_f(xy, ignorebounds=True), ignorebounds=True), xy) + +# ### Assert that prices are correct +# +# $$ +# p \simeq -\frac{\Delta y}{\Delta x} +# $$ + +for alpha in np.linspace(0.01, 0.99, 100): + ci = CPC.from_xyal(100, 200, alpha=alpha) + dy = ci.yfromx_f(ci.x+0.1, ignorebounds=True)-ci.yfromx_f(ci.x-0.1, ignorebounds=True) + assert iseq(dy/0.2, -ci.p, eps=1e-2), f"error: {dy/0.2/ci.p+1}" + +# ### Check `dyfromdx_f` against `yfromx_f` + +for dxy in np.linspace(0.1, 99, 100): + for ci in cc: + assert iseq(ci.dyfromdx_f(dxy, ignorebounds=True), + (ci.yfromx_f(ci.x+dxy, ignorebounds=True)-ci.y)) + assert iseq(ci.dxfromdy_f(dxy, ignorebounds=True), + (ci.xfromy_f(ci.y+dxy, ignorebounds=True)-ci.x)) + +# ## Charts [NOTEST] + +plt.style.use('seaborn-v0_8-dark') +plt.rcParams['figure.figsize'] = [12,6] # only picked up at second run (?!?) + +c0 = CPC.from_xyal(100, 200) +c1 = CPC.from_xyal(100, 200, eta=2) +c2 = CPC.from_xyal(100, 200, eta=0.5) +cc = [c0, c1, c2] +xvals = np.linspace(50,200) +pvals = np.linspace(1,4) + +for ci in cc: + plt.plot(xvals, [ci.yfromx_f(x, ignorebounds=True) for x in xvals], label=f"eta={ci.eta:0.2f}") +plt.grid() +plt.legend() +plt.title("Indifference curve token balance y vs token balance x at different weights") +plt.xlabel("Token balance x [native units]") +plt.ylabel("Token balance y [native units]") +plt.show() + +for ci in cc: + plt.plot( + xvals, + [ + -(ci.yfromx_f(x+0.1, ignorebounds=True) - ci.yfromx_f(x-0.1, ignorebounds=True))/0.2 + for x in xvals + + ], + label=f"eta={ci.eta:0.2f}") +plt.grid() +plt.legend() +plt.title("Price vs token balance x at different weights") +plt.xlabel("Token balance x [native units]") +plt.ylabel("Price [dy/dx]") +plt.show() + +for ci in cc: + plt.plot( + pvals, + [ + ci.xyfromp_f(p, ignorebounds=True)[1] + for p in pvals + + ], + label=f"eta={ci.eta:0.2f}") +plt.grid() +plt.legend() +plt.title("Token balance y vs price at different weights") +plt.xlabel("Price [dy/dx]") +plt.ylabel("Token balance y [native units]") +plt.show() + + + + diff --git a/resources/NBTest/NBTest_049_CustomTradingFees.py b/resources/NBTest/NBTest_049_CustomTradingFees.py index 2e024bf8c..97dbb1e30 100644 --- a/resources/NBTest/NBTest_049_CustomTradingFees.py +++ b/resources/NBTest/NBTest_049_CustomTradingFees.py @@ -6,7 +6,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.14.7 +# jupytext_version: 1.13.1 # kernelspec: # display_name: Python 3 # language: python diff --git a/resources/NBTest/NBTest_900_OptimizerDetailedSlow.py b/resources/NBTest/NBTest_900_OptimizerDetailedSlow.py index 4333ca68d..ca53e7848 100644 --- a/resources/NBTest/NBTest_900_OptimizerDetailedSlow.py +++ b/resources/NBTest/NBTest_900_OptimizerDetailedSlow.py @@ -7,7 +7,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.14.7 +# jupytext_version: 1.13.1 # kernelspec: # display_name: Python 3 # language: python diff --git a/resources/NBTest/NBTest_903_FlashloanTokens.py b/resources/NBTest/NBTest_903_FlashloanTokens.py index fe98a6659..56fb951b7 100644 --- a/resources/NBTest/NBTest_903_FlashloanTokens.py +++ b/resources/NBTest/NBTest_903_FlashloanTokens.py @@ -5,7 +5,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.14.7 +# jupytext_version: 1.13.1 # kernelspec: # display_name: Python 3 (ipykernel) # language: python diff --git a/resources/NBTest/NBTest_904_Bancor3DataValidation.py b/resources/NBTest/NBTest_904_Bancor3DataValidation.py index b1c10e073..adb4fbdd0 100644 --- a/resources/NBTest/NBTest_904_Bancor3DataValidation.py +++ b/resources/NBTest/NBTest_904_Bancor3DataValidation.py @@ -5,7 +5,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.14.7 +# jupytext_version: 1.13.1 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -82,7 +82,7 @@ def run_command(arb_mode, expected_log_line): result = subprocess.run(cmd, text=True, capture_output=True, check=True, timeout=120) # Check if the expected log line is in the output - if expected_log_line in result.stderr: + if expected_log_line in result.stderr or expected_log_line in result.stdout: found = True if not found: diff --git a/resources/NBTest/NBTest_905_RespectMinProfitClickParam.py b/resources/NBTest/NBTest_905_RespectMinProfitClickParam.py index f733b94b7..76b509012 100644 --- a/resources/NBTest/NBTest_905_RespectMinProfitClickParam.py +++ b/resources/NBTest/NBTest_905_RespectMinProfitClickParam.py @@ -5,7 +5,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.14.7 +# jupytext_version: 1.13.1 # kernelspec: # display_name: Python 3 (ipykernel) # language: python @@ -94,7 +94,6 @@ def run_command(arb_mode, expected_log_line): # ## Test Minimum Profit BNT Is Respected -# + is_executing=true expected_log_line = "Bot successfully updated min profit" arb_mode = "multi" run_command(arb_mode=arb_mode, expected_log_line=expected_log_line) diff --git a/resources/NBTest/NBTest_906_TargetTokens.py b/resources/NBTest/NBTest_906_TargetTokens.py index 4a18adc45..7c25dcbc1 100644 --- a/resources/NBTest/NBTest_906_TargetTokens.py +++ b/resources/NBTest/NBTest_906_TargetTokens.py @@ -5,7 +5,7 @@ # extension: .py # format_name: light # format_version: '1.5' -# jupytext_version: 1.14.7 +# jupytext_version: 1.13.1 # kernelspec: # display_name: Python 3 (ipykernel) # language: python diff --git a/resources/NBTest/test_bancor_v3_mode.py b/resources/NBTest/test_bancor_v3_mode.py new file mode 100644 index 000000000..76d8a285e --- /dev/null +++ b/resources/NBTest/test_bancor_v3_mode.py @@ -0,0 +1,219 @@ +# --- +# jupyter: +# jupytext: +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.13.1 +# kernelspec: +# display_name: Python 3 (ipykernel) +# language: python +# name: python3 +# --- + +# + +# coding=utf-8 +""" +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, SushiswapV2, CarbonV1, BancorV3 +from fastlane_bot.events.interface import QueryInterface +from fastlane_bot.helpers.poolandtokens import PoolAndTokens +from fastlane_bot.helpers import TradeInstruction, TxReceiptHandler, TxRouteHandler, TxSubmitHandler, TxHelpers, TxHelper +from fastlane_bot.events.managers import manager +from fastlane_bot.events.interface import QueryInterface +import pytest +import math +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(SushiswapV2)) +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) +assert (C.NETWORK == C.NETWORK_MAINNET) +assert (C.PROVIDER == C.PROVIDER_ALCHEMY) +setup_bot = CarbonBot(ConfigObj=C) + +# + +import json + +pools = None +with open('latest_pool_data.json') as f: + pools = json.load(f) +pools = [pool for pool in pools] +# - + +pools[0] +static_pools = pools + +print(pools[0]) +print(static_pools[0]) + +state = pools +exchanges = list({ex['exchange_name'] for ex in state}) +db = QueryInterface(state=state, ConfigObj=C, exchanges=exchanges) +setup_bot.db = db + +print(state[0]) + +print(pools[0]) + +# + +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(",") +arb_mode = "bancor_v3" + +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() + ] + + +# + +# Filter out pools that are not in the supported exchanges +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() + +# + +from joblib import Parallel, delayed + +# Initialize data fetch manager +mgr = manager( + web3=cfg.w3, + 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"), +) + +# Add initial pools for each row in the static_pool_data +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}") + +# check if any duplicate cid's exist in the pool data +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) + +# add data cleanup steps from main.py +bot.db.handle_token_key_cleanup() +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.key: (t.address, int(t.decimals)) for t in tokens if not math.isnan(t.decimals)} +# ADDRDEC = {t.key: (t.address, int(t.decimals)) for t in tokens} + +# + + +print(len(ADDRDEC)) +# - + +flashloan_tokens = bot.setup_flashloan_tokens(["BNT-FF1C"]) +CCm = bot.setup_CCm(None) + +pools = db.get_pool_data_with_tokens() + +print(len(pools)) + +# + +#print(bot.db.mgr) + +# + +# single = bot.run_single_mode(flashloan_tokens, CCm, arb_mode, run_data_validator=True) + +single = bot._run(flashloan_tokens=flashloan_tokens, CCm=CCm, arb_mode=arb_mode, data_validator=True, result="calc_trade_instr") + + +# + +# bot.run( +# polling_interval=12, +# flashloan_tokens=["BNT"], +# mode="single", +# arb_mode="bancor_v3", +# run_data_validator=True +# ) +# - + + + + + + + + + + + + + + diff --git a/resources/docs/BalancerArbitrage.py b/resources/docs/BalancerArbitrage.py new file mode 100644 index 000000000..07e7ccdc2 --- /dev/null +++ b/resources/docs/BalancerArbitrage.py @@ -0,0 +1,365 @@ +# --- +# jupyter: +# jupytext: +# formats: ipynb,py:light +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.13.1 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +# # Balancer Arbitrage Code + +# ## Documentation + +# The $r_k$ are the asset weight factors in the pool +# $$ +# \forall r_{_{k}} \in \left \{ r_{_{1}}, r_{_{2}}, \cdots r_{_{n}} \right \}, \; r_{_{k}} > 0 +# $$ + +# They are normalized to sum up to unity +# $$ +# \sum_{k = 1} ^ {n} r_{_{k}} \equiv r_{_{1}} + r_{_{2}} \cdots + r_{_{n}} = 1 +# $$ + +# The $x_l$ are the token balances in the pool, in native units +# $$ +# \forall x_{_{k}} \in \left \{ x_{_{1}}, x_{_{2}}, \cdots x_{_{n}} \right \}, \; x_{_{k}} > 0 +# $$ + +# **Equation 1 (Pool Invariant)** + +# $$ +# \prod_{k = 1}^{n} +# x_{_{k}} ^ {r_{_{k}}} +# \equiv +# x_{_{1}} ^ {r_{_{1}}} +# x_{_{2}} ^ {r_{_{2}}} +# \cdots\ +# x_{_{n}} ^ {r_{_{n}}} +# = \kappa +# = {constant} +# $$ + +# **Equation 2 (Isolation)** +# $$ +# x_{_{i}} = +# \left( +# \kappa \prod_{\substack{ k = 1 \\ k \neq i}}^{n} x_{_{k}} ^ {- r_{_{k}} } +# \right) ^ { \frac{ 1 }{ r_{_{i}} }} +# $$ + +# **Equation 3 (Marginal Price)** +# +# Note: the $P_i$ are prices in any numeraire (they only ever appear as ratio so any numeraire factor will divide out) + +# $$ +# - \frac{ \partial x_{_{i}} } { \partial x_{_{j}} } +# = \frac {P_i} {P_j} +# = +# \frac{ +# x_{_{i}} +# } +# { +# x_{_{j}} +# } +# \left( +# \frac{ r_{_{i}} } { r_{_{j}} } +# \right) ^ { - 1 } +# = \frac{x_i\,r_j}{x_j\,r_i} +# $$ + +# **Equation 4 (Rebalancing)** + +# $$ +# x_i = +# \kappa P_{_{i}} r_{_{i}} \prod_{k = 1} ^ {n} \left( P_{_{k}} r_{_{k}} \right) ^ {- r_{_{k}}} +# $$ + +# $$ +# x_i = +# \frac{\kappa P_{_{i}} r_{_{i}}} +# {\prod_{k = 1} ^ {n} \left( P_{_{k}} r_{_{k}} \right) ^ {r_{_{k}}}} +# $$ + +# If we define $\pi_i = P_i r_i$ the "weighted price i" then the above formula becomes +# $$ +# x_i = +# \frac{ \kappa \pi_i } +# {\prod_{k = 1} ^ {n} \pi_k ^ {r_{_{k}}}} +# $$ + +# We can also substitute $\kappa$ using the invariant equation and token balances +# $$ +# x_i +# = P_i r_i \prod_{k = 1} ^ {n} \left( \frac {x_k}{P_k r_k} \right)^{r_k} +# = P_i r_i \prod_{k = 1} ^ {n} \left( \frac {x_k}{P_k r_k} \right)^{r_k} +# $$ + +# We can also substitute $\kappa$ using the invariant equation and token balances +# $$ +# x_i +# = +# P_i r_i \prod_{k = 1} ^ {n} \left( \frac {x_k}{P_k r_k} \right)^{r_k} +# = +# \pi_i \prod_{k = 1} ^ {n} \left( \frac {x_k}{\pi_k} \right)^{r_k} +# = +# \frac +# {P_i r_i \prod_{k = 1} ^ {n} x_k{}^{r_k}} +# {\prod_{k = 1} ^ {n} (P_k r_k)^{r_k}} +# = +# \frac +# {\pi_i \prod_{k = 1} ^ {n} x_k{}^{r_k}} +# {\prod_{k = 1} ^ {n} \pi_k{}^{r_k}} +# $$ + +# **Equation 5 (Delta x)** + +# $$ +# \forall \Delta{x_{_{k}}} \in \left \{ \Delta{x_{_{1}}}, \Delta{x_{_{2}}}, \cdots \Delta{x_{_{n}}} \right \}, \; \Delta{x_{_{k}}} > 0 +# $$ + +# $$ +# \Delta{x_{_{j}}} +# = +# x_{_{j}} +# \left( +# 1 - +# \left( +# \frac{ x_{_{i}} } { \left( x_{_{i}} + \Delta{x_{_{i}}} \right) } +# \right) ^ { \frac{ r_{i} } { r_{j} } } +# \right) +# $$ +# + +# $$ +# \Delta{x_{_{i}}} +# = +# x_{_{i}} +# \left( +# \left( +# \frac{ x_{_{j}} } { \left( x_{_{j}} - \Delta{x_{_{j}}} \right) } +# \right) ^ { \frac{ r_{j} } { r_{i} } } +# - 1 +# \right) +# $$ + +# ## Code + +from decimal import * +getcontext().prec = 100 +from math import prod +from typing import List, Dict, Tuple +from tabulate import tabulate + + +class BalancerArbitrage: + def __init__( + self, + x_: Dict[str, Decimal], + r_: Dict[str, Decimal], + P_: Dict[str, Decimal], + ): + self.ZERO = Decimal('0') + self.ONE = Decimal('1') + self.x_ = x_ + self.r_ = r_ + self.P_ = P_ + self.k, self.n = self.initialize_k_n() + self.kappa = self.calculate_kappa() + + def isclose_decimal( + self, + num_1: Decimal, + num_2: Decimal, + rel_tol: Decimal = Decimal('1') / Decimal('2') ** Decimal('256') + ) -> bool: + return abs(num_1 - num_2) <= max(abs(num_1), abs(num_2)) * rel_tol + + def initialize_k_n( + self + ) -> Tuple[List[str], int]: + assert all(val > self.ZERO for val in self.x_.values()), "Not all values in x_ are > 0" + assert all(val > self.ZERO for val in self.r_.values()), "Not all values in r_ are > 0" + assert all(val > self.ZERO for val in self.P_.values()), "Not all values in P_ are > 0" + if self.x_.keys() == self.r_.keys() and self.r_.keys() == self.P_.keys(): + return list(self.x_.keys()), int(len(self.x_.keys())) + else: + raise ValueError("Keys of input dictionaries do not match.") + + def calculate_kappa( + self + ) -> Decimal: + return prod(self.x_[k] ** self.r_[k] for k in self.k) + + def calculate_marginal_price( + self, + i: str, + j: str + ) -> Decimal: + return (self.x_[i] / self.x_[j]) / (self.r_[i] / self.r_[j]) + + def adjust_reserves_after_trade( + self, + i: str, # source + j: str, # target + Dx_i: Decimal, # source amount + Dx_j: Decimal # target amount + ) -> None: + self.x_[i] += Dx_i + self.x_[j] -= Dx_j + return None + + def trade_by_source( + self, + i: str, # source + j: str, # target + Dx_i: Decimal, # source amount + commit: bool = True + ) -> Decimal: + Dx_j = self.x_[j] * (self.ONE - (self.x_[i] / (self.x_[i] + Dx_i)) ** (self.r_[i] / self.r_[j])) + assert self.x_[j] >= Dx_j, f"Insufficient {j} reserves to support this trade. Something is wrong." + if commit: + self.adjust_reserves_after_trade(i, j, Dx_i, Dx_j) + return Dx_j + + def trade_by_target( + self, + i: str, # source + j: str, # target + Dx_j: Decimal, # target amount + commit: bool = True + ) -> Decimal: + Dx_i = self.x_[i] * ((self.x_[j] / (self.x_[j] - Dx_j)) ** (self.r_[j] / self.r_[i]) - self.ONE) + assert self.x_[j] >= Dx_j, f"Insufficient {j} reserves to support this trade. Something is wrong." + if commit: + self.adjust_reserves_after_trade(i, j, Dx_i, Dx_j) + return Dx_i + + def calculate_balanced_coordinate( + self, + i: str + ) -> Decimal: + return self.kappa * self.P_[i] * self.r_[i] * prod((self.P_[k] * self.r_[k]) ** (- self.r_[k]) for k in self.k) + + def determine_balanced_pool_state( + self + ) -> Dict[str, Decimal]: + return {i: self.calculate_balanced_coordinate(i) for i in self.k} + + def get_rebalance_trade_sets( + self, + balanced_coordinates_: Dict[str, Decimal] + ) -> Tuple[Dict[str, Decimal], Dict[str, Decimal]]: + target_x_ = {} + source_x_ = {} + for k in self.k: + difference = balanced_coordinates_[k] - self.x_[k] + if difference < 0: + target_x_[k] = abs(difference) + elif difference > 0: + source_x_[k] = abs(difference) + return (target_x_, source_x_) + + def get_largest_value_from_trade_set( + self, + trade_set: Dict[str, Decimal] + ) -> Tuple[str, Decimal]: + return max(trade_set.items(), key=lambda x: x[1]) + + def find_rebalancing_path( + self, + target_x_: Tuple[str, Decimal], + source_x_: Tuple[str, Decimal] + ) -> Tuple[Dict[str, Decimal], Dict[str, Decimal]]: + target_id, target_amount = self.get_largest_value_from_trade_set(target_x_) + source_id, source_amount = self.get_largest_value_from_trade_set(source_x_) + try: + target_amount = self.trade_by_source(source_id, target_id, source_amount) + message = f"Swap {source_amount:.18f} x_{source_id} for {target_amount:.18f} x_{target_id}" + except AssertionError: + source_amount = self.trade_by_target(source_id, target_id, target_amount) + message = f"Swap {source_amount:.18f} x_{source_id} for {target_amount:.18f} x_{target_id}" + return message + + def rebalance_pool( + self + ): + rebalance_instructions = [] + balanced_coordinates_ = self.determine_balanced_pool_state() + target_x_, source_x_ = self.get_rebalance_trade_sets(balanced_coordinates_) + while any(not self.isclose_decimal(v, self.ZERO) for v in target_x_.values()): + rebalance_instructions.append(self.find_rebalancing_path(target_x_, source_x_)) + target_x_, source_x_ = self.get_rebalance_trade_sets(balanced_coordinates_) + if len(target_x_) == 0 or len(source_x_) == 0: + break + return rebalance_instructions + + def update_oracle_prices( + self, + updated_P_: Dict[str, Decimal] + ) -> None: + assert all(val > self.ZERO for val in updated_P_.values()), "Not all values in P_ are > 0" + if self.P_.keys() == updated_P_.keys(): + self.P_ = updated_P_ + else: + raise ValueError("Keys do not match. Are these the correct oracle prices?.") + + def initialize_k_n( + self + ) -> Tuple[List[str], int]: + assert all(val > self.ZERO for val in self.x_.values()), "Not all values in x_ are > 0" + assert all(val > self.ZERO for val in self.r_.values()), "Not all values in r_ are > 0" + + if self.x_.keys() == self.r_.keys() and self.r_.keys() == self.P_.keys(): + return list(self.x_.keys()), int(len(self.x_.keys())) + + def state_printer( + self + ) -> None: + data1 = [[k, + f"{self.x_[k]:.18f}", + f"{self.r_[k]:.18f}", + f"${1/self.P_[k]:.2f}" # inverse of P_ for Oracle price + ] for k in self.k] + data2 = [[i] + [f"{self.calculate_marginal_price(i, j):.18f}" for j in self.k] for i in self.k] + print("Table 1: Reserves, Ratios, and Oracle Prices\n") + print(tabulate(data1, + headers=["token", "Reserve balance", "Reserve ratio", "Oracle price"], + tablefmt="pretty")) + print("\n") + print("Table 2: Exchange Rates\n") + print(tabulate(data2, + headers=[""] + self.k, + tablefmt="pretty")) + return(None) + + +# + +x_ = {'a' : Decimal('100'), 'b' : Decimal('75'), 'c' : Decimal('16') + Decimal('2')/Decimal('3'), 'd' : Decimal('18.75'), 'e' : Decimal('25')} +r_ = {'a' : Decimal('0.2'), 'b' : Decimal('0.3'), 'c' : Decimal('0.1'), 'd' : Decimal('0.15'), 'e' : Decimal('0.25')} +P_ = {'a' : Decimal('1')/Decimal('1.4'), 'b' : Decimal('1')/Decimal('2.55'), 'c' : Decimal('1')/Decimal('3'), 'd' : Decimal('1')/Decimal('4'), 'e' : Decimal('1')/Decimal('5')} + +pool = BalancerArbitrage(x_, r_, P_) +# - + +pool.state_printer() + +pool.rebalance_pool() + +pool.state_printer() + +updated_P_ = {'a' : Decimal('1'), 'b' : Decimal('1'), 'c' : Decimal('1'), 'd' : Decimal('1'), 'e' : Decimal('1')} +pool.update_oracle_prices(updated_P_) +pool.state_printer() + +pool.rebalance_pool() + +pool.state_printer() + + diff --git a/resources/docs/Weighted Constant Product.md b/resources/docs/Weighted Constant Product.md new file mode 100644 index 000000000..6092f8dbd --- /dev/null +++ b/resources/docs/Weighted Constant Product.md @@ -0,0 +1,53 @@ +# Weighted Constant Product Formulas + +**Parameter definitions** + +- $x,y$ are the token balances in their native units +- $\alpha$ is the weight of the $x$ token ($1/2$ is standard constant product) +- $\lambda = {\alpha}/{1-\alpha}$ is the weight ratio, equivalent to $\alpha$ but providing a different parameterization +- $k$ the pool invariant + +Formula D1. (Definition of lambda) +$$ +\lambda = \frac{\alpha}{1-\alpha} +$$ + +Formula D2. (Reverse lambda) +$$ +\alpha = \frac{\lambda}{1-\lambda} +$$ + +Formula D3. (Lambda relationship) +$$ +\frac{1}{\lambda-1} = \alpha - 1 +$$ + +Formula 1. (Invariant) +$$ +x^\alpha y^{1-\alpha} = k^\alpha +$$ + +Formula 2, 3. (y in terms of x) +$$ +y(x) = +\left(\frac{k}{x}\right)^{\frac{\alpha}{1-\alpha}} = +\left(\frac{k}{x}\right)^\lambda +$$ + +Formula 4. (marginal price) +$$ +p = \frac{dy}{dx} = \lambda k^\lambda x^{\lambda-1} = \lambda \frac{y}{x} +$$ + +Formula 5. (x in terms of p) +$$ +x(p) = k^\alpha \left(\frac{p}{\lambda}\right)^{\alpha-1} +$$ + +Formula 6. (x in terms of p) +$$ +y(p) = k^\alpha \left(\frac{p}{\lambda}\right)^{\alpha} +$$ + + + diff --git a/resources/docs/Weighted Constant Product.py b/resources/docs/Weighted Constant Product.py new file mode 100644 index 000000000..f682db6fe --- /dev/null +++ b/resources/docs/Weighted Constant Product.py @@ -0,0 +1,206 @@ +# --- +# jupyter: +# jupytext: +# formats: ipynb,py:light +# text_representation: +# extension: .py +# format_name: light +# format_version: '1.5' +# jupytext_version: 1.13.1 +# kernelspec: +# display_name: Python 3 +# language: python +# name: python3 +# --- + +import numpy as np +import sympy as sp +import matplotlib.pyplot as plt + +# # Weighted Constant Product Formulas + + +# ### Definitions + +# - $x,y$ are the token balances in their native units +# - $\alpha$ is the weight of the $x$ token ($1/2$ is standard constant product) +# - $\eta = {\alpha}/{1-\alpha}$ is the weight ratio, equivalent to $\alpha$ but providing a different parameterization +# - $k$ the pool invariant + +# #### Formula D1. (Definition of eta) +# $$ +# \eta = \frac{\alpha}{1-\alpha} +# $$ + +# #### Formula D2. (Reverse eta) **OK** +# $$ +# \alpha = \frac{\eta}{\eta+1} +# $$ + + +# #### Formula D3. +# $$ +# \frac{\eta}{\eta-1} = \frac \alpha {2\alpha -1} +# $$ + +# #### Formula D4. +# $$ +# \frac{1}{\eta-1} = \frac {1-\alpha} {2\alpha -1} +# $$ + +# #### Formula D5. +# $$ +# \eta + 1 = \frac{1}{1-\alpha} +# $$ + +# #### Formula D6. + +# $$ +# \eta(1-\alpha)=\alpha +# $$ + +# ### Operational formulas + +# #### Formula 1. (Invariant) +# $$ +# x^\alpha y^{1-\alpha} = k^\alpha +# $$ + +# #### Formula 2. (x in terms of y) +# $$ +# x(y) +# = \frac{k}{ y^{\frac{1-\alpha}{\alpha}} } +# = \frac{k}{ y^{\frac{1}{\eta}} } +# $$ + +# #### Formula 3. (y in terms of x) +# $$ +# y(x) = +# \left(\frac{k}{x}\right)^{\frac{\alpha}{1-\alpha}} = +# \left(\frac{k}{x}\right)^\eta +# $$ + + +# #### Formula 4. (marginal price) +# $$ +# p = \frac{dy}{dx} +# = \eta \frac{y}{x} = \eta k^\eta x^{\eta-1} +# = \eta k^{-1} y^{1+\frac 1 \eta} +# $$ + +# #### Formula 5. (price response function $x(p)$) +# $$ +# x(p) +# = +# \left(\frac \eta p\right)^{1-\alpha} k^\alpha +# $$ + +# #### Formula 6. (price response function $y(p)$) +# $$ +# y(p) = \left( \frac{kp}{\eta} \right)^\alpha +# $$ + + +# ## Reconciliation + +# $$ +# \prod_{l} +# x_l{} ^ {r_l} +# = \kappa +# $$ + +# $$ +# x_i {}^ {r_i} +# = +# \kappa \prod_{l \neq i} x_l ^ {- r_l} +# $$ + +# $$ +# - \frac{ \partial x_{_{i}} } { \partial x_{_{j}} } +# = \frac {P_i} {P_j} +# = +# \frac{x_i}{x_j} +# \left(\frac{ r_i } { r_j } \right) ^ { - 1 } +# = \frac{x_i\,r_j}{x_j\,r_i} +# $$ + +# In this equation +# $$ +# x_i = +# \frac +# {\kappa P_i r_i} +# {\prod_{l} \left( P_l\, r_l \right) ^ {r_l}} +# $$ + +# For $x$ we get Formula 5 starting with and simplifying the below formula using we choose the token balances $x=x_1, y=x_2$, the weights $\alpha_1=\alpha, \alpha_2=1-\alpha$, and the prices $p=p_1/p_2$ and the pool constant $\kappa = k^\alpha$: +# $$ +# x_1 = \frac{\kappa p_1 \alpha_1} +# {(p_1 \alpha_1)^{\alpha_1}\cdot (p_2 \alpha_2)^{\alpha_2}} +# $$ +# +# Formula 6 we get when we start with the same equation except with $x_2=\cdots$ and $\kappa p_2 \alpha_2$ in the numerator + +# ## Testing + + + +x, y, k, p, al, eta = sp.symbols(r"x y k p \alpha \eta", real=True, positive=True) + +eta_eq = sp.Eq(eta, al/(1-al)) +eta_eq + +reta_eq = sp.Eq(al, eta/(1+eta)) +reta_eq + + +pxl_eq = sp.Eq(x, (1/k)**(eta/(eta-1)) * (p/eta)**(1/(eta-1))) +pxl_eq + +pxa_eq = pxl_eq.subs(eta_eq.lhs, eta_eq.rhs).simplify() +pxa_eq + +pya_eq = sp.Eq(y, k**al * (p/eta)**al).subs(eta_eq.lhs, eta_eq.rhs) +pya_eq + +inv_eq.subs(pxa_eq.lhs, pxa_eq.rhs).subs(pya_eq.lhs, pya_eq.rhs).simplify() + + +al_eq = sp.Eq(al, sp.solve(eta_eq, al)[0]) +al_eq + +eta_eq2 = sp.Eq(eta/(eta-1), (eta/(eta-1)).subs(eta, eta_eq.rhs).simplify()) +eta_eq2 + +eta_eq3 = sp.Eq(1/(eta-1), (1/(eta-1)).subs(eta, eta_eq.rhs).simplify()) +eta_eq3 + +px_eq0 = sp.Eq(x, (1/k)**(al/(2*al-1)) * (p/eta)**(al-1)) +px_eq = sp.Eq(x_eq0.lhs, x_eq0.rhs.subs(eta, eta_eq.rhs)) +px_eq0 + +py_eq0 = sp.Eq(y, (1/k)**(al/(2*al-1)) * (p/eta)**((1-al)/(2*al-1))) +py_eq = sp.Eq(x_eq0.lhs, x_eq0.rhs.subs(eta, eta_eq.rhs)) +py_eq0 + +inv_eq = sp.Eq(x**al * y**(1-al), k**al) +inv_eq + +y_eq0 = sp.Eq(y, (k/x)**eta) +y_eq = sp.Eq(y_eq0.lhs, y_eq0.rhs.subs(eta, eta_eq.rhs)) +y_eq0 + +y_f1 = sp.solve(inv_eq, y)[0] +y_f1 + +y_f2 = (k/x)**(al/(1-al)) +y_f2 + +(y_f1-y_f2).simplify() + +(y_f1-y_eq.rhs).simplify() + +(sp.solve(inv_eq, y)[0]/y_eq.rhs).simplify() + +inv_eq.subs(x, px_eq.rhs).subs(y, py_eq.rhs).simplify() + + diff --git a/resources/open-issues.md b/resources/open-issues.md deleted file mode 100644 index 7ce26f61b..000000000 --- a/resources/open-issues.md +++ /dev/null @@ -1,49 +0,0 @@ -# Open Issues - -## Prioritized - -1. Granular logging of update process to see where it hangs [DB2] -- CHECK -1. Carbon-only updater [DB6] -1. Update scripts (ix and win), producing at least heartbeat output [DB3] -1. Event updater for mainnet [DB4] -1. Non-volatile CIDs [DB7] - -## Laundry List - -### Database - -1. Missing decimals issue - DONE -2. Granular logging of update process to see where it hangs -3. Update scripts (ix and win), producing at least heartbeat output -4. Event updater for mainnet -5. Session persistance / session closure -6. Carbon-only updater -7. Non-volatile CIDs -8. Writing database tests - -### Provider - -1. Expiring filters -2. Brownie hanging issues - -### Sundry testing - -1. Instantiate unittest database bot and do stats on get_curves - -### Methodology - -1. Triangles methodology -2. Arbitrage protocol health dashboard - -## Stragic roadmap - -1. Make the bot running for pairs on mainnet and can be deployed - 1. Ensure database is working smoothly on mainnet (and ideally on tenderly) - 1. Ensure provider is working smoothly on mainnet (and ideally on tenderly) - 1. Verify that addressable arbitrage opportunities are found and closed - 1. Deploy bot on server or on some other connected machine -1. Make sure the bot installs and runs cleanly on a number of architectures -1. Review bot structure (do not start refactoring) -1. Add triangles methodology -1. Effectuate refactoring (possibly before triangles) -1. Review and complete tests \ No newline at end of file diff --git a/resources/repo-rules.md b/resources/repo-rules.md deleted file mode 100644 index cbb40a94d..000000000 --- a/resources/repo-rules.md +++ /dev/null @@ -1,38 +0,0 @@ -# Repo Rules [whilst repo is private] - -## Core rules - -1. The main branch is `main`. This branch is _forward-only_, ie no merge-commits, and there is a strict no-rewrite rule enforced on the server. If you push code erroneously you must correct this using `git revert`. - -2. In order to be able to push to main, you need to `git rebase` your code on top of the latest main. The command for this is `git checkout main` then `git pull` to get the latest main, then `git checkout mybranch` and `git rebase --onto main`. Here `` is the last commit of main below your pre-rebase branch. **If you have conflicts you must resolve them before you can push. You must not delete anyone else's functional code in this process, and it is in your interest to push things to main early to avoid merge conflicts associated with long-running topic branches.** - -3. Unit tests will be prepared using the `NBTest` framework, like it is done [in the `Carbon Simulator` repo][nbt]. The key features of this framework are - - 1. All tests live in Jupyter notebooks, and typically you will create those Jupyter notebooks as you go along creating your code, so by the time your code is finished your tests will be finished too. - - 2. In the notebooks, Heading 2 sections (`## Heading2`) indicate an individual test. The test name is the text of the heading (meaning you should only use alphanumeric characters and spaces in the heading text). Note: variables that you define in one test will **not** be available in another test. However, all variables defined _before_ the first Heading 2 section will be available in all tests. - - 3. Test notebooks **must** print out all the version numbers of the components they are using, so that it can be asserted that the tests are in line with the latest available versions [see below]. - - 4. The NBTest framework relies on [`JupyText`][jt] as it picks up the tests from the `lightscript` version of the notebook; you must install JupyText on your system; if you usually run test notebooks in VSCode you may have to open and save them in the standard Jupyter Lab environment before you commit. - -4. All objects tested have a `__VERSION__` and a `__DATE__` (spelled thusly, in capital letters). It is recommended to set those variables at the top of the file in which they are defined, and then just include them into the classes using `__VERSION__ = __VERSION__` immediately after the end of the docstring - - 1. Every day you touch an object, you **must** update its version and date, regardless how small the change, and re-run and re-commit the associated NBTest notebook. - - 2. Whenever you push to main, the top-most commit changing an object must also change its version number, ie you cannot amend objects on main without changing their version number. - - 3. If you want you _can_ merge multiple version updates into one, effectively skipping version numbers. Of course this only works before you have pushed to main, as main cannot be rewritten. - -5. You **must not sit on private branches** and work should be merged back into main as soon as feasible, ideally after assuring that all tests pass (ie `run_tests` works). **You must never keep private branches when you are not reachable on Telegram** either because you have left for the day or because you are taking a break of more than a few hours. If you know you will be gone for a day or more **you must ensure that your code is merged into main before you leave and reasonably clean**. Plan accordingly. - -## Other rules - -1. There will be **no use of Black or any other automated formatters on the repo level**. If you like to run Black locally you are free to do so on the modules that are your responsibility. However, **Black changes must be committed separately from functional changes**, clearly marked as such in the commit message, and Black changes must never be merged with functional changes. - -2. **Pull requests**: in my personal view, pull requests are over engineered for a small number of core contributors: you should just merge your own code into main and take responsibility for it. However -- if you prefer the github-enabled pull request process then I am not against it. **In any case it is up to the person making the pull request to ensure that it is being taken care of in a sufficiently timely manner.** - -_Note: once the repo is published we may need to introduce and intermediate branch called `beta` or `devmain` to which the above rules will apply, and that is periodically merged into the actual `main`. Whether or not you want to allow rewrites before the merge is up to the consensus of the contributors, keeping in mind that rewriting devmain will require (slightly) more git skills as the two-argument form of `git rebase` will be required._ - -[jt]:https://jupytext.readthedocs.io/en/latest/ -[nbt]:https://github.com/bancorprotocol/carbon-simulator/tree/main/resources/NBTest \ No newline at end of file diff --git a/resources/whiskeyrules.md b/resources/whiskeyrules.md deleted file mode 100644 index 17453466c..000000000 --- a/resources/whiskeyrules.md +++ /dev/null @@ -1,48 +0,0 @@ -# Whiskey Rules - - ,,,,,,,,,,,,. - &,...,/@@#,,..*, - &,..,*@@%,,,,(. - ,*****//(/****( - **//(((((/// - ##%%%%%%%%%% - %%%%%%%%%%%% - ##%%%%%%%%%% - #****#%/***( - .. ......... - ,**//##(////////(%#(//*,. - /%(/**////(((############(((((((((#% - #,***/////((((############(((((((////#% - (****/////((((###############(((((((///#) - &****/////(((((################((((((////#. - %/***/////((((###################((((((////% - /***/////(((###########(#########(((((((////% - @/**/////(((#/##////##(///####///(##(((((////) - /***////(((##/####/##(#####/#(###*##((((((////% - #/**/////(((##/###((##/#####/####*####((((((///% - &***////((((##/###/####((((/####/((###((((((///| - %**////(((((#############/#############(((((////. - #**////(((((###########################((((((///* - %*/////(((((############################(((((//#, - @//////(((((############((##############(((((//# - #(////((((((######################(#####(((((//& - #*///((((((###############(############(((((/(/ - #*////(((((###########################(((((//% - /**////((((((((((((((((((((((((((((((((((//* - &*,*******************************/((#%#**. - @#/,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,#% - -From this commit onwards the following infractions WILL result in an immediate fine of 1 bottle of whiskey owed by the `git blame`d party: - -(1) Leaving print statements in code without searchable text: - - print(variable) # fined - print("variable: ", variable) # not fined - -(2) Ditto for all other logging statements (for `logger.debug` exceptions can be made by consensus) - - logger.___(variable) # fined - logger.___("variable: ", variable) # not fined - - -