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 modifyCurrent 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:
- Not type-safe: Typos (
pe_ratiovs.p_e_ratio) fail at runtime - Hard to validate: Portfolio managers can’t easily verify logic
- Testing burden: Each strategy needs 50+ test cases
- Duplication: Same scoring patterns repeated across 15 strategies
- No reusability: Can’t compose strategies from building blocks
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
}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
}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
// Compiler prevents typos // let badMetric: KeyPath
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 constrainedHoldingsprivate 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