Skip to main content
PriceSignal provides real-time candlestick charts powered by KLineCharts, allowing you to visualize price movements, analyze trends, and monitor your cryptocurrency investments.

Chart Features

  • Real-time price updates via GraphQL subscriptions
  • Candlestick (K-line) visualization
  • Built-in technical indicators (RSI, MA, etc.)
  • Multiple timeframe support
  • Historical data with 500 data points
  • Auto-updating latest candle

Chart Implementation

The charting interface is built using KLineCharts, a lightweight charting library optimized for financial data.

Basic Chart Component

The main chart component is defined in src/react-app/src/features/prices/components/prices-chart.tsx:
import { useEffect, useRef } from 'react'
import { init, dispose, KLineData, Chart } from 'klinecharts'

type Props = {
    data: KLineData[]
}

export const PricesChart = ({ data }: Props) => {
    const chart = useRef<Chart | null>();
    
    useEffect(() => {
        // Initialize chart
        chart.current = init('chart')
        chart.current?.applyNewData(data)
        
        // Add RSI indicator
        chart.current?.createIndicator('RSI', true, { id: 'chart' })
        
        return () => {
            dispose('chart')
        }
    }, [])

    useEffect(() => {
        // Update latest candle
        const lastData = data[data.length - 1]
        lastData.low = Math.min(lastData.open, lastData.close)
        lastData.high = Math.max(lastData.open, lastData.close)
        chart.current?.updateData(lastData)
    }, [data])

    return <div id="chart" style={{ height: '90%' }} />
}

Fetching Chart Data

1

Query historical prices

Use GraphQL to fetch historical price data for the chart:
query GetPriceHistory {
  prices(
    last: 500
    interval: ONE_HOUR
    where: { symbol: { eq: "BTCUSDT" } }
  ) {
    edges {
      node {
        bucket
        open
        high
        low
        close
        volume
        symbol
      }
    }
  }
}
Parameters:
  • last: Number of data points to retrieve (max 500)
  • interval: Time interval (ONE_MINUTE, FIVE_MINUTES, ONE_HOUR, ONE_DAY)
  • where: Filter by symbol
2

Transform data to KLineData format

Convert GraphQL response to KLineCharts format:
const chartData: KLineData[] = data.prices.edges.map(edge => ({
  timestamp: new Date(edge.node.bucket).getTime(),
  open: edge.node.open,
  high: edge.node.high,
  low: edge.node.low,
  close: edge.node.close,
  volume: edge.node.volume
}))
3

Initialize chart with data

Pass the transformed data to the chart component:
import { PricesChart } from '@/features/prices/components/prices-chart'

function ChartView() {
  const { data } = useQuery(GetPriceHistoryQuery)
  
  if (!data) return <p>Loading...</p>
  
  const chartData = transformData(data)
  
  return <PricesChart data={chartData} />
}

Multi-Chart View

Display multiple cryptocurrency charts side by side using the FocusedPricesCharts component (src/react-app/src/features/prices/components/focused-prices-charts.tsx):
import { graphql } from '@/gql'
import { useQuery } from "@apollo/client"
import { PriceChartSimple } from './price-chart-simple'

const focusedPriceLineQuery = graphql(`
  query GetFocusedPriceLine {
    ETH: prices(last: 500, interval: ONE_HOUR, where: {symbol:{eq:"ETHUSDT"}}) {
      edges {
        node {
          bucket
          close
          symbol
          volume
        }
      }
    }
    BTC: prices(last: 500, interval: ONE_HOUR, where: {symbol:{eq:"BTCUSDT"}}) {
      edges {
        node {
          bucket
          close
          symbol
          volume
        }
      }
    }
  }
`)

export const FocusedPricesCharts = () => {
    const { data, loading } = useQuery(focusedPriceLineQuery)
    
    if (loading) return <p>Loading...</p>
    if (!data) return <p>No data</p>

    const ETHData = data.ETH?.edges?.map(e => ({
        bucket: e.node.bucket,
        price: e.node.close
    }))
    
    const BTCData = data.BTC?.edges?.map(e => ({
        bucket: e.node.bucket,
        price: e.node.close
    }))

    return (
        <>
            <PriceChartSimple title="ETH" data={ETHData} />
            <PriceChartSimple title="BTC" data={BTCData} />
        </>
    )
}
This query uses GraphQL aliases (ETH: and BTC:) to fetch data for multiple symbols in a single request.

Adding Technical Indicators

KLineCharts supports numerous technical indicators out of the box:

RSI (Relative Strength Index)

