DeFiPy 1.0.8__tar.gz → 1.0.9__tar.gz
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-1.0.8 → defipy-1.0.9}/DeFiPy.egg-info/PKG-INFO +12 -2
- {defipy-1.0.8 → defipy-1.0.9}/DeFiPy.egg-info/SOURCES.txt +8 -6
- {defipy-1.0.8 → defipy-1.0.9}/PKG-INFO +12 -2
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/__init__.py +1 -0
- defipy-1.0.9/python/prod/agents/ImpermanentLossAgent.py +182 -0
- defipy-1.0.9/python/prod/agents/PriceThresholdSwapAgent.py +169 -0
- defipy-1.0.9/python/prod/agents/TVLBasedLiquidityExitAgent.py +174 -0
- defipy-1.0.9/python/prod/agents/VolumeSpikeNotifierAgent.py +190 -0
- defipy-1.0.9/python/prod/agents/__init__.py +4 -0
- defipy-1.0.9/python/prod/agents/config/ImpermanentLossConfig.py +28 -0
- defipy-1.0.9/python/prod/agents/config/PriceThresholdConfig.py +27 -0
- defipy-1.0.9/python/prod/agents/config/TVLExitConfig.py +28 -0
- defipy-1.0.9/python/prod/agents/config/VolumeSpikeConfig.py +27 -0
- defipy-1.0.9/python/prod/agents/config/__init__.py +7 -0
- defipy-1.0.9/python/prod/agents/data/UniswapPoolData.py +26 -0
- defipy-1.0.9/python/prod/agents/data/__init__.py +1 -0
- {defipy-1.0.8 → defipy-1.0.9}/setup.py +2 -1
- defipy-1.0.8/python/prod/agents/PriceThresholdSwapAgent.py +0 -56
- defipy-1.0.8/python/prod/agents/__init__.py +0 -1
- defipy-1.0.8/python/prod/agents/config/PriceThresholdConfig.py +0 -30
- defipy-1.0.8/python/prod/agents/config/__init__.py +0 -1
- {defipy-1.0.8 → defipy-1.0.9}/DeFiPy.egg-info/dependency_links.txt +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/DeFiPy.egg-info/not-zip-safe +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/DeFiPy.egg-info/requires.txt +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/DeFiPy.egg-info/top_level.txt +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/LICENSE +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/NOTICE +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/README.md +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/analytics/risk/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/analytics/simulate/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/erc/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/math/basic/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/math/interest/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/math/interest/ips/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/math/interest/ips/aggregate/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/math/model/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/math/risk/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/process/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/process/burn/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/process/deposit/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/process/join/Join.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/process/join/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/process/liquidity/AddLiquidity.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/process/liquidity/RemoveLiquidity.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/process/liquidity/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/process/mint/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/process/swap/Swap.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/process/swap/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/utils/client/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/utils/client/contract/ExecuteScript.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/utils/client/contract/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/utils/data/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/utils/interfaces/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/utils/tools/UniswapScriptHelper.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/utils/tools/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/python/prod/utils/tools/v3/__init__.py +0 -0
- {defipy-1.0.8 → defipy-1.0.9}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: DeFiPy
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.9
|
|
4
4
|
Summary: Python SDK for DeFi Analytics, Simulation, and Agents
|
|
5
5
|
Home-page: http://github.com/defipy-devs/defipy
|
|
6
6
|
Author: icmoore
|
|
@@ -22,6 +22,16 @@ Requires-Dist: bokeh==3.3.4
|
|
|
22
22
|
Requires-Dist: uniswappy>=1.7.4
|
|
23
23
|
Requires-Dist: stableswappy>=1.0.3
|
|
24
24
|
Requires-Dist: balancerpy>=1.0.4
|
|
25
|
+
Dynamic: author
|
|
26
|
+
Dynamic: author-email
|
|
27
|
+
Dynamic: classifier
|
|
28
|
+
Dynamic: description
|
|
29
|
+
Dynamic: description-content-type
|
|
30
|
+
Dynamic: home-page
|
|
31
|
+
Dynamic: license
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
Dynamic: requires-dist
|
|
34
|
+
Dynamic: summary
|
|
25
35
|
|
|
26
36
|
# DeFiPy: Python SDK for DeFi Analytics and Agents
|
|
27
37
|
|
|
@@ -9,17 +9,19 @@ DeFiPy.egg-info/dependency_links.txt
|
|
|
9
9
|
DeFiPy.egg-info/not-zip-safe
|
|
10
10
|
DeFiPy.egg-info/requires.txt
|
|
11
11
|
DeFiPy.egg-info/top_level.txt
|
|
12
|
-
defipy.egg-info/PKG-INFO
|
|
13
|
-
defipy.egg-info/SOURCES.txt
|
|
14
|
-
defipy.egg-info/dependency_links.txt
|
|
15
|
-
defipy.egg-info/not-zip-safe
|
|
16
|
-
defipy.egg-info/requires.txt
|
|
17
|
-
defipy.egg-info/top_level.txt
|
|
18
12
|
python/prod/__init__.py
|
|
13
|
+
python/prod/agents/ImpermanentLossAgent.py
|
|
19
14
|
python/prod/agents/PriceThresholdSwapAgent.py
|
|
15
|
+
python/prod/agents/TVLBasedLiquidityExitAgent.py
|
|
16
|
+
python/prod/agents/VolumeSpikeNotifierAgent.py
|
|
20
17
|
python/prod/agents/__init__.py
|
|
18
|
+
python/prod/agents/config/ImpermanentLossConfig.py
|
|
21
19
|
python/prod/agents/config/PriceThresholdConfig.py
|
|
20
|
+
python/prod/agents/config/TVLExitConfig.py
|
|
21
|
+
python/prod/agents/config/VolumeSpikeConfig.py
|
|
22
22
|
python/prod/agents/config/__init__.py
|
|
23
|
+
python/prod/agents/data/UniswapPoolData.py
|
|
24
|
+
python/prod/agents/data/__init__.py
|
|
23
25
|
python/prod/analytics/risk/__init__.py
|
|
24
26
|
python/prod/analytics/simulate/__init__.py
|
|
25
27
|
python/prod/erc/__init__.py
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: DeFiPy
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.9
|
|
4
4
|
Summary: Python SDK for DeFi Analytics, Simulation, and Agents
|
|
5
5
|
Home-page: http://github.com/defipy-devs/defipy
|
|
6
6
|
Author: icmoore
|
|
@@ -22,6 +22,16 @@ Requires-Dist: bokeh==3.3.4
|
|
|
22
22
|
Requires-Dist: uniswappy>=1.7.4
|
|
23
23
|
Requires-Dist: stableswappy>=1.0.3
|
|
24
24
|
Requires-Dist: balancerpy>=1.0.4
|
|
25
|
+
Dynamic: author
|
|
26
|
+
Dynamic: author-email
|
|
27
|
+
Dynamic: classifier
|
|
28
|
+
Dynamic: description
|
|
29
|
+
Dynamic: description-content-type
|
|
30
|
+
Dynamic: home-page
|
|
31
|
+
Dynamic: license
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
Dynamic: requires-dist
|
|
34
|
+
Dynamic: summary
|
|
25
35
|
|
|
26
36
|
# DeFiPy: Python SDK for DeFi Analytics and Agents
|
|
27
37
|
|
|
@@ -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
|
|
@@ -0,0 +1,190 @@
|
|
|
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 VolumeSpikeConfig
|
|
25
|
+
from .data import UniswapPoolData
|
|
26
|
+
from uniswappy import *
|
|
27
|
+
from web3 import Web3
|
|
28
|
+
|
|
29
|
+
class VolumeSpikeNotifierAgent:
|
|
30
|
+
def __init__(self, config: VolumeSpikeConfig, 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.pool_volume = None
|
|
37
|
+
self.lp_contract = 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 get_connector(self):
|
|
57
|
+
return self.connector
|
|
58
|
+
|
|
59
|
+
def get_abi(self):
|
|
60
|
+
return self.abi
|
|
61
|
+
|
|
62
|
+
def get_w3(self):
|
|
63
|
+
return self.connector.get_w3()
|
|
64
|
+
|
|
65
|
+
def get_contract_instance(self):
|
|
66
|
+
return self.lp_contract
|
|
67
|
+
|
|
68
|
+
def get_lp_data(self):
|
|
69
|
+
return self.lp_data
|
|
70
|
+
|
|
71
|
+
def prime_mock_pool(self, start_block, user_nm = None):
|
|
72
|
+
w3 = self.get_w3()
|
|
73
|
+
fetch_tkn = FetchToken(w3)
|
|
74
|
+
|
|
75
|
+
lp_contract = self._init_lp_contract()
|
|
76
|
+
tkn0_addr = lp_contract.functions.token0().call()
|
|
77
|
+
tkn1_addr = lp_contract.functions.token1().call()
|
|
78
|
+
total_supply = lp_contract.functions.totalSupply().call(block_identifier=start_block)
|
|
79
|
+
reserves = lp_contract.functions.getReserves().call(block_identifier=start_block)
|
|
80
|
+
|
|
81
|
+
# Step 2: Define tokens
|
|
82
|
+
tkn0 = fetch_tkn.apply(tkn0_addr)
|
|
83
|
+
tkn1 = fetch_tkn.apply(tkn1_addr)
|
|
84
|
+
|
|
85
|
+
amt0 = fetch_tkn.amt_to_decimal(tkn0, reserves[0])
|
|
86
|
+
amt1 = fetch_tkn.amt_to_decimal(tkn1, reserves[1])
|
|
87
|
+
|
|
88
|
+
# Step 3: Initialize factory
|
|
89
|
+
factory = UniswapFactory("Pool factory", "0x2")
|
|
90
|
+
|
|
91
|
+
# Step 4: Set up exchange data for V2
|
|
92
|
+
exch_data = UniswapExchangeData(tkn0=tkn0, tkn1=tkn1, symbol="LP", address=self.config.pool_address)
|
|
93
|
+
|
|
94
|
+
# Step 5: Deploy pool
|
|
95
|
+
self.lp_state = factory.deploy(exch_data)
|
|
96
|
+
|
|
97
|
+
# Step 6: Add initial liquidity
|
|
98
|
+
join = Join()
|
|
99
|
+
join.apply(self.lp_state, user_nm, amt0, amt1)
|
|
100
|
+
self.lp_state.total_supply = total_supply # override total supply
|
|
101
|
+
|
|
102
|
+
return self.lp_state
|
|
103
|
+
|
|
104
|
+
def update_mock_pool(self, lp, cur_block):
|
|
105
|
+
w3 = self.get_w3()
|
|
106
|
+
fetch_tkn = FetchToken(w3)
|
|
107
|
+
|
|
108
|
+
lp_contract = self._init_lp_contract()
|
|
109
|
+
tkn0_addr = lp_contract.functions.token0().call()
|
|
110
|
+
tkn1_addr = lp_contract.functions.token1().call()
|
|
111
|
+
total_supply = lp_contract.functions.totalSupply().call(block_identifier=int(cur_block))
|
|
112
|
+
reserves = lp_contract.functions.getReserves().call(block_identifier=int(cur_block))
|
|
113
|
+
|
|
114
|
+
tkn0 = self.get_lp_data().tkn0
|
|
115
|
+
tkn1 = self.get_lp_data().tkn1
|
|
116
|
+
amt0 = fetch_tkn.amt_to_decimal(tkn0, reserves[0])
|
|
117
|
+
amt1 = fetch_tkn.amt_to_decimal(tkn1, reserves[1])
|
|
118
|
+
|
|
119
|
+
prev_total_supply = lp.total_supply
|
|
120
|
+
lp.reserve0 = lp.convert_to_machine(amt0) # override reserve0
|
|
121
|
+
lp.reserve1 = lp.convert_to_machine(amt1) # override reserve1
|
|
122
|
+
lp.total_supply = total_supply # override total supply
|
|
123
|
+
lp.last_liquidity_deposit = abs(prev_total_supply - lp.total_supply)
|
|
124
|
+
|
|
125
|
+
return lp
|
|
126
|
+
|
|
127
|
+
def run_batch(self, lp, tkn, user_nm, events: dict):
|
|
128
|
+
"""Process batched Sync events to check TVL and trigger exits."""
|
|
129
|
+
if not events:
|
|
130
|
+
print("No Sync events found in range.")
|
|
131
|
+
return
|
|
132
|
+
for k in events:
|
|
133
|
+
block_num = events[k]['blockNumber']
|
|
134
|
+
self.apply(lp, tkn, user_nm, block_num)
|
|
135
|
+
|
|
136
|
+
def apply(self, lp, tkn, user_nm, block_num):
|
|
137
|
+
"""Execute liquidity exit if condition met."""
|
|
138
|
+
if self.check_condition(lp, tkn, self.config.volume_threshold, block_num):
|
|
139
|
+
vol = self.pool_volume
|
|
140
|
+
print(f"Block {block_num}: Volume ({tkn.token_name}) = {vol}, outside threshold {self.config.volume_threshold}")
|
|
141
|
+
return vol
|
|
142
|
+
else:
|
|
143
|
+
print(f"Block {block_num}: Volume threshold condition met for {lp.name} LP")
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
def take_mock_position(self, lp, tkn, user_nm, amt):
|
|
147
|
+
SwapDeposit().apply(lp, tkn, user_nm, amt)
|
|
148
|
+
self.mock_lp_pos_amt = lp.get_last_liquidity_deposit()
|
|
149
|
+
return self.mock_lp_pos_amt
|
|
150
|
+
|
|
151
|
+
def withdraw_mock_position(self, lp, tkn, user_nm, lp_amt = None):
|
|
152
|
+
assert self.mock_lp_pos_amt != None, 'TVLBasedLiquidityExitAgent: MOCK_POSITION_UNAVAILABLE'
|
|
153
|
+
lp_amt = self.mock_lp_pos_amt if lp_amt == None else lp_amt
|
|
154
|
+
tkn_amt = LPQuote(False).get_amount_from_lp(lp, tkn0, lp_amt)
|
|
155
|
+
amount_out = WithdrawSwap().apply(lp, tkn0, user_nm, tkn_amt)
|
|
156
|
+
return amount_out
|
|
157
|
+
|
|
158
|
+
def get_pool_volume(self, lp, tkn, block_num):
|
|
159
|
+
"""Calculate TVL from reserves (sum in USD, assuming base_token normalization)."""
|
|
160
|
+
|
|
161
|
+
tkn0 = self.get_lp_data().tkn0
|
|
162
|
+
tkn1 = self.get_lp_data().tkn1
|
|
163
|
+
prev_tkn0 = lp.get_reserve(tkn0)
|
|
164
|
+
prev_tkn1 = lp.get_reserve(tkn1)
|
|
165
|
+
|
|
166
|
+
lp = self.update_mock_pool(lp, block_num)
|
|
167
|
+
|
|
168
|
+
dtkn0 = abs(lp.get_reserve(tkn0) - prev_tkn0)
|
|
169
|
+
dtkn1 = abs(lp.get_reserve(tkn1) - prev_tkn1)
|
|
170
|
+
|
|
171
|
+
if(tkn.token_name == tkn0.token_name):
|
|
172
|
+
volume = dtkn0 + LPQuote().get_amount(lp, tkn1, dtkn1)
|
|
173
|
+
elif(tkn.token_name == tkn1.token_name):
|
|
174
|
+
volume = dtkn1 + LPQuote().get_amount(lp, tkn0, dtkn0)
|
|
175
|
+
|
|
176
|
+
self.pool_volume = volume
|
|
177
|
+
return volume
|
|
178
|
+
|
|
179
|
+
def check_condition(self, lp, tkn, threshold, block_num = None):
|
|
180
|
+
"""Check if TVL is below threshold."""
|
|
181
|
+
block_num = self.get_w3().eth.block_number if block_num == None else block_num
|
|
182
|
+
volume = self.get_pool_volume(lp, tkn, block_num)
|
|
183
|
+
return volume > threshold
|
|
184
|
+
|
|
185
|
+
def _init_lp_contract(self):
|
|
186
|
+
pair_address = self.config.pool_address
|
|
187
|
+
w3 = self.get_w3()
|
|
188
|
+
abi_obj = self.get_abi()
|
|
189
|
+
lp_contract = abi_obj.apply(w3, pair_address)
|
|
190
|
+
return lp_contract
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
|
|
21
|
+
class ImpermanentLossConfig(BaseModel):
|
|
22
|
+
il_threshold: float # Impermanent loss threshold percentage (e.g., 5.0 for 5%)
|
|
23
|
+
pool_address: str # Uniswap V2 pool address
|
|
24
|
+
provider_url: str # Web3 provider URL (e.g., Infura)
|
|
25
|
+
abi_name: str # e.g., 'UniswapV2Pair' (new field for ABI identifier)
|
|
26
|
+
platform: str # e.g., 'UNI' or 'SUSHI' for the protocoll
|
|
27
|
+
user_position: float # Initial mock position amount for off-chain testing
|
|
28
|
+
exit_percentage: float # Percentage of position to exit upon trigger (e.g., 1.0 for 100%)
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
|
|
21
|
+
class PriceThresholdConfig(BaseModel):
|
|
22
|
+
threshold: float # e.g., 3000.0 (price above which to swap)
|
|
23
|
+
swap_amount: float # e.g., 1.0 (swap amount if threshold met)
|
|
24
|
+
pool_address: str # Uniswap V2 pool
|
|
25
|
+
provider_url: str # e.g., Infura for Web3Scout
|
|
26
|
+
abi_name: str # e.g., 'UniswapV2Pair' (new field for ABI identifier)
|
|
27
|
+
platform: str # e.g., 'UNI' or 'SUSHI' for the protocoll
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
|
|
21
|
+
class TVLExitConfig(BaseModel):
|
|
22
|
+
tvl_threshold: float # e.g., 1000000.0 (minimum TVL in USD)
|
|
23
|
+
exit_percentage: float # e.g., 1.0 (full exit) or 0.5 (half)
|
|
24
|
+
pool_address: str # Pool contract address
|
|
25
|
+
provider_url: str # Web3 provider URL
|
|
26
|
+
abi_name: str # e.g., 'UniswapV2Pair' (new field for ABI identifier)
|
|
27
|
+
platform: str # e.g., 'UNI' or 'SUSHI' for the protocoll
|
|
28
|
+
user_position: float # User's LP shares or amount
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
|
|
21
|
+
class VolumeSpikeConfig(BaseModel):
|
|
22
|
+
volume_threshold: float # Volume threshold for notification (e.g., USD value of trades)
|
|
23
|
+
pool_address: str # Uniswap V2 pool address
|
|
24
|
+
provider_url: str # Web3 provider URL (e.g., Infura)
|
|
25
|
+
abi_name: str # e.g., 'UniswapV2Pair' (new field for ABI identifier)
|
|
26
|
+
platform: str # e.g., 'UNI' or 'SUSHI' for the protocoll
|
|
27
|
+
user_position: float # User's LP shares or amount
|
|
@@ -0,0 +1,26 @@
|
|
|
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 dataclasses import dataclass
|
|
20
|
+
from uniswappy.erc import ERC20
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class UniswapPoolData:
|
|
24
|
+
tkn0: ERC20 = None
|
|
25
|
+
tkn1: ERC20 = None
|
|
26
|
+
reserves: list = None
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .UniswapPoolData import UniswapPoolData
|
|
@@ -4,7 +4,7 @@ with open('README.md') as f:
|
|
|
4
4
|
long_description = f.read()
|
|
5
5
|
|
|
6
6
|
setup(name='DeFiPy',
|
|
7
|
-
version='1.0.
|
|
7
|
+
version='1.0.9',
|
|
8
8
|
description='Python SDK for DeFi Analytics, Simulation, and Agents',
|
|
9
9
|
long_description=long_description,
|
|
10
10
|
long_description_content_type="text/markdown",
|
|
@@ -47,6 +47,7 @@ setup(name='DeFiPy',
|
|
|
47
47
|
'defipy.utils.tools',
|
|
48
48
|
'defipy.utils.tools.v3',
|
|
49
49
|
'defipy.agents.config',
|
|
50
|
+
'defipy.agents.data',
|
|
50
51
|
'defipy.agents',
|
|
51
52
|
],
|
|
52
53
|
install_requires=[
|
|
@@ -1,56 +0,0 @@
|
|
|
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 ..process.swap import Swap
|
|
21
|
-
# from uniswappy.cpt.quote import LPQuote
|
|
22
|
-
# from web3scout.event.process.retrieve_events import RetrieveEvents
|
|
23
|
-
# from web3scout.utils.connect import ConnectW3
|
|
24
|
-
# from .config import PriceThresholdConfig
|
|
25
|
-
|
|
26
|
-
# class PriceThresholdSwapAgent:
|
|
27
|
-
# def __init__(self, config: PriceThresholdConfig):
|
|
28
|
-
# self.config = config
|
|
29
|
-
# self.abi = ABIload(self.config.abi_name, self.config.platform) # Load ABI here
|
|
30
|
-
# self.connector = ConnectW3(self.config.provider_url) # Web3Scout setup
|
|
31
|
-
# self.event_retriever = EventRetriever(self.connector, self.abi)
|
|
32
|
-
|
|
33
|
-
# def get_current_price(self):
|
|
34
|
-
# # Use DeFiPy for price quote (simulated or via reserves)
|
|
35
|
-
# quote = LPQuote() # Or fetch reserves via Web3Scout
|
|
36
|
-
# price = quote.get_price(self.config.token_in, self.config.token_out) # Adjust per docs
|
|
37
|
-
# return price
|
|
38
|
-
|
|
39
|
-
# def check_condition(self):
|
|
40
|
-
# price = self.get_current_price()
|
|
41
|
-
# if price > self.config.threshold:
|
|
42
|
-
# return True
|
|
43
|
-
# return False
|
|
44
|
-
|
|
45
|
-
# def execute_action(self):
|
|
46
|
-
# if self.check_condition():
|
|
47
|
-
# swap = Swap()
|
|
48
|
-
# # Trigger swap; integrate with DeFiPy's Swap.apply()
|
|
49
|
-
# print(f"Swapping {self.config.swap_amount} {self.config.token_in} for {self.config.token_out}")
|
|
50
|
-
# # Add actual swap logic here, e.g., via Web3Scout transaction
|
|
51
|
-
|
|
52
|
-
# def run(self):
|
|
53
|
-
# # Loop or event-driven: Poll every 60s or listen to Sync events
|
|
54
|
-
# events = self.event_retriever.get_events('Sync') # Web3Scout for feeds
|
|
55
|
-
# for event in events:
|
|
56
|
-
# self.execute_action() # Or use a while loop for polling
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# from .PriceThresholdSwapAgent import PriceThresholdSwapAgent
|
|
@@ -1,30 +0,0 @@
|
|
|
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
|
-
|
|
21
|
-
# class PriceThresholdConfig(BaseModel):
|
|
22
|
-
|
|
23
|
-
# token_in: str # e.g., "ETH"
|
|
24
|
-
# token_out: str # e.g., "USDC"
|
|
25
|
-
# threshold: float # e.g., 3000.0 (price above which to swap)
|
|
26
|
-
# swap_amount: float # Amount to swap when triggered
|
|
27
|
-
# pool_address: str # Uniswap pool
|
|
28
|
-
# provider_url: str # e.g., Infura for Web3Scout
|
|
29
|
-
# abi_name: str # e.g., 'UniswapV2Pair' (new field for ABI identifier)
|
|
30
|
-
# platform: str # e.g., 'UNI' or 'SUSHI' for the protocol
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# from .PriceThresholdConfig import PriceThresholdConfig
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|