wayfinder-paths 0.1.7__py3-none-any.whl → 0.1.9__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.
- wayfinder_paths/CONFIG_GUIDE.md +5 -14
- wayfinder_paths/adapters/brap_adapter/README.md +1 -1
- wayfinder_paths/adapters/brap_adapter/adapter.py +1 -53
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +5 -7
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +0 -7
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +0 -54
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +1 -1
- wayfinder_paths/adapters/ledger_adapter/README.md +1 -1
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +3 -0
- wayfinder_paths/adapters/pool_adapter/README.md +3 -104
- wayfinder_paths/adapters/pool_adapter/adapter.py +0 -194
- wayfinder_paths/adapters/pool_adapter/examples.json +0 -100
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +0 -134
- wayfinder_paths/adapters/token_adapter/README.md +1 -1
- wayfinder_paths/core/clients/AuthClient.py +0 -3
- wayfinder_paths/core/clients/BRAPClient.py +1 -0
- wayfinder_paths/core/clients/ClientManager.py +1 -22
- wayfinder_paths/core/clients/PoolClient.py +0 -16
- wayfinder_paths/core/clients/WalletClient.py +0 -8
- wayfinder_paths/core/clients/WayfinderClient.py +9 -14
- wayfinder_paths/core/clients/__init__.py +0 -8
- wayfinder_paths/core/clients/protocols.py +0 -64
- wayfinder_paths/core/config.py +5 -45
- wayfinder_paths/core/engine/StrategyJob.py +0 -3
- wayfinder_paths/core/services/base.py +0 -49
- wayfinder_paths/core/services/local_evm_txn.py +3 -82
- wayfinder_paths/core/services/local_token_txn.py +61 -70
- wayfinder_paths/core/services/web3_service.py +0 -2
- wayfinder_paths/core/settings.py +8 -8
- wayfinder_paths/core/strategies/Strategy.py +1 -5
- wayfinder_paths/core/utils/evm_helpers.py +7 -12
- wayfinder_paths/core/wallets/README.md +3 -6
- wayfinder_paths/run_strategy.py +29 -32
- wayfinder_paths/scripts/make_wallets.py +1 -25
- wayfinder_paths/scripts/run_strategy.py +0 -2
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1 -3
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +86 -137
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +96 -58
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/README.md +2 -2
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +4 -1
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +106 -28
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +53 -14
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +1 -6
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +40 -17
- wayfinder_paths/templates/strategy/test_strategy.py +0 -4
- {wayfinder_paths-0.1.7.dist-info → wayfinder_paths-0.1.9.dist-info}/METADATA +33 -15
- {wayfinder_paths-0.1.7.dist-info → wayfinder_paths-0.1.9.dist-info}/RECORD +49 -51
- wayfinder_paths/core/clients/SimulationClient.py +0 -192
- wayfinder_paths/core/clients/TransactionClient.py +0 -63
- {wayfinder_paths-0.1.7.dist-info → wayfinder_paths-0.1.9.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.7.dist-info → wayfinder_paths-0.1.9.dist-info}/WHEEL +0 -0
|
@@ -59,7 +59,7 @@ def resolve_rpc_url(
|
|
|
59
59
|
explicit_rpc_url: str | None = None,
|
|
60
60
|
) -> str:
|
|
61
61
|
"""
|
|
62
|
-
Resolve RPC URL from config
|
|
62
|
+
Resolve RPC URL from config.
|
|
63
63
|
|
|
64
64
|
Args:
|
|
65
65
|
chain_id: Chain ID to look up RPC URL for
|
|
@@ -83,10 +83,7 @@ def resolve_rpc_url(
|
|
|
83
83
|
by_str = mapping.get(str(chain_id))
|
|
84
84
|
if by_str:
|
|
85
85
|
return str(by_str)
|
|
86
|
-
|
|
87
|
-
if env_rpc:
|
|
88
|
-
return env_rpc
|
|
89
|
-
raise ValueError("RPC URL not provided. Set strategy.rpc_urls or env RPC_URL.")
|
|
86
|
+
raise ValueError("RPC URL not provided. Set strategy.rpc_urls in config.json.")
|
|
90
87
|
|
|
91
88
|
|
|
92
89
|
async def get_next_nonce(
|
|
@@ -119,7 +116,7 @@ def resolve_private_key_for_from_address(
|
|
|
119
116
|
from_address: str, config: dict[str, Any]
|
|
120
117
|
) -> str | None:
|
|
121
118
|
"""
|
|
122
|
-
Resolve private key for the given address from config
|
|
119
|
+
Resolve private key for the given address from config.
|
|
123
120
|
|
|
124
121
|
Args:
|
|
125
122
|
from_address: Address to resolve private key for
|
|
@@ -159,14 +156,12 @@ def resolve_private_key_for_from_address(
|
|
|
159
156
|
logger.debug("Error resolving addresses from wallet config: %s", e)
|
|
160
157
|
|
|
161
158
|
if main_addr and from_addr_norm == (main_addr or "").lower():
|
|
162
|
-
return main_pk
|
|
159
|
+
return main_pk
|
|
163
160
|
if strategy_addr and from_addr_norm == (strategy_addr or "").lower():
|
|
164
|
-
return
|
|
165
|
-
strategy_pk or os.getenv("PRIVATE_KEY_STRATEGY") or os.getenv("PRIVATE_KEY")
|
|
166
|
-
)
|
|
161
|
+
return strategy_pk
|
|
167
162
|
|
|
168
|
-
#
|
|
169
|
-
return
|
|
163
|
+
# No fallback - private keys must be in config or wallets.json
|
|
164
|
+
return None
|
|
170
165
|
|
|
171
166
|
|
|
172
167
|
async def _get_abi(chain_id: int, address: str) -> str | None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Wallet Abstraction Layer
|
|
2
2
|
|
|
3
|
-
Wayfinder strategies interact with blockchains through a single abstraction: the `EvmTxn` interface defined in `wayfinder_paths/core/services/base.py`. The default implementation (`LocalEvmTxn`) signs transactions with private keys pulled from config
|
|
3
|
+
Wayfinder strategies interact with blockchains through a single abstraction: the `EvmTxn` interface defined in `wayfinder_paths/core/services/base.py`. The default implementation (`LocalEvmTxn`) signs transactions with private keys pulled from config.json or wallets.json, while `WalletManager` resolves which provider to use at runtime.
|
|
4
4
|
|
|
5
5
|
## Pieces
|
|
6
6
|
|
|
@@ -46,9 +46,6 @@ class PrivyWallet(EvmTxn):
|
|
|
46
46
|
async def read_erc20_allowance(self, chain_id: int, token_address: str, owner_address: str, spender_address: str) -> tuple[bool, Any]:
|
|
47
47
|
...
|
|
48
48
|
|
|
49
|
-
async def approve_token(...):
|
|
50
|
-
...
|
|
51
|
-
|
|
52
49
|
async def broadcast_transaction(...):
|
|
53
50
|
...
|
|
54
51
|
|
|
@@ -76,8 +73,8 @@ web3_service = DefaultWeb3Service(config, wallet_provider=custom_wallet)
|
|
|
76
73
|
{
|
|
77
74
|
"strategy": {
|
|
78
75
|
"wallet_type": "local",
|
|
79
|
-
"main_wallet": {"address": "0x...", "wallet_type": "local"},
|
|
80
|
-
"strategy_wallet": {"address": "0x..."}
|
|
76
|
+
"main_wallet": { "address": "0x...", "wallet_type": "local" },
|
|
77
|
+
"strategy_wallet": { "address": "0x..." }
|
|
81
78
|
}
|
|
82
79
|
}
|
|
83
80
|
```
|
wayfinder_paths/run_strategy.py
CHANGED
|
@@ -12,7 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
|
|
13
13
|
from loguru import logger
|
|
14
14
|
|
|
15
|
-
from wayfinder_paths.core.config import StrategyJobConfig
|
|
15
|
+
from wayfinder_paths.core.config import StrategyJobConfig
|
|
16
16
|
from wayfinder_paths.core.engine.manifest import load_manifest, validate_manifest
|
|
17
17
|
from wayfinder_paths.core.engine.StrategyJob import StrategyJob
|
|
18
18
|
|
|
@@ -21,7 +21,6 @@ def load_strategy(
|
|
|
21
21
|
strategy_name: str,
|
|
22
22
|
*,
|
|
23
23
|
strategy_config: dict | None = None,
|
|
24
|
-
simulation: bool = False,
|
|
25
24
|
api_key: str | None = None,
|
|
26
25
|
):
|
|
27
26
|
"""
|
|
@@ -30,7 +29,6 @@ def load_strategy(
|
|
|
30
29
|
Args:
|
|
31
30
|
strategy_name: Name of the strategy to load (directory name in strategies/)
|
|
32
31
|
strategy_config: Configuration dict for the strategy
|
|
33
|
-
simulation: Enable simulation mode for testing
|
|
34
32
|
api_key: Optional API key for service account authentication
|
|
35
33
|
|
|
36
34
|
Returns:
|
|
@@ -59,36 +57,45 @@ def load_strategy(
|
|
|
59
57
|
module = __import__(module_path, fromlist=[class_name])
|
|
60
58
|
strategy_class = getattr(module, class_name)
|
|
61
59
|
|
|
62
|
-
return strategy_class(
|
|
63
|
-
config=strategy_config, simulation=simulation, api_key=api_key
|
|
64
|
-
)
|
|
60
|
+
return strategy_class(config=strategy_config, api_key=api_key)
|
|
65
61
|
|
|
66
62
|
|
|
67
63
|
def load_config(
|
|
68
64
|
config_path: str | None = None, strategy_name: str | None = None
|
|
69
65
|
) -> StrategyJobConfig:
|
|
70
66
|
"""
|
|
71
|
-
Load configuration from file
|
|
67
|
+
Load configuration from config.json file
|
|
72
68
|
|
|
73
69
|
Args:
|
|
74
|
-
config_path:
|
|
70
|
+
config_path: Path to config file (defaults to "config.json")
|
|
75
71
|
strategy_name: Optional strategy name for per-strategy wallet lookup
|
|
76
72
|
|
|
77
73
|
Returns:
|
|
78
74
|
StrategyJobConfig instance
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
FileNotFoundError: If config file does not exist
|
|
79
78
|
"""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
79
|
+
# Default to config.json if not provided
|
|
80
|
+
if not config_path:
|
|
81
|
+
config_path = "config.json"
|
|
82
|
+
|
|
83
|
+
config_file = Path(config_path)
|
|
84
|
+
if not config_file.exists():
|
|
85
|
+
raise FileNotFoundError(
|
|
86
|
+
f"Config file not found: {config_path}. "
|
|
87
|
+
"Please create config.json (see wayfinder_paths/config.example.json for template)"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
logger.info(f"Loading config from {config_path}")
|
|
91
|
+
with open(config_file) as f:
|
|
92
|
+
config_data = json.load(f)
|
|
93
|
+
|
|
94
|
+
config = StrategyJobConfig.from_dict(config_data, strategy_name=strategy_name)
|
|
95
|
+
if strategy_name:
|
|
96
|
+
config.strategy_config["_strategy_name"] = strategy_name
|
|
97
|
+
config.__post_init__()
|
|
98
|
+
return config
|
|
92
99
|
|
|
93
100
|
|
|
94
101
|
async def run_strategy(
|
|
@@ -96,7 +103,6 @@ async def run_strategy(
|
|
|
96
103
|
config_path: str | None = None,
|
|
97
104
|
action: str = "run",
|
|
98
105
|
manifest_path: str | None = None,
|
|
99
|
-
simulation: bool = False,
|
|
100
106
|
**kwargs,
|
|
101
107
|
):
|
|
102
108
|
"""
|
|
@@ -159,9 +165,7 @@ async def run_strategy(
|
|
|
159
165
|
module_path, class_name = manifest.entrypoint.rsplit(".", 1)
|
|
160
166
|
module = __import__(module_path, fromlist=[class_name])
|
|
161
167
|
strategy_class = getattr(module, class_name)
|
|
162
|
-
strategy = strategy_class(
|
|
163
|
-
config=config.strategy_config, simulation=simulation
|
|
164
|
-
)
|
|
168
|
+
strategy = strategy_class(config=config.strategy_config)
|
|
165
169
|
logger.info(
|
|
166
170
|
f"Loaded strategy from manifest: {strategy_name_for_wallet or 'unnamed'}"
|
|
167
171
|
)
|
|
@@ -169,7 +173,6 @@ async def run_strategy(
|
|
|
169
173
|
strategy = load_strategy(
|
|
170
174
|
strategy_name,
|
|
171
175
|
strategy_config=config.strategy_config,
|
|
172
|
-
simulation=simulation,
|
|
173
176
|
)
|
|
174
177
|
logger.info(f"Loaded strategy: {strategy.name}")
|
|
175
178
|
|
|
@@ -326,7 +329,7 @@ def main():
|
|
|
326
329
|
help="Path to strategy manifest YAML (alternative to strategy name)",
|
|
327
330
|
)
|
|
328
331
|
parser.add_argument(
|
|
329
|
-
"--config", help="Path to config file (defaults to
|
|
332
|
+
"--config", help="Path to config file (defaults to config.json)"
|
|
330
333
|
)
|
|
331
334
|
parser.add_argument(
|
|
332
335
|
"--action",
|
|
@@ -372,11 +375,6 @@ def main():
|
|
|
372
375
|
"--duration", type=int, help="Duration in seconds for script action"
|
|
373
376
|
)
|
|
374
377
|
parser.add_argument("--debug", action="store_true", help="Enable debug logging")
|
|
375
|
-
parser.add_argument(
|
|
376
|
-
"--simulation",
|
|
377
|
-
action="store_true",
|
|
378
|
-
help="Run in simulation mode (no real transactions)",
|
|
379
|
-
)
|
|
380
378
|
parser.add_argument(
|
|
381
379
|
"--wallet-id",
|
|
382
380
|
help="Wallet ID for policy rendering (replaces FORMAT_WALLET_ID in policies)",
|
|
@@ -401,7 +399,6 @@ def main():
|
|
|
401
399
|
gas_token_amount=args.gas_token_amount,
|
|
402
400
|
interval=args.interval,
|
|
403
401
|
duration=args.duration,
|
|
404
|
-
simulation=args.simulation,
|
|
405
402
|
wallet_id=getattr(args, "wallet_id", None),
|
|
406
403
|
)
|
|
407
404
|
)
|
|
@@ -15,27 +15,6 @@ def to_keystore_json(private_key_hex: str, password: str):
|
|
|
15
15
|
return Account.encrypt(private_key_hex, password)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def write_env(rows: list[dict[str, str]], out_dir: Path) -> None:
|
|
19
|
-
with open(out_dir / ".env.example", "w") as f:
|
|
20
|
-
if rows:
|
|
21
|
-
label_to_wallet = {r.get("label"): r for r in rows if r.get("label")}
|
|
22
|
-
main_w = (
|
|
23
|
-
label_to_wallet.get("main") or label_to_wallet.get("default") or rows[0]
|
|
24
|
-
)
|
|
25
|
-
strategy_w = label_to_wallet.get("strategy")
|
|
26
|
-
|
|
27
|
-
f.write("RPC_URL=https://rpc.ankr.com/eth\n")
|
|
28
|
-
# Back-compat defaults
|
|
29
|
-
f.write(f"PRIVATE_KEY={main_w['private_key_hex']}\n")
|
|
30
|
-
f.write(f"FROM_ADDRESS={main_w['address']}\n")
|
|
31
|
-
# Explicit main/strategy variables
|
|
32
|
-
f.write(f"MAIN_WALLET_ADDRESS={main_w['address']}\n")
|
|
33
|
-
if strategy_w:
|
|
34
|
-
f.write(f"STRATEGY_WALLET_ADDRESS={strategy_w['address']}\n")
|
|
35
|
-
# Optional: expose strategy private key for local dev only
|
|
36
|
-
f.write(f"PRIVATE_KEY_STRATEGY={strategy_w['private_key_hex']}\n")
|
|
37
|
-
|
|
38
|
-
|
|
39
18
|
def main():
|
|
40
19
|
parser = argparse.ArgumentParser(description="Generate local dev wallets")
|
|
41
20
|
parser.add_argument(
|
|
@@ -48,7 +27,7 @@ def main():
|
|
|
48
27
|
"--out-dir",
|
|
49
28
|
type=Path,
|
|
50
29
|
default=Path("."),
|
|
51
|
-
help="Output directory for wallets.json (and
|
|
30
|
+
help="Output directory for wallets.json (and keystore files)",
|
|
52
31
|
)
|
|
53
32
|
parser.add_argument(
|
|
54
33
|
"--keystore-password",
|
|
@@ -161,9 +140,6 @@ def main():
|
|
|
161
140
|
ks_path.write_text(json.dumps(ks))
|
|
162
141
|
index += 1
|
|
163
142
|
|
|
164
|
-
# Convenience outputs
|
|
165
|
-
write_env(rows, args.out_dir)
|
|
166
|
-
|
|
167
143
|
|
|
168
144
|
if __name__ == "__main__":
|
|
169
145
|
main()
|
|
@@ -55,7 +55,6 @@ async def _run(args: argparse.Namespace) -> int:
|
|
|
55
55
|
"main_wallet": main_wallet,
|
|
56
56
|
"strategy_wallet": strategy_wallet,
|
|
57
57
|
},
|
|
58
|
-
simulation=args.simulation,
|
|
59
58
|
)
|
|
60
59
|
|
|
61
60
|
await s.setup()
|
|
@@ -101,7 +100,6 @@ def main() -> int:
|
|
|
101
100
|
)
|
|
102
101
|
p.add_argument("--main-wallet-label", default="main")
|
|
103
102
|
p.add_argument("--strategy-wallet-label", default="basis_trading_strategy")
|
|
104
|
-
p.add_argument("--simulation", action="store_true")
|
|
105
103
|
|
|
106
104
|
sub = p.add_subparsers(dest="command", required=True)
|
|
107
105
|
|
|
@@ -8,7 +8,6 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import asyncio
|
|
10
10
|
import json
|
|
11
|
-
import os
|
|
12
11
|
import time
|
|
13
12
|
from datetime import UTC, datetime
|
|
14
13
|
from decimal import ROUND_DOWN, Decimal
|
|
@@ -1007,5 +1006,4 @@ class BasisSnapshotMixin:
|
|
|
1007
1006
|
)
|
|
1008
1007
|
if isinstance(val, str) and val.strip():
|
|
1009
1008
|
return val.strip()
|
|
1010
|
-
|
|
1011
|
-
return env.strip() if isinstance(env, str) and env.strip() else None
|
|
1009
|
+
return None
|
|
@@ -208,7 +208,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
208
208
|
*,
|
|
209
209
|
main_wallet: dict[str, Any] | None = None,
|
|
210
210
|
strategy_wallet: dict[str, Any] | None = None,
|
|
211
|
-
simulation: bool = False,
|
|
212
211
|
web3_service: Web3Service | None = None,
|
|
213
212
|
hyperliquid_executor: HyperliquidExecutor | None = None,
|
|
214
213
|
api_key: str | None = None,
|
|
@@ -221,7 +220,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
221
220
|
if strategy_wallet:
|
|
222
221
|
merged_config["strategy_wallet"] = strategy_wallet
|
|
223
222
|
self.config = merged_config
|
|
224
|
-
self.simulation = simulation
|
|
225
223
|
|
|
226
224
|
# Position tracking
|
|
227
225
|
self.current_position: BasisPosition | None = None
|
|
@@ -249,10 +247,10 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
249
247
|
"strategy": self.config,
|
|
250
248
|
}
|
|
251
249
|
|
|
252
|
-
# Create Hyperliquid executor if not provided
|
|
250
|
+
# Create Hyperliquid executor if not provided.
|
|
253
251
|
# This is only required for placing/canceling orders (not market reads).
|
|
254
252
|
hl_executor = hyperliquid_executor
|
|
255
|
-
if hl_executor is None
|
|
253
|
+
if hl_executor is None:
|
|
256
254
|
try:
|
|
257
255
|
hl_executor = LocalHyperliquidExecutor(config=adapter_config)
|
|
258
256
|
self.logger.info("Created LocalHyperliquidExecutor for real execution")
|
|
@@ -267,7 +265,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
267
265
|
try:
|
|
268
266
|
self.hyperliquid_adapter = HyperliquidAdapter(
|
|
269
267
|
config=adapter_config,
|
|
270
|
-
simulation=self.simulation,
|
|
271
268
|
executor=hl_executor,
|
|
272
269
|
)
|
|
273
270
|
except Exception as e:
|
|
@@ -280,7 +277,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
280
277
|
tx_adapter = LocalTokenTxnService(
|
|
281
278
|
adapter_config,
|
|
282
279
|
wallet_provider=wallet_provider,
|
|
283
|
-
simulation=self.simulation,
|
|
284
280
|
)
|
|
285
281
|
web3_service = DefaultWeb3Service(
|
|
286
282
|
wallet_provider=wallet_provider, evm_transactions=tx_adapter
|
|
@@ -502,18 +498,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
502
498
|
return (False, f"Failed to transfer ETH for gas: {gas_res}")
|
|
503
499
|
self.logger.info(f"Gas transfer successful: {gas_res}")
|
|
504
500
|
|
|
505
|
-
# Simulation mode - just track the deposit
|
|
506
|
-
if self.simulation:
|
|
507
|
-
self.logger.info(
|
|
508
|
-
f"[SIMULATION] Would send {main_token_amount} USDC to Hyperliquid bridge"
|
|
509
|
-
)
|
|
510
|
-
self.deposit_amount = main_token_amount
|
|
511
|
-
return (
|
|
512
|
-
True,
|
|
513
|
-
f"[SIMULATION] Deposited {main_token_amount} USDC. "
|
|
514
|
-
f"Call update() to analyze and open positions.",
|
|
515
|
-
)
|
|
516
|
-
|
|
517
501
|
# Real deposit: ensure funds are in the strategy wallet, then send USDC to bridge.
|
|
518
502
|
try:
|
|
519
503
|
main_address = self._get_main_wallet_address()
|
|
@@ -926,11 +910,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
926
910
|
if not close_success:
|
|
927
911
|
return (False, f"Failed to close position: {close_msg}")
|
|
928
912
|
|
|
929
|
-
if self.simulation:
|
|
930
|
-
withdrawn = self.deposit_amount
|
|
931
|
-
self.deposit_amount = 0
|
|
932
|
-
return (True, f"[SIMULATION] Withdrew {withdrawn} USDC to main wallet")
|
|
933
|
-
|
|
934
913
|
# Step 1: Transfer any spot USDC to perp for withdrawal
|
|
935
914
|
success, spot_state = await self.hyperliquid_adapter.get_spot_user_state(
|
|
936
915
|
address
|
|
@@ -1168,9 +1147,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1168
1147
|
if not self.builder_fee:
|
|
1169
1148
|
return True, "No builder fee configured"
|
|
1170
1149
|
|
|
1171
|
-
if self.simulation:
|
|
1172
|
-
return True, "[SIMULATION] Builder fee approval skipped"
|
|
1173
|
-
|
|
1174
1150
|
address = self._get_strategy_wallet_address()
|
|
1175
1151
|
builder = self.builder_fee.get("b", "")
|
|
1176
1152
|
required_fee = self.builder_fee.get("f", 0)
|
|
@@ -1357,112 +1333,98 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1357
1333
|
Decimal(str(order_usd)).quantize(Decimal("0.01"), rounding=ROUND_UP)
|
|
1358
1334
|
)
|
|
1359
1335
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
)
|
|
1364
|
-
spot_filled = target_qty
|
|
1365
|
-
perp_filled = target_qty
|
|
1366
|
-
spot_notional = order_usd
|
|
1367
|
-
perp_notional = order_usd
|
|
1368
|
-
entry_price = float(best.get("mark_price", 0.0) or 100.0)
|
|
1369
|
-
else:
|
|
1370
|
-
# Step 1: Ensure builder fee is approved
|
|
1371
|
-
fee_success, fee_msg = await self.ensure_builder_fee_approved()
|
|
1372
|
-
if not fee_success:
|
|
1373
|
-
return (False, f"Builder fee approval failed: {fee_msg}")
|
|
1374
|
-
|
|
1375
|
-
# Step 2: Update leverage for the perp asset
|
|
1376
|
-
self.logger.info(f"Setting leverage to {leverage}x for {coin}")
|
|
1377
|
-
success, lev_result = await self.hyperliquid_adapter.update_leverage(
|
|
1378
|
-
asset_id=perp_asset_id,
|
|
1379
|
-
leverage=leverage,
|
|
1380
|
-
is_cross=True,
|
|
1381
|
-
address=address,
|
|
1382
|
-
)
|
|
1383
|
-
if not success:
|
|
1384
|
-
self.logger.warning(f"Failed to set leverage: {lev_result}")
|
|
1385
|
-
# Continue anyway - leverage might already be set
|
|
1336
|
+
# Step 1: Ensure builder fee is approved
|
|
1337
|
+
fee_success, fee_msg = await self.ensure_builder_fee_approved()
|
|
1338
|
+
if not fee_success:
|
|
1339
|
+
return (False, f"Builder fee approval failed: {fee_msg}")
|
|
1386
1340
|
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
)
|
|
1399
|
-
if not success:
|
|
1400
|
-
self.logger.warning(
|
|
1401
|
-
f"Perp to spot transfer failed: {transfer_result}"
|
|
1402
|
-
)
|
|
1403
|
-
# May fail if already in spot, continue
|
|
1341
|
+
# Step 2: Update leverage for the perp asset
|
|
1342
|
+
self.logger.info(f"Setting leverage to {leverage}x for {coin}")
|
|
1343
|
+
success, lev_result = await self.hyperliquid_adapter.update_leverage(
|
|
1344
|
+
asset_id=perp_asset_id,
|
|
1345
|
+
leverage=leverage,
|
|
1346
|
+
is_cross=True,
|
|
1347
|
+
address=address,
|
|
1348
|
+
)
|
|
1349
|
+
if not success:
|
|
1350
|
+
self.logger.warning(f"Failed to set leverage: {lev_result}")
|
|
1351
|
+
# Continue anyway - leverage might already be set
|
|
1404
1352
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1353
|
+
# Step 3: Transfer USDC from perp to spot for spot purchase
|
|
1354
|
+
# We need approximately order_usd in spot to buy the asset
|
|
1355
|
+
self.logger.info(
|
|
1356
|
+
f"Transferring ${order_usd:.2f} from perp to spot for {coin}"
|
|
1357
|
+
)
|
|
1358
|
+
(
|
|
1359
|
+
success,
|
|
1360
|
+
transfer_result,
|
|
1361
|
+
) = await self.hyperliquid_adapter.transfer_perp_to_spot(
|
|
1362
|
+
amount=order_usd,
|
|
1363
|
+
address=address,
|
|
1364
|
+
)
|
|
1365
|
+
if not success:
|
|
1366
|
+
self.logger.warning(f"Perp to spot transfer failed: {transfer_result}")
|
|
1367
|
+
# May fail if already in spot, continue
|
|
1411
1368
|
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
perp_pointers,
|
|
1419
|
-
) = await filler.fill_pair_units(
|
|
1420
|
-
coin=coin,
|
|
1421
|
-
spot_asset_id=spot_asset_id,
|
|
1422
|
-
perp_asset_id=perp_asset_id,
|
|
1423
|
-
total_units=target_qty,
|
|
1424
|
-
direction="long_spot_short_perp",
|
|
1425
|
-
builder_fee=self.builder_fee,
|
|
1426
|
-
)
|
|
1369
|
+
# Step 4: Execute paired fill
|
|
1370
|
+
filler = PairedFiller(
|
|
1371
|
+
adapter=self.hyperliquid_adapter,
|
|
1372
|
+
address=address,
|
|
1373
|
+
cfg=FillConfig(max_slip_bps=35, max_chunk_usd=7500.0),
|
|
1374
|
+
)
|
|
1427
1375
|
|
|
1428
|
-
|
|
1429
|
-
|
|
1376
|
+
(
|
|
1377
|
+
spot_filled,
|
|
1378
|
+
perp_filled,
|
|
1379
|
+
spot_notional,
|
|
1380
|
+
perp_notional,
|
|
1381
|
+
spot_pointers,
|
|
1382
|
+
perp_pointers,
|
|
1383
|
+
) = await filler.fill_pair_units(
|
|
1384
|
+
coin=coin,
|
|
1385
|
+
spot_asset_id=spot_asset_id,
|
|
1386
|
+
perp_asset_id=perp_asset_id,
|
|
1387
|
+
total_units=target_qty,
|
|
1388
|
+
direction="long_spot_short_perp",
|
|
1389
|
+
builder_fee=self.builder_fee,
|
|
1390
|
+
)
|
|
1430
1391
|
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
f"notional=${spot_notional:.2f}/${perp_notional:.2f}"
|
|
1434
|
-
)
|
|
1392
|
+
if spot_filled <= 0 or perp_filled <= 0:
|
|
1393
|
+
return (False, f"Failed to fill basis position on {coin}")
|
|
1435
1394
|
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1395
|
+
self.logger.info(
|
|
1396
|
+
f"Filled basis position: spot={spot_filled:.6f}, perp={perp_filled:.6f}, "
|
|
1397
|
+
f"notional=${spot_notional:.2f}/${perp_notional:.2f}"
|
|
1398
|
+
)
|
|
1439
1399
|
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
)
|
|
1444
|
-
liquidation_price = None
|
|
1445
|
-
if success:
|
|
1446
|
-
for pos_wrapper in user_state.get("assetPositions", []):
|
|
1447
|
-
pos = pos_wrapper.get("position", {})
|
|
1448
|
-
if pos.get("coin") == coin:
|
|
1449
|
-
liquidation_price = float(pos.get("liquidationPx", 0))
|
|
1450
|
-
break
|
|
1400
|
+
# Get entry price from current mid
|
|
1401
|
+
success, mids = await self.hyperliquid_adapter.get_all_mid_prices()
|
|
1402
|
+
entry_price = mids.get(coin, 0.0) if success else 0.0
|
|
1451
1403
|
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1404
|
+
# Step 5: Get liquidation price and place stop-loss
|
|
1405
|
+
success, user_state = await self.hyperliquid_adapter.get_user_state(address)
|
|
1406
|
+
liquidation_price = None
|
|
1407
|
+
if success:
|
|
1408
|
+
for pos_wrapper in user_state.get("assetPositions", []):
|
|
1409
|
+
pos = pos_wrapper.get("position", {})
|
|
1410
|
+
if pos.get("coin") == coin:
|
|
1411
|
+
liquidation_price = float(pos.get("liquidationPx", 0))
|
|
1412
|
+
break
|
|
1413
|
+
|
|
1414
|
+
if liquidation_price and liquidation_price > 0:
|
|
1415
|
+
sl_success, sl_msg = await self._place_stop_loss_orders(
|
|
1416
|
+
coin=coin,
|
|
1417
|
+
perp_asset_id=perp_asset_id,
|
|
1418
|
+
position_size=perp_filled,
|
|
1419
|
+
entry_price=entry_price,
|
|
1420
|
+
liquidation_price=liquidation_price,
|
|
1421
|
+
spot_asset_id=spot_asset_id,
|
|
1422
|
+
spot_position_size=spot_filled,
|
|
1423
|
+
)
|
|
1424
|
+
if not sl_success:
|
|
1425
|
+
self.logger.warning(f"Stop-loss placement failed: {sl_msg}")
|
|
1426
|
+
else:
|
|
1427
|
+
self.logger.warning("Could not get liquidation price for stop-loss")
|
|
1466
1428
|
|
|
1467
1429
|
# Create position record
|
|
1468
1430
|
self.current_position = BasisPosition(
|
|
@@ -1836,9 +1798,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1836
1798
|
if self.current_position is None:
|
|
1837
1799
|
return True, "No position"
|
|
1838
1800
|
|
|
1839
|
-
if self.simulation:
|
|
1840
|
-
return True, "[SIMULATION] Would repair leg imbalance"
|
|
1841
|
-
|
|
1842
1801
|
pos = self.current_position
|
|
1843
1802
|
coin = pos.coin
|
|
1844
1803
|
address = self._get_strategy_wallet_address()
|
|
@@ -1930,9 +1889,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
1930
1889
|
if self.current_position is None:
|
|
1931
1890
|
return True, "No position"
|
|
1932
1891
|
|
|
1933
|
-
if self.simulation:
|
|
1934
|
-
return True, "[SIMULATION] Stop-loss check skipped"
|
|
1935
|
-
|
|
1936
1892
|
pos = self.current_position
|
|
1937
1893
|
coin = pos.coin
|
|
1938
1894
|
|
|
@@ -2026,13 +1982,6 @@ class BasisTradingStrategy(BasisSnapshotMixin, Strategy):
|
|
|
2026
1982
|
pos = self.current_position
|
|
2027
1983
|
self.logger.info(f"Closing position on {pos.coin}")
|
|
2028
1984
|
|
|
2029
|
-
if self.simulation:
|
|
2030
|
-
self.logger.info(
|
|
2031
|
-
f"[SIMULATION] Would close {pos.spot_amount} {pos.coin} basis position"
|
|
2032
|
-
)
|
|
2033
|
-
self.current_position = None
|
|
2034
|
-
return (True, "Position closed (simulation)")
|
|
2035
|
-
|
|
2036
1985
|
# Cancel all stop-loss and limit orders first
|
|
2037
1986
|
await self._cancel_all_position_orders()
|
|
2038
1987
|
|