Parallel Multi-Start Optimization: Finding Global Optima

BusinessMath Quarterly Series

19 min read

Part 32 of 12-Week BusinessMath Series


What You’ll Learn


The Problem

Many optimization problems have multiple local minima: Example: Portfolio optimization starting from equal weights might find a local optimum with Sharpe ratio 0.85. The global optimum with Sharpe ratio 1.12 exists, but gradient-based methods can’t escape the local basin.

Your optimization finds a solution, but not necessarily the best solution.


The Solution

BusinessMath’s ParallelOptimizer runs the same optimization algorithm from multiple random starting points in parallel, then returns the best result found. This dramatically increases the chance of finding the global optimum.

Automatic Parallel Multi-Start Optimization

Business Problem: Optimize portfolio allocation, but don’t get stuck in local minima.
import BusinessMath
import Foundation

let assets = [“US Stocks”, “Intl Stocks”, “Bonds”, “Real Estate”]
let expectedReturns = VectorN([0.10, 0.12, 0.04, 0.09])
let riskFreeRate = 0.03

// Covariance matrix
let covarianceMatrix = [
[0.0400, 0.0150, 0.0020, 0.0180], // US Stocks
[0.0150, 0.0625, 0.0015, 0.0200], // Intl Stocks
[0.0020, 0.0015, 0.0036, 0.0010], // Bonds
[0.0180, 0.0200, 0.0010, 0.0400] // Real Estate
]

// Objective: Maximize Sharpe ratio
let portfolioObjective: @Sendable (VectorN ) -> Double = { weights in
let expectedReturn = weights.dot(expectedReturns)

var variance = 0.0
for i in 0.. for j in 0.. variance += weights[i] * weights[j] * covarianceMatrix[i][j]
}
}

let risk = sqrt(variance)
let sharpeRatio = (expectedReturn - riskFreeRate) / risk

return -sharpeRatio // Minimize negative = maximize positive
}

// Constraints: budget + long-only
let budgetConstraint = MultivariateConstraint >.budgetConstraint
let longOnlyConstraints = MultivariateConstraint >.nonNegativity(dimension: assets.count)
let constraints: [MultivariateConstraint >] = [budgetConstraint] + longOnlyConstraints

// Create parallel multi-start optimizer
let parallelOptimizer = ParallelOptimizer >(
algorithm: .inequality, // Use inequality-constrained optimizer
numberOfStarts: 20, // Try 20 different starting points
maxIterations: 1000,
tolerance: 1e-6
)

// Define search region for random starting points
let searchRegion = (
lower: VectorN(repeating: 0.0, count: assets.count),
upper: VectorN(repeating: 1.0, count: assets.count)
)

// Run optimization in parallel (async/await)
// Note: Wrap in Task for playground execution
Task {
do {
let result = try await parallelOptimizer.optimize(
objective: portfolioObjective,
searchRegion: searchRegion,
constraints: constraints
)

print(“Parallel Multi-Start Optimization:”)
print(” Attempts: (result.allResults.count)”)
print(” Converged: (result.allResults.filter(.converged).count)”)
print(” Success rate: (result.successRate.percent())”)
print(” Best Sharpe ratio: ((-result.objectiveValue).number())”)

print(”\nBest Solution:”)
for (asset, weight) in zip(assets, result.solution.toArray()) {
if weight > 0.01 {
print(” (asset): (weight.percent())”)
}
}
} catch {
print(“Optimization failed: (error)”)
}
}

Comparing Single-Start vs Multi-Start

Pattern: See how multi-start improves over single-start optimization.
// Single-start optimization (baseline)
let singleStartOptimizer = AdaptiveOptimizer >()

let singleResult = try singleStartOptimizer.optimize(
objective: portfolioObjective,
initialGuess: VectorN.equalWeights(dimension: assets.count),
constraints: constraints
)

print(“Single-Start Result:”)
print(” Sharpe ratio: ((-singleResult.objectiveValue).number())”)
print(” Algorithm: (singleResult.algorithmUsed)”)

