@tradejs/app 2.0.2 → 2.0.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tradejs/app",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
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.1.42",
53
53
  "@langchain/openai": "^1.4.5",
54
- "@tradejs/connectors": "^2.0.2",
55
- "@tradejs/core": "^2.0.2",
56
- "@tradejs/indicators": "^2.0.2",
57
- "@tradejs/infra": "^2.0.2",
58
- "@tradejs/node": "^2.0.2",
59
- "@tradejs/types": "^2.0.2",
54
+ "@tradejs/connectors": "^2.0.4",
55
+ "@tradejs/core": "^2.0.4",
56
+ "@tradejs/indicators": "^2.0.4",
57
+ "@tradejs/infra": "^2.0.4",
58
+ "@tradejs/node": "^2.0.4",
59
+ "@tradejs/types": "^2.0.4",
60
60
  "@types/bcryptjs": "2.4.6",
61
61
  "@types/lodash": "4.14.202",
62
62
  "@types/node": "24.13.3",
@@ -69,7 +69,7 @@
69
69
  "idb-keyval": "^6.2.2",
70
70
  "klinecharts": "10.0.0-alpha9",
71
71
  "lodash": "^4.18.1",
72
- "next": "16.2.4",
72
+ "next": "16.2.11",
73
73
  "next-auth": "5.0.0-beta.30",
74
74
  "next-themes": "^0.4.6",
75
75
  "react": "19.2.3",
@@ -7,8 +7,9 @@ export { useVolIndicator } from './useVolIndicator';
7
7
  export { useBtcIndicator } from './useBtcIndicator';
8
8
  export { useBtcCorrelation } from './useBtcCorrelation';
9
9
  export { useSpreadIndicator } from './useSpreadIndicator';
10
- export { useSignal } from './useSignal';
10
+ export { useDashboardSignal } from './useDashboardSignal';
11
+ export { useSignalFigures } from './useSignal';
11
12
  export { useBacktest } from './useBacktest';
12
13
  export { useSupportResistanceLines } from './useSupportResistanceLines';
13
14
  export { useResize } from './useResize';
14
- export { useSetup } from './useSetup';
15
+ export { useTradeSetup } from './useSetup';
@@ -0,0 +1,83 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useMemo, useState } from 'react';
4
+ import { getSignal } from '#actions/signal';
5
+ import type { Signal } from '@tradejs/types';
6
+
7
+ export type DashboardSignalStatus =
8
+ | 'idle'
9
+ | 'loading'
10
+ | 'loaded'
11
+ | 'missing'
12
+ | 'error';
13
+
14
+ type DashboardSignalState = {
15
+ queryKey: string | null;
16
+ signal: Signal | null;
17
+ status: DashboardSignalStatus;
18
+ };
19
+
20
+ const INITIAL_STATE: DashboardSignalState = {
21
+ queryKey: null,
22
+ signal: null,
23
+ status: 'idle',
24
+ };
25
+
26
+ export const useDashboardSignal = ({
27
+ symbol,
28
+ signalId,
29
+ }: {
30
+ symbol: string;
31
+ signalId: string | null;
32
+ }) => {
33
+ const queryKey = useMemo(
34
+ () => (symbol && signalId ? `${symbol}:${signalId}` : null),
35
+ [signalId, symbol],
36
+ );
37
+ const [state, setState] = useState<DashboardSignalState>(INITIAL_STATE);
38
+
39
+ useEffect(() => {
40
+ let active = true;
41
+
42
+ if (!queryKey || !signalId) {
43
+ setState(INITIAL_STATE);
44
+ return () => {
45
+ active = false;
46
+ };
47
+ }
48
+
49
+ setState({ queryKey, signal: null, status: 'loading' });
50
+
51
+ void getSignal(symbol, signalId)
52
+ .then((signal) => {
53
+ if (!active) return;
54
+ setState({
55
+ queryKey,
56
+ signal,
57
+ status: signal ? 'loaded' : 'missing',
58
+ });
59
+ })
60
+ .catch(() => {
61
+ if (!active) return;
62
+ setState({ queryKey, signal: null, status: 'error' });
63
+ });
64
+
65
+ return () => {
66
+ active = false;
67
+ };
68
+ }, [queryKey, signalId, symbol]);
69
+
70
+ if (!queryKey) {
71
+ return INITIAL_STATE;
72
+ }
73
+
74
+ if (state.queryKey !== queryKey) {
75
+ return {
76
+ queryKey,
77
+ signal: null,
78
+ status: 'loading' as const,
79
+ };
80
+ }
81
+
82
+ return state;
83
+ };
@@ -1,11 +1,7 @@
1
- 'use client';
2
-
3
1
  import { useEffect, useMemo, useState } from 'react';
