feat: add ML service scaffolding with Python FastAPI, Docker, and MLflow setup
This commit is contained in:
parent
92abab5316
commit
1a653c5866
18 changed files with 1952 additions and 2593 deletions
427
inference-ui-prompt.md
Normal file
427
inference-ui-prompt.md
Normal file
|
|
@ -0,0 +1,427 @@
|
|||
# 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
|
||||
|
||||
```typescript
|
||||
// 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."
|
||||
|
||||
```typescript
|
||||
interface BatchPredictRequest {
|
||||
pair: string;
|
||||
timeframe: string;
|
||||
start_date: string; // ISO 8601
|
||||
end_date: string;
|
||||
}
|
||||
```
|
||||
|
||||
### `/api/model/info` — Get current model metadata
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
```typescript
|
||||
// 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.
|
||||
|
||||
```typescript
|
||||
// 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.
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
```json
|
||||
{
|
||||
"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
|
||||
# .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
|
||||
Loading…
Add table
Add a link
Reference in a new issue