@oanda/labs-crowd-view-widget 1.0.52 → 1.0.53

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 (134) hide show
  1. package/CHANGELOG.md +216 -0
  2. package/dist/main/CrowdViewWidget/Main.js +1 -5
  3. package/dist/main/CrowdViewWidget/Main.js.map +1 -1
  4. package/dist/main/CrowdViewWidget/components/Chart/Chart.js +16 -6
  5. package/dist/main/CrowdViewWidget/components/Chart/Chart.js.map +1 -1
  6. package/dist/main/CrowdViewWidget/components/Chart/ChartWithData.js +15 -6
  7. package/dist/main/CrowdViewWidget/components/Chart/ChartWithData.js.map +1 -1
  8. package/dist/main/CrowdViewWidget/components/Chart/chartOptions.js +69 -29
  9. package/dist/main/CrowdViewWidget/components/Chart/chartOptions.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 +49 -26
  12. package/dist/main/CrowdViewWidget/components/Chart/useCrowdViewData.js.map +1 -1
  13. package/dist/main/CrowdViewWidget/components/Chart/utils/chartUtils.js +9 -10
  14. package/dist/main/CrowdViewWidget/components/Chart/utils/chartUtils.js.map +1 -1
  15. package/dist/main/CrowdViewWidget/components/Chart/utils/index.js +0 -33
  16. package/dist/main/CrowdViewWidget/components/Chart/utils/index.js.map +1 -1
  17. package/dist/main/CrowdViewWidget/components/Chart/utils/processOrderPositionBooks.js +54 -12
  18. package/dist/main/CrowdViewWidget/components/Chart/utils/processOrderPositionBooks.js.map +1 -1
  19. package/dist/main/CrowdViewWidget/components/Chart/utils/processPriceCandles.js +49 -27
  20. package/dist/main/CrowdViewWidget/components/Chart/utils/processPriceCandles.js.map +1 -1
  21. package/dist/main/CrowdViewWidget/components/Chart/utils/processSentiments.js +32 -17
  22. package/dist/main/CrowdViewWidget/components/Chart/utils/processSentiments.js.map +1 -1
  23. package/dist/main/CrowdViewWidget/components/Chart/utils/validateData.js +8 -2
  24. package/dist/main/CrowdViewWidget/components/Chart/utils/validateData.js.map +1 -1
  25. package/dist/main/CrowdViewWidget/components/Legend/Legend.js +2 -3
  26. package/dist/main/CrowdViewWidget/components/Legend/Legend.js.map +1 -1
  27. package/dist/main/CrowdViewWidget/components/Legend/LegendBar.js +2 -2
  28. package/dist/main/CrowdViewWidget/components/Legend/LegendBar.js.map +1 -1
  29. package/dist/main/CrowdViewWidget/constants.js +2 -6
  30. package/dist/main/CrowdViewWidget/constants.js.map +1 -1
  31. package/dist/main/gql/getOrderPositionBooks.js +1 -1
  32. package/dist/main/gql/getOrderPositionBooks.js.map +1 -1
  33. package/dist/main/gql/getPriceCandles.js +1 -1
  34. package/dist/main/gql/getPriceCandles.js.map +1 -1
  35. package/dist/main/gql/types/gql.js +2 -2
  36. package/dist/main/gql/types/gql.js.map +1 -1
  37. package/dist/main/gql/types/graphql.js +111 -18
  38. package/dist/main/gql/types/graphql.js.map +1 -1
  39. package/dist/module/CrowdViewWidget/Main.js +2 -6
  40. package/dist/module/CrowdViewWidget/Main.js.map +1 -1
  41. package/dist/module/CrowdViewWidget/components/Chart/Chart.js +17 -7
  42. package/dist/module/CrowdViewWidget/components/Chart/Chart.js.map +1 -1
  43. package/dist/module/CrowdViewWidget/components/Chart/ChartWithData.js +15 -6
  44. package/dist/module/CrowdViewWidget/components/Chart/ChartWithData.js.map +1 -1
  45. package/dist/module/CrowdViewWidget/components/Chart/chartOptions.js +70 -30
  46. package/dist/module/CrowdViewWidget/components/Chart/chartOptions.js.map +1 -1
  47. package/dist/module/CrowdViewWidget/components/Chart/types.js.map +1 -1
  48. package/dist/module/CrowdViewWidget/components/Chart/useCrowdViewData.js +50 -27
  49. package/dist/module/CrowdViewWidget/components/Chart/useCrowdViewData.js.map +1 -1
  50. package/dist/module/CrowdViewWidget/components/Chart/utils/chartUtils.js +10 -11
  51. package/dist/module/CrowdViewWidget/components/Chart/utils/chartUtils.js.map +1 -1
  52. package/dist/module/CrowdViewWidget/components/Chart/utils/index.js +0 -3
  53. package/dist/module/CrowdViewWidget/components/Chart/utils/index.js.map +1 -1
  54. package/dist/module/CrowdViewWidget/components/Chart/utils/processOrderPositionBooks.js +54 -12
  55. package/dist/module/CrowdViewWidget/components/Chart/utils/processOrderPositionBooks.js.map +1 -1
  56. package/dist/module/CrowdViewWidget/components/Chart/utils/processPriceCandles.js +49 -27
  57. package/dist/module/CrowdViewWidget/components/Chart/utils/processPriceCandles.js.map +1 -1
  58. package/dist/module/CrowdViewWidget/components/Chart/utils/processSentiments.js +32 -17
  59. package/dist/module/CrowdViewWidget/components/Chart/utils/processSentiments.js.map +1 -1
  60. package/dist/module/CrowdViewWidget/components/Chart/utils/validateData.js +8 -2
  61. package/dist/module/CrowdViewWidget/components/Chart/utils/validateData.js.map +1 -1
  62. package/dist/module/CrowdViewWidget/components/Legend/Legend.js +2 -3
  63. package/dist/module/CrowdViewWidget/components/Legend/Legend.js.map +1 -1
  64. package/dist/module/CrowdViewWidget/components/Legend/LegendBar.js +2 -2
  65. package/dist/module/CrowdViewWidget/components/Legend/LegendBar.js.map +1 -1
  66. package/dist/module/CrowdViewWidget/constants.js +1 -5
  67. package/dist/module/CrowdViewWidget/constants.js.map +1 -1
  68. package/dist/module/gql/getOrderPositionBooks.js +1 -1
  69. package/dist/module/gql/getOrderPositionBooks.js.map +1 -1
  70. package/dist/module/gql/getPriceCandles.js +1 -1
  71. package/dist/module/gql/getPriceCandles.js.map +1 -1
  72. package/dist/module/gql/types/gql.js +2 -2
  73. package/dist/module/gql/types/gql.js.map +1 -1
  74. package/dist/module/gql/types/graphql.js +111 -18
  75. package/dist/module/gql/types/graphql.js.map +1 -1
  76. package/dist/types/CrowdViewWidget/components/Chart/Chart.d.ts +1 -1
  77. package/dist/types/CrowdViewWidget/components/Chart/types.d.ts +28 -11
  78. package/dist/types/CrowdViewWidget/components/Chart/utils/chartUtils.d.ts +3 -4
  79. package/dist/types/CrowdViewWidget/components/Chart/utils/index.d.ts +0 -3
  80. package/dist/types/CrowdViewWidget/components/Chart/utils/processOrderPositionBooks.d.ts +10 -7
  81. package/dist/types/CrowdViewWidget/components/Chart/utils/processPriceCandles.d.ts +6 -21
  82. package/dist/types/CrowdViewWidget/components/Chart/utils/processSentiments.d.ts +5 -2
  83. package/dist/types/CrowdViewWidget/components/Chart/utils/validateData.d.ts +1 -1
  84. package/dist/types/CrowdViewWidget/components/Legend/Legend.d.ts +2 -2
  85. package/dist/types/CrowdViewWidget/components/Legend/LegendBar.d.ts +1 -1
  86. package/dist/types/CrowdViewWidget/constants.d.ts +1 -5
  87. package/dist/types/gql/types/gql.d.ts +6 -4
  88. package/dist/types/gql/types/graphql.d.ts +30 -11
  89. package/package.json +3 -3
  90. package/src/CrowdViewWidget/Main.tsx +2 -4
  91. package/src/CrowdViewWidget/components/Chart/Chart.tsx +15 -6
  92. package/src/CrowdViewWidget/components/Chart/ChartWithData.tsx +21 -4
  93. package/src/CrowdViewWidget/components/Chart/chartOptions.ts +78 -30
  94. package/src/CrowdViewWidget/components/Chart/types.ts +30 -19
  95. package/src/CrowdViewWidget/components/Chart/useCrowdViewData.ts +82 -65
  96. package/src/CrowdViewWidget/components/Chart/utils/chartUtils.ts +32 -20
  97. package/src/CrowdViewWidget/components/Chart/utils/index.ts +0 -3
  98. package/src/CrowdViewWidget/components/Chart/utils/processOrderPositionBooks.ts +84 -22
  99. package/src/CrowdViewWidget/components/Chart/utils/processPriceCandles.ts +52 -38
  100. package/src/CrowdViewWidget/components/Chart/utils/processSentiments.ts +45 -32
  101. package/src/CrowdViewWidget/components/Chart/utils/validateData.ts +10 -2
  102. package/src/CrowdViewWidget/components/Legend/Legend.tsx +4 -5
  103. package/src/CrowdViewWidget/components/Legend/LegendBar.tsx +3 -3
  104. package/src/CrowdViewWidget/constants.ts +1 -6
  105. package/src/gql/getOrderPositionBooks.ts +13 -5
  106. package/src/gql/getPriceCandles.ts +1 -0
  107. package/src/gql/types/gql.ts +6 -6
  108. package/src/gql/types/graphql.ts +98 -16
  109. package/test/components/Chart/utils/chartUtils.test.ts +32 -14
  110. package/test/components/Chart/utils/processSentiments.test.ts +137 -29
  111. package/test/utils/processOrderPositionBooks.test.ts +201 -84
  112. package/test/utils/processPriceCandles.test.ts +93 -67
  113. package/test/utils/validateData.test.ts +136 -38
  114. package/dist/main/CrowdViewWidget/components/Chart/utils/aggregateBuckets.js +0 -37
  115. package/dist/main/CrowdViewWidget/components/Chart/utils/aggregateBuckets.js.map +0 -1
  116. package/dist/main/CrowdViewWidget/components/Chart/utils/getTargetBucketWidth.js +0 -14
  117. package/dist/main/CrowdViewWidget/components/Chart/utils/getTargetBucketWidth.js.map +0 -1
  118. package/dist/main/CrowdViewWidget/components/Chart/utils/processBuckets.js +0 -29
  119. package/dist/main/CrowdViewWidget/components/Chart/utils/processBuckets.js.map +0 -1
  120. package/dist/module/CrowdViewWidget/components/Chart/utils/aggregateBuckets.js +0 -29
  121. package/dist/module/CrowdViewWidget/components/Chart/utils/aggregateBuckets.js.map +0 -1
  122. package/dist/module/CrowdViewWidget/components/Chart/utils/getTargetBucketWidth.js +0 -7
  123. package/dist/module/CrowdViewWidget/components/Chart/utils/getTargetBucketWidth.js.map +0 -1
  124. package/dist/module/CrowdViewWidget/components/Chart/utils/processBuckets.js +0 -22
  125. package/dist/module/CrowdViewWidget/components/Chart/utils/processBuckets.js.map +0 -1
  126. package/dist/types/CrowdViewWidget/components/Chart/utils/aggregateBuckets.d.ts +0 -2
  127. package/dist/types/CrowdViewWidget/components/Chart/utils/getTargetBucketWidth.d.ts +0 -3
  128. package/dist/types/CrowdViewWidget/components/Chart/utils/processBuckets.d.ts +0 -3
  129. package/src/CrowdViewWidget/components/Chart/utils/aggregateBuckets.ts +0 -44
  130. package/src/CrowdViewWidget/components/Chart/utils/getTargetBucketWidth.ts +0 -13
  131. package/src/CrowdViewWidget/components/Chart/utils/processBuckets.ts +0 -43
  132. package/test/utils/aggregateBuckets.test.ts +0 -82
  133. package/test/utils/getTargetBucketWidth.test.ts +0 -37
  134. package/test/utils/processBuckets.test.ts +0 -153
