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.

Files changed (122) hide show
  1. wayfinder_paths/adapters/balance_adapter/adapter.py +250 -0
  2. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  3. wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -11
  4. wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
  5. wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
  6. wayfinder_paths/adapters/boros_adapter/client.py +476 -0
  7. wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
  8. wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
  9. wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
  10. wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
  11. wayfinder_paths/adapters/boros_adapter/types.py +70 -0
  12. wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
  13. wayfinder_paths/adapters/brap_adapter/adapter.py +1 -1
  14. wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
  15. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +161 -26
  16. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
  17. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +77 -13
  18. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
  19. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +585 -61
  20. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
  21. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
  22. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
  23. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
  24. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
  25. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
  26. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
  27. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
  28. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
  29. wayfinder_paths/adapters/moonwell_adapter/adapter.py +592 -400
  30. wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
  31. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +126 -219
  32. wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
  33. wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
  34. wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
  35. wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
  36. wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
  37. wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
  38. wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
  39. wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
  40. wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
  41. wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
  42. wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
  43. wayfinder_paths/adapters/token_adapter/examples.json +0 -4
  44. wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
  45. wayfinder_paths/conftest.py +24 -17
  46. wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
  47. wayfinder_paths/core/adapters/models.py +17 -7
  48. wayfinder_paths/core/clients/BRAPClient.py +1 -1
  49. wayfinder_paths/core/clients/TokenClient.py +47 -1
  50. wayfinder_paths/core/clients/WayfinderClient.py +1 -2
  51. wayfinder_paths/core/clients/protocols.py +21 -22
  52. wayfinder_paths/core/clients/test_ledger_client.py +448 -0
  53. wayfinder_paths/core/config.py +12 -0
  54. wayfinder_paths/core/constants/__init__.py +15 -0
  55. wayfinder_paths/core/constants/base.py +6 -1
  56. wayfinder_paths/core/constants/contracts.py +39 -26
  57. wayfinder_paths/core/constants/erc20_abi.py +0 -1
  58. wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
  59. wayfinder_paths/core/constants/hyperliquid.py +16 -0
  60. wayfinder_paths/core/constants/moonwell_abi.py +0 -15
  61. wayfinder_paths/core/engine/manifest.py +66 -0
  62. wayfinder_paths/core/strategies/Strategy.py +0 -61
  63. wayfinder_paths/core/strategies/__init__.py +10 -1
  64. wayfinder_paths/core/strategies/opa_loop.py +167 -0
  65. wayfinder_paths/core/utils/test_transaction.py +289 -0
  66. wayfinder_paths/core/utils/transaction.py +44 -1
  67. wayfinder_paths/core/utils/web3.py +3 -0
  68. wayfinder_paths/mcp/__init__.py +5 -0
  69. wayfinder_paths/mcp/preview.py +185 -0
  70. wayfinder_paths/mcp/scripting.py +84 -0
  71. wayfinder_paths/mcp/server.py +52 -0
  72. wayfinder_paths/mcp/state/profile_store.py +195 -0
  73. wayfinder_paths/mcp/state/store.py +89 -0
  74. wayfinder_paths/mcp/test_scripting.py +267 -0
  75. wayfinder_paths/mcp/tools/__init__.py +0 -0
  76. wayfinder_paths/mcp/tools/balances.py +290 -0
  77. wayfinder_paths/mcp/tools/discovery.py +158 -0
  78. wayfinder_paths/mcp/tools/execute.py +770 -0
  79. wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
  80. wayfinder_paths/mcp/tools/quotes.py +288 -0
  81. wayfinder_paths/mcp/tools/run_script.py +286 -0
  82. wayfinder_paths/mcp/tools/strategies.py +188 -0
  83. wayfinder_paths/mcp/tools/tokens.py +46 -0
  84. wayfinder_paths/mcp/tools/wallets.py +354 -0
  85. wayfinder_paths/mcp/utils.py +129 -0
  86. wayfinder_paths/policies/hyperliquid.py +1 -1
  87. wayfinder_paths/policies/lifi.py +18 -0
  88. wayfinder_paths/policies/util.py +8 -2
  89. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +28 -119
  90. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
  91. wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
  92. wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
  93. wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
  94. wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
  95. wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
  96. wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
  97. wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
  98. wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
  99. wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
  100. wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
  101. wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
  102. wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
  103. wayfinder_paths/strategies/boros_hype_strategy/test_strategy.py +202 -0
  104. wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
  105. wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
  106. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +3 -12
  107. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +7 -29
  108. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +63 -40
  109. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
  110. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +0 -34
  111. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +11 -34
  112. wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
  113. wayfinder_paths/tests/test_test_coverage.py +1 -4
  114. wayfinder_paths-0.1.24.dist-info/METADATA +378 -0
  115. wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
  116. {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
  117. wayfinder_paths/scripts/create_strategy.py +0 -139
  118. wayfinder_paths/scripts/make_wallets.py +0 -142
  119. wayfinder_paths-0.1.23.dist-info/METADATA +0 -354
  120. wayfinder_paths-0.1.23.dist-info/RECORD +0 -120
  121. /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
  122. {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