import {useCallback, useEffect, useState} from "react";
import {ReadyState} from "../enums/readyState";

type CupItem = {
  futures_price_micro: number;
  quantity: number;
  spot_quantity: number;
  side: string;
};

export interface BestMicroPrice {
  buy: number;
  sell: number;
}

export function useRustWsServer() {
  const [connection, setConnection] = useState<WebSocket|null>(null);
  const [readyState, setReadyState] = useState(0);
  const [cup, setCup] = useState<Array<CupItem>>([]);
  const [bestMicroPrice, setBestMicroPrice] = useState<BestMicroPrice|null>(null);
  const [maxVolume, setMaxVolume] = useState(1);

  function splitCupSides(rawData: {[key: number]: CupItem}): Array<CupItem> {
    const sellRecords = [];
    const buyRecords = [];
    let max = 0;

    for (const value of Object.values(rawData)) {
      if (value.side === "Buy") {
        buyRecords.push(value);
      } else if (value.side === "Sell") {
        sellRecords.push(value);
      }

      if (value.quantity > max) {
        max = value.quantity;
      }
    }

    sellRecords.sort((a, b) => {
      return b.futures_price_micro - a.futures_price_micro;
    });

    buyRecords.sort((a, b) => {
      return b.futures_price_micro - a.futures_price_micro;
    });

    setMaxVolume(max);

    return [...sellRecords, ...buyRecords];
  }

  const cupSubscribe = useCallback((symbol: string, camera: number, zoom: number, rowCount: number) => {
    if (null === connection || readyState !== ReadyState.OPEN) return;

    connection.send(JSON.stringify({
      "commands": [
        {
          commandType: "SUBSCRIBE_SYMBOL",
          symbol,
          camera: Math.round(camera / zoom) * zoom,
          zoom,
          rowCount,
        },
      ],
    }));
  }, [readyState]);

  const cupUnsubscribe = useCallback((symbol: string) => {
    if (null === connection || readyState !== ReadyState.OPEN) return;

    connection.send(JSON.stringify({
      "commands": [
        {
          commandType: "UNSUBSCRIBE_SYMBOL",
          symbol,
        },
      ],
    }));
  }, [readyState]);

  useEffect(() => {
    const url = process.env.NEXT_PUBLIC_RUST_WS_SERVER;

    if (url) {
      const ws = new WebSocket(url);

      setConnection(ws);
    }
  }, []);

  useEffect(() => {
    if (null !== connection) {

      connection.onmessage = (message: MessageEvent) => {
        if (!message.data) return;

        const data = JSON.parse(message.data);

        if (!data?.commands || data.commands.length === 0) return;

        const domUpdate = data.commands.find((item: any) => "undefined" !== typeof item.SymbolDomUpdate);

        if (!domUpdate) return;

        setCup(splitCupSides(domUpdate.SymbolDomUpdate.dom_rows));
        setBestMicroPrice({
          buy: domUpdate.SymbolDomUpdate.best_prices_futures.best_ask_micro,
          sell: domUpdate.SymbolDomUpdate.best_prices_futures.best_bid_micro,
        });
      };

      connection.onopen = () => {
        setReadyState(ReadyState.OPEN);
      };

      connection.onclose = () => {
        setReadyState(ReadyState.CLOSED);
      };
    }
  }, [connection]);

  return {
    readyState,
    cupSubscribe,
    cupUnsubscribe,
    cup,
    maxVolume,
    bestMicroPrice,
  };
}

import {BestMicroPrice, useRustWsServer} from "../../hooks/rustWsServer";
import {createContext, Reducer, useEffect, useReducer, useRef, useState} from "react";
import CupDrawer from "../CupDrawer/CupDrawer";
import {IconButton} from "@mui/material";
import {AddRounded, RemoveRounded} from "@mui/icons-material";
import {ReadyState} from "../../enums/readyState";
import {useSelector} from "react-redux";
import {AppState} from "../../store/store";

interface CupConfigSubscription {
  pair: string | null;
  zoom: number;
  camera: number;
  rowCount: number;
}

export const CupControlsContext = createContext<{
  cupControlsState: any;
  cupControlsDispatcher: any;
}>({
  cupControlsState: null,
  cupControlsDispatcher: null,
});

const TradingCup = () => {
  const symbol = useSelector((state: AppState) => state.screenerSlice.symbol);
  const {cup, bestMicroPrice, maxVolume, readyState, cupSubscribe, cupUnsubscribe} = useRustWsServer();
  const precision = useSelector((state: AppState) => state.binancePrecision.futures[symbol.toUpperCase()]);
  const tickSize = useSelector((state: AppState) => state.binanceTickSize.futures[symbol.toUpperCase()]);
  const [cupConfig, setCupConfig] = useState<CupConfigSubscription>({
    pair: null,
    zoom: 10,
    camera: 0,
    rowCount: 40,
  });

  useEffect(() => {
    if (symbol) {
      setCupConfig({
        ...cupConfig,
        pair: symbol.toUpperCase(),
        camera: 0,
      });
    }
  }, [symbol]);

  useEffect(() => {
    if (readyState === ReadyState.OPEN) {
      if (null !== cupConfig.pair) {
        cupSubscribe(
          cupConfig.pair,
          cupConfig.camera,
          cupConfig.zoom,
          cupConfig.rowCount,
        );
      }
    }

    return () => {
      if (cupConfig.pair != null) {
        cupUnsubscribe(cupConfig.pair);
      }
    };
  }, [
    cupConfig.pair,
    cupConfig.camera,
    cupConfig.zoom,
    cupConfig.rowCount,
    readyState,
  ]);
  return (
    <>
    </>
  );
};

