Creating a Real-Time Candlestick Chart Using WebSocket

This tutorial demonstrates how to subscribe to Upbit’s real-time candlestick stream using WebSocket and visualize the data with a live chart.

Get Started

To develop trading strategies based on price trends and trading activity, it is often necessary to collect data over extended periods. Rather than repeatedly calling the REST API, you can subscribe to a real-time data stream using a WebSocket connection. This tutorial demonstrates how to subscribe to live candlestick (candle) data via WebSocket and visualize it in a dynamic chart.

You can find a general introduction to Websocket Usage Guide, including usage patterns and error handling, in the WebSocket Guide.

Check the proper endpoint based on your region.
The examples in this page is written using Singapore fiat code(SGD). Set the quote currency to match your region. The base_url differs by country/region. Make sure to specify the correct region value for your environment.

- Singapore (sg): https://sg-api.upbit.com
- Indonesia (id): https://id-api.upbit.com
- Thailand (th): https://th-api.upbit.com

Development Environment & Libraries

This tutorial uses Python as the primary language. Refer to the Development Environment Setup Guide to set up your Python environment. To receive WebSocket data, we use the websocket-client library, and for chart visualization, we use pandas and matplotlib. Install the required libraries in your (virtual) environment using the following command:

pip install websocket-client matplotlib pandas

Subscribing to Real-Time Candle Streams via WebSocket

First, let’s write Python code to subscribe to the real-time candlestick stream. The example below uses the websocket-client library to establish a WebSocket connection and subscribe to 1-second candle data. The run_ws() function connects to the Upbit WebSocket. We use threading to run the WebSocket connection in a separate thread. In the on_open function, a subscription message for the candle.1s stream is sent once the connection is established. This example subscribes to the SGD-BTC trading pair.

import websocket
import threading
import uuid
import json
import time

def on_open(ws):
    print("WebSocket connected")
    subscribe_message = [
        {"ticket": str(uuid.uuid4())},
        {"type": "candle.1s", "codes": ["SGD-BTC"]}
    ]
    ws.send(json.dumps(subscribe_message))
    
def on_message(ws, message):
    try:
        obj = json.loads(message)
        print(obj)
    except Exception:
        print("Received(raw):", message)
        return
    
def on_error(err):
        print("Error:", err)

def on_close():
        print("Closed")

def run_ws():
    ws = websocket.WebSocketApp(
        "wss://sg-api.upbit.com/websocket/v1",
        on_open=on_open,
        on_message=on_message,
        on_error=on_error,
        on_close=on_close
    )
    ws.run_forever(ping_interval=30, ping_timeout=10, reconnect=2)

ws_thread = threading.Thread(target=run_ws, daemon=True)
ws_thread.start()

try:
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    pass

Once connected, the subscription message is sent and you will start receiving 1-second candle data. To extract the necessary fields for visualization and store the data, define the on_message(ws, message) function as shown below, and include it in your WebSocket connection. This function will be executed automatically every time a message is received.

import websocket
import threading
import json
import pandas as pd

candle_data_list = []
data_lock = threading.Lock()

def on_message(ws, message):
    """Receive live candle data from Upbit WebSocket and store it in a list."""
    global candle_data_list
    data = json.loads(message)

    if data.get('type') == 'candle.1s':
        candle_time = data['candle_date_time_utc']
        opening_price = float(data['opening_price'])
        high_price = float(data['high_price'])
        low_price = float(data['low_price'])
        close_price = float(data['trade_price'])
        volume = float(data['candle_acc_trade_volume'])

        with data_lock:
            timestamp = pd.to_datetime(candle_time)
            candle_data_list.append({
                'timestamp': timestamp,
                'open': opening_price,
                'high': high_price,
                'low': low_price,
                'close': close_price,
                'volume': volume
            })
            # Keep only the latest 50 candles
            if len(candle_data_list) > 50:
                candle_data_list = candle_data_list[-50:]

def on_open(ws):
    """Send the subscription message on WebSocket connection."""
    print("WebSocket connected")
    subscribe_message = [
        {"ticket": "test_unique_identifier_89asydfhnmn2_34"},
        {
            "type": "candle.1s",
            "codes": ["SGD-BTC"],
        }
    ]
    ws.send(json.dumps(subscribe_message))

def run_ws():
    """Run the WebSocket client in a separate thread."""
    ws = websocket.WebSocketApp(
        "wss://sg-api.upbit.com/websocket/v1",
        on_message=on_message,
        on_open=on_open
    )
    ws.run_forever()

# Start the WebSocket thread (for data collection)
ws_thread = threading.Thread(target=run_ws, daemon=True)
ws_thread.start()

