wayfinder-paths 0.1.6__py3-none-any.whl → 0.1.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of wayfinder-paths might be problematic. Click here for more details.
- wayfinder_paths/adapters/balance_adapter/README.md +0 -10
- wayfinder_paths/adapters/balance_adapter/adapter.py +0 -20
- wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -30
- wayfinder_paths/adapters/brap_adapter/adapter.py +3 -2
- wayfinder_paths/adapters/brap_adapter/test_adapter.py +9 -13
- wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +14 -7
- wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +18 -0
- wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1093 -0
- wayfinder_paths/adapters/hyperliquid_adapter/executor.py +549 -0
- wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +8 -0
- wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +1050 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +126 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +219 -0
- wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +220 -0
- wayfinder_paths/adapters/hyperliquid_adapter/utils.py +134 -0
- wayfinder_paths/adapters/ledger_adapter/test_adapter.py +7 -6
- wayfinder_paths/adapters/pool_adapter/README.md +3 -28
- wayfinder_paths/adapters/pool_adapter/adapter.py +0 -72
- wayfinder_paths/adapters/pool_adapter/examples.json +0 -43
- wayfinder_paths/adapters/pool_adapter/test_adapter.py +4 -54
- wayfinder_paths/adapters/token_adapter/test_adapter.py +4 -14
- wayfinder_paths/core/adapters/models.py +9 -4
- wayfinder_paths/core/analytics/__init__.py +11 -0
- wayfinder_paths/core/analytics/bootstrap.py +57 -0
- wayfinder_paths/core/analytics/stats.py +48 -0
- wayfinder_paths/core/analytics/test_analytics.py +170 -0
- wayfinder_paths/core/clients/BRAPClient.py +1 -0
- wayfinder_paths/core/clients/LedgerClient.py +2 -7
- wayfinder_paths/core/clients/PoolClient.py +0 -16
- wayfinder_paths/core/clients/WalletClient.py +0 -27
- wayfinder_paths/core/clients/protocols.py +104 -18
- wayfinder_paths/scripts/make_wallets.py +9 -0
- wayfinder_paths/scripts/run_strategy.py +124 -0
- wayfinder_paths/strategies/basis_trading_strategy/README.md +213 -0
- wayfinder_paths/strategies/basis_trading_strategy/__init__.py +3 -0
- wayfinder_paths/strategies/basis_trading_strategy/constants.py +1 -0
- wayfinder_paths/strategies/basis_trading_strategy/examples.json +16 -0
- wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +23 -0
- wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1011 -0
- wayfinder_paths/strategies/basis_trading_strategy/strategy.py +4522 -0
- wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +727 -0
- wayfinder_paths/strategies/basis_trading_strategy/types.py +39 -0
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +1 -9
- wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +36 -5
- wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +367 -278
- wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +204 -7
- {wayfinder_paths-0.1.6.dist-info → wayfinder_paths-0.1.8.dist-info}/METADATA +32 -3
- {wayfinder_paths-0.1.6.dist-info → wayfinder_paths-0.1.8.dist-info}/RECORD +50 -27
- {wayfinder_paths-0.1.6.dist-info → wayfinder_paths-0.1.8.dist-info}/LICENSE +0 -0
- {wayfinder_paths-0.1.6.dist-info → wayfinder_paths-0.1.8.dist-info}/WHEEL +0 -0
|
@@ -179,6 +179,10 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
179
179
|
self.pool_adapter = None
|
|
180
180
|
self.brap_adapter = None
|
|
181
181
|
|
|
182
|
+
# State tracking for deterministic token management
|
|
183
|
+
self.tracked_token_ids: set[str] = set() # All tokens strategy might hold
|
|
184
|
+
self.tracked_balances: dict[str, int] = {} # token_id -> balance in wei
|
|
185
|
+
|
|
182
186
|
try:
|
|
183
187
|
main_wallet_cfg = self.config.get("main_wallet")
|
|
184
188
|
strategy_wallet_cfg = self.config.get("strategy_wallet")
|
|
@@ -251,6 +255,45 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
251
255
|
raise ValueError("main_wallet address not found in config")
|
|
252
256
|
return str(address)
|
|
253
257
|
|
|
258
|
+
def _track_token(self, token_id: str, balance_wei: int = 0):
|
|
259
|
+
"""Track a token that the strategy holds or might hold."""
|
|
260
|
+
if token_id:
|
|
261
|
+
self.tracked_token_ids.add(token_id)
|
|
262
|
+
if balance_wei > 0:
|
|
263
|
+
self.tracked_balances[token_id] = balance_wei
|
|
264
|
+
|
|
265
|
+
def _update_balance(self, token_id: str, balance_wei: int):
|
|
266
|
+
"""Update the tracked balance for a token."""
|
|
267
|
+
if token_id:
|
|
268
|
+
self.tracked_balances[token_id] = balance_wei
|
|
269
|
+
if balance_wei > 0:
|
|
270
|
+
self.tracked_token_ids.add(token_id)
|
|
271
|
+
|
|
272
|
+
async def _refresh_tracked_balances(self):
|
|
273
|
+
"""Refresh balances for all tracked tokens from on-chain data."""
|
|
274
|
+
strategy_address = self._get_strategy_wallet_address()
|
|
275
|
+
for token_id in self.tracked_token_ids:
|
|
276
|
+
try:
|
|
277
|
+
success, balance_wei = await self.balance_adapter.get_balance(
|
|
278
|
+
token_id=token_id,
|
|
279
|
+
wallet_address=strategy_address,
|
|
280
|
+
)
|
|
281
|
+
if success and balance_wei:
|
|
282
|
+
self.tracked_balances[token_id] = int(balance_wei)
|
|
283
|
+
else:
|
|
284
|
+
self.tracked_balances[token_id] = 0
|
|
285
|
+
except Exception as e:
|
|
286
|
+
logger.warning(f"Failed to refresh balance for {token_id}: {e}")
|
|
287
|
+
self.tracked_balances[token_id] = 0
|
|
288
|
+
|
|
289
|
+
def _get_non_zero_tracked_tokens(self) -> list[tuple[str, int]]:
|
|
290
|
+
"""Get list of (token_id, balance_wei) for tokens with non-zero balances."""
|
|
291
|
+
return [
|
|
292
|
+
(token_id, balance)
|
|
293
|
+
for token_id, balance in self.tracked_balances.items()
|
|
294
|
+
if balance > 0
|
|
295
|
+
]
|
|
296
|
+
|
|
254
297
|
async def setup(self):
|
|
255
298
|
logger.info("Starting StablecoinYieldStrategy setup")
|
|
256
299
|
start_time = time.time()
|
|
@@ -292,6 +335,10 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
292
335
|
logger.error(f"Error fetching USDC token info: {e}")
|
|
293
336
|
self.usdc_token_info = {}
|
|
294
337
|
|
|
338
|
+
# Always track USDC as baseline token
|
|
339
|
+
if self.usdc_token_info.get("token_id"):
|
|
340
|
+
self._track_token(self.usdc_token_info.get("token_id"))
|
|
341
|
+
|
|
295
342
|
self.current_pool = {
|
|
296
343
|
"token_id": self.usdc_token_info.get("token_id"),
|
|
297
344
|
"name": self.usdc_token_info.get("name"),
|
|
@@ -316,6 +363,9 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
316
363
|
logger.info(
|
|
317
364
|
f"Gas token loaded: {gas_token_data.get('symbol', 'Unknown')}"
|
|
318
365
|
)
|
|
366
|
+
# Track gas token (but don't count it as a strategy asset)
|
|
367
|
+
if self.gas_token.get("id"):
|
|
368
|
+
self._track_token(self.gas_token.get("id"))
|
|
319
369
|
else:
|
|
320
370
|
logger.warning("Failed to fetch gas token info, using empty dict")
|
|
321
371
|
self.gas_token = {}
|
|
@@ -328,9 +378,9 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
328
378
|
self.current_pool_balance = 0
|
|
329
379
|
return
|
|
330
380
|
|
|
331
|
-
# Get strategy transactions to determine current position
|
|
381
|
+
# Get strategy transactions to determine current position and build tracked token set
|
|
332
382
|
try:
|
|
333
|
-
logger.info("Fetching strategy transaction history")
|
|
383
|
+
logger.info("Fetching strategy transaction history to build state")
|
|
334
384
|
success, txns_data = await self.ledger_adapter.get_strategy_transactions(
|
|
335
385
|
wallet_address=self._get_strategy_wallet_address(),
|
|
336
386
|
)
|
|
@@ -341,6 +391,20 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
341
391
|
if txn.get("operation") != "DEPOSIT"
|
|
342
392
|
]
|
|
343
393
|
logger.info(f"Found {len(txns)} non-deposit transactions")
|
|
394
|
+
|
|
395
|
+
# Build tracked token set from transaction history
|
|
396
|
+
for txn in txns:
|
|
397
|
+
op_data = txn.get("data", {}).get("op_data", {})
|
|
398
|
+
# Track any token that was swapped TO
|
|
399
|
+
if op_data.get("to_token_id"):
|
|
400
|
+
self._track_token(op_data.get("to_token_id"))
|
|
401
|
+
# Track any token that was swapped FROM
|
|
402
|
+
if op_data.get("from_token_id"):
|
|
403
|
+
self._track_token(op_data.get("from_token_id"))
|
|
404
|
+
|
|
405
|
+
logger.info(
|
|
406
|
+
f"Tracking {len(self.tracked_token_ids)} tokens from history"
|
|
407
|
+
)
|
|
344
408
|
else:
|
|
345
409
|
logger.error(f"Failed to fetch strategy transactions: {txns_data}")
|
|
346
410
|
txns = []
|
|
@@ -363,6 +427,9 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
363
427
|
"address": token_info.get("address"),
|
|
364
428
|
"chain": token_info.get("chain"),
|
|
365
429
|
}
|
|
430
|
+
# Track the current pool token
|
|
431
|
+
if token_info.get("token_id"):
|
|
432
|
+
self._track_token(token_info.get("token_id"))
|
|
366
433
|
|
|
367
434
|
success, reports = await self.pool_adapter.get_pools_by_ids(
|
|
368
435
|
pool_ids=[self.current_pool.get("token_id")],
|
|
@@ -442,6 +509,12 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
442
509
|
== self.current_pool.get("chain").get("id")
|
|
443
510
|
else None
|
|
444
511
|
)
|
|
512
|
+
# Refresh all tracked balances from blockchain
|
|
513
|
+
await self._refresh_tracked_balances()
|
|
514
|
+
logger.info(
|
|
515
|
+
f"Refreshed balances for {len(self.tracked_balances)} tracked tokens"
|
|
516
|
+
)
|
|
517
|
+
|
|
445
518
|
if (
|
|
446
519
|
baseline_token
|
|
447
520
|
and self.current_pool.get("token_id") != baseline_token.get("token_id")
|
|
@@ -449,7 +522,8 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
449
522
|
):
|
|
450
523
|
return
|
|
451
524
|
|
|
452
|
-
|
|
525
|
+
# Fallback: Try to infer active pool from tracked tokens with balances
|
|
526
|
+
inferred = await self._infer_active_pool_from_tracked_tokens()
|
|
453
527
|
if inferred is not None:
|
|
454
528
|
inferred_token, inferred_balance, inferred_entry = inferred
|
|
455
529
|
self.current_pool = inferred_token
|
|
@@ -502,95 +576,60 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
502
576
|
continue
|
|
503
577
|
return total_usd
|
|
504
578
|
|
|
505
|
-
async def
|
|
579
|
+
async def _infer_active_pool_from_tracked_tokens(self):
|
|
580
|
+
"""Infer the active pool from tracked tokens with non-zero balances."""
|
|
506
581
|
try:
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
wallet_balances,
|
|
510
|
-
) = await self.balance_adapter.get_all_balances(
|
|
511
|
-
wallet_address=self._get_strategy_wallet_address(),
|
|
512
|
-
enrich=True,
|
|
513
|
-
from_cache=False,
|
|
514
|
-
add_llama=True,
|
|
515
|
-
)
|
|
516
|
-
except Exception:
|
|
517
|
-
return None
|
|
582
|
+
# Refresh balances for tracked tokens
|
|
583
|
+
await self._refresh_tracked_balances()
|
|
518
584
|
|
|
519
|
-
|
|
520
|
-
|
|
585
|
+
usdc_token_id = self.usdc_token_info.get("token_id")
|
|
586
|
+
gas_token_id = self.gas_token.get("id") if self.gas_token else None
|
|
521
587
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
self.usdc_token_info
|
|
525
|
-
if self.usdc_token_info.get("chain").get("name").lower() == target_chain
|
|
526
|
-
else None
|
|
527
|
-
)
|
|
528
|
-
usdc_address = usdc_token.get("address").lower() if usdc_token else ""
|
|
588
|
+
best_token_id = None
|
|
589
|
+
best_balance_wei = 0
|
|
529
590
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
591
|
+
# Find the non-gas, non-USDC token with the largest balance
|
|
592
|
+
for token_id, balance_wei in self.tracked_balances.items():
|
|
593
|
+
if balance_wei <= 0:
|
|
594
|
+
continue
|
|
595
|
+
if token_id == gas_token_id:
|
|
596
|
+
continue
|
|
597
|
+
if token_id == usdc_token_id:
|
|
598
|
+
continue
|
|
533
599
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
600
|
+
# Prefer tokens with larger balances
|
|
601
|
+
if balance_wei > best_balance_wei:
|
|
602
|
+
best_token_id = token_id
|
|
603
|
+
best_balance_wei = balance_wei
|
|
537
604
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
continue
|
|
605
|
+
if not best_token_id:
|
|
606
|
+
return None
|
|
541
607
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
608
|
+
# Fetch token info
|
|
609
|
+
success, token = await self.token_adapter.get_token(best_token_id)
|
|
610
|
+
if not success:
|
|
611
|
+
return None
|
|
545
612
|
|
|
546
|
-
|
|
613
|
+
# Get fresh on-chain balance
|
|
614
|
+
strategy_address = self._get_strategy_wallet_address()
|
|
547
615
|
try:
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
continue
|
|
557
|
-
|
|
558
|
-
if best_entry is None or balance_usd > best_balance_usd:
|
|
559
|
-
best_entry = balance
|
|
560
|
-
best_balance_wei = balance_wei
|
|
561
|
-
best_balance_usd = balance_usd
|
|
562
|
-
|
|
563
|
-
# TODO: no successful best entry found in manual testing
|
|
564
|
-
if not best_entry:
|
|
565
|
-
return None
|
|
566
|
-
|
|
567
|
-
token_id = best_entry.get("token_id")
|
|
568
|
-
token_address = best_entry.get("tokenAddress")
|
|
569
|
-
if not token_id and token_address:
|
|
570
|
-
token_id = f"{target_chain}_{token_address.lower()}"
|
|
571
|
-
|
|
572
|
-
if not token_id:
|
|
573
|
-
return None
|
|
574
|
-
|
|
575
|
-
success, token = await self.token_adapter.get_token(token_id)
|
|
576
|
-
if not success:
|
|
577
|
-
return None
|
|
616
|
+
success, onchain_balance = await self.balance_adapter.get_balance(
|
|
617
|
+
token_id=token.get("token_id"),
|
|
618
|
+
wallet_address=strategy_address,
|
|
619
|
+
)
|
|
620
|
+
if success and onchain_balance:
|
|
621
|
+
best_balance_wei = int(onchain_balance)
|
|
622
|
+
except Exception:
|
|
623
|
+
pass
|
|
578
624
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
(
|
|
582
|
-
success,
|
|
583
|
-
onchain_balance,
|
|
584
|
-
) = await self.balance_adapter.get_balance(
|
|
585
|
-
token_id=token.get("token_id"),
|
|
586
|
-
wallet_address=strategy_address,
|
|
625
|
+
logger.info(
|
|
626
|
+
f"Inferred active pool: {token.get('symbol')} with balance {best_balance_wei}"
|
|
587
627
|
)
|
|
588
|
-
|
|
589
|
-
onchain_balance = best_balance_wei
|
|
590
|
-
except Exception:
|
|
591
|
-
onchain_balance = best_balance_wei
|
|
628
|
+
return token, best_balance_wei, None
|
|
592
629
|
|
|
593
|
-
|
|
630
|
+
except Exception as e:
|
|
631
|
+
logger.error(f"Failed to infer active pool from tracked tokens: {e}")
|
|
632
|
+
return None
|
|
594
633
|
|
|
595
634
|
def _is_gas_balance_entry(self, balance: dict[str, Any]) -> bool:
|
|
596
635
|
"""Check if a balance entry represents a gas token."""
|
|
@@ -791,6 +830,13 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
791
830
|
return (False, f"USDC transfer to strategy failed: {msg}")
|
|
792
831
|
logger.info("USDC transfer completed successfully")
|
|
793
832
|
|
|
833
|
+
# Update tracked state
|
|
834
|
+
self._track_token(self.usdc_token_info.get("token_id"))
|
|
835
|
+
self._update_balance(
|
|
836
|
+
self.usdc_token_info.get("token_id"),
|
|
837
|
+
int(main_token_amount * (10 ** self.current_pool.get("decimals"))),
|
|
838
|
+
)
|
|
839
|
+
|
|
794
840
|
# Transfer gas if provided or if strategy needs top-up
|
|
795
841
|
if gas_token_amount > 0:
|
|
796
842
|
# Get gas symbol if not already defined
|
|
@@ -1149,6 +1195,10 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1149
1195
|
strategy_name=self.name,
|
|
1150
1196
|
)
|
|
1151
1197
|
|
|
1198
|
+
# Track the new target pool token
|
|
1199
|
+
if target_pool and target_pool.get("token_id"):
|
|
1200
|
+
self._track_token(target_pool.get("token_id"))
|
|
1201
|
+
|
|
1152
1202
|
self.current_pool = target_pool
|
|
1153
1203
|
if self.current_pool and self.current_pool.get("token_id"):
|
|
1154
1204
|
success, pool_reports = await self.pool_adapter.get_pools_by_ids(
|
|
@@ -1206,49 +1256,81 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1206
1256
|
except Exception:
|
|
1207
1257
|
pass
|
|
1208
1258
|
|
|
1209
|
-
# TODO untested
|
|
1210
1259
|
async def _sweep_wallet(self, target_token):
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
if self._is_gas_balance_entry(balance):
|
|
1260
|
+
"""Sweep all tracked non-target tokens into the target token."""
|
|
1261
|
+
# Refresh tracked balances
|
|
1262
|
+
await self._refresh_tracked_balances()
|
|
1263
|
+
|
|
1264
|
+
target_token_id = target_token.get("token_id")
|
|
1265
|
+
target_chain = target_token.get("chain").get("code", "").lower()
|
|
1266
|
+
target_address = target_token.get("address", "").lower()
|
|
1267
|
+
gas_token_id = self.gas_token.get("id") if self.gas_token else None
|
|
1268
|
+
|
|
1269
|
+
# Swap all non-target, non-gas tokens to the target
|
|
1270
|
+
for token_id, balance_wei in list(self.tracked_balances.items()):
|
|
1271
|
+
# Skip if no balance
|
|
1272
|
+
if balance_wei <= 0:
|
|
1225
1273
|
continue
|
|
1226
1274
|
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
network = balance.get("network", "").lower()
|
|
1230
|
-
|
|
1231
|
-
if not balance_raw or not token_address:
|
|
1275
|
+
# Skip gas token
|
|
1276
|
+
if token_id == gas_token_id:
|
|
1232
1277
|
continue
|
|
1233
|
-
|
|
1278
|
+
|
|
1279
|
+
# Skip if it's already the target token
|
|
1280
|
+
if token_id == target_token_id:
|
|
1234
1281
|
continue
|
|
1235
1282
|
|
|
1283
|
+
# Get fresh balance to ensure accuracy
|
|
1236
1284
|
try:
|
|
1237
|
-
|
|
1238
|
-
|
|
1285
|
+
success, fresh_balance = await self.balance_adapter.get_balance(
|
|
1286
|
+
token_id=token_id,
|
|
1287
|
+
wallet_address=self._get_strategy_wallet_address(),
|
|
1288
|
+
)
|
|
1289
|
+
if not success or not fresh_balance or int(fresh_balance) <= 0:
|
|
1290
|
+
self._update_balance(token_id, 0)
|
|
1291
|
+
continue
|
|
1292
|
+
|
|
1293
|
+
balance_wei = int(fresh_balance)
|
|
1294
|
+
except Exception:
|
|
1295
|
+
continue
|
|
1239
1296
|
|
|
1297
|
+
# Construct target token ID for swap
|
|
1298
|
+
target_token_id_for_swap = f"{target_chain}_{target_address}"
|
|
1299
|
+
|
|
1300
|
+
try:
|
|
1301
|
+
logger.info(
|
|
1302
|
+
f"Sweeping {token_id} (balance: {balance_wei}) to {target_token_id}"
|
|
1303
|
+
)
|
|
1240
1304
|
success, msg = await self.brap_adapter.swap_from_token_ids(
|
|
1241
|
-
|
|
1242
|
-
|
|
1305
|
+
token_id,
|
|
1306
|
+
target_token_id_for_swap,
|
|
1243
1307
|
self._get_strategy_wallet_address(),
|
|
1244
|
-
|
|
1308
|
+
str(balance_wei),
|
|
1245
1309
|
strategy_name=self.name,
|
|
1246
1310
|
)
|
|
1247
|
-
if
|
|
1248
|
-
|
|
1249
|
-
|
|
1311
|
+
if success:
|
|
1312
|
+
# Update tracked state: source token now has 0 balance
|
|
1313
|
+
self._update_balance(token_id, 0)
|
|
1314
|
+
logger.info(f"Successfully swept {token_id} to {target_token_id}")
|
|
1315
|
+
else:
|
|
1316
|
+
logger.warning(f"Failed to sweep {token_id}: {msg}")
|
|
1317
|
+
except Exception as e:
|
|
1318
|
+
logger.error(f"Error sweeping {token_id}: {e}")
|
|
1250
1319
|
continue
|
|
1251
1320
|
|
|
1321
|
+
# Track the target token
|
|
1322
|
+
self._track_token(target_token_id)
|
|
1323
|
+
# Refresh target token balance
|
|
1324
|
+
try:
|
|
1325
|
+
success, target_balance = await self.balance_adapter.get_balance(
|
|
1326
|
+
token_id=target_token_id,
|
|
1327
|
+
wallet_address=self._get_strategy_wallet_address(),
|
|
1328
|
+
)
|
|
1329
|
+
if success and target_balance:
|
|
1330
|
+
self._update_balance(target_token_id, int(target_balance))
|
|
1331
|
+
except Exception:
|
|
1332
|
+
pass
|
|
1333
|
+
|
|
1252
1334
|
async def _rebalance_gas(self, target_pool) -> tuple[bool, str]:
|
|
1253
1335
|
if self.gas_token.get("chain").get("id") != target_pool.get("chain").get("id"):
|
|
1254
1336
|
return False, "Unsupported chain for gas management."
|
|
@@ -1326,82 +1408,40 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1326
1408
|
)
|
|
1327
1409
|
|
|
1328
1410
|
async def _get_non_gas_balances(self) -> list[dict[str, Any]]:
|
|
1329
|
-
|
|
1411
|
+
"""Get non-gas balances from tracked tokens."""
|
|
1412
|
+
# Refresh tracked balances
|
|
1413
|
+
await self._refresh_tracked_balances()
|
|
1330
1414
|
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
status,
|
|
1334
|
-
balance_wei,
|
|
1335
|
-
) = await self.balance_adapter.get_balance(
|
|
1336
|
-
token_id=self.usdc_token_info.get("token_id"),
|
|
1337
|
-
wallet_address=self._get_strategy_wallet_address(),
|
|
1338
|
-
)
|
|
1339
|
-
if not status or not balance_wei:
|
|
1340
|
-
pass
|
|
1341
|
-
results.append(
|
|
1342
|
-
{
|
|
1343
|
-
"token_id": self.usdc_token_info.get("token_id"),
|
|
1344
|
-
"tokenAddress": self.usdc_token_info.get("address"),
|
|
1345
|
-
"network": self.usdc_token_info.get("chain").get("code").upper(),
|
|
1346
|
-
"_amount_wei": balance_wei,
|
|
1347
|
-
}
|
|
1348
|
-
)
|
|
1349
|
-
else:
|
|
1350
|
-
results.append(
|
|
1351
|
-
{
|
|
1352
|
-
"token_id": self.usdc_token_info.get("token_id"),
|
|
1353
|
-
"tokenAddress": self.usdc_token_info.get("address"),
|
|
1354
|
-
"network": self.usdc_token_info.get("chain").get("code").upper(),
|
|
1355
|
-
"_amount_wei": self.current_pool_balance
|
|
1356
|
-
* (10 ** self.usdc_token_info.get("decimals")),
|
|
1357
|
-
}
|
|
1358
|
-
)
|
|
1415
|
+
gas_token_id = self.gas_token.get("id") if self.gas_token else None
|
|
1416
|
+
results = []
|
|
1359
1417
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
) = await self.balance_adapter.get_all_balances(
|
|
1365
|
-
wallet_address=self._get_strategy_wallet_address(),
|
|
1366
|
-
enrich=True,
|
|
1367
|
-
from_cache=False,
|
|
1368
|
-
add_llama=True,
|
|
1369
|
-
)
|
|
1370
|
-
except Exception:
|
|
1371
|
-
wallet_balances = []
|
|
1418
|
+
for token_id, balance_wei in self.tracked_balances.items():
|
|
1419
|
+
# Skip gas token
|
|
1420
|
+
if token_id == gas_token_id:
|
|
1421
|
+
continue
|
|
1372
1422
|
|
|
1373
|
-
|
|
1374
|
-
if
|
|
1423
|
+
# Skip zero balances
|
|
1424
|
+
if balance_wei <= 0:
|
|
1375
1425
|
continue
|
|
1376
1426
|
|
|
1377
|
-
|
|
1427
|
+
# Fetch token info to get address and chain
|
|
1378
1428
|
try:
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
if amount_wei <= 0:
|
|
1383
|
-
continue
|
|
1429
|
+
success, token_info = await self.token_adapter.get_token(token_id)
|
|
1430
|
+
if not success or not token_info:
|
|
1431
|
+
continue
|
|
1384
1432
|
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1433
|
+
results.append(
|
|
1434
|
+
{
|
|
1435
|
+
"token_id": token_id,
|
|
1436
|
+
"tokenAddress": token_info.get("address"),
|
|
1437
|
+
"network": token_info.get("chain", {}).get("code", "").upper(),
|
|
1438
|
+
"_amount_wei": balance_wei,
|
|
1439
|
+
}
|
|
1440
|
+
)
|
|
1441
|
+
except Exception as e:
|
|
1442
|
+
logger.warning(f"Failed to get token info for {token_id}: {e}")
|
|
1388
1443
|
continue
|
|
1389
1444
|
|
|
1390
|
-
candidate = {
|
|
1391
|
-
"token_id": balance.get("token_id")
|
|
1392
|
-
or f"{network}_{token_address.lower()}",
|
|
1393
|
-
"tokenAddress": token_address,
|
|
1394
|
-
"network": network.upper(),
|
|
1395
|
-
"_amount_wei": amount_wei,
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
exists = any(
|
|
1399
|
-
entry.get("token_id").lower() == candidate.get("token_id").lower()
|
|
1400
|
-
for entry in results
|
|
1401
|
-
)
|
|
1402
|
-
if not exists:
|
|
1403
|
-
results.append(candidate)
|
|
1404
|
-
|
|
1405
1445
|
return results
|
|
1406
1446
|
|
|
1407
1447
|
async def _find_best_pool(self) -> tuple[bool, dict[str, Any]]:
|
|
@@ -1545,37 +1585,63 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1545
1585
|
return float(gas_price) * float(amount) / (10 ** token.get("decimals"))
|
|
1546
1586
|
|
|
1547
1587
|
async def _status(self) -> StatusDict:
|
|
1588
|
+
# Get ETH gas balance
|
|
1589
|
+
gas_success, gas_balance_wei = await self.balance_adapter.get_balance(
|
|
1590
|
+
token_id=self.gas_token.get("id"),
|
|
1591
|
+
wallet_address=self._get_strategy_wallet_address(),
|
|
1592
|
+
)
|
|
1593
|
+
gas_balance = (
|
|
1594
|
+
float(gas_balance_wei) / (10 ** self.gas_token.get("decimals"))
|
|
1595
|
+
if gas_success
|
|
1596
|
+
else 0.0
|
|
1597
|
+
)
|
|
1598
|
+
|
|
1548
1599
|
if not self.DEPOSIT_USDC:
|
|
1549
|
-
|
|
1550
|
-
_,
|
|
1551
|
-
wallet_balances,
|
|
1552
|
-
) = await self.balance_adapter.get_all_balances(
|
|
1553
|
-
wallet_address=self._get_main_wallet_address(),
|
|
1554
|
-
enrich=True,
|
|
1555
|
-
from_cache=False,
|
|
1556
|
-
add_llama=True,
|
|
1557
|
-
)
|
|
1558
|
-
total_value = self._sum_non_gas_balance_usd(wallet_balances.get("balances"))
|
|
1600
|
+
# No deposits recorded - report minimal status
|
|
1559
1601
|
status_payload = {
|
|
1560
|
-
"info": "No recorded strategy deposits
|
|
1561
|
-
"idle_usd":
|
|
1602
|
+
"info": "No recorded strategy deposits.",
|
|
1603
|
+
"idle_usd": 0.0,
|
|
1562
1604
|
}
|
|
1563
1605
|
|
|
1564
1606
|
return StatusDict(
|
|
1565
|
-
portfolio_value=
|
|
1607
|
+
portfolio_value=0.0,
|
|
1566
1608
|
net_deposit=0,
|
|
1567
1609
|
strategy_status=status_payload,
|
|
1610
|
+
gas_available=gas_balance,
|
|
1611
|
+
gassed_up=gas_balance >= self.GAS_MAXIMUM * self.GAS_SAFETY_FRACTION,
|
|
1568
1612
|
)
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
)
|
|
1578
|
-
|
|
1613
|
+
|
|
1614
|
+
# Refresh tracked balances
|
|
1615
|
+
await self._refresh_tracked_balances()
|
|
1616
|
+
|
|
1617
|
+
# Calculate total value from tracked non-gas balances
|
|
1618
|
+
total_value = 0.0
|
|
1619
|
+
gas_token_id = self.gas_token.get("id") if self.gas_token else None
|
|
1620
|
+
|
|
1621
|
+
for token_id, balance_wei in self.tracked_balances.items():
|
|
1622
|
+
if token_id == gas_token_id:
|
|
1623
|
+
continue
|
|
1624
|
+
if balance_wei <= 0:
|
|
1625
|
+
continue
|
|
1626
|
+
|
|
1627
|
+
try:
|
|
1628
|
+
# Get token price to calculate USD value
|
|
1629
|
+
success, price_data = await self.token_adapter.get_token_price(token_id)
|
|
1630
|
+
if not success:
|
|
1631
|
+
continue
|
|
1632
|
+
|
|
1633
|
+
success, token_info = await self.token_adapter.get_token(token_id)
|
|
1634
|
+
if not success:
|
|
1635
|
+
continue
|
|
1636
|
+
|
|
1637
|
+
decimals = token_info.get("decimals", 18)
|
|
1638
|
+
price = price_data.get("current_price", 0.0)
|
|
1639
|
+
balance_usd = (float(balance_wei) / (10**decimals)) * price
|
|
1640
|
+
total_value += balance_usd
|
|
1641
|
+
except Exception as e:
|
|
1642
|
+
logger.warning(f"Failed to calculate value for {token_id}: {e}")
|
|
1643
|
+
continue
|
|
1644
|
+
|
|
1579
1645
|
status_payload = (
|
|
1580
1646
|
{
|
|
1581
1647
|
"current_pool": self.current_pool.get("token_id"),
|
|
@@ -1597,6 +1663,8 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1597
1663
|
portfolio_value=total_value,
|
|
1598
1664
|
net_deposit=self.DEPOSIT_USDC,
|
|
1599
1665
|
strategy_status=status_payload,
|
|
1666
|
+
gas_available=gas_balance,
|
|
1667
|
+
gassed_up=gas_balance >= self.GAS_MAXIMUM * self.GAS_SAFETY_FRACTION,
|
|
1600
1668
|
)
|
|
1601
1669
|
|
|
1602
1670
|
@staticmethod
|
|
@@ -1611,111 +1679,132 @@ class StablecoinYieldStrategy(Strategy):
|
|
|
1611
1679
|
return [f"({wallet_id}) && (({approve_enso}) || ({swap_enso})) "]
|
|
1612
1680
|
|
|
1613
1681
|
async def partial_liquidate(self, usd_value: float) -> StatusTuple:
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
add_llama=True,
|
|
1622
|
-
)
|
|
1623
|
-
_, total_usdc_wei = await self.balance_adapter.get_balance(
|
|
1624
|
-
token_id=self.usdc_token_info.get("token_id"),
|
|
1625
|
-
wallet_address=self._get_strategy_wallet_address(),
|
|
1626
|
-
)
|
|
1627
|
-
available_usdc_usd = float(total_usdc_wei or 0) / (
|
|
1628
|
-
10 ** self.usdc_token_info.get("decimals")
|
|
1629
|
-
)
|
|
1682
|
+
"""Liquidate strategy assets to reach target USD value in USDC."""
|
|
1683
|
+
# Refresh tracked balances
|
|
1684
|
+
await self._refresh_tracked_balances()
|
|
1685
|
+
|
|
1686
|
+
usdc_token_id = self.usdc_token_info.get("token_id")
|
|
1687
|
+
usdc_decimals = self.usdc_token_info.get("decimals")
|
|
1688
|
+
gas_token_id = self.gas_token.get("id") if self.gas_token else None
|
|
1630
1689
|
|
|
1631
|
-
|
|
1690
|
+
# Check current USDC balance
|
|
1691
|
+
available_usdc_wei = self.tracked_balances.get(usdc_token_id, 0)
|
|
1692
|
+
available_usdc_usd = float(available_usdc_wei) / (10**usdc_decimals)
|
|
1693
|
+
|
|
1694
|
+
# Liquidate non-USDC, non-gas, non-current-pool tokens first
|
|
1695
|
+
for token_id, balance_wei in list(self.tracked_balances.items()):
|
|
1632
1696
|
if available_usdc_usd >= usd_value:
|
|
1633
1697
|
break
|
|
1634
1698
|
|
|
1635
|
-
|
|
1636
|
-
if token_id ==
|
|
1699
|
+
# Skip USDC, gas, and current pool
|
|
1700
|
+
if token_id == usdc_token_id:
|
|
1637
1701
|
continue
|
|
1638
|
-
if token_id ==
|
|
1702
|
+
if token_id == gas_token_id:
|
|
1639
1703
|
continue
|
|
1640
1704
|
if self.current_pool and token_id == self.current_pool.get("token_id"):
|
|
1641
1705
|
continue
|
|
1642
1706
|
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
balance_wei,
|
|
1646
|
-
) = await self.balance_adapter.get_balance(
|
|
1647
|
-
token_id=token_id,
|
|
1648
|
-
wallet_address=self._get_strategy_wallet_address(),
|
|
1649
|
-
)
|
|
1650
|
-
if not balance_status or not balance_wei:
|
|
1707
|
+
# Skip zero balances
|
|
1708
|
+
if balance_wei <= 0:
|
|
1651
1709
|
continue
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1710
|
+
|
|
1711
|
+
# Get token info and price
|
|
1712
|
+
try:
|
|
1713
|
+
success, token_info = await self.token_adapter.get_token(token_id)
|
|
1714
|
+
if not success:
|
|
1715
|
+
continue
|
|
1716
|
+
|
|
1717
|
+
success, price_data = await self.token_adapter.get_token_price(token_id)
|
|
1718
|
+
if not success:
|
|
1719
|
+
continue
|
|
1720
|
+
|
|
1721
|
+
decimals = token_info.get("decimals", 18)
|
|
1722
|
+
price = price_data.get("current_price", 0.0)
|
|
1723
|
+
token_usd_value = price * float(balance_wei) / (10**decimals)
|
|
1724
|
+
|
|
1725
|
+
if token_usd_value > 1.0:
|
|
1726
|
+
needed_usd = usd_value - available_usdc_usd
|
|
1727
|
+
required_token_wei = int(
|
|
1728
|
+
math.ceil((needed_usd * (10**decimals)) / price)
|
|
1729
|
+
)
|
|
1730
|
+
amount_to_swap = min(required_token_wei, balance_wei)
|
|
1731
|
+
|
|
1732
|
+
logger.info(f"Liquidating {token_id} to USDC: {amount_to_swap} wei")
|
|
1733
|
+
success, msg = await self.brap_adapter.swap_from_token_ids(
|
|
1734
|
+
token_id,
|
|
1735
|
+
f"{self.usdc_token_info.get('chain').get('code')}_{self.usdc_token_info.get('address').lower()}",
|
|
1669
1736
|
self._get_strategy_wallet_address(),
|
|
1670
|
-
amount_to_swap,
|
|
1737
|
+
str(amount_to_swap),
|
|
1671
1738
|
strategy_name=self.name,
|
|
1672
1739
|
)
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1740
|
+
if success:
|
|
1741
|
+
swapped_usd = (amount_to_swap / (10**decimals)) * price
|
|
1742
|
+
available_usdc_usd += swapped_usd
|
|
1743
|
+
# Update tracked state
|
|
1744
|
+
self._update_balance(token_id, balance_wei - amount_to_swap)
|
|
1745
|
+
else:
|
|
1746
|
+
logger.warning(f"Failed to liquidate {token_id}: {msg}")
|
|
1747
|
+
except Exception as e:
|
|
1748
|
+
logger.error(f"Error liquidating {token_id}: {e}")
|
|
1749
|
+
continue
|
|
1677
1750
|
|
|
1678
|
-
|
|
1679
|
-
|
|
1751
|
+
# Refresh USDC balance after swaps
|
|
1752
|
+
success, usdc_wei = await self.balance_adapter.get_balance(
|
|
1753
|
+
token_id=usdc_token_id,
|
|
1680
1754
|
wallet_address=self._get_strategy_wallet_address(),
|
|
1681
1755
|
)
|
|
1682
|
-
|
|
1683
|
-
|
|
1756
|
+
if success and usdc_wei:
|
|
1757
|
+
available_usdc_wei = int(usdc_wei)
|
|
1758
|
+
available_usdc_usd = float(available_usdc_wei) / (10**usdc_decimals)
|
|
1759
|
+
self._update_balance(usdc_token_id, available_usdc_wei)
|
|
1684
1760
|
|
|
1761
|
+
# If still not enough, liquidate from current pool
|
|
1685
1762
|
if (
|
|
1686
1763
|
available_usdc_usd < usd_value
|
|
1687
1764
|
and self.current_pool
|
|
1688
|
-
and self.current_pool.get("token_id")
|
|
1689
|
-
!= self.usdc_token_info.get("token_id")
|
|
1765
|
+
and self.current_pool.get("token_id") != usdc_token_id
|
|
1690
1766
|
):
|
|
1691
1767
|
remaining_usd = usd_value - available_usdc_usd
|
|
1692
|
-
(
|
|
1693
|
-
|
|
1694
|
-
pool_balance_wei,
|
|
1695
|
-
) = await self.balance_adapter.get_pool_balance(
|
|
1696
|
-
pool_address=self.current_pool.get("address"),
|
|
1697
|
-
chain_id=self.current_pool.get("chain").get("id"),
|
|
1698
|
-
user_address=self._get_strategy_wallet_address(),
|
|
1768
|
+
pool_balance_wei = self.tracked_balances.get(
|
|
1769
|
+
self.current_pool.get("token_id"), 0
|
|
1699
1770
|
)
|
|
1700
1771
|
pool_decimals = self.current_pool.get("decimals")
|
|
1701
1772
|
amount_to_swap = min(
|
|
1702
|
-
|
|
1773
|
+
pool_balance_wei, int(remaining_usd * (10**pool_decimals))
|
|
1703
1774
|
)
|
|
1704
1775
|
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1776
|
+
if amount_to_swap > 0:
|
|
1777
|
+
try:
|
|
1778
|
+
logger.info(
|
|
1779
|
+
f"Liquidating from current pool {self.current_pool.get('token_id')}"
|
|
1780
|
+
)
|
|
1781
|
+
success, msg = await self.brap_adapter.swap_from_token_ids(
|
|
1782
|
+
self.current_pool.get("token_id"),
|
|
1783
|
+
f"{self.usdc_token_info.get('chain').get('code')}_{self.usdc_token_info.get('address').lower()}",
|
|
1784
|
+
self._get_strategy_wallet_address(),
|
|
1785
|
+
str(amount_to_swap),
|
|
1786
|
+
strategy_name=self.name,
|
|
1787
|
+
)
|
|
1788
|
+
if success:
|
|
1789
|
+
self._update_balance(
|
|
1790
|
+
self.current_pool.get("token_id"),
|
|
1791
|
+
pool_balance_wei - amount_to_swap,
|
|
1792
|
+
)
|
|
1793
|
+
except Exception as e:
|
|
1794
|
+
logger.error(f"Error swapping pool to USDC: {e}")
|
|
1795
|
+
|
|
1796
|
+
# Refresh USDC balance again
|
|
1797
|
+
success, usdc_wei = await self.balance_adapter.get_balance(
|
|
1798
|
+
token_id=usdc_token_id,
|
|
1799
|
+
wallet_address=self._get_strategy_wallet_address(),
|
|
1712
1800
|
)
|
|
1713
|
-
|
|
1714
|
-
|
|
1801
|
+
if success and usdc_wei:
|
|
1802
|
+
available_usdc_wei = int(usdc_wei)
|
|
1803
|
+
self._update_balance(usdc_token_id, available_usdc_wei)
|
|
1715
1804
|
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1805
|
+
to_pay = min(available_usdc_wei, int(usd_value * (10**usdc_decimals)))
|
|
1806
|
+
to_pay_usd = float(to_pay) / (10**usdc_decimals)
|
|
1807
|
+
return (
|
|
1808
|
+
True,
|
|
1809
|
+
f"Partial liquidation completed. Available: {to_pay_usd:.2f} USDC",
|
|
1810
|
+
)
|