home / skills / mkalhitti-cloud / universal-or-strategy / live-price-tracking

live-price-tracking skill

/.agent/skills/live-price-tracking

This skill enables real-time live price tracking for NinjaTrader strategies by using OnMarketData and tick data to update trailing stops.

npx playbooks add skill mkalhitti-cloud/universal-or-strategy --skill live-price-tracking

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
12.7 KB
---
name: live-price-tracking
description: Critical guide for implementing live price tracking in NinjaTrader strategies. Use when fixing the Close[0] bug, implementing OnMarketData hooks, adding tick-level price tracking, or debugging trailing stops that only update at bar close.
---
# CRITICAL: Live Price Tracking vs. Bar Close Data

**Severity:** CRITICAL - Impacts real-time trading performance
**Discovered:** V5.3 development (multi-AI code review)
**Status:** FIXED in V5.3.1
**Impact:** 50-90% improvement in trailing stop execution

---

## The Problem: Close[0] Bug

### Symptom
Trailing stops not updating between bar closes. Position hits profit target intra-bar but order doesn't execute until next bar, losing significant profit potential.

### Root Cause
```csharp
// ❌ CRITICAL BUG - Only evaluates at bar close
protected override void OnBarUpdate()
{
    if (Close[0] > highestPrice)
    {
        highestPrice = Close[0];
        double newStop = highestPrice - atrDistance;
        SetStopLoss(newStop);  // Order update delayed!
    }
}
```

**Why This Fails:**
- `Close[0]` only updates when bar closes
- OnBarUpdate() only fires at bar close (default Calculate mode)
- Intra-bar price movements ignored completely

### Real-World Impact Example
```
10:05:00 - Enter long @ 4500
10:05:15 - Price hits 4510 (10 point profit)
10:05:30 - Price reverses to 4505
10:06:00 - Bar closes @ 4505
          → Trailing stop ONLY NOW updates to 4495
10:06:15 - Price drops to 4495, stopped out

Result: 5 point loss instead of 10 point profit
Lost:   15 points due to delayed stop update
```

---

## The Solution: OnMarketData Pattern

### How It Works
```csharp
protected override void OnMarketData(MarketDataEventArgs e)
{
    // Filter: Only actual trades, not bid/ask/volume
    if (e.MarketDataType != MarketDataType.Last)
        return;

    // Filter: Only this instrument
    if (e.Instrument != Instrument)
        return;

    // Live price available tick-by-tick
    double livePrice = e.Price;
    lastTickTime = DateTime.Now;

    // Update trailing stop in real-time
    if (Position.MarketPosition == MarketPosition.Long && livePrice > highestPrice)
    {
        highestPrice = livePrice;
        double newStop = highestPrice - atrDistance;

        // Rate-limited modification (Apex compliance)
        if (CanModifyOrder())
            SetStopLoss(newStop);
    }
}
```

### Key Points
1. `e.MarketDataType == MarketDataType.Last` - Only real trades (not bid/ask updates)
2. `e.Instrument == Instrument` - Only this chart's instrument
3. `e.Price` - Live tick-by-tick price
4. Fires on **every tick**, not just bar close

---

## GetLivePrice() Helper (Fallback Chain)

### Purpose
Provides live price even when OnMarketData not firing (rare cases)

### Implementation
```csharp
private double GetLivePrice()
{
    // Priority 1: Bid/Ask midpoint (most accurate)
    if (Ask > 0 && Bid > 0)
        return (Bid + Ask) / 2.0;

    // Priority 2: Ask alone
    if (Ask > 0)
        return Ask;

    // Priority 3: Bid alone
    if (Bid > 0)
        return Bid;

    // Last resort: Last bar close (stale data warning)
    return Close[0];
}
```

### Usage
```csharp
protected override void OnBarUpdate()
{
    // Use GetLivePrice() instead of Close[0]
    double currentPrice = GetLivePrice();

    if (currentPrice > sessionHigh + TickSize)
    {
        EnterLong(qty, "ORB_Long");
        SetStopLoss(currentPrice - (atr * 2.0));
    }
}
```

---

## Rithmic-Specific Considerations

