Feat/129 pennant chart (#214)

* initial commit for adding chartt lib with pennant chart

* add pennant package, fix dynamic import of chart

* use updated pennant library

* Create separate chart and depth-chart libs

* Remove leftover generated files

* Use more targeted queries and subscriptions

* Fix jestConfig value for depth-chart

* Add jest-canvas-mock

* Refactor updateDepthUpdate function

* Add updateDpethUpdate test

* Add jest-canvas-mock to chart tests

* Avoid using any type in test

* Use correct casing for gql queries and subscriptions

* Make ButtonRadio generic in option value type

* Add padding and margin to chart container

* Remove unused subscriptions and methods from data source

* Use correct React imports

Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
This commit is contained in:
John Walley 2022-04-08 18:49:45 +01:00 committed by GitHub
parent dbd0514515
commit f721a21d0f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 2832 additions and 775 deletions

View File

@ -0,0 +1,38 @@
import { Button } from '@vegaprotocol/ui-toolkit';
interface ButtonRadioProps<T> {
name: string;
options: Array<{ value: T; text: string }>;
currentOption: T | null;
onSelect: (option: T) => void;
}
export const ButtonRadio = <T extends string>({
name,
options,
currentOption,
onSelect,
}: ButtonRadioProps<T>) => {
return (
<div className="flex gap-8">
{options.map((option) => {
const isSelected = option.value === currentOption;
return (
<Button
onClick={() => onSelect(option.value)}
className="flex-1"
variant={isSelected ? 'accent' : undefined}
data-testid={
isSelected
? `${name}-${option.value}-selected`
: `${name}-${option.value}`
}
key={option.value}
>
{option.text}
</Button>
);
})}
</div>
);
};

View File

@ -0,0 +1,39 @@
import { ButtonRadio } from './button-radio';
import { DepthChartContainer } from '@vegaprotocol/depth-chart';
import { TradingChartContainer } from '@vegaprotocol/chart';
import { useState } from 'react';
type ChartType = 'depth' | 'trading';
interface ChartContainerProps {
marketId: string;
}
export const ChartContainer = ({ marketId }: ChartContainerProps) => {
const [chartType, setChartType] = useState<ChartType>('trading');
return (
<div className="px-4 py-8 flex flex-col h-full">
<div className="mb-4">
<ButtonRadio
name="chart-type"
options={[
{ text: 'Trading view', value: 'trading' },
{ text: 'Depth', value: 'depth' },
]}
currentOption={chartType}
onSelect={(value) => {
setChartType(value);
}}
/>
</div>
<div className="flex-1">
{chartType === 'trading' ? (
<TradingChartContainer marketId={marketId} />
) : (
<DepthChartContainer marketId={marketId} />
)}
</div>
</div>
);
};

View File

@ -0,0 +1 @@
export { ChartContainer } from './chart-container';

View File

@ -5,6 +5,7 @@ import { useState } from 'react';
import { GridTab, GridTabs } from './grid-tabs';
import { DealTicketContainer } from '@vegaprotocol/deal-ticket';
import { OrderListContainer } from '@vegaprotocol/order-list';
import { ChartContainer } from '../../components/chart-container';
import { TradesContainer } from '@vegaprotocol/trades';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { PositionsContainer } from '@vegaprotocol/positions';
@ -12,11 +13,6 @@ import type { Market_market } from './__generated__/Market';
import { t } from '@vegaprotocol/react-helpers';
import { AccountsContainer } from '@vegaprotocol/accounts';
const Chart = () => (
<Splash>
<p>{t('Chart')}</p>
</Splash>
);
const Orderbook = () => (
<Splash>
<p>{t('Orderbook')}</p>
@ -24,7 +20,7 @@ const Orderbook = () => (
);
const TradingViews = {
Chart: Chart,
Chart: ChartContainer,
Ticket: DealTicketContainer,
Orderbook: Orderbook,
Orders: OrderListContainer,
@ -54,7 +50,7 @@ export const TradeGrid = ({ market }: TradeGridProps) => {
</h1>
</header>
<TradeGridChild className="col-start-1 col-end-2">
<TradingViews.Chart />
<TradingViews.Chart marketId={market.id} />
</TradeGridChild>
<TradeGridChild className="row-start-1 row-end-3">
<TradingViews.Ticket marketId={market.id} />

12
libs/chart/.babelrc Normal file
View File

@ -0,0 +1,12 @@
{
"presets": [
[
"@nrwl/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}

18
libs/chart/.eslintrc.json Normal file
View File

@ -0,0 +1,18 @@
{
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "__generated__"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

7
libs/chart/README.md Normal file
View File

@ -0,0 +1,7 @@
# chart
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test chart` to execute the unit tests via [Jest](https://jestjs.io).

10
libs/chart/jest.config.js Normal file
View File

@ -0,0 +1,10 @@
module.exports = {
displayName: 'chart',
preset: '../../jest.preset.js',
transform: {
'^.+\\.[tj]sx?$': 'babel-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/libs/chart',
setupFiles: ['jest-canvas-mock'],
};

4
libs/chart/package.json Normal file
View File

@ -0,0 +1,4 @@
{
"name": "@vegaprotocol/chart",
"version": "0.0.1"
}

43
libs/chart/project.json Normal file
View File

@ -0,0 +1,43 @@
{
"root": "libs/chart",
"sourceRoot": "libs/chart/src",
"projectType": "library",
"tags": [],
"targets": {
"build": {
"executor": "@nrwl/web:rollup",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/chart",
"tsConfig": "libs/chart/tsconfig.lib.json",
"project": "libs/chart/package.json",
"entryFile": "libs/chart/src/index.ts",
"external": ["react/jsx-runtime"],
"rollupConfig": "@nrwl/react/plugins/bundle-rollup",
"compiler": "babel",
"assets": [
{
"glob": "libs/chart/README.md",
"input": ".",
"output": "."
}
]
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/chart/**/*.{ts,tsx,js,jsx}"]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["coverage/libs/chart"],
"options": {
"jestConfig": "libs/chart/jest.config.js",
"passWithNoTests": true
}
}
}
}

1
libs/chart/src/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './lib/trading-chart';

View File

@ -0,0 +1,36 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL fragment: CandleFields
// ====================================================
export interface CandleFields {
__typename: "Candle";
/**
* RFC3339Nano formatted date and time for the candle
*/
datetime: string;
/**
* High price (uint64)
*/
high: string;
/**
* Low price (uint64)
*/
low: string;
/**
* Open price (uint64)
*/
open: string;
/**
* Close price (uint64)
*/
close: string;
/**
* Volume price (uint64)
*/
volume: string;
}

View File

@ -0,0 +1,108 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { Interval } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: Candles
// ====================================================
export interface Candles_market_tradableInstrument_instrument {
__typename: 'Instrument';
/**
* Uniquely identify an instrument across all instruments available on Vega (string)
*/
id: string;
/**
* Full and fairly descriptive name for the instrument
*/
name: string;
/**
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
*/
code: string;
}
export interface Candles_market_tradableInstrument {
__typename: 'TradableInstrument';
/**
* An instance of or reference to a fully specified instrument.
*/
instrument: Candles_market_tradableInstrument_instrument;
}
export interface Candles_market_candles {
__typename: 'Candle';
/**
* RFC3339Nano formatted date and time for the candle
*/
datetime: string;
/**
* High price (uint64)
*/
high: string;
/**
* Low price (uint64)
*/
low: string;
/**
* Open price (uint64)
*/
open: string;
/**
* Close price (uint64)
*/
close: string;
/**
* Volume price (uint64)
*/
volume: string;
}
export interface Candles_market {
__typename: 'Market';
/**
* Market ID
*/
id: string;
/**
* 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)
*
* Examples:
* Currency Balance decimalPlaces Real Balance
* GBP 100 0 GBP 100
* GBP 100 2 GBP 1.00
* GBP 100 4 GBP 0.01
* GBP 1 4 GBP 0.0001 ( 0.01p )
*
* GBX (pence) 100 0 GBP 1.00 (100p )
* GBX (pence) 100 2 GBP 0.01 ( 1p )
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
*/
decimalPlaces: number;
/**
* An instance of or reference to a tradable instrument.
*/
tradableInstrument: Candles_market_tradableInstrument;
/**
* Candles on a market, for the 'last' n candles, at 'interval' seconds as specified by params
*/
candles: (Candles_market_candles | null)[] | null;
}
export interface Candles {
/**
* An instrument that is trading on the VEGA network
*/
market: Candles_market | null;
}
export interface CandlesVariables {
marketId: string;
interval: Interval;
since: string;
}

View File

@ -0,0 +1,50 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { Interval } from '@vegaprotocol/types';
// ====================================================
// GraphQL subscription operation: CandlesSub
// ====================================================
export interface CandlesSub_candles {
__typename: "Candle";
/**
* RFC3339Nano formatted date and time for the candle
*/
datetime: string;
/**
* High price (uint64)
*/
high: string;
/**
* Low price (uint64)
*/
low: string;
/**
* Open price (uint64)
*/
open: string;
/**
* Close price (uint64)
*/
close: string;
/**
* Volume price (uint64)
*/
volume: string;
}
export interface CandlesSub {
/**
* Subscribe to the candles updates
*/
candles: CandlesSub_candles;
}
export interface CandlesSubVariables {
marketId: string;
interval: Interval;
}

View File

@ -0,0 +1,68 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: Chart
// ====================================================
export interface Chart_market_data_priceMonitoringBounds {
__typename: "PriceMonitoringBounds";
/**
* Minimum price that isn't currently breaching the specified price monitoring trigger
*/
minValidPrice: string;
/**
* Maximum price that isn't currently breaching the specified price monitoring trigger
*/
maxValidPrice: string;
/**
* Reference price used to calculate the valid price range
*/
referencePrice: string;
}
export interface Chart_market_data {
__typename: "MarketData";
/**
* A list of valid price ranges per associated trigger
*/
priceMonitoringBounds: Chart_market_data_priceMonitoringBounds[] | null;
}
export interface Chart_market {
__typename: "Market";
/**
* 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)
*
* Examples:
* Currency Balance decimalPlaces Real Balance
* GBP 100 0 GBP 100
* GBP 100 2 GBP 1.00
* GBP 100 4 GBP 0.01
* GBP 1 4 GBP 0.0001 ( 0.01p )
*
* GBX (pence) 100 0 GBP 1.00 (100p )
* GBX (pence) 100 2 GBP 0.01 ( 1p )
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
*/
decimalPlaces: number;
/**
* marketData for the given market
*/
data: Chart_market_data | null;
}
export interface Chart {
/**
* An instrument that is trading on the VEGA network
*/
market: Chart_market | null;
}
export interface ChartVariables {
marketId: string;
}

View File

@ -0,0 +1,235 @@
import type { ApolloClient } from '@apollo/client';
import { gql } from '@apollo/client';
import type { Candle, DataSource } from 'pennant';
import { Interval } from 'pennant';
import { addDecimal } from '@vegaprotocol/react-helpers';
import type { Chart, ChartVariables } from './__generated__/Chart';
import type { Candles, CandlesVariables } from './__generated__/Candles';
import type { CandleFields } from './__generated__/CandleFields';
import type {
CandlesSub,
CandlesSubVariables,
} from './__generated__/CandlesSub';
import type { Subscription } from 'zen-observable-ts';
export const CANDLE_FRAGMENT = gql`
fragment CandleFields on Candle {
datetime
high
low
open
close
volume
}
`;
export const CANDLES_QUERY = gql`
${CANDLE_FRAGMENT}
query Candles($marketId: ID!, $interval: Interval!, $since: String!) {
market(id: $marketId) {
id
decimalPlaces
tradableInstrument {
instrument {
id
name
code
}
}
candles(interval: $interval, since: $since) {
...CandleFields
}
}
}
`;
export const CANDLES_SUB = gql`
${CANDLE_FRAGMENT}
subscription CandlesSub($marketId: ID!, $interval: Interval!) {
candles(marketId: $marketId, interval: $interval) {
...CandleFields
}
}
`;
const CHART_QUERY = gql`
query Chart($marketId: ID!) {
market(id: $marketId) {
decimalPlaces
data {
priceMonitoringBounds {
minValidPrice
maxValidPrice
referencePrice
}
}
}
}
`;
const defaultConfig = {
decimalPlaces: 5,
supportedIntervals: [
Interval.I1D,
Interval.I6H,
Interval.I1H,
Interval.I15M,
Interval.I5M,
Interval.I1M,
],
priceMonitoringBounds: [],
};
/**
* A data access object that provides access to the Vega GraphQL API.
*/
export class VegaDataSource implements DataSource {
client: ApolloClient<object>;
marketId: string;
partyId: null | string;
_decimalPlaces = 0;
candlesSub: Subscription | null = null;
/**
* 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.
*/
get decimalPlaces(): number {
return this._decimalPlaces;
}
/**
*
* @param client - An ApolloClient instance.
* @param marketId - Market identifier.
* @param partyId - Party identifier.
*/
constructor(
client: ApolloClient<object>,
marketId: string,
partyId: null | string = null
) {
this.client = client;
this.marketId = marketId;
this.partyId = partyId;
}
/**
* Used by the charting library to initialize itself.
*/
async onReady() {
try {
const { data } = await this.client.query<Chart, ChartVariables>({
query: CHART_QUERY,
variables: {
marketId: this.marketId,
},
fetchPolicy: 'no-cache',
});
if (data && data.market && data.market.data) {
this._decimalPlaces = data.market.decimalPlaces;
return {
decimalPlaces: this._decimalPlaces,
supportedIntervals: [
Interval.I1D,
Interval.I6H,
Interval.I1H,
Interval.I15M,
Interval.I5M,
Interval.I1M,
],
priceMonitoringBounds:
data.market.data.priceMonitoringBounds?.map((bounds) => ({
maxValidPrice: Number(
addDecimal(bounds.maxValidPrice, this._decimalPlaces)
),
minValidPrice: Number(
addDecimal(bounds.minValidPrice, this._decimalPlaces)
),
referencePrice: Number(
addDecimal(bounds.referencePrice, this._decimalPlaces)
),
})) ?? [],
};
} else {
return defaultConfig;
}
} catch {
return defaultConfig;
}
}
/**
* Used by the charting library to get historical data.
*/
async query(interval: Interval, from: string) {
try {
const { data } = await this.client.query<Candles, CandlesVariables>({
query: CANDLES_QUERY,
variables: {
marketId: this.marketId,
interval,
since: from,
},
fetchPolicy: 'no-cache',
});
if (data && data.market && data.market.candles) {
const decimalPlaces = data.market.decimalPlaces;
const candles = data.market.candles
.filter((d): d is CandleFields => d !== null)
.map((d) => parseCandle(d, decimalPlaces));
return candles;
} else {
return [];
}
} catch (error) {
return [];
}
}
/**
* Used by the charting library to create a subscription to streaming data.
*/
subscribeData(
interval: Interval,
onSubscriptionData: (data: Candle) => void
) {
const res = this.client.subscribe<CandlesSub, CandlesSubVariables>({
query: CANDLES_SUB,
variables: { marketId: this.marketId, interval },
});
this.candlesSub = res.subscribe(({ data }) => {
if (data) {
const candle = parseCandle(data.candles, this.decimalPlaces);
onSubscriptionData(candle);
}
});
}
/**
* Used by the charting library to clean-up a subscription to streaming data.
*/
unsubscribeData() {
this.candlesSub && this.candlesSub.unsubscribe();
}
}
function parseCandle(candle: CandleFields, decimalPlaces: number): Candle {
return {
date: new Date(candle.datetime),
high: Number(addDecimal(candle.high, decimalPlaces)),
low: Number(addDecimal(candle.low, decimalPlaces)),
open: Number(addDecimal(candle.open, decimalPlaces)),
close: Number(addDecimal(candle.close, decimalPlaces)),
volume: Number(candle.volume),
};
}

View File

@ -0,0 +1,17 @@
import { TradingChartContainer } from './trading-chart';
import { render } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletContext } from '@vegaprotocol/wallet';
describe('TradingChart', () => {
it('should render successfully', () => {
const { baseElement } = render(
<MockedProvider>
<VegaWalletContext.Provider value={{} as never}>
<TradingChartContainer marketId={'market-id'} />
</VegaWalletContext.Provider>
</MockedProvider>
);
expect(baseElement).toBeTruthy();
});
});

View File

@ -0,0 +1,32 @@
import 'pennant/dist/style.css';
import { Chart as TradingChart, Interval } from 'pennant';
import { VegaDataSource } from './data-source';
import { useApolloClient } from '@apollo/client';
import { useContext, useMemo } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { ThemeContext } from '@vegaprotocol/react-helpers';
export type TradingChartContainerProps = {
marketId: string;
};
export const TradingChartContainer = ({
marketId,
}: TradingChartContainerProps) => {
const client = useApolloClient();
const { keypair } = useVegaWallet();
const theme = useContext(ThemeContext);
const dataSource = useMemo(() => {
return new VegaDataSource(client, marketId, keypair?.pub);
}, [client, marketId, keypair]);
return (
<TradingChart
dataSource={dataSource}
interval={Interval.I15M}
theme={theme}
/>
);
};

25
libs/chart/tsconfig.json Normal file
View File

@ -0,0 +1,25 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@ -0,0 +1,22 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": ["node"]
},
"files": [
"../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
"../../node_modules/@nrwl/react/typings/image.d.ts"
],
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx"
],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

View File

@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"**/*.test.ts",
"**/*.spec.ts",
"**/*.test.tsx",
"**/*.spec.tsx",
"**/*.test.js",
"**/*.spec.js",
"**/*.test.jsx",
"**/*.spec.jsx",
"**/*.d.ts"
]
}

12
libs/depth-chart/.babelrc Normal file
View File

@ -0,0 +1,12 @@
{
"presets": [
[
"@nrwl/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}

View File

@ -0,0 +1,18 @@
{
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*", "__generated__"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@ -0,0 +1,11 @@
# 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).

View File

@ -0,0 +1,10 @@
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'],
};

View File

@ -0,0 +1,4 @@
{
"name": "@vegaprotocol/depth-chart",
"version": "0.0.1"
}

View File

@ -0,0 +1,43 @@
{
"root": "libs/depth-chart",
"sourceRoot": "libs/depth-chart/src",
"projectType": "library",
"tags": [],
"targets": {
"build": {
"executor": "@nrwl/web:rollup",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/depth-chart",
"tsConfig": "libs/depth-chart/tsconfig.lib.json",
"project": "libs/depth-chart/package.json",
"entryFile": "libs/depth-chart/src/index.ts",
"external": ["react/jsx-runtime"],
"rollupConfig": "@nrwl/react/plugins/bundle-rollup",
"compiler": "babel",
"assets": [
{
"glob": "libs/depth-chart/README.md",
"input": ".",
"output": "."
}
]
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/depth-chart/**/*.{ts,tsx,js,jsx}"]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["coverage/libs/depth-chart"],
"options": {
"jestConfig": "libs/depth-chart/jest.config.js",
"passWithNoTests": true
}
}
}
}

View File

@ -0,0 +1 @@
export * from './lib/depth-chart';

View File

@ -0,0 +1,120 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: marketDepth
// ====================================================
export interface marketDepth_market_data {
__typename: "MarketData";
/**
* the arithmetic average of the best bid price and best offer price.
*/
midPrice: string;
}
export interface marketDepth_market_depth_lastTrade {
__typename: "Trade";
/**
* The price of the trade (probably initially the passive order price, other determination algorithms are possible though) (uint64)
*/
price: string;
}
export interface marketDepth_market_depth_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 marketDepth_market_depth_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 marketDepth_market_depth {
__typename: "MarketDepth";
/**
* Last trade for the given market (if available)
*/
lastTrade: marketDepth_market_depth_lastTrade | null;
/**
* Sell side price levels (if available)
*/
sell: marketDepth_market_depth_sell[] | null;
/**
* Buy side price levels (if available)
*/
buy: marketDepth_market_depth_buy[] | null;
/**
* Sequence number for the current snapshot of the market depth
*/
sequenceNumber: string;
}
export interface marketDepth_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
/**
* 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)
*
* Examples:
* Currency Balance decimalPlaces Real Balance
* GBP 100 0 GBP 100
* GBP 100 2 GBP 1.00
* GBP 100 4 GBP 0.01
* GBP 1 4 GBP 0.0001 ( 0.01p )
*
* GBX (pence) 100 0 GBP 1.00 (100p )
* GBX (pence) 100 2 GBP 0.01 ( 1p )
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
*/
decimalPlaces: number;
/**
* marketData for the given market
*/
data: marketDepth_market_data | null;
/**
* Current depth on the orderbook for this market
*/
depth: marketDepth_market_depth;
}
export interface marketDepth {
/**
* An instrument that is trading on the VEGA network
*/
market: marketDepth_market | null;
}
export interface marketDepthVariables {
marketId: string;
}

View File

@ -0,0 +1,67 @@
/* 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;
}

View File

@ -0,0 +1,91 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL subscription operation: marketDepthUpdateSubscribe
// ====================================================
export interface marketDepthUpdateSubscribe_marketDepthUpdate_market_data {
__typename: "MarketData";
/**
* the arithmetic average of the best bid price and best offer price.
*/
midPrice: string;
}
export interface marketDepthUpdateSubscribe_marketDepthUpdate_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
/**
* marketData for the given market
*/
data: marketDepthUpdateSubscribe_marketDepthUpdate_market_data | null;
}
export interface marketDepthUpdateSubscribe_marketDepthUpdate_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 marketDepthUpdateSubscribe_marketDepthUpdate_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 marketDepthUpdateSubscribe_marketDepthUpdate {
__typename: "MarketDepthUpdate";
/**
* Market id
*/
market: marketDepthUpdateSubscribe_marketDepthUpdate_market;
/**
* Sell side price levels (if available)
*/
sell: marketDepthUpdateSubscribe_marketDepthUpdate_sell[] | null;
/**
* Buy side price levels (if available)
*/
buy: marketDepthUpdateSubscribe_marketDepthUpdate_buy[] | null;
/**
* Sequence number for the current snapshot of the market depth
*/
sequenceNumber: string;
}
export interface marketDepthUpdateSubscribe {
/**
* Subscribe to price level market depth updates
*/
marketDepthUpdate: marketDepthUpdateSubscribe_marketDepthUpdate;
}
export interface marketDepthUpdateSubscribeVariables {
marketId: string;
}

View File

@ -0,0 +1,14 @@
import { DepthChartContainer } from './depth-chart';
import { render } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
describe('DepthChart', () => {
it('should render successfully', () => {
const { baseElement } = render(
<MockedProvider>
<DepthChartContainer marketId={'market-id'} />
</MockedProvider>
);
expect(baseElement).toBeTruthy();
});
});

View File

@ -0,0 +1,65 @@
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} />;
};

View File

@ -0,0 +1 @@
export * from './update-depth-update';

View File

@ -0,0 +1,110 @@
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',
},
},
};
}

View File

@ -0,0 +1,90 @@
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)),
};
}

View File

@ -0,0 +1,135 @@
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;
}

View File

@ -0,0 +1,53 @@
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
}
}
`;

View File

@ -0,0 +1,25 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": []
},
"exclude": [
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx"
],
"include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
}

View File

@ -0,0 +1,19 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"types": ["jest", "node"]
},
"include": [
"**/*.test.ts",
"**/*.spec.ts",
"**/*.test.tsx",
"**/*.spec.tsx",
"**/*.test.js",
"**/*.spec.js",
"**/*.test.jsx",
"**/*.spec.jsx",
"**/*.d.ts"
]
}

View File

@ -76,6 +76,18 @@ export enum DepositStatus {
Open = "Open",
}
/**
* The interval for trade candles when subscribing via VEGA graphql, default is I15M
*/
export enum Interval {
I15M = "I15M",
I1D = "I1D",
I1H = "I1H",
I1M = "I1M",
I5M = "I5M",
I6H = "I6H",
}
/**
* The current state of a market
*/

View File

@ -47,6 +47,7 @@
"lodash": "^4.17.21",
"next": "12.0.7",
"nx": "^13.8.3",
"pennant": "0.4.5",
"postcss": "^8.4.6",
"react": "17.0.2",
"react-copy-to-clipboard": "^5.0.4",
@ -112,6 +113,7 @@
"eslint-plugin-unicorn": "^41.0.0",
"husky": "^7.0.4",
"jest": "27.2.3",
"jest-canvas-mock": "^2.3.1",
"lint-staged": "^12.3.3",
"nx": "^13.8.3",
"prettier": "^2.5.1",

View File

@ -16,9 +16,11 @@
"baseUrl": ".",
"paths": {
"@vegaprotocol/accounts": ["libs/accounts/src/index.ts"],
"@vegaprotocol/chart": ["libs/chart/src/index.ts"],
"@vegaprotocol/cypress": ["libs/cypress/src/index.ts"],
"@vegaprotocol/deal-ticket": ["libs/deal-ticket/src/index.ts"],
"@vegaprotocol/deposits": ["libs/deposits/src/index.ts"],
"@vegaprotocol/depth-chart": ["libs/depth-chart/src/index.ts"],
"@vegaprotocol/market-list": ["libs/market-list/src/index.ts"],
"@vegaprotocol/network-stats": ["libs/network-stats/src/index.ts"],
"@vegaprotocol/order-list": ["libs/order-list/src/index.ts"],

View File

@ -2,9 +2,11 @@
"version": 2,
"projects": {
"accounts": "libs/accounts",
"chart": "libs/chart",
"cypress": "libs/cypress",
"deal-ticket": "libs/deal-ticket",
"deposits": "libs/deposits",
"depth-chart": "libs/depth-chart",
"explorer": "apps/explorer",
"explorer-e2e": "apps/explorer-e2e",
"market-list": "libs/market-list",

1886
yarn.lock

File diff suppressed because it is too large Load Diff