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.

Files changed (156) hide show
  1. wayfinder_paths/__init__.py +0 -4
  2. wayfinder_paths/adapters/balance_adapter/README.md +0 -1
  3. wayfinder_paths/adapters/balance_adapter/adapter.py +313 -167
  4. wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
  5. wayfinder_paths/adapters/balance_adapter/test_adapter.py +41 -124
  6. wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
  7. wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
  8. wayfinder_paths/adapters/boros_adapter/client.py +476 -0
  9. wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
  10. wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
  11. wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
  12. wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
  13. wayfinder_paths/adapters/boros_adapter/types.py +70 -0
  14. wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
  15. wayfinder_paths/adapters/brap_adapter/README.md +22 -75
  16. wayfinder_paths/adapters/brap_adapter/adapter.py +187 -576
  17. wayfinder_paths/adapters/brap_adapter/examples.json +21 -140
  18. wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
  19. wayfinder_paths/adapters/brap_adapter/test_adapter.py +6 -234
  20. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +180 -92
  21. wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
  22. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +82 -14
  23. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
  24. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +586 -61
  25. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
  26. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
  27. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
  28. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
  29. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
  30. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
  31. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
  32. wayfinder_paths/adapters/ledger_adapter/README.md +4 -1
  33. wayfinder_paths/adapters/ledger_adapter/adapter.py +3 -3
  34. wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
  35. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
  36. wayfinder_paths/adapters/moonwell_adapter/adapter.py +649 -547
  37. wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
  38. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +160 -239
  39. wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
  40. wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
  41. wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
  42. wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
  43. wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
  44. wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
  45. wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
  46. wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
  47. wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
  48. wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
  49. wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
  50. wayfinder_paths/adapters/token_adapter/adapter.py +14 -0
  51. wayfinder_paths/adapters/token_adapter/examples.json +0 -4
  52. wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
  53. wayfinder_paths/conftest.py +24 -17
  54. wayfinder_paths/core/__init__.py +0 -3
  55. wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
  56. wayfinder_paths/core/adapters/models.py +17 -7
  57. wayfinder_paths/core/clients/BRAPClient.py +4 -1
  58. wayfinder_paths/core/clients/ClientManager.py +0 -7
  59. wayfinder_paths/core/clients/LedgerClient.py +196 -172
  60. wayfinder_paths/core/clients/TokenClient.py +47 -1
  61. wayfinder_paths/core/clients/WayfinderClient.py +1 -3
  62. wayfinder_paths/core/clients/__init__.py +0 -5
  63. wayfinder_paths/core/clients/protocols.py +21 -35
  64. wayfinder_paths/core/clients/test_ledger_client.py +448 -0
  65. wayfinder_paths/core/config.py +10 -162
  66. wayfinder_paths/core/constants/__init__.py +73 -2
  67. wayfinder_paths/core/constants/base.py +8 -17
  68. wayfinder_paths/core/constants/chains.py +36 -0
  69. wayfinder_paths/core/constants/contracts.py +52 -0
  70. wayfinder_paths/core/constants/erc20_abi.py +0 -1
  71. wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
  72. wayfinder_paths/core/constants/hyperliquid.py +16 -0
  73. wayfinder_paths/core/constants/moonwell_abi.py +0 -15
  74. wayfinder_paths/core/constants/tokens.py +9 -0
  75. wayfinder_paths/core/engine/manifest.py +66 -0
  76. wayfinder_paths/core/strategies/Strategy.py +0 -71
  77. wayfinder_paths/core/strategies/__init__.py +10 -1
  78. wayfinder_paths/core/strategies/opa_loop.py +167 -0
  79. wayfinder_paths/core/utils/evm_helpers.py +5 -15
  80. wayfinder_paths/core/utils/test_transaction.py +289 -0
  81. wayfinder_paths/core/utils/tokens.py +28 -0
  82. wayfinder_paths/core/utils/transaction.py +57 -8
  83. wayfinder_paths/core/utils/web3.py +8 -3
  84. wayfinder_paths/mcp/__init__.py +5 -0
  85. wayfinder_paths/mcp/preview.py +185 -0
  86. wayfinder_paths/mcp/scripting.py +84 -0
  87. wayfinder_paths/mcp/server.py +52 -0
  88. wayfinder_paths/mcp/state/profile_store.py +195 -0
  89. wayfinder_paths/mcp/state/store.py +89 -0
  90. wayfinder_paths/mcp/test_scripting.py +267 -0
  91. wayfinder_paths/mcp/tools/__init__.py +0 -0
  92. wayfinder_paths/mcp/tools/balances.py +290 -0
  93. wayfinder_paths/mcp/tools/discovery.py +158 -0
  94. wayfinder_paths/mcp/tools/execute.py +770 -0
  95. wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
  96. wayfinder_paths/mcp/tools/quotes.py +288 -0
  97. wayfinder_paths/mcp/tools/run_script.py +286 -0
  98. wayfinder_paths/mcp/tools/strategies.py +188 -0
  99. wayfinder_paths/mcp/tools/tokens.py +46 -0
  100. wayfinder_paths/mcp/tools/wallets.py +354 -0
  101. wayfinder_paths/mcp/utils.py +129 -0
  102. wayfinder_paths/policies/enso.py +1 -2
  103. wayfinder_paths/policies/hyper_evm.py +6 -3
  104. wayfinder_paths/policies/hyperlend.py +1 -2
  105. wayfinder_paths/policies/hyperliquid.py +1 -1
  106. wayfinder_paths/policies/lifi.py +18 -0
  107. wayfinder_paths/policies/moonwell.py +12 -7
  108. wayfinder_paths/policies/prjx.py +1 -3
  109. wayfinder_paths/policies/util.py +8 -2
  110. wayfinder_paths/run_strategy.py +97 -300
  111. wayfinder_paths/strategies/basis_trading_strategy/constants.py +3 -1
  112. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +47 -133
  113. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
  114. wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
  115. wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
  116. wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
  117. wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
  118. wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
  119. wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
  120. wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
  121. wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
  122. wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
  123. wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
  124. wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
  125. wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
  126. wayfinder_paths/{templates/strategy → strategies/boros_hype_strategy}/test_strategy.py +99 -63
  127. wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
  128. wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
  129. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +15 -23
  130. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +27 -62
  131. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +84 -58
  132. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
  133. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +69 -164
  134. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +43 -76
  135. wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
  136. wayfinder_paths/tests/test_test_coverage.py +1 -4
  137. wayfinder_paths-0.1.24.dist-info/METADATA +378 -0
  138. wayfinder_paths-0.1.24.dist-info/RECORD +185 -0
  139. {wayfinder_paths-0.1.22.dist-info → wayfinder_paths-0.1.24.dist-info}/WHEEL +1 -1
  140. wayfinder_paths/core/clients/WalletClient.py +0 -41
  141. wayfinder_paths/core/engine/StrategyJob.py +0 -110
  142. wayfinder_paths/core/services/test_local_evm_txn.py +0 -145
  143. wayfinder_paths/scripts/create_strategy.py +0 -139
  144. wayfinder_paths/scripts/make_wallets.py +0 -142
  145. wayfinder_paths/templates/adapter/README.md +0 -150
  146. wayfinder_paths/templates/adapter/adapter.py +0 -16
  147. wayfinder_paths/templates/adapter/examples.json +0 -8
  148. wayfinder_paths/templates/adapter/test_adapter.py +0 -30
  149. wayfinder_paths/templates/strategy/README.md +0 -186
  150. wayfinder_paths/templates/strategy/examples.json +0 -11
  151. wayfinder_paths/templates/strategy/strategy.py +0 -35
  152. wayfinder_paths/tests/test_smoke_manifest.py +0 -63
  153. wayfinder_paths-0.1.22.dist-info/METADATA +0 -355
  154. wayfinder_paths-0.1.22.dist-info/RECORD +0 -129
  155. /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
  156. {wayfinder_paths-0.1.22.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)
