@tradejs/app 2.0.9 → 2.0.11
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.
|
|
3
|
+
"version": "2.0.11",
|
|
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.11",
|
|
55
|
+
"@tradejs/core": "^2.0.11",
|
|
56
|
+
"@tradejs/indicators": "^2.0.11",
|
|
57
|
+
"@tradejs/infra": "^2.0.11",
|
|
58
|
+
"@tradejs/node": "^2.0.11",
|
|
59
|
+
"@tradejs/types": "^2.0.11",
|
|
60
60
|
"@types/bcryptjs": "2.4.6",
|
|
61
61
|
"@types/lodash": "4.17.24",
|
|
62
62
|
"@types/node": "24.13.3",
|
|
@@ -29,10 +29,13 @@ import { getCurrentUserName } from '#app/lib/currentUser';
|
|
|
29
29
|
import {
|
|
30
30
|
assignLegacyRuntimeTradeAccountScopes,
|
|
31
31
|
buildRuntimeStrategyAnalytics,
|
|
32
|
+
buildRuntimeStrategyAiGateChanges,
|
|
32
33
|
buildRuntimeStrategyIdentityKey,
|
|
33
34
|
buildExchangeFallbackRuntimeTrades,
|
|
34
35
|
isRuntimeTradeRecord,
|
|
36
|
+
isRuntimeStrategyLineageScope,
|
|
35
37
|
resolveStrategyConfigIdentityByKey,
|
|
38
|
+
RuntimeStrategyLineageScope,
|
|
36
39
|
RuntimeStrategiesResponse,
|
|
37
40
|
selectTradesForWindow,
|
|
38
41
|
toRuntimeTradeView,
|
|
@@ -52,6 +55,7 @@ const MIN_HOURS = 6;
|
|
|
52
55
|
const MAX_HOURS = 24 * 90;
|
|
53
56
|
const BYBIT_MAX_TIME_RANGE_MS = 7 * 24 * 60 * 60 * 1000 - 1_000;
|
|
54
57
|
const EXCHANGE_REQUEST_TIMEOUT_MS = 15_000;
|
|
58
|
+
const RUNTIME_LINEAGE_BASELINE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
55
59
|
|
|
56
60
|
const coerceHours = (value: string | null) => {
|
|
57
61
|
const parsed = Number(value ?? Number.NaN);
|
|
@@ -162,6 +166,26 @@ const loadRuntimeTrades = async (
|
|
|
162
166
|
.sort((left, right) => left.entryTimestamp - right.entryTimestamp);
|
|
163
167
|
};
|
|
164
168
|
|
|
169
|
+
const loadRuntimeLineageScopes = async (
|
|
170
|
+
userName: string,
|
|
171
|
+
startTime: number,
|
|
172
|
+
endTime: number,
|
|
173
|
+
): Promise<RuntimeStrategyLineageScope[]> =>
|
|
174
|
+
(
|
|
175
|
+
await Promise.all(
|
|
176
|
+
getRuntimeStorageDayKeys(
|
|
177
|
+
Math.max(0, startTime - RUNTIME_LINEAGE_BASELINE_MS),
|
|
178
|
+
endTime,
|
|
179
|
+
).map((dayKey) =>
|
|
180
|
+
getHashJsonValues<RuntimeStrategyLineageScope>(
|
|
181
|
+
redisKeys.runtimeLineageScopeBucket(userName, dayKey),
|
|
182
|
+
),
|
|
183
|
+
),
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
.flat()
|
|
187
|
+
.filter(isRuntimeStrategyLineageScope);
|
|
188
|
+
|
|
165
189
|
const buildExchangeTimeRanges = (startTime: number, endTime: number) => {
|
|
166
190
|
const ranges: Array<{ startTime: number; endTime: number }> = [];
|
|
167
191
|
let cursor = startTime;
|
|
@@ -380,6 +404,7 @@ export const GET = async (request: NextRequest) => {
|
|
|
380
404
|
openPositionsSnapshot,
|
|
381
405
|
runtimeDeployments,
|
|
382
406
|
tradingAccounts,
|
|
407
|
+
runtimeLineageScopes,
|
|
383
408
|
] = await Promise.all([
|
|
384
409
|
loadRuntimeStrategyConfigs(userName),
|
|
385
410
|
loadConfiguredStrategyNames(),
|
|
@@ -400,6 +425,7 @@ export const GET = async (request: NextRequest) => {
|
|
|
400
425
|
loadOpenPositions(connector, exchangeErrors),
|
|
401
426
|
listRuntimeDeployments(userName),
|
|
402
427
|
listTradingAccounts(userName),
|
|
428
|
+
loadRuntimeLineageScopes(userName, startTime, endTime),
|
|
403
429
|
]);
|
|
404
430
|
const relevantTrades = selectTradesForWindow(
|
|
405
431
|
runtimeTrades,
|
|
@@ -607,6 +633,13 @@ export const GET = async (request: NextRequest) => {
|
|
|
607
633
|
stat: analytics.stat,
|
|
608
634
|
summary: analytics.summary,
|
|
609
635
|
orderLog: analytics.orderLog,
|
|
636
|
+
aiGateChanges: buildRuntimeStrategyAiGateChanges({
|
|
637
|
+
scopes: runtimeLineageScopes,
|
|
638
|
+
strategyName,
|
|
639
|
+
configId: identity.configId,
|
|
640
|
+
startTime,
|
|
641
|
+
endTime,
|
|
642
|
+
}),
|
|
610
643
|
recentTrades: strategyTrades
|
|
611
644
|
.slice(0, 8)
|
|
612
645
|
.map((trade) => toRuntimeTradeView(trade, endTime)),
|
|
@@ -2175,7 +2175,11 @@ export const RuntimeStrategyCard = ({
|
|
|
2175
2175
|
</Portal>
|
|
2176
2176
|
</Drawer.Root>
|
|
2177
2177
|
|
|
2178
|
-
<RuntimeStrategyChart
|
|
2178
|
+
<RuntimeStrategyChart
|
|
2179
|
+
orderLog={strategy.orderLog}
|
|
2180
|
+
stat={strategy.stat}
|
|
2181
|
+
aiGateChanges={strategy.aiGateChanges}
|
|
2182
|
+
/>
|
|
2179
2183
|
|
|
2180
2184
|
<SimpleGrid columns={{ base: 4, md: 8 }} p={4}>
|
|
2181
2185
|
<StatItem stat={strategy.stat} id="netProfit" title="P&L" />
|
|
@@ -16,22 +16,25 @@ import {
|
|
|
16
16
|
import { format } from 'date-fns';
|
|
17
17
|
import { getFormatted } from '@tradejs/core/backtest';
|
|
18
18
|
import type { SimpleOrderLogData, TestStat } from '@tradejs/types';
|
|
19
|
+
import type { RuntimeStrategyAiGateChange } from '#app/lib/runtimeStrategies';
|
|
19
20
|
|
|
20
21
|
interface RuntimeStrategyChartProps {
|
|
21
22
|
orderLog: SimpleOrderLogData;
|
|
22
23
|
stat: TestStat;
|
|
24
|
+
aiGateChanges: RuntimeStrategyAiGateChange[];
|
|
23
25
|
height?: string | number;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export const RuntimeStrategyChart = ({
|
|
27
29
|
orderLog,
|
|
28
30
|
stat,
|
|
31
|
+
aiGateChanges,
|
|
29
32
|
height = '350px',
|
|
30
33
|
}: RuntimeStrategyChartProps) => {
|
|
31
34
|
const chartData = useMemo(
|
|
32
35
|
() => ({
|
|
33
36
|
data: orderLog.map(([timestamp, amount]) => ({
|
|
34
|
-
timestamp
|
|
37
|
+
timestamp,
|
|
35
38
|
equity: amount,
|
|
36
39
|
})),
|
|
37
40
|
series: [
|
|
@@ -47,6 +50,7 @@ export const RuntimeStrategyChart = ({
|
|
|
47
50
|
const chart = useChart(chartData as any);
|
|
48
51
|
const { formatted: maxAmount } = getFormatted(stat, 'maxAmount');
|
|
49
52
|
const { formatted: minAmount } = getFormatted(stat, 'minAmount');
|
|
53
|
+
const gateChangeColor = chart.color('purple.400');
|
|
50
54
|
|
|
51
55
|
if (!orderLog.length) {
|
|
52
56
|
return (
|
|
@@ -96,12 +100,40 @@ export const RuntimeStrategyChart = ({
|
|
|
96
100
|
position: 'bottom',
|
|
97
101
|
}}
|
|
98
102
|
/>
|
|
99
|
-
|
|
103
|
+
{aiGateChanges.map((change) => (
|
|
104
|
+
<ReferenceLine
|
|
105
|
+
key={`${change.timestamp}:${change.fingerprint}`}
|
|
106
|
+
x={change.timestamp}
|
|
107
|
+
stroke={gateChangeColor}
|
|
108
|
+
strokeDasharray="3 5"
|
|
109
|
+
strokeWidth={1.5}
|
|
110
|
+
label={{
|
|
111
|
+
value: `AI-gate ${change.fingerprint.slice(0, 7)}`,
|
|
112
|
+
fill: gateChangeColor,
|
|
113
|
+
fontSize: 10,
|
|
114
|
+
offset: 6,
|
|
115
|
+
position: 'insideTopRight',
|
|
116
|
+
}}
|
|
117
|
+
/>
|
|
118
|
+
))}
|
|
119
|
+
<XAxis
|
|
120
|
+
dataKey="timestamp"
|
|
121
|
+
type="number"
|
|
122
|
+
scale="time"
|
|
123
|
+
domain={['dataMin', 'dataMax']}
|
|
124
|
+
tickFormatter={(timestamp) => format(timestamp, 'dd.MM')}
|
|
125
|
+
/>
|
|
100
126
|
<YAxis tickCount={10} domain={[stat.minAmount - 10, 'auto']} />
|
|
101
127
|
<Tooltip
|
|
102
128
|
animationDuration={100}
|
|
103
129
|
cursor={false}
|
|
104
|
-
content={
|
|
130
|
+
content={
|
|
131
|
+
<Chart.Tooltip
|
|
132
|
+
labelFormatter={(timestamp) =>
|
|
133
|
+
format(Number(timestamp), 'dd.MM.yyyy HH:mm')
|
|
134
|
+
}
|
|
135
|
+
/>
|
|
136
|
+
}
|
|
105
137
|
/>
|
|
106
138
|
{chart.series.map((item) => (
|
|
107
139
|
<Line
|
|
@@ -111,6 +143,7 @@ export const RuntimeStrategyChart = ({
|
|
|
111
143
|
stroke={chart.color(item.color)}
|
|
112
144
|
strokeWidth={2}
|
|
113
145
|
dot={false}
|
|
146
|
+
activeDot={{ r: 5, strokeWidth: 2 }}
|
|
114
147
|
/>
|
|
115
148
|
))}
|
|
116
149
|
</LineChart>
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
ExchangeEntryRecord,
|
|
10
10
|
PositionPnlSnapshot,
|
|
11
11
|
RuntimeTradeRecord,
|
|
12
|
+
RuntimeLineage,
|
|
12
13
|
SimpleOrderLogData,
|
|
13
14
|
StrategyConfig,
|
|
14
15
|
TestStat,
|
|
@@ -72,6 +73,21 @@ export interface RuntimeStrategyTradeView {
|
|
|
72
73
|
lastSyncedAt: number | null;
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
export interface RuntimeStrategyLineageScope {
|
|
77
|
+
strategy: string;
|
|
78
|
+
symbol: string;
|
|
79
|
+
runtimeConfigId?: string;
|
|
80
|
+
lineage: RuntimeLineage;
|
|
81
|
+
firstTimestamp: number;
|
|
82
|
+
lastTimestamp: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface RuntimeStrategyAiGateChange {
|
|
86
|
+
timestamp: number;
|
|
87
|
+
previousFingerprint: string;
|
|
88
|
+
fingerprint: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
75
91
|
export interface RuntimeStrategyView {
|
|
76
92
|
runtimeKey: string;
|
|
77
93
|
strategyName: string;
|
|
@@ -89,6 +105,7 @@ export interface RuntimeStrategyView {
|
|
|
89
105
|
stat: TestStat;
|
|
90
106
|
summary: RuntimeStrategyTradeSummary;
|
|
91
107
|
orderLog: SimpleOrderLogData;
|
|
108
|
+
aiGateChanges: RuntimeStrategyAiGateChange[];
|
|
92
109
|
recentTrades: RuntimeStrategyTradeView[];
|
|
93
110
|
orders: RuntimeStrategyTradeView[];
|
|
94
111
|
}
|
|
@@ -164,6 +181,104 @@ export interface RuntimeStrategiesResponse {
|
|
|
164
181
|
strategies: RuntimeStrategyView[];
|
|
165
182
|
}
|
|
166
183
|
|
|
184
|
+
export const isRuntimeStrategyLineageScope = (
|
|
185
|
+
value: unknown,
|
|
186
|
+
): value is RuntimeStrategyLineageScope => {
|
|
187
|
+
if (!value || typeof value !== 'object') {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const record = value as Record<string, unknown>;
|
|
192
|
+
const lineage = record.lineage as Record<string, unknown> | undefined;
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
typeof record.strategy === 'string' &&
|
|
196
|
+
typeof record.symbol === 'string' &&
|
|
197
|
+
typeof record.firstTimestamp === 'number' &&
|
|
198
|
+
Number.isFinite(record.firstTimestamp) &&
|
|
199
|
+
typeof record.lastTimestamp === 'number' &&
|
|
200
|
+
Number.isFinite(record.lastTimestamp) &&
|
|
201
|
+
lineage != null &&
|
|
202
|
+
typeof lineage.gateFingerprint === 'string' &&
|
|
203
|
+
lineage.gateFingerprint.trim().length > 0
|
|
204
|
+
);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const buildRuntimeStrategyAiGateChanges = ({
|
|
208
|
+
scopes,
|
|
209
|
+
strategyName,
|
|
210
|
+
configId,
|
|
211
|
+
startTime,
|
|
212
|
+
endTime,
|
|
213
|
+
}: {
|
|
214
|
+
scopes: RuntimeStrategyLineageScope[];
|
|
215
|
+
strategyName: string;
|
|
216
|
+
configId?: string;
|
|
217
|
+
startTime: number;
|
|
218
|
+
endTime: number;
|
|
219
|
+
}): RuntimeStrategyAiGateChange[] => {
|
|
220
|
+
const normalizedConfigId = configId ?? 'config';
|
|
221
|
+
const observationsByTimestamp = new Map<
|
|
222
|
+
number,
|
|
223
|
+
{ fingerprint: string; lastTimestamp: number }
|
|
224
|
+
>();
|
|
225
|
+
|
|
226
|
+
for (const scope of scopes) {
|
|
227
|
+
if (
|
|
228
|
+
scope.strategy !== strategyName ||
|
|
229
|
+
(scope.runtimeConfigId ?? 'config') !== normalizedConfigId ||
|
|
230
|
+
scope.firstTimestamp > endTime
|
|
231
|
+
) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const fingerprint = scope.lineage.gateFingerprint.trim();
|
|
236
|
+
const existing = observationsByTimestamp.get(scope.firstTimestamp);
|
|
237
|
+
|
|
238
|
+
// Multiple symbols are evaluated for the same strategy timestamp. If a
|
|
239
|
+
// deploy happens during that cycle, prefer the lineage that kept running
|
|
240
|
+
// afterwards instead of making the marker order depend on the symbol name.
|
|
241
|
+
if (
|
|
242
|
+
!existing ||
|
|
243
|
+
scope.lastTimestamp > existing.lastTimestamp ||
|
|
244
|
+
(scope.lastTimestamp === existing.lastTimestamp &&
|
|
245
|
+
fingerprint > existing.fingerprint)
|
|
246
|
+
) {
|
|
247
|
+
observationsByTimestamp.set(scope.firstTimestamp, {
|
|
248
|
+
fingerprint,
|
|
249
|
+
lastTimestamp: scope.lastTimestamp,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const observations = [...observationsByTimestamp.entries()].sort(
|
|
255
|
+
([leftTimestamp], [rightTimestamp]) => leftTimestamp - rightTimestamp,
|
|
256
|
+
);
|
|
257
|
+
const changes: RuntimeStrategyAiGateChange[] = [];
|
|
258
|
+
let currentFingerprint: string | null = null;
|
|
259
|
+
|
|
260
|
+
for (const [timestamp, observation] of observations) {
|
|
261
|
+
if (currentFingerprint == null) {
|
|
262
|
+
currentFingerprint = observation.fingerprint;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (observation.fingerprint === currentFingerprint) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (timestamp >= startTime) {
|
|
270
|
+
changes.push({
|
|
271
|
+
timestamp,
|
|
272
|
+
previousFingerprint: currentFingerprint,
|
|
273
|
+
fingerprint: observation.fingerprint,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
currentFingerprint = observation.fingerprint;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return changes;
|
|
280
|
+
};
|
|
281
|
+
|
|
167
282
|
const roundValue = (value: number, digits = 2) => {
|
|
168
283
|
if (!Number.isFinite(value)) {
|
|
169
284
|
return 0;
|