const TradingCup = () => {
  const cupWorkerRef = useRef<Worker>();
  const orderFeedWorkerRef = useRef<Worker>();
 const symbol = useSelector((state: AppState) => state.screenerSlice.symbol);

 useEffect(() => {
    const channel = new MessageChannel();
    cupWorkerRef.current?.postMessage({type: "set_port", port: channel.port1}, [channel.port1]);
    orderFeedWorkerRef.current?.postMessage({type: "set_port", port: channel.port2}, [channel.port2]);
 
}, []);
 return <Stack direction="row" height="100%" position="relative" >
    <div className={styles.OfferFeed}>
      <OrderFeed
        symbol={symbol}
        workerRef={orderFeedWorkerRef}
 />
    </div>
    <div className={styles.Cup}>
      <Cup
        symbol={symbol}
        workerRef={cupWorkerRef}
       />
    </div>
  </Stack>;
};

export default TradingCup;


const Cup = ({workerRef, symbol, 
}: CupProps) => {
 const cupParams = useSelector((state: AppState) => state.cupSlice);
  const [dpiScale, setDpiScale] = useState(Math.ceil(window.devicePixelRatio));
  const [canvasSize, setCanvasSize] = useState<CanvasSize>({height: 0, width: 0});
  const containerRef = useRef<HTMLDivElement|null>(null);
  const canvasRef = useRef<HTMLCanvasElement|null>(null);
  const [zoom, setZoom] = useState(1);

const size = useComponentResizeListener(canvasRef);
const dispatch = useDispatch();
  const {diaryToken} = useAuthContext();
  const {selectedSingleApiKey} = useApiKeyProvider();
  const {enqueueSnackbar} = useSnackbar();

  const cupSubscribe = useCallback(async(pair: string, zoom: number) => {
    workerRef.current?.postMessage(JSON.stringify({type: "subscribe", pair, zoom}));
  }, []);

  const cupUnsubscribe = useCallback(async(pair: string) => {
    workerRef.current?.postMessage(JSON.stringify({type: "unsubscribe", pair}));
  }, []);

  const wheelHandler = (e: WheelEvent) => {
    e.preventDefault();
    workerRef.current?.postMessage(JSON.stringify({type: e.deltaY < 0 ? "camera_up" : "camera_down"}));
  };
  const zoomAdd = () => {
    let newZoom;

    if (zoom >= 1 && zoom < 10) {
      newZoom = zoom + 1;
    } else if (zoom >= 10 && zoom < 30) {
      newZoom = zoom + 5;
    }

    setZoom(newZoom);
  };
const zoomSub = () => {
    let newZoom;

    if (zoom > 1 && zoom <= 10) {
      newZoom = zoom - 1;
    } else if (zoom > 10 && zoom <= 30) {
      newZoom = zoom - 5;
    }

    setZoom(newZoom);
  };
useEffect(() => {
    workerRef.current = new Worker(new URL("/workers/cup-builder.ts", import.meta.url));
    canvasRef.current?.addEventListener("wheel", wheelHandler, {passive: false});
  
 return () => {
      workerRef.current?.terminate();
      canvasRef.current?.removeEventListener("wheel", wheelHandler);
    };
  }, []);

  useEffect(() => {
    if (!workerRef.current) return;
    let animationFrameId: number|null = null;

   if (event?.data?.type === "update_cup") {
        if (null !== animationFrameId) {
          cancelAnimationFrame(animationFrameId);
        }

        animationFrameId = requestAnimationFrame(() => {
          const data = event.data as UpdateCupEvent;

          const context = canvasRef.current?.getContext("2d");
          const zoomedTickSize = data.priceStep * data.aggregation;

          if (context) {
            const rowsOnScreenCount = cupTools.getRowsCountOnScreen(
              canvasSize.height,
              cupOptions().cell.defaultHeight * dpiScale,
            );
            const realCellHeight = parseInt((canvasSize.height / rowsOnScreenCount).toFixed(0));

  if (data.rowsCount !== rowsOnScreenCount) {
              workerRef.current?.postMessage(JSON.stringify({type: "change_rows_count", value: rowsOnScreenCount}));
            }

            cupDrawer.clear(context, canvasSize);

            if (cupParams.rowCount !== rowsOnScreenCount
              || cupParams.cellHeight !== realCellHeight
              || cupParams.aggregation !== data.aggregation
            ) {
              dispatch(setCupParams({
                aggregation: data.aggregation,
                rowCount: rowsOnScreenCount,
                cellHeight: realCellHeight,
                pricePrecision: data.pricePrecision,
                priceStep: data.priceStep,
                quantityPrecision: data.quantityPrecision,
              }));
            }
  if (data.camera !== 0) {
              cupDrawer.draw(
                context,
                canvasSize,
                dpiScale,
                data.bestBidPrice,
                data.bestAskPrice,
                data.maxVolume,
                data.pricePrecision,
                data.quantityPrecision,
                data.priceStep,
                data.aggregation,
                rowsOnScreenCount,
                data.camera,
                realCellHeight,
                {
                  buy: parseInt((Math.floor(data.bestBidPrice / zoomedTickSize) * zoomedTickSize).toFixed(0)),
                  sell: parseInt((Math.ceil(data.bestAskPrice / zoomedTickSize) * zoomedTickSize).toFixed(0)),
                },
                darkMode,
                data.volumeAsDollars,
                data.cup,
              );
            }
          }
        });
      }
    };

    return () => {
      if (null !== animationFrameId) {
        cancelAnimationFrame(animationFrameId);
      }
    };
  }, [workerRef.current, canvasSize, darkMode, dpiScale, isLoaded, quantity]);

