import React from 'react'; import StockHeatmap from '@rongmz/react-stock-heatmap'; import '@rongmz/react-stock-heatmap/example/src/index.css'; import toast, { Toaster } from 'react-hot-toast'; import { v4 as uuidv4 } from 'uuid'; import axios from 'axios'; import { Select, Button, Input, Space } from 'tdesign-react'; import { DeleteIcon } from 'tdesign-icons-react'; import 'tdesign-react/es/style/index.css'; import PQueue from 'p-queue'; const queue = new PQueue({ concurrency: 5 }); const {Option} = Select; function formatTimestamp(timestamp) { // 创建一个新的 Date 对象 const date = new Date(timestamp); // 获取小时、分钟和秒 const hours = date.getHours(); const minutes = date.getMinutes(); const seconds = date.getSeconds(); // 获取毫秒 const milliseconds = date.getMilliseconds(); // 将小时、分钟、秒、毫秒格式化为两位数字(除了毫秒可能是三位) const formattedHours = hours.toString().padStart(2, '0'); const formattedMinutes = minutes.toString().padStart(2, '0'); const formattedSeconds = seconds.toString().padStart(2, '0'); const formattedMilliseconds = milliseconds.toString().padStart(3, '0'); // 返回格式化的时间字符串 return `${formattedHours}:${formattedMinutes}:${formattedSeconds}.${formattedMilliseconds}`; } function parseStockData(data) { // Extracting values from the input data const { asks, bids, last_price, last_qty, total_qty, time, side } = data; // Convert asks and bids to the required format const processOrders = (orders) => orders.map(([rate, qty]) => ({ rate: rate, orders: 1, // Assuming each price level has one order qty })); // Calculate additional values const high = Math.max(...asks.map(a => a[0]), last_price); const low = Math.min(...bids.map(b => b[0]), last_price); const open = bids[0][0]; // Assuming the first bid rate as open const close = last_price; const volume = total_qty; // Total traded volume // Calculate average price (simplified as an average of high and low) const avgPrice = (high + low) / 2; // Construct the final object return { marketDepth: { lastBuyPrice: side === 'buy' ? last_price : 0, lastBuyQty: side === 'buy' ? last_qty : 0, lastSellPrice: side === 'sell' ? last_price : 0, lastSellQty: side === 'sell' ? last_qty : 0, priceChangeAmt: last_price - open, // Simplified price change amount priceChangePct: ((last_price - open) / open * 100).toFixed(2), lastTradedTS: Date.now(), open, high, low, close, volume, avgPrice, buyOrderVolume: bids.reduce((sum, [, qty]) => sum + qty, 0), buys: processOrders(bids), sellOrderVolume: asks.reduce((sum, [, qty]) => sum + qty, 0), sells: processOrders(asks), side: side }, ts: formatTimestamp(time), time: time, tradingsymbol: "XYZ123", pendingOrders: [] }; } const flushMemoryDbData = async (data, symbol) => { // console.log('Invoking flush-memory-db-data', data.length, symbol); try { await window.electronAPI.flushMemoryDbData(data, symbol); // const result = await window.electronAPI.flushMemoryDbData(data, symbol); // console.log('Flush successful', result); } catch (error) { // console.error('Error flushing memory DB data', error); } }; export default () => { const [isActivation, setIsActivation] = React.useState(false); const [activationCode, setActivationCode] = React.useState(); const [loginSymbol,setLoginSymbol]= React.useState(); const [loading, setLoading] = React.useState(true); const progressRef = React.useRef(null); /** @type {React.MutableRefObject} */ const heatmapRef = React.useRef(null); const [windowDim, setWindowDim] = React.useState([0, 0]); const [autoScroll, setAutoScroll] = React.useState(true); const toggleAutoScroll = (value) => { setAutoScroll(value); }; // 下拉框部分 const [symbolOptions, setSymbolOptions] = React.useState([]); const [editOrCreate, toggleEditOrCreate] = React.useState('edit'); const [inputSymbol, changeInputSymbol] = React.useState({ }); const handleClickConfirm = async () => { toast.remove() if (!inputSymbol.symbol) return toast.error("品种不能为空!") if (!inputSymbol.port) return toast.error("端口号不能为空!") const options = {...inputSymbol, id: uuidv4()}; const newOptions = [...symbolOptions, options]; setSymbolOptions(newOptions); changeInputSymbol({symbol:"",port:""}); toggleEditOrCreate('edit'); await window.electronAPI.setSymbolData(newOptions) }; const handleDeleteSymbolOption= async (option)=>{ const newOptions = symbolOptions.filter(item=>item.id !== option.id) setSymbolOptions(newOptions) if(loginSymbol === option.id) setLoginSymbol(undefined) await window.electronAPI.setSymbolData(newOptions) } // ------------ Load data ------------- // React.useEffect(() => { // const symbolInfo = symbolOptions.find((item)=> item.id == loginSymbol) // const ws = new WebSocket(`ws://localhost:${symbolInfo.port}`); // let ref = heatmapRef.current // // console.log('ws创建完成') // ws.onmessage = function(event) { // const message = JSON.parse(event.data); // // let stock = parseStockData(message) // // ref.addData(stock) // // if (progressRef.current !== null) { // progressRef.current.innerHTML = ` 等待数据推送 ${(100 * ref.data.length / ref.windowLength + 1).toFixed(0)}% ...` // // if (ref.data.length >= ref.windowLength) { // setLoading(false) // } // } // }; // // ws.onerror = function(event) { // console.error("WebSocket error observed:", event); // }; // }, []); const connectWebSocket = ()=>{ const symbolInfo = symbolOptions.find((item)=> item.id === loginSymbol) const ws = new WebSocket(`ws://localhost:${symbolInfo.port}`); let ref = heatmapRef.current // let prevFlushMemoryDbDataTimestamp = new Date().getTime() console.log('ws创建完成') ws.onmessage = function(event) { const message = JSON.parse(event.data); let stock = parseStockData(message) ref.addData(stock) // 每1秒刷新一次后台的数据 // let now = new Date().getTime() // if (now - 1000 > prevFlushMemoryDbDataTimestamp) { // // 使用 slice 方法提取最后 5000 条数据,保存到本地 // const startIndex = Math.max(ref.data.length - 5000, 0); // let saveData = ref.data.slice(startIndex); // queue.add(() => flushMemoryDbData(saveData, symbolInfo.symbol)); // } // 数据推送进度条 if (progressRef.current !== null) { progressRef.current.innerHTML = ` 等待数据推送 ${(100 * ref.data.length / ref.windowLength + 1).toFixed(0)}% ...` if (ref.data.length >= ref.windowLength) { setLoading(false) } } }; ws.onerror = function(event) { console.error("WebSocket error observed:", event); }; } // ------------ Load data ------------- const checkStatus = async ()=>{ toast.remove(); const loginInfo = await window.electronAPI.getLoginInfoData() || {} const params = { "code": loginInfo.code, "machine": loginInfo.machine } const response = await axios.post('http://139.159.224.218:38888/check_status',params) if(response.data.code !== 200){ if(window.sessionStorage.getItem("_HEATMAP_IS_LOGIN") === "1") toast.error(response.data.msg) window.sessionStorage.setItem("_HEATMAP_IS_LOGIN", "0") setIsActivation(false) return } setIsActivation(true) window.sessionStorage.setItem("_HEATMAP_IS_LOGIN", "1") } // 保存数据库到本地 const saveLocalDb = async () => { const symbolInfo = symbolOptions.find((item)=> item.id === loginSymbol) let ref = heatmapRef.current await window.electronAPI.setDbData(ref.data, symbolInfo.symbol) } // 获取本地的数据库 const readLocalDb = async () => { const symbolInfo = symbolOptions.find((item)=> item.id === loginSymbol) let ref = heatmapRef.current ref.data = await window.electronAPI.getDbData(symbolInfo.symbol) } const handleActivation = async ()=>{ toast.remove(); const params = { "code": activationCode, "machine": uuidv4() } const response = await axios.post('http://139.159.224.218:38888/login', params) if(response.data.code === 200){ await window.electronAPI.setLoginInfoData(params) window.sessionStorage.setItem("_HEATMAP_IS_LOGIN", "1") toast.success(response.data.msg) setInterval(()=>{ checkStatus() },5000) setIsActivation(true) await readLocalDb() connectWebSocket() // 每300秒存放一次本地数据 setInterval(() => { saveLocalDb() }, 300 * 1000) }else{ toast.error(response.data.msg); } } // ---------- window update ------------ React.useEffect( async () => { // 处理品种选择 let symbolInfo = await window.electronAPI.getSymbolData(); if(symbolInfo.length > 0) { setSymbolOptions(symbolInfo) } // 处理是否有登录信息 let loginInfo = await window.electronAPI.getLoginInfoData() || {}; if(loginInfo.code) { toast.remove(); try { const code = loginInfo.code || '' setActivationCode(code || '') }catch (err){ await window.electronAPI.setLoginInfoData({}) window.sessionStorage.setItem("_HEATMAP_IS_LOGIN", "0") return } } const updateFn = () => { setWindowDim([ window.innerWidth, window.innerHeight ]); } updateFn(); window.addEventListener('resize', updateFn); return () => window.removeEventListener('resize', updateFn); }, []); // ---------- window update ------------ return (
{loading &&
等待数据推送...
等待数据推送 0%
}
{!autoScroll &&
{ setAutoScroll(true) }}> 继续
} {/* */}
{!isActivation &&
软件登录
{setActivationCode(value)}} /> changeInputSymbol({...inputSymbol,symbol:value})}> changeInputSymbol({...inputSymbol,port:value})}>
)}
} > {symbolOptions.map((option) => ( ))}
登 录
} ) }