wayfinder-paths 0.1.13__py3-none-any.whl → 0.1.15__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wayfinder-paths might be problematic. Click here for more details.
- wayfinder_paths/adapters/balance_adapter/README.md +13 -14
- wayfinder_paths/adapters/balance_adapter/adapter.py +73 -32
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +123 -0
- wayfinder_paths/adapters/brap_adapter/README.md +11 -16
- wayfinder_paths/adapters/brap_adapter/adapter.py +144 -78
- wayfinder_paths/adapters/brap_adapter/examples.json +63 -52
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +127 -65
- wayfinder_paths/adapters/hyperlend_adapter/adapter.py +30 -14
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +121 -67
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +6 -6
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +12 -12
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +6 -6
- wayfinder_paths/adapters/moonwell_adapter/adapter.py +332 -9
- wayfinder_paths/adapters/moonwell_adapter/test_adapter.py +13 -13
- wayfinder_paths/adapters/pool_adapter/README.md +9 -10
- wayfinder_paths/adapters/pool_adapter/adapter.py +9 -10
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +2 -2
- wayfinder_paths/adapters/token_adapter/README.md +2 -14
- wayfinder_paths/adapters/token_adapter/adapter.py +16 -10
- wayfinder_paths/adapters/token_adapter/examples.json +4 -8
- wayfinder_paths/adapters/token_adapter/test_adapter.py +9 -7
- wayfinder_paths/core/clients/BRAPClient.py +102 -61
- wayfinder_paths/core/clients/ClientManager.py +1 -68
- wayfinder_paths/core/clients/HyperlendClient.py +125 -64
- wayfinder_paths/core/clients/LedgerClient.py +1 -4
- wayfinder_paths/core/clients/PoolClient.py +122 -48
- wayfinder_paths/core/clients/TokenClient.py +91 -36
- wayfinder_paths/core/clients/WalletClient.py +26 -56
- wayfinder_paths/core/clients/WayfinderClient.py +28 -160
- wayfinder_paths/core/clients/__init__.py +0 -2
- wayfinder_paths/core/clients/protocols.py +35 -46
- wayfinder_paths/core/clients/sdk_example.py +37 -22
- wayfinder_paths/core/constants/erc20_abi.py +0 -11
- wayfinder_paths/core/engine/StrategyJob.py +10 -56
- wayfinder_paths/core/services/base.py +1 -0
- wayfinder_paths/core/services/local_evm_txn.py +25 -9
- wayfinder_paths/core/services/local_token_txn.py +2 -6
- wayfinder_paths/core/services/test_local_evm_txn.py +145 -0
- wayfinder_paths/core/strategies/Strategy.py +16 -4
- wayfinder_paths/core/utils/evm_helpers.py +2 -9
- wayfinder_paths/policies/erc20.py +1 -1
- wayfinder_paths/run_strategy.py +13 -19
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +77 -11
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +6 -6
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/strategy.py +107 -23
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +54 -9
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/README.md +6 -5
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/strategy.py +2246 -1279
- wayfinder_paths/strategies/moonwell_wsteth_loop_strategy/test_strategy.py +276 -109
- wayfinder_paths/strategies/stablecoin_yield_strategy/README.md +1 -1
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +153 -56
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +16 -12
- wayfinder_paths/templates/adapter/README.md +1 -1
- wayfinder_paths/templates/strategy/README.md +3 -3
- wayfinder_paths/templates/strategy/test_strategy.py +3 -2
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/METADATA +14 -49
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/RECORD +59 -60
- wayfinder_paths/abis/generic/erc20.json +0 -383
- wayfinder_paths/core/clients/AuthClient.py +0 -83
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.13.dist-info → wayfinder_paths-0.1.15.dist-info}/WHEEL +0 -0
|
@@ -217,32 +217,36 @@ async def test_canonical_usage(strategy, mock_adapter_responses):
|
|
|
217
217
|
@pytest.mark.asyncio
|
|
218
218
|
async def test_status_returns_status_dict(strategy, mock_adapter_responses):
|
|
219
219
|
"""Test that _status returns a proper StatusDict."""
|
|
220
|
+
snap = MagicMock()
|
|
221
|
+
snap.totals_usd = {}
|
|
222
|
+
snap.ltv = 0.5
|
|
223
|
+
snap.debt_usd = 100.0
|
|
224
|
+
snap.net_equity_usd = 0.0
|
|
225
|
+
|
|
220
226
|
with patch.object(
|
|
221
|
-
strategy, "
|
|
222
|
-
) as
|
|
223
|
-
|
|
224
|
-
with patch.object(
|
|
225
|
-
|
|
227
|
+
strategy, "_accounting_snapshot", new_callable=AsyncMock
|
|
228
|
+
) as mock_snap:
|
|
229
|
+
mock_snap.return_value = (snap, (0.8, 0.8))
|
|
230
|
+
with patch.object(
|
|
231
|
+
strategy, "_get_gas_balance", new_callable=AsyncMock
|
|
232
|
+
) as mock_gas:
|
|
233
|
+
mock_gas.return_value = 100000000000000000 # 0.1 ETH
|
|
226
234
|
with patch.object(
|
|
227
|
-
strategy, "
|
|
228
|
-
) as
|
|
229
|
-
|
|
235
|
+
strategy, "get_peg_diff", new_callable=AsyncMock
|
|
236
|
+
) as mock_peg:
|
|
237
|
+
mock_peg.return_value = 0.001
|
|
230
238
|
with patch.object(
|
|
231
|
-
strategy, "
|
|
232
|
-
) as
|
|
233
|
-
|
|
234
|
-
with patch.object(
|
|
235
|
-
strategy, "quote", new_callable=AsyncMock
|
|
236
|
-
) as mock_quote:
|
|
237
|
-
mock_quote.return_value = {"apy": 0.1, "data": {}}
|
|
239
|
+
strategy, "quote", new_callable=AsyncMock
|
|
240
|
+
) as mock_quote:
|
|
241
|
+
mock_quote.return_value = {"apy": 0.1, "data": {}}
|
|
238
242
|
|
|
239
|
-
|
|
243
|
+
status = await strategy._status()
|
|
240
244
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
245
|
+
assert "portfolio_value" in status
|
|
246
|
+
assert "net_deposit" in status
|
|
247
|
+
assert "strategy_status" in status
|
|
248
|
+
assert "gas_available" in status
|
|
249
|
+
assert "gassed_up" in status
|
|
246
250
|
|
|
247
251
|
|
|
248
252
|
@pytest.mark.asyncio
|
|
@@ -417,93 +421,181 @@ async def test_swap_with_retries_aborts_on_unknown_outcome(
|
|
|
417
421
|
|
|
418
422
|
|
|
419
423
|
@pytest.mark.asyncio
|
|
420
|
-
async def
|
|
421
|
-
|
|
422
|
-
)
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
# Mock prices: wstETH = $2000, WETH = $2000 (so 2 wstETH > 1 WETH debt)
|
|
436
|
-
strategy.token_adapter.get_token_price = AsyncMock(
|
|
437
|
-
return_value=(True, {"current_price": 2000.0})
|
|
438
|
-
)
|
|
439
|
-
|
|
440
|
-
success, msg = await strategy._balance_weth_debt()
|
|
424
|
+
async def test_post_run_guard_no_action_when_delta_ok(strategy, mock_adapter_responses):
|
|
425
|
+
"""Post-run guard should do nothing when delta is within tolerance."""
|
|
426
|
+
snap = MagicMock()
|
|
427
|
+
snap.wallet_wsteth = 0
|
|
428
|
+
snap.usdc_supplied = 0
|
|
429
|
+
snap.wsteth_supplied = 0
|
|
430
|
+
snap.weth_debt = 5 * 10**18
|
|
431
|
+
snap.debt_usd = 100.0
|
|
432
|
+
snap.hf = 1.3
|
|
433
|
+
snap.capacity_usd = 1000.0
|
|
434
|
+
snap.totals_usd = {
|
|
435
|
+
f"Base_{M_WSTETH}": 96.0,
|
|
436
|
+
f"Base_{M_USDC}": 0.0,
|
|
437
|
+
f"Base_{WETH}": -100.0,
|
|
438
|
+
}
|
|
441
439
|
|
|
442
|
-
|
|
443
|
-
|
|
440
|
+
with patch.object(strategy, "_get_gas_balance", new_callable=AsyncMock) as mock_gas:
|
|
441
|
+
mock_gas.return_value = int(0.01 * 10**18)
|
|
442
|
+
with patch.object(
|
|
443
|
+
strategy, "_get_collateral_factors", new_callable=AsyncMock
|
|
444
|
+
) as mock_cf:
|
|
445
|
+
mock_cf.return_value = (0.8, 0.8)
|
|
446
|
+
with patch.object(
|
|
447
|
+
strategy, "_accounting_snapshot", new_callable=AsyncMock
|
|
448
|
+
) as mock_snap:
|
|
449
|
+
mock_snap.return_value = (snap, (0.8, 0.8))
|
|
450
|
+
with patch.object(
|
|
451
|
+
strategy, "_ensure_markets_for_state", new_callable=AsyncMock
|
|
452
|
+
) as mock_markets:
|
|
453
|
+
mock_markets.return_value = (True, "ok")
|
|
454
|
+
with patch.object(
|
|
455
|
+
strategy,
|
|
456
|
+
"_reconcile_wallet_into_position",
|
|
457
|
+
new_callable=AsyncMock,
|
|
458
|
+
) as mock_reconcile:
|
|
459
|
+
with patch.object(
|
|
460
|
+
strategy,
|
|
461
|
+
"_settle_weth_debt_to_target_usd",
|
|
462
|
+
new_callable=AsyncMock,
|
|
463
|
+
) as mock_settle:
|
|
464
|
+
ok, msg = await strategy._post_run_guard(mode="operate")
|
|
465
|
+
assert ok is True
|
|
466
|
+
assert "delta ok" in msg.lower()
|
|
467
|
+
assert not mock_reconcile.called
|
|
468
|
+
assert not mock_settle.called
|
|
444
469
|
|
|
445
470
|
|
|
446
471
|
@pytest.mark.asyncio
|
|
447
|
-
async def
|
|
472
|
+
async def test_post_run_guard_restores_delta_via_reconcile(
|
|
448
473
|
strategy, mock_adapter_responses
|
|
449
474
|
):
|
|
450
|
-
"""
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
475
|
+
"""Post-run guard should attempt wallet reconcile in operate mode when net short."""
|
|
476
|
+
snap1 = MagicMock()
|
|
477
|
+
snap1.wallet_wsteth = 0
|
|
478
|
+
snap1.usdc_supplied = 0
|
|
479
|
+
snap1.wsteth_supplied = 0
|
|
480
|
+
snap1.weth_debt = 5 * 10**18
|
|
481
|
+
snap1.debt_usd = 100.0
|
|
482
|
+
snap1.hf = 1.3
|
|
483
|
+
snap1.capacity_usd = 1000.0
|
|
484
|
+
snap1.totals_usd = {f"Base_{M_WSTETH}": 0.0, f"Base_{WETH}": -100.0}
|
|
485
|
+
|
|
486
|
+
snap2 = MagicMock()
|
|
487
|
+
snap2.wallet_wsteth = 0
|
|
488
|
+
snap2.usdc_supplied = 0
|
|
489
|
+
snap2.wsteth_supplied = 0
|
|
490
|
+
snap2.weth_debt = 5 * 10**18
|
|
491
|
+
snap2.debt_usd = 100.0
|
|
492
|
+
snap2.hf = 1.3
|
|
493
|
+
snap2.capacity_usd = 1000.0
|
|
494
|
+
snap2.totals_usd = {f"Base_{M_WSTETH}": 98.0, f"Base_{WETH}": -100.0}
|
|
495
|
+
|
|
496
|
+
with patch.object(strategy, "_get_gas_balance", new_callable=AsyncMock) as mock_gas:
|
|
497
|
+
mock_gas.return_value = int(0.01 * 10**18)
|
|
498
|
+
with patch.object(
|
|
499
|
+
strategy, "_get_collateral_factors", new_callable=AsyncMock
|
|
500
|
+
) as mock_cf:
|
|
501
|
+
mock_cf.return_value = (0.8, 0.8)
|
|
502
|
+
with patch.object(
|
|
503
|
+
strategy, "_accounting_snapshot", new_callable=AsyncMock
|
|
504
|
+
) as mock_snap:
|
|
505
|
+
mock_snap.side_effect = [(snap1, (0.8, 0.8)), (snap2, (0.8, 0.8))]
|
|
506
|
+
with patch.object(
|
|
507
|
+
strategy, "_ensure_markets_for_state", new_callable=AsyncMock
|
|
508
|
+
) as mock_markets:
|
|
509
|
+
mock_markets.return_value = (True, "ok")
|
|
510
|
+
with patch.object(
|
|
511
|
+
strategy,
|
|
512
|
+
"_reconcile_wallet_into_position",
|
|
513
|
+
new_callable=AsyncMock,
|
|
514
|
+
) as mock_reconcile:
|
|
515
|
+
mock_reconcile.return_value = (True, "ok")
|
|
516
|
+
with patch.object(
|
|
517
|
+
strategy,
|
|
518
|
+
"_settle_weth_debt_to_target_usd",
|
|
519
|
+
new_callable=AsyncMock,
|
|
520
|
+
) as mock_settle:
|
|
521
|
+
ok, msg = await strategy._post_run_guard(mode="operate")
|
|
522
|
+
assert ok is True
|
|
523
|
+
assert "restored by reconcile" in msg.lower()
|
|
524
|
+
assert mock_reconcile.called
|
|
525
|
+
assert not mock_settle.called
|
|
478
526
|
|
|
479
527
|
|
|
480
528
|
@pytest.mark.asyncio
|
|
481
|
-
async def
|
|
529
|
+
async def test_post_run_guard_delevers_to_delta_when_reconcile_insufficient(
|
|
482
530
|
strategy, mock_adapter_responses
|
|
483
531
|
):
|
|
484
|
-
"""
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
532
|
+
"""Post-run guard should delever debt when still net short after reconcile."""
|
|
533
|
+
snap1 = MagicMock()
|
|
534
|
+
snap1.wallet_wsteth = 0
|
|
535
|
+
snap1.usdc_supplied = 0
|
|
536
|
+
snap1.wsteth_supplied = 0
|
|
537
|
+
snap1.weth_debt = 5 * 10**18
|
|
538
|
+
snap1.debt_usd = 100.0
|
|
539
|
+
snap1.hf = 1.3
|
|
540
|
+
snap1.capacity_usd = 1000.0
|
|
541
|
+
snap1.totals_usd = {f"Base_{M_WSTETH}": 0.0, f"Base_{WETH}": -100.0}
|
|
542
|
+
|
|
543
|
+
snap2 = MagicMock()
|
|
544
|
+
snap2.wallet_wsteth = 0
|
|
545
|
+
snap2.usdc_supplied = 0
|
|
546
|
+
snap2.wsteth_supplied = 0
|
|
547
|
+
snap2.weth_debt = 5 * 10**18
|
|
548
|
+
snap2.debt_usd = 100.0
|
|
549
|
+
snap2.hf = 1.3
|
|
550
|
+
snap2.capacity_usd = 1000.0
|
|
551
|
+
snap2.totals_usd = {f"Base_{M_WSTETH}": 0.0, f"Base_{WETH}": -100.0}
|
|
552
|
+
|
|
553
|
+
snap3 = MagicMock()
|
|
554
|
+
snap3.wallet_wsteth = 0
|
|
555
|
+
snap3.usdc_supplied = 0
|
|
556
|
+
snap3.wsteth_supplied = 0
|
|
557
|
+
snap3.weth_debt = int(0.005 * 10**18) # small debt
|
|
558
|
+
snap3.debt_usd = 12.0
|
|
559
|
+
snap3.hf = 1.3
|
|
560
|
+
snap3.capacity_usd = 1000.0
|
|
561
|
+
# mismatch = debt_usd - wsteth_coll_usd = 12 - 15 = -3 (within DELTA_TOL_USD of 5)
|
|
562
|
+
snap3.totals_usd = {f"Base_{M_WSTETH}": 15.0, f"Base_{WETH}": -12.0}
|
|
563
|
+
|
|
564
|
+
with patch.object(strategy, "_get_gas_balance", new_callable=AsyncMock) as mock_gas:
|
|
565
|
+
mock_gas.return_value = int(0.01 * 10**18)
|
|
566
|
+
with patch.object(
|
|
567
|
+
strategy, "_get_collateral_factors", new_callable=AsyncMock
|
|
568
|
+
) as mock_cf:
|
|
569
|
+
mock_cf.return_value = (0.8, 0.8)
|
|
570
|
+
with patch.object(
|
|
571
|
+
strategy, "_accounting_snapshot", new_callable=AsyncMock
|
|
572
|
+
) as mock_snap:
|
|
573
|
+
mock_snap.side_effect = [
|
|
574
|
+
(snap1, (0.8, 0.8)),
|
|
575
|
+
(snap2, (0.8, 0.8)),
|
|
576
|
+
(snap3, (0.8, 0.8)),
|
|
577
|
+
]
|
|
578
|
+
with patch.object(
|
|
579
|
+
strategy, "_ensure_markets_for_state", new_callable=AsyncMock
|
|
580
|
+
) as mock_markets:
|
|
581
|
+
mock_markets.return_value = (True, "ok")
|
|
582
|
+
with patch.object(
|
|
583
|
+
strategy,
|
|
584
|
+
"_reconcile_wallet_into_position",
|
|
585
|
+
new_callable=AsyncMock,
|
|
586
|
+
) as mock_reconcile:
|
|
587
|
+
mock_reconcile.return_value = (True, "ok")
|
|
588
|
+
with patch.object(
|
|
589
|
+
strategy,
|
|
590
|
+
"_settle_weth_debt_to_target_usd",
|
|
591
|
+
new_callable=AsyncMock,
|
|
592
|
+
) as mock_settle:
|
|
593
|
+
mock_settle.return_value = (True, "ok")
|
|
594
|
+
ok, msg = await strategy._post_run_guard(mode="operate")
|
|
595
|
+
assert ok is True
|
|
596
|
+
assert "delta ok" in msg.lower()
|
|
597
|
+
assert mock_reconcile.called
|
|
598
|
+
assert mock_settle.called
|
|
507
599
|
|
|
508
600
|
|
|
509
601
|
@pytest.mark.asyncio
|
|
@@ -569,17 +661,21 @@ async def test_atomic_deposit_iteration_swaps_from_eth_when_borrow_surfaces_as_e
|
|
|
569
661
|
|
|
570
662
|
|
|
571
663
|
@pytest.mark.asyncio
|
|
572
|
-
async def
|
|
664
|
+
async def test_reconcile_wallet_into_position_uses_eth_inventory(
|
|
573
665
|
strategy, mock_adapter_responses
|
|
574
666
|
):
|
|
575
667
|
"""If debt exists but wstETH collateral is missing, use wallet ETH to complete the loop."""
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
(True, {"underlying_balance": 0})
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
668
|
+
|
|
669
|
+
async def get_pos_side_effect(*, mtoken: str):
|
|
670
|
+
if mtoken == M_USDC:
|
|
671
|
+
return (True, {"underlying_balance": 0, "borrow_balance": 0})
|
|
672
|
+
if mtoken == M_WSTETH:
|
|
673
|
+
return (True, {"underlying_balance": 0, "borrow_balance": 0})
|
|
674
|
+
if mtoken == M_WETH:
|
|
675
|
+
return (True, {"underlying_balance": 0, "borrow_balance": 5 * 10**18})
|
|
676
|
+
return (True, {})
|
|
677
|
+
|
|
678
|
+
strategy.moonwell_adapter.get_pos = AsyncMock(side_effect=get_pos_side_effect)
|
|
583
679
|
|
|
584
680
|
balances: dict[str, int] = {
|
|
585
681
|
ETH_TOKEN_ID: 10 * 10**18,
|
|
@@ -604,10 +700,13 @@ async def test_complete_unpaired_weth_borrow_uses_eth_inventory(
|
|
|
604
700
|
|
|
605
701
|
strategy._swap_with_retries = AsyncMock(side_effect=swap_side_effect)
|
|
606
702
|
|
|
607
|
-
success, msg = await strategy.
|
|
703
|
+
success, msg = await strategy._reconcile_wallet_into_position(
|
|
704
|
+
collateral_factors=(0.8, 0.8),
|
|
705
|
+
max_batch_usd=100000.0,
|
|
706
|
+
)
|
|
608
707
|
|
|
609
708
|
assert success is True
|
|
610
|
-
assert
|
|
709
|
+
assert strategy._swap_with_retries.called
|
|
611
710
|
strategy.moonwell_adapter.lend.assert_called()
|
|
612
711
|
|
|
613
712
|
|
|
@@ -691,6 +790,62 @@ def test_min_leverage_gain_constant_exists(strategy):
|
|
|
691
790
|
assert strategy._MIN_LEVERAGE_GAIN_BPS == 50e-4 # 50 bps
|
|
692
791
|
|
|
693
792
|
|
|
793
|
+
@pytest.mark.asyncio
|
|
794
|
+
async def test_update_runs_post_run_guard(strategy, mock_adapter_responses):
|
|
795
|
+
with patch.object(strategy, "_update_impl", new_callable=AsyncMock) as mock_impl:
|
|
796
|
+
with patch.object(
|
|
797
|
+
strategy, "_post_run_guard", new_callable=AsyncMock
|
|
798
|
+
) as mock_guard:
|
|
799
|
+
mock_impl.return_value = (True, "ok")
|
|
800
|
+
mock_guard.return_value = (True, "guard ok")
|
|
801
|
+
|
|
802
|
+
ok, msg = await strategy.update()
|
|
803
|
+
|
|
804
|
+
assert ok is True
|
|
805
|
+
assert "finalizer:" in msg
|
|
806
|
+
mock_guard.assert_awaited()
|
|
807
|
+
assert mock_guard.call_args.kwargs.get("mode") == "operate"
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
@pytest.mark.asyncio
|
|
811
|
+
async def test_update_fails_if_post_run_guard_fails(strategy, mock_adapter_responses):
|
|
812
|
+
with patch.object(strategy, "_update_impl", new_callable=AsyncMock) as mock_impl:
|
|
813
|
+
with patch.object(
|
|
814
|
+
strategy, "_post_run_guard", new_callable=AsyncMock
|
|
815
|
+
) as mock_guard:
|
|
816
|
+
mock_impl.return_value = (True, "ok")
|
|
817
|
+
mock_guard.return_value = (False, "guard failed")
|
|
818
|
+
|
|
819
|
+
ok, msg = await strategy.update()
|
|
820
|
+
|
|
821
|
+
assert ok is False
|
|
822
|
+
assert "finalizer failed" in msg.lower()
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
@pytest.mark.asyncio
|
|
826
|
+
async def test_withdraw_runs_post_run_guard_only_on_failure(
|
|
827
|
+
strategy, mock_adapter_responses
|
|
828
|
+
):
|
|
829
|
+
with patch.object(strategy, "_withdraw_impl", new_callable=AsyncMock) as mock_impl:
|
|
830
|
+
with patch.object(
|
|
831
|
+
strategy, "_post_run_guard", new_callable=AsyncMock
|
|
832
|
+
) as mock_guard:
|
|
833
|
+
mock_impl.return_value = (True, "ok")
|
|
834
|
+
mock_guard.return_value = (True, "guard ok")
|
|
835
|
+
|
|
836
|
+
ok, _ = await strategy.withdraw()
|
|
837
|
+
|
|
838
|
+
assert ok is True
|
|
839
|
+
mock_guard.assert_not_awaited()
|
|
840
|
+
|
|
841
|
+
mock_impl.return_value = (False, "failed")
|
|
842
|
+
ok, msg = await strategy.withdraw()
|
|
843
|
+
assert ok is False
|
|
844
|
+
assert "finalizer:" in msg
|
|
845
|
+
mock_guard.assert_awaited()
|
|
846
|
+
assert mock_guard.call_args.kwargs.get("mode") == "exit"
|
|
847
|
+
|
|
848
|
+
|
|
694
849
|
@pytest.mark.asyncio
|
|
695
850
|
async def test_leverage_calc_handles_high_cf_w(strategy, mock_adapter_responses):
|
|
696
851
|
"""Test that leverage calculation handles high collateral factors safely."""
|
|
@@ -762,7 +917,13 @@ async def test_partial_liquidate_prefers_wsteth_when_excess(strategy):
|
|
|
762
917
|
f"Base_{M_USDC}": 1000.0,
|
|
763
918
|
f"Base_{WETH}": -200.0,
|
|
764
919
|
}
|
|
765
|
-
|
|
920
|
+
snap = MagicMock()
|
|
921
|
+
snap.totals_usd = totals_usd
|
|
922
|
+
snap.debt_usd = 200.0
|
|
923
|
+
snap.wsteth_price = 2000.0
|
|
924
|
+
snap.wsteth_dec = 18
|
|
925
|
+
|
|
926
|
+
strategy._accounting_snapshot = AsyncMock(return_value=(snap, (0.8, 0.8)))
|
|
766
927
|
|
|
767
928
|
# Collateral factors
|
|
768
929
|
strategy.moonwell_adapter.get_collateral_factor = AsyncMock(
|
|
@@ -849,7 +1010,13 @@ async def test_partial_liquidate_uses_usdc_collateral_when_no_wsteth_excess(stra
|
|
|
849
1010
|
f"Base_{M_USDC}": 500.0,
|
|
850
1011
|
f"Base_{WETH}": -200.0,
|
|
851
1012
|
}
|
|
852
|
-
|
|
1013
|
+
snap = MagicMock()
|
|
1014
|
+
snap.totals_usd = totals_usd
|
|
1015
|
+
snap.debt_usd = 200.0
|
|
1016
|
+
snap.usdc_price = 1.0
|
|
1017
|
+
snap.usdc_dec = 6
|
|
1018
|
+
|
|
1019
|
+
strategy._accounting_snapshot = AsyncMock(return_value=(snap, (0.8, 0.8)))
|
|
853
1020
|
|
|
854
1021
|
strategy.moonwell_adapter.get_collateral_factor = AsyncMock(
|
|
855
1022
|
return_value=(True, 0.8)
|
|
@@ -62,7 +62,7 @@ Transactions are scoped to the strategy wallet and Enso Router approval/swap cal
|
|
|
62
62
|
### Withdraw
|
|
63
63
|
|
|
64
64
|
- Requires a prior deposit (the strategy tracks `self.DEPOSIT_USDC`).
|
|
65
|
-
- Reads the pool balance via `BalanceAdapter.
|
|
65
|
+
- Reads the pool balance via `BalanceAdapter.get_balance` (with pool address and chain_id), unwinds via BRAP swaps back to USDC, and moves USDC from the strategy wallet to the main wallet via `BalanceAdapter.move_from_strategy_wallet_to_main_wallet`.
|
|
66
66
|
- Updates the ledger and clears cached pool state.
|
|
67
67
|
|
|
68
68
|
## Running locally
|