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.

Files changed (98) hide show
  1. wayfinder_paths/__init__.py +0 -2
  2. wayfinder_paths/adapters/balance_adapter/README.md +59 -45
  3. wayfinder_paths/adapters/balance_adapter/adapter.py +0 -21
  4. wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -14
  5. wayfinder_paths/adapters/brap_adapter/README.md +61 -184
  6. wayfinder_paths/adapters/brap_adapter/__init__.py +0 -4
  7. wayfinder_paths/adapters/brap_adapter/adapter.py +0 -147
  8. wayfinder_paths/adapters/brap_adapter/test_adapter.py +0 -15
  9. wayfinder_paths/adapters/hyperlend_adapter/__init__.py +0 -4
  10. wayfinder_paths/adapters/hyperlend_adapter/adapter.py +0 -9
  11. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +0 -17
  12. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +3 -312
  13. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +1 -71
  14. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +0 -57
  15. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +0 -17
  16. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +2 -42
  17. wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +1 -9
  18. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +15 -47
  19. wayfinder_paths/adapters/hyperliquid_adapter/utils.py +0 -7
  20. wayfinder_paths/adapters/ledger_adapter/README.md +54 -74
  21. wayfinder_paths/adapters/ledger_adapter/__init__.py +0 -4
  22. wayfinder_paths/adapters/ledger_adapter/adapter.py +0 -106
  23. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +0 -12
  24. wayfinder_paths/adapters/moonwell_adapter/README.md +67 -106
  25. wayfinder_paths/adapters/moonwell_adapter/__init__.py +0 -4
  26. wayfinder_paths/adapters/moonwell_adapter/adapter.py +9 -121
  27. wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +84 -83
  28. wayfinder_paths/adapters/pool_adapter/README.md +30 -51
  29. wayfinder_paths/adapters/pool_adapter/__init__.py +0 -4
  30. wayfinder_paths/adapters/pool_adapter/adapter.py +0 -19
  31. wayfinder_paths/adapters/pool_adapter/test_adapter.py +0 -8
  32. wayfinder_paths/adapters/token_adapter/README.md +41 -49
  33. wayfinder_paths/adapters/token_adapter/adapter.py +0 -32
  34. wayfinder_paths/adapters/token_adapter/test_adapter.py +1 -12
  35. wayfinder_paths/conftest.py +0 -8
  36. wayfinder_paths/core/__init__.py +0 -2
  37. wayfinder_paths/core/adapters/BaseAdapter.py +0 -22
  38. wayfinder_paths/core/adapters/__init__.py +0 -5
  39. wayfinder_paths/core/adapters/models.py +0 -5
  40. wayfinder_paths/core/analytics/__init__.py +0 -2
  41. wayfinder_paths/core/analytics/bootstrap.py +0 -16
  42. wayfinder_paths/core/analytics/stats.py +0 -7
  43. wayfinder_paths/core/analytics/test_analytics.py +5 -34
  44. wayfinder_paths/core/clients/BRAPClient.py +0 -35
  45. wayfinder_paths/core/clients/ClientManager.py +0 -51
  46. wayfinder_paths/core/clients/HyperlendClient.py +0 -77
  47. wayfinder_paths/core/clients/LedgerClient.py +2 -122
  48. wayfinder_paths/core/clients/PoolClient.py +0 -2
  49. wayfinder_paths/core/clients/TokenClient.py +0 -39
  50. wayfinder_paths/core/clients/WalletClient.py +0 -15
  51. wayfinder_paths/core/clients/WayfinderClient.py +0 -24
  52. wayfinder_paths/core/clients/__init__.py +0 -4
  53. wayfinder_paths/core/clients/protocols.py +25 -98
  54. wayfinder_paths/core/config.py +0 -24
  55. wayfinder_paths/core/constants/__init__.py +0 -7
  56. wayfinder_paths/core/constants/base.py +2 -9
  57. wayfinder_paths/core/constants/erc20_abi.py +0 -5
  58. wayfinder_paths/core/constants/hyperlend_abi.py +0 -7
  59. wayfinder_paths/core/constants/moonwell_abi.py +0 -35
  60. wayfinder_paths/core/engine/StrategyJob.py +0 -32
  61. wayfinder_paths/core/strategies/Strategy.py +0 -99
  62. wayfinder_paths/core/strategies/__init__.py +0 -2
  63. wayfinder_paths/core/utils/__init__.py +0 -1
  64. wayfinder_paths/core/utils/erc20_service.py +0 -1
  65. wayfinder_paths/core/utils/evm_helpers.py +0 -50
  66. wayfinder_paths/core/utils/transaction.py +0 -1
  67. wayfinder_paths/run_strategy.py +0 -46
  68. wayfinder_paths/scripts/create_strategy.py +0 -17
  69. wayfinder_paths/scripts/make_wallets.py +1 -4
  70. wayfinder_paths/strategies/basis_trading_strategy/README.md +71 -163
  71. wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +0 -24
  72. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +36 -400
  73. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +15 -64
  74. wayfinder_paths/strategies/basis_trading_strategy/types.py +0 -4
  75. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +65 -56
  76. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +4 -27
  77. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +0 -10
  78. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +71 -72
  79. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +23 -227
  80. wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +120 -113
  81. wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +64 -59
  82. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +4 -44
  83. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +2 -35
  84. wayfinder_paths/templates/adapter/README.md +107 -46
  85. wayfinder_paths/templates/adapter/adapter.py +0 -9
  86. wayfinder_paths/templates/adapter/test_adapter.py +0 -19
  87. wayfinder_paths/templates/strategy/README.md +113 -59
  88. wayfinder_paths/templates/strategy/strategy.py +0 -22
  89. wayfinder_paths/templates/strategy/test_strategy.py +0 -28
  90. wayfinder_paths/tests/test_test_coverage.py +2 -12
  91. wayfinder_paths/tests/test_utils.py +1 -31
  92. wayfinder_paths-0.1.20.dist-info/METADATA +355 -0
  93. wayfinder_paths-0.1.20.dist-info/RECORD +129 -0
  94. wayfinder_paths/core/adapters/base.py +0 -5
  95. wayfinder_paths-0.1.19.dist-info/METADATA +0 -592
  96. wayfinder_paths-0.1.19.dist-info/RECORD +0 -130
  97. {wayfinder_paths-0.1.19.dist-info → wayfinder_paths-0.1.20.dist-info}/LICENSE +0 -0
  98. {wayfinder_paths-0.1.19.dist-info → wayfinder_paths-0.1.20.dist-info}/WHEEL +0 -0
