Mở đầu
Chào mọi người, chắc hẳn mọi người đã quá quen thuộc với trào lưu Trading trong những năm gần đây. Dưới góc độ của một nhà phát triển chúng ta hãy cùng tìm hiểu các yếu tố kĩ thuật tạo nên một sàn giao dịch như thế. Mở đầu hôm nay chúng ta sẽ cùng làm một module khá quen thuộc, tạo nên nét đặt trưng của các sàn giao dịch đó là Candlestick Chart
Công nghệ mình dùng cho project là: ReactJS, SVG
Nội dung
1.Data
Đầu tiên chúng ta cùng tìm hiểu về những ngọn nến của Candle Chart. Một Candle (ngọn nến) sẽ bao gồm ít nhất 4 thuộc tính: Open price, High price, Low price và Close price. Nếu Open price < Close price ngọn nến sẽ có màu xanh (mua nhà) và ngược lại ngọn nến sẽ có màu đỏ (bán nhà)
Về mặt dữ liệu, đầu vào Candle Chart của chúng ta sẽ là một mảng các object, mỗi object { O, H, L, C }
đại điện cho các thuộc tính kể trên sẽ được dùng để vẽ nên một candle
.
Ở đây chúng ta sẽ tạo một module data.js
để generate dữ liệu
const Direction = () => (Math.random() < 0.5 ? -1 : 1)
const Random = (rand) => Math.random() * rand
const RandomProbabilityZero = (rand) => (Math.random() < 0.3 ? 0 : Random(rand)) //cái random này để có 30% trả về 0 để cho cái Chart của chúng ra đỡ rối mắt thôi
const Round = (num) => Math.round(num * 100) / 100 // lấy 2 chữ số thập phân export function genCandle(prevCandleC) { const O = prevCandleC // ở đây mình lấy Open price của nến hiện tại sẽ bằng Close price của cây nến trước đó. // Thực tế 2 thông số này thường sẽ không bằng nhau, nhưng mình để thế cho dễ generate vậy :(( const C = Round(O + Direction() * Random(500)) const H = Round((O < C ? C : O) + RandomProbabilityZero(300)) const L = Round((O > C ? C : O) - RandomProbabilityZero(300)) return { O, H, L, C }
} export function genCandles(initNum, amount) { let candles = [genCandle(initNum)] Array.from(Array(amount)).forEach((x, i) => { // hàm này tương đương for(let i = 0; i < amount; i++) nhìn cũng rối nhưng không sao, mình thích vậy :)) candles.push(genCandle(candles[i].C)) }) return candles
}
2.Vẽ nến nào
Ở đây mình sẽ dùng thẻ svg
, bạn nào chưa biết có thể tìm hiểu thông qua bài viết trước của mình nhé npmjs chart
// CandleChart.jsx
<svg width='1200' height='600'
> <path fill='#ee4947' stroke='#ee4947' d={pathD.dRed}></path> <path fill='#22f6db' stroke='#22f6db' d={pathD.dGreen}></path> // ...
</svg>
Chúng ta sẽ nhóm hết các cây nến màu xanh, đỏ vào với nhau để trong cùng một thẻ path
nhằm cho đỡ rối và tăng hiệu xuất.
Trước hết tạo module Candle.js
để tính ra pathD từ dữ liệu đầu vào là một mảng các cây nến candles
. Ở đây mình sẽ fix cứng một số thông số
const HEIGHT = 600
const CANDLE_W = 20 // chiều rộng cấy nến
const D = 4 // khoảng cách 2 cây nến
let deltaY = 0 // tỉ số số pixel tương ứng với giá trị (H,O,L,C price) của cây nến
let min = 0 // max price của tất cả cây nến
let max = 0 export function calcPathD(candles) { // hàm tính `d` của tất cả cây nến findMinMax(candles) deltaY = HEIGHT / (max - min) let dRed = '' let dGreen = '' candles.forEach((candle, index) => { if (candle.O < candle.C) { // nến xanh dGreen += calcD(candle, index) } else { // nến đỏ let candleTem = { H: candle.H, O: candle.C, C: candle.O, L: candle.L } dRed += calcD(candleTem, index) } }) return { dRed, dGreen }
} function calcD(candle, index) { // hàm tính `d` của một cây nến cụ thể let posH = (max - candle.H) * deltaY let posO = (max - candle.O) * deltaY let posC = (max - candle.C) * deltaY let posL = (max - candle.L) * deltaY let posXCandle = index * (CANDLE_W + D) + CANDLE_W / 2 // tính tọa độ của cây nến theo trục X let d = `M ${posXCandle} ${posH} L ${posXCandle} ${posO} ` // vẽ cái đường upper shadow d += `M ${posXCandle - CANDLE_W / 2} ${posO} ` // vẽ body cây nến d += `L ${posXCandle + CANDLE_W / 2} ${posO} ` d += `L ${posXCandle + CANDLE_W / 2} ${posC} ` d += `L ${posXCandle - CANDLE_W / 2} ${posC} Z ` d += `M ${posXCandle} ${posC} L ${posXCandle} ${posL} ` // vẽ đường lower shadow return d
} // ....
const findMinMax = (candles) => { min = Number.MAX_VALUE max = Number.MIN_VALUE candles.forEach((candle) => { if (max < candle.H) max = candle.H if (min > candle.L) min = candle.L })
}
Ở component CandleChart
chúng ta sẽ import 2 module và khởi tạo Chart
import { genCandle, genCandles } from './data'
import { calcPathD, calcValueY } from './Candle' let candles = genCandles(10000, 40) khởi tạo function CandleChart() { const [pathD, setPathD] = useState(calcPathD(candles)) // tính attribute d của thẻ path //... return ( <> ///... <svg width='1200' height='600' > <path fill='#ee4947' stroke='#ee4947' d={pathD.dRed}></path> <path fill='#22f6db' stroke='#22f6db' d={pathD.dGreen}></path> //... </svg> </> )
} export default CandleChart
3.Thêm sự kiện mouse xem giá trị của từng cây nến
import React, { useState, useEffect } from 'react'
import { genCandle, genCandles } from './data'
import { calcPathD, calcValueY } from './Candle' let candles = genCandles(10000, 40) function CandleChart() { const [pathD, setPathD] = useState(calcPathD(candles)) const [pointer, setPointer] = useState({ x: -100, y: -100, candleIndex: -1 }) const handleMouseMove = (e) => { let x = e.nativeEvent.layerX let y = e.nativeEvent.layerY let candleIndex = Math.floor(x / 24) x = candleIndex * 24 + 10 setPointer({ x, y, candleIndex }) } const handleMouseLeonMouseLeave = (e) => { setPointer({ x: -100, y: -100, candleIndex: -1 }) } const candleSelected = candles[pointer.candleIndex] const color = candleSelected?.O > candleSelected?.C ? 'red' : 'green' let delta = (candleSelected?.C - candleSelected?.O).toFixed(2) delta = delta > 0 ? `+${delta}` : delta const pointerValueY = calcValueY(pointer.y) return ( <> <div className='info'> // các price của cây nến <div className='label'>{'Open '}</div> <div className={`value ${color}`}>{candleSelected?.O}</div> <div className='label'>{'High '}</div> <div className={`value ${color}`}>{candleSelected?.H}</div> <div className='label'>{'Low '}</div> <div className={`value ${color}`}>{candleSelected?.L}</div> <div className='label'>{'Close '}</div> <div className={`value ${color}`}>{candleSelected?.C}</div> <div className={`value ${color}`}>{delta}</div> </div> <svg width='1200' height='600' onMouseMove={handleMouseMove} onMouseLeave={handleMouseLeonMouseLeave} > <path fill='#ee4947' stroke='#ee4947' d={pathD.dRed}></path> <path fill='#22f6db' stroke='#22f6db' d={pathD.dGreen}></path> <line x1={pointer.x} y1={0} x2={pointer.x} y2={800} stroke='#888' strokeDasharray='5' /> // vẽ 2 đường nét đứt ngang và dọc <line x1={0} y1={pointer.y} x2={1130} y2={pointer.y} stroke='#888' strokeDasharray='5' /> <g> <line x1={1130} y1={0} x2={1130} y2={600} stroke='#666' /> <text x={1135} y={pointer.y + 6} fill='#aaa'> // xem price của tọa độ đang chọn theo trục Y {pointerValueY} </text> </g> </svg> </> )
} export default CandleChart
4.Thêm nến vào biểu đồ để biểu đồ chuyển động như thật : ))
Chúng ta chỉ cần thêm setInterval
để thêm candle
vào biến candles
mỗi một khoảng thời gian là chart sẽ chạy ngay thôi
// CandleChart.jsx
//...
useEffect(() => { let setIntervalId = setInterval(() => { candles.shift() // thêm vào thì phải xóa đi candles.push(genCandle(candles[candles.length - 1].C)) setPathD(calcPathD(candles)) // set lại giá trị pathD là React sẽ tự rereder lại cho ta }, 2000) //thêm mỗi 2 giây return () => { clearInterval(setIntervalId) }
}, [])
//...
Vậy là ok rồi đó
Nhìn qua có vẻ ổn đúng không? Nhưng có một vấn đề là mỗi lần chúng ta move chuột trên biểu đồ thì component sẽ được rerender lại để cập nhật thông tin chi tiết của cây nến, 2 đường nét đứt và cả price theo trục y nữa, mỗi lần như thế hàm calcPathD
sẽ được gọi lại gây lãng phí tài nguyên, với dữ liệu nhiều sẽ làm trình duyệt bị giật ảnh hưởng đến trải nghiệm của người dùng (trong thực tế vấn đề này xảy ra khá nhiều, những bạn mới thường không để ý, hoặc không biết để xử lý). Nếu bạn nào quan tâm vấn đề này hãy ping mình biết (upvote, follow, comment ), mình sẽ viết một bài giới thiệu một kĩ thuật rất phổ biến memorization
để giải quyết bài toán này và một số bài toán tương tự nhé.
Ngoài ra bạn nào muốn mình viết thêm bài tích hợp xử lý realtime
nhận dữ liệu từ server vào project này thì cũng ping mình biết nhé
À đây là demo dạo này mạng hơi kém, ai không xem được có thể bookmark lại để xem sau nhé
Kết luận
Cái title mình giật hơi quá mong các anh chị, các bạn, các em thông cảm nhé
Cảm ơn các bạn đã đọc!!!