Skip to content

Preview rebalance trades for a symphony before actually executing.

"""
Preview rebalance trades for a symphony before actually executing.
This helps you understand what trades will be made without placing them.
"""

from composer import ComposerClient
from composer.http_client import ComposerAPIError
import json
import os
from dotenv import load_dotenv

load_dotenv()

# Initialize client
client = ComposerClient(
    api_key=os.getenv("COMPOSER_API_KEY"),
    api_secret=os.getenv("COMPOSER_API_SECRET"),
)

# Get first account and symphony (same logic as symphony_holdings example)
all_accounts = client.accounts.list()
first_account = all_accounts.accounts[0]

all_symphonies = client.portfolio.get_symphony_stats_meta(first_account.account_uuid)
first_symphony = all_symphonies.symphonies[0]

symphony_id = first_symphony.id
symphony_name = first_symphony.name or symphony_id

print(f"Previewing rebalance for: {symphony_name}")
print(f"Symphony ID: {symphony_id}")

# Get trade preview with error handling
try:
    preview = client.dry_run.create_trade_preview(
        symphony_id,
        broker_account_uuid=first_account.account_uuid
    )

    print("="*60)
    print("REBALANCE PREVIEW SUMMARY")
    print("="*60)

    print(f"Symphony Name: {preview.symphony_name}")
    print(f"Current Value: ${preview.symphony_value:,.2f}")
    print(f"Needs Rebalance: {'Yes' if preview.rebalanced else 'No'}")
    print(f"Next Rebalance After: {preview.next_rebalance_after}")
    print(f"Queued Cash Change: ${preview.queued_cash_change:,.2f}")

    if preview.rebalanced:
        print("\n" + "-"*60)
        print("RECOMMENDED TRADES")
        print("-"*60)

        # Display trades in a table format
        print(f"\n{'Symbol':<10} {'Name':<25} {'Side':<6} {'Shares':>10} {'Price':>12} {'Value Chg':>14}")
        print("-"*80)

        total_buy = 0
        total_sell = 0

        for trade in preview.recommended_trades:
            symbol = trade.symbol
            name = (trade.name[:22] + "...") if trade.name and len(trade.name) > 25 else (trade.name or "")
            side = trade.side.value
            shares = trade.share_change
            price = trade.average_price
            value = trade.cash_change

            print(f"{symbol:<10} {name:<25} {side:<6} {shares:>10} ${price:>11.2f} ${value:>13.2f}")

            if value > 0:
                total_buy += value
            else:
                total_sell += abs(value)

        print("-"*80)
        print(f"{'Total Buys:':<42} ${total_buy:>13.2f}")
        print(f"{'Total Sells:':<42} ${total_sell:>13.2f}")
        print(f"{'Net:':<42} ${total_buy - total_sell:>13.2f}")

        print("\n" + "-"*60)
        print("WEIGHT CHANGES")
        print("-"*60)

        # Show weight changes
        print(f"\n{'Symbol':<10} {'Previous':>12} {'Target':>12} {'Change':>12}")
        print("-"*50)

        for trade in preview.recommended_trades:
            symbol = trade.symbol
            prev_wt = trade.prev_weight * 100
            next_wt = trade.next_weight * 100
            wt_change = (trade.next_weight - trade.prev_weight) * 100

            print(f"{symbol:<10} {prev_wt:>11.2f}% {next_wt:>11.2f}% {wt_change:>+11.2f}%")
    else:
        print("\nNo rebalance needed at this time.")
        print("The symphony is within its target allocation corridors.")

    print("\n" + "="*60)
    print("NOTE: This is a preview only. No trades have been placed.")
    print("To execute, use: client.deploy.rebalance(symphony_id)")
    print("="*60)

except ComposerAPIError as e:
    # Try to parse the error response for specific error codes
    try:
        error_data = json.loads(str(e).replace("HTTP Error 400: ", ""))
        errors = error_data.get("errors", [])

        for error in errors:
            error_code = error.get("code", "")
            error_title = error.get("title", "")

            if error_code == "dry-run-markets-closed":
                print("\n" + "="*60)
                print("MARKETS CLOSED")
                print("="*60)
                print(f"\n{error_title}")
                print("\nTrade previews are only available during market hours.")
                print("For equity symphonies, try running this during market hours (9:30 AM - 4:00 PM ET).")
                print("For crypto symphonies, previews may be available outside market hours.")
                print("="*60)
                break
            else:
                # Re-raise if it's a different error
                print(f"\nAPI Error: {error_title}")
                raise
    except (json.JSONDecodeError, KeyError):
        # If we can't parse the error, re-raise
        raise

Output:

Previewing rebalance for: The Holy Grail (Invest Copy) (Buy Copy)
Symphony ID: SvfHiUshxXcHUfoCyvn5
============================================================
REBALANCE PREVIEW SUMMARY
============================================================
Symphony Name: The Holy Grail (Invest Copy) (Buy Copy)
Current Value: $12,054.15
Needs Rebalance: Yes
Next Rebalance After: 2026-02-17
Queued Cash Change: $0.00

------------------------------------------------------------
RECOMMENDED TRADES
------------------------------------------------------------

Symbol     Name                      Side       Shares        Price      Value Chg
--------------------------------------------------------------------------------
TQQQ       ProShares Trust - ProS... BUY    0.022593015 $      48.24 $        -1.09
$USD       Symphony Cash Remainder   SELL   -1.0899999999999999 $       1.00 $        -1.09
--------------------------------------------------------------------------------
Total Buys:                                $         0.00
Total Sells:                               $         2.18
Net:                                       $        -2.18

------------------------------------------------------------
WEIGHT CHANGES
------------------------------------------------------------

Symbol         Previous       Target       Change
--------------------------------------------------
TQQQ             99.89%       99.90%       +0.01%
$USD              0.11%        0.10%       -0.01%

============================================================
NOTE: This is a preview only. No trades have been placed.
To execute, use: client.deploy.rebalance(symphony_id)
============================================================
============================================================

Warning

If market is closed, it will throw an error

Output:

Previewing rebalance for: The Holy Grail (Invest Copy) (Buy Copy)
Symphony ID: SvfHiUshxXcHUfoCyvn5

============================================================
MARKETS CLOSED
============================================================

Markets are currently closed for equity symphonies.

Trade previews are only available during market hours.
For equity symphonies, try running this during market hours (9:30 AM - 4:00 PM ET).
For crypto symphonies, previews may be available outside market hours.
============================================================