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,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
|
-
|
|
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 =
|
|
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
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|