Просмотр исходного кода

1. 内部延迟问题忽略
2. 查丢数据的问题
3. 画球大小的问题,思路在这
https://bookmap.com/knowledgebase/docs/KB-SettingUpAndOperating-HeatmapTradedVolumeVisualization
它现在是:最大交易量

改成:该算法将特定像素时间单位内已执行交易的总交易量与交易工具的平均交易量相对比。

4. 鼠标指上去看成交
5. 接个binance数据看看
6. 增量数据没处理
7. LogInfo延迟比较高

skyffire 1 год назад
Родитель
Сommit
77727420aa
3 измененных файлов с 158 добавлено и 54 удалено
  1. 1 1
      example/src/App.js
  2. 134 41
      src/index.js
  3. 23 12
      src/utils.js

+ 1 - 1
example/src/App.js

@@ -30,7 +30,7 @@ function parseStockData(data) {
 
   // Convert asks and bids to the required format
   const processOrders = (orders) => orders.map(([rate, qty]) => ({
-    rate,
+    rate: rate,
     orders: 1,  // Assuming each price level has one order
     qty
   }));

+ 134 - 41
src/index.js

@@ -7,7 +7,7 @@ import * as d3Interpolate from 'd3-interpolate';
 import * as d3Shape from 'd3-shape';
 import * as d3Timer from 'd3-timer';
 import * as d3Ease from 'd3-ease';
-import { extractBidPrices, extractBidVolumes, extractMaxTradedVolume, extractMaxVolume, zoomTimeFormat } from './utils';
+import { extractBidPrices, extractBidVolumes, extractMaxTradedVolume, extractAvgTradedVolume, extractMaxVolume, zoomTimeFormat } from './utils';
 
 export const d3 = Object.assign(
   Object.assign(
@@ -36,6 +36,13 @@ export default class StockHeatmap extends React.Component {
   windowPosition = 0;
   autoScroll = true;
 
+  mouse = {
+    x: 0,
+    y: 0
+  }
+
+  circles = []
+
   /** Default Theme colors and dimensions */
   defaults = {
     borderPadding: [5, 5, 0, 0],
@@ -55,7 +62,7 @@ export default class StockHeatmap extends React.Component {
     yAxisTextPadding: 6,
     bidAskGraphPaddingLeft: 10,
     bidAskTransitionDuration: 500,
-    volumeCircleMaxRadius: 10,
+    volumeCircleMaxRadius: 1,
     runningRatioSeconds: 5,
     hmWidth: () => (this.props.width - this.defaults.borderPadding[1] - this.defaults.borderPadding[3] - this.defaults.bidAskWidth - this.defaults.axisYWidth),
     hmHeight: () => (this.props.height - this.defaults.borderPadding[0] - this.defaults.borderPadding[2] - this.defaults.axisXHeight),
@@ -80,6 +87,11 @@ export default class StockHeatmap extends React.Component {
       this.updateHeatmap();
       this.attachMouseListeners();
     }
+
+    const panel = this
+    setInterval(() => {
+      panel.updateHeatmap()
+    }, 10)
   }
   componentDidUpdate() {
     // console.log('component updtated');
@@ -149,37 +161,16 @@ export default class StockHeatmap extends React.Component {
       // console.log('drag x=', dragLength, moveDataPointsCount, this.windowPosition);
       this.moveDataWindow(this.windowPosition + moveDataPointsCount * (dragLength >= 0 ? -1 : 1));
 
-      this.autoScroll = false
+      if (dragLength > 60) {
+        this.autoScroll = false
+      }
     } else {
-      // const canvas = this.canvasRef.current;
-      // if (!canvas) return;
-      //
-      // const context = canvas.getContext('2d');
-      // if (!context) return;
-      //
-      // // 获取鼠标位置相对于 canvas 的坐标
-      // const rect = canvas.getBoundingClientRect();
-      // const x = event.clientX - rect.left;
-      // const y = event.clientY - rect.top;
-      //
-      // // 清除canvas并重新绘制
-      // context.clearRect(0, 0, canvas.width, canvas.height);
-      // // this.drawChart(); // 确保drawChart方法可以访问到context或者在方法内使用this.canvasRef获取
-      //
-      // this.updateHeatmap();
-      //
-      // // 绘制交叉线和文本
-      // context.beginPath();
-      // context.moveTo(x, 0);
-      // context.lineTo(x, canvas.height);
-      // context.moveTo(0, y);
-      // context.lineTo(canvas.width, y);
-      // context.strokeStyle = '#eeeeee';
-      // context.stroke();
-      //
-      // context.fillStyle = '#000';
-      // context.fillText(`X: ${x.toFixed(2)}`, x + 5, y + 15);
-      // context.fillText(`Y: ${y.toFixed(2)}`, x + 5, y + 30);
+      if (e.clientX < this.defaults.hmWidth() && e.clientY < this.defaults.hmHeight()) {
+        this.mouse = {
+          x: e.clientX,
+          y: e.clientY
+        };
+      }
     }
   }
 
@@ -295,12 +286,78 @@ export default class StockHeatmap extends React.Component {
       // 4. Draw buy-to-sell ratio
       this.drawBuy2SellRatio();
 
+      // 5. Draw mouse
+      this.drawMouse();
+
       // console.log('heatmap draw update');
       // this.clearCanvas(0, 0, this.defaults.hmWidth(), this.defaults.hmHeight(), '#aaaaaa');
     }
   }
 
   // ------------------------------ START: Canvas draw functions ---------------------------------------
+  getMousePos = (canvas, x, y) =>  {
+    var rect = canvas.getBoundingClientRect();
+
+    // 使用容器的内部宽度进行计算,排除 padding 影响
+    var style = window.getComputedStyle(canvas.parentNode);
+    var paddingLeft = parseFloat(style.paddingLeft) || 0;
+    var paddingRight = parseFloat(style.paddingRight) || 0;
+    var effectiveWidth = rect.width - paddingLeft - paddingRight;
+
+    return {
+      x: (x - rect.left - paddingLeft) * (canvas.width / effectiveWidth) * 1.03,
+      y: (y - rect.top) * (canvas.height / rect.height)
+    };
+  }
+  drawMouse = () => {
+    const canvas = this.canvasRef.current;
+    if (!canvas) return;
+
+    const context = canvas.getContext('2d');
+    if (!context) return;
+
+    let mouse = this.getMousePos(canvas, this.mouse.x, this.mouse.y)
+
+    const x = mouse.x;
+    const y = mouse.y;
+
+    // 移动事件处理
+    this.circles.forEach(circle => {
+      const distance = Math.sqrt(Math.pow(x - circle.x, 2) + Math.pow(y - circle.y, 2));
+      if (distance <= circle.radius) {
+        let d = circle.data
+        let depth = d.marketDepth;
+        let text = `${depth.side} ${depth.lastTradedQty} at ${depth.lastTradedPrice} `;
+
+        let color = d3.color('#fff').rgb();
+        color.opacity = 0.7
+        this.drawingContext.fillStyle = color.toString();
+        this.drawingContext.fillRect(
+          x + 2,
+          y + 2,
+          text.length * 10,
+          35
+        );
+
+        // 在这里执行更多的操作,如显示详情、更新数据等
+        context.fillStyle = d3.color(depth.side == 'sell' ? '#222' : '#222').rgb();
+        context.fillText(`${d.ts}`, x + 5, y + 15);
+        context.fillText(text, x + 5, y + 30);
+      }
+    });
+    //
+    // // 清除canvas并重新绘制
+    // context.clearRect(0, 0, canvas.width, canvas.height);
+
+    // 绘制交叉线和文本
+    context.beginPath();
+    context.moveTo(x, 30);
+    context.lineTo(x, this.defaults.hmHeight());
+    context.moveTo(30, y);
+    context.lineTo(this.defaults.hmWidth(), y);
+    context.strokeStyle = '#fff';
+    context.stroke();
+  }
 
   /**
    * Draw buy/sell ratio at bottom right corner
@@ -357,7 +414,7 @@ export default class StockHeatmap extends React.Component {
     this.drawingContext.textBaseline = 'top';
     // const assumedTextWidth = this.drawingContext.measureText('77:77:77').width + 20;
     // const maxTickInterval = this.defaults.hmWidth() / assumedTextWidth;
-    const bandInterval = Math.max(1, parseInt(this.windowedData.length / 18));
+    const bandInterval = Math.max(1, parseInt(this.windowedData.length / (this.defaults.hmWidth() / 102)));
     // console.log('bandInterval=', bandInterval);
     this.windowedData.map((d, i) => {
       if (i !=0 && i % bandInterval === 0) {
@@ -415,8 +472,21 @@ export default class StockHeatmap extends React.Component {
         let y = this.yScale(d) + yh2;
         this.drawingContext.moveTo(0, y);
         this.drawingContext.lineTo(this.defaults.axisTickSize, y);
-        this.drawingContext.fillText(d.toFixed(2), this.defaults.axisTickSize + this.defaults.yAxisTextPadding, y, this.defaults.axisYWidth - this.defaults.axisTickSize + this.defaults.yAxisTextPadding);
-        let tw = this.drawingContext.measureText(d.toFixed(2)).width;
+
+        // 大于7位,换行绘制
+        if (String(d).length <= 7) {
+          y -= 5
+          this.drawingContext.fillText(d, this.defaults.axisTickSize + this.defaults.yAxisTextPadding, y, this.defaults.axisYWidth - this.defaults.axisTickSize + this.defaults.yAxisTextPadding);
+        } else {
+          y -= 10
+          let text = String(d)
+          let [t0, t1] = text.split('.')
+
+          this.drawingContext.fillText(t0, this.defaults.axisTickSize + this.defaults.yAxisTextPadding, y, this.defaults.axisYWidth - this.defaults.axisTickSize + this.defaults.yAxisTextPadding);
+          this.drawingContext.fillText('.'.concat(t1), this.defaults.axisTickSize + this.defaults.yAxisTextPadding, y + 10, this.defaults.axisYWidth - this.defaults.axisTickSize + this.defaults.yAxisTextPadding);
+        }
+
+        let tw = this.drawingContext.measureText(d).width;
         maxTextWidth = maxTextWidth >= tw ? maxTextWidth : tw;
       });
       this.drawingContext.lineWidth = 1.2;
@@ -453,6 +523,11 @@ export default class StockHeatmap extends React.Component {
         );
         const h = this.yScale.bandwidth() - 2;
         const d = this.windowedData[this.windowedData.length - 1];
+
+        if (!d) {
+          return
+        }
+
         const maxBidAskVol = extractMaxVolume(d);
         this.drawingContext.save();
         this.drawingContext.translate(x, y);
@@ -496,7 +571,7 @@ export default class StockHeatmap extends React.Component {
   drawMainGraph = () => {
     this.drawingContext.save();
     if (this.xScale && this.yScale && this.bidAskScale && this.drawingContext !== null) {
-      const maxTradedVolume = extractMaxTradedVolume(this.windowedData);
+      const avgTradedVolume = extractAvgTradedVolume(this.windowedData);
       const xh2 = this.xScale.bandwidth() * 0.5;
       const yh2 = this.yScale.bandwidth() * 0.5;
       this.drawingContext.translate(this.defaults.borderPadding[3], this.defaults.borderPadding[0]);
@@ -534,11 +609,21 @@ export default class StockHeatmap extends React.Component {
             );
           });
         }
+      });
+
+      // draw trade size
+      this.circles = []
+      this.windowedData.map(d => {
+        const marketDepth = d.marketDepth;
+        const ask1 = marketDepth.sells[0];
+        const bid1 = marketDepth.buys[0];
+        const ts = d.ts;
+        const maxBidAskVol = extractMaxVolume(d);
 
-        // draw trade size
-        let trade_color = marketDepth.side == 'sell' ? this.defaults.sellColor : this.defaults.buyColor;
+        let trade_color = d3.color(marketDepth.side == 'sell' ? '#cc5040' : '#44c98b').rgb();
+        trade_color.opacity = 0.7;
         this.drawingContext.fillStyle = trade_color.toString();
-        const r = /*xh2*/ this.defaults.volumeCircleMaxRadius * (+marketDepth.lastTradedQty / maxTradedVolume) + 3;
+        const r = /*xh2*/ Math.min(this.defaults.volumeCircleMaxRadius * (+marketDepth.lastTradedQty / avgTradedVolume) + 3, 50);
         this.drawingContext.beginPath();
         this.drawingContext.arc(
           this.xScale(ts) + xh2,
@@ -552,7 +637,15 @@ export default class StockHeatmap extends React.Component {
         this.drawingContext.lineWidth = 1; // 设置线宽,可以根据需要调整
         this.drawingContext.strokeStyle = 'white'; // 设置描边颜色为白色
         this.drawingContext.stroke(); // 执行描边操作
-      });
+
+        // 事件
+        this.circles.push({
+          x: this.xScale(ts) + xh2,
+          y: this.yScale(+marketDepth.lastTradedPrice) + yh2,
+          radius: r,
+          data: d  // 存储与圆相关的数据,便于在事件处理时使用
+        });
+      })
 
       // draw buy line path
       let buy_line_color = d3.color(this.defaults.buyColor).rgb();
@@ -650,7 +743,7 @@ export default class StockHeatmap extends React.Component {
       // 延迟日志
       if (this.windowedData.length > 1) {
         let last = this.windowedData[this.windowedData.length - 1]
-        console.log(new Date().getTime() - last.time, last)
+        console.log(new Date().getTime() - last.time, last.marketDepth.lastTradedPrice, last.marketDepth.lastTradedQty)
       }
 
       this.windowPosition = position;

+ 23 - 12
src/utils.js

@@ -1,6 +1,6 @@
 /**
  * Extract buy/sell bid prices from data points
- * @param {any[]} data 
+ * @param {any[]} data
  * @returns {number[]}
  */
 export const extractBidPrices = (data) => {
@@ -18,22 +18,24 @@ export const extractBidPrices = (data) => {
 
 /**
  * Extract buy/sell bid volumnes from a single data point
- * @param {any} data 
+ * @param {any} data
  * @returns {number[]}
  */
 export const extractBidVolumes = (data) => {
-  const marketDepth = data.marketDepth;
-  if (marketDepth) {
-    let buys = marketDepth.buys.map(b => +b.qty);
-    let sells = marketDepth.sells.map(b => +b.qty);
-    return buys.concat(sells);
+  if (data) {
+    const marketDepth = data.marketDepth;
+    if (marketDepth) {
+      let buys = marketDepth.buys.map(b => +b.qty);
+      let sells = marketDepth.sells.map(b => +b.qty);
+      return buys.concat(sells);
+    }
   }
   else return [];
 }
 
 /**
  * Extract max volume (buy or sell) for a data point
- * @param {any} data 
+ * @param {any} data
  * @returns {number}
  */
 export const extractMaxVolume = (data) => {
@@ -44,7 +46,7 @@ export const extractMaxVolume = (data) => {
 
 /**
  * Extract max traded volume within a given set of datapoints
- * @param {any[]} data 
+ * @param {any[]} data
  * @returns {number}
  */
 export const extractMaxTradedVolume = (data) => {
@@ -56,9 +58,18 @@ export const extractMaxTradedVolume = (data) => {
   else return 1;
 }
 
+export const extractAvgTradedVolume = (data) => {
+  let vols = data.map(d => {
+    if (d.marketDepth) return +d.marketDepth.lastTradedQty;
+    else return 0;
+  });
+  if (vols.length > 0) return vols.reduce((acc, curr) => acc + curr, 0) / vols.length;
+  else return 1;
+}
+
 /**
  * Format zoom scale time
- * @param {number} seconds 
+ * @param {number} seconds
  */
 export const zoomTimeFormat = (seconds, decimal) => {
   if(!decimal) decimal = 2;
@@ -66,11 +77,11 @@ export const zoomTimeFormat = (seconds, decimal) => {
     if(seconds > 3599) {
       let hrs = seconds/3600;
       return `${hrs.toFixed(decimal)} hour${hrs>1?'s':''}`;
-    } 
+    }
     else {
       let mins = seconds/60;
       return `${mins.toFixed(decimal)} minute${mins>1?'s':''}`;
     }
   }
   else return `${seconds} second${seconds>1?'s':''}`;
-}
+}