@@ -1,19 +1,16 @@
1
+ """Tests for BorosHypeStrategy."""
2
+
3
+ import importlib.util
1
4
  import sys
2
5
  from pathlib import Path
3
-
4
- # TODO: Replace MyStrategy with your actual strategy class name
5
- from wayfinder_paths.strategies.your_strategy.strategy import (
6
- MyStrategy, # noqa: E402
7
- )
6
+ from unittest.mock import AsyncMock
8
7
 
9
8
  # Ensure wayfinder-paths is on path for tests.test_utils import
10
- # This is a workaround until conftest loading order is resolved
11
- _wayfinder_path_dir = Path(__file__).parent.parent.parent.parent.resolve()
9
+ _wayfinder_path_dir = Path(__file__).parent.parent.parent.resolve()
12
10
  _wayfinder_path_str = str(_wayfinder_path_dir)
13
11
  if _wayfinder_path_str not in sys.path:
14
12
  sys.path.insert(0, _wayfinder_path_str)
15
13
  elif sys.path.index(_wayfinder_path_str) > 0:
16
- # Move to front to take precedence
17
14
  sys.path.remove(_wayfinder_path_str)
18
15
  sys.path.insert(0, _wayfinder_path_str)
19
16
 
@@ -22,9 +19,6 @@ import pytest # noqa: E402
22
19
  try:
