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,460 @@
|
|
|
1
|
+
"""Tests for BorosAdapter."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import AsyncMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from wayfinder_paths.adapters.boros_adapter.adapter import (
|
|
8
|
+
BorosAdapter,
|
|
9
|
+
BorosLimitOrder,
|
|
10
|
+
BorosMarketQuote,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestBorosAdapter:
|
|
15
|
+
"""Test cases for BorosAdapter."""
|
|
16
|
+
|
|
17
|
+
@pytest.fixture
|
|
18
|
+
def mock_boros_client(self):
|
|
19
|
+
"""Mock BorosClient for testing."""
|
|
20
|
+
mock_client = AsyncMock()
|
|
21
|
+
return mock_client
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def adapter(self, mock_boros_client):
|
|
25
|
+
"""Create a BorosAdapter instance with mocked client for testing."""
|
|
26
|
+
mock_config = {
|
|
27
|
+
"strategy_wallet": {
|
|
28
|
+
"address": "0x1234567890123456789012345678901234567890"
|
|
29
|
+
},
|
|
30
|
+
"boros_adapter": {},
|
|
31
|
+
}
|
|
32
|
+
with patch(
|
|
33
|
+
"wayfinder_paths.adapters.boros_adapter.adapter.BorosClient",
|
|
34
|
+
return_value=mock_boros_client,
|
|
35
|
+
):
|
|
36
|
+
adapter = BorosAdapter(
|
|
37
|
+
config=mock_config,
|
|
38
|
+
simulation=True,
|
|
39
|
+
)
|
|
40
|
+
adapter.boros_client = mock_boros_client
|
|
41
|
+
return adapter
|
|
42
|
+
|
|
43
|
+
def test_adapter_type(self, adapter):
|
|
44
|
+
"""Test adapter has correct type."""
|
|
45
|
+
assert adapter.adapter_type == "BOROS"
|
|
46
|
+
|
|
47
|
+
@pytest.mark.asyncio
|
|
48
|
+
async def test_connect_success(self, adapter, mock_boros_client):
|
|
49
|
+
"""Test successful connection."""
|
|
50
|
+
mock_boros_client.list_markets = AsyncMock(
|
|
51
|
+
return_value=[{"marketId": 1, "symbol": "HYPE-USD"}]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
result = await adapter.connect()
|
|
55
|
+
assert result is True
|
|
56
|
+
|
|
57
|
+
@pytest.mark.asyncio
|
|
58
|
+
async def test_connect_failure(self, adapter, mock_boros_client):
|
|
59
|
+
"""Test connection failure."""
|
|
60
|
+
mock_boros_client.list_markets = AsyncMock(side_effect=Exception("API Error"))
|
|
61
|
+
|
|
62
|
+
result = await adapter.connect()
|
|
63
|
+
assert result is False
|
|
64
|
+
|
|
65
|
+
@pytest.mark.asyncio
|
|
66
|
+
async def test_list_markets_success(self, adapter, mock_boros_client):
|
|
67
|
+
"""Test successful market listing."""
|
|
68
|
+
mock_response = [
|
|
69
|
+
{"marketId": 1, "symbol": "HYPE-USD", "underlying": "HYPE"},
|
|
70
|
+
{"marketId": 2, "symbol": "BTC-USD", "underlying": "BTC"},
|
|
71
|
+
]
|
|
72
|
+
mock_boros_client.list_markets = AsyncMock(return_value=mock_response)
|
|
73
|
+
|
|
74
|
+
success, markets = await adapter.list_markets()
|
|
75
|
+
|
|
76
|
+
assert success is True
|
|
77
|
+
assert len(markets) == 2
|
|
78
|
+
assert markets[0]["marketId"] == 1
|
|
79
|
+
|
|
80
|
+
@pytest.mark.asyncio
|
|
81
|
+
async def test_list_markets_failure(self, adapter, mock_boros_client):
|
|
82
|
+
"""Test market listing failure."""
|
|
83
|
+
mock_boros_client.list_markets = AsyncMock(side_effect=Exception("API Error"))
|
|
84
|
+
|
|
85
|
+
success, data = await adapter.list_markets()
|
|
86
|
+
|
|
87
|
+
assert success is False
|
|
88
|
+
assert "API Error" in str(data)
|
|
89
|
+
|
|
90
|
+
@pytest.mark.asyncio
|
|
91
|
+
async def test_get_market_success(self, adapter, mock_boros_client):
|
|
92
|
+
"""Test successful single market fetch."""
|
|
93
|
+
mock_response = {
|
|
94
|
+
"marketId": 18,
|
|
95
|
+
"symbol": "HYPERLIQUID-HYPE-USD",
|
|
96
|
+
"underlying": "HYPE",
|
|
97
|
+
}
|
|
98
|
+
mock_boros_client.get_market = AsyncMock(return_value=mock_response)
|
|
99
|
+
|
|
100
|
+
success, market = await adapter.get_market(18)
|
|
101
|
+
|
|
102
|
+
assert success is True
|
|
103
|
+
assert market["marketId"] == 18
|
|
104
|
+
|
|
105
|
+
@pytest.mark.asyncio
|
|
106
|
+
async def test_get_orderbook_success(self, adapter, mock_boros_client):
|
|
107
|
+
"""Test successful orderbook fetch."""
|
|
108
|
+
mock_response = {
|
|
109
|
+
"long": {"ia": [100, 105, 110], "sz": [1000, 2000, 3000]},
|
|
110
|
+
"short": {"ia": [115, 120, 125], "sz": [1500, 2500, 3500]},
|
|
111
|
+
}
|
|
112
|
+
mock_boros_client.get_order_book = AsyncMock(return_value=mock_response)
|
|
113
|
+
|
|
114
|
+
success, book = await adapter.get_orderbook(18)
|
|
115
|
+
|
|
116
|
+
assert success is True
|
|
117
|
+
assert "long" in book
|
|
118
|
+
assert "short" in book
|
|
119
|
+
|
|
120
|
+
@pytest.mark.asyncio
|
|
121
|
+
async def test_quote_market_success(self, adapter, mock_boros_client):
|
|
122
|
+
"""Test successful market quote."""
|
|
123
|
+
mock_market = {
|
|
124
|
+
"marketId": 18,
|
|
125
|
+
"address": "0xabcd",
|
|
126
|
+
"symbol": "HYPERLIQUID-HYPE-USD",
|
|
127
|
+
"underlying": "HYPE",
|
|
128
|
+
"imData": {"tickStep": 1, "maturity": 1735689600},
|
|
129
|
+
"tokenId": 3,
|
|
130
|
+
}
|
|
131
|
+
mock_orderbook = {
|
|
132
|
+
"long": {"ia": [100, 105]},
|
|
133
|
+
"short": {"ia": [115, 120]},
|
|
134
|
+
}
|
|
135
|
+
mock_boros_client.get_order_book = AsyncMock(return_value=mock_orderbook)
|
|
136
|
+
|
|
137
|
+
success, quote = await adapter.quote_market(mock_market)
|
|
138
|
+
|
|
139
|
+
assert success is True
|
|
140
|
+
assert isinstance(quote, BorosMarketQuote)
|
|
141
|
+
assert quote.market_id == 18
|
|
142
|
+
assert quote.best_bid_apr == 0.105 # max(long) * tick_size
|
|
143
|
+
assert quote.best_ask_apr == 0.115 # min(short) * tick_size
|
|
144
|
+
|
|
145
|
+
@pytest.mark.asyncio
|
|
146
|
+
async def test_quote_markets_for_underlying_success(
|
|
147
|
+
self, adapter, mock_boros_client
|
|
148
|
+
):
|
|
149
|
+
"""Test quoting markets for underlying."""
|
|
150
|
+
mock_markets = [
|
|
151
|
+
{
|
|
152
|
+
"marketId": 18,
|
|
153
|
+
"symbol": "HYPERLIQUID-HYPE-USD-30D",
|
|
154
|
+
"underlying": "HYPE",
|
|
155
|
+
"imData": {"maturity": 1735689600},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"marketId": 19,
|
|
159
|
+
"symbol": "HYPERLIQUID-HYPE-USD-60D",
|
|
160
|
+
"underlying": "HYPE",
|
|
161
|
+
"imData": {"maturity": 1738368000},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"marketId": 20,
|
|
165
|
+
"symbol": "BTC-USD-30D",
|
|
166
|
+
"underlying": "BTC",
|
|
167
|
+
"imData": {"maturity": 1735689600},
|
|
168
|
+
},
|
|
169
|
+
]
|
|
170
|
+
mock_boros_client.list_markets = AsyncMock(return_value=mock_markets)
|
|
171
|
+
mock_boros_client.get_order_book = AsyncMock(
|
|
172
|
+
return_value={"long": {"ia": [100]}, "short": {"ia": [110]}}
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
success, quotes = await adapter.quote_markets_for_underlying("HYPE")
|
|
176
|
+
|
|
177
|
+
assert success is True
|
|
178
|
+
assert len(quotes) == 2 # Only HYPE markets
|
|
179
|
+
assert all(q.underlying == "HYPE" for q in quotes)
|
|
180
|
+
|
|
181
|
+
@pytest.mark.asyncio
|
|
182
|
+
async def test_get_collaterals_success(self, adapter, mock_boros_client):
|
|
183
|
+
"""Test collateral fetch."""
|
|
184
|
+
mock_response = {
|
|
185
|
+
"collaterals": [
|
|
186
|
+
{
|
|
187
|
+
"tokenId": 3,
|
|
188
|
+
"crossPosition": {"netBalance": "100000000000000000000"},
|
|
189
|
+
"isolatedPositions": [],
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
mock_boros_client.get_collaterals = AsyncMock(return_value=mock_response)
|
|
194
|
+
|
|
195
|
+
success, data = await adapter.get_collaterals()
|
|
196
|
+
|
|
197
|
+
assert success is True
|
|
198
|
+
assert "collaterals" in data
|
|
199
|
+
|
|
200
|
+
@pytest.mark.asyncio
|
|
201
|
+
async def test_get_account_balances_success(self, adapter, mock_boros_client):
|
|
202
|
+
"""Test account balance fetch."""
|
|
203
|
+
mock_response = {
|
|
204
|
+
"collaterals": [
|
|
205
|
+
{
|
|
206
|
+
"tokenId": 3,
|
|
207
|
+
"crossPosition": {"availableBalance": "100000000000000000000"},
|
|
208
|
+
"isolatedPositions": [{"availableBalance": "50000000000000000000"}],
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
}
|
|
212
|
+
mock_boros_client.get_collaterals = AsyncMock(return_value=mock_response)
|
|
213
|
+
|
|
214
|
+
success, balances = await adapter.get_account_balances(token_id=3)
|
|
215
|
+
|
|
216
|
+
assert success is True
|
|
217
|
+
assert balances["cross"] == 100.0
|
|
218
|
+
assert balances["isolated"] == 50.0
|
|
219
|
+
assert balances["total"] == 150.0
|
|
220
|
+
|
|
221
|
+
@pytest.mark.asyncio
|
|
222
|
+
async def test_get_active_positions_success(self, adapter, mock_boros_client):
|
|
223
|
+
"""Test active positions fetch."""
|
|
224
|
+
mock_response = {
|
|
225
|
+
"collaterals": [
|
|
226
|
+
{
|
|
227
|
+
"tokenId": 3,
|
|
228
|
+
"crossPosition": {
|
|
229
|
+
"marketPositions": [
|
|
230
|
+
{
|
|
231
|
+
"marketId": 18,
|
|
232
|
+
"side": 1,
|
|
233
|
+
"sizeWei": "1000000000000000000",
|
|
234
|
+
}
|
|
235
|
+
]
|
|
236
|
+
},
|
|
237
|
+
"isolatedPositions": [],
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
}
|
|
241
|
+
mock_boros_client.get_collaterals = AsyncMock(return_value=mock_response)
|
|
242
|
+
|
|
243
|
+
success, positions = await adapter.get_active_positions(market_id=18)
|
|
244
|
+
|
|
245
|
+
assert success is True
|
|
246
|
+
assert len(positions) == 1
|
|
247
|
+
assert positions[0]["marketId"] == 18
|
|
248
|
+
assert positions[0]["size"] == 1.0
|
|
249
|
+
|
|
250
|
+
@pytest.mark.asyncio
|
|
251
|
+
async def test_get_open_limit_orders_success(self, adapter, mock_boros_client):
|
|
252
|
+
"""Test open orders fetch."""
|
|
253
|
+
mock_response = [
|
|
254
|
+
{
|
|
255
|
+
"orderId": "order-123",
|
|
256
|
+
"marketId": 18,
|
|
257
|
+
"side": 1,
|
|
258
|
+
"size": "1000000000000000000",
|
|
259
|
+
"filledSize": "500000000000000000",
|
|
260
|
+
"limitTick": 100,
|
|
261
|
+
"tickStep": 1,
|
|
262
|
+
"status": "open",
|
|
263
|
+
}
|
|
264
|
+
]
|
|
265
|
+
mock_boros_client.get_open_orders = AsyncMock(return_value=mock_response)
|
|
266
|
+
|
|
267
|
+
success, orders = await adapter.get_open_limit_orders()
|
|
268
|
+
|
|
269
|
+
assert success is True
|
|
270
|
+
assert len(orders) == 1
|
|
271
|
+
assert isinstance(orders[0], BorosLimitOrder)
|
|
272
|
+
assert orders[0].order_id == "order-123"
|
|
273
|
+
assert orders[0].size == 1.0
|
|
274
|
+
assert orders[0].filled_size == 0.5
|
|
275
|
+
assert orders[0].remaining_size == 0.5
|
|
276
|
+
|
|
277
|
+
@pytest.mark.asyncio
|
|
278
|
+
async def test_get_full_user_state_success(self, adapter, mock_boros_client):
|
|
279
|
+
market_acc = "0x" + ("0" * 58) + "000012" # 0x12 == 18
|
|
280
|
+
mock_boros_client.get_collaterals = AsyncMock(
|
|
281
|
+
return_value={
|
|
282
|
+
"collaterals": [
|
|
283
|
+
{
|
|
284
|
+
"tokenId": 3,
|
|
285
|
+
"crossPosition": {
|
|
286
|
+
"availableBalance": "1000000000000000000",
|
|
287
|
+
"marketPositions": [
|
|
288
|
+
{
|
|
289
|
+
"marketId": 18,
|
|
290
|
+
"side": 1,
|
|
291
|
+
"notionalSize": "1000000000000000000",
|
|
292
|
+
"pnl": {
|
|
293
|
+
"unrealisedPnl": "0",
|
|
294
|
+
"rateSettlementPnl": "0",
|
|
295
|
+
},
|
|
296
|
+
}
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
"isolatedPositions": [
|
|
300
|
+
{
|
|
301
|
+
"availableBalance": "2000000000000000000",
|
|
302
|
+
"marketAcc": market_acc,
|
|
303
|
+
"marketPositions": [
|
|
304
|
+
{
|
|
305
|
+
"marketId": 18,
|
|
306
|
+
"side": 0,
|
|
307
|
+
"notionalSize": "2000000000000000000",
|
|
308
|
+
"pnl": {
|
|
309
|
+
"unrealisedPnl": "0",
|
|
310
|
+
"rateSettlementPnl": "0",
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
],
|
|
314
|
+
}
|
|
315
|
+
],
|
|
316
|
+
"withdrawal": {
|
|
317
|
+
"lastWithdrawalRequestTime": 0,
|
|
318
|
+
"lastWithdrawalAmount": 0,
|
|
319
|
+
},
|
|
320
|
+
}
|
|
321
|
+
]
|
|
322
|
+
}
|
|
323
|
+
)
|
|
324
|
+
mock_boros_client.get_open_orders = AsyncMock(
|
|
325
|
+
return_value=[
|
|
326
|
+
{
|
|
327
|
+
"orderId": "order-1",
|
|
328
|
+
"marketId": 18,
|
|
329
|
+
"side": 1,
|
|
330
|
+
"size": "1000000000000000000",
|
|
331
|
+
"filledSize": "0",
|
|
332
|
+
"limitTick": 100,
|
|
333
|
+
"tickStep": 1,
|
|
334
|
+
"status": "open",
|
|
335
|
+
}
|
|
336
|
+
]
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
ok, state = await adapter.get_full_user_state(include_withdrawal_status=False)
|
|
340
|
+
assert ok is True
|
|
341
|
+
assert state["protocol"] == "boros"
|
|
342
|
+
assert state["chainId"] == adapter.chain_id
|
|
343
|
+
assert "collaterals" in state
|
|
344
|
+
assert state["balances"]["total"] == 3.0
|
|
345
|
+
assert len(state["positions"]) == 2
|
|
346
|
+
assert len(state["openOrders"]) == 1
|
|
347
|
+
|
|
348
|
+
@pytest.mark.asyncio
|
|
349
|
+
async def test_deposit_to_cross_margin_simulation(self, adapter):
|
|
350
|
+
"""Test deposit in simulation mode."""
|
|
351
|
+
success, result = await adapter.deposit_to_cross_margin(
|
|
352
|
+
collateral_address="0xabcd",
|
|
353
|
+
amount_wei=1000000, # 1 USDT
|
|
354
|
+
token_id=3,
|
|
355
|
+
market_id=18,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
assert success is True
|
|
359
|
+
assert result["status"] == "simulated"
|
|
360
|
+
|
|
361
|
+
@pytest.mark.asyncio
|
|
362
|
+
async def test_withdraw_collateral_simulation(self, adapter):
|
|
363
|
+
"""Test withdraw in simulation mode."""
|
|
364
|
+
success, result = await adapter.withdraw_collateral(
|
|
365
|
+
token_id=3,
|
|
366
|
+
amount_wei=1000000000000000000,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
assert success is True
|
|
370
|
+
assert result["status"] == "simulated"
|
|
371
|
+
|
|
372
|
+
@pytest.mark.asyncio
|
|
373
|
+
async def test_place_rate_order_simulation(self, adapter):
|
|
374
|
+
"""Test place order in simulation mode."""
|
|
375
|
+
success, result = await adapter.place_rate_order(
|
|
376
|
+
market_id=18,
|
|
377
|
+
token_id=3,
|
|
378
|
+
size_yu_wei=1000000000000000000,
|
|
379
|
+
side="short",
|
|
380
|
+
limit_tick=100,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
assert success is True
|
|
384
|
+
assert result["status"] == "simulated"
|
|
385
|
+
|
|
386
|
+
@pytest.mark.asyncio
|
|
387
|
+
async def test_close_positions_market_simulation(self, adapter, mock_boros_client):
|
|
388
|
+
"""Test close position in simulation mode."""
|
|
389
|
+
mock_response = {
|
|
390
|
+
"collaterals": [
|
|
391
|
+
{
|
|
392
|
+
"tokenId": 3,
|
|
393
|
+
"crossPosition": {
|
|
394
|
+
"marketPositions": [
|
|
395
|
+
{
|
|
396
|
+
"marketId": 18,
|
|
397
|
+
"side": 1,
|
|
398
|
+
"sizeWei": "1000000000000000000",
|
|
399
|
+
}
|
|
400
|
+
]
|
|
401
|
+
},
|
|
402
|
+
"isolatedPositions": [],
|
|
403
|
+
}
|
|
404
|
+
]
|
|
405
|
+
}
|
|
406
|
+
mock_boros_client.get_collaterals = AsyncMock(return_value=mock_response)
|
|
407
|
+
|
|
408
|
+
success, result = await adapter.close_positions_market(market_id=18)
|
|
409
|
+
|
|
410
|
+
assert success is True
|
|
411
|
+
assert result["status"] == "simulated"
|
|
412
|
+
|
|
413
|
+
@pytest.mark.asyncio
|
|
414
|
+
async def test_cancel_orders_simulation(self, adapter):
|
|
415
|
+
"""Test cancel orders in simulation mode."""
|
|
416
|
+
success, result = await adapter.cancel_orders(
|
|
417
|
+
market_id=18,
|
|
418
|
+
cancel_all=True,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
assert success is True
|
|
422
|
+
assert result["status"] == "simulated"
|
|
423
|
+
|
|
424
|
+
def test_tick_from_rate(self):
|
|
425
|
+
"""Test APR to tick conversion."""
|
|
426
|
+
# 10% APR with tick_step=1
|
|
427
|
+
tick = BorosAdapter.tick_from_rate(0.10, tick_step=1, round_down=False)
|
|
428
|
+
assert tick > 0
|
|
429
|
+
|
|
430
|
+
# Verify roundtrip
|
|
431
|
+
rate_back = BorosAdapter.rate_from_tick(tick, tick_step=1)
|
|
432
|
+
assert abs(rate_back - 0.10) < 0.001
|
|
433
|
+
|
|
434
|
+
def test_rate_from_tick(self):
|
|
435
|
+
"""Test tick to APR conversion."""
|
|
436
|
+
rate = BorosAdapter.rate_from_tick(954, tick_step=1)
|
|
437
|
+
assert rate > 0
|
|
438
|
+
assert rate < 1 # Should be a decimal
|
|
439
|
+
|
|
440
|
+
# Negative tick
|
|
441
|
+
rate_neg = BorosAdapter.rate_from_tick(-954, tick_step=1)
|
|
442
|
+
assert rate_neg < 0
|
|
443
|
+
|
|
444
|
+
def test_normalize_apr(self):
|
|
445
|
+
"""Test APR normalization."""
|
|
446
|
+
# Already decimal
|
|
447
|
+
assert BorosAdapter.normalize_apr(0.10) == 0.10
|
|
448
|
+
|
|
449
|
+
# Percent (values between 1 and 1000)
|
|
450
|
+
assert BorosAdapter.normalize_apr(10.0) == 0.10
|
|
451
|
+
|
|
452
|
+
# BPS (values > 1000)
|
|
453
|
+
assert BorosAdapter.normalize_apr(1100) == 0.11 # 1100 bps = 11%
|
|
454
|
+
|
|
455
|
+
# 1e18 scaled
|
|
456
|
+
result = BorosAdapter.normalize_apr(100000000000000000)
|
|
457
|
+
assert abs(result - 0.10) < 0.001
|
|
458
|
+
|
|
459
|
+
# None
|
|
460
|
+
assert BorosAdapter.normalize_apr(None) is None
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Golden tests for BorosAdapter parsing/quoting behavior.
|
|
2
|
+
|
|
3
|
+
These are meant to be stable regression tests during refactors (types/utils split).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from unittest.mock import AsyncMock, patch
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
from wayfinder_paths.adapters.boros_adapter.adapter import BorosAdapter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def mock_boros_client():
|
|
17
|
+
return AsyncMock()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def adapter(mock_boros_client):
|
|
22
|
+
mock_config = {
|
|
23
|
+
"strategy_wallet": {"address": "0x1234567890123456789012345678901234567890"},
|
|
24
|
+
"boros_adapter": {},
|
|
25
|
+
}
|
|
26
|
+
with patch(
|
|
27
|
+
"wayfinder_paths.adapters.boros_adapter.adapter.BorosClient",
|
|
28
|
+
return_value=mock_boros_client,
|
|
29
|
+
):
|
|
30
|
+
a = BorosAdapter(config=mock_config, simulation=True)
|
|
31
|
+
a.boros_client = mock_boros_client
|
|
32
|
+
return a
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.asyncio
|
|
36
|
+
async def test_quote_markets_for_underlying_golden(adapter, mock_boros_client):
|
|
37
|
+
# Make tenor_days deterministic (quote_market uses current wall-clock time).
|
|
38
|
+
adapter._time_to_maturity_days = lambda maturity_ts: (maturity_ts or 0) / 1000.0 # type: ignore[method-assign]
|
|
39
|
+
|
|
40
|
+
mock_markets = [
|
|
41
|
+
{
|
|
42
|
+
"marketId": 2,
|
|
43
|
+
"address": "0x2222",
|
|
44
|
+
"tokenId": 3,
|
|
45
|
+
"imData": {
|
|
46
|
+
"symbol": "HYPERLIQUID-HYPE-USD",
|
|
47
|
+
"underlying": "HYPE",
|
|
48
|
+
"maturity": 2000,
|
|
49
|
+
"tickStep": 7,
|
|
50
|
+
"collateral": "0xUSDT",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"marketId": 1,
|
|
55
|
+
"address": "0x1111",
|
|
56
|
+
"tokenId": 3,
|
|
57
|
+
"imData": {
|
|
58
|
+
"symbol": "HYPERLIQUID-HYPE-USD",
|
|
59
|
+
"underlying": "HYPE",
|
|
60
|
+
"maturity": 1000,
|
|
61
|
+
"tickStep": 5,
|
|
62
|
+
"collateral": "0xUSDT",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
# Same underlying but wrong platform should be filtered out when platform is set.
|
|
66
|
+
{
|
|
67
|
+
"marketId": 3,
|
|
68
|
+
"address": "0x3333",
|
|
69
|
+
"tokenId": 3,
|
|
70
|
+
"imData": {
|
|
71
|
+
"symbol": "OTHER-HYPE-USD",
|
|
72
|
+
"underlying": "HYPE",
|
|
73
|
+
"maturity": 1500,
|
|
74
|
+
"tickStep": 1,
|
|
75
|
+
"collateral": "0xUSDT",
|
|
76
|
+
},
|
|
77
|
+
"metadata": {"platformName": "OTHER"},
|
|
78
|
+
"platform": {"name": "OTHER"},
|
|
79
|
+
},
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
async def _get_order_book(market_id: int, tick_size: float = 0.001):
|
|
83
|
+
if market_id == 1:
|
|
84
|
+
return {"long": {"ia": [100, 110]}, "short": {"ia": [120, 130]}}
|
|
85
|
+
if market_id == 2:
|
|
86
|
+
return {"long": {"ia": [200]}, "short": {"ia": [250]}}
|
|
87
|
+
return {"long": {"ia": [1]}, "short": {"ia": [2]}}
|
|
88
|
+
|
|
89
|
+
mock_boros_client.list_markets = AsyncMock(return_value=mock_markets)
|
|
90
|
+
mock_boros_client.get_order_book = AsyncMock(side_effect=_get_order_book)
|
|
91
|
+
|
|
92
|
+
ok, quotes = await adapter.quote_markets_for_underlying(
|
|
93
|
+
"HYPE", platform="hyperliquid", tick_size=0.001
|
|
94
|
+
)
|
|
95
|
+
assert ok is True
|
|
96
|
+
|
|
97
|
+
# Only the two Hyperliquid-tagged markets should remain.
|
|
98
|
+
assert [q.market_id for q in quotes] == [1, 2]
|
|
99
|
+
|
|
100
|
+
q1 = quotes[0]
|
|
101
|
+
assert q1.underlying == "HYPE"
|
|
102
|
+
assert q1.symbol == "HYPERLIQUID-HYPE-USD"
|
|
103
|
+
assert q1.maturity_ts == 1000
|
|
104
|
+
assert q1.tenor_days == 1.0
|
|
105
|
+
assert q1.tick_step == 5
|
|
106
|
+
assert q1.best_bid_apr == 0.11
|
|
107
|
+
assert q1.best_ask_apr == 0.12
|
|
108
|
+
assert q1.mid_apr == pytest.approx(0.115)
|
|
109
|
+
|
|
110
|
+
q2 = quotes[1]
|
|
111
|
+
assert q2.maturity_ts == 2000
|
|
112
|
+
assert q2.tenor_days == 2.0
|
|
113
|
+
assert q2.tick_step == 7
|
|
114
|
+
assert q2.best_bid_apr == 0.2
|
|
115
|
+
assert q2.best_ask_apr == 0.25
|
|
116
|
+
assert q2.mid_apr == pytest.approx(0.225)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@pytest.mark.asyncio
|
|
120
|
+
async def test_get_account_balances_isolated_market_id_golden(
|
|
121
|
+
adapter, mock_boros_client
|
|
122
|
+
):
|
|
123
|
+
# marketAcc parsing: last 6 hex chars represent the market id (3 bytes).
|
|
124
|
+
market_acc = "0x" + ("0" * 58) + "000012" # 0x12 == 18
|
|
125
|
+
|
|
126
|
+
mock_boros_client.get_collaterals = AsyncMock(
|
|
127
|
+
return_value={
|
|
128
|
+
"collaterals": [
|
|
129
|
+
{
|
|
130
|
+
"tokenId": 3,
|
|
131
|
+
"crossPosition": {"availableBalance": "100000000000000000000"},
|
|
132
|
+
"isolatedPositions": [
|
|
133
|
+
{
|
|
134
|
+
"availableBalance": "50000000000000000000",
|
|
135
|
+
"marketAcc": market_acc,
|
|
136
|
+
}
|
|
137
|
+
],
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
ok, balances = await adapter.get_account_balances(token_id=3)
|
|
144
|
+
assert ok is True
|
|
145
|
+
assert balances["cross"] == 100.0
|
|
146
|
+
assert balances["isolated"] == 50.0
|
|
147
|
+
assert balances["total"] == 150.0
|
|
148
|
+
assert balances["isolated_market_id"] == 18
|
|
149
|
+
assert balances["isolated_positions"] == [
|
|
150
|
+
{
|
|
151
|
+
"market_id": 18,
|
|
152
|
+
"balance": 50.0,
|
|
153
|
+
"balance_wei": 50000000000000000000,
|
|
154
|
+
"marketAcc": market_acc,
|
|
155
|
+
}
|
|
156
|
+
]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Types for BorosAdapter (dataclasses)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class BorosMarketQuote:
|
|
12
|
+
"""Quote data for a Boros market."""
|
|
13
|
+
|
|
14
|
+
market_id: int
|
|
15
|
+
market_address: str
|
|
16
|
+
symbol: str
|
|
17
|
+
underlying: str
|
|
18
|
+
tenor_days: float
|
|
19
|
+
maturity_ts: int
|
|
20
|
+
collateral_address: str
|
|
21
|
+
collateral_token_id: int | None
|
|
22
|
+
tick_step: int | None
|
|
23
|
+
mid_apr: float | None
|
|
24
|
+
best_bid_apr: float | None
|
|
25
|
+
best_ask_apr: float | None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class BorosTenorQuote:
|
|
30
|
+
"""Tenor curve data for a Boros market."""
|
|
31
|
+
|
|
32
|
+
market_id: int
|
|
33
|
+
address: str
|
|
34
|
+
symbol: str
|
|
35
|
+
underlying_symbol: str
|
|
36
|
+
maturity: int
|
|
37
|
+
tenor_days: float
|
|
38
|
+
mid_apr: float | None
|
|
39
|
+
mark_apr: float | None
|
|
40
|
+
floating_apr: float | None
|
|
41
|
+
long_yield_apr: float | None
|
|
42
|
+
volume_24h: float | None
|
|
43
|
+
notional_oi: float | None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class BorosLimitOrder:
|
|
48
|
+
"""Represents an open limit order on Boros."""
|
|
49
|
+
|
|
50
|
+
order_id: str
|
|
51
|
+
market_id: int
|
|
52
|
+
side: str # "long" or "short"
|
|
53
|
+
size: float # Size in YU
|
|
54
|
+
limit_tick: int # APR in bps
|
|
55
|
+
limit_apr: float # APR as decimal (e.g., 0.05 = 5%)
|
|
56
|
+
filled_size: float
|
|
57
|
+
remaining_size: float
|
|
58
|
+
status: str # "open", "partially_filled", etc.
|
|
59
|
+
created_at: datetime | None = None
|
|
60
|
+
raw: dict[str, Any] | None = field(default=None, repr=False)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class MarginHealth:
|
|
65
|
+
"""Margin health metrics for a Boros account."""
|
|
66
|
+
|
|
67
|
+
margin_ratio: float
|
|
68
|
+
maint_margin: float
|
|
69
|
+
net_balance: float
|
|
70
|
+
positions: list[dict[str, Any]]
|