const TradingCup = () => {
const cupWorkerRef = useRef<Worker>();
const orderFeedWorkerRef = useRef<Worker>();
const symbol = useSelector((state: AppState) => state.screenerSlice.symbol);
const [settings, setSettings] = useState<SettingsCup>({
timeout: “40”,
aggregation: true,
maxVolume: window.localStorage.getItem(cup-volume-${symbol}) || “1000”,
isFixedVolume: null !== window.localStorage.getItem(cup-volume-${symbol}),
minQuantity: window.localStorage.getItem(order-feed-min-qty-${symbol}) || “0”,
dollarsVolume: null !== window.localStorage.getItem(cup-dollars-volume-${symbol}),
});
const [showSettings, setShowSettings] = useState(false);

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]);
}, []);

useEffect(() => {
setSettings({
…settings,
maxVolume: window.localStorage.getItem(cup-volume-${symbol}) || “1000”,
isFixedVolume: null !== window.localStorage.getItem(cup-volume-${symbol}),
minQuantity: window.localStorage.getItem(order-feed-min-qty-${symbol}) || “0”,
dollarsVolume: null !== window.localStorage.getItem(cup-dollars-volume-${symbol}),
});
}, [symbol]);

return <Stack direction=“row” height=“100%”>
<div className={styles.OfferFeed}>
<OrderFeed
symbol={symbol}
workerRef={orderFeedWorkerRef}
settings={settings}
/>
</div>
<div className={styles.Cup}>
<Cup
symbol={symbol}
workerRef={cupWorkerRef}
settings={settings}
setShowSettings={setShowSettings}
/>
</div>
</Stack>;
};

export default TradingCup;

const Cup = ({workerRef, symbol, settings, setShowSettings}: 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 [isLoaded, setIsLoaded] = useState(false);
const size = useComponentResizeListener(canvasRef);
const darkMode = useTheme().palette.mode === “dark”;
const dispatch = useDispatch();

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 sendSettings = () => {
workerRef.current?.postMessage(JSON.stringify({
type: “change_max_volume”,
isFixed: settings.isFixedVolume,
maxVolume: parseInt(settings.maxVolume || “0”) < 0 ? 1 : parseInt(settings.maxVolume),
}));
workerRef.current?.postMessage(JSON.stringify({
type: “change_publisher_timeout”,
value: parseInt(settings.timeout || “0”) < 40 ? 40 : parseInt(settings.timeout),
}));
};

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

useEffect(() => {
workerRef.current = new Worker(new URL(“/workers/cup-builder.ts”, import.meta.url));
canvasRef.current?.addEventListener(“wheel”, wheelHandler, {passive: false});
sendSettings();

return () => {
workerRef.current?.terminate();
canvasRef.current?.removeEventListener(“wheel”, wheelHandler);
};
}, []);

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

workerRef.current.onmessage = (event: MessageEvent<{
type: string,
camera: number,
aggregation: number,
bestBidPrice: number,
bestAskPrice: number,
maxVolume: number,
pricePrecision: number,
quantityPrecision: number,
priceStep: number,
cup: {[key: number]: CupItem},
rowsCount: number,
}>) => {
useEffect(() => {
cupSubscribe(symbol.toUpperCase(), zoom);

return () => {
cupUnsubscribe(symbol.toUpperCase());
};
}, [symbol, zoom]);

useEffect(() => {
if (settings.isFixedVolume) {
window.localStorage.setItem(cup-volume-${symbol}, settings.maxVolume);
} else {
window.localStorage.removeItem(cup-volume-${symbol});
}

sendSettings();
}, [settings]);

return <div ref={containerRef} className={styles.canvasWrapper}>
<div className={styles.controls}>
<IconButton onClick={zoomSub} size=“small” disabled={!isLoaded}>
<RemoveRounded />
</IconButton>
<span>x{zoom}</span>
<IconButton onClick={zoomAdd} size=“small” disabled={!isLoaded}>
<AddRounded />
</IconButton>
<IconButton onClick={() => setShowSettings(true)} size=“small”>
<SettingsRounded
// @ts-ignore
fontSize=“xx-small”
/>
</IconButton>
</div>
<canvas
ref={canvasRef}
className={[styles.canvas, isLoaded ? “” : styles.loading].join(" ")}
width={canvasSize?.width}
height={canvasSize?.height}
/>
</div>;
};

export default Cup;

вот код!!
Когда dollarsVolume true - перемножать quantity на price при отрисовке строк стакана. И в воркере workerRef максимальный объем определять тоже путем перемножения цены на кол-во.
Сделай блин это