mirror of
https://github.com/hanzoskill/kraken.git
synced 2026-10-30 21:05:45 +00:00
462 lines
16 KiB
Python
462 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Kraken Portfolio CLI - Smart Wrapper for Hanzo Bot
|
|
|
|
High-level commands with accurate portfolio calculations.
|
|
For raw API access, use kraken_api.py directly.
|
|
|
|
Usage:
|
|
python kraken_cli.py <command> [--options]
|
|
|
|
Commands:
|
|
# Portfolio (Smart Calculations)
|
|
summary - Complete portfolio overview (no double-counting)
|
|
net-worth - Total net worth calculation
|
|
performance - Returns vs deposits
|
|
holdings - Asset breakdown with current prices
|
|
staking - Staking positions and rewards
|
|
|
|
# Pass-through to kraken_api.py
|
|
api <command> [args] - Run any kraken_api.py command directly
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import argparse
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
from typing import Optional
|
|
|
|
# Import the API layer
|
|
from kraken_api import (
|
|
get_clients,
|
|
format_currency,
|
|
cmd_balance,
|
|
cmd_portfolio,
|
|
cmd_earn,
|
|
cmd_ticker,
|
|
cmd_ledger,
|
|
)
|
|
|
|
from dotenv import load_dotenv
|
|
from kraken.spot import User, Market, Earn, Funding
|
|
|
|
# Load .env file from skill directory
|
|
skill_dir = os.path.dirname(os.path.abspath(__file__))
|
|
load_dotenv(os.path.join(skill_dir, ".env"))
|
|
|
|
|
|
def get_asset_prices(market: Market, pairs: list[str]) -> dict[str, float]:
|
|
"""Fetch current USD prices for a list of asset pairs."""
|
|
prices = {}
|
|
for pair in pairs:
|
|
try:
|
|
result = market.get_ticker(pair=pair)
|
|
if result and pair in result:
|
|
prices[pair] = float(result[pair].get("c", [0])[0])
|
|
except Exception:
|
|
pass
|
|
return prices
|
|
|
|
|
|
def calculate_portfolio_summary(
|
|
user: Optional[User],
|
|
market: Market,
|
|
earn: Optional[Earn],
|
|
) -> dict:
|
|
"""
|
|
Calculate accurate portfolio summary without double-counting.
|
|
|
|
Returns dict with:
|
|
- main_equity: USD value of Main wallet (includes Auto Earn)
|
|
- earn_wallet: USD value of Earn wallet (bonded/locked rewards)
|
|
- total_net_worth: main_equity + earn_wallet
|
|
- spot_balances: dict of asset -> quantity
|
|
- staking_allocations: list of staking positions
|
|
- total_staking_rewards: sum of all rewards earned
|
|
"""
|
|
result = {
|
|
"main_equity": 0.0,
|
|
"earn_wallet": 0.0,
|
|
"total_net_worth": 0.0,
|
|
"free_margin": 0.0,
|
|
"spot_balances": {},
|
|
"staking_allocations": [],
|
|
"total_staking_rewards": 0.0,
|
|
"error": None,
|
|
}
|
|
|
|
if not user:
|
|
result["error"] = "API credentials required"
|
|
return result
|
|
|
|
# Step 1: Get Trade Balance (Equity = Main wallet value)
|
|
try:
|
|
trade_balance = user.get_trade_balance(asset="USD")
|
|
if trade_balance:
|
|
result["main_equity"] = float(trade_balance.get("eb", 0))
|
|
result["free_margin"] = float(trade_balance.get("mf", 0))
|
|
except Exception as e:
|
|
result["error"] = f"Trade balance error: {e}"
|
|
return result
|
|
|
|
# Step 2: Get Spot Balances (for breakdown)
|
|
try:
|
|
balances = user.get_account_balance()
|
|
if balances:
|
|
for asset, qty in balances.items():
|
|
if float(qty) > 0:
|
|
result["spot_balances"][asset] = float(qty)
|
|
except Exception:
|
|
pass # Non-critical
|
|
|
|
# Step 3: Get Earn Allocations and classify by lock type
|
|
# Kraken has two staking types:
|
|
# - 'flex' (Auto Earn): Assets stay in Main wallet, included in Trade Balance
|
|
# - 'bonded': Assets move to Earn wallet, NOT in Trade Balance
|
|
if earn:
|
|
try:
|
|
# First get strategies to build lock_type map
|
|
strategies = earn.list_earn_strategies()
|
|
lock_types = {}
|
|
for strat in strategies.get("items", []):
|
|
lock_types[strat.get("id")] = strat.get("lock_type", {}).get("type", "unknown")
|
|
|
|
# Now get allocations and classify
|
|
earn_data = earn.list_earn_allocations(
|
|
converted_asset="USD",
|
|
hide_zero_allocations="true"
|
|
)
|
|
if earn_data:
|
|
result["total_staking_rewards"] = float(earn_data.get("total_rewarded", 0))
|
|
|
|
bonded_value = 0.0
|
|
flex_value = 0.0
|
|
|
|
for item in earn_data.get("items", []):
|
|
strat_id = item.get("strategy_id", "")
|
|
lock_type = lock_types.get(strat_id, "unknown")
|
|
is_bonded = lock_type == "bonded"
|
|
|
|
usd_value = float(item.get("amount_allocated", {}).get("total", {}).get("converted", 0))
|
|
rewards = float(item.get("total_rewarded", {}).get("converted", 0))
|
|
|
|
allocation = {
|
|
"asset": item.get("native_asset", "?"),
|
|
"amount": float(item.get("amount_allocated", {}).get("total", {}).get("native", 0)),
|
|
"usd_value": usd_value,
|
|
"rewards": rewards,
|
|
"lock_type": lock_type,
|
|
"is_bonded": is_bonded,
|
|
}
|
|
result["staking_allocations"].append(allocation)
|
|
|
|
if is_bonded:
|
|
bonded_value += usd_value
|
|
else:
|
|
flex_value += usd_value
|
|
|
|
# Earn wallet = bonded staking (NOT in main equity)
|
|
result["earn_wallet"] = bonded_value
|
|
result["flex_value"] = flex_value
|
|
|
|
except Exception:
|
|
pass # Non-critical
|
|
|
|
# Step 4: Calculate Total (Main + Earn rewards)
|
|
# Note: Earn wallet on dashboard shows accrued rewards not yet in Main
|
|
result["total_net_worth"] = result["main_equity"] + result["earn_wallet"]
|
|
|
|
return result
|
|
|
|
|
|
def cmd_summary() -> str:
|
|
"""Generate complete portfolio summary with accurate calculations."""
|
|
user, market, earn, _ = get_clients()
|
|
|
|
summary = calculate_portfolio_summary(user, market, earn)
|
|
|
|
if summary["error"]:
|
|
return f"Error: {summary['error']}"
|
|
|
|
lines = []
|
|
lines.append("=" * 50)
|
|
lines.append(" KRAKEN PORTFOLIO SUMMARY")
|
|
lines.append("=" * 50)
|
|
lines.append("")
|
|
|
|
# Total Net Worth
|
|
lines.append("TOTAL NET WORTH")
|
|
lines.append(f" Main Wallet (Equity): ${summary['main_equity']:,.2f}")
|
|
lines.append(f" Earn Wallet (Bonded): ${summary['earn_wallet']:,.2f}")
|
|
lines.append(f" ─────────────────────────────────")
|
|
lines.append(f" TOTAL: ${summary['total_net_worth']:,.2f}")
|
|
lines.append("")
|
|
|
|
# Staking Positions - separate by type
|
|
if summary["staking_allocations"]:
|
|
bonded = [a for a in summary["staking_allocations"] if a.get("is_bonded")]
|
|
flex = [a for a in summary["staking_allocations"] if not a.get("is_bonded")]
|
|
|
|
if flex:
|
|
lines.append("AUTO EARN (Flexible) - in Main Wallet")
|
|
for alloc in sorted(flex, key=lambda x: x["usd_value"], reverse=True):
|
|
if alloc["usd_value"] > 0.01:
|
|
lines.append(f" {alloc['asset']:6s}: ${alloc['usd_value']:>10,.2f} "
|
|
f"(rewards: ${alloc['rewards']:,.2f})")
|
|
lines.append("")
|
|
|
|
if bonded:
|
|
lines.append("BONDED STAKING - in Earn Wallet")
|
|
for alloc in sorted(bonded, key=lambda x: x["usd_value"], reverse=True):
|
|
if alloc["usd_value"] > 0.01:
|
|
lines.append(f" {alloc['asset']:6s}: ${alloc['usd_value']:>10,.2f} "
|
|
f"(rewards: ${alloc['rewards']:,.2f})")
|
|
lines.append("")
|
|
|
|
lines.append(f" Total Staking Rewards: ${summary['total_staking_rewards']:,.2f}")
|
|
lines.append("")
|
|
|
|
# Spot Holdings
|
|
if summary["spot_balances"]:
|
|
lines.append("ASSET HOLDINGS")
|
|
for asset, qty in sorted(summary["spot_balances"].items(), key=lambda x: x[1], reverse=True):
|
|
# Mark staked assets
|
|
is_staked = any(a["asset"] == asset.replace(".S", "") for a in summary["staking_allocations"])
|
|
staked_marker = " (staked)" if ".S" in asset or is_staked else ""
|
|
lines.append(f" {asset:8s}: {qty:>12.6f}{staked_marker}")
|
|
lines.append("")
|
|
|
|
lines.append("=" * 50)
|
|
lines.append(f" Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
lines.append("=" * 50)
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def cmd_net_worth() -> str:
|
|
"""Get just the net worth calculation."""
|
|
user, market, earn, _ = get_clients()
|
|
|
|
summary = calculate_portfolio_summary(user, market, earn)
|
|
|
|
if summary["error"]:
|
|
return f"Error: {summary['error']}"
|
|
|
|
lines = [
|
|
"=== NET WORTH ===",
|
|
f" Main Wallet: ${summary['main_equity']:,.2f}",
|
|
f" Earn Wallet: ${summary['earn_wallet']:,.2f}",
|
|
f" TOTAL: ${summary['total_net_worth']:,.2f}",
|
|
]
|
|
return "\n".join(lines)
|
|
|
|
|
|
def cmd_performance() -> str:
|
|
"""Calculate performance vs deposits."""
|
|
user, market, earn, _ = get_clients()
|
|
|
|
if not user:
|
|
return "Error: API credentials required"
|
|
|
|
# Get current portfolio value
|
|
summary = calculate_portfolio_summary(user, market, earn)
|
|
if summary["error"]:
|
|
return f"Error: {summary['error']}"
|
|
|
|
# Get deposit history from ledger
|
|
total_deposits = 0.0
|
|
total_withdrawals = 0.0
|
|
|
|
try:
|
|
ledger_result = user.get_ledgers_info()
|
|
if ledger_result and "ledger" in ledger_result:
|
|
for _, entry in ledger_result.get("ledger", {}).items():
|
|
entry_type = entry.get("type", "")
|
|
asset = entry.get("asset", "")
|
|
amount = float(entry.get("amount", 0))
|
|
|
|
# Only count fiat deposits/withdrawals
|
|
if asset in ["ZUSD", "USD", "EUR", "ZEUR", "GBP"]:
|
|
if entry_type == "deposit":
|
|
total_deposits += abs(amount)
|
|
elif entry_type == "withdrawal":
|
|
total_withdrawals += abs(amount)
|
|
except Exception as e:
|
|
return f"Error reading ledger: {e}"
|
|
|
|
net_investment = total_deposits - total_withdrawals
|
|
current_value = summary["total_net_worth"]
|
|
total_return = current_value - net_investment
|
|
return_pct = (total_return / net_investment * 100) if net_investment > 0 else 0
|
|
|
|
lines = [
|
|
"=== PERFORMANCE ===",
|
|
f" Total Deposits: ${total_deposits:,.2f}",
|
|
f" Total Withdrawals: ${total_withdrawals:,.2f}",
|
|
f" Net Investment: ${net_investment:,.2f}",
|
|
"",
|
|
f" Current Value: ${current_value:,.2f}",
|
|
f" Total Return: ${total_return:+,.2f} ({return_pct:+.1f}%)",
|
|
"",
|
|
f" Staking Rewards: ${summary['total_staking_rewards']:,.2f}",
|
|
]
|
|
return "\n".join(lines)
|
|
|
|
|
|
def cmd_holdings() -> str:
|
|
"""Show asset holdings with current USD values."""
|
|
user, market, earn, _ = get_clients()
|
|
|
|
if not user:
|
|
return "Error: API credentials required"
|
|
|
|
try:
|
|
balances = user.get_account_balance()
|
|
if not balances:
|
|
return "No balance data."
|
|
|
|
# Asset pair mapping for price lookups
|
|
pair_map = {
|
|
"XXBT": "XXBTZUSD",
|
|
"XETH": "XETHZUSD",
|
|
"SOL": "SOLUSD",
|
|
"DOT": "DOTUSD",
|
|
"SOL.S": "SOLUSD",
|
|
"DOT.S": "DOTUSD",
|
|
"SOL03.S": "SOLUSD",
|
|
"DOT28.S": "DOTUSD",
|
|
}
|
|
|
|
lines = ["=== ASSET HOLDINGS ==="]
|
|
lines.append(f" {'Asset':<10} {'Quantity':>14} {'Price':>12} {'Value':>12}")
|
|
lines.append(" " + "-" * 50)
|
|
|
|
total_usd = 0.0
|
|
|
|
for asset, qty in sorted(balances.items(), key=lambda x: float(x[1]), reverse=True):
|
|
qty_float = float(qty)
|
|
if qty_float <= 0:
|
|
continue
|
|
|
|
# Get price
|
|
pair = pair_map.get(asset, f"{asset}USD")
|
|
price = 0.0
|
|
try:
|
|
result = market.get_ticker(pair=pair)
|
|
if result and pair in result:
|
|
price = float(result[pair].get("c", [0])[0])
|
|
except Exception:
|
|
pass
|
|
|
|
value = qty_float * price
|
|
total_usd += value
|
|
|
|
price_str = f"${price:,.2f}" if price > 0 else "N/A"
|
|
value_str = f"${value:,.2f}" if price > 0 else "N/A"
|
|
|
|
lines.append(f" {asset:<10} {qty_float:>14.6f} {price_str:>12} {value_str:>12}")
|
|
|
|
lines.append(" " + "-" * 50)
|
|
lines.append(f" {'TOTAL':<10} {'':<14} {'':<12} ${total_usd:>11,.2f}")
|
|
|
|
return "\n".join(lines)
|
|
except Exception as e:
|
|
return f"Error: {e}"
|
|
|
|
|
|
def cmd_staking() -> str:
|
|
"""Show staking positions and rewards."""
|
|
user, market, earn, _ = get_clients()
|
|
|
|
if not earn:
|
|
return "Error: API credentials required"
|
|
|
|
try:
|
|
result = earn.list_earn_allocations(
|
|
converted_asset="USD",
|
|
hide_zero_allocations="true"
|
|
)
|
|
if not result or "items" not in result:
|
|
return "No staking positions found."
|
|
|
|
total_allocated = float(result.get("total_allocated", 0))
|
|
total_rewarded = float(result.get("total_rewarded", 0))
|
|
|
|
lines = ["=== STAKING POSITIONS ==="]
|
|
lines.append(f" Total Allocated: ${total_allocated:,.2f}")
|
|
lines.append(f" Total Rewards: ${total_rewarded:,.2f}")
|
|
lines.append("")
|
|
|
|
for item in result.get("items", []):
|
|
asset = item.get("native_asset", "?")
|
|
amount = item.get("amount_allocated", {}).get("total", {}).get("native", "0")
|
|
usd_value = float(item.get("amount_allocated", {}).get("total", {}).get("converted", 0))
|
|
rewards = float(item.get("total_rewarded", {}).get("converted", 0))
|
|
|
|
lines.append(f" {asset}")
|
|
lines.append(f" Amount: {amount}")
|
|
lines.append(f" Value: ${usd_value:,.2f}")
|
|
lines.append(f" Rewards: ${rewards:,.2f}")
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|
|
except Exception as e:
|
|
return f"Error: {e}"
|
|
|
|
|
|
def run_api_passthrough(args: list[str]) -> str:
|
|
"""Pass through to kraken_api.py for raw commands."""
|
|
import subprocess
|
|
api_script = os.path.join(skill_dir, "kraken_api.py")
|
|
result = subprocess.run(
|
|
["python", api_script] + args,
|
|
capture_output=True,
|
|
text=True,
|
|
cwd=skill_dir,
|
|
)
|
|
output = result.stdout + result.stderr
|
|
return output.strip()
|
|
|
|
|
|
def main():
|
|
# Handle high-level commands first
|
|
high_level_cmds = {"summary", "net-worth", "performance", "holdings", "staking", "help", "-h", "--help"}
|
|
|
|
if len(sys.argv) < 2:
|
|
# Default to summary
|
|
print(cmd_summary())
|
|
return
|
|
|
|
cmd = sys.argv[1].lower()
|
|
|
|
# High-level commands with smart calculations
|
|
if cmd == "summary":
|
|
print(cmd_summary())
|
|
elif cmd == "net-worth":
|
|
print(cmd_net_worth())
|
|
elif cmd == "performance":
|
|
print(cmd_performance())
|
|
elif cmd == "holdings":
|
|
print(cmd_holdings())
|
|
elif cmd == "staking":
|
|
print(cmd_staking())
|
|
elif cmd in ("help", "-h", "--help"):
|
|
print(__doc__)
|
|
print("\nFor raw API commands, pass them directly:")
|
|
print(" python kraken_cli.py ticker --pair XXBTZUSD")
|
|
print(" python kraken_cli.py balance")
|
|
print(" python kraken_cli.py api <command> [args] # explicit API call")
|
|
|
|
# Pass-through to kraken_api.py for all other commands
|
|
elif cmd == "api":
|
|
# Explicit API passthrough
|
|
print(run_api_passthrough(sys.argv[2:]))
|
|
else:
|
|
# Direct passthrough for backward compatibility
|
|
print(run_api_passthrough(sys.argv[1:]))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|