wayfinder-paths 0.1.23__py3-none-any.whl → 0.1.25__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 +2 -0
- wayfinder_paths/adapters/balance_adapter/adapter.py +250 -0
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -11
- wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
- wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
- wayfinder_paths/adapters/boros_adapter/client.py +476 -0
- wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
- wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
- wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
- wayfinder_paths/adapters/boros_adapter/types.py +70 -0
- wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
- wayfinder_paths/adapters/brap_adapter/adapter.py +1 -1
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +161 -26
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +77 -13
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +585 -61
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +592 -400
- wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +126 -219
- wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
- wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
- wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
- wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
- wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
- wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
- wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
- wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
- wayfinder_paths/adapters/token_adapter/examples.json +0 -4
- wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
- wayfinder_paths/conftest.py +24 -17
- wayfinder_paths/core/__init__.py +2 -0
- wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
- wayfinder_paths/core/adapters/models.py +17 -7
- wayfinder_paths/core/clients/BRAPClient.py +1 -1
- wayfinder_paths/core/clients/TokenClient.py +47 -1
- wayfinder_paths/core/clients/WayfinderClient.py +1 -2
- wayfinder_paths/core/clients/protocols.py +21 -22
- wayfinder_paths/core/clients/test_ledger_client.py +448 -0
- wayfinder_paths/core/config.py +12 -0
- wayfinder_paths/core/constants/__init__.py +15 -0
- wayfinder_paths/core/constants/base.py +6 -1
- wayfinder_paths/core/constants/contracts.py +39 -26
- wayfinder_paths/core/constants/erc20_abi.py +0 -1
- wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
- wayfinder_paths/core/constants/hyperliquid.py +16 -0
- wayfinder_paths/core/constants/moonwell_abi.py +0 -15
- wayfinder_paths/core/engine/manifest.py +66 -0
- wayfinder_paths/core/strategies/Strategy.py +0 -61
- wayfinder_paths/core/strategies/__init__.py +10 -1
- wayfinder_paths/core/strategies/opa_loop.py +167 -0
- wayfinder_paths/core/utils/test_transaction.py +289 -0
- wayfinder_paths/core/utils/transaction.py +44 -1
- wayfinder_paths/core/utils/web3.py +3 -0
- wayfinder_paths/mcp/__init__.py +5 -0
- wayfinder_paths/mcp/preview.py +185 -0
- wayfinder_paths/mcp/scripting.py +84 -0
- wayfinder_paths/mcp/server.py +52 -0
- wayfinder_paths/mcp/state/profile_store.py +195 -0
- wayfinder_paths/mcp/state/store.py +89 -0
- wayfinder_paths/mcp/test_scripting.py +267 -0
- wayfinder_paths/mcp/tools/__init__.py +0 -0
- wayfinder_paths/mcp/tools/balances.py +290 -0
- wayfinder_paths/mcp/tools/discovery.py +158 -0
- wayfinder_paths/mcp/tools/execute.py +770 -0
- wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
- wayfinder_paths/mcp/tools/quotes.py +288 -0
- wayfinder_paths/mcp/tools/run_script.py +286 -0
- wayfinder_paths/mcp/tools/strategies.py +188 -0
- wayfinder_paths/mcp/tools/tokens.py +46 -0
- wayfinder_paths/mcp/tools/wallets.py +354 -0
- wayfinder_paths/mcp/utils.py +129 -0
- wayfinder_paths/policies/hyperliquid.py +1 -1
- wayfinder_paths/policies/lifi.py +18 -0
- wayfinder_paths/policies/util.py +8 -2
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +28 -119
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
- wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
- wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
- wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
- wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
- wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
- wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
- wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
- wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
- wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
- wayfinder_paths/strategies/boros_hype_strategy/test_strategy.py +202 -0
- wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
- wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +3 -12
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +7 -29
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +63 -40
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +0 -34
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +11 -34
- wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
- wayfinder_paths/tests/test_test_coverage.py +1 -4
- wayfinder_paths-0.1.25.dist-info/METADATA +377 -0
- wayfinder_paths-0.1.25.dist-info/RECORD +185 -0
- wayfinder_paths/scripts/create_strategy.py +0 -139
- wayfinder_paths/scripts/make_wallets.py +0 -142
- wayfinder_paths-0.1.23.dist-info/METADATA +0 -354
- wayfinder_paths-0.1.23.dist-info/RECORD +0 -120
- /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
- {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.25.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.25.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
"""Golden tests for the Boros HYPE planner.
|
|
2
|
+
|
|
3
|
+
These tests lock in the current Observe→Plan behavior so we can safely refactor
|
|
4
|
+
the strategy into smaller modules without changing what it *does*.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from wayfinder_paths.adapters.boros_adapter import BorosMarketQuote
|
|
12
|
+
from wayfinder_paths.strategies.boros_hype_strategy.constants import (
|
|
13
|
+
BOROS_HYPE_MARKET_ID,
|
|
14
|
+
BOROS_HYPE_TOKEN_ID,
|
|
15
|
+
HYPE_OFT_ADDRESS,
|
|
16
|
+
MIN_HYPE_GAS,
|
|
17
|
+
)
|
|
18
|
+
from wayfinder_paths.strategies.boros_hype_strategy.strategy import build_plan
|
|
19
|
+
from wayfinder_paths.strategies.boros_hype_strategy.types import (
|
|
20
|
+
AllocationStatus,
|
|
21
|
+
HedgeConfig,
|
|
22
|
+
Inventory,
|
|
23
|
+
PlannerConfig,
|
|
24
|
+
PlannerRuntime,
|
|
25
|
+
PlanOp,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _inv(**overrides) -> Inventory:
|
|
30
|
+
base = {
|
|
31
|
+
"hype_hyperevm_balance": 0.0,
|
|
32
|
+
"hype_hyperevm_value_usd": 0.0,
|
|
33
|
+
"whype_balance": 0.0,
|
|
34
|
+
"whype_value_usd": 0.0,
|
|
35
|
+
"khype_balance": 0.0,
|
|
36
|
+
"khype_value_usd": 0.0,
|
|
37
|
+
"looped_hype_balance": 0.0,
|
|
38
|
+
"looped_hype_value_usd": 0.0,
|
|
39
|
+
"usdc_arb_idle": 0.0,
|
|
40
|
+
"usdt_arb_idle": 0.0,
|
|
41
|
+
"eth_arb_balance": 0.0,
|
|
42
|
+
"hype_oft_arb_balance": 0.0,
|
|
43
|
+
"hype_oft_arb_value_usd": 0.0,
|
|
44
|
+
"hl_perp_margin": 0.0,
|
|
45
|
+
"hl_spot_usdc": 0.0,
|
|
46
|
+
"hl_spot_hype": 0.0,
|
|
47
|
+
"hl_spot_hype_value_usd": 0.0,
|
|
48
|
+
"hl_short_size_hype": 0.0,
|
|
49
|
+
"hl_short_value_usd": 0.0,
|
|
50
|
+
"hl_unrealized_pnl": 0.0,
|
|
51
|
+
"hl_withdrawable_usd": 0.0,
|
|
52
|
+
"boros_idle_collateral_isolated": 0.0,
|
|
53
|
+
"boros_idle_collateral_cross": 0.0,
|
|
54
|
+
"boros_collateral_hype": 0.0,
|
|
55
|
+
"boros_collateral_usd": 0.0,
|
|
56
|
+
"boros_pending_withdrawal_hype": 0.0,
|
|
57
|
+
"boros_pending_withdrawal_usd": 0.0,
|
|
58
|
+
"boros_committed_collateral_usd": 0.0,
|
|
59
|
+
"boros_position_size": 0.0,
|
|
60
|
+
"boros_position_value": 0.0,
|
|
61
|
+
"khype_to_hype_ratio": 1.0,
|
|
62
|
+
"looped_hype_to_hype_ratio": 1.0,
|
|
63
|
+
"hype_price_usd": 25.0,
|
|
64
|
+
"spot_value_usd": 0.0,
|
|
65
|
+
"total_hype_exposure": 0.0,
|
|
66
|
+
"total_value": 0.0,
|
|
67
|
+
"boros_position_market_ids": None,
|
|
68
|
+
}
|
|
69
|
+
base.update(overrides)
|
|
70
|
+
return Inventory(**base)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _alloc(**overrides) -> AllocationStatus:
|
|
74
|
+
base = {
|
|
75
|
+
"spot_value": 0.0,
|
|
76
|
+
"hl_value": 0.0,
|
|
77
|
+
"boros_value": 0.0,
|
|
78
|
+
"idle_value": 0.0,
|
|
79
|
+
"total_value": 1.0,
|
|
80
|
+
"spot_pct_actual": 0.0,
|
|
81
|
+
"hl_pct_actual": 0.0,
|
|
82
|
+
"boros_pct_actual": 0.0,
|
|
83
|
+
"spot_deviation": 0.0,
|
|
84
|
+
"hl_deviation": 0.0,
|
|
85
|
+
"boros_deviation": 0.0,
|
|
86
|
+
"spot_needed_usd": 0.0,
|
|
87
|
+
"hl_needed_usd": 0.0,
|
|
88
|
+
"boros_needed_usd": 0.0,
|
|
89
|
+
}
|
|
90
|
+
base.update(overrides)
|
|
91
|
+
return AllocationStatus(**base)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _one_hype_quote() -> list[BorosMarketQuote]:
|
|
95
|
+
return [
|
|
96
|
+
BorosMarketQuote(
|
|
97
|
+
market_id=BOROS_HYPE_MARKET_ID,
|
|
98
|
+
market_address="0x0000000000000000000000000000000000000000",
|
|
99
|
+
symbol="HYPE-USD",
|
|
100
|
+
underlying="HYPE",
|
|
101
|
+
tenor_days=7.0,
|
|
102
|
+
maturity_ts=1_800_000_000,
|
|
103
|
+
collateral_address=HYPE_OFT_ADDRESS,
|
|
104
|
+
collateral_token_id=BOROS_HYPE_TOKEN_ID,
|
|
105
|
+
tick_step=1,
|
|
106
|
+
mid_apr=0.10,
|
|
107
|
+
best_bid_apr=0.095,
|
|
108
|
+
best_ask_apr=0.105,
|
|
109
|
+
)
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def test_build_plan_pending_withdrawal_is_noop():
|
|
114
|
+
inv = _inv(total_value=1000.0, boros_pending_withdrawal_usd=5.0)
|
|
115
|
+
alloc = _alloc(total_value=1000.0)
|
|
116
|
+
runtime = PlannerRuntime()
|
|
117
|
+
hedge_cfg = HedgeConfig(
|
|
118
|
+
spot_pct=0.60,
|
|
119
|
+
khype_fraction=0.5,
|
|
120
|
+
looped_hype_fraction=0.5,
|
|
121
|
+
hyperliquid_pct=0.35,
|
|
122
|
+
boros_pct=0.05,
|
|
123
|
+
)
|
|
124
|
+
config = PlannerConfig()
|
|
125
|
+
|
|
126
|
+
plan = build_plan(
|
|
127
|
+
inv=inv,
|
|
128
|
+
alloc=alloc,
|
|
129
|
+
risk_progress=0.0,
|
|
130
|
+
hedge_cfg=hedge_cfg,
|
|
131
|
+
config=config,
|
|
132
|
+
runtime=runtime,
|
|
133
|
+
boros_quotes=_one_hype_quote(),
|
|
134
|
+
pending_withdrawal_completion=False,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
assert plan.steps == []
|
|
138
|
+
assert any("pending boros withdrawal" in msg.lower() for msg in plan.messages)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_build_plan_routes_capital_idle_usdc():
|
|
142
|
+
total = 1000.0
|
|
143
|
+
hedge_cfg = HedgeConfig(
|
|
144
|
+
spot_pct=0.60,
|
|
145
|
+
khype_fraction=0.5,
|
|
146
|
+
looped_hype_fraction=0.5,
|
|
147
|
+
hyperliquid_pct=0.35,
|
|
148
|
+
boros_pct=0.05,
|
|
149
|
+
)
|
|
150
|
+
config = PlannerConfig()
|
|
151
|
+
runtime = PlannerRuntime()
|
|
152
|
+
|
|
153
|
+
inv = _inv(
|
|
154
|
+
total_value=total,
|
|
155
|
+
usdc_arb_idle=total,
|
|
156
|
+
hype_hyperevm_balance=max(0.0, MIN_HYPE_GAS - 0.01), # force gas step
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
alloc = _alloc(
|
|
160
|
+
spot_value=0.0,
|
|
161
|
+
hl_value=0.0,
|
|
162
|
+
boros_value=0.0,
|
|
163
|
+
idle_value=total,
|
|
164
|
+
total_value=total,
|
|
165
|
+
spot_pct_actual=0.0,
|
|
166
|
+
hl_pct_actual=0.0,
|
|
167
|
+
boros_pct_actual=0.0,
|
|
168
|
+
spot_deviation=-0.60,
|
|
169
|
+
hl_deviation=-0.35,
|
|
170
|
+
boros_deviation=-0.05,
|
|
171
|
+
spot_needed_usd=600.0,
|
|
172
|
+
hl_needed_usd=350.0,
|
|
173
|
+
boros_needed_usd=50.0,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
plan = build_plan(
|
|
177
|
+
inv=inv,
|
|
178
|
+
alloc=alloc,
|
|
179
|
+
risk_progress=0.0,
|
|
180
|
+
hedge_cfg=hedge_cfg,
|
|
181
|
+
config=config,
|
|
182
|
+
runtime=runtime,
|
|
183
|
+
boros_quotes=_one_hype_quote(),
|
|
184
|
+
pending_withdrawal_completion=False,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
ops = [s.op for s in plan.steps]
|
|
188
|
+
assert ops == [
|
|
189
|
+
PlanOp.ENSURE_GAS_ON_HYPEREVM,
|
|
190
|
+
PlanOp.FUND_BOROS,
|
|
191
|
+
PlanOp.SEND_USDC_TO_HL,
|
|
192
|
+
PlanOp.BRIDGE_TO_HYPEREVM,
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
fund_step = plan.steps[1]
|
|
196
|
+
assert abs(fund_step.params["amount_usd"] - 50.0) < 1e-9 # 5% target
|
|
197
|
+
|
|
198
|
+
send_step = plan.steps[2]
|
|
199
|
+
assert abs(send_step.params["amount_usd"] - 1000.0) < 1e-9 # send all to HL
|
|
200
|
+
|
|
201
|
+
bridge_step = plan.steps[3]
|
|
202
|
+
assert abs(bridge_step.params["amount_usd"] - 600.0) < 1e-9
|
|
203
|
+
assert abs(bridge_step.params["reserve_hl_margin_usd"] - 350.0) < 1e-9
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_build_plan_sizes_hedge_and_boros_position():
|
|
207
|
+
total = 1000.0
|
|
208
|
+
hedge_cfg = HedgeConfig(
|
|
209
|
+
spot_pct=0.60,
|
|
210
|
+
khype_fraction=0.5,
|
|
211
|
+
looped_hype_fraction=0.5,
|
|
212
|
+
hyperliquid_pct=0.35,
|
|
213
|
+
boros_pct=0.05,
|
|
214
|
+
)
|
|
215
|
+
config = PlannerConfig()
|
|
216
|
+
runtime = PlannerRuntime()
|
|
217
|
+
|
|
218
|
+
inv = _inv(
|
|
219
|
+
total_value=total,
|
|
220
|
+
spot_value_usd=600.0,
|
|
221
|
+
hl_perp_margin=350.0,
|
|
222
|
+
boros_collateral_usd=50.0,
|
|
223
|
+
boros_committed_collateral_usd=50.0,
|
|
224
|
+
usdc_arb_idle=0.0,
|
|
225
|
+
usdt_arb_idle=0.0,
|
|
226
|
+
hype_hyperevm_balance=MIN_HYPE_GAS, # avoid gas + LST swap steps
|
|
227
|
+
total_hype_exposure=10.0,
|
|
228
|
+
hl_short_size_hype=0.0,
|
|
229
|
+
hype_price_usd=24.0, # avoids .5 rounding in target Boros size
|
|
230
|
+
boros_position_size=0.0,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
alloc = _alloc(
|
|
234
|
+
spot_value=600.0,
|
|
235
|
+
hl_value=350.0,
|
|
236
|
+
boros_value=50.0,
|
|
237
|
+
idle_value=0.0,
|
|
238
|
+
total_value=total,
|
|
239
|
+
spot_pct_actual=0.60,
|
|
240
|
+
hl_pct_actual=0.35,
|
|
241
|
+
boros_pct_actual=0.05,
|
|
242
|
+
spot_deviation=0.0,
|
|
243
|
+
hl_deviation=0.0,
|
|
244
|
+
boros_deviation=0.0,
|
|
245
|
+
spot_needed_usd=0.0,
|
|
246
|
+
hl_needed_usd=0.0,
|
|
247
|
+
boros_needed_usd=0.0,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
plan = build_plan(
|
|
251
|
+
inv=inv,
|
|
252
|
+
alloc=alloc,
|
|
253
|
+
risk_progress=0.0,
|
|
254
|
+
hedge_cfg=hedge_cfg,
|
|
255
|
+
config=config,
|
|
256
|
+
runtime=runtime,
|
|
257
|
+
boros_quotes=_one_hype_quote(),
|
|
258
|
+
pending_withdrawal_completion=False,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
ops = [s.op for s in plan.steps]
|
|
262
|
+
assert ops == [PlanOp.ENSURE_HL_SHORT, PlanOp.ENSURE_BOROS_POSITION]
|
|
263
|
+
|
|
264
|
+
hedge_step = plan.steps[0]
|
|
265
|
+
assert hedge_step.params == {"target_size": 10.0, "current_size": 0.0}
|
|
266
|
+
|
|
267
|
+
boros_step = plan.steps[1]
|
|
268
|
+
assert boros_step.params["market_id"] == BOROS_HYPE_MARKET_ID
|
|
269
|
+
assert abs(boros_step.params["target_size_usd"] - 240.0) < 1e-9
|
|
270
|
+
|
|
271
|
+
# Market selection should persist in runtime (hysteresis)
|
|
272
|
+
assert runtime.current_boros_market_id == BOROS_HYPE_MARKET_ID
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_build_plan_redeploy_mode_is_single_step():
|
|
276
|
+
total = 1000.0
|
|
277
|
+
runtime = PlannerRuntime()
|
|
278
|
+
hedge_cfg = HedgeConfig(
|
|
279
|
+
spot_pct=0.60,
|
|
280
|
+
khype_fraction=0.5,
|
|
281
|
+
looped_hype_fraction=0.5,
|
|
282
|
+
hyperliquid_pct=0.35,
|
|
283
|
+
boros_pct=0.05,
|
|
284
|
+
)
|
|
285
|
+
config = PlannerConfig()
|
|
286
|
+
|
|
287
|
+
inv = _inv(total_value=total, usdc_arb_idle=total)
|
|
288
|
+
alloc = _alloc(total_value=total)
|
|
289
|
+
|
|
290
|
+
plan = build_plan(
|
|
291
|
+
inv=inv,
|
|
292
|
+
alloc=alloc,
|
|
293
|
+
risk_progress=0.95, # above full_rebalance_threshold
|
|
294
|
+
hedge_cfg=hedge_cfg,
|
|
295
|
+
config=config,
|
|
296
|
+
runtime=runtime,
|
|
297
|
+
boros_quotes=_one_hype_quote(),
|
|
298
|
+
pending_withdrawal_completion=False,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
assert [s.op for s in plan.steps] == [PlanOp.CLOSE_AND_REDEPLOY]
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def test_build_plan_trim_mode_adds_partial_trim_first():
|
|
305
|
+
total = 1000.0
|
|
306
|
+
runtime = PlannerRuntime()
|
|
307
|
+
hedge_cfg = HedgeConfig(
|
|
308
|
+
spot_pct=0.60,
|
|
309
|
+
khype_fraction=0.5,
|
|
310
|
+
looped_hype_fraction=0.5,
|
|
311
|
+
hyperliquid_pct=0.35,
|
|
312
|
+
boros_pct=0.05,
|
|
313
|
+
)
|
|
314
|
+
config = PlannerConfig()
|
|
315
|
+
|
|
316
|
+
inv = _inv(
|
|
317
|
+
total_value=total,
|
|
318
|
+
spot_value_usd=600.0,
|
|
319
|
+
hype_price_usd=25.0,
|
|
320
|
+
hl_short_size_hype=10.0, # $250 notional short
|
|
321
|
+
hl_perp_margin=50.0, # intentionally low margin to force trim
|
|
322
|
+
hype_hyperevm_balance=MIN_HYPE_GAS, # avoid gas step
|
|
323
|
+
)
|
|
324
|
+
alloc = _alloc(total_value=total)
|
|
325
|
+
|
|
326
|
+
plan = build_plan(
|
|
327
|
+
inv=inv,
|
|
328
|
+
alloc=alloc,
|
|
329
|
+
risk_progress=0.80, # TRIM band
|
|
330
|
+
hedge_cfg=hedge_cfg,
|
|
331
|
+
config=config,
|
|
332
|
+
runtime=runtime,
|
|
333
|
+
boros_quotes=_one_hype_quote(),
|
|
334
|
+
pending_withdrawal_completion=False,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
assert plan.steps, "Expected at least one step in TRIM mode"
|
|
338
|
+
assert plan.steps[0].op == PlanOp.PARTIAL_TRIM_SPOT
|
|
339
|
+
assert plan.steps[0].params["trim_pct"] == pytest.approx(0.196875)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def test_build_plan_skips_boros_funding_when_collateral_already_committed():
|
|
343
|
+
total = 1000.0
|
|
344
|
+
runtime = PlannerRuntime()
|
|
345
|
+
hedge_cfg = HedgeConfig(
|
|
346
|
+
spot_pct=0.60,
|
|
347
|
+
khype_fraction=0.5,
|
|
348
|
+
looped_hype_fraction=0.5,
|
|
349
|
+
hyperliquid_pct=0.35,
|
|
350
|
+
boros_pct=0.05,
|
|
351
|
+
)
|
|
352
|
+
config = PlannerConfig()
|
|
353
|
+
|
|
354
|
+
inv = _inv(
|
|
355
|
+
total_value=total,
|
|
356
|
+
usdc_arb_idle=0.0,
|
|
357
|
+
usdt_arb_idle=0.0,
|
|
358
|
+
boros_committed_collateral_usd=60.0, # already above target
|
|
359
|
+
hype_hyperevm_balance=MIN_HYPE_GAS, # avoid gas step
|
|
360
|
+
)
|
|
361
|
+
alloc = _alloc(total_value=total)
|
|
362
|
+
|
|
363
|
+
plan = build_plan(
|
|
364
|
+
inv=inv,
|
|
365
|
+
alloc=alloc,
|
|
366
|
+
risk_progress=0.0,
|
|
367
|
+
hedge_cfg=hedge_cfg,
|
|
368
|
+
config=config,
|
|
369
|
+
runtime=runtime,
|
|
370
|
+
boros_quotes=_one_hype_quote(),
|
|
371
|
+
pending_withdrawal_completion=False,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
assert all(s.op != PlanOp.FUND_BOROS for s in plan.steps)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Tests for BorosHypeStrategy."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from unittest.mock import AsyncMock
|
|
7
|
+
|
|
8
|
+
# Ensure wayfinder-paths is on path for tests.test_utils import
|
|
9
|
+
_wayfinder_path_dir = Path(__file__).parent.parent.parent.resolve()
|
|
10
|
+
_wayfinder_path_str = str(_wayfinder_path_dir)
|
|
11
|
+
if _wayfinder_path_str not in sys.path:
|
|
12
|
+
sys.path.insert(0, _wayfinder_path_str)
|
|
13
|
+
elif sys.path.index(_wayfinder_path_str) > 0:
|
|
14
|
+
sys.path.remove(_wayfinder_path_str)
|
|
15
|
+
sys.path.insert(0, _wayfinder_path_str)
|
|
16
|
+
|
|
17
|
+
import pytest # noqa: E402
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
from tests.test_utils import get_canonical_examples, load_strategy_examples
|
|
21
|
+
except ImportError:
|
|
22
|
+
test_utils_path = Path(_wayfinder_path_dir) / "tests" / "test_utils.py"
|
|
23
|
+
spec = importlib.util.spec_from_file_location("tests.test_utils", test_utils_path)
|
|
24
|
+
test_utils = importlib.util.module_from_spec(spec)
|
|
25
|
+
spec.loader.exec_module(test_utils)
|
|
26
|
+
get_canonical_examples = test_utils.get_canonical_examples
|
|
27
|
+
load_strategy_examples = test_utils.load_strategy_examples
|
|
28
|
+
|
|
29
|
+
from wayfinder_paths.strategies.boros_hype_strategy.strategy import ( # noqa: E402
|
|
30
|
+
BorosHypeStrategy,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def strategy():
|
|
36
|
+
"""Create a strategy instance for testing with minimal config."""
|
|
37
|
+
mock_config = {
|
|
38
|
+
"main_wallet": {"address": "0x1234567890123456789012345678901234567890"},
|
|
39
|
+
"strategy_wallet": {"address": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
s = BorosHypeStrategy(
|
|
43
|
+
config=mock_config,
|
|
44
|
+
main_wallet=mock_config["main_wallet"],
|
|
45
|
+
strategy_wallet=mock_config["strategy_wallet"],
|
|
46
|
+
simulation=True,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Mock the Boros adapter
|
|
50
|
+
if hasattr(s, "boros_adapter") and s.boros_adapter:
|
|
51
|
+
s.boros_adapter.quote_markets_for_underlying = AsyncMock(
|
|
52
|
+
return_value=(True, [])
|
|
53
|
+
)
|
|
54
|
+
s.boros_adapter.get_account_balances = AsyncMock(
|
|
55
|
+
return_value=(True, {"total": 0.0, "cross": 0.0, "isolated": 0.0})
|
|
56
|
+
)
|
|
57
|
+
s.boros_adapter.get_active_positions = AsyncMock(return_value=(True, []))
|
|
58
|
+
|
|
59
|
+
return s
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.mark.asyncio
|
|
63
|
+
@pytest.mark.smoke
|
|
64
|
+
async def test_smoke(strategy):
|
|
65
|
+
"""REQUIRED: Basic smoke test - verifies strategy lifecycle."""
|
|
66
|
+
examples = load_strategy_examples(Path(__file__))
|
|
67
|
+
smoke_data = examples["smoke"]
|
|
68
|
+
|
|
69
|
+
await strategy.setup()
|
|
70
|
+
|
|
71
|
+
st = await strategy.status()
|
|
72
|
+
assert isinstance(st, dict)
|
|
73
|
+
assert "portfolio_value" in st or "net_deposit" in st or "strategy_status" in st
|
|
74
|
+
|
|
75
|
+
deposit_params = smoke_data.get("deposit", {})
|
|
76
|
+
ok, msg = await strategy.deposit(**deposit_params)
|
|
77
|
+
assert isinstance(ok, bool)
|
|
78
|
+
assert isinstance(msg, str)
|
|
79
|
+
|
|
80
|
+
# update() returns (success, message, rotated) - 3 values
|
|
81
|
+
update_result = await strategy.update(**smoke_data.get("update", {}))
|
|
82
|
+
ok = update_result[0]
|
|
83
|
+
assert isinstance(ok, bool)
|
|
84
|
+
|
|
85
|
+
ok, msg = await strategy.withdraw(**smoke_data.get("withdraw", {}))
|
|
86
|
+
assert isinstance(ok, bool)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.mark.asyncio
|
|
90
|
+
async def test_canonical_usage(strategy):
|
|
91
|
+
"""REQUIRED: Test canonical usage examples from examples.json."""
|
|
92
|
+
await strategy.setup()
|
|
93
|
+
|
|
94
|
+
examples = load_strategy_examples(Path(__file__))
|
|
95
|
+
canonical = get_canonical_examples(examples)
|
|
96
|
+
|
|
97
|
+
for example_name, example_data in canonical.items():
|
|
98
|
+
if "deposit" in example_data:
|
|
99
|
+
deposit_params = example_data.get("deposit", {})
|
|
100
|
+
ok, _ = await strategy.deposit(**deposit_params)
|
|
101
|
+
assert ok, f"Canonical example '{example_name}' deposit failed"
|
|
102
|
+
|
|
103
|
+
if "update" in example_data:
|
|
104
|
+
update_result = await strategy.update()
|
|
105
|
+
ok = update_result[0]
|
|
106
|
+
msg = update_result[1] if len(update_result) > 1 else ""
|
|
107
|
+
assert ok, f"Canonical example '{example_name}' update failed: {msg}"
|
|
108
|
+
|
|
109
|
+
if "status" in example_data:
|
|
110
|
+
st = await strategy.status()
|
|
111
|
+
assert isinstance(st, dict), (
|
|
112
|
+
f"Canonical example '{example_name}' status failed"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@pytest.mark.asyncio
|
|
117
|
+
async def test_error_cases(strategy):
|
|
118
|
+
"""OPTIONAL: Test error scenarios from examples.json."""
|
|
119
|
+
await strategy.setup()
|
|
120
|
+
|
|
121
|
+
examples = load_strategy_examples(Path(__file__))
|
|
122
|
+
|
|
123
|
+
for example_name, example_data in examples.items():
|
|
124
|
+
if isinstance(example_data, dict) and "expect" in example_data:
|
|
125
|
+
expect = example_data.get("expect", {})
|
|
126
|
+
|
|
127
|
+
if "deposit" in example_data:
|
|
128
|
+
deposit_params = example_data.get("deposit", {})
|
|
129
|
+
ok, _ = await strategy.deposit(**deposit_params)
|
|
130
|
+
|
|
131
|
+
if expect.get("success") is False:
|
|
132
|
+
assert ok is False, (
|
|
133
|
+
f"Expected {example_name} deposit to fail but it succeeded"
|
|
134
|
+
)
|
|
135
|
+
elif expect.get("success") is True:
|
|
136
|
+
assert ok is True, (
|
|
137
|
+
f"Expected {example_name} deposit to succeed but it failed"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@pytest.mark.asyncio
|
|
142
|
+
async def test_below_minimum_deposit(strategy):
|
|
143
|
+
"""Test deposit below minimum threshold fails."""
|
|
144
|
+
await strategy.setup()
|
|
145
|
+
|
|
146
|
+
ok, msg = await strategy.deposit(main_token_amount=50.0, gas_token_amount=0.01)
|
|
147
|
+
assert ok is False
|
|
148
|
+
assert "minimum" in msg.lower() or "150" in msg
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@pytest.mark.asyncio
|
|
152
|
+
async def test_opa_loop_structure(strategy):
|
|
153
|
+
"""Test OPA loop components are properly configured."""
|
|
154
|
+
await strategy.setup()
|
|
155
|
+
|
|
156
|
+
# Verify OPA config
|
|
157
|
+
config = strategy.opa_config
|
|
158
|
+
assert config.max_iterations_per_tick > 0
|
|
159
|
+
assert config.max_steps_per_iteration > 0
|
|
160
|
+
assert config.max_total_steps_per_tick > 0
|
|
161
|
+
|
|
162
|
+
# Verify inventory changing ops
|
|
163
|
+
ops = strategy.get_inventory_changing_ops()
|
|
164
|
+
assert len(ops) > 0
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@pytest.mark.asyncio
|
|
168
|
+
async def test_observe_returns_inventory(strategy):
|
|
169
|
+
"""Test observe() returns valid inventory."""
|
|
170
|
+
await strategy.setup()
|
|
171
|
+
|
|
172
|
+
inv = await strategy.observe()
|
|
173
|
+
assert inv is not None
|
|
174
|
+
assert hasattr(inv, "hype_hyperevm_balance")
|
|
175
|
+
assert hasattr(inv, "total_value")
|
|
176
|
+
assert hasattr(inv, "hype_price_usd")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@pytest.mark.asyncio
|
|
180
|
+
async def test_plan_returns_plan(strategy):
|
|
181
|
+
"""Test plan() returns valid plan."""
|
|
182
|
+
await strategy.setup()
|
|
183
|
+
|
|
184
|
+
inv = await strategy.observe()
|
|
185
|
+
plan = strategy.plan(inv)
|
|
186
|
+
|
|
187
|
+
assert plan is not None
|
|
188
|
+
assert hasattr(plan, "steps")
|
|
189
|
+
assert hasattr(plan, "desired_state")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@pytest.mark.asyncio
|
|
193
|
+
async def test_status_returns_expected_fields(strategy):
|
|
194
|
+
"""Test status() returns expected fields."""
|
|
195
|
+
await strategy.setup()
|
|
196
|
+
|
|
197
|
+
status = await strategy.status()
|
|
198
|
+
|
|
199
|
+
assert "portfolio_value" in status
|
|
200
|
+
assert "strategy_status" in status
|
|
201
|
+
assert "gas_available" in status
|
|
202
|
+
assert "gassed_up" in status
|