wayfinder-paths 0.1.22__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/__init__.py +0 -4
- wayfinder_paths/adapters/balance_adapter/README.md +0 -1
- wayfinder_paths/adapters/balance_adapter/adapter.py +313 -167
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +41 -124
- 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/README.md +22 -75
- wayfinder_paths/adapters/brap_adapter/adapter.py +187 -576
- wayfinder_paths/adapters/brap_adapter/examples.json +21 -140
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +6 -234
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +180 -92
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +82 -14
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +586 -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/README.md +4 -1
- wayfinder_paths/adapters/ledger_adapter/adapter.py +3 -3
- 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 +649 -547
- wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +160 -239
- 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/adapter.py +14 -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/__init__.py +0 -3
- wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
- wayfinder_paths/core/adapters/models.py +17 -7
- wayfinder_paths/core/clients/BRAPClient.py +4 -1
- wayfinder_paths/core/clients/ClientManager.py +0 -7
- wayfinder_paths/core/clients/LedgerClient.py +196 -172
- wayfinder_paths/core/clients/TokenClient.py +47 -1
- wayfinder_paths/core/clients/WayfinderClient.py +1 -3
- wayfinder_paths/core/clients/__init__.py +0 -5
- wayfinder_paths/core/clients/protocols.py +21 -35
- wayfinder_paths/core/clients/test_ledger_client.py +448 -0
- wayfinder_paths/core/config.py +10 -162
- wayfinder_paths/core/constants/__init__.py +73 -2
- wayfinder_paths/core/constants/base.py +8 -17
- wayfinder_paths/core/constants/chains.py +36 -0
- wayfinder_paths/core/constants/contracts.py +52 -0
- 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/constants/tokens.py +9 -0
- wayfinder_paths/core/engine/manifest.py +66 -0
- wayfinder_paths/core/strategies/Strategy.py +0 -71
- wayfinder_paths/core/strategies/__init__.py +10 -1
- wayfinder_paths/core/strategies/opa_loop.py +167 -0
- wayfinder_paths/core/utils/evm_helpers.py +5 -15
- wayfinder_paths/core/utils/test_transaction.py +289 -0
- wayfinder_paths/core/utils/tokens.py +28 -0
- wayfinder_paths/core/utils/transaction.py +57 -8
- wayfinder_paths/core/utils/web3.py +8 -3
- 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/enso.py +1 -2
- wayfinder_paths/policies/hyper_evm.py +6 -3
- wayfinder_paths/policies/hyperlend.py +1 -2
- wayfinder_paths/policies/hyperliquid.py +1 -1
- wayfinder_paths/policies/lifi.py +18 -0
- wayfinder_paths/policies/moonwell.py +12 -7
- wayfinder_paths/policies/prjx.py +1 -3
- wayfinder_paths/policies/util.py +8 -2
- wayfinder_paths/run_strategy.py +97 -300
- wayfinder_paths/strategies/basis_trading_strategy/constants.py +3 -1
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +47 -133
- 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/{templates/strategy → strategies/boros_hype_strategy}/test_strategy.py +99 -63
- 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 +15 -23
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +27 -62
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +84 -58
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +69 -164
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +43 -76
- 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.22.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
- wayfinder_paths/core/clients/WalletClient.py +0 -41
- wayfinder_paths/core/engine/StrategyJob.py +0 -110
- wayfinder_paths/core/services/test_local_evm_txn.py +0 -145
- wayfinder_paths/scripts/create_strategy.py +0 -139
- wayfinder_paths/scripts/make_wallets.py +0 -142
- wayfinder_paths/templates/adapter/README.md +0 -150
- wayfinder_paths/templates/adapter/adapter.py +0 -16
- wayfinder_paths/templates/adapter/examples.json +0 -8
- wayfinder_paths/templates/adapter/test_adapter.py +0 -30
- wayfinder_paths/templates/strategy/README.md +0 -186
- wayfinder_paths/templates/strategy/examples.json +0 -11
- wayfinder_paths/templates/strategy/strategy.py +0 -35
- wayfinder_paths/tests/test_smoke_manifest.py +0 -63
- wayfinder_paths-0.1.22.dist-info/METADATA +0 -355
- wayfinder_paths-0.1.22.dist-info/RECORD +0 -129
- /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
- {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,1194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Boros HYPE Strategy - Delta-neutral HYPE yield with rate lock.
|
|
3
|
+
|
|
4
|
+
This strategy combines three legs:
|
|
5
|
+
1. Spot leg: kHYPE + looped HYPE yield on HyperEVM
|
|
6
|
+
2. Hedge leg: Hyperliquid HYPE perp short
|
|
7
|
+
3. Rate lock leg: Boros fixed-rate markets
|
|
8
|
+
|
|
9
|
+
The strategy uses an Observe→Plan→Act (OPA) control loop pattern where:
|
|
10
|
+
- observe() builds a full inventory snapshot
|
|
11
|
+
- plan() is a mostly-pure planner outputting prioritized PlanOp steps
|
|
12
|
+
- execute_step() dispatches to concrete adapter calls
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from eth_account import Account
|
|
22
|
+
from loguru import logger
|
|
23
|
+
|
|
24
|
+
from wayfinder_paths.adapters.balance_adapter.adapter import BalanceAdapter
|
|
25
|
+
from wayfinder_paths.adapters.boros_adapter import BorosAdapter, BorosMarketQuote
|
|
26
|
+
from wayfinder_paths.adapters.boros_adapter.client import BorosClient
|
|
27
|
+
from wayfinder_paths.adapters.brap_adapter.adapter import BRAPAdapter
|
|
28
|
+
from wayfinder_paths.adapters.hyperliquid_adapter.adapter import (
|
|
29
|
+
HyperliquidAdapter,
|
|
30
|
+
)
|
|
31
|
+
from wayfinder_paths.adapters.hyperliquid_adapter.executor import (
|
|
32
|
+
LocalHyperliquidExecutor,
|
|
33
|
+
)
|
|
34
|
+
from wayfinder_paths.adapters.ledger_adapter.adapter import LedgerAdapter
|
|
35
|
+
from wayfinder_paths.core.constants.hyperliquid import (
|
|
36
|
+
DEFAULT_HYPERLIQUID_BUILDER_FEE_TENTHS_BP,
|
|
37
|
+
HYPE_FEE_WALLET,
|
|
38
|
+
)
|
|
39
|
+
from wayfinder_paths.core.strategies import (
|
|
40
|
+
OPAConfig,
|
|
41
|
+
OPALoopMixin,
|
|
42
|
+
StatusDict,
|
|
43
|
+
StatusTuple,
|
|
44
|
+
Strategy,
|
|
45
|
+
)
|
|
46
|
+
from wayfinder_paths.core.strategies import (
|
|
47
|
+
Plan as OPAPlan,
|
|
48
|
+
)
|
|
49
|
+
from wayfinder_paths.core.strategies import (
|
|
50
|
+
PlanStep as OPAPlanStep,
|
|
51
|
+
)
|
|
52
|
+
from wayfinder_paths.core.strategies.descriptors import (
|
|
53
|
+
Complexity,
|
|
54
|
+
Directionality,
|
|
55
|
+
Frequency,
|
|
56
|
+
StratDescriptor,
|
|
57
|
+
TokenExposure,
|
|
58
|
+
Volatility,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
from .boros_ops_mixin import BorosHypeBorosOpsMixin
|
|
62
|
+
from .constants import (
|
|
63
|
+
BOROS_HYPE_TOKEN_ID,
|
|
64
|
+
ETH_ARB,
|
|
65
|
+
MIN_HYPE_GAS,
|
|
66
|
+
MIN_NET_DEPOSIT,
|
|
67
|
+
USDC_ARB,
|
|
68
|
+
USDT_ARB,
|
|
69
|
+
)
|
|
70
|
+
from .hyperevm_ops_mixin import BorosHypeHyperEvmOpsMixin
|
|
71
|
+
from .hyperliquid_ops_mixin import BorosHypeHyperliquidOpsMixin
|
|
72
|
+
from .planner import build_plan
|
|
73
|
+
from .risk_ops_mixin import BorosHypeRiskOpsMixin
|
|
74
|
+
from .snapshot_mixin import BorosHypeSnapshotMixin, fetch_khype_apy, fetch_lhype_apy
|
|
75
|
+
from .types import (
|
|
76
|
+
INVENTORY_CHANGING_OPS,
|
|
77
|
+
AllocationStatus,
|
|
78
|
+
HedgeConfig,
|
|
79
|
+
Inventory,
|
|
80
|
+
PlannerConfig,
|
|
81
|
+
PlannerRuntime,
|
|
82
|
+
PlanOp,
|
|
83
|
+
YieldInfo,
|
|
84
|
+
)
|
|
85
|
+
from .withdraw_mixin import BorosHypeWithdrawMixin
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class BorosHypeStrategy(
|
|
89
|
+
BorosHypeSnapshotMixin,
|
|
90
|
+
BorosHypeWithdrawMixin,
|
|
91
|
+
BorosHypeHyperEvmOpsMixin,
|
|
92
|
+
BorosHypeBorosOpsMixin,
|
|
93
|
+
BorosHypeHyperliquidOpsMixin,
|
|
94
|
+
BorosHypeRiskOpsMixin,
|
|
95
|
+
OPALoopMixin[Inventory, PlanOp],
|
|
96
|
+
Strategy,
|
|
97
|
+
):
|
|
98
|
+
name = "HYPE Spot + Hyperliquid + Boros Strategy"
|
|
99
|
+
|
|
100
|
+
INFO = StratDescriptor(
|
|
101
|
+
description=(
|
|
102
|
+
"Delta-neutral HYPE yield strategy combining three legs: "
|
|
103
|
+
"1) Spot yield from kHYPE and looped HYPE on HyperEVM, "
|
|
104
|
+
"2) Hyperliquid HYPE perp short for delta hedging, "
|
|
105
|
+
"3) Boros fixed-rate markets for rate locking. "
|
|
106
|
+
"Deposits are Arbitrum USDC + ETH (for gas). "
|
|
107
|
+
"The strategy routes capital to all venues automatically."
|
|
108
|
+
),
|
|
109
|
+
summary=(
|
|
110
|
+
"Earns yield from HYPE spot (kHYPE/lHYPE) while hedging price exposure "
|
|
111
|
+
"via Hyperliquid perp shorts and locking in rates on Boros."
|
|
112
|
+
),
|
|
113
|
+
risk_description=(
|
|
114
|
+
"Higher risk than pure funding rate strategies due to lack of limit orders "
|
|
115
|
+
"on spot assets - entries and exits occur at market prices which can result "
|
|
116
|
+
"in slippage during volatile conditions. Additional smart contract risk across "
|
|
117
|
+
"multiple protocols (HyperEVM staking, Hyperliquid perps, Boros fixed-rate markets). "
|
|
118
|
+
"Liquidation risk on the perp short if funding diverges significantly. "
|
|
119
|
+
"Bridge risk when moving assets between chains."
|
|
120
|
+
),
|
|
121
|
+
gas_token_symbol="ETH",
|
|
122
|
+
gas_token_id="ethereum-arbitrum",
|
|
123
|
+
deposit_token_id="usd-coin-arbitrum",
|
|
124
|
+
minimum_net_deposit=MIN_NET_DEPOSIT,
|
|
125
|
+
gas_maximum=0.1,
|
|
126
|
+
gas_threshold=0.03,
|
|
127
|
+
volatility=Volatility.LOW,
|
|
128
|
+
volatility_description="Delta-neutral strategy minimizes price exposure.",
|
|
129
|
+
directionality=Directionality.DELTA_NEUTRAL,
|
|
130
|
+
directionality_description=(
|
|
131
|
+
"Long HYPE spot (kHYPE/lHYPE) hedged by short HYPE perp on Hyperliquid."
|
|
132
|
+
),
|
|
133
|
+
complexity=Complexity.HIGH,
|
|
134
|
+
complexity_description=(
|
|
135
|
+
"Complex multi-chain, multi-venue strategy requiring careful orchestration."
|
|
136
|
+
),
|
|
137
|
+
token_exposure=TokenExposure.ALTS,
|
|
138
|
+
token_exposure_description="Exposed to HYPE through hedged yield positions.",
|
|
139
|
+
frequency=Frequency.LOW,
|
|
140
|
+
frequency_description="Positions held for weeks to capture yield and funding.",
|
|
141
|
+
return_drivers=[
|
|
142
|
+
"kHYPE staking yield",
|
|
143
|
+
"lHYPE loop yield",
|
|
144
|
+
"funding rate",
|
|
145
|
+
"Boros fixed rate",
|
|
146
|
+
],
|
|
147
|
+
config={
|
|
148
|
+
"deposit": {
|
|
149
|
+
"description": "Deposit USDC and ETH to fund the strategy.",
|
|
150
|
+
"parameters": {
|
|
151
|
+
"main_token_amount": {
|
|
152
|
+
"type": "float",
|
|
153
|
+
"unit": "USDC",
|
|
154
|
+
"description": "Amount of USDC (Arbitrum) to deposit.",
|
|
155
|
+
"minimum": MIN_NET_DEPOSIT,
|
|
156
|
+
},
|
|
157
|
+
"gas_token_amount": {
|
|
158
|
+
"type": "float",
|
|
159
|
+
"unit": "ETH",
|
|
160
|
+
"description": "Amount of ETH (Arbitrum) for gas on multiple chains.",
|
|
161
|
+
"minimum": 0.0,
|
|
162
|
+
"maximum": 0.1,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
"update": {
|
|
167
|
+
"description": "Run the OPA control loop to manage positions.",
|
|
168
|
+
},
|
|
169
|
+
"withdraw": {
|
|
170
|
+
"description": "Close all positions and return funds to main wallet.",
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def __init__(
|
|
176
|
+
self,
|
|
177
|
+
config: dict[str, Any] | None = None,
|
|
178
|
+
*,
|
|
179
|
+
main_wallet: dict[str, Any] | None = None,
|
|
180
|
+
strategy_wallet: dict[str, Any] | None = None,
|
|
181
|
+
simulation: bool = False,
|
|
182
|
+
**kwargs,
|
|
183
|
+
) -> None:
|
|
184
|
+
super().__init__(
|
|
185
|
+
config=config,
|
|
186
|
+
main_wallet=main_wallet,
|
|
187
|
+
strategy_wallet=strategy_wallet,
|
|
188
|
+
**kwargs,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
self.simulation = simulation
|
|
192
|
+
self._config = config or {}
|
|
193
|
+
|
|
194
|
+
# Configuration
|
|
195
|
+
self.hedge_cfg = HedgeConfig.default()
|
|
196
|
+
self._planner_config = PlannerConfig.default()
|
|
197
|
+
self._planner_runtime = PlannerRuntime()
|
|
198
|
+
# Hyperliquid builder attribution is mandatory and fixed to HYPE_FEE_WALLET.
|
|
199
|
+
expected_builder = HYPE_FEE_WALLET.lower()
|
|
200
|
+
cfg_builder_fee = self._config.get("builder_fee")
|
|
201
|
+
fee = None
|
|
202
|
+
if isinstance(cfg_builder_fee, dict):
|
|
203
|
+
cfg_builder = str(cfg_builder_fee.get("b") or "").strip()
|
|
204
|
+
if cfg_builder and cfg_builder.lower() != expected_builder:
|
|
205
|
+
raise ValueError(
|
|
206
|
+
f"builder_fee.b must be {expected_builder} (got {cfg_builder})"
|
|
207
|
+
)
|
|
208
|
+
if cfg_builder_fee.get("f") is not None:
|
|
209
|
+
fee = cfg_builder_fee.get("f")
|
|
210
|
+
|
|
211
|
+
if fee is None:
|
|
212
|
+
fee = DEFAULT_HYPERLIQUID_BUILDER_FEE_TENTHS_BP
|
|
213
|
+
try:
|
|
214
|
+
fee_i = int(fee)
|
|
215
|
+
except (TypeError, ValueError) as exc:
|
|
216
|
+
raise ValueError("builder_fee.f must be an int (tenths of bp)") from exc
|
|
217
|
+
if fee_i <= 0:
|
|
218
|
+
raise ValueError("builder_fee.f must be > 0 (tenths of bp)")
|
|
219
|
+
|
|
220
|
+
self.builder_fee: dict[str, Any] = {"b": expected_builder, "f": fee_i}
|
|
221
|
+
|
|
222
|
+
# OPA context (populated in observe())
|
|
223
|
+
self._opa_alloc: AllocationStatus | None = None
|
|
224
|
+
self._opa_risk_progress: float = 0.0
|
|
225
|
+
self._opa_boros_quotes: list[BorosMarketQuote] = []
|
|
226
|
+
self._opa_yield_info: YieldInfo | None = None
|
|
227
|
+
|
|
228
|
+
# Pending withdrawal state tracking
|
|
229
|
+
self._opa_pending_withdrawal: bool = False
|
|
230
|
+
self._opa_completed_pending_withdrawal_this_tick: bool = False
|
|
231
|
+
|
|
232
|
+
# Emergency flags (best-effort, per-process)
|
|
233
|
+
self._failsafe_triggered: bool = False
|
|
234
|
+
self._failsafe_message: str | None = None
|
|
235
|
+
|
|
236
|
+
# Adapters (initialized in setup)
|
|
237
|
+
self.boros_adapter: BorosAdapter | None = None
|
|
238
|
+
self.hyperliquid_adapter: HyperliquidAdapter | None = None
|
|
239
|
+
self.balance_adapter: BalanceAdapter | None = None
|
|
240
|
+
self.brap_adapter: BRAPAdapter | None = None
|
|
241
|
+
self._sign_callback = None
|
|
242
|
+
|
|
243
|
+
def _make_sign_callback(self, private_key: str):
|
|
244
|
+
account = Account.from_key(private_key)
|
|
245
|
+
|
|
246
|
+
async def sign_callback(transaction: dict) -> bytes:
|
|
247
|
+
signed = account.sign_transaction(transaction)
|
|
248
|
+
return signed.raw_transaction
|
|
249
|
+
|
|
250
|
+
return sign_callback
|
|
251
|
+
|
|
252
|
+
async def setup(self) -> None:
|
|
253
|
+
strategy_wallet = self._config.get("strategy_wallet", {})
|
|
254
|
+
main_wallet = self._config.get("main_wallet", {})
|
|
255
|
+
user_address = strategy_wallet.get("address") if strategy_wallet else None
|
|
256
|
+
|
|
257
|
+
# Create signing callbacks from wallet private keys
|
|
258
|
+
strategy_pk = (
|
|
259
|
+
strategy_wallet.get("private_key") or strategy_wallet.get("private_key_hex")
|
|
260
|
+
if strategy_wallet
|
|
261
|
+
else None
|
|
262
|
+
)
|
|
263
|
+
main_pk = (
|
|
264
|
+
main_wallet.get("private_key") or main_wallet.get("private_key_hex")
|
|
265
|
+
if main_wallet
|
|
266
|
+
else None
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
self._sign_callback = (
|
|
270
|
+
self._make_sign_callback(strategy_pk) if strategy_pk else None
|
|
271
|
+
)
|
|
272
|
+
main_sign_callback = self._make_sign_callback(main_pk) if main_pk else None
|
|
273
|
+
|
|
274
|
+
# Initialize Boros adapter
|
|
275
|
+
self.boros_adapter = BorosAdapter(
|
|
276
|
+
config=self._config,
|
|
277
|
+
sign_callback=self._sign_callback,
|
|
278
|
+
simulation=self.simulation,
|
|
279
|
+
user_address=user_address,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Verify Boros connection
|
|
283
|
+
try:
|
|
284
|
+
connected = await self.boros_adapter.connect()
|
|
285
|
+
except Exception as e:
|
|
286
|
+
connected = False
|
|
287
|
+
logger.warning(f"Boros adapter connection failed: {e}")
|
|
288
|
+
if not connected:
|
|
289
|
+
logger.warning(
|
|
290
|
+
"Boros adapter connection failed - some features may not work"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Initialize Hyperliquid adapter for market data and perp trading
|
|
294
|
+
try:
|
|
295
|
+
# Create executor for non-simulation mode (requires private key)
|
|
296
|
+
hl_executor = None
|
|
297
|
+
if not self.simulation:
|
|
298
|
+
try:
|
|
299
|
+
hl_executor = LocalHyperliquidExecutor(config=self._config)
|
|
300
|
+
logger.info(
|
|
301
|
+
f"Hyperliquid executor initialized for {hl_executor.address}"
|
|
302
|
+
)
|
|
303
|
+
except (ImportError, ValueError) as e:
|
|
304
|
+
logger.warning(f"Hyperliquid executor not available: {e}")
|
|
305
|
+
|
|
306
|
+
self.hyperliquid_adapter = HyperliquidAdapter(
|
|
307
|
+
config=self._config,
|
|
308
|
+
simulation=self.simulation,
|
|
309
|
+
executor=hl_executor,
|
|
310
|
+
)
|
|
311
|
+
try:
|
|
312
|
+
hl_connected = await self.hyperliquid_adapter.connect()
|
|
313
|
+
except Exception as e:
|
|
314
|
+
hl_connected = False
|
|
315
|
+
logger.warning(f"Hyperliquid adapter connection failed: {e}")
|
|
316
|
+
if not hl_connected:
|
|
317
|
+
logger.warning("Hyperliquid adapter connection failed")
|
|
318
|
+
except Exception as e:
|
|
319
|
+
logger.warning(f"Hyperliquid adapter not available: {e}")
|
|
320
|
+
self.hyperliquid_adapter = None
|
|
321
|
+
|
|
322
|
+
# Initialize BalanceAdapter for on-chain balance reads
|
|
323
|
+
try:
|
|
324
|
+
self.balance_adapter = BalanceAdapter(
|
|
325
|
+
config=self._config,
|
|
326
|
+
main_wallet_signing_callback=main_sign_callback,
|
|
327
|
+
strategy_wallet_signing_callback=self._sign_callback,
|
|
328
|
+
)
|
|
329
|
+
logger.debug("BalanceAdapter initialized for on-chain balance reads")
|
|
330
|
+
except Exception as e:
|
|
331
|
+
logger.warning(f"BalanceAdapter initialization failed: {e}")
|
|
332
|
+
self.balance_adapter = None
|
|
333
|
+
|
|
334
|
+
# Initialize BRAP adapter for swaps and bridging
|
|
335
|
+
try:
|
|
336
|
+
self.brap_adapter = BRAPAdapter(
|
|
337
|
+
config=self._config,
|
|
338
|
+
strategy_wallet_signing_callback=self._sign_callback,
|
|
339
|
+
)
|
|
340
|
+
logger.debug("BRAPAdapter initialized for swaps")
|
|
341
|
+
except Exception as e:
|
|
342
|
+
logger.warning(f"BRAPAdapter initialization failed: {e}")
|
|
343
|
+
self.brap_adapter = None
|
|
344
|
+
|
|
345
|
+
# Initialize ledger adapter for transaction recording
|
|
346
|
+
try:
|
|
347
|
+
self.ledger_adapter = LedgerAdapter()
|
|
348
|
+
logger.debug("LedgerAdapter initialized")
|
|
349
|
+
except Exception as e:
|
|
350
|
+
logger.warning(f"LedgerAdapter initialization failed: {e}")
|
|
351
|
+
self.ledger_adapter = None
|
|
352
|
+
|
|
353
|
+
logger.info(f"BorosHypeStrategy setup complete (simulation={self.simulation})")
|
|
354
|
+
|
|
355
|
+
async def analyze(self, deposit_usdc: float = 1000.0) -> dict[str, Any]:
|
|
356
|
+
# Read-only market analysis returning Boros fixed-rate markets for HYPE
|
|
357
|
+
client = (
|
|
358
|
+
self.boros_adapter.boros_client if self.boros_adapter else BorosClient()
|
|
359
|
+
)
|
|
360
|
+
try:
|
|
361
|
+
markets = await client.list_markets(is_whitelisted=True, skip=0, limit=250)
|
|
362
|
+
except Exception as exc: # noqa: BLE001
|
|
363
|
+
return {
|
|
364
|
+
"success": False,
|
|
365
|
+
"error": str(exc),
|
|
366
|
+
"deposit_usdc": float(deposit_usdc),
|
|
367
|
+
"hype_markets": [],
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
out: list[dict[str, Any]] = []
|
|
371
|
+
for m in markets:
|
|
372
|
+
if not isinstance(m, dict):
|
|
373
|
+
continue
|
|
374
|
+
im = m.get("imData") if isinstance(m.get("imData"), dict) else {}
|
|
375
|
+
meta = m.get("metadata") if isinstance(m.get("metadata"), dict) else {}
|
|
376
|
+
platform_obj = (
|
|
377
|
+
m.get("platform") if isinstance(m.get("platform"), dict) else {}
|
|
378
|
+
)
|
|
379
|
+
data = m.get("data") if isinstance(m.get("data"), dict) else {}
|
|
380
|
+
|
|
381
|
+
name = str(im.get("name") or meta.get("marketName") or "").strip()
|
|
382
|
+
symbol = str(im.get("symbol") or "").strip()
|
|
383
|
+
platform = str(
|
|
384
|
+
platform_obj.get("name") or meta.get("platformName") or ""
|
|
385
|
+
).strip()
|
|
386
|
+
asset_symbol = str(
|
|
387
|
+
meta.get("assetSymbol") or meta.get("name") or ""
|
|
388
|
+
).strip()
|
|
389
|
+
|
|
390
|
+
hay = " ".join([name, symbol, platform, asset_symbol]).upper()
|
|
391
|
+
if "HYPE" not in hay:
|
|
392
|
+
continue
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
market_id = int(m.get("marketId") or m.get("id") or 0)
|
|
396
|
+
except Exception:
|
|
397
|
+
market_id = 0
|
|
398
|
+
|
|
399
|
+
maturity_ts = im.get("maturity")
|
|
400
|
+
try:
|
|
401
|
+
maturity_ts_i = int(maturity_ts) if maturity_ts is not None else None
|
|
402
|
+
except Exception:
|
|
403
|
+
maturity_ts_i = None
|
|
404
|
+
|
|
405
|
+
def _f(x: Any) -> float | None:
|
|
406
|
+
try:
|
|
407
|
+
if x is None:
|
|
408
|
+
return None
|
|
409
|
+
return float(x)
|
|
410
|
+
except Exception:
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
out.append(
|
|
414
|
+
{
|
|
415
|
+
"market_id": market_id,
|
|
416
|
+
"name": name or None,
|
|
417
|
+
"symbol": symbol or None,
|
|
418
|
+
"platform": platform or None,
|
|
419
|
+
"asset_symbol": asset_symbol.upper() if asset_symbol else None,
|
|
420
|
+
"maturity_ts": maturity_ts_i,
|
|
421
|
+
"time_to_maturity_s": _f(data.get("timeToMaturity")),
|
|
422
|
+
"mark_apr": _f(data.get("markApr")),
|
|
423
|
+
"mid_apr": _f(data.get("midApr")),
|
|
424
|
+
"best_bid_apr": _f(data.get("bestBid")),
|
|
425
|
+
"best_ask_apr": _f(data.get("bestAsk")),
|
|
426
|
+
"floating_apr": _f(data.get("floatingApr")),
|
|
427
|
+
}
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
out.sort(key=lambda x: (x.get("maturity_ts") or 0, x.get("market_id") or 0))
|
|
431
|
+
|
|
432
|
+
primary = next(
|
|
433
|
+
(
|
|
434
|
+
x
|
|
435
|
+
for x in out
|
|
436
|
+
if (x.get("platform") or "").lower() == "hyperliquid"
|
|
437
|
+
and (x.get("asset_symbol") or "").upper() == "HYPE"
|
|
438
|
+
),
|
|
439
|
+
None,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
"success": True,
|
|
444
|
+
"deposit_usdc": float(deposit_usdc),
|
|
445
|
+
"primary_market": primary,
|
|
446
|
+
"hype_markets": out,
|
|
447
|
+
"notes": {
|
|
448
|
+
"apr_units": "Decimals (0.10 = 10% APR).",
|
|
449
|
+
"lock_hint": "The fixed rate to lock is typically around best_bid_apr ↔ best_ask_apr (depends on side).",
|
|
450
|
+
},
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
@property
|
|
454
|
+
def opa_config(self) -> OPAConfig:
|
|
455
|
+
return OPAConfig(
|
|
456
|
+
max_iterations_per_tick=self._planner_config.max_iterations_per_tick,
|
|
457
|
+
max_steps_per_iteration=self._planner_config.max_steps_per_iteration,
|
|
458
|
+
max_total_steps_per_tick=self._planner_config.max_total_steps_per_tick,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
def plan(self, inventory: Inventory) -> OPAPlan[PlanOp]:
|
|
462
|
+
plan = build_plan(
|
|
463
|
+
inv=inventory,
|
|
464
|
+
alloc=self._opa_alloc or self._get_allocation_status(inventory),
|
|
465
|
+
risk_progress=self._opa_risk_progress,
|
|
466
|
+
hedge_cfg=self.hedge_cfg,
|
|
467
|
+
config=self._planner_config,
|
|
468
|
+
runtime=self._planner_runtime,
|
|
469
|
+
boros_quotes=self._opa_boros_quotes,
|
|
470
|
+
pending_withdrawal_completion=self._opa_pending_withdrawal,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# Convert to OPA Plan format
|
|
474
|
+
opa_plan = OPAPlan[PlanOp](
|
|
475
|
+
steps=[
|
|
476
|
+
OPAPlanStep(
|
|
477
|
+
op=step.op,
|
|
478
|
+
priority=step.priority,
|
|
479
|
+
key=step.key,
|
|
480
|
+
params=step.params,
|
|
481
|
+
reason=step.reason,
|
|
482
|
+
)
|
|
483
|
+
for step in plan.steps
|
|
484
|
+
],
|
|
485
|
+
desired_state={
|
|
486
|
+
"mode": plan.desired_state.mode.name,
|
|
487
|
+
"target_spot_usd": plan.desired_state.target_spot_usd,
|
|
488
|
+
"target_hl_margin_usd": plan.desired_state.target_hl_margin_usd,
|
|
489
|
+
"boros_market_id": plan.desired_state.boros_market_id,
|
|
490
|
+
},
|
|
491
|
+
)
|
|
492
|
+
return opa_plan
|
|
493
|
+
|
|
494
|
+
async def execute_step(
|
|
495
|
+
self, step: OPAPlanStep[PlanOp], inventory: Inventory
|
|
496
|
+
) -> tuple[bool, str]:
|
|
497
|
+
op = step.op
|
|
498
|
+
params = step.params
|
|
499
|
+
|
|
500
|
+
logger.info(f"Executing {op.name}: {step.reason}")
|
|
501
|
+
|
|
502
|
+
if self.simulation:
|
|
503
|
+
return True, f"[SIMULATION] {op.name} executed"
|
|
504
|
+
|
|
505
|
+
if self._failsafe_triggered:
|
|
506
|
+
return False, "Failsafe already triggered; skipping further actions"
|
|
507
|
+
|
|
508
|
+
# Dispatch to handlers - complete mapping for all PlanOps
|
|
509
|
+
handlers = {
|
|
510
|
+
# Priority 0: Safety/Risk mitigation
|
|
511
|
+
PlanOp.CLOSE_AND_REDEPLOY: self._close_and_redeploy,
|
|
512
|
+
PlanOp.PARTIAL_TRIM_SPOT: self._partial_trim_spot,
|
|
513
|
+
PlanOp.COMPLETE_PENDING_WITHDRAWAL: self._complete_pending_withdrawal,
|
|
514
|
+
# Priority 5: Gas routing
|
|
515
|
+
PlanOp.ENSURE_GAS_ON_HYPEREVM: self._ensure_gas_on_hyperevm,
|
|
516
|
+
PlanOp.ENSURE_GAS_ON_ARBITRUM: self._ensure_gas_on_arbitrum,
|
|
517
|
+
# Priority 10-14: Capital routing
|
|
518
|
+
PlanOp.FUND_BOROS: self._fund_boros,
|
|
519
|
+
PlanOp.SEND_USDC_TO_HL: self._send_usdc_to_hl,
|
|
520
|
+
PlanOp.BRIDGE_TO_HYPEREVM: self._bridge_to_hyperevm,
|
|
521
|
+
PlanOp.DEPLOY_EXCESS_HL_MARGIN: self._deploy_excess_hl_margin,
|
|
522
|
+
PlanOp.TRANSFER_HL_SPOT_TO_HYPEREVM: self._transfer_hl_spot_to_hyperevm,
|
|
523
|
+
# Priority 20: Position management
|
|
524
|
+
PlanOp.SWAP_HYPE_TO_LST: self._swap_hype_to_lst,
|
|
525
|
+
PlanOp.ENSURE_HL_SHORT: self._ensure_hl_short,
|
|
526
|
+
# Priority 30: Rate positions
|
|
527
|
+
PlanOp.ENSURE_BOROS_POSITION: self._ensure_boros_position,
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
handler = handlers.get(op)
|
|
531
|
+
if handler:
|
|
532
|
+
return await handler(params, inventory)
|
|
533
|
+
|
|
534
|
+
logger.warning(f"No handler implemented for {op.name}")
|
|
535
|
+
return False, f"No handler for {op.name}"
|
|
536
|
+
|
|
537
|
+
def get_inventory_changing_ops(self) -> set[PlanOp]:
|
|
538
|
+
return INVENTORY_CHANGING_OPS
|
|
539
|
+
|
|
540
|
+
async def on_loop_start(self) -> tuple[bool, str] | None:
|
|
541
|
+
# Pre-loop setup: reset tick flags, check pending withdrawals, approve builder fee
|
|
542
|
+
self._planner_runtime.reset_virtual_ledger()
|
|
543
|
+
self._planner_runtime.reset_tick_flags()
|
|
544
|
+
self._planner_runtime.last_update_at = datetime.utcnow()
|
|
545
|
+
self._opa_completed_pending_withdrawal_this_tick = False
|
|
546
|
+
self._failsafe_triggered = False
|
|
547
|
+
self._failsafe_message = None
|
|
548
|
+
|
|
549
|
+
# Pre-check for pending withdrawal from Boros
|
|
550
|
+
# This allows build_plan() to prioritize withdrawal completion
|
|
551
|
+
self._opa_pending_withdrawal = False
|
|
552
|
+
if self.boros_adapter:
|
|
553
|
+
try:
|
|
554
|
+
token_id = (
|
|
555
|
+
self._planner_runtime.current_boros_token_id or BOROS_HYPE_TOKEN_ID
|
|
556
|
+
)
|
|
557
|
+
(
|
|
558
|
+
ok_pending,
|
|
559
|
+
pending_amt,
|
|
560
|
+
) = await self.boros_adapter.get_pending_withdrawal_amount(
|
|
561
|
+
token_id=int(token_id),
|
|
562
|
+
token_decimals=18,
|
|
563
|
+
)
|
|
564
|
+
if ok_pending and pending_amt > 0:
|
|
565
|
+
self._opa_pending_withdrawal = True
|
|
566
|
+
logger.info(
|
|
567
|
+
f"Pending Boros withdrawal detected: {pending_amt:.6f} collateral units"
|
|
568
|
+
)
|
|
569
|
+
# We do NOT perform any OPA actions while Boros withdrawals are pending.
|
|
570
|
+
# Withdrawal settlement can take 10-20 minutes; running update() in the
|
|
571
|
+
# meantime risks redeploying or hedging against an in-flight withdrawal.
|
|
572
|
+
return (
|
|
573
|
+
True,
|
|
574
|
+
f"Pending Boros withdrawal ({pending_amt:.6f}) - skipping update tick",
|
|
575
|
+
)
|
|
576
|
+
except Exception as e:
|
|
577
|
+
logger.warning(f"Failed to check pending withdrawal: {e}")
|
|
578
|
+
|
|
579
|
+
# Ensure Hyperliquid builder fee is approved (required prerequisite).
|
|
580
|
+
if (
|
|
581
|
+
not self.simulation
|
|
582
|
+
and self.hyperliquid_adapter
|
|
583
|
+
and self.builder_fee
|
|
584
|
+
and not self._planner_runtime.builder_fee_approved
|
|
585
|
+
):
|
|
586
|
+
strategy_wallet = self._config.get("strategy_wallet", {})
|
|
587
|
+
address = strategy_wallet.get("address")
|
|
588
|
+
|
|
589
|
+
if address:
|
|
590
|
+
ok, msg = await self.hyperliquid_adapter.ensure_builder_fee_approved(
|
|
591
|
+
address=address,
|
|
592
|
+
builder_fee=self.builder_fee,
|
|
593
|
+
)
|
|
594
|
+
if not ok:
|
|
595
|
+
return False, f"Failed to approve Hyperliquid builder fee: {msg}"
|
|
596
|
+
self._planner_runtime.builder_fee_approved = True
|
|
597
|
+
|
|
598
|
+
# Ensure Hyperliquid HYPE leverage is set *before* any paired fills/perp orders.
|
|
599
|
+
if (
|
|
600
|
+
not self.simulation
|
|
601
|
+
and self.hyperliquid_adapter
|
|
602
|
+
and not self._planner_runtime.leverage_set_for_hype
|
|
603
|
+
):
|
|
604
|
+
strategy_wallet = self._config.get("strategy_wallet", {})
|
|
605
|
+
address = strategy_wallet.get("address")
|
|
606
|
+
if address:
|
|
607
|
+
ok_lev, lev_msg = await self._ensure_hl_hype_leverage_set(address)
|
|
608
|
+
if not ok_lev:
|
|
609
|
+
return False, lev_msg
|
|
610
|
+
|
|
611
|
+
return None # Continue with loop
|
|
612
|
+
|
|
613
|
+
async def on_step_executed(
|
|
614
|
+
self,
|
|
615
|
+
step: OPAPlanStep[PlanOp],
|
|
616
|
+
success: bool,
|
|
617
|
+
message: str,
|
|
618
|
+
) -> None:
|
|
619
|
+
if step.op == PlanOp.COMPLETE_PENDING_WITHDRAWAL and success:
|
|
620
|
+
# Mark that we completed the pending withdrawal this tick
|
|
621
|
+
self._opa_pending_withdrawal = False
|
|
622
|
+
self._opa_completed_pending_withdrawal_this_tick = True
|
|
623
|
+
logger.info("Pending withdrawal completed this tick")
|
|
624
|
+
|
|
625
|
+
if step.op == PlanOp.FUND_BOROS and success:
|
|
626
|
+
# Prevent repeated Boros funding within same tick
|
|
627
|
+
self._planner_runtime.funded_boros_this_tick = True
|
|
628
|
+
logger.debug("Boros funded this tick - preventing duplicate funding")
|
|
629
|
+
|
|
630
|
+
def should_stop_early(
|
|
631
|
+
self, inventory: Inventory, iteration: int
|
|
632
|
+
) -> tuple[bool, str] | None:
|
|
633
|
+
if inventory.boros_pending_withdrawal_usd > 1.0:
|
|
634
|
+
self._opa_pending_withdrawal = True
|
|
635
|
+
return (
|
|
636
|
+
True,
|
|
637
|
+
f"Pending Boros withdrawal (${inventory.boros_pending_withdrawal_usd:.2f}) - skipping update tick",
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
# Stop if we completed a pending withdrawal this tick
|
|
641
|
+
# (we don't want to redeploy capital in the same tick)
|
|
642
|
+
if self._opa_completed_pending_withdrawal_this_tick:
|
|
643
|
+
return (
|
|
644
|
+
True,
|
|
645
|
+
"Pending withdrawal completed - stopping to avoid same-tick redeployment",
|
|
646
|
+
)
|
|
647
|
+
|
|
648
|
+
return None
|
|
649
|
+
|
|
650
|
+
def _get_allocation_status(self, inv: Inventory) -> AllocationStatus:
|
|
651
|
+
total = inv.total_value or 1.0
|
|
652
|
+
|
|
653
|
+
spot_actual = inv.spot_value_usd
|
|
654
|
+
hl_actual = inv.hl_perp_margin + inv.hl_spot_usdc
|
|
655
|
+
boros_actual = inv.boros_committed_collateral_usd
|
|
656
|
+
idle_actual = inv.usdc_arb_idle + inv.usdt_arb_idle
|
|
657
|
+
|
|
658
|
+
return AllocationStatus(
|
|
659
|
+
spot_value=spot_actual,
|
|
660
|
+
hl_value=hl_actual,
|
|
661
|
+
boros_value=boros_actual,
|
|
662
|
+
idle_value=idle_actual,
|
|
663
|
+
total_value=total,
|
|
664
|
+
spot_pct_actual=spot_actual / total,
|
|
665
|
+
hl_pct_actual=hl_actual / total,
|
|
666
|
+
boros_pct_actual=boros_actual / total,
|
|
667
|
+
spot_deviation=(spot_actual / total) - self.hedge_cfg.spot_pct,
|
|
668
|
+
hl_deviation=(hl_actual / total) - self.hedge_cfg.hyperliquid_pct,
|
|
669
|
+
boros_deviation=(boros_actual / total) - self.hedge_cfg.boros_pct,
|
|
670
|
+
spot_needed_usd=max(0, (self.hedge_cfg.spot_pct * total) - spot_actual),
|
|
671
|
+
hl_needed_usd=max(0, (self.hedge_cfg.hyperliquid_pct * total) - hl_actual),
|
|
672
|
+
boros_needed_usd=max(0, (self.hedge_cfg.boros_pct * total) - boros_actual),
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
676
|
+
# Risk & Invariant Helpers
|
|
677
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
678
|
+
|
|
679
|
+
def _delta_neutral_ok(self, inv: Inventory) -> tuple[bool, str]:
|
|
680
|
+
# Combined absolute + relative tolerance to avoid dust chasing
|
|
681
|
+
exp = float(inv.total_hype_exposure or 0.0)
|
|
682
|
+
short = float(inv.hl_short_size_hype or 0.0)
|
|
683
|
+
if exp < 0.1:
|
|
684
|
+
if abs(short) < 0.01:
|
|
685
|
+
return True, "No meaningful HYPE exposure"
|
|
686
|
+
return (
|
|
687
|
+
False,
|
|
688
|
+
f"Unexpected HL short without spot exposure (short={short:.4f} HYPE, spot={exp:.4f} HYPE)",
|
|
689
|
+
)
|
|
690
|
+
diff = abs(short - exp)
|
|
691
|
+
|
|
692
|
+
# Combined tolerance: max of absolute and relative
|
|
693
|
+
tol = max(
|
|
694
|
+
self._planner_config.delta_neutral_abs_tol_hype, # ~$2 at $25 HYPE
|
|
695
|
+
exp * self._planner_config.delta_neutral_rel_tol, # 2% relative
|
|
696
|
+
)
|
|
697
|
+
ok = diff <= tol
|
|
698
|
+
return ok, f"Δ={diff:.4f} tol={tol:.4f} (spot={exp:.4f}, short={short:.4f})"
|
|
699
|
+
|
|
700
|
+
def _boros_coverage_ok(
|
|
701
|
+
self, inv: Inventory, quotes: list[BorosMarketQuote] | None = None
|
|
702
|
+
) -> tuple[bool, str]:
|
|
703
|
+
# Target coverage is 85% of spot exposure with 5% band
|
|
704
|
+
if not self._planner_runtime.current_boros_market_id:
|
|
705
|
+
return True, "No Boros market selected"
|
|
706
|
+
|
|
707
|
+
if inv.total_value < self._planner_config.min_total_for_boros:
|
|
708
|
+
return True, "Below Boros minimum - skipping coverage check"
|
|
709
|
+
|
|
710
|
+
spot_usd = inv.total_hype_exposure * inv.hype_price_usd
|
|
711
|
+
if spot_usd < 10:
|
|
712
|
+
return True, "Negligible spot exposure"
|
|
713
|
+
|
|
714
|
+
target_position = spot_usd * self._planner_config.boros_coverage_target
|
|
715
|
+
current_position = inv.boros_position_size
|
|
716
|
+
|
|
717
|
+
if target_position < 10:
|
|
718
|
+
return True, "Target Boros position too small"
|
|
719
|
+
|
|
720
|
+
# Use resize threshold as hysteresis band
|
|
721
|
+
diff = abs(current_position - target_position)
|
|
722
|
+
ok = diff <= self._planner_config.boros_resize_min_excess_usd
|
|
723
|
+
|
|
724
|
+
return ok, (
|
|
725
|
+
f"diff=${diff:.2f} "
|
|
726
|
+
f"(current=${current_position:.0f}, target=${target_position:.0f})"
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
730
|
+
# Step Handlers
|
|
731
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
732
|
+
|
|
733
|
+
# Implemented in mixins:
|
|
734
|
+
# - boros_ops_mixin.py
|
|
735
|
+
# - hyperliquid_ops_mixin.py
|
|
736
|
+
# - hyperevm_ops_mixin.py
|
|
737
|
+
# - risk_ops_mixin.py
|
|
738
|
+
|
|
739
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
740
|
+
# Strategy Interface
|
|
741
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
742
|
+
|
|
743
|
+
async def deposit(
|
|
744
|
+
self,
|
|
745
|
+
main_token_amount: float = 0.0,
|
|
746
|
+
gas_token_amount: float = 0.0,
|
|
747
|
+
**kwargs,
|
|
748
|
+
) -> StatusTuple:
|
|
749
|
+
if main_token_amount < MIN_NET_DEPOSIT:
|
|
750
|
+
return (
|
|
751
|
+
False,
|
|
752
|
+
f"Minimum deposit is ${MIN_NET_DEPOSIT:.0f} USDC, got ${main_token_amount:.2f}",
|
|
753
|
+
)
|
|
754
|
+
|
|
755
|
+
if self.simulation:
|
|
756
|
+
return (
|
|
757
|
+
True,
|
|
758
|
+
f"[SIMULATION] Deposited ${main_token_amount:.2f} USDC + {gas_token_amount:.4f} ETH",
|
|
759
|
+
)
|
|
760
|
+
|
|
761
|
+
if not self.balance_adapter:
|
|
762
|
+
return False, "Balance adapter not configured"
|
|
763
|
+
|
|
764
|
+
main_wallet = self._config.get("main_wallet")
|
|
765
|
+
strategy_wallet = self._config.get("strategy_wallet")
|
|
766
|
+
main_address = (
|
|
767
|
+
main_wallet.get("address") if isinstance(main_wallet, dict) else None
|
|
768
|
+
)
|
|
769
|
+
strategy_address = (
|
|
770
|
+
strategy_wallet.get("address")
|
|
771
|
+
if isinstance(strategy_wallet, dict)
|
|
772
|
+
else None
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
if not main_address or not strategy_address:
|
|
776
|
+
return False, "main_wallet or strategy_wallet missing address"
|
|
777
|
+
|
|
778
|
+
# USDC (Arbitrum) deposit
|
|
779
|
+
usdc_to_move = float(main_token_amount)
|
|
780
|
+
if main_address.lower() == strategy_address.lower():
|
|
781
|
+
usdc_to_move = 0.0
|
|
782
|
+
else:
|
|
783
|
+
ok, usdc_raw = await self.balance_adapter.get_vault_wallet_balance(USDC_ARB)
|
|
784
|
+
existing_usdc = (usdc_raw / 1e6) if ok else 0.0
|
|
785
|
+
usdc_to_move = max(0.0, float(main_token_amount) - existing_usdc)
|
|
786
|
+
|
|
787
|
+
if usdc_to_move > 0.01:
|
|
788
|
+
(
|
|
789
|
+
move_ok,
|
|
790
|
+
move_res,
|
|
791
|
+
) = await self.balance_adapter.move_from_main_wallet_to_strategy_wallet(
|
|
792
|
+
token_id=USDC_ARB,
|
|
793
|
+
amount=usdc_to_move,
|
|
794
|
+
strategy_name="boros_hype_strategy",
|
|
795
|
+
skip_ledger=True,
|
|
796
|
+
)
|
|
797
|
+
if not move_ok:
|
|
798
|
+
return False, f"Failed to move USDC to strategy wallet: {move_res}"
|
|
799
|
+
|
|
800
|
+
# ETH (Arbitrum) gas deposit
|
|
801
|
+
eth_to_move = float(gas_token_amount)
|
|
802
|
+
if main_address.lower() == strategy_address.lower():
|
|
803
|
+
eth_to_move = 0.0
|
|
804
|
+
else:
|
|
805
|
+
ok, eth_raw = await self.balance_adapter.get_vault_wallet_balance(ETH_ARB)
|
|
806
|
+
existing_eth = (eth_raw / 1e18) if ok else 0.0
|
|
807
|
+
eth_to_move = max(0.0, float(gas_token_amount) - existing_eth)
|
|
808
|
+
|
|
809
|
+
if eth_to_move > 0.00001:
|
|
810
|
+
(
|
|
811
|
+
move_ok,
|
|
812
|
+
move_res,
|
|
813
|
+
) = await self.balance_adapter.move_from_main_wallet_to_strategy_wallet(
|
|
814
|
+
token_id=ETH_ARB,
|
|
815
|
+
amount=eth_to_move,
|
|
816
|
+
strategy_name="boros_hype_strategy",
|
|
817
|
+
skip_ledger=True,
|
|
818
|
+
)
|
|
819
|
+
if not move_ok:
|
|
820
|
+
return False, f"Failed to move ETH to strategy wallet: {move_res}"
|
|
821
|
+
|
|
822
|
+
return True, (
|
|
823
|
+
f"Deposit ready. Moved {usdc_to_move:.2f} USDC + {eth_to_move:.4f} ETH to strategy wallet. "
|
|
824
|
+
"Run update() to deploy."
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
async def update(self) -> tuple[bool, str, bool]:
|
|
828
|
+
# Run OPA loop, then enforce invariants: LST allocation, delta-neutrality, Boros coverage
|
|
829
|
+
result = await self.run_opa_loop()
|
|
830
|
+
success, message, rotated = result
|
|
831
|
+
|
|
832
|
+
# If a failsafe liquidation was triggered inside the loop, treat update as failed.
|
|
833
|
+
if self._failsafe_triggered:
|
|
834
|
+
return False, self._failsafe_message or message, rotated
|
|
835
|
+
|
|
836
|
+
# If OPA loop failed, return immediately
|
|
837
|
+
if not success:
|
|
838
|
+
return result
|
|
839
|
+
|
|
840
|
+
# If a withdrawal is pending, do nothing else this tick.
|
|
841
|
+
# We intentionally avoid the safety pass and any redeployment/hedging.
|
|
842
|
+
if self._opa_pending_withdrawal:
|
|
843
|
+
return result
|
|
844
|
+
|
|
845
|
+
# FINAL SAFETY PASS - Enforce invariants after OPA loop
|
|
846
|
+
# Even with good planning, we can end a tick offside because:
|
|
847
|
+
# - A hedge order partially fills
|
|
848
|
+
# - Step budget exhausted before hedge steps run
|
|
849
|
+
# - Never re-observed after final hedge step
|
|
850
|
+
# This pass uses fresh inventory and fixes drift immediately.
|
|
851
|
+
|
|
852
|
+
try:
|
|
853
|
+
inv = await self.observe()
|
|
854
|
+
safety_messages: list[str] = []
|
|
855
|
+
|
|
856
|
+
# 1) Spot should be allocated into LSTs (kHYPE + looped HYPE)
|
|
857
|
+
swappable_hype = max(
|
|
858
|
+
0.0, float(inv.hype_hyperevm_balance or 0.0) - MIN_HYPE_GAS
|
|
859
|
+
)
|
|
860
|
+
if swappable_hype > self._planner_config.min_hype_swap:
|
|
861
|
+
logger.info(
|
|
862
|
+
f"[SAFETY] Unallocated spot HYPE detected: {swappable_hype:.4f} HYPE"
|
|
863
|
+
)
|
|
864
|
+
ok, msg = await self._swap_hype_to_lst(
|
|
865
|
+
{"hype_amount": swappable_hype}, inv
|
|
866
|
+
)
|
|
867
|
+
if ok:
|
|
868
|
+
safety_messages.append(f"Swapped {swappable_hype:.2f} HYPE to LST")
|
|
869
|
+
inv = await self.observe() # Refresh after swap
|
|
870
|
+
|
|
871
|
+
# 2) Delta neutrality must hold
|
|
872
|
+
ok_delta, delta_msg = self._delta_neutral_ok(inv)
|
|
873
|
+
if not ok_delta:
|
|
874
|
+
logger.warning(f"[SAFETY] Delta imbalance detected: {delta_msg}")
|
|
875
|
+
target_short = inv.total_hype_exposure
|
|
876
|
+
ok, msg = await self._ensure_hl_short(
|
|
877
|
+
{
|
|
878
|
+
"target_size": target_short,
|
|
879
|
+
"current_size": inv.hl_short_size_hype,
|
|
880
|
+
},
|
|
881
|
+
inv,
|
|
882
|
+
)
|
|
883
|
+
if not ok:
|
|
884
|
+
if not self.simulation:
|
|
885
|
+
ok_fs, msg_fs = await self._failsafe_liquidate_all(
|
|
886
|
+
f"Failed to restore delta neutrality: {delta_msg} | hedge_err={msg}"
|
|
887
|
+
)
|
|
888
|
+
return ok_fs, msg_fs, rotated
|
|
889
|
+
return (
|
|
890
|
+
False,
|
|
891
|
+
f"{message} | SAFETY FAIL: {delta_msg} | hedge_err={msg}",
|
|
892
|
+
rotated,
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
# Recheck after hedge
|
|
896
|
+
inv = await self.observe()
|
|
897
|
+
ok_delta, delta_msg = self._delta_neutral_ok(inv)
|
|
898
|
+
if not ok_delta:
|
|
899
|
+
if not self.simulation:
|
|
900
|
+
ok_fs, msg_fs = await self._failsafe_liquidate_all(
|
|
901
|
+
f"Delta neutrality still broken after hedge: {delta_msg}"
|
|
902
|
+
)
|
|
903
|
+
return ok_fs, msg_fs, rotated
|
|
904
|
+
return (
|
|
905
|
+
False,
|
|
906
|
+
f"{message} | SAFETY FAIL after hedge: {delta_msg}",
|
|
907
|
+
rotated,
|
|
908
|
+
)
|
|
909
|
+
safety_messages.append("Delta-neutral hedge fixed")
|
|
910
|
+
|
|
911
|
+
# 3) Boros coverage should be ~85% (best effort, don't hard-fail)
|
|
912
|
+
ok_boros, boros_msg = self._boros_coverage_ok(inv, self._opa_boros_quotes)
|
|
913
|
+
if not ok_boros:
|
|
914
|
+
logger.info(f"[SAFETY] Boros coverage drift: {boros_msg}")
|
|
915
|
+
# Attempt fix; if Boros cannot be brought back into a sane state, melt down.
|
|
916
|
+
spot_usd = inv.total_hype_exposure * inv.hype_price_usd
|
|
917
|
+
target_usd = spot_usd * self._planner_config.boros_coverage_target
|
|
918
|
+
|
|
919
|
+
if self._planner_runtime.current_boros_market_id:
|
|
920
|
+
ok, msg = await self._ensure_boros_position(
|
|
921
|
+
{
|
|
922
|
+
"market_id": self._planner_runtime.current_boros_market_id,
|
|
923
|
+
"target_size_usd": target_usd,
|
|
924
|
+
},
|
|
925
|
+
inv,
|
|
926
|
+
)
|
|
927
|
+
if ok:
|
|
928
|
+
safety_messages.append(
|
|
929
|
+
f"Boros coverage adjusted to ${target_usd:.0f}"
|
|
930
|
+
)
|
|
931
|
+
else:
|
|
932
|
+
if not self.simulation:
|
|
933
|
+
ok_fs, msg_fs = await self._failsafe_liquidate_all(
|
|
934
|
+
f"Failed to restore Boros coverage: {boros_msg} | err={msg}"
|
|
935
|
+
)
|
|
936
|
+
return ok_fs, msg_fs, rotated
|
|
937
|
+
logger.warning(f"[SAFETY] Boros coverage fix failed: {msg}")
|
|
938
|
+
|
|
939
|
+
# Append safety messages to result
|
|
940
|
+
if safety_messages:
|
|
941
|
+
message = f"{message} | SAFETY: {'; '.join(safety_messages)}"
|
|
942
|
+
|
|
943
|
+
except Exception as e:
|
|
944
|
+
logger.error(f"Safety pass failed: {e}")
|
|
945
|
+
# Don't fail the whole update if safety pass has an exception
|
|
946
|
+
# The main OPA loop already succeeded
|
|
947
|
+
|
|
948
|
+
return success, message, rotated
|
|
949
|
+
|
|
950
|
+
async def _get_yield_info(self, inv: Inventory) -> YieldInfo:
|
|
951
|
+
yield_info = YieldInfo()
|
|
952
|
+
|
|
953
|
+
# Fetch external APYs in parallel
|
|
954
|
+
khype_apy, lhype_apy = await asyncio.gather(
|
|
955
|
+
fetch_khype_apy(), fetch_lhype_apy()
|
|
956
|
+
)
|
|
957
|
+
yield_info.khype_apy = khype_apy
|
|
958
|
+
yield_info.lhype_apy = lhype_apy
|
|
959
|
+
|
|
960
|
+
# Get Boros APR from active position
|
|
961
|
+
boros_notional = 0.0
|
|
962
|
+
if self.boros_adapter:
|
|
963
|
+
try:
|
|
964
|
+
success, positions = await self.boros_adapter.get_active_positions()
|
|
965
|
+
if success and positions:
|
|
966
|
+
pos = positions[0]
|
|
967
|
+
# fixedApr is the locked-in rate
|
|
968
|
+
fixed_apr = pos.get("fixedApr")
|
|
969
|
+
if fixed_apr is not None:
|
|
970
|
+
yield_info.boros_apr = float(fixed_apr)
|
|
971
|
+
boros_notional = abs(pos.get("notionalSizeFloat", 0) or 0)
|
|
972
|
+
if boros_notional == 0:
|
|
973
|
+
boros_notional = abs(inv.boros_position_size or 0)
|
|
974
|
+
except Exception as e:
|
|
975
|
+
logger.warning(f"Failed to get Boros APR: {e}")
|
|
976
|
+
|
|
977
|
+
# Calculate expected annual yields in USD
|
|
978
|
+
|
|
979
|
+
# kHYPE yield: APY applied to kHYPE value
|
|
980
|
+
if yield_info.khype_apy and inv.khype_value_usd > 0:
|
|
981
|
+
yield_info.khype_expected_yield_usd = (
|
|
982
|
+
inv.khype_value_usd * yield_info.khype_apy
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
# lHYPE yield: APY applied to lHYPE value
|
|
986
|
+
if yield_info.lhype_apy and inv.looped_hype_value_usd > 0:
|
|
987
|
+
yield_info.lhype_expected_yield_usd = (
|
|
988
|
+
inv.looped_hype_value_usd * yield_info.lhype_apy
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
# Boros yield: Locks in funding rate on perp position
|
|
992
|
+
if yield_info.boros_apr is not None and boros_notional > 0:
|
|
993
|
+
yield_info.boros_expected_yield_usd = yield_info.boros_apr * boros_notional
|
|
994
|
+
|
|
995
|
+
# Total expected yield
|
|
996
|
+
yield_info.total_expected_yield_usd = (
|
|
997
|
+
yield_info.khype_expected_yield_usd
|
|
998
|
+
+ yield_info.lhype_expected_yield_usd
|
|
999
|
+
+ yield_info.boros_expected_yield_usd
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
# Blended APY based on total value
|
|
1003
|
+
if inv.total_value > 0:
|
|
1004
|
+
yield_info.blended_apy = (
|
|
1005
|
+
yield_info.total_expected_yield_usd / inv.total_value
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
return yield_info
|
|
1009
|
+
|
|
1010
|
+
async def exit(self, **kwargs) -> StatusTuple:
|
|
1011
|
+
# Transfer remaining balances to main wallet (does NOT close positions - call withdraw() first)
|
|
1012
|
+
if not self.balance_adapter:
|
|
1013
|
+
return False, "Balance adapter not configured"
|
|
1014
|
+
|
|
1015
|
+
strategy_wallet = self._config.get("strategy_wallet", {})
|
|
1016
|
+
main_wallet = self._config.get("main_wallet", {})
|
|
1017
|
+
strategy_addr = strategy_wallet.get("address") if strategy_wallet else None
|
|
1018
|
+
main_addr = main_wallet.get("address") if main_wallet else None
|
|
1019
|
+
|
|
1020
|
+
if not strategy_addr or not main_addr:
|
|
1021
|
+
return False, "Strategy or main wallet address not configured"
|
|
1022
|
+
|
|
1023
|
+
if strategy_addr.lower() == main_addr.lower():
|
|
1024
|
+
return True, "Strategy wallet is main wallet, no transfer needed"
|
|
1025
|
+
|
|
1026
|
+
transferred = []
|
|
1027
|
+
|
|
1028
|
+
# Transfer USDC on Arbitrum
|
|
1029
|
+
ok, usdc_raw = await self.balance_adapter.get_vault_wallet_balance(USDC_ARB)
|
|
1030
|
+
if ok and isinstance(usdc_raw, int) and usdc_raw > 0:
|
|
1031
|
+
usdc_amount = usdc_raw / 1e6
|
|
1032
|
+
if usdc_amount > 0.01:
|
|
1033
|
+
(
|
|
1034
|
+
ok,
|
|
1035
|
+
msg,
|
|
1036
|
+
) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
|
|
1037
|
+
USDC_ARB, usdc_amount, "boros_hype_strategy", skip_ledger=True
|
|
1038
|
+
)
|
|
1039
|
+
if ok:
|
|
1040
|
+
transferred.append(f"{usdc_amount:.2f} USDC")
|
|
1041
|
+
else:
|
|
1042
|
+
return False, f"USDC transfer failed: {msg}"
|
|
1043
|
+
|
|
1044
|
+
# Transfer USDT on Arbitrum
|
|
1045
|
+
ok, usdt_raw = await self.balance_adapter.get_vault_wallet_balance(USDT_ARB)
|
|
1046
|
+
if ok and isinstance(usdt_raw, int) and usdt_raw > 0:
|
|
1047
|
+
usdt_amount = usdt_raw / 1e6
|
|
1048
|
+
if usdt_amount > 0.01:
|
|
1049
|
+
(
|
|
1050
|
+
ok,
|
|
1051
|
+
msg,
|
|
1052
|
+
) = await self.balance_adapter.move_from_strategy_wallet_to_main_wallet(
|
|
1053
|
+
USDT_ARB, usdt_amount, "boros_hype_strategy", skip_ledger=True
|
|
1054
|
+
)
|
|
1055
|
+
if ok:
|
|
1056
|
+
transferred.append(f"{usdt_amount:.2f} USDT")
|
|
1057
|
+
else:
|
|
1058
|
+
return False, f"USDT transfer failed: {msg}"
|
|
1059
|
+
|
|
1060
|
+
if transferred:
|
|
1061
|
+
return True, f"Transferred to main wallet: {', '.join(transferred)}"
|
|
1062
|
+
return True, "No balances to transfer"
|
|
1063
|
+
|
|
1064
|
+
async def _status(self) -> StatusDict:
|
|
1065
|
+
inv = await self.observe()
|
|
1066
|
+
alloc = self._get_allocation_status(inv)
|
|
1067
|
+
yield_info = await self._get_yield_info(inv)
|
|
1068
|
+
|
|
1069
|
+
# Build human-readable summary with full breakdown
|
|
1070
|
+
spot_parts = []
|
|
1071
|
+
if inv.khype_balance > 0.001:
|
|
1072
|
+
spot_parts.append(
|
|
1073
|
+
f"{inv.khype_balance:.4f} kHYPE (${inv.khype_value_usd:.2f})"
|
|
1074
|
+
)
|
|
1075
|
+
if inv.looped_hype_balance > 0.001:
|
|
1076
|
+
spot_parts.append(
|
|
1077
|
+
f"{inv.looped_hype_balance:.4f} lHYPE (${inv.looped_hype_value_usd:.2f})"
|
|
1078
|
+
)
|
|
1079
|
+
if inv.whype_balance > 0.001:
|
|
1080
|
+
spot_parts.append(
|
|
1081
|
+
f"{inv.whype_balance:.4f} WHYPE (${inv.whype_value_usd:.2f})"
|
|
1082
|
+
)
|
|
1083
|
+
if inv.hype_hyperevm_balance > 0.001:
|
|
1084
|
+
spot_parts.append(
|
|
1085
|
+
f"{inv.hype_hyperevm_balance:.4f} HYPE (${inv.hype_hyperevm_value_usd:.2f})"
|
|
1086
|
+
)
|
|
1087
|
+
if inv.hl_spot_hype > 0.001:
|
|
1088
|
+
spot_parts.append(
|
|
1089
|
+
f"{inv.hl_spot_hype:.4f} HYPE on HL spot (${inv.hl_spot_hype_value_usd:.2f})"
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
if spot_parts:
|
|
1093
|
+
spot_summary = (
|
|
1094
|
+
f"Spot: {' + '.join(spot_parts)} = "
|
|
1095
|
+
f"{inv.total_hype_exposure:.4f} HYPE equivalent (${inv.spot_value_usd:.2f})"
|
|
1096
|
+
)
|
|
1097
|
+
else:
|
|
1098
|
+
spot_summary = f"Spot: No HYPE exposure (${inv.spot_value_usd:.2f})"
|
|
1099
|
+
hl_summary = (
|
|
1100
|
+
f"Hyperliquid: ${inv.hl_perp_margin:.2f} margin, "
|
|
1101
|
+
f"{inv.hl_short_size_hype:.4f} HYPE short (${inv.hl_short_value_usd:.2f} notional)"
|
|
1102
|
+
)
|
|
1103
|
+
boros_summary = (
|
|
1104
|
+
f"Boros: ${inv.boros_collateral_usd:.2f} collateral, "
|
|
1105
|
+
f"{inv.boros_position_size:.2f} YU position"
|
|
1106
|
+
)
|
|
1107
|
+
|
|
1108
|
+
# Yield summary
|
|
1109
|
+
yield_parts = []
|
|
1110
|
+
if yield_info.khype_apy is not None:
|
|
1111
|
+
yield_parts.append(
|
|
1112
|
+
f"kHYPE: {yield_info.khype_apy * 100:.2f}% APY (${yield_info.khype_expected_yield_usd:.2f}/yr)"
|
|
1113
|
+
)
|
|
1114
|
+
if yield_info.lhype_apy is not None:
|
|
1115
|
+
yield_parts.append(
|
|
1116
|
+
f"lHYPE: {yield_info.lhype_apy * 100:.2f}% APY (${yield_info.lhype_expected_yield_usd:.2f}/yr)"
|
|
1117
|
+
)
|
|
1118
|
+
if yield_info.boros_apr is not None:
|
|
1119
|
+
yield_parts.append(
|
|
1120
|
+
f"Boros: {yield_info.boros_apr * 100:.2f}% APR locked (${yield_info.boros_expected_yield_usd:.2f}/yr)"
|
|
1121
|
+
)
|
|
1122
|
+
yield_summary = (
|
|
1123
|
+
"Yields: " + ", ".join(yield_parts) if yield_parts else "Yields: N/A"
|
|
1124
|
+
)
|
|
1125
|
+
|
|
1126
|
+
if yield_info.blended_apy is not None:
|
|
1127
|
+
yield_summary += f"\nBlended APY: {yield_info.blended_apy * 100:.2f}% (${yield_info.total_expected_yield_usd:.2f}/yr expected)"
|
|
1128
|
+
|
|
1129
|
+
strategy_summary = (
|
|
1130
|
+
f"{spot_summary}\n{hl_summary}\n{boros_summary}\n{yield_summary}"
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
return {
|
|
1134
|
+
"portfolio_value": inv.total_value,
|
|
1135
|
+
"net_deposit": 0.0,
|
|
1136
|
+
"strategy_summary": strategy_summary,
|
|
1137
|
+
"strategy_status": {
|
|
1138
|
+
"mode": "NORMAL",
|
|
1139
|
+
"allocations": {
|
|
1140
|
+
"spot": {
|
|
1141
|
+
"value": alloc.spot_value,
|
|
1142
|
+
"pct": alloc.spot_pct_actual,
|
|
1143
|
+
"target_pct": self.hedge_cfg.spot_pct,
|
|
1144
|
+
},
|
|
1145
|
+
"hyperliquid": {
|
|
1146
|
+
"value": alloc.hl_value,
|
|
1147
|
+
"pct": alloc.hl_pct_actual,
|
|
1148
|
+
"target_pct": self.hedge_cfg.hyperliquid_pct,
|
|
1149
|
+
},
|
|
1150
|
+
"boros": {
|
|
1151
|
+
"value": alloc.boros_value,
|
|
1152
|
+
"pct": alloc.boros_pct_actual,
|
|
1153
|
+
"target_pct": self.hedge_cfg.boros_pct,
|
|
1154
|
+
},
|
|
1155
|
+
},
|
|
1156
|
+
},
|
|
1157
|
+
"positions": {
|
|
1158
|
+
"spot": {
|
|
1159
|
+
"khype_balance": inv.khype_balance,
|
|
1160
|
+
"khype_value_usd": inv.khype_value_usd,
|
|
1161
|
+
"lhype_balance": inv.looped_hype_balance,
|
|
1162
|
+
"lhype_value_usd": inv.looped_hype_value_usd,
|
|
1163
|
+
"whype_balance": inv.whype_balance,
|
|
1164
|
+
"whype_value_usd": inv.whype_value_usd,
|
|
1165
|
+
"hype_hyperevm_balance": inv.hype_hyperevm_balance,
|
|
1166
|
+
"hype_hyperevm_value_usd": inv.hype_hyperevm_value_usd,
|
|
1167
|
+
"hl_spot_hype": inv.hl_spot_hype,
|
|
1168
|
+
"hl_spot_hype_value_usd": inv.hl_spot_hype_value_usd,
|
|
1169
|
+
"total_hype_equivalent": inv.total_hype_exposure,
|
|
1170
|
+
"total_spot_value_usd": inv.spot_value_usd,
|
|
1171
|
+
},
|
|
1172
|
+
"hyperliquid": {
|
|
1173
|
+
"perp_margin": inv.hl_perp_margin,
|
|
1174
|
+
"short_size_hype": inv.hl_short_size_hype,
|
|
1175
|
+
"short_value_usd": inv.hl_short_value_usd,
|
|
1176
|
+
},
|
|
1177
|
+
"boros": {
|
|
1178
|
+
"collateral_usd": inv.boros_collateral_usd,
|
|
1179
|
+
"position_size_yu": inv.boros_position_size,
|
|
1180
|
+
},
|
|
1181
|
+
},
|
|
1182
|
+
"yield_info": {
|
|
1183
|
+
"khype_apy": yield_info.khype_apy,
|
|
1184
|
+
"lhype_apy": yield_info.lhype_apy,
|
|
1185
|
+
"boros_apr": yield_info.boros_apr,
|
|
1186
|
+
"khype_expected_yield_usd": yield_info.khype_expected_yield_usd,
|
|
1187
|
+
"lhype_expected_yield_usd": yield_info.lhype_expected_yield_usd,
|
|
1188
|
+
"boros_expected_yield_usd": yield_info.boros_expected_yield_usd,
|
|
1189
|
+
"total_expected_yield_usd": yield_info.total_expected_yield_usd,
|
|
1190
|
+
"blended_apy": yield_info.blended_apy,
|
|
1191
|
+
},
|
|
1192
|
+
"gas_available": inv.hype_hyperevm_balance,
|
|
1193
|
+
"gassed_up": inv.hype_hyperevm_balance >= MIN_HYPE_GAS,
|
|
1194
|
+
}
|