Build candlestick charts in minutes with QuestDB and React

QuestDB is a next-generation database for market data. It offers premium ingestion throughput, enhanced SQL analytics that can power through analysis, and cost-saving hardware efficiency. It's open source, applies open formats, and is ideal for tick data.

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 or SAMPLE 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!

Bart laughing

...

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:

  1. Fetching and transforming data from QuestDB
  2. Chart initialization and lifecycle
  3. 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: string
columns: 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 (
SELECT
timestamp_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_price
FROM trades
WHERE symbol = 'BTC-USD'
AND timestamp > dateadd('m', -45, now())
GROUP BY timestamp_floor('5s', timestamp), symbol
)
SELECT * FROM intervals
ORDER 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] = row
return [
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) return
const chart = echarts.init(chartRef.current)
chartInstanceRef.current = chart
// Initial QuestDB fetch
fetchAndUpdateData()
// Poll for updates every 500ms
const intervalId = setInterval(fetchAndUpdateData, 500)
// Handle window resizing
let resizeTimeout: NodeJS.Timeout
const handleResize = () => {
if (resizeTimeout) clearTimeout(resizeTimeout)
resizeTimeout = setTimeout(() => {
chart.resize()
}, 100)
}
window.addEventListener('resize', handleResize)
// Cleanup
return () => {
clearInterval(intervalId)
window.removeEventListener('resize', handleResize)
if (resizeTimeout) clearTimeout(resizeTimeout)
chart.dispose()
}
}, [])
return (
<div
ref={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 candle
color: '#00b07c', // Bullish candle
borderColor0: '#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:

Want to chat with us? Holler on social media or join in our engaging Community Forum or our public Slack.

RedditHackerNewsX
Subscribe to our newsletters for the latest. Secure and never shared or sold.