useEffect(() => {
    cupSubscribe(symbol.toUpperCase(), zoom);

    return () => {
      cupUnsubscribe(symbol.toUpperCase());
    };
  }, [symbol, zoom]);
 return <div ref={containerRef} className={styles.canvasWrapper}>
 
 <canvas
      ref={canvasRef}
      className={[styles.canvas, isLoaded ? "" : styles.loading].join(" ")}
      width={canvasSize?.width}
      height={canvasSize?.height}
    />
  </div>;
};
export default Cup;

import {CupItem} from "../hooks/rustWsServer";
import {MessagePort} from "worker_threads";

let cup: {[key: number]: CupItem} = {};

let publisherIntervalId: any = null;
let wsConnection: WebSocket|null = null;

let cameras: {[key: string]: number} = {};

let rowsCount: number = 60;
let quantityDivider = 1;
let priceDivider = 1;
let diffModifier = 2;
const connectToWs = (callback: () => void) => {
  if (wsConnection) {
    callback();

    return;
  }

  wsConnection = new WebSocket(`${process.env.NEXT_PUBLIC_RUST_WS_SERVER}`);

  wsConnection.onopen = () => {
    if (null !== wsConnection) {
      callback();
    }
  };

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

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

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

    const exchangeInitial = data?.commands?.find((item: any) => "ExchangeInitial" in item);
    if (exchangeInitial) {
      cup = exchangeInitial.ExchangeInitial.rows;
      pricePrecision = exchangeInitial.ExchangeInitial.params.pricePrecision;
      priceStep = exchangeInitial.ExchangeInitial.params.priceStep;
      quantityPrecision = exchangeInitial.ExchangeInitial.params.quantityPrecision;
      quantityDivider = Math.pow(10, quantityPrecision);
      priceDivider = Math.pow(10, pricePrecision);
    }
  };
};
const publish = () => {
  if (!isSubscribed) return;

  if (!cameras[pair]) {
    cameras[pair] = 0;
  }

  const zoomedTickSize = priceStep * aggregation;
 const rows: {[key: number]: CupItem} = {};
diffModifier
 
diffModifier
  for (let index = 0; index <= rowsCount; index++) {
    const microPrice = startMicroPrice - index * zoomedTickSize;

    if (microPrice < 0) continue;

    rows[microPrice] = cup[microPrice] || {};
maxVolume
Math.max(maxVolume, (ask || bid || 0) / quantityDivider
);

  port?.postMessage({type: "set_camera", value: cameras[pair]});

  postMessage({
    type: "update_cup",
    cup: rows,
    camera: cameras[pair],
    aggregation,
    bestBidPrice,
    bestAskPrice,
    pricePrecision,
    priceStep,
    quantityPrecision,
    rowsCount,
    maxVolume: volumeIsFixed
      ? fixedMaxVolume
      : maxVolume,
    volumeAsDollars,
  });
};
const publisherStart = () => {
  if (publisherIntervalId) {
    clearInterval(publisherIntervalId);
  }

  publisherIntervalId = setInterval(publish, publisherTimeoutInMs);
};

const publisherStop = () => {
  if (publisherIntervalId) {
    clearInterval(publisherIntervalId);
  }
};

onmessage = (event: MessageEvent<any>) => {
  const data = "string" === typeof event.data ? JSON.parse(`${event.data}`) : event.data;

  if (data && data?.type === "subscribe") {
    pair = data.pair;
    aggregation = data.zoom;
    isSubscribed = true;
    cameras[pair] = 0;
    maxVolume = 0;
    cup = {};
    publisherStart();

    if (wsConnection?.readyState === 3) {
      wsConnection = null;
    }

    connectToWs(() => {
      if (null === wsConnection) return;

      wsConnection.send(JSON.stringify({
        "commands": [
          {
            commandType: "SUBSCRIBE_SYMBOL",
            exchange: `FT:${pair}`,
            aggregation: aggregation,
          },
        ],
      }));
    });
  }
  if (data && data?.type === "unsubscribe") {
    isSubscribed = false;
    cameras[pair] = 0;
    pair = "";
    cup = {};
    publisherStop();

    if (null !== wsConnection) {
      wsConnection.send(JSON.stringify({
        "commands": [
          {
            commandType: "UNSUBSCRIBE_SYMBOL",
            exchange: `FT:${data.pair}`,
          },
        ],
      }));
    }
  }

  if (data && data?.type === "change_publisher_timeout") {
    publisherTimeoutInMs = data.value;
    publisherStart();
  }
  if (data && data?.type === "set_port") {
    port = data.port;
  }
};

export {};

Нужно создать новую табличку, где отображать эти данные. Сделать три колонки (монета, цена, кол-во).
 wsConnection.send(JSON.stringify({
        "commands": [
          {
            commandType: "SUBSCRIBE_BIG_ORDERS",
            exchange: `FT:${symbol}`,
          },
        ],
      }));
  const bigOrder = data.commands.find((item: any) => "undefined" !== typeof item.BigOrder);
   const tradeSymbol = bigOrder.BigOrder.exchange.replace("FT:", "").toUpperCase();

      if (tradeSymbol === symbol) {
        price = bigOrder.BigOrder.price;
        quantity = bigOrder.BigOrder.quantity;

 }
    }