Portfolio Rebalancer
This guide builds a portfolio rebalancer that reads your wallet holdings, compares actual vs. target allocations, calculates drift, and executes the minimum swaps needed to bring your portfolio back in line. It demonstrates multi-endpoint REST API orchestration across portfolio, price, quote, and swap endpoints.
What the Script Does
1. Loads your API key and wallet address from environment variables
2. Fetches current portfolio holdings via GET /portfolio
3. Fetches current prices via GET /prices
4. Calculates each token's actual allocation percentage vs. your targets
5. Identifies tokens that have drifted beyond a configurable threshold (default: 5%)
6. Generates a swap plan — sell overweight tokens, buy underweight tokens
7. Executes each swap via POST /quote + POST /swap/execute
8. Tracks each swap to completion via GET /swap/status/:id
9. Prints a before/after allocation table
Python Version
class=class="hl-str">"hl-comment">#!/usr/bin/env python3
class="hl-str">""class="hl-str">"
Suwappu Portfolio Rebalancer — Python
Reads your portfolio, compares to target allocations, and swaps to rebalance.
"class="hl-str">""
import os
import sys
import time
import requests
BASE_URL = class="hl-str">"https:class="hl-commentclass="hl-str">">//api.suwappu.bot/v1/agent"
class=class="hl-str">"hl-comment"># --- Configuration ---
class=class="hl-str">"hl-comment"># Target allocations must sum to 100
TARGET_ALLOCATIONS = {
class="hl-str">"ETH": 50,
class="hl-str">"USDC": 30,
class="hl-str">"WBTC": 20,
}
CHAIN = class="hl-str">"ethereum"
DRIFT_THRESHOLD = 5.0 class=class="hl-str">"hl-comment"># Only rebalance if a token is >5% off target
SLIPPAGE = 0.01 class=class="hl-str">"hl-comment"># 1% max slippage
def get_portfolio(headers, wallet_address):
class="hl-str">""class="hl-str">"Fetch current portfolio balances."class="hl-str">""
response = requests.get(
fclass="hl-str">"{BASE_URL}/portfolio",
headers=headers,
params={class="hl-str">"wallet_address": wallet_address, class="hl-str">"chain": CHAIN},
)
response.raise_for_status()
return response.json()
def get_prices(headers, symbols):
class="hl-str">""class="hl-str">"Fetch current USD prices for the given symbols."class="hl-str">""
response = requests.get(
fclass="hl-str">"{BASE_URL}/prices",
headers=headers,
params={class="hl-str">"symbols": class="hl-str">",".join(symbols)},
)
response.raise_for_status()
return response.json()[class="hl-str">"prices"]
def get_quote(headers, from_token, to_token, amount):
class="hl-str">""class="hl-str">"Get a swap quote."class="hl-str">""
response = requests.post(
fclass="hl-str">"{BASE_URL}/quote",
headers=headers,
json={
class="hl-str">"from_token": from_token,
class="hl-str">"to_token": to_token,
class="hl-str">"amount": str(amount),
class="hl-str">"chain": CHAIN,
class="hl-str">"slippage": SLIPPAGE,
},
)
response.raise_for_status()
return response.json()
def execute_swap(headers, quote_id):
class="hl-str">""class="hl-str">"Execute a swap from a quote."class="hl-str">""
response = requests.post(
fclass="hl-str">"{BASE_URL}/swap/execute",
headers=headers,
json={class="hl-str">"quote_id": quote_id},
)
response.raise_for_status()
return response.json()
def wait_for_swap(headers, swap_id):
class="hl-str">""class="hl-str">"Poll swap status until completed or failed."class="hl-str">""
while True:
response = requests.get(
fclass="hl-str">"{BASE_URL}/swap/status/{swap_id}",
headers=headers,
)
response.raise_for_status()
status = response.json()
if status[class="hl-str">"status"] == class="hl-str">"completed":
print(fclass="hl-str">" ✓ Swap {swap_id} completed (tx: {status[class="hl-str">'tx_hash']})")
return True
elif status[class="hl-str">"status"] == class="hl-str">"failed":
print(fclass="hl-str">" ✗ Swap {swap_id} failed")
return False
time.sleep(5)
def calculate_allocations(balances, prices):
class="hl-str">""class="hl-str">"Calculate current allocation percentages from balances and prices."class="hl-str">""
total_usd = 0.0
holdings = {}
for bal in balances:
symbol = bal[class="hl-str">"symbol"]
if symbol in TARGET_ALLOCATIONS:
usd_value = bal[class="hl-str">"usd_value"]
holdings[symbol] = {
class="hl-str">"balance": float(bal[class="hl-str">"balance"]),
class="hl-str">"usd_value": usd_value,
}
total_usd += usd_value
class=class="hl-str">"hl-comment"># Add missing target tokens with zero balance
for symbol in TARGET_ALLOCATIONS:
if symbol not in holdings:
holdings[symbol] = {class="hl-str">"balance": 0.0, class="hl-str">"usd_value": 0.0}
class=class="hl-str">"hl-comment"># Calculate percentages
for symbol in holdings:
if total_usd > 0:
holdings[symbol][class="hl-str">"actual_pct"] = (holdings[symbol][class="hl-str">"usd_value"] / total_usd) * 100
else:
holdings[symbol][class="hl-str">"actual_pct"] = 0.0
holdings[symbol][class="hl-str">"target_pct"] = TARGET_ALLOCATIONS[symbol]
holdings[symbol][class="hl-str">"drift"] = holdings[symbol][class="hl-str">"actual_pct"] - holdings[symbol][class="hl-str">"target_pct"]
return holdings, total_usd
def print_allocation_table(holdings, label):
class="hl-str">""class="hl-str">"Print a formatted allocation table."class="hl-str">""
print(fclass="hl-str">"\n{class="hl-str">'=' * 55}")
print(fclass="hl-str">" {label}")
print(fclass="hl-str">"{class="hl-str">'=' * 55}")
print(fclass="hl-str">" {class="hl-str">'Token':<8} {class="hl-str">'Balance':>10} {class="hl-str">'USD Value':>12} {class="hl-str">'Actual':>8} {class="hl-str">'Target':>8}")
print(fclass="hl-str">" {class="hl-str">'-' * 50}")
for symbol, data in sorted(holdings.items()):
print(
fclass="hl-str">" {symbol:<8} {data[class="hl-str">'balance']:>10.4f} "
fclass="hl-str">"${data[class="hl-str">'usd_value']:>10.2f} "
fclass="hl-str">"{data[class="hl-str">'actual_pct']:>7.1f}% "
fclass="hl-str">"{data[class="hl-str">'target_pct']:>7.1f}%"
)
print()
def generate_swap_plan(holdings, total_usd, prices):
class="hl-str">""class="hl-str">"Generate the minimum set of swaps to rebalance.
Strategy: sell overweight tokens for USDC, then buy underweight tokens with USDC.
If USDC is one of the target tokens, handle it directly.
"class="hl-str">""
sells = [] class=class="hl-str">"hl-comment"># (symbol, usd_amount_to_sell)
buys = [] class=class="hl-str">"hl-comment"># (symbol, usd_amount_to_buy)
for symbol, data in holdings.items():
drift = data[class="hl-str">"drift"]
if abs(drift) < DRIFT_THRESHOLD:
continue
usd_delta = (drift / 100) * total_usd
if drift > 0:
class=class="hl-str">"hl-comment"># Overweight — need to sell
price = prices[symbol][class="hl-str">"usd"]
token_amount = usd_delta / price
sells.append((symbol, token_amount, usd_delta))
else:
class=class="hl-str">"hl-comment"># Underweight — need to buy
buys.append((symbol, abs(usd_delta)))
return sells, buys
def main():
api_key = os.environ.get(class="hl-str">"SUWAPPU_API_KEY")
if not api_key:
print(class="hl-str">"Error: Set SUWAPPU_API_KEY environment variable.")
print(class="hl-str">" export SUWAPPU_API_KEY=suwappu_sk_your_api_key")
sys.exit(1)
wallet_address = os.environ.get(class="hl-str">"WALLET_ADDRESS")
if not wallet_address:
print(class="hl-str">"Error: Set WALLET_ADDRESS environment variable.")
print(class="hl-str">" export WALLET_ADDRESS=0xYourWalletAddress")
sys.exit(1)
headers = {class="hl-str">"Authorization": fclass="hl-str">"Bearer {api_key}"}
symbols = list(TARGET_ALLOCATIONS.keys())
class=class="hl-str">"hl-comment"># Step 1: Fetch portfolio
print(class="hl-str">"Fetching portfolio...")
portfolio = get_portfolio(headers, wallet_address)
balances = portfolio[class="hl-str">"balances"]
class=class="hl-str">"hl-comment"># Step 2: Fetch prices
print(class="hl-str">"Fetching prices...")
prices = get_prices(headers, symbols)
class=class="hl-str">"hl-comment"># Step 3: Calculate allocations
holdings, total_usd = calculate_allocations(balances, prices)
if total_usd == 0:
print(class="hl-str">"Portfolio is empty. Fund your wallet first.")
sys.exit(0)
print(fclass="hl-str">"Total portfolio value: ${total_usd:,.2f}")
print_allocation_table(holdings, class="hl-str">"BEFORE Rebalance")
class=class="hl-str">"hl-comment"># Step 4: Generate swap plan
sells, buys = generate_swap_plan(holdings, total_usd, prices)
if not sells and not buys:
print(class="hl-str">"Portfolio is within threshold. No rebalancing needed.")
sys.exit(0)
class=class="hl-str">"hl-comment"># Step 5: Print and confirm swap plan
print(class="hl-str">"Rebalance plan:")
for symbol, amount, usd in sells:
print(fclass="hl-str">" SELL {amount:.6f} {symbol} (~${usd:,.2f})")
for symbol, usd in buys:
print(fclass="hl-str">" BUY ~${usd:,.2f} worth of {symbol}")
print()
class=class="hl-str">"hl-comment"># Step 6: Execute sells (overweight → USDC)
for symbol, amount, usd in sells:
if symbol == class="hl-str">"USDC":
continue
print(fclass="hl-str">" Swapping {amount:.6f} {symbol} → USDC...")
quote = get_quote(headers, symbol, class="hl-str">"USDC", round(amount, 6))
print(fclass="hl-str">" Quote: {quote[class="hl-str">'amount_in']} {symbol} → {quote[class="hl-str">'amount_out']} USDC")
swap = execute_swap(headers, quote[class="hl-str">"quote_id"])
wait_for_swap(headers, swap[class="hl-str">"swap_id"])
class=class="hl-str">"hl-comment"># Step 7: Execute buys (USDC → underweight tokens)
for symbol, usd in buys:
if symbol == class="hl-str">"USDC":
continue
print(fclass="hl-str">" Swapping ~${usd:,.2f} USDC → {symbol}...")
usdc_amount = round(usd, 2)
quote = get_quote(headers, class="hl-str">"USDC", symbol, usdc_amount)
print(fclass="hl-str">" Quote: {quote[class="hl-str">'amount_in']} USDC → {quote[class="hl-str">'amount_out']} {symbol}")
swap = execute_swap(headers, quote[class="hl-str">"quote_id"])
wait_for_swap(headers, swap[class="hl-str">"swap_id"])
class=class="hl-str">"hl-comment"># Step 8: Fetch updated portfolio and print results
print(class="hl-str">"\nFetching updated portfolio...")
updated = get_portfolio(headers, wallet_address)
updated_prices = get_prices(headers, symbols)
updated_holdings, updated_total = calculate_allocations(updated[class="hl-str">"balances"], updated_prices)
print_allocation_table(updated_holdings, class="hl-str">"AFTER Rebalance")
print(class="hl-str">"Rebalancing complete.")
if __name__ == class="hl-str">"__main__":
main()
Running the Python Version
-str">"hl-comment"># Install dependencies
-kw">pip install requests
-str">"hl-comment"># Set environment variables
-kw">export SUWAPPU_API_KEY=suwappu_sk_your_api_key
-kw">export WALLET_ADDRESS=0xYourWalletAddress
-str">"hl-comment"># Run the rebalancer
python portfolio_rebalancer.py
---
TypeScript Version
class=class="hl-str">"hl-comment">#!/usr/bin/env npx tsx
/**
* Suwappu Portfolio Rebalancer — TypeScript
* Reads your portfolio, compares to target allocations, and swaps to rebalance.
*/
const BASE_URL = class="hl-str">"https:class="hl-commentclass="hl-str">">//api.suwappu.bot/v1/agent";
class=class="hl-str">"hl-comment">// --- Configuration ---
const TARGET_ALLOCATIONS: Record<string, number> = {
ETH: 50,
USDC: 30,
WBTC: 20,
};
const CHAIN = class="hl-str">"ethereum";
const DRIFT_THRESHOLD = 5.0;
const SLIPPAGE = 0.01;
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
interface Balance {
symbol: string;
chain: string;
balance: string;
usd_value: number;
}
interface Holding {
balance: number;
usd_value: number;
actual_pct: number;
target_pct: number;
drift: number;
}
async function api(
path: string,
options: RequestInit & { params?: Record<string, string> } = {}
) {
const apiKey = process.env.SUWAPPU_API_KEY!;
const { params, ...fetchOptions } = options;
let url = ${BASE_URL}${path};
if (params) url += ?${new URLSearchParams(params)};
const headers: Record<string, string> = {
class="hl-str">"Content-Type": class="hl-str">"application/json",
Authorization: Bearer ${apiKey},
...(options.headers as Record<string, string>),
};
const response = await fetch(url, { ...fetchOptions, headers });
if (!response.ok) {
const text = await response.text();
throw new Error(HTTP ${response.status}: ${text});
}
return response.json();
}
async function getPortfolio(walletAddress: string): Promise<{ balances: Balance[]; total_usd: number }> {
return api(class="hl-str">"/portfolio", { params: { wallet_address: walletAddress, chain: CHAIN } });
}
async function getPrices(symbols: string[]): Promise<Record<string, { usd: number; change_24h: number }>> {
const data = await api(class="hl-str">"/prices", { params: { symbols: symbols.join(class="hl-str">",") } });
return data.prices;
}
async function getQuote(fromToken: string, toToken: string, amount: number) {
return api(class="hl-str">"/quote", {
method: class="hl-str">"POST",
body: JSON.stringify({
from_token: fromToken,
to_token: toToken,
amount: String(amount),
chain: CHAIN,
slippage: SLIPPAGE,
}),
});
}
async function executeSwap(quoteId: string) {
return api(class="hl-str">"/swap/execute", {
method: class="hl-str">"POST",
body: JSON.stringify({ quote_id: quoteId }),
});
}
async function waitForSwap(swapId: number): Promise<boolean> {
while (true) {
const status = await api(/swap/status/${swapId});
if (status.status === class="hl-str">"completed") {
console.log( ✓ Swap ${swapId} completed (tx: ${status.tx_hash}));
return true;
}
if (status.status === class="hl-str">"failed") {
console.log( ✗ Swap ${swapId} failed);
return false;
}
await sleep(5000);
}
}
function calculateAllocations(
balances: Balance[],
prices: Record<string, { usd: number }>
): { holdings: Record<string, Holding>; totalUsd: number } {
let totalUsd = 0;
const holdings: Record<string, Holding> = {};
for (const bal of balances) {
if (bal.symbol in TARGET_ALLOCATIONS) {
holdings[bal.symbol] = {
balance: parseFloat(bal.balance),
usd_value: bal.usd_value,
actual_pct: 0,
target_pct: TARGET_ALLOCATIONS[bal.symbol],
drift: 0,
};
totalUsd += bal.usd_value;
}
}
class=class="hl-str">"hl-comment">// Add missing target tokens
for (const symbol of Object.keys(TARGET_ALLOCATIONS)) {
if (!(symbol in holdings)) {
holdings[symbol] = {
balance: 0,
usd_value: 0,
actual_pct: 0,
target_pct: TARGET_ALLOCATIONS[symbol],
drift: 0,
};
}
}
class=class="hl-str">"hl-comment">// Calculate percentages
for (const symbol of Object.keys(holdings)) {
if (totalUsd > 0) {
holdings[symbol].actual_pct = (holdings[symbol].usd_value / totalUsd) * 100;
}
holdings[symbol].drift = holdings[symbol].actual_pct - holdings[symbol].target_pct;
}
return { holdings, totalUsd };
}
function printTable(holdings: Record<string, Holding>, label: string) {
console.log(\n${class="hl-str">"=".repeat(55)});
console.log( ${label});
console.log(${class="hl-str">"=".repeat(55)});
console.log(
${class="hl-str">"Token".padEnd(8)} ${class="hl-str">"Balance".padStart(10)} ${class="hl-str">"USD Value".padStart(12)} ${class="hl-str">"Actual".padStart(8)} ${class="hl-str">"Target".padStart(8)}
);
console.log( ${class="hl-str">"-".repeat(50)});
for (const symbol of Object.keys(holdings).sort()) {
const h = holdings[symbol];
console.log(
${symbol.padEnd(8)} ${h.balance.toFixed(4).padStart(10)} ${(class="hl-str">"$" + h.usd_value.toFixed(2)).padStart(12)} ${(h.actual_pct.toFixed(1) + class="hl-str">"%").padStart(8)} ${(h.target_pct.toFixed(1) + class="hl-str">"%").padStart(8)}
);
}
console.log();
}
async function main() {
const apiKey = process.env.SUWAPPU_API_KEY;
if (!apiKey) {
console.error(class="hl-str">"Error: Set SUWAPPU_API_KEY environment variable.");
process.exit(1);
}
const walletAddress = process.env.WALLET_ADDRESS;
if (!walletAddress) {
console.error(class="hl-str">"Error: Set WALLET_ADDRESS environment variable.");
process.exit(1);
}
const symbols = Object.keys(TARGET_ALLOCATIONS);
class=class="hl-str">"hl-comment">// Step 1: Fetch portfolio and prices
console.log(class="hl-str">"Fetching portfolio...");
const portfolio = await getPortfolio(walletAddress);
console.log(class="hl-str">"Fetching prices...");
const prices = await getPrices(symbols);
class=class="hl-str">"hl-comment">// Step 2: Calculate allocations
const { holdings, totalUsd } = calculateAllocations(portfolio.balances, prices);
if (totalUsd === 0) {
console.log(class="hl-str">"Portfolio is empty. Fund your wallet first.");
process.exit(0);
}
console.log(Total portfolio value: $${totalUsd.toLocaleString(class="hl-str">"en-US", { minimumFractionDigits: 2 })});
printTable(holdings, class="hl-str">"BEFORE Rebalance");
class=class="hl-str">"hl-comment">// Step 3: Generate swap plan
const sells: Array<{ symbol: string; amount: number; usd: number }> = [];
const buys: Array<{ symbol: string; usd: number }> = [];
for (const [symbol, data] of Object.entries(holdings)) {
if (Math.abs(data.drift) < DRIFT_THRESHOLD) continue;
const usdDelta = (data.drift / 100) * totalUsd;
if (data.drift > 0) {
const price = prices[symbol].usd;
sells.push({ symbol, amount: usdDelta / price, usd: usdDelta });
} else {
buys.push({ symbol, usd: Math.abs(usdDelta) });
}
}
if (sells.length === 0 && buys.length === 0) {
console.log(class="hl-str">"Portfolio is within threshold. No rebalancing needed.");
process.exit(0);
}
class=class="hl-str">"hl-comment">// Step 4: Print swap plan
console.log(class="hl-str">"Rebalance plan:");
for (const s of sells) console.log( SELL ${s.amount.toFixed(6)} ${s.symbol} (~$${s.usd.toFixed(2)}));
for (const b of buys) console.log( BUY ~$${b.usd.toFixed(2)} worth of ${b.symbol});
console.log();
class=class="hl-str">"hl-comment">// Step 5: Execute sells (overweight → USDC)
for (const s of sells) {
if (s.symbol === class="hl-str">"USDC") continue;
console.log( Swapping ${s.amount.toFixed(6)} ${s.symbol} → USDC...);
const quote = await getQuote(s.symbol, class="hl-str">"USDC", parseFloat(s.amount.toFixed(6)));
console.log( Quote: ${quote.amount_in} ${s.symbol} → ${quote.amount_out} USDC);
const swap = await executeSwap(quote.quote_id);
await waitForSwap(swap.swap_id);
}
class=class="hl-str">"hl-comment">// Step 6: Execute buys (USDC → underweight)
for (const b of buys) {
if (b.symbol === class="hl-str">"USDC") continue;
console.log( Swapping ~$${b.usd.toFixed(2)} USDC → ${b.symbol}...);
const quote = await getQuote(class="hl-str">"USDC", b.symbol, parseFloat(b.usd.toFixed(2)));
console.log( Quote: ${quote.amount_in} USDC → ${quote.amount_out} ${b.symbol});
const swap = await executeSwap(quote.quote_id);
await waitForSwap(swap.swap_id);
}
class=class="hl-str">"hl-comment">// Step 7: Print updated allocations
console.log(class="hl-str">"\nFetching updated portfolio...");
const updated = await getPortfolio(walletAddress);
const updatedPrices = await getPrices(symbols);
const { holdings: updatedHoldings } = calculateAllocations(updated.balances, updatedPrices);
printTable(updatedHoldings, class="hl-str">"AFTER Rebalance");
console.log(class="hl-str">"Rebalancing complete.");
}
main().catch(console.error);
Running the TypeScript Version
-str">"hl-comment"># Install tsx for running TypeScript directly
-kw">npm install -g tsx
-str">"hl-comment"># Set environment variables
-kw">export SUWAPPU_API_KEY=suwappu_sk_your_api_key
-kw">export WALLET_ADDRESS=0xYourWalletAddress
-str">"hl-comment"># Run the rebalancer
npx tsx portfolio_rebalancer.ts
---
Customization Tips
Change Target Allocations
Edit the TARGET_ALLOCATIONS dictionary to match your desired portfolio. Percentages must sum to 100:
TARGET_ALLOCATIONS = {
class="hl-str">"ETH": 40,
class="hl-str">"USDC": 20,
class="hl-str">"WBTC": 15,
class="hl-str">"SOL": 15,
class="hl-str">"ARB": 10,
}
Adjust the Drift Threshold
Lower the threshold to rebalance more aggressively, or raise it to reduce swap frequency and fees:
DRIFT_THRESHOLD = 2.0 class=class="hl-str">"hl-comment"># Rebalance when >2% off target (more frequent)
DRIFT_THRESHOLD = 10.0 class=class="hl-str">"hl-comment"># Rebalance when >10% off target (less frequent)
Multi-Chain Rebalancing
To rebalance across multiple chains, remove the chain filter from the portfolio call and group swaps by chain:
class=class="hl-str">"hl-comment"># Fetch across all chains
portfolio = get_portfolio(headers, wallet_address)
class=class="hl-str">"hl-comment"># Then group balances by chain and rebalance each chain independently
Schedule with Cron
Run the rebalancer daily or weekly:
-str">"hl-comment"># Rebalance every day at 9am UTC
0 9 * * * SUWAPPU_API_KEY=suwappu_sk_... WALLET_ADDRESS=0x... python /path/to/portfolio_rebalancer.py >> /var/log/rebalancer.log 2>&1