- vừa được xem lúc

Cùng làm Candlestick Chart. Thay vì làm người chơi, hãy làm chủ

0 0 89

Người đăng: Phan Quang Hiếu

Theo Viblo Asia

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

Bình luận

Bài viết tương tự

- vừa được xem lúc

Giới thiệu Typescript - Sự khác nhau giữa Typescript và Javascript

Typescript là gì. TypeScript là một ngôn ngữ giúp cung cấp quy mô lớn hơn so với JavaScript.

0 0 525

- vừa được xem lúc

Bạn đã biết các tips này khi làm việc với chuỗi trong JavaScript chưa ?

Hi xin chào các bạn, tiếp tục chuỗi chủ đề về cái thằng JavaScript này, hôm nay mình sẽ giới thiệu cho các bạn một số thủ thuật hay ho khi làm việc với chuỗi trong JavaScript có thể bạn đã hoặc chưa từng dùng. Cụ thể như nào thì hãy cùng mình tìm hiểu trong bài viết này nhé (go).

0 0 433

- vừa được xem lúc

Một số phương thức với object trong Javascript

Trong Javascript có hỗ trợ các loại dữ liệu cơ bản là giống với hầu hết những ngôn ngữ lập trình khác. Bài viết này mình sẽ giới thiệu về Object và một số phương thức thường dùng với nó.

0 0 153

- vừa được xem lúc

Tìm hiểu về thư viện axios

Giới thiệu. Axios là gì? Axios là một thư viện HTTP Client dựa trên Promise.

0 0 145

- vừa được xem lúc

Imports và Exports trong JavaScript ES6

. Giới thiệu. ES6 cung cấp cho chúng ta import (nhập), export (xuất) các functions, biến từ module này sang module khác và sử dụng nó trong các file khác.

0 0 110

- vừa được xem lúc

Bài toán đọc số thành chữ (phần 2) - Hoàn chỉnh chương trình dưới 100 dòng code

Tiếp tục bài viết còn dang dở ở phần trước Phân tích bài toán đọc số thành chữ (phần 1) - Phân tích đề và những mảnh ghép đầu tiên. Bạn nào chưa đọc thì có thể xem ở link trên trước nhé.

0 0 245