This project uses Ktor and Kotlin Language, and it's intended to be just a playground project.
NOTE: This project is not a trading-bot nor a financial adviser tool, try it out on your own.
Dollar-Cost Averaging is an investment strategy where a person constantly buy more of its assets, (every month, or week, etc), in order to avoid trying to time the market.
From Investopedia:
Dollar-cost averaging (DCA) is an investment strategy in which an investor divides up the total amount to be invested across periodic purchases of a target asset in an effort to reduce the impact of volatility on the overall purchase. The purchases occur regardless of the asset's price and at regular intervals.
If you hold multiple assets, you probably should give a weight target for each on them on your portfolio.
The dca-optimizer tries to use some criteria to better distribute your dca investment calculation.
Some criteria are:
- The asset weight has to be smaller than its target.
- How far below the weight is from its target.
- The asset "distance" from its ATH (all-time high) or 52 weeks high how also often used for stocks, can be used to define if an asset will be invested.
Imagine a hypothetical portfolio with 5 assets:
| Ticker | Weight | Target | From ATH |
|---|---|---|---|
| A | 25.0% | 20.0% | 18.0% |
| B | 15.0% | 20.0% | 8.0% |
| C | 15.0% | 25.0% | 15.5% |
| D | 10.0% | 25.0% | 17.1% |
| F | 5.0% | 10.0% | 22.0% |
In a configuration where you define that the ATH threshold is 10% (only assets that are below this value will be invested):
- The first asset with ticker A, won't be invested, because it's over its target.
- The second asset with ticker B, won't be invested, because it's below the ATH threshold of 10%.
The point is to help balance out a portfolio with under/over weighted assets and minimize just a little buying assets that are very high in price currently. (against its ATH/52 weeks price)
This doesn't guarantee any significant portfolio performance on the long term, but it might do slightly better overall.
- WEIGHT: The current asset's weight distance from its target is used to determine the DCA distribution. (Assets with same target might get different results)
- TARGET: The asset's target is used to determine the DCA distribution, over-weighted assets are discarded. (Assets with same target will get the same result)
- PORTFOLIO: All assets will be invested, but over-weighted assets will have its target reduced and the difference is distributed among all under-target assets.
- RATING: All rated assets will be invested, ONLY the rating values will be used to calculate the distribution. (Think on a 5 stars rating system)
- LINEAR_PROGRAMMING: Uses mathematical optimization (linear programming) to find the optimal distribution that minimizes deviation from target weights while respecting constraints. Provides mathematically optimal solutions compared to heuristic approaches.
- Default mode (
diversify: false): Allocates all funds to the highest priority asset (corner solution) - Diversified mode (
diversify: true): Spreads funds proportionally across all eligible assets based on priority scores
- Default mode (
The LINEAR_PROGRAMMING strategy uses linear programming to optimize asset allocation based on priority scores:
Priority Calculation:
priority = (target_weight - current_weight) × target_weight
This formula prioritizes assets that are:
- Further below their target weight (higher deviation)
- Have higher target allocations in the portfolio
Two Operating Modes:
| Mode | Behavior | Use Case |
|---|---|---|
diversify: false |
Corner Solution - Linear programming allocates all funds to the single highest priority asset. Mathematically optimal for maximizing the objective function. | When you want aggressive rebalancing focused on the most underweight asset. Accepts concentrated positions. |
diversify: true |
Proportional Allocation - Distributes funds across all eligible assets proportionally to their priority scores: allocation_i = (priority_i / total_priority) × amount |
When you want balanced diversification while still respecting priority rankings. Spreads risk across multiple assets. |
Example Comparison:
Given three underweight assets:
- Asset A: priority = 70.0 (deviation=1.4, target=50%)
- Asset B: priority = 27.5 (deviation=2.5, target=11%)
- Asset C: priority = 18.0 (deviation=0.9, target=20%)
With $10,000 to invest:
diversify: false→ Asset A: $10,000, Asset B: $0, Asset C: $0diversify: true→ Asset A: $6,061.69, Asset B: $2,380.95, Asset C: $1,557.36
Why Corner Solutions Occur:
Linear programming with linear objectives always finds optimal solutions at the corner points (vertices) of the feasible region. Since all funds must be allocated and the asset with the highest priority coefficient maximizes the objective function, the optimal solution is to allocate everything to that asset. This is mathematically correct but results in concentrated positions.
Smart Concentration Cap (maxSingleAssetPct):
To prevent extreme concentration in diversify: false mode, the optimizer includes smart capping logic that automatically limits single-asset allocation to 90% in specific scenarios:
When Smart Cap Applies (automatically set to 90%):
- Three or more eligible assets - Multiple opportunities exist for diversification
- Close priority competition - Top two assets have similar priorities (second within 50% of first)
- Small deviations - All assets within 5% of their targets (minor rebalancing only)
When Smart Cap Does NOT Apply (100% allowed):
- Single eligible asset - No choice available
- Clear winner - One asset has significantly higher priority (2x or more than second)
- Large deviations - At least one asset is 5%+ below target (aggressive rebalancing needed)
Manual Override:
You can explicitly control the cap via maxSingleAssetPct:
{
"strategy": {
"type": "LINEAR_PROGRAMMING",
"diversify": false,
"maxSingleAssetPct": 0.75 // Force 75% cap
}
}maxSingleAssetPct: 0.75→ 75% maximum allocation to any single assetmaxSingleAssetPct: 1.0→ 100% allowed (disables smart cap entirely)maxSingleAssetPct: null→ Use smart cap logic (default)
While diversify: true mode spreads investments across assets, it differs from other strategies in how it calculates allocation priorities.
WEIGHT Strategy - Pure deviation-based allocation:
allocation_factor = (target - weight) / Σ(all_deviations)
This strategy only considers how far each asset is from its target, treating all deviations equally.
LINEAR_PROGRAMMING (diversify: true) - Target-weighted priority allocation:
priority = (target - weight) × target
allocation_factor = priority / Σ(all_priorities)
This strategy multiplies deviation by the target weight, giving preference to core holdings (assets with higher target allocations).
PORTFOLIO Strategy - Whole portfolio rebalancing:
adjusted_target = target + (excess_weight / underweight_count)
allocation_factor = adjusted_target / Σ(all_adjusted_targets)
This strategy includes all assets (even overweighted ones) by redistributing excess weight across underweighted assets.
Concrete Example:
Portfolio state:
- Asset A: weight=48.6%, target=50.0% → deviation=1.4%
- Asset B: weight=8.5%, target=11.0% → deviation=2.5%
- Asset C: weight=19.1%, target=20.0% → deviation=0.9%
Investing $10,000:
| Strategy | Asset A | Asset B | Asset C | Rationale |
|---|---|---|---|---|
| WEIGHT | $2,916 (29.2%) | $5,208 (52.1%) | $1,876 (18.7%) | Asset B has the largest deviation (2.5%) |
| LINEAR_PROGRAMMING (diversify=true) | $6,062 (60.6%) | $2,381 (23.8%) | $1,557 (15.6%) | Asset A has the highest priority (70.0 = 1.4 × 50.0) |
| PORTFOLIO | $3,333 (33.3%) | $3,333 (33.3%) | $3,333 (33.3%) | Includes all assets with adjusted targets |
Mathematical Breakdown for LINEAR_PROGRAMMING:
Asset A: priority = 1.4 × 50.0 = 70.0
Asset B: priority = 2.5 × 11.0 = 27.5
Asset C: priority = 0.9 × 20.0 = 18.0
Total priority = 115.5
Asset A allocation = (70.0 / 115.5) × $10,000 = $6,062
Asset B allocation = (27.5 / 115.5) × $10,000 = $2,381
Asset C allocation = (18.0 / 115.5) × $10,000 = $1,557
When to Use Each:
- WEIGHT: You want pure rebalancing based solely on deviation, treating all assets equally regardless of their portfolio importance.
- LINEAR_PROGRAMMING (diversify=true): You want to prioritize your core holdings (high-target assets) while still maintaining diversification across all underweighted assets.
- LINEAR_PROGRAMMING (diversify=false): You want mathematically optimal aggressive rebalancing, accepting concentrated positions.
- PORTFOLIO: You want conservative allocation that includes the entire portfolio, even overweighted assets.
POST http://localhost:8080/api/optimize
{
"amount": "1000.00",
"strategy": {
"type": "WEIGHT",
"thresholds": {
"fromAth": 10.0,
"overTarget": 0.1
}
},
"assets": [
{
"ticker": "A",
"weight": 25.0,
"target": 20.0,
"fromAth": 18.0
},
{
"ticker": "B",
"weight": 15.0,
"target": 20.0,
"fromAth": 8.0
},
{
"ticker": "C",
"weight": 15.0,
"target": 25.0,
"fromAth": 15.5
},
{
"ticker": "D",
"weight": 10.0,
"target": 25.0,
"fromAth": 17.1
},
{
"ticker": "E",
"weight": 5.0,
"target": 10.0,
"fromAth": 22.0
}
]
}{
"amount": "1000.00",
"strategy": {
"type": "RATING"
},
"assets": [
{
"ticker": "A",
"rating": 3
},
{
"ticker": "B",
"rating": 5
},
{
"ticker": "C",
"rating": 5
},
{
"ticker": "D",
"fromAth": 2
},
{
"ticker": "E",
"rating": 4
}
]
}LINEAR_PROGRAMMING (Default - Corner Solution)
{
"amount": "1000.00",
"strategy": {
"type": "LINEAR_PROGRAMMING",
"thresholds": {
"fromAth": 10.0,
"overTarget": 0.0
},
"diversify": false
},
"assets": [
{
"ticker": "AAPL",
"weight": 15.0,
"target": 20.0,
"fromAth": 15.0
},
{
"ticker": "GOOGL",
"weight": 10.0,
"target": 25.0,
"fromAth": 12.0
},
{
"ticker": "MSFT",
"weight": 25.0,
"target": 20.0,
"fromAth": 8.0
}
]
}Response (without smart cap - only 2 assets with large priority gap):
{
"distribution": {
"GOOGL": "1000.00",
"AAPL": "0.00"
}
}LINEAR_PROGRAMMING (With Smart Cap - Multiple Assets)
{
"amount": "10000.00",
"strategy": {
"type": "LINEAR_PROGRAMMING",
"thresholds": {
"fromAth": 5.0,
"overTarget": 0.0
},
"diversify": false
},
"assets": [
{
"ticker": "HIGH",
"weight": 5.0,
"target": 30.0,
"fromAth": 10.0
},
{
"ticker": "MED",
"weight": 10.0,
"target": 25.0,
"fromAth": 12.0
},
{
"ticker": "LOW",
"weight": 15.0,
"target": 20.0,
"fromAth": 15.0
}
]
}Response (smart cap applied - 3+ assets, so max 90% to one asset):
{
"distribution": {
"HIGH": "9000.00",
"MED": "1000.00",
"LOW": "0.00"
}
}LINEAR_PROGRAMMING (Diversified Mode)
{
"amount": "10000.00",
"strategy": {
"type": "LINEAR_PROGRAMMING",
"thresholds": {
"fromAth": 10.0,
"overTarget": 0.3
},
"diversify": true
},
"assets": [
{
"ticker": "S&P500",
"weight": 48.6,
"target": 50.0
},
{
"ticker": "BTC",
"weight": 19.1,
"target": 20.0
},
{
"ticker": "WORLD",
"weight": 8.5,
"target": 11.0
},
{
"ticker": "EU",
"weight": 9.9,
"target": 8.0
}
]
}Response (proportional allocation based on priorities):
{
"distribution": {
"S&P500": "6061.69",
"WORLD": "2380.95",
"BTC": "1557.36"
}
}Note: With
diversify: true, funds are distributed proportionally across all eligible assets based on their priority scores (deviation × target). This provides diversification while still respecting priority rankings. The EU asset is excluded because it's overweight (9.9% > 8.0% target).
The application includes a feature to calculate how many years a total wealth amount will last, given specific withdrawal and return parameters.
The withdrawal calculator provides two different approaches to determine how long a total wealth amount will last:
-
Formula-based calculation: Uses the mathematical formula
n = -ln(1 - r*P/W) / ln(1 + r)to calculate the duration directly, where:- n = number of months
- r = monthly interest rate (as a decimal)
- P = principal (total amount)
- W = monthly withdrawal amount
-
Simulation-based calculation: Simulates the withdrawal process month by month, adding returns and subtracting withdrawals until the amount reaches zero.
Both methods handle special cases like infinite duration (when returns exceed withdrawals) and zero return rates.
{
"totalAmount": "1000000.00",
"monthlyWithdraw": "5000.00",
"expectedYearlyReturn": 4.0
}{
"years": 25.15,
"isInfinite": false
}When returns exceed withdrawals, the money will last indefinitely:
{
"years": Infinity,
"isInfinite": true
}- Zero total amount
- Zero monthly withdrawal
- Zero expected return
- Negative expected return
- Very small monthly withdrawals
- Very large monthly withdrawals
- Cases where returns exceed withdrawals (infinite duration)
The application also includes a feature to calculate the initial amount needed for a specific withdrawal duration.
The initial amount calculator provides two different approaches to determine how much money is needed to last for a specific number of years:
-
Formula-based calculation: Rearranges the withdrawal duration formula to solve for the initial amount:
P = W * (1 - (1 + r)^(-n)) / r, where:- n = number of months
- r = monthly interest rate (as a decimal)
- P = principal (total amount)
- W = monthly withdrawal amount
-
Simulation-based calculation: Uses binary search to find the initial amount that will last for the specified duration.
{
"shouldLastForYears": 30.0,
"monthlyWithdraw": "4000.00",
"expectedYearlyReturn": 6.0
}{
"totalAmount": "752487.56"
}- Zero years
- Zero monthly withdrawal
- Zero expected return
- Negative expected return
The application includes an enhanced withdrawal calculator that accounts for inflation and taxes, providing a more realistic projection of financial longevity.
The advanced withdrawal calculator extends the basic withdrawal calculation by incorporating:
-
Inflation Adjustment: Reduces the purchasing power of money over time and increases the withdrawal amount annually to maintain the same real value.
-
Tax Considerations: Calculates tax on investment returns based on a yearly tax allowance and average tax rate, reducing the effective return.
-
Real Return Calculation: Determines the effective return rate after accounting for both inflation and taxes.
-
Yearly Breakdown: Provides a detailed year-by-year analysis of the portfolio, showing starting balance, returns, withdrawals, tax paid, inflation impact, and ending balance.
{
"totalAmount": 100000.00,
"monthlyWithdraw": 500.00,
"expectedYearlyReturn": 7.0,
"yearlyInflationRate": 2.0,
"yearlyTaxAllowance": 12000.00,
"averageTaxRate": 20.0
}{
"years": 18.75,
"isInfinite": false,
"realReturn": 3.6,
"totalTaxPaid": 2345.67,
"inflationAdjustedWithdrawal": 750.23,
"yearlyBreakdown": [
{
"year": 1,
"startingBalance": 100000.00,
"returns": 7000.00,
"withdrawals": 6000.00,
"taxPaid": 0.00,
"inflationImpact": 2000.00,
"endingBalance": 99000.00
},
// Additional years...
]
}- Zero total amount
- Zero monthly withdrawal
- Zero expected return
- High inflation scenarios
- Cases where real returns (after inflation and taxes) exceed withdrawals (infinite duration)
- Various tax scenarios including zero tax and high tax rates
- More realistic financial planning by accounting for inflation
- Tax-aware calculations for better retirement planning
- Detailed yearly breakdown for deeper analysis
- Comparison between nominal and real (inflation-adjusted) returns
The application provides the following API endpoints:
POST /api/optimize
Optimizes dollar-cost averaging distribution based on the selected strategy.
POST /api/calculate-withdrawal
Calculates how long a total amount will last with regular withdrawals and expected returns.
POST /api/calculate-advanced-withdrawal
Calculates how long a total amount will last, accounting for inflation and taxes.
POST /api/calculate-target-amount
Calculates the initial amount needed to sustain withdrawals for a specific duration.
- JDK 21 or higher
- Gradle
-
Clone the repository
-
Run the application:
./gradlew run
-
The application APIs will be available at
http://localhost:8080