4
- import { useSearchParams } from 'next/navigation';
5
2
  import { Chart, registerOverlay } from 'klinecharts';
6
- import { getSignal } from '#actions/signal';
7
3
  import { toMs } from '@tradejs/core/time';
8
- import { Signal } from '@tradejs/types';
4
+ import type { Signal } from '@tradejs/types';
9
5
  import { createTradeZonePointFigure } from '../figures/tradeZonePointFigure';
10
6
 
11
7
  const SETUP = 'Setup';
@@ -15,20 +11,21 @@ const FALLBACK_WIDTH_MS = 24 * 60 * 60_000;
15
11
 
16
12
  type Point = { timestamp: number; value: number };
17
13
 
18
- export const useSetup = (chart: Chart | null, enabled: boolean) => {
19
- const [signal, setSignal] = useState<Signal | null>(null);
20
- const searchParams = useSearchParams();
21
- const signalId = searchParams.get('signalId');
22
-
23
- const symbol = chart?.getSymbol()?.ticker || '';
14
+ type RenderedTradeSetup = {
15
+ chart: Chart;
16
+ signalId: string;
17
+ };
24
18
 
25
- useEffect(() => {
26
- if (!signalId || !symbol) {
27
- setSignal(null);
28
- return;
29
- }
30
- getSignal(symbol, signalId).then(setSignal);
31
- }, [symbol, signalId]);
19
+ export const useTradeSetup = ({
20
+ chart,
21
+ enabled,
22
+ signal,
23
+ }: {
24
+ chart: Chart | null;
25
+ enabled: boolean;
26
+ signal: Signal | null;
27
+ }) => {
28
+ const [rendered, setRendered] = useState<RenderedTradeSetup | null>(null);
32
29
 
33
30
  useEffect(() => {
34
31
  registerOverlay({
@@ -80,10 +77,16 @@ export const useSetup = (chart: Chart | null, enabled: boolean) => {
80
77
  }, [signal]);
81
78
 
82
79
  useEffect(() => {
83
- if (!chart || !enabled || !signalId || !signal || !setupPoints) return;
80
+ if (!chart || !enabled || !signal || !setupPoints) {
81
+ setRendered(null);
82
+ return;
83
+ }
84
84
 
85
85
  const currentSymbol = chart.getSymbol()?.ticker;
86
- if (signal.symbol !== currentSymbol) return;
86
+ if (signal.symbol !== currentSymbol) {
87
+ setRendered(null);
88
+ return;
89
+ }
87
90
 
88
91
  const tpId = `${signal.signalId}-tp`;
89
92
  const slId = `${signal.signalId}-sl`;
@@ -111,12 +114,24 @@ export const useSetup = (chart: Chart | null, enabled: boolean) => {
111
114
  points: [setupPoints.start],
112
115
  });
113
116
 
117
+ setRendered((current) =>
118
+ current?.chart === chart && current.signalId === signal.signalId
119
+ ? current
120
+ : { chart, signalId: signal.signalId },
121
+ );
122
+
114
123
  return () => {
115
124
  chart.removeOverlay({ id: tpId, name: SETUP });
116
125
  chart.removeOverlay({ id: slId, name: SETUP });
117
126
  chart.removeOverlay({ name: SETUP_START });
118
127
  };
119
- }, [chart, enabled, signalId, signal, setupPoints]);
120
-
121
- return { signal };
128
+ }, [chart, enabled, signal, setupPoints]);
129
+
130
+ return Boolean(
131
+ enabled &&
132
+ chart &&
133
+ signal &&
134
+ rendered?.chart === chart &&
135
+ rendered.signalId === signal.signalId,
136
+ );
122
137
  };
@@ -1,11 +1,6 @@
1
- 'use client';
2
-
3
1
  import { useEffect, useState } from 'react';
4
- import { useSearchParams } from 'next/navigation';
5
- import _ from 'lodash';
6
2
  import { Chart } from 'klinecharts';
7
- import { getSignal } from '#actions/signal';
8
- import { Signal } from '@tradejs/types';
3
+ import type { Signal } from '@tradejs/types';
9
4
  import {
10
5
  drawSignalFigures,
11
6
  ensureBaseFigureOverlaysRegistered,
@@ -37,49 +32,74 @@ const fitKeepRightZoom = (chart: Chart, lastDataTsMs: number) => {
37
32
  chart.setOffsetRightDistance?.(rightOffsetPx);
38
33
  };
39
34
 
40
- export const useSignal = (chart: Chart | null, enabled: boolean) => {
41
- const [signal, setSignal] = useState<Signal | null>(null);
42
- const searchParams = useSearchParams();
43
- const signalId = searchParams.get('signalId');
44
- const autoZoom = Boolean(searchParams.get('autoZoom')) ?? false;
35
+ type RenderedSignalFigures = {
36
+ chart: Chart;
37
+ signalId: string;
38
+ };
45
39
 
46
- const data = chart?.getDataList();
47
- const symbol = chart?.getSymbol()?.ticker || '';
40
+ export const useSignalFigures = ({
41
+ chart,
42
+ lastDataTimestamp,
43
+ enabled,
44
+ signal,
45
+ autoZoom,
46
+ }: {
47
+ chart: Chart | null;
48
+ lastDataTimestamp: number | null;
49
+ enabled: boolean;
50
+ signal: Signal | null;
51
+ autoZoom: boolean;
52
+ }) => {
53
+ const [rendered, setRendered] = useState<RenderedSignalFigures | null>(null);
48
54
 
49
55
  useEffect(() => {
50
- if (!signalId || !symbol) {
51
- setSignal(null);
56
+ if (!chart || !enabled || lastDataTimestamp == null || !signal) {
57
+ setRendered(null);
52
58
  return;
53
59
  }
54
- getSignal(symbol, signalId).then(setSignal);
55
- }, [signalId, symbol]);
56
-
57
- useEffect(() => {
58
- if (!chart || !enabled || !data || _.isEmpty(data) || !signal) return;
59
60
 
60
61
  const currentSymbol = chart.getSymbol()?.ticker;
61
- if (signal.symbol !== currentSymbol) return;
62
+ if (signal.symbol !== currentSymbol) {
63
+ setRendered(null);
64
+ return;
65
+ }
62
66
 
63
67
  const normalized = normalizeSignalFigures(signal);
64
- if (!normalized) return;
65
-
66
- ensureBaseFigureOverlaysRegistered();
67
-
68
- const overlays = drawSignalFigures({
69
- chart,
70
- idPrefix: `signal-${signal.signalId}`,
71
- figures: normalized,
72
- });
68
+ const overlays = normalized
69
+ ? (() => {
70
+ ensureBaseFigureOverlaysRegistered();
71
+
72
+ return drawSignalFigures({
73
+ chart,
74
+ idPrefix: `signal-${signal.signalId}`,
75
+ figures: normalized,
76
+ });
77
+ })()
78
+ : [];
73
79
 
74
80
  if (autoZoom) {
75
- const lastDataTsMs = toMs(data[data.length - 1].timestamp);
81
+ const lastDataTsMs = toMs(lastDataTimestamp);
76
82
  if (Number.isFinite(lastDataTsMs)) {
77
83
  fitKeepRightZoom(chart, lastDataTsMs);
78
84
  }
79
85
  }
80
86
 
87
+ setRendered((current) =>
88
+ current?.chart === chart && current.signalId === signal.signalId
89
+ ? current
90
+ : { chart, signalId: signal.signalId },
91
+ );
92
+
81
93
  return () => {
82
94
  removeSignalFigures(chart, overlays);
83
95
  };
84
- }, [autoZoom, chart, data, enabled, signal]);
96
+ }, [autoZoom, chart, enabled, lastDataTimestamp, signal]);
97
+
98
+ return Boolean(
99
+ enabled &&
100
+ chart &&
101
+ signal &&
102
+ rendered?.chart === chart &&
103
+ rendered.signalId === signal.signalId,
104
+ );
85
105
  };
@@ -1,6 +1,7 @@
1
1
  'use client';
2
2
 
3
- import React, { useEffect, useRef } from 'react';
3
+ import React, { useEffect, useRef, useState } from 'react';
4
+ import { useSearchParams } from 'next/navigation';
4
5
  import _ from 'lodash';
5
6
  import {
6
7
  init,
@@ -20,11 +21,12 @@ import {
20
21
  useBtcIndicator,
21
22
  useBtcCorrelation,
22
23
  useSpreadIndicator,
23
- useSignal,
24
+ useDashboardSignal,
25
+ useSignalFigures,
24
26
  useBacktest,
25
27
  useSupportResistanceLines,
26
28
  useResize,
27
- useSetup,
29
+ useTradeSetup,
28
30
  } from './hooks';
29
31
  import { usePluginIndicators } from './hooks/usePluginIndicators';
30
32
  import { IndicatorRendererConfig, useData } from '#store';
@@ -45,31 +47,49 @@ export const KlineChart = ({
45
47
  indicatorRenderers,
46
48
  live = true,
47
49
  }: KlineChartProps) => {
48
- const chartRef = useRef<Chart | null>(null);
50
+ const [chart, setChart] = useState<Chart | null>(null);
51
+ const [initializedHistory, setInitializedHistory] = useState<{
52
+ chart: Chart;
53
+ key: string;
54
+ } | null>(null);
49
55
  const { data, key, fulfilled } = useData(filters, live);
56
+ const searchParams = useSearchParams();
57
+ const signalId = searchParams.get('signalId');
58
+ const autoZoom = searchParams.get('autoZoom') === 'true';
59
+ const dashboardSignal = useDashboardSignal({
60
+ symbol: filters.symbol,
61
+ signalId,
62
+ });
50
63
  const updateDataCallback = useRef<
51
64
  DataLoaderSubscribeBarParams['callback'] | null
52
65
  >(null);
53
66
  const RIGHT_EDGE_EPSILON_BARS = 1;
67
+ const historyKey = `${id}:${key}:${filters.symbol}:${filters.interval}`;
68
+ const historyReady = Boolean(
69
+ chart &&
70
+ fulfilled &&
71
+ !_.isEmpty(data) &&
72
+ initializedHistory?.chart === chart &&
73
+ initializedHistory.key === historyKey,
74
+ );
54
75
 
55
76
  useEffect(() => {
56
- const chart = init(id) as Chart;
57
- chartRef.current = chart;
77
+ const nextChart = init(id) as Chart;
78
+ setChart(nextChart);
58
79
 
59
- darkTheme(chart);
80
+ darkTheme(nextChart);
60
81
 
61
82
  return () => {
62
83
  dispose(id);
63
- chartRef.current = null;
84
+ setChart((current) => (current === nextChart ? null : current));
64
85
  };
65
86
  }, [id]);
66
87
 
67
88
  useEffect(() => {
68
- if (!chartRef.current || !fulfilled || _.isEmpty(data)) {
89
+ if (!chart || !fulfilled || _.isEmpty(data)) {
69
90
  return;
70
91
  }
71
92
 
72
- const chart = chartRef.current;
73
93
  const currentSymbol = chart.getSymbol()?.ticker;
74
94
  const currentInterval = chart.getPeriod()?.span;
75
95
  const nextInterval = parseInt(filters.interval, 10);
@@ -77,15 +97,20 @@ export const KlineChart = ({
77
97
  const intervalChanged = currentInterval !== nextInterval;
78
98
 
79
99
  if (symbolChanged || intervalChanged) {
80
- chartRef.current.setSymbol({ ticker: filters.symbol, pricePrecision: 9 });
81
- chartRef.current.setPeriod({
100
+ chart.setSymbol({ ticker: filters.symbol, pricePrecision: 9 });
101
+ chart.setPeriod({
82
102
  span: nextInterval,
83
103
  type: 'minute',
84
104
  });
85
105
 
86
- chartRef.current.setDataLoader({
106
+ chart.setDataLoader({
87
107
  getBars: ({ callback }) => {
88
108
  callback(data);
109
+ setInitializedHistory((current) =>
110
+ current?.chart === chart && current.key === historyKey
111
+ ? current
112
+ : { chart, key: historyKey },
113
+ );
89
114
  },
90
115
  subscribeBar: ({ callback }) => {
91
116
  updateDataCallback.current = callback;
@@ -142,9 +167,12 @@ export const KlineChart = ({
142
167
  if (!wasPinnedToRightEdge) {
143
168
  chart.scrollToDataIndex(dataIndexToKeepVisible);
144
169
  }
145
- }, [data, filters.interval, filters.symbol, fulfilled, key]);
146
-
147
- const chart = chartRef.current;
170
+ setInitializedHistory((current) =>
171
+ current?.chart === chart && current.key === historyKey
172
+ ? current
173
+ : { chart, key: historyKey },
174
+ );
175
+ }, [chart, data, filters.interval, filters.symbol, fulfilled, historyKey]);
148
176
 
149
177
  useResize(chart, id);
150
178
  useAtrIndicator(chart, indicators.atr.enabled, indicators.atr.periods || []);
@@ -166,16 +194,34 @@ export const KlineChart = ({
166
194
  );
167
195
  useBacktest(chart, filters.backtestId || undefined);
168
196
  useSupportResistanceLines(chart, indicators.resistant?.enabled);
169
- useSignal(chart, true);
170
- useSetup(chart, true);
197
+ const signalReady =
198
+ dashboardSignal.status === 'loaded' && dashboardSignal.signal != null;
199
+ const signalFiguresReady = useSignalFigures({
200
+ chart,
201
+ lastDataTimestamp: data.at(-1)?.timestamp ?? null,
202
+ enabled: historyReady && signalReady,
203
+ signal: dashboardSignal.signal,
204
+ autoZoom,
205
+ });
206
+ const tradeSetupReady = useTradeSetup({
207
+ chart,
208
+ enabled: historyReady && signalReady,
209
+ signal: dashboardSignal.signal,
210
+ });
171
211
  usePluginIndicators(chart, indicators, indicatorRenderers, data);
172
212
 
213
+ const screenshotReady = signalId
214
+ ? historyReady && signalReady && signalFiguresReady && tradeSetupReady
215
+ : historyReady;
216
+
173
217
  return (
174
218
  <>
175
219
  <div
176
220
  id={id}
177
221
  data-testid="market-chart"
178
- data-chart-ready={fulfilled && !_.isEmpty(data) ? 'true' : 'false'}
222
+ data-chart-ready={historyReady ? 'true' : 'false'}
223
+ data-signal-status={dashboardSignal.status}
224
+ data-screenshot-ready={screenshotReady ? 'true' : 'false'}
179
225
  />
180
226
  {!fulfilled && <OverlaySpinner />}
181
227
  </>