wayfinder-paths 0.1.19__py3-none-any.whl → 0.1.20__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 -2
- wayfinder_paths/adapters/balance_adapter/README.md +59 -45
- wayfinder_paths/adapters/balance_adapter/adapter.py +0 -21
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -14
- wayfinder_paths/adapters/brap_adapter/README.md +61 -184
- wayfinder_paths/adapters/brap_adapter/__init__.py +0 -4
- wayfinder_paths/adapters/brap_adapter/adapter.py +0 -147
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +0 -15
- wayfinder_paths/adapters/hyperlend_adapter/__init__.py +0 -4
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +0 -9
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +0 -17
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +3 -312
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +1 -71
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +0 -57
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +0 -17
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +2 -42
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +1 -9
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +15 -47
- wayfinder_paths/adapters/hyperliquid_adapter/utils.py +0 -7
- wayfinder_paths/adapters/ledger_adapter/README.md +54 -74
- wayfinder_paths/adapters/ledger_adapter/__init__.py +0 -4
- wayfinder_paths/adapters/ledger_adapter/adapter.py +0 -106
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +0 -12
- wayfinder_paths/adapters/moonwell_adapter/README.md +67 -106
- wayfinder_paths/adapters/moonwell_adapter/__init__.py +0 -4
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +9 -121
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +84 -83
- wayfinder_paths/adapters/pool_adapter/README.md +30 -51
- wayfinder_paths/adapters/pool_adapter/__init__.py +0 -4
- wayfinder_paths/adapters/pool_adapter/adapter.py +0 -19
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +0 -8
- wayfinder_paths/adapters/token_adapter/README.md +41 -49
- wayfinder_paths/adapters/token_adapter/adapter.py +0 -32
- wayfinder_paths/adapters/token_adapter/test_adapter.py +1 -12
- wayfinder_paths/conftest.py +0 -8
- wayfinder_paths/core/__init__.py +0 -2
- wayfinder_paths/core/adapters/BaseAdapter.py +0 -22
- wayfinder_paths/core/adapters/__init__.py +0 -5
- wayfinder_paths/core/adapters/models.py +0 -5
- wayfinder_paths/core/analytics/__init__.py +0 -2
- wayfinder_paths/core/analytics/bootstrap.py +0 -16
- wayfinder_paths/core/analytics/stats.py +0 -7
- wayfinder_paths/core/analytics/test_analytics.py +5 -34
- wayfinder_paths/core/clients/BRAPClient.py +0 -35
- wayfinder_paths/core/clients/ClientManager.py +0 -51
- wayfinder_paths/core/clients/HyperlendClient.py +0 -77
- wayfinder_paths/core/clients/LedgerClient.py +2 -122
- wayfinder_paths/core/clients/PoolClient.py +0 -2
- wayfinder_paths/core/clients/TokenClient.py +0 -39
- wayfinder_paths/core/clients/WalletClient.py +0 -15
- wayfinder_paths/core/clients/WayfinderClient.py +0 -24
- wayfinder_paths/core/clients/__init__.py +0 -4
- wayfinder_paths/core/clients/protocols.py +25 -98
- wayfinder_paths/core/config.py +0 -24
- wayfinder_paths/core/constants/__init__.py +0 -7
- wayfinder_paths/core/constants/base.py +2 -9
- wayfinder_paths/core/constants/erc20_abi.py +0 -5
- wayfinder_paths/core/constants/hyperlend_abi.py +0 -7
- wayfinder_paths/core/constants/moonwell_abi.py +0 -35
- wayfinder_paths/core/engine/StrategyJob.py +0 -32
- wayfinder_paths/core/strategies/Strategy.py +0 -99
- wayfinder_paths/core/strategies/__init__.py +0 -2
- wayfinder_paths/core/utils/__init__.py +0 -1
- wayfinder_paths/core/utils/erc20_service.py +0 -1
- wayfinder_paths/core/utils/evm_helpers.py +0 -50
- wayfinder_paths/core/utils/transaction.py +0 -1
- wayfinder_paths/run_strategy.py +0 -46
- wayfinder_paths/scripts/create_strategy.py +0 -17
- wayfinder_paths/scripts/make_wallets.py +1 -4
- wayfinder_paths/strategies/basis_trading_strategy/README.md +71 -163
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +0 -24
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +36 -400
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +15 -64
- wayfinder_paths/strategies/basis_trading_strategy/types.py +0 -4
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +65 -56
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +4 -27
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -10
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +71 -72
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +23 -227
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +120 -113
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +64 -59
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +4 -44
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +2 -35
- wayfinder_paths/templates/adapter/README.md +107 -46
- wayfinder_paths/templates/adapter/adapter.py +0 -9
- wayfinder_paths/templates/adapter/test_adapter.py +0 -19
- wayfinder_paths/templates/strategy/README.md +113 -59
- wayfinder_paths/templates/strategy/strategy.py +0 -22
- wayfinder_paths/templates/strategy/test_strategy.py +0 -28
- wayfinder_paths/tests/test_test_coverage.py +2 -12
- wayfinder_paths/tests/test_utils.py +1 -31
- wayfinder_paths-0.1.20.dist-info/METADATA +355 -0
- wayfinder_paths-0.1.20.dist-info/RECORD +129 -0
- wayfinder_paths/core/adapters/base.py +0 -5
- wayfinder_paths-0.1.19.dist-info/METADATA +0 -592
- wayfinder_paths-0.1.19.dist-info/RECORD +0 -130
- {wayfinder_paths-0.1.19.dist-info → wayfinder_paths-0.1.20.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.19.dist-info → wayfinder_paths-0.1.20.dist-info}/WHEEL +0 -0
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Live API tests for HyperliquidAdapter.
|
|
3
|
-
|
|
4
|
-
These tests hit the real Hyperliquid API to verify:
|
|
5
|
-
- Spot asset ID resolution (PURR, ETH, BTC, HYPE)
|
|
6
|
-
- Perp asset ID resolution
|
|
7
|
-
- API connectivity
|
|
8
|
-
|
|
9
|
-
Run with: pytest wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py -v
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
1
|
import pytest
|
|
13
2
|
|
|
14
3
|
from wayfinder_paths.adapters.hyperliquid_adapter.adapter import HyperliquidAdapter
|
|
@@ -16,16 +5,12 @@ from wayfinder_paths.adapters.hyperliquid_adapter.adapter import HyperliquidAdap
|
|
|
16
5
|
|
|
17
6
|
@pytest.fixture
|
|
18
7
|
def live_adapter():
|
|
19
|
-
"""Create adapter connected to real Hyperliquid API."""
|
|
20
8
|
return HyperliquidAdapter(config={})
|
|
21
9
|
|
|
22
10
|
|
|
23
11
|
class TestSpotAssetIDs:
|
|
24
|
-
"""Test spot asset ID resolution against live API."""
|
|
25
|
-
|
|
26
12
|
@pytest.mark.asyncio
|
|
27
13
|
async def test_get_spot_assets_returns_dict(self, live_adapter):
|
|
28
|
-
"""Verify get_spot_assets returns a populated dict."""
|
|
29
14
|
success, spot_assets = await live_adapter.get_spot_assets()
|
|
30
15
|
|
|
31
16
|
assert success
|
|
@@ -34,7 +19,6 @@ class TestSpotAssetIDs:
|
|
|
34
19
|
|
|
35
20
|
@pytest.mark.asyncio
|
|
36
21
|
async def test_purr_spot_asset_id(self, live_adapter):
|
|
37
|
-
"""PURR/USDC should be the first spot pair (index 0 + 10000 = 10000)."""
|
|
38
22
|
success, spot_assets = await live_adapter.get_spot_assets()
|
|
39
23
|
|
|
40
24
|
assert success
|
|
@@ -43,7 +27,6 @@ class TestSpotAssetIDs:
|
|
|
43
27
|
|
|
44
28
|
@pytest.mark.asyncio
|
|
45
29
|
async def test_hype_spot_asset_id(self, live_adapter):
|
|
46
|
-
"""HYPE/USDC should have asset ID 10107."""
|
|
47
30
|
success, spot_assets = await live_adapter.get_spot_assets()
|
|
48
31
|
|
|
49
32
|
assert success
|
|
@@ -53,7 +36,6 @@ class TestSpotAssetIDs:
|
|
|
53
36
|
|
|
54
37
|
@pytest.mark.asyncio
|
|
55
38
|
async def test_eth_spot_exists(self, live_adapter):
|
|
56
|
-
"""ETH/USDC spot pair should exist."""
|
|
57
39
|
success, spot_assets = await live_adapter.get_spot_assets()
|
|
58
40
|
|
|
59
41
|
assert success
|
|
@@ -65,7 +47,6 @@ class TestSpotAssetIDs:
|
|
|
65
47
|
|
|
66
48
|
@pytest.mark.asyncio
|
|
67
49
|
async def test_btc_spot_exists(self, live_adapter):
|
|
68
|
-
"""BTC/USDC spot pair should exist."""
|
|
69
50
|
success, spot_assets = await live_adapter.get_spot_assets()
|
|
70
51
|
|
|
71
52
|
assert success
|
|
@@ -77,7 +58,6 @@ class TestSpotAssetIDs:
|
|
|
77
58
|
|
|
78
59
|
@pytest.mark.asyncio
|
|
79
60
|
async def test_spot_asset_ids_are_valid(self, live_adapter):
|
|
80
|
-
"""All spot asset IDs should be >= 10000."""
|
|
81
61
|
success, spot_assets = await live_adapter.get_spot_assets()
|
|
82
62
|
|
|
83
63
|
assert success
|
|
@@ -86,7 +66,6 @@ class TestSpotAssetIDs:
|
|
|
86
66
|
|
|
87
67
|
@pytest.mark.asyncio
|
|
88
68
|
async def test_get_spot_asset_id_helper(self, live_adapter):
|
|
89
|
-
"""Test synchronous helper after cache is populated."""
|
|
90
69
|
# First populate cache
|
|
91
70
|
success, _ = await live_adapter.get_spot_assets()
|
|
92
71
|
assert success
|
|
@@ -104,11 +83,8 @@ class TestSpotAssetIDs:
|
|
|
104
83
|
|
|
105
84
|
|
|
106
85
|
class TestPerpAssetIDs:
|
|
107
|
-
"""Test perp asset ID resolution against live API."""
|
|
108
|
-
|
|
109
86
|
@pytest.mark.asyncio
|
|
110
87
|
async def test_coin_to_asset_mapping(self, live_adapter):
|
|
111
|
-
"""Verify perp coin to asset mapping is populated."""
|
|
112
88
|
coin_to_asset = live_adapter.coin_to_asset
|
|
113
89
|
|
|
114
90
|
assert isinstance(coin_to_asset, dict)
|
|
@@ -116,7 +92,6 @@ class TestPerpAssetIDs:
|
|
|
116
92
|
|
|
117
93
|
@pytest.mark.asyncio
|
|
118
94
|
async def test_btc_perp_asset_id(self, live_adapter):
|
|
119
|
-
"""BTC perp should be asset_id 0."""
|
|
120
95
|
coin_to_asset = live_adapter.coin_to_asset
|
|
121
96
|
|
|
122
97
|
assert "BTC" in coin_to_asset
|
|
@@ -124,7 +99,6 @@ class TestPerpAssetIDs:
|
|
|
124
99
|
|
|
125
100
|
@pytest.mark.asyncio
|
|
126
101
|
async def test_eth_perp_asset_id(self, live_adapter):
|
|
127
|
-
"""ETH perp should be asset_id 1."""
|
|
128
102
|
coin_to_asset = live_adapter.coin_to_asset
|
|
129
103
|
|
|
130
104
|
assert "ETH" in coin_to_asset
|
|
@@ -132,19 +106,15 @@ class TestPerpAssetIDs:
|
|
|
132
106
|
|
|
133
107
|
@pytest.mark.asyncio
|
|
134
108
|
async def test_hype_perp_exists(self, live_adapter):
|
|
135
|
-
"""HYPE perp should exist with valid asset_id."""
|
|
136
109
|
coin_to_asset = live_adapter.coin_to_asset
|
|
137
110
|
|
|
138
111
|
assert "HYPE" in coin_to_asset
|
|
139
|
-
assert coin_to_asset["HYPE"] < 10000
|
|
112
|
+
assert coin_to_asset["HYPE"] < 10000
|
|
140
113
|
|
|
141
114
|
|
|
142
115
|
class TestSpotMetaStructure:
|
|
143
|
-
"""Test spot_meta API response structure."""
|
|
144
|
-
|
|
145
116
|
@pytest.mark.asyncio
|
|
146
117
|
async def test_spot_meta_has_tokens(self, live_adapter):
|
|
147
|
-
"""Spot meta should have tokens array."""
|
|
148
118
|
success, spot_meta = await live_adapter.get_spot_meta()
|
|
149
119
|
|
|
150
120
|
assert success
|
|
@@ -154,7 +124,6 @@ class TestSpotMetaStructure:
|
|
|
154
124
|
|
|
155
125
|
@pytest.mark.asyncio
|
|
156
126
|
async def test_spot_meta_has_universe(self, live_adapter):
|
|
157
|
-
"""Spot meta should have universe array with pairs."""
|
|
158
127
|
success, spot_meta = await live_adapter.get_spot_meta()
|
|
159
128
|
|
|
160
129
|
assert success
|
|
@@ -164,22 +133,18 @@ class TestSpotMetaStructure:
|
|
|
164
133
|
|
|
165
134
|
@pytest.mark.asyncio
|
|
166
135
|
async def test_spot_universe_pair_structure(self, live_adapter):
|
|
167
|
-
"""Each spot universe entry should have tokens and index."""
|
|
168
136
|
success, spot_meta = await live_adapter.get_spot_meta()
|
|
169
137
|
|
|
170
138
|
assert success
|
|
171
|
-
for pair in spot_meta["universe"][:5]:
|
|
139
|
+
for pair in spot_meta["universe"][:5]:
|
|
172
140
|
assert "tokens" in pair, f"Missing tokens in {pair}"
|
|
173
141
|
assert "index" in pair, f"Missing index in {pair}"
|
|
174
142
|
assert len(pair["tokens"]) >= 2, f"Invalid tokens in {pair}"
|
|
175
143
|
|
|
176
144
|
|
|
177
145
|
class TestL2BookResolution:
|
|
178
|
-
"""Test that spot asset IDs work with L2 book API."""
|
|
179
|
-
|
|
180
146
|
@pytest.mark.asyncio
|
|
181
147
|
async def test_purr_spot_l2_book(self, live_adapter):
|
|
182
|
-
"""PURR/USDC (10000) should return valid L2 book."""
|
|
183
148
|
success, book = await live_adapter.get_spot_l2_book(10000)
|
|
184
149
|
|
|
185
150
|
assert success
|
|
@@ -187,7 +152,6 @@ class TestL2BookResolution:
|
|
|
187
152
|
|
|
188
153
|
@pytest.mark.asyncio
|
|
189
154
|
async def test_hype_spot_l2_book(self, live_adapter):
|
|
190
|
-
"""HYPE/USDC (10107) should return valid L2 book."""
|
|
191
155
|
success, book = await live_adapter.get_spot_l2_book(10107)
|
|
192
156
|
|
|
193
157
|
assert success
|
|
@@ -195,11 +159,8 @@ class TestL2BookResolution:
|
|
|
195
159
|
|
|
196
160
|
|
|
197
161
|
class TestSzDecimals:
|
|
198
|
-
"""Test size decimals resolution for spot assets."""
|
|
199
|
-
|
|
200
162
|
@pytest.mark.asyncio
|
|
201
163
|
async def test_spot_sz_decimals(self, live_adapter):
|
|
202
|
-
"""Spot assets should have valid sz_decimals."""
|
|
203
164
|
# HYPE spot = 10107
|
|
204
165
|
decimals = live_adapter.get_sz_decimals(10107)
|
|
205
166
|
assert isinstance(decimals, int)
|
|
@@ -207,7 +168,6 @@ class TestSzDecimals:
|
|
|
207
168
|
|
|
208
169
|
@pytest.mark.asyncio
|
|
209
170
|
async def test_perp_sz_decimals(self, live_adapter):
|
|
210
|
-
"""Perp assets should have valid sz_decimals."""
|
|
211
171
|
# BTC perp = 0
|
|
212
172
|
decimals = live_adapter.get_sz_decimals(0)
|
|
213
173
|
assert isinstance(decimals, int)
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"""Tests for LocalHyperliquidExecutor."""
|
|
2
|
-
|
|
3
1
|
from types import SimpleNamespace
|
|
4
2
|
from unittest.mock import MagicMock, patch
|
|
5
3
|
|
|
@@ -7,16 +5,12 @@ import pytest
|
|
|
7
5
|
|
|
8
6
|
|
|
9
7
|
class TestLocalHyperliquidExecutor:
|
|
10
|
-
"""Tests for LocalHyperliquidExecutor."""
|
|
11
|
-
|
|
12
8
|
@pytest.fixture
|
|
13
9
|
def mock_info_without_asset_to_coin(self):
|
|
14
|
-
"""Mock Info that only exposes coin_to_asset (no asset_to_coin)."""
|
|
15
10
|
return SimpleNamespace(coin_to_asset={"HYPE": 7})
|
|
16
11
|
|
|
17
12
|
@pytest.fixture
|
|
18
13
|
def mock_exchange(self):
|
|
19
|
-
"""Mock Exchange client."""
|
|
20
14
|
mock = MagicMock()
|
|
21
15
|
mock.update_leverage.return_value = {"status": "ok"}
|
|
22
16
|
mock.market_open.return_value = {"status": "ok"}
|
|
@@ -26,7 +20,6 @@ class TestLocalHyperliquidExecutor:
|
|
|
26
20
|
async def test_update_leverage_works_without_asset_to_coin(
|
|
27
21
|
self, mock_info_without_asset_to_coin, mock_exchange
|
|
28
22
|
):
|
|
29
|
-
"""update_leverage should map asset_id->coin via coin_to_asset inversion."""
|
|
30
23
|
dummy_wallet = SimpleNamespace(address="0xabc")
|
|
31
24
|
|
|
32
25
|
with patch(
|
|
@@ -66,7 +59,6 @@ class TestLocalHyperliquidExecutor:
|
|
|
66
59
|
async def test_place_market_order_perp_works_without_asset_to_coin(
|
|
67
60
|
self, mock_info_without_asset_to_coin, mock_exchange
|
|
68
61
|
):
|
|
69
|
-
"""place_market_order should map asset_id->coin via coin_to_asset inversion."""
|
|
70
62
|
dummy_wallet = SimpleNamespace(address="0xabc")
|
|
71
63
|
|
|
72
64
|
with patch(
|
|
@@ -95,7 +87,7 @@ class TestLocalHyperliquidExecutor:
|
|
|
95
87
|
size=1.0,
|
|
96
88
|
address="0xabc",
|
|
97
89
|
reduce_only=False,
|
|
98
|
-
cloid=MagicMock(),
|
|
90
|
+
cloid=MagicMock(),
|
|
99
91
|
)
|
|
100
92
|
|
|
101
93
|
assert resp.get("status") == "ok"
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"""Tests for Hyperliquid adapter utility functions."""
|
|
2
|
-
|
|
3
1
|
import pytest
|
|
4
2
|
|
|
5
3
|
from wayfinder_paths.adapters.hyperliquid_adapter.utils import (
|
|
@@ -13,16 +11,12 @@ from wayfinder_paths.adapters.hyperliquid_adapter.utils import (
|
|
|
13
11
|
|
|
14
12
|
|
|
15
13
|
class TestSpotIndexFromAssetId:
|
|
16
|
-
"""Tests for spot_index_from_asset_id function."""
|
|
17
|
-
|
|
18
14
|
def test_valid_spot_id(self):
|
|
19
|
-
"""Valid spot asset IDs (>=10000) should return index."""
|
|
20
15
|
assert spot_index_from_asset_id(10000) == 0
|
|
21
16
|
assert spot_index_from_asset_id(10001) == 1
|
|
22
17
|
assert spot_index_from_asset_id(10107) == 107
|
|
23
18
|
|
|
24
19
|
def test_rejects_perp_id(self):
|
|
25
|
-
"""Perp asset IDs (<10000) should raise ValueError."""
|
|
26
20
|
with pytest.raises(ValueError, match="Expected spot asset_id >= 10000"):
|
|
27
21
|
spot_index_from_asset_id(0)
|
|
28
22
|
|
|
@@ -31,14 +25,11 @@ class TestSpotIndexFromAssetId:
|
|
|
31
25
|
|
|
32
26
|
|
|
33
27
|
class TestNormalizeL2Book:
|
|
34
|
-
"""Tests for normalize_l2_book function."""
|
|
35
|
-
|
|
36
28
|
def test_levels_format(self):
|
|
37
|
-
"""Should handle Hyperliquid 'levels' format (nested arrays)."""
|
|
38
29
|
raw = {
|
|
39
30
|
"levels": [
|
|
40
|
-
[{"px": "100.5", "sz": "10"}],
|
|
41
|
-
[{"px": "101.0", "sz": "5"}],
|
|
31
|
+
[{"px": "100.5", "sz": "10"}],
|
|
32
|
+
[{"px": "101.0", "sz": "5"}],
|
|
42
33
|
],
|
|
43
34
|
"midPx": "100.75",
|
|
44
35
|
}
|
|
@@ -49,7 +40,6 @@ class TestNormalizeL2Book:
|
|
|
49
40
|
assert result["midPx"] == 100.75
|
|
50
41
|
|
|
51
42
|
def test_bids_asks_format(self):
|
|
52
|
-
"""Should handle flat bids/asks format."""
|
|
53
43
|
raw = {
|
|
54
44
|
"bids": [{"px": "100.5", "sz": "10"}],
|
|
55
45
|
"asks": [{"px": "101.0", "sz": "5"}],
|
|
@@ -60,7 +50,6 @@ class TestNormalizeL2Book:
|
|
|
60
50
|
assert result["asks"] == [(101.0, 5.0)]
|
|
61
51
|
|
|
62
52
|
def test_calculates_mid_from_bids_asks(self):
|
|
63
|
-
"""Should calculate mid price from best bid/ask when not provided."""
|
|
64
53
|
raw = {
|
|
65
54
|
"levels": [
|
|
66
55
|
[{"px": "100.0", "sz": "10"}],
|
|
@@ -69,38 +58,35 @@ class TestNormalizeL2Book:
|
|
|
69
58
|
}
|
|
70
59
|
result = normalize_l2_book(raw)
|
|
71
60
|
|
|
72
|
-
assert result["midPx"] == 101.0
|
|
61
|
+
assert result["midPx"] == 101.0
|
|
73
62
|
|
|
74
63
|
def test_fallback_mid(self):
|
|
75
|
-
"""Should use fallback_mid when no mid can be calculated."""
|
|
76
64
|
raw = {"levels": [[], []]}
|
|
77
65
|
result = normalize_l2_book(raw, fallback_mid=99.0)
|
|
78
66
|
|
|
79
67
|
assert result["midPx"] == 99.0
|
|
80
68
|
|
|
81
69
|
def test_invalid_levels_skipped(self):
|
|
82
|
-
"""Invalid level entries should be skipped."""
|
|
83
70
|
raw = {
|
|
84
71
|
"levels": [
|
|
85
72
|
[
|
|
86
|
-
{"px": "100.0", "sz": "10"},
|
|
87
|
-
{"px": "invalid", "sz": "5"},
|
|
88
|
-
{"px": "0", "sz": "5"},
|
|
89
|
-
{"px": "50", "sz": "0"},
|
|
73
|
+
{"px": "100.0", "sz": "10"},
|
|
74
|
+
{"px": "invalid", "sz": "5"},
|
|
75
|
+
{"px": "0", "sz": "5"},
|
|
76
|
+
{"px": "50", "sz": "0"},
|
|
90
77
|
],
|
|
91
78
|
[{"px": "101.0", "sz": "5"}],
|
|
92
79
|
],
|
|
93
80
|
}
|
|
94
81
|
result = normalize_l2_book(raw)
|
|
95
82
|
|
|
96
|
-
assert result["bids"] == [(100.0, 10.0)]
|
|
83
|
+
assert result["bids"] == [(100.0, 10.0)]
|
|
97
84
|
|
|
98
85
|
def test_tuple_format_levels(self):
|
|
99
|
-
"""Should handle tuple/list format levels."""
|
|
100
86
|
raw = {
|
|
101
87
|
"levels": [
|
|
102
|
-
[[100.0, 10.0]],
|
|
103
|
-
[[101.0, 5.0]],
|
|
88
|
+
[[100.0, 10.0]],
|
|
89
|
+
[[101.0, 5.0]],
|
|
104
90
|
],
|
|
105
91
|
}
|
|
106
92
|
result = normalize_l2_book(raw)
|
|
@@ -110,13 +96,10 @@ class TestNormalizeL2Book:
|
|
|
110
96
|
|
|
111
97
|
|
|
112
98
|
class TestUsdDepthInBand:
|
|
113
|
-
"""Tests for usd_depth_in_band function."""
|
|
114
|
-
|
|
115
99
|
def test_buy_side(self):
|
|
116
|
-
"""Buy side should measure ask depth within band."""
|
|
117
100
|
book = {
|
|
118
|
-
"bids": [(99.0, 10.0), (98.0, 20.0)],
|
|
119
|
-
"asks": [(101.0, 5.0), (102.0, 10.0)],
|
|
101
|
+
"bids": [(99.0, 10.0), (98.0, 20.0)],
|
|
102
|
+
"asks": [(101.0, 5.0), (102.0, 10.0)],
|
|
120
103
|
"midPx": 100.0,
|
|
121
104
|
}
|
|
122
105
|
# 100 bps = 1% band, so hi = 101.0
|
|
@@ -127,7 +110,6 @@ class TestUsdDepthInBand:
|
|
|
127
110
|
assert depth == 505.0
|
|
128
111
|
|
|
129
112
|
def test_sell_side(self):
|
|
130
|
-
"""Sell side should measure bid depth within band."""
|
|
131
113
|
book = {
|
|
132
114
|
"bids": [(99.0, 10.0), (98.0, 20.0)],
|
|
133
115
|
"asks": [(101.0, 5.0), (102.0, 10.0)],
|
|
@@ -141,7 +123,6 @@ class TestUsdDepthInBand:
|
|
|
141
123
|
assert depth == 990.0
|
|
142
124
|
|
|
143
125
|
def test_zero_mid(self):
|
|
144
|
-
"""Zero mid price should return zero depth."""
|
|
145
126
|
book = {"bids": [(99.0, 10.0)], "asks": [(101.0, 5.0)], "midPx": 0.0}
|
|
146
127
|
depth, mid = usd_depth_in_band(book, band_bps=100, side="buy")
|
|
147
128
|
|
|
@@ -150,10 +131,7 @@ class TestUsdDepthInBand:
|
|
|
150
131
|
|
|
151
132
|
|
|
152
133
|
class TestSzDecimalsForAsset:
|
|
153
|
-
"""Tests for sz_decimals_for_asset function."""
|
|
154
|
-
|
|
155
134
|
def test_known_asset(self):
|
|
156
|
-
"""Should return decimals for known asset."""
|
|
157
135
|
asset_to_sz = {0: 4, 1: 3, 10000: 6}
|
|
158
136
|
|
|
159
137
|
assert sz_decimals_for_asset(asset_to_sz, 0) == 4
|
|
@@ -161,7 +139,6 @@ class TestSzDecimalsForAsset:
|
|
|
161
139
|
assert sz_decimals_for_asset(asset_to_sz, 10000) == 6
|
|
162
140
|
|
|
163
141
|
def test_unknown_raises(self):
|
|
164
|
-
"""Unknown asset should raise ValueError."""
|
|
165
142
|
asset_to_sz = {0: 4}
|
|
166
143
|
|
|
167
144
|
with pytest.raises(ValueError, match="Unknown asset_id 999"):
|
|
@@ -169,10 +146,7 @@ class TestSzDecimalsForAsset:
|
|
|
169
146
|
|
|
170
147
|
|
|
171
148
|
class TestSizeStep:
|
|
172
|
-
"""Tests for size_step function."""
|
|
173
|
-
|
|
174
149
|
def test_size_step_calculation(self):
|
|
175
|
-
"""Size step should be 10^(-decimals)."""
|
|
176
150
|
from decimal import Decimal
|
|
177
151
|
|
|
178
152
|
asset_to_sz = {0: 4, 1: 2}
|
|
@@ -182,26 +156,21 @@ class TestSizeStep:
|
|
|
182
156
|
|
|
183
157
|
|
|
184
158
|
class TestRoundSizeForAsset:
|
|
185
|
-
"""Tests for round_size_for_asset function."""
|
|
186
|
-
|
|
187
159
|
def test_floors_correctly(self):
|
|
188
|
-
|
|
189
|
-
asset_to_sz = {0: 4} # Step = 0.0001
|
|
160
|
+
asset_to_sz = {0: 4}
|
|
190
161
|
|
|
191
162
|
# 1.23456789 should floor to 1.2345
|
|
192
163
|
result = round_size_for_asset(asset_to_sz, 0, 1.23456789)
|
|
193
164
|
assert result == 1.2345
|
|
194
165
|
|
|
195
166
|
def test_zero_returns_zero(self):
|
|
196
|
-
"""Zero or negative size returns 0."""
|
|
197
167
|
asset_to_sz = {0: 4}
|
|
198
168
|
|
|
199
169
|
assert round_size_for_asset(asset_to_sz, 0, 0.0) == 0.0
|
|
200
170
|
assert round_size_for_asset(asset_to_sz, 0, -1.0) == 0.0
|
|
201
171
|
|
|
202
172
|
def test_ensure_min_step(self):
|
|
203
|
-
|
|
204
|
-
asset_to_sz = {0: 4} # Step = 0.0001
|
|
173
|
+
asset_to_sz = {0: 4}
|
|
205
174
|
|
|
206
175
|
# Without ensure_min_step: 0.00001 floors to 0
|
|
207
176
|
result = round_size_for_asset(asset_to_sz, 0, 0.00001, ensure_min_step=False)
|
|
@@ -212,8 +181,7 @@ class TestRoundSizeForAsset:
|
|
|
212
181
|
assert result == 0.0001
|
|
213
182
|
|
|
214
183
|
def test_preserves_precision(self):
|
|
215
|
-
|
|
216
|
-
asset_to_sz = {0: 2} # Step = 0.01
|
|
184
|
+
asset_to_sz = {0: 2}
|
|
217
185
|
|
|
218
186
|
# 0.1 + 0.2 = 0.30000000000000004 in float
|
|
219
187
|
result = round_size_for_asset(asset_to_sz, 0, 0.1 + 0.2)
|
|
@@ -6,7 +6,6 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def spot_index_from_asset_id(spot_asset_id: int) -> int:
|
|
9
|
-
"""Convert Hyperliquid spot asset_id (>=10000) to spot index."""
|
|
10
9
|
if spot_asset_id < 10000:
|
|
11
10
|
raise ValueError(f"Expected spot asset_id >= 10000, got {spot_asset_id}")
|
|
12
11
|
return int(spot_asset_id) - 10000
|
|
@@ -17,8 +16,6 @@ def normalize_l2_book(
|
|
|
17
16
|
*,
|
|
18
17
|
fallback_mid: float | None = None,
|
|
19
18
|
) -> dict[str, Any]:
|
|
20
|
-
"""Normalize Hyperliquid L2 into bids/asks lists with floats."""
|
|
21
|
-
|
|
22
19
|
def coerce_levels(levels: Any) -> list[tuple[float, float]]:
|
|
23
20
|
normalized: list[tuple[float, float]] = []
|
|
24
21
|
if not isinstance(levels, list):
|
|
@@ -74,7 +71,6 @@ def normalize_l2_book(
|
|
|
74
71
|
def usd_depth_in_band(
|
|
75
72
|
book: dict[str, Any], band_bps: int, side: str
|
|
76
73
|
) -> tuple[float, float]:
|
|
77
|
-
"""Return USD depth inside a +/- band (bps) around mid price for a side."""
|
|
78
74
|
bids = book.get("bids") or []
|
|
79
75
|
asks = book.get("asks") or []
|
|
80
76
|
mid = float(book.get("midPx") or 0.0)
|
|
@@ -102,14 +98,12 @@ def usd_depth_in_band(
|
|
|
102
98
|
def sz_decimals_for_asset(
|
|
103
99
|
asset_to_sz_decimals: Mapping[int, int], asset_id: int
|
|
104
100
|
) -> int:
|
|
105
|
-
"""Return Hyperliquid size decimals (szDecimals) for an asset_id."""
|
|
106
101
|
if asset_id not in asset_to_sz_decimals:
|
|
107
102
|
raise ValueError(f"Unknown asset_id {asset_id}: missing szDecimals")
|
|
108
103
|
return int(asset_to_sz_decimals[asset_id])
|
|
109
104
|
|
|
110
105
|
|
|
111
106
|
def size_step(asset_to_sz_decimals: Mapping[int, int], asset_id: int) -> Decimal:
|
|
112
|
-
"""Return the size increment step for an asset_id."""
|
|
113
107
|
return Decimal(10) ** (-sz_decimals_for_asset(asset_to_sz_decimals, asset_id))
|
|
114
108
|
|
|
115
109
|
|
|
@@ -120,7 +114,6 @@ def round_size_for_asset(
|
|
|
120
114
|
*,
|
|
121
115
|
ensure_min_step: bool = False,
|
|
122
116
|
) -> float:
|
|
123
|
-
"""Floor to size step using Decimal to avoid float issues."""
|
|
124
117
|
size_d = size if isinstance(size, Decimal) else Decimal(str(size))
|
|
125
118
|
if size_d <= 0:
|
|
126
119
|
return 0.0
|
|
@@ -1,145 +1,125 @@
|
|
|
1
1
|
# Ledger Adapter
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Adapter for strategy transaction recording and bookkeeping.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- **Type**: `LEDGER`
|
|
6
|
+
- **Module**: `wayfinder_paths.adapters.ledger_adapter.adapter.LedgerAdapter`
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
- `ledger.write`: Record deposits, withdrawals, and operations
|
|
9
|
-
- `strategy.transactions`: Access strategy transaction history
|
|
10
|
-
- `strategy.deposits`: Record deposit transactions
|
|
11
|
-
- `strategy.withdrawals`: Record withdrawal transactions
|
|
12
|
-
- `strategy.operations`: Record strategy operations (swaps, rebalances, etc.)
|
|
8
|
+
## Overview
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- Use the WAYFINDER_API_URL from settings
|
|
20
|
-
- Handle authentication via config.json
|
|
21
|
-
- Manage token refresh and retry logic
|
|
10
|
+
The LedgerAdapter provides:
|
|
11
|
+
- Transaction history tracking
|
|
12
|
+
- Net deposit calculations
|
|
13
|
+
- Deposit/withdrawal recording
|
|
14
|
+
- Strategy operation logging
|
|
22
15
|
|
|
23
16
|
## Usage
|
|
24
17
|
|
|
25
|
-
### Initialize the Adapter
|
|
26
|
-
|
|
27
18
|
```python
|
|
28
19
|
from wayfinder_paths.adapters.ledger_adapter.adapter import LedgerAdapter
|
|
29
20
|
|
|
30
|
-
# No configuration needed - uses LedgerClient with automatic settings
|
|
31
21
|
adapter = LedgerAdapter()
|
|
32
22
|
```
|
|
33
23
|
|
|
34
|
-
|
|
24
|
+
## Methods
|
|
25
|
+
|
|
26
|
+
### get_strategy_transactions
|
|
27
|
+
|
|
28
|
+
Get transaction history for a strategy wallet.
|
|
35
29
|
|
|
36
30
|
```python
|
|
37
31
|
success, data = await adapter.get_strategy_transactions(
|
|
38
|
-
wallet_address="
|
|
32
|
+
wallet_address="0x...",
|
|
39
33
|
limit=10,
|
|
40
|
-
offset=0
|
|
34
|
+
offset=0,
|
|
41
35
|
)
|
|
42
36
|
if success:
|
|
43
37
|
transactions = data.get("transactions", [])
|
|
44
|
-
print(f"Found {len(transactions)} transactions")
|
|
45
|
-
else:
|
|
46
|
-
print(f"Error: {data}")
|
|
47
38
|
```
|
|
48
39
|
|
|
49
|
-
###
|
|
40
|
+
### get_strategy_net_deposit
|
|
41
|
+
|
|
42
|
+
Get the net deposit amount for a strategy.
|
|
50
43
|
|
|
51
44
|
```python
|
|
52
45
|
success, data = await adapter.get_strategy_net_deposit(
|
|
53
|
-
wallet_address="
|
|
46
|
+
wallet_address="0x..."
|
|
54
47
|
)
|
|
55
48
|
if success:
|
|
56
49
|
net_deposit = data.get("net_deposit", 0)
|
|
57
|
-
print(f"Net deposit: {net_deposit} USDC")
|
|
58
|
-
else:
|
|
59
|
-
print(f"Error: {data}")
|
|
60
50
|
```
|
|
61
51
|
|
|
62
|
-
###
|
|
52
|
+
### record_deposit
|
|
53
|
+
|
|
54
|
+
Record a deposit transaction.
|
|
63
55
|
|
|
64
56
|
```python
|
|
65
57
|
success, data = await adapter.record_deposit(
|
|
66
|
-
wallet_address="
|
|
58
|
+
wallet_address="0x...",
|
|
67
59
|
chain_id=8453,
|
|
68
|
-
token_address="
|
|
69
|
-
token_amount="
|
|
60
|
+
token_address="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
61
|
+
token_amount="1000000000",
|
|
70
62
|
usd_value="1000.00",
|
|
71
|
-
strategy_name="
|
|
63
|
+
strategy_name="my_strategy",
|
|
72
64
|
)
|
|
73
|
-
if success:
|
|
74
|
-
print(f"Deposit recorded: {data.get('transaction_id')}")
|
|
75
|
-
else:
|
|
76
|
-
print(f"Error: {data}")
|
|
77
65
|
```
|
|
78
66
|
|
|
79
|
-
###
|
|
67
|
+
### record_withdrawal
|
|
68
|
+
|
|
69
|
+
Record a withdrawal transaction.
|
|
80
70
|
|
|
81
71
|
```python
|
|
82
72
|
success, data = await adapter.record_withdrawal(
|
|
83
|
-
wallet_address="
|
|
73
|
+
wallet_address="0x...",
|
|
84
74
|
chain_id=8453,
|
|
85
|
-
token_address="
|
|
86
|
-
token_amount="
|
|
75
|
+
token_address="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
|
|
76
|
+
token_amount="500000000",
|
|
87
77
|
usd_value="500.00",
|
|
88
|
-
strategy_name="
|
|
78
|
+
strategy_name="my_strategy",
|
|
89
79
|
)
|
|
90
|
-
if success:
|
|
91
|
-
print(f"Withdrawal recorded: {data.get('transaction_id')}")
|
|
92
|
-
else:
|
|
93
|
-
print(f"Error: {data}")
|
|
94
80
|
```
|
|
95
81
|
|
|
96
|
-
###
|
|
82
|
+
### record_operation
|
|
83
|
+
|
|
84
|
+
Record a strategy operation (swap, rebalance, etc.).
|
|
97
85
|
|
|
98
86
|
```python
|
|
99
87
|
from wayfinder_paths.adapters.ledger_adapter.models import SWAP
|
|
100
88
|
|
|
101
89
|
operation = SWAP(
|
|
102
|
-
from_token_id="
|
|
103
|
-
to_token_id="
|
|
104
|
-
from_amount="
|
|
105
|
-
to_amount="
|
|
90
|
+
from_token_id="0x...",
|
|
91
|
+
to_token_id="0x...",
|
|
92
|
+
from_amount="1000000000",
|
|
93
|
+
to_amount="995000000",
|
|
106
94
|
from_amount_usd=1000.0,
|
|
107
95
|
to_amount_usd=995.0,
|
|
108
96
|
)
|
|
109
97
|
|
|
110
|
-
success,
|
|
111
|
-
wallet_address=
|
|
98
|
+
success, data = await adapter.record_operation(
|
|
99
|
+
wallet_address="0x...",
|
|
112
100
|
operation_data=operation,
|
|
113
101
|
usd_value="1000.00",
|
|
114
|
-
strategy_name="
|
|
102
|
+
strategy_name="my_strategy",
|
|
115
103
|
)
|
|
116
|
-
|
|
117
104
|
```
|
|
118
105
|
|
|
119
|
-
###
|
|
106
|
+
### record_strategy_snapshot
|
|
107
|
+
|
|
108
|
+
Record a strategy status snapshot.
|
|
120
109
|
|
|
121
110
|
```python
|
|
122
|
-
success,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
111
|
+
success, data = await adapter.record_strategy_snapshot(
|
|
112
|
+
wallet_address="0x...",
|
|
113
|
+
strategy_status={"portfolio_value": 1000.0, ...},
|
|
114
|
+
)
|
|
126
115
|
```
|
|
127
116
|
|
|
128
|
-
##
|
|
117
|
+
## Dependencies
|
|
129
118
|
|
|
130
|
-
|
|
131
|
-
- `success` is `True` if the operation succeeded
|
|
132
|
-
- `data` contains the response data on success or error message on failure
|
|
119
|
+
- `LedgerClient` - Low-level API client
|
|
133
120
|
|
|
134
121
|
## Testing
|
|
135
122
|
|
|
136
|
-
Run the adapter tests:
|
|
137
|
-
|
|
138
123
|
```bash
|
|
139
|
-
pytest wayfinder_paths/adapters/ledger_adapter/
|
|
124
|
+
poetry run pytest wayfinder_paths/adapters/ledger_adapter/ -v
|
|
140
125
|
```
|
|
141
|
-
|
|
142
|
-
## Dependencies
|
|
143
|
-
|
|
144
|
-
- `LedgerClient` - Low-level API client for ledger operations
|
|
145
|
-
- `BaseAdapter` - Base adapter class with common functionality
|