@tradejs/app 2.0.11 → 2.0.13
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/package.json +7 -7
- package/src/app/components/Backtest/TestCard/Chart/index.tsx +15 -3
- package/src/app/components/Backtest/TestCard/Chart/utils/index.ts +2 -5
- package/src/app/components/Shared/Charts/TimeSeriesXAxis.tsx +51 -0
- package/src/app/components/Strategies/RuntimeStrategyCard.tsx +6 -0
- package/src/app/components/Strategies/RuntimeStrategyChart.tsx +10 -11
- package/src/app/components/Strategies/StrategySnapshotChart.tsx +15 -5
- package/src/app/lib/timeSeriesChart.ts +100 -0
- package/src/app/routes/derivatives/page.tsx +85 -17
- package/src/app/routes/strategies/StrategiesPageClient.tsx +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tradejs/app",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.13",
|
|
4
4
|
"description": "Installable Next.js UI for the TradeJS TypeScript framework: dashboards, backtests, charts, and runtime data.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tradejs",
|
|
@@ -51,12 +51,12 @@
|
|
|
51
51
|
"@emotion/react": "^11.14.0",
|
|
52
52
|
"@langchain/core": "^1.2.3",
|
|
53
53
|
"@langchain/openai": "^1.5.5",
|
|
54
|
-
"@tradejs/connectors": "^2.0.
|
|
55
|
-
"@tradejs/core": "^2.0.
|
|
56
|
-
"@tradejs/indicators": "^2.0.
|
|
57
|
-
"@tradejs/infra": "^2.0.
|
|
58
|
-
"@tradejs/node": "^2.0.
|
|
59
|
-
"@tradejs/types": "^2.0.
|
|
54
|
+
"@tradejs/connectors": "^2.0.13",
|
|
55
|
+
"@tradejs/core": "^2.0.13",
|
|
56
|
+
"@tradejs/indicators": "^2.0.13",
|
|
57
|
+
"@tradejs/infra": "^2.0.13",
|
|
58
|
+
"@tradejs/node": "^2.0.13",
|
|
59
|
+
"@tradejs/types": "^2.0.13",
|
|
60
60
|
"@types/bcryptjs": "2.4.6",
|
|
61
61
|
"@types/lodash": "4.17.24",
|
|
62
62
|
"@types/node": "24.13.3",
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
LineChart,
|
|
8
8
|
CartesianGrid,
|
|
9
9
|
Line,
|
|
10
|
-
XAxis,
|
|
11
10
|
YAxis,
|
|
12
11
|
Tooltip,
|
|
13
12
|
ReferenceLine,
|
|
@@ -18,6 +17,8 @@ import { useTestContext } from '../context';
|
|
|
18
17
|
import { TestCompareList } from '@tradejs/types';
|
|
19
18
|
import { mapOrderLogToChartData, getChartData } from './utils';
|
|
20
19
|
import { getFormatted, getTimeline } from '@tradejs/core/backtest';
|
|
20
|
+
import { formatTimeSeriesTooltipTimestamp } from '#app/lib/timeSeriesChart';
|
|
21
|
+
import { TimeSeriesXAxis } from '#shared/Charts/TimeSeriesXAxis';
|
|
21
22
|
|
|
22
23
|
interface TestCardChartProps {
|
|
23
24
|
height?: string | number;
|
|
@@ -58,6 +59,9 @@ export const TestCardChart = ({ height = '350px' }: TestCardChartProps) => {
|
|
|
58
59
|
|
|
59
60
|
const { formatted: maxAmount } = getFormatted(stat, 'maxAmount');
|
|
60
61
|
const { formatted: minAmount } = getFormatted(stat, 'minAmount');
|
|
62
|
+
const startTimestamp = test.options.start ?? testResult.orderLog[0]?.[0] ?? 0;
|
|
63
|
+
const endTimestamp =
|
|
64
|
+
test.options.end ?? testResult.orderLog.at(-1)?.[0] ?? startTimestamp;
|
|
61
65
|
|
|
62
66
|
return (
|
|
63
67
|
<Box w="100%" minW="600px" h={height} pr={2}>
|
|
@@ -92,12 +96,19 @@ export const TestCardChart = ({ height = '350px' }: TestCardChartProps) => {
|
|
|
92
96
|
position: 'bottom',
|
|
93
97
|
}}
|
|
94
98
|
/>
|
|
95
|
-
<
|
|
99
|
+
<TimeSeriesXAxis
|
|
100
|
+
startTimestamp={startTimestamp}
|
|
101
|
+
endTimestamp={endTimestamp}
|
|
102
|
+
/>
|
|
96
103
|
<YAxis tickCount={10} domain={[stat.minAmount - 10, 'auto']} />
|
|
97
104
|
<Tooltip
|
|
98
105
|
animationDuration={100}
|
|
99
106
|
cursor={false}
|
|
100
|
-
content={
|
|
107
|
+
content={
|
|
108
|
+
<Chart.Tooltip
|
|
109
|
+
labelFormatter={formatTimeSeriesTooltipTimestamp}
|
|
110
|
+
/>
|
|
111
|
+
}
|
|
101
112
|
/>
|
|
102
113
|
|
|
103
114
|
{chart.series.map((item) => (
|
|
@@ -108,6 +119,7 @@ export const TestCardChart = ({ height = '350px' }: TestCardChartProps) => {
|
|
|
108
119
|
stroke={chart.color(item.color)}
|
|
109
120
|
strokeWidth={2}
|
|
110
121
|
dot={false}
|
|
122
|
+
activeDot={{ r: 5, strokeWidth: 2 }}
|
|
111
123
|
/>
|
|
112
124
|
))}
|
|
113
125
|
</LineChart>
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { format } from 'date-fns';
|
|
2
1
|
import {
|
|
3
2
|
SimpleOrderLogData,
|
|
4
3
|
TestResult,
|
|
@@ -11,7 +10,7 @@ const getLineName = (testResult: TestResult) =>
|
|
|
11
10
|
export const mapOrderLogToChartData = (testResult: TestResult) => {
|
|
12
11
|
const data = testResult.orderLog.map(([timestamp, amount]) => ({
|
|
13
12
|
[getLineName(testResult)]: amount,
|
|
14
|
-
timestamp
|
|
13
|
+
timestamp,
|
|
15
14
|
}));
|
|
16
15
|
|
|
17
16
|
const series = [
|
|
@@ -52,8 +51,6 @@ export const getChartData = (testList: TestCompareList, timeline: number[]) => {
|
|
|
52
51
|
const values: Record<string, number> = {};
|
|
53
52
|
|
|
54
53
|
const data = timeline.map((timestamp, ind) => {
|
|
55
|
-
const formattedTimestamp = format(timestamp, 'dd.MM');
|
|
56
|
-
|
|
57
54
|
testList.forEach(({ testResult }) => {
|
|
58
55
|
values[getLineName(testResult)] = getAmountFromOrderLog(
|
|
59
56
|
ind,
|
|
@@ -65,7 +62,7 @@ export const getChartData = (testList: TestCompareList, timeline: number[]) => {
|
|
|
65
62
|
|
|
66
63
|
return {
|
|
67
64
|
...values,
|
|
68
|
-
timestamp
|
|
65
|
+
timestamp,
|
|
69
66
|
};
|
|
70
67
|
});
|
|
71
68
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { XAxis } from 'recharts';
|
|
5
|
+
import {
|
|
6
|
+
buildTimeSeriesTicks,
|
|
7
|
+
formatTimeSeriesAxisTimestamp,
|
|
8
|
+
} from '#app/lib/timeSeriesChart';
|
|
9
|
+
|
|
10
|
+
export const TimeSeriesXAxis = ({
|
|
11
|
+
startTimestamp,
|
|
12
|
+
endTimestamp,
|
|
13
|
+
tickCount = 7,
|
|
14
|
+
minTickGap = 24,
|
|
15
|
+
includeTime,
|
|
16
|
+
}: {
|
|
17
|
+
startTimestamp: number;
|
|
18
|
+
endTimestamp: number;
|
|
19
|
+
tickCount?: number;
|
|
20
|
+
minTickGap?: number;
|
|
21
|
+
includeTime?: boolean;
|
|
22
|
+
}) => {
|
|
23
|
+
const start = Math.min(startTimestamp, endTimestamp);
|
|
24
|
+
const end = Math.max(startTimestamp, endTimestamp);
|
|
25
|
+
const ticks = useMemo(
|
|
26
|
+
() => buildTimeSeriesTicks(start, end, tickCount),
|
|
27
|
+
[end, start, tickCount],
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<XAxis
|
|
32
|
+
dataKey="timestamp"
|
|
33
|
+
type="number"
|
|
34
|
+
scale="time"
|
|
35
|
+
domain={[start, end]}
|
|
36
|
+
ticks={ticks}
|
|
37
|
+
tickFormatter={(timestamp) =>
|
|
38
|
+
formatTimeSeriesAxisTimestamp({
|
|
39
|
+
timestamp,
|
|
40
|
+
startTimestamp: start,
|
|
41
|
+
endTimestamp: end,
|
|
42
|
+
includeTime,
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
tickCount={tickCount}
|
|
46
|
+
interval="preserveStartEnd"
|
|
47
|
+
minTickGap={minTickGap}
|
|
48
|
+
allowDataOverflow
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
@@ -1434,10 +1434,14 @@ const mapRuntimeOrder = (order: RuntimeOrderView): OrdersDrawerOrder => {
|
|
|
1434
1434
|
export const RuntimeStrategyCard = ({
|
|
1435
1435
|
strategy,
|
|
1436
1436
|
provider,
|
|
1437
|
+
startTimestamp,
|
|
1438
|
+
endTimestamp,
|
|
1437
1439
|
onUpdated,
|
|
1438
1440
|
}: {
|
|
1439
1441
|
strategy: RuntimeStrategyView;
|
|
1440
1442
|
provider: string;
|
|
1443
|
+
startTimestamp: number;
|
|
1444
|
+
endTimestamp: number;
|
|
1441
1445
|
onUpdated: () => Promise<void> | void;
|
|
1442
1446
|
}) => {
|
|
1443
1447
|
const [configOpen, setConfigOpen] = useState(false);
|
|
@@ -2179,6 +2183,8 @@ export const RuntimeStrategyCard = ({
|
|
|
2179
2183
|
orderLog={strategy.orderLog}
|
|
2180
2184
|
stat={strategy.stat}
|
|
2181
2185
|
aiGateChanges={strategy.aiGateChanges}
|
|
2186
|
+
startTimestamp={startTimestamp}
|
|
2187
|
+
endTimestamp={endTimestamp}
|
|
2182
2188
|
/>
|
|
2183
2189
|
|
|
2184
2190
|
<SimpleGrid columns={{ base: 4, md: 8 }} p={4}>
|
|
@@ -10,18 +10,20 @@ import {
|
|
|
10
10
|
ReferenceLine,
|
|
11
11
|
ResponsiveContainer,
|
|
12
12
|
Tooltip,
|
|
13
|
-
XAxis,
|
|
14
13
|
YAxis,
|
|
15
14
|
} from 'recharts';
|
|
16
|
-
import { format } from 'date-fns';
|
|
17
15
|
import { getFormatted } from '@tradejs/core/backtest';
|
|
18
16
|
import type { SimpleOrderLogData, TestStat } from '@tradejs/types';
|
|
19
17
|
import type { RuntimeStrategyAiGateChange } from '#app/lib/runtimeStrategies';
|
|
18
|
+
import { formatTimeSeriesTooltipTimestamp } from '#app/lib/timeSeriesChart';
|
|
19
|
+
import { TimeSeriesXAxis } from '#shared/Charts/TimeSeriesXAxis';
|
|
20
20
|
|
|
21
21
|
interface RuntimeStrategyChartProps {
|
|
22
22
|
orderLog: SimpleOrderLogData;
|
|
23
23
|
stat: TestStat;
|
|
24
24
|
aiGateChanges: RuntimeStrategyAiGateChange[];
|
|
25
|
+
startTimestamp: number;
|
|
26
|
+
endTimestamp: number;
|
|
25
27
|
height?: string | number;
|
|
26
28
|
}
|
|
27
29
|
|
|
@@ -29,6 +31,8 @@ export const RuntimeStrategyChart = ({
|
|
|
29
31
|
orderLog,
|
|
30
32
|
stat,
|
|
31
33
|
aiGateChanges,
|
|
34
|
+
startTimestamp,
|
|
35
|
+
endTimestamp,
|
|
32
36
|
height = '350px',
|
|
33
37
|
}: RuntimeStrategyChartProps) => {
|
|
34
38
|
const chartData = useMemo(
|
|
@@ -116,12 +120,9 @@ export const RuntimeStrategyChart = ({
|
|
|
116
120
|
}}
|
|
117
121
|
/>
|
|
118
122
|
))}
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
scale="time"
|
|
123
|
-
domain={['dataMin', 'dataMax']}
|
|
124
|
-
tickFormatter={(timestamp) => format(timestamp, 'dd.MM')}
|
|
123
|
+
<TimeSeriesXAxis
|
|
124
|
+
startTimestamp={startTimestamp}
|
|
125
|
+
endTimestamp={endTimestamp}
|
|
125
126
|
/>
|
|
126
127
|
<YAxis tickCount={10} domain={[stat.minAmount - 10, 'auto']} />
|
|
127
128
|
<Tooltip
|
|
@@ -129,9 +130,7 @@ export const RuntimeStrategyChart = ({
|
|
|
129
130
|
cursor={false}
|
|
130
131
|
content={
|
|
131
132
|
<Chart.Tooltip
|
|
132
|
-
labelFormatter={
|
|
133
|
-
format(Number(timestamp), 'dd.MM.yyyy HH:mm')
|
|
134
|
-
}
|
|
133
|
+
labelFormatter={formatTimeSeriesTooltipTimestamp}
|
|
135
134
|
/>
|
|
136
135
|
}
|
|
137
136
|
/>
|
|
@@ -9,11 +9,11 @@ import {
|
|
|
9
9
|
LineChart,
|
|
10
10
|
ResponsiveContainer,
|
|
11
11
|
Tooltip,
|
|
12
|
-
XAxis,
|
|
13
12
|
YAxis,
|
|
14
13
|
} from 'recharts';
|
|
15
|
-
import { format } from 'date-fns';
|
|
16
14
|
import type { SimpleOrderLogData } from '@tradejs/types';
|
|
15
|
+
import { formatTimeSeriesTooltipTimestamp } from '#app/lib/timeSeriesChart';
|
|
16
|
+
import { TimeSeriesXAxis } from '#shared/Charts/TimeSeriesXAxis';
|
|
17
17
|
|
|
18
18
|
export const StrategySnapshotChart = ({
|
|
19
19
|
orderLog,
|
|
@@ -27,7 +27,7 @@ export const StrategySnapshotChart = ({
|
|
|
27
27
|
const chartData = useMemo(
|
|
28
28
|
() => ({
|
|
29
29
|
data: orderLog.map(([timestamp, amount]) => ({
|
|
30
|
-
timestamp
|
|
30
|
+
timestamp,
|
|
31
31
|
equity: amount,
|
|
32
32
|
})),
|
|
33
33
|
series: [
|
|
@@ -59,6 +59,8 @@ export const StrategySnapshotChart = ({
|
|
|
59
59
|
|
|
60
60
|
const values = orderLog.map(([, amount]) => amount);
|
|
61
61
|
const minValue = Math.min(...values);
|
|
62
|
+
const startTimestamp = orderLog[0][0];
|
|
63
|
+
const endTimestamp = orderLog[orderLog.length - 1][0];
|
|
62
64
|
|
|
63
65
|
return (
|
|
64
66
|
<Box w="100%" minW="600px" h={height} pr={2}>
|
|
@@ -66,7 +68,10 @@ export const StrategySnapshotChart = ({
|
|
|
66
68
|
<Chart.Root maxH="md" chart={chart}>
|
|
67
69
|
<LineChart data={chart.data}>
|
|
68
70
|
<CartesianGrid stroke={chart.color('border')} vertical={false} />
|
|
69
|
-
<
|
|
71
|
+
<TimeSeriesXAxis
|
|
72
|
+
startTimestamp={startTimestamp}
|
|
73
|
+
endTimestamp={endTimestamp}
|
|
74
|
+
/>
|
|
70
75
|
<YAxis
|
|
71
76
|
tickCount={10}
|
|
72
77
|
domain={[Math.min(minValue - 10, 0), 'auto']}
|
|
@@ -74,7 +79,11 @@ export const StrategySnapshotChart = ({
|
|
|
74
79
|
<Tooltip
|
|
75
80
|
animationDuration={100}
|
|
76
81
|
cursor={false}
|
|
77
|
-
content={
|
|
82
|
+
content={
|
|
83
|
+
<Chart.Tooltip
|
|
84
|
+
labelFormatter={formatTimeSeriesTooltipTimestamp}
|
|
85
|
+
/>
|
|
86
|
+
}
|
|
78
87
|
/>
|
|
79
88
|
{chart.series.map((item) => (
|
|
80
89
|
<Line
|
|
@@ -84,6 +93,7 @@ export const StrategySnapshotChart = ({
|
|
|
84
93
|
stroke={chart.color(item.color)}
|
|
85
94
|
strokeWidth={2}
|
|
86
95
|
dot={false}
|
|
96
|
+
activeDot={{ r: 5, strokeWidth: 2 }}
|
|
87
97
|
/>
|
|
88
98
|
))}
|
|
89
99
|
</LineChart>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { format } from 'date-fns';
|
|
2
|
+
|
|
3
|
+
const SHORT_TIME_RANGE_MS = 48 * 60 * 60 * 1000;
|
|
4
|
+
|
|
5
|
+
type TooltipPayloadEntry = {
|
|
6
|
+
payload?: unknown;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const getValidDate = (value: unknown) => {
|
|
10
|
+
if (
|
|
11
|
+
value === null ||
|
|
12
|
+
value === undefined ||
|
|
13
|
+
typeof value === 'boolean' ||
|
|
14
|
+
(typeof value === 'string' && value.trim() === '')
|
|
15
|
+
) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let timestamp: number;
|
|
20
|
+
try {
|
|
21
|
+
timestamp = Number(value);
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!Number.isFinite(timestamp)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const date = new Date(timestamp);
|
|
31
|
+
return Number.isNaN(date.getTime()) ? null : date;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const buildTimeSeriesTicks = (
|
|
35
|
+
startTimestamp: number,
|
|
36
|
+
endTimestamp: number,
|
|
37
|
+
tickCount = 7,
|
|
38
|
+
) => {
|
|
39
|
+
if (!getValidDate(startTimestamp) || !getValidDate(endTimestamp)) {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const start = Math.min(startTimestamp, endTimestamp);
|
|
44
|
+
const end = Math.max(startTimestamp, endTimestamp);
|
|
45
|
+
if (start === end) {
|
|
46
|
+
return [start];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const count = Math.max(2, Math.trunc(tickCount));
|
|
50
|
+
const ticks = Array.from({ length: count }, (_, index) =>
|
|
51
|
+
index === count - 1
|
|
52
|
+
? end
|
|
53
|
+
: Math.round(start + ((end - start) * index) / (count - 1)),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return [...new Set(ticks)];
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const formatTimeSeriesAxisTimestamp = ({
|
|
60
|
+
timestamp,
|
|
61
|
+
startTimestamp,
|
|
62
|
+
endTimestamp,
|
|
63
|
+
includeTime,
|
|
64
|
+
}: {
|
|
65
|
+
timestamp: number;
|
|
66
|
+
startTimestamp: number;
|
|
67
|
+
endTimestamp: number;
|
|
68
|
+
includeTime?: boolean;
|
|
69
|
+
}) => {
|
|
70
|
+
const date = getValidDate(timestamp);
|
|
71
|
+
if (!date) {
|
|
72
|
+
return String(timestamp);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return format(
|
|
76
|
+
date,
|
|
77
|
+
includeTime ?? endTimestamp - startTimestamp <= SHORT_TIME_RANGE_MS
|
|
78
|
+
? 'dd.MM HH:mm'
|
|
79
|
+
: 'dd.MM',
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const formatTimeSeriesTooltipTimestamp = (
|
|
84
|
+
value: unknown,
|
|
85
|
+
payload: ReadonlyArray<TooltipPayloadEntry> = [],
|
|
86
|
+
) => {
|
|
87
|
+
const payloadTimestamp = payload.reduce<unknown>((timestamp, item) => {
|
|
88
|
+
if (timestamp !== undefined) {
|
|
89
|
+
return timestamp;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const row = item.payload;
|
|
93
|
+
return row && typeof row === 'object' && 'timestamp' in row
|
|
94
|
+
? row.timestamp
|
|
95
|
+
: undefined;
|
|
96
|
+
}, undefined);
|
|
97
|
+
const date = getValidDate(value) ?? getValidDate(payloadTimestamp);
|
|
98
|
+
|
|
99
|
+
return date ? format(date, 'dd.MM.yyyy HH:mm') : String(value ?? '');
|
|
100
|
+
};
|
|
@@ -33,13 +33,14 @@ import {
|
|
|
33
33
|
ReferenceLine,
|
|
34
34
|
ResponsiveContainer,
|
|
35
35
|
Tooltip,
|
|
36
|
-
XAxis,
|
|
37
36
|
YAxis,
|
|
38
37
|
} from 'recharts';
|
|
39
38
|
import { format } from 'date-fns';
|
|
40
39
|
import { FiBarChart2 } from 'react-icons/fi';
|
|
41
40
|
import { API } from '@tradejs/core/api';
|
|
42
41
|
import { buildKlinePath } from '#app/lib/marketRoutes';
|
|
42
|
+
import { formatTimeSeriesTooltipTimestamp } from '#app/lib/timeSeriesChart';
|
|
43
|
+
import { TimeSeriesXAxis } from '#shared/Charts/TimeSeriesXAxis';
|
|
43
44
|
import { EmptyState, Segment, Select, toaster } from '#ui';
|
|
44
45
|
|
|
45
46
|
type SummaryItem = {
|
|
@@ -91,6 +92,11 @@ type PriceResponse = {
|
|
|
91
92
|
data?: PriceRow[];
|
|
92
93
|
};
|
|
93
94
|
|
|
95
|
+
type ChartWindow = {
|
|
96
|
+
startTimestamp: number;
|
|
97
|
+
endTimestamp: number;
|
|
98
|
+
};
|
|
99
|
+
|
|
94
100
|
type BiasTone = 'teal' | 'green' | 'red' | 'orange' | 'gray';
|
|
95
101
|
|
|
96
102
|
type SymbolMetrics = {
|
|
@@ -224,12 +230,6 @@ const getChartDomain = (
|
|
|
224
230
|
return [min - basePadding, max + basePadding];
|
|
225
231
|
};
|
|
226
232
|
|
|
227
|
-
const formatTimeLabel = (value: string) => {
|
|
228
|
-
const parsed = new Date(value);
|
|
229
|
-
if (Number.isNaN(parsed.getTime())) return value;
|
|
230
|
-
return format(parsed, 'dd.MM HH:mm');
|
|
231
|
-
};
|
|
232
|
-
|
|
233
233
|
const formatFullTime = (value: string | null | undefined) => {
|
|
234
234
|
if (!value) return 'n/a';
|
|
235
235
|
const parsed = new Date(value);
|
|
@@ -572,7 +572,7 @@ const ChartCard = ({
|
|
|
572
572
|
|
|
573
573
|
const mapRowsToChartRows = (rows: DetailRow[]) =>
|
|
574
574
|
rows.map((row) => ({
|
|
575
|
-
timestamp:
|
|
575
|
+
timestamp: new Date(row.ts).getTime(),
|
|
576
576
|
openInterest: toFiniteNumber(row.open_interest) ?? 0,
|
|
577
577
|
funding: (toFiniteNumber(row.funding_rate) ?? 0) * 10_000,
|
|
578
578
|
longLiquidations: -(toFiniteNumber(row.liq_long) ?? 0),
|
|
@@ -582,15 +582,17 @@ const mapRowsToChartRows = (rows: DetailRow[]) =>
|
|
|
582
582
|
const mapPriceRowsToChartRows = (rows: PriceRow[]) =>
|
|
583
583
|
rows.map((row) => ({
|
|
584
584
|
price: toFiniteNumber(row.close) ?? 0,
|
|
585
|
-
timestamp:
|
|
585
|
+
timestamp: row.timestamp,
|
|
586
586
|
}));
|
|
587
587
|
|
|
588
588
|
const SymbolPriceCard = ({
|
|
589
589
|
symbol,
|
|
590
590
|
rows,
|
|
591
|
+
window,
|
|
591
592
|
}: {
|
|
592
593
|
symbol: string;
|
|
593
594
|
rows: PriceRow[];
|
|
595
|
+
window: ChartWindow;
|
|
594
596
|
}) => {
|
|
595
597
|
const theme = SYMBOL_THEMES[symbol];
|
|
596
598
|
const symbolLabel = getSymbolLabel(symbol);
|
|
@@ -644,9 +646,21 @@ const SymbolPriceCard = ({
|
|
|
644
646
|
stroke={priceChart.color('border')}
|
|
645
647
|
vertical={false}
|
|
646
648
|
/>
|
|
647
|
-
<
|
|
649
|
+
<TimeSeriesXAxis
|
|
650
|
+
startTimestamp={window.startTimestamp}
|
|
651
|
+
endTimestamp={window.endTimestamp}
|
|
652
|
+
tickCount={5}
|
|
653
|
+
minTickGap={36}
|
|
654
|
+
/>
|
|
648
655
|
<YAxis domain={priceDomain} tickFormatter={formatPrice} />
|
|
649
|
-
<Tooltip
|
|
656
|
+
<Tooltip
|
|
657
|
+
cursor={false}
|
|
658
|
+
content={
|
|
659
|
+
<Chart.Tooltip
|
|
660
|
+
labelFormatter={formatTimeSeriesTooltipTimestamp}
|
|
661
|
+
/>
|
|
662
|
+
}
|
|
663
|
+
/>
|
|
650
664
|
<Area
|
|
651
665
|
type="monotone"
|
|
652
666
|
dataKey={priceChart.key('price') as string}
|
|
@@ -667,9 +681,11 @@ const SymbolPriceCard = ({
|
|
|
667
681
|
const SymbolOpenInterestCard = ({
|
|
668
682
|
symbol,
|
|
669
683
|
rows,
|
|
684
|
+
window,
|
|
670
685
|
}: {
|
|
671
686
|
symbol: string;
|
|
672
687
|
rows: DetailRow[];
|
|
688
|
+
window: ChartWindow;
|
|
673
689
|
}) => {
|
|
674
690
|
const theme = SYMBOL_THEMES[symbol];
|
|
675
691
|
const symbolLabel = getSymbolLabel(symbol);
|
|
@@ -722,9 +738,21 @@ const SymbolOpenInterestCard = ({
|
|
|
722
738
|
stroke={oiChart.color('border')}
|
|
723
739
|
vertical={false}
|
|
724
740
|
/>
|
|
725
|
-
<
|
|
741
|
+
<TimeSeriesXAxis
|
|
742
|
+
startTimestamp={window.startTimestamp}
|
|
743
|
+
endTimestamp={window.endTimestamp}
|
|
744
|
+
tickCount={5}
|
|
745
|
+
minTickGap={36}
|
|
746
|
+
/>
|
|
726
747
|
<YAxis domain={oiDomain} tickFormatter={formatAxisCompact} />
|
|
727
|
-
<Tooltip
|
|
748
|
+
<Tooltip
|
|
749
|
+
cursor={false}
|
|
750
|
+
content={
|
|
751
|
+
<Chart.Tooltip
|
|
752
|
+
labelFormatter={formatTimeSeriesTooltipTimestamp}
|
|
753
|
+
/>
|
|
754
|
+
}
|
|
755
|
+
/>
|
|
728
756
|
<Area
|
|
729
757
|
type="monotone"
|
|
730
758
|
dataKey={oiChart.key('openInterest') as string}
|
|
@@ -745,9 +773,11 @@ const SymbolOpenInterestCard = ({
|
|
|
745
773
|
const SymbolFundingCard = ({
|
|
746
774
|
symbol,
|
|
747
775
|
rows,
|
|
776
|
+
window,
|
|
748
777
|
}: {
|
|
749
778
|
symbol: string;
|
|
750
779
|
rows: DetailRow[];
|
|
780
|
+
window: ChartWindow;
|
|
751
781
|
}) => {
|
|
752
782
|
const theme = SYMBOL_THEMES[symbol];
|
|
753
783
|
const symbolLabel = getSymbolLabel(symbol);
|
|
@@ -781,9 +811,21 @@ const SymbolFundingCard = ({
|
|
|
781
811
|
stroke={fundingChart.color('gray.600')}
|
|
782
812
|
strokeDasharray="4 4"
|
|
783
813
|
/>
|
|
784
|
-
<
|
|
814
|
+
<TimeSeriesXAxis
|
|
815
|
+
startTimestamp={window.startTimestamp}
|
|
816
|
+
endTimestamp={window.endTimestamp}
|
|
817
|
+
tickCount={5}
|
|
818
|
+
minTickGap={36}
|
|
819
|
+
/>
|
|
785
820
|
<YAxis tickFormatter={(value) => `${value} bps`} />
|
|
786
|
-
<Tooltip
|
|
821
|
+
<Tooltip
|
|
822
|
+
cursor={false}
|
|
823
|
+
content={
|
|
824
|
+
<Chart.Tooltip
|
|
825
|
+
labelFormatter={formatTimeSeriesTooltipTimestamp}
|
|
826
|
+
/>
|
|
827
|
+
}
|
|
828
|
+
/>
|
|
787
829
|
<Bar
|
|
788
830
|
dataKey={fundingChart.key('funding') as string}
|
|
789
831
|
isAnimationActive={false}
|
|
@@ -810,9 +852,11 @@ const SymbolFundingCard = ({
|
|
|
810
852
|
const SymbolLiquidationCard = ({
|
|
811
853
|
symbol,
|
|
812
854
|
rows,
|
|
855
|
+
window,
|
|
813
856
|
}: {
|
|
814
857
|
symbol: string;
|
|
815
858
|
rows: DetailRow[];
|
|
859
|
+
window: ChartWindow;
|
|
816
860
|
}) => {
|
|
817
861
|
const theme = SYMBOL_THEMES[symbol];
|
|
818
862
|
const symbolLabel = getSymbolLabel(symbol);
|
|
@@ -849,9 +893,21 @@ const SymbolLiquidationCard = ({
|
|
|
849
893
|
stroke={liquidationChart.color('gray.600')}
|
|
850
894
|
strokeDasharray="4 4"
|
|
851
895
|
/>
|
|
852
|
-
<
|
|
896
|
+
<TimeSeriesXAxis
|
|
897
|
+
startTimestamp={window.startTimestamp}
|
|
898
|
+
endTimestamp={window.endTimestamp}
|
|
899
|
+
tickCount={5}
|
|
900
|
+
minTickGap={28}
|
|
901
|
+
/>
|
|
853
902
|
<YAxis tickFormatter={formatAxisCompact} />
|
|
854
|
-
<Tooltip
|
|
903
|
+
<Tooltip
|
|
904
|
+
cursor={false}
|
|
905
|
+
content={
|
|
906
|
+
<Chart.Tooltip
|
|
907
|
+
labelFormatter={formatTimeSeriesTooltipTimestamp}
|
|
908
|
+
/>
|
|
909
|
+
}
|
|
910
|
+
/>
|
|
855
911
|
<Legend />
|
|
856
912
|
<Bar
|
|
857
913
|
dataKey={liquidationChart.key('longLiquidations') as string}
|
|
@@ -883,6 +939,13 @@ const DerivativesPage = () => {
|
|
|
883
939
|
const [pricesBySymbol, setPricesBySymbol] = useState<
|
|
884
940
|
Record<string, PriceRow[]>
|
|
885
941
|
>({});
|
|
942
|
+
const [chartWindow, setChartWindow] = useState<ChartWindow>(() => {
|
|
943
|
+
const endTimestamp = Date.now();
|
|
944
|
+
return {
|
|
945
|
+
startTimestamp: endTimestamp - 24 * 60 * 60 * 1000,
|
|
946
|
+
endTimestamp,
|
|
947
|
+
};
|
|
948
|
+
});
|
|
886
949
|
const [summaryLoading, setSummaryLoading] = useState(false);
|
|
887
950
|
const [detailLoading, setDetailLoading] = useState(false);
|
|
888
951
|
const [summaryError, setSummaryError] = useState('');
|
|
@@ -959,6 +1022,7 @@ const DerivativesPage = () => {
|
|
|
959
1022
|
responses.map(([symbol, payload]) => [symbol, payload.priceRows]),
|
|
960
1023
|
),
|
|
961
1024
|
);
|
|
1025
|
+
setChartWindow({ startTimestamp: from, endTimestamp: now });
|
|
962
1026
|
} catch (err) {
|
|
963
1027
|
const message =
|
|
964
1028
|
(err as Error)?.message ||
|
|
@@ -1101,6 +1165,7 @@ const DerivativesPage = () => {
|
|
|
1101
1165
|
key={`${symbol}:price`}
|
|
1102
1166
|
symbol={symbol}
|
|
1103
1167
|
rows={pricesBySymbol[symbol] ?? []}
|
|
1168
|
+
window={chartWindow}
|
|
1104
1169
|
/>
|
|
1105
1170
|
))}
|
|
1106
1171
|
</SimpleGrid>
|
|
@@ -1111,6 +1176,7 @@ const DerivativesPage = () => {
|
|
|
1111
1176
|
key={`${symbol}:oi`}
|
|
1112
1177
|
symbol={symbol}
|
|
1113
1178
|
rows={detailsBySymbol[symbol] ?? []}
|
|
1179
|
+
window={chartWindow}
|
|
1114
1180
|
/>
|
|
1115
1181
|
))}
|
|
1116
1182
|
</SimpleGrid>
|
|
@@ -1121,6 +1187,7 @@ const DerivativesPage = () => {
|
|
|
1121
1187
|
key={`${symbol}:funding`}
|
|
1122
1188
|
symbol={symbol}
|
|
1123
1189
|
rows={detailsBySymbol[symbol] ?? []}
|
|
1190
|
+
window={chartWindow}
|
|
1124
1191
|
/>
|
|
1125
1192
|
))}
|
|
1126
1193
|
</SimpleGrid>
|
|
@@ -1131,6 +1198,7 @@ const DerivativesPage = () => {
|
|
|
1131
1198
|
key={`${symbol}:liq`}
|
|
1132
1199
|
symbol={symbol}
|
|
1133
1200
|
rows={detailsBySymbol[symbol] ?? []}
|
|
1201
|
+
window={chartWindow}
|
|
1134
1202
|
/>
|
|
1135
1203
|
))}
|
|
1136
1204
|
</SimpleGrid>
|
|
@@ -89,6 +89,17 @@ const RuntimeStrategiesContent = () => {
|
|
|
89
89
|
const isSnapshotMode = mode === 'replay' || mode === 'ai';
|
|
90
90
|
const snapshotModeLabel = mode === 'replay' ? 'Replay' : 'AI';
|
|
91
91
|
const snapshotModeLabelLower = snapshotModeLabel.toLowerCase();
|
|
92
|
+
const runtimeChartWindow = useMemo(() => {
|
|
93
|
+
if (!runtimeData) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
startTimestamp:
|
|
99
|
+
runtimeData.generatedAt - runtimeData.hours * 60 * 60 * 1000,
|
|
100
|
+
endTimestamp: runtimeData.generatedAt,
|
|
101
|
+
};
|
|
102
|
+
}, [runtimeData]);
|
|
92
103
|
|
|
93
104
|
useEffect(() => {
|
|
94
105
|
setMode(routeMode);
|
|
@@ -557,11 +568,14 @@ const RuntimeStrategiesContent = () => {
|
|
|
557
568
|
|
|
558
569
|
{!loading &&
|
|
559
570
|
mode === 'runtime' &&
|
|
571
|
+
runtimeChartWindow &&
|
|
560
572
|
filteredRuntimeStrategies.map((strategy) => (
|
|
561
573
|
<RuntimeStrategyCard
|
|
562
574
|
key={strategy.runtimeKey}
|
|
563
575
|
strategy={strategy}
|
|
564
576
|
provider={runtimeData?.provider || 'bybit'}
|
|
577
|
+
startTimestamp={runtimeChartWindow.startTimestamp}
|
|
578
|
+
endTimestamp={runtimeChartWindow.endTimestamp}
|
|
565
579
|
onUpdated={load}
|
|
566
580
|
/>
|
|
567
581
|
))}
|