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.

Files changed (50) hide show
  1. wayfinder_paths/adapters/balance_adapter/README.md +0 -10
  2. wayfinder_paths/adapters/balance_adapter/adapter.py +0 -20
  3. wayfinder_paths/adapters/balance_adapter/test_adapter.py +0 -30
  4. wayfinder_paths/adapters/brap_adapter/adapter.py +3 -2
  5. wayfinder_paths/adapters/brap_adapter/test_adapter.py +9 -13
  6. wayfinder_paths/adapters/hyperlend_adapter/test_adapter.py +14 -7
  7. wayfinder_paths/adapters/hyperliquid_adapter/__init__.py +18 -0
  8. wayfinder_paths/adapters/hyperliquid_adapter/adapter.py +1093 -0
  9. wayfinder_paths/adapters/hyperliquid_adapter/executor.py +549 -0
  10. wayfinder_paths/adapters/hyperliquid_adapter/manifest.yaml +8 -0
  11. wayfinder_paths/adapters/hyperliquid_adapter/paired_filler.py +1050 -0
  12. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter.py +126 -0
  13. wayfinder_paths/adapters/hyperliquid_adapter/test_adapter_live.py +219 -0
  14. wayfinder_paths/adapters/hyperliquid_adapter/test_utils.py +220 -0
  15. wayfinder_paths/adapters/hyperliquid_adapter/utils.py +134 -0
  16. wayfinder_paths/adapters/ledger_adapter/test_adapter.py +7 -6
  17. wayfinder_paths/adapters/pool_adapter/README.md +3 -28
  18. wayfinder_paths/adapters/pool_adapter/adapter.py +0 -72
  19. wayfinder_paths/adapters/pool_adapter/examples.json +0 -43
  20. wayfinder_paths/adapters/pool_adapter/test_adapter.py +4 -54
  21. wayfinder_paths/adapters/token_adapter/test_adapter.py +4 -14
  22. wayfinder_paths/core/adapters/models.py +9 -4
  23. wayfinder_paths/core/analytics/__init__.py +11 -0
  24. wayfinder_paths/core/analytics/bootstrap.py +57 -0
  25. wayfinder_paths/core/analytics/stats.py +48 -0
  26. wayfinder_paths/core/analytics/test_analytics.py +170 -0
  27. wayfinder_paths/core/clients/BRAPClient.py +1 -0
  28. wayfinder_paths/core/clients/LedgerClient.py +2 -7
  29. wayfinder_paths/core/clients/PoolClient.py +0 -16
  30. wayfinder_paths/core/clients/WalletClient.py +0 -27
  31. wayfinder_paths/core/clients/protocols.py +104 -18
  32. wayfinder_paths/scripts/make_wallets.py +9 -0
  33. wayfinder_paths/scripts/run_strategy.py +124 -0
  34. wayfinder_paths/strategies/basis_trading_strategy/README.md +213 -0
  35. wayfinder_paths/strategies/basis_trading_strategy/__init__.py +3 -0
  36. wayfinder_paths/strategies/basis_trading_strategy/constants.py +1 -0
  37. wayfinder_paths/strategies/basis_trading_strategy/examples.json +16 -0
  38. wayfinder_paths/strategies/basis_trading_strategy/manifest.yaml +23 -0
  39. wayfinder_paths/strategies/basis_trading_strategy/snapshot_mixin.py +1011 -0
  40. wayfinder_paths/strategies/basis_trading_strategy/strategy.py +4522 -0
  41. wayfinder_paths/strategies/basis_trading_strategy/test_strategy.py +727 -0
  42. wayfinder_paths/strategies/basis_trading_strategy/types.py +39 -0
  43. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/examples.json +1 -9
  44. wayfinder_paths/strategies/hyperlend_stable_yield_strategy/test_strategy.py +36 -5
  45. wayfinder_paths/strategies/stablecoin_yield_strategy/strategy.py +367 -278
  46. wayfinder_paths/strategies/stablecoin_yield_strategy/test_strategy.py +204 -7
  47. {wayfinder_paths-0.1.6.dist-info → wayfinder_paths-0.1.8.dist-info}/METADATA +32 -3
  48. {wayfinder_paths-0.1.6.dist-info → wayfinder_paths-0.1.8.dist-info}/RECORD +50 -27
  49. {wayfinder_paths-0.1.6.dist-info → wayfinder_paths-0.1.8.dist-info}/LICENSE +0 -0
  50. {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
- inferred = await self._infer_active_pool_from_wallet()
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 _infer_active_pool_from_wallet(self):
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
- if not wallet_balances:
520
- return None
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
- target_chain = self.current_pool.get("chain").get("name").lower()
523
- usdc_token = (
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
- best_entry = None
531
- best_balance_wei = 0
532
- best_balance_usd = 0.0
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
- for balance in wallet_balances.get("balances"):
535
- if self._is_gas_balance_entry(balance):
536
- continue
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
- network = (balance.get("network") or "").lower()
539
- if network != target_chain:
540
- continue
605
+ if not best_token_id:
606
+ return None
541
607
 
542
- token_address = balance.get("tokenAddress")
543
- if not token_address or token_address.lower() == usdc_address:
544
- continue
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
- raw_balance = balance.get("balance_base_units")
613
+ # Get fresh on-chain balance
614
+ strategy_address = self._get_strategy_wallet_address()
547
615
  try:
548
- balance_wei = int(raw_balance)
549
- except (TypeError, ValueError):
550
- continue
551
- if balance_wei <= 0:
552
- continue
553
-
554
- balance_usd = float(balance.get("balanceUSD") or 0.0)
555
- if balance_usd <= 0:
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
- try:
580
- strategy_address = self._get_strategy_wallet_address()
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
- if not success:
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
- return token, onchain_balance, dict(best_entry)
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
- wallet_balances,
1214
- ) = await self.balance_adapter.get_all_balances(
1215
- wallet_address=self._get_strategy_wallet_address(),
1216
- enrich=True,
1217
- from_cache=False,
1218
- add_llama=True,
1219
- )
1220
- target_chain = target_token.get("chain").get("name").lower()
1221
- target_address = target_token.get("address").lower()
1222
-
1223
- for balance in wallet_balances.get("balances"):
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
- balance_raw = balance.get("balance_base_units")
1228
- token_address = balance.get("tokenAddress")
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
- if network == target_chain and token_address.lower() == target_address:
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
- token_1_id = f"{network}_{token_address.lower()}"
1238
- target_token_id = f"{target_chain}_{target_address.lower()}"
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
- token_1_id,
1242
- target_token_id,
1305
+ token_id,
1306
+ target_token_id_for_swap,
1243
1307
  self._get_strategy_wallet_address(),
1244
- balance_raw,
1308
+ str(balance_wei),
1245
1309
  strategy_name=self.name,
1246
1310
  )
