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:
parent
dbd0514515
commit
f721a21d0f
38
apps/trading/components/chart-container/button-radio.tsx
Normal file
38
apps/trading/components/chart-container/button-radio.tsx
Normal 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>
|
||||
);
|
||||
};
|
39
apps/trading/components/chart-container/chart-container.tsx
Normal file
39
apps/trading/components/chart-container/chart-container.tsx
Normal 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>
|
||||
);
|
||||
};
|
1
apps/trading/components/chart-container/index.ts
Normal file
1
apps/trading/components/chart-container/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { ChartContainer } from './chart-container';
|
@ -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
12
libs/chart/.babelrc
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nrwl/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
18
libs/chart/.eslintrc.json
Normal file
18
libs/chart/.eslintrc.json
Normal 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
7
libs/chart/README.md
Normal 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
10
libs/chart/jest.config.js
Normal 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
4
libs/chart/package.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "@vegaprotocol/chart",
|
||||
"version": "0.0.1"
|
||||
}
|
43
libs/chart/project.json
Normal file
43
libs/chart/project.json
Normal 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
1
libs/chart/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './lib/trading-chart';
|
36
libs/chart/src/lib/__generated__/CandleFields.ts
generated
Normal file
36
libs/chart/src/lib/__generated__/CandleFields.ts
generated
Normal 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;
|
||||
}
|
108
libs/chart/src/lib/__generated__/Candles.ts
generated
Normal file
108
libs/chart/src/lib/__generated__/Candles.ts
generated
Normal 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;
|
||||
}
|
50
libs/chart/src/lib/__generated__/CandlesSub.ts
generated
Normal file
50
libs/chart/src/lib/__generated__/CandlesSub.ts
generated
Normal 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;
|
||||
}
|
68
libs/chart/src/lib/__generated__/Chart.ts
generated
Normal file
68
libs/chart/src/lib/__generated__/Chart.ts
generated
Normal 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;
|
||||
}
|
235
libs/chart/src/lib/data-source.ts
Normal file
235
libs/chart/src/lib/data-source.ts
Normal 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),
|
||||
};
|
||||
}
|
17
libs/chart/src/lib/trading-chart.spec.tsx
Normal file
17
libs/chart/src/lib/trading-chart.spec.tsx
Normal 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();
|
||||
});
|
||||
});
|
32
libs/chart/src/lib/trading-chart.tsx
Normal file
32
libs/chart/src/lib/trading-chart.tsx
Normal 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
25
libs/chart/tsconfig.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
22
libs/chart/tsconfig.lib.json
Normal file
22
libs/chart/tsconfig.lib.json
Normal 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"]
|
||||
}
|
19
libs/chart/tsconfig.spec.json
Normal file
19
libs/chart/tsconfig.spec.json
Normal 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
12
libs/depth-chart/.babelrc
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nrwl/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
18
libs/depth-chart/.eslintrc.json
Normal file
18
libs/depth-chart/.eslintrc.json
Normal 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": {}
|
||||
}
|
||||
]
|
||||
}
|
11
libs/depth-chart/README.md
Normal file
11
libs/depth-chart/README.md
Normal 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).
|
10
libs/depth-chart/jest.config.js
Normal file
10
libs/depth-chart/jest.config.js
Normal 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'],
|
||||
};
|
4
libs/depth-chart/package.json
Normal file
4
libs/depth-chart/package.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "@vegaprotocol/depth-chart",
|
||||
"version": "0.0.1"
|
||||
}
|
43
libs/depth-chart/project.json
Normal file
43
libs/depth-chart/project.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
libs/depth-chart/src/index.ts
Normal file
1
libs/depth-chart/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './lib/depth-chart';
|
120
libs/depth-chart/src/lib/__generated__/marketDepth.ts
generated
Normal file
120
libs/depth-chart/src/lib/__generated__/marketDepth.ts
generated
Normal 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;
|
||||
}
|
67
libs/depth-chart/src/lib/__generated__/marketDepthSubscribe.ts
generated
Normal file
67
libs/depth-chart/src/lib/__generated__/marketDepthSubscribe.ts
generated
Normal 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;
|
||||
}
|
91
libs/depth-chart/src/lib/__generated__/marketDepthUpdateSubscribe.ts
generated
Normal file
91
libs/depth-chart/src/lib/__generated__/marketDepthUpdateSubscribe.ts
generated
Normal 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;
|
||||
}
|
14
libs/depth-chart/src/lib/depth-chart.spec.tsx
Normal file
14
libs/depth-chart/src/lib/depth-chart.spec.tsx
Normal 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();
|
||||
});
|
||||
});
|
65
libs/depth-chart/src/lib/depth-chart.tsx
Normal file
65
libs/depth-chart/src/lib/depth-chart.tsx
Normal 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} />;
|
||||
};
|
1
libs/depth-chart/src/lib/helpers/index.ts
Normal file
1
libs/depth-chart/src/lib/helpers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './update-depth-update';
|
110
libs/depth-chart/src/lib/helpers/update-depth-update.spec.ts
Normal file
110
libs/depth-chart/src/lib/helpers/update-depth-update.spec.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
90
libs/depth-chart/src/lib/helpers/update-depth-update.ts
Normal file
90
libs/depth-chart/src/lib/helpers/update-depth-update.ts
Normal 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)),
|
||||
};
|
||||
}
|
135
libs/depth-chart/src/lib/hooks/use-depth-update.ts
Normal file
135
libs/depth-chart/src/lib/hooks/use-depth-update.ts
Normal 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;
|
||||
}
|
53
libs/depth-chart/src/lib/queries/market-depth.ts
Normal file
53
libs/depth-chart/src/lib/queries/market-depth.ts
Normal 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
|
||||
}
|
||||
}
|
||||
`;
|
25
libs/depth-chart/tsconfig.json
Normal file
25
libs/depth-chart/tsconfig.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
19
libs/depth-chart/tsconfig.lib.json
Normal file
19
libs/depth-chart/tsconfig.lib.json
Normal 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"]
|
||||
}
|
19
libs/depth-chart/tsconfig.spec.json
Normal file
19
libs/depth-chart/tsconfig.spec.json
Normal 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/types/src/__generated__/globalTypes.ts
generated
12
libs/types/src/__generated__/globalTypes.ts
generated
@ -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
|
||||
*/
|
||||
|
@ -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",
|
||||
|
@ -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"],
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user