wayfinder-paths 0.1.6__py3-none-any.whl → 0.1.8__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/adapters/balance_adapter/README.md +0 -10
- wayfinder_paths/adapters/balance_adapter/adapter.py +0 -20
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -30
- wayfinder_paths/adapters/brap_adapter/adapter.py +3 -2
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +9 -13
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +14 -7
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +18 -0
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1093 -0
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +549 -0
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +1050 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +126 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +219 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +220 -0
- wayfinder_paths/adapters/hyperliquid_adapter/utils.py +134 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +7 -6
- wayfinder_paths/adapters/pool_adapter/README.md +3 -28
- wayfinder_paths/adapters/pool_adapter/adapter.py +0 -72
- wayfinder_paths/adapters/pool_adapter/examples.json +0 -43
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +4 -54
- wayfinder_paths/adapters/token_adapter/test_adapter.py +4 -14
- wayfinder_paths/core/adapters/models.py +9 -4
- wayfinder_paths/core/analytics/__init__.py +11 -0
- wayfinder_paths/core/analytics/bootstrap.py +57 -0
- wayfinder_paths/core/analytics/stats.py +48 -0
- wayfinder_paths/core/analytics/test_analytics.py +170 -0
- wayfinder_paths/core/clients/BRAPClient.py +1 -0
- wayfinder_paths/core/clients/LedgerClient.py +2 -7
- wayfinder_paths/core/clients/PoolClient.py +0 -16
- wayfinder_paths/core/clients/WalletClient.py +0 -27
- wayfinder_paths/core/clients/protocols.py +104 -18
- wayfinder_paths/scripts/make_wallets.py +9 -0
- wayfinder_paths/scripts/run_strategy.py +124 -0
- wayfinder_paths/strategies/basis_trading_strategy/README.md +213 -0
- wayfinder_paths/strategies/basis_trading_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/basis_trading_strategy/constants.py +1 -0
- wayfinder_paths/strategies/basis_trading_strategy/examples.json +16 -0
- wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +23 -0
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1011 -0
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +4522 -0
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +727 -0
- wayfinder_paths/strategies/basis_trading_strategy/types.py +39 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +1 -9
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +36 -5
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +367 -278
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +204 -7
- {wayfinder_paths-0.1.6.dist-info → wayfinder_paths-0.1.8.dist-info}/METADATA +32 -3
- {wayfinder_paths-0.1.6.dist-info → wayfinder_paths-0.1.8.dist-info}/RECORD +50 -27
- {wayfinder_paths-0.1.6.dist-info → wayfinder_paths-0.1.8.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.6.dist-info → wayfinder_paths-0.1.8.dist-info}/WHEEL +0 -0
|
@@ -176,7 +176,7 @@ class LedgerClient:
|
|
|
176
176
|
"offset": offset,
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
async def get_strategy_net_deposit(self, *, wallet_address: str) ->
|
|
179
|
+
async def get_strategy_net_deposit(self, *, wallet_address: str) -> float:
|
|
180
180
|
"""
|
|
181
181
|
Calculate the net deposit (deposits - withdrawals) for a strategy wallet.
|
|
182
182
|
|
|
@@ -210,12 +210,7 @@ class LedgerClient:
|
|
|
210
210
|
|
|
211
211
|
net_deposit = total_deposits - total_withdrawals
|
|
212
212
|
|
|
213
|
-
return
|
|
214
|
-
"net_deposit": str(net_deposit),
|
|
215
|
-
"total_deposits": str(total_deposits),
|
|
216
|
-
"total_withdrawals": str(total_withdrawals),
|
|
217
|
-
"wallet_address": wallet_address,
|
|
218
|
-
}
|
|
213
|
+
return float(net_deposit)
|
|
219
214
|
|
|
220
215
|
async def get_strategy_latest_transactions(
|
|
221
216
|
self, *, wallet_address: str, limit: int = 10
|
|
@@ -85,22 +85,6 @@ class PoolClient(WayfinderClient):
|
|
|
85
85
|
data = response.json()
|
|
86
86
|
return data.get("data", data)
|
|
87
87
|
|
|
88
|
-
async def get_all_pools(self, *, merge_external: bool | None = None) -> PoolList:
|
|
89
|
-
"""
|
|
90
|
-
Fetch all pools.
|
|
91
|
-
|
|
92
|
-
Example:
|
|
93
|
-
GET /api/v1/public/pools/?merge_external=false
|
|
94
|
-
"""
|
|
95
|
-
url = f"{self.api_base_url}/public/pools/"
|
|
96
|
-
params: dict[str, Any] = {}
|
|
97
|
-
if merge_external is not None:
|
|
98
|
-
params["merge_external"] = "true" if merge_external else "false"
|
|
99
|
-
response = await self._request("GET", url, params=params, headers={})
|
|
100
|
-
response.raise_for_status()
|
|
101
|
-
data = response.json()
|
|
102
|
-
return data.get("data", data)
|
|
103
|
-
|
|
104
88
|
async def get_llama_matches(self) -> dict[str, LlamaMatch]:
|
|
105
89
|
"""
|
|
106
90
|
Fetch Llama matches for pools.
|
|
@@ -92,30 +92,3 @@ class WalletClient(WayfinderClient):
|
|
|
92
92
|
response = await self._authed_request("POST", url, json=payload)
|
|
93
93
|
data = response.json()
|
|
94
94
|
return data.get("data", data)
|
|
95
|
-
|
|
96
|
-
async def get_all_enriched_token_balances_for_wallet(
|
|
97
|
-
self,
|
|
98
|
-
*,
|
|
99
|
-
wallet_address: str,
|
|
100
|
-
enrich: bool = True,
|
|
101
|
-
from_cache: bool = False,
|
|
102
|
-
add_llama: bool = True,
|
|
103
|
-
) -> EnrichedBalances:
|
|
104
|
-
"""
|
|
105
|
-
Fetch all token balances for a wallet with enrichment via the enriched endpoint.
|
|
106
|
-
|
|
107
|
-
Mirrors POST /api/v1/public/balances/enriched/
|
|
108
|
-
"""
|
|
109
|
-
url = f"{self.api_base_url}/public/balances/enriched/"
|
|
110
|
-
payload = {
|
|
111
|
-
"wallet_address": wallet_address,
|
|
112
|
-
"enrich": enrich,
|
|
113
|
-
"from_cache": from_cache,
|
|
114
|
-
"add_llama": add_llama,
|
|
115
|
-
}
|
|
116
|
-
try:
|
|
117
|
-
response = await self._authed_request("POST", url, json=payload)
|
|
118
|
-
data = response.json()
|
|
119
|
-
return data.get("data", data)
|
|
120
|
-
except Exception:
|
|
121
|
-
raise
|
|
@@ -20,7 +20,6 @@ if TYPE_CHECKING:
|
|
|
20
20
|
StableMarket,
|
|
21
21
|
)
|
|
22
22
|
from wayfinder_paths.core.clients.LedgerClient import (
|
|
23
|
-
NetDeposit,
|
|
24
23
|
StrategyTransactionList,
|
|
25
24
|
TransactionRecord,
|
|
26
25
|
)
|
|
@@ -36,7 +35,6 @@ if TYPE_CHECKING:
|
|
|
36
35
|
)
|
|
37
36
|
from wayfinder_paths.core.clients.TransactionClient import TransactionPayload
|
|
38
37
|
from wayfinder_paths.core.clients.WalletClient import (
|
|
39
|
-
EnrichedBalances,
|
|
40
38
|
PoolBalance,
|
|
41
39
|
TokenBalance,
|
|
42
40
|
)
|
|
@@ -113,7 +111,7 @@ class LedgerClientProtocol(Protocol):
|
|
|
113
111
|
"""Fetch a paginated list of transactions for a given strategy wallet"""
|
|
114
112
|
...
|
|
115
113
|
|
|
116
|
-
async def get_strategy_net_deposit(self, *, wallet_address: str) ->
|
|
114
|
+
async def get_strategy_net_deposit(self, *, wallet_address: str) -> float:
|
|
117
115
|
"""Fetch the net deposit (deposits - withdrawals) for a strategy"""
|
|
118
116
|
...
|
|
119
117
|
|
|
@@ -187,17 +185,6 @@ class WalletClientProtocol(Protocol):
|
|
|
187
185
|
"""Fetch a wallet's LP/share balance for a given pool address and chain"""
|
|
188
186
|
...
|
|
189
187
|
|
|
190
|
-
async def get_all_enriched_token_balances_for_wallet(
|
|
191
|
-
self,
|
|
192
|
-
*,
|
|
193
|
-
wallet_address: str,
|
|
194
|
-
enrich: bool = True,
|
|
195
|
-
from_cache: bool = False,
|
|
196
|
-
add_llama: bool = True,
|
|
197
|
-
) -> EnrichedBalances:
|
|
198
|
-
"""Fetch all token balances for a wallet with enrichment"""
|
|
199
|
-
...
|
|
200
|
-
|
|
201
188
|
|
|
202
189
|
class TransactionClientProtocol(Protocol):
|
|
203
190
|
"""Protocol for transaction operations"""
|
|
@@ -226,10 +213,6 @@ class PoolClientProtocol(Protocol):
|
|
|
226
213
|
"""Fetch pools by comma-separated pool ids"""
|
|
227
214
|
...
|
|
228
215
|
|
|
229
|
-
async def get_all_pools(self, *, merge_external: bool | None = None) -> PoolList:
|
|
230
|
-
"""Fetch all pools"""
|
|
231
|
-
...
|
|
232
|
-
|
|
233
216
|
async def get_llama_matches(self) -> dict[str, LlamaMatch]:
|
|
234
217
|
"""Fetch Llama matches for pools"""
|
|
235
218
|
...
|
|
@@ -300,3 +283,106 @@ class SimulationClientProtocol(Protocol):
|
|
|
300
283
|
) -> SimulationResult:
|
|
301
284
|
"""Simulate token swap operation"""
|
|
302
285
|
...
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class HyperliquidExecutorProtocol(Protocol):
|
|
289
|
+
"""Protocol for Hyperliquid order execution operations."""
|
|
290
|
+
|
|
291
|
+
async def place_market_order(
|
|
292
|
+
self,
|
|
293
|
+
*,
|
|
294
|
+
asset_id: int,
|
|
295
|
+
is_buy: bool,
|
|
296
|
+
slippage: float,
|
|
297
|
+
size: float,
|
|
298
|
+
address: str,
|
|
299
|
+
reduce_only: bool = False,
|
|
300
|
+
cloid: Any = None,
|
|
301
|
+
builder: dict[str, Any] | None = None,
|
|
302
|
+
) -> dict[str, Any]:
|
|
303
|
+
"""Place a market order."""
|
|
304
|
+
...
|
|
305
|
+
|
|
306
|
+
async def cancel_order(
|
|
307
|
+
self,
|
|
308
|
+
*,
|
|
309
|
+
asset_id: int,
|
|
310
|
+
order_id: int,
|
|
311
|
+
address: str,
|
|
312
|
+
) -> dict[str, Any]:
|
|
313
|
+
"""Cancel an open order."""
|
|
314
|
+
...
|
|
315
|
+
|
|
316
|
+
async def update_leverage(
|
|
317
|
+
self,
|
|
318
|
+
*,
|
|
319
|
+
asset_id: int,
|
|
320
|
+
leverage: int,
|
|
321
|
+
is_cross: bool,
|
|
322
|
+
address: str,
|
|
323
|
+
) -> dict[str, Any]:
|
|
324
|
+
"""Update leverage for an asset."""
|
|
325
|
+
...
|
|
326
|
+
|
|
327
|
+
async def transfer_spot_to_perp(
|
|
328
|
+
self,
|
|
329
|
+
*,
|
|
330
|
+
amount: float,
|
|
331
|
+
address: str,
|
|
332
|
+
) -> dict[str, Any]:
|
|
333
|
+
"""Transfer USDC from spot to perp balance."""
|
|
334
|
+
...
|
|
335
|
+
|
|
336
|
+
async def transfer_perp_to_spot(
|
|
337
|
+
self,
|
|
338
|
+
*,
|
|
339
|
+
amount: float,
|
|
340
|
+
address: str,
|
|
341
|
+
) -> dict[str, Any]:
|
|
342
|
+
"""Transfer USDC from perp to spot balance."""
|
|
343
|
+
...
|
|
344
|
+
|
|
345
|
+
async def place_stop_loss(
|
|
346
|
+
self,
|
|
347
|
+
*,
|
|
348
|
+
asset_id: int,
|
|
349
|
+
is_buy: bool,
|
|
350
|
+
trigger_price: float,
|
|
351
|
+
size: float,
|
|
352
|
+
address: str,
|
|
353
|
+
) -> dict[str, Any]:
|
|
354
|
+
"""Place a stop-loss order."""
|
|
355
|
+
...
|
|
356
|
+
|
|
357
|
+
async def place_limit_order(
|
|
358
|
+
self,
|
|
359
|
+
*,
|
|
360
|
+
asset_id: int,
|
|
361
|
+
is_buy: bool,
|
|
362
|
+
price: float,
|
|
363
|
+
size: float,
|
|
364
|
+
address: str,
|
|
365
|
+
reduce_only: bool = False,
|
|
366
|
+
builder: dict[str, Any] | None = None,
|
|
367
|
+
) -> dict[str, Any]:
|
|
368
|
+
"""Place a limit order."""
|
|
369
|
+
...
|
|
370
|
+
|
|
371
|
+
async def withdraw(
|
|
372
|
+
self,
|
|
373
|
+
*,
|
|
374
|
+
amount: float,
|
|
375
|
+
address: str,
|
|
376
|
+
) -> dict[str, Any]:
|
|
377
|
+
"""Withdraw USDC from Hyperliquid to Arbitrum."""
|
|
378
|
+
...
|
|
379
|
+
|
|
380
|
+
async def approve_builder_fee(
|
|
381
|
+
self,
|
|
382
|
+
*,
|
|
383
|
+
builder: str,
|
|
384
|
+
max_fee_rate: str,
|
|
385
|
+
address: str,
|
|
386
|
+
) -> dict[str, Any]:
|
|
387
|
+
"""Approve a builder fee for the user."""
|
|
388
|
+
...
|
|
@@ -62,8 +62,17 @@ def main():
|
|
|
62
62
|
default=None,
|
|
63
63
|
help="Create a wallet with a custom label (e.g., strategy name). If not provided, auto-generates labels.",
|
|
64
64
|
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--default",
|
|
67
|
+
action="store_true",
|
|
68
|
+
help="Create a default 'main' wallet if none exists (used by CI)",
|
|
69
|
+
)
|
|
65
70
|
args = parser.parse_args()
|
|
66
71
|
|
|
72
|
+
# --default is equivalent to -n 1 (create main wallet if needed)
|
|
73
|
+
if args.default and args.n == 0 and not args.label:
|
|
74
|
+
args.n = 1
|
|
75
|
+
|
|
67
76
|
args.out_dir.mkdir(parents=True, exist_ok=True)
|
|
68
77
|
|
|
69
78
|
# Load existing wallets
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _load_wallets(path: Path) -> list[dict[str, Any]]:
|
|
11
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
12
|
+
if not isinstance(data, list):
|
|
13
|
+
raise ValueError(f"Expected a list in {path}")
|
|
14
|
+
return [w for w in data if isinstance(w, dict)]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _find_wallet(wallets: list[dict[str, Any]], label: str) -> dict[str, Any]:
|
|
18
|
+
for w in wallets:
|
|
19
|
+
if w.get("label") == label:
|
|
20
|
+
return w
|
|
21
|
+
raise ValueError(f"Wallet label not found in wallets.json: {label}")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _get_strategy_class(strategy: str):
|
|
25
|
+
if strategy == "basis_trading_strategy":
|
|
26
|
+
from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
|
|
27
|
+
BasisTradingStrategy,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
return BasisTradingStrategy
|
|
31
|
+
|
|
32
|
+
if strategy == "hyperlend_stable_yield_strategy":
|
|
33
|
+
from wayfinder_paths.strategies.hyperlend_stable_yield_strategy.strategy import (
|
|
34
|
+
HyperlendStableYieldStrategy,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
return HyperlendStableYieldStrategy
|
|
38
|
+
|
|
39
|
+
raise ValueError(f"Unknown strategy: {strategy}")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
async def _run(args: argparse.Namespace) -> int:
|
|
43
|
+
repo_root = Path(__file__).resolve().parents[2]
|
|
44
|
+
wallets_path = (
|
|
45
|
+
Path(args.wallets).resolve() if args.wallets else repo_root / "wallets.json"
|
|
46
|
+
)
|
|
47
|
+
wallets = _load_wallets(wallets_path)
|
|
48
|
+
|
|
49
|
+
main_wallet = _find_wallet(wallets, args.main_wallet_label)
|
|
50
|
+
strategy_wallet = _find_wallet(wallets, args.strategy_wallet_label)
|
|
51
|
+
|
|
52
|
+
strategy_class = _get_strategy_class(args.strategy)
|
|
53
|
+
s = strategy_class(
|
|
54
|
+
{
|
|
55
|
+
"main_wallet": main_wallet,
|
|
56
|
+
"strategy_wallet": strategy_wallet,
|
|
57
|
+
},
|
|
58
|
+
simulation=args.simulation,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
await s.setup()
|
|
62
|
+
|
|
63
|
+
if args.command == "deposit":
|
|
64
|
+
ok, msg = await s.deposit(
|
|
65
|
+
main_token_amount=float(args.usdc), gas_token_amount=float(args.eth)
|
|
66
|
+
)
|
|
67
|
+
print(msg)
|
|
68
|
+
return 0 if ok else 1
|
|
69
|
+
|
|
70
|
+
if args.command == "update":
|
|
71
|
+
ok, msg = await s.update()
|
|
72
|
+
print(msg)
|
|
73
|
+
return 0 if ok else 1
|
|
74
|
+
|
|
75
|
+
if args.command == "withdraw":
|
|
76
|
+
ok, msg = await s.withdraw(
|
|
77
|
+
amount=float(args.amount) if args.amount is not None else None
|
|
78
|
+
)
|
|
79
|
+
print(msg)
|
|
80
|
+
return 0 if ok else 1
|
|
81
|
+
|
|
82
|
+
if args.command == "status":
|
|
83
|
+
st = await s.status()
|
|
84
|
+
print(json.dumps(st, indent=2, sort_keys=True))
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
raise ValueError(f"Unknown command: {args.command}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def main() -> int:
|
|
91
|
+
p = argparse.ArgumentParser(
|
|
92
|
+
description="Run a strategy locally (deposit/update/withdraw/status)."
|
|
93
|
+
)
|
|
94
|
+
p.add_argument(
|
|
95
|
+
"--strategy",
|
|
96
|
+
default="basis_trading_strategy",
|
|
97
|
+
choices=["basis_trading_strategy", "hyperlend_stable_yield_strategy"],
|
|
98
|
+
)
|
|
99
|
+
p.add_argument(
|
|
100
|
+
"--wallets", default=None, help="Path to wallets.json (default: repo root)"
|
|
101
|
+
)
|
|
102
|
+
p.add_argument("--main-wallet-label", default="main")
|
|
103
|
+
p.add_argument("--strategy-wallet-label", default="basis_trading_strategy")
|
|
104
|
+
p.add_argument("--simulation", action="store_true")
|
|
105
|
+
|
|
106
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
107
|
+
|
|
108
|
+
dep = sub.add_parser("deposit")
|
|
109
|
+
dep.add_argument("--usdc", required=True, type=float)
|
|
110
|
+
dep.add_argument("--eth", default=0.0, type=float)
|
|
111
|
+
|
|
112
|
+
sub.add_parser("update")
|
|
113
|
+
|
|
114
|
+
wd = sub.add_parser("withdraw")
|
|
115
|
+
wd.add_argument("--amount", default=None, type=float)
|
|
116
|
+
|
|
117
|
+
sub.add_parser("status")
|
|
118
|
+
|
|
119
|
+
args = p.parse_args()
|
|
120
|
+
return asyncio.run(_run(args))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Basis Trading Strategy
|
|
2
|
+
|
|
3
|
+
Delta-neutral basis trading on Hyperliquid that captures funding rate payments through matched spot long and perpetual short positions.
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
### Delta-Neutral Basis Trading
|
|
8
|
+
|
|
9
|
+
The strategy maintains market neutrality by holding equal-and-opposite positions:
|
|
10
|
+
- **Long Spot**: Buy the underlying asset (e.g., HYPE)
|
|
11
|
+
- **Short Perp**: Short the perpetual contract for the same asset
|
|
12
|
+
|
|
13
|
+
This creates a "basis trade" where:
|
|
14
|
+
- Price movements cancel out (if HYPE goes up 10%, spot gains +10%, perp loses -10%)
|
|
15
|
+
- You collect funding payments when longs pay shorts (positive funding rate)
|
|
16
|
+
- The position is "delta-neutral" - profit comes from funding, not price direction
|
|
17
|
+
|
|
18
|
+
### Position Sizing with Leverage
|
|
19
|
+
|
|
20
|
+
Given a deposit of `D` USDC and leverage `L`:
|
|
21
|
+
- **Order Size**: `order_usd = D * (L / (L + 1))`
|
|
22
|
+
- **Margin Reserved**: `D / (L + 1)`
|
|
23
|
+
|
|
24
|
+
Example with $100 deposit at 2x leverage:
|
|
25
|
+
- Order size: $100 * (2/3) = $66.67 per leg
|
|
26
|
+
- Margin: $100 / 3 = $33.33
|
|
27
|
+
|
|
28
|
+
## Opportunity Selection
|
|
29
|
+
|
|
30
|
+
### 1. Candidate Discovery
|
|
31
|
+
|
|
32
|
+
The strategy scans all Hyperliquid markets to find spot-perp pairs:
|
|
33
|
+
- Spots quoted in USDC that have matching perpetual contracts
|
|
34
|
+
- Filters: minimum open interest, daily volume, order book depth
|
|
35
|
+
|
|
36
|
+
### 2. Historical Analysis (Backtesting)
|
|
37
|
+
|
|
38
|
+
For each candidate, fetches up to 180 days of hourly data:
|
|
39
|
+
- **Funding rates**: Mean, volatility, negative hour fraction, worst 24h/7d sums
|
|
40
|
+
- **Price candles**: Hourly closes and highs for volatility calculation
|
|
41
|
+
|
|
42
|
+
### 3. Safe Leverage Calculation
|
|
43
|
+
|
|
44
|
+
Uses a deterministic "stress test" approach over rolling historical windows:
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
For each window of N hours:
|
|
48
|
+
- Track cumulative negative funding (adjusted for price run-up)
|
|
49
|
+
- Track maximum price run-up (high / entry - 1)
|
|
50
|
+
- Calculate buffer requirement:
|
|
51
|
+
buffer = maintenance_margin * (1 + runup) + runup + cum_neg_funding + fees
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The worst-case buffer across all windows determines the maximum safe leverage:
|
|
55
|
+
- If buffer requirement is 50%, max safe leverage = 2x
|
|
56
|
+
- If buffer requirement is 33%, max safe leverage = 3x
|
|
57
|
+
|
|
58
|
+
### 4. Bootstrap Simulation (Optional)
|
|
59
|
+
|
|
60
|
+
For additional statistical confidence, the strategy can run Monte Carlo simulations:
|
|
61
|
+
- Resamples historical funding/price data in blocks (default 24h blocks)
|
|
62
|
+
- Runs N simulations (configurable, e.g., 1000)
|
|
63
|
+
- Calculates VaR at specified confidence level (default 97.5%)
|
|
64
|
+
|
|
65
|
+
Configure via:
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"strategy_config": {
|
|
69
|
+
"bootstrap_sims": 1000,
|
|
70
|
+
"bootstrap_block_hours": 24
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 5. Ranking
|
|
76
|
+
|
|
77
|
+
Opportunities are ranked by expected APY:
|
|
78
|
+
```
|
|
79
|
+
expected_apy = mean_hourly_funding * 24 * 365 * safe_leverage
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Position Management
|
|
83
|
+
|
|
84
|
+
### Opening a Position
|
|
85
|
+
|
|
86
|
+
1. Transfers USDC from main wallet to strategy wallet
|
|
87
|
+
2. Bridges USDC to Hyperliquid via Arbitrum
|
|
88
|
+
3. Splits between perp margin and spot
|
|
89
|
+
4. Uses `PairedFiller` to atomically execute both legs (buy spot + sell perp)
|
|
90
|
+
5. Places protective orders:
|
|
91
|
+
- **Stop-loss**: Triggers if price approaches liquidation (default 65% of distance)
|
|
92
|
+
- **Limit sell**: Closes spot if funding flips negative
|
|
93
|
+
|
|
94
|
+
### Incremental Scaling
|
|
95
|
+
|
|
96
|
+
When you deposit additional funds with an existing position:
|
|
97
|
+
- Detects idle capital (undeployed USDC on Hyperliquid)
|
|
98
|
+
- Calculates additional units to add to each leg
|
|
99
|
+
- Uses `PairedFiller` to atomically add to both positions
|
|
100
|
+
- Maintains delta neutrality throughout
|
|
101
|
+
|
|
102
|
+
### Monitoring (update)
|
|
103
|
+
|
|
104
|
+
The `update` action:
|
|
105
|
+
1. Checks if position needs rebalancing (funding flipped, leverage drift, etc.)
|
|
106
|
+
2. Deploys any idle capital via scale-up
|
|
107
|
+
3. Verifies leg balance (spot amount ≈ perp amount)
|
|
108
|
+
4. Updates stop-loss/limit orders if liquidation price changed
|
|
109
|
+
|
|
110
|
+
### Closing a Position
|
|
111
|
+
|
|
112
|
+
1. Cancels all open orders
|
|
113
|
+
2. Uses `PairedFiller` to atomically close both legs (sell spot + buy perp)
|
|
114
|
+
3. Withdraws USDC from Hyperliquid to Arbitrum
|
|
115
|
+
4. Sends funds back to main wallet
|
|
116
|
+
|
|
117
|
+
## CLI Usage
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Analyze opportunities for a $1000 deposit (doesn't open position)
|
|
121
|
+
poetry run python wayfinder_paths/run_strategy.py basis_trading_strategy \
|
|
122
|
+
--action analyze --amount 1000 --config config.json
|
|
123
|
+
|
|
124
|
+
# Deposit $100 USDC from main wallet
|
|
125
|
+
poetry run python wayfinder_paths/run_strategy.py basis_trading_strategy \
|
|
126
|
+
--action deposit --main-token-amount 100 --config config.json
|
|
127
|
+
|
|
128
|
+
# Analyze and open/manage position
|
|
129
|
+
poetry run python wayfinder_paths/run_strategy.py basis_trading_strategy \
|
|
130
|
+
--action update --config config.json
|
|
131
|
+
|
|
132
|
+
# Check current status
|
|
133
|
+
poetry run python wayfinder_paths/run_strategy.py basis_trading_strategy \
|
|
134
|
+
--action status --config config.json
|
|
135
|
+
|
|
136
|
+
# Withdraw all funds back to main wallet
|
|
137
|
+
poetry run python wayfinder_paths/run_strategy.py basis_trading_strategy \
|
|
138
|
+
--action withdraw --config config.json
|
|
139
|
+
|
|
140
|
+
# Generate batch snapshot of all opportunities
|
|
141
|
+
poetry run python wayfinder_paths/run_strategy.py basis_trading_strategy \
|
|
142
|
+
--action snapshot --amount 1000 --config config.json
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Configuration
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"main_wallet": {
|
|
150
|
+
"address": "0x...",
|
|
151
|
+
"private_key": "0x..."
|
|
152
|
+
},
|
|
153
|
+
"strategy_wallet": {
|
|
154
|
+
"address": "0x...",
|
|
155
|
+
"private_key": "0x..."
|
|
156
|
+
},
|
|
157
|
+
"strategy_config": {
|
|
158
|
+
"max_leverage": 3,
|
|
159
|
+
"lookback_days": 180,
|
|
160
|
+
"bootstrap_sims": 0,
|
|
161
|
+
"bootstrap_block_hours": 24
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Parameters
|
|
167
|
+
|
|
168
|
+
| Parameter | Default | Description |
|
|
169
|
+
|-----------|---------|-------------|
|
|
170
|
+
| `max_leverage` | 3 | Maximum leverage allowed |
|
|
171
|
+
| `lookback_days` | 180 | Days of historical data for analysis |
|
|
172
|
+
| `confidence` | 0.975 | VaR confidence level (97.5%) |
|
|
173
|
+
| `fee_eps` | 0.003 | Fee buffer (0.3%) |
|
|
174
|
+
| `oi_floor` | 50 | Minimum open interest (USD) |
|
|
175
|
+
| `day_vlm_floor` | 100,000 | Minimum daily volume (USD) |
|
|
176
|
+
| `bootstrap_sims` | 50 | Monte Carlo simulations for VaR estimation |
|
|
177
|
+
| `bootstrap_block_hours` | 24 | Block size for bootstrap resampling |
|
|
178
|
+
|
|
179
|
+
### Thresholds
|
|
180
|
+
|
|
181
|
+
| Constant | Value | Description |
|
|
182
|
+
|----------|-------|-------------|
|
|
183
|
+
| `MIN_DEPOSIT_USDC` | 50 | Minimum deposit |
|
|
184
|
+
| `LIQUIDATION_REBALANCE_THRESHOLD` | 0.65 | Stop-loss at 65% of liquidation distance |
|
|
185
|
+
| `MIN_UNUSED_USD` | 5.0 | Minimum idle capital to trigger scale-up |
|
|
186
|
+
| `UNUSED_REL_EPS` | 0.05 | Relative threshold (5% of deposit) |
|
|
187
|
+
|
|
188
|
+
## Adapters Used
|
|
189
|
+
|
|
190
|
+
- **BALANCE**: Wallet balances and ERC20 transfers
|
|
191
|
+
- **LEDGER**: Transaction recording for deposit/withdraw tracking
|
|
192
|
+
- **TOKEN**: Token metadata (decimals, addresses)
|
|
193
|
+
- **HYPERLIQUID**: Market data, order execution, account state
|
|
194
|
+
|
|
195
|
+
## Risk Factors
|
|
196
|
+
|
|
197
|
+
1. **Funding Rate Flips**: Rates can turn negative, causing losses instead of gains
|
|
198
|
+
2. **Liquidation Risk**: High leverage + adverse price movement can liquidate the perp
|
|
199
|
+
3. **Execution Slippage**: Large orders may move the market
|
|
200
|
+
4. **Withdrawal Delays**: Hyperliquid withdrawals take ~15-30 minutes
|
|
201
|
+
5. **Smart Contract Risk**: Funds are held on Hyperliquid's L1
|
|
202
|
+
|
|
203
|
+
## Architecture
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
BasisTradingStrategy
|
|
207
|
+
├── HyperliquidAdapter # Market data, account state
|
|
208
|
+
├── LocalHyperliquidExecutor # Order execution (spot + perp)
|
|
209
|
+
├── PairedFiller # Atomic paired order execution
|
|
210
|
+
├── BalanceAdapter # Arbitrum wallet balances
|
|
211
|
+
├── LedgerAdapter # Deposit/withdraw tracking
|
|
212
|
+
└── LocalEvmTxn # Arbitrum transaction signing
|
|
213
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
USDC_ARBITRUM_TOKEN_ID = "usd-coin-arbitrum"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"smoke": {
|
|
3
|
+
"deposit": {"main_token_amount": 100, "gas_token_amount": 0.0001},
|
|
4
|
+
"update": {},
|
|
5
|
+
"status": {},
|
|
6
|
+
"withdraw": {}
|
|
7
|
+
},
|
|
8
|
+
"min_deposit_fail": {
|
|
9
|
+
"deposit": {"main_token_amount": 10, "gas_token_amount": 0.0},
|
|
10
|
+
"expect": {"success": false, "message_contains": "Minimum deposit"}
|
|
11
|
+
},
|
|
12
|
+
"analysis_only": {
|
|
13
|
+
"deposit": {"main_token_amount": 200, "gas_token_amount": 0.0001},
|
|
14
|
+
"update": {}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
schema_version: "0.1"
|
|
2
|
+
entrypoint: "wayfinder_paths.strategies.basis_trading_strategy.strategy.BasisTradingStrategy"
|
|
3
|
+
permissions:
|
|
4
|
+
policy: |
|
|
5
|
+
(wallet.id == 'FORMAT_WALLET_ID') AND (
|
|
6
|
+
# Allow Hyperliquid EIP-712 order actions
|
|
7
|
+
(action.type == 'hyperliquid_order') OR
|
|
8
|
+
(action.type == 'hyperliquid_cancel') OR
|
|
9
|
+
(action.type == 'hyperliquid_transfer') OR
|
|
10
|
+
# Allow USDC transfers to Hyperliquid bridge
|
|
11
|
+
(action.type == 'erc20_transfer' AND action.to == '0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7') OR
|
|
12
|
+
# Allow USDC withdraw to main wallet
|
|
13
|
+
(action.type == 'erc20_transfer' AND action.to == main_wallet.address)
|
|
14
|
+
)
|
|
15
|
+
adapters:
|
|
16
|
+
- name: "BALANCE"
|
|
17
|
+
capabilities: ["wallet_read", "wallet_transfer"]
|
|
18
|
+
- name: "LEDGER"
|
|
19
|
+
capabilities: ["ledger.read", "ledger.write", "strategy.transactions"]
|
|
20
|
+
- name: "TOKEN"
|
|
21
|
+
capabilities: ["token.read"]
|
|
22
|
+
- name: "HYPERLIQUID"
|
|
23
|
+
capabilities: ["market.read", "market.meta", "market.funding", "market.candles", "market.orderbook", "order.execute", "order.cancel", "position.manage", "transfer"]
|