Spaces:
Sleeping
Sleeping
Yan Wang commited on
Commit ·
46472e0
1
Parent(s): b029aed
remove function of multi-day intraday
Browse files- src/App_New.tsx +3 -161
src/App_New.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import { useEffect, useMemo, useState } from "react";
|
| 2 |
import {
|
| 3 |
LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, CartesianGrid,
|
| 4 |
-
AreaChart, Area, BarChart, Bar
|
| 5 |
} from "recharts";
|
| 6 |
import {
|
| 7 |
apiGetLatestDataset,
|
|
@@ -12,7 +12,7 @@ import {
|
|
| 12 |
} from "./api";
|
| 13 |
|
| 14 |
type SeriesPoint = { date: string; close: number[] };
|
| 15 |
-
type ViewMode = "daily" | "intraday"
|
| 16 |
|
| 17 |
const START_DAY = 7;
|
| 18 |
|
|
@@ -103,7 +103,6 @@ export default function App() {
|
|
| 103 |
|
| 104 |
const [viewMode, setViewMode] = useState<ViewMode>("daily");
|
| 105 |
const [intradayDayIdx, setIntradayDayIdx] = useState<number | null>(null);
|
| 106 |
-
const [multiNormalize, setMultiNormalize] = useState<boolean>(true); // toggle for multi view
|
| 107 |
|
| 108 |
/* ---------- localStorage keys ---------- */
|
| 109 |
const LS_DATASET_KEY = "annot_dataset_meta";
|
|
@@ -314,88 +313,6 @@ export default function App() {
|
|
| 314 |
return rows;
|
| 315 |
}, [assets, rawData, dates.length, activeIntradayIdx]);
|
| 316 |
|
| 317 |
-
// ---------------- Multi-day intraday (稳健逐日拼接 + 跨天累计) ----------------
|
| 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 |
-
// 跨天累计倍率(仅 normalized 使用)
|
| 324 |
-
const cumIdx: Record<string, number> = {};
|
| 325 |
-
assets.forEach(a => { cumIdx[a] = 1; });
|
| 326 |
-
|
| 327 |
-
let x = 0;
|
| 328 |
-
const D = Math.min(windowLen, dates.length);
|
| 329 |
-
|
| 330 |
-
for (let d = 0; d < D; d++) {
|
| 331 |
-
starts.push({ x: x + 1, date: dates[d] });
|
| 332 |
-
|
| 333 |
-
// 本日最长分钟数(至少 1)
|
| 334 |
-
const dayLen = Math.max(
|
| 335 |
-
1,
|
| 336 |
-
assets.reduce((m, a) => Math.max(m, rawData[a]?.[d]?.close?.length ?? 0), 0)
|
| 337 |
-
);
|
| 338 |
-
|
| 339 |
-
// 找“当日首个有效价”作为锚,并准备“本日最后一个有效价”
|
| 340 |
-
const dayFirst: Record<string, number | null> = {};
|
| 341 |
-
const lastSeen: Record<string, number | null> = {};
|
| 342 |
-
|
| 343 |
-
assets.forEach(a => {
|
| 344 |
-
const arr = rawData[a]?.[d]?.close ?? [];
|
| 345 |
-
let first: number | null = null;
|
| 346 |
-
for (let i = 0; i < arr.length; i++) {
|
| 347 |
-
const v = arr[i];
|
| 348 |
-
if (typeof v === "number" && Number.isFinite(v)) { first = v; break; }
|
| 349 |
-
}
|
| 350 |
-
dayFirst[a] = first;
|
| 351 |
-
lastSeen[a] = null;
|
| 352 |
-
});
|
| 353 |
-
|
| 354 |
-
// 逐分钟行;优先当前值,其次上一分钟值,再次用 dayFirst 兜底
|
| 355 |
-
for (let i = 0; i < dayLen; i++) {
|
| 356 |
-
const row: Record<string, any> = { x: x + 1, day: dates[d], minute: i + 1 };
|
| 357 |
-
|
| 358 |
-
assets.forEach(a => {
|
| 359 |
-
const arr = rawData[a]?.[d]?.close ?? [];
|
| 360 |
-
const v = arr[i];
|
| 361 |
-
|
| 362 |
-
let price: number | null = null;
|
| 363 |
-
if (typeof v === "number" && Number.isFinite(v)) {
|
| 364 |
-
price = v;
|
| 365 |
-
lastSeen[a] = v;
|
| 366 |
-
} else if (lastSeen[a] != null) {
|
| 367 |
-
price = lastSeen[a];
|
| 368 |
-
} else if (dayFirst[a] != null) {
|
| 369 |
-
price = dayFirst[a];
|
| 370 |
-
lastSeen[a] = price;
|
| 371 |
-
} else {
|
| 372 |
-
price = null; // 这一整天没有任何有效价
|
| 373 |
-
}
|
| 374 |
-
|
| 375 |
-
if (price == null) {
|
| 376 |
-
row[a] = null;
|
| 377 |
-
} else {
|
| 378 |
-
row[a] = multiNormalize
|
| 379 |
-
? (dayFirst[a] && Number.isFinite(dayFirst[a]) ? cumIdx[a] * (price / (dayFirst[a] as number)) : null)
|
| 380 |
-
: price;
|
| 381 |
-
}
|
| 382 |
-
});
|
| 383 |
-
|
| 384 |
-
rows.push(row);
|
| 385 |
-
x++;
|
| 386 |
-
}
|
| 387 |
-
|
| 388 |
-
// 天末:若有有效价,则把“末价/首价”乘到 cumIdx,保证下一天从累计值继续
|
| 389 |
-
assets.forEach(a => {
|
| 390 |
-
if (multiNormalize && dayFirst[a] != null && lastSeen[a] != null && dayFirst[a]! > 0) {
|
| 391 |
-
cumIdx[a] *= (lastSeen[a]! / dayFirst[a]!);
|
| 392 |
-
}
|
| 393 |
-
});
|
| 394 |
-
}
|
| 395 |
-
|
| 396 |
-
return { multiRows: rows, dayStarts: starts };
|
| 397 |
-
}, [assets, rawData, dates, windowLen, multiNormalize]);
|
| 398 |
-
|
| 399 |
function realizedNextDayReturn(asset: string) {
|
| 400 |
const t = windowLen - 1;
|
| 401 |
if (t + 1 >= dates.length) return null as any;
|
|
@@ -558,20 +475,6 @@ export default function App() {
|
|
| 558 |
</div>
|
| 559 |
);
|
| 560 |
|
| 561 |
-
// ------- helper to give Y-axis safe padding even when dataMin == dataMax -------
|
| 562 |
-
const multiYAxisDomain: any = [
|
| 563 |
-
(min: number) => {
|
| 564 |
-
if (!Number.isFinite(min)) return "auto";
|
| 565 |
-
const pad = Math.abs(min) * 0.005 || 0.01;
|
| 566 |
-
return min - pad;
|
| 567 |
-
},
|
| 568 |
-
(max: number) => {
|
| 569 |
-
if (!Number.isFinite(max)) return "auto";
|
| 570 |
-
const pad = Math.abs(max) * 0.005 || 0.01;
|
| 571 |
-
return max + pad;
|
| 572 |
-
}
|
| 573 |
-
];
|
| 574 |
-
|
| 575 |
return (
|
| 576 |
<div className="p-6 bg-gray-50 min-h-screen flex flex-col gap-6">
|
| 577 |
<div className="flex flex-wrap justify-between items-center gap-3">
|
|
@@ -590,19 +493,8 @@ export default function App() {
|
|
| 590 |
<div className="flex rounded-xl overflow-hidden border border-gray-200">
|
| 591 |
<button className={`text-xs px-3 py-1.5 ${viewMode==="daily" ? "bg-gray-900 text-white" : "bg-white text-gray-700 hover:bg-gray-50"}`} onClick={()=>setViewMode("daily")}>Daily</button>
|
| 592 |
<button className={`text-xs px-3 py-1.5 ${viewMode==="intraday" ? "bg-gray-900 text-white" : "bg-white text-gray-700 hover:bg-gray-50"}`} onClick={()=>setViewMode("intraday")}>Intraday (1 day)</button>
|
| 593 |
-
<button className={`text-xs px-3 py-1.5 ${viewMode==="multi" ? "bg-gray-900 text-white" : "bg-white text-gray-700 hover:bg-gray-50"}`} onClick={()=>setViewMode("multi")}>Multi-day Intraday</button>
|
| 594 |
</div>
|
| 595 |
|
| 596 |
-
{viewMode==="multi" && (
|
| 597 |
-
<button
|
| 598 |
-
className="text-xs px-3 py-1.5 rounded-xl border border-gray-200 hover:bg-gray-50"
|
| 599 |
-
onClick={() => setMultiNormalize(v => !v)}
|
| 600 |
-
title="Toggle normalized cumulative index vs raw price"
|
| 601 |
-
>
|
| 602 |
-
{multiNormalize ? "Normalized" : "Raw Price"}
|
| 603 |
-
</button>
|
| 604 |
-
)}
|
| 605 |
-
|
| 606 |
<button onClick={exportLog} className="text-sm px-3 py-1.5 rounded-xl bg-gray-900 text-white hover:bg-black">Export My Log</button>
|
| 607 |
</div>
|
| 608 |
</div>
|
|
@@ -650,56 +542,6 @@ export default function App() {
|
|
| 650 |
/>
|
| 651 |
))}
|
| 652 |
</LineChart>
|
| 653 |
-
) : viewMode === "multi" ? (
|
| 654 |
-
<>
|
| 655 |
-
<div className="absolute right-2 top-2 z-10 text-[10px] px-2 py-0.5 rounded bg-black/60 text-white">
|
| 656 |
-
Multi • {multiNormalize ? "Normalized (cum ×)" : "Raw Price"}
|
| 657 |
-
</div>
|
| 658 |
-
<LineChart data={multiRows}>
|
| 659 |
-
<CartesianGrid strokeDasharray="3 3" />
|
| 660 |
-
<XAxis
|
| 661 |
-
dataKey="x"
|
| 662 |
-
tick={{ fontSize: 10 }}
|
| 663 |
-
label={{ value: "Minute (concatenated days)", position: "insideBottomRight", offset: -2, fontSize: 10 }}
|
| 664 |
-
/>
|
| 665 |
-
{/* Y 轴安全 padding,避免平直小波动时看不见 */}
|
| 666 |
-
<YAxis domain={multiYAxisDomain} tick={{ fontSize: 10 }} allowDataOverflow />
|
| 667 |
-
<Tooltip
|
| 668 |
-
contentStyle={{ fontSize: 12 }}
|
| 669 |
-
labelFormatter={(_, payload: any) => {
|
| 670 |
-
const p = payload?.[0]?.payload;
|
| 671 |
-
return p ? `${p.day} #${p.minute}` : "";
|
| 672 |
-
}}
|
| 673 |
-
formatter={(value: any, name: any) => {
|
| 674 |
-
const display = multiNormalize
|
| 675 |
-
? (Number.isFinite(value) ? `${(value as number).toFixed(4)}×` : value)
|
| 676 |
-
: value;
|
| 677 |
-
return [display, aliasMap[name] || name];
|
| 678 |
-
}}
|
| 679 |
-
/>
|
| 680 |
-
<Legend onClick={(o: any) => setSelectedAsset(o.value)} wrapperStyle={{ cursor: "pointer" }} formatter={(v: any) => aliasMap[v] || v} />
|
| 681 |
-
{dayStarts.map(s => (
|
| 682 |
-
<ReferenceLine key={s.x} x={s.x} stroke="#6b7280" strokeDasharray="4 2" ifOverflow="extendDomain" />
|
| 683 |
-
))}
|
| 684 |
-
{assets.map((a, i) => (
|
| 685 |
-
<Line
|
| 686 |
-
key={a}
|
| 687 |
-
type="linear"
|
| 688 |
-
dataKey={a}
|
| 689 |
-
name={aliasMap[a] || a}
|
| 690 |
-
strokeWidth={selectedAsset === a ? 5 : hoverAsset === a ? 4 : 2.5}
|
| 691 |
-
strokeOpacity={selectedAsset && selectedAsset !== a ? 0.3 : 1}
|
| 692 |
-
dot={false}
|
| 693 |
-
isAnimationActive={false}
|
| 694 |
-
stroke={`hsl(${(360 / assets.length) * i},70%,50%)`}
|
| 695 |
-
onMouseEnter={() => setHoverAsset(a)}
|
| 696 |
-
onMouseLeave={() => setHoverAsset(null)}
|
| 697 |
-
onClick={() => setSelectedAsset((p) => (p === a ? null : a))}
|
| 698 |
-
connectNulls={true} // 关键:跨缺口仍然连线
|
| 699 |
-
/>
|
| 700 |
-
))}
|
| 701 |
-
</LineChart>
|
| 702 |
-
</>
|
| 703 |
) : (
|
| 704 |
<LineChart data={windowData}>
|
| 705 |
<CartesianGrid strokeDasharray="3 3" />
|
|
@@ -734,7 +576,7 @@ export default function App() {
|
|
| 734 |
</div>
|
| 735 |
)}
|
| 736 |
|
| 737 |
-
{viewMode!=="
|
| 738 |
<div className="flex justify-between items-center mt-3">
|
| 739 |
<div className="text-sm text-gray-600">
|
| 740 |
Selected: {selectedAsset ? aliasOf(selectedAsset) : "(none)"} {message && <span className="ml-2 text-gray-500">{message}</span>}
|
|
|
|
| 1 |
import { useEffect, useMemo, useState } from "react";
|
| 2 |
import {
|
| 3 |
LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, Legend, CartesianGrid,
|
| 4 |
+
AreaChart, Area, BarChart, Bar
|
| 5 |
} from "recharts";
|
| 6 |
import {
|
| 7 |
apiGetLatestDataset,
|
|
|
|
| 12 |
} from "./api";
|
| 13 |
|
| 14 |
type SeriesPoint = { date: string; close: number[] };
|
| 15 |
+
type ViewMode = "daily" | "intraday"; // 移除 multi
|
| 16 |
|
| 17 |
const START_DAY = 7;
|
| 18 |
|
|
|
|
| 103 |
|
| 104 |
const [viewMode, setViewMode] = useState<ViewMode>("daily");
|
| 105 |
const [intradayDayIdx, setIntradayDayIdx] = useState<number | null>(null);
|
|
|
|
| 106 |
|
| 107 |
/* ---------- localStorage keys ---------- */
|
| 108 |
const LS_DATASET_KEY = "annot_dataset_meta";
|
|
|
|
| 313 |
return rows;
|
| 314 |
}, [assets, rawData, dates.length, activeIntradayIdx]);
|
| 315 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
function realizedNextDayReturn(asset: string) {
|
| 317 |
const t = windowLen - 1;
|
| 318 |
if (t + 1 >= dates.length) return null as any;
|
|
|
|
| 475 |
</div>
|
| 476 |
);
|
| 477 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
return (
|
| 479 |
<div className="p-6 bg-gray-50 min-h-screen flex flex-col gap-6">
|
| 480 |
<div className="flex flex-wrap justify-between items-center gap-3">
|
|
|
|
| 493 |
<div className="flex rounded-xl overflow-hidden border border-gray-200">
|
| 494 |
<button className={`text-xs px-3 py-1.5 ${viewMode==="daily" ? "bg-gray-900 text-white" : "bg-white text-gray-700 hover:bg-gray-50"}`} onClick={()=>setViewMode("daily")}>Daily</button>
|
| 495 |
<button className={`text-xs px-3 py-1.5 ${viewMode==="intraday" ? "bg-gray-900 text-white" : "bg-white text-gray-700 hover:bg-gray-50"}`} onClick={()=>setViewMode("intraday")}>Intraday (1 day)</button>
|
|
|
|
| 496 |
</div>
|
| 497 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
<button onClick={exportLog} className="text-sm px-3 py-1.5 rounded-xl bg-gray-900 text-white hover:bg-black">Export My Log</button>
|
| 499 |
</div>
|
| 500 |
</div>
|
|
|
|
| 542 |
/>
|
| 543 |
))}
|
| 544 |
</LineChart>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 545 |
) : (
|
| 546 |
<LineChart data={windowData}>
|
| 547 |
<CartesianGrid strokeDasharray="3 3" />
|
|
|
|
| 576 |
</div>
|
| 577 |
)}
|
| 578 |
|
| 579 |
+
{viewMode!=="intraday" && (
|
| 580 |
<div className="flex justify-between items-center mt-3">
|
| 581 |
<div className="text-sm text-gray-600">
|
| 582 |
Selected: {selectedAsset ? aliasOf(selectedAsset) : "(none)"} {message && <span className="ml-2 text-gray-500">{message}</span>}
|