@@ -1,89 +1,81 @@
1
1
  # Token Adapter
2
2
 
3
- A Wayfinder adapter that wraps the `_get_token_via_api` method for fetching token data via HeadlessAPIViewSet endpoints. This adapter supports both address and token_id lookups.
3
+ Adapter for token metadata and price feeds.
4
4
 
5
- ## Capabilities
5
+ - **Type**: `TOKEN`
6
+ - **Module**: `wayfinder_paths.adapters.token_adapter.adapter.TokenAdapter`
6
7
 
7
- - `token.read`: Retrieve token information by address or token ID
8
+ ## Overview
8
9
 
9
- ## Configuration
10
-
11
- The adapter uses the TokenClient which automatically handles authentication and API configuration through the Wayfinder settings. No additional configuration is required.
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
- ### Get Token Metadata
23
+ ## Methods
24
+
25
+ ### get_token
30
26
 
31
- Both contract addresses and token ids are supported with the same method. Pass whichever identifier you have:
27
+ Get token metadata by address or token ID.
32
28
 
33
29
  ```python
34
- success, data = await adapter.get_token("0x1234...") # by address
35
- # or
36
- success, data = await adapter.get_token("usd-coin-base") # by token id
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
- else:
41
- print(f"Error: {data}")
37
+ print(f"Symbol: {data['symbol']}")
38
+ print(f"Decimals: {data['decimals']}")
39
+ print(f"Address: {data['address']}")
42
40
  ```
43
41
 
44
- ### Get Token Price
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("token-123")
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
- ### Get Gas Token
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']} - {data['name']}")
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
- ## API Endpoints
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
- ## Error Handling
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
- All methods return a tuple of `(success: bool, data: Any)` where:
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
- ## Health Check
75
+ - `TokenClient` - Low-level API client
79
76
 
80
- The adapter includes a health check that tests connectivity to the API:
77
+ ## Testing
81
78
 
82
- ```python
83
- health = await adapter.health_check()
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 # 0.05 * 100
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
 
@@ -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:
@@ -1,5 +1,3 @@
1
- """Wayfinder Paths Core Engine"""
2
-
3
1
  from wayfinder_paths.core.adapters.BaseAdapter import BaseAdapter
4
2
  from wayfinder_paths.core.engine.StrategyJob import StrategyJob
5
3
  from wayfinder_paths.core.strategies.Strategy import (
@@ -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 +0,0 @@
1
- """Core Adapter Module - SDK surface re-export"""
2
-
3
- from .base import BaseAdapter
4
-
5
- __all__ = ["BaseAdapter"]
@@ -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
 
@@ -1,5 +1,3 @@
1
- """Reusable, strategy-agnostic analytics helpers."""
2
-
3
1
  from .bootstrap import block_bootstrap_paths
4
2
  from .stats import percentile, rolling_min_sum, z_from_conf
5
3
 
@@ -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 # One series
46
- assert len(path[0]) == 5 # Same length as original
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 # Two series
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 # Clamped to 0
170
- assert percentile(sorted_values, 2.0) == 3.0 # Clamped to 1
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
  )