The example above defines a list candle_data_list to store candlestick data for charting. Each time a message is received, key candle fields are extracted and appended to the list.

Now, let’s move on to rendering the chart based on the stored data.


Full Code Example

import websocket
import threading
import uuid
import json
import time
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

candle_data_list = []
data_lock = threading.Lock()

def on_open(ws):
    print("WebSocket connected")
    subscribe_message = [
        {"ticket": str(uuid.uuid4())},
        {"type": "candle.1s", "codes": ["SGD-BTC"]}
    ]
    ws.send(json.dumps(subscribe_message))
    
def on_message(ws, message):
    global candle_data_list
    data = json.loads(message)

    if data.get('type') == 'candle.1s':
        candle_time = data['candle_date_time_utc']
        opening_price = float(data['opening_price'])
        high_price = float(data['high_price'])
        low_price = float(data['low_price'])
        close_price = float(data['trade_price'])
        volume = float(data['candle_acc_trade_volume'])

        with data_lock:
            timestamp = pd.to_datetime(candle_time)
            candle_data_list.append({
                'timestamp': timestamp,
                'open': opening_price,
                'high': high_price,
                'low': low_price,
                'close': close_price,
                'volume': volume
            })
            if len(candle_data_list) > 50:
                candle_data_list = candle_data_list[-50:]
    
def on_error(err):
        print("Error:", err)

def on_close():
        print("Closed")

def run_ws():
    ws = websocket.WebSocketApp(
        "wss://sg-api.upbit.com/websocket/v1",
        on_open=on_open,
        on_message=on_message,
        on_error=on_error,
        on_close=on_close
    )
    ws.run_forever(ping_interval=30, ping_timeout=10, reconnect=2)

def update_chart(frame):
    with data_lock:
        if len(candle_data_list) < 2:
            return

        data_copy = candle_data_list.copy()
    
    ax1.clear()
    ax2.clear()
    
    timestamps = [d['timestamp'] for d in data_copy]
    opens = [d['open'] for d in data_copy]
    highs = [d['high'] for d in data_copy]
    lows = [d['low'] for d in data_copy]
    closes = [d['close'] for d in data_copy]
    volumes = [d['volume'] for d in data_copy]
    
    for i, (ts, o, h, l, c) in enumerate(zip(timestamps, opens, highs, lows, closes)):
        color = 'red' if c >= o else 'blue'
        ax1.plot([i, i], [o, c], color=color, linewidth=12)
        ax1.plot([i, i], [l, h], color=color, linewidth=2)
    
    ax1.set_title('Upbit SGD-BTC Real-time 1s Candlestick Chart', fontsize=14)
    ax1.set_ylabel('Price (SGD)', fontsize=12)
    ax1.grid(True, alpha=0.3)
    
    if len(timestamps) > 0:
        step = max(1, len(timestamps) // 10) 
        ax1.set_xticks(range(0, len(timestamps), step))
        ax1.set_xticklabels([ts.strftime('%H:%M:%S') for ts in timestamps[::step]], 
                            rotation=45, fontsize=10)
    
    ax2.bar(range(len(volumes)), volumes, color='gray', alpha=0.6)
    ax2.set_ylabel('Volume', fontsize=12)
    ax2.set_xlabel('Time', fontsize=12)
    ax2.grid(True, alpha=0.3)
    
    if len(timestamps) > 0:
        ax2.set_xticks(range(0, len(timestamps), step))
        ax2.set_xticklabels([ts.strftime('%H:%M:%S') for ts in timestamps[::step]], 
                            rotation=45, fontsize=10)
    
    plt.tight_layout()

ws_thread = threading.Thread(target=run_ws, daemon=True)
ws_thread.start()

fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10), gridspec_kw={'height_ratios': [3, 1]})

ani = FuncAnimation(fig, update_chart, interval=1000, cache_frame_data=False)
plt.show()

When you run this code, the following steps occur:

  1. A new thread is created to connect to the Upbit public WebSocket server and send a subscription message for 1-second candle data on the SGD-BTC pair.
  2. Each received candle is added to a list (up to 50 records).
  3. The main thread runs a chart animation that updates once per second with the latest candle data.

A real-time candlestick chart will appear as shown below. Note: Candle data is received only when trades occur within the given interval. If there are no trades in a second, or if only a single trade price is recorded, the chart may not update. This is expected behavior.


Wrapping Up

In this tutorial, we built an example that uses WebSocket to receive real-time candle data and render it as a live chart. You can adapt this example to save the data to a file or implement real-time analysis features, which can then be applied to develop trading strategies.