messy work. backtests / mock-data
This commit is contained in:
parent
4e4a048988
commit
fa70ada2bb
51 changed files with 2576 additions and 887 deletions
215
apps/stock/web-app/src/components/charts/Chart.tsx
Normal file
215
apps/stock/web-app/src/components/charts/Chart.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
2
apps/stock/web-app/src/components/charts/index.ts
Normal file
2
apps/stock/web-app/src/components/charts/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { Chart } from './Chart';
|
||||
export type { ChartProps, ChartData } from './Chart';
|
||||
Loading…
Add table
Add a link
Reference in a new issue