wayfinder-paths 0.1.23__py3-none-any.whl → 0.1.24__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of wayfinder-paths might be problematic. Click here for more details.

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