wayfinder-paths 0.1.8__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 +0 -51
- 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/pool_adapter/README.md +1 -77
- wayfinder_paths/adapters/pool_adapter/adapter.py +0 -122
- wayfinder_paths/adapters/pool_adapter/examples.json +0 -57
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +0 -86
- wayfinder_paths/adapters/token_adapter/README.md +1 -1
- wayfinder_paths/core/clients/AuthClient.py +0 -3
- wayfinder_paths/core/clients/ClientManager.py +1 -22
- 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 -60
- 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 +0 -4
- wayfinder_paths/templates/strategy/test_strategy.py +0 -4
- {wayfinder_paths-0.1.8.dist-info → wayfinder_paths-0.1.9.dist-info}/METADATA +5 -15
- {wayfinder_paths-0.1.8.dist-info → wayfinder_paths-0.1.9.dist-info}/RECORD +45 -47
- wayfinder_paths/core/clients/SimulationClient.py +0 -192
- wayfinder_paths/core/clients/TransactionClient.py +0 -63
- {wayfinder_paths-0.1.8.dist-info → wayfinder_paths-0.1.9.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.8.dist-info → wayfinder_paths-0.1.9.dist-info}/WHEEL +0 -0
|
@@ -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
|
|
|
@@ -119,12 +119,32 @@ class TestBasisTradingStrategy:
|
|
|
119
119
|
return_value=(
|
|
120
120
|
True,
|
|
121
121
|
{
|
|
122
|
-
"marginSummary": {"accountValue": "0"
|
|
122
|
+
"marginSummary": {"accountValue": "0"},
|
|
123
|
+
"withdrawable": "100.0", # Top-level withdrawable for withdraw() method
|
|
123
124
|
"assetPositions": [],
|
|
124
125
|
},
|
|
125
126
|
)
|
|
126
127
|
)
|
|
127
128
|
mock.get_spot_user_state = AsyncMock(return_value=(True, {"balances": []}))
|
|
129
|
+
mock.get_max_builder_fee = AsyncMock(return_value=(True, 0))
|
|
130
|
+
mock.approve_builder_fee = AsyncMock(return_value=(True, {"status": "ok"}))
|
|
131
|
+
mock.update_leverage = AsyncMock(return_value=(True, {"status": "ok"}))
|
|
132
|
+
mock.transfer_perp_to_spot = AsyncMock(return_value=(True, {"status": "ok"}))
|
|
133
|
+
mock.transfer_spot_to_perp = AsyncMock(return_value=(True, {"status": "ok"}))
|
|
134
|
+
mock.place_market_order = AsyncMock(return_value=(True, {"status": "ok"}))
|
|
135
|
+
mock.place_limit_order = AsyncMock(return_value=(True, {"status": "ok"}))
|
|
136
|
+
mock.place_stop_loss = AsyncMock(return_value=(True, {"status": "ok"}))
|
|
137
|
+
mock.cancel_order = AsyncMock(return_value=(True, {"status": "ok"}))
|
|
138
|
+
mock.withdraw = AsyncMock(return_value=(True, {"status": "ok"}))
|
|
139
|
+
mock.get_open_orders = AsyncMock(return_value=(True, []))
|
|
140
|
+
mock.get_frontend_open_orders = AsyncMock(return_value=(True, []))
|
|
141
|
+
mock.get_valid_order_size = MagicMock(side_effect=lambda _asset, size: size)
|
|
142
|
+
mock.wait_for_deposit = AsyncMock(
|
|
143
|
+
return_value=(True, 100.0) # (deposit_confirmed, final_balance)
|
|
144
|
+
)
|
|
145
|
+
mock.wait_for_withdrawal = AsyncMock(
|
|
146
|
+
return_value=(True, {"0x123456": 100.0}) # tx_hash -> amount (float)
|
|
147
|
+
)
|
|
128
148
|
return mock
|
|
129
149
|
|
|
130
150
|
@pytest.fixture
|
|
@@ -162,7 +182,6 @@ class TestBasisTradingStrategy:
|
|
|
162
182
|
"main_wallet": {"address": "0x1234"},
|
|
163
183
|
"strategy_wallet": {"address": "0x5678"},
|
|
164
184
|
},
|
|
165
|
-
simulation=True,
|
|
166
185
|
)
|
|
167
186
|
s.hyperliquid_adapter = mock_hyperliquid_adapter
|
|
168
187
|
s.ledger_adapter = ledger_adapter
|
|
@@ -173,9 +192,45 @@ class TestBasisTradingStrategy:
|
|
|
173
192
|
s.balance_adapter.move_from_main_wallet_to_strategy_wallet = AsyncMock(
|
|
174
193
|
return_value=(True, {})
|
|
175
194
|
)
|
|
195
|
+
s.balance_adapter.move_from_strategy_wallet_to_main_wallet = AsyncMock(
|
|
196
|
+
return_value=(True, {})
|
|
197
|
+
)
|
|
176
198
|
s.balance_adapter.send_to_address = AsyncMock(
|
|
177
199
|
return_value=(True, {"tx_hash": "0x123"})
|
|
178
200
|
)
|
|
201
|
+
# Mock internal dependencies to prevent MagicMock await errors
|
|
202
|
+
# These are needed if the real method somehow gets called
|
|
203
|
+
s.balance_adapter.token_client = AsyncMock()
|
|
204
|
+
s.balance_adapter.token_client.get_token_details = (
|
|
205
|
+
AsyncMock(
|
|
206
|
+
return_value={
|
|
207
|
+
"id": "usdc",
|
|
208
|
+
"address": "0x1234",
|
|
209
|
+
"decimals": 6,
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
s.balance_adapter.token_transactions = AsyncMock()
|
|
214
|
+
s.balance_adapter.token_transactions.build_send = AsyncMock(
|
|
215
|
+
return_value=(True, {"transaction": "0xMOCK"})
|
|
216
|
+
)
|
|
217
|
+
s.balance_adapter.wallet_provider = AsyncMock()
|
|
218
|
+
s.balance_adapter.wallet_provider.broadcast_transaction = (
|
|
219
|
+
AsyncMock(
|
|
220
|
+
return_value=(True, {"transaction_hash": "0x123"})
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
s.balance_adapter.token_adapter = AsyncMock()
|
|
224
|
+
s.balance_adapter.token_adapter.get_token_price = AsyncMock(
|
|
225
|
+
return_value=(True, {"current_price": 1.0})
|
|
226
|
+
)
|
|
227
|
+
# ledger_adapter is real, but ensure its methods are async-mockable
|
|
228
|
+
s.balance_adapter.ledger_adapter = ledger_adapter
|
|
229
|
+
# Also ensure the balance_adapter's _move_between_wallets won't call real methods
|
|
230
|
+
# by making sure all its dependencies return AsyncMock
|
|
231
|
+
s.balance_adapter._move_between_wallets = AsyncMock(
|
|
232
|
+
return_value=(True, {"transaction_hash": "0x123"})
|
|
233
|
+
)
|
|
179
234
|
return s
|
|
180
235
|
|
|
181
236
|
@pytest.mark.asyncio
|
|
@@ -185,22 +240,32 @@ class TestBasisTradingStrategy:
|
|
|
185
240
|
examples = load_examples()
|
|
186
241
|
smoke = examples["smoke"]
|
|
187
242
|
|
|
188
|
-
#
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
243
|
+
# Mock PairedFiller for update() and withdraw() to work
|
|
244
|
+
with patch(
|
|
245
|
+
"wayfinder_paths.strategies.basis_trading_strategy.strategy.PairedFiller"
|
|
246
|
+
) as mock_filler_class:
|
|
247
|
+
mock_filler = MagicMock()
|
|
248
|
+
mock_filler.fill_pair_units = AsyncMock(
|
|
249
|
+
return_value=(0.5, 0.5, 1000.0, 1000.0, [], [])
|
|
250
|
+
)
|
|
251
|
+
mock_filler_class.return_value = mock_filler
|
|
252
|
+
|
|
253
|
+
# Deposit
|
|
254
|
+
deposit_params = smoke.get("deposit", {})
|
|
255
|
+
success, msg = await strategy.deposit(**deposit_params)
|
|
256
|
+
assert success, f"Deposit failed: {msg}"
|
|
192
257
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
258
|
+
# Update
|
|
259
|
+
success, msg = await strategy.update()
|
|
260
|
+
assert success, f"Update failed: {msg}"
|
|
196
261
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
262
|
+
# Status
|
|
263
|
+
status = await strategy.status()
|
|
264
|
+
assert "portfolio_value" in status
|
|
200
265
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
266
|
+
# Withdraw (needs PairedFiller mock for _close_position)
|
|
267
|
+
success, msg = await strategy.withdraw()
|
|
268
|
+
assert success, f"Withdraw failed: {msg}"
|
|
204
269
|
|
|
205
270
|
@pytest.mark.asyncio
|
|
206
271
|
async def test_deposit_minimum(self, strategy):
|
|
@@ -311,8 +376,23 @@ class TestBasisTradingStrategy:
|
|
|
311
376
|
assert opps[0]["selection"]["net_apy"] is not None
|
|
312
377
|
|
|
313
378
|
@pytest.mark.asyncio
|
|
314
|
-
async def test_get_undeployed_capital_empty(
|
|
379
|
+
async def test_get_undeployed_capital_empty(
|
|
380
|
+
self, strategy, mock_hyperliquid_adapter
|
|
381
|
+
):
|
|
315
382
|
"""Test _get_undeployed_capital with no capital."""
|
|
383
|
+
mock_hyperliquid_adapter.get_user_state = AsyncMock(
|
|
384
|
+
return_value=(
|
|
385
|
+
True,
|
|
386
|
+
{
|
|
387
|
+
"marginSummary": {"accountValue": "0", "withdrawable": "0"},
|
|
388
|
+
"withdrawable": "0",
|
|
389
|
+
"assetPositions": [],
|
|
390
|
+
},
|
|
391
|
+
)
|
|
392
|
+
)
|
|
393
|
+
mock_hyperliquid_adapter.get_spot_user_state = AsyncMock(
|
|
394
|
+
return_value=(True, {"balances": []})
|
|
395
|
+
)
|
|
316
396
|
perp_margin, spot_usdc = await strategy._get_undeployed_capital()
|
|
317
397
|
assert perp_margin == 0.0
|
|
318
398
|
assert spot_usdc == 0.0
|
|
@@ -508,7 +588,6 @@ class TestBasisTradingStrategy:
|
|
|
508
588
|
"main_wallet": {"address": "0x1234"},
|
|
509
589
|
"strategy_wallet": {"address": "0x5678"},
|
|
510
590
|
},
|
|
511
|
-
simulation=False, # Not simulation mode
|
|
512
591
|
)
|
|
513
592
|
s.hyperliquid_adapter = mock_hyperliquid_adapter
|
|
514
593
|
s.ledger_adapter = ledger_adapter
|
|
@@ -561,7 +640,6 @@ class TestBasisTradingStrategy:
|
|
|
561
640
|
"main_wallet": {"address": "0x1234"},
|
|
562
641
|
"strategy_wallet": {"address": "0x5678"},
|
|
563
642
|
},
|
|
564
|
-
simulation=False, # Not simulation mode
|
|
565
643
|
)
|
|
566
644
|
s.hyperliquid_adapter = mock_hyperliquid_adapter
|
|
567
645
|
s.ledger_adapter = ledger_adapter
|
|
@@ -580,46 +658,6 @@ class TestBasisTradingStrategy:
|
|
|
580
658
|
# Should have called approve_builder_fee
|
|
581
659
|
mock_hyperliquid_adapter.approve_builder_fee.assert_called_once()
|
|
582
660
|
|
|
583
|
-
@pytest.mark.asyncio
|
|
584
|
-
async def test_ensure_builder_fee_simulation_mode(
|
|
585
|
-
self, mock_hyperliquid_adapter, ledger_adapter
|
|
586
|
-
):
|
|
587
|
-
"""Test ensure_builder_fee_approved in simulation mode."""
|
|
588
|
-
with patch(
|
|
589
|
-
"wayfinder_paths.strategies.basis_trading_strategy.strategy.HyperliquidAdapter",
|
|
590
|
-
return_value=mock_hyperliquid_adapter,
|
|
591
|
-
):
|
|
592
|
-
with patch(
|
|
593
|
-
"wayfinder_paths.strategies.basis_trading_strategy.strategy.BalanceAdapter"
|
|
594
|
-
):
|
|
595
|
-
with patch(
|
|
596
|
-
"wayfinder_paths.strategies.basis_trading_strategy.strategy.TokenAdapter"
|
|
597
|
-
):
|
|
598
|
-
with patch(
|
|
599
|
-
"wayfinder_paths.strategies.basis_trading_strategy.strategy.LedgerAdapter",
|
|
600
|
-
return_value=ledger_adapter,
|
|
601
|
-
):
|
|
602
|
-
with patch(
|
|
603
|
-
"wayfinder_paths.strategies.basis_trading_strategy.strategy.WalletManager"
|
|
604
|
-
):
|
|
605
|
-
from wayfinder_paths.strategies.basis_trading_strategy.strategy import (
|
|
606
|
-
BasisTradingStrategy,
|
|
607
|
-
)
|
|
608
|
-
|
|
609
|
-
s = BasisTradingStrategy(
|
|
610
|
-
config={
|
|
611
|
-
"main_wallet": {"address": "0x1234"},
|
|
612
|
-
"strategy_wallet": {"address": "0x5678"},
|
|
613
|
-
},
|
|
614
|
-
simulation=True, # Simulation mode
|
|
615
|
-
)
|
|
616
|
-
s.hyperliquid_adapter = mock_hyperliquid_adapter
|
|
617
|
-
s.ledger_adapter = ledger_adapter
|
|
618
|
-
|
|
619
|
-
success, msg = await s.ensure_builder_fee_approved()
|
|
620
|
-
assert success is True
|
|
621
|
-
assert "simulation" in msg.lower()
|
|
622
|
-
|
|
623
661
|
@pytest.mark.asyncio
|
|
624
662
|
async def test_portfolio_value_includes_spot_holdings(
|
|
625
663
|
self, strategy, mock_hyperliquid_adapter
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
# Hyperlend Stable Yield Strategy
|
|
2
2
|
|
|
3
3
|
- Entrypoint: `strategies.hyperlend_stable_yield_strategy.strategy.HyperlendStableYieldStrategy`
|
|
4
4
|
- Manifest: `manifest.yaml`
|
|
@@ -79,7 +79,7 @@ poetry install
|
|
|
79
79
|
# Creates a main wallet (or use 'just create-strategy' which auto-creates wallets)
|
|
80
80
|
poetry run python wayfinder_paths/scripts/make_wallets.py -n 1
|
|
81
81
|
|
|
82
|
-
# Copy config and edit credentials
|
|
82
|
+
# Copy config and edit credentials
|
|
83
83
|
cp wayfinder_paths/config.example.json config.json
|
|
84
84
|
|
|
85
85
|
# Check status / health
|