chart.current?.createIndicator('RSI', true, { id: 'chart' })

Moving Average

chart.current?.createIndicator('MA', false, {
  id: 'chart',
  paneOptions: {
    height: 100
  }
})

MACD

chart.current?.createIndicator('MACD', true, { id: 'chart' })

Bollinger Bands

chart.current?.createIndicator('BOLL', false, { id: 'chart' })

Real-time Updates

To keep charts updated with live prices, implement a polling or subscription strategy:

Polling Strategy

import { useQuery } from '@apollo/client'

const { data } = useQuery(GetPriceHistoryQuery, {
  pollInterval: 5000, // Poll every 5 seconds
})

GraphQL Subscription

For true real-time updates, use GraphQL subscriptions:
subscription OnPriceUpdate($symbol: String!) {
  priceUpdated(symbol: $symbol) {
    bucket
    open
    high
    low
    close
    volume
    symbol
  }
}
Implement in your component:
import { useSubscription } from '@apollo/client'

function RealtimeChart({ symbol }) {
  const { data: subscriptionData } = useSubscription(
    OnPriceUpdateSubscription,
    { variables: { symbol } }
  )
  
  useEffect(() => {
    if (subscriptionData?.priceUpdated) {
      // Update chart with new data point
      updateChartData(subscriptionData.priceUpdated)
    }
  }, [subscriptionData])
  
  // ...
}

Supported Intervals

PriceSignal aggregates price data at multiple intervals:
IntervalGraphQL ValueUse Case
1 minuteONE_MINUTEShort-term trading
5 minutesFIVE_MINUTESIntraday analysis
15 minutesFIFTEEN_MINUTESMedium-term trends
1 hourONE_HOURDaily monitoring
4 hoursFOUR_HOURSSwing trading
1 dayONE_DAYLong-term analysis

Chart Customization

Customize chart appearance and behavior:

Custom Styles

import { init } from 'klinecharts'

const chart = init('chart', {
  styles: {
    candle: {
      type: 'candle_solid',
      bar: {
        upColor: '#26A69A',
        downColor: '#EF5350'
      }
    },
    grid: {
      horizontal: {
        show: true,
        color: '#E0E0E0'
      }
    }
  }
})

Time Format

chart.setLocale('en-US')
chart.setTimezone('America/New_York')

Precision

chart.setPriceVolumePrecision(2, 0) // 2 decimal places for price, 0 for volume

Price Data Structure

The price data returned from the backend follows this schema:
interface PriceData {
  bucket: string        // ISO 8601 timestamp
  open: number         // Opening price
  high: number         // Highest price in interval
  low: number          // Lowest price in interval
  close: number        // Closing price
  volume: number       // Trading volume
  symbol: string       // Trading pair (e.g., "BTCUSDT")
}

Performance Optimization

Limit Data Points

Query only the data you need:
query GetRecentPrices {
  prices(last: 100, interval: ONE_HOUR, where: {symbol:{eq:"BTCUSDT"}}) {
    # Only fetch last 100 candles for better performance
  }
}

Debounce Updates

When receiving frequent updates, debounce chart refreshes:
import { debounce } from 'lodash'

const updateChart = debounce((newData) => {
  chart.current?.updateData(newData)
}, 100)

Lazy Load Historical Data

Load older data on demand:
function loadMoreData() {
  const oldestTimestamp = chartData[0].timestamp
  
  // Fetch data before oldest timestamp
  fetchPrices({
    before: oldestTimestamp,
    limit: 100
  })
}

Integration with Price Rules

Visualize your price rules on the chart by adding horizontal lines:
function addRuleLine(price: number, ruleId: string) {
  chart.current?.createShape({
    name: 'horizontalRayLine',
    points: [{ timestamp: Date.now(), value: price }],
    id: ruleId,
    styles: {
      line: {
        color: '#FF6B6B',
        size: 2,
        style: 'dashed'
      }
    }
  })
}

Troubleshooting

Chart not rendering

  • Ensure container has explicit height: style={{ height: '400px' }}
  • Verify data is in correct KLineData format
  • Check browser console for initialization errors

Updates not showing

  • Confirm data array reference is changing (not mutating)
  • Use updateData() for latest candle, applyNewData() for full dataset
  • Check timestamp formats are consistent

Performance issues

  • Reduce number of data points (use last parameter)
  • Disable auto-scaling if not needed
  • Remove unused indicators
  • Use requestAnimationFrame for smooth updates

Next Steps

Creating Rules

Set up alerts based on chart patterns

Telegram Setup

Get notified when price targets are hit

Build docs developers (and LLMs) love