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,460 @@
|
|
|
1
|
+
"""Planner for BorosHypeStrategy.
|
|
2
|
+
|
|
3
|
+
This module is intentionally mostly-pure: given the current Inventory snapshot
|
|
4
|
+
and configuration, it outputs a prioritized list of PlanOp steps.
|
|
5
|
+
|
|
6
|
+
Extracted from `strategy.py` to make the strategy easier to read/refactor and to
|
|
7
|
+
enable independent golden testing.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
from wayfinder_paths.adapters.boros_adapter import BorosMarketQuote
|
|
15
|
+
|
|
16
|
+
from .constants import BOROS_MIN_DEPOSIT_USD, BOROS_MIN_TENOR_DAYS, MIN_HYPE_GAS
|
|
17
|
+
from .types import (
|
|
18
|
+
AllocationStatus,
|
|
19
|
+
DesiredState,
|
|
20
|
+
HedgeConfig,
|
|
21
|
+
Inventory,
|
|
22
|
+
Mode,
|
|
23
|
+
Plan,
|
|
24
|
+
PlannerConfig,
|
|
25
|
+
PlannerRuntime,
|
|
26
|
+
PlanOp,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _choose_boros_market(
|
|
31
|
+
quotes: list[BorosMarketQuote],
|
|
32
|
+
runtime: PlannerRuntime,
|
|
33
|
+
config: PlannerConfig,
|
|
34
|
+
) -> tuple[int | None, str | None, float | None, int | None, str | None]:
|
|
35
|
+
# Hysteresis to prevent thrashing. Updates runtime state when a new market is selected.
|
|
36
|
+
if not quotes:
|
|
37
|
+
return None, None, None, None, None
|
|
38
|
+
|
|
39
|
+
valid_quotes = [q for q in quotes if q.market_id is not None]
|
|
40
|
+
if not valid_quotes:
|
|
41
|
+
return None, None, None, None, None
|
|
42
|
+
|
|
43
|
+
# Sort by tenor (prefer longer tenors for reduced rollover frequency)
|
|
44
|
+
sorted_quotes = sorted(
|
|
45
|
+
valid_quotes,
|
|
46
|
+
key=lambda q: (q.tenor_days or 0, q.mid_apr or 0),
|
|
47
|
+
reverse=True,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
sorted_quotes = [
|
|
51
|
+
q for q in sorted_quotes if (q.tenor_days or 0) >= BOROS_MIN_TENOR_DAYS
|
|
52
|
+
]
|
|
53
|
+
if not sorted_quotes:
|
|
54
|
+
return None, None, None, None, None
|
|
55
|
+
|
|
56
|
+
best = sorted_quotes[0]
|
|
57
|
+
|
|
58
|
+
if runtime.current_boros_market_id is None:
|
|
59
|
+
runtime.current_boros_market_id = best.market_id
|
|
60
|
+
runtime.current_boros_token_id = best.collateral_token_id
|
|
61
|
+
runtime.current_boros_collateral_address = best.collateral_address
|
|
62
|
+
runtime.boros_market_selected_at = datetime.utcnow()
|
|
63
|
+
return (
|
|
64
|
+
best.market_id,
|
|
65
|
+
best.symbol,
|
|
66
|
+
best.tenor_days,
|
|
67
|
+
best.collateral_token_id,
|
|
68
|
+
best.collateral_address,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
current = next(
|
|
72
|
+
(q for q in sorted_quotes if q.market_id == runtime.current_boros_market_id),
|
|
73
|
+
None,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if current is None: # Current market gone/invalid - switch to best
|
|
77
|
+
runtime.current_boros_market_id = best.market_id
|
|
78
|
+
runtime.current_boros_token_id = best.collateral_token_id
|
|
79
|
+
runtime.current_boros_collateral_address = best.collateral_address
|
|
80
|
+
runtime.boros_market_selected_at = datetime.utcnow()
|
|
81
|
+
return (
|
|
82
|
+
best.market_id,
|
|
83
|
+
best.symbol,
|
|
84
|
+
best.tenor_days,
|
|
85
|
+
best.collateral_token_id,
|
|
86
|
+
best.collateral_address,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if runtime.boros_market_selected_at: # Check cooldown
|
|
90
|
+
hours_since = (
|
|
91
|
+
datetime.utcnow() - runtime.boros_market_selected_at
|
|
92
|
+
).total_seconds() / 3600
|
|
93
|
+
if hours_since < config.boros_market_switch_cooldown_hours:
|
|
94
|
+
return (
|
|
95
|
+
current.market_id,
|
|
96
|
+
current.symbol,
|
|
97
|
+
current.tenor_days,
|
|
98
|
+
current.collateral_token_id,
|
|
99
|
+
current.collateral_address,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Only switch if APR improvement exceeds threshold
|
|
103
|
+
if best.market_id != current.market_id:
|
|
104
|
+
current_apr = current.mid_apr or 0
|
|
105
|
+
best_apr = best.mid_apr or 0
|
|
106
|
+
if best_apr - current_apr > config.boros_apr_improvement_threshold:
|
|
107
|
+
runtime.current_boros_market_id = best.market_id
|
|
108
|
+
runtime.current_boros_token_id = best.collateral_token_id
|
|
109
|
+
runtime.current_boros_collateral_address = best.collateral_address
|
|
110
|
+
runtime.boros_market_selected_at = datetime.utcnow()
|
|
111
|
+
return (
|
|
112
|
+
best.market_id,
|
|
113
|
+
best.symbol,
|
|
114
|
+
best.tenor_days,
|
|
115
|
+
best.collateral_token_id,
|
|
116
|
+
best.collateral_address,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
current.market_id,
|
|
121
|
+
current.symbol,
|
|
122
|
+
current.tenor_days,
|
|
123
|
+
current.collateral_token_id,
|
|
124
|
+
current.collateral_address,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def build_plan(
|
|
129
|
+
inv: Inventory,
|
|
130
|
+
alloc: AllocationStatus,
|
|
131
|
+
risk_progress: float,
|
|
132
|
+
hedge_cfg: HedgeConfig,
|
|
133
|
+
config: PlannerConfig,
|
|
134
|
+
runtime: PlannerRuntime,
|
|
135
|
+
boros_quotes: list[BorosMarketQuote],
|
|
136
|
+
*,
|
|
137
|
+
pending_withdrawal_completion: bool = False,
|
|
138
|
+
) -> Plan:
|
|
139
|
+
total = inv.total_value
|
|
140
|
+
|
|
141
|
+
# Pending withdrawal: do not run any actions in update().
|
|
142
|
+
if pending_withdrawal_completion or inv.boros_pending_withdrawal_usd > 0:
|
|
143
|
+
plan = Plan(
|
|
144
|
+
desired_state=DesiredState(
|
|
145
|
+
mode=Mode.NORMAL,
|
|
146
|
+
target_spot_usd=0,
|
|
147
|
+
target_hl_margin_usd=0,
|
|
148
|
+
target_boros_collateral_usd=0,
|
|
149
|
+
target_hype_short_size=0,
|
|
150
|
+
target_boros_position_usd=0,
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
plan.messages.append(
|
|
154
|
+
f"Pending Boros withdrawal detected (${inv.boros_pending_withdrawal_usd:.2f}); skipping plan"
|
|
155
|
+
)
|
|
156
|
+
return plan # Skip all other operations
|
|
157
|
+
|
|
158
|
+
# Check for HL liquidation detection
|
|
159
|
+
if inv.hl_liquidation_detected:
|
|
160
|
+
# Log the liquidation alert in plan messages
|
|
161
|
+
plan = Plan(
|
|
162
|
+
desired_state=DesiredState(
|
|
163
|
+
mode=Mode.REDEPLOY,
|
|
164
|
+
target_spot_usd=0,
|
|
165
|
+
target_hl_margin_usd=0,
|
|
166
|
+
target_boros_collateral_usd=0,
|
|
167
|
+
target_hype_short_size=0,
|
|
168
|
+
target_boros_position_usd=0,
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
plan.messages.append(
|
|
172
|
+
"[LIQUIDATION] HL short was liquidated - entering recovery mode"
|
|
173
|
+
)
|
|
174
|
+
# Add close and redeploy step
|
|
175
|
+
plan.add_step(
|
|
176
|
+
PlanOp.CLOSE_AND_REDEPLOY,
|
|
177
|
+
priority=0,
|
|
178
|
+
key="close_and_redeploy_post_liquidation",
|
|
179
|
+
reason="HL position was liquidated - need to rebalance",
|
|
180
|
+
)
|
|
181
|
+
return plan
|
|
182
|
+
|
|
183
|
+
# Determine mode based on risk
|
|
184
|
+
if risk_progress >= config.full_rebalance_threshold:
|
|
185
|
+
mode = Mode.REDEPLOY
|
|
186
|
+
elif risk_progress >= config.partial_trim_threshold:
|
|
187
|
+
mode = Mode.TRIM
|
|
188
|
+
else:
|
|
189
|
+
mode = Mode.NORMAL
|
|
190
|
+
|
|
191
|
+
# Gap 3: Boros enable/disable + min-deposit floor logic
|
|
192
|
+
# Disable Boros if:
|
|
193
|
+
# - Total AUM below minimum threshold
|
|
194
|
+
# - Pending withdrawal exists (prevents double-collateralizing)
|
|
195
|
+
# Note: funded_boros_this_tick only prevents repeated FUND_BOROS steps,
|
|
196
|
+
# not Boros positions/coverage entirely
|
|
197
|
+
boros_enabled = (
|
|
198
|
+
total >= config.min_total_for_boros and inv.boros_pending_withdrawal_usd <= 0
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Calculate target allocations
|
|
202
|
+
target_spot = total * hedge_cfg.spot_pct
|
|
203
|
+
target_hl = total * hedge_cfg.hyperliquid_pct
|
|
204
|
+
|
|
205
|
+
# Boros target: use MAX of percentage allocation and minimum deposit + buffer.
|
|
206
|
+
# We keep a small buffer so the USDC→USDT swap doesn't undershoot the Boros minimum.
|
|
207
|
+
target_boros = (
|
|
208
|
+
max(hedge_cfg.boros_pct * total, BOROS_MIN_DEPOSIT_USD + 1.0)
|
|
209
|
+
if boros_enabled
|
|
210
|
+
else 0.0
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Select Boros market with hysteresis
|
|
214
|
+
(
|
|
215
|
+
market_id,
|
|
216
|
+
market_symbol,
|
|
217
|
+
tenor_days,
|
|
218
|
+
boros_token_id,
|
|
219
|
+
boros_collateral_address,
|
|
220
|
+
) = _choose_boros_market(boros_quotes, runtime, config)
|
|
221
|
+
|
|
222
|
+
# Gap 5: Delta-neutral targeting - use actual spot exposure, not target
|
|
223
|
+
# Short target = current HYPE exposure (to hedge what we actually have)
|
|
224
|
+
target_hype_short = inv.total_hype_exposure
|
|
225
|
+
|
|
226
|
+
# Boros coverage target = percentage of spot exposure
|
|
227
|
+
target_boros_position = (
|
|
228
|
+
target_hype_short * inv.hype_price_usd * config.boros_coverage_target
|
|
229
|
+
if boros_enabled and market_id
|
|
230
|
+
else 0.0
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
desired = DesiredState(
|
|
234
|
+
mode=mode,
|
|
235
|
+
target_spot_usd=target_spot,
|
|
236
|
+
target_hl_margin_usd=target_hl,
|
|
237
|
+
target_boros_collateral_usd=target_boros,
|
|
238
|
+
target_hype_short_size=target_hype_short,
|
|
239
|
+
target_boros_position_usd=target_boros_position,
|
|
240
|
+
boros_market_id=market_id,
|
|
241
|
+
boros_market_symbol=market_symbol,
|
|
242
|
+
boros_tenor_days=tenor_days,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
plan = Plan(desired_state=desired)
|
|
246
|
+
|
|
247
|
+
# Priority 0: Safety - emergency actions
|
|
248
|
+
if mode == Mode.REDEPLOY:
|
|
249
|
+
plan.add_step(
|
|
250
|
+
PlanOp.CLOSE_AND_REDEPLOY,
|
|
251
|
+
priority=0,
|
|
252
|
+
key="close_and_redeploy",
|
|
253
|
+
reason=f"Risk at {risk_progress:.0%}, triggering full rebalance",
|
|
254
|
+
)
|
|
255
|
+
plan.sort_steps()
|
|
256
|
+
return plan
|
|
257
|
+
|
|
258
|
+
if mode == Mode.TRIM:
|
|
259
|
+
# Calculate margin shortfall instead of blunt 25% trim
|
|
260
|
+
# This prevents the whipsaw of "trim → deploy excess margin back"
|
|
261
|
+
short_notional = inv.hl_short_size_hype * inv.hype_price_usd
|
|
262
|
+
required_margin = short_notional * config.hl_target_margin_ratio # 50% for 2x
|
|
263
|
+
buffer_margin = short_notional * config.hl_margin_buffer_ratio # 15% buffer
|
|
264
|
+
current_margin = inv.hl_perp_margin
|
|
265
|
+
margin_shortfall = max(0, required_margin + buffer_margin - current_margin)
|
|
266
|
+
|
|
267
|
+
# Only trim what's needed (plus 5% buffer for slippage)
|
|
268
|
+
trim_amount_usd = margin_shortfall * 1.05 if margin_shortfall > 5 else 0
|
|
269
|
+
|
|
270
|
+
if trim_amount_usd > 0 and inv.spot_value_usd > 0:
|
|
271
|
+
# Convert to percentage of spot value, capped at 50%
|
|
272
|
+
trim_pct = min(trim_amount_usd / inv.spot_value_usd, 0.50)
|
|
273
|
+
if trim_pct > 0.02: # Only trim if > 2%
|
|
274
|
+
plan.add_step(
|
|
275
|
+
PlanOp.PARTIAL_TRIM_SPOT,
|
|
276
|
+
priority=0,
|
|
277
|
+
key="partial_trim_spot",
|
|
278
|
+
params={"trim_pct": trim_pct},
|
|
279
|
+
reason=f"Risk at {risk_progress:.0%} - need ${margin_shortfall:.2f} margin",
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Priority 5: Gas routing (must happen before capital routing)
|
|
283
|
+
if inv.hype_hyperevm_balance < MIN_HYPE_GAS:
|
|
284
|
+
plan.add_step(
|
|
285
|
+
PlanOp.ENSURE_GAS_ON_HYPEREVM,
|
|
286
|
+
priority=5,
|
|
287
|
+
key="ensure_gas_hyperevm",
|
|
288
|
+
params={"min_hype": MIN_HYPE_GAS},
|
|
289
|
+
reason=f"HyperEVM HYPE balance ({inv.hype_hyperevm_balance:.4f}) below minimum gas",
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
# Gap 4: Priority 10 - Capital routing with virtual ledger
|
|
293
|
+
# Use runtime.available_usdc_arb() to avoid double-spending within a tick
|
|
294
|
+
|
|
295
|
+
# Fund Boros (highest priority among capital routing)
|
|
296
|
+
# Check funded_boros_this_tick to prevent repeated funding in same tick.
|
|
297
|
+
#
|
|
298
|
+
# IMPORTANT: Use the Boros target (includes minimum+buffer) instead of pct-based allocation,
|
|
299
|
+
# otherwise small deposits get skipped (Boros min deposit).
|
|
300
|
+
if boros_enabled and not runtime.funded_boros_this_tick:
|
|
301
|
+
boros_shortfall = max(
|
|
302
|
+
0.0, target_boros - float(inv.boros_committed_collateral_usd or 0.0)
|
|
303
|
+
)
|
|
304
|
+
if boros_shortfall >= config.min_usdt_action:
|
|
305
|
+
if boros_shortfall >= BOROS_MIN_DEPOSIT_USD:
|
|
306
|
+
plan.add_step(
|
|
307
|
+
PlanOp.FUND_BOROS,
|
|
308
|
+
priority=10,
|
|
309
|
+
key=f"fund_boros_{boros_shortfall:.2f}",
|
|
310
|
+
params={
|
|
311
|
+
"amount_usd": float(boros_shortfall),
|
|
312
|
+
"market_id": int(market_id) if market_id else None,
|
|
313
|
+
"token_id": int(boros_token_id) if boros_token_id else None,
|
|
314
|
+
"collateral_address": str(boros_collateral_address or ""),
|
|
315
|
+
},
|
|
316
|
+
reason=f"Funding Boros toward target (${target_boros:.2f}) via HYPE collateral",
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Send USDC to Hyperliquid ONCE to cover both:
|
|
320
|
+
# - HL margin allocation
|
|
321
|
+
# - HyperEVM spot deployment pipeline (paired fill + bridge)
|
|
322
|
+
need_hl = alloc.hl_deviation < -config.allocation_deviation_threshold
|
|
323
|
+
need_spot = alloc.spot_deviation < -config.allocation_deviation_threshold
|
|
324
|
+
available_usdc = runtime.available_usdc_arb(inv.usdc_arb_idle)
|
|
325
|
+
if available_usdc > config.min_usdc_action and (need_hl or need_spot):
|
|
326
|
+
# New routing: always send all Arbitrum USDC to Hyperliquid first.
|
|
327
|
+
amount = float(available_usdc)
|
|
328
|
+
plan.add_step(
|
|
329
|
+
PlanOp.SEND_USDC_TO_HL,
|
|
330
|
+
priority=10,
|
|
331
|
+
key=f"send_usdc_hl_{amount:.0f}",
|
|
332
|
+
params={"amount_usd": amount},
|
|
333
|
+
reason="Routing Arbitrum USDC to Hyperliquid (primary venue)",
|
|
334
|
+
)
|
|
335
|
+
runtime.commit_usdc(amount) # Mark as committed
|
|
336
|
+
|
|
337
|
+
# Bridge to HyperEVM for spot using HL margin (the HL deposit is handled by SEND_USDC_TO_HL).
|
|
338
|
+
if need_spot:
|
|
339
|
+
plan.add_step(
|
|
340
|
+
PlanOp.BRIDGE_TO_HYPEREVM,
|
|
341
|
+
priority=10,
|
|
342
|
+
key=f"bridge_hyperevm_{alloc.spot_needed_usd:.0f}",
|
|
343
|
+
params={
|
|
344
|
+
"amount_usd": float(alloc.spot_needed_usd or 0.0),
|
|
345
|
+
# Keep target HL margin in perp while transferring to spot for the paired fill.
|
|
346
|
+
"reserve_hl_margin_usd": float(target_hl),
|
|
347
|
+
},
|
|
348
|
+
reason=f"Spot underallocated by {abs(alloc.spot_deviation):.1%}",
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Deploy excess HL margin to spot if margin ratio is too high
|
|
352
|
+
if inv.hl_short_size_hype > 0:
|
|
353
|
+
short_notional = inv.hl_short_size_hype * inv.hype_price_usd
|
|
354
|
+
current_margin_ratio = (
|
|
355
|
+
inv.hl_perp_margin / short_notional if short_notional > 0 else 0
|
|
356
|
+
)
|
|
357
|
+
target_margin_ratio = (
|
|
358
|
+
config.hl_target_margin_ratio + config.hl_margin_buffer_ratio
|
|
359
|
+
) # ~65%
|
|
360
|
+
if current_margin_ratio > target_margin_ratio + 0.10: # 10% excess
|
|
361
|
+
excess_margin = (
|
|
362
|
+
current_margin_ratio - target_margin_ratio
|
|
363
|
+
) * short_notional
|
|
364
|
+
if excess_margin > config.min_usdc_action:
|
|
365
|
+
plan.add_step(
|
|
366
|
+
PlanOp.DEPLOY_EXCESS_HL_MARGIN,
|
|
367
|
+
priority=13,
|
|
368
|
+
key=f"deploy_excess_margin_{excess_margin:.0f}",
|
|
369
|
+
params={"excess_margin_usd": excess_margin},
|
|
370
|
+
reason=f"Deploy ${excess_margin:.2f} excess HL margin to spot",
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Transfer HYPE stuck on HL spot to HyperEVM
|
|
374
|
+
if inv.hl_spot_hype > 0.1:
|
|
375
|
+
plan.add_step(
|
|
376
|
+
PlanOp.TRANSFER_HL_SPOT_TO_HYPEREVM,
|
|
377
|
+
priority=14,
|
|
378
|
+
key=f"transfer_hl_spot_{inv.hl_spot_hype:.4f}",
|
|
379
|
+
params={"hype_amount": inv.hl_spot_hype},
|
|
380
|
+
reason=f"Transfer {inv.hl_spot_hype:.4f} HYPE from HL spot to HyperEVM",
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
# Priority 20: Position management
|
|
384
|
+
# Swap unallocated HYPE to LSTs (leave MIN_HYPE_GAS for gas)
|
|
385
|
+
swappable_hype = max(0.0, inv.hype_hyperevm_balance - MIN_HYPE_GAS)
|
|
386
|
+
if swappable_hype > config.min_hype_swap:
|
|
387
|
+
plan.add_step(
|
|
388
|
+
PlanOp.SWAP_HYPE_TO_LST,
|
|
389
|
+
priority=20,
|
|
390
|
+
key=f"swap_hype_lst_{swappable_hype:.2f}",
|
|
391
|
+
params={"hype_amount": swappable_hype},
|
|
392
|
+
reason=f"Converting {swappable_hype:.2f} HYPE to yield-bearing LST",
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
# Gap 5: Ensure delta neutral short using actual exposure
|
|
396
|
+
# Use combined absolute + relative tolerance to avoid dust chasing
|
|
397
|
+
current_short = inv.hl_short_size_hype
|
|
398
|
+
short_delta = abs(target_hype_short - current_short)
|
|
399
|
+
|
|
400
|
+
# Combined tolerance: max of absolute and relative
|
|
401
|
+
tolerance = max(
|
|
402
|
+
config.delta_neutral_abs_tol_hype,
|
|
403
|
+
target_hype_short * config.delta_neutral_rel_tol,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
if short_delta > tolerance and target_hype_short > 0.1:
|
|
407
|
+
plan.add_step(
|
|
408
|
+
PlanOp.ENSURE_HL_SHORT,
|
|
409
|
+
priority=20,
|
|
410
|
+
key=f"ensure_hl_short_{target_hype_short:.2f}",
|
|
411
|
+
params={"target_size": target_hype_short, "current_size": current_short},
|
|
412
|
+
reason=f"Delta imbalance: short={current_short:.4f}, exposure={target_hype_short:.4f}",
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Priority 30: Rate positions (Boros)
|
|
416
|
+
if boros_enabled and market_id:
|
|
417
|
+
current_boros_size = inv.boros_position_size
|
|
418
|
+
size_diff = abs(target_boros_position - current_boros_size)
|
|
419
|
+
|
|
420
|
+
# Trigger if:
|
|
421
|
+
# 1. Position size needs adjustment (resize threshold met)
|
|
422
|
+
# 2. There's isolated collateral that needs to move to cross (market expiry/rotation)
|
|
423
|
+
# 3. There's cross collateral but no position (collateral sitting idle)
|
|
424
|
+
needs_position_resize = size_diff > config.boros_resize_min_excess_usd
|
|
425
|
+
isolated_usd = float(inv.boros_idle_collateral_isolated or 0.0) * float(
|
|
426
|
+
inv.hype_price_usd or 0.0
|
|
427
|
+
)
|
|
428
|
+
cross_usd = float(inv.boros_idle_collateral_cross or 0.0) * float(
|
|
429
|
+
inv.hype_price_usd or 0.0
|
|
430
|
+
)
|
|
431
|
+
has_stranded_isolated = isolated_usd > 0.5 # $0.50 threshold
|
|
432
|
+
has_idle_cross = cross_usd > 1.0 and current_boros_size < 1.0
|
|
433
|
+
|
|
434
|
+
if needs_position_resize or has_stranded_isolated or has_idle_cross:
|
|
435
|
+
reason = f"Adjusting Boros position to {target_boros_position:.0f} YU"
|
|
436
|
+
if has_stranded_isolated and not needs_position_resize:
|
|
437
|
+
reason = (
|
|
438
|
+
"Moving "
|
|
439
|
+
f"{inv.boros_idle_collateral_isolated:.6f} HYPE (≈${isolated_usd:.2f}) "
|
|
440
|
+
"from isolated to cross margin"
|
|
441
|
+
)
|
|
442
|
+
elif has_idle_cross and not needs_position_resize:
|
|
443
|
+
reason = (
|
|
444
|
+
"Deploying "
|
|
445
|
+
f"{inv.boros_idle_collateral_cross:.6f} HYPE (≈${cross_usd:.2f}) "
|
|
446
|
+
"idle cross collateral as rate position"
|
|
447
|
+
)
|
|
448
|
+
plan.add_step(
|
|
449
|
+
PlanOp.ENSURE_BOROS_POSITION,
|
|
450
|
+
priority=20, # Same as ENSURE_HL_SHORT so both execute before re-observe
|
|
451
|
+
key=f"ensure_boros_pos_{target_boros_position:.0f}",
|
|
452
|
+
params={
|
|
453
|
+
"market_id": market_id,
|
|
454
|
+
"target_size_usd": target_boros_position,
|
|
455
|
+
},
|
|
456
|
+
reason=reason,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
plan.sort_steps()
|
|
460
|
+
return plan
|