### Tick Frequency
```
RTH (9:30-16:00 ET):   50-200 ticks/min
Pre-market:             5-20 ticks/min
Overnight:              5-50 ticks/min
```

### Testing OnMarketData
```csharp
private int tickCount = 0;

protected override void OnMarketData(MarketDataEventArgs e)
{
    if (e.MarketDataType != MarketDataType.Last)
        return;

    tickCount++;

    // Log every 100 ticks to verify firing
    if (tickCount % 100 == 0)
        Print($"Ticks received: {tickCount}");
}
```

**Expected Output During RTH:**
```
Should see 100 ticks every 30-120 seconds
If gap > 2 minutes, Rithmic may have disconnected
```

### Disconnect Detection
```csharp
private DateTime lastTickTime = DateTime.Now;

protected override void OnMarketData(MarketDataEventArgs e)
{
    if (e.MarketDataType == MarketDataType.Last)
        lastTickTime = DateTime.Now;
}

protected override void OnBarUpdate()
{
    // Detect stale data (> 5 seconds without ticks during RTH)
    if ((DateTime.Now - lastTickTime).TotalSeconds > 5 && IsRTH())
    {
        Print("WARNING: Rithmic disconnect detected");

        // Pause trading or close positions
        if (Position.MarketPosition != MarketPosition.Flat)
            FlattenAll("Data disconnect");
    }
}
```

---

## Rate-Limiting Order Modifications (Apex Compliance)

### The Rule
Maximum 1 order modification per second (Apex account requirement)

### Implementation
```csharp
private DateTime lastModTime = DateTime.MinValue;
private const int MOD_DELAY_MS = 1000;

private bool CanModifyOrder()
{
    TimeSpan elapsed = DateTime.Now - lastModTime;

    if (elapsed.TotalMilliseconds < MOD_DELAY_MS)
        return false;  // Blocked - too soon

    lastModTime = DateTime.Now;
    return true;  // Allowed
}
```

### Usage in OnMarketData
```csharp
protected override void OnMarketData(MarketDataEventArgs e)
{
    if (e.MarketDataType != MarketDataType.Last)
        return;

    double livePrice = e.Price;

    // Calculate new stop
    double newStop = livePrice - (cachedATR * 2.0);

    // Only modify if:
    // 1. Stop moved in favorable direction
    // 2. Rate-limit allows modification
    if (newStop > currentStop && CanModifyOrder())
    {
        ChangeOrder(stopOrder, stopOrder.Quantity, 0, newStop);
        currentStop = newStop;
    }
}
```

**Why Rate-Limiting Matters:**
- Without it: 50-200 modifications per minute (1 per tick)
- Violates Apex rules → Account warning or closure
- With rate-limiting: Max 60 modifications per minute (safe)

---

## Complete Implementation Example

### Full Trailing Stop Pattern
```csharp
// Variables
private double highestPrice = 0;
private double lowestPrice = double.MaxValue;
private double cachedATR = 0;
private double currentStop = 0;
private DateTime lastModTime = DateTime.MinValue;
private DateTime lastTickTime = DateTime.Now;
private const int MOD_DELAY_MS = 1000;

// Cache ATR in OnBarUpdate (don't recalculate every tick)
protected override void OnBarUpdate()
{
    if (CurrentBar < BarsRequiredToPlot)
        return;

    cachedATR = ATR(14)[0];

    // Check for data staleness
    if ((DateTime.Now - lastTickTime).TotalSeconds > 5 && IsRTH())
    {
        Print("WARNING: No ticks for 5+ seconds");
    }
}

// Update trailing stops tick-by-tick
protected override void OnMarketData(MarketDataEventArgs e)
{
    if (e.MarketDataType != MarketDataType.Last)
        return;

    if (e.Instrument != Instrument)
        return;

    double livePrice = e.Price;
    lastTickTime = DateTime.Now;

    // Long position trailing
    if (Position.MarketPosition == MarketPosition.Long)
    {
        if (livePrice > highestPrice)
        {
            highestPrice = livePrice;
            double newStop = highestPrice - (cachedATR * 2.0);

            if (newStop > currentStop && CanModifyOrder())
            {
                ChangeOrder(stopOrder, stopOrder.Quantity, 0, newStop);
                currentStop = newStop;
                Print($"Stop updated: {currentStop:F2}");
            }
        }
    }
    // Short position trailing
    else if (Position.MarketPosition == MarketPosition.Short)
    {
        if (livePrice < lowestPrice)
        {
            lowestPrice = livePrice;
            double newStop = lowestPrice + (cachedATR * 2.0);

            if (newStop < currentStop && CanModifyOrder())
            {
                ChangeOrder(stopOrder, stopOrder.Quantity, 0, newStop);
                currentStop = newStop;
                Print($"Stop updated: {currentStop:F2}");
            }
        }
    }
}

// Rate-limiting function
private bool CanModifyOrder()
{
    if ((DateTime.Now - lastModTime).TotalMilliseconds < MOD_DELAY_MS)
        return false;

    lastModTime = DateTime.Now;
    return true;
}

// Fallback price helper
private double GetLivePrice()
{
    if (Ask > 0 && Bid > 0)
        return (Bid + Ask) / 2.0;

    if (Ask > 0) return Ask;
    if (Bid > 0) return Bid;

    return Close[0];
}
```

