wayfinder-paths 0.1.23__py3-none-any.whl → 0.1.24__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.
Potentially problematic release.
This version of wayfinder-paths might be problematic. Click here for more details.
- wayfinder_paths/adapters/balance_adapter/adapter.py +250 -0
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -11
- wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
- wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
- wayfinder_paths/adapters/boros_adapter/client.py +476 -0
- wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
- wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
- wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
- wayfinder_paths/adapters/boros_adapter/types.py +70 -0
- wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
- wayfinder_paths/adapters/brap_adapter/adapter.py +1 -1
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +161 -26
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +77 -13
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +585 -61
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +592 -400
- wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +126 -219
- wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
- wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
- wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
- wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
- wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
- wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
- wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
- wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
- wayfinder_paths/adapters/token_adapter/examples.json +0 -4
- wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
- wayfinder_paths/conftest.py +24 -17
- wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
- wayfinder_paths/core/adapters/models.py +17 -7
- wayfinder_paths/core/clients/BRAPClient.py +1 -1
- wayfinder_paths/core/clients/TokenClient.py +47 -1
- wayfinder_paths/core/clients/WayfinderClient.py +1 -2
- wayfinder_paths/core/clients/protocols.py +21 -22
- wayfinder_paths/core/clients/test_ledger_client.py +448 -0
- wayfinder_paths/core/config.py +12 -0
- wayfinder_paths/core/constants/__init__.py +15 -0
- wayfinder_paths/core/constants/base.py +6 -1
- wayfinder_paths/core/constants/contracts.py +39 -26
- wayfinder_paths/core/constants/erc20_abi.py +0 -1
- wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
- wayfinder_paths/core/constants/hyperliquid.py +16 -0
- wayfinder_paths/core/constants/moonwell_abi.py +0 -15
- wayfinder_paths/core/engine/manifest.py +66 -0
- wayfinder_paths/core/strategies/Strategy.py +0 -61
- wayfinder_paths/core/strategies/__init__.py +10 -1
- wayfinder_paths/core/strategies/opa_loop.py +167 -0
- wayfinder_paths/core/utils/test_transaction.py +289 -0
- wayfinder_paths/core/utils/transaction.py +44 -1
- wayfinder_paths/core/utils/web3.py +3 -0
- wayfinder_paths/mcp/__init__.py +5 -0
- wayfinder_paths/mcp/preview.py +185 -0
- wayfinder_paths/mcp/scripting.py +84 -0
- wayfinder_paths/mcp/server.py +52 -0
- wayfinder_paths/mcp/state/profile_store.py +195 -0
- wayfinder_paths/mcp/state/store.py +89 -0
- wayfinder_paths/mcp/test_scripting.py +267 -0
- wayfinder_paths/mcp/tools/__init__.py +0 -0
- wayfinder_paths/mcp/tools/balances.py +290 -0
- wayfinder_paths/mcp/tools/discovery.py +158 -0
- wayfinder_paths/mcp/tools/execute.py +770 -0
- wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
- wayfinder_paths/mcp/tools/quotes.py +288 -0
- wayfinder_paths/mcp/tools/run_script.py +286 -0
- wayfinder_paths/mcp/tools/strategies.py +188 -0
- wayfinder_paths/mcp/tools/tokens.py +46 -0
- wayfinder_paths/mcp/tools/wallets.py +354 -0
- wayfinder_paths/mcp/utils.py +129 -0
- wayfinder_paths/policies/hyperliquid.py +1 -1
- wayfinder_paths/policies/lifi.py +18 -0
- wayfinder_paths/policies/util.py +8 -2
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +28 -119
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
- wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
- wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
- wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
- wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
- wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
- wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
- wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
- wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
- wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
- wayfinder_paths/strategies/boros_hype_strategy/test_strategy.py +202 -0
- wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
- wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +3 -12
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +7 -29
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +63 -40
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +0 -34
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +11 -34
- wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
- wayfinder_paths/tests/test_test_coverage.py +1 -4
- wayfinder_paths-0.1.24.dist-info/METADATA +378 -0
- wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
- {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
- wayfinder_paths/scripts/create_strategy.py +0 -139
- wayfinder_paths/scripts/make_wallets.py +0 -142
- wayfinder_paths-0.1.23.dist-info/METADATA +0 -354
- wayfinder_paths-0.1.23.dist-info/RECORD +0 -120
- /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
- {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.24.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Boros HYPE snapshot helpers.
|
|
3
|
+
|
|
4
|
+
Kept as a mixin so the main strategy file stays readable without changing behavior.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import aiohttp
|
|
13
|
+
from loguru import logger
|
|
14
|
+
|
|
15
|
+
from wayfinder_paths.core.utils.web3 import web3_from_chain_id
|
|
16
|
+
|
|
17
|
+
from .constants import (
|
|
18
|
+
BOROS_HYPE_TOKEN_ID,
|
|
19
|
+
ETH_ARB,
|
|
20
|
+
HYPE_NATIVE,
|
|
21
|
+
HYPE_OFT_ADDRESS,
|
|
22
|
+
HYPEREVM_CHAIN_ID,
|
|
23
|
+
KHYPE_API_URL,
|
|
24
|
+
KHYPE_LST,
|
|
25
|
+
KHYPE_STAKING_ACCOUNTANT,
|
|
26
|
+
KHYPE_STAKING_ACCOUNTANT_ABI,
|
|
27
|
+
LHYPE_ACCOUNTANT,
|
|
28
|
+
LHYPE_ACCOUNTANT_ABI,
|
|
29
|
+
LHYPE_API_URL,
|
|
30
|
+
LOOPED_HYPE,
|
|
31
|
+
MIN_HYPE_GAS,
|
|
32
|
+
USDC_ARB,
|
|
33
|
+
USDT_ARB,
|
|
34
|
+
WHYPE,
|
|
35
|
+
WHYPE_ADDRESS,
|
|
36
|
+
)
|
|
37
|
+
from .types import Inventory
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def fetch_lhype_apy() -> float | None:
|
|
41
|
+
try:
|
|
42
|
+
async with aiohttp.ClientSession() as session:
|
|
43
|
+
async with session.get(LHYPE_API_URL, timeout=10) as resp:
|
|
44
|
+
if resp.status == 200:
|
|
45
|
+
data = await resp.json()
|
|
46
|
+
if data.get("success") and data.get("result"):
|
|
47
|
+
reward_rate = data["result"].get("reward_rate")
|
|
48
|
+
if reward_rate is not None:
|
|
49
|
+
return float(reward_rate) / 100.0
|
|
50
|
+
except Exception as e:
|
|
51
|
+
logger.warning(f"Failed to fetch lHYPE APY: {e}")
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
async def fetch_khype_apy() -> float | None:
|
|
56
|
+
try:
|
|
57
|
+
async with aiohttp.ClientSession() as session:
|
|
58
|
+
async with session.get(KHYPE_API_URL, timeout=10) as resp:
|
|
59
|
+
if resp.status == 200:
|
|
60
|
+
data = await resp.json()
|
|
61
|
+
apy_14d = data.get("apy_14d")
|
|
62
|
+
if apy_14d is not None:
|
|
63
|
+
return float(apy_14d)
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.warning(f"Failed to fetch kHYPE APY: {e}")
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class BorosHypeSnapshotMixin:
|
|
70
|
+
async def observe(self) -> Inventory:
|
|
71
|
+
self._planner_runtime.reset_virtual_ledger()
|
|
72
|
+
|
|
73
|
+
strategy_wallet = self._config.get("strategy_wallet", {})
|
|
74
|
+
user_address = strategy_wallet.get("address") if strategy_wallet else None
|
|
75
|
+
|
|
76
|
+
# Default values
|
|
77
|
+
hype_price_usd = 25.0
|
|
78
|
+
hl_perp_margin = 0.0
|
|
79
|
+
hl_spot_usdc = 0.0
|
|
80
|
+
hl_spot_hype = 0.0
|
|
81
|
+
hl_short_size_hype = 0.0
|
|
82
|
+
hl_unrealized_pnl = 0.0
|
|
83
|
+
hl_withdrawable_usd = 0.0
|
|
84
|
+
mid_prices: dict[str, float] = {}
|
|
85
|
+
perp_position: dict[str, Any] | None = None
|
|
86
|
+
|
|
87
|
+
if self.hyperliquid_adapter and user_address:
|
|
88
|
+
try:
|
|
89
|
+
success, prices = await self.hyperliquid_adapter.get_all_mid_prices()
|
|
90
|
+
if success and isinstance(prices, dict):
|
|
91
|
+
mid_prices = prices
|
|
92
|
+
hype_price_usd = prices.get("HYPE", 25.0)
|
|
93
|
+
|
|
94
|
+
success, user_state = await self.hyperliquid_adapter.get_user_state(
|
|
95
|
+
user_address
|
|
96
|
+
)
|
|
97
|
+
if success and isinstance(user_state, dict):
|
|
98
|
+
hl_perp_margin = self.hyperliquid_adapter.get_perp_margin_amount(
|
|
99
|
+
user_state
|
|
100
|
+
)
|
|
101
|
+
hl_withdrawable_usd = float(
|
|
102
|
+
user_state.get("withdrawable", 0)
|
|
103
|
+
or user_state.get("marginSummary", {}).get("totalRawUsd", 0)
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
positions = user_state.get("assetPositions", [])
|
|
107
|
+
for pos in positions:
|
|
108
|
+
pos_info = pos.get("position", {})
|
|
109
|
+
if pos_info.get("coin") == "HYPE":
|
|
110
|
+
perp_position = pos_info
|
|
111
|
+
szi = float(pos_info.get("szi", 0))
|
|
112
|
+
# Negative szi = short position
|
|
113
|
+
if szi < 0:
|
|
114
|
+
hl_short_size_hype = abs(szi)
|
|
115
|
+
hl_unrealized_pnl = float(pos_info.get("unrealizedPnl", 0))
|
|
116
|
+
break
|
|
117
|
+
|
|
118
|
+
(
|
|
119
|
+
success,
|
|
120
|
+
spot_state,
|
|
121
|
+
) = await self.hyperliquid_adapter.get_spot_user_state(user_address)
|
|
122
|
+
if success and isinstance(spot_state, dict):
|
|
123
|
+
balances = spot_state.get("balances", [])
|
|
124
|
+
for bal in balances:
|
|
125
|
+
token = bal.get("coin") or bal.get("token")
|
|
126
|
+
hold = float(bal.get("hold", 0))
|
|
127
|
+
total = float(bal.get("total", 0))
|
|
128
|
+
available = total - hold
|
|
129
|
+
if token == "USDC":
|
|
130
|
+
hl_spot_usdc = available
|
|
131
|
+
elif token == "HYPE":
|
|
132
|
+
hl_spot_hype = available
|
|
133
|
+
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logger.warning(f"Failed to get Hyperliquid state: {e}")
|
|
136
|
+
|
|
137
|
+
boros_idle_collateral_cross = 0.0
|
|
138
|
+
boros_idle_collateral_isolated = 0.0
|
|
139
|
+
boros_collateral_hype = 0.0
|
|
140
|
+
boros_collateral_usd = 0.0
|
|
141
|
+
boros_pending_withdrawal_hype = 0.0
|
|
142
|
+
boros_pending_withdrawal_usd = 0.0
|
|
143
|
+
boros_position_size = 0.0
|
|
144
|
+
boros_position_value = 0.0
|
|
145
|
+
boros_position_market_ids: set[int] = set()
|
|
146
|
+
|
|
147
|
+
if self.boros_adapter:
|
|
148
|
+
try:
|
|
149
|
+
token_id = (
|
|
150
|
+
self._planner_runtime.current_boros_token_id or BOROS_HYPE_TOKEN_ID
|
|
151
|
+
)
|
|
152
|
+
success, balances = await self.boros_adapter.get_account_balances(
|
|
153
|
+
token_id=int(token_id)
|
|
154
|
+
)
|
|
155
|
+
if success and isinstance(balances, dict):
|
|
156
|
+
# Balances are returned in Boros cash units; for the HYPE-collateralized
|
|
157
|
+
# market these correspond to HYPE (18 decimals).
|
|
158
|
+
boros_collateral_hype = float(balances.get("total", 0))
|
|
159
|
+
boros_idle_collateral_cross = float(balances.get("cross", 0))
|
|
160
|
+
boros_idle_collateral_isolated = float(balances.get("isolated", 0))
|
|
161
|
+
|
|
162
|
+
(
|
|
163
|
+
ok_pending,
|
|
164
|
+
pending_hype,
|
|
165
|
+
) = await self.boros_adapter.get_pending_withdrawal_amount(
|
|
166
|
+
token_id=int(token_id), token_decimals=18
|
|
167
|
+
)
|
|
168
|
+
if ok_pending:
|
|
169
|
+
boros_pending_withdrawal_hype = float(pending_hype)
|
|
170
|
+
|
|
171
|
+
success, positions = await self.boros_adapter.get_active_positions()
|
|
172
|
+
if success and isinstance(positions, list):
|
|
173
|
+
for pos in positions:
|
|
174
|
+
size = float(pos.get("size") or pos.get("notional", 0))
|
|
175
|
+
boros_position_size += abs(size)
|
|
176
|
+
boros_position_value += abs(size)
|
|
177
|
+
mid = pos.get("marketId") or pos.get("market_id")
|
|
178
|
+
try:
|
|
179
|
+
mid_int = int(mid) if mid is not None else None
|
|
180
|
+
except (TypeError, ValueError):
|
|
181
|
+
mid_int = None
|
|
182
|
+
if mid_int and mid_int > 0:
|
|
183
|
+
boros_position_market_ids.add(mid_int)
|
|
184
|
+
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.warning(f"Failed to get Boros state: {e}")
|
|
187
|
+
|
|
188
|
+
hype_hyperevm_balance = 0.0
|
|
189
|
+
whype_balance = 0.0
|
|
190
|
+
khype_balance = 0.0
|
|
191
|
+
looped_hype_balance = 0.0
|
|
192
|
+
usdc_arb_idle = 0.0
|
|
193
|
+
usdt_arb_idle = 0.0
|
|
194
|
+
eth_arb_balance = 0.0
|
|
195
|
+
hype_oft_arb_balance = 0.0
|
|
196
|
+
|
|
197
|
+
if self.balance_adapter:
|
|
198
|
+
try:
|
|
199
|
+
assets = [
|
|
200
|
+
{"token_id": HYPE_NATIVE}, # 0: HyperEVM native HYPE
|
|
201
|
+
{"token_id": WHYPE}, # 1: Wrapped HYPE
|
|
202
|
+
{"token_id": KHYPE_LST}, # 2: kHYPE
|
|
203
|
+
{"token_id": LOOPED_HYPE}, # 3: lHYPE
|
|
204
|
+
{"token_id": USDC_ARB}, # 4: Arbitrum USDC
|
|
205
|
+
{"token_id": USDT_ARB}, # 5: Arbitrum USDT
|
|
206
|
+
{"token_id": ETH_ARB}, # 6: Arbitrum ETH
|
|
207
|
+
{
|
|
208
|
+
"token_address": HYPE_OFT_ADDRESS,
|
|
209
|
+
"chain_id": 42161,
|
|
210
|
+
}, # 7: Arbitrum OFT HYPE
|
|
211
|
+
]
|
|
212
|
+
ok, results = await self.balance_adapter.get_wallet_balances_multicall(
|
|
213
|
+
assets=assets
|
|
214
|
+
)
|
|
215
|
+
if ok and isinstance(results, list):
|
|
216
|
+
if results[0].get("success"):
|
|
217
|
+
hype_hyperevm_balance = results[0].get("balance_decimal") or 0.0
|
|
218
|
+
if results[1].get("success"):
|
|
219
|
+
whype_balance = results[1].get("balance_decimal") or 0.0
|
|
220
|
+
if results[2].get("success"):
|
|
221
|
+
khype_balance = results[2].get("balance_decimal") or 0.0
|
|
222
|
+
if results[3].get("success"):
|
|
223
|
+
looped_hype_balance = results[3].get("balance_decimal") or 0.0
|
|
224
|
+
if results[4].get("success"):
|
|
225
|
+
usdc_arb_idle = results[4].get("balance_decimal") or 0.0
|
|
226
|
+
if results[5].get("success"):
|
|
227
|
+
usdt_arb_idle = results[5].get("balance_decimal") or 0.0
|
|
228
|
+
if results[6].get("success"):
|
|
229
|
+
eth_arb_balance = results[6].get("balance_decimal") or 0.0
|
|
230
|
+
if results[7].get("success"):
|
|
231
|
+
hype_oft_arb_balance = results[7].get("balance_decimal") or 0.0
|
|
232
|
+
|
|
233
|
+
except Exception as e:
|
|
234
|
+
logger.warning(f"Failed to get wallet balances via multicall: {e}")
|
|
235
|
+
|
|
236
|
+
# If we recently initiated a HyperEVM -> Arbitrum OFT bridge, the HYPE is
|
|
237
|
+
# temporarily "in flight" (deducted from HyperEVM, not yet minted on Arb).
|
|
238
|
+
# Track it in runtime to prevent hedge thrash + repeated funding.
|
|
239
|
+
in_flight_hype = float(self._planner_runtime.in_flight_boros_oft_hype or 0.0)
|
|
240
|
+
if in_flight_hype > 0:
|
|
241
|
+
balance_before = float(
|
|
242
|
+
self._planner_runtime.in_flight_boros_oft_hype_balance_before or 0.0
|
|
243
|
+
)
|
|
244
|
+
# Clear in-flight once Arb balance has increased by ~the bridged amount.
|
|
245
|
+
if hype_oft_arb_balance >= balance_before + (in_flight_hype * 0.95):
|
|
246
|
+
logger.info(
|
|
247
|
+
"Detected OFT HYPE arrival on Arbitrum; clearing in-flight bridge tracking"
|
|
248
|
+
)
|
|
249
|
+
self._planner_runtime.in_flight_boros_oft_hype = 0.0
|
|
250
|
+
self._planner_runtime.in_flight_boros_oft_hype_balance_before = 0.0
|
|
251
|
+
self._planner_runtime.in_flight_boros_oft_hype_started_at = None
|
|
252
|
+
in_flight_hype = 0.0
|
|
253
|
+
|
|
254
|
+
khype_to_hype_ratio = await self._get_khype_to_hype_ratio()
|
|
255
|
+
looped_hype_to_hype_ratio = await self._get_looped_hype_to_hype_ratio()
|
|
256
|
+
|
|
257
|
+
hl_spot_hype_value_usd = hl_spot_hype * hype_price_usd
|
|
258
|
+
hype_hyperevm_value_usd = hype_hyperevm_balance * hype_price_usd
|
|
259
|
+
whype_value_usd = whype_balance * hype_price_usd # WHYPE is 1:1 with HYPE
|
|
260
|
+
khype_value_usd = khype_balance * khype_to_hype_ratio * hype_price_usd
|
|
261
|
+
looped_hype_value_usd = (
|
|
262
|
+
looped_hype_balance * looped_hype_to_hype_ratio * hype_price_usd
|
|
263
|
+
)
|
|
264
|
+
hype_oft_arb_value_usd = hype_oft_arb_balance * hype_price_usd
|
|
265
|
+
in_flight_hype_value_usd = in_flight_hype * hype_price_usd
|
|
266
|
+
|
|
267
|
+
boros_collateral_usd = boros_collateral_hype * hype_price_usd
|
|
268
|
+
boros_pending_withdrawal_usd = boros_pending_withdrawal_hype * hype_price_usd
|
|
269
|
+
boros_committed_collateral_usd = (
|
|
270
|
+
boros_collateral_usd + hype_oft_arb_value_usd + in_flight_hype_value_usd
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
# HyperEVM spot value (LSTs + native HYPE/WHYPE). Boros collateral and
|
|
274
|
+
# Arbitrum OFT HYPE are tracked separately.
|
|
275
|
+
spot_value_usd = (
|
|
276
|
+
hype_hyperevm_value_usd
|
|
277
|
+
+ whype_value_usd
|
|
278
|
+
+ khype_value_usd
|
|
279
|
+
+ looped_hype_value_usd
|
|
280
|
+
+ hl_spot_hype_value_usd
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Gas reserve shouldn't be hedged; WHYPE counts as 1:1 HYPE exposure
|
|
284
|
+
hedgeable_hyperevm_hype = max(0.0, hype_hyperevm_balance - MIN_HYPE_GAS)
|
|
285
|
+
total_hype_exposure = (
|
|
286
|
+
hedgeable_hyperevm_hype
|
|
287
|
+
+ whype_balance # WHYPE is 1:1 with HYPE
|
|
288
|
+
+ (khype_balance * khype_to_hype_ratio)
|
|
289
|
+
+ (looped_hype_balance * looped_hype_to_hype_ratio)
|
|
290
|
+
+ hl_spot_hype
|
|
291
|
+
+ hype_oft_arb_balance
|
|
292
|
+
+ in_flight_hype
|
|
293
|
+
+ boros_collateral_hype
|
|
294
|
+
+ boros_pending_withdrawal_hype
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
hl_short_value_usd = hl_short_size_hype * hype_price_usd
|
|
298
|
+
|
|
299
|
+
total_value = (
|
|
300
|
+
spot_value_usd
|
|
301
|
+
+ hl_perp_margin
|
|
302
|
+
+ hl_spot_usdc
|
|
303
|
+
+ boros_collateral_usd
|
|
304
|
+
+ boros_pending_withdrawal_usd
|
|
305
|
+
+ usdc_arb_idle
|
|
306
|
+
+ usdt_arb_idle
|
|
307
|
+
+ hype_oft_arb_value_usd
|
|
308
|
+
+ in_flight_hype_value_usd
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
inv = Inventory(
|
|
312
|
+
hype_hyperevm_balance=hype_hyperevm_balance,
|
|
313
|
+
hype_hyperevm_value_usd=hype_hyperevm_value_usd,
|
|
314
|
+
whype_balance=whype_balance,
|
|
315
|
+
whype_value_usd=whype_value_usd,
|
|
316
|
+
khype_balance=khype_balance,
|
|
317
|
+
khype_value_usd=khype_value_usd,
|
|
318
|
+
looped_hype_balance=looped_hype_balance,
|
|
319
|
+
looped_hype_value_usd=looped_hype_value_usd,
|
|
320
|
+
usdc_arb_idle=usdc_arb_idle,
|
|
321
|
+
usdt_arb_idle=usdt_arb_idle,
|
|
322
|
+
eth_arb_balance=eth_arb_balance,
|
|
323
|
+
hype_oft_arb_balance=hype_oft_arb_balance,
|
|
324
|
+
hype_oft_arb_value_usd=hype_oft_arb_value_usd,
|
|
325
|
+
hl_perp_margin=hl_perp_margin,
|
|
326
|
+
hl_spot_usdc=hl_spot_usdc,
|
|
327
|
+
hl_spot_hype=hl_spot_hype,
|
|
328
|
+
hl_spot_hype_value_usd=hl_spot_hype_value_usd,
|
|
329
|
+
hl_short_size_hype=hl_short_size_hype,
|
|
330
|
+
hl_short_value_usd=hl_short_value_usd,
|
|
331
|
+
hl_unrealized_pnl=hl_unrealized_pnl,
|
|
332
|
+
hl_withdrawable_usd=hl_withdrawable_usd,
|
|
333
|
+
boros_idle_collateral_isolated=boros_idle_collateral_isolated,
|
|
334
|
+
boros_idle_collateral_cross=boros_idle_collateral_cross,
|
|
335
|
+
boros_collateral_hype=boros_collateral_hype,
|
|
336
|
+
boros_collateral_usd=boros_collateral_usd,
|
|
337
|
+
boros_pending_withdrawal_hype=boros_pending_withdrawal_hype,
|
|
338
|
+
boros_pending_withdrawal_usd=boros_pending_withdrawal_usd,
|
|
339
|
+
boros_committed_collateral_usd=boros_committed_collateral_usd,
|
|
340
|
+
boros_position_size=boros_position_size,
|
|
341
|
+
boros_position_value=boros_position_value,
|
|
342
|
+
khype_to_hype_ratio=khype_to_hype_ratio,
|
|
343
|
+
looped_hype_to_hype_ratio=looped_hype_to_hype_ratio,
|
|
344
|
+
hype_price_usd=hype_price_usd,
|
|
345
|
+
spot_value_usd=spot_value_usd,
|
|
346
|
+
total_hype_exposure=total_hype_exposure,
|
|
347
|
+
total_value=total_value,
|
|
348
|
+
boros_position_market_ids=sorted(boros_position_market_ids)
|
|
349
|
+
if boros_position_market_ids
|
|
350
|
+
else None,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
self._opa_alloc = self._get_allocation_status(inv)
|
|
354
|
+
|
|
355
|
+
self._opa_risk_progress = self._hyperliquid_liquidation_progress(
|
|
356
|
+
perp_position, mid_prices
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
# Only set pending flag for actual Boros withdrawal (not for idle USDT on Arb)
|
|
360
|
+
if inv.boros_pending_withdrawal_usd > 1.0:
|
|
361
|
+
self._opa_pending_withdrawal = True
|
|
362
|
+
|
|
363
|
+
# Check for HL liquidation only when hedge is gone but spot exposure exists
|
|
364
|
+
has_no_short = abs(inv.hl_short_size_hype) < 0.01
|
|
365
|
+
has_spot_exposure = inv.total_hype_exposure > 0.1
|
|
366
|
+
|
|
367
|
+
if (
|
|
368
|
+
has_no_short
|
|
369
|
+
and has_spot_exposure
|
|
370
|
+
and self.hyperliquid_adapter
|
|
371
|
+
and user_address
|
|
372
|
+
):
|
|
373
|
+
since_ms = int((time.time() - 43200) * 1000) # last 12 hours
|
|
374
|
+
try:
|
|
375
|
+
(
|
|
376
|
+
ok,
|
|
377
|
+
liq_fills,
|
|
378
|
+
) = await self.hyperliquid_adapter.check_recent_liquidations(
|
|
379
|
+
user_address, since_ms
|
|
380
|
+
)
|
|
381
|
+
if ok and liq_fills:
|
|
382
|
+
inv.hl_liquidation_detected = True
|
|
383
|
+
inv.hl_liquidation_fills = liq_fills
|
|
384
|
+
logger.warning(
|
|
385
|
+
f"[LIQUIDATION] HL position was liquidated! "
|
|
386
|
+
f"Short={inv.hl_short_size_hype:.4f}, "
|
|
387
|
+
f"Spot exposure={inv.total_hype_exposure:.4f}"
|
|
388
|
+
)
|
|
389
|
+
for fill in liq_fills:
|
|
390
|
+
liq = fill.get("liquidation", {})
|
|
391
|
+
logger.warning(
|
|
392
|
+
f"[LIQUIDATION] coin={fill.get('coin')}, sz={fill.get('sz')}, "
|
|
393
|
+
f"method={liq.get('method')}, markPx={liq.get('markPx')}"
|
|
394
|
+
)
|
|
395
|
+
except Exception as e:
|
|
396
|
+
logger.warning(f"Failed to check for liquidations: {e}")
|
|
397
|
+
|
|
398
|
+
if self.boros_adapter:
|
|
399
|
+
try:
|
|
400
|
+
success, quotes = await self.boros_adapter.quote_markets_for_underlying(
|
|
401
|
+
"HYPE"
|
|
402
|
+
)
|
|
403
|
+
if success:
|
|
404
|
+
self._opa_boros_quotes = quotes
|
|
405
|
+
logger.debug(f"Fetched {len(quotes)} Boros HYPE quotes")
|
|
406
|
+
except Exception as e:
|
|
407
|
+
logger.warning(f"Failed to get Boros quotes: {e}")
|
|
408
|
+
self._opa_boros_quotes = []
|
|
409
|
+
|
|
410
|
+
return inv
|
|
411
|
+
|
|
412
|
+
def _hyperliquid_liquidation_progress(
|
|
413
|
+
self,
|
|
414
|
+
perp_pos: dict[str, Any] | None,
|
|
415
|
+
mid_prices: dict[str, float] | None = None,
|
|
416
|
+
) -> float:
|
|
417
|
+
# Returns fraction [0,1] of distance from entry to liquidation (0 = at entry, 1 = at liq)
|
|
418
|
+
if not perp_pos:
|
|
419
|
+
return 0.0
|
|
420
|
+
|
|
421
|
+
liq_px = perp_pos.get("liquidationPx") or perp_pos.get("liqPx")
|
|
422
|
+
entry_px = perp_pos.get("entryPx") or perp_pos.get("entryPrice")
|
|
423
|
+
szi = perp_pos.get("szi") or perp_pos.get("size")
|
|
424
|
+
coin = perp_pos.get("coin", "HYPE")
|
|
425
|
+
|
|
426
|
+
mark_px = None
|
|
427
|
+
if mid_prices:
|
|
428
|
+
mark_px = mid_prices.get(coin)
|
|
429
|
+
if mark_px is None:
|
|
430
|
+
mark_px = perp_pos.get("px") or perp_pos.get("markPx")
|
|
431
|
+
|
|
432
|
+
if not all([liq_px, entry_px, mark_px, szi]):
|
|
433
|
+
return 0.0
|
|
434
|
+
|
|
435
|
+
try:
|
|
436
|
+
liq = float(liq_px)
|
|
437
|
+
entry = float(entry_px)
|
|
438
|
+
mark = float(mark_px)
|
|
439
|
+
size = float(szi)
|
|
440
|
+
|
|
441
|
+
if abs(liq - entry) < 0.0001:
|
|
442
|
+
return 0.0
|
|
443
|
+
|
|
444
|
+
# For SHORT positions (szi < 0):
|
|
445
|
+
# Progress = (mark - entry) / (liq - entry)
|
|
446
|
+
# When mark rises toward liq, progress → 1
|
|
447
|
+
if size < 0:
|
|
448
|
+
progress = (mark - entry) / (liq - entry)
|
|
449
|
+
else:
|
|
450
|
+
# For LONG positions (szi > 0):
|
|
451
|
+
# Progress = (entry - mark) / (entry - liq)
|
|
452
|
+
# When mark falls toward liq, progress → 1
|
|
453
|
+
progress = (entry - mark) / (entry - liq)
|
|
454
|
+
|
|
455
|
+
return max(0.0, min(1.0, progress))
|
|
456
|
+
except (ValueError, ZeroDivisionError):
|
|
457
|
+
return 0.0
|
|
458
|
+
|
|
459
|
+
async def _get_khype_to_hype_ratio(self) -> float:
|
|
460
|
+
# Query Kinetiq StakingAccountant kHYPEToHYPE(1e18) for HYPE per 1 kHYPE
|
|
461
|
+
try:
|
|
462
|
+
async with web3_from_chain_id(HYPEREVM_CHAIN_ID) as w3:
|
|
463
|
+
# kHYPE has 18 decimals, so 1 kHYPE = 1e18
|
|
464
|
+
one_khype = 10**18
|
|
465
|
+
|
|
466
|
+
contract = w3.eth.contract(
|
|
467
|
+
address=w3.to_checksum_address(KHYPE_STAKING_ACCOUNTANT),
|
|
468
|
+
abi=KHYPE_STAKING_ACCOUNTANT_ABI,
|
|
469
|
+
)
|
|
470
|
+
hype_raw = await contract.functions.kHYPEToHYPE(one_khype).call()
|
|
471
|
+
|
|
472
|
+
# HYPE also has 18 decimals
|
|
473
|
+
return int(hype_raw) / (10**18)
|
|
474
|
+
except Exception as e:
|
|
475
|
+
logger.warning(f"Failed to get kHYPE exchange rate: {e}")
|
|
476
|
+
return 1.0 # Default to 1:1
|
|
477
|
+
|
|
478
|
+
async def _get_looped_hype_to_hype_ratio(self) -> float:
|
|
479
|
+
# Query Looping Accountant getRateInQuote(WHYPE) for HYPE per 1 LHYPE
|
|
480
|
+
try:
|
|
481
|
+
async with web3_from_chain_id(HYPEREVM_CHAIN_ID) as w3:
|
|
482
|
+
contract = w3.eth.contract(
|
|
483
|
+
address=w3.to_checksum_address(LHYPE_ACCOUNTANT),
|
|
484
|
+
abi=LHYPE_ACCOUNTANT_ABI,
|
|
485
|
+
)
|
|
486
|
+
rate_raw = await contract.functions.getRateInQuote(
|
|
487
|
+
w3.to_checksum_address(WHYPE_ADDRESS)
|
|
488
|
+
).call()
|
|
489
|
+
|
|
490
|
+
# Rate is returned with 18 decimals (WHYPE decimals)
|
|
491
|
+
return int(rate_raw) / (10**18)
|
|
492
|
+
except Exception as e:
|
|
493
|
+
logger.warning(f"Failed to get LHYPE exchange rate: {e}")
|
|
494
|
+
return 1.0 # Default to 1:1
|