@oanda/labs-crowd-view-widget 1.0.44 → 1.0.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/CHANGELOG.md +372 -0
  2. package/dist/main/CrowdViewWidget/Main.js +20 -7
  3. package/dist/main/CrowdViewWidget/Main.js.map +1 -1
  4. package/dist/main/CrowdViewWidget/components/Chart/Chart.js +6 -10
  5. package/dist/main/CrowdViewWidget/components/Chart/Chart.js.map +1 -1
  6. package/dist/main/CrowdViewWidget/components/Chart/chartOptions.js +46 -20
  7. package/dist/main/CrowdViewWidget/components/Chart/chartOptions.js.map +1 -1
  8. package/dist/main/CrowdViewWidget/components/Chart/index.js +4 -4
  9. package/dist/main/CrowdViewWidget/components/Chart/index.js.map +1 -1
  10. package/dist/main/CrowdViewWidget/components/Chart/types.js.map +1 -1
  11. package/dist/main/CrowdViewWidget/components/Chart/useCrowdViewData.js +18 -88
  12. package/dist/main/CrowdViewWidget/components/Chart/useCrowdViewData.js.map +1 -1
  13. package/dist/main/CrowdViewWidget/components/Chart/utils/aggregateBuckets.js +37 -0
  14. package/dist/main/CrowdViewWidget/components/Chart/utils/aggregateBuckets.js.map +1 -0
  15. package/dist/main/CrowdViewWidget/components/Chart/utils/chartUtils.js +54 -2
  16. package/dist/main/CrowdViewWidget/components/Chart/utils/chartUtils.js.map +1 -1
  17. package/dist/main/CrowdViewWidget/components/Chart/utils/getTargetBucketWidth.js +14 -0
  18. package/dist/main/CrowdViewWidget/components/Chart/utils/getTargetBucketWidth.js.map +1 -0
  19. package/dist/main/CrowdViewWidget/components/Chart/utils/index.js +83 -0
  20. package/dist/main/CrowdViewWidget/components/Chart/utils/index.js.map +1 -0
  21. package/dist/main/CrowdViewWidget/components/Chart/utils/processBuckets.js +29 -0
  22. package/dist/main/CrowdViewWidget/components/Chart/utils/processBuckets.js.map +1 -0
  23. package/dist/main/CrowdViewWidget/components/Chart/utils/processOrderPositionBooks.js +23 -0
  24. package/dist/main/CrowdViewWidget/components/Chart/utils/processOrderPositionBooks.js.map +1 -0
  25. package/dist/main/CrowdViewWidget/components/Chart/utils/processPriceCandles.js +43 -0
  26. package/dist/main/CrowdViewWidget/components/Chart/utils/processPriceCandles.js.map +1 -0
  27. package/dist/main/CrowdViewWidget/components/Chart/utils/validateData.js +23 -0
  28. package/dist/main/CrowdViewWidget/components/Chart/utils/validateData.js.map +1 -0
  29. package/dist/main/CrowdViewWidget/components/Legend/Legend.js +5 -3
  30. package/dist/main/CrowdViewWidget/components/Legend/Legend.js.map +1 -1
  31. package/dist/main/CrowdViewWidget/constants.js +105 -5
  32. package/dist/main/CrowdViewWidget/constants.js.map +1 -1
  33. package/dist/main/CrowdViewWidget/selectConfig.js +18 -60
  34. package/dist/main/CrowdViewWidget/selectConfig.js.map +1 -1
  35. package/dist/main/CrowdViewWidget/types.js +20 -0
  36. package/dist/main/CrowdViewWidget/types.js.map +1 -1
  37. package/dist/main/translations/sources/en.json +29 -0
  38. package/dist/module/CrowdViewWidget/Main.js +21 -8
  39. package/dist/module/CrowdViewWidget/Main.js.map +1 -1
  40. package/dist/module/CrowdViewWidget/components/Chart/Chart.js +7 -11
  41. package/dist/module/CrowdViewWidget/components/Chart/Chart.js.map +1 -1
  42. package/dist/module/CrowdViewWidget/components/Chart/chartOptions.js +47 -21
  43. package/dist/module/CrowdViewWidget/components/Chart/chartOptions.js.map +1 -1
  44. package/dist/module/CrowdViewWidget/components/Chart/index.js +1 -1
  45. package/dist/module/CrowdViewWidget/components/Chart/index.js.map +1 -1
  46. package/dist/module/CrowdViewWidget/components/Chart/types.js.map +1 -1
  47. package/dist/module/CrowdViewWidget/components/Chart/useCrowdViewData.js +14 -84
  48. package/dist/module/CrowdViewWidget/components/Chart/useCrowdViewData.js.map +1 -1
  49. package/dist/module/CrowdViewWidget/components/Chart/utils/aggregateBuckets.js +29 -0
  50. package/dist/module/CrowdViewWidget/components/Chart/utils/aggregateBuckets.js.map +1 -0
  51. package/dist/module/CrowdViewWidget/components/Chart/utils/chartUtils.js +52 -2
  52. package/dist/module/CrowdViewWidget/components/Chart/utils/chartUtils.js.map +1 -1
  53. package/dist/module/CrowdViewWidget/components/Chart/utils/getTargetBucketWidth.js +7 -0
  54. package/dist/module/CrowdViewWidget/components/Chart/utils/getTargetBucketWidth.js.map +1 -0
  55. package/dist/module/CrowdViewWidget/components/Chart/utils/index.js +8 -0
  56. package/dist/module/CrowdViewWidget/components/Chart/utils/index.js.map +1 -0
  57. package/dist/module/CrowdViewWidget/components/Chart/utils/processBuckets.js +22 -0
  58. package/dist/module/CrowdViewWidget/components/Chart/utils/processBuckets.js.map +1 -0
  59. package/dist/module/CrowdViewWidget/components/Chart/utils/processOrderPositionBooks.js +16 -0
  60. package/dist/module/CrowdViewWidget/components/Chart/utils/processOrderPositionBooks.js.map +1 -0
  61. package/dist/module/CrowdViewWidget/components/Chart/utils/processPriceCandles.js +36 -0
  62. package/dist/module/CrowdViewWidget/components/Chart/utils/processPriceCandles.js.map +1 -0
  63. package/dist/module/CrowdViewWidget/components/Chart/utils/validateData.js +16 -0
  64. package/dist/module/CrowdViewWidget/components/Chart/utils/validateData.js.map +1 -0
  65. package/dist/module/CrowdViewWidget/components/Legend/Legend.js +5 -3
  66. package/dist/module/CrowdViewWidget/components/Legend/Legend.js.map +1 -1
  67. package/dist/module/CrowdViewWidget/constants.js +104 -4
  68. package/dist/module/CrowdViewWidget/constants.js.map +1 -1
  69. package/dist/module/CrowdViewWidget/selectConfig.js +3 -45
  70. package/dist/module/CrowdViewWidget/selectConfig.js.map +1 -1
  71. package/dist/module/CrowdViewWidget/types.js +19 -1
  72. package/dist/module/CrowdViewWidget/types.js.map +1 -1
  73. package/dist/module/translations/sources/en.json +29 -0
  74. package/dist/types/CrowdViewWidget/components/Chart/index.d.ts +1 -1
  75. package/dist/types/CrowdViewWidget/components/Chart/types.d.ts +12 -4
  76. package/dist/types/CrowdViewWidget/components/Chart/utils/aggregateBuckets.d.ts +2 -0
  77. package/dist/types/CrowdViewWidget/components/Chart/utils/chartUtils.d.ts +12 -2
  78. package/dist/types/CrowdViewWidget/components/Chart/utils/getTargetBucketWidth.d.ts +3 -0
  79. package/dist/types/CrowdViewWidget/components/Chart/utils/index.d.ts +7 -0
  80. package/dist/types/CrowdViewWidget/components/Chart/utils/processBuckets.d.ts +3 -0
  81. package/dist/types/CrowdViewWidget/components/Chart/utils/processOrderPositionBooks.d.ts +8 -0
  82. package/dist/types/CrowdViewWidget/components/Chart/utils/processPriceCandles.d.ts +27 -0
  83. package/dist/types/CrowdViewWidget/components/Chart/utils/validateData.d.ts +2 -0
  84. package/dist/types/CrowdViewWidget/components/Legend/Legend.d.ts +3 -1
  85. package/dist/types/CrowdViewWidget/constants.d.ts +11 -3
  86. package/dist/types/CrowdViewWidget/selectConfig.d.ts +2 -2
  87. package/dist/types/CrowdViewWidget/types.d.ts +18 -1
  88. package/dist/types/CrowdViewWidget/utils/instrumentUtils.d.ts +1 -4
  89. package/lokalise.config.json +1 -1
  90. package/package.json +4 -3
  91. package/src/CrowdViewWidget/Main.tsx +25 -10
  92. package/src/CrowdViewWidget/components/Chart/Chart.tsx +6 -12
  93. package/src/CrowdViewWidget/components/Chart/chartOptions.ts +70 -36
  94. package/src/CrowdViewWidget/components/Chart/index.ts +1 -1
  95. package/src/CrowdViewWidget/components/Chart/types.ts +16 -4
  96. package/src/CrowdViewWidget/components/Chart/useCrowdViewData.ts +41 -140
  97. package/src/CrowdViewWidget/components/Chart/utils/aggregateBuckets.ts +44 -0
  98. package/src/CrowdViewWidget/components/Chart/utils/chartUtils.ts +96 -3
  99. package/src/CrowdViewWidget/components/Chart/utils/getTargetBucketWidth.ts +13 -0
  100. package/src/CrowdViewWidget/components/Chart/utils/index.ts +7 -0
  101. package/src/CrowdViewWidget/components/Chart/utils/processBuckets.ts +43 -0
  102. package/src/CrowdViewWidget/components/Chart/utils/processOrderPositionBooks.ts +30 -0
  103. package/src/CrowdViewWidget/components/Chart/utils/processPriceCandles.ts +53 -0
  104. package/src/CrowdViewWidget/components/Chart/utils/validateData.ts +27 -0
  105. package/src/CrowdViewWidget/components/Legend/Legend.tsx +13 -2
  106. package/src/CrowdViewWidget/constants.ts +113 -3
  107. package/src/CrowdViewWidget/selectConfig.ts +5 -60
  108. package/src/CrowdViewWidget/types.ts +18 -1
  109. package/src/translations/sources/en.json +29 -0
  110. package/test/Main.test.tsx +73 -27
  111. package/test/components/Chart/utils/chartUtils.test.ts +158 -0
  112. package/test/components/Legend.test.tsx +6 -1
  113. package/test/utils/aggregateBuckets.test.ts +82 -0
  114. package/test/utils/getTargetBucketWidth.test.ts +37 -0
  115. package/test/utils/instrumentUtils.test.ts +13 -7
  116. package/test/utils/processBuckets.test.ts +153 -0
  117. package/test/utils/processOrderPositionBooks.test.ts +127 -0
  118. package/test/utils/processPriceCandles.test.ts +245 -0
  119. package/test/utils/validateData.test.ts +201 -0
  120. package/dist/main/CrowdViewWidget/types/index.js +0 -17
  121. package/dist/main/CrowdViewWidget/types/index.js.map +0 -1
  122. package/dist/main/CrowdViewWidget/types/instruments.js +0 -45
  123. package/dist/main/CrowdViewWidget/types/instruments.js.map +0 -1
  124. package/dist/module/CrowdViewWidget/types/index.js +0 -2
  125. package/dist/module/CrowdViewWidget/types/index.js.map +0 -1
  126. package/dist/module/CrowdViewWidget/types/instruments.js +0 -39
  127. package/dist/module/CrowdViewWidget/types/instruments.js.map +0 -1
  128. package/dist/types/CrowdViewWidget/types/index.d.ts +0 -1
  129. package/dist/types/CrowdViewWidget/types/instruments.d.ts +0 -36
  130. package/src/CrowdViewWidget/types/index.ts +0 -1
  131. package/src/CrowdViewWidget/types/instruments.ts +0 -37
