|
|
@@ -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]
|