Skip to content

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