Domain-Driven Technical Indicators: A Journey from Booleans to Value Objects
In our recent work on the Mercury TA service, we encountered an interesting challenge with our Ichimoku Cloud indicator implementation. The issue arose from using primitive boolean values to represent complex market states, leading to ambiguous and sometimes incorrect interpretations. This post outlines our three-step solution to create a more robust and maintainable technical analysis API.
The Problem
Our initial implementation used boolean flags to represent cloud breakouts and price positions:
interface IchimokuData {
// ... other fields
cloud_breakout: boolean;
price_relative_to_cloud: boolean;
}
This led to several issues:
- Ambiguous meaning (what does
trueactually mean?) - Loss of information (no way to represent "inside cloud" state)
- Difficult to extend (adding new states requires breaking changes)
- Inconsistent interpretation between frontend and backend
Solution 1: Strict Enums
Our first step was to replace boolean flags with strict enums that clearly represent all possible states:
export enum CloudPosition {
ABOVE = 'ABOVE',
BELOW = 'BELOW',
INSIDE = 'INSIDE',
}
export enum CloudBreakout {
BULLISH = 1,
BEARISH = -1,
NONE = 0,
}
export enum CloudStrength {
STRONG = 'STRONG',
MODERATE = 'MODERATE',
WEAK = 'WEAK',
}
export interface IchimokuData {
tenkan_sen: number;
kijun_sen: number;
senkou_span_a: number;
senkou_span_b: number;
chikou_span: number | null;
cloud_strength: number;
cloud_direction: CloudDirection;
cloud_breakout: CloudBreakout;
price_relative_to_cloud: CloudPosition;
}
This immediately provided better type safety and self-documenting code. However, the business logic was still scattered across different parts of the application.
Solution 2: Domain Value Objects
The next step was to encapsulate the business logic into domain value objects. This is where things get interesting. Instead of passing raw values around, we create immutable objects that represent our domain concepts:
class CloudAnalysis {
private constructor(
private readonly price: number,
private readonly spanA: number,
private readonly spanB: number,
) {}
static fromPrice(price: number, spanA: number, spanB: number): CloudAnalysis {
return new CloudAnalysis(price, spanA, spanB);
}
get position(): CloudPosition {
const maxSpan = Math.max(this.spanA, this.spanB);
const minSpan = Math.min(this.spanA, this.spanB);
if (this.price > maxSpan) return CloudPosition.ABOVE;
if (this.price < minSpan) return CloudPosition.BELOW;
return CloudPosition.INSIDE;
}
get breakout(): CloudBreakout {
const previousPosition = this.getPreviousPosition();
if (
previousPosition === CloudPosition.BELOW &&
this.position === CloudPosition.ABOVE
) {
return CloudBreakout.BULLISH;
}
if (
previousPosition === CloudPosition.ABOVE &&
this.position === CloudPosition.BELOW
) {
return CloudBreakout.BEARISH;
}
return CloudBreakout.NONE;
}
get strength(): CloudStrength {
const spread = Math.abs(this.spanA - this.spanB);
const relativeSpread = spread / this.price;
if (relativeSpread > 0.02) return CloudStrength.STRONG;
if (relativeSpread > 0.01) return CloudStrength.MODERATE;
return CloudStrength.WEAK;
}
private getPreviousPosition(): CloudPosition {
// Implementation to get previous position from historical data
// This could be injected or stored in the class
}
}
class IchimokuAnalysis {
constructor(
private readonly data: {
tenkanSen: number;
kijunSen: number;
senkouSpanA: number;
senkouSpanB: number;
chikouSpan: number | null;
currentPrice: number;
},
) {}
get cloudAnalysis(): CloudAnalysis {
return CloudAnalysis.fromPrice(
this.data.currentPrice,
this.data.senkouSpanA,
this.data.senkouSpanB,
);
}
get signals(): TradingSignals {
return new TradingSignals({
cloudBreakout: this.cloudAnalysis.breakout,
trendStrength: this.cloudAnalysis.strength,
pricePosition: this.cloudAnalysis.position,
});
}
}
The benefits of this approach include:
- Encapsulated business logic
- Immutable data structures
- Self-validating objects
- Clear domain boundaries
- Easy to test in isolation
- Single source of truth for calculations
Solution 3: Response Schema Validation
Finally, we added strict runtime validation using Zod to ensure our API responses always match our domain model:
import { z } from 'zod';
const CloudPositionSchema = z.enum(['ABOVE', 'BELOW', 'INSIDE']);
const CloudBreakoutSchema = z.enum(['BULLISH', 'BEARISH', 'NONE']);
const CloudStrengthSchema = z.enum(['STRONG', 'MODERATE', 'WEAK']);
const IchimokuAnalysisSchema = z.object({
tenkan_sen: z.number(),
kijun_sen: z.number(),
senkou_span_a: z.number(),
senkou_span_b: z.number(),
chikou_span: z.number().nullable(),
cloud_analysis: z.object({
position: CloudPositionSchema,
breakout: CloudBreakoutSchema,
strength: CloudStrengthSchema
})
});
// In API endpoint:
@Get('/ichimoku')
async getIchimoku(@Query() params: IchimokuParams): Promise<z.infer<typeof IchimokuAnalysisSchema>> {
const analysis = new IchimokuAnalysis(
await this.taService.computeIchimoku(params)
);
return IchimokuAnalysisSchema.parse({
tenkan_sen: analysis.data.tenkanSen,
kijun_sen: analysis.data.kijunSen,
senkou_span_a: analysis.data.senkouSpanA,
senkou_span_b: analysis.data.senkouSpanB,
chikou_span: analysis.data.chikouSpan,
cloud_analysis: {
position: analysis.cloudAnalysis.position,
breakout: analysis.cloudAnalysis.breakout,
strength: analysis.cloudAnalysis.strength
}
});
}
Implementation Plan
-
Phase 1: Enum Migration (Week 1)
- Define all necessary enums
- Update existing interfaces
- Update API responses
- Update frontend type definitions
- Add enum validation tests
-
Phase 2: Value Objects (Week 2-3)
- Create base value object classes
- Implement domain logic in value objects
- Write comprehensive tests
- Refactor existing services to use value objects
- Document domain model
-
Phase 3: Schema Validation (Week 4)
- Set up Zod schemas
- Implement API response validation
- Add error handling middleware
- Update API documentation
- Add integration tests
-
Phase 4: Migration (Week 5)
- Deploy changes gradually
- Monitor for any issues
- Update client libraries
- Update documentation
Conclusion
By moving from primitive boolean values to a proper domain model, we've created a more robust and maintainable technical analysis system. The combination of strict enums, value objects, and schema validation gives us:
- Type safety and clear semantics
- Encapsulated business logic
- Immutable data structures
- Runtime validation
- Better developer experience
- Easier testing
- Clear upgrade path for future changes
Remember, technical indicators are complex domain concepts that deserve proper modeling. Don't let the simplicity of booleans tempt you into oversimplifying your domain model.
Next Steps
- Implement similar patterns for other technical indicators
- Create a shared library of technical analysis value objects
- Add more sophisticated market state analysis
- Consider event sourcing for tracking state changes
- Implement real-time updates using the new model
Implementation Checklists
Solution 1: Enum Migration
- EMA
- Define signal direction enum
- Add trend strength enum
- Update cross signals
- RSI
- Create trend state enum
- Add signal strength enum
- Update overbought/oversold states
- Bollinger Bands
- Define band position enum
- Add volatility state enum
- Update band touch signals
- MACD
- Create signal type enum
- Add trend strength enum
- Update crossover states
- Stochastic
- Define oscillator state enum
- Add momentum strength enum
- Update overbought/oversold states
- Ichimoku
- Update cloud position enum
- Refine breakout enum
- Add trend strength enum
- ADX/DMI
- Create trend strength enum
- Add direction state enum
- Update trend signals
- ATR
- Define volatility state enum
- Add range strength enum
- Update volatility signals
- OBV
- Create volume trend enum
- Add momentum state enum
- Update volume signals
Solution 2: Value Objects
- Base Classes
- Create BaseIndicator value object
- Implement TimeframeAware interface
- Add validation rules
- Trend Indicators
- EMAAnalysis value object
- MACDAnalysis value object
- ADXAnalysis value object
- IchimokuAnalysis value object
- Momentum Indicators
- RSIAnalysis value object
- StochasticAnalysis value object
- Volatility Indicators
- BollingerAnalysis value object
- ATRAnalysis value object
- Volume Indicators
- OBVAnalysis value object
- Composite Objects
- TrendAnalysis composite
- VolatilityAnalysis composite
- MomentumAnalysis composite
- VolumeAnalysis composite
Solution 3: Schema Validation
- Base Schemas
- Create base indicator schema
- Add timeframe validation
- Implement error handling
- Trend Schemas
- EMA response schema
- MACD response schema
- ADX response schema
- Ichimoku response schema
- Momentum Schemas
- RSI response schema
- Stochastic response schema
- Volatility Schemas
- Bollinger response schema
- ATR response schema
- Volume Schemas
- OBV response schema
- Composite Schemas
- Market state schema
- Signal strength schema
- Trend analysis schema
