-
Notifications
You must be signed in to change notification settings - Fork 154
Expand file tree
/
Copy pathsolver.py
More file actions
127 lines (113 loc) · 6.05 KB
/
solver.py
File metadata and controls
127 lines (113 loc) · 6.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
import os
import yaml
import time
import logging
from datetime import datetime
from web3 import Web3
from scipy.optimize import minimize_scalar
POOL_MANAGER_ABI = [
{"inputs": [{"internalType": "PoolId", "name": "id", "type": "bytes32"}], "name": "getLiquidity", "outputs": [{"internalType": "uint128", "name": "
8000
;liquidity", "type": "uint128"}], "stateMutability": "view", "type": "function"},
{"inputs": [{"internalType": "PoolId", "name": "id", "type": "bytes32"}], "name": "getSlot0", "outputs": [{"internalType": "uint160", "name": "sqrtPriceX96", "type": "uint160"}, {"internalType": "int24", "name": "tick", "type": "int24"}, {"internalType": "uint16", "name": "protocolFee", "type": "uint16"}, {"internalType": "uint24", "name": "lpFee", "type": "uint24"}], "stateMutability": "view", "type": "function"}
]
HOOK_ABI = [
{"inputs": [{"components": [{"internalType": "uint128", "name": "thresholdRatioBps", "type": "uint128"}, {"internalType": "uint24", "name": "feePips", "type": "uint24"}], "internalType": "struct JITDefenseHook.FeeTier[]", "name": "_newTiers", "type": "tuple[]"}], "name": "setFeeTiers", "outputs": [], "stateMutability": "nonpayable", "type": "function"}
]
class ProductionHJISolver:
def __init__(self, config_path):
with open(config_path, 'r') as f:
self.cfg = yaml.safe_load(f)
# logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
self.log = logging.getLogger("ProductionHJISolver")
# Web3 setup
self.w3 = Web3(Web3.HTTPProvider(self.cfg['network']['rpc_url']))
self.pm = self.w3.eth.contract(address=self.cfg['contracts']['pool_manager'], abi=POOL_MANAGER_ABI)
self.hook = self.w3.eth.contract(address=self.cfg['contracts']['hook_address'], abi=HOOK_ABI)
self.pool_id = self.cfg['contracts']['pool_id']
# Private key must be provided via environment variable for safety
priv_env = self.cfg.get('network', {}).get('private_key_env', 'GOV_PRIVATE_KEY')
priv = os.environ.get(priv_env)
if not priv:
raise RuntimeError(f"Private key environment variable '{priv_env}' not set")
self.acct = self.w3.eth.account.from_key(priv)
# client runtime parameters
self.poll_interval = float(self.cfg.get('runtime', {}).get('poll_interval_seconds', 60))
self.rate_limit_seconds = float(self.cfg.get('runtime', {}).get('min_seconds_between_txs', 30))
self.max_retries = int(self.cfg.get('runtime', {}).get('max_tx_retries', 5))
self.last_tx_time = 0.0
def get_realtime_state(self):
liquidity = self.pm.functions.getLiquidity(self.pool_id).call()
slot0 = self.pm.functions.getSlot0(self.pool_id).call()
gas_price = self.w3.eth.gas_price
return liquidity, slot0[1], gas_price
def solve_phi_crit(self, ratio_bps, L_active, gas_price, v_swap):
"""求解 HJI 临界点"""
alpha = (L_active * ratio_bps) / 10000
c_gas_eth = (gas_price * self.cfg['market_assumptions']['jit_gas_usage']) / 1e18
def max_profit_for_phi(phi):
res = minimize_scalar(
lambda a: -(phi * (a / (L_active + a)) * v_swap - (c_gas_eth + 0.5 * self.cfg['market_assumptions']['kappa'] * a**2)),
bounds=(alpha * 0.8, alpha * 1.2),
method='bounded'
)
return -res.fun
low, high = 0.0005, 0.1 # 5bps to 10000bps
for _ in range(20):
mid = (low + high) / 2
if max_profit_for_phi(mid) > 0: low = mid
else: high = mid
return high
def sync_to_chain(self):
L, tick, gp = self.get_realtime_state()
if L == 0: return
tiers_payload = []
for r in self.cfg['strategy']['ratio_tiers']:
phi = self.solve_phi_crit(r, L, gp, self.cfg['market_assumptions']['v_swap_nominal'])
tiers_payload.append({
"thresholdRatioBps": r,
"feePips": int(phi * 1_000_000)
})
# Rate limiting: ensure minimum interval between on-chain updates
now = time.time()
since_last = now - self.last_tx_time
if since_last < self.rate_limit_seconds:
self.log.info("Skipping on-chain update due to rate limit (%.1fs remaining)", self.rate_limit_seconds - since_last)
return
tx = self.hook.functions.setFeeTiers(tiers_payload).build_transaction({
'from': self.acct.address,
'nonce': self.w3.eth.get_transaction_count(self.acct.address),
'gas': 500000,
'maxFeePerGas': int(gp * 1.2),
'maxPriorityFeePerGas': self.w3.eth.max_priority_fee
})
# Exponential backoff retry loop for sending transaction
attempt = 0
while attempt < self.max_retries:
try:
signed_tx = self.w3.eth.account.sign_transaction(tx, self.acct.key)
raw = signed_tx.rawTransaction
tx_hash = self.w3.eth.send_raw_transaction(raw)
self.last_tx_time = time.time()
self.log.info("Strategy updated. L_active: %s, Tick: %s, Tx: %s", L, tick, tx_hash.hex())
break
except Exception as e:
attempt += 1
backoff = min(60, (2 ** attempt))
# log full context for debugging
self.log.error("Tx attempt %d failed: %s; backoff %ds; payload snapshot: %s", attempt, str(e), backoff, {
'L_active': L,
'tick': tick,
'tiers_count': len(tiers_payload),
})
if attempt >= self.max_retries:
self.log.exception("Max retries reached, aborting tx")
raise
time.sleep(backoff)
if __name__ == "__main__":
solver = ProductionHJISolver("config.yaml")
while True:
try:
solver.sync_to_chain()
except Exception as e:
solver.log.exception("Error in sync loop: %s", str(e))
time.sleep(solver.poll_interval)