candle-annotator/inference-ui-prompt.md

16 KiB

Inference Integration: ML Predictions → Next.js Annotation Tool

Context

We have an existing Next.js frontend app that uses TradingView Lightweight Charts for candlestick rendering and span annotation of forex patterns. We also have a Python ML pipeline that trains pattern recognition models and serves predictions via a REST API (FastAPI on port 8001).

This prompt describes how to connect the inference API to the frontend so users can see model predictions overlaid on their charts alongside their own human annotations.


Architecture

┌─────────────────────────────────────────────────────┐
│  Next.js Frontend                                   │
│                                                     │
│  ┌───────────────────────────────────────────────┐  │
│  │  Lightweight Charts                           │  │
│  │                                               │  │
│  │  [Candles] ← raw OHLCV                        │  │
│  │  [Human Annotations] ← solid colored overlays │  │
│  │  [Model Predictions] ← dashed/hatched overlays│  │
│  │  [Disagreements] ← highlighted borders        │  │
│  └───────────────────────────────────────────────┘  │
│                                                     │
│  ┌──────────────┐  ┌──────────────┐                 │
│  │ Annotation   │  │ Prediction   │                 │
│  │ Panel        │  │ Panel        │                 │
│  └──────────────┘  └──────────────┘                 │
└──────────────────┬──────────────────────────────────┘
                   │
          Next.js API routes
          /api/predict
          /api/predict/batch
                   │
       ┌───────────▼───────────┐
       │  Python Inference API │
       │  FastAPI :8001        │
       │                       │
       │  /predict             │
       │  /predict/batch       │
       │  /model/info          │
       │  /model/labels        │
       └───────────────────────┘

1. Next.js API Routes (Proxy to Python Backend)

Create Next.js API routes that proxy requests to the Python inference server. This avoids CORS issues and lets you add auth/rate-limiting on the Next.js side.

/api/predict — Predict patterns for visible candles

// app/api/predict/route.ts

// Request body:
interface PredictRequest {
  pair: string;              // e.g. "EURUSD"
  timeframe: string;         // e.g. "1H"
  candles: {
    time: number;            // unix timestamp
    open: number;
    high: number;
    low: number;
    close: number;
    volume: number;
  }[];
}

// Response body from Python API:
interface PredictResponse {
  predictions: {
    time: number;
    label: string;           // "bull_flag", "head_and_shoulders", "O" (no pattern)
    confidence: number;      // 0.0 - 1.0
  }[];
  spans: {                   // predictions grouped into continuous spans
    start_time: number;
    end_time: number;
    label: string;
    avg_confidence: number;
  }[];
  model_info: {
    model_name: string;
    model_version: string;
    trained_at: string;
    dataset_version: string;
  };
}

/api/predict/batch — Predict on full historical dataset

Same interface but accepts a date range instead of raw candles. The Python backend loads the data from its own OHLCV store and returns predictions. Useful for backfill — "show me all patterns the model finds in the last 6 months."

interface BatchPredictRequest {
  pair: string;
  timeframe: string;
  start_date: string;        // ISO 8601
  end_date: string;
}

/api/model/info — Get current model metadata

interface ModelInfoResponse {
  model_name: string;
  model_version: string;
  model_type: string;        // "xgboost", "cnn_1d", etc.
  trained_at: string;
  dataset_version: string;
  feature_engineering: boolean;
  labels: string[];          // all pattern labels the model knows
  per_class_metrics: {
    [label: string]: {
      precision: number;
      recall: number;
      f1: number;
      training_samples: number;
    };
  };
}

2. Prediction State Management

Add a prediction layer to the existing app state. Keep it separate from annotation state — they are independent data sources that render on the same chart.

// types/predictions.ts

interface PredictionSpan {
  id: string;                  // generated from start_time + label
  startTime: number;
  endTime: number;
  label: string;
  avgConfidence: number;
  source: "model";             // always "model", vs "human" for annotations
}

interface PredictionState {
  spans: PredictionSpan[];
  isLoading: boolean;
  error: string | null;
  modelInfo: ModelInfoResponse | null;
  visible: boolean;            // toggle prediction layer on/off
  confidenceThreshold: number; // filter: only show predictions above this
  selectedLabels: string[];    // filter: which pattern types to show
  autoPredict: boolean;        // auto-run predictions when chart scrolls
}

When to fetch predictions

Predictions should be fetched:

  • On demand: User clicks a "Run Model" button
  • On chart scroll/zoom (if autoPredict is on): When the visible candle range changes, debounce 500ms, then send the visible candles to /api/predict. Only send candles that haven't been predicted yet (cache previous results by time range).
  • On batch backfill: User selects a date range and clicks "Predict All"

