Feature/151 orderbook (#266)
* [#151] Add orderbook * [#151] Add orderbook components * [#151] Add market-depth data provider * [#151] Add orderbook manager * [#151] handle market depth updates in orderbook manager * [#151] Fix PageQueryContainer usage of AsyncRenderer * [#151] Move data handling logig out from orderbook manager * [#151] Use mocked data, fix data issues * [#151] Add compact order book data unit tests * [#151] Add updateData unit tests * [#151] Add updateCompactedData unit tests * [#151] update cummulative vol on update, move data ref handling from effect to manager * [#151] Update cummulative vol on update, fixes * [#151] Optimieze orderbook ag-grid render * [#151] Replace ag-grid with orderbook component * [#151] Use react memo to optimize orderbook rendering * [#151] Fix cummulative vol computation on compacted data update * [#151] Add resolution controls * [#151] Fix cumulative spelling mistake * [#151] Use number instead of string for cumulative values * [#151] Expose restart callback in useDataProvider hook * [#151] Update empty cumulative values * [#151] Add horizontal line under header in orderbook * [#151] reuse orderbook data in market-depth chart * [#151] restart market depth provider on sequenceNumber gap * [#151] use throttle in orderbook and depth-chart * [#151] Add comments, refactor data functions * [#151] Add comments, refactor data functions * [#151] move orderbook and depth-chart libs to market-depth
This commit is contained in:
parent
b5788379a3
commit
d0ec016adc
@ -7,19 +7,22 @@ import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
|||||||
interface PageQueryContainerProps<TData, TVariables> {
|
interface PageQueryContainerProps<TData, TVariables> {
|
||||||
query: DocumentNode;
|
query: DocumentNode;
|
||||||
options?: QueryHookOptions<TData, TVariables>;
|
options?: QueryHookOptions<TData, TVariables>;
|
||||||
children: (data: TData) => ReactNode;
|
render: (data: TData) => ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PageQueryContainer = <TData, TVariables = OperationVariables>({
|
export const PageQueryContainer = <TData, TVariables = OperationVariables>({
|
||||||
query,
|
query,
|
||||||
options,
|
options,
|
||||||
children,
|
render,
|
||||||
}: PageQueryContainerProps<TData, TVariables>) => {
|
}: PageQueryContainerProps<TData, TVariables>) => {
|
||||||
const { data, loading, error } = useQuery<TData, TVariables>(query, options);
|
const { data, loading, error } = useQuery<TData, TVariables>(query, options);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer<TData> loading={loading} error={error} data={data}>
|
<AsyncRenderer<TData>
|
||||||
{(data) => children(data)}
|
loading={loading}
|
||||||
</AsyncRenderer>
|
error={error}
|
||||||
|
data={data}
|
||||||
|
render={render}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -46,8 +46,9 @@ interface Web3ContainerProps {
|
|||||||
export const Web3Container = ({ children }: Web3ContainerProps) => {
|
export const Web3Container = ({ children }: Web3ContainerProps) => {
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
<PageQueryContainer<NetworkParamsQuery> query={NETWORK_PARAMS_QUERY}>
|
<PageQueryContainer<NetworkParamsQuery>
|
||||||
{(data) => {
|
query={NETWORK_PARAMS_QUERY}
|
||||||
|
render={(data) => {
|
||||||
const ethereumConfigParam = data.networkParameters?.find(
|
const ethereumConfigParam = data.networkParameters?.find(
|
||||||
(np) => np.key === 'blockchains.ethereumConfig'
|
(np) => np.key === 'blockchains.ethereumConfig'
|
||||||
);
|
);
|
||||||
@ -89,7 +90,7 @@ export const Web3Container = ({ children }: Web3ContainerProps) => {
|
|||||||
</Web3Provider>
|
</Web3Provider>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</PageQueryContainer>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -44,8 +44,7 @@ const MarketPage = () => {
|
|||||||
},
|
},
|
||||||
fetchPolicy: 'network-only',
|
fetchPolicy: 'network-only',
|
||||||
}}
|
}}
|
||||||
>
|
render={({ market }) => {
|
||||||
{({ market }) => {
|
|
||||||
if (!market) {
|
if (!market) {
|
||||||
return <Splash>{t('Market not found')}</Splash>;
|
return <Splash>{t('Market not found')}</Splash>;
|
||||||
}
|
}
|
||||||
@ -56,7 +55,7 @@ const MarketPage = () => {
|
|||||||
<TradePanels market={market} />
|
<TradePanels market={market} />
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</PageQueryContainer>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -6,25 +6,19 @@ import { GridTab, GridTabs } from './grid-tabs';
|
|||||||
import { DealTicketContainer } from '@vegaprotocol/deal-ticket';
|
import { DealTicketContainer } from '@vegaprotocol/deal-ticket';
|
||||||
import { OrderListContainer } from '@vegaprotocol/order-list';
|
import { OrderListContainer } from '@vegaprotocol/order-list';
|
||||||
import { TradesContainer } from '@vegaprotocol/trades';
|
import { TradesContainer } from '@vegaprotocol/trades';
|
||||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { PositionsContainer } from '@vegaprotocol/positions';
|
import { PositionsContainer } from '@vegaprotocol/positions';
|
||||||
|
import { OrderbookContainer } from '@vegaprotocol/market-depth';
|
||||||
import type { Market_market } from './__generated__/Market';
|
import type { Market_market } from './__generated__/Market';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { AccountsContainer } from '@vegaprotocol/accounts';
|
import { AccountsContainer } from '@vegaprotocol/accounts';
|
||||||
import { DepthChartContainer } from '@vegaprotocol/depth-chart';
|
import { DepthChartContainer } from '@vegaprotocol/market-depth';
|
||||||
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
|
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
|
||||||
|
|
||||||
const Orderbook = () => (
|
|
||||||
<Splash>
|
|
||||||
<p>{t('Orderbook')}</p>
|
|
||||||
</Splash>
|
|
||||||
);
|
|
||||||
|
|
||||||
const TradingViews = {
|
const TradingViews = {
|
||||||
Candles: CandlesChartContainer,
|
Candles: CandlesChartContainer,
|
||||||
Depth: DepthChartContainer,
|
Depth: DepthChartContainer,
|
||||||
Ticket: DealTicketContainer,
|
Ticket: DealTicketContainer,
|
||||||
Orderbook: Orderbook,
|
Orderbook: OrderbookContainer,
|
||||||
Orders: OrderListContainer,
|
Orders: OrderListContainer,
|
||||||
Positions: PositionsContainer,
|
Positions: PositionsContainer,
|
||||||
Accounts: AccountsContainer,
|
Accounts: AccountsContainer,
|
||||||
@ -70,7 +64,7 @@ export const TradeGrid = ({ market }: TradeGridProps) => {
|
|||||||
<TradingViews.Trades marketId={market.id} />
|
<TradingViews.Trades marketId={market.id} />
|
||||||
</GridTab>
|
</GridTab>
|
||||||
<GridTab id="orderbook" name={t('Orderbook')}>
|
<GridTab id="orderbook" name={t('Orderbook')}>
|
||||||
<TradingViews.Orderbook />
|
<TradingViews.Orderbook marketId={market.id} />
|
||||||
</GridTab>
|
</GridTab>
|
||||||
</GridTabs>
|
</GridTabs>
|
||||||
</TradeGridChild>
|
</TradeGridChild>
|
||||||
|
@ -39,8 +39,9 @@ export const DepositContainer = ({
|
|||||||
assetId,
|
assetId,
|
||||||
}: DepositContainerProps) => {
|
}: DepositContainerProps) => {
|
||||||
return (
|
return (
|
||||||
<PageQueryContainer<DepositPage> query={DEPOSIT_PAGE_QUERY}>
|
<PageQueryContainer<DepositPage>
|
||||||
{(data) => {
|
query={DEPOSIT_PAGE_QUERY}
|
||||||
|
render={(data) => {
|
||||||
if (!data.assets?.length) {
|
if (!data.assets?.length) {
|
||||||
return (
|
return (
|
||||||
<Splash>
|
<Splash>
|
||||||
@ -58,7 +59,7 @@ export const DepositContainer = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</PageQueryContainer>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ export const AccountsManager = ({ partyId }: AccountsManagerProps) => {
|
|||||||
>(accountsDataProvider, update, variables);
|
>(accountsDataProvider, update, variables);
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||||
{(data) => <AccountsTable ref={gridRef} data={data} />}
|
<AccountsTable ref={gridRef} data={data} />
|
||||||
</AsyncRenderer>
|
</AsyncRenderer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -40,16 +40,13 @@ export const DealTicketContainer = ({ marketId }: DealTicketContainerProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer<DealTicketQuery> data={data} loading={loading} error={error}>
|
<AsyncRenderer<DealTicketQuery> data={data} loading={loading} error={error}>
|
||||||
{(data) => {
|
{data && data.market ? (
|
||||||
if (!data.market) {
|
<DealTicketManager market={data.market} />
|
||||||
return (
|
) : (
|
||||||
<Splash>
|
<Splash>
|
||||||
<p>{t('Could not load market')}</p>
|
<p>{t('Could not load market')}</p>
|
||||||
</Splash>
|
</Splash>
|
||||||
);
|
)}
|
||||||
}
|
|
||||||
return <DealTicketManager market={data.market} />;
|
|
||||||
}}
|
|
||||||
</AsyncRenderer>
|
</AsyncRenderer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
# depth-chart
|
|
||||||
|
|
||||||
This library was generated with [Nx](https://nx.dev).
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
Run `nx build depth-chart` to build the library.
|
|
||||||
|
|
||||||
## Running unit tests
|
|
||||||
|
|
||||||
Run `nx test depth-chart` to execute the unit tests via [Jest](https://jestjs.io).
|
|
@ -1,10 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
displayName: 'depth-chart',
|
|
||||||
preset: '../../jest.preset.js',
|
|
||||||
transform: {
|
|
||||||
'^.+\\.[tj]sx?$': 'babel-jest',
|
|
||||||
},
|
|
||||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
|
||||||
coverageDirectory: '../../coverage/libs/depth-chart',
|
|
||||||
setupFiles: ['jest-canvas-mock'],
|
|
||||||
};
|
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@vegaprotocol/depth-chart",
|
|
||||||
"version": "0.0.1"
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
export * from './lib/depth-chart';
|
|
@ -1,67 +0,0 @@
|
|||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
// @generated
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
// ====================================================
|
|
||||||
// GraphQL subscription operation: marketDepthSubscribe
|
|
||||||
// ====================================================
|
|
||||||
|
|
||||||
export interface marketDepthSubscribe_marketDepth_sell {
|
|
||||||
__typename: "PriceLevel";
|
|
||||||
/**
|
|
||||||
* The price of all the orders at this level (uint64)
|
|
||||||
*/
|
|
||||||
price: string;
|
|
||||||
/**
|
|
||||||
* The total remaining size of all orders at this level (uint64)
|
|
||||||
*/
|
|
||||||
volume: string;
|
|
||||||
/**
|
|
||||||
* The number of orders at this price level (uint64)
|
|
||||||
*/
|
|
||||||
numberOfOrders: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface marketDepthSubscribe_marketDepth_buy {
|
|
||||||
__typename: "PriceLevel";
|
|
||||||
/**
|
|
||||||
* The price of all the orders at this level (uint64)
|
|
||||||
*/
|
|
||||||
price: string;
|
|
||||||
/**
|
|
||||||
* The total remaining size of all orders at this level (uint64)
|
|
||||||
*/
|
|
||||||
volume: string;
|
|
||||||
/**
|
|
||||||
* The number of orders at this price level (uint64)
|
|
||||||
*/
|
|
||||||
numberOfOrders: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface marketDepthSubscribe_marketDepth {
|
|
||||||
__typename: "MarketDepth";
|
|
||||||
/**
|
|
||||||
* Sell side price levels (if available)
|
|
||||||
*/
|
|
||||||
sell: marketDepthSubscribe_marketDepth_sell[] | null;
|
|
||||||
/**
|
|
||||||
* Buy side price levels (if available)
|
|
||||||
*/
|
|
||||||
buy: marketDepthSubscribe_marketDepth_buy[] | null;
|
|
||||||
/**
|
|
||||||
* Sequence number for the current snapshot of the market depth
|
|
||||||
*/
|
|
||||||
sequenceNumber: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface marketDepthSubscribe {
|
|
||||||
/**
|
|
||||||
* Subscribe to the market depths update
|
|
||||||
*/
|
|
||||||
marketDepth: marketDepthSubscribe_marketDepth;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface marketDepthSubscribeVariables {
|
|
||||||
marketId: string;
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
import 'pennant/dist/style.css';
|
|
||||||
|
|
||||||
import { addDecimal, ThemeContext } from '@vegaprotocol/react-helpers';
|
|
||||||
import { DepthChart } from 'pennant';
|
|
||||||
import type { DepthChartProps } from 'pennant';
|
|
||||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import type { marketDepthUpdateSubscribe_marketDepthUpdate_sell } from './__generated__/marketDepthUpdateSubscribe';
|
|
||||||
|
|
||||||
import { useContext } from 'react';
|
|
||||||
import { useDepthUpdate } from './hooks/use-depth-update';
|
|
||||||
|
|
||||||
type PriceLevel = Pick<
|
|
||||||
marketDepthUpdateSubscribe_marketDepthUpdate_sell,
|
|
||||||
'price' | 'volume'
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type DepthChartContainerProps = {
|
|
||||||
marketId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DepthChartContainer = ({ marketId }: DepthChartContainerProps) => {
|
|
||||||
const theme = useContext(ThemeContext);
|
|
||||||
|
|
||||||
const { data, loading, error } = useDepthUpdate({ marketId }, 500);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <Splash>Error</Splash>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <Splash>Loading...</Splash>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data || !data.market) {
|
|
||||||
return <Splash>No Data</Splash>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const market = data.market;
|
|
||||||
const decimalPlaces = data.market.decimalPlaces;
|
|
||||||
const depthData: DepthChartProps['data'] = { buy: [], sell: [] };
|
|
||||||
|
|
||||||
if (market.depth) {
|
|
||||||
if (market.depth.buy) {
|
|
||||||
depthData.buy = market?.depth.buy?.map((priceLevel: PriceLevel) => ({
|
|
||||||
price: Number(addDecimal(priceLevel.price, decimalPlaces)),
|
|
||||||
volume: Number(priceLevel.volume),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (market.depth.sell) {
|
|
||||||
depthData.sell = market?.depth.sell?.map((priceLevel: PriceLevel) => ({
|
|
||||||
price: Number(addDecimal(priceLevel.price, decimalPlaces)),
|
|
||||||
volume: Number(priceLevel.volume),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let midPrice: number | undefined = undefined;
|
|
||||||
|
|
||||||
if (market.data?.midPrice) {
|
|
||||||
midPrice = Number(addDecimal(market.data.midPrice, decimalPlaces));
|
|
||||||
}
|
|
||||||
|
|
||||||
return <DepthChart data={depthData} midPrice={midPrice} theme={theme} />;
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
export * from './update-depth-update';
|
|
@ -1,110 +0,0 @@
|
|||||||
import { updateDepthUpdate } from './update-depth-update';
|
|
||||||
|
|
||||||
describe('updateDepthUpdate', () => {
|
|
||||||
it('Updates typical case', () => {
|
|
||||||
const prev = createMarketDepth([{ price: '100', volume: '10' }], null);
|
|
||||||
const update = createMarketDepthUpdate(
|
|
||||||
[{ price: '200', volume: '20' }],
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const expected = createMarketDepth(
|
|
||||||
[
|
|
||||||
{ price: '200', volume: '20' },
|
|
||||||
{ price: '100', volume: '10' },
|
|
||||||
],
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(updateDepthUpdate(prev, update)).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Removes price level', () => {
|
|
||||||
const prev = createMarketDepth(
|
|
||||||
[
|
|
||||||
{ price: '200', volume: '20' },
|
|
||||||
{ price: '100', volume: '10' },
|
|
||||||
],
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const update = createMarketDepthUpdate(
|
|
||||||
[{ price: '200', volume: '0' }],
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const expected = createMarketDepth([{ price: '100', volume: '10' }], []);
|
|
||||||
|
|
||||||
expect(updateDepthUpdate(prev, update)).toEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function createMarketDepth(
|
|
||||||
buy: { price: string; volume: string }[] | null,
|
|
||||||
sell: { price: string; volume: string }[] | null
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
market: {
|
|
||||||
__typename: 'Market' as const,
|
|
||||||
id: 'id',
|
|
||||||
decimalPlaces: 0,
|
|
||||||
data: { __typename: 'MarketData' as const, midPrice: '100' },
|
|
||||||
depth: {
|
|
||||||
__typename: 'MarketDepth' as const,
|
|
||||||
lastTrade: { __typename: 'Trade' as const, price: '100' },
|
|
||||||
sell: sell
|
|
||||||
? sell.map((priceLevel) => ({
|
|
||||||
__typename: 'PriceLevel' as const,
|
|
||||||
price: priceLevel.price,
|
|
||||||
volume: priceLevel.volume,
|
|
||||||
numberOfOrders: '20',
|
|
||||||
}))
|
|
||||||
: null,
|
|
||||||
buy: buy
|
|
||||||
? buy.map((priceLevel) => ({
|
|
||||||
__typename: 'PriceLevel' as const,
|
|
||||||
price: priceLevel.price,
|
|
||||||
volume: priceLevel.volume,
|
|
||||||
numberOfOrders: '20',
|
|
||||||
}))
|
|
||||||
: null,
|
|
||||||
sequenceNumber: '0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMarketDepthUpdate(
|
|
||||||
buy: { price: string; volume: string }[] | null,
|
|
||||||
sell: { price: string; volume: string }[] | null
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
data: {
|
|
||||||
marketDepthUpdate: {
|
|
||||||
__typename: 'MarketDepthUpdate' as const,
|
|
||||||
market: {
|
|
||||||
__typename: 'Market' as const,
|
|
||||||
id: 'id',
|
|
||||||
data: { __typename: 'MarketData' as const, midPrice: '100' },
|
|
||||||
},
|
|
||||||
sell: sell
|
|
||||||
? sell.map((priceLevel) => ({
|
|
||||||
__typename: 'PriceLevel' as const,
|
|
||||||
price: priceLevel.price,
|
|
||||||
volume: priceLevel.volume,
|
|
||||||
numberOfOrders: '20',
|
|
||||||
}))
|
|
||||||
: null,
|
|
||||||
buy: buy
|
|
||||||
? buy.map((priceLevel) => ({
|
|
||||||
__typename: 'PriceLevel' as const,
|
|
||||||
price: priceLevel.price,
|
|
||||||
volume: priceLevel.volume,
|
|
||||||
numberOfOrders: '20',
|
|
||||||
}))
|
|
||||||
: null,
|
|
||||||
sequenceNumber: '1',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
import type {
|
|
||||||
marketDepth,
|
|
||||||
marketDepth_market_depth,
|
|
||||||
} from '../__generated__/marketDepth';
|
|
||||||
import type { marketDepthUpdateSubscribe } from '../__generated__/marketDepthUpdateSubscribe';
|
|
||||||
import sortBy from 'lodash/sortBy';
|
|
||||||
|
|
||||||
type MarketDepth = Pick<marketDepth_market_depth, 'buy' | 'sell'>;
|
|
||||||
|
|
||||||
export function updateDepthUpdate(
|
|
||||||
prev: marketDepth,
|
|
||||||
subscriptionData: { data: marketDepthUpdateSubscribe }
|
|
||||||
): marketDepth {
|
|
||||||
if (!subscriptionData.data.marketDepthUpdate || !prev.market) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
market: {
|
|
||||||
...prev.market,
|
|
||||||
...(prev.market.data && {
|
|
||||||
data: {
|
|
||||||
...prev.market.data,
|
|
||||||
midPrice:
|
|
||||||
subscriptionData.data.marketDepthUpdate.market.data?.midPrice ??
|
|
||||||
prev.market.data.midPrice,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
depth: {
|
|
||||||
...prev.market.depth,
|
|
||||||
...merge(prev.market.depth, subscriptionData.data.marketDepthUpdate),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function merge(snapshot: MarketDepth, update: MarketDepth): MarketDepth {
|
|
||||||
let buy = snapshot.buy ? [...snapshot.buy] : null;
|
|
||||||
let sell = snapshot.sell ? [...snapshot.sell] : null;
|
|
||||||
|
|
||||||
if (buy !== null) {
|
|
||||||
if (update.buy !== null) {
|
|
||||||
for (const priceLevel of update.buy) {
|
|
||||||
const index = buy.findIndex(
|
|
||||||
(level) => level.price === priceLevel.price
|
|
||||||
);
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
if (priceLevel.volume !== '0') {
|
|
||||||
buy.splice(index, 1, priceLevel);
|
|
||||||
} else {
|
|
||||||
buy.splice(index, 1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
buy.push(priceLevel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
buy = update.buy;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sell !== null) {
|
|
||||||
if (update.sell !== null) {
|
|
||||||
for (const priceLevel of update.sell) {
|
|
||||||
const index = sell.findIndex(
|
|
||||||
(level) => level.price === priceLevel.price
|
|
||||||
);
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
if (priceLevel.volume !== '0') {
|
|
||||||
sell.splice(index, 1, priceLevel);
|
|
||||||
} else {
|
|
||||||
sell.splice(index, 1);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sell.push(priceLevel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sell = update.sell;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
buy: sortBy(buy, (d) => -parseInt(d.price)),
|
|
||||||
sell: sortBy(sell, (d) => parseInt(d.price)),
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,135 +0,0 @@
|
|||||||
import type { ApolloError } from '@apollo/client';
|
|
||||||
import { useApolloClient } from '@apollo/client';
|
|
||||||
import throttle from 'lodash/throttle';
|
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { updateDepthUpdate } from '../helpers';
|
|
||||||
import {
|
|
||||||
MARKET_DEPTH_QUERY,
|
|
||||||
MARKET_DEPTH_UPDATE_SUB,
|
|
||||||
} from '../queries/market-depth';
|
|
||||||
import type {
|
|
||||||
marketDepth,
|
|
||||||
marketDepthVariables,
|
|
||||||
} from '../__generated__/marketDepth';
|
|
||||||
import type {
|
|
||||||
marketDepthUpdateSubscribe,
|
|
||||||
marketDepthUpdateSubscribeVariables,
|
|
||||||
} from '../__generated__/marketDepthUpdateSubscribe';
|
|
||||||
|
|
||||||
export interface QueryResult<TData> {
|
|
||||||
data: TData | undefined;
|
|
||||||
loading: boolean;
|
|
||||||
error?: ApolloError;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useDepthUpdate({ marketId }: marketDepthVariables, wait = 0) {
|
|
||||||
const queryResultRef = useRef<QueryResult<marketDepth>>({
|
|
||||||
data: undefined,
|
|
||||||
loading: true,
|
|
||||||
error: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [queryResult, setQueryResult] = useState<QueryResult<marketDepth>>({
|
|
||||||
data: undefined,
|
|
||||||
loading: true,
|
|
||||||
error: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const sequenceNumber = useRef<null | number>(null);
|
|
||||||
const [stallCount, setStallCount] = useState(0);
|
|
||||||
|
|
||||||
const client = useApolloClient();
|
|
||||||
|
|
||||||
const handleUpdate = useMemo(
|
|
||||||
() => throttle(setQueryResult, wait, { leading: true }),
|
|
||||||
[wait]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
const { data, loading, error } = await client.query<
|
|
||||||
marketDepth,
|
|
||||||
marketDepthVariables
|
|
||||||
>({
|
|
||||||
query: MARKET_DEPTH_QUERY,
|
|
||||||
variables: { marketId },
|
|
||||||
fetchPolicy: 'no-cache',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data.market?.depth.sequenceNumber) {
|
|
||||||
sequenceNumber.current = Number.parseInt(
|
|
||||||
data.market?.depth.sequenceNumber
|
|
||||||
);
|
|
||||||
|
|
||||||
queryResultRef.current = { data, loading, error };
|
|
||||||
handleUpdate({ data, loading, error });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchData();
|
|
||||||
}, [client, handleUpdate, marketId, stallCount]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!marketId) return;
|
|
||||||
|
|
||||||
const result = client.subscribe<
|
|
||||||
marketDepthUpdateSubscribe,
|
|
||||||
marketDepthUpdateSubscribeVariables
|
|
||||||
>({
|
|
||||||
query: MARKET_DEPTH_UPDATE_SUB,
|
|
||||||
variables: { marketId },
|
|
||||||
fetchPolicy: 'no-cache',
|
|
||||||
errorPolicy: 'none',
|
|
||||||
});
|
|
||||||
|
|
||||||
const subscription = result.subscribe((result) => {
|
|
||||||
const prev = queryResultRef.current.data;
|
|
||||||
const subscriptionData = result;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!prev ||
|
|
||||||
!subscriptionData.data ||
|
|
||||||
subscriptionData.data?.marketDepthUpdate?.market?.id !== prev.market?.id
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextSequenceNumber = Number.parseInt(
|
|
||||||
subscriptionData.data.marketDepthUpdate.sequenceNumber
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
prev.market &&
|
|
||||||
subscriptionData.data?.marketDepthUpdate &&
|
|
||||||
sequenceNumber.current !== null &&
|
|
||||||
nextSequenceNumber !== sequenceNumber.current + 1
|
|
||||||
) {
|
|
||||||
console.log(
|
|
||||||
`Refetching: Expected ${
|
|
||||||
sequenceNumber.current + 1
|
|
||||||
} but got ${nextSequenceNumber}`
|
|
||||||
);
|
|
||||||
|
|
||||||
sequenceNumber.current = null;
|
|
||||||
|
|
||||||
// Trigger refetch
|
|
||||||
setStallCount((count) => count + 1);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sequenceNumber.current = nextSequenceNumber;
|
|
||||||
|
|
||||||
const depth = updateDepthUpdate(prev, { data: subscriptionData.data });
|
|
||||||
|
|
||||||
queryResultRef.current.data = depth;
|
|
||||||
handleUpdate({ data: depth, loading: false });
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
subscription && subscription.unsubscribe();
|
|
||||||
};
|
|
||||||
}, [client, handleUpdate, marketId]);
|
|
||||||
|
|
||||||
return queryResult;
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
import { gql } from '@apollo/client';
|
|
||||||
|
|
||||||
export const MARKET_DEPTH_QUERY = gql`
|
|
||||||
query MarketDepth($marketId: ID!) {
|
|
||||||
market(id: $marketId) {
|
|
||||||
id
|
|
||||||
decimalPlaces
|
|
||||||
data {
|
|
||||||
midPrice
|
|
||||||
}
|
|
||||||
depth {
|
|
||||||
lastTrade {
|
|
||||||
price
|
|
||||||
}
|
|
||||||
sell {
|
|
||||||
price
|
|
||||||
volume
|
|
||||||
numberOfOrders
|
|
||||||
}
|
|
||||||
buy {
|
|
||||||
price
|
|
||||||
volume
|
|
||||||
numberOfOrders
|
|
||||||
}
|
|
||||||
sequenceNumber
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const MARKET_DEPTH_UPDATE_SUB = gql`
|
|
||||||
subscription MarketDepthUpdateSubscribe($marketId: ID!) {
|
|
||||||
marketDepthUpdate(marketId: $marketId) {
|
|
||||||
market {
|
|
||||||
id
|
|
||||||
data {
|
|
||||||
midPrice
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sell {
|
|
||||||
price
|
|
||||||
volume
|
|
||||||
numberOfOrders
|
|
||||||
}
|
|
||||||
buy {
|
|
||||||
price
|
|
||||||
volume
|
|
||||||
numberOfOrders
|
|
||||||
}
|
|
||||||
sequenceNumber
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
7
libs/market-depth/README.md
Normal file
7
libs/market-depth/README.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# orderbook
|
||||||
|
|
||||||
|
This library was generated with [Nx](https://nx.dev).
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `nx test orderbook` to execute the unit tests via [Jest](https://jestjs.io).
|
15
libs/market-depth/jest.config.js
Normal file
15
libs/market-depth/jest.config.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
module.exports = {
|
||||||
|
displayName: 'orderbook',
|
||||||
|
preset: '../../jest.preset.js',
|
||||||
|
globals: {
|
||||||
|
'ts-jest': {
|
||||||
|
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
transform: {
|
||||||
|
'^.+\\.[tj]sx?$': 'ts-jest',
|
||||||
|
},
|
||||||
|
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||||
|
coverageDirectory: '../../coverage/libs/market-depth',
|
||||||
|
setupFilesAfterEnv: ['./src/setup-tests.ts'],
|
||||||
|
};
|
4
libs/market-depth/package.json
Normal file
4
libs/market-depth/package.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"name": "@vegaprotocol/market-depth",
|
||||||
|
"version": "0.0.1"
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"root": "libs/depth-chart",
|
"root": "libs/market-depth",
|
||||||
"sourceRoot": "libs/depth-chart/src",
|
"sourceRoot": "libs/market-depth/src",
|
||||||
"projectType": "library",
|
"projectType": "library",
|
||||||
"tags": [],
|
"tags": [],
|
||||||
"targets": {
|
"targets": {
|
||||||
@ -8,16 +8,16 @@
|
|||||||
"executor": "@nrwl/web:rollup",
|
"executor": "@nrwl/web:rollup",
|
||||||
"outputs": ["{options.outputPath}"],
|
"outputs": ["{options.outputPath}"],
|
||||||
"options": {
|
"options": {
|
||||||
"outputPath": "dist/libs/depth-chart",
|
"outputPath": "dist/libs/market-depth",
|
||||||
"tsConfig": "libs/depth-chart/tsconfig.lib.json",
|
"tsConfig": "libs/market-depth/tsconfig.lib.json",
|
||||||
"project": "libs/depth-chart/package.json",
|
"project": "libs/market-depth/package.json",
|
||||||
"entryFile": "libs/depth-chart/src/index.ts",
|
"entryFile": "libs/market-depth/src/index.ts",
|
||||||
"external": ["react/jsx-runtime"],
|
"external": ["react/jsx-runtime"],
|
||||||
"rollupConfig": "@nrwl/react/plugins/bundle-rollup",
|
"rollupConfig": "@nrwl/react/plugins/bundle-rollup",
|
||||||
"compiler": "babel",
|
"compiler": "babel",
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"glob": "libs/depth-chart/README.md",
|
"glob": "libs/market-depth/README.md",
|
||||||
"input": ".",
|
"input": ".",
|
||||||
"output": "."
|
"output": "."
|
||||||
}
|
}
|
||||||
@ -28,14 +28,14 @@
|
|||||||
"executor": "@nrwl/linter:eslint",
|
"executor": "@nrwl/linter:eslint",
|
||||||
"outputs": ["{options.outputFile}"],
|
"outputs": ["{options.outputFile}"],
|
||||||
"options": {
|
"options": {
|
||||||
"lintFilePatterns": ["libs/depth-chart/**/*.{ts,tsx,js,jsx}"]
|
"lintFilePatterns": ["libs/market-depth/**/*.{ts,tsx,js,jsx}"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"executor": "@nrwl/jest:jest",
|
"executor": "@nrwl/jest:jest",
|
||||||
"outputs": ["coverage/libs/depth-chart"],
|
"outputs": ["coverage/libs/market-depth"],
|
||||||
"options": {
|
"options": {
|
||||||
"jestConfig": "libs/depth-chart/jest.config.js",
|
"jestConfig": "libs/market-depth/jest.config.js",
|
||||||
"passWithNoTests": true
|
"passWithNoTests": true
|
||||||
}
|
}
|
||||||
}
|
}
|
2
libs/market-depth/src/index.ts
Normal file
2
libs/market-depth/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './lib/depth-chart';
|
||||||
|
export * from './lib/orderbook-container';
|
@ -4,18 +4,30 @@
|
|||||||
// This file was automatically generated and should not be edited.
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
// ====================================================
|
// ====================================================
|
||||||
// GraphQL query operation: marketDepth
|
// GraphQL query operation: MarketDepth
|
||||||
// ====================================================
|
// ====================================================
|
||||||
|
|
||||||
export interface marketDepth_market_data {
|
export interface MarketDepth_market_data_market {
|
||||||
|
__typename: "Market";
|
||||||
|
/**
|
||||||
|
* Market ID
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketDepth_market_data {
|
||||||
__typename: "MarketData";
|
__typename: "MarketData";
|
||||||
/**
|
/**
|
||||||
* the arithmetic average of the best bid price and best offer price.
|
* the arithmetic average of the best bid price and best offer price.
|
||||||
*/
|
*/
|
||||||
midPrice: string;
|
midPrice: string;
|
||||||
|
/**
|
||||||
|
* market id of the associated mark price
|
||||||
|
*/
|
||||||
|
market: MarketDepth_market_data_market;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface marketDepth_market_depth_lastTrade {
|
export interface MarketDepth_market_depth_lastTrade {
|
||||||
__typename: "Trade";
|
__typename: "Trade";
|
||||||
/**
|
/**
|
||||||
* The price of the trade (probably initially the passive order price, other determination algorithms are possible though) (uint64)
|
* The price of the trade (probably initially the passive order price, other determination algorithms are possible though) (uint64)
|
||||||
@ -23,7 +35,7 @@ export interface marketDepth_market_depth_lastTrade {
|
|||||||
price: string;
|
price: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface marketDepth_market_depth_sell {
|
export interface MarketDepth_market_depth_sell {
|
||||||
__typename: "PriceLevel";
|
__typename: "PriceLevel";
|
||||||
/**
|
/**
|
||||||
* The price of all the orders at this level (uint64)
|
* The price of all the orders at this level (uint64)
|
||||||
@ -39,7 +51,7 @@ export interface marketDepth_market_depth_sell {
|
|||||||
numberOfOrders: string;
|
numberOfOrders: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface marketDepth_market_depth_buy {
|
export interface MarketDepth_market_depth_buy {
|
||||||
__typename: "PriceLevel";
|
__typename: "PriceLevel";
|
||||||
/**
|
/**
|
||||||
* The price of all the orders at this level (uint64)
|
* The price of all the orders at this level (uint64)
|
||||||
@ -55,27 +67,27 @@ export interface marketDepth_market_depth_buy {
|
|||||||
numberOfOrders: string;
|
numberOfOrders: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface marketDepth_market_depth {
|
export interface MarketDepth_market_depth {
|
||||||
__typename: "MarketDepth";
|
__typename: "MarketDepth";
|
||||||
/**
|
/**
|
||||||
* Last trade for the given market (if available)
|
* Last trade for the given market (if available)
|
||||||
*/
|
*/
|
||||||
lastTrade: marketDepth_market_depth_lastTrade | null;
|
lastTrade: MarketDepth_market_depth_lastTrade | null;
|
||||||
/**
|
/**
|
||||||
* Sell side price levels (if available)
|
* Sell side price levels (if available)
|
||||||
*/
|
*/
|
||||||
sell: marketDepth_market_depth_sell[] | null;
|
sell: MarketDepth_market_depth_sell[] | null;
|
||||||
/**
|
/**
|
||||||
* Buy side price levels (if available)
|
* Buy side price levels (if available)
|
||||||
*/
|
*/
|
||||||
buy: marketDepth_market_depth_buy[] | null;
|
buy: MarketDepth_market_depth_buy[] | null;
|
||||||
/**
|
/**
|
||||||
* Sequence number for the current snapshot of the market depth
|
* Sequence number for the current snapshot of the market depth
|
||||||
*/
|
*/
|
||||||
sequenceNumber: string;
|
sequenceNumber: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface marketDepth_market {
|
export interface MarketDepth_market {
|
||||||
__typename: "Market";
|
__typename: "Market";
|
||||||
/**
|
/**
|
||||||
* Market ID
|
* Market ID
|
||||||
@ -84,14 +96,14 @@ export interface marketDepth_market {
|
|||||||
/**
|
/**
|
||||||
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
|
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
|
||||||
* number denominated in the currency of the Market. (uint64)
|
* number denominated in the currency of the Market. (uint64)
|
||||||
*
|
*
|
||||||
* Examples:
|
* Examples:
|
||||||
* Currency Balance decimalPlaces Real Balance
|
* Currency Balance decimalPlaces Real Balance
|
||||||
* GBP 100 0 GBP 100
|
* GBP 100 0 GBP 100
|
||||||
* GBP 100 2 GBP 1.00
|
* GBP 100 2 GBP 1.00
|
||||||
* GBP 100 4 GBP 0.01
|
* GBP 100 4 GBP 0.01
|
||||||
* GBP 1 4 GBP 0.0001 ( 0.01p )
|
* GBP 1 4 GBP 0.0001 ( 0.01p )
|
||||||
*
|
*
|
||||||
* GBX (pence) 100 0 GBP 1.00 (100p )
|
* GBX (pence) 100 0 GBP 1.00 (100p )
|
||||||
* GBX (pence) 100 2 GBP 0.01 ( 1p )
|
* GBX (pence) 100 2 GBP 0.01 ( 1p )
|
||||||
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
|
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
|
||||||
@ -101,20 +113,20 @@ export interface marketDepth_market {
|
|||||||
/**
|
/**
|
||||||
* marketData for the given market
|
* marketData for the given market
|
||||||
*/
|
*/
|
||||||
data: marketDepth_market_data | null;
|
data: MarketDepth_market_data | null;
|
||||||
/**
|
/**
|
||||||
* Current depth on the orderbook for this market
|
* Current depth on the order book for this market
|
||||||
*/
|
*/
|
||||||
depth: marketDepth_market_depth;
|
depth: MarketDepth_market_depth;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface marketDepth {
|
export interface MarketDepth {
|
||||||
/**
|
/**
|
||||||
* An instrument that is trading on the VEGA network
|
* An instrument that is trading on the VEGA network
|
||||||
*/
|
*/
|
||||||
market: marketDepth_market | null;
|
market: MarketDepth_market | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface marketDepthVariables {
|
export interface MarketDepthVariables {
|
||||||
marketId: string;
|
marketId: string;
|
||||||
}
|
}
|
@ -4,18 +4,30 @@
|
|||||||
// This file was automatically generated and should not be edited.
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
// ====================================================
|
// ====================================================
|
||||||
// GraphQL subscription operation: marketDepthUpdateSubscribe
|
// GraphQL subscription operation: MarketDepthSubscription
|
||||||
// ====================================================
|
// ====================================================
|
||||||
|
|
||||||
export interface marketDepthUpdateSubscribe_marketDepthUpdate_market_data {
|
export interface MarketDepthSubscription_marketDepthUpdate_market_data_market {
|
||||||
|
__typename: "Market";
|
||||||
|
/**
|
||||||
|
* Market ID
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MarketDepthSubscription_marketDepthUpdate_market_data {
|
||||||
__typename: "MarketData";
|
__typename: "MarketData";
|
||||||
/**
|
/**
|
||||||
* the arithmetic average of the best bid price and best offer price.
|
* the arithmetic average of the best bid price and best offer price.
|
||||||
*/
|
*/
|
||||||
midPrice: string;
|
midPrice: string;
|
||||||
|
/**
|
||||||
|
* market id of the associated mark price
|
||||||
|
*/
|
||||||
|
market: MarketDepthSubscription_marketDepthUpdate_market_data_market;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface marketDepthUpdateSubscribe_marketDepthUpdate_market {
|
export interface MarketDepthSubscription_marketDepthUpdate_market {
|
||||||
__typename: "Market";
|
__typename: "Market";
|
||||||
/**
|
/**
|
||||||
* Market ID
|
* Market ID
|
||||||
@ -24,10 +36,10 @@ export interface marketDepthUpdateSubscribe_marketDepthUpdate_market {
|
|||||||
/**
|
/**
|
||||||
* marketData for the given market
|
* marketData for the given market
|
||||||
*/
|
*/
|
||||||
data: marketDepthUpdateSubscribe_marketDepthUpdate_market_data | null;
|
data: MarketDepthSubscription_marketDepthUpdate_market_data | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface marketDepthUpdateSubscribe_marketDepthUpdate_sell {
|
export interface MarketDepthSubscription_marketDepthUpdate_sell {
|
||||||
__typename: "PriceLevel";
|
__typename: "PriceLevel";
|
||||||
/**
|
/**
|
||||||
* The price of all the orders at this level (uint64)
|
* The price of all the orders at this level (uint64)
|
||||||
@ -43,7 +55,7 @@ export interface marketDepthUpdateSubscribe_marketDepthUpdate_sell {
|
|||||||
numberOfOrders: string;
|
numberOfOrders: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface marketDepthUpdateSubscribe_marketDepthUpdate_buy {
|
export interface MarketDepthSubscription_marketDepthUpdate_buy {
|
||||||
__typename: "PriceLevel";
|
__typename: "PriceLevel";
|
||||||
/**
|
/**
|
||||||
* The price of all the orders at this level (uint64)
|
* The price of all the orders at this level (uint64)
|
||||||
@ -59,33 +71,33 @@ export interface marketDepthUpdateSubscribe_marketDepthUpdate_buy {
|
|||||||
numberOfOrders: string;
|
numberOfOrders: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface marketDepthUpdateSubscribe_marketDepthUpdate {
|
export interface MarketDepthSubscription_marketDepthUpdate {
|
||||||
__typename: "MarketDepthUpdate";
|
__typename: "MarketDepthUpdate";
|
||||||
/**
|
/**
|
||||||
* Market id
|
* Market id
|
||||||
*/
|
*/
|
||||||
market: marketDepthUpdateSubscribe_marketDepthUpdate_market;
|
market: MarketDepthSubscription_marketDepthUpdate_market;
|
||||||
/**
|
/**
|
||||||
* Sell side price levels (if available)
|
* Sell side price levels (if available)
|
||||||
*/
|
*/
|
||||||
sell: marketDepthUpdateSubscribe_marketDepthUpdate_sell[] | null;
|
sell: MarketDepthSubscription_marketDepthUpdate_sell[] | null;
|
||||||
/**
|
/**
|
||||||
* Buy side price levels (if available)
|
* Buy side price levels (if available)
|
||||||
*/
|
*/
|
||||||
buy: marketDepthUpdateSubscribe_marketDepthUpdate_buy[] | null;
|
buy: MarketDepthSubscription_marketDepthUpdate_buy[] | null;
|
||||||
/**
|
/**
|
||||||
* Sequence number for the current snapshot of the market depth
|
* Sequence number for the current snapshot of the market depth
|
||||||
*/
|
*/
|
||||||
sequenceNumber: string;
|
sequenceNumber: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface marketDepthUpdateSubscribe {
|
export interface MarketDepthSubscription {
|
||||||
/**
|
/**
|
||||||
* Subscribe to price level market depth updates
|
* Subscribe to price level market depth updates
|
||||||
*/
|
*/
|
||||||
marketDepthUpdate: marketDepthUpdateSubscribe_marketDepthUpdate;
|
marketDepthUpdate: MarketDepthSubscription_marketDepthUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface marketDepthUpdateSubscribeVariables {
|
export interface MarketDepthSubscriptionVariables {
|
||||||
marketId: string;
|
marketId: string;
|
||||||
}
|
}
|
159
libs/market-depth/src/lib/depth-chart.tsx
Normal file
159
libs/market-depth/src/lib/depth-chart.tsx
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import { DepthChart } from 'pennant';
|
||||||
|
import { produce } from 'immer';
|
||||||
|
import throttle from 'lodash/throttle';
|
||||||
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import {
|
||||||
|
useDataProvider,
|
||||||
|
addDecimal,
|
||||||
|
ThemeContext,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
import { marketDepthDataProvider } from './market-depth-data-provider';
|
||||||
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
useContext,
|
||||||
|
} from 'react';
|
||||||
|
import type {
|
||||||
|
MarketDepthSubscription_marketDepthUpdate_buy,
|
||||||
|
MarketDepthSubscription_marketDepthUpdate_sell,
|
||||||
|
MarketDepthSubscription_marketDepthUpdate,
|
||||||
|
} from './__generated__/MarketDepthSubscription';
|
||||||
|
import type {
|
||||||
|
MarketDepth_market_depth_buy,
|
||||||
|
MarketDepth_market_depth_sell,
|
||||||
|
} from './__generated__/MarketDepth';
|
||||||
|
import type { DepthChartProps } from 'pennant';
|
||||||
|
|
||||||
|
interface DepthChartManagerProps {
|
||||||
|
marketId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PriceLevel {
|
||||||
|
price: number;
|
||||||
|
volume: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatLevel = (
|
||||||
|
priceLevel: MarketDepth_market_depth_buy | MarketDepth_market_depth_sell,
|
||||||
|
decimalPlaces: number
|
||||||
|
): PriceLevel => ({
|
||||||
|
price: Number(addDecimal(priceLevel.price, decimalPlaces)),
|
||||||
|
volume: Number(priceLevel.volume),
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateLevels = (
|
||||||
|
levels: PriceLevel[],
|
||||||
|
updates: (
|
||||||
|
| MarketDepthSubscription_marketDepthUpdate_buy
|
||||||
|
| MarketDepthSubscription_marketDepthUpdate_sell
|
||||||
|
)[],
|
||||||
|
decimalPlaces: number
|
||||||
|
) => {
|
||||||
|
updates.forEach((update) => {
|
||||||
|
const updateLevel = formatLevel(update, decimalPlaces);
|
||||||
|
let index = levels.findIndex((level) => level.price === updateLevel.price);
|
||||||
|
if (index !== -1) {
|
||||||
|
if (update.volume === '0') {
|
||||||
|
levels.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
Object.assign(levels[index], updateLevel);
|
||||||
|
}
|
||||||
|
} else if (update.volume !== '0') {
|
||||||
|
index = levels.findIndex((level) => level.price > updateLevel.price);
|
||||||
|
if (index !== -1) {
|
||||||
|
levels.splice(index, 0, updateLevel);
|
||||||
|
} else {
|
||||||
|
levels.push(updateLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return levels;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatMidPrice = (midPrice: string, decimalPlaces: number) =>
|
||||||
|
Number(addDecimal(midPrice, decimalPlaces));
|
||||||
|
|
||||||
|
type DepthData = Pick<DepthChartProps, 'data' | 'midPrice'>;
|
||||||
|
|
||||||
|
export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
|
||||||
|
const theme = useContext(ThemeContext);
|
||||||
|
const variables = useMemo(() => ({ marketId }), [marketId]);
|
||||||
|
const [depthData, setDepthData] = useState<DepthData | null>(null);
|
||||||
|
const decimalPlacesRef = useRef<number>(0);
|
||||||
|
const dataRef = useRef<DepthData | null>(null);
|
||||||
|
const setDepthDataThrottledRef = useRef(throttle(setDepthData, 1000));
|
||||||
|
|
||||||
|
// Apply updates to the table
|
||||||
|
const update = useCallback(
|
||||||
|
(delta: MarketDepthSubscription_marketDepthUpdate) => {
|
||||||
|
if (!dataRef.current) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
dataRef.current = produce(dataRef.current, (draft) => {
|
||||||
|
if (delta.buy) {
|
||||||
|
draft.data.buy = updateLevels(
|
||||||
|
draft.data.buy,
|
||||||
|
delta.buy,
|
||||||
|
decimalPlacesRef.current
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (delta.sell) {
|
||||||
|
draft.data.sell = updateLevels(
|
||||||
|
draft.data.sell,
|
||||||
|
delta.sell,
|
||||||
|
decimalPlacesRef.current
|
||||||
|
);
|
||||||
|
}
|
||||||
|
draft.midPrice = delta.market.data?.midPrice
|
||||||
|
? formatMidPrice(
|
||||||
|
delta.market.data?.midPrice,
|
||||||
|
decimalPlacesRef.current
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
});
|
||||||
|
setDepthDataThrottledRef.current(dataRef.current);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data, error, loading } = useDataProvider(
|
||||||
|
marketDepthDataProvider,
|
||||||
|
update,
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data) {
|
||||||
|
dataRef.current = null;
|
||||||
|
setDepthData(dataRef.current);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dataRef.current = {
|
||||||
|
midPrice: data.data?.midPrice
|
||||||
|
? formatMidPrice(data.data?.midPrice, data.decimalPlaces)
|
||||||
|
: undefined,
|
||||||
|
data: {
|
||||||
|
buy:
|
||||||
|
data.depth.buy?.map((priceLevel) =>
|
||||||
|
formatLevel(priceLevel, data.decimalPlaces)
|
||||||
|
) ?? [],
|
||||||
|
sell:
|
||||||
|
data.depth.sell?.map((priceLevel) =>
|
||||||
|
formatLevel(priceLevel, data.decimalPlaces)
|
||||||
|
) ?? [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setDepthData(dataRef.current);
|
||||||
|
decimalPlacesRef.current = data.decimalPlaces;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||||
|
{depthData && <DepthChart {...depthData} theme={theme} />}
|
||||||
|
</AsyncRenderer>
|
||||||
|
);
|
||||||
|
};
|
116
libs/market-depth/src/lib/market-depth-data-provider.ts
Normal file
116
libs/market-depth/src/lib/market-depth-data-provider.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import { makeDataProvider } from '@vegaprotocol/react-helpers';
|
||||||
|
import { updateLevels } from './orderbook-data';
|
||||||
|
import type { Update } from '@vegaprotocol/react-helpers';
|
||||||
|
import type {
|
||||||
|
MarketDepth,
|
||||||
|
MarketDepth_market,
|
||||||
|
} from './__generated__/MarketDepth';
|
||||||
|
import type {
|
||||||
|
MarketDepthSubscription,
|
||||||
|
MarketDepthSubscription_marketDepthUpdate,
|
||||||
|
} from './__generated__/MarketDepthSubscription';
|
||||||
|
|
||||||
|
const MARKET_DEPTH_QUERY = gql`
|
||||||
|
query MarketDepth($marketId: ID!) {
|
||||||
|
market(id: $marketId) {
|
||||||
|
id
|
||||||
|
decimalPlaces
|
||||||
|
data {
|
||||||
|
midPrice
|
||||||
|
market {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
depth {
|
||||||
|
lastTrade {
|
||||||
|
price
|
||||||
|
}
|
||||||
|
sell {
|
||||||
|
price
|
||||||
|
volume
|
||||||
|
numberOfOrders
|
||||||
|
}
|
||||||
|
buy {
|
||||||
|
price
|
||||||
|
volume
|
||||||
|
numberOfOrders
|
||||||
|
}
|
||||||
|
sequenceNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const MARKET_DEPTH_SUBSCRIPTION_QUERY = gql`
|
||||||
|
subscription MarketDepthSubscription($marketId: ID!) {
|
||||||
|
marketDepthUpdate(marketId: $marketId) {
|
||||||
|
market {
|
||||||
|
id
|
||||||
|
data {
|
||||||
|
midPrice
|
||||||
|
market {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sell {
|
||||||
|
price
|
||||||
|
volume
|
||||||
|
numberOfOrders
|
||||||
|
}
|
||||||
|
buy {
|
||||||
|
price
|
||||||
|
volume
|
||||||
|
numberOfOrders
|
||||||
|
}
|
||||||
|
sequenceNumber
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const sequenceNumbers: Record<string, number> = {};
|
||||||
|
|
||||||
|
const update: Update<
|
||||||
|
MarketDepth_market,
|
||||||
|
MarketDepthSubscription_marketDepthUpdate
|
||||||
|
> = (draft, delta, restart) => {
|
||||||
|
if (delta.market.id !== draft.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const sequenceNumber = Number(delta.sequenceNumber);
|
||||||
|
if (sequenceNumber <= sequenceNumbers[delta.market.id]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sequenceNumber - 1 !== sequenceNumbers[delta.market.id]) {
|
||||||
|
sequenceNumbers[delta.market.id] = 0;
|
||||||
|
restart(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sequenceNumbers[delta.market.id] = sequenceNumber;
|
||||||
|
if (delta.buy) {
|
||||||
|
draft.depth.buy = updateLevels(draft.depth.buy ?? [], delta.buy);
|
||||||
|
}
|
||||||
|
if (delta.sell) {
|
||||||
|
draft.depth.sell = updateLevels(draft.depth.sell ?? [], delta.sell);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getData = (responseData: MarketDepth) => {
|
||||||
|
if (responseData.market?.id) {
|
||||||
|
sequenceNumbers[responseData.market.id] = Number(
|
||||||
|
responseData.market.depth.sequenceNumber
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return responseData.market;
|
||||||
|
};
|
||||||
|
const getDelta = (subscriptionData: MarketDepthSubscription) =>
|
||||||
|
subscriptionData.marketDepthUpdate;
|
||||||
|
|
||||||
|
export const marketDepthDataProvider = makeDataProvider(
|
||||||
|
MARKET_DEPTH_QUERY,
|
||||||
|
MARKET_DEPTH_SUBSCRIPTION_QUERY,
|
||||||
|
update,
|
||||||
|
getData,
|
||||||
|
getDelta
|
||||||
|
);
|
57
libs/market-depth/src/lib/market-depth-mock.ts
Normal file
57
libs/market-depth/src/lib/market-depth-mock.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import type {
|
||||||
|
MarketDepth_market,
|
||||||
|
MarketDepth_market_depth_sell,
|
||||||
|
MarketDepth_market_depth_buy,
|
||||||
|
} from './__generated__/MarketDepth';
|
||||||
|
|
||||||
|
const depthRow = (
|
||||||
|
price: number
|
||||||
|
): MarketDepth_market_depth_sell | MarketDepth_market_depth_buy => {
|
||||||
|
return {
|
||||||
|
__typename: 'PriceLevel',
|
||||||
|
price: price.toString(),
|
||||||
|
volume: Math.round(Math.random() * 100).toString(),
|
||||||
|
numberOfOrders: Math.round(Math.random() * 20).toString(),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const sell = (
|
||||||
|
price: number,
|
||||||
|
numberOfRecords: number
|
||||||
|
): MarketDepth_market_depth_sell[] => {
|
||||||
|
const distance = Math.random() * price * 0.1;
|
||||||
|
return new Array(numberOfRecords)
|
||||||
|
.fill(null)
|
||||||
|
.map(() => depthRow(price + Math.round(Math.random() * distance)));
|
||||||
|
};
|
||||||
|
|
||||||
|
const buy = (
|
||||||
|
price: number,
|
||||||
|
numberOfRecords: number
|
||||||
|
): MarketDepth_market_depth_buy[] => {
|
||||||
|
const distance = Math.random() * price * 0.1;
|
||||||
|
return new Array(numberOfRecords)
|
||||||
|
.fill(null)
|
||||||
|
.map(() => depthRow(price - Math.round(Math.random() * distance)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMockedData = (id?: string): MarketDepth_market => ({
|
||||||
|
__typename: 'Market',
|
||||||
|
id: id || '',
|
||||||
|
decimalPlaces: 2,
|
||||||
|
// "positionDecimalPlaces": 0,
|
||||||
|
data: {
|
||||||
|
__typename: 'MarketData',
|
||||||
|
midPrice: '0',
|
||||||
|
},
|
||||||
|
depth: {
|
||||||
|
__typename: 'MarketDepth',
|
||||||
|
lastTrade: {
|
||||||
|
__typename: 'Trade',
|
||||||
|
price: '12350',
|
||||||
|
},
|
||||||
|
sell: sell(12350 * 0.99, 100),
|
||||||
|
buy: buy(12350, 100),
|
||||||
|
sequenceNumber: '118118448',
|
||||||
|
},
|
||||||
|
});
|
31
libs/market-depth/src/lib/orderbook-container.tsx
Normal file
31
libs/market-depth/src/lib/orderbook-container.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { OrderbookManager } from './orderbook-manager';
|
||||||
|
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
|
export const OrderbookContainer = ({ marketId }: { marketId: string }) => {
|
||||||
|
const [resolution, setResolution] = useState(1);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex gap-8">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setResolution(resolution * 10)}
|
||||||
|
appendIconName="minus"
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
Zoom out
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setResolution(Math.max(resolution / 10, 1))}
|
||||||
|
appendIconName="plus"
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
Zoom in
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<OrderbookManager resolution={resolution} marketId={marketId} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
302
libs/market-depth/src/lib/orderbook-data.spec.ts
Normal file
302
libs/market-depth/src/lib/orderbook-data.spec.ts
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
import {
|
||||||
|
compactData,
|
||||||
|
updateLevels,
|
||||||
|
updateCompactedData,
|
||||||
|
} from './orderbook-data';
|
||||||
|
import type { OrderbookData } from './orderbook-data';
|
||||||
|
import type { MarketDepth_market_depth_sell } from './__generated__/MarketDepth';
|
||||||
|
import type {
|
||||||
|
MarketDepthSubscription_marketDepthUpdate_sell,
|
||||||
|
MarketDepthSubscription_marketDepthUpdate_buy,
|
||||||
|
} from './__generated__/MarketDepthSubscription';
|
||||||
|
|
||||||
|
describe('compactData', () => {
|
||||||
|
const numberOfRows = 100;
|
||||||
|
const middle = 1000;
|
||||||
|
const sell: MarketDepth_market_depth_sell[] = new Array(numberOfRows)
|
||||||
|
.fill(null)
|
||||||
|
.map((n, i) => ({
|
||||||
|
__typename: 'PriceLevel',
|
||||||
|
volume: i.toString(),
|
||||||
|
price: (middle + numberOfRows - i).toString(),
|
||||||
|
numberOfOrders: i.toString(),
|
||||||
|
}));
|
||||||
|
const buy: MarketDepth_market_depth_sell[] = new Array(numberOfRows)
|
||||||
|
.fill(null)
|
||||||
|
.map((n, i) => ({
|
||||||
|
__typename: 'PriceLevel',
|
||||||
|
volume: (numberOfRows - 1 - i).toString(),
|
||||||
|
price: (middle - i).toString(),
|
||||||
|
numberOfOrders: (numberOfRows - i).toString(),
|
||||||
|
}));
|
||||||
|
it('groups data by price and resolution', () => {
|
||||||
|
expect(compactData(sell, buy, 1).length).toEqual(200);
|
||||||
|
expect(compactData(sell, buy, 5).length).toEqual(41);
|
||||||
|
expect(compactData(sell, buy, 10).length).toEqual(21);
|
||||||
|
});
|
||||||
|
it('counts cumulative vol', () => {
|
||||||
|
const orderbookData = compactData(sell, buy, 10);
|
||||||
|
expect(orderbookData[0].cumulativeVol.ask).toEqual(4950);
|
||||||
|
expect(orderbookData[0].cumulativeVol.bid).toEqual(0);
|
||||||
|
expect(orderbookData[10].cumulativeVol.ask).toEqual(390);
|
||||||
|
expect(orderbookData[10].cumulativeVol.bid).toEqual(579);
|
||||||
|
expect(orderbookData[orderbookData.length - 1].cumulativeVol.bid).toEqual(
|
||||||
|
4950
|
||||||
|
);
|
||||||
|
expect(orderbookData[orderbookData.length - 1].cumulativeVol.ask).toEqual(
|
||||||
|
0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('stores volume by level', () => {
|
||||||
|
const orderbookData = compactData(sell, buy, 10);
|
||||||
|
expect(orderbookData[0].askByLevel).toEqual({
|
||||||
|
'1095': 5,
|
||||||
|
'1096': 4,
|
||||||
|
'1097': 3,
|
||||||
|
'1098': 2,
|
||||||
|
'1099': 1,
|
||||||
|
'1100': 0,
|
||||||
|
});
|
||||||
|
expect(orderbookData[orderbookData.length - 1].bidByLevel).toEqual({
|
||||||
|
'901': 0,
|
||||||
|
'902': 1,
|
||||||
|
'903': 2,
|
||||||
|
'904': 3,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates relative data', () => {
|
||||||
|
const orderbookData = compactData(sell, buy, 10);
|
||||||
|
expect(orderbookData[0].cumulativeVol.relativeAsk).toEqual(100);
|
||||||
|
expect(orderbookData[0].cumulativeVol.relativeBid).toEqual(0);
|
||||||
|
expect(orderbookData[0].relativeAskVol).toEqual(2);
|
||||||
|
expect(orderbookData[0].relativeBidVol).toEqual(0);
|
||||||
|
expect(orderbookData[10].cumulativeVol.relativeAsk).toEqual(8);
|
||||||
|
expect(orderbookData[10].cumulativeVol.relativeBid).toEqual(12);
|
||||||
|
expect(orderbookData[10].relativeAskVol).toEqual(44);
|
||||||
|
expect(orderbookData[10].relativeBidVol).toEqual(66);
|
||||||
|
expect(orderbookData[orderbookData.length - 1].relativeAskVol).toEqual(0);
|
||||||
|
expect(orderbookData[orderbookData.length - 1].relativeBidVol).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateLevels', () => {
|
||||||
|
const levels: MarketDepth_market_depth_sell[] = new Array(10)
|
||||||
|
.fill(null)
|
||||||
|
.map((n, i) => ({
|
||||||
|
__typename: 'PriceLevel',
|
||||||
|
volume: ((i + 1) * 10).toString(),
|
||||||
|
price: ((i + 1) * 10).toString(),
|
||||||
|
numberOfOrders: ((i + 1) * 10).toString(),
|
||||||
|
}));
|
||||||
|
it('updates, removes and adds new items', () => {
|
||||||
|
const removeFirstRow: MarketDepthSubscription_marketDepthUpdate_sell = {
|
||||||
|
__typename: 'PriceLevel',
|
||||||
|
price: '10',
|
||||||
|
volume: '0',
|
||||||
|
numberOfOrders: '0',
|
||||||
|
};
|
||||||
|
updateLevels(levels, [removeFirstRow]);
|
||||||
|
expect(levels[0].price).toEqual('20');
|
||||||
|
updateLevels(levels, [removeFirstRow]);
|
||||||
|
expect(levels[0].price).toEqual('20');
|
||||||
|
expect(updateLevels([], [removeFirstRow])).toEqual([]);
|
||||||
|
const addFirstRow: MarketDepthSubscription_marketDepthUpdate_sell = {
|
||||||
|
__typename: 'PriceLevel',
|
||||||
|
price: '10',
|
||||||
|
volume: '10',
|
||||||
|
numberOfOrders: '10',
|
||||||
|
};
|
||||||
|
updateLevels(levels, [addFirstRow]);
|
||||||
|
expect(levels[0].price).toEqual('10');
|
||||||
|
const addBeforeLastRow: MarketDepthSubscription_marketDepthUpdate_sell = {
|
||||||
|
__typename: 'PriceLevel',
|
||||||
|
price: '95',
|
||||||
|
volume: '95',
|
||||||
|
numberOfOrders: '95',
|
||||||
|
};
|
||||||
|
updateLevels(levels, [addBeforeLastRow]);
|
||||||
|
expect(levels[levels.length - 2].price).toEqual('95');
|
||||||
|
const addAtTheEnd: MarketDepthSubscription_marketDepthUpdate_sell = {
|
||||||
|
__typename: 'PriceLevel',
|
||||||
|
price: '115',
|
||||||
|
volume: '115',
|
||||||
|
numberOfOrders: '115',
|
||||||
|
};
|
||||||
|
updateLevels(levels, [addAtTheEnd]);
|
||||||
|
expect(levels[levels.length - 1].price).toEqual('115');
|
||||||
|
const updateLastRow: MarketDepthSubscription_marketDepthUpdate_sell = {
|
||||||
|
__typename: 'PriceLevel',
|
||||||
|
price: '115',
|
||||||
|
volume: '116',
|
||||||
|
numberOfOrders: '115',
|
||||||
|
};
|
||||||
|
updateLevels(levels, [updateLastRow]);
|
||||||
|
expect(levels[levels.length - 1]).toEqual(updateLastRow);
|
||||||
|
expect(updateLevels([], [updateLastRow])).toEqual([updateLastRow]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateCompactedData', () => {
|
||||||
|
const orderbookData: OrderbookData[] = [
|
||||||
|
{
|
||||||
|
price: '120',
|
||||||
|
cumulativeVol: {
|
||||||
|
ask: 50,
|
||||||
|
relativeAsk: 100,
|
||||||
|
bid: 0,
|
||||||
|
relativeBid: 0,
|
||||||
|
},
|
||||||
|
askByLevel: {
|
||||||
|
'121': 10,
|
||||||
|
},
|
||||||
|
bidByLevel: {},
|
||||||
|
ask: 10,
|
||||||
|
bid: 0,
|
||||||
|
relativeAskVol: 25,
|
||||||
|
relativeBidVol: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
price: '100',
|
||||||
|
cumulativeVol: {
|
||||||
|
ask: 40,
|
||||||
|
relativeAsk: 80,
|
||||||
|
bid: 40,
|
||||||
|
relativeBid: 80,
|
||||||
|
},
|
||||||
|
askByLevel: {
|
||||||
|
'101': 10,
|
||||||
|
'102': 30,
|
||||||
|
},
|
||||||
|
bidByLevel: {
|
||||||
|
'99': 10,
|
||||||
|
'98': 30,
|
||||||
|
},
|
||||||
|
ask: 40,
|
||||||
|
bid: 40,
|
||||||
|
relativeAskVol: 100,
|
||||||
|
relativeBidVol: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
price: '80',
|
||||||
|
cumulativeVol: {
|
||||||
|
ask: 0,
|
||||||
|
relativeAsk: 0,
|
||||||
|
bid: 50,
|
||||||
|
relativeBid: 100,
|
||||||
|
},
|
||||||
|
askByLevel: {},
|
||||||
|
bidByLevel: {
|
||||||
|
'79': 10,
|
||||||
|
},
|
||||||
|
ask: 0,
|
||||||
|
bid: 10,
|
||||||
|
relativeAskVol: 0,
|
||||||
|
relativeBidVol: 25,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const resolution = 10;
|
||||||
|
|
||||||
|
it('update volume', () => {
|
||||||
|
const sell: MarketDepthSubscription_marketDepthUpdate_sell = {
|
||||||
|
__typename: 'PriceLevel',
|
||||||
|
price: '120',
|
||||||
|
volume: '10',
|
||||||
|
numberOfOrders: '10',
|
||||||
|
};
|
||||||
|
const buy: MarketDepthSubscription_marketDepthUpdate_buy = {
|
||||||
|
__typename: 'PriceLevel',
|
||||||
|
price: '80',
|
||||||
|
volume: '10',
|
||||||
|
numberOfOrders: '10',
|
||||||
|
};
|
||||||
|
const updatedData = updateCompactedData(
|
||||||
|
orderbookData,
|
||||||
|
[sell],
|
||||||
|
[buy],
|
||||||
|
resolution
|
||||||
|
);
|
||||||
|
expect(updatedData[0].ask).toEqual(20);
|
||||||
|
expect(updatedData[0].askByLevel?.[120]).toEqual(10);
|
||||||
|
expect(updatedData[0].cumulativeVol.ask).toEqual(60);
|
||||||
|
expect(updatedData[2].bid).toEqual(20);
|
||||||
|
expect(updatedData[2].bidByLevel?.[80]).toEqual(10);
|
||||||
|
expect(updatedData[2].cumulativeVol.bid).toEqual(60);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('remove row', () => {
|
||||||
|
const sell: MarketDepthSubscription_marketDepthUpdate_sell = {
|
||||||
|
__typename: 'PriceLevel',
|
||||||
|
price: '121',
|
||||||
|
volume: '0',
|
||||||
|
numberOfOrders: '0',
|
||||||
|
};
|
||||||
|
const buy: MarketDepthSubscription_marketDepthUpdate_buy = {
|
||||||
|
__typename: 'PriceLevel',
|
||||||
|
price: '79',
|
||||||
|
volume: '0',
|
||||||
|
numberOfOrders: '0',
|
||||||
|
};
|
||||||
|
const updatedData = updateCompactedData(
|
||||||
|
orderbookData,
|
||||||
|
[sell],
|
||||||
|
[buy],
|
||||||
|
resolution
|
||||||
|
);
|
||||||
|
expect(updatedData.length).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('add new row at the end', () => {
|
||||||
|
const sell: MarketDepthSubscription_marketDepthUpdate_sell = {
|
||||||
|
__typename: 'PriceLevel',
|
||||||
|
price: '131',
|
||||||
|
volume: '5',
|
||||||
|
numberOfOrders: '5',
|
||||||
|
};
|
||||||
|
const buy: MarketDepthSubscription_marketDepthUpdate_buy = {
|
||||||
|
__typename: 'PriceLevel',
|
||||||
|
price: '59',
|
||||||
|
volume: '5',
|
||||||
|
numberOfOrders: '5',
|
||||||
|
};
|
||||||
|
const updatedData = updateCompactedData(
|
||||||
|
orderbookData,
|
||||||
|
[sell],
|
||||||
|
[buy],
|
||||||
|
resolution
|
||||||
|
);
|
||||||
|
expect(updatedData.length).toEqual(5);
|
||||||
|
expect(updatedData[0].price).toEqual('130');
|
||||||
|
expect(updatedData[0].cumulativeVol.ask).toEqual(55);
|
||||||
|
expect(updatedData[4].price).toEqual('60');
|
||||||
|
expect(updatedData[4].cumulativeVol.bid).toEqual(55);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('add new row in the middle', () => {
|
||||||
|
const sell: MarketDepthSubscription_marketDepthUpdate_sell = {
|
||||||
|
__typename: 'PriceLevel',
|
||||||
|
price: '111',
|
||||||
|
volume: '5',
|
||||||
|
numberOfOrders: '5',
|
||||||
|
};
|
||||||
|
const buy: MarketDepthSubscription_marketDepthUpdate_buy = {
|
||||||
|
__typename: 'PriceLevel',
|
||||||
|
price: '91',
|
||||||
|
volume: '5',
|
||||||
|
numberOfOrders: '5',
|
||||||
|
};
|
||||||
|
const updatedData = updateCompactedData(
|
||||||
|
orderbookData,
|
||||||
|
[sell],
|
||||||
|
[buy],
|
||||||
|
resolution
|
||||||
|
);
|
||||||
|
expect(updatedData.length).toEqual(5);
|
||||||
|
expect(updatedData[1].price).toEqual('110');
|
||||||
|
expect(updatedData[1].cumulativeVol.ask).toEqual(45);
|
||||||
|
expect(updatedData[0].cumulativeVol.ask).toEqual(55);
|
||||||
|
expect(updatedData[3].price).toEqual('90');
|
||||||
|
expect(updatedData[3].cumulativeVol.bid).toEqual(45);
|
||||||
|
expect(updatedData[4].cumulativeVol.bid).toEqual(55);
|
||||||
|
});
|
||||||
|
});
|
319
libs/market-depth/src/lib/orderbook-data.ts
Normal file
319
libs/market-depth/src/lib/orderbook-data.ts
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
import produce from 'immer';
|
||||||
|
import groupBy from 'lodash/groupBy';
|
||||||
|
import type {
|
||||||
|
MarketDepth_market_depth_sell,
|
||||||
|
MarketDepth_market_depth_buy,
|
||||||
|
} from './__generated__/MarketDepth';
|
||||||
|
import type {
|
||||||
|
MarketDepthSubscription_marketDepthUpdate_sell,
|
||||||
|
MarketDepthSubscription_marketDepthUpdate_buy,
|
||||||
|
} from './__generated__/MarketDepthSubscription';
|
||||||
|
|
||||||
|
export interface CumulativeVol {
|
||||||
|
bid: number;
|
||||||
|
relativeBid?: number;
|
||||||
|
ask: number;
|
||||||
|
relativeAsk?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderbookData {
|
||||||
|
price: string;
|
||||||
|
bid: number;
|
||||||
|
bidByLevel: Record<string, number>;
|
||||||
|
relativeBidVol?: number;
|
||||||
|
ask: number;
|
||||||
|
askByLevel: Record<string, number>;
|
||||||
|
relativeAskVol?: number;
|
||||||
|
cumulativeVol: CumulativeVol;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getGroupPrice = (price: string, resolution: number) => {
|
||||||
|
const p = BigInt(price);
|
||||||
|
const r = BigInt(resolution);
|
||||||
|
let groupPrice = (p / r) * r;
|
||||||
|
if (p - groupPrice >= resolution / 2) {
|
||||||
|
groupPrice += BigInt(resolution);
|
||||||
|
}
|
||||||
|
return groupPrice.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMaxVolumes = (orderbookData: OrderbookData[]) => ({
|
||||||
|
bid: Math.max(...orderbookData.map((data) => data.bid)),
|
||||||
|
ask: Math.max(...orderbookData.map((data) => data.ask)),
|
||||||
|
cumulativeVol: Math.max(
|
||||||
|
orderbookData[0]?.cumulativeVol.ask,
|
||||||
|
orderbookData[orderbookData.length - 1]?.cumulativeVol.bid
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
// round instead of ceil so we will not show 0 if value if different than 0
|
||||||
|
const toPercentValue = (value?: number) => Math.ceil((value ?? 0) * 100);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary Updates relativeAskVol, relativeBidVol, cumulativeVol.relativeAsk, cumulativeVol.relativeBid
|
||||||
|
*/
|
||||||
|
const updateRelativeData = (data: OrderbookData[]) => {
|
||||||
|
const { bid, ask, cumulativeVol } = getMaxVolumes(data);
|
||||||
|
data.forEach((data) => {
|
||||||
|
data.relativeAskVol = toPercentValue(data.ask / ask);
|
||||||
|
data.relativeBidVol = toPercentValue(data.bid / bid);
|
||||||
|
data.cumulativeVol.relativeAsk = toPercentValue(
|
||||||
|
data.cumulativeVol.ask / cumulativeVol
|
||||||
|
);
|
||||||
|
data.cumulativeVol.relativeBid = toPercentValue(
|
||||||
|
data.cumulativeVol.bid / cumulativeVol
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createData = (
|
||||||
|
price: string,
|
||||||
|
volume = 0,
|
||||||
|
dataType?: 'sell' | 'buy'
|
||||||
|
): OrderbookData => ({
|
||||||
|
price,
|
||||||
|
ask: dataType === 'sell' ? volume : 0,
|
||||||
|
bid: dataType === 'buy' ? volume : 0,
|
||||||
|
cumulativeVol: {
|
||||||
|
ask: dataType === 'sell' ? volume : 0,
|
||||||
|
bid: dataType === 'buy' ? volume : 0,
|
||||||
|
},
|
||||||
|
askByLevel: dataType === 'sell' ? { [price]: volume } : {},
|
||||||
|
bidByLevel: dataType === 'buy' ? { [price]: volume } : {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mapRawData =
|
||||||
|
(dataType: 'sell' | 'buy') =>
|
||||||
|
(
|
||||||
|
data:
|
||||||
|
| MarketDepth_market_depth_sell
|
||||||
|
| MarketDepthSubscription_marketDepthUpdate_sell
|
||||||
|
| MarketDepth_market_depth_buy
|
||||||
|
| MarketDepthSubscription_marketDepthUpdate_buy
|
||||||
|
): OrderbookData =>
|
||||||
|
createData(data.price, Number(data.volume), dataType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @summary merges sell amd buy data, orders by price desc, group by price level, counts cumulative and relative values
|
||||||
|
*/
|
||||||
|
export const compactData = (
|
||||||
|
sell:
|
||||||
|
| (
|
||||||
|
| MarketDepth_market_depth_sell
|
||||||
|
| MarketDepthSubscription_marketDepthUpdate_sell
|
||||||
|
)[]
|
||||||
|
| null,
|
||||||
|
buy:
|
||||||
|
| (
|
||||||
|
| MarketDepth_market_depth_buy
|
||||||
|
| MarketDepthSubscription_marketDepthUpdate_buy
|
||||||
|
)[]
|
||||||
|
| null,
|
||||||
|
resolution: number
|
||||||
|
) => {
|
||||||
|
// map raw sell data to OrderbookData
|
||||||
|
const askOrderbookData = [...(sell ?? [])].map<OrderbookData>(
|
||||||
|
mapRawData('sell')
|
||||||
|
);
|
||||||
|
// map raw buy data to OrderbookData
|
||||||
|
const bidOrderbookData = [...(buy ?? [])].map<OrderbookData>(
|
||||||
|
mapRawData('buy')
|
||||||
|
);
|
||||||
|
|
||||||
|
// group by price level
|
||||||
|
const groupedByLevel = groupBy<OrderbookData>(
|
||||||
|
[...askOrderbookData, ...bidOrderbookData],
|
||||||
|
(row) => getGroupPrice(row.price, resolution)
|
||||||
|
);
|
||||||
|
|
||||||
|
// create single OrderbookData from grouped OrderbookData[], sum volumes and atore volume by level
|
||||||
|
const orderbookData = Object.keys(groupedByLevel).reduce<OrderbookData[]>(
|
||||||
|
(rows, price) =>
|
||||||
|
rows.concat(
|
||||||
|
groupedByLevel[price].reduce<OrderbookData>(
|
||||||
|
(a, c) => ({
|
||||||
|
...a,
|
||||||
|
ask: a.ask + c.ask,
|
||||||
|
askByLevel: Object.assign(a.askByLevel, c.askByLevel),
|
||||||
|
bid: (a.bid ?? 0) + (c.bid ?? 0),
|
||||||
|
bidByLevel: Object.assign(a.bidByLevel, c.bidByLevel),
|
||||||
|
}),
|
||||||
|
createData(price)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
// order by price, it's safe to cast to number price diff sould not exceed Number.MAX_SAFE_INTEGER
|
||||||
|
orderbookData.sort((a, b) => Number(BigInt(b.price) - BigInt(a.price)));
|
||||||
|
// count cumulative volumes
|
||||||
|
if (orderbookData.length > 1) {
|
||||||
|
const maxIndex = orderbookData.length - 1;
|
||||||
|
for (let i = 0; i <= maxIndex; i++) {
|
||||||
|
orderbookData[i].cumulativeVol.bid =
|
||||||
|
orderbookData[i].bid +
|
||||||
|
(i !== 0 ? orderbookData[i - 1].cumulativeVol.bid : 0);
|
||||||
|
}
|
||||||
|
for (let i = maxIndex; i >= 0; i--) {
|
||||||
|
if (!orderbookData[i].cumulativeVol.ask) {
|
||||||
|
orderbookData[i].cumulativeVol.ask =
|
||||||
|
orderbookData[i].ask +
|
||||||
|
(i !== maxIndex ? orderbookData[i + 1].cumulativeVol.ask : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// count relative volumes
|
||||||
|
updateRelativeData(orderbookData);
|
||||||
|
return orderbookData;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param type
|
||||||
|
* @param draft
|
||||||
|
* @param delta
|
||||||
|
* @param resolution
|
||||||
|
* @param modifiedIndex
|
||||||
|
* @returns max (sell) or min (buy) modified index in draft data, mutates draft
|
||||||
|
*/
|
||||||
|
const partiallyUpdateCompactedData = (
|
||||||
|
dataType: 'sell' | 'buy',
|
||||||
|
draft: OrderbookData[],
|
||||||
|
delta:
|
||||||
|
| MarketDepthSubscription_marketDepthUpdate_sell
|
||||||
|
| MarketDepthSubscription_marketDepthUpdate_buy,
|
||||||
|
resolution: number,
|
||||||
|
modifiedIndex: number
|
||||||
|
) => {
|
||||||
|
const { price } = delta;
|
||||||
|
const volume = Number(delta.volume);
|
||||||
|
const groupPrice = getGroupPrice(price, resolution);
|
||||||
|
const volKey = dataType === 'sell' ? 'ask' : 'bid';
|
||||||
|
const oppositeVolKey = dataType === 'sell' ? 'bid' : 'ask';
|
||||||
|
const volByLevelKey = dataType === 'sell' ? 'askByLevel' : 'bidByLevel';
|
||||||
|
const resolveModifiedIndex = dataType === 'sell' ? Math.max : Math.min;
|
||||||
|
let index = draft.findIndex((data) => data.price === groupPrice);
|
||||||
|
if (index !== -1) {
|
||||||
|
modifiedIndex = resolveModifiedIndex(modifiedIndex, index);
|
||||||
|
draft[index][volKey] =
|
||||||
|
draft[index][volKey] - (draft[index][volByLevelKey][price] || 0) + volume;
|
||||||
|
draft[index][volByLevelKey][price] = volume;
|
||||||
|
} else {
|
||||||
|
const newData: OrderbookData = createData(groupPrice, volume, dataType);
|
||||||
|
index = draft.findIndex((data) => BigInt(data.price) < BigInt(groupPrice));
|
||||||
|
if (index !== -1) {
|
||||||
|
draft.splice(index, 0, newData);
|
||||||
|
newData.cumulativeVol[oppositeVolKey] =
|
||||||
|
draft[index + (groupPrice === 'sell' ? -1 : 1)].cumulativeVol[
|
||||||
|
oppositeVolKey
|
||||||
|
];
|
||||||
|
modifiedIndex = resolveModifiedIndex(modifiedIndex, index);
|
||||||
|
} else {
|
||||||
|
draft.push(newData);
|
||||||
|
modifiedIndex = draft.length - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return modifiedIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates OrderbookData[] with new data received from subscription - mutates input
|
||||||
|
*
|
||||||
|
* @param orderbookData
|
||||||
|
* @param sell
|
||||||
|
* @param buy
|
||||||
|
* @param resolution
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
export const updateCompactedData = (
|
||||||
|
orderbookData: OrderbookData[],
|
||||||
|
sell: MarketDepthSubscription_marketDepthUpdate_sell[] | null,
|
||||||
|
buy: MarketDepthSubscription_marketDepthUpdate_buy[] | null,
|
||||||
|
resolution: number
|
||||||
|
) =>
|
||||||
|
produce(orderbookData, (draft) => {
|
||||||
|
let sellModifiedIndex = -1;
|
||||||
|
sell?.forEach((buy) => {
|
||||||
|
sellModifiedIndex = partiallyUpdateCompactedData(
|
||||||
|
'sell',
|
||||||
|
draft,
|
||||||
|
buy,
|
||||||
|
resolution,
|
||||||
|
sellModifiedIndex
|
||||||
|
);
|
||||||
|
});
|
||||||
|
let buyModifiedIndex = draft.length;
|
||||||
|
buy?.forEach((sell) => {
|
||||||
|
buyModifiedIndex = partiallyUpdateCompactedData(
|
||||||
|
'buy',
|
||||||
|
draft,
|
||||||
|
sell,
|
||||||
|
resolution,
|
||||||
|
buyModifiedIndex
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// update cummulative ask only below hihgest modified price level
|
||||||
|
if (sellModifiedIndex !== -1) {
|
||||||
|
for (let i = Math.min(sellModifiedIndex, draft.length - 2); i >= 0; i--) {
|
||||||
|
draft[i].cumulativeVol.ask =
|
||||||
|
draft[i + 1].cumulativeVol.ask + draft[i].ask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// update cummulative bid only above lowest modified price level
|
||||||
|
if (buyModifiedIndex !== draft.length) {
|
||||||
|
for (
|
||||||
|
let i = Math.max(buyModifiedIndex, 1), l = draft.length;
|
||||||
|
i < l;
|
||||||
|
i++
|
||||||
|
) {
|
||||||
|
draft[i].cumulativeVol.bid =
|
||||||
|
draft[i - 1].cumulativeVol.bid + draft[i].bid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let index = 0;
|
||||||
|
// remove levels that do not have any volume
|
||||||
|
while (index < draft.length) {
|
||||||
|
if (!draft[index].ask && !draft[index].bid) {
|
||||||
|
draft.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// count relative volumes
|
||||||
|
updateRelativeData(draft);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates raw data with new data received from subscription - mutates input
|
||||||
|
* @param levels
|
||||||
|
* @param updates
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const updateLevels = (
|
||||||
|
levels: (MarketDepth_market_depth_buy | MarketDepth_market_depth_sell)[],
|
||||||
|
updates: (
|
||||||
|
| MarketDepthSubscription_marketDepthUpdate_buy
|
||||||
|
| MarketDepthSubscription_marketDepthUpdate_sell
|
||||||
|
)[]
|
||||||
|
) => {
|
||||||
|
updates.forEach((update) => {
|
||||||
|
let index = levels.findIndex((level) => level.price === update.price);
|
||||||
|
if (index !== -1) {
|
||||||
|
if (update.volume === '0') {
|
||||||
|
levels.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
Object.assign(levels[index], update);
|
||||||
|
}
|
||||||
|
} else if (update.volume !== '0') {
|
||||||
|
index = levels.findIndex(
|
||||||
|
(level) => Number(level.price) > Number(update.price)
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
levels.splice(index, 0, update);
|
||||||
|
} else {
|
||||||
|
levels.push(update);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return levels;
|
||||||
|
};
|
74
libs/market-depth/src/lib/orderbook-manager.tsx
Normal file
74
libs/market-depth/src/lib/orderbook-manager.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import throttle from 'lodash/throttle';
|
||||||
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { Orderbook } from './orderbook';
|
||||||
|
import { useDataProvider } from '@vegaprotocol/react-helpers';
|
||||||
|
import { marketDepthDataProvider } from './market-depth-data-provider';
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import type { MarketDepthSubscription_marketDepthUpdate } from './__generated__/MarketDepthSubscription';
|
||||||
|
import { compactData, updateCompactedData } from './orderbook-data';
|
||||||
|
import type { OrderbookData } from './orderbook-data';
|
||||||
|
|
||||||
|
interface OrderbookManagerProps {
|
||||||
|
marketId: string;
|
||||||
|
resolution: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OrderbookManager = ({
|
||||||
|
marketId,
|
||||||
|
resolution,
|
||||||
|
}: OrderbookManagerProps) => {
|
||||||
|
const variables = useMemo(() => ({ marketId }), [marketId]);
|
||||||
|
const resolutionRef = useRef(resolution);
|
||||||
|
const [orderbookData, setOrderbookData] = useState<OrderbookData[] | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const dataRef = useRef<OrderbookData[] | null>(null);
|
||||||
|
const setOrderbookDataThrottled = useRef(throttle(setOrderbookData, 1000));
|
||||||
|
|
||||||
|
const update = useCallback(
|
||||||
|
(delta: MarketDepthSubscription_marketDepthUpdate) => {
|
||||||
|
if (!dataRef.current) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
dataRef.current = updateCompactedData(
|
||||||
|
dataRef.current,
|
||||||
|
delta.sell,
|
||||||
|
delta.buy,
|
||||||
|
resolutionRef.current
|
||||||
|
);
|
||||||
|
setOrderbookDataThrottled.current(dataRef.current);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data, error, loading, flush } = useDataProvider(
|
||||||
|
marketDepthDataProvider,
|
||||||
|
update,
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!data) {
|
||||||
|
dataRef.current = null;
|
||||||
|
setOrderbookData(dataRef.current);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dataRef.current = compactData(data.depth.sell, data.depth.buy, resolution);
|
||||||
|
setOrderbookData(dataRef.current);
|
||||||
|
}, [data, resolution]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
resolutionRef.current = resolution;
|
||||||
|
flush();
|
||||||
|
}, [resolution, flush]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||||
|
<Orderbook
|
||||||
|
data={orderbookData}
|
||||||
|
decimalPlaces={data?.decimalPlaces ?? 0}
|
||||||
|
/>
|
||||||
|
</AsyncRenderer>
|
||||||
|
);
|
||||||
|
};
|
50
libs/market-depth/src/lib/orderbook-row.tsx
Normal file
50
libs/market-depth/src/lib/orderbook-row.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
PriceCell,
|
||||||
|
Vol,
|
||||||
|
CumulativeVol,
|
||||||
|
formatNumber,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
|
interface OrderbookRowProps {
|
||||||
|
bid: number;
|
||||||
|
relativeBidVol?: number;
|
||||||
|
price: string;
|
||||||
|
ask: number;
|
||||||
|
relativeAskVol?: number;
|
||||||
|
cumulativeRelativeAsk?: number;
|
||||||
|
cumulativeRelativeBid?: number;
|
||||||
|
decimalPlaces: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OrderbookRow = React.memo(
|
||||||
|
({
|
||||||
|
bid,
|
||||||
|
relativeBidVol,
|
||||||
|
price,
|
||||||
|
ask,
|
||||||
|
relativeAskVol,
|
||||||
|
decimalPlaces,
|
||||||
|
cumulativeRelativeAsk,
|
||||||
|
cumulativeRelativeBid,
|
||||||
|
}: OrderbookRowProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Vol value={bid} relativeValue={relativeBidVol} type="bid" />
|
||||||
|
<PriceCell
|
||||||
|
value={BigInt(price)}
|
||||||
|
valueFormatted={formatNumber(price, decimalPlaces)}
|
||||||
|
/>
|
||||||
|
<Vol value={ask} relativeValue={relativeAskVol} type="ask" />
|
||||||
|
<CumulativeVol
|
||||||
|
relativeAsk={cumulativeRelativeAsk}
|
||||||
|
relativeBid={cumulativeRelativeBid}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
OrderbookRow.displayName = 'OrderbookRow';
|
||||||
|
|
||||||
|
export default OrderbookRow;
|
10
libs/market-depth/src/lib/orderbook.spec.tsx
Normal file
10
libs/market-depth/src/lib/orderbook.spec.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { render } from '@testing-library/react';
|
||||||
|
|
||||||
|
import Orderbook from './orderbook';
|
||||||
|
|
||||||
|
describe('Orderbook', () => {
|
||||||
|
it('should render successfully', () => {
|
||||||
|
const { baseElement } = render(<Orderbook data={null} decimalPlaces={4} />);
|
||||||
|
expect(baseElement).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
36
libs/market-depth/src/lib/orderbook.tsx
Normal file
36
libs/market-depth/src/lib/orderbook.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { OrderbookRow } from './orderbook-row';
|
||||||
|
import type { OrderbookData } from './orderbook-data';
|
||||||
|
|
||||||
|
interface OrderbookProps {
|
||||||
|
data: OrderbookData[] | null;
|
||||||
|
decimalPlaces: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Orderbook = ({ data, decimalPlaces }: OrderbookProps) => (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-4 gap-4 border-b-1 text-ui-small mb-2 pb-2">
|
||||||
|
<div>{t('Bid Vol')}</div>
|
||||||
|
<div>{t('Price')}</div>
|
||||||
|
<div>{t('Ask Vol')}</div>
|
||||||
|
<div>{t('Cumulative Vol')}</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-4 gap-4 text-right text-ui-small">
|
||||||
|
{data?.map((data) => (
|
||||||
|
<OrderbookRow
|
||||||
|
key={data.price}
|
||||||
|
price={data.price}
|
||||||
|
decimalPlaces={decimalPlaces}
|
||||||
|
bid={data.bid}
|
||||||
|
relativeBidVol={data.relativeBidVol}
|
||||||
|
cumulativeRelativeBid={data.cumulativeVol.relativeBid}
|
||||||
|
ask={data.ask}
|
||||||
|
relativeAskVol={data.relativeAskVol}
|
||||||
|
cumulativeRelativeAsk={data.cumulativeVol.relativeAsk}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Orderbook;
|
1
libs/market-depth/src/setup-tests.ts
Normal file
1
libs/market-depth/src/setup-tests.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
@ -2,9 +2,12 @@
|
|||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "../../dist/out-tsc",
|
"outDir": "../../dist/out-tsc",
|
||||||
"declaration": true,
|
"types": ["node"]
|
||||||
"types": []
|
|
||||||
},
|
},
|
||||||
|
"files": [
|
||||||
|
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
|
||||||
|
"../../node_modules/@nrwl/react/typings/image.d.ts"
|
||||||
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"**/*.spec.ts",
|
"**/*.spec.ts",
|
||||||
"**/*.test.ts",
|
"**/*.test.ts",
|
@ -1,6 +1,6 @@
|
|||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import type { ValueFormatterParams } from 'ag-grid-community';
|
import type { ValueFormatterParams } from 'ag-grid-community';
|
||||||
import { PriceCell, formatNumber, t } from '@vegaprotocol/react-helpers';
|
import { PriceFlashCell, formatNumber, t } from '@vegaprotocol/react-helpers';
|
||||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||||
import type {
|
import type {
|
||||||
Markets_markets,
|
Markets_markets,
|
||||||
@ -37,7 +37,7 @@ export const MarketListTable = forwardRef<AgGridReact, MarketListTableProps>(
|
|||||||
onRowClicked={({ data }: { data: Markets_markets }) =>
|
onRowClicked={({ data }: { data: Markets_markets }) =>
|
||||||
onRowClicked(data.id)
|
onRowClicked(data.id)
|
||||||
}
|
}
|
||||||
components={{ PriceCell }}
|
components={{ PriceFlashCell }}
|
||||||
>
|
>
|
||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
headerName={t('Market')}
|
headerName={t('Market')}
|
||||||
@ -58,7 +58,7 @@ export const MarketListTable = forwardRef<AgGridReact, MarketListTableProps>(
|
|||||||
headerName={t('Best bid')}
|
headerName={t('Best bid')}
|
||||||
field="data.bestBidPrice"
|
field="data.bestBidPrice"
|
||||||
type="rightAligned"
|
type="rightAligned"
|
||||||
cellRenderer="PriceCell"
|
cellRenderer="PriceFlashCell"
|
||||||
valueFormatter={({ value, data }: ValueFormatterParams) =>
|
valueFormatter={({ value, data }: ValueFormatterParams) =>
|
||||||
formatNumber(value, data.decimalPlaces)
|
formatNumber(value, data.decimalPlaces)
|
||||||
}
|
}
|
||||||
@ -70,13 +70,13 @@ export const MarketListTable = forwardRef<AgGridReact, MarketListTableProps>(
|
|||||||
valueFormatter={({ value, data }: ValueFormatterParams) =>
|
valueFormatter={({ value, data }: ValueFormatterParams) =>
|
||||||
formatNumber(value, data.decimalPlaces)
|
formatNumber(value, data.decimalPlaces)
|
||||||
}
|
}
|
||||||
cellRenderer="PriceCell"
|
cellRenderer="PriceFlashCell"
|
||||||
/>
|
/>
|
||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
headerName={t('Mark price')}
|
headerName={t('Mark price')}
|
||||||
field="data.markPrice"
|
field="data.markPrice"
|
||||||
type="rightAligned"
|
type="rightAligned"
|
||||||
cellRenderer="PriceCell"
|
cellRenderer="PriceFlashCell"
|
||||||
valueFormatter={({ value, data }: ValueFormatterParams) =>
|
valueFormatter={({ value, data }: ValueFormatterParams) =>
|
||||||
formatNumber(value, data.decimalPlaces)
|
formatNumber(value, data.decimalPlaces)
|
||||||
}
|
}
|
||||||
|
@ -53,15 +53,13 @@ export const MarketsContainer = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||||
{(data) => (
|
<MarketListTable
|
||||||
<MarketListTable
|
ref={gridRef}
|
||||||
ref={gridRef}
|
data={data}
|
||||||
data={data}
|
onRowClicked={(id) =>
|
||||||
onRowClicked={(id) =>
|
push(`${pathname}/${id}?portfolio=orders&trade=orderbook`)
|
||||||
push(`${pathname}/${id}?portfolio=orders&trade=orderbook`)
|
}
|
||||||
}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AsyncRenderer>
|
</AsyncRenderer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -73,7 +73,7 @@ export const OrderListManager = ({ partyId }: OrderListManagerProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer loading={loading} error={error} data={orders}>
|
<AsyncRenderer loading={loading} error={error} data={orders}>
|
||||||
{(data) => <OrderList ref={gridRef} data={data} />}
|
<OrderList ref={gridRef} data={data} />
|
||||||
</AsyncRenderer>
|
</AsyncRenderer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -55,7 +55,7 @@ export const PositionsManager = ({ partyId }: PositionsManagerProps) => {
|
|||||||
>(positionsDataProvider, update, variables);
|
>(positionsDataProvider, update, variables);
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||||
{(data) => <PositionsTable ref={gridRef} data={data} />}
|
<PositionsTable ref={gridRef} data={data} />
|
||||||
</AsyncRenderer>
|
</AsyncRenderer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import type { ValueFormatterParams } from 'ag-grid-community';
|
import type { ValueFormatterParams } from 'ag-grid-community';
|
||||||
import {
|
import {
|
||||||
PriceCell,
|
PriceFlashCell,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
volumePrefix,
|
volumePrefix,
|
||||||
addDecimal,
|
addDecimal,
|
||||||
@ -77,7 +77,7 @@ export const PositionsTable = forwardRef<AgGridReact, PositionsTableProps>(
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
components={{ PriceCell }}
|
components={{ PriceFlashCell }}
|
||||||
>
|
>
|
||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
headerName={t('Market')}
|
headerName={t('Market')}
|
||||||
@ -95,7 +95,7 @@ export const PositionsTable = forwardRef<AgGridReact, PositionsTableProps>(
|
|||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
headerName={t('Average Entry Price')}
|
headerName={t('Average Entry Price')}
|
||||||
field="averageEntryPrice"
|
field="averageEntryPrice"
|
||||||
cellRenderer="PriceCell"
|
cellRenderer="PriceFlashCell"
|
||||||
valueFormatter={({
|
valueFormatter={({
|
||||||
value,
|
value,
|
||||||
data,
|
data,
|
||||||
@ -107,7 +107,7 @@ export const PositionsTable = forwardRef<AgGridReact, PositionsTableProps>(
|
|||||||
headerName={t('Mark Price')}
|
headerName={t('Mark Price')}
|
||||||
field="market.data.markPrice"
|
field="market.data.markPrice"
|
||||||
type="rightAligned"
|
type="rightAligned"
|
||||||
cellRenderer="PriceCell"
|
cellRenderer="PriceFlashCell"
|
||||||
valueFormatter={({
|
valueFormatter={({
|
||||||
value,
|
value,
|
||||||
data,
|
data,
|
||||||
@ -134,7 +134,7 @@ export const PositionsTable = forwardRef<AgGridReact, PositionsTableProps>(
|
|||||||
valueFormatter={({ value }: ValueFormatterParams) =>
|
valueFormatter={({ value }: ValueFormatterParams) =>
|
||||||
volumePrefix(value)
|
volumePrefix(value)
|
||||||
}
|
}
|
||||||
cellRenderer="PriceCell"
|
cellRenderer="PriceFlashCell"
|
||||||
/>
|
/>
|
||||||
</AgGrid>
|
</AgGrid>
|
||||||
);
|
);
|
||||||
|
@ -3,6 +3,13 @@ import { useApolloClient } from '@apollo/client';
|
|||||||
import type { OperationVariables } from '@apollo/client';
|
import type { OperationVariables } from '@apollo/client';
|
||||||
import type { Subscribe } from '../lib/generic-data-provider';
|
import type { Subscribe } from '../lib/generic-data-provider';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param dataProvider subscribe function created by makeDataProvider
|
||||||
|
* @param update optional function called on each delta received in subscription, if returns true updated data will be not passed from hook (component handles updates internally)
|
||||||
|
* @param variables optional
|
||||||
|
* @returns state: data, loading, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
|
||||||
|
*/
|
||||||
export function useDataProvider<Data, Delta>(
|
export function useDataProvider<Data, Delta>(
|
||||||
dataProvider: Subscribe<Data, Delta>,
|
dataProvider: Subscribe<Data, Delta>,
|
||||||
update?: (delta: Delta) => boolean,
|
update?: (delta: Delta) => boolean,
|
||||||
@ -12,12 +19,26 @@ export function useDataProvider<Data, Delta>(
|
|||||||
const [data, setData] = useState<Data | null>(null);
|
const [data, setData] = useState<Data | null>(null);
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [error, setError] = useState<Error | undefined>(undefined);
|
const [error, setError] = useState<Error | undefined>(undefined);
|
||||||
|
const flushRef = useRef<(() => void) | undefined>(undefined);
|
||||||
|
const restartRef = useRef<((force?: boolean) => void) | undefined>(undefined);
|
||||||
const initialized = useRef<boolean>(false);
|
const initialized = useRef<boolean>(false);
|
||||||
|
const flush = useCallback(() => {
|
||||||
|
if (flushRef.current) {
|
||||||
|
flushRef.current();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
const restart = useCallback((force = false) => {
|
||||||
|
if (restartRef.current) {
|
||||||
|
restartRef.current(force);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
const callback = useCallback(
|
const callback = useCallback(
|
||||||
({ data, error, loading, delta }) => {
|
({ data, error, loading, delta }) => {
|
||||||
setError(error);
|
setError(error);
|
||||||
setLoading(loading);
|
setLoading(loading);
|
||||||
if (!error && !loading) {
|
if (!error && !loading) {
|
||||||
|
// if update function returns true it means that component handles updates
|
||||||
|
// component can use flush() which will call callback without delta and cause data state update
|
||||||
if (!initialized.current || !delta || !update || !update(delta)) {
|
if (!initialized.current || !delta || !update || !update(delta)) {
|
||||||
initialized.current = true;
|
initialized.current = true;
|
||||||
setData(data);
|
setData(data);
|
||||||
@ -27,7 +48,14 @@ export function useDataProvider<Data, Delta>(
|
|||||||
[update]
|
[update]
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return dataProvider(callback, client, variables);
|
const { unsubscribe, flush, restart } = dataProvider(
|
||||||
|
callback,
|
||||||
|
client,
|
||||||
|
variables
|
||||||
|
);
|
||||||
|
flushRef.current = flush;
|
||||||
|
restartRef.current = restart;
|
||||||
|
return unsubscribe;
|
||||||
}, [client, initialized, dataProvider, callback, variables]);
|
}, [client, initialized, dataProvider, callback, variables]);
|
||||||
return { data, loading, error };
|
return { data, loading, error, flush, restart };
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
export * from './lib/context';
|
|
||||||
export * from './lib/format';
|
|
||||||
export * from './lib/grid';
|
|
||||||
export * from './lib/storage';
|
|
||||||
export * from './lib/generic-data-provider';
|
|
||||||
export * from './lib/i18n';
|
|
||||||
export * from './lib/ethereum-error';
|
|
||||||
export * from './lib/validate';
|
|
||||||
export * from './lib/remove-0x';
|
|
||||||
|
|
||||||
export * from './hooks';
|
export * from './hooks';
|
||||||
|
export * from './lib/context';
|
||||||
|
export * from './lib/ethereum-error';
|
||||||
|
export * from './lib/format';
|
||||||
|
export * from './lib/generic-data-provider';
|
||||||
|
export * from './lib/grid';
|
||||||
|
export * from './lib/i18n';
|
||||||
|
export * from './lib/remove-0x';
|
||||||
|
export * from './lib/storage';
|
||||||
|
export * from './lib/validate';
|
||||||
|
@ -3,11 +3,11 @@ import memoize from 'lodash/memoize';
|
|||||||
import { getUserLocale } from './utils';
|
import { getUserLocale } from './utils';
|
||||||
|
|
||||||
export function addDecimal(
|
export function addDecimal(
|
||||||
value: string,
|
value: string | number,
|
||||||
decimals: number,
|
decimals: number,
|
||||||
decimalPrecision = decimals
|
decimalPrecision = decimals
|
||||||
): string {
|
): string {
|
||||||
if (!decimals) return value;
|
if (!decimals) return value.toString();
|
||||||
return new BigNumber(value || 0)
|
return new BigNumber(value || 0)
|
||||||
.dividedBy(Math.pow(10, decimals))
|
.dividedBy(Math.pow(10, decimals))
|
||||||
.toFixed(decimalPrecision);
|
.toFixed(decimalPrecision);
|
||||||
@ -28,7 +28,7 @@ export const getNumberFormat = memoize(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const formatNumber = (
|
export const formatNumber = (
|
||||||
rawValue: string,
|
rawValue: string | number,
|
||||||
decimalPlaces: number,
|
decimalPlaces: number,
|
||||||
formatDecimals: number = decimalPlaces
|
formatDecimals: number = decimalPlaces
|
||||||
) => {
|
) => {
|
||||||
|
@ -23,14 +23,18 @@ export interface Subscribe<Data, Delta> {
|
|||||||
callback: UpdateCallback<Data, Delta>,
|
callback: UpdateCallback<Data, Delta>,
|
||||||
client: ApolloClient<object>,
|
client: ApolloClient<object>,
|
||||||
variables?: OperationVariables
|
variables?: OperationVariables
|
||||||
): () => void;
|
): {
|
||||||
|
unsubscribe: () => void;
|
||||||
|
restart: (force?: boolean) => void;
|
||||||
|
flush: () => void;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type Query<Result> = DocumentNode | TypedDocumentNode<Result, any>;
|
type Query<Result> = DocumentNode | TypedDocumentNode<Result, any>;
|
||||||
|
|
||||||
interface Update<Data, Delta> {
|
export interface Update<Data, Delta> {
|
||||||
(draft: Draft<Data>, delta: Delta): void;
|
(draft: Draft<Data>, delta: Delta, restart: (force?: boolean) => void): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetData<QueryData, Data> {
|
interface GetData<QueryData, Data> {
|
||||||
@ -41,6 +45,14 @@ interface GetDelta<SubscriptionData, Delta> {
|
|||||||
(subscriptionData: SubscriptionData): Delta;
|
(subscriptionData: SubscriptionData): Delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param subscriptionQuery query that will beused for subscription
|
||||||
|
* @param update function that will be execued on each onNext, it should update data base on delta, it can restart data provider
|
||||||
|
* @param getData transforms received query data to format that will be stored in data provider
|
||||||
|
* @param getDelta transforms delta data to format that will be stored in data provider
|
||||||
|
* @param fetchPolicy
|
||||||
|
* @returns subscribe function
|
||||||
|
*/
|
||||||
function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
||||||
query: Query<QueryData>,
|
query: Query<QueryData>,
|
||||||
subscriptionQuery: Query<SubscriptionData>,
|
subscriptionQuery: Query<SubscriptionData>,
|
||||||
@ -49,7 +61,9 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
|||||||
getDelta: GetDelta<SubscriptionData, Delta>,
|
getDelta: GetDelta<SubscriptionData, Delta>,
|
||||||
fetchPolicy: FetchPolicy = 'no-cache'
|
fetchPolicy: FetchPolicy = 'no-cache'
|
||||||
): Subscribe<Data, Delta> {
|
): Subscribe<Data, Delta> {
|
||||||
|
// list of callbacks passed through subscribe call
|
||||||
const callbacks: UpdateCallback<Data, Delta>[] = [];
|
const callbacks: UpdateCallback<Data, Delta>[] = [];
|
||||||
|
// subscription is started before inital query, all deltas that will arrive before inital query response are put on queue
|
||||||
const updateQueue: Delta[] = [];
|
const updateQueue: Delta[] = [];
|
||||||
|
|
||||||
let variables: OperationVariables | undefined = undefined;
|
let variables: OperationVariables | undefined = undefined;
|
||||||
@ -59,6 +73,7 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
|||||||
let client: ApolloClient<object> | undefined = undefined;
|
let client: ApolloClient<object> | undefined = undefined;
|
||||||
let subscription: Subscription | undefined = undefined;
|
let subscription: Subscription | undefined = undefined;
|
||||||
|
|
||||||
|
// notify single callback about current state, delta is passes optionally only if notify was invoked onNext
|
||||||
const notify = (callback: UpdateCallback<Data, Delta>, delta?: Delta) => {
|
const notify = (callback: UpdateCallback<Data, Delta>, delta?: Delta) => {
|
||||||
callback({
|
callback({
|
||||||
data,
|
data,
|
||||||
@ -68,12 +83,64 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// notify all callbacks
|
||||||
const notifyAll = (delta?: Delta) => {
|
const notifyAll = (delta?: Delta) => {
|
||||||
callbacks.forEach((callback) => notify(callback, delta));
|
callbacks.forEach((callback) => notify(callback, delta));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const initalFetch = async () => {
|
||||||
|
if (!client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await client.query<QueryData>({
|
||||||
|
query,
|
||||||
|
variables,
|
||||||
|
fetchPolicy,
|
||||||
|
});
|
||||||
|
data = getData(res.data);
|
||||||
|
// if there was some updates received from subscription during initial query loading apply them on just reveived data
|
||||||
|
if (data && updateQueue && updateQueue.length > 0) {
|
||||||
|
data = produce(data, (draft) => {
|
||||||
|
while (updateQueue.length) {
|
||||||
|
const delta = updateQueue.shift();
|
||||||
|
if (delta) {
|
||||||
|
update(draft, delta, restart);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// if error will occur data provider stops subscription
|
||||||
|
error = e as Error;
|
||||||
|
if (subscription) {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
}
|
||||||
|
subscription = undefined;
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
notifyAll();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// restart function is passed to update and as a returned by subscribe function
|
||||||
|
const restart = (hard = false) => {
|
||||||
|
if (loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// hard reset on demand or when there is no apollo subscription yet
|
||||||
|
if (hard || !subscription) {
|
||||||
|
reset();
|
||||||
|
initialize();
|
||||||
|
} else {
|
||||||
|
loading = true;
|
||||||
|
error = undefined;
|
||||||
|
initalFetch();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const initialize = async () => {
|
const initialize = async () => {
|
||||||
if (subscription) {
|
if (subscription || loading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loading = true;
|
loading = true;
|
||||||
@ -88,64 +155,50 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
|||||||
variables,
|
variables,
|
||||||
fetchPolicy,
|
fetchPolicy,
|
||||||
})
|
})
|
||||||
.subscribe(({ data: subscriptionData }) => {
|
.subscribe(
|
||||||
if (!subscriptionData) {
|
({ data: subscriptionData }) => {
|
||||||
return;
|
if (!subscriptionData) {
|
||||||
}
|
|
||||||
const delta = getDelta(subscriptionData);
|
|
||||||
if (loading || !data) {
|
|
||||||
updateQueue.push(delta);
|
|
||||||
} else {
|
|
||||||
const newData = produce(data, (draft) => {
|
|
||||||
update(draft, delta);
|
|
||||||
});
|
|
||||||
if (newData === data) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
data = newData;
|
const delta = getDelta(subscriptionData);
|
||||||
notifyAll(delta);
|
if (loading || !data) {
|
||||||
}
|
updateQueue.push(delta);
|
||||||
});
|
} else {
|
||||||
try {
|
const newData = produce(data, (draft) => {
|
||||||
const res = await client.query<QueryData>({
|
update(draft, delta, restart);
|
||||||
query,
|
});
|
||||||
variables,
|
if (newData === data) {
|
||||||
fetchPolicy,
|
return;
|
||||||
});
|
|
||||||
data = getData(res.data);
|
|
||||||
if (data && updateQueue && updateQueue.length > 0) {
|
|
||||||
data = produce(data, (draft) => {
|
|
||||||
while (updateQueue.length) {
|
|
||||||
const delta = updateQueue.shift();
|
|
||||||
if (delta) {
|
|
||||||
update(draft, delta);
|
|
||||||
}
|
}
|
||||||
|
data = newData;
|
||||||
|
notifyAll(delta);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
}
|
() => restart()
|
||||||
} catch (e) {
|
);
|
||||||
error = e as Error;
|
await initalFetch();
|
||||||
subscription.unsubscribe();
|
|
||||||
subscription = undefined;
|
|
||||||
} finally {
|
|
||||||
loading = false;
|
|
||||||
notifyAll();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
if (subscription) {
|
||||||
|
subscription.unsubscribe();
|
||||||
|
subscription = undefined;
|
||||||
|
}
|
||||||
|
data = null;
|
||||||
|
error = undefined;
|
||||||
|
loading = false;
|
||||||
|
notifyAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
// remove callback from list, and unsubscribe if there is no more callbacks registered
|
||||||
const unsubscribe = (callback: UpdateCallback<Data, Delta>) => {
|
const unsubscribe = (callback: UpdateCallback<Data, Delta>) => {
|
||||||
callbacks.splice(callbacks.indexOf(callback), 1);
|
callbacks.splice(callbacks.indexOf(callback), 1);
|
||||||
if (callbacks.length === 0) {
|
if (callbacks.length === 0) {
|
||||||
if (subscription) {
|
reset();
|
||||||
subscription.unsubscribe();
|
|
||||||
subscription = undefined;
|
|
||||||
}
|
|
||||||
data = null;
|
|
||||||
error = undefined;
|
|
||||||
loading = false;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//
|
||||||
return (callback, c, v) => {
|
return (callback, c, v) => {
|
||||||
callbacks.push(callback);
|
callbacks.push(callback);
|
||||||
if (callbacks.length === 1) {
|
if (callbacks.length === 1) {
|
||||||
@ -155,10 +208,20 @@ function makeDataProviderInternal<QueryData, Data, SubscriptionData, Delta>(
|
|||||||
} else {
|
} else {
|
||||||
notify(callback);
|
notify(callback);
|
||||||
}
|
}
|
||||||
return () => unsubscribe(callback);
|
return {
|
||||||
|
unsubscribe: () => unsubscribe(callback),
|
||||||
|
restart,
|
||||||
|
flush: () => notify(callback),
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memoizes data provider instances using query variables as cache key
|
||||||
|
*
|
||||||
|
* @param fn
|
||||||
|
* @returns subscibe function
|
||||||
|
*/
|
||||||
const memoize = <Data, Delta>(
|
const memoize = <Data, Delta>(
|
||||||
fn: (variables?: OperationVariables) => Subscribe<Data, Delta>
|
fn: (variables?: OperationVariables) => Subscribe<Data, Delta>
|
||||||
) => {
|
) => {
|
||||||
@ -177,6 +240,30 @@ const memoize = <Data, Delta>(
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param query Query<QueryData>
|
||||||
|
* @param subscriptionQuery Query<SubscriptionData> query that will beused for subscription
|
||||||
|
* @param update Update<Data, Delta> function that will be execued on each onNext, it should update data base on delta, it can restart data provider
|
||||||
|
* @param getData transforms received query data to format that will be stored in data provider
|
||||||
|
* @param getDelta transforms delta data to format that will be stored in data provider
|
||||||
|
* @param fetchPolicy
|
||||||
|
* @returns Subscribe<Data, Delta> subscribe function
|
||||||
|
* @example
|
||||||
|
* const marketMidPriceProvider = makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
|
||||||
|
* gql`query MarketMidPrice($marketId: ID!) { market(id: $marketId) { data { midPrice } } }`,
|
||||||
|
* gql`subscription MarketMidPriceSubscription($marketId: ID!) { marketDepthUpdate(marketId: $marketId) { market { data { midPrice } } } }`,
|
||||||
|
* (draft: Draft<Data>, delta: Delta, restart: (force?: boolean) => void) => { draft.midPrice = delta.midPrice }
|
||||||
|
* (data:QueryData) => data.market.data.midPrice
|
||||||
|
* (delta:SubscriptionData) => delta.marketData.market
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* const { unsubscribe, flush, restart } = marketMidPriceProvider(
|
||||||
|
* ({ data, error, loading, delta }) => { ... },
|
||||||
|
* apolloClient,
|
||||||
|
* { id: '1fd726454fa1220038acbf6ff9ac701d8b8bf3f2d77c93a4998544471dc58747' }
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
*/
|
||||||
export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
|
export function makeDataProvider<QueryData, Data, SubscriptionData, Delta>(
|
||||||
query: Query<QueryData>,
|
query: Query<QueryData>,
|
||||||
subscriptionQuery: Query<SubscriptionData>,
|
subscriptionQuery: Query<SubscriptionData>,
|
||||||
|
67
libs/react-helpers/src/lib/grid/cumulative-vol-cell.tsx
Normal file
67
libs/react-helpers/src/lib/grid/cumulative-vol-cell.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { ICellRendererParams } from 'ag-grid-community';
|
||||||
|
|
||||||
|
import { BID_COLOR, ASK_COLOR } from './vol-cell';
|
||||||
|
|
||||||
|
const INTERSECT_COLOR = 'darkgray';
|
||||||
|
|
||||||
|
export interface CumulativeVolProps {
|
||||||
|
relativeAsk?: number;
|
||||||
|
relativeBid?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICumulativeVolCellProps extends ICellRendererParams {
|
||||||
|
value: CumulativeVolProps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CumulativeVol = React.memo(
|
||||||
|
({ relativeAsk, relativeBid }: CumulativeVolProps) => {
|
||||||
|
const bid = relativeBid ? (
|
||||||
|
<div
|
||||||
|
className="h-full absolute top-0 right-0"
|
||||||
|
style={{
|
||||||
|
width: `${relativeBid}%`,
|
||||||
|
backgroundColor:
|
||||||
|
relativeAsk && relativeAsk > relativeBid
|
||||||
|
? INTERSECT_COLOR
|
||||||
|
: BID_COLOR,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
) : null;
|
||||||
|
const ask = relativeAsk ? (
|
||||||
|
<div
|
||||||
|
className="h-full absolute top-0 left-0"
|
||||||
|
style={{
|
||||||
|
width: `${relativeAsk}%`,
|
||||||
|
backgroundColor:
|
||||||
|
relativeBid && relativeBid > relativeAsk
|
||||||
|
? INTERSECT_COLOR
|
||||||
|
: ASK_COLOR,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
) : null;
|
||||||
|
return (
|
||||||
|
<div className="h-full relative" data-testid="vol">
|
||||||
|
{relativeBid && relativeAsk && relativeBid > relativeAsk ? (
|
||||||
|
<>
|
||||||
|
{ask}
|
||||||
|
{bid}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{bid}
|
||||||
|
{ask}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
CumulativeVol.displayName = 'CumulativeVol';
|
||||||
|
|
||||||
|
export const CumulativeVolCell = ({ value }: ICumulativeVolCellProps) => (
|
||||||
|
<CumulativeVol {...value} />
|
||||||
|
);
|
||||||
|
|
||||||
|
CumulativeVolCell.displayName = 'CumulativeVolCell';
|
@ -1,3 +1,6 @@
|
|||||||
|
export * from './cumulative-vol-cell';
|
||||||
export * from './flash-cell';
|
export * from './flash-cell';
|
||||||
export * from './price-cell';
|
export * from './price-cell';
|
||||||
|
export * from './price-flash-cell';
|
||||||
export * from './summary-rows';
|
export * from './summary-rows';
|
||||||
|
export * from './vol-cell';
|
||||||
|
@ -2,20 +2,20 @@ import { render, screen } from '@testing-library/react';
|
|||||||
import '@testing-library/jest-dom/extend-expect';
|
import '@testing-library/jest-dom/extend-expect';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { PriceCell } from './price-cell';
|
import { PriceFlashCell } from './price-flash-cell';
|
||||||
|
|
||||||
describe('<PriceCell />', () => {
|
describe('<PriceFlashCell />', () => {
|
||||||
it('Displays formatted value', () => {
|
it('Displays formatted value', () => {
|
||||||
render(<PriceCell value={100} valueFormatted="100.00" />);
|
render(<PriceFlashCell value={100} valueFormatted="100.00" />);
|
||||||
expect(screen.getByTestId('price')).toHaveTextContent('100.00');
|
expect(screen.getByTestId('price')).toHaveTextContent('100.00');
|
||||||
});
|
});
|
||||||
it('Displays 0', () => {
|
it('Displays 0', () => {
|
||||||
render(<PriceCell value={0} valueFormatted="0.00" />);
|
render(<PriceFlashCell value={0} valueFormatted="0.00" />);
|
||||||
expect(screen.getByTestId('price')).toHaveTextContent('0.00');
|
expect(screen.getByTestId('price')).toHaveTextContent('0.00');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Displays - if value is not a number', () => {
|
it('Displays - if value is not a number', () => {
|
||||||
render(<PriceCell value={null} valueFormatted="" />);
|
render(<PriceFlashCell value={null} valueFormatted="" />);
|
||||||
expect(screen.getByTestId('price')).toHaveTextContent('-');
|
expect(screen.getByTestId('price')).toHaveTextContent('-');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,19 +1,23 @@
|
|||||||
import { FlashCell } from './flash-cell';
|
import React from 'react';
|
||||||
|
|
||||||
export interface IPriceCellProps {
|
export interface IPriceCellProps {
|
||||||
value: number | bigint | null | undefined;
|
value: number | bigint | null | undefined;
|
||||||
valueFormatted: string;
|
valueFormatted: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PriceCell = ({ value, valueFormatted }: IPriceCellProps) => {
|
export const PriceCell = React.memo(
|
||||||
if ((!value && value !== 0) || isNaN(Number(value))) {
|
({ value, valueFormatted }: IPriceCellProps) => {
|
||||||
return <span data-testid="price">-</span>;
|
if (
|
||||||
|
(!value && value !== 0) ||
|
||||||
|
(typeof value === 'number' && isNaN(Number(value)))
|
||||||
|
) {
|
||||||
|
return <span data-testid="price">-</span>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span className="font-mono relative" data-testid="price">
|
||||||
|
{valueFormatted}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return (
|
);
|
||||||
<span className="font-mono" data-testid="price">
|
|
||||||
<FlashCell value={Number(value)}>{valueFormatted}</FlashCell>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
PriceCell.displayName = 'PriceCell';
|
PriceCell.displayName = 'PriceCell';
|
||||||
|
21
libs/react-helpers/src/lib/grid/price-flash-cell.test.tsx
Normal file
21
libs/react-helpers/src/lib/grid/price-flash-cell.test.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom/extend-expect';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { PriceFlashCell } from './price-flash-cell';
|
||||||
|
|
||||||
|
describe('<PriceFlashCell />', () => {
|
||||||
|
it('Displays formatted value', () => {
|
||||||
|
render(<PriceFlashCell value={100} valueFormatted="100.00" />);
|
||||||
|
expect(screen.getByTestId('price')).toHaveTextContent('100.00');
|
||||||
|
});
|
||||||
|
it('Displays 0', () => {
|
||||||
|
render(<PriceFlashCell value={0} valueFormatted="0.00" />);
|
||||||
|
expect(screen.getByTestId('price')).toHaveTextContent('0.00');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Displays - if value is not a number', () => {
|
||||||
|
render(<PriceFlashCell value={null} valueFormatted="" />);
|
||||||
|
expect(screen.getByTestId('price')).toHaveTextContent('-');
|
||||||
|
});
|
||||||
|
});
|
22
libs/react-helpers/src/lib/grid/price-flash-cell.tsx
Normal file
22
libs/react-helpers/src/lib/grid/price-flash-cell.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { FlashCell } from './flash-cell';
|
||||||
|
|
||||||
|
export interface IPriceFlashCellProps {
|
||||||
|
value: number | bigint | null | undefined;
|
||||||
|
valueFormatted: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PriceFlashCell = ({
|
||||||
|
value,
|
||||||
|
valueFormatted,
|
||||||
|
}: IPriceFlashCellProps) => {
|
||||||
|
if ((!value && value !== 0) || isNaN(Number(value))) {
|
||||||
|
return <span data-testid="price">-</span>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<span className="font-mono" data-testid="price">
|
||||||
|
<FlashCell value={Number(value)}>{valueFormatted}</FlashCell>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PriceFlashCell.displayName = 'PriceFlashCell';
|
42
libs/react-helpers/src/lib/grid/vol-cell.tsx
Normal file
42
libs/react-helpers/src/lib/grid/vol-cell.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import type { ICellRendererParams } from 'ag-grid-community';
|
||||||
|
import { PriceCell } from './price-cell';
|
||||||
|
|
||||||
|
export interface VolProps {
|
||||||
|
value: number | bigint | null | undefined;
|
||||||
|
relativeValue?: number;
|
||||||
|
type: 'bid' | 'ask';
|
||||||
|
}
|
||||||
|
export interface IVolCellProps extends ICellRendererParams {
|
||||||
|
value: number | bigint | null | undefined;
|
||||||
|
valueFormatted: Omit<VolProps, 'value'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BID_COLOR = 'darkgreen';
|
||||||
|
export const ASK_COLOR = 'maroon';
|
||||||
|
|
||||||
|
export const Vol = React.memo(({ value, relativeValue, type }: VolProps) => {
|
||||||
|
if ((!value && value !== 0) || isNaN(Number(value))) {
|
||||||
|
return <div data-testid="vol">-</div>;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="relative" data-testid="vol">
|
||||||
|
<div
|
||||||
|
className="h-full absolute top-0 left-0"
|
||||||
|
style={{
|
||||||
|
width: relativeValue ? `${relativeValue}%` : '0%',
|
||||||
|
backgroundColor: type === 'bid' ? BID_COLOR : ASK_COLOR,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<PriceCell value={value} valueFormatted={value.toString()} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Vol.displayName = 'Vol';
|
||||||
|
|
||||||
|
export const VolCell = ({ value, valueFormatted }: IVolCellProps) => (
|
||||||
|
<Vol value={value} {...valueFormatted} />
|
||||||
|
);
|
||||||
|
|
||||||
|
VolCell.displayName = 'VolCell';
|
@ -50,9 +50,12 @@ export const TradesContainer = ({ marketId }: TradesContainerProps) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
<AsyncRenderer
|
||||||
{(data) => <TradesTable ref={gridRef} data={data} />}
|
loading={loading}
|
||||||
</AsyncRenderer>
|
error={error}
|
||||||
|
data={data}
|
||||||
|
render={(data) => <TradesTable ref={gridRef} data={data} />}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { Splash } from '../splash';
|
import { Splash } from '../splash';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactElement, ReactNode } from 'react';
|
||||||
|
|
||||||
interface AsyncRendererProps<T> {
|
interface AsyncRendererProps<T> {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: Error | undefined | null;
|
error: Error | undefined | null;
|
||||||
data: T | undefined;
|
data: T | undefined;
|
||||||
children: (data: T) => ReactNode;
|
children?: ReactElement | null;
|
||||||
|
render?: (data: T) => ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AsyncRenderer<T = object>({
|
export function AsyncRenderer<T = object>({
|
||||||
@ -13,6 +14,7 @@ export function AsyncRenderer<T = object>({
|
|||||||
error,
|
error,
|
||||||
data,
|
data,
|
||||||
children,
|
children,
|
||||||
|
render,
|
||||||
}: AsyncRendererProps<T>) {
|
}: AsyncRendererProps<T>) {
|
||||||
if (error) {
|
if (error) {
|
||||||
return <Splash>Something went wrong: {error.message}</Splash>;
|
return <Splash>Something went wrong: {error.message}</Splash>;
|
||||||
@ -25,6 +27,6 @@ export function AsyncRenderer<T = object>({
|
|||||||
if (!data) {
|
if (!data) {
|
||||||
return <Splash>No data</Splash>;
|
return <Splash>No data</Splash>;
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
return <>{children(data)}</>;
|
return <>{render ? render(data) : children}</>;
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
"@vegaprotocol/cypress": ["libs/cypress/src/index.ts"],
|
"@vegaprotocol/cypress": ["libs/cypress/src/index.ts"],
|
||||||
"@vegaprotocol/deal-ticket": ["libs/deal-ticket/src/index.ts"],
|
"@vegaprotocol/deal-ticket": ["libs/deal-ticket/src/index.ts"],
|
||||||
"@vegaprotocol/deposits": ["libs/deposits/src/index.ts"],
|
"@vegaprotocol/deposits": ["libs/deposits/src/index.ts"],
|
||||||
"@vegaprotocol/depth-chart": ["libs/depth-chart/src/index.ts"],
|
"@vegaprotocol/market-depth": ["libs/market-depth/src/index.ts"],
|
||||||
"@vegaprotocol/market-list": ["libs/market-list/src/index.ts"],
|
"@vegaprotocol/market-list": ["libs/market-list/src/index.ts"],
|
||||||
"@vegaprotocol/network-stats": ["libs/network-stats/src/index.ts"],
|
"@vegaprotocol/network-stats": ["libs/network-stats/src/index.ts"],
|
||||||
"@vegaprotocol/order-list": ["libs/order-list/src/index.ts"],
|
"@vegaprotocol/order-list": ["libs/order-list/src/index.ts"],
|
||||||
|
@ -6,9 +6,9 @@
|
|||||||
"cypress": "libs/cypress",
|
"cypress": "libs/cypress",
|
||||||
"deal-ticket": "libs/deal-ticket",
|
"deal-ticket": "libs/deal-ticket",
|
||||||
"deposits": "libs/deposits",
|
"deposits": "libs/deposits",
|
||||||
"depth-chart": "libs/depth-chart",
|
|
||||||
"explorer": "apps/explorer",
|
"explorer": "apps/explorer",
|
||||||
"explorer-e2e": "apps/explorer-e2e",
|
"explorer-e2e": "apps/explorer-e2e",
|
||||||
|
"market-depth": "libs/market-depth",
|
||||||
"market-list": "libs/market-list",
|
"market-list": "libs/market-list",
|
||||||
"network-stats": "libs/network-stats",
|
"network-stats": "libs/network-stats",
|
||||||
"order-list": "libs/order-list",
|
"order-list": "libs/order-list",
|
||||||
|
Loading…
Reference in New Issue
Block a user