---

## Testing & Verification

### Verification Checklist
1. **OnMarketData Firing:**
   ```
   - [ ] Add Print($"Tick: {e.Price}") to OnMarketData
   - [ ] Open Output window
   - [ ] Should see 50+ prints per minute during RTH
   ```

2. **Trailing Stop Updates:**
   ```
   - [ ] Enter position
   - [ ] Watch price move in favor
   - [ ] WITHOUT bar closing, check if stop updates
   - [ ] Stop line should move on chart between bars
   ```

3. **Rate-Limiting Works:**
   ```
   - [ ] Monitor Print output for "Stop updated"
   - [ ] Should see max 1 update per second
   - [ ] NOT 50+ updates per minute
   ```

4. **Disconnect Detection:**
   ```
   - [ ] Pause data feed (Control Center → Connection)
   - [ ] Should see "WARNING: No ticks" after 5 seconds
   - [ ] Resume feed, verify recovery
   ```

---

## Migration Checklist

When updating old code:
- [ ] Search entire file for `Close[0]`
- [ ] Check each occurrence - is it for real-time decisions?
- [ ] Replace with OnMarketData hook or GetLivePrice()
- [ ] Add rate-limiting to order modifications
- [ ] Implement disconnect detection
- [ ] Test with 1-min, 5-min, and 15-min charts
- [ ] Verify memory doesn't leak (1+ hour test)
- [ ] Check no recursive OnMarketData calls
- [ ] Test Rithmic disconnect/reconnect

---

## Common Mistakes

### ❌ Processing ALL MarketDataTypes
```csharp
// WRONG - Fires on bid, ask, volume updates too
protected override void OnMarketData(MarketDataEventArgs e)
{
    UpdateTrailingStop(e.Price);  // Executes 200+ times per minute!
}
```

**CORRECT:**
```csharp
protected override void OnMarketData(MarketDataEventArgs e)
{
    if (e.MarketDataType != MarketDataType.Last)
        return;  // Only actual trades

    UpdateTrailingStop(e.Price);
}
```

### ❌ Not Checking Instrument
```csharp
// WRONG - Processes other charts' ticks
protected override void OnMarketData(MarketDataEventArgs e)
{
    if (e.MarketDataType == MarketDataType.Last)
        UpdateTrailingStop(e.Price);
}
```

**CORRECT:**
```csharp
protected override void OnMarketData(MarketDataEventArgs e)
{
    if (e.MarketDataType != MarketDataType.Last)
        return;

    if (e.Instrument != Instrument)
        return;  // Filter by instrument

    UpdateTrailingStop(e.Price);
}
```

### ❌ Calling Indicators in OnMarketData
```csharp
// WRONG - Slow indicator recalculation every tick
protected override void OnMarketData(MarketDataEventArgs e)
{
    if (e.MarketDataType != MarketDataType.Last)
        return;

    double atr = ATR(14)[0];  // ❌ Expensive!
    double newStop = e.Price - atr;
}
```