Caching

Cache predictions in a Map keyed by ${pair}_${timeframe}_${startTime}_${endTime}_${modelVersion}. Invalidate cache when the model version changes. This prevents re-predicting the same candles on every scroll.


3. Rendering Predictions on Lightweight Charts

Predictions render as a separate visual layer on the same chart instance as human annotations. They must be visually distinct from human annotations.

Visual Distinction

Property Human Annotations Model Predictions
Background fill Solid color, 20% opacity Diagonal hatched pattern or 10% opacity
Border Solid 2px Dashed 2px
Label tag Solid background Outlined/hollow background
Label text "bull_flag" "bull_flag (87%)" with confidence
Position Above candles Below candles (avoid overlap)

Implementation with Lightweight Charts

Lightweight Charts doesn't have a native "span highlight" feature, so use one of these approaches:

Option A: Custom Series Markers + Box Plugin

Use the createBox or a custom plugin to draw rectangles behind candle ranges. Lightweight Charts v4+ supports plugins that can draw on the canvas.

// Pseudo-code for rendering a prediction span

function renderPredictionSpan(chart, span: PredictionSpan, labelConfig: LabelConfig) {
  // Use a box/rectangle primitive
  // Position: from span.startTime to span.endTime on X axis
  // Full price range of candles in that span on Y axis
  // Style: dashed border, hatched or low-opacity fill
  // Color: from labelConfig based on span.label

  // Add a marker at the first candle of the span with label text
  series.setMarkers([
    ...existingMarkers,
    {
      time: span.startTime,
      position: 'belowBar',       // below for predictions, above for human
      color: labelConfig.color,
      shape: 'square',
      text: `${span.label} (${Math.round(span.avgConfidence * 100)}%)`,
    }
  ]);
}

Option B: Background color per candle using a secondary series

Create a histogram series behind the candles that uses color to indicate predictions. Each bar's color maps to a pattern label. Simpler but less visually rich.

const predictionSeries = chart.addHistogramSeries({
  priceScaleId: '',          // overlay on main scale
  color: 'transparent',
  lastValueVisible: false,
  priceLineVisible: false,
});

// Set data with per-bar colors
predictionSeries.setData(
  predictedCandles.map(c => ({
    time: c.time,
    value: c.high,             // height of the bar
    color: c.label !== 'O'
      ? `${labelColorMap[c.label]}33`  // 20% opacity hex
      : 'transparent',
  }))
);

Option C: Custom drawing on the chart canvas (most control)

Use the Lightweight Charts plugin API to draw directly on the canvas. This gives full control over hatching, dashed borders, etc.

import { createChart, IChartApi } from 'lightweight-charts';

// After chart is created, access the canvas and draw overlays
// Use requestAnimationFrame to sync with chart rendering
// Listen to chart.timeScale().subscribeVisibleLogicalRangeChange()
// to redraw when the user scrolls

Recommended approach: Start with Option B (histogram series) for a quick implementation, then upgrade to Option C (canvas plugin) for the polished version. Option A works if you find or build a good box plugin.


4. Prediction Controls Panel

Add a panel (sidebar or floating) next to the annotation panel with these controls:

Controls

┌─────────────────────────────────────────┐
│ Model Predictions                  [ON] │ ← master toggle
├─────────────────────────────────────────┤
│ Model: candlestick_v1 (v3)             │
│ Type: XGBoost                           │
│ Trained: 2024-03-20                     │
│ Dataset: v12 (847 annotations)          │
├─────────────────────────────────────────┤
│ [Run on Visible] [Predict All]          │ ← action buttons
│ Auto-predict on scroll: [OFF]           │
├─────────────────────────────────────────┤
│ Confidence threshold: ━━━━━━●━━ 0.70    │ ← slider, filters
│                                         │
│ Show patterns:                          │
│  ☑ bull_flag      (P:0.89 R:0.76)     │ ← per-class metrics
│  ☑ bear_flag      (P:0.82 R:0.71)     │
│  ☑ head_shoulders (P:0.74 R:0.65)     │
│  ☐ double_bottom  (P:0.68 R:0.52)     │ ← low recall, user may hide
│  ☑ wedge_up       (P:0.85 R:0.79)     │
├─────────────────────────────────────────┤
│ Predictions visible: 12                 │
│ Agreements with human: 8/12             │
│ Disagreements: 4                        │
│  → [Show only disagreements]            │
└─────────────────────────────────────────┘