// Multi-start optimization
Task {
do {
let multiStartResult = try await parallelOptimizer.optimize(
objective: portfolioObjective,
searchRegion: searchRegion,
constraints: constraints
)

print(”\nMulti-Start Result (20 starting points):”)
print(” Best Sharpe ratio: ((-multiStartResult.objectiveValue).number())”)
print(” Success rate: (multiStartResult.successRate.percent())”)

// Compare
let improvement = (-multiStartResult.objectiveValue) / (-singleResult.objectiveValue) - 1.0
print(”\nImprovement: (improvement.percent(1))”)
} catch {
print(“Multi-start optimization failed: (error)”)
}
}

Analyzing Result Distribution

Pattern: Understand variation across different starting points.
Task {
do {
// Run multi-start optimization
let result = try await parallelOptimizer.optimize(
objective: portfolioObjective,
searchRegion: searchRegion,
constraints: constraints
)

// Analyze distribution of objectives found
let objectives = result.allResults.map(.value)
let sortedObjectives = objectives.sorted()

print(“Result Distribution:”)
print(” Best: (sortedObjectives.first?.number() ?? “N/A”)”)
print(” Median: (sortedObjectives[sortedObjectives.count / 2].number())”)
print(” Worst: (sortedObjectives.last?.number() ?? “N/A”)”)
print(” Range: ((sortedObjectives.last! - sortedObjectives.first!).number())”)

// Show how many found the global optimum (within 1% of best)
let globalThreshold = sortedObjectives.first! * 1.01
let globalCount = sortedObjectives.filter { $0 <= globalThreshold }.count

print(”\nFound global optimum: (globalCount)/(sortedObjectives.count) attempts”)
print(” (((Double(globalCount) / Double(sortedObjectives.count)).percent(0)))”)
} catch {
print(“Analysis failed: (error)”)
}
}

Choosing Number of Starting Points

Pattern: Trade off between solution quality and computation time.
Task {
// Test different numbers of starting points
for numStarts in [5, 10, 20, 50] {
do {
let optimizer = ParallelOptimizer >(
algorithm: .inequality,
numberOfStarts: numStarts,
maxIterations: 500,
tolerance: 1e-5
)

let startTime = Date()
let result = try await optimizer.optimize(
objective: portfolioObjective,
searchRegion: searchRegion,
constraints: constraints
)
let elapsedTime = Date().timeIntervalSince(startTime)

print(”\n(numStarts) starting points:”)
print(” Best objective: (result.objectiveValue.number())”)
print(” Success rate: (result.successRate.percent())”)
print(” Time: (elapsedTime.number(2))s”)
print(” Time per start: ((elapsedTime / Double(numStarts)).number(2))s”)
} catch {
print(”\n(numStarts) starting points: FAILED”)
}
}
}
Rule of Thumb:

How It Works

Parallel Execution with Swift Concurrency

BusinessMath uses Swift’s async/await and TaskGroup to run optimizations in parallel:
// Inside ParallelOptimizer (simplified)
let results = try await withThrowingTaskGroup(
of: (startingPoint: V, result: MultivariateOptimizationResult ).self
) { group in

// Add a task for each starting point
for start in startingPoints {
group.addTask {
let result = try ParallelOptimizer.runSingleOptimization(
algorithm: algorithm,
objective: objective,
initialGuess: start,
constraints: constraints
)
return (startingPoint: start, result: result)
}
}

// Collect all results as they complete
var allResults: [(V, MultivariateOptimizationResult )] = []
for try await (start, result) in group {
allResults.append((start, result))
}
return allResults
}

// Find best result (lowest objective value)
let best = results.min(by: { $0.1.value < $1.1.value })!

Algorithm Selection

ParallelOptimizer supports multiple base algorithms:
public enum Algorithm {
case gradientDescent(learningRate: Double)
case newtonRaphson
case constrained
case inequality
}

// Choose based on problem characteristics
let optimizer = ParallelOptimizer >(
algorithm: .inequality, // For problems with inequality constraints
numberOfStarts: 20
)

// Or use gradient descent for unconstrained problems
let unconstrainedOptimizer = ParallelOptimizer >(
algorithm: .gradientDescent(learningRate: 0.01),
numberOfStarts: 20
)

Performance Characteristics

Speedup depends on:
  1. Number of CPU cores: 8-core M3 can run 8 optimizations simultaneously
  2. Objective function cost: Expensive functions (>10ms) benefit most
  3. Number of starting points: 20 starts on 8 cores ≈ 2.5× speedup
Typical Results (M3 MacBook Pro, 8 cores):
Starting Points Sequential Time Parallel Time Speedup
5 starts 15s 5s 3.0×
10 starts 30s 8s 3.75×
20 starts 60s 15s 4.0×
50 starts 150s 35s 4.3×
Key Insight: Speedup plateaus around number of physical cores (8 for M3).

When to Use Multi-Start Optimization

Strong Candidates

Use multi-start when: Examples:

Weak Candidates

Don’t use multi-start when: Examples:

Hybrid Approach

Pattern: Use multi-start to find good region, then refine with single-start.
Task {
do {
// Phase 1: Broad search with low accuracy
let explorationOptimizer = ParallelOptimizer >(
algorithm: .gradientDescent(learningRate: 0.01),
numberOfStarts: 50,
maxIterations: 100, // Low iterations
tolerance: 1e-3 // Loose tolerance
)

let roughSolution = try await explorationOptimizer.optimize(
objective: portfolioObjective,
searchRegion: searchRegion,
constraints: constraints
)

print(“Phase 1 (exploration): Sharpe ((-roughSolution.objectiveValue).number())”)

// Phase 2: Refine best solution with high accuracy
let refinementOptimizer = AdaptiveOptimizer >(
maxIterations: 2000,
tolerance: 1e-8
)

let finalSolution = try refinementOptimizer.optimize(
objective: portfolioObjective,
initialGuess: roughSolution.solution,
constraints: constraints
)

print(“Phase 2 (refinement): Sharpe ((-finalSolution.objectiveValue).number())”)
} catch {
print(“Hybrid optimization failed: (error)”)
}
}

Real-World Application

Investment Firm: Quarterly Rebalancing

Company: Mid-sized investment firm managing $2B across 80 assets Challenge: Optimize portfolio quarterly, accounting for: Problem Characteristics: Single-Start Results (10 trials from different starting points): Multi-Start Solution:
import BusinessMath
import Foundation

// Simplified 80-asset portfolio problem
let numAssets = 80
let portfolioValue = 2_000_000_000.0 // $2B AUM

// Generate realistic expected returns (4% to 15%, mean ~9%)
let expectedReturns80 = VectorN((0.. 0.04 + 0.11 * Double.random(in: 0…1)
})

// Simplified covariance: diagonal-dominant with moderate correlations
var covariance80 = [[Double]](
repeating: [Double](repeating: 0.0, count: numAssets),
count: numAssets
)
for i in 0.. let volatility = 0.10 + 0.30 * Double.random(in: 0…1) // 10-40% volatility
covariance80[i][i] = volatility * volatility

// Add some correlation with nearby assets
for j in (i+1).. let correlation = 0.3 * Double.random(in: 0…1)
let vol_i = sqrt(covariance80[i][i])
let vol_j = 0.10 + 0.30 * Double.random(in: 0…1)
covariance80[j][j] = vol_j * vol_j
covariance80[i][j] = correlation * vol_i * vol_j
covariance80[j][i] = covariance80[i][j]
}
}

// Current holdings (starting point before rebalancing)
let currentHoldings = VectorN((0.. 0.005 + 0.015 * Double.random(in: 0…1) // 0.5% to 2% per asset
}).simplexProjection() // Normalize to sum to 1

// Objective with transaction costs
let transactionCostBps = 5.0 // 5 basis points per trade
let riskFreeRate80 = 0.03

let objectiveWithCosts: @Sendable (VectorN ) -> Double = { weights in
// Expected return
let expectedReturn = weights.dot(expectedReturns80)

// Portfolio variance
var variance = 0.0
for i in 0.. for j in 0.. variance += weights[i] * weights[j] * covariance80[i][j]
}
}
let risk = sqrt(variance)

// Transaction costs (creates non-convexity)
var totalTurnover = 0.0
for i in 0.. totalTurnover += abs(weights[i] - currentHoldings[i])
}
let transactionCost = (transactionCostBps / 10000.0) * totalTurnover

// Net return after costs
let netReturn = expectedReturn - transactionCost
let sharpeRatio = (netReturn - riskFreeRate80) / risk

return -sharpeRatio // Minimize negative Sharpe
}

// Constraints
let budgetConstraint80 = MultivariateConstraint >.budgetConstraint
let longOnlyConstraints80 = MultivariateConstraint >.nonNegativity(dimension: numAssets)

// Position limits: no more than 5% per asset (diversification requirement)
let positionLimits80 = (0.. MultivariateConstraint >.inequality { w in
w[i] - 0.05 // w[i] ≤ 5%
}
}

let allConstraints80 = [budgetConstraint80] + longOnlyConstraints80 + positionLimits80

// Multi-start optimization
Task {
do {
print(String(repeating: “=”, count: 70))
print(“REAL-WORLD EXAMPLE: 80-ASSET PORTFOLIO REBALANCING”)
print(String(repeating: “=”, count: 70))
print(“Portfolio value: $((portfolioValue / 1_000_000_000).number(1))B”)
print(“Number of assets: (numAssets)”)
print(“Transaction costs: (transactionCostBps) bps”)
print()

let robustOptimizer = ParallelOptimizer >(
algorithm: .inequality,
numberOfStarts: 30,
maxIterations: 1500,
tolerance: 1e-6
)

let startTime = Date()
let result = try await robustOptimizer.optimize(
objective: objectiveWithCosts,
searchRegion: (
lower: VectorN(repeating: 0.0, count: numAssets),
upper: VectorN(repeating: 0.05, count: numAssets) // Max 5% per asset
),
constraints: allConstraints80
)
let elapsedTime = Date().timeIntervalSince(startTime)

print(“Multi-Start Optimization (30 starts):”)
print(” Best Sharpe ratio: ((-result.objectiveValue).number())”)
print(” Success rate: (result.successRate.percent())”)
print(” Total time: ((elapsedTime / 60).number(1)) minutes”)

// Calculate turnover
var totalTurnover = 0.0
var numPositions = 0
for i in 0.. let change = abs(result.solution[i] - currentHoldings[i])
totalTurnover += change
if result.solution[i] > 0.001 {
numPositions += 1
}
}

print(”\nPortfolio Characteristics:”)
print(” Active positions: (numPositions)/(numAssets)”)
print(” Total turnover: (totalTurnover.percent(1))”)
print(” Trading costs: $((portfolioValue * totalTurnover * transactionCostBps / 10000).currency(0))”)

// Show top 10 positions
let topPositions = result.solution.toArray()
.enumerated()
.sorted { $0.element > $1.element }
.prefix(10)

print(”\nTop 10 Positions:”)
for (i, (idx, weight)) in topPositions.enumerated() {
print(” (i+1). Asset (idx): (weight.percent(2))”)
}

} catch {
print(“Robust optimization failed: (error)”)
}
}
Results:

Try It Yourself

Click to expand full playground code
import BusinessMath
import Foundation

let assets = ["US Stocks", "Intl Stocks", "Bonds", "Real Estate"]
let expectedReturns = VectorN([0.10, 0.12, 0.04, 0.09])
let riskFreeRate = 0.03

// Covariance matrix
let covarianceMatrix = [
[0.0400, 0.0150, 0.0020, 0.0180], // US Stocks
[0.0150, 0.0625, 0.0015, 0.0200], // Intl Stocks
[0.0020, 0.0015, 0.0036, 0.0010], // Bonds
[0.0180, 0.0200, 0.0010, 0.0400] // Real Estate
]

// Objective: Maximize Sharpe ratio
let portfolioObjective: @Sendable (VectorN ) -> Double = { weights in
let expectedReturn = weights.dot(expectedReturns)

var variance = 0.0
for i in 0.. for j in 0.. variance += weights[i] * weights[j] * covarianceMatrix[i][j]
}
}

let risk = sqrt(variance)
let sharpeRatio = (expectedReturn - riskFreeRate) / risk

return -sharpeRatio // Minimize negative = maximize positive
}

// Constraints: budget + long-only
let budgetConstraint = MultivariateConstraint >.budgetConstraint
let longOnlyConstraints = MultivariateConstraint >.nonNegativity(dimension: assets.count)
let constraints: [MultivariateConstraint >] = [budgetConstraint] + longOnlyConstraints

// Create parallel multi-start optimizer
let parallelOptimizer = ParallelOptimizer >(
algorithm: .inequality, // Use inequality-constrained optimizer
numberOfStarts: 20, // Try 20 different starting points
maxIterations: 1000,
tolerance: 1e-6
)

// Define search region for random starting points
let searchRegion = (
lower: VectorN(repeating: 0.0, count: assets.count),
upper: VectorN(repeating: 1.0, count: assets.count)
)

// Run optimization in parallel (async/await)
// Note: Playgrounds require Task wrapper for async code
Task {
do {
let result = try await parallelOptimizer.optimize(
objective: portfolioObjective,
searchRegion: searchRegion,
constraints: constraints
)

print("Parallel Multi-Start Optimization:")
print(" Attempts: \(result.allResults.count)")
print(" Converged: \(result.allResults.filter(\.converged).count)")
print(" Success rate: \(result.successRate.percent())")
print(" Best Sharpe ratio: \((-result.objectiveValue).number())")

print("\nBest Solution:")
for (asset, weight) in zip(assets, result.solution.toArray()) {
if weight > 0.01 {
print(" \(asset): \(weight.percent())")
}
}
} catch {
print("Optimization failed: \(error)")
}


// MARK: - Single-Start vs. Multi-Start Optimization
// Single-start optimization (baseline)
let singleStartOptimizer = AdaptiveOptimizer >()

let singleResult = try singleStartOptimizer.optimize(
objective: portfolioObjective,
initialGuess: VectorN.equalWeights(dimension: assets.count),
constraints: constraints
)

print("Single-Start Result:")
print(" Sharpe ratio: \((-singleResult.objectiveValue).number())")
print(" Algorithm: \(singleResult.algorithmUsed)")
print()

// Multi-start optimization
do {
let multiStartResult = try await parallelOptimizer.optimize(
objective: portfolioObjective,
searchRegion: searchRegion,
constraints: constraints
)

print("\nMulti-Start Result (20 starting points):")
print(" Best Sharpe ratio: \((-multiStartResult.objectiveValue).number())")
print(" Success rate: \(multiStartResult.successRate.percent())")

// Compare
let improvement = (-multiStartResult.objectiveValue) / (-singleResult.objectiveValue) - 1.0
print("\nImprovement: \((improvement.percent(1)))")
} catch {
print("Multi-start optimization failed: \(error)")
}

// MARK: - Analyzing Result Distribution
do {
// Run multi-start optimization
let result = try await parallelOptimizer.optimize(
objective: portfolioObjective,
searchRegion: searchRegion,
constraints: constraints
)

// Analyze distribution of objectives found
let objectives = result.allResults.map(\.value)
let sortedObjectives = objectives.sorted()

print("Result Distribution:")
print(" Best: \(sortedObjectives.first?.number() ?? "N/A")")
print(" Median: \(sortedObjectives[sortedObjectives.count / 2].number())")
print(" Worst: \(sortedObjectives.last?.number() ?? "N/A")")
print(" Range: \((sortedObjectives.last! - sortedObjectives.first!).number())")

// Show how many found the global optimum (within 1% of best)
let globalThreshold = sortedObjectives.first! * 1.01
let globalCount = sortedObjectives.filter { $0 <= globalThreshold }.count

print("\nFound global optimum: \(globalCount)/\(sortedObjectives.count) attempts")
print(" (\((Double(globalCount) / Double(sortedObjectives.count)).percent(0)))")
} catch {
print("Analysis failed: \(error)")
}

// MARK: - Choosing Number of Starting Points
// Test different numbers of starting points
for numStarts in [5, 10, 20, 50] {
do {
let optimizer = ParallelOptimizer >(
algorithm: .inequality,
numberOfStarts: numStarts,
maxIterations: 500,
tolerance: 1e-5
)

let startTime = Date()
let result = try await optimizer.optimize(
objective: portfolioObjective,
searchRegion: searchRegion,
constraints: constraints
)
let elapsedTime = Date().timeIntervalSince(startTime)

print("\n\(numStarts) starting points:")
print(" Best objective: \(result.objectiveValue.number())")
print(" Success rate: \(result.successRate.percent())")
print(" Time: \(elapsedTime.number(2))s")
print(" Time per start: \((elapsedTime / Double(numStarts)).number(2))s")
} catch {
print("\n\(numStarts) starting points: FAILED")
}
}

// MARK: - Hybrid Approach
do {
// Phase 1: Broad search with low accuracy
let explorationOptimizer = ParallelOptimizer >(
algorithm: .gradientDescent(learningRate: 0.01),
numberOfStarts: 50,
maxIterations: 100, // Low iterations
tolerance: 1e-3 // Loose tolerance
)

let roughSolution = try await explorationOptimizer.optimize(
objective: portfolioObjective,
searchRegion: searchRegion,
constraints: constraints
)

print("Phase 1 (exploration): Sharpe \((-roughSolution.objectiveValue).number())")

// Phase 2: Refine best solution with high accuracy
let refinementOptimizer = AdaptiveOptimizer >(
maxIterations: 2000,
tolerance: 1e-8
)

let finalSolution = try refinementOptimizer.optimize(
objective: portfolioObjective,
initialGuess: roughSolution.solution,
constraints: constraints
)

print("Phase 2 (refinement): Sharpe \((-finalSolution.objectiveValue).number())")
} catch {
print("Hybrid optimization failed: \(error)")
}

// MARK: - Real-World Example 80-Asset Portfolio Rebalancing
// Simplified 80-asset portfolio problem
let numAssets = 80
let portfolioValue = 2_000_000_000.0 // $2B AUM

// Generate realistic expected returns (4% to 15%, mean ~9%)
let expectedReturns80 = VectorN((0.. 0.04 + 0.11 * Double.random(in: 0...1)
})

// Simplified covariance: diagonal-dominant with moderate correlations
var covariance80 = [[Double]](
repeating: [Double](repeating: 0.0, count: numAssets),
count: numAssets
)
for i in 0.. let volatility = 0.10 + 0.30 * Double.random(in: 0...1) // 10-40% volatility
covariance80[i][i] = volatility * volatility

// Add some correlation with nearby assets
for j in (i+1).. let correlation = 0.3 * Double.random(in: 0...1)
let vol_i = sqrt(covariance80[i][i])
let vol_j = 0.10 + 0.30 * Double.random(in: 0...1)
covariance80[j][j] = vol_j * vol_j
covariance80[i][j] = correlation * vol_i * vol_j
covariance80[j][i] = covariance80[i][j]
}
}

// Current holdings (starting point before rebalancing)
let currentHoldings = VectorN((0.. 0.005 + 0.015 * Double.random(in: 0...1) // 0.5% to 2% per asset
}).simplexProjection() // Normalize to sum to 1

// Objective with transaction costs
let transactionCostBps = 5.0 // 5 basis points per trade
let riskFreeRate80 = 0.03

let covariance80Locked = covariance80
let objectiveWithCosts: @Sendable (VectorN ) -> Double = { weights in
// Expected return
let expectedReturn = weights.dot(expectedReturns80)

// Portfolio variance
var variance = 0.0
for i in 0.. for j in 0.. variance += weights[i] * weights[j] * covariance80Locked[i][j]
}
}
let risk = sqrt(variance)

// Transaction costs (creates non-convexity)
var totalTurnover = 0.0
for i in 0.. totalTurnover += abs(weights[i] - currentHoldings[i])
}
let transactionCost = (transactionCostBps / 10000.0) * totalTurnover

// Net return after costs
let netReturn = expectedReturn - transactionCost
let sharpeRatio = (netReturn - riskFreeRate80) / risk

return -sharpeRatio // Minimize negative Sharpe
}

// Constraints
let budgetConstraint80 = MultivariateConstraint >.budgetConstraint
let longOnlyConstraints80 = MultivariateConstraint >.nonNegativity(dimension: numAssets)

// Position limits: no more than 5% per asset (diversification requirement)
let positionLimits80 = (0.. MultivariateConstraint >.inequality { w in
w[i] - 0.05 // w[i] ≤ 5%
}
}

let allConstraints80 = [budgetConstraint80] + longOnlyConstraints80 + positionLimits80

// Multi-start optimization
do {
print(String(repeating: "=", count: 70))
print("REAL-WORLD EXAMPLE: 80-ASSET PORTFOLIO REBALANCING")
print(String(repeating: "=", count: 70))
print("Portfolio value: $\((portfolioValue / 1_000_000_000).number(1))B")
print("Number of assets: \(numAssets)")
print("Transaction costs: \(transactionCostBps) bps")
print()

let robustOptimizer = ParallelOptimizer >(
algorithm: .inequality,
numberOfStarts: 30,
maxIterations: 1500,
tolerance: 1e-6
)

let startTime = Date()
let result = try await robustOptimizer.optimize(
objective: objectiveWithCosts,
searchRegion: (
lower: VectorN(repeating: 0.0, count: numAssets),
upper: VectorN(repeating: 0.05, count: numAssets) // Max 5% per asset
),
constraints: allConstraints80
)
let elapsedTime = Date().timeIntervalSince(startTime)

print("Multi-Start Optimization (30 starts):")
print(" Best Sharpe ratio: \((-result.objectiveValue).number())")
print(" Success rate: \(result.successRate.percent())")
print(" Total time: \((elapsedTime / 60).number(1)) minutes")

// Calculate turnover
var totalTurnover = 0.0
var numPositions = 0
for i in 0.. let change = abs(result.solution[i] - currentHoldings[i])
totalTurnover += change
if result.solution[i] > 0.001 {
numPositions += 1
}
}

print("\nPortfolio Characteristics:")
print(" Active positions: \(numPositions)/\(numAssets)")
print(" Total turnover: \(totalTurnover.percent(1))")
print(" Trading costs: $\((portfolioValue * totalTurnover * transactionCostBps / 10000).currency(0))")

// Show top 10 positions
let topPositions = result.solution.toArray()
.enumerated()
.sorted { $0.element > $1.element }
.prefix(10)

print("\nTop 10 Positions:")
for (i, (idx, weight)) in topPositions.enumerated() {
print(" \(i+1). Asset \(idx): \(weight.percent(2))")
}

} catch {
print("Robust optimization failed: \(error)")
}
}

// Keep playground alive long enough for async task to complete
RunLoop.main.run(until: Date().addingTimeInterval(30))
→ Full API Reference: BusinessMath Docs – Parallel Optimization

Experiments to Try

  1. Starting Point Sensitivity: Run single-start from 10 different random starting points. How much does the result vary?
  2. Scaling Study: Compare 5, 10, 20, 50 starting points. When do diminishing returns start?
  3. Algorithm Comparison: Try different base algorithms (.gradientDescent vs .inequality vs .constrained). Which works best for your problem?
  4. Search Region Impact: Try narrow vs wide search regions. Does a tighter region around a good initial guess help?

Next Steps

Next Week: Week 10 explores Advanced Optimization Algorithms including L-BFGS for large-scale problems, conjugate gradient methods, and simulated annealing for global optimization.

Case Study Coming: Week 11 includes Real-Time Portfolio Optimization with streaming market data and dynamic constraints.


Series: [Week 9 of 12] | Topic: [Part 5 - Business Applications] | Completed: [4/6]

Topics Covered: Multi-start optimization • Parallel execution • Swift concurrency • Global optimization • Success rate analysis

Playgrounds: [Week 1-9 available] • [Next: Advanced algorithms]


Tagged with: optimization, concurrency