messy work. backtests / mock-data

This commit is contained in:
Boki 2025-07-03 08:37:23 -04:00
parent 4e4a048988
commit fa70ada2bb
51 changed files with 2576 additions and 887 deletions

View file

@ -0,0 +1,215 @@
import * as LightweightCharts from 'lightweight-charts';
import { useEffect, useRef, useState } from 'react';
export interface ChartData {
time: number;
open?: number;
high?: number;
low?: number;
close?: number;
value?: number;
volume?: number;
}
export interface ChartProps {
data: ChartData[];
height?: number;
type?: 'candlestick' | 'line' | 'area';
showVolume?: boolean;
theme?: 'light' | 'dark';
overlayData?: Array<{
name: string;
data: Array<{ time: number; value: number }>;
color?: string;
lineWidth?: number;
}>;
className?: string;
}
export function Chart({
data,
height = 400,
type = 'candlestick',
showVolume = true,
theme = 'dark',
overlayData = [],
className = '',
}: ChartProps) {
const chartContainerRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<LightweightCharts.IChartApi | null>(null);
const mainSeriesRef = useRef<LightweightCharts.ISeriesApi<any> | null>(null);
const volumeSeriesRef = useRef<LightweightCharts.ISeriesApi<any> | null>(null);
const overlaySeriesRef = useRef<Map<string, LightweightCharts.ISeriesApi<any>>>(new Map());
// Debug logging
console.log('Chart - data received:', data);
console.log('Chart - data length:', data?.length);
console.log('Chart - data type:', Array.isArray(data) ? 'array' : typeof data);
console.log('Chart - first data point:', data?.[0]);
useEffect(() => {
if (!chartContainerRef.current || !data || !data.length) {
console.log('Chart - early return:', {
hasContainer: !!chartContainerRef.current,
hasData: !!data,
dataLength: data?.length
});
return;
}
// Create chart
const chart = LightweightCharts.createChart(chartContainerRef.current, {
width: chartContainerRef.current.clientWidth,
height: height,
layout: {
background: {
type: LightweightCharts.ColorType.Solid,
color: theme === 'dark' ? '#0f0f0f' : '#ffffff'
},
textColor: theme === 'dark' ? '#d1d5db' : '#374151',
},
grid: {
vertLines: {
color: theme === 'dark' ? '#1f2937' : '#e5e7eb',
visible: true,
},
horzLines: {
color: theme === 'dark' ? '#1f2937' : '#e5e7eb',
visible: true,
},
},
crosshair: {
mode: LightweightCharts.CrosshairMode.Normal,
},
rightPriceScale: {
borderColor: theme === 'dark' ? '#1f2937' : '#e5e7eb',
autoScale: true,
},
timeScale: {
borderColor: theme === 'dark' ? '#1f2937' : '#e5e7eb',
timeVisible: true,
secondsVisible: false,
},
});
chartRef.current = chart;
// Create main series
if (type === 'candlestick' && data[0].open !== undefined) {
mainSeriesRef.current = chart.addCandlestickSeries({
upColor: '#10b981',
downColor: '#ef4444',
borderUpColor: '#10b981',
borderDownColor: '#ef4444',
wickUpColor: '#10b981',
wickDownColor: '#ef4444',
});
mainSeriesRef.current.setData(data as LightweightCharts.CandlestickData[]);
} else if (type === 'line' || (type === 'candlestick' && data[0].value !== undefined)) {
mainSeriesRef.current = chart.addLineSeries({
color: '#3b82f6',
lineWidth: 2,
});
const lineData = data.map(d => ({
time: d.time,
value: d.value ?? d.close ?? 0
}));
mainSeriesRef.current.setData(lineData);
} else if (type === 'area') {
mainSeriesRef.current = chart.addAreaSeries({
lineColor: '#3b82f6',
topColor: '#3b82f6',
bottomColor: 'rgba(59, 130, 246, 0.1)',
lineWidth: 2,
});
const areaData = data.map(d => ({
time: d.time,
value: d.value ?? d.close ?? 0
}));
mainSeriesRef.current.setData(areaData);
}
// Add volume if available
if (showVolume && data.some(d => d.volume)) {
volumeSeriesRef.current = chart.addHistogramSeries({
color: '#3b82f680',
priceFormat: {
type: 'volume',
},
priceScaleId: 'volume',
});
volumeSeriesRef.current.priceScale().applyOptions({
scaleMargins: {
top: 0.8,
bottom: 0,
},
});
const volumeData = data
.filter(d => d.volume !== undefined)
.map(d => ({
time: d.time,
value: d.volume!,
color: d.close && d.open ?
(d.close >= d.open ? '#10b98140' : '#ef444440') :
'#3b82f640',
}));
volumeSeriesRef.current.setData(volumeData);
}
// Add overlay series
overlaySeriesRef.current.clear();
overlayData.forEach((overlay, index) => {
const series = chart.addLineSeries({
color: overlay.color || ['#ff9800', '#4caf50', '#9c27b0', '#f44336'][index % 4],
lineWidth: overlay.lineWidth || 2,
title: overlay.name,
priceScaleId: index === 0 ? '' : `overlay-${index}`, // First overlay uses main scale
});
if (index > 0) {
series.priceScale().applyOptions({
scaleMargins: {
top: 0.1,
bottom: 0.1,
},
});
}
series.setData(overlay.data);
overlaySeriesRef.current.set(overlay.name, series);
});
// Fit content
chart.timeScale().fitContent();
// Handle resize
const handleResize = () => {
if (chartContainerRef.current && chart) {
chart.applyOptions({
width: chartContainerRef.current.clientWidth,
});
}
};
window.addEventListener('resize', handleResize);
// Cleanup
return () => {
window.removeEventListener('resize', handleResize);
if (chart) {
chart.remove();
}
};
}, [data, height, type, showVolume, theme, overlayData]);
return (
<div className={`relative ${className}`}>
<div
ref={chartContainerRef}
style={{ width: '100%', height: `${height}px` }}
/>
</div>
);
}

View file

@ -0,0 +1,2 @@
export { Chart } from './Chart';
export type { ChartProps, ChartData } from './Chart';