Per-class metrics display

Fetch from /api/model/info on load. Show precision (P) and recall (R) next to each label checkbox so the user knows which patterns the model is reliable for. Low-metric patterns can be hidden by default.


5. Disagreement Detection

The most valuable feature: comparing human annotations with model predictions.

Logic

For each time range, compare human annotation spans with prediction spans:

interface Disagreement {
  time_range: { start: number; end: number };
  human_label: string | null;      // null if model predicted but human didn't annotate
  model_label: string | null;      // null if human annotated but model missed
  model_confidence: number;
  type: "missed_by_model"          // human annotated, model said "O"
      | "missed_by_human"          // model predicted pattern, human didn't annotate
      | "label_mismatch";          // both see a pattern but disagree on which
}

How to compute disagreements

  1. Get all human annotation spans for the visible range.
  2. Get all model prediction spans for the visible range.
  3. For each human span, check if any prediction span overlaps (>50% time overlap):
    • No overlap → missed_by_model
    • Overlap but different label → label_mismatch
    • Overlap and same label → agreement
  4. For each prediction span not matched to a human span → missed_by_human

Rendering disagreements

  • missed_by_model: Red dashed border around the human annotation (model couldn't see this)
  • missed_by_human: Yellow pulsing/blinking highlight (model found something you didn't label — review it!)
  • label_mismatch: Orange border with both labels shown

missed_by_human is especially valuable — the model may be finding patterns you haven't annotated yet. Add a quick action: clicking a missed_by_human prediction opens the annotation dialog pre-filled with the model's suggested label so you can confirm or correct it with one click.


6. Feedback Loop: Predictions → New Annotations

This is what makes the system improve over time.

Flow

1. Model predicts a pattern the user didn't annotate (missed_by_human)
2. User sees it highlighted on the chart
3. User clicks it → annotation dialog opens pre-filled:
   - Start/end time from prediction span
   - Label from prediction
   - Confidence shown
4. User either:
   a. Confirms → saves as a new human annotation
   b. Corrects label → saves with corrected label
   c. Dismisses → optionally mark as "not a pattern" (negative example)
5. New annotation is exported → fed into next training cycle

"Not a pattern" negative examples

When the model predicts something and the user explicitly says "this is not a pattern," save this as a negative annotation:

{
  "start_time": "...",
  "end_time": "...",
  "label": "O",
  "source": "human_correction",
  "model_predicted": "bull_flag",
  "model_confidence": 0.72
}

These negative examples are extremely valuable for training — they teach the model to stop making specific false positive mistakes.


7. API Error Handling & Loading States

When inference API is unavailable

  • Show a subtle banner: "Model server offline — predictions unavailable"
  • All prediction UI controls become disabled/greyed out
  • Human annotation continues to work normally (never block annotation on inference)
  • Poll /api/model/info every 30 seconds, auto-reconnect when available

Loading states

  • While predictions are loading, show a skeleton/shimmer overlay on the chart area
  • For batch predictions on large date ranges, show a progress indicator
  • Allow the user to cancel a long-running batch prediction

Stale predictions

  • When the user scrolls to a range that has cached predictions from an older model version, show a subtle indicator: "Predictions from model v2 — click to refresh with v3"
  • When a new model is deployed, invalidate all cached predictions and show "New model available — rerun predictions?"

8. Environment Configuration

# .env.local

# Inference API
INFERENCE_API_URL=http://localhost:8001
INFERENCE_API_TIMEOUT=10000       # ms, for single prediction requests
INFERENCE_BATCH_TIMEOUT=120000    # ms, for batch predictions

# Feature flags
NEXT_PUBLIC_PREDICTIONS_ENABLED=true
NEXT_PUBLIC_AUTO_PREDICT_DEFAULT=false
NEXT_PUBLIC_DEFAULT_CONFIDENCE_THRESHOLD=0.70

Summary of Integration Points

  1. Next.js API routes proxy to Python FastAPI inference server
  2. Prediction state lives alongside annotation state, fetched on demand or auto-scroll
  3. Rendering uses Lightweight Charts histogram series (quick) or canvas plugin (polished) with visual distinction from human annotations
  4. Controls panel lets user toggle predictions, filter by confidence/label, view model metrics
  5. Disagreement detection compares human vs model and highlights mismatches
  6. Feedback loop lets user confirm/correct/dismiss model predictions as new annotations
  7. Negative examples from dismissed predictions feed back into training
  8. Error handling ensures annotation tool works independently of inference availability