| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444 |
- 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, Checkbox } 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 [isPersistent,setIsPersistent]= React.useState(false);
- const [loading, setLoading] = React.useState(true);
- const progressRef = React.useRef(null);
- /** @type {React.MutableRefObject<StockHeatmap>} */
- 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()
- if (isPersistent){
- // 每30秒存放一次本地数据
- setInterval(() => {
- saveLocalDb()
- }, 30 * 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
- }
- }
- // 处理持久化选项
- let persistent = await window.electronAPI.getIsPersistentData() || false;
- if (persistent) setIsPersistent(persistent)
- const updateFn = () => {
- setWindowDim([
- window.innerWidth,
- window.innerHeight
- ]);
- }
- updateFn();
- window.addEventListener('resize', updateFn);
- return () => window.removeEventListener('resize', updateFn);
- }, []);
- // ---------- window update ------------
- return (
- <div className="wapper">
- <div className={isActivation ? "visibilityVisible" : "visibilityHidden"}>
- <React.Fragment>
- {loading &&
- <div className="loadingIndicator">
- <div className="loadingSpinner">
- <div className="loader">等待数据推送...</div>
- </div>
- <div ref={progressRef}> 等待数据推送 0%</div>
- </div>
- }
- <StockHeatmap ref={heatmapRef} width={windowDim[0]} height={windowDim[1]} autoScroll={autoScroll}
- toggleAutoScroll={toggleAutoScroll} />
- <div className="btnContainer">
- <button onClick={() => {
- if (heatmapRef.current !== null) heatmapRef.current.setZoomLevel(60)
- }}>1分钟视域
- </button>
- <button onClick={() => {
- if (heatmapRef.current !== null) heatmapRef.current.setZoomLevel(60 * 5)
- }}>5分钟视域
- </button>
- <button onClick={() => {
- if (heatmapRef.current !== null) heatmapRef.current.setZoomLevel(60 * 10)
- }}>10分钟视域
- </button>
- <button onClick={() => {
- if (heatmapRef.current !== null) heatmapRef.current.setZoomLevel(60 * 15)
- }}>15分钟视域
- </button>
- <button onClick={() => {
- if (heatmapRef.current !== null) heatmapRef.current.setZoomLevel(60 * 30)
- }}>30分钟视域
- </button>
- <button onClick={() => {
- if (heatmapRef.current !== null) heatmapRef.current.setZoomLevel(60 * 60)
- }}>60分钟视域
- </button>
- {!autoScroll &&
- <div className="playBtn" onClick={() => {
- setAutoScroll(true)
- }}> 继续
- </div>
- }
- {/* <button onClick={() => { */}
- {/* const HHmmss = window.prompt('Enter HH:mm:ss', '00:00:00'); */}
- {/* let split = HHmmss.split(':'); */}
- {/* let position = (+split[0]-9)*3600 + (+split[1]*60) + (+split[2]); */}
- {/* if (heatmapRef.current !== null) heatmapRef.current.moveDataWindow(position); */}
- {/* }}>Set Position</button> */}
- </div>
- </React.Fragment>
- </div>
- {!isActivation &&
- <div className="layoutContainer">
- <div className="activationBox">
- <div className="title">
- 软件登录
- </div>
- <div className="iptWp customIpt">
- <Input className="password ipt" placeholder="请输入登录码" type="password" value={activationCode || ''} onChange={(value)=>{setActivationCode(value)}} />
- <Select
- placeholder="请选择品种"
- className="symbol ipt"
- clearable
- value={loginSymbol}
- onChange={(value)=>{setLoginSymbol(value)}}
- panelBottomContent={
- <div
- className="select-panel-footer"
- style={{
- position: 'sticky',
- bottom: 0,
- backgroundColor: 'var(--td-bg-color-container)',
- zIndex: 2,
- }}
- >
- {editOrCreate === 'edit' ? (
- <div
- style={{
- padding: '8px 6px',
- borderTop: '1px solid var(--td-border-level-2-color)',
- }}
- >
- <Button theme="primary" size="small" variant="text" onClick={() => toggleEditOrCreate('create')}>
- 新增选项
- </Button>
- </div>
- ) : (
- <div style={{ padding: 8, borderTop: '1px solid var(--td-border-level-2-color)' }}>
- <Space>
- <Input placeholder="请输入品种" size="small" autofocus value={inputSymbol.symbol} onChange={(value) => changeInputSymbol({...inputSymbol,symbol:value})}></Input>
- <Input placeholder="请输入端口号" size="small" autofocus value={inputSymbol.port} onChange={(value) => changeInputSymbol({...inputSymbol,port:value})}></Input>
- </Space>
- <Button size="small" style={{ marginTop: '12px' }} onClick={handleClickConfirm}>
- 确认
- </Button>
- <Button
- theme="default"
- size="small"
- style={{ marginTop: '12px', marginLeft: '8px' }}
- onClick={() => toggleEditOrCreate('edit')}
- >
- 取消
- </Button>
- </div>
- )}
- </div>
- }
- >
- {symbolOptions.map((option) => (
- <Option key={option.id} value={option.id} label={`${option.symbol}:${option.port}`}>
- <div style={{ display: 'flex', alignItems: 'center' }}>
- <div style={{ flex:"1",paddingRight:"10px",boxSizing:"border-box" }}>{`${option.symbol}:${option.port}`}</div>
- <DeleteIcon style={{ width:"16px"}} onClick={(e)=>{
- e.stopPropagation();
- handleDeleteSymbolOption(option)
- }}/>
- </div>
- </Option>
- ))}
- </Select>
- <Checkbox checked={isPersistent} onChange={async (value)=>{
- setIsPersistent(value)
- await window.electronAPI.setIsPersistentData(value)
- }}>是否持久化数据</Checkbox>
- </div>
- <div className="btnWp">
- <div className="btn" onClick={handleActivation}>登 录</div>
- </div>
- </div>
- </div>
- }
- <Toaster className="toasterWp" />
- </div>
- )
- }
|