export default TradingCup;


import {each, get, map, reduce, range, clamp, reverse} from 'lodash'
import {ESide} from "../../interfaces/interfaces";
import {abbreviateNumber, blendColors, blendRGBColors, getRatio, shadeColor} from "../../utils/utils";
import {
    bubbleSize, clusterBg,
    clusterGreen,
    clusterRed,
    clustersCountUI,
    deepGreen,
    deepRed,
    lightGreen,
    lightRed,
    maxClusterWidth,
    minuteMs,
    rowHeight,
    timeFrame,
    visibleClustersCount
} from "../../constants/consts";


export default class ClustersClientControllers {
    xWidthInMs = timeFrame * clustersCountUI
    DOMBorderOffset = 0
    abnormalDensities = 200
    clusters = []
    currentMin = 0
    tempCluster = {}
    tempCurrentMin
    totals = []
    tempTotal = {}

    root: ClientController
    canvasHeight = 0
    canvasWidth = 0
	tradesArr: any = []
	public bestPrices: any = null
	clustersCtx
	orderFeedCtx
	public cameraPrice = null
	public zoom = 10
	clusterCellWidth
	virtualServerTime = null
    tradesFilterBySymbol = {}

	constructor(root) {
		this.root = root
		window['clusters'] = this
        this.restoreClusterSettings()
	}

    renderTrades = () => {
        this.clearOrderFeed();
        reduce(this.tradesArr, (prev, cur, index) => {
            this.renderTrade(prev, cur, this.tradesArr.length - (index as any))
            prev = cur
            console.log(prev);
            
            return prev
        })

    }
    clearOrderFeed = () => {
        this.orderFeedCtx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
    }

    renderTrade = (prev, item, index) => {
        //const anomalyQty = this.root.instruments[this.root.selectedSymbol].anomalies.anomaly_qty;

    console.log(item); // 
price_float
: 
0.4139
price_micro
: 
4139000
quantity
: 
6
side
: 
"Buy"
time
: 
1685607036920
        

        //if (size < 1) return;
        const ctx = this.orderFeedCtx
        let xPos = (this.canvasWidth - (index * (bubbleSize * 1.5))) - bubbleSize;
        const offsetFromTop = this.root.tradingDriverController.upperPrice - item.price_micro;
        const y = ((offsetFromTop / this.root.tradingDriverController.getZoomedStepMicro()) - 1) * rowHeight
        const label = abbreviateNumber(item.quantity * item.price_float)
        const {width: textWidth} = ctx.measureText(label);
        const itemUsdt = item.quantity * item.price_float;
        const tradeFilter = this.getTradeFilterBySymbol(this.getSymbol())
        const maxUsdtBubbleAmount = tradeFilter * 30;
        const maxPixelBubbleAmount = 35;
        const realBubbleSize = (itemUsdt / maxUsdtBubbleAmount) *  maxPixelBubbleAmount
        const size = clamp(realBubbleSize, (textWidth/2)+3, maxPixelBubbleAmount)

        const bubbleX = xPos;
        const bubbleY = y + 8;
        ctx.beginPath();
        let bigRatio = (realBubbleSize / maxPixelBubbleAmount) / 3;
        bigRatio = bigRatio > 0.95 ? 0.95 : bigRatio;
        ctx.fillStyle = item.side === "Sell" ? deepGreen.lighten(bigRatio).toString() : deepRed.lighten(bigRatio).toString()
        ctx.strokeStyle = 'black';
        ctx.arc(xPos, bubbleY, size, 0, 2 * Math.PI)
        ctx.fill();
        ctx.stroke();

        ctx.fillStyle = "#FFFFFF"
		ctx.fillText(label, bubbleX - (textWidth / 2), (bubbleY + (rowHeight / 2)) - 2)

    }



1. В компоненте TradingCup  запрашивается cup из useRustWsServer, сделать отрисовку канвас как в методах renderTrade renderTrades .
2. renderTrade renderTrades то другой  немного компонент с немного другими данными, нужно использовать его в TradingCup  только с нашими данными type CupItem = {
  futures_price_micro: number;
  quantity: number;
  spot_quantity: number;
  side: string;
};

3. В методе renderTrade() показатели quantity и price_float перемножаются, чтобы получить объем в $. Нам это не нужно, будем выводить только quantity.
Нужно все отрисовать в канвасе И использовать тоже самое соединение, что установлено в rustWsServer.ts 
обязательно везде typescript