@@ -22,38 +22,44 @@ function seededRandom(seed: number): () => number {
2222 } ;
2323}
2424
25- export function generateMockPrediction ( ) : PredictionResponse {
26- // Use minute-of-day as seed so it changes every 5 minutes but is stable within
25+ /** Shared candle seed — ensures hindcast and prediction use the same price walk. */
26+ function getCandleSeed ( ) : number {
2727 const now = new Date ( ) ;
28- const minuteSlot = Math . floor (
29- ( now . getHours ( ) * 60 + now . getMinutes ( ) ) / 5 ,
30- ) ;
31- const rand = seededRandom ( minuteSlot * 1337 + now . getDate ( ) * 7 ) ;
28+ const minuteSlot = Math . floor ( ( now . getHours ( ) * 60 + now . getMinutes ( ) ) / 5 ) ;
29+ return minuteSlot * 1337 + now . getDate ( ) * 7 ;
30+ }
3231
33- // Random walk for context candles (24 hours = 288 five-min bars)
34- const contextCandles = [ ] ;
35- let price = BASE_PRICE + ( rand ( ) - 0.5 ) * 80 ;
36- const baseTime = Math . floor ( now . getTime ( ) / 1000 ) - 288 * 300 ;
32+ interface MockCandle { time : number ; open : number ; high : number ; low : number ; close : number ; volume : number ; }
3733
34+ /** Generate 288 five-minute candles using the shared seed. */
35+ function generateCandles ( ) : MockCandle [ ] {
36+ const rand = seededRandom ( getCandleSeed ( ) ) ;
37+ const now = new Date ( ) ;
38+ const baseTime = Math . floor ( now . getTime ( ) / 1000 ) - 288 * 300 ;
39+ const candles : MockCandle [ ] = [ ] ;
40+ let price = BASE_PRICE + ( rand ( ) - 0.5 ) * 80 ;
3841 for ( let i = 0 ; i < 288 ; i ++ ) {
3942 const ret = ( rand ( ) - 0.5 ) * 4 ;
4043 const open = round ( price ) ;
4144 const close = round ( price + ret ) ;
4245 const high = round ( Math . max ( open , close ) + rand ( ) * 3 ) ;
4346 const low = round ( Math . min ( open , close ) - rand ( ) * 3 ) ;
4447 const volume = Math . floor ( 5000 + rand ( ) * 30000 ) ;
45- contextCandles . push ( {
46- time : baseTime + i * 300 ,
47- open,
48- high,
49- low,
50- close,
51- volume,
52- } ) ;
48+ candles . push ( { time : baseTime + i * 300 , open, high, low, close, volume } ) ;
5349 price = close ;
5450 }
51+ return candles ;
52+ }
5553
56- const lastClose = price ;
54+ export function generateMockPrediction ( ) : PredictionResponse {
55+ const now = new Date ( ) ;
56+ const contextCandles = generateCandles ( ) ;
57+ const lastClose = contextCandles [ contextCandles . length - 1 ] . close ;
58+
59+ // Use same seed for forecast portion (advanced past the candle generation)
60+ const rand = seededRandom ( getCandleSeed ( ) ) ;
61+ // Advance rand past the candle generation (1 for initial price + 4 per candle: ret, high, low, volume)
62+ for ( let i = 0 ; i < 288 * 4 + 1 ; i ++ ) rand ( ) ;
5763
5864 // Forecast: slight drift + expanding uncertainty
5965 const drift = ( rand ( ) - 0.45 ) * 0.15 ; // slight long bias
@@ -168,14 +174,24 @@ export function generateMockPrediction(): PredictionResponse {
168174}
169175
170176export function generateMockHindcast ( n = 6 ) : HindcastResponse {
171- const now = Date . now ( ) ;
177+ const now = new Date ( ) ;
172178 const rand = seededRandom ( 77 ) ;
173179 const predictions = [ ] ;
174180
181+ // Use the SAME candle series as the prediction so prices align
182+ const candles = generateCandles ( ) ;
183+
175184 for ( let i = n ; i >= 1 ; i -- ) {
176- const predTime = now - i * 300_000 ; // Each prediction 5 min apart
177- const barsElapsed = i ;
178- let price = BASE_PRICE + ( rand ( ) - 0.5 ) * 40 ;
185+ const predTime = now . getTime ( ) - i * 1_800_000 ; // 30 min apart
186+ const barsElapsed = i * 6 ;
187+
188+ // Anchor to actual candle close at prediction time
189+ const predTs = predTime / 1000 ;
190+ let anchorBar = 0 ;
191+ for ( let ci = candles . length - 1 ; ci >= 0 ; ci -- ) {
192+ if ( candles [ ci ] . time <= predTs ) { anchorBar = ci ; break ; }
193+ }
194+ const price = candles [ anchorBar ] . close ;
179195 const drift = ( rand ( ) - 0.45 ) * 0.12 ;
180196
181197 const horizons = [ ...Array . from ( { length : 26 } , ( _ , j ) => 1 + j * 3 ) , 78 ]
@@ -207,12 +223,15 @@ export function generateMockHindcast(n = 6): HindcastResponse {
207223 samplePaths . push ( path ) ;
208224 }
209225
210- // Realized prices: fill in for elapsed bars, null for future
226+ // Realized prices: use actual candle closes from the shared price walk
211227 const realizedPrices : ( number | null ) [ ] = horizons . map ( ( h ) => {
212228 if ( h <= barsElapsed ) {
213- // Simulate realized price near median with some noise
214- const mid = price + drift * h * 0.3 ;
215- return round ( mid + ( rand ( ) - 0.5 ) * 4 ) ;
229+ const realBar = anchorBar + h ;
230+ if ( realBar >= 0 && realBar < candles . length ) {
231+ return candles [ realBar ] . close ;
232+ }
233+ // Fallback if beyond candle range
234+ return round ( price + drift * h * 0.3 + ( rand ( ) - 0.5 ) * 4 ) ;
216235 }
217236 return null ;
218237 } ) ;
@@ -224,11 +243,35 @@ export function generateMockHindcast(n = 6): HindcastResponse {
224243 const dirCorrect = rand ( ) > 0.4 ;
225244 const coverageOuter = + ( 0.5 + rand ( ) * 0.4 ) . toFixed ( 4 ) ;
226245 const coverageInner = + ( 0.3 + rand ( ) * 0.3 ) . toFixed ( 4 ) ;
227- const bestRmse = + ( 0.5 + rand ( ) * 3 ) . toFixed ( 2 ) ;
228246 const trackDur = Math . floor ( 3 + rand ( ) * 10 ) ;
229247 const verdict = coverageOuter >= 0.7 && dirCorrect ? "PASS" as const
230248 : coverageOuter >= 0.5 || dirCorrect ? "PARTIAL" as const : "FAIL" as const ;
231249
250+ // Compute top-10 best paths by RMSE against realized prices
251+ const pathScores = samplePaths . map ( ( path , idx ) => {
252+ let sumSq = 0 ;
253+ let count = 0 ;
254+ for ( let hi = 0 ; hi < horizons . length ; hi ++ ) {
255+ const rp = realizedPrices [ hi ] ;
256+ if ( rp != null ) {
257+ sumSq += ( path [ hi ] - ( rp as number ) ) ** 2 ;
258+ count ++ ;
259+ }
260+ }
261+ return { idx, rmse : count > 0 ? Math . sqrt ( sumSq / count ) : Infinity } ;
262+ } ) . sort ( ( a , b ) => a . rmse - b . rmse ) . slice ( 0 , 10 ) ;
263+
264+ const bestPathsList = pathScores . map ( ( { idx, rmse } ) => ( {
265+ path_index : idx ,
266+ path_values : samplePaths [ idx ] ,
267+ rmse_pts : + rmse . toFixed ( 2 ) ,
268+ tracking_duration_bars : trackDur ,
269+ tracking_threshold_pts : 2.0 ,
270+ deviations : realizedPrices
271+ . filter ( ( v ) : v is number => v != null )
272+ . map ( ( ) => + ( ( rand ( ) - 0.5 ) * 3 ) . toFixed ( 2 ) ) ,
273+ } ) ) ;
274+
232275 predictions . push ( {
233276 timestamp : new Date ( predTime ) . toISOString ( ) ,
234277 last_close : round ( price ) ,
@@ -242,16 +285,7 @@ export function generateMockHindcast(n = 6): HindcastResponse {
242285 coverage_p25_p75 : coverageInner ,
243286 direction_correct : dirCorrect ,
244287 median_rmse_pts : + ( 1 + rand ( ) * 5 ) . toFixed ( 2 ) ,
245- best_paths : [ {
246- path_index : Math . floor ( rand ( ) * 30 ) ,
247- path_values : samplePaths [ Math . floor ( rand ( ) * 30 ) ] ,
248- rmse_pts : bestRmse ,
249- tracking_duration_bars : trackDur ,
250- tracking_threshold_pts : 2.0 ,
251- deviations : realizedPrices
252- . filter ( ( v ) : v is number => v != null )
253- . map ( ( ) => + ( ( rand ( ) - 0.5 ) * 3 ) . toFixed ( 2 ) ) ,
254- } ] ,
288+ best_paths : bestPathsList ,
255289 verdict,
256290 signal_direction : sigDir ,
257291 expected_return_pts : + ( ( rand ( ) - 0.5 ) * 8 ) . toFixed ( 2 ) ,
0 commit comments