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;

}
}


создай отдельный компонент, отдельный воркер