Ver Fonte

合并算法搞定。

skyffire há 1 ano atrás
pai
commit
6accc57186
1 ficheiros alterados com 199 adições e 6 exclusões
  1. 199 6
      src/index.js

+ 199 - 6
src/index.js

@@ -40,13 +40,23 @@ export default class StockHeatmap extends React.Component {
   windowedData = [];
   windowLength = 20;
   windowPosition = 0;
+  isMerged = false;
 
   mouse = {
     x: 0,
     y: 0
   }
 
-  circles = []
+  circles = [];
+
+  maxBidAskVolume = 0;
+
+  orderbookColors = [
+    '#086892',
+    '#2f9dd2',
+    '#fffc19',
+    '#ff9a01',
+  ];
 
   /** Default Theme colors and dimensions */
   defaults = {
@@ -168,7 +178,11 @@ export default class StockHeatmap extends React.Component {
     // 其他事件处理
     if (this.isMouseDown && this.xScale) {
       // Mouse drag, scroll the time series,距离上一次移动视野的鼠标拖拽距离
-      const dragX = e.clientX - this.mouse.x;
+      let dragX = e.clientX - this.mouse.x;
+      // 合并数据,拖动范围要增加
+      if (this.isMerged) {
+        dragX = dragX * 10;
+      }
       const moveDataPointsCount = Math.floor(Math.abs(dragX) / this.xScale.bandwidth());
       if (moveDataPointsCount > 0) this.mouse.x = e.x
       // const moveDataPointDirection = dragLength >= 0 ? 'right' : 'left';
@@ -603,6 +617,7 @@ export default class StockHeatmap extends React.Component {
       const maxBidAskVolume = extractMaxAskBidVolume(this.windowedData);
       const xh2 = this.xScale.bandwidth() * 0.5;
       const yh2 = this.yScale.bandwidth() * 0.5;
+      const panel = this
       this.drawingContext.translate(this.defaults.borderPadding[3], this.defaults.borderPadding[0]);
       this.windowedData.map(d => {
         const marketDepth = d.marketDepth;
@@ -611,9 +626,12 @@ export default class StockHeatmap extends React.Component {
         const ts = d.ts;
         // draw buys
         if (marketDepth.buys && marketDepth.buys.length > 0) {
-          let color = d3.color('#1a506d').rgb();
           marketDepth.buys.map(buy => {
-            color.opacity = buy.qty / maxBidAskVolume;
+            let rate = buy.qty / maxBidAskVolume;
+            let colorIndex = Math.min(parseInt(rate / 0.25), panel.orderbookColors.length - 1);
+            let color = d3.color(panel.orderbookColors[colorIndex]).rgb()
+
+            color.opacity = (rate % 0.25) / 0.25;
             this.drawingContext.fillStyle = color.toString();
             this.drawingContext.fillRect(
               this.xScale(ts),
@@ -625,9 +643,12 @@ export default class StockHeatmap extends React.Component {
         }
         // draw sells
         if (marketDepth.sells && marketDepth.sells.length > 0) {
-          let color = d3.color('#1a506d').rgb();
           marketDepth.sells.map(sell => {
-            color.opacity = sell.qty / maxBidAskVolume;
+            let rate = sell.qty / maxBidAskVolume;
+            let colorIndex = Math.min(parseInt(rate / 0.25), panel.orderbookColors.length - 1);
+            let color = d3.color(panel.orderbookColors[colorIndex]).rgb()
+
+            color.opacity = (rate % 0.25) / 0.25;
             this.drawingContext.fillStyle = color.toString();
             this.drawingContext.fillRect(
               this.xScale(ts),
@@ -763,6 +784,171 @@ export default class StockHeatmap extends React.Component {
     }
   }
 
+  isOrderBooksEquals = (orderbooks1, orderbooks2) => {
+    for (let i = 0; i < orderbooks1.length; i++) {
+      let orderbook1 = orderbooks1[i]
+      let orderbook2 = orderbooks2[i]
+
+      // 如果d1有,d2没有,那证明不相等
+      if (!orderbook2) return false
+      if (orderbook1.rate !== orderbook2.rate) return false
+
+      let r1 = (orderbook1.qty / this.maxBidAskVolume);
+      let o1ColorIndex = Math.min(parseInt(r1 / 0.25), this.orderbookColors.length - 1);
+
+      let r2 = (orderbook2.qty / this.maxBidAskVolume);
+      let o2ColorIndex = Math.min(parseInt(r2 / 0.25), this.orderbookColors.length - 1);
+      if (o1ColorIndex !== o2ColorIndex) return false
+    }
+
+    return true
+  }
+
+  isDepthEquals = (d1, d2) => {
+    // buys
+    if (!this.isOrderBooksEquals(d1.buys, d2.buys)) return false
+    // sells
+    if (!this.isOrderBooksEquals(d1.sells, d2.sells)) return false
+
+    return true
+  }
+
+  mergeSnapshots = (snapshots) => {
+    // 初始化合并后的结构
+    const merged = {
+      marketDepth: {
+        avgPrice: 0,
+        buyOrderVolume: 0,
+        sellOrderVolume: 0
+      },
+      buys: [],
+      sells: [],
+      lastTradeQty: 0,
+      lastTradePrice: 0,
+      close: 0,
+      high: 0,
+      low: 0,
+      open: 0,
+      priceChangeAmt: 0,
+      priceChangePct: "0",
+      pendingOrders: [],
+      time: "",
+      tradingsymbol: "",
+      ts: ""
+    };
+
+    // 初始化计数器
+    let totalAvgPrice = 0;
+    let totalBuyOrderVolume = 0;
+    let totalSellOrderVolume = 0;
+    let totalLastTradePrice = 0;
+    let totalSnapshots = snapshots.length;
+
+    // 初始化买卖数组
+    let buySums = Array(5).fill({ rate: 0, orders: 0, qty: 0 });
+    let sellSums = Array(5).fill({ rate: 0, orders: 0, qty: 0 });
+
+    // 初始化交易数量
+    let totalLastTradeQtyBuy = 0;
+    let totalLastTradeQtySell = 0;
+
+    // 记录第一个快照的时间和交易符号
+    merged.time = snapshots[0].time;
+    merged.tradingsymbol = snapshots[0].tradingsymbol;
+    merged.ts = snapshots[0].ts;
+
+    // 遍历所有快照数据
+    snapshots.forEach(snapshot => {
+      // 累加市场深度数据
+      totalAvgPrice += snapshot.marketDepth.avgPrice;
+      totalBuyOrderVolume += snapshot.marketDepth.buyOrderVolume;
+      totalSellOrderVolume += snapshot.marketDepth.sellOrderVolume;
+
+      // 合并买单和卖单
+      snapshot.buys.forEach((buy, index) => {
+        buySums[index] = {
+          rate: buy.rate,
+          orders: buySums[index].orders + buy.orders,
+          qty: buySums[index].qty + buy.qty
+        };
+      });
+
+      snapshot.sells.forEach((sell, index) => {
+        sellSums[index] = {
+          rate: sell.rate,
+          orders: sellSums[index].orders + sell.orders,
+          qty: sellSums[index].qty + sell.qty
+        };
+      });
+
+      // 合并最后交易的数量和价格
+      if (snapshot.side === 'buy') {
+        totalLastTradeQtyBuy += snapshot.lastTradedQty;
+      } else if (snapshot.side === 'sell') {
+        totalLastTradeQtySell += snapshot.lastTradedQty;
+      }
+      totalLastTradePrice += snapshot.lastTradedPrice;
+
+      // 合并其他字段
+      merged.close = snapshot.close;
+      merged.high = snapshot.high;
+      merged.low = snapshot.low;
+      merged.open = snapshot.open;
+      merged.priceChangeAmt = snapshot.priceChangeAmt;
+      merged.priceChangePct = snapshot.priceChangePct;
+    });
+
+    // 计算平均市场深度数据
+    merged.marketDepth.avgPrice = totalAvgPrice / totalSnapshots;
+    merged.marketDepth.buyOrderVolume = totalBuyOrderVolume / totalSnapshots;
+    merged.marketDepth.sellOrderVolume = totalSellOrderVolume / totalSnapshots;
+
+    // 计算平均买单和卖单
+    merged.buys = buySums.map(buy => ({
+      rate: buy.rate,
+      orders: buy.orders / totalSnapshots,
+      qty: buy.qty / totalSnapshots
+    }));
+    merged.sells = sellSums.map(sell => ({
+      rate: sell.rate,
+      orders: sell.orders / totalSnapshots,
+      qty: sell.qty / totalSnapshots
+    }));
+
+    // 计算最终的最后交易的数量和价格
+    let lastTradeQtyDiff = totalLastTradeQtyBuy - totalLastTradeQtySell;
+    merged.lastTradeQty = Math.abs(lastTradeQtyDiff);
+    merged.side = lastTradeQtyDiff > 0 ? 'buy' : 'sell';
+    merged.lastTradePrice = totalLastTradePrice / totalSnapshots;
+
+    return merged;
+  }
+
+  mergeWindowedData = () => {
+    let windowedData = this.windowedData;
+    this.maxBidAskVolume = extractMaxAskBidVolume(this.windowedData);
+    let mergedWindowedData = [];
+    let panel = this
+
+    let prevData = undefined;
+    let pendingDataList = [];
+    windowedData.map((d) => {
+      if (!prevData) {
+        mergedWindowedData.push(d)
+        prevData = d
+      } else if (!panel.isDepthEquals(prevData.marketDepth, d.marketDepth)) {
+        mergedWindowedData.push(d)
+        prevData = d
+
+        pendingDataList = [];
+      }
+
+      pendingDataList.push(d)
+    });
+
+    return mergedWindowedData;
+  }
+
   /**
    * Move the position of data window within the main data.
    * @param {number} position The target position of the window to be moved to.
@@ -772,6 +958,13 @@ export default class StockHeatmap extends React.Component {
       // move position only if within valid range
       this.windowedData = this.data.slice(position, position + this.windowLength + 1);
 
+      if (this.windowedData.length > 200) {
+        this.windowedData = this.mergeWindowedData();
+        this.isMerged = true;
+      } else {
+        this.isMerged = false;
+      }
+
       // 延迟日志
       if (this.windowedData.length > 1) {
         let last = this.windowedData[this.windowedData.length - 1]