Spaces:
Running
Running
Yan Wang
commited on
Commit
·
be72531
1
Parent(s):
aa090f2
update App_New
Browse files- src/App_New.tsx +52 -59
src/App_New.tsx
CHANGED
|
@@ -69,7 +69,6 @@ function generateFakeJSON(maxLen = 500): DataDict {
|
|
| 69 |
intraday.push(Number(p.toFixed(2)));
|
| 70 |
}
|
| 71 |
series.push({ date: dates[i], close: intraday });
|
| 72 |
-
// update end-of-day anchor for next day open drift
|
| 73 |
const drift = mu + (sigma / Math.sqrt(252)) * ((Math.random() - 0.5) * 2.0);
|
| 74 |
price = intraday[intraday.length - 1] * (1 + drift);
|
| 75 |
}
|
|
@@ -315,83 +314,62 @@ export default function App() {
|
|
| 315 |
return rows;
|
| 316 |
}, [assets, rawData, dates.length, activeIntradayIdx]);
|
| 317 |
|
| 318 |
-
|
| 319 |
const { multiRows, dayStarts } = useMemo(() => {
|
| 320 |
const rows: any[] = [];
|
| 321 |
const starts: { x: number; date: string }[] = [];
|
| 322 |
if (!dates.length || windowLen < 1) return { multiRows: rows, dayStarts: starts };
|
| 323 |
|
| 324 |
-
//
|
| 325 |
-
const
|
| 326 |
-
// cumulative index per asset (for normalized mode)
|
| 327 |
const cumIdx: Record<string, number> = {};
|
| 328 |
-
assets.forEach(a => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
|
| 330 |
let x = 0;
|
| 331 |
-
|
|
|
|
|
|
|
| 332 |
|
| 333 |
-
|
| 334 |
-
const dayLen =
|
| 335 |
-
if (dayLen === 0) continue;
|
| 336 |
-
|
| 337 |
-
// first valid price of the day per asset (fallback for day start)
|
| 338 |
-
const firstOfDay: Record<string, number | null> = {};
|
| 339 |
-
assets.forEach(a => {
|
| 340 |
-
const arr = rawData[a]?.[d]?.close ?? [];
|
| 341 |
-
let v: number | null = null;
|
| 342 |
-
for (let j = 0; j < arr.length; j++) {
|
| 343 |
-
if (typeof arr[j] === "number" && Number.isFinite(arr[j])) { v = arr[j]!; break; }
|
| 344 |
-
}
|
| 345 |
-
firstOfDay[a] = v;
|
| 346 |
-
});
|
| 347 |
-
|
| 348 |
-
const dayStartX = x + 1;
|
| 349 |
|
| 350 |
for (let i = 0; i < dayLen; i++) {
|
| 351 |
const row: Record<string, any> = { x: x + 1, day: dates[d], minute: i + 1 };
|
| 352 |
-
let anyValue = false;
|
| 353 |
|
| 354 |
assets.forEach(a => {
|
| 355 |
-
const arr = rawData[a]?.[d]?.close
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
if (lastSeen[a] != null) v = lastSeen[a] as number;
|
| 364 |
-
else if (firstOfDay[a] != null) v = firstOfDay[a] as number; // fallback to first valid of the day
|
| 365 |
-
}
|
| 366 |
-
|
| 367 |
-
if (v != null) {
|
| 368 |
-
if (multiNormalize) {
|
| 369 |
-
if (lastSeen[a] != null && v !== lastSeen[a]) {
|
| 370 |
-
const r = v / (lastSeen[a] as number);
|
| 371 |
-
if (Number.isFinite(r) && r > 0) cumIdx[a] *= r;
|
| 372 |
-
}
|
| 373 |
-
row[a] = cumIdx[a];
|
| 374 |
-
} else {
|
| 375 |
-
row[a] = v;
|
| 376 |
}
|
| 377 |
-
|
| 378 |
-
|
| 379 |
} else {
|
| 380 |
row[a] = null;
|
| 381 |
}
|
| 382 |
});
|
| 383 |
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
x++;
|
| 387 |
-
}
|
| 388 |
-
}
|
| 389 |
-
|
| 390 |
-
if (x >= dayStartX) {
|
| 391 |
-
starts.push({ x: dayStartX, date: dates[d] });
|
| 392 |
}
|
| 393 |
}
|
| 394 |
-
|
| 395 |
return { multiRows: rows, dayStarts: starts };
|
| 396 |
}, [assets, rawData, dates, windowLen, multiNormalize]);
|
| 397 |
|
|
@@ -557,6 +535,20 @@ export default function App() {
|
|
| 557 |
</div>
|
| 558 |
);
|
| 559 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 560 |
return (
|
| 561 |
<div className="p-6 bg-gray-50 min-h-screen flex flex-col gap-6">
|
| 562 |
<div className="flex flex-wrap justify-between items-center gap-3">
|
|
@@ -647,8 +639,8 @@ export default function App() {
|
|
| 647 |
tick={{ fontSize: 10 }}
|
| 648 |
label={{ value: "Minute (concatenated days)", position: "insideBottomRight", offset: -2, fontSize: 10 }}
|
| 649 |
/>
|
| 650 |
-
{/*
|
| 651 |
-
<YAxis domain={
|
| 652 |
<Tooltip
|
| 653 |
contentStyle={{ fontSize: 12 }}
|
| 654 |
labelFormatter={(_, payload: any) => {
|
|
@@ -680,7 +672,8 @@ export default function App() {
|
|
| 680 |
onMouseEnter={() => setHoverAsset(a)}
|
| 681 |
onMouseLeave={() => setHoverAsset(null)}
|
| 682 |
onClick={() => setSelectedAsset((p) => (p === a ? null : a))}
|
| 683 |
-
|
|
|
|
| 684 |
/>
|
| 685 |
))}
|
| 686 |
</LineChart>
|
|
|
|
| 69 |
intraday.push(Number(p.toFixed(2)));
|
| 70 |
}
|
| 71 |
series.push({ date: dates[i], close: intraday });
|
|
|
|
| 72 |
const drift = mu + (sigma / Math.sqrt(252)) * ((Math.random() - 0.5) * 2.0);
|
| 73 |
price = intraday[intraday.length - 1] * (1 + drift);
|
| 74 |
}
|
|
|
|
| 314 |
return rows;
|
| 315 |
}, [assets, rawData, dates.length, activeIntradayIdx]);
|
| 316 |
|
| 317 |
+
// Multi-day intraday with robust carry-forward
|
| 318 |
const { multiRows, dayStarts } = useMemo(() => {
|
| 319 |
const rows: any[] = [];
|
| 320 |
const starts: { x: number; date: string }[] = [];
|
| 321 |
if (!dates.length || windowLen < 1) return { multiRows: rows, dayStarts: starts };
|
| 322 |
|
| 323 |
+
// previous minute price & cumulative index across days
|
| 324 |
+
const prevPrice: Record<string, number | null> = {};
|
|
|
|
| 325 |
const cumIdx: Record<string, number> = {};
|
| 326 |
+
assets.forEach(a => { prevPrice[a] = null; cumIdx[a] = 1; });
|
| 327 |
+
|
| 328 |
+
// helper: forward-fill within the day if possible; otherwise use prevPrice
|
| 329 |
+
function resolvePriceForMinute(arr: number[] | undefined, i: number, a: string): number | null {
|
| 330 |
+
if (arr && typeof arr[i] === "number") return arr[i]!;
|
| 331 |
+
if (prevPrice[a] != null) return prevPrice[a] as number;
|
| 332 |
+
// look ahead within the same day to find the first future tick to backfill start-of-day
|
| 333 |
+
if (arr && arr.length) {
|
| 334 |
+
for (let j = i + 1; j < arr.length; j++) {
|
| 335 |
+
if (typeof arr[j] === "number") return arr[j]!;
|
| 336 |
+
}
|
| 337 |
+
}
|
| 338 |
+
return null;
|
| 339 |
+
}
|
| 340 |
|
| 341 |
let x = 0;
|
| 342 |
+
for (let d = 0; d < Math.min(windowLen, dates.length); d++) {
|
| 343 |
+
const startX = x + 1;
|
| 344 |
+
starts.push({ x: startX, date: dates[d] });
|
| 345 |
|
| 346 |
+
const dayLenRaw = assets.reduce((m, a) => Math.max(m, rawData[a]?.[d]?.close?.length ?? 0), 0);
|
| 347 |
+
const dayLen = Math.max(1, dayLenRaw); // ensure at least one row per day
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
|
| 349 |
for (let i = 0; i < dayLen; i++) {
|
| 350 |
const row: Record<string, any> = { x: x + 1, day: dates[d], minute: i + 1 };
|
|
|
|
| 351 |
|
| 352 |
assets.forEach(a => {
|
| 353 |
+
const arr = rawData[a]?.[d]?.close;
|
| 354 |
+
const resolved = resolvePriceForMinute(arr, i, a);
|
| 355 |
+
|
| 356 |
+
if (resolved != null) {
|
| 357 |
+
// update cumIdx only when a "new real tick" occurs
|
| 358 |
+
if (prevPrice[a] != null && resolved !== prevPrice[a]) {
|
| 359 |
+
const r = resolved / (prevPrice[a] as number);
|
| 360 |
+
if (Number.isFinite(r) && r > 0) cumIdx[a] *= r;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 361 |
}
|
| 362 |
+
row[a] = multiNormalize ? cumIdx[a] : resolved;
|
| 363 |
+
prevPrice[a] = resolved;
|
| 364 |
} else {
|
| 365 |
row[a] = null;
|
| 366 |
}
|
| 367 |
});
|
| 368 |
|
| 369 |
+
rows.push(row);
|
| 370 |
+
x++;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
}
|
| 372 |
}
|
|
|
|
| 373 |
return { multiRows: rows, dayStarts: starts };
|
| 374 |
}, [assets, rawData, dates, windowLen, multiNormalize]);
|
| 375 |
|
|
|
|
| 535 |
</div>
|
| 536 |
);
|
| 537 |
|
| 538 |
+
// ------- helper to give Y-axis safe padding even when dataMin == dataMax -------
|
| 539 |
+
const multiYAxisDomain: any = [
|
| 540 |
+
(min: number) => {
|
| 541 |
+
if (!Number.isFinite(min)) return "auto";
|
| 542 |
+
const pad = Math.abs(min) * 0.005 || 0.01;
|
| 543 |
+
return min - pad;
|
| 544 |
+
},
|
| 545 |
+
(max: number) => {
|
| 546 |
+
if (!Number.isFinite(max)) return "auto";
|
| 547 |
+
const pad = Math.abs(max) * 0.005 || 0.01;
|
| 548 |
+
return max + pad;
|
| 549 |
+
}
|
| 550 |
+
];
|
| 551 |
+
|
| 552 |
return (
|
| 553 |
<div className="p-6 bg-gray-50 min-h-screen flex flex-col gap-6">
|
| 554 |
<div className="flex flex-wrap justify-between items-center gap-3">
|
|
|
|
| 639 |
tick={{ fontSize: 10 }}
|
| 640 |
label={{ value: "Minute (concatenated days)", position: "insideBottomRight", offset: -2, fontSize: 10 }}
|
| 641 |
/>
|
| 642 |
+
{/* FIX 1: robust Y domain with padding so flat lines are visible */}
|
| 643 |
+
<YAxis domain={multiYAxisDomain} tick={{ fontSize: 10 }} allowDataOverflow />
|
| 644 |
<Tooltip
|
| 645 |
contentStyle={{ fontSize: 12 }}
|
| 646 |
labelFormatter={(_, payload: any) => {
|
|
|
|
| 672 |
onMouseEnter={() => setHoverAsset(a)}
|
| 673 |
onMouseLeave={() => setHoverAsset(null)}
|
| 674 |
onClick={() => setSelectedAsset((p) => (p === a ? null : a))}
|
| 675 |
+
/* FIX 2: connect null gaps so lines render across sparse minutes */
|
| 676 |
+
connectNulls={true}
|
| 677 |
/>
|
| 678 |
))}
|
| 679 |
</LineChart>
|