Skip to content

Commit 5a7ac78

Browse files
davclaude
authored andcommitted
Fix tracking path: use horizon offsets instead of linear indices
path_values are sampled at sparse horizons [1,4,7,10,...,78] (26 pts), not at every bar. The old code mapped them to adjacent chart indices (anchorIndex+0, +1, +2...) which compressed the entire 78-bar forecast into 26 adjacent candles. Now correctly maps each path value to anchorIndex + horizon[i], spacing them at the actual 3-bar intervals. Tracking path and projection both use explicit offset arrays for proper chart positioning. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1cc834c commit 5a7ac78

File tree

2 files changed

+32
-12
lines changed

2 files changed

+32
-12
lines changed

src/components/charts/FanChart.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ export type ChartType = "line" | "candlestick" | "ohlc";
3737
export type ForecastStyle = "bands" | "spaghetti";
3838

3939
export interface TrackingPath {
40-
realizedPrices: number[]; // price values at context candle indices
40+
realizedPrices: number[]; // price values at realized horizon points
41+
realizedOffsets: number[]; // bar offsets from anchor for each realized price
4142
projectedPrices: number[]; // price values in forecast region
42-
anchorIndex: number; // x-axis index where the path starts
43+
projectedOffsets: number[]; // horizon offsets for projected prices (relative to forecast start)
44+
anchorIndex: number; // x-axis index in context candles where prediction was made
4345
rmse: number;
4446
pathIndex: number;
4547
totalPaths: number;
@@ -535,23 +537,28 @@ export function FanChart({
535537
? (() => {
536538
const tp = trackingPath;
537539
const totalLen = allTimes.length;
538-
// Realized portion: solid teal line through context candles
540+
// Realized portion: solid teal line through context candles at correct horizon offsets
539541
const realizedData: (number | null)[] = new Array(totalLen).fill(null);
540542
for (let i = 0; i < tp.realizedPrices.length; i++) {
541-
const idx = tp.anchorIndex + i;
543+
const offset = tp.realizedOffsets[i] ?? i;
544+
const idx = tp.anchorIndex + offset;
542545
if (idx >= 0 && idx < ctxLen) {
543546
realizedData[idx] = tp.realizedPrices[i];
544547
}
545548
}
546549
// Projected portion: dashed teal line through forecast region
547550
const projectedData: (number | null)[] = new Array(totalLen).fill(null);
548-
// Connect at the NOW boundary
549-
const lastRealizedIdx = tp.anchorIndex + tp.realizedPrices.length - 1;
550-
if (lastRealizedIdx >= 0 && lastRealizedIdx < totalLen) {
551-
projectedData[lastRealizedIdx] = tp.realizedPrices[tp.realizedPrices.length - 1];
551+
// Connect at the last realized point
552+
if (tp.realizedPrices.length > 0) {
553+
const lastOffset = tp.realizedOffsets[tp.realizedPrices.length - 1] ?? (tp.realizedPrices.length - 1);
554+
const lastIdx = tp.anchorIndex + lastOffset;
555+
if (lastIdx >= 0 && lastIdx < totalLen) {
556+
projectedData[lastIdx] = tp.realizedPrices[tp.realizedPrices.length - 1];
557+
}
552558
}
553559
for (let i = 0; i < tp.projectedPrices.length; i++) {
554-
const idx = ctxLen + i;
560+
const offset = tp.projectedOffsets[i] ?? i;
561+
const idx = ctxLen + offset;
555562
if (idx < totalLen) {
556563
projectedData[idx] = tp.projectedPrices[i];
557564
}

src/components/layout/Dashboard.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ export function Dashboard() {
115115
const rawCandles = prediction.context_candles ?? [];
116116
if (!rawCandles.length) return null;
117117

118+
// Horizons from the hindcast prediction (sparse: [1, 4, 7, 10, ...])
119+
const horizons = scored.horizons;
120+
if (!horizons.length) return null;
121+
118122
// Find anchor: context candle closest to prediction timestamp
119123
const predTs = new Date(scored.timestamp).getTime() / 1000;
120124
let anchorIndex = -1;
@@ -124,24 +128,33 @@ export function Dashboard() {
124128
if (anchorIndex < 0) return null;
125129

126130
// Split path_values into realized (context region) and projected (forecast region)
131+
// Each path_values[i] corresponds to horizons[i] bars ahead of anchor
127132
const ctxLen = rawCandles.length;
128133
const realizedPrices: number[] = [];
134+
const realizedOffsets: number[] = [];
129135
const projectedPrices: number[] = [];
136+
const projectedOffsets: number[] = [];
130137

131-
for (let i = 0; i < bestPath.path_values.length; i++) {
132-
const chartIdx = anchorIndex + i;
138+
for (let i = 0; i < bestPath.path_values.length && i < horizons.length; i++) {
139+
const barOffset = horizons[i]; // bars ahead of anchor
140+
const chartIdx = anchorIndex + barOffset;
133141
if (chartIdx < ctxLen) {
134142
realizedPrices.push(bestPath.path_values[i]);
143+
realizedOffsets.push(barOffset);
135144
} else {
145+
// Forecast region: offset relative to ctxLen
136146
projectedPrices.push(bestPath.path_values[i]);
147+
projectedOffsets.push(chartIdx - ctxLen);
137148
}
138149
}
139150

140-
if (realizedPrices.length < 3) return null;
151+
if (realizedPrices.length < 2) return null;
141152

142153
return {
143154
realizedPrices,
155+
realizedOffsets,
144156
projectedPrices,
157+
projectedOffsets,
145158
anchorIndex,
146159
rmse: bestPath.rmse_pts,
147160
pathIndex: bestPath.path_index,

0 commit comments

Comments
 (0)