@tradejs/app 1.0.0

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 (126) hide show
  1. package/README.md +12 -0
  2. package/bin/tradejs-app.mjs +54 -0
  3. package/next-env.d.ts +6 -0
  4. package/next.config.mjs +31 -0
  5. package/package.json +60 -0
  6. package/src/app/actions/ai.ts +33 -0
  7. package/src/app/actions/backtest.ts +55 -0
  8. package/src/app/actions/kline.ts +18 -0
  9. package/src/app/actions/scanner.ts +10 -0
  10. package/src/app/actions/signal.ts +19 -0
  11. package/src/app/api/ai/route.ts +151 -0
  12. package/src/app/api/auth/[...nextauth]/route.ts +5 -0
  13. package/src/app/api/backtest/files/route.ts +60 -0
  14. package/src/app/api/backtest/order-log/[strategy]/[name]/route.ts +47 -0
  15. package/src/app/api/backtest/result/[strategy]/[name]/route.ts +63 -0
  16. package/src/app/api/backtest/test/[strategy]/[name]/route.ts +57 -0
  17. package/src/app/api/cron/route.ts +4 -0
  18. package/src/app/api/derivatives/[symbol]/[interval]/route.ts +57 -0
  19. package/src/app/api/derivatives/summary/route.ts +20 -0
  20. package/src/app/api/files/screenshot/[name]/route.ts +42 -0
  21. package/src/app/api/indicators/route.ts +24 -0
  22. package/src/app/api/kline/[provider]/[symbol]/[interval]/route.ts +123 -0
  23. package/src/app/api/scanner/[provider]/route.ts +41 -0
  24. package/src/app/api/scanner/route.ts +31 -0
  25. package/src/app/api/signal/[symbol]/[signalId]/route.ts +42 -0
  26. package/src/app/api/spread/[symbol]/[interval]/route.ts +57 -0
  27. package/src/app/api/spread/summary/route.ts +20 -0
  28. package/src/app/auth.ts +76 -0
  29. package/src/app/components/Backtest/CompareList/index.tsx +34 -0
  30. package/src/app/components/Backtest/TestCard/Chart/index.tsx +118 -0
  31. package/src/app/components/Backtest/TestCard/Chart/utils/index.ts +81 -0
  32. package/src/app/components/Backtest/TestCard/CompareButton/index.tsx +21 -0
  33. package/src/app/components/Backtest/TestCard/ConfigDrawer/JsonCodeBlock.tsx +46 -0
  34. package/src/app/components/Backtest/TestCard/ConfigDrawer/index.tsx +94 -0
  35. package/src/app/components/Backtest/TestCard/DeleteButton/index.tsx +128 -0
  36. package/src/app/components/Backtest/TestCard/FavoriteIndicator/index.tsx +18 -0
  37. package/src/app/components/Backtest/TestCard/OpenDashboardButton/index.tsx +40 -0
  38. package/src/app/components/Backtest/TestCard/OpenReportButton/index.tsx +24 -0
  39. package/src/app/components/Backtest/TestCard/Root/index.tsx +55 -0
  40. package/src/app/components/Backtest/TestCard/Skeleton/index.tsx +21 -0
  41. package/src/app/components/Backtest/TestCard/Stat/index.tsx +119 -0
  42. package/src/app/components/Backtest/TestCard/Title/index.tsx +84 -0
  43. package/src/app/components/Backtest/TestCard/context.ts +14 -0
  44. package/src/app/components/Backtest/TestCard/index.ts +28 -0
  45. package/src/app/components/Backtest/TestList/index.tsx +124 -0
  46. package/src/app/components/Dashboard/AiDrawer/Message.tsx +34 -0
  47. package/src/app/components/Dashboard/AiDrawer/index.tsx +163 -0
  48. package/src/app/components/Dashboard/KlineChart/figures/backtestFigureTypes.ts +7 -0
  49. package/src/app/components/Dashboard/KlineChart/figures/backtestMarkersPointFigure.ts +76 -0
  50. package/src/app/components/Dashboard/KlineChart/figures/circle.ts +15 -0
  51. package/src/app/components/Dashboard/KlineChart/figures/diamond.ts +25 -0
  52. package/src/app/components/Dashboard/KlineChart/figures/entryLinePointFigure.ts +1 -0
  53. package/src/app/components/Dashboard/KlineChart/figures/entryPointsPointFigure.ts +1 -0
  54. package/src/app/components/Dashboard/KlineChart/figures/entryZonePointFigure.ts +1 -0
  55. package/src/app/components/Dashboard/KlineChart/figures/index.ts +213 -0
  56. package/src/app/components/Dashboard/KlineChart/figures/label.ts +14 -0
  57. package/src/app/components/Dashboard/KlineChart/figures/rectangle.ts +20 -0
  58. package/src/app/components/Dashboard/KlineChart/figures/square.ts +21 -0
  59. package/src/app/components/Dashboard/KlineChart/figures/star.ts +39 -0
  60. package/src/app/components/Dashboard/KlineChart/figures/tradeZonePointFigure.ts +44 -0
  61. package/src/app/components/Dashboard/KlineChart/figures/trendLinePointFigure.ts +37 -0
  62. package/src/app/components/Dashboard/KlineChart/figures/trendLinePointsPointFigure.ts +26 -0
  63. package/src/app/components/Dashboard/KlineChart/figures/triangle.ts +23 -0
  64. package/src/app/components/Dashboard/KlineChart/hooks/index.ts +14 -0
  65. package/src/app/components/Dashboard/KlineChart/hooks/indicatorShared.ts +30 -0
  66. package/src/app/components/Dashboard/KlineChart/hooks/useAtrIndicator.ts +75 -0
  67. package/src/app/components/Dashboard/KlineChart/hooks/useBacktest.ts +533 -0
  68. package/src/app/components/Dashboard/KlineChart/hooks/useBbIndicator.ts +74 -0
  69. package/src/app/components/Dashboard/KlineChart/hooks/useBtcCorrelation.ts +155 -0
  70. package/src/app/components/Dashboard/KlineChart/hooks/useBtcIndicator.ts +185 -0
  71. package/src/app/components/Dashboard/KlineChart/hooks/useEmaIndicator.ts +62 -0
  72. package/src/app/components/Dashboard/KlineChart/hooks/useMaIndicator.ts +62 -0
  73. package/src/app/components/Dashboard/KlineChart/hooks/useManagedIndicator.ts +140 -0
  74. package/src/app/components/Dashboard/KlineChart/hooks/usePluginIndicators.ts +212 -0
  75. package/src/app/components/Dashboard/KlineChart/hooks/useResize.ts +29 -0
  76. package/src/app/components/Dashboard/KlineChart/hooks/useSetup.ts +122 -0
  77. package/src/app/components/Dashboard/KlineChart/hooks/useSignal.ts +85 -0
  78. package/src/app/components/Dashboard/KlineChart/hooks/useSpreadIndicator.ts +243 -0
  79. package/src/app/components/Dashboard/KlineChart/hooks/useSupportResistanceLines.ts +125 -0
  80. package/src/app/components/Dashboard/KlineChart/hooks/useTrendLine.ts +139 -0
  81. package/src/app/components/Dashboard/KlineChart/hooks/useVolIndicator.ts +18 -0
  82. package/src/app/components/Dashboard/KlineChart/hooks/useWmaIndicator.ts +62 -0
  83. package/src/app/components/Dashboard/KlineChart/index.tsx +169 -0
  84. package/src/app/components/Dashboard/KlineChart/styles.ts +70 -0
  85. package/src/app/components/Dashboard/MainChart/index.tsx +35 -0
  86. package/src/app/components/Shared/AppShell.tsx +28 -0
  87. package/src/app/components/Shared/FavoriteButton/index.tsx +23 -0
  88. package/src/app/components/Shared/Filters/Backtest/index.tsx +164 -0
  89. package/src/app/components/Shared/Filters/FavoriteIndicator/index.tsx +18 -0
  90. package/src/app/components/Shared/Filters/Indicators/index.tsx +21 -0
  91. package/src/app/components/Shared/Filters/Interval/index.tsx +31 -0
  92. package/src/app/components/Shared/Filters/Interval/intervals.ts +6 -0
  93. package/src/app/components/Shared/Filters/Provider/index.tsx +32 -0
  94. package/src/app/components/Shared/Filters/Root/index.tsx +28 -0
  95. package/src/app/components/Shared/Filters/Symbol/index.tsx +49 -0
  96. package/src/app/components/Shared/Filters/context.ts +17 -0
  97. package/src/app/components/Shared/Filters/index.ts +17 -0
  98. package/src/app/components/Shared/Sidebar/index.tsx +72 -0
  99. package/src/app/components/UI/ColorMode/index.tsx +112 -0
  100. package/src/app/components/UI/EmptyState/index.tsx +28 -0
  101. package/src/app/components/UI/OverlaySpinner/index.tsx +23 -0
  102. package/src/app/components/UI/Segment/index.tsx +23 -0
  103. package/src/app/components/UI/Select/index.tsx +81 -0
  104. package/src/app/components/UI/SelectWithSearch/index.tsx +104 -0
  105. package/src/app/components/UI/Switcher/index.tsx +24 -0
  106. package/src/app/components/UI/Toaster/index.tsx +45 -0
  107. package/src/app/components/UI/index.ts +8 -0
  108. package/src/app/favicon.ico +0 -0
  109. package/src/app/globals.css +5 -0
  110. package/src/app/layout.tsx +31 -0
  111. package/src/app/page.tsx +14 -0
  112. package/src/app/provider.tsx +39 -0
  113. package/src/app/routes/backtest/[test]/page.tsx +33 -0
  114. package/src/app/routes/backtest/page.tsx +374 -0
  115. package/src/app/routes/dashboard/[provider]/[symbol]/[interval]/page.tsx +124 -0
  116. package/src/app/routes/dashboard/page.tsx +20 -0
  117. package/src/app/routes/derivatives/page.tsx +202 -0
  118. package/src/app/routes/signin/page.tsx +155 -0
  119. package/src/app/store/data.ts +144 -0
  120. package/src/app/store/filters.ts +29 -0
  121. package/src/app/store/index.ts +13 -0
  122. package/src/app/store/indicators.ts +229 -0
  123. package/src/app/store/tests.ts +464 -0
  124. package/src/app/store/tickers.ts +89 -0
  125. package/src/proxy.ts +142 -0
  126. package/tsconfig.json +40 -0