23
20
  from tests.test_utils import get_canonical_examples, load_strategy_examples
24
21
  except ImportError:
25
- # Fallback if path setup didn't work
26
- import importlib.util
27
-
28
22
  test_utils_path = Path(_wayfinder_path_dir) / "tests" / "test_utils.py"
29
23
  spec = importlib.util.spec_from_file_location("tests.test_utils", test_utils_path)
30
24
  test_utils = importlib.util.module_from_spec(spec)
@@ -32,61 +26,35 @@ except ImportError:
32
26
  get_canonical_examples = test_utils.get_canonical_examples
33
27
  load_strategy_examples = test_utils.load_strategy_examples
34
28
 
29
+ from wayfinder_paths.strategies.boros_hype_strategy.strategy import ( # noqa: E402
30
+ BorosHypeStrategy,
31
+ )
32
+
35
33
 
36
34
  @pytest.fixture
37
35
  def strategy():
36
+ """Create a strategy instance for testing with minimal config."""
38
37
  mock_config = {
39
38
  "main_wallet": {"address": "0x1234567890123456789012345678901234567890"},
40
39
  "strategy_wallet": {"address": "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"},
41
40
  }
42
41
 
43
- s = MyStrategy(
42
+ s = BorosHypeStrategy(
44
43
  config=mock_config,
45
44
  main_wallet=mock_config["main_wallet"],
46
45
  strategy_wallet=mock_config["strategy_wallet"],
46
+ simulation=True,
47
47
  )
48
48
 
49
- # TODO: Add mocking for your adapters here if needed
50
- # Example for balance_adapter:
51
- # if hasattr(s, "balance_adapter") and s.balance_adapter:
52
- # usdc_balance_mock = AsyncMock(return_value=(True, 60000000))
53
- # gas_balance_mock = AsyncMock(return_value=(True, 2000000000000000))
54
- #
55
- # def get_balance_side_effect(query, wallet_address, **kwargs):
56
- # token_id = query if isinstance(query, str) else (query or {}).get("token_id")
57
- # if token_id == "usd-coin-base" or token_id == "usd-coin":
58
- # elif token_id == "ethereum-base" or token_id == "ethereum":
59
- #
60
- # s.balance_adapter.get_balance = AsyncMock(
61
- # side_effect=get_balance_side_effect
62
- # )
63
-
64
- # Example for token_adapter:
65
- # if hasattr(s, "token_adapter") and s.token_adapter:
66
- # default_token = {
67
- # "id": "usd-coin-base",
68
- # "symbol": "USDC",
69
- # "name": "USD Coin",
70
- # "decimals": 6,
71
- # "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
72
- # "chain": {"code": "base", "id": 8453, "name": "Base"},
73
- # }
74
- # s.token_adapter.get_token = AsyncMock(return_value=(True, default_token))
75
- # s.token_adapter.get_gas_token = AsyncMock(return_value=(True, default_token))
76
-
77
- # Example for transaction adapters:
78
- # if hasattr(s, "tx_adapter") and s.tx_adapter:
79
- # s.tx_adapter.move_from_main_wallet_to_strategy_wallet = AsyncMock(
80
- # )
81
- # s.tx_adapter.move_from_strategy_wallet_to_main_wallet = AsyncMock(
82
- # )
83
-
84
- # Example for ledger_adapter:
85
- # if hasattr(s, "ledger_adapter") and s.ledger_adapter:
86
- # s.ledger_adapter.get_strategy_net_deposit = AsyncMock(
87
- # )
88
- # s.ledger_adapter.get_strategy_transactions = AsyncMock(
89
- # )
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, []))
90
58
 
