Build candlestick charts in minutes with QuestDB and React
When analyzing financial markets, one of the most popular ways to visualize asset prices (such as crypto tokens, stocks, or forex pairs) is the candlestick chart. Candlestick charts deliver an at-a-glance view of price changes over time — including an asset's open, high, low, and close (often abbreviated as OHLC). With just a quick glimpse, traders can identify potential trends, reversals, and patterns like doji candles, hammer patterns, or engulfing patterns.
In this post, we walk through how to build candlestick charts using QuestDB (for data storage and queries) and Apache ECharts (for interactive chart rendering). We also explain how to read these charts and show a complete React component that updates rapidly with QuestDB's demo data.
In the end, you'll have your own embeddable component - like this:
This is a real-time candlestick chart that updates every 500ms.
It's hitting our public QuestDB instance to retrieve BTC-USD tick data.
Cool, right? It's quick and easy for any trader or analyst to start creating their own custom dashboards. Let's get started.
Why candlestick charts?
Candlestick charts originated in Japan more than a century ago and remain an essential tool for traders. Due to their great utility, they have endured. But why? What makes them so useful?
- Quick visual identification: The color of each candle indicates whether the price closed higher (green) or lower (red) than it opened, in an instant
- Price range: The "wick" (thin line) above and below the candle body shows the asset's highest and lowest prices during that time period
- Patterns: Candlestick patterns can provide hints into momentum shifts and potential trend reversals. See our comprehensive guide to candlestick patterns for detailed examples
Compared to line charts that only show close prices, candlestick charts offer more granularity — this is especially crucial in fast-moving markets where intraperiod highs and lows matter.
How to read a candlestick
If you're not familiar with these charts, at a high level, they represent prices:
- Open: The price of the asset at the start of the period (e.g., 5s, 1m, 1h)
- Close: The price of the asset at the end of the period
- High: The highest traded price in that interval
- Low: The lowest traded price in that interval
The candlestick's 'body' spans the range between the open and close prices. Meanwhile, the "wick" or "shadow" extends above and below the body. These thin lines represent the highest and lowest prices reached during that interval.
Visually:
A candle is green if the price closes above where it opened, and red if the price closes below. The wick is important because it indicates intraperiod volatility.
Even if the body is short or the open/close prices are near each other, a long top or bottom wick might reveal how far the price briefly moved beyond the open and close. This makes candlestick charts a convenient way to quickly gauge both the direction and the volatility of an asset in a given timeframe.
Learn much more about candlestick charts from our deep candlestick chart glossary.
Enter QuestDB
Fluid candlesticks charts can put intensive demand on your underlying infrastructure. Tick data is time-oriented, so using a specialized time-series database like QuestDB is a perfect fit.
A time-series database can handle the raw throughput requirement of heavy tick data, while also handling deduplication, out-of-order indexing, and compression. QuestDB has all this, is open source, and has a very efficient performance profile. You can do a lot more with a lot less resources.
So, with it, let's build our "data layer".
We can store raw trades in a table (e.g., trades) with columns like:
- timestamp (timestamp type)
- symbol (symbol type)
- price (float/double)
- size (if relevant)
- exchange (symbol, optional)
If we want to generate OHLC intervals in real-time, we can apply extended SQL syntax for grouping by time buckets (e.g., 5 seconds or 1 minute). One popular approach - and the one we'll apply - is to:
- Ingest raw trades continuously into QuestDB
- Use
timestamp_floor
orSAMPLE BY
in yur SQL queries to aggregate into open, high, low, close - Return the result to the charting library
Candle making with ECharts
Apache ECharts is a rich charting library that focuses on data-intense, interactive visuals. Creating a chart with the base candlestick type is straightforward:
series: [{name: 'BTC-USD',type: 'candlestick',data: [// Each item is [timestamp, open, close, low, high]// Timestamps can be converted to a numeric value via new Date().getTime()],},]
This tells ECharts to render a candlestick chart with the data we provide. Next, we’ll write a query to fetch data from QuestDB. Then, we'll transform the data into OHLC format, and define how to render our candlestick chart. Naturally, our tuning will ensure it looks and feels right for our needs.
So to that tune, below is a minimal React component to fetch data from QuestDB, transform it into OHLC data, and render a candlestick chart. We'll poll the database every 500ms (yes, that's quite frequent — you can adjust as needed!).
But before we do that...
Is QuestDB running?
Well... then you'd better go catch it!
...
To start QuestDB, checkout our quick start guide.
If you run it yourself, you can bring your own data.
For whatever tick data you have, you can then build your own charts.
Or, you can hit our demo instance, like in the live example at the top of the page.
You'll be confined to the data we host, and punish our servers. But no matter!
QuestDB can handle it.
Okay, with QuestDB running, let's build our React component.
React-ified candlesticks
The full component is available below. But first, we'll break it down into three main parts. Our main goal is a plug-and-play component that to add to any dashboards, app, blog, or whatever you'd like.
Our three main functional parts are thus:
- Fetching and transforming data from QuestDB
- Chart initialization and lifecycle
- Visuals
Let's outline them, and then assemble them.
Fetch and transform data
First, we define our data fetching logic.
This part handles:
- Queryies a running QuestDB instance's REST API for OHLC data
- Transforms the response into ECharts' expected format
- Updates the chart with new data
interface QuestDBResponse {query: stringcolumns: Array<{ name: string; type: string }>dataset: Array<[string, string, number, number, number, number]>count: number}const fetchAndUpdateData = async () => {try {// Bring your own instance and tick data!const response = await fetch('https://demo.questdb.io/exec?' +new URLSearchParams({query: `WITH intervals AS (SELECTtimestamp_floor('5s', timestamp) AS interval_start,symbol,first(price) as open_price,max(price) as high_price,min(price) as low_price,last(price) as close_priceFROM tradesWHERE symbol = 'BTC-USD'AND timestamp > dateadd('m', -45, now())GROUP BY timestamp_floor('5s', timestamp), symbol)SELECT * FROM intervalsORDER BY interval_start ASC;`}))const responseData: QuestDBResponse = await response.json()if (!responseData?.dataset?.length || !chartInstanceRef.current) {return}const candlestickData = responseData.dataset.map(row => {const [intervalStart, _symbol, open, high, low, close] = rowreturn [new Date(intervalStart).getTime(),open,close,low,high]})chartInstanceRef.current.setOption({series: [{ data: candlestickData }]})} catch (error) {console.error('Error fetching data:', error)}}
Initialize and update chart
Next, we handle the chart's lifecycle using React's useEffect
hook.
This includes:
- Creating the ECharts instance
- Setting up data polling
- Managing window resize events
- Cleanup on unmount
export const EChartsDemo: React.FC = () => {const chartRef = useRef<HTMLDivElement>(null)const chartInstanceRef = useRef<echarts.ECharts | null>(null)useEffect(() => {if (!chartRef.current) returnconst chart = echarts.init(chartRef.current)chartInstanceRef.current = chart// Initial QuestDB fetchfetchAndUpdateData()// Poll for updates every 500msconst intervalId = setInterval(fetchAndUpdateData, 500)// Handle window resizinglet resizeTimeout: NodeJS.Timeoutconst handleResize = () => {if (resizeTimeout) clearTimeout(resizeTimeout)resizeTimeout = setTimeout(() => {chart.resize()}, 100)}window.addEventListener('resize', handleResize)// Cleanupreturn () => {clearInterval(intervalId)window.removeEventListener('resize', handleResize)if (resizeTimeout) clearTimeout(resizeTimeout)chart.dispose()}}, [])return (<divref={chartRef}className="w-full"style={{height: 'min(400px, 60vw)',minHeight: '250px',willChange: 'transform'}}/>)}
Customize visuals
Finally, we define how the chart looks and behaves. This includes:
- Candlestick styling
- Axis configuration
- Tooltips and interactions
- Zoom controls
One item to note is the association between the dates and the OHLC candlestick nodes. As you'd expect, precise time is required to ensure you open and close at an interval that matches the time-frame of the data you're querying.
In other words, when modifying time intervals in the query, say when changing timestamp_floor('5s')
to '1m'
, make sure to adjust the polling frequency (setInterval
) and data zoom settings (start: 95
) accordingly. For example, 1-minute candles might work better with a 2-second poll rate and a wider zoom window to show meaningful price action.
const option: EChartsOption = {backgroundColor: 'transparent',animation: true,animationDuration: 300,animationEasing: 'linear',tooltip: {trigger: 'axis',axisPointer: {type: 'cross',crossStyle: { color: '#cccccc' }},formatter: (params: any) => {const date = new Date(params[0].axisValue)const timeStr = date.toLocaleTimeString([], {hour: '2-digit',minute: '2-digit',second: '2-digit',hour12: false,fractionalSecondDigits: 3})const [open, high, low, close] = params[0].data.slice(1)return `Time: ${timeStr}<br/>Open: $${open.toLocaleString()}<br/>High: $${high.toLocaleString()}<br/>Low: $${low.toLocaleString()}<br/>Close: $${close.toLocaleString()}`},backgroundColor: 'rgba(0, 0, 0, 0.8)',borderColor: '#333',textStyle: { color: '#fff' }},grid: {left: '5%',right: '5%',top: '5%',bottom: '15%',containLabel: true},dataZoom: [{type: 'slider',show: true,xAxisIndex: [0],start: 95,end: 100,top: '90%',borderColor: '#333333',textStyle: { color: '#e1e1e1' },backgroundColor: 'rgba(47, 69, 84, 0.3)',fillerColor: 'rgba(167, 183, 204, 0.2)',handleStyle: { color: '#cccccc' }},{type: 'inside',xAxisIndex: [0],start: 95,end: 100}],xAxis: [{type: 'time',splitLine: { show: false },axisLabel: {color: '#e1e1e1',formatter: (value: number) => {return new Date(value).toLocaleTimeString([], {hour: '2-digit',minute: '2-digit',second: '2-digit',hour12: false})}},axisLine: {onZero: false,lineStyle: { color: '#333' }},min: 'dataMin',max: 'dataMax'}],yAxis: [{type: 'value',scale: true,splitLine: {show: true,lineStyle: {color: '#333',type: 'dashed'}},axisLabel: {color: '#e1e1e1',formatter: (value: number) => `$${value.toLocaleString()}`},axisLine: {lineStyle: { color: '#333' }}}],series: [{name: 'BTC-USD',type: 'candlestick',data: [],itemStyle: {color0: '#ef5350', // Bearish candlecolor: '#00b07c', // Bullish candleborderColor0: '#ef5350',borderColor: '#00b07c'}}]}
Each section focuses on a specific aspect of the component, making it easier to understand and maintain. The data fetching logic handles communication with QuestDB, the lifecycle management ensures proper initialization and cleanup, and the visual configuration creates a polished, interactive chart.
Get the full gist on GitHub.
Now you can customize each part as needed:
- Modify the query to change the time interval or symbol
- Update the QuestDB query to fetch your own data
- Adjust the polling frequency for different update rates
- Tweak the visual settings to match your design needs
Summary
Candlestick charts are a great way to visualize price movements over a given interval. With QuestDB storing time-series data, we can quickly run time-windowed SQL queries and produce real-time candlesticks. Apache ECharts provides a seamless way to transform and render that data in any frontend application, complete with zoom, tooltips, and dynamic refreshes.
You could also use other visualization tools, such as Grafana. But the ease of React means a plug-and-play component, with a performant query nested within it, means candlesticks are minutes away.
For more articles on integrating QuestDB with your trading or analytics stack, check out:
- All about candlestick charts
- Scaling trading bot with time-series database
- Making a trading gameboy
- Stories in French real estate data
- Ingesting financial tick data using a time-series database
- Ingesting live market data from DataBento
Want to chat with us? Holler on social media or join in our engaging Community Forum or our public Slack.