uni-exec-engine 0.2.0__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.
- uni_exec_engine-0.2.0.dist-info/METADATA +576 -0
- uni_exec_engine-0.2.0.dist-info/RECORD +80 -0
- uni_exec_engine-0.2.0.dist-info/WHEEL +5 -0
- uni_exec_engine-0.2.0.dist-info/licenses/LICENSE +21 -0
- uni_exec_engine-0.2.0.dist-info/top_level.txt +1 -0
- uniswap_autopilot/__init__.py +3 -0
- uniswap_autopilot/analytics/__init__.py +0 -0
- uniswap_autopilot/analytics/il_calculator.py +525 -0
- uniswap_autopilot/analytics/portfolio.py +234 -0
- uniswap_autopilot/analytics/position.py +433 -0
- uniswap_autopilot/analytics/range_suggest.py +270 -0
- uniswap_autopilot/audit.py +194 -0
- uniswap_autopilot/common/__init__.py +0 -0
- uniswap_autopilot/common/approval_cleanup.py +255 -0
- uniswap_autopilot/common/check_balance.py +64 -0
- uniswap_autopilot/common/common.py +804 -0
- uniswap_autopilot/common/deep_link.py +132 -0
- uniswap_autopilot/common/gas.py +141 -0
- uniswap_autopilot/data/auto_trade_policy.example.json +29 -0
- uniswap_autopilot/data/chains.json +135 -0
- uniswap_autopilot/data/common-token-addresses.json +777 -0
- uniswap_autopilot/execute/__init__.py +0 -0
- uniswap_autopilot/execute/_internal/__init__.py +5 -0
- uniswap_autopilot/execute/_internal/constants.py +15 -0
- uniswap_autopilot/execute/_internal/preflight.py +150 -0
- uniswap_autopilot/execute/_internal/pure_signer.py +182 -0
- uniswap_autopilot/execute/_internal/rpc.py +462 -0
- uniswap_autopilot/execute/_internal/signer.py +298 -0
- uniswap_autopilot/execute/_internal/submit.py +73 -0
- uniswap_autopilot/execute/_internal/tx.py +370 -0
- uniswap_autopilot/execute/broadcast.py +380 -0
- uniswap_autopilot/execute/detect.py +52 -0
- uniswap_autopilot/execute/telegram_confirm.py +272 -0
- uniswap_autopilot/lp/compare_pools.py +338 -0
- uniswap_autopilot/lp/v2/__init__.py +0 -0
- uniswap_autopilot/lp/v2/approve.py +100 -0
- uniswap_autopilot/lp/v2/build_tx.py +226 -0
- uniswap_autopilot/lp/v2/flow.py +204 -0
- uniswap_autopilot/lp/v2/pair.py +135 -0
- uniswap_autopilot/lp/v2/positions.py +177 -0
- uniswap_autopilot/lp/v3/__init__.py +0 -0
- uniswap_autopilot/lp/v3/approve.py +109 -0
- uniswap_autopilot/lp/v3/auto_rebalance.py +282 -0
- uniswap_autopilot/lp/v3/build_tx.py +465 -0
- uniswap_autopilot/lp/v3/compound.py +258 -0
- uniswap_autopilot/lp/v3/flow.py +358 -0
- uniswap_autopilot/lp/v3/pool.py +175 -0
- uniswap_autopilot/lp/v3/position.py +112 -0
- uniswap_autopilot/lp/v3/tick.py +75 -0
- uniswap_autopilot/lp/v4/__init__.py +0 -0
- uniswap_autopilot/lp/v4/approve.py +105 -0
- uniswap_autopilot/lp/v4/build_tx.py +669 -0
- uniswap_autopilot/lp/v4/flow.py +368 -0
- uniswap_autopilot/lp/v4/pool.py +174 -0
- uniswap_autopilot/lp/v4/position.py +185 -0
- uniswap_autopilot/policy.py +371 -0
- uniswap_autopilot/price_feed.py +100 -0
- uniswap_autopilot/py.typed +0 -0
- uniswap_autopilot/search/__init__.py +0 -0
- uniswap_autopilot/search/risk.py +200 -0
- uniswap_autopilot/search/search.py +580 -0
- uniswap_autopilot/state_machine.py +315 -0
- uniswap_autopilot/swap/__init__.py +1 -0
- uniswap_autopilot/swap/deep_link.py +69 -0
- uniswap_autopilot/swap/extensions/__init__.py +2 -0
- uniswap_autopilot/swap/extensions/bridge.py +197 -0
- uniswap_autopilot/swap/extensions/limit_order.py +272 -0
- uniswap_autopilot/swap/extensions/slippage.py +123 -0
- uniswap_autopilot/swap/flow.py +693 -0
- uniswap_autopilot/swap/flow_core/__init__.py +2 -0
- uniswap_autopilot/swap/flow_core/artifacts.py +11 -0
- uniswap_autopilot/swap/flow_core/broadcast.py +50 -0
- uniswap_autopilot/swap/flow_core/diagnostics.py +216 -0
- uniswap_autopilot/swap/flow_core/paper.py +113 -0
- uniswap_autopilot/swap/flow_core/policy.py +142 -0
- uniswap_autopilot/swap/links/__init__.py +2 -0
- uniswap_autopilot/swap/links/deep_link.py +69 -0
- uniswap_autopilot/swap/trading_api/permit.py +41 -0
- uniswap_autopilot/swap/trading_api/quote.py +282 -0
- uniswap_autopilot/swap/trading_api/swap.py +248 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from uniswap_autopilot.common.common import (
|
|
12
|
+
decimal_to_base_units,
|
|
13
|
+
dump_json,
|
|
14
|
+
load_local_env,
|
|
15
|
+
normalize_chain,
|
|
16
|
+
override_decimals,
|
|
17
|
+
parse_amount,
|
|
18
|
+
post_json,
|
|
19
|
+
require_api_key,
|
|
20
|
+
resolve_api_token,
|
|
21
|
+
resolve_token,
|
|
22
|
+
resolve_wallet_address,
|
|
23
|
+
)
|
|
24
|
+
from uniswap_autopilot.swap.trading_api.quote import build_quote_payload, summarize_quote
|
|
25
|
+
from uniswap_autopilot.swap.trading_api.swap import (
|
|
26
|
+
build_swap_payload,
|
|
27
|
+
is_uniswapx_route,
|
|
28
|
+
load_quote_payload,
|
|
29
|
+
load_signature_value,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def prepare_limit_order_quote(
|
|
34
|
+
chain_name: str,
|
|
35
|
+
token_in_name: str,
|
|
36
|
+
token_out_name: str,
|
|
37
|
+
amount: str,
|
|
38
|
+
target_price: float | None = None,
|
|
39
|
+
target_output: str | None = None,
|
|
40
|
+
wallet: str | None = None,
|
|
41
|
+
slippage: float = 0.5,
|
|
42
|
+
token_in_decimals: int | None = None,
|
|
43
|
+
token_out_decimals: int | None = None,
|
|
44
|
+
request_only: bool = False,
|
|
45
|
+
) -> dict[str, Any]:
|
|
46
|
+
chain = normalize_chain(chain_name)
|
|
47
|
+
token_in = override_decimals(resolve_token(chain, token_in_name), token_in_decimals)
|
|
48
|
+
token_out = override_decimals(resolve_token(chain, token_out_name), token_out_decimals)
|
|
49
|
+
api_token_in = resolve_api_token(chain, token_in)
|
|
50
|
+
api_token_out = resolve_api_token(chain, token_out)
|
|
51
|
+
validated_wallet = resolve_wallet_address(wallet, "wallet")
|
|
52
|
+
|
|
53
|
+
input_amount = parse_amount(amount)
|
|
54
|
+
|
|
55
|
+
# Compute target output amount in token_out base units
|
|
56
|
+
if target_output is not None:
|
|
57
|
+
output_human = float(parse_amount(target_output))
|
|
58
|
+
elif target_price is not None:
|
|
59
|
+
output_human = float(input_amount) * target_price
|
|
60
|
+
else:
|
|
61
|
+
raise ValueError("either --target-price or --target-output is required")
|
|
62
|
+
|
|
63
|
+
output_base = decimal_to_base_units(parse_amount(str(output_human)), api_token_out["decimals"])
|
|
64
|
+
|
|
65
|
+
payload = build_quote_payload(
|
|
66
|
+
wallet=validated_wallet,
|
|
67
|
+
chain_id=chain.chain_id,
|
|
68
|
+
api_token_in=api_token_in,
|
|
69
|
+
api_token_out=api_token_out,
|
|
70
|
+
base_amount=output_base,
|
|
71
|
+
swap_type="EXACT_OUTPUT",
|
|
72
|
+
slippage=slippage,
|
|
73
|
+
routing_preference="BEST_PRICE",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
response: dict[str, Any] = {
|
|
77
|
+
"action": "limit_order_quote",
|
|
78
|
+
"chain": {"key": chain.key, "chainId": chain.chain_id},
|
|
79
|
+
"tokenIn": token_in,
|
|
80
|
+
"tokenOut": token_out,
|
|
81
|
+
"apiTokenIn": api_token_in,
|
|
82
|
+
"apiTokenOut": api_token_out,
|
|
83
|
+
"wallet": validated_wallet,
|
|
84
|
+
"inputHumanAmount": format(input_amount, "f"),
|
|
85
|
+
"targetOutputHumanAmount": format(output_human, "f"),
|
|
86
|
+
"targetOutputBase": output_base,
|
|
87
|
+
"targetPrice": target_price,
|
|
88
|
+
"slippage": slippage,
|
|
89
|
+
"requestPayload": payload,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if not request_only:
|
|
93
|
+
api_key = require_api_key()
|
|
94
|
+
raw_quote = post_json("quote", payload, api_key)
|
|
95
|
+
response["quoteSummary"] = summarize_quote(raw_quote)
|
|
96
|
+
response["rawQuote"] = raw_quote
|
|
97
|
+
routing = raw_quote.get("routing")
|
|
98
|
+
if routing and not is_uniswapx_route(raw_quote):
|
|
99
|
+
response["warning"] = f"routing is {routing}, not a UniswapX Dutch order; limit order semantics may not apply"
|
|
100
|
+
|
|
101
|
+
return response
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def submit_limit_order(
|
|
105
|
+
quote_file: str,
|
|
106
|
+
signature: str | None = None,
|
|
107
|
+
signature_file: str | None = None,
|
|
108
|
+
simulate: bool = True,
|
|
109
|
+
) -> dict[str, Any]:
|
|
110
|
+
quote_data = load_quote_payload(quote_file)
|
|
111
|
+
raw_quote = quote_data.get("rawQuote") or quote_data
|
|
112
|
+
|
|
113
|
+
if not is_uniswapx_route(raw_quote):
|
|
114
|
+
routing = raw_quote.get("routing", "unknown")
|
|
115
|
+
return {
|
|
116
|
+
"action": "limit_order_submit",
|
|
117
|
+
"status": "rejected",
|
|
118
|
+
"reason": f"quote routing is '{routing}', not a UniswapX Dutch order; cannot submit as limit order",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
effective_signature = load_signature_value(signature=signature, signature_file=signature_file)
|
|
122
|
+
|
|
123
|
+
api_key = require_api_key()
|
|
124
|
+
swap_payload = build_swap_payload(
|
|
125
|
+
raw_quote=raw_quote,
|
|
126
|
+
signature=effective_signature,
|
|
127
|
+
simulate_transaction=simulate,
|
|
128
|
+
refresh_gas_price=True,
|
|
129
|
+
)
|
|
130
|
+
swap_response = post_json("swap", swap_payload, api_key)
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
"action": "limit_order_submit",
|
|
134
|
+
"status": "submitted",
|
|
135
|
+
"routing": raw_quote.get("routing"),
|
|
136
|
+
"simulated": simulate,
|
|
137
|
+
"swapPayload": swap_payload,
|
|
138
|
+
"swapResponse": swap_response,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def check_order_status(
|
|
143
|
+
chain_name: str,
|
|
144
|
+
tx_hash: str | None = None,
|
|
145
|
+
rpc_url: str | None = None,
|
|
146
|
+
) -> dict[str, Any]:
|
|
147
|
+
if not tx_hash:
|
|
148
|
+
raise ValueError("--tx-hash is required")
|
|
149
|
+
|
|
150
|
+
chain = normalize_chain(chain_name)
|
|
151
|
+
from uniswap_autopilot.execute._internal.rpc import execute_cast_receipt, resolve_rpc_url
|
|
152
|
+
|
|
153
|
+
rpc, _ = resolve_rpc_url(rpc_url, chain.chain_id)
|
|
154
|
+
if not rpc:
|
|
155
|
+
raise RuntimeError(f"RPC URL not configured for {chain_name}")
|
|
156
|
+
|
|
157
|
+
receipt = execute_cast_receipt(tx_hash=tx_hash, rpc_url=rpc, confirmations=1)
|
|
158
|
+
status = receipt.get("status")
|
|
159
|
+
succeeded = status == "1" or status == 1
|
|
160
|
+
|
|
161
|
+
# Check for Dutch order events in logs
|
|
162
|
+
dutch_order_detected = False
|
|
163
|
+
logs = receipt.get("logs") or []
|
|
164
|
+
for log in logs:
|
|
165
|
+
topics = log.get("topics") or []
|
|
166
|
+
if topics and len(topics[0]) == 66:
|
|
167
|
+
# DutchOrderFilled event from UniswapX Reactor
|
|
168
|
+
if str(topics[0]).startswith("0x5ab3"):
|
|
169
|
+
dutch_order_detected = True
|
|
170
|
+
break
|
|
171
|
+
|
|
172
|
+
# Try to get block timestamp for expiry check
|
|
173
|
+
block_number_hex = receipt.get("blockNumber")
|
|
174
|
+
current_time = None
|
|
175
|
+
if block_number_hex:
|
|
176
|
+
try:
|
|
177
|
+
from uniswap_autopilot.execute._internal.rpc import _json_rpc
|
|
178
|
+
block = _json_rpc("eth_getBlockByNumber", [block_number_hex, False], rpc)
|
|
179
|
+
ts_hex = block.get("timestamp") if isinstance(block, dict) else None
|
|
180
|
+
if ts_hex:
|
|
181
|
+
current_time = int(ts_hex, 16)
|
|
182
|
+
except Exception:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
"action": "limit_order_status",
|
|
187
|
+
"chain": {"key": chain.key, "chainId": chain.chain_id},
|
|
188
|
+
"txHash": tx_hash,
|
|
189
|
+
"status": "filled" if succeeded else "failed",
|
|
190
|
+
"dutchOrderDetected": dutch_order_detected,
|
|
191
|
+
"receipt": receipt,
|
|
192
|
+
"blockTimestamp": current_time,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def main() -> None:
|
|
197
|
+
parser = argparse.ArgumentParser(description="Uniswap limit orders via UniswapX Dutch auction")
|
|
198
|
+
sub = parser.add_subparsers(dest="command")
|
|
199
|
+
|
|
200
|
+
q = sub.add_parser("quote", help="Get a limit order quote (EXACT_OUTPUT via UniswapX)")
|
|
201
|
+
q.add_argument("--chain", required=True)
|
|
202
|
+
q.add_argument("--token-in", required=True)
|
|
203
|
+
q.add_argument("--token-out", required=True)
|
|
204
|
+
q.add_argument("--amount", required=True, help="Input amount")
|
|
205
|
+
q.add_argument("--target-price", type=float, help="Target price (token_out per token_in)")
|
|
206
|
+
q.add_argument("--target-output", help="Exact output amount (alternative to --target-price)")
|
|
207
|
+
q.add_argument("--wallet")
|
|
208
|
+
q.add_argument("--slippage", type=float, default=0.5)
|
|
209
|
+
q.add_argument("--token-in-decimals", type=int)
|
|
210
|
+
q.add_argument("--token-out-decimals", type=int)
|
|
211
|
+
q.add_argument("--request-only", action="store_true")
|
|
212
|
+
q.add_argument("--output")
|
|
213
|
+
|
|
214
|
+
s = sub.add_parser("submit", help="Submit a signed limit order")
|
|
215
|
+
s.add_argument("--quote-file", required=True)
|
|
216
|
+
s.add_argument("--signature")
|
|
217
|
+
s.add_argument("--signature-file")
|
|
218
|
+
s.add_argument("--no-simulate", action="store_true")
|
|
219
|
+
s.add_argument("--output")
|
|
220
|
+
|
|
221
|
+
c = sub.add_parser("status", help="Check order fill status by tx hash")
|
|
222
|
+
c.add_argument("--chain", required=True)
|
|
223
|
+
c.add_argument("--tx-hash", required=True)
|
|
224
|
+
c.add_argument("--rpc-url")
|
|
225
|
+
c.add_argument("--output")
|
|
226
|
+
|
|
227
|
+
args = parser.parse_args()
|
|
228
|
+
load_local_env()
|
|
229
|
+
|
|
230
|
+
if args.command == "quote":
|
|
231
|
+
result = prepare_limit_order_quote(
|
|
232
|
+
chain_name=args.chain,
|
|
233
|
+
token_in_name=args.token_in,
|
|
234
|
+
token_out_name=args.token_out,
|
|
235
|
+
amount=args.amount,
|
|
236
|
+
target_price=args.target_price,
|
|
237
|
+
target_output=args.target_output,
|
|
238
|
+
wallet=args.wallet,
|
|
239
|
+
slippage=args.slippage,
|
|
240
|
+
token_in_decimals=args.token_in_decimals,
|
|
241
|
+
token_out_decimals=args.token_out_decimals,
|
|
242
|
+
request_only=args.request_only,
|
|
243
|
+
)
|
|
244
|
+
if "warning" in result:
|
|
245
|
+
print(f"WARNING: {result['warning']}", file=sys.stderr)
|
|
246
|
+
print(f"Limit order quote: {args.amount} {args.token_in} -> {result['targetOutputHumanAmount']} {args.token_out}")
|
|
247
|
+
if args.output:
|
|
248
|
+
Path(args.output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
249
|
+
dump_json(result)
|
|
250
|
+
elif args.command == "submit":
|
|
251
|
+
result = submit_limit_order(
|
|
252
|
+
quote_file=args.quote_file,
|
|
253
|
+
signature=args.signature,
|
|
254
|
+
signature_file=args.signature_file,
|
|
255
|
+
simulate=not args.no_simulate,
|
|
256
|
+
)
|
|
257
|
+
print(f"Limit order status: {result.get('status', 'unknown')}")
|
|
258
|
+
if args.output:
|
|
259
|
+
Path(args.output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
260
|
+
dump_json(result)
|
|
261
|
+
elif args.command == "status":
|
|
262
|
+
result = check_order_status(args.chain, args.tx_hash, args.rpc_url)
|
|
263
|
+
print(f"Order status: {result['status']}")
|
|
264
|
+
if args.output:
|
|
265
|
+
Path(args.output).write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
266
|
+
dump_json(result)
|
|
267
|
+
else:
|
|
268
|
+
parser.print_help()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
if __name__ == "__main__":
|
|
272
|
+
main()
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from uniswap_autopilot.common.common import dump_json, load_local_env, normalize_chain, resolve_token
|
|
9
|
+
from uniswap_autopilot.search.search import _ds_lookup
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
MAJOR_CATEGORIES = {"wrapped-native", "wrapped-btc", "wrapped-eth"}
|
|
13
|
+
MAJOR_SYMBOLS = {"ETH", "WETH", "BTC", "WBTC", "CBETH", "CBBTC"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _token_is_stable(token: dict[str, Any]) -> bool:
|
|
17
|
+
return bool(token.get("isStable")) or token.get("category") == "stablecoin"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _token_is_major(token: dict[str, Any]) -> bool:
|
|
21
|
+
return (
|
|
22
|
+
token.get("category") in MAJOR_CATEGORIES
|
|
23
|
+
or token.get("symbol", "").upper() in MAJOR_SYMBOLS
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_liquidity(chain_key: str, token_address: str) -> float:
|
|
28
|
+
if token_address == "NATIVE":
|
|
29
|
+
return float("inf")
|
|
30
|
+
ds = _ds_lookup(chain_key, token_address)
|
|
31
|
+
if ds:
|
|
32
|
+
return float(ds.get("liquidityUsd") or 0)
|
|
33
|
+
return 0.0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def suggest_slippage(
|
|
37
|
+
chain_name: str,
|
|
38
|
+
token_in_name: str,
|
|
39
|
+
token_out_name: str,
|
|
40
|
+
amount_usd: float | None = None,
|
|
41
|
+
) -> dict[str, Any]:
|
|
42
|
+
chain = normalize_chain(chain_name)
|
|
43
|
+
tok_in = resolve_token(chain, token_in_name)
|
|
44
|
+
tok_out = resolve_token(chain, token_out_name)
|
|
45
|
+
|
|
46
|
+
in_stable = _token_is_stable(tok_in)
|
|
47
|
+
out_stable = _token_is_stable(tok_out)
|
|
48
|
+
in_major = _token_is_major(tok_in)
|
|
49
|
+
out_major = _token_is_major(tok_out)
|
|
50
|
+
|
|
51
|
+
liq_in = _get_liquidity(chain.key, tok_in["address"])
|
|
52
|
+
liq_out = _get_liquidity(chain.key, tok_out["address"])
|
|
53
|
+
pool_liq = min(liq_in, liq_out) if liq_in and liq_out else max(liq_in, liq_out)
|
|
54
|
+
|
|
55
|
+
if in_stable and out_stable:
|
|
56
|
+
category = "stable_stable"
|
|
57
|
+
base = 0.1
|
|
58
|
+
elif (in_stable or out_stable) and (in_major or out_major):
|
|
59
|
+
category = "stable_major"
|
|
60
|
+
base = 0.5
|
|
61
|
+
elif in_stable or out_stable:
|
|
62
|
+
if pool_liq >= 1_000_000:
|
|
63
|
+
category = "stable_midcap"
|
|
64
|
+
base = 1.0
|
|
65
|
+
elif pool_liq >= 100_000:
|
|
66
|
+
category = "stable_smallcap"
|
|
67
|
+
base = 2.0
|
|
68
|
+
else:
|
|
69
|
+
category = "stable_microcap"
|
|
70
|
+
base = 3.0
|
|
71
|
+
else:
|
|
72
|
+
if pool_liq >= 1_000_000:
|
|
73
|
+
category = "volatile_midcap"
|
|
74
|
+
base = 1.5
|
|
75
|
+
elif pool_liq >= 100_000:
|
|
76
|
+
category = "volatile_smallcap"
|
|
77
|
+
base = 3.0
|
|
78
|
+
else:
|
|
79
|
+
category = "volatile_microcap"
|
|
80
|
+
base = 5.0
|
|
81
|
+
|
|
82
|
+
dynamic_scale = 1.0
|
|
83
|
+
if amount_usd and pool_liq > 0:
|
|
84
|
+
trade_pct = amount_usd / pool_liq
|
|
85
|
+
if trade_pct > 0.02:
|
|
86
|
+
dynamic_scale = trade_pct / 0.02
|
|
87
|
+
elif amount_usd and pool_liq == 0:
|
|
88
|
+
dynamic_scale = 2.0
|
|
89
|
+
final = min(base * dynamic_scale, 10.0)
|
|
90
|
+
final = round(final, 2)
|
|
91
|
+
|
|
92
|
+
reasoning = f"{category}: base={base}%"
|
|
93
|
+
if dynamic_scale > 1.0:
|
|
94
|
+
reasoning += f", dynamic_scale={dynamic_scale:.2f}x (trade=${amount_usd:.0f} vs pool_liq=${pool_liq:,.0f})"
|
|
95
|
+
if pool_liq == 0:
|
|
96
|
+
reasoning += ", pool_liq=0 (no DexScreener data, using base)"
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
"recommendedSlippage": final,
|
|
100
|
+
"category": category,
|
|
101
|
+
"baseSlippage": base,
|
|
102
|
+
"dynamicScale": round(dynamic_scale, 3),
|
|
103
|
+
"poolLiquidityUsd": pool_liq,
|
|
104
|
+
"reasoning": reasoning,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def main() -> None:
|
|
109
|
+
parser = argparse.ArgumentParser(description="Suggest slippage tolerance based on token pair")
|
|
110
|
+
parser.add_argument("--chain", required=True)
|
|
111
|
+
parser.add_argument("--token-in", required=True)
|
|
112
|
+
parser.add_argument("--token-out", required=True)
|
|
113
|
+
parser.add_argument("--amount-usd", type=float, help="Trade amount in USD for dynamic scaling")
|
|
114
|
+
args = parser.parse_args()
|
|
115
|
+
|
|
116
|
+
load_local_env()
|
|
117
|
+
result = suggest_slippage(args.chain, args.token_in, args.token_out, args.amount_usd)
|
|
118
|
+
print(f"Recommended: {result['recommendedSlippage']}% ({result['category']})")
|
|
119
|
+
dump_json(result)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
main()
|