@@ -5,9 +5,15 @@ import type {
5
5
  Division,
6
6
  Granularity,
7
7
  } from '../../../gql/types/graphql';
8
+ import type { InstrumentId } from '../../types';
9
+
10
+ export interface Bucket {
11
+ price: number;
12
+ sentiment: number;
13
+ }
8
14
 
9
15
  export interface UseCrowdViewDataProps {
10
- instrument: string;
16
+ instrument: InstrumentId;
11
17
  bookType: BookType;
12
18
  division: Division;
13
19
  granularity: Granularity;
@@ -15,9 +21,14 @@ export interface UseCrowdViewDataProps {
15
21
 
16
22
  interface CrowdViewData {
17
23
  xAxisData: string[];
24
+ // [open, close, low, high]
18
25
  candlesSeriesData: [number, number, number, number][];
19
- orderPositionBooks: ([string, number, string] | null)[];
26
+ // [time, price, index]
27
+ orderPositionBooks: [string, number | null, number][];
20
28
  bucketWidth: number;
29
+ buckets: Bucket[][];
30
+ precision: number;
31
+ bookType: BookType;
21
32
  }
22
33
 
23
34
  export interface UseCrowdViewDataReturn {
@@ -28,7 +39,8 @@ export interface UseCrowdViewDataReturn {
28
39
 
29
40
  export type GetOptionType = (
30
41
  props: CrowdViewData,
31
- isDark: boolean
42
+ isDark: boolean,
43
+ labelCallback: (key: string, params?: Record<string, unknown>) => string
32
44
  ) => EChartsOption;
33
45
 
34
46
  export interface ChartProps {
@@ -38,7 +50,7 @@ export interface ChartProps {
38
50
  export interface ChartWithDataProps {
39
51
  bookType: BookType;
40
52
  division: Division;
41
- instrument: string;
53
+ instrument: InstrumentId;
42
54
  granularity: Granularity;
43
55
  }
44
56
 
@@ -10,134 +10,16 @@ import type {
10
10
  GetPriceCandlesQueryVariables,
11
11
  } from '../../../gql/types/graphql';
12
12
  import { BookType, DataSource, Division } from '../../../gql/types/graphql';
13
- import { BOOKS_THRESHOLDS, BUCKET_CONFIG } from '../../constants';
13
+ import { BUCKET_CONFIG, INSTRUMENTS_CONFIG } from '../../constants';
14
14
  import type { UseCrowdViewDataProps, UseCrowdViewDataReturn } from './types';
15
- import { getTimeSpanForGranularity } from './utils/chartUtils';
16
-
17
- const processPriceCandles = (
18
- priceCandlesData: GetPriceCandlesQuery | undefined
19
- ) => {
20
- if (!priceCandlesData?.priceCandles?.candle?.length) {
21
- return {
22
- minPrice: 0,
23
- maxPrice: 0,
24
- hasValidCandles: false,
25
- candleMap: new Map(),
26
- candles: [],
27
- };
28
- }
29
-
30
- const candles = priceCandlesData.priceCandles.candle;
31
- let calculatedMinPrice = Number.MAX_VALUE;
32
- let calculatedMaxPrice = Number.MIN_VALUE;
33
-
34
- const candleMap = new Map<
35
- string,
36
- {
37
- point?: string;
38
- high?: number;
39
- low?: number;
40
- open?: number;
41
- close?: number;
42
- }
43
- >();
44
-
45
- candles.forEach((candle) => {
46
- if (!candle) return;
47
-
48
- if (candle.high > calculatedMaxPrice) {
49
- calculatedMaxPrice = candle.high;
50
- }
51
- if (candle.low < calculatedMinPrice) {
52
- calculatedMinPrice = candle.low;
53
- }
54
-
55
- if (candle.point) {
56
- candleMap.set(candle.point, candle);
57
- }
58
- });
59
-
60
- return {
61
- minPrice: calculatedMinPrice,
62
- maxPrice: calculatedMaxPrice,
63
- hasValidCandles: true,
64
- candleMap,
65
- candles,
66
- };
67
- };
68
-
69
- const processOrderPositionBooks = (
70
- orderPositionData: GetOrderPositionBooksQuery | undefined,
71
- candleMap: Map<
72
- string,
73
- {
74
- point?: string;
75
- high?: number;
76
- low?: number;
77
- open?: number;
78
- close?: number;
79
- }
80
- >
81
- ) => {
82
- if (!orderPositionData?.orderPositionBooks?.length) {
83
- return [];
84
- }
85
-
86
- return orderPositionData.orderPositionBooks
87
- .filter((book): book is NonNullable<typeof book> => {
88
- return book !== null && book.buckets?.length > 0;
89
- })
90
- .map((book) => {
91
- const filteredBuckets = book.buckets
92
- .filter((bucket) => {
93
- if (
94
- !bucket ||
95
- bucket.sentiment === undefined ||
96
- bucket.sentiment === null ||
97
- bucket.price === undefined
98
- ) {
99
- return false;
100
- }
101
- return Math.abs(bucket.sentiment) >= BOOKS_THRESHOLDS.MIN;
102
- })
103
- .map((bucket) => ({
104
- price: bucket!.price!,
105
- sentiment: bucket!.sentiment!,
106
- }));
107
-
108
- const candle = candleMap.get(book.time);
109
- const price = candle?.high ?? null;
110
-
111
- return [book.time, price, JSON.stringify(filteredBuckets)] as [
112
- string,
113
- number,
114
- string,
115
- ];
116
- });
117
- };
118
-
119
- const validateData = (
120
- priceCandlesData: GetPriceCandlesQuery | undefined,
121
- orderPositionData: GetOrderPositionBooksQuery | undefined,
122
- hasValidCandles: boolean
123
- ): Error | null => {
124
- const hasValidPriceData =
125
- (priceCandlesData?.priceCandles?.candle?.length ?? 0) >= 1;
126
- const hasValidOrderData =
127
- (orderPositionData?.orderPositionBooks?.length ?? 0) >= 1;
128
-
129
- if (!hasValidPriceData) {
130
- return new Error('Insufficient price candle data');
131
- }
132
- if (!hasValidOrderData) {
133
- return new Error('Insufficient order position data');
134
- }
135
- if (!hasValidCandles) {
136
- return new Error('Invalid candle data');
137
- }
138
-
139
- return null;
140
- };
15
+ import {
16
+ getTargetBucketWidth,
17
+ getTimeSpanForGranularity,
18
+ processBuckets,
19
+ processOrderPositionBooks,
20
+ processPriceCandles,
21
+ validateData,
22
+ } from './utils';
141
23
 
142
24
  export const useCrowdViewData = ({
143
25
  instrument,
@@ -153,9 +35,12 @@ export const useCrowdViewData = ({
153
35
  getPriceCandles,
154
36
  {
155
37
  variables: {
156
- dataSource: division === Division.Oc ? DataSource.V20 : DataSource.Mt5,
38
+ dataSource: division === Division.Ogm ? DataSource.Mt5 : DataSource.V20,
157
39
  division,
158
- instrument,
40
+ instrument:
41
+ division === Division.Ogm
42
+ ? INSTRUMENTS_CONFIG[instrument].mt5name
43
+ : INSTRUMENTS_CONFIG[instrument].v20name,
159
44
  granularity,
160
45
  timeSpan: getTimeSpanForGranularity(granularity),
161
46
  },
@@ -171,18 +56,16 @@ export const useCrowdViewData = ({
171
56
  const { minPrice, maxPrice, hasValidCandles, candleMap, candles } =
172
57
  priceCandlesProcessed;
173
58
 
59
+ const targetBucketWidth = getTargetBucketWidth(granularity, instrument);
60
+
174
61
  const maxBookPrice = useMemo(
175
- () =>
176
- maxPrice +
177
- BUCKET_CONFIG.DEFAULT_WIDTH * BUCKET_CONFIG.PRICE_PADDING_MULTIPLIER,
178
- [maxPrice]
62
+ () => maxPrice + targetBucketWidth * BUCKET_CONFIG.PRICE_PADDING_MULTIPLIER,
63
+ [maxPrice, targetBucketWidth]
179
64
  );
180
65
 
181
66
  const minBookPrice = useMemo(
182
- () =>
183
- minPrice -
184
- BUCKET_CONFIG.DEFAULT_WIDTH * BUCKET_CONFIG.PRICE_PADDING_MULTIPLIER,
185
- [minPrice]
67
+ () => minPrice - targetBucketWidth * BUCKET_CONFIG.PRICE_PADDING_MULTIPLIER,
68
+ [minPrice, targetBucketWidth]
186
69
  );
187
70
 
188
71
  const {
@@ -193,7 +76,7 @@ export const useCrowdViewData = ({
193
76
  getOrderPositionBooks,
194
77
  {
195
78
  variables: {
196
- instrument,
79
+ instrument: INSTRUMENTS_CONFIG[instrument].v20name,
197
80
  bookType: bookType || BookType.Order,
198
81
  timeSpan: getTimeSpanForGranularity(granularity),
199
82
  granularity,
@@ -212,6 +95,11 @@ export const useCrowdViewData = ({
212
95
  [orderPositionData, candleMap]
213
96
  );
214
97
 
98
+ const buckets = useMemo(
99
+ () => processBuckets(orderPositionData, targetBucketWidth),
100
+ [orderPositionData, targetBucketWidth]
101
+ );
102
+
215
103
  const error = useMemo((): Error | null => {
216
104
  if (priceCandlesError) {
217
105
  return new Error(`Price candles error: ${priceCandlesError.message}`);
@@ -248,12 +136,25 @@ export const useCrowdViewData = ({
248
136
  );
249
137
 
250
138
  return {
139
+ buckets,
251
140
  xAxisData,
252
141
  candlesSeriesData,
253
142
  orderPositionBooks,
254
- bucketWidth: orderPositionData.orderPositionBooks?.[0]?.bucketWidth || 0,
143
+ bucketWidth: targetBucketWidth,
144
+ precision: INSTRUMENTS_CONFIG[instrument].precision,
145
+ bookType,
255
146
  };
256
- }, [priceCandlesData, orderPositionData, error, candles, orderPositionBooks]);
147
+ }, [
148
+ priceCandlesData,
149
+ orderPositionData,
150
+ error,
151
+ candles,
152
+ buckets,
153
+ orderPositionBooks,
154
+ targetBucketWidth,
155
+ instrument,
156
+ bookType,
157
+ ]);
257
158
 
258
159
  return {
259
160
  data,
@@ -0,0 +1,44 @@
1
+ import Decimal from 'decimal.js';
2
+
3
+ import type { Bucket } from '../types';
4
+
5
+ export const aggregateBuckets = (
6
+ buckets: Bucket[],
7
+ newBucketWidth: number
8
+ ): Bucket[] => {
9
+ if (!buckets.length) {
10
+ return [];
11
+ }
12
+
13
+ const bucketWidthDecimal = new Decimal(newBucketWidth);
14
+
15
+ const aggregatedMap = new Map<string, Decimal>();
16
+
17
+ for (const bucket of buckets) {
18
+ const priceDecimal = new Decimal(bucket.price);
19
+ const sentimentDecimal = new Decimal(bucket.sentiment);
20
+
21
+ const bucketIndex = priceDecimal.div(bucketWidthDecimal).floor();
22
+ const groupingKey = bucketIndex.mul(bucketWidthDecimal);
23
+
24
+ const groupingKeyStr = groupingKey.toString();
25
+
26
+ const currentSentiment =
27
+ aggregatedMap.get(groupingKeyStr) ?? new Decimal(0);
28
+ aggregatedMap.set(groupingKeyStr, currentSentiment.add(sentimentDecimal));
29
+ }
30
+
31
+ const aggregatedBuckets = Array.from(aggregatedMap.entries()).map(
32
+ ([priceStr, sentimentDecimal]) => {
33
+ const price = new Decimal(priceStr).toNumber();
34
+ const sentiment = sentimentDecimal.toNumber();
35
+
36
+ return {
37
+ price,
38
+ sentiment,
39
+ };
40
+ }
41
+ );
42
+
43
+ return aggregatedBuckets;
44
+ };
@@ -1,13 +1,13 @@
1
1
  import chroma from 'chroma-js';
2
2
 
3
- import { Granularity, TimeSpan } from '../../../../gql/types/graphql';
3
+ import { BookType, Granularity, TimeSpan } from '../../../../gql/types/graphql';
4
4
  import {
5
5
  BOOKS_THRESHOLDS,
6
6
  CHART_CONFIG,
7
7
  COLOR_MAP,
8
8
  TIME_THRESHOLDS,
9
9
  } from '../../../constants';
10
- import type { GetLabelsDataProps } from '../types';
10
+ import type { Bucket, GetLabelsDataProps } from '../types';
11
11
 
12
12
  export const getLabelData = ({
13
13
  xAxisData,
@@ -28,7 +28,7 @@ export const getLabelData = ({
28
28
  })
29
29
  .map((item) => ({
30
30
  name: new Date(item).toLocaleDateString(undefined, {
31
- month: 'short',
31
+ month: isGreaterThanTwoWeeks ? 'short' : 'long',
32
32
  day: isGreaterThanTwoWeeks ? 'numeric' : undefined,
33
33
  }),
34
34
  xAxis: item,
@@ -91,3 +91,96 @@ export const getRectColor = (sentiment: number) =>
91
91
  sentiment < 0
92
92
  ? getGradientColor(sentiment * -1, COLOR_MAP.short[0], COLOR_MAP.short[1])
93
93
  : getGradientColor(sentiment, COLOR_MAP.long[0], COLOR_MAP.long[1]);
94
+
95
+ export const getTooltipFormatter = ({
96
+ params,
97
+ buckets,
98
+ bucketWidth,
99
+ selectedPrice,
100
+ precision,
101
+ bookType,
102
+ labelCallback,
103
+ }: {
104
+ params: unknown;
105
+ buckets: Bucket[][];
106
+ bucketWidth: number;
107
+ selectedPrice: number;
108
+ precision: number;
109
+ bookType: BookType;
110
+ labelCallback: (key: string) => string;
111
+ }) => {
112
+ const arr = params as unknown as Array<Record<string, unknown>>;
113
+ if (!arr || !Array.isArray(arr) || arr.length === 0) {
114
+ return undefined;
115
+ }
116
+ const candleParam = arr[0] as {
117
+ axisValue: string;
118
+ value: [number, number, number, number, number];
119
+ };
120
+ const booksParam = arr[1] as { value: [string, number, number] };
121
+ const time = new Date(candleParam.axisValue as string);
122
+
123
+ const candleData = candleParam.value;
124
+ const booksData = booksParam?.value ?? [];
125
+ const bucketsIndex = booksData[2];
126
+ const selectedBuckets = buckets[bucketsIndex];
127
+
128
+ const matchedBucket = selectedBuckets?.find(
129
+ ({ price }) => selectedPrice >= price && selectedPrice < price + bucketWidth
130
+ );
131
+ const showCandles =
132
+ !!candleData[1] && !!candleData[2] && !!candleData[3] && !!candleData[4];
133
+
134
+ if (!showCandles && !matchedBucket) {
135
+ return undefined;
136
+ }
137
+
138
+ return `<p>${time.toLocaleString(undefined, {
139
+ hour: '2-digit',
140
+ minute: '2-digit',
141
+ year: 'numeric',
142
+ day: 'numeric',
143
+ month: 'numeric',
144
+ timeZoneName: 'short',
145
+ })}</p><br />
146
+ ${
147
+ showCandles
148
+ ? `<p><b>${labelCallback('candle')}:</b></p>
149
+ <p>${labelCallback('open_price')}: ${candleData[1]} </p>
150
+ <p>${labelCallback('close_price')}: ${candleData[2]} </p>
151
+ <p>${labelCallback('low')}: ${candleData[3]} </p>
152
+ <p>${labelCallback('high')}: ${candleData[4]} </p>
153
+ `
154
+ : ''
155
+ }
156
+ ${
157
+ matchedBucket
158
+ ? `<br /><p><b>${labelCallback(bookType === BookType.Order ? 'orders' : 'positions')}:</b></p>
159
+ <p>${labelCallback('price_range')}: ${matchedBucket.price.toFixed(precision - 1)} - ${Number(matchedBucket.price + bucketWidth).toFixed(precision - 1)} </p>
160
+ <p>${
161
+ matchedBucket.sentiment < 0
162
+ ? labelCallback(
163
+ bookType === BookType.Order ? 'sell_advantage' : 'short_advantage'
164
+ )
165
+ : labelCallback(
166
+ bookType === BookType.Order ? 'buy_advantage' : 'long_advantage'
167
+ )
168
+ }: ${Math.abs(matchedBucket.sentiment)}% </p>`
169
+ : ''
170
+ }`;
171
+ };
172
+
173
+ export const formatXAxisLabel = (
174
+ value: unknown,
175
+ isGreaterThanTwoWeeks: boolean
176
+ ) => {
177
+ const date = new Date(value as string);
178
+ return isGreaterThanTwoWeeks
179
+ ? `${date.toLocaleTimeString(undefined, {
180
+ hour: '2-digit',
181
+ minute: '2-digit',
182
+ })}`
183
+ : `${CHART_CONFIG.X_AXIS_DATE_PADDING}${date.toLocaleDateString(undefined, {
184
+ day: 'numeric',
185
+ })}${CHART_CONFIG.X_AXIS_DATE_PADDING}`;
186
+ };
@@ -0,0 +1,13 @@
1
+ import { Granularity } from '../../../../gql/types/graphql';
2
+ import { BUCKET_CONFIG, INSTRUMENTS_CONFIG } from '../../../constants';
3
+ import type { InstrumentId } from '../../../types';
4
+
5
+ export const getTargetBucketWidth = (
6
+ granularity: Granularity,
7
+ instrument: InstrumentId
8
+ ): number => {
9
+ const bucketWidth = INSTRUMENTS_CONFIG[instrument].defaultBucketWidth;
10
+ return granularity === Granularity.H1 || granularity === Granularity.H4
11
+ ? bucketWidth * BUCKET_CONFIG.MULTIPLIER
12
+ : bucketWidth;
13
+ };
@@ -0,0 +1,7 @@
1
+ export * from './aggregateBuckets';
2
+ export * from './chartUtils';
3
+ export * from './getTargetBucketWidth';
4
+ export * from './processBuckets';
5
+ export * from './processOrderPositionBooks';
6
+ export * from './processPriceCandles';
7
+ export * from './validateData';
@@ -0,0 +1,43 @@
1
+ import type { GetOrderPositionBooksQuery } from '../../../../gql/types/graphql';
2
+ import { BOOKS_THRESHOLDS } from '../../../constants';
3
+ import type { Bucket } from '../types';
4
+ import { aggregateBuckets } from './aggregateBuckets';
5
+
6
+ export const processBuckets = (
7
+ orderPositionData: GetOrderPositionBooksQuery | undefined,
8
+ targetBucketWidth: number
9
+ ): Bucket[][] => {
10
+ if (!orderPositionData?.orderPositionBooks?.length) {
11
+ return [];
12
+ }
13
+
14
+ return orderPositionData.orderPositionBooks
15
+ .filter((book): book is NonNullable<typeof book> => {
16
+ return book !== null && book.buckets?.length > 0;
17
+ })
18
+ .map((book) => {
19
+ const validBuckets = book.buckets
20
+ .filter(
21
+ (bucket): bucket is NonNullable<typeof bucket> =>
22
+ bucket !== null &&
23
+ bucket.price !== undefined &&
24
+ bucket.sentiment !== undefined &&
25
+ bucket.sentiment !== null
26
+ )
27
+ .map((bucket) => ({
28
+ price: bucket.price!,
29
+ sentiment: bucket.sentiment!,
30
+ }));
31
+
32
+ const bucketsToFilter =
33
+ targetBucketWidth > (book.bucketWidth ?? 0)
34
+ ? aggregateBuckets(validBuckets, targetBucketWidth)
35
+ : validBuckets;
36
+
37
+ const filteredBuckets = bucketsToFilter.filter(
38
+ (bucket: Bucket) => Math.abs(bucket.sentiment) >= BOOKS_THRESHOLDS.MIN
39
+ );
40
+
41
+ return filteredBuckets;
42
+ });
43
+ };
@@ -0,0 +1,30 @@
1
+ import type { GetOrderPositionBooksQuery } from '../../../../gql/types/graphql';
2
+
3
+ export const processOrderPositionBooks = (
4
+ orderPositionData: GetOrderPositionBooksQuery | undefined,
5
+ candleMap: Map<
6
+ string,
7
+ {
8
+ point?: string;
9
+ high?: number;
10
+ low?: number;
11
+ open?: number;
12
+ close?: number;
13
+ }
14
+ >
15
+ ): [string, number | null, number][] => {
16
+ if (!orderPositionData?.orderPositionBooks?.length) {
17
+ return [];
18
+ }
19
+
20
+ return orderPositionData.orderPositionBooks
21
+ .filter((book): book is NonNullable<typeof book> => {
22
+ return book !== null && book.buckets?.length > 0;
23
+ })
24
+ .map((book, index) => {
25
+ const candle = candleMap.get(book.time);
26
+ const price = candle?.high ?? null;
27
+
28
+ return [book.time, price, index] as [string, number | null, number];
29
+ });
30
+ };
@@ -0,0 +1,53 @@
1
+ import type { GetPriceCandlesQuery } from '../../../../gql/types/graphql';
2
+
3
+ export const processPriceCandles = (
4
+ priceCandlesData: GetPriceCandlesQuery | undefined
5
+ ) => {
6
+ if (!priceCandlesData?.priceCandles?.candle?.length) {
7
+ return {
8
+ minPrice: 0,
9
+ maxPrice: 0,
10
+ hasValidCandles: false,
11
+ candleMap: new Map(),
12
+ candles: [],
13
+ };
14
+ }
15
+
16
+ const candles = priceCandlesData.priceCandles.candle;
17
+ let calculatedMinPrice = Number.MAX_VALUE;
18
+ let calculatedMaxPrice = Number.MIN_VALUE;
19
+
20
+ const candleMap = new Map<
21
+ string,
22
+ {
23
+ point?: string;
24
+ high?: number;
25
+ low?: number;
26
+ open?: number;
27
+ close?: number;
28
+ }
29
+ >();
30
+
31
+ candles.forEach((candle) => {
32
+ if (!candle) return;
33
+
34
+ if (candle.high > calculatedMaxPrice) {
35
+ calculatedMaxPrice = candle.high;
36
+ }
37
+ if (candle.low < calculatedMinPrice) {
38
+ calculatedMinPrice = candle.low;
39
+ }
40
+
41
+ if (candle.point) {
42
+ candleMap.set(candle.point, candle);
43
+ }
44
+ });
45
+
46
+ return {
47
+ minPrice: calculatedMinPrice,
48
+ maxPrice: calculatedMaxPrice,
49
+ hasValidCandles: true,
50
+ candleMap,
51
+ candles,
52
+ };
53
+ };
@@ -0,0 +1,27 @@
1
+ import type {
2
+ GetOrderPositionBooksQuery,
3
+ GetPriceCandlesQuery,
4
+ } from '../../../../gql/types/graphql';
5
+
6
+ export const validateData = (
7
+ priceCandlesData: GetPriceCandlesQuery | undefined,
8
+ orderPositionData: GetOrderPositionBooksQuery | undefined,
9
+ hasValidCandles: boolean
10
+ ): Error | null => {
11
+ const hasValidPriceData =
12
+ (priceCandlesData?.priceCandles?.candle?.length ?? 0) >= 1;
13
+ const hasValidOrderData =
14
+ (orderPositionData?.orderPositionBooks?.length ?? 0) >= 1;
15
+
16
+ if (!hasValidPriceData) {
17
+ return new Error('Insufficient price candle data');
18
+ }
19
+ if (!hasValidOrderData) {
20
+ return new Error('Insufficient order position data');
21
+ }
22
+ if (!hasValidCandles) {
23
+ return new Error('Invalid candle data');
24
+ }
25
+
26
+ return null;
27
+ };
@@ -1,24 +1,35 @@
1
1
  import { useLocale } from '@oanda/mono-i18n';
2
2
  import React from 'react';
3
3
 
4
+ import { BookType } from '../../../gql/types/graphql';
4
5
  import { BOOKS_THRESHOLDS } from '../../constants';
5
6
  import { LegendBar } from './LegendBar';
6
7
 
7
8
  interface LegendProps {
8
9
  longValues?: [number, number];
9
10
  shortValues?: [number, number];
11
+ bookType: BookType;
10
12
  }
11
13
 
12
14
  export const Legend = ({
13
15
  longValues = [BOOKS_THRESHOLDS.MIN, BOOKS_THRESHOLDS.MAX],
14
16
  shortValues = [BOOKS_THRESHOLDS.MIN, BOOKS_THRESHOLDS.MAX],
17
+ bookType,
15
18
  }: LegendProps) => {
16
19
  const { lang } = useLocale();
17
20
 
18
21
  return (
19
22
  <div className="lw-mx-auto lw-flex lw-w-full lw-flex-col lw-items-center lw-space-y-4 lw-py-6 sm:lw-max-w-md lg:lw-max-w-xl">
20
- <LegendBar label={lang('long')} type="long" values={longValues} />
21
- <LegendBar label={lang('short')} type="short" values={shortValues} />
23
+ <LegendBar
24
+ label={lang(bookType === BookType.Order ? 'buy' : 'long')}
25
+ type="long"
26
+ values={longValues}
27
+ />
28
+ <LegendBar
29
+ label={lang(bookType === BookType.Order ? 'sell' : 'short')}
30
+ type="short"
31
+ values={shortValues}
32
+ />
22
33
  </div>
23
34
  );
24
35
  };