DockerSpace / frontend /src /components /CandlestickChart.tsx
DennisChan0909's picture
feat: add chip_score, MA120/240, margin flow, and TA distillation
cdead71
Raw
History Blame Contribute Delete
5.19 kB
import React, { useMemo } from 'react'
import {
ComposedChart,
Line,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from 'recharts'
import { OHLCVPoint } from '../api/stockApi'
interface Props {
data: OHLCVPoint[]
}
// Show every Nth date label to avoid crowding
function tickFormatter(value: string, index: number, total: number): string {
const step = Math.max(1, Math.floor(total / 10))
if (index % step === 0) return value
return ''
}
const CustomTooltip = ({ active, payload, label }: any) => {
if (!active || !payload?.length) return null
const d: OHLCVPoint = payload[0]?.payload
if (!d) return null
return (
<div className="bg-slate-800 border border-slate-600 rounded p-3 text-xs space-y-1 shadow-lg">
<p className="text-slate-300 font-semibold">{label}</p>
<p>O: <span className="text-white">{d.open?.toFixed(2)}</span></p>
<p>H: <span className="text-green-400">{d.high?.toFixed(2)}</span></p>
<p>L: <span className="text-red-400">{d.low?.toFixed(2)}</span></p>
<p>C: <span className="text-yellow-300 font-bold">{d.close?.toFixed(2)}</span></p>
{d.ma5 != null && <p>MA5: <span className="text-yellow-400">{d.ma5.toFixed(2)}</span></p>}
{d.ma20 != null && <p>MA20: <span className="text-blue-400">{d.ma20.toFixed(2)}</span></p>}
{d.ma60 != null && <p>MA60: <span className="text-red-400">{d.ma60.toFixed(2)}</span></p>}
{d.ma120 != null && <p>MA120: <span className="text-purple-400">{d.ma120.toFixed(2)}</span></p>}
{d.ma240 != null && <p>MA240: <span className="text-slate-400">{d.ma240.toFixed(2)}</span></p>}
</div>
)
}
export const CandlestickChart: React.FC<Props> = ({ data }) => {
const len = data.length
// Build Bollinger band area data: pairs of [lower, upper] for Area fill
const chartData = useMemo(
() =>
data.map((d) => ({
...d,
bb_band: d.bb_upper != null && d.bb_lower != null
? [d.bb_lower, d.bb_upper]
: [null, null],
})),
[data]
)
const allPrices = data.flatMap((d) => [d.low, d.high, d.bb_upper, d.bb_lower].filter(Boolean) as number[])
const yMin = Math.floor(Math.min(...allPrices) * 0.995)
const yMax = Math.ceil(Math.max(...allPrices) * 1.005)
return (
<ResponsiveContainer width="100%" height={320}>
<ComposedChart data={chartData} margin={{ top: 8, right: 16, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
<XAxis
dataKey="date"
tick={{ fill: '#94a3b8', fontSize: 11 }}
tickLine={false}
tickFormatter={(val, idx) => tickFormatter(val, idx, len)}
interval={0}
/>
<YAxis
domain={[yMin, yMax]}
tick={{ fill: '#94a3b8', fontSize: 11 }}
tickLine={false}
tickFormatter={(v) => v.toFixed(0)}
width={55}
/>
<Tooltip content={<CustomTooltip />} />
<Legend
wrapperStyle={{ fontSize: 12, paddingTop: 4 }}
formatter={(value) => <span style={{ color: '#94a3b8' }}>{value}</span>}
/>
{/* Bollinger Band fill — rendered first so it's behind price lines */}
<Area
dataKey="bb_upper"
stroke="#334155"
strokeWidth={1}
fill="#1e293b"
fillOpacity={0.5}
dot={false}
legendType="none"
name="BB Upper"
isAnimationActive={false}
/>
<Area
dataKey="bb_lower"
stroke="#334155"
strokeWidth={1}
fill="#0f172a"
fillOpacity={1}
dot={false}
legendType="none"
name="BB Lower"
isAnimationActive={false}
/>
{/* Close price as thick line */}
<Line
dataKey="close"
stroke="#e2e8f0"
strokeWidth={2}
dot={false}
name="Close"
isAnimationActive={false}
/>
{/* Moving averages */}
<Line
dataKey="ma5"
stroke="#facc15"
strokeWidth={1.5}
dot={false}
name="MA5"
isAnimationActive={false}
connectNulls
/>
<Line
dataKey="ma20"
stroke="#60a5fa"
strokeWidth={1.5}
dot={false}
name="MA20"
isAnimationActive={false}
connectNulls
/>
<Line
dataKey="ma60"
stroke="#f87171"
strokeWidth={1.5}
dot={false}
name="MA60"
isAnimationActive={false}
connectNulls
/>
<Line
dataKey="ma120"
stroke="#a78bfa"
strokeWidth={1.5}
dot={false}
name="MA120"
isAnimationActive={false}
connectNulls
strokeDasharray="4 2"
/>
<Line
dataKey="ma240"
stroke="#94a3b8"
strokeWidth={1.5}
dot={false}
name="MA240"
isAnimationActive={false}
connectNulls
strokeDasharray="6 3"
/>
</ComposedChart>
</ResponsiveContainer>
)
}