91
59
  return s
92
60
 
@@ -94,9 +62,12 @@ def strategy():
94
62
  @pytest.mark.asyncio
95
63
  @pytest.mark.smoke
96
64
  async def test_smoke(strategy):
65
+ """REQUIRED: Basic smoke test - verifies strategy lifecycle."""
97
66
  examples = load_strategy_examples(Path(__file__))
98
67
  smoke_data = examples["smoke"]
99
68
 
69
+ await strategy.setup()
70
+
100
71
  st = await strategy.status()
101
72
  assert isinstance(st, dict)
102
73
  assert "portfolio_value" in st or "net_deposit" in st or "strategy_status" in st
@@ -106,7 +77,9 @@ async def test_smoke(strategy):
106
77
  assert isinstance(ok, bool)
107
78
  assert isinstance(msg, str)
108
79
 
109
- ok, msg = await strategy.update(**smoke_data.get("update", {}))
80
+ # update() returns (success, message, rotated) - 3 values
81
+ update_result = await strategy.update(**smoke_data.get("update", {}))
82
+ ok = update_result[0]
110
83
  assert isinstance(ok, bool)
111
84
 
112
85
  ok, msg = await strategy.withdraw(**smoke_data.get("withdraw", {}))
@@ -115,6 +88,9 @@ async def test_smoke(strategy):
115
88
 
116
89
  @pytest.mark.asyncio
117
90
  async def test_canonical_usage(strategy):
91
+ """REQUIRED: Test canonical usage examples from examples.json."""
92
+ await strategy.setup()
93
+
118
94
  examples = load_strategy_examples(Path(__file__))
119
95
  canonical = get_canonical_examples(examples)
120
96
 
@@ -125,7 +101,9 @@ async def test_canonical_usage(strategy):
125
101
  assert ok, f"Canonical example '{example_name}' deposit failed"
126
102
 
127
103
  if "update" in example_data:
128
- ok, msg = await strategy.update()
104
+ update_result = await strategy.update()
105
+ ok = update_result[0]
106
+ msg = update_result[1] if len(update_result) > 1 else ""
129
107
  assert ok, f"Canonical example '{example_name}' update failed: {msg}"
130
108
 
131
109
  if "status" in example_data:
@@ -137,6 +115,9 @@ async def test_canonical_usage(strategy):
137
115
 
138
116
  @pytest.mark.asyncio
139
117
  async def test_error_cases(strategy):
118
+ """OPTIONAL: Test error scenarios from examples.json."""
119
+ await strategy.setup()
120
+
140
121
  examples = load_strategy_examples(Path(__file__))
141
122
 
142
123
  for example_name, example_data in examples.items():
@@ -156,11 +137,66 @@ async def test_error_cases(strategy):
156
137
  f"Expected {example_name} deposit to succeed but it failed"
157
138
  )
158
139
 
159
- if "update" in example_data:
160
- ok, _ = await strategy.update()
161
- if "success" in expect:
162
- expected_success = expect.get("success")
163
- assert ok == expected_success, (
164
- f"Expected {example_name} update to "
165
- f"{'succeed' if expected_success else 'fail'} but got opposite"
166
- )
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