DeFiPy 1.0.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- defipy/__init__.py +51 -0
- defipy/agents/ImpermanentLossAgent.py +182 -0
- defipy/agents/PriceThresholdSwapAgent.py +169 -0
- defipy/agents/TVLBasedLiquidityExitAgent.py +174 -0
- defipy/agents/VolumeSpikeNotifierAgent.py +190 -0
- defipy/agents/__init__.py +4 -0
- defipy/agents/config/ImpermanentLossConfig.py +28 -0
- defipy/agents/config/PriceThresholdConfig.py +27 -0
- defipy/agents/config/TVLExitConfig.py +28 -0
- defipy/agents/config/VolumeSpikeConfig.py +27 -0
- defipy/agents/config/__init__.py +7 -0
- defipy/agents/data/UniswapPoolData.py +26 -0
- defipy/agents/data/__init__.py +1 -0
- defipy/analytics/risk/__init__.py +1 -0
- defipy/analytics/simulate/__init__.py +1 -0
- defipy/erc/__init__.py +1 -0
- defipy/math/basic/__init__.py +1 -0
- defipy/math/interest/__init__.py +1 -0
- defipy/math/interest/ips/__init__.py +1 -0
- defipy/math/interest/ips/aggregate/__init__.py +1 -0
- defipy/math/model/__init__.py +1 -0
- defipy/math/risk/__init__.py +1 -0
- defipy/process/__init__.py +1 -0
- defipy/process/burn/__init__.py +1 -0
- defipy/process/deposit/__init__.py +1 -0
- defipy/process/join/Join.py +57 -0
- defipy/process/join/__init__.py +2 -0
- defipy/process/liquidity/AddLiquidity.py +57 -0
- defipy/process/liquidity/RemoveLiquidity.py +57 -0
- defipy/process/liquidity/__init__.py +2 -0
- defipy/process/mint/__init__.py +1 -0
- defipy/process/swap/Swap.py +57 -0
- defipy/process/swap/__init__.py +2 -0
- defipy/utils/client/__init__.py +1 -0
- defipy/utils/client/contract/ExecuteScript.py +57 -0
- defipy/utils/client/contract/__init__.py +1 -0
- defipy/utils/data/__init__.py +1 -0
- defipy/utils/interfaces/__init__.py +1 -0
- defipy/utils/tools/UniswapScriptHelper.py +81 -0
- defipy/utils/tools/__init__.py +2 -0
- defipy/utils/tools/v3/__init__.py +1 -0
- defipy-1.0.9.dist-info/METADATA +247 -0
- defipy-1.0.9.dist-info/RECORD +47 -0
- defipy-1.0.9.dist-info/WHEEL +5 -0
- defipy-1.0.9.dist-info/licenses/LICENSE +178 -0
- defipy-1.0.9.dist-info/licenses/NOTICE +16 -0
- defipy-1.0.9.dist-info/top_level.txt +1 -0
defipy/__init__.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# This file participates in a symbolic cognition substrate.
|
|
2
|
+
|
|
3
|
+
from defipy.erc import *
|
|
4
|
+
from defipy.math.basic import *
|
|
5
|
+
from defipy.math.interest import *
|
|
6
|
+
from defipy.math.interest.ips import *
|
|
7
|
+
from defipy.math.interest.ips.aggregate import *
|
|
8
|
+
from defipy.math.model import *
|
|
9
|
+
from defipy.math.risk import *
|
|
10
|
+
from defipy.process import *
|
|
11
|
+
from defipy.process.burn import *
|
|
12
|
+
from defipy.process.deposit import *
|
|
13
|
+
from defipy.process.liquidity import *
|
|
14
|
+
from defipy.process.mint import *
|
|
15
|
+
from defipy.process.swap import *
|
|
16
|
+
from defipy.process.join import *
|
|
17
|
+
from defipy.analytics.simulate import *
|
|
18
|
+
from defipy.analytics.risk import *
|
|
19
|
+
from defipy.utils.interfaces import *
|
|
20
|
+
from defipy.utils.data import *
|
|
21
|
+
from defipy.utils.client import *
|
|
22
|
+
from defipy.utils.client.contract import *
|
|
23
|
+
from defipy.utils.tools import *
|
|
24
|
+
from defipy.agents.config import *
|
|
25
|
+
from defipy.agents.data import *
|
|
26
|
+
from defipy.agents import *
|
|
27
|
+
|
|
28
|
+
from uniswappy.cpt.exchg import *
|
|
29
|
+
from uniswappy.cpt.factory import *
|
|
30
|
+
from uniswappy.cpt.index import *
|
|
31
|
+
from uniswappy.cpt.quote import *
|
|
32
|
+
from uniswappy.cpt.vault import *
|
|
33
|
+
from uniswappy.cpt.wallet import *
|
|
34
|
+
from uniswappy.utils.tools.v3 import *
|
|
35
|
+
|
|
36
|
+
from stableswappy.quote import *
|
|
37
|
+
from stableswappy.vault import *
|
|
38
|
+
from stableswappy.cst.factory import *
|
|
39
|
+
from stableswappy.cst.exchg import *
|
|
40
|
+
from stableswappy.utils.data import StableswapExchangeData
|
|
41
|
+
|
|
42
|
+
from balancerpy.quote import *
|
|
43
|
+
from balancerpy.vault import *
|
|
44
|
+
from balancerpy.cwpt.factory import *
|
|
45
|
+
from balancerpy.cwpt.exchg import *
|
|
46
|
+
from balancerpy.enums import *
|
|
47
|
+
from balancerpy.utils.data import BalancerExchangeData
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
# Apache 2.0 License (DeFiPy)
|
|
3
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
# Copyright 2023–2025 Ian Moore
|
|
5
|
+
# Email: defipy.devs@gmail.com
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel
|
|
20
|
+
from web3scout.utils.connect import ConnectW3
|
|
21
|
+
from web3scout.abi.abi_load import ABILoad
|
|
22
|
+
from web3scout.event.process.retrieve_events import RetrieveEvents
|
|
23
|
+
from web3scout.token.fetch.fetch_token import FetchToken
|
|
24
|
+
from .config import ImpermanentLossConfig
|
|
25
|
+
from .data import UniswapPoolData
|
|
26
|
+
from uniswappy import *
|
|
27
|
+
from web3 import Web3
|
|
28
|
+
|
|
29
|
+
class ImpermanentLossAgent:
|
|
30
|
+
def __init__(self, config: ImpermanentLossConfig, verbose: bool = False):
|
|
31
|
+
self.config = config
|
|
32
|
+
self.abi = ABILoad(self.config.platform, self.config.abi_name) # Load ABI here
|
|
33
|
+
self.connector = ConnectW3(self.config.provider_url) # Web3Scout setup
|
|
34
|
+
self.connector.apply()
|
|
35
|
+
self.verbose = verbose
|
|
36
|
+
self.user_position = config.user_position
|
|
37
|
+
self.exit_percentage = config.exit_percentage
|
|
38
|
+
self.iLoss = None
|
|
39
|
+
self.lp_contract = None
|
|
40
|
+
self.lp_data = None
|
|
41
|
+
self.lp_state = None
|
|
42
|
+
|
|
43
|
+
def init(self):
|
|
44
|
+
self.lp_contract = self._init_lp_contract()
|
|
45
|
+
|
|
46
|
+
reserves = self.lp_contract.functions.getReserves().call()
|
|
47
|
+
token0_address = self.lp_contract.functions.token0().call()
|
|
48
|
+
token1_address = self.lp_contract.functions.token1().call()
|
|
49
|
+
reserve0 = reserves[0]; reserve1 = reserves[1]
|
|
50
|
+
|
|
51
|
+
w3 = self.connector.get_w3()
|
|
52
|
+
FetchERC20 = FetchToken(w3)
|
|
53
|
+
TKN0 = FetchERC20.apply(token0_address)
|
|
54
|
+
TKN1 = FetchERC20.apply(token1_address)
|
|
55
|
+
|
|
56
|
+
self.lp_data = UniswapPoolData(TKN0, TKN1, reserves)
|
|
57
|
+
|
|
58
|
+
def get_connector(self):
|
|
59
|
+
return self.connector
|
|
60
|
+
|
|
61
|
+
def get_abi(self):
|
|
62
|
+
return self.abi
|
|
63
|
+
|
|
64
|
+
def get_w3(self):
|
|
65
|
+
return self.connector.get_w3()
|
|
66
|
+
|
|
67
|
+
def get_contract_instance(self):
|
|
68
|
+
return self.lp_contract
|
|
69
|
+
|
|
70
|
+
def get_lp_data(self):
|
|
71
|
+
return self.lp_data
|
|
72
|
+
|
|
73
|
+
def get_iloss(self):
|
|
74
|
+
return self.iLoss
|
|
75
|
+
|
|
76
|
+
def prime_mock_pool(self, start_block, user_nm = None):
|
|
77
|
+
w3 = self.get_w3()
|
|
78
|
+
fetch_tkn = FetchToken(w3)
|
|
79
|
+
|
|
80
|
+
lp_contract = self._init_lp_contract()
|
|
81
|
+
tkn0_addr = lp_contract.functions.token0().call()
|
|
82
|
+
tkn1_addr = lp_contract.functions.token1().call()
|
|
83
|
+
total_supply = lp_contract.functions.totalSupply().call(block_identifier=start_block)
|
|
84
|
+
reserves = lp_contract.functions.getReserves().call(block_identifier=start_block)
|
|
85
|
+
|
|
86
|
+
# Step 2: Define tokens
|
|
87
|
+
tkn0 = fetch_tkn.apply(tkn0_addr)
|
|
88
|
+
tkn1 = fetch_tkn.apply(tkn1_addr)
|
|
89
|
+
|
|
90
|
+
amt0 = fetch_tkn.amt_to_decimal(tkn0, reserves[0])
|
|
91
|
+
amt1 = fetch_tkn.amt_to_decimal(tkn1, reserves[1])
|
|
92
|
+
|
|
93
|
+
# Step 3: Initialize factory
|
|
94
|
+
factory = UniswapFactory("Pool factory", "0x2")
|
|
95
|
+
|
|
96
|
+
# Step 4: Set up exchange data for V2
|
|
97
|
+
exch_data = UniswapExchangeData(tkn0=tkn0, tkn1=tkn1, symbol="LP", address=self.config.pool_address)
|
|
98
|
+
|
|
99
|
+
# Step 5: Deploy pool
|
|
100
|
+
self.lp_state = factory.deploy(exch_data)
|
|
101
|
+
|
|
102
|
+
# Step 6: Add initial liquidity
|
|
103
|
+
join = Join()
|
|
104
|
+
join.apply(self.lp_state, user_nm, amt0, amt1)
|
|
105
|
+
self.lp_state.total_supply = total_supply # override total supply
|
|
106
|
+
|
|
107
|
+
return self.lp_state
|
|
108
|
+
|
|
109
|
+
def update_mock_pool(self, lp, cur_block):
|
|
110
|
+
w3 = self.get_w3()
|
|
111
|
+
fetch_tkn = FetchToken(w3)
|
|
112
|
+
|
|
113
|
+
lp_contract = self._init_lp_contract()
|
|
114
|
+
tkn0_addr = lp_contract.functions.token0().call()
|
|
115
|
+
tkn1_addr = lp_contract.functions.token1().call()
|
|
116
|
+
total_supply = lp_contract.functions.totalSupply().call(block_identifier=int(cur_block))
|
|
117
|
+
reserves = lp_contract.functions.getReserves().call(block_identifier=int(cur_block))
|
|
118
|
+
|
|
119
|
+
tkn0 = self.get_lp_data().tkn0
|
|
120
|
+
tkn1 = self.get_lp_data().tkn1
|
|
121
|
+
amt0 = fetch_tkn.amt_to_decimal(tkn0, reserves[0])
|
|
122
|
+
amt1 = fetch_tkn.amt_to_decimal(tkn1, reserves[1])
|
|
123
|
+
|
|
124
|
+
prev_total_supply = lp.total_supply
|
|
125
|
+
lp.reserve0 = lp.convert_to_machine(amt0) # override reserve0
|
|
126
|
+
lp.reserve1 = lp.convert_to_machine(amt1) # override reserve1
|
|
127
|
+
lp.total_supply = total_supply # override total supply
|
|
128
|
+
lp.last_liquidity_deposit = abs(prev_total_supply - lp.total_supply)
|
|
129
|
+
|
|
130
|
+
def run_batch(self, lp, tkn, user_nm, events: dict):
|
|
131
|
+
"""Process batched Sync events to check TVL and trigger exits."""
|
|
132
|
+
if not events:
|
|
133
|
+
print("No Sync events found in range.")
|
|
134
|
+
return
|
|
135
|
+
for k in events:
|
|
136
|
+
block_num = events[k]['blockNumber']
|
|
137
|
+
self.apply(lp, tkn, user_nm, block_num)
|
|
138
|
+
|
|
139
|
+
def apply(self, lp, tkn, user_nm, block_num):
|
|
140
|
+
"""Execute liquidity exit if condition met."""
|
|
141
|
+
self.update_mock_pool(lp, block_num)
|
|
142
|
+
if self.check_condition(tkn, self.config.il_threshold):
|
|
143
|
+
val = self.get_current_position_value(tkn)
|
|
144
|
+
print(f"Block {block_num}: Value ({tkn.token_name}) = {val}, outside loss threshold {self.config.il_threshold}")
|
|
145
|
+
return val
|
|
146
|
+
else:
|
|
147
|
+
print(f"Block {block_num}: Value threshold condition met for {lp.name} LP")
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
def take_mock_position(self, lp, tkn, user_nm, amt):
|
|
151
|
+
SwapDeposit().apply(lp, tkn, user_nm, amt)
|
|
152
|
+
self.mock_lp_pos_amt = lp.get_last_liquidity_deposit()
|
|
153
|
+
self.iLoss = UniswapImpLoss(lp, self.mock_lp_pos_amt)
|
|
154
|
+
return self.mock_lp_pos_amt
|
|
155
|
+
|
|
156
|
+
def get_impermanent_loss(self) -> float:
|
|
157
|
+
"""Calculate impermanent loss percentage based on initial and current reserves."""
|
|
158
|
+
returns_calc = self.iLoss.apply(fees = True)
|
|
159
|
+
return returns_calc
|
|
160
|
+
|
|
161
|
+
def get_current_position_value(self, tkn) -> float:
|
|
162
|
+
current_position_value = self.iLoss.current_position_value(tkn)
|
|
163
|
+
return current_position_value
|
|
164
|
+
|
|
165
|
+
def check_condition(self, tkn, threshold):
|
|
166
|
+
"""Check if TVL is below threshold."""
|
|
167
|
+
position_value = self.get_current_position_value(tkn)
|
|
168
|
+
return position_value < threshold
|
|
169
|
+
|
|
170
|
+
def withdraw_mock_position(self, lp, tkn, user_nm, lp_amt = None):
|
|
171
|
+
assert self.mock_lp_pos_amt != None, 'TVLBasedLiquidityExitAgent: MOCK_POSITION_UNAVAILABLE'
|
|
172
|
+
lp_amt = self.mock_lp_pos_amt if lp_amt == None else lp_amt
|
|
173
|
+
tkn_amt = LPQuote(False).get_amount_from_lp(lp, tkn0, lp_amt)
|
|
174
|
+
amount_out = WithdrawSwap().apply(lp, tkn0, user_nm, tkn_amt)
|
|
175
|
+
return amount_out
|
|
176
|
+
|
|
177
|
+
def _init_lp_contract(self):
|
|
178
|
+
pair_address = self.config.pool_address
|
|
179
|
+
w3 = self.get_w3()
|
|
180
|
+
abi_obj = self.get_abi()
|
|
181
|
+
lp_contract = abi_obj.apply(w3, pair_address)
|
|
182
|
+
return lp_contract
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
# Apache 2.0 License (DeFiPy)
|
|
3
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
# Copyright 2023–2025 Ian Moore
|
|
5
|
+
# Email: defipy.devs@gmail.com
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
|
|
19
|
+
from web3scout.event.process.retrieve_events import RetrieveEvents
|
|
20
|
+
from web3scout.utils.connect import ConnectW3
|
|
21
|
+
from web3scout.abi.abi_load import ABILoad
|
|
22
|
+
from web3scout.event.process.retrieve_events import RetrieveEvents
|
|
23
|
+
from web3scout.token.fetch.fetch_token import FetchToken
|
|
24
|
+
from web3scout.enums.event_type_enum import EventTypeEnum as EventType
|
|
25
|
+
from .config import PriceThresholdConfig
|
|
26
|
+
from .data import UniswapPoolData
|
|
27
|
+
from uniswappy import *
|
|
28
|
+
|
|
29
|
+
class PriceThresholdSwapAgent:
|
|
30
|
+
def __init__(self, config: PriceThresholdConfig, verbose: bool = False):
|
|
31
|
+
self.config = config
|
|
32
|
+
self.abi = ABILoad(self.config.platform, self.config.abi_name) # Load ABI here
|
|
33
|
+
self.connector = ConnectW3(self.config.provider_url) # Web3Scout setup
|
|
34
|
+
self.connector.apply()
|
|
35
|
+
self.verbose = verbose
|
|
36
|
+
self.lp_contract = None
|
|
37
|
+
self.lp_data = None
|
|
38
|
+
self.lp_state = None
|
|
39
|
+
|
|
40
|
+
def apply(self):
|
|
41
|
+
self.lp_contract = self._init_lp_contract()
|
|
42
|
+
|
|
43
|
+
reserves = self.lp_contract.functions.getReserves().call()
|
|
44
|
+
token0_address = self.lp_contract.functions.token0().call()
|
|
45
|
+
token1_address = self.lp_contract.functions.token1().call()
|
|
46
|
+
reserve0 = reserves[0]; reserve1 = reserves[1]
|
|
47
|
+
|
|
48
|
+
w3 = self.connector.get_w3()
|
|
49
|
+
FetchERC20 = FetchToken(w3)
|
|
50
|
+
TKN0 = FetchERC20.apply(token0_address)
|
|
51
|
+
TKN1 = FetchERC20.apply(token1_address)
|
|
52
|
+
|
|
53
|
+
self.lp_data = UniswapPoolData(TKN0, TKN1, reserves)
|
|
54
|
+
|
|
55
|
+
def run_batch(self, tkn, events):
|
|
56
|
+
start_block = events[0]['blockNumber']
|
|
57
|
+
lp = self.prime_pool_state(start_block, 'user')
|
|
58
|
+
|
|
59
|
+
"""Fetch batch of Sync events and process sequentially."""
|
|
60
|
+
if not events:
|
|
61
|
+
print("No Sync events found in range.")
|
|
62
|
+
return
|
|
63
|
+
for k in events:
|
|
64
|
+
reserve0 = events[k]['args']['reserve0']
|
|
65
|
+
reserve1 = events[k]['args']['reserve1']
|
|
66
|
+
block_num = events[k]['blockNumber']
|
|
67
|
+
event_price = self.calc_price(reserve0, reserve1, tkn1_over_tkn0 = True)
|
|
68
|
+
self.execute_action(lp, tkn, event_price, block_num)
|
|
69
|
+
|
|
70
|
+
def prime_pool_state(self, start_block, user_nm = None):
|
|
71
|
+
w3 = self.get_w3()
|
|
72
|
+
fetch_tkn = FetchToken(w3)
|
|
73
|
+
|
|
74
|
+
lp_contract = self._init_lp_contract()
|
|
75
|
+
tkn0_addr = lp_contract.functions.token0().call()
|
|
76
|
+
tkn1_addr = lp_contract.functions.token1().call()
|
|
77
|
+
total_supply = lp_contract.functions.totalSupply().call(block_identifier=start_block)
|
|
78
|
+
reserves = lp_contract.functions.getReserves().call(block_identifier=start_block)
|
|
79
|
+
|
|
80
|
+
# Step 2: Define tokens
|
|
81
|
+
tkn0 = fetch_tkn.apply(tkn0_addr)
|
|
82
|
+
tkn1 = fetch_tkn.apply(tkn1_addr)
|
|
83
|
+
|
|
84
|
+
amt0 = fetch_tkn.amt_to_decimal(tkn0, reserves[0])
|
|
85
|
+
amt1 = fetch_tkn.amt_to_decimal(tkn1, reserves[1])
|
|
86
|
+
|
|
87
|
+
# Step 3: Initialize factory
|
|
88
|
+
factory = UniswapFactory("Pool factory", "0x2")
|
|
89
|
+
|
|
90
|
+
# Step 4: Set up exchange data for V2
|
|
91
|
+
exch_data = UniswapExchangeData(tkn0=tkn0, tkn1=tkn1, symbol="LP", address=self.config.pool_address)
|
|
92
|
+
|
|
93
|
+
# Step 5: Deploy pool
|
|
94
|
+
self.lp_state = factory.deploy(exch_data)
|
|
95
|
+
|
|
96
|
+
# Step 6: Add initial liquidity
|
|
97
|
+
join = Join()
|
|
98
|
+
join.apply(self.lp_state, user_nm, amt0, amt1)
|
|
99
|
+
self.lp_state.total_supply = total_supply # override total supply
|
|
100
|
+
|
|
101
|
+
return self.lp_state
|
|
102
|
+
|
|
103
|
+
def execute_action(self, lp, tkn, price, block_num, tkn1_over_tkn0 = True):
|
|
104
|
+
|
|
105
|
+
tkn0 = self.lp_data.tkn0
|
|
106
|
+
tkn1 = self.lp_data.tkn1
|
|
107
|
+
|
|
108
|
+
"""Execute swap if condition met (simulated or live)."""
|
|
109
|
+
if self.check_condition(block_num = block_num, tkn1_over_tkn0 = tkn1_over_tkn0):
|
|
110
|
+
try:
|
|
111
|
+
out = Swap().apply(lp, tkn, "test_action", self.config.swap_amount)
|
|
112
|
+
print(f"Block {block_num}: Swapped {self.config.swap_amount} {tkn0.token_name} for {out} {tkn1.token_name}")
|
|
113
|
+
except Exception as e:
|
|
114
|
+
print(f"Block {block_number}: Swap failed: {e}")
|
|
115
|
+
|
|
116
|
+
def get_token_price(self, tkn1_over_tkn0 = True, block_num = None):
|
|
117
|
+
|
|
118
|
+
if(block_num == None):
|
|
119
|
+
reserves = self.lp_data.reserves
|
|
120
|
+
else:
|
|
121
|
+
lp_contract = self._init_lp_contract()
|
|
122
|
+
reserves = lp_contract.functions.getReserves().call(block_identifier=block_num)
|
|
123
|
+
|
|
124
|
+
price = self.calc_price(reserves[0], reserves[1], tkn1_over_tkn0)
|
|
125
|
+
|
|
126
|
+
return price
|
|
127
|
+
|
|
128
|
+
def calc_price(self, reserve0, reserve1, tkn1_over_tkn0 = True):
|
|
129
|
+
tkn0 = self.lp_data.tkn0
|
|
130
|
+
tkn1 = self.lp_data.tkn1
|
|
131
|
+
tkn0_decimal = tkn0.token_decimal
|
|
132
|
+
tkn1_decimal = tkn1.token_decimal
|
|
133
|
+
|
|
134
|
+
if(tkn1_over_tkn0):
|
|
135
|
+
price = (reserve0 / reserve1) * (10 ** (tkn1_decimal - tkn0_decimal))
|
|
136
|
+
if(self.verbose): print(f"{tkn1.token_name} Price in {tkn0.token_name}: {price}")
|
|
137
|
+
else:
|
|
138
|
+
price = (reserve1 / reserve0) * (10 ** (tkn0_decimal - tkn1_decimal))
|
|
139
|
+
if(self.verbose): print(f"{tkn0.token_name} Price in {tkn1.token_name}: {price}")
|
|
140
|
+
|
|
141
|
+
return price
|
|
142
|
+
|
|
143
|
+
def check_condition(self, threshold = None, tkn1_over_tkn0 = True, block_num = None):
|
|
144
|
+
self.config.threshold = self.config.threshold if threshold == None else threshold;
|
|
145
|
+
self.apply()
|
|
146
|
+
price = self.get_token_price(tkn1_over_tkn0, block_num)
|
|
147
|
+
return price > self.config.threshold
|
|
148
|
+
|
|
149
|
+
def get_connector(self):
|
|
150
|
+
return self.connector
|
|
151
|
+
|
|
152
|
+
def get_abi(self):
|
|
153
|
+
return self.abi
|
|
154
|
+
|
|
155
|
+
def get_w3(self):
|
|
156
|
+
return self.connector.get_w3()
|
|
157
|
+
|
|
158
|
+
def get_contract_instance(self):
|
|
159
|
+
return self.lp_contract
|
|
160
|
+
|
|
161
|
+
def get_lp_data(self):
|
|
162
|
+
return self.lp_data
|
|
163
|
+
|
|
164
|
+
def _init_lp_contract(self):
|
|
165
|
+
pair_address = self.config.pool_address
|
|
166
|
+
w3 = self.get_w3()
|
|
167
|
+
abi_obj = self.get_abi()
|
|
168
|
+
lp_contract = abi_obj.apply(w3, pair_address)
|
|
169
|
+
return lp_contract
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
# Apache 2.0 License (DeFiPy)
|
|
3
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
# Copyright 2023–2025 Ian Moore
|
|
5
|
+
# Email: defipy.devs@gmail.com
|
|
6
|
+
#
|
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
# you may not use this file except in compliance with the License.
|
|
9
|
+
# You may obtain a copy of the License at
|
|
10
|
+
#
|
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
#
|
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
# See the License for the specific language governing permissions and
|
|
17
|
+
# limitations under the License.
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel
|
|
20
|
+
from web3scout.utils.connect import ConnectW3
|
|
21
|
+
from web3scout.abi.abi_load import ABILoad
|
|
22
|
+
from web3scout.event.process.retrieve_events import RetrieveEvents
|
|
23
|
+
from web3scout.token.fetch.fetch_token import FetchToken
|
|
24
|
+
from .config import TVLExitConfig
|
|
25
|
+
from .data import UniswapPoolData
|
|
26
|
+
from uniswappy import *
|
|
27
|
+
from web3 import Web3
|
|
28
|
+
|
|
29
|
+
class TVLBasedLiquidityExitAgent:
|
|
30
|
+
def __init__(self, config: TVLExitConfig, verbose: bool = False):
|
|
31
|
+
self.config = config
|
|
32
|
+
self.abi = ABILoad(self.config.platform, self.config.abi_name) # Load ABI here
|
|
33
|
+
self.connector = ConnectW3(self.config.provider_url) # Web3Scout setup
|
|
34
|
+
self.connector.apply()
|
|
35
|
+
self.verbose = verbose
|
|
36
|
+
self.lp_contract = None
|
|
37
|
+
self.mock_lp_pos_amt = None
|
|
38
|
+
self.lp_data = None
|
|
39
|
+
self.lp_state = None
|
|
40
|
+
|
|
41
|
+
def init(self):
|
|
42
|
+
self.lp_contract = self._init_lp_contract()
|
|
43
|
+
|
|
44
|
+
reserves = self.lp_contract.functions.getReserves().call()
|
|
45
|
+
token0_address = self.lp_contract.functions.token0().call()
|
|
46
|
+
token1_address = self.lp_contract.functions.token1().call()
|
|
47
|
+
reserve0 = reserves[0]; reserve1 = reserves[1]
|
|
48
|
+
|
|
49
|
+
w3 = self.connector.get_w3()
|
|
50
|
+
FetchERC20 = FetchToken(w3)
|
|
51
|
+
TKN0 = FetchERC20.apply(token0_address)
|
|
52
|
+
TKN1 = FetchERC20.apply(token1_address)
|
|
53
|
+
|
|
54
|
+
self.lp_data = UniswapPoolData(TKN0, TKN1, reserves)
|
|
55
|
+
|
|
56
|
+
def run_batch(self, lp, tkn, user_nm, events: dict):
|
|
57
|
+
"""Process batched Sync events to check TVL and trigger exits."""
|
|
58
|
+
if not events:
|
|
59
|
+
print("No Sync events found in range.")
|
|
60
|
+
return
|
|
61
|
+
for k in events:
|
|
62
|
+
block_num = events[k]['blockNumber']
|
|
63
|
+
self.apply(lp, tkn, user_nm, block_num)
|
|
64
|
+
|
|
65
|
+
def apply(self, lp, tkn, user_nm, block_num):
|
|
66
|
+
"""Execute liquidity exit if condition met."""
|
|
67
|
+
if self.check_condition(lp, tkn, self.config.tvl_threshold, block_num):
|
|
68
|
+
amount_out = self.withdraw_mock_position(lp, tkn, user_nm)
|
|
69
|
+
print(f"Block {block_num}: Withdrawing {amount_out} {tkn.token_name} from {lp.name} LP")
|
|
70
|
+
return amount_out
|
|
71
|
+
else:
|
|
72
|
+
print(f"Block {block_num}: TVL threshold condition met for {lp.name} LP")
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def check_condition(self, lp, tkn, threshold, block_num = None):
|
|
76
|
+
"""Check if TVL is below threshold."""
|
|
77
|
+
block_num = self.get_w3().eth.block_number if block_num == None else block_num
|
|
78
|
+
tvl = self.get_pool_tvl(lp, tkn, block_num)
|
|
79
|
+
return tvl < threshold
|
|
80
|
+
|
|
81
|
+
def get_pool_tvl(self, lp, tkn, block_num):
|
|
82
|
+
"""Calculate TVL from reserves (sum in USD, assuming base_token normalization)."""
|
|
83
|
+
lp = self.update_mock_pool(lp, block_num)
|
|
84
|
+
tot_lp = lp.get_liquidity()
|
|
85
|
+
tvl = LPQuote(False).get_amount_from_lp(lp, tkn, tot_lp)
|
|
86
|
+
return tvl
|
|
87
|
+
|
|
88
|
+
def take_mock_position(self, lp, tkn, user_nm, amt):
|
|
89
|
+
SwapDeposit().apply(lp, tkn, user_nm, amt)
|
|
90
|
+
self.mock_lp_pos_amt = lp.get_last_liquidity_deposit()
|
|
91
|
+
return self.mock_lp_pos_amt
|
|
92
|
+
|
|
93
|
+
def withdraw_mock_position(self, lp, tkn, user_nm, lp_amt = None):
|
|
94
|
+
assert self.mock_lp_pos_amt != None, 'TVLBasedLiquidityExitAgent: MOCK_POSITION_UNAVAILABLE'
|
|
95
|
+
lp_amt = self.mock_lp_pos_amt if lp_amt == None else lp_amt
|
|
96
|
+
tkn_amt = LPQuote(False).get_amount_from_lp(lp, tkn0, lp_amt)
|
|
97
|
+
amount_out = WithdrawSwap().apply(lp, tkn0, user_nm, tkn_amt)
|
|
98
|
+
return amount_out
|
|
99
|
+
|
|
100
|
+
def update_mock_pool(self, lp, cur_block):
|
|
101
|
+
w3 = self.get_w3()
|
|
102
|
+
fetch_tkn = FetchToken(w3)
|
|
103
|
+
|
|
104
|
+
lp_contract = self._init_lp_contract()
|
|
105
|
+
tkn0_addr = lp_contract.functions.token0().call()
|
|
106
|
+
tkn1_addr = lp_contract.functions.token1().call()
|
|
107
|
+
total_supply = lp_contract.functions.totalSupply().call(block_identifier=cur_block)
|
|
108
|
+
reserves = lp_contract.functions.getReserves().call(block_identifier=cur_block)
|
|
109
|
+
|
|
110
|
+
tkn0 = self.get_lp_data().tkn0
|
|
111
|
+
tkn1 = self.get_lp_data().tkn1
|
|
112
|
+
amt0 = fetch_tkn.amt_to_decimal(tkn0, reserves[0])
|
|
113
|
+
amt1 = fetch_tkn.amt_to_decimal(tkn1, reserves[1])
|
|
114
|
+
|
|
115
|
+
lp.reserve0 = lp.convert_to_machine(amt0) # override reserve0
|
|
116
|
+
lp.reserve1 = lp.convert_to_machine(amt1) # override reserve1
|
|
117
|
+
lp.total_supply = total_supply # override total supply
|
|
118
|
+
|
|
119
|
+
return lp
|
|
120
|
+
|
|
121
|
+
def prime_mock_pool(self, start_block, user_nm = None):
|
|
122
|
+
w3 = self.get_w3()
|
|
123
|
+
fetch_tkn = FetchToken(w3)
|
|
124
|
+
|
|
125
|
+
lp_contract = self._init_lp_contract()
|
|
126
|
+
tkn0_addr = lp_contract.functions.token0().call()
|
|
127
|
+
tkn1_addr = lp_contract.functions.token1().call()
|
|
128
|
+
total_supply = lp_contract.functions.totalSupply().call(block_identifier=start_block)
|
|
129
|
+
reserves = lp_contract.functions.getReserves().call(block_identifier=start_block)
|
|
130
|
+
|
|
131
|
+
# Step 2: Define tokens
|
|
132
|
+
tkn0 = fetch_tkn.apply(tkn0_addr)
|
|
133
|
+
tkn1 = fetch_tkn.apply(tkn1_addr)
|
|
134
|
+
|
|
135
|
+
amt0 = fetch_tkn.amt_to_decimal(tkn0, reserves[0])
|
|
136
|
+
amt1 = fetch_tkn.amt_to_decimal(tkn1, reserves[1])
|
|
137
|
+
|
|
138
|
+
# Step 3: Initialize factory
|
|
139
|
+
factory = UniswapFactory("Pool factory", "0x2")
|
|
140
|
+
|
|
141
|
+
# Step 4: Set up exchange data for V2
|
|
142
|
+
exch_data = UniswapExchangeData(tkn0=tkn0, tkn1=tkn1, symbol="LP", address=self.config.pool_address)
|
|
143
|
+
|
|
144
|
+
# Step 5: Deploy pool
|
|
145
|
+
self.lp_state = factory.deploy(exch_data)
|
|
146
|
+
|
|
147
|
+
# Step 6: Add initial liquidity
|
|
148
|
+
join = Join()
|
|
149
|
+
join.apply(self.lp_state, user_nm, amt0, amt1)
|
|
150
|
+
self.lp_state.total_supply = total_supply # override total supply
|
|
151
|
+
|
|
152
|
+
return self.lp_state
|
|
153
|
+
|
|
154
|
+
def get_connector(self):
|
|
155
|
+
return self.connector
|
|
156
|
+
|
|
157
|
+
def get_abi(self):
|
|
158
|
+
return self.abi
|
|
159
|
+
|
|
160
|
+
def get_w3(self):
|
|
161
|
+
return self.connector.get_w3()
|
|
162
|
+
|
|
163
|
+
def get_contract_instance(self):
|
|
164
|
+
return self.lp_contract
|
|
165
|
+
|
|
166
|
+
def get_lp_data(self):
|
|
167
|
+
return self.lp_data
|
|
168
|
+
|
|
169
|
+
def _init_lp_contract(self):
|
|
170
|
+
pair_address = self.config.pool_address
|
|
171
|
+
w3 = self.get_w3()
|
|
172
|
+
abi_obj = self.get_abi()
|
|
173
|
+
lp_contract = abi_obj.apply(w3, pair_address)
|
|
174
|
+
return lp_contract
|