wayfinder-paths 0.1.19__py3-none-any.whl → 0.1.21__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 +1 -22
- 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 +1 -148
- 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 +1 -10
- 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 +10 -122
- 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/evm_helpers.py +0 -50
- wayfinder_paths/core/utils/{erc20_service.py → tokens.py} +25 -21
- 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.21.dist-info/METADATA +355 -0
- wayfinder_paths-0.1.21.dist-info/RECORD +129 -0
- {wayfinder_paths-0.1.19.dist-info → wayfinder_paths-0.1.21.dist-info}/WHEEL +1 -1
- 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.21.dist-info}/LICENSE +0 -0
|
@@ -1,89 +1,81 @@
|
|
|
1
1
|
# Token Adapter
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Adapter for token metadata and price feeds.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- **Type**: `TOKEN`
|
|
6
|
+
- **Module**: `wayfinder_paths.adapters.token_adapter.adapter.TokenAdapter`
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
## Overview
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
The TokenClient will automatically:
|
|
14
|
-
- Use the WAYFINDER_API_URL from settings
|
|
15
|
-
- Handle authentication via config.json
|
|
16
|
-
- Manage token refresh and retry logic
|
|
10
|
+
The TokenAdapter provides:
|
|
11
|
+
- Token metadata (address, decimals, symbol)
|
|
12
|
+
- Live price data
|
|
13
|
+
- Gas token lookups by chain
|
|
17
14
|
|
|
18
15
|
## Usage
|
|
19
16
|
|
|
20
|
-
### Initialize the Adapter
|
|
21
|
-
|
|
22
17
|
```python
|
|
23
18
|
from wayfinder_paths.adapters.token_adapter.adapter import TokenAdapter
|
|
24
19
|
|
|
25
|
-
# No configuration needed - uses TokenClient with automatic settings
|
|
26
20
|
adapter = TokenAdapter()
|
|
27
21
|
```
|
|
28
22
|
|
|
29
|
-
|
|
23
|
+
## Methods
|
|
24
|
+
|
|
25
|
+
### get_token
|
|
30
26
|
|
|
31
|
-
|
|
27
|
+
Get token metadata by address or token ID.
|
|
32
28
|
|
|
33
29
|
```python
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
# By token ID
|
|
31
|
+
success, data = await adapter.get_token("usd-coin-base")
|
|
32
|
+
|
|
33
|
+
# By address
|
|
34
|
+
success, data = await adapter.get_token("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")
|
|
37
35
|
|
|
38
36
|
if success:
|
|
39
|
-
print(data)
|
|
40
|
-
|
|
41
|
-
print(f"
|
|
37
|
+
print(f"Symbol: {data['symbol']}")
|
|
38
|
+
print(f"Decimals: {data['decimals']}")
|
|
39
|
+
print(f"Address: {data['address']}")
|
|
42
40
|
```
|
|
43
41
|
|
|
44
|
-
###
|
|
42
|
+
### get_token_price
|
|
43
|
+
|
|
44
|
+
Get current price data for a token.
|
|
45
45
|
|
|
46
46
|
```python
|
|
47
|
-
success, data = await adapter.get_token_price("
|
|
47
|
+
success, data = await adapter.get_token_price("usd-coin-base")
|
|
48
48
|
if success:
|
|
49
49
|
print(f"Price: ${data['current_price']}")
|
|
50
50
|
print(f"24h Change: {data['price_change_percentage_24h']}%")
|
|
51
|
-
else:
|
|
52
|
-
print(f"Error: {data}")
|
|
53
51
|
```
|
|
54
52
|
|
|
55
|
-
###
|
|
53
|
+
### get_gas_token
|
|
54
|
+
|
|
55
|
+
Get the native gas token for a chain.
|
|
56
56
|
|
|
57
57
|
```python
|
|
58
58
|
success, data = await adapter.get_gas_token("base")
|
|
59
59
|
if success:
|
|
60
|
-
print(f"Gas token: {data['symbol']}
|
|
61
|
-
print(f"Address: {data['address']}")
|
|
62
|
-
else:
|
|
63
|
-
print(f"Error: {data}")
|
|
60
|
+
print(f"Gas token: {data['symbol']}")
|
|
64
61
|
```
|
|
65
62
|
|
|
66
|
-
##
|
|
67
|
-
|
|
68
|
-
The adapter uses the following Wayfinder API endpoint:
|
|
69
|
-
|
|
70
|
-
- `GET /api/v1/blockchain/tokens/detail/?query=...&market_data=...&chain_id=...`
|
|
63
|
+
## Response Format
|
|
71
64
|
|
|
72
|
-
|
|
65
|
+
Token metadata includes:
|
|
66
|
+
- `symbol` - Token symbol (e.g., "USDC")
|
|
67
|
+
- `name` - Full name
|
|
68
|
+
- `address` - Contract address
|
|
69
|
+
- `decimals` - Token decimals
|
|
70
|
+
- `chain_id` - Chain ID
|
|
71
|
+
- `token_id` - Wayfinder token identifier
|
|
73
72
|
|
|
74
|
-
|
|
75
|
-
- `success` indicates whether the operation was successful
|
|
76
|
-
- `data` contains either the token information (on success) or an error message (on failure)
|
|
73
|
+
## Dependencies
|
|
77
74
|
|
|
78
|
-
|
|
75
|
+
- `TokenClient` - Low-level API client
|
|
79
76
|
|
|
80
|
-
|
|
77
|
+
## Testing
|
|
81
78
|
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
print(f"Status: {health['status']}")
|
|
79
|
+
```bash
|
|
80
|
+
poetry run pytest wayfinder_paths/adapters/token_adapter/ -v
|
|
85
81
|
```
|
|
86
|
-
|
|
87
|
-
## Examples
|
|
88
|
-
|
|
89
|
-
See `examples.json` for more detailed usage examples.
|
|
@@ -9,11 +9,6 @@ from wayfinder_paths.core.clients.TokenClient import (
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class TokenAdapter(BaseAdapter):
|
|
12
|
-
"""
|
|
13
|
-
Token adapter that wraps the _get_token_via_api method for fetching token data
|
|
14
|
-
via HeadlessAPIViewSet endpoints. Supports both address and token_id lookups.
|
|
15
|
-
"""
|
|
16
|
-
|
|
17
12
|
adapter_type: str = "TOKEN"
|
|
18
13
|
|
|
19
14
|
def __init__(
|
|
@@ -27,15 +22,6 @@ class TokenAdapter(BaseAdapter):
|
|
|
27
22
|
async def get_token(
|
|
28
23
|
self, query: str, *, chain_id: int | None = None
|
|
29
24
|
) -> tuple[bool, TokenDetails | str]:
|
|
30
|
-
"""
|
|
31
|
-
Get token data by address using the token-details endpoint.
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
address: Token contract address
|
|
35
|
-
|
|
36
|
-
Returns:
|
|
37
|
-
Tuple of (success, data) where data is the token information or error message
|
|
38
|
-
"""
|
|
39
25
|
try:
|
|
40
26
|
data = await self.token_client.get_token_details(query, chain_id=chain_id)
|
|
41
27
|
if not data:
|
|
@@ -48,15 +34,6 @@ class TokenAdapter(BaseAdapter):
|
|
|
48
34
|
async def get_token_price(
|
|
49
35
|
self, token_id: str, *, chain_id: int | None = None
|
|
50
36
|
) -> tuple[bool, dict[str, Any] | str]:
|
|
51
|
-
"""
|
|
52
|
-
Get token price by token ID or address using the token-details endpoint.
|
|
53
|
-
|
|
54
|
-
Args:
|
|
55
|
-
token_id: Token identifier or address
|
|
56
|
-
|
|
57
|
-
Returns:
|
|
58
|
-
Tuple of (success, data) where data is the price information or error message
|
|
59
|
-
"""
|
|
60
37
|
try:
|
|
61
38
|
data = await self.token_client.get_token_details(
|
|
62
39
|
token_id, market_data=True, chain_id=chain_id
|
|
@@ -83,15 +60,6 @@ class TokenAdapter(BaseAdapter):
|
|
|
83
60
|
return (False, str(e))
|
|
84
61
|
|
|
85
62
|
async def get_gas_token(self, chain_code: str) -> tuple[bool, GasToken | str]:
|
|
86
|
-
"""
|
|
87
|
-
Get gas token for a given chain code.
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
chain_code: Chain code (e.g., "base", "ethereum")
|
|
91
|
-
|
|
92
|
-
Returns:
|
|
93
|
-
Tuple of (success, data) where data is the gas token information or error message
|
|
94
|
-
"""
|
|
95
63
|
try:
|
|
96
64
|
data = await self.token_client.get_gas_token(chain_code)
|
|
97
65
|
if not data:
|
|
@@ -6,21 +6,17 @@ from wayfinder_paths.adapters.token_adapter.adapter import TokenAdapter
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class TestTokenAdapter:
|
|
9
|
-
"""Test cases for TokenAdapter"""
|
|
10
|
-
|
|
11
9
|
@pytest.fixture
|
|
12
10
|
def adapter(self):
|
|
13
11
|
return TokenAdapter()
|
|
14
12
|
|
|
15
13
|
def test_init_with_default_config(self):
|
|
16
|
-
"""Test adapter initialization with default config"""
|
|
17
14
|
adapter = TokenAdapter()
|
|
18
15
|
assert adapter.adapter_type == "TOKEN"
|
|
19
16
|
assert adapter.token_client is not None
|
|
20
17
|
|
|
21
18
|
@pytest.mark.asyncio
|
|
22
19
|
async def test_get_token_success(self, adapter):
|
|
23
|
-
"""Test successful token retrieval by address"""
|
|
24
20
|
mock_token_data = {
|
|
25
21
|
"address": "0x1234...",
|
|
26
22
|
"symbol": "TEST",
|
|
@@ -38,7 +34,6 @@ class TestTokenAdapter:
|
|
|
38
34
|
|
|
39
35
|
@pytest.mark.asyncio
|
|
40
36
|
async def test_get_token_not_found(self, adapter):
|
|
41
|
-
"""Test token not found by address"""
|
|
42
37
|
with patch.object(adapter.token_client, "get_token_details", return_value=None):
|
|
43
38
|
success, data = await adapter.get_token("0x1234...")
|
|
44
39
|
|
|
@@ -47,7 +42,6 @@ class TestTokenAdapter:
|
|
|
47
42
|
|
|
48
43
|
@pytest.mark.asyncio
|
|
49
44
|
async def test_get_token_by_token_id(self, adapter):
|
|
50
|
-
"""Test token retrieval with token_id"""
|
|
51
45
|
mock_token_data = {"address": "0x1234...", "symbol": "TEST"}
|
|
52
46
|
|
|
53
47
|
with patch.object(
|
|
@@ -59,12 +53,10 @@ class TestTokenAdapter:
|
|
|
59
53
|
assert data == mock_token_data
|
|
60
54
|
|
|
61
55
|
def test_adapter_type(self, adapter):
|
|
62
|
-
"""Test adapter has adapter_type"""
|
|
63
56
|
assert adapter.adapter_type == "TOKEN"
|
|
64
57
|
|
|
65
58
|
@pytest.mark.asyncio
|
|
66
59
|
async def test_get_token_price_success(self, adapter):
|
|
67
|
-
"""Test successful token price retrieval"""
|
|
68
60
|
mock_token_data = {
|
|
69
61
|
"current_price": 1.50,
|
|
70
62
|
"price_change_24h": 0.05,
|
|
@@ -85,11 +77,10 @@ class TestTokenAdapter:
|
|
|
85
77
|
assert data["symbol"] == "TEST"
|
|
86
78
|
assert data["name"] == "Test Token"
|
|
87
79
|
assert data["total_volume"] == 50000
|
|
88
|
-
assert data["price_change_percentage_24h"] == 5.0
|
|
80
|
+
assert data["price_change_percentage_24h"] == 5.0
|
|
89
81
|
|
|
90
82
|
@pytest.mark.asyncio
|
|
91
83
|
async def test_get_token_price_not_found(self, adapter):
|
|
92
|
-
"""Test token price not found"""
|
|
93
84
|
with patch.object(adapter.token_client, "get_token_details", return_value=None):
|
|
94
85
|
success, data = await adapter.get_token_price("invalid-token")
|
|
95
86
|
|
|
@@ -98,7 +89,6 @@ class TestTokenAdapter:
|
|
|
98
89
|
|
|
99
90
|
@pytest.mark.asyncio
|
|
100
91
|
async def test_get_gas_token_success(self, adapter):
|
|
101
|
-
"""Test successful gas token retrieval"""
|
|
102
92
|
mock_gas_token_data = {
|
|
103
93
|
"id": "ethereum_0x0000000000000000000000000000000000000000",
|
|
104
94
|
"token_id": "ethereum_0x0000000000000000000000000000000000000000",
|
|
@@ -119,7 +109,6 @@ class TestTokenAdapter:
|
|
|
119
109
|
|
|
120
110
|
@pytest.mark.asyncio
|
|
121
111
|
async def test_get_gas_token_not_found(self, adapter):
|
|
122
|
-
"""Test gas token not found"""
|
|
123
112
|
with patch.object(adapter.token_client, "get_gas_token", return_value=None):
|
|
124
113
|
success, data = await adapter.get_gas_token("invalid-chain")
|
|
125
114
|
|
wayfinder_paths/conftest.py
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Conftest for wayfinder-paths package tests.
|
|
3
|
-
Adds wayfinder-paths directory to Python path for imports.
|
|
4
|
-
This must run early, so imports like 'from tests.test_utils' work.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
1
|
import sys
|
|
8
2
|
from pathlib import Path
|
|
9
3
|
|
|
10
|
-
# Add wayfinder-paths directory to Python path for imports (for tests.test_utils)
|
|
11
4
|
# This needs to be at index 0 to take precedence over repo root 'tests/' directory
|
|
12
5
|
_wayfinder_path_dir = Path(__file__).parent
|
|
13
6
|
_wayfinder_path_str = str(_wayfinder_path_dir)
|
|
14
7
|
|
|
15
8
|
|
|
16
9
|
def pytest_configure(config):
|
|
17
|
-
"""Configure pytest - runs early to set up imports."""
|
|
18
10
|
if _wayfinder_path_str not in sys.path:
|
|
19
11
|
sys.path.insert(0, _wayfinder_path_str)
|
|
20
12
|
elif sys.path.index(_wayfinder_path_str) > 0:
|
wayfinder_paths/core/__init__.py
CHANGED
|
@@ -7,8 +7,6 @@ from loguru import logger
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class BaseAdapter(ABC):
|
|
10
|
-
"""Base adapter class for exchange/protocol integrations"""
|
|
11
|
-
|
|
12
10
|
adapter_type: str | None = None
|
|
13
11
|
|
|
14
12
|
def __init__(self, name: str, config: dict[str, Any] | None = None):
|
|
@@ -17,24 +15,9 @@ class BaseAdapter(ABC):
|
|
|
17
15
|
self.logger = logger.bind(adapter=self.__class__.__name__)
|
|
18
16
|
|
|
19
17
|
async def connect(self) -> bool:
|
|
20
|
-
"""Optional: establish connectivity. Defaults to True."""
|
|
21
18
|
return True
|
|
22
19
|
|
|
23
20
|
async def get_balance(self, asset: str) -> dict[str, Any]:
|
|
24
|
-
"""
|
|
25
|
-
Get balance for an asset.
|
|
26
|
-
Optional method that can be overridden by subclasses.
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
asset: Asset identifier (token address, token ID, etc.).
|
|
30
|
-
|
|
31
|
-
Returns:
|
|
32
|
-
Dictionary containing balance information.
|
|
33
|
-
|
|
34
|
-
Raises:
|
|
35
|
-
ValueError: If asset is empty or invalid.
|
|
36
|
-
NotImplementedError: If this adapter does not support balance queries.
|
|
37
|
-
"""
|
|
38
21
|
if not asset or not isinstance(asset, str) or not asset.strip():
|
|
39
22
|
raise ValueError("asset must be a non-empty string")
|
|
40
23
|
raise NotImplementedError(
|
|
@@ -42,10 +25,6 @@ class BaseAdapter(ABC):
|
|
|
42
25
|
)
|
|
43
26
|
|
|
44
27
|
async def health_check(self) -> dict[str, Any]:
|
|
45
|
-
"""
|
|
46
|
-
Check adapter health and connectivity
|
|
47
|
-
Returns: Health status dictionary
|
|
48
|
-
"""
|
|
49
28
|
try:
|
|
50
29
|
connected = await self.connect()
|
|
51
30
|
return {
|
|
@@ -61,5 +40,4 @@ class BaseAdapter(ABC):
|
|
|
61
40
|
}
|
|
62
41
|
|
|
63
42
|
async def close(self) -> None:
|
|
64
|
-
"""Clean up resources"""
|
|
65
43
|
pass
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"""Pydantic models for ledger operations."""
|
|
2
|
-
|
|
3
1
|
from typing import Annotated, Any, Literal
|
|
4
2
|
|
|
5
3
|
from pydantic import BaseModel, Field
|
|
@@ -12,8 +10,6 @@ class OperationBase(BaseModel):
|
|
|
12
10
|
|
|
13
11
|
|
|
14
12
|
class SWAP(OperationBase):
|
|
15
|
-
"""Swap operation."""
|
|
16
|
-
|
|
17
13
|
type: Literal["SWAP"] = "SWAP"
|
|
18
14
|
from_token_id: str
|
|
19
15
|
to_token_id: str
|
|
@@ -38,7 +34,6 @@ class UNLEND(OperationBase):
|
|
|
38
34
|
|
|
39
35
|
|
|
40
36
|
# Type alias for operation types (currently only SWAP is used)
|
|
41
|
-
# Add more operation types here as needed
|
|
42
37
|
Operation = SWAP | LEND | UNLEND
|
|
43
38
|
|
|
44
39
|
|
|
@@ -10,22 +10,6 @@ def block_bootstrap_paths(
|
|
|
10
10
|
sims: int,
|
|
11
11
|
rng: random.Random,
|
|
12
12
|
) -> list[tuple[list[float], ...]]:
|
|
13
|
-
"""
|
|
14
|
-
Block-bootstrap aligned series together.
|
|
15
|
-
|
|
16
|
-
Samples contiguous blocks from the same indices across every series so their
|
|
17
|
-
time alignment is preserved (useful for resampling correlated paths).
|
|
18
|
-
|
|
19
|
-
Args:
|
|
20
|
-
*series: One or more equal-frequency series (funding, closes, highs, ...)
|
|
21
|
-
block_hours: Block length for sampling (clamped to [1, base_len])
|
|
22
|
-
sims: Number of bootstrap paths to return
|
|
23
|
-
rng: Random generator
|
|
24
|
-
|
|
25
|
-
Returns:
|
|
26
|
-
List of tuples, each containing resampled series lists (truncated to the
|
|
27
|
-
shared base length).
|
|
28
|
-
"""
|
|
29
13
|
if sims <= 0 or not series:
|
|
30
14
|
return []
|
|
31
15
|
|
|
@@ -6,12 +6,10 @@ from statistics import NormalDist
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def z_from_conf(confidence: float) -> float:
|
|
9
|
-
"""Return the two-sided z-score for a confidence level (e.g. 0.975)."""
|
|
10
9
|
return NormalDist().inv_cdf((1 + float(confidence)) / 2)
|
|
11
10
|
|
|
12
11
|
|
|
13
12
|
def rolling_min_sum(arr: Sequence[float], window: int) -> float:
|
|
14
|
-
"""Return the minimum rolling window sum over `arr`."""
|
|
15
13
|
values = list(arr)
|
|
16
14
|
if window <= 0:
|
|
17
15
|
return 0.0
|
|
@@ -27,11 +25,6 @@ def rolling_min_sum(arr: Sequence[float], window: int) -> float:
|
|
|
27
25
|
|
|
28
26
|
|
|
29
27
|
def percentile(sorted_values: Sequence[float], pct: float) -> float:
|
|
30
|
-
"""
|
|
31
|
-
Inclusive percentile on a pre-sorted list.
|
|
32
|
-
|
|
33
|
-
Mirrors a simple linear interpolation between closest ranks.
|
|
34
|
-
"""
|
|
35
28
|
values = list(sorted_values)
|
|
36
29
|
if not values:
|
|
37
30
|
return float("nan")
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
"""Tests for core analytics modules (bootstrap, stats)."""
|
|
2
|
-
|
|
3
1
|
import math
|
|
4
2
|
import random
|
|
5
3
|
|
|
@@ -12,29 +10,23 @@ from wayfinder_paths.core.analytics.stats import (
|
|
|
12
10
|
|
|
13
11
|
|
|
14
12
|
class TestBlockBootstrapPaths:
|
|
15
|
-
"""Tests for block_bootstrap_paths function."""
|
|
16
|
-
|
|
17
13
|
def test_returns_empty_when_sims_zero(self):
|
|
18
|
-
"""Should return empty list when sims=0."""
|
|
19
14
|
result = block_bootstrap_paths(
|
|
20
15
|
[1.0, 2.0, 3.0], block_hours=2, sims=0, rng=random.Random(42)
|
|
21
16
|
)
|
|
22
17
|
assert result == []
|
|
23
18
|
|
|
24
19
|
def test_returns_empty_when_series_empty(self):
|
|
25
|
-
"""Should return empty list when no series provided."""
|
|
26
20
|
result = block_bootstrap_paths(block_hours=2, sims=10, rng=random.Random(42))
|
|
27
21
|
assert result == []
|
|
28
22
|
|
|
29
23
|
def test_returns_empty_when_base_len_one(self):
|
|
30
|
-
"""Should return empty list when series has only one element."""
|
|
31
24
|
result = block_bootstrap_paths(
|
|
32
25
|
[1.0], block_hours=2, sims=10, rng=random.Random(42)
|
|
33
26
|
)
|
|
34
27
|
assert result == []
|
|
35
28
|
|
|
36
29
|
def test_single_series(self):
|
|
37
|
-
"""Should bootstrap a single series correctly."""
|
|
38
30
|
series = [1.0, 2.0, 3.0, 4.0, 5.0]
|
|
39
31
|
result = block_bootstrap_paths(
|
|
40
32
|
series, block_hours=2, sims=5, rng=random.Random(42)
|
|
@@ -42,11 +34,10 @@ class TestBlockBootstrapPaths:
|
|
|
42
34
|
|
|
43
35
|
assert len(result) == 5
|
|
44
36
|
for path in result:
|
|
45
|
-
assert len(path) == 1
|
|
46
|
-
assert len(path[0]) == 5
|
|
37
|
+
assert len(path) == 1
|
|
38
|
+
assert len(path[0]) == 5
|
|
47
39
|
|
|
48
40
|
def test_multiple_aligned_series(self):
|
|
49
|
-
"""Should bootstrap multiple series with aligned indices."""
|
|
50
41
|
series_a = [1.0, 2.0, 3.0, 4.0, 5.0]
|
|
51
42
|
series_b = [10.0, 20.0, 30.0, 40.0, 50.0]
|
|
52
43
|
result = block_bootstrap_paths(
|
|
@@ -55,12 +46,11 @@ class TestBlockBootstrapPaths:
|
|
|
55
46
|
|
|
56
47
|
assert len(result) == 3
|
|
57
48
|
for path in result:
|
|
58
|
-
assert len(path) == 2
|
|
49
|
+
assert len(path) == 2
|
|
59
50
|
assert len(path[0]) == 5
|
|
60
51
|
assert len(path[1]) == 5
|
|
61
52
|
|
|
62
53
|
def test_preserves_length(self):
|
|
63
|
-
"""Bootstrapped paths should have same length as input."""
|
|
64
54
|
series = list(range(100))
|
|
65
55
|
series_float = [float(x) for x in series]
|
|
66
56
|
result = block_bootstrap_paths(
|
|
@@ -71,7 +61,6 @@ class TestBlockBootstrapPaths:
|
|
|
71
61
|
assert len(path[0]) == 100
|
|
72
62
|
|
|
73
63
|
def test_block_clamping(self):
|
|
74
|
-
"""Block hours should be clamped to valid range."""
|
|
75
64
|
series = [1.0, 2.0, 3.0]
|
|
76
65
|
|
|
77
66
|
# Block larger than series - should still work
|
|
@@ -88,48 +77,37 @@ class TestBlockBootstrapPaths:
|
|
|
88
77
|
|
|
89
78
|
|
|
90
79
|
class TestZFromConf:
|
|
91
|
-
"""Tests for z_from_conf function."""
|
|
92
|
-
|
|
93
80
|
def test_95_confidence(self):
|
|
94
|
-
"""95% confidence should give z ≈ 1.96."""
|
|
95
81
|
z = z_from_conf(0.95)
|
|
96
82
|
assert 1.95 < z < 1.97
|
|
97
83
|
|
|
98
84
|
def test_99_confidence(self):
|
|
99
|
-
"""99% confidence should give z ≈ 2.576."""
|
|
100
85
|
z = z_from_conf(0.99)
|
|
101
86
|
assert 2.57 < z < 2.58
|
|
102
87
|
|
|
103
88
|
def test_90_confidence(self):
|
|
104
|
-
"""90% confidence should give z ≈ 1.645."""
|
|
105
89
|
z = z_from_conf(0.90)
|
|
106
90
|
assert 1.64 < z < 1.66
|
|
107
91
|
|
|
108
92
|
|
|
109
93
|
class TestRollingMinSum:
|
|
110
|
-
"""Tests for rolling_min_sum function."""
|
|
111
|
-
|
|
112
94
|
def test_basic(self):
|
|
113
|
-
"""Basic rolling min sum calculation."""
|
|
114
95
|
arr = [1, -2, 3, -4, 5]
|
|
115
96
|
result = rolling_min_sum(arr, 2)
|
|
116
97
|
# Windows: [1,-2]=-1, [-2,3]=1, [3,-4]=-1, [-4,5]=1
|
|
117
98
|
assert result == -1
|
|
118
99
|
|
|
119
100
|
def test_window_larger_than_arr(self):
|
|
120
|
-
"""Window larger than array returns sum of array."""
|
|
121
101
|
arr = [1.0, 2.0, 3.0]
|
|
122
102
|
result = rolling_min_sum(arr, 10)
|
|
123
103
|
assert result == 6.0
|
|
124
104
|
|
|
125
105
|
def test_window_zero(self):
|
|
126
|
-
"""Window of zero returns 0."""
|
|
127
106
|
arr = [1.0, 2.0, 3.0]
|
|
128
107
|
result = rolling_min_sum(arr, 0)
|
|
129
108
|
assert result == 0.0
|
|
130
109
|
|
|
131
110
|
def test_all_negative(self):
|
|
132
|
-
"""All negative values."""
|
|
133
111
|
arr = [-1.0, -2.0, -3.0, -4.0]
|
|
134
112
|
result = rolling_min_sum(arr, 2)
|
|
135
113
|
# Windows: [-1,-2]=-3, [-2,-3]=-5, [-3,-4]=-7
|
|
@@ -137,34 +115,27 @@ class TestRollingMinSum:
|
|
|
137
115
|
|
|
138
116
|
|
|
139
117
|
class TestPercentile:
|
|
140
|
-
"""Tests for percentile function."""
|
|
141
|
-
|
|
142
118
|
def test_empty_returns_nan(self):
|
|
143
|
-
"""Empty list returns nan."""
|
|
144
119
|
result = percentile([], 0.5)
|
|
145
120
|
assert math.isnan(result)
|
|
146
121
|
|
|
147
122
|
def test_single_value(self):
|
|
148
|
-
"""Single value returns that value regardless of percentile."""
|
|
149
123
|
assert percentile([42.0], 0.0) == 42.0
|
|
150
124
|
assert percentile([42.0], 0.5) == 42.0
|
|
151
125
|
assert percentile([42.0], 1.0) == 42.0
|
|
152
126
|
|
|
153
127
|
def test_median(self):
|
|
154
|
-
"""50th percentile (median) calculation."""
|
|
155
128
|
sorted_values = [1.0, 2.0, 3.0, 4.0, 5.0]
|
|
156
129
|
result = percentile(sorted_values, 0.5)
|
|
157
130
|
assert result == 3.0
|
|
158
131
|
|
|
159
132
|
def test_interpolation(self):
|
|
160
|
-
"""Percentile with interpolation."""
|
|
161
133
|
sorted_values = [0.0, 10.0]
|
|
162
134
|
# 25th percentile should interpolate to 2.5
|
|
163
135
|
result = percentile(sorted_values, 0.25)
|
|
164
136
|
assert result == 2.5
|
|
165
137
|
|
|
166
138
|
def test_bounds_clamped(self):
|
|
167
|
-
"""Percentile values outside [0,1] are clamped."""
|
|
168
139
|
sorted_values = [1.0, 2.0, 3.0]
|
|
169
|
-
assert percentile(sorted_values, -1.0) == 1.0
|
|
170
|
-
assert percentile(sorted_values, 2.0) == 3.0
|
|
140
|
+
assert percentile(sorted_values, -1.0) == 1.0
|
|
141
|
+
assert percentile(sorted_values, 2.0) == 3.0
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
"""
|
|
2
|
-
BRAP (Bridge/Router/Adapter Protocol) Client
|
|
3
|
-
Provides access to quote operations via the blockchain quote endpoint.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
1
|
from __future__ import annotations
|
|
7
2
|
|
|
8
3
|
import time
|
|
@@ -15,8 +10,6 @@ from wayfinder_paths.core.config import get_api_base_url
|
|
|
15
10
|
|
|
16
11
|
|
|
17
12
|
class QuoteTx(TypedDict, total=False):
|
|
18
|
-
"""Quote transaction data structure"""
|
|
19
|
-
|
|
20
13
|
data: str
|
|
21
14
|
to: str
|
|
22
15
|
value: str
|
|
@@ -24,8 +17,6 @@ class QuoteTx(TypedDict, total=False):
|
|
|
24
17
|
|
|
25
18
|
|
|
26
19
|
class QuoteData(TypedDict):
|
|
27
|
-
"""Quote data structure"""
|
|
28
|
-
|
|
29
20
|
gas: Required[str]
|
|
30
21
|
amountOut: Required[str]
|
|
31
22
|
priceImpact: Required[int]
|
|
@@ -37,8 +28,6 @@ class QuoteData(TypedDict):
|
|
|
37
28
|
|
|
38
29
|
|
|
39
30
|
class FeeBreakdown(TypedDict):
|
|
40
|
-
"""Fee breakdown structure"""
|
|
41
|
-
|
|
42
31
|
name: Required[str]
|
|
43
32
|
amount: Required[int]
|
|
44
33
|
amount_usd: Required[float]
|
|
@@ -47,15 +36,11 @@ class FeeBreakdown(TypedDict):
|
|
|
47
36
|
|
|
48
37
|
|
|
49
38
|
class FeeEstimate(TypedDict):
|
|
50
|
-
"""Fee estimate structure"""
|
|
51
|
-
|
|
52
39
|
fee_total_usd: Required[float]
|
|
53
40
|
fee_breakdown: Required[list[FeeBreakdown]]
|
|
54
41
|
|
|
55
42
|
|
|
56
43
|
class Calldata(TypedDict, total=False):
|
|
57
|
-
"""Calldata structure"""
|
|
58
|
-
|
|
59
44
|
data: str
|
|
60
45
|
to: str
|
|
61
46
|
value: str
|
|
@@ -63,8 +48,6 @@ class Calldata(TypedDict, total=False):
|
|
|
63
48
|
|
|
64
49
|
|
|
65
50
|
class BRAPQuoteEntry(TypedDict):
|
|
66
|
-
"""BRAP quote entry structure"""
|
|
67
|
-
|
|
68
51
|
provider: Required[str]
|
|
69
52
|
quote: Required[QuoteData]
|
|
70
53
|
calldata: Required[Calldata]
|
|
@@ -82,15 +65,11 @@ class BRAPQuoteEntry(TypedDict):
|
|
|
82
65
|
|
|
83
66
|
|
|
84
67
|
class BRAPQuoteResponse(TypedDict):
|
|
85
|
-
"""BRAP quote response structure"""
|
|
86
|
-
|
|
87
68
|
quotes: Required[list[BRAPQuoteEntry]]
|
|
88
69
|
best_quote: Required[BRAPQuoteEntry]
|
|
89
70
|
|
|
90
71
|
|
|
91
72
|
class BRAPClient(WayfinderClient):
|
|
92
|
-
"""Client for BRAP quote operations"""
|
|
93
|
-
|
|
94
73
|
def __init__(self):
|
|
95
74
|
super().__init__()
|
|
96
75
|
self.api_base_url = f"{get_api_base_url()}/v1/blockchain/braps"
|
|
@@ -105,20 +84,6 @@ class BRAPClient(WayfinderClient):
|
|
|
105
84
|
from_wallet: str,
|
|
106
85
|
from_amount: str,
|
|
107
86
|
) -> BRAPQuoteResponse: # type: ignore # noqa: E501
|
|
108
|
-
"""
|
|
109
|
-
Get a quote for a bridge/swap operation.
|
|
110
|
-
|
|
111
|
-
Args:
|
|
112
|
-
from_token: Source token contract address
|
|
113
|
-
to_token: Destination token contract address
|
|
114
|
-
from_chain: Source chain ID
|
|
115
|
-
to_chain: Destination chain ID
|
|
116
|
-
from_wallet: Source wallet address
|
|
117
|
-
from_amount: Amount to swap (in smallest units)
|
|
118
|
-
|
|
119
|
-
Returns:
|
|
120
|
-
Quote response including quotes array and best_quote
|
|
121
|
-
"""
|
|
122
87
|
logger.info(
|
|
123
88
|
f"Getting BRAP quote: {from_token} -> {to_token} (chain {from_chain} -> {to_chain})"
|
|
124
89
|
)
|