@@ -0,0 +1,125 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import _ from 'lodash';
5
+ import { Chart, registerOverlay } from 'klinecharts';
6
+ import { getSupportResistanceLevels } from '@tradejs/core/indicators';
7
+
8
+ type SupportResistanceOverlayPoint = {
9
+ timestamp: number;
10
+ value: number;
11
+ };
12
+
13
+ export const useSupportResistanceLines = (
14
+ chart: Chart | null,
15
+ enabled: boolean,
16
+ ) => {
17
+ const data = chart?.getDataList();
18
+
19
+ // регистрируем оверлеи один раз
20
+ useEffect(() => {
21
+ registerOverlay({
22
+ name: 'SupportLine',
23
+ totalStep: 2,
24
+ needDefaultPointFigure: false,
25
+ needDefaultXAxisFigure: false,
26
+ needDefaultYAxisFigure: false,
27
+ createPointFigures: ({ coordinates }) => {
28
+ if (coordinates.length < 2) return [];
29
+ return [
30
+ {
31
+ type: 'line',
32
+ attrs: {
33
+ coordinates: [coordinates[0], coordinates[1]],
34
+ },
35
+ styles: {
36
+ color: '#22c55e', // зелёный
37
+ size: 1,
38
+ style: 'dashed',
39
+ },
40
+ },
41
+ ];
42
+ },
43
+ });
44
+
45
+ registerOverlay({
46
+ name: 'ResistanceLine',
47
+ totalStep: 2,
48
+ needDefaultPointFigure: false,
49
+ needDefaultXAxisFigure: false,
50
+ needDefaultYAxisFigure: false,
51
+ createPointFigures: ({ coordinates }) => {
52
+ if (coordinates.length < 2) return [];
53
+ return [
54
+ {
55
+ type: 'line',
56
+ attrs: {
57
+ coordinates: [coordinates[0], coordinates[1]],
58
+ },
59
+ styles: {
60
+ color: '#ef4444', // красный
61
+ size: 1,
62
+ style: 'dashed',
63
+ },
64
+ },
65
+ ];
66
+ },
67
+ });
68
+ }, []);
69
+
70
+ useEffect(() => {
71
+ if (!chart || !enabled || !data || _.isEmpty(data)) return;
72
+
73
+ // находим уровни
74
+ const { supportLevels, resistanceLevels } =
75
+ getSupportResistanceLevels(data);
76
+
77
+ // конструируем точки для горизонтальных линий:
78
+ // нам нужна линия "цена = const" через всю видимую область.
79
+ // У оверлея klinecharts минимум 2 точки [ (t1,price), (t2,price) ].
80
+ // Возьмём первую и последнюю свечу, чтобы растянуть линию.
81
+ const first = data[0];
82
+ const last = data[data.length - 1];
83
+
84
+ const firstTs = first.timestamp;
85
+ const lastTs = last.timestamp;
86
+
87
+ // SUPPORT
88
+ for (const level of supportLevels) {
89
+ const points: SupportResistanceOverlayPoint[] = [
90
+ { timestamp: firstTs, value: level.price },
91
+ { timestamp: lastTs, value: level.price },
92
+ ];
93
+
94
+ chart.createOverlay({
95
+ name: 'SupportLine',
96
+ id: level.id,
97
+ points,
98
+ });
99
+ }
100
+
101
+ // RESISTANCE
102
+ for (const level of resistanceLevels) {
103
+ const points: SupportResistanceOverlayPoint[] = [
104
+ { timestamp: firstTs, value: level.price },
105
+ { timestamp: lastTs, value: level.price },
106
+ ];
107
+
108
+ chart.createOverlay({
109
+ name: 'ResistanceLine',
110
+ id: level.id,
111
+ points,
112
+ });
113
+ }
114
+
115
+ // cleanup: убрать все созданные уровни при следующем ререндере
116
+ return () => {
117
+ if (supportLevels.length) {
118
+ chart.removeOverlay({ name: 'SupportLine' });
119
+ }
120
+ if (resistanceLevels.length) {
121
+ chart.removeOverlay({ name: 'ResistanceLine' });
122
+ }
123
+ };
124
+ }, [chart, enabled, data]);
125
+ };
@@ -0,0 +1,139 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { useSearchParams } from 'next/navigation';
5
+ import _ from 'lodash';
6
+ import { Chart, registerOverlay } from 'klinecharts';
7
+ import { getSignal } from '@actions/signal';
8
+ import { toMs } from '@tradejs/core/time';
9
+ import { createTrendlineEngine } from '@tradejs/core/indicators';
10
+ import { Signal, TrendLine } from '@tradejs/types';
11
+ import { createTrendLinePointFigure } from '../figures/trendLinePointFigure';
12
+ import { createTrendLinePointsPointFigure } from '../figures/trendLinePointsPointFigure';
13
+
14
+ const fitKeepRightZoom = (chart: Chart, lastDataTsMs: number) => {
15
+ if (!Number.isFinite(lastDataTsMs)) return;
16
+
17
+ const size = chart.getSize?.();
18
+ const width = size?.width ?? 0;
19
+
20
+ if (!width) return;
21
+
22
+ const MAX_STEPS = 15;
23
+ const SCALE = 0.85;
24
+ const RIGHT_MARGIN_RATIO = 0.1;
25
+
26
+ chart.scrollToTimestamp(lastDataTsMs);
27
+
28
+ for (let i = 0; i < MAX_STEPS; i++) {
29
+ chart.zoomAtTimestamp(SCALE, lastDataTsMs);
30
+ }
31
+
32
+ const rightOffsetPx = width * RIGHT_MARGIN_RATIO;
33
+
34
+ chart.setOffsetRightDistance?.(rightOffsetPx);
35
+ };
36
+
37
+ export const useTrendLine = (chart: Chart | null, enabled: boolean) => {
38
+ const [signal, setSignal] = useState<Signal | null>(null);
39
+ const searchParams = useSearchParams();
40
+ const signalId = searchParams.get('signalId');
41
+ const autoZoom = Boolean(searchParams.get('autoZoom')) ?? false;
42
+
43
+ const data = chart?.getDataList() || [];
44
+ const symbol = chart?.getSymbol()?.ticker || '';
45
+
46
+ useEffect(() => {
47
+ if (!signalId) return;
48
+ getSignal(symbol, signalId).then(setSignal);
49
+ }, [signalId, symbol]);
50
+
51
+ useEffect(() => {
52
+ registerOverlay({
53
+ name: 'TrendLine',
54
+ totalStep: 2,
55
+ needDefaultPointFigure: false,
56
+ needDefaultXAxisFigure: false,
57
+ needDefaultYAxisFigure: false,
58
+ createPointFigures: createTrendLinePointFigure,
59
+ });
60
+
61
+ registerOverlay({
62
+ name: 'TrendLinePoints',
63
+ needDefaultPointFigure: true,
64
+ needDefaultXAxisFigure: false,
65
+ needDefaultYAxisFigure: false,
66
+ createPointFigures: createTrendLinePointsPointFigure,
67
+ });
68
+ }, []);
69
+
70
+ useEffect(() => {
71
+ if (!chart || !enabled || !data || _.isEmpty(data)) return;
72
+
73
+ const lastDataTsMs = toMs(data[data.length - 1].timestamp);
74
+
75
+ const currentSymbol = chart.getSymbol()?.ticker;
76
+
77
+ const buildLinesForMode = (mode: TrendLine['mode']) =>
78
+ createTrendlineEngine(data, { mode, minTouches: 4 }).getLines();
79
+
80
+ const trendLine = signal?.figures?.trendLine;
81
+
82
+ if (!trendLine) {
83
+ return;
84
+ }
85
+
86
+ const lowLines: TrendLine[] =
87
+ signalId && signal?.symbol === currentSymbol
88
+ ? trendLine?.mode === 'lows'
89
+ ? [trendLine]
90
+ : []
91
+ : buildLinesForMode('lows');
92
+
93
+ const highLines: TrendLine[] =
94
+ signalId && signal?.symbol === currentSymbol
95
+ ? trendLine?.mode === 'highs'
96
+ ? [trendLine]
97
+ : []
98
+ : buildLinesForMode('highs');
99
+
100
+ const lines = [...lowLines, ...highLines];
101
+
102
+ if (!lines) {
103
+ return;
104
+ }
105
+
106
+ for (const line of lines) {
107
+ const points = [...line.points, ...line.touches];
108
+
109
+ const extendData = {
110
+ mode: line.mode,
111
+ };
112
+
113
+ chart.createOverlay({
114
+ name: 'TrendLine',
115
+ id: line.id,
116
+ points: line.points,
117
+ zLevel: 10,
118
+ extendData,
119
+ });
120
+
121
+ chart.createOverlay({
122
+ name: 'TrendLinePoints',
123
+ id: `${line.id}-points`,
124
+ points: points,
125
+ zLevel: 12,
126
+ });
127
+ }
128
+
129
+ if (autoZoom && Number.isFinite(lastDataTsMs)) {
130
+ fitKeepRightZoom(chart, lastDataTsMs);
131
+ }
132
+
133
+ return () => {
134
+ chart.removeOverlay({ name: 'TrendLine' });
135
+ chart.removeOverlay({ name: 'TrendLinePoints' });
136
+ };
137
+ // eslint-disable-next-line react-hooks/exhaustive-deps
138
+ }, [chart, enabled, data.length, signal]);
139
+ };
@@ -0,0 +1,18 @@
1
+ import { useEffect } from 'react';
2
+ import { Chart } from 'klinecharts';
3
+
4
+ export const useVolIndicator = (chart: Chart | null, enabled: boolean) => {
5
+ useEffect(() => {
6
+ if (!chart || !enabled) {
7
+ return () => null;
8
+ }
9
+
10
+ setTimeout(() => {
11
+ chart.createIndicator('VOL', true, { minHeight: 80 });
12
+ }, 100);
13
+
14
+ return () => {
15
+ chart.removeIndicator({ name: 'VOL' });
16
+ };
17
+ }, [chart, enabled]);
18
+ };
@@ -0,0 +1,62 @@
1
+ import { useEffect } from 'react';
2
+ import { WMA } from 'technicalindicators';
3
+ import { registerIndicator, Chart } from 'klinecharts';
4
+
5
+ export const useWmaIndicator = (
6
+ chart: Chart | null,
7
+ enabled: boolean,
8
+ periods: number[],
9
+ ) => {
10
+ useEffect(() => {
11
+ registerIndicator({
12
+ name: 'WMA',
13
+ shortName: 'WMA',
14
+ calcParams: periods,
15
+ figures: periods.map((period) => ({
16
+ key: `WMA${period}`,
17
+ title: `WMA${period}: `,
18
+ type: 'line',
19
+ })),
20
+
21
+ // Calculation results
22
+ calc: (kLineDataList) => {
23
+ const closesPrices = kLineDataList.map((item) => item.close);
24
+
25
+ const values = periods.map((period) =>
26
+ WMA.calculate({
27
+ period,
28
+ values: closesPrices,
29
+ }),
30
+ );
31
+
32
+ return kLineDataList.reduce<Record<number, Record<string, number>>>(
33
+ (acc, { timestamp }, candleIndex) => {
34
+ const ma: Record<string, number> = {};
35
+ periods.forEach((period, j) => {
36
+ if (candleIndex >= period - 1) {
37
+ ma[`WMA${period}`] = values[j][candleIndex - (period - 1)];
38
+ }
39
+ });
40
+
41
+ acc[timestamp] = ma;
42
+
43
+ return acc;
44
+ },
45
+ {},
46
+ );
47
+ },
48
+ });
49
+ }, [periods]);
50
+
51
+ useEffect(() => {
52
+ if (!chart || !enabled) {
53
+ return () => null;
54
+ }
55
+
56
+ chart.createIndicator('WMA', true, { id: 'candle_pane' });
57
+
58
+ return () => {
59
+ chart.removeIndicator({ name: 'WMA' });
60
+ };
61
+ }, [chart, enabled]);
62
+ };
@@ -0,0 +1,169 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useRef } from 'react';
4
+ import _ from 'lodash';
5
+ import {
6
+ init,
7
+ Chart,
8
+ dispose,
9
+ DataLoaderSubscribeBarParams,
10
+ } from 'klinecharts';
11
+ import { OverlaySpinner } from '@UI';
12
+ import { Indicator, UIFilters } from '@tradejs/types';
13
+ import {
14
+ useBbIndicator,
15
+ useAtrIndicator,
16
+ useMaIndicator,
17
+ useEmaIndicator,
18
+ useWmaIndicator,
19
+ useVolIndicator,
20
+ useBtcIndicator,
21
+ useBtcCorrelation,
22
+ useSpreadIndicator,
23
+ useSignal,
24
+ useBacktest,
25
+ useSupportResistanceLines,
26
+ useResize,
27
+ useSetup,
28
+ } from './hooks';
29
+ import { usePluginIndicators } from './hooks/usePluginIndicators';
30
+ import { IndicatorRendererConfig, useData } from '@store';
31
+ import { darkTheme } from './styles';
32
+
33
+ interface KlineChartProps {
34
+ id: string;
35
+ filters: UIFilters;
36
+ indicators: Record<string, Indicator>;
37
+ indicatorRenderers: Record<string, IndicatorRendererConfig>;
38
+ }
39
+
40
+ export const KlineChart = ({
41
+ id,
42
+ filters,
43
+ indicators,
44
+ indicatorRenderers,
45
+ }: KlineChartProps) => {
46
+ const chartRef = useRef<Chart | null>(null);
47
+ const { data, key, fulfilled } = useData(filters);
48
+ const updateDataCallback = useRef<
49
+ DataLoaderSubscribeBarParams['callback'] | null
50
+ >(null);
51
+ const RIGHT_EDGE_EPSILON_BARS = 1;
52
+
53
+ useEffect(() => {
54
+ const chart = init(id) as Chart;
55
+ chartRef.current = chart;
56
+
57
+ darkTheme(chart);
58
+
59
+ return () => {
60
+ dispose(id);
61
+ chartRef.current = null;
62
+ };
63
+ }, [id]);
64
+
65
+ useEffect(() => {
66
+ if (!chartRef.current || _.isEmpty(data)) {
67
+ return;
68
+ }
69
+
70
+ const chart = chartRef.current;
71
+ const currentSymbol = chart.getSymbol()?.ticker;
72
+ const currentInterval = chart.getPeriod()?.span;
73
+ const nextInterval = parseInt(filters.interval, 10);
74
+ const symbolChanged = currentSymbol !== filters.symbol;
75
+ const intervalChanged = currentInterval !== nextInterval;
76
+
77
+ if (symbolChanged || intervalChanged) {
78
+ chartRef.current.setSymbol({ ticker: filters.symbol, pricePrecision: 9 });
79
+ chartRef.current.setPeriod({
80
+ span: nextInterval,
81
+ type: 'minute',
82
+ });
83
+
84
+ chartRef.current.setDataLoader({
85
+ getBars: ({ callback }) => {
86
+ callback(data);
87
+ },
88
+ subscribeBar: ({ callback }) => {
89
+ updateDataCallback.current = callback;
90
+ },
91
+ });
92
+
93
+ return;
94
+ }
95
+
96
+ if (!fulfilled || !updateDataCallback.current) {
97
+ return;
98
+ }
99
+
100
+ const currentData = chart.getDataList();
101
+ const visibleRangeBeforeUpdate = chart.getVisibleRange();
102
+ const maxVisibleDataIndex = currentData.length - 1;
103
+ const wasPinnedToRightEdge =
104
+ maxVisibleDataIndex <= 0 ||
105
+ maxVisibleDataIndex - visibleRangeBeforeUpdate.realTo <=
106
+ RIGHT_EDGE_EPSILON_BARS;
107
+ const dataIndexToKeepVisible = Math.max(
108
+ 0,
109
+ Math.min(
110
+ maxVisibleDataIndex,
111
+ Math.floor(visibleRangeBeforeUpdate.realTo),
112
+ ),
113
+ );
114
+ const dataByTimestamp = _.keyBy(currentData, 'timestamp');
115
+
116
+ const updatedCandles = data.filter((c) => {
117
+ const prevCandle = dataByTimestamp[c.timestamp];
118
+
119
+ if (!prevCandle) {
120
+ return true;
121
+ }
122
+
123
+ if (
124
+ prevCandle.close !== c.close ||
125
+ prevCandle.open !== c.open ||
126
+ prevCandle.high !== c.high ||
127
+ prevCandle.low !== c.low ||
128
+ prevCandle.volume !== c.volume
129
+ ) {
130
+ return true;
131
+ }
132
+
133
+ return false;
134
+ });
135
+
136
+ updatedCandles.forEach((candle) => {
137
+ updateDataCallback.current?.(candle);
138
+ });
139
+
140
+ if (!wasPinnedToRightEdge) {
141
+ chart.scrollToDataIndex(dataIndexToKeepVisible);
142
+ }
143
+ }, [data, filters.interval, filters.symbol, fulfilled, key]);
144
+
145
+ const chart = chartRef.current;
146
+
147
+ useResize(chart, id);
148
+ useAtrIndicator(chart, indicators.atr.enabled, indicators.atr.periods || []);
149
+ useBbIndicator(chart, indicators.bb.enabled, indicators.bb.periods || []);
150
+ useMaIndicator(chart, indicators.ma.enabled, indicators.ma.periods || []);
151
+ useEmaIndicator(chart, indicators.ema.enabled, indicators.ema.periods || []);
152
+ useWmaIndicator(chart, indicators.wma.enabled, indicators.wma.periods || []);
153
+ useVolIndicator(chart, indicators.vol.enabled);
154
+ useBtcIndicator(chart, indicators.btc.enabled, filters);
155
+ useBtcCorrelation(chart, indicators.btcCorrelation?.enabled, filters);
156
+ useSpreadIndicator(chart, indicators.spread?.enabled, filters);
157
+ useBacktest(chart, filters.backtestId || undefined);
158
+ useSupportResistanceLines(chart, indicators.resistant?.enabled);
159
+ useSignal(chart, true);
160
+ useSetup(chart, true);
161
+ usePluginIndicators(chart, indicators, indicatorRenderers, data);
162
+
163
+ return (
164
+ <>
165
+ <div id={id} />
166
+ {!fulfilled && <OverlaySpinner />}
167
+ </>
168
+ );
169
+ };
@@ -0,0 +1,70 @@
1
+ import { Chart } from 'klinecharts';
2
+
3
+ export const darkTheme = (chart: Chart) => {
4
+ chart.setStyles({
5
+ grid: {
6
+ horizontal: {
7
+ color: '#374151', // цвет горизонтальных линий сетки
8
+ },
9
+ vertical: {
10
+ color: '#374151', // цвет вертикальных линий сетки
11
+ },
12
+ },
13
+ candle: {
14
+ // type: 'candle_solid',
15
+ bar: {
16
+ upColor: '#26a69a', // цвет свечи вверх
17
+ downColor: '#ef5350', // цвет свечи вниз
18
+ noChangeColor: '#888888', // цвет свечи без изменения
19
+ upBorderColor: '#26a69a',
20
+ downBorderColor: '#ef5350',
21
+ noChangeBorderColor: '#888888',
22
+ upWickColor: '#26a69a',
23
+ downWickColor: '#ef5350',
24
+ noChangeWickColor: '#888888',
25
+ },
26
+ },
27
+ xAxis: {
28
+ axisLine: {
29
+ color: '#4b5563', // цвет оси X
30
+ },
31
+ tickLine: {
32
+ color: '#4b5563',
33
+ },
34
+ tickText: {
35
+ color: '#9ca3af', // цвет текста подписей по оси X
36
+ },
37
+ },
38
+ yAxis: {
39
+ axisLine: {
40
+ color: '#4b5563', // цвет оси Y
41
+ },
42
+ tickLine: {
43
+ color: '#4b5563',
44
+ },
45
+ tickText: {
46
+ color: '#9ca3af', // цвет текста подписей по оси Y
47
+ },
48
+ },
49
+ crosshair: {
50
+ horizontal: {
51
+ line: {
52
+ color: '#6b7280', // цвет горизонтальной линии перекрестия
53
+ },
54
+ text: {
55
+ color: '#f3f4f6', // цвет текста горизонтальной линии перекрестия
56
+ backgroundColor: '#1f2937', // фон текста горизонтальной линии перекрестия
57
+ },
58
+ },
59
+ vertical: {
60
+ line: {
61
+ color: '#6b7280', // цвет вертикальной линии перекрестия
62
+ },
63
+ text: {
64
+ color: '#f3f4f6', // цвет текста вертикальной линии перекрестия
65
+ backgroundColor: '#1f2937', // фон текста вертикальной линии перекрестия
66
+ },
67
+ },
68
+ },
69
+ });
70
+ };
@@ -0,0 +1,35 @@
1
+ 'use client';
2
+
3
+ import _ from 'lodash';
4
+ import { useEffect } from 'react';
5
+ import { useFilters, useIndicators } from '@store';
6
+ import { getTimestamp } from '@tradejs/core/time';
7
+ import { KlineChart } from '../KlineChart';
8
+
9
+ const DASHBOARD_REFRESH_DELAY = 10_000;
10
+
11
+ export const MainChart = () => {
12
+ const { filters, setFilters } = useFilters();
13
+ const { indicatorsByKey, indicatorRenderers } = useIndicators();
14
+
15
+ useEffect(() => {
16
+ const intervalId = setInterval(() => {
17
+ setFilters({
18
+ end: getTimestamp(),
19
+ });
20
+ }, DASHBOARD_REFRESH_DELAY);
21
+
22
+ return () => {
23
+ clearInterval(intervalId);
24
+ };
25
+ }, [setFilters]);
26
+
27
+ return (
28
+ <KlineChart
29
+ id="main-chart"
30
+ filters={filters}
31
+ indicators={indicatorsByKey}
32
+ indicatorRenderers={indicatorRenderers}
33
+ />
34
+ );
35
+ };
@@ -0,0 +1,28 @@
1
+ 'use client';
2
+
3
+ import { Box } from '@chakra-ui/react';
4
+ import { usePathname } from 'next/navigation';
5
+ import { Sidebar } from '@shared/Sidebar';
6
+
7
+ const AUTH_ROUTES = ['/routes/signin'];
8
+
9
+ const isAuthRoute = (pathname: string) =>
10
+ AUTH_ROUTES.some((route) => pathname.startsWith(route));
11
+
12
+ export const AppShell = ({ children }: { children: React.ReactNode }) => {
13
+ const pathname = usePathname();
14
+ const hideSidebar = isAuthRoute(pathname);
15
+
16
+ if (hideSidebar) {
17
+ return <Box minH="100vh">{children}</Box>;
18
+ }
19
+
20
+ return (
21
+ <>
22
+ <Sidebar />
23
+ <Box ml="60px" minH="100vh">
24
+ {children}
25
+ </Box>
26
+ </>
27
+ );
28
+ };
@@ -0,0 +1,23 @@
1
+ import { IconButton } from '@chakra-ui/react';
2
+ import { FaStar, FaRegStar } from 'react-icons/fa';
3
+
4
+ interface FavoriteButtonProps {
5
+ isFavorite: boolean;
6
+ onChangeFavorite: () => void;
7
+ }
8
+
9
+ export const FavoriteButton = ({
10
+ isFavorite,
11
+ onChangeFavorite,
12
+ }: FavoriteButtonProps) => {
13
+ return (
14
+ <IconButton
15
+ size="xs"
16
+ colorPalette="teal"
17
+ variant={isFavorite ? 'solid' : 'outline'}
18
+ onClick={() => onChangeFavorite()}
19
+ >
20
+ {isFavorite ? <FaStar /> : <FaRegStar />}
21
+ </IconButton>
22
+ );
23
+ };