@@ -1,19 +1,14 @@
1
1
  import chroma from 'chroma-js';
2
2
 
3
3
  import { BookType, Granularity, TimeSpan } from '../../../../gql/types/graphql';
4
- import {
5
- BOOKS_THRESHOLDS,
6
- CHART_CONFIG,
7
- COLOR_MAP,
8
- TIME_THRESHOLDS,
9
- } from '../../../constants';
4
+ import { CHART_CONFIG, COLOR_MAP, TIME_THRESHOLDS } from '../../../constants';
10
5
  import type { Bucket, GetLabelsDataProps, TooltipParam } from '../types';
11
6
 
12
7
  export const getLabelData = ({
13
- xAxisData,
8
+ dates,
14
9
  isGreaterThanTwoWeeks,
15
10
  }: GetLabelsDataProps) =>
16
- xAxisData
11
+ dates
17
12
  .filter((record, index, arr) => {
18
13
  if (index === 0) {
19
14
  return false;
@@ -74,29 +69,41 @@ export const getTimeSpanForGranularity = (
74
69
  const getGradientColor = (
75
70
  value: number,
76
71
  startColor: string,
77
- targetColor: string
72
+ targetColor: string,
73
+ minThreshold: number,
74
+ maxThreshold: number
78
75
  ): string => {
79
- const startThreshold = BOOKS_THRESHOLDS.MIN;
80
- const endThreshold = BOOKS_THRESHOLDS.MAX;
81
-
82
76
  const colorScale = chroma
83
77
  .scale([startColor, targetColor])
84
- .domain([startThreshold, endThreshold])
78
+ .domain([minThreshold, maxThreshold])
85
79
  .mode('rgb');
86
80
 
87
81
  return colorScale(value).hex();
88
82
  };
89
83
 
90
- export const getRectColor = (sentiment: number, isDark: boolean) => {
84
+ export const getRectColor = (
85
+ sentiment: number,
86
+ isDark: boolean,
87
+ minThreshold: number,
88
+ maxThreshold: number
89
+ ) => {
91
90
  const colorPalette = isDark ? COLOR_MAP.dark : COLOR_MAP.light;
92
91
 
93
92
  return sentiment < 0
94
93
  ? getGradientColor(
95
94
  sentiment * -1,
96
95
  colorPalette.short[0],
97
- colorPalette.short[1]
96
+ colorPalette.short[1],
97
+ minThreshold,
98
+ maxThreshold
98
99
  )
99
- : getGradientColor(sentiment, colorPalette.long[0], colorPalette.long[1]);
100
+ : getGradientColor(
101
+ sentiment,
102
+ colorPalette.long[0],
103
+ colorPalette.long[1],
104
+ minThreshold,
105
+ maxThreshold
106
+ );
100
107
  };
101
108
 
102
109
  export const getTooltipFormatter = ({
@@ -104,7 +111,6 @@ export const getTooltipFormatter = ({
104
111
  buckets,
105
112
  bucketWidth,
106
113
  selectedPrice,
107
- precision,
108
114
  bookType,
109
115
  labelCallback,
110
116
  }: {
@@ -112,10 +118,12 @@ export const getTooltipFormatter = ({
112
118
  buckets: Bucket[][];
113
119
  bucketWidth: number;
114
120
  selectedPrice: number;
115
- precision: number;
116
121
  bookType: BookType;
117
122
  labelCallback: (key: string) => string;
118
123
  }) => {
124
+ const bucketDisplayPrecision =
125
+ bucketWidth.toString().split('.')[1]?.length || 0;
126
+
119
127
  if (!params || !Array.isArray(params) || params.length === 0) {
120
128
  return undefined;
121
129
  }
@@ -176,7 +184,11 @@ ${
176
184
  ${
177
185
  matchedBucket
178
186
  ? `<br /><p><b>${labelCallback(bookType === BookType.Order ? 'orders' : 'positions')}:</b></p>
179
- <p>${labelCallback('price_range')}: ${matchedBucket.price.toFixed(precision - 1)} - ${Number(matchedBucket.price + bucketWidth).toFixed(precision - 1)} </p>
187
+ <p>${labelCallback('price_range')}: ${matchedBucket.price.toFixed(
188
+ bucketDisplayPrecision
189
+ )} - ${Number(matchedBucket.price + bucketWidth).toFixed(
190
+ bucketDisplayPrecision
191
+ )} </p>
180
192
  <p>${
181
193
  matchedBucket.sentiment < 0
182
194
  ? labelCallback(
@@ -189,7 +201,7 @@ ${
189
201
  ? 'buy_overbalance'
190
202
  : 'long_overbalance'
191
203
  )
192
- }: ${Math.abs(matchedBucket.sentiment)}% </p>`
204
+ }: ${Math.abs(Number(matchedBucket.sentiment.toFixed(bucketDisplayPrecision)))}% </p>`
193
205
  : ''
194
206
  }${
195
207
  showSentiment && sentimentParam
@@ -1,8 +1,5 @@
1
- export * from './aggregateBuckets';
2
1
  export * from './chartUtils';
3
2
  export * from './getChartStyles';
4
- export * from './getTargetBucketWidth';
5
- export * from './processBuckets';
6
3
  export * from './processOrderPositionBooks';
7
4
  export * from './processPriceCandles';
8
5
  export * from './processSentiments';
@@ -1,30 +1,92 @@
1
1
  import type { GetOrderPositionBooksQuery } from '../../../../gql/types/graphql';
2
+ import type { Bucket } from '../types';
2
3
 
3
4
  export const processOrderPositionBooks = (
4
5
  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 [];
6
+ dates: string[]
7
+ ) => {
8
+ // Input validation
9
+ if (!Array.isArray(dates) || dates.length === 0) {
10
+ return {
11
+ bookPrices: [],
12
+ bookIndexes: [],
13
+ buckets: [],
14
+ bucketWidth: 0,
15
+ sentimentThresholdMin: 0,
16
+ sentimentThresholdMax: 0,
17
+ hasValidBooks: false,
18
+ };
18
19
  }
19
20
 
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;
21
+ if (
22
+ !orderPositionData?.orderPositionBooks?.books?.length ||
23
+ !orderPositionData?.orderPositionBooks?.sentimentThresholdMin ||
24
+ !orderPositionData?.orderPositionBooks?.sentimentThresholdMax ||
25
+ !orderPositionData?.orderPositionBooks?.bucketWidth
26
+ ) {
27
+ return {
28
+ bookPrices: [],
29
+ bookIndexes: [],
30
+ buckets: [],
31
+ bucketWidth: 0,
32
+ sentimentThresholdMin: 0,
33
+ sentimentThresholdMax: 0,
34
+ hasValidBooks: false,
35
+ };
36
+ }
37
+
38
+ const { bucketWidth, sentimentThresholdMin, sentimentThresholdMax } =
39
+ orderPositionData.orderPositionBooks;
40
+
41
+ const booksDataMap = new Map(
42
+ orderPositionData.orderPositionBooks.books.map((item) => [
43
+ item.time,
44
+ {
45
+ price: item.price,
46
+ buckets: item.buckets,
47
+ },
48
+ ])
49
+ );
50
+
51
+ // Use forEach + push for O(n) performance instead of reduce with spreads (O(n²))
52
+ const bookPrices: (number | null)[] = [];
53
+ const bookIndexes: number[] = [];
54
+ const buckets: Bucket[][] = [];
55
+
56
+ dates.forEach((date, index) => {
57
+ const book = booksDataMap.get(date);
58
+
59
+ const currentBuckets =
60
+ book?.buckets
61
+ .filter(
62
+ (
63
+ bucket
64
+ ): bucket is {
65
+ price: number;
66
+ sentiment: number;
67
+ } =>
68
+ bucket !== null &&
69
+ typeof bucket.price === 'number' &&
70
+ typeof bucket.sentiment === 'number' &&
71
+ Math.abs(bucket.sentiment) >= sentimentThresholdMin
72
+ )
73
+ .map((bucket) => ({
74
+ price: bucket.price,
75
+ sentiment: bucket.sentiment,
76
+ })) || [];
77
+
78
+ bookPrices.push(book?.price ?? null);
79
+ bookIndexes.push(index);
80
+ buckets.push(currentBuckets);
81
+ });
27
82
 
28
- return [book.time, price, index] as [string, number | null, number];
29
- });
83
+ return {
84
+ bookPrices,
85
+ bookIndexes,
86
+ buckets,
87
+ bucketWidth,
88
+ sentimentThresholdMin,
89
+ sentimentThresholdMax,
90
+ hasValidBooks: true,
91
+ };
30
92
  };
@@ -3,51 +3,65 @@ import type { GetPriceCandlesQuery } from '../../../../gql/types/graphql';
3
3
  export const processPriceCandles = (
4
4
  priceCandlesData: GetPriceCandlesQuery | undefined
5
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
- }
6
+ const errorResult = {
7
+ minPrice: 0,
8
+ maxPrice: 0,
9
+ pipsLocation: 0,
10
+ hasValidCandles: false,
11
+ candlesOpen: [],
12
+ candlesClose: [],
13
+ candlesLow: [],
14
+ candlesHigh: [],
15
+ dates: [],
16
+ };
15
17
 
16
- const candles = priceCandlesData.priceCandles.candle;
17
- let calculatedMinPrice = Number.MAX_VALUE;
18
- let calculatedMaxPrice = Number.MIN_VALUE;
18
+ if (
19
+ !priceCandlesData?.priceCandles?.candle?.length ||
20
+ !priceCandlesData?.priceCandles?.pipsLocation
21
+ ) {
22
+ return errorResult;
23
+ }
19
24
 
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
- >();
25
+ const { dates, candlesOpen, candlesClose, candlesLow, candlesHigh } =
26
+ priceCandlesData.priceCandles.candle.reduce(
27
+ (acc, candle) => {
28
+ if (!candle) {
29
+ return acc;
30
+ }
30
31
 
31
- candles.forEach((candle) => {
32
- if (!candle) return;
32
+ return {
33
+ dates: [...acc.dates, candle.point],
34
+ candlesOpen: [...acc.candlesOpen, candle.open],
35
+ candlesClose: [...acc.candlesClose, candle.close],
36
+ candlesLow: [...acc.candlesLow, candle.low],
37
+ candlesHigh: [...acc.candlesHigh, candle.high],
38
+ };
39
+ },
40
+ {
41
+ dates: [] as string[],
42
+ candlesOpen: [] as number[],
43
+ candlesClose: [] as number[],
44
+ candlesLow: [] as number[],
45
+ candlesHigh: [] as number[],
46
+ }
47
+ );
33
48
 
34
- if (candle.high > calculatedMaxPrice) {
35
- calculatedMaxPrice = candle.high;
36
- }
37
- if (candle.low < calculatedMinPrice) {
38
- calculatedMinPrice = candle.low;
39
- }
49
+ if (dates.length === 0) {
50
+ return errorResult;
51
+ }
40
52
 
41
- if (candle.point) {
42
- candleMap.set(candle.point, candle);
43
- }
44
- });
53
+ const minPrice = Math.min(...candlesLow);
54
+ const maxPrice = Math.max(...candlesHigh);
45
55
 
46
56
  return {
47
- minPrice: calculatedMinPrice,
48
- maxPrice: calculatedMaxPrice,
57
+ minPrice,
58
+ maxPrice,
59
+ candlesOpen,
60
+ candlesClose,
61
+ candlesLow,
62
+ candlesHigh,
63
+ dates,
49
64
  hasValidCandles: true,
50
- candleMap,
51
- candles,
65
+ pipsLocation: priceCandlesData.priceCandles.pipsLocation,
52
66
  };
53
67
  };
@@ -1,42 +1,55 @@
1
1
  import type { GetSentimentsQuery } from '../../../../gql/types/graphql';
2
- import type { ProcessedSentiment } from '../types';
3
2
 
4
3
  export const processSentiments = (
5
4
  sentimentsData: GetSentimentsQuery | undefined,
6
- xAxisData: string[]
7
- ): ProcessedSentiment[] => {
8
- if (!sentimentsData?.sentiments?.sentiments?.length) {
9
- return [];
5
+ dates: string[]
6
+ ) => {
7
+ if (
8
+ !Array.isArray(dates) ||
9
+ dates.length === 0 ||
10
+ !sentimentsData?.sentiments?.sentiments?.length
11
+ ) {
12
+ return {
13
+ sentimentLongs: [],
14
+ sentimentShorts: [],
15
+ hasValidSentiments: false,
16
+ };
10
17
  }
11
18
 
12
19
  const sentimentMap = new Map(
13
- sentimentsData.sentiments.sentiments
14
- .filter(
15
- (item): item is NonNullable<typeof item> =>
16
- item?.time != null &&
17
- item.sentiment?.shortPercent != null &&
18
- item.sentiment?.longPercent != null
19
- )
20
- .map((item) => [
21
- item.time,
22
- {
23
- shortPercent: item.sentiment.shortPercent,
24
- longPercent: item.sentiment.longPercent,
25
- },
26
- ])
20
+ sentimentsData.sentiments.sentiments.map((item) => [
21
+ item?.time,
22
+ {
23
+ sentimentShorts: item?.sentiment?.shortPercent ?? null,
24
+ sentimentLongs: item?.sentiment?.longPercent ?? null,
25
+ },
26
+ ])
27
27
  );
28
28
 
29
- return xAxisData
30
- .map((time) => {
31
- const sentiment = sentimentMap.get(time);
32
- if (!sentiment) {
33
- return undefined;
34
- }
35
- return [
36
- time,
37
- sentiment.shortPercent,
38
- sentiment.longPercent,
39
- ] as ProcessedSentiment;
40
- })
41
- .filter((item): item is ProcessedSentiment => item !== undefined);
29
+ const { sentimentLongs, sentimentShorts } = dates.reduce(
30
+ (acc, date) => {
31
+ const sentiment = sentimentMap.get(date);
32
+
33
+ return {
34
+ sentimentLongs: [
35
+ ...acc.sentimentLongs,
36
+ sentiment?.sentimentLongs || null,
37
+ ],
38
+ sentimentShorts: [
39
+ ...acc.sentimentShorts,
40
+ sentiment?.sentimentShorts || null,
41
+ ],
42
+ };
43
+ },
44
+ {
45
+ sentimentLongs: [] as (number | null)[],
46
+ sentimentShorts: [] as (number | null)[],
47
+ }
48
+ );
49
+
50
+ return {
51
+ sentimentLongs,
52
+ sentimentShorts,
53
+ hasValidSentiments: true,
54
+ };
42
55
  };
@@ -6,12 +6,14 @@ import type {
6
6
  export const validateData = (
7
7
  priceCandlesData: GetPriceCandlesQuery | undefined,
8
8
  orderPositionData: GetOrderPositionBooksQuery | undefined,
9
- hasValidCandles: boolean
9
+ hasValidCandles: boolean,
10
+ hasValidBooks: boolean,
11
+ hasValidSentiments: boolean
10
12
  ): Error | null => {
11
13
  const hasValidPriceData =
12
14
  (priceCandlesData?.priceCandles?.candle?.length ?? 0) >= 1;
13
15
  const hasValidOrderData =
14
- (orderPositionData?.orderPositionBooks?.length ?? 0) >= 1;
16
+ (orderPositionData?.orderPositionBooks?.books?.length ?? 0) >= 1;
15
17
 
16
18
  if (!hasValidPriceData) {
17
19
  return new Error('Insufficient price candle data');
@@ -22,6 +24,12 @@ export const validateData = (
22
24
  if (!hasValidCandles) {
23
25
  return new Error('Invalid candle data');
24
26
  }
27
+ if (!hasValidBooks) {
28
+ return new Error('Invalid book data');
29
+ }
30
+ if (!hasValidSentiments) {
31
+ return new Error('Invalid sentiment data');
32
+ }
25
33
 
26
34
  return null;
27
35
  };
@@ -2,19 +2,18 @@ import { useLocale } from '@oanda/mono-i18n';
2
2
  import React from 'react';
3
3
 
4
4
  import { BookType } from '../../../gql/types/graphql';
5
- import { BOOKS_THRESHOLDS } from '../../constants';
6
5
  import { LegendBar } from './LegendBar';
7
6
 
8
7
  interface LegendProps {
9
- longValues?: [number, number];
10
- shortValues?: [number, number];
8
+ longValues: (number | undefined)[];
9
+ shortValues: (number | undefined)[];
11
10
  bookType: BookType;
12
11
  isDark: boolean;
13
12
  }
14
13
 
15
14
  export const Legend = ({
16
- longValues = [BOOKS_THRESHOLDS.MIN, BOOKS_THRESHOLDS.MAX],
17
- shortValues = [BOOKS_THRESHOLDS.MIN, BOOKS_THRESHOLDS.MAX],
15
+ longValues,
16
+ shortValues,
18
17
  bookType,
19
18
  isDark,
20
19
  }: LegendProps) => {
@@ -5,7 +5,7 @@ import { COLOR_MAP } from '../../constants';
5
5
  export type LegendType = 'long' | 'short';
6
6
 
7
7
  interface LegendBarProps {
8
- values: number[];
8
+ values: (number | undefined)[];
9
9
  type: LegendType;
10
10
  label: string;
11
11
  isDark: boolean;
@@ -32,10 +32,10 @@ export const LegendBar = ({ values, type, label, isDark }: LegendBarProps) => {
32
32
 
33
33
  <div className="lw-flex lw-w-full lw-justify-between">
34
34
  <span className="lw-whitespace-nowrap lw-font-sans lw-text-xs lw-text-text-primary">
35
- {values[0].toFixed(2)}%
35
+ {values[0] ? `${values[0].toFixed(2)}%` : '—'}
36
36
  </span>
37
37
  <span className="lw-whitespace-nowrap lw-font-sans lw-text-xs lw-text-text-primary">
38
- {`≤ ${values[1].toFixed(2)}`}%
38
+ {values[1] ? `≤ ${values[1].toFixed(2)}%` : '—'}
39
39
  </span>
40
40
  </div>
41
41
  </div>
@@ -2,14 +2,9 @@ import { colorPalette } from '@oanda/labs-widget-common';
2
2
 
3
3
  import { InstrumentId } from './types';
4
4
 
5
- export const BOOKS_THRESHOLDS = {
6
- MIN: 0.15,
7
- MAX: 0.55,
8
- } as const;
9
-
10
5
  export const BUCKET_CONFIG = {
11
6
  MULTIPLIER: 4,
12
- PRICE_PADDING_MULTIPLIER: 2,
7
+ PRICE_MARGIN_MULTIPLIER: 2,
13
8
  } as const;
14
9
 
15
10
  export const TIME_THRESHOLDS = {
@@ -8,6 +8,8 @@ const getOrderPositionBooks = gql`
8
8
  $granularity: Granularity!
9
9
  $maxBookPrice: Float
10
10
  $minBookPrice: Float
11
+ $bucketMultiplier: Int!
12
+ $bucketMargin: Int!
11
13
  ) {
12
14
  orderPositionBooks(
13
15
  instrument: $instrument
@@ -16,14 +18,20 @@ const getOrderPositionBooks = gql`
16
18
  granularity: $granularity
17
19
  maxBookPrice: $maxBookPrice
18
20
  minBookPrice: $minBookPrice
21
+ bucketMultiplier: $bucketMultiplier
22
+ bucketMargin: $bucketMargin
19
23
  ) {
20
- bucketWidth
21
- price
22
- time
23
- buckets {
24
+ books {
25
+ time
24
26
  price
25
- sentiment
27
+ buckets {
28
+ price
29
+ sentiment
30
+ }
26
31
  }
32
+ bucketWidth
33
+ sentimentThresholdMax
34
+ sentimentThresholdMin
27
35
  }
28
36
  }
29
37
  `;
@@ -22,6 +22,7 @@ const getPriceCandles = gql`
22
22
  open
23
23
  close
24
24
  }
25
+ pipsLocation
25
26
  }
26
27
  }
27
28
  `;
@@ -13,9 +13,9 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/
13
13
  * Therefore it is highly recommended to use the babel or swc plugin for production.
14
14
  */
15
15
  const documents = {
16
- '\n query GetOrderPositionBooks(\n $instrument: String!\n $bookType: BookType!\n $timeSpan: TimeSpan!\n $granularity: Granularity!\n $maxBookPrice: Float\n $minBookPrice: Float\n ) {\n orderPositionBooks(\n instrument: $instrument\n bookType: $bookType\n timeSpan: $timeSpan\n granularity: $granularity\n maxBookPrice: $maxBookPrice\n minBookPrice: $minBookPrice\n ) {\n bucketWidth\n price\n time\n buckets {\n price\n sentiment\n }\n }\n }\n':
16
+ '\n query GetOrderPositionBooks(\n $instrument: String!\n $bookType: BookType!\n $timeSpan: TimeSpan!\n $granularity: Granularity!\n $maxBookPrice: Float\n $minBookPrice: Float\n $bucketMultiplier: Int!\n $bucketMargin: Int!\n ) {\n orderPositionBooks(\n instrument: $instrument\n bookType: $bookType\n timeSpan: $timeSpan\n granularity: $granularity\n maxBookPrice: $maxBookPrice\n minBookPrice: $minBookPrice\n bucketMultiplier: $bucketMultiplier\n bucketMargin: $bucketMargin\n ) {\n books {\n time\n price\n buckets {\n price\n sentiment\n }\n }\n bucketWidth\n sentimentThresholdMax\n sentimentThresholdMin\n }\n }\n':
17
17
  types.GetOrderPositionBooksDocument,
18
- '\n query GetPriceCandles(\n $dataSource: DataSource!\n $division: Division!\n $instrument: String!\n $granularity: Granularity!\n $timeSpan: TimeSpan!\n ) {\n priceCandles(\n dataSource: $dataSource\n division: $division\n instrument: $instrument\n granularity: $granularity\n timeSpan: $timeSpan\n ) {\n candle {\n point\n high\n low\n open\n close\n }\n }\n }\n':
18
+ '\n query GetPriceCandles(\n $dataSource: DataSource!\n $division: Division!\n $instrument: String!\n $granularity: Granularity!\n $timeSpan: TimeSpan!\n ) {\n priceCandles(\n dataSource: $dataSource\n division: $division\n instrument: $instrument\n granularity: $granularity\n timeSpan: $timeSpan\n ) {\n candle {\n point\n high\n low\n open\n close\n }\n pipsLocation\n }\n }\n':
19
19
  types.GetPriceCandlesDocument,
20
20
  '\n query GetSentiments(\n $instrument: String!\n $granularity: Granularity!\n $timeSpan: TimeSpan!\n ) {\n sentiments(\n instrument: $instrument\n granularity: $granularity\n timeSpan: $timeSpan\n ) {\n sentiments {\n sentiment {\n longPercent\n shortPercent\n }\n time\n }\n }\n }\n':
21
21
  types.GetSentimentsDocument,
@@ -39,14 +39,14 @@ export function graphql(source: string): unknown;
39
39
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
40
40
  */
41
41
  export function graphql(
42
- source: '\n query GetOrderPositionBooks(\n $instrument: String!\n $bookType: BookType!\n $timeSpan: TimeSpan!\n $granularity: Granularity!\n $maxBookPrice: Float\n $minBookPrice: Float\n ) {\n orderPositionBooks(\n instrument: $instrument\n bookType: $bookType\n timeSpan: $timeSpan\n granularity: $granularity\n maxBookPrice: $maxBookPrice\n minBookPrice: $minBookPrice\n ) {\n bucketWidth\n price\n time\n buckets {\n price\n sentiment\n }\n }\n }\n'
43
- ): (typeof documents)['\n query GetOrderPositionBooks(\n $instrument: String!\n $bookType: BookType!\n $timeSpan: TimeSpan!\n $granularity: Granularity!\n $maxBookPrice: Float\n $minBookPrice: Float\n ) {\n orderPositionBooks(\n instrument: $instrument\n bookType: $bookType\n timeSpan: $timeSpan\n granularity: $granularity\n maxBookPrice: $maxBookPrice\n minBookPrice: $minBookPrice\n ) {\n bucketWidth\n price\n time\n buckets {\n price\n sentiment\n }\n }\n }\n'];
42
+ source: '\n query GetOrderPositionBooks(\n $instrument: String!\n $bookType: BookType!\n $timeSpan: TimeSpan!\n $granularity: Granularity!\n $maxBookPrice: Float\n $minBookPrice: Float\n $bucketMultiplier: Int!\n $bucketMargin: Int!\n ) {\n orderPositionBooks(\n instrument: $instrument\n bookType: $bookType\n timeSpan: $timeSpan\n granularity: $granularity\n maxBookPrice: $maxBookPrice\n minBookPrice: $minBookPrice\n bucketMultiplier: $bucketMultiplier\n bucketMargin: $bucketMargin\n ) {\n books {\n time\n price\n buckets {\n price\n sentiment\n }\n }\n bucketWidth\n sentimentThresholdMax\n sentimentThresholdMin\n }\n }\n'
43
+ ): (typeof documents)['\n query GetOrderPositionBooks(\n $instrument: String!\n $bookType: BookType!\n $timeSpan: TimeSpan!\n $granularity: Granularity!\n $maxBookPrice: Float\n $minBookPrice: Float\n $bucketMultiplier: Int!\n $bucketMargin: Int!\n ) {\n orderPositionBooks(\n instrument: $instrument\n bookType: $bookType\n timeSpan: $timeSpan\n granularity: $granularity\n maxBookPrice: $maxBookPrice\n minBookPrice: $minBookPrice\n bucketMultiplier: $bucketMultiplier\n bucketMargin: $bucketMargin\n ) {\n books {\n time\n price\n buckets {\n price\n sentiment\n }\n }\n bucketWidth\n sentimentThresholdMax\n sentimentThresholdMin\n }\n }\n'];
44
44
  /**
45
45
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
46
46
  */
47
47
  export function graphql(
48
- source: '\n query GetPriceCandles(\n $dataSource: DataSource!\n $division: Division!\n $instrument: String!\n $granularity: Granularity!\n $timeSpan: TimeSpan!\n ) {\n priceCandles(\n dataSource: $dataSource\n division: $division\n instrument: $instrument\n granularity: $granularity\n timeSpan: $timeSpan\n ) {\n candle {\n point\n high\n low\n open\n close\n }\n }\n }\n'
49
- ): (typeof documents)['\n query GetPriceCandles(\n $dataSource: DataSource!\n $division: Division!\n $instrument: String!\n $granularity: Granularity!\n $timeSpan: TimeSpan!\n ) {\n priceCandles(\n dataSource: $dataSource\n division: $division\n instrument: $instrument\n granularity: $granularity\n timeSpan: $timeSpan\n ) {\n candle {\n point\n high\n low\n open\n close\n }\n }\n }\n'];
48
+ source: '\n query GetPriceCandles(\n $dataSource: DataSource!\n $division: Division!\n $instrument: String!\n $granularity: Granularity!\n $timeSpan: TimeSpan!\n ) {\n priceCandles(\n dataSource: $dataSource\n division: $division\n instrument: $instrument\n granularity: $granularity\n timeSpan: $timeSpan\n ) {\n candle {\n point\n high\n low\n open\n close\n }\n pipsLocation\n }\n }\n'
49
+ ): (typeof documents)['\n query GetPriceCandles(\n $dataSource: DataSource!\n $division: Division!\n $instrument: String!\n $granularity: Granularity!\n $timeSpan: TimeSpan!\n ) {\n priceCandles(\n dataSource: $dataSource\n division: $division\n instrument: $instrument\n granularity: $granularity\n timeSpan: $timeSpan\n ) {\n candle {\n point\n high\n low\n open\n close\n }\n pipsLocation\n }\n }\n'];
50
50
  /**
51
51
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
52
52
  */