1247
- if not success:
1248
- return (False, f"Swap failed: {msg}")
1249
- except Exception:
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
- results = []
1411
+ """Get non-gas balances from tracked tokens."""
1412
+ # Refresh tracked balances
1413
+ await self._refresh_tracked_balances()
1330
1414
 
1331
- if self.current_pool != self.usdc_token_info:
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
- try:
1361
- (
1362
- _,
1363
- wallet_balances,
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
- for balance in wallet_balances.get("balances"):
1374
- if self._is_gas_balance_entry(balance):
1423
+ # Skip zero balances
1424
+ if balance_wei <= 0:
1375
1425
  continue
1376
1426
 
1377
- raw = balance.get("balance_base_units")
1427
+ # Fetch token info to get address and chain
1378
1428
  try:
1379
- amount_wei = int(raw)
1380
- except (TypeError, ValueError):
1381
- continue
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
- token_address = balance.get("tokenAddress")
1386
- network = (balance.get("network") or "").lower()
1387
- if not token_address or not network:
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; reporting current wallet holdings only.",
1561
- "idle_usd": total_value,
1602
+ "info": "No recorded strategy deposits.",
1603
+ "idle_usd": 0.0,
1562
1604
  }
1563
1605
 
1564
1606
  return StatusDict(
1565
- portfolio_value=total_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
- wallet_balances,
1572
- ) = await self.balance_adapter.get_all_balances(
1573
- wallet_address=self._get_strategy_wallet_address(),
1574
- enrich=True,
1575
- from_cache=False,
1576
- add_llama=True,
1577
- )
1578
- total_value = self._sum_non_gas_balance_usd(wallet_balances.get("balances"))
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
- wallet_balances,
1617
- ) = await self.balance_adapter.get_all_balances(
1618
- wallet_address=self._get_strategy_wallet_address(),
1619
- enrich=True,
1620
- from_cache=False,
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
- for balance in wallet_balances.get("balances"):
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
- token_id = balance.get("token_id")
1636
- if token_id == self.usdc_token_info.get("token_id"):
1699
+ # Skip USDC, gas, and current pool
1700
+ if token_id == usdc_token_id:
1637
1701
  continue
1638
- if token_id == self.gas_token.get("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
- balance_status,
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
- price = float(balance.get("price", 0.0))
1653
- decimals = int(balance.get("decimals", 18))
1654
- token_usd_value = price * float(balance_wei) / (10**decimals)
1655
-
1656
- if token_usd_value > 1.0:
1657
- needed_usd = usd_value - available_usdc_usd
1658
- required_token_wei = int(
1659
- math.ceil((needed_usd * (10**decimals)) / price)
1660
- )
1661
- amount_to_swap = min(required_token_wei, balance_wei)
1662
- token_address = balance.get("tokenAddress")
1663
- network = balance.get("network", "").lower()
1664
- token_1_id = f"{network}_{token_address.lower()}"
1665
- try:
1666
- await self.brap_adapter.swap_from_token_ids(
1667
- token_1_id,
1668
- f"{self.usdc_token_info.get('chain').get('name')}_{self.usdc_token_info.get('address').lower()}",
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
- swapped_usd = (amount_to_swap / (10**decimals)) * price
1674
- available_usdc_usd += swapped_usd
1675
- except Exception as e:
1676
- logger.error(f"Error swapping {token_address} to USDC: {e}")
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
- _, total_usdc_wei = await self.balance_adapter.get_balance(
1679
- token_id=self.usdc_token_info.get("token_id"),
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
- usdc_decimals = self.usdc_token_info.get("decimals")
1683
- available_usdc_usd = float(total_usdc_wei or 0) / (10**usdc_decimals)
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
- int(pool_balance_wei or 0), int(remaining_usd * (10**pool_decimals))
1773
+ pool_balance_wei, int(remaining_usd * (10**pool_decimals))
1703
1774
  )
1704
1775
 
1705
- try:
1706
- await self.brap_adapter.swap_from_token_ids(
1707
- f"{self.current_pool.get('chain').get('code').lower()}_{self.current_pool.get('address').lower()}",
1708
- f"{self.usdc_token_info.get('chain').get('name')}_{self.usdc_token_info.get('address').lower()}",
1709
- self._get_strategy_wallet_address(),
1710
- amount_to_swap,
1711
- strategy_name=self.name,
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
- except Exception as e:
1714
- logger.error(f"Error swapping pool to USDC: {e}")
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
- _, total_usdc_wei = await self.balance_adapter.get_balance(
1717
- token_id=self.usdc_token_info.get("token_id"),
1718
- wallet_address=self._get_strategy_wallet_address(),
1719
- )
1720
- to_pay = min(int(total_usdc_wei or 0), int(usd_value * (10**usdc_decimals)))
1721
- return (True, f"Partial liquidation completed. Paid {to_pay} USDC")
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
+ )