Portfolio Management
Overview
The Portfolio Management system handles rebalancing operations to maintain target asset allocations across the trading portfolio. It implements a two-phase rebalancing algorithm that respects capital constraints, minimum trade sizes, and slippage thresholds.
The system converts raw account state into allocation percentages, calculates deltas between current and target allocations, and generates an ordered sequence of trades to achieve the desired portfolio composition.
Architecture Diagram
Key Concepts
PortfolioState
Current portfolio snapshot with allocation percentages for each asset. Includes total portfolio value, available balance, position details, and timestamp.
TargetAllocation
Desired portfolio composition expressed as percentages (0.0 to 1.0) for each asset. Must sum to approximately 1.0 to be valid.
RebalancingPlan
Ordered sequence of trade actions to move from current to target allocation, including estimated costs and reasoning.
Two-Phase Rebalancing
- Phase 1 (Close/Reduce): Sell overweight positions to generate capital
- Phase 2 (Open/Increase): Buy underweight positions using available capital
Rebalancing Algorithm
Algorithm Overview
The rebalancing algorithm follows a capital-constrained approach:
- Calculate Deltas: Compute difference between target and current allocation for each asset
- Filter Significant Deviations: Only rebalance assets with deviation ≥ threshold (default 5%)
- Phase 1 - Generate Capital: Close or reduce overweight positions first
- Phase 2 - Deploy Capital: Open or increase underweight positions using freed capital
Two-Phase Execution
Phase 1: Close/Reduce Overweight Positions
# Pseudocode for Phase 1
for coin in sorted_by_delta_ascending:
if delta >= 0: # Skip underweight
continue
current_value = current_allocation * total_value
target_value = target_allocation * total_value
reduce_value = current_value - target_value
if reduce_value >= min_trade_value:
if target_allocation == 0:
action = close_position(coin)
else:
action = sell_partial(coin, reduce_value)
actions.append(action)
available_capital += reduce_valueWhy Phase 1 First?
- Generates capital needed for Phase 2
- Reduces risk exposure before adding new positions
- Ensures we have funds available for buys
Phase 2: Open/Increase Underweight Positions
# Pseudocode for Phase 2
available_capital = initial_balance + phase1_proceeds
for coin in sorted_by_delta_descending:
if delta <= 0: # Skip overweight
continue
target_value = target_allocation * total_value
current_value = current_allocation * total_value
increase_value = target_value - current_value
# Capital constraint
increase_value = min(increase_value, available_capital)
if increase_value >= min_trade_value:
action = buy(coin, increase_value)
actions.append(action)
available_capital -= increase_valueCapital Constraints:
- Cannot spend more than available balance + Phase 1 proceeds
- Each buy reduces available capital for subsequent buys
- Skips trades below minimum trade value
Rebalancing Flow Diagram
Configuration Parameters
The PortfolioRebalancer accepts three key parameters:
rebalancer = PortfolioRebalancer(
min_trade_value=10.0, # Minimum trade size in USD
max_slippage_pct=0.005, # 0.5% max slippage
rebalance_threshold=0.05 # 5% deviation triggers rebalance
)Parameter Descriptions:
min_trade_value: Minimum USD value for a trade to execute. Prevents dust trades that waste fees.max_slippage_pct: Maximum acceptable slippage percentage (currently informational, not enforced).rebalance_threshold: Minimum allocation deviation to trigger rebalancing. Prevents excessive trading on small deviations.
Target Allocation Calculation
Allocation Percentages
Target allocations are expressed as percentages (0.0 to 1.0) that must sum to approximately 1.0:
target = TargetAllocation(
allocations={
"BTC": 0.40, # 40% Bitcoin
"ETH": 0.30, # 30% Ethereum
"SOL": 0.20, # 20% Solana
"USDC": 0.10 # 10% Cash
},
strategy_id="balanced_portfolio",
reasoning="Diversified allocation across major assets"
)
# Validation
assert target.validate() # Checks sum is between 0.99 and 1.01Delta Calculation
Deltas represent the difference between target and current allocations:
delta = target_percentage - current_percentageDelta Interpretation:
- Positive delta: Underweight position, need to buy
- Negative delta: Overweight position, need to sell
- Zero delta: At target, no action needed
Example:
# Current state
current = {
"BTC": 0.50, # 50% (overweight by 10%)
"ETH": 0.25, # 25% (underweight by 5%)
"SOL": 0.15, # 15% (underweight by 5%)
"USDC": 0.10 # 10% (at target)
}
# Target state
target = {
"BTC": 0.40,
"ETH": 0.30,
"SOL": 0.20,
"USDC": 0.10
}
# Calculated deltas
deltas = {
"BTC": -0.10, # Sell 10% of portfolio value
"ETH": +0.05, # Buy 5% of portfolio value
"SOL": +0.05, # Buy 5% of portfolio value
"USDC": 0.00 # No change
}Filtering Significant Deviations
Only deltas exceeding the rebalance threshold trigger trades:
rebalance_threshold = 0.05 # 5%
significant_deltas = {
coin: delta
for coin, delta in deltas.items()
if abs(delta) >= rebalance_threshold
}
# Result: Only BTC, ETH, SOL rebalanced (all at 5%+ deviation)
# USDC skipped (0% deviation)Portfolio State Management
PortfolioState Dataclass
The PortfolioState dataclass represents the current portfolio snapshot:
@dataclass
class PortfolioState:
total_value: float # Total portfolio value in USD
available_balance: float # Available USDC balance
allocations: dict[str, float] # coin -> current percentage
positions: dict[str, Position] # coin -> Position object
timestamp: float # State timestampConversion from AccountState
The from_account_state class method converts raw account data into allocation percentages:
portfolio_state = PortfolioState.from_account_state(account_state)Conversion Logic:
- Calculate Total Value: Use portfolio value, or available balance if portfolio is empty
- Build Position Map: Create dictionary of coin → Position
- Calculate USDC Allocation: Sum of perp margin + spot USDC balance
- Calculate Position Allocations: For each position, compute
(size × price) / total_value
Example:
# Input: AccountState
account_state = AccountState(
portfolio_value=10000.0,
available_balance=2000.0, # Perp margin
spot_balances={"USDC": 1000.0},
positions=[
Position(coin="BTC", size=0.1, current_price=50000.0, ...),
Position(coin="ETH", size=2.0, current_price=2500.0, ...)
],
timestamp=1234567890.0
)
# Output: PortfolioState
portfolio_state = PortfolioState(
total_value=10000.0,
available_balance=2000.0,
allocations={
"USDC": 0.30, # (2000 + 1000) / 10000 = 30%
"BTC": 0.50, # (0.1 × 50000) / 10000 = 50%
"ETH": 0.20 # (2.0 × 2500) / 10000 = 20%
},
positions={
"BTC": Position(...),
"ETH": Position(...)
},
timestamp=1234567890.0
)Usage Example
from hyperliquid_agent.monitor import AccountState
from hyperliquid_agent.portfolio import PortfolioState, TargetAllocation, PortfolioRebalancer
# Get current account state from monitor
account_state = monitor.get_account_state()
# Convert to portfolio state with allocations
current = PortfolioState.from_account_state(account_state)
# Define target allocation
target = TargetAllocation(
allocations={
"BTC": 0.40,
"ETH": 0.30,
"SOL": 0.20,
"USDC": 0.10
}
)
# Create rebalancing plan
rebalancer = PortfolioRebalancer(min_trade_value=10.0)
plan = rebalancer.create_rebalancing_plan(current, target)
# Execute plan
for action in plan.actions:
executor.execute_action(action)Trade Ordering and Optimization
Minimum Trade Value
The min_trade_value parameter prevents dust trades that waste fees:
min_trade_value = 10.0 # USD
# Trade skipped if value < $10
if trade_value < min_trade_value:
continue # Skip this tradeWhy Minimum Trade Value?
- Trading fees make small trades unprofitable
- Reduces API calls and execution overhead
- Prevents portfolio fragmentation
Recommended Values:
- Conservative: $50+ (fewer trades, lower fees)
- Moderate: $10-50 (balanced approach)
- Aggressive: $5-10 (more precise rebalancing)
Slippage Thresholds
The max_slippage_pct parameter defines acceptable slippage:
max_slippage_pct = 0.005 # 0.5%Note: Currently informational only. Future versions may:
- Use limit orders instead of market orders
- Split large orders to reduce impact
- Monitor actual slippage and adjust
Capital Constraints
The rebalancer respects capital availability:
- Initial Capital: Available balance at start
- Phase 1 Proceeds: Capital freed from closing positions
- Sequential Allocation: Each buy reduces available capital
# Example capital flow
initial_balance = 1000.0
phase1_proceeds = 2000.0 # From selling overweight positions
available_capital = initial_balance + phase1_proceeds # 3000.0
# Buy 1: BTC for $1500
available_capital -= 1500.0 # Now 1500.0
# Buy 2: ETH for $1000
available_capital -= 1000.0 # Now 500.0
# Buy 3: SOL for $800 - SKIPPED (insufficient capital)Trade Ordering Strategy
Phase 1 Ordering: Most overweight first (ascending delta)
- Maximizes capital generation early
- Reduces risk exposure quickly
- Ensures funds available for Phase 2
Phase 2 Ordering: Most underweight first (descending delta)
- Prioritizes largest allocation gaps
- Uses available capital efficiently
- May not fill all targets if capital limited
Optimization Tips
Increase Rebalance Threshold: Reduce trading frequency
pythonrebalancer = PortfolioRebalancer(rebalance_threshold=0.10) # 10%Raise Minimum Trade Value: Reduce small trades
pythonrebalancer = PortfolioRebalancer(min_trade_value=50.0) # $50Batch Rebalancing: Rebalance on schedule, not continuously
python# Rebalance once per day instead of every loop if time_since_last_rebalance > 86400: # 24 hours plan = rebalancer.create_rebalancing_plan(current, target)Monitor Estimated Costs: Skip rebalancing if costs exceed benefit
pythonplan = rebalancer.create_rebalancing_plan(current, target) if plan.estimated_cost > 0.01 * current.total_value: # 1% of portfolio # Skip rebalancing, costs too high pass
Error Handling
Invalid Target Allocation
target = TargetAllocation(allocations={"BTC": 0.50, "ETH": 0.30}) # Sum = 0.80
plan = rebalancer.create_rebalancing_plan(current, target)
# Returns empty plan with reasoning:
# "Invalid target allocation (does not sum to 1.0)"No Significant Deviations
# Current and target are very close
plan = rebalancer.create_rebalancing_plan(current, target)
# Returns empty plan with reasoning:
# "No significant deviations from target allocation"Missing Price Data
# Trying to buy a coin not currently held
# Returns plan with reasoning:
# "Skipped SOL: no price data available for new position"Workaround: Ensure price data is available for all target coins before rebalancing.
Insufficient Capital
# Phase 2 may not complete all buys if capital runs out
# Actions are generated until available_capital < min_trade_value
# Remaining underweight positions are skippedPerformance Considerations
Computational Complexity
- Delta Calculation: O(n) where n = number of unique coins
- Sorting: O(n log n) for ordering trades
- Trade Generation: O(n) for iterating through deltas
- Overall: O(n log n) - efficient for typical portfolio sizes (5-50 coins)
Memory Usage
- Minimal: Only stores current state, target, and action list
- Scales linearly with number of positions
API Call Optimization
- Rebalancing plan generation requires no API calls
- Only execution phase makes API calls (one per trade action)
- Batch execution can reduce latency
Related Documentation
- Trade Executor - Executes rebalancing actions
- Position Monitor - Provides AccountState
- Configuration Guide - Portfolio settings
- Governance System - Strategy-driven allocation
Example: Complete Rebalancing Flow
from hyperliquid_agent.monitor import PositionMonitor
from hyperliquid_agent.portfolio import PortfolioState, TargetAllocation, PortfolioRebalancer
from hyperliquid_agent.executor import TradeExecutor
# Initialize components
monitor = PositionMonitor(config)
rebalancer = PortfolioRebalancer(
min_trade_value=10.0,
rebalance_threshold=0.05
)
executor = TradeExecutor(config, registry)
# Get current state
account_state = monitor.get_account_state()
current = PortfolioState.from_account_state(account_state)
print(f"Current allocations: {current.allocations}")
# Output: {'USDC': 0.30, 'BTC': 0.50, 'ETH': 0.20}
# Define target
target = TargetAllocation(
allocations={
"BTC": 0.40,
"ETH": 0.30,
"SOL": 0.20,
"USDC": 0.10
},
strategy_id="balanced",
reasoning="Diversified allocation with SOL exposure"
)
# Create rebalancing plan
plan = rebalancer.create_rebalancing_plan(current, target, market_type="perp")
print(f"Actions: {len(plan.actions)}")
print(f"Estimated cost: ${plan.estimated_cost:.2f}")
print(f"Reasoning: {plan.reasoning}")
# Execute plan
if plan.actions:
for action in plan.actions:
print(f"Executing: {action.action_type} {action.size} {action.coin}")
result = executor.execute_action(action)
if not result.success:
print(f"Failed: {result.error}")
break
else:
print("No rebalancing needed")Expected Output:
Current allocations: {'USDC': 0.30, 'BTC': 0.50, 'ETH': 0.20}
Actions: 3
Estimated cost: $1.50
Reasoning: Reduce BTC: 50.0% → 40.0%; Increase ETH: 20.0% → 30.0%; Increase SOL: 0.0% → 20.0%
Executing: sell 0.02 BTC
Executing: buy 2.0 ETH
Executing: buy 10.0 SOL