feat: generate queries add period params

This commit is contained in:
Matthew Russell
2023-12-12 14:18:28 -08:00
parent fdb0d03ec0
commit 360c6a3b09
8 changed files with 497 additions and 237 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"extends": ["plugin:@nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"ignorePatterns": ["!**/*", "__generated__"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
+40
View File
@@ -0,0 +1,40 @@
fragment Bar on Candle {
periodStart
lastUpdateInPeriod
high
low
open
close
volume
}
query GetBars(
$marketId: ID!
$interval: Interval!
$since: String!
$to: String
) {
market(id: $marketId) {
id
decimalPlaces
positionDecimalPlaces
candlesConnection(
interval: $interval
since: $since
to: $to
pagination: { last: 5000 }
) {
edges {
node {
...Bar
}
}
}
}
}
subscription LastBar($marketId: ID!, $interval: Interval!) {
candles(marketId: $marketId, interval: $interval) {
...Bar
}
}
+21
View File
@@ -0,0 +1,21 @@
query Symbol($marketId: ID!) {
market(id: $marketId) {
id
decimalPlaces
positionDecimalPlaces
tradableInstrument {
instrument {
code
name
product {
... on Future {
__typename
}
... on Perpetual {
__typename
}
}
}
}
}
}
+119
View File
@@ -0,0 +1,119 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type BarFragment = { __typename?: 'Candle', periodStart: any, lastUpdateInPeriod: any, high: string, low: string, open: string, close: string, volume: string };
export type GetBarsQueryVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
interval: Types.Interval;
since: Types.Scalars['String'];
to?: Types.InputMaybe<Types.Scalars['String']>;
}>;
export type GetBarsQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, candlesConnection?: { __typename?: 'CandleDataConnection', edges?: Array<{ __typename?: 'CandleEdge', node: { __typename?: 'Candle', periodStart: any, lastUpdateInPeriod: any, high: string, low: string, open: string, close: string, volume: string } } | null> | null } | null } | null };
export type LastBarSubscriptionVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
interval: Types.Interval;
}>;
export type LastBarSubscription = { __typename?: 'Subscription', candles: { __typename?: 'Candle', periodStart: any, lastUpdateInPeriod: any, high: string, low: string, open: string, close: string, volume: string } };
export const BarFragmentDoc = gql`
fragment Bar on Candle {
periodStart
lastUpdateInPeriod
high
low
open
close
volume
}
`;
export const GetBarsDocument = gql`
query GetBars($marketId: ID!, $interval: Interval!, $since: String!, $to: String) {
market(id: $marketId) {
id
decimalPlaces
positionDecimalPlaces
candlesConnection(
interval: $interval
since: $since
to: $to
pagination: {last: 5000}
) {
edges {
node {
...Bar
}
}
}
}
}
${BarFragmentDoc}`;
/**
* __useGetBarsQuery__
*
* To run a query within a React component, call `useGetBarsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetBarsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetBarsQuery({
* variables: {
* marketId: // value for 'marketId'
* interval: // value for 'interval'
* since: // value for 'since'
* to: // value for 'to'
* },
* });
*/
export function useGetBarsQuery(baseOptions: Apollo.QueryHookOptions<GetBarsQuery, GetBarsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetBarsQuery, GetBarsQueryVariables>(GetBarsDocument, options);
}
export function useGetBarsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetBarsQuery, GetBarsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetBarsQuery, GetBarsQueryVariables>(GetBarsDocument, options);
}
export type GetBarsQueryHookResult = ReturnType<typeof useGetBarsQuery>;
export type GetBarsLazyQueryHookResult = ReturnType<typeof useGetBarsLazyQuery>;
export type GetBarsQueryResult = Apollo.QueryResult<GetBarsQuery, GetBarsQueryVariables>;
export const LastBarDocument = gql`
subscription LastBar($marketId: ID!, $interval: Interval!) {
candles(marketId: $marketId, interval: $interval) {
...Bar
}
}
${BarFragmentDoc}`;
/**
* __useLastBarSubscription__
*
* To run a query within a React component, call `useLastBarSubscription` and pass it any options that fit your needs.
* When your component renders, `useLastBarSubscription` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useLastBarSubscription({
* variables: {
* marketId: // value for 'marketId'
* interval: // value for 'interval'
* },
* });
*/
export function useLastBarSubscription(baseOptions: Apollo.SubscriptionHookOptions<LastBarSubscription, LastBarSubscriptionVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useSubscription<LastBarSubscription, LastBarSubscriptionVariables>(LastBarDocument, options);
}
export type LastBarSubscriptionHookResult = ReturnType<typeof useLastBarSubscription>;
export type LastBarSubscriptionResult = Apollo.SubscriptionResult<LastBarSubscription>;
+64
View File
@@ -0,0 +1,64 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type SymbolQueryVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
}>;
export type SymbolQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', code: string, name: string, product: { __typename: 'Future' } | { __typename: 'Perpetual' } | { __typename?: 'Spot' } } } } | null };
export const SymbolDocument = gql`
query Symbol($marketId: ID!) {
market(id: $marketId) {
id
decimalPlaces
positionDecimalPlaces
tradableInstrument {
instrument {
code
name
product {
... on Future {
__typename
}
... on Perpetual {
__typename
}
}
}
}
}
}
`;
/**
* __useSymbolQuery__
*
* To run a query within a React component, call `useSymbolQuery` and pass it any options that fit your needs.
* When your component renders, `useSymbolQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useSymbolQuery({
* variables: {
* marketId: // value for 'marketId'
* },
* });
*/
export function useSymbolQuery(baseOptions: Apollo.QueryHookOptions<SymbolQuery, SymbolQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<SymbolQuery, SymbolQueryVariables>(SymbolDocument, options);
}
export function useSymbolLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SymbolQuery, SymbolQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<SymbolQuery, SymbolQueryVariables>(SymbolDocument, options);
}
export type SymbolQueryHookResult = ReturnType<typeof useSymbolQuery>;
export type SymbolLazyQueryHookResult = ReturnType<typeof useSymbolLazyQuery>;
export type SymbolQueryResult = Apollo.QueryResult<SymbolQuery, SymbolQueryVariables>;
-214
View File
@@ -1,214 +0,0 @@
import { useEffect, useMemo } from 'react';
import {
type LibrarySymbolInfo,
type IBasicDataFeed,
type ResolutionString,
type SeriesFormat,
} from '../charting_library/charting_library';
type Candle = {
start: string;
lastUpdate: string;
high: string;
low: string;
open: string;
close: string;
volume: string;
notional: string;
};
/* eslint-disable no-console */
const url = 'https://api.n07.testnet.vega.xyz/api/v2';
const websocketUrl = 'wss://api.n07.testnet.vega.xyz/api/v2/stream/candle/data';
let socket: WebSocket;
const configurationData = {
// only showing Vega ofc
exchanges: undefined,
// Represents the resolutions for bars supported by your datafeed
supported_resolutions: ['1'] as ResolutionString[],
} as const;
const resolutionMap: Record<ResolutionString, (marketId: string) => string> = {
// @ts-ignore something something
'1': (marketId: string) => `trades_candle_1_minute_${marketId}`,
};
export const useDataFeed = (marketId: string) => {
const datafeed = useMemo(() => {
const feed: IBasicDataFeed = {
onReady: (callback) => {
console.log('[onReady]: Method call');
setTimeout(() => callback(configurationData));
},
searchSymbols: () => {
console.log('[searchSymbols]: Method call');
},
resolveSymbol: async (
marketId,
onSymbolResolvedCallback,
onResolveErrorCallback,
) => {
console.log('[resolveSymbol]: Method call', marketId);
try {
const data = await request('market/' + marketId);
const symbolInfo: LibrarySymbolInfo = {
ticker: data.market.id,
name: data.market.tradableInstrument.instrument.code,
full_name: data.market.tradableInstrument.instrument.code,
listed_exchange: 'vega',
format: 'price' as SeriesFormat,
description: data.market.tradableInstrument.instrument.name,
type: 'futures',
session: '24x7',
timezone: 'Etc/UTC',
exchange: '',
minmov: 1,
pricescale: 100,
visible_plots_set: 'ohlc',
supported_resolutions: ['1'] as ResolutionString[],
volume_precision: data.market.positionDecimalPlaces,
data_status: 'pulsed',
delay: 1000,
has_intraday: true, // required for less than 1 day interval
};
onSymbolResolvedCallback(symbolInfo);
} catch (err) {
onResolveErrorCallback('Cannot resolve symbol');
}
},
getBars: async (
symbolInfo,
resolution,
periodParams,
onHistoryCallback,
onErrorCallback,
) => {
console.log('HERE', resolution, symbolInfo);
if (!symbolInfo.ticker) {
onErrorCallback('No symbol.ticker');
return;
}
try {
const candleId = resolutionMap[resolution](symbolInfo.ticker);
console.log(candleId);
const data = await request(`candle?candleId=${candleId}`);
if (!data.candles.edges.length) {
onHistoryCallback([], { noData: true });
return;
}
const bars = data.candles.edges
.map((e: { node: Candle }) => {
return prepareBar(e.node);
})
.reverse();
console.log(bars);
onHistoryCallback(bars, { noData: false });
} catch (err) {
console.log('Cannot getBars', err);
onErrorCallback(err as Error);
}
},
subscribeBars: (
symbolInfo,
resolution,
onTick,
subscriberUID,
onResetCacheNeededCallback,
) => {
console.log(
'[subscribeBars]: Method call with subscriberUID:',
subscriberUID,
);
if (!symbolInfo.ticker) {
console.error('No symbolInfo.ticker');
return;
}
const candleId = resolutionMap[resolution](symbolInfo.ticker);
socket = new WebSocket(`${websocketUrl}?candleId=${candleId}`);
socket.onopen = (event) => {
console.log('open', event);
};
socket.onmessage = (event) => {
console.log('message', event);
try {
// TODO: update or append to candle set
const data = JSON.parse(event.data);
const bar = prepareBar(data.result.candle);
onTick(bar);
} catch (err) {
console.error(err);
}
};
socket.onerror = (event) => {
console.log('error', event);
};
socket.onclose = (event) => {
console.log('close', event);
};
},
unsubscribeBars: (subscriberUID) => {
console.log(
'[unsubscribeBars]: Method call with subscriberUID:',
subscriberUID,
);
if (socket) {
socket.close();
}
},
};
return feed;
}, []);
useEffect(() => {
return () => {
if (socket) {
socket.close();
}
};
}, []);
return datafeed;
};
const prepareBar = (candle: Candle) => {
return {
time: Number(candle.start.slice(0, -6)), // trim to milliseconds
low: Number(candle.low),
high: Number(candle.high),
open: Number(candle.open),
close: Number(candle.close),
volume: Number(candle.volume),
};
};
const request = async (path: string) => {
try {
const res = await fetch(`${url}/${path}`);
const json = await res.json();
return json;
console.log(json);
} catch (err) {
console.error(err);
}
};
+2 -22
View File
@@ -6,14 +6,14 @@ import {
type ResolutionString,
widget,
} from '../charting_library';
import { useDataFeed } from './datafeed';
import { useDatafeed } from './use-datafeed';
export const TradingView = ({ marketId }: { marketId: string }) => {
const { theme } = useThemeSwitcher();
const chartContainerRef =
useRef<HTMLDivElement>() as React.MutableRefObject<HTMLInputElement>;
const datafeed = useDataFeed(marketId);
const datafeed = useDatafeed();
useEffect(() => {
const widgetOptions: ChartingLibraryWidgetOptions = {
@@ -43,26 +43,6 @@ export const TradingView = ({ marketId }: { marketId: string }) => {
const tvWidget = new widget(widgetOptions);
// Add a custom button
tvWidget.onChartReady(() => {
tvWidget.headerReady().then(() => {
const button = tvWidget.createButton();
button.setAttribute('title', 'Click to show a notification popup');
button.classList.add('apply-common-tooltip');
button.addEventListener('click', () =>
tvWidget.showNoticeDialog({
title: 'Notification',
body: 'TradingView Charting Library API works correctly',
callback: () => {
console.log('Noticed!');
},
}),
);
button.innerHTML = 'Check API';
});
});
return () => {
tvWidget.remove();
};
+250
View File
@@ -0,0 +1,250 @@
import { useEffect, useMemo, useRef } from 'react';
import compact from 'lodash/compact';
import { useApolloClient } from '@apollo/client';
import { type Subscription } from 'zen-observable-ts';
import {
type LibrarySymbolInfo,
type IBasicDataFeed,
type ResolutionString,
type SeriesFormat,
} from '../charting_library/charting_library';
import {
GetBarsDocument,
LastBarDocument,
type BarFragment,
type GetBarsQuery,
type GetBarsQueryVariables,
type LastBarSubscription,
type LastBarSubscriptionVariables,
} from './__generated__/Bars';
import { Interval } from '@vegaprotocol/types';
import {
SymbolDocument,
type SymbolQuery,
type SymbolQueryVariables,
} from './__generated__/Symbol';
import { toBigNum } from '@vegaprotocol/utils';
const resolutionMap: Record<string, Interval> = {
'1T': Interval.INTERVAL_BLOCK,
'1': Interval.INTERVAL_I1M,
'5': Interval.INTERVAL_I5M,
'15': Interval.INTERVAL_I15M,
'60': Interval.INTERVAL_I1H,
'360': Interval.INTERVAL_I6H,
'1D': Interval.INTERVAL_I1D,
} as const;
const supportedResolutions = Object.keys(resolutionMap);
const configurationData = {
// only showing Vega ofc
exchanges: undefined,
// Represents the resolutions for bars supported by your datafeed
supported_resolutions: supportedResolutions as ResolutionString[],
} as const;
export const useDatafeed = () => {
const subRef = useRef<Subscription>();
const client = useApolloClient();
const datafeed = useMemo(() => {
const feed: IBasicDataFeed = {
onReady: (callback) => {
setTimeout(() => callback(configurationData));
},
searchSymbols: () => {
/* no op, we handle finding markets in app */
},
resolveSymbol: async (
marketId,
onSymbolResolvedCallback,
onResolveErrorCallback
) => {
try {
const result = await client.query<SymbolQuery, SymbolQueryVariables>({
query: SymbolDocument,
variables: {
marketId,
},
});
if (!result.data.market) {
onResolveErrorCallback('Cannot resolve symbol: market not found');
return;
}
const market = result.data.market;
const instrument = market.tradableInstrument.instrument;
const productType = instrument.product.__typename;
if (!productType) {
onResolveErrorCallback(
'Cannot resolve symbol: invalid product type'
);
return;
}
const symbolInfo: LibrarySymbolInfo = {
ticker: market.id,
name: instrument.code,
full_name: instrument.code,
listed_exchange: 'vega',
format: 'price' as SeriesFormat,
description: instrument.name,
type: 'futures', // TODO no hard code
session: '24x7',
timezone: 'Etc/UTC',
exchange: '',
minmov: 1,
pricescale: Number('1' + '0'.repeat(market.decimalPlaces)), // for number of decimal places
visible_plots_set: 'ohlc',
volume_precision: market.positionDecimalPlaces,
data_status: 'pulsed',
delay: 1000,
has_intraday: true, // required for less than 1 day interval
// @ts-ignore required for data conversion
vegaDecimalPlaces: market.decimalPlaces,
// @ts-ignore required for data conversion
vegaPositionDecimalPlaces: market.positionDecimalPlaces,
};
onSymbolResolvedCallback(symbolInfo);
} catch (err) {
onResolveErrorCallback('Cannot resolve symbol');
}
},
getBars: async (
symbolInfo,
resolution,
periodParams,
onHistoryCallback,
onErrorCallback
) => {
if (!symbolInfo.ticker) {
onErrorCallback('No symbol.ticker');
return;
}
try {
const result = await client.query<
GetBarsQuery,
GetBarsQueryVariables
>({
query: GetBarsDocument,
variables: {
marketId: symbolInfo.ticker,
since: unixTimestampToDate(periodParams.from).toISOString(),
to: unixTimestampToDate(periodParams.to).toISOString(),
interval: resolutionMap[resolution],
},
});
const candleEdges = compact(
result.data.market?.candlesConnection?.edges
);
if (!candleEdges.length) {
onHistoryCallback([], { noData: true });
return;
}
const bars = candleEdges.map((edge) => {
return prepareBar(
edge.node,
// @ts-ignore added in resolveSymbol
symbolInfo.vegaDecimalPlaces,
// @ts-ignore added in resolveSymbol
symbolInfo.vegaPositionDecimalPlaces
);
});
onHistoryCallback(bars, { noData: false });
} catch (err) {
onErrorCallback(
err instanceof Error ? err.message : 'Failed to get bars'
);
}
},
subscribeBars: (
symbolInfo,
resolution,
onTick
// subscriberUID, // chart will subscribe and unsbuscribe when the parent market of the page changes so we don't need to use subscriberUID as of now
// TODO: figure out how/when we should use onResetCacheNeededCallback
// onResetCacheNeededCallback,
) => {
if (!symbolInfo.ticker) {
throw new Error('No symbolInfo.ticker');
}
subRef.current = client
.subscribe<LastBarSubscription, LastBarSubscriptionVariables>({
query: LastBarDocument,
variables: {
marketId: symbolInfo.ticker,
interval: resolutionMap[resolution],
},
})
.subscribe(({ data }) => {
if (data) {
const bar = prepareBar(
data.candles,
// @ts-ignore added in resolveSymbol
symbolInfo.vegaDecimalPlaces,
// @ts-ignore added in resolveSymbol
symbolInfo.vegaPositionDecimalPlaces
);
onTick(bar);
}
});
},
/**
* We only have one active subscription no need to use the uid provided by unsubscribeBars
*/
unsubscribeBars: () => {
if (subRef.current) {
subRef.current.unsubscribe();
}
},
};
return feed;
}, [client]);
useEffect(() => {
return () => {
if (subRef.current) {
subRef.current.unsubscribe();
}
};
}, []);
return datafeed;
};
const prepareBar = (
bar: BarFragment,
decimalPlaces: number,
positionDecimalPlaces: number
) => {
return {
time: new Date(bar.periodStart).getTime(),
low: toBigNum(bar.low, decimalPlaces).toNumber(),
high: toBigNum(bar.high, decimalPlaces).toNumber(),
open: toBigNum(bar.open, decimalPlaces).toNumber(),
close: toBigNum(bar.close, decimalPlaces).toNumber(),
volume: toBigNum(bar.volume, positionDecimalPlaces).toNumber(),
};
};
const unixTimestampToDate = (timestamp: number) => {
return new Date(timestamp * 1000);
};