Skip to content

Commit 1e52abc

Browse files
davclaude
authored andcommitted
Fix favicon, candle alignment, distribution overflow, equity times
- Add SVG favicon (candlestick + fan chart icon on dark bg) - Fix candle time alignment: aggregateCandles now snaps to wall-clock boundaries (15m bars at :00/:15/:30/:45, 30m at :00/:30, 1h at :00) instead of aligning relative to array position - Fix distribution pane: increase grid padding, smaller rotated labels, add overflow:hidden to chart container, compact probability callouts - Fix equity curve: sort history chronologically, show date labels when entries span multiple calendar days Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6e7d2a5 commit 1e52abc

File tree

5 files changed

+71
-24
lines changed

5 files changed

+71
-24
lines changed

index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<meta name="description" content="Real-time probabilistic ensemble forecasts for ES futures with fan charts, trading signals, and live performance tracking." />
77
<meta name="theme-color" content="#0a0e17" />
8+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
89
<title>ES Futures Forecast</title>
910
<style>
1011
/* Prevent flash of white background */

public/favicon.svg

Lines changed: 7 additions & 0 deletions
Loading

src/api/timeframe.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,44 @@ export const TIMEFRAME_OPTIONS: { value: Timeframe; label: string }[] = [
2727
/** Number of aggregated context bars to display */
2828
const DISPLAY_BARS = 24;
2929

30-
/** Aggregate 5-min candles into larger timeframe bars, aligned from the end. */
30+
/** Aggregate 5-min candles into larger timeframe bars, aligned to wall-clock boundaries.
31+
* 15m bars snap to :00/:15/:30/:45, 30m to :00/:30, 1h to :00. */
3132
export function aggregateCandles(candles: CandleData[], factor: number): CandleData[] {
3233
if (factor <= 1) return candles;
3334

35+
const intervalSec = factor * 300; // seconds per aggregated bar
3436
const result: CandleData[] = [];
35-
// Align from the end so the last group is complete
36-
const alignedStart = candles.length % factor;
37-
for (let i = alignedStart; i < candles.length; i += factor) {
38-
const group = candles.slice(i, i + factor);
39-
if (group.length === 0) continue;
37+
let currentBucket = -1;
38+
let group: CandleData[] = [];
39+
40+
for (const c of candles) {
41+
const bucket = Math.floor(c.time / intervalSec) * intervalSec;
42+
if (bucket !== currentBucket) {
43+
if (group.length > 0) {
44+
result.push({
45+
time: currentBucket,
46+
open: group[0].open,
47+
high: Math.max(...group.map((g) => g.high)),
48+
low: Math.min(...group.map((g) => g.low)),
49+
close: group[group.length - 1].close,
50+
volume: group.reduce((sum, g) => sum + g.volume, 0),
51+
});
52+
}
53+
currentBucket = bucket;
54+
group = [c];
55+
} else {
56+
group.push(c);
57+
}
58+
}
59+
// Flush last group
60+
if (group.length > 0) {
4061
result.push({
41-
time: group[0].time,
62+
time: currentBucket,
4263
open: group[0].open,
43-
high: Math.max(...group.map((c) => c.high)),
44-
low: Math.min(...group.map((c) => c.low)),
64+
high: Math.max(...group.map((g) => g.high)),
65+
low: Math.min(...group.map((g) => g.low)),
4566
close: group[group.length - 1].close,
46-
volume: group.reduce((sum, c) => sum + c.volume, 0),
67+
volume: group.reduce((sum, g) => sum + g.volume, 0),
4768
});
4869
}
4970
return result;

src/components/charts/EquityCurve.tsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,14 @@ export function EquityCurve({ history }: Props) {
2727
// Check if multi-horizon data is available
2828
const hasMultiHorizon = history.some((e) => e.realized_returns != null);
2929

30+
// Sort history by timestamp to ensure chronological order
31+
const sorted = [...history].sort(
32+
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(),
33+
);
34+
3035
// Compute cumulative equity from realized returns at selected horizon
3136
let cumReturn = 0;
32-
const equity = history
37+
const equity = sorted
3338
.filter((e) => {
3439
if (hasMultiHorizon && e.realized_returns) {
3540
const ret = e.realized_returns[String(selectedHorizon)];
@@ -50,16 +55,25 @@ export function EquityCurve({ history }: Props) {
5055

5156
const pnl = signalDir * ret;
5257
cumReturn += pnl;
58+
59+
// Include date if history spans multiple days
60+
const d = new Date(e.timestamp);
61+
const timeStr = d.toLocaleTimeString("en-US", {
62+
hour: "2-digit",
63+
minute: "2-digit",
64+
hour12: false,
65+
});
5366
return {
54-
time: new Date(e.timestamp).toLocaleTimeString("en-US", {
55-
hour: "2-digit",
56-
minute: "2-digit",
57-
hour12: false,
58-
}),
67+
time: timeStr,
68+
fullTime: d.toLocaleDateString("en-US", { month: "short", day: "numeric" }) + " " + timeStr,
5969
value: +(cumReturn * 100).toFixed(3),
6070
};
6171
});
6272

73+
// Detect if entries span multiple calendar days
74+
const multiDay = equity.length > 1 &&
75+
equity[0].fullTime.slice(0, 6) !== equity[equity.length - 1].fullTime.slice(0, 6);
76+
6377
const isPositive = cumReturn >= 0;
6478

6579
const option: echarts.EChartsCoreOption = {
@@ -73,8 +87,8 @@ export function EquityCurve({ history }: Props) {
7387
grid: { left: 50, right: 10, top: 10, bottom: 30 },
7488
xAxis: {
7589
type: "category",
76-
data: equity.map((e) => e.time),
77-
axisLabel: { color: "#94a3b8", fontSize: 10, interval: Math.max(0, Math.floor(equity.length / 6)) },
90+
data: equity.map((e) => multiDay ? e.fullTime : e.time),
91+
axisLabel: { color: "#94a3b8", fontSize: multiDay ? 8 : 10, interval: Math.max(0, Math.floor(equity.length / 6)), rotate: multiDay ? 30 : 0 },
7892
axisLine: { lineStyle: { color: "#334155" } },
7993
},
8094
yAxis: {

src/components/charts/ProbabilityDist.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,14 +112,16 @@ export function ProbabilityDist({ prediction }: Props) {
112112
return `${p.name}<br/>Count: ${p.value}`;
113113
},
114114
},
115-
grid: { left: 40, right: 10, top: 10, bottom: hasSamplePaths ? 50 : 30 },
115+
grid: { left: 40, right: 10, top: 10, bottom: 45 },
116116
xAxis: {
117117
type: "category",
118118
data: bins.map((b) => b.label),
119119
axisLabel: {
120120
color: "#94a3b8",
121-
fontSize: 9,
122-
rotate: bins.length > 10 ? 45 : 0,
121+
fontSize: 8,
122+
rotate: bins.length > 8 ? 45 : 0,
123+
overflow: "truncate" as const,
124+
width: 50,
123125
},
124126
axisLine: { lineStyle: { color: "#334155" } },
125127
},
@@ -175,7 +177,7 @@ export function ProbabilityDist({ prediction }: Props) {
175177
))}
176178
</div>
177179
</div>
178-
<div style={{ flex: 1, minHeight: 0 }}>
180+
<div style={{ flex: 1, minHeight: 0, overflow: "hidden" }}>
179181
<ReactEChartsCore
180182
echarts={echarts}
181183
option={option}
@@ -190,10 +192,12 @@ export function ProbabilityDist({ prediction }: Props) {
190192
style={{
191193
display: "flex",
192194
justifyContent: "space-around",
193-
padding: "6px 4px 2px",
195+
padding: "4px 2px 2px",
194196
borderTop: "1px solid #1e293b",
195197
fontFamily: "JetBrains Mono, monospace",
196-
fontSize: 11,
198+
fontSize: 10,
199+
flexWrap: "wrap",
200+
gap: 2,
197201
}}
198202
>
199203
<span>

0 commit comments

Comments
 (0)