-
Notifications
You must be signed in to change notification settings - Fork 9
Oracle update #215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Oracle update #215
Changes from 12 commits
a414add
6fc4ca3
7fb30f6
63a8104
3e5c713
6a75308
1c51bba
ddb79ce
2fb9ddd
bf0486a
f2b4399
3ce134a
0aeb5b6
990c33d
30db8c5
407b592
03dafb6
acc81ef
d1a0843
ad22d33
f31a3a7
a3d2f37
d16f99f
2f4d7d0
39501a1
7479f8b
08c5da1
2ae6e16
4b6307d
f881406
89504b9
802eebb
9697d8b
036280e
c8f3910
cc4c4ef
abf9120
08cd1fb
f96993a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -10,6 +10,7 @@ def __init__(self, input_state: Exchange): | |||||
| self.withdrawals = {tkn: 0 for tkn in input_state.asset_list} | ||||||
| self.lps = {tkn: 0 for tkn in input_state.asset_list} | ||||||
| self.asset_list = input_state.asset_list.copy() | ||||||
| self.time_step = input_state.time_step | ||||||
|
|
||||||
|
|
||||||
| class Oracle: | ||||||
|
|
@@ -37,28 +38,29 @@ def __init__(self, first_block: Block = None, decay_factor: float = 0, sma_equiv | |||||
| else: | ||||||
| raise ValueError('Either last_values or first_block must be specified') | ||||||
| self.age = 0 | ||||||
| self.last_updated = {tkn: first_block.time_step - 1 if first_block is not None else 0 for tkn in self.asset_list} | ||||||
|
|
||||||
| def add_asset(self, tkn: str, liquidity: float): | ||||||
| self.liquidity[tkn] = liquidity | ||||||
| self.volume_in[tkn] = 0 | ||||||
| self.volume_out[tkn] = 0 | ||||||
| self.asset_list.append(tkn) | ||||||
|
|
||||||
| def update(self, block: Block): | ||||||
| self.age += 1 | ||||||
| for tkn in block.liquidity: | ||||||
| self.liquidity[tkn] = ( | ||||||
| (1 - self.decay_factor) * self.liquidity[tkn] + self.decay_factor * block.liquidity[tkn] | ||||||
| ) if tkn in self.liquidity else block.liquidity[tkn] | ||||||
| self.price[tkn] = ( | ||||||
| (1 - self.decay_factor) * self.price[tkn] + self.decay_factor * block.price[tkn] | ||||||
| ) if tkn in self.price else block.price[tkn] | ||||||
| self.volume_in[tkn] = ( | ||||||
| (1 - self.decay_factor) * self.volume_in[tkn] + self.decay_factor * block.volume_in[tkn] | ||||||
| ) if tkn in self.volume_in else block.volume_in[tkn] | ||||||
| self.volume_out[tkn] = ( | ||||||
| (1 - self.decay_factor) * self.volume_out[tkn] + self.decay_factor * block.volume_out[tkn] | ||||||
| ) if tkn in self.volume_out else block.volume_out[tkn] | ||||||
| def update(self, block: Block, assets: list[str] = None): | ||||||
| if assets is None: | ||||||
| assets = self.asset_list | ||||||
| update_steps = {tkn: max(block.time_step - self.last_updated[tkn], 1) for tkn in assets} | ||||||
| update_factor = {tkn: (1 - self.decay_factor) ** update_steps[tkn] for tkn in assets} | ||||||
| for attr in ['liquidity', 'price', 'volume_in', 'volume_out']: | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not requesting any change here... This is maybe a level of abstraction that makes it a little less clear what is going on for little benefit? I realize that on some level it saves doing the same thing 4 times, but there is cost to abstraction too... |
||||||
| block_attr = getattr(block, attr) | ||||||
| self_attr = getattr(self, attr) | ||||||
| for tkn in assets: | ||||||
| self_attr[tkn] = ( | ||||||
| update_factor[tkn] * self_attr.get(tkn, block_attr[tkn]) | ||||||
|
||||||
| update_factor[tkn] * self_attr.get(tkn, block_attr[tkn]) | |
| update_factor[tkn] * self_attr[tkn] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,7 +13,7 @@ | |
| from hydradx.model.amm.agents import Agent | ||
| from hydradx.model.amm.global_state import GlobalState | ||
| from hydradx.model.amm.omnipool_amm import DynamicFee, OmnipoolState, OmnipoolLiquidityPosition, trade_to_price | ||
| from hydradx.model.amm.trade_strategies import constant_swaps, omnipool_arbitrage | ||
| from hydradx.model.amm.trade_strategies import constant_swaps, omnipool_arbitrage, schedule_swaps | ||
| from hydradx.tests.strategies_omnipool import omnipool_reasonable_config, omnipool_config, assets_config | ||
|
|
||
| mp.dps = 50 | ||
|
|
@@ -1419,27 +1419,28 @@ def test_oracle_one_empty_block(liquidity: list[float], lrna: list[float], oracl | |
|
|
||
| events = run.run(initial_state=initial_state, time_steps=1, silent=True) | ||
| omnipool_oracle = events[0].pools['omnipool'].oracles['price'] | ||
| # manually update oracle - it won't automatically update tokens that weren't used this block | ||
| omnipool_oracle.update(events[-1].pools['omnipool'].current_block) | ||
| for tkn in ['HDX', 'USD', 'DOT']: | ||
| expected_liquidity = init_oracle['liquidity'][tkn] * (1 - alpha) + alpha * init_liquidity[tkn]['liquidity'] | ||
| if omnipool_oracle.liquidity[tkn] != expected_liquidity: | ||
| if omnipool_oracle.liquidity[tkn] != pytest.approx(expected_liquidity, rel=1e-12): | ||
| raise AssertionError('Liquidity is not correct.') | ||
|
|
||
| expected_vol_in = init_oracle['volume_in'][tkn] * (1 - alpha) | ||
| if omnipool_oracle.volume_in[tkn] != expected_vol_in: | ||
| if omnipool_oracle.volume_in[tkn] != pytest.approx(expected_vol_in, rel=1e-12): | ||
| raise AssertionError('Volume is not correct.') | ||
|
|
||
| expected_vol_out = init_oracle['volume_out'][tkn] * (1 - alpha) | ||
| if omnipool_oracle.volume_out[tkn] != expected_vol_out: | ||
| if omnipool_oracle.volume_out[tkn] != pytest.approx(expected_vol_out, rel=1e-12): | ||
| raise AssertionError('Volume is not correct.') | ||
|
|
||
| init_price = init_liquidity[tkn]['LRNA'] / init_liquidity[tkn]['liquidity'] | ||
| expected_price = init_oracle['price'][tkn] * (1 - alpha) + alpha * init_price | ||
| if omnipool_oracle.price[tkn] != expected_price: | ||
| if omnipool_oracle.price[tkn] != pytest.approx(expected_price, rel=1e-12): | ||
| raise AssertionError('Price is not correct.') | ||
|
|
||
|
|
||
| @given( | ||
| st.lists(asset_quantity_strategy, min_size=3, max_size=3), | ||
| st.lists(asset_quantity_bounded_strategy, min_size=3, max_size=3), | ||
| st.lists(asset_quantity_strategy, min_size=3, max_size=3), | ||
| st.lists(asset_quantity_strategy, min_size=3, max_size=3), | ||
|
|
@@ -1451,15 +1452,15 @@ def test_oracle_one_empty_block(liquidity: list[float], lrna: list[float], oracl | |
| @settings( | ||
| print_blob=True | ||
| ) | ||
| def test_oracle_one_block_with_swaps(liquidity: list[float], lrna: list[float], oracle_liquidity: list[float], | ||
| def test_oracle_one_block_with_swaps(lrna: list[float], oracle_liquidity: list[float], | ||
| oracle_volume_in: list[float], oracle_volume_out: list[float], | ||
| oracle_prices: list[float], trade_sizes: list[float], n): | ||
| alpha = 2 / (n + 1) | ||
|
|
||
| init_liquidity = { | ||
| 'HDX': {'liquidity': liquidity[0], 'LRNA': lrna[0]}, | ||
| 'USD': {'liquidity': liquidity[1], 'LRNA': lrna[1]}, | ||
| 'DOT': {'liquidity': liquidity[2], 'LRNA': lrna[2]}, | ||
| 'HDX': {'liquidity': 1000, 'LRNA': lrna[0]}, | ||
| 'USD': {'liquidity': 1000, 'LRNA': lrna[1]}, | ||
| 'DOT': {'liquidity': 1000, 'LRNA': lrna[2]}, | ||
| } | ||
|
|
||
| init_oracle = { | ||
|
|
@@ -1481,83 +1482,120 @@ def test_oracle_one_block_with_swaps(liquidity: list[float], lrna: list[float], | |
| } | ||
| ) | ||
|
|
||
| trader1_holdings = {'HDX': 1000000000, 'USD': 1000000000, 'LRNA': 1000000000, 'DOT': 1000000000} | ||
| trader2_holdings = {'HDX': 1000000000, 'USD': 1000000000, 'LRNA': 1000000000, 'DOT': 1000000000} | ||
|
|
||
| initial_state = GlobalState( | ||
| pools={'omnipool': initial_omnipool}, | ||
| agents={ | ||
| 'Trader1': Agent( | ||
| holdings=trader1_holdings, | ||
| trade_strategy=constant_swaps( | ||
| pool_id='omnipool', | ||
| sell_quantity=trade_sizes[0], | ||
| sell_asset='LRNA', | ||
| buy_asset='DOT' | ||
| ) | ||
| ), | ||
| 'Trader2': Agent( | ||
| holdings=trader2_holdings, | ||
| trade_strategy=constant_swaps( | ||
| pool_id='omnipool', | ||
| sell_quantity=trade_sizes[1], | ||
| sell_asset='DOT', | ||
| buy_asset='LRNA' | ||
| ) | ||
| ), | ||
| } | ||
| ) | ||
|
|
||
| events = run.run(initial_state=initial_state, time_steps=2, silent=True) | ||
| omnipool_oracle_0 = events[0].pools['omnipool'].oracles['price'] | ||
|
|
||
| vol_in = { | ||
| 'HDX': 0, | ||
| 'USD': 0, | ||
| 'DOT': trader2_holdings['DOT'] - events[0].agents['Trader2'].holdings['DOT'] | ||
| } | ||
|
|
||
| vol_out = { | ||
| 'HDX': 0, | ||
| 'USD': 0, | ||
| 'DOT': events[0].agents['Trader1'].holdings['DOT'] - trader1_holdings['DOT'], | ||
| } | ||
| omnipool_0 = initial_omnipool.copy() | ||
| omnipool_oracle_0 = omnipool_0.oracles['price'].update(omnipool_0.current_block) | ||
|
|
||
| for tkn in ['HDX', 'USD', 'DOT']: | ||
| # alpha_mod = alpha if vol_in[tkn] != 0 or vol_out[tkn] != 0 else 0 | ||
| expected_liquidity = init_oracle['liquidity'][tkn] * (1 - alpha) + alpha * init_liquidity[tkn]['liquidity'] | ||
| if omnipool_oracle_0.liquidity[tkn] != expected_liquidity: | ||
| if omnipool_oracle_0.liquidity[tkn] != pytest.approx(expected_liquidity, rel=1e-12): | ||
| raise AssertionError('Liquidity is not correct.') | ||
|
|
||
| expected_vol_in = init_oracle['volume_in'][tkn] * (1 - alpha) | ||
| if omnipool_oracle_0.volume_in[tkn] != expected_vol_in: | ||
| if omnipool_oracle_0.volume_in[tkn] != pytest.approx(expected_vol_in, rel=1e-12): | ||
| raise AssertionError('Volume is not correct.') | ||
|
|
||
| expected_vol_out = init_oracle['volume_out'][tkn] * (1 - alpha) | ||
| if omnipool_oracle_0.volume_out[tkn] != expected_vol_out: | ||
| if omnipool_oracle_0.volume_out[tkn] != pytest.approx(expected_vol_out, rel=1e-12): | ||
| raise AssertionError('Volume is not correct.') | ||
|
|
||
| init_price = init_liquidity[tkn]['LRNA'] / init_liquidity[tkn]['liquidity'] | ||
| expected_price = init_oracle['price'][tkn] * (1 - alpha) + alpha * init_price | ||
| if omnipool_oracle_0.price[tkn] != expected_price: | ||
| if omnipool_oracle_0.price[tkn] != pytest.approx(expected_price, rel=1e-12): | ||
| raise AssertionError('Price is not correct.') | ||
|
|
||
| omnipool_oracle_1 = events[1].pools['omnipool'].oracles['price'] | ||
| trader = Agent(enforce_holdings=False) | ||
| omnipool_1 = omnipool_0.copy().swap( | ||
| agent=trader, | ||
| tkn_sell='DOT', | ||
| tkn_buy='LRNA', | ||
| sell_quantity=trade_sizes[0] | ||
| ).swap( | ||
| agent=trader, | ||
| tkn_sell='LRNA', | ||
| tkn_buy='DOT', | ||
| buy_quantity=trade_sizes[1] | ||
| ) | ||
| vol_in = omnipool_1.current_block.volume_in | ||
| vol_out = omnipool_1.current_block.volume_out | ||
| omnipool_oracle_1 = omnipool_1.oracles['price'].update(omnipool_1.current_block) | ||
| for tkn in ['HDX', 'USD', 'DOT']: | ||
| expected_liquidity = omnipool_oracle_0.liquidity[tkn] * (1 - alpha) + alpha * init_liquidity[tkn]['liquidity'] | ||
| if omnipool_oracle_1.liquidity[tkn] != pytest.approx(expected_liquidity, 1e-10): | ||
| if omnipool_oracle_1.liquidity[tkn] != pytest.approx(expected_liquidity, 1e-12): | ||
| raise AssertionError('Liquidity is not correct.') | ||
|
|
||
| expected_vol_in = omnipool_oracle_0.volume_in[tkn] * (1 - alpha) + alpha * vol_in[tkn] | ||
| if omnipool_oracle_1.volume_in[tkn] != pytest.approx(expected_vol_in, 1e-9): | ||
| if omnipool_oracle_1.volume_in[tkn] != pytest.approx(expected_vol_in, 1e-12): | ||
| raise AssertionError('Volume is not correct.') | ||
|
|
||
| expected_vol_out = omnipool_oracle_0.volume_out[tkn] * (1 - alpha) + alpha * vol_out[tkn] | ||
| if omnipool_oracle_1.volume_out[tkn] != pytest.approx(expected_vol_out, 1e-9): | ||
| if omnipool_oracle_1.volume_out[tkn] != pytest.approx(expected_vol_out, 1e-12): | ||
| raise AssertionError('Volume out is not correct.') | ||
|
|
||
| expected_price = omnipool_oracle_0.price[tkn] * (1 - alpha) + alpha * omnipool_1.lrna_price(tkn) | ||
| if omnipool_oracle_1.price[tkn] != pytest.approx(expected_price, 1e-12): | ||
| raise AssertionError('Price is not correct.') | ||
|
|
||
|
|
||
| def test_oracle_multi_block(): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we fuzz the number of blocks?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not on this one, because it's testing for exact values. I do fuzz time steps on other tests. |
||
| init_liquidity = { | ||
| 'HDX': {'liquidity': 100000, 'LRNA': 100000}, | ||
| 'USD': {'liquidity': 100000, 'LRNA': 100000}, | ||
| 'DOT': {'liquidity': 100000, 'LRNA': 100000}, | ||
| } | ||
|
|
||
| init_oracle = { | ||
| 'liquidity': {'HDX': 100000, 'USD': 100000, 'DOT': 100000}, | ||
| 'volume_in': {'HDX': 0, 'USD': 0, 'DOT': 0}, | ||
| 'volume_out': {'HDX': 0, 'USD': 0, 'DOT': 0}, | ||
| 'price': {'HDX': 1.0, 'USD': 1.0, 'DOT': 1.0}, | ||
| } | ||
|
|
||
| initial_omnipool = oamm.OmnipoolState( | ||
| tokens=copy.deepcopy(init_liquidity), | ||
| oracles={ | ||
| 'price': 10 | ||
| }, | ||
| asset_fee=0.0025, | ||
| lrna_fee=0.0005, | ||
| last_oracle_values={ | ||
| 'price': copy.deepcopy(init_oracle) | ||
| } | ||
| ) | ||
|
|
||
| initial_state = GlobalState( | ||
| pools={'omnipool': initial_omnipool}, | ||
| agents={'trader': Agent( | ||
| enforce_holdings=False, | ||
| trade_strategy=schedule_swaps( | ||
| 'omnipool', swaps=[ | ||
| *[None] * 4, | ||
| {'tkn_sell': 'DOT', 'tkn_buy': 'HDX', 'sell_quantity': 1000}, | ||
| *[None] * 4, | ||
| {'tkn_sell': 'HDX', 'tkn_buy': 'DOT', 'buy_quantity': 1000}, | ||
| ] | ||
| ) | ||
| )} | ||
| ) | ||
|
|
||
| events = run.run(initial_state=initial_state, time_steps=10, silent=True) | ||
| final_omnipool = events[-1].pools['omnipool'].update() | ||
| omnipool_oracle = final_omnipool.oracles['price'] | ||
| expected_vol_in = {'DOT': 232.21719930388855, 'HDX': 622.8414188596074, 'USD': 0} | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know this is harder to set up, but I think it'd be much better to test against our old setup of updating oracles every block, rather than test against hardcoded values? We should test the 1-block oracle update in other ways (potentially against hardcoded values), but those tests should already be in place I think?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's what this one does: |
||
| expected_vol_out = {'DOT': 633.3521679467996, 'HDX': 226.98232636186913, 'USD': 0} | ||
| expected_liquidity = {'DOT': 100633.35216794681, 'HDX': 99380.92549166107, 'USD': 100000} | ||
| expected_price = {'DOT': 0.9954561519474251, 'HDX': 1.0045748061650337, 'USD': 1.0} | ||
| for tkn in initial_omnipool.asset_list: | ||
| if omnipool_oracle.liquidity[tkn] != pytest.approx(expected_liquidity[tkn], rel=1e-12): | ||
| raise AssertionError('Liquidity is not correct.') | ||
|
|
||
| if omnipool_oracle.volume_in[tkn] != pytest.approx(expected_vol_in[tkn], rel=1e-12): | ||
| raise AssertionError('Volume in is not correct.') | ||
|
|
||
| if omnipool_oracle.volume_out[tkn] != pytest.approx(expected_vol_out[tkn], rel=1e-12): | ||
| raise AssertionError('Volume out is not correct.') | ||
|
|
||
| price_1 = events[0].pools['omnipool'].price(tkn) | ||
| expected_price = omnipool_oracle_0.price[tkn] * (1 - alpha) + alpha * price_1 | ||
| if omnipool_oracle_1.price[tkn] != pytest.approx(expected_price, 1e-10): | ||
| if omnipool_oracle.price[tkn] != pytest.approx(expected_price[tkn], rel=1e-12): | ||
| raise AssertionError('Price is not correct.') | ||
|
|
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we have sometimes used math.pow() instead of "**", I don't exactly remember if it was for accuracy or what?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked it up, and it seems that the difference is that while ** is slightly faster, math.pow will always return a float (regardless of input type, including int or mpf). In this case it seems like ** is best.