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
constDirection=()=>(Math.random()<0.5?-1:1)constRandom=(rand)=> Math.random()* rand
constRandomProbabilityZero=(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ôiconstRound=(num)=> Math.round(num *100)/100// lấy 2 chữ số thập phânexportfunctiongenCandle(prevCandleC){constO= 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 :((constC=Round(O+Direction()*Random(500))constH=Round((O<C?C:O)+RandomProbabilityZero(300))constL=Round((O>C?C:O)-RandomProbabilityZero(300))return{O,H,L,C}}exportfunctiongenCandles(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ố
constHEIGHT=600constCANDLE_W=20// chiều rộng cấy nếnconstD=4// khoảng cách 2 cây nếnlet deltaY =0// tỉ số số pixel tương ứng với giá trị (H,O,L,C price) của cây nếnlet min =0// max price của tất cả cây nếnlet max =0exportfunctioncalcPathD(candles){// hàm tính `d` của tất cả cây nếnfindMinMax(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 }}functioncalcD(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 Xlet 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 shadowreturn d
}// ....constfindMinMax=(candles)=>{
min = Number.MAX_VALUE
max = Number.MIN_VALUE
candles.forEach((candle)=>{if(max < candle.H) max = candle.Hif(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
functionCandleChart(){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></>)}exportdefault 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)functionCandleChart(){const[pathD, setPathD]=useState(calcPathD(candles))const[pointer, setPointer]=useState({ x:-100, y:-100, candleIndex:-1})consthandleMouseMove=(e)=>{let x = e.nativeEvent.layerX
let y = e.nativeEvent.layerY
let candleIndex = Math.floor(x /24)
x = candleIndex *24+10setPointer({ x, y, candleIndex })}consthandleMouseLeonMouseLeave=(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></>)}exportdefault 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âyreturn()=>{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!!!
Nguồn: viblo.asia