@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.
- package/CHANGELOG.md +372 -0
- package/dist/main/CrowdViewWidget/Main.js +20 -7
- package/dist/main/CrowdViewWidget/Main.js.map +1 -1
- package/dist/main/CrowdViewWidget/components/Chart/Chart.js +6 -10
- package/dist/main/CrowdViewWidget/components/Chart/Chart.js.map +1 -1
- package/dist/main/CrowdViewWidget/components/Chart/chartOptions.js +46 -20
- package/dist/main/CrowdViewWidget/components/Chart/chartOptions.js.map +1 -1
- package/dist/main/CrowdViewWidget/components/Chart/index.js +4 -4
- package/dist/main/CrowdViewWidget/components/Chart/index.js.map +1 -1
- package/dist/main/CrowdViewWidget/components/Chart/types.js.map +1 -1
- package/dist/main/CrowdViewWidget/components/Chart/useCrowdViewData.js +18 -88
- package/dist/main/CrowdViewWidget/components/Chart/useCrowdViewData.js.map +1 -1
- package/dist/main/CrowdViewWidget/components/Chart/utils/aggregateBuckets.js +37 -0
- package/dist/main/CrowdViewWidget/components/Chart/utils/aggregateBuckets.js.map +1 -0
- package/dist/main/CrowdViewWidget/components/Chart/utils/chartUtils.js +54 -2
- package/dist/main/CrowdViewWidget/components/Chart/utils/chartUtils.js.map +1 -1
- package/dist/main/CrowdViewWidget/components/Chart/utils/getTargetBucketWidth.js +14 -0
- package/dist/main/CrowdViewWidget/components/Chart/utils/getTargetBucketWidth.js.map +1 -0
- package/dist/main/CrowdViewWidget/components/Chart/utils/index.js +83 -0
- package/dist/main/CrowdViewWidget/components/Chart/utils/index.js.map +1 -0
- package/dist/main/CrowdViewWidget/components/Chart/utils/processBuckets.js +29 -0
- package/dist/main/CrowdViewWidget/components/Chart/utils/processBuckets.js.map +1 -0
- package/dist/main/CrowdViewWidget/components/Chart/utils/processOrderPositionBooks.js +23 -0
- package/dist/main/CrowdViewWidget/components/Chart/utils/processOrderPositionBooks.js.map +1 -0
- package/dist/main/CrowdViewWidget/components/Chart/utils/processPriceCandles.js +43 -0
- package/dist/main/CrowdViewWidget/components/Chart/utils/processPriceCandles.js.map +1 -0
- package/dist/main/CrowdViewWidget/components/Chart/utils/validateData.js +23 -0
- package/dist/main/CrowdViewWidget/components/Chart/utils/validateData.js.map +1 -0
- package/dist/main/CrowdViewWidget/components/Legend/Legend.js +5 -3
- package/dist/main/CrowdViewWidget/components/Legend/Legend.js.map +1 -1
- package/dist/main/CrowdViewWidget/constants.js +105 -5
- package/dist/main/CrowdViewWidget/constants.js.map +1 -1
- package/dist/main/CrowdViewWidget/selectConfig.js +18 -60
- package/dist/main/CrowdViewWidget/selectConfig.js.map +1 -1
- package/dist/main/CrowdViewWidget/types.js +20 -0
- package/dist/main/CrowdViewWidget/types.js.map +1 -1
- package/dist/main/translations/sources/en.json +29 -0
- package/dist/module/CrowdViewWidget/Main.js +21 -8
- package/dist/module/CrowdViewWidget/Main.js.map +1 -1
- package/dist/module/CrowdViewWidget/components/Chart/Chart.js +7 -11
- package/dist/module/CrowdViewWidget/components/Chart/Chart.js.map +1 -1
- package/dist/module/CrowdViewWidget/components/Chart/chartOptions.js +47 -21
- package/dist/module/CrowdViewWidget/components/Chart/chartOptions.js.map +1 -1
- package/dist/module/CrowdViewWidget/components/Chart/index.js +1 -1
- package/dist/module/CrowdViewWidget/components/Chart/index.js.map +1 -1
- package/dist/module/CrowdViewWidget/components/Chart/types.js.map +1 -1
- package/dist/module/CrowdViewWidget/components/Chart/useCrowdViewData.js +14 -84
- package/dist/module/CrowdViewWidget/components/Chart/useCrowdViewData.js.map +1 -1
- package/dist/module/CrowdViewWidget/components/Chart/utils/aggregateBuckets.js +29 -0
- package/dist/module/CrowdViewWidget/components/Chart/utils/aggregateBuckets.js.map +1 -0
- package/dist/module/CrowdViewWidget/components/Chart/utils/chartUtils.js +52 -2
- package/dist/module/CrowdViewWidget/components/Chart/utils/chartUtils.js.map +1 -1
- package/dist/module/CrowdViewWidget/components/Chart/utils/getTargetBucketWidth.js +7 -0
- package/dist/module/CrowdViewWidget/components/Chart/utils/getTargetBucketWidth.js.map +1 -0
- package/dist/module/CrowdViewWidget/components/Chart/utils/index.js +8 -0
- package/dist/module/CrowdViewWidget/components/Chart/utils/index.js.map +1 -0
- package/dist/module/CrowdViewWidget/components/Chart/utils/processBuckets.js +22 -0
- package/dist/module/CrowdViewWidget/components/Chart/utils/processBuckets.js.map +1 -0
- package/dist/module/CrowdViewWidget/components/Chart/utils/processOrderPositionBooks.js +16 -0
- package/dist/module/CrowdViewWidget/components/Chart/utils/processOrderPositionBooks.js.map +1 -0
- package/dist/module/CrowdViewWidget/components/Chart/utils/processPriceCandles.js +36 -0
- package/dist/module/CrowdViewWidget/components/Chart/utils/processPriceCandles.js.map +1 -0
- package/dist/module/CrowdViewWidget/components/Chart/utils/validateData.js +16 -0
- package/dist/module/CrowdViewWidget/components/Chart/utils/validateData.js.map +1 -0
- package/dist/module/CrowdViewWidget/components/Legend/Legend.js +5 -3
- package/dist/module/CrowdViewWidget/components/Legend/Legend.js.map +1 -1
- package/dist/module/CrowdViewWidget/constants.js +104 -4
- package/dist/module/CrowdViewWidget/constants.js.map +1 -1
- package/dist/module/CrowdViewWidget/selectConfig.js +3 -45
- package/dist/module/CrowdViewWidget/selectConfig.js.map +1 -1
- package/dist/module/CrowdViewWidget/types.js +19 -1
- package/dist/module/CrowdViewWidget/types.js.map +1 -1
- package/dist/module/translations/sources/en.json +29 -0
- package/dist/types/CrowdViewWidget/components/Chart/index.d.ts +1 -1
- package/dist/types/CrowdViewWidget/components/Chart/types.d.ts +12 -4
- package/dist/types/CrowdViewWidget/components/Chart/utils/aggregateBuckets.d.ts +2 -0
- package/dist/types/CrowdViewWidget/components/Chart/utils/chartUtils.d.ts +12 -2
- package/dist/types/CrowdViewWidget/components/Chart/utils/getTargetBucketWidth.d.ts +3 -0
- package/dist/types/CrowdViewWidget/components/Chart/utils/index.d.ts +7 -0
- package/dist/types/CrowdViewWidget/components/Chart/utils/processBuckets.d.ts +3 -0
- package/dist/types/CrowdViewWidget/components/Chart/utils/processOrderPositionBooks.d.ts +8 -0
- package/dist/types/CrowdViewWidget/components/Chart/utils/processPriceCandles.d.ts +27 -0
- package/dist/types/CrowdViewWidget/components/Chart/utils/validateData.d.ts +2 -0
- package/dist/types/CrowdViewWidget/components/Legend/Legend.d.ts +3 -1
- package/dist/types/CrowdViewWidget/constants.d.ts +11 -3
- package/dist/types/CrowdViewWidget/selectConfig.d.ts +2 -2
- package/dist/types/CrowdViewWidget/types.d.ts +18 -1
- package/dist/types/CrowdViewWidget/utils/instrumentUtils.d.ts +1 -4
- package/lokalise.config.json +1 -1
- package/package.json +4 -3
- package/src/CrowdViewWidget/Main.tsx +25 -10
- package/src/CrowdViewWidget/components/Chart/Chart.tsx +6 -12
- package/src/CrowdViewWidget/components/Chart/chartOptions.ts +70 -36
- package/src/CrowdViewWidget/components/Chart/index.ts +1 -1
- package/src/CrowdViewWidget/components/Chart/types.ts +16 -4
- package/src/CrowdViewWidget/components/Chart/useCrowdViewData.ts +41 -140
- package/src/CrowdViewWidget/components/Chart/utils/aggregateBuckets.ts +44 -0
- package/src/CrowdViewWidget/components/Chart/utils/chartUtils.ts +96 -3
- package/src/CrowdViewWidget/components/Chart/utils/getTargetBucketWidth.ts +13 -0
- package/src/CrowdViewWidget/components/Chart/utils/index.ts +7 -0
- package/src/CrowdViewWidget/components/Chart/utils/processBuckets.ts +43 -0
- package/src/CrowdViewWidget/components/Chart/utils/processOrderPositionBooks.ts +30 -0
- package/src/CrowdViewWidget/components/Chart/utils/processPriceCandles.ts +53 -0
- package/src/CrowdViewWidget/components/Chart/utils/validateData.ts +27 -0
- package/src/CrowdViewWidget/components/Legend/Legend.tsx +13 -2
- package/src/CrowdViewWidget/constants.ts +113 -3
- package/src/CrowdViewWidget/selectConfig.ts +5 -60
- package/src/CrowdViewWidget/types.ts +18 -1
- package/src/translations/sources/en.json +29 -0
- package/test/Main.test.tsx +73 -27
- package/test/components/Chart/utils/chartUtils.test.ts +158 -0
- package/test/components/Legend.test.tsx +6 -1
- package/test/utils/aggregateBuckets.test.ts +82 -0
- package/test/utils/getTargetBucketWidth.test.ts +37 -0
- package/test/utils/instrumentUtils.test.ts +13 -7
- package/test/utils/processBuckets.test.ts +153 -0
- package/test/utils/processOrderPositionBooks.test.ts +127 -0
- package/test/utils/processPriceCandles.test.ts +245 -0
- package/test/utils/validateData.test.ts +201 -0
- package/dist/main/CrowdViewWidget/types/index.js +0 -17
- package/dist/main/CrowdViewWidget/types/index.js.map +0 -1
- package/dist/main/CrowdViewWidget/types/instruments.js +0 -45
- package/dist/main/CrowdViewWidget/types/instruments.js.map +0 -1
- package/dist/module/CrowdViewWidget/types/index.js +0 -2
- package/dist/module/CrowdViewWidget/types/index.js.map +0 -1
- package/dist/module/CrowdViewWidget/types/instruments.js +0 -39
- package/dist/module/CrowdViewWidget/types/instruments.js.map +0 -1
- package/dist/types/CrowdViewWidget/types/index.d.ts +0 -1
- package/dist/types/CrowdViewWidget/types/instruments.d.ts +0 -36
- package/src/CrowdViewWidget/types/index.ts +0 -1
- 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:
|
|
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
|
-
|
|
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:
|
|
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 {
|
|
13
|
+
import { BUCKET_CONFIG, INSTRUMENTS_CONFIG } from '../../constants';
|
|
14
14
|
import type { UseCrowdViewDataProps, UseCrowdViewDataReturn } from './types';
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
143
|
+
bucketWidth: targetBucketWidth,
|
|
144
|
+
precision: INSTRUMENTS_CONFIG[instrument].precision,
|
|
145
|
+
bookType,
|
|
255
146
|
};
|
|
256
|
-
}, [
|
|
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,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
|
|
21
|
-
|
|
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
|
};
|