Oracle, built by Double Finance, is a sophisticated portfolio optimization engine that tax loss harvests and intelligently rebalances portfolios while considering tax implications, trading costs, and various constraints placed upon portfolios (holding time restrictions, etc). It's designed to help investors maintain their target asset allocations while minimizing tax burdens and transaction costs.
We use this in production at Double Finance for daily Tax Loss Harvesting and Automated Rebalancing. Oracle simply returns optimal trades to make for a portfolio - it does not execute anything. You must bring your own price and target/index data. Oracle does not fetch anything from the internet. It allows for customization and control regarding how aggressive you want to be along pretty much every axis.
It provides the basics of a complete portfolio optimization engine similar to:
- Rowboat Advisors (Acquired by Betterment May 2025)
- Smartleaf
- AlphaThena
- BlackRock's Tax-Managed Equity SMA by Aperio
- AdvisorArch (Acquired by Apex 2024)
- Parti Pris
- Tax Loss Harvesting: Ability to specify a threshold at which to TLH a given security, while respecting wash sale rules.
- Tax-Aware Rebalancing: Optimizes trades to minimize tax impact while reducing drift
- Multi-Asset Support: Can handle any asset type (stocks, etfs, mutual funds, crypto)
- Consider Trading Costs: Considers spreads/trading costs while recommending trades.
- Factor Model Rebalancing Integration: Supports considering a factor model for use the Direct Indexed based TLH
- Tax Lot Management: Tracks and optimizes individual tax lots.
- Wash Sale Prevention: Built-in protection against wash sales
- Stock Restrictions: Can restrict buying/selling of specific securities
- Holding Time Restrictions: Enforces minimum holding periods before selling tax lots
- ESG Tilts: No support for ESG scoring or ESG-based portfolio tilts
- Household Level Optimization: Cannot optimize across multiple accounts in a household
- Sector Constraints: No support for sector exposure limits or constraints
Oracle supports several optimization types for any given strategy through the OracleOptimizationType enum:
- HOLD: No trading allowed, maintains current positions
- BUY_ONLY: Only allows buy trades, no sells permitted
- TAX_UNAWARE: Rebalances towards targets ignoring tax implications
- TAX_AWARE: Rebalances considering tax implications (default)
- PAIRS_TLH: Tax-Loss Harvesting specifically for pairs trading
- DIRECT_INDEX: Direct Indexing strategy with factor model considerations
Each optimization type has specific behaviors:
- TLH Support: Only PAIRS_TLH and DIRECT_INDEX support tax-loss harvesting
- Sell Restrictions: BUY_ONLY and HOLD types don't allow sell trades
- Withdrawal Support: All types except HOLD and BUY_ONLY can handle portfolio withdrawals
- Weight Adjustments:
- TAX_UNAWARE: Ignores tax implications
- HOLD: Weights don't matter as no trading occurs
- DIRECT_INDEX: Can prioritize factor model considerations
- Others: Use standard tax-aware optimization
The compute_optimal_trades_for_all_strategies method returns a tuple of (results, netted_trades):
The optimizer uses several key parameters to control its behavior:
These parameters control how much the optimizer "cares" about different aspects of the optimization:
- weight_tax: How much to prioritize tax efficiency (e.g., harvesting losses)
- weight_drift: How much to prioritize staying close to target weights
- weight_transaction: How much to prioritize minimizing transaction costs
- weight_factor_model: How much to prioritize factor model alignment (for DIRECT_INDEX)
- weight_cash_drag: How much to penalize excess cash positions
These control when trades are triggered:
- rebalance_threshold: Minimum deviation from target weight to trigger a rebalance (e.g., 0.001 = 0.1%)
- buy_threshold: Minimum deviation to trigger a buy-only trade (typically lower than rebalance_threshold)
- tlh_min_loss_threshold: Minimum loss percentage to consider for tax-loss harvesting (e.g., 0.015 = 1.5%)
Parameters that control trade execution:
- min_notional: Minimum trade size in dollars (e.g., 5.0 = $5 minimum trade)
- trade_rounding: Number of decimal places to round trade quantities
- range_min_weight_multiplier: Minimum allowed weight as a fraction of target (e.g., 0.5 = 50% of target)
- range_max_weight_multiplier: Maximum allowed weight as a fraction of target (e.g., 2.0 = 200% of target)
- rank_penalty_factor: Penalty for deviating from factor model rankings (for PAIRS_TLH)
When using the PAIRS_TLH optimization type, you can specify pairs of securities that can be used as tax-loss harvesting alternatives for each other. The pairs are specified in the identifiers array of your targets DataFrame, where the order matters:
In this example:
- ['SPY', 'IVV', 'VOO'] forms one group of interchangeable securities
- ['VTI', 'ITOT', 'SCHB'] forms another group
- Within each group, earlier securities are preferred over later ones (controlled by rank_penalty_factor)
The rank_penalty_factor parameter (default 0.0) controls how strongly the optimizer prefers securities earlier in each identifiers list:
- 0.0: No preference, treats all securities in a group equally
- 0.00001: Mild preference for earlier securities
- 0.0001: Strong preference for earlier securities
- 0.001: Very strong preference for earlier securities
For example, with a high rank_penalty_factor:
- The optimizer will prefer to hold SPY over IVV or VOO
- When tax-loss harvesting SPY, it will prefer to switch to IVV before considering VOO
- The preference strength increases with the rank penalty factor
This ranking system helps maintain a preference hierarchy while still allowing flexibility for tax-loss harvesting opportunities.
The optimizer returns both individual strategy trades and netted trades. This is important because:
- Each strategy is optimized independently to maintain separation of concerns
- Multiple strategies might want to trade the same security
- Netted trades combine all strategy trades to show the final, consolidated trades needed
For example, if Strategy A wants to buy 100 shares of AAPL and Strategy B wants to sell 50 shares of AAPL, the netted trades would show a single buy of 50 shares of AAPL.
Oracle is designed to run as an AWS Lambda function. The deployment process:
- Builds a Docker container with all dependencies
- Packages the code and dependencies
- Deploys to AWS Lambda
To deploy:
The Lambda function expects input in the following format:
Oracle uses multiple objective terms in its optimization, each weighted according to the strategy's needs:
- Minimizes realized capital gains/losses
- Considers short-term vs long-term tax implications
- Supports tax-loss harvesting (TLH) in PAIRS_TLH and DIRECT_INDEX modes
- Includes wash sale prevention logic
- Minimizes deviation from target asset allocations
- Uses vectorized calculations for performance
- Supports rank-based penalties for preferred securities
- Accounts for bid-ask spreads
- Minimizes trading costs
- Considers market impact
- Normalized by total portfolio value
- Used in DIRECT_INDEX optimization type
- Aligns portfolio with target factor exposures
- Supports piecewise linear approximation
- Minimizes cash drag when no withdrawal is planned
- Optimizes cash utilization
- Balances cash needs with investment opportunities
- Considers minimum cash requirements
- Special objective for withdrawal scenarios
- Optimizes tax efficiency of withdrawals
- Maintains target allocations during withdrawals
- Considers transaction costs in withdrawal execution
Oracle enforces various constraints to ensure portfolio validity and meet specific requirements:
- Maintains minimum cash balance
- Ensures sufficient cash for withdrawals
- Validates cash flow from trades
- Prevents negative cash positions
- Minimum notional amount for trades
- No simultaneous buys/sells of same security
- Buy-only or sell-only restrictions
- Trade size rounding requirements
- Enforces minimum holding periods
- Prevents premature tax lot sales
- Considers tax implications
- Supports tax-aware trading
- Individual security trading restrictions
- Wash sale prevention rules
- Regulatory compliance checks
- Custom trading rules
- Minimum weight multiplier (e.g., 50% of target)
- Maximum weight multiplier (e.g., 200% of target)
- Asset class drift limits
- Special handling for PAIRS_TLH and DIRECT_INDEX
Each constraint can be customized through the settings dictionary when calling compute_optimal_trades_for_all_strategies:
MIT License. See LICENSE.md for details.
.png)
![Better Data Is All You Need [video]](https://www.youtube.com/img/desktop/supported_browsers/opera.png)
