@tradejs/node 3.0.0 → 3.1.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.
- package/README.md +2 -0
- package/dist/ai.d.mts +26 -4
- package/dist/ai.d.ts +26 -4
- package/dist/ai.js +161 -165
- package/dist/ai.mjs +3 -1
- package/dist/backtest.js +496 -188
- package/dist/backtest.mjs +3 -3
- package/dist/chunk-3TWULKHV.mjs +595 -0
- package/dist/chunk-FB5NUEOQ.mjs +295 -0
- package/dist/{chunk-VRXU4E4O.mjs → chunk-KOQSLFT2.mjs} +169 -169
- package/dist/chunk-LAJ7NA3Q.mjs +377 -0
- package/dist/{chunk-OGAWBO3Z.mjs → chunk-MKLTJQLY.mjs} +1 -1
- package/dist/cli.js +150 -161
- package/dist/cli.mjs +1 -1
- package/dist/connectors.d.mts +5 -2
- package/dist/connectors.d.ts +5 -2
- package/dist/connectors.js +329 -8
- package/dist/connectors.mjs +5 -1
- package/dist/registry.d.mts +2 -1
- package/dist/registry.d.ts +2 -1
- package/dist/registry.js +157 -161
- package/dist/registry.mjs +4 -2
- package/dist/runtimeDashboard.d.mts +12 -0
- package/dist/runtimeDashboard.d.ts +12 -0
- package/dist/runtimeDashboard.js +8433 -0
- package/dist/runtimeDashboard.mjs +955 -0
- package/dist/runtimeTrades.d.mts +31 -0
- package/dist/runtimeTrades.d.ts +31 -0
- package/dist/runtimeTrades.js +321 -0
- package/dist/runtimeTrades.mjs +11 -0
- package/dist/strategies.d.mts +2 -2
- package/dist/strategies.d.ts +2 -2
- package/dist/strategies.js +168 -165
- package/dist/strategies.mjs +13 -372
- package/package.json +18 -5
- package/dist/chunk-V3YMKE4I.mjs +0 -271
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// src/runtimeTrades.ts
|
|
2
|
+
import { resolveStrategyNameByOrderLinkId } from "@tradejs/core/runtimeTrades";
|
|
3
|
+
|
|
4
|
+
// src/runtimeTradeReconciliation.ts
|
|
5
|
+
var toNonEmptyString = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
|
|
6
|
+
var removeFromExactMaps = (exactByOrderLinkId, exactByOrderId, row) => {
|
|
7
|
+
for (const [key, value] of exactByOrderLinkId) {
|
|
8
|
+
if (value === row) exactByOrderLinkId.delete(key);
|
|
9
|
+
}
|
|
10
|
+
for (const [key, value] of exactByOrderId) {
|
|
11
|
+
if (value === row) exactByOrderId.delete(key);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
var removeFromSymbolBuckets = (buckets, row) => {
|
|
15
|
+
const rows = buckets.get(row.symbol);
|
|
16
|
+
const index = rows?.findIndex((candidate) => candidate === row) ?? -1;
|
|
17
|
+
if (index >= 0) rows?.splice(index, 1);
|
|
18
|
+
};
|
|
19
|
+
var takeExactClosedPnlMatch = ({
|
|
20
|
+
exactByOrderLinkId,
|
|
21
|
+
exactByOrderId,
|
|
22
|
+
symbolBuckets,
|
|
23
|
+
orderLinkId,
|
|
24
|
+
orderId
|
|
25
|
+
}) => {
|
|
26
|
+
const keys = [
|
|
27
|
+
[exactByOrderLinkId, orderLinkId],
|
|
28
|
+
[exactByOrderId, orderId]
|
|
29
|
+
];
|
|
30
|
+
for (const [bucket, key] of keys) {
|
|
31
|
+
const normalizedKey = toNonEmptyString(key);
|
|
32
|
+
if (!normalizedKey) continue;
|
|
33
|
+
const match = bucket.get(normalizedKey);
|
|
34
|
+
if (!match) continue;
|
|
35
|
+
removeFromExactMaps(exactByOrderLinkId, exactByOrderId, match);
|
|
36
|
+
removeFromSymbolBuckets(symbolBuckets, match);
|
|
37
|
+
return match;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
};
|
|
41
|
+
var takeClosedPnlMatch = ({
|
|
42
|
+
exactByOrderLinkId,
|
|
43
|
+
exactByOrderId = /* @__PURE__ */ new Map(),
|
|
44
|
+
symbolBuckets,
|
|
45
|
+
trade
|
|
46
|
+
}) => {
|
|
47
|
+
const exactMatch = takeExactClosedPnlMatch({
|
|
48
|
+
exactByOrderLinkId,
|
|
49
|
+
exactByOrderId,
|
|
50
|
+
symbolBuckets,
|
|
51
|
+
orderLinkId: trade.orderId,
|
|
52
|
+
orderId: trade.orderId
|
|
53
|
+
});
|
|
54
|
+
if (exactMatch) return exactMatch;
|
|
55
|
+
const rows = symbolBuckets.get(trade.symbol);
|
|
56
|
+
if (!rows?.length) return null;
|
|
57
|
+
const minimumClosedAt = trade.entryTimestamp - 5 * 6e4;
|
|
58
|
+
const matchIndex = rows.reduce((bestIndex, row2, index) => {
|
|
59
|
+
if (!Number.isFinite(row2.closedAt) || row2.closedAt < minimumClosedAt || row2.direction && row2.direction !== trade.direction) {
|
|
60
|
+
return bestIndex;
|
|
61
|
+
}
|
|
62
|
+
if (bestIndex < 0) return index;
|
|
63
|
+
return row2.closedAt < rows[bestIndex].closedAt ? index : bestIndex;
|
|
64
|
+
}, -1);
|
|
65
|
+
if (matchIndex < 0) return null;
|
|
66
|
+
const [row] = rows.splice(matchIndex, 1);
|
|
67
|
+
if (row) removeFromExactMaps(exactByOrderLinkId, exactByOrderId, row);
|
|
68
|
+
return row ?? null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// src/runtimeTrades.ts
|
|
72
|
+
var toNonEmptyString2 = (value) => typeof value === "string" && value.trim() ? value.trim() : null;
|
|
73
|
+
var roundValue = (value, digits = 2) => {
|
|
74
|
+
if (!Number.isFinite(value)) return 0;
|
|
75
|
+
const factor = 10 ** digits;
|
|
76
|
+
return Math.round(value * factor) / factor;
|
|
77
|
+
};
|
|
78
|
+
var removeExactMatches = (exactByOrderLinkId, exactByOrderId, row) => {
|
|
79
|
+
if (row.orderLinkId) exactByOrderLinkId.delete(row.orderLinkId);
|
|
80
|
+
if (row.orderId) exactByOrderId.delete(row.orderId);
|
|
81
|
+
};
|
|
82
|
+
var takeClosedPnlMatchForEntry = ({
|
|
83
|
+
exactByOrderLinkId,
|
|
84
|
+
exactByOrderId,
|
|
85
|
+
symbolBuckets,
|
|
86
|
+
entry
|
|
87
|
+
}) => {
|
|
88
|
+
const exactMatch = takeExactClosedPnlMatch({
|
|
89
|
+
exactByOrderLinkId,
|
|
90
|
+
exactByOrderId,
|
|
91
|
+
symbolBuckets,
|
|
92
|
+
orderLinkId: entry.orderLinkId,
|
|
93
|
+
orderId: entry.orderId
|
|
94
|
+
});
|
|
95
|
+
if (exactMatch) return exactMatch;
|
|
96
|
+
const rows = symbolBuckets.get(entry.symbol);
|
|
97
|
+
if (!rows?.length) return null;
|
|
98
|
+
const minimumClosedAt = entry.entryTimestamp - 5 * 6e4;
|
|
99
|
+
const matchIndex = rows.reduce((bestIndex, row, index) => {
|
|
100
|
+
if (!Number.isFinite(row.closedAt) || row.closedAt < minimumClosedAt || row.direction && row.direction !== entry.direction) {
|
|
101
|
+
return bestIndex;
|
|
102
|
+
}
|
|
103
|
+
if (bestIndex < 0) return index;
|
|
104
|
+
return row.closedAt < rows[bestIndex].closedAt ? index : bestIndex;
|
|
105
|
+
}, -1);
|
|
106
|
+
if (matchIndex < 0) return null;
|
|
107
|
+
const [match] = rows.splice(matchIndex, 1);
|
|
108
|
+
if (match) removeExactMatches(exactByOrderLinkId, exactByOrderId, match);
|
|
109
|
+
return match ?? null;
|
|
110
|
+
};
|
|
111
|
+
var aggregateExchangeEntriesByOrder = (entryRows) => {
|
|
112
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
113
|
+
entryRows.forEach((entry, index) => {
|
|
114
|
+
const orderLinkId = toNonEmptyString2(entry.orderLinkId);
|
|
115
|
+
const orderId = toNonEmptyString2(entry.orderId);
|
|
116
|
+
const key = orderLinkId || orderId || `${entry.symbol}:${entry.direction}:${entry.entryTimestamp}:${index}`;
|
|
117
|
+
const existing = grouped.get(key);
|
|
118
|
+
const hasPrice = Number.isFinite(entry.qty) && typeof entry.entryPrice === "number" && Number.isFinite(entry.entryPrice);
|
|
119
|
+
if (!existing) {
|
|
120
|
+
grouped.set(key, {
|
|
121
|
+
...entry,
|
|
122
|
+
qty: Number.isFinite(entry.qty) ? entry.qty : 0,
|
|
123
|
+
pricingQty: hasPrice ? entry.qty : 0,
|
|
124
|
+
pricingNotional: hasPrice ? entry.qty * (entry.entryPrice ?? 0) : 0
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
existing.qty += Number.isFinite(entry.qty) ? entry.qty : 0;
|
|
129
|
+
existing.entryTimestamp = Math.min(
|
|
130
|
+
existing.entryTimestamp,
|
|
131
|
+
entry.entryTimestamp
|
|
132
|
+
);
|
|
133
|
+
if (hasPrice) {
|
|
134
|
+
existing.pricingQty += entry.qty;
|
|
135
|
+
existing.pricingNotional += entry.qty * (entry.entryPrice ?? 0);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
return [...grouped.values()].map(({ pricingQty, pricingNotional, ...entry }) => ({
|
|
139
|
+
...entry,
|
|
140
|
+
qty: roundValue(entry.qty, 8),
|
|
141
|
+
entryPrice: pricingQty > 0 ? roundValue(pricingNotional / pricingQty, 8) : null
|
|
142
|
+
})).sort((left, right) => left.entryTimestamp - right.entryTimestamp);
|
|
143
|
+
};
|
|
144
|
+
var resolveStrategy = ({
|
|
145
|
+
orderLinkId,
|
|
146
|
+
orderId,
|
|
147
|
+
strategyNameByOrderId,
|
|
148
|
+
strategyNames
|
|
149
|
+
}) => (orderLinkId ? strategyNameByOrderId.get(orderLinkId) : null) ?? (orderId ? strategyNameByOrderId.get(orderId) : null) ?? resolveStrategyNameByOrderLinkId({ orderLinkId, strategyNames });
|
|
150
|
+
var buildRiskLevels = (position) => {
|
|
151
|
+
const takeProfitPrice = position?.takeProfitPrice;
|
|
152
|
+
const stopLossPrice = position?.stopLossPrice;
|
|
153
|
+
if ((typeof takeProfitPrice !== "number" || !Number.isFinite(takeProfitPrice)) && (typeof stopLossPrice !== "number" || !Number.isFinite(stopLossPrice))) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
...typeof takeProfitPrice === "number" && Number.isFinite(takeProfitPrice) ? { takeProfitPrice } : {},
|
|
158
|
+
...typeof stopLossPrice === "number" && Number.isFinite(stopLossPrice) ? { stopLossPrice } : {}
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
var buildExchangeFallbackRuntimeTrades = ({
|
|
162
|
+
entryRows,
|
|
163
|
+
closedPnlRows,
|
|
164
|
+
openPositions,
|
|
165
|
+
strategyNames,
|
|
166
|
+
existingTrades,
|
|
167
|
+
endTime
|
|
168
|
+
}) => {
|
|
169
|
+
if (!entryRows.length && !closedPnlRows.length) return [];
|
|
170
|
+
const strategyNameByOrderId = new Map(
|
|
171
|
+
existingTrades.filter(
|
|
172
|
+
(trade) => Boolean(trade.orderId?.trim() && trade.strategy?.trim())
|
|
173
|
+
).map((trade) => [trade.orderId, trade.strategy])
|
|
174
|
+
);
|
|
175
|
+
const strategyNamesPool = [
|
|
176
|
+
.../* @__PURE__ */ new Set([
|
|
177
|
+
...strategyNames,
|
|
178
|
+
...existingTrades.map(({ strategy }) => strategy)
|
|
179
|
+
])
|
|
180
|
+
];
|
|
181
|
+
const openPositionBySymbol = new Map(
|
|
182
|
+
openPositions.map((position) => [position.symbol, position])
|
|
183
|
+
);
|
|
184
|
+
const existingOrderIds = new Set(
|
|
185
|
+
existingTrades.map(({ orderId }) => toNonEmptyString2(orderId)).filter((value) => value != null)
|
|
186
|
+
);
|
|
187
|
+
const exactByOrderLinkId = new Map(
|
|
188
|
+
closedPnlRows.filter((row) => Boolean(row.orderLinkId)).map((row) => [row.orderLinkId, row])
|
|
189
|
+
);
|
|
190
|
+
const exactByOrderId = new Map(
|
|
191
|
+
closedPnlRows.filter((row) => Boolean(row.orderId)).map((row) => [row.orderId, row])
|
|
192
|
+
);
|
|
193
|
+
const symbolBuckets = /* @__PURE__ */ new Map();
|
|
194
|
+
for (const row of closedPnlRows) {
|
|
195
|
+
const bucket = symbolBuckets.get(row.symbol) ?? [];
|
|
196
|
+
bucket.push(row);
|
|
197
|
+
symbolBuckets.set(row.symbol, bucket);
|
|
198
|
+
}
|
|
199
|
+
const fallbackTrades = aggregateExchangeEntriesByOrder(entryRows).map((entry) => {
|
|
200
|
+
const orderLinkId = toNonEmptyString2(entry.orderLinkId);
|
|
201
|
+
const orderId = toNonEmptyString2(entry.orderId);
|
|
202
|
+
const runtimeOrderId = orderLinkId ?? orderId;
|
|
203
|
+
if (!runtimeOrderId || existingOrderIds.has(runtimeOrderId)) return null;
|
|
204
|
+
const strategy = resolveStrategy({
|
|
205
|
+
orderLinkId,
|
|
206
|
+
orderId,
|
|
207
|
+
strategyNameByOrderId,
|
|
208
|
+
strategyNames: strategyNamesPool
|
|
209
|
+
});
|
|
210
|
+
if (!strategy) return null;
|
|
211
|
+
const closed = takeClosedPnlMatchForEntry({
|
|
212
|
+
exactByOrderLinkId,
|
|
213
|
+
exactByOrderId,
|
|
214
|
+
symbolBuckets,
|
|
215
|
+
entry
|
|
216
|
+
});
|
|
217
|
+
const position = openPositionBySymbol.get(entry.symbol);
|
|
218
|
+
const isActive = !closed && position?.direction === entry.direction && Number.isFinite(position.currentPrice) && Number.isFinite(position.unrealizedPnl);
|
|
219
|
+
const entryPrice = typeof entry.entryPrice === "number" && Number.isFinite(entry.entryPrice) ? entry.entryPrice : typeof closed?.entryPrice === "number" && Number.isFinite(closed.entryPrice) ? closed.entryPrice : null;
|
|
220
|
+
if (entryPrice == null) return null;
|
|
221
|
+
return {
|
|
222
|
+
orderId: runtimeOrderId,
|
|
223
|
+
strategy,
|
|
224
|
+
symbol: entry.symbol,
|
|
225
|
+
direction: entry.direction,
|
|
226
|
+
qty: entry.qty,
|
|
227
|
+
entryPrice,
|
|
228
|
+
actualEntryPrice: closed?.entryPrice ?? entry.entryPrice ?? null,
|
|
229
|
+
entryTimestamp: entry.entryTimestamp,
|
|
230
|
+
status: isActive ? "active" : "closed",
|
|
231
|
+
currentPrice: isActive ? position?.currentPrice ?? null : closed?.exitPrice ?? null,
|
|
232
|
+
currentPnl: isActive ? position?.unrealizedPnl ?? null : closed?.closedPnl ?? null,
|
|
233
|
+
closedPnl: isActive ? null : closed?.closedPnl ?? null,
|
|
234
|
+
exitPrice: isActive ? null : closed?.exitPrice ?? null,
|
|
235
|
+
actualExitPrice: isActive ? null : closed?.exitPrice ?? null,
|
|
236
|
+
exitTimestamp: isActive ? null : closed?.closedAt ?? null,
|
|
237
|
+
aiAnalysis: isActive ? buildRiskLevels(position) : null,
|
|
238
|
+
openFee: closed?.openFee ?? entry.openFee ?? null,
|
|
239
|
+
closeFee: closed?.closeFee ?? entry.closeFee ?? null,
|
|
240
|
+
fundingFee: closed?.fundingFee ?? entry.fundingFee ?? null,
|
|
241
|
+
totalFee: closed?.totalFee ?? entry.totalFee ?? null,
|
|
242
|
+
lastSyncedAt: endTime
|
|
243
|
+
};
|
|
244
|
+
}).filter((trade) => trade != null);
|
|
245
|
+
const usedOrderIds = /* @__PURE__ */ new Set([
|
|
246
|
+
...existingOrderIds,
|
|
247
|
+
...fallbackTrades.map(({ orderId }) => orderId)
|
|
248
|
+
]);
|
|
249
|
+
const remainingClosedTrades = [...symbolBuckets.values()].flat().map((row) => {
|
|
250
|
+
const orderLinkId = toNonEmptyString2(row.orderLinkId);
|
|
251
|
+
const orderId = toNonEmptyString2(row.orderId);
|
|
252
|
+
const runtimeOrderId = orderLinkId ?? orderId;
|
|
253
|
+
if (!runtimeOrderId || usedOrderIds.has(runtimeOrderId)) return null;
|
|
254
|
+
const strategy = resolveStrategy({
|
|
255
|
+
orderLinkId,
|
|
256
|
+
orderId,
|
|
257
|
+
strategyNameByOrderId,
|
|
258
|
+
strategyNames: strategyNamesPool
|
|
259
|
+
});
|
|
260
|
+
if (!strategy || row.entryPrice == null || !Number.isFinite(row.entryPrice) || !row.direction) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
orderId: runtimeOrderId,
|
|
265
|
+
strategy,
|
|
266
|
+
symbol: row.symbol,
|
|
267
|
+
direction: row.direction,
|
|
268
|
+
qty: row.qty,
|
|
269
|
+
entryPrice: row.entryPrice,
|
|
270
|
+
actualEntryPrice: row.entryPrice,
|
|
271
|
+
entryTimestamp: typeof row.entryTimestamp === "number" && Number.isFinite(row.entryTimestamp) ? row.entryTimestamp : row.closedAt,
|
|
272
|
+
status: "closed",
|
|
273
|
+
currentPrice: row.exitPrice,
|
|
274
|
+
currentPnl: row.closedPnl,
|
|
275
|
+
closedPnl: row.closedPnl,
|
|
276
|
+
exitPrice: row.exitPrice,
|
|
277
|
+
actualExitPrice: row.exitPrice,
|
|
278
|
+
exitTimestamp: row.closedAt,
|
|
279
|
+
openFee: row.openFee ?? null,
|
|
280
|
+
closeFee: row.closeFee ?? null,
|
|
281
|
+
fundingFee: row.fundingFee ?? null,
|
|
282
|
+
totalFee: row.totalFee ?? null,
|
|
283
|
+
lastSyncedAt: endTime
|
|
284
|
+
};
|
|
285
|
+
}).filter((trade) => trade != null);
|
|
286
|
+
return [...fallbackTrades, ...remainingClosedTrades].sort(
|
|
287
|
+
(left, right) => left.entryTimestamp - right.entryTimestamp
|
|
288
|
+
);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
export {
|
|
292
|
+
takeExactClosedPnlMatch,
|
|
293
|
+
takeClosedPnlMatch,
|
|
294
|
+
buildExchangeFallbackRuntimeTrades
|
|
295
|
+
};
|
|
@@ -8,15 +8,9 @@ import {
|
|
|
8
8
|
// src/ai.ts
|
|
9
9
|
import {
|
|
10
10
|
DEFAULT_AI_RESPONSE_LANGUAGE,
|
|
11
|
-
getAiResponseLanguagePromptName
|
|
12
|
-
normalizeAiResponseLanguage
|
|
11
|
+
getAiResponseLanguagePromptName
|
|
13
12
|
} from "@tradejs/core/aiLanguages";
|
|
14
|
-
import { normalizeAiEndpoint } from "@tradejs/core/aiEndpoints";
|
|
15
|
-
import { normalizeAiModel } from "@tradejs/core/aiModels";
|
|
16
13
|
import { setData, redisKeys } from "@tradejs/infra/redis";
|
|
17
|
-
import {
|
|
18
|
-
getUserSettings
|
|
19
|
-
} from "@tradejs/infra/userSettings";
|
|
20
14
|
|
|
21
15
|
// src/aiShared.ts
|
|
22
16
|
var MAX_AI_SERIES_POINTS = 5;
|
|
@@ -792,6 +786,11 @@ var getStrategyCreator = async (name, cwd = getTradejsProjectCwd()) => {
|
|
|
792
786
|
const { state } = getStrategyRegistryState(cwd);
|
|
793
787
|
return state.strategyCreators.get(name);
|
|
794
788
|
};
|
|
789
|
+
var getStrategyDefaults = async (name, cwd = getTradejsProjectCwd()) => {
|
|
790
|
+
await ensureStrategyPluginsLoaded(cwd);
|
|
791
|
+
const { state } = getStrategyRegistryState(cwd);
|
|
792
|
+
return state.strategyEntriesMap.get(name)?.defaults;
|
|
793
|
+
};
|
|
795
794
|
var getAvailableStrategyNames = async (cwd = getTradejsProjectCwd()) => {
|
|
796
795
|
await ensureStrategyPluginsLoaded(cwd);
|
|
797
796
|
const { state } = getStrategyRegistryState(cwd);
|
|
@@ -974,7 +973,151 @@ var postProcessLocalAiAnalysisByStrategy = (signal, analysis, payload = buildAiP
|
|
|
974
973
|
}) ?? strategyAnalysis;
|
|
975
974
|
};
|
|
976
975
|
|
|
976
|
+
// src/aiProvider.ts
|
|
977
|
+
import { normalizeAiEndpoint } from "@tradejs/core/aiEndpoints";
|
|
978
|
+
import { normalizeAiModel } from "@tradejs/core/aiModels";
|
|
979
|
+
import { normalizeAiResponseLanguage } from "@tradejs/core/aiLanguages";
|
|
980
|
+
import {
|
|
981
|
+
getUserSettings
|
|
982
|
+
} from "@tradejs/infra/userSettings";
|
|
983
|
+
var DEFAULT_AI_MODEL = "openai/gpt-5-mini";
|
|
984
|
+
var userSettingsCache = /* @__PURE__ */ new Map();
|
|
985
|
+
var aiModelCache = /* @__PURE__ */ new Map();
|
|
986
|
+
var normalizeResponseContent = (content) => {
|
|
987
|
+
if (typeof content === "string") return content;
|
|
988
|
+
if (content && typeof content === "object" && !Array.isArray(content)) {
|
|
989
|
+
return content;
|
|
990
|
+
}
|
|
991
|
+
if (Array.isArray(content)) {
|
|
992
|
+
return content.map(
|
|
993
|
+
(part) => typeof part?.text === "string" ? part.text : ""
|
|
994
|
+
).join("\n").trim();
|
|
995
|
+
}
|
|
996
|
+
return String(content ?? "");
|
|
997
|
+
};
|
|
998
|
+
var getAiInvocationError = (error) => {
|
|
999
|
+
const details = error instanceof Error && error.message.trim() ? error.message.trim() : String(error);
|
|
1000
|
+
const isEmptyCompletion = error instanceof TypeError && /Cannot read properties of undefined \(reading ['"]message['"]\)/.test(
|
|
1001
|
+
details
|
|
1002
|
+
);
|
|
1003
|
+
const wrapped = new Error(
|
|
1004
|
+
isEmptyCompletion ? "AI provider returned an empty chat completion" : `AI model invocation failed: ${details}`
|
|
1005
|
+
);
|
|
1006
|
+
wrapped.cause = error;
|
|
1007
|
+
return wrapped;
|
|
1008
|
+
};
|
|
1009
|
+
var isEmptyResponseContent = (content) => typeof content === "string" ? content.trim().length === 0 : Object.keys(content).length === 0;
|
|
1010
|
+
var getAiModelCacheKey = (userName, modelName, temperature) => `${userName}::${modelName}::${temperature}`;
|
|
1011
|
+
var resolveAiModelName = (settings, requestedModelName) => {
|
|
1012
|
+
const explicitModelName = requestedModelName?.trim() ?? "";
|
|
1013
|
+
if (explicitModelName) return explicitModelName;
|
|
1014
|
+
return settings.AI_MODEL?.trim() || DEFAULT_AI_MODEL;
|
|
1015
|
+
};
|
|
1016
|
+
var getOpenRouterModelKwargs = (apiEndpoint) => {
|
|
1017
|
+
const endpoint = String(apiEndpoint ?? "").trim();
|
|
1018
|
+
if (!endpoint) return {};
|
|
1019
|
+
let hostname = "";
|
|
1020
|
+
try {
|
|
1021
|
+
hostname = new URL(endpoint).hostname;
|
|
1022
|
+
} catch {
|
|
1023
|
+
hostname = endpoint;
|
|
1024
|
+
}
|
|
1025
|
+
return hostname.toLowerCase().includes("openrouter") ? { provider: { ignore: ["azure"] } } : {};
|
|
1026
|
+
};
|
|
1027
|
+
var getAiUserSettings = async (userName = "root") => {
|
|
1028
|
+
let settingsPromise = userSettingsCache.get(userName);
|
|
1029
|
+
if (!settingsPromise) {
|
|
1030
|
+
settingsPromise = getUserSettings(userName).then((settings2) => {
|
|
1031
|
+
const endpoint = normalizeAiEndpoint(settings2.AI_API_ENDPOINT);
|
|
1032
|
+
return {
|
|
1033
|
+
...settings2,
|
|
1034
|
+
AI_API_ENDPOINT: endpoint,
|
|
1035
|
+
AI_MODEL: normalizeAiModel(settings2.AI_MODEL, endpoint),
|
|
1036
|
+
AI_RESPONSE_LANGUAGE: normalizeAiResponseLanguage(
|
|
1037
|
+
settings2.AI_RESPONSE_LANGUAGE
|
|
1038
|
+
)
|
|
1039
|
+
};
|
|
1040
|
+
});
|
|
1041
|
+
settingsPromise.catch(() => userSettingsCache.delete(userName));
|
|
1042
|
+
userSettingsCache.set(userName, settingsPromise);
|
|
1043
|
+
}
|
|
1044
|
+
const settings = await settingsPromise;
|
|
1045
|
+
if (!settings.AI_API_KEY || !settings.AI_API_ENDPOINT) {
|
|
1046
|
+
throw new Error(`AI settings are incomplete for user ${userName}`);
|
|
1047
|
+
}
|
|
1048
|
+
return settings;
|
|
1049
|
+
};
|
|
1050
|
+
var getAiModel = async (userName = "root", requestedModelName, temperature = 0.2) => {
|
|
1051
|
+
const settings = await getAiUserSettings(userName);
|
|
1052
|
+
const modelName = resolveAiModelName(settings, requestedModelName);
|
|
1053
|
+
const cacheKey = getAiModelCacheKey(userName, modelName, temperature);
|
|
1054
|
+
let modelPromise = aiModelCache.get(cacheKey);
|
|
1055
|
+
if (!modelPromise) {
|
|
1056
|
+
modelPromise = import("@langchain/openai").then(({ ChatOpenAI }) => {
|
|
1057
|
+
const modelKwargs = getOpenRouterModelKwargs(settings.AI_API_ENDPOINT);
|
|
1058
|
+
return new ChatOpenAI({
|
|
1059
|
+
temperature,
|
|
1060
|
+
modelName,
|
|
1061
|
+
apiKey: settings.AI_API_KEY,
|
|
1062
|
+
...Object.keys(modelKwargs).length ? { modelKwargs } : {},
|
|
1063
|
+
configuration: {
|
|
1064
|
+
baseURL: settings.AI_API_ENDPOINT,
|
|
1065
|
+
defaultHeaders: {
|
|
1066
|
+
"HTTP-Referer": "https://tradejs.dev",
|
|
1067
|
+
"X-Title": "Inv"
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
});
|
|
1071
|
+
});
|
|
1072
|
+
modelPromise.catch(() => aiModelCache.delete(cacheKey));
|
|
1073
|
+
aiModelCache.set(cacheKey, modelPromise);
|
|
1074
|
+
}
|
|
1075
|
+
try {
|
|
1076
|
+
return await modelPromise;
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
aiModelCache.delete(cacheKey);
|
|
1079
|
+
userSettingsCache.delete(userName);
|
|
1080
|
+
throw error;
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
var resetAiRuntimeCache = () => {
|
|
1084
|
+
aiModelCache.clear();
|
|
1085
|
+
userSettingsCache.clear();
|
|
1086
|
+
};
|
|
1087
|
+
var invokeAiChatWithUserMessageEncoding = async ({
|
|
1088
|
+
messages,
|
|
1089
|
+
userName = "root",
|
|
1090
|
+
model,
|
|
1091
|
+
temperature = 0.2
|
|
1092
|
+
}, resolveUserMessageEncoding) => {
|
|
1093
|
+
const [{ HumanMessage, SystemMessage }, aiModel] = await Promise.all([
|
|
1094
|
+
import("@langchain/core/messages"),
|
|
1095
|
+
getAiModel(userName, model, temperature)
|
|
1096
|
+
]);
|
|
1097
|
+
const providerMessages = messages.map(
|
|
1098
|
+
(message) => message.role === "system" ? new SystemMessage(message.content) : new HumanMessage(
|
|
1099
|
+
resolveUserMessageEncoding(message) === "text-block" ? { content: [{ type: "text", text: message.content }] } : message.content
|
|
1100
|
+
)
|
|
1101
|
+
);
|
|
1102
|
+
try {
|
|
1103
|
+
const response = await aiModel.invoke(providerMessages);
|
|
1104
|
+
const content = normalizeResponseContent(response?.content);
|
|
1105
|
+
if (isEmptyResponseContent(content)) {
|
|
1106
|
+
throw new Error("AI provider returned an empty chat completion");
|
|
1107
|
+
}
|
|
1108
|
+
return { content };
|
|
1109
|
+
} catch (error) {
|
|
1110
|
+
throw getAiInvocationError(error);
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
var invokeAiPromptChat = (options) => invokeAiChatWithUserMessageEncoding(options, () => "text-block");
|
|
1114
|
+
var invokeCompatibleAiChat = (options) => invokeAiChatWithUserMessageEncoding(
|
|
1115
|
+
options,
|
|
1116
|
+
(message) => message.format ?? "plain"
|
|
1117
|
+
);
|
|
1118
|
+
|
|
977
1119
|
// src/ai.ts
|
|
1120
|
+
var invokeAiChat = (options) => invokeCompatibleAiChat(options);
|
|
978
1121
|
var parseAIResponse = (input) => {
|
|
979
1122
|
try {
|
|
980
1123
|
if (typeof input === "object" && input !== null) return input;
|
|
@@ -987,18 +1130,6 @@ var parseAIResponse = (input) => {
|
|
|
987
1130
|
return {};
|
|
988
1131
|
}
|
|
989
1132
|
};
|
|
990
|
-
var normalizeResponseContent = (content) => {
|
|
991
|
-
if (typeof content === "string" || content && typeof content === "object") {
|
|
992
|
-
if (typeof content !== "object" || !Array.isArray(content)) {
|
|
993
|
-
return content;
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
if (Array.isArray(content)) {
|
|
997
|
-
const text = content.map((part) => typeof part?.text === "string" ? part.text : "").join("\n").trim();
|
|
998
|
-
return text;
|
|
999
|
-
}
|
|
1000
|
-
return String(content ?? "");
|
|
1001
|
-
};
|
|
1002
1133
|
var normalizeAnalysis = (raw) => {
|
|
1003
1134
|
const direction = raw?.direction === "LONG" || raw?.direction === "SHORT" ? raw.direction : null;
|
|
1004
1135
|
const qualityNum = typeof raw?.quality === "number" ? Math.max(1, Math.min(5, Math.round(raw.quality))) : void 0;
|
|
@@ -1232,120 +1363,6 @@ Trade payload:
|
|
|
1232
1363
|
${JSON.stringify(payload)}
|
|
1233
1364
|
${buildAiHumanPromptAddonByStrategy(signal, payload)}
|
|
1234
1365
|
`;
|
|
1235
|
-
var getAiInvocationError = (error) => {
|
|
1236
|
-
const details = error instanceof Error && error.message.trim() ? error.message.trim() : String(error);
|
|
1237
|
-
const isEmptyCompletion = error instanceof TypeError && /Cannot read properties of undefined \(reading ['"]message['"]\)/.test(
|
|
1238
|
-
details
|
|
1239
|
-
);
|
|
1240
|
-
const wrapped = new Error(
|
|
1241
|
-
isEmptyCompletion ? "AI provider returned an empty chat completion" : `AI model invocation failed: ${details}`
|
|
1242
|
-
);
|
|
1243
|
-
wrapped.cause = error;
|
|
1244
|
-
return wrapped;
|
|
1245
|
-
};
|
|
1246
|
-
var isEmptyResponseContent = (content) => typeof content === "string" ? content.trim().length === 0 : Object.keys(content).length === 0;
|
|
1247
|
-
var DEFAULT_AI_MODEL = "openai/gpt-5-mini";
|
|
1248
|
-
var userSettingsCache = /* @__PURE__ */ new Map();
|
|
1249
|
-
var aiModelCache = /* @__PURE__ */ new Map();
|
|
1250
|
-
var getAiModelCacheKey = (userName, modelName) => `${userName}::${modelName}`;
|
|
1251
|
-
var resolveAiModelName = (settings, requestedModelName) => {
|
|
1252
|
-
const explicitModelName = typeof requestedModelName === "string" ? requestedModelName.trim() : "";
|
|
1253
|
-
if (explicitModelName) {
|
|
1254
|
-
return explicitModelName;
|
|
1255
|
-
}
|
|
1256
|
-
const settingsModelName = typeof settings.AI_MODEL === "string" ? settings.AI_MODEL.trim() : "";
|
|
1257
|
-
return settingsModelName || DEFAULT_AI_MODEL;
|
|
1258
|
-
};
|
|
1259
|
-
var getOpenRouterModelKwargs = (apiEndpoint) => {
|
|
1260
|
-
const endpoint = String(apiEndpoint ?? "").trim();
|
|
1261
|
-
if (!endpoint) {
|
|
1262
|
-
return {};
|
|
1263
|
-
}
|
|
1264
|
-
let hostname = "";
|
|
1265
|
-
try {
|
|
1266
|
-
hostname = new URL(endpoint).hostname;
|
|
1267
|
-
} catch {
|
|
1268
|
-
hostname = endpoint;
|
|
1269
|
-
}
|
|
1270
|
-
if (!hostname.toLowerCase().includes("openrouter")) {
|
|
1271
|
-
return {};
|
|
1272
|
-
}
|
|
1273
|
-
return {
|
|
1274
|
-
provider: {
|
|
1275
|
-
ignore: ["azure"]
|
|
1276
|
-
}
|
|
1277
|
-
};
|
|
1278
|
-
};
|
|
1279
|
-
var getAiSettings = async (userName = "root") => {
|
|
1280
|
-
let settingsPromise = userSettingsCache.get(userName);
|
|
1281
|
-
if (!settingsPromise) {
|
|
1282
|
-
settingsPromise = getUserSettings(userName).then((settings2) => {
|
|
1283
|
-
const endpoint = normalizeAiEndpoint(settings2.AI_API_ENDPOINT);
|
|
1284
|
-
return {
|
|
1285
|
-
...settings2,
|
|
1286
|
-
AI_API_ENDPOINT: endpoint,
|
|
1287
|
-
AI_MODEL: normalizeAiModel(settings2.AI_MODEL, endpoint),
|
|
1288
|
-
AI_RESPONSE_LANGUAGE: normalizeAiResponseLanguage(
|
|
1289
|
-
settings2.AI_RESPONSE_LANGUAGE
|
|
1290
|
-
)
|
|
1291
|
-
};
|
|
1292
|
-
});
|
|
1293
|
-
settingsPromise.catch(() => {
|
|
1294
|
-
userSettingsCache.delete(userName);
|
|
1295
|
-
});
|
|
1296
|
-
userSettingsCache.set(userName, settingsPromise);
|
|
1297
|
-
}
|
|
1298
|
-
const settings = await settingsPromise;
|
|
1299
|
-
if (!settings.AI_API_KEY || !settings.AI_API_ENDPOINT) {
|
|
1300
|
-
throw new Error(`AI settings are incomplete for user ${userName}`);
|
|
1301
|
-
}
|
|
1302
|
-
return settings;
|
|
1303
|
-
};
|
|
1304
|
-
var createAiModel = async (userName = "root", requestedModelName) => {
|
|
1305
|
-
const settings = await getAiSettings(userName);
|
|
1306
|
-
const modelName = resolveAiModelName(settings, requestedModelName);
|
|
1307
|
-
const cacheKey = getAiModelCacheKey(userName, modelName);
|
|
1308
|
-
let modelPromise = aiModelCache.get(cacheKey);
|
|
1309
|
-
if (!modelPromise) {
|
|
1310
|
-
modelPromise = (async () => {
|
|
1311
|
-
const { ChatOpenAI } = await import("@langchain/openai");
|
|
1312
|
-
const modelKwargs = getOpenRouterModelKwargs(settings.AI_API_ENDPOINT);
|
|
1313
|
-
return new ChatOpenAI({
|
|
1314
|
-
temperature: 0.2,
|
|
1315
|
-
modelName,
|
|
1316
|
-
apiKey: settings.AI_API_KEY,
|
|
1317
|
-
...Object.keys(modelKwargs).length ? { modelKwargs } : {},
|
|
1318
|
-
configuration: {
|
|
1319
|
-
baseURL: settings.AI_API_ENDPOINT,
|
|
1320
|
-
defaultHeaders: {
|
|
1321
|
-
"HTTP-Referer": "https://tradejs.dev",
|
|
1322
|
-
"X-Title": "Inv"
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
});
|
|
1326
|
-
})();
|
|
1327
|
-
modelPromise.catch(() => {
|
|
1328
|
-
aiModelCache.delete(cacheKey);
|
|
1329
|
-
});
|
|
1330
|
-
aiModelCache.set(cacheKey, modelPromise);
|
|
1331
|
-
}
|
|
1332
|
-
return modelPromise;
|
|
1333
|
-
};
|
|
1334
|
-
var getAiModel = async (userName = "root", requestedModelName) => {
|
|
1335
|
-
const settings = await getAiSettings(userName);
|
|
1336
|
-
const resolvedModelName = resolveAiModelName(settings, requestedModelName);
|
|
1337
|
-
try {
|
|
1338
|
-
return await createAiModel(userName, resolvedModelName);
|
|
1339
|
-
} catch (error) {
|
|
1340
|
-
aiModelCache.delete(getAiModelCacheKey(userName, resolvedModelName));
|
|
1341
|
-
userSettingsCache.delete(userName);
|
|
1342
|
-
throw error;
|
|
1343
|
-
}
|
|
1344
|
-
};
|
|
1345
|
-
var resetAiRuntimeCache = () => {
|
|
1346
|
-
aiModelCache.clear();
|
|
1347
|
-
userSettingsCache.clear();
|
|
1348
|
-
};
|
|
1349
1366
|
var buildAiPrompts = (signal) => {
|
|
1350
1367
|
const payload = buildAiPayload(signal);
|
|
1351
1368
|
return {
|
|
@@ -1354,42 +1371,23 @@ var buildAiPrompts = (signal) => {
|
|
|
1354
1371
|
};
|
|
1355
1372
|
};
|
|
1356
1373
|
var runAiPrompt = async ({ systemPrompt, humanPrompt }, options = {}) => {
|
|
1357
|
-
const
|
|
1358
|
-
import("@langchain/core/messages"),
|
|
1359
|
-
getAiModel(options.userName, options.model),
|
|
1360
|
-
getAiSettings(options.userName)
|
|
1361
|
-
]);
|
|
1362
|
-
const messages = [];
|
|
1374
|
+
const settings = await getAiUserSettings(options.userName);
|
|
1363
1375
|
const responseLanguage = getAiResponseLanguagePromptName(
|
|
1364
1376
|
settings.AI_RESPONSE_LANGUAGE || DEFAULT_AI_RESPONSE_LANGUAGE
|
|
1365
1377
|
);
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
]
|
|
1380
|
-
})
|
|
1381
|
-
);
|
|
1382
|
-
let response;
|
|
1383
|
-
try {
|
|
1384
|
-
response = await model.invoke(messages);
|
|
1385
|
-
} catch (error) {
|
|
1386
|
-
throw getAiInvocationError(error);
|
|
1387
|
-
}
|
|
1388
|
-
const responseContent = normalizeResponseContent(response?.content);
|
|
1389
|
-
if (isEmptyResponseContent(responseContent)) {
|
|
1390
|
-
throw new Error("AI provider returned an empty chat completion");
|
|
1391
|
-
}
|
|
1392
|
-
const parsed = parseAIResponse(responseContent);
|
|
1378
|
+
const response = await invokeAiPromptChat({
|
|
1379
|
+
userName: options.userName,
|
|
1380
|
+
model: options.model,
|
|
1381
|
+
messages: [
|
|
1382
|
+
{ role: "system", content: systemPrompt },
|
|
1383
|
+
{
|
|
1384
|
+
role: "system",
|
|
1385
|
+
content: `Write all user-visible text fields in ${responseLanguage}. Keep field names and JSON syntax unchanged.`
|
|
1386
|
+
},
|
|
1387
|
+
{ role: "user", content: humanPrompt }
|
|
1388
|
+
]
|
|
1389
|
+
});
|
|
1390
|
+
const parsed = parseAIResponse(response.content);
|
|
1393
1391
|
const normalized = normalizeAnalysis(parsed);
|
|
1394
1392
|
if (!options.signal) {
|
|
1395
1393
|
return normalized;
|
|
@@ -1445,6 +1443,7 @@ export {
|
|
|
1445
1443
|
ensureStrategyPluginsLoaded,
|
|
1446
1444
|
ensureIndicatorPluginsLoaded,
|
|
1447
1445
|
getStrategyCreator,
|
|
1446
|
+
getStrategyDefaults,
|
|
1448
1447
|
getAvailableStrategyNames,
|
|
1449
1448
|
getRegisteredStrategies,
|
|
1450
1449
|
getRegisteredManifests,
|
|
@@ -1455,13 +1454,14 @@ export {
|
|
|
1455
1454
|
strategies,
|
|
1456
1455
|
resolveStrategyPolicyProfile,
|
|
1457
1456
|
getStrategyProfileMlAdapter,
|
|
1457
|
+
DEFAULT_AI_MODEL,
|
|
1458
|
+
getOpenRouterModelKwargs,
|
|
1459
|
+
resetAiRuntimeCache,
|
|
1460
|
+
invokeAiChat,
|
|
1458
1461
|
buildAiSystemPrompt,
|
|
1459
1462
|
buildAiPayload,
|
|
1460
1463
|
getDeterministicAiGateContext,
|
|
1461
1464
|
buildAiHumanPrompt,
|
|
1462
|
-
DEFAULT_AI_MODEL,
|
|
1463
|
-
getOpenRouterModelKwargs,
|
|
1464
|
-
resetAiRuntimeCache,
|
|
1465
1465
|
buildAiPrompts,
|
|
1466
1466
|
runAiPrompt,
|
|
1467
1467
|
runAiPromptLocal,
|