wayfinder-paths 0.1.23__py3-none-any.whl → 0.1.25__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wayfinder-paths might be problematic. Click here for more details.
- wayfinder_paths/__init__.py +2 -0
- wayfinder_paths/adapters/balance_adapter/adapter.py +250 -0
- wayfinder_paths/adapters/balance_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -11
- wayfinder_paths/adapters/boros_adapter/__init__.py +17 -0
- wayfinder_paths/adapters/boros_adapter/adapter.py +1574 -0
- wayfinder_paths/adapters/boros_adapter/client.py +476 -0
- wayfinder_paths/adapters/boros_adapter/manifest.yaml +10 -0
- wayfinder_paths/adapters/boros_adapter/parsers.py +88 -0
- wayfinder_paths/adapters/boros_adapter/test_adapter.py +460 -0
- wayfinder_paths/adapters/boros_adapter/test_golden.py +156 -0
- wayfinder_paths/adapters/boros_adapter/types.py +70 -0
- wayfinder_paths/adapters/boros_adapter/utils.py +85 -0
- wayfinder_paths/adapters/brap_adapter/adapter.py +1 -1
- wayfinder_paths/adapters/brap_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +161 -26
- wayfinder_paths/adapters/hyperlend_adapter/manifest.yaml +9 -0
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +77 -13
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +2 -9
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +585 -61
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +47 -68
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +2 -3
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +17 -21
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +3 -6
- wayfinder_paths/adapters/hyperliquid_adapter/test_executor.py +4 -8
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +2 -2
- wayfinder_paths/adapters/ledger_adapter/manifest.yaml +7 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +1 -2
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +592 -400
- wayfinder_paths/adapters/moonwell_adapter/manifest.yaml +14 -0
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +126 -219
- wayfinder_paths/adapters/multicall_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/multicall_adapter/adapter.py +166 -0
- wayfinder_paths/adapters/multicall_adapter/manifest.yaml +5 -0
- wayfinder_paths/adapters/multicall_adapter/test_adapter.py +97 -0
- wayfinder_paths/adapters/pendle_adapter/README.md +102 -0
- wayfinder_paths/adapters/pendle_adapter/__init__.py +7 -0
- wayfinder_paths/adapters/pendle_adapter/adapter.py +1992 -0
- wayfinder_paths/adapters/pendle_adapter/examples.json +11 -0
- wayfinder_paths/adapters/pendle_adapter/manifest.yaml +21 -0
- wayfinder_paths/adapters/pendle_adapter/test_adapter.py +666 -0
- wayfinder_paths/adapters/pool_adapter/manifest.yaml +6 -0
- wayfinder_paths/adapters/token_adapter/examples.json +0 -4
- wayfinder_paths/adapters/token_adapter/manifest.yaml +7 -0
- wayfinder_paths/conftest.py +24 -17
- wayfinder_paths/core/__init__.py +2 -0
- wayfinder_paths/core/adapters/BaseAdapter.py +0 -25
- wayfinder_paths/core/adapters/models.py +17 -7
- wayfinder_paths/core/clients/BRAPClient.py +1 -1
- wayfinder_paths/core/clients/TokenClient.py +47 -1
- wayfinder_paths/core/clients/WayfinderClient.py +1 -2
- wayfinder_paths/core/clients/protocols.py +21 -22
- wayfinder_paths/core/clients/test_ledger_client.py +448 -0
- wayfinder_paths/core/config.py +12 -0
- wayfinder_paths/core/constants/__init__.py +15 -0
- wayfinder_paths/core/constants/base.py +6 -1
- wayfinder_paths/core/constants/contracts.py +39 -26
- wayfinder_paths/core/constants/erc20_abi.py +0 -1
- wayfinder_paths/core/constants/hyperlend_abi.py +0 -4
- wayfinder_paths/core/constants/hyperliquid.py +16 -0
- wayfinder_paths/core/constants/moonwell_abi.py +0 -15
- wayfinder_paths/core/engine/manifest.py +66 -0
- wayfinder_paths/core/strategies/Strategy.py +0 -61
- wayfinder_paths/core/strategies/__init__.py +10 -1
- wayfinder_paths/core/strategies/opa_loop.py +167 -0
- wayfinder_paths/core/utils/test_transaction.py +289 -0
- wayfinder_paths/core/utils/transaction.py +44 -1
- wayfinder_paths/core/utils/web3.py +3 -0
- wayfinder_paths/mcp/__init__.py +5 -0
- wayfinder_paths/mcp/preview.py +185 -0
- wayfinder_paths/mcp/scripting.py +84 -0
- wayfinder_paths/mcp/server.py +52 -0
- wayfinder_paths/mcp/state/profile_store.py +195 -0
- wayfinder_paths/mcp/state/store.py +89 -0
- wayfinder_paths/mcp/test_scripting.py +267 -0
- wayfinder_paths/mcp/tools/__init__.py +0 -0
- wayfinder_paths/mcp/tools/balances.py +290 -0
- wayfinder_paths/mcp/tools/discovery.py +158 -0
- wayfinder_paths/mcp/tools/execute.py +770 -0
- wayfinder_paths/mcp/tools/hyperliquid.py +931 -0
- wayfinder_paths/mcp/tools/quotes.py +288 -0
- wayfinder_paths/mcp/tools/run_script.py +286 -0
- wayfinder_paths/mcp/tools/strategies.py +188 -0
- wayfinder_paths/mcp/tools/tokens.py +46 -0
- wayfinder_paths/mcp/tools/wallets.py +354 -0
- wayfinder_paths/mcp/utils.py +129 -0
- wayfinder_paths/policies/hyperliquid.py +1 -1
- wayfinder_paths/policies/lifi.py +18 -0
- wayfinder_paths/policies/util.py +8 -2
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +28 -119
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +24 -53
- wayfinder_paths/strategies/boros_hype_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/boros_hype_strategy/boros_ops_mixin.py +450 -0
- wayfinder_paths/strategies/boros_hype_strategy/constants.py +255 -0
- wayfinder_paths/strategies/boros_hype_strategy/examples.json +37 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperevm_ops_mixin.py +114 -0
- wayfinder_paths/strategies/boros_hype_strategy/hyperliquid_ops_mixin.py +642 -0
- wayfinder_paths/strategies/boros_hype_strategy/manifest.yaml +36 -0
- wayfinder_paths/strategies/boros_hype_strategy/planner.py +460 -0
- wayfinder_paths/strategies/boros_hype_strategy/risk_ops_mixin.py +886 -0
- wayfinder_paths/strategies/boros_hype_strategy/snapshot_mixin.py +494 -0
- wayfinder_paths/strategies/boros_hype_strategy/strategy.py +1194 -0
- wayfinder_paths/strategies/boros_hype_strategy/test_planner_golden.py +374 -0
- wayfinder_paths/strategies/boros_hype_strategy/test_strategy.py +202 -0
- wayfinder_paths/strategies/boros_hype_strategy/types.py +365 -0
- wayfinder_paths/strategies/boros_hype_strategy/withdraw_mixin.py +997 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +3 -12
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +7 -29
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +63 -40
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +5 -15
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +0 -34
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +11 -34
- wayfinder_paths/tests/test_mcp_quote_swap.py +165 -0
- wayfinder_paths/tests/test_test_coverage.py +1 -4
- wayfinder_paths-0.1.25.dist-info/METADATA +377 -0
- wayfinder_paths-0.1.25.dist-info/RECORD +185 -0
- wayfinder_paths/scripts/create_strategy.py +0 -139
- wayfinder_paths/scripts/make_wallets.py +0 -142
- wayfinder_paths-0.1.23.dist-info/METADATA +0 -354
- wayfinder_paths-0.1.23.dist-info/RECORD +0 -120
- /wayfinder_paths/{scripts → mcp/state}/__init__.py +0 -0
- {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.25.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.23.dist-info → wayfinder_paths-0.1.25.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from decimal import ROUND_DOWN, Decimal, InvalidOperation, getcontext
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
getcontext().prec = 78
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def ok(result: Any) -> dict[str, Any]:
|
|
16
|
+
return {"ok": True, "result": result}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def err(code: str, message: str, details: Any | None = None) -> dict[str, Any]:
|
|
20
|
+
return {
|
|
21
|
+
"ok": False,
|
|
22
|
+
"error": {"code": str(code), "message": str(message), "details": details},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def repo_root() -> Path:
|
|
27
|
+
cur = Path(__file__).resolve()
|
|
28
|
+
for parent in [cur, *cur.parents]:
|
|
29
|
+
if (parent / "pyproject.toml").exists():
|
|
30
|
+
return parent
|
|
31
|
+
return Path.cwd()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def read_yaml(path: Path) -> dict[str, Any]:
|
|
35
|
+
data = yaml.safe_load(path.read_text())
|
|
36
|
+
return data if isinstance(data, dict) else {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def read_text_excerpt(path: Path, *, max_chars: int = 1200) -> str | None:
|
|
40
|
+
try:
|
|
41
|
+
text = path.read_text(encoding="utf-8", errors="replace").strip()
|
|
42
|
+
except OSError:
|
|
43
|
+
return None
|
|
44
|
+
if not text:
|
|
45
|
+
return None
|
|
46
|
+
if len(text) <= max_chars:
|
|
47
|
+
return text
|
|
48
|
+
return text[: max_chars - 3] + "..."
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def load_json_file(path: Path) -> Any:
|
|
52
|
+
try:
|
|
53
|
+
return json.loads(path.read_text())
|
|
54
|
+
except Exception:
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_config_json() -> dict[str, Any]:
|
|
59
|
+
cfg_path = os.getenv("WAYFINDER_CONFIG_PATH", "config.json")
|
|
60
|
+
path = (
|
|
61
|
+
repo_root() / cfg_path if not Path(cfg_path).is_absolute() else Path(cfg_path)
|
|
62
|
+
)
|
|
63
|
+
parsed = load_json_file(path)
|
|
64
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def wallets_path() -> Path:
|
|
68
|
+
cfg = load_config_json()
|
|
69
|
+
system = cfg.get("system") if isinstance(cfg.get("system"), dict) else {}
|
|
70
|
+
candidate = (
|
|
71
|
+
(system or {}).get("wallets_path")
|
|
72
|
+
or os.getenv("WALLETS_PATH")
|
|
73
|
+
or "wallets.json"
|
|
74
|
+
)
|
|
75
|
+
p = Path(candidate)
|
|
76
|
+
if not p.is_absolute():
|
|
77
|
+
p = repo_root() / p
|
|
78
|
+
return p
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load_wallets() -> list[dict[str, Any]]:
|
|
82
|
+
cfg = load_config_json()
|
|
83
|
+
if isinstance(cfg.get("wallets"), list):
|
|
84
|
+
return [w for w in cfg["wallets"] if isinstance(w, dict)]
|
|
85
|
+
|
|
86
|
+
p = wallets_path()
|
|
87
|
+
if p.exists():
|
|
88
|
+
parsed = load_json_file(p)
|
|
89
|
+
if isinstance(parsed, list):
|
|
90
|
+
return [w for w in parsed if isinstance(w, dict)]
|
|
91
|
+
if isinstance(parsed, dict) and isinstance(parsed.get("wallets"), list):
|
|
92
|
+
return [w for w in parsed["wallets"] if isinstance(w, dict)]
|
|
93
|
+
return []
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def find_wallet_by_label(label: str) -> dict[str, Any] | None:
|
|
97
|
+
want = str(label).strip()
|
|
98
|
+
if not want:
|
|
99
|
+
return None
|
|
100
|
+
for w in load_wallets():
|
|
101
|
+
if str(w.get("label", "")).strip() == want:
|
|
102
|
+
return w
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def normalize_address(addr: str | None) -> str | None:
|
|
107
|
+
if not addr:
|
|
108
|
+
return None
|
|
109
|
+
a = str(addr).strip()
|
|
110
|
+
return a if a else None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def parse_amount_to_raw(amount: str, decimals: int) -> int:
|
|
114
|
+
try:
|
|
115
|
+
d = Decimal(str(amount).strip())
|
|
116
|
+
except (InvalidOperation, ValueError) as exc:
|
|
117
|
+
raise ValueError(f"Invalid amount: {amount}") from exc
|
|
118
|
+
if d <= 0:
|
|
119
|
+
raise ValueError("Amount must be positive")
|
|
120
|
+
scale = Decimal(10) ** int(decimals)
|
|
121
|
+
raw = (d * scale).to_integral_value(rounding=ROUND_DOWN)
|
|
122
|
+
if raw <= 0:
|
|
123
|
+
raise ValueError("Amount is too small after decimal scaling")
|
|
124
|
+
return int(raw)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def sha256_json(obj: Any) -> str:
|
|
128
|
+
payload = json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
|
129
|
+
return "sha256:" + hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from wayfinder_paths.policies.util import allow_functions
|
|
2
|
+
|
|
3
|
+
LIFI_ROUTERS: dict[int:str] = {999: "0x0a0758d937d1059c356D4714e57F5df0239bce1A"}
|
|
4
|
+
LIFI_GENERIC = "0x31a9b1835864706Af10103b31Ea2b79bdb995F5F"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def lifi_swap(chain_id):
|
|
8
|
+
# NOTE: we get the abi from the base contract as it is a generic abi used everywhere
|
|
9
|
+
# and not all chains have this ABI published
|
|
10
|
+
return await allow_functions(
|
|
11
|
+
policy_name="Allow LIFI Swap",
|
|
12
|
+
abi_chain_id=8453,
|
|
13
|
+
address=LIFI_ROUTERS[chain_id],
|
|
14
|
+
function_names=[
|
|
15
|
+
"swapTokensMultipleV3ERC20ToERC20",
|
|
16
|
+
],
|
|
17
|
+
abi_address_override=LIFI_GENERIC,
|
|
18
|
+
)
|
wayfinder_paths/policies/util.py
CHANGED
|
@@ -2,7 +2,11 @@ from wayfinder_paths.core.utils.evm_helpers import get_abi_filtered
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
async def allow_functions(
|
|
5
|
-
policy_name: str,
|
|
5
|
+
policy_name: str,
|
|
6
|
+
abi_chain_id: int,
|
|
7
|
+
address: str,
|
|
8
|
+
function_names: list[str],
|
|
9
|
+
abi_address_override=None,
|
|
6
10
|
):
|
|
7
11
|
# Note: ChainID is just for fetching ABI, doesn't appear in the final policy. Doesn't bind a strict chain.
|
|
8
12
|
return {
|
|
@@ -19,7 +23,9 @@ async def allow_functions(
|
|
|
19
23
|
{
|
|
20
24
|
"field_source": "ethereum_calldata",
|
|
21
25
|
"field": "function_name",
|
|
22
|
-
"abi": await get_abi_filtered(
|
|
26
|
+
"abi": await get_abi_filtered(
|
|
27
|
+
abi_chain_id, abi_address_override or address, function_names
|
|
28
|
+
),
|
|
23
29
|
"operator": "in",
|
|
24
30
|
"value": function_names,
|
|
25
31
|
},
|
|
@@ -4,6 +4,7 @@ import asyncio
|
|
|
4
4
|
import math
|
|
5
5
|
import random
|
|
6
6
|
import time
|
|
7
|
+
import traceback
|
|
7
8
|
from collections.abc import Awaitable, Callable
|
|
8
9
|
from datetime import UTC, datetime, timedelta
|
|
9
10
|
from decimal import ROUND_DOWN, ROUND_UP, Decimal, getcontext
|
|
@@ -12,12 +13,8 @@ from statistics import fmean
|
|
|
12
13
|
from typing import Any
|
|
13
14
|
|
|
14
15
|
from wayfinder_paths.adapters.balance_adapter.adapter import BalanceAdapter
|
|
15
|
-
from wayfinder_paths.adapters.hyperliquid_adapter.adapter import
|
|
16
|
-
HYPERLIQUID_BRIDGE_ADDRESS,
|
|
17
|
-
HyperliquidAdapter,
|
|
18
|
-
)
|
|
16
|
+
from wayfinder_paths.adapters.hyperliquid_adapter.adapter import HyperliquidAdapter
|
|
19
17
|
from wayfinder_paths.adapters.hyperliquid_adapter.executor import (
|
|
20
|
-
HyperliquidExecutor,
|
|
21
18
|
LocalHyperliquidExecutor,
|
|
22
19
|
)
|
|
23
20
|
from wayfinder_paths.adapters.hyperliquid_adapter.paired_filler import (
|
|
@@ -54,6 +51,10 @@ from wayfinder_paths.core.analytics import (
|
|
|
54
51
|
from wayfinder_paths.core.analytics import (
|
|
55
52
|
z_from_conf as analytics_z_from_conf,
|
|
56
53
|
)
|
|
54
|
+
from wayfinder_paths.core.clients.protocols import (
|
|
55
|
+
HyperliquidExecutorProtocol as HyperliquidExecutor,
|
|
56
|
+
)
|
|
57
|
+
from wayfinder_paths.core.constants.contracts import HYPERLIQUID_BRIDGE
|
|
57
58
|
from wayfinder_paths.core.strategies.descriptors import (
|
|
58
59
|
Complexity,
|
|
59
60
|
Directionality,
|
|
@@ -63,6 +64,11 @@ from wayfinder_paths.core.strategies.descriptors import (
|
|
|
63
64
|
Volatility,
|
|
64
65
|
)
|
|
65
66
|
from wayfinder_paths.core.strategies.Strategy import StatusDict, StatusTuple, Strategy
|
|
67
|
+
from wayfinder_paths.policies.erc20 import any_erc20_function
|
|
68
|
+
from wayfinder_paths.policies.hyperliquid import (
|
|
69
|
+
any_hyperliquid_l1_payload,
|
|
70
|
+
any_hyperliquid_user_payload,
|
|
71
|
+
)
|
|
66
72
|
from wayfinder_paths.strategies.basis_trading_strategy.constants import (
|
|
67
73
|
USDC_ARBITRUM_TOKEN_ID,
|
|
68
74
|
)
|
|
@@ -201,7 +207,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
201
207
|
merged_config["strategy_wallet"] = strategy_wallet
|
|
202
208
|
self.config = merged_config
|
|
203
209
|
|
|
204
|
-
# Position tracking
|
|
205
210
|
self.current_position: BasisPosition | None = None
|
|
206
211
|
self.deposit_amount: float = 0.0
|
|
207
212
|
|
|
@@ -266,8 +271,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
266
271
|
adapters.append(self.ledger_adapter)
|
|
267
272
|
if self.hyperliquid_adapter is not None:
|
|
268
273
|
adapters.append(self.hyperliquid_adapter)
|
|
269
|
-
if adapters:
|
|
270
|
-
self.register_adapters(adapters)
|
|
271
274
|
|
|
272
275
|
async def setup(self) -> None:
|
|
273
276
|
self.logger.info("Starting BasisTradingStrategy setup")
|
|
@@ -332,7 +335,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
332
335
|
)
|
|
333
336
|
return
|
|
334
337
|
|
|
335
|
-
# Find matching spot position
|
|
336
338
|
spot_position = None
|
|
337
339
|
spot_balances = spot_state.get("balances", [])
|
|
338
340
|
for bal in spot_balances:
|
|
@@ -437,7 +439,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
437
439
|
return (False, f"Failed to transfer ETH for gas: {gas_res}")
|
|
438
440
|
self.logger.info(f"Gas transfer successful: {gas_res}")
|
|
439
441
|
|
|
440
|
-
# Real deposit: ensure funds are in the strategy wallet.
|
|
441
442
|
try:
|
|
442
443
|
main_address = self._get_main_wallet_address()
|
|
443
444
|
strategy_address = self._get_strategy_wallet_address()
|
|
@@ -481,7 +482,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
481
482
|
f"Strategy wallet already has {strategy_usdc:.2f} USDC, skipping transfer from main"
|
|
482
483
|
)
|
|
483
484
|
|
|
484
|
-
# Accumulate deposit amount for bridging in update()
|
|
485
485
|
self.deposit_amount += main_token_amount
|
|
486
486
|
|
|
487
487
|
return (
|
|
@@ -527,7 +527,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
527
527
|
if total_available < 1.0 and self.current_position is None:
|
|
528
528
|
return (False, "No funds to manage. Call deposit() first.")
|
|
529
529
|
|
|
530
|
-
# Bridge USDC from strategy wallet to Hyperliquid if needed
|
|
531
530
|
if strategy_usdc > 10.0:
|
|
532
531
|
try:
|
|
533
532
|
self.logger.info(
|
|
@@ -538,7 +537,7 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
538
537
|
token_id=USDC_ARBITRUM_TOKEN_ID,
|
|
539
538
|
amount=strategy_usdc,
|
|
540
539
|
from_wallet=strategy_wallet,
|
|
541
|
-
to_address=
|
|
540
|
+
to_address=HYPERLIQUID_BRIDGE,
|
|
542
541
|
)
|
|
543
542
|
|
|
544
543
|
if not success:
|
|
@@ -547,7 +546,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
547
546
|
|
|
548
547
|
self.logger.info(f"USDC sent to bridge, tx: {result}")
|
|
549
548
|
|
|
550
|
-
# Wait for Hyperliquid to credit the deposit
|
|
551
549
|
self.logger.info("Waiting for Hyperliquid to credit the deposit...")
|
|
552
550
|
|
|
553
551
|
(
|
|
@@ -578,7 +576,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
578
576
|
except Exception as e:
|
|
579
577
|
self.logger.warning(f"Failed to bridge USDC to Hyperliquid: {e}")
|
|
580
578
|
|
|
581
|
-
# If no position, find and open one
|
|
582
579
|
if self.current_position is None:
|
|
583
580
|
return await self._find_and_open_position()
|
|
584
581
|
|
|
@@ -702,8 +699,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
702
699
|
|
|
703
700
|
except Exception as e:
|
|
704
701
|
self.logger.error(f"Analysis failed: {e}")
|
|
705
|
-
import traceback
|
|
706
|
-
|
|
707
702
|
traceback.print_exc()
|
|
708
703
|
return {
|
|
709
704
|
"success": False,
|
|
@@ -774,7 +769,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
774
769
|
margin_summary = user_state.get("marginSummary", {})
|
|
775
770
|
hl_perp_value = float(margin_summary.get("accountValue", 0))
|
|
776
771
|
|
|
777
|
-
# Also check spot USDC balance
|
|
778
772
|
success, spot_state = await self.hyperliquid_adapter.get_spot_user_state(
|
|
779
773
|
address
|
|
780
774
|
)
|
|
@@ -798,13 +792,11 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
798
792
|
f"Call exit() to transfer to main wallet.",
|
|
799
793
|
)
|
|
800
794
|
|
|
801
|
-
# Close any open position
|
|
802
795
|
if self.current_position is not None:
|
|
803
796
|
close_success, close_msg = await self._close_position()
|
|
804
797
|
if not close_success:
|
|
805
798
|
return (False, f"Failed to close position: {close_msg}")
|
|
806
799
|
|
|
807
|
-
# Step 1: Transfer any spot USDC to perp for withdrawal
|
|
808
800
|
# Wait for spot sale to settle before checking balance
|
|
809
801
|
await asyncio.sleep(5)
|
|
810
802
|
|
|
@@ -850,7 +842,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
850
842
|
break
|
|
851
843
|
break
|
|
852
844
|
|
|
853
|
-
# Step 2: Get updated perp balance for withdrawal (with retry)
|
|
854
845
|
# Wait a moment for transfers to settle
|
|
855
846
|
await asyncio.sleep(2)
|
|
856
847
|
|
|
@@ -874,7 +865,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
874
865
|
if withdrawable <= 0:
|
|
875
866
|
return (False, "No withdrawable funds available")
|
|
876
867
|
|
|
877
|
-
# Step 3: Withdraw from Hyperliquid to Arbitrum (strategy wallet)
|
|
878
868
|
self.logger.info(
|
|
879
869
|
f"Withdrawing ${withdrawable:.2f} from Hyperliquid to Arbitrum"
|
|
880
870
|
)
|
|
@@ -888,7 +878,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
888
878
|
|
|
889
879
|
self.logger.info(f"Withdrawal initiated: {withdraw_result}")
|
|
890
880
|
|
|
891
|
-
# Step 4: Wait for withdrawal to appear on-chain
|
|
892
881
|
# Hyperliquid withdrawals typically take 5-15 minutes
|
|
893
882
|
self.logger.info("Waiting for withdrawal to appear on-chain...")
|
|
894
883
|
|
|
@@ -915,7 +904,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
915
904
|
f"Withdrawal confirmed: tx={tx_hash}, amount=${withdrawn_amount:.2f}"
|
|
916
905
|
)
|
|
917
906
|
|
|
918
|
-
# Step 5: Wait a bit for the USDC to be credited on Arbitrum
|
|
919
907
|
await asyncio.sleep(10)
|
|
920
908
|
|
|
921
909
|
final_balance = 0.0
|
|
@@ -949,7 +937,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
949
937
|
|
|
950
938
|
transferred_items = []
|
|
951
939
|
|
|
952
|
-
# Transfer USDC to main wallet
|
|
953
940
|
usdc_ok, usdc_raw = await self.balance_adapter.get_balance(
|
|
954
941
|
token_id=USDC_ARBITRUM_TOKEN_ID,
|
|
955
942
|
wallet_address=strategy_address,
|
|
@@ -1045,63 +1032,16 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1045
1032
|
gassed_up=True,
|
|
1046
1033
|
)
|
|
1047
1034
|
|
|
1048
|
-
@staticmethod
|
|
1049
|
-
async def policies() -> list[str]:
|
|
1050
|
-
# Placeholder - would include Hyperliquid-specific policies
|
|
1051
|
-
return []
|
|
1052
|
-
|
|
1053
1035
|
async def ensure_builder_fee_approved(self) -> StatusTuple:
|
|
1054
|
-
if not self.
|
|
1055
|
-
return
|
|
1056
|
-
|
|
1036
|
+
if not self.hyperliquid_adapter:
|
|
1037
|
+
return False, "Hyperliquid adapter not configured"
|
|
1057
1038
|
address = self._get_strategy_wallet_address()
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
try:
|
|
1065
|
-
success, current_fee = await self.hyperliquid_adapter.get_max_builder_fee(
|
|
1066
|
-
user=address,
|
|
1067
|
-
builder=builder,
|
|
1068
|
-
)
|
|
1069
|
-
|
|
1070
|
-
if not success:
|
|
1071
|
-
self.logger.warning(
|
|
1072
|
-
"Failed to check builder fee approval, continuing anyway"
|
|
1073
|
-
)
|
|
1074
|
-
return True, "Could not verify builder fee, proceeding"
|
|
1075
|
-
|
|
1076
|
-
self.logger.info(
|
|
1077
|
-
f"Builder fee approval check: current={current_fee}, required={required_fee}"
|
|
1078
|
-
)
|
|
1079
|
-
|
|
1080
|
-
if current_fee >= required_fee:
|
|
1081
|
-
return True, f"Builder fee already approved: {current_fee}"
|
|
1082
|
-
|
|
1083
|
-
# Need to approve
|
|
1084
|
-
max_fee_rate = f"{required_fee / 1000:.3f}%"
|
|
1085
|
-
self.logger.info(
|
|
1086
|
-
f"Approving builder fee: builder={builder}, rate={max_fee_rate}"
|
|
1087
|
-
)
|
|
1088
|
-
|
|
1089
|
-
success, result = await self.hyperliquid_adapter.approve_builder_fee(
|
|
1090
|
-
builder=builder,
|
|
1091
|
-
max_fee_rate=max_fee_rate,
|
|
1092
|
-
address=address,
|
|
1093
|
-
)
|
|
1094
|
-
|
|
1095
|
-
if not success:
|
|
1096
|
-
self.logger.error(f"Builder fee approval failed: {result}")
|
|
1097
|
-
return False, f"Builder fee approval failed: {result}"
|
|
1098
|
-
|
|
1099
|
-
self.logger.info(f"Builder fee approved: {result}")
|
|
1100
|
-
return True, f"Builder fee approved at {max_fee_rate}"
|
|
1101
|
-
|
|
1102
|
-
except Exception as e:
|
|
1103
|
-
self.logger.error(f"Builder fee approval error: {e}")
|
|
1104
|
-
return False, f"Builder fee approval error: {e}"
|
|
1039
|
+
if not address:
|
|
1040
|
+
return False, "No strategy wallet address"
|
|
1041
|
+
return await self.hyperliquid_adapter.ensure_builder_fee_approved(
|
|
1042
|
+
address=address,
|
|
1043
|
+
builder_fee=self.builder_fee,
|
|
1044
|
+
)
|
|
1105
1045
|
|
|
1106
1046
|
# ------------------------------------------------------------------ #
|
|
1107
1047
|
# Position Management #
|
|
@@ -1218,7 +1158,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1218
1158
|
coin = best.get("coin", "unknown")
|
|
1219
1159
|
safe = best.get("safe", {})
|
|
1220
1160
|
|
|
1221
|
-
# Use 7-day horizon sizing by default
|
|
1222
1161
|
safe_7 = safe.get("7") or {}
|
|
1223
1162
|
if safe_7.get("spot_usdc", 0) <= 0 or safe_7.get("perp_amount", 0) <= 0:
|
|
1224
1163
|
return (True, f"Best opportunity ({coin}) returned zero sizing.")
|
|
@@ -1237,19 +1176,16 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1237
1176
|
f"expected net APY: {expected_net_apy_pct:.2f}%, target qty: {target_qty}"
|
|
1238
1177
|
)
|
|
1239
1178
|
|
|
1240
|
-
# Execute position using PairedFiller
|
|
1241
1179
|
address = self._get_strategy_wallet_address()
|
|
1242
1180
|
order_usd = float(safe_7.get("spot_usdc", 0.0) or 0.0)
|
|
1243
1181
|
order_usd = float(
|
|
1244
1182
|
Decimal(str(order_usd)).quantize(Decimal("0.01"), rounding=ROUND_UP)
|
|
1245
1183
|
)
|
|
1246
1184
|
|
|
1247
|
-
# Step 1: Ensure builder fee is approved
|
|
1248
1185
|
fee_success, fee_msg = await self.ensure_builder_fee_approved()
|
|
1249
1186
|
if not fee_success:
|
|
1250
1187
|
return (False, f"Builder fee approval failed: {fee_msg}")
|
|
1251
1188
|
|
|
1252
|
-
# Step 2: Update leverage for the perp asset
|
|
1253
1189
|
self.logger.info(f"Setting leverage to {leverage}x for {coin}")
|
|
1254
1190
|
success, lev_result = await self.hyperliquid_adapter.update_leverage(
|
|
1255
1191
|
asset_id=perp_asset_id,
|
|
@@ -1261,7 +1197,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1261
1197
|
self.logger.warning(f"Failed to set leverage: {lev_result}")
|
|
1262
1198
|
# Continue anyway - leverage might already be set
|
|
1263
1199
|
|
|
1264
|
-
# Step 3: Ensure USDC is split correctly between spot and perp.
|
|
1265
1200
|
# Target: spot has order_usd for the spot buy, perp holds the remainder as margin.
|
|
1266
1201
|
split_ok, split_msg = await self._rebalance_usdc_between_perp_and_spot(
|
|
1267
1202
|
target_spot_usdc=order_usd,
|
|
@@ -1270,7 +1205,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1270
1205
|
if not split_ok:
|
|
1271
1206
|
self.logger.warning(f"USDC rebalance failed: {split_msg}")
|
|
1272
1207
|
|
|
1273
|
-
# Step 4: Execute paired fill
|
|
1274
1208
|
filler = PairedFiller(
|
|
1275
1209
|
adapter=self.hyperliquid_adapter,
|
|
1276
1210
|
address=address,
|
|
@@ -1304,7 +1238,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1304
1238
|
success, mids = await self.hyperliquid_adapter.get_all_mid_prices()
|
|
1305
1239
|
entry_price = self._resolve_mid_price(coin, mids) if success else 0.0
|
|
1306
1240
|
|
|
1307
|
-
# Step 5: Get liquidation price and place stop-loss
|
|
1308
1241
|
success, user_state = await self.hyperliquid_adapter.get_user_state(address)
|
|
1309
1242
|
liquidation_price = None
|
|
1310
1243
|
if success:
|
|
@@ -1473,7 +1406,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1473
1406
|
|
|
1474
1407
|
units_to_add = order_usd / price
|
|
1475
1408
|
|
|
1476
|
-
# Round to valid decimals for the assets
|
|
1477
1409
|
spot_valid = self.hyperliquid_adapter.get_valid_order_size(
|
|
1478
1410
|
pos.spot_asset_id, units_to_add
|
|
1479
1411
|
)
|
|
@@ -1493,7 +1425,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1493
1425
|
f"(${order_usd:.2f}) at {leverage}x leverage"
|
|
1494
1426
|
)
|
|
1495
1427
|
|
|
1496
|
-
# Ensure idle USDC is split correctly between spot and perp for this scale-up.
|
|
1497
1428
|
# Target: spot has order_usd for the spot buy, perp holds the remainder as margin.
|
|
1498
1429
|
split_ok, split_msg = await self._rebalance_usdc_between_perp_and_spot(
|
|
1499
1430
|
target_spot_usdc=order_usd,
|
|
@@ -1502,7 +1433,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1502
1433
|
if not split_ok:
|
|
1503
1434
|
self.logger.warning(f"USDC rebalance failed: {split_msg}")
|
|
1504
1435
|
|
|
1505
|
-
# Execute paired fill to add to both legs
|
|
1506
1436
|
filler = PairedFiller(
|
|
1507
1437
|
adapter=self.hyperliquid_adapter,
|
|
1508
1438
|
address=address,
|
|
@@ -1585,8 +1515,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1585
1515
|
)
|
|
1586
1516
|
return await self._find_and_open_position()
|
|
1587
1517
|
|
|
1588
|
-
# ------------------------------------------------------------------ #
|
|
1589
|
-
# ------------------------------------------------------------------ #
|
|
1590
1518
|
needs_rebalance, reason = await self._needs_new_position(state, hl_value)
|
|
1591
1519
|
|
|
1592
1520
|
if needs_rebalance:
|
|
@@ -1599,31 +1527,22 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1599
1527
|
f"Position needs attention but in cooldown: {cooldown_reason}",
|
|
1600
1528
|
)
|
|
1601
1529
|
|
|
1602
|
-
# Perform rebalance: close and reopen
|
|
1603
1530
|
self.logger.info(f"Rebalancing position: {reason}")
|
|
1604
|
-
|
|
1605
|
-
# Close existing position
|
|
1606
1531
|
close_success, close_msg = await self._close_position()
|
|
1607
1532
|
if not close_success:
|
|
1608
1533
|
return (False, f"Rebalance failed - could not close: {close_msg}")
|
|
1609
1534
|
|
|
1610
|
-
# Open new position
|
|
1611
1535
|
return await self._find_and_open_position()
|
|
1612
1536
|
|
|
1613
|
-
# ------------------------------------------------------------------ #
|
|
1614
|
-
# ------------------------------------------------------------------ #
|
|
1615
1537
|
leg_ok, leg_msg = await self._verify_leg_balance(state)
|
|
1616
1538
|
if not leg_ok:
|
|
1617
1539
|
self.logger.warning(f"Leg imbalance detected: {leg_msg}")
|
|
1618
|
-
# Try to repair the imbalance
|
|
1619
1540
|
repair_ok, repair_msg = await self._repair_leg_imbalance(state)
|
|
1620
1541
|
if repair_ok:
|
|
1621
1542
|
actions_taken.append(f"Repaired leg imbalance: {repair_msg}")
|
|
1622
1543
|
else:
|
|
1623
1544
|
actions_taken.append(f"Leg imbalance repair failed: {repair_msg}")
|
|
1624
1545
|
|
|
1625
|
-
# ------------------------------------------------------------------ #
|
|
1626
|
-
# ------------------------------------------------------------------ #
|
|
1627
1546
|
perp_margin, spot_usdc = await self._get_undeployed_capital()
|
|
1628
1547
|
total_idle = perp_margin + spot_usdc
|
|
1629
1548
|
min_deploy = max(self.MIN_UNUSED_USD, self.UNUSED_REL_EPS * self.deposit_amount)
|
|
@@ -1642,8 +1561,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1642
1561
|
else:
|
|
1643
1562
|
actions_taken.append(f"Scale-up failed: {scale_msg}")
|
|
1644
1563
|
|
|
1645
|
-
# ------------------------------------------------------------------ #
|
|
1646
|
-
# ------------------------------------------------------------------ #
|
|
1647
1564
|
sl_ok, sl_msg = await self._ensure_stop_loss_valid(state)
|
|
1648
1565
|
if not sl_ok:
|
|
1649
1566
|
actions_taken.append(f"Stop-loss issue: {sl_msg}")
|
|
@@ -1915,7 +1832,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1915
1832
|
order_coin = order.get("coin", "")
|
|
1916
1833
|
order_id = order.get("oid")
|
|
1917
1834
|
|
|
1918
|
-
# Cancel perp orders for this coin
|
|
1919
1835
|
if order_coin == pos.coin and order_id:
|
|
1920
1836
|
self.logger.info(f"Canceling perp order {order_id} for {pos.coin}")
|
|
1921
1837
|
await self.hyperliquid_adapter.cancel_order(
|
|
@@ -1924,7 +1840,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1924
1840
|
address=address,
|
|
1925
1841
|
)
|
|
1926
1842
|
|
|
1927
|
-
# Cancel spot orders for this coin
|
|
1928
1843
|
if spot_coin and order_coin == spot_coin and order_id:
|
|
1929
1844
|
self.logger.info(f"Canceling spot order {order_id} for {spot_coin}")
|
|
1930
1845
|
await self.hyperliquid_adapter.cancel_order(
|
|
@@ -1940,10 +1855,8 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1940
1855
|
pos = self.current_position
|
|
1941
1856
|
self.logger.info(f"Closing position on {pos.coin}")
|
|
1942
1857
|
|
|
1943
|
-
# Cancel all stop-loss and limit orders first
|
|
1944
1858
|
await self._cancel_all_position_orders()
|
|
1945
1859
|
|
|
1946
|
-
# Real execution via PairedFiller - reverse direction to close
|
|
1947
1860
|
try:
|
|
1948
1861
|
address = self._get_strategy_wallet_address()
|
|
1949
1862
|
filler = PairedFiller(
|
|
@@ -2011,7 +1924,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
2011
1924
|
if funding_earned > deposited_amount * self.FUNDING_REBALANCE_THRESHOLD:
|
|
2012
1925
|
return True, f"Funding earned {funding_earned:.2f} exceeds threshold"
|
|
2013
1926
|
|
|
2014
|
-
# Check 4: Perp must be SHORT
|
|
2015
1927
|
perp_size = float(perp_position.get("szi", 0))
|
|
2016
1928
|
if perp_size >= 0:
|
|
2017
1929
|
return True, "Perp position is not short"
|
|
@@ -2110,7 +2022,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
2110
2022
|
address
|
|
2111
2023
|
)
|
|
2112
2024
|
|
|
2113
|
-
# Track existing valid orders and orders to cancel
|
|
2114
2025
|
has_valid_perp_stop = False
|
|
2115
2026
|
has_valid_spot_limit = False
|
|
2116
2027
|
orders_to_cancel = []
|
|
@@ -2183,7 +2094,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
2183
2094
|
(spot_asset_id, order_id, "spot limit")
|
|
2184
2095
|
)
|
|
2185
2096
|
|
|
2186
|
-
# Cancel invalid/duplicate orders
|
|
2187
2097
|
for asset_id, order_id, order_desc in orders_to_cancel:
|
|
2188
2098
|
self.logger.info(f"Canceling {order_desc} order {order_id}")
|
|
2189
2099
|
await self.hyperliquid_adapter.cancel_order(
|
|
@@ -2192,7 +2102,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
2192
2102
|
address=address,
|
|
2193
2103
|
)
|
|
2194
2104
|
|
|
2195
|
-
# Place perp stop-loss if not valid one exists
|
|
2196
2105
|
if not has_valid_perp_stop:
|
|
2197
2106
|
success, result = await self.hyperliquid_adapter.place_stop_loss(
|
|
2198
2107
|
asset_id=perp_asset_id,
|
|
@@ -2205,7 +2114,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
2205
2114
|
return False, f"Failed to place perp stop-loss: {result}"
|
|
2206
2115
|
self.logger.info(f"Placed perp stop-loss at {stop_loss_price} for {coin}")
|
|
2207
2116
|
|
|
2208
|
-
# Place spot limit sell if needed
|
|
2209
2117
|
if (
|
|
2210
2118
|
spot_asset_id
|
|
2211
2119
|
and spot_position_size
|
|
@@ -2331,7 +2239,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
2331
2239
|
# USDC is 1:1
|
|
2332
2240
|
hl_value += total
|
|
2333
2241
|
else:
|
|
2334
|
-
# Look up mid price for non-USDC assets
|
|
2335
2242
|
mid_price = self._resolve_mid_price(coin, mid_prices)
|
|
2336
2243
|
if mid_price > 0:
|
|
2337
2244
|
hl_value += total * mid_price
|
|
@@ -2424,7 +2331,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
2424
2331
|
oi_usd = oi_base * mark_px
|
|
2425
2332
|
day_ntl_usd = float(ctx.get("dayNtlVlm") or 0.0)
|
|
2426
2333
|
|
|
2427
|
-
# Apply liquidity filters
|
|
2428
2334
|
if oi_usd < oi_floor or day_ntl_usd < day_vlm_floor:
|
|
2429
2335
|
continue
|
|
2430
2336
|
|
|
@@ -2538,14 +2444,12 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
2538
2444
|
)
|
|
2539
2445
|
continue
|
|
2540
2446
|
|
|
2541
|
-
# Dedupe and merge
|
|
2542
2447
|
for record in data:
|
|
2543
2448
|
ts = record.get("time", 0)
|
|
2544
2449
|
if ts not in seen_times:
|
|
2545
2450
|
seen_times.add(ts)
|
|
2546
2451
|
all_funding.append(record)
|
|
2547
2452
|
|
|
2548
|
-
# Sort by time
|
|
2549
2453
|
all_funding.sort(key=lambda x: x.get("time", 0))
|
|
2550
2454
|
|
|
2551
2455
|
if not all_funding:
|
|
@@ -2585,14 +2489,12 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
2585
2489
|
)
|
|
2586
2490
|
continue
|
|
2587
2491
|
|
|
2588
|
-
# Dedupe and merge
|
|
2589
2492
|
for candle in data:
|
|
2590
2493
|
ts = candle.get("t", 0)
|
|
2591
2494
|
if ts not in seen_times:
|
|
2592
2495
|
seen_times.add(ts)
|
|
2593
2496
|
all_candles.append(candle)
|
|
2594
2497
|
|
|
2595
|
-
# Sort by time
|
|
2596
2498
|
all_candles.sort(key=lambda x: x.get("t", 0))
|
|
2597
2499
|
|
|
2598
2500
|
if not all_candles:
|
|
@@ -3410,7 +3312,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
3410
3312
|
if len(pts) > 256:
|
|
3411
3313
|
break
|
|
3412
3314
|
pts.append(float(upper))
|
|
3413
|
-
# Dedupe + sort
|
|
3414
3315
|
return sorted({float(p) for p in pts if p > 0.0})
|
|
3415
3316
|
|
|
3416
3317
|
async def max_spot_order_usd_for_book(
|
|
@@ -3833,3 +3734,11 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
3833
3734
|
if not address:
|
|
3834
3735
|
raise ValueError("main_wallet address not found")
|
|
3835
3736
|
return str(address)
|
|
3737
|
+
|
|
3738
|
+
@staticmethod
|
|
3739
|
+
async def policies() -> list[str]:
|
|
3740
|
+
return [
|
|
3741
|
+
any_hyperliquid_l1_payload(),
|
|
3742
|
+
any_hyperliquid_user_payload(),
|
|
3743
|
+
any_erc20_function(HYPERLIQUID_BRIDGE),
|
|
3744
|
+
]
|