Ninjascript Candle Detection

## Production Postmortem: Adaptive Candle Pattern Detection Failure in NinjaTrader

### Summary
A trading strategy relying on rigid candle pattern detection thresholds experienced low detection rates and adaptability issues during live market volatility shifts. The static ratio-based approach failed to identify key patterns dynamically.

### Root Cause
- **Fixed thresholds** insensitive to changing market conditions
- **Undefined `avgBody` calculation** (not shown in provided code) resisting volatility adaptation
- **Hard-coded ratios** (0.7, 1.5, etc.) not normalized to instrument characteristics
- **Missing adaptive calculation** for pattern thresholds using volatility-based metrics

### Why This Happens in Real Systems
- Volatility changes continuously shift candle characteristics
- Single-set thresholds work only during specific market regimes
- Generic ratios ignore instrument-specific volatility profiles
- Moving average dependencies require complete historical initialization
- Pattern recognition logic lacks flexibility for customization

### Real-World Impact
- Significant missed trading signals during volatile sessions
- Failed entries/exits causing strategy slippage
- Reduced P&L due to undetected opportunities
- Erosion of trading strategy confidence
- Pattern-dependent strategies becoming temporarily ineffective

### Example Code
Initial Implementation:
```csharp
// Problematic rigid thresholds
private bool IsMomentumCandle(double body, double range) 
{
if (range == 0 || CurrentBar < 14) return false;
double bodyRatio = body / range;
double avgBodySize = avgBody[0]; 

return bodyRatio >= 0.7 && body >= avgBodySize * 1.5;
}

// Inflexible pin detection
private bool IsVectorLowerPin(double body, double upperWick, double lowerWick) 
{
if (body == 0) return false;
return lowerWick >= body * 1.5 && upperWick <= body * 0.5;
}

Refactored Adaptive Solution:

// Volatility-adaptive momentum detection
private bool IsMomentumCandle(Bar bar)
{
if (CurrentBar < MIN_BARS) return false;

double volatilityUnit = ATR(14)[0]; // Use Average True Range
double bodyHeight = Math.Abs(bar.Close - bar.Open);
double range = bar.High - bar.Low;

return bodyHeight > volatilityUnit * MIN_MOMENTUM_FACTOR
&& bodyHeight/range > MIN_BODY_RATIO;
}

How Senior Engineers Fix It

  • Parameterize thresholds using extern variables for runtime optimization
  • Normalize measurements against volatility indicators (ATR/SD)
  • Implement dynamic threshold calculations based on:
    • Rolling percentile ranges for wick sizes
    • Instrument-specific volatility regressions
  • Create candle pattern factory to enable additive pattern matching
  • Develop parameter optimization routines:
    protected override void OnOptimize()
    {
    MAX_WICK_RATIO = Optimization.ParamRange(0.25, 3.0, 0.25);
    MIN_BODY_RATIO = Optimization.ParamRange(0.1, 0.9, 0.1);
    }
  • Add regime-awareness using volatility state detection

Why Juniors Miss It

  • Tendency to hardcode ratios without volatility correlation checks
  • Overlooking instrument-specific calibration requirements
  • Underestimating threshold drift in volatile environments
  • Failure to implement parameterization hooks for future optimization
  • Missing historical validation with regime filtering