@tradejs/app 1.0.3 → 1.0.5
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/README.md +1 -1
- package/next.config.mjs +1 -0
- package/package.json +16 -7
- package/src/app/api/derivatives/[symbol]/[interval]/route.ts +2 -4
- package/src/app/api/files/screenshot/[name]/route.ts +53 -17
- package/src/app/api/spread/[symbol]/[interval]/route.ts +2 -4
- package/src/app/components/Dashboard/MainChart/index.tsx +10 -2
- package/src/app/routes/dashboard/[provider]/[symbol]/[interval]/page.tsx +22 -19
package/README.md
CHANGED
package/next.config.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tradejs/app",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
|
+
"description": "Installable Next.js UI for the TradeJS open-source framework: dashboards, backtests, charts, and runtime data.",
|
|
4
5
|
"keywords": [
|
|
5
6
|
"tradejs",
|
|
6
7
|
"trading",
|
|
@@ -27,18 +28,26 @@
|
|
|
27
28
|
"publishConfig": {
|
|
28
29
|
"access": "public"
|
|
29
30
|
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/tradejs-dev/tradejs.git",
|
|
34
|
+
"directory": "apps/app"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/tradejs-dev/tradejs/issues"
|
|
38
|
+
},
|
|
30
39
|
"dependencies": {
|
|
31
40
|
"@chakra-ui/charts": "^3.24.0",
|
|
32
41
|
"@chakra-ui/react": "^3.24.2",
|
|
33
42
|
"@emotion/react": "^11.14.0",
|
|
34
43
|
"@langchain/core": "^0.3.68",
|
|
35
44
|
"@langchain/openai": "^0.6.11",
|
|
36
|
-
"@tradejs/connectors": "^1.0.
|
|
37
|
-
"@tradejs/core": "^1.0.
|
|
38
|
-
"@tradejs/indicators": "^1.0.
|
|
39
|
-
"@tradejs/infra": "^1.0.
|
|
40
|
-
"@tradejs/node": "^1.0.
|
|
41
|
-
"@tradejs/types": "^1.0.
|
|
45
|
+
"@tradejs/connectors": "^1.0.5",
|
|
46
|
+
"@tradejs/core": "^1.0.5",
|
|
47
|
+
"@tradejs/indicators": "^1.0.5",
|
|
48
|
+
"@tradejs/infra": "^1.0.5",
|
|
49
|
+
"@tradejs/node": "^1.0.5",
|
|
50
|
+
"@tradejs/types": "^1.0.5",
|
|
42
51
|
"bcryptjs": "^2.4.3",
|
|
43
52
|
"date-fns": "^3.3.1",
|
|
44
53
|
"idb-keyval": "^6.2.2",
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import {
|
|
3
|
-
DerivativesInterval,
|
|
4
|
-
getDerivativesRangeForSymbols,
|
|
5
|
-
} from '@tradejs/infra/timescale';
|
|
2
|
+
import { getDerivativesRangeForSymbols } from '@tradejs/infra/timescale';
|
|
6
3
|
import { logger } from '@tradejs/infra/logger';
|
|
4
|
+
import type { DerivativesInterval } from '@tradejs/types';
|
|
7
5
|
|
|
8
6
|
export const dynamic = 'force-dynamic';
|
|
9
7
|
|
|
@@ -3,12 +3,36 @@ import fs from 'fs/promises';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
|
|
5
5
|
export const runtime = 'nodejs';
|
|
6
|
+
export const dynamic = 'force-dynamic';
|
|
6
7
|
|
|
7
8
|
const getProjectRoot = (): string => {
|
|
8
9
|
const fromEnv = String(process.env.PROJECT_CWD || '').trim();
|
|
9
10
|
return fromEnv ? path.resolve(fromEnv) : process.cwd();
|
|
10
11
|
};
|
|
11
12
|
|
|
13
|
+
const stripPngExtension = (value: string) => value.replace(/\.png$/i, '');
|
|
14
|
+
|
|
15
|
+
const isSafeScreenshotName = (value: string) =>
|
|
16
|
+
/^[A-Za-z0-9_.-]+$/.test(value) && path.basename(value) === value;
|
|
17
|
+
|
|
18
|
+
const getScreenshotCandidates = (name: string) => {
|
|
19
|
+
const projectRoot = getProjectRoot();
|
|
20
|
+
const normalizedName = stripPngExtension(name);
|
|
21
|
+
|
|
22
|
+
return [
|
|
23
|
+
path.join(projectRoot, 'data', 'screenshots', `${normalizedName}.png`),
|
|
24
|
+
path.join(projectRoot, 'public', 'screenshots', `${normalizedName}.png`),
|
|
25
|
+
path.join(
|
|
26
|
+
projectRoot,
|
|
27
|
+
'apps',
|
|
28
|
+
'app',
|
|
29
|
+
'public',
|
|
30
|
+
'screenshots',
|
|
31
|
+
`${normalizedName}.png`,
|
|
32
|
+
),
|
|
33
|
+
];
|
|
34
|
+
};
|
|
35
|
+
|
|
12
36
|
interface Params {
|
|
13
37
|
name: string;
|
|
14
38
|
}
|
|
@@ -17,26 +41,38 @@ export async function GET(
|
|
|
17
41
|
_req: Request,
|
|
18
42
|
{ params }: { params: Promise<Params> },
|
|
19
43
|
) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'
|
|
25
|
-
|
|
26
|
-
`${name}.png`,
|
|
44
|
+
const { name } = await params;
|
|
45
|
+
|
|
46
|
+
if (!isSafeScreenshotName(name)) {
|
|
47
|
+
return NextResponse.json(
|
|
48
|
+
{ error: 'Invalid screenshot name' },
|
|
49
|
+
{ status: 400 },
|
|
27
50
|
);
|
|
28
|
-
|
|
51
|
+
}
|
|
29
52
|
|
|
30
|
-
|
|
53
|
+
try {
|
|
54
|
+
for (const filePath of getScreenshotCandidates(name)) {
|
|
55
|
+
try {
|
|
56
|
+
const file = await fs.readFile(filePath);
|
|
57
|
+
const body = new Uint8Array(file);
|
|
58
|
+
|
|
59
|
+
return new NextResponse(body, {
|
|
60
|
+
headers: {
|
|
61
|
+
'Content-Type': 'image/png',
|
|
62
|
+
'Content-Length': String(file.byteLength),
|
|
63
|
+
'Cache-Control': 'public, max-age=60, immutable',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
} catch {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
31
70
|
|
|
32
|
-
return new NextResponse(body, {
|
|
33
|
-
headers: {
|
|
34
|
-
'Content-Type': 'image/png',
|
|
35
|
-
'Content-Length': String(file.byteLength),
|
|
36
|
-
'Cache-Control': 'public, max-age=60, immutable',
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
} catch {
|
|
40
71
|
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
|
72
|
+
} catch {
|
|
73
|
+
return NextResponse.json(
|
|
74
|
+
{ error: 'Failed to load screenshot' },
|
|
75
|
+
{ status: 500 },
|
|
76
|
+
);
|
|
41
77
|
}
|
|
42
78
|
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import {
|
|
3
|
-
DerivativesInterval,
|
|
4
|
-
getSpreadRangeForSymbols,
|
|
5
|
-
} from '@tradejs/infra/timescale';
|
|
2
|
+
import { getSpreadRangeForSymbols } from '@tradejs/infra/timescale';
|
|
6
3
|
import { logger } from '@tradejs/infra/logger';
|
|
4
|
+
import type { DerivativesInterval } from '@tradejs/types';
|
|
7
5
|
|
|
8
6
|
export const dynamic = 'force-dynamic';
|
|
9
7
|
|
|
@@ -8,11 +8,19 @@ import { KlineChart } from '../KlineChart';
|
|
|
8
8
|
|
|
9
9
|
const DASHBOARD_REFRESH_DELAY = 10_000;
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
interface MainChartProps {
|
|
12
|
+
screenshotMode?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const MainChart = ({ screenshotMode = false }: MainChartProps) => {
|
|
12
16
|
const { filters, setFilters } = useFilters();
|
|
13
17
|
const { indicatorsByKey, indicatorRenderers } = useIndicators();
|
|
14
18
|
|
|
15
19
|
useEffect(() => {
|
|
20
|
+
if (screenshotMode) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
const intervalId = setInterval(() => {
|
|
17
25
|
setFilters({
|
|
18
26
|
end: getTimestamp(),
|
|
@@ -22,7 +30,7 @@ export const MainChart = () => {
|
|
|
22
30
|
return () => {
|
|
23
31
|
clearInterval(intervalId);
|
|
24
32
|
};
|
|
25
|
-
}, [setFilters]);
|
|
33
|
+
}, [screenshotMode, setFilters]);
|
|
26
34
|
|
|
27
35
|
return (
|
|
28
36
|
<KlineChart
|
|
@@ -17,6 +17,7 @@ const Dashboard = () => {
|
|
|
17
17
|
const hasBacktestStrategy = searchParams.has('backtestStrategy');
|
|
18
18
|
const backtestId = searchParams.get('backtestId');
|
|
19
19
|
const backtestStrategy = searchParams.get('backtestStrategy');
|
|
20
|
+
const isScreenshotMode = searchParams.get('screenshot') === '1';
|
|
20
21
|
|
|
21
22
|
const parseDashboardPath = useCallback(() => {
|
|
22
23
|
const parts = window.location.pathname.split('/').filter(Boolean);
|
|
@@ -89,32 +90,34 @@ const Dashboard = () => {
|
|
|
89
90
|
<Box
|
|
90
91
|
as="main"
|
|
91
92
|
minH="100vh"
|
|
92
|
-
p={4}
|
|
93
|
+
p={isScreenshotMode ? 0 : 4}
|
|
93
94
|
bg="gray.900"
|
|
94
95
|
display="flex"
|
|
95
96
|
flexDirection="column"
|
|
96
97
|
justifyContent="space-between"
|
|
97
98
|
alignItems="flex-start"
|
|
98
99
|
>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
100
|
+
{!isScreenshotMode && (
|
|
101
|
+
<Filters.Root
|
|
102
|
+
filters={filters}
|
|
103
|
+
tickers={tickers}
|
|
104
|
+
backtestFiles={tests}
|
|
105
|
+
onChangeFilters={onChangeFilters}
|
|
106
|
+
>
|
|
107
|
+
<Flex mb={2} gap={4} alignItems="center" flexDirection="row">
|
|
108
|
+
<Filters.SelectProvider />
|
|
109
|
+
<Filters.SelectSymbol />
|
|
110
|
+
<Filters.FavoriteIndicator />
|
|
111
|
+
<Filters.SelectInterval />
|
|
112
|
+
<Filters.SelectIndicator />
|
|
113
|
+
</Flex>
|
|
114
|
+
<Flex mb={4} gap={4} flexDirection="row">
|
|
115
|
+
<Filters.SelectBacktest />
|
|
116
|
+
</Flex>
|
|
117
|
+
</Filters.Root>
|
|
118
|
+
)}
|
|
116
119
|
<Box position="relative" flex="1" w="full">
|
|
117
|
-
<MainChart />
|
|
120
|
+
<MainChart screenshotMode={isScreenshotMode} />
|
|
118
121
|
</Box>
|
|
119
122
|
</Box>
|
|
120
123
|
</ClientOnly>
|