**CORRECT:**
```csharp
// Cache in OnBarUpdate
private double cachedATR = 0;

protected override void OnBarUpdate()
{
    cachedATR = ATR(14)[0];
}

protected override void OnMarketData(MarketDataEventArgs e)
{
    if (e.MarketDataType != MarketDataType.Last)
        return;

    double newStop = e.Price - cachedATR;  // ✅ Fast
}
```

---

## Performance Impact

### Before Fix (Using Close[0])
```
Memory:         +2-5 KB per position
CPU:            < 1% (OnBarUpdate only)
Order Latency:  500ms - 60 seconds (depends on bar size)
Profit Impact:  -50% to -90% on trailing stop exits
```

### After Fix (Using OnMarketData)
```
Memory:         +2-5 KB per position (same)
CPU:            < 1% per 1000 ticks (negligible)
Order Latency:  < 50ms (sub-second)
Profit Impact:  50-90% improvement on trailing exits
```

---

## Summary

### Critical Rules
1. **NEVER use Close[0] for real-time decisions**
2. **ALWAYS use OnMarketData for trailing stops**
3. **ALWAYS rate-limit order modifications (1/second)**
4. **ALWAYS filter MarketDataType.Last only**
5. **ALWAYS check e.Instrument == Instrument**
6. **ALWAYS cache indicators (don't recalculate every tick)**

### Quick Audit
```
Search your code for: "Close[0]"
If found in context of:
- Trailing stop updates → FIX REQUIRED
- Live price decisions → FIX REQUIRED
- Entry/exit logic → FIX REQUIRED

Replace with OnMarketData pattern shown above
```

---

## Related Skills
- [ninjatrader-strategy-dev](../ninjatrader-strategy-dev/SKILL.md) - Full code patterns
- [apex-rithmic-trading](../apex-rithmic-trading/SKILL.md) - Account compliance
- [trading-code-review](../trading-code-review/SKILL.md) - Pre-live checklist
- [wsgta-trading-system](../wsgta-trading-system/SKILL.md) - Trading rules
- [delegation-bridge](../delegation-bridge/SKILL.md) - Safe deployment execution
- [wearable-project](../antigravity-core/wearable-project.md) - Portability standards

Overview

This skill is a practical guide for implementing live price tracking in NinjaTrader 8 strategies to eliminate delayed trailing stops and other real-time decision bugs. It explains the Close[0] pitfall, the OnMarketData pattern, rate-limited order modifications, and fallbacks for stale feeds. Use it to improve intra-bar execution and reduce missed profit opportunities.

How this skill works

It replaces bar-close-dependent logic with tick-level handling via OnMarketData, filtering for MarketDataType.Last and the current instrument. Indicators are cached on bar close and used during ticks to compute stops without recalculating expensive indicators. A rate limiter ensures order changes comply with broker limits and a last-tick timestamp detects data disconnects.

When to use it

  • Trailing stops that only update at bar close
  • Entry/exit rules that need intrabar accuracy
  • Fixing logic that relies on Close[0] for real-time decisions
  • Detecting and reacting to data feed disconnects
  • Implementing Apex/Rithmic order modification limits

Best practices

  • Filter OnMarketData for MarketDataType.Last and e.Instrument == Instrument
  • Cache indicators in OnBarUpdate; reference cached values in OnMarketData
  • Rate-limit order modifications (e.g., 1 per 1000 ms) to avoid broker penalties
  • Use a GetLivePrice() fallback that prefers midpoint (Bid+Ask)/2, then Ask, then Bid, then Close[0]
  • Log tick counts and last-tick times to verify feed health and detect disconnects

Example use cases

  • Convert a trailing stop that used Close[0] to an OnMarketData-driven update so stops move intra-bar
  • Use GetLivePrice() in entry logic on multi-timeframe charts where OnMarketData may not fire consistently
  • Add disconnect detection to flatten positions if no ticks arrive for a configured threshold (e.g., 5s during RTH)
  • Implement rate-limited ChangeOrder/SetStopLoss to stay within Apex modification rules

FAQ

Why not use Close[0] for trailing stops?

Close[0] updates only at bar close, so trailing stops based on it will ignore intra-bar favorable moves and can cause missed profits.

How do I avoid recalculating ATR every tick?

Cache ATR in OnBarUpdate and reference the cached value inside OnMarketData; do not call ATR(14)[0] on every tick.