Case Study: Investment Strategy DSL with Result Builders

BusinessMath Quarterly Series

10 min read

Case Study #6 of 6 • Capstone for Result Builders + Complete Library


The Business Challenge

Company: Quantitative hedge fund with 15 investment strategies Portfolio: $2B across multiple asset classes Challenge: Encode investment strategies in code that portfolio managers can read, validate, and modify

Current State (Python/Excel):

# Strategy: Growth + Value + Momentum positions = [] for stock in universe: score = 0 # Growth scoring if stock.revenue_growth > 0.15: score += 2 elif stock.revenue_growth > 0.10: score += 1
# Value scoring
if stock.pe_ratio < sector_median_pe * 0.8:
    score += 2
elif stock.pe_ratio < sector_median_pe:
    score += 1

# Momentum scoring
if stock.returns_6mo > 0.20:
    score += 2
elif stock.returns_6mo > 0.10:
    score += 1

if score >= 4:  # Buy threshold
    weight = score / total_score
    positions.append((stock, weight))
Problems:
  1. Not type-safe: Typos (pe_ratio vs. p_e_ratio) fail at runtime
  2. Hard to validate: Portfolio managers can’t easily verify logic
  3. Testing burden: Each strategy needs 50+ test cases
  4. Duplication: Same scoring patterns repeated across 15 strategies
  5. No reusability: Can’t compose strategies from building blocks
The Ask: “Can we write strategies that read like English but execute like code?”

The Solution: Investment Strategy DSL

Using Swift result builders, we create a domain-specific language where strategies are declarative, type-safe, and composable.

Part 1: Strategy DSL with Result Builders

import BusinessMath

// Define a strategy using result builder syntax @InvestmentStrategyBuilder var growthValueMomentum: InvestmentStrategy { // Strategy metadata Name(“Growth + Value + Momentum”) Description(“Combines three quantitative factors with equal weighting”) RebalanceFrequency(.monthly)

// Universe selection Universe { Market(.us) MinMarketCap(5_000_000_000)  // $5B minimum ExcludeSectors([.financials, .utilities])  // Regulated sectors }

// Scoring factors ScoringModel { // Growth factor Factor(“Revenue Growth”) { Metric(.revenueGrowth) Threshold(strong: 0.15, moderate: 0.10) Weight(0.33) }

// Value factor Factor(“Valuation”) { Metric(.peRatio) Comparison(.lessThan) Benchmark(.sectorMedian) Threshold(strong: 0.80, moderate: 1.00) Weight(0.33) }

// Momentum factor Factor(“Price Momentum”) { Metric(.returns6Month) Threshold(strong: 0.20, moderate: 0.10) Weight(0.34) }

}

// Selection and weighting Selection { ScoreThreshold(4.0) // Minimum composite score MaxPositions(50) PositionWeighting(.equalWeight) // Equal-weight top 50 }

// Risk controls RiskLimits { MaxPositionSize(0.05) // 5% max per position MaxSectorExposure(0.30) // 30% max per sector TargetVolatility(0.15) // 15% annual volatility MaxDrawdown(0.20) // 20% max drawdown before defensive }

}

// The DSL compiles to an executable strategy let holdings = growthValueMomentum.execute(universe: stockUniverse)

print(“Strategy: (growthValueMomentum.name)”) print(“Selected (holdings.count) positions:”) for holding in holdings.prefix(10) { print(” (holding.ticker): ((holding.weight * 100).number())% (score: (holding.score.number()))”) }

Part 2: Result Builder Implementation

The magic happens in the @InvestmentStrategyBuilder:
@resultBuilder struct InvestmentStrategyBuilder { // Build strategy from components static func buildBlock(_ components: StrategyComponent…) -> InvestmentStrategy { InvestmentStrategy(components: components) }
// Support if/else conditionals static func buildEither(first component: StrategyComponent) -> StrategyComponent { component }

static func buildEither(second component: StrategyComponent) -> StrategyComponent { component }

// Support optional components static func buildOptional(_ component: StrategyComponent?) -> StrategyComponent { component ?? EmptyComponent() }

// Support for loops static func buildArray(_ components: [StrategyComponent]) -> StrategyComponent { CompositeComponent(components) }

}

// Base protocol for all strategy components protocol StrategyComponent { func apply(to strategy: inout InvestmentStrategy) }

// Example component: Factor definition struct Factor: StrategyComponent { let name: String let metric: KeyPath let threshold: (strong: Double, moderate: Double) let weight: Double let comparison: ComparisonType

init( _ name: String, @FactorBuilder builder: () -> FactorConfiguration ) { self.name = name let config = builder() self.metric = config.metric self.threshold = config.threshold self.weight = config.weight self.comparison = config.comparison }

func apply(to strategy: inout InvestmentStrategy) { strategy.scoringFactors.append( ScoringFactor( name: name, metric: metric, threshold: threshold, weight: weight, comparison: comparison ) ) }

}

// Nested result builder for factor configuration @resultBuilder struct FactorBuilder { static func buildBlock(_ components: FactorConfigComponent…) -> FactorConfiguration { var config = FactorConfiguration() for component in components { component.apply(to: &config) } return config } }

// Factor configuration components struct Metric : FactorConfigComponent { let keyPath: KeyPath

init(_ keyPath: KeyPath
            
              ) { self.keyPath = keyPath }
              

func apply(to config: inout FactorConfiguration) { config.metric = keyPath as! KeyPath }

}

struct Threshold: FactorConfigComponent { let strong: Double let moderate: Double

func apply(to config: inout FactorConfiguration) { config.threshold = (strong, moderate) } 
}

struct Weight: FactorConfigComponent { let value: Double

init(_ value: Double) { self.value = value }

func apply(to config: inout FactorConfiguration) { config.weight = value }

}

Part 3: Type-Safe Stock Data

The DSL uses Swift key paths for type-safe metric access:
struct Stock { // Company identifiers let ticker: String let name: String let sector: Sector
// Fundamentals let marketCap: Double let revenueGrowth: Double let earningsGrowth: Double let peRatio: Double let pbRatio: Double let debtToEquity: Double

// Price data let price: Double let returns1Month: Double let returns6Month: Double let returns12Month: Double let volatility: Double

// Valuation var relativeValuation: Double { // Compare to sector median peRatio / sector.medianPE }

}

// Key paths provide type safety let revenueGrowthMetric: KeyPath = .revenueGrowth let peRatioMetric: KeyPath = .peRatio

// Compiler prevents typos // let badMetric: KeyPath = .reveueGrowth // ✗ Compile error!

Part 4: Strategy Execution Engine

The DSL compiles to executable code:
struct InvestmentStrategy { var name: String = “” var description: String = “” var rebalanceFrequency: RebalanceFrequency = .monthly
var universeFilters: [UniverseFilter] = [] var scoringFactors: [ScoringFactor] = [] var selectionRules: SelectionRules = SelectionRules() var riskLimits: RiskLimits = RiskLimits()

// Execute strategy on stock universe func execute(universe: [Stock]) -> [Holding] { // 1. Apply universe filters let filteredUniverse = universe.filter { stock in universeFilters.allSatisfy { $0.passes(stock) } }

// 2. Score each stock let scoredStocks = filteredUniverse.map { stock in (stock: stock, score: calculateScore(for: stock)) }

// 3. Select top stocks let selectedStocks = scoredStocks .filter { $0.score >= selectionRules.scoreThreshold } .sorted { $0.score > $1.score } .prefix(selectionRules.maxPositions)

// 4. Calculate weights let holdings = calculateWeights( selectedStocks: Array(selectedStocks), method: selectionRules.weightingMethod )

// 5. Apply risk limits let constrainedHoldings = applyRiskLimits(holdings)

return constrainedHoldings

}

private func calculateScore(for stock: Stock) -> Double { var totalScore = 0.0

for factor in scoringFactors { let value = stock[keyPath: factor.metric] let benchmark = factor.benchmark?.value(for: stock) ?? 0.0
let comparison = factor.comparison == .greaterThan ? value > benchmark : value < benchmark

if comparison { if factor.threshold.strong > 0 && value >= factor.threshold.strong { totalScore += 2.0 * factor.weight } else if factor.threshold.moderate > 0 && value >= factor.threshold.moderate { totalScore += 1.0 * factor.weight } }

}

return totalScore

}

private func calculateWeights( selectedStocks: [(stock: Stock, score: Double)], method: WeightingMethod ) -> [Holding] { switch method { case .equalWeight: let weight = 1.0 / Double(selectedStocks.count) return selectedStocks.map { Holding(stock: $0.stock, weight: weight, score: $0.score) }

case .scoreWeighted: let totalScore = selectedStocks.map(.score).reduce(0, +) return selectedStocks.map { Holding(stock: $0.stock, weight: $0.score / totalScore, score: $0.score) }

case .marketCapWeighted: let totalMarketCap = selectedStocks.map { $0.stock.marketCap }.reduce(0, +) return selectedStocks.map { Holding(stock: $0.stock, weight: $0.stock.marketCap / totalMarketCap, score: $0.score) } }

}

private func applyRiskLimits(_ holdings: [Holding]) -> [Holding] { var adjusted = holdings

// Cap individual positions adjusted = adjusted.map { holding in var h = holding h.weight = min(h.weight, riskLimits.maxPositionSize) return h }

// Renormalize to 100% let totalWeight = adjusted.map(.weight).reduce(0, +) adjusted = adjusted.map { holding in var h = holding h.weight = h.weight / totalWeight return h }

return adjusted

} }

struct Holding { let stock: Stock var weight: Double let score: Double

var ticker: String { stock.ticker } 
}

Part 5: Strategy Composition

Compose complex strategies from building blocks:
// Reusable factor definitions let growthFactor = Factor(“Revenue Growth”) { Metric(.revenueGrowth) Threshold(strong: 0.15, moderate: 0.10) Weight(0.50) }

let valueFactor = Factor(“Valuation”) { Metric(.peRatio) Comparison(.lessThan) Benchmark(.sectorMedian) Threshold(strong: 0.80, moderate: 1.00) Weight(0.50) }

// Compose strategies @InvestmentStrategyBuilder var pureGrowth: InvestmentStrategy { Name(“Pure Growth”) ScoringModel { growthFactor // Single factor strategy } }

@InvestmentStrategyBuilder var growthAtReasonablePrice: InvestmentStrategy { Name(“Growth at Reasonable Price (GARP)”) ScoringModel { growthFactor valueFactor // Combines two factors } }

// Conditional strategies @InvestmentStrategyBuilder var adaptiveStrategy: InvestmentStrategy { Name(“Adaptive Multi-Factor”)

ScoringModel { if marketCondition == .bull { growthFactor  // Growth in bull markets } else { valueFactor   // Value in bear markets } } 
}

The Results

Code Comparison

Before (Python):
# 150 lines of procedural code

Runtime errors common

Hard for PMs to validate

After (Swift DSL):
// 30 lines of declarative code // Reusable components // Compile-time type safety // PMs can read and modify 
Code Reduction: 80% fewer lines per strategy

Validation and Testing

// Strategies are testable! func testGrowthValueMomentumStrategy() { let strategy = growthValueMomentum
// Test universe filtering XCTAssertEqual(strategy.universeFilters.count, 3)

// Test scoring factors XCTAssertEqual(strategy.scoringFactors.count, 3) XCTAssertEqual(strategy.scoringFactors[0].name, “Revenue Growth”)

// Test with mock data let mockUniverse = createMockStocks() let holdings = strategy.execute(universe: mockUniverse)

// Verify risk limits applied XCTAssertTrue(holdings.allSatisfy { $0.weight <= 0.05 })

}

// Property-based testing func testStrategyInvariants() { for _ in 0..<100 { let randomUniverse = generateRandomStocks(count: 500) let holdings = growthValueMomentum.execute(universe: randomUniverse)

    // Invariants that MUST hold let totalWeight = holdings.map(.weight).reduce(0, +) XCTAssertEqual(totalWeight, 1.0, accuracy: 1e-6, “Weights must sum to 100%”) XCTAssertLessOrEqual(holdings.count, 50, “Max 50 positions”) XCTAssertTrue(holdings.allSatisfy { $0.weight <= 0.05 }, “No position > 5%”) } 
}
Test Coverage: 15 strategies × 20 tests each = 300 automated tests

Business Value

Before DSL: After DSL: Annual Impact: Technology ROI:

What Worked

  1. Domain Expert Empowerment: Portfolio managers can now write strategies (with light developer support)
  2. Type Safety: Compiler catches errors that were runtime failures in Python
  3. Composability: Reusable factor definitions across all 15 strategies
  4. Testability: Every strategy has 20+ automated tests
  5. Documentation: Strategies are self-documenting (“reads like English”)

What Didn’t Work

  1. Initial Learning Curve: PMs needed 2-day training on Swift basics
  2. Complex Nesting: Deeply nested result builders got confusing (limited to 2 levels)
  3. Error Messages: Result builder compile errors can be cryptic (improved with better type annotations)

The Insight

The best DSL doesn’t feel like code—it feels like structured English.

When a portfolio manager looks at this:

Factor(“Revenue Growth”) { Metric(.revenueGrowth) Threshold(strong: 0.15, moderate: 0.10) Weight(0.50) } 
They see: “Revenue growth factor, strong threshold 15%, moderate 10%, weight 50%.”

Not: “Function call with closure parameter accepting lambda with key path and tuple.”

That’s the magic of result builders: Hide the machinery, expose the meaning.

And when they try to write:

Metric(.reveueGrowth)  // Typo 
The compiler says: “No such property ‘reveueGrowth’ on Stock”

That’s the magic of type safety: Catch errors at compile time, not in production.

Combine these two—readable DSL + type safety—and you get something remarkable: Domain experts writing production code that actually works.


Try It Yourself

Download the complete case study playground:
→ Download: CaseStudies/InvestmentStrategyDSL.playground → Includes: Full DSL implementation, 15 strategy examples, test suite → Extensions: Add machine learning factors, real-time data feeds 

Series Conclusion

This is the final post in the 12-week BusinessMath blog series. We’ve covered:

Weeks 1-2: Foundation (getting started, time series, TVM, ratios) Weeks 3-5: Financial Modeling (growth, forecasting, statements, loans, bonds) Week 6: Simulation (Monte Carlo, scenarios) Weeks 7-11: Optimization (gradient descent → BFGS → genetic → PSO → annealing) Week 12: Reflections and this final case study

6 Case Studies:

  1. Retirement Planning (Week 1)
  2. Capital Equipment (Week 3)
  3. Option Pricing (Week 6)
  4. Portfolio Optimization (Week 8)
  5. Real-Time Rebalancing (Week 11)
  6. Investment Strategy DSL (Week 12) ← You are here
Thank you for following along on this journey. From NPV calculations to GPU-accelerated optimization to type-safe investment strategies—we’ve built something powerful.

Now go build something remarkable.


Series: [Week 12 of 12] COMPLETE | Case Study [6/6] COMPLETE | Topics Combined: Result Builders + Type Safety + Full Library

The End 🎉


